hub_kernel-api 0.9.0 → 0.10.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 +231 -2
- data/app/controllers/hub_kernel/api/hub_calls_controller.rb +7 -1
- data/app/controllers/hub_kernel/api/hub_controller.rb +44 -0
- data/app/controllers/hub_kernel/api/hubs_controller.rb +1 -5
- data/app/controllers/hub_kernel/api/versions_controller.rb +15 -0
- data/config/routes.rb +1 -0
- data/lib/hub_kernel/api/description.rb +17 -0
- data/lib/hub_kernel/api/description_check.rb +54 -0
- data/lib/hub_kernel/api/description_files.rb +13 -0
- data/lib/hub_kernel/api/earlier_answers.rb +11 -0
- data/lib/hub_kernel/api/major_version.rb +13 -0
- data/lib/hub_kernel/api/test_helper.rb +24 -0
- data/lib/hub_kernel/api/version.rb +1 -1
- data/lib/hub_kernel/api.rb +72 -1
- data/lib/tasks/hub_kernel_api.rake +14 -0
- data/the_local/agents/hub_kernel-api-develop.md +343 -41
- data/the_local/agents/hub_kernel-api-info.md +102 -13
- data/the_local/agents/hub_kernel-api-install.md +118 -17
- data/the_local/interface.yml +26 -0
- metadata +11 -3
|
@@ -1,33 +1,87 @@
|
|
|
1
1
|
---
|
|
2
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.
|
|
3
|
+
description: Use PROACTIVELY for calling a served hub over HTTP — listing every served hub's methods and the version at the API's root address, listing the methods a caller may call at a hub's address, reading the JSON Schema of each method's answer from a listing, calling a read with GET, calling a write with POST, sending its values, naming the major version a client was built for with Accept-Version, reading the Api-Version a call was answered at, noticing from Api-Newest-Version that a newer major is served, noticing from Deprecation and Sunset that the pinned major is deprecated, when it was deprecated and when it stops being served, reading its answer or error status, handling a call refused because it names a retired major, writing each major's description file with the describe task, checking in CI that no major removes, renames or silently adds to what its committed description file holds, handling a BreakingChangeError from the check or describe task, checking a recorded answer against a description file without a running host, and testing in the host that a method's answer still matches its declared answer shape at a major — MUST BE USED instead of hand-writing a controller or endpoint per hub method, versioning the URL by hand, hand-writing a schema or type for a method's answer, hand-writing an API description file, hand-diffing description files to spot a breaking change, hand-rolling a JSON Schema check of an answer in a host test, or guessing at the API's responses.
|
|
4
4
|
tools: Read, Write, Edit, Grep
|
|
5
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-interface's permission check and account scope
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
This local writes client code against the
|
|
9
|
-
|
|
8
|
+
This local writes client code against the four endpoints, the six headers, the answer
|
|
9
|
+
shapes, the describe task and the description files below, runs the breaking change check
|
|
10
|
+
on those files, writes host tests with the answer shape assertion, and follows the steps in
|
|
11
|
+
order.
|
|
12
|
+
Where a step names a decision, it asks the developer and does not pick.
|
|
10
13
|
|
|
11
14
|
## What hub_kernel-api is
|
|
12
15
|
|
|
13
16
|
A Rails engine that answers HTTP calls to the exposed methods of the hubs a host serves,
|
|
14
17
|
each call made after the host's sign-in and checked and scoped by the permission check and
|
|
15
|
-
account scope that hub_kernel-interface holds.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
account scope that hub_kernel-interface holds. When the host lists major versions, every
|
|
19
|
+
method answers at the same address at every major, and the caller picks the major with a
|
|
20
|
+
request header. A host can keep a major listed but mark it retired, and then every call
|
|
21
|
+
naming it is refused as gone. A host can also mark a major it still serves deprecated, and
|
|
22
|
+
must give the date it was deprecated and the date it stops being served, and every answer
|
|
23
|
+
at it then carries headers giving those dates. Every
|
|
24
|
+
answer at a major older than the newest carries a header naming the newest. A listing can give the JSON Schema of each method's answer at that major,
|
|
25
|
+
and a rake task writes the same description for each major into a file a client can use
|
|
26
|
+
without reaching a running host. A second rake task fails when a major's current
|
|
27
|
+
description removes or renames something its committed file holds, or adds something
|
|
28
|
+
without the major's minor being raised. A call never checks its answer against its declared
|
|
29
|
+
shape, so the gem gives the host a test assertion that does. Use this local when writing a
|
|
30
|
+
front end, mobile app, service or request test that calls those methods, when generating
|
|
31
|
+
client types from the answer shapes, when writing or using the description files, when
|
|
32
|
+
checking a hub change for a breaking change or handling a failed check, when
|
|
33
|
+
testing in the host that a method's answer matches its shape, or when working out why a
|
|
34
|
+
call answered as it did. Adding the gem, choosing the served hubs, listing the majors, deprecating
|
|
35
|
+
or retiring a major, declaring
|
|
36
|
+
answer shapes and mounting the engine belong to the install local.
|
|
19
37
|
|
|
20
38
|
## Interface
|
|
21
39
|
|
|
40
|
+
- `GET /` — lists every served hub's methods the signed-in caller may call, keyed by the
|
|
41
|
+
hub's address, with the full version the listing describes, as
|
|
42
|
+
`{ "version", "hubs": { "<hub>": [...] } }`.
|
|
22
43
|
- `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" }`.
|
|
44
|
+
address, for the caller's account, each as `{ "name", "takes", "verb", "answer_shape" }`.
|
|
24
45
|
- `GET /<hub>/<method>` — calls an exposed read with the values sent in the query string
|
|
25
46
|
and answers `{ "answer": <return value> }`.
|
|
26
47
|
- `POST /<hub>/<method>` — calls an exposed write with the values sent in the request body
|
|
27
48
|
or query string and answers `{ "answer": <return value> }`.
|
|
49
|
+
- `Accept-Version` request header — names the major version the client was built for, as
|
|
50
|
+
the bare major number, such as `Accept-Version: 1`.
|
|
51
|
+
- `Api-Version` response header — the full major.minor.patch version the call was answered
|
|
52
|
+
at, such as `1.4.0`, absent on a 406 or 410.
|
|
53
|
+
- `Api-Newest-Version` response header — the full version of the newest major the host
|
|
54
|
+
lists, such as `2.0.1`, sent only when the answer was given at an older major.
|
|
55
|
+
- `Deprecation` response header — the date the major was deprecated, as `@` and the Unix
|
|
56
|
+
seconds of that date's midnight UTC, such as `@1790726400`, sent only when the answer was
|
|
57
|
+
given at a major the host marked deprecated.
|
|
58
|
+
- `Sunset` response header — the date a deprecated major stops being served, as an HTTP
|
|
59
|
+
date such as `Wed, 31 Mar 2027 00:00:00 GMT`, sent only when the answer was given at a
|
|
60
|
+
major the host marked deprecated.
|
|
61
|
+
- `Vary` response header — `Accept-Version` on every answer from a host that lists majors,
|
|
62
|
+
refusals included, so a cache keeps one copy per major.
|
|
63
|
+
- `answer_shape` listing field — the JSON Schema of a method's answer at the major the
|
|
64
|
+
listing describes, present only when one is declared for that major.
|
|
65
|
+
- `bin/rails hub_kernel_api:describe` — run in the host, writes one description file for
|
|
66
|
+
each major the host lists that is not retired into the host's `api_descriptions` folder,
|
|
67
|
+
and writes none while any major removes or renames something its committed file holds,
|
|
68
|
+
or adds something without its minor being raised.
|
|
69
|
+
- `bin/rails hub_kernel_api:check_descriptions` — run in the host, usually in CI, fails
|
|
70
|
+
when a major that is not retired and has a committed file removes or renames something
|
|
71
|
+
that file holds, or adds something without its minor being raised above the file's.
|
|
72
|
+
- `HubKernel::Api::BreakingChangeError` — the error both tasks raise, naming every
|
|
73
|
+
problem at once, one to a line.
|
|
74
|
+
- `api_descriptions/v<major>.json` — one major's description, written for no retired major, `{ "version", "hubs":
|
|
75
|
+
{ "<hub>": [...] } }`, holding every served method at that major whatever any caller may
|
|
76
|
+
call.
|
|
77
|
+
- `HubKernel::Api::TestHelper` — a module a host's Minitest test case includes, after
|
|
78
|
+
`require "hub_kernel/api/test_helper"`, to get `assert_answer_shape`.
|
|
79
|
+
- `assert_answer_shape(hub, name, answer, major:)` — translates a method's own answer down
|
|
80
|
+
to the named major as a call does, and fails the test when it does not match the method's
|
|
81
|
+
answer shape at that major.
|
|
28
82
|
|
|
29
|
-
Every path is relative to the path the host mounted the engine at, such as
|
|
30
|
-
`/api/v1/hubs`.
|
|
83
|
+
Every path in an endpoint is relative to the path the host mounted the engine at, such as
|
|
84
|
+
`/api/v1/hubs`. The `api_descriptions` folder is relative to the host's root.
|
|
31
85
|
|
|
32
86
|
## How to use it
|
|
33
87
|
|
|
@@ -37,63 +91,295 @@ Every path is relative to the path the host mounted the engine at, such as
|
|
|
37
91
|
2. Find the hub's address. A hub answers at its module name underscored, so `Supplies`
|
|
38
92
|
answers at `supplies`, unless the host listed it under a name of its own, such as
|
|
39
93
|
`{ "money" => Billing::Ledger }`, in which case it answers only at `money`. Read the
|
|
40
|
-
host's served hubs list in its initializer to find it
|
|
94
|
+
host's served hubs list in its initializer to find it, or read the keys of the root
|
|
95
|
+
address listing in step 6.
|
|
41
96
|
|
|
42
|
-
3.
|
|
97
|
+
3. Find the majors the host lists in its initializer, where each is listed by its full
|
|
98
|
+
version, such as `"2.0.1"`, and its major is the first number. A major listed with
|
|
99
|
+
`retired: true` is retired: it stays listed, but every call naming it is refused with a
|
|
100
|
+
410. A major listed with `deprecated: true` is still served in full, and always carries
|
|
101
|
+
a `deprecated_on:` date on which it was deprecated and a `sunset:` date on which the host
|
|
102
|
+
plans to stop serving it. A host whose majors hold a mistake, such as a deprecated major
|
|
103
|
+
missing either date, a major listed twice, or every major retired, does not start, so at
|
|
104
|
+
least one listed major is always served. The error of a 406 answer
|
|
105
|
+
also names every major listed that is not retired, and the error of a 410 names the
|
|
106
|
+
newest major listed.
|
|
107
|
+
|
|
108
|
+
- If the host lists no majors, skip step 4. The API ignores any `Accept-Version`
|
|
109
|
+
header on every endpoint, the root address included, every call answers with the
|
|
110
|
+
method's own answer, every listing gives each method's own answer shape, no answer
|
|
111
|
+
carries a version header or a `version` key, and the describe task writes no files.
|
|
112
|
+
- If the host lists majors, go to step 4.
|
|
113
|
+
|
|
114
|
+
4. Choose the major the client is built for, and send it on every request, both listings
|
|
115
|
+
included:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Accept-Version: 2
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Which major to pin is the developer's decision: ask. A new client normally pins the
|
|
122
|
+
newest major listed. Never pin a retired major, since every request naming it is
|
|
123
|
+
refused with a 410. Avoid pinning a deprecated major in a new client, since the host
|
|
124
|
+
plans to retire it. A request with no `Accept-Version` header is answered at the oldest
|
|
125
|
+
major listed that is not retired, so a client that omits it silently gets that major's
|
|
126
|
+
answer shape, and moves to a newer one when the host retires it. Send the major alone,
|
|
127
|
+
never the full version: `2`, not `2.0.1`.
|
|
128
|
+
|
|
129
|
+
At an older major, a method answers in the shape it had at that major. A method whose
|
|
130
|
+
answer changed declares its earlier answers on the hub, and the API converts the
|
|
131
|
+
newest answer down to the major named. At the newest major, the method's own answer is
|
|
132
|
+
returned. Write the client against the shape of the major it pins.
|
|
133
|
+
|
|
134
|
+
5. Authenticate the request the way the host's sign-in requires, such as a session
|
|
43
135
|
cookie or a bearer token. Ask the developer which one the client uses. A request the
|
|
44
136
|
host's sign-in refuses is answered by the host, with the host's own status, and
|
|
45
137
|
reaches no hub.
|
|
46
138
|
|
|
47
|
-
|
|
139
|
+
6. To discover everything the caller may call in one request, list the root address:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
GET /api/v1/hubs/
|
|
143
|
+
Accept-Version: 1
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{ "version": "1.4.0",
|
|
148
|
+
"hubs": { "supplies": [{ "name": "shelf_of", "takes": ["item"], "verb": "GET",
|
|
149
|
+
"answer_shape": { "type": "object", "required": ["shelf"] } }],
|
|
150
|
+
"money": [] } }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Each key under `hubs` is a served hub's address, and its list is that hub's listing as
|
|
154
|
+
step 7 gives it. A served hub on which the caller may call nothing is listed with an
|
|
155
|
+
empty list. `version` is the full version of the major the listing describes, and it is
|
|
156
|
+
absent when the host lists no majors.
|
|
157
|
+
|
|
158
|
+
7. To list one hub, list its address:
|
|
48
159
|
|
|
49
160
|
```
|
|
50
161
|
GET /api/v1/hubs/supplies
|
|
162
|
+
Accept-Version: 2
|
|
51
163
|
```
|
|
52
164
|
|
|
53
165
|
```json
|
|
54
|
-
[{ "name": "price_of", "takes": ["item"], "verb": "GET"
|
|
166
|
+
[{ "name": "price_of", "takes": ["item"], "verb": "GET",
|
|
167
|
+
"answer_shape": { "type": "object", "required": ["cents"] } },
|
|
55
168
|
{ "name": "reorder", "takes": ["item", "quantity"], "verb": "POST" }]
|
|
56
169
|
```
|
|
57
170
|
|
|
58
171
|
The list holds only the methods this caller is permitted on this account. Use `verb`
|
|
59
172
|
to choose GET or POST, and `takes` for the value names the method accepts.
|
|
60
173
|
|
|
61
|
-
|
|
174
|
+
8. Read each method's `answer_shape`, when present, as the JSON Schema of its `answer` at
|
|
175
|
+
the major the listing was asked at. At the newest major it is the method's own declared
|
|
176
|
+
shape. At an older major it is the shape declared for the nearest earlier answer at or
|
|
177
|
+
after that major, or the method's own when it declares no earlier answer at or after
|
|
178
|
+
that major. A method with no shape declared at that major has no `answer_shape` key, so
|
|
179
|
+
treat its answer as untyped rather than reusing another major's shape.
|
|
180
|
+
|
|
181
|
+
Whether the client validates answers against the shape or generates types from it is
|
|
182
|
+
the developer's decision: ask. Fetch the shapes with the same `Accept-Version` the
|
|
183
|
+
client sends on its calls, since a listing at another major gives another major's
|
|
184
|
+
shapes.
|
|
185
|
+
|
|
186
|
+
9. To have every method's shape at each major without a request, write the description
|
|
187
|
+
files in the host:
|
|
62
188
|
|
|
189
|
+
```sh
|
|
190
|
+
bin/rails hub_kernel_api:describe
|
|
63
191
|
```
|
|
64
|
-
|
|
192
|
+
|
|
193
|
+
The task creates the host's `api_descriptions` folder if it is missing and writes one
|
|
194
|
+
file per major the host lists that is not retired, named after the major, such as `api_descriptions/v1.json`
|
|
195
|
+
for a major listed as `"1.4.0"`:
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{ "version": "1.4.0",
|
|
199
|
+
"hubs": { "supplies": [{ "name": "shelf_of", "takes": ["item"], "verb": "GET",
|
|
200
|
+
"answer_shape": { "type": "object", "required": ["shelf"] } }] } }
|
|
65
201
|
```
|
|
66
202
|
|
|
67
|
-
|
|
203
|
+
`version` is the major's full version. Each key under `hubs` is a served hub's address,
|
|
204
|
+
and each method is given as the root address listing gives it at that major, with
|
|
205
|
+
`answer_shape` chosen by the rule in step 8 and absent when none is declared at that
|
|
206
|
+
major. Unlike a listing, a file holds every method each served hub exposes, whatever
|
|
207
|
+
any caller may call, and names no caller, so never read it as what a given caller may
|
|
208
|
+
call. Use a listing for that.
|
|
68
209
|
|
|
69
|
-
|
|
70
|
-
|
|
210
|
+
Run the task again after any change to a hub's exposed methods, their answer shapes,
|
|
211
|
+
their earlier answers, the served hubs or the listed majors. Running it with no change
|
|
212
|
+
writes identical files. The task overwrites the file of every major listed that is not
|
|
213
|
+
retired and deletes no file, so delete by hand the file of a major the host has retired
|
|
214
|
+
or no longer lists.
|
|
71
215
|
|
|
72
|
-
|
|
73
|
-
POST /api/v1/hubs/supplies/reorder
|
|
74
|
-
Content-Type: application/json
|
|
216
|
+
Whether the host commits the files is the developer's decision: ask.
|
|
75
217
|
|
|
76
|
-
|
|
77
|
-
|
|
218
|
+
The task refuses to write any file while a major removes or renames something its
|
|
219
|
+
committed file holds, or adds something without its minor being raised, and raises
|
|
220
|
+
`HubKernel::Api::BreakingChangeError` naming each problem, as step 10 describes.
|
|
221
|
+
|
|
222
|
+
10. To catch a breaking change before it ships, run the check in the host, and in the
|
|
223
|
+
host's CI, against the committed description files:
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
bin/rails hub_kernel_api:check_descriptions
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
It compares each major that is listed, is not retired and has a file in
|
|
230
|
+
`api_descriptions` with that major's current description, and passes or raises
|
|
231
|
+
`HubKernel::Api::BreakingChangeError` naming every problem at once, one to a line:
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
Major 2 removes or renames supplies.shelf_of.answer_shape.required.shelf, which its committed file holds
|
|
235
|
+
Major 2 adds supplies.shelf_of.takes.size without raising its minor above 2.1.0
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Each problem names the major and a dotted path into its file: the hub's address, the
|
|
239
|
+
method's name, then `takes`, `verb` or `answer_shape` and the keys and values below
|
|
240
|
+
it. A changed value shows as the old value removed, so a method moved from GET to
|
|
241
|
+
POST shows as `verb.GET` removed, and a rename shows as the old name removed. An
|
|
242
|
+
object in a list that has no `name` key, such as a choice in an `anyOf`, is named by
|
|
243
|
+
its position in the list, such as `answer_shape.anyOf.2`.
|
|
244
|
+
|
|
245
|
+
- A removal or rename breaks clients pinned to that major. Whether to keep the old
|
|
246
|
+
hub, method, value, verb or shape key at that major, or to make the change only in a
|
|
247
|
+
new major, is the developer's decision: ask. Never delete or edit the committed file
|
|
248
|
+
to make the check pass.
|
|
249
|
+
- An addition without a raised minor is fixed by raising the minor of that major's full
|
|
250
|
+
version in the host's version list above the minor the problem names, such as
|
|
251
|
+
`"2.1.0"` to `"2.2.0"`, then running `bin/rails hub_kernel_api:describe` to rewrite
|
|
252
|
+
the file. Ask the developer before changing the version.
|
|
253
|
+
- A change that keeps every key and value but changes what a value means passes, so
|
|
254
|
+
the check does not replace reviewing such a change.
|
|
255
|
+
- A retired major, a major the host no longer lists, and a major with no committed
|
|
256
|
+
file are not compared.
|
|
257
|
+
|
|
258
|
+
The describe task refuses to write while the check would fail, so fixing every
|
|
259
|
+
problem the check names comes before rewriting the files.
|
|
260
|
+
|
|
261
|
+
11. To check answers in a client repo without reaching a running host, copy in the file
|
|
262
|
+
for the major the client pins, and check a recorded `answer` against the method's
|
|
263
|
+
`answer_shape` from that file with a JSON Schema library. Which library to use is the
|
|
264
|
+
developer's decision: ask. Copy the file again whenever the host rewrites it.
|
|
265
|
+
|
|
266
|
+
12. Call a read with GET, sending its values as query parameters:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
GET /api/v1/hubs/supplies/price_of?item=42
|
|
270
|
+
Accept-Version: 2
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Query values arrive as strings.
|
|
274
|
+
|
|
275
|
+
13. Call a write with POST, sending its values as a JSON body with
|
|
276
|
+
`Content-Type: application/json`, keyed directly by value name with no wrapping key:
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
POST /api/v1/hubs/supplies/reorder
|
|
280
|
+
Content-Type: application/json
|
|
281
|
+
Accept-Version: 2
|
|
282
|
+
|
|
283
|
+
{ "item": 42, "quantity": 3 }
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Query parameters on a POST are sent to the method as well. Do not send a value whose
|
|
287
|
+
name is not in `takes`.
|
|
288
|
+
|
|
289
|
+
14. Read the response by status:
|
|
290
|
+
|
|
291
|
+
| Case | Status | Body |
|
|
292
|
+
| --- | --- | --- |
|
|
293
|
+
| The method answers, or a listing is given | 200 | `{"answer": <return value>}` for a call, the listing for a listing |
|
|
294
|
+
| 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"}` |
|
|
295
|
+
| The hub refuses the call, or a required value is missing | 422 | `{"error": "<reason>"}` |
|
|
296
|
+
| A permitted call sends a value the method does not take | 422 | `{"error": "<method> does not take <values>"}` |
|
|
297
|
+
| A record the call names does not exist | 404 | `{"error": "No <record> has the id <id>"}` |
|
|
298
|
+
| `Accept-Version` names a major the host does not list, on any endpoint | 406 | `{"error": "Major <n> is not served, the majors served are <majors>"}` |
|
|
299
|
+
| `Accept-Version` names a retired major, on any endpoint, whether or not the hub is served | 410 | `{"error": "Major <n> is no longer served, the newest major is <newest>"}` |
|
|
300
|
+
|
|
301
|
+
Show the `error` text of a 422 to the user, since it is the hub's reason. Treat
|
|
302
|
+
`Not found` as a method that does not exist for this caller. Treat a 406 as a client
|
|
303
|
+
pinned to a major the host does not list, and a 410 as a client pinned to a major the
|
|
304
|
+
host has retired. Fix the pinned major for either rather than retrying, and ask the
|
|
305
|
+
developer which major to move the client to.
|
|
306
|
+
|
|
307
|
+
15. Read the response headers from a host that lists majors:
|
|
308
|
+
|
|
309
|
+
- `Api-Version` holds the full version the answer was given at, such as `2.0.1`, on
|
|
310
|
+
every answer except a 406 or a 410. Use it to log or show which version served the
|
|
311
|
+
call.
|
|
312
|
+
- `Api-Newest-Version` holds the full version of the newest major listed, such as
|
|
313
|
+
`2.0.1`, on every answer given at an older major except a 406 or a 410. Its presence
|
|
314
|
+
means a newer major is served than the one the client pins.
|
|
315
|
+
- `Deprecation` holds the date the pinned major was deprecated, as `@` and the Unix
|
|
316
|
+
seconds of that date's midnight UTC, such as `@1790726400` for 30 September 2026, as
|
|
317
|
+
RFC 9745 defines it. It is on every answer given at a deprecated major except a 406
|
|
318
|
+
or a 410. Its presence means the host plans to stop serving the pinned major.
|
|
319
|
+
- `Sunset` holds the date the deprecated major stops being served, as an HTTP date such
|
|
320
|
+
as `Wed, 31 Mar 2027 00:00:00 GMT`. It is on every answer that carries `Deprecation`,
|
|
321
|
+
and on no other. After that date the host may retire the major, and calls naming it
|
|
322
|
+
are then refused with a 410.
|
|
323
|
+
- None of these headers changes the body of an answer. An answer at the newest major
|
|
324
|
+
carries no `Api-Newest-Version`, and carries `Deprecation` and `Sunset` only when the
|
|
325
|
+
newest major is itself deprecated.
|
|
326
|
+
- `Vary: Accept-Version` is on every answer, the 406 and 410 included. Any cache between the
|
|
327
|
+
client and the API must honour it.
|
|
328
|
+
|
|
329
|
+
What the client does on `Api-Newest-Version` or `Deprecation`, such as logging a
|
|
330
|
+
warning, showing a notice, or failing a check in CI, is the developer's decision: ask.
|
|
331
|
+
Never move the pinned major automatically from these headers, since the answer shapes
|
|
332
|
+
differ between majors.
|
|
333
|
+
|
|
334
|
+
16. For a request test in the host, sign in as a person with and without the permission,
|
|
335
|
+
and assert the 200 answer for one and the `Not found` 404 for the other. When the
|
|
336
|
+
host lists majors, also assert the `Api-Version` header for a named major, the
|
|
337
|
+
answer shape at an older major for a method that declares earlier answers, the
|
|
338
|
+
`answer_shape` a listing gives at that major, the root address listing's `version`,
|
|
339
|
+
the `Api-Newest-Version` header at an older major and its absence at the newest, the
|
|
340
|
+
`Deprecation` and `Sunset` header values at a deprecated major, the 406 for a major the
|
|
341
|
+
host does not list, and the 410 for a retired major.
|
|
342
|
+
|
|
343
|
+
17. To test in the host that a method's answer still matches its declared shape, add the
|
|
344
|
+
json_schemer gem to the host's test group, since the assertion needs it and the gem
|
|
345
|
+
does not depend on it:
|
|
346
|
+
|
|
347
|
+
```ruby
|
|
348
|
+
group :test do
|
|
349
|
+
gem "json_schemer"
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Then include the helper in a Minitest test case and assert one method's answer at one
|
|
354
|
+
major:
|
|
78
355
|
|
|
79
|
-
|
|
80
|
-
|
|
356
|
+
```ruby
|
|
357
|
+
require "hub_kernel/api/test_helper"
|
|
81
358
|
|
|
82
|
-
|
|
359
|
+
class SuppliesShapeTest < ActiveSupport::TestCase
|
|
360
|
+
include HubKernel::Api::TestHelper
|
|
83
361
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
| A permitted call sends a value the method does not take | 422 | `{"error": "<method> does not take <values>"}` |
|
|
90
|
-
| A record the call names does not exist | 404 | `{"error": "No <record> has the id <id>"}` |
|
|
362
|
+
test "shelf_of answers in its shape at major 1" do
|
|
363
|
+
assert_answer_shape(Supplies, :shelf_of, Supplies.shelf_of(item: "flour"), major: 1)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
```
|
|
91
367
|
|
|
92
|
-
|
|
93
|
-
|
|
368
|
+
- `hub` is the hub module, and `name` is the exposed method's name as a symbol.
|
|
369
|
+
- `answer` is the method's own answer, the value the hub returns, never an answer
|
|
370
|
+
already converted to an older major. The assertion converts it down to `major:`
|
|
371
|
+
through the earlier answers the hub declares, as a call does.
|
|
372
|
+
- `major:` is a bare major number the host lists, such as `1`, not a full version.
|
|
373
|
+
- The shape checked is the one a hub listing gives at that major, by the rule in step 8.
|
|
374
|
+
- A failure names the method and the major, then each place the answer differs from
|
|
375
|
+
the shape, one to a line.
|
|
376
|
+
- It also fails, naming the major and the method, when the host lists no such major,
|
|
377
|
+
which includes every major on a host that lists none, or when the method declares no
|
|
378
|
+
shape at that major.
|
|
379
|
+
- Without json_schemer installed it raises a `LoadError` naming the gem to add.
|
|
94
380
|
|
|
95
|
-
|
|
96
|
-
|
|
381
|
+
Which methods and which majors to assert, and which inputs to call each method with,
|
|
382
|
+
are the developer's decision: ask.
|
|
97
383
|
|
|
98
384
|
## Conventions
|
|
99
385
|
|
|
@@ -101,10 +387,26 @@ Every path is relative to the path the host mounted the engine at, such as
|
|
|
101
387
|
never as a method error.
|
|
102
388
|
- A forbidden method and a missing one both answer `Not found`, so a client cannot tell
|
|
103
389
|
them apart, and must not try.
|
|
104
|
-
- The `answer` is the method's return value as JSON
|
|
390
|
+
- The `answer` is the method's return value as JSON, converted to the named major's shape
|
|
391
|
+
when the host lists majors; nothing else is added to it.
|
|
392
|
+
- The major is chosen by the `Accept-Version` header alone; never put a version in the
|
|
393
|
+
path after the mount point or in a query parameter.
|
|
394
|
+
- A client against a host that lists majors always sends `Accept-Version`, and never
|
|
395
|
+
relies on the default, and never sends a retired major.
|
|
396
|
+
- An `answer_shape` describes one major only; never apply a shape fetched at one major to
|
|
397
|
+
answers at another, and never hand-write a shape the listing or a description file
|
|
398
|
+
already gives.
|
|
399
|
+
- A call never checks its answer against its shape; check it in a host test with
|
|
400
|
+
`assert_answer_shape`, never with a hand-written JSON Schema check of the declared shape.
|
|
401
|
+
- A description file is written by the describe task only; never edit one by hand, and
|
|
402
|
+
never use one to decide what a caller may call.
|
|
403
|
+
- A breaking change is found by `bin/rails hub_kernel_api:check_descriptions` against the
|
|
404
|
+
committed files; never pass it by deleting or editing a committed file.
|
|
105
405
|
- The account a call is made for comes from the host's sign-in, never from a value in the
|
|
106
406
|
request.
|
|
107
407
|
- Only the methods a hub exposes are reachable; adding a method to the API means exposing
|
|
108
408
|
it on the hub, not adding a route.
|
|
109
|
-
-
|
|
110
|
-
|
|
409
|
+
- `Api-Newest-Version`, `Deprecation` and `Sunset` are notices only; never treat an
|
|
410
|
+
answer carrying them as an error, and never switch majors at runtime because of them.
|
|
411
|
+
- Installing the gem, configuring it, choosing the served hubs, listing the majors,
|
|
412
|
+
deprecating or retiring a major, declaring a method's earlier answers and declaring answer shapes are out of scope here.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
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.
|
|
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, the listings at a hub's address and at the root address with each method's answer shape, the major versions a host serves, deprecates with the date it was deprecated and the date it stops, and retires, the headers telling a caller its major is not the newest or is deprecated, the description file written for each served major, the check that fails when a major removes something its file holds or adds something without raising its minor, the test assertion that checks an answer against its shape, and how sign-in, permission and account scope apply to each call.
|
|
4
4
|
tools: Read
|
|
5
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-interface's permission check and account scope
|
|
6
6
|
---
|
|
@@ -11,9 +11,9 @@ This local explains hub_kernel-api and makes no changes.
|
|
|
11
11
|
|
|
12
12
|
hub_kernel-api is a Rails engine that serves the exposed methods of hub_kernel hubs
|
|
13
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
|
|
15
|
-
|
|
16
|
-
already exposes.
|
|
14
|
+
gem answers HTTP calls to their exposed methods, a listing of one hub's methods, and a
|
|
15
|
+
listing of every served hub's methods at the API's root address. It adds no business
|
|
16
|
+
logic of its own: every answer is the return value of a method the hub already exposes.
|
|
17
17
|
|
|
18
18
|
It depends on hub_kernel-interface alone, not on hub_kernel. hub_kernel-interface
|
|
19
19
|
holds the permission check and the account scope, and the host sets both.
|
|
@@ -22,24 +22,48 @@ Reach for it when a hub's exposed methods need to be called over HTTP, by a mobi
|
|
|
22
22
|
app, another service or a front end, without writing a controller per method. Each
|
|
23
23
|
call runs the host's own sign-in first, then hub_kernel-interface's permission check
|
|
24
24
|
and account scope, so the API never shows a caller more than the app itself would.
|
|
25
|
+
A client can learn from the listings what it may call and what shape each answer
|
|
26
|
+
takes. A host can also serve several major versions of the API at the same addresses,
|
|
27
|
+
so an older client keeps getting the answers it was built for after a method's answer
|
|
28
|
+
changes. It can mark a major it plans to stop serving as deprecated, with the date it
|
|
29
|
+
was deprecated and the date it stops, so callers at that major are told on every
|
|
30
|
+
answer, and can retire a major it no longer serves so callers naming it are told it is
|
|
31
|
+
gone. It can write a JSON description of each served major into the app, so the API at
|
|
32
|
+
each major can be read and compared without calling it, and its CI can fail a change that
|
|
33
|
+
breaks a major's file. A host's tests can check that a
|
|
34
|
+
method's answer still matches the shape it declares at a major, since a call never checks
|
|
35
|
+
that itself.
|
|
25
36
|
|
|
26
37
|
## Interface
|
|
27
38
|
|
|
28
39
|
This local declares no commands. The surface is split between the other two locals:
|
|
29
40
|
|
|
30
41
|
- **hub_kernel-api-install** owns adding the gem to a host, the configuration that
|
|
31
|
-
names the base controller, the person and account methods
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
names the base controller, the person and account methods, the served hubs, the
|
|
43
|
+
majors served, which of them are deprecated, when each was deprecated and when each
|
|
44
|
+
stops, and which are retired, the startup check that rejects a hub that cannot be served
|
|
45
|
+
or a list of majors with a mistake in it, the errors a host can meet, and mounting the
|
|
46
|
+
engine.
|
|
47
|
+
- **hub_kernel-api-develop** owns the HTTP endpoints a client calls: the listing at
|
|
48
|
+
the root address, the listing at a hub's address, and the call to one exposed
|
|
49
|
+
method, with their verbs, request values, the headers that name and report a
|
|
50
|
+
version, the headers that report a newer major and a deprecated one, the answer
|
|
51
|
+
shapes the listings give, responses and error statuses, the task that writes each
|
|
52
|
+
served major's description file and where those files go, the check that compares
|
|
53
|
+
each major with its file and the error it raises, and the test assertion
|
|
54
|
+
that checks an answer against its shape.
|
|
36
55
|
|
|
37
56
|
## How to use it
|
|
38
57
|
|
|
39
|
-
- To put the API into a Rails app,
|
|
40
|
-
the install
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
- To put the API into a Rails app, to serve another hub from one that has it, to
|
|
59
|
+
list the majors a host serves, to deprecate one, or to retire one, use the install
|
|
60
|
+
local.
|
|
61
|
+
- To write a client against the API, to discover what it may call and the shape of
|
|
62
|
+
each answer, to call it at a particular major, to notice that its major is
|
|
63
|
+
deprecated or has a newer one, to work out why a call answered
|
|
64
|
+
as it did, to write and read the description files, to have CI fail a breaking
|
|
65
|
+
change to a major, or to test that a method's
|
|
66
|
+
answer matches its shape, use the develop local.
|
|
43
67
|
|
|
44
68
|
## Conventions
|
|
45
69
|
|
|
@@ -58,9 +82,74 @@ This local declares no commands. The surface is split between the other two loca
|
|
|
58
82
|
- **Person and account** — who is calling and which account the call is made for,
|
|
59
83
|
both supplied by the host's base controller after its own sign-in. Every call and
|
|
60
84
|
every listing is scoped to them.
|
|
85
|
+
- **Hub listing** — the methods of one served hub the caller may call, each with its
|
|
86
|
+
name, the values it takes, its verb and its answer shape.
|
|
87
|
+
- **Root address listing** — every served hub's listing keyed by the hub's address,
|
|
88
|
+
given at the API's root address, together with the full version of the major it
|
|
89
|
+
describes.
|
|
61
90
|
- **Answer** — a successful call returns the method's return value under the
|
|
62
91
|
`answer` key.
|
|
92
|
+
- **Answer shape** — the JSON Schema a method declares for its answer, through
|
|
93
|
+
hub_kernel-interface. A listing gives each method's shape at the major the caller
|
|
94
|
+
named, and a method with no declared shape at that major is listed with no shape.
|
|
63
95
|
- **Not found and refused** — a call the permission check denies is answered as not
|
|
64
96
|
found, so a caller cannot tell a forbidden method from a missing one. A call the hub
|
|
65
97
|
itself refuses, or one missing a required value, is answered as unprocessable with
|
|
66
98
|
the reason.
|
|
99
|
+
- **Major and full version** — the host lists each major it serves by a full version
|
|
100
|
+
in major.minor.patch form, and the major is that version's first part. Every
|
|
101
|
+
method answers at the same address at every major, and a call or listing that names
|
|
102
|
+
no major is answered at the oldest one listed that is not retired.
|
|
103
|
+
- **Earlier answer** — a hub method always answers in the newest major's shape. A
|
|
104
|
+
method whose answer changed declares, through hub_kernel-interface, how to turn its
|
|
105
|
+
answer back into an older major's shape, and may declare that older shape too. A
|
|
106
|
+
call at an older major gets the answer passed through each of those from the newest
|
|
107
|
+
down to the major named.
|
|
108
|
+
- **Newest major** — the listed major with the highest number. Every answer at an
|
|
109
|
+
older major carries a header giving the newest major's full version, so a caller
|
|
110
|
+
learns a newer major exists without asking.
|
|
111
|
+
- **Deprecated major** — a major the host still serves but plans to stop serving, given
|
|
112
|
+
the date it was deprecated and the date it stops, both as dates. Every answer at it is
|
|
113
|
+
given as usual, and carries a header giving each of those dates. The headers never
|
|
114
|
+
change an answer's body, and a deprecated major can still be the one a call that names
|
|
115
|
+
no major is answered at.
|
|
116
|
+
- **Unserved major** — a call or listing naming a major the host does not list is
|
|
117
|
+
answered as not acceptable, with a message naming the majors that are served.
|
|
118
|
+
- **Retired major** — a major the host still lists but has marked as no longer served.
|
|
119
|
+
A call or listing naming it is answered as gone, with a message naming the newest
|
|
120
|
+
major, whether or not the hub it names is served. A retired major is never the one
|
|
121
|
+
a call that names no major is answered at.
|
|
122
|
+
- **No majors listed** — a host that lists no majors does no versioning: every call
|
|
123
|
+
gets the method's own answer, every listing gives each method's own shape, no
|
|
124
|
+
version is reported, no description file is written, and the startup check checks
|
|
125
|
+
no majors.
|
|
126
|
+
- **Startup check** — run when the host boots, it refuses to let the app start while a
|
|
127
|
+
served hub cannot be served or the listed majors hold a mistake, and names every
|
|
128
|
+
problem it found. A mistake in the majors is a version not a string in major.minor.patch form,
|
|
129
|
+
a major listed twice, a deprecated major missing either date, a date given as
|
|
130
|
+
something other than a date, every major retired, or a hub declaring an earlier answer
|
|
131
|
+
for a major the host does not list or for the newest major.
|
|
132
|
+
- **Description file** — one JSON file per listed major that is not retired, written into the host app on
|
|
133
|
+
request and named after the major. It gives the major's full version and every served
|
|
134
|
+
hub's methods, keyed by address, each with its values, verb and answer shape at that
|
|
135
|
+
major. Unlike a listing, it is written for no caller, so it holds every exposed method
|
|
136
|
+
of every served hub with no permission check or account scope applied. Writing the
|
|
137
|
+
files again with nothing changed writes identical files. Writing them is refused, with
|
|
138
|
+
nothing written, while any major removes or renames something its existing file holds,
|
|
139
|
+
or adds something without raising its minor.
|
|
140
|
+
- **Breaking change check** — a check a host's CI runs that compares each major that is
|
|
141
|
+
not retired with the description file it already has, and fails naming every problem
|
|
142
|
+
at once. A major breaks its file when its current description no longer holds a hub, a
|
|
143
|
+
method, a value the method takes, its verb, or a key or value in its answer shape that
|
|
144
|
+
the file holds, and a rename counts as the old name removed. An object in a list that
|
|
145
|
+
has no name, such as a choice in an `anyOf`, is matched by its position. It also breaks its file
|
|
146
|
+
when it adds something the file does not hold while the minor of its full version is no
|
|
147
|
+
higher than the minor in the file. A change that keeps every key and value but changes
|
|
148
|
+
what a value means passes, and a retired major or a major with no file is not compared.
|
|
149
|
+
- **Shape assertion** — a test assertion a host includes in its own tests. It takes a
|
|
150
|
+
hub, a method's name, the method's own answer and a listed major, turns the answer
|
|
151
|
+
down to that major as a call would, and checks it against the method's shape at that
|
|
152
|
+
major. It fails naming the method, the major and each place the answer differs, and
|
|
153
|
+
also fails when the host lists no such major or the method declares no shape at it.
|
|
154
|
+
It needs a JSON Schema gem in the host's test group, and hub_kernel-api loads that
|
|
155
|
+
gem only when the assertion runs, so a host without it still loads hub_kernel-api.
|