torikago 0.0.1
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.ja.md +144 -0
- data/README.md +142 -0
- data/exe/torikago +8 -0
- data/lib/torikago/checker.rb +159 -0
- data/lib/torikago/cli.rb +235 -0
- data/lib/torikago/configuration.rb +45 -0
- data/lib/torikago/current_execution.rb +23 -0
- data/lib/torikago/engine_container.rb +340 -0
- data/lib/torikago/errors.rb +16 -0
- data/lib/torikago/gateway.rb +86 -0
- data/lib/torikago/package_api_updater.rb +101 -0
- data/lib/torikago/registry.rb +34 -0
- data/lib/torikago/version.rb +3 -0
- data/lib/torikago.rb +71 -0
- data/test/test_helper.rb +9 -0
- data/test/torikago/checker.rb +164 -0
- data/test/torikago/cli.rb +143 -0
- data/test/torikago/configuration.rb +74 -0
- data/test/torikago/engine_container.rb +549 -0
- data/test/torikago/gateway.rb +155 -0
- data/test/torikago/package_api_updater.rb +94 -0
- data/test/torikago/registry.rb +83 -0
- data/test/torikago/version.rb +11 -0
- data/test/torikago.rb +110 -0
- metadata +73 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
require "yaml"
|
|
3
|
+
|
|
4
|
+
module Torikago
|
|
5
|
+
# Runtime entrypoint for all cross-module calls. It validates the package API
|
|
6
|
+
# manifest before dispatching into the target module container.
|
|
7
|
+
class Gateway
|
|
8
|
+
class << self
|
|
9
|
+
def call(...)
|
|
10
|
+
Torikago.gateway.call(...)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(registry:, configuration:, manifest_loader: nil)
|
|
15
|
+
@registry = registry
|
|
16
|
+
@configuration = configuration
|
|
17
|
+
@manifest_loader = manifest_loader || method(:load_manifest)
|
|
18
|
+
@manifests = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(public_api_class_name, *args, **kwargs)
|
|
22
|
+
target_module = infer_target_module(public_api_class_name)
|
|
23
|
+
caller_module = CurrentExecution.current_box
|
|
24
|
+
|
|
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)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :configuration, :manifest_loader, :manifests, :registry
|
|
34
|
+
|
|
35
|
+
def validate_public_api!(target_module, public_api_class_name, caller_module)
|
|
36
|
+
target_name = target_module.to_sym
|
|
37
|
+
definition = configuration.fetch(target_name)
|
|
38
|
+
manifest = manifests.fetch(target_name) do
|
|
39
|
+
manifests[target_name] = manifest_loader.call(definition)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
public_api_entry = exported_package_apis(manifest).fetch(public_api_class_name, nil)
|
|
43
|
+
if public_api_entry.nil?
|
|
44
|
+
raise PublicApiError, "package api export not declared for #{target_name}: #{public_api_class_name}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
return if caller_module.nil?
|
|
48
|
+
|
|
49
|
+
caller_name = caller_module.to_sym
|
|
50
|
+
# A module may always call its own public API; allowed_callers only governs
|
|
51
|
+
# calls crossing from one module box into another.
|
|
52
|
+
return if caller_name == target_name
|
|
53
|
+
return if dependency_allowed?(public_api_entry, caller_name)
|
|
54
|
+
|
|
55
|
+
raise DependencyError,
|
|
56
|
+
"module dependency not allowed: #{caller_name} -> #{target_name}##{public_api_class_name}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def load_manifest(definition)
|
|
60
|
+
manifest_path = package_api_manifest_path(definition)
|
|
61
|
+
|
|
62
|
+
unless manifest_path.exist?
|
|
63
|
+
raise DependencyError,
|
|
64
|
+
"package_api manifest not found for #{definition.name}: #{manifest_path}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
YAML.safe_load(manifest_path.read, permitted_classes: [], aliases: false) || {}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def package_api_manifest_path(definition)
|
|
71
|
+
Pathname(definition.root).join("package_api.yml")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def infer_target_module(public_api_class_name)
|
|
75
|
+
public_api_class_name.split("::").first.downcase.to_sym
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def exported_package_apis(manifest)
|
|
79
|
+
manifest.fetch("exports") { manifest.fetch("public_api", {}) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def dependency_allowed?(public_api_entry, caller_name)
|
|
83
|
+
Array(public_api_entry["allowed_callers"]).map(&:to_sym).include?(caller_name)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
require "yaml"
|
|
3
|
+
|
|
4
|
+
module Torikago
|
|
5
|
+
# Regenerates package_api.yml from files under a module's public API
|
|
6
|
+
# entrypoint while preserving caller permissions already chosen by humans.
|
|
7
|
+
class PackageApiUpdater
|
|
8
|
+
def initialize(configuration:)
|
|
9
|
+
@configuration = configuration
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(module_name = nil)
|
|
13
|
+
definitions_for(module_name).each_with_object({}) do |definition, updates|
|
|
14
|
+
manifest_path = definition.root.join("package_api.yml")
|
|
15
|
+
existing_manifest = load_manifest(manifest_path)
|
|
16
|
+
updated_manifest = build_manifest(definition, existing_manifest)
|
|
17
|
+
|
|
18
|
+
manifest_path.write(render_manifest(updated_manifest))
|
|
19
|
+
updates[definition.name] = manifest_path
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :configuration
|
|
26
|
+
|
|
27
|
+
def definitions_for(module_name)
|
|
28
|
+
return configuration.each_definition.to_a if module_name.nil?
|
|
29
|
+
|
|
30
|
+
[configuration.fetch(module_name)]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def load_manifest(path)
|
|
34
|
+
return {} unless path.exist?
|
|
35
|
+
|
|
36
|
+
YAML.safe_load(path.read, permitted_classes: [], aliases: false) || {}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_manifest(definition, existing_manifest)
|
|
40
|
+
existing_public_api = exported_package_apis(existing_manifest)
|
|
41
|
+
|
|
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, {})
|
|
44
|
+
|
|
45
|
+
# update-package-api owns discovery, not policy. Keep allowed_callers so
|
|
46
|
+
# the command does not silently widen or narrow module dependencies.
|
|
47
|
+
entries[class_name] = {
|
|
48
|
+
"allowed_callers" => Array(existing_entry["allowed_callers"]).map(&:to_s)
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
{ "exports" => public_api_entries }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def exported_package_apis(manifest)
|
|
56
|
+
manifest.fetch("exports") { manifest.fetch("public_api", {}) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def render_manifest(manifest)
|
|
60
|
+
<<~YAML
|
|
61
|
+
# This file declares the Package APIs exported by this module.
|
|
62
|
+
#
|
|
63
|
+
# Each key under exports is a class that may be called through:
|
|
64
|
+
#
|
|
65
|
+
# Torikago::Gateway.call("ModuleName::SomeQuery")
|
|
66
|
+
#
|
|
67
|
+
# allowed_callers lists other modules that may call that export. The
|
|
68
|
+
# host app and the module itself are allowed implicitly.
|
|
69
|
+
#
|
|
70
|
+
YAML
|
|
71
|
+
.then { |header| header + YAML.dump(manifest) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def discover_public_api_classes(definition)
|
|
75
|
+
Dir[public_api_root(definition).join("**/*.rb").to_s].sort.map do |path|
|
|
76
|
+
relative_path = Pathname(path).relative_path_from(public_api_root(definition)).to_s
|
|
77
|
+
class_name_from(relative_path)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def public_api_root(definition)
|
|
82
|
+
return definition.root.join("app/package_api") if definition.entrypoint.nil?
|
|
83
|
+
|
|
84
|
+
# Match EngineContainer and Checker: directory entrypoints are roots,
|
|
85
|
+
# while file entrypoints imply implementations live beside the file.
|
|
86
|
+
candidate = definition.root.join(definition.entrypoint)
|
|
87
|
+
return candidate if candidate.directory?
|
|
88
|
+
return candidate unless candidate.extname == ".rb"
|
|
89
|
+
|
|
90
|
+
candidate.dirname
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def class_name_from(relative_path)
|
|
94
|
+
relative_path.delete_suffix(".rb").split("/").map { |segment| camelize(segment) }.join("::")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def camelize(segment)
|
|
98
|
+
segment.split("_").map(&:capitalize).join
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Torikago
|
|
2
|
+
# Lazily builds one EngineContainer per registered module and reuses it for
|
|
3
|
+
# subsequent Gateway calls.
|
|
4
|
+
class Registry
|
|
5
|
+
def initialize(configuration:, &container_factory)
|
|
6
|
+
@configuration = configuration
|
|
7
|
+
@containers = {}
|
|
8
|
+
@container_factory = container_factory || method(:build_container)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def resolve(name)
|
|
12
|
+
normalized_name = name.to_sym
|
|
13
|
+
|
|
14
|
+
@containers.fetch(normalized_name) do
|
|
15
|
+
definition = @configuration.fetch(name)
|
|
16
|
+
container = @container_factory.call(definition)
|
|
17
|
+
|
|
18
|
+
@containers[definition.name] = container
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def build_container(definition)
|
|
25
|
+
EngineContainer.new(
|
|
26
|
+
name: definition.name,
|
|
27
|
+
module_root: definition.root,
|
|
28
|
+
entrypoint: definition.entrypoint,
|
|
29
|
+
setup: definition.setup,
|
|
30
|
+
gemfile: definition.gemfile
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/torikago.rb
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require_relative "torikago/configuration"
|
|
2
|
+
require_relative "torikago/current_execution"
|
|
3
|
+
require_relative "torikago/checker"
|
|
4
|
+
require_relative "torikago/cli"
|
|
5
|
+
require_relative "torikago/engine_container"
|
|
6
|
+
require_relative "torikago/errors"
|
|
7
|
+
require_relative "torikago/gateway"
|
|
8
|
+
require_relative "torikago/package_api_updater"
|
|
9
|
+
require_relative "torikago/registry"
|
|
10
|
+
require_relative "torikago/version"
|
|
11
|
+
|
|
12
|
+
module Torikago
|
|
13
|
+
class << self
|
|
14
|
+
def configuration
|
|
15
|
+
@configuration ||= Configuration.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure
|
|
19
|
+
yield(configuration)
|
|
20
|
+
# Rebuild runtime collaborators after configuration changes so newly
|
|
21
|
+
# registered modules are visible to future Gateway calls.
|
|
22
|
+
reset_runtime_state!
|
|
23
|
+
configuration
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def gateway
|
|
27
|
+
@gateway ||= Gateway.new(registry: registry, configuration: configuration)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def registry
|
|
31
|
+
@registry ||= Registry.new(configuration: configuration)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def version
|
|
35
|
+
VERSION
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def rails_app?
|
|
41
|
+
return false unless defined?(Rails)
|
|
42
|
+
return false unless Rails.respond_to?(:application)
|
|
43
|
+
|
|
44
|
+
!Rails.application.nil?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def ruby_box_enabled?
|
|
48
|
+
ENV["RUBY_BOX"] == "1"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def warn_if_ruby_box_is_disabled_in_rails!
|
|
52
|
+
return unless rails_app?
|
|
53
|
+
return if ruby_box_enabled?
|
|
54
|
+
|
|
55
|
+
# The gem can run without Ruby::Box for local development, but a Rails app
|
|
56
|
+
# using torikago usually expects runtime isolation to be active.
|
|
57
|
+
warn "[warn] torikago is loaded in a Rails app without RUBY_BOX=1; Ruby::Box isolation is disabled."
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def reset_runtime_state!
|
|
61
|
+
@gateway = nil
|
|
62
|
+
@registry = nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def version
|
|
67
|
+
VERSION
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
Torikago.send(:warn_if_ruby_box_is_disabled_in_rails!)
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
require_relative "../test_helper"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
class TorikagoCheckerTest < Minitest::Test
|
|
6
|
+
def test_check_returns_no_errors_for_declared_calls
|
|
7
|
+
with_project do |root, configuration|
|
|
8
|
+
FileUtils.mkdir_p(File.join(root, "modules/foo"))
|
|
9
|
+
File.write(
|
|
10
|
+
File.join(root, "modules/foo/service.rb"),
|
|
11
|
+
<<~RUBY
|
|
12
|
+
class FooService
|
|
13
|
+
def call
|
|
14
|
+
Torikago::Gateway.call("Bar::SubmitOrderCommand")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
RUBY
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
checker = Torikago::Checker.new(
|
|
21
|
+
configuration: configuration,
|
|
22
|
+
source_roots: [File.join(root, "modules")]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
result = Dir.chdir(root) { checker.call }
|
|
26
|
+
|
|
27
|
+
assert result.ok?
|
|
28
|
+
assert_equal [], result.errors
|
|
29
|
+
assert_equal 2, result.scanned_file_count
|
|
30
|
+
assert_equal 1, result.gateway_call_count
|
|
31
|
+
assert_equal 2, result.manifest_count
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_check_reports_undeclared_calls_and_missing_public_api_files
|
|
36
|
+
with_project do |root, configuration|
|
|
37
|
+
FileUtils.mkdir_p(File.join(root, "modules/foo"))
|
|
38
|
+
File.write(
|
|
39
|
+
File.join(root, "modules/foo/service.rb"),
|
|
40
|
+
<<~RUBY
|
|
41
|
+
class FooService
|
|
42
|
+
def call
|
|
43
|
+
Torikago::Gateway.call("Bar::MissingCommand")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
RUBY
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
File.write(
|
|
50
|
+
File.join(root, "modules/bar/package_api.yml"),
|
|
51
|
+
<<~YAML
|
|
52
|
+
exports:
|
|
53
|
+
Bar::MissingCommand:
|
|
54
|
+
allowed_callers:
|
|
55
|
+
- foo
|
|
56
|
+
YAML
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
checker = Torikago::Checker.new(
|
|
60
|
+
configuration: configuration,
|
|
61
|
+
source_roots: [File.join(root, "modules")]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
result = Dir.chdir(root) { checker.call }
|
|
65
|
+
|
|
66
|
+
refute result.ok?
|
|
67
|
+
assert_equal 1, result.errors.size
|
|
68
|
+
assert_match(/matching file/, result.errors.first)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_check_uses_a_configured_entrypoint_directory_when_matching_manifest_files
|
|
73
|
+
with_project(entrypoint: "components/public_api") do |root, configuration|
|
|
74
|
+
FileUtils.mkdir_p(File.join(root, "modules/foo"))
|
|
75
|
+
File.write(
|
|
76
|
+
File.join(root, "modules/foo/service.rb"),
|
|
77
|
+
<<~RUBY
|
|
78
|
+
class FooService
|
|
79
|
+
def call
|
|
80
|
+
Torikago::Gateway.call("Bar::SubmitOrderCommand")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
RUBY
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
checker = Torikago::Checker.new(
|
|
87
|
+
configuration: configuration,
|
|
88
|
+
source_roots: [File.join(root, "modules")]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
result = Dir.chdir(root) { checker.call }
|
|
92
|
+
|
|
93
|
+
assert result.ok?
|
|
94
|
+
assert_equal [], result.errors
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_check_does_not_match_manifest_entries_against_parent_when_configured_entrypoint_directory_is_missing
|
|
99
|
+
Dir.mktmpdir("torikago-checker") do |root|
|
|
100
|
+
foo_root = File.join(root, "modules/foo")
|
|
101
|
+
FileUtils.mkdir_p(File.join(foo_root, "app/models/foo"))
|
|
102
|
+
File.write(File.join(foo_root, "app/models/foo/widget.rb"), "")
|
|
103
|
+
File.write(
|
|
104
|
+
File.join(foo_root, "package_api.yml"),
|
|
105
|
+
<<~YAML
|
|
106
|
+
exports:
|
|
107
|
+
Models::Foo::Widget:
|
|
108
|
+
allowed_callers: []
|
|
109
|
+
YAML
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
configuration = Torikago::Configuration.new
|
|
113
|
+
configuration.register(:foo, root: foo_root, entrypoint: "app/package_api")
|
|
114
|
+
|
|
115
|
+
checker = Torikago::Checker.new(
|
|
116
|
+
configuration: configuration,
|
|
117
|
+
source_roots: [File.join(root, "modules")]
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
result = Dir.chdir(root) { checker.call }
|
|
121
|
+
|
|
122
|
+
refute result.ok?
|
|
123
|
+
assert_equal 1, result.errors.size
|
|
124
|
+
assert_match(/matching file/, result.errors.first)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def with_project(entrypoint: nil)
|
|
131
|
+
Dir.mktmpdir("torikago-checker") do |root|
|
|
132
|
+
foo_root = File.join(root, "modules/foo")
|
|
133
|
+
bar_root = File.join(root, "modules/bar")
|
|
134
|
+
public_api_root = entrypoint ? File.join(bar_root, entrypoint, "bar") : File.join(bar_root, "app/package_api/bar")
|
|
135
|
+
FileUtils.mkdir_p(public_api_root)
|
|
136
|
+
|
|
137
|
+
File.write(
|
|
138
|
+
File.join(bar_root, "package_api.yml"),
|
|
139
|
+
<<~YAML
|
|
140
|
+
exports:
|
|
141
|
+
Bar::SubmitOrderCommand:
|
|
142
|
+
allowed_callers:
|
|
143
|
+
- foo
|
|
144
|
+
YAML
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
File.write(
|
|
148
|
+
File.join(public_api_root, "submit_order_command.rb"),
|
|
149
|
+
<<~RUBY
|
|
150
|
+
class Bar::SubmitOrderCommand
|
|
151
|
+
def call
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
RUBY
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
configuration = Torikago::Configuration.new
|
|
158
|
+
configuration.register(:foo, root: foo_root)
|
|
159
|
+
configuration.register(:bar, root: bar_root, entrypoint: entrypoint)
|
|
160
|
+
|
|
161
|
+
yield root, configuration
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
require_relative "../test_helper"
|
|
2
|
+
require "stringio"
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
class TorikagoCliTest < Minitest::Test
|
|
8
|
+
def test_help_option_prints_usage
|
|
9
|
+
stdout = StringIO.new
|
|
10
|
+
stderr = StringIO.new
|
|
11
|
+
|
|
12
|
+
exit_code = Torikago::CLI.new(stdout: stdout, stderr: stderr).run(["--help"])
|
|
13
|
+
|
|
14
|
+
assert_equal 0, exit_code
|
|
15
|
+
assert_match(/usage: torikago COMMAND/, stdout.string)
|
|
16
|
+
assert_match(/init/, stdout.string)
|
|
17
|
+
assert_match(/configured public API entrypoint/, stdout.string)
|
|
18
|
+
assert_equal "", stderr.string
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_init_interactively_generates_manifests_and_initializer
|
|
22
|
+
Dir.mktmpdir("torikago-init") do |root|
|
|
23
|
+
FileUtils.mkdir_p(File.join(root, "config/initializers"))
|
|
24
|
+
FileUtils.mkdir_p(File.join(root, "modules/foo/app/package_api/foo"))
|
|
25
|
+
FileUtils.mkdir_p(File.join(root, "modules/bar/components/public_api/bar"))
|
|
26
|
+
|
|
27
|
+
File.write(
|
|
28
|
+
File.join(root, "modules/foo/app/package_api/foo/list_products_query.rb"),
|
|
29
|
+
<<~RUBY
|
|
30
|
+
class Foo::ListProductsQuery
|
|
31
|
+
end
|
|
32
|
+
RUBY
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
File.write(
|
|
36
|
+
File.join(root, "modules/bar/components/public_api/bar/submit_order_command.rb"),
|
|
37
|
+
<<~RUBY
|
|
38
|
+
class Bar::SubmitOrderCommand
|
|
39
|
+
end
|
|
40
|
+
RUBY
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
stdin = StringIO.new("modules\ncomponents/public_api\n\nY\n")
|
|
44
|
+
stdout = StringIO.new
|
|
45
|
+
stderr = StringIO.new
|
|
46
|
+
|
|
47
|
+
exit_code = Dir.chdir(root) do
|
|
48
|
+
Torikago::CLI.new(stdin: stdin, stdout: stdout, stderr: stderr).run(["init"])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
assert_equal 0, exit_code
|
|
52
|
+
assert_equal "", stderr.string
|
|
53
|
+
|
|
54
|
+
foo_manifest = YAML.safe_load(File.read(File.join(root, "modules/foo/package_api.yml")))
|
|
55
|
+
bar_manifest = YAML.safe_load(File.read(File.join(root, "modules/bar/package_api.yml")))
|
|
56
|
+
initializer = File.read(File.join(root, "config/initializers/torikago.rb"))
|
|
57
|
+
|
|
58
|
+
assert_equal({ "allowed_callers" => [] }, foo_manifest.dig("exports", "Foo::ListProductsQuery"))
|
|
59
|
+
assert_equal({ "allowed_callers" => [] }, bar_manifest.dig("exports", "Bar::SubmitOrderCommand"))
|
|
60
|
+
assert_match(/root: Rails\.root\.join\("modules\/bar"\)/, initializer)
|
|
61
|
+
assert_match(/entrypoint: "components\/public_api"/, initializer)
|
|
62
|
+
assert_match(/root: Rails\.root\.join\("modules\/foo"\)/, initializer)
|
|
63
|
+
assert_match(/entrypoint: "app\/package_api"/, initializer)
|
|
64
|
+
assert_match(/Run `torikago update-package-api` now\?/, stdout.string)
|
|
65
|
+
assert_match(/updated 2 package_api manifests/, stdout.string)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_check_prints_summary_information
|
|
70
|
+
with_cli_project do |root|
|
|
71
|
+
stdout = StringIO.new
|
|
72
|
+
stderr = StringIO.new
|
|
73
|
+
|
|
74
|
+
exit_code = Dir.chdir(root) do
|
|
75
|
+
Torikago::CLI.new(stdout: stdout, stderr: stderr).run(["check"])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
assert_equal 0, exit_code
|
|
79
|
+
assert_match(/scanned 2 Ruby files/, stdout.string)
|
|
80
|
+
assert_match(/found 1 Gateway\.call usages/, stdout.string)
|
|
81
|
+
assert_match(/validated 2 package_api manifests/, stdout.string)
|
|
82
|
+
assert_equal "", stderr.string
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_update_package_api_prints_updated_count
|
|
87
|
+
with_cli_project do |root|
|
|
88
|
+
stdout = StringIO.new
|
|
89
|
+
stderr = StringIO.new
|
|
90
|
+
|
|
91
|
+
exit_code = Dir.chdir(root) do
|
|
92
|
+
Torikago::CLI.new(stdout: stdout, stderr: stderr).run(["update-package-api", "bar"])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
assert_equal 0, exit_code
|
|
96
|
+
assert_match(/updated .*modules\/bar\/package_api\.yml/, stdout.string)
|
|
97
|
+
assert_match(/updated 1 package_api manifest/, stdout.string)
|
|
98
|
+
assert_equal "", stderr.string
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def with_cli_project
|
|
105
|
+
Dir.mktmpdir("torikago-cli") do |root|
|
|
106
|
+
FileUtils.mkdir_p(File.join(root, "modules/foo"))
|
|
107
|
+
FileUtils.mkdir_p(File.join(root, "modules/bar/app/package_api/bar"))
|
|
108
|
+
|
|
109
|
+
File.write(
|
|
110
|
+
File.join(root, "modules/foo/service.rb"),
|
|
111
|
+
<<~RUBY
|
|
112
|
+
class FooService
|
|
113
|
+
def call
|
|
114
|
+
Torikago::Gateway.call("Bar::SubmitOrderCommand")
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
RUBY
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
File.write(
|
|
121
|
+
File.join(root, "modules/bar/package_api.yml"),
|
|
122
|
+
<<~YAML
|
|
123
|
+
exports:
|
|
124
|
+
Bar::SubmitOrderCommand:
|
|
125
|
+
allowed_callers:
|
|
126
|
+
- foo
|
|
127
|
+
YAML
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
File.write(
|
|
131
|
+
File.join(root, "modules/bar/app/package_api/bar/submit_order_command.rb"),
|
|
132
|
+
<<~RUBY
|
|
133
|
+
class Bar::SubmitOrderCommand
|
|
134
|
+
def call
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
RUBY
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
yield root
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require_relative "../test_helper"
|
|
2
|
+
|
|
3
|
+
class TorikagoConfigurationTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
@configuration = Torikago::Configuration.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def test_register_stores_definition_for_a_module
|
|
9
|
+
@configuration.register(
|
|
10
|
+
:foo,
|
|
11
|
+
root: "/modules/foo",
|
|
12
|
+
entrypoint: "lib/foo/box_runtime.rb",
|
|
13
|
+
setup: "config/box_setup.rb",
|
|
14
|
+
gemfile: "Gemfile"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
definition = @configuration.fetch(:foo)
|
|
18
|
+
|
|
19
|
+
assert_equal :foo, definition.name
|
|
20
|
+
assert_equal "/modules/foo", definition.root.to_s
|
|
21
|
+
assert_equal "lib/foo/box_runtime.rb", definition.entrypoint
|
|
22
|
+
assert_equal "config/box_setup.rb", definition.setup
|
|
23
|
+
assert_equal "Gemfile", definition.gemfile
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_registered_distinguishes_registered_and_unregistered_modules
|
|
27
|
+
@configuration.register(
|
|
28
|
+
:foo,
|
|
29
|
+
root: "/modules/foo",
|
|
30
|
+
entrypoint: "lib/foo/box_runtime.rb"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assert @configuration.registered?(:foo)
|
|
34
|
+
refute @configuration.registered?(:bar)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_module_names_are_normalized_between_string_and_symbol
|
|
38
|
+
@configuration.register(
|
|
39
|
+
"foo",
|
|
40
|
+
root: "/modules/foo",
|
|
41
|
+
entrypoint: "lib/foo/box_runtime.rb"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
assert @configuration.registered?(:foo)
|
|
45
|
+
assert @configuration.registered?("foo")
|
|
46
|
+
assert_equal :foo, @configuration.fetch("foo").name
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_fetch_fails_clearly_for_an_unknown_module
|
|
50
|
+
error = assert_raises(KeyError) do
|
|
51
|
+
@configuration.fetch(:missing)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
assert_match(/missing/, error.message)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_register_rejects_duplicate_module_names
|
|
58
|
+
@configuration.register(
|
|
59
|
+
:foo,
|
|
60
|
+
root: "/modules/foo",
|
|
61
|
+
entrypoint: "lib/foo/box_runtime.rb"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
error = assert_raises(ArgumentError) do
|
|
65
|
+
@configuration.register(
|
|
66
|
+
"foo",
|
|
67
|
+
root: "/modules/another_foo",
|
|
68
|
+
entrypoint: "lib/foo/alternative_runtime.rb"
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
assert_match(/foo/, error.message)
|
|
73
|
+
end
|
|
74
|
+
end
|