@amritk/generate-parsers 0.7.2 → 0.9.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.
Files changed (73) hide show
  1. package/README.md +1 -1
  2. package/dist/generators/build-schema.d.ts +0 -1
  3. package/dist/generators/generate-default-from-pattern.d.ts +0 -1
  4. package/dist/generators/generate-discriminated-union-validation.d.ts +0 -1
  5. package/dist/generators/generate-enum-check.d.ts +0 -1
  6. package/dist/generators/generate-files.d.ts +0 -1
  7. package/dist/generators/generate-non-discriminated-union-validation.d.ts +0 -1
  8. package/dist/generators/generate-parser-function.d.ts +3 -3
  9. package/dist/generators/generate-parser-function.js +267 -32
  10. package/dist/generators/generate-schema-checks.d.ts +0 -1
  11. package/dist/generators/generate-strict-assertion.d.ts +0 -1
  12. package/dist/generators/generate-validation-expression.d.ts +0 -1
  13. package/dist/helpers/apply-schema-extensions.d.ts +0 -1
  14. package/dist/helpers/collect-helpers.d.ts +0 -1
  15. package/dist/helpers/collect-imports.d.ts +0 -1
  16. package/dist/helpers/find-discriminator.d.ts +0 -1
  17. package/dist/helpers/get-default-value.d.ts +0 -1
  18. package/dist/helpers/get-discriminator-value.d.ts +0 -1
  19. package/dist/index.d.ts +0 -1
  20. package/dist/types/schema-extensions.d.ts +0 -1
  21. package/package.json +3 -5
  22. package/dist/generators/build-schema.d.ts.map +0 -1
  23. package/dist/generators/generate-default-from-pattern.d.ts.map +0 -1
  24. package/dist/generators/generate-discriminated-union-validation.d.ts.map +0 -1
  25. package/dist/generators/generate-enum-check.d.ts.map +0 -1
  26. package/dist/generators/generate-files.d.ts.map +0 -1
  27. package/dist/generators/generate-non-discriminated-union-validation.d.ts.map +0 -1
  28. package/dist/generators/generate-parser-function.d.ts.map +0 -1
  29. package/dist/generators/generate-schema-checks.d.ts.map +0 -1
  30. package/dist/generators/generate-strict-assertion.d.ts.map +0 -1
  31. package/dist/generators/generate-validation-expression.d.ts.map +0 -1
  32. package/dist/helpers/apply-schema-extensions.d.ts.map +0 -1
  33. package/dist/helpers/collect-helpers.d.ts.map +0 -1
  34. package/dist/helpers/collect-imports.d.ts.map +0 -1
  35. package/dist/helpers/find-discriminator.d.ts.map +0 -1
  36. package/dist/helpers/get-default-value.d.ts.map +0 -1
  37. package/dist/helpers/get-discriminator-value.d.ts.map +0 -1
  38. package/dist/index.d.ts.map +0 -1
  39. package/dist/types/schema-extensions.d.ts.map +0 -1
  40. package/src/generators/build-schema.test.ts +0 -854
  41. package/src/generators/build-schema.ts +0 -147
  42. package/src/generators/generate-default-from-pattern.test.ts +0 -81
  43. package/src/generators/generate-default-from-pattern.ts +0 -39
  44. package/src/generators/generate-discriminated-union-validation.test.ts +0 -84
  45. package/src/generators/generate-discriminated-union-validation.ts +0 -54
  46. package/src/generators/generate-enum-check.test.ts +0 -45
  47. package/src/generators/generate-enum-check.ts +0 -8
  48. package/src/generators/generate-files.test.ts +0 -448
  49. package/src/generators/generate-files.ts +0 -155
  50. package/src/generators/generate-non-discriminated-union-validation.test.ts +0 -74
  51. package/src/generators/generate-non-discriminated-union-validation.ts +0 -40
  52. package/src/generators/generate-parser-function.test.ts +0 -1794
  53. package/src/generators/generate-parser-function.ts +0 -1340
  54. package/src/generators/generate-schema-checks.test.ts +0 -330
  55. package/src/generators/generate-schema-checks.ts +0 -128
  56. package/src/generators/generate-strict-assertion.test.ts +0 -67
  57. package/src/generators/generate-strict-assertion.ts +0 -243
  58. package/src/generators/generate-validation-expression.test.ts +0 -831
  59. package/src/generators/generate-validation-expression.ts +0 -327
  60. package/src/helpers/apply-schema-extensions.test.ts +0 -194
  61. package/src/helpers/apply-schema-extensions.ts +0 -63
  62. package/src/helpers/collect-helpers.test.ts +0 -115
  63. package/src/helpers/collect-helpers.ts +0 -79
  64. package/src/helpers/collect-imports.test.ts +0 -547
  65. package/src/helpers/collect-imports.ts +0 -275
  66. package/src/helpers/find-discriminator.test.ts +0 -88
  67. package/src/helpers/find-discriminator.ts +0 -39
  68. package/src/helpers/get-default-value.test.ts +0 -139
  69. package/src/helpers/get-default-value.ts +0 -95
  70. package/src/helpers/get-discriminator-value.test.ts +0 -83
  71. package/src/helpers/get-discriminator-value.ts +0 -21
  72. package/src/index.ts +0 -1
  73. package/src/types/schema-extensions.ts +0 -35
