@maroonedog/luq 2.4.4 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -10
- package/dist/chain/slot-type-guard.js +20 -6
- package/dist/chain/slot-type-guard.mjs +20 -6
- package/dist/compile/resolve-presence.js +36 -5
- package/dist/compile/resolve-presence.mjs +36 -5
- package/dist/json-schema/array-keyword-guards.d.ts +31 -0
- package/dist/json-schema/array-keyword-guards.js +120 -0
- package/dist/json-schema/array-keyword-guards.mjs +114 -0
- package/dist/json-schema/assert-object-keyword-values.d.ts +59 -0
- package/dist/json-schema/assert-object-keyword-values.js +149 -0
- package/dist/json-schema/assert-object-keyword-values.mjs +141 -0
- package/dist/json-schema/assert-supported-dialect.d.ts +37 -0
- package/dist/json-schema/assert-supported-dialect.js +113 -0
- package/dist/json-schema/assert-supported-dialect.mjs +109 -0
- package/dist/json-schema/build-from-schema.d.ts +22 -3
- package/dist/json-schema/build-from-schema.js +27 -5
- package/dist/json-schema/build-from-schema.mjs +27 -5
- package/dist/json-schema/collect-definitions.d.ts +7 -0
- package/dist/json-schema/collect-definitions.js +9 -0
- package/dist/json-schema/collect-definitions.mjs +10 -1
- package/dist/json-schema/declare-additional-properties.d.ts +11 -1
- package/dist/json-schema/declare-additional-properties.js +13 -1
- package/dist/json-schema/declare-additional-properties.mjs +13 -1
- package/dist/json-schema/declare-object-keywords.d.ts +4 -0
- package/dist/json-schema/declare-object-keywords.js +11 -4
- package/dist/json-schema/declare-object-keywords.mjs +11 -4
- package/dist/json-schema/declare-required-properties.js +6 -0
- package/dist/json-schema/declare-required-properties.mjs +6 -0
- package/dist/json-schema/declare-value-keywords.d.ts +10 -1
- package/dist/json-schema/declare-value-keywords.js +43 -4
- package/dist/json-schema/declare-value-keywords.mjs +43 -4
- package/dist/json-schema/extensions/json-schema/index.d.ts +2 -2
- package/dist/json-schema/extensions/json-schema/index.js +4 -1
- package/dist/json-schema/extensions/json-schema/index.mjs +1 -1
- package/dist/json-schema/extensions/json-schema/json-schema.d.ts +2 -2
- package/dist/json-schema/extensions/json-schema/json-schema.js +3 -0
- package/dist/json-schema/extensions/json-schema/json-schema.mjs +4 -1
- package/dist/json-schema/extensions/json-schema-full-feature/index.d.ts +3 -1
- package/dist/json-schema/extensions/json-schema-full-feature/index.js +18 -1
- package/dist/json-schema/extensions/json-schema-full-feature/index.mjs +11 -0
- package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.d.ts +9 -1
- package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.js +9 -2
- package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.mjs +9 -2
- package/dist/json-schema/flatten-array-schema.d.ts +11 -1
- package/dist/json-schema/flatten-array-schema.js +20 -7
- package/dist/json-schema/flatten-array-schema.mjs +20 -7
- package/dist/json-schema/index.d.ts +4 -0
- package/dist/json-schema/index.js +9 -1
- package/dist/json-schema/index.mjs +3 -0
- package/dist/json-schema/keyword-map-core.js +3 -1
- package/dist/json-schema/keyword-map-core.mjs +3 -1
- package/dist/json-schema/keyword-map-string.js +30 -1
- package/dist/json-schema/keyword-map-string.mjs +30 -1
- package/dist/json-schema/keyword-map.js +5 -0
- package/dist/json-schema/keyword-map.mjs +5 -0
- package/dist/json-schema/malformed-schema-error.d.ts +38 -0
- package/dist/json-schema/malformed-schema-error.js +103 -0
- package/dist/json-schema/malformed-schema-error.mjs +98 -0
- package/dist/json-schema/unsupported-dialect-error.d.ts +21 -0
- package/dist/json-schema/unsupported-dialect-error.js +58 -0
- package/dist/json-schema/unsupported-dialect-error.mjs +54 -0
- package/dist/plugins/one-of/one-of.js +18 -4
- package/dist/plugins/one-of/one-of.mjs +18 -4
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ compile error, not a rule that quietly never fires.
|
|
|
42
42
|
| Descending into a built-in (`"when.getTime"` on a `Date`) | compile error |
|
|
43
43
|
| A method that does not exist inside an element sub-chain | compile error |
|
|
44
44
|
| A JSON Schema keyword bound to a chain method that does not exist | compile error |
|
|
45
|
+
| A JSON Schema document declaring 2019-09 or 2020-12 | throws when the validator is built, rather than being read as Draft-07 |
|
|
45
46
|
| A documented example drifting from the API | fails CI |
|
|
46
47
|
|
|
47
48
|
That matters most when the code calling this library is generated rather than
|
|
@@ -50,10 +51,16 @@ an array wildcard gets a red squiggle, not a validator that passes everything.
|
|
|
50
51
|
|
|
51
52
|
Two consequences worth knowing before you read further:
|
|
52
53
|
|
|
53
|
-
- **Adoption is a patch,
|
|
54
|
+
- **Adoption is a patch, and so is removal.** A path you did not declare is not
|
|
54
55
|
validated, not required, and not read, so a partly-covered type is a normal
|
|
55
56
|
state rather than a half-finished one. There is nothing global to migrate: no
|
|
56
|
-
registry, no plugin installation, no shared configuration object.
|
|
57
|
+
registry, no plugin installation, no shared configuration object. Leaving is
|
|
58
|
+
the same size of change in the other direction — your types were never
|
|
59
|
+
authored here, so there is no generated file to delete and no inferred type to
|
|
60
|
+
replace by hand, and a consumer that takes a [Standard
|
|
61
|
+
Schema](https://luq.dev/standard-schema) does not change when you hand it a
|
|
62
|
+
different value. The rules themselves you would rewrite; that part is real,
|
|
63
|
+
and it is the same work whichever way you go.
|
|
57
64
|
- **Every rule you can call is a plugin you imported by name**, so the bundle
|
|
58
65
|
contains what you used and nothing else — an unimported plugin's method does
|
|
59
66
|
not even typecheck. What each configuration costs is measured on every build:
|
|
@@ -95,7 +102,9 @@ somewhere.
|
|
|
95
102
|
npm install @maroonedog/luq
|
|
96
103
|
```
|
|
97
104
|
|
|
98
|
-
Zero runtime dependencies. TypeScript 5.0 or later.
|
|
105
|
+
Zero runtime dependencies. TypeScript 5.0 or later. Your `tsconfig.json` does
|
|
106
|
+
not need `strict` for any of the compile errors above, and they are checked with
|
|
107
|
+
it on as well.
|
|
99
108
|
|
|
100
109
|
## The whole API
|
|
101
110
|
|
|
@@ -128,17 +137,58 @@ const result = orderValidator.validate({ reference: "ab", quantity: 0 });
|
|
|
128
137
|
if (!result.valid) {
|
|
129
138
|
for (const issue of result.issues) {
|
|
130
139
|
// issue.path "reference" — where, with array indices filled in
|
|
131
|
-
// issue.code "stringMin" — which rule
|
|
140
|
+
// issue.code "stringMin" — which rule; the vocabulary is pinned
|
|
132
141
|
// issue.message — the text, overridable per call
|
|
133
142
|
// issue.severity "error" — only "error" makes the value invalid
|
|
134
143
|
}
|
|
135
144
|
}
|
|
136
145
|
```
|
|
137
146
|
|
|
147
|
+
`issue.code` is the machine-readable half: a plugin's own name by default,
|
|
148
|
+
overridden per rule with `{ code }`. The set is not a convention. The codes the
|
|
149
|
+
library reports are enumerated in `config/issue-code.lock.json`, derived from
|
|
150
|
+
the source by `npm run generate:issue-codes` and checked by
|
|
151
|
+
`npm run check:issue-code-lock`, so renaming one is a diff in a committed file
|
|
152
|
+
and a deliberate act rather than a silent break. The lock records who reports
|
|
153
|
+
each code, because some codes are shared on purpose — a missing root and a
|
|
154
|
+
missing field both report `required`. Codes a gate carries (`skip`,
|
|
155
|
+
`validateIf`) sit in a separate list: they are accepted from a caller but no
|
|
156
|
+
issue can ever carry one.
|
|
157
|
+
|
|
138
158
|
`.v(path, chain)` declares rules for one field. A path you do not declare is
|
|
139
159
|
not validated, not required and not read, so covering a type partly is a normal
|
|
140
160
|
state rather than a half-finished one.
|
|
141
161
|
|
|
162
|
+
### `.strict()` — when you want the whole type covered
|
|
163
|
+
|
|
164
|
+
Partial cover is the default, so nothing tells you a field was forgotten. Add
|
|
165
|
+
`.strict()` before `.build()` and the compiler does: it asserts every leaf path
|
|
166
|
+
of your type has been declared, and if one is missing the returned object has
|
|
167
|
+
no `build()` and names what is missing.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { Builder } from "@maroonedog/luq";
|
|
171
|
+
import { requiredPlugin } from "@maroonedog/luq/plugins/required";
|
|
172
|
+
|
|
173
|
+
interface Pair {
|
|
174
|
+
readonly left: string;
|
|
175
|
+
readonly right: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export const pairs = Builder()
|
|
179
|
+
.use(requiredPlugin)
|
|
180
|
+
.for<Pair>()
|
|
181
|
+
.v("left", (b) => b.string.required())
|
|
182
|
+
.v("right", (b) => b.string.required())
|
|
183
|
+
.strict() // drop either .v() above and this line stops compiling
|
|
184
|
+
.build();
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
It has no runtime effect and rejects no extra property at run time — it is a
|
|
188
|
+
statement about your declarations, not about the data. For refusing unknown
|
|
189
|
+
keys in the value, see
|
|
190
|
+
[objectAdditionalProperties](https://luq.dev/plugins).
|
|
191
|
+
|
|
142
192
|
### What a built validator gives you
|
|
143
193
|
|
|
144
194
|
```ts
|
|
@@ -257,7 +307,7 @@ const forStringMin = PLUGIN_MANIFEST.filter((entry) =>
|
|
|
257
307
|
| [Getting started](https://luq.dev/docs/getting-started) | the builder, defaults, `normalize`, reading a result |
|
|
258
308
|
| [Core concepts](https://luq.dev/docs/core-concepts) | field paths, slots, presence, transforms |
|
|
259
309
|
| [Plugins](https://luq.dev/plugins) | every subpath, symbol, chain method and slot |
|
|
260
|
-
| [JSON Schema](https://luq.dev/json-schema) | reading a document in, writing one back out, measured
|
|
310
|
+
| [JSON Schema](https://luq.dev/json-schema) | reading a Draft-07 document in, writing one back out, measured conformance, and why a newer-dialect document is refused |
|
|
261
311
|
| [Standard Schema](https://luq.dev/standard-schema) | tRPC, TanStack Form, Hono, react-hook-form — and what does not cross that boundary |
|
|
262
312
|
| [Benchmarks](https://luq.dev/benchmarks) | bundle size and throughput, with the method |
|
|
263
313
|
| [Luq or zod?](https://luq.dev/luq-or-zod) | when schema-first is the better answer |
|
|
@@ -273,6 +323,8 @@ Breaking changes happen in a major and nowhere else, an API being removed is
|
|
|
273
323
|
deprecated one major ahead, and each major ships with the codemod needed to
|
|
274
324
|
cross it.
|
|
275
325
|
|
|
326
|
+
- **[CHANGELOG.md](https://github.com/maroonedog/luq/blob/master/CHANGELOG.md)** — every released version, what a caller
|
|
327
|
+
sees change in each, and which ones need reading before you take them
|
|
276
328
|
- **[CONTRIBUTING.md](https://github.com/maroonedog/luq/blob/master/CONTRIBUTING.md)** — `npm run verify` is the whole
|
|
277
329
|
contract; the gates and what each one refuses
|
|
278
330
|
- **[SECURITY.md](https://github.com/maroonedog/luq/blob/master/SECURITY.md)** — reporting, zero runtime dependencies, the
|
|
@@ -282,11 +334,13 @@ cross it.
|
|
|
282
334
|
|
|
283
335
|
## About the "universal platform" goal
|
|
284
336
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
337
|
+
The first published line — `0.1.0-alpha` through `0.1.2-alpha`, which the
|
|
338
|
+
migration guide and the docs site both call "1.x" although no `1.x` was ever
|
|
339
|
+
published — described a `.luq` DSL that would generate validators for other
|
|
340
|
+
languages, against dated milestones. Those dates have passed and none of it
|
|
341
|
+
shipped, so the plan has been withdrawn rather than moved: no part of it is in
|
|
342
|
+
this package, and this release makes no claim about when any of it will exist.
|
|
343
|
+
What is in the box is the TypeScript validation library described above.
|
|
290
344
|
|
|
291
345
|
## License
|
|
292
346
|
|
|
@@ -3,16 +3,30 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.slotTypeGuard = slotTypeGuard;
|
|
4
4
|
const types_1 = require("../types");
|
|
5
5
|
const create_rule_1 = require("../plugin-kit/create-rule");
|
|
6
|
+
/**
|
|
7
|
+
* Each code is SPELLED here rather than interpolated from the slot name.
|
|
8
|
+
*
|
|
9
|
+
* `${slot}Type` produced the same six strings, and produced them nowhere a
|
|
10
|
+
* reader could find them: config/issue-code.lock.json enumerates every code
|
|
11
|
+
* the library can report, and a code that only exists once a template has been
|
|
12
|
+
* evaluated cannot be enumerated, so these six were missing from the published
|
|
13
|
+
* vocabulary while being among the most commonly reported codes there are.
|
|
14
|
+
*/
|
|
6
15
|
const SLOT_TYPES = Object.freeze({
|
|
7
|
-
string: { accepts: types_1.isString, noun: "a string" },
|
|
8
|
-
number: { accepts: types_1.isNumber, noun: "a number" },
|
|
16
|
+
string: { accepts: types_1.isString, code: "stringType", noun: "a string" },
|
|
17
|
+
number: { accepts: types_1.isNumber, code: "numberType", noun: "a number" },
|
|
9
18
|
boolean: {
|
|
10
19
|
accepts: (value) => typeof value === "boolean",
|
|
20
|
+
code: "booleanType",
|
|
11
21
|
noun: "a boolean",
|
|
12
22
|
},
|
|
13
|
-
date: {
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
date: {
|
|
24
|
+
accepts: (value) => value instanceof Date,
|
|
25
|
+
code: "dateType",
|
|
26
|
+
noun: "a Date",
|
|
27
|
+
},
|
|
28
|
+
array: { accepts: types_1.isArray, code: "arrayType", noun: "an array" },
|
|
29
|
+
object: { accepts: types_1.isPlainObject, code: "objectType", noun: "an object" },
|
|
16
30
|
});
|
|
17
31
|
/**
|
|
18
32
|
* The rule `b.<slot>` starts its chain with, or null for a slot that claims
|
|
@@ -26,7 +40,7 @@ function slotTypeGuard(slot, severity) {
|
|
|
26
40
|
if (slotType === undefined)
|
|
27
41
|
return null;
|
|
28
42
|
return (0, create_rule_1.check)({
|
|
29
|
-
code:
|
|
43
|
+
code: slotType.code,
|
|
30
44
|
severity,
|
|
31
45
|
run: (value) => value === undefined || value === null || slotType.accepts(value)
|
|
32
46
|
? types_1.PASS
|
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { PASS, fail, isArray, isNumber, isPlainObject, isString, } from "../types/index.mjs";
|
|
2
2
|
import { check } from "../plugin-kit/create-rule.mjs";
|
|
3
|
+
/**
|
|
4
|
+
* Each code is SPELLED here rather than interpolated from the slot name.
|
|
5
|
+
*
|
|
6
|
+
* `${slot}Type` produced the same six strings, and produced them nowhere a
|
|
7
|
+
* reader could find them: config/issue-code.lock.json enumerates every code
|
|
8
|
+
* the library can report, and a code that only exists once a template has been
|
|
9
|
+
* evaluated cannot be enumerated, so these six were missing from the published
|
|
10
|
+
* vocabulary while being among the most commonly reported codes there are.
|
|
11
|
+
*/
|
|
3
12
|
const SLOT_TYPES = Object.freeze({
|
|
4
|
-
string: { accepts: isString, noun: "a string" },
|
|
5
|
-
number: { accepts: isNumber, noun: "a number" },
|
|
13
|
+
string: { accepts: isString, code: "stringType", noun: "a string" },
|
|
14
|
+
number: { accepts: isNumber, code: "numberType", noun: "a number" },
|
|
6
15
|
boolean: {
|
|
7
16
|
accepts: (value) => typeof value === "boolean",
|
|
17
|
+
code: "booleanType",
|
|
8
18
|
noun: "a boolean",
|
|
9
19
|
},
|
|
10
|
-
date: {
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
date: {
|
|
21
|
+
accepts: (value) => value instanceof Date,
|
|
22
|
+
code: "dateType",
|
|
23
|
+
noun: "a Date",
|
|
24
|
+
},
|
|
25
|
+
array: { accepts: isArray, code: "arrayType", noun: "an array" },
|
|
26
|
+
object: { accepts: isPlainObject, code: "objectType", noun: "an object" },
|
|
13
27
|
});
|
|
14
28
|
/**
|
|
15
29
|
* The rule `b.<slot>` starts its chain with, or null for a slot that claims
|
|
@@ -23,7 +37,7 @@ export function slotTypeGuard(slot, severity) {
|
|
|
23
37
|
if (slotType === undefined)
|
|
24
38
|
return null;
|
|
25
39
|
return check({
|
|
26
|
-
code:
|
|
40
|
+
code: slotType.code,
|
|
27
41
|
severity,
|
|
28
42
|
run: (value) => value === undefined || value === null || slotType.accepts(value)
|
|
29
43
|
? PASS
|
|
@@ -38,13 +38,43 @@ function countRestrictions(rule) {
|
|
|
38
38
|
(rule.allowNull ? 0 : 1) +
|
|
39
39
|
(rule.emptyStringIsMissing ? 1 : 0));
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Which of two equally restrictive rules is reported, decided on WHAT each one
|
|
43
|
+
* forbids: undefined first, then null, then the empty string.
|
|
44
|
+
*
|
|
45
|
+
* The order is declared here rather than falling out of something else. It
|
|
46
|
+
* runs from the broadest absence to the narrowest — a field that is not there
|
|
47
|
+
* at all, a field that is there holding null, a field holding "" — so the
|
|
48
|
+
* identity a caller sees names the largest thing that was missing.
|
|
49
|
+
*
|
|
50
|
+
* Nothing in this comparison reads a code. A code is a published name, pinned
|
|
51
|
+
* in config/issue-code.lock.json, and re-spelling one must not move which rule
|
|
52
|
+
* a failure is reported under; comparing the two codes made exactly that
|
|
53
|
+
* happen, silently, from a one-word rename.
|
|
54
|
+
*
|
|
55
|
+
* Two rules forbidding the identical three things are indistinguishable to
|
|
56
|
+
* every reader of the policy, so neither outranks the other and the one
|
|
57
|
+
* declared first stays. That is the only case where the order the methods were
|
|
58
|
+
* typed in is observable at all.
|
|
59
|
+
*/
|
|
60
|
+
function outranksOnRestriction(rule, incumbent) {
|
|
61
|
+
if (rule.allowUndefined !== incumbent.allowUndefined) {
|
|
62
|
+
return !rule.allowUndefined;
|
|
63
|
+
}
|
|
64
|
+
if (rule.allowNull !== incumbent.allowNull)
|
|
65
|
+
return !rule.allowNull;
|
|
66
|
+
if (rule.emptyStringIsMissing !== incumbent.emptyStringIsMissing) {
|
|
67
|
+
return rule.emptyStringIsMissing;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
41
71
|
/**
|
|
42
72
|
* The identity a failure is reported under: the rule that forbids the most,
|
|
43
|
-
* ties broken by
|
|
73
|
+
* ties broken by which absence it forbids.
|
|
44
74
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
75
|
+
* Both halves of the decision read only the three presence flags, so the whole
|
|
76
|
+
* policy — not merely its flags — is independent of the order the rules
|
|
77
|
+
* arrived in. `.optional().nullable()` and `.nullable().optional()` are
|
|
48
78
|
* deep-equal, which is the property the tests assert.
|
|
49
79
|
*/
|
|
50
80
|
function selectStrictestPresence(rules) {
|
|
@@ -55,7 +85,8 @@ function selectStrictestPresence(rules) {
|
|
|
55
85
|
continue;
|
|
56
86
|
}
|
|
57
87
|
const difference = countRestrictions(rule) - countRestrictions(strictest);
|
|
58
|
-
if (difference > 0 ||
|
|
88
|
+
if (difference > 0 ||
|
|
89
|
+
(difference === 0 && outranksOnRestriction(rule, strictest))) {
|
|
59
90
|
strictest = rule;
|
|
60
91
|
}
|
|
61
92
|
}
|
|
@@ -34,13 +34,43 @@ function countRestrictions(rule) {
|
|
|
34
34
|
(rule.allowNull ? 0 : 1) +
|
|
35
35
|
(rule.emptyStringIsMissing ? 1 : 0));
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Which of two equally restrictive rules is reported, decided on WHAT each one
|
|
39
|
+
* forbids: undefined first, then null, then the empty string.
|
|
40
|
+
*
|
|
41
|
+
* The order is declared here rather than falling out of something else. It
|
|
42
|
+
* runs from the broadest absence to the narrowest — a field that is not there
|
|
43
|
+
* at all, a field that is there holding null, a field holding "" — so the
|
|
44
|
+
* identity a caller sees names the largest thing that was missing.
|
|
45
|
+
*
|
|
46
|
+
* Nothing in this comparison reads a code. A code is a published name, pinned
|
|
47
|
+
* in config/issue-code.lock.json, and re-spelling one must not move which rule
|
|
48
|
+
* a failure is reported under; comparing the two codes made exactly that
|
|
49
|
+
* happen, silently, from a one-word rename.
|
|
50
|
+
*
|
|
51
|
+
* Two rules forbidding the identical three things are indistinguishable to
|
|
52
|
+
* every reader of the policy, so neither outranks the other and the one
|
|
53
|
+
* declared first stays. That is the only case where the order the methods were
|
|
54
|
+
* typed in is observable at all.
|
|
55
|
+
*/
|
|
56
|
+
function outranksOnRestriction(rule, incumbent) {
|
|
57
|
+
if (rule.allowUndefined !== incumbent.allowUndefined) {
|
|
58
|
+
return !rule.allowUndefined;
|
|
59
|
+
}
|
|
60
|
+
if (rule.allowNull !== incumbent.allowNull)
|
|
61
|
+
return !rule.allowNull;
|
|
62
|
+
if (rule.emptyStringIsMissing !== incumbent.emptyStringIsMissing) {
|
|
63
|
+
return rule.emptyStringIsMissing;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
37
67
|
/**
|
|
38
68
|
* The identity a failure is reported under: the rule that forbids the most,
|
|
39
|
-
* ties broken by
|
|
69
|
+
* ties broken by which absence it forbids.
|
|
40
70
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
71
|
+
* Both halves of the decision read only the three presence flags, so the whole
|
|
72
|
+
* policy — not merely its flags — is independent of the order the rules
|
|
73
|
+
* arrived in. `.optional().nullable()` and `.nullable().optional()` are
|
|
44
74
|
* deep-equal, which is the property the tests assert.
|
|
45
75
|
*/
|
|
46
76
|
function selectStrictestPresence(rules) {
|
|
@@ -51,7 +81,8 @@ function selectStrictestPresence(rules) {
|
|
|
51
81
|
continue;
|
|
52
82
|
}
|
|
53
83
|
const difference = countRestrictions(rule) - countRestrictions(strictest);
|
|
54
|
-
if (difference > 0 ||
|
|
84
|
+
if (difference > 0 ||
|
|
85
|
+
(difference === 0 && outranksOnRestriction(rule, strictest))) {
|
|
55
86
|
strictest = rule;
|
|
56
87
|
}
|
|
57
88
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Draft07Schema, Draft07SchemaObject } from "./draft07.types";
|
|
2
|
+
/** The two shapes §9.3.1 permits `items` to take. */
|
|
3
|
+
export type ItemsValue = Draft07Schema | readonly Draft07Schema[];
|
|
4
|
+
/**
|
|
5
|
+
* Reads `items` after checking it against the meta-schema: the keyword's own
|
|
6
|
+
* value, or undefined when the keyword is absent, and a refusal when it is
|
|
7
|
+
* neither of the two forms §9.3.1 defines.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readCheckedItems(schema: Draft07SchemaObject): ItemsValue | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Reads `additionalItems` after checking it is a single schema: the keyword's
|
|
12
|
+
* own value, or undefined when the keyword is absent, and a refusal for
|
|
13
|
+
* anything §4.4 does not call a schema.
|
|
14
|
+
*
|
|
15
|
+
* It is read only where §6.4.2 gives the keyword effect, which is beside the
|
|
16
|
+
* tuple form of `items`; beside the single form the draft ignores it, no rest
|
|
17
|
+
* branch is built and this reader is never reached.
|
|
18
|
+
*/
|
|
19
|
+
export declare function readCheckedAdditionalItems(schema: Draft07SchemaObject): Draft07Schema | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Reads `contains` after checking it is a single schema: the keyword's own
|
|
22
|
+
* value, or undefined when the keyword is absent, and a refusal for anything
|
|
23
|
+
* §4.4 does not call a schema. Unlike `items`, `contains` has no array form —
|
|
24
|
+
* a tuple there is a malformed value, not a second reading.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readCheckedContains(schema: Draft07SchemaObject): Draft07Schema | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Reads `uniqueItems` after checking it is a boolean: the keyword's own value,
|
|
29
|
+
* or undefined when the keyword is absent, and a refusal for anything else.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readCheckedUniqueItems(schema: Draft07SchemaObject): boolean | undefined;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readCheckedItems = readCheckedItems;
|
|
4
|
+
exports.readCheckedAdditionalItems = readCheckedAdditionalItems;
|
|
5
|
+
exports.readCheckedContains = readCheckedContains;
|
|
6
|
+
exports.readCheckedUniqueItems = readCheckedUniqueItems;
|
|
7
|
+
// ===========================================================================
|
|
8
|
+
// L8 src/json-schema/array-keyword-guards.ts — the meta-schema checks for
|
|
9
|
+
// `items`, `additionalItems`, `contains` and `uniqueItems`, read by
|
|
10
|
+
// flatten-array-schema.ts.
|
|
11
|
+
//
|
|
12
|
+
// Every one of these keywords decides whether a rule EXISTS, and each had a
|
|
13
|
+
// route where a value the meta-schema forbids produced no rule at all rather
|
|
14
|
+
// than an error. `items: 5` is not a schema, but the sub-schema walker takes it
|
|
15
|
+
// anyway and iterates its keys; a number has none, so the walker yields zero
|
|
16
|
+
// rules and every element passes — the document asked for a constraint and the
|
|
17
|
+
// built validator carries an always-true one in its place. `additionalItems: 5`
|
|
18
|
+
// takes that same route for the rest branch, so every element past the tuple
|
|
19
|
+
// goes unchecked. `contains: 5` is the same bug wearing a disguise: an
|
|
20
|
+
// always-true element branch matches the FIRST element, so the existence
|
|
21
|
+
// requirement collapses into "the array is non-empty" — the empty array is
|
|
22
|
+
// still refused, and that lawful-looking verdict is what makes the loss of the
|
|
23
|
+
// real constraint easy to miss. `uniqueItems` is honoured only on a strict
|
|
24
|
+
// `=== true`, so `uniqueItems: "yes"`, the shape a form encoding or a loose
|
|
25
|
+
// YAML loader produces, drops to nothing the same way.
|
|
26
|
+
//
|
|
27
|
+
// A validator that quietly enforces LESS than its document says cannot be
|
|
28
|
+
// detected downstream: it answers "valid" for exactly the inputs the schema
|
|
29
|
+
// meant to refuse, and nothing in the result says a constraint went missing.
|
|
30
|
+
// Build time is therefore the only place a caller can still act, so these
|
|
31
|
+
// read the value and refuse it there.
|
|
32
|
+
//
|
|
33
|
+
// What must survive the refusal, because Draft-07 defines it: `items` has TWO
|
|
34
|
+
// legal forms — one schema for every element, or an ARRAY of schemas where
|
|
35
|
+
// position i constrains element i (§9.3.1) — `additionalItems` and `contains`
|
|
36
|
+
// take the boolean form of a schema as readily as the object form (§4.4), and
|
|
37
|
+
// `uniqueItems: false` is a lawful no-op (§6.4.3), so only a NON-boolean is
|
|
38
|
+
// refused.
|
|
39
|
+
// ===========================================================================
|
|
40
|
+
const types_1 = require("../types");
|
|
41
|
+
const draft07_types_1 = require("./draft07.types");
|
|
42
|
+
const malformed_schema_error_1 = require("./malformed-schema-error");
|
|
43
|
+
const ITEMS_REASON = "the value must be a schema or an array of schemas";
|
|
44
|
+
const UNIQUE_ITEMS_REASON = "the value must be a boolean";
|
|
45
|
+
/**
|
|
46
|
+
* Reads `items` after checking it against the meta-schema: the keyword's own
|
|
47
|
+
* value, or undefined when the keyword is absent, and a refusal when it is
|
|
48
|
+
* neither of the two forms §9.3.1 defines.
|
|
49
|
+
*/
|
|
50
|
+
function readCheckedItems(schema) {
|
|
51
|
+
const items = schema.items;
|
|
52
|
+
if (items === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
if ((0, types_1.isArray)(items))
|
|
55
|
+
return checkTuplePositions(items);
|
|
56
|
+
if (!(0, draft07_types_1.isDraft07Schema)(items)) {
|
|
57
|
+
throw new malformed_schema_error_1.MalformedSchemaError("items", ITEMS_REASON, items);
|
|
58
|
+
}
|
|
59
|
+
return items;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Every position of the tuple form is a schema in its own right, so a bad one
|
|
63
|
+
* names its index: with the whole array rendered instead, a caller holding a
|
|
64
|
+
* long tuple is told only that something in it is wrong.
|
|
65
|
+
*/
|
|
66
|
+
function checkTuplePositions(positions) {
|
|
67
|
+
for (let index = 0; index < positions.length; index += 1) {
|
|
68
|
+
const position = positions[index];
|
|
69
|
+
if ((0, draft07_types_1.isDraft07Schema)(position))
|
|
70
|
+
continue;
|
|
71
|
+
throw new malformed_schema_error_1.MalformedSchemaError("items", `position ${String(index)} of the array form ${malformed_schema_error_1.SCHEMA_FORMS}`, position);
|
|
72
|
+
}
|
|
73
|
+
return positions;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Reads `additionalItems` after checking it is a single schema: the keyword's
|
|
77
|
+
* own value, or undefined when the keyword is absent, and a refusal for
|
|
78
|
+
* anything §4.4 does not call a schema.
|
|
79
|
+
*
|
|
80
|
+
* It is read only where §6.4.2 gives the keyword effect, which is beside the
|
|
81
|
+
* tuple form of `items`; beside the single form the draft ignores it, no rest
|
|
82
|
+
* branch is built and this reader is never reached.
|
|
83
|
+
*/
|
|
84
|
+
function readCheckedAdditionalItems(schema) {
|
|
85
|
+
const additionalItems = schema.additionalItems;
|
|
86
|
+
if (additionalItems === undefined)
|
|
87
|
+
return undefined;
|
|
88
|
+
if (!(0, draft07_types_1.isDraft07Schema)(additionalItems)) {
|
|
89
|
+
throw new malformed_schema_error_1.MalformedSchemaError("additionalItems", `the value ${malformed_schema_error_1.SCHEMA_FORMS}`, additionalItems);
|
|
90
|
+
}
|
|
91
|
+
return additionalItems;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Reads `contains` after checking it is a single schema: the keyword's own
|
|
95
|
+
* value, or undefined when the keyword is absent, and a refusal for anything
|
|
96
|
+
* §4.4 does not call a schema. Unlike `items`, `contains` has no array form —
|
|
97
|
+
* a tuple there is a malformed value, not a second reading.
|
|
98
|
+
*/
|
|
99
|
+
function readCheckedContains(schema) {
|
|
100
|
+
const contains = schema.contains;
|
|
101
|
+
if (contains === undefined)
|
|
102
|
+
return undefined;
|
|
103
|
+
if (!(0, draft07_types_1.isDraft07Schema)(contains)) {
|
|
104
|
+
throw new malformed_schema_error_1.MalformedSchemaError("contains", `the value ${malformed_schema_error_1.SCHEMA_FORMS}`, contains);
|
|
105
|
+
}
|
|
106
|
+
return contains;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Reads `uniqueItems` after checking it is a boolean: the keyword's own value,
|
|
110
|
+
* or undefined when the keyword is absent, and a refusal for anything else.
|
|
111
|
+
*/
|
|
112
|
+
function readCheckedUniqueItems(schema) {
|
|
113
|
+
const uniqueItems = schema.uniqueItems;
|
|
114
|
+
if (uniqueItems === undefined)
|
|
115
|
+
return undefined;
|
|
116
|
+
if (typeof uniqueItems !== "boolean") {
|
|
117
|
+
throw new malformed_schema_error_1.MalformedSchemaError("uniqueItems", UNIQUE_ITEMS_REASON, uniqueItems);
|
|
118
|
+
}
|
|
119
|
+
return uniqueItems;
|
|
120
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// ===========================================================================
|
|
2
|
+
// L8 src/json-schema/array-keyword-guards.ts — the meta-schema checks for
|
|
3
|
+
// `items`, `additionalItems`, `contains` and `uniqueItems`, read by
|
|
4
|
+
// flatten-array-schema.ts.
|
|
5
|
+
//
|
|
6
|
+
// Every one of these keywords decides whether a rule EXISTS, and each had a
|
|
7
|
+
// route where a value the meta-schema forbids produced no rule at all rather
|
|
8
|
+
// than an error. `items: 5` is not a schema, but the sub-schema walker takes it
|
|
9
|
+
// anyway and iterates its keys; a number has none, so the walker yields zero
|
|
10
|
+
// rules and every element passes — the document asked for a constraint and the
|
|
11
|
+
// built validator carries an always-true one in its place. `additionalItems: 5`
|
|
12
|
+
// takes that same route for the rest branch, so every element past the tuple
|
|
13
|
+
// goes unchecked. `contains: 5` is the same bug wearing a disguise: an
|
|
14
|
+
// always-true element branch matches the FIRST element, so the existence
|
|
15
|
+
// requirement collapses into "the array is non-empty" — the empty array is
|
|
16
|
+
// still refused, and that lawful-looking verdict is what makes the loss of the
|
|
17
|
+
// real constraint easy to miss. `uniqueItems` is honoured only on a strict
|
|
18
|
+
// `=== true`, so `uniqueItems: "yes"`, the shape a form encoding or a loose
|
|
19
|
+
// YAML loader produces, drops to nothing the same way.
|
|
20
|
+
//
|
|
21
|
+
// A validator that quietly enforces LESS than its document says cannot be
|
|
22
|
+
// detected downstream: it answers "valid" for exactly the inputs the schema
|
|
23
|
+
// meant to refuse, and nothing in the result says a constraint went missing.
|
|
24
|
+
// Build time is therefore the only place a caller can still act, so these
|
|
25
|
+
// read the value and refuse it there.
|
|
26
|
+
//
|
|
27
|
+
// What must survive the refusal, because Draft-07 defines it: `items` has TWO
|
|
28
|
+
// legal forms — one schema for every element, or an ARRAY of schemas where
|
|
29
|
+
// position i constrains element i (§9.3.1) — `additionalItems` and `contains`
|
|
30
|
+
// take the boolean form of a schema as readily as the object form (§4.4), and
|
|
31
|
+
// `uniqueItems: false` is a lawful no-op (§6.4.3), so only a NON-boolean is
|
|
32
|
+
// refused.
|
|
33
|
+
// ===========================================================================
|
|
34
|
+
import { isArray } from "../types/index.mjs";
|
|
35
|
+
import { isDraft07Schema } from "./draft07.types.mjs";
|
|
36
|
+
import { MalformedSchemaError, SCHEMA_FORMS } from "./malformed-schema-error.mjs";
|
|
37
|
+
const ITEMS_REASON = "the value must be a schema or an array of schemas";
|
|
38
|
+
const UNIQUE_ITEMS_REASON = "the value must be a boolean";
|
|
39
|
+
/**
|
|
40
|
+
* Reads `items` after checking it against the meta-schema: the keyword's own
|
|
41
|
+
* value, or undefined when the keyword is absent, and a refusal when it is
|
|
42
|
+
* neither of the two forms §9.3.1 defines.
|
|
43
|
+
*/
|
|
44
|
+
export function readCheckedItems(schema) {
|
|
45
|
+
const items = schema.items;
|
|
46
|
+
if (items === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (isArray(items))
|
|
49
|
+
return checkTuplePositions(items);
|
|
50
|
+
if (!isDraft07Schema(items)) {
|
|
51
|
+
throw new MalformedSchemaError("items", ITEMS_REASON, items);
|
|
52
|
+
}
|
|
53
|
+
return items;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Every position of the tuple form is a schema in its own right, so a bad one
|
|
57
|
+
* names its index: with the whole array rendered instead, a caller holding a
|
|
58
|
+
* long tuple is told only that something in it is wrong.
|
|
59
|
+
*/
|
|
60
|
+
function checkTuplePositions(positions) {
|
|
61
|
+
for (let index = 0; index < positions.length; index += 1) {
|
|
62
|
+
const position = positions[index];
|
|
63
|
+
if (isDraft07Schema(position))
|
|
64
|
+
continue;
|
|
65
|
+
throw new MalformedSchemaError("items", `position ${String(index)} of the array form ${SCHEMA_FORMS}`, position);
|
|
66
|
+
}
|
|
67
|
+
return positions;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Reads `additionalItems` after checking it is a single schema: the keyword's
|
|
71
|
+
* own value, or undefined when the keyword is absent, and a refusal for
|
|
72
|
+
* anything §4.4 does not call a schema.
|
|
73
|
+
*
|
|
74
|
+
* It is read only where §6.4.2 gives the keyword effect, which is beside the
|
|
75
|
+
* tuple form of `items`; beside the single form the draft ignores it, no rest
|
|
76
|
+
* branch is built and this reader is never reached.
|
|
77
|
+
*/
|
|
78
|
+
export function readCheckedAdditionalItems(schema) {
|
|
79
|
+
const additionalItems = schema.additionalItems;
|
|
80
|
+
if (additionalItems === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (!isDraft07Schema(additionalItems)) {
|
|
83
|
+
throw new MalformedSchemaError("additionalItems", `the value ${SCHEMA_FORMS}`, additionalItems);
|
|
84
|
+
}
|
|
85
|
+
return additionalItems;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Reads `contains` after checking it is a single schema: the keyword's own
|
|
89
|
+
* value, or undefined when the keyword is absent, and a refusal for anything
|
|
90
|
+
* §4.4 does not call a schema. Unlike `items`, `contains` has no array form —
|
|
91
|
+
* a tuple there is a malformed value, not a second reading.
|
|
92
|
+
*/
|
|
93
|
+
export function readCheckedContains(schema) {
|
|
94
|
+
const contains = schema.contains;
|
|
95
|
+
if (contains === undefined)
|
|
96
|
+
return undefined;
|
|
97
|
+
if (!isDraft07Schema(contains)) {
|
|
98
|
+
throw new MalformedSchemaError("contains", `the value ${SCHEMA_FORMS}`, contains);
|
|
99
|
+
}
|
|
100
|
+
return contains;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Reads `uniqueItems` after checking it is a boolean: the keyword's own value,
|
|
104
|
+
* or undefined when the keyword is absent, and a refusal for anything else.
|
|
105
|
+
*/
|
|
106
|
+
export function readCheckedUniqueItems(schema) {
|
|
107
|
+
const uniqueItems = schema.uniqueItems;
|
|
108
|
+
if (uniqueItems === undefined)
|
|
109
|
+
return undefined;
|
|
110
|
+
if (typeof uniqueItems !== "boolean") {
|
|
111
|
+
throw new MalformedSchemaError("uniqueItems", UNIQUE_ITEMS_REASON, uniqueItems);
|
|
112
|
+
}
|
|
113
|
+
return uniqueItems;
|
|
114
|
+
}
|