hub_kernel 0.16.0 → 0.18.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23120333c3f60fb91e59ac4711d4b94eb15897f85ca13dcd1fd9ed175310f334
4
- data.tar.gz: 107feb16ebe566b00a0005a71d92a378cf1015952823ed676eb24d1669ade53a
3
+ metadata.gz: 026174ef5963518769182562042464615e204f2a32f7e4260c9ed390fc5b2160
4
+ data.tar.gz: 5d2fbdbb14248e8e145eb545246063e883fd196fb68b930180ff9b067a9c341c
5
5
  SHA512:
6
- metadata.gz: 13236d35df7bf32b70c011a3385592f3c07cbd52867c46ff21d9ad323be4bff9eb80644748c1bd07185b2853066ca27d5a7de8575f51c35d6d76d957dcbd3129
7
- data.tar.gz: c92d9a506afca9cd3bacee2f7ff7ece9204e21651b76f784fc1bc08611e58269e3412a1b7ebcc34f478768e1ffb0d8e0393427cc09ad1be85c2a433820562faa
6
+ metadata.gz: 9a7a2a568be438e982a2c0d688fdc61e5fd4ade3b49070d3ffccff71e662b6748185e2e19a1b1beedb6f3c2d14bf5e899faf12598ce7cbcf6e225d7a8c859389
7
+ data.tar.gz: 5f189db08ded136cb2adc871613545ba62f8f163938b63587e47a7382a745d12cceff6606fb7a5f137b4b0db5ca2c09db321cd91dc600140a7c4edde1e2f4c1f
data/README.md CHANGED
@@ -68,7 +68,7 @@ app left unfilled raises `HubKernel::UnwiredPortError`, such as "Supplies' spend
68
68
  recorder is not wired".
69
69
 
70
70
  ### The start check
71
- Every hub that declares a port is on `HubKernel::Hubs.list`. Call
71
+ Every hub that declares a port or exposes a method is on `HubKernel::Hubs.list`. Call
72
72
  `HubKernel::Hubs.check!` after the app fills its ports, and the app refuses to start
73
73
  while any port is unfilled:
74
74
 
@@ -83,6 +83,10 @@ It raises `HubKernel::UnwiredPortError` naming every unfilled port with its hub,
83
83
  line. Running it inside `to_prepare` checks again after every code reload, and a hub
84
84
  declared again on a reload stays on the list once.
85
85
 
86
+ While any hub exposes a method, it also names the permission check or the account scope
87
+ when either is unset or set to something that cannot be called. An app whose hubs expose
88
+ nothing starts without either.
89
+
86
90
  ### The hub check
87
91
  A hub's own test can check that the app filled every one of its ports, so a missing
88
92
  fill shows in the suite rather than on the next start:
@@ -4,26 +4,5 @@ module HubKernel
4
4
 
5
5
  module Authz
6
6
  singleton_class.attr_accessor :check
7
-
8
- module Vocabulary
9
- def gates(*actions)
10
- gated_actions.concat(actions)
11
- end
12
-
13
- def gated_actions
14
- @gated_actions ||= []
15
- end
16
- end
17
-
18
- def self.included(hub)
19
- hub.extend(Vocabulary)
20
- end
21
-
22
- def allowed?(actor, action)
23
- answer = authz.allowed?(actor, action)
24
- raise NonBooleanAnswerError, "allowed? must return true or false, got #{answer.inspect}" unless [ true, false ].include?(answer)
25
-
26
- answer
27
- end
28
7
  end
29
8
  end
@@ -1,15 +1,5 @@
1
1
  module HubKernel
2
2
  module Context
3
3
  singleton_class.attr_accessor :scope
4
-
5
- class Null
6
- def actor
7
- :null_actor
8
- end
9
-
10
- def tenant
11
- :null_tenant
12
- end
13
- end
14
4
  end
15
5
  end
@@ -2,6 +2,7 @@ require "hub_kernel/action"
2
2
  require "hub_kernel/authz"
3
3
  require "hub_kernel/context"
4
4
  require "hub_kernel/ports"
5
+ require "hub_kernel/hubs"
5
6
 
6
7
  module HubKernel
7
8
  class UnexposedMethodError < StandardError; end
@@ -11,6 +12,7 @@ module HubKernel
11
12
  Exposed = Data.define(:name, :takes, :writes)
12
13
 
13
14
  def exposes(name, takes:, writes:)
15
+ Hubs.add(self)
14
16
  exposed_methods[name.to_s] = Exposed.new(name: name, takes: takes, writes: writes)
15
17
  end
16
18
 
@@ -1,3 +1,6 @@
1
+ require "hub_kernel/authz"
2
+ require "hub_kernel/context"
3
+
1
4
  module HubKernel
