@amritk/generate-parsers 0.16.3 → 0.17.1

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,471 +1,332 @@
1
- import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
2
- import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
3
- import { multipleOfPassExpr } from '@amritk/helpers/multiple-of-check';
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, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject, } from '@amritk/helpers/schema-guards';
7
- import { findDiscriminator } from '../helpers/find-discriminator.js';
8
- import { getDefaultValue } from '../helpers/get-default-value.js';
9
- import { generateDiscriminatedUnionValidation } from './generate-discriminated-union-validation.js';
10
- import { generateEnumCaseInsensitiveCoercion, generateEnumCheck } from './generate-enum-check.js';
11
- import { generateNonDiscriminatedUnionValidation } from './generate-non-discriminated-union-validation.js';
12
- import { generateSchemaChecks } from './generate-schema-checks.js';
13
- /**
14
- * Coercion expression for an `x-mjst` instanceOf value, when one is known.
15
- * `Date` can be reconstructed from the common JSON encodings (ISO string or
16
- * epoch number); other classes have no safe generic coercion, so we return null
17
- * and the caller falls back to the default.
18
- */
1
+ import { escapeRegexPattern } from "@amritk/helpers/escape-regex-pattern";
2
+ import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extension";
3
+ import { multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
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, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject } from "@amritk/helpers/schema-guards";
7
+ import { findDiscriminator } from "../helpers/find-discriminator.js";
8
+ import { getDefaultValue } from "../helpers/get-default-value.js";
9
+ import { generateDiscriminatedUnionValidation } from "./generate-discriminated-union-validation.js";
10
+ import { generateEnumCaseInsensitiveCoercion, generateEnumCheck } from "./generate-enum-check.js";
11
+ import { generateNonDiscriminatedUnionValidation } from "./generate-non-discriminated-union-validation.js";
12
+ import { generateSchemaChecks } from "./generate-schema-checks.js";
19
13
  const getInstanceCoercion = (accessor, instanceOf, defaultValue) => {
20
- // A bare `new Date(bad)` yields an `Invalid Date` — an object that is
21
- // `instanceof Date` yet whose every operation is `NaN`, silently poisoning
22
- // downstream code. Construct once, then fall back to the default when the
23
- // result is invalid. This sits on the coercion (failure) branch only, so the
24
- // hot path never pays for it.
25
- if (instanceOf === 'Date')
26
- return `((_d) => Number.isNaN(_d.getTime()) ? ${defaultValue} : _d)(new Date(${accessor} as string | number | Date))`;
27
- return null;
14
+ if (instanceOf === "Date")
15
+ return `((_d) => Number.isNaN(_d.getTime()) ? ${defaultValue} : _d)(new Date(${accessor} as string | number | Date))`;
16
+ return null;
28
17
  };
