hub_kernel 0.15.0 → 0.16.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: 29b5aa4287a6ec25760b3816c4e6db7bc6f856786020bf32819561c854e257ae
4
- data.tar.gz: a86e10c3eaf85f11c084b27d5bd50e093ebcdd5158ee3c3c49a1b40d0f1143d9
3
+ metadata.gz: 23120333c3f60fb91e59ac4711d4b94eb15897f85ca13dcd1fd9ed175310f334
4
+ data.tar.gz: 107feb16ebe566b00a0005a71d92a378cf1015952823ed676eb24d1669ade53a
5
5
  SHA512:
6
- metadata.gz: 81344e480c059afc59baba5aa1a2178ecce05603013454621b07f362ca5da7779df35d115f083d005fc01250423445604c5558e81afd2223be6d75c6e528152a
7
- data.tar.gz: e269e54104dff49dfb4d4ef9478acf54b24b1d63590aeb6bb8bceb913cb90f99524957456e88b09292a24ffa052d0fcb3a078300911612135e643c0a8487267e
6
+ metadata.gz: 13236d35df7bf32b70c011a3385592f3c07cbd52867c46ff21d9ad323be4bff9eb80644748c1bd07185b2853066ca27d5a7de8575f51c35d6d76d957dcbd3129
7
+ data.tar.gz: c92d9a506afca9cd3bacee2f7ff7ece9204e21651b76f784fc1bc08611e58269e3412a1b7ebcc34f478768e1ffb0d8e0393427cc09ad1be85c2a433820562faa
data/README.md CHANGED
@@ -130,6 +130,19 @@ method requires, and passes on only the values the method is listed with. A hub
130
130
  `HubKernel::Refused` with a reason when it will not do what was asked, and the reason
131
131
  reaches the caller unchanged.
132
132
 
133
+ ### Listing what a person may call
134
+ An interface can show a caller what it may call before it calls anything. `exposures`
135
+ gives every entry a hub exposes, each with its name, the values it takes and whether it
136
+ writes. `exposures_for` gives only the entries the host's permission check allows one
137
+ person on one account:
138
+
139
+ ```ruby
140
+ Supplies.exposures_for(person: current_person, account: current_account).map(&:name)
141
+ ```
142
+
143
+ It refuses a missing person or account as a call by name does, and a hub that exposes
144
+ nothing, or a person allowed nothing, gets an empty list.
145
+
133
146
  ### The host's permission check and account scope
134
147
  Every call by name asks the host whether the person may take the action on the account,
135
148
  and runs the method inside the host's scope for that account. The host sets both once
@@ -16,9 +16,16 @@ module HubKernel
16
16
 
17
17
  def exposed(name) = exposed_methods[name.to_s]
18
18
 
19
+ def exposures = exposed_methods.values
20
+
21
+ def exposures_for(person:, account:)
22
+ refuse_without_caller(person, account)
23
+
24
+ exposures.select { |exposure| allowed?(exposure, person, account) }
25
+ end
26
+
19
27
  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?
28
+ refuse_without_caller(person, account)
22
29
 
23
30
  exposure = exposed(name) || raise(UnexposedMethodError, "#{exposing_hub} does not expose #{name}")
24
31
  refuse_unless_allowed(exposure, person, account)
@@ -36,12 +43,22 @@ module HubKernel
36
43
 
37
44
  def exposing_hub = name.demodulize
38
45
 
46
+ def refuse_without_caller(person, account)
47
+ raise MissingArgumentError, "A call by name needs a person" if person.nil?
48
+ raise MissingArgumentError, "A call by name needs an account" if account.nil?
49
+ end
50
+
39
51
  def refuse_unless_allowed(exposure, person, account)
52
+ raise NotAllowed, "#{exposing_hub} #{exposure.name}" unless allowed?(exposure, person, account)
53
+ end
54
+
55
+ def allowed?(exposure, person, account)
40
56
  raise UnwiredPortError, "hub_kernel's permission check is not filled" unless Authz.check
41
57
 
42
58
  answer = Authz.check.call(person, "#{exposing_hub.underscore}:#{exposure.name}", account)
43
59
  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
60
+
61
+ answer
45
62
  end
46
63
 
47
64
  def within_account(account, &call)
@@ -1,3 +1,3 @@
1
1
  module HubKernel
