@amritk/generate-parsers 0.12.3 → 0.14.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/dist/generators/build-schema.d.ts +6 -1
- package/dist/generators/build-schema.js +30 -5
- package/dist/generators/generate-files.d.ts +8 -0
- package/dist/generators/generate-files.js +12 -5
- package/dist/generators/generate-parser-function.d.ts +9 -1
- package/dist/generators/generate-parser-function.js +346 -192
- package/dist/generators/generate-strict-assertion.d.ts +16 -1
- package/dist/generators/generate-strict-assertion.js +71 -12
- package/dist/generators/generate-type-checks.d.ts +82 -0
- package/dist/generators/generate-type-checks.js +438 -0
- package/dist/generators/generate-validation-expression.d.ts +7 -0
- package/dist/generators/generate-validation-expression.js +17 -5
- package/dist/helpers/collect-helpers.d.ts +1 -9
- package/dist/helpers/collect-helpers.js +36 -9
- package/dist/helpers/collect-imports.d.ts +15 -0
- package/dist/helpers/collect-imports.js +36 -14
- package/dist/index.d.ts +1 -0
- package/package.json +2 -2
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
|
+
/**
|
|
3
|
+
* Context for assertions that reach beyond the property's own schema: union
|
|
4
|
+
* membership checks may call imported `validate{X}Shape` predicates ($ref
|
|
5
|
+
* branches), which requires knowing the ref-import mode, the type-name suffix,
|
|
6
|
+
* and the root document (to prove those validators are real — see
|
|
7
|
+
* canEnforceUnion). `stripUnknown` disables union enforcement entirely, since
|
|
8
|
+
* shape validators then treat undeclared keys as a mismatch while the
|
|
9
|
+
* stripUnknown contract is to drop them.
|
|
10
|
+
*/
|
|
11
|
+
export type StrictAssertionContext = {
|
|
12
|
+
readonly useRefImports?: boolean;
|
|
13
|
+
readonly suffix?: string;
|
|
14
|
+
readonly rootSchema?: Record<string, unknown>;
|
|
15
|
+
readonly stripUnknown?: boolean;
|
|
16
|
+
};
|
|
2
17
|
/**
|
|
3
18
|
* Generates strict-mode assertions for the body of an object parser.
|
|
4
19
|
* Throws on:
|
|
@@ -10,7 +25,7 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
|
10
25
|
* Properties with a `$ref` are validated by the nested parser's own strict
|
|
11
26
|
* check when that parser is invoked downstream.
|
|
12
27
|
*/
|
|
13
|
-
export declare const generateObjectStrictAssertion: (schema: JSONSchema, typeName: string) => string[];
|
|
28
|
+
export declare const generateObjectStrictAssertion: (schema: JSONSchema, typeName: string, context?: StrictAssertionContext) => string[];
|
|
14
29
|
/**
|
|
15
30
|
* Generates a single strict-mode assertion line for a non-object scalar parser.
|
|
16
31
|
* Returns null when the schema has no type information to assert on.
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
|
|
2
2
|
import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
|
|
3
3
|
import { multipleOfFailExpr } from '@amritk/helpers/multiple-of-check';
|
|
4
|
+
import { quoteJsString } from '@amritk/helpers/quote-js-string';
|
|
4
5
|
import { safeAccessor } from '@amritk/helpers/safe-accessor';
|
|
5
|
-
import { hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject, } from '@amritk/helpers/schema-guards';
|
|
6
|
+
import { hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject, } from '@amritk/helpers/schema-guards';
|
|
7
|
+
import { generateEnumCheck } from './generate-enum-check.js';
|
|
8
|
+
import { canEnforceUnion, generateUnionCheck, getUnionBranches } from './generate-type-checks.js';
|
|
9
|
+
import { scalarItemTypeCheck } from './generate-validation-expression.js';
|
|
6
10
|
/**
|
|
7
11
|
* Returns the inline condition that is true when `accessor` is the wrong type
|
|
8
12
|
* for the given JSON Schema primitive type.
|
|
@@ -18,6 +22,11 @@ const wrongTypeCondition = (accessor, type) => {
|
|
|
18
22
|
return `(typeof ${accessor} !== "number" || !Number.isInteger(${accessor}))`;
|
|
19
23
|
case 'boolean':
|
|
20
24
|
return `typeof ${accessor} !== "boolean"`;
|
|
25
|
+
case 'null':
|
|
26
|
+
// Missing from this switch historically, which meant a null-typed
|
|
27
|
+
// property was never enforced on the assertion path — the strict-mode
|
|
28
|
+
// differential fuzzer caught non-null values sailing through.
|
|
29
|
+
return `${accessor} !== null`;
|
|
21
30
|
case 'array':
|
|
22
31
|
return `!Array.isArray(${accessor})`;
|
|
23
32
|
case 'object':
|
|
@@ -33,14 +42,13 @@ const wrongTypeCondition = (accessor, type) => {
|
|
|
33
42
|
const typeLabel = (type) => (type === 'integer' ? 'number' : type);
|
|
34
43
|
/**
|
|
35
44
|
* Emits `throw new Error(<message>[ + <suffixExpr>])`. The static message may
|
|
36
|
-
* contain schema-controlled text (property names, patterns, enum values), so
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* message (e.g. `typeof input`).
|
|
45
|
+
* contain schema-controlled text (property names, patterns, enum values), so
|
|
46
|
+
* it goes through the shared {@link quoteJsString} escape-or-quote decision.
|
|
47
|
+
* `suffixExpr`, when given, is a runtime expression appended to the message
|
|
48
|
+
* (e.g. `typeof input`).
|
|
41
49
|
*/
|
|
42
50
|
const throwError = (message, suffixExpr) => {
|
|
43
|
-
const literal =
|
|
51
|
+
const literal = quoteJsString(message);
|
|
44
52
|
return suffixExpr ? `throw new Error(${literal} + (${suffixExpr}))` : `throw new Error(${literal})`;
|
|
45
53
|
};
|
|
46
54
|
/**
|
|
@@ -94,15 +102,41 @@ const generateConstraintChecks = (acc, propSchema, typeName, key) => {
|
|
|
94
102
|
// a plain `new Set` would only catch primitive duplicates, not equal objects.
|
|
95
103
|
lines.push(` if (Array.isArray(${acc}) && new Set(${acc}.map((_u) => JSON.stringify(_u))).size !== ${acc}.length) ${throwError(`${field} must NOT have duplicate items`)};`);
|
|
96
104
|
}
|
|
105
|
+
// Item types: the fast path proves them via `.every`, but this slow path
|
|
106
|
+
// used to check only length/uniqueness, letting e.g. a number slip into a
|
|
107
|
+
// declared `string[]`. Enforce scalar and enum item schemas here; richer
|
|
108
|
+
// item schemas ($refs, objects) are validated by their own parsers.
|
|
109
|
+
const itemCheck = generateItemCheck(propSchema);
|
|
110
|
+
if (itemCheck) {
|
|
111
|
+
lines.push(` if (Array.isArray(${acc}) && !${acc}.every((_it) => ${itemCheck.check})) ${throwError(`${field} ${itemCheck.message}`)};`);
|
|
112
|
+
}
|
|
97
113
|
}
|
|
98
114
|
return lines;
|
|
99
115
|
};
|
|
116
|
+
/**
|
|
117
|
+
* Boolean per-item check (bound to `_it`) for an array schema's `items`, with
|
|
118
|
+
* the error-message fragment describing what was expected. Only scalar types
|
|
119
|
+
* and enums are checked — returns null for anything richer.
|
|
120
|
+
*/
|
|
121
|
+
const generateItemCheck = (schema) => {
|
|
122
|
+
if (!isSchemaObject(schema) || !hasItems(schema) || Array.isArray(schema.items))
|
|
123
|
+
return null;
|
|
124
|
+
const items = schema.items;
|
|
125
|
+
if (isSchemaObject(items) && hasEnum(items) && items.enum.length > 0) {
|
|
126
|
+
const label = items.enum.map((v) => JSON.stringify(v)).join(', ');
|
|
127
|
+
return { check: generateEnumCheck('_it', items.enum), message: `items must be one of: ${label}` };
|
|
128
|
+
}
|
|
129
|
+
const scalarCheck = scalarItemTypeCheck(items, '_it');
|
|
130
|
+
if (scalarCheck === null || !isSchemaObject(items) || !hasType(items))
|
|
131
|
+
return null;
|
|
132
|
+
return { check: scalarCheck, message: `items expected ${typeLabel(items.type)}` };
|
|
133
|
+
};
|
|
100
134
|
/**
|
|
101
135
|
* Generates strict-mode lines for a single property of an object schema.
|
|
102
136
|
* Properties with a `$ref` are skipped here — the nested parser handles its own
|
|
103
137
|
* strict check when called from the parent's slow path.
|
|
104
138
|
*/
|
|
105
|
-
const generatePropertyAssertion = (key, propSchema, isRequired, typeName) => {
|
|
139
|
+
const generatePropertyAssertion = (key, propSchema, isRequired, typeName, context = {}) => {
|
|
106
140
|
const acc = safeAccessor('input', key);
|
|
107
141
|
const field = `[${typeName}] field '${key}'`;
|
|
108
142
|
const lines = [];
|
|
@@ -113,6 +147,21 @@ const generatePropertyAssertion = (key, propSchema, isRequired, typeName) => {
|
|
|
113
147
|
return lines;
|
|
114
148
|
if (hasRef(propSchema))
|
|
115
149
|
return lines;
|
|
150
|
+
// Union properties: enforce membership when every branch check is
|
|
151
|
+
// false-sound (canEnforceUnion), so a value matching no variant throws
|
|
152
|
+
// instead of passing through untyped. Left unenforced (pass-through, the
|
|
153
|
+
// historical behavior) when any branch is too complex to check safely.
|
|
154
|
+
const unionBranches = getUnionBranches(propSchema);
|
|
155
|
+
if (unionBranches) {
|
|
156
|
+
if (!context.stripUnknown && canEnforceUnion(unionBranches, context.rootSchema)) {
|
|
157
|
+
const check = generateUnionCheck(acc, unionBranches, context.useRefImports ?? false, context.suffix ?? '');
|
|
158
|
+
if (check !== null) {
|
|
159
|
+
const failure = throwError(`${field} does not match any allowed variant`);
|
|
160
|
+
lines.push(isRequired ? ` if (!(${check})) ${failure};` : ` if (${acc} !== undefined && !(${check})) ${failure};`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return lines;
|
|
164
|
+
}
|
|
116
165
|
const instanceOf = getMjstInstanceOf(propSchema);
|
|
117
166
|
if (instanceOf) {
|
|
118
167
|
if (isRequired) {
|
|
@@ -171,14 +220,15 @@ const generatePropertyAssertion = (key, propSchema, isRequired, typeName) => {
|
|
|
171
220
|
* Properties with a `$ref` are validated by the nested parser's own strict
|
|
172
221
|
* check when that parser is invoked downstream.
|
|
173
222
|
*/
|
|
174
|
-
export const generateObjectStrictAssertion = (schema, typeName) => {
|
|
223
|
+
export const generateObjectStrictAssertion = (schema, typeName, context = {}) => {
|
|
175
224
|
const lines = [];
|
|
176
225
|
lines.push(` if (!isObject(input)) ${throwError(`[${typeName}] expected object, got `, 'input === null ? "null" : typeof input')};`);
|
|
177
226
|
if (!hasProperties(schema) || !isSchemaObject(schema))
|
|
178
227
|
return lines;
|
|
179
228
|
const required = new Set(hasRequired(schema) ? schema.required : []);
|
|
180
|
-
|
|
181
|
-
|
|
229
|
+
const props = schema.properties;
|
|
230
|
+
for (const key in props) {
|
|
231
|
+
lines.push(...generatePropertyAssertion(key, props[key], required.has(key), typeName, context));
|
|
182
232
|
}
|
|
183
233
|
return lines;
|
|
184
234
|
};
|
|
@@ -202,5 +252,14 @@ export const generateScalarStrictAssertion = (schema, typeName) => {
|
|
|
202
252
|
const wrongType = wrongTypeCondition('input', t);
|
|
203
253
|
if (!wrongType)
|
|
204
254
|
return null;
|
|
205
|
-
|
|
255
|
+
const lines = [` if (${wrongType}) ${throwError(`[${typeName}] expected ${typeLabel(t)}, got `, got)};`];
|
|
256
|
+
// Root-level arrays enforce scalar/enum item types too — the same gap the
|
|
257
|
+
// property path closes in generateConstraintChecks.
|
|
258
|
+
if (t === 'array') {
|
|
259
|
+
const itemCheck = generateItemCheck(schema);
|
|
260
|
+
if (itemCheck) {
|
|
261
|
+
lines.push(` if (!(input as readonly unknown[]).every((_it) => ${itemCheck.check})) ${throwError(`[${typeName}] ${itemCheck.message}`)};`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return lines.join('\n');
|
|
206
265
|
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
|
+
/**
|
|
3
|
+
* Boolean type-check expression builders shared by the shape validators, the
|
|
4
|
+
* parsers' fast paths, and the strict-mode assertions. They live in their own
|
|
5
|
+
* module (rather than generate-parser-function) so generate-strict-assertion
|
|
6
|
+
* can use them without an import cycle.
|
|
7
|
+
*
|
|
8
|
+
* Every expression built here is *true-sound*: when it evaluates to true the
|
|
9
|
+
* value definitely matches the schema fragment. That is all a fast path needs.
|
|
10
|
+
* The reverse direction — false definitely means invalid — only holds when
|
|
11
|
+
* {@link canEnforceUnion} approves the schema, because a check may call an
|
|
12
|
+
* imported `validate{X}Shape` that was generated as a conservative `=> false`
|
|
13
|
+
* stub. Strict-mode code that *throws* on a false check must gate on it.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Returns the shape-predicate function name for a given type name.
|
|
17
|
+
* The predicate is generated alongside each parser and tells callers
|
|
18
|
+
* whether `input` is already in the shape produced by the parser's fast path.
|
|
19
|
+
*/
|
|
20
|
+
export declare const shapeValidatorName: (typeName: string) => string;
|
|
21
|
+
/**
|
|
22
|
+
* Matches an inline nested object property — an object schema written directly
|
|
23
|
+
* under `properties` with its own `properties`, rather than referenced via
|
|
24
|
+
* `$ref`. These get a private sub-parser (and shape predicate) in the same
|
|
25
|
+
* generated file so their fields are actually parsed; anything involving
|
|
26
|
+
* composition, conditionals, enums, or record semantics keeps the existing
|
|
27
|
+
* code paths.
|
|
28
|
+
*/
|
|
29
|
+
export declare const isInlineObjectProperty: (propSchema: JSONSchema) => propSchema is JSONSchema.Object;
|
|
30
|
+
/**
|
|
31
|
+
* Matches an array property whose `items` is an inline object schema — the
|
|
32
|
+
* array-items analogue of {@link isInlineObjectProperty}. These get a private
|
|
33
|
+
* item sub-parser (and shape predicate) so element values — including nested
|
|
34
|
+
* enums and `$ref`s — are actually validated instead of only passing an
|
|
35
|
+
* `Array.isArray` check.
|
|
36
|
+
*/
|
|
37
|
+
export declare const isInlineObjectArrayProperty: (propSchema: JSONSchema) => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Extracts the branch list of a `oneOf`/`anyOf` union, or `null` when the
|
|
40
|
+
* schema is not a union (or mixes in other composition keywords we cannot
|
|
41
|
+
* turn into a membership check).
|
|
42
|
+
*/
|
|
43
|
+
export declare const getUnionBranches: (schema: JSONSchema) => readonly JSONSchema[] | null;
|
|
44
|
+
/**
|
|
45
|
+
* Generates a fast-path type check expression for a property.
|
|
46
|
+
* Returns null if the schema is too complex for a simple type check.
|
|
47
|
+
*
|
|
48
|
+
* When `useRefImports` is true, $ref properties (and arrays of $refs) are
|
|
49
|
+
* checked by calling the imported shape predicate, allowing parent parsers
|
|
50
|
+
* to fast-path through nested ref types without recursing into their parsers.
|
|
51
|
+
*/
|
|
52
|
+
export declare const generatePropertyTypeCheck: (varName: string, schema: JSONSchema, useRefImports: boolean, suffix: string) => string | null;
|
|
53
|
+
/**
|
|
54
|
+
* Generates a boolean check that `varName` matches an inline object schema —
|
|
55
|
+
* `isObject` plus a per-property check for every declared property (and a
|
|
56
|
+
* presence check for `required` keys without a declared schema). Used for
|
|
57
|
+
* union branches written inline, where no named shape validator exists to
|
|
58
|
+
* call. Returns null when any property is too complex to check.
|
|
59
|
+
*/
|
|
60
|
+
export declare const generateInlineObjectCheck: (varName: string, schema: JSONSchema, useRefImports: boolean, suffix: string) => string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Generates a union membership check: true when `varName` matches at least one
|
|
63
|
+
* branch of a `oneOf`/`anyOf`. Inline object branches get a per-property check;
|
|
64
|
+
* everything else goes through {@link generatePropertyTypeCheck} ($refs call
|
|
65
|
+
* the imported shape validator, consts/enums/scalars check directly). Returns
|
|
66
|
+
* null when any branch cannot be checked.
|
|
67
|
+
*/
|
|
68
|
+
export declare const generateUnionCheck: (varName: string, branches: readonly JSONSchema[], useRefImports: boolean, suffix: string) => string | null;
|
|
69
|
+
/**
|
|
70
|
+
* True when the union membership check emitted by {@link generateUnionCheck}
|
|
71
|
+
* is also *false-sound* — a false result definitely means the value is invalid
|
|
72
|
+
* — so strict mode may throw on it.
|
|
73
|
+
*
|
|
74
|
+
* The risk is `$ref`s: a branch (or a property inside one) whose check calls
|
|
75
|
+
* an imported `validate{X}Shape` is only trustworthy when that validator was
|
|
76
|
+
* generated as a real predicate. This walks every reachable ref (mirroring
|
|
77
|
+
* generateShapeValidator's stub conditions) and rejects the union when any of
|
|
78
|
+
* them would have produced the conservative `=> false` stub, or is itself
|
|
79
|
+
* built on one. Cycles (e.g. a recursive `expr` union) are assumed sound —
|
|
80
|
+
* the recursion bottoms out on the data, not the schema.
|
|
81
|
+
*/
|
|
82
|
+
export declare const canEnforceUnion: (branches: readonly JSONSchema[], rootSchema: Record<string, unknown> | undefined) => boolean;
|