@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.
Files changed (41) hide show
  1. package/README.md +3 -1
  2. package/dist/generators/build-schema.d.ts +8 -1
  3. package/dist/generators/generate-files.d.ts +20 -2
  4. package/dist/generators/generate-parser-function.d.ts +6 -0
  5. package/dist/generators/generate-strict-assertion.d.ts +18 -0
  6. package/dist/helpers/collect-helpers.d.ts +16 -2
  7. package/dist/index.js +261 -50
  8. package/package.json +6 -2
  9. package/src/generators/build-schema.test.ts +830 -0
  10. package/src/generators/build-schema.ts +280 -0
  11. package/src/generators/generate-default-from-pattern.test.ts +81 -0
  12. package/src/generators/generate-default-from-pattern.ts +39 -0
  13. package/src/generators/generate-discriminated-union-validation.test.ts +84 -0
  14. package/src/generators/generate-discriminated-union-validation.ts +54 -0
  15. package/src/generators/generate-enum-check.test.ts +45 -0
  16. package/src/generators/generate-enum-check.ts +8 -0
  17. package/src/generators/generate-files.test.ts +435 -0
  18. package/src/generators/generate-files.ts +136 -0
  19. package/src/generators/generate-non-discriminated-union-validation.test.ts +74 -0
  20. package/src/generators/generate-non-discriminated-union-validation.ts +40 -0
  21. package/src/generators/generate-parser-function.test.ts +1798 -0
  22. package/src/generators/generate-parser-function.ts +1319 -0
  23. package/src/generators/generate-schema-checks.test.ts +322 -0
  24. package/src/generators/generate-schema-checks.ts +114 -0
  25. package/src/generators/generate-strict-assertion.ts +210 -0
  26. package/src/generators/generate-validation-expression.test.ts +794 -0
  27. package/src/generators/generate-validation-expression.ts +286 -0
  28. package/src/helpers/apply-schema-extensions.test.ts +194 -0
  29. package/src/helpers/apply-schema-extensions.ts +63 -0
  30. package/src/helpers/collect-helpers.test.ts +103 -0
  31. package/src/helpers/collect-helpers.ts +64 -0
  32. package/src/helpers/collect-imports.test.ts +587 -0
  33. package/src/helpers/collect-imports.ts +268 -0
  34. package/src/helpers/find-discriminator.test.ts +88 -0
  35. package/src/helpers/find-discriminator.ts +39 -0
  36. package/src/helpers/get-default-value.test.ts +135 -0
  37. package/src/helpers/get-default-value.ts +89 -0
  38. package/src/helpers/get-discriminator-value.test.ts +83 -0
  39. package/src/helpers/get-discriminator-value.ts +21 -0
  40. package/src/index.ts +1 -0
  41. package/src/types/schema-extensions.ts +35 -0