2
5
  module Hubs
3
6
  def self.add(hub)
@@ -8,8 +11,22 @@ module HubKernel
8
11
  def self.list = @list ||= []
9
12
 
10
13
  def self.check!
11
- unwired = list.flat_map(&:unwired_ports)
14
+ unwired = list.select { |hub| hub.respond_to?(:unwired_ports) }.flat_map(&:unwired_ports) + unfilled_host_answers
12
15
  raise UnwiredPortError, unwired.join("\n") if unwired.any?
13
16
  end
17
+
18
+ def self.unfilled_host_answers
19
+ return [] unless list.any? { |hub| hub.respond_to?(:exposures) && hub.exposures.any? }
20
+
21
+ { "permission check" => Authz.check, "account scope" => Context.scope }.filter_map { |answer, filled| host_answer_problem(answer, filled) }
22
+ end
23
+ private_class_method :unfilled_host_answers
24
+
25
+ def self.host_answer_problem(answer, filled)
26
+ return "hub_kernel's #{answer} is not filled" unless filled
27
+
28
+ "hub_kernel's #{answer} is filled with something that cannot be called" unless filled.respond_to?(:call)
29
+ end
30
+ private_class_method :host_answer_problem
14
31
  end
15
32
  end
@@ -1,3 +1,3 @@
1
1
  module HubKernel
2
- VERSION = "0.16.0"
2
+ VERSION = "0.18.0"
3
3
  end
@@ -38,7 +38,9 @@ hub_kernel lets a hub, one domain area written as a module, state what it needs
38
38
  bin/rails generate hub_kernel:hub Ledger entry_recorder:record_entry
