hub_kernel-api 0.4.0 → 0.6.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: e54d19b67798c82119ecff94f0f294353cd927da83a55282b2f61ffbf111061d
4
- data.tar.gz: 1d48fc7eb9520cb6300315462a74159b68a0e236c797f87d97964c4455eefce1
3
+ metadata.gz: 16098ba0ceb84acb8536551867732dae646869b478d18299bb85a6839d1db8e1
4
+ data.tar.gz: 5b5b3d49bd804284796e8c7702b861b704c6e6ac77dba9a8e90b73f216845d52
5
5
  SHA512:
6
- metadata.gz: fa3f514ecb094b92a30e80fc390414abf616e68c8b479485462d5f7298d77cab57f3d813ced143fbafc58330f8685a7cac2d94aad14693a006730ff0c0ba6060
7
- data.tar.gz: 07c3ae8786feb3ad091474071d2dc7699ee8cc0b7ebb798f95cd53c7afaac53096e7df8d261bbd5c1e01ba4f13a613998b40cea868c4afccabcc12a0789c0fe9
6
+ metadata.gz: 816027b58264d0c192482819bd043cf28f14c3ec65c91043953c7fba75abdf21725c09db3809f65f506cdab7e89b04a556d7a225cce99da925e1c4b4726fbffc
7
+ data.tar.gz: 2befb90df535ea4e2df15e8d229b058d46339bf730937dabb3e4511bc890abc0a1d2a1f945b6b38b15258453d7ea9c8ef57212685ff0a7f7e8e32038de40bfbb
data/README.md CHANGED
@@ -17,9 +17,15 @@ HubKernel::Api.account_method = :current_account
17
17
 
18
18
  Rails.application.config.to_prepare do
19
19
  HubKernel::Api.hubs = [ Supplies ]
20
+ HubKernel::Api.check!
20
21
  end
