torikago 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.ja.md +102 -15
- data/README.md +106 -15
- data/lib/torikago/checker.rb +279 -30
- data/lib/torikago/cli.rb +24 -10
- data/lib/torikago/configuration.rb +4 -3
- data/lib/torikago/controller_endpoint.rb +30 -0
- data/lib/torikago/engine_container.rb +485 -58
- data/lib/torikago/gateway.rb +75 -14
- data/lib/torikago/invocation.rb +26 -0
- data/lib/torikago/package_api_updater.rb +32 -11
- data/lib/torikago/rack_endpoint.rb +23 -0
- data/lib/torikago/registry.rb +4 -2
- data/lib/torikago/root_constant_resolver.rb +69 -0
- data/lib/torikago/version.rb +1 -1
- data/lib/torikago.rb +8 -0
- data/sig/torikago.rbs +440 -0
- data/test/test_helper.rb +0 -1
- data/test/torikago/checker.rb +119 -93
- data/test/torikago/cli.rb +19 -4
- data/test/torikago/configuration.rb +2 -0
- data/test/torikago/controller_endpoint.rb +30 -0
- data/test/torikago/engine_container.rb +824 -30
- data/test/torikago/gateway.rb +138 -92
- data/test/torikago/package_api_updater.rb +6 -3
- data/test/torikago/rack_endpoint.rb +25 -0
- data/test/torikago/registry.rb +15 -0
- data/test/torikago/root_constant_resolver.rb +114 -0
- metadata +9 -1
data/lib/torikago/checker.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "pathname"
|
|
2
|
+
require "ripper"
|
|
2
3
|
require "yaml"
|
|
3
4
|
|
|
4
5
|
module Torikago
|
|
@@ -6,37 +7,145 @@ module Torikago
|
|
|
6
7
|
# manifests. This is intentionally conservative; runtime enforcement still
|
|
7
8
|
# lives in Gateway.
|
|
8
9
|
class Checker
|
|
9
|
-
Result
|
|
10
|
-
:errors,
|
|
11
|
-
|
|
12
|
-
:gateway_call_count,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
class Result
|
|
11
|
+
attr_accessor :dynamic_gateway_call_count, :errors, :scanned_file_count, :gateway_call_count, :manifest_count
|
|
12
|
+
|
|
13
|
+
def initialize(errors: Array.new, scanned_file_count: 0, gateway_call_count: 0, dynamic_gateway_call_count: 0, manifest_count: 0)
|
|
14
|
+
@errors = errors
|
|
15
|
+
@scanned_file_count = scanned_file_count
|
|
16
|
+
@gateway_call_count = gateway_call_count
|
|
17
|
+
@dynamic_gateway_call_count = dynamic_gateway_call_count
|
|
18
|
+
@manifest_count = manifest_count
|
|
19
|
+
end
|
|
20
|
+
|
|
16
21
|
def ok?
|
|
17
22
|
errors.empty?
|
|
18
23
|
end
|
|
19
24
|
end
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
GatewayCall = Struct.new(:class_name, :method_name, keyword_init: true)
|
|
27
|
+
|
|
28
|
+
class GatewayCallExtractor
|
|
29
|
+
attr_reader :calls, :dynamic_call_count
|
|
30
|
+
|
|
31
|
+
def initialize(source)
|
|
32
|
+
@sexp = Ripper.sexp(source)
|
|
33
|
+
@calls = Array.new
|
|
34
|
+
@dynamic_call_count = 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def call
|
|
38
|
+
walk(sexp)
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
attr_reader :sexp
|
|
45
|
+
|
|
46
|
+
def walk(node)
|
|
47
|
+
return unless node.is_a?(Array)
|
|
48
|
+
|
|
49
|
+
extract(node) if node.first == :method_add_arg
|
|
50
|
+
node.each { |child| walk(child) if child.is_a?(Array) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def extract(node)
|
|
54
|
+
call_node = node[1]
|
|
55
|
+
return unless call_node&.first == :call
|
|
56
|
+
return unless token_value(call_node[3]) == "invoke"
|
|
57
|
+
|
|
58
|
+
receiver = call_node[1]
|
|
59
|
+
arguments = arguments_from(node[2])
|
|
60
|
+
|
|
61
|
+
if gateway_constant?(receiver)
|
|
62
|
+
record(string_literal(arguments[0]), symbol_literal(arguments[1]))
|
|
63
|
+
elsif build_call?(receiver)
|
|
64
|
+
build_arguments = arguments_from(receiver[2])
|
|
65
|
+
record(string_literal(build_arguments[0]), symbol_literal(arguments[0]))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def record(class_name, method_name)
|
|
70
|
+
if class_name && method_name
|
|
71
|
+
calls << GatewayCall.new(class_name: class_name, method_name: method_name)
|
|
72
|
+
else
|
|
73
|
+
@dynamic_call_count += 1
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def build_call?(node)
|
|
78
|
+
return false unless node&.first == :method_add_arg
|
|
79
|
+
|
|
80
|
+
call_node = node[1]
|
|
81
|
+
call_node&.first == :call &&
|
|
82
|
+
token_value(call_node[3]) == "build" &&
|
|
83
|
+
gateway_constant?(call_node[1])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def gateway_constant?(node)
|
|
87
|
+
constant_name(node) == "Torikago::Gateway"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def constant_name(node)
|
|
91
|
+
return unless node.is_a?(Array)
|
|
92
|
+
|
|
93
|
+
case node.first
|
|
94
|
+
when :var_ref, :const_ref, :top_const_ref
|
|
95
|
+
token_value(node[1])
|
|
96
|
+
when :const_path_ref
|
|
97
|
+
[constant_name(node[1]), token_value(node[2])].compact.join("::")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def arguments_from(node)
|
|
102
|
+
return Array.new unless node&.first == :arg_paren
|
|
103
|
+
|
|
104
|
+
args_add_block = node[1]
|
|
105
|
+
return Array.new unless args_add_block&.first == :args_add_block
|
|
106
|
+
|
|
107
|
+
args_add_block[1] || Array.new
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def string_literal(node)
|
|
111
|
+
return unless node&.first == :string_literal
|
|
112
|
+
|
|
113
|
+
content = node[1]
|
|
114
|
+
parts = content&.first == :string_content ? content.drop(1) : Array.new
|
|
115
|
+
return unless parts.length == 1 && parts.first&.first == :@tstring_content
|
|
116
|
+
|
|
117
|
+
token_value(parts.first)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def symbol_literal(node)
|
|
121
|
+
return unless node&.first == :symbol_literal
|
|
122
|
+
|
|
123
|
+
symbol = node[1]
|
|
124
|
+
token = symbol&.first == :symbol ? symbol[1] : nil
|
|
125
|
+
token_value(token)&.to_sym
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def token_value(token)
|
|
129
|
+
token[1] if token.is_a?(Array) && token.first.to_s.start_with?("@")
|
|
130
|
+
end
|
|
131
|
+
end
|
|
26
132
|
|
|
27
133
|
def initialize(configuration:, source_roots:)
|
|
28
134
|
@configuration = configuration
|
|
29
135
|
@source_roots = Array(source_roots).map { |root| Pathname(root) }
|
|
30
|
-
@manifests =
|
|
136
|
+
@manifests = Hash.new
|
|
31
137
|
end
|
|
32
138
|
|
|
33
139
|
def call
|
|
34
|
-
errors =
|
|
140
|
+
errors = Array.new
|
|
35
141
|
gateway_call_count = 0
|
|
142
|
+
dynamic_gateway_call_count = 0
|
|
36
143
|
scanned_files = source_files
|
|
37
144
|
|
|
38
145
|
scanned_files.each do |path|
|
|
39
|
-
|
|
146
|
+
static_count, dynamic_count = scan_gateway_calls(path, errors)
|
|
147
|
+
gateway_call_count += static_count
|
|
148
|
+
dynamic_gateway_call_count += dynamic_count
|
|
40
149
|
end
|
|
41
150
|
|
|
42
151
|
manifest_count = 0
|
|
@@ -49,6 +158,7 @@ module Torikago
|
|
|
49
158
|
errors: errors,
|
|
50
159
|
scanned_file_count: scanned_files.size,
|
|
51
160
|
gateway_call_count: gateway_call_count,
|
|
161
|
+
dynamic_gateway_call_count: dynamic_gateway_call_count,
|
|
52
162
|
manifest_count: manifest_count
|
|
53
163
|
)
|
|
54
164
|
end
|
|
@@ -62,11 +172,11 @@ module Torikago
|
|
|
62
172
|
end
|
|
63
173
|
|
|
64
174
|
def scan_gateway_calls(path, errors)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
175
|
+
extraction = GatewayCallExtractor.new(File.read(path)).call
|
|
176
|
+
extraction.calls.each do |gateway_call|
|
|
177
|
+
class_name = gateway_call.class_name
|
|
178
|
+
method_name = gateway_call.method_name
|
|
179
|
+
|
|
70
180
|
# Public API names are expected to start with their owning module
|
|
71
181
|
# namespace, e.g. Foo::ListProductsQuery targets the :foo box.
|
|
72
182
|
target_box = infer_box_name(class_name)
|
|
@@ -74,30 +184,54 @@ module Torikago
|
|
|
74
184
|
caller_box = infer_caller_box_from_path(path)
|
|
75
185
|
|
|
76
186
|
if manifest_entry.nil?
|
|
77
|
-
errors << "#{path}: #{class_name} is not declared in #{target_box}/package_api.yml exports"
|
|
187
|
+
errors << "#{path}: #{class_name}##{method_name} is not declared in #{target_box}/package_api.yml exports"
|
|
188
|
+
next
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
methods = exported_methods(manifest_entry)
|
|
192
|
+
if !methods.empty? && !methods.include?(method_name.to_s)
|
|
193
|
+
errors << "#{path}: #{class_name}##{method_name} is not exported by #{target_box}/package_api.yml"
|
|
78
194
|
next
|
|
79
195
|
end
|
|
80
196
|
|
|
81
197
|
next if caller_box.nil?
|
|
82
198
|
next if caller_box == target_box
|
|
83
199
|
|
|
84
|
-
allowed_callers =
|
|
200
|
+
allowed_callers = allowed_callers(manifest_entry).map { |caller| caller.to_s }
|
|
85
201
|
next if allowed_callers.include?(caller_box.to_s)
|
|
86
202
|
|
|
87
|
-
errors << "#{path}: #{caller_box} is not allowed to call #{class_name}"
|
|
203
|
+
errors << "#{path}: #{caller_box} is not allowed to call #{class_name}##{method_name}"
|
|
88
204
|
end
|
|
89
205
|
|
|
90
|
-
|
|
206
|
+
[extraction.calls.size, extraction.dynamic_call_count]
|
|
91
207
|
end
|
|
92
208
|
|
|
93
209
|
def validate_manifest_entries(definition, errors)
|
|
94
|
-
|
|
210
|
+
manifest_path = definition.root.join("package_api.yml")
|
|
211
|
+
exported_package_apis(load_manifest(definition)).each do |class_name, manifest_entry|
|
|
212
|
+
methods = exported_methods(manifest_entry)
|
|
213
|
+
if methods.empty?
|
|
214
|
+
errors << "#{manifest_path}: #{class_name} must declare a non-empty methods array"
|
|
215
|
+
end
|
|
216
|
+
|
|
95
217
|
# The manifest is the contract, but the checker also catches stale
|
|
96
218
|
# entries whose implementation file has been deleted or moved.
|
|
97
219
|
expected_path = expected_public_api_path(definition, class_name)
|
|
98
|
-
|
|
220
|
+
unless expected_path.exist?
|
|
221
|
+
errors << "#{manifest_path}: #{class_name} does not have a matching file at #{expected_path}"
|
|
222
|
+
next
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
next if methods.empty?
|
|
226
|
+
|
|
227
|
+
implemented_methods, dynamic_definition = public_instance_methods(expected_path, class_name)
|
|
228
|
+
next if dynamic_definition
|
|
229
|
+
|
|
230
|
+
methods.each do |method_name|
|
|
231
|
+
next if implemented_methods.include?(method_name)
|
|
99
232
|
|
|
100
|
-
|
|
233
|
+
errors << "#{expected_path}: #{class_name}##{method_name} is exported but no public instance method definition was found"
|
|
234
|
+
end
|
|
101
235
|
end
|
|
102
236
|
end
|
|
103
237
|
|
|
@@ -112,9 +246,9 @@ module Torikago
|
|
|
112
246
|
manifest_path = definition.root.join("package_api.yml")
|
|
113
247
|
manifests[definition.name] =
|
|
114
248
|
if manifest_path.exist?
|
|
115
|
-
YAML.safe_load(manifest_path.read, permitted_classes:
|
|
249
|
+
YAML.safe_load(manifest_path.read, permitted_classes: Array.new, aliases: false) || Hash.new
|
|
116
250
|
else
|
|
117
|
-
|
|
251
|
+
Hash.new
|
|
118
252
|
end
|
|
119
253
|
end
|
|
120
254
|
end
|
|
@@ -135,7 +269,122 @@ module Torikago
|
|
|
135
269
|
end
|
|
136
270
|
|
|
137
271
|
def exported_package_apis(manifest)
|
|
138
|
-
manifest.fetch("exports") { manifest.fetch("public_api",
|
|
272
|
+
manifest.fetch("exports") { manifest.fetch("public_api", Hash.new) }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def allowed_callers(manifest_entry)
|
|
276
|
+
return Array.new unless manifest_entry.is_a?(Hash)
|
|
277
|
+
|
|
278
|
+
allowed = manifest_entry["allowed_callers"]
|
|
279
|
+
return allowed if allowed.is_a?(Array)
|
|
280
|
+
|
|
281
|
+
Array.new
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def exported_methods(manifest_entry)
|
|
285
|
+
return Array.new unless manifest_entry.is_a?(Hash)
|
|
286
|
+
|
|
287
|
+
methods = manifest_entry["methods"]
|
|
288
|
+
return methods.map(&:to_s) if methods.is_a?(Array)
|
|
289
|
+
|
|
290
|
+
Array.new
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def public_instance_methods(path, class_name)
|
|
294
|
+
sexp = Ripper.sexp(File.read(path))
|
|
295
|
+
methods = Array.new
|
|
296
|
+
dynamic_definition = false
|
|
297
|
+
|
|
298
|
+
visit_definitions = lambda do |node, namespace|
|
|
299
|
+
next unless node.is_a?(Array)
|
|
300
|
+
|
|
301
|
+
case node.first
|
|
302
|
+
when :program
|
|
303
|
+
Array(node[1]).each { |child| visit_definitions.call(child, namespace) }
|
|
304
|
+
when :module
|
|
305
|
+
name = qualified_constant_name(node[1], namespace)
|
|
306
|
+
visit_definitions.call(node[2], name)
|
|
307
|
+
when :class
|
|
308
|
+
name = qualified_constant_name(node[1], namespace)
|
|
309
|
+
if name == class_name
|
|
310
|
+
class_body_methods(node[3]).tap do |result|
|
|
311
|
+
methods.concat(result.fetch(:methods))
|
|
312
|
+
dynamic_definition ||= result.fetch(:dynamic)
|
|
313
|
+
end
|
|
314
|
+
else
|
|
315
|
+
visit_definitions.call(node[3], name)
|
|
316
|
+
end
|
|
317
|
+
when :bodystmt
|
|
318
|
+
Array(node[1]).each { |child| visit_definitions.call(child, namespace) }
|
|
319
|
+
else
|
|
320
|
+
node.each { |child| visit_definitions.call(child, namespace) if child.is_a?(Array) }
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
visit_definitions.call(sexp, nil)
|
|
325
|
+
[methods.uniq, dynamic_definition]
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def class_body_methods(body)
|
|
329
|
+
visibility = :public
|
|
330
|
+
methods = Array.new
|
|
331
|
+
dynamic = false
|
|
332
|
+
statements = body&.first == :bodystmt ? Array(body[1]) : Array.new
|
|
333
|
+
|
|
334
|
+
statements.each do |statement|
|
|
335
|
+
visibility_name = bare_call_name(statement)
|
|
336
|
+
case visibility_name
|
|
337
|
+
when "public"
|
|
338
|
+
visibility = :public
|
|
339
|
+
next
|
|
340
|
+
when "private"
|
|
341
|
+
visibility = :private
|
|
342
|
+
next
|
|
343
|
+
when "protected"
|
|
344
|
+
visibility = :protected
|
|
345
|
+
next
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
if statement&.first == :def
|
|
349
|
+
methods << statement[1][1].to_s if visibility == :public
|
|
350
|
+
elsif contains_call?(statement, "define_method")
|
|
351
|
+
dynamic = true
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
{ methods: methods, dynamic: dynamic }
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def bare_call_name(node)
|
|
359
|
+
return unless node&.first == :vcall
|
|
360
|
+
|
|
361
|
+
token = node[1]
|
|
362
|
+
token[1] if token&.first == :@ident
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def contains_call?(node, method_name)
|
|
366
|
+
return false unless node.is_a?(Array)
|
|
367
|
+
return true if %i[vcall fcall].include?(node.first) && node.dig(1, 1) == method_name
|
|
368
|
+
|
|
369
|
+
node.any? { |child| child.is_a?(Array) && contains_call?(child, method_name) }
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def qualified_constant_name(node, namespace)
|
|
373
|
+
name = constant_name(node)
|
|
374
|
+
return name if name&.include?("::") || namespace.nil?
|
|
375
|
+
|
|
376
|
+
"#{namespace}::#{name}"
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def constant_name(node)
|
|
380
|
+
return unless node.is_a?(Array)
|
|
381
|
+
|
|
382
|
+
case node.first
|
|
383
|
+
when :var_ref, :const_ref, :top_const_ref
|
|
384
|
+
node.dig(1, 1)
|
|
385
|
+
when :const_path_ref
|
|
386
|
+
[constant_name(node[1]), node.dig(2, 1)].compact.join("::")
|
|
387
|
+
end
|
|
139
388
|
end
|
|
140
389
|
|
|
141
390
|
def underscore(name)
|
data/lib/torikago/cli.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Torikago
|
|
|
13
13
|
init
|
|
14
14
|
interactively generate package_api.yml files and config/initializers/torikago.rb
|
|
15
15
|
check
|
|
16
|
-
validate Gateway
|
|
16
|
+
validate Gateway invocations against package_api.yml
|
|
17
17
|
update-package-api [BOX]
|
|
18
18
|
regenerate package_api.yml entries from the configured public API entrypoint
|
|
19
19
|
help, --help, -h
|
|
@@ -74,7 +74,7 @@ module Torikago
|
|
|
74
74
|
)
|
|
75
75
|
|
|
76
76
|
manifest_path = module_root.join("package_api.yml")
|
|
77
|
-
manifest = manifest_path.exist? ? load_yaml_file(manifest_path) : { "exports" =>
|
|
77
|
+
manifest = manifest_path.exist? ? load_yaml_file(manifest_path) : { "exports" => Hash.new }
|
|
78
78
|
manifest_path.write(render_package_api_manifest(manifest))
|
|
79
79
|
stdout.puts("generated #{manifest_path}")
|
|
80
80
|
end
|
|
@@ -102,7 +102,7 @@ module Torikago
|
|
|
102
102
|
if result.ok?
|
|
103
103
|
stdout.puts(
|
|
104
104
|
"ok: scanned #{result.scanned_file_count} Ruby files, " \
|
|
105
|
-
"found #{result.gateway_call_count} Gateway
|
|
105
|
+
"found #{result.gateway_call_count} static Gateway invocations, " \
|
|
106
106
|
"validated #{result.manifest_count} package_api manifests"
|
|
107
107
|
)
|
|
108
108
|
0
|
|
@@ -110,7 +110,7 @@ module Torikago
|
|
|
110
110
|
result.errors.each { |error| stderr.puts(error) }
|
|
111
111
|
stderr.puts(
|
|
112
112
|
"failed: scanned #{result.scanned_file_count} Ruby files, " \
|
|
113
|
-
"found #{result.gateway_call_count} Gateway
|
|
113
|
+
"found #{result.gateway_call_count} static Gateway invocations, " \
|
|
114
114
|
"validated #{result.manifest_count} package_api manifests, " \
|
|
115
115
|
"#{result.errors.size} errors"
|
|
116
116
|
)
|
|
@@ -158,7 +158,7 @@ module Torikago
|
|
|
158
158
|
end
|
|
159
159
|
|
|
160
160
|
def load_yaml_file(path)
|
|
161
|
-
YAML.safe_load(path.read, permitted_classes:
|
|
161
|
+
YAML.safe_load(path.read, permitted_classes: Array.new, aliases: false) || Hash.new
|
|
162
162
|
end
|
|
163
163
|
|
|
164
164
|
def render_initializer(configuration)
|
|
@@ -166,7 +166,7 @@ module Torikago
|
|
|
166
166
|
"# This file registers torikago runtime boundaries for your Rails app.",
|
|
167
167
|
"#",
|
|
168
168
|
"# Each config.register call defines one module root. Calls between modules",
|
|
169
|
-
"# should go through Torikago::Gateway.
|
|
169
|
+
"# should go through Torikago::Gateway.invoke(\"Module::ExportedApi\", :call) instead",
|
|
170
170
|
"# of reaching across module constants directly.",
|
|
171
171
|
"#",
|
|
172
172
|
"# Options:",
|
|
@@ -178,6 +178,11 @@ module Torikago
|
|
|
178
178
|
"# Directory containing exported Package API classes. The conventional",
|
|
179
179
|
"# default is app/package_api.",
|
|
180
180
|
"#",
|
|
181
|
+
"# rails_engine:",
|
|
182
|
+
"# Set true when the module owns a Rails::Engine entrypoint at",
|
|
183
|
+
"# lib/<module_name>.rb. That entrypoint is then left for Rails and",
|
|
184
|
+
"# skipped by the module Box runtime.",
|
|
185
|
+
"#",
|
|
181
186
|
"# gemfile:",
|
|
182
187
|
"# Optional module-local Gemfile. When Ruby::Box isolation is enabled,",
|
|
183
188
|
"# torikago prepends that module's resolved gem require paths inside the",
|
|
@@ -192,7 +197,7 @@ module Torikago
|
|
|
192
197
|
"#",
|
|
193
198
|
"# bin/torikago update-package-api",
|
|
194
199
|
"#",
|
|
195
|
-
"# To validate Gateway
|
|
200
|
+
"# To validate Gateway invocations against package_api.yml, run:",
|
|
196
201
|
"#",
|
|
197
202
|
"# bin/torikago check",
|
|
198
203
|
"",
|
|
@@ -203,8 +208,16 @@ module Torikago
|
|
|
203
208
|
lines << " config.register("
|
|
204
209
|
lines << " :#{definition.name},"
|
|
205
210
|
lines << " root: Rails.root.join(\"#{definition.root.to_s}\"),"
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
option_lines = Array.new
|
|
212
|
+
option_lines << " entrypoint: #{definition.entrypoint.inspect}" if definition.entrypoint
|
|
213
|
+
option_lines << " rails_engine: #{definition.rails_engine.inspect}" if definition.rails_engine
|
|
214
|
+
option_lines << " setup: #{definition.setup.inspect}" if definition.setup
|
|
215
|
+
option_lines << " gemfile: #{definition.gemfile.inspect}" if definition.gemfile
|
|
216
|
+
|
|
217
|
+
option_lines.each_with_index do |option_line, index|
|
|
218
|
+
comma = index == option_lines.length - 1 ? "" : ","
|
|
219
|
+
lines << "#{option_line}#{comma}"
|
|
220
|
+
end
|
|
208
221
|
lines << " )"
|
|
209
222
|
lines << ""
|
|
210
223
|
end
|
|
@@ -219,8 +232,9 @@ module Torikago
|
|
|
219
232
|
#
|
|
220
233
|
# Each key under exports is a class that may be called through:
|
|
221
234
|
#
|
|
222
|
-
# Torikago::Gateway.
|
|
235
|
+
# Torikago::Gateway.invoke("ModuleName::SomeQuery", :call)
|
|
223
236
|
#
|
|
237
|
+
# methods lists the public instance methods exposed through Gateway.
|
|
224
238
|
# allowed_callers lists other modules that may call that export. The
|
|
225
239
|
# host app and the module itself are allowed implicitly.
|
|
226
240
|
#
|
|
@@ -4,13 +4,13 @@ module Torikago
|
|
|
4
4
|
# Stores module definitions declared by the host application. Other runtime
|
|
5
5
|
# objects treat this as the source of truth for module roots and boot options.
|
|
6
6
|
class Configuration
|
|
7
|
-
Definition = Struct.new(:name, :root, :entrypoint, :setup, :gemfile, keyword_init: true)
|
|
7
|
+
Definition = Struct.new(:name, :root, :entrypoint, :rails_engine, :setup, :gemfile, keyword_init: true)
|
|
8
8
|
|
|
9
9
|
def initialize
|
|
10
|
-
@definitions =
|
|
10
|
+
@definitions = Hash.new
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def register(name, root:, entrypoint: nil, setup: nil, gemfile: nil)
|
|
13
|
+
def register(name, root:, entrypoint: nil, rails_engine: false, setup: nil, gemfile: nil)
|
|
14
14
|
if @definitions.key?(name.to_sym)
|
|
15
15
|
raise ArgumentError, "module already registered: #{name}"
|
|
16
16
|
end
|
|
@@ -19,6 +19,7 @@ module Torikago
|
|
|
19
19
|
name: name.to_sym,
|
|
20
20
|
root: Pathname(root),
|
|
21
21
|
entrypoint: entrypoint,
|
|
22
|
+
rails_engine: rails_engine,
|
|
22
23
|
setup: setup,
|
|
23
24
|
gemfile: gemfile
|
|
24
25
|
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Torikago
|
|
2
|
+
# A host-owned Rack-compatible route endpoint that dispatches directly to a
|
|
3
|
+
# controller class loaded inside a registered module runtime. It is not Rack
|
|
4
|
+
# middleware. The host router never resolves the controller constant, so
|
|
5
|
+
# Rails::Engine is not required for isolation.
|
|
6
|
+
class ControllerEndpoint
|
|
7
|
+
def initialize(module_name, controller_name, action_name, registry: nil)
|
|
8
|
+
@module_name = module_name.to_sym
|
|
9
|
+
@controller_name = controller_name.to_s
|
|
10
|
+
@action_name = action_name.to_sym
|
|
11
|
+
@registry = registry
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(env)
|
|
15
|
+
registry.resolve(module_name).dispatch_controller(
|
|
16
|
+
env,
|
|
17
|
+
controller_name: controller_name,
|
|
18
|
+
action_name: action_name
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :action_name, :controller_name, :module_name
|
|
25
|
+
|
|
26
|
+
def registry
|
|
27
|
+
@registry || Torikago.registry
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|