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.
@@ -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 three endpoints below and follows the steps in
9
- order. Where a step names a decision, it asks the developer and does not pick.
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. Use this local when writing a front end,
16
- mobile app, service or request test that calls those methods, or when working out why a
17
- call answered as it did. Adding the gem, choosing the served hubs and mounting the engine
18
- belong to the install local.
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. Authenticate the request the way the host's sign-in requires, such as a session
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
- 4. List what the caller may call:
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
- 5. Call a read with GET, sending its values as query parameters:
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
- GET /api/v1/hubs/supplies/price_of?item=42
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
- Query values arrive as strings.
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
- 6. Call a write with POST, sending its values as a JSON body with
70
- `Content-Type: application/json`, keyed directly by value name with no wrapping key:
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
- { "item": 42, "quantity": 3 }
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
- Query parameters on a POST are sent to the method as well. Do not send a value whose
80
- name is not in `takes`.
356
+ ```ruby
357
+ require "hub_kernel/api/test_helper"
81
358
 
82
- 7. Read the response by status:
359
+ class SuppliesShapeTest < ActiveSupport::TestCase
360
+ include HubKernel::Api::TestHelper
83
361
 
84
- | Case | Status | Body |
85
- | --- | --- | --- |
86
- | The method answers | 200 | `{"answer": <return value>}` |
87
- | 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"}` |
88
- | The hub refuses the call, or a required value is missing | 422 | `{"error": "<reason>"}` |
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
- Show the `error` text of a 422 to the user, since it is the hub's reason. Treat
93
- `Not found` as a method that does not exist for this caller.
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
- 8. For a request test in the host, sign in as a person with and without the permission,
96
- and assert the 200 answer for one and the `Not found` 404 for the other.
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; nothing else is added to it.
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
- - Installing the gem, configuring it and choosing the served hubs are out of scope here;
110
- they belong to the install local.
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 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.
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 and the served hubs, the
32
- startup check that rejects a hub that cannot be served, and mounting the engine.
33
- - **hub_kernel-api-develop** owns the HTTP endpoints a client calls: the listing at a
34
- hub's address and the call to one exposed method, with their verbs, request values,
35
- responses and error statuses.
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, or to serve another hub from one that has it, use
40
- the install local.
41
- - To write a client against the API, or to work out why a call answered as it did,
42
- use the develop local.
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.