21
22
  ```
22
23
 
24
+ `HubKernel::Api.check!` raises `HubKernel::Api::UnservableHubError` when a served entry
25
+ exposes no methods, when two served hubs answer at the same address, or when a served
26
+ hub's exposed list has a problem, naming each. Inside `to_prepare` it runs again after
27
+ every code reload.
28
+
23
29
  The base controller's own sign-in runs before every call. hub_kernel's permission check
24
30
  and account scope must also be set, as hub_kernel's readme describes. Mount the engine:
25
31
 
data/Rakefile CHANGED
@@ -4,3 +4,5 @@ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
4
  load "rails/tasks/engine.rake"
5
5
 
6
6
  require "bundler/gem_tasks"
7
+
8
+ require "the_local/rake"
@@ -1,5 +1,5 @@
1
1
  module HubKernel
2
2
  module Api
3
- VERSION = "0.4.0"
3
+ VERSION = "0.6.0"
4
4
  end
5
5
  end
@@ -4,6 +4,8 @@ require "hub_kernel/api/engine"
4
4
 
5
5
  module HubKernel
6
6
  module Api
7
+ class UnservableHubError < StandardError; end
8
+
7
9
  mattr_accessor :hubs, default: []
8
10
  mattr_accessor :base_controller, default: "ActionController::API"
9
11
  mattr_accessor :person_method
@@ -11,6 +13,24 @@ module HubKernel
11
13
 
12
14
  def self.find(name) = addresses[name]
13
15
 
14
- def self.addresses = hubs.reduce({}) { |found, entry| found.merge(entry.is_a?(Hash) ? entry : { entry.name.demodulize.underscore => entry }) }
16
+ def self.check!
17
+ problems = unexposed_hubs + shared_addresses + exposure_problems
18
+ raise UnservableHubError, problems.join("\n") if problems.any?
19
+ end
20
+
21
+ def self.addresses = served.to_h
22
+
23
+ def self.served = hubs.flat_map { |entry| entry.is_a?(Hash) ? entry.to_a : [ [ entry.name.demodulize.underscore, entry ] ] }
24
+
25
+ def self.unexposed_hubs = served.map(&:last).reject { |hub| hub.respond_to?(:exposures) }.map { |hub| "#{hub.name} exposes no methods to serve" }
26
+
27
+ def self.shared_addresses
28
+ served.group_by(&:first).select { |_address, entries| entries.size > 1 }.map do |address, entries|
29
+ "#{entries.map { |_address, hub| hub.name }.join(" and ")} both answer at #{address}"
30
+ end
31
+ end
32
+
33
+ def self.exposure_problems = served.map(&:last).select { |hub| hub.respond_to?(:exposure_problems) }.flat_map(&:exposure_problems)
34
+ private_class_method :served, :unexposed_hubs, :shared_addresses, :exposure_problems
15
35
  end
16
36
  end
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: hub_kernel-api-develop
3
+ description: Use PROACTIVELY for calling a served hub over HTTP — listing the methods a caller may call at a hub's address, calling a read with GET, calling a write with POST, sending its values, and reading its answer or error status — MUST BE USED instead of hand-writing a controller or endpoint per hub method, or guessing at the API's responses.
4
+ tools: Read, Write, Edit, Grep
5
+ scope: hub JSON API — serving a hub_kernel hub's exposed methods as a JSON API in a host Rails app, each call behind the host's own sign-in and hub_kernel's permission check and account scope
6
+ ---
7
+
8
+ This local writes client code against the three endpoints below and follows the steps in
9
+ order. Where a step names a decision, it asks the developer and does not pick.
10
+
11
+ ## What hub_kernel-api is
12
+
13
+ A Rails engine that answers HTTP calls to the exposed methods of the hubs a host serves,
14
+ each call made after the host's sign-in and checked and scoped by hub_kernel's permission
15
+ check and account scope. Use this local when writing a front end, mobile app, service or
16
+ request test that calls those methods, or when working out why a call answered as it did.
17
+ Adding the gem, choosing the served hubs and mounting the engine belong to the install
18
+ local.
19
+
20
+ ## Interface
21
+
22
+ - `GET /<hub>` — lists the methods the signed-in caller may call on the hub at that
23
+ address, for the caller's account, each as `{ "name", "takes", "verb" }`.
24
+ - `GET /<hub>/<method>` — calls an exposed read with the values sent in the query string
25
+ and answers `{ "answer": <return value> }`.
26
+ - `POST /<hub>/<method>` — calls an exposed write with the values sent in the request body
27
+ or query string and answers `{ "answer": <return value> }`.
28
+
29
+ Every path is relative to the path the host mounted the engine at, such as
30
+ `/api/v1/hubs`.
31
+
32
+ ## How to use it
33
+
34
+ 1. Find the path the host mounted the API at in its `config/routes.rb`. Every URL
35
+ below starts with it. If the API is not mounted,
36
+ stop and use the install local.
37
+
38
+ 2. Find the hub's address. A hub answers at its module name underscored, so `Supplies`
39
+ answers at `supplies`, unless the host listed it under a name of its own, such as
40
+ `{ "money" => Billing::Ledger }`, in which case it answers only at `money`. Read the
41
+ host's served hubs list in its initializer to find it.
42
+
43
+ 3. Authenticate the request the way the host's sign-in requires, such as a session
44
+ cookie or a bearer token. Ask the developer which one the client uses. A request the
45
+ host's sign-in refuses is answered by the host, with the host's own status, and
46
+ reaches no hub.
47
+
48
+ 4. List what the caller may call:
49
+
50
+ ```
51
+ GET /api/v1/hubs/supplies
52
+ ```
53
+
54
+ ```json
55
+ [{ "name": "price_of", "takes": ["item"], "verb": "GET" },
56
+ { "name": "reorder", "takes": ["item", "quantity"], "verb": "POST" }]
57
+ ```
58
+
59
+ The list holds only the methods this caller is permitted on this account. Use `verb`
60
+ to choose GET or POST, and `takes` for the value names the method accepts.
61
+
62
+ 5. Call a read with GET, sending its values as query parameters:
63
+
64
+ ```
65
+ GET /api/v1/hubs/supplies/price_of?item=42
66
+ ```
67
+
68
+ Query values arrive as strings.
69
+
70
+ 6. Call a write with POST, sending its values as a JSON body with
71
+ `Content-Type: application/json`, keyed directly by value name with no wrapping key:
72
+
73
+ ```
74
+ POST /api/v1/hubs/supplies/reorder
75
+ Content-Type: application/json
76
+
77
+ { "item": 42, "quantity": 3 }
78
+ ```
79
+
80
+ Query parameters on a POST are sent to the method as well. Do not send a value whose
81
+ name is not in `takes`.
82
+
83
+ 7. Read the response by status:
84
+
85
+ | Case | Status | Body |
86
+ | --- | --- | --- |
87
+ | The method answers | 200 | `{"answer": <return value>}` |
88
+ | The hub is not served, the method is not exposed, the verb is wrong, or the permission check refuses the caller | 404 | `{"error": "Not found"}` |
89
+ | The hub refuses the call, or a required value is missing | 422 | `{"error": "<reason>"}` |
90
+ | A permitted call sends a value the method does not take | 422 | `{"error": "<method> does not take <values>"}` |
91
+ | A record the call names does not exist | 404 | `{"error": "No <record> has the id <id>"}` |
92
+
93
+ Show the `error` text of a 422 to the user, since it is the hub's reason. Treat
94
+ `Not found` as a method that does not exist for this caller.
95
+
96
+ 8. For a request test in the host, sign in as a person with and without the permission,
97
+ and assert the 200 answer for one and the `Not found` 404 for the other.
98
+
99
+ ## Conventions
100
+
101
+ - Never send GET to a write or POST to a read; the wrong verb is answered as not found,
102
+ never as a method error.
103
+ - A forbidden method and a missing one both answer `Not found`, so a client cannot tell
104
+ them apart, and must not try.
105
+ - The `answer` is the method's return value as JSON; nothing else is added to it.
106
+ - The account a call is made for comes from the host's sign-in, never from a value in the
107
+ request.
108
+ - Only the methods a hub exposes are reachable; adding a method to the API means exposing
109
+ it on the hub in hub_kernel, not adding a route.
110
+ - Installing the gem, configuring it and choosing the served hubs are out of scope here;
111
+ they belong to the install local.
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: hub_kernel-api-info
3
+ description: Use to learn what hub_kernel-api offers — serving a hub's exposed methods as a JSON API, the served hubs and their addresses, reads and writes, and how sign-in, permission and account scope apply to each call.
4
+ tools: Read
5
+ scope: hub JSON API — serving a hub_kernel hub's exposed methods as a JSON API in a host Rails app, each call behind the host's own sign-in and hub_kernel's permission check and account scope
6
+ ---
7
+
8
+ This local explains hub_kernel-api and makes no changes.
9
+
10
+ ## What hub_kernel-api is
11
+
12
+ hub_kernel-api is a Rails engine that serves the exposed methods of hub_kernel hubs
13
+ as a JSON API inside a host Rails app. The host lists which hubs it serves, and the
14
+ gem answers HTTP calls to their exposed methods and to a listing of them. It adds no
15
+ business logic of its own: every answer is the return value of a method the hub
16
+ already exposes.
17
+
18
+ Reach for it when a hub's exposed methods need to be called over HTTP, by a mobile
19
+ app, another service or a front end, without writing a controller per method. Each
20
+ call runs the host's own sign-in first, then hub_kernel's permission check and
21
+ account scope, so the API never shows a caller more than the app itself would.
22
+
23
+ ## Interface
24
+
25
+ This local declares no commands. The surface is split between the other two locals:
26
+
27
+ - **hub_kernel-api-install** owns adding the gem to a host, the configuration that
28
+ names the base controller, the person and account methods and the served hubs, the
29
+ startup check that rejects a hub that cannot be served, and mounting the engine.
30
+ - **hub_kernel-api-develop** owns the HTTP endpoints a client calls: the listing at a
31
+ hub's address and the call to one exposed method, with their verbs, request values,
32
+ responses and error statuses.
33
+
34
+ ## How to use it
35
+
36
+ - To put the API into a Rails app, or to serve another hub from one that has it, use
37
+ the install local.
38
+ - To write a client against the API, or to work out why a call answered as it did,
39
+ use the develop local.
40
+
41
+ ## Conventions
42
+
43
+ - **Hub** — a hub_kernel module whose methods are declared as exposed. Only exposed
44
+ methods are reachable; nothing else on the hub is.
45
+ - **Served hub** — a hub the host has listed for the API. A hub that is not served is
46
+ answered as not found, as if it did not exist.
47
+ - **Address** — the name a served hub answers at. By default it is the hub's module
48
+ name underscored, so `Supplies` answers at `supplies`. The host may give a hub a
49
+ different address, and the permission check still asks about the hub by its own
50
+ name, such as `ledger:record_spend`.
51
+ - **Read and write** — each exposed method is one or the other. A read answers GET
52
+ and a write answers POST, and the wrong verb is answered as not found.
53
+ - **Takes** — the values an exposed method is listed with. A permitted call that
54
+ sends a value outside that list is refused.
55
+ - **Person and account** — who is calling and which account the call is made for,
56
+ both supplied by the host's base controller after its own sign-in. Every call and
57
+ every listing is scoped to them.
58
+ - **Answer** — a successful call returns the method's return value under the
59
+ `answer` key.
60
+ - **Not found and refused** — a call the permission check denies is answered as not
61
+ found, so a caller cannot tell a forbidden method from a missing one. A call the hub
62
+ itself refuses, or one missing a required value, is answered as unprocessable with
63
+ the reason.
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: hub_kernel-api-install
3
+ description: Use to hook hub_kernel-api into a project — adding the gem, naming the base controller and its person and account methods, listing the served hubs, running the startup check, and mounting the engine.
4
+ tools: Bash, Read, Edit
5
+ scope: hub JSON API — serving a hub_kernel hub's exposed methods as a JSON API in a host Rails app, each call behind the host's own sign-in and hub_kernel's permission check and account scope
6
+ ---
7
+
8
+ This local follows the steps below exactly and invents none. Where a step names a
9
+ decision, it asks the developer and does not pick.
10
+
11
+ ## What hub_kernel-api is
12
+
13
+ A Rails engine that serves the exposed methods of hub_kernel hubs as a JSON API; hook it
14
+ in when a Rails app needs its hubs callable over HTTP.
15
+
16
+ ## Interface
17
+
18
+ - `gem "hub_kernel-api"` — the Gemfile line that adds the gem; it brings hub_kernel with it.
19
+ - `HubKernel::Api.base_controller=` — the name, as a String, of the host controller the
20
+ API's controllers inherit from; defaults to `"ActionController::API"`.
21
+ - `HubKernel::Api.person_method=` — the name, as a Symbol, of the method on the base
22
+ controller that returns the person a call is made for; no default.
23
+ - `HubKernel::Api.account_method=` — the name, as a Symbol, of the method on the base
24
+ controller that returns the account a call is made for; no default.
25
+ - `HubKernel::Api.hubs=` — the Array of hubs the API serves, each a hub module or a
26
+ one-pair Hash of address to hub module; defaults to empty.
27
+ - `HubKernel::Api.check!` — checks the served hubs and raises if any cannot be served.
28
+ - `HubKernel::Api::UnservableHubError` — the error `check!` raises, its message naming
29
+ every problem on its own line.
30
+ - `mount HubKernel::Api::Engine` — the routes line that puts the API at a path in the host.
31
+
32
+ ## How to use it
33
+
34
+ 1. Add the gem to the host's `Gemfile` and run `bundle install`:
35
+
36
+ ```ruby
37
+ gem "hub_kernel-api"
38
+ ```
39
+
40
+ 2. Confirm hub_kernel's permission check and account scope are already configured in
41
+ the host. Every call is checked and scoped by them. If the host has a hub_kernel
42
+ install local, use it for this; otherwise stop and ask the developer how they are
43
+ set, and do not configure them from guesswork.
44
+
45
+ 3. Ask the developer which controller the API inherits from. It must:
46
+ - run the host's sign-in before every action, as a `before_action` that refuses an
47
+ unsigned caller;
48
+ - define the method that returns the calling person;
49
+ - define the method that returns the account the call is made for.
50
+
51
+ The methods may be private. Offer two options: an existing API base controller that
52
+ already does all three, or a new one under `app/controllers/` written for this API.
53
+ Do not leave the default `ActionController::API`: it has no sign-in and no person or
54
+ account method, so every call would fail.
55
+
56
+ 4. Ask the developer which hubs to serve and at what address each answers. A hub module
57
+ answers at its module name underscored, so `Supplies` answers at `supplies`. To use a
58
+ different address, the hub is listed as a one-pair Hash, `{ "money" => Billing::Ledger }`.
59
+ Every listed hub must be a hub_kernel hub that exposes methods, and no two may share
60
+ an address.
61
+
62
+ 5. Create `config/initializers/hub_kernel_api.rb` with the answers from steps 3 and 4:
63
+
64
+ ```ruby
65
+ HubKernel::Api.base_controller = "Api::HubBaseController"
66
+ HubKernel::Api.person_method = :current_person
67
+ HubKernel::Api.account_method = :current_account
68
+
69
+ Rails.application.config.to_prepare do
70
+ HubKernel::Api.hubs = [ Supplies, { "money" => Billing::Ledger } ]
71
+ HubKernel::Api.check!
72
+ end
73
+ ```
74
+
75
+ - `base_controller`, `person_method` and `account_method` go at the top of the file,
76
+ outside `to_prepare`. The base controller name is read once, when the API's
77
+ controllers load, so it must be set before then.
78
+ - `hubs` and `check!` go inside `to_prepare`, so the hub constants resolve after each
79
+ code reload and the check runs again after each one.
80
+
81
+ 6. Ask the developer the path to mount the API at, then add the mount to
82
+ `config/routes.rb`:
83
+
84
+ ```ruby
85
+ mount HubKernel::Api::Engine => "/api/v1/hubs"
86
+ ```
87
+
88
+ 7. Boot the app, for example with `bin/rails runner "puts :ok"`. A
89
+ `HubKernel::Api::UnservableHubError` at boot names each problem:
90
+ - `<Hub> exposes no methods to serve` — the entry is not a hub with exposed methods;
91
+ remove it, or expose methods on it in hub_kernel.
92
+ - `<Hub> and <Hub> both answer at <address>` — give one of them its own address with
93
+ a one-pair Hash.
94
+ - Any other line is a problem with that hub's list of exposed methods, reported by
95
+ hub_kernel; fix it in the hub.
96
+
97
+ ## Conventions
98
+
99
+ - Run `bin/rails routes` after mounting and confirm the engine appears at the chosen path.
100
+ - To serve another hub later, add it to the `hubs` list inside `to_prepare` and boot
101
+ again so `check!` runs.
102
+ - An edit to the initializer takes effect only after the server restarts.
103
+ - Keep `HubKernel::Api.check!` in the initializer; without it a hub that cannot be served
104
+ is found only when a client calls it.
105
+ - Calling the API, its responses and its error statuses are out of scope here; they
106
+ belong to the develop local.
@@ -0,0 +1,25 @@
1
+ scope: hub JSON API — serving a hub_kernel hub's exposed methods as a JSON API in a host Rails app, each call behind the host's own sign-in and hub_kernel's permission check and account scope
2
+
3
+ install:
4
+ - gem "hub_kernel-api"
5
+ - HubKernel::Api.base_controller=
6
+ - HubKernel::Api.person_method=
7
+ - HubKernel::Api.account_method=
8
+ - HubKernel::Api.hubs=
9
+ - HubKernel::Api.check!
10
+ - HubKernel::Api::UnservableHubError
11
+ - mount HubKernel::Api::Engine
12
+
13
+ develop:
14
+ - GET /<hub>
15
+ - GET /<hub>/<method>
16
+ - POST /<hub>/<method>
17
+
18
+ sources:
19
+ - lib/hub_kernel/api.rb
20
+ - lib/hub_kernel/api/engine.rb
21
+ - config/routes.rb
22
+ - app/controllers/hub_kernel/api/hub_controller.rb
23
+ - app/controllers/hub_kernel/api/hubs_controller.rb
24
+ - app/controllers/hub_kernel/api/hub_calls_controller.rb
25
+ - README.md
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hub_kernel-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tylercschneider
@@ -57,6 +57,10 @@ files:
57
57
  - lib/hub_kernel/api.rb
58
58
  - lib/hub_kernel/api/engine.rb
59
59
  - lib/hub_kernel/api/version.rb
60
+ - the_local/agents/hub_kernel-api-develop.md
61
+ - the_local/agents/hub_kernel-api-info.md
62
+ - the_local/agents/hub_kernel-api-install.md
63
+ - the_local/interface.yml
60
64
  homepage: https://github.com/DYB-Development/hub_kernel-api
61
65
  licenses:
62
66
  - MIT