@amritk/generate-parsers 0.16.2 → 0.17.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.
@@ -1,486 +1,313 @@
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 { getPrefixItems, prefixItemsCapsLength, 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);
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 { getPrefixItems, prefixItemsCapsLength, scalarItemTypeCheck } from "./generate-validation-expression.js";
9
+ const shapeValidatorName = (typeName) => `validate${typeName}Shape`;
10
+ const isInlineObjectProperty = (propSchema) => {
11
+ if (!isSchemaObject(propSchema))
12
+ return false;
13
+ if (hasRef(propSchema) || hasEnum(propSchema) || hasConst(propSchema))
14
+ return false;
15
+ if (hasOneOf(propSchema) || hasAnyOf(propSchema) || hasAllOf(propSchema))
16
+ return false;
17
+ if ("patternProperties" in propSchema || "not" in propSchema)
18
+ return false;
19
+ if ("if" in propSchema || "then" in propSchema || "else" in propSchema)
20
+ return false;
21
+ if (hasAdditionalProperties(propSchema) && typeof propSchema.additionalProperties !== "boolean")
22
+ return false;
23
+ return isObjectSchema(propSchema) && hasProperties(propSchema);
56
24
  };
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);
25
+ const isInlineObjectArrayProperty = (propSchema) => {
26
+ if (!isSchemaObject(propSchema))
27
+ return false;
28
+ if (!("type" in propSchema) || propSchema.type !== "array")
29
+ return false;
30
+ if (!hasItems(propSchema) || Array.isArray(propSchema.items))
31
+ return false;
32
+ return isInlineObjectProperty(propSchema.items);
72
33
  };
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;
34
+ const getUnionBranches = (schema) => {
35
+ if (!isSchemaObject(schema))
36
+ return null;
37
+ if (hasAllOf(schema) || "not" in schema)
87
38
  return null;
39
+ if (hasOneOf(schema) && schema.oneOf.length > 0)
40
+ return schema.oneOf;
41
+ if (hasAnyOf(schema) && schema.anyOf.length > 0)
42
+ return schema.anyOf;
43
+ return null;
88
44
  };
