@amritk/generate-parsers 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +267 -32
- 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,275 +0,0 @@
|
|
|
1
|
-
import { refToFilename } from '@amritk/helpers/ref-to-filename'
|
|
2
|
-
import { refToName } from '@amritk/helpers/ref-to-name'
|
|
3
|
-
import { resolveRef } from '@amritk/helpers/resolve-ref'
|
|
4
|
-
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasItems, hasOneOf, hasRef } from '@amritk/helpers/schema-guards'
|
|
5
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
6
|
-
|
|
7
|
-
const getImportPathForFilename = (filename: string): string => `./${filename}`
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Options for controlling how imports are collected.
|
|
11
|
-
*/
|
|
12
|
-
type CollectImportsOptions = {
|
|
13
|
-
/**
|
|
14
|
-
* When true, only generate type-only imports (no parser function imports).
|
|
15
|
-
* Use this when generating types-only files that do not include parser functions.
|
|
16
|
-
*/
|
|
17
|
-
readonly typesOnly?: boolean
|
|
18
|
-
/**
|
|
19
|
-
* The $ref path of the schema being generated (e.g. `#/$defs/encoding`).
|
|
20
|
-
* When provided, any $ref that resolves to the same filename is excluded from
|
|
21
|
-
* the import list, preventing a file from importing itself.
|
|
22
|
-
*/
|
|
23
|
-
readonly selfRef?: string | undefined
|
|
24
|
-
/**
|
|
25
|
-
* The root schema document. When provided, URI refs that cannot be resolved
|
|
26
|
-
* within the root schema's $defs are excluded from the import list, preventing
|
|
27
|
-
* imports for external schemas that were never generated as files.
|
|
28
|
-
*/
|
|
29
|
-
readonly rootSchema?: Record<string, unknown> | undefined
|
|
30
|
-
/**
|
|
31
|
-
* Suffix appended to every type name derived from a `$ref`. Must match the
|
|
32
|
-
* suffix used when generating the referenced files so imports resolve.
|
|
33
|
-
* Defaults to `''` (no suffix).
|
|
34
|
-
*/
|
|
35
|
-
readonly typeSuffix?: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Collects all import statements needed for a schema by finding $ref references
|
|
40
|
-
* that are directly used in the type definition (from properties, items, etc.).
|
|
41
|
-
* Does not include refs from nested schema definitions.
|
|
42
|
-
*
|
|
43
|
-
* Generates both type imports and parser function imports for each $ref.
|
|
44
|
-
* The parser imports allow validators to delegate validation to the referenced
|
|
45
|
-
* schema's parser instead of inlining the validation logic.
|
|
46
|
-
*
|
|
47
|
-
* Also detects when helper functions (isObject) and validators (validateRecord,
|
|
48
|
-
* validateArray) are needed based on the schema structure.
|
|
49
|
-
*
|
|
50
|
-
* @param schema - The JSON Schema to collect imports from
|
|
51
|
-
* @param options - Optional settings to control import generation
|
|
52
|
-
* @returns An array of import statements
|
|
53
|
-
*
|
|
54
|
-
* @example
|
|
55
|
-
* ```ts
|
|
56
|
-
* const schema = {
|
|
57
|
-
* type: 'object',
|
|
58
|
-
* properties: {
|
|
59
|
-
* contact: { $ref: '#/$defs/contact' },
|
|
60
|
-
* server: { $ref: '#/$defs/server' }
|
|
61
|
-
* }
|
|
62
|
-
* }
|
|
63
|
-
* const imports = collectImports(schema)
|
|
64
|
-
* // imports = [
|
|
65
|
-
* // "import { type Contact, parseContact } from './contact';",
|
|
66
|
-
* // "import { type Server, parseServer } from './server';"
|
|
67
|
-
* // ]
|
|
68
|
-
*
|
|
69
|
-
* // With typesOnly, only type imports are generated:
|
|
70
|
-
* const typeImports = collectImports(schema, { typesOnly: true })
|
|
71
|
-
* // typeImports = [
|
|
72
|
-
* // "import type { Contact } from './contact';",
|
|
73
|
-
* // "import type { Server } from './server';"
|
|
74
|
-
* // ]
|
|
75
|
-
* ```
|
|
76
|
-
*/
|
|
77
|
-
export const collectImports = (schema: JSONSchema, options?: CollectImportsOptions): string[] => {
|
|
78
|
-
const typesOnly = options?.typesOnly === true
|
|
79
|
-
const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : undefined
|
|
80
|
-
const rootSchema = options?.rootSchema
|
|
81
|
-
const typeSuffix = options?.typeSuffix
|
|
82
|
-
const refs = new Set<string>()
|
|
83
|
-
|
|
84
|
-
const collectRefsFromValue = (value: unknown): void => {
|
|
85
|
-
if (typeof value !== 'object' || value === null) {
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (Array.isArray(value)) {
|
|
90
|
-
for (const item of value) {
|
|
91
|
-
collectRefsFromValue(item)
|
|
92
|
-
}
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const record = value as Record<string, unknown>
|
|
97
|
-
|
|
98
|
-
// If this is a $ref, add it — but skip:
|
|
99
|
-
// - Relative path refs (e.g. /components/messages/foo) which point into example data
|
|
100
|
-
// - URI refs with fragments pointing into `properties` (not standalone definitions)
|
|
101
|
-
if (hasRef(record)) {
|
|
102
|
-
const ref = record.$ref
|
|
103
|
-
const isInternal = ref.startsWith('#')
|
|
104
|
-
const isUri = ref.startsWith('http://') || ref.startsWith('https://')
|
|
105
|
-
const isPropertyFragment = isUri && ref.includes('#/properties/')
|
|
106
|
-
if (isInternal || (isUri && !isPropertyFragment)) {
|
|
107
|
-
refs.add(ref)
|
|
108
|
-
}
|
|
109
|
-
return // Don't traverse further into a $ref
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Check if this is an object type with additionalProperties that has a $ref
|
|
113
|
-
// This pattern requires validateRecord
|
|
114
|
-
if (hasAdditionalProperties(record) && hasRef(record.additionalProperties)) {
|
|
115
|
-
collectRefsFromValue(record.additionalProperties)
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Check if this is an array type with items that has a $ref
|
|
120
|
-
// This pattern requires validateArray
|
|
121
|
-
if (record.type === 'array' && hasItems(record) && hasRef(record.items)) {
|
|
122
|
-
collectRefsFromValue(record.items)
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Traverse nested properties regardless of whether `type` is explicitly set.
|
|
127
|
-
if ('properties' in record && typeof record.properties === 'object' && record.properties !== null) {
|
|
128
|
-
const props = record.properties as Record<string, unknown>
|
|
129
|
-
for (const key in props) {
|
|
130
|
-
collectRefsFromValue(props[key])
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Traverse into type composition keywords
|
|
135
|
-
if (hasOneOf(record)) {
|
|
136
|
-
for (const item of record.oneOf) {
|
|
137
|
-
collectRefsFromValue(item)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (hasAnyOf(record)) {
|
|
141
|
-
for (const item of record.anyOf) {
|
|
142
|
-
collectRefsFromValue(item)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
if (hasAllOf(record)) {
|
|
146
|
-
for (const item of record.allOf) {
|
|
147
|
-
collectRefsFromValue(item)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Traverse into array items
|
|
152
|
-
if (hasItems(record)) {
|
|
153
|
-
collectRefsFromValue(record.items)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Traverse into additionalProperties
|
|
157
|
-
if (hasAdditionalProperties(record)) {
|
|
158
|
-
collectRefsFromValue(record.additionalProperties)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Traverse all patternProperties
|
|
162
|
-
if (
|
|
163
|
-
'patternProperties' in record &&
|
|
164
|
-
typeof record.patternProperties === 'object' &&
|
|
165
|
-
record.patternProperties !== null
|
|
166
|
-
) {
|
|
167
|
-
const patternProps = record.patternProperties as Record<string, unknown>
|
|
168
|
-
for (const key in patternProps) {
|
|
169
|
-
collectRefsFromValue(patternProps[key])
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Traverse into if/then/else branches
|
|
174
|
-
if ('then' in record) {
|
|
175
|
-
collectRefsFromValue(record.then)
|
|
176
|
-
}
|
|
177
|
-
if ('else' in record) {
|
|
178
|
-
collectRefsFromValue(record.else)
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Collect refs from root-level $ref
|
|
183
|
-
if (typeof schema === 'object' && schema !== null && hasRef(schema)) {
|
|
184
|
-
const ref = schema.$ref
|
|
185
|
-
const isInternal = ref.startsWith('#')
|
|
186
|
-
const isUri = ref.startsWith('http://') || ref.startsWith('https://')
|
|
187
|
-
const isPropertyFragment = isUri && ref.includes('#/properties/')
|
|
188
|
-
if (isInternal || (isUri && !isPropertyFragment)) {
|
|
189
|
-
refs.add(ref)
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Collect refs from properties
|
|
194
|
-
if (typeof schema === 'object' && schema !== null && 'properties' in schema) {
|
|
195
|
-
const properties = schema.properties as Record<string, unknown>
|
|
196
|
-
for (const key in properties) {
|
|
197
|
-
collectRefsFromValue(properties[key])
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Collect refs from root-level additionalProperties
|
|
202
|
-
if (typeof schema === 'object' && schema !== null && 'additionalProperties' in schema) {
|
|
203
|
-
collectRefsFromValue(schema.additionalProperties)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Collect refs from root-level patternProperties
|
|
207
|
-
if (typeof schema === 'object' && schema !== null && 'patternProperties' in schema) {
|
|
208
|
-
const patternProps = schema.patternProperties as Record<string, unknown>
|
|
209
|
-
for (const key in patternProps) {
|
|
210
|
-
collectRefsFromValue(patternProps[key])
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Collect refs from root-level items (when the schema itself is an array type)
|
|
215
|
-
if (typeof schema === 'object' && schema !== null && hasItems(schema)) {
|
|
216
|
-
collectRefsFromValue(schema.items)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Collect refs from root-level composition keywords.
|
|
220
|
-
if (typeof schema === 'object' && schema !== null && hasOneOf(schema)) {
|
|
221
|
-
for (const item of schema.oneOf) {
|
|
222
|
-
collectRefsFromValue(item)
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (typeof schema === 'object' && schema !== null && hasAnyOf(schema)) {
|
|
226
|
-
for (const item of schema.anyOf) {
|
|
227
|
-
collectRefsFromValue(item)
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
if (typeof schema === 'object' && schema !== null && hasAllOf(schema)) {
|
|
231
|
-
for (const item of schema.allOf) {
|
|
232
|
-
collectRefsFromValue(item)
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Collect refs from root-level conditional branches.
|
|
237
|
-
if (typeof schema === 'object' && schema !== null && 'if' in schema) {
|
|
238
|
-
collectRefsFromValue(schema.if)
|
|
239
|
-
}
|
|
240
|
-
if (typeof schema === 'object' && schema !== null && 'then' in schema) {
|
|
241
|
-
collectRefsFromValue(schema.then)
|
|
242
|
-
}
|
|
243
|
-
if (typeof schema === 'object' && schema !== null && 'else' in schema) {
|
|
244
|
-
collectRefsFromValue(schema.else)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Convert refs to combined import statements, deduplicating by filename
|
|
248
|
-
const importMap = new Map<string, string>()
|
|
249
|
-
|
|
250
|
-
for (const ref of refs) {
|
|
251
|
-
// For URI refs, skip if the base URI is not resolvable in the root schema.
|
|
252
|
-
// This prevents generating imports for external schemas that were never generated as files.
|
|
253
|
-
if (rootSchema !== undefined && (ref.startsWith('http://') || ref.startsWith('https://'))) {
|
|
254
|
-
if (!resolveRef(ref, rootSchema)) continue
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const typeName = refToName(ref, typeSuffix)
|
|
258
|
-
const filename = refToFilename(ref)
|
|
259
|
-
|
|
260
|
-
// Skip self-referential imports — a file must not import from itself
|
|
261
|
-
if (selfFilename !== undefined && filename === selfFilename) {
|
|
262
|
-
continue
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const importPath = getImportPathForFilename(filename)
|
|
266
|
-
|
|
267
|
-
// In types-only mode, omit the parser function import since there is no parser to call
|
|
268
|
-
const importStatement = typesOnly
|
|
269
|
-
? `import type { ${typeName} } from '${importPath}';`
|
|
270
|
-
: `import { type ${typeName}, parse${typeName}, validate${typeName}Shape } from '${importPath}';`
|
|
271
|
-
importMap.set(filename, importStatement)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return Array.from(importMap.values()).sort()
|
|
275
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { findDiscriminator } from './find-discriminator'
|
|
4
|
-
|
|
5
|
-
describe('find-discriminator', () => {
|
|
6
|
-
it('finds discriminator when each schema has a unique const value', () => {
|
|
7
|
-
const schemas = [
|
|
8
|
-
{ type: 'object' as const, properties: { kind: { const: 'a' } } },
|
|
9
|
-
{ type: 'object' as const, properties: { kind: { const: 'b' } } },
|
|
10
|
-
{ type: 'object' as const, properties: { kind: { const: 'c' } } },
|
|
11
|
-
]
|
|
12
|
-
expect(findDiscriminator(schemas)).toBe('kind')
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('finds discriminator when each schema has a single enum value', () => {
|
|
16
|
-
const schemas = [
|
|
17
|
-
{ type: 'object' as const, properties: { type: { enum: ['cat'] } } },
|
|
18
|
-
{ type: 'object' as const, properties: { type: { enum: ['dog'] } } },
|
|
19
|
-
]
|
|
20
|
-
expect(findDiscriminator(schemas)).toBe('type')
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('returns null when schemas share the same const value', () => {
|
|
24
|
-
const schemas = [
|
|
25
|
-
{ type: 'object' as const, properties: { kind: { const: 'a' } } },
|
|
26
|
-
{ type: 'object' as const, properties: { kind: { const: 'a' } } },
|
|
27
|
-
]
|
|
28
|
-
expect(findDiscriminator(schemas)).toBeNull()
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('returns null when schemas have no properties', () => {
|
|
32
|
-
const schemas = [{ type: 'object' as const }, { type: 'object' as const }]
|
|
33
|
-
expect(findDiscriminator(schemas)).toBeNull()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('returns null for empty schemas array', () => {
|
|
37
|
-
expect(findDiscriminator([])).toBeNull()
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('returns null when schemas are boolean', () => {
|
|
41
|
-
expect(findDiscriminator([true, false])).toBeNull()
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('returns null when not all schemas have the discriminator property', () => {
|
|
45
|
-
const schemas = [
|
|
46
|
-
{ type: 'object' as const, properties: { kind: { const: 'a' } } },
|
|
47
|
-
{ type: 'object' as const, properties: { name: { type: 'string' as const } } },
|
|
48
|
-
]
|
|
49
|
-
expect(findDiscriminator(schemas)).toBeNull()
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('returns null when property has multi-value enum', () => {
|
|
53
|
-
const schemas = [
|
|
54
|
-
{ type: 'object' as const, properties: { type: { enum: ['a', 'b'] } } },
|
|
55
|
-
{ type: 'object' as const, properties: { type: { enum: ['c'] } } },
|
|
56
|
-
]
|
|
57
|
-
// Multi-value enum does not count as a discriminator value
|
|
58
|
-
expect(findDiscriminator(schemas)).toBeNull()
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('returns null when property schema has no const or enum', () => {
|
|
62
|
-
const schemas = [
|
|
63
|
-
{ type: 'object' as const, properties: { kind: { type: 'string' as const } } },
|
|
64
|
-
{ type: 'object' as const, properties: { kind: { type: 'string' as const } } },
|
|
65
|
-
]
|
|
66
|
-
expect(findDiscriminator(schemas)).toBeNull()
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('prefers the first property that matches across all schemas', () => {
|
|
70
|
-
const schemas = [
|
|
71
|
-
{ type: 'object' as const, properties: { a: { const: 1 }, b: { const: 'x' } } },
|
|
72
|
-
{ type: 'object' as const, properties: { a: { const: 2 }, b: { const: 'y' } } },
|
|
73
|
-
]
|
|
74
|
-
// Both 'a' and 'b' are valid discriminators; returns the first one found
|
|
75
|
-
const result = findDiscriminator(schemas)
|
|
76
|
-
expect(result === 'a' || result === 'b').toBe(true)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('ignores schemas without type object', () => {
|
|
80
|
-
const schemas = [{ type: 'string' as const }, { type: 'object' as const, properties: { kind: { const: 'a' } } }]
|
|
81
|
-
expect(findDiscriminator(schemas)).toBeNull()
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('handles schemas identified as objects by having properties without explicit type', () => {
|
|
85
|
-
const schemas = [{ properties: { kind: { const: 'a' } } }, { properties: { kind: { const: 'b' } } }]
|
|
86
|
-
expect(findDiscriminator(schemas)).toBe('kind')
|
|
87
|
-
})
|
|
88
|
-
})
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { hasConst, hasEnum, hasProperties, isObjectSchema, isSchemaObject } from '@amritk/helpers/schema-guards'
|
|
2
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Attempts to find a discriminator property that can be used to distinguish between union types.
|
|
6
|
-
* Returns the property name if a suitable discriminator is found, otherwise null.
|
|
7
|
-
*/
|
|
8
|
-
export const findDiscriminator = (schemas: readonly JSONSchema[]): string | null => {
|
|
9
|
-
// Look for a property that has different constant/enum values in each schema
|
|
10
|
-
const propertyValues = new Map<string, Set<unknown>>()
|
|
11
|
-
|
|
12
|
-
for (const schema of schemas) {
|
|
13
|
-
if (isObjectSchema(schema) && hasProperties(schema)) {
|
|
14
|
-
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
15
|
-
if (!propertyValues.has(key)) {
|
|
16
|
-
propertyValues.set(key, new Set())
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Check if property has a const value
|
|
20
|
-
if (isSchemaObject(propSchema) && hasConst(propSchema)) {
|
|
21
|
-
propertyValues.get(key)?.add(propSchema.const)
|
|
22
|
-
}
|
|
23
|
-
// Check if property has a single enum value
|
|
24
|
-
else if (isSchemaObject(propSchema) && hasEnum(propSchema) && propSchema.enum.length === 1) {
|
|
25
|
-
propertyValues.get(key)?.add(propSchema.enum[0])
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Find a property where each schema has a unique value
|
|
32
|
-
for (const [key, values] of propertyValues.entries()) {
|
|
33
|
-
if (values.size === schemas.length) {
|
|
34
|
-
return key
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return null
|
|
39
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { getDefaultValue } from './get-default-value'
|
|
4
|
-
|
|
5
|
-
describe('get-default-value', () => {
|
|
6
|
-
it('returns undefined for boolean schema', () => {
|
|
7
|
-
expect(getDefaultValue(true)).toBe('undefined')
|
|
8
|
-
expect(getDefaultValue(false)).toBe('undefined')
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
it('returns explicit default value when present', () => {
|
|
12
|
-
expect(getDefaultValue({ default: 'hello' })).toBe('"hello"')
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('returns explicit default for numeric value', () => {
|
|
16
|
-
expect(getDefaultValue({ default: 42 })).toBe('42')
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('returns explicit default for boolean value', () => {
|
|
20
|
-
expect(getDefaultValue({ default: true })).toBe('true')
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('returns explicit default for null', () => {
|
|
24
|
-
expect(getDefaultValue({ default: null })).toBe('null')
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('returns explicit default for object value', () => {
|
|
28
|
-
expect(getDefaultValue({ default: { key: 'value' } })).toBe('{"key":"value"}')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('returns first enum value when no default', () => {
|
|
32
|
-
expect(getDefaultValue({ enum: ['a', 'b', 'c'] })).toBe('"a"')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('returns first example value when no default or enum', () => {
|
|
36
|
-
expect(getDefaultValue({ examples: ['example1', 'example2'] })).toBe('"example1"')
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('returns default from first oneOf schema', () => {
|
|
40
|
-
const schema = {
|
|
41
|
-
oneOf: [{ type: 'string' as const, default: 'from-oneof' }, { type: 'number' as const }],
|
|
42
|
-
}
|
|
43
|
-
expect(getDefaultValue(schema)).toBe('"from-oneof"')
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('recurses into oneOf when first schema has no explicit default', () => {
|
|
47
|
-
const schema = {
|
|
48
|
-
oneOf: [{ type: 'string' as const }, { type: 'number' as const }],
|
|
49
|
-
}
|
|
50
|
-
expect(getDefaultValue(schema)).toBe('""')
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('returns default from first anyOf schema', () => {
|
|
54
|
-
const schema = {
|
|
55
|
-
anyOf: [{ type: 'number' as const }],
|
|
56
|
-
}
|
|
57
|
-
expect(getDefaultValue(schema)).toBe('0')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('returns default from first allOf schema', () => {
|
|
61
|
-
const schema = {
|
|
62
|
-
allOf: [{ type: 'boolean' as const }],
|
|
63
|
-
}
|
|
64
|
-
expect(getDefaultValue(schema)).toBe('false')
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('returns undefined when no type is specified', () => {
|
|
68
|
-
expect(getDefaultValue({ description: 'no type' })).toBe('undefined')
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('returns empty string for string type', () => {
|
|
72
|
-
expect(getDefaultValue({ type: 'string' })).toBe('""')
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('returns 0 for number type', () => {
|
|
76
|
-
expect(getDefaultValue({ type: 'number' })).toBe('0')
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('returns 0 for integer type', () => {
|
|
80
|
-
expect(getDefaultValue({ type: 'integer' })).toBe('0')
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('returns false for boolean type', () => {
|
|
84
|
-
expect(getDefaultValue({ type: 'boolean' })).toBe('false')
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('returns empty array for array type', () => {
|
|
88
|
-
expect(getDefaultValue({ type: 'array' })).toBe('[]')
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('returns empty object for object type', () => {
|
|
92
|
-
expect(getDefaultValue({ type: 'object' })).toBe('{}')
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('returns undefined for unknown type', () => {
|
|
96
|
-
expect(getDefaultValue({ type: 'null' })).toBe('undefined')
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('uses pattern-based default for string with email pattern', () => {
|
|
100
|
-
const schema = { type: 'string' as const, pattern: '^[a-z]+@[a-z]+\\.[a-z]+$' }
|
|
101
|
-
expect(getDefaultValue(schema)).toBe('"user@example.com"')
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('falls back to empty string when pattern is not recognized', () => {
|
|
105
|
-
const schema = { type: 'string' as const, pattern: '^[a-z]+$' }
|
|
106
|
-
expect(getDefaultValue(schema)).toBe('""')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('prioritizes default over enum', () => {
|
|
110
|
-
const schema = { default: 'explicit', enum: ['a', 'b'] }
|
|
111
|
-
expect(getDefaultValue(schema)).toBe('"explicit"')
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('prioritizes enum over examples', () => {
|
|
115
|
-
const schema = { enum: ['enumVal'], examples: ['exampleVal'] }
|
|
116
|
-
expect(getDefaultValue(schema)).toBe('"enumVal"')
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('prioritizes examples over type-based default', () => {
|
|
120
|
-
const schema = { type: 'string' as const, examples: ['myExample'] }
|
|
121
|
-
expect(getDefaultValue(schema)).toBe('"myExample"')
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('handles empty enum array and falls back', () => {
|
|
125
|
-
expect(getDefaultValue({ enum: [], type: 'string' })).toBe('""')
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('handles empty examples array and falls back', () => {
|
|
129
|
-
expect(getDefaultValue({ examples: [], type: 'number' })).toBe('0')
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('handles empty oneOf array and falls back', () => {
|
|
133
|
-
expect(getDefaultValue({ oneOf: [], type: 'boolean' })).toBe('false')
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('returns 0n for an x-mjst bigint schema', () => {
|
|
137
|
-
expect(getDefaultValue({ 'x-mjst': { primitive: 'bigint' } })).toBe('0n')
|
|
138
|
-
})
|
|
139
|
-
})
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { getMjstPrimitive } from '@amritk/helpers/mjst-extension'
|
|
2
|
-
import {
|
|
3
|
-
hasAllOf,
|
|
4
|
-
hasAnyOf,
|
|
5
|
-
hasDefault,
|
|
6
|
-
hasEnum,
|
|
7
|
-
hasExamples,
|
|
8
|
-
hasOneOf,
|
|
9
|
-
hasPattern,
|
|
10
|
-
hasType,
|
|
11
|
-
isSchemaObject,
|
|
12
|
-
} from '@amritk/helpers/schema-guards'
|
|
13
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
14
|
-
import { generateDefaultFromPattern } from '#generators/generate-default-from-pattern'
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Returns the default value for a JSON Schema property.
|
|
18
|
-
* Priority order: explicit default > first enum value > first example > union first schema > pattern-based > type-based.
|
|
19
|
-
* These defaults ensure that parsing never fails, even with missing data.
|
|
20
|
-
*/
|
|
21
|
-
export const getDefaultValue = (schema: JSONSchema): string => {
|
|
22
|
-
if (!isSchemaObject(schema)) {
|
|
23
|
-
return 'undefined'
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Explicit default takes highest priority
|
|
27
|
-
if (hasDefault(schema)) {
|
|
28
|
-
return JSON.stringify(schema.default)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Use first enum value if available
|
|
32
|
-
if (hasEnum(schema) && schema.enum.length > 0) {
|
|
33
|
-
return JSON.stringify(schema.enum[0])
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Use first example if available
|
|
37
|
-
if (hasExamples(schema) && schema.examples.length > 0) {
|
|
38
|
-
return JSON.stringify(schema.examples[0])
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Handle union types - use first schema's default
|
|
42
|
-
if (hasOneOf(schema) && schema.oneOf.length > 0) {
|
|
43
|
-
const firstSchema = schema.oneOf[0]
|
|
44
|
-
if (firstSchema !== undefined) {
|
|
45
|
-
return getDefaultValue(firstSchema)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (hasAnyOf(schema) && schema.anyOf.length > 0) {
|
|
50
|
-
const firstSchema = schema.anyOf[0]
|
|
51
|
-
if (firstSchema !== undefined) {
|
|
52
|
-
return getDefaultValue(firstSchema)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Handle allOf - use first schema's default
|
|
57
|
-
if (hasAllOf(schema) && schema.allOf.length > 0) {
|
|
58
|
-
const firstSchema = schema.allOf[0]
|
|
59
|
-
if (firstSchema !== undefined) {
|
|
60
|
-
return getDefaultValue(firstSchema)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// x-mjst bigint has no native JSON type but a sensible zero default.
|
|
65
|
-
if (getMjstPrimitive(schema) === 'bigint') {
|
|
66
|
-
return '0n'
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (!hasType(schema)) {
|
|
70
|
-
return 'undefined'
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
switch (schema.type) {
|
|
74
|
-
case 'string': {
|
|
75
|
-
if (hasPattern(schema)) {
|
|
76
|
-
const patternDefault = generateDefaultFromPattern(schema.pattern)
|
|
77
|
-
if (patternDefault) {
|
|
78
|
-
return patternDefault
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return '""'
|
|
82
|
-
}
|
|
83
|
-
case 'number':
|
|
84
|
-
case 'integer':
|
|
85
|
-
return '0'
|
|
86
|
-
case 'boolean':
|
|
87
|
-
return 'false'
|
|
88
|
-
case 'array':
|
|
89
|
-
return '[]'
|
|
90
|
-
case 'object':
|
|
91
|
-
return '{}'
|
|
92
|
-
default:
|
|
93
|
-
return 'undefined'
|
|
94
|
-
}
|
|
95
|
-
}
|