@@ -1,1794 +0,0 @@
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, 'User')
18
- expect(result).toBe(
19
- `export const parseUser = (input: unknown): User => {
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 User;
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 User;
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, 'Product')
44
- expect(result).toBe(
45
- `export const parseProduct = (input: unknown): Product => {
46
- if (!isObject(input)) return {} as Product;
47
- const _id = input.id;
48
- if ((_id === undefined || typeof _id === "number")) return { ...input } as Product;
49
- return {
50
- ...input,
51
- ...(_id !== undefined && { id: typeof _id === "number" ? _id : (Number.isFinite(Number(_id)) ? Number(_id) : 0) }),
52
- } as unknown as Product;
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, 'Empty')
63
- expect(result).toBe(
64
- 'export const parseEmpty = (input: unknown): Empty => isObject(input) ? { ...input } as Empty : {} as Empty;',
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, 'User')
91
-
92
- expect(result).toBe(
93
- `export const parseUser = (input: unknown): User => {
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 User;
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 User;
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, 'Item')
123
-
124
- expect(result).toBe(
125
- `export const parseItem = (input: unknown): Item => {
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 Item;
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 Item;
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, 'Mixed')
151
-
152
- expect(result).toBe(
153
- `export const parseMixed = (input: unknown): Mixed => {
154
- if (!isObject(input)) return {} as Mixed;
155
- const _validProp = input.validProp;
156
- return {
157
- ...input,
158
- ...(_validProp !== undefined && { validProp: typeof _validProp === "string" ? _validProp : String(_validProp) }),
159
- } as unknown as Mixed;
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, 'User')
173
-
174
- expect(result).toBe(
175
- `export const parseUser = (input: unknown): User => {
176
- if (!isObject(input)) return {} as User;
177
- const _contact = input.contact;
178
- return {
179
- ...input,
180
- ...(_contact !== undefined && { contact: _contact ?? undefined }),
181
- } as unknown as User;
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, 'User', { useRefImports: true })
196
-
197
- expect(result).toBe(
198
- `export const parseUser = (input: unknown): User => {
199
- if (!isObject(input)) return {
200
- contact: parseContact(undefined),
201
- };
202
- const _contact = input.contact;
203
- if (validateContactShape(_contact)) return { ...input } as User;
204
- return {
205
- ...input,
206
- contact: parseContact(_contact),
207
- } as unknown as User;
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, 'User', { useRefImports: true })
221
-
222
- expect(result).toBe(
223
- `export const parseUser = (input: unknown): User => {
224
- if (!isObject(input)) return {} as User;
225
- const _contact = input.contact;
226
- if ((_contact === undefined || validateContactShape(_contact))) return { ...input } as User;
227
- return {
228
- ...input,
229
- ...(_contact !== undefined && { contact: parseContact(_contact) }),
230
- } as unknown as User;
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, 'User', { useRefImports: true })
248
-
249
- expect(result).toBe(
250
- `export const parseUser = (input: unknown): User => {
251
- if (!isObject(input)) return {
252
- contacts: [],
253
- };
254
- const _contacts = input.contacts;
255
- if (Array.isArray(_contacts) && _contacts.every(validateContactShape)) return { ...input } as User;
256
- return {
257
- ...input,
258
- contacts: validateArray(_contacts, parseContact),
259
- } as unknown as User;
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, 'User', { useRefImports: true })
276
-
277
- expect(result).toBe(
278
- `export const parseUser = (input: unknown): User => {
279
- if (!isObject(input)) return {} as User;
280
- const _contacts = input.contacts;
281
- if ((_contacts === undefined || Array.isArray(_contacts) && _contacts.every(validateContactShape))) return { ...input } as User;
282
- return {
283
- ...input,
284
- ...(_contacts !== undefined && { contacts: validateArray(_contacts, parseContact) }),
285
- } as unknown as User;
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, 'User', { useRefImports: false })
302
-
303
- expect(result).toBe(
304
- `export const parseUser = (input: unknown): User => {
305
- if (!isObject(input)) return {} as User;
306
- const _contacts = input.contacts;
307
- if ((_contacts === undefined || Array.isArray(_contacts))) return { ...input } as User;
308
- return {
309
- ...input,
310
- ...(_contacts !== undefined && { contacts: Array.isArray(_contacts) ? _contacts : [] }),
311
- } as unknown as User;
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, 'StringMap')
325
-
326
- expect(result).toBe(
327
- 'export const parseStringMap = (input: unknown): StringMap => validateRecord(input, (value: unknown) => typeof value === "string" ? value : "") as StringMap;',
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, 'ContactMap', { useRefImports: false })
340
-
341
- expect(result).toBe(
342
- `export const parseContactMap = (input: unknown): ContactMap => isObject(input) ? { ...input } as ContactMap : {};`,
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, 'ContactMap', { useRefImports: true })
355
-
356
- expect(result).toBe(
357
- `export const parseContactMap = (input: unknown): ContactMap => validateRecord(input, parseContact) as ContactMap;`,
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, 'Strict')
368
-
369
- expect(result).toBe(
370
- 'export const parseStrict = (input: unknown): Strict => isObject(input) ? { ...input } as Strict : {} as Strict;',
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, 'Complex')
388
-
389
- expect(result).toBe(
390
- `export const parseComplex = (input: unknown): Complex => {
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 Complex;
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 Complex;
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, 'ApiSpec', { useRefImports: true })
422
-
423
- expect(result).toBe(
424
- `export const parseApiSpec = (input: unknown): ApiSpec => {
425
- if (!isObject(input)) return {} as ApiSpec;
426
- const _externalDoc = input.externalDoc;
427
- if ((_externalDoc === undefined || validateExternalDocumentationShape(_externalDoc))) return { ...input } as ApiSpec;
428
- return {
429
- ...input,
430
- ...(_externalDoc !== undefined && { externalDoc: parseExternalDocumentation(_externalDoc) }),
431
- } as unknown as ApiSpec;
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, 'User', { useRefImports: true })
448
-
449
- expect(result).toBe(
450
- `export const parseUser = (input: unknown): User => {
451
- if (!isObject(input)) return {
452
- contact: parseContact(undefined),
453
- };
454
- const _contact = input.contact;
455
- const _address = input.address;
456
- const _company = input.company;
457
- if (validateContactShape(_contact) && (_address === undefined || validateAddressShape(_address)) && (_company === undefined || validateCompanyShape(_company))) return { ...input } as User;
458
- return {
459
- ...input,
460
- contact: parseContact(_contact),
461
- ...(_address !== undefined && { address: parseAddress(_address) }),
462
- ...(_company !== undefined && { company: parseCompany(_company) }),
463
- } as unknown as User;
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, 'User', { useRefImports: true })
480
-
481
- expect(result).toBe(
482
- `export const parseUser = (input: unknown): User => {
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 || validateContactShape(_contact)) && typeof _name === "string") return { ...input } as User;
491
- return {
492
- ...input,
493
- id: typeof _id === "number" ? _id : (_id !== undefined ? (Number.isFinite(Number(_id)) ? Number(_id) : 0) : 0),
494
- ...(_contact !== undefined && { contact: parseContact(_contact) }),
495
- name: typeof _name === "string" ? _name : (_name !== undefined ? String(_name) : ""),
496
- } as unknown as User;
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, 'Tagged', { useRefImports: true })
513
-
514
- expect(result).toBe(
515
- `export const parseTagged = (input: unknown): Tagged => {
516
- if (!isObject(input)) return {} as Tagged;
517
- const _tags = input.tags;
518
- if ((_tags === undefined || Array.isArray(_tags))) return { ...input } as Tagged;
519
- return {
520
- ...input,
521
- ...(_tags !== undefined && { tags: Array.isArray(_tags) ? _tags : [] }),
522
- } as unknown as Tagged;
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, 'ItemsContainer', { useRefImports: true })
538
-
539
- expect(result).toBe(
540
- `export const parseItemsContainer = (input: unknown): ItemsContainer => {
541
- if (!isObject(input)) return {} as ItemsContainer;
542
- const _items = input.items;
543
- if ((_items === undefined || Array.isArray(_items))) return { ...input } as ItemsContainer;
544
- return {
545
- ...input,
546
- ...(_items !== undefined && { items: Array.isArray(_items) ? _items : [] }),
547
- } as unknown as ItemsContainer;
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, 'User', { useRefImports: true })
562
-
563
- expect(result).toBe(
564
- `export const parseUser = (input: unknown): User => {
565
- if (!isObject(input)) return {} as User;
566
- const _contact = input.contact;
567
- const _age = input.age;
568
- if ((_contact === undefined || validateContactShape(_contact)) && (_age === undefined || typeof _age === "number")) return { ...input } as User;
569
- return {
570
- ...input,
571
- ...(_contact !== undefined && { contact: parseContact(_contact) }),
572
- ...(_age !== undefined && { age: typeof _age === "number" ? _age : (Number.isFinite(Number(_age)) ? Number(_age) : 0) }),
573
- } as unknown as User;
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, 'Empty')
585
-
586
- expect(result).toBe(
587
- `export const parseEmpty = (input: unknown): Empty => {
588
- if (!isObject(input)) return {} as Empty;
589
- return {
590
- ...input,
591
- } as unknown as Empty;
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, 'AllInvalid')
607
-
608
- expect(result).toBe(
609
- `export const parseAllInvalid = (input: unknown): AllInvalid => {
610
- if (!isObject(input)) return {} as AllInvalid;
611
- return {
612
- ...input,
613
- } as unknown as AllInvalid;
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, 'MyCustomType')
627
-
628
- expect(result).toBe(
629
- `export const parseMyCustomType = (input: unknown): MyCustomType => {
630
- if (!isObject(input)) return {} as MyCustomType;
631
- const _id = input.id;
632
- if ((_id === undefined || typeof _id === "number")) return { ...input } as MyCustomType;
633
- return {
634
- ...input,
635
- ...(_id !== undefined && { id: typeof _id === "number" ? _id : (Number.isFinite(Number(_id)) ? Number(_id) : 0) }),
636
- } as unknown as MyCustomType;
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, 'myCustomType')
650
-
651
- expect(result).toBe(
652
- `export const parsemyCustomType = (input: unknown): myCustomType => {
653
- if (!isObject(input)) return {} as myCustomType;
654
- const _id = input.id;
655
- if ((_id === undefined || typeof _id === "number")) return { ...input } as myCustomType;
656
- return {
657
- ...input,
658
- ...(_id !== undefined && { id: typeof _id === "number" ? _id : (Number.isFinite(Number(_id)) ? Number(_id) : 0) }),
659
- } as unknown as myCustomType;
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, 'Implicit')
672
-
673
- expect(result).toBe(
674
- `export const parseImplicit = (input: unknown): Implicit => {
675
- if (!isObject(input)) return {} as Implicit;
676
- const _name = input.name;
677
- if ((_name === undefined || typeof _name === "string")) return { ...input } as Implicit;
678
- return {
679
- ...input,
680
- ...(_name !== undefined && { name: typeof _name === "string" ? _name : String(_name) }),
681
- } as unknown as Implicit;
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, 'AnyAdditional')
693
-
694
- expect(result).toBe(
695
- 'export const parseAnyAdditional = (input: unknown): AnyAdditional => isObject(input) ? { ...input } as AnyAdditional : {} as AnyAdditional;',
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, 'MapOfStrings', { useRefImports: true })
708
-
709
- expect(result).toBe(
710
- 'export const parseMapOfStrings = (input: unknown): MapOfStrings => validateRecord(input, (value: unknown) => typeof value === "string" ? value : "") as MapOfStrings;',
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, 'SecurityRequirement', { useRefImports: true })
726
-
727
- expect(result).toBe(
728
- 'export const parseSecurityRequirement = (input: unknown): SecurityRequirement => validateRecord(input, (value: unknown) => Array.isArray(value) ? value : []) as SecurityRequirement;',
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, 'Container', { useRefImports: true })
744
-
745
- expect(result).toBe(
746
- `export const parseContainer = (input: unknown): Container => {
747
- if (!isObject(input)) return {} as Container;
748
- const _data = input.data;
749
- if ((_data === undefined || Array.isArray(_data))) return { ...input } as Container;
750
- return {
751
- ...input,
752
- ...(_data !== undefined && { data: Array.isArray(_data) ? _data : [] }),
753
- } as unknown as Container;
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, 'User', { useRefImports: true })
767
-
768
- expect(result).toBe(
769
- `export const parseUser = (input: unknown): User => {
770
- if (!isObject(input)) return {} as User;
771
- const _nested = input.nested;
772
- if ((_nested === undefined || validateTypeShape(_nested))) return { ...input } as User;
773
- return {
774
- ...input,
775
- ...(_nested !== undefined && { nested: parseType(_nested) }),
776
- } as unknown as User;
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, 'User', { useRefImports: true })
797
-
798
- expect(result).toBe(
799
- `export const parseUser = (input: unknown): User => {
800
- if (!isObject(input)) return {} as User;
801
- const _contacts = input.contacts;
802
- const _addresses = input.addresses;
803
- if ((_contacts === undefined || Array.isArray(_contacts) && _contacts.every(validateContactShape)) && (_addresses === undefined || Array.isArray(_addresses) && _addresses.every(validateAddressShape))) return { ...input } as User;
804
- return {
805
- ...input,
806
- ...(_contacts !== undefined && { contacts: validateArray(_contacts, parseContact) }),
807
- ...(_addresses !== undefined && { addresses: validateArray(_addresses, parseAddress) }),
808
- } as unknown as User;
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, 'User')
824
-
825
- expect(result).toBe(
826
- `export const parseUser = (input: unknown): User => {
827
- if (!isObject(input)) return {} as User;
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 User;
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 User;
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, 'User')
849
-
850
- expect(result).toBe(
851
- `export const parseUser = (input: unknown): User => {
852
- if (!isObject(input)) return {} as User;
853
- const _id = input.id;
854
- if ((_id === undefined || typeof _id === "number")) return { ...input } as User;
855
- return {
856
- ...input,
857
- ...(_id !== undefined && { id: typeof _id === "number" ? _id : (Number.isFinite(Number(_id)) ? Number(_id) : 0) }),
858
- } as unknown as User;
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, 'Map', { useRefImports: true })
872
-
873
- expect(result).toBe(`export const parseMap = (input: unknown): Map => validateRecord(input, parseValue) as Map;`)
874
- })
875
-
876
- it('generates a simple object pass-through parser for allOf-only schema', () => {
877
- // A schema with only allOf refs and type: object but no properties generates
878
- // a simple spread parser since there is nothing to specifically parse.
879
- const schema: JSONSchema = {
880
- type: 'object',
881
- allOf: [{ $ref: '#/$defs/type-apikey' }, { $ref: '#/$defs/type-http-bearer' }, { $ref: '#/$defs/type-oauth2' }],
882
- }
883
-
884
- const result = generateParserFunction(schema, 'SecurityScheme', { useRefImports: true })
885
-
886
- expect(result).toBe(
887
- `export const parseSecurityScheme = (input: unknown): SecurityScheme => isObject(input) ? { ...input } as SecurityScheme : {} as SecurityScheme;`,
888
- )
889
- })
890
-
891
- it('generates a parser for the components object', () => {
892
- const components: JSONSchema.Object = {
893
- type: 'object',
894
- properties: {
895
- responses: {
896
- type: 'object',
897
- additionalProperties: { $ref: '#/$defs/response' },
898
- },
899
- parameters: {
900
- type: 'object',
901
- additionalProperties: { $ref: '#/$defs/parameter' },
902
- },
903
- headers: {
904
- type: 'object',
905
- additionalProperties: { $ref: '#/$defs/header' },
906
- },
907
- pathItems: {
908
- type: 'object',
909
- additionalProperties: { $ref: '#/$defs/path-item' },
910
- },
911
- },
912
- }
913
-
914
- const result = generateParserFunction(components, 'Components', { useRefImports: true })
915
- expect(result).toBe(
916
- `export const parseComponents = (input: unknown): Components => {
917
- if (!isObject(input)) return {} as Components;
918
- const _responses = input.responses;
919
- const _parameters = input.parameters;
920
- const _headers = input.headers;
921
- const _pathItems = input.pathItems;
922
- return {
923
- ...input,
924
- ...(_responses !== undefined && { responses: validateRecord(_responses, parseResponse) }),
925
- ...(_parameters !== undefined && { parameters: validateRecord(_parameters, parseParameter) }),
926
- ...(_headers !== undefined && { headers: validateRecord(_headers, parseHeader) }),
927
- ...(_pathItems !== undefined && { pathItems: validateRecord(_pathItems, parsePathItem) }),
928
- } as unknown as Components;
929
- }`,
930
- )
931
- })
932
-
933
- it('handles ref schema with conditional if/then/else logic', () => {
934
- const schema: JSONSchema = {
935
- if: { type: 'object', required: ['$ref'] },
936
- then: { $ref: '#/$defs/reference' },
937
- else: { $ref: '#/$defs/callbacks' },
938
- }
939
-
940
- const result = generateParserFunction(schema, 'Conditional')
941
- expect(result).toBe(
942
- `export const parseConditional = (input: unknown): Conditional | Reference =>
943
- hasRef(input) ? parseReference(input) : parseCallbacks(input)
944
- `,
945
- )
946
- })
947
-
948
- it('handles OpenAPI paths schema with patternProperties', () => {
949
- // This is the actual schema from OpenAPI 3.1.2 for the paths object
950
- const schema: JSONSchema = {
951
- type: 'object',
952
- patternProperties: {
953
- '^/': { $ref: '#/$defs/path-item' },
954
- },
955
- $ref: '#/$defs/specification-extensions',
956
- unevaluatedProperties: false,
957
- }
958
-
959
- const result = generateParserFunction(schema, 'Paths', { useRefImports: true })
960
-
961
- expect(result).toBe(
962
- `export const parsePaths = (input: unknown): Paths => {
963
- if (!isObject(input)) {
964
- return {} as unknown as Paths;
965
- }
966
- const result = {
967
- ...input,
968
- } as unknown as Paths;
969
- for (const key in input) {
970
- if (/^\\//.test(key)) {
971
- const value = input[key];
972
- (result as Record<string, unknown>)[key] = parsePathItem(value);
973
- }
974
- }
975
- return result;
976
- };`,
977
- )
978
- })
979
-
980
- it('generates a simple object pass-through parser for allOf-only schema with multiple subtypes', () => {
981
- // With the OpenAPI SecurityScheme dispatcher logic removed, a schema that only has
982
- // allOf refs and no fixed properties generates a simple spread parser.
983
- const schema: JSONSchema = {
984
- type: 'object',
985
- allOf: [
986
- { $ref: '#/$defs/type-apikey' },
987
- { $ref: '#/$defs/type-http' },
988
- { $ref: '#/$defs/type-http-bearer' },
989
- { $ref: '#/$defs/type-oauth2' },
990
- { $ref: '#/$defs/type-oidc' },
991
- ],
992
- }
993
-
994
- const result = generateParserFunction(schema, 'SecurityScheme', { useRefImports: true })
995
-
996
- expect(result).toBe(
997
- `export const parseSecurityScheme = (input: unknown): SecurityScheme => isObject(input) ? { ...input } as SecurityScheme : {} as SecurityScheme;`,
998
- )
999
- })
1000
-
1001
- it('documents the difference: webhooks uses additionalProperties and works correctly', () => {
1002
- // This is how webhooks is defined in OpenAPI - uses additionalProperties instead of patternProperties
1003
- const schema: JSONSchema = {
1004
- type: 'object',
1005
- additionalProperties: { $ref: '#/$defs/path-item' },
1006
- }
1007
-
1008
- const result = generateParserFunction(schema, 'Webhooks', { useRefImports: true })
1009
-
1010
- // This works correctly because additionalProperties is handled
1011
- expect(result).toBe(
1012
- `export const parseWebhooks = (input: unknown): Webhooks => validateRecord(input, parsePathItem) as Webhooks;`,
1013
- )
1014
- expect(result).not.toContain('input as Webhooks')
1015
- })
1016
-
1017
- it('handles simple patternProperties with $ref', () => {
1018
- // A simpler case with just patternProperties
1019
- const schema: JSONSchema = {
1020
- type: 'object',
1021
- patternProperties: {
1022
- '^/': { $ref: '#/$defs/path-item' },
1023
- },
1024
- }
1025
-
1026
- const result = generateParserFunction(schema, 'SimplePaths', { useRefImports: true })
1027
-
1028
- expect(result).toBe(
1029
- `export const parseSimplePaths = (input: unknown): SimplePaths => {
1030
- if (!isObject(input)) {
1031
- return {} as unknown as SimplePaths;
1032
- }
1033
- const result = {
1034
- ...input,
1035
- } as unknown as SimplePaths;
1036
- for (const key in input) {
1037
- if (/^\\//.test(key)) {
1038
- const value = input[key];
1039
- (result as Record<string, unknown>)[key] = parsePathItem(value);
1040
- }
1041
- }
1042
- return result;
1043
- };`,
1044
- )
1045
- })
1046
-
1047
- it('generates consistent code for optional direct $ref properties in Document-like schemas', () => {
1048
- // This simulates the OpenAPI Document schema structure
1049
- const schema: JSONSchema = {
1050
- type: 'object',
1051
- properties: {
1052
- openapi: { type: 'string' },
1053
- info: { $ref: '#/$defs/info' },
1054
- servers: {
1055
- type: 'array',
1056
- items: { $ref: '#/$defs/server' },
1057
- },
1058
- paths: { $ref: '#/$defs/paths' },
1059
- webhooks: {
1060
- type: 'object',
1061
- additionalProperties: { $ref: '#/$defs/path-item' },
1062
- },
1063
- components: { $ref: '#/$defs/components' },
1064
- externalDocs: { $ref: '#/$defs/external-documentation' },
1065
- },
1066
- required: ['openapi', 'info'],
1067
- }
1068
-
1069
- const result = generateParserFunction(schema, 'Document', { useRefImports: true })
1070
- expect(result).toBe(`export const parseDocument = (input: unknown): Document => {
1071
- if (!isObject(input)) return {
1072
- openapi: "",
1073
- info: parseInfo(undefined),
1074
- };
1075
- const _openapi = input.openapi;
1076
- const _info = input.info;
1077
- const _servers = input.servers;
1078
- const _paths = input.paths;
1079
- const _webhooks = input.webhooks;
1080
- const _components = input.components;
1081
- const _externalDocs = input.externalDocs;
1082
- return {
1083
- ...input,
1084
- openapi: typeof _openapi === "string" ? _openapi : (_openapi !== undefined ? String(_openapi) : ""),
1085
- info: parseInfo(_info),
1086
- ...(_servers !== undefined && { servers: validateArray(_servers, parseServer) }),
1087
- ...(_paths !== undefined && { paths: parsePaths(_paths) }),
1088
- ...(_webhooks !== undefined && { webhooks: validateRecord(_webhooks, parsePathItem) }),
1089
- ...(_components !== undefined && { components: parseComponents(_components) }),
1090
- ...(_externalDocs !== undefined && { externalDocs: parseExternalDocumentation(_externalDocs) }),
1091
- } as unknown as Document;
1092
- }`)
1093
- })
1094
-
1095
- it('generates parser for components-like schema with $dynamicRef and record properties', () => {
1096
- // Tests that $dynamicRef in additionalProperties generates the schema pass-through,
1097
- // while plain $ref properties generate proper validateRecord calls.
1098
- const schema: JSONSchema = {
1099
- type: 'object',
1100
- properties: {
1101
- schemas: {
1102
- type: 'object',
1103
- additionalProperties: { $dynamicRef: '#meta' },
1104
- },
1105
- responses: {
1106
- type: 'object',
1107
- additionalProperties: { $ref: '#/$defs/response' },
1108
- },
1109
- parameters: {
1110
- type: 'object',
1111
- additionalProperties: { $ref: '#/$defs/parameter' },
1112
- },
1113
- pathItems: {
1114
- type: 'object',
1115
- additionalProperties: { $ref: '#/$defs/path-item' },
1116
- },
1117
- },
1118
- }
1119
-
1120
- const result = generateParserFunction(schema, 'Components', { useRefImports: true })
1121
- expect(result).toBe(
1122
- `export const parseComponents = (input: unknown): Components => {
1123
- if (!isObject(input)) return {} as Components;
1124
- const _schemas = input.schemas;
1125
- const _responses = input.responses;
1126
- const _parameters = input.parameters;
1127
- const _pathItems = input.pathItems;
1128
- return {
1129
- ...input,
1130
- ...(_schemas !== undefined && { schemas: isObject(_schemas) ? _schemas : typeof _schemas === "object" && _schemas !== null ? _schemas : {} }),
1131
- ...(_responses !== undefined && { responses: validateRecord(_responses, parseResponse) }),
1132
- ...(_parameters !== undefined && { parameters: validateRecord(_parameters, parseParameter) }),
1133
- ...(_pathItems !== undefined && { pathItems: validateRecord(_pathItems, parsePathItem) }),
1134
- } as unknown as Components;
1135
- }`,
1136
- )
1137
- })
1138
-
1139
- it('generates combined parser for schema with both properties and patternProperties', () => {
1140
- // A schema with a known property and patternProperties for dynamic keys
1141
- const schema: JSONSchema = {
1142
- type: 'object',
1143
- properties: {
1144
- default: { $ref: '#/$defs/response' },
1145
- },
1146
- patternProperties: {
1147
- '^[1-5](?:[0-9]{2}|XX)$': { $ref: '#/$defs/response' },
1148
- },
1149
- }
1150
-
1151
- const result = generateParserFunction(schema, 'Responses', { useRefImports: true })
1152
-
1153
- expect(result).toBe(
1154
- `export const parseResponses = (input: unknown): Responses => {
1155
- if (!isObject(input)) {
1156
- return {} as unknown as Responses;
1157
- }
1158
- const result = {
1159
- ...input,
1160
- ...(input.default && { default: parseResponse(input.default) }),
1161
- } as unknown as Responses;
1162
- for (const key in input) {
1163
- if (/^[1-5](?:[0-9]{2}|XX)$/.test(key)) {
1164
- const value = input[key];
1165
- (result as Record<string, unknown>)[key] = parseResponse(value);
1166
- }
1167
- }
1168
- return result;
1169
- };`,
1170
- )
1171
- })
1172
-
1173
- it('falls back to object parser when patternProperties has no $ref and useRefImports is false', () => {
1174
- const schema: JSONSchema = {
1175
- type: 'object',
1176
- properties: {
1177
- default: { $ref: '#/$defs/response' },
1178
- },
1179
- patternProperties: {
1180
- '^[1-5]': { type: 'string' },
1181
- },
1182
- }
1183
-
1184
- const result = generateParserFunction(schema, 'Responses', { useRefImports: false })
1185
-
1186
- // Without useRefImports, falls back to regular object parser
1187
- expect(result).toContain('isObject(input)')
1188
- expect(result).toContain('default:')
1189
- expect(result).not.toContain('for (const [key, value]')
1190
- })
1191
-
1192
- it('generates parser for schema object with $dynamicRef in additionalProperties', () => {
1193
- // This tests the JSON Schema 2020-12 $dynamicRef feature used in OpenAPI 3.1.2
1194
- // for the schemas property which can contain any valid JSON Schema
1195
- const schema: JSONSchema = {
1196
- type: 'object',
1197
- additionalProperties: {
1198
- $dynamicRef: '#meta',
1199
- },
1200
- }
1201
-
1202
- const result = generateParserFunction(schema, 'Schema', { useRefImports: true })
1203
-
1204
- // Schema is a special case that handles both boolean and object types
1205
- expect(result).toContain("if (typeof input === 'boolean')")
1206
- expect(result).toContain('return input as Schema')
1207
- expect(result).toContain('if (!isObject(input))')
1208
- expect(result).toContain('return {} as Schema')
1209
- })
1210
-
1211
- it('uses bracket notation for hyphenated property keys with $ref', () => {
1212
- const schema: JSONSchema = {
1213
- type: 'object',
1214
- properties: {
1215
- 'x-linkedin': { $ref: '#/$defs/extension' },
1216
- },
1217
- }
1218
-
1219
- const result = generateParserFunction(schema, 'InfoExtensions', { useRefImports: true })
1220
-
1221
- expect(result).toContain("input['x-linkedin']")
1222
- expect(result).toContain("'x-linkedin':")
1223
- expect(result).not.toContain('input.x-linkedin')
1224
- })
1225
-
1226
- it('uses bracket notation for hyphenated property keys with validation', () => {
1227
- const schema: JSONSchema = {
1228
- type: 'object',
1229
- properties: {
1230
- 'x-custom': { type: 'string' },
1231
- },
1232
- }
1233
-
1234
- const result = generateParserFunction(schema, 'Test')
1235
-
1236
- expect(result).toContain("input['x-custom']")
1237
- expect(result).toContain("'x-custom':")
1238
- expect(result).not.toContain('input.x-custom')
1239
- })
1240
-
1241
- it('generates object parser from conditional if/then fragments', () => {
1242
- const schema: JSONSchema = {
1243
- if: {
1244
- properties: {
1245
- type: {
1246
- const: 'http',
1247
- },
1248
- },
1249
- },
1250
- then: {
1251
- properties: {
1252
- scheme: {
1253
- type: 'string',
1254
- },
1255
- },
1256
- required: ['scheme'],
1257
- },
1258
- }
1259
-
1260
- const result = generateParserFunction(schema, 'TypeHttp', { useRefImports: true })
1261
-
1262
- expect(result).toContain('isObject(input)')
1263
- expect(result).toContain('type:')
1264
- expect(result).toContain('scheme:')
1265
- expect(result).not.toContain('input as TypeHttp')
1266
- })
1267
-
1268
- it('generates parser for patternProperties-only schema without explicit type', () => {
1269
- const schema: JSONSchema = {
1270
- patternProperties: {
1271
- '^x-': true,
1272
- },
1273
- }
1274
-
1275
- const result = generateParserFunction(schema, 'SpecificationExtensions', { useRefImports: true })
1276
-
1277
- expect(result).toContain('for (const key in input)')
1278
- expect(result).toContain('...input,')
1279
- expect(result).not.toContain('input as SpecificationExtensions')
1280
- })
1281
-
1282
- it('optimization: inlines variables for simple properties without fast-path', () => {
1283
- // When there's no fast-path, simple properties should be inlined
1284
- const schema: JSONSchema = {
1285
- type: 'object',
1286
- properties: {
1287
- summary: { type: 'string' },
1288
- description: { type: 'string' },
1289
- },
1290
- }
1291
-
1292
- const result = generateParserFunction(schema, 'Example')
1293
-
1294
- // Should NOT cache variables when no fast-path is possible
1295
- // (in this case, fast-path exists, so variables ARE cached)
1296
- expect(result).toContain('const _summary')
1297
- expect(result).toContain('const _description')
1298
- })
1299
-
1300
- it('optimization: caches variables when fast-path exists', () => {
1301
- const schema: JSONSchema = {
1302
- type: 'object',
1303
- properties: {
1304
- name: { type: 'string' },
1305
- age: { type: 'number' },
1306
- },
1307
- required: ['name'],
1308
- }
1309
-
1310
- const result = generateParserFunction(schema, 'User')
1311
-
1312
- // Should cache variables because fast-path exists
1313
- expect(result).toContain('const _name = input.name')
1314
- expect(result).toContain('const _age = input.age')
1315
- // Should have fast-path check
1316
- expect(result).toContain('if (typeof _name === "string"')
1317
- expect(result).toContain('return { ...input } as User')
1318
- })
1319
-
1320
- it('optimization: removes redundant undefined checks in optional properties', () => {
1321
- const schema: JSONSchema = {
1322
- type: 'object',
1323
- properties: {
1324
- summary: { type: 'string' },
1325
- description: { type: 'string' },
1326
- },
1327
- }
1328
-
1329
- const result = generateParserFunction(schema, 'Example')
1330
-
1331
- // The generated code should have the outer undefined check
1332
- expect(result).toContain('!== undefined &&')
1333
- // But should NOT have redundant nested undefined checks in the coercion
1334
- // Count occurrences - should only appear in the outer check, not in String() coercion
1335
- const matches = result.match(/!== undefined/g) || []
1336
- // Should have exactly 2 occurrences (one for each optional property's outer check)
1337
- expect(matches.length).toBe(2)
1338
- })
1339
-
1340
- it('optimization: inlines properties with $ref when they cannot use fast-path', () => {
1341
- const schema: JSONSchema = {
1342
- type: 'object',
1343
- properties: {
1344
- contact: { $ref: '#/$defs/Contact' },
1345
- },
1346
- }
1347
-
1348
- const result = generateParserFunction(schema, 'User', { useRefImports: true })
1349
-
1350
- // Should still cache because $ref properties always need caching
1351
- expect(result).toContain('const _contact = input.contact')
1352
- })
1353
-
1354
- it('optimization: caches variables for complex schemas with multiple constraints', () => {
1355
- const schema: JSONSchema = {
1356
- type: 'object',
1357
- properties: {
1358
- code: {
1359
- type: 'string',
1360
- pattern: '^[A-Z]',
1361
- minLength: 3,
1362
- maxLength: 10,
1363
- },
1364
- },
1365
- }
1366
-
1367
- const result = generateParserFunction(schema, 'Code')
1368
-
1369
- // Should cache because property has multiple constraints
1370
- expect(result).toContain('const _code = input.code')
1371
- })
1372
-
1373
- it('optimization: fast-path returns input directly when all properties valid', () => {
1374
- const schema: JSONSchema = {
1375
- type: 'object',
1376
- properties: {
1377
- id: { type: 'number' },
1378
- name: { type: 'string' },
1379
- },
1380
- required: ['id'],
1381
- }
1382
-
1383
- const result = generateParserFunction(schema, 'Item')
1384
-
1385
- // Should have fast-path that returns a new shallow copy
1386
- expect(result).toContain('return { ...input } as Item')
1387
- // Fast-path check should exist
1388
- expect(result).toContain('if (typeof _id')
1389
- // Should have both fast and slow paths
1390
- expect(result).toContain('return {')
1391
- })
1392
-
1393
- it('optimization: ref properties participate in deep fast-path via shape predicate', () => {
1394
- const schema: JSONSchema = {
1395
- type: 'object',
1396
- properties: {
1397
- contact: { $ref: '#/$defs/Contact' },
1398
- name: { type: 'string' },
1399
- },
1400
- }
1401
-
1402
- const result = generateParserFunction(schema, 'User', { useRefImports: true })
1403
-
1404
- // Fast-path now handles refs by calling the imported shape predicate.
1405
- expect(result).toContain('validateContactShape(_contact)')
1406
- expect(result).toContain('return { ...input } as User')
1407
- // Slow path still exists for invalid input.
1408
- expect(result).toContain('return {')
1409
- })
1410
-
1411
- it('optimization: generates efficient code for all optional properties', () => {
1412
- const schema: JSONSchema = {
1413
- type: 'object',
1414
- properties: {
1415
- a: { type: 'string' },
1416
- b: { type: 'string' },
1417
- c: { type: 'string' },
1418
- },
1419
- }
1420
-
1421
- const result = generateParserFunction(schema, 'Optional')
1422
-
1423
- // Should have fast-path for all optional
1424
- expect(result).toContain('return { ...input } as Optional')
1425
- // Should use spread operators for optional properties
1426
- const spreadCount = (result.match(/\.\.\./g) || []).length
1427
- expect(spreadCount).toBeGreaterThan(0)
1428
- })
1429
-
1430
- it('optimization: handles mixed required and optional with fast-path', () => {
1431
- const schema: JSONSchema = {
1432
- type: 'object',
1433
- properties: {
1434
- id: { type: 'number' },
1435
- name: { type: 'string' },
1436
- email: { type: 'string' },
1437
- },
1438
- required: ['id', 'name'],
1439
- }
1440
-
1441
- const result = generateParserFunction(schema, 'User')
1442
-
1443
- // Should cache all variables for fast-path
1444
- expect(result).toContain('const _id')
1445
- expect(result).toContain('const _name')
1446
- expect(result).toContain('const _email')
1447
- // Should have fast-path check
1448
- expect(result).toContain('return { ...input } as User')
1449
- // Fast-path should check required fields and optional fields
1450
- expect(result).toContain('typeof _id === "number"')
1451
- expect(result).toContain('typeof _name === "string"')
1452
- expect(result).toContain('_email === undefined || typeof _email === "string"')
1453
- })
1454
-
1455
- it('optimization: selective caching based on property complexity', () => {
1456
- const schema: JSONSchema = {
1457
- type: 'object',
1458
- properties: {
1459
- // Simple property - could be inlined if no fast-path
1460
- simple: { type: 'string' },
1461
- // Complex property - should be cached
1462
- complex: {
1463
- type: 'string',
1464
- pattern: '^[A-Z]',
1465
- minLength: 5,
1466
- maxLength: 100,
1467
- },
1468
- },
1469
- }
1470
-
1471
- const result = generateParserFunction(schema, 'Mixed')
1472
-
1473
- // Both should be cached because fast-path exists
1474
- expect(result).toContain('const _simple')
1475
- expect(result).toContain('const _complex')
1476
- })
1477
-
1478
- it('generates parser for product schema with required fields, minimum constraint, boolean, and array', () => {
1479
- const schema: JSONSchema = {
1480
- description: 'A product available for purchase in the catalog.',
1481
- type: 'object',
1482
- properties: {
1483
- id: { description: 'Unique product identifier (UUID).', type: 'string' },
1484
- name: { description: 'Display name shown to customers.', type: 'string' },
1485
- price: { description: 'Unit price in USD cents (must be non-negative).', type: 'number', minimum: 0 },
1486
- inStock: { description: 'Whether the product is currently available for purchase.', type: 'boolean' },
1487
- tags: {
1488
- description: 'Searchable labels associated with the product.',
1489
- type: 'array',
1490
- items: { type: 'string' },
1491
- },
1492
- },
1493
- required: ['id', 'name', 'price'],
1494
- }
1495
-
1496
- const result = generateParserFunction(schema, 'Product')
1497
-
1498
- expect(result).toBe(
1499
- `export const parseProduct = (input: unknown): Product => {
1500
- if (!isObject(input)) return {
1501
- id: "",
1502
- name: "",
1503
- price: 0,
1504
- };
1505
- const _id = input.id;
1506
- const _name = input.name;
1507
- const _price = input.price;
1508
- const _inStock = input.inStock;
1509
- const _tags = input.tags;
1510
- 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;
1511
- return {
1512
- ...input,
1513
- id: typeof _id === "string" ? _id : (_id !== undefined ? String(_id) : ""),
1514
- name: typeof _name === "string" ? _name : (_name !== undefined ? String(_name) : ""),
1515
- price: typeof _price === "number" && _price >= 0 ? _price : (_price !== undefined ? (Number.isFinite(Number(_price)) ? Number(_price) : 0) : 0),
1516
- ...(_inStock !== undefined && { inStock: typeof _inStock === "boolean" ? _inStock : Boolean(_inStock) }),
1517
- ...(_tags !== undefined && { tags: Array.isArray(_tags) ? _tags : [] }),
1518
- } as unknown as Product;
1519
- }`,
1520
- )
1521
- })
1522
-
1523
- it('generates parser for pagination params with integer minimum/maximum constraints', () => {
1524
- const schema: JSONSchema = {
1525
- description: 'Query parameters for paginated list endpoints.',
1526
- type: 'object',
1527
- properties: {
1528
- page: { description: 'The 1-based page number to retrieve.', type: 'integer', minimum: 1 },
1529
- perPage: { description: 'Number of items per page.', type: 'integer', minimum: 1, maximum: 100 },
1530
- search: { description: 'Optional full-text search query.', type: 'string' },
1531
- },
1532
- }
1533
-
1534
- const result = generateParserFunction(schema, 'PageParams')
1535
-
1536
- expect(result).toBe(
1537
- `export const parsePageParams = (input: unknown): PageParams => {
1538
- if (!isObject(input)) return {} as PageParams;
1539
- const _page = input.page;
1540
- const _perPage = input.perPage;
1541
- const _search = input.search;
1542
- 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;
1543
- return {
1544
- ...input,
1545
- ...(_page !== undefined && { page: typeof _page === "number" && _page >= 1 ? _page : (Number.isFinite(Number(_page)) ? Number(_page) : 0) }),
1546
- ...(_perPage !== undefined && { perPage: typeof _perPage === "number" && _perPage >= 1 && _perPage <= 100 ? _perPage : (Number.isFinite(Number(_perPage)) ? Number(_perPage) : 0) }),
1547
- ...(_search !== undefined && { search: typeof _search === "string" ? _search : String(_search) }),
1548
- } as unknown as PageParams;
1549
- }`,
1550
- )
1551
- })
1552
-
1553
- it('generates parser for string enum schema', () => {
1554
- const schema: JSONSchema = {
1555
- description: 'One of the supported theme colors.',
1556
- type: 'string',
1557
- enum: ['red', 'green', 'blue', 'yellow', 'purple'],
1558
- }
1559
-
1560
- const result = generateParserFunction(schema, 'ThemeColor')
1561
-
1562
- expect(result).toBe(
1563
- 'export const parseThemeColor = (input: unknown): ThemeColor => typeof input === "string" ? input as ThemeColor : "" as ThemeColor;',
1564
- )
1565
- })
1566
-
1567
- it('generates parser for geo coordinate with min/max on required and optional number fields', () => {
1568
- const schema: JSONSchema = {
1569
- description: 'A geographic coordinate pair.',
1570
- type: 'object',
1571
- properties: {
1572
- latitude: { description: 'Degrees latitude, from -90 to 90.', type: 'number', minimum: -90, maximum: 90 },
1573
- longitude: { description: 'Degrees longitude, from -180 to 180.', type: 'number', minimum: -180, maximum: 180 },
1574
- altitude: { description: 'Elevation in metres above sea level.', type: 'number' },
1575
- label: { description: 'Human-readable name for this location.', type: 'string' },
1576
- },
1577
- required: ['latitude', 'longitude'],
1578
- }
1579
-
1580
- const result = generateParserFunction(schema, 'GeoCoordinate')
1581
-
1582
- expect(result).toBe(
1583
- `export const parseGeoCoordinate = (input: unknown): GeoCoordinate => {
1584
- if (!isObject(input)) return {
1585
- latitude: 0,
1586
- longitude: 0,
1587
- };
1588
- const _latitude = input.latitude;
1589
- const _longitude = input.longitude;
1590
- const _altitude = input.altitude;
1591
- const _label = input.label;
1592
- 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;
1593
- return {
1594
- ...input,
1595
- latitude: typeof _latitude === "number" && _latitude >= -90 && _latitude <= 90 ? _latitude : (_latitude !== undefined ? (Number.isFinite(Number(_latitude)) ? Number(_latitude) : 0) : 0),
1596
- longitude: typeof _longitude === "number" && _longitude >= -180 && _longitude <= 180 ? _longitude : (_longitude !== undefined ? (Number.isFinite(Number(_longitude)) ? Number(_longitude) : 0) : 0),
1597
- ...(_altitude !== undefined && { altitude: typeof _altitude === "number" ? _altitude : (Number.isFinite(Number(_altitude)) ? Number(_altitude) : 0) }),
1598
- ...(_label !== undefined && { label: typeof _label === "string" ? _label : String(_label) }),
1599
- } as unknown as GeoCoordinate;
1600
- }`,
1601
- )
1602
- })
1603
-
1604
- describe('logWarnings option', () => {
1605
- it('emits a console.warn loop for unknown properties when logWarnings is true', () => {
1606
- const schema: JSONSchema = {
1607
- type: 'object',
1608
- properties: {
1609
- name: { type: 'string' },
1610
- age: { type: 'number' },
1611
- },
1612
- required: ['name'],
1613
- }
1614
-
1615
- const result = generateParserFunction(schema, 'User', { logWarnings: true })
1616
- expect(result).toBe(
1617
- `export const parseUser = (input: unknown): User => {
1618
- if (!isObject(input)) return {
1619
- name: "",
1620
- };
1621
- const _name = input.name;
1622
- const _age = input.age;
1623
- const _knownKeys = new Set(["name", "age"]);
1624
- for (const _k in input) {
1625
- if (!_knownKeys.has(_k)) {
1626
- console.warn(\`[User] Unknown property "\${_k}"\`);
1627
- }
1628
- }
1629
- if (typeof _name === "string" && (_age === undefined || typeof _age === "number")) return { ...input } as User;
1630
- return {
1631
- ...input,
1632
- name: typeof _name === "string" ? _name : (_name !== undefined ? String(_name) : ""),
1633
- ...(_age !== undefined && { age: typeof _age === "number" ? _age : (Number.isFinite(Number(_age)) ? Number(_age) : 0) }),
1634
- } as unknown as User;
1635
- }`,
1636
- )
1637
- })
1638
-
1639
- it('does not emit a console.warn loop when logWarnings is false', () => {
1640
- const schema: JSONSchema = {
1641
- type: 'object',
1642
- properties: {
1643
- name: { type: 'string' },
1644
- },
1645
- }
1646
-
1647
- const result = generateParserFunction(schema, 'User', { logWarnings: false })
1648
- expect(result).not.toContain('console.warn')
1649
- expect(result).not.toContain('_knownKeys')
1650
- })
1651
-
1652
- it('does not emit a console.warn loop when logWarnings is not set', () => {
1653
- const schema: JSONSchema = {
1654
- type: 'object',
1655
- properties: {
1656
- name: { type: 'string' },
1657
- },
1658
- }
1659
-
1660
- const result = generateParserFunction(schema, 'User')
1661
- expect(result).not.toContain('console.warn')
1662
- })
1663
- })
1664
-
1665
- describe('strict option', () => {
1666
- it('throws on non-object input for object schemas', () => {
1667
- const schema: JSONSchema = {
1668
- type: 'object',
1669
- properties: { name: { type: 'string' } },
1670
- required: ['name'],
1671
- }
1672
- const result = generateParserFunction(schema, 'User', { strict: true })
1673
- expect(result).toContain(
1674
- 'if (!isObject(input)) throw new Error(`[User] expected object, got ${input === null ? "null" : typeof input}`)',
1675
- )
1676
- expect(result).not.toContain('if (!isObject(input)) return')
1677
- })
1678
-
1679
- it('throws on missing required property', () => {
1680
- const schema: JSONSchema = {
1681
- type: 'object',
1682
- properties: { name: { type: 'string' } },
1683
- required: ['name'],
1684
- }
1685
- const result = generateParserFunction(schema, 'User', { strict: true })
1686
- expect(result).toContain('if (!("name" in input)) throw new Error(\'[User] missing required property "name"\')')
1687
- })
1688
-
1689
- it('throws on wrong primitive type for required property', () => {
1690
- const schema: JSONSchema = {
1691
- type: 'object',
1692
- properties: { name: { type: 'string' } },
1693
- required: ['name'],
1694
- }
1695
- const result = generateParserFunction(schema, 'User', { strict: true })
1696
- expect(result).toContain(
1697
- 'if (typeof input.name !== "string") throw new Error(`[User] field "name" expected string, got ${typeof input.name}`)',
1698
- )
1699
- })
1700
-
1701
- it('only throws on wrong type for optional property when provided', () => {
1702
- const schema: JSONSchema = {
1703
- type: 'object',
1704
- properties: { age: { type: 'number' } },
1705
- }
1706
- const result = generateParserFunction(schema, 'User', { strict: true })
1707
- expect(result).toContain(
1708
- 'if (input.age !== undefined && (typeof input.age !== "number")) throw new Error(`[User] field "age" expected number, got ${typeof input.age}`)',
1709
- )
1710
- })
1711
-
1712
- it('throws on enum mismatch', () => {
1713
- const schema: JSONSchema = {
1714
- type: 'object',
1715
- properties: { role: { enum: ['admin', 'user'] } },
1716
- required: ['role'],
1717
- }
1718
- const result = generateParserFunction(schema, 'User', { strict: true })
1719
- expect(result).toContain('must be one of: "admin", "user"')
1720
- })
1721
-
1722
- it('throws on pattern mismatch for strings', () => {
1723
- const schema: JSONSchema = {
1724
- type: 'object',
1725
- properties: { code: { type: 'string', pattern: '^[A-Z]+$' } },
1726
- required: ['code'],
1727
- }
1728
- const result = generateParserFunction(schema, 'User', { strict: true })
1729
- expect(result).toContain('must match pattern ^[A-Z]+$')
1730
- })
1731
-
1732
- it('throws on minLength / maxLength violations', () => {
1733
- const schema: JSONSchema = {
1734
- type: 'object',
1735
- properties: { name: { type: 'string', minLength: 1, maxLength: 10 } },
1736
- required: ['name'],
1737
- }
1738
- const result = generateParserFunction(schema, 'User', { strict: true })
1739
- expect(result).toContain('must have at least 1 characters')
1740
- expect(result).toContain('must have at most 10 characters')
1741
- })
1742
-
1743
- it('throws on minimum / maximum / multipleOf violations', () => {
1744
- const schema: JSONSchema = {
1745
- type: 'object',
1746
- properties: { age: { type: 'number', minimum: 0, maximum: 120, multipleOf: 1 } },
1747
- required: ['age'],
1748
- }
1749
- const result = generateParserFunction(schema, 'User', { strict: true })
1750
- expect(result).toContain('must be >= 0')
1751
- expect(result).toContain('must be <= 120')
1752
- expect(result).toContain('must be a multiple of 1')
1753
- })
1754
-
1755
- it('does not generate strict assertions for $ref properties (delegated to nested parser)', () => {
1756
- const schema: JSONSchema = {
1757
- type: 'object',
1758
- properties: { contact: { $ref: '#/$defs/contact' } },
1759
- required: ['contact'],
1760
- }
1761
- const result = generateParserFunction(schema, 'User', { strict: true, useRefImports: true })
1762
- // The missing-required check is still emitted, but no inline type check.
1763
- expect(result).toContain('missing required property "contact"')
1764
- expect(result).not.toContain('field "contact" expected')
1765
- })
1766
-
1767
- it('throws on wrong type for non-object scalar schemas', () => {
1768
- const schema: JSONSchema = { type: 'string' }
1769
- const result = generateParserFunction(schema, 'Name', { strict: true })
1770
- expect(result).toContain(
1771
- 'if (typeof input !== "string") throw new Error(`[Name] expected string, got ${input === null ? "null" : typeof input}`)',
1772
- )
1773
- expect(result).toContain('return input as Name;')
1774
- })
1775
-
1776
- it('throws on non-object input for empty object schemas', () => {
1777
- const schema: JSONSchema = { type: 'object' }
1778
- const result = generateParserFunction(schema, 'Any', { strict: true })
1779
- expect(result).toContain('if (!isObject(input)) throw new Error')
1780
- expect(result).toContain('return { ...input } as Any;')
1781
- })
1782
-
1783
- it('preserves existing safe behavior when strict is not set', () => {
1784
- const schema: JSONSchema = {
1785
- type: 'object',
1786
- properties: { name: { type: 'string' } },
1787
- required: ['name'],
1788
- }
1789
- const result = generateParserFunction(schema, 'User')
1790
- expect(result).not.toContain('throw new Error')
1791
- expect(result).toContain('if (!isObject(input)) return')
1792
- })
1793
- })
1794
- })