89
- /**
90
- * True when a schema carries a keyword the strict slow path enforces but which
91
- * no flat type check can mirror: `contains` (array item counting),
92
- * `dependentRequired` / `dependentSchemas` (cross-property presence), or
93
- * `propertyNames` (per-key constraints). A fast path or shape validator that
94
- * accepted such a value on a bare type check would skip the enforcement, so
95
- * both must bail when this is true — the slow path then runs the real checks.
96
- */
97
- export const hasFastPathBlockingKeyword = (schema) => {
98
- if (!isSchemaObject(schema))
99
- return false;
100
- const record = schema;
101
- // Plain `in` checks (not the typed `hasDependentRequired`): this runs several
102
- // times per property during codegen, and a present-but-malformed keyword still
103
- // belongs on the slow path, where the typed guards handle it.
104
- return ('contains' in record || 'dependentSchemas' in record || 'propertyNames' in record || 'dependentRequired' in record);
45
+ const hasFastPathBlockingKeyword = (schema) => {
46
+ if (!isSchemaObject(schema))
47
+ return false;
48
+ const record = schema;
49
+ return "contains" in record || "dependentSchemas" in record || "propertyNames" in record || "dependentRequired" in record;
105
50
  };
106
- /**
107
- * Generates a fast-path type check expression for a property.
108
- * Returns null if the schema is too complex for a simple type check.
109
- *
110
- * When `useRefImports` is true, $ref properties (and arrays of $refs) are
111
- * checked by calling the imported shape predicate, allowing parent parsers
112
- * to fast-path through nested ref types without recursing into their parsers.
113
- */
114
- export const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
115
- if (!isSchemaObject(schema))
116
- return null;
117
- // Keywords the strict slow path enforces but no flat check can prove: bail so
118
- // neither the fast path nor the shape validator accepts a value they would
119
- // otherwise wave through untested. Inlined here (rather than calling
120
- // hasFastPathBlockingKeyword) to reuse the isSchemaObject narrowing above —
121
- // this is a codegen hot path hit several times per property. Keep in sync with
122
- // hasFastPathBlockingKeyword.
123
- const record = schema;
124
- if ('contains' in record ||
125
- 'dependentSchemas' in record ||
126
- 'propertyNames' in record ||
127
- 'dependentRequired' in record) {
128
- return null;
129
- }
130
- // $ref via shape predicate (deep fast-path through nested types).
131
- if (useRefImports && hasRef(schema)) {
132
- const refName = refToName(schema.$ref, suffix);
133
- return `${shapeValidatorName(refName)}(${varName})`;
134
- }
135
- if (hasRef(schema))
136
- return null;
137
- // A fixed member set is directly predicable regardless of `type` — this is
138
- // what lets a nested object carrying an enum property (e.g. `axiom.kind`)
139
- // keep a real shape validator instead of the `=> false` stub.
140
- if (hasEnum(schema)) {
141
- return schema.enum.length > 0 ? generateEnumCheck(varName, schema.enum) : null;
142
- }
143
- // A union property is predicable when every branch is: membership is the
144
- // disjunction of the branch checks.
145
- const branches = getUnionBranches(schema);
146
- if (branches) {
147
- return generateUnionCheck(varName, branches, useRefImports, suffix);
148
- }
149
- // Remaining composition (allOf, not, or a union mixed with them) cannot be
150
- // expressed as a simple boolean check.
151
- if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || 'not' in schema) {
152
- return null;
153
- }
154
- // A `const` property (the usual discriminated-union tag, e.g. `kind: "lit"`) has
155
- // no `type`, but is fast-path predicable via strict equality. Handling it here is
156
- // what lets a discriminated branch's shape validator be a real predicate instead
157
- // of the `=> false` stub. Structural consts need deep equality, so skip those.
158
- if (hasConst(schema)) {
159
- const c = schema.const;
160
- return c === null || typeof c !== 'object' ? `${varName} === ${JSON.stringify(c)}` : null;
51
+ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
52
+ if (!isSchemaObject(schema))
53
+ return null;
54
+ const record = schema;
55
+ if ("contains" in record || "dependentSchemas" in record || "propertyNames" in record || "dependentRequired" in record) {
56
+ return null;
57
+ }
58
+ if (useRefImports && hasRef(schema)) {
59
+ const refName = refToName(schema.$ref, suffix);
60
+ return `${shapeValidatorName(refName)}(${varName})`;
61
+ }
62
+ if (hasRef(schema))
63
+ return null;
64
+ if (hasEnum(schema)) {
65
+ return schema.enum.length > 0 ? generateEnumCheck(varName, schema.enum) : null;
66
+ }
67
+ const branches = getUnionBranches(schema);
68
+ if (branches) {
69
+ return generateUnionCheck(varName, branches, useRefImports, suffix);
70
+ }
71
+ if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || "not" in schema) {
72
+ return null;
73
+ }
74
+ if (hasConst(schema)) {
75
+ const c = schema.const;
76
+ return c === null || typeof c !== "object" ? `${varName} === ${JSON.stringify(c)}` : null;
77
+ }
78
+ if (!hasType(schema))
79
+ return null;
80
+ if (useRefImports && schema.type === "array" && hasItems(schema) && isSchemaObject(schema.items) && hasRef(schema.items)) {
81
+ const refName = refToName(schema.items.$ref, suffix);
82
+ return `Array.isArray(${varName}) && ${varName}.every(${shapeValidatorName(refName)})`;
83
+ }
84
+ const checks = [];
85
+ switch (schema.type) {
86
+ case "string": {
87
+ checks.push(`typeof ${varName} === "string"`);
88
+ if (hasPattern(schema))
89
+ checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${varName})`);
90
+ if (hasMinLength(schema))
91
+ checks.push(`${varName}.length >= ${schema.minLength}`);
92
+ if (hasMaxLength(schema))
93
+ checks.push(`${varName}.length <= ${schema.maxLength}`);
94
+ break;
161
95
  }
162
- if (!hasType(schema))
163
- return null;
164
- // Array of $refs: each item must satisfy the imported shape predicate.
165
- if (useRefImports &&
166
- schema.type === 'array' &&
167
- hasItems(schema) &&
168
- isSchemaObject(schema.items) &&
169
- hasRef(schema.items)) {
170
- const refName = refToName(schema.items.$ref, suffix);
171
- return `Array.isArray(${varName}) && ${varName}.every(${shapeValidatorName(refName)})`;
96
+ case "number":
97
+ case "integer": {
98
+ checks.push(`typeof ${varName} === "number"`);
99
+ if (schema.type === "integer")
100
+ checks.push(`Number.isInteger(${varName})`);
101
+ if (hasMinimum(schema))
102
+ checks.push(`${varName} >= ${schema.minimum}`);
103
+ if (hasMaximum(schema))
104
+ checks.push(`${varName} <= ${schema.maximum}`);
105
+ if (hasExclusiveMinimum(schema))
106
+ checks.push(`${varName} > ${schema.exclusiveMinimum}`);
107
+ if (hasExclusiveMaximum(schema))
108
+ checks.push(`${varName} < ${schema.exclusiveMaximum}`);
109
+ if (hasMultipleOf(schema))
110
+ checks.push(multipleOfPassExpr(varName, schema.multipleOf));
111
+ break;
172
112
  }
173
- const checks = [];
174
- switch (schema.type) {
175
- case 'string': {
176
- checks.push(`typeof ${varName} === "string"`);
177
- if (hasPattern(schema))
178
- checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${varName})`);
179
- if (hasMinLength(schema))
180
- checks.push(`${varName}.length >= ${schema.minLength}`);
181
- if (hasMaxLength(schema))
182
- checks.push(`${varName}.length <= ${schema.maxLength}`);
183
- break;
184
- }
185
- case 'number':
186
- case 'integer': {
187
- checks.push(`typeof ${varName} === "number"`);
188
- // `integer` must reject non-integral numbers on the fast path too, otherwise
189
- // `1.5` would pass through uncoerced.
190
- if (schema.type === 'integer')
191
- checks.push(`Number.isInteger(${varName})`);
192
- if (hasMinimum(schema))
193
- checks.push(`${varName} >= ${schema.minimum}`);
194
- if (hasMaximum(schema))
195
- checks.push(`${varName} <= ${schema.maximum}`);
196
- if (hasExclusiveMinimum(schema))
197
- checks.push(`${varName} > ${schema.exclusiveMinimum}`);
198
- if (hasExclusiveMaximum(schema))
199
- checks.push(`${varName} < ${schema.exclusiveMaximum}`);
200
- if (hasMultipleOf(schema))
201
- checks.push(multipleOfPassExpr(varName, schema.multipleOf));
202
- break;
203
- }
204
- case 'boolean':
205
- checks.push(`typeof ${varName} === "boolean"`);
206
- break;
207
- case 'array': {
208
- checks.push(`Array.isArray(${varName})`);
209
- if (hasMinItems(schema))
210
- checks.push(`${varName}.length >= ${schema.minItems}`);
211
- if (hasMaxItems(schema))
212
- checks.push(`${varName}.length <= ${schema.maxItems}`);
213
- if (hasUniqueItems(schema) && schema.uniqueItems === true) {
214
- checks.push(`new Set(${varName}).size === ${varName}.length`);
215
- }
216
- // For a scalar or enum item type, every element must already be well-typed
217
- // to take the fast path; a mismatched element routes the array to the slow
218
- // path where each element is coerced (or, in strict mode, throws).
219
- if (hasItems(schema) && !Array.isArray(schema.items)) {
220
- const items = schema.items;
221
- const itemCheck = scalarItemTypeCheck(items, '_it') ??
222
- (isSchemaObject(items) && hasEnum(items) && items.enum.length > 0
223
- ? generateEnumCheck('_it', items.enum)
224
- : null);
225
- if (itemCheck)
226
- checks.push(`${varName}.every((_it) => ${itemCheck})`);
227
- }
228
- // Tuple `prefixItems`: every present position must already match its
229
- // subschema, and a sibling `items: false` bars extra elements. Positions
230
- // are checked with ref imports disabled, so no imported shape-validator
231
- // call is introduced here — a position needing one returns null and simply
232
- // disables the fast path, keeping the union trust-walk sound without a
233
- // mirrored prefixItems traversal. A mistyped tuple then routes to the slow
234
- // path (coerced) or the strict assertion (thrown).
235
- const prefix = getPrefixItems(schema);
236
- if (prefix) {
237
- for (let i = 0; i < prefix.length; i++) {
238
- const posCheck = generatePropertyTypeCheck(`${varName}[${i}]`, prefix[i], false, suffix);
239
- if (posCheck === null)
240
- return null;
241
- checks.push(`(${varName}.length <= ${i} || (${posCheck}))`);
242
- }
243
- if (prefixItemsCapsLength(schema))
244
- checks.push(`${varName}.length <= ${prefix.length}`);
245
- }
246
- break;
247
- }
248
- case 'object':
249
- checks.push(`isObject(${varName})`);
250
- break;
251
- default:
113
+ case "boolean":
114
+ checks.push(`typeof ${varName} === "boolean"`);
115
+ break;
116
+ case "array": {
117
+ checks.push(`Array.isArray(${varName})`);
118
+ if (hasMinItems(schema))
119
+ checks.push(`${varName}.length >= ${schema.minItems}`);
120
+ if (hasMaxItems(schema))
121
+ checks.push(`${varName}.length <= ${schema.maxItems}`);
122
+ if (hasUniqueItems(schema) && schema.uniqueItems === true) {
123
+ checks.push(`new Set(${varName}).size === ${varName}.length`);
124
+ }
125
+ if (hasItems(schema) && !Array.isArray(schema.items)) {
126
+ const items = schema.items;
127
+ const itemCheck = scalarItemTypeCheck(items, "_it") ?? (isSchemaObject(items) && hasEnum(items) && items.enum.length > 0 ? generateEnumCheck("_it", items.enum) : null);
128
+ if (itemCheck)
129
+ checks.push(`${varName}.every((_it) => ${itemCheck})`);
130
+ }
131
+ const prefix = getPrefixItems(schema);
132
+ if (prefix) {
133
+ for (let i = 0; i < prefix.length; i++) {
134
+ const posCheck = generatePropertyTypeCheck(`${varName}[${i}]`, prefix[i], false, suffix);
135
+ if (posCheck === null)
252
136
  return null;
137
+ checks.push(`(${varName}.length <= ${i} || (${posCheck}))`);
138
+ }
139
+ if (prefixItemsCapsLength(schema))
140
+ checks.push(`${varName}.length <= ${prefix.length}`);
141
+ }
142
+ break;
253
143
  }
