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/gateway.rb
CHANGED
|
@@ -6,8 +6,12 @@ module Torikago
|
|
|
6
6
|
# manifest before dispatching into the target module container.
|
|
7
7
|
class Gateway
|
|
8
8
|
class << self
|
|
9
|
-
def
|
|
10
|
-
Torikago.gateway.
|
|
9
|
+
def build(...)
|
|
10
|
+
Torikago.gateway.build(...)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def invoke(...)
|
|
14
|
+
Torikago.gateway.invoke(...)
|
|
11
15
|
end
|
|
12
16
|
end
|
|
13
17
|
|
|
@@ -15,24 +19,51 @@ module Torikago
|
|
|
15
19
|
@registry = registry
|
|
16
20
|
@configuration = configuration
|
|
17
21
|
@manifest_loader = manifest_loader || method(:load_manifest)
|
|
18
|
-
@manifests =
|
|
22
|
+
@manifests = Hash.new
|
|
19
23
|
end
|
|
20
24
|
|
|
21
|
-
def
|
|
25
|
+
def build(public_api_class_name, *constructor_args, **constructor_kwargs)
|
|
26
|
+
Invocation.new(
|
|
27
|
+
gateway: self,
|
|
28
|
+
public_api_class_name: public_api_class_name,
|
|
29
|
+
constructor_args: constructor_args,
|
|
30
|
+
constructor_kwargs: constructor_kwargs
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def invoke(public_api_class_name, method_name, *method_args, **method_kwargs)
|
|
35
|
+
dispatch(
|
|
36
|
+
public_api_class_name: public_api_class_name,
|
|
37
|
+
method_name: method_name,
|
|
38
|
+
constructor_args: Array.new,
|
|
39
|
+
constructor_kwargs: Hash.new,
|
|
40
|
+
method_args: method_args,
|
|
41
|
+
method_kwargs: method_kwargs
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Internal interface used by Invocation. Validation deliberately precedes
|
|
46
|
+
# Registry resolution so rejected calls cannot boot the target Box.
|
|
47
|
+
def dispatch(public_api_class_name:, method_name:, constructor_args:, constructor_kwargs:, method_args:, method_kwargs:)
|
|
22
48
|
target_module = infer_target_module(public_api_class_name)
|
|
23
49
|
caller_module = CurrentExecution.current_box
|
|
24
50
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
51
|
+
validate_public_api!(target_module, public_api_class_name, method_name, caller_module)
|
|
52
|
+
registry.resolve(target_module).invoke(
|
|
53
|
+
public_api_class_name,
|
|
54
|
+
method_name,
|
|
55
|
+
constructor_args: constructor_args,
|
|
56
|
+
constructor_kwargs: constructor_kwargs,
|
|
57
|
+
method_args: method_args,
|
|
58
|
+
method_kwargs: method_kwargs
|
|
59
|
+
)
|
|
29
60
|
end
|
|
30
61
|
|
|
31
62
|
private
|
|
32
63
|
|
|
33
64
|
attr_reader :configuration, :manifest_loader, :manifests, :registry
|
|
34
65
|
|
|
35
|
-
def validate_public_api!(target_module, public_api_class_name, caller_module)
|
|
66
|
+
def validate_public_api!(target_module, public_api_class_name, method_name, caller_module)
|
|
36
67
|
target_name = target_module.to_sym
|
|
37
68
|
definition = configuration.fetch(target_name)
|
|
38
69
|
manifest = manifests.fetch(target_name) do
|
|
@@ -41,7 +72,19 @@ module Torikago
|
|
|
41
72
|
|
|
42
73
|
public_api_entry = exported_package_apis(manifest).fetch(public_api_class_name, nil)
|
|
43
74
|
if public_api_entry.nil?
|
|
44
|
-
raise PublicApiError,
|
|
75
|
+
raise PublicApiError,
|
|
76
|
+
"package api export not declared for #{target_name}: #{public_api_class_name}##{method_name}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
methods = exported_methods(public_api_entry)
|
|
80
|
+
if methods.empty?
|
|
81
|
+
raise PublicApiError,
|
|
82
|
+
"package api methods are not configured: #{public_api_class_name}##{method_name}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
unless methods.include?(method_name.to_s)
|
|
86
|
+
raise PublicApiError,
|
|
87
|
+
"package api method is not exported: #{public_api_class_name}##{method_name}"
|
|
45
88
|
end
|
|
46
89
|
|
|
47
90
|
return if caller_module.nil?
|
|
@@ -53,7 +96,7 @@ module Torikago
|
|
|
53
96
|
return if dependency_allowed?(public_api_entry, caller_name)
|
|
54
97
|
|
|
55
98
|
raise DependencyError,
|
|
56
|
-
"module dependency not allowed: #{caller_name} -> #{target_name}##{public_api_class_name}"
|
|
99
|
+
"module dependency not allowed: #{caller_name} -> #{target_name}##{public_api_class_name}##{method_name}"
|
|
57
100
|
end
|
|
58
101
|
|
|
59
102
|
def load_manifest(definition)
|
|
@@ -64,7 +107,7 @@ module Torikago
|
|
|
64
107
|
"package_api manifest not found for #{definition.name}: #{manifest_path}"
|
|
65
108
|
end
|
|
66
109
|
|
|
67
|
-
YAML.safe_load(manifest_path.read, permitted_classes:
|
|
110
|
+
YAML.safe_load(manifest_path.read, permitted_classes: Array.new, aliases: false) || Hash.new
|
|
68
111
|
end
|
|
69
112
|
|
|
70
113
|
def package_api_manifest_path(definition)
|
|
@@ -76,11 +119,29 @@ module Torikago
|
|
|
76
119
|
end
|
|
77
120
|
|
|
78
121
|
def exported_package_apis(manifest)
|
|
79
|
-
manifest.fetch("exports") { manifest.fetch("public_api",
|
|
122
|
+
manifest.fetch("exports") { manifest.fetch("public_api", Hash.new) }
|
|
80
123
|
end
|
|
81
124
|
|
|
82
125
|
def dependency_allowed?(public_api_entry, caller_name)
|
|
83
|
-
|
|
126
|
+
allowed_callers(public_api_entry).map { |caller| caller.to_sym }.include?(caller_name)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def exported_methods(public_api_entry)
|
|
130
|
+
return Array.new unless public_api_entry.is_a?(Hash)
|
|
131
|
+
|
|
132
|
+
methods = public_api_entry["methods"]
|
|
133
|
+
return methods.map(&:to_s) if methods.is_a?(Array)
|
|
134
|
+
|
|
135
|
+
Array.new
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def allowed_callers(public_api_entry)
|
|
139
|
+
return Array.new unless public_api_entry.is_a?(Hash)
|
|
140
|
+
|
|
141
|
+
allowed = public_api_entry["allowed_callers"]
|
|
142
|
+
return allowed if allowed.is_a?(Array)
|
|
143
|
+
|
|
144
|
+
Array.new
|
|
84
145
|
end
|
|
85
146
|
end
|
|
86
147
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Torikago
|
|
2
|
+
# Describes one package API invocation without resolving or booting its Box.
|
|
3
|
+
class Invocation
|
|
4
|
+
def initialize(gateway:, public_api_class_name:, constructor_args:, constructor_kwargs:)
|
|
5
|
+
@gateway = gateway
|
|
6
|
+
@public_api_class_name = public_api_class_name
|
|
7
|
+
@constructor_args = constructor_args.freeze
|
|
8
|
+
@constructor_kwargs = constructor_kwargs.freeze
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def invoke(method_name, *method_args, **method_kwargs)
|
|
12
|
+
gateway.dispatch(
|
|
13
|
+
public_api_class_name: public_api_class_name,
|
|
14
|
+
method_name: method_name,
|
|
15
|
+
constructor_args: constructor_args,
|
|
16
|
+
constructor_kwargs: constructor_kwargs,
|
|
17
|
+
method_args: method_args,
|
|
18
|
+
method_kwargs: method_kwargs
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :constructor_args, :constructor_kwargs, :gateway, :public_api_class_name
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -3,14 +3,15 @@ require "yaml"
|
|
|
3
3
|
|
|
4
4
|
module Torikago
|
|
5
5
|
# Regenerates package_api.yml from files under a module's public API
|
|
6
|
-
# entrypoint while preserving caller permissions already
|
|
6
|
+
# entrypoint while preserving exported methods and caller permissions already
|
|
7
|
+
# chosen by humans.
|
|
7
8
|
class PackageApiUpdater
|
|
8
9
|
def initialize(configuration:)
|
|
9
10
|
@configuration = configuration
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def call(module_name = nil)
|
|
13
|
-
definitions_for(module_name).each_with_object(
|
|
14
|
+
definitions_for(module_name).each_with_object(Hash.new) do |definition, updates|
|
|
14
15
|
manifest_path = definition.root.join("package_api.yml")
|
|
15
16
|
existing_manifest = load_manifest(manifest_path)
|
|
16
17
|
updated_manifest = build_manifest(definition, existing_manifest)
|
|
@@ -31,21 +32,22 @@ module Torikago
|
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def load_manifest(path)
|
|
34
|
-
return
|
|
35
|
+
return Hash.new unless path.exist?
|
|
35
36
|
|
|
36
|
-
YAML.safe_load(path.read, permitted_classes:
|
|
37
|
+
YAML.safe_load(path.read, permitted_classes: Array.new, aliases: false) || Hash.new
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
def build_manifest(definition, existing_manifest)
|
|
40
41
|
existing_public_api = exported_package_apis(existing_manifest)
|
|
41
42
|
|
|
42
|
-
public_api_entries = discover_public_api_classes(definition).each_with_object(
|
|
43
|
-
existing_entry = existing_public_api.fetch(class_name,
|
|
43
|
+
public_api_entries = discover_public_api_classes(definition).each_with_object(Hash.new) do |class_name, entries|
|
|
44
|
+
existing_entry = existing_public_api.fetch(class_name, Hash.new)
|
|
44
45
|
|
|
45
|
-
# update-package-api owns discovery, not policy. Keep
|
|
46
|
-
# the command does not
|
|
46
|
+
# update-package-api owns discovery, not policy. Keep methods and
|
|
47
|
+
# allowed_callers so the command does not change the public surface.
|
|
47
48
|
entries[class_name] = {
|
|
48
|
-
"
|
|
49
|
+
"methods" => exported_methods(existing_entry),
|
|
50
|
+
"allowed_callers" => allowed_callers(existing_entry).map { |caller| caller.to_s }
|
|
49
51
|
}
|
|
50
52
|
end
|
|
51
53
|
|
|
@@ -53,7 +55,25 @@ module Torikago
|
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
def exported_package_apis(manifest)
|
|
56
|
-
manifest.fetch("exports") { manifest.fetch("public_api",
|
|
58
|
+
manifest.fetch("exports") { manifest.fetch("public_api", Hash.new) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def allowed_callers(manifest_entry)
|
|
62
|
+
return Array.new unless manifest_entry.is_a?(Hash)
|
|
63
|
+
|
|
64
|
+
allowed = manifest_entry["allowed_callers"]
|
|
65
|
+
return allowed if allowed.is_a?(Array)
|
|
66
|
+
|
|
67
|
+
Array.new
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def exported_methods(manifest_entry)
|
|
71
|
+
return Array.new unless manifest_entry.is_a?(Hash)
|
|
72
|
+
|
|
73
|
+
methods = manifest_entry["methods"]
|
|
74
|
+
return methods.map(&:to_s) if methods.is_a?(Array)
|
|
75
|
+
|
|
76
|
+
Array.new
|
|
57
77
|
end
|
|
58
78
|
|
|
59
79
|
def render_manifest(manifest)
|
|
@@ -62,8 +82,9 @@ module Torikago
|
|
|
62
82
|
#
|
|
63
83
|
# Each key under exports is a class that may be called through:
|
|
64
84
|
#
|
|
65
|
-
# Torikago::Gateway.
|
|
85
|
+
# Torikago::Gateway.invoke("ModuleName::SomeQuery", :call)
|
|
66
86
|
#
|
|
87
|
+
# methods lists the public instance methods exposed through Gateway.
|
|
67
88
|
# allowed_callers lists other modules that may call that export. The
|
|
68
89
|
# host app and the module itself are allowed implicitly.
|
|
69
90
|
#
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Torikago
|
|
2
|
+
# A host-owned Rack endpoint that forwards requests to a registered module
|
|
3
|
+
# without exposing the module's Rails::Engine or controller constants in the
|
|
4
|
+
# main Box.
|
|
5
|
+
class RackEndpoint
|
|
6
|
+
def initialize(module_name, registry: nil)
|
|
7
|
+
@module_name = module_name.to_sym
|
|
8
|
+
@registry = registry
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(env)
|
|
12
|
+
registry.resolve(module_name).call(env)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
attr_reader :module_name
|
|
18
|
+
|
|
19
|
+
def registry
|
|
20
|
+
@registry || Torikago.registry
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/torikago/registry.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Torikago
|
|
|
4
4
|
class Registry
|
|
5
5
|
def initialize(configuration:, &container_factory)
|
|
6
6
|
@configuration = configuration
|
|
7
|
-
@containers =
|
|
7
|
+
@containers = Hash.new
|
|
8
8
|
@container_factory = container_factory || method(:build_container)
|
|
9
9
|
end
|
|
10
10
|
|
|
@@ -26,8 +26,10 @@ module Torikago
|
|
|
26
26
|
name: definition.name,
|
|
27
27
|
module_root: definition.root,
|
|
28
28
|
entrypoint: definition.entrypoint,
|
|
29
|
+
rails_engine: definition.rails_engine,
|
|
29
30
|
setup: definition.setup,
|
|
30
|
-
gemfile: definition.gemfile
|
|
31
|
+
gemfile: definition.gemfile,
|
|
32
|
+
registered_roots: @configuration.each_definition.map(&:root)
|
|
31
33
|
)
|
|
32
34
|
end
|
|
33
35
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
|
|
3
|
+
module Torikago
|
|
4
|
+
# Resolves top-level constants from the main Box while keeping constants
|
|
5
|
+
# owned by registered module roots private to their respective module Box.
|
|
6
|
+
class RootConstantResolver
|
|
7
|
+
UNRESOLVED = Object.new.freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :unresolved
|
|
10
|
+
|
|
11
|
+
def initialize(registered_roots:, main_box: Object)
|
|
12
|
+
@registered_roots = registered_roots.map { |root| normalize_path(root) }
|
|
13
|
+
@main_box = main_box
|
|
14
|
+
@unresolved = UNRESOLVED
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def resolve(name)
|
|
18
|
+
return unresolved unless main_box.const_defined?(name, false)
|
|
19
|
+
return unresolved if registered_source?(main_box.autoload?(name, false))
|
|
20
|
+
|
|
21
|
+
source_location = main_box.const_source_location(name, false)
|
|
22
|
+
return unresolved if registered_source?(source_location&.first)
|
|
23
|
+
|
|
24
|
+
constant = main_box.const_get(name, false)
|
|
25
|
+
return unresolved if constant.is_a?(Module) && registered_descendant?(constant)
|
|
26
|
+
|
|
27
|
+
constant
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
attr_reader :main_box, :registered_roots
|
|
33
|
+
|
|
34
|
+
def registered_source?(source_path)
|
|
35
|
+
return false unless source_path
|
|
36
|
+
|
|
37
|
+
normalized_source = normalize_path(source_path)
|
|
38
|
+
registered_roots.any? do |root|
|
|
39
|
+
normalized_source == root || normalized_source.start_with?("#{root}#{File::SEPARATOR}")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def registered_descendant?(namespace, visited = {})
|
|
44
|
+
return false if visited[namespace]
|
|
45
|
+
|
|
46
|
+
visited[namespace] = true
|
|
47
|
+
namespace.constants(false).any? do |name|
|
|
48
|
+
autoload_path = namespace.autoload?(name, false)
|
|
49
|
+
next true if registered_source?(autoload_path)
|
|
50
|
+
|
|
51
|
+
source_location = namespace.const_source_location(name, false)
|
|
52
|
+
next true if registered_source?(source_location&.first)
|
|
53
|
+
next false if autoload_path
|
|
54
|
+
|
|
55
|
+
child = namespace.const_get(name, false)
|
|
56
|
+
child.is_a?(Module) && registered_descendant?(child, visited)
|
|
57
|
+
rescue NameError
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def normalize_path(path)
|
|
63
|
+
expanded_path = Pathname(path).expand_path
|
|
64
|
+
expanded_path.exist? ? expanded_path.realpath.to_s : expanded_path.to_s
|
|
65
|
+
rescue SystemCallError
|
|
66
|
+
expanded_path.to_s
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/torikago/version.rb
CHANGED
data/lib/torikago.rb
CHANGED
|
@@ -2,10 +2,14 @@ require_relative "torikago/configuration"
|
|
|
2
2
|
require_relative "torikago/current_execution"
|
|
3
3
|
require_relative "torikago/checker"
|
|
4
4
|
require_relative "torikago/cli"
|
|
5
|
+
require_relative "torikago/controller_endpoint"
|
|
6
|
+
require_relative "torikago/root_constant_resolver"
|
|
5
7
|
require_relative "torikago/engine_container"
|
|
6
8
|
require_relative "torikago/errors"
|
|
9
|
+
require_relative "torikago/invocation"
|
|
7
10
|
require_relative "torikago/gateway"
|
|
8
11
|
require_relative "torikago/package_api_updater"
|
|
12
|
+
require_relative "torikago/rack_endpoint"
|
|
9
13
|
require_relative "torikago/registry"
|
|
10
14
|
require_relative "torikago/version"
|
|
11
15
|
|
|
@@ -31,6 +35,10 @@ module Torikago
|
|
|
31
35
|
@registry ||= Registry.new(configuration: configuration)
|
|
32
36
|
end
|
|
33
37
|
|
|
38
|
+
def action(module_name, controller_name, action_name)
|
|
39
|
+
ControllerEndpoint.new(module_name, controller_name, action_name)
|
|
40
|
+
end
|
|
41
|
+
|
|
34
42
|
def version
|
|
35
43
|
VERSION
|
|
36
44
|
end
|