@amritk/generate-parsers 0.21.0 → 0.22.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 +67 -0
- package/dist/generators/generate-default-from-pattern.d.ts +24 -3
- package/dist/generators/generate-default-from-pattern.js +211 -14
- package/dist/generators/generate-parser-function.js +232 -4
- package/dist/generators/generate-strict-assertion.js +8 -2
- package/dist/generators/generate-type-checks.d.ts +21 -0
- package/dist/generators/generate-type-checks.js +38 -2
- package/dist/generators/generate-validation-expression.js +10 -1
- package/dist/helpers/get-default-value.js +2 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -254,6 +254,73 @@ emitting a parser that quietly accepts what the schema forbids. Coercing
|
|
|
254
254
|
(non-strict) parsers are documented to repair rather than reject, so they ignore
|
|
255
255
|
the rejecting keywords by design.
|
|
256
256
|
|
|
257
|
+
### How a coercing parser repairs a boolean
|
|
258
|
+
|
|
259
|
+
`type: 'boolean'` is repaired from a table of spellings, not from JavaScript
|
|
260
|
+
truthiness. `Boolean("false")` is `true` — as is `Boolean("no")`, `Boolean("0")`
|
|
261
|
+
and `Boolean(2)` — so a truthiness test read every conventional way of writing
|
|
262
|
+
*off* as *on*, which is precisely the input a coercing parser gets from
|
|
263
|
+
environment variables, query strings and hand-written config.
|
|
264
|
+
|
|
265
|
+
The accepted spellings are matched with surrounding whitespace trimmed and case
|
|
266
|
+
folded, so `FALSE` and ` No ` land with `false` and `no`:
|
|
267
|
+
|
|
268
|
+
| Input | Result |
|
|
269
|
+
| --- | --- |
|
|
270
|
+
| `true`, `yes`, `y`, `on`, `1`, the number `1` | `true` |
|
|
271
|
+
| `false`, `no`, `n`, `off`, `0`, `""`, the number `0` | `false` |
|
|
272
|
+
| anything else (`2`, `"maybe"`, an object, `null`) | the schema's `default`, else `false` |
|
|
273
|
+
|
|
274
|
+
The last row is the same rule the other scalars follow: a value that does not
|
|
275
|
+
denote a boolean is not repaired into a guess, it falls back to the default. This
|
|
276
|
+
is wider than Ajv's `coerceTypes`, which takes `"true"` / `"false"`, the numbers
|
|
277
|
+
`1` / `0`, and `null` (as `false`), and rejects the rest — including `"0"`,
|
|
278
|
+
`"1"` and `""`. A coercing parser has no "reject", so its choice is between a
|
|
279
|
+
default and a wrong answer. `null` is the one value both answer for and answer
|
|
280
|
+
differently: Ajv says `false`, a coercing parser says whatever the schema's
|
|
281
|
+
`default` is.
|
|
282
|
+
|
|
283
|
+
### How a coercing parser repairs a union
|
|
284
|
+
|
|
285
|
+
A coercing parser's contract is that whatever it returns is a valid instance of
|
|
286
|
+
the schema that produced it, and
|
|
287
|
+
`src/generators/coerced-output-validity.differential.test.ts` fuzzes exactly that
|
|
288
|
+
property against Ajv.
|
|
289
|
+
|
|
290
|
+
A union is the hard case, because "repair this value" first has to answer "toward
|
|
291
|
+
which branch?". The generated parser answers it in two steps:
|
|
292
|
+
|
|
293
|
+
1. **Recognition.** Every branch's shape predicate runs first. A value already in
|
|
294
|
+
a branch's shape takes that branch's parser and comes back unchanged, so valid
|
|
295
|
+
input costs one predicate call and is never rebuilt.
|
|
296
|
+
2. **Scoring.** A value matching no branch is scored against each branch and
|
|
297
|
+
repaired toward the best fit. A `const` tag is near-decisive — a matching tag
|
|
298
|
+
names the branch, a present-but-wrong one rules it out. Below that, a present
|
|
299
|
+
required property is strong evidence and a present, well-typed declared
|
|
300
|
+
property is weak evidence. Ties keep the earliest branch, which is `anyOf`'s
|
|
301
|
+
own order.
|
|
302
|
+
|
|
303
|
+
Scoring is why `{ name, folder }` is repaired toward the branch that declares
|
|
304
|
+
`folder`, rather than toward whichever branch happens to be written first —
|
|
305
|
+
which would invent a `sidebar` and discard the `folder` the author wrote. Every
|
|
306
|
+
term is decided at build time, so the emitted code is a handful of property
|
|
307
|
+
reads and no schema walking, and none of it runs for input that already matches.
|
|
308
|
+
|
|
309
|
+
Every union position is dispatched: a definition, a `$ref`, an array's `items`
|
|
310
|
+
(including a recursive one, where a branch's parser calls back into the
|
|
311
|
+
dispatcher), and a union written directly as a property value. Measured over
|
|
312
|
+
4000 mutated documents of the published Scalar configuration schema, **every
|
|
313
|
+
coerced output is a valid instance of its own schema**, and each of the 2514
|
|
314
|
+
documents Ajv rejects is repaired into one Ajv accepts.
|
|
315
|
+
|
|
316
|
+
Two limits are worth stating. A union carrying its *own* keywords alongside its
|
|
317
|
+
branches (`{ anyOf: […], required: […] }`) keeps the general coercion path,
|
|
318
|
+
because the dispatcher expresses the branches and not the siblings. And a union
|
|
319
|
+
of bare scalars in property position is left to that same path, since scoring
|
|
320
|
+
reads keys: with no object branch there is nothing to discriminate on, every
|
|
321
|
+
branch ties, and the first one wins — which is what the general path already
|
|
322
|
+
does.
|
|
323
|
+
|
|
257
324
|
### Conformance, measured
|
|
258
325
|
|
|
259
326
|
The coverage above is not a claim — it is checked against the corpus every
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Builds a default string for a `pattern`-constrained schema — a value a
|
|
3
|
+
* coercing parser can fall back to when the input cannot be repaired.
|
|
4
|
+
*
|
|
5
|
+
* The one invariant: **a returned default always matches the pattern it was
|
|
6
|
+
* built from** (and any length bounds handed in alongside it). Every candidate,
|
|
7
|
+
* synthesized or recognized, is tested against the real `RegExp` before it is
|
|
8
|
+
* returned, and anything that fails is discarded. That check is the whole point.
|
|
9
|
+
* Without it this returned a value the schema rejects — `'^\\+?\\d{3}\\d{4}$'`
|
|
10
|
+
* was answered with `"+1234567890"`, which has three digits too many — so a
|
|
11
|
+
* coercing parser "repaired" input into a document still invalid against its own
|
|
12
|
+
* schema, and the array or object built around it was invalid for the same
|
|
13
|
+
* reason.
|
|
14
|
+
*
|
|
15
|
+
* Two sources of candidates, in order:
|
|
16
|
+
*
|
|
17
|
+
* 1. {@link synthesize}, which reads the pattern and constructs a matching
|
|
18
|
+
* string from it. This covers the ordinary shapes real schemas use — slugs,
|
|
19
|
+
* identifiers, fixed-width codes — that no fixed list can anticipate.
|
|
20
|
+
* 2. A short list of well-known shapes (email, UUID, URL, semver, ISO date),
|
|
21
|
+
* which read better than a synthesized `"aaa"` when they happen to fit.
|
|
22
|
+
*
|
|
23
|
+
* Returns `null` when nothing verifies, which callers already treat as "no
|
|
24
|
+
* usable default" — the safe answer, and far better than a confident wrong one.
|
|
4
25
|
*/
|
|
5
|
-
export declare const generateDefaultFromPattern: (pattern: string) => string | null;
|
|
26
|
+
export declare const generateDefaultFromPattern: (pattern: string, minLength?: number, maxLength?: number) => string | null;
|
|
@@ -1,23 +1,220 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const RECOGNIZED_DEFAULTS = [
|
|
2
|
+
"user@example.com",
|
|
3
|
+
"00000000-0000-0000-0000-000000000000",
|
|
4
|
+
"https://example.com",
|
|
5
|
+
"1.0.0",
|
|
6
|
+
"2000-01-01",
|
|
7
|
+
"+1234567890"
|
|
8
|
+
];
|
|
9
|
+
const UNSUPPORTED = /\(\?[=!<]|\\[1-9]|\\b|\\B/;
|
|
10
|
+
const PREFERRED = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
11
|
+
const escapedChar = (sequence) => {
|
|
12
|
+
switch (sequence) {
|
|
13
|
+
case "d":
|
|
14
|
+
return "0";
|
|
15
|
+
case "w":
|
|
16
|
+
return "a";
|
|
17
|
+
case "s":
|
|
18
|
+
return " ";
|
|
19
|
+
case "D":
|
|
20
|
+
case "S":
|
|
21
|
+
return "a";
|
|
22
|
+
case "W":
|
|
23
|
+
return "-";
|
|
24
|
+
case "n":
|
|
25
|
+
return "\n";
|
|
26
|
+
case "t":
|
|
27
|
+
return " ";
|
|
28
|
+
case "r":
|
|
29
|
+
return "\r";
|
|
30
|
+
default:
|
|
31
|
+
return null;
|
|
4
32
|
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
33
|
+
};
|
|
34
|
+
const ONCE = { min: 1, max: 1 };
|
|
35
|
+
const pickFromClass = (body) => {
|
|
36
|
+
const negated = body.startsWith("^");
|
|
37
|
+
const source = negated ? body.slice(1) : body;
|
|
38
|
+
const members = [];
|
|
39
|
+
for (let i = 0; i < source.length; i++) {
|
|
40
|
+
const char = source[i];
|
|
41
|
+
if (char === "\\") {
|
|
42
|
+
const next = source[++i];
|
|
43
|
+
if (next === void 0)
|
|
44
|
+
return null;
|
|
45
|
+
members.push(escapedChar(next) ?? next);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (source[i + 1] === "-" && i + 2 < source.length && source[i + 2] !== "]") {
|
|
49
|
+
const low = char;
|
|
50
|
+
const high = source[i + 2];
|
|
51
|
+
const preferred = [...PREFERRED].find((candidate) => candidate >= low && candidate <= high);
|
|
52
|
+
members.push(preferred ?? low);
|
|
53
|
+
i += 2;
|
|
54
|
+
continue;
|
|
8
55
|
}
|
|
56
|
+
members.push(char);
|
|
9
57
|
}
|
|
10
|
-
if (
|
|
11
|
-
return
|
|
58
|
+
if (!negated) {
|
|
59
|
+
return [...PREFERRED].find((candidate) => members.includes(candidate)) ?? members[0] ?? null;
|
|
12
60
|
}
|
|
13
|
-
|
|
14
|
-
|
|
61
|
+
return [...PREFERRED].find((candidate) => !members.includes(candidate)) ?? null;
|
|
62
|
+
};
|
|
63
|
+
const readQuantifier = (pattern, start) => {
|
|
64
|
+
const char = pattern[start];
|
|
65
|
+
let quantifier = ONCE;
|
|
66
|
+
let next = start;
|
|
67
|
+
if (char === "*") {
|
|
68
|
+
quantifier = { min: 0, max: Number.POSITIVE_INFINITY };
|
|
69
|
+
next = start + 1;
|
|
70
|
+
} else if (char === "+") {
|
|
71
|
+
quantifier = { min: 1, max: Number.POSITIVE_INFINITY };
|
|
72
|
+
next = start + 1;
|
|
73
|
+
} else if (char === "?") {
|
|
74
|
+
quantifier = { min: 0, max: 1 };
|
|
75
|
+
next = start + 1;
|
|
76
|
+
} else if (char === "{") {
|
|
77
|
+
const close = pattern.indexOf("}", start);
|
|
78
|
+
const body = close === -1 ? null : pattern.slice(start + 1, close);
|
|
79
|
+
const match = body === null ? null : /^(\d+)(,(\d*)?)?$/.exec(body);
|
|
80
|
+
if (match && close !== -1) {
|
|
81
|
+
const min = Number(match[1]);
|
|
82
|
+
const max = match[2] === void 0 ? min : match[3] ? Number(match[3]) : Number.POSITIVE_INFINITY;
|
|
83
|
+
quantifier = { min, max };
|
|
84
|
+
next = close + 1;
|
|
85
|
+
}
|
|
15
86
|
}
|
|
16
|
-
if (pattern
|
|
17
|
-
|
|
87
|
+
if (quantifier !== ONCE && (pattern[next] === "?" || pattern[next] === "+"))
|
|
88
|
+
next += 1;
|
|
89
|
+
return { quantifier, next };
|
|
90
|
+
};
|
|
91
|
+
const parseAlternatives = (pattern, start, stop) => {
|
|
92
|
+
const alternatives = [];
|
|
93
|
+
let current = [];
|
|
94
|
+
let i = start;
|
|
95
|
+
while (i < stop) {
|
|
96
|
+
const char = pattern[i];
|
|
97
|
+
if (char === ")")
|
|
98
|
+
break;
|
|
99
|
+
if (char === "|") {
|
|
100
|
+
alternatives.push(current);
|
|
101
|
+
current = [];
|
|
102
|
+
i += 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
let node = null;
|
|
106
|
+
if (char === "(") {
|
|
107
|
+
let bodyStart = i + 1;
|
|
108
|
+
if (pattern.startsWith("(?:", i))
|
|
109
|
+
bodyStart = i + 3;
|
|
110
|
+
else if (pattern.startsWith("(?<", i)) {
|
|
111
|
+
const close = pattern.indexOf(">", i);
|
|
112
|
+
if (close === -1)
|
|
113
|
+
return { alternatives: [], next: stop };
|
|
114
|
+
bodyStart = close + 1;
|
|
115
|
+
}
|
|
116
|
+
const inner = parseAlternatives(pattern, bodyStart, stop);
|
|
117
|
+
if (pattern[inner.next] !== ")")
|
|
118
|
+
return { alternatives: [], next: stop };
|
|
119
|
+
const { quantifier, next } = readQuantifier(pattern, inner.next + 1);
|
|
120
|
+
node = { kind: "group", alternatives: inner.alternatives, quantifier };
|
|
121
|
+
i = next;
|
|
122
|
+
} else if (char === "[") {
|
|
123
|
+
let end = i + 1;
|
|
124
|
+
if (pattern[end] === "^")
|
|
125
|
+
end += 1;
|
|
126
|
+
if (pattern[end] === "]")
|
|
127
|
+
end += 1;
|
|
128
|
+
while (end < stop && pattern[end] !== "]")
|
|
129
|
+
end += pattern[end] === "\\" ? 2 : 1;
|
|
130
|
+
if (end >= stop)
|
|
131
|
+
return { alternatives: [], next: stop };
|
|
132
|
+
const picked = pickFromClass(pattern.slice(i + 1, end));
|
|
133
|
+
if (picked === null)
|
|
134
|
+
return { alternatives: [], next: stop };
|
|
135
|
+
const { quantifier, next } = readQuantifier(pattern, end + 1);
|
|
136
|
+
node = { kind: "class", value: picked, quantifier };
|
|
137
|
+
i = next;
|
|
138
|
+
} else if (char === "\\") {
|
|
139
|
+
const sequence = pattern[i + 1];
|
|
140
|
+
if (sequence === void 0)
|
|
141
|
+
return { alternatives: [], next: stop };
|
|
142
|
+
const { quantifier, next } = readQuantifier(pattern, i + 2);
|
|
143
|
+
node = { kind: "literal", value: escapedChar(sequence) ?? sequence, quantifier };
|
|
144
|
+
i = next;
|
|
145
|
+
} else if (char === ".") {
|
|
146
|
+
const { quantifier, next } = readQuantifier(pattern, i + 1);
|
|
147
|
+
node = { kind: "class", value: "a", quantifier };
|
|
148
|
+
i = next;
|
|
149
|
+
} else if (char === "^" || char === "$") {
|
|
150
|
+
i += 1;
|
|
151
|
+
continue;
|
|
152
|
+
} else {
|
|
153
|
+
const { quantifier, next } = readQuantifier(pattern, i + 1);
|
|
154
|
+
node = { kind: "literal", value: char, quantifier };
|
|
155
|
+
i = next;
|
|
156
|
+
}
|
|
157
|
+
current.push(node);
|
|
158
|
+
}
|
|
159
|
+
alternatives.push(current);
|
|
160
|
+
return { alternatives, next: i };
|
|
161
|
+
};
|
|
162
|
+
const render = (nodes, budget) => {
|
|
163
|
+
let out = "";
|
|
164
|
+
for (const node of nodes) {
|
|
165
|
+
const { min, max } = node.quantifier;
|
|
166
|
+
const count = Math.min(min + (max > min ? budget : 0), max);
|
|
167
|
+
const unit = node.kind === "group" ? render(node.alternatives[0] ?? [], budget) : node.value;
|
|
168
|
+
out += unit.repeat(count);
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
};
|
|
172
|
+
const synthesize = (pattern) => {
|
|
173
|
+
if (UNSUPPORTED.test(pattern))
|
|
174
|
+
return [];
|
|
175
|
+
const { alternatives } = parseAlternatives(pattern, 0, pattern.length);
|
|
176
|
+
const candidates = [];
|
|
177
|
+
for (const alternative of alternatives) {
|
|
178
|
+
for (let budget = 0; budget <= 64; budget++) {
|
|
179
|
+
const rendered = render(alternative, budget);
|
|
180
|
+
candidates.push(rendered);
|
|
181
|
+
if (rendered.length > 512)
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return candidates;
|
|
186
|
+
};
|
|
187
|
+
const compile = (pattern) => {
|
|
188
|
+
try {
|
|
189
|
+
return new RegExp(pattern, "u");
|
|
190
|
+
} catch {
|
|
191
|
+
try {
|
|
192
|
+
return new RegExp(pattern);
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const generateDefaultFromPattern = (pattern, minLength, maxLength) => {
|
|
199
|
+
if (pattern === "")
|
|
200
|
+
return null;
|
|
201
|
+
const regex = compile(pattern);
|
|
202
|
+
if (regex === null)
|
|
203
|
+
return null;
|
|
204
|
+
const low = typeof minLength === "number" && minLength > 0 ? minLength : 0;
|
|
205
|
+
const high = typeof maxLength === "number" && maxLength >= 0 ? maxLength : Number.POSITIVE_INFINITY;
|
|
206
|
+
const fits = (candidate) => {
|
|
207
|
+
if (candidate.length < low || candidate.length > high)
|
|
208
|
+
return false;
|
|
209
|
+
return regex.test(candidate);
|
|
210
|
+
};
|
|
211
|
+
for (const candidate of RECOGNIZED_DEFAULTS) {
|
|
212
|
+
if (fits(candidate))
|
|
213
|
+
return JSON.stringify(candidate);
|
|
18
214
|
}
|
|
19
|
-
|
|
20
|
-
|
|
215
|
+
for (const candidate of synthesize(pattern)) {
|
|
216
|
+
if (fits(candidate))
|
|
217
|
+
return JSON.stringify(candidate);
|
|
21
218
|
}
|
|
22
219
|
return null;
|
|
23
220
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { regexLiteral } from "@amritk/helpers/escape-regex-pattern";
|
|
2
|
+
import { generateTypeDefinition } from "@amritk/helpers/generate-type-definition";
|
|
2
3
|
import { quoteJsString } from "@amritk/helpers/quote-js-string";
|
|
3
4
|
import { refToName } from "@amritk/helpers/ref-to-name";
|
|
4
5
|
import { resolveRef } from "@amritk/helpers/resolve-ref";
|
|
5
|
-
import { missingCheck, safeAccessor, safeKey } from "@amritk/helpers/safe-accessor";
|
|
6
|
+
import { hasOwnCheck, missingCheck, safeAccessor, safeKey } from "@amritk/helpers/safe-accessor";
|
|
6
7
|
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasItems, hasMaxItems, hasMaxProperties, hasMinItems, hasMinProperties, hasOneOf, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
7
8
|
import { unknownKeyCheck } from "@amritk/helpers/unknown-key-check";
|
|
8
9
|
import { DEFAULT_UNKNOWN_KEYS } from "@amritk/helpers/unknown-keys-strategy";
|
|
@@ -12,7 +13,7 @@ import { getDiscriminatorValue } from "../helpers/get-discriminator-value.js";
|
|
|
12
13
|
import { assertNoUnsupportedKeywords } from "./assert-supported-keywords.js";
|
|
13
14
|
import { generateEnumCaseInsensitiveCoercion } from "./generate-enum-check.js";
|
|
14
15
|
import { generateBackstopAssertion, generateCompositionChecks, generateContainsCheck, generateKeyedValueChecks, generateObjectKeywordChecks, generateObjectSizeChecks, generateObjectStrictAssertion, generateScalarStrictAssertion, inlineAllOfMembers } from "./generate-strict-assertion.js";
|
|
15
|
-
import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, hasFastPathBlockingKeyword, isExclusiveUnion, isInlineObjectArrayProperty, isInlineObjectProperty, shapeValidatorName } from "./generate-type-checks.js";
|
|
16
|
+
import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, hasFastPathBlockingKeyword, isExclusiveUnion, isInlineObjectArrayProperty, isInlineObjectProperty, isInlineUnionProperty, isUnionArrayProperty, shapeValidatorName } from "./generate-type-checks.js";
|
|
16
17
|
import { generateUniqueItemsCheck } from "./generate-unique-items-check.js";
|
|
17
18
|
import { generatePrefixItemsMap, generateValidationExpression, getPrefixItems, isCoercibleItemSchema, prefixItemsCapsLength } from "./generate-validation-expression.js";
|
|
18
19
|
const generateParserName = (typeName) => {
|
|
@@ -209,6 +210,24 @@ const generateFallbackObject = (schema, useRefImports, typeName, suffix, subType
|
|
|
209
210
|
result += "\n }";
|
|
210
211
|
return declaresPrototypeMemberProperty(schema) ? `${result} as unknown as ${typeName}` : result;
|
|
211
212
|
};
|
|
213
|
+
const SCALAR_ROOT_CONSTRAINTS = [
|
|
214
|
+
"pattern",
|
|
215
|
+
"minLength",
|
|
216
|
+
"maxLength",
|
|
217
|
+
"minimum",
|
|
218
|
+
"maximum",
|
|
219
|
+
"exclusiveMinimum",
|
|
220
|
+
"exclusiveMaximum",
|
|
221
|
+
"multipleOf"
|
|
222
|
+
];
|
|
223
|
+
const isConstrainedScalarRoot = (schema) => {
|
|
224
|
+
if (!isSchemaObject(schema) || !hasType(schema))
|
|
225
|
+
return false;
|
|
226
|
+
if (schema.type !== "string" && schema.type !== "number" && schema.type !== "integer")
|
|
227
|
+
return false;
|
|
228
|
+
const record = schema;
|
|
229
|
+
return SCALAR_ROOT_CONSTRAINTS.some((keyword) => keyword in record);
|
|
230
|
+
};
|
|
212
231
|
const scalarDefaultLiteral = (schema) => {
|
|
213
232
|
const value = getDefaultValue(schema);
|
|
214
233
|
return value === "undefined" ? "{}" : value;
|
|
@@ -252,6 +271,149 @@ ${prelude} const _disc = ${discExpr};
|
|
|
252
271
|
return ${expr};
|
|
253
272
|
};`;
|
|
254
273
|
};
|
|
274
|
+
const SCORE_TAG_MATCH = 100;
|
|
275
|
+
const SCORE_TAG_MISMATCH = -100;
|
|
276
|
+
const SCORE_REQUIRED_PRESENT = 4;
|
|
277
|
+
const SCORE_REQUIRED_MISSING = -4;
|
|
278
|
+
const SCORE_DECLARED_PRESENT = 1;
|
|
279
|
+
const SCORE_DECLARED_WELL_TYPED = 2;
|
|
280
|
+
const resolveForScoring = (schema, rootSchema) => {
|
|
281
|
+
if (!isSchemaObject(schema) || !hasRef(schema) || rootSchema === void 0)
|
|
282
|
+
return schema;
|
|
283
|
+
const target = resolveRef(schema.$ref, rootSchema);
|
|
284
|
+
return target !== void 0 && isSchemaObject(target) ? target : schema;
|
|
285
|
+
};
|
|
286
|
+
const tagValueOf = (schema) => {
|
|
287
|
+
if (!isSchemaObject(schema))
|
|
288
|
+
return void 0;
|
|
289
|
+
if (hasConst(schema))
|
|
290
|
+
return schema.const;
|
|
291
|
+
if (hasEnum(schema) && schema.enum.length === 1)
|
|
292
|
+
return schema.enum[0];
|
|
293
|
+
return void 0;
|
|
294
|
+
};
|
|
295
|
+
const branchScoreExpression = (branch, ctx) => {
|
|
296
|
+
const resolved = resolveForScoring(branch, ctx.rootSchema);
|
|
297
|
+
if (!isSchemaObject(resolved) || !hasProperties(resolved))
|
|
298
|
+
return "0";
|
|
299
|
+
const properties = resolved.properties;
|
|
300
|
+
const required = new Set(hasRequired(resolved) ? resolved.required : []);
|
|
301
|
+
const terms = [];
|
|
302
|
+
for (const key of Object.keys(properties)) {
|
|
303
|
+
const propertySchema = resolveForScoring(properties[key], ctx.rootSchema);
|
|
304
|
+
const accessor = safeAccessor("input", key);
|
|
305
|
+
const present = hasOwnCheck("input", key);
|
|
306
|
+
const tag = tagValueOf(propertySchema);
|
|
307
|
+
if (tag !== void 0) {
|
|
308
|
+
terms.push(`(${accessor} === ${JSON.stringify(tag)} ? ${SCORE_TAG_MATCH} : ${present} ? ${SCORE_TAG_MISMATCH} : 0)`);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (required.has(key)) {
|
|
312
|
+
terms.push(`(${present} ? ${SCORE_REQUIRED_PRESENT} : ${SCORE_REQUIRED_MISSING})`);
|
|
313
|
+
} else {
|
|
314
|
+
terms.push(`(${present} ? ${SCORE_DECLARED_PRESENT} : 0)`);
|
|
315
|
+
}
|
|
316
|
+
const typeCheck = generatePropertyTypeCheck(accessor, properties[key], ctx.useRefImports, ctx.suffix);
|
|
317
|
+
if (typeCheck !== null)
|
|
318
|
+
terms.push(`(${typeCheck} ? ${SCORE_DECLARED_WELL_TYPED} : 0)`);
|
|
319
|
+
}
|
|
320
|
+
return terms.length === 0 ? "0" : terms.join(" + ");
|
|
321
|
+
};
|
|
322
|
+
const generateScoredUnionParser = (functionName, typeName, branches, ctx, exported = true) => {
|
|
323
|
+
if (branches.length < 2)
|
|
324
|
+
return null;
|
|
325
|
+
const preamble = [];
|
|
326
|
+
const dispatch = [];
|
|
327
|
+
const reserved = new Set(ctx.reservedNames ?? []);
|
|
328
|
+
const needsUnnarrowedAlias = branches.some((branch) => isSchemaObject(branch) && !(hasRef(branch) && ctx.useRefImports) && !(isObjectSchema(branch) && hasProperties(branch)));
|
|
329
|
+
const valueRef = needsUnnarrowedAlias ? "_u" : "input";
|
|
330
|
+
for (const [index, branch] of branches.entries()) {
|
|
331
|
+
if (!isSchemaObject(branch))
|
|
332
|
+
return null;
|
|
333
|
+
if (hasRef(branch) && ctx.useRefImports) {
|
|
334
|
+
const refName = refToName(branch.$ref, ctx.suffix);
|
|
335
|
+
if (refName === typeName)
|
|
336
|
+
return null;
|
|
337
|
+
dispatch.push({
|
|
338
|
+
predicate: `${shapeValidatorName(refName)}(input)`,
|
|
339
|
+
result: `${generateParserName(refName)}(${valueRef}) as ${typeName}`
|
|
340
|
+
});
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (hasRef(branch))
|
|
344
|
+
return null;
|
|
345
|
+
if (hasOneOf(branch) || hasAnyOf(branch) || hasAllOf(branch) || "not" in branch)
|
|
346
|
+
return null;
|
|
347
|
+
if ("if" in branch || "then" in branch || "else" in branch || "patternProperties" in branch)
|
|
348
|
+
return null;
|
|
349
|
+
if (!isObjectSchema(branch) || !hasProperties(branch)) {
|
|
350
|
+
const predicate = generatePropertyTypeCheck("input", branch, ctx.useRefImports, ctx.suffix);
|
|
351
|
+
if (predicate === null)
|
|
352
|
+
return null;
|
|
353
|
+
const coerced = generateValidationExpression("", branch, scalarDefaultLiteral(branch), true, ctx.rootSchema, void 0, valueRef, false, ctx.caseInsensitive);
|
|
354
|
+
dispatch.push({ predicate, result: `(${coerced}) as ${typeName}` });
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
let subName = `${typeName}_B${index}`;
|
|
358
|
+
while (reserved.has(subName))
|
|
359
|
+
subName = `${subName}_`;
|
|
360
|
+
reserved.add(subName);
|
|
361
|
+
const shapeValidator = generateShapeValidator(branch, subName, ctx.useRefImports, ctx.suffix, false, ctx.stripUnknown ?? false, reserved, ctx.unknownKeys ?? DEFAULT_UNKNOWN_KEYS);
|
|
362
|
+
if (shapeValidator.includes("(_input: unknown): boolean => false"))
|
|
363
|
+
return null;
|
|
364
|
+
preamble.push(generateTypeDefinition(branch, subName, {
|
|
365
|
+
...ctx.suffix ? { typeSuffix: ctx.suffix } : {},
|
|
366
|
+
...ctx.rootSchema ? { rootSchema: ctx.rootSchema } : {}
|
|
367
|
+
}).replace(/^export /, ""));
|
|
368
|
+
preamble.push(shapeValidator);
|
|
369
|
+
preamble.push(generateObjectParser(
|
|
370
|
+
branch,
|
|
371
|
+
subName,
|
|
372
|
+
ctx.useRefImports,
|
|
373
|
+
ctx.suffix,
|
|
374
|
+
ctx.logWarnings,
|
|
375
|
+
// Always the coercing parser: this whole path exists to repair.
|
|
376
|
+
false,
|
|
377
|
+
false,
|
|
378
|
+
ctx.stripUnknown ?? false,
|
|
379
|
+
ctx.rootSchema,
|
|
380
|
+
reserved,
|
|
381
|
+
ctx.caseInsensitive ?? false,
|
|
382
|
+
shapeValidator,
|
|
383
|
+
ctx.unknownKeys ?? DEFAULT_UNKNOWN_KEYS
|
|
384
|
+
));
|
|
385
|
+
dispatch.push({
|
|
386
|
+
predicate: `${shapeValidatorName(subName)}(input)`,
|
|
387
|
+
result: `${generateParserName(subName)}(${valueRef}) as ${typeName}`
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
const first = dispatch[0];
|
|
391
|
+
if (first === void 0)
|
|
392
|
+
return null;
|
|
393
|
+
const lines = [];
|
|
394
|
+
if (needsUnnarrowedAlias)
|
|
395
|
+
lines.push(" const _u: unknown = input;");
|
|
396
|
+
for (const { predicate, result } of dispatch) {
|
|
397
|
+
lines.push(` if (${predicate}) return ${result};`);
|
|
398
|
+
}
|
|
399
|
+
lines.push(` if (!isObject(input)) return ${first.result};`);
|
|
400
|
+
const scores = branches.map((branch, index) => ` const _s${index} = ${branchScoreExpression(branch, ctx)};`);
|
|
401
|
+
lines.push(...scores);
|
|
402
|
+
const last = dispatch[dispatch.length - 1];
|
|
403
|
+
let best = `${last?.result}`;
|
|
404
|
+
for (let index = dispatch.length - 2; index >= 0; index--) {
|
|
405
|
+
const atLeastAsGood = dispatch.slice(index + 1).map((_, offset) => `_s${index} >= _s${index + 1 + offset}`).join(" && ");
|
|
406
|
+
best = `${atLeastAsGood} ? ${dispatch[index]?.result} : ${best}`;
|
|
407
|
+
}
|
|
408
|
+
lines.push(` return ${best};`);
|
|
409
|
+
const head = preamble.length > 0 ? `${preamble.join("\n\n")}
|
|
410
|
+
|
|
411
|
+
` : "";
|
|
412
|
+
const exportPrefix = exported ? "export " : "";
|
|
413
|
+
return `${head}${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {
|
|
414
|
+
${lines.join("\n")}
|
|
415
|
+
};`;
|
|
416
|
+
};
|
|
255
417
|
const strictContext = (ctx) => ({
|
|
256
418
|
useRefImports: ctx?.useRefImports ?? false,
|
|
257
419
|
suffix: ctx?.suffix ?? "",
|
|
@@ -314,6 +476,9 @@ ${assertions}
|
|
|
314
476
|
}
|
|
315
477
|
if (hasOneOf(schema) || hasAnyOf(schema)) {
|
|
316
478
|
const branches = hasOneOf(schema) ? schema.oneOf : hasAnyOf(schema) ? schema.anyOf : [];
|
|
479
|
+
const scored = unionCtx ? generateScoredUnionParser(functionName, typeName, branches, unionCtx) : null;
|
|
480
|
+
if (scored !== null)
|
|
481
|
+
return scored;
|
|
317
482
|
const hasRefBranch = branches.some((b) => isSchemaObject(b) && hasRef(b));
|
|
318
483
|
if (branches.length > 0 && !hasRefBranch) {
|
|
319
484
|
const fallback = scalarDefaultLiteral(branches[0]);
|
|
@@ -415,6 +580,10 @@ ${assertion}
|
|
|
415
580
|
return ${returnExpr};
|
|
416
581
|
};`;
|
|
417
582
|
}
|
|
583
|
+
if (isConstrainedScalarRoot(schema)) {
|
|
584
|
+
const expr = generateValidationExpression("", schema, getDefaultValue(schema), true, unionCtx?.rootSchema, void 0, "input", true, unionCtx?.caseInsensitive);
|
|
585
|
+
return `export const ${functionName} = (input: unknown): ${typeName} => (${expr}) as ${typeName};`;
|
|
586
|
+
}
|
|
418
587
|
switch (schema.type) {
|
|
419
588
|
case "string":
|
|
420
589
|
return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "string" ? input as ${typeName} : "" as ${typeName};`;
|
|
@@ -519,7 +688,7 @@ const collectInlineSubTypes = (schema, typeName, reservedNames = NO_RESERVED_NAM
|
|
|
519
688
|
if (isInlineObjectProperty(propSchema)) {
|
|
520
689
|
objects ??= /* @__PURE__ */ new Map();
|
|
521
690
|
objects.set(key, claim(`${typeName}_${pascalCaseKey(key) || "Value"}`));
|
|
522
|
-
} else if (isInlineObjectArrayProperty(propSchema)) {
|
|
691
|
+
} else if (isInlineObjectArrayProperty(propSchema) || isUnionArrayProperty(propSchema)) {
|
|
523
692
|
arrayItems ??= /* @__PURE__ */ new Map();
|
|
524
693
|
arrayItems.set(key, claim(`${typeName}_${pascalCaseKey(key) || "Value"}Item`));
|
|
525
694
|
}
|
|
@@ -691,7 +860,51 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
691
860
|
for (let i = 0; i < arr.length; i++) if (!${shapeValidatorName(subName)}(arr[i])) return false;
|
|
692
861
|
return true;
|
|
693
862
|
};`);
|
|
694
|
-
|
|
863
|
+
const unionBranches = strict ? null : getUnionBranches(itemSchema);
|
|
864
|
+
const scoredItemParser = unionBranches === null ? null : generateScoredUnionParser(generateParserName(subName), subName, unionBranches, {
|
|
865
|
+
useRefImports,
|
|
866
|
+
suffix,
|
|
867
|
+
...rootSchema !== void 0 ? { rootSchema } : {},
|
|
868
|
+
...logWarnings !== void 0 ? { logWarnings } : {},
|
|
869
|
+
reservedNames,
|
|
870
|
+
stripUnknown,
|
|
871
|
+
unknownKeys,
|
|
872
|
+
caseInsensitive
|
|
873
|
+
}, false);
|
|
874
|
+
preamble.push(scoredItemParser ?? generateObjectParser(itemSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive, itemShapeValidator, unknownKeys));
|
|
875
|
+
}
|
|
876
|
+
const unionSubNames = /* @__PURE__ */ new Map();
|
|
877
|
+
if (!strict) {
|
|
878
|
+
const claimedUnionNames = /* @__PURE__ */ new Set([...reservedNames, ...subTypeNames.values(), ...subItemNames.values()]);
|
|
879
|
+
for (const key of Object.keys(schemaProps)) {
|
|
880
|
+
if (subTypeNames.has(key) || subItemNames.has(key))
|
|
881
|
+
continue;
|
|
882
|
+
const propSchema = schemaProps[key];
|
|
883
|
+
if (!isInlineUnionProperty(propSchema))
|
|
884
|
+
continue;
|
|
885
|
+
const branches = getUnionBranches(propSchema);
|
|
886
|
+
if (branches === null)
|
|
887
|
+
continue;
|
|
888
|
+
let unionSubName = `${typeName}_${pascalCaseKey(key) || "Value"}Union`;
|
|
889
|
+
while (claimedUnionNames.has(unionSubName))
|
|
890
|
+
unionSubName = `${unionSubName}_`;
|
|
891
|
+
claimedUnionNames.add(unionSubName);
|
|
892
|
+
const unionParser = generateScoredUnionParser(generateParserName(unionSubName), unionSubName, branches, {
|
|
893
|
+
useRefImports,
|
|
894
|
+
suffix,
|
|
895
|
+
...rootSchema !== void 0 ? { rootSchema } : {},
|
|
896
|
+
...logWarnings !== void 0 ? { logWarnings } : {},
|
|
897
|
+
reservedNames: claimedUnionNames,
|
|
898
|
+
stripUnknown,
|
|
899
|
+
unknownKeys,
|
|
900
|
+
caseInsensitive
|
|
901
|
+
}, false);
|
|
902
|
+
if (unionParser === null)
|
|
903
|
+
continue;
|
|
904
|
+
preamble.push(`type ${unionSubName} = NonNullable<${typeName}>[${JSON.stringify(key)}];`);
|
|
905
|
+
preamble.push(unionParser);
|
|
906
|
+
unionSubNames.set(key, unionSubName);
|
|
907
|
+
}
|
|
695
908
|
}
|
|
696
909
|
const strictKeys = hasStrictKeys(schema);
|
|
697
910
|
const stripKeys = strictKeys || stripUnknown;
|
|
@@ -860,6 +1073,16 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
860
1073
|
for (const { key, varName, isRequired, propSchema } of propInfo) {
|
|
861
1074
|
const shouldCache = shouldCacheVariable(propSchema);
|
|
862
1075
|
const accessor = shouldCache ? varName : safeAccessor("input", key);
|
|
1076
|
+
const unionSubName = unionSubNames.get(key);
|
|
1077
|
+
if (unionSubName) {
|
|
1078
|
+
const unionParserName = generateParserName(unionSubName);
|
|
1079
|
+
if (isRequired) {
|
|
1080
|
+
entries.push({ kind: "field", key, value: `${unionParserName}(${accessor})` });
|
|
1081
|
+
} else {
|
|
1082
|
+
entries.push({ kind: "optional", key, read: accessor, value: `${unionParserName}(${accessor})` });
|
|
1083
|
+
}
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
863
1086
|
const subName = subTypeNames.get(key);
|
|
864
1087
|
if (subName) {
|
|
865
1088
|
const subParserName = generateParserName(subName);
|
|
@@ -1621,6 +1844,11 @@ const generateShapeValidator = (schema, typeName, useRefImports, suffix = "", ex
|
|
|
1621
1844
|
if (check !== null) {
|
|
1622
1845
|
return `${exportPrefix}const ${fnName} = (input: unknown): boolean => ${check};`;
|
|
1623
1846
|
}
|
|
1847
|
+
return stub;
|
|
1848
|
+
}
|
|
1849
|
+
const direct = generatePropertyTypeCheck("input", schema, useRefImports, suffix);
|
|
1850
|
+
if (direct !== null) {
|
|
1851
|
+
return `${exportPrefix}const ${fnName} = (input: unknown): boolean => ${direct};`;
|
|
1624
1852
|
}
|
|
1625
1853
|
return stub;
|
|
1626
1854
|
}
|
|
@@ -221,9 +221,15 @@ const generateItemCheck = (schema, context) => {
|
|
|
221
221
|
if (hasRef(items) && context.useRefImports === true || shapeIsDelegated(items, context))
|
|
222
222
|
return null;
|
|
223
223
|
const match = subschemaMatchExpr("_it", items, matchContext(context));
|
|
224
|
-
if (match
|
|
224
|
+
if (match !== null)
|
|
225
|
+
return match === "true" ? null : { check: match, message: "items do not match the item schema" };
|
|
226
|
+
const branches = getUnionBranches(items);
|
|
227
|
+
if (branches === null || context.stripUnknown === true)
|
|
228
|
+
return null;
|
|
229
|
+
if (!canEnforceUnion(branches, context.rootSchema))
|
|
225
230
|
return null;
|
|
226
|
-
|
|
231
|
+
const union = generateUnionCheck("_it", branches, context.useRefImports ?? false, context.suffix ?? "", isExclusiveUnion(items));
|
|
232
|
+
return union === null ? null : { check: union, message: "items do not match the item schema" };
|
|
227
233
|
};
|
|
228
234
|
const nullableObjectShapeChecks = (acc, schema, type, label, optional, context) => {
|
|
229
235
|
if (type !== "object")
|
|
@@ -35,6 +35,27 @@ export declare const isInlineObjectProperty: (propSchema: JSONSchema) => propSch
|
|
|
35
35
|
* `Array.isArray` check.
|
|
36
36
|
*/
|
|
37
37
|
export declare const isInlineObjectArrayProperty: (propSchema: JSONSchema) => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Matches an array property whose `items` is a *union* of object branches — the
|
|
40
|
+
* shape a coercing parser needs a private item sub-parser for, so an element can
|
|
41
|
+
* be dispatched to (and repaired toward) a branch rather than passed through.
|
|
42
|
+
* {@link isInlineObjectArrayProperty} deliberately does not cover this: a union
|
|
43
|
+
* is not one inline object, and the trust walk reads that predicate to decide
|
|
44
|
+
* what a shape validator proves.
|
|
45
|
+
*/
|
|
46
|
+
export declare const isUnionArrayProperty: (propSchema: JSONSchema) => boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Matches a union written directly as a property value — `logo: { anyOf: [...] }`
|
|
49
|
+
* rather than a `$ref`, a definition, or an array's `items`. This is the one
|
|
50
|
+
* union position the coercing dispatcher did not reach, so a value matching no
|
|
51
|
+
* branch was passed through unrepaired.
|
|
52
|
+
*
|
|
53
|
+
* The union's *own* keywords are the reason for the exclusions: 2020-12 applies
|
|
54
|
+
* them alongside the branches, and a dispatcher expresses none of them, so a
|
|
55
|
+
* constrained union keeps the general path rather than getting a parser that
|
|
56
|
+
* quietly drops the constraint.
|
|
57
|
+
*/
|
|
58
|
+
export declare const isInlineUnionProperty: (propSchema: JSONSchema) => boolean;
|
|
38
59
|
/**
|
|
39
60
|
* Extracts the branch list of a `oneOf`/`anyOf` union, or `null` when the
|
|
40
61
|
* schema is not a union (or mixes in other composition keywords we cannot
|
|
@@ -36,6 +36,37 @@ const isInlineObjectArrayProperty = (propSchema) => {
|
|
|
36
36
|
return false;
|
|
37
37
|
return isInlineObjectProperty(propSchema.items);
|
|
38
38
|
};
|
|
39
|
+
const isUnionArrayProperty = (propSchema) => {
|
|
40
|
+
if (!isSchemaObject(propSchema))
|
|
41
|
+
return false;
|
|
42
|
+
if (!("type" in propSchema) || propSchema.type !== "array")
|
|
43
|
+
return false;
|
|
44
|
+
if (!hasItems(propSchema) || Array.isArray(propSchema.items))
|
|
45
|
+
return false;
|
|
46
|
+
const items = propSchema.items;
|
|
47
|
+
if (!isSchemaObject(items))
|
|
48
|
+
return false;
|
|
49
|
+
const branches = getUnionBranches(items);
|
|
50
|
+
if (branches === null || branches.length < 2)
|
|
51
|
+
return false;
|
|
52
|
+
return branches.every((branch) => isSchemaObject(branch) && (hasRef(branch) || isInlineObjectProperty(branch)));
|
|
53
|
+
};
|
|
54
|
+
const isInlineUnionProperty = (propSchema) => {
|
|
55
|
+
if (!isSchemaObject(propSchema))
|
|
56
|
+
return false;
|
|
57
|
+
if (hasRef(propSchema) || hasAllOf(propSchema) || "not" in propSchema)
|
|
58
|
+
return false;
|
|
59
|
+
if ("if" in propSchema || "then" in propSchema || "else" in propSchema)
|
|
60
|
+
return false;
|
|
61
|
+
if ("patternProperties" in propSchema)
|
|
62
|
+
return false;
|
|
63
|
+
const branches = getUnionBranches(propSchema);
|
|
64
|
+
if (branches === null || branches.length < 2)
|
|
65
|
+
return false;
|
|
66
|
+
if (hasBranchSiblingConstraint(propSchema))
|
|
67
|
+
return false;
|
|
68
|
+
return branches.every((branch) => isSchemaObject(branch) && (hasRef(branch) || isInlineObjectProperty(branch)));
|
|
69
|
+
};
|
|
39
70
|
const getUnionBranches = (schema) => {
|
|
40
71
|
if (!isSchemaObject(schema))
|
|
41
72
|
return null;
|
|
@@ -385,8 +416,11 @@ const canTrustReferencedValidator = (ref, rootSchema, visiting) => {
|
|
|
385
416
|
}
|
|
386
417
|
if (!hasProperties(resolved)) {
|
|
387
418
|
const branches = getUnionBranches(resolved);
|
|
388
|
-
if (!branches)
|
|
389
|
-
|
|
419
|
+
if (!branches) {
|
|
420
|
+
if ("patternProperties" in resolved || "if" in resolved)
|
|
421
|
+
return false;
|
|
422
|
+
return canTrustPropertyCheck(resolved, rootSchema, visiting);
|
|
423
|
+
}
|
|
390
424
|
if ("patternProperties" in resolved || "if" in resolved)
|
|
391
425
|
return false;
|
|
392
426
|
return branches.every((branch) => {
|
|
@@ -413,6 +447,8 @@ export {
|
|
|
413
447
|
isExclusiveUnion,
|
|
414
448
|
isInlineObjectArrayProperty,
|
|
415
449
|
isInlineObjectProperty,
|
|
450
|
+
isInlineUnionProperty,
|
|
451
|
+
isUnionArrayProperty,
|
|
416
452
|
multiTypeCheck,
|
|
417
453
|
shapeValidatorName
|
|
418
454
|
};
|
|
@@ -141,6 +141,15 @@ const numberBoundChecks = (valueExpr, schema) => {
|
|
|
141
141
|
checks.push(multipleOfPassExpr(valueExpr, schema.multipleOf));
|
|
142
142
|
return checks;
|
|
143
143
|
};
|
|
144
|
+
const BOOLEAN_TRUE_TOKENS = ["true", "yes", "y", "on", "1"];
|
|
145
|
+
const BOOLEAN_FALSE_TOKENS = ["false", "no", "n", "off", "0", ""];
|
|
146
|
+
const getBooleanCoercion = (accessor, defaultValue) => {
|
|
147
|
+
const side = (tokens, literals) => [...tokens.map((token) => `"${token}"`), ...literals].map((term) => `_b === ${term}`).join(" || ");
|
|
148
|
+
const normalized = `typeof ${accessor} === "string" ? ${accessor}.trim().toLowerCase() : ${accessor}`;
|
|
149
|
+
const truthy = side(BOOLEAN_TRUE_TOKENS, ["1", "true"]);
|
|
150
|
+
const falsy = side(BOOLEAN_FALSE_TOKENS, ["0", "false"]);
|
|
151
|
+
return `((_b) => ${truthy} ? true : ${falsy} ? false : ${defaultValue})(${normalized})`;
|
|
152
|
+
};
|
|
144
153
|
const getTypeCoercion = (accessor, schema, defaultValue) => {
|
|
145
154
|
if (!hasType(schema)) {
|
|
146
155
|
return null;
|
|
@@ -163,7 +172,7 @@ const getTypeCoercion = (accessor, schema, defaultValue) => {
|
|
|
163
172
|
return `((_n) => ${[`${wellFormed}(_n)`, ...bounds].join(" && ")} ? _n : ${defaultValue})(${n})`;
|
|
164
173
|
}
|
|
165
174
|
case "boolean":
|
|
166
|
-
return
|
|
175
|
+
return getBooleanCoercion(accessor, defaultValue);
|
|
167
176
|
case "array":
|
|
168
177
|
return defaultValue;
|
|
169
178
|
case "object":
|
|
@@ -46,8 +46,9 @@ const arrayFallback = (schema) => {
|
|
|
46
46
|
};
|
|
47
47
|
const MAX_FALLBACK_LENGTH = 256;
|
|
48
48
|
const stringFallback = (schema) => {
|
|
49
|
+
const bounds = schema;
|
|
49
50
|
if (hasPattern(schema)) {
|
|
50
|
-
const patternDefault = generateDefaultFromPattern(schema.pattern);
|
|
51
|
+
const patternDefault = generateDefaultFromPattern(schema.pattern, typeof bounds["minLength"] === "number" ? bounds["minLength"] : void 0, typeof bounds["maxLength"] === "number" ? bounds["maxLength"] : void 0);
|
|
51
52
|
if (patternDefault)
|
|
52
53
|
return patternDefault;
|
|
53
54
|
return '""';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-parsers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"json-schema-typed": "^8.0.1",
|
|
63
|
-
"@amritk/helpers": "^0.
|
|
63
|
+
"@amritk/helpers": "^0.20.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@amritk/generate-markdown": "^0.6.0",
|