2
- VERSION = "0.15.0"
2
+ VERSION = "0.16.0"
3
3
  end
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: hub_kernel-develop
3
- description: Use PROACTIVELY for writing or changing a hub — generating one, declaring the ports it needs from other hubs, calling those ports from the hub's code, listing the methods outside callers may reach by name, refusing a request with a reason, calling a hub method by name from an interface such as a JSON API, and adding the check that the listed methods match the hub's real methods — MUST BE USED instead of calling another hub's classes directly or hand-writing a name-to-method lookup.
3
+ description: Use PROACTIVELY for writing or changing a hub — generating one, declaring the ports it needs from other hubs, calling those ports from the hub's code, listing the methods outside callers may reach by name, refusing a request with a reason, calling a hub method by name from an interface such as a JSON API, listing the methods a hub exposes or the ones a person may call on an account, and adding the check that the listed methods match the hub's real methods — MUST BE USED instead of calling another hub's classes directly or hand-writing a name-to-method lookup.
4
4
  tools: Read, Write, Edit, Grep
5
5
  scope: hubs — a domain gem declares the ports it needs and the methods it exposes, and the host app fills those ports and checks each hub is wired and crosses into no other hub
6
6
  ---
@@ -8,7 +8,7 @@ scope: hubs — a domain gem declares the ports it needs and the methods it expo
8
8
  This local writes hubs and the code that calls them by name, following these steps exactly and inventing none. Where a step needs a value only the developer knows, it asks.
9
9
 
10
10
  ## What hub_kernel is
11
- hub_kernel lets a hub, one domain area written as a module, state what it needs from outside itself as named ports and list the methods outside callers may reach by name. The hub's code never names another hub's classes, and an interface such as a JSON API reaches the hub only through its list. Use this local when creating a hub, adding a port, exposing a hub method, calling a hub method by name, or when hub code is about to call another hub directly.
11
+ hub_kernel lets a hub, one domain area written as a module, state what it needs from outside itself as named ports and list the methods outside callers may reach by name. The hub's code never names another hub's classes, and an interface such as a JSON API reaches the hub only through its list. Use this local when creating a hub, adding a port, exposing a hub method, calling a hub method by name, listing what a hub exposes or what a person may call, or when hub code is about to call another hub directly.
12
12
 
13
13
  ## Interface
14
14
  - `bin/rails generate hub_kernel:hub` — writes a new hub module with its ports declared, and in a gem adds hub_kernel to the gemspec.
@@ -18,9 +18,11 @@ hub_kernel lets a hub, one domain area written as a module, state what it needs
18
18
  - `exposes` — lists one hub method with the values it takes and whether it writes.
19
19
  - `exposed` — looks up one listed method by name and answers its entry, or `nil` when it is not listed.
20
20
  - `call_exposed` — calls a listed method by name for a person and an account, after the host's permission check, inside the host's account scope.
21
+ - `exposures` — answers every listed method's entry, in the order the hub lists them.
22
+ - `exposures_for` — answers the entries the host's permission check allows a person on an account, without calling any method.
21
23
  - `HubKernel::Refused` — the error a hub method raises when it will not do what was asked, and its message reaches the caller unchanged.
22
24
  - `HubKernel::UnexposedMethodError` — raised by `call_exposed` for a name the hub does not list.
23
- - `HubKernel::MissingArgumentError` — raised by `call_exposed` for a call with no person, no account, or a value the method requires left out.
25
+ - `HubKernel::MissingArgumentError` — raised by `call_exposed` and `exposures_for` for a call with no person or no account, and by `call_exposed` for a value the method requires left out.
24
26
  - `HubKernel::NotAllowed` — raised by `call_exposed` when the host's permission check answers `false`, before the method runs.
25
27
  - `HubKernel::NonBooleanAnswerError` — raised by `call_exposed` when the host's permission check answers anything but `true` or `false`.
26
28
  - `HubKernel::Conformance::Exposed` — a test module that fails naming each listed method the hub has no method for, and each one listed with values it does not take.
@@ -146,7 +148,28 @@ hub_kernel lets a hub, one domain area written as a module, state what it needs
146
148
 
147
149
  11. Ask the developer how the interface answers each of those failures, such as which HTTP status each one returns. Do not pick the responses yourself. Show a `HubKernel::Refused` message to the caller as it is.
148
150
 