254
- if (checks.length === 0)
255
- return null;
256
- // checks[0] is safe: we guard with checks.length === 0 above
257
- let result = checks[0];
258
- for (let i = 1; i < checks.length; i++) {
259
- result += ' && ' + checks[i];
260
- }
261
- return result;
144
+ case "object":
145
+ checks.push(`isObject(${varName})`);
146
+ break;
147
+ default:
148
+ return null;
149
+ }
150
+ if (checks.length === 0)
151
+ return null;
152
+ let result = checks[0];
153
+ for (let i = 1; i < checks.length; i++) {
154
+ result += " && " + checks[i];
155
+ }
156
+ return result;
262
157
  };
263
- /**
264
- * Generates a boolean check that `varName` matches an inline object schema —
265
- * `isObject` plus a per-property check for every declared property (and a
266
- * presence check for `required` keys without a declared schema). Used for
267
- * union branches written inline, where no named shape validator exists to
268
- * call. Returns null when any property is too complex to check.
269
- */
270
- export const generateInlineObjectCheck = (varName, schema, useRefImports, suffix) => {
271
- if (!isSchemaObject(schema) || !isObjectSchema(schema) || !hasProperties(schema))
272
- return null;
273
- if (hasAllOf(schema) || hasOneOf(schema) || hasAnyOf(schema) || 'not' in schema)
274
- return null;
275
- if ('patternProperties' in schema || 'if' in schema)
276
- return null;
277
- // additionalProperties-as-schema (records) would leave the record values
278
- // unchecked, breaking the "true means valid" guarantee.
279
- if (hasAdditionalProperties(schema) && typeof schema.additionalProperties !== 'boolean')
280
- return null;
281
- const required = new Set(Array.isArray(schema.required) ? schema.required : []);
282
- const checks = [`isObject(${varName})`];
283
- for (const [key, propSchema] of Object.entries(schema.properties)) {
284
- const acc = safeAccessor(varName, key);
285
- const check = generatePropertyTypeCheck(acc, propSchema, useRefImports, suffix);
286
- if (check === null)
287
- return null;
288
- checks.push(required.has(key) ? check : `(${acc} === undefined || ${check})`);
289
- required.delete(key);
290
- }
291
- // `required` keys without a declared property schema still need a presence check.
292
- for (const key of required) {
293
- checks.push(`${JSON.stringify(key)} in ${varName}`);
294
- }
295
- // additionalProperties: false — extras make the value invalid for this branch.
296
- if (hasAdditionalProperties(schema) && schema.additionalProperties === false) {
297
- const allowedKeys = JSON.stringify(Object.keys(schema.properties));
298
- checks.push(`Object.keys(${varName}).every((_k) => ${allowedKeys}.includes(_k))`);
299
- }
300
- return checks.join(' && ');
158
+ const generateInlineObjectCheck = (varName, schema, useRefImports, suffix) => {
159
+ if (!isSchemaObject(schema) || !isObjectSchema(schema) || !hasProperties(schema))
160
+ return null;
161
+ if (hasAllOf(schema) || hasOneOf(schema) || hasAnyOf(schema) || "not" in schema)
162
+ return null;
163
+ if ("patternProperties" in schema || "if" in schema)
164
+ return null;
165
+ if (hasAdditionalProperties(schema) && typeof schema.additionalProperties !== "boolean")
166
+ return null;
167
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
168
+ const checks = [`isObject(${varName})`];
169
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
170
+ const acc = safeAccessor(varName, key);
171
+ const check = generatePropertyTypeCheck(acc, propSchema, useRefImports, suffix);
172
+ if (check === null)
173
+ return null;
174
+ checks.push(required.has(key) ? check : `(${acc} === undefined || ${check})`);
175
+ required.delete(key);
176
+ }
177
+ for (const key of required) {
178
+ checks.push(`${JSON.stringify(key)} in ${varName}`);
179
+ }
180
+ if (hasAdditionalProperties(schema) && schema.additionalProperties === false) {
181
+ const allowedKeys = JSON.stringify(Object.keys(schema.properties));
182
+ checks.push(`Object.keys(${varName}).every((_k) => ${allowedKeys}.includes(_k))`);
183
+ }
184
+ return checks.join(" && ");
301
185
  };
