@amritk/generate-parsers 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/generators/build-schema.d.ts +0 -1
- package/dist/generators/generate-default-from-pattern.d.ts +0 -1
- package/dist/generators/generate-discriminated-union-validation.d.ts +0 -1
- package/dist/generators/generate-enum-check.d.ts +0 -1
- package/dist/generators/generate-files.d.ts +0 -1
- package/dist/generators/generate-non-discriminated-union-validation.d.ts +0 -1
- package/dist/generators/generate-parser-function.d.ts +3 -3
- package/dist/generators/generate-parser-function.js +165 -37
- package/dist/generators/generate-schema-checks.d.ts +0 -1
- package/dist/generators/generate-schema-checks.js +2 -1
- package/dist/generators/generate-strict-assertion.d.ts +0 -1
- package/dist/generators/generate-strict-assertion.js +1 -6
- package/dist/generators/generate-validation-expression.d.ts +0 -1
- package/dist/generators/generate-validation-expression.js +2 -1
- package/dist/helpers/apply-schema-extensions.d.ts +0 -1
- package/dist/helpers/collect-helpers.d.ts +0 -1
- package/dist/helpers/collect-imports.d.ts +0 -1
- package/dist/helpers/find-discriminator.d.ts +0 -1
- package/dist/helpers/get-default-value.d.ts +0 -1
- package/dist/helpers/get-discriminator-value.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/types/schema-extensions.d.ts +0 -1
- package/package.json +3 -5
- package/dist/generators/build-schema.d.ts.map +0 -1
- package/dist/generators/generate-default-from-pattern.d.ts.map +0 -1
- package/dist/generators/generate-discriminated-union-validation.d.ts.map +0 -1
- package/dist/generators/generate-enum-check.d.ts.map +0 -1
- package/dist/generators/generate-files.d.ts.map +0 -1
- package/dist/generators/generate-non-discriminated-union-validation.d.ts.map +0 -1
- package/dist/generators/generate-parser-function.d.ts.map +0 -1
- package/dist/generators/generate-schema-checks.d.ts.map +0 -1
- package/dist/generators/generate-strict-assertion.d.ts.map +0 -1
- package/dist/generators/generate-validation-expression.d.ts.map +0 -1
- package/dist/helpers/apply-schema-extensions.d.ts.map +0 -1
- package/dist/helpers/collect-helpers.d.ts.map +0 -1
- package/dist/helpers/collect-imports.d.ts.map +0 -1
- package/dist/helpers/find-discriminator.d.ts.map +0 -1
- package/dist/helpers/get-default-value.d.ts.map +0 -1
- package/dist/helpers/get-discriminator-value.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/types/schema-extensions.d.ts.map +0 -1
- package/src/generators/build-schema.test.ts +0 -854
- package/src/generators/build-schema.ts +0 -147
- package/src/generators/generate-default-from-pattern.test.ts +0 -81
- package/src/generators/generate-default-from-pattern.ts +0 -39
- package/src/generators/generate-discriminated-union-validation.test.ts +0 -84
- package/src/generators/generate-discriminated-union-validation.ts +0 -54
- package/src/generators/generate-enum-check.test.ts +0 -45
- package/src/generators/generate-enum-check.ts +0 -8
- package/src/generators/generate-files.test.ts +0 -448
- package/src/generators/generate-files.ts +0 -155
- package/src/generators/generate-non-discriminated-union-validation.test.ts +0 -74
- package/src/generators/generate-non-discriminated-union-validation.ts +0 -40
- package/src/generators/generate-parser-function.test.ts +0 -1794
- package/src/generators/generate-parser-function.ts +0 -1348
- package/src/generators/generate-schema-checks.test.ts +0 -330
- package/src/generators/generate-schema-checks.ts +0 -127
- package/src/generators/generate-strict-assertion.test.ts +0 -67
- package/src/generators/generate-strict-assertion.ts +0 -249
- package/src/generators/generate-validation-expression.test.ts +0 -831
- package/src/generators/generate-validation-expression.ts +0 -326
- package/src/helpers/apply-schema-extensions.test.ts +0 -194
- package/src/helpers/apply-schema-extensions.ts +0 -63
- package/src/helpers/collect-helpers.test.ts +0 -115
- package/src/helpers/collect-helpers.ts +0 -79
- package/src/helpers/collect-imports.test.ts +0 -547
- package/src/helpers/collect-imports.ts +0 -275
- package/src/helpers/find-discriminator.test.ts +0 -88
- package/src/helpers/find-discriminator.ts +0 -39
- package/src/helpers/get-default-value.test.ts +0 -139
- package/src/helpers/get-default-value.ts +0 -95
- package/src/helpers/get-discriminator-value.test.ts +0 -83
- package/src/helpers/get-discriminator-value.ts +0 -21
- package/src/index.ts +0 -1
- package/src/types/schema-extensions.ts +0 -35
|
@@ -1,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
|
-
})
|