@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,487 +1,351 @@
1
- import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
2
- import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
3
- import { multipleOfFailExpr } from '@amritk/helpers/multiple-of-check';
4
- import { quoteJsString } from '@amritk/helpers/quote-js-string';
5
- import { resolveRef } from '@amritk/helpers/resolve-ref';
6
- import { safeAccessor } from '@amritk/helpers/safe-accessor';
7
- import { hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject, } from '@amritk/helpers/schema-guards';
8
- import { generateEnumCheck } from './generate-enum-check.js';
9
- import { canEnforceUnion, generateUnionCheck, getUnionBranches, isInlineObjectProperty } from './generate-type-checks.js';
10
- import { getPrefixItems, prefixItemsCapsLength, scalarItemTypeCheck } from './generate-validation-expression.js';
11
- import { subschemaMatchExpr } from './subschema-match.js';
12
- /**
13
- * Returns the inline condition that is true when `accessor` is the wrong type
14
- * for the given JSON Schema primitive type.
15
- */
1
+ import { escapeRegexPattern } from "@amritk/helpers/escape-regex-pattern";
2
+ import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extension";
3
+ import { multipleOfFailExpr } from "@amritk/helpers/multiple-of-check";
4
+ import { quoteJsString } from "@amritk/helpers/quote-js-string";
5
+ import { resolveRef } from "@amritk/helpers/resolve-ref";
6
+ import { safeAccessor } from "@amritk/helpers/safe-accessor";
7
+ import { hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject } from "@amritk/helpers/schema-guards";
8
+ import { generateEnumCheck } from "./generate-enum-check.js";
9
+ import { canEnforceUnion, generateUnionCheck, getUnionBranches, isInlineObjectProperty } from "./generate-type-checks.js";
10
+ import { getPrefixItems, prefixItemsCapsLength, scalarItemTypeCheck } from "./generate-validation-expression.js";
11
+ import { subschemaMatchExpr } from "./subschema-match.js";
16
12
  const wrongTypeCondition = (accessor, type) => {
17
- switch (type) {
18
- case 'string':
19
- return `typeof ${accessor} !== "string"`;
20
- case 'number':
21
- return `typeof ${accessor} !== "number"`;
22
- case 'integer':
23
- // `integer` also rejects non-integral numbers; a bare typeof accepts `1.5`.
24
- return `(typeof ${accessor} !== "number" || !Number.isInteger(${accessor}))`;
25
- case 'boolean':
26
- return `typeof ${accessor} !== "boolean"`;
27
- case 'null':
28
- // Missing from this switch historically, which meant a null-typed
29
- // property was never enforced on the assertion path — the strict-mode
30
- // differential fuzzer caught non-null values sailing through.
31
- return `${accessor} !== null`;
32
- case 'array':
33
- return `!Array.isArray(${accessor})`;
34
- case 'object':
35
- return `!isObject(${accessor})`;
36
- default:
37
- return null;
38
- }
13
+ switch (type) {
14
+ case "string":
15
+ return `typeof ${accessor} !== "string"`;
16
+ case "number":
17
+ return `typeof ${accessor} !== "number"`;
18
+ case "integer":
19
+ return `(typeof ${accessor} !== "number" || !Number.isInteger(${accessor}))`;
20
+ case "boolean":
21
+ return `typeof ${accessor} !== "boolean"`;
22
+ case "null":
23
+ return `${accessor} !== null`;
24
+ case "array":
25
+ return `!Array.isArray(${accessor})`;
26
+ case "object":
27
+ return `!isObject(${accessor})`;
28
+ default:
29
+ return null;
30
+ }
39
31
  };
40
- /**
41
- * Maps a JSON Schema type to the label used in error messages.
42
- * `integer` collapses to `number` since both are validated via `typeof === "number"`.
43
- */
44
- const typeLabel = (type) => (type === 'integer' ? 'number' : type);
45
- /**
46
- * Emits `throw new Error(<message>[ + <suffixExpr>])`. The static message may
47
- * contain schema-controlled text (property names, patterns, enum values), so
48
- * it goes through the shared {@link quoteJsString} escape-or-quote decision.
49
- * `suffixExpr`, when given, is a runtime expression appended to the message
50
- * (e.g. `typeof input`).
51
- */
32
+ const typeLabel = (type) => type === "integer" ? "number" : type;
52
33
  const throwError = (message, suffixExpr) => {
53
- const literal = quoteJsString(message);
54
- return suffixExpr ? `throw new Error(${literal} + (${suffixExpr}))` : `throw new Error(${literal})`;
34
+ const literal = quoteJsString(message);
35
+ return suffixExpr ? `throw new Error(${literal} + (${suffixExpr}))` : `throw new Error(${literal})`;
55
36
  };