302
- /**
303
- * Generates a union membership check: true when `varName` matches at least one
304
- * branch of a `oneOf`/`anyOf`. Inline object branches get a per-property check;
305
- * everything else goes through {@link generatePropertyTypeCheck} ($refs call
306
- * the imported shape validator, consts/enums/scalars check directly). Returns
307
- * null when any branch cannot be checked.
308
- */
309
- export const generateUnionCheck = (varName, branches, useRefImports, suffix) => {
310
- if (branches.length === 0)
311
- return null;
312
- const parts = [];
313
- for (const branch of branches) {
314
- if (!isSchemaObject(branch))
315
- return null;
316
- const check = isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
317
- ? generateInlineObjectCheck(varName, branch, useRefImports, suffix)
318
- : generatePropertyTypeCheck(varName, branch, useRefImports, suffix);
319
- if (check === null)
320
- return null;
321
- parts.push(`(${check})`);
322
- }
323
- // Callers conjoin this with other checks, so a multi-branch disjunction must
324
- // carry its own parentheses — `a || b && c` binds the wrong way without them.
325
- return parts.length === 1 ? parts[0] : `(${parts.join(' || ')})`;
186
+ const generateUnionCheck = (varName, branches, useRefImports, suffix) => {
187
+ if (branches.length === 0)
188
+ return null;
189
+ const parts = [];
190
+ for (const branch of branches) {
191
+ if (!isSchemaObject(branch))
192
+ return null;
193
+ const check = isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch) ? generateInlineObjectCheck(varName, branch, useRefImports, suffix) : generatePropertyTypeCheck(varName, branch, useRefImports, suffix);
194
+ if (check === null)
195
+ return null;
196
+ parts.push(`(${check})`);
197
+ }
198
+ return parts.length === 1 ? parts[0] : `(${parts.join(" || ")})`;
326
199
  };