@@ -0,0 +1,268 @@
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
+
32
+ /**
33
+ * Collects all import statements needed for a schema by finding $ref references
34
+ * that are directly used in the type definition (from properties, items, etc.).
35
+ * Does not include refs from nested schema definitions.
36
+ *
37
+ * Generates both type imports and parser function imports for each $ref.
38
+ * The parser imports allow validators to delegate validation to the referenced
39
+ * schema's parser instead of inlining the validation logic.
40
+ *
41
+ * Also detects when helper functions (isObject) and validators (validateRecord,
42
+ * validateArray) are needed based on the schema structure.
43
+ *
44
+ * @param schema - The JSON Schema to collect imports from
45
+ * @param options - Optional settings to control import generation
46
+ * @returns An array of import statements
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const schema = {
51
+ * type: 'object',
52
+ * properties: {
53
+ * contact: { $ref: '#/$defs/contact' },
54
+ * server: { $ref: '#/$defs/server' }
55
+ * }
56
+ * }
57
+ * const imports = collectImports(schema)
58
+ * // imports = [
59
+ * // "import { type Contact, parseContact } from './contact';",
60
+ * // "import { type Server, parseServer } from './server';"
61
+ * // ]
62
+ *
63
+ * // With typesOnly, only type imports are generated:
64
+ * const typeImports = collectImports(schema, { typesOnly: true })
65
+ * // typeImports = [
66
+ * // "import type { Contact } from './contact';",
67
+ * // "import type { Server } from './server';"
68
+ * // ]
69
+ * ```
70
+ */
71
+ export const collectImports = (schema: JSONSchema, options?: CollectImportsOptions): string[] => {
72
+ const typesOnly = options?.typesOnly === true
73
+ const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : undefined
74
+ const rootSchema = options?.rootSchema
75
+ const refs = new Set<string>()
76
+
77
+ const collectRefsFromValue = (value: unknown): void => {
78
+ if (typeof value !== 'object' || value === null) {
79
+ return
80
+ }
81
+
82
+ if (Array.isArray(value)) {
83
+ for (const item of value) {
84
+ collectRefsFromValue(item)
85
+ }
86
+ return
87
+ }
88
+
89
+ const record = value as Record<string, unknown>
90
+
91
+ // If this is a $ref, add it — but skip:
92
+ // - Relative path refs (e.g. /components/messages/foo) which point into example data
93
+ // - URI refs with fragments pointing into `properties` (not standalone definitions)
94
+ if (hasRef(record)) {
95
+ const ref = record.$ref
96
+ const isInternal = ref.startsWith('#')
97
+ const isUri = ref.startsWith('http://') || ref.startsWith('https://')
98
+ const isPropertyFragment = isUri && ref.includes('#/properties/')
99
+ if (isInternal || (isUri && !isPropertyFragment)) {
100
+ refs.add(ref)
101
+ }
102
+ return // Don't traverse further into a $ref
103
+ }
104
+
105
+ // Check if this is an object type with additionalProperties that has a $ref
106
+ // This pattern requires validateRecord
107
+ if (hasAdditionalProperties(record) && hasRef(record.additionalProperties)) {
108
+ collectRefsFromValue(record.additionalProperties)
109
+ return
110
+ }
111
+
112
+ // Check if this is an array type with items that has a $ref
113
+ // This pattern requires validateArray
114
+ if (record.type === 'array' && hasItems(record) && hasRef(record.items)) {
115
+ collectRefsFromValue(record.items)
116
+ return
117
+ }
118
+
119
+ // Traverse nested properties regardless of whether `type` is explicitly set.
120
+ if ('properties' in record && typeof record.properties === 'object' && record.properties !== null) {
121
+ const props = record.properties as Record<string, unknown>
122
+ for (const key in props) {
123
+ collectRefsFromValue(props[key])
124
+ }
125
+ }
126
+
127
+ // Traverse into type composition keywords
128
+ if (hasOneOf(record)) {
129
+ for (const item of record.oneOf) {
130
+ collectRefsFromValue(item)
131
+ }
132
+ }
133
+ if (hasAnyOf(record)) {
134
+ for (const item of record.anyOf) {
135
+ collectRefsFromValue(item)
136
+ }
137
+ }
138
+ if (hasAllOf(record)) {
139
+ for (const item of record.allOf) {
140
+ collectRefsFromValue(item)
141
+ }
142
+ }
143
+
144
+ // Traverse into array items
145
+ if (hasItems(record)) {
146
+ collectRefsFromValue(record.items)
147
+ }
148
+
149
+ // Traverse into additionalProperties
150
+ if (hasAdditionalProperties(record)) {
151
+ collectRefsFromValue(record.additionalProperties)
152
+ }
153
+
154
+ // Traverse all patternProperties
155
+ if (
156
+ 'patternProperties' in record &&
157
+ typeof record.patternProperties === 'object' &&
158
+ record.patternProperties !== null
159
+ ) {
160
+ const patternProps = record.patternProperties as Record<string, unknown>
161
+ for (const key in patternProps) {
162
+ collectRefsFromValue(patternProps[key])
163
+ }
164
+ }
165
+
166
+ // Traverse into if/then/else branches
167
+ if ('then' in record) {
168
+ collectRefsFromValue(record.then)
169
+ }
170
+ if ('else' in record) {
171
+ collectRefsFromValue(record.else)
172
+ }
173
+ }
174
+
175
+ // Collect refs from root-level $ref
176
+ if (typeof schema === 'object' && schema !== null && hasRef(schema)) {
177
+ const ref = schema.$ref
178
+ const isInternal = ref.startsWith('#')
179
+ const isUri = ref.startsWith('http://') || ref.startsWith('https://')
180
+ const isPropertyFragment = isUri && ref.includes('#/properties/')
181
+ if (isInternal || (isUri && !isPropertyFragment)) {
182
+ refs.add(ref)
183
+ }
184
+ }
185
+
186
+ // Collect refs from properties
187
+ if (typeof schema === 'object' && schema !== null && 'properties' in schema) {
188
+ const properties = schema.properties as Record<string, unknown>
189
+ for (const key in properties) {
190
+ collectRefsFromValue(properties[key])
191
+ }
192
+ }
193
+
194
+ // Collect refs from root-level additionalProperties
195
+ if (typeof schema === 'object' && schema !== null && 'additionalProperties' in schema) {
196
+ collectRefsFromValue(schema.additionalProperties)
197
+ }
198
+
199
+ // Collect refs from root-level patternProperties
200
+ if (typeof schema === 'object' && schema !== null && 'patternProperties' in schema) {
201
+ const patternProps = schema.patternProperties as Record<string, unknown>
202
+ for (const key in patternProps) {
203
+ collectRefsFromValue(patternProps[key])
204
+ }
205
+ }
206
+
207
+ // Collect refs from root-level items (when the schema itself is an array type)
208
+ if (typeof schema === 'object' && schema !== null && hasItems(schema)) {
209
+ collectRefsFromValue(schema.items)
210
+ }
211
+
212
+ // Collect refs from root-level composition keywords.
213
+ if (typeof schema === 'object' && schema !== null && hasOneOf(schema)) {
214
+ for (const item of schema.oneOf) {
215
+ collectRefsFromValue(item)
216
+ }
217
+ }
218
+ if (typeof schema === 'object' && schema !== null && hasAnyOf(schema)) {
219
+ for (const item of schema.anyOf) {
220
+ collectRefsFromValue(item)
221
+ }
222
+ }
223
+ if (typeof schema === 'object' && schema !== null && hasAllOf(schema)) {
224
+ for (const item of schema.allOf) {
225
+ collectRefsFromValue(item)
226
+ }
227
+ }
228
+
229
+ // Collect refs from root-level conditional branches.
230
+ if (typeof schema === 'object' && schema !== null && 'if' in schema) {
231
+ collectRefsFromValue(schema.if)
232
+ }
233
+ if (typeof schema === 'object' && schema !== null && 'then' in schema) {
234
+ collectRefsFromValue(schema.then)
235
+ }
236
+ if (typeof schema === 'object' && schema !== null && 'else' in schema) {
237
+ collectRefsFromValue(schema.else)
238
+ }
239
+
240
+ // Convert refs to combined import statements, deduplicating by filename
241
+ const importMap = new Map<string, string>()
242
+
243
+ for (const ref of refs) {
244
+ // For URI refs, skip if the base URI is not resolvable in the root schema.
245
+ // This prevents generating imports for external schemas that were never generated as files.
246
+ if (rootSchema !== undefined && (ref.startsWith('http://') || ref.startsWith('https://'))) {
247
+ if (!resolveRef(ref, rootSchema)) continue
248
+ }
249
+
250
+ const typeName = refToName(ref)
251
+ const filename = refToFilename(ref)
252
+
253
+ // Skip self-referential imports — a file must not import from itself
254
+ if (selfFilename !== undefined && filename === selfFilename) {
255
+ continue
256
+ }
257
+
258
+ const importPath = getImportPathForFilename(filename)
259
+
260
+ // In types-only mode, omit the parser function import since there is no parser to call
261
+ const importStatement = typesOnly
262
+ ? `import type { ${typeName} } from '${importPath}';`
263
+ : `import { type ${typeName}, parse${typeName}, validate${typeName}Shape } from '${importPath}';`
264
+ importMap.set(filename, importStatement)
265
+ }
266
+
267
+ return Array.from(importMap.values()).sort()
268
+ }
@@ -0,0 +1,88 @@
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
+ })
@@ -0,0 +1,39 @@
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
+ }
@@ -0,0 +1,135 @@
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
+ })
@@ -0,0 +1,89 @@
1
+ import {
2
+ hasAllOf,
3
+ hasAnyOf,
4
+ hasDefault,
5
+ hasEnum,
6
+ hasExamples,
7
+ hasOneOf,
8
+ hasPattern,
9
+ hasType,
10
+ isSchemaObject,
11
+ } from '@amritk/helpers/schema-guards'
12
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
13
+ import { generateDefaultFromPattern } from '#generators/generate-default-from-pattern'
14
+
15
+ /**
16
+ * Returns the default value for a JSON Schema property.
17
+ * Priority order: explicit default > first enum value > first example > union first schema > pattern-based > type-based.
18
+ * These defaults ensure that parsing never fails, even with missing data.
19
+ */
20
+ export const getDefaultValue = (schema: JSONSchema): string => {
21
+ if (!isSchemaObject(schema)) {
22
+ return 'undefined'
23
+ }
24
+
25
+ // Explicit default takes highest priority
26
+ if (hasDefault(schema)) {
27
+ return JSON.stringify(schema.default)
28
+ }
29
+
30
+ // Use first enum value if available
31
+ if (hasEnum(schema) && schema.enum.length > 0) {
32
+ return JSON.stringify(schema.enum[0])
33
+ }
34
+
35
+ // Use first example if available
36
+ if (hasExamples(schema) && schema.examples.length > 0) {
37
+ return JSON.stringify(schema.examples[0])
38
+ }
39
+
40
+ // Handle union types - use first schema's default
41
+ if (hasOneOf(schema) && schema.oneOf.length > 0) {
42
+ const firstSchema = schema.oneOf[0]
43
+ if (firstSchema !== undefined) {
44
+ return getDefaultValue(firstSchema)
45
+ }
46
+ }
47
+
48
+ if (hasAnyOf(schema) && schema.anyOf.length > 0) {
49
+ const firstSchema = schema.anyOf[0]
50
+ if (firstSchema !== undefined) {
51
+ return getDefaultValue(firstSchema)
52
+ }
53
+ }
54
+
55
+ // Handle allOf - use first schema's default
56
+ if (hasAllOf(schema) && schema.allOf.length > 0) {
57
+ const firstSchema = schema.allOf[0]
58
+ if (firstSchema !== undefined) {
59
+ return getDefaultValue(firstSchema)
60
+ }
61
+ }
62
+
63
+ if (!hasType(schema)) {
64
+ return 'undefined'
65
+ }
66
+
67
+ switch (schema.type) {
68
+ case 'string': {
69
+ if (hasPattern(schema)) {
70
+ const patternDefault = generateDefaultFromPattern(schema.pattern)
71
+ if (patternDefault) {
72
+ return patternDefault
73
+ }
74
+ }
75
+ return '""'
76
+ }
77
+ case 'number':
78
+ case 'integer':
79
+ return '0'
80
+ case 'boolean':
81
+ return 'false'
82
+ case 'array':
83
+ return '[]'
84
+ case 'object':
85
+ return '{}'
86
+ default:
87
+ return 'undefined'
88
+ }
89
+ }