29
- /**
30
- * A boolean type check for an array element whose schema is a single scalar type
31
- * (the cases the element coercion below can handle). `integer` is treated as
32
- * `number` because the generated TS element type is `number`. Returns `null` for
33
- * objects, arrays, unions, `$ref`s, and anything richer.
34
- */
35
- export const scalarItemTypeCheck = (itemSchema, accessor) => {
36
- if (!isSchemaObject(itemSchema) || !hasType(itemSchema) || hasEnum(itemSchema))
37
- return null;
38
- switch (itemSchema.type) {
39
- case 'string':
40
- return `typeof ${accessor} === "string"`;
41
- case 'number':
42
- return `typeof ${accessor} === "number"`;
43
- case 'integer':
44
- // `integer` items reject non-integral numbers; a bare typeof accepts `1.5`.
45
- return `typeof ${accessor} === "number" && Number.isInteger(${accessor})`;
46
- case 'boolean':
47
- return `typeof ${accessor} === "boolean"`;
48
- case 'null':
49
- return `${accessor} === null`;
50
- default:
51
- return null;
52
- }
18
+ const scalarItemTypeCheck = (itemSchema, accessor) => {
19
+ if (!isSchemaObject(itemSchema) || !hasType(itemSchema) || hasEnum(itemSchema))
20
+ return null;
21
+ switch (itemSchema.type) {
22
+ case "string":
23
+ return `typeof ${accessor} === "string"`;
24
+ case "number":
25
+ return `typeof ${accessor} === "number"`;
26
+ case "integer":
27
+ return `typeof ${accessor} === "number" && Number.isInteger(${accessor})`;
28
+ case "boolean":
29
+ return `typeof ${accessor} === "boolean"`;
30
+ case "null":
31
+ return `${accessor} === null`;
32
+ default:
33
+ return null;
34
+ }
53
35
  };
54
- /**
55
- * True when an array's `items` schema can be coerced element-by-element by the
56
- * mapping slow path: single scalar types (via {@link scalarItemTypeCheck}) and
57
- * enums (whose validation expression coerces a non-member to the first member).
58
- * Anything richer (objects, unions, `$ref`s) needs a real item parser instead.
59
- */
60
- export const isCoercibleItemSchema = (itemSchema) => {
61
- if (scalarItemTypeCheck(itemSchema, '_it') !== null)
62
- return true;
63
- return isSchemaObject(itemSchema) && hasEnum(itemSchema) && itemSchema.enum.length > 0;
36
+ const isCoercibleItemSchema = (itemSchema) => {
37
+ if (scalarItemTypeCheck(itemSchema, "_it") !== null)
38
+ return true;
39
+ return isSchemaObject(itemSchema) && hasEnum(itemSchema) && itemSchema.enum.length > 0;
64
40
  };
65
- /**
66
- * The tuple `prefixItems` array when the schema declares one, else `null`.
67
- * `prefixItems` (JSON Schema 2020-12) positions each element against its own
68
- * subschema; the draft array-form `items` is handled by the existing item
69
- * paths, not here.
70
- */
71
- export const getPrefixItems = (schema) => {
72
- if (!isSchemaObject(schema))
73
- return null;
74
- const prefix = schema['prefixItems'];
75
- return Array.isArray(prefix) ? prefix : null;
41
+ const getPrefixItems = (schema) => {
42
+ if (!isSchemaObject(schema))
43
+ return null;
44
+ const prefix = schema["prefixItems"];
45
+ return Array.isArray(prefix) ? prefix : null;
76
46
  };
77
- /**
78
- * True when a sibling `items: false` (2020-12) or `additionalItems: false`
79
- * (draft) caps a tuple's length — any element past the declared prefix is
80
- * disallowed, so the coercer drops it and strict mode rejects it.
81
- */
82
- export const prefixItemsCapsLength = (schema) => {
83
- if (!isSchemaObject(schema))
84
- return false;
85
- const s = schema;
86
- return s['items'] === false || s['additionalItems'] === false;
47
+ const prefixItemsCapsLength = (schema) => {
48
+ if (!isSchemaObject(schema))
49
+ return false;
50
+ const s = schema;
51
+ return s["items"] === false || s["additionalItems"] === false;
87
52
  };
88
- /**
89
- * A boolean type check for a single JSON Schema primitive type name. Unlike
90
- * {@link scalarItemTypeCheck} this also covers `array`/`object` (as inline shape
91
- * checks, so no `isObject` import is assumed) and enforces `integer` with
92
- * `Number.isInteger` — a bare `typeof === "number"` accepts `1.5`. Used to build
93
- * the disjunction for an array-form `type` (e.g. `["string","null"]`).
94
- */
95
53
  const singleTypeCheck = (accessor, type) => {
96
- switch (type) {
97
- case 'string':
98
- return `typeof ${accessor} === "string"`;
99
- case 'number':
100
- return `typeof ${accessor} === "number"`;
101
- case 'integer':
102
- return `typeof ${accessor} === "number" && Number.isInteger(${accessor})`;
103
- case 'boolean':
104
- return `typeof ${accessor} === "boolean"`;
105
- case 'null':
106
- return `${accessor} === null`;
107
- case 'array':
108
- return `Array.isArray(${accessor})`;
109
- case 'object':
110
- return `typeof ${accessor} === "object" && ${accessor} !== null && !Array.isArray(${accessor})`;
111
- default:
112
- return null;
113
- }
54
+ switch (type) {
55
+ case "string":
56
+ return `typeof ${accessor} === "string"`;
57
+ case "number":
58
+ return `typeof ${accessor} === "number"`;
59
+ case "integer":
60
+ return `typeof ${accessor} === "number" && Number.isInteger(${accessor})`;
61
+ case "boolean":
62
+ return `typeof ${accessor} === "boolean"`;
63
+ case "null":
64
+ return `${accessor} === null`;
65
+ case "array":
66
+ return `Array.isArray(${accessor})`;
67
+ case "object":
68
+ return `typeof ${accessor} === "object" && ${accessor} !== null && !Array.isArray(${accessor})`;
69
+ default:
70
+ return null;
71
+ }
114
72
  };
115
- /**
116
- * Returns the list of type names when `schema.type` is an array (the JSON Schema
117
- * multi-type / nullable idiom, e.g. `["string","null"]`), or `null` for a single
118
- * or absent type. Multi-type is validated as a *disjunction* of per-type checks.
119
- */
120
73
  const getTypeArray = (schema) => {
121
- if (!isSchemaObject(schema) || !('type' in schema) || !Array.isArray(schema.type))
122
- return null;
123
- return schema.type;
74
+ if (!isSchemaObject(schema) || !("type" in schema) || !Array.isArray(schema.type))
75
+ return null;
76
+ return schema.type;
124
77
  };
125
- /**
126
- * Generates a type coercion expression for converting a value to the expected type.
127
- */
128
78
  const getTypeCoercion = (accessor, schema, defaultValue) => {
129
- if (!hasType(schema)) {
130
- return null;
79
+ if (!hasType(schema)) {
80
+ return null;
81
+ }
82
+ switch (schema.type) {
83
+ case "string":
84
+ return `String(${accessor})`;
85
+ case "number": {
86
+ const n = `Number(${accessor})`;
87
+ return `(Number.isFinite(${n}) ? ${n} : ${defaultValue})`;
131
88
  }
132
- switch (schema.type) {
133
- case 'string':
134
- return `String(${accessor})`;
135
- case 'number': {
136
- // Number() of a non-numeric string produces NaN, which silently poisons
137
- // arithmetic. Guard with Number.isFinite and fall back to the default.
138
- const n = `Number(${accessor})`;
139
- return `(Number.isFinite(${n}) ? ${n} : ${defaultValue})`;
140
- }
141
- case 'integer': {
142
- // An `integer` must coerce to a whole number, or the "repaired" value still
143
- // fails validation (a bare `Number.isFinite` accepts `1.5`). Require
144
- // integrality and fall back to the default otherwise — matching the
145
- // root-level integer parser, which yields the default for a non-integer.
146
- const n = `Number(${accessor})`;
147
- return `(Number.isInteger(${n}) ? ${n} : ${defaultValue})`;
148
- }
149
- case 'boolean':
150
- return `Boolean(${accessor})`;
151
- case 'array':
152
- return `[]`;
153
- case 'object':
154
- // Fall back to the schema's default (which fills required properties)
155
- // rather than a bare `{}`, so a coerced object is a valid instance.
156
- return `typeof ${accessor} === "object" && ${accessor} !== null ? ${accessor} : ${defaultValue}`;
157
- default:
158
- return null;
89
+ case "integer": {
90
+ const n = `Number(${accessor})`;
91
+ return `(Number.isInteger(${n}) ? ${n} : ${defaultValue})`;
159
92
  }
93
+ case "boolean":
94
+ return `Boolean(${accessor})`;
95
+ case "array":
96
+ return `[]`;
97
+ case "object":
98
+ return `typeof ${accessor} === "object" && ${accessor} !== null ? ${accessor} : ${defaultValue}`;
99
+ default:
100
+ return null;
101
+ }
160
102
  };
161
- /**
162
- * Generates a validation expression that checks if a value matches the expected type.
163
- * Returns the value if valid, coerces to the correct type if invalid, or uses default if required and missing.
164
- * Supports type checking, pattern validation, enum validation, union types, and $ref resolution.
165
- */
166
- export const generateValidationExpression = (key, schema, defaultValue, isRequired, rootSchema, visitedRefs, accessorOverride, knownNotUndefined, caseInsensitive) => {
167
- const accessor = accessorOverride ?? safeAccessor('input?', key);
168
- const checks = [];
169
- if (!isSchemaObject(schema)) {
170
- return isRequired ? `${accessor} ?? ${defaultValue}` : accessor;
103
+ const generateValidationExpression = (key, schema, defaultValue, isRequired, rootSchema, visitedRefs, accessorOverride, knownNotUndefined, caseInsensitive) => {
104
+ const accessor = accessorOverride ?? safeAccessor("input?", key);
105
+ const checks = [];
106
+ if (!isSchemaObject(schema)) {
107
+ return isRequired ? `${accessor} ?? ${defaultValue}` : accessor;
108
+ }
109
+ const instanceOf = getMjstInstanceOf(schema);
110
+ if (instanceOf) {
111
+ const valid = `${accessor} instanceof ${instanceOf}`;
112
+ if (isRequired) {
113
+ const coercion2 = getInstanceCoercion(accessor, instanceOf, defaultValue);
114
+ return coercion2 ? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion2} : ${defaultValue})` : `${valid} ? ${accessor} : ${defaultValue}`;
171
115
  }
172
- // Handle x-mjst instanceOf (e.g. Date): the value must be an instance of the
173
- // named class. Invalid values are coerced when a coercer is known, otherwise
174
- // they fall back to the default (required) or undefined (optional).
175
- const instanceOf = getMjstInstanceOf(schema);
176
- if (instanceOf) {
177
- const valid = `${accessor} instanceof ${instanceOf}`;
178
- // The coercer's own invalid-value fallback matches the branch it sits on:
179
- // the default for a required property, `undefined` for an optional one.
180
- if (isRequired) {
181
- const coercion = getInstanceCoercion(accessor, instanceOf, defaultValue);
182
- return coercion
183
- ? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion} : ${defaultValue})`
184
- : `${valid} ? ${accessor} : ${defaultValue}`;
185
- }
186
- const coercion = getInstanceCoercion(accessor, instanceOf, 'undefined');
187
- return coercion
188
- ? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion} : undefined)`
189
- : `${valid} ? ${accessor} : undefined`;
116
+ const coercion = getInstanceCoercion(accessor, instanceOf, "undefined");
117
+ return coercion ? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion} : undefined)` : `${valid} ? ${accessor} : undefined`;
118
+ }
119
+ const primitive = getMjstPrimitive(schema);
120
+ if (primitive) {
121
+ const valid = `typeof ${accessor} === "${primitive}"`;
122
+ return isRequired ? `${valid} ? ${accessor} : ${defaultValue}` : `${valid} ? ${accessor} : undefined`;
123
+ }
124
+ if (hasRef(schema) && rootSchema) {
125
+ const ref = schema.$ref;
126
+ if (visitedRefs?.has(ref)) {
127
+ return isRequired ? `${accessor} ?? ${defaultValue}` : accessor;
128
+ }
129
+ const resolvedSchema = resolveRef(ref, rootSchema);
130
+ if (resolvedSchema) {
131
+ const visited = new Set(visitedRefs);
132
+ visited.add(ref);
133
+ return generateValidationExpression(key, resolvedSchema, defaultValue, isRequired, rootSchema, visited, void 0, void 0, caseInsensitive);
134
+ }
135
+ }
136
+ if (hasOneOf(schema) && schema.oneOf.length > 0) {
137
+ const discriminator = findDiscriminator(schema.oneOf);
138
+ if (discriminator) {
139
+ return generateDiscriminatedUnionValidation(accessor, schema.oneOf, discriminator, defaultValue, isRequired);
140
+ }
141
+ return generateNonDiscriminatedUnionValidation(accessor, schema.oneOf, defaultValue, isRequired);
142
+ }
143
+ if (hasAnyOf(schema) && schema.anyOf.length > 0) {
144
+ const discriminator = findDiscriminator(schema.anyOf);
145
+ if (discriminator) {
146
+ return generateDiscriminatedUnionValidation(accessor, schema.anyOf, discriminator, defaultValue, isRequired);
147
+ }
148
+ return generateNonDiscriminatedUnionValidation(accessor, schema.anyOf, defaultValue, isRequired);
149
+ }
150
+ if (hasAllOf(schema) && schema.allOf.length > 0) {
151
+ const allChecks = [];
152
+ for (const subSchema of schema.allOf) {
153
+ const subChecks = generateSchemaChecks(accessor, subSchema);
154
+ allChecks.push(...subChecks);
155
+ }
156
+ if (allChecks.length > 0) {
157
+ let combinedCheck2 = allChecks[0];
158
+ for (let i = 1; i < allChecks.length; i++) {
159
+ combinedCheck2 += " && " + allChecks[i];
160
+ }
161
+ return `${combinedCheck2} ? ${accessor} : ${defaultValue}`;
190
162
  }
191
- // Handle x-mjst primitive (e.g. bigint): validate with typeof. We do not
192
- // coerce — BigInt() throws on invalid input, which a pure coercion expression
193
- // cannot guard — so invalid values fall back to the default (required) or
194
- // undefined (optional).
195
- const primitive = getMjstPrimitive(schema);
196
- if (primitive) {
197
- const valid = `typeof ${accessor} === "${primitive}"`;
198
- return isRequired ? `${valid} ? ${accessor} : ${defaultValue}` : `${valid} ? ${accessor} : undefined`;
163
+ return `${accessor} ?? ${defaultValue}`;
164
+ }
165
+ if ("not" in schema && schema.not) {
166
+ const notChecks = generateSchemaChecks(accessor, schema.not);
167
+ if (notChecks.length > 0) {
168
+ let combinedNotCheck = notChecks[0];
169
+ for (let i = 1; i < notChecks.length; i++) {
170
+ combinedNotCheck += " && " + notChecks[i];
171
+ }
172
+ return `!(${combinedNotCheck}) ? ${accessor} : ${defaultValue}`;
199
173
  }
200
- // Handle $ref - resolve and validate against the referenced schema
201
- if (hasRef(schema) && rootSchema) {
202
- const ref = schema.$ref;
203
- // If we have already visited this ref, we have a circular reference - return accessor to break cycle
204
- if (visitedRefs?.has(ref)) {
205
- return isRequired ? `${accessor} ?? ${defaultValue}` : accessor;
174
+ }
175
+ if (hasConst(schema)) {
176
+ const constLiteral = JSON.stringify(schema.const);
177
+ return isRequired ? `${accessor} === ${constLiteral} ? ${accessor} : ${constLiteral}` : `${accessor} === ${constLiteral} ? ${accessor} : (${accessor} !== undefined ? ${constLiteral} : undefined)`;
178
+ }
179
+ const typeArray = getTypeArray(schema);
180
+ if (typeArray && !hasEnum(schema)) {
181
+ const checks2 = typeArray.map((t) => singleTypeCheck(accessor, t)).filter((c) => c !== null);
182
+ if (checks2.length > 0) {
183
+ const combined = checks2.map((c) => `(${c})`).join(" || ");
184
+ return isRequired ? `${combined} ? ${accessor} : ${defaultValue}` : `${combined} ? ${accessor} : undefined`;
185
+ }
186
+ }
187
+ if (!hasType(schema) && !hasEnum(schema)) {
188
+ return `${accessor} ?? ${defaultValue}`;
189
+ }
190
+ const prefixItems = getPrefixItems(schema);
191
+ if (hasType(schema) && schema.type === "array" && prefixItems) {
192
+ const mapped = generatePrefixItemsMap(accessor, prefixItems, prefixItemsCapsLength(schema), rootSchema, visitedRefs, caseInsensitive);
193
+ const built = `(Array.isArray(${accessor}) ? ${mapped} : ${defaultValue})`;
194
+ return isRequired ? built : `(${accessor} !== undefined ? ${built} : undefined)`;
195
+ }
196
+ if (hasType(schema) && schema.type === "array" && hasItems(schema) && !Array.isArray(schema.items) && isCoercibleItemSchema(schema.items)) {
197
+ const itemSchema = schema.items;
198
+ const itemExpr = generateValidationExpression("", itemSchema, getDefaultValue(itemSchema), true, rootSchema, visitedRefs, "_it", true, caseInsensitive);
199
+ const mapped = `(Array.isArray(${accessor}) ? (${accessor} as unknown[]).map((_it) => ${itemExpr}) : ${defaultValue})`;
200
+ return isRequired ? mapped : `(${accessor} !== undefined ? ${mapped} : undefined)`;
201
+ }
202
+ if (hasType(schema)) {
203
+ switch (schema.type) {
204
+ case "string": {
205
+ checks.push(`typeof ${accessor} === "string"`);
206
+ if (hasPattern(schema)) {
207
+ checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${accessor})`);
206
208
  }
207
- const resolvedSchema = resolveRef(ref, rootSchema);
208
- if (resolvedSchema) {
209
- const visited = new Set(visitedRefs);
210
- visited.add(ref);
211
- return generateValidationExpression(key, resolvedSchema, defaultValue, isRequired, rootSchema, visited, undefined, undefined, caseInsensitive);
209
+ if (hasMinLength(schema)) {
210
+ checks.push(`${accessor}.length >= ${schema.minLength}`);
212
211
  }
213
- }
214
- // Handle union types (oneOf, anyOf)
215
- if (hasOneOf(schema) && schema.oneOf.length > 0) {
216
- const discriminator = findDiscriminator(schema.oneOf);
217
- if (discriminator) {
218
- return generateDiscriminatedUnionValidation(accessor, schema.oneOf, discriminator, defaultValue, isRequired);
212
+ if (hasMaxLength(schema)) {
213
+ checks.push(`${accessor}.length <= ${schema.maxLength}`);
219
214
  }
220
- return generateNonDiscriminatedUnionValidation(accessor, schema.oneOf, defaultValue, isRequired);
221
- }
222
- if (hasAnyOf(schema) && schema.anyOf.length > 0) {
223
- const discriminator = findDiscriminator(schema.anyOf);
224
- if (discriminator) {
225
- return generateDiscriminatedUnionValidation(accessor, schema.anyOf, discriminator, defaultValue, isRequired);
215
+ break;
216
+ }
217
+ case "number":
218
+ case "integer": {
219
+ checks.push(`typeof ${accessor} === "number"`);
220
+ if (schema.type === "integer") {
221
+ checks.push(`Number.isInteger(${accessor})`);
226
222
  }
227
- return generateNonDiscriminatedUnionValidation(accessor, schema.anyOf, defaultValue, isRequired);
228
- }
229
- // Handle allOf (intersection - all schemas must match)
230
- if (hasAllOf(schema) && schema.allOf.length > 0) {
231
- const allChecks = [];
232
- for (const subSchema of schema.allOf) {
233
- const subChecks = generateSchemaChecks(accessor, subSchema);
234
- allChecks.push(...subChecks);
223
+ if (hasMinimum(schema)) {
224
+ checks.push(`${accessor} >= ${schema.minimum}`);
235
225
  }
236
- if (allChecks.length > 0) {
237
- let combinedCheck = allChecks[0];
238
- for (let i = 1; i < allChecks.length; i++) {
239
- combinedCheck += ' && ' + allChecks[i];
240
- }
241
- return `${combinedCheck} ? ${accessor} : ${defaultValue}`;
226
+ if (hasMaximum(schema)) {
227
+ checks.push(`${accessor} <= ${schema.maximum}`);
242
228
  }
243
- return `${accessor} ?? ${defaultValue}`;
244
- }
245
- // Handle not (negation - value must NOT match this schema)
246
- if ('not' in schema && schema.not) {
247
- const notChecks = generateSchemaChecks(accessor, schema.not);
248
- if (notChecks.length > 0) {
249
- let combinedNotCheck = notChecks[0];
250
- for (let i = 1; i < notChecks.length; i++) {
251
- combinedNotCheck += ' && ' + notChecks[i];
252
- }
253
- return `!(${combinedNotCheck}) ? ${accessor} : ${defaultValue}`;
229
+ if (hasExclusiveMinimum(schema)) {
230
+ checks.push(`${accessor} > ${schema.exclusiveMinimum}`);
254
231
  }
255
- }
256
- // Handle const — single exact value that must match
257
- if (hasConst(schema)) {
258
- const constLiteral = JSON.stringify(schema.const);
259
- return isRequired
260
- ? `${accessor} === ${constLiteral} ? ${accessor} : ${constLiteral}`
261
- : `${accessor} === ${constLiteral} ? ${accessor} : (${accessor} !== undefined ? ${constLiteral} : undefined)`;
262
- }
263
- // Array-form `type` (multi-type / nullable, e.g. `["string","null"]`). `hasType`
264
- // is false for an array `type`, so without this the value would fall through
265
- // every branch below and emit NO validation — not even a required-presence
266
- // check. Emit a disjunction: the value is accepted when it matches ANY listed
267
- // type, otherwise it is coerced to the default (required) or dropped (optional).
268
- // A missing value fails every disjunct, so `required` presence is still enforced.
269
- const typeArray = getTypeArray(schema);
270
- if (typeArray && !hasEnum(schema)) {
271
- const checks = typeArray.map((t) => singleTypeCheck(accessor, t)).filter((c) => c !== null);
272
- if (checks.length > 0) {
273
- const combined = checks.map((c) => `(${c})`).join(' || ');
274
- return isRequired ? `${combined} ? ${accessor} : ${defaultValue}` : `${combined} ? ${accessor} : undefined`;
232
+ if (hasExclusiveMaximum(schema)) {
233
+ checks.push(`${accessor} < ${schema.exclusiveMaximum}`);
275
234
  }
276
- }
277
- if (!hasType(schema) && !hasEnum(schema)) {
278
- return `${accessor} ?? ${defaultValue}`;
279
- }
280
- // Tuple `prefixItems`: coerce each declared position through its own subschema
281
- // and, with a sibling `items: false`/`additionalItems: false`, drop any element
282
- // past the tuple length. A shorter input keeps its absent trailing positions.
283
- // The fast path only takes a tuple whose present positions are already
284
- // well-typed, so a mistyped position reaches here.
285
- const prefixItems = getPrefixItems(schema);
286
- if (hasType(schema) && schema.type === 'array' && prefixItems) {
287
- const mapped = generatePrefixItemsMap(accessor, prefixItems, prefixItemsCapsLength(schema), rootSchema, visitedRefs, caseInsensitive);
288
- const built = `(Array.isArray(${accessor}) ? ${mapped} : ${defaultValue})`;
289
- return isRequired ? built : `(${accessor} !== undefined ? ${built} : undefined)`;
290
- }
291
- // An array of scalar or enum items: coerce each element so e.g. `number[]`
292
- // actually contains numbers and an enum member set holds. The fast path only
293
- // takes a well-typed array, so a mistyped element reaches here. ($ref and
294
- // inline-object items use the caller's validateArray path; union items still
295
- // pass through.)
296
- if (hasType(schema) &&
297
- schema.type === 'array' &&
298
- hasItems(schema) &&
299
- !Array.isArray(schema.items) &&
300
- isCoercibleItemSchema(schema.items)) {
301
- const itemSchema = schema.items;
302
- const itemExpr = generateValidationExpression('', itemSchema, getDefaultValue(itemSchema), true, rootSchema, visitedRefs, '_it', true, caseInsensitive);
303
- const mapped = `(Array.isArray(${accessor}) ? (${accessor} as unknown[]).map((_it) => ${itemExpr}) : ${defaultValue})`;
304
- return isRequired ? mapped : `(${accessor} !== undefined ? ${mapped} : undefined)`;
305
- }
306
- // Build type-specific checks
307
- if (hasType(schema)) {
308
- switch (schema.type) {
309
- case 'string': {
310
- checks.push(`typeof ${accessor} === "string"`);
311
- if (hasPattern(schema)) {
312
- checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${accessor})`);
313
- }
314
- if (hasMinLength(schema)) {
315
- checks.push(`${accessor}.length >= ${schema.minLength}`);
316
- }
317
- if (hasMaxLength(schema)) {
318
- checks.push(`${accessor}.length <= ${schema.maxLength}`);
319
- }
320
- break;
321
- }
322
- case 'number':
323
- case 'integer': {
324
- checks.push(`typeof ${accessor} === "number"`);
325
- // `integer` must reject non-integral numbers; a bare `typeof === "number"`
326
- // accepts `1.5`. Matches the validators package (which uses Number.isInteger).
327
- if (schema.type === 'integer') {
328
- checks.push(`Number.isInteger(${accessor})`);
329
- }
330
- if (hasMinimum(schema)) {
331
- checks.push(`${accessor} >= ${schema.minimum}`);
332
- }
333
- if (hasMaximum(schema)) {
334
- checks.push(`${accessor} <= ${schema.maximum}`);
335
- }
336
- if (hasExclusiveMinimum(schema)) {
337
- checks.push(`${accessor} > ${schema.exclusiveMinimum}`);
338
- }
339
- if (hasExclusiveMaximum(schema)) {
340
- checks.push(`${accessor} < ${schema.exclusiveMaximum}`);
341
- }
342
- if (hasMultipleOf(schema)) {
343
- checks.push(multipleOfPassExpr(accessor, schema.multipleOf));
344
- }
345
- break;
346
- }
347
- case 'boolean':
348
- checks.push(`typeof ${accessor} === "boolean"`);
349
- break;
350
- case 'null':
351
- checks.push(`${accessor} === null`);
352
- break;
353
- case 'array': {
354
- checks.push(`Array.isArray(${accessor})`);
355
- if (hasMinItems(schema)) {
356
- checks.push(`${accessor}.length >= ${schema.minItems}`);
357
- }
358
- if (hasMaxItems(schema)) {
359
- checks.push(`${accessor}.length <= ${schema.maxItems}`);
360
- }
361
- if (hasUniqueItems(schema) && schema.uniqueItems === true) {
362
- checks.push(`new Set(${accessor}).size === ${accessor}.length`);
363
- }
364
- break;
365
- }
366
- case 'object': {
367
- checks.push(`isObject(${accessor})`);
368
- // Check required properties exist
369
- if (hasRequired(schema) && schema.required.length > 0) {
370
- for (const requiredKey of schema.required) {
371
- checks.push(`${JSON.stringify(requiredKey)} in ${accessor}`);
372
- }
373
- }
374
- // Check minProperties
375
- if (hasMinProperties(schema)) {
376
- checks.push(`Object.keys(${accessor}).length >= ${schema.minProperties}`);
377
- }
378
- // Check maxProperties
379
- if (hasMaxProperties(schema)) {
380
- checks.push(`Object.keys(${accessor}).length <= ${schema.maxProperties}`);
381
- }
382
- // Check additionalProperties
383
- if (hasAdditionalProperties(schema) && schema.additionalProperties === false && hasProperties(schema)) {
384
- const allowedKeys = Object.keys(schema.properties);
385
- const allowedKeysStr = JSON.stringify(allowedKeys);
386
- checks.push(`Object.keys(${accessor}).every(k => ${allowedKeysStr}.includes(k))`);
387
- }
388
- break;
389
- }
235
+ if (hasMultipleOf(schema)) {
236
+ checks.push(multipleOfPassExpr(accessor, schema.multipleOf));
390
237
  }
238
+ break;
239
+ }
240
+ case "boolean":
241
+ checks.push(`typeof ${accessor} === "boolean"`);
242
+ break;
243
+ case "null":
244
+ checks.push(`${accessor} === null`);
245
+ break;
246
+ case "array": {
247
+ checks.push(`Array.isArray(${accessor})`);
248
+ if (hasMinItems(schema)) {
249
+ checks.push(`${accessor}.length >= ${schema.minItems}`);
250
+ }
251
+ if (hasMaxItems(schema)) {
252
+ checks.push(`${accessor}.length <= ${schema.maxItems}`);
253
+ }
254
+ if (hasUniqueItems(schema) && schema.uniqueItems === true) {
255
+ checks.push(`new Set(${accessor}).size === ${accessor}.length`);
256
+ }
257
+ break;
258
+ }
259
+ case "object": {
260
+ checks.push(`isObject(${accessor})`);
261
+ if (hasRequired(schema) && schema.required.length > 0) {
262
+ for (const requiredKey of schema.required) {
263
+ checks.push(`${JSON.stringify(requiredKey)} in ${accessor}`);
264
+ }
265
+ }
266
+ if (hasMinProperties(schema)) {
267
+ checks.push(`Object.keys(${accessor}).length >= ${schema.minProperties}`);
268
+ }
269
+ if (hasMaxProperties(schema)) {
270
+ checks.push(`Object.keys(${accessor}).length <= ${schema.maxProperties}`);
271
+ }
272
+ if (hasAdditionalProperties(schema) && schema.additionalProperties === false && hasProperties(schema)) {
273
+ const allowedKeys = Object.keys(schema.properties);
274
+ const allowedKeysStr = JSON.stringify(allowedKeys);
275
+ checks.push(`Object.keys(${accessor}).every(k => ${allowedKeysStr}.includes(k))`);
276
+ }
277
+ break;
278
+ }
391
279
  }
392
- // Add enum check if present
393
- if (hasEnum(schema) && schema.enum.length > 0) {
394
- checks.push(generateEnumCheck(accessor, schema.enum));
395
- }
396
- // Combine all checks
397
- if (checks.length === 0) {
398
- return isRequired ? `${accessor} ?? ${defaultValue}` : accessor;
399
- }
400
- let combinedCheck = checks[0];
401
- for (let i = 1; i < checks.length; i++) {
402
- combinedCheck += ' && ' + checks[i];
280
+ }
281
+ if (hasEnum(schema) && schema.enum.length > 0) {
282
+ checks.push(generateEnumCheck(accessor, schema.enum));
283
+ }
284
+ if (checks.length === 0) {
285
+ return isRequired ? `${accessor} ?? ${defaultValue}` : accessor;
286
+ }
287
+ let combinedCheck = checks[0];
288
+ for (let i = 1; i < checks.length; i++) {
289
+ combinedCheck += " && " + checks[i];
290
+ }
291
+ const typeCoercion = hasEnum(schema) ? null : getTypeCoercion(accessor, schema, defaultValue);
292
+ const enumFallback = (fallback) => (caseInsensitive && hasEnum(schema) && schema.enum.length > 0 ? generateEnumCaseInsensitiveCoercion(accessor, schema.enum, fallback) : null) ?? fallback;
293
+ if (isRequired) {
294
+ if (typeCoercion) {
295
+ if (knownNotUndefined) {
296
+ return `${combinedCheck} ? ${accessor} : ${typeCoercion}`;
297
+ }
298
+ return `${combinedCheck} ? ${accessor} : (${accessor} !== undefined ? ${typeCoercion} : ${defaultValue})`;
403
299
  }
404
- // Generate the fallback value
405
- // If the value exists but fails validation, try to coerce it
406
- // If the value is missing and required, use the default
407
- //
408
- // `enum` constrains the value to a fixed set, so type coercion (e.g.
409
- // `String(x)`) can't rescue a non-member — `String("z")` is still not in the
410
- // enum. Fall back to the default (the first enum value) so the result is always
411
- // a valid member of the declared literal-union type.
412
- const typeCoercion = hasEnum(schema) ? null : getTypeCoercion(accessor, schema, defaultValue);
413
- // Case-insensitive enum normalization is spliced into the *failure* branch of
414
- // the ternary only, so an already-valid (exactly-cased) member takes the
415
- // `${accessor}` fast branch untouched. A mis-cased string folds to the exact
416
- // member; anything else falls through to the branch's normal fallback. `null`
417
- // when the flag is off or no member is a string, so the plain fallback stands.
418
- const enumFallback = (fallback) => (caseInsensitive && hasEnum(schema) && schema.enum.length > 0
419
- ? generateEnumCaseInsensitiveCoercion(accessor, schema.enum, fallback)
420
- : null) ?? fallback;
421
- if (isRequired) {
422
- // For required fields: valid ? use_value : (exists ? coerce : default)
423
- if (typeCoercion) {
424
- // If we know the value is not undefined (e.g., we're inside an undefined check),
425
- // we can skip the redundant undefined check
426
- if (knownNotUndefined) {
427
- return `${combinedCheck} ? ${accessor} : ${typeCoercion}`;
428
- }
429
- return `${combinedCheck} ? ${accessor} : (${accessor} !== undefined ? ${typeCoercion} : ${defaultValue})`;
430
- }
431
- return `${combinedCheck} ? ${accessor} : ${enumFallback(defaultValue)}`;
300
+ return `${combinedCheck} ? ${accessor} : ${enumFallback(defaultValue)}`;
301
+ } else {
302
+ if (typeCoercion) {
303
+ if (knownNotUndefined) {
304
+ return `${combinedCheck} ? ${accessor} : ${typeCoercion}`;
305
+ }
306
+ return `${combinedCheck} ? ${accessor} : (${accessor} !== undefined ? ${typeCoercion} : undefined)`;
432
307
  }
433
- else {
434
- // For optional fields: valid ? use_value : (exists ? coerce : undefined)
435
- if (typeCoercion) {
436
- // If we know the value is not undefined, we can skip the check and just coerce
437
- if (knownNotUndefined) {
438
- return `${combinedCheck} ? ${accessor} : ${typeCoercion}`;
439
- }
440
- return `${combinedCheck} ? ${accessor} : (${accessor} !== undefined ? ${typeCoercion} : undefined)`;
441
- }
442
- return `${combinedCheck} ? ${accessor} : ${enumFallback('undefined')}`;
308
+ return `${combinedCheck} ? ${accessor} : ${enumFallback("undefined")}`;
309
+ }
310
+ };
311
+ const generatePrefixItemsMap = (accessor, prefix, capLength, rootSchema, visitedRefs, caseInsensitive) => {
312
+ const branches = prefix.map((rawPos, i) => {
313
+ let pos = rawPos;
314
+ if (isSchemaObject(pos) && hasRef(pos) && rootSchema) {
315
+ const resolved = resolveRef(pos.$ref, rootSchema);
316
+ if (resolved)
317
+ pos = resolved;
443
318
  }
319
+ const posExpr = generateValidationExpression("", pos, getDefaultValue(pos), true, rootSchema, visitedRefs, "_el", true, caseInsensitive);
320
+ return `_i === ${i} ? ${posExpr}`;
321
+ });
322
+ const mapped = `(${accessor} as unknown[]).map((_el, _i) => ${[...branches, "_el"].join(" : ")})`;
323
+ return capLength ? `${mapped}.slice(0, ${prefix.length})` : mapped;
444
324
  };
445
- /**
446
- * Builds the coercion transform for a tuple `prefixItems` array: an index-matched
447
- * `.map` where each declared position runs through its own subschema's coercion
448
- * (present positions only — a shorter input keeps its absent trailing positions),
449
- * and every element past the tuple passes through unchanged. When `capLength` is
450
- * set (a sibling `items: false`), a trailing `.slice(0, N)` drops those extras.
451
- * `accessor` must already be known to be an array (the caller guards with
452
- * `Array.isArray`). `$ref` positions resolve against `rootSchema` when provided.
453
- */
454
- export const generatePrefixItemsMap = (accessor, prefix, capLength, rootSchema, visitedRefs, caseInsensitive) => {
455
- const branches = prefix.map((rawPos, i) => {
456
- // Resolve a position `$ref` to its target here: generateValidationExpression's
457
- // own ref path rebuilds the accessor from the property key, which would
458
- // discard the `_el` element accessor an override needs — so hand it the
459
- // concrete schema directly instead.
460
- let pos = rawPos;
461
- if (isSchemaObject(pos) && hasRef(pos) && rootSchema) {
462
- const resolved = resolveRef(pos.$ref, rootSchema);
463
- if (resolved)
464
- pos = resolved;
465
- }
466
- const posExpr = generateValidationExpression('', pos, getDefaultValue(pos), true, rootSchema, visitedRefs, '_el', true, caseInsensitive);
467
- return `_i === ${i} ? ${posExpr}`;
468
- });
469
- const mapped = `(${accessor} as unknown[]).map((_el, _i) => ${[...branches, '_el'].join(' : ')})`;
470
- return capLength ? `${mapped}.slice(0, ${prefix.length})` : mapped;
325
+ export {
326
+ generatePrefixItemsMap,
327
+ generateValidationExpression,
328
+ getPrefixItems,
329
+ isCoercibleItemSchema,
330
+ prefixItemsCapsLength,
331
+ scalarItemTypeCheck
471
332
  };