327
- /**
328
- * True when the union membership check emitted by {@link generateUnionCheck}
329
- * is also *false-sound* — a false result definitely means the value is invalid
330
- * — so strict mode may throw on it.
331
- *
332
- * The risk is `$ref`s: a branch (or a property inside one) whose check calls
333
- * an imported `validate{X}Shape` is only trustworthy when that validator was
334
- * generated as a real predicate. This walks every reachable ref (mirroring
335
- * generateShapeValidator's stub conditions) and rejects the union when any of
336
- * them would have produced the conservative `=> false` stub, or is itself
337
- * built on one. Cycles (e.g. a recursive `expr` union) are assumed sound —
338
- * the recursion bottoms out on the data, not the schema.
339
- */
340
- export const canEnforceUnion = (branches, rootSchema) => {
341
- const visiting = new Set();
342
- return (branches.length > 0 &&
343
- branches.every((branch) => {
344
- if (!isSchemaObject(branch))
345
- return false;
346
- return isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
347
- ? canTrustInlineObjectCheck(branch, rootSchema, visiting)
348
- : canTrustPropertyCheck(branch, rootSchema, visiting);
349
- }));
200
+ const canEnforceUnion = (branches, rootSchema) => {
201
+ const visiting = /* @__PURE__ */ new Set();
202
+ return branches.length > 0 && branches.every((branch) => {
203
+ if (!isSchemaObject(branch))
204
+ return false;
205
+ return isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch) ? canTrustInlineObjectCheck(branch, rootSchema, visiting) : canTrustPropertyCheck(branch, rootSchema, visiting);
206
+ });
350
207
  };
