Class: Torikago::CLI
- Inherits:
-
Object
- Object
- Torikago::CLI
- Defined in:
- lib/torikago/cli.rb,
sig/torikago.rbs
Overview
Small command dispatcher used by the gem executable. Commands intentionally delegate to service objects so the CLI stays thin and easy to test.
Constant Summary collapse
- HELP_TEXT =
<<~TEXT.freeze usage: torikago COMMAND [ARGS] commands: init interactively generate package_api.yml files and config/initializers/torikago.rb check validate Gateway invocations against package_api.yml update-package-api [BOX] regenerate package_api.yml entries from the configured public API entrypoint help, --help, -h show this help TEXT
Instance Attribute Summary collapse
-
#stderr ⇒ Object
readonly
Returns the value of attribute stderr.
-
#stdin ⇒ Object
readonly
Returns the value of attribute stdin.
-
#stdout ⇒ Object
readonly
Returns the value of attribute stdout.
Instance Method Summary collapse
- #ask(prompt, default: nil) ⇒ Object
- #discover_configuration ⇒ Configuration
- #discover_module_directories(modules_root) ⇒ Array[Pathname]
-
#initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr) ⇒ CLI
constructor
A new instance of CLI.
- #load_yaml_file(path) ⇒ Hash[String, untyped]
- #render_initializer(configuration) ⇒ String
- #render_package_api_manifest(manifest) ⇒ String
- #run(argv) ⇒ Integer
- #run_check ⇒ Integer
- #run_init ⇒ Integer
- #run_update_package_api(module_name) ⇒ Integer
- #yes?(input) ⇒ Boolean
Constructor Details
#initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr) ⇒ CLI
Returns a new instance of CLI.
23 24 25 26 27 |
# File 'lib/torikago/cli.rb', line 23 def initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr) @stdin = stdin @stdout = stdout @stderr = stderr end |
Instance Attribute Details
#stderr ⇒ Object (readonly)
Returns the value of attribute stderr.
51 52 53 |
# File 'lib/torikago/cli.rb', line 51 def stderr @stderr end |
#stdin ⇒ Object (readonly)
Returns the value of attribute stdin.
51 52 53 |
# File 'lib/torikago/cli.rb', line 51 def stdin @stdin end |
#stdout ⇒ Object (readonly)
Returns the value of attribute stdout.
51 52 53 |
# File 'lib/torikago/cli.rb', line 51 def stdout @stdout end |
Instance Method Details
#ask(prompt, default:) ⇒ String #ask(prompt, default:) ⇒ String?
145 146 147 148 149 150 151 152 153 154 |
# File 'lib/torikago/cli.rb', line 145 def ask(prompt, default: nil) stdout.print("#{prompt}") stdout.print(" [#{default}]") if default stdout.print(": ") input = stdin.gets&.strip return default if input.nil? || input.empty? input end |
#discover_configuration ⇒ Configuration
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/torikago/cli.rb', line 128 def discover_configuration if File.exist?("config/environment.rb") # In Rails apps, prefer the application's configured module registry # over filesystem guessing. require File.("config/environment") return Torikago.configuration end # Outside Rails, use the conventional modules/* layout as a lightweight # fallback so check/update-package-api can run in early prototypes. Configuration.new.tap do |configuration| Dir["modules/*"].sort.each do |module_root| configuration.register(File.basename(module_root).to_sym, root: module_root) end end end |
#discover_module_directories(modules_root) ⇒ Array[Pathname]
156 157 158 |
# File 'lib/torikago/cli.rb', line 156 def discover_module_directories(modules_root) Dir[modules_root.join("*").to_s].map { |path| Pathname(path) }.select(&:directory?).sort end |
#load_yaml_file(path) ⇒ Hash[String, untyped]
160 161 162 |
# File 'lib/torikago/cli.rb', line 160 def load_yaml_file(path) YAML.safe_load(path.read, permitted_classes: Array.new, aliases: false) || Hash.new end |
#render_initializer(configuration) ⇒ String
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/torikago/cli.rb', line 164 def render_initializer(configuration) lines = [ "# This file registers torikago runtime boundaries for your Rails app.", "#", "# Each config.register call defines one module root. Calls between modules", "# should go through Torikago::Gateway.invoke(\"Module::ExportedApi\", :call) instead", "# of reaching across module constants directly.", "#", "# Options:", "#", "# root:", "# Filesystem root for the module.", "#", "# entrypoint:", "# Directory containing exported Package API classes. The conventional", "# default is app/package_api.", "#", "# rails_engine:", "# Set true when the module owns a Rails::Engine entrypoint at", "# lib/<module_name>.rb. That entrypoint is then left for Rails and", "# skipped by the module Box runtime.", "#", "# gemfile:", "# Optional module-local Gemfile. When Ruby::Box isolation is enabled,", "# torikago prepends that module's resolved gem require paths inside the", "# module Box.", "#", "# setup:", "# Optional setup file loaded before Package API files. Use this for", "# explicit module boot behavior such as carefully scoped monkey patches.", "#", "# Package API permissions live in each module's package_api.yml under exports.", "# After changing Package API files, run:", "#", "# bin/torikago update-package-api", "#", "# To validate Gateway invocations against package_api.yml, run:", "#", "# bin/torikago check", "", "Torikago.configure do |config|" ] configuration.each_definition do |definition| lines << " config.register(" lines << " :#{definition.name}," lines << " root: Rails.root.join(\"#{definition.root.to_s}\")," option_lines = Array.new option_lines << " entrypoint: #{definition.entrypoint.inspect}" if definition.entrypoint option_lines << " rails_engine: #{definition.rails_engine.inspect}" if definition.rails_engine option_lines << " setup: #{definition.setup.inspect}" if definition.setup option_lines << " gemfile: #{definition.gemfile.inspect}" if definition.gemfile option_lines.each_with_index do |option_line, index| comma = index == option_lines.length - 1 ? "" : "," lines << "#{option_line}#{comma}" end lines << " )" lines << "" end lines << "end" lines.join("\n") end |
#render_package_api_manifest(manifest) ⇒ String
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/torikago/cli.rb', line 229 def render_package_api_manifest(manifest) <<~YAML # This file declares the Package APIs exported by this module. # # Each key under exports is a class that may be called through: # # Torikago::Gateway.invoke("ModuleName::SomeQuery", :call) # # methods lists the public instance methods exposed through Gateway. # allowed_callers lists other modules that may call that export. The # host app and the module itself are allowed implicitly. # YAML .then { |header| header + YAML.dump(manifest) } end |
#run(argv) ⇒ Integer
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/torikago/cli.rb', line 29 def run(argv) command = argv.shift case command when nil, "help", "--help", "-h" stdout.print(HELP_TEXT) 0 when "init" run_init when "check" run_check when "update-package-api" run_update_package_api(argv.shift) else stderr.puts("unknown command: #{command}") stderr.print(HELP_TEXT) 1 end end |
#run_check ⇒ Integer
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/torikago/cli.rb', line 96 def run_check result = Checker.new( configuration: discover_configuration, source_roots: [Pathname("app"), Pathname("modules")] ).call if result.ok? stdout.puts( "ok: scanned #{result.scanned_file_count} Ruby files, " \ "found #{result.gateway_call_count} static Gateway invocations, " \ "validated #{result.manifest_count} package_api manifests" ) 0 else result.errors.each { |error| stderr.puts(error) } stderr.puts( "failed: scanned #{result.scanned_file_count} Ruby files, " \ "found #{result.gateway_call_count} static Gateway invocations, " \ "validated #{result.manifest_count} package_api manifests, " \ "#{result.errors.size} errors" ) 1 end end |
#run_init ⇒ Integer
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/torikago/cli.rb', line 53 def run_init modules_root_input = ask("Modules directory", default: "modules") modules_root = Pathname(modules_root_input) module_directories = discover_module_directories(modules_root) if module_directories.empty? stderr.puts("no modules found under #{modules_root}") return 1 end configuration = Configuration.new module_directories.each do |module_root| module_name = module_root.basename.to_s entrypoint = ask("Public API directory for #{module_name}", default: "app/package_api") configuration.register( module_name.to_sym, root: module_root.to_s, entrypoint: entrypoint, gemfile: module_root.join("Gemfile").exist? ? "Gemfile" : nil ) manifest_path = module_root.join("package_api.yml") manifest = manifest_path.exist? ? load_yaml_file(manifest_path) : { "exports" => Hash.new } manifest_path.write(render_package_api_manifest(manifest)) stdout.puts("generated #{manifest_path}") end initializer_path = Pathname("config/initializers/torikago.rb") FileUtils.mkdir_p(initializer_path.dirname) initializer_path.write(render_initializer(configuration)) stdout.puts("generated #{initializer_path}") if yes?(ask("Run `torikago update-package-api` now?", default: "Y")) updates = PackageApiUpdater.new(configuration: configuration).call updates.each_value { |path| stdout.puts("updated #{path}") } stdout.puts("updated #{updates.size} package_api manifest#{'s' unless updates.size == 1}") end 0 end |
#run_update_package_api(module_name) ⇒ Integer
121 122 123 124 125 126 |
# File 'lib/torikago/cli.rb', line 121 def run_update_package_api(module_name) updates = PackageApiUpdater.new(configuration: discover_configuration).call(module_name) updates.each_value { |path| stdout.puts("updated #{path}") } stdout.puts("updated #{updates.size} package_api manifest#{'s' unless updates.size == 1}") 0 end |
#yes?(input) ⇒ Boolean
245 246 247 |
# File 'lib/torikago/cli.rb', line 245 def yes?(input) input.to_s.strip.empty? || %w[Y y yes YES].include?(input) end |