@amritk/generate-parsers 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/generators/build-schema.d.ts +5 -1
  2. package/dist/generators/generate-files.d.ts +15 -2
  3. package/dist/helpers/collect-helpers.d.ts +16 -2
  4. package/dist/index.js +64 -19
  5. package/package.json +3 -2
  6. package/src/generators/build-schema.test.ts +830 -0
  7. package/src/generators/build-schema.ts +280 -0
  8. package/src/generators/generate-default-from-pattern.test.ts +81 -0
  9. package/src/generators/generate-default-from-pattern.ts +39 -0
  10. package/src/generators/generate-discriminated-union-validation.test.ts +84 -0
  11. package/src/generators/generate-discriminated-union-validation.ts +54 -0
  12. package/src/generators/generate-enum-check.test.ts +45 -0
  13. package/src/generators/generate-enum-check.ts +8 -0
  14. package/src/generators/generate-files.test.ts +435 -0
  15. package/src/generators/generate-files.ts +136 -0
  16. package/src/generators/generate-non-discriminated-union-validation.test.ts +74 -0
  17. package/src/generators/generate-non-discriminated-union-validation.ts +40 -0
  18. package/src/generators/generate-parser-function.test.ts +1798 -0
  19. package/src/generators/generate-parser-function.ts +1319 -0
  20. package/src/generators/generate-schema-checks.test.ts +322 -0
  21. package/src/generators/generate-schema-checks.ts +114 -0
  22. package/src/generators/generate-strict-assertion.ts +210 -0
  23. package/src/generators/generate-validation-expression.test.ts +794 -0
  24. package/src/generators/generate-validation-expression.ts +286 -0
  25. package/src/helpers/apply-schema-extensions.test.ts +194 -0
  26. package/src/helpers/apply-schema-extensions.ts +63 -0
  27. package/src/helpers/collect-helpers.test.ts +103 -0
  28. package/src/helpers/collect-helpers.ts +64 -0
  29. package/src/helpers/collect-imports.test.ts +587 -0
  30. package/src/helpers/collect-imports.ts +268 -0
  31. package/src/helpers/find-discriminator.test.ts +88 -0
  32. package/src/helpers/find-discriminator.ts +39 -0
  33. package/src/helpers/get-default-value.test.ts +135 -0
  34. package/src/helpers/get-default-value.ts +89 -0
  35. package/src/helpers/get-discriminator-value.test.ts +83 -0
  36. package/src/helpers/get-discriminator-value.ts +21 -0
  37. package/src/index.ts +1 -0
  38. 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
+ }