@amritk/generate-parsers 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/generators/build-schema.d.ts +0 -1
- package/dist/generators/generate-default-from-pattern.d.ts +0 -1
- package/dist/generators/generate-discriminated-union-validation.d.ts +0 -1
- package/dist/generators/generate-enum-check.d.ts +0 -1
- package/dist/generators/generate-files.d.ts +0 -1
- package/dist/generators/generate-non-discriminated-union-validation.d.ts +0 -1
- package/dist/generators/generate-parser-function.d.ts +3 -3
- package/dist/generators/generate-parser-function.js +164 -29
- package/dist/generators/generate-schema-checks.d.ts +0 -1
- package/dist/generators/generate-strict-assertion.d.ts +0 -1
- package/dist/generators/generate-validation-expression.d.ts +0 -1
- package/dist/helpers/apply-schema-extensions.d.ts +0 -1
- package/dist/helpers/collect-helpers.d.ts +0 -1
- package/dist/helpers/collect-imports.d.ts +0 -1
- package/dist/helpers/find-discriminator.d.ts +0 -1
- package/dist/helpers/get-default-value.d.ts +0 -1
- package/dist/helpers/get-discriminator-value.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/types/schema-extensions.d.ts +0 -1
- package/package.json +3 -5
- package/dist/generators/build-schema.d.ts.map +0 -1
- package/dist/generators/generate-default-from-pattern.d.ts.map +0 -1
- package/dist/generators/generate-discriminated-union-validation.d.ts.map +0 -1
- package/dist/generators/generate-enum-check.d.ts.map +0 -1
- package/dist/generators/generate-files.d.ts.map +0 -1
- package/dist/generators/generate-non-discriminated-union-validation.d.ts.map +0 -1
- package/dist/generators/generate-parser-function.d.ts.map +0 -1
- package/dist/generators/generate-schema-checks.d.ts.map +0 -1
- package/dist/generators/generate-strict-assertion.d.ts.map +0 -1
- package/dist/generators/generate-validation-expression.d.ts.map +0 -1
- package/dist/helpers/apply-schema-extensions.d.ts.map +0 -1
- package/dist/helpers/collect-helpers.d.ts.map +0 -1
- package/dist/helpers/collect-imports.d.ts.map +0 -1
- package/dist/helpers/find-discriminator.d.ts.map +0 -1
- package/dist/helpers/get-default-value.d.ts.map +0 -1
- package/dist/helpers/get-discriminator-value.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/types/schema-extensions.d.ts.map +0 -1
- package/src/generators/build-schema.test.ts +0 -854
- package/src/generators/build-schema.ts +0 -147
- package/src/generators/generate-default-from-pattern.test.ts +0 -81
- package/src/generators/generate-default-from-pattern.ts +0 -39
- package/src/generators/generate-discriminated-union-validation.test.ts +0 -84
- package/src/generators/generate-discriminated-union-validation.ts +0 -54
- package/src/generators/generate-enum-check.test.ts +0 -45
- package/src/generators/generate-enum-check.ts +0 -8
- package/src/generators/generate-files.test.ts +0 -448
- package/src/generators/generate-files.ts +0 -155
- package/src/generators/generate-non-discriminated-union-validation.test.ts +0 -74
- package/src/generators/generate-non-discriminated-union-validation.ts +0 -40
- package/src/generators/generate-parser-function.test.ts +0 -1794
- package/src/generators/generate-parser-function.ts +0 -1340
- package/src/generators/generate-schema-checks.test.ts +0 -330
- package/src/generators/generate-schema-checks.ts +0 -128
- package/src/generators/generate-strict-assertion.test.ts +0 -67
- package/src/generators/generate-strict-assertion.ts +0 -243
- package/src/generators/generate-validation-expression.test.ts +0 -831
- package/src/generators/generate-validation-expression.ts +0 -327
- package/src/helpers/apply-schema-extensions.test.ts +0 -194
- package/src/helpers/apply-schema-extensions.ts +0 -63
- package/src/helpers/collect-helpers.test.ts +0 -115
- package/src/helpers/collect-helpers.ts +0 -79
- package/src/helpers/collect-imports.test.ts +0 -547
- package/src/helpers/collect-imports.ts +0 -275
- package/src/helpers/find-discriminator.test.ts +0 -88
- package/src/helpers/find-discriminator.ts +0 -39
- package/src/helpers/get-default-value.test.ts +0 -139
- package/src/helpers/get-default-value.ts +0 -95
- package/src/helpers/get-discriminator-value.test.ts +0 -83
- package/src/helpers/get-discriminator-value.ts +0 -21
- package/src/index.ts +0 -1
- package/src/types/schema-extensions.ts +0 -35
|
@@ -1,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
|
-
})
|