Class: Torikago::CLI

Inherits:
Object
  • Object
show all
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 =

Returns:

  • (String)
<<~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

Instance Method Summary collapse

Constructor Details

#initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr) ⇒ CLI

Returns a new instance of CLI.

Parameters:

  • stdin: (Object) (defaults to: $stdin)
  • stdout: (Object) (defaults to: $stdout)
  • stderr: (Object) (defaults to: $stderr)


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

#stderrObject (readonly)

Returns the value of attribute stderr.

Returns:

  • (Object)


51
52
53
# File 'lib/torikago/cli.rb', line 51

def stderr
  @stderr
end

#stdinObject (readonly)

Returns the value of attribute stdin.

Returns:

  • (Object)


51
52
53
# File 'lib/torikago/cli.rb', line 51

def stdin
  @stdin
end

#stdoutObject (readonly)

Returns the value of attribute stdout.

Returns:

  • (Object)


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?

Overloads:

  • #ask(prompt, default:) ⇒ String

    Parameters:

    • prompt (String)
    • default: (String)

    Returns:

    • (String)
  • #ask(prompt, default:) ⇒ String?

    Parameters:

    • prompt (String)
    • default: (String, nil)

    Returns:

    • (String, nil)


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_configurationConfiguration

Returns:



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.expand_path("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]

Parameters:

  • modules_root (Pathname)

Returns:

  • (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]

Parameters:

  • path (Pathname)

Returns:

  • (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

Parameters:

Returns:

  • (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

Parameters:

  • manifest (Hash[String, untyped])

Returns:

  • (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

Parameters:

  • argv (Array[String])

Returns:

  • (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_checkInteger

Returns:

  • (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_initInteger

Returns:

  • (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

Parameters:

  • module_name (String, nil)

Returns:

  • (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

Parameters:

  • input (String, nil)

Returns:

  • (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