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.
@@ -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 call(...)
10
- Torikago.gateway.call(...)
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 call(public_api_class_name, *args, **kwargs)
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
- # Validation happens before resolving the container so denied calls do not
26
- # accidentally boot or load the target module.
27
- validate_public_api!(target_module, public_api_class_name, caller_module)
28
- registry.resolve(target_module).call(public_api_class_name, *args, **kwargs)
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, "package api export not declared for #{target_name}: #{public_api_class_name}"
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: [], aliases: false) || {}
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
- Array(public_api_entry["allowed_callers"]).map(&:to_sym).include?(caller_name)
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 chosen by humans.
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({}) do |definition, updates|
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 {} unless path.exist?
35
+ return Hash.new unless path.exist?
35
36
 
36
- YAML.safe_load(path.read, permitted_classes: [], aliases: false) || {}
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({}) do |class_name, entries|
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 allowed_callers so
46
- # the command does not silently widen or narrow module dependencies.
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
- "allowed_callers" => Array(existing_entry["allowed_callers"]).map(&:to_s)
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.call("ModuleName::SomeQuery")
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Torikago
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
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