hub_kernel 0.10.0 → 0.11.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 +33 -0
- data/lib/hub_kernel/authz.rb +3 -0
- data/lib/hub_kernel/conformance/crossings.rb +5 -2
- data/lib/hub_kernel/context.rb +2 -0
- data/lib/hub_kernel/crossings.rb +2 -0
- data/lib/hub_kernel/exposes.rb +37 -0
- data/lib/hub_kernel/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 582093847b3b8cf75dbadd831d8a68df615bebccce57eeb89be12d3496fc89f0
|
|
4
|
+
data.tar.gz: 4544cb83a72d80f920abbf78f6eee6984b4ab8d28e330d568eb86bd21ef97dc8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5d72c62b32c671f49ae4babcc4755dd871fc4fcfaa35573c78689250ca6b27774bb9ce2db460405940774fc017d1abd742da4df1d646565f8df770d379deb00
|
|
7
|
+
data.tar.gz: 4d5dff84ef2be0d5ebc3a53ce43e22e42d5d73ad53b0601663139f3fc0a68876e8c874a1833c200d078b29684d0bcd4056f11dd84150c0181dc6462943e9262a
|
data/README.md
CHANGED
|
@@ -116,6 +116,36 @@ Supplies.exposed("record_purchase").takes # => [:supplier_id, :bought_on, :lines
|
|
|
116
116
|
Supplies.exposed("delete_everything") # => nil
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
### Calling an exposed method by name
|
|
120
|
+
An interface such as a JSON API calls a hub through its exposed list, by name, with the
|
|
121
|
+
values a caller sent and the person and account the call is made for:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
Supplies.call_exposed("record_purchase", values: { supplier_id: 4, bought_on: "2026-10-01", lines: [] }, person: current_person, account: current_account)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
It raises `HubKernel::UnexposedMethodError` for a name the hub does not expose,
|
|
128
|
+
`HubKernel::MissingArgumentError` for a call with no person, no account, or a value the
|
|
129
|
+
method requires, and passes on only the values the method is listed with. A hub raises
|
|
130
|
+
`HubKernel::Refused` with a reason when it will not do what was asked, and the reason
|
|
131
|
+
reaches the caller unchanged.
|
|
132
|
+
|
|
133
|
+
### The host's permission check and account scope
|
|
134
|
+
Every call by name asks the host whether the person may take the action on the account,
|
|
135
|
+
and runs the method inside the host's scope for that account. The host sets both once
|
|
136
|
+
for the whole app:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
HubKernel::Authz.check = ->(person, action, account) { Permissions.allow?(person, action, account) }
|
|
140
|
+
HubKernel::Context.scope = ->(account, &call) { Current.set(account: account, &call) }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The action is named after the hub and the method, such as `supplies:record_purchase`. A
|
|
144
|
+
refused call raises `HubKernel::NotAllowed` before the method runs or its values are
|
|
145
|
+
checked, and a check that answers anything but true or false raises
|
|
146
|
+
`HubKernel::NonBooleanAnswerError`. A call made while either is unset raises
|
|
147
|
+
`HubKernel::UnwiredPortError` naming it.
|
|
148
|
+
|
|
119
149
|
### The exposed-list check
|
|
120
150
|
A hub's own test can check its exposed list, so a listed method the hub lacks, or one
|
|
121
151
|
listed with values it does not take, shows in the suite rather than on a caller's
|
|
@@ -163,6 +193,9 @@ end
|
|
|
163
193
|
record its folder is named after, and otherwise to the `host_layer`.
|
|
164
194
|
- The host's layer may name any hub's interface module, and a hub naming another hub's
|
|
165
195
|
interface module is a crossing.
|
|
196
|
+
- The test fails with "The crossing check found no files to read" when none of the files
|
|
197
|
+
it read belongs to a hub or the host's layer, so a wrong `files:` pattern or an empty
|
|
198
|
+
map does not pass unnoticed.
|
|
166
199
|
|
|
167
200
|
## Installation
|
|
168
201
|
Add this line to your application's Gemfile:
|
data/lib/hub_kernel/authz.rb
CHANGED
|
@@ -17,10 +17,13 @@ module HubKernel
|
|
|
17
17
|
|
|
18
18
|
included do
|
|
19
19
|
test "no file names a class another hub owns" do
|
|
20
|
-
|
|
20
|
+
paths = (Dir.glob(crossing_files, base: crossing_root) - crossing_exceptions).sort
|
|
21
|
+
flunk "The crossing check found no files to read" if paths.none? { |path| the_crossings.belongs?(path) }
|
|
22
|
+
|
|
23
|
+
found = paths.flat_map do |path|
|
|
21
24
|
the_crossings.in(path, File.read(File.join(crossing_root, path))).map { |name| "#{path} names #{name}" }
|
|
22
25
|
end
|
|
23
|
-
|
|
26
|
+
assert found.empty?, found.join("\n")
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
end
|
data/lib/hub_kernel/context.rb
CHANGED
data/lib/hub_kernel/crossings.rb
CHANGED
data/lib/hub_kernel/exposes.rb
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
require "hub_kernel/action"
|
|
2
|
+
require "hub_kernel/authz"
|
|
3
|
+
require "hub_kernel/context"
|
|
4
|
+
require "hub_kernel/ports"
|
|
5
|
+
|
|
1
6
|
module HubKernel
|
|
7
|
+
class UnexposedMethodError < StandardError; end
|
|
8
|
+
class Refused < StandardError; end
|
|
9
|
+
|
|
2
10
|
module Exposes
|
|
3
11
|
Exposed = Data.define(:name, :takes, :writes)
|
|
4
12
|
|
|
@@ -8,6 +16,16 @@ module HubKernel
|
|
|
8
16
|
|
|
9
17
|
def exposed(name) = exposed_methods[name.to_s]
|
|
10
18
|
|
|
19
|
+
def call_exposed(name, values:, person:, account:)
|
|
20
|
+
raise MissingArgumentError, "A call by name needs a person" if person.nil?
|
|
21
|
+
raise MissingArgumentError, "A call by name needs an account" if account.nil?
|
|
22
|
+
|
|
23
|
+
exposure = exposed(name) || raise(UnexposedMethodError, "#{exposing_hub} does not expose #{name}")
|
|
24
|
+
refuse_unless_allowed(exposure, person, account)
|
|
25
|
+
refuse_missing_values(exposure, values)
|
|
26
|
+
within_account(account) { public_send(exposure.name, **values.slice(*exposure.takes)) }
|
|
27
|
+
end
|
|
28
|
+
|
|
11
29
|
def exposure_problems
|
|
12
30
|
exposed_methods.values.filter_map { |exposure| exposure_problem(exposure) }
|
|
13
31
|
end
|
|
@@ -18,6 +36,25 @@ module HubKernel
|
|
|
18
36
|
|
|
19
37
|
def exposing_hub = name.demodulize
|
|
20
38
|
|
|
39
|
+
def refuse_unless_allowed(exposure, person, account)
|
|
40
|
+
raise UnwiredPortError, "hub_kernel's permission check is not filled" unless Authz.check
|
|
41
|
+
|
|
42
|
+
answer = Authz.check.call(person, "#{exposing_hub.underscore}:#{exposure.name}", account)
|
|
43
|
+
raise NonBooleanAnswerError, "The permission check must answer true or false, got #{answer.inspect}" unless [ true, false ].include?(answer)
|
|
44
|
+
raise NotAllowed, "#{exposing_hub} #{exposure.name}" unless answer
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def within_account(account, &call)
|
|
48
|
+
raise UnwiredPortError, "hub_kernel's account scope is not filled" unless Context.scope
|
|
49
|
+
|
|
50
|
+
Context.scope.call(account, &call)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def refuse_missing_values(exposure, values)
|
|
54
|
+
missing = keywords(exposure, :keyreq) - values.keys
|
|
55
|
+
raise MissingArgumentError, "Give #{missing.join(", ")}" if missing.any?
|
|
56
|
+
end
|
|
57
|
+
|
|
21
58
|
def exposure_problem(exposure)
|
|
22
59
|
return "#{exposing_hub} exposes #{exposure.name}, which it has no method for" unless respond_to?(exposure.name)
|
|
23
60
|
return if takes_listed_values?(exposure)
|
data/lib/hub_kernel/version.rb
CHANGED