@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.
- package/LICENSE +21 -0
- package/dist/generators/assert-supported-keywords.js +45 -72
- package/dist/generators/build-schema.js +56 -168
- package/dist/generators/generate-default-from-pattern.js +24 -31
- package/dist/generators/generate-discriminated-union-validation.js +27 -47
- package/dist/generators/generate-enum-check.js +27 -61
- package/dist/generators/generate-files.js +54 -83
- package/dist/generators/generate-non-discriminated-union-validation.js +24 -26
- package/dist/generators/generate-parser-function.d.ts +12 -0
- package/dist/generators/generate-parser-function.js +1132 -1711
- package/dist/generators/generate-schema-checks.js +216 -250
- package/dist/generators/generate-strict-assertion.js +311 -447
- package/dist/generators/generate-type-checks.js +287 -460
- package/dist/generators/generate-validation-expression.js +300 -439
- package/dist/generators/subschema-match.js +201 -266
- package/dist/helpers/apply-schema-extensions.js +18 -49
- package/dist/helpers/collect-helpers.js +58 -71
- package/dist/helpers/collect-imports.js +161 -237
- package/dist/helpers/find-discriminator.js +23 -29
- package/dist/helpers/get-default-value.js +76 -96
- package/dist/helpers/get-discriminator-value.js +16 -17
- package/dist/index.js +4 -1
- package/package.json +6 -6
|
@@ -1,471 +1,332 @@
|
|
|
1
|
-
import { escapeRegexPattern } from
|
|
2
|
-
import { getMjstInstanceOf, getMjstPrimitive } from
|
|
3
|
-
import { multipleOfPassExpr } from
|
|
4
|
-
import { resolveRef } from
|
|
5
|
-
import { safeAccessor } from
|
|
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
|
|
7
|
-
import { findDiscriminator } from
|
|
8
|
-
import { getDefaultValue } from
|
|
9
|
-
import { generateDiscriminatedUnionValidation } from
|
|
10
|
-
import { generateEnumCaseInsensitiveCoercion, generateEnumCheck } from
|
|
11
|
-
import { generateNonDiscriminatedUnionValidation } from
|
|
12
|
-
import { generateSchemaChecks } from
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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 (
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
};
|