39
39
  ```
40
40
 
41
- - In a Rails engine gem it writes the hub under the gem's namespace, such as `app/models/billing/ledger.rb` holding `module Billing::Ledger`, and adds hub_kernel to the gemspec. It writes no test, since the gem never fills its own ports.
41
+ - In a Rails engine gem it adds hub_kernel to the gemspec once, and writes no test, since the gem never fills its own ports.
42
+ - When the engine isolates its namespace, the hub goes under that namespace, such as `app/models/billing/ledger.rb` holding `module Billing::Ledger`.
43
+ - A namespaced name such as `Console::Hubs::Billing` writes the hub at the matching nested path, such as `app/models/console/hubs/billing.rb`.
42
44
  - In an app it writes the hub under `app/models` and the hub's wiring test under `test/models`.
43
45
 
44
46
  The generated hub:
@@ -177,4 +179,5 @@ hub_kernel lets a hub, one domain area written as a module, state what it needs
177
179
  - Every exposed method is listed with `exposes`, and the exposed-list check passes.
178
180
  - When an exposed method gains, loses or renames a keyword, update its `takes:` in the same change.
179
181
  - When a hub gains a port, tell the developer every host app must fill it, since calling it unfilled raises an error and the host's start check fails.
182
+ - When a hub exposes its first method, tell the developer every host app must set the permission check and the account scope, since the host's start check fails while any hub exposes a method and either is unset or cannot be called.
180
183
  - Adding the gem to a host, filling ports, running the start check, setting the permission check and the account scope, and the host's hub and crossing checks belong to the install local and are out of scope here.
@@ -24,21 +24,23 @@ This local documents no commands. The surface belongs to the other two locals:
24
24
 
25
25
  Decide which side you are on:
26
26
 
27
- - You are wiring hubs into an app, or the app will not start because something is not wired: use **hub_kernel-install**.
27
+ - You are wiring hubs into an app, or the app will not start because something is not wired or not filled: use **hub_kernel-install**.
28
28
  - You are writing or changing a hub, inside a domain gem or the app: use **hub_kernel-develop**.
29
29
 
30
30
  ## Conventions
31
31
 
32
- - **Hub** — one domain area, written as a module. In a domain gem it sits under the gem's namespace, such as Billing::Ledger.
32
+ - **Hub** — one domain area, written as a module. In a domain gem it sits under the gem's namespace, such as Billing::Ledger. A hub is known to the app once it declares a port or lists a method callable by name.
33
33
  - **Ports** — what a hub needs from outside. Each has a name and the method the hub's own code calls, such as a spend recorder called as record_spend.
34
34
  - **Filling ports** — the host app sets each of a hub's ports to any callable, usually another hub's method, in its setup that runs again on every code reload. Ports are "wired" once they are filled.
35
- - **Start check** — the app refuses to start while any hub's ports are not wired, and names each one, such as "Supplies' spend recorder is not wired".
35
+ - **Start check** — the app refuses to start while any hub's ports are not wired, and names each one, such as "Supplies' spend recorder is not wired". While any hub lists a method callable by name, it also refuses to start until the permission check and the account scope are both set to something that can be called, and names whichever is missing or cannot be called. An app whose hubs list no such methods starts without either.
36
36
  - **Hub check** — a host test that fails naming each of a hub's ports left unfilled or filled with something that cannot be called. A domain gem never runs it, since the gem never fills its own ports.
37
- - **Call by name** — a caller reaches a hub method by its name as a string, with the values sent, the person, and the account. Unknown names, a missing person or account, and missing required values are refused.
37
+ - **Call by name** — a caller reaches a hub method by its name as a string, with the values sent, the person, and the account. A missing person or account, an unknown name, a call the permission check does not allow, and missing required values are each refused, in that order. Only the values the method is listed with are passed on.
38
38
  - **What a person may call** — the hub's list of methods callable by name, kept to those the permission check allows for one person on one account. Asking with no person or no account is refused, the same as a call by name. A hub that lists nothing, or a person allowed nothing, gets an empty list rather than an error.
39
39
  - **Action** — the name a permission is asked for, made of the hub and the method, such as supplies:record_purchase.
40
40
  - **Permission check** — one host-wide rule answering true or false for a person, an action, and an account. Any other answer is an error.
41
41
  - **Account scope** — one host-wide wrapper that runs each call by name inside the account it was made for.
42
42
  - **Refusing** — what a hub does when it will not do what was asked, and its reason reaches the caller unchanged.
43
+ - **Listed-methods check** — a hub's own test that fails naming each listed method the hub has no method for, and each one listed with values it does not take.
43
44
  - **Crossing** — a file in one hub naming a class another hub owns. A constant inside that class counts.
44
- - **Crossing map** — the host's statement of which classes each hub owns, plus the classes shared by all, the host's own layer, each hub's interface module, and any namespace a hub owns whole. Only the host's layer may name a hub's interface module, and a view belongs to the hub that owns its controller.
45
+ - **Crossing map** — the host's statement of which classes each hub owns, plus the classes shared by all, the host's own layer, each hub's interface module, and any namespace a hub owns whole. Only the host's layer may name a hub's interface module. A view belongs to the hub that owns its controller, then to the hub that owns the record its folder is named after, and otherwise to the host's layer.
46
+ - **Crossing check** — a host test that fails naming each crossing, each class no hub owns, each class given to two hubs, and each class the map names that no longer exists. It also fails when none of the files it read belongs to a hub, so a wrong file pattern does not pass unnoticed.
@@ -12,8 +12,8 @@ hub_kernel lets a Rails host app fill the ports each hub declares, refuse to sta
12
12
 
13
13
  ## Interface
14
14
  - `gem "hub_kernel"` — the Gemfile line that adds the gem to the host app.
15
- - `HubKernel::Hubs.check!` — raises `HubKernel::UnwiredPortError` naming every unfilled port of every loaded hub, one per line, such as "Supplies' spend recorder is not wired".
16
- - `HubKernel::Hubs.list` — the hubs that have loaded and declared at least one port.
15
+ - `HubKernel::Hubs.check!` — raises `HubKernel::UnwiredPortError` naming every unfilled port of every loaded hub, one per line, such as "Supplies' spend recorder is not wired", and, while any loaded hub exposes a method, the permission check or the account scope when either is unset or cannot be called.
16
+ - `HubKernel::Hubs.list` — the hubs that have loaded and declared at least one port or exposed at least one method.
17
17
  - `HubKernel::UnwiredPortError` — raised by the start check, by calling an unfilled port, by a call by name while the permission check or the account scope is unset, and by listing what a person may call from a hub that exposes at least one method while the permission check is unset.
18
18
  - `HubKernel::Authz.check` — the host-wide permission rule, a callable taking a person, an action and an account and answering `true` or `false`.
19
19
  - `HubKernel::Context.scope` — the host-wide account scope, a callable taking an account and a block and running the block inside that account.
@@ -42,22 +42,27 @@ hub_kernel lets a Rails host app fill the ports each hub declares, refuse to sta
42
42
 
43
43
  A port accepts any object that responds to `call`. The start check only sees hubs that have loaded, so name every hub in this block. Run `HubKernel::Hubs.list` in `bin/rails console` to see which hubs it saw.
44
44
 
45
- 4. Ask the developer whether any interface, such as a JSON API, calls hub methods by name or lists which of a hub's exposed methods a person may call. If neither, skip to step 6.
45
+ 4. Run `bin/rails runner 'HubKernel::Hubs.check!'`. If the error names "hub_kernel's permission check" or "hub_kernel's account scope", a loaded hub exposes a method and step 5 is required. If it names neither, skip to step 6.
46
46
 
47
- 5. Ask the developer for the app's permission rule and the account scope, since neither has a default. Set both once, in the same initializer:
47
+ 5. Ask the developer for the app's permission rule and the account scope, since neither has a default. Set both in the same `to_prepare` block, before the start check:
48
48
 
49
49
  ```ruby
