@amritk/generate-parsers 0.19.4 → 0.19.5
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 +9 -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 +38 -39
- 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,23 @@ 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
313
|
|
|
314
314
|
The trade is a one-shot **prepare** cost that only mjst pays — generating the
|
|
315
|
-
parser source — which measures **~0.
|
|
315
|
+
parser source — which measures **~0.2–0.8 ms** per schema here (zod and TypeBox
|
|
316
316
|
author or interpret their parsers with no separate build step, so there is
|
|
317
317
|
nothing to time). That is trivially amortized: you generate once at build time
|
|
318
318
|
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,7 +548,7 @@ 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
|
};
|
|
558
554
|
const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema, reservedNames = NO_RESERVED_NAMES, caseInsensitive = false, shapeValidatorSource) => {
|
|
@@ -592,9 +588,11 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
592
588
|
}
|
|
593
589
|
const strictKeys = hasStrictKeys(schema);
|
|
594
590
|
const stripKeys = strictKeys || stripUnknown;
|
|
595
|
-
const
|
|
591
|
+
const rejectsUnknownKeys = strict && isSchemaObject(schema) && hasAdditionalProperties(schema) && schema.additionalProperties === false && !("patternProperties" in schema) || false;
|
|
592
|
+
const guardKeys = stripKeys || rejectsUnknownKeys;
|
|
593
|
+
const exactKeyCount = guardKeys ? exactKeyCountOf(schema) : null;
|
|
596
594
|
const strictKeyCheck = unknownKeyCheck(propertyKeys, `_knownKeys${typeName}`);
|
|
597
|
-
if (
|
|
595
|
+
if (guardKeys && exactKeyCount === null) {
|
|
598
596
|
for (const declaration of strictKeyCheck.declarations) {
|
|
599
597
|
preamble.push(`${declaration};`);
|
|
600
598
|
}
|
|
@@ -602,7 +600,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
602
600
|
for (const _k in input) if (${strictKeyCheck.isUnknown("_k")}) return false;
|
|
603
601
|
return true;
|
|
604
602
|
};`);
|
|
605
|
-
} else if (
|
|
603
|
+
} else if (rejectsUnknownKeys) {
|
|
606
604
|
for (const declaration of strictKeyCheck.declarations) {
|
|
607
605
|
preamble.push(`${declaration};`);
|
|
608
606
|
}
|
|
@@ -661,7 +659,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
661
659
|
fastPathAccessorChecks.push(`Object.keys(input).length <= ${schema.maxProperties}`);
|
|
662
660
|
}
|
|
663
661
|
}
|
|
664
|
-
if (
|
|
662
|
+
if (guardKeys) {
|
|
665
663
|
fastPathChecks.push(exactKeyCount !== null ? `Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${exactKeyCount}` : `_hasOnlyKnownKeys${typeName}(input)`);
|
|
666
664
|
}
|
|
667
665
|
const deepGuard = canFastPath && fastPathChecks.length > 0 ? fastPathChecks.join(" && ") : null;
|
|
@@ -710,7 +708,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
710
708
|
const shallowGuard = canShallowGuard && shallowChecks.length > 0 ? shallowChecks.join(" && ") : null;
|
|
711
709
|
const varDeclLines = [];
|
|
712
710
|
for (const { key, varName, propSchema } of propInfo) {
|
|
713
|
-
if (shouldCacheVariable(propSchema
|
|
711
|
+
if (shouldCacheVariable(propSchema)) {
|
|
714
712
|
varDeclLines.push(` const ${varName} = ${safeAccessor("input", key)};`);
|
|
715
713
|
}
|
|
716
714
|
}
|
|
@@ -740,7 +738,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
740
738
|
}
|
|
741
739
|
}
|
|
742
740
|
for (const { key, varName, isRequired, propSchema } of propInfo) {
|
|
743
|
-
const shouldCache = shouldCacheVariable(propSchema
|
|
741
|
+
const shouldCache = shouldCacheVariable(propSchema);
|
|
744
742
|
const accessor = shouldCache ? varName : safeAccessor("input", key);
|
|
745
743
|
const subName = subTypeNames.get(key);
|
|
746
744
|
if (subName) {
|
|
@@ -848,12 +846,12 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
848
846
|
}
|
|
849
847
|
continue;
|
|
850
848
|
}
|
|
851
|
-
const accessor = shouldCacheVariable(propSchema
|
|
849
|
+
const accessor = shouldCacheVariable(propSchema) ? varName : safeAccessor("input", key);
|
|
852
850
|
fields.push(isRequired ? ` ${safeLiteralKey(key)}: ${accessor},` : ` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${accessor} }),`);
|
|
853
851
|
}
|
|
854
852
|
lines2.push(` if (${deepGuardExpr}) return {`);
|
|
855
853
|
lines2.push(fields.join("\n"));
|
|
856
|
-
lines2.push(` } as ${typeName};`);
|
|
854
|
+
lines2.push(` } as unknown as ${typeName};`);
|
|
857
855
|
};
|
|
858
856
|
const lines = [];
|
|
859
857
|
lines.push(`${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {`);
|
|
@@ -903,7 +901,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
903
901
|
for (const assertionLine of assertionLines) {
|
|
904
902
|
lines.push(assertionLine);
|
|
905
903
|
}
|
|
906
|
-
if (
|
|
904
|
+
if (rejectsUnknownKeys) {
|
|
907
905
|
lines.push(` for (const _k in input) {`);
|
|
908
906
|
lines.push(` if (${strictKeyCheck.isUnknown("_k")}) throw new Error(\`[${typeName}] unknown property "\${_k}"\`);`);
|
|
909
907
|
lines.push(` }`);
|
|
@@ -1280,6 +1278,7 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1280
1278
|
stripUnknown,
|
|
1281
1279
|
...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {}
|
|
1282
1280
|
};
|
|
1281
|
+
const copyLoopContext = { ...context, subParsers: false };
|
|
1283
1282
|
if (!options?.isRoot && typeName === `Schema${suffix}`) {
|
|
1284
1283
|
return generateSchemaObjectParser(typeName);
|
|
1285
1284
|
}
|
|
@@ -1296,7 +1295,7 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1296
1295
|
});
|
|
1297
1296
|
}
|
|
1298
1297
|
if (hasProperties(schema) && isSchemaObject(schema) && "patternProperties" in schema) {
|
|
1299
|
-
return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown, caseInsensitive,
|
|
1298
|
+
return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown, caseInsensitive, copyLoopContext);
|
|
1300
1299
|
}
|
|
1301
1300
|
if (strict && isSchemaObject(schema) && Array.isArray(schema.type) && schema.type.includes("null")) {
|
|
1302
1301
|
return generateNonObjectParser(typeName, schema, strict, {
|
|
@@ -1342,14 +1341,14 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1342
1341
|
});
|
|
1343
1342
|
}
|
|
1344
1343
|
if ("patternProperties" in schema) {
|
|
1345
|
-
return generatePatternPropertiesParser(schema, typeName, useRefImports, suffix, strict,
|
|
1344
|
+
return generatePatternPropertiesParser(schema, typeName, useRefImports, suffix, strict, copyLoopContext);
|
|
1346
1345
|
}
|
|
1347
1346
|
if ("additionalProperties" in schema && !hasProperties(schema)) {
|
|
1348
1347
|
const additionalProps = schema.additionalProperties;
|
|
1349
1348
|
if (additionalProps === true || additionalProps === false) {
|
|
1350
1349
|
return generateEmptyObjectParser(typeName, strict, schema, context);
|
|
1351
1350
|
}
|
|
1352
|
-
return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict,
|
|
1351
|
+
return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict, copyLoopContext);
|
|
1353
1352
|
}
|
|
1354
1353
|
if ("type" in schema && schema.type === "object" && !hasProperties(schema)) {
|
|
1355
1354
|
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.19.
|
|
3
|
+
"version": "0.19.5",
|
|
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.16.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"
|