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