351
- /** Mirrors {@link generatePropertyTypeCheck}: non-null AND every embedded validator call is real. */
352
208
  const canTrustPropertyCheck = (schema, rootSchema, visiting) => {
353
- if (!isSchemaObject(schema))
209
+ if (!isSchemaObject(schema))
210
+ return false;
211
+ if (hasRef(schema)) {
212
+ return canTrustReferencedValidator(schema.$ref, rootSchema, visiting);
213
+ }
214
+ if (hasEnum(schema))
215
+ return schema.enum.length > 0;
216
+ const branches = getUnionBranches(schema);
217
+ if (branches) {
218
+ return branches.every((branch) => {
219
+ if (!isSchemaObject(branch))
354
220
  return false;
355
- if (hasRef(schema)) {
356
- return canTrustReferencedValidator(schema.$ref, rootSchema, visiting);
357
- }
358
- if (hasEnum(schema))
359
- return schema.enum.length > 0;
360
- const branches = getUnionBranches(schema);
361
- if (branches) {
362
- return branches.every((branch) => {
363
- if (!isSchemaObject(branch))
364
- return false;
365
- return isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
366
- ? canTrustInlineObjectCheck(branch, rootSchema, visiting)
367
- : canTrustPropertyCheck(branch, rootSchema, visiting);
368
- });
369
- }
370
- if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || 'not' in schema)
371
- return false;
372
- if (hasConst(schema))
373
- return schema.const === null || typeof schema.const !== 'object';
374
- if (!hasType(schema))
375
- return false;
376
- if (schema.type === 'array' && hasItems(schema) && isSchemaObject(schema.items) && hasRef(schema.items)) {
377
- return canTrustReferencedValidator(schema.items.$ref, rootSchema, visiting);
378
- }
379
- switch (schema.type) {
380
- case 'string':
381
- case 'number':
382
- case 'integer':
383
- case 'boolean':
384
- case 'array':
385
- case 'object':
386
- return true;
387
- default:
388
- return false;
389
- }
221
+ return isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch) ? canTrustInlineObjectCheck(branch, rootSchema, visiting) : canTrustPropertyCheck(branch, rootSchema, visiting);
222
+ });
223
+ }
224
+ if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || "not" in schema)
225
+ return false;
226
+ if (hasConst(schema))
227
+ return schema.const === null || typeof schema.const !== "object";
228
+ if (!hasType(schema))
229
+ return false;
230
+ if (schema.type === "array" && hasItems(schema) && isSchemaObject(schema.items) && hasRef(schema.items)) {
231
+ return canTrustReferencedValidator(schema.items.$ref, rootSchema, visiting);
232
+ }
233
+ switch (schema.type) {
234
+ case "string":
235
+ case "number":
236
+ case "integer":
237
+ case "boolean":
238
+ case "array":
239
+ case "object":
240
+ return true;
241
+ default:
242
+ return false;
243
+ }
390
244
  };
