@amritk/generate-parsers 0.2.1 → 0.3.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/dist/generators/build-schema.d.ts +5 -1
- package/dist/generators/generate-files.d.ts +15 -2
- package/dist/helpers/collect-helpers.d.ts +16 -2
- package/dist/index.js +64 -19
- package/package.json +3 -2
- package/src/generators/build-schema.test.ts +830 -0
- package/src/generators/build-schema.ts +280 -0
- package/src/generators/generate-default-from-pattern.test.ts +81 -0
- package/src/generators/generate-default-from-pattern.ts +39 -0
- package/src/generators/generate-discriminated-union-validation.test.ts +84 -0
- package/src/generators/generate-discriminated-union-validation.ts +54 -0
- package/src/generators/generate-enum-check.test.ts +45 -0
- package/src/generators/generate-enum-check.ts +8 -0
- package/src/generators/generate-files.test.ts +435 -0
- package/src/generators/generate-files.ts +136 -0
- package/src/generators/generate-non-discriminated-union-validation.test.ts +74 -0
- package/src/generators/generate-non-discriminated-union-validation.ts +40 -0
- package/src/generators/generate-parser-function.test.ts +1798 -0
- package/src/generators/generate-parser-function.ts +1319 -0
- package/src/generators/generate-schema-checks.test.ts +322 -0
- package/src/generators/generate-schema-checks.ts +114 -0
- package/src/generators/generate-strict-assertion.ts +210 -0
- package/src/generators/generate-validation-expression.test.ts +794 -0
- package/src/generators/generate-validation-expression.ts +286 -0
- package/src/helpers/apply-schema-extensions.test.ts +194 -0
- package/src/helpers/apply-schema-extensions.ts +63 -0
- package/src/helpers/collect-helpers.test.ts +103 -0
- package/src/helpers/collect-helpers.ts +64 -0
- package/src/helpers/collect-imports.test.ts +587 -0
- package/src/helpers/collect-imports.ts +268 -0
- package/src/helpers/find-discriminator.test.ts +88 -0
- package/src/helpers/find-discriminator.ts +39 -0
- package/src/helpers/get-default-value.test.ts +135 -0
- package/src/helpers/get-default-value.ts +89 -0
- package/src/helpers/get-discriminator-value.test.ts +83 -0
- package/src/helpers/get-discriminator-value.ts +21 -0
- package/src/index.ts +1 -0
- package/src/types/schema-extensions.ts +35 -0
|
@@ -0,0 +1,1798 @@
|
|
|
1
|
+
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { generateParserFunction } from './generate-parser-function'
|
|
5
|
+
|
|
6
|
+
describe('generate-parser-function', () => {
|
|
7
|
+
it('generates parser function for simple object schema', () => {
|
|
8
|
+
const schema: JSONSchema = {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
name: { type: 'string' },
|
|
12
|
+
age: { type: 'number' },
|
|
13
|
+
},
|
|
14
|
+
required: ['name'],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const result = generateParserFunction(schema, 'UserObject')
|
|
18
|
+
expect(result).toBe(
|
|
19
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
20
|
+
if (!isObject(input)) return {
|
|
21
|
+
name: "",
|
|
22
|
+
};
|
|
23
|
+
const _name = input.name;
|
|
24
|
+
const _age = input.age;
|
|
25
|
+
if (typeof _name === "string" && (_age === undefined || typeof _age === "number")) return { ...input } as UserObject;
|
|
26
|
+
return {
|
|
27
|
+
...input,
|
|
28
|
+
name: typeof _name === "string" ? _name : (_name !== undefined ? String(_name) : ""),
|
|
29
|
+
...(_age !== undefined && { age: typeof _age === "number" ? _age : (Number.isFinite(Number(_age)) ? Number(_age) : 0) }),
|
|
30
|
+
} as unknown as UserObject;
|
|
31
|
+
}`,
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('generates parser function with correct type name', () => {
|
|
36
|
+
const schema: JSONSchema = {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
id: { type: 'number' },
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = generateParserFunction(schema, 'ProductObject')
|
|
44
|
+
expect(result).toBe(
|
|
45
|
+
`export const parseProductObject = (input: unknown): ProductObject => {
|
|
46
|
+
if (!isObject(input)) return {} as ProductObject;
|
|
47
|
+
const _id = input.id;
|
|
48
|
+
if ((_id === undefined || typeof _id === "number")) return { ...input } as ProductObject;
|
|
49
|
+
return {
|
|
50
|
+
...input,
|
|
51
|
+
...(_id !== undefined && { id: typeof _id === "number" ? _id : (Number.isFinite(Number(_id)) ? Number(_id) : 0) }),
|
|
52
|
+
} as unknown as ProductObject;
|
|
53
|
+
}`,
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('handles object schema without properties', () => {
|
|
58
|
+
const schema: JSONSchema = {
|
|
59
|
+
type: 'object',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = generateParserFunction(schema, 'EmptyObject')
|
|
63
|
+
expect(result).toBe(
|
|
64
|
+
'export const parseEmptyObject = (input: unknown): EmptyObject => isObject(input) ? { ...input } as EmptyObject : {} as EmptyObject;',
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('handles non-object schema with type validation', () => {
|
|
69
|
+
const schema: JSONSchema = {
|
|
70
|
+
type: 'string',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = generateParserFunction(schema, 'StringType')
|
|
74
|
+
expect(result).toBe(
|
|
75
|
+
'export const parseStringType = (input: unknown): StringType => typeof input === "string" ? input as StringType : "" as StringType;',
|
|
76
|
+
)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('handles schema with required fields', () => {
|
|
80
|
+
const schema: JSONSchema = {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
id: { type: 'number' },
|
|
84
|
+
name: { type: 'string' },
|
|
85
|
+
email: { type: 'string' },
|
|
86
|
+
},
|
|
87
|
+
required: ['id', 'name'],
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = generateParserFunction(schema, 'UserObject')
|
|
91
|
+
|
|
92
|
+
expect(result).toBe(
|
|
93
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
94
|
+
if (!isObject(input)) return {
|
|
95
|
+
id: 0,
|
|
96
|
+
name: "",
|
|
97
|
+
};
|
|
98
|
+
const _id = input.id;
|
|
99
|
+
const _name = input.name;
|
|
100
|
+
const _email = input.email;
|
|
101
|
+
if (typeof _id === "number" && typeof _name === "string" && (_email === undefined || typeof _email === "string")) return { ...input } as UserObject;
|
|
102
|
+
return {
|
|
103
|
+
...input,
|
|
104
|
+
id: typeof _id === "number" ? _id : (_id !== undefined ? (Number.isFinite(Number(_id)) ? Number(_id) : 0) : 0),
|
|
105
|
+
name: typeof _name === "string" ? _name : (_name !== undefined ? String(_name) : ""),
|
|
106
|
+
...(_email !== undefined && { email: typeof _email === "string" ? _email : String(_email) }),
|
|
107
|
+
} as unknown as UserObject;
|
|
108
|
+
}`,
|
|
109
|
+
)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('handles schema with optional fields', () => {
|
|
113
|
+
const schema: JSONSchema = {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
id: { type: 'number' },
|
|
117
|
+
description: { type: 'string' },
|
|
118
|
+
},
|
|
119
|
+
required: ['id'],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = generateParserFunction(schema, 'ItemObject')
|
|
123
|
+
|
|
124
|
+
expect(result).toBe(
|
|
125
|
+
`export const parseItemObject = (input: unknown): ItemObject => {
|
|
126
|
+
if (!isObject(input)) return {
|
|
127
|
+
id: 0,
|
|
128
|
+
};
|
|
129
|
+
const _id = input.id;
|
|
130
|
+
const _description = input.description;
|
|
131
|
+
if (typeof _id === "number" && (_description === undefined || typeof _description === "string")) return { ...input } as ItemObject;
|
|
132
|
+
return {
|
|
133
|
+
...input,
|
|
134
|
+
id: typeof _id === "number" ? _id : (_id !== undefined ? (Number.isFinite(Number(_id)) ? Number(_id) : 0) : 0),
|
|
135
|
+
...(_description !== undefined && { description: typeof _description === "string" ? _description : String(_description) }),
|
|
136
|
+
} as unknown as ItemObject;
|
|
137
|
+
}`,
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('handles non-schema object properties', () => {
|
|
142
|
+
const schema: JSONSchema = {
|
|
143
|
+
type: 'object',
|
|
144
|
+
properties: {
|
|
145
|
+
validProp: { type: 'string' },
|
|
146
|
+
invalidProp: true,
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const result = generateParserFunction(schema, 'MixedObject')
|
|
151
|
+
|
|
152
|
+
expect(result).toBe(
|
|
153
|
+
`export const parseMixedObject = (input: unknown): MixedObject => {
|
|
154
|
+
if (!isObject(input)) return {} as MixedObject;
|
|
155
|
+
const _validProp = input.validProp;
|
|
156
|
+
return {
|
|
157
|
+
...input,
|
|
158
|
+
...(_validProp !== undefined && { validProp: typeof _validProp === "string" ? _validProp : String(_validProp) }),
|
|
159
|
+
} as unknown as MixedObject;
|
|
160
|
+
}`,
|
|
161
|
+
)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('generates parser without useRefImports by default', () => {
|
|
165
|
+
const schema: JSONSchema = {
|
|
166
|
+
type: 'object',
|
|
167
|
+
properties: {
|
|
168
|
+
contact: { $ref: '#/$defs/Contact' },
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const result = generateParserFunction(schema, 'UserObject')
|
|
173
|
+
|
|
174
|
+
expect(result).toBe(
|
|
175
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
176
|
+
if (!isObject(input)) return {} as UserObject;
|
|
177
|
+
const _contact = input.contact;
|
|
178
|
+
return {
|
|
179
|
+
...input,
|
|
180
|
+
...(_contact !== undefined && { contact: _contact ?? undefined }),
|
|
181
|
+
} as unknown as UserObject;
|
|
182
|
+
}`,
|
|
183
|
+
)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('generates ref parser call when useRefImports is true for required field', () => {
|
|
187
|
+
const schema: JSONSchema = {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
contact: { $ref: '#/$defs/Contact' },
|
|
191
|
+
},
|
|
192
|
+
required: ['contact'],
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const result = generateParserFunction(schema, 'UserObject', { useRefImports: true })
|
|
196
|
+
|
|
197
|
+
expect(result).toBe(
|
|
198
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
199
|
+
if (!isObject(input)) return {
|
|
200
|
+
contact: parseContactObject(undefined),
|
|
201
|
+
};
|
|
202
|
+
const _contact = input.contact;
|
|
203
|
+
if (validateContactObjectShape(_contact)) return { ...input } as UserObject;
|
|
204
|
+
return {
|
|
205
|
+
...input,
|
|
206
|
+
contact: parseContactObject(_contact),
|
|
207
|
+
} as unknown as UserObject;
|
|
208
|
+
}`,
|
|
209
|
+
)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('generates ref parser call when useRefImports is true for optional field', () => {
|
|
213
|
+
const schema: JSONSchema = {
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: {
|
|
216
|
+
contact: { $ref: '#/$defs/Contact' },
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const result = generateParserFunction(schema, 'UserObject', { useRefImports: true })
|
|
221
|
+
|
|
222
|
+
expect(result).toBe(
|
|
223
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
224
|
+
if (!isObject(input)) return {} as UserObject;
|
|
225
|
+
const _contact = input.contact;
|
|
226
|
+
if ((_contact === undefined || validateContactObjectShape(_contact))) return { ...input } as UserObject;
|
|
227
|
+
return {
|
|
228
|
+
...input,
|
|
229
|
+
...(_contact !== undefined && { contact: parseContactObject(_contact) }),
|
|
230
|
+
} as unknown as UserObject;
|
|
231
|
+
}`,
|
|
232
|
+
)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('generates array ref parser call for required array with ref items', () => {
|
|
236
|
+
const schema: JSONSchema = {
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: {
|
|
239
|
+
contacts: {
|
|
240
|
+
type: 'array',
|
|
241
|
+
items: { $ref: '#/$defs/Contact' },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
required: ['contacts'],
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const result = generateParserFunction(schema, 'UserObject', { useRefImports: true })
|
|
248
|
+
|
|
249
|
+
expect(result).toBe(
|
|
250
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
251
|
+
if (!isObject(input)) return {
|
|
252
|
+
contacts: [],
|
|
253
|
+
};
|
|
254
|
+
const _contacts = input.contacts;
|
|
255
|
+
if (Array.isArray(_contacts) && _contacts.every(validateContactObjectShape)) return { ...input } as UserObject;
|
|
256
|
+
return {
|
|
257
|
+
...input,
|
|
258
|
+
contacts: validateArray(_contacts, parseContactObject),
|
|
259
|
+
} as unknown as UserObject;
|
|
260
|
+
}`,
|
|
261
|
+
)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('generates array ref parser call for optional array with ref items', () => {
|
|
265
|
+
const schema: JSONSchema = {
|
|
266
|
+
type: 'object',
|
|
267
|
+
properties: {
|
|
268
|
+
contacts: {
|
|
269
|
+
type: 'array',
|
|
270
|
+
items: { $ref: '#/$defs/Contact' },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const result = generateParserFunction(schema, 'UserObject', { useRefImports: true })
|
|
276
|
+
|
|
277
|
+
expect(result).toBe(
|
|
278
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
279
|
+
if (!isObject(input)) return {} as UserObject;
|
|
280
|
+
const _contacts = input.contacts;
|
|
281
|
+
if ((_contacts === undefined || Array.isArray(_contacts) && _contacts.every(validateContactObjectShape))) return { ...input } as UserObject;
|
|
282
|
+
return {
|
|
283
|
+
...input,
|
|
284
|
+
...(_contacts !== undefined && { contacts: validateArray(_contacts, parseContactObject) }),
|
|
285
|
+
} as unknown as UserObject;
|
|
286
|
+
}`,
|
|
287
|
+
)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('does not generate array ref parser when useRefImports is false', () => {
|
|
291
|
+
const schema: JSONSchema = {
|
|
292
|
+
type: 'object',
|
|
293
|
+
properties: {
|
|
294
|
+
contacts: {
|
|
295
|
+
type: 'array',
|
|
296
|
+
items: { $ref: '#/$defs/Contact' },
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const result = generateParserFunction(schema, 'UserObject', { useRefImports: false })
|
|
302
|
+
|
|
303
|
+
expect(result).toBe(
|
|
304
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
305
|
+
if (!isObject(input)) return {} as UserObject;
|
|
306
|
+
const _contacts = input.contacts;
|
|
307
|
+
if ((_contacts === undefined || Array.isArray(_contacts))) return { ...input } as UserObject;
|
|
308
|
+
return {
|
|
309
|
+
...input,
|
|
310
|
+
...(_contacts !== undefined && { contacts: Array.isArray(_contacts) ? _contacts : [] }),
|
|
311
|
+
} as unknown as UserObject;
|
|
312
|
+
}`,
|
|
313
|
+
)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('handles object with additionalProperties as object', () => {
|
|
317
|
+
const schema: JSONSchema = {
|
|
318
|
+
type: 'object',
|
|
319
|
+
additionalProperties: {
|
|
320
|
+
type: 'string',
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const result = generateParserFunction(schema, 'StringMapObject')
|
|
325
|
+
|
|
326
|
+
expect(result).toBe(
|
|
327
|
+
'export const parseStringMapObject = (input: unknown): StringMapObject => validateRecord(input, (value: unknown) => typeof value === "string" ? value : "") as StringMapObject;',
|
|
328
|
+
)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('handles object with additionalProperties as ref without useRefImports', () => {
|
|
332
|
+
const schema: JSONSchema = {
|
|
333
|
+
type: 'object',
|
|
334
|
+
additionalProperties: {
|
|
335
|
+
$ref: '#/$defs/Contact',
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const result = generateParserFunction(schema, 'ContactMapObject', { useRefImports: false })
|
|
340
|
+
|
|
341
|
+
expect(result).toBe(
|
|
342
|
+
`export const parseContactMapObject = (input: unknown): ContactMapObject => isObject(input) ? { ...input } as ContactMapObject : {};`,
|
|
343
|
+
)
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it('handles object with additionalProperties as ref with useRefImports', () => {
|
|
347
|
+
const schema: JSONSchema = {
|
|
348
|
+
type: 'object',
|
|
349
|
+
additionalProperties: {
|
|
350
|
+
$ref: '#/$defs/Contact',
|
|
351
|
+
},
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const result = generateParserFunction(schema, 'ContactMapObject', { useRefImports: true })
|
|
355
|
+
|
|
356
|
+
expect(result).toBe(
|
|
357
|
+
`export const parseContactMapObject = (input: unknown): ContactMapObject => validateRecord(input, parseContactObject) as ContactMapObject;`,
|
|
358
|
+
)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('handles object with additionalProperties false', () => {
|
|
362
|
+
const schema: JSONSchema = {
|
|
363
|
+
type: 'object',
|
|
364
|
+
additionalProperties: false,
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const result = generateParserFunction(schema, 'StrictObject')
|
|
368
|
+
|
|
369
|
+
expect(result).toBe(
|
|
370
|
+
'export const parseStrictObject = (input: unknown): StrictObject => isObject(input) ? { ...input } as StrictObject : {} as StrictObject;',
|
|
371
|
+
)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('handles complex object with multiple property types', () => {
|
|
375
|
+
const schema: JSONSchema = {
|
|
376
|
+
type: 'object',
|
|
377
|
+
properties: {
|
|
378
|
+
id: { type: 'number' },
|
|
379
|
+
name: { type: 'string' },
|
|
380
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
381
|
+
metadata: { type: 'object' },
|
|
382
|
+
isActive: { type: 'boolean' },
|
|
383
|
+
},
|
|
384
|
+
required: ['id', 'name'],
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const result = generateParserFunction(schema, 'ComplexObject')
|
|
388
|
+
|
|
389
|
+
expect(result).toBe(
|
|
390
|
+
`export const parseComplexObject = (input: unknown): ComplexObject => {
|
|
391
|
+
if (!isObject(input)) return {
|
|
392
|
+
id: 0,
|
|
393
|
+
name: "",
|
|
394
|
+
};
|
|
395
|
+
const _id = input.id;
|
|
396
|
+
const _name = input.name;
|
|
397
|
+
const _tags = input.tags;
|
|
398
|
+
const _metadata = input.metadata;
|
|
399
|
+
const _isActive = input.isActive;
|
|
400
|
+
if (typeof _id === "number" && typeof _name === "string" && (_tags === undefined || Array.isArray(_tags)) && (_metadata === undefined || isObject(_metadata)) && (_isActive === undefined || typeof _isActive === "boolean")) return { ...input } as ComplexObject;
|
|
401
|
+
return {
|
|
402
|
+
...input,
|
|
403
|
+
id: typeof _id === "number" ? _id : (_id !== undefined ? (Number.isFinite(Number(_id)) ? Number(_id) : 0) : 0),
|
|
404
|
+
name: typeof _name === "string" ? _name : (_name !== undefined ? String(_name) : ""),
|
|
405
|
+
...(_tags !== undefined && { tags: Array.isArray(_tags) ? _tags : [] }),
|
|
406
|
+
...(_metadata !== undefined && { metadata: isObject(_metadata) ? _metadata : typeof _metadata === "object" && _metadata !== null ? _metadata : {} }),
|
|
407
|
+
...(_isActive !== undefined && { isActive: typeof _isActive === "boolean" ? _isActive : Boolean(_isActive) }),
|
|
408
|
+
} as unknown as ComplexObject;
|
|
409
|
+
}`,
|
|
410
|
+
)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it('handles schema with kebab-case ref names', () => {
|
|
414
|
+
const schema: JSONSchema = {
|
|
415
|
+
type: 'object',
|
|
416
|
+
properties: {
|
|
417
|
+
externalDoc: { $ref: '#/$defs/external-documentation' },
|
|
418
|
+
},
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const result = generateParserFunction(schema, 'ApiSpecObject', { useRefImports: true })
|
|
422
|
+
|
|
423
|
+
expect(result).toBe(
|
|
424
|
+
`export const parseApiSpecObject = (input: unknown): ApiSpecObject => {
|
|
425
|
+
if (!isObject(input)) return {} as ApiSpecObject;
|
|
426
|
+
const _externalDoc = input.externalDoc;
|
|
427
|
+
if ((_externalDoc === undefined || validateExternalDocumentationObjectShape(_externalDoc))) return { ...input } as ApiSpecObject;
|
|
428
|
+
return {
|
|
429
|
+
...input,
|
|
430
|
+
...(_externalDoc !== undefined && { externalDoc: parseExternalDocumentationObject(_externalDoc) }),
|
|
431
|
+
} as unknown as ApiSpecObject;
|
|
432
|
+
}`,
|
|
433
|
+
)
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it('handles schema with multiple refs', () => {
|
|
437
|
+
const schema: JSONSchema = {
|
|
438
|
+
type: 'object',
|
|
439
|
+
properties: {
|
|
440
|
+
contact: { $ref: '#/$defs/Contact' },
|
|
441
|
+
address: { $ref: '#/$defs/Address' },
|
|
442
|
+
company: { $ref: '#/$defs/Company' },
|
|
443
|
+
},
|
|
444
|
+
required: ['contact'],
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const result = generateParserFunction(schema, 'UserObject', { useRefImports: true })
|
|
448
|
+
|
|
449
|
+
expect(result).toBe(
|
|
450
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
451
|
+
if (!isObject(input)) return {
|
|
452
|
+
contact: parseContactObject(undefined),
|
|
453
|
+
};
|
|
454
|
+
const _contact = input.contact;
|
|
455
|
+
const _address = input.address;
|
|
456
|
+
const _company = input.company;
|
|
457
|
+
if (validateContactObjectShape(_contact) && (_address === undefined || validateAddressObjectShape(_address)) && (_company === undefined || validateCompanyObjectShape(_company))) return { ...input } as UserObject;
|
|
458
|
+
return {
|
|
459
|
+
...input,
|
|
460
|
+
contact: parseContactObject(_contact),
|
|
461
|
+
...(_address !== undefined && { address: parseAddressObject(_address) }),
|
|
462
|
+
...(_company !== undefined && { company: parseCompanyObject(_company) }),
|
|
463
|
+
} as unknown as UserObject;
|
|
464
|
+
}`,
|
|
465
|
+
)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('handles mixed ref and non-ref properties', () => {
|
|
469
|
+
const schema: JSONSchema = {
|
|
470
|
+
type: 'object',
|
|
471
|
+
properties: {
|
|
472
|
+
id: { type: 'number' },
|
|
473
|
+
contact: { $ref: '#/$defs/Contact' },
|
|
474
|
+
name: { type: 'string' },
|
|
475
|
+
},
|
|
476
|
+
required: ['id', 'name'],
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const result = generateParserFunction(schema, 'UserObject', { useRefImports: true })
|
|
480
|
+
|
|
481
|
+
expect(result).toBe(
|
|
482
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
483
|
+
if (!isObject(input)) return {
|
|
484
|
+
id: 0,
|
|
485
|
+
name: "",
|
|
486
|
+
};
|
|
487
|
+
const _id = input.id;
|
|
488
|
+
const _contact = input.contact;
|
|
489
|
+
const _name = input.name;
|
|
490
|
+
if (typeof _id === "number" && (_contact === undefined || validateContactObjectShape(_contact)) && typeof _name === "string") return { ...input } as UserObject;
|
|
491
|
+
return {
|
|
492
|
+
...input,
|
|
493
|
+
id: typeof _id === "number" ? _id : (_id !== undefined ? (Number.isFinite(Number(_id)) ? Number(_id) : 0) : 0),
|
|
494
|
+
...(_contact !== undefined && { contact: parseContactObject(_contact) }),
|
|
495
|
+
name: typeof _name === "string" ? _name : (_name !== undefined ? String(_name) : ""),
|
|
496
|
+
} as unknown as UserObject;
|
|
497
|
+
}`,
|
|
498
|
+
)
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
it('handles array without ref items when useRefImports is true', () => {
|
|
502
|
+
const schema: JSONSchema = {
|
|
503
|
+
type: 'object',
|
|
504
|
+
properties: {
|
|
505
|
+
tags: {
|
|
506
|
+
type: 'array',
|
|
507
|
+
items: { type: 'string' },
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const result = generateParserFunction(schema, 'TaggedObject', { useRefImports: true })
|
|
513
|
+
|
|
514
|
+
expect(result).toBe(
|
|
515
|
+
`export const parseTaggedObject = (input: unknown): TaggedObject => {
|
|
516
|
+
if (!isObject(input)) return {} as TaggedObject;
|
|
517
|
+
const _tags = input.tags;
|
|
518
|
+
if ((_tags === undefined || Array.isArray(_tags))) return { ...input } as TaggedObject;
|
|
519
|
+
return {
|
|
520
|
+
...input,
|
|
521
|
+
...(_tags !== undefined && { tags: Array.isArray(_tags) ? _tags : [] }),
|
|
522
|
+
} as unknown as TaggedObject;
|
|
523
|
+
}`,
|
|
524
|
+
)
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
it('handles array without items property', () => {
|
|
528
|
+
const schema: JSONSchema = {
|
|
529
|
+
type: 'object',
|
|
530
|
+
properties: {
|
|
531
|
+
items: {
|
|
532
|
+
type: 'array',
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const result = generateParserFunction(schema, 'ItemsContainerObject', { useRefImports: true })
|
|
538
|
+
|
|
539
|
+
expect(result).toBe(
|
|
540
|
+
`export const parseItemsContainerObject = (input: unknown): ItemsContainerObject => {
|
|
541
|
+
if (!isObject(input)) return {} as ItemsContainerObject;
|
|
542
|
+
const _items = input.items;
|
|
543
|
+
if ((_items === undefined || Array.isArray(_items))) return { ...input } as ItemsContainerObject;
|
|
544
|
+
return {
|
|
545
|
+
...input,
|
|
546
|
+
...(_items !== undefined && { items: Array.isArray(_items) ? _items : [] }),
|
|
547
|
+
} as unknown as ItemsContainerObject;
|
|
548
|
+
}`,
|
|
549
|
+
)
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
it('handles non-array type with ref when useRefImports is true', () => {
|
|
553
|
+
const schema: JSONSchema = {
|
|
554
|
+
type: 'object',
|
|
555
|
+
properties: {
|
|
556
|
+
contact: { $ref: '#/$defs/Contact' },
|
|
557
|
+
age: { type: 'number' },
|
|
558
|
+
},
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const result = generateParserFunction(schema, 'UserObject', { useRefImports: true })
|
|
562
|
+
|
|
563
|
+
expect(result).toBe(
|
|
564
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
565
|
+
if (!isObject(input)) return {} as UserObject;
|
|
566
|
+
const _contact = input.contact;
|
|
567
|
+
const _age = input.age;
|
|
568
|
+
if ((_contact === undefined || validateContactObjectShape(_contact)) && (_age === undefined || typeof _age === "number")) return { ...input } as UserObject;
|
|
569
|
+
return {
|
|
570
|
+
...input,
|
|
571
|
+
...(_contact !== undefined && { contact: parseContactObject(_contact) }),
|
|
572
|
+
...(_age !== undefined && { age: typeof _age === "number" ? _age : (Number.isFinite(Number(_age)) ? Number(_age) : 0) }),
|
|
573
|
+
} as unknown as UserObject;
|
|
574
|
+
}`,
|
|
575
|
+
)
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it('handles empty properties object', () => {
|
|
579
|
+
const schema: JSONSchema = {
|
|
580
|
+
type: 'object',
|
|
581
|
+
properties: {},
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const result = generateParserFunction(schema, 'EmptyObject')
|
|
585
|
+
|
|
586
|
+
expect(result).toBe(
|
|
587
|
+
`export const parseEmptyObject = (input: unknown): EmptyObject => {
|
|
588
|
+
if (!isObject(input)) return {} as EmptyObject;
|
|
589
|
+
return {
|
|
590
|
+
...input,
|
|
591
|
+
} as unknown as EmptyObject;
|
|
592
|
+
}`,
|
|
593
|
+
)
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
it('handles schema with only non-schema object properties', () => {
|
|
597
|
+
const schema: JSONSchema = {
|
|
598
|
+
type: 'object',
|
|
599
|
+
properties: {
|
|
600
|
+
prop1: true,
|
|
601
|
+
prop2: false,
|
|
602
|
+
prop3: true,
|
|
603
|
+
},
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const result = generateParserFunction(schema, 'AllInvalidObject')
|
|
607
|
+
|
|
608
|
+
expect(result).toBe(
|
|
609
|
+
`export const parseAllInvalidObject = (input: unknown): AllInvalidObject => {
|
|
610
|
+
if (!isObject(input)) return {} as AllInvalidObject;
|
|
611
|
+
return {
|
|
612
|
+
...input,
|
|
613
|
+
} as unknown as AllInvalidObject;
|
|
614
|
+
}`,
|
|
615
|
+
)
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
it('generates correct parser name for PascalCase type names', () => {
|
|
619
|
+
const schema: JSONSchema = {
|
|
620
|
+
type: 'object',
|
|
621
|
+
properties: {
|
|
622
|
+
id: { type: 'number' },
|
|
623
|
+
},
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const result = generateParserFunction(schema, 'MyCustomTypeObject')
|
|
627
|
+
|
|
628
|
+
expect(result).toBe(
|
|
629
|
+
`export const parseMyCustomTypeObject = (input: unknown): MyCustomTypeObject => {
|
|
630
|
+
if (!isObject(input)) return {} as MyCustomTypeObject;
|
|
631
|
+
const _id = input.id;
|
|
632
|
+
if ((_id === undefined || typeof _id === "number")) return { ...input } as MyCustomTypeObject;
|
|
633
|
+
return {
|
|
634
|
+
...input,
|
|
635
|
+
...(_id !== undefined && { id: typeof _id === "number" ? _id : (Number.isFinite(Number(_id)) ? Number(_id) : 0) }),
|
|
636
|
+
} as unknown as MyCustomTypeObject;
|
|
637
|
+
}`,
|
|
638
|
+
)
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
it('generates correct parser name for camelCase type names', () => {
|
|
642
|
+
const schema: JSONSchema = {
|
|
643
|
+
type: 'object',
|
|
644
|
+
properties: {
|
|
645
|
+
id: { type: 'number' },
|
|
646
|
+
},
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const result = generateParserFunction(schema, 'myCustomTypeObject')
|
|
650
|
+
|
|
651
|
+
expect(result).toBe(
|
|
652
|
+
`export const parsemyCustomTypeObject = (input: unknown): myCustomTypeObject => {
|
|
653
|
+
if (!isObject(input)) return {} as myCustomTypeObject;
|
|
654
|
+
const _id = input.id;
|
|
655
|
+
if ((_id === undefined || typeof _id === "number")) return { ...input } as myCustomTypeObject;
|
|
656
|
+
return {
|
|
657
|
+
...input,
|
|
658
|
+
...(_id !== undefined && { id: typeof _id === "number" ? _id : (Number.isFinite(Number(_id)) ? Number(_id) : 0) }),
|
|
659
|
+
} as unknown as myCustomTypeObject;
|
|
660
|
+
}`,
|
|
661
|
+
)
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
it('handles object schema with properties property but no type', () => {
|
|
665
|
+
const schema: JSONSchema = {
|
|
666
|
+
properties: {
|
|
667
|
+
name: { type: 'string' },
|
|
668
|
+
},
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const result = generateParserFunction(schema, 'ImplicitObject')
|
|
672
|
+
|
|
673
|
+
expect(result).toBe(
|
|
674
|
+
`export const parseImplicitObject = (input: unknown): ImplicitObject => {
|
|
675
|
+
if (!isObject(input)) return {} as ImplicitObject;
|
|
676
|
+
const _name = input.name;
|
|
677
|
+
if ((_name === undefined || typeof _name === "string")) return { ...input } as ImplicitObject;
|
|
678
|
+
return {
|
|
679
|
+
...input,
|
|
680
|
+
...(_name !== undefined && { name: typeof _name === "string" ? _name : String(_name) }),
|
|
681
|
+
} as unknown as ImplicitObject;
|
|
682
|
+
}`,
|
|
683
|
+
)
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
it('handles additionalProperties as true', () => {
|
|
687
|
+
const schema: JSONSchema = {
|
|
688
|
+
type: 'object',
|
|
689
|
+
additionalProperties: true,
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const result = generateParserFunction(schema, 'AnyAdditionalObject')
|
|
693
|
+
|
|
694
|
+
expect(result).toBe(
|
|
695
|
+
'export const parseAnyAdditionalObject = (input: unknown): AnyAdditionalObject => isObject(input) ? { ...input } as AnyAdditionalObject : {} as AnyAdditionalObject;',
|
|
696
|
+
)
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
it('handles additionalProperties with string type by validating values', () => {
|
|
700
|
+
const schema: JSONSchema = {
|
|
701
|
+
type: 'object',
|
|
702
|
+
additionalProperties: {
|
|
703
|
+
type: 'string',
|
|
704
|
+
},
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const result = generateParserFunction(schema, 'MapOfStringsObject', { useRefImports: true })
|
|
708
|
+
|
|
709
|
+
expect(result).toBe(
|
|
710
|
+
'export const parseMapOfStringsObject = (input: unknown): MapOfStringsObject => validateRecord(input, (value: unknown) => typeof value === "string" ? value : "") as MapOfStringsObject;',
|
|
711
|
+
)
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
it('handles additionalProperties with array type by validating values', () => {
|
|
715
|
+
const schema: JSONSchema = {
|
|
716
|
+
type: 'object',
|
|
717
|
+
additionalProperties: {
|
|
718
|
+
type: 'array',
|
|
719
|
+
items: {
|
|
720
|
+
type: 'string',
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const result = generateParserFunction(schema, 'SecurityRequirementObject', { useRefImports: true })
|
|
726
|
+
|
|
727
|
+
expect(result).toBe(
|
|
728
|
+
'export const parseSecurityRequirementObject = (input: unknown): SecurityRequirementObject => validateRecord(input, (value: unknown) => Array.isArray(value) ? value : []) as SecurityRequirementObject;',
|
|
729
|
+
)
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
it('handles array with items as boolean schema', () => {
|
|
733
|
+
const schema: JSONSchema = {
|
|
734
|
+
type: 'object',
|
|
735
|
+
properties: {
|
|
736
|
+
data: {
|
|
737
|
+
type: 'array',
|
|
738
|
+
items: true,
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const result = generateParserFunction(schema, 'ContainerObject', { useRefImports: true })
|
|
744
|
+
|
|
745
|
+
expect(result).toBe(
|
|
746
|
+
`export const parseContainerObject = (input: unknown): ContainerObject => {
|
|
747
|
+
if (!isObject(input)) return {} as ContainerObject;
|
|
748
|
+
const _data = input.data;
|
|
749
|
+
if ((_data === undefined || Array.isArray(_data))) return { ...input } as ContainerObject;
|
|
750
|
+
return {
|
|
751
|
+
...input,
|
|
752
|
+
...(_data !== undefined && { data: Array.isArray(_data) ? _data : [] }),
|
|
753
|
+
} as unknown as ContainerObject;
|
|
754
|
+
}`,
|
|
755
|
+
)
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
it('handles ref with complex path', () => {
|
|
759
|
+
const schema: JSONSchema = {
|
|
760
|
+
type: 'object',
|
|
761
|
+
properties: {
|
|
762
|
+
nested: { $ref: '#/$defs/deeply/nested/Type' },
|
|
763
|
+
},
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const result = generateParserFunction(schema, 'UserObject', { useRefImports: true })
|
|
767
|
+
|
|
768
|
+
expect(result).toBe(
|
|
769
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
770
|
+
if (!isObject(input)) return {} as UserObject;
|
|
771
|
+
const _nested = input.nested;
|
|
772
|
+
if ((_nested === undefined || validateTypeObjectShape(_nested))) return { ...input } as UserObject;
|
|
773
|
+
return {
|
|
774
|
+
...input,
|
|
775
|
+
...(_nested !== undefined && { nested: parseTypeObject(_nested) }),
|
|
776
|
+
} as unknown as UserObject;
|
|
777
|
+
}`,
|
|
778
|
+
)
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
it('handles multiple array properties with refs', () => {
|
|
782
|
+
const schema: JSONSchema = {
|
|
783
|
+
type: 'object',
|
|
784
|
+
properties: {
|
|
785
|
+
contacts: {
|
|
786
|
+
type: 'array',
|
|
787
|
+
items: { $ref: '#/$defs/Contact' },
|
|
788
|
+
},
|
|
789
|
+
addresses: {
|
|
790
|
+
type: 'array',
|
|
791
|
+
items: { $ref: '#/$defs/Address' },
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const result = generateParserFunction(schema, 'UserObject', { useRefImports: true })
|
|
797
|
+
|
|
798
|
+
expect(result).toBe(
|
|
799
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
800
|
+
if (!isObject(input)) return {} as UserObject;
|
|
801
|
+
const _contacts = input.contacts;
|
|
802
|
+
const _addresses = input.addresses;
|
|
803
|
+
if ((_contacts === undefined || Array.isArray(_contacts) && _contacts.every(validateContactObjectShape)) && (_addresses === undefined || Array.isArray(_addresses) && _addresses.every(validateAddressObjectShape))) return { ...input } as UserObject;
|
|
804
|
+
return {
|
|
805
|
+
...input,
|
|
806
|
+
...(_contacts !== undefined && { contacts: validateArray(_contacts, parseContactObject) }),
|
|
807
|
+
...(_addresses !== undefined && { addresses: validateArray(_addresses, parseAddressObject) }),
|
|
808
|
+
} as unknown as UserObject;
|
|
809
|
+
}`,
|
|
810
|
+
)
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
it('handles empty required array', () => {
|
|
814
|
+
const schema: JSONSchema = {
|
|
815
|
+
type: 'object',
|
|
816
|
+
properties: {
|
|
817
|
+
id: { type: 'number' },
|
|
818
|
+
name: { type: 'string' },
|
|
819
|
+
},
|
|
820
|
+
required: [],
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const result = generateParserFunction(schema, 'UserObject')
|
|
824
|
+
|
|
825
|
+
expect(result).toBe(
|
|
826
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
827
|
+
if (!isObject(input)) return {} as UserObject;
|
|
828
|
+
const _id = input.id;
|
|
829
|
+
const _name = input.name;
|
|
830
|
+
if ((_id === undefined || typeof _id === "number") && (_name === undefined || typeof _name === "string")) return { ...input } as UserObject;
|
|
831
|
+
return {
|
|
832
|
+
...input,
|
|
833
|
+
...(_id !== undefined && { id: typeof _id === "number" ? _id : (Number.isFinite(Number(_id)) ? Number(_id) : 0) }),
|
|
834
|
+
...(_name !== undefined && { name: typeof _name === "string" ? _name : String(_name) }),
|
|
835
|
+
} as unknown as UserObject;
|
|
836
|
+
}`,
|
|
837
|
+
)
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
it('handles schema with undefined required property', () => {
|
|
841
|
+
const schema: JSONSchema = {
|
|
842
|
+
type: 'object',
|
|
843
|
+
properties: {
|
|
844
|
+
id: { type: 'number' },
|
|
845
|
+
},
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const result = generateParserFunction(schema, 'UserObject')
|
|
849
|
+
|
|
850
|
+
expect(result).toBe(
|
|
851
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
852
|
+
if (!isObject(input)) return {} as UserObject;
|
|
853
|
+
const _id = input.id;
|
|
854
|
+
if ((_id === undefined || typeof _id === "number")) return { ...input } as UserObject;
|
|
855
|
+
return {
|
|
856
|
+
...input,
|
|
857
|
+
...(_id !== undefined && { id: typeof _id === "number" ? _id : (Number.isFinite(Number(_id)) ? Number(_id) : 0) }),
|
|
858
|
+
} as unknown as UserObject;
|
|
859
|
+
}`,
|
|
860
|
+
)
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
it('generates parser for additionalProperties with ref and validates input type', () => {
|
|
864
|
+
const schema: JSONSchema = {
|
|
865
|
+
type: 'object',
|
|
866
|
+
additionalProperties: {
|
|
867
|
+
$ref: '#/$defs/Value',
|
|
868
|
+
},
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const result = generateParserFunction(schema, 'MapObject', { useRefImports: true })
|
|
872
|
+
|
|
873
|
+
expect(result).toBe(
|
|
874
|
+
`export const parseMapObject = (input: unknown): MapObject => validateRecord(input, parseValueObject) as MapObject;`,
|
|
875
|
+
)
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
it('generates a simple object pass-through parser for allOf-only schema', () => {
|
|
879
|
+
// A schema with only allOf refs and type: object but no properties generates
|
|
880
|
+
// a simple spread parser since there is nothing to specifically parse.
|
|
881
|
+
const schema: JSONSchema = {
|
|
882
|
+
type: 'object',
|
|
883
|
+
allOf: [{ $ref: '#/$defs/type-apikey' }, { $ref: '#/$defs/type-http-bearer' }, { $ref: '#/$defs/type-oauth2' }],
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const result = generateParserFunction(schema, 'SecuritySchemeObject', { useRefImports: true })
|
|
887
|
+
|
|
888
|
+
expect(result).toBe(
|
|
889
|
+
`export const parseSecuritySchemeObject = (input: unknown): SecuritySchemeObject => isObject(input) ? { ...input } as SecuritySchemeObject : {} as SecuritySchemeObject;`,
|
|
890
|
+
)
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
it('generates a parser for the components object', () => {
|
|
894
|
+
const components: JSONSchema.Object = {
|
|
895
|
+
type: 'object',
|
|
896
|
+
properties: {
|
|
897
|
+
responses: {
|
|
898
|
+
type: 'object',
|
|
899
|
+
additionalProperties: { $ref: '#/$defs/response' },
|
|
900
|
+
},
|
|
901
|
+
parameters: {
|
|
902
|
+
type: 'object',
|
|
903
|
+
additionalProperties: { $ref: '#/$defs/parameter' },
|
|
904
|
+
},
|
|
905
|
+
headers: {
|
|
906
|
+
type: 'object',
|
|
907
|
+
additionalProperties: { $ref: '#/$defs/header' },
|
|
908
|
+
},
|
|
909
|
+
pathItems: {
|
|
910
|
+
type: 'object',
|
|
911
|
+
additionalProperties: { $ref: '#/$defs/path-item' },
|
|
912
|
+
},
|
|
913
|
+
},
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const result = generateParserFunction(components, 'ComponentsObject', { useRefImports: true })
|
|
917
|
+
expect(result).toBe(
|
|
918
|
+
`export const parseComponentsObject = (input: unknown): ComponentsObject => {
|
|
919
|
+
if (!isObject(input)) return {} as ComponentsObject;
|
|
920
|
+
const _responses = input.responses;
|
|
921
|
+
const _parameters = input.parameters;
|
|
922
|
+
const _headers = input.headers;
|
|
923
|
+
const _pathItems = input.pathItems;
|
|
924
|
+
return {
|
|
925
|
+
...input,
|
|
926
|
+
...(_responses !== undefined && { responses: validateRecord(_responses, parseResponseObject) }),
|
|
927
|
+
...(_parameters !== undefined && { parameters: validateRecord(_parameters, parseParameterObject) }),
|
|
928
|
+
...(_headers !== undefined && { headers: validateRecord(_headers, parseHeaderObject) }),
|
|
929
|
+
...(_pathItems !== undefined && { pathItems: validateRecord(_pathItems, parsePathItemObject) }),
|
|
930
|
+
} as unknown as ComponentsObject;
|
|
931
|
+
}`,
|
|
932
|
+
)
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
it('handles ref schema with conditional if/then/else logic', () => {
|
|
936
|
+
const schema: JSONSchema = {
|
|
937
|
+
if: { type: 'object', required: ['$ref'] },
|
|
938
|
+
then: { $ref: '#/$defs/reference' },
|
|
939
|
+
else: { $ref: '#/$defs/callbacks' },
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const result = generateParserFunction(schema, 'ConditionalObject')
|
|
943
|
+
expect(result).toBe(
|
|
944
|
+
`export const parseConditionalObject = (input: unknown): ConditionalObject | ReferenceObject =>
|
|
945
|
+
hasRef(input) ? parseReferenceObject(input) : parseCallbacksObject(input)
|
|
946
|
+
`,
|
|
947
|
+
)
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
it('handles OpenAPI paths schema with patternProperties', () => {
|
|
951
|
+
// This is the actual schema from OpenAPI 3.1.2 for the paths object
|
|
952
|
+
const schema: JSONSchema = {
|
|
953
|
+
type: 'object',
|
|
954
|
+
patternProperties: {
|
|
955
|
+
'^/': { $ref: '#/$defs/path-item' },
|
|
956
|
+
},
|
|
957
|
+
$ref: '#/$defs/specification-extensions',
|
|
958
|
+
unevaluatedProperties: false,
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const result = generateParserFunction(schema, 'PathsObject', { useRefImports: true })
|
|
962
|
+
|
|
963
|
+
expect(result).toBe(
|
|
964
|
+
`export const parsePathsObject = (input: unknown): PathsObject => {
|
|
965
|
+
if (!isObject(input)) {
|
|
966
|
+
return {} as unknown as PathsObject;
|
|
967
|
+
}
|
|
968
|
+
const result = {
|
|
969
|
+
...input,
|
|
970
|
+
} as unknown as PathsObject;
|
|
971
|
+
for (const key in input) {
|
|
972
|
+
if (/^\\//.test(key)) {
|
|
973
|
+
const value = input[key];
|
|
974
|
+
(result as Record<string, unknown>)[key] = parsePathItemObject(value);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return result;
|
|
978
|
+
};`,
|
|
979
|
+
)
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
it('generates a simple object pass-through parser for allOf-only schema with multiple subtypes', () => {
|
|
983
|
+
// With the OpenAPI SecuritySchemeObject dispatcher logic removed, a schema that only has
|
|
984
|
+
// allOf refs and no fixed properties generates a simple spread parser.
|
|
985
|
+
const schema: JSONSchema = {
|
|
986
|
+
type: 'object',
|
|
987
|
+
allOf: [
|
|
988
|
+
{ $ref: '#/$defs/type-apikey' },
|
|
989
|
+
{ $ref: '#/$defs/type-http' },
|
|
990
|
+
{ $ref: '#/$defs/type-http-bearer' },
|
|
991
|
+
{ $ref: '#/$defs/type-oauth2' },
|
|
992
|
+
{ $ref: '#/$defs/type-oidc' },
|
|
993
|
+
],
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const result = generateParserFunction(schema, 'SecuritySchemeObject', { useRefImports: true })
|
|
997
|
+
|
|
998
|
+
expect(result).toBe(
|
|
999
|
+
`export const parseSecuritySchemeObject = (input: unknown): SecuritySchemeObject => isObject(input) ? { ...input } as SecuritySchemeObject : {} as SecuritySchemeObject;`,
|
|
1000
|
+
)
|
|
1001
|
+
})
|
|
1002
|
+
|
|
1003
|
+
it('documents the difference: webhooks uses additionalProperties and works correctly', () => {
|
|
1004
|
+
// This is how webhooks is defined in OpenAPI - uses additionalProperties instead of patternProperties
|
|
1005
|
+
const schema: JSONSchema = {
|
|
1006
|
+
type: 'object',
|
|
1007
|
+
additionalProperties: { $ref: '#/$defs/path-item' },
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const result = generateParserFunction(schema, 'Webhooks', { useRefImports: true })
|
|
1011
|
+
|
|
1012
|
+
// This works correctly because additionalProperties is handled
|
|
1013
|
+
expect(result).toBe(
|
|
1014
|
+
`export const parseWebhooks = (input: unknown): Webhooks => validateRecord(input, parsePathItemObject) as Webhooks;`,
|
|
1015
|
+
)
|
|
1016
|
+
expect(result).not.toContain('input as Webhooks')
|
|
1017
|
+
})
|
|
1018
|
+
|
|
1019
|
+
it('handles simple patternProperties with $ref', () => {
|
|
1020
|
+
// A simpler case with just patternProperties
|
|
1021
|
+
const schema: JSONSchema = {
|
|
1022
|
+
type: 'object',
|
|
1023
|
+
patternProperties: {
|
|
1024
|
+
'^/': { $ref: '#/$defs/path-item' },
|
|
1025
|
+
},
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const result = generateParserFunction(schema, 'SimplePaths', { useRefImports: true })
|
|
1029
|
+
|
|
1030
|
+
expect(result).toBe(
|
|
1031
|
+
`export const parseSimplePaths = (input: unknown): SimplePaths => {
|
|
1032
|
+
if (!isObject(input)) {
|
|
1033
|
+
return {} as unknown as SimplePaths;
|
|
1034
|
+
}
|
|
1035
|
+
const result = {
|
|
1036
|
+
...input,
|
|
1037
|
+
} as unknown as SimplePaths;
|
|
1038
|
+
for (const key in input) {
|
|
1039
|
+
if (/^\\//.test(key)) {
|
|
1040
|
+
const value = input[key];
|
|
1041
|
+
(result as Record<string, unknown>)[key] = parsePathItemObject(value);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return result;
|
|
1045
|
+
};`,
|
|
1046
|
+
)
|
|
1047
|
+
})
|
|
1048
|
+
|
|
1049
|
+
it('generates consistent code for optional direct $ref properties in Document-like schemas', () => {
|
|
1050
|
+
// This simulates the OpenAPI Document schema structure
|
|
1051
|
+
const schema: JSONSchema = {
|
|
1052
|
+
type: 'object',
|
|
1053
|
+
properties: {
|
|
1054
|
+
openapi: { type: 'string' },
|
|
1055
|
+
info: { $ref: '#/$defs/info' },
|
|
1056
|
+
servers: {
|
|
1057
|
+
type: 'array',
|
|
1058
|
+
items: { $ref: '#/$defs/server' },
|
|
1059
|
+
},
|
|
1060
|
+
paths: { $ref: '#/$defs/paths' },
|
|
1061
|
+
webhooks: {
|
|
1062
|
+
type: 'object',
|
|
1063
|
+
additionalProperties: { $ref: '#/$defs/path-item' },
|
|
1064
|
+
},
|
|
1065
|
+
components: { $ref: '#/$defs/components' },
|
|
1066
|
+
externalDocs: { $ref: '#/$defs/external-documentation' },
|
|
1067
|
+
},
|
|
1068
|
+
required: ['openapi', 'info'],
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const result = generateParserFunction(schema, 'Document', { useRefImports: true })
|
|
1072
|
+
expect(result).toBe(`export const parseDocument = (input: unknown): Document => {
|
|
1073
|
+
if (!isObject(input)) return {
|
|
1074
|
+
openapi: "",
|
|
1075
|
+
info: parseInfoObject(undefined),
|
|
1076
|
+
};
|
|
1077
|
+
const _openapi = input.openapi;
|
|
1078
|
+
const _info = input.info;
|
|
1079
|
+
const _servers = input.servers;
|
|
1080
|
+
const _paths = input.paths;
|
|
1081
|
+
const _webhooks = input.webhooks;
|
|
1082
|
+
const _components = input.components;
|
|
1083
|
+
const _externalDocs = input.externalDocs;
|
|
1084
|
+
return {
|
|
1085
|
+
...input,
|
|
1086
|
+
openapi: typeof _openapi === "string" ? _openapi : (_openapi !== undefined ? String(_openapi) : ""),
|
|
1087
|
+
info: parseInfoObject(_info),
|
|
1088
|
+
...(_servers !== undefined && { servers: validateArray(_servers, parseServerObject) }),
|
|
1089
|
+
...(_paths !== undefined && { paths: parsePathsObject(_paths) }),
|
|
1090
|
+
...(_webhooks !== undefined && { webhooks: validateRecord(_webhooks, parsePathItemObject) }),
|
|
1091
|
+
...(_components !== undefined && { components: parseComponentsObject(_components) }),
|
|
1092
|
+
...(_externalDocs !== undefined && { externalDocs: parseExternalDocumentationObject(_externalDocs) }),
|
|
1093
|
+
} as unknown as Document;
|
|
1094
|
+
}`)
|
|
1095
|
+
})
|
|
1096
|
+
|
|
1097
|
+
it('generates parser for components-like schema with $dynamicRef and record properties', () => {
|
|
1098
|
+
// Tests that $dynamicRef in additionalProperties generates the schema pass-through,
|
|
1099
|
+
// while plain $ref properties generate proper validateRecord calls.
|
|
1100
|
+
const schema: JSONSchema = {
|
|
1101
|
+
type: 'object',
|
|
1102
|
+
properties: {
|
|
1103
|
+
schemas: {
|
|
1104
|
+
type: 'object',
|
|
1105
|
+
additionalProperties: { $dynamicRef: '#meta' },
|
|
1106
|
+
},
|
|
1107
|
+
responses: {
|
|
1108
|
+
type: 'object',
|
|
1109
|
+
additionalProperties: { $ref: '#/$defs/response' },
|
|
1110
|
+
},
|
|
1111
|
+
parameters: {
|
|
1112
|
+
type: 'object',
|
|
1113
|
+
additionalProperties: { $ref: '#/$defs/parameter' },
|
|
1114
|
+
},
|
|
1115
|
+
pathItems: {
|
|
1116
|
+
type: 'object',
|
|
1117
|
+
additionalProperties: { $ref: '#/$defs/path-item' },
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const result = generateParserFunction(schema, 'ComponentsObject', { useRefImports: true })
|
|
1123
|
+
expect(result).toBe(
|
|
1124
|
+
`export const parseComponentsObject = (input: unknown): ComponentsObject => {
|
|
1125
|
+
if (!isObject(input)) return {} as ComponentsObject;
|
|
1126
|
+
const _schemas = input.schemas;
|
|
1127
|
+
const _responses = input.responses;
|
|
1128
|
+
const _parameters = input.parameters;
|
|
1129
|
+
const _pathItems = input.pathItems;
|
|
1130
|
+
return {
|
|
1131
|
+
...input,
|
|
1132
|
+
...(_schemas !== undefined && { schemas: isObject(_schemas) ? _schemas : typeof _schemas === "object" && _schemas !== null ? _schemas : {} }),
|
|
1133
|
+
...(_responses !== undefined && { responses: validateRecord(_responses, parseResponseObject) }),
|
|
1134
|
+
...(_parameters !== undefined && { parameters: validateRecord(_parameters, parseParameterObject) }),
|
|
1135
|
+
...(_pathItems !== undefined && { pathItems: validateRecord(_pathItems, parsePathItemObject) }),
|
|
1136
|
+
} as unknown as ComponentsObject;
|
|
1137
|
+
}`,
|
|
1138
|
+
)
|
|
1139
|
+
})
|
|
1140
|
+
|
|
1141
|
+
it('generates combined parser for schema with both properties and patternProperties', () => {
|
|
1142
|
+
// A schema with a known property and patternProperties for dynamic keys
|
|
1143
|
+
const schema: JSONSchema = {
|
|
1144
|
+
type: 'object',
|
|
1145
|
+
properties: {
|
|
1146
|
+
default: { $ref: '#/$defs/response' },
|
|
1147
|
+
},
|
|
1148
|
+
patternProperties: {
|
|
1149
|
+
'^[1-5](?:[0-9]{2}|XX)$': { $ref: '#/$defs/response' },
|
|
1150
|
+
},
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const result = generateParserFunction(schema, 'ResponsesObject', { useRefImports: true })
|
|
1154
|
+
|
|
1155
|
+
expect(result).toBe(
|
|
1156
|
+
`export const parseResponsesObject = (input: unknown): ResponsesObject => {
|
|
1157
|
+
if (!isObject(input)) {
|
|
1158
|
+
return {} as unknown as ResponsesObject;
|
|
1159
|
+
}
|
|
1160
|
+
const result = {
|
|
1161
|
+
...input,
|
|
1162
|
+
...(input.default && { default: parseResponseObject(input.default) }),
|
|
1163
|
+
} as unknown as ResponsesObject;
|
|
1164
|
+
for (const key in input) {
|
|
1165
|
+
if (/^[1-5](?:[0-9]{2}|XX)$/.test(key)) {
|
|
1166
|
+
const value = input[key];
|
|
1167
|
+
(result as Record<string, unknown>)[key] = parseResponseObject(value);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return result;
|
|
1171
|
+
};`,
|
|
1172
|
+
)
|
|
1173
|
+
})
|
|
1174
|
+
|
|
1175
|
+
it('falls back to object parser when patternProperties has no $ref and useRefImports is false', () => {
|
|
1176
|
+
const schema: JSONSchema = {
|
|
1177
|
+
type: 'object',
|
|
1178
|
+
properties: {
|
|
1179
|
+
default: { $ref: '#/$defs/response' },
|
|
1180
|
+
},
|
|
1181
|
+
patternProperties: {
|
|
1182
|
+
'^[1-5]': { type: 'string' },
|
|
1183
|
+
},
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const result = generateParserFunction(schema, 'ResponsesObject', { useRefImports: false })
|
|
1187
|
+
|
|
1188
|
+
// Without useRefImports, falls back to regular object parser
|
|
1189
|
+
expect(result).toContain('isObject(input)')
|
|
1190
|
+
expect(result).toContain('default:')
|
|
1191
|
+
expect(result).not.toContain('for (const [key, value]')
|
|
1192
|
+
})
|
|
1193
|
+
|
|
1194
|
+
it('generates parser for schema object with $dynamicRef in additionalProperties', () => {
|
|
1195
|
+
// This tests the JSON Schema 2020-12 $dynamicRef feature used in OpenAPI 3.1.2
|
|
1196
|
+
// for the schemas property which can contain any valid JSON Schema
|
|
1197
|
+
const schema: JSONSchema = {
|
|
1198
|
+
type: 'object',
|
|
1199
|
+
additionalProperties: {
|
|
1200
|
+
$dynamicRef: '#meta',
|
|
1201
|
+
},
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const result = generateParserFunction(schema, 'SchemaObject', { useRefImports: true })
|
|
1205
|
+
|
|
1206
|
+
// SchemaObject is a special case that handles both boolean and object types
|
|
1207
|
+
expect(result).toContain("if (typeof input === 'boolean')")
|
|
1208
|
+
expect(result).toContain('return input as SchemaObject')
|
|
1209
|
+
expect(result).toContain('if (!isObject(input))')
|
|
1210
|
+
expect(result).toContain('return {} as SchemaObject')
|
|
1211
|
+
})
|
|
1212
|
+
|
|
1213
|
+
it('uses bracket notation for hyphenated property keys with $ref', () => {
|
|
1214
|
+
const schema: JSONSchema = {
|
|
1215
|
+
type: 'object',
|
|
1216
|
+
properties: {
|
|
1217
|
+
'x-linkedin': { $ref: '#/$defs/extension' },
|
|
1218
|
+
},
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const result = generateParserFunction(schema, 'InfoExtensionsObject', { useRefImports: true })
|
|
1222
|
+
|
|
1223
|
+
expect(result).toContain("input['x-linkedin']")
|
|
1224
|
+
expect(result).toContain("'x-linkedin':")
|
|
1225
|
+
expect(result).not.toContain('input.x-linkedin')
|
|
1226
|
+
})
|
|
1227
|
+
|
|
1228
|
+
it('uses bracket notation for hyphenated property keys with validation', () => {
|
|
1229
|
+
const schema: JSONSchema = {
|
|
1230
|
+
type: 'object',
|
|
1231
|
+
properties: {
|
|
1232
|
+
'x-custom': { type: 'string' },
|
|
1233
|
+
},
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const result = generateParserFunction(schema, 'TestObject')
|
|
1237
|
+
|
|
1238
|
+
expect(result).toContain("input['x-custom']")
|
|
1239
|
+
expect(result).toContain("'x-custom':")
|
|
1240
|
+
expect(result).not.toContain('input.x-custom')
|
|
1241
|
+
})
|
|
1242
|
+
|
|
1243
|
+
it('generates object parser from conditional if/then fragments', () => {
|
|
1244
|
+
const schema: JSONSchema = {
|
|
1245
|
+
if: {
|
|
1246
|
+
properties: {
|
|
1247
|
+
type: {
|
|
1248
|
+
const: 'http',
|
|
1249
|
+
},
|
|
1250
|
+
},
|
|
1251
|
+
},
|
|
1252
|
+
then: {
|
|
1253
|
+
properties: {
|
|
1254
|
+
scheme: {
|
|
1255
|
+
type: 'string',
|
|
1256
|
+
},
|
|
1257
|
+
},
|
|
1258
|
+
required: ['scheme'],
|
|
1259
|
+
},
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const result = generateParserFunction(schema, 'TypeHttpObject', { useRefImports: true })
|
|
1263
|
+
|
|
1264
|
+
expect(result).toContain('isObject(input)')
|
|
1265
|
+
expect(result).toContain('type:')
|
|
1266
|
+
expect(result).toContain('scheme:')
|
|
1267
|
+
expect(result).not.toContain('input as TypeHttpObject')
|
|
1268
|
+
})
|
|
1269
|
+
|
|
1270
|
+
it('generates parser for patternProperties-only schema without explicit type', () => {
|
|
1271
|
+
const schema: JSONSchema = {
|
|
1272
|
+
patternProperties: {
|
|
1273
|
+
'^x-': true,
|
|
1274
|
+
},
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
const result = generateParserFunction(schema, 'SpecificationExtensionsObject', { useRefImports: true })
|
|
1278
|
+
|
|
1279
|
+
expect(result).toContain('for (const key in input)')
|
|
1280
|
+
expect(result).toContain('...input,')
|
|
1281
|
+
expect(result).not.toContain('input as SpecificationExtensionsObject')
|
|
1282
|
+
})
|
|
1283
|
+
|
|
1284
|
+
it('optimization: inlines variables for simple properties without fast-path', () => {
|
|
1285
|
+
// When there's no fast-path, simple properties should be inlined
|
|
1286
|
+
const schema: JSONSchema = {
|
|
1287
|
+
type: 'object',
|
|
1288
|
+
properties: {
|
|
1289
|
+
summary: { type: 'string' },
|
|
1290
|
+
description: { type: 'string' },
|
|
1291
|
+
},
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const result = generateParserFunction(schema, 'ExampleObject')
|
|
1295
|
+
|
|
1296
|
+
// Should NOT cache variables when no fast-path is possible
|
|
1297
|
+
// (in this case, fast-path exists, so variables ARE cached)
|
|
1298
|
+
expect(result).toContain('const _summary')
|
|
1299
|
+
expect(result).toContain('const _description')
|
|
1300
|
+
})
|
|
1301
|
+
|
|
1302
|
+
it('optimization: caches variables when fast-path exists', () => {
|
|
1303
|
+
const schema: JSONSchema = {
|
|
1304
|
+
type: 'object',
|
|
1305
|
+
properties: {
|
|
1306
|
+
name: { type: 'string' },
|
|
1307
|
+
age: { type: 'number' },
|
|
1308
|
+
},
|
|
1309
|
+
required: ['name'],
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const result = generateParserFunction(schema, 'UserObject')
|
|
1313
|
+
|
|
1314
|
+
// Should cache variables because fast-path exists
|
|
1315
|
+
expect(result).toContain('const _name = input.name')
|
|
1316
|
+
expect(result).toContain('const _age = input.age')
|
|
1317
|
+
// Should have fast-path check
|
|
1318
|
+
expect(result).toContain('if (typeof _name === "string"')
|
|
1319
|
+
expect(result).toContain('return { ...input } as UserObject')
|
|
1320
|
+
})
|
|
1321
|
+
|
|
1322
|
+
it('optimization: removes redundant undefined checks in optional properties', () => {
|
|
1323
|
+
const schema: JSONSchema = {
|
|
1324
|
+
type: 'object',
|
|
1325
|
+
properties: {
|
|
1326
|
+
summary: { type: 'string' },
|
|
1327
|
+
description: { type: 'string' },
|
|
1328
|
+
},
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const result = generateParserFunction(schema, 'ExampleObject')
|
|
1332
|
+
|
|
1333
|
+
// The generated code should have the outer undefined check
|
|
1334
|
+
expect(result).toContain('!== undefined &&')
|
|
1335
|
+
// But should NOT have redundant nested undefined checks in the coercion
|
|
1336
|
+
// Count occurrences - should only appear in the outer check, not in String() coercion
|
|
1337
|
+
const matches = result.match(/!== undefined/g) || []
|
|
1338
|
+
// Should have exactly 2 occurrences (one for each optional property's outer check)
|
|
1339
|
+
expect(matches.length).toBe(2)
|
|
1340
|
+
})
|
|
1341
|
+
|
|
1342
|
+
it('optimization: inlines properties with $ref when they cannot use fast-path', () => {
|
|
1343
|
+
const schema: JSONSchema = {
|
|
1344
|
+
type: 'object',
|
|
1345
|
+
properties: {
|
|
1346
|
+
contact: { $ref: '#/$defs/Contact' },
|
|
1347
|
+
},
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
const result = generateParserFunction(schema, 'UserObject', { useRefImports: true })
|
|
1351
|
+
|
|
1352
|
+
// Should still cache because $ref properties always need caching
|
|
1353
|
+
expect(result).toContain('const _contact = input.contact')
|
|
1354
|
+
})
|
|
1355
|
+
|
|
1356
|
+
it('optimization: caches variables for complex schemas with multiple constraints', () => {
|
|
1357
|
+
const schema: JSONSchema = {
|
|
1358
|
+
type: 'object',
|
|
1359
|
+
properties: {
|
|
1360
|
+
code: {
|
|
1361
|
+
type: 'string',
|
|
1362
|
+
pattern: '^[A-Z]',
|
|
1363
|
+
minLength: 3,
|
|
1364
|
+
maxLength: 10,
|
|
1365
|
+
},
|
|
1366
|
+
},
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const result = generateParserFunction(schema, 'CodeObject')
|
|
1370
|
+
|
|
1371
|
+
// Should cache because property has multiple constraints
|
|
1372
|
+
expect(result).toContain('const _code = input.code')
|
|
1373
|
+
})
|
|
1374
|
+
|
|
1375
|
+
it('optimization: fast-path returns input directly when all properties valid', () => {
|
|
1376
|
+
const schema: JSONSchema = {
|
|
1377
|
+
type: 'object',
|
|
1378
|
+
properties: {
|
|
1379
|
+
id: { type: 'number' },
|
|
1380
|
+
name: { type: 'string' },
|
|
1381
|
+
},
|
|
1382
|
+
required: ['id'],
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const result = generateParserFunction(schema, 'ItemObject')
|
|
1386
|
+
|
|
1387
|
+
// Should have fast-path that returns a new shallow copy
|
|
1388
|
+
expect(result).toContain('return { ...input } as ItemObject')
|
|
1389
|
+
// Fast-path check should exist
|
|
1390
|
+
expect(result).toContain('if (typeof _id')
|
|
1391
|
+
// Should have both fast and slow paths
|
|
1392
|
+
expect(result).toContain('return {')
|
|
1393
|
+
})
|
|
1394
|
+
|
|
1395
|
+
it('optimization: ref properties participate in deep fast-path via shape predicate', () => {
|
|
1396
|
+
const schema: JSONSchema = {
|
|
1397
|
+
type: 'object',
|
|
1398
|
+
properties: {
|
|
1399
|
+
contact: { $ref: '#/$defs/Contact' },
|
|
1400
|
+
name: { type: 'string' },
|
|
1401
|
+
},
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const result = generateParserFunction(schema, 'UserObject', { useRefImports: true })
|
|
1405
|
+
|
|
1406
|
+
// Fast-path now handles refs by calling the imported shape predicate.
|
|
1407
|
+
expect(result).toContain('validateContactObjectShape(_contact)')
|
|
1408
|
+
expect(result).toContain('return { ...input } as UserObject')
|
|
1409
|
+
// Slow path still exists for invalid input.
|
|
1410
|
+
expect(result).toContain('return {')
|
|
1411
|
+
})
|
|
1412
|
+
|
|
1413
|
+
it('optimization: generates efficient code for all optional properties', () => {
|
|
1414
|
+
const schema: JSONSchema = {
|
|
1415
|
+
type: 'object',
|
|
1416
|
+
properties: {
|
|
1417
|
+
a: { type: 'string' },
|
|
1418
|
+
b: { type: 'string' },
|
|
1419
|
+
c: { type: 'string' },
|
|
1420
|
+
},
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const result = generateParserFunction(schema, 'OptionalObject')
|
|
1424
|
+
|
|
1425
|
+
// Should have fast-path for all optional
|
|
1426
|
+
expect(result).toContain('return { ...input } as OptionalObject')
|
|
1427
|
+
// Should use spread operators for optional properties
|
|
1428
|
+
const spreadCount = (result.match(/\.\.\./g) || []).length
|
|
1429
|
+
expect(spreadCount).toBeGreaterThan(0)
|
|
1430
|
+
})
|
|
1431
|
+
|
|
1432
|
+
it('optimization: handles mixed required and optional with fast-path', () => {
|
|
1433
|
+
const schema: JSONSchema = {
|
|
1434
|
+
type: 'object',
|
|
1435
|
+
properties: {
|
|
1436
|
+
id: { type: 'number' },
|
|
1437
|
+
name: { type: 'string' },
|
|
1438
|
+
email: { type: 'string' },
|
|
1439
|
+
},
|
|
1440
|
+
required: ['id', 'name'],
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const result = generateParserFunction(schema, 'UserObject')
|
|
1444
|
+
|
|
1445
|
+
// Should cache all variables for fast-path
|
|
1446
|
+
expect(result).toContain('const _id')
|
|
1447
|
+
expect(result).toContain('const _name')
|
|
1448
|
+
expect(result).toContain('const _email')
|
|
1449
|
+
// Should have fast-path check
|
|
1450
|
+
expect(result).toContain('return { ...input } as UserObject')
|
|
1451
|
+
// Fast-path should check required fields and optional fields
|
|
1452
|
+
expect(result).toContain('typeof _id === "number"')
|
|
1453
|
+
expect(result).toContain('typeof _name === "string"')
|
|
1454
|
+
expect(result).toContain('_email === undefined || typeof _email === "string"')
|
|
1455
|
+
})
|
|
1456
|
+
|
|
1457
|
+
it('optimization: selective caching based on property complexity', () => {
|
|
1458
|
+
const schema: JSONSchema = {
|
|
1459
|
+
type: 'object',
|
|
1460
|
+
properties: {
|
|
1461
|
+
// Simple property - could be inlined if no fast-path
|
|
1462
|
+
simple: { type: 'string' },
|
|
1463
|
+
// Complex property - should be cached
|
|
1464
|
+
complex: {
|
|
1465
|
+
type: 'string',
|
|
1466
|
+
pattern: '^[A-Z]',
|
|
1467
|
+
minLength: 5,
|
|
1468
|
+
maxLength: 100,
|
|
1469
|
+
},
|
|
1470
|
+
},
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const result = generateParserFunction(schema, 'MixedObject')
|
|
1474
|
+
|
|
1475
|
+
// Both should be cached because fast-path exists
|
|
1476
|
+
expect(result).toContain('const _simple')
|
|
1477
|
+
expect(result).toContain('const _complex')
|
|
1478
|
+
})
|
|
1479
|
+
|
|
1480
|
+
it('generates parser for product schema with required fields, minimum constraint, boolean, and array', () => {
|
|
1481
|
+
const schema: JSONSchema = {
|
|
1482
|
+
description: 'A product available for purchase in the catalog.',
|
|
1483
|
+
type: 'object',
|
|
1484
|
+
properties: {
|
|
1485
|
+
id: { description: 'Unique product identifier (UUID).', type: 'string' },
|
|
1486
|
+
name: { description: 'Display name shown to customers.', type: 'string' },
|
|
1487
|
+
price: { description: 'Unit price in USD cents (must be non-negative).', type: 'number', minimum: 0 },
|
|
1488
|
+
inStock: { description: 'Whether the product is currently available for purchase.', type: 'boolean' },
|
|
1489
|
+
tags: {
|
|
1490
|
+
description: 'Searchable labels associated with the product.',
|
|
1491
|
+
type: 'array',
|
|
1492
|
+
items: { type: 'string' },
|
|
1493
|
+
},
|
|
1494
|
+
},
|
|
1495
|
+
required: ['id', 'name', 'price'],
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const result = generateParserFunction(schema, 'Product')
|
|
1499
|
+
|
|
1500
|
+
expect(result).toBe(
|
|
1501
|
+
`export const parseProduct = (input: unknown): Product => {
|
|
1502
|
+
if (!isObject(input)) return {
|
|
1503
|
+
id: "",
|
|
1504
|
+
name: "",
|
|
1505
|
+
price: 0,
|
|
1506
|
+
};
|
|
1507
|
+
const _id = input.id;
|
|
1508
|
+
const _name = input.name;
|
|
1509
|
+
const _price = input.price;
|
|
1510
|
+
const _inStock = input.inStock;
|
|
1511
|
+
const _tags = input.tags;
|
|
1512
|
+
if (typeof _id === "string" && typeof _name === "string" && typeof _price === "number" && _price >= 0 && (_inStock === undefined || typeof _inStock === "boolean") && (_tags === undefined || Array.isArray(_tags))) return { ...input } as Product;
|
|
1513
|
+
return {
|
|
1514
|
+
...input,
|
|
1515
|
+
id: typeof _id === "string" ? _id : (_id !== undefined ? String(_id) : ""),
|
|
1516
|
+
name: typeof _name === "string" ? _name : (_name !== undefined ? String(_name) : ""),
|
|
1517
|
+
price: typeof _price === "number" && _price >= 0 ? _price : (_price !== undefined ? (Number.isFinite(Number(_price)) ? Number(_price) : 0) : 0),
|
|
1518
|
+
...(_inStock !== undefined && { inStock: typeof _inStock === "boolean" ? _inStock : Boolean(_inStock) }),
|
|
1519
|
+
...(_tags !== undefined && { tags: Array.isArray(_tags) ? _tags : [] }),
|
|
1520
|
+
} as unknown as Product;
|
|
1521
|
+
}`,
|
|
1522
|
+
)
|
|
1523
|
+
})
|
|
1524
|
+
|
|
1525
|
+
it('generates parser for pagination params with integer minimum/maximum constraints', () => {
|
|
1526
|
+
const schema: JSONSchema = {
|
|
1527
|
+
description: 'Query parameters for paginated list endpoints.',
|
|
1528
|
+
type: 'object',
|
|
1529
|
+
properties: {
|
|
1530
|
+
page: { description: 'The 1-based page number to retrieve.', type: 'integer', minimum: 1 },
|
|
1531
|
+
perPage: { description: 'Number of items per page.', type: 'integer', minimum: 1, maximum: 100 },
|
|
1532
|
+
search: { description: 'Optional full-text search query.', type: 'string' },
|
|
1533
|
+
},
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const result = generateParserFunction(schema, 'PageParams')
|
|
1537
|
+
|
|
1538
|
+
expect(result).toBe(
|
|
1539
|
+
`export const parsePageParams = (input: unknown): PageParams => {
|
|
1540
|
+
if (!isObject(input)) return {} as PageParams;
|
|
1541
|
+
const _page = input.page;
|
|
1542
|
+
const _perPage = input.perPage;
|
|
1543
|
+
const _search = input.search;
|
|
1544
|
+
if ((_page === undefined || typeof _page === "number" && _page >= 1) && (_perPage === undefined || typeof _perPage === "number" && _perPage >= 1 && _perPage <= 100) && (_search === undefined || typeof _search === "string")) return { ...input } as PageParams;
|
|
1545
|
+
return {
|
|
1546
|
+
...input,
|
|
1547
|
+
...(_page !== undefined && { page: typeof _page === "number" && _page >= 1 ? _page : (Number.isFinite(Number(_page)) ? Number(_page) : 0) }),
|
|
1548
|
+
...(_perPage !== undefined && { perPage: typeof _perPage === "number" && _perPage >= 1 && _perPage <= 100 ? _perPage : (Number.isFinite(Number(_perPage)) ? Number(_perPage) : 0) }),
|
|
1549
|
+
...(_search !== undefined && { search: typeof _search === "string" ? _search : String(_search) }),
|
|
1550
|
+
} as unknown as PageParams;
|
|
1551
|
+
}`,
|
|
1552
|
+
)
|
|
1553
|
+
})
|
|
1554
|
+
|
|
1555
|
+
it('generates parser for string enum schema', () => {
|
|
1556
|
+
const schema: JSONSchema = {
|
|
1557
|
+
description: 'One of the supported theme colors.',
|
|
1558
|
+
type: 'string',
|
|
1559
|
+
enum: ['red', 'green', 'blue', 'yellow', 'purple'],
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
const result = generateParserFunction(schema, 'ThemeColor')
|
|
1563
|
+
|
|
1564
|
+
expect(result).toBe(
|
|
1565
|
+
'export const parseThemeColor = (input: unknown): ThemeColor => typeof input === "string" ? input as ThemeColor : "" as ThemeColor;',
|
|
1566
|
+
)
|
|
1567
|
+
})
|
|
1568
|
+
|
|
1569
|
+
it('generates parser for geo coordinate with min/max on required and optional number fields', () => {
|
|
1570
|
+
const schema: JSONSchema = {
|
|
1571
|
+
description: 'A geographic coordinate pair.',
|
|
1572
|
+
type: 'object',
|
|
1573
|
+
properties: {
|
|
1574
|
+
latitude: { description: 'Degrees latitude, from -90 to 90.', type: 'number', minimum: -90, maximum: 90 },
|
|
1575
|
+
longitude: { description: 'Degrees longitude, from -180 to 180.', type: 'number', minimum: -180, maximum: 180 },
|
|
1576
|
+
altitude: { description: 'Elevation in metres above sea level.', type: 'number' },
|
|
1577
|
+
label: { description: 'Human-readable name for this location.', type: 'string' },
|
|
1578
|
+
},
|
|
1579
|
+
required: ['latitude', 'longitude'],
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
const result = generateParserFunction(schema, 'GeoCoordinate')
|
|
1583
|
+
|
|
1584
|
+
expect(result).toBe(
|
|
1585
|
+
`export const parseGeoCoordinate = (input: unknown): GeoCoordinate => {
|
|
1586
|
+
if (!isObject(input)) return {
|
|
1587
|
+
latitude: 0,
|
|
1588
|
+
longitude: 0,
|
|
1589
|
+
};
|
|
1590
|
+
const _latitude = input.latitude;
|
|
1591
|
+
const _longitude = input.longitude;
|
|
1592
|
+
const _altitude = input.altitude;
|
|
1593
|
+
const _label = input.label;
|
|
1594
|
+
if (typeof _latitude === "number" && _latitude >= -90 && _latitude <= 90 && typeof _longitude === "number" && _longitude >= -180 && _longitude <= 180 && (_altitude === undefined || typeof _altitude === "number") && (_label === undefined || typeof _label === "string")) return { ...input } as GeoCoordinate;
|
|
1595
|
+
return {
|
|
1596
|
+
...input,
|
|
1597
|
+
latitude: typeof _latitude === "number" && _latitude >= -90 && _latitude <= 90 ? _latitude : (_latitude !== undefined ? (Number.isFinite(Number(_latitude)) ? Number(_latitude) : 0) : 0),
|
|
1598
|
+
longitude: typeof _longitude === "number" && _longitude >= -180 && _longitude <= 180 ? _longitude : (_longitude !== undefined ? (Number.isFinite(Number(_longitude)) ? Number(_longitude) : 0) : 0),
|
|
1599
|
+
...(_altitude !== undefined && { altitude: typeof _altitude === "number" ? _altitude : (Number.isFinite(Number(_altitude)) ? Number(_altitude) : 0) }),
|
|
1600
|
+
...(_label !== undefined && { label: typeof _label === "string" ? _label : String(_label) }),
|
|
1601
|
+
} as unknown as GeoCoordinate;
|
|
1602
|
+
}`,
|
|
1603
|
+
)
|
|
1604
|
+
})
|
|
1605
|
+
|
|
1606
|
+
describe('logWarnings option', () => {
|
|
1607
|
+
it('emits a console.warn loop for unknown properties when logWarnings is true', () => {
|
|
1608
|
+
const schema: JSONSchema = {
|
|
1609
|
+
type: 'object',
|
|
1610
|
+
properties: {
|
|
1611
|
+
name: { type: 'string' },
|
|
1612
|
+
age: { type: 'number' },
|
|
1613
|
+
},
|
|
1614
|
+
required: ['name'],
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
const result = generateParserFunction(schema, 'UserObject', { logWarnings: true })
|
|
1618
|
+
expect(result).toBe(
|
|
1619
|
+
`export const parseUserObject = (input: unknown): UserObject => {
|
|
1620
|
+
if (!isObject(input)) return {
|
|
1621
|
+
name: "",
|
|
1622
|
+
};
|
|
1623
|
+
const _name = input.name;
|
|
1624
|
+
const _age = input.age;
|
|
1625
|
+
const _knownKeys = new Set(["name", "age"]);
|
|
1626
|
+
for (const _k in input) {
|
|
1627
|
+
if (!_knownKeys.has(_k)) {
|
|
1628
|
+
console.warn(\`[UserObject] Unknown property "\${_k}"\`);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
if (typeof _name === "string" && (_age === undefined || typeof _age === "number")) return { ...input } as UserObject;
|
|
1632
|
+
return {
|
|
1633
|
+
...input,
|
|
1634
|
+
name: typeof _name === "string" ? _name : (_name !== undefined ? String(_name) : ""),
|
|
1635
|
+
...(_age !== undefined && { age: typeof _age === "number" ? _age : (Number.isFinite(Number(_age)) ? Number(_age) : 0) }),
|
|
1636
|
+
} as unknown as UserObject;
|
|
1637
|
+
}`,
|
|
1638
|
+
)
|
|
1639
|
+
})
|
|
1640
|
+
|
|
1641
|
+
it('does not emit a console.warn loop when logWarnings is false', () => {
|
|
1642
|
+
const schema: JSONSchema = {
|
|
1643
|
+
type: 'object',
|
|
1644
|
+
properties: {
|
|
1645
|
+
name: { type: 'string' },
|
|
1646
|
+
},
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const result = generateParserFunction(schema, 'UserObject', { logWarnings: false })
|
|
1650
|
+
expect(result).not.toContain('console.warn')
|
|
1651
|
+
expect(result).not.toContain('_knownKeys')
|
|
1652
|
+
})
|
|
1653
|
+
|
|
1654
|
+
it('does not emit a console.warn loop when logWarnings is not set', () => {
|
|
1655
|
+
const schema: JSONSchema = {
|
|
1656
|
+
type: 'object',
|
|
1657
|
+
properties: {
|
|
1658
|
+
name: { type: 'string' },
|
|
1659
|
+
},
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
const result = generateParserFunction(schema, 'UserObject')
|
|
1663
|
+
expect(result).not.toContain('console.warn')
|
|
1664
|
+
})
|
|
1665
|
+
})
|
|
1666
|
+
|
|
1667
|
+
describe('strict option', () => {
|
|
1668
|
+
it('throws on non-object input for object schemas', () => {
|
|
1669
|
+
const schema: JSONSchema = {
|
|
1670
|
+
type: 'object',
|
|
1671
|
+
properties: { name: { type: 'string' } },
|
|
1672
|
+
required: ['name'],
|
|
1673
|
+
}
|
|
1674
|
+
const result = generateParserFunction(schema, 'UserObject', { strict: true })
|
|
1675
|
+
expect(result).toContain(
|
|
1676
|
+
'if (!isObject(input)) throw new Error(`[UserObject] expected object, got ${input === null ? "null" : typeof input}`)',
|
|
1677
|
+
)
|
|
1678
|
+
expect(result).not.toContain('if (!isObject(input)) return')
|
|
1679
|
+
})
|
|
1680
|
+
|
|
1681
|
+
it('throws on missing required property', () => {
|
|
1682
|
+
const schema: JSONSchema = {
|
|
1683
|
+
type: 'object',
|
|
1684
|
+
properties: { name: { type: 'string' } },
|
|
1685
|
+
required: ['name'],
|
|
1686
|
+
}
|
|
1687
|
+
const result = generateParserFunction(schema, 'UserObject', { strict: true })
|
|
1688
|
+
expect(result).toContain(
|
|
1689
|
+
'if (!("name" in input)) throw new Error(\'[UserObject] missing required property "name"\')',
|
|
1690
|
+
)
|
|
1691
|
+
})
|
|
1692
|
+
|
|
1693
|
+
it('throws on wrong primitive type for required property', () => {
|
|
1694
|
+
const schema: JSONSchema = {
|
|
1695
|
+
type: 'object',
|
|
1696
|
+
properties: { name: { type: 'string' } },
|
|
1697
|
+
required: ['name'],
|
|
1698
|
+
}
|
|
1699
|
+
const result = generateParserFunction(schema, 'UserObject', { strict: true })
|
|
1700
|
+
expect(result).toContain(
|
|
1701
|
+
'if (typeof input.name !== "string") throw new Error(`[UserObject] field "name" expected string, got ${typeof input.name}`)',
|
|
1702
|
+
)
|
|
1703
|
+
})
|
|
1704
|
+
|
|
1705
|
+
it('only throws on wrong type for optional property when provided', () => {
|
|
1706
|
+
const schema: JSONSchema = {
|
|
1707
|
+
type: 'object',
|
|
1708
|
+
properties: { age: { type: 'number' } },
|
|
1709
|
+
}
|
|
1710
|
+
const result = generateParserFunction(schema, 'UserObject', { strict: true })
|
|
1711
|
+
expect(result).toContain(
|
|
1712
|
+
'if (input.age !== undefined && (typeof input.age !== "number")) throw new Error(`[UserObject] field "age" expected number, got ${typeof input.age}`)',
|
|
1713
|
+
)
|
|
1714
|
+
})
|
|
1715
|
+
|
|
1716
|
+
it('throws on enum mismatch', () => {
|
|
1717
|
+
const schema: JSONSchema = {
|
|
1718
|
+
type: 'object',
|
|
1719
|
+
properties: { role: { enum: ['admin', 'user'] } },
|
|
1720
|
+
required: ['role'],
|
|
1721
|
+
}
|
|
1722
|
+
const result = generateParserFunction(schema, 'UserObject', { strict: true })
|
|
1723
|
+
expect(result).toContain('must be one of: "admin", "user"')
|
|
1724
|
+
})
|
|
1725
|
+
|
|
1726
|
+
it('throws on pattern mismatch for strings', () => {
|
|
1727
|
+
const schema: JSONSchema = {
|
|
1728
|
+
type: 'object',
|
|
1729
|
+
properties: { code: { type: 'string', pattern: '^[A-Z]+$' } },
|
|
1730
|
+
required: ['code'],
|
|
1731
|
+
}
|
|
1732
|
+
const result = generateParserFunction(schema, 'UserObject', { strict: true })
|
|
1733
|
+
expect(result).toContain('must match pattern ^[A-Z]+$')
|
|
1734
|
+
})
|
|
1735
|
+
|
|
1736
|
+
it('throws on minLength / maxLength violations', () => {
|
|
1737
|
+
const schema: JSONSchema = {
|
|
1738
|
+
type: 'object',
|
|
1739
|
+
properties: { name: { type: 'string', minLength: 1, maxLength: 10 } },
|
|
1740
|
+
required: ['name'],
|
|
1741
|
+
}
|
|
1742
|
+
const result = generateParserFunction(schema, 'UserObject', { strict: true })
|
|
1743
|
+
expect(result).toContain('must have at least 1 characters')
|
|
1744
|
+
expect(result).toContain('must have at most 10 characters')
|
|
1745
|
+
})
|
|
1746
|
+
|
|
1747
|
+
it('throws on minimum / maximum / multipleOf violations', () => {
|
|
1748
|
+
const schema: JSONSchema = {
|
|
1749
|
+
type: 'object',
|
|
1750
|
+
properties: { age: { type: 'number', minimum: 0, maximum: 120, multipleOf: 1 } },
|
|
1751
|
+
required: ['age'],
|
|
1752
|
+
}
|
|
1753
|
+
const result = generateParserFunction(schema, 'UserObject', { strict: true })
|
|
1754
|
+
expect(result).toContain('must be >= 0')
|
|
1755
|
+
expect(result).toContain('must be <= 120')
|
|
1756
|
+
expect(result).toContain('must be a multiple of 1')
|
|
1757
|
+
})
|
|
1758
|
+
|
|
1759
|
+
it('does not generate strict assertions for $ref properties (delegated to nested parser)', () => {
|
|
1760
|
+
const schema: JSONSchema = {
|
|
1761
|
+
type: 'object',
|
|
1762
|
+
properties: { contact: { $ref: '#/$defs/contact' } },
|
|
1763
|
+
required: ['contact'],
|
|
1764
|
+
}
|
|
1765
|
+
const result = generateParserFunction(schema, 'UserObject', { strict: true, useRefImports: true })
|
|
1766
|
+
// The missing-required check is still emitted, but no inline type check.
|
|
1767
|
+
expect(result).toContain('missing required property "contact"')
|
|
1768
|
+
expect(result).not.toContain('field "contact" expected')
|
|
1769
|
+
})
|
|
1770
|
+
|
|
1771
|
+
it('throws on wrong type for non-object scalar schemas', () => {
|
|
1772
|
+
const schema: JSONSchema = { type: 'string' }
|
|
1773
|
+
const result = generateParserFunction(schema, 'Name', { strict: true })
|
|
1774
|
+
expect(result).toContain(
|
|
1775
|
+
'if (typeof input !== "string") throw new Error(`[Name] expected string, got ${input === null ? "null" : typeof input}`)',
|
|
1776
|
+
)
|
|
1777
|
+
expect(result).toContain('return input as Name;')
|
|
1778
|
+
})
|
|
1779
|
+
|
|
1780
|
+
it('throws on non-object input for empty object schemas', () => {
|
|
1781
|
+
const schema: JSONSchema = { type: 'object' }
|
|
1782
|
+
const result = generateParserFunction(schema, 'AnyObject', { strict: true })
|
|
1783
|
+
expect(result).toContain('if (!isObject(input)) throw new Error')
|
|
1784
|
+
expect(result).toContain('return { ...input } as AnyObject;')
|
|
1785
|
+
})
|
|
1786
|
+
|
|
1787
|
+
it('preserves existing safe behavior when strict is not set', () => {
|
|
1788
|
+
const schema: JSONSchema = {
|
|
1789
|
+
type: 'object',
|
|
1790
|
+
properties: { name: { type: 'string' } },
|
|
1791
|
+
required: ['name'],
|
|
1792
|
+
}
|
|
1793
|
+
const result = generateParserFunction(schema, 'UserObject')
|
|
1794
|
+
expect(result).not.toContain('throw new Error')
|
|
1795
|
+
expect(result).toContain('if (!isObject(input)) return')
|
|
1796
|
+
})
|
|
1797
|
+
})
|
|
1798
|
+
})
|