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
|
@@ -1,31 +1,93 @@
|
|
|
1
1
|
require "pathname"
|
|
2
|
+
require_relative "root_constant_resolver"
|
|
2
3
|
|
|
3
4
|
module Torikago
|
|
4
5
|
# Owns the runtime for one registered module. When Ruby::Box is available it
|
|
5
6
|
# loads the module into an isolated Box; otherwise it falls back to the host
|
|
6
7
|
# process for development and tests.
|
|
7
8
|
class EngineContainer
|
|
8
|
-
|
|
9
|
+
BUNDLER_SETUP_ENV_MUTEX = Mutex.new
|
|
10
|
+
TORIKAGO_RUNTIME_CONSTANTS = %i[
|
|
11
|
+
Error
|
|
12
|
+
DependencyError
|
|
13
|
+
BoxUnavailableError
|
|
14
|
+
PublicApiError
|
|
15
|
+
GemfileOverrideError
|
|
16
|
+
Gateway
|
|
17
|
+
].freeze
|
|
18
|
+
FRAMEWORK_CONSTANTS = %i[
|
|
19
|
+
Rails
|
|
20
|
+
ActionController
|
|
21
|
+
ActionDispatch
|
|
22
|
+
ActionView
|
|
23
|
+
ActiveModel
|
|
24
|
+
ActiveRecord
|
|
25
|
+
ActiveSupport
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
def initialize(name:, module_root:, entrypoint: nil, rails_engine: false, setup: nil, gemfile: nil, registered_roots: nil, box_factory: nil, gemfile_dependency_loader: nil, gem_activator: nil, root_constant_resolver: nil)
|
|
9
29
|
@name = name
|
|
10
30
|
@module_root = Pathname(module_root)
|
|
11
31
|
@entrypoint = entrypoint
|
|
32
|
+
@rails_engine = rails_engine
|
|
12
33
|
@setup = setup
|
|
13
34
|
@gemfile = gemfile
|
|
35
|
+
@registered_roots = registered_roots || [@module_root]
|
|
14
36
|
@box_factory = box_factory
|
|
15
37
|
@gemfile_dependency_loader = gemfile_dependency_loader || method(:load_gemfile_dependencies)
|
|
16
38
|
@gem_activator = gem_activator || method(:activate_gem_dependency)
|
|
39
|
+
@root_constant_resolver = root_constant_resolver
|
|
17
40
|
end
|
|
18
41
|
|
|
19
|
-
def
|
|
42
|
+
def invoke(public_api_class_name, method_name, constructor_args:, constructor_kwargs:, method_args:, method_kwargs:)
|
|
20
43
|
CurrentExecution.with_box(name) do
|
|
21
44
|
public_api_class = resolve_public_api_class(public_api_class_name)
|
|
22
|
-
public_api_class.new
|
|
45
|
+
instance = public_api_class.new(*constructor_args, **constructor_kwargs)
|
|
46
|
+
instance.public_send(method_name, *method_args, **method_kwargs)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Dispatches a Rack request to a Rails::Engine owned by this module. Route
|
|
51
|
+
# recognition happens against the Box-local engine, then the Box-local
|
|
52
|
+
# controller class is invoked directly so Rails never constantizes the
|
|
53
|
+
# controller in the host application's Object namespace.
|
|
54
|
+
def call(env)
|
|
55
|
+
unless rails_engine?
|
|
56
|
+
raise ArgumentError, "module #{name} is not registered with rails_engine: true"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
CurrentExecution.with_box(name) do
|
|
60
|
+
boot_runtime!
|
|
61
|
+
route, rack_response = recognize_route(env)
|
|
62
|
+
return rack_response if rack_response
|
|
63
|
+
|
|
64
|
+
controller = resolve_runtime_class("#{camelize_controller(route.fetch(:controller))}Controller")
|
|
65
|
+
|
|
66
|
+
dispatch_controller_action(env, controller, route.fetch(:action), route)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Dispatches a host-owned route to a controller that belongs to this module.
|
|
71
|
+
# Unlike #call, route recognition has already happened in the host router, so
|
|
72
|
+
# this path does not require a Rails::Engine inside the module.
|
|
73
|
+
def dispatch_controller(env, controller_name:, action_name:)
|
|
74
|
+
CurrentExecution.with_box(name) do
|
|
75
|
+
boot_controller_runtime!
|
|
76
|
+
controller = resolve_runtime_class(controller_name)
|
|
77
|
+
|
|
78
|
+
dispatch_controller_action(
|
|
79
|
+
env,
|
|
80
|
+
controller,
|
|
81
|
+
action_name,
|
|
82
|
+
controller: controller_name,
|
|
83
|
+
action: action_name.to_s
|
|
84
|
+
)
|
|
23
85
|
end
|
|
24
86
|
end
|
|
25
87
|
|
|
26
88
|
private
|
|
27
89
|
|
|
28
|
-
attr_reader :box_factory, :entrypoint, :gem_activator, :gemfile, :gemfile_dependency_loader, :module_root, :name, :setup
|
|
90
|
+
attr_reader :box_factory, :entrypoint, :gem_activator, :gemfile, :gemfile_dependency_loader, :module_root, :name, :registered_roots, :root_constant_resolver, :setup
|
|
29
91
|
|
|
30
92
|
def boot_runtime!
|
|
31
93
|
return if @booted
|
|
@@ -33,15 +95,23 @@ module Torikago
|
|
|
33
95
|
files = runtime_files
|
|
34
96
|
gemfile_dependencies
|
|
35
97
|
if isolated_box_enabled?
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
98
|
+
without_bundler_setup_env do
|
|
99
|
+
# The Box starts with an independent load path and constant table, so
|
|
100
|
+
# boot has to copy enough host context before loading module code.
|
|
101
|
+
with_box_runtime_errors do
|
|
102
|
+
prepare_box!
|
|
103
|
+
prepend_gemfile_require_paths_to_box!
|
|
104
|
+
install_host_runtime_bridges!
|
|
105
|
+
load_setup_hook_into_box!
|
|
106
|
+
ensure_root_namespace_in_box!
|
|
107
|
+
prepare_rails_application_controller_in_box!
|
|
108
|
+
|
|
109
|
+
files.each do |path|
|
|
110
|
+
box.load(path)
|
|
111
|
+
normalize_rails_constant_name!(path)
|
|
112
|
+
end
|
|
113
|
+
attach_rails_helpers!
|
|
114
|
+
end
|
|
45
115
|
end
|
|
46
116
|
else
|
|
47
117
|
# Non-Box mode preserves the same public behavior while giving up
|
|
@@ -52,12 +122,203 @@ module Torikago
|
|
|
52
122
|
|
|
53
123
|
files.each do |path|
|
|
54
124
|
load path
|
|
125
|
+
normalize_rails_constant_name!(path)
|
|
55
126
|
end
|
|
127
|
+
attach_rails_helpers!
|
|
56
128
|
end
|
|
57
129
|
|
|
58
130
|
@booted = true
|
|
59
131
|
end
|
|
60
132
|
|
|
133
|
+
def boot_controller_runtime!
|
|
134
|
+
boot_runtime!
|
|
135
|
+
return if rails_engine?
|
|
136
|
+
return if @controller_runtime_booted
|
|
137
|
+
|
|
138
|
+
files = controller_runtime_files - runtime_files
|
|
139
|
+
if isolated_box_enabled?
|
|
140
|
+
without_bundler_setup_env do
|
|
141
|
+
with_box_runtime_errors do
|
|
142
|
+
install_rails_runtime_support!
|
|
143
|
+
prepare_rails_application_controller_in_box!
|
|
144
|
+
load_runtime_files_into_box!(files)
|
|
145
|
+
attach_rails_helpers!
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
else
|
|
149
|
+
files.each do |path|
|
|
150
|
+
load path
|
|
151
|
+
normalize_rails_constant_name!(path)
|
|
152
|
+
end
|
|
153
|
+
attach_rails_helpers!
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
@controller_runtime_booted = true
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def load_runtime_files_into_box!(files)
|
|
160
|
+
files.each do |path|
|
|
161
|
+
box.load(path)
|
|
162
|
+
normalize_rails_constant_name!(path)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def dispatch_controller_action(env, controller, action_name, path_parameters)
|
|
167
|
+
current_parameters = env.fetch(
|
|
168
|
+
"action_dispatch.request.path_parameters",
|
|
169
|
+
Hash.new
|
|
170
|
+
)
|
|
171
|
+
request_env = env.merge(
|
|
172
|
+
"action_dispatch.request.path_parameters" =>
|
|
173
|
+
current_parameters.merge(path_parameters.transform_keys(&:to_sym))
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
controller.action(action_name).call(request_env)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def attach_rails_helpers!
|
|
180
|
+
helper_modules = Dir[module_root.join("app/helpers/**/*_helper.rb").to_s].sort.filter_map do |path|
|
|
181
|
+
constant_name = rails_constant_name_for(path)
|
|
182
|
+
resolve_runtime_class(constant_name) if constant_name
|
|
183
|
+
rescue NameError
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
return if helper_modules.empty?
|
|
187
|
+
|
|
188
|
+
rails_controller_files.each do |path|
|
|
189
|
+
constant_name = rails_constant_name_for(path)
|
|
190
|
+
next unless constant_name
|
|
191
|
+
|
|
192
|
+
controller = resolve_runtime_class(constant_name)
|
|
193
|
+
next unless controller.respond_to?(:helper)
|
|
194
|
+
|
|
195
|
+
helper_modules.each { |helper_module| controller.helper(helper_module) }
|
|
196
|
+
rescue NameError
|
|
197
|
+
next
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def prepare_rails_application_controller_in_box!
|
|
202
|
+
return unless Object.const_defined?(:ActionController, false)
|
|
203
|
+
return unless module_root.join("app/controllers/#{name}/application_controller.rb").exist?
|
|
204
|
+
|
|
205
|
+
# FIXME: This workaround only supports controllers under the namespace
|
|
206
|
+
# derived from the registered module name. Real top-level ActionController
|
|
207
|
+
# subclasses still expose Ruby::Box's internal prefix to Rails inherited
|
|
208
|
+
# hooks and helper lookup. Track support for non-namespaced controllers in:
|
|
209
|
+
# https://github.com/se4weed/torikago/issues/15
|
|
210
|
+
namespace_name = camelize(name.to_s)
|
|
211
|
+
namespace = box.const_get(namespace_name)
|
|
212
|
+
application_controller = if namespace.const_defined?(:ApplicationController, false)
|
|
213
|
+
namespace.const_get(:ApplicationController, false)
|
|
214
|
+
else
|
|
215
|
+
action_controller_base = Object.const_get(:ActionController).const_get(:Base)
|
|
216
|
+
namespace.const_set(:ApplicationController, Class.new(action_controller_base))
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
application_controller.define_singleton_method(:name) { "#{namespace_name}::ApplicationController" }
|
|
220
|
+
application_controller.define_singleton_method(:inherited) do |subclass|
|
|
221
|
+
native_name = Module.instance_method(:name).bind_call(subclass)
|
|
222
|
+
marker = "::#{namespace_name}::"
|
|
223
|
+
conventional_name = if native_name&.include?(marker)
|
|
224
|
+
"#{namespace_name}::#{native_name.split(marker, 2).last}"
|
|
225
|
+
else
|
|
226
|
+
native_name
|
|
227
|
+
end
|
|
228
|
+
# A class is still anonymous while inherited() runs. Do not let it
|
|
229
|
+
# inherit ApplicationController's singleton name implementation.
|
|
230
|
+
subclass.define_singleton_method(:name) { conventional_name }
|
|
231
|
+
# ActiveSupport's default module_parents implementation constantizes
|
|
232
|
+
# through the host Object. Return the already-resolved Box namespace.
|
|
233
|
+
subclass.define_singleton_method(:module_parents) { [namespace] }
|
|
234
|
+
# AbstractController tries to constantize "<Controller>Helper" in the
|
|
235
|
+
# host Object namespace during inherited(). Module helpers live in this
|
|
236
|
+
# Box, so they are attached explicitly after runtime files are loaded.
|
|
237
|
+
subclass.define_singleton_method(:default_helper_module!) {}
|
|
238
|
+
subclass.singleton_class.send(:private, :default_helper_module!)
|
|
239
|
+
super(subclass)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def normalize_rails_constant_name!(path)
|
|
244
|
+
constant_name = rails_constant_name_for(path)
|
|
245
|
+
return unless constant_name
|
|
246
|
+
|
|
247
|
+
constant = resolve_runtime_class(constant_name)
|
|
248
|
+
return unless constant.respond_to?(:define_singleton_method)
|
|
249
|
+
|
|
250
|
+
# Ruby::Box includes an internal Box prefix in Module#name. Rails derives
|
|
251
|
+
# helper and model constants from that value, so expose the conventional
|
|
252
|
+
# application constant name to Rails while keeping the constant itself in
|
|
253
|
+
# the Box.
|
|
254
|
+
constant.define_singleton_method(:name) { constant_name }
|
|
255
|
+
constant.remove_instance_variable(:@controller_path) if constant.instance_variable_defined?(:@controller_path)
|
|
256
|
+
constant.remove_instance_variable(:@controller_name) if constant.instance_variable_defined?(:@controller_name)
|
|
257
|
+
if path.include?("/app/controllers/") && constant.respond_to?(:prepend_view_path)
|
|
258
|
+
constant.prepend_view_path(module_root.join("app/views").to_s)
|
|
259
|
+
end
|
|
260
|
+
rescue NameError
|
|
261
|
+
# Namespace-only files and files that define a differently named helper
|
|
262
|
+
# do not need normalization.
|
|
263
|
+
nil
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def rails_constant_name_for(path)
|
|
267
|
+
app_root = module_root.join("app").to_s
|
|
268
|
+
return unless path.start_with?("#{app_root}/")
|
|
269
|
+
|
|
270
|
+
relative_path = path.delete_prefix("#{app_root}/")
|
|
271
|
+
category, constant_path = relative_path.split("/", 2)
|
|
272
|
+
return unless %w[controllers models helpers].include?(category)
|
|
273
|
+
return unless constant_path&.end_with?(".rb")
|
|
274
|
+
|
|
275
|
+
constant_path.delete_suffix(".rb").split("/").map { |segment| camelize(segment) }.join("::")
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def recognize_route(env)
|
|
279
|
+
engine = resolve_runtime_class("#{camelize(name.to_s)}::Engine")
|
|
280
|
+
routes = engine.routes
|
|
281
|
+
return recognize_rails_route(routes, env) if routes.respond_to?(:router) && Object.const_defined?(:ActionDispatch, false)
|
|
282
|
+
|
|
283
|
+
route = routes.recognize_path(
|
|
284
|
+
env.fetch("PATH_INFO", "/"),
|
|
285
|
+
method: env.fetch("REQUEST_METHOD", "GET").downcase.to_sym
|
|
286
|
+
)
|
|
287
|
+
[route.transform_keys(&:to_sym), nil]
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def recognize_rails_route(routes, env)
|
|
291
|
+
action_dispatch = Object.const_get(:ActionDispatch)
|
|
292
|
+
request_class = action_dispatch.const_get(:Request)
|
|
293
|
+
cascade_header = action_dispatch.const_get(:Constants).const_get(:X_CASCADE)
|
|
294
|
+
request = request_class.new(env)
|
|
295
|
+
recognized = nil
|
|
296
|
+
|
|
297
|
+
routes.router.recognize(request) do |route, parameters|
|
|
298
|
+
next unless route.app.matches?(request)
|
|
299
|
+
|
|
300
|
+
request.path_parameters = parameters
|
|
301
|
+
if route.app.dispatcher?
|
|
302
|
+
recognized = [parameters.transform_keys(&:to_sym), nil]
|
|
303
|
+
else
|
|
304
|
+
response = route.app.serve(request)
|
|
305
|
+
next if response[1][cascade_header] == "pass"
|
|
306
|
+
|
|
307
|
+
recognized = [nil, response]
|
|
308
|
+
end
|
|
309
|
+
break
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
return recognized if recognized
|
|
313
|
+
|
|
314
|
+
routing_error = Object.const_get(:ActionController).const_get(:RoutingError)
|
|
315
|
+
raise routing_error, "No route matches #{env.fetch("PATH_INFO", "/").inspect}"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def camelize_controller(controller_path)
|
|
319
|
+
controller_path.to_s.split("/").map { |segment| camelize(segment) }.join("::")
|
|
320
|
+
end
|
|
321
|
+
|
|
61
322
|
def load_setup_hook!
|
|
62
323
|
path = setup_path
|
|
63
324
|
return unless path
|
|
@@ -88,30 +349,69 @@ module Torikago
|
|
|
88
349
|
|
|
89
350
|
# Put module-specific gems ahead of the host load path so the Box resolves
|
|
90
351
|
# dependency versions from the module Gemfile first.
|
|
91
|
-
box.load_path.replace(paths.map
|
|
352
|
+
box.load_path.replace(paths.map { |path| path.to_s } + box.load_path)
|
|
92
353
|
end
|
|
93
354
|
|
|
94
355
|
def gemfile_dependencies
|
|
95
356
|
path = gemfile_path
|
|
96
|
-
return
|
|
357
|
+
return Array.new unless path
|
|
97
358
|
|
|
98
359
|
@gemfile_dependencies ||= gemfile_dependency_loader.call(path)
|
|
99
360
|
end
|
|
100
361
|
|
|
101
362
|
def resolve_public_api_class(class_name)
|
|
102
363
|
boot_runtime!
|
|
364
|
+
resolve_runtime_class(class_name)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def resolve_runtime_class(class_name)
|
|
103
368
|
root = isolated_box_enabled? ? box : Object
|
|
104
369
|
# const_get is evaluated inside the Box object when isolation is enabled,
|
|
105
|
-
# which keeps
|
|
370
|
+
# which keeps module-owned constants out of the host Object namespace.
|
|
106
371
|
class_name.split("::").reduce(root) { |context, segment| context.const_get(segment) }
|
|
107
372
|
end
|
|
108
373
|
|
|
109
374
|
def runtime_files
|
|
110
|
-
@runtime_files ||=
|
|
375
|
+
@runtime_files ||= if rails_engine?
|
|
376
|
+
rails_runtime_files
|
|
377
|
+
else
|
|
378
|
+
[
|
|
379
|
+
*library_files,
|
|
380
|
+
*gateway_model_files,
|
|
381
|
+
*Dir[public_api_root.join("**/*.rb").to_s].sort
|
|
382
|
+
]
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def rails_runtime_files
|
|
387
|
+
[
|
|
388
|
+
*rails_engine_entrypoint_files,
|
|
111
389
|
*library_files,
|
|
112
|
-
*
|
|
390
|
+
*Dir[module_root.join("app/models/**/*.rb").to_s].sort,
|
|
391
|
+
*Dir[module_root.join("app/helpers/**/*.rb").to_s].sort,
|
|
392
|
+
*rails_controller_files,
|
|
393
|
+
*Dir[public_api_root.join("**/*.rb").to_s].sort,
|
|
394
|
+
*Dir[module_root.join("config/routes.rb").to_s].sort
|
|
395
|
+
].uniq
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def controller_runtime_files
|
|
399
|
+
[
|
|
400
|
+
*library_files,
|
|
401
|
+
*Dir[module_root.join("app/models/**/*.rb").to_s].sort,
|
|
402
|
+
*Dir[module_root.join("app/helpers/**/*.rb").to_s].sort,
|
|
403
|
+
*rails_controller_files,
|
|
113
404
|
*Dir[public_api_root.join("**/*.rb").to_s].sort
|
|
114
|
-
]
|
|
405
|
+
].uniq
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def rails_controller_files
|
|
409
|
+
files = Dir[module_root.join("app/controllers/**/*.rb").to_s].sort
|
|
410
|
+
application_controllers, other_controllers = files.partition do |path|
|
|
411
|
+
File.basename(path) == "application_controller.rb"
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
application_controllers + other_controllers
|
|
115
415
|
end
|
|
116
416
|
|
|
117
417
|
def public_api_root
|
|
@@ -161,8 +461,24 @@ module Torikago
|
|
|
161
461
|
@box ||= if box_factory
|
|
162
462
|
box_factory.call
|
|
163
463
|
else
|
|
164
|
-
Ruby::Box.new
|
|
464
|
+
without_bundler_setup_env { Ruby::Box.new }
|
|
165
465
|
end
|
|
466
|
+
rescue BoxUnavailableError
|
|
467
|
+
raise
|
|
468
|
+
rescue StandardError => e
|
|
469
|
+
raise unless ENV["RUBY_BOX"] == "1"
|
|
470
|
+
|
|
471
|
+
raise BoxUnavailableError, "Ruby::Box is unavailable for module #{name}: #{e.message}"
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def with_box_runtime_errors
|
|
475
|
+
yield
|
|
476
|
+
rescue BoxUnavailableError
|
|
477
|
+
raise
|
|
478
|
+
rescue StandardError => e
|
|
479
|
+
raise unless ENV["RUBY_BOX"] == "1"
|
|
480
|
+
|
|
481
|
+
raise BoxUnavailableError, "Ruby::Box is unavailable for module #{name}: #{e.message}"
|
|
166
482
|
end
|
|
167
483
|
|
|
168
484
|
def prepare_box!
|
|
@@ -172,9 +488,65 @@ module Torikago
|
|
|
172
488
|
if box.respond_to?(:require)
|
|
173
489
|
box.require("torikago/current_execution")
|
|
174
490
|
end
|
|
491
|
+
install_torikago_runtime_bridges!
|
|
492
|
+
install_root_constant_fallback!
|
|
175
493
|
@box_prepared = true
|
|
176
494
|
end
|
|
177
495
|
|
|
496
|
+
def install_torikago_runtime_bridges!
|
|
497
|
+
box_torikago = box.const_get(:Torikago)
|
|
498
|
+
|
|
499
|
+
TORIKAGO_RUNTIME_CONSTANTS.each do |constant_name|
|
|
500
|
+
next if box_torikago.const_defined?(constant_name, false)
|
|
501
|
+
|
|
502
|
+
box_torikago.const_set(constant_name, Torikago.const_get(constant_name, false))
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def install_root_constant_fallback!
|
|
507
|
+
return unless box.respond_to?(:eval)
|
|
508
|
+
|
|
509
|
+
resolver = root_constant_resolver || RootConstantResolver.new(registered_roots: registered_roots)
|
|
510
|
+
box_torikago = box.const_get(:Torikago)
|
|
511
|
+
box_torikago.const_set(:ROOT_CONSTANT_RESOLVER, resolver)
|
|
512
|
+
box_torikago.const_set(:ROOT_CONSTANT_UNRESOLVED, resolver.unresolved)
|
|
513
|
+
box.eval(<<~RUBY)
|
|
514
|
+
module Torikago
|
|
515
|
+
module RootConstantFallback
|
|
516
|
+
def const_missing(name)
|
|
517
|
+
resolved = ROOT_CONSTANT_RESOLVER.resolve(name)
|
|
518
|
+
return resolved unless resolved.equal?(ROOT_CONSTANT_UNRESOLVED)
|
|
519
|
+
|
|
520
|
+
super
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
Object.singleton_class.prepend(Torikago::RootConstantFallback)
|
|
526
|
+
RUBY
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def install_host_runtime_bridges!
|
|
530
|
+
FRAMEWORK_CONSTANTS.each { |constant_name| install_host_constant_bridge!(constant_name) }
|
|
531
|
+
install_rails_runtime_support! if rails_engine?
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def install_rails_runtime_support!
|
|
535
|
+
install_host_constant_bridge!(:ApplicationController)
|
|
536
|
+
install_host_constant_bridge!(:ApplicationRecord)
|
|
537
|
+
return unless Object.const_defined?(:ActiveSupport, false)
|
|
538
|
+
return unless box.respond_to?(:require)
|
|
539
|
+
|
|
540
|
+
box.require("active_support/core_ext/object/blank")
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def install_host_constant_bridge!(constant_name)
|
|
544
|
+
return unless Object.const_defined?(constant_name, false)
|
|
545
|
+
return if box.const_defined?(constant_name, false)
|
|
546
|
+
|
|
547
|
+
box.const_set(constant_name, Object.const_get(constant_name, false))
|
|
548
|
+
end
|
|
549
|
+
|
|
178
550
|
def gateway_model_files
|
|
179
551
|
# Package APIs may depend on small PORO-style models. Active Record models
|
|
180
552
|
# are skipped because Rails owns their loading and database connection.
|
|
@@ -205,22 +577,22 @@ module Torikago
|
|
|
205
577
|
|
|
206
578
|
# Monkey patches are only loaded through the explicit setup hook so a
|
|
207
579
|
# module has to opt in to global-ish runtime changes.
|
|
208
|
-
all_files - monkey_patch_files
|
|
580
|
+
all_files - monkey_patch_files - rails_engine_entrypoint_files
|
|
209
581
|
end
|
|
210
582
|
|
|
211
|
-
def
|
|
212
|
-
return
|
|
583
|
+
def rails_engine_entrypoint_files
|
|
584
|
+
return Array.new unless rails_engine?
|
|
585
|
+
|
|
586
|
+
path = module_root.join("lib/#{name}.rb")
|
|
587
|
+
path.exist? ? [path.to_s] : Array.new
|
|
588
|
+
end
|
|
213
589
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
end
|
|
221
|
-
else
|
|
222
|
-
false
|
|
223
|
-
end
|
|
590
|
+
def rails_engine?
|
|
591
|
+
@rails_engine
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def ruby_box_runtime_available?
|
|
595
|
+
ENV["RUBY_BOX"] == "1"
|
|
224
596
|
end
|
|
225
597
|
|
|
226
598
|
def camelize(segment)
|
|
@@ -240,33 +612,15 @@ module Torikago
|
|
|
240
612
|
installed_gem_dependencies = load_installed_gem_dependencies(path)
|
|
241
613
|
return installed_gem_dependencies unless installed_gem_dependencies.empty?
|
|
242
614
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
lockfile = Pathname("#{path}.lock")
|
|
246
|
-
definition = Bundler::Definition.build(path.to_s, lockfile.exist? ? lockfile.to_s : nil, nil)
|
|
247
|
-
|
|
248
|
-
specs_by_name = definition.specs.each_with_object({}) do |spec, specs|
|
|
249
|
-
specs[spec.name] ||= spec
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
definition.dependencies.filter_map do |dependency|
|
|
253
|
-
spec = specs_by_name.fetch(dependency.name, nil)
|
|
254
|
-
next unless spec
|
|
255
|
-
|
|
256
|
-
requirement = dependency.requirement.to_s
|
|
257
|
-
{
|
|
258
|
-
name: dependency.name,
|
|
259
|
-
requirement: requirement,
|
|
260
|
-
require_paths: spec.full_require_paths
|
|
261
|
-
}
|
|
262
|
-
end
|
|
263
|
-
rescue Bundler::BundlerError => e
|
|
264
|
-
raise GemfileOverrideError, "failed to load gemfile for #{name}: #{e.message}"
|
|
615
|
+
load_bundler_gem_dependencies(path)
|
|
265
616
|
end
|
|
266
617
|
|
|
267
618
|
def load_path_gem_dependencies(path)
|
|
268
619
|
path.dirname.then do |gemfile_root|
|
|
269
620
|
path.read.scan(/^\s*gem\s+["']([^"']+)["']\s*,\s*path:\s*["']([^"']+)["']/).filter_map do |gem_name, relative_path|
|
|
621
|
+
next unless relative_path
|
|
622
|
+
|
|
623
|
+
relative_path = relative_path.to_s
|
|
270
624
|
gem_root = gemfile_root.join(relative_path)
|
|
271
625
|
gemspec_path = gem_root.join("#{gem_name}.gemspec")
|
|
272
626
|
gemspec_path = Dir[gem_root.join("*.gemspec").to_s].sort.first unless gemspec_path.exist?
|
|
@@ -284,9 +638,38 @@ module Torikago
|
|
|
284
638
|
end
|
|
285
639
|
end
|
|
286
640
|
|
|
641
|
+
def load_bundler_gem_dependencies(path)
|
|
642
|
+
require "bundler"
|
|
643
|
+
|
|
644
|
+
lockfile = Pathname("#{path}.lock")
|
|
645
|
+
definition = Bundler::Definition.build(path.to_s, lockfile.exist? ? lockfile.to_s : nil, nil)
|
|
646
|
+
|
|
647
|
+
specs_by_name = definition.specs.each_with_object(Hash.new) do |spec, specs|
|
|
648
|
+
specs[spec.name] ||= spec
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
definition.dependencies.filter_map do |dependency|
|
|
652
|
+
spec = specs_by_name.fetch(dependency.name, nil)
|
|
653
|
+
next unless spec
|
|
654
|
+
|
|
655
|
+
requirement = dependency.requirement.to_s
|
|
656
|
+
{
|
|
657
|
+
name: dependency.name,
|
|
658
|
+
requirement: requirement,
|
|
659
|
+
require_paths: spec.full_require_paths
|
|
660
|
+
}
|
|
661
|
+
end
|
|
662
|
+
rescue LoadError => e
|
|
663
|
+
raise GemfileOverrideError, "failed to load bundler for #{name}: #{e.message}"
|
|
664
|
+
rescue StandardError => e
|
|
665
|
+
raise unless defined?(Bundler::BundlerError) && e.is_a?(Bundler::BundlerError)
|
|
666
|
+
|
|
667
|
+
raise GemfileOverrideError, "failed to load gemfile for #{name}: #{e.message}"
|
|
668
|
+
end
|
|
669
|
+
|
|
287
670
|
def load_installed_gem_dependencies(path)
|
|
288
671
|
dependencies = exact_version_gemfile_dependencies(path)
|
|
289
|
-
return
|
|
672
|
+
return Array.new if dependencies.empty?
|
|
290
673
|
|
|
291
674
|
dependencies.filter_map do |dependency|
|
|
292
675
|
specs = installed_specs_for(dependency.fetch(:name), dependency.fetch(:requirement))
|
|
@@ -326,9 +709,13 @@ module Torikago
|
|
|
326
709
|
match = line.match(/^\s*gem\s+["']([^"']+)["']\s*,\s*["']=\s*([^"']+)["']/)
|
|
327
710
|
next unless match
|
|
328
711
|
|
|
712
|
+
gem_name = match[1]
|
|
713
|
+
version = match[2]
|
|
714
|
+
next unless gem_name && version
|
|
715
|
+
|
|
329
716
|
{
|
|
330
|
-
name:
|
|
331
|
-
requirement: "= #{
|
|
717
|
+
name: gem_name,
|
|
718
|
+
requirement: "= #{version}"
|
|
332
719
|
}
|
|
333
720
|
end
|
|
334
721
|
end
|
|
@@ -336,5 +723,45 @@ module Torikago
|
|
|
336
723
|
def activate_gem_dependency(dependency)
|
|
337
724
|
Kernel.send(:gem, dependency.fetch(:name), dependency.fetch(:requirement))
|
|
338
725
|
end
|
|
726
|
+
|
|
727
|
+
def without_bundler_setup_env
|
|
728
|
+
if BUNDLER_SETUP_ENV_MUTEX.owned?
|
|
729
|
+
without_bundler_setup_env_unsynchronized { yield }
|
|
730
|
+
else
|
|
731
|
+
BUNDLER_SETUP_ENV_MUTEX.synchronize do
|
|
732
|
+
without_bundler_setup_env_unsynchronized { yield }
|
|
733
|
+
end
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
def without_bundler_setup_env_unsynchronized
|
|
738
|
+
rubyopt = ENV["RUBYOPT"]
|
|
739
|
+
bundler_setup = ENV["BUNDLER_SETUP"]
|
|
740
|
+
|
|
741
|
+
if rubyopt&.include?("bundler/setup")
|
|
742
|
+
options = rubyopt.split.reject { |option| option.include?("bundler/setup") }
|
|
743
|
+
if options.empty?
|
|
744
|
+
ENV.delete("RUBYOPT")
|
|
745
|
+
else
|
|
746
|
+
ENV["RUBYOPT"] = options.join(" ")
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
ENV.delete("BUNDLER_SETUP")
|
|
751
|
+
|
|
752
|
+
yield
|
|
753
|
+
ensure
|
|
754
|
+
if rubyopt
|
|
755
|
+
ENV["RUBYOPT"] = rubyopt
|
|
756
|
+
else
|
|
757
|
+
ENV.delete("RUBYOPT")
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
if bundler_setup
|
|
761
|
+
ENV["BUNDLER_SETUP"] = bundler_setup
|
|
762
|
+
else
|
|
763
|
+
ENV.delete("BUNDLER_SETUP")
|
|
764
|
+
end
|
|
765
|
+
end
|
|
339
766
|
end
|
|
340
767
|
end
|