391
- /**
392
- * Mirrors the per-property check {@link generateShapeValidator} emits for a
393
- * generated file's exported validator, which is *deeper* than
394
- * {@link generatePropertyTypeCheck}: inline object properties and arrays of
395
- * inline object items are checked through private sub-predicates generated
396
- * from the same schema, and a sub-predicate is a conservative `=> false` stub
397
- * whenever some nested property check cannot be built. A validator built on a
398
- * stub returns false on *valid* input, so trusting it requires every reachable
399
- * sub-predicate to be a real check, recursively.
400
- */
401
245
  const canTrustShapeProperty = (propSchema, rootSchema, visiting) => {
402
- if (!isSchemaObject(propSchema))
403
- return false;
404
- // Record-valued additionalProperties: records of refs stub the validator;
405
- // stay conservative for any schema-valued record, matching the historical
406
- // bail in canTrustReferencedValidator.
407
- if (hasAdditionalProperties(propSchema) && typeof propSchema.additionalProperties !== 'boolean')
408
- return false;
409
- if (isInlineObjectProperty(propSchema)) {
410
- return Object.values(propSchema.properties ?? {}).every((sub) => canTrustShapeProperty(sub, rootSchema, visiting));
411
- }
412
- if (isInlineObjectArrayProperty(propSchema)) {
413
- const items = propSchema.items;
414
- return Object.values(items.properties ?? {}).every((sub) => canTrustShapeProperty(sub, rootSchema, visiting));
415
- }
416
- return canTrustPropertyCheck(propSchema, rootSchema, visiting);
246
+ if (!isSchemaObject(propSchema))
247
+ return false;
248
+ if (hasAdditionalProperties(propSchema) && typeof propSchema.additionalProperties !== "boolean")
249
+ return false;
250
+ if (isInlineObjectProperty(propSchema)) {
251
+ return Object.values(propSchema.properties ?? {}).every((sub) => canTrustShapeProperty(sub, rootSchema, visiting));
252
+ }
253
+ if (isInlineObjectArrayProperty(propSchema)) {
254
+ const items = propSchema.items;
255
+ return Object.values(items.properties ?? {}).every((sub) => canTrustShapeProperty(sub, rootSchema, visiting));
256
+ }
257
+ return canTrustPropertyCheck(propSchema, rootSchema, visiting);
417
258
  };
418
- /** Mirrors {@link generateInlineObjectCheck}: non-null AND every property check is trustworthy. */
419
259
  const canTrustInlineObjectCheck = (schema, rootSchema, visiting) => {
420
- if (!isSchemaObject(schema) || !isObjectSchema(schema) || !hasProperties(schema))
421
- return false;
422
- if (hasAllOf(schema) || hasOneOf(schema) || hasAnyOf(schema) || 'not' in schema)
423
- return false;
424
- if ('patternProperties' in schema || 'if' in schema)
425
- return false;
426
- if (hasAdditionalProperties(schema) && typeof schema.additionalProperties !== 'boolean')
427
- return false;
428
- return Object.values(schema.properties).every((propSchema) => canTrustPropertyCheck(propSchema, rootSchema, visiting));
260
+ if (!isSchemaObject(schema) || !isObjectSchema(schema) || !hasProperties(schema))
261
+ return false;
262
+ if (hasAllOf(schema) || hasOneOf(schema) || hasAnyOf(schema) || "not" in schema)
263
+ return false;
264
+ if ("patternProperties" in schema || "if" in schema)
265
+ return false;
266
+ if (hasAdditionalProperties(schema) && typeof schema.additionalProperties !== "boolean")
267
+ return false;
268
+ return Object.values(schema.properties).every((propSchema) => canTrustPropertyCheck(propSchema, rootSchema, visiting));
429
269
  };