50
- HubKernel::Authz.check = ->(person, action, account) { Permissions.allow?(person, action, account) }
51
- HubKernel::Context.scope = ->(account, &call) { Current.set(account: account, &call) }
50
+ Rails.application.config.to_prepare do
51
+ Supplies.spend_recorder = Finance.method(:record_spend)
52
+ HubKernel::Authz.check = ->(person, action, account) { Permissions.allow?(person, action, account) }
53
+ HubKernel::Context.scope = ->(account, &call) { Current.set(account: account, &call) }
54
+ HubKernel::Hubs.check!
55
+ end
52
56
  ```
53
57
 
58
+ - While any loaded hub exposes a method, the start check refuses to start until both are set to something that responds to `call`.
54
59
  - The permission check answers both a call by name and a listing of what a person may call, so the two always agree.
55
60
  - Listing what a person may call needs only the permission check, while a call by name needs both.
56
- - The action is a string made of the hub and the method, such as `"supplies:record_purchase"`.
61
+ - The action is a string made of the hub's own name, without its namespace and in snake case, then the method, such as `"supplies:record_purchase"` for `Shop::Supplies`.
57
62
  - The permission check must answer exactly `true` or `false`, and any other answer makes the call or the listing raise an error.
58
63
  - The account scope must take the block and run it, or the hub method never runs.
59
64
 
60
- 6. Add one hub check per hub to the host's tests, such as `test/hubs/supplies_hub_test.rb`:
65
+ 6. Add one hub check to the host's tests for each hub that declares at least one port, such as `test/hubs/supplies_hub_test.rb`:
61
66
 
62
67
  ```ruby
63
68
  require "hub_kernel/conformance/hub"
@@ -69,7 +74,8 @@ hub_kernel lets a Rails host app fill the ports each hub declares, refuse to sta
69
74
  end
70
75
  ```
71
76
 
72
- The `require` line is needed because the gem does not load the test modules on its own.
77
+ - The `require` line is needed because the gem does not load the test modules on its own.
78
+ - A hub that exposes methods but declares no port gets no hub check, since the check errors on a hub with no ports.
73
79
 
74
80
  7. Ask the developer for the crossing map, since only they know which hub owns which class:
75
81
  - which classes each hub owns;
@@ -109,12 +115,14 @@ hub_kernel lets a Rails host app fill the ports each hub declares, refuse to sta
109
115
 
110
116
  ## Conventions
111
117
  - After installing, `bin/rails runner 'HubKernel::Hubs.check!'` exits with no error, and every hub check and the crossing check pass.
112
- - The start check reports unfilled ports only, while the hub check also reports a port filled with something that cannot be called.
118
+ - The start check reports unfilled ports, and the permission check or account scope when unset or not callable while a hub exposes a method.
119
+ - The hub check reports one hub's unfilled ports and each port filled with something that cannot be called, which the start check does not report.
113
120
  - A domain gem never runs the hub check, since it never fills its own ports, so the hub check always lives in the host's tests.
114
121
  - A view belongs to the hub that owns its controller, then to the hub that owns the record its folder is named after, and otherwise to the host's layer.
115
122
  - A constant inside another hub's class counts as naming that class.
116
- - Only the host's layer may name a hub's interface module.
123
+ - A hub's interface module may be named by the host's layer and by that hub, and by no other hub.
117
124
  - A class listed by name under `owners` belongs to that hub even inside another hub's namespace.
125
+ - The check for unowned classes reads only Ruby files outside `app/views`, so a view never needs its own entry in the map.
118
126
  - The crossing check fails with "The crossing check found no files to read" when its `files:` pattern matches nothing the map owns, so fix the pattern or the map rather than the test.
119
- - When a hub gains a port, fill it in the `to_prepare` block. When a class is added, renamed or removed, update the crossing map, since the check fails on a class no hub owns and on a mapped class that no longer exists.
127
+ - When a hub gains a port, fill it in the `to_prepare` block. When a hub first exposes a method, set the permission check and the account scope there too. When a class is added, renamed or removed, update the crossing map, since the check fails on a class no hub owns and on a mapped class that no longer exists.
120
128
  - Writing a hub, declaring its ports, and listing the methods it exposes are not part of installing and are out of scope here.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hub_kernel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tylercschneider