@amritk/generate-parsers 0.7.1 → 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 +165 -37
- package/dist/generators/generate-schema-checks.d.ts +0 -1
- package/dist/generators/generate-schema-checks.js +2 -1
- package/dist/generators/generate-strict-assertion.d.ts +0 -1
- package/dist/generators/generate-strict-assertion.js +1 -6
- package/dist/generators/generate-validation-expression.d.ts +0 -1
- package/dist/generators/generate-validation-expression.js +2 -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 -1348
- package/src/generators/generate-schema-checks.test.ts +0 -330
- package/src/generators/generate-schema-checks.ts +0 -127
- package/src/generators/generate-strict-assertion.test.ts +0 -67
- package/src/generators/generate-strict-assertion.ts +0 -249
- package/src/generators/generate-validation-expression.test.ts +0 -831
- package/src/generators/generate-validation-expression.ts +0 -326
- 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,448 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { generateFile } from './generate-files'
|
|
4
|
-
|
|
5
|
-
describe('generate-files', () => {
|
|
6
|
-
it('generates combined import for $ref property', () => {
|
|
7
|
-
const schema = {
|
|
8
|
-
type: 'object',
|
|
9
|
-
properties: {
|
|
10
|
-
contact: { $ref: '#/$defs/contact' },
|
|
11
|
-
},
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const { content: result } = generateFile(schema, 'Document')
|
|
15
|
-
|
|
16
|
-
expect(result).toContain("import { type Contact, parseContact, validateContactShape } from './contact';")
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('calls imported parser for optional $ref property', () => {
|
|
20
|
-
const schema = {
|
|
21
|
-
type: 'object',
|
|
22
|
-
properties: {
|
|
23
|
-
contact: { $ref: '#/$defs/contact' },
|
|
24
|
-
},
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const { content: result } = generateFile(schema, 'Document')
|
|
28
|
-
|
|
29
|
-
// With local variable caching, optional $ref properties use the cached variable
|
|
30
|
-
expect(result).toContain('_contact !== undefined && { contact: parseContact(_contact) }')
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('calls imported parser for required $ref property', () => {
|
|
34
|
-
const schema = {
|
|
35
|
-
type: 'object',
|
|
36
|
-
properties: {
|
|
37
|
-
contact: { $ref: '#/$defs/contact' },
|
|
38
|
-
},
|
|
39
|
-
required: ['contact'],
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const { content: result } = generateFile(schema, 'Document')
|
|
43
|
-
|
|
44
|
-
// With local variable caching, required $ref properties use the cached variable
|
|
45
|
-
expect(result).toContain('contact: parseContact(_contact),')
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('generates combined imports for multiple $ref properties', () => {
|
|
49
|
-
const schema = {
|
|
50
|
-
type: 'object',
|
|
51
|
-
properties: {
|
|
52
|
-
contact: { $ref: '#/$defs/contact' },
|
|
53
|
-
server: { $ref: '#/$defs/server' },
|
|
54
|
-
},
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const { content: result } = generateFile(schema, 'Document')
|
|
58
|
-
|
|
59
|
-
expect(result).toContain("import { type Contact, parseContact, validateContactShape } from './contact';")
|
|
60
|
-
expect(result).toContain("import { type Server, parseServer, validateServerShape } from './server';")
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('generates correct parser name for multi-word $ref', () => {
|
|
64
|
-
const schema = {
|
|
65
|
-
type: 'object',
|
|
66
|
-
properties: {
|
|
67
|
-
externalDoc: { $ref: '#/$defs/external-documentation' },
|
|
68
|
-
},
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const { content: result } = generateFile(schema, 'Document')
|
|
72
|
-
|
|
73
|
-
expect(result).toContain(
|
|
74
|
-
"import { type ExternalDocumentation, parseExternalDocumentation, validateExternalDocumentationShape } from './external-documentation';",
|
|
75
|
-
)
|
|
76
|
-
expect(result).toContain('parseExternalDocumentation(_externalDoc)')
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('does not generate ref imports when no $ref properties exist', () => {
|
|
80
|
-
const schema = {
|
|
81
|
-
type: 'object',
|
|
82
|
-
properties: {
|
|
83
|
-
name: { type: 'string' },
|
|
84
|
-
age: { type: 'number' },
|
|
85
|
-
},
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const { content: result } = generateFile(schema, 'Simple')
|
|
89
|
-
|
|
90
|
-
// Should still import isObject helper
|
|
91
|
-
expect(result).toContain("import { isObject } from '@amritk/helpers/is-object';")
|
|
92
|
-
// But should not import any ref types (check for type imports specifically)
|
|
93
|
-
expect(result).not.toContain('import { type')
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('still validates non-ref properties inline', () => {
|
|
97
|
-
const schema = {
|
|
98
|
-
type: 'object',
|
|
99
|
-
properties: {
|
|
100
|
-
name: { type: 'string' },
|
|
101
|
-
contact: { $ref: '#/$defs/contact' },
|
|
102
|
-
},
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const { content: result } = generateFile(schema, 'Document')
|
|
106
|
-
|
|
107
|
-
// Non-ref property should have inline validation against the cached local
|
|
108
|
-
expect(result).toContain('typeof _name === "string"')
|
|
109
|
-
// Ref property should call imported parser using cached variable
|
|
110
|
-
expect(result).toContain('parseContact(_contact)')
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('generates a correct import for -or-reference style $ref names', () => {
|
|
114
|
-
// Without stripping, 'callbacks-or-reference' maps to the filename 'callbacks-or-reference'
|
|
115
|
-
const schema = {
|
|
116
|
-
type: 'object',
|
|
117
|
-
properties: {
|
|
118
|
-
callbacks: { $ref: '#/$defs/callbacks-or-reference' },
|
|
119
|
-
},
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const { content: result } = generateFile(schema, 'Document')
|
|
123
|
-
|
|
124
|
-
expect(result).toContain(
|
|
125
|
-
"import { type CallbacksOrReference, parseCallbacksOrReference, validateCallbacksOrReferenceShape } from './callbacks-or-reference';",
|
|
126
|
-
)
|
|
127
|
-
expect(result).toContain('parseCallbacksOrReference(_callbacks)')
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('generates arrow function parser', () => {
|
|
131
|
-
const schema = {
|
|
132
|
-
type: 'object',
|
|
133
|
-
properties: {
|
|
134
|
-
name: { type: 'string' },
|
|
135
|
-
},
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const { content: result } = generateFile(schema, 'Simple')
|
|
139
|
-
|
|
140
|
-
expect(result).toContain('export const parseSimple = (input: unknown): Simple =>')
|
|
141
|
-
expect(result).not.toContain('function parseSimple')
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('maps array items through imported parser for optional array with $ref items', () => {
|
|
145
|
-
const schema = {
|
|
146
|
-
type: 'object',
|
|
147
|
-
properties: {
|
|
148
|
-
servers: {
|
|
149
|
-
type: 'array',
|
|
150
|
-
items: { $ref: '#/$defs/server' },
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const { content: result } = generateFile(schema, 'Document')
|
|
156
|
-
|
|
157
|
-
expect(result).toContain("import { type Server, parseServer, validateServerShape } from './server';")
|
|
158
|
-
// With local variable caching, array validators use the cached variable
|
|
159
|
-
expect(result).toContain('validateArray(_servers, parseServer)')
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('maps array items through imported parser for required array with $ref items', () => {
|
|
163
|
-
const schema = {
|
|
164
|
-
type: 'object',
|
|
165
|
-
properties: {
|
|
166
|
-
servers: {
|
|
167
|
-
type: 'array',
|
|
168
|
-
items: { $ref: '#/$defs/server' },
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
required: ['servers'],
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const { content: result } = generateFile(schema, 'Document')
|
|
175
|
-
|
|
176
|
-
// Check for required imports
|
|
177
|
-
expect(result).toContain("import { type Server, parseServer, validateServerShape } from './server';")
|
|
178
|
-
expect(result).toContain("import { validateArray } from '@amritk/helpers/validate-array';")
|
|
179
|
-
expect(result).toContain("import { isObject } from '@amritk/helpers/is-object';")
|
|
180
|
-
|
|
181
|
-
// Check for required array validation using cached variable
|
|
182
|
-
expect(result).toContain('servers: validateArray(_servers, parseServer),')
|
|
183
|
-
|
|
184
|
-
// Should not use spread operator for required fields
|
|
185
|
-
expect(result).not.toContain('...(_servers')
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it('generates type and parser for schema with extension properties', () => {
|
|
189
|
-
const schema = {
|
|
190
|
-
type: 'object',
|
|
191
|
-
properties: {
|
|
192
|
-
name: { type: 'string' },
|
|
193
|
-
'x-enabled': { type: 'boolean' },
|
|
194
|
-
'x-internal': { type: 'boolean' },
|
|
195
|
-
},
|
|
196
|
-
required: ['name'],
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const { content: result } = generateFile(schema, 'Parameter')
|
|
200
|
-
|
|
201
|
-
// Type should include extension properties as optional
|
|
202
|
-
expect(result).toContain("'x-enabled'?: boolean")
|
|
203
|
-
expect(result).toContain("'x-internal'?: boolean")
|
|
204
|
-
// Parser should validate extension properties
|
|
205
|
-
expect(result).toContain('x_enabled')
|
|
206
|
-
expect(result).toContain('x_internal')
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
it('generates type and parser for schema with complex extension property', () => {
|
|
210
|
-
const schema = {
|
|
211
|
-
type: 'object',
|
|
212
|
-
properties: {
|
|
213
|
-
summary: { type: 'string' },
|
|
214
|
-
'x-codegen': {
|
|
215
|
-
type: 'object',
|
|
216
|
-
properties: {
|
|
217
|
-
methodName: { type: 'string' },
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const { content: result } = generateFile(schema, 'Operation')
|
|
224
|
-
|
|
225
|
-
// Type should include the complex extension
|
|
226
|
-
expect(result).toContain("'x-codegen'")
|
|
227
|
-
expect(result).toContain('methodName')
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
it('omits parser function in types-only mode', () => {
|
|
231
|
-
const schema = {
|
|
232
|
-
type: 'object',
|
|
233
|
-
properties: {
|
|
234
|
-
name: { type: 'string' },
|
|
235
|
-
},
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const { content: result } = generateFile(schema, 'Simple', { typesOnly: true })
|
|
239
|
-
|
|
240
|
-
expect(result).toContain('export type Simple')
|
|
241
|
-
expect(result).not.toContain('export const parseSimple')
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
it('uses type-only imports for $ref properties in types-only mode', () => {
|
|
245
|
-
const schema = {
|
|
246
|
-
type: 'object',
|
|
247
|
-
properties: {
|
|
248
|
-
contact: { $ref: '#/$defs/contact' },
|
|
249
|
-
},
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const { content: result } = generateFile(schema, 'Document', { typesOnly: true })
|
|
253
|
-
|
|
254
|
-
expect(result).toContain("import type { Contact } from './contact';")
|
|
255
|
-
expect(result).not.toContain('parseContact')
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
it('does not include runtime helper imports in types-only mode', () => {
|
|
259
|
-
const schema = {
|
|
260
|
-
type: 'object',
|
|
261
|
-
properties: {
|
|
262
|
-
name: { type: 'string' },
|
|
263
|
-
},
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const { content: result } = generateFile(schema, 'Simple', { typesOnly: true })
|
|
267
|
-
|
|
268
|
-
// No parser helpers should be imported since there is no parser function
|
|
269
|
-
expect(result).not.toContain('import { isObject }')
|
|
270
|
-
expect(result).not.toContain('import { validateArray }')
|
|
271
|
-
expect(result).not.toContain('import { validateRecord }')
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
it('still generates the type definition in types-only mode', () => {
|
|
275
|
-
const schema = {
|
|
276
|
-
type: 'object',
|
|
277
|
-
properties: {
|
|
278
|
-
name: { type: 'string' },
|
|
279
|
-
age: { type: 'number' },
|
|
280
|
-
},
|
|
281
|
-
required: ['name'],
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const { content: result } = generateFile(schema, 'User', { typesOnly: true })
|
|
285
|
-
|
|
286
|
-
expect(result).toContain('export type User')
|
|
287
|
-
expect(result).toContain('name: string')
|
|
288
|
-
expect(result).toContain('age?: number')
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
it('generates type-only import for array items $ref in types-only mode', () => {
|
|
292
|
-
const schema = {
|
|
293
|
-
type: 'object',
|
|
294
|
-
properties: {
|
|
295
|
-
servers: {
|
|
296
|
-
type: 'array',
|
|
297
|
-
items: { $ref: '#/$defs/server' },
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const { content: result } = generateFile(schema, 'Document', { typesOnly: true })
|
|
303
|
-
|
|
304
|
-
expect(result).toContain("import type { Server } from './server';")
|
|
305
|
-
expect(result).not.toContain('parseServer')
|
|
306
|
-
expect(result).not.toContain('validateArray')
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
it('generates type-only import for additionalProperties $ref in types-only mode', () => {
|
|
310
|
-
const schema = {
|
|
311
|
-
type: 'object',
|
|
312
|
-
additionalProperties: { $ref: '#/$defs/path-item' },
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const { content: result } = generateFile(schema, 'Paths', { typesOnly: true })
|
|
316
|
-
|
|
317
|
-
expect(result).toContain("import type { PathItem } from './path-item';")
|
|
318
|
-
expect(result).not.toContain('parsePathItem')
|
|
319
|
-
expect(result).not.toContain('validateRecord')
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
it('generates type-only imports for oneOf $ref variants in types-only mode', () => {
|
|
323
|
-
const schema = {
|
|
324
|
-
oneOf: [{ $ref: '#/$defs/cat' }, { $ref: '#/$defs/dog' }],
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const { content: result } = generateFile(schema, 'Pet', { typesOnly: true })
|
|
328
|
-
|
|
329
|
-
expect(result).toContain("import type { Cat } from './cat';")
|
|
330
|
-
expect(result).toContain("import type { Dog } from './dog';")
|
|
331
|
-
expect(result).not.toContain('parseCat')
|
|
332
|
-
expect(result).not.toContain('parseDog')
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
it('generates type-only imports for anyOf $ref variants in types-only mode', () => {
|
|
336
|
-
const schema = {
|
|
337
|
-
type: 'object',
|
|
338
|
-
properties: {
|
|
339
|
-
value: {
|
|
340
|
-
anyOf: [{ $ref: '#/$defs/string-value' }, { $ref: '#/$defs/number-value' }],
|
|
341
|
-
},
|
|
342
|
-
},
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const { content: result } = generateFile(schema, 'Wrapper', { typesOnly: true })
|
|
346
|
-
|
|
347
|
-
expect(result).toContain("import type { StringValue } from './string-value';")
|
|
348
|
-
expect(result).toContain("import type { NumberValue } from './number-value';")
|
|
349
|
-
expect(result).not.toContain('parseStringValue')
|
|
350
|
-
expect(result).not.toContain('parseNumberValue')
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
it('does not change default behavior when typesOnly is false', () => {
|
|
354
|
-
const schema = {
|
|
355
|
-
type: 'object',
|
|
356
|
-
properties: {
|
|
357
|
-
contact: { $ref: '#/$defs/contact' },
|
|
358
|
-
},
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const { content: defaultResult } = generateFile(schema, 'Document')
|
|
362
|
-
const { content: explicitFalseResult } = generateFile(schema, 'Document', { typesOnly: false })
|
|
363
|
-
|
|
364
|
-
expect(defaultResult).toBe(explicitFalseResult)
|
|
365
|
-
expect(defaultResult).toContain("import { type Contact, parseContact, validateContactShape } from './contact';")
|
|
366
|
-
expect(defaultResult).toContain('export const parseDocument')
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
it('does not generate a self-import for recursive schemas in types-only mode', () => {
|
|
370
|
-
// Mirrors the 3.2.0 encoding schema: Encoding has properties itemEncoding, prefixEncoding,
|
|
371
|
-
// and encoding that all $ref back to #/$defs/encoding. The generated encoding.ts must not
|
|
372
|
-
// import Encoding from itself.
|
|
373
|
-
const schema = {
|
|
374
|
-
type: 'object',
|
|
375
|
-
properties: {
|
|
376
|
-
itemEncoding: { $ref: '#/$defs/encoding' },
|
|
377
|
-
prefixEncoding: {
|
|
378
|
-
type: 'array',
|
|
379
|
-
items: { $ref: '#/$defs/encoding' },
|
|
380
|
-
},
|
|
381
|
-
encoding: {
|
|
382
|
-
type: 'object',
|
|
383
|
-
additionalProperties: { $ref: '#/$defs/encoding' },
|
|
384
|
-
},
|
|
385
|
-
headers: {
|
|
386
|
-
type: 'object',
|
|
387
|
-
additionalProperties: { $ref: '#/$defs/header-or-reference' },
|
|
388
|
-
},
|
|
389
|
-
},
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const { content: result } = generateFile(schema, 'Encoding', {
|
|
393
|
-
typesOnly: true,
|
|
394
|
-
selfRef: '#/$defs/encoding',
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
expect(result).not.toContain("import type { Encoding } from './encoding';")
|
|
398
|
-
expect(result).toContain("import type { HeaderOrReference } from './header-or-reference';")
|
|
399
|
-
expect(result).toContain('export type Encoding')
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
it('does not generate a self-import for recursive schemas in full parser mode', () => {
|
|
403
|
-
const schema = {
|
|
404
|
-
type: 'object',
|
|
405
|
-
properties: {
|
|
406
|
-
itemEncoding: { $ref: '#/$defs/encoding' },
|
|
407
|
-
headers: {
|
|
408
|
-
type: 'object',
|
|
409
|
-
additionalProperties: { $ref: '#/$defs/header-or-reference' },
|
|
410
|
-
},
|
|
411
|
-
},
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const { content: result } = generateFile(schema, 'Encoding', { selfRef: '#/$defs/encoding' })
|
|
415
|
-
|
|
416
|
-
expect(result).not.toContain("import { type Encoding, parseEncoding, validateEncodingShape } from './encoding';")
|
|
417
|
-
expect(result).toContain(
|
|
418
|
-
"import { type HeaderOrReference, parseHeaderOrReference, validateHeaderOrReferenceShape } from './header-or-reference';",
|
|
419
|
-
)
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
describe('typeSuffix', () => {
|
|
423
|
-
const schema = {
|
|
424
|
-
type: 'object',
|
|
425
|
-
properties: {
|
|
426
|
-
contact: { $ref: '#/$defs/contact' },
|
|
427
|
-
},
|
|
428
|
-
required: ['contact'],
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
it('appends the suffix to ref-derived type, parser, and import names', () => {
|
|
432
|
-
const { content: result } = generateFile(schema, 'Document', { typeSuffix: 'Object' })
|
|
433
|
-
|
|
434
|
-
expect(result).toContain(
|
|
435
|
-
"import { type ContactObject, parseContactObject, validateContactObjectShape } from './contact';",
|
|
436
|
-
)
|
|
437
|
-
expect(result).toContain('contact: ContactObject;')
|
|
438
|
-
expect(result).toContain('contact: parseContactObject(_contact)')
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
it('emits no suffix by default', () => {
|
|
442
|
-
const { content: result } = generateFile(schema, 'Document')
|
|
443
|
-
|
|
444
|
-
expect(result).toContain("import { type Contact, parseContact, validateContactShape } from './contact';")
|
|
445
|
-
expect(result).not.toContain('ContactObject')
|
|
446
|
-
})
|
|
447
|
-
})
|
|
448
|
-
})
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import { generateTypeDefinition } from '@amritk/helpers/generate-type-definition'
|
|
2
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
3
|
-
import {
|
|
4
|
-
type CollectedHelpers,
|
|
5
|
-
collectHelpers,
|
|
6
|
-
type HelpersMode,
|
|
7
|
-
type RuntimeHelperName,
|
|
8
|
-
} from '#helpers/collect-helpers'
|
|
9
|
-
import { collectImports } from '#helpers/collect-imports'
|
|
10
|
-
|
|
11
|
-
import { generateParserFunction, generateShapeValidator } from './generate-parser-function'
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Options for controlling what gets generated in a file.
|
|
15
|
-
*/
|
|
16
|
-
type GenerateFileOptions = {
|
|
17
|
-
/**
|
|
18
|
-
* When true, only emit the TypeScript type definition and skip the parser function.
|
|
19
|
-
* Imports will be type-only as well, since there are no parsers to call.
|
|
20
|
-
*/
|
|
21
|
-
readonly typesOnly?: boolean
|
|
22
|
-
/**
|
|
23
|
-
* The $ref path of the schema being generated (e.g. `#/$defs/encoding`).
|
|
24
|
-
* When provided, any $ref that resolves to the same filename is excluded from
|
|
25
|
-
* the import list, preventing a file from importing itself.
|
|
26
|
-
*/
|
|
27
|
-
readonly selfRef?: string
|
|
28
|
-
/**
|
|
29
|
-
* The root schema document. When provided, URI refs that cannot be resolved
|
|
30
|
-
* within the root schema's $defs are excluded from the import list.
|
|
31
|
-
*/
|
|
32
|
-
readonly rootSchema?: Record<string, unknown>
|
|
33
|
-
/**
|
|
34
|
-
* When true, the generated parser emits a console.warn for every input key
|
|
35
|
-
* that is not declared in the schema's properties.
|
|
36
|
-
*/
|
|
37
|
-
readonly logWarnings?: boolean
|
|
38
|
-
/**
|
|
39
|
-
* When true, the generated parser throws on type/shape mismatches instead
|
|
40
|
-
* of coercing invalid input to default values.
|
|
41
|
-
*/
|
|
42
|
-
readonly strict?: boolean
|
|
43
|
-
/**
|
|
44
|
-
* Controls how runtime helpers are referenced.
|
|
45
|
-
* - `'package'` (default): emit imports from `@amritk/helpers/...`.
|
|
46
|
-
* - `'embedded'`: emit imports from `./_helpers/...` so the helper sources can
|
|
47
|
-
* be shipped alongside the generated files.
|
|
48
|
-
*/
|
|
49
|
-
readonly helpersMode?: HelpersMode
|
|
50
|
-
/**
|
|
51
|
-
* Relative path prefix to the shared `_helpers/` directory in embedded mode.
|
|
52
|
-
* Defaults to `'./'`. The recursive multi-schema build passes `'../'`,
|
|
53
|
-
* `'../../'`, etc. so a nested parser can reach a single `_helpers/` directory
|
|
54
|
-
* at the output root.
|
|
55
|
-
*/
|
|
56
|
-
readonly helpersImportPrefix?: string
|
|
57
|
-
/**
|
|
58
|
-
* When true, every property, array, and record in the generated type
|
|
59
|
-
* definitions is emitted as `readonly`.
|
|
60
|
-
*/
|
|
61
|
-
readonly readonly?: boolean
|
|
62
|
-
/**
|
|
63
|
-
* Suffix appended to every type/parser name derived from a `$ref`.
|
|
64
|
-
* Defaults to `''` (no suffix). Set to e.g. `'Object'` to emit `ContactObject`.
|
|
65
|
-
*/
|
|
66
|
-
readonly typeSuffix?: string
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Result of generating a single parser file. */
|
|
70
|
-
export type GeneratedFileContent = {
|
|
71
|
-
readonly content: string
|
|
72
|
-
readonly usedHelpers: ReadonlySet<RuntimeHelperName>
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Generates a complete TypeScript file from a JSON Schema.
|
|
77
|
-
*
|
|
78
|
-
* The generated file contains imports, type definition, and parser function.
|
|
79
|
-
* When typesOnly is true, only the type definition is emitted without any parser.
|
|
80
|
-
*
|
|
81
|
-
* @param schema - The JSON Schema to generate code from
|
|
82
|
-
* @param typeName - The name to use for the generated TypeScript type
|
|
83
|
-
* @param options - Optional settings to control what gets generated
|
|
84
|
-
* @returns The generated file content and the set of runtime helpers it references
|
|
85
|
-
*
|
|
86
|
-
* @example
|
|
87
|
-
* ```typescript
|
|
88
|
-
* const schema = {
|
|
89
|
-
* type: "object",
|
|
90
|
-
* properties: {
|
|
91
|
-
* name: { type: "string" },
|
|
92
|
-
* age: { type: "number" }
|
|
93
|
-
* },
|
|
94
|
-
* required: ["name"]
|
|
95
|
-
* };
|
|
96
|
-
*
|
|
97
|
-
* const result = generateFile(schema, "User");
|
|
98
|
-
* // result contains imports, type User, and parseUser function
|
|
99
|
-
*
|
|
100
|
-
* const typesOnly = generateFile(schema, "User", { typesOnly: true });
|
|
101
|
-
* // typesOnly contains only type-only imports and type User (no parser)
|
|
102
|
-
* ```
|
|
103
|
-
*/
|
|
104
|
-
export const generateFile = (
|
|
105
|
-
schema: JSONSchema,
|
|
106
|
-
typeName: string,
|
|
107
|
-
options?: GenerateFileOptions,
|
|
108
|
-
): GeneratedFileContent => {
|
|
109
|
-
const typesOnly = options?.typesOnly === true
|
|
110
|
-
const selfRef = options?.selfRef
|
|
111
|
-
const rootSchema = options?.rootSchema
|
|
112
|
-
const helpersMode: HelpersMode = options?.helpersMode ?? 'package'
|
|
113
|
-
const typeSuffix = options?.typeSuffix ?? ''
|
|
114
|
-
const typeDefinition = generateTypeDefinition(schema, typeName, { readonly: options?.readonly ?? false, typeSuffix })
|
|
115
|
-
|
|
116
|
-
if (typesOnly) {
|
|
117
|
-
// In types-only mode, skip the parser function and use type-only imports
|
|
118
|
-
const imports = collectImports(schema, { typesOnly: true, selfRef, rootSchema, typeSuffix })
|
|
119
|
-
let result = ''
|
|
120
|
-
|
|
121
|
-
if (imports.length > 0) {
|
|
122
|
-
result += imports[0]
|
|
123
|
-
for (let i = 1; i < imports.length; i++) {
|
|
124
|
-
result += '\n' + imports[i]
|
|
125
|
-
}
|
|
126
|
-
result += '\n\n'
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return { content: result + typeDefinition, usedHelpers: new Set() }
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const parserFunction = generateParserFunction(schema, typeName, {
|
|
133
|
-
useRefImports: true,
|
|
134
|
-
typeSuffix,
|
|
135
|
-
...(options?.logWarnings !== undefined ? { logWarnings: options.logWarnings } : {}),
|
|
136
|
-
...(options?.strict !== undefined ? { strict: options.strict } : {}),
|
|
137
|
-
})
|
|
138
|
-
const shapeValidator = generateShapeValidator(schema, typeName, true, typeSuffix)
|
|
139
|
-
const combinedFunctions = `${shapeValidator}\n\n${parserFunction}`
|
|
140
|
-
const helpers: CollectedHelpers = collectHelpers(combinedFunctions, helpersMode, options?.helpersImportPrefix)
|
|
141
|
-
const imports = [...collectImports(schema, { selfRef, rootSchema, typeSuffix }), ...helpers.imports]
|
|
142
|
-
|
|
143
|
-
// Build file output using string concatenation instead of array join for performance
|
|
144
|
-
let result = ''
|
|
145
|
-
|
|
146
|
-
if (imports.length > 0) {
|
|
147
|
-
result += imports[0]
|
|
148
|
-
for (let i = 1; i < imports.length; i++) {
|
|
149
|
-
result += '\n' + imports[i]
|
|
150
|
-
}
|
|
151
|
-
result += '\n\n'
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return { content: result + typeDefinition + '\n\n' + combinedFunctions, usedHelpers: helpers.used }
|
|
155
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { generateNonDiscriminatedUnionValidation } from './generate-non-discriminated-union-validation'
|
|
4
|
-
|
|
5
|
-
describe('generate-non-discriminated-union-validation', () => {
|
|
6
|
-
it('generates OR chain for multiple schema checks', () => {
|
|
7
|
-
const schemas = [{ type: 'string' as const }, { type: 'number' as const }]
|
|
8
|
-
const result = generateNonDiscriminatedUnionValidation('value', schemas, '""', true)
|
|
9
|
-
expect(result).toContain('typeof value === "string"')
|
|
10
|
-
expect(result).toContain('typeof value === "number"')
|
|
11
|
-
expect(result).toContain('||')
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('returns fallback with nullish coalescing when required and no checks', () => {
|
|
15
|
-
const schemas = [{ description: 'no type' }]
|
|
16
|
-
const result = generateNonDiscriminatedUnionValidation('value', schemas, '""', true)
|
|
17
|
-
expect(result).toBe('value ?? ""')
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('returns accessor when not required and no checks', () => {
|
|
21
|
-
const schemas = [{ description: 'no type' }]
|
|
22
|
-
const result = generateNonDiscriminatedUnionValidation('value', schemas, '""', false)
|
|
23
|
-
expect(result).toBe('value')
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('wraps valid check in ternary with default value when required', () => {
|
|
27
|
-
const schemas = [{ type: 'string' as const }]
|
|
28
|
-
const result = generateNonDiscriminatedUnionValidation('value', schemas, '""', true)
|
|
29
|
-
expect(result).toBe('((typeof value === "string")) ? value : ""')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('uses undefined as fallback when not required', () => {
|
|
33
|
-
const schemas = [{ type: 'string' as const }]
|
|
34
|
-
const result = generateNonDiscriminatedUnionValidation('value', schemas, '""', false)
|
|
35
|
-
expect(result).toBe('((typeof value === "string")) ? value : undefined')
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('handles schemas with multiple constraints combined with &&', () => {
|
|
39
|
-
const schemas = [{ type: 'string' as const, minLength: 1, maxLength: 100 }]
|
|
40
|
-
const result = generateNonDiscriminatedUnionValidation('value', schemas, '""', true)
|
|
41
|
-
expect(result).toContain('typeof value === "string"')
|
|
42
|
-
expect(result).toContain('value.length >= 1')
|
|
43
|
-
expect(result).toContain('value.length <= 100')
|
|
44
|
-
expect(result).toContain('&&')
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('handles empty schemas array when required', () => {
|
|
48
|
-
const result = generateNonDiscriminatedUnionValidation('value', [], '""', true)
|
|
49
|
-
expect(result).toBe('value ?? ""')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('handles empty schemas array when not required', () => {
|
|
53
|
-
const result = generateNonDiscriminatedUnionValidation('value', [], '""', false)
|
|
54
|
-
expect(result).toBe('value')
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('generates multiple OR groups for different types', () => {
|
|
58
|
-
const schemas = [{ type: 'string' as const }, { type: 'number' as const }, { type: 'boolean' as const }]
|
|
59
|
-
const result = generateNonDiscriminatedUnionValidation('value', schemas, 'null', true)
|
|
60
|
-
expect(result).toContain('typeof value === "string"')
|
|
61
|
-
expect(result).toContain('typeof value === "number"')
|
|
62
|
-
expect(result).toContain('typeof value === "boolean"')
|
|
63
|
-
// Should use || between groups
|
|
64
|
-
const orCount = (result.match(/\|\|/g) || []).length
|
|
65
|
-
expect(orCount).toBe(2)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('uses the provided accessor throughout', () => {
|
|
69
|
-
const schemas = [{ type: 'string' as const }]
|
|
70
|
-
const result = generateNonDiscriminatedUnionValidation('input?.name', schemas, '""', true)
|
|
71
|
-
expect(result).toContain('typeof input?.name === "string"')
|
|
72
|
-
expect(result).toContain('input?.name :')
|
|
73
|
-
})
|
|
74
|
-
})
|