@amritk/generate-validators 0.11.5 → 0.11.7
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
CHANGED
|
@@ -92,6 +92,27 @@ Returns: `Promise<GeneratedFile[]>` where `GeneratedFile = { filename: string; c
|
|
|
92
92
|
|
|
93
93
|
---
|
|
94
94
|
|
|
95
|
+
## Semantics
|
|
96
|
+
|
|
97
|
+
Generated validators track the `@amritk/runtime-validators` interpreter. Array
|
|
98
|
+
items are validated in full — an item's type, `$ref`, nested `properties` /
|
|
99
|
+
`required`, and scalar constraints (`minLength`, `minimum`, …) are all enforced,
|
|
100
|
+
recursing to any depth — and the boolean guard (`isX`) reaches the identical
|
|
101
|
+
verdict. Validating array item *contents* costs throughput proportional to the
|
|
102
|
+
per-item work (a bare `string[]` is free; a closed object with several fields is
|
|
103
|
+
meaningfully slower), which is why array-heavy schemas validate more slowly than
|
|
104
|
+
scalar/object ones.
|
|
105
|
+
|
|
106
|
+
One divergence is worth calling out: **`NaN` satisfies a constrained number.**
|
|
107
|
+
Because the numeric bound checks are the exact negation of the error condition
|
|
108
|
+
(e.g. `!(x < minimum)`), and every comparison against `NaN` is `false`, a `NaN`
|
|
109
|
+
passes `minimum`/`maximum`/`exclusive*`/`multipleOf`. This matches the interpreter
|
|
110
|
+
but differs from validators (e.g. Ajv) that reject `NaN` for `type: "number"`.
|
|
111
|
+
`NaN` never appears in parsed JSON; guard against it upstream if your values can
|
|
112
|
+
be non-JSON.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
95
116
|
## Benchmarks
|
|
96
117
|
|
|
97
118
|
Generated validators are straight-line, monomorphic TypeScript with no generic
|
|
@@ -108,20 +129,21 @@ Measured on Bun 1.3 (Linux x64), validating valid input at steady state:
|
|
|
108
129
|
|
|
109
130
|
| schema | mjst (generated) | typia (transformed) | ajv (compiled) | typebox (compiled) | zod |
|
|
110
131
|
|:--|--:|--:|--:|--:|--:|
|
|
111
|
-
| small (4 fields) | **~
|
|
112
|
-
| order (nested + array) | **~
|
|
113
|
-
| assert-loose | **~
|
|
114
|
-
| assert-strict | **~
|
|
132
|
+
| small (4 fields) | **~48M** ops/s | ~5M ops/s | ~10.5M ops/s | ~5.3M ops/s | ~2M ops/s |
|
|
133
|
+
| order (nested + array) | **~7.8M** ops/s | ~2.2M ops/s | ~3.5M ops/s | ~2.1M ops/s | ~0.5M ops/s |
|
|
134
|
+
| assert-loose | **~184M** ops/s | ~183M ops/s | ~45M ops/s | ~63M ops/s | ~3.8M ops/s |
|
|
135
|
+
| assert-strict | **~162M** ops/s | ~148M ops/s | ~22M ops/s | ~38M ops/s | ~1.3M ops/s |
|
|
115
136
|
|
|
116
137
|
The `assert-loose` / `assert-strict` rows are the exact shape used by
|
|
117
138
|
[`moltar/typescript-runtime-type-benchmarks`](https://github.com/moltar/typescript-runtime-type-benchmarks)
|
|
118
|
-
(seven scalar roots plus a nested object)
|
|
119
|
-
typia on
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
139
|
+
(seven scalar roots plus a nested object): the boolean guard puts mjst clearly
|
|
140
|
+
ahead of typia on `assert-strict` (with `additionalProperties: false`) and
|
|
141
|
+
neck-and-neck with it on `assert-loose` — the two trade the lead run-to-run
|
|
142
|
+
within noise. (typia and TypeBox still win the *invalid* path, where they bail on
|
|
143
|
+
the first error rather than collecting a full error list.)
|
|
144
|
+
|
|
145
|
+
Preparing a validator costs ~0.3–0.6 ms for mjst codegen and ~0.05–0.2 ms for a
|
|
146
|
+
TypeBox `TypeCompiler` compile, versus ~9–12 ms for an Ajv compile. Every library
|
|
125
147
|
agrees on every verdict; parity is asserted before timing (TypeBox is given
|
|
126
148
|
uuid/email format checkers so every library does the same work). Each library is
|
|
127
149
|
timed in an isolated process over a pool of distinct inputs, reporting the median
|
|
@@ -45,6 +45,35 @@ export const valuesEqual = (a: unknown, b: unknown): boolean => {
|
|
|
45
45
|
}
|
|
46
46
|
return true
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* True when every element of \`arr\` is distinct under structural equality
|
|
51
|
+
* ({@link valuesEqual}). Backs generated \`uniqueItems\` checks whose items may be
|
|
52
|
+
* objects or arrays, where a \`JSON.stringify\` dedupe key would be key-order
|
|
53
|
+
* sensitive and let a reordered-but-equal duplicate (\`{ a: 1, b: 2 }\` vs
|
|
54
|
+
* \`{ b: 2, a: 1 }\`) slip through. A native \`Set\` dedupes the all-primitive case
|
|
55
|
+
* in one linear pass; object/array elements fall back to an exact pairwise
|
|
56
|
+
* structural comparison.
|
|
57
|
+
*/
|
|
58
|
+
export const allUnique = (arr: readonly unknown[]): boolean => {
|
|
59
|
+
const len = arr.length
|
|
60
|
+
if (len < 2) return true
|
|
61
|
+
let allPrimitive = true
|
|
62
|
+
for (let i = 0; i < len; i++) {
|
|
63
|
+
const v = arr[i]
|
|
64
|
+
if (v !== null && typeof v === 'object') {
|
|
65
|
+
allPrimitive = false
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (allPrimitive) return new Set(arr).size === len
|
|
70
|
+
for (let i = 0; i < len; i++) {
|
|
71
|
+
for (let j = i + 1; j < len; j++) {
|
|
72
|
+
if (valuesEqual(arr[i], arr[j])) return false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
48
77
|
`;
|
|
49
78
|
/**
|
|
50
79
|
* Builds all TypeScript validator files from a JSON Schema by traversing all
|
|
@@ -54,7 +54,11 @@ const collectDirectRefs = (value, refs = []) => {
|
|
|
54
54
|
// generated files, not inlined by this validator. `collectDirectRefs`
|
|
55
55
|
// self-guards on non-objects, so a keyword that is a boolean or missing is a
|
|
56
56
|
// harmless no-op.
|
|
57
|
-
|
|
57
|
+
// `dependencies` (draft-07) is dual-form: a string array (dependentRequired) or
|
|
58
|
+
// a subschema (dependentSchemas). The emitter delegates the schema form via
|
|
59
|
+
// `validateX`, so a `$ref` inside it must be imported; the string-array form is
|
|
60
|
+
// a harmless no-op here (its values are strings, not schemas).
|
|
61
|
+
const subSchemaMaps = ['properties', 'patternProperties', 'dependentSchemas', 'dependencies'];
|
|
58
62
|
for (const mapKey of subSchemaMaps) {
|
|
59
63
|
const map = schema[mapKey];
|
|
60
64
|
if (typeof map === 'object' && map !== null && !Array.isArray(map)) {
|
|
@@ -37,11 +37,14 @@ export const generateValidatorFile = (schema, typeName, options) => {
|
|
|
37
37
|
const booleanGuard = generateBooleanGuard(schema, typeName, typeSuffix);
|
|
38
38
|
// `.js` extension so the relative import resolves under Node ESM, not only Bun.
|
|
39
39
|
let result = `import type { ValidationResult, ValidationError } from './validation-result.js'\n`;
|
|
40
|
-
// `const` checks
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
// Structural `const` checks call the runtime `valuesEqual` helper; structural
|
|
41
|
+
// `uniqueItems` checks call `allUnique`. Both live in `validation-result.js`;
|
|
42
|
+
// import each only when the generated body (validator or boolean guard) uses
|
|
43
|
+
// it, so files that need neither carry no unused import.
|
|
44
|
+
const body = validatorFunction + booleanGuard;
|
|
45
|
+
const runtimeHelpers = ['valuesEqual', 'allUnique'].filter((name) => body.includes(`${name}(`));
|
|
46
|
+
if (runtimeHelpers.length > 0) {
|
|
47
|
+
result += `import { ${runtimeHelpers.join(', ')} } from './validation-result.js'\n`;
|
|
45
48
|
}
|
|
46
49
|
for (const imp of refImports) {
|
|
47
50
|
result += imp + '\n';
|
|
@@ -3,7 +3,7 @@ import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extens
|
|
|
3
3
|
import { multipleOfFailExpr, multipleOfPassExpr } from '@amritk/helpers/multiple-of-check';
|
|
4
4
|
import { refToName } from '@amritk/helpers/ref-to-name';
|
|
5
5
|
import { safeAccessor } from '@amritk/helpers/safe-accessor';
|
|
6
|
-
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasStrictExclusiveMaximum, hasStrictExclusiveMinimum, hasType, hasUniqueItems, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
|
|
6
|
+
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasStrictExclusiveMaximum, hasStrictExclusiveMinimum, hasType, hasUniqueItems, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
|
|
7
7
|
import { unknownKeyCheck } from '@amritk/helpers/unknown-key-check';
|
|
8
8
|
/**
|
|
9
9
|
* Derives the validator function name from a type name.
|
|
@@ -31,6 +31,43 @@ const constMismatchCondition = (accessor, value) => {
|
|
|
31
31
|
}
|
|
32
32
|
return `!valuesEqual(${accessor}, ${JSON.stringify(value)})`;
|
|
33
33
|
};
|
|
34
|
+
const SCALAR_ITEM_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'null']);
|
|
35
|
+
/**
|
|
36
|
+
* True when a schema's values are provably JSON scalars — its `type` is present
|
|
37
|
+
* and every listed type is a primitive. Conservative: a `$ref`, a boolean/absent
|
|
38
|
+
* schema, an `object`/`array` type, or a missing `type` all fail this test.
|
|
39
|
+
*/
|
|
40
|
+
const schemaIsScalarOnly = (schema) => {
|
|
41
|
+
if (!isSchemaObject(schema))
|
|
42
|
+
return false;
|
|
43
|
+
const t = schema['type'];
|
|
44
|
+
if (t === undefined)
|
|
45
|
+
return false;
|
|
46
|
+
const types = Array.isArray(t) ? t : [t];
|
|
47
|
+
return types.length > 0 && types.every((x) => typeof x === 'string' && SCALAR_ITEM_TYPES.has(x));
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* True when an array's elements can only be JSON scalars, so a `uniqueItems`
|
|
51
|
+
* check can dedupe by the cheap `JSON.stringify` projection. When items may be
|
|
52
|
+
* objects or arrays this returns false, and the check must instead compare
|
|
53
|
+
* structurally (the `allUnique` runtime helper): `JSON.stringify` is key-order
|
|
54
|
+
* sensitive and would treat `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` as distinct,
|
|
55
|
+
* disagreeing with the interpreter's order-independent deep equality.
|
|
56
|
+
*/
|
|
57
|
+
const arrayItemsAreScalarOnly = (schema) => {
|
|
58
|
+
const prefix = schema['prefixItems'];
|
|
59
|
+
if (Array.isArray(prefix)) {
|
|
60
|
+
if (!prefix.every((p) => schemaIsScalarOnly(p)))
|
|
61
|
+
return false;
|
|
62
|
+
// Tuple tail: a closed tuple (`items`/`additionalItems: false`) has no tail;
|
|
63
|
+
// otherwise the tail schema must itself be scalar-only.
|
|
64
|
+
const tail = 'items' in schema ? schema['items'] : schema['additionalItems'];
|
|
65
|
+
if (tail === false)
|
|
66
|
+
return true;
|
|
67
|
+
return schemaIsScalarOnly(tail);
|
|
68
|
+
}
|
|
69
|
+
return schemaIsScalarOnly(schema['items']);
|
|
70
|
+
};
|
|
34
71
|
/**
|
|
35
72
|
* Generates the inline condition that is TRUE when a value is the wrong type.
|
|
36
73
|
*/
|
|
@@ -68,6 +105,21 @@ const getTypeArray = (schema) => {
|
|
|
68
105
|
return schema.type;
|
|
69
106
|
};
|
|
70
107
|
const createRootContext = () => ({ objVar: 'obj', pathPrefix: '${_path}', depth: 0, hoisted: [] });
|
|
108
|
+
/**
|
|
109
|
+
* Renders a schema-controlled property name as a static error-path segment.
|
|
110
|
+
*
|
|
111
|
+
* The name is appended to a backtick template-literal path (`` `${_path}/…` ``),
|
|
112
|
+
* so two independent escapings apply. First the JSON Pointer escape (`~`→`~0`,
|
|
113
|
+
* `/`→`~1`, `~` first) so a key containing `/` or `~` reads back unambiguously —
|
|
114
|
+
* matching the paths the runtime-validators interpreter emits. Then a
|
|
115
|
+
* template-literal escape of `` ` ``, `\`, and `$`, so a key like `` a`b `` or
|
|
116
|
+
* `${x}` cannot terminate the literal (a build failure) or inject an
|
|
117
|
+
* interpolation (a runtime `ReferenceError` / arbitrary expression).
|
|
118
|
+
*/
|
|
119
|
+
const pointerSegment = (key) => key
|
|
120
|
+
.replace(/~/g, '~0')
|
|
121
|
+
.replace(/\//g, '~1')
|
|
122
|
+
.replace(/[\\`$]/g, '\\$&');
|
|
71
123
|
/**
|
|
72
124
|
* Returns the `patternProperties` regex sources, or an empty array when the
|
|
73
125
|
* schema declares none. The keys of `patternProperties` are the patterns.
|
|
@@ -146,31 +198,54 @@ const generateMissingRequiredChecks = (schema, ctx) => {
|
|
|
146
198
|
* and recursion into inline nested objects.
|
|
147
199
|
*/
|
|
148
200
|
const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
|
|
149
|
-
if (!isSchemaObject(propSchema))
|
|
201
|
+
if (!isSchemaObject(propSchema)) {
|
|
202
|
+
// A boolean `true` (accept-anything) schema carries no shape checks, but a
|
|
203
|
+
// required key must still be present. `false` never validates a present value,
|
|
204
|
+
// which the strict-key / additionalProperties path handles; here we only need
|
|
205
|
+
// to enforce presence for `true`.
|
|
206
|
+
if (isRequired && propSchema === true) {
|
|
207
|
+
const parentPath = ctx.depth === 0 ? '_path' : `\`${ctx.pathPrefix}\``;
|
|
208
|
+
return [
|
|
209
|
+
` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`,
|
|
210
|
+
` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`,
|
|
211
|
+
` }`,
|
|
212
|
+
];
|
|
213
|
+
}
|
|
150
214
|
return [];
|
|
215
|
+
}
|
|
151
216
|
const raw = `${ctx.objVar}[${JSON.stringify(key)}]`;
|
|
152
|
-
const path = `\`${ctx.pathPrefix}/${key}\``;
|
|
217
|
+
const path = `\`${ctx.pathPrefix}/${pointerSegment(key)}\``;
|
|
153
218
|
// Missing-property errors report at the parent object's path. At the root
|
|
154
219
|
// that is the `_path` parameter itself; inside nested objects it is the
|
|
155
220
|
// parent's accumulated static path.
|
|
156
221
|
const parentPath = ctx.depth === 0 ? '_path' : `\`${ctx.pathPrefix}\``;
|
|
157
222
|
const lines = [];
|
|
158
|
-
// $ref — delegate to the imported validator
|
|
223
|
+
// $ref — delegate to the imported validator. Per 2020-12, sibling keywords
|
|
224
|
+
// alongside `$ref` still apply to the same value, so any constraint/combinator
|
|
225
|
+
// siblings (e.g. `{ $ref, minLength: 5 }`) run after the delegation. A bare
|
|
226
|
+
// `{ $ref }` produces no siblings, leaving the output unchanged.
|
|
159
227
|
if (hasRef(propSchema)) {
|
|
160
228
|
const ref = propSchema.$ref;
|
|
161
229
|
const vName = validatorName(refToName(ref, suffix));
|
|
230
|
+
const siblings = [
|
|
231
|
+
...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx),
|
|
232
|
+
...generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx),
|
|
233
|
+
];
|
|
234
|
+
const delegate = [
|
|
235
|
+
` const _r = ${vName}(${raw}, ${path})`,
|
|
236
|
+
` if (_r !== true) errors.push(..._r.errors)`,
|
|
237
|
+
...siblings,
|
|
238
|
+
];
|
|
162
239
|
if (isRequired) {
|
|
163
240
|
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
164
241
|
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
165
242
|
lines.push(` } else {`);
|
|
166
|
-
lines.push(
|
|
167
|
-
lines.push(` if (_r !== true) errors.push(..._r.errors)`);
|
|
243
|
+
lines.push(...delegate);
|
|
168
244
|
lines.push(` }`);
|
|
169
245
|
}
|
|
170
246
|
else {
|
|
171
247
|
lines.push(` if (${raw} !== undefined) {`);
|
|
172
|
-
lines.push(
|
|
173
|
-
lines.push(` if (_r !== true) errors.push(..._r.errors)`);
|
|
248
|
+
lines.push(...delegate);
|
|
174
249
|
lines.push(` }`);
|
|
175
250
|
}
|
|
176
251
|
return lines;
|
|
@@ -310,20 +385,32 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
|
|
|
310
385
|
...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx),
|
|
311
386
|
...generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx),
|
|
312
387
|
];
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
lines.push(`
|
|
318
|
-
lines.push(` } else {`);
|
|
388
|
+
if (hasType(propSchema)) {
|
|
389
|
+
// Presence was already enforced in the `hasType` branch; only wrap the
|
|
390
|
+
// combinator siblings so they run when the value is present.
|
|
391
|
+
if (extraLines.length > 0) {
|
|
392
|
+
lines.push(` if (${raw} !== undefined) {`);
|
|
319
393
|
lines.push(...extraLines);
|
|
320
394
|
lines.push(` }`);
|
|
321
395
|
}
|
|
322
|
-
|
|
323
|
-
|
|
396
|
+
}
|
|
397
|
+
else if (isRequired) {
|
|
398
|
+
// Type-less required property. Presence must be enforced even when the schema
|
|
399
|
+
// contributes no other checks (e.g. `{}` — an accept-anything schema), so a
|
|
400
|
+
// missing required key is still an error. Any extra checks run in the `else`.
|
|
401
|
+
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
402
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
403
|
+
if (extraLines.length > 0) {
|
|
404
|
+
lines.push(` } else {`);
|
|
324
405
|
lines.push(...extraLines);
|
|
325
|
-
lines.push(` }`);
|
|
326
406
|
}
|
|
407
|
+
lines.push(` }`);
|
|
408
|
+
}
|
|
409
|
+
else if (extraLines.length > 0) {
|
|
410
|
+
// Type-less optional property: run any checks only when the value is present.
|
|
411
|
+
lines.push(` if (${raw} !== undefined) {`);
|
|
412
|
+
lines.push(...extraLines);
|
|
413
|
+
lines.push(` }`);
|
|
327
414
|
}
|
|
328
415
|
return lines;
|
|
329
416
|
};
|
|
@@ -405,40 +492,59 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
405
492
|
lines.push(` }`);
|
|
406
493
|
}
|
|
407
494
|
}
|
|
408
|
-
// Array
|
|
495
|
+
// Array items. `$ref` items delegate to the referenced validator. Any other
|
|
496
|
+
// item subschema is validated in full — matching the interpreter — but wrapped
|
|
497
|
+
// in a per-item boolean fast-check (`booleanLeafExpr`): a valid item passes the
|
|
498
|
+
// flat expression and skips the error-collecting body entirely, so the common
|
|
499
|
+
// valid case stays allocation-free (the same hot/cold split the top-level
|
|
500
|
+
// validator uses). This keeps array-heavy throughput close to a bare type check
|
|
501
|
+
// while still fully validating every item. The loop variables carry the nesting
|
|
502
|
+
// depth so item loops can nest (array-of-arrays) without colliding.
|
|
409
503
|
if (hasItems(propSchema)) {
|
|
410
504
|
const itemSchema = propSchema.items;
|
|
505
|
+
const iv = `_i${ctx.depth}`;
|
|
506
|
+
const itemPath = `\`${path.slice(1, -1)}/\${${iv}}\``;
|
|
411
507
|
if (hasRef(itemSchema)) {
|
|
412
508
|
const vName = validatorName(refToName(itemSchema.$ref, suffix));
|
|
413
509
|
lines.push(` if (Array.isArray(${raw})) {`);
|
|
414
|
-
lines.push(` for (let
|
|
415
|
-
lines.push(` const _ir = ${vName}(${raw}[
|
|
510
|
+
lines.push(` for (let ${iv} = 0; ${iv} < ${raw}.length; ${iv}++) {`);
|
|
511
|
+
lines.push(` const _ir = ${vName}(${raw}[${iv}], ${itemPath})`);
|
|
416
512
|
lines.push(` if (_ir !== true) errors.push(..._ir.errors)`);
|
|
417
513
|
lines.push(` }`);
|
|
418
514
|
lines.push(` }`);
|
|
419
515
|
}
|
|
420
|
-
else if (
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
if (itemWrong) {
|
|
516
|
+
else if (isSchemaObject(itemSchema)) {
|
|
517
|
+
const itemVar = `_item${ctx.depth}`;
|
|
518
|
+
const detail = generateValueChecks('', itemVar, itemPath, itemSchema, suffix, ctx, true);
|
|
519
|
+
if (detail.length > 0) {
|
|
425
520
|
lines.push(` if (Array.isArray(${raw})) {`);
|
|
426
|
-
lines.push(` for (let
|
|
427
|
-
lines.push(` const
|
|
428
|
-
lines.push(
|
|
521
|
+
lines.push(` for (let ${iv} = 0; ${iv} < ${raw}.length; ${iv}++) {`);
|
|
522
|
+
lines.push(` const ${itemVar} = ${raw}[${iv}]`);
|
|
523
|
+
lines.push(...detail.map((l) => ` ${l}`));
|
|
429
524
|
lines.push(` }`);
|
|
430
525
|
lines.push(` }`);
|
|
431
526
|
}
|
|
432
527
|
}
|
|
433
528
|
}
|
|
434
|
-
// Array length / uniqueness. `uniqueItems` dedupes by a
|
|
435
|
-
// for primitives
|
|
436
|
-
//
|
|
529
|
+
// Array length / uniqueness. `uniqueItems` dedupes scalar items by a cheap
|
|
530
|
+
// `JSON.stringify` projection (exact for primitives, what the type guard also
|
|
531
|
+
// uses), but falls back to the structural `allUnique` helper when items may be
|
|
532
|
+
// objects/arrays — `JSON.stringify` is key-order sensitive and would disagree
|
|
533
|
+
// with the interpreter's order-independent deep equality.
|
|
437
534
|
if (hasMinItems(propSchema) ||
|
|
438
535
|
hasMaxItems(propSchema) ||
|
|
439
536
|
(hasUniqueItems(propSchema) && propSchema.uniqueItems === true) ||
|
|
440
537
|
isSchemaObject(sp['contains']) ||
|
|
441
|
-
Array.isArray(sp['prefixItems'])
|
|
538
|
+
Array.isArray(sp['prefixItems']) ||
|
|
539
|
+
(sp['items'] === false && !Array.isArray(sp['prefixItems']))) {
|
|
540
|
+
// `items: false` with no `prefixItems` forbids every element, so the array
|
|
541
|
+
// must be empty. (With `prefixItems`, the tuple block below caps the length
|
|
542
|
+
// instead.) Without this the constraint was silently ignored.
|
|
543
|
+
if (sp['items'] === false && !Array.isArray(sp['prefixItems'])) {
|
|
544
|
+
lines.push(` if (Array.isArray(${raw}) && ${raw}.length > 0) {`);
|
|
545
|
+
lines.push(` errors.push({ message: 'must NOT have more than 0 items', path: ${path} })`);
|
|
546
|
+
lines.push(` }`);
|
|
547
|
+
}
|
|
442
548
|
if (hasMinItems(propSchema)) {
|
|
443
549
|
lines.push(` if (Array.isArray(${raw}) && ${raw}.length < ${propSchema.minItems}) {`);
|
|
444
550
|
lines.push(` errors.push({ message: 'must have at least ${propSchema.minItems} items', path: ${path} })`);
|
|
@@ -450,7 +556,10 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
450
556
|
lines.push(` }`);
|
|
451
557
|
}
|
|
452
558
|
if (hasUniqueItems(propSchema) && propSchema.uniqueItems === true) {
|
|
453
|
-
|
|
559
|
+
const dupCond = arrayItemsAreScalarOnly(sp)
|
|
560
|
+
? `new Set((${raw} as unknown[]).map((_u) => JSON.stringify(_u))).size !== ${raw}.length`
|
|
561
|
+
: `!allUnique(${raw} as unknown[])`;
|
|
562
|
+
lines.push(` if (Array.isArray(${raw}) && ${dupCond}) {`);
|
|
454
563
|
lines.push(` errors.push({ message: 'must NOT have duplicate items', path: ${path} })`);
|
|
455
564
|
lines.push(` }`);
|
|
456
565
|
}
|
|
@@ -499,34 +608,49 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
499
608
|
};
|
|
500
609
|
/**
|
|
501
610
|
* Validates a value located at a *dynamic* key (a `patternProperties` or
|
|
502
|
-
* `additionalProperties`
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
611
|
+
* `additionalProperties` value), an array item, a combinator branch, or a
|
|
612
|
+
* `dependentSchemas` subschema against `propSchema`. `raw` and `path` are
|
|
613
|
+
* caller-supplied expressions (e.g. `obj[_k]` and `` `${_path}/${_k}` ``) so the
|
|
614
|
+
* checks read a runtime location. By default the leaf checks are
|
|
615
|
+
* `!== undefined`-guarded (an absent optional value is valid); pass
|
|
616
|
+
* `required = true` for values that must be present (array items — a sparse hole
|
|
617
|
+
* reads as `undefined` and must fail), which drops that guard. `_key` is unused
|
|
618
|
+
* (the location is fully encoded by `path`) but kept for positional-call parity
|
|
619
|
+
* with the combinator generators.
|
|
507
620
|
*/
|
|
508
|
-
const generateValueChecks = (
|
|
621
|
+
const generateValueChecks = (_key, raw, path, propSchema, suffix, ctx, required = false) => {
|
|
509
622
|
if (!isSchemaObject(propSchema))
|
|
510
623
|
return [];
|
|
511
624
|
const lines = [];
|
|
625
|
+
// Optional values skip validation when absent, so their leaf checks are
|
|
626
|
+
// `!== undefined`-guarded. Array items are unconditionally present — a sparse
|
|
627
|
+
// hole reads as `undefined` and must FAIL its type/const/enum check — so
|
|
628
|
+
// `required` drops the guard.
|
|
629
|
+
const presence = required ? '' : `${raw} !== undefined && `;
|
|
512
630
|
if (hasRef(propSchema)) {
|
|
513
631
|
const vName = validatorName(refToName(propSchema.$ref, suffix));
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
632
|
+
if (required) {
|
|
633
|
+
lines.push(` const _r = ${vName}(${raw}, ${path})`);
|
|
634
|
+
lines.push(` if (_r !== true) errors.push(..._r.errors)`);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
lines.push(` if (${raw} !== undefined) {`);
|
|
638
|
+
lines.push(` const _r = ${vName}(${raw}, ${path})`);
|
|
639
|
+
lines.push(` if (_r !== true) errors.push(..._r.errors)`);
|
|
640
|
+
lines.push(` }`);
|
|
641
|
+
}
|
|
518
642
|
return lines;
|
|
519
643
|
}
|
|
520
644
|
const instanceOf = getMjstInstanceOf(propSchema);
|
|
521
645
|
if (instanceOf) {
|
|
522
|
-
lines.push(` if (${
|
|
646
|
+
lines.push(` if (${presence}!(${raw} instanceof ${instanceOf})) {`);
|
|
523
647
|
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
|
|
524
648
|
lines.push(` }`);
|
|
525
649
|
return lines;
|
|
526
650
|
}
|
|
527
651
|
const primitive = getMjstPrimitive(propSchema);
|
|
528
652
|
if (primitive) {
|
|
529
|
-
lines.push(` if (${
|
|
653
|
+
lines.push(` if (${presence}typeof ${raw} !== "${primitive}") {`);
|
|
530
654
|
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
|
|
531
655
|
lines.push(` }`);
|
|
532
656
|
return lines;
|
|
@@ -534,7 +658,7 @@ const generateValueChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
534
658
|
if (hasConst(propSchema)) {
|
|
535
659
|
const mismatch = constMismatchCondition(raw, propSchema.const);
|
|
536
660
|
const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`);
|
|
537
|
-
lines.push(` if (${
|
|
661
|
+
lines.push(` if (${presence}${mismatch}) {`);
|
|
538
662
|
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
539
663
|
lines.push(` }`);
|
|
540
664
|
return lines;
|
|
@@ -542,7 +666,7 @@ const generateValueChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
542
666
|
if (hasEnum(propSchema)) {
|
|
543
667
|
const allowed = JSON.stringify(propSchema.enum);
|
|
544
668
|
const label = propSchema.enum.map((v) => JSON.stringify(v)).join(', ');
|
|
545
|
-
lines.push(` if (${
|
|
669
|
+
lines.push(` if (${presence}!(${allowed} as unknown[]).includes(${raw})) {`);
|
|
546
670
|
lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
|
|
547
671
|
lines.push(` }`);
|
|
548
672
|
return lines;
|
|
@@ -552,7 +676,7 @@ const generateValueChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
552
676
|
const wrongType = wrongTypeCondition(raw, t);
|
|
553
677
|
const typLabel = typeofString(t);
|
|
554
678
|
if (wrongType) {
|
|
555
|
-
lines.push(` if (${
|
|
679
|
+
lines.push(` if (${presence}(${wrongType})) {`);
|
|
556
680
|
lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
|
|
557
681
|
lines.push(` }`);
|
|
558
682
|
}
|
|
@@ -561,8 +685,20 @@ const generateValueChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
561
685
|
// gate on keyword presence + a runtime-type guard, so a type-less subschema
|
|
562
686
|
// (a combinator branch like `{ required: [...] }` or `{ minItems: 2 }`) is
|
|
563
687
|
// still validated rather than collapsing to "matches everything".
|
|
564
|
-
|
|
565
|
-
|
|
688
|
+
//
|
|
689
|
+
// This value lives at `path` (a template literal), so anchor the recursion's
|
|
690
|
+
// context there and one nesting level deeper: any nested object/array it emits
|
|
691
|
+
// then builds paths relative to THIS value and mints collision-free variable
|
|
692
|
+
// names, independent of the caller's context. `key` is intentionally dropped
|
|
693
|
+
// (set to `''`) because `path` already locates the value.
|
|
694
|
+
const valueCtx = {
|
|
695
|
+
objVar: ctx.objVar,
|
|
696
|
+
pathPrefix: path.slice(1, -1),
|
|
697
|
+
depth: ctx.depth + 1,
|
|
698
|
+
hoisted: ctx.hoisted,
|
|
699
|
+
};
|
|
700
|
+
lines.push(...generateConstraintChecks('', raw, path, propSchema, suffix, valueCtx));
|
|
701
|
+
lines.push(...generateCombinatorChecks('', raw, path, propSchema, suffix, valueCtx));
|
|
566
702
|
return lines;
|
|
567
703
|
};
|
|
568
704
|
/**
|
|
@@ -698,7 +834,10 @@ const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
|
|
|
698
834
|
return [];
|
|
699
835
|
const child = {
|
|
700
836
|
objVar: `_obj${ctx.depth + 1}`,
|
|
701
|
-
|
|
837
|
+
// When `key` is empty the value is located AT `ctx.pathPrefix` already (e.g. an
|
|
838
|
+
// inline object reached through a combinator branch or a dynamic-key value), so
|
|
839
|
+
// appending `/${key}` would emit a spurious `//` or trailing `/` in error paths.
|
|
840
|
+
pathPrefix: key === '' ? ctx.pathPrefix : `${ctx.pathPrefix}/${pointerSegment(key)}`,
|
|
702
841
|
depth: ctx.depth + 1,
|
|
703
842
|
hoisted: ctx.hoisted,
|
|
704
843
|
};
|
|
@@ -712,6 +851,9 @@ const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
|
|
|
712
851
|
innerLines.push(...generatePatternAndAdditionalChecks(propSchema, suffix, child));
|
|
713
852
|
innerLines.push(...generateStrictKeyChecks(propSchema, child));
|
|
714
853
|
innerLines.push(...generateDependentRequiredChecks(propSchema, child));
|
|
854
|
+
innerLines.push(...generateDependentSchemasChecks(propSchema, suffix, child));
|
|
855
|
+
innerLines.push(...generateDependenciesChecks(propSchema, suffix, child));
|
|
856
|
+
innerLines.push(...generateMinMaxPropertiesChecks(propSchema, child));
|
|
715
857
|
if (hasPropertyNames(propSchema) && isSchemaObject(propSchema.propertyNames)) {
|
|
716
858
|
innerLines.push(...generatePropertyNameChecks(propSchema.propertyNames, suffix, child));
|
|
717
859
|
}
|
|
@@ -727,49 +869,29 @@ const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
|
|
|
727
869
|
];
|
|
728
870
|
};
|
|
729
871
|
/**
|
|
730
|
-
* Generates the `propertyNames` loop: every object key is a string,
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
872
|
+
* Generates the `propertyNames` loop: every object key is a string, and the
|
|
873
|
+
* *whole* subschema is validated against each key — not just the
|
|
874
|
+
* `pattern`/length/`enum`/`const`/`$ref` subset. Delegating to
|
|
875
|
+
* {@link generateValueChecks} keeps the generator in lockstep with the
|
|
876
|
+
* interpreter, which runs `matchesSchema(nameSchema, key)` per key, so a
|
|
877
|
+
* subschema carrying a combinator, `type`, `multipleOf`, etc. is enforced too.
|
|
878
|
+
* A key is always a present string, so the value checks run in `required` mode
|
|
879
|
+
* (no `!== undefined` guard).
|
|
734
880
|
*/
|
|
735
881
|
const generatePropertyNameChecks = (nameSchema, suffix, ctx) => {
|
|
736
882
|
if (!isSchemaObject(nameSchema))
|
|
737
883
|
return [];
|
|
738
884
|
const at = `\`${ctx.pathPrefix}/\${_name}\``;
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
if (hasPattern(nameSchema)) {
|
|
747
|
-
const re = escapeRegexPattern(nameSchema.pattern);
|
|
748
|
-
const msg = JSON.stringify(`property name must match pattern ${nameSchema.pattern}`);
|
|
749
|
-
checks.push(` if (!/${re}/.test(_name)) errors.push({ message: ${msg}, path: ${at} })`);
|
|
750
|
-
}
|
|
751
|
-
if (hasMinLength(nameSchema)) {
|
|
752
|
-
const msg = JSON.stringify(`property name must have at least ${nameSchema.minLength} characters`);
|
|
753
|
-
checks.push(` if (_name.length < ${nameSchema.minLength}) errors.push({ message: ${msg}, path: ${at} })`);
|
|
754
|
-
}
|
|
755
|
-
if (hasMaxLength(nameSchema)) {
|
|
756
|
-
const msg = JSON.stringify(`property name must have at most ${nameSchema.maxLength} characters`);
|
|
757
|
-
checks.push(` if (_name.length > ${nameSchema.maxLength}) errors.push({ message: ${msg}, path: ${at} })`);
|
|
758
|
-
}
|
|
759
|
-
if (hasEnum(nameSchema)) {
|
|
760
|
-
const allowed = JSON.stringify(nameSchema.enum);
|
|
761
|
-
const label = nameSchema.enum.map((v) => JSON.stringify(v)).join(', ');
|
|
762
|
-
const msg = JSON.stringify(`property name must be one of: ${label}`);
|
|
763
|
-
checks.push(` if (!(${allowed} as unknown[]).includes(_name)) errors.push({ message: ${msg}, path: ${at} })`);
|
|
764
|
-
}
|
|
765
|
-
if (hasConst(nameSchema)) {
|
|
766
|
-
const msg = JSON.stringify(`property name must be ${JSON.stringify(nameSchema.const)}`);
|
|
767
|
-
checks.push(` if (_name !== ${JSON.stringify(nameSchema.const)}) errors.push({ message: ${msg}, path: ${at} })`);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
885
|
+
const nameCtx = {
|
|
886
|
+
objVar: ctx.objVar,
|
|
887
|
+
pathPrefix: `${ctx.pathPrefix}/\${_name}`,
|
|
888
|
+
depth: ctx.depth + 1,
|
|
889
|
+
hoisted: ctx.hoisted,
|
|
890
|
+
};
|
|
891
|
+
const checks = generateValueChecks('', '_name', at, nameSchema, suffix, nameCtx, true);
|
|
770
892
|
if (checks.length === 0)
|
|
771
893
|
return [];
|
|
772
|
-
return [` for (const _name of Object.keys(${ctx.objVar})) {`, ...checks, ` }`];
|
|
894
|
+
return [` for (const _name of Object.keys(${ctx.objVar})) {`, ...checks.map((line) => ` ${line}`), ` }`];
|
|
773
895
|
};
|
|
774
896
|
/**
|
|
775
897
|
* Emits `dependentRequired` checks: when a trigger key is present, each of its
|
|
@@ -794,6 +916,129 @@ const generateDependentRequiredChecks = (schema, ctx) => {
|
|
|
794
916
|
}
|
|
795
917
|
return lines;
|
|
796
918
|
};
|
|
919
|
+
/**
|
|
920
|
+
* Emits `dependentSchemas` checks (2020-12): when a trigger property is present,
|
|
921
|
+
* the *whole object* must also match the associated subschema. Mirrors the
|
|
922
|
+
* interpreter, which applies the subschema in place against the object. A `true`
|
|
923
|
+
* subschema permits everything (no-op); a `false` subschema makes the trigger's
|
|
924
|
+
* presence always invalid.
|
|
925
|
+
*/
|
|
926
|
+
const generateDependentSchemasChecks = (schema, suffix, ctx) => {
|
|
927
|
+
if (!isSchemaObject(schema))
|
|
928
|
+
return [];
|
|
929
|
+
const dep = schema['dependentSchemas'];
|
|
930
|
+
if (typeof dep !== 'object' || dep === null || Array.isArray(dep))
|
|
931
|
+
return [];
|
|
932
|
+
const obj = ctx.objVar;
|
|
933
|
+
const at = ctx.depth === 0 ? '_path' : `\`${ctx.pathPrefix}\``;
|
|
934
|
+
const objPath = `\`${ctx.pathPrefix}\``;
|
|
935
|
+
const lines = [];
|
|
936
|
+
for (const [trigger, sub] of Object.entries(dep)) {
|
|
937
|
+
if (sub === true)
|
|
938
|
+
continue;
|
|
939
|
+
if (sub === false) {
|
|
940
|
+
const msg = JSON.stringify(`must NOT have property '${trigger}'`);
|
|
941
|
+
lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
|
|
942
|
+
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
943
|
+
lines.push(` }`);
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (!isSchemaObject(sub))
|
|
947
|
+
continue;
|
|
948
|
+
// The subschema applies to the object itself, so validate the current object
|
|
949
|
+
// variable against it and gate the whole block on the trigger's presence.
|
|
950
|
+
const checks = generateValueChecks('', obj, objPath, sub, suffix, ctx);
|
|
951
|
+
if (checks.length === 0)
|
|
952
|
+
continue;
|
|
953
|
+
lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
|
|
954
|
+
lines.push(...checks.map((line) => ` ${line}`));
|
|
955
|
+
lines.push(` }`);
|
|
956
|
+
}
|
|
957
|
+
return lines;
|
|
958
|
+
};
|
|
959
|
+
/**
|
|
960
|
+
* Emits draft-07 `dependencies` — the dual-form predecessor of
|
|
961
|
+
* `dependentRequired` + `dependentSchemas`. When a trigger property is present,
|
|
962
|
+
* an array value requires each listed key, and a schema value is applied to the
|
|
963
|
+
* *whole object*. Mirrors the interpreter, which branches on the value's shape.
|
|
964
|
+
* A `false` subschema makes the trigger's mere presence invalid; a `true`
|
|
965
|
+
* subschema is a no-op.
|
|
966
|
+
*/
|
|
967
|
+
const generateDependenciesChecks = (schema, suffix, ctx) => {
|
|
968
|
+
if (!isSchemaObject(schema))
|
|
969
|
+
return [];
|
|
970
|
+
const dep = schema['dependencies'];
|
|
971
|
+
if (typeof dep !== 'object' || dep === null || Array.isArray(dep))
|
|
972
|
+
return [];
|
|
973
|
+
const obj = ctx.objVar;
|
|
974
|
+
const at = ctx.depth === 0 ? '_path' : `\`${ctx.pathPrefix}\``;
|
|
975
|
+
const objPath = `\`${ctx.pathPrefix}\``;
|
|
976
|
+
const lines = [];
|
|
977
|
+
for (const [trigger, value] of Object.entries(dep)) {
|
|
978
|
+
// Array form: each listed key must be present when the trigger is.
|
|
979
|
+
if (Array.isArray(value)) {
|
|
980
|
+
for (const key of value) {
|
|
981
|
+
if (typeof key !== 'string')
|
|
982
|
+
continue;
|
|
983
|
+
const msg = JSON.stringify(`must have property '${key}' when '${trigger}' is present`);
|
|
984
|
+
lines.push(` if (${JSON.stringify(trigger)} in ${obj} && !(${JSON.stringify(key)} in ${obj})) {`);
|
|
985
|
+
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
986
|
+
lines.push(` }`);
|
|
987
|
+
}
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
// Schema form: the subschema applies to the object itself.
|
|
991
|
+
if (value === true)
|
|
992
|
+
continue;
|
|
993
|
+
if (value === false) {
|
|
994
|
+
const msg = JSON.stringify(`must NOT have property '${trigger}'`);
|
|
995
|
+
lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
|
|
996
|
+
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
997
|
+
lines.push(` }`);
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
if (!isSchemaObject(value))
|
|
1001
|
+
continue;
|
|
1002
|
+
const checks = generateValueChecks('', obj, objPath, value, suffix, ctx);
|
|
1003
|
+
if (checks.length === 0)
|
|
1004
|
+
continue;
|
|
1005
|
+
lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
|
|
1006
|
+
lines.push(...checks.map((line) => ` ${line}`));
|
|
1007
|
+
lines.push(` }`);
|
|
1008
|
+
}
|
|
1009
|
+
return lines;
|
|
1010
|
+
};
|
|
1011
|
+
/**
|
|
1012
|
+
* Emits `minProperties` / `maxProperties` bounds on the object's key count,
|
|
1013
|
+
* mirroring the interpreter (which counts the object's own enumerable keys and
|
|
1014
|
+
* reports at the object node). Counting once into a depth-scoped local keeps the
|
|
1015
|
+
* two bounds from re-walking the keys.
|
|
1016
|
+
*/
|
|
1017
|
+
const generateMinMaxPropertiesChecks = (schema, ctx) => {
|
|
1018
|
+
if (!isSchemaObject(schema))
|
|
1019
|
+
return [];
|
|
1020
|
+
const hasMin = hasMinProperties(schema);
|
|
1021
|
+
const hasMax = hasMaxProperties(schema);
|
|
1022
|
+
if (!hasMin && !hasMax)
|
|
1023
|
+
return [];
|
|
1024
|
+
const obj = ctx.objVar;
|
|
1025
|
+
const at = ctx.depth === 0 ? '_path' : `\`${ctx.pathPrefix}\``;
|
|
1026
|
+
const count = `_pc${ctx.depth}`;
|
|
1027
|
+
const lines = [` const ${count} = Object.keys(${obj}).length`];
|
|
1028
|
+
if (hasMin) {
|
|
1029
|
+
const msg = JSON.stringify(`must have at least ${schema.minProperties} properties`);
|
|
1030
|
+
lines.push(` if (${count} < ${schema.minProperties}) {`);
|
|
1031
|
+
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
1032
|
+
lines.push(` }`);
|
|
1033
|
+
}
|
|
1034
|
+
if (hasMax) {
|
|
1035
|
+
const msg = JSON.stringify(`must have at most ${schema.maxProperties} properties`);
|
|
1036
|
+
lines.push(` if (${count} > ${schema.maxProperties}) {`);
|
|
1037
|
+
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
1038
|
+
lines.push(` }`);
|
|
1039
|
+
}
|
|
1040
|
+
return lines;
|
|
1041
|
+
};
|
|
797
1042
|
/**
|
|
798
1043
|
* Builds the `&&` conditions that prove a single property is valid, or `null`
|
|
799
1044
|
* when the property carries any keyword the slow path enforces beyond a bare
|
|
@@ -803,6 +1048,17 @@ const generateDependentRequiredChecks = (schema, ctx) => {
|
|
|
803
1048
|
* input, never weakening a verdict. `objAcc` is the expression yielding the
|
|
804
1049
|
* parent object (already narrowed to a record); `key` indexes into it.
|
|
805
1050
|
*/
|
|
1051
|
+
/**
|
|
1052
|
+
* Whether an object schema carries a combinator keyword (`allOf`, `anyOf`,
|
|
1053
|
+
* `oneOf`, `not`, `if`) that the error-collecting slow path enforces but neither
|
|
1054
|
+
* flat guard can mirror. When present, both guards must bail so their early
|
|
1055
|
+
* `return true` never accepts a document the combinator would reject.
|
|
1056
|
+
*/
|
|
1057
|
+
const hasObjectLevelCombinator = (schema) => {
|
|
1058
|
+
if (!isSchemaObject(schema))
|
|
1059
|
+
return false;
|
|
1060
|
+
return hasAllOf(schema) || hasAnyOf(schema) || hasOneOf(schema) || 'not' in schema || 'if' in schema;
|
|
1061
|
+
};
|
|
806
1062
|
const guardPropConditions = (key, propSchema, objAcc) => {
|
|
807
1063
|
if (!isSchemaObject(propSchema))
|
|
808
1064
|
return null;
|
|
@@ -898,10 +1154,17 @@ const arrayRejectedByRequiredProp = (keys, required, properties) => {
|
|
|
898
1154
|
const guardObjectConditions = (schema, raw, objAcc) => {
|
|
899
1155
|
if (!isObjectSchema(schema))
|
|
900
1156
|
return null;
|
|
901
|
-
if (hasDependentRequired(schema) || hasPropertyNames(schema))
|
|
1157
|
+
if (hasDependentRequired(schema) || hasPropertyNames(schema) || 'dependentSchemas' in schema)
|
|
1158
|
+
return null;
|
|
1159
|
+
if (hasMinProperties(schema) || hasMaxProperties(schema) || 'dependencies' in schema)
|
|
902
1160
|
return null;
|
|
903
1161
|
if (isSchemaObject(schema) && 'patternProperties' in schema)
|
|
904
1162
|
return null;
|
|
1163
|
+
// Object-level combinators are enforced by the slow path but cannot be mirrored
|
|
1164
|
+
// by this flat guard, so bail — otherwise the guard's early `return true` would
|
|
1165
|
+
// accept documents the combinators reject.
|
|
1166
|
+
if (hasObjectLevelCombinator(schema))
|
|
1167
|
+
return null;
|
|
905
1168
|
let strict = false;
|
|
906
1169
|
if (hasAdditionalProperties(schema)) {
|
|
907
1170
|
// Only `additionalProperties: false` is guardable (via the key-count trick
|
|
@@ -969,6 +1232,14 @@ const generateObjectValidator = (schema, typeName, suffix) => {
|
|
|
969
1232
|
propertyLines.push(...generateStrictKeyChecks(schema, ctx));
|
|
970
1233
|
// dependentRequired — when a trigger property is present, its dependencies must be too.
|
|
971
1234
|
propertyLines.push(...generateDependentRequiredChecks(schema, ctx));
|
|
1235
|
+
// dependentSchemas — when a trigger property is present, the whole object must
|
|
1236
|
+
// also match the associated subschema.
|
|
1237
|
+
propertyLines.push(...generateDependentSchemasChecks(schema, suffix, ctx));
|
|
1238
|
+
// dependencies (draft-07) — the dual-form predecessor of dependentRequired +
|
|
1239
|
+
// dependentSchemas.
|
|
1240
|
+
propertyLines.push(...generateDependenciesChecks(schema, suffix, ctx));
|
|
1241
|
+
// minProperties / maxProperties — bound the object's key count.
|
|
1242
|
+
propertyLines.push(...generateMinMaxPropertiesChecks(schema, ctx));
|
|
972
1243
|
// propertyNames — every key (always a string) must satisfy the subschema. This
|
|
973
1244
|
// mirrors the interpreter, which runs the full subschema against each key.
|
|
974
1245
|
if (hasPropertyNames(schema) && isSchemaObject(schema.propertyNames)) {
|
|
@@ -1040,32 +1311,6 @@ const generateObjectValidator = (schema, typeName, suffix) => {
|
|
|
1040
1311
|
* e.g. "InfoObject" → "isInfoObject"
|
|
1041
1312
|
*/
|
|
1042
1313
|
const guardName = (typeName) => `is${typeName}`;
|
|
1043
|
-
/**
|
|
1044
|
-
* Positive type check for a value — the negation of {@link wrongTypeCondition}.
|
|
1045
|
-
* Used by the boolean type-guard, which proves validity with `&&` conditions
|
|
1046
|
-
* rather than collecting errors. Object is a shape-only check (matching the
|
|
1047
|
-
* validator, which never recurses into array items or untyped object values).
|
|
1048
|
-
*/
|
|
1049
|
-
const rightTypeCondition = (accessor, type) => {
|
|
1050
|
-
switch (type) {
|
|
1051
|
-
case 'string':
|
|
1052
|
-
return `typeof ${accessor} === 'string'`;
|
|
1053
|
-
case 'number':
|
|
1054
|
-
return `typeof ${accessor} === 'number'`;
|
|
1055
|
-
case 'integer':
|
|
1056
|
-
return `typeof ${accessor} === 'number' && Number.isInteger(${accessor})`;
|
|
1057
|
-
case 'boolean':
|
|
1058
|
-
return `typeof ${accessor} === 'boolean'`;
|
|
1059
|
-
case 'array':
|
|
1060
|
-
return `Array.isArray(${accessor})`;
|
|
1061
|
-
case 'null':
|
|
1062
|
-
return `${accessor} === null`;
|
|
1063
|
-
case 'object':
|
|
1064
|
-
return `typeof ${accessor} === 'object' && ${accessor} !== null && !Array.isArray(${accessor})`;
|
|
1065
|
-
default:
|
|
1066
|
-
return null;
|
|
1067
|
-
}
|
|
1068
|
-
};
|
|
1069
1314
|
/**
|
|
1070
1315
|
* Builds the membership test for an `enum`, matching the slow path's
|
|
1071
1316
|
* `[...].includes(value)` verdict exactly. For the common all-primitive case it
|
|
@@ -1163,10 +1408,10 @@ const booleanLeafExpr = (schema, acc) => {
|
|
|
1163
1408
|
}
|
|
1164
1409
|
};
|
|
1165
1410
|
/**
|
|
1166
|
-
* Boolean expression for an array value. Mirrors the validator
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1411
|
+
* Boolean expression for an array value. Mirrors the validator: array shape,
|
|
1412
|
+
* `minItems`/`maxItems`/`uniqueItems`, and each item validated in full via
|
|
1413
|
+
* {@link booleanLeafExpr}. Returns `null` for `$ref` items, or when an item schema
|
|
1414
|
+
* can't be expressed flat, so the whole guard defers to the validator.
|
|
1170
1415
|
*
|
|
1171
1416
|
* Item iteration goes through `Array.from` rather than `Array.prototype.every`
|
|
1172
1417
|
* because `every` *skips holes* in a sparse array (`[, 'x']`), whereas the
|
|
@@ -1183,7 +1428,11 @@ const booleanArrayExpr = (schema, acc) => {
|
|
|
1183
1428
|
if (hasMaxItems(schema))
|
|
1184
1429
|
parts.push(`${acc}.length <= ${schema.maxItems}`);
|
|
1185
1430
|
if (hasUniqueItems(schema) && schema.uniqueItems === true) {
|
|
1186
|
-
|
|
1431
|
+
// Same scalar-vs-structural split as the validator, so the guard's verdict
|
|
1432
|
+
// matches the slow path's for object items in a reordered key order.
|
|
1433
|
+
parts.push(arrayItemsAreScalarOnly(schema)
|
|
1434
|
+
? `new Set((${acc} as unknown[]).map((_u) => JSON.stringify(_u))).size === ${acc}.length`
|
|
1435
|
+
: `allUnique(${acc} as unknown[])`);
|
|
1187
1436
|
}
|
|
1188
1437
|
const base = parts.join(' && ');
|
|
1189
1438
|
if (!hasItems(schema))
|
|
@@ -1193,12 +1442,14 @@ const booleanArrayExpr = (schema, acc) => {
|
|
|
1193
1442
|
return base;
|
|
1194
1443
|
if (hasRef(items))
|
|
1195
1444
|
return null;
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1445
|
+
// Validate each item in full, mirroring the validator's per-item checks so the
|
|
1446
|
+
// guard reaches the identical verdict. `booleanLeafExpr` returns `null` for item
|
|
1447
|
+
// schemas it can't express flat — bail so the validator decides, keeping the
|
|
1448
|
+
// guard from ever accepting what the slow path would reject.
|
|
1449
|
+
const itemExpr = booleanLeafExpr(items, '_it');
|
|
1450
|
+
if (itemExpr === null)
|
|
1451
|
+
return null;
|
|
1452
|
+
return `${base} && Array.from(${acc} as unknown[]).every((_it) => (${itemExpr}))`;
|
|
1202
1453
|
};
|
|
1203
1454
|
/**
|
|
1204
1455
|
* Builds the `&&` conditions proving an object value is valid (same verdict as
|
|
@@ -1210,10 +1461,16 @@ const booleanObjectParts = (schema, raw, objAcc) => {
|
|
|
1210
1461
|
if (!isObjectSchema(schema))
|
|
1211
1462
|
return null;
|
|
1212
1463
|
// These need per-key loops or cross-references the flat form can't express.
|
|
1213
|
-
if (hasDependentRequired(schema) || hasPropertyNames(schema))
|
|
1464
|
+
if (hasDependentRequired(schema) || hasPropertyNames(schema) || 'dependentSchemas' in schema)
|
|
1465
|
+
return null;
|
|
1466
|
+
if (hasMinProperties(schema) || hasMaxProperties(schema) || 'dependencies' in schema)
|
|
1214
1467
|
return null;
|
|
1215
1468
|
if (isSchemaObject(schema) && 'patternProperties' in schema)
|
|
1216
1469
|
return null;
|
|
1470
|
+
// Object-level combinators change the verdict but can't be expressed flat, so
|
|
1471
|
+
// defer to the validator rather than emit a guard that ignores them.
|
|
1472
|
+
if (hasObjectLevelCombinator(schema))
|
|
1473
|
+
return null;
|
|
1217
1474
|
let strict = false;
|
|
1218
1475
|
if (hasAdditionalProperties(schema)) {
|
|
1219
1476
|
// Only `additionalProperties: false` is expressible; a schema needs per-key
|
|
@@ -1272,8 +1529,12 @@ const booleanObjectParts = (schema, raw, objAcc) => {
|
|
|
1272
1529
|
export const generateBooleanGuard = (schema, typeName, _suffix = '') => {
|
|
1273
1530
|
const name = guardName(typeName);
|
|
1274
1531
|
const fallback = `export const ${name} = (input: unknown): input is ${typeName} => ${validatorName(typeName)}(input) === true`;
|
|
1275
|
-
|
|
1276
|
-
|
|
1532
|
+
// Fold `nullable: true` into `anyOf` so the guard's verdict matches the
|
|
1533
|
+
// validator's (which applies the same rewrite). Without this a nullable node's
|
|
1534
|
+
// guard would reject `null` while the validator accepts it.
|
|
1535
|
+
const rewritten = rewriteNullable(schema);
|
|
1536
|
+
if (isObjectSchema(rewritten)) {
|
|
1537
|
+
const parts = booleanObjectParts(rewritten, 'input', 'obj');
|
|
1277
1538
|
if (parts === null)
|
|
1278
1539
|
return fallback;
|
|
1279
1540
|
return [
|
|
@@ -1286,7 +1547,7 @@ export const generateBooleanGuard = (schema, typeName, _suffix = '') => {
|
|
|
1286
1547
|
].join('\n');
|
|
1287
1548
|
}
|
|
1288
1549
|
// Non-object roots (scalar, enum, array) can often be expressed inline too.
|
|
1289
|
-
const expr = booleanLeafExpr(
|
|
1550
|
+
const expr = booleanLeafExpr(rewritten, 'input');
|
|
1290
1551
|
if (expr === null)
|
|
1291
1552
|
return fallback;
|
|
1292
1553
|
return `export const ${name} = (input: unknown): input is ${typeName} => ${expr}`;
|
|
@@ -1364,10 +1625,44 @@ const generateScalarValidator = (schema, typeName, suffix) => {
|
|
|
1364
1625
|
// (exactly one) and inline branches included, not just `$ref` branches.
|
|
1365
1626
|
if (hasAllOf(schema) || hasAnyOf(schema) || hasOneOf(schema) || 'not' in schema || 'if' in schema) {
|
|
1366
1627
|
const ctx = createRootContext();
|
|
1367
|
-
const checks =
|
|
1628
|
+
const checks = [];
|
|
1629
|
+
// The root path expression the shared emitters use, as a template literal body.
|
|
1630
|
+
const rootPath = '`${_path}`';
|
|
1631
|
+
// A `type` (and its sibling value constraints) alongside a combinator still
|
|
1632
|
+
// applies — the value must satisfy BOTH. Emit the type/constraint checks first,
|
|
1633
|
+
// then the combinator checks, so a schema like `{ type: 'string', not: {…} }`
|
|
1634
|
+
// or `{ type: 'number', minimum: 10, allOf: [{ maximum: 100 }] }` no longer
|
|
1635
|
+
// drops the `type` check and its siblings.
|
|
1636
|
+
const rootTypeArray = getTypeArray(schema);
|
|
1637
|
+
if (rootTypeArray) {
|
|
1638
|
+
const allWrong = rootTypeArray
|
|
1639
|
+
.map((t) => wrongTypeCondition('input', t))
|
|
1640
|
+
.filter((c) => c !== '')
|
|
1641
|
+
.map((c) => `(${c})`)
|
|
1642
|
+
.join(' && ');
|
|
1643
|
+
if (allWrong) {
|
|
1644
|
+
const label = rootTypeArray.map((t) => typeofString(t)).join(' or ');
|
|
1645
|
+
checks.push(` if (${allWrong}) {`);
|
|
1646
|
+
checks.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${rootPath} })`);
|
|
1647
|
+
checks.push(` }`);
|
|
1648
|
+
}
|
|
1649
|
+
checks.push(...generateConstraintChecks('', 'input', rootPath, schema, suffix, ctx));
|
|
1650
|
+
}
|
|
1651
|
+
else if (hasType(schema)) {
|
|
1652
|
+
const t = schema.type;
|
|
1653
|
+
const wrongType = wrongTypeCondition('input', t);
|
|
1654
|
+
if (wrongType) {
|
|
1655
|
+
checks.push(` if (${wrongType}) {`);
|
|
1656
|
+
checks.push(` errors.push({ message: 'must be ${typeofString(t)}', path: ${rootPath} })`);
|
|
1657
|
+
checks.push(` }`);
|
|
1658
|
+
}
|
|
1659
|
+
checks.push(...generateConstraintChecks('', 'input', rootPath, schema, suffix, ctx));
|
|
1660
|
+
}
|
|
1661
|
+
checks.push(...generateCombinatorChecks('', 'input', rootPath, schema, suffix, ctx));
|
|
1368
1662
|
const body = checks.join('\n').replaceAll('errors.push(', '(errors ??= []).push(');
|
|
1663
|
+
const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join('\n')}\n\n` : '';
|
|
1369
1664
|
return [
|
|
1370
|
-
|
|
1665
|
+
`${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1371
1666
|
` let errors: ValidationError[] | undefined`,
|
|
1372
1667
|
body,
|
|
1373
1668
|
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
@@ -1487,6 +1782,76 @@ const assertNoUnsupportedKeywords = (schema, typeName) => {
|
|
|
1487
1782
|
};
|
|
1488
1783
|
visit(schema);
|
|
1489
1784
|
};
|
|
1785
|
+
/** Keywords whose value is a single subschema (or a boolean schema). */
|
|
1786
|
+
const SINGLE_SUBSCHEMA_KEYS = new Set([
|
|
1787
|
+
'additionalProperties',
|
|
1788
|
+
'additionalItems',
|
|
1789
|
+
'contains',
|
|
1790
|
+
'propertyNames',
|
|
1791
|
+
'not',
|
|
1792
|
+
'if',
|
|
1793
|
+
'then',
|
|
1794
|
+
'else',
|
|
1795
|
+
'unevaluatedProperties',
|
|
1796
|
+
'unevaluatedItems',
|
|
1797
|
+
]);
|
|
1798
|
+
/** Keywords whose value is an array of subschemas. */
|
|
1799
|
+
const SUBSCHEMA_LIST_KEYS = new Set(['allOf', 'anyOf', 'oneOf', 'prefixItems']);
|
|
1800
|
+
/** Keywords whose value is a map of names to subschemas. */
|
|
1801
|
+
const SUBSCHEMA_MAP_KEYS = new Set(['properties', 'patternProperties', 'dependentSchemas', '$defs', 'definitions']);
|
|
1802
|
+
/**
|
|
1803
|
+
* Rewrites OpenAPI 3.0 `nullable: true` into a form the generator already
|
|
1804
|
+
* enforces. A node `{ nullable: true, ...rest }` accepts a value iff the value is
|
|
1805
|
+
* `null` OR it matches `rest` — exactly `{ anyOf: [{ type: 'null' }, rest] }`.
|
|
1806
|
+
* Emitting that lets the existing `anyOf` machinery (and its guard bail-outs)
|
|
1807
|
+
* mirror the interpreter, which short-circuits every keyword when a `nullable`
|
|
1808
|
+
* node sees `null`.
|
|
1809
|
+
*
|
|
1810
|
+
* The walk descends only into genuine subschema positions (never into `enum` /
|
|
1811
|
+
* `const` / `default` data), so a data value that merely looks like a schema is
|
|
1812
|
+
* never rewritten. Returns a fresh tree; the caller's schema is untouched.
|
|
1813
|
+
*/
|
|
1814
|
+
const rewriteNullable = (node) => {
|
|
1815
|
+
if (typeof node !== 'object' || node === null || Array.isArray(node))
|
|
1816
|
+
return node;
|
|
1817
|
+
const src = node;
|
|
1818
|
+
const out = {};
|
|
1819
|
+
for (const [key, value] of Object.entries(src)) {
|
|
1820
|
+
if (key === 'nullable')
|
|
1821
|
+
continue; // folded into the `anyOf` wrapper below
|
|
1822
|
+
if (SINGLE_SUBSCHEMA_KEYS.has(key)) {
|
|
1823
|
+
out[key] = rewriteNullable(value);
|
|
1824
|
+
}
|
|
1825
|
+
else if (key === 'items') {
|
|
1826
|
+
// `items` is either a single subschema (2020-12) or a tuple array (draft).
|
|
1827
|
+
out[key] = Array.isArray(value) ? value.map(rewriteNullable) : rewriteNullable(value);
|
|
1828
|
+
}
|
|
1829
|
+
else if (SUBSCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
|
|
1830
|
+
out[key] = value.map(rewriteNullable);
|
|
1831
|
+
}
|
|
1832
|
+
else if (SUBSCHEMA_MAP_KEYS.has(key) && typeof value === 'object' && value !== null) {
|
|
1833
|
+
const mapped = {};
|
|
1834
|
+
for (const [name, sub] of Object.entries(value))
|
|
1835
|
+
mapped[name] = rewriteNullable(sub);
|
|
1836
|
+
out[key] = mapped;
|
|
1837
|
+
}
|
|
1838
|
+
else if (key === 'dependencies' && typeof value === 'object' && value !== null) {
|
|
1839
|
+
// Dual-form: an array value lists required keys (data), a schema value is a
|
|
1840
|
+
// subschema.
|
|
1841
|
+
const mapped = {};
|
|
1842
|
+
for (const [name, sub] of Object.entries(value)) {
|
|
1843
|
+
mapped[name] = Array.isArray(sub) ? sub : rewriteNullable(sub);
|
|
1844
|
+
}
|
|
1845
|
+
out[key] = mapped;
|
|
1846
|
+
}
|
|
1847
|
+
else {
|
|
1848
|
+
out[key] = value;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (src['nullable'] === true)
|
|
1852
|
+
return { anyOf: [{ type: 'null' }, out] };
|
|
1853
|
+
return out;
|
|
1854
|
+
};
|
|
1490
1855
|
/**
|
|
1491
1856
|
* Generates a TypeScript validator function from a JSON Schema.
|
|
1492
1857
|
*
|
|
@@ -1511,8 +1876,11 @@ const assertNoUnsupportedKeywords = (schema, typeName) => {
|
|
|
1511
1876
|
*/
|
|
1512
1877
|
export const generateValidatorFunction = (schema, typeName, suffix = '') => {
|
|
1513
1878
|
assertNoUnsupportedKeywords(schema, typeName);
|
|
1514
|
-
|
|
1515
|
-
|
|
1879
|
+
// Fold OpenAPI `nullable: true` into the `anyOf` form the generator already
|
|
1880
|
+
// enforces, so the emitted checks match the interpreter's null short-circuit.
|
|
1881
|
+
const rewritten = rewriteNullable(schema);
|
|
1882
|
+
if (isObjectSchema(rewritten)) {
|
|
1883
|
+
return generateObjectValidator(rewritten, typeName, suffix);
|
|
1516
1884
|
}
|
|
1517
|
-
return generateScalarValidator(
|
|
1885
|
+
return generateScalarValidator(rewritten, typeName, suffix);
|
|
1518
1886
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-validators",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.7",
|
|
4
4
|
"description": "Generate TypeScript validation functions from JSON Schemas.",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -46,9 +46,10 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"json-schema-typed": "^8.0.1",
|
|
49
|
-
"@amritk/helpers": "0.
|
|
49
|
+
"@amritk/helpers": "0.13.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
+
"@amritk/runtime-validators": "0.7.1",
|
|
52
53
|
"@ryoppippi/unplugin-typia": "^2.6.5",
|
|
53
54
|
"@scalar/openapi-parser": "^0.26.1",
|
|
54
55
|
"@sinclair/typebox": "^0.34.49",
|