56
- /**
57
- * Generates strict-mode constraint checks for a typed property
58
- * (pattern, length, min/max, multipleOf).
59
- */
60
37
  const generateConstraintChecks = (acc, propSchema, field, context = {}) => {
61
- if (!isSchemaObject(propSchema) || !hasType(propSchema))
62
- return [];
63
- const t = propSchema.type;
64
- const lines = [];
65
- if (t === 'string') {
66
- if (hasPattern(propSchema)) {
67
- const pattern = escapeRegexPattern(propSchema.pattern);
68
- lines.push(` if (typeof ${acc} === "string" && !/${pattern}/.test(${acc})) ${throwError(`${field} must match pattern ${propSchema.pattern}`)};`);
69
- }
70
- if (hasMinLength(propSchema)) {
71
- lines.push(` if (typeof ${acc} === "string" && ${acc}.length < ${propSchema.minLength}) ${throwError(`${field} must have at least ${propSchema.minLength} characters`)};`);
72
- }
73
- if (hasMaxLength(propSchema)) {
74
- lines.push(` if (typeof ${acc} === "string" && ${acc}.length > ${propSchema.maxLength}) ${throwError(`${field} must have at most ${propSchema.maxLength} characters`)};`);
75
- }
38
+ if (!isSchemaObject(propSchema) || !hasType(propSchema))
39
+ return [];
40
+ const t = propSchema.type;
41
+ const lines = [];
42
+ if (t === "string") {
43
+ if (hasPattern(propSchema)) {
44
+ const pattern = escapeRegexPattern(propSchema.pattern);
45
+ lines.push(` if (typeof ${acc} === "string" && !/${pattern}/.test(${acc})) ${throwError(`${field} must match pattern ${propSchema.pattern}`)};`);
76
46
  }
77
- if (t === 'number' || t === 'integer') {
78
- if (hasMinimum(propSchema)) {
79
- lines.push(` if (typeof ${acc} === "number" && ${acc} < ${propSchema.minimum}) ${throwError(`${field} must be >= ${propSchema.minimum}`)};`);
80
- }
81
- if (hasMaximum(propSchema)) {
82
- lines.push(` if (typeof ${acc} === "number" && ${acc} > ${propSchema.maximum}) ${throwError(`${field} must be <= ${propSchema.maximum}`)};`);
83
- }
84
- if (hasExclusiveMinimum(propSchema)) {
85
- lines.push(` if (typeof ${acc} === "number" && ${acc} <= ${propSchema.exclusiveMinimum}) ${throwError(`${field} must be > ${propSchema.exclusiveMinimum}`)};`);
86
- }
87
- if (hasExclusiveMaximum(propSchema)) {
88
- lines.push(` if (typeof ${acc} === "number" && ${acc} >= ${propSchema.exclusiveMaximum}) ${throwError(`${field} must be < ${propSchema.exclusiveMaximum}`)};`);
89
- }
90
- if (hasMultipleOf(propSchema)) {
91
- lines.push(` if (typeof ${acc} === "number" && ${multipleOfFailExpr(acc, propSchema.multipleOf)}) ${throwError(`${field} must be a multiple of ${propSchema.multipleOf}`)};`);
92
- }
47
+ if (hasMinLength(propSchema)) {
48
+ lines.push(` if (typeof ${acc} === "string" && ${acc}.length < ${propSchema.minLength}) ${throwError(`${field} must have at least ${propSchema.minLength} characters`)};`);
93
49
  }
94
- if (t === 'array') {
95
- if (hasMinItems(propSchema)) {
96
- lines.push(` if (Array.isArray(${acc}) && ${acc}.length < ${propSchema.minItems}) ${throwError(`${field} must have at least ${propSchema.minItems} items`)};`);
97
- }
98
- if (hasMaxItems(propSchema)) {
99
- lines.push(` if (Array.isArray(${acc}) && ${acc}.length > ${propSchema.maxItems}) ${throwError(`${field} must have at most ${propSchema.maxItems} items`)};`);
100
- }
101
- if (hasUniqueItems(propSchema) && propSchema.uniqueItems === true) {
102
- // Deep-equality dedupe via JSON keys, matching the interpreter's semantics —
103
- // a plain `new Set` would only catch primitive duplicates, not equal objects.
104
- lines.push(` if (Array.isArray(${acc}) && new Set(${acc}.map((_u) => JSON.stringify(_u))).size !== ${acc}.length) ${throwError(`${field} must NOT have duplicate items`)};`);
105
- }
106
- // Item types: the fast path proves them via `.every`, but this slow path
107
- // used to check only length/uniqueness, letting e.g. a number slip into a
108
- // declared `string[]`. Enforce scalar and enum item schemas here; richer
109
- // item schemas ($refs, objects) are validated by their own parsers.
110
- const itemCheck = generateItemCheck(propSchema);
111
- if (itemCheck) {
112
- lines.push(` if (Array.isArray(${acc}) && !${acc}.every((_it) => ${itemCheck.check})) ${throwError(`${field} ${itemCheck.message}`)};`);
113
- }
114
- // Tuple `prefixItems`: assert each position and cap length under items:false.
115
- lines.push(...generatePrefixItemsAssertion(acc, field, propSchema, context.rootSchema));
50
+ if (hasMaxLength(propSchema)) {
51
+ lines.push(` if (typeof ${acc} === "string" && ${acc}.length > ${propSchema.maxLength}) ${throwError(`${field} must have at most ${propSchema.maxLength} characters`)};`);
116
52
  }
117
- return lines;
53
+ }
54
+ if (t === "number" || t === "integer") {
55
+ if (hasMinimum(propSchema)) {
56
+ lines.push(` if (typeof ${acc} === "number" && ${acc} < ${propSchema.minimum}) ${throwError(`${field} must be >= ${propSchema.minimum}`)};`);
57
+ }
58
+ if (hasMaximum(propSchema)) {
59
+ lines.push(` if (typeof ${acc} === "number" && ${acc} > ${propSchema.maximum}) ${throwError(`${field} must be <= ${propSchema.maximum}`)};`);
60
+ }
61
+ if (hasExclusiveMinimum(propSchema)) {
62
+ lines.push(` if (typeof ${acc} === "number" && ${acc} <= ${propSchema.exclusiveMinimum}) ${throwError(`${field} must be > ${propSchema.exclusiveMinimum}`)};`);
63
+ }
64
+ if (hasExclusiveMaximum(propSchema)) {
65
+ lines.push(` if (typeof ${acc} === "number" && ${acc} >= ${propSchema.exclusiveMaximum}) ${throwError(`${field} must be < ${propSchema.exclusiveMaximum}`)};`);
66
+ }
67
+ if (hasMultipleOf(propSchema)) {
68
+ lines.push(` if (typeof ${acc} === "number" && ${multipleOfFailExpr(acc, propSchema.multipleOf)}) ${throwError(`${field} must be a multiple of ${propSchema.multipleOf}`)};`);
69
+ }
70
+ }
71
+ if (t === "array") {
72
+ if (hasMinItems(propSchema)) {
73
+ lines.push(` if (Array.isArray(${acc}) && ${acc}.length < ${propSchema.minItems}) ${throwError(`${field} must have at least ${propSchema.minItems} items`)};`);
74
+ }
75
+ if (hasMaxItems(propSchema)) {
76
+ lines.push(` if (Array.isArray(${acc}) && ${acc}.length > ${propSchema.maxItems}) ${throwError(`${field} must have at most ${propSchema.maxItems} items`)};`);
77
+ }
78
+ if (hasUniqueItems(propSchema) && propSchema.uniqueItems === true) {
79
+ lines.push(` if (Array.isArray(${acc}) && new Set(${acc}.map((_u) => JSON.stringify(_u))).size !== ${acc}.length) ${throwError(`${field} must NOT have duplicate items`)};`);
80
+ }
81
+ const itemCheck = generateItemCheck(propSchema);
82
+ if (itemCheck) {
83
+ lines.push(` if (Array.isArray(${acc}) && !${acc}.every((_it) => ${itemCheck.check})) ${throwError(`${field} ${itemCheck.message}`)};`);
84
+ }
85
+ lines.push(...generatePrefixItemsAssertion(acc, field, propSchema, context.rootSchema));
86
+ }
87
+ return lines;
118
88
  };
119
- /**
120
- * Boolean per-item check (bound to `_it`) for an array schema's `items`, with
121
- * the error-message fragment describing what was expected. Only scalar types
122
- * and enums are checked — returns null for anything richer.
123
- */
124
89
  const generateItemCheck = (schema) => {
125
- if (!isSchemaObject(schema) || !hasItems(schema) || Array.isArray(schema.items))
126
- return null;
127
- const items = schema.items;
128
- if (isSchemaObject(items) && hasEnum(items) && items.enum.length > 0) {
129
- const label = items.enum.map((v) => JSON.stringify(v)).join(', ');
130
- return { check: generateEnumCheck('_it', items.enum), message: `items must be one of: ${label}` };
131
- }
132
- const scalarCheck = scalarItemTypeCheck(items, '_it');
133
- if (scalarCheck === null || !isSchemaObject(items) || !hasType(items))
134
- return null;
135
- return { check: scalarCheck, message: `items expected ${typeLabel(items.type)}` };
90
+ if (!isSchemaObject(schema) || !hasItems(schema) || Array.isArray(schema.items))
91
+ return null;
92
+ const items = schema.items;
93
+ if (isSchemaObject(items) && hasEnum(items) && items.enum.length > 0) {
94
+ const label = items.enum.map((v) => JSON.stringify(v)).join(", ");
95
+ return { check: generateEnumCheck("_it", items.enum), message: `items must be one of: ${label}` };
96
+ }
97
+ const scalarCheck = scalarItemTypeCheck(items, "_it");
98
+ if (scalarCheck === null || !isSchemaObject(items) || !hasType(items))
99
+ return null;
100
+ return { check: scalarCheck, message: `items expected ${typeLabel(items.type)}` };
136
101
  };
137
- /**
138
- * Strict-mode `contains` / `minContains` / `maxContains`: at least `minContains`
139
- * (default 1) and at most `maxContains` (default ∞) items of the array must
140
- * match the `contains` subschema. Runtime-gated on `Array.isArray` so a
141
- * non-array value is left to the type check, and wrapped in a block so the
142
- * per-array count variable never collides with a sibling check. `minContains: 0`
143
- * makes the lower bound trivially satisfied. Emits nothing when there is no
144
- * `contains` or its subschema is not matchable inline (the generation-time guard
145
- * rejects the latter, so a strict parser never silently drops the constraint).
146
- */
147
- export const generateContainsCheck = (acc, schema, label) => {
148
- if (!isSchemaObject(schema))
149
- return [];
150
- const sp = schema;
151
- if (!('contains' in sp))
152
- return [];
153
- const match = subschemaMatchExpr('_c', sp['contains']);
154
- if (match === null)
155
- return [];
156
- const min = typeof sp['minContains'] === 'number' ? sp['minContains'] : 1;
157
- const max = typeof sp['maxContains'] === 'number' ? sp['maxContains'] : undefined;
158
- const bound = max !== undefined ? `_cn < ${min} || _cn > ${max}` : `_cn < ${min}`;
159
- return [
160
- ` if (Array.isArray(${acc})) {`,
161
- ` const _cn = (${acc} as unknown[]).filter((_c) => ${match}).length;`,
162
- ` if (${bound}) ${throwError(`${label} array does not contain the required matching items`)};`,
163
- ` }`,
164
- ];
102
+ const generateContainsCheck = (acc, schema, label) => {
103
+ if (!isSchemaObject(schema))
104
+ return [];
105
+ const sp = schema;
106
+ if (!("contains" in sp))
107
+ return [];
108
+ const match = subschemaMatchExpr("_c", sp["contains"]);
109
+ if (match === null)
110
+ return [];
111
+ const min = typeof sp["minContains"] === "number" ? sp["minContains"] : 1;
112
+ const max = typeof sp["maxContains"] === "number" ? sp["maxContains"] : void 0;
113
+ const bound = max !== void 0 ? `_cn < ${min} || _cn > ${max}` : `_cn < ${min}`;
114
+ return [
115
+ ` if (Array.isArray(${acc})) {`,
116
+ ` const _cn = (${acc} as unknown[]).filter((_c) => ${match}).length;`,
117
+ ` if (${bound}) ${throwError(`${label} array does not contain the required matching items`)};`,
118
+ ` }`
119
+ ];
165
120
  };
166
- /**
167
- * Strict-mode `dependentRequired`: when a trigger key is present, every declared
168
- * dependency key must be present too.
169
- */
170
121
  const generateDependentRequiredChecks = (obj, schema, label) => {
171
- if (!hasDependentRequired(schema))
172
- return [];
173
- const lines = [];
174
- for (const [trigger, deps] of Object.entries(schema.dependentRequired)) {
175
- if (!Array.isArray(deps))
176
- continue;
177
- for (const dep of deps) {
178
- lines.push(` if (${JSON.stringify(trigger)} in ${obj} && !(${JSON.stringify(dep)} in ${obj})) ${throwError(`${label} must have property '${dep}' when '${trigger}' is present`)};`);
179
- }
122
+ if (!hasDependentRequired(schema))
123
+ return [];
124
+ const lines = [];
125
+ for (const [trigger, deps] of Object.entries(schema.dependentRequired)) {
126
+ if (!Array.isArray(deps))
127
+ continue;
128
+ for (const dep of deps) {
129
+ lines.push(` if (${JSON.stringify(trigger)} in ${obj} && !(${JSON.stringify(dep)} in ${obj})) ${throwError(`${label} must have property '${dep}' when '${trigger}' is present`)};`);
180
130
  }
181
- return lines;
131
+ }
132
+ return lines;
182
133
  };
183
- /**
184
- * Strict-mode `dependentSchemas` (2020-12): when a trigger property is present,
185
- * the *whole object* must also match the associated subschema. A `true`
186
- * subschema is a no-op; a `false` subschema makes the trigger's presence always
187
- * invalid. Object subschemas are enforced via {@link subschemaMatchExpr}; a
188
- * subschema it cannot match inline is rejected at generation time by the guard.
189
- */
190
134
  const generateDependentSchemasChecks = (obj, schema, label) => {
191
- const dep = schema['dependentSchemas'];
192
- if (typeof dep !== 'object' || dep === null || Array.isArray(dep))
193
- return [];
194
- const lines = [];
195
- for (const [trigger, sub] of Object.entries(dep)) {
196
- if (sub === true)
197
- continue;
198
- if (sub === false) {
199
- lines.push(` if (${JSON.stringify(trigger)} in ${obj}) ${throwError(`${label} must NOT have property '${trigger}'`)};`);
200
- continue;
201
- }
202
- const match = subschemaMatchExpr(obj, sub);
203
- if (match === null || match === 'true')
204
- continue;
205
- lines.push(` if (${JSON.stringify(trigger)} in ${obj} && !(${match})) ${throwError(`${label} does not satisfy the schema required when '${trigger}' is present`)};`);
135
+ const dep = schema["dependentSchemas"];
136
+ if (typeof dep !== "object" || dep === null || Array.isArray(dep))
137
+ return [];
138
+ const lines = [];
139
+ for (const [trigger, sub] of Object.entries(dep)) {
140
+ if (sub === true)
141
+ continue;
142
+ if (sub === false) {
143
+ lines.push(` if (${JSON.stringify(trigger)} in ${obj}) ${throwError(`${label} must NOT have property '${trigger}'`)};`);
144
+ continue;
206
145
  }
207
- return lines;
146
+ const match = subschemaMatchExpr(obj, sub);
147
+ if (match === null || match === "true")
148
+ continue;
149
+ lines.push(` if (${JSON.stringify(trigger)} in ${obj} && !(${match})) ${throwError(`${label} does not satisfy the schema required when '${trigger}' is present`)};`);
150
+ }
151
+ return lines;
208
152
  };
209
- /**
210
- * Resolves a tuple position's schema through a single `$ref` (via `rootSchema`)
211
- * so the assertion can read its `type`/`enum`. A `$ref` with no resolvable target
212
- * (or no root document) is returned as-is, leaving the position unasserted.
213
- */
214
153
  const resolvePositionSchema = (pos, rootSchema) => {
215
- if (isSchemaObject(pos) && hasRef(pos) && rootSchema) {
216
- const resolved = resolveRef(pos.$ref, rootSchema);
217
- if (resolved)
218
- return resolved;
219
- }
220
- return pos;
154
+ if (isSchemaObject(pos) && hasRef(pos) && rootSchema) {
155
+ const resolved = resolveRef(pos.$ref, rootSchema);
156
+ if (resolved)
157
+ return resolved;
158
+ }
159
+ return pos;
221
160
  };
222
- /**
223
- * Strict-mode assertion lines for a tuple `prefixItems`: each present position is
224
- * asserted against its subschema (a scalar type, an enum, or a `$ref`/inline
225
- * schema resolved to one via `rootSchema`), and a sibling `items: false` /
226
- * `additionalItems: false` rejects any extra element. `acc` is the array
227
- * accessor and `field` the error-message prefix (`[Type] field 'k'` or
228
- * `[Type]`). Mirrors the validators' tuple pass, but throws on the first
229
- * violation instead of collecting errors. Positions whose schema is richer than
230
- * a scalar/enum are left to their own downstream handling (like array `items`).
231
- */
232
161
  const generatePrefixItemsAssertion = (acc, field, schema, rootSchema) => {
233
- const prefix = getPrefixItems(schema);
234
- if (!prefix)
235
- return [];
236
- const lines = [];
237
- for (let i = 0; i < prefix.length; i++) {
238
- const pos = resolvePositionSchema(prefix[i], rootSchema);
239
- const el = `${acc}[${i}]`;
240
- // A shorter input simply has no element at this position — `prefixItems`
241
- // does not require presence (that is `minItems`' job), so guard on length.
242
- const present = `Array.isArray(${acc}) && ${acc}.length > ${i}`;
243
- if (isSchemaObject(pos) && hasEnum(pos) && pos.enum.length > 0) {
244
- const allowed = JSON.stringify(pos.enum);
245
- const label = pos.enum.map((v) => JSON.stringify(v)).join(', ');
246
- lines.push(` if (${present} && !(${allowed} as readonly unknown[]).includes(${el})) ${throwError(`${field}[${i}] must be one of: ${label}`)};`);
247
- continue;
248
- }
249
- if (isSchemaObject(pos) && hasType(pos)) {
250
- const pt = pos.type;
251
- const wrong = wrongTypeCondition(el, pt);
252
- if (wrong) {
253
- lines.push(` if (${present} && (${wrong})) ${throwError(`${field}[${i}] expected ${typeLabel(pt)}, got `, `typeof ${el}`)};`);
254
- }
255
- }
162
+ const prefix = getPrefixItems(schema);
163
+ if (!prefix)
164
+ return [];
165
+ const lines = [];
166
+ for (let i = 0; i < prefix.length; i++) {
167
+ const pos = resolvePositionSchema(prefix[i], rootSchema);
168
+ const el = `${acc}[${i}]`;
169
+ const present = `Array.isArray(${acc}) && ${acc}.length > ${i}`;
170
+ if (isSchemaObject(pos) && hasEnum(pos) && pos.enum.length > 0) {
171
+ const allowed = JSON.stringify(pos.enum);
172
+ const label = pos.enum.map((v) => JSON.stringify(v)).join(", ");
173
+ lines.push(` if (${present} && !(${allowed} as readonly unknown[]).includes(${el})) ${throwError(`${field}[${i}] must be one of: ${label}`)};`);
174
+ continue;
256
175
  }
257
- if (prefixItemsCapsLength(schema)) {
258
- lines.push(` if (Array.isArray(${acc}) && ${acc}.length > ${prefix.length}) ${throwError(`${field} must NOT have more than ${prefix.length} items`)};`);
176
+ if (isSchemaObject(pos) && hasType(pos)) {
177
+ const pt = pos.type;
178
+ const wrong = wrongTypeCondition(el, pt);
179
+ if (wrong) {
180
+ lines.push(` if (${present} && (${wrong})) ${throwError(`${field}[${i}] expected ${typeLabel(pt)}, got `, `typeof ${el}`)};`);
181
+ }
259
182
  }
260
- return lines;
183
+ }
184
+ if (prefixItemsCapsLength(schema)) {
185
+ lines.push(` if (Array.isArray(${acc}) && ${acc}.length > ${prefix.length}) ${throwError(`${field} must NOT have more than ${prefix.length} items`)};`);
186
+ }
187
+ return lines;
261
188
  };
262
- /**
263
- * Strict-mode `propertyNames`: every own key of the object (keys are always
264
- * strings) must satisfy the name subschema. A trivially-true matcher imposes no
265
- * constraint, so no loop is emitted; an unmatchable subschema is rejected at
266
- * generation time by the guard.
267
- */
268
189
  const generatePropertyNameChecks = (obj, schema, label) => {
269
- if (!hasPropertyNames(schema) || !isSchemaObject(schema.propertyNames))
270
- return [];
271
- const match = subschemaMatchExpr('_name', schema.propertyNames);
272
- if (match === null || match === 'true')
273
- return [];
274
- return [
275
- ` for (const _name of Object.keys(${obj})) {`,
276
- ` if (!(${match})) ${throwError(`${label} invalid property name: `, '_name')};`,
277
- ` }`,
278
- ];
190
+ if (!hasPropertyNames(schema) || !isSchemaObject(schema.propertyNames))
191
+ return [];
192
+ const match = subschemaMatchExpr("_name", schema.propertyNames);
193
+ if (match === null || match === "true")
194
+ return [];
195
+ return [
196
+ ` for (const _name of Object.keys(${obj})) {`,
197
+ ` if (!(${match})) ${throwError(`${label} invalid property name: `, "_name")};`,
198
+ ` }`
199
+ ];
279
200
  };
280
- /**
281
- * Emits the object-level keyword checks (`dependentRequired`, `dependentSchemas`,
282
- * `propertyNames`) for the object reached by `obj`. When `guard` is true the
283
- * whole block is wrapped in a runtime `isObject` check — used for object-typed
284
- * *properties*, whose value may be absent or a non-object (the type check
285
- * reports that separately). The root object and inline sub-parsers pass
286
- * `guard = false` because the enclosing parser has already proven the value is
287
- * an object.
288
- */
289
- export const generateObjectKeywordChecks = (obj, schema, label, guard) => {
290
- if (!isSchemaObject(schema))
291
- return [];
292
- const inner = [
293
- ...generateDependentRequiredChecks(obj, schema, label),
294
- ...generateDependentSchemasChecks(obj, schema, label),
295
- ...generatePropertyNameChecks(obj, schema, label),
296
- ];
297
- if (inner.length === 0)
298
- return [];
299
- if (!guard)
300
- return inner;
301
- return [` if (isObject(${obj})) {`, ...inner.map((line) => ` ${line}`), ` }`];
201
+ const generateObjectKeywordChecks = (obj, schema, label, guard) => {
202
+ if (!isSchemaObject(schema))
203
+ return [];
204
+ const inner = [
205
+ ...generateDependentRequiredChecks(obj, schema, label),
206
+ ...generateDependentSchemasChecks(obj, schema, label),
207
+ ...generatePropertyNameChecks(obj, schema, label)
208
+ ];
209
+ if (inner.length === 0)
210
+ return [];
211
+ if (!guard)
212
+ return inner;
213
+ return [` if (isObject(${obj})) {`, ...inner.map((line) => ` ${line}`), ` }`];
302
214
  };
303
- /**
304
- * Generates strict-mode lines for a single property of an object schema.
305
- * Properties with a `$ref` are skipped here — the nested parser handles its own
306
- * strict check when called from the parent's slow path.
307
- */
308
215
  const generatePropertyAssertion = (key, propSchema, isRequired, typeName, context = {}) => {
309
- const acc = safeAccessor('input', key);
310
- const field = `[${typeName}] field '${key}'`;
311
- const lines = [];
312
- if (isRequired) {
313
- lines.push(` if (!(${JSON.stringify(key)} in input)) ${throwError(`[${typeName}] missing required property '${key}'`)};`);
314
- }
315
- if (!isSchemaObject(propSchema))
316
- return lines;
317
- if (hasRef(propSchema))
318
- return lines;
319
- // Union properties: enforce membership when every branch check is
320
- // false-sound (canEnforceUnion), so a value matching no variant throws
321
- // instead of passing through untyped. Left unenforced (pass-through, the
322
- // historical behavior) when any branch is too complex to check safely.
323
- const unionBranches = getUnionBranches(propSchema);
324
- if (unionBranches) {
325
- if (!context.stripUnknown && canEnforceUnion(unionBranches, context.rootSchema)) {
326
- const check = generateUnionCheck(acc, unionBranches, context.useRefImports ?? false, context.suffix ?? '');
327
- if (check !== null) {
328
- const failure = throwError(`${field} does not match any allowed variant`);
329
- lines.push(isRequired ? ` if (!(${check})) ${failure};` : ` if (${acc} !== undefined && !(${check})) ${failure};`);
330
- }
331
- }
332
- return lines;
333
- }
334
- const instanceOf = getMjstInstanceOf(propSchema);
335
- if (instanceOf) {
336
- if (isRequired) {
337
- lines.push(` if (!(${acc} instanceof ${instanceOf})) ${throwError(`${field} must be ${instanceOf}`)};`);
338
- }
339
- else {
340
- lines.push(` if (${acc} !== undefined && !(${acc} instanceof ${instanceOf})) ${throwError(`${field} must be ${instanceOf}`)};`);
341
- }
342
- return lines;
343
- }
344
- const primitive = getMjstPrimitive(propSchema);
345
- if (primitive) {
346
- if (isRequired) {
347
- lines.push(` if (typeof ${acc} !== "${primitive}") ${throwError(`${field} must be ${primitive}`)};`);
348
- }
349
- else {
350
- lines.push(` if (${acc} !== undefined && typeof ${acc} !== "${primitive}") ${throwError(`${field} must be ${primitive}`)};`);
351
- }
352
- return lines;
353
- }
354
- if (hasEnum(propSchema)) {
355
- const allowed = JSON.stringify(propSchema.enum);
356
- const label = propSchema.enum.map((v) => JSON.stringify(v)).join(', ');
357
- if (isRequired) {
358
- lines.push(` if (!(${allowed} as readonly unknown[]).includes(${acc})) ${throwError(`${field} must be one of: ${label}`)};`);
359
- }
360
- else {
361
- lines.push(` if (${acc} !== undefined && !(${allowed} as readonly unknown[]).includes(${acc})) ${throwError(`${field} must be one of: ${label}`)};`);
362
- }
363
- return lines;
364
- }
365
- if (hasType(propSchema)) {
366
- const t = propSchema.type;
367
- const wrongType = wrongTypeCondition(acc, t);
368
- if (wrongType) {
369
- const expected = throwError(`${field} expected ${typeLabel(t)}, got `, `typeof ${acc}`);
370
- if (isRequired) {
371
- lines.push(` if (${wrongType}) ${expected};`);
372
- }
373
- else {
374
- lines.push(` if (${acc} !== undefined && (${wrongType})) ${expected};`);
375
- }
376
- }
377
- lines.push(...generateConstraintChecks(acc, propSchema, field, context));
216
+ const acc = safeAccessor("input", key);
217
+ const field = `[${typeName}] field '${key}'`;
218
+ const lines = [];
219
+ if (isRequired) {
220
+ lines.push(` if (!(${JSON.stringify(key)} in input)) ${throwError(`[${typeName}] missing required property '${key}'`)};`);
221
+ }
222
+ if (!isSchemaObject(propSchema))
223
+ return lines;
224
+ if (hasRef(propSchema))
225
+ return lines;
226
+ const unionBranches = getUnionBranches(propSchema);
227
+ if (unionBranches) {
228
+ if (!context.stripUnknown && canEnforceUnion(unionBranches, context.rootSchema)) {
229
+ const check = generateUnionCheck(acc, unionBranches, context.useRefImports ?? false, context.suffix ?? "");
230
+ if (check !== null) {
231
+ const failure = throwError(`${field} does not match any allowed variant`);
232
+ lines.push(isRequired ? ` if (!(${check})) ${failure};` : ` if (${acc} !== undefined && !(${check})) ${failure};`);
233
+ }
378
234
  }
379
- // Type-independent constraints, each runtime-gated on the value's shape:
380
- // - `contains` asserts on arrays (no-op otherwise),
381
- // - the object-level keywords assert on objects.
382
- // Gated on a single `isSchemaObject` + a few `in` checks so a plain scalar
383
- // property — the common case — skips the costlier `isInlineObjectProperty`
384
- // probe and the check builders entirely. Inline object properties are
385
- // deep-validated by their own sub-parser (which runs these same object-level
386
- // checks), so emitting them here too would be premature and redundant.
387
- if (isSchemaObject(propSchema)) {
388
- const r = propSchema;
389
- if ('contains' in r)
390
- lines.push(...generateContainsCheck(acc, propSchema, field));
391
- if (('dependentRequired' in r || 'dependentSchemas' in r || 'propertyNames' in r) &&
392
- !isInlineObjectProperty(propSchema)) {
393
- lines.push(...generateObjectKeywordChecks(acc, propSchema, field, true));
394
- }
235
+ return lines;
236
+ }
237
+ const instanceOf = getMjstInstanceOf(propSchema);
238
+ if (instanceOf) {
239
+ if (isRequired) {
240
+ lines.push(` if (!(${acc} instanceof ${instanceOf})) ${throwError(`${field} must be ${instanceOf}`)};`);
241
+ } else {
242
+ lines.push(` if (${acc} !== undefined && !(${acc} instanceof ${instanceOf})) ${throwError(`${field} must be ${instanceOf}`)};`);
395
243
  }
396
244
  return lines;
397
- };
398
- /**
399
- * Generates strict-mode assertions for the body of an object parser.
400
- * Throws on:
401
- * - non-object input
402
- * - missing required property
403
- * - property of the wrong primitive type
404
- * - enum / pattern / length / min / max / multipleOf violations
405
- *
406
- * Properties with a `$ref` are validated by the nested parser's own strict
407
- * check when that parser is invoked downstream.
408
- */
409
- export const generateObjectStrictAssertion = (schema, typeName, context = {}) => {
410
- const lines = [];
411
- lines.push(` if (!isObject(input)) ${throwError(`[${typeName}] expected object, got `, 'input === null ? "null" : typeof input')};`);
412
- if (!isSchemaObject(schema))
413
- return lines;
414
- if (hasProperties(schema)) {
415
- const required = new Set(hasRequired(schema) ? schema.required : []);
416
- const props = schema.properties;
417
- for (const key in props) {
418
- lines.push(...generatePropertyAssertion(key, props[key], required.has(key), typeName, context));
419
- }
245
+ }
246
+ const primitive = getMjstPrimitive(propSchema);
247
+ if (primitive) {
248
+ if (isRequired) {
249
+ lines.push(` if (typeof ${acc} !== "${primitive}") ${throwError(`${field} must be ${primitive}`)};`);
250
+ } else {
251
+ lines.push(` if (${acc} !== undefined && typeof ${acc} !== "${primitive}") ${throwError(`${field} must be ${primitive}`)};`);
420
252
  }
421
- // Object-level keywords for the object itself. `input` is already proven an
422
- // object above, so no runtime guard is needed. Emitted even when the schema
423
- // has no `properties` (e.g. a constrained-key map: `{ type: 'object',
424
- // propertyNames: {...} }`). Gated on a cheap presence check so a keyword-free
425
- // object skips the three check builders.
426
- const sr = schema;
427
- if ('dependentRequired' in sr || 'dependentSchemas' in sr || 'propertyNames' in sr) {
428
- lines.push(...generateObjectKeywordChecks('input', schema, `[${typeName}]`, false));
253
+ return lines;
254
+ }
255
+ if (hasEnum(propSchema)) {
256
+ const allowed = JSON.stringify(propSchema.enum);
257
+ const label = propSchema.enum.map((v) => JSON.stringify(v)).join(", ");
258
+ if (isRequired) {
259
+ lines.push(` if (!(${allowed} as readonly unknown[]).includes(${acc})) ${throwError(`${field} must be one of: ${label}`)};`);
260
+ } else {
261
+ lines.push(` if (${acc} !== undefined && !(${allowed} as readonly unknown[]).includes(${acc})) ${throwError(`${field} must be one of: ${label}`)};`);
429
262
  }
430
263
  return lines;
431
- };
432
- /**
433
- * Generates a single strict-mode assertion line for a non-object scalar parser.
434
- * Returns null when the schema has no type information to assert on.
435
- */
436
- export const generateScalarStrictAssertion = (schema, typeName, rootSchema) => {
437
- const got = 'input === null ? "null" : typeof input';
438
- const instanceOf = getMjstInstanceOf(schema);
439
- if (instanceOf) {
440
- return ` if (!(input instanceof ${instanceOf})) ${throwError(`[${typeName}] expected ${instanceOf}, got `, got)};`;
264
+ }
265
+ if (hasType(propSchema)) {
266
+ const t = propSchema.type;
267
+ const wrongType = wrongTypeCondition(acc, t);
268
+ if (wrongType) {
269
+ const expected = throwError(`${field} expected ${typeLabel(t)}, got `, `typeof ${acc}`);
270
+ if (isRequired) {
271
+ lines.push(` if (${wrongType}) ${expected};`);
272
+ } else {
273
+ lines.push(` if (${acc} !== undefined && (${wrongType})) ${expected};`);
274
+ }
441
275
  }
442
- const primitive = getMjstPrimitive(schema);
443
- if (primitive) {
444
- return ` if (typeof input !== "${primitive}") ${throwError(`[${typeName}] expected ${primitive}, got `, got)};`;
276
+ lines.push(...generateConstraintChecks(acc, propSchema, field, context));
277
+ }
278
+ if (isSchemaObject(propSchema)) {
279
+ const r = propSchema;
280
+ if ("contains" in r)
281
+ lines.push(...generateContainsCheck(acc, propSchema, field));
282
+ if (("dependentRequired" in r || "dependentSchemas" in r || "propertyNames" in r) && !isInlineObjectProperty(propSchema)) {
283
+ lines.push(...generateObjectKeywordChecks(acc, propSchema, field, true));
445
284
  }
446
- if (!isSchemaObject(schema))
447
- return null;
448
- const label = `[${typeName}]`;
449
- const lines = [];
450
- // `const` / `enum` apply to a root value regardless of a declared `type` — a
451
- // root `{ enum: [...] }` or `{ const: ... }` must reject a non-member, not
452
- // silently coerce it. `const` is compared through `JSON.stringify` so it works
453
- // for any JSON value; the value must match exactly.
454
- if (hasConst(schema)) {
455
- const wanted = JSON.stringify(JSON.stringify(schema.const));
456
- lines.push(` if (JSON.stringify(input) !== ${wanted}) ${throwError(`${label} must be ${JSON.stringify(schema.const)}`)};`);
285
+ }
286
+ return lines;
287
+ };
288
+ const generateObjectStrictAssertion = (schema, typeName, context = {}) => {
289
+ const lines = [];
290
+ lines.push(` if (!isObject(input)) ${throwError(`[${typeName}] expected object, got `, 'input === null ? "null" : typeof input')};`);
291
+ if (!isSchemaObject(schema))
292
+ return lines;
293
+ if (hasProperties(schema)) {
294
+ const required = new Set(hasRequired(schema) ? schema.required : []);
295
+ const props = schema.properties;
296
+ for (const key in props) {
297
+ lines.push(...generatePropertyAssertion(key, props[key], required.has(key), typeName, context));
457
298
  }
458
- else if (hasEnum(schema)) {
459
- const allowed = JSON.stringify(schema.enum);
460
- const enumLabel = schema.enum.map((v) => JSON.stringify(v)).join(', ');
461
- lines.push(` if (!(${allowed} as readonly unknown[]).includes(input)) ${throwError(`${label} must be one of: ${enumLabel}`)};`);
299
+ }
300
+ const sr = schema;
301
+ if ("dependentRequired" in sr || "dependentSchemas" in sr || "propertyNames" in sr) {
302
+ lines.push(...generateObjectKeywordChecks("input", schema, `[${typeName}]`, false));
303
+ }
304
+ return lines;
305
+ };
306
+ const generateScalarStrictAssertion = (schema, typeName, rootSchema) => {
307
+ const got = 'input === null ? "null" : typeof input';
308
+ const instanceOf = getMjstInstanceOf(schema);
309
+ if (instanceOf) {
310
+ return ` if (!(input instanceof ${instanceOf})) ${throwError(`[${typeName}] expected ${instanceOf}, got `, got)};`;
311
+ }
312
+ const primitive = getMjstPrimitive(schema);
313
+ if (primitive) {
314
+ return ` if (typeof input !== "${primitive}") ${throwError(`[${typeName}] expected ${primitive}, got `, got)};`;
315
+ }
316
+ if (!isSchemaObject(schema))
317
+ return null;
318
+ const label = `[${typeName}]`;
319
+ const lines = [];
320
+ if (hasConst(schema)) {
321
+ const wanted = JSON.stringify(JSON.stringify(schema.const));
322
+ lines.push(` if (JSON.stringify(input) !== ${wanted}) ${throwError(`${label} must be ${JSON.stringify(schema.const)}`)};`);
323
+ } else if (hasEnum(schema)) {
324
+ const allowed = JSON.stringify(schema.enum);
325
+ const enumLabel = schema.enum.map((v) => JSON.stringify(v)).join(", ");
326
+ lines.push(` if (!(${allowed} as readonly unknown[]).includes(input)) ${throwError(`${label} must be one of: ${enumLabel}`)};`);
327
+ }
328
+ if (hasType(schema)) {
329
+ const t = schema.type;
330
+ const wrongType = wrongTypeCondition("input", t);
331
+ if (wrongType) {
332
+ lines.push(` if (${wrongType}) ${throwError(`${label} expected ${typeLabel(t)}, got `, got)};`);
462
333
  }
463
- if (hasType(schema)) {
464
- const t = schema.type;
465
- const wrongType = wrongTypeCondition('input', t);
466
- if (wrongType) {
467
- lines.push(` if (${wrongType}) ${throwError(`${label} expected ${typeLabel(t)}, got `, got)};`);
468
- }
469
- // Root-level arrays enforce scalar/enum item types too — the same gap the
470
- // property path closes in generateConstraintChecks — plus `contains`.
471
- if (t === 'array') {
472
- const itemCheck = generateItemCheck(schema);
473
- if (itemCheck) {
474
- lines.push(` if (!(input as readonly unknown[]).every((_it) => ${itemCheck.check})) ${throwError(`${label} ${itemCheck.message}`)};`);
475
- }
476
- lines.push(...generateContainsCheck('input', schema, label));
477
- // Tuple `prefixItems`: assert each position and cap length under items:false.
478
- lines.push(...generatePrefixItemsAssertion('input', label, schema, rootSchema));
479
- }
480
- // String (pattern, min/maxLength), number/integer (bounds, multipleOf) and
481
- // array (length, uniqueItems) constraints — previously enforced only for
482
- // named properties, so a root scalar accepted e.g. `{type:'string',minLength:5}`
483
- // violations.
484
- lines.push(...generateConstraintChecks('input', schema, label, rootSchema ? { rootSchema } : {}));
334
+ if (t === "array") {
335
+ const itemCheck = generateItemCheck(schema);
336
+ if (itemCheck) {
337
+ lines.push(` if (!(input as readonly unknown[]).every((_it) => ${itemCheck.check})) ${throwError(`${label} ${itemCheck.message}`)};`);
338
+ }
339
+ lines.push(...generateContainsCheck("input", schema, label));
340
+ lines.push(...generatePrefixItemsAssertion("input", label, schema, rootSchema));
485
341
  }
486
- return lines.length > 0 ? lines.join('\n') : null;
342
+ lines.push(...generateConstraintChecks("input", schema, label, rootSchema ? { rootSchema } : {}));
343
+ }
344
+ return lines.length > 0 ? lines.join("\n") : null;
345
+ };
346
+ export {
347
+ generateContainsCheck,
348
+ generateObjectKeywordChecks,
349
+ generateObjectStrictAssertion,
350
+ generateScalarStrictAssertion
487
351
  };