@amritk/generate-parsers 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +164 -29
  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,854 +0,0 @@
1
- import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
2
- import { describe, expect, it, vi } from 'vitest'
3
-
4
- import { buildSchema } from './build-schema'
5
-
6
- describe('build-schema', () => {
7
- it('generates schema.ts from #/$defs/schema like any other ref', async () => {
8
- const schema: JSONSchema = {
9
- type: 'object',
10
- properties: {
11
- schema: { $ref: '#/$defs/schema' },
12
- contact: { $ref: '#/$defs/contact' },
13
- },
14
- $defs: {
15
- schema: {
16
- type: 'object',
17
- properties: {
18
- type: { type: 'string' },
19
- },
20
- },
21
- contact: {
22
- type: 'object',
23
- properties: {
24
- name: { type: 'string' },
25
- },
26
- },
27
- },
28
- }
29
-
30
- const result = await buildSchema(schema, 'Document')
31
- const filenames = result.map((file) => file.filename)
32
-
33
- expect(filenames).toContain('document.ts')
34
- expect(filenames).toContain('contact.ts')
35
- expect(filenames).toContain('schema.ts')
36
- const schemaFile = result.find((file) => file.filename === 'schema.ts')
37
- // The generated schema.ts should declare Schema and parseSchema from the user's $defs/schema
38
- expect(schemaFile?.content).toContain('export type Schema')
39
- expect(schemaFile?.content).toContain('parseSchema')
40
- })
41
-
42
- it('applies extensions to matching definitions during build', async () => {
43
- const schema: JSONSchema = {
44
- type: 'object',
45
- properties: {
46
- parameter: { $ref: '#/$defs/parameter' },
47
- },
48
- $defs: {
49
- parameter: {
50
- type: 'object',
51
- properties: {
52
- name: { type: 'string' },
53
- },
54
- required: ['name'],
55
- },
56
- },
57
- }
58
-
59
- const result = await buildSchema(schema, 'Document', {
60
- parameter: {
61
- 'x-enabled': { type: 'boolean' },
62
- },
63
- })
64
-
65
- const parameterFile = result.find((file) => file.filename === 'parameter.ts')
66
- expect(parameterFile).toBeDefined()
67
- // The generated type should include the x-enabled extension property
68
- expect(parameterFile?.content).toContain("'x-enabled'")
69
- // The generated parser should validate the extension property
70
- expect(parameterFile?.content).toContain('x_enabled')
71
- })
72
-
73
- it('does not affect definitions without matching extensions', async () => {
74
- const schema: JSONSchema = {
75
- type: 'object',
76
- properties: {
77
- parameter: { $ref: '#/$defs/parameter' },
78
- info: { $ref: '#/$defs/info' },
79
- },
80
- $defs: {
81
- parameter: {
82
- type: 'object',
83
- properties: {
84
- name: { type: 'string' },
85
- },
86
- },
87
- info: {
88
- type: 'object',
89
- properties: {
90
- title: { type: 'string' },
91
- },
92
- },
93
- },
94
- }
95
-
96
- const result = await buildSchema(schema, 'Document', {
97
- parameter: {
98
- 'x-enabled': { type: 'boolean' },
99
- },
100
- })
101
-
102
- const infoFile = result.find((file) => file.filename === 'info.ts')
103
- expect(infoFile).toBeDefined()
104
- // The info file should not contain the extension
105
- expect(infoFile?.content).not.toContain('x-enabled')
106
- })
107
-
108
- it('applies extensions to the root schema', async () => {
109
- const schema: JSONSchema = {
110
- type: 'object',
111
- properties: {
112
- openapi: { type: 'string' },
113
- },
114
- required: ['openapi'],
115
- }
116
-
117
- const result = await buildSchema(schema, 'Document', {
118
- document: {
119
- 'x-generator': { type: 'string' },
120
- },
121
- })
122
-
123
- const documentFile = result.find((file) => file.filename === 'document.ts')
124
- expect(documentFile).toBeDefined()
125
- expect(documentFile?.content).toContain("'x-generator'")
126
- })
127
-
128
- it('works without extensions parameter', async () => {
129
- const schema: JSONSchema = {
130
- type: 'object',
131
- properties: {
132
- name: { type: 'string' },
133
- },
134
- }
135
-
136
- const result = await buildSchema(schema, 'Document')
137
- const filenames = result.map((file) => file.filename)
138
- expect(filenames).toContain('document.ts')
139
- // No schema.ts unless the input defines #/$defs/schema
140
- expect(filenames).not.toContain('schema.ts')
141
- })
142
-
143
- it('only generates type files in types-only mode', async () => {
144
- const schema: JSONSchema = {
145
- type: 'object',
146
- properties: {
147
- name: { type: 'string' },
148
- },
149
- }
150
-
151
- const result = await buildSchema(schema, 'Document', undefined, true)
152
-
153
- // document.ts plus index.ts — no schema.ts since input has no #/$defs/schema
154
- expect(result).toHaveLength(2)
155
- const filenames = result.map((f) => f.filename)
156
- expect(filenames).toContain('document.ts')
157
- expect(filenames).toContain('index.ts')
158
- })
159
-
160
- it('does not include parser functions in types-only mode', async () => {
161
- const schema: JSONSchema = {
162
- type: 'object',
163
- properties: {
164
- name: { type: 'string' },
165
- age: { type: 'number' },
166
- },
167
- required: ['name'],
168
- }
169
-
170
- const result = await buildSchema(schema, 'Document', undefined, true)
171
- const documentFile = result.find((file) => file.filename === 'document.ts')
172
-
173
- expect(documentFile?.content).toContain('export type Document')
174
- expect(documentFile?.content).not.toContain('export const parseDocument')
175
- })
176
-
177
- it('uses type-only imports for $ref properties in types-only mode', async () => {
178
- const schema: JSONSchema = {
179
- type: 'object',
180
- properties: {
181
- contact: { $ref: '#/$defs/contact' },
182
- },
183
- $defs: {
184
- contact: {
185
- type: 'object',
186
- properties: {
187
- email: { type: 'string' },
188
- },
189
- },
190
- },
191
- }
192
-
193
- const result = await buildSchema(schema, 'Document', undefined, true)
194
- const documentFile = result.find((file) => file.filename === 'document.ts')
195
-
196
- expect(documentFile?.content).toContain("import type { Contact } from './contact';")
197
- expect(documentFile?.content).not.toContain('parseContact')
198
- })
199
-
200
- it('still generates files for all $ref definitions in types-only mode', async () => {
201
- const schema: JSONSchema = {
202
- type: 'object',
203
- properties: {
204
- contact: { $ref: '#/$defs/contact' },
205
- server: { $ref: '#/$defs/server' },
206
- },
207
- $defs: {
208
- contact: {
209
- type: 'object',
210
- properties: { email: { type: 'string' } },
211
- },
212
- server: {
213
- type: 'object',
214
- properties: { url: { type: 'string' } },
215
- },
216
- },
217
- }
218
-
219
- const result = await buildSchema(schema, 'Document', undefined, true)
220
- const filenames = result.map((file) => file.filename)
221
-
222
- expect(filenames).toContain('document.ts')
223
- expect(filenames).toContain('contact.ts')
224
- expect(filenames).toContain('server.ts')
225
- expect(filenames).toContain('index.ts')
226
- expect(result).toHaveLength(4)
227
- })
228
-
229
- it('generated ref files in types-only mode also omit parsers', async () => {
230
- const schema: JSONSchema = {
231
- type: 'object',
232
- properties: {
233
- contact: { $ref: '#/$defs/contact' },
234
- },
235
- $defs: {
236
- contact: {
237
- type: 'object',
238
- properties: {
239
- email: { type: 'string' },
240
- },
241
- required: ['email'],
242
- },
243
- },
244
- }
245
-
246
- const result = await buildSchema(schema, 'Document', undefined, true)
247
- const contactFile = result.find((file) => file.filename === 'contact.ts')
248
-
249
- expect(contactFile?.content).toContain('export type Contact')
250
- expect(contactFile?.content).not.toContain('export const parseContact')
251
- })
252
-
253
- it('applies extensions correctly in types-only mode', async () => {
254
- const schema: JSONSchema = {
255
- type: 'object',
256
- properties: {
257
- parameter: { $ref: '#/$defs/parameter' },
258
- },
259
- $defs: {
260
- parameter: {
261
- type: 'object',
262
- properties: {
263
- name: { type: 'string' },
264
- },
265
- required: ['name'],
266
- },
267
- },
268
- }
269
-
270
- const result = await buildSchema(schema, 'Document', { parameter: { 'x-enabled': { type: 'boolean' } } }, true)
271
-
272
- const parameterFile = result.find((file) => file.filename === 'parameter.ts')
273
- // Extension property should still appear in the type
274
- expect(parameterFile?.content).toContain("'x-enabled'")
275
- // But no parser should be generated
276
- expect(parameterFile?.content).not.toContain('parseParameter')
277
- })
278
-
279
- it('warns and skips when a $ref cannot be resolved', async () => {
280
- const schema: JSONSchema = {
281
- type: 'object',
282
- properties: {
283
- // This ref points to a def that does not exist in $defs
284
- ghost: { $ref: '#/$defs/nonexistent' },
285
- },
286
- $defs: {},
287
- }
288
-
289
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
290
-
291
- const result = await buildSchema(schema, 'Document')
292
-
293
- expect(warnSpy).toHaveBeenCalledWith('Warning: Could not resolve ref: #/$defs/nonexistent')
294
- // The root document file should still be generated despite the unresolvable ref
295
- expect(result.some((file) => file.filename === 'document.ts')).toBe(true)
296
-
297
- warnSpy.mockRestore()
298
- })
299
-
300
- it('generates intersection type for schema with allOf $ref', async () => {
301
- const schema: JSONSchema = {
302
- type: 'object',
303
- properties: {
304
- name: { type: 'string' },
305
- },
306
- allOf: [{ $ref: '#/$defs/taggable' }],
307
- $defs: {
308
- taggable: {
309
- properties: {
310
- tags: {
311
- type: 'array',
312
- items: { type: 'string' },
313
- },
314
- },
315
- },
316
- },
317
- }
318
-
319
- const result = await buildSchema(schema, 'Document')
320
- const documentFile = result.find((file) => file.filename === 'document.ts')
321
-
322
- // Own property should be present in the object literal
323
- expect(documentFile?.content).toContain('name?:')
324
- // allOf ref generates an intersection type with the referenced type
325
- expect(documentFile?.content).toContain('Taggable')
326
- // The taggable definition should generate its own file
327
- const filenames = result.map((f) => f.filename)
328
- expect(filenames).toContain('taggable.ts')
329
- })
330
-
331
- it('generates a separate file for allOf $ref definition and imports it via intersection type', async () => {
332
- const schema: JSONSchema = {
333
- type: 'object',
334
- properties: {
335
- title: { type: 'string' },
336
- },
337
- allOf: [{ $ref: '#/$defs/examples' }],
338
- $defs: {
339
- examples: {
340
- properties: {
341
- example: true,
342
- examples: {
343
- type: 'object',
344
- additionalProperties: { $ref: '#/$defs/example-item' },
345
- },
346
- },
347
- },
348
- 'example-item': {
349
- type: 'object',
350
- properties: { value: { type: 'string' } },
351
- },
352
- },
353
- }
354
-
355
- const result = await buildSchema(schema, 'Document')
356
- const documentFile = result.find((file) => file.filename === 'document.ts')
357
- const filenames = result.map((f) => f.filename)
358
-
359
- // Examples IS imported since allOf generates an intersection type
360
- expect(documentFile?.content).toContain('Examples')
361
- // The examples definition generates its own file
362
- expect(filenames).toContain('examples.ts')
363
- // ExampleItem is also resolved from additionalProperties
364
- expect(filenames).toContain('example-item.ts')
365
- })
366
-
367
- it('own properties take precedence over mixin properties with the same key', async () => {
368
- const schema: JSONSchema = {
369
- type: 'object',
370
- properties: {
371
- // Own `tags` property overrides the mixin's `tags`
372
- tags: { type: 'string' },
373
- },
374
- allOf: [{ $ref: '#/$defs/taggable' }],
375
- $defs: {
376
- taggable: {
377
- properties: {
378
- tags: { type: 'array', items: { type: 'string' } },
379
- },
380
- },
381
- },
382
- }
383
-
384
- const result = await buildSchema(schema, 'Document', undefined, true)
385
- const documentFile = result.find((file) => file.filename === 'document.ts')
386
-
387
- // Own `tags: string` should win over mixin `tags: string[]`
388
- expect(documentFile?.content).toContain('tags?: string')
389
- expect(documentFile?.content).not.toContain('tags?: string[]')
390
- })
391
-
392
- it('does not merge allOf refs that have structural keywords (non-mixin)', async () => {
393
- const schema: JSONSchema = {
394
- type: 'object',
395
- properties: {
396
- name: { type: 'string' },
397
- },
398
- allOf: [{ $ref: '#/$defs/conditional' }],
399
- $defs: {
400
- conditional: {
401
- // Has `if`/`then` — not a pure property mixin
402
- if: { properties: { style: { const: 'form' } }, required: ['style'] },
403
- then: { properties: { explode: { default: true } } },
404
- properties: { style: { type: 'string' }, explode: { type: 'boolean' } },
405
- },
406
- },
407
- }
408
-
409
- const result = await buildSchema(schema, 'Document', undefined, true)
410
- const documentFile = result.find((file) => file.filename === 'document.ts')
411
-
412
- // The conditional ref should NOT be merged — Document should only have `name`
413
- expect(documentFile?.content).not.toContain('style?:')
414
- expect(documentFile?.content).not.toContain('explode?:')
415
- })
416
-
417
- it('generates intersection type for allOf $ref including specification-extensions', async () => {
418
- const schema: JSONSchema = {
419
- type: 'object',
420
- properties: {
421
- name: { type: 'string' },
422
- },
423
- allOf: [{ $ref: '#/$defs/specification-extensions' }],
424
- $defs: {
425
- 'specification-extensions': {
426
- properties: {
427
- 'x-custom': { type: 'string' },
428
- },
429
- },
430
- },
431
- }
432
-
433
- const result = await buildSchema(schema, 'Document')
434
- const documentFile = result.find((file) => file.filename === 'document.ts')
435
- const filenames = result.map((f) => f.filename)
436
-
437
- // specification-extensions is now treated like any other allOf ref
438
- expect(documentFile?.content).toContain('SpecificationExtensions')
439
- // The specification-extensions definition generates its own file
440
- expect(filenames).toContain('specification-extensions.ts')
441
- // x-custom property is NOT inlined into document.ts — it's in specification-extensions.ts
442
- expect(documentFile?.content).not.toContain("'x-custom'")
443
- })
444
-
445
- it('generates JSDoc from $comment plain text on nested $defs', async () => {
446
- const schema: JSONSchema = {
447
- type: 'object',
448
- properties: {
449
- flows: { $ref: '#/$defs/oauth-flows' },
450
- },
451
- $defs: {
452
- 'oauth-flows': {
453
- type: 'object',
454
- properties: {
455
- password: { $ref: '#/$defs/oauth-flows/$defs/password' },
456
- },
457
- $defs: {
458
- password: {
459
- $comment: 'Configuration details for a supported OAuth Flow.',
460
- type: 'object',
461
- properties: {
462
- tokenUrl: { type: 'string' },
463
- scopes: { type: 'object' },
464
- },
465
- required: ['tokenUrl', 'scopes'],
466
- },
467
- },
468
- },
469
- },
470
- }
471
-
472
- const result = await buildSchema(schema, 'Document')
473
- const passwordFile = result.find((file) => file.filename === 'password.ts')
474
-
475
- expect(passwordFile).toBeDefined()
476
- // JSDoc should contain the plain-text $comment description
477
- expect(passwordFile?.content).toContain('Configuration details for a supported OAuth Flow.')
478
- })
479
-
480
- it('uses the $comment URL as plain-text JSDoc description', async () => {
481
- const schema: JSONSchema = {
482
- type: 'object',
483
- properties: {
484
- flows: { $ref: '#/$defs/oauth-flows' },
485
- },
486
- $defs: {
487
- 'oauth-flows': {
488
- type: 'object',
489
- properties: {
490
- password: { $ref: '#/$defs/oauth-flows/$defs/password' },
491
- },
492
- $defs: {
493
- password: {
494
- $comment: 'https://example.com#custom-flow-object',
495
- type: 'object',
496
- properties: {
497
- tokenUrl: { type: 'string' },
498
- },
499
- },
500
- },
501
- },
502
- },
503
- }
504
-
505
- const result = await buildSchema(schema, 'Document')
506
- const passwordFile = result.find((file) => file.filename === 'password.ts')
507
-
508
- expect(passwordFile).toBeDefined()
509
- // URL $comment is emitted as the description in the JSDoc block
510
- expect(passwordFile?.content).toContain('https://example.com#custom-flow-object')
511
- })
512
-
513
- it('generates a file for if/then/else conditional refs', async () => {
514
- // With -or-reference stripping removed, a def named 'parameter-or-reference' now maps
515
- // to its own filename 'parameter-or-reference' and gets its own file.
516
- const schema: JSONSchema = {
517
- type: 'object',
518
- properties: {
519
- param: { $ref: '#/$defs/parameter-or-reference' },
520
- },
521
- $defs: {
522
- reference: {
523
- type: 'object',
524
- properties: { $ref: { type: 'string' } },
525
- required: ['$ref'],
526
- },
527
- parameter: {
528
- type: 'object',
529
- properties: {
530
- name: { type: 'string' },
531
- in: { type: 'string' },
532
- },
533
- required: ['name', 'in'],
534
- },
535
- 'parameter-or-reference': {
536
- if: { type: 'object', required: ['$ref'] },
537
- then: { $ref: '#/$defs/reference' },
538
- else: { $ref: '#/$defs/parameter' },
539
- },
540
- },
541
- }
542
-
543
- const result = await buildSchema(schema, 'Document')
544
- const filenames = result.map((f) => f.filename)
545
-
546
- // Both files are generated independently — they map to different filenames now
547
- expect(filenames).toContain('parameter.ts')
548
- expect(filenames).toContain('parameter-or-reference.ts')
549
- // The parameter.ts should contain the real Parameter type
550
- const parameterFile = result.find((f) => f.filename === 'parameter.ts')
551
- expect(parameterFile?.content).toContain('name')
552
- expect(parameterFile?.content).toContain('in')
553
- })
554
-
555
- it('generates an index.ts with named re-exports from all generated files', async () => {
556
- const schema: JSONSchema = {
557
- type: 'object',
558
- properties: {
559
- contact: { $ref: '#/$defs/contact' },
560
- },
561
- $defs: {
562
- contact: {
563
- type: 'object',
564
- properties: { email: { type: 'string' } },
565
- },
566
- },
567
- }
568
-
569
- const result = await buildSchema(schema, 'Document')
570
- const indexFile = result.find((f) => f.filename === 'index.ts')
571
-
572
- expect(indexFile).toBeDefined()
573
- // Named type + parser + shape-validator exports for each file
574
- expect(indexFile?.content).toContain(
575
- "export { type Contact, validateContactShape, parseContact } from './contact';",
576
- )
577
- expect(indexFile?.content).toContain(
578
- "export { type Document, validateDocumentShape, parseDocument } from './document';",
579
- )
580
- // No wildcard exports
581
- expect(indexFile?.content).not.toContain('export *')
582
- expect(indexFile?.content).not.toContain('export type *')
583
- })
584
-
585
- it('generates an index.ts with named type-only re-exports in types-only mode', async () => {
586
- const schema: JSONSchema = {
587
- type: 'object',
588
- properties: {
589
- contact: { $ref: '#/$defs/contact' },
590
- },
591
- $defs: {
592
- contact: {
593
- type: 'object',
594
- properties: { email: { type: 'string' } },
595
- },
596
- },
597
- }
598
-
599
- const result = await buildSchema(schema, 'Document', undefined, true)
600
- const indexFile = result.find((f) => f.filename === 'index.ts')
601
-
602
- expect(indexFile).toBeDefined()
603
- // Types-only: export type { ... } syntax, no parsers
604
- expect(indexFile?.content).toContain("export type { Contact } from './contact';")
605
- expect(indexFile?.content).toContain("export type { Document } from './document';")
606
- expect(indexFile?.content).not.toContain('parseContact')
607
- expect(indexFile?.content).not.toContain('parseDocument')
608
- // No wildcard exports
609
- expect(indexFile?.content).not.toContain('export *')
610
- expect(indexFile?.content).not.toContain('export type *')
611
- })
612
-
613
- it('index.ts entries are sorted alphabetically', async () => {
614
- const schema: JSONSchema = {
615
- type: 'object',
616
- properties: {
617
- z: { $ref: '#/$defs/zebra' },
618
- a: { $ref: '#/$defs/alpha' },
619
- },
620
- $defs: {
621
- zebra: { type: 'object', properties: { name: { type: 'string' } } },
622
- alpha: { type: 'object', properties: { name: { type: 'string' } } },
623
- },
624
- }
625
-
626
- const result = await buildSchema(schema, 'Document')
627
- const indexFile = result.find((f) => f.filename === 'index.ts')
628
-
629
- expect(indexFile).toBeDefined()
630
- const lines = indexFile?.content.trim().split('\n')
631
- const sorted = [...lines].sort()
632
- expect(lines).toEqual(sorted)
633
- })
634
-
635
- it('skips adding a nested ref to the queue when it was already processed', async () => {
636
- // When "common" appears first in the root properties it gets processed before "item".
637
- // Later, when "item" is processed and its nested refs are extracted, "common" is
638
- // already in processedRefs so it should NOT be added to the queue again (line 184 false branch).
639
- const schema: JSONSchema = {
640
- type: 'object',
641
- properties: {
642
- common: { $ref: '#/$defs/common' },
643
- item: { $ref: '#/$defs/item' },
644
- },
645
- $defs: {
646
- common: {
647
- type: 'object',
648
- properties: { name: { type: 'string' } },
649
- },
650
- item: {
651
- type: 'object',
652
- properties: {
653
- // item references common, which will already be processed when item is handled
654
- common: { $ref: '#/$defs/common' },
655
- },
656
- },
657
- },
658
- }
659
-
660
- const result = await buildSchema(schema, 'Document')
661
- const filenames = result.map((file) => file.filename)
662
-
663
- // Both defs should produce exactly one file each (no duplicates from re-processing)
664
- expect(filenames.filter((f) => f === 'common.ts')).toHaveLength(1)
665
- expect(filenames.filter((f) => f === 'item.ts')).toHaveLength(1)
666
- })
667
-
668
- describe('helpersMode: embedded', () => {
669
- it('emits relative ./_helpers/ imports and a _helpers/is-object.ts file for a basic schema', async () => {
670
- const schema: JSONSchema = {
671
- type: 'object',
672
- properties: { name: { type: 'string' } },
673
- }
674
-
675
- const result = await buildSchema(schema, 'Document', undefined, undefined, undefined, undefined, 'embedded')
676
- const filenames = result.map((f) => f.filename)
677
- const root = result.find((f) => f.filename === 'document.ts')
678
-
679
- expect(root?.content).toContain("import { isObject } from './_helpers/is-object';")
680
- expect(root?.content).not.toContain('@amritk/helpers')
681
- expect(filenames).toContain('_helpers/is-object.ts')
682
- })
683
-
684
- it('honours a custom helpersImportPrefix so nested parsers import from a shared root _helpers/', async () => {
685
- const schema: JSONSchema = {
686
- type: 'object',
687
- properties: { name: { type: 'string' } },
688
- }
689
-
690
- const result = await buildSchema(
691
- schema,
692
- 'Document',
693
- undefined,
694
- undefined,
695
- undefined,
696
- undefined,
697
- 'embedded',
698
- '../../',
699
- )
700
- const root = result.find((f) => f.filename === 'document.ts')
701
- const filenames = result.map((f) => f.filename)
702
-
703
- expect(root?.content).toContain("import { isObject } from '../../_helpers/is-object';")
704
- // Helper sources keep their canonical filenames; the orchestrator relocates them to the root.
705
- expect(filenames).toContain('_helpers/is-object.ts')
706
- })
707
-
708
- it('ships validate-array.ts (plus its transitive is-object.ts) for array schemas', async () => {
709
- const schema: JSONSchema = {
710
- type: 'object',
711
- properties: {
712
- tags: { type: 'array', items: { $ref: '#/$defs/tag' } },
713
- },
714
- $defs: {
715
- tag: { type: 'object', properties: { value: { type: 'string' } } },
716
- },
717
- }
718
-
719
- const result = await buildSchema(schema, 'Document', undefined, undefined, undefined, undefined, 'embedded')
720
- const filenames = result.map((f) => f.filename)
721
-
722
- expect(filenames).toContain('_helpers/validate-array.ts')
723
- expect(filenames).toContain('_helpers/is-object.ts')
724
- expect(filenames).not.toContain('_helpers/validate-record.ts')
725
- expect(filenames).not.toContain('_helpers/has-ref.ts')
726
- })
727
-
728
- it('ships validate-record.ts plus is-object.ts when additionalProperties is a $ref', async () => {
729
- const schema: JSONSchema = {
730
- type: 'object',
731
- properties: {
732
- headers: { type: 'object', additionalProperties: { $ref: '#/$defs/header' } },
733
- },
734
- $defs: {
735
- header: { type: 'object', properties: { value: { type: 'string' } } },
736
- },
737
- }
738
-
739
- const result = await buildSchema(schema, 'Document', undefined, undefined, undefined, undefined, 'embedded')
740
- const filenames = result.map((f) => f.filename)
741
-
742
- expect(filenames).toContain('_helpers/validate-record.ts')
743
- expect(filenames).toContain('_helpers/is-object.ts')
744
- })
745
-
746
- it('ships has-ref.ts for schemas using if/then/else with $ref branches', async () => {
747
- // hasRef(input) is emitted into runtime parser bodies only for conditional schemas
748
- // whose `if` requires `$ref` and whose then/else branches are both $refs.
749
- const schema: JSONSchema = {
750
- type: 'object',
751
- properties: {
752
- param: { $ref: '#/$defs/parameter-or-reference' },
753
- },
754
- $defs: {
755
- reference: {
756
- type: 'object',
757
- properties: { $ref: { type: 'string' } },
758
- required: ['$ref'],
759
- },
760
- parameter: {
761
- type: 'object',
762
- properties: { name: { type: 'string' } },
763
- },
764
- 'parameter-or-reference': {
765
- if: { type: 'object', required: ['$ref'] },
766
- then: { $ref: '#/$defs/reference' },
767
- else: { $ref: '#/$defs/parameter' },
768
- },
769
- },
770
- }
771
-
772
- const result = await buildSchema(schema, 'Document', undefined, undefined, undefined, undefined, 'embedded')
773
- const filenames = result.map((f) => f.filename)
774
-
775
- expect(filenames).toContain('_helpers/has-ref.ts')
776
- const conditional = result.find((f) => f.filename === 'parameter-or-reference.ts')
777
- expect(conditional?.content).toContain("import { hasRef } from './_helpers/has-ref';")
778
- })
779
-
780
- it('emits zero _helpers/*.ts files in types-only mode', async () => {
781
- const schema: JSONSchema = {
782
- type: 'object',
783
- properties: {
784
- tags: { type: 'array', items: { type: 'string' } },
785
- },
786
- }
787
-
788
- const result = await buildSchema(schema, 'Document', undefined, true, undefined, undefined, 'embedded')
789
- const filenames = result.map((f) => f.filename)
790
-
791
- expect(filenames.some((f) => f.startsWith('_helpers/'))).toBe(false)
792
- })
793
-
794
- it('does not include _helpers/ entries in the generated index.ts re-exports', async () => {
795
- const schema: JSONSchema = {
796
- type: 'object',
797
- properties: { name: { type: 'string' } },
798
- }
799
-
800
- const result = await buildSchema(schema, 'Document', undefined, undefined, undefined, undefined, 'embedded')
801
- const index = result.find((f) => f.filename === 'index.ts')
802
-
803
- expect(index?.content).not.toContain('_helpers')
804
- expect(index?.content).not.toContain('isObject')
805
- })
806
-
807
- it('emits _helpers/has-ref.ts content with the standalone hasRef (no JSONSchema import)', async () => {
808
- const schema: JSONSchema = {
809
- type: 'object',
810
- properties: {
811
- param: { $ref: '#/$defs/parameter-or-reference' },
812
- },
813
- $defs: {
814
- reference: {
815
- type: 'object',
816
- properties: { $ref: { type: 'string' } },
817
- required: ['$ref'],
818
- },
819
- parameter: { type: 'object' },
820
- 'parameter-or-reference': {
821
- if: { type: 'object', required: ['$ref'] },
822
- then: { $ref: '#/$defs/reference' },
823
- else: { $ref: '#/$defs/parameter' },
824
- },
825
- },
826
- }
827
-
828
- const result = await buildSchema(schema, 'Document', undefined, undefined, undefined, undefined, 'embedded')
829
- const hasRefFile = result.find((f) => f.filename === '_helpers/has-ref.ts')
830
-
831
- expect(hasRefFile?.content).toContain('export const hasRef')
832
- // The embedded helper must not pull in json-schema-typed; that would defeat self-containment.
833
- expect(hasRefFile?.content).not.toContain('json-schema-typed')
834
- })
835
- })
836
-
837
- describe('helpersMode: package (default)', () => {
838
- it('keeps the historical @amritk/helpers/... imports and emits no _helpers/ files', async () => {
839
- const schema: JSONSchema = {
840
- type: 'object',
841
- properties: {
842
- tags: { type: 'array', items: { type: 'string' } },
843
- },
844
- }
845
-
846
- const result = await buildSchema(schema, 'Document')
847
- const filenames = result.map((f) => f.filename)
848
- const root = result.find((f) => f.filename === 'document.ts')
849
-
850
- expect(root?.content).toContain("from '@amritk/helpers/")
851
- expect(filenames.some((f) => f.startsWith('_helpers/'))).toBe(false)
852
- })
853
- })
854
- })