hub_kernel 0.18.0 → 0.19.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.md +6 -1
- data/lib/hub_kernel/action.rb +2 -2
- data/lib/hub_kernel/hubs.rb +6 -3
- data/lib/hub_kernel/ports.rb +2 -2
- data/lib/hub_kernel/version.rb +1 -1
- data/lib/hub_kernel.rb +1 -1
- data/the_local/agents/hub_kernel-develop.md +1 -4
- data/the_local/agents/hub_kernel-info.md +3 -2
- data/the_local/agents/hub_kernel-install.md +3 -2
- data/the_local/interface.yml +0 -3
- metadata +15 -4
- data/lib/hub_kernel/authz.rb +0 -8
- data/lib/hub_kernel/context.rb +0 -5
- data/lib/hub_kernel/exposes.rb +0 -92
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3efd66220ac58f67d7c6018b2e552f70641539e11da6ef13810d274cb1daea52
|
|
4
|
+
data.tar.gz: 803ca1aa3d1258ae639ae2a6beb842b8e969f213b3d604e07d7acff7b9dc6523
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 111dfbefdd5d3fbb62241dc8142a5b16b8626f9af8d227df56033c60205a2d2bbd9db4a27fa39e487f31ea891dd1041a18c76519e5ba88e2a7a7279659e667cc
|
|
7
|
+
data.tar.gz: bac66dfb083524ae2f30f2db97c637df7d88a70f19ce59f264acdbc40baa24e39fa3218ef19d472713e3c3c6ba7df8cf959078f2993ceddbc0b9790b44badff1
|
data/README.md
CHANGED
|
@@ -105,6 +105,11 @@ The test fails and names each port the app left unfilled, and each port filled w
|
|
|
105
105
|
something that cannot be called.
|
|
106
106
|
|
|
107
107
|
### The exposed list
|
|
108
|
+
The exposed list, calling by name, the permission check, the account scope and the errors
|
|
109
|
+
they raise come from hub_kernel-interface, which hub_kernel depends on, so a hub keeps
|
|
110
|
+
writing `extend HubKernel::Exposes` with no other gem to add. Interface gems such as
|
|
111
|
+
hub_kernel-api depend on hub_kernel-interface alone.
|
|
112
|
+
|
|
108
113
|
A hub names each method outside callers may reach, the values it takes, and whether it
|
|
109
114
|
writes. A layer such as a JSON endpoint looks a method up by name and serves only what
|
|
110
115
|
the hub exposes:
|
|
@@ -148,7 +153,7 @@ It refuses a missing person or account as a call by name does, and a hub that ex
|
|
|
148
153
|
nothing, or a person allowed nothing, gets an empty list.
|
|
149
154
|
|
|
150
155
|
### The host's permission check and account scope
|
|
151
|
-
Every call by name asks the host whether the person may take the action on the account,
|
|
156
|
+
hub_kernel-interface holds both settings. Every call by name asks the host whether the person may take the action on the account,
|
|
152
157
|
and runs the method inside the host's scope for that account. The host sets both once
|
|
153
158
|
for the whole app:
|
|
154
159
|
|
data/lib/hub_kernel/action.rb
CHANGED
data/lib/hub_kernel/hubs.rb
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
require "hub_kernel/authz"
|
|
2
2
|
require "hub_kernel/context"
|
|
3
|
+
require "hub_kernel/interface/exposing_hubs"
|
|
3
4
|
|
|
4
5
|
module HubKernel
|
|
5
6
|
module Hubs
|
|
6
7
|
def self.add(hub)
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
registered.reject! { |listed| listed.name == hub.name }
|
|
9
|
+
registered << hub
|
|
9
10
|
end
|
|
10
11
|
|
|
11
|
-
def self.
|
|
12
|
+
def self.registered = @registered ||= []
|
|
13
|
+
|
|
14
|
+
def self.list = (registered + Interface::ExposingHubs.list).uniq { |hub| hub.name || hub }
|
|
12
15
|
|
|
13
16
|
def self.check!
|
|
14
17
|
unwired = list.select { |hub| hub.respond_to?(:unwired_ports) }.flat_map(&:unwired_ports) + unfilled_host_answers
|
data/lib/hub_kernel/ports.rb
CHANGED
data/lib/hub_kernel/version.rb
CHANGED
data/lib/hub_kernel.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "hub_kernel-interface"
|
|
1
2
|
require "hub_kernel/version"
|
|
2
3
|
require "hub_kernel/engine"
|
|
3
4
|
require "hub_kernel/action"
|
|
@@ -5,7 +6,6 @@ require "hub_kernel/answer"
|
|
|
5
6
|
require "hub_kernel/follow_up"
|
|
6
7
|
require "hub_kernel/markup"
|
|
7
8
|
require "hub_kernel/ports"
|
|
8
|
-
require "hub_kernel/exposes"
|
|
9
9
|
|
|
10
10
|
module HubKernel
|
|
11
11
|
end
|
|
@@ -164,10 +164,7 @@ hub_kernel lets a hub, one domain area written as a module, state what it needs
|
|
|
164
164
|
- Each entry is the same one `exposed` answers.
|
|
165
165
|
- A hub that lists nothing answers `[]` from both, and a person allowed nothing answers `[]` from `exposures_for`.
|
|
166
166
|
- `exposures_for` asks the permission check once per entry with the same action name `call_exposed` uses, such as `"supplies:record_purchase"`, and runs no hub method.
|
|
167
|
-
- `exposures_for`
|
|
168
|
-
1. `HubKernel::MissingArgumentError` when `person:` or `account:` is `nil`.
|
|
169
|
-
2. An error naming the host's permission check when the host has not set it and the hub lists at least one method.
|
|
170
|
-
3. `HubKernel::NonBooleanAnswerError` when the permission check answers anything but `true` or `false`.
|
|
167
|
+
- `exposures_for` raises `HubKernel::MissingArgumentError` when `person:` or `account:` is `nil`, before asking the permission check.
|
|
171
168
|
- Pass the same `person:` and `account:` the interface passes to `call_exposed`.
|
|
172
169
|
- Ask the developer whether the interface shows every exposed method or only those the person may call. Do not pick yourself.
|
|
173
170
|
|
|
@@ -11,7 +11,7 @@ This local explains hub_kernel and the words it uses. It makes no changes and gi
|
|
|
11
11
|
|
|
12
12
|
hub_kernel splits a Rails app into hubs, where each hub is one area of the domain, such as supplies or finance, usually shipped in its own domain gem. A hub states what it needs from outside itself as named ports, and the host app decides what fills each one, so a hub never names another hub's code directly.
|
|
13
13
|
|
|
14
|
-
A hub also lists the methods outside callers may reach by name, with the values each takes and whether it writes. An interface such as a JSON API calls through that list, and every call is checked against the host's permission rule and run inside the host's account scope. The same list can be read back whole, or cut down to only the methods one person is allowed to call on one account, so an interface can show a person what they may do before they try it. Reach for hub_kernel when an app has several domain areas that should talk to each other only through declared entry points, and you want the app to refuse to start, or the suite to fail, when that is not true.
|
|
14
|
+
A hub also lists the methods outside callers may reach by name, with the values each takes and whether it writes. An interface such as a JSON API calls through that list, and every call is checked against the host's permission rule and run inside the host's account scope. The same list can be read back whole, or cut down to only the methods one person is allowed to call on one account, so an interface can show a person what they may do before they try it. That list, calling by name, the permission check and the account scope come from hub_kernel-interface, which hub_kernel depends on, so a hub that lists methods needs no second gem. Reach for hub_kernel when an app has several domain areas that should talk to each other only through declared entry points, and you want the app to refuse to start, or the suite to fail, when that is not true.
|
|
15
15
|
|
|
16
16
|
## Interface
|
|
17
17
|
|
|
@@ -29,7 +29,8 @@ Decide which side you are on:
|
|
|
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. A hub is known to the app once it declares a port or lists a method callable by name.
|
|
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, and the start check covers both kinds.
|
|
33
|
+
- **hub_kernel-interface** — the smaller gem hub_kernel builds on, holding the methods-callable-by-name list, the permission check and the account scope. Interface gems such as hub_kernel-api depend on it alone, while hubs and host apps keep depending on hub_kernel.
|
|
33
34
|
- **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
35
|
- **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
36
|
- **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.
|
|
@@ -14,7 +14,7 @@ hub_kernel lets a Rails host app fill the ports each hub declares, refuse to sta
|
|
|
14
14
|
- `gem "hub_kernel"` — the Gemfile line that adds the gem to the host app.
|
|
15
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
16
|
- `HubKernel::Hubs.list` — the hubs that have loaded and declared at least one port or exposed at least one method.
|
|
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
|
|
17
|
+
- `HubKernel::UnwiredPortError` — raised by the start check, by calling an unfilled port, and by a call by name while the permission check or the account scope 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.
|
|
20
20
|
- `HubKernel::Conformance::Hub` — a test module that fails naming each of one hub's ports left unfilled or filled with something that cannot be called.
|
|
@@ -28,6 +28,8 @@ hub_kernel lets a Rails host app fill the ports each hub declares, refuse to sta
|
|
|
28
28
|
gem "hub_kernel"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
`bundle install` also installs hub_kernel-interface, which holds the permission check and the account scope, so add no other gem for them.
|
|
32
|
+
|
|
31
33
|
2. List the hubs the app uses and each port they declare. Ask the developer what fills each port, such as another hub's method. Do not pick a filler yourself.
|
|
32
34
|
|
|
33
35
|
3. Fill every port and run the start check in a `to_prepare` block in an initializer, such as `config/initializers/hubs.rb`, so both run again after every code reload:
|
|
@@ -57,7 +59,6 @@ hub_kernel lets a Rails host app fill the ports each hub declares, refuse to sta
|
|
|
57
59
|
|
|
58
60
|
- While any loaded hub exposes a method, the start check refuses to start until both are set to something that responds to `call`.
|
|
59
61
|
- The permission check answers both a call by name and a listing of what a person may call, so the two always agree.
|
|
60
|
-
- Listing what a person may call needs only the permission check, while a call by name needs both.
|
|
61
62
|
- 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`.
|
|
62
63
|
- The permission check must answer exactly `true` or `false`, and any other answer makes the call or the listing raise an error.
|
|
63
64
|
- The account scope must take the block and run it, or the hub method never runs.
|
data/the_local/interface.yml
CHANGED
|
@@ -32,10 +32,7 @@ sources:
|
|
|
32
32
|
- lib/hub_kernel.rb
|
|
33
33
|
- lib/hub_kernel/ports.rb
|
|
34
34
|
- lib/hub_kernel/hubs.rb
|
|
35
|
-
- lib/hub_kernel/exposes.rb
|
|
36
35
|
- lib/hub_kernel/action.rb
|
|
37
|
-
- lib/hub_kernel/authz.rb
|
|
38
|
-
- lib/hub_kernel/context.rb
|
|
39
36
|
- lib/hub_kernel/crossings.rb
|
|
40
37
|
- lib/hub_kernel/conformance/hub.rb
|
|
41
38
|
- lib/hub_kernel/conformance/exposed.rb
|
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.
|
|
4
|
+
version: 0.19.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tylercschneider
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: 8.1.3
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: hub_kernel-interface
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.3'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.3'
|
|
26
40
|
description: 'hub_kernel wraps a domain and connects it to a host app''s infrastructure
|
|
27
41
|
through named ports. It ships no domain of its own: each hub is hub_kernel wrapped
|
|
28
42
|
around one domain.'
|
|
@@ -40,16 +54,13 @@ files:
|
|
|
40
54
|
- lib/hub_kernel.rb
|
|
41
55
|
- lib/hub_kernel/action.rb
|
|
42
56
|
- lib/hub_kernel/answer.rb
|
|
43
|
-
- lib/hub_kernel/authz.rb
|
|
44
57
|
- lib/hub_kernel/conformance/action.rb
|
|
45
58
|
- lib/hub_kernel/conformance/crossings.rb
|
|
46
59
|
- lib/hub_kernel/conformance/exposed.rb
|
|
47
60
|
- lib/hub_kernel/conformance/hub.rb
|
|
48
61
|
- lib/hub_kernel/conformance/partial.rb
|
|
49
|
-
- lib/hub_kernel/context.rb
|
|
50
62
|
- lib/hub_kernel/crossings.rb
|
|
51
63
|
- lib/hub_kernel/engine.rb
|
|
52
|
-
- lib/hub_kernel/exposes.rb
|
|
53
64
|
- lib/hub_kernel/follow_up.rb
|
|
54
65
|
- lib/hub_kernel/hubs.rb
|
|
55
66
|
- lib/hub_kernel/markup.rb
|
data/lib/hub_kernel/authz.rb
DELETED
data/lib/hub_kernel/context.rb
DELETED
data/lib/hub_kernel/exposes.rb
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
require "hub_kernel/action"
|
|
2
|
-
require "hub_kernel/authz"
|
|
3
|
-
require "hub_kernel/context"
|
|
4
|
-
require "hub_kernel/ports"
|
|
5
|
-
require "hub_kernel/hubs"
|
|
6
|
-
|
|
7
|
-
module HubKernel
|
|
8
|
-
class UnexposedMethodError < StandardError; end
|
|
9
|
-
class Refused < StandardError; end
|
|
10
|
-
|
|
11
|
-
module Exposes
|
|
12
|
-
Exposed = Data.define(:name, :takes, :writes)
|
|
13
|
-
|
|
14
|
-
def exposes(name, takes:, writes:)
|
|
15
|
-
Hubs.add(self)
|
|
16
|
-
exposed_methods[name.to_s] = Exposed.new(name: name, takes: takes, writes: writes)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def exposed(name) = exposed_methods[name.to_s]
|
|
20
|
-
|
|
21
|
-
def exposures = exposed_methods.values
|
|
22
|
-
|
|
23
|
-
def exposures_for(person:, account:)
|
|
24
|
-
refuse_without_caller(person, account)
|
|
25
|
-
|
|
26
|
-
exposures.select { |exposure| allowed?(exposure, person, account) }
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def call_exposed(name, values:, person:, account:)
|
|
30
|
-
refuse_without_caller(person, account)
|
|
31
|
-
|
|
32
|
-
exposure = exposed(name) || raise(UnexposedMethodError, "#{exposing_hub} does not expose #{name}")
|
|
33
|
-
refuse_unless_allowed(exposure, person, account)
|
|
34
|
-
refuse_missing_values(exposure, values)
|
|
35
|
-
within_account(account) { public_send(exposure.name, **values.slice(*exposure.takes)) }
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def exposure_problems
|
|
39
|
-
exposed_methods.values.filter_map { |exposure| exposure_problem(exposure) }
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
def exposed_methods = @exposed_methods ||= {}
|
|
45
|
-
|
|
46
|
-
def exposing_hub = name.demodulize
|
|
47
|
-
|
|
48
|
-
def refuse_without_caller(person, account)
|
|
49
|
-
raise MissingArgumentError, "A call by name needs a person" if person.nil?
|
|
50
|
-
raise MissingArgumentError, "A call by name needs an account" if account.nil?
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def refuse_unless_allowed(exposure, person, account)
|
|
54
|
-
raise NotAllowed, "#{exposing_hub} #{exposure.name}" unless allowed?(exposure, person, account)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def allowed?(exposure, person, account)
|
|
58
|
-
raise UnwiredPortError, "hub_kernel's permission check is not filled" unless Authz.check
|
|
59
|
-
|
|
60
|
-
answer = Authz.check.call(person, "#{exposing_hub.underscore}:#{exposure.name}", account)
|
|
61
|
-
raise NonBooleanAnswerError, "The permission check must answer true or false, got #{answer.inspect}" unless [ true, false ].include?(answer)
|
|
62
|
-
|
|
63
|
-
answer
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def within_account(account, &call)
|
|
67
|
-
raise UnwiredPortError, "hub_kernel's account scope is not filled" unless Context.scope
|
|
68
|
-
|
|
69
|
-
Context.scope.call(account, &call)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def refuse_missing_values(exposure, values)
|
|
73
|
-
missing = keywords(exposure, :keyreq) - values.keys
|
|
74
|
-
raise MissingArgumentError, "Give #{missing.join(", ")}" if missing.any?
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def exposure_problem(exposure)
|
|
78
|
-
return "#{exposing_hub} exposes #{exposure.name}, which it has no method for" unless respond_to?(exposure.name)
|
|
79
|
-
return if takes_listed_values?(exposure)
|
|
80
|
-
|
|
81
|
-
"#{exposing_hub} exposes #{exposure.name} with #{exposure.takes.join(", ")}, but it takes #{keywords(exposure, :keyreq, :key).join(", ")}"
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def takes_listed_values?(exposure)
|
|
85
|
-
(keywords(exposure, :keyreq) - exposure.takes).empty? && (takes_any_values?(exposure) || (exposure.takes - keywords(exposure, :keyreq, :key)).empty?)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def takes_any_values?(exposure) = method(exposure.name).parameters.any? { |kind, _| kind == :keyrest }
|
|
89
|
-
|
|
90
|
-
def keywords(exposure, *kinds) = method(exposure.name).parameters.filter_map { |kind, value| value if kinds.include?(kind) }
|
|
91
|
-
end
|
|
92
|
-
end
|