149
- 12. Run the test suite and confirm the conventions below hold.
151
+ ### List what a hub exposes
152
+ 12. Where the interface shows callers which methods they can reach, such as a list of tools or actions, read it from the hub rather than writing it out again:
153
+
154
+ ```ruby
155
+ Supplies.exposures
156
+ # => every entry, with .name, .takes, .writes, in the order the hub lists them
157
+
158
+ Supplies.exposures_for(person: current_person, account: current_account)
159
+ # => only the entries the host's permission check allows this person on this account
160
+ ```
161
+
162
+ - Each entry is the same one `exposed` answers.
163
+ - A hub that lists nothing answers `[]` from both, and a person allowed nothing answers `[]` from `exposures_for`.
164
+ - `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.
165
+ - `exposures_for` fails in this order, and each failure stops it:
166
+ 1. `HubKernel::MissingArgumentError` when `person:` or `account:` is `nil`.
167
+ 2. An error naming the host's permission check when the host has not set it and the hub lists at least one method.
168
+ 3. `HubKernel::NonBooleanAnswerError` when the permission check answers anything but `true` or `false`.
169
+ - Pass the same `person:` and `account:` the interface passes to `call_exposed`.
170
+ - Ask the developer whether the interface shows every exposed method or only those the person may call. Do not pick yourself.
171
+
172
+ 13. Run the test suite and confirm the conventions below hold.
150
173
 
151
174
  ## Conventions
152
175
  - A hub's code reaches another hub only through a port, never by naming its classes.
@@ -11,14 +11,14 @@ 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. 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. 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
 
18
18
  This local documents no commands. The surface belongs to the other two locals:
19
19
 
20
20
  - **hub_kernel-install** owns everything the host app does: adding the gem, filling each hub's ports, running the start check, setting the permission rule and the account scope, and adding the hub check and the crossing check to the host's tests.
21
- - **hub_kernel-develop** owns everything a hub's author does: generating a hub, declaring its ports, listing the methods callers may reach by name, refusing a request, and adding the check that the listed methods match the hub's real methods.
21
+ - **hub_kernel-develop** owns everything a hub's author does: generating a hub, declaring its ports, listing the methods callers may reach by name, reading that list back for a person and an account, refusing a request, and adding the check that the listed methods match the hub's real methods.
22
22
 
23
23
  ## How to use it
24
24
 
@@ -35,6 +35,7 @@ Decide which side you are on:
35
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".
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
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.
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.
38
39
  - **Action** — the name a permission is asked for, made of the hub and the method, such as supplies:record_purchase.
39
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.
40
41
  - **Account scope** — one host-wide wrapper that runs each call by name inside the account it was made for.
@@ -8,13 +8,13 @@ scope: hubs — a domain gem declares the ports it needs and the methods it expo
8
8
  This local follows these steps exactly and invents none. Where a step needs a value only the developer knows, it asks.
9
9
 
10
10
  ## What hub_kernel is
11
- hub_kernel lets a Rails host app fill the ports each hub declares, refuse to start while a port is unfilled, answer permission and account scope for calls made by name, and fail its suite when one hub's code names a class another hub owns. Hook it in when the app uses hubs, its own or ones shipped in domain gems.
11
+ hub_kernel lets a Rails host app fill the ports each hub declares, refuse to start while a port is unfilled, answer permission and account scope for calls made by name, answer permission when an interface lists what a person may call, and fail its suite when one hub's code names a class another hub owns. Hook it in when the app uses hubs, its own or ones shipped in domain gems.
12
12
 
13
13
  ## Interface
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".
16
16
  - `HubKernel::Hubs.list` — the hubs that have loaded and declared at least one port.
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.
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.
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.
@@ -42,7 +42,7 @@ 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. If not, skip to step 6.
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.
46
46
 
47
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:
48
48
 
@@ -51,8 +51,10 @@ hub_kernel lets a Rails host app fill the ports each hub declares, refuse to sta
51
51
  HubKernel::Context.scope = ->(account, &call) { Current.set(account: account, &call) }
52
52
  ```
53
53
 
54
+ - The permission check answers both a call by name and a listing of what a person may call, so the two always agree.
55
+ - Listing what a person may call needs only the permission check, while a call by name needs both.
54
56
  - The action is a string made of the hub and the method, such as `"supplies:record_purchase"`.
55
- - The permission check must answer exactly `true` or `false`, and any other answer makes the call raise an error.
57
+ - The permission check must answer exactly `true` or `false`, and any other answer makes the call or the listing raise an error.
56
58
  - The account scope must take the block and run it, or the hub method never runs.
57
59
 
58
60
  6. Add one hub check per hub to the host's tests, such as `test/hubs/supplies_hub_test.rb`:
@@ -19,6 +19,8 @@ develop:
19
19
  - exposes
20
20
  - exposed
21
21
  - call_exposed
22
+ - exposures
23
+ - exposures_for
22
24
  - HubKernel::Refused
23
25
  - HubKernel::UnexposedMethodError
24
26
  - HubKernel::MissingArgumentError
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.15.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tylercschneider