@amritk/generate-parsers 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/generators/build-schema.d.ts +8 -1
- package/dist/generators/generate-files.d.ts +20 -2
- package/dist/generators/generate-parser-function.d.ts +6 -0
- package/dist/generators/generate-strict-assertion.d.ts +18 -0
- package/dist/helpers/collect-helpers.d.ts +16 -2
- package/dist/index.js +261 -50
- package/package.json +6 -2
- package/src/generators/build-schema.test.ts +830 -0
- package/src/generators/build-schema.ts +280 -0
- package/src/generators/generate-default-from-pattern.test.ts +81 -0
- package/src/generators/generate-default-from-pattern.ts +39 -0
- package/src/generators/generate-discriminated-union-validation.test.ts +84 -0
- package/src/generators/generate-discriminated-union-validation.ts +54 -0
- package/src/generators/generate-enum-check.test.ts +45 -0
- package/src/generators/generate-enum-check.ts +8 -0
- package/src/generators/generate-files.test.ts +435 -0
- package/src/generators/generate-files.ts +136 -0
- package/src/generators/generate-non-discriminated-union-validation.test.ts +74 -0
- package/src/generators/generate-non-discriminated-union-validation.ts +40 -0
- package/src/generators/generate-parser-function.test.ts +1798 -0
- package/src/generators/generate-parser-function.ts +1319 -0
- package/src/generators/generate-schema-checks.test.ts +322 -0
- package/src/generators/generate-schema-checks.ts +114 -0
- package/src/generators/generate-strict-assertion.ts +210 -0
- package/src/generators/generate-validation-expression.test.ts +794 -0
- package/src/generators/generate-validation-expression.ts +286 -0
- package/src/helpers/apply-schema-extensions.test.ts +194 -0
- package/src/helpers/apply-schema-extensions.ts +63 -0
- package/src/helpers/collect-helpers.test.ts +103 -0
- package/src/helpers/collect-helpers.ts +64 -0
- package/src/helpers/collect-imports.test.ts +587 -0
- package/src/helpers/collect-imports.ts +268 -0
- package/src/helpers/find-discriminator.test.ts +88 -0
- package/src/helpers/find-discriminator.ts +39 -0
- package/src/helpers/get-default-value.test.ts +135 -0
- package/src/helpers/get-default-value.ts +89 -0
- package/src/helpers/get-discriminator-value.test.ts +83 -0
- package/src/helpers/get-discriminator-value.ts +21 -0
- package/src/index.ts +1 -0
- package/src/types/schema-extensions.ts +35 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Names (without `.ts`) of runtime helpers that can be embedded into generated output. */
|
|
2
|
+
export type RuntimeHelperName = 'is-object' | 'validate-array' | 'validate-record' | 'has-ref'
|
|
3
|
+
|
|
4
|
+
/** Controls how generated parsers reference their runtime helpers. */
|
|
5
|
+
export type HelpersMode = 'package' | 'embedded'
|
|
6
|
+
|
|
7
|
+
export type CollectedHelpers = {
|
|
8
|
+
/** Import lines to splice into the generated file. */
|
|
9
|
+
readonly imports: string[]
|
|
10
|
+
/**
|
|
11
|
+
* Helper names that need to ship alongside the generated file in embedded mode.
|
|
12
|
+
* Includes transitive deps (e.g. `validate-record` pulls in `is-object`) so the
|
|
13
|
+
* orchestrator can emit a complete `_helpers/` directory.
|
|
14
|
+
*/
|
|
15
|
+
readonly used: Set<RuntimeHelperName>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PACKAGE_IMPORTS: Record<RuntimeHelperName, string> = {
|
|
19
|
+
'is-object': "import { isObject } from '@amritk/helpers/is-object';",
|
|
20
|
+
'validate-array': "import { validateArray } from '@amritk/helpers/validate-array';",
|
|
21
|
+
'validate-record': "import { validateRecord } from '@amritk/helpers/validate-record';",
|
|
22
|
+
// hasRef has historically lived in the schema-guards subpath; preserve that for package mode.
|
|
23
|
+
'has-ref': "import { hasRef } from '@amritk/helpers/schema-guards';",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const EMBEDDED_IMPORTS: Record<RuntimeHelperName, string> = {
|
|
27
|
+
'is-object': "import { isObject } from './_helpers/is-object';",
|
|
28
|
+
'validate-array': "import { validateArray } from './_helpers/validate-array';",
|
|
29
|
+
'validate-record': "import { validateRecord } from './_helpers/validate-record';",
|
|
30
|
+
'has-ref': "import { hasRef } from './_helpers/has-ref';",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Detects which runtime helpers a generated parser body references. */
|
|
34
|
+
export const collectHelpers = (parserFunction: string, mode: HelpersMode): CollectedHelpers => {
|
|
35
|
+
const imports: string[] = []
|
|
36
|
+
const used = new Set<RuntimeHelperName>()
|
|
37
|
+
const table = mode === 'embedded' ? EMBEDDED_IMPORTS : PACKAGE_IMPORTS
|
|
38
|
+
|
|
39
|
+
if (parserFunction.includes('validateArray')) {
|
|
40
|
+
imports.push(table['validate-array'])
|
|
41
|
+
used.add('validate-array')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (parserFunction.includes('validateRecord')) {
|
|
45
|
+
imports.push(table['validate-record'])
|
|
46
|
+
used.add('validate-record')
|
|
47
|
+
// The embedded validate-record.ts imports is-object, so it must be shipped too —
|
|
48
|
+
// but only the `validate-record` line is needed in the parser file unless the
|
|
49
|
+
// parser body itself references `isObject` (handled below).
|
|
50
|
+
used.add('is-object')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (parserFunction.includes('isObject')) {
|
|
54
|
+
imports.push(table['is-object'])
|
|
55
|
+
used.add('is-object')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (parserFunction.includes('hasRef(')) {
|
|
59
|
+
imports.push(table['has-ref'])
|
|
60
|
+
used.add('has-ref')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { imports, used }
|
|
64
|
+
}
|
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { collectImports } from './collect-imports'
|
|
5
|
+
|
|
6
|
+
describe('collect-imports', () => {
|
|
7
|
+
it('collects imports from properties with $ref', () => {
|
|
8
|
+
const schema: JSONSchema = {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
contact: { $ref: '#/$defs/contact' },
|
|
12
|
+
server: { $ref: '#/$defs/server' },
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const result = collectImports(schema)
|
|
17
|
+
|
|
18
|
+
expect(result).toEqual([
|
|
19
|
+
"import { type ContactObject, parseContactObject, validateContactObjectShape } from './contact';",
|
|
20
|
+
"import { type ServerObject, parseServerObject, validateServerObjectShape } from './server';",
|
|
21
|
+
])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('collects imports from array items with $ref', () => {
|
|
25
|
+
const schema: JSONSchema = {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
servers: {
|
|
29
|
+
type: 'array',
|
|
30
|
+
items: { $ref: '#/$defs/server' },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = collectImports(schema)
|
|
36
|
+
|
|
37
|
+
expect(result).toEqual([
|
|
38
|
+
"import { type ServerObject, parseServerObject, validateServerObjectShape } from './server';",
|
|
39
|
+
])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('collects imports from additionalProperties with $ref', () => {
|
|
43
|
+
const schema: JSONSchema = {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
webhooks: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
additionalProperties: { $ref: '#/$defs/path-item' },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const result = collectImports(schema)
|
|
54
|
+
|
|
55
|
+
expect(result).toEqual([
|
|
56
|
+
"import { type PathItemObject, parsePathItemObject, validatePathItemObjectShape } from './path-item';",
|
|
57
|
+
])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('collects imports from non-extension patternProperties with $ref', () => {
|
|
61
|
+
const schema: JSONSchema = {
|
|
62
|
+
type: 'object',
|
|
63
|
+
patternProperties: {
|
|
64
|
+
'^/': { $ref: '#/$defs/path-item' },
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = collectImports(schema)
|
|
69
|
+
|
|
70
|
+
expect(result).toEqual([
|
|
71
|
+
"import { type PathItemObject, parsePathItemObject, validatePathItemObjectShape } from './path-item';",
|
|
72
|
+
])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('collects imports from patternProperties with $ref', () => {
|
|
76
|
+
const schema: JSONSchema = {
|
|
77
|
+
type: 'object',
|
|
78
|
+
patternProperties: {
|
|
79
|
+
'^/': { $ref: '#/$defs/path-item' },
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = collectImports(schema)
|
|
84
|
+
|
|
85
|
+
expect(result).toEqual([
|
|
86
|
+
"import { type PathItemObject, parsePathItemObject, validatePathItemObjectShape } from './path-item';",
|
|
87
|
+
])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('collects imports from oneOf with $ref', () => {
|
|
91
|
+
const schema: JSONSchema = {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
response: {
|
|
95
|
+
oneOf: [{ $ref: '#/$defs/response' }, { $ref: '#/$defs/reference' }],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const result = collectImports(schema)
|
|
101
|
+
|
|
102
|
+
expect(result).toEqual([
|
|
103
|
+
"import { type ReferenceObject, parseReferenceObject, validateReferenceObjectShape } from './reference';",
|
|
104
|
+
"import { type ResponseObject, parseResponseObject, validateResponseObjectShape } from './response';",
|
|
105
|
+
])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('collects imports from anyOf with $ref', () => {
|
|
109
|
+
const schema: JSONSchema = {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
value: {
|
|
113
|
+
anyOf: [{ $ref: '#/$defs/string-value' }, { $ref: '#/$defs/number-value' }],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const result = collectImports(schema)
|
|
119
|
+
|
|
120
|
+
expect(result).toEqual([
|
|
121
|
+
"import { type NumberValueObject, parseNumberValueObject, validateNumberValueObjectShape } from './number-value';",
|
|
122
|
+
"import { type StringValueObject, parseStringValueObject, validateStringValueObjectShape } from './string-value';",
|
|
123
|
+
])
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('collects imports from allOf with $ref', () => {
|
|
127
|
+
const schema: JSONSchema = {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
combined: {
|
|
131
|
+
allOf: [{ $ref: '#/$defs/base' }, { $ref: '#/$defs/extension' }],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const result = collectImports(schema)
|
|
137
|
+
|
|
138
|
+
expect(result).toEqual([
|
|
139
|
+
"import { type BaseObject, parseBaseObject, validateBaseObjectShape } from './base';",
|
|
140
|
+
"import { type ExtensionObject, parseExtensionObject, validateExtensionObjectShape } from './extension';",
|
|
141
|
+
])
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('deduplicates imports by filename', () => {
|
|
145
|
+
const schema: JSONSchema = {
|
|
146
|
+
type: 'object',
|
|
147
|
+
properties: {
|
|
148
|
+
contact1: { $ref: '#/$defs/contact' },
|
|
149
|
+
contact2: { $ref: '#/$defs/contact' },
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result = collectImports(schema)
|
|
154
|
+
|
|
155
|
+
expect(result).toEqual([
|
|
156
|
+
"import { type ContactObject, parseContactObject, validateContactObjectShape } from './contact';",
|
|
157
|
+
])
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('collects imports from $ref properties', () => {
|
|
161
|
+
const schema: JSONSchema = {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
response: { $ref: '#/$defs/response' },
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const result = collectImports(schema)
|
|
169
|
+
|
|
170
|
+
expect(result).toEqual([
|
|
171
|
+
"import { type ResponseObject, parseResponseObject, validateResponseObjectShape } from './response';",
|
|
172
|
+
])
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('handles nested refs in complex schemas', () => {
|
|
176
|
+
const schema: JSONSchema = {
|
|
177
|
+
type: 'object',
|
|
178
|
+
properties: {
|
|
179
|
+
servers: {
|
|
180
|
+
type: 'array',
|
|
181
|
+
items: { $ref: '#/$defs/server' },
|
|
182
|
+
},
|
|
183
|
+
callbacks: {
|
|
184
|
+
type: 'object',
|
|
185
|
+
additionalProperties: { $ref: '#/$defs/callback' },
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const result = collectImports(schema)
|
|
191
|
+
|
|
192
|
+
expect(result).toEqual([
|
|
193
|
+
"import { type CallbackObject, parseCallbackObject, validateCallbackObjectShape } from './callback';",
|
|
194
|
+
"import { type ServerObject, parseServerObject, validateServerObjectShape } from './server';",
|
|
195
|
+
])
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('collects imports from root-level additionalProperties', () => {
|
|
199
|
+
const schema: JSONSchema = {
|
|
200
|
+
type: 'object',
|
|
201
|
+
additionalProperties: { $ref: '#/$defs/path-item' },
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const result = collectImports(schema)
|
|
205
|
+
|
|
206
|
+
expect(result).toEqual([
|
|
207
|
+
"import { type PathItemObject, parsePathItemObject, validatePathItemObjectShape } from './path-item';",
|
|
208
|
+
])
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('imports SchemaObject parser and type from generated schema.ts', () => {
|
|
212
|
+
const schema: JSONSchema = {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: {
|
|
215
|
+
schema: { $ref: '#/$defs/schema' },
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const result = collectImports(schema)
|
|
220
|
+
|
|
221
|
+
expect(result).toEqual([
|
|
222
|
+
"import { type SchemaObject, parseSchemaObject, validateSchemaObjectShape } from './schema';",
|
|
223
|
+
])
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('handles empty schema', () => {
|
|
227
|
+
const schema: JSONSchema = {
|
|
228
|
+
type: 'object',
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const result = collectImports(schema)
|
|
232
|
+
|
|
233
|
+
expect(result).toEqual([])
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('handles schema without properties', () => {
|
|
237
|
+
const schema: JSONSchema = {
|
|
238
|
+
type: 'string',
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const result = collectImports(schema)
|
|
242
|
+
|
|
243
|
+
expect(result).toEqual([])
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('collects imports from root-level allOf refs', () => {
|
|
247
|
+
const schema: JSONSchema = {
|
|
248
|
+
type: 'object',
|
|
249
|
+
allOf: [
|
|
250
|
+
{ $ref: '#/$defs/security-scheme/$defs/type-apikey' },
|
|
251
|
+
{ $ref: '#/$defs/security-scheme/$defs/type-http' },
|
|
252
|
+
],
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const result = collectImports(schema)
|
|
256
|
+
|
|
257
|
+
expect(result).toEqual([
|
|
258
|
+
"import { type TypeApikeyObject, parseTypeApikeyObject, validateTypeApikeyObjectShape } from './type-apikey';",
|
|
259
|
+
"import { type TypeHttpObject, parseTypeHttpObject, validateTypeHttpObjectShape } from './type-http';",
|
|
260
|
+
])
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('collects imports from all allOf $ref entries', () => {
|
|
264
|
+
const schema: JSONSchema = {
|
|
265
|
+
type: 'object',
|
|
266
|
+
properties: {
|
|
267
|
+
contentType: { type: 'string' },
|
|
268
|
+
},
|
|
269
|
+
allOf: [{ $ref: '#/$defs/base-schema' }, { $ref: '#/$defs/styles-for-form' }],
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const result = collectImports(schema)
|
|
273
|
+
|
|
274
|
+
expect(result).toEqual([
|
|
275
|
+
"import { type BaseSchemaObject, parseBaseSchemaObject, validateBaseSchemaObjectShape } from './base-schema';",
|
|
276
|
+
"import { type StylesForFormObject, parseStylesForFormObject, validateStylesForFormObjectShape } from './styles-for-form';",
|
|
277
|
+
])
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('generates type-only imports when typesOnly is true', () => {
|
|
281
|
+
const schema: JSONSchema = {
|
|
282
|
+
type: 'object',
|
|
283
|
+
properties: {
|
|
284
|
+
contact: { $ref: '#/$defs/contact' },
|
|
285
|
+
server: { $ref: '#/$defs/server' },
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const result = collectImports(schema, { typesOnly: true })
|
|
290
|
+
|
|
291
|
+
expect(result).toEqual([
|
|
292
|
+
"import type { ContactObject } from './contact';",
|
|
293
|
+
"import type { ServerObject } from './server';",
|
|
294
|
+
])
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('does not include parser names in type-only imports', () => {
|
|
298
|
+
const schema: JSONSchema = {
|
|
299
|
+
type: 'object',
|
|
300
|
+
properties: {
|
|
301
|
+
contact: { $ref: '#/$defs/contact' },
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const result = collectImports(schema, { typesOnly: true })
|
|
306
|
+
|
|
307
|
+
expect(result).toEqual(["import type { ContactObject } from './contact';"])
|
|
308
|
+
expect(result[0]).not.toContain('parseContactObject')
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('collects type-only imports from $ref properties in typesOnly mode', () => {
|
|
312
|
+
const schema: JSONSchema = {
|
|
313
|
+
type: 'object',
|
|
314
|
+
properties: {
|
|
315
|
+
response: { $ref: '#/$defs/response' },
|
|
316
|
+
},
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const result = collectImports(schema, { typesOnly: true })
|
|
320
|
+
|
|
321
|
+
expect(result).toEqual(["import type { ResponseObject } from './response';"])
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('generates type-only imports from array items $ref in typesOnly mode', () => {
|
|
325
|
+
const schema: JSONSchema = {
|
|
326
|
+
type: 'object',
|
|
327
|
+
properties: {
|
|
328
|
+
servers: {
|
|
329
|
+
type: 'array',
|
|
330
|
+
items: { $ref: '#/$defs/server' },
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const result = collectImports(schema, { typesOnly: true })
|
|
336
|
+
|
|
337
|
+
expect(result).toEqual(["import type { ServerObject } from './server';"])
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('generates type-only imports from additionalProperties $ref in typesOnly mode', () => {
|
|
341
|
+
const schema: JSONSchema = {
|
|
342
|
+
type: 'object',
|
|
343
|
+
additionalProperties: { $ref: '#/$defs/path-item' },
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const result = collectImports(schema, { typesOnly: true })
|
|
347
|
+
|
|
348
|
+
expect(result).toEqual(["import type { PathItemObject } from './path-item';"])
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('generates type-only imports from anyOf $ref in typesOnly mode', () => {
|
|
352
|
+
const schema: JSONSchema = {
|
|
353
|
+
type: 'object',
|
|
354
|
+
properties: {
|
|
355
|
+
value: {
|
|
356
|
+
anyOf: [{ $ref: '#/$defs/string-value' }, { $ref: '#/$defs/number-value' }],
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const result = collectImports(schema, { typesOnly: true })
|
|
362
|
+
|
|
363
|
+
expect(result).toEqual([
|
|
364
|
+
"import type { NumberValueObject } from './number-value';",
|
|
365
|
+
"import type { StringValueObject } from './string-value';",
|
|
366
|
+
])
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('generates type-only imports from allOf $ref in typesOnly mode', () => {
|
|
370
|
+
const schema: JSONSchema = {
|
|
371
|
+
type: 'object',
|
|
372
|
+
allOf: [{ $ref: '#/$defs/base' }, { $ref: '#/$defs/extension' }],
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const result = collectImports(schema, { typesOnly: true })
|
|
376
|
+
|
|
377
|
+
expect(result).toEqual([
|
|
378
|
+
"import type { BaseObject } from './base';",
|
|
379
|
+
"import type { ExtensionObject } from './extension';",
|
|
380
|
+
])
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('deduplicates type-only imports by filename', () => {
|
|
384
|
+
const schema: JSONSchema = {
|
|
385
|
+
type: 'object',
|
|
386
|
+
properties: {
|
|
387
|
+
contact1: { $ref: '#/$defs/contact' },
|
|
388
|
+
contact2: { $ref: '#/$defs/contact' },
|
|
389
|
+
},
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const result = collectImports(schema, { typesOnly: true })
|
|
393
|
+
|
|
394
|
+
expect(result).toEqual(["import type { ContactObject } from './contact';"])
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
it('returns empty array for schema with no $ref in typesOnly mode', () => {
|
|
398
|
+
const schema: JSONSchema = {
|
|
399
|
+
type: 'object',
|
|
400
|
+
properties: {
|
|
401
|
+
name: { type: 'string' },
|
|
402
|
+
age: { type: 'number' },
|
|
403
|
+
},
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const result = collectImports(schema, { typesOnly: true })
|
|
407
|
+
|
|
408
|
+
expect(result).toEqual([])
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('collects imports from root-level array items with $ref', () => {
|
|
412
|
+
const schema: JSONSchema = {
|
|
413
|
+
type: 'array',
|
|
414
|
+
items: { $ref: '#/$defs/server' },
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const result = collectImports(schema)
|
|
418
|
+
|
|
419
|
+
expect(result).toEqual([
|
|
420
|
+
"import { type ServerObject, parseServerObject, validateServerObjectShape } from './server';",
|
|
421
|
+
])
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('collects imports from root-level array items $ref in typesOnly mode', () => {
|
|
425
|
+
const schema: JSONSchema = {
|
|
426
|
+
type: 'array',
|
|
427
|
+
items: { $ref: '#/$defs/parameter' },
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const result = collectImports(schema, { typesOnly: true })
|
|
431
|
+
|
|
432
|
+
expect(result).toEqual(["import type { ParameterObject } from './parameter';"])
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('collects imports from root-level oneOf refs', () => {
|
|
436
|
+
const schema: JSONSchema = {
|
|
437
|
+
type: 'object',
|
|
438
|
+
oneOf: [{ $ref: '#/$defs/contact' }, { $ref: '#/$defs/server' }],
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const result = collectImports(schema)
|
|
442
|
+
|
|
443
|
+
expect(result).toContain(
|
|
444
|
+
"import { type ContactObject, parseContactObject, validateContactObjectShape } from './contact';",
|
|
445
|
+
)
|
|
446
|
+
expect(result).toContain(
|
|
447
|
+
"import { type ServerObject, parseServerObject, validateServerObjectShape } from './server';",
|
|
448
|
+
)
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('collects imports from root-level anyOf refs', () => {
|
|
452
|
+
const schema: JSONSchema = {
|
|
453
|
+
type: 'object',
|
|
454
|
+
anyOf: [{ $ref: '#/$defs/contact' }, { $ref: '#/$defs/server' }],
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const result = collectImports(schema)
|
|
458
|
+
|
|
459
|
+
expect(result).toContain(
|
|
460
|
+
"import { type ContactObject, parseContactObject, validateContactObjectShape } from './contact';",
|
|
461
|
+
)
|
|
462
|
+
expect(result).toContain(
|
|
463
|
+
"import { type ServerObject, parseServerObject, validateServerObjectShape } from './server';",
|
|
464
|
+
)
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
it('collects imports from root-level if branch', () => {
|
|
468
|
+
const schema: JSONSchema = {
|
|
469
|
+
if: { $ref: '#/$defs/contact' },
|
|
470
|
+
then: { type: 'object' },
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const result = collectImports(schema)
|
|
474
|
+
|
|
475
|
+
expect(result).toContain(
|
|
476
|
+
"import { type ContactObject, parseContactObject, validateContactObjectShape } from './contact';",
|
|
477
|
+
)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('collects imports from root-level then branch', () => {
|
|
481
|
+
const schema: JSONSchema = {
|
|
482
|
+
if: { type: 'object' },
|
|
483
|
+
then: { $ref: '#/$defs/server' },
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const result = collectImports(schema)
|
|
487
|
+
|
|
488
|
+
expect(result).toContain(
|
|
489
|
+
"import { type ServerObject, parseServerObject, validateServerObjectShape } from './server';",
|
|
490
|
+
)
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
it('collects imports from root-level else branch', () => {
|
|
494
|
+
const schema: JSONSchema = {
|
|
495
|
+
if: { type: 'object' },
|
|
496
|
+
else: { $ref: '#/$defs/contact' },
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const result = collectImports(schema)
|
|
500
|
+
|
|
501
|
+
expect(result).toContain(
|
|
502
|
+
"import { type ContactObject, parseContactObject, validateContactObjectShape } from './contact';",
|
|
503
|
+
)
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
it('does not generate a self-import when a schema references its own $defs key via a property', () => {
|
|
507
|
+
// Mirrors the encoding schema: encoding.ts has a property `itemEncoding` that is a direct
|
|
508
|
+
// $ref back to #/$defs/encoding. Generating `import ... from './encoding'` inside encoding.ts
|
|
509
|
+
// would be a circular self-import that crashes at runtime.
|
|
510
|
+
const schema: JSONSchema = {
|
|
511
|
+
type: 'object',
|
|
512
|
+
properties: {
|
|
513
|
+
itemEncoding: { $ref: '#/$defs/encoding' },
|
|
514
|
+
header: { $ref: '#/$defs/header' },
|
|
515
|
+
},
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const result = collectImports(schema, { selfRef: '#/$defs/encoding' })
|
|
519
|
+
|
|
520
|
+
expect(result).not.toContain("'./encoding'")
|
|
521
|
+
expect(result).toContain(
|
|
522
|
+
"import { type HeaderObject, parseHeaderObject, validateHeaderObjectShape } from './header';",
|
|
523
|
+
)
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
it('does not generate a self-import when a schema references its own $defs key via additionalProperties', () => {
|
|
527
|
+
const schema: JSONSchema = {
|
|
528
|
+
type: 'object',
|
|
529
|
+
properties: {
|
|
530
|
+
encoding: {
|
|
531
|
+
type: 'object',
|
|
532
|
+
additionalProperties: { $ref: '#/$defs/encoding' },
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const result = collectImports(schema, { selfRef: '#/$defs/encoding' })
|
|
538
|
+
|
|
539
|
+
expect(result).not.toContain("'./encoding'")
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
it('does not generate a self-import when a schema references its own $defs key via array items', () => {
|
|
543
|
+
const schema: JSONSchema = {
|
|
544
|
+
type: 'object',
|
|
545
|
+
properties: {
|
|
546
|
+
prefixEncoding: {
|
|
547
|
+
type: 'array',
|
|
548
|
+
items: { $ref: '#/$defs/encoding' },
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const result = collectImports(schema, { selfRef: '#/$defs/encoding' })
|
|
554
|
+
|
|
555
|
+
expect(result).not.toContain("'./encoding'")
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
it('does not collect imports for external $refs', () => {
|
|
559
|
+
// External refs (e.g. from draft-04 schemas) cannot be resolved locally and have no generated file.
|
|
560
|
+
const schema: JSONSchema = {
|
|
561
|
+
type: 'object',
|
|
562
|
+
properties: {
|
|
563
|
+
maximum: { $ref: 'http://json-schema.org/draft-04/schema#/properties/maximum' },
|
|
564
|
+
name: { type: 'string' },
|
|
565
|
+
},
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const result = collectImports(schema)
|
|
569
|
+
|
|
570
|
+
expect(result).toEqual([])
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
it('does not generate a self-import in types-only mode', () => {
|
|
574
|
+
const schema: JSONSchema = {
|
|
575
|
+
type: 'object',
|
|
576
|
+
properties: {
|
|
577
|
+
itemEncoding: { $ref: '#/$defs/encoding' },
|
|
578
|
+
header: { $ref: '#/$defs/header' },
|
|
579
|
+
},
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const result = collectImports(schema, { typesOnly: true, selfRef: '#/$defs/encoding' })
|
|
583
|
+
|
|
584
|
+
expect(result).not.toContain("'./encoding'")
|
|
585
|
+
expect(result).toContain("import type { HeaderObject } from './header';")
|
|
586
|
+
})
|
|
587
|
+
})
|