@amritk/generate-parsers 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/generators/build-schema.d.ts +8 -1
- package/dist/generators/generate-files.d.ts +20 -2
- package/dist/generators/generate-parser-function.d.ts +6 -0
- package/dist/generators/generate-strict-assertion.d.ts +18 -0
- package/dist/helpers/collect-helpers.d.ts +16 -2
- package/dist/index.js +261 -50
- package/package.json +6 -2
- package/src/generators/build-schema.test.ts +830 -0
- package/src/generators/build-schema.ts +280 -0
- package/src/generators/generate-default-from-pattern.test.ts +81 -0
- package/src/generators/generate-default-from-pattern.ts +39 -0
- package/src/generators/generate-discriminated-union-validation.test.ts +84 -0
- package/src/generators/generate-discriminated-union-validation.ts +54 -0
- package/src/generators/generate-enum-check.test.ts +45 -0
- package/src/generators/generate-enum-check.ts +8 -0
- package/src/generators/generate-files.test.ts +435 -0
- package/src/generators/generate-files.ts +136 -0
- package/src/generators/generate-non-discriminated-union-validation.test.ts +74 -0
- package/src/generators/generate-non-discriminated-union-validation.ts +40 -0
- package/src/generators/generate-parser-function.test.ts +1798 -0
- package/src/generators/generate-parser-function.ts +1319 -0
- package/src/generators/generate-schema-checks.test.ts +322 -0
- package/src/generators/generate-schema-checks.ts +114 -0
- package/src/generators/generate-strict-assertion.ts +210 -0
- package/src/generators/generate-validation-expression.test.ts +794 -0
- package/src/generators/generate-validation-expression.ts +286 -0
- package/src/helpers/apply-schema-extensions.test.ts +194 -0
- package/src/helpers/apply-schema-extensions.ts +63 -0
- package/src/helpers/collect-helpers.test.ts +103 -0
- package/src/helpers/collect-helpers.ts +64 -0
- package/src/helpers/collect-imports.test.ts +587 -0
- package/src/helpers/collect-imports.ts +268 -0
- package/src/helpers/find-discriminator.test.ts +88 -0
- package/src/helpers/find-discriminator.ts +39 -0
- package/src/helpers/get-default-value.test.ts +135 -0
- package/src/helpers/get-default-value.ts +89 -0
- package/src/helpers/get-discriminator-value.test.ts +83 -0
- package/src/helpers/get-discriminator-value.ts +21 -0
- package/src/index.ts +1 -0
- package/src/types/schema-extensions.ts +35 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { generateSchemaChecks } from './generate-schema-checks'
|
|
4
|
+
|
|
5
|
+
describe('generate-schema-checks', () => {
|
|
6
|
+
it('returns empty checks for a schema without type and no inferrable keywords', () => {
|
|
7
|
+
const result = generateSchemaChecks('value', { description: 'no type' })
|
|
8
|
+
expect(result).toEqual([])
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it.todo('infers object type from properties keyword', () => {
|
|
12
|
+
const result = generateSchemaChecks('value', {
|
|
13
|
+
properties: { id: { type: 'string' } },
|
|
14
|
+
})
|
|
15
|
+
expect(result).toEqual(['typeof value === "object" && value !== null && !Array.isArray(value)'])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it.todo('infers object type from required keyword', () => {
|
|
19
|
+
const result = generateSchemaChecks('value', {
|
|
20
|
+
required: ['id', 'name'],
|
|
21
|
+
})
|
|
22
|
+
expect(result).toEqual([
|
|
23
|
+
'typeof value === "object" && value !== null && !Array.isArray(value)',
|
|
24
|
+
'"id" in value',
|
|
25
|
+
'"name" in value',
|
|
26
|
+
])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it.todo('infers array type from items keyword', () => {
|
|
30
|
+
const result = generateSchemaChecks('value', {
|
|
31
|
+
items: { type: 'string' },
|
|
32
|
+
})
|
|
33
|
+
expect(result).toEqual(['Array.isArray(value)'])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it.todo('infers array type from minItems/maxItems keywords', () => {
|
|
37
|
+
const result = generateSchemaChecks('value', {
|
|
38
|
+
minItems: 1,
|
|
39
|
+
maxItems: 10,
|
|
40
|
+
})
|
|
41
|
+
expect(result).toEqual(['Array.isArray(value)', 'value.length >= 1', 'value.length <= 10'])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it.todo('infers string type from minLength keyword', () => {
|
|
45
|
+
const result = generateSchemaChecks('value', {
|
|
46
|
+
minLength: 1,
|
|
47
|
+
})
|
|
48
|
+
expect(result).toEqual(['typeof value === "string"', 'value.length >= 1'])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it.todo('infers string type from pattern keyword', () => {
|
|
52
|
+
const result = generateSchemaChecks('value', {
|
|
53
|
+
pattern: '^[a-z]+$',
|
|
54
|
+
})
|
|
55
|
+
expect(result).toEqual(['typeof value === "string"', '/^[a-z]+$/.test(value)'])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it.todo('infers number type from minimum keyword', () => {
|
|
59
|
+
const result = generateSchemaChecks('value', {
|
|
60
|
+
minimum: 0,
|
|
61
|
+
})
|
|
62
|
+
expect(result).toEqual(['typeof value === "number"', 'value >= 0'])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it.todo('infers number type from multipleOf keyword', () => {
|
|
66
|
+
const result = generateSchemaChecks('value', {
|
|
67
|
+
multipleOf: 5,
|
|
68
|
+
})
|
|
69
|
+
expect(result).toEqual(['typeof value === "number"', 'value % 5 === 0'])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it.todo('infers boolean type from const boolean value', () => {
|
|
73
|
+
const result = generateSchemaChecks('value', {
|
|
74
|
+
const: true,
|
|
75
|
+
})
|
|
76
|
+
expect(result).toEqual(['typeof value === "boolean"'])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it.todo('infers null type from const null value', () => {
|
|
80
|
+
const result = generateSchemaChecks('value', {
|
|
81
|
+
const: null,
|
|
82
|
+
})
|
|
83
|
+
expect(result).toEqual(['value === null'])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it.todo('infers null type from all-null enum', () => {
|
|
87
|
+
const result = generateSchemaChecks('value', {
|
|
88
|
+
enum: [null, null],
|
|
89
|
+
})
|
|
90
|
+
expect(result).toEqual(['[null,null].includes(value as never)'])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// When object and array keywords tie, object wins (matches inferSchemaType priority)
|
|
94
|
+
it.todo('prefers object over array when keyword scores tie', () => {
|
|
95
|
+
const result = generateSchemaChecks('value', {
|
|
96
|
+
minProperties: 1,
|
|
97
|
+
minItems: 1,
|
|
98
|
+
})
|
|
99
|
+
expect(result).toContain('typeof value === "object" && value !== null && !Array.isArray(value)')
|
|
100
|
+
expect(result).not.toContain('Array.isArray(value)')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Array keywords outnumber string keywords so array wins
|
|
104
|
+
it.todo('selects highest scoring type when multiple keyword categories present', () => {
|
|
105
|
+
const result = generateSchemaChecks('value', {
|
|
106
|
+
minItems: 1,
|
|
107
|
+
maxItems: 3,
|
|
108
|
+
minLength: 2,
|
|
109
|
+
})
|
|
110
|
+
expect(result).toContain('Array.isArray(value)')
|
|
111
|
+
expect(result).not.toContain('typeof value === "string"')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it.todo('applies inferred type constraints alongside explicit enum check', () => {
|
|
115
|
+
const result = generateSchemaChecks('value', {
|
|
116
|
+
minimum: 0,
|
|
117
|
+
maximum: 10,
|
|
118
|
+
enum: [0, 5, 10],
|
|
119
|
+
})
|
|
120
|
+
expect(result).toContain('typeof value === "number"')
|
|
121
|
+
expect(result).toContain('value >= 0')
|
|
122
|
+
expect(result).toContain('value <= 10')
|
|
123
|
+
expect(result).toContain('[0,5,10].includes(value as never)')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('returns empty checks for a boolean schema', () => {
|
|
127
|
+
const result = generateSchemaChecks('value', true)
|
|
128
|
+
expect(result).toEqual([])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('generates typeof check for string type', () => {
|
|
132
|
+
const result = generateSchemaChecks('value', { type: 'string' })
|
|
133
|
+
expect(result).toEqual(['typeof value === "string"'])
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('generates string check with pattern', () => {
|
|
137
|
+
const result = generateSchemaChecks('value', { type: 'string', pattern: '^[a-z]+$' })
|
|
138
|
+
expect(result).toEqual(['typeof value === "string"', '/^[a-z]+$/.test(value)'])
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('generates string check with minLength', () => {
|
|
142
|
+
const result = generateSchemaChecks('value', { type: 'string', minLength: 1 })
|
|
143
|
+
expect(result).toEqual(['typeof value === "string"', 'value.length >= 1'])
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('generates string check with maxLength', () => {
|
|
147
|
+
const result = generateSchemaChecks('value', { type: 'string', maxLength: 100 })
|
|
148
|
+
expect(result).toEqual(['typeof value === "string"', 'value.length <= 100'])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('generates string check with both min and max length', () => {
|
|
152
|
+
const result = generateSchemaChecks('value', { type: 'string', minLength: 1, maxLength: 50 })
|
|
153
|
+
expect(result).toEqual(['typeof value === "string"', 'value.length >= 1', 'value.length <= 50'])
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('generates typeof check for number type', () => {
|
|
157
|
+
const result = generateSchemaChecks('value', { type: 'number' })
|
|
158
|
+
expect(result).toEqual(['typeof value === "number"'])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('generates number check with minimum', () => {
|
|
162
|
+
const result = generateSchemaChecks('value', { type: 'number', minimum: 0 })
|
|
163
|
+
expect(result).toEqual(['typeof value === "number"', 'value >= 0'])
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('generates number check with maximum', () => {
|
|
167
|
+
const result = generateSchemaChecks('value', { type: 'number', maximum: 100 })
|
|
168
|
+
expect(result).toEqual(['typeof value === "number"', 'value <= 100'])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('generates number check with exclusiveMinimum', () => {
|
|
172
|
+
const result = generateSchemaChecks('value', { type: 'number', exclusiveMinimum: 0 })
|
|
173
|
+
expect(result).toEqual(['typeof value === "number"', 'value > 0'])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('generates number check with exclusiveMaximum', () => {
|
|
177
|
+
const result = generateSchemaChecks('value', { type: 'number', exclusiveMaximum: 100 })
|
|
178
|
+
expect(result).toEqual(['typeof value === "number"', 'value < 100'])
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('generates number check with multipleOf', () => {
|
|
182
|
+
const result = generateSchemaChecks('value', { type: 'number', multipleOf: 5 })
|
|
183
|
+
expect(result).toEqual(['typeof value === "number"', 'value % 5 === 0'])
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('generates typeof check for integer type', () => {
|
|
187
|
+
const result = generateSchemaChecks('value', { type: 'integer' })
|
|
188
|
+
expect(result).toEqual(['typeof value === "number"'])
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('generates integer check with all numeric constraints', () => {
|
|
192
|
+
const result = generateSchemaChecks('value', {
|
|
193
|
+
type: 'integer',
|
|
194
|
+
minimum: 1,
|
|
195
|
+
maximum: 10,
|
|
196
|
+
exclusiveMinimum: 0,
|
|
197
|
+
exclusiveMaximum: 11,
|
|
198
|
+
multipleOf: 2,
|
|
199
|
+
})
|
|
200
|
+
expect(result).toEqual([
|
|
201
|
+
'typeof value === "number"',
|
|
202
|
+
'value >= 1',
|
|
203
|
+
'value <= 10',
|
|
204
|
+
'value > 0',
|
|
205
|
+
'value < 11',
|
|
206
|
+
'value % 2 === 0',
|
|
207
|
+
])
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('generates typeof check for boolean type', () => {
|
|
211
|
+
const result = generateSchemaChecks('value', { type: 'boolean' })
|
|
212
|
+
expect(result).toEqual(['typeof value === "boolean"'])
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('generates Array.isArray check for array type', () => {
|
|
216
|
+
const result = generateSchemaChecks('value', { type: 'array' })
|
|
217
|
+
expect(result).toEqual(['Array.isArray(value)'])
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('generates array check with minItems', () => {
|
|
221
|
+
const result = generateSchemaChecks('value', { type: 'array', minItems: 1 })
|
|
222
|
+
expect(result).toEqual(['Array.isArray(value)', 'value.length >= 1'])
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('generates array check with maxItems', () => {
|
|
226
|
+
const result = generateSchemaChecks('value', { type: 'array', maxItems: 10 })
|
|
227
|
+
expect(result).toEqual(['Array.isArray(value)', 'value.length <= 10'])
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('generates array check with uniqueItems true', () => {
|
|
231
|
+
const result = generateSchemaChecks('value', { type: 'array', uniqueItems: true })
|
|
232
|
+
expect(result).toEqual(['Array.isArray(value)', 'new Set(value).size === value.length'])
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('does not add uniqueItems check when uniqueItems is false', () => {
|
|
236
|
+
const result = generateSchemaChecks('value', { type: 'array', uniqueItems: false })
|
|
237
|
+
expect(result).toEqual(['Array.isArray(value)'])
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('generates object type check', () => {
|
|
241
|
+
const result = generateSchemaChecks('value', { type: 'object' })
|
|
242
|
+
expect(result).toEqual(['typeof value === "object" && value !== null && !Array.isArray(value)'])
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('generates object check with required properties', () => {
|
|
246
|
+
const result = generateSchemaChecks('value', {
|
|
247
|
+
type: 'object',
|
|
248
|
+
required: ['name', 'age'],
|
|
249
|
+
})
|
|
250
|
+
expect(result).toEqual([
|
|
251
|
+
'typeof value === "object" && value !== null && !Array.isArray(value)',
|
|
252
|
+
'"name" in value',
|
|
253
|
+
'"age" in value',
|
|
254
|
+
])
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('generates object check with minProperties', () => {
|
|
258
|
+
const result = generateSchemaChecks('value', { type: 'object', minProperties: 1 })
|
|
259
|
+
expect(result).toEqual([
|
|
260
|
+
'typeof value === "object" && value !== null && !Array.isArray(value)',
|
|
261
|
+
'Object.keys(value).length >= 1',
|
|
262
|
+
])
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('generates object check with maxProperties', () => {
|
|
266
|
+
const result = generateSchemaChecks('value', { type: 'object', maxProperties: 5 })
|
|
267
|
+
expect(result).toEqual([
|
|
268
|
+
'typeof value === "object" && value !== null && !Array.isArray(value)',
|
|
269
|
+
'Object.keys(value).length <= 5',
|
|
270
|
+
])
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('generates object check with additionalProperties false', () => {
|
|
274
|
+
const result = generateSchemaChecks('value', {
|
|
275
|
+
type: 'object',
|
|
276
|
+
properties: { name: { type: 'string' } },
|
|
277
|
+
additionalProperties: false,
|
|
278
|
+
})
|
|
279
|
+
expect(result).toEqual([
|
|
280
|
+
'typeof value === "object" && value !== null && !Array.isArray(value)',
|
|
281
|
+
'Object.keys(value).every(k => ["name"].includes(k))',
|
|
282
|
+
])
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('does not add additionalProperties check when additionalProperties is true', () => {
|
|
286
|
+
const result = generateSchemaChecks('value', {
|
|
287
|
+
type: 'object',
|
|
288
|
+
properties: { name: { type: 'string' } },
|
|
289
|
+
additionalProperties: true,
|
|
290
|
+
})
|
|
291
|
+
expect(result).toEqual(['typeof value === "object" && value !== null && !Array.isArray(value)'])
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('does not add additionalProperties check when no properties defined', () => {
|
|
295
|
+
const result = generateSchemaChecks('value', {
|
|
296
|
+
type: 'object',
|
|
297
|
+
additionalProperties: false,
|
|
298
|
+
})
|
|
299
|
+
expect(result).toEqual(['typeof value === "object" && value !== null && !Array.isArray(value)'])
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('skips required check when required array is empty', () => {
|
|
303
|
+
const result = generateSchemaChecks('value', { type: 'object', required: [] })
|
|
304
|
+
expect(result).toEqual(['typeof value === "object" && value !== null && !Array.isArray(value)'])
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('appends enum check regardless of type', () => {
|
|
308
|
+
const result = generateSchemaChecks('value', { type: 'string', enum: ['a', 'b'] })
|
|
309
|
+
expect(result).toContain('typeof value === "string"')
|
|
310
|
+
expect(result).toContain('["a","b"].includes(value as never)')
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('does not append enum check when enum is empty', () => {
|
|
314
|
+
const result = generateSchemaChecks('value', { type: 'string', enum: [] })
|
|
315
|
+
expect(result).toEqual(['typeof value === "string"'])
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('uses the provided accessor in all checks', () => {
|
|
319
|
+
const result = generateSchemaChecks('input?.name', { type: 'string', minLength: 1 })
|
|
320
|
+
expect(result).toEqual(['typeof input?.name === "string"', 'input?.name.length >= 1'])
|
|
321
|
+
})
|
|
322
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasAdditionalProperties,
|
|
3
|
+
hasEnum,
|
|
4
|
+
hasExclusiveMaximum,
|
|
5
|
+
hasExclusiveMinimum,
|
|
6
|
+
hasMaxItems,
|
|
7
|
+
hasMaximum,
|
|
8
|
+
hasMaxLength,
|
|
9
|
+
hasMaxProperties,
|
|
10
|
+
hasMinItems,
|
|
11
|
+
hasMinimum,
|
|
12
|
+
hasMinLength,
|
|
13
|
+
hasMinProperties,
|
|
14
|
+
hasMultipleOf,
|
|
15
|
+
hasPattern,
|
|
16
|
+
hasProperties,
|
|
17
|
+
hasRequired,
|
|
18
|
+
hasType,
|
|
19
|
+
hasUniqueItems,
|
|
20
|
+
} from '@amritk/helpers/schema-guards'
|
|
21
|
+
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
22
|
+
|
|
23
|
+
import { generateEnumCheck } from './generate-enum-check'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generates validation checks for a single schema (used in union validation).
|
|
27
|
+
*/
|
|
28
|
+
export const generateSchemaChecks = (accessor: string, schema: JSONSchema): string[] => {
|
|
29
|
+
const checks: string[] = []
|
|
30
|
+
|
|
31
|
+
if (!hasType(schema)) {
|
|
32
|
+
return checks
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
switch (schema.type) {
|
|
36
|
+
case 'string': {
|
|
37
|
+
checks.push(`typeof ${accessor} === "string"`)
|
|
38
|
+
if (hasPattern(schema)) {
|
|
39
|
+
checks.push(`/${schema.pattern.replace(/\//g, '\\/')}/.test(${accessor})`)
|
|
40
|
+
}
|
|
41
|
+
if (hasMinLength(schema)) {
|
|
42
|
+
checks.push(`${accessor}.length >= ${schema.minLength}`)
|
|
43
|
+
}
|
|
44
|
+
if (hasMaxLength(schema)) {
|
|
45
|
+
checks.push(`${accessor}.length <= ${schema.maxLength}`)
|
|
46
|
+
}
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
case 'number':
|
|
50
|
+
case 'integer':
|
|
51
|
+
checks.push(`typeof ${accessor} === "number"`)
|
|
52
|
+
if (hasMinimum(schema)) {
|
|
53
|
+
checks.push(`${accessor} >= ${schema.minimum}`)
|
|
54
|
+
}
|
|
55
|
+
if (hasMaximum(schema)) {
|
|
56
|
+
checks.push(`${accessor} <= ${schema.maximum}`)
|
|
57
|
+
}
|
|
58
|
+
if (hasExclusiveMinimum(schema)) {
|
|
59
|
+
checks.push(`${accessor} > ${schema.exclusiveMinimum}`)
|
|
60
|
+
}
|
|
61
|
+
if (hasExclusiveMaximum(schema)) {
|
|
62
|
+
checks.push(`${accessor} < ${schema.exclusiveMaximum}`)
|
|
63
|
+
}
|
|
64
|
+
if (hasMultipleOf(schema)) {
|
|
65
|
+
checks.push(`${accessor} % ${schema.multipleOf} === 0`)
|
|
66
|
+
}
|
|
67
|
+
break
|
|
68
|
+
case 'boolean':
|
|
69
|
+
checks.push(`typeof ${accessor} === "boolean"`)
|
|
70
|
+
break
|
|
71
|
+
case 'array':
|
|
72
|
+
checks.push(`Array.isArray(${accessor})`)
|
|
73
|
+
if (hasMinItems(schema)) {
|
|
74
|
+
checks.push(`${accessor}.length >= ${schema.minItems}`)
|
|
75
|
+
}
|
|
76
|
+
if (hasMaxItems(schema)) {
|
|
77
|
+
checks.push(`${accessor}.length <= ${schema.maxItems}`)
|
|
78
|
+
}
|
|
79
|
+
if (hasUniqueItems(schema) && schema.uniqueItems === true) {
|
|
80
|
+
checks.push(`new Set(${accessor}).size === ${accessor}.length`)
|
|
81
|
+
}
|
|
82
|
+
break
|
|
83
|
+
case 'object': {
|
|
84
|
+
checks.push(`typeof ${accessor} === "object" && ${accessor} !== null && !Array.isArray(${accessor})`)
|
|
85
|
+
// Check required properties exist
|
|
86
|
+
if (hasRequired(schema) && schema.required.length > 0) {
|
|
87
|
+
for (const requiredKey of schema.required) {
|
|
88
|
+
checks.push(`"${requiredKey}" in ${accessor}`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Check minProperties
|
|
92
|
+
if (hasMinProperties(schema)) {
|
|
93
|
+
checks.push(`Object.keys(${accessor}).length >= ${schema.minProperties}`)
|
|
94
|
+
}
|
|
95
|
+
// Check maxProperties
|
|
96
|
+
if (hasMaxProperties(schema)) {
|
|
97
|
+
checks.push(`Object.keys(${accessor}).length <= ${schema.maxProperties}`)
|
|
98
|
+
}
|
|
99
|
+
// Check additionalProperties
|
|
100
|
+
if (hasAdditionalProperties(schema) && schema.additionalProperties === false && hasProperties(schema)) {
|
|
101
|
+
const allowedKeys = Object.keys(schema.properties)
|
|
102
|
+
const allowedKeysStr = JSON.stringify(allowedKeys)
|
|
103
|
+
checks.push(`Object.keys(${accessor}).every(k => ${allowedKeysStr}.includes(k))`)
|
|
104
|
+
}
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (hasEnum(schema) && schema.enum.length > 0) {
|
|
110
|
+
checks.push(generateEnumCheck(accessor, schema.enum))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return checks
|
|
114
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { safeAccessor } from '@amritk/helpers/safe-accessor'
|
|
2
|
+
import {
|
|
3
|
+
hasEnum,
|
|
4
|
+
hasExclusiveMaximum,
|
|
5
|
+
hasExclusiveMinimum,
|
|
6
|
+
hasMaximum,
|
|
7
|
+
hasMaxLength,
|
|
8
|
+
hasMinimum,
|
|
9
|
+
hasMinLength,
|
|
10
|
+
hasMultipleOf,
|
|
11
|
+
hasPattern,
|
|
12
|
+
hasProperties,
|
|
13
|
+
hasRef,
|
|
14
|
+
hasRequired,
|
|
15
|
+
hasType,
|
|
16
|
+
isSchemaObject,
|
|
17
|
+
} from '@amritk/helpers/schema-guards'
|
|
18
|
+
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns the inline condition that is true when `accessor` is the wrong type
|
|
22
|
+
* for the given JSON Schema primitive type.
|
|
23
|
+
*/
|
|
24
|
+
const wrongTypeCondition = (accessor: string, type: string): string | null => {
|
|
25
|
+
switch (type) {
|
|
26
|
+
case 'string':
|
|
27
|
+
return `typeof ${accessor} !== "string"`
|
|
28
|
+
case 'number':
|
|
29
|
+
case 'integer':
|
|
30
|
+
return `typeof ${accessor} !== "number"`
|
|
31
|
+
case 'boolean':
|
|
32
|
+
return `typeof ${accessor} !== "boolean"`
|
|
33
|
+
case 'array':
|
|
34
|
+
return `!Array.isArray(${accessor})`
|
|
35
|
+
case 'object':
|
|
36
|
+
return `!isObject(${accessor})`
|
|
37
|
+
default:
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Maps a JSON Schema type to the label used in error messages.
|
|
44
|
+
* `integer` collapses to `number` since both are validated via `typeof === "number"`.
|
|
45
|
+
*/
|
|
46
|
+
const typeLabel = (type: string): string => (type === 'integer' ? 'number' : type)
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Escapes special regex characters in a pattern string for use in a regex literal.
|
|
50
|
+
*/
|
|
51
|
+
const escapeRegexPattern = (pattern: string): string => {
|
|
52
|
+
return pattern.replace(/\\/g, '\\\\').replace(/\//g, '\\/')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generates strict-mode constraint checks for a typed property
|
|
57
|
+
* (pattern, length, min/max, multipleOf).
|
|
58
|
+
*/
|
|
59
|
+
const generateConstraintChecks = (acc: string, propSchema: JSONSchema, typeName: string, key: string): string[] => {
|
|
60
|
+
if (!isSchemaObject(propSchema) || !hasType(propSchema)) return []
|
|
61
|
+
const t = propSchema.type as string
|
|
62
|
+
const lines: string[] = []
|
|
63
|
+
|
|
64
|
+
if (t === 'string') {
|
|
65
|
+
if (hasPattern(propSchema)) {
|
|
66
|
+
const pattern = escapeRegexPattern(propSchema.pattern)
|
|
67
|
+
lines.push(
|
|
68
|
+
` if (typeof ${acc} === "string" && !/${pattern}/.test(${acc})) throw new Error('[${typeName}] field "${key}" must match pattern ${propSchema.pattern}');`,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
if (hasMinLength(propSchema)) {
|
|
72
|
+
lines.push(
|
|
73
|
+
` if (typeof ${acc} === "string" && ${acc}.length < ${propSchema.minLength}) throw new Error('[${typeName}] field "${key}" must have at least ${propSchema.minLength} characters');`,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
if (hasMaxLength(propSchema)) {
|
|
77
|
+
lines.push(
|
|
78
|
+
` if (typeof ${acc} === "string" && ${acc}.length > ${propSchema.maxLength}) throw new Error('[${typeName}] field "${key}" must have at most ${propSchema.maxLength} characters');`,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (t === 'number' || t === 'integer') {
|
|
84
|
+
if (hasMinimum(propSchema)) {
|
|
85
|
+
lines.push(
|
|
86
|
+
` if (typeof ${acc} === "number" && ${acc} < ${propSchema.minimum}) throw new Error('[${typeName}] field "${key}" must be >= ${propSchema.minimum}');`,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
if (hasMaximum(propSchema)) {
|
|
90
|
+
lines.push(
|
|
91
|
+
` if (typeof ${acc} === "number" && ${acc} > ${propSchema.maximum}) throw new Error('[${typeName}] field "${key}" must be <= ${propSchema.maximum}');`,
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
if (hasExclusiveMinimum(propSchema)) {
|
|
95
|
+
lines.push(
|
|
96
|
+
` if (typeof ${acc} === "number" && ${acc} <= ${propSchema.exclusiveMinimum}) throw new Error('[${typeName}] field "${key}" must be > ${propSchema.exclusiveMinimum}');`,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
if (hasExclusiveMaximum(propSchema)) {
|
|
100
|
+
lines.push(
|
|
101
|
+
` if (typeof ${acc} === "number" && ${acc} >= ${propSchema.exclusiveMaximum}) throw new Error('[${typeName}] field "${key}" must be < ${propSchema.exclusiveMaximum}');`,
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
if (hasMultipleOf(propSchema)) {
|
|
105
|
+
lines.push(
|
|
106
|
+
` if (typeof ${acc} === "number" && ${acc} % ${propSchema.multipleOf} !== 0) throw new Error('[${typeName}] field "${key}" must be a multiple of ${propSchema.multipleOf}');`,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return lines
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Generates strict-mode lines for a single property of an object schema.
|
|
116
|
+
* Properties with a `$ref` are skipped here — the nested parser handles its own
|
|
117
|
+
* strict check when called from the parent's slow path.
|
|
118
|
+
*/
|
|
119
|
+
const generatePropertyAssertion = (
|
|
120
|
+
key: string,
|
|
121
|
+
propSchema: JSONSchema,
|
|
122
|
+
isRequired: boolean,
|
|
123
|
+
typeName: string,
|
|
124
|
+
): string[] => {
|
|
125
|
+
const acc = safeAccessor('input', key)
|
|
126
|
+
const lines: string[] = []
|
|
127
|
+
|
|
128
|
+
if (isRequired) {
|
|
129
|
+
lines.push(
|
|
130
|
+
` if (!(${JSON.stringify(key)} in input)) throw new Error('[${typeName}] missing required property "${key}"');`,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!isSchemaObject(propSchema)) return lines
|
|
135
|
+
if (hasRef(propSchema)) return lines
|
|
136
|
+
|
|
137
|
+
if (hasEnum(propSchema)) {
|
|
138
|
+
const allowed = JSON.stringify(propSchema.enum)
|
|
139
|
+
const label = (propSchema.enum as unknown[]).map((v) => JSON.stringify(v)).join(', ')
|
|
140
|
+
if (isRequired) {
|
|
141
|
+
lines.push(
|
|
142
|
+
` if (!(${allowed} as readonly unknown[]).includes(${acc})) throw new Error('[${typeName}] field "${key}" must be one of: ${label}');`,
|
|
143
|
+
)
|
|
144
|
+
} else {
|
|
145
|
+
lines.push(
|
|
146
|
+
` if (${acc} !== undefined && !(${allowed} as readonly unknown[]).includes(${acc})) throw new Error('[${typeName}] field "${key}" must be one of: ${label}');`,
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
return lines
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (hasType(propSchema)) {
|
|
153
|
+
const t = propSchema.type as string
|
|
154
|
+
const wrongType = wrongTypeCondition(acc, t)
|
|
155
|
+
if (wrongType) {
|
|
156
|
+
if (isRequired) {
|
|
157
|
+
lines.push(
|
|
158
|
+
` if (${wrongType}) throw new Error(\`[${typeName}] field "${key}" expected ${typeLabel(t)}, got \${typeof ${acc}}\`);`,
|
|
159
|
+
)
|
|
160
|
+
} else {
|
|
161
|
+
lines.push(
|
|
162
|
+
` if (${acc} !== undefined && (${wrongType})) throw new Error(\`[${typeName}] field "${key}" expected ${typeLabel(t)}, got \${typeof ${acc}}\`);`,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
lines.push(...generateConstraintChecks(acc, propSchema, typeName, key))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return lines
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Generates strict-mode assertions for the body of an object parser.
|
|
174
|
+
* Throws on:
|
|
175
|
+
* - non-object input
|
|
176
|
+
* - missing required property
|
|
177
|
+
* - property of the wrong primitive type
|
|
178
|
+
* - enum / pattern / length / min / max / multipleOf violations
|
|
179
|
+
*
|
|
180
|
+
* Properties with a `$ref` are validated by the nested parser's own strict
|
|
181
|
+
* check when that parser is invoked downstream.
|
|
182
|
+
*/
|
|
183
|
+
export const generateObjectStrictAssertion = (schema: JSONSchema, typeName: string): string[] => {
|
|
184
|
+
const lines: string[] = []
|
|
185
|
+
lines.push(
|
|
186
|
+
` if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if (!hasProperties(schema) || !isSchemaObject(schema)) return lines
|
|
190
|
+
|
|
191
|
+
const required = new Set<string>(hasRequired(schema) ? schema.required : [])
|
|
192
|
+
|
|
193
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
194
|
+
lines.push(...generatePropertyAssertion(key, propSchema, required.has(key), typeName))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return lines
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Generates a single strict-mode assertion line for a non-object scalar parser.
|
|
202
|
+
* Returns null when the schema has no type information to assert on.
|
|
203
|
+
*/
|
|
204
|
+
export const generateScalarStrictAssertion = (schema: JSONSchema, typeName: string): string | null => {
|
|
205
|
+
if (!isSchemaObject(schema) || !hasType(schema)) return null
|
|
206
|
+
const t = schema.type as string
|
|
207
|
+
const wrongType = wrongTypeCondition('input', t)
|
|
208
|
+
if (!wrongType) return null
|
|
209
|
+
return ` if (${wrongType}) throw new Error(\`[${typeName}] expected ${typeLabel(t)}, got \${input === null ? "null" : typeof input}\`);`
|
|
210
|
+
}
|