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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4067561b7267c62468f0ba974be7a68e536fa620cc25c7356ba643f774bc14d9
|
|
4
|
+
data.tar.gz: 5ceb5e3693ba1a767a2e93f439384df0d568306beabbbb62efb9538dc807dc79
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9bbe65e72b673873e80dfc51c64de86a1c3240976ebad98f964d47abdef046b99058dc8f33365d9aba9714e6e2a92685db98737084e7b81bb34cc17321fe37fa
|
|
7
|
+
data.tar.gz: 6968dad15fcdc9e710819468925d4e5da7778b90d53615d31308fcba9f58a9f99d7a6d6f9082d6091618c480cef96083a7476ea013c07c28d3418e55f0f4d9ef
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 se4weed
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.ja.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# torikago
|
|
2
|
+
|
|
3
|
+
`torikago`は、Railsのmodular monolithでmoduleごとの実行境界を扱うためのgemです。`packwerk`や`Rails::Engine`で構造上の境界を作るだけでなく、`Ruby::Box`を使って実行時の境界も強くすることを目指しています。
|
|
4
|
+
|
|
5
|
+
`torikago`では、module間の呼び出しを`Torikago::Gateway.call(...)`に集約し、各moduleのPackage APIと呼び出し可能なmoduleを事前に定義します。これによって、意図していないmodule間参照を実行時に防ぎやすくします。
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## 設定例
|
|
10
|
+
|
|
11
|
+
Rails app側でmoduleを登録します。
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
Torikago.configure do |config|
|
|
15
|
+
config.register(
|
|
16
|
+
:foo,
|
|
17
|
+
root: Rails.root.join("modules/foo"),
|
|
18
|
+
entrypoint: "app/package_api", # optional
|
|
19
|
+
setup: "config/box_setup.rb", # optional
|
|
20
|
+
gemfile: "Gemfile" # optional
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`config.register`で指定できる主な項目は次のとおりです。
|
|
26
|
+
|
|
27
|
+
- `root`
|
|
28
|
+
- moduleのルートディレクトリ
|
|
29
|
+
- `entrypoint`
|
|
30
|
+
- public APIを探索するディレクトリ、またはその配下のファイル
|
|
31
|
+
- 未指定時は`app/package_api`
|
|
32
|
+
- `setup`
|
|
33
|
+
- Box boot前に読み込むsetup hook
|
|
34
|
+
- monkey patchやbox固有の初期化処理に使う
|
|
35
|
+
- `gemfile`
|
|
36
|
+
- そのBoxで優先したいgem require pathを解決するためのGemfile
|
|
37
|
+
- Box cold boot時に、解決したrequire pathをそのBoxの`load_path`先頭側へ追加する
|
|
38
|
+
- main box側のgem activationに頼らず、module codeからmodule-localなgem versionを`require`できるようにするための設定
|
|
39
|
+
|
|
40
|
+
module側では、公開するPackage APIと、どのmoduleから呼べるかを定義します。
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
exports:
|
|
44
|
+
Foo::ListProductsQuery:
|
|
45
|
+
allowed_callers:
|
|
46
|
+
- baz
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
module自身からの呼び出しとmain boxからの呼び出しは許可されます。`allowed_callers`は、他moduleからの参照だけを制限します。
|
|
50
|
+
|
|
51
|
+
呼び出し側では、対象のclass名を使って呼び出します。
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
Torikago::Gateway.call("Foo::ListProductsQuery")
|
|
55
|
+
|
|
56
|
+
# 引数を渡す場合
|
|
57
|
+
Torikago::Gateway.call("Bar::SubmitOrderCommand", title: "Book")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`Torikago::Gateway.call(...)`は、class名から対象moduleを解決し、そのmoduleのexportされたPackage API定義を確認したうえで、対象Boxの中で`new.call(...)`を実行します。
|
|
61
|
+
|
|
62
|
+
## Example app
|
|
63
|
+
|
|
64
|
+
`example/rails-modular-monolith/`に、最小のRails example appが入っています。
|
|
65
|
+
|
|
66
|
+
## 使い方
|
|
67
|
+
|
|
68
|
+
### gem本体のテスト
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
bundle exec rake test
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### example appのテスト
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
cd example/rails-modular-monolith
|
|
78
|
+
RUBY_BOX=1 bundle exec rails test
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### example appの起動
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
cd example/rails-modular-monolith
|
|
85
|
+
RUBY_BOX=1 bundle exec rails s
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`Ruby::Box`を実際に有効にするには`RUBY_BOX=1`が必要です。
|
|
89
|
+
|
|
90
|
+
## CLI
|
|
91
|
+
|
|
92
|
+
`exe/torikago`からCLIを利用できます。
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
bundle exec ruby exe/torikago --help
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
主なコマンド:
|
|
99
|
+
|
|
100
|
+
- `torikago init`
|
|
101
|
+
- 対話式で`package_api.yml`と`config/initializers/torikago.rb`を生成する
|
|
102
|
+
- `torikago check`
|
|
103
|
+
- `Gateway.call`とmanifestの整合性を検証する
|
|
104
|
+
- `torikago update-package-api [BOX]`
|
|
105
|
+
- 設定済みentrypointから`package_api.yml`を更新する
|
|
106
|
+
|
|
107
|
+
`torikago check`は、`Torikago::Gateway.call("...")`の呼び出しを走査し、
|
|
108
|
+
|
|
109
|
+
- manifestにそのclassが定義されているか
|
|
110
|
+
- 呼び出し元moduleが`allowed_callers`に含まれているか
|
|
111
|
+
- manifest上のclassに対応するファイルが存在するか
|
|
112
|
+
|
|
113
|
+
を確認します。
|
|
114
|
+
|
|
115
|
+
## `RUBY_BOX=1`とbootについて
|
|
116
|
+
|
|
117
|
+
現状のexample appでは、`RUBY_BOX=1`下でRails bootを安定させるために、いくつか回避策を入れています。
|
|
118
|
+
|
|
119
|
+
- Bundler pluginを無効化する
|
|
120
|
+
- `tmpdir`を早めに読み込む
|
|
121
|
+
- `RUBY_BOX=1`時は`Bundler.require(*Rails.groups)`を避け、必要なgemを明示的に`require`する
|
|
122
|
+
|
|
123
|
+
これらは`torikago`の最終形というより、現時点でexampleを安定して動かすための実務的なworkaroundです。
|
|
124
|
+
|
|
125
|
+
## 現時点の制約
|
|
126
|
+
|
|
127
|
+
- 初回のBox bootは重い
|
|
128
|
+
- cold boot時は数秒オーダーのコストが出ることがある
|
|
129
|
+
- `Ruby::Box`自体がexperimental
|
|
130
|
+
- segfaultや不安定さに遭遇することがある
|
|
131
|
+
- Railsや一部gemとの相性問題がある
|
|
132
|
+
- とくにVM全体へ影響するglobal-effect gemは、きれいに分離しきれない
|
|
133
|
+
- full `Rails::Engine` confinementを素直にやるのはまだ難しい
|
|
134
|
+
|
|
135
|
+
代表的な例外:
|
|
136
|
+
|
|
137
|
+
- `Torikago::DependencyError`
|
|
138
|
+
- 許可されていないmodule間参照
|
|
139
|
+
- `Torikago::PublicApiError`
|
|
140
|
+
- manifestに宣言されていないPackage APIの呼び出し
|
|
141
|
+
- `Torikago::GemfileOverrideError`
|
|
142
|
+
- Box用Gemfileの解決やactivateに失敗したとき
|
|
143
|
+
|
|
144
|
+
そのため、現時点の`torikago`は「すぐ本番導入できる完成品」というより、modular monolithの実行境界をどこまで強くできるかを探る実装です。
|
data/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# torikago
|
|
2
|
+
|
|
3
|
+
`torikago` is a gem for introducing per-module runtime boundaries to Rails modular monoliths. It aims to strengthen runtime isolation with `Ruby::Box`, in addition to the structural boundaries you can already get from tools like `packwerk` and `Rails::Engine`.
|
|
4
|
+
|
|
5
|
+
With `torikago`, module-to-module calls are funneled through `Torikago::Gateway.call(...)`, and each module declares which Package APIs it exposes and which modules may call them. This makes it easier to prevent unintended cross-module references at runtime.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Configuration example
|
|
10
|
+
|
|
11
|
+
Register modules from your Rails app:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
Torikago.configure do |config|
|
|
15
|
+
config.register(
|
|
16
|
+
:foo,
|
|
17
|
+
root: Rails.root.join("modules/foo"),
|
|
18
|
+
entrypoint: "app/package_api", # optional
|
|
19
|
+
setup: "config/box_setup.rb", # optional
|
|
20
|
+
gemfile: "Gemfile" # optional
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The main `config.register` options are:
|
|
26
|
+
|
|
27
|
+
- `root`
|
|
28
|
+
- the module root directory
|
|
29
|
+
- `entrypoint`
|
|
30
|
+
- the directory, or file under that directory, used to discover public APIs
|
|
31
|
+
- defaults to `app/package_api`
|
|
32
|
+
- `setup`
|
|
33
|
+
- a setup hook loaded before Box boot completes
|
|
34
|
+
- useful for monkey patches or Box-specific initialization
|
|
35
|
+
- `gemfile`
|
|
36
|
+
- a Gemfile used to resolve Box-specific gem require paths
|
|
37
|
+
- during Box cold boot, the resolved require paths are prepended to that Box's `load_path`
|
|
38
|
+
- this is intended to let module code `require` module-local gem versions without relying on main-box gem activation
|
|
39
|
+
|
|
40
|
+
On the module side, declare the Package APIs you expose and which modules may call them:
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
exports:
|
|
44
|
+
Foo::ListProductsQuery:
|
|
45
|
+
allowed_callers:
|
|
46
|
+
- baz
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Calls from the module itself and from the main box are allowed implicitly. `allowed_callers` only restricts calls coming from other modules.
|
|
50
|
+
|
|
51
|
+
Call a Package API by class name:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
Torikago::Gateway.call("Foo::ListProductsQuery")
|
|
55
|
+
|
|
56
|
+
# with arguments
|
|
57
|
+
Torikago::Gateway.call("Bar::SubmitOrderCommand", title: "Book")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`Torikago::Gateway.call(...)` resolves the target module from the class name, checks the exported Package API declaration for that module, and then runs `new.call(...)` inside the target Box.
|
|
61
|
+
|
|
62
|
+
## Example app
|
|
63
|
+
|
|
64
|
+
A minimal Rails example app lives in `example/rails-modular-monolith/`.
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
### Run gem tests
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
bundle exec rake test
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Run example app tests
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
cd example/rails-modular-monolith
|
|
78
|
+
RUBY_BOX=1 bundle exec rails test
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Start the example app
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
cd example/rails-modular-monolith
|
|
85
|
+
RUBY_BOX=1 bundle exec rails s
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`RUBY_BOX=1` is required to actually enable `Ruby::Box`.
|
|
89
|
+
|
|
90
|
+
## CLI
|
|
91
|
+
|
|
92
|
+
Use the CLI via `exe/torikago`:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
bundle exec ruby exe/torikago --help
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Main commands:
|
|
99
|
+
|
|
100
|
+
- `torikago init`
|
|
101
|
+
- interactively generate `package_api.yml` files and `config/initializers/torikago.rb`
|
|
102
|
+
- `torikago check`
|
|
103
|
+
- validate `Gateway.call` usage against manifests
|
|
104
|
+
- `torikago update-package-api [BOX]`
|
|
105
|
+
- regenerate `package_api.yml` from the configured entrypoint
|
|
106
|
+
|
|
107
|
+
`torikago check` scans `Torikago::Gateway.call("...")` usage and verifies:
|
|
108
|
+
|
|
109
|
+
- the class is declared in a manifest
|
|
110
|
+
- the caller module is included in `allowed_callers`
|
|
111
|
+
- the manifest entry has a matching implementation file
|
|
112
|
+
|
|
113
|
+
## About `RUBY_BOX=1` and boot
|
|
114
|
+
|
|
115
|
+
The current example app includes a few practical boot workarounds to keep Rails startup stable under `RUBY_BOX=1`:
|
|
116
|
+
|
|
117
|
+
- disable Bundler plugins
|
|
118
|
+
- load `tmpdir` early
|
|
119
|
+
- avoid `Bundler.require(*Rails.groups)` under `RUBY_BOX=1` and require the needed gems explicitly
|
|
120
|
+
|
|
121
|
+
These are pragmatic workarounds for the current example app, not a finalized long-term contract for `torikago`.
|
|
122
|
+
|
|
123
|
+
## Current limitations
|
|
124
|
+
|
|
125
|
+
- Initial Box boot is slow
|
|
126
|
+
- cold boot can take several seconds
|
|
127
|
+
- `Ruby::Box` itself is still experimental
|
|
128
|
+
- segfaults and instability can happen
|
|
129
|
+
- Some gems do not cooperate well with this model
|
|
130
|
+
- especially global-effect gems that influence the whole VM
|
|
131
|
+
- Full `Rails::Engine` confinement is still difficult to do cleanly
|
|
132
|
+
|
|
133
|
+
Common errors:
|
|
134
|
+
|
|
135
|
+
- `Torikago::DependencyError`
|
|
136
|
+
- an unauthorized cross-module reference
|
|
137
|
+
- `Torikago::PublicApiError`
|
|
138
|
+
- calling a Package API that is not declared in the manifest
|
|
139
|
+
- `Torikago::GemfileOverrideError`
|
|
140
|
+
- failure while resolving or activating a Box-specific Gemfile override
|
|
141
|
+
|
|
142
|
+
So, at the moment, `torikago` is better understood as an implementation exploring how far runtime boundaries in a modular monolith can be pushed, rather than a fully production-ready finished product.
|
data/exe/torikago
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
require "yaml"
|
|
3
|
+
|
|
4
|
+
module Torikago
|
|
5
|
+
# Lightweight static checks for explicit Gateway calls and package API
|
|
6
|
+
# manifests. This is intentionally conservative; runtime enforcement still
|
|
7
|
+
# lives in Gateway.
|
|
8
|
+
class Checker
|
|
9
|
+
Result = Struct.new(
|
|
10
|
+
:errors,
|
|
11
|
+
:scanned_file_count,
|
|
12
|
+
:gateway_call_count,
|
|
13
|
+
:manifest_count,
|
|
14
|
+
keyword_init: true
|
|
15
|
+
) do
|
|
16
|
+
def ok?
|
|
17
|
+
errors.empty?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
CALL_PATTERN = /
|
|
22
|
+
Torikago::Gateway\.call\(
|
|
23
|
+
\s*
|
|
24
|
+
["'](?<class_name>[A-Z][A-Za-z0-9:]+)["']
|
|
25
|
+
/x.freeze
|
|
26
|
+
|
|
27
|
+
def initialize(configuration:, source_roots:)
|
|
28
|
+
@configuration = configuration
|
|
29
|
+
@source_roots = Array(source_roots).map { |root| Pathname(root) }
|
|
30
|
+
@manifests = {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def call
|
|
34
|
+
errors = []
|
|
35
|
+
gateway_call_count = 0
|
|
36
|
+
scanned_files = source_files
|
|
37
|
+
|
|
38
|
+
scanned_files.each do |path|
|
|
39
|
+
gateway_call_count += scan_gateway_calls(path, errors)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
manifest_count = 0
|
|
43
|
+
configuration.each_definition do |definition|
|
|
44
|
+
manifest_count += 1
|
|
45
|
+
validate_manifest_entries(definition, errors)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Result.new(
|
|
49
|
+
errors: errors,
|
|
50
|
+
scanned_file_count: scanned_files.size,
|
|
51
|
+
gateway_call_count: gateway_call_count,
|
|
52
|
+
manifest_count: manifest_count
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
attr_reader :configuration, :manifests, :source_roots
|
|
59
|
+
|
|
60
|
+
def source_files
|
|
61
|
+
source_roots.flat_map { |root| Dir[root.join("**/*.rb").to_s] }.sort.uniq
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def scan_gateway_calls(path, errors)
|
|
65
|
+
content = File.read(path)
|
|
66
|
+
call_count = 0
|
|
67
|
+
content.to_enum(:scan, CALL_PATTERN).each do
|
|
68
|
+
call_count += 1
|
|
69
|
+
class_name = Regexp.last_match[:class_name]
|
|
70
|
+
# Public API names are expected to start with their owning module
|
|
71
|
+
# namespace, e.g. Foo::ListProductsQuery targets the :foo box.
|
|
72
|
+
target_box = infer_box_name(class_name)
|
|
73
|
+
manifest_entry = public_api_entry_for(target_box, class_name)
|
|
74
|
+
caller_box = infer_caller_box_from_path(path)
|
|
75
|
+
|
|
76
|
+
if manifest_entry.nil?
|
|
77
|
+
errors << "#{path}: #{class_name} is not declared in #{target_box}/package_api.yml exports"
|
|
78
|
+
next
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
next if caller_box.nil?
|
|
82
|
+
next if caller_box == target_box
|
|
83
|
+
|
|
84
|
+
allowed_callers = Array(manifest_entry["allowed_callers"]).map(&:to_s)
|
|
85
|
+
next if allowed_callers.include?(caller_box.to_s)
|
|
86
|
+
|
|
87
|
+
errors << "#{path}: #{caller_box} is not allowed to call #{class_name}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
call_count
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def validate_manifest_entries(definition, errors)
|
|
94
|
+
exported_package_apis(load_manifest(definition)).each_key do |class_name|
|
|
95
|
+
# The manifest is the contract, but the checker also catches stale
|
|
96
|
+
# entries whose implementation file has been deleted or moved.
|
|
97
|
+
expected_path = expected_public_api_path(definition, class_name)
|
|
98
|
+
next if expected_path.exist?
|
|
99
|
+
|
|
100
|
+
errors << "#{definition.root.join('package_api.yml')}: #{class_name} does not have a matching file at #{expected_path}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def public_api_entry_for(box_name, class_name)
|
|
105
|
+
exported_package_apis(load_manifest(configuration.fetch(box_name)))[class_name]
|
|
106
|
+
rescue KeyError
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def load_manifest(definition)
|
|
111
|
+
manifests.fetch(definition.name) do
|
|
112
|
+
manifest_path = definition.root.join("package_api.yml")
|
|
113
|
+
manifests[definition.name] =
|
|
114
|
+
if manifest_path.exist?
|
|
115
|
+
YAML.safe_load(manifest_path.read, permitted_classes: [], aliases: false) || {}
|
|
116
|
+
else
|
|
117
|
+
{}
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def expected_public_api_path(definition, class_name)
|
|
123
|
+
relative_path = class_name.split("::").map { |segment| underscore(segment) }.join("/")
|
|
124
|
+
public_api_root(definition).join("#{relative_path}.rb")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def infer_box_name(class_name)
|
|
128
|
+
class_name.split("::").first.downcase.to_sym
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def infer_caller_box_from_path(path)
|
|
132
|
+
# This path heuristic matches the Rails modular-monolith layout that
|
|
133
|
+
# torikago is designed around: modules/<box-name>/...
|
|
134
|
+
path.match(%r{/modules/(?<box>[a-z0-9_]+)/})&.named_captures&.fetch("box", nil)&.to_sym
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def exported_package_apis(manifest)
|
|
138
|
+
manifest.fetch("exports") { manifest.fetch("public_api", {}) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def underscore(name)
|
|
142
|
+
word = name.gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
|
143
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
|
144
|
+
word.downcase
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def public_api_root(definition)
|
|
148
|
+
return definition.root.join("app/package_api") if definition.entrypoint.nil?
|
|
149
|
+
|
|
150
|
+
# entrypoint may point at either a directory or a single boot file. For a
|
|
151
|
+
# file entrypoint, package API implementations live next to that file.
|
|
152
|
+
candidate = definition.root.join(definition.entrypoint)
|
|
153
|
+
return candidate if candidate.directory?
|
|
154
|
+
return candidate unless candidate.extname == ".rb"
|
|
155
|
+
|
|
156
|
+
candidate.dirname
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|