430
- /**
431
- * True when the file generated for `ref`'s target carries a *real* (non-stub)
432
- * shape validator whose result can be trusted in both directions. Mirrors the
433
- * schema classes generateShapeValidator (and its new union/alias paths) turns
434
- * into real predicates, recursing into every reachable ref.
435
- */
436
270
  const canTrustReferencedValidator = (ref, rootSchema, visiting) => {
437
- if (visiting.has(ref))
438
- return true;
439
- if (!rootSchema)
271
+ if (visiting.has(ref))
272
+ return true;
273
+ if (!rootSchema)
274
+ return false;
275
+ const resolved = resolveRef(ref, rootSchema);
276
+ if (!resolved || !isSchemaObject(resolved))
277
+ return false;
278
+ visiting.add(ref);
279
+ try {
280
+ if (hasRef(resolved) && !hasProperties(resolved)) {
281
+ return canTrustReferencedValidator(resolved.$ref, rootSchema, visiting);
282
+ }
283
+ if (!hasProperties(resolved)) {
284
+ const branches = getUnionBranches(resolved);
285
+ if (!branches)
440
286
  return false;
441
- const resolved = resolveRef(ref, rootSchema);
442
- if (!resolved || !isSchemaObject(resolved))
287
+ if ("patternProperties" in resolved || "if" in resolved)
443
288
  return false;
444
- visiting.add(ref);
445
- try {
446
- // Alias definition (bare $ref): its validator delegates to the target's.
447
- if (hasRef(resolved) && !hasProperties(resolved)) {
448
- return canTrustReferencedValidator(resolved.$ref, rootSchema, visiting);
449
- }
450
- // Pure union definition: real when the membership check is enforceable.
451
- if (!hasProperties(resolved)) {
452
- const branches = getUnionBranches(resolved);
453
- if (!branches)
454
- return false;
455
- if ('patternProperties' in resolved || 'if' in resolved)
456
- return false;
457
- return branches.every((branch) => {
458
- if (!isSchemaObject(branch))
459
- return false;
460
- return isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
461
- ? canTrustInlineObjectCheck(branch, rootSchema, visiting)
462
- : canTrustPropertyCheck(branch, rootSchema, visiting);
463
- });
464
- }
465
- // Object definition: real when generateShapeValidator emits a predicate —
466
- // no composition/conditional keywords, and every property check resolves.
467
- if (hasOneOf(resolved) ||
468
- hasAnyOf(resolved) ||
469
- hasAllOf(resolved) ||
470
- 'not' in resolved ||
471
- 'patternProperties' in resolved ||
472
- 'if' in resolved ||
473
- 'then' in resolved ||
474
- 'else' in resolved) {
475
- return false;
476
- }
477
- // Walk with the deep shape-validator mirror: the emitted validator routes
478
- // inline object properties (and arrays of inline object items) through
479
- // private sub-predicates, so trust must recurse the same way instead of
480
- // stopping at a shallow isObject/Array.isArray reading.
481
- return Object.values(resolved.properties).every((propSchema) => canTrustShapeProperty(propSchema, rootSchema, visiting));
289
+ return branches.every((branch) => {
290
+ if (!isSchemaObject(branch))
291
+ return false;
292
+ return isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch) ? canTrustInlineObjectCheck(branch, rootSchema, visiting) : canTrustPropertyCheck(branch, rootSchema, visiting);
293
+ });
482
294
  }
483
- finally {
484
- visiting.delete(ref);
295
+ if (hasOneOf(resolved) || hasAnyOf(resolved) || hasAllOf(resolved) || "not" in resolved || "patternProperties" in resolved || "if" in resolved || "then" in resolved || "else" in resolved) {
296
+ return false;
485
297
  }
298
+ return Object.values(resolved.properties).every((propSchema) => canTrustShapeProperty(propSchema, rootSchema, visiting));
299
+ } finally {
300
+ visiting.delete(ref);
301
+ }
302
+ };
303
+ export {
304
+ canEnforceUnion,
305
+ generateInlineObjectCheck,
306
+ generatePropertyTypeCheck,
307
+ generateUnionCheck,
308
+ getUnionBranches,
309
+ hasFastPathBlockingKeyword,
310
+ isInlineObjectArrayProperty,
311
+ isInlineObjectProperty,
312
+ shapeValidatorName
486
313
  };