@amritk/generate-parsers 0.19.4 → 0.20.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/AI.md +5 -1
- package/README.md +18 -9
- package/dist/generators/build-schema.d.ts +4 -1
- package/dist/generators/build-schema.js +7 -0
- package/dist/generators/generate-parser-function.js +105 -44
- package/dist/generators/generate-schema-checks.js +30 -4
- package/dist/generators/generate-strict-assertion.d.ts +11 -0
- package/dist/generators/generate-strict-assertion.js +33 -3
- package/dist/generators/generate-type-checks.js +14 -5
- package/dist/generators/generate-validation-expression.d.ts +10 -0
- package/dist/generators/generate-validation-expression.js +80 -15
- package/dist/generators/subschema-match.js +3 -3
- package/dist/generators/unevaluated-match.js +2 -2
- package/dist/helpers/get-default-value.js +37 -10
- package/package.json +3 -3
package/AI.md
CHANGED
|
@@ -26,7 +26,11 @@ const files = await buildSchema(schema, 'Document')
|
|
|
26
26
|
To set a later flag you must pass every intervening positional. (The README's
|
|
27
27
|
short form omits the trailing seven.)
|
|
28
28
|
2. **It returns files in memory — it does NOT write to disk.** You write the
|
|
29
|
-
`GeneratedFile[]` (`{ filename, content }`) yourself.
|
|
29
|
+
`GeneratedFile[]` (`{ filename, content }`) yourself. `rootTypeName` becomes
|
|
30
|
+
that filename (lowercased) as well as the `export type` name, so it must be a
|
|
31
|
+
TypeScript identifier — `buildSchema` throws otherwise rather than hand you a
|
|
32
|
+
path that escapes your output directory. Derive it with `@amritk/helpers`'
|
|
33
|
+
`deriveRootTypeName`, which always produces one.
|
|
30
34
|
3. **Output is more than one file per `$def`:** always an `index.ts` barrel, and
|
|
31
35
|
(unless `typesOnly`) runtime helper files.
|
|
32
36
|
4. **Default parsers COERCE invalid input to defaults** rather than throwing;
|
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ Every option after the first two is **positional** — pass them in this order:
|
|
|
82
82
|
| # | Parameter | Type | Default | Description |
|
|
83
83
|
|--:|:---|:---|:---|:---|
|
|
84
84
|
| 1 | `rootSchema` | `JSONSchema` | — | The root schema to traverse. `$ref` and `$dynamicRef` are resolved recursively. |
|
|
85
|
-
| 2 | `rootTypeName` | `string` | — | Name used for the root type (e.g. `"Document"`). |
|
|
85
|
+
| 2 | `rootTypeName` | `string` | — | Name used for the root type (e.g. `"Document"`). Must be a TypeScript identifier: it is emitted verbatim as `export type <name>` and, lowercased, becomes the output filename, so a name derived from an untrusted document is refused rather than written outside your output directory. `@amritk/helpers`' `deriveRootTypeName` always produces one. |
|
|
86
86
|
| 3 | `extensions` | `SchemaExtensions` | — | Map of definition name → extra optional properties to merge in before generation. |
|
|
87
87
|
| 4 | `typesOnly` | `boolean` | — | Only emit `.ts` type definitions — skip parser functions and runtime helpers. |
|
|
88
88
|
| 5 | `logWarnings` | `boolean` | — | Generated parsers emit a `console.warn` for every input key not declared in the schema's properties. |
|
|
@@ -296,23 +296,32 @@ keys) and `parseStrict` (assert + reject undeclared keys) halves of
|
|
|
296
296
|
[`moltar/typescript-runtime-type-benchmarks`](https://github.com/moltar/typescript-runtime-type-benchmarks)
|
|
297
297
|
against the other *pure* parsers — the ones that return a new typed value rather
|
|
298
298
|
than mutating in place — [zod](https://zod.dev) and
|
|
299
|
-
[TypeBox](https://github.com/sinclairzx81/typebox). Measured on Bun 1.
|
|
299
|
+
[TypeBox](https://github.com/sinclairzx81/typebox). Measured on Bun 1.4 (Linux
|
|
300
300
|
x64), parsing valid input at steady state:
|
|
301
301
|
|
|
302
302
|
| case | mode | mjst (generated) | zod | typebox |
|
|
303
303
|
|:--|:--|--:|--:|--:|
|
|
304
|
-
| user (4 fields) | parseSafe | **~18M** ops/s | ~
|
|
305
|
-
| order (nested + array) | parseSafe | **~
|
|
306
|
-
| user (4 fields) | parseStrict | **~
|
|
307
|
-
| order (nested + array) | parseStrict | **~
|
|
304
|
+
| user (4 fields) | parseSafe | **~18M** ops/s | ~3.7M ops/s | ~1.6M ops/s |
|
|
305
|
+
| order (nested + array) | parseSafe | **~6.9M** ops/s | ~0.71M ops/s | ~0.24M ops/s |
|
|
306
|
+
| user (4 fields) | parseStrict | **~14M** ops/s | ~2.3M ops/s | ~2.04M ops/s |
|
|
307
|
+
| order (nested + array) | parseStrict | **~7M** ops/s | ~0.43M ops/s | ~0.36M ops/s |
|
|
308
308
|
|
|
309
309
|
The upstream `assert` case (seven scalar roots plus a nested object) runs faster
|
|
310
|
-
still — ~
|
|
310
|
+
still — ~51M ops/s strict, ~127M safe — but at that size the numbers swing enough
|
|
311
311
|
run-to-run that the ratio, not the absolute, is the honest signal: mjst lands
|
|
312
|
-
~
|
|
312
|
+
~5–29× ahead of zod across every case above.
|
|
313
|
+
|
|
314
|
+
What is replicated upstream is the *cases* — the two parse modes and their
|
|
315
|
+
shapes — not the harness. These numbers come from this repo's own measurement
|
|
316
|
+
core (direct calls over a pool of distinct inputs, median of many trials) and
|
|
317
|
+
are not comparable with the public leaderboard, which measures every operation
|
|
318
|
+
through benny and reports something an order of magnitude smaller for the same
|
|
319
|
+
function. `@amritk/generate-validators` carries the side-by-side and a
|
|
320
|
+
reproducible run of both harnesses:
|
|
321
|
+
[Against the moltar harness](../generate-validators#against-the-moltar-harness).
|
|
313
322
|
|
|
314
323
|
The trade is a one-shot **prepare** cost that only mjst pays — generating the
|
|
315
|
-
parser source — which measures **~0.
|
|
324
|
+
parser source — which measures **~0.2–0.8 ms** per schema here (zod and TypeBox
|
|
316
325
|
author or interpret their parsers with no separate build step, so there is
|
|
317
326
|
nothing to time). That is trivially amortized: you generate once at build time
|
|
318
327
|
and run the emitted code forever.
|
|
@@ -19,7 +19,10 @@ export type GeneratedFile = {
|
|
|
19
19
|
* TypeScript file, then emits an `index.ts` barrel re-exporting them all.
|
|
20
20
|
*
|
|
21
21
|
* @param rootSchema - The root JSON Schema to build from
|
|
22
|
-
* @param rootTypeName - The name for the root type (e.g., "Document")
|
|
22
|
+
* @param rootTypeName - The name for the root type (e.g., "Document"). Must be a TypeScript
|
|
23
|
+
* identifier: it is emitted verbatim as `export type <name>` and, lowercased, becomes the
|
|
24
|
+
* output filename, so a name derived from an untrusted document is refused rather than
|
|
25
|
+
* written outside the output directory.
|
|
23
26
|
* @param extensions - Optional map of custom extension properties to add to specific definitions.
|
|
24
27
|
* Keys are definition names (matching $defs keys), values are records of extension property
|
|
25
28
|
* names to their JSON Schema definitions. Extensions are merged as optional properties before
|
|
@@ -20,7 +20,14 @@ const readHelperSource = async (helper, importExt) => {
|
|
|
20
20
|
return { ext: "js", content: compiled };
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
|
+
const ROOT_TYPE_NAME = /^[\p{ID_Start}_$][\p{ID_Continue}$]*$/u;
|
|
24
|
+
const assertRootTypeName = (rootTypeName) => {
|
|
25
|
+
if (ROOT_TYPE_NAME.test(rootTypeName))
|
|
26
|
+
return;
|
|
27
|
+
throw new Error(`Invalid root type name ${JSON.stringify(rootTypeName)}: expected a TypeScript identifier. The name is emitted as \`export type <name>\` and, lowercased, used as the output filename, so anything else either fails to compile or escapes the output directory. Derive it with @amritk/helpers' \`deriveRootTypeName\`, which always produces one.`);
|
|
28
|
+
};
|
|
23
29
|
const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = "package", helpersImportPrefix = "./", readonly = false, stripUnknown = false, typeSuffix = "", importExt = "js", caseInsensitive = false, schemas) => {
|
|
30
|
+
assertRootTypeName(rootTypeName);
|
|
24
31
|
const files = [];
|
|
25
32
|
const usedHelpers = /* @__PURE__ */ new Set();
|
|
26
33
|
const foldedSchema = foldNullable(rootSchema);
|
|
@@ -151,7 +151,25 @@ const generateFallbackValue = (_, propSchema, useRefImports, suffix) => {
|
|
|
151
151
|
return getDefaultValue(propSchema);
|
|
152
152
|
};
|
|
153
153
|
const PROTOTYPE_MEMBER_NAMES = new Set(Object.getOwnPropertyNames(Object.prototype));
|
|
154
|
-
const declaresPrototypeMemberProperty = (schema
|
|
154
|
+
const declaresPrototypeMemberProperty = (schema, depth = 4) => {
|
|
155
|
+
if (!isSchemaObject(schema) || depth < 0)
|
|
156
|
+
return false;
|
|
157
|
+
if (hasProperties(schema) && Object.keys(schema.properties).some((key) => PROTOTYPE_MEMBER_NAMES.has(key)))
|
|
158
|
+
return true;
|
|
159
|
+
const record = schema;
|
|
160
|
+
const children = [];
|
|
161
|
+
if (hasProperties(schema))
|
|
162
|
+
children.push(...Object.values(schema.properties));
|
|
163
|
+
const items = record["items"];
|
|
164
|
+
if (Array.isArray(items))
|
|
165
|
+
children.push(...items);
|
|
166
|
+
else if (items !== void 0)
|
|
167
|
+
children.push(items);
|
|
168
|
+
const prefixItems = record["prefixItems"];
|
|
169
|
+
if (Array.isArray(prefixItems))
|
|
170
|
+
children.push(...prefixItems);
|
|
171
|
+
return children.some((child) => declaresPrototypeMemberProperty(child, depth - 1));
|
|
172
|
+
};
|
|
155
173
|
const generateFallbackObject = (schema, useRefImports, typeName, suffix, subTypeNames) => {
|
|
156
174
|
if (!hasProperties(schema)) {
|
|
157
175
|
return `{} as ${typeName}`;
|
|
@@ -183,33 +201,11 @@ const generateFallbackObject = (schema, useRefImports, typeName, suffix, subType
|
|
|
183
201
|
}
|
|
184
202
|
}
|
|
185
203
|
result += "\n }";
|
|
186
|
-
return declaresPrototypeMemberProperty(schema) ? `${result} as ${typeName}` : result;
|
|
204
|
+
return declaresPrototypeMemberProperty(schema) ? `${result} as unknown as ${typeName}` : result;
|
|
187
205
|
};
|
|
188
206
|
const scalarDefaultLiteral = (schema) => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (hasConst(schema))
|
|
192
|
-
return JSON.stringify(schema.const);
|
|
193
|
-
if (hasEnum(schema) && schema.enum.length > 0)
|
|
194
|
-
return JSON.stringify(schema.enum[0]);
|
|
195
|
-
if (hasType(schema)) {
|
|
196
|
-
switch (schema.type) {
|
|
197
|
-
case "string":
|
|
198
|
-
return '""';
|
|
199
|
-
case "number":
|
|
200
|
-
case "integer":
|
|
201
|
-
return "0";
|
|
202
|
-
case "boolean":
|
|
203
|
-
return "false";
|
|
204
|
-
case "null":
|
|
205
|
-
return "null";
|
|
206
|
-
case "array":
|
|
207
|
-
return "[]";
|
|
208
|
-
case "object":
|
|
209
|
-
return "{}";
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return "{}";
|
|
207
|
+
const value = getDefaultValue(schema);
|
|
208
|
+
return value === "undefined" ? "{}" : value;
|
|
213
209
|
};
|
|
214
210
|
const generateRefUnionDispatch = (functionName, typeName, schema, branches, strict, ctx) => {
|
|
215
211
|
const rootSchema = ctx.rootSchema;
|
|
@@ -552,9 +548,57 @@ const exactKeyCountOf = (schema) => {
|
|
|
552
548
|
return null;
|
|
553
549
|
return declaredCount;
|
|
554
550
|
};
|
|
555
|
-
const shouldCacheVariable = (propSchema
|
|
551
|
+
const shouldCacheVariable = (propSchema) => {
|
|
556
552
|
return isSchemaObject(propSchema);
|
|
557
553
|
};
|
|
554
|
+
const INLINE_NESTED_MAX_PROPERTIES = 12;
|
|
555
|
+
const INLINE_NESTED_MAX_SITES = 4;
|
|
556
|
+
const INLINE_NESTED_MAX_TOTAL_PROPERTIES = 24;
|
|
557
|
+
const inlineNestedFastPath = (childSchema, childTypeName, objectExpr, useRefImports, suffix, stripUnknown, reservedNames) => {
|
|
558
|
+
if (!isSchemaObject(childSchema) || !hasProperties(childSchema))
|
|
559
|
+
return null;
|
|
560
|
+
if (!stripUnknown)
|
|
561
|
+
return null;
|
|
562
|
+
if (hasAdditionalProperties(childSchema) && childSchema.additionalProperties === false)
|
|
563
|
+
return null;
|
|
564
|
+
if (hasAllOf(childSchema) || hasOneOf(childSchema) || hasAnyOf(childSchema))
|
|
565
|
+
return null;
|
|
566
|
+
if (hasFastPathBlockingKeyword(childSchema))
|
|
567
|
+
return null;
|
|
568
|
+
if (hasMinProperties(childSchema) || hasMaxProperties(childSchema))
|
|
569
|
+
return null;
|
|
570
|
+
if (exactKeyCountOf(childSchema) === null)
|
|
571
|
+
return null;
|
|
572
|
+
const { objects, arrayItems } = collectInlineSubTypes(childSchema, childTypeName, reservedNames);
|
|
573
|
+
if (objects.size > 0 || arrayItems.size > 0)
|
|
574
|
+
return null;
|
|
575
|
+
const props = childSchema.properties;
|
|
576
|
+
const keys = Object.keys(props);
|
|
577
|
+
if (keys.length === 0 || keys.length > INLINE_NESTED_MAX_PROPERTIES)
|
|
578
|
+
return null;
|
|
579
|
+
const checks = [];
|
|
580
|
+
const fields = [];
|
|
581
|
+
const readRoot = `(${objectExpr} as Record<string, any>)`;
|
|
582
|
+
for (const key of keys) {
|
|
583
|
+
const propSchema = props[key];
|
|
584
|
+
if (shouldUseRefImport(propSchema, useRefImports) || shouldUseArrayRefImport(propSchema, useRefImports) || shouldUseRecordRefImport(propSchema, useRefImports)) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
const accessor = safeAccessor(readRoot, key);
|
|
588
|
+
const check = generatePropertyTypeCheck(accessor, propSchema, useRefImports, suffix);
|
|
589
|
+
if (check === null)
|
|
590
|
+
return null;
|
|
591
|
+
checks.push(check);
|
|
592
|
+
fields.push(`${safeLiteralKey(key)}: ${accessor}`);
|
|
593
|
+
}
|
|
594
|
+
const condition = [
|
|
595
|
+
`typeof ${objectExpr} === "object"`,
|
|
596
|
+
`${objectExpr} !== null`,
|
|
597
|
+
`!Array.isArray(${objectExpr})`,
|
|
598
|
+
...checks
|
|
599
|
+
].join(" && ");
|
|
600
|
+
return { condition, literal: `{ ${fields.join(", ")} }`, propertyCount: keys.length };
|
|
601
|
+
};
|
|
558
602
|
const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema, reservedNames = NO_RESERVED_NAMES, caseInsensitive = false, shapeValidatorSource) => {
|
|
559
603
|
const functionName = generateParserName(typeName);
|
|
560
604
|
const exportPrefix = exported ? "export " : "";
|
|
@@ -592,9 +636,15 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
592
636
|
}
|
|
593
637
|
const strictKeys = hasStrictKeys(schema);
|
|
594
638
|
const stripKeys = strictKeys || stripUnknown;
|
|
595
|
-
const
|
|
639
|
+
const rejectsUnknownKeys = strict && isSchemaObject(schema) && hasAdditionalProperties(schema) && schema.additionalProperties === false && !("patternProperties" in schema) || false;
|
|
640
|
+
const guardKeys = stripKeys || rejectsUnknownKeys;
|
|
641
|
+
const fastPathBuildsDeclaredLiteral = stripKeys && !(isSchemaObject(schema) && hasAllOf(schema));
|
|
642
|
+
const fastPathNeedsKnownKeys = guardKeys && (rejectsUnknownKeys || !fastPathBuildsDeclaredLiteral);
|
|
643
|
+
const droppedKnownKeysTerm = guardKeys && !fastPathNeedsKnownKeys;
|
|
644
|
+
const exactKeyCount = guardKeys ? exactKeyCountOf(schema) : null;
|
|
596
645
|
const strictKeyCheck = unknownKeyCheck(propertyKeys, `_knownKeys${typeName}`);
|
|
597
|
-
|
|
646
|
+
const shapeValidatorNeedsKnownKeys = shapeValidatorSource?.includes(`_hasOnlyKnownKeys${typeName}(`) ?? false;
|
|
647
|
+
if ((fastPathNeedsKnownKeys || shapeValidatorNeedsKnownKeys) && exactKeyCount === null) {
|
|
598
648
|
for (const declaration of strictKeyCheck.declarations) {
|
|
599
649
|
preamble.push(`${declaration};`);
|
|
600
650
|
}
|
|
@@ -602,7 +652,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
602
652
|
for (const _k in input) if (${strictKeyCheck.isUnknown("_k")}) return false;
|
|
603
653
|
return true;
|
|
604
654
|
};`);
|
|
605
|
-
} else if (
|
|
655
|
+
} else if (rejectsUnknownKeys) {
|
|
606
656
|
for (const declaration of strictKeyCheck.declarations) {
|
|
607
657
|
preamble.push(`${declaration};`);
|
|
608
658
|
}
|
|
@@ -661,13 +711,13 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
661
711
|
fastPathAccessorChecks.push(`Object.keys(input).length <= ${schema.maxProperties}`);
|
|
662
712
|
}
|
|
663
713
|
}
|
|
664
|
-
if (
|
|
714
|
+
if (fastPathNeedsKnownKeys) {
|
|
665
715
|
fastPathChecks.push(exactKeyCount !== null ? `Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${exactKeyCount}` : `_hasOnlyKnownKeys${typeName}(input)`);
|
|
666
716
|
}
|
|
667
717
|
const deepGuard = canFastPath && fastPathChecks.length > 0 ? fastPathChecks.join(" && ") : null;
|
|
668
718
|
const literalReturnUsesVars = stripKeys && !(isSchemaObject(schema) && hasAllOf(schema)) && exported;
|
|
669
719
|
let deepGuardExpr = deepGuard;
|
|
670
|
-
if (deepGuard !== null && shapeValidatorSource !== void 0 && !literalReturnUsesVars) {
|
|
720
|
+
if (deepGuard !== null && shapeValidatorSource !== void 0 && !literalReturnUsesVars && !droppedKnownKeysTerm) {
|
|
671
721
|
const shapeFnName = shapeValidatorName(typeName);
|
|
672
722
|
const shapeStrictKeysGuard = stripKeys && exactKeyCount === null ? `
|
|
673
723
|
if (!_hasOnlyKnownKeys${typeName}(input)) return false;` : "";
|
|
@@ -710,7 +760,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
710
760
|
const shallowGuard = canShallowGuard && shallowChecks.length > 0 ? shallowChecks.join(" && ") : null;
|
|
711
761
|
const varDeclLines = [];
|
|
712
762
|
for (const { key, varName, propSchema } of propInfo) {
|
|
713
|
-
if (shouldCacheVariable(propSchema
|
|
763
|
+
if (shouldCacheVariable(propSchema)) {
|
|
714
764
|
varDeclLines.push(` const ${varName} = ${safeAccessor("input", key)};`);
|
|
715
765
|
}
|
|
716
766
|
}
|
|
@@ -728,6 +778,8 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
728
778
|
}
|
|
729
779
|
const buildObjectLines = (directAssign) => {
|
|
730
780
|
const objectLines = [];
|
|
781
|
+
let inlinedSites = 0;
|
|
782
|
+
let inlinedProperties = 0;
|
|
731
783
|
if (!stripKeys) {
|
|
732
784
|
objectLines.push(" ...input,");
|
|
733
785
|
}
|
|
@@ -740,12 +792,21 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
740
792
|
}
|
|
741
793
|
}
|
|
742
794
|
for (const { key, varName, isRequired, propSchema } of propInfo) {
|
|
743
|
-
const shouldCache = shouldCacheVariable(propSchema
|
|
795
|
+
const shouldCache = shouldCacheVariable(propSchema);
|
|
744
796
|
const accessor = shouldCache ? varName : safeAccessor("input", key);
|
|
745
797
|
const subName = subTypeNames.get(key);
|
|
746
798
|
if (subName) {
|
|
747
799
|
const subParserName = generateParserName(subName);
|
|
748
800
|
if (isRequired) {
|
|
801
|
+
const inlined = directAssign && !logWarnings && inlinedSites < INLINE_NESTED_MAX_SITES ? inlineNestedFastPath(propSchema, subName, accessor, useRefImports, suffix, stripUnknown, reservedNames) : null;
|
|
802
|
+
if (inlined !== null && inlinedProperties + inlined.propertyCount <= INLINE_NESTED_MAX_TOTAL_PROPERTIES) {
|
|
803
|
+
inlinedSites++;
|
|
804
|
+
inlinedProperties += inlined.propertyCount;
|
|
805
|
+
objectLines.push(` ${safeLiteralKey(key)}: (${inlined.condition})
|
|
806
|
+
? ${inlined.literal}
|
|
807
|
+
: ${subParserName}(${accessor}),`);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
749
810
|
objectLines.push(` ${safeLiteralKey(key)}: ${subParserName}(${accessor}),`);
|
|
750
811
|
} else {
|
|
751
812
|
objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${subParserName}(${accessor}) }),`);
|
|
@@ -828,12 +889,11 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
828
889
|
lines2.push(` } as unknown as ${typeName};`);
|
|
829
890
|
};
|
|
830
891
|
const emitDeepGuardReturn = (lines2) => {
|
|
831
|
-
|
|
832
|
-
if (!canLiteral) {
|
|
892
|
+
if (!fastPathBuildsDeclaredLiteral) {
|
|
833
893
|
lines2.push(` if (${deepGuardExpr}) return { ...input } as ${typeName};`);
|
|
834
894
|
return;
|
|
835
895
|
}
|
|
836
|
-
if (!exported) {
|
|
896
|
+
if (!exported && fastPathNeedsKnownKeys) {
|
|
837
897
|
lines2.push(` if (${deepGuardExpr}) return input as ${typeName};`);
|
|
838
898
|
return;
|
|
839
899
|
}
|
|
@@ -848,12 +908,12 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
848
908
|
}
|
|
849
909
|
continue;
|
|
850
910
|
}
|
|
851
|
-
const accessor = shouldCacheVariable(propSchema
|
|
911
|
+
const accessor = shouldCacheVariable(propSchema) ? varName : safeAccessor("input", key);
|
|
852
912
|
fields.push(isRequired ? ` ${safeLiteralKey(key)}: ${accessor},` : ` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${accessor} }),`);
|
|
853
913
|
}
|
|
854
914
|
lines2.push(` if (${deepGuardExpr}) return {`);
|
|
855
915
|
lines2.push(fields.join("\n"));
|
|
856
|
-
lines2.push(` } as ${typeName};`);
|
|
916
|
+
lines2.push(` } as unknown as ${typeName};`);
|
|
857
917
|
};
|
|
858
918
|
const lines = [];
|
|
859
919
|
lines.push(`${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {`);
|
|
@@ -870,7 +930,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
870
930
|
...rootSchema !== void 0 ? { rootSchema } : {}
|
|
871
931
|
}).slice(1);
|
|
872
932
|
if (shallowGuard) {
|
|
873
|
-
if (!exported && deepGuard) {
|
|
933
|
+
if (!exported && deepGuard && fastPathNeedsKnownKeys) {
|
|
874
934
|
const residual = [];
|
|
875
935
|
for (let i = 0; i < fastPathChecks.length; i++) {
|
|
876
936
|
const deep = fastPathChecks[i];
|
|
@@ -903,7 +963,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
903
963
|
for (const assertionLine of assertionLines) {
|
|
904
964
|
lines.push(assertionLine);
|
|
905
965
|
}
|
|
906
|
-
if (
|
|
966
|
+
if (rejectsUnknownKeys) {
|
|
907
967
|
lines.push(` for (const _k in input) {`);
|
|
908
968
|
lines.push(` if (${strictKeyCheck.isUnknown("_k")}) throw new Error(\`[${typeName}] unknown property "\${_k}"\`);`);
|
|
909
969
|
lines.push(` }`);
|
|
@@ -1280,6 +1340,7 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1280
1340
|
stripUnknown,
|
|
1281
1341
|
...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {}
|
|
1282
1342
|
};
|
|
1343
|
+
const copyLoopContext = { ...context, subParsers: false };
|
|
1283
1344
|
if (!options?.isRoot && typeName === `Schema${suffix}`) {
|
|
1284
1345
|
return generateSchemaObjectParser(typeName);
|
|
1285
1346
|
}
|
|
@@ -1296,7 +1357,7 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1296
1357
|
});
|
|
1297
1358
|
}
|
|
1298
1359
|
if (hasProperties(schema) && isSchemaObject(schema) && "patternProperties" in schema) {
|
|
1299
|
-
return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown, caseInsensitive,
|
|
1360
|
+
return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown, caseInsensitive, copyLoopContext);
|
|
1300
1361
|
}
|
|
1301
1362
|
if (strict && isSchemaObject(schema) && Array.isArray(schema.type) && schema.type.includes("null")) {
|
|
1302
1363
|
return generateNonObjectParser(typeName, schema, strict, {
|
|
@@ -1342,14 +1403,14 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1342
1403
|
});
|
|
1343
1404
|
}
|
|
1344
1405
|
if ("patternProperties" in schema) {
|
|
1345
|
-
return generatePatternPropertiesParser(schema, typeName, useRefImports, suffix, strict,
|
|
1406
|
+
return generatePatternPropertiesParser(schema, typeName, useRefImports, suffix, strict, copyLoopContext);
|
|
1346
1407
|
}
|
|
1347
1408
|
if ("additionalProperties" in schema && !hasProperties(schema)) {
|
|
1348
1409
|
const additionalProps = schema.additionalProperties;
|
|
1349
1410
|
if (additionalProps === true || additionalProps === false) {
|
|
1350
1411
|
return generateEmptyObjectParser(typeName, strict, schema, context);
|
|
1351
1412
|
}
|
|
1352
|
-
return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict,
|
|
1413
|
+
return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict, copyLoopContext);
|
|
1353
1414
|
}
|
|
1354
1415
|
if ("type" in schema && schema.type === "object" && !hasProperties(schema)) {
|
|
1355
1416
|
return generateEmptyObjectParser(typeName, strict, schema, context);
|
|
@@ -3,6 +3,8 @@ import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extens
|
|
|
3
3
|
import { multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
|
|
4
4
|
import { hasOwnCheck } from "@amritk/helpers/safe-accessor";
|
|
5
5
|
import { hasAdditionalProperties, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasPattern, hasProperties, hasRequired, hasType, hasUniqueItems, isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
6
|
+
import { maxLengthPassExpr, minLengthPassExpr } from "@amritk/helpers/string-length-check";
|
|
7
|
+
import { generateDeepEqualCheck } from "./generate-deep-equal-check.js";
|
|
6
8
|
import { generateEnumCheck } from "./generate-enum-check.js";
|
|
7
9
|
import { generateUniqueItemsCheck } from "./generate-unique-items-check.js";
|
|
8
10
|
const OBJECT_KEYWORDS = [
|
|
@@ -95,10 +97,10 @@ const generateInferredChecks = (accessor, schema, type) => {
|
|
|
95
97
|
checks.push(`${regexLiteral(schema.pattern)}.test(${accessor})`);
|
|
96
98
|
}
|
|
97
99
|
if (hasMinLength(schema)) {
|
|
98
|
-
checks.push(
|
|
100
|
+
checks.push(minLengthPassExpr(accessor, schema.minLength));
|
|
99
101
|
}
|
|
100
102
|
if (hasMaxLength(schema)) {
|
|
101
|
-
checks.push(
|
|
103
|
+
checks.push(maxLengthPassExpr(accessor, schema.maxLength));
|
|
102
104
|
}
|
|
103
105
|
break;
|
|
104
106
|
case "number":
|
|
@@ -140,6 +142,23 @@ const generateSchemaChecks = (accessor, schema) => {
|
|
|
140
142
|
if (primitive) {
|
|
141
143
|
return [`typeof ${accessor} === "${primitive}"`];
|
|
142
144
|
}
|
|
145
|
+
if (hasConst(schema)) {
|
|
146
|
+
return [generateDeepEqualCheck(accessor, schema.const)];
|
|
147
|
+
}
|
|
148
|
+
if (isSchemaObject(schema) && Array.isArray(schema.type)) {
|
|
149
|
+
const branches = [];
|
|
150
|
+
for (const type of schema.type) {
|
|
151
|
+
if (typeof type !== "string")
|
|
152
|
+
continue;
|
|
153
|
+
const branch = generateSchemaChecks(accessor, { ...schema, type });
|
|
154
|
+
if (branch.length === 0)
|
|
155
|
+
continue;
|
|
156
|
+
branches.push(branch.length === 1 ? branch[0] : `(${branch.join(" && ")})`);
|
|
157
|
+
}
|
|
158
|
+
if (branches.length === 0)
|
|
159
|
+
return checks;
|
|
160
|
+
return [branches.length === 1 ? branches[0] : `(${branches.join(" || ")})`];
|
|
161
|
+
}
|
|
143
162
|
if (!hasType(schema)) {
|
|
144
163
|
const inferred = inferSchemaType(schema);
|
|
145
164
|
if (!inferred) {
|
|
@@ -158,10 +177,10 @@ const generateSchemaChecks = (accessor, schema) => {
|
|
|
158
177
|
checks.push(`${regexLiteral(schema.pattern)}.test(${accessor})`);
|
|
159
178
|
}
|
|
160
179
|
if (hasMinLength(schema)) {
|
|
161
|
-
checks.push(
|
|
180
|
+
checks.push(minLengthPassExpr(accessor, schema.minLength));
|
|
162
181
|
}
|
|
163
182
|
if (hasMaxLength(schema)) {
|
|
164
|
-
checks.push(
|
|
183
|
+
checks.push(maxLengthPassExpr(accessor, schema.maxLength));
|
|
165
184
|
}
|
|
166
185
|
break;
|
|
167
186
|
}
|
|
@@ -190,6 +209,13 @@ const generateSchemaChecks = (accessor, schema) => {
|
|
|
190
209
|
case "boolean":
|
|
191
210
|
checks.push(`typeof ${accessor} === "boolean"`);
|
|
192
211
|
break;
|
|
212
|
+
// Without this the `null` branch of a union produced NO check, which reads
|
|
213
|
+
// as "this branch matches nothing": `{ anyOf: [{ type: 'string' },
|
|
214
|
+
// { type: 'null' }] }` rejected `null` — the one value the branch exists to
|
|
215
|
+
// admit — and the coercing build dropped the property instead of keeping it.
|
|
216
|
+
case "null":
|
|
217
|
+
checks.push(`${accessor} === null`);
|
|
218
|
+
break;
|
|
193
219
|
case "array":
|
|
194
220
|
checks.push(`Array.isArray(${accessor})`);
|
|
195
221
|
if (hasMinItems(schema)) {
|
|
@@ -13,6 +13,17 @@ export type StrictAssertionContext = {
|
|
|
13
13
|
readonly suffix?: string;
|
|
14
14
|
readonly rootSchema?: Record<string, unknown>;
|
|
15
15
|
readonly stripUnknown?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the parser these assertions run inside also emits the private
|
|
18
|
+
* sub-parsers for its inline object properties (and inline-object array
|
|
19
|
+
* items). `generateObjectParser` does, so the shape of such a property is
|
|
20
|
+
* proven by the call it delegates to and asserting it here would be
|
|
21
|
+
* redundant. The pattern-property parsers do NOT — they build their result
|
|
22
|
+
* with a copy loop — so a nested object property there is proven by nobody
|
|
23
|
+
* unless these assertions prove it. Defaults to true, the historical
|
|
24
|
+
* assumption.
|
|
25
|
+
*/
|
|
26
|
+
readonly subParsers?: boolean;
|
|
16
27
|
};
|
|
17
28
|
/**
|
|
18
29
|
* Why a node needs the whole-schema backstop, or `null` when the specialized
|
|
@@ -10,8 +10,9 @@ import { generateDeepEqualCheck } from "./generate-deep-equal-check.js";
|
|
|
10
10
|
import { generateEnumCheck } from "./generate-enum-check.js";
|
|
11
11
|
import { canEnforceUnion, generateUnionCheck, getUnionBranches, isExclusiveUnion, isInlineObjectProperty, multiTypeCheck } from "./generate-type-checks.js";
|
|
12
12
|
import { generateUniqueItemsCheck } from "./generate-unique-items-check.js";
|
|
13
|
-
import { everyTailItem, getPrefixItems,
|
|
13
|
+
import { everyTailItem, getPrefixItems, plainScalarItemCheck, prefixItemsCapsLength } from "./generate-validation-expression.js";
|
|
14
14
|
import { hasConstrainingRefSibling, subschemaMatchExpr } from "./subschema-match.js";
|
|
15
|
+
const shapeIsDelegated = (schema, context) => (context.subParsers ?? true) && isInlineObjectProperty(schema);
|
|
15
16
|
const matchContext = (context) => context.rootSchema ? { rootSchema: context.rootSchema } : {};
|
|
16
17
|
const TYPELESS_CONSTRAINT_KEYWORDS = [
|
|
17
18
|
"minimum",
|
|
@@ -212,11 +213,11 @@ const generateItemCheck = (schema, context) => {
|
|
|
212
213
|
const label = items.enum.map((v) => JSON.stringify(v)).join(", ");
|
|
213
214
|
return { check: generateEnumCheck("_it", items.enum), message: `items must be one of: ${label}` };
|
|
214
215
|
}
|
|
215
|
-
const scalarCheck =
|
|
216
|
+
const scalarCheck = plainScalarItemCheck(items, "_it");
|
|
216
217
|
if (scalarCheck !== null && isSchemaObject(items) && hasType(items)) {
|
|
217
218
|
return { check: scalarCheck, message: `items expected ${typeLabel(items.type)}` };
|
|
218
219
|
}
|
|
219
|
-
if (hasRef(items) && context.useRefImports === true ||
|
|
220
|
+
if (hasRef(items) && context.useRefImports === true || shapeIsDelegated(items, context))
|
|
220
221
|
return null;
|
|
221
222
|
const match = subschemaMatchExpr("_it", items, matchContext(context));
|
|
222
223
|
if (match === null || match === "true")
|
|
@@ -232,6 +233,34 @@ const nullableObjectShapeChecks = (acc, schema, type, label, optional, context)
|
|
|
232
233
|
const presence = optional ? `${acc} !== undefined && ${acc} !== null` : `${acc} !== null`;
|
|
233
234
|
return [` if (${presence} && !(${match})) ${throwError(`${label} does not match the declared object shape`)};`];
|
|
234
235
|
};
|
|
236
|
+
const objectShapeResidual = (schema, context) => {
|
|
237
|
+
if (!isSchemaObject(schema) || !hasType(schema) || schema.type !== "object")
|
|
238
|
+
return null;
|
|
239
|
+
if (shapeIsDelegated(schema, context))
|
|
240
|
+
return null;
|
|
241
|
+
const record = schema;
|
|
242
|
+
const residual = { type: "object" };
|
|
243
|
+
if (hasProperties(schema))
|
|
244
|
+
residual["properties"] = record["properties"];
|
|
245
|
+
if (hasRequired(schema))
|
|
246
|
+
residual["required"] = record["required"];
|
|
247
|
+
if (record["additionalProperties"] === false) {
|
|
248
|
+
residual["additionalProperties"] = false;
|
|
249
|
+
if ("patternProperties" in record)
|
|
250
|
+
residual["patternProperties"] = record["patternProperties"];
|
|
251
|
+
}
|
|
252
|
+
return "properties" in residual || "required" in residual || "additionalProperties" in residual ? residual : null;
|
|
253
|
+
};
|
|
254
|
+
const declaredObjectShapeChecks = (acc, schema, label, optional, context) => {
|
|
255
|
+
const residual = objectShapeResidual(schema, context);
|
|
256
|
+
if (residual === null)
|
|
257
|
+
return [];
|
|
258
|
+
const match = subschemaMatchExpr(acc, residual, matchContext(context));
|
|
259
|
+
if (match === null || match === "true")
|
|
260
|
+
return [];
|
|
261
|
+
const presence = optional ? `${acc} !== undefined && ` : "";
|
|
262
|
+
return [` if (${presence}!(${match})) ${throwError(`${label} does not match the declared object shape`)};`];
|
|
263
|
+
};
|
|
235
264
|
const generateNotCheck = (acc, schema, label, optional, context) => {
|
|
236
265
|
if (!isSchemaObject(schema) || !("not" in schema))
|
|
237
266
|
return [];
|
|
@@ -518,6 +547,7 @@ const generatePropertyAssertion = (key, propSchema, isRequired, typeName, contex
|
|
|
518
547
|
lines.push(...generateAllOfChecks(acc, propSchema, field, !isRequired, context));
|
|
519
548
|
lines.push(...generateUnionMatchChecks(acc, propSchema, field, !isRequired, context));
|
|
520
549
|
lines.push(...generateConditionalChecks(acc, propSchema, field, context));
|
|
550
|
+
lines.push(...declaredObjectShapeChecks(acc, propSchema, field, !isRequired, context));
|
|
521
551
|
const unionBranches = getUnionBranches(propSchema);
|
|
522
552
|
if (unionBranches) {
|
|
523
553
|
if (!context.stripUnknown && canEnforceUnion(unionBranches, context.rootSchema)) {
|
|
@@ -3,10 +3,11 @@ import { multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
|
|
|
3
3
|
import { refToName } from "@amritk/helpers/ref-to-name";
|
|
4
4
|
import { resolveRef } from "@amritk/helpers/resolve-ref";
|
|
5
5
|
import { hasOwnCheck, safeAccessor } from "@amritk/helpers/safe-accessor";
|
|
6
|
-
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
6
|
+
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
7
|
+
import { maxLengthPassExpr, minLengthPassExpr } from "@amritk/helpers/string-length-check";
|
|
7
8
|
import { generateEnumCheck } from "./generate-enum-check.js";
|
|
8
9
|
import { generateUniqueItemsCheck } from "./generate-unique-items-check.js";
|
|
9
|
-
import { everyTailItem, getPrefixItems,
|
|
10
|
+
import { everyTailItem, getPrefixItems, plainScalarItemCheck, prefixItemsCapsLength, singleTypeCheck } from "./generate-validation-expression.js";
|
|
10
11
|
import { hasConstrainingRefSibling, subschemaMatchExpr } from "./subschema-match.js";
|
|
11
12
|
const shapeValidatorName = (typeName) => `validate${typeName}Shape`;
|
|
12
13
|
const isInlineObjectProperty = (propSchema) => {
|
|
@@ -92,6 +93,10 @@ const CONSTRAINT_KEYWORDS = [
|
|
|
92
93
|
"minProperties",
|
|
93
94
|
"maxProperties"
|
|
94
95
|
];
|
|
96
|
+
const hasBranchSiblingConstraint = (schema) => {
|
|
97
|
+
const record = schema;
|
|
98
|
+
return "type" in record || CONSTRAINT_KEYWORDS.some((keyword) => keyword in record);
|
|
99
|
+
};
|
|
95
100
|
const multiTypeCheck = (varName, schema, options = {}) => {
|
|
96
101
|
if (!isSchemaObject(schema) || !Array.isArray(schema.type))
|
|
97
102
|
return void 0;
|
|
@@ -125,6 +130,8 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
|
|
|
125
130
|
}
|
|
126
131
|
const branches = getUnionBranches(schema);
|
|
127
132
|
if (branches) {
|
|
133
|
+
if (hasBranchSiblingConstraint(schema))
|
|
134
|
+
return null;
|
|
128
135
|
return generateUnionCheck(varName, branches, useRefImports, suffix, isExclusiveUnion(schema));
|
|
129
136
|
}
|
|
130
137
|
if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || "not" in schema) {
|
|
@@ -150,9 +157,9 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
|
|
|
150
157
|
if (hasPattern(schema))
|
|
151
158
|
checks.push(`${regexLiteral(schema.pattern)}.test(${varName})`);
|
|
152
159
|
if (hasMinLength(schema))
|
|
153
|
-
checks.push(
|
|
160
|
+
checks.push(minLengthPassExpr(varName, schema.minLength));
|
|
154
161
|
if (hasMaxLength(schema))
|
|
155
|
-
checks.push(
|
|
162
|
+
checks.push(maxLengthPassExpr(varName, schema.maxLength));
|
|
156
163
|
break;
|
|
157
164
|
}
|
|
158
165
|
case "number":
|
|
@@ -186,7 +193,7 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
|
|
|
186
193
|
}
|
|
187
194
|
if (hasItems(schema) && !Array.isArray(schema.items)) {
|
|
188
195
|
const items = schema.items;
|
|
189
|
-
const itemCheck =
|
|
196
|
+
const itemCheck = plainScalarItemCheck(items, "_it") ?? (isSchemaObject(items) && hasEnum(items) && items.enum.length > 0 ? generateEnumCheck("_it", items.enum) : null);
|
|
190
197
|
if (itemCheck) {
|
|
191
198
|
checks.push(everyTailItem(varName, itemCheck, schema));
|
|
192
199
|
} else if (!(isSchemaObject(items) && (hasRef(items) || isInlineObjectProperty(items)))) {
|
|
@@ -211,6 +218,8 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
|
|
|
211
218
|
break;
|
|
212
219
|
}
|
|
213
220
|
case "object":
|
|
221
|
+
if (hasProperties(schema) || hasRequired(schema))
|
|
222
|
+
return null;
|
|
214
223
|
checks.push(`isObject(${varName})`);
|
|
215
224
|
if (hasMinProperties(schema))
|
|
216
225
|
checks.push(`Object.keys(${varName}).length >= ${schema.minProperties}`);
|
|
@@ -6,6 +6,16 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
|
6
6
|
* objects, arrays, unions, `$ref`s, and anything richer.
|
|
7
7
|
*/
|
|
8
8
|
export declare const scalarItemTypeCheck: (itemSchema: JSONSchema, accessor: string) => string | null;
|
|
9
|
+
/**
|
|
10
|
+
* {@link scalarItemTypeCheck}, but only for an item schema whose `type` is the
|
|
11
|
+
* *whole* constraint — the one case where the bare `typeof` test is an exact
|
|
12
|
+
* match rather than a weakening of it. Emit sites that need a check they can
|
|
13
|
+
* trust in both directions (a true-sound fast-path guard, a strict assertion
|
|
14
|
+
* that throws) take this one; the coercion path keeps the looser
|
|
15
|
+
* {@link isCoercibleItemSchema}, which coerces each element through the full
|
|
16
|
+
* per-element expression and so applies the constraints anyway.
|
|
17
|
+
*/
|
|
18
|
+
export declare const plainScalarItemCheck: (itemSchema: JSONSchema, accessor: string) => string | null;
|
|
9
19
|
/**
|
|
10
20
|
* True when an array's `items` schema can be coerced element-by-element by the
|
|
11
21
|
* mapping slow path: single scalar types (via {@link scalarItemTypeCheck}) and
|
|
@@ -4,6 +4,7 @@ import { multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
|
|
|
4
4
|
import { resolveRef } from "@amritk/helpers/resolve-ref";
|
|
5
5
|
import { hasOwnCheck, safeAccessor } from "@amritk/helpers/safe-accessor";
|
|
6
6
|
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
7
|
+
import { maxLengthPassExpr, minLengthPassExpr } from "@amritk/helpers/string-length-check";
|
|
7
8
|
import { findDiscriminator } from "../helpers/find-discriminator.js";
|
|
8
9
|
import { getDefaultValue } from "../helpers/get-default-value.js";
|
|
9
10
|
import { generateDiscriminatedUnionValidation } from "./generate-discriminated-union-validation.js";
|
|
@@ -34,10 +35,43 @@ const scalarItemTypeCheck = (itemSchema, accessor) => {
|
|
|
34
35
|
return null;
|
|
35
36
|
}
|
|
36
37
|
};
|
|
38
|
+
const SCALAR_ITEM_CONSTRAINTS = [
|
|
39
|
+
"pattern",
|
|
40
|
+
"minLength",
|
|
41
|
+
"maxLength",
|
|
42
|
+
"minimum",
|
|
43
|
+
"maximum",
|
|
44
|
+
"exclusiveMinimum",
|
|
45
|
+
"exclusiveMaximum",
|
|
46
|
+
"multipleOf",
|
|
47
|
+
"const",
|
|
48
|
+
"$ref",
|
|
49
|
+
"not",
|
|
50
|
+
"allOf",
|
|
51
|
+
"anyOf",
|
|
52
|
+
"oneOf",
|
|
53
|
+
"if"
|
|
54
|
+
];
|
|
55
|
+
const plainScalarItemCheck = (itemSchema, accessor) => {
|
|
56
|
+
if (!isSchemaObject(itemSchema))
|
|
57
|
+
return null;
|
|
58
|
+
const record = itemSchema;
|
|
59
|
+
if (SCALAR_ITEM_CONSTRAINTS.some((keyword) => keyword in record))
|
|
60
|
+
return null;
|
|
61
|
+
return scalarItemTypeCheck(itemSchema, accessor);
|
|
62
|
+
};
|
|
37
63
|
const isCoercibleItemSchema = (itemSchema) => {
|
|
38
64
|
if (scalarItemTypeCheck(itemSchema, "_it") !== null)
|
|
39
65
|
return true;
|
|
40
|
-
|
|
66
|
+
if (!isSchemaObject(itemSchema))
|
|
67
|
+
return false;
|
|
68
|
+
const itemTypes = itemSchema["type"];
|
|
69
|
+
if (Array.isArray(itemTypes) && itemTypes.length > 0) {
|
|
70
|
+
return itemTypes.every((type) => typeof type === "string" && scalarItemTypeCheck({ type }, "_it") !== null);
|
|
71
|
+
}
|
|
72
|
+
if (hasConst(itemSchema))
|
|
73
|
+
return true;
|
|
74
|
+
return hasEnum(itemSchema) && itemSchema.enum.length > 0;
|
|
41
75
|
};
|
|
42
76
|
const getPrefixItems = (schema) => {
|
|
43
77
|
if (!isSchemaObject(schema))
|
|
@@ -50,7 +84,7 @@ const getPrefixItems = (schema) => {
|
|
|
50
84
|
const everyTailItem = (accessor, itemCheck, schema) => {
|
|
51
85
|
const prefix = isSchemaObject(schema) ? schema["prefixItems"] : void 0;
|
|
52
86
|
const tailStart = Array.isArray(prefix) ? prefix.length : 0;
|
|
53
|
-
return tailStart === 0 ? `${accessor}.every((_it) => ${itemCheck})` : `${accessor}.every((_it, _ix) => _ix < ${tailStart} || (${itemCheck}))`;
|
|
87
|
+
return tailStart === 0 ? `${accessor}.every((_it: unknown) => ${itemCheck})` : `${accessor}.every((_it: unknown, _ix: number) => _ix < ${tailStart} || (${itemCheck}))`;
|
|
54
88
|
};
|
|
55
89
|
const prefixItemsCapsLength = (schema) => {
|
|
56
90
|
if (!isSchemaObject(schema))
|
|
@@ -83,25 +117,55 @@ const getTypeArray = (schema) => {
|
|
|
83
117
|
return null;
|
|
84
118
|
return schema.type;
|
|
85
119
|
};
|
|
120
|
+
const stringBoundChecks = (valueExpr, schema) => {
|
|
121
|
+
const checks = [];
|
|
122
|
+
if (hasPattern(schema))
|
|
123
|
+
checks.push(`${regexLiteral(schema.pattern)}.test(${valueExpr})`);
|
|
124
|
+
if (hasMinLength(schema))
|
|
125
|
+
checks.push(minLengthPassExpr(valueExpr, schema.minLength));
|
|
126
|
+
if (hasMaxLength(schema))
|
|
127
|
+
checks.push(maxLengthPassExpr(valueExpr, schema.maxLength));
|
|
128
|
+
return checks;
|
|
129
|
+
};
|
|
130
|
+
const numberBoundChecks = (valueExpr, schema) => {
|
|
131
|
+
const checks = [];
|
|
132
|
+
if (hasMinimum(schema))
|
|
133
|
+
checks.push(`${valueExpr} >= ${schema.minimum}`);
|
|
134
|
+
if (hasMaximum(schema))
|
|
135
|
+
checks.push(`${valueExpr} <= ${schema.maximum}`);
|
|
136
|
+
if (hasExclusiveMinimum(schema))
|
|
137
|
+
checks.push(`${valueExpr} > ${schema.exclusiveMinimum}`);
|
|
138
|
+
if (hasExclusiveMaximum(schema))
|
|
139
|
+
checks.push(`${valueExpr} < ${schema.exclusiveMaximum}`);
|
|
140
|
+
if (hasMultipleOf(schema))
|
|
141
|
+
checks.push(multipleOfPassExpr(valueExpr, schema.multipleOf));
|
|
142
|
+
return checks;
|
|
143
|
+
};
|
|
86
144
|
const getTypeCoercion = (accessor, schema, defaultValue) => {
|
|
87
145
|
if (!hasType(schema)) {
|
|
88
146
|
return null;
|
|
89
147
|
}
|
|
90
148
|
switch (schema.type) {
|
|
91
|
-
case "string":
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
149
|
+
case "string": {
|
|
150
|
+
const coerced = `String(${accessor})`;
|
|
151
|
+
const bounds = stringBoundChecks("_s", schema);
|
|
152
|
+
if (bounds.length === 0)
|
|
153
|
+
return coerced;
|
|
154
|
+
return `((_s) => ${bounds.join(" && ")} ? _s : ${defaultValue})(${coerced})`;
|
|
96
155
|
}
|
|
156
|
+
case "number":
|
|
97
157
|
case "integer": {
|
|
158
|
+
const wellFormed = schema.type === "integer" ? "Number.isInteger" : "Number.isFinite";
|
|
159
|
+
const bounds = numberBoundChecks("_n", schema);
|
|
98
160
|
const n = `Number(${accessor})`;
|
|
99
|
-
|
|
161
|
+
if (bounds.length === 0)
|
|
162
|
+
return `(${wellFormed}(${n}) ? ${n} : ${defaultValue})`;
|
|
163
|
+
return `((_n) => ${[`${wellFormed}(_n)`, ...bounds].join(" && ")} ? _n : ${defaultValue})(${n})`;
|
|
100
164
|
}
|
|
101
165
|
case "boolean":
|
|
102
166
|
return `Boolean(${accessor})`;
|
|
103
167
|
case "array":
|
|
104
|
-
return
|
|
168
|
+
return defaultValue;
|
|
105
169
|
case "object":
|
|
106
170
|
return `typeof ${accessor} === "object" && ${accessor} !== null ? ${accessor} : ${defaultValue}`;
|
|
107
171
|
default:
|
|
@@ -166,9 +230,9 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
|
|
|
166
230
|
for (let i = 1; i < allChecks.length; i++) {
|
|
167
231
|
combinedCheck2 += " && " + allChecks[i];
|
|
168
232
|
}
|
|
169
|
-
return `${combinedCheck2} ? ${accessor} : ${defaultValue}`;
|
|
233
|
+
return `${combinedCheck2} ? ${accessor} : ${isRequired ? defaultValue : "undefined"}`;
|
|
170
234
|
}
|
|
171
|
-
return `${accessor} ?? ${defaultValue}
|
|
235
|
+
return isRequired ? `${accessor} ?? ${defaultValue}` : accessor;
|
|
172
236
|
}
|
|
173
237
|
if ("not" in schema && schema.not) {
|
|
174
238
|
const notChecks = generateSchemaChecks(accessor, schema.not);
|
|
@@ -177,7 +241,7 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
|
|
|
177
241
|
for (let i = 1; i < notChecks.length; i++) {
|
|
178
242
|
combinedNotCheck += " && " + notChecks[i];
|
|
179
243
|
}
|
|
180
|
-
return `!(${combinedNotCheck}) ? ${accessor} : ${defaultValue}`;
|
|
244
|
+
return `!(${combinedNotCheck}) ? ${accessor} : ${isRequired ? defaultValue : "undefined"}`;
|
|
181
245
|
}
|
|
182
246
|
}
|
|
183
247
|
if (hasConst(schema)) {
|
|
@@ -193,7 +257,7 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
|
|
|
193
257
|
}
|
|
194
258
|
}
|
|
195
259
|
if (!hasType(schema) && !hasEnum(schema)) {
|
|
196
|
-
return `${accessor} ?? ${defaultValue}
|
|
260
|
+
return isRequired ? `${accessor} ?? ${defaultValue}` : accessor;
|
|
197
261
|
}
|
|
198
262
|
const prefixItems = getPrefixItems(schema);
|
|
199
263
|
if (hasType(schema) && schema.type === "array" && prefixItems) {
|
|
@@ -215,10 +279,10 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
|
|
|
215
279
|
checks.push(`${regexLiteral(schema.pattern)}.test(${accessor})`);
|
|
216
280
|
}
|
|
217
281
|
if (hasMinLength(schema)) {
|
|
218
|
-
checks.push(
|
|
282
|
+
checks.push(minLengthPassExpr(accessor, schema.minLength));
|
|
219
283
|
}
|
|
220
284
|
if (hasMaxLength(schema)) {
|
|
221
|
-
checks.push(
|
|
285
|
+
checks.push(maxLengthPassExpr(accessor, schema.maxLength));
|
|
222
286
|
}
|
|
223
287
|
break;
|
|
224
288
|
}
|
|
@@ -336,6 +400,7 @@ export {
|
|
|
336
400
|
generateValidationExpression,
|
|
337
401
|
getPrefixItems,
|
|
338
402
|
isCoercibleItemSchema,
|
|
403
|
+
plainScalarItemCheck,
|
|
339
404
|
prefixItemsCapsLength,
|
|
340
405
|
scalarItemTypeCheck,
|
|
341
406
|
singleTypeCheck
|
|
@@ -73,7 +73,7 @@ const ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
73
73
|
const depthOf = (ctx) => ctx.depth ?? 0;
|
|
74
74
|
const nest = (ctx) => ({ ...ctx, depth: depthOf(ctx) + 1 });
|
|
75
75
|
const isObjectExpr = (acc) => `typeof ${acc} === "object" && ${acc} !== null && !Array.isArray(${acc})`;
|
|
76
|
-
const recordOf = (acc) => `(${acc} as Record<string,
|
|
76
|
+
const recordOf = (acc) => `(${acc} as Record<string, any>)`;
|
|
77
77
|
const patternEntriesOf = (schema) => {
|
|
78
78
|
const source = schema["patternProperties"];
|
|
79
79
|
if (typeof source !== "object" || source === null || Array.isArray(source))
|
|
@@ -118,7 +118,7 @@ const tupleShape = (schema) => {
|
|
|
118
118
|
const arrayConstraints = (acc, schema, ctx) => {
|
|
119
119
|
const c = [];
|
|
120
120
|
const d = depthOf(ctx);
|
|
121
|
-
const elements = `(${acc} as
|
|
121
|
+
const elements = `(${acc} as any[])`;
|
|
122
122
|
if (hasMinItems(schema))
|
|
123
123
|
c.push(`${acc}.length >= ${schema.minItems}`);
|
|
124
124
|
if (hasMaxItems(schema))
|
|
@@ -182,7 +182,7 @@ const containsConstraint = (acc, schema, ctx) => {
|
|
|
182
182
|
const match = subschemaMatchExpr(el, s["contains"], nest(ctx));
|
|
183
183
|
if (match === null)
|
|
184
184
|
return null;
|
|
185
|
-
const count = `(${acc} as
|
|
185
|
+
const count = `(${acc} as any[]).filter((${el}) => ${match}).length`;
|
|
186
186
|
const bounds = [];
|
|
187
187
|
if (min > 0)
|
|
188
188
|
bounds.push(`${count} >= ${min}`);
|
|
@@ -52,7 +52,7 @@ const ownItemCoverage = (schema, indexVar, ignoreOwnUnevaluated) => {
|
|
|
52
52
|
const containsCoverage = (schema, indexVar, acc, ctx, match) => {
|
|
53
53
|
if (!("contains" in schema))
|
|
54
54
|
return NONE;
|
|
55
|
-
const itemMatch = match(`(${acc} as
|
|
55
|
+
const itemMatch = match(`(${acc} as any[])[${indexVar}]`, schema["contains"], nest(ctx));
|
|
56
56
|
if (itemMatch === null)
|
|
57
57
|
return null;
|
|
58
58
|
if (itemMatch === "true")
|
|
@@ -186,7 +186,7 @@ const unevaluatedItemsExpr = (acc, schema, ctx, match) => {
|
|
|
186
186
|
return null;
|
|
187
187
|
if (coverage.all)
|
|
188
188
|
return void 0;
|
|
189
|
-
const elements = `(${acc} as
|
|
189
|
+
const elements = `(${acc} as any[])`;
|
|
190
190
|
const covered = coverage.terms.length > 0 ? orJoin(coverage.terms) : null;
|
|
191
191
|
if (unevaluated === false) {
|
|
192
192
|
if (covered === null)
|
|
@@ -44,6 +44,40 @@ const arrayFallback = (schema) => {
|
|
|
44
44
|
}
|
|
45
45
|
return `[${elements.join(", ")}]`;
|
|
46
46
|
};
|
|
47
|
+
const MAX_FALLBACK_LENGTH = 256;
|
|
48
|
+
const stringFallback = (schema) => {
|
|
49
|
+
if (hasPattern(schema)) {
|
|
50
|
+
const patternDefault = generateDefaultFromPattern(schema.pattern);
|
|
51
|
+
if (patternDefault)
|
|
52
|
+
return patternDefault;
|
|
53
|
+
return '""';
|
|
54
|
+
}
|
|
55
|
+
const minLength = schema["minLength"];
|
|
56
|
+
if (typeof minLength !== "number" || !(minLength > 0))
|
|
57
|
+
return '""';
|
|
58
|
+
return JSON.stringify("x".repeat(Math.min(minLength, MAX_FALLBACK_LENGTH)));
|
|
59
|
+
};
|
|
60
|
+
const finiteKeyword = (schema, keyword) => {
|
|
61
|
+
const value = schema[keyword];
|
|
62
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
63
|
+
};
|
|
64
|
+
const numberFallback = (schema, integer) => {
|
|
65
|
+
const minimum = finiteKeyword(schema, "minimum");
|
|
66
|
+
const maximum = finiteKeyword(schema, "maximum");
|
|
67
|
+
const exclusiveMinimum = finiteKeyword(schema, "exclusiveMinimum");
|
|
68
|
+
const exclusiveMaximum = finiteKeyword(schema, "exclusiveMaximum");
|
|
69
|
+
const multipleOf = finiteKeyword(schema, "multipleOf");
|
|
70
|
+
const low = Math.max(minimum ?? -Infinity, exclusiveMinimum !== void 0 ? exclusiveMinimum + 1 : -Infinity);
|
|
71
|
+
const high = Math.min(maximum ?? Infinity, exclusiveMaximum !== void 0 ? exclusiveMaximum - 1 : Infinity);
|
|
72
|
+
let candidate = Math.min(Math.max(0, low), high);
|
|
73
|
+
if (integer)
|
|
74
|
+
candidate = Math.ceil(candidate);
|
|
75
|
+
if (multipleOf !== void 0 && multipleOf > 0 && candidate !== 0) {
|
|
76
|
+
candidate = Math.ceil(candidate / multipleOf) * multipleOf;
|
|
77
|
+
}
|
|
78
|
+
const satisfies = Number.isFinite(candidate) && (minimum === void 0 || candidate >= minimum) && (maximum === void 0 || candidate <= maximum) && (exclusiveMinimum === void 0 || candidate > exclusiveMinimum) && (exclusiveMaximum === void 0 || candidate < exclusiveMaximum) && (!integer || Number.isInteger(candidate)) && (multipleOf === void 0 || multipleOf <= 0 || Number.isInteger(candidate / multipleOf));
|
|
79
|
+
return satisfies ? String(candidate) : "0";
|
|
80
|
+
};
|
|
47
81
|
const getDefaultValue = (schema) => {
|
|
48
82
|
if (!isSchemaObject(schema)) {
|
|
49
83
|
return "undefined";
|
|
@@ -89,18 +123,11 @@ const getDefaultValue = (schema) => {
|
|
|
89
123
|
return "undefined";
|
|
90
124
|
}
|
|
91
125
|
switch (schema.type) {
|
|
92
|
-
case "string":
|
|
93
|
-
|
|
94
|
-
const patternDefault = generateDefaultFromPattern(schema.pattern);
|
|
95
|
-
if (patternDefault) {
|
|
96
|
-
return patternDefault;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return '""';
|
|
100
|
-
}
|
|
126
|
+
case "string":
|
|
127
|
+
return stringFallback(schema);
|
|
101
128
|
case "number":
|
|
102
129
|
case "integer":
|
|
103
|
-
return "
|
|
130
|
+
return numberFallback(schema, schema.type === "integer");
|
|
104
131
|
case "boolean":
|
|
105
132
|
return "false";
|
|
106
133
|
case "null":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-parsers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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",
|
|
@@ -56,10 +56,10 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"json-schema-typed": "^8.0.1",
|
|
59
|
-
"@amritk/helpers": "^0.
|
|
59
|
+
"@amritk/helpers": "^0.17.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@amritk/generate-markdown": "^0.
|
|
62
|
+
"@amritk/generate-markdown": "^0.5.0",
|
|
63
63
|
"@sinclair/typebox": "^0.34.49",
|
|
64
64
|
"ajv": "^8.17.1",
|
|
65
65
|
"zod": "^4.4.3"
|