@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.
Files changed (76) hide show
  1. package/README.md +1 -1
  2. package/dist/generators/build-schema.d.ts +0 -1
  3. package/dist/generators/generate-default-from-pattern.d.ts +0 -1
  4. package/dist/generators/generate-discriminated-union-validation.d.ts +0 -1
  5. package/dist/generators/generate-enum-check.d.ts +0 -1
  6. package/dist/generators/generate-files.d.ts +0 -1
  7. package/dist/generators/generate-non-discriminated-union-validation.d.ts +0 -1
  8. package/dist/generators/generate-parser-function.d.ts +3 -3
  9. package/dist/generators/generate-parser-function.js +165 -37
  10. package/dist/generators/generate-schema-checks.d.ts +0 -1
  11. package/dist/generators/generate-schema-checks.js +2 -1
  12. package/dist/generators/generate-strict-assertion.d.ts +0 -1
  13. package/dist/generators/generate-strict-assertion.js +1 -6
  14. package/dist/generators/generate-validation-expression.d.ts +0 -1
  15. package/dist/generators/generate-validation-expression.js +2 -1
  16. package/dist/helpers/apply-schema-extensions.d.ts +0 -1
  17. package/dist/helpers/collect-helpers.d.ts +0 -1
  18. package/dist/helpers/collect-imports.d.ts +0 -1
  19. package/dist/helpers/find-discriminator.d.ts +0 -1
  20. package/dist/helpers/get-default-value.d.ts +0 -1
  21. package/dist/helpers/get-discriminator-value.d.ts +0 -1
  22. package/dist/index.d.ts +0 -1
  23. package/dist/types/schema-extensions.d.ts +0 -1
  24. package/package.json +3 -5
  25. package/dist/generators/build-schema.d.ts.map +0 -1
  26. package/dist/generators/generate-default-from-pattern.d.ts.map +0 -1
  27. package/dist/generators/generate-discriminated-union-validation.d.ts.map +0 -1
  28. package/dist/generators/generate-enum-check.d.ts.map +0 -1
  29. package/dist/generators/generate-files.d.ts.map +0 -1
  30. package/dist/generators/generate-non-discriminated-union-validation.d.ts.map +0 -1
  31. package/dist/generators/generate-parser-function.d.ts.map +0 -1
  32. package/dist/generators/generate-schema-checks.d.ts.map +0 -1
  33. package/dist/generators/generate-strict-assertion.d.ts.map +0 -1
  34. package/dist/generators/generate-validation-expression.d.ts.map +0 -1
  35. package/dist/helpers/apply-schema-extensions.d.ts.map +0 -1
  36. package/dist/helpers/collect-helpers.d.ts.map +0 -1
  37. package/dist/helpers/collect-imports.d.ts.map +0 -1
  38. package/dist/helpers/find-discriminator.d.ts.map +0 -1
  39. package/dist/helpers/get-default-value.d.ts.map +0 -1
  40. package/dist/helpers/get-discriminator-value.d.ts.map +0 -1
  41. package/dist/index.d.ts.map +0 -1
  42. package/dist/types/schema-extensions.d.ts.map +0 -1
  43. package/src/generators/build-schema.test.ts +0 -854
  44. package/src/generators/build-schema.ts +0 -147
  45. package/src/generators/generate-default-from-pattern.test.ts +0 -81
  46. package/src/generators/generate-default-from-pattern.ts +0 -39
  47. package/src/generators/generate-discriminated-union-validation.test.ts +0 -84
  48. package/src/generators/generate-discriminated-union-validation.ts +0 -54
  49. package/src/generators/generate-enum-check.test.ts +0 -45
  50. package/src/generators/generate-enum-check.ts +0 -8
  51. package/src/generators/generate-files.test.ts +0 -448
  52. package/src/generators/generate-files.ts +0 -155
  53. package/src/generators/generate-non-discriminated-union-validation.test.ts +0 -74
  54. package/src/generators/generate-non-discriminated-union-validation.ts +0 -40
  55. package/src/generators/generate-parser-function.test.ts +0 -1794
  56. package/src/generators/generate-parser-function.ts +0 -1348
  57. package/src/generators/generate-schema-checks.test.ts +0 -330
  58. package/src/generators/generate-schema-checks.ts +0 -127
  59. package/src/generators/generate-strict-assertion.test.ts +0 -67
  60. package/src/generators/generate-strict-assertion.ts +0 -249
  61. package/src/generators/generate-validation-expression.test.ts +0 -831
  62. package/src/generators/generate-validation-expression.ts +0 -326
  63. package/src/helpers/apply-schema-extensions.test.ts +0 -194
  64. package/src/helpers/apply-schema-extensions.ts +0 -63
  65. package/src/helpers/collect-helpers.test.ts +0 -115
  66. package/src/helpers/collect-helpers.ts +0 -79
  67. package/src/helpers/collect-imports.test.ts +0 -547
  68. package/src/helpers/collect-imports.ts +0 -275
  69. package/src/helpers/find-discriminator.test.ts +0 -88
  70. package/src/helpers/find-discriminator.ts +0 -39
  71. package/src/helpers/get-default-value.test.ts +0 -139
  72. package/src/helpers/get-default-value.ts +0 -95
  73. package/src/helpers/get-discriminator-value.test.ts +0 -83
  74. package/src/helpers/get-discriminator-value.ts +0 -21
  75. package/src/index.ts +0 -1
  76. package/src/types/schema-extensions.ts +0 -35
@@ -1,326 +0,0 @@
1
- import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension'
2
- import { resolveRef } from '@amritk/helpers/resolve-ref'
3
- import { safeAccessor } from '@amritk/helpers/safe-accessor'
4
- import {
5
- hasAdditionalProperties,
6
- hasAllOf,
7
- hasAnyOf,
8
- hasConst,
9
- hasEnum,
10
- hasExclusiveMaximum,
11
- hasExclusiveMinimum,
12
- hasMaxItems,
13
- hasMaximum,
14
- hasMaxLength,
15
- hasMaxProperties,
16
- hasMinItems,
17
- hasMinimum,
18
- hasMinLength,
19
- hasMinProperties,
20
- hasMultipleOf,
21
- hasOneOf,
22
- hasPattern,
23
- hasProperties,
24
- hasRef,
25
- hasRequired,
26
- hasType,
27
- hasUniqueItems,
28
- isSchemaObject,
29
- } from '@amritk/helpers/schema-guards'
30
- import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
31
- import { findDiscriminator } from '#helpers/find-discriminator'
32
-
33
- import { generateDiscriminatedUnionValidation } from './generate-discriminated-union-validation'
34
- import { generateEnumCheck } from './generate-enum-check'
35
- import { generateNonDiscriminatedUnionValidation } from './generate-non-discriminated-union-validation'
36
- import { generateSchemaChecks } from './generate-schema-checks'
37
-
38
- /**
39
- * Coercion expression for an `x-mjst` instanceOf value, when one is known.
40
- * `Date` can be reconstructed from the common JSON encodings (ISO string or
41
- * epoch number); other classes have no safe generic coercion, so we return null
42
- * and the caller falls back to the default.
43
- */
44
- const getInstanceCoercion = (accessor: string, instanceOf: string): string | null => {
45
- if (instanceOf === 'Date') return `new Date(${accessor} as string | number | Date)`
46
- return null
47
- }
48
-
49
- /**
50
- * Generates a type coercion expression for converting a value to the expected type.
51
- */
52
- const getTypeCoercion = (accessor: string, schema: JSONSchema, defaultValue: string): string | null => {
53
- if (!hasType(schema)) {
54
- return null
55
- }
56
-
57
- switch (schema.type) {
58
- case 'string':
59
- return `String(${accessor})`
60
- case 'number':
61
- case 'integer': {
62
- // Number() of a non-numeric string produces NaN, which silently poisons
63
- // arithmetic. Guard with Number.isFinite and fall back to the default.
64
- const n = `Number(${accessor})`
65
- return `(Number.isFinite(${n}) ? ${n} : ${defaultValue})`
66
- }
67
- case 'boolean':
68
- return `Boolean(${accessor})`
69
- case 'array':
70
- return `[]`
71
- case 'object':
72
- return `typeof ${accessor} === "object" && ${accessor} !== null ? ${accessor} : {}`
73
- default:
74
- return null
75
- }
76
- }
77
-
78
- /**
79
- * Generates a validation expression that checks if a value matches the expected type.
80
- * Returns the value if valid, coerces to the correct type if invalid, or uses default if required and missing.
81
- * Supports type checking, pattern validation, enum validation, union types, and $ref resolution.
82
- */
83
- export const generateValidationExpression = (
84
- key: string,
85
- schema: JSONSchema,
86
- defaultValue: string,
87
- isRequired: boolean,
88
- rootSchema?: Record<string, unknown>,
89
- visitedRefs?: Set<string>,
90
- accessorOverride?: string,
91
- knownNotUndefined?: boolean,
92
- ): string => {
93
- const accessor = accessorOverride ?? safeAccessor('input?', key)
94
- const checks: string[] = []
95
-
96
- if (!isSchemaObject(schema)) {
97
- return isRequired ? `${accessor} ?? ${defaultValue}` : accessor
98
- }
99
-
100
- // Handle x-mjst instanceOf (e.g. Date): the value must be an instance of the
101
- // named class. Invalid values are coerced when a coercer is known, otherwise
102
- // they fall back to the default (required) or undefined (optional).
103
- const instanceOf = getMjstInstanceOf(schema)
104
- if (instanceOf) {
105
- const valid = `${accessor} instanceof ${instanceOf}`
106
- const coercion = getInstanceCoercion(accessor, instanceOf)
107
-
108
- if (isRequired) {
109
- return coercion
110
- ? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion} : ${defaultValue})`
111
- : `${valid} ? ${accessor} : ${defaultValue}`
112
- }
113
- return coercion
114
- ? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion} : undefined)`
115
- : `${valid} ? ${accessor} : undefined`
116
- }
117
-
118
- // Handle x-mjst primitive (e.g. bigint): validate with typeof. We do not
119
- // coerce — BigInt() throws on invalid input, which a pure coercion expression
120
- // cannot guard — so invalid values fall back to the default (required) or
121
- // undefined (optional).
122
- const primitive = getMjstPrimitive(schema)
123
- if (primitive) {
124
- const valid = `typeof ${accessor} === "${primitive}"`
125
- return isRequired ? `${valid} ? ${accessor} : ${defaultValue}` : `${valid} ? ${accessor} : undefined`
126
- }
127
-
128
- // Handle $ref - resolve and validate against the referenced schema
129
- if (hasRef(schema) && rootSchema) {
130
- const ref = schema.$ref
131
-
132
- // If we have already visited this ref, we have a circular reference - return accessor to break cycle
133
- if (visitedRefs?.has(ref)) {
134
- return isRequired ? `${accessor} ?? ${defaultValue}` : accessor
135
- }
136
-
137
- const resolvedSchema = resolveRef(ref, rootSchema)
138
- if (resolvedSchema) {
139
- const visited = new Set(visitedRefs)
140
- visited.add(ref)
141
-
142
- return generateValidationExpression(key, resolvedSchema, defaultValue, isRequired, rootSchema, visited)
143
- }
144
- }
145
-
146
- // Handle union types (oneOf, anyOf)
147
- if (hasOneOf(schema) && schema.oneOf.length > 0) {
148
- const discriminator = findDiscriminator(schema.oneOf)
149
- if (discriminator) {
150
- return generateDiscriminatedUnionValidation(accessor, schema.oneOf, discriminator, defaultValue, isRequired)
151
- }
152
- return generateNonDiscriminatedUnionValidation(accessor, schema.oneOf, defaultValue, isRequired)
153
- }
154
-
155
- if (hasAnyOf(schema) && schema.anyOf.length > 0) {
156
- const discriminator = findDiscriminator(schema.anyOf)
157
- if (discriminator) {
158
- return generateDiscriminatedUnionValidation(accessor, schema.anyOf, discriminator, defaultValue, isRequired)
159
- }
160
- return generateNonDiscriminatedUnionValidation(accessor, schema.anyOf, defaultValue, isRequired)
161
- }
162
-
163
- // Handle allOf (intersection - all schemas must match)
164
- if (hasAllOf(schema) && schema.allOf.length > 0) {
165
- const allChecks: string[] = []
166
- for (const subSchema of schema.allOf) {
167
- const subChecks = generateSchemaChecks(accessor, subSchema)
168
- allChecks.push(...subChecks)
169
- }
170
-
171
- if (allChecks.length > 0) {
172
- let combinedCheck = allChecks[0]
173
- for (let i = 1; i < allChecks.length; i++) {
174
- combinedCheck += ' && ' + allChecks[i]
175
- }
176
- return `${combinedCheck} ? ${accessor} : ${defaultValue}`
177
- }
178
- return `${accessor} ?? ${defaultValue}`
179
- }
180
-
181
- // Handle not (negation - value must NOT match this schema)
182
- if ('not' in schema && schema.not) {
183
- const notChecks = generateSchemaChecks(accessor, schema.not)
184
- if (notChecks.length > 0) {
185
- let combinedNotCheck = notChecks[0]
186
- for (let i = 1; i < notChecks.length; i++) {
187
- combinedNotCheck += ' && ' + notChecks[i]
188
- }
189
- return `!(${combinedNotCheck}) ? ${accessor} : ${defaultValue}`
190
- }
191
- }
192
-
193
- // Handle const — single exact value that must match
194
- if (hasConst(schema)) {
195
- const constLiteral = JSON.stringify(schema.const)
196
- return isRequired
197
- ? `${accessor} === ${constLiteral} ? ${accessor} : ${constLiteral}`
198
- : `${accessor} === ${constLiteral} ? ${accessor} : (${accessor} !== undefined ? ${constLiteral} : undefined)`
199
- }
200
-
201
- if (!hasType(schema) && !hasEnum(schema)) {
202
- return `${accessor} ?? ${defaultValue}`
203
- }
204
-
205
- // Build type-specific checks
206
- if (hasType(schema)) {
207
- switch (schema.type) {
208
- case 'string': {
209
- checks.push(`typeof ${accessor} === "string"`)
210
- if (hasPattern(schema)) {
211
- checks.push(`/${schema.pattern.replace(/\//g, '\\/')}/.test(${accessor})`)
212
- }
213
- if (hasMinLength(schema)) {
214
- checks.push(`${accessor}.length >= ${schema.minLength}`)
215
- }
216
- if (hasMaxLength(schema)) {
217
- checks.push(`${accessor}.length <= ${schema.maxLength}`)
218
- }
219
- break
220
- }
221
- case 'number':
222
- case 'integer': {
223
- checks.push(`typeof ${accessor} === "number"`)
224
- if (hasMinimum(schema)) {
225
- checks.push(`${accessor} >= ${schema.minimum}`)
226
- }
227
- if (hasMaximum(schema)) {
228
- checks.push(`${accessor} <= ${schema.maximum}`)
229
- }
230
- if (hasExclusiveMinimum(schema)) {
231
- checks.push(`${accessor} > ${schema.exclusiveMinimum}`)
232
- }
233
- if (hasExclusiveMaximum(schema)) {
234
- checks.push(`${accessor} < ${schema.exclusiveMaximum}`)
235
- }
236
- if (hasMultipleOf(schema)) {
237
- checks.push(`${accessor} % ${schema.multipleOf} === 0`)
238
- }
239
- break
240
- }
241
- case 'boolean':
242
- checks.push(`typeof ${accessor} === "boolean"`)
243
- break
244
- case 'array': {
245
- checks.push(`Array.isArray(${accessor})`)
246
- if (hasMinItems(schema)) {
247
- checks.push(`${accessor}.length >= ${schema.minItems}`)
248
- }
249
- if (hasMaxItems(schema)) {
250
- checks.push(`${accessor}.length <= ${schema.maxItems}`)
251
- }
252
- if (hasUniqueItems(schema) && schema.uniqueItems === true) {
253
- checks.push(`new Set(${accessor}).size === ${accessor}.length`)
254
- }
255
- break
256
- }
257
- case 'object': {
258
- checks.push(`isObject(${accessor})`)
259
- // Check required properties exist
260
- if (hasRequired(schema) && schema.required.length > 0) {
261
- for (const requiredKey of schema.required) {
262
- checks.push(`"${requiredKey}" in ${accessor}`)
263
- }
264
- }
265
- // Check minProperties
266
- if (hasMinProperties(schema)) {
267
- checks.push(`Object.keys(${accessor}).length >= ${schema.minProperties}`)
268
- }
269
- // Check maxProperties
270
- if (hasMaxProperties(schema)) {
271
- checks.push(`Object.keys(${accessor}).length <= ${schema.maxProperties}`)
272
- }
273
- // Check additionalProperties
274
- if (hasAdditionalProperties(schema) && schema.additionalProperties === false && hasProperties(schema)) {
275
- const allowedKeys = Object.keys(schema.properties)
276
- const allowedKeysStr = JSON.stringify(allowedKeys)
277
- checks.push(`Object.keys(${accessor}).every(k => ${allowedKeysStr}.includes(k))`)
278
- }
279
- break
280
- }
281
- }
282
- }
283
-
284
- // Add enum check if present
285
- if (hasEnum(schema) && schema.enum.length > 0) {
286
- checks.push(generateEnumCheck(accessor, schema.enum))
287
- }
288
-
289
- // Combine all checks
290
- if (checks.length === 0) {
291
- return isRequired ? `${accessor} ?? ${defaultValue}` : accessor
292
- }
293
-
294
- let combinedCheck = checks[0]
295
- for (let i = 1; i < checks.length; i++) {
296
- combinedCheck += ' && ' + checks[i]
297
- }
298
-
299
- // Generate the fallback value
300
- // If the value exists but fails validation, try to coerce it
301
- // If the value is missing and required, use the default
302
- const typeCoercion = getTypeCoercion(accessor, schema, defaultValue)
303
-
304
- if (isRequired) {
305
- // For required fields: valid ? use_value : (exists ? coerce : default)
306
- if (typeCoercion) {
307
- // If we know the value is not undefined (e.g., we're inside an undefined check),
308
- // we can skip the redundant undefined check
309
- if (knownNotUndefined) {
310
- return `${combinedCheck} ? ${accessor} : ${typeCoercion}`
311
- }
312
- return `${combinedCheck} ? ${accessor} : (${accessor} !== undefined ? ${typeCoercion} : ${defaultValue})`
313
- }
314
- return `${combinedCheck} ? ${accessor} : ${defaultValue}`
315
- } else {
316
- // For optional fields: valid ? use_value : (exists ? coerce : undefined)
317
- if (typeCoercion) {
318
- // If we know the value is not undefined, we can skip the check and just coerce
319
- if (knownNotUndefined) {
320
- return `${combinedCheck} ? ${accessor} : ${typeCoercion}`
321
- }
322
- return `${combinedCheck} ? ${accessor} : (${accessor} !== undefined ? ${typeCoercion} : undefined)`
323
- }
324
- return `${combinedCheck} ? ${accessor} : undefined`
325
- }
326
- }
@@ -1,194 +0,0 @@
1
- import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
2
- import { describe, expect, it } from 'vitest'
3
-
4
- import { applySchemaExtensions } from './apply-schema-extensions'
5
-
6
- describe('apply-schema-extensions', () => {
7
- it('merges extension properties into schema properties', () => {
8
- const schema: JSONSchema = {
9
- type: 'object',
10
- properties: {
11
- name: { type: 'string' },
12
- },
13
- required: ['name'],
14
- }
15
-
16
- const result = applySchemaExtensions(schema, 'parameter', {
17
- parameter: {
18
- 'x-enabled': { type: 'boolean' },
19
- },
20
- })
21
-
22
- expect(result).toEqual({
23
- type: 'object',
24
- properties: {
25
- name: { type: 'string' },
26
- 'x-enabled': { type: 'boolean' },
27
- },
28
- required: ['name'],
29
- })
30
- })
31
-
32
- it('returns original schema when no extensions match the definition name', () => {
33
- const schema: JSONSchema = {
34
- type: 'object',
35
- properties: {
36
- name: { type: 'string' },
37
- },
38
- }
39
-
40
- const result = applySchemaExtensions(schema, 'parameter', {
41
- operation: {
42
- 'x-codegen': { type: 'string' },
43
- },
44
- })
45
-
46
- expect(result).toBe(schema)
47
- })
48
-
49
- it('returns original schema when extensions record is empty', () => {
50
- const schema: JSONSchema = {
51
- type: 'object',
52
- properties: {
53
- name: { type: 'string' },
54
- },
55
- }
56
-
57
- const result = applySchemaExtensions(schema, 'parameter', {})
58
-
59
- expect(result).toBe(schema)
60
- })
61
-
62
- it('returns original schema when matching extensions have no properties', () => {
63
- const schema: JSONSchema = {
64
- type: 'object',
65
- properties: {
66
- name: { type: 'string' },
67
- },
68
- }
69
-
70
- const result = applySchemaExtensions(schema, 'parameter', {
71
- parameter: {},
72
- })
73
-
74
- expect(result).toBe(schema)
75
- })
76
-
77
- it('returns boolean schema unchanged', () => {
78
- const result = applySchemaExtensions(true, 'parameter', {
79
- parameter: {
80
- 'x-enabled': { type: 'boolean' },
81
- },
82
- })
83
-
84
- expect(result).toBe(true)
85
- })
86
-
87
- it('adds properties to a schema that has no existing properties', () => {
88
- const schema: JSONSchema = {
89
- type: 'object',
90
- }
91
-
92
- const result = applySchemaExtensions(schema, 'parameter', {
93
- parameter: {
94
- 'x-enabled': { type: 'boolean' },
95
- },
96
- })
97
-
98
- expect(result).toEqual({
99
- type: 'object',
100
- properties: {
101
- 'x-enabled': { type: 'boolean' },
102
- },
103
- })
104
- })
105
-
106
- it('merges multiple extension properties', () => {
107
- const schema: JSONSchema = {
108
- type: 'object',
109
- properties: {
110
- name: { type: 'string' },
111
- },
112
- }
113
-
114
- const result = applySchemaExtensions(schema, 'parameter', {
115
- parameter: {
116
- 'x-enabled': { type: 'boolean' },
117
- 'x-internal': { type: 'boolean', default: false },
118
- 'x-display-name': { type: 'string' },
119
- },
120
- })
121
-
122
- const properties = (result as Record<string, unknown>)['properties'] as Record<string, unknown>
123
- expect(Object.keys(properties)).toEqual(['name', 'x-enabled', 'x-internal', 'x-display-name'])
124
- })
125
-
126
- it('does not modify the required array', () => {
127
- const schema: JSONSchema = {
128
- type: 'object',
129
- properties: {
130
- name: { type: 'string' },
131
- },
132
- required: ['name'],
133
- }
134
-
135
- const result = applySchemaExtensions(schema, 'parameter', {
136
- parameter: {
137
- 'x-enabled': { type: 'boolean' },
138
- },
139
- })
140
-
141
- expect((result as Record<string, unknown>)['required']).toEqual(['name'])
142
- })
143
-
144
- it('handles complex extension schemas', () => {
145
- const schema: JSONSchema = {
146
- type: 'object',
147
- properties: {
148
- name: { type: 'string' },
149
- },
150
- }
151
-
152
- const result = applySchemaExtensions(schema, 'operation', {
153
- operation: {
154
- 'x-codegen': {
155
- type: 'object',
156
- properties: {
157
- methodName: { type: 'string' },
158
- deprecated: { type: 'boolean' },
159
- },
160
- required: ['methodName'],
161
- },
162
- },
163
- })
164
-
165
- const properties = (result as Record<string, unknown>)['properties'] as Record<string, JSONSchema>
166
- expect(properties['x-codegen']).toEqual({
167
- type: 'object',
168
- properties: {
169
- methodName: { type: 'string' },
170
- deprecated: { type: 'boolean' },
171
- },
172
- required: ['methodName'],
173
- })
174
- })
175
-
176
- it('does not mutate the original schema', () => {
177
- const schema: JSONSchema = {
178
- type: 'object',
179
- properties: {
180
- name: { type: 'string' },
181
- },
182
- }
183
-
184
- const originalProperties = { ...((schema as Record<string, unknown>)['properties'] as Record<string, unknown>) }
185
-
186
- applySchemaExtensions(schema, 'parameter', {
187
- parameter: {
188
- 'x-enabled': { type: 'boolean' },
189
- },
190
- })
191
-
192
- expect((schema as Record<string, unknown>)['properties']).toEqual(originalProperties)
193
- })
194
- })
@@ -1,63 +0,0 @@
1
- import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
2
- import type { SchemaExtensions } from '#types/schema-extensions'
3
-
4
- /**
5
- * Merges custom extension properties into a schema's properties based on the
6
- * definition name. Extension properties are added as optional (not included
7
- * in the required array) since they are custom additions that may not always
8
- * be present.
9
- *
10
- * If the schema is not an object, has no matching extensions, or the
11
- * extensions record is empty, the original schema is returned unchanged.
12
- *
13
- * @param schema - The JSON Schema to extend
14
- * @param definitionName - The name of the definition (e.g., "parameter")
15
- * @param extensions - The extensions configuration mapping definition names to properties
16
- * @returns A new schema with extension properties merged in, or the original if unchanged
17
- *
18
- * @example
19
- * ```typescript
20
- * const schema = {
21
- * type: 'object',
22
- * properties: { name: { type: 'string' } },
23
- * required: ['name'],
24
- * }
25
- *
26
- * const extensions = {
27
- * parameter: {
28
- * 'x-enabled': { type: 'boolean' },
29
- * },
30
- * }
31
- *
32
- * const extended = applySchemaExtensions(schema, 'parameter', extensions)
33
- * // extended.properties now includes 'x-enabled' alongside 'name'
34
- * ```
35
- */
36
- export const applySchemaExtensions = (
37
- schema: JSONSchema,
38
- definitionName: string,
39
- extensions: SchemaExtensions,
40
- ): JSONSchema => {
41
- const extensionProperties = extensions[definitionName]
42
-
43
- if (!extensionProperties || Object.keys(extensionProperties).length === 0) {
44
- return schema
45
- }
46
-
47
- if (typeof schema !== 'object' || schema === null) {
48
- return schema
49
- }
50
-
51
- const existingProperties =
52
- 'properties' in schema && typeof schema.properties === 'object' && schema.properties !== null
53
- ? (schema.properties as Record<string, JSONSchema>)
54
- : {}
55
-
56
- return {
57
- ...schema,
58
- properties: {
59
- ...existingProperties,
60
- ...extensionProperties,
61
- },
62
- }
63
- }
@@ -1,115 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { collectHelpers } from './collect-helpers'
4
-
5
- describe('collect-helpers', () => {
6
- describe('package mode', () => {
7
- it('returns validateArray import when parser contains validateArray', () => {
8
- const result = collectHelpers('const items = validateArray(input, parseItem)', 'package')
9
- expect(result.imports).toEqual(["import { validateArray } from '@amritk/helpers/validate-array';"])
10
- expect(result.used).toEqual(new Set(['validate-array']))
11
- })
12
-
13
- it('returns validateRecord import (and transitively flags is-object) for embedded shipping', () => {
14
- const result = collectHelpers('const record = validateRecord(input, parseValue)', 'package')
15
- expect(result.imports).toEqual(["import { validateRecord } from '@amritk/helpers/validate-record';"])
16
- expect(result.used).toEqual(new Set(['validate-record', 'is-object']))
17
- })
18
-
19
- it('returns isObject import when parser contains isObject', () => {
20
- const result = collectHelpers('if (isObject(input)) {', 'package')
21
- expect(result.imports).toEqual(["import { isObject } from '@amritk/helpers/is-object';"])
22
- expect(result.used).toEqual(new Set(['is-object']))
23
- })
24
-
25
- it('returns hasRef import from schema-guards when parser contains hasRef(', () => {
26
- const result = collectHelpers('if (hasRef(s)) { return parseFoo(input) }', 'package')
27
- expect(result.imports).toEqual(["import { hasRef } from '@amritk/helpers/schema-guards';"])
28
- expect(result.used).toEqual(new Set(['has-ref']))
29
- })
30
-
31
- it('returns all imports when parser uses every helper', () => {
32
- const parser = `
33
- const arr = validateArray(input.items, parseItem)
34
- const rec = validateRecord(input.map, parseValue)
35
- if (isObject(input)) { return input }
36
- if (hasRef(schema)) { /* */ }
37
- `
38
- const result = collectHelpers(parser, 'package')
39
- expect(result.imports).toHaveLength(4)
40
- expect(result.imports).toContain("import { validateArray } from '@amritk/helpers/validate-array';")
41
- expect(result.imports).toContain("import { validateRecord } from '@amritk/helpers/validate-record';")
42
- expect(result.imports).toContain("import { isObject } from '@amritk/helpers/is-object';")
43
- expect(result.imports).toContain("import { hasRef } from '@amritk/helpers/schema-guards';")
44
- expect(result.used).toEqual(new Set(['validate-array', 'validate-record', 'is-object', 'has-ref']))
45
- })
46
-
47
- it('returns empty result when parser uses no helpers', () => {
48
- const result = collectHelpers('const x = input.name ?? ""', 'package')
49
- expect(result.imports).toEqual([])
50
- expect(result.used.size).toBe(0)
51
- })
52
-
53
- it('handles empty parser string', () => {
54
- const result = collectHelpers('', 'package')
55
- expect(result.imports).toEqual([])
56
- expect(result.used.size).toBe(0)
57
- })
58
-
59
- it('matches helpers as substrings in longer identifiers (existing .includes() behaviour)', () => {
60
- const result = collectHelpers('const validateArrayItems = true', 'package')
61
- expect(result.imports).toHaveLength(1)
62
- expect(result.used.has('validate-array')).toBe(true)
63
- })
64
- })
65
-
66
- describe('embedded mode', () => {
67
- it('emits relative imports targeting ./_helpers/', () => {
68
- const parser = `
69
- const arr = validateArray(input.items, parseItem)
70
- const rec = validateRecord(input.map, parseValue)
71
- if (isObject(input)) { return input }
72
- if (hasRef(schema)) { /* */ }
73
- `
74
- const result = collectHelpers(parser, 'embedded')
75
- expect(result.imports).toContain("import { validateArray } from './_helpers/validate-array';")
76
- expect(result.imports).toContain("import { validateRecord } from './_helpers/validate-record';")
77
- expect(result.imports).toContain("import { isObject } from './_helpers/is-object';")
78
- expect(result.imports).toContain("import { hasRef } from './_helpers/has-ref';")
79
- })
80
-
81
- it('marks is-object as used when validateRecord is referenced (transitive dep)', () => {
82
- // validate-record.ts imports isObject, so the embedded build must ship is-object too,
83
- // even if the generated parser body itself never mentions isObject directly.
84
- const result = collectHelpers('const record = validateRecord(input, parseValue)', 'embedded')
85
- expect(result.used.has('validate-record')).toBe(true)
86
- expect(result.used.has('is-object')).toBe(true)
87
- })
88
-
89
- it('does not duplicate the isObject import line when only validateRecord is referenced', () => {
90
- const result = collectHelpers('const record = validateRecord(input, parseValue)', 'embedded')
91
- expect(result.imports).toEqual(["import { validateRecord } from './_helpers/validate-record';"])
92
- })
93
-
94
- it('emits both validateRecord and isObject import lines when both names appear', () => {
95
- const result = collectHelpers(
96
- 'if (!isObject(input)) return {}; const r = validateRecord(input, parse)',
97
- 'embedded',
98
- )
99
- expect(result.imports).toContain("import { validateRecord } from './_helpers/validate-record';")
100
- expect(result.imports).toContain("import { isObject } from './_helpers/is-object';")
101
- })
102
-
103
- it('applies a custom import prefix so nested parsers can reach a shared _helpers/ at the root', () => {
104
- const parser = 'if (isObject(input)) { const arr = validateArray(input.items, parseItem) }'
105
- const result = collectHelpers(parser, 'embedded', '../../')
106
- expect(result.imports).toContain("import { isObject } from '../../_helpers/is-object';")
107
- expect(result.imports).toContain("import { validateArray } from '../../_helpers/validate-array';")
108
- })
109
-
110
- it('ignores the import prefix in package mode', () => {
111
- const result = collectHelpers('if (isObject(input)) {', 'package', '../')
112
- expect(result.imports).toEqual(["import { isObject } from '@amritk/helpers/is-object';"])
113
- })
114
- })
115
- })