@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
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
|
|
2
|
+
import { multipleOfPassExpr } from '@amritk/helpers/multiple-of-check';
|
|
3
|
+
import { refToName } from '@amritk/helpers/ref-to-name';
|
|
4
|
+
import { resolveRef } from '@amritk/helpers/resolve-ref';
|
|
5
|
+
import { safeAccessor } from '@amritk/helpers/safe-accessor';
|
|
6
|
+
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasType, hasUniqueItems, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
|
|
7
|
+
import { generateEnumCheck } from './generate-enum-check.js';
|
|
8
|
+
import { scalarItemTypeCheck } from './generate-validation-expression.js';
|
|
9
|
+
/**
|
|
10
|
+
* Boolean type-check expression builders shared by the shape validators, the
|
|
11
|
+
* parsers' fast paths, and the strict-mode assertions. They live in their own
|
|
12
|
+
* module (rather than generate-parser-function) so generate-strict-assertion
|
|
13
|
+
* can use them without an import cycle.
|
|
14
|
+
*
|
|
15
|
+
* Every expression built here is *true-sound*: when it evaluates to true the
|
|
16
|
+
* value definitely matches the schema fragment. That is all a fast path needs.
|
|
17
|
+
* The reverse direction — false definitely means invalid — only holds when
|
|
18
|
+
* {@link canEnforceUnion} approves the schema, because a check may call an
|
|
19
|
+
* imported `validate{X}Shape` that was generated as a conservative `=> false`
|
|
20
|
+
* stub. Strict-mode code that *throws* on a false check must gate on it.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Returns the shape-predicate function name for a given type name.
|
|
24
|
+
* The predicate is generated alongside each parser and tells callers
|
|
25
|
+
* whether `input` is already in the shape produced by the parser's fast path.
|
|
26
|
+
*/
|
|
27
|
+
export const shapeValidatorName = (typeName) => `validate${typeName}Shape`;
|
|
28
|
+
/**
|
|
29
|
+
* Matches an inline nested object property — an object schema written directly
|
|
30
|
+
* under `properties` with its own `properties`, rather than referenced via
|
|
31
|
+
* `$ref`. These get a private sub-parser (and shape predicate) in the same
|
|
32
|
+
* generated file so their fields are actually parsed; anything involving
|
|
33
|
+
* composition, conditionals, enums, or record semantics keeps the existing
|
|
34
|
+
* code paths.
|
|
35
|
+
*/
|
|
36
|
+
export const isInlineObjectProperty = (propSchema) => {
|
|
37
|
+
if (!isSchemaObject(propSchema))
|
|
38
|
+
return false;
|
|
39
|
+
if (hasRef(propSchema) || hasEnum(propSchema) || hasConst(propSchema))
|
|
40
|
+
return false;
|
|
41
|
+
if (hasOneOf(propSchema) || hasAnyOf(propSchema) || hasAllOf(propSchema))
|
|
42
|
+
return false;
|
|
43
|
+
// `then`/`else` must be excluded even without `if`: generateShapeValidator
|
|
44
|
+
// stubs any schema carrying them, so admitting one here would wire an
|
|
45
|
+
// always-false predicate into parent guards — and into validators the
|
|
46
|
+
// strict-union trust walk treats as false-sound, making a strict union
|
|
47
|
+
// reject valid input.
|
|
48
|
+
if ('patternProperties' in propSchema || 'not' in propSchema)
|
|
49
|
+
return false;
|
|
50
|
+
if ('if' in propSchema || 'then' in propSchema || 'else' in propSchema)
|
|
51
|
+
return false;
|
|
52
|
+
// additionalProperties-as-schema records go through the record paths instead
|
|
53
|
+
if (hasAdditionalProperties(propSchema) && typeof propSchema.additionalProperties !== 'boolean')
|
|
54
|
+
return false;
|
|
55
|
+
return isObjectSchema(propSchema) && hasProperties(propSchema);
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Matches an array property whose `items` is an inline object schema — the
|
|
59
|
+
* array-items analogue of {@link isInlineObjectProperty}. These get a private
|
|
60
|
+
* item sub-parser (and shape predicate) so element values — including nested
|
|
61
|
+
* enums and `$ref`s — are actually validated instead of only passing an
|
|
62
|
+
* `Array.isArray` check.
|
|
63
|
+
*/
|
|
64
|
+
export const isInlineObjectArrayProperty = (propSchema) => {
|
|
65
|
+
if (!isSchemaObject(propSchema))
|
|
66
|
+
return false;
|
|
67
|
+
if (!('type' in propSchema) || propSchema.type !== 'array')
|
|
68
|
+
return false;
|
|
69
|
+
if (!hasItems(propSchema) || Array.isArray(propSchema.items))
|
|
70
|
+
return false;
|
|
71
|
+
return isInlineObjectProperty(propSchema.items);
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Extracts the branch list of a `oneOf`/`anyOf` union, or `null` when the
|
|
75
|
+
* schema is not a union (or mixes in other composition keywords we cannot
|
|
76
|
+
* turn into a membership check).
|
|
77
|
+
*/
|
|
78
|
+
export const getUnionBranches = (schema) => {
|
|
79
|
+
if (!isSchemaObject(schema))
|
|
80
|
+
return null;
|
|
81
|
+
if (hasAllOf(schema) || 'not' in schema)
|
|
82
|
+
return null;
|
|
83
|
+
if (hasOneOf(schema) && schema.oneOf.length > 0)
|
|
84
|
+
return schema.oneOf;
|
|
85
|
+
if (hasAnyOf(schema) && schema.anyOf.length > 0)
|
|
86
|
+
return schema.anyOf;
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Generates a fast-path type check expression for a property.
|
|
91
|
+
* Returns null if the schema is too complex for a simple type check.
|
|
92
|
+
*
|
|
93
|
+
* When `useRefImports` is true, $ref properties (and arrays of $refs) are
|
|
94
|
+
* checked by calling the imported shape predicate, allowing parent parsers
|
|
95
|
+
* to fast-path through nested ref types without recursing into their parsers.
|
|
96
|
+
*/
|
|
97
|
+
export const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
|
|
98
|
+
if (!isSchemaObject(schema))
|
|
99
|
+
return null;
|
|
100
|
+
// $ref via shape predicate (deep fast-path through nested types).
|
|
101
|
+
if (useRefImports && hasRef(schema)) {
|
|
102
|
+
const refName = refToName(schema.$ref, suffix);
|
|
103
|
+
return `${shapeValidatorName(refName)}(${varName})`;
|
|
104
|
+
}
|
|
105
|
+
if (hasRef(schema))
|
|
106
|
+
return null;
|
|
107
|
+
// A fixed member set is directly predicable regardless of `type` — this is
|
|
108
|
+
// what lets a nested object carrying an enum property (e.g. `axiom.kind`)
|
|
109
|
+
// keep a real shape validator instead of the `=> false` stub.
|
|
110
|
+
if (hasEnum(schema)) {
|
|
111
|
+
return schema.enum.length > 0 ? generateEnumCheck(varName, schema.enum) : null;
|
|
112
|
+
}
|
|
113
|
+
// A union property is predicable when every branch is: membership is the
|
|
114
|
+
// disjunction of the branch checks.
|
|
115
|
+
const branches = getUnionBranches(schema);
|
|
116
|
+
if (branches) {
|
|
117
|
+
return generateUnionCheck(varName, branches, useRefImports, suffix);
|
|
118
|
+
}
|
|
119
|
+
// Remaining composition (allOf, not, or a union mixed with them) cannot be
|
|
120
|
+
// expressed as a simple boolean check.
|
|
121
|
+
if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || 'not' in schema) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
// A `const` property (the usual discriminated-union tag, e.g. `kind: "lit"`) has
|
|
125
|
+
// no `type`, but is fast-path predicable via strict equality. Handling it here is
|
|
126
|
+
// what lets a discriminated branch's shape validator be a real predicate instead
|
|
127
|
+
// of the `=> false` stub. Structural consts need deep equality, so skip those.
|
|
128
|
+
if (hasConst(schema)) {
|
|
129
|
+
const c = schema.const;
|
|
130
|
+
return c === null || typeof c !== 'object' ? `${varName} === ${JSON.stringify(c)}` : null;
|
|
131
|
+
}
|
|
132
|
+
if (!hasType(schema))
|
|
133
|
+
return null;
|
|
134
|
+
// Array of $refs: each item must satisfy the imported shape predicate.
|
|
135
|
+
if (useRefImports &&
|
|
136
|
+
schema.type === 'array' &&
|
|
137
|
+
hasItems(schema) &&
|
|
138
|
+
isSchemaObject(schema.items) &&
|
|
139
|
+
hasRef(schema.items)) {
|
|
140
|
+
const refName = refToName(schema.items.$ref, suffix);
|
|
141
|
+
return `Array.isArray(${varName}) && ${varName}.every(${shapeValidatorName(refName)})`;
|
|
142
|
+
}
|
|
143
|
+
const checks = [];
|
|
144
|
+
switch (schema.type) {
|
|
145
|
+
case 'string': {
|
|
146
|
+
checks.push(`typeof ${varName} === "string"`);
|
|
147
|
+
if (hasPattern(schema))
|
|
148
|
+
checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${varName})`);
|
|
149
|
+
if (hasMinLength(schema))
|
|
150
|
+
checks.push(`${varName}.length >= ${schema.minLength}`);
|
|
151
|
+
if (hasMaxLength(schema))
|
|
152
|
+
checks.push(`${varName}.length <= ${schema.maxLength}`);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'number':
|
|
156
|
+
case 'integer': {
|
|
157
|
+
checks.push(`typeof ${varName} === "number"`);
|
|
158
|
+
// `integer` must reject non-integral numbers on the fast path too, otherwise
|
|
159
|
+
// `1.5` would pass through uncoerced.
|
|
160
|
+
if (schema.type === 'integer')
|
|
161
|
+
checks.push(`Number.isInteger(${varName})`);
|
|
162
|
+
if (hasMinimum(schema))
|
|
163
|
+
checks.push(`${varName} >= ${schema.minimum}`);
|
|
164
|
+
if (hasMaximum(schema))
|
|
165
|
+
checks.push(`${varName} <= ${schema.maximum}`);
|
|
166
|
+
if (hasExclusiveMinimum(schema))
|
|
167
|
+
checks.push(`${varName} > ${schema.exclusiveMinimum}`);
|
|
168
|
+
if (hasExclusiveMaximum(schema))
|
|
169
|
+
checks.push(`${varName} < ${schema.exclusiveMaximum}`);
|
|
170
|
+
if (hasMultipleOf(schema))
|
|
171
|
+
checks.push(multipleOfPassExpr(varName, schema.multipleOf));
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case 'boolean':
|
|
175
|
+
checks.push(`typeof ${varName} === "boolean"`);
|
|
176
|
+
break;
|
|
177
|
+
case 'array': {
|
|
178
|
+
checks.push(`Array.isArray(${varName})`);
|
|
179
|
+
if (hasMinItems(schema))
|
|
180
|
+
checks.push(`${varName}.length >= ${schema.minItems}`);
|
|
181
|
+
if (hasMaxItems(schema))
|
|
182
|
+
checks.push(`${varName}.length <= ${schema.maxItems}`);
|
|
183
|
+
if (hasUniqueItems(schema) && schema.uniqueItems === true) {
|
|
184
|
+
checks.push(`new Set(${varName}).size === ${varName}.length`);
|
|
185
|
+
}
|
|
186
|
+
// For a scalar or enum item type, every element must already be well-typed
|
|
187
|
+
// to take the fast path; a mismatched element routes the array to the slow
|
|
188
|
+
// path where each element is coerced (or, in strict mode, throws).
|
|
189
|
+
if (hasItems(schema) && !Array.isArray(schema.items)) {
|
|
190
|
+
const items = schema.items;
|
|
191
|
+
const itemCheck = scalarItemTypeCheck(items, '_it') ??
|
|
192
|
+
(isSchemaObject(items) && hasEnum(items) && items.enum.length > 0
|
|
193
|
+
? generateEnumCheck('_it', items.enum)
|
|
194
|
+
: null);
|
|
195
|
+
if (itemCheck)
|
|
196
|
+
checks.push(`${varName}.every((_it) => ${itemCheck})`);
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case 'object':
|
|
201
|
+
checks.push(`isObject(${varName})`);
|
|
202
|
+
break;
|
|
203
|
+
default:
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
if (checks.length === 0)
|
|
207
|
+
return null;
|
|
208
|
+
// checks[0] is safe: we guard with checks.length === 0 above
|
|
209
|
+
let result = checks[0];
|
|
210
|
+
for (let i = 1; i < checks.length; i++) {
|
|
211
|
+
result += ' && ' + checks[i];
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Generates a boolean check that `varName` matches an inline object schema —
|
|
217
|
+
* `isObject` plus a per-property check for every declared property (and a
|
|
218
|
+
* presence check for `required` keys without a declared schema). Used for
|
|
219
|
+
* union branches written inline, where no named shape validator exists to
|
|
220
|
+
* call. Returns null when any property is too complex to check.
|
|
221
|
+
*/
|
|
222
|
+
export const generateInlineObjectCheck = (varName, schema, useRefImports, suffix) => {
|
|
223
|
+
if (!isSchemaObject(schema) || !isObjectSchema(schema) || !hasProperties(schema))
|
|
224
|
+
return null;
|
|
225
|
+
if (hasAllOf(schema) || hasOneOf(schema) || hasAnyOf(schema) || 'not' in schema)
|
|
226
|
+
return null;
|
|
227
|
+
if ('patternProperties' in schema || 'if' in schema)
|
|
228
|
+
return null;
|
|
229
|
+
// additionalProperties-as-schema (records) would leave the record values
|
|
230
|
+
// unchecked, breaking the "true means valid" guarantee.
|
|
231
|
+
if (hasAdditionalProperties(schema) && typeof schema.additionalProperties !== 'boolean')
|
|
232
|
+
return null;
|
|
233
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
234
|
+
const checks = [`isObject(${varName})`];
|
|
235
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
236
|
+
const acc = safeAccessor(varName, key);
|
|
237
|
+
const check = generatePropertyTypeCheck(acc, propSchema, useRefImports, suffix);
|
|
238
|
+
if (check === null)
|
|
239
|
+
return null;
|
|
240
|
+
checks.push(required.has(key) ? check : `(${acc} === undefined || ${check})`);
|
|
241
|
+
required.delete(key);
|
|
242
|
+
}
|
|
243
|
+
// `required` keys without a declared property schema still need a presence check.
|
|
244
|
+
for (const key of required) {
|
|
245
|
+
checks.push(`${JSON.stringify(key)} in ${varName}`);
|
|
246
|
+
}
|
|
247
|
+
// additionalProperties: false — extras make the value invalid for this branch.
|
|
248
|
+
if (hasAdditionalProperties(schema) && schema.additionalProperties === false) {
|
|
249
|
+
const allowedKeys = JSON.stringify(Object.keys(schema.properties));
|
|
250
|
+
checks.push(`Object.keys(${varName}).every((_k) => ${allowedKeys}.includes(_k))`);
|
|
251
|
+
}
|
|
252
|
+
return checks.join(' && ');
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* Generates a union membership check: true when `varName` matches at least one
|
|
256
|
+
* branch of a `oneOf`/`anyOf`. Inline object branches get a per-property check;
|
|
257
|
+
* everything else goes through {@link generatePropertyTypeCheck} ($refs call
|
|
258
|
+
* the imported shape validator, consts/enums/scalars check directly). Returns
|
|
259
|
+
* null when any branch cannot be checked.
|
|
260
|
+
*/
|
|
261
|
+
export const generateUnionCheck = (varName, branches, useRefImports, suffix) => {
|
|
262
|
+
if (branches.length === 0)
|
|
263
|
+
return null;
|
|
264
|
+
const parts = [];
|
|
265
|
+
for (const branch of branches) {
|
|
266
|
+
if (!isSchemaObject(branch))
|
|
267
|
+
return null;
|
|
268
|
+
const check = isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
|
|
269
|
+
? generateInlineObjectCheck(varName, branch, useRefImports, suffix)
|
|
270
|
+
: generatePropertyTypeCheck(varName, branch, useRefImports, suffix);
|
|
271
|
+
if (check === null)
|
|
272
|
+
return null;
|
|
273
|
+
parts.push(`(${check})`);
|
|
274
|
+
}
|
|
275
|
+
// Callers conjoin this with other checks, so a multi-branch disjunction must
|
|
276
|
+
// carry its own parentheses — `a || b && c` binds the wrong way without them.
|
|
277
|
+
return parts.length === 1 ? parts[0] : `(${parts.join(' || ')})`;
|
|
278
|
+
};
|
|
279
|
+
/**
|
|
280
|
+
* True when the union membership check emitted by {@link generateUnionCheck}
|
|
281
|
+
* is also *false-sound* — a false result definitely means the value is invalid
|
|
282
|
+
* — so strict mode may throw on it.
|
|
283
|
+
*
|
|
284
|
+
* The risk is `$ref`s: a branch (or a property inside one) whose check calls
|
|
285
|
+
* an imported `validate{X}Shape` is only trustworthy when that validator was
|
|
286
|
+
* generated as a real predicate. This walks every reachable ref (mirroring
|
|
287
|
+
* generateShapeValidator's stub conditions) and rejects the union when any of
|
|
288
|
+
* them would have produced the conservative `=> false` stub, or is itself
|
|
289
|
+
* built on one. Cycles (e.g. a recursive `expr` union) are assumed sound —
|
|
290
|
+
* the recursion bottoms out on the data, not the schema.
|
|
291
|
+
*/
|
|
292
|
+
export const canEnforceUnion = (branches, rootSchema) => {
|
|
293
|
+
const visiting = new Set();
|
|
294
|
+
return (branches.length > 0 &&
|
|
295
|
+
branches.every((branch) => {
|
|
296
|
+
if (!isSchemaObject(branch))
|
|
297
|
+
return false;
|
|
298
|
+
return isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
|
|
299
|
+
? canTrustInlineObjectCheck(branch, rootSchema, visiting)
|
|
300
|
+
: canTrustPropertyCheck(branch, rootSchema, visiting);
|
|
301
|
+
}));
|
|
302
|
+
};
|
|
303
|
+
/** Mirrors {@link generatePropertyTypeCheck}: non-null AND every embedded validator call is real. */
|
|
304
|
+
const canTrustPropertyCheck = (schema, rootSchema, visiting) => {
|
|
305
|
+
if (!isSchemaObject(schema))
|
|
306
|
+
return false;
|
|
307
|
+
if (hasRef(schema)) {
|
|
308
|
+
return canTrustReferencedValidator(schema.$ref, rootSchema, visiting);
|
|
309
|
+
}
|
|
310
|
+
if (hasEnum(schema))
|
|
311
|
+
return schema.enum.length > 0;
|
|
312
|
+
const branches = getUnionBranches(schema);
|
|
313
|
+
if (branches) {
|
|
314
|
+
return branches.every((branch) => {
|
|
315
|
+
if (!isSchemaObject(branch))
|
|
316
|
+
return false;
|
|
317
|
+
return isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
|
|
318
|
+
? canTrustInlineObjectCheck(branch, rootSchema, visiting)
|
|
319
|
+
: canTrustPropertyCheck(branch, rootSchema, visiting);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || 'not' in schema)
|
|
323
|
+
return false;
|
|
324
|
+
if (hasConst(schema))
|
|
325
|
+
return schema.const === null || typeof schema.const !== 'object';
|
|
326
|
+
if (!hasType(schema))
|
|
327
|
+
return false;
|
|
328
|
+
if (schema.type === 'array' && hasItems(schema) && isSchemaObject(schema.items) && hasRef(schema.items)) {
|
|
329
|
+
return canTrustReferencedValidator(schema.items.$ref, rootSchema, visiting);
|
|
330
|
+
}
|
|
331
|
+
switch (schema.type) {
|
|
332
|
+
case 'string':
|
|
333
|
+
case 'number':
|
|
334
|
+
case 'integer':
|
|
335
|
+
case 'boolean':
|
|
336
|
+
case 'array':
|
|
337
|
+
case 'object':
|
|
338
|
+
return true;
|
|
339
|
+
default:
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* Mirrors the per-property check {@link generateShapeValidator} emits for a
|
|
345
|
+
* generated file's exported validator, which is *deeper* than
|
|
346
|
+
* {@link generatePropertyTypeCheck}: inline object properties and arrays of
|
|
347
|
+
* inline object items are checked through private sub-predicates generated
|
|
348
|
+
* from the same schema, and a sub-predicate is a conservative `=> false` stub
|
|
349
|
+
* whenever some nested property check cannot be built. A validator built on a
|
|
350
|
+
* stub returns false on *valid* input, so trusting it requires every reachable
|
|
351
|
+
* sub-predicate to be a real check, recursively.
|
|
352
|
+
*/
|
|
353
|
+
const canTrustShapeProperty = (propSchema, rootSchema, visiting) => {
|
|
354
|
+
if (!isSchemaObject(propSchema))
|
|
355
|
+
return false;
|
|
356
|
+
// Record-valued additionalProperties: records of refs stub the validator;
|
|
357
|
+
// stay conservative for any schema-valued record, matching the historical
|
|
358
|
+
// bail in canTrustReferencedValidator.
|
|
359
|
+
if (hasAdditionalProperties(propSchema) && typeof propSchema.additionalProperties !== 'boolean')
|
|
360
|
+
return false;
|
|
361
|
+
if (isInlineObjectProperty(propSchema)) {
|
|
362
|
+
return Object.values(propSchema.properties ?? {}).every((sub) => canTrustShapeProperty(sub, rootSchema, visiting));
|
|
363
|
+
}
|
|
364
|
+
if (isInlineObjectArrayProperty(propSchema)) {
|
|
365
|
+
const items = propSchema.items;
|
|
366
|
+
return Object.values(items.properties ?? {}).every((sub) => canTrustShapeProperty(sub, rootSchema, visiting));
|
|
367
|
+
}
|
|
368
|
+
return canTrustPropertyCheck(propSchema, rootSchema, visiting);
|
|
369
|
+
};
|
|
370
|
+
/** Mirrors {@link generateInlineObjectCheck}: non-null AND every property check is trustworthy. */
|
|
371
|
+
const canTrustInlineObjectCheck = (schema, rootSchema, visiting) => {
|
|
372
|
+
if (!isSchemaObject(schema) || !isObjectSchema(schema) || !hasProperties(schema))
|
|
373
|
+
return false;
|
|
374
|
+
if (hasAllOf(schema) || hasOneOf(schema) || hasAnyOf(schema) || 'not' in schema)
|
|
375
|
+
return false;
|
|
376
|
+
if ('patternProperties' in schema || 'if' in schema)
|
|
377
|
+
return false;
|
|
378
|
+
if (hasAdditionalProperties(schema) && typeof schema.additionalProperties !== 'boolean')
|
|
379
|
+
return false;
|
|
380
|
+
return Object.values(schema.properties).every((propSchema) => canTrustPropertyCheck(propSchema, rootSchema, visiting));
|
|
381
|
+
};
|
|
382
|
+
/**
|
|
383
|
+
* True when the file generated for `ref`'s target carries a *real* (non-stub)
|
|
384
|
+
* shape validator whose result can be trusted in both directions. Mirrors the
|
|
385
|
+
* schema classes generateShapeValidator (and its new union/alias paths) turns
|
|
386
|
+
* into real predicates, recursing into every reachable ref.
|
|
387
|
+
*/
|
|
388
|
+
const canTrustReferencedValidator = (ref, rootSchema, visiting) => {
|
|
389
|
+
if (visiting.has(ref))
|
|
390
|
+
return true;
|
|
391
|
+
if (!rootSchema)
|
|
392
|
+
return false;
|
|
393
|
+
const resolved = resolveRef(ref, rootSchema);
|
|
394
|
+
if (!resolved || !isSchemaObject(resolved))
|
|
395
|
+
return false;
|
|
396
|
+
visiting.add(ref);
|
|
397
|
+
try {
|
|
398
|
+
// Alias definition (bare $ref): its validator delegates to the target's.
|
|
399
|
+
if (hasRef(resolved) && !hasProperties(resolved)) {
|
|
400
|
+
return canTrustReferencedValidator(resolved.$ref, rootSchema, visiting);
|
|
401
|
+
}
|
|
402
|
+
// Pure union definition: real when the membership check is enforceable.
|
|
403
|
+
if (!hasProperties(resolved)) {
|
|
404
|
+
const branches = getUnionBranches(resolved);
|
|
405
|
+
if (!branches)
|
|
406
|
+
return false;
|
|
407
|
+
if ('patternProperties' in resolved || 'if' in resolved)
|
|
408
|
+
return false;
|
|
409
|
+
return branches.every((branch) => {
|
|
410
|
+
if (!isSchemaObject(branch))
|
|
411
|
+
return false;
|
|
412
|
+
return isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
|
|
413
|
+
? canTrustInlineObjectCheck(branch, rootSchema, visiting)
|
|
414
|
+
: canTrustPropertyCheck(branch, rootSchema, visiting);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
// Object definition: real when generateShapeValidator emits a predicate —
|
|
418
|
+
// no composition/conditional keywords, and every property check resolves.
|
|
419
|
+
if (hasOneOf(resolved) ||
|
|
420
|
+
hasAnyOf(resolved) ||
|
|
421
|
+
hasAllOf(resolved) ||
|
|
422
|
+
'not' in resolved ||
|
|
423
|
+
'patternProperties' in resolved ||
|
|
424
|
+
'if' in resolved ||
|
|
425
|
+
'then' in resolved ||
|
|
426
|
+
'else' in resolved) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
// Walk with the deep shape-validator mirror: the emitted validator routes
|
|
430
|
+
// inline object properties (and arrays of inline object items) through
|
|
431
|
+
// private sub-predicates, so trust must recurse the same way instead of
|
|
432
|
+
// stopping at a shallow isObject/Array.isArray reading.
|
|
433
|
+
return Object.values(resolved.properties).every((propSchema) => canTrustShapeProperty(propSchema, rootSchema, visiting));
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
visiting.delete(ref);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
@@ -6,6 +6,13 @@ 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
|
+
* True when an array's `items` schema can be coerced element-by-element by the
|
|
11
|
+
* mapping slow path: single scalar types (via {@link scalarItemTypeCheck}) and
|
|
12
|
+
* enums (whose validation expression coerces a non-member to the first member).
|
|
13
|
+
* Anything richer (objects, unions, `$ref`s) needs a real item parser instead.
|
|
14
|
+
*/
|
|
15
|
+
export declare const isCoercibleItemSchema: (itemSchema: JSONSchema) => boolean;
|
|
9
16
|
/**
|
|
10
17
|
* Generates a validation expression that checks if a value matches the expected type.
|
|
11
18
|
* Returns the value if valid, coerces to the correct type if invalid, or uses default if required and missing.
|
|
@@ -46,6 +46,17 @@ export const scalarItemTypeCheck = (itemSchema, accessor) => {
|
|
|
46
46
|
return null;
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* True when an array's `items` schema can be coerced element-by-element by the
|
|
51
|
+
* mapping slow path: single scalar types (via {@link scalarItemTypeCheck}) and
|
|
52
|
+
* enums (whose validation expression coerces a non-member to the first member).
|
|
53
|
+
* Anything richer (objects, unions, `$ref`s) needs a real item parser instead.
|
|
54
|
+
*/
|
|
55
|
+
export const isCoercibleItemSchema = (itemSchema) => {
|
|
56
|
+
if (scalarItemTypeCheck(itemSchema, '_it') !== null)
|
|
57
|
+
return true;
|
|
58
|
+
return isSchemaObject(itemSchema) && hasEnum(itemSchema) && itemSchema.enum.length > 0;
|
|
59
|
+
};
|
|
49
60
|
/**
|
|
50
61
|
* A boolean type check for a single JSON Schema primitive type name. Unlike
|
|
51
62
|
* {@link scalarItemTypeCheck} this also covers `array`/`object` (as inline shape
|
|
@@ -228,15 +239,16 @@ export const generateValidationExpression = (key, schema, defaultValue, isRequir
|
|
|
228
239
|
if (!hasType(schema) && !hasEnum(schema)) {
|
|
229
240
|
return `${accessor} ?? ${defaultValue}`;
|
|
230
241
|
}
|
|
231
|
-
// An array of scalar items: coerce each element so e.g. `number[]`
|
|
232
|
-
// contains numbers
|
|
233
|
-
// element reaches here. ($ref
|
|
234
|
-
// object
|
|
242
|
+
// An array of scalar or enum items: coerce each element so e.g. `number[]`
|
|
243
|
+
// actually contains numbers and an enum member set holds. The fast path only
|
|
244
|
+
// takes a well-typed array, so a mistyped element reaches here. ($ref and
|
|
245
|
+
// inline-object items use the caller's validateArray path; union items still
|
|
246
|
+
// pass through.)
|
|
235
247
|
if (hasType(schema) &&
|
|
236
248
|
schema.type === 'array' &&
|
|
237
249
|
hasItems(schema) &&
|
|
238
250
|
!Array.isArray(schema.items) &&
|
|
239
|
-
|
|
251
|
+
isCoercibleItemSchema(schema.items)) {
|
|
240
252
|
const itemSchema = schema.items;
|
|
241
253
|
const itemExpr = generateValidationExpression('', itemSchema, getDefaultValue(itemSchema), true, rootSchema, visitedRefs, '_it', true);
|
|
242
254
|
const mapped = `(Array.isArray(${accessor}) ? (${accessor} as unknown[]).map((_it) => ${itemExpr}) : ${defaultValue})`;
|
|
@@ -12,12 +12,4 @@ export type CollectedHelpers = {
|
|
|
12
12
|
*/
|
|
13
13
|
readonly used: Set<RuntimeHelperName>;
|
|
14
14
|
};
|
|
15
|
-
|
|
16
|
-
* Detects which runtime helpers a generated parser body references.
|
|
17
|
-
*
|
|
18
|
-
* @param helpersImportPrefix - Relative path prefix to the shared `_helpers/`
|
|
19
|
-
* directory in embedded mode. Defaults to `'./'`. The recursive multi-schema
|
|
20
|
-
* build passes `'../'`, `'../../'`, etc. so nested parsers can reach a single
|
|
21
|
-
* `_helpers/` directory at the output root.
|
|
22
|
-
*/
|
|
23
|
-
export declare const collectHelpers: (parserFunction: string, mode: HelpersMode, helpersImportPrefix?: string) => CollectedHelpers;
|
|
15
|
+
export declare const collectHelpers: (parserFunction: string, mode: HelpersMode, helpersImportPrefix?: string, importExt?: 'js' | 'ts') => CollectedHelpers;
|
|
@@ -11,9 +11,9 @@ const EMBEDDED_NAMED_EXPORTS = {
|
|
|
11
11
|
'validate-record': 'validateRecord',
|
|
12
12
|
'has-ref': 'hasRef',
|
|
13
13
|
};
|
|
14
|
-
const embeddedImport = (helper, prefix) =>
|
|
15
|
-
//
|
|
16
|
-
`import { ${EMBEDDED_NAMED_EXPORTS[helper]} } from '${prefix}_helpers/${helper}
|
|
14
|
+
const embeddedImport = (helper, prefix, ext) =>
|
|
15
|
+
// An explicit extension so the embedded-helper import resolves under Node ESM, not only Bun.
|
|
16
|
+
`import { ${EMBEDDED_NAMED_EXPORTS[helper]} } from '${prefix}_helpers/${helper}.${ext}';`;
|
|
17
17
|
/**
|
|
18
18
|
* Detects which runtime helpers a generated parser body references.
|
|
19
19
|
*
|
|
@@ -21,16 +21,43 @@ const embeddedImport = (helper, prefix) =>
|
|
|
21
21
|
* directory in embedded mode. Defaults to `'./'`. The recursive multi-schema
|
|
22
22
|
* build passes `'../'`, `'../../'`, etc. so nested parsers can reach a single
|
|
23
23
|
* `_helpers/` directory at the output root.
|
|
24
|
+
* @param importExt - Extension used on embedded-helper import specifiers.
|
|
25
|
+
* Defaults to `'js'` (the TS NodeNext form); `'ts'` makes the output runnable
|
|
26
|
+
* under Node's type stripping.
|
|
24
27
|
*/
|
|
25
|
-
|
|
28
|
+
// One alternation pass over the generated source instead of four full-text
|
|
29
|
+
// `.includes` scans — an absent helper name used to cost a complete rescan
|
|
30
|
+
// each, which showed up at several percent of total generation time.
|
|
31
|
+
const HELPER_USAGE = /validateArray|validateRecord|isObject|hasRef\(/g;
|
|
32
|
+
export const collectHelpers = (parserFunction, mode, helpersImportPrefix = './', importExt = 'js') => {
|
|
26
33
|
const imports = [];
|
|
27
34
|
const used = new Set();
|
|
28
|
-
const importFor = (helper) => mode === 'embedded' ? embeddedImport(helper, helpersImportPrefix) : PACKAGE_IMPORTS[helper];
|
|
29
|
-
|
|
35
|
+
const importFor = (helper) => mode === 'embedded' ? embeddedImport(helper, helpersImportPrefix, importExt) : PACKAGE_IMPORTS[helper];
|
|
36
|
+
let sawValidateArray = false;
|
|
37
|
+
let sawValidateRecord = false;
|
|
38
|
+
let sawIsObject = false;
|
|
39
|
+
let sawHasRef = false;
|
|
40
|
+
HELPER_USAGE.lastIndex = 0;
|
|
41
|
+
let match = HELPER_USAGE.exec(parserFunction);
|
|
42
|
+
while (match !== null) {
|
|
43
|
+
const token = match[0];
|
|
44
|
+
if (token === 'validateArray')
|
|
45
|
+
sawValidateArray = true;
|
|
46
|
+
else if (token === 'validateRecord')
|
|
47
|
+
sawValidateRecord = true;
|
|
48
|
+
else if (token === 'isObject')
|
|
49
|
+
sawIsObject = true;
|
|
50
|
+
else
|
|
51
|
+
sawHasRef = true;
|
|
52
|
+
if (sawValidateArray && sawValidateRecord && sawIsObject && sawHasRef)
|
|
53
|
+
break;
|
|
54
|
+
match = HELPER_USAGE.exec(parserFunction);
|
|
55
|
+
}
|
|
56
|
+
if (sawValidateArray) {
|
|
30
57
|
imports.push(importFor('validate-array'));
|
|
31
58
|
used.add('validate-array');
|
|
32
59
|
}
|
|
33
|
-
if (
|
|
60
|
+
if (sawValidateRecord) {
|
|
34
61
|
imports.push(importFor('validate-record'));
|
|
35
62
|
used.add('validate-record');
|
|
36
63
|
// The embedded validate-record.ts imports is-object, so it must be shipped too —
|
|
@@ -38,11 +65,11 @@ export const collectHelpers = (parserFunction, mode, helpersImportPrefix = './')
|
|
|
38
65
|
// parser body itself references `isObject` (handled below).
|
|
39
66
|
used.add('is-object');
|
|
40
67
|
}
|
|
41
|
-
if (
|
|
68
|
+
if (sawIsObject) {
|
|
42
69
|
imports.push(importFor('is-object'));
|
|
43
70
|
used.add('is-object');
|
|
44
71
|
}
|
|
45
|
-
if (
|
|
72
|
+
if (sawHasRef) {
|
|
46
73
|
imports.push(importFor('has-ref'));
|
|
47
74
|
used.add('has-ref');
|
|
48
75
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
|
+
/** Extension emitted on every relative import specifier in generated code. */
|
|
3
|
+
export type ImportExtension = 'js' | 'ts';
|
|
2
4
|
/**
|
|
3
5
|
* Options for controlling how imports are collected.
|
|
4
6
|
*/
|
|
@@ -26,6 +28,12 @@ type CollectImportsOptions = {
|
|
|
26
28
|
* Defaults to `''` (no suffix).
|
|
27
29
|
*/
|
|
28
30
|
readonly typeSuffix?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Extension used on every relative import specifier. Defaults to `'js'`
|
|
33
|
+
* (the TS NodeNext form); `'ts'` makes the output runnable under Node's
|
|
34
|
+
* type stripping.
|
|
35
|
+
*/
|
|
36
|
+
readonly importExt?: ImportExtension;
|
|
29
37
|
};
|
|
30
38
|
/**
|
|
31
39
|
* Collects all import statements needed for a schema by finding $ref references
|
|
@@ -67,4 +75,11 @@ type CollectImportsOptions = {
|
|
|
67
75
|
* ```
|
|
68
76
|
*/
|
|
69
77
|
export declare const collectImports: (schema: JSONSchema, options?: CollectImportsOptions) => string[];
|
|
78
|
+
/**
|
|
79
|
+
* The type names a generated file will import for its `$ref`s — the same
|
|
80
|
+
* dedup/skip rules as {@link collectImports}. Generators seed their private
|
|
81
|
+
* sub-type naming with this set so a synthesized name (e.g. a root array's
|
|
82
|
+
* `FooItem`) can never shadow an imported identifier.
|
|
83
|
+
*/
|
|
84
|
+
export declare const collectImportTypeNames: (schema: JSONSchema, options?: CollectImportsOptions) => Set<string>;
|
|
70
85
|
export {};
|