@abide/abide 0.123.1 → 0.123.2
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.
- package/CHANGELOG.md +6 -0
- package/docs/REGISTRY.md +2 -2
- package/docs/RULEBOOK.md +2 -2
- package/docs/abide.md +2 -2
- package/package.json +1 -1
- package/src/server/derivedArgs.ts +2 -71
- package/src/shared/validateJson.ts +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @abide/abide
|
|
2
2
|
|
|
3
|
+
## 0.123.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4ce217a: `validateJson` reads a member left `undefined` as absent, so an `args: jsonSchema(...)` gate accepts `note: undefined` where `note` is optional rather than refusing it 422, as the derived args schema already did (RULEBOOK 19.25).
|
|
8
|
+
|
|
3
9
|
## 0.123.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/docs/REGISTRY.md
CHANGED
|
@@ -273,13 +273,13 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
273
273
|
|
|
274
274
|
| Name | Signature | Meaning | Rules |
|
|
275
275
|
| --- | --- | --- | --- |
|
|
276
|
-
| `Schema<T>` | `((value: unknown) => T) \| StandardSchemaV1<T>` | The two forms a shape may be declared in. | 19.2, 19.3, 19.4, 19.6, 19.8, 19.9, 19.10, 19.18, 19.
|
|
276
|
+
| `Schema<T>` | `((value: unknown) => T) \| StandardSchemaV1<T>` | The two forms a shape may be declared in. | 19.2, 19.3, 19.4, 19.6, 19.8, 19.9, 19.10, 19.18, 19.11, 19.12 |
|
|
277
277
|
| `JsonValue` | `null \| boolean \| number \| string \| JsonValue[] \| { [k: string]: JsonValue }` | What an `Args` may hold. | — |
|
|
278
278
|
| `Issues<T>` | `T extends object ? Partial<Record<Paths<T> \| '', string[]>> : string[]` | What was wrong, keyed by where. | 19.4, 19.19 |
|
|
279
279
|
| `Paths<T>` | `string` | The dot-joined leaf paths of a type. | 19.5 |
|
|
280
280
|
| `JsonSchema` | `JsonValue` | A JSON Schema document, the native form. | 19.7, 19.14 |
|
|
281
281
|
| `jsonSchema` | `<T>(document: JsonSchema) => StandardSchemaV1<T>` | A JSON Schema document as a `Schema`, carrying its validator. | 19.13, 19.14, 19.23, 20.11 |
|
|
282
|
-
| `validateJson` | `<T>(schema: JsonSchema, value: unknown) => Issues<T> \| null` | The native validator. | 19.13 |
|
|
282
|
+
| `validateJson` | `<T>(schema: JsonSchema, value: unknown) => Issues<T> \| null` | The native validator. | 19.13, 19.25 |
|
|
283
283
|
|
|
284
284
|
## Headers abide generates
|
|
285
285
|
|
package/docs/RULEBOOK.md
CHANGED
|
@@ -1335,8 +1335,8 @@ to, and MUST NOT change the type of the value that slot gates.
|
|
|
1335
1335
|
19.24 The value a body produces MUST be typed from the body alone, and a `schema` declared beside it
|
|
1336
1336
|
MUST NOT change that type.
|
|
1337
1337
|
|
|
1338
|
-
19.25
|
|
1339
|
-
|
|
1338
|
+
19.25 `validateJson` MUST read a member left `undefined`, at any depth of a record, as absent to
|
|
1339
|
+
`required`, `properties` and `additionalProperties` — in process as the wire carries it (30.28).
|
|
1340
1340
|
|
|
1341
1341
|
# 20. Generated surfaces
|
|
1342
1342
|
|
package/docs/abide.md
CHANGED
|
@@ -8067,7 +8067,7 @@ Transports: what is written in place rather than imported.
|
|
|
8067
8067
|
|
|
8068
8068
|
| Name | Signature | Meaning | Rules |
|
|
8069
8069
|
| --- | --- | --- | --- |
|
|
8070
|
-
| `Schema<T>` | `((value: unknown) => T) \| StandardSchemaV1<T>` | The two forms a shape may be declared in. | [19.2](/rulebook#19.2), [19.3](/rulebook#19.3), [19.4](/rulebook#19.4), [19.6](/rulebook#19.6), [19.8](/rulebook#19.8), [19.9](/rulebook#19.9), [19.10](/rulebook#19.10), [19.18](/rulebook#19.18), [19.
|
|
8070
|
+
| `Schema<T>` | `((value: unknown) => T) \| StandardSchemaV1<T>` | The two forms a shape may be declared in. | [19.2](/rulebook#19.2), [19.3](/rulebook#19.3), [19.4](/rulebook#19.4), [19.6](/rulebook#19.6), [19.8](/rulebook#19.8), [19.9](/rulebook#19.9), [19.10](/rulebook#19.10), [19.18](/rulebook#19.18), [19.11](/rulebook#19.11), [19.12](/rulebook#19.12) |
|
|
8071
8071
|
| `JsonSchema` | `JsonValue` | A JSON Schema document, the native form. | [19.7](/rulebook#19.7), [19.14](/rulebook#19.14) |
|
|
8072
8072
|
|
|
8073
8073
|
## Taught in
|
|
@@ -8158,7 +8158,7 @@ Transports: what is written in place rather than imported.
|
|
|
8158
8158
|
|
|
8159
8159
|
| Name | Signature | Meaning | Rules |
|
|
8160
8160
|
| --- | --- | --- | --- |
|
|
8161
|
-
| `validateJson` | `<T>(schema: JsonSchema, value: unknown) => Issues<T> \| null` | The native validator. | [19.13](/rulebook#19.13) |
|
|
8161
|
+
| `validateJson` | `<T>(schema: JsonSchema, value: unknown) => Issues<T> \| null` | The native validator. | [19.13](/rulebook#19.13), [19.25](/rulebook#19.25) |
|
|
8162
8162
|
|
|
8163
8163
|
## Taught in
|
|
8164
8164
|
|
package/package.json
CHANGED
|
@@ -15,81 +15,12 @@ import { argsDocumentOf } from './coerceArgs.ts'
|
|
|
15
15
|
* called, whether or not an app mounted it. Not where its `args` gate publishes a document of its
|
|
16
16
|
* own, which holds the args itself (19.7). Args that did not come are held as an empty record: a
|
|
17
17
|
* schema is only derived where the handler's args are a required record, so a required member
|
|
18
|
-
* missing is refused, not read off nothing.
|
|
19
|
-
* passing a member `undefined` is the call the wire carries without it.
|
|
18
|
+
* missing is refused, not read off nothing.
|
|
20
19
|
*/
|
|
21
20
|
export function derivedArgs(
|
|
22
21
|
handler: HeldToAnnotation & Gated<unknown>,
|
|
23
22
|
schema: JsonValue,
|
|
24
23
|
): void {
|
|
25
24
|
if (argsDocumentOf(handler) !== undefined) return
|
|
26
|
-
handler[DERIVED_ARGS] = (args) => validateJson(schema,
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** The records `asSent` is inside of, so a cyclic value is walked once rather than forever. */
|
|
30
|
-
const WALKING: object[] = []
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* `value` as the wire carries it, which drops a member left `undefined` (19.25): a URL omits it
|
|
34
|
-
* (30.28) and JSON leaves it out, at any depth of a body. `value` itself wherever it holds none, so
|
|
35
|
-
* the walk allocates nothing for the call that writes no `undefined` member.
|
|
36
|
-
*
|
|
37
|
-
* AN `undefined` IN A LIST IS KEPT. The two encodings part there — JSON writes `null`, a URL the
|
|
38
|
-
* text `undefined` — so neither is the wire's, and the schema answers the value as given.
|
|
39
|
-
*/
|
|
40
|
-
export function asSent(value: unknown): unknown {
|
|
41
|
-
if (value === null || typeof value !== 'object' || WALKING.includes(value))
|
|
42
|
-
return value
|
|
43
|
-
WALKING.push(value)
|
|
44
|
-
try {
|
|
45
|
-
return Array.isArray(value)
|
|
46
|
-
? listAsSent(value)
|
|
47
|
-
: recordAsSent(value as Record<string, unknown>)
|
|
48
|
-
} finally {
|
|
49
|
-
WALKING.pop()
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function listAsSent(list: unknown[]): unknown[] {
|
|
54
|
-
let sent: unknown[] | null = null
|
|
55
|
-
for (let index = 0; index < list.length; index += 1) {
|
|
56
|
-
const member = list[index]
|
|
57
|
-
const held = asSent(member)
|
|
58
|
-
if (sent === null && held !== member) sent = list.slice(0, index)
|
|
59
|
-
sent?.push(held)
|
|
60
|
-
}
|
|
61
|
-
return sent ?? list
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function recordAsSent(
|
|
65
|
-
record: Record<string, unknown>,
|
|
66
|
-
): Record<string, unknown> {
|
|
67
|
-
let sent: Record<string, unknown> | null = null
|
|
68
|
-
for (const key in record) {
|
|
69
|
-
if (!Object.hasOwn(record, key)) continue
|
|
70
|
-
const member = record[key]
|
|
71
|
-
const held = member === undefined ? undefined : asSent(member)
|
|
72
|
-
if (sent === null && (member === undefined || held !== member))
|
|
73
|
-
sent = membersBefore(record, key)
|
|
74
|
-
if (sent !== null && held !== undefined) sent[key] = held
|
|
75
|
-
}
|
|
76
|
-
return sent ?? record
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* A copy of `record`'s own members ahead of `key`, in their order.
|
|
81
|
-
*
|
|
82
|
-
* NO PROTOTYPE, so an own `__proto__` member JSON parsed stays a member the schema reads rather
|
|
83
|
-
* than becoming the copy's prototype.
|
|
84
|
-
*/
|
|
85
|
-
function membersBefore(
|
|
86
|
-
record: Record<string, unknown>,
|
|
87
|
-
key: string,
|
|
88
|
-
): Record<string, unknown> {
|
|
89
|
-
const copy: Record<string, unknown> = Object.create(null)
|
|
90
|
-
for (const earlier in record) {
|
|
91
|
-
if (earlier === key) break
|
|
92
|
-
if (Object.hasOwn(record, earlier)) copy[earlier] = record[earlier]
|
|
93
|
-
}
|
|
94
|
-
return copy
|
|
25
|
+
handler[DERIVED_ARGS] = (args) => validateJson(schema, args ?? {})
|
|
95
26
|
}
|
|
@@ -486,6 +486,17 @@ function checkArray(
|
|
|
486
486
|
check(items, value[index], join(path, String(index)), found)
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Whether `value` carries `key` as JSON would: an own member, and not one left `undefined`.
|
|
491
|
+
*
|
|
492
|
+
* 19.25 — `undefined` IS NOT A JSON VALUE, so a member holding it is the member the wire leaves out:
|
|
493
|
+
* JSON drops it and a URL omits it (30.28). Read as present, `note?: string` refused a call in process
|
|
494
|
+
* passing `note: undefined` at 422 that the same call over the wire was answered for.
|
|
495
|
+
*/
|
|
496
|
+
function holds(value: Record<string, unknown>, key: string): boolean {
|
|
497
|
+
return value[key] !== undefined && Object.hasOwn(value, key)
|
|
498
|
+
}
|
|
499
|
+
|
|
489
500
|
function checkObject(
|
|
490
501
|
node: Node,
|
|
491
502
|
value: Record<string, unknown>,
|
|
@@ -504,7 +515,7 @@ function checkObject(
|
|
|
504
515
|
// `required: ['toString']` was satisfied by `{}`, and — the one that matters —
|
|
505
516
|
// `'__proto__' in declared` answered true, so `additionalProperties: false` accepted
|
|
506
517
|
// `{"__proto__": …}` as a declared member and handed the object on unchecked.
|
|
507
|
-
if (!
|
|
518
|
+
if (!holds(value, key))
|
|
508
519
|
noteOr(found, join(path, key), 'is required')
|
|
509
520
|
}
|
|
510
521
|
}
|
|
@@ -515,7 +526,7 @@ function checkObject(
|
|
|
515
526
|
if (!Object.hasOwn(declared, key)) continue
|
|
516
527
|
// An absent member is `required`'s business, not its schema's — checking it
|
|
517
528
|
// here would report every optional member as the wrong type.
|
|
518
|
-
if (!
|
|
529
|
+
if (!holds(value, key)) continue
|
|
519
530
|
check(
|
|
520
531
|
declared[key] as JsonValue,
|
|
521
532
|
value[key],
|
|
@@ -527,7 +538,7 @@ function checkObject(
|
|
|
527
538
|
const extra = node.additionalProperties
|
|
528
539
|
if (extra === undefined) return
|
|
529
540
|
for (const key in value) {
|
|
530
|
-
if (!
|
|
541
|
+
if (!holds(value, key)) continue
|
|
531
542
|
if (declared !== null && Object.hasOwn(declared, key)) continue
|
|
532
543
|
if (extra === false) {
|
|
533
544
|
noteOr(found, join(path, key), 'is not a known member')
|