@amritk/generate-parsers 0.7.2 → 0.8.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/README.md +1 -1
- package/dist/generators/build-schema.d.ts +0 -1
- package/dist/generators/generate-default-from-pattern.d.ts +0 -1
- package/dist/generators/generate-discriminated-union-validation.d.ts +0 -1
- package/dist/generators/generate-enum-check.d.ts +0 -1
- package/dist/generators/generate-files.d.ts +0 -1
- package/dist/generators/generate-non-discriminated-union-validation.d.ts +0 -1
- package/dist/generators/generate-parser-function.d.ts +3 -3
- package/dist/generators/generate-parser-function.js +164 -29
- package/dist/generators/generate-schema-checks.d.ts +0 -1
- package/dist/generators/generate-strict-assertion.d.ts +0 -1
- package/dist/generators/generate-validation-expression.d.ts +0 -1
- package/dist/helpers/apply-schema-extensions.d.ts +0 -1
- package/dist/helpers/collect-helpers.d.ts +0 -1
- package/dist/helpers/collect-imports.d.ts +0 -1
- package/dist/helpers/find-discriminator.d.ts +0 -1
- package/dist/helpers/get-default-value.d.ts +0 -1
- package/dist/helpers/get-discriminator-value.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/types/schema-extensions.d.ts +0 -1
- package/package.json +3 -5
- package/dist/generators/build-schema.d.ts.map +0 -1
- package/dist/generators/generate-default-from-pattern.d.ts.map +0 -1
- package/dist/generators/generate-discriminated-union-validation.d.ts.map +0 -1
- package/dist/generators/generate-enum-check.d.ts.map +0 -1
- package/dist/generators/generate-files.d.ts.map +0 -1
- package/dist/generators/generate-non-discriminated-union-validation.d.ts.map +0 -1
- package/dist/generators/generate-parser-function.d.ts.map +0 -1
- package/dist/generators/generate-schema-checks.d.ts.map +0 -1
- package/dist/generators/generate-strict-assertion.d.ts.map +0 -1
- package/dist/generators/generate-validation-expression.d.ts.map +0 -1
- package/dist/helpers/apply-schema-extensions.d.ts.map +0 -1
- package/dist/helpers/collect-helpers.d.ts.map +0 -1
- package/dist/helpers/collect-imports.d.ts.map +0 -1
- package/dist/helpers/find-discriminator.d.ts.map +0 -1
- package/dist/helpers/get-default-value.d.ts.map +0 -1
- package/dist/helpers/get-discriminator-value.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/types/schema-extensions.d.ts.map +0 -1
- package/src/generators/build-schema.test.ts +0 -854
- package/src/generators/build-schema.ts +0 -147
- package/src/generators/generate-default-from-pattern.test.ts +0 -81
- package/src/generators/generate-default-from-pattern.ts +0 -39
- package/src/generators/generate-discriminated-union-validation.test.ts +0 -84
- package/src/generators/generate-discriminated-union-validation.ts +0 -54
- package/src/generators/generate-enum-check.test.ts +0 -45
- package/src/generators/generate-enum-check.ts +0 -8
- package/src/generators/generate-files.test.ts +0 -448
- package/src/generators/generate-files.ts +0 -155
- package/src/generators/generate-non-discriminated-union-validation.test.ts +0 -74
- package/src/generators/generate-non-discriminated-union-validation.ts +0 -40
- package/src/generators/generate-parser-function.test.ts +0 -1794
- package/src/generators/generate-parser-function.ts +0 -1340
- package/src/generators/generate-schema-checks.test.ts +0 -330
- package/src/generators/generate-schema-checks.ts +0 -128
- package/src/generators/generate-strict-assertion.test.ts +0 -67
- package/src/generators/generate-strict-assertion.ts +0 -243
- package/src/generators/generate-validation-expression.test.ts +0 -831
- package/src/generators/generate-validation-expression.ts +0 -327
- package/src/helpers/apply-schema-extensions.test.ts +0 -194
- package/src/helpers/apply-schema-extensions.ts +0 -63
- package/src/helpers/collect-helpers.test.ts +0 -115
- package/src/helpers/collect-helpers.ts +0 -79
- package/src/helpers/collect-imports.test.ts +0 -547
- package/src/helpers/collect-imports.ts +0 -275
- package/src/helpers/find-discriminator.test.ts +0 -88
- package/src/helpers/find-discriminator.ts +0 -39
- package/src/helpers/get-default-value.test.ts +0 -139
- package/src/helpers/get-default-value.ts +0 -95
- package/src/helpers/get-discriminator-value.test.ts +0 -83
- package/src/helpers/get-discriminator-value.ts +0 -21
- package/src/index.ts +0 -1
- package/src/types/schema-extensions.ts +0 -35
|
@@ -1,1340 +0,0 @@
|
|
|
1
|
-
import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern'
|
|
2
|
-
import { refToName } from '@amritk/helpers/ref-to-name'
|
|
3
|
-
import { safeAccessor, safeKey } from '@amritk/helpers/safe-accessor'
|
|
4
|
-
import {
|
|
5
|
-
hasAllOf,
|
|
6
|
-
hasAnyOf,
|
|
7
|
-
hasConst,
|
|
8
|
-
hasDefault,
|
|
9
|
-
hasEnum,
|
|
10
|
-
hasExamples,
|
|
11
|
-
hasExclusiveMaximum,
|
|
12
|
-
hasExclusiveMinimum,
|
|
13
|
-
hasItems,
|
|
14
|
-
hasMaxItems,
|
|
15
|
-
hasMaximum,
|
|
16
|
-
hasMaxLength,
|
|
17
|
-
hasMinItems,
|
|
18
|
-
hasMinimum,
|
|
19
|
-
hasMinLength,
|
|
20
|
-
hasMultipleOf,
|
|
21
|
-
hasOneOf,
|
|
22
|
-
hasPattern,
|
|
23
|
-
hasProperties,
|
|
24
|
-
hasRef,
|
|
25
|
-
hasType,
|
|
26
|
-
hasUniqueItems,
|
|
27
|
-
isObjectSchema,
|
|
28
|
-
isSchemaObject,
|
|
29
|
-
} from '@amritk/helpers/schema-guards'
|
|
30
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
31
|
-
import { getDefaultValue } from '#helpers/get-default-value'
|
|
32
|
-
|
|
33
|
-
import { generateObjectStrictAssertion, generateScalarStrictAssertion } from './generate-strict-assertion'
|
|
34
|
-
import { generateValidationExpression } from './generate-validation-expression'
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Options for controlling parser function generation behavior.
|
|
38
|
-
*/
|
|
39
|
-
type GenerateParserOptions = {
|
|
40
|
-
/**
|
|
41
|
-
* When true, properties with $ref will call the imported parser function
|
|
42
|
-
* instead of inlining the validation logic. This is used when generating
|
|
43
|
-
* files where each ref has its own separate file with a parser.
|
|
44
|
-
*/
|
|
45
|
-
readonly useRefImports?: boolean
|
|
46
|
-
/**
|
|
47
|
-
* When true, the generated parser emits a console.warn for every input key
|
|
48
|
-
* that is not declared in the schema's properties.
|
|
49
|
-
*/
|
|
50
|
-
readonly logWarnings?: boolean
|
|
51
|
-
/**
|
|
52
|
-
* When true, the generated parser throws on type/shape mismatches instead
|
|
53
|
-
* of coercing invalid input to default values. Throws on the first violation
|
|
54
|
-
* with a path-aware Error. Unknown extra keys are still allowed.
|
|
55
|
-
*/
|
|
56
|
-
readonly strict?: boolean
|
|
57
|
-
/**
|
|
58
|
-
* Suffix appended to every type/parser name derived from a `$ref`. Must match
|
|
59
|
-
* the suffix used when generating the referenced files. Defaults to `''`.
|
|
60
|
-
*/
|
|
61
|
-
readonly typeSuffix?: string
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Represents a property in the generated object literal.
|
|
66
|
-
*/
|
|
67
|
-
type PropertyEntry = {
|
|
68
|
-
readonly key: string
|
|
69
|
-
readonly value: string
|
|
70
|
-
readonly isOptional: boolean
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Generates the parser function name from a type name.
|
|
75
|
-
* Converts "UserObject" to "parseUserObject".
|
|
76
|
-
*/
|
|
77
|
-
const generateParserName = (typeName: string): string => {
|
|
78
|
-
return `parse${typeName}`
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Checks if a property is required based on the schema's required array.
|
|
83
|
-
*/
|
|
84
|
-
const isPropertyRequired = (key: string, schema: JSONSchema): boolean => {
|
|
85
|
-
if (!isSchemaObject(schema)) {
|
|
86
|
-
return false
|
|
87
|
-
}
|
|
88
|
-
if (!('required' in schema) || !Array.isArray(schema.required)) {
|
|
89
|
-
return false
|
|
90
|
-
}
|
|
91
|
-
return schema.required.includes(key)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Generates a parser call expression for a required $ref property.
|
|
96
|
-
* Example: parseContact(input.contact)
|
|
97
|
-
*/
|
|
98
|
-
const generateRequiredRefCall = (key: string, ref: string, suffix: string): string => {
|
|
99
|
-
const acc = safeAccessor('input', key)
|
|
100
|
-
const parserName = generateParserName(refToName(ref, suffix))
|
|
101
|
-
return `${parserName}(${acc})`
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Generates a parser call expression for an optional $ref property.
|
|
106
|
-
* Example: ...(input.contact && { contact: parseContact(input.contact) })
|
|
107
|
-
*/
|
|
108
|
-
const generateOptionalRefCall = (key: string, ref: string, suffix: string): string => {
|
|
109
|
-
const acc = safeAccessor('input', key)
|
|
110
|
-
const parserName = generateParserName(refToName(ref, suffix))
|
|
111
|
-
return `...(${acc} && { ${safeKey(key)}: ${parserName}(${acc}) })`
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Generates a validateArray call for required array properties with $ref items.
|
|
116
|
-
* Example: validateArray(input.contacts, parseContact)
|
|
117
|
-
*/
|
|
118
|
-
const generateRequiredArrayRefCall = (key: string, ref: string, suffix: string): string => {
|
|
119
|
-
const parserName = generateParserName(refToName(ref, suffix))
|
|
120
|
-
return `validateArray(${safeAccessor('input', key)}, ${parserName})`
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Generates a validateArray call for optional array properties with $ref items.
|
|
125
|
-
* Example: ...(input.contacts && { contacts: validateArray(input.contacts, parseContact) })
|
|
126
|
-
*/
|
|
127
|
-
const generateOptionalArrayRefCall = (key: string, ref: string, suffix: string): string => {
|
|
128
|
-
const parserName = generateParserName(refToName(ref, suffix))
|
|
129
|
-
const acc = safeAccessor('input', key)
|
|
130
|
-
return `...(${acc} && { ${safeKey(key)}: validateArray(${acc}, ${parserName}) })`
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Generates a validateRecord call for required object properties with additionalProperties $ref.
|
|
135
|
-
* Example: validateRecord(input.responses, parseResponse)
|
|
136
|
-
*/
|
|
137
|
-
const generateRequiredRecordRefCall = (key: string, ref: string, suffix: string): string => {
|
|
138
|
-
const acc = safeAccessor('input', key)
|
|
139
|
-
const parserName = generateParserName(refToName(ref, suffix))
|
|
140
|
-
return `validateRecord(${acc}, ${parserName})`
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Generates a validateRecord call for optional object properties with additionalProperties $ref.
|
|
145
|
-
* Example: ...(input.responses && { responses: validateRecord(input.responses, parseResponse) })
|
|
146
|
-
*/
|
|
147
|
-
const generateOptionalRecordRefCall = (key: string, ref: string, suffix: string): string => {
|
|
148
|
-
const acc = safeAccessor('input', key)
|
|
149
|
-
const parserName = generateParserName(refToName(ref, suffix))
|
|
150
|
-
return `...(${acc} && { ${safeKey(key)}: validateRecord(${acc}, ${parserName}) })`
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Generates a spread entry for optional inline properties.
|
|
155
|
-
* The value expression is evaluated once and omitted when undefined.
|
|
156
|
-
*/
|
|
157
|
-
const generateOptionalInlineProperty = (key: string, valueExpression: string): string => {
|
|
158
|
-
return `...((value => value === undefined ? {} : { ${safeKey(key)}: value })(${valueExpression}))`
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Determines if a property schema should use ref imports for validation.
|
|
163
|
-
*/
|
|
164
|
-
const shouldUseRefImport = (propSchema: JSONSchema, useRefImports: boolean): boolean => {
|
|
165
|
-
if (!useRefImports || !hasRef(propSchema)) {
|
|
166
|
-
return false
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const ref = (propSchema as { $ref: string }).$ref
|
|
170
|
-
// Skip external URI refs with property/definition fragments — these are not generated as files
|
|
171
|
-
const isUri = ref.startsWith('http://') || ref.startsWith('https://')
|
|
172
|
-
if (isUri && (ref.includes('#/properties/') || ref.includes('#/definitions/'))) {
|
|
173
|
-
return false
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return true
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Determines if an array property should use ref imports for its items.
|
|
181
|
-
*/
|
|
182
|
-
const shouldUseArrayRefImport = (propSchema: JSONSchema, useRefImports: boolean): boolean => {
|
|
183
|
-
if (!useRefImports) {
|
|
184
|
-
return false
|
|
185
|
-
}
|
|
186
|
-
if (!isSchemaObject(propSchema)) {
|
|
187
|
-
return false
|
|
188
|
-
}
|
|
189
|
-
if (!('type' in propSchema) || propSchema.type !== 'array') {
|
|
190
|
-
return false
|
|
191
|
-
}
|
|
192
|
-
return hasItems(propSchema) && hasRef(propSchema.items)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Determines if a property with additionalProperties should use ref imports.
|
|
197
|
-
*/
|
|
198
|
-
const shouldUseRecordRefImport = (propSchema: JSONSchema, useRefImports: boolean): boolean => {
|
|
199
|
-
if (!useRefImports) {
|
|
200
|
-
return false
|
|
201
|
-
}
|
|
202
|
-
if (!isSchemaObject(propSchema)) {
|
|
203
|
-
return false
|
|
204
|
-
}
|
|
205
|
-
if (!('type' in propSchema) || propSchema.type !== 'object') {
|
|
206
|
-
return false
|
|
207
|
-
}
|
|
208
|
-
if (!('additionalProperties' in propSchema)) {
|
|
209
|
-
return false
|
|
210
|
-
}
|
|
211
|
-
const additionalProps = propSchema.additionalProperties
|
|
212
|
-
return isSchemaObject(additionalProps) && hasRef(additionalProps)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Generates the value expression for a property based on its schema and options.
|
|
217
|
-
*/
|
|
218
|
-
const generatePropertyValue = (
|
|
219
|
-
key: string,
|
|
220
|
-
propSchema: JSONSchema,
|
|
221
|
-
isRequired: boolean,
|
|
222
|
-
useRefImports: boolean,
|
|
223
|
-
suffix: string,
|
|
224
|
-
): string => {
|
|
225
|
-
// Handle direct $ref properties
|
|
226
|
-
if (shouldUseRefImport(propSchema, useRefImports)) {
|
|
227
|
-
const ref = (propSchema as { $ref: string }).$ref
|
|
228
|
-
return isRequired ? generateRequiredRefCall(key, ref, suffix) : generateOptionalRefCall(key, ref, suffix)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Handle array properties with $ref items
|
|
232
|
-
if (shouldUseArrayRefImport(propSchema, useRefImports)) {
|
|
233
|
-
const items = (propSchema as { items: { $ref: string } }).items
|
|
234
|
-
const ref = items.$ref
|
|
235
|
-
return isRequired ? generateRequiredArrayRefCall(key, ref, suffix) : generateOptionalArrayRefCall(key, ref, suffix)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Handle object properties with additionalProperties $ref
|
|
239
|
-
if (shouldUseRecordRefImport(propSchema, useRefImports)) {
|
|
240
|
-
const additionalProps = (propSchema as { additionalProperties: { $ref: string } }).additionalProperties
|
|
241
|
-
const ref = additionalProps.$ref
|
|
242
|
-
return isRequired
|
|
243
|
-
? generateRequiredRecordRefCall(key, ref, suffix)
|
|
244
|
-
: generateOptionalRecordRefCall(key, ref, suffix)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Handle non-schema object properties (true/false)
|
|
248
|
-
if (!isSchemaObject(propSchema)) {
|
|
249
|
-
return isRequired ? 'undefined' : generateOptionalInlineProperty(key, 'undefined')
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Generate standard validation expression
|
|
253
|
-
const defaultValue = getDefaultValue(propSchema)
|
|
254
|
-
const valueExpression = generateValidationExpression(key, propSchema, defaultValue, isRequired)
|
|
255
|
-
return isRequired ? valueExpression : generateOptionalInlineProperty(key, valueExpression)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Generates property entries for all properties in the schema.
|
|
260
|
-
*/
|
|
261
|
-
const generatePropertyEntries = (schema: JSONSchema, useRefImports: boolean, suffix: string): PropertyEntry[] => {
|
|
262
|
-
if (!hasProperties(schema)) {
|
|
263
|
-
return []
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const entries: PropertyEntry[] = []
|
|
267
|
-
|
|
268
|
-
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
269
|
-
const isRequired = isPropertyRequired(key, schema)
|
|
270
|
-
const value = generatePropertyValue(key, propSchema, isRequired, useRefImports, suffix)
|
|
271
|
-
|
|
272
|
-
// Determine if the property uses spread syntax (making it optional in the object literal)
|
|
273
|
-
const isOptional = value.startsWith('...')
|
|
274
|
-
|
|
275
|
-
entries.push({ key, value, isOptional })
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return entries
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Generates a fallback value for a required property.
|
|
283
|
-
* This returns simple type-based defaults without pattern inference,
|
|
284
|
-
* matching TypeBox's coercion behavior.
|
|
285
|
-
*/
|
|
286
|
-
const generateFallbackValue = (_: string, propSchema: JSONSchema, useRefImports: boolean, suffix: string): string => {
|
|
287
|
-
// Handle direct $ref properties - call the parser with undefined
|
|
288
|
-
if (useRefImports && hasRef(propSchema)) {
|
|
289
|
-
const ref = (propSchema as { $ref: string }).$ref
|
|
290
|
-
const parserName = generateParserName(refToName(ref, suffix))
|
|
291
|
-
return `${parserName}(undefined)`
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Handle array properties with $ref items
|
|
295
|
-
if (
|
|
296
|
-
useRefImports &&
|
|
297
|
-
isSchemaObject(propSchema) &&
|
|
298
|
-
propSchema.type === 'array' &&
|
|
299
|
-
hasItems(propSchema) &&
|
|
300
|
-
hasRef(propSchema.items)
|
|
301
|
-
) {
|
|
302
|
-
return '[]'
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Handle object properties with additionalProperties $ref
|
|
306
|
-
if (
|
|
307
|
-
useRefImports &&
|
|
308
|
-
isSchemaObject(propSchema) &&
|
|
309
|
-
propSchema.type === 'object' &&
|
|
310
|
-
'additionalProperties' in propSchema
|
|
311
|
-
) {
|
|
312
|
-
const additionalProps = propSchema.additionalProperties
|
|
313
|
-
if (isSchemaObject(additionalProps) && hasRef(additionalProps)) {
|
|
314
|
-
return '{}'
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// For non-schema objects, return undefined
|
|
319
|
-
if (!isSchemaObject(propSchema)) {
|
|
320
|
-
return 'undefined'
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Use explicit default if provided
|
|
324
|
-
if (hasDefault(propSchema)) {
|
|
325
|
-
return JSON.stringify(propSchema.default)
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Use first enum value if available
|
|
329
|
-
if (hasEnum(propSchema) && propSchema.enum.length > 0) {
|
|
330
|
-
return JSON.stringify(propSchema.enum[0])
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Use first example if available
|
|
334
|
-
if (hasExamples(propSchema) && propSchema.examples.length > 0) {
|
|
335
|
-
return JSON.stringify(propSchema.examples[0])
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Return simple type-based defaults without pattern inference
|
|
339
|
-
if (!hasType(propSchema)) {
|
|
340
|
-
return 'undefined'
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
switch (propSchema.type) {
|
|
344
|
-
case 'string':
|
|
345
|
-
return '""'
|
|
346
|
-
case 'number':
|
|
347
|
-
case 'integer':
|
|
348
|
-
return '0'
|
|
349
|
-
case 'boolean':
|
|
350
|
-
return 'false'
|
|
351
|
-
case 'array':
|
|
352
|
-
return '[]'
|
|
353
|
-
case 'object':
|
|
354
|
-
return '{}'
|
|
355
|
-
default:
|
|
356
|
-
return 'undefined'
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Generates a fallback object with required properties filled with default values.
|
|
362
|
-
* This is used when input is not an object (undefined, null, etc.).
|
|
363
|
-
*/
|
|
364
|
-
const generateFallbackObject = (
|
|
365
|
-
schema: JSONSchema,
|
|
366
|
-
useRefImports: boolean,
|
|
367
|
-
typeName: string,
|
|
368
|
-
suffix: string,
|
|
369
|
-
): string => {
|
|
370
|
-
if (!hasProperties(schema)) {
|
|
371
|
-
return `{} as ${typeName}`
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// For schemas with conditional branches (if/then/else), the merged properties
|
|
375
|
-
// may not fully represent all required fields. Use a simple cast to avoid
|
|
376
|
-
// generating incomplete fallback objects.
|
|
377
|
-
if (isSchemaObject(schema) && ('if' in schema || 'then' in schema || 'else' in schema)) {
|
|
378
|
-
return `{} as ${typeName}`
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const requiredProps: string[] = []
|
|
382
|
-
|
|
383
|
-
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
384
|
-
const isRequired = isPropertyRequired(key, schema)
|
|
385
|
-
if (isRequired) {
|
|
386
|
-
const fallbackValue = generateFallbackValue(key, propSchema, useRefImports, suffix)
|
|
387
|
-
if (fallbackValue === 'undefined') {
|
|
388
|
-
return `{} as ${typeName}`
|
|
389
|
-
}
|
|
390
|
-
requiredProps.push(` ${safeKey(key)}: ${fallbackValue},`)
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (requiredProps.length === 0) {
|
|
395
|
-
return `{} as ${typeName}`
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
let result = '{\n'
|
|
399
|
-
for (let i = 0; i < requiredProps.length; i++) {
|
|
400
|
-
result += requiredProps[i]
|
|
401
|
-
if (i < requiredProps.length - 1) {
|
|
402
|
-
result += '\n'
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
result += '\n }'
|
|
406
|
-
return result
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Generates a parser for non-object schemas (string, number, boolean, array, etc.)
|
|
411
|
-
* that validates the input matches the expected primitive type before casting.
|
|
412
|
-
*/
|
|
413
|
-
const generateNonObjectParser = (typeName: string, schema: JSONSchema, strict?: boolean): string => {
|
|
414
|
-
const functionName = generateParserName(typeName)
|
|
415
|
-
|
|
416
|
-
if (!isSchemaObject(schema) || !hasType(schema)) {
|
|
417
|
-
// Schema without type information cannot be validated beyond a cast
|
|
418
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (strict) {
|
|
422
|
-
const assertion = generateScalarStrictAssertion(schema, typeName)
|
|
423
|
-
if (assertion === null) {
|
|
424
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`
|
|
425
|
-
}
|
|
426
|
-
const returnExpr =
|
|
427
|
-
schema.type === 'array' ? `[...(input as readonly unknown[])] as ${typeName}` : `input as ${typeName}`
|
|
428
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => {\n${assertion}\n return ${returnExpr};\n};`
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
switch (schema.type) {
|
|
432
|
-
case 'string':
|
|
433
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "string" ? input as ${typeName} : "" as ${typeName};`
|
|
434
|
-
case 'number':
|
|
435
|
-
case 'integer':
|
|
436
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "number" ? input as ${typeName} : 0 as ${typeName};`
|
|
437
|
-
case 'boolean':
|
|
438
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "boolean" ? input as ${typeName} : false as ${typeName};`
|
|
439
|
-
case 'array':
|
|
440
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? [...input] as ${typeName} : [] as ${typeName};`
|
|
441
|
-
default:
|
|
442
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Generates a parser for empty object schemas or schemas with only additionalProperties.
|
|
448
|
-
* Validates the input is an object before casting, falling back to an empty object.
|
|
449
|
-
* Returns a shallow copy to avoid mutating the original input.
|
|
450
|
-
*/
|
|
451
|
-
const generateEmptyObjectParser = (typeName: string, strict?: boolean): string => {
|
|
452
|
-
const functionName = generateParserName(typeName)
|
|
453
|
-
if (strict) {
|
|
454
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return { ...input } as ${typeName};\n};`
|
|
455
|
-
}
|
|
456
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {} as ${typeName};`
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Converts a property key to a safe local variable name for caching.
|
|
461
|
-
* Replaces non-identifier characters with underscores.
|
|
462
|
-
*/
|
|
463
|
-
const toVarName = (key: string): string => {
|
|
464
|
-
const safe = key.replace(/[^a-zA-Z0-9_$]/g, '_')
|
|
465
|
-
return `_${safe}`
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* Returns the shape-predicate function name for a given type name.
|
|
470
|
-
* The predicate is generated alongside each parser and tells callers
|
|
471
|
-
* whether `input` is already in the shape produced by the parser's fast path.
|
|
472
|
-
*/
|
|
473
|
-
const shapeValidatorName = (typeName: string): string => `validate${typeName}Shape`
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Generates a fast-path type check expression for a property.
|
|
477
|
-
* Returns null if the schema is too complex for a simple type check
|
|
478
|
-
* (e.g. has unions, enums that require more elaborate validation).
|
|
479
|
-
*
|
|
480
|
-
* When `useRefImports` is true, $ref properties (and arrays of $refs) are
|
|
481
|
-
* checked by calling the imported shape predicate, allowing parent parsers
|
|
482
|
-
* to fast-path through nested ref types without recursing into their parsers.
|
|
483
|
-
*/
|
|
484
|
-
const generatePropertyTypeCheck = (
|
|
485
|
-
varName: string,
|
|
486
|
-
schema: JSONSchema,
|
|
487
|
-
useRefImports: boolean,
|
|
488
|
-
suffix: string,
|
|
489
|
-
): string | null => {
|
|
490
|
-
if (!isSchemaObject(schema)) return null
|
|
491
|
-
|
|
492
|
-
// $ref via shape predicate (deep fast-path through nested types).
|
|
493
|
-
if (useRefImports && hasRef(schema)) {
|
|
494
|
-
const refName = refToName((schema as { $ref: string }).$ref, suffix)
|
|
495
|
-
return `${shapeValidatorName(refName)}(${varName})`
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Skip fast path for complex schemas that need more elaborate validation
|
|
499
|
-
if (
|
|
500
|
-
hasEnum(schema) ||
|
|
501
|
-
hasOneOf(schema) ||
|
|
502
|
-
hasAnyOf(schema) ||
|
|
503
|
-
hasAllOf(schema) ||
|
|
504
|
-
hasRef(schema) ||
|
|
505
|
-
'not' in schema
|
|
506
|
-
) {
|
|
507
|
-
return null
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (!hasType(schema)) return null
|
|
511
|
-
|
|
512
|
-
// Array of $refs: each item must satisfy the imported shape predicate.
|
|
513
|
-
if (
|
|
514
|
-
useRefImports &&
|
|
515
|
-
schema.type === 'array' &&
|
|
516
|
-
hasItems(schema) &&
|
|
517
|
-
isSchemaObject(schema.items) &&
|
|
518
|
-
hasRef(schema.items)
|
|
519
|
-
) {
|
|
520
|
-
const refName = refToName((schema.items as { $ref: string }).$ref, suffix)
|
|
521
|
-
return `Array.isArray(${varName}) && ${varName}.every(${shapeValidatorName(refName)})`
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const checks: string[] = []
|
|
525
|
-
|
|
526
|
-
switch (schema.type) {
|
|
527
|
-
case 'string': {
|
|
528
|
-
checks.push(`typeof ${varName} === "string"`)
|
|
529
|
-
if (hasPattern(schema)) checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${varName})`)
|
|
530
|
-
if (hasMinLength(schema)) checks.push(`${varName}.length >= ${schema.minLength}`)
|
|
531
|
-
if (hasMaxLength(schema)) checks.push(`${varName}.length <= ${schema.maxLength}`)
|
|
532
|
-
break
|
|
533
|
-
}
|
|
534
|
-
case 'number':
|
|
535
|
-
case 'integer': {
|
|
536
|
-
checks.push(`typeof ${varName} === "number"`)
|
|
537
|
-
if (hasMinimum(schema)) checks.push(`${varName} >= ${schema.minimum}`)
|
|
538
|
-
if (hasMaximum(schema)) checks.push(`${varName} <= ${schema.maximum}`)
|
|
539
|
-
if (hasExclusiveMinimum(schema)) checks.push(`${varName} > ${schema.exclusiveMinimum}`)
|
|
540
|
-
if (hasExclusiveMaximum(schema)) checks.push(`${varName} < ${schema.exclusiveMaximum}`)
|
|
541
|
-
if (hasMultipleOf(schema)) checks.push(`${varName} % ${schema.multipleOf} === 0`)
|
|
542
|
-
break
|
|
543
|
-
}
|
|
544
|
-
case 'boolean':
|
|
545
|
-
checks.push(`typeof ${varName} === "boolean"`)
|
|
546
|
-
break
|
|
547
|
-
case 'array': {
|
|
548
|
-
checks.push(`Array.isArray(${varName})`)
|
|
549
|
-
if (hasMinItems(schema)) checks.push(`${varName}.length >= ${schema.minItems}`)
|
|
550
|
-
if (hasMaxItems(schema)) checks.push(`${varName}.length <= ${schema.maxItems}`)
|
|
551
|
-
if (hasUniqueItems(schema) && schema.uniqueItems === true) {
|
|
552
|
-
checks.push(`new Set(${varName}).size === ${varName}.length`)
|
|
553
|
-
}
|
|
554
|
-
break
|
|
555
|
-
}
|
|
556
|
-
case 'object':
|
|
557
|
-
checks.push(`isObject(${varName})`)
|
|
558
|
-
break
|
|
559
|
-
default:
|
|
560
|
-
return null
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (checks.length === 0) return null
|
|
564
|
-
// checks[0] is safe: we guard with checks.length === 0 above
|
|
565
|
-
let result = checks[0] as string
|
|
566
|
-
for (let i = 1; i < checks.length; i++) {
|
|
567
|
-
result += ' && ' + checks[i]
|
|
568
|
-
}
|
|
569
|
-
return result
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Determines if a property needs a local variable or can be inlined.
|
|
574
|
-
* Schema-object properties are always cached: the slow-path ternary chain
|
|
575
|
-
* reads each value 3-4x (typeof check, valid branch, undefined check, coerce),
|
|
576
|
-
* so a single hoisted load is strictly cheaper than the inlined optional chain.
|
|
577
|
-
*/
|
|
578
|
-
const shouldCacheVariable = (propSchema: JSONSchema, _canFastPath: boolean, _useRefImports: boolean): boolean => {
|
|
579
|
-
// Non-schema-object properties (true/false JSON Schema literals) generate
|
|
580
|
-
// `undefined` with no value access, so caching would be wasted.
|
|
581
|
-
return isSchemaObject(propSchema)
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* Generates a parser for object schemas with properties.
|
|
586
|
-
*
|
|
587
|
-
* Uses an optimized function body with:
|
|
588
|
-
* - Early return for non-object input
|
|
589
|
-
* - Selective variable caching (only when needed)
|
|
590
|
-
* - Fast path that returns input directly when all properties are already valid
|
|
591
|
-
* - Slow path with optimized expressions
|
|
592
|
-
*/
|
|
593
|
-
const generateObjectParser = (
|
|
594
|
-
schema: JSONSchema,
|
|
595
|
-
typeName: string,
|
|
596
|
-
useRefImports: boolean,
|
|
597
|
-
suffix: string,
|
|
598
|
-
logWarnings?: boolean,
|
|
599
|
-
strict?: boolean,
|
|
600
|
-
): string => {
|
|
601
|
-
const functionName = generateParserName(typeName)
|
|
602
|
-
|
|
603
|
-
if (!hasProperties(schema)) {
|
|
604
|
-
if (strict) {
|
|
605
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return input as ${typeName};\n};`
|
|
606
|
-
}
|
|
607
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? input as ${typeName} : {} as ${typeName};`
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
const fallbackObject = generateFallbackObject(schema, useRefImports, typeName, suffix)
|
|
611
|
-
const properties = Object.entries((schema as { properties: Record<string, JSONSchema> }).properties)
|
|
612
|
-
|
|
613
|
-
const lines: string[] = []
|
|
614
|
-
lines.push(`export const ${functionName} = (input: unknown): ${typeName} => {`)
|
|
615
|
-
if (strict) {
|
|
616
|
-
for (const assertionLine of generateObjectStrictAssertion(schema, typeName)) {
|
|
617
|
-
lines.push(assertionLine)
|
|
618
|
-
}
|
|
619
|
-
} else {
|
|
620
|
-
lines.push(` if (!isObject(input)) return ${fallbackObject};`)
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// First pass: determine if we can generate a fast path
|
|
624
|
-
const propInfo: {
|
|
625
|
-
readonly key: string
|
|
626
|
-
readonly varName: string
|
|
627
|
-
readonly isRequired: boolean
|
|
628
|
-
readonly propSchema: JSONSchema
|
|
629
|
-
}[] = []
|
|
630
|
-
|
|
631
|
-
// Disable fast path when allOf $ref parsers need to be spread — the fast path
|
|
632
|
-
// returns input as-is and would skip those coercions.
|
|
633
|
-
const hasAllOfRefParsers =
|
|
634
|
-
useRefImports &&
|
|
635
|
-
isSchemaObject(schema) &&
|
|
636
|
-
hasAllOf(schema) &&
|
|
637
|
-
schema.allOf.some((entry) => isSchemaObject(entry) && hasRef(entry))
|
|
638
|
-
|
|
639
|
-
let canFastPath = !hasAllOfRefParsers
|
|
640
|
-
const fastPathChecks: string[] = []
|
|
641
|
-
|
|
642
|
-
for (const [key, propSchema] of properties) {
|
|
643
|
-
const isRequired = isPropertyRequired(key, schema)
|
|
644
|
-
const varName = toVarName(key)
|
|
645
|
-
propInfo.push({ key, varName, isRequired, propSchema })
|
|
646
|
-
|
|
647
|
-
// Check if this property can participate in fast path
|
|
648
|
-
if (canFastPath) {
|
|
649
|
-
// Records of refs require iterating every value to check shape — too
|
|
650
|
-
// expensive to inline into the parser's fast path. Disable.
|
|
651
|
-
if (shouldUseRecordRefImport(propSchema, useRefImports)) {
|
|
652
|
-
canFastPath = false
|
|
653
|
-
} else {
|
|
654
|
-
const check = generatePropertyTypeCheck(varName, propSchema, useRefImports, suffix)
|
|
655
|
-
if (check === null) {
|
|
656
|
-
canFastPath = false
|
|
657
|
-
} else {
|
|
658
|
-
if (isRequired) {
|
|
659
|
-
fastPathChecks.push(check)
|
|
660
|
-
} else {
|
|
661
|
-
fastPathChecks.push(`(${varName} === undefined || ${check})`)
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// Second pass: generate variable declarations only for properties that need them
|
|
669
|
-
for (const { key, varName, propSchema } of propInfo) {
|
|
670
|
-
if (shouldCacheVariable(propSchema, canFastPath, useRefImports)) {
|
|
671
|
-
lines.push(` const ${varName} = ${safeAccessor('input', key)};`)
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// Emit a warning for any input key not declared in the schema's properties
|
|
676
|
-
if (logWarnings && propInfo.length > 0) {
|
|
677
|
-
const keysList = propInfo.map(({ key }) => JSON.stringify(key)).join(', ')
|
|
678
|
-
lines.push(` const _knownKeys = new Set([${keysList}]);`)
|
|
679
|
-
lines.push(` for (const _k in input) {`)
|
|
680
|
-
lines.push(` if (!_knownKeys.has(_k)) {`)
|
|
681
|
-
lines.push(` console.warn(\`[${typeName}] Unknown property "\${_k}"\`);`)
|
|
682
|
-
lines.push(` }`)
|
|
683
|
-
lines.push(` }`)
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// Generate fast-path check if possible
|
|
687
|
-
if (canFastPath && fastPathChecks.length > 0) {
|
|
688
|
-
let fastPathExpr = fastPathChecks[0]
|
|
689
|
-
for (let i = 1; i < fastPathChecks.length; i++) {
|
|
690
|
-
fastPathExpr += ' && ' + fastPathChecks[i]
|
|
691
|
-
}
|
|
692
|
-
lines.push(` if (${fastPathExpr}) return { ...input } as ${typeName};`)
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
// Generate slow-path object construction
|
|
696
|
-
const objectLines: string[] = []
|
|
697
|
-
objectLines.push(' ...input,')
|
|
698
|
-
|
|
699
|
-
// Spread allOf $ref parsers so their field coercions are applied before
|
|
700
|
-
// the explicit property validations below.
|
|
701
|
-
if (useRefImports && isSchemaObject(schema) && hasAllOf(schema)) {
|
|
702
|
-
for (const entry of schema.allOf) {
|
|
703
|
-
if (isSchemaObject(entry) && hasRef(entry)) {
|
|
704
|
-
const parserName = generateParserName(refToName(entry.$ref, suffix))
|
|
705
|
-
objectLines.push(` ...(${parserName}(input) as Record<string, unknown>),`)
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
for (const { key, varName, isRequired, propSchema } of propInfo) {
|
|
711
|
-
const shouldCache = shouldCacheVariable(propSchema, canFastPath, useRefImports)
|
|
712
|
-
const accessor = shouldCache ? varName : safeAccessor('input', key)
|
|
713
|
-
|
|
714
|
-
// Handle direct $ref properties via imported parsers
|
|
715
|
-
if (shouldUseRefImport(propSchema, useRefImports)) {
|
|
716
|
-
const ref = (propSchema as { $ref: string }).$ref
|
|
717
|
-
const parserName = generateParserName(refToName(ref, suffix))
|
|
718
|
-
if (isRequired) {
|
|
719
|
-
objectLines.push(` ${safeKey(key)}: ${parserName}(${accessor}),`)
|
|
720
|
-
} else {
|
|
721
|
-
objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: ${parserName}(${accessor}) }),`)
|
|
722
|
-
}
|
|
723
|
-
continue
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// Handle array properties with $ref items
|
|
727
|
-
if (shouldUseArrayRefImport(propSchema, useRefImports)) {
|
|
728
|
-
const items = (propSchema as { items: { $ref: string } }).items
|
|
729
|
-
const ref = items.$ref
|
|
730
|
-
const parserName = generateParserName(refToName(ref, suffix))
|
|
731
|
-
if (isRequired) {
|
|
732
|
-
objectLines.push(` ${safeKey(key)}: validateArray(${accessor}, ${parserName}),`)
|
|
733
|
-
} else {
|
|
734
|
-
objectLines.push(
|
|
735
|
-
` ...(${accessor} !== undefined && { ${safeKey(key)}: validateArray(${accessor}, ${parserName}) }),`,
|
|
736
|
-
)
|
|
737
|
-
}
|
|
738
|
-
continue
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// Handle object properties with additionalProperties $ref
|
|
742
|
-
if (shouldUseRecordRefImport(propSchema, useRefImports)) {
|
|
743
|
-
const additionalProps = (propSchema as { additionalProperties: { $ref: string } }).additionalProperties
|
|
744
|
-
const ref = additionalProps.$ref
|
|
745
|
-
const parserName = generateParserName(refToName(ref, suffix))
|
|
746
|
-
if (isRequired) {
|
|
747
|
-
objectLines.push(` ${safeKey(key)}: validateRecord(${accessor}, ${parserName}),`)
|
|
748
|
-
} else {
|
|
749
|
-
objectLines.push(
|
|
750
|
-
` ...(${accessor} !== undefined && { ${safeKey(key)}: validateRecord(${accessor}, ${parserName}) }),`,
|
|
751
|
-
)
|
|
752
|
-
}
|
|
753
|
-
continue
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// Handle non-schema-object properties
|
|
757
|
-
if (!isSchemaObject(propSchema)) {
|
|
758
|
-
if (isRequired) {
|
|
759
|
-
objectLines.push(` ${safeKey(key)}: undefined,`)
|
|
760
|
-
}
|
|
761
|
-
continue
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// Generate validation expression
|
|
765
|
-
const defaultValue = getDefaultValue(propSchema)
|
|
766
|
-
// For optional properties in the slow path, we know the value is not undefined
|
|
767
|
-
// because we're inside the `...(accessor !== undefined && { ... })` check
|
|
768
|
-
const knownNotUndefined = !isRequired
|
|
769
|
-
const valueExpr = generateValidationExpression(
|
|
770
|
-
key,
|
|
771
|
-
propSchema,
|
|
772
|
-
defaultValue,
|
|
773
|
-
true,
|
|
774
|
-
undefined,
|
|
775
|
-
undefined,
|
|
776
|
-
shouldCache ? varName : undefined,
|
|
777
|
-
knownNotUndefined,
|
|
778
|
-
)
|
|
779
|
-
|
|
780
|
-
if (isRequired) {
|
|
781
|
-
objectLines.push(` ${safeKey(key)}: ${valueExpr},`)
|
|
782
|
-
} else {
|
|
783
|
-
objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: ${valueExpr} }),`)
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
lines.push(` return {`)
|
|
788
|
-
// objectLines[0] is safe: objectLines is always non-empty (has at least the closing brace line)
|
|
789
|
-
let objectBody = objectLines[0] as string
|
|
790
|
-
for (let i = 1; i < objectLines.length; i++) {
|
|
791
|
-
objectBody += '\n' + objectLines[i]
|
|
792
|
-
}
|
|
793
|
-
lines.push(objectBody)
|
|
794
|
-
lines.push(` } as unknown as ${typeName};`)
|
|
795
|
-
lines.push(`}`)
|
|
796
|
-
|
|
797
|
-
// lines[0] is safe: lines is always non-empty (it always has the function declaration)
|
|
798
|
-
let result = lines[0] as string
|
|
799
|
-
for (let i = 1; i < lines.length; i++) {
|
|
800
|
-
result += '\n' + lines[i]
|
|
801
|
-
}
|
|
802
|
-
return result
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
/**
|
|
806
|
-
* Generates a parser for schemas that have both properties AND patternProperties.
|
|
807
|
-
* Parses the known properties first, then iterates remaining keys to match patterns.
|
|
808
|
-
*/
|
|
809
|
-
const generateCombinedObjectParser = (
|
|
810
|
-
schema: JSONSchema,
|
|
811
|
-
typeName: string,
|
|
812
|
-
useRefImports: boolean,
|
|
813
|
-
suffix: string,
|
|
814
|
-
logWarnings?: boolean,
|
|
815
|
-
strict?: boolean,
|
|
816
|
-
): string => {
|
|
817
|
-
const functionName = generateParserName(typeName)
|
|
818
|
-
const entries = generatePropertyEntries(schema, useRefImports, suffix)
|
|
819
|
-
|
|
820
|
-
// Build the known property lines for the initial object
|
|
821
|
-
const propertyLines = entries.map((entry) => {
|
|
822
|
-
if (entry.isOptional) {
|
|
823
|
-
return ` ${entry.value},`
|
|
824
|
-
}
|
|
825
|
-
return ` ${safeKey(entry.key)}: ${entry.value},`
|
|
826
|
-
})
|
|
827
|
-
|
|
828
|
-
// Find the first pattern with a $ref for parser delegation
|
|
829
|
-
if (!isSchemaObject(schema) || !('patternProperties' in schema)) {
|
|
830
|
-
return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict)
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
const patternProps = schema.patternProperties as Record<string, JSONSchema>
|
|
834
|
-
const patterns = Object.entries(patternProps)
|
|
835
|
-
const refPattern = patterns.find(([, ps]) => isSchemaObject(ps) && hasRef(ps))
|
|
836
|
-
|
|
837
|
-
if (!refPattern || !useRefImports) {
|
|
838
|
-
return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict)
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
const [pattern, patternSchema] = refPattern
|
|
842
|
-
const ref = (patternSchema as { $ref: string }).$ref
|
|
843
|
-
const parserName = generateParserName(refToName(ref, suffix))
|
|
844
|
-
const assignmentCode = `(result as Record<string, unknown>)[key] = ${parserName}(value);`
|
|
845
|
-
|
|
846
|
-
const escapedPattern = escapeRegexPattern(pattern)
|
|
847
|
-
|
|
848
|
-
const inputSpread = ' ...input,'
|
|
849
|
-
let objectProperties = inputSpread
|
|
850
|
-
if (propertyLines.length > 0) {
|
|
851
|
-
for (const line of propertyLines) {
|
|
852
|
-
objectProperties += '\n' + line
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
const notObjectBranch = strict
|
|
857
|
-
? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
|
|
858
|
-
: ` return {} as unknown as ${typeName};`
|
|
859
|
-
|
|
860
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => {
|
|
861
|
-
if (!isObject(input)) {
|
|
862
|
-
${notObjectBranch}
|
|
863
|
-
}
|
|
864
|
-
const result = {
|
|
865
|
-
${objectProperties}
|
|
866
|
-
} as unknown as ${typeName};
|
|
867
|
-
for (const key in input) {
|
|
868
|
-
if (/${escapedPattern}/.test(key)) {
|
|
869
|
-
const value = input[key];
|
|
870
|
-
${assignmentCode}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
return result;
|
|
874
|
-
};`
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
/**
|
|
878
|
-
* Generates a parser for schemas with additionalProperties.
|
|
879
|
-
*/
|
|
880
|
-
const generateAdditionalPropertiesParser = (
|
|
881
|
-
schema: JSONSchema.Object,
|
|
882
|
-
typeName: string,
|
|
883
|
-
useRefImports: boolean,
|
|
884
|
-
suffix: string,
|
|
885
|
-
strict?: boolean,
|
|
886
|
-
): string => {
|
|
887
|
-
const functionName = generateParserName(typeName)
|
|
888
|
-
const additionalProps = schema.additionalProperties
|
|
889
|
-
|
|
890
|
-
// Check if additionalProps is defined before using it
|
|
891
|
-
if (!additionalProps) {
|
|
892
|
-
return generateEmptyObjectParser(typeName, strict)
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// If additionalProperties is a $ref and useRefImports is true, generate a loop
|
|
896
|
-
if (useRefImports && isSchemaObject(additionalProps) && hasRef(additionalProps)) {
|
|
897
|
-
const ref = additionalProps.$ref
|
|
898
|
-
const parserName = generateParserName(refToName(ref, suffix))
|
|
899
|
-
|
|
900
|
-
if (strict) {
|
|
901
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return validateRecord(input, ${parserName}) as ${typeName};\n};`
|
|
902
|
-
}
|
|
903
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${parserName}) as ${typeName};`
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// Handle additionalProperties with a known type by generating inline validation
|
|
907
|
-
if (isSchemaObject(additionalProps) && hasType(additionalProps)) {
|
|
908
|
-
const inlineParser = generateInlineValueParser(additionalProps)
|
|
909
|
-
if (inlineParser) {
|
|
910
|
-
if (strict) {
|
|
911
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return validateRecord(input, ${inlineParser}) as ${typeName};\n};`
|
|
912
|
-
}
|
|
913
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${inlineParser}) as ${typeName};`
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
// Otherwise, just validate the input type and shallow copy
|
|
918
|
-
if (strict) {
|
|
919
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return { ...input } as ${typeName};\n};`
|
|
920
|
-
}
|
|
921
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {};`
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/**
|
|
925
|
-
* Generates an inline arrow function that validates a single value based on
|
|
926
|
-
* the additionalProperties schema type. Returns null if the type is not supported.
|
|
927
|
-
*
|
|
928
|
-
* Used to generate parsers for Record-like types where values have a known
|
|
929
|
-
* primitive or array type (e.g. Record<string, string> or Record<string, string[]>).
|
|
930
|
-
*/
|
|
931
|
-
const generateInlineValueParser = (schema: JSONSchema): string | null => {
|
|
932
|
-
if (!isSchemaObject(schema) || !hasType(schema)) {
|
|
933
|
-
return null
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
switch (schema.type) {
|
|
937
|
-
case 'string':
|
|
938
|
-
return '(value: unknown) => typeof value === "string" ? value : ""'
|
|
939
|
-
case 'number':
|
|
940
|
-
case 'integer':
|
|
941
|
-
return '(value: unknown) => typeof value === "number" ? value : 0'
|
|
942
|
-
case 'boolean':
|
|
943
|
-
return '(value: unknown) => typeof value === "boolean" ? value : false'
|
|
944
|
-
case 'array':
|
|
945
|
-
return '(value: unknown) => Array.isArray(value) ? value : []'
|
|
946
|
-
default:
|
|
947
|
-
return null
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
/**
|
|
952
|
-
* Generates a parser for schemas with patternProperties.
|
|
953
|
-
* Handles both patternProperties and specification-extensions (x- prefix).
|
|
954
|
-
*/
|
|
955
|
-
const generatePatternPropertiesParser = (
|
|
956
|
-
schema: JSONSchema.Object,
|
|
957
|
-
typeName: string,
|
|
958
|
-
useRefImports: boolean,
|
|
959
|
-
suffix: string,
|
|
960
|
-
strict?: boolean,
|
|
961
|
-
): string => {
|
|
962
|
-
const functionName = generateParserName(typeName)
|
|
963
|
-
|
|
964
|
-
if (!('patternProperties' in schema) || typeof schema.patternProperties !== 'object') {
|
|
965
|
-
return generateEmptyObjectParser(typeName, strict)
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
const patternProps = schema.patternProperties as Record<string, JSONSchema>
|
|
969
|
-
|
|
970
|
-
// Find the first pattern and its schema
|
|
971
|
-
const patterns = Object.entries(patternProps)
|
|
972
|
-
if (patterns.length === 0) {
|
|
973
|
-
return generateEmptyObjectParser(typeName, strict)
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
const [pattern, patternSchema] = patterns[0] as [string, JSONSchema]
|
|
977
|
-
let patternAssignment = '(result as Record<string, unknown>)[key] = value;'
|
|
978
|
-
|
|
979
|
-
// Use imported parser when pattern schema points to a $ref.
|
|
980
|
-
if (useRefImports && isSchemaObject(patternSchema) && hasRef(patternSchema)) {
|
|
981
|
-
const ref = patternSchema.$ref
|
|
982
|
-
const parserName = generateParserName(refToName(ref, suffix))
|
|
983
|
-
patternAssignment = `(result as Record<string, unknown>)[key] = ${parserName}(value);`
|
|
984
|
-
} else if (patternSchema === false) {
|
|
985
|
-
// `false` means no values are allowed for matching keys.
|
|
986
|
-
patternAssignment = ''
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// Escape the pattern for safe inclusion in generated code
|
|
990
|
-
const escapedPattern = escapeRegexPattern(pattern)
|
|
991
|
-
|
|
992
|
-
const notObjectBranch = strict
|
|
993
|
-
? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
|
|
994
|
-
: ` return {} as unknown as ${typeName};`
|
|
995
|
-
|
|
996
|
-
// Generate a parser that handles both pattern matching and x- extensions
|
|
997
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => {
|
|
998
|
-
if (!isObject(input)) {
|
|
999
|
-
${notObjectBranch}
|
|
1000
|
-
}
|
|
1001
|
-
const result = {
|
|
1002
|
-
...input,
|
|
1003
|
-
} as unknown as ${typeName};
|
|
1004
|
-
for (const key in input) {
|
|
1005
|
-
if (/${escapedPattern}/.test(key)) {
|
|
1006
|
-
const value = input[key];
|
|
1007
|
-
${patternAssignment}
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
return result;
|
|
1011
|
-
};`
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
/**
|
|
1015
|
-
* Generates a parser for conditional schemas with if/then/else.
|
|
1016
|
-
* This always generates parser calls regardless of useRefImports setting,
|
|
1017
|
-
* because conditional logic requires delegating to the appropriate parser.
|
|
1018
|
-
*/
|
|
1019
|
-
const generateConditionalParser = (schema: JSONSchema.Object, typeName: string, suffix: string): string => {
|
|
1020
|
-
const functionName = generateParserName(typeName)
|
|
1021
|
-
|
|
1022
|
-
// Extract the condition and branches
|
|
1023
|
-
const ifSchema = schema.if
|
|
1024
|
-
const thenSchema = schema.then
|
|
1025
|
-
const elseSchema = schema.else
|
|
1026
|
-
|
|
1027
|
-
// Check if branches are defined and have $ref
|
|
1028
|
-
const thenHasRef = thenSchema && isSchemaObject(thenSchema) && hasRef(thenSchema)
|
|
1029
|
-
const elseHasRef = elseSchema && isSchemaObject(elseSchema) && hasRef(elseSchema)
|
|
1030
|
-
|
|
1031
|
-
if (!thenHasRef || !elseHasRef) {
|
|
1032
|
-
// Non-$ref branches (e.g. if/then/else providing defaults for properties).
|
|
1033
|
-
// Flatten all three branches into a single object schema and generate a
|
|
1034
|
-
// regular object parser from the merged property set.
|
|
1035
|
-
const mergedSchema = getConditionalObjectSchema(schema)
|
|
1036
|
-
if (mergedSchema) {
|
|
1037
|
-
return generateObjectParser(mergedSchema, typeName, false, suffix)
|
|
1038
|
-
}
|
|
1039
|
-
return generateEmptyObjectParser(typeName)
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
// Check if the condition is checking for $ref property
|
|
1043
|
-
const isRefCondition =
|
|
1044
|
-
ifSchema &&
|
|
1045
|
-
isSchemaObject(ifSchema) &&
|
|
1046
|
-
'required' in ifSchema &&
|
|
1047
|
-
Array.isArray(ifSchema.required) &&
|
|
1048
|
-
ifSchema.required.includes('$ref')
|
|
1049
|
-
|
|
1050
|
-
if (!isRefCondition) {
|
|
1051
|
-
// Condition is not a $ref check — flatten into an object parser
|
|
1052
|
-
const mergedSchema = getConditionalObjectSchema(schema)
|
|
1053
|
-
if (mergedSchema) {
|
|
1054
|
-
return generateObjectParser(mergedSchema, typeName, false, suffix)
|
|
1055
|
-
}
|
|
1056
|
-
return generateEmptyObjectParser(typeName)
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
const thenRef = thenSchema.$ref
|
|
1060
|
-
const elseRef = elseSchema.$ref
|
|
1061
|
-
|
|
1062
|
-
const thenParserName = generateParserName(refToName(thenRef, suffix))
|
|
1063
|
-
const elseParserName = generateParserName(refToName(elseRef, suffix))
|
|
1064
|
-
const thenTypeName = refToName(thenRef, suffix)
|
|
1065
|
-
|
|
1066
|
-
return `export const ${functionName} = (input: unknown): ${typeName} | ${thenTypeName} =>
|
|
1067
|
-
hasRef(input) ? ${thenParserName}(input) : ${elseParserName}(input)
|
|
1068
|
-
`
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
/**
|
|
1072
|
-
* Builds an object schema from conditional if/then keywords when the schema does not
|
|
1073
|
-
* declare type: "object". Flattens if/then/else property sets into a single object schema.
|
|
1074
|
-
*/
|
|
1075
|
-
const getConditionalObjectSchema = (schema: JSONSchema): JSONSchema.Object | null => {
|
|
1076
|
-
if (!isSchemaObject(schema)) {
|
|
1077
|
-
return null
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
if (!('if' in schema) || !('then' in schema)) {
|
|
1081
|
-
return null
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
const ifSchema = schema.if
|
|
1085
|
-
const thenSchema = schema.then
|
|
1086
|
-
const elseSchema = 'else' in schema ? schema.else : undefined
|
|
1087
|
-
|
|
1088
|
-
if (!isSchemaObject(ifSchema) || !isSchemaObject(thenSchema)) {
|
|
1089
|
-
return null
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
const ifProperties = ifSchema.properties
|
|
1093
|
-
const thenProperties = thenSchema.properties
|
|
1094
|
-
const elseProperties = elseSchema && isSchemaObject(elseSchema) ? elseSchema.properties : undefined
|
|
1095
|
-
const hasIfProperties = ifProperties && typeof ifProperties === 'object'
|
|
1096
|
-
const hasThenProperties = thenProperties && typeof thenProperties === 'object'
|
|
1097
|
-
const hasElseProperties = elseProperties && typeof elseProperties === 'object'
|
|
1098
|
-
|
|
1099
|
-
if (!hasIfProperties && !hasThenProperties && !hasElseProperties) {
|
|
1100
|
-
return null
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
const required = new Set<string>()
|
|
1104
|
-
|
|
1105
|
-
if (Array.isArray(ifSchema.required)) {
|
|
1106
|
-
for (const key of ifSchema.required) {
|
|
1107
|
-
required.add(key)
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// If the `if` condition checks for a `const` value on a property, that property
|
|
1112
|
-
// is effectively required (the schema only applies when the condition is true).
|
|
1113
|
-
if (hasIfProperties && ifProperties && typeof ifProperties === 'object') {
|
|
1114
|
-
for (const [key, propSchema] of Object.entries(ifProperties as Record<string, JSONSchema>)) {
|
|
1115
|
-
if (isSchemaObject(propSchema) && hasConst(propSchema)) {
|
|
1116
|
-
required.add(key)
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
if (Array.isArray(thenSchema.required)) {
|
|
1122
|
-
for (const key of thenSchema.required) {
|
|
1123
|
-
required.add(key)
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
return {
|
|
1128
|
-
type: 'object',
|
|
1129
|
-
properties: {
|
|
1130
|
-
// else properties first so then properties take precedence on overlap
|
|
1131
|
-
...(hasElseProperties ? elseProperties : {}),
|
|
1132
|
-
...(hasIfProperties ? ifProperties : {}),
|
|
1133
|
-
...(hasThenProperties ? thenProperties : {}),
|
|
1134
|
-
},
|
|
1135
|
-
...(required.size > 0 ? { required: Array.from(required) } : {}),
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
/**
|
|
1140
|
-
* Generates a parser for SchemaObject that validates all JSON Schema 2020-12 properties.
|
|
1141
|
-
* This handles the special case where a schema can be any valid JSON Schema.
|
|
1142
|
-
*/
|
|
1143
|
-
const generateSchemaObjectParser = (typeName: string): string => {
|
|
1144
|
-
const functionName = generateParserName(typeName)
|
|
1145
|
-
|
|
1146
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => {
|
|
1147
|
-
if (typeof input === 'boolean') {
|
|
1148
|
-
return input as ${typeName};
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
if (!isObject(input)) {
|
|
1152
|
-
return {} as ${typeName};
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
return input as ${typeName};
|
|
1156
|
-
};`
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
/**
|
|
1160
|
-
* Determines the appropriate parser generation strategy for a schema.
|
|
1161
|
-
*/
|
|
1162
|
-
const selectParserStrategy = (schema: JSONSchema, typeName: string, options?: GenerateParserOptions): string => {
|
|
1163
|
-
const useRefImports = options?.useRefImports ?? false
|
|
1164
|
-
const logWarnings = options?.logWarnings ?? false
|
|
1165
|
-
const strict = options?.strict ?? false
|
|
1166
|
-
const suffix = options?.typeSuffix ?? ''
|
|
1167
|
-
|
|
1168
|
-
// Special case for the self-referential JSON Schema meta-schema type (e.g.
|
|
1169
|
-
// `Schema` / `SchemaObject`) - it can be any JSON Schema.
|
|
1170
|
-
if (typeName === `Schema${suffix}`) {
|
|
1171
|
-
return generateSchemaObjectParser(typeName)
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
const isObjectLikeSchema =
|
|
1175
|
-
isObjectSchema(schema) ||
|
|
1176
|
-
(isSchemaObject(schema) && ('patternProperties' in schema || 'additionalProperties' in schema))
|
|
1177
|
-
|
|
1178
|
-
// Handle non-object schemas with type-appropriate validation
|
|
1179
|
-
if (!isObjectLikeSchema && !isSchemaObject(schema)) {
|
|
1180
|
-
return generateNonObjectParser(typeName, schema, strict)
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// Handle schemas with both properties AND patternProperties.
|
|
1184
|
-
// This generates a parser that handles known properties and also iterates
|
|
1185
|
-
// pattern-matched keys (e.g. responses with "default" + "200", "4XX").
|
|
1186
|
-
if (hasProperties(schema) && isSchemaObject(schema) && 'patternProperties' in schema) {
|
|
1187
|
-
return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict)
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
// Handle schemas that have explicit properties — generate a full object parser.
|
|
1191
|
-
// This intentionally runs before if/then checks so that schemas with both
|
|
1192
|
-
// properties AND conditional keywords use all declared properties rather than
|
|
1193
|
-
// only the if/then fragment.
|
|
1194
|
-
if (hasProperties(schema)) {
|
|
1195
|
-
return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict)
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
// Handle conditional schemas (if/then/else) for schemas without explicit properties.
|
|
1199
|
-
if (isSchemaObject(schema) && 'if' in schema && 'then' in schema && 'else' in schema) {
|
|
1200
|
-
return generateConditionalParser(schema as JSONSchema.Object, typeName, suffix)
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
// Handle conditional schemas that only define if/then object fragments.
|
|
1204
|
-
// We flatten the fragments into a regular object parser.
|
|
1205
|
-
const conditionalObjectSchema = getConditionalObjectSchema(schema)
|
|
1206
|
-
if (conditionalObjectSchema) {
|
|
1207
|
-
return generateObjectParser(conditionalObjectSchema, typeName, useRefImports, suffix, logWarnings, strict)
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
// Handle non-object schemas with type-appropriate validation (no properties, no conditionals)
|
|
1211
|
-
if (!isObjectLikeSchema) {
|
|
1212
|
-
return generateNonObjectParser(typeName, schema, strict)
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// Handle schemas with patternProperties (but no properties)
|
|
1216
|
-
if ('patternProperties' in schema) {
|
|
1217
|
-
return generatePatternPropertiesParser(schema as JSONSchema.Object, typeName, useRefImports, suffix, strict)
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
// Handle schemas with additionalProperties as true or false (but no properties)
|
|
1221
|
-
if ('additionalProperties' in schema && !hasProperties(schema)) {
|
|
1222
|
-
const additionalProps = schema.additionalProperties
|
|
1223
|
-
|
|
1224
|
-
// If additionalProperties is true or false, validate it is an object
|
|
1225
|
-
if (additionalProps === true || additionalProps === false) {
|
|
1226
|
-
return generateEmptyObjectParser(typeName, strict)
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
// Otherwise, handle as a schema (could be a $ref or object schema)
|
|
1230
|
-
return generateAdditionalPropertiesParser(schema as JSONSchema.Object, typeName, useRefImports, suffix, strict)
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
// Handle empty object schemas
|
|
1234
|
-
if ('type' in schema && schema.type === 'object' && !hasProperties(schema)) {
|
|
1235
|
-
return generateEmptyObjectParser(typeName, strict)
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
// Default fallback - validate it is an object since we passed the isObjectSchema check
|
|
1239
|
-
return generateEmptyObjectParser(typeName, strict)
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
/**
|
|
1243
|
-
* Generates a safe parser as an arrow function that never throws.
|
|
1244
|
-
* The parser provides default values for all missing or invalid fields.
|
|
1245
|
-
*
|
|
1246
|
-
* When useRefImports is enabled, properties with $ref will call the imported
|
|
1247
|
-
* parser function (e.g., parseContact(input?.contact)) instead of inlining
|
|
1248
|
-
* the resolved schema's validation logic. Array properties whose items are a
|
|
1249
|
-
* $ref will map each element through the imported parser.
|
|
1250
|
-
*/
|
|
1251
|
-
export const generateParserFunction = (
|
|
1252
|
-
schema: JSONSchema,
|
|
1253
|
-
typeName: string,
|
|
1254
|
-
options?: GenerateParserOptions,
|
|
1255
|
-
): string => {
|
|
1256
|
-
return selectParserStrategy(schema, typeName, options)
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
/**
|
|
1260
|
-
* Returns the predicate function name for a generated shape validator.
|
|
1261
|
-
* @example shapeValidatorFunctionName('CustomerObject') // 'validateCustomerObjectShape'
|
|
1262
|
-
*/
|
|
1263
|
-
export const shapeValidatorFunctionName = (typeName: string): string => shapeValidatorName(typeName)
|
|
1264
|
-
|
|
1265
|
-
/**
|
|
1266
|
-
* Generates a `validate{TypeName}Shape(input)` predicate that returns true
|
|
1267
|
-
* iff `input` already matches the shape produced by the parser's fast path —
|
|
1268
|
-
* i.e. the parser would return `{ ...input } as TypeName` without coercion.
|
|
1269
|
-
*
|
|
1270
|
-
* Parents call this predicate to fast-path through nested ref properties
|
|
1271
|
-
* (and arrays of refs) without recursing into their parser.
|
|
1272
|
-
*
|
|
1273
|
-
* Returns a stub that always returns `false` for schemas that cannot be
|
|
1274
|
-
* predicated (composition, conditionals, pattern properties, complex refs
|
|
1275
|
-
* in additionalProperties). The stub is safe: parents calling it will fall
|
|
1276
|
-
* through to the slow path, matching the pre-deep-fast-path behavior.
|
|
1277
|
-
*/
|
|
1278
|
-
export const generateShapeValidator = (
|
|
1279
|
-
schema: JSONSchema,
|
|
1280
|
-
typeName: string,
|
|
1281
|
-
useRefImports: boolean,
|
|
1282
|
-
suffix = '',
|
|
1283
|
-
): string => {
|
|
1284
|
-
const fnName = shapeValidatorName(typeName)
|
|
1285
|
-
const stub = `export const ${fnName} = (_input: unknown): boolean => false;`
|
|
1286
|
-
|
|
1287
|
-
if (!isSchemaObject(schema)) return stub
|
|
1288
|
-
if (!hasProperties(schema)) return stub
|
|
1289
|
-
|
|
1290
|
-
// Composition / conditional schemas can match in many shapes — bail.
|
|
1291
|
-
if (
|
|
1292
|
-
hasOneOf(schema) ||
|
|
1293
|
-
hasAnyOf(schema) ||
|
|
1294
|
-
hasAllOf(schema) ||
|
|
1295
|
-
'not' in schema ||
|
|
1296
|
-
'patternProperties' in schema ||
|
|
1297
|
-
'if' in schema ||
|
|
1298
|
-
'then' in schema ||
|
|
1299
|
-
'else' in schema
|
|
1300
|
-
) {
|
|
1301
|
-
return stub
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
const properties = Object.entries((schema as { properties: Record<string, JSONSchema> }).properties)
|
|
1305
|
-
const checks: string[] = []
|
|
1306
|
-
|
|
1307
|
-
for (const [key, propSchema] of properties) {
|
|
1308
|
-
// Records of refs require iterating every value — too expensive for the fast path.
|
|
1309
|
-
if (shouldUseRecordRefImport(propSchema, useRefImports)) {
|
|
1310
|
-
return stub
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
const accessor = safeAccessor('input', key)
|
|
1314
|
-
const isRequired = isPropertyRequired(key, schema)
|
|
1315
|
-
const check = generatePropertyTypeCheck(accessor, propSchema, useRefImports, suffix)
|
|
1316
|
-
if (check === null) {
|
|
1317
|
-
return stub
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
if (isRequired) {
|
|
1321
|
-
checks.push(check)
|
|
1322
|
-
} else {
|
|
1323
|
-
checks.push(`(${accessor} === undefined || ${check})`)
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
if (checks.length === 0) {
|
|
1328
|
-
return `export const ${fnName} = (input: unknown): boolean => isObject(input);`
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
let body = checks[0] as string
|
|
1332
|
-
for (let i = 1; i < checks.length; i++) {
|
|
1333
|
-
body += '\n && ' + checks[i]
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
return `export const ${fnName} = (input: unknown): boolean => {
|
|
1337
|
-
if (!isObject(input)) return false;
|
|
1338
|
-
return ${body};
|
|
1339
|
-
};`
|
|
1340
|
-
}
|