ata-validator 0.2.0 → 0.4.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.
@@ -0,0 +1,845 @@
1
+ 'use strict'
2
+
3
+ // Compile a JSON Schema into a pure JS validator function.
4
+ // No eval, no new Function, CSP-safe.
5
+ // Returns null if the schema is too complex for JS compilation.
6
+
7
+ function compileToJS(schema, defs) {
8
+ if (typeof schema === 'boolean') {
9
+ return schema ? () => true : () => false
10
+ }
11
+ if (typeof schema !== 'object' || schema === null) return null
12
+
13
+ // Bail if schema has edge cases that JS fast path gets wrong
14
+ if (!defs && !codegenSafe(schema)) return null
15
+
16
+ // Collect $defs early so sub-schemas can resolve $ref
17
+ const rootDefs = defs || collectDefs(schema)
18
+
19
+ // Bail on features that are too complex for JS fast path
20
+ if (schema.patternProperties ||
21
+ schema.dependentSchemas ||
22
+ schema.propertyNames) {
23
+ return null
24
+ }
25
+
26
+ const checks = []
27
+
28
+ // $ref (local only)
29
+ if (schema.$ref) {
30
+ const refFn = resolveRef(schema.$ref, rootDefs)
31
+ if (!refFn) return null
32
+ checks.push(refFn)
33
+ }
34
+
35
+ // type
36
+ if (schema.type) {
37
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type]
38
+ checks.push(buildTypeCheck(types))
39
+ }
40
+
41
+ // enum
42
+ if (schema.enum) {
43
+ const vals = schema.enum
44
+ const primitives = vals.filter(v => v === null || typeof v !== 'object')
45
+ const objects = vals.filter(v => v !== null && typeof v === 'object')
46
+ const primSet = new Set(primitives.map(v => v === null ? 'null' : typeof v === 'string' ? 's:' + v : 'n:' + v))
47
+ const objStrs = objects.map(v => JSON.stringify(v))
48
+ checks.push((d) => {
49
+ // Fast primitive check
50
+ const key = d === null ? 'null' : typeof d === 'string' ? 's:' + d : typeof d === 'number' || typeof d === 'boolean' ? 'n:' + d : null
51
+ if (key !== null && primSet.has(key)) return true
52
+ // Slow object check
53
+ const ds = JSON.stringify(d)
54
+ for (let i = 0; i < objStrs.length; i++) {
55
+ if (ds === objStrs[i]) return true
56
+ }
57
+ // Also check primitives by stringify for edge cases (boolean in enum)
58
+ for (let i = 0; i < primitives.length; i++) {
59
+ if (d === primitives[i]) return true
60
+ }
61
+ return false
62
+ })
63
+ }
64
+
65
+ // const
66
+ if (schema.const !== undefined) {
67
+ const cv = schema.const
68
+ if (cv === null || typeof cv !== 'object') {
69
+ checks.push((d) => d === cv)
70
+ } else {
71
+ const cs = JSON.stringify(cv)
72
+ checks.push((d) => JSON.stringify(d) === cs)
73
+ }
74
+ }
75
+
76
+ // required
77
+ if (schema.required && Array.isArray(schema.required)) {
78
+ for (const key of schema.required) {
79
+ checks.push((d) => typeof d === 'object' && d !== null && key in d)
80
+ }
81
+ }
82
+
83
+ // properties
84
+ if (schema.properties) {
85
+ for (const [key, prop] of Object.entries(schema.properties)) {
86
+ const propCheck = compileToJS(prop, rootDefs)
87
+ if (!propCheck) return null // bail if sub-schema too complex
88
+ checks.push((d) => {
89
+ if (typeof d !== 'object' || d === null || !(key in d)) return true
90
+ return propCheck(d[key])
91
+ })
92
+ }
93
+ }
94
+
95
+ // additionalProperties
96
+ if (schema.additionalProperties !== undefined && schema.properties) {
97
+ if (schema.additionalProperties === false) {
98
+ const allowed = new Set(Object.keys(schema.properties))
99
+ checks.push((d) => {
100
+ if (typeof d !== 'object' || d === null || Array.isArray(d)) return true
101
+ const keys = Object.keys(d)
102
+ for (let i = 0; i < keys.length; i++) {
103
+ if (!allowed.has(keys[i])) return false
104
+ }
105
+ return true
106
+ })
107
+ } else if (typeof schema.additionalProperties === 'object') {
108
+ const apCheck = compileToJS(schema.additionalProperties, rootDefs)
109
+ if (!apCheck) return null
110
+ const known = new Set(Object.keys(schema.properties || {}))
111
+ checks.push((d) => {
112
+ if (typeof d !== 'object' || d === null || Array.isArray(d)) return true
113
+ const keys = Object.keys(d)
114
+ for (let i = 0; i < keys.length; i++) {
115
+ if (!known.has(keys[i]) && !apCheck(d[keys[i]])) return false
116
+ }
117
+ return true
118
+ })
119
+ }
120
+ }
121
+
122
+ // dependentRequired
123
+ if (schema.dependentRequired) {
124
+ for (const [key, deps] of Object.entries(schema.dependentRequired)) {
125
+ checks.push((d) => {
126
+ if (typeof d !== 'object' || d === null || !(key in d)) return true
127
+ for (let i = 0; i < deps.length; i++) {
128
+ if (!(deps[i] in d)) return false
129
+ }
130
+ return true
131
+ })
132
+ }
133
+ }
134
+
135
+ // items
136
+ if (schema.items) {
137
+ const itemCheck = compileToJS(schema.items, rootDefs)
138
+ if (!itemCheck) return null
139
+ checks.push((d) => {
140
+ if (!Array.isArray(d)) return true
141
+ for (let i = 0; i < d.length; i++) {
142
+ if (!itemCheck(d[i])) return false
143
+ }
144
+ return true
145
+ })
146
+ }
147
+
148
+ // prefixItems
149
+ if (schema.prefixItems) {
150
+ const prefixChecks = []
151
+ for (const ps of schema.prefixItems) {
152
+ const pc = compileToJS(ps, rootDefs)
153
+ if (!pc) return null
154
+ prefixChecks.push(pc)
155
+ }
156
+ checks.push((d) => {
157
+ if (!Array.isArray(d)) return true
158
+ for (let i = 0; i < prefixChecks.length && i < d.length; i++) {
159
+ if (!prefixChecks[i](d[i])) return false
160
+ }
161
+ return true
162
+ })
163
+ }
164
+
165
+ // contains
166
+ if (schema.contains) {
167
+ const containsCheck = compileToJS(schema.contains, rootDefs)
168
+ if (!containsCheck) return null
169
+ const minC = schema.minContains !== undefined ? schema.minContains : 1
170
+ const maxC = schema.maxContains !== undefined ? schema.maxContains : Infinity
171
+ checks.push((d) => {
172
+ if (!Array.isArray(d)) return true
173
+ let count = 0
174
+ for (let i = 0; i < d.length; i++) {
175
+ if (containsCheck(d[i])) count++
176
+ }
177
+ return count >= minC && count <= maxC
178
+ })
179
+ }
180
+
181
+ // uniqueItems — sorted-key canonical form for correct object comparison
182
+ if (schema.uniqueItems) {
183
+ const canonical = (x) => {
184
+ if (x === null || typeof x !== 'object') return typeof x + ':' + x
185
+ if (Array.isArray(x)) return '[' + x.map(canonical).join(',') + ']'
186
+ return '{' + Object.keys(x).sort().map(k => JSON.stringify(k) + ':' + canonical(x[k])).join(',') + '}'
187
+ }
188
+ checks.push((d) => {
189
+ if (!Array.isArray(d)) return true
190
+ const seen = new Set()
191
+ for (let i = 0; i < d.length; i++) {
192
+ const key = canonical(d[i])
193
+ if (seen.has(key)) return false
194
+ seen.add(key)
195
+ }
196
+ return true
197
+ })
198
+ }
199
+
200
+ // numeric
201
+ if (schema.minimum !== undefined) {
202
+ const min = schema.minimum
203
+ checks.push((d) => typeof d !== 'number' || d >= min)
204
+ }
205
+ if (schema.maximum !== undefined) {
206
+ const max = schema.maximum
207
+ checks.push((d) => typeof d !== 'number' || d <= max)
208
+ }
209
+ if (schema.exclusiveMinimum !== undefined) {
210
+ const min = schema.exclusiveMinimum
211
+ checks.push((d) => typeof d !== 'number' || d > min)
212
+ }
213
+ if (schema.exclusiveMaximum !== undefined) {
214
+ const max = schema.exclusiveMaximum
215
+ checks.push((d) => typeof d !== 'number' || d < max)
216
+ }
217
+ if (schema.multipleOf !== undefined) {
218
+ const div = schema.multipleOf
219
+ checks.push((d) => typeof d !== 'number' || d % div === 0)
220
+ }
221
+
222
+ // string
223
+ if (schema.minLength !== undefined) {
224
+ const min = schema.minLength
225
+ checks.push((d) => typeof d !== 'string' || d.length >= min)
226
+ }
227
+ if (schema.maxLength !== undefined) {
228
+ const max = schema.maxLength
229
+ checks.push((d) => typeof d !== 'string' || d.length <= max)
230
+ }
231
+ if (schema.pattern) {
232
+ try {
233
+ const re = new RegExp(schema.pattern)
234
+ checks.push((d) => typeof d !== 'string' || re.test(d))
235
+ } catch {
236
+ return null
237
+ }
238
+ }
239
+
240
+ // format — hand-written fast checks
241
+ if (schema.format) {
242
+ const fc = FORMAT_CHECKS[schema.format]
243
+ if (fc) checks.push((d) => typeof d !== 'string' || fc(d))
244
+ }
245
+
246
+ // array size
247
+ if (schema.minItems !== undefined) {
248
+ const min = schema.minItems
249
+ checks.push((d) => !Array.isArray(d) || d.length >= min)
250
+ }
251
+ if (schema.maxItems !== undefined) {
252
+ const max = schema.maxItems
253
+ checks.push((d) => !Array.isArray(d) || d.length <= max)
254
+ }
255
+
256
+ // object size
257
+ if (schema.minProperties !== undefined) {
258
+ const min = schema.minProperties
259
+ checks.push((d) => typeof d !== 'object' || d === null || Object.keys(d).length >= min)
260
+ }
261
+ if (schema.maxProperties !== undefined) {
262
+ const max = schema.maxProperties
263
+ checks.push((d) => typeof d !== 'object' || d === null || Object.keys(d).length <= max)
264
+ }
265
+
266
+ // allOf
267
+ if (schema.allOf) {
268
+ const subs = []
269
+ for (const s of schema.allOf) {
270
+ const fn = compileToJS(s, rootDefs)
271
+ if (!fn) return null
272
+ subs.push(fn)
273
+ }
274
+ checks.push((d) => {
275
+ for (let i = 0; i < subs.length; i++) {
276
+ if (!subs[i](d)) return false
277
+ }
278
+ return true
279
+ })
280
+ }
281
+
282
+ // anyOf
283
+ if (schema.anyOf) {
284
+ const subs = []
285
+ for (const s of schema.anyOf) {
286
+ const fn = compileToJS(s, rootDefs)
287
+ if (!fn) return null
288
+ subs.push(fn)
289
+ }
290
+ checks.push((d) => {
291
+ for (let i = 0; i < subs.length; i++) {
292
+ if (subs[i](d)) return true
293
+ }
294
+ return false
295
+ })
296
+ }
297
+
298
+ // oneOf
299
+ if (schema.oneOf) {
300
+ const subs = []
301
+ for (const s of schema.oneOf) {
302
+ const fn = compileToJS(s, rootDefs)
303
+ if (!fn) return null
304
+ subs.push(fn)
305
+ }
306
+ checks.push((d) => {
307
+ let count = 0
308
+ for (let i = 0; i < subs.length; i++) {
309
+ if (subs[i](d)) count++
310
+ if (count > 1) return false
311
+ }
312
+ return count === 1
313
+ })
314
+ }
315
+
316
+ // not
317
+ if (schema.not) {
318
+ const notFn = compileToJS(schema.not, rootDefs)
319
+ if (!notFn) return null
320
+ checks.push((d) => !notFn(d))
321
+ }
322
+
323
+ // if/then/else
324
+ if (schema.if) {
325
+ const ifFn = compileToJS(schema.if, rootDefs)
326
+ if (!ifFn) return null
327
+ const thenFn = schema.then ? compileToJS(schema.then, rootDefs) : null
328
+ const elseFn = schema.else ? compileToJS(schema.else, rootDefs) : null
329
+ if (schema.then && !thenFn) return null
330
+ if (schema.else && !elseFn) return null
331
+ checks.push((d) => {
332
+ if (ifFn(d)) {
333
+ return thenFn ? thenFn(d) : true
334
+ } else {
335
+ return elseFn ? elseFn(d) : true
336
+ }
337
+ })
338
+ }
339
+
340
+ if (checks.length === 0) return () => true
341
+ if (checks.length === 1) return checks[0]
342
+
343
+ // Flatten to a single function — V8 JIT will inline
344
+ return (data) => {
345
+ for (let i = 0; i < checks.length; i++) {
346
+ if (!checks[i](data)) return false
347
+ }
348
+ return true
349
+ }
350
+ }
351
+
352
+ function collectDefs(schema) {
353
+ const defs = {}
354
+ const raw = schema.$defs || schema.definitions
355
+ if (raw && typeof raw === 'object') {
356
+ for (const [name, def] of Object.entries(raw)) {
357
+ // Lazy compile with circular guard — return true (permissive) on cycle
358
+ let cached = undefined
359
+ defs[name] = {
360
+ get fn() {
361
+ if (cached === undefined) {
362
+ cached = null // sentinel: compilation in progress
363
+ cached = compileToJS(def, defs)
364
+ }
365
+ // cached===null means circular ref or compile failure — be permissive
366
+ return cached || (() => true)
367
+ },
368
+ raw: def
369
+ }
370
+ }
371
+ }
372
+ return defs
373
+ }
374
+
375
+ function resolveRef(ref, defs) {
376
+ if (!defs) return null
377
+ // #/$defs/Name or #/definitions/Name
378
+ const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
379
+ if (!m) return null
380
+ const name = m[1]
381
+ const entry = defs[name]
382
+ if (!entry) return null
383
+ return (d) => {
384
+ const fn = entry.fn
385
+ return fn ? fn(d) : true
386
+ }
387
+ }
388
+
389
+ function buildTypeCheck(types) {
390
+ if (types.length === 1) {
391
+ return TYPE_CHECKS[types[0]] || (() => true)
392
+ }
393
+ const fns = types.map(t => TYPE_CHECKS[t]).filter(Boolean)
394
+ return (d) => {
395
+ for (let i = 0; i < fns.length; i++) {
396
+ if (fns[i](d)) return true
397
+ }
398
+ return false
399
+ }
400
+ }
401
+
402
+ const TYPE_CHECKS = {
403
+ string: (d) => typeof d === 'string',
404
+ number: (d) => typeof d === 'number' && isFinite(d),
405
+ integer: (d) => Number.isInteger(d),
406
+ boolean: (d) => typeof d === 'boolean',
407
+ null: (d) => d === null,
408
+ array: (d) => Array.isArray(d),
409
+ object: (d) => typeof d === 'object' && d !== null && !Array.isArray(d),
410
+ }
411
+
412
+ const FORMAT_CHECKS = {
413
+ email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1 },
414
+ date: (s) => s.length === 10 && /^\d{4}-\d{2}-\d{2}$/.test(s),
415
+ uuid: (s) => s.length === 36 && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s),
416
+ ipv4: (s) => { const p = s.split('.'); return p.length === 4 && p.every(n => { const v = +n; return v >= 0 && v <= 255 && String(v) === n }) },
417
+ hostname: (s) => s.length > 0 && s.length <= 253 && /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i.test(s),
418
+ }
419
+
420
+ // Dangerous JS property names that exist on Object.prototype
421
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'toString', 'valueOf',
422
+ 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString'])
423
+
424
+ // Recursively check if a schema can be safely compiled to JS codegen.
425
+ // Returns false if any sub-schema contains features codegen gets wrong.
426
+ function codegenSafe(schema) {
427
+ if (typeof schema === 'boolean') return true
428
+ if (typeof schema !== 'object' || schema === null) return true
429
+
430
+ // Boolean sub-schemas anywhere cause bail — codegen doesn't handle schema=false correctly
431
+ if (schema.items === false || schema.items === true) return false
432
+ if (schema.additionalProperties === true) return true // permissive — fine
433
+ if (schema.properties) {
434
+ for (const v of Object.values(schema.properties)) {
435
+ if (typeof v === 'boolean') return false
436
+ if (!codegenSafe(v)) return false
437
+ }
438
+ }
439
+
440
+ // Required keys that collide with Object.prototype
441
+ if (schema.required) {
442
+ for (const k of schema.required) {
443
+ if (UNSAFE_KEYS.has(k)) return false
444
+ }
445
+ }
446
+
447
+ // Unicode property escapes in pattern need 'u' flag — codegen uses RegExp without it
448
+ if (schema.pattern && /\\[pP]\{/.test(schema.pattern)) return false
449
+
450
+ // $ref — bail entirely from codegen; ref resolution has too many edge cases
451
+ if (schema.$ref) return false
452
+
453
+ // additionalProperties as schema — bail entirely, too many edge cases with allOf interaction
454
+ if (typeof schema.additionalProperties === 'object') return false
455
+ if (schema.additionalProperties === false && !schema.properties) return false
456
+
457
+ // propertyNames: false — codegen doesn't handle this
458
+ if (schema.propertyNames === false) return false
459
+
460
+ // Recurse into sub-schemas — bail on boolean schemas in any position
461
+ const subs = [
462
+ schema.items, schema.contains, schema.not,
463
+ schema.if, schema.then, schema.else,
464
+ ...(schema.prefixItems || []),
465
+ ...(schema.allOf || []),
466
+ ...(schema.anyOf || []),
467
+ ...(schema.oneOf || []),
468
+ ]
469
+ if (typeof schema.additionalProperties === 'object') subs.push(schema.additionalProperties)
470
+ for (const s of subs) {
471
+ if (s === undefined || s === null) continue
472
+ if (typeof s === 'boolean') return false // boolean sub-schema
473
+ if (!codegenSafe(s)) return false
474
+ }
475
+
476
+ return true
477
+ }
478
+
479
+ // --- Codegen mode: generates a single Function (NOT CSP-safe) ---
480
+ // This matches ajv's approach: one monolithic function, V8 JIT fully inlines it
481
+ function compileToJSCodegen(schema) {
482
+ if (typeof schema === 'boolean') return schema ? () => true : () => false
483
+ if (typeof schema !== 'object' || schema === null) return null
484
+
485
+ // Bail if schema contains features that codegen can't handle correctly
486
+ if (!codegenSafe(schema)) return null
487
+
488
+ // Collect defs for $ref resolution
489
+ const rootDefs = schema.$defs || schema.definitions || null
490
+
491
+ // Bail only on truly unsupported features
492
+ if (schema.patternProperties ||
493
+ schema.dependentSchemas ||
494
+ schema.propertyNames) return null
495
+
496
+ const ctx = { varCounter: 0, helpers: [], helperCode: [], rootDefs, refStack: new Set() }
497
+ const lines = []
498
+ genCode(schema, 'd', lines, ctx)
499
+ if (lines.length === 0) return () => true
500
+
501
+ const body = (ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
502
+ lines.join('\n ') + '\n return true'
503
+ try {
504
+ return new Function('d', body)
505
+ } catch {
506
+ return null
507
+ }
508
+ }
509
+
510
+ // knownType: if parent already verified the type, skip redundant guards.
511
+ // 'object' = we know v is a non-null non-array object
512
+ // 'array' = we know v is an array
513
+ // 'string' / 'number' / 'integer' = we know the primitive type
514
+ function genCode(schema, v, lines, ctx, knownType) {
515
+ if (typeof schema !== 'object' || schema === null) return
516
+
517
+ // $ref — guard against circular references
518
+ if (schema.$ref && ctx.rootDefs) {
519
+ const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
520
+ if (m && ctx.rootDefs[m[1]]) {
521
+ if (ctx.refStack.has(schema.$ref)) return // circular — bail
522
+ ctx.refStack.add(schema.$ref)
523
+ genCode(ctx.rootDefs[m[1]], v, lines, ctx, knownType)
524
+ ctx.refStack.delete(schema.$ref)
525
+ } else {
526
+ return
527
+ }
528
+ }
529
+
530
+ // Determine the single known type after this schema's type check
531
+ const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
532
+ let effectiveType = knownType
533
+ if (types) {
534
+ if (!knownType) {
535
+ // Emit the type check
536
+ const conds = types.map(t => {
537
+ switch (t) {
538
+ case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
539
+ case 'array': return `Array.isArray(${v})`
540
+ case 'string': return `typeof ${v}==='string'`
541
+ case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
542
+ case 'integer': return `Number.isInteger(${v})`
543
+ case 'boolean': return `typeof ${v}==='boolean'`
544
+ case 'null': return `${v}===null`
545
+ default: return 'true'
546
+ }
547
+ })
548
+ lines.push(`if(!(${conds.join('||')}))return false`)
549
+ }
550
+ // If single type, downstream checks can skip guards
551
+ if (types.length === 1) effectiveType = types[0]
552
+ }
553
+
554
+ const isObj = effectiveType === 'object'
555
+ const isArr = effectiveType === 'array'
556
+ const isStr = effectiveType === 'string'
557
+ const isNum = effectiveType === 'number' || effectiveType === 'integer'
558
+ const objGuard = isObj ? '' : `typeof ${v}==='object'&&${v}!==null&&`
559
+ const objCheck = isObj ? '' : `if(typeof ${v}!=='object'||${v}===null)return false;`
560
+
561
+ // enum
562
+ if (schema.enum) {
563
+ const vals = schema.enum
564
+ const primitives = vals.filter(v => v === null || typeof v !== 'object')
565
+ const objects = vals.filter(v => v !== null && typeof v === 'object')
566
+ const primChecks = primitives.map(p => `${v}===${JSON.stringify(p)}`).join('||')
567
+ const objChecks = objects.map(o => `JSON.stringify(${v})===${JSON.stringify(JSON.stringify(o))}`).join('||')
568
+ const allChecks = [primChecks, objChecks].filter(Boolean).join('||')
569
+ lines.push(`if(!(${allChecks || 'false'}))return false`)
570
+ }
571
+
572
+ // const
573
+ if (schema.const !== undefined) {
574
+ const cv = schema.const
575
+ if (cv === null || typeof cv !== 'object') {
576
+ lines.push(`if(${v}!==${JSON.stringify(cv)})return false`)
577
+ } else {
578
+ lines.push(`if(JSON.stringify(${v})!==${JSON.stringify(JSON.stringify(cv))})return false`)
579
+ }
580
+ }
581
+
582
+ // Collect required keys so property checks can skip 'in' guard
583
+ const requiredSet = new Set(schema.required || [])
584
+
585
+ // required + property hoisting via destructuring.
586
+ // V8 TurboFan optimizes destructuring into a single batch hidden-class-aware read.
587
+ // `d.key !== undefined` is faster than `'key' in d` (no prototype chain walk).
588
+ const hoisted = {} // key -> local var name
589
+ if (schema.required && schema.properties && isObj) {
590
+ const destructKeys = []
591
+ const reqChecks = []
592
+ for (const key of schema.required) {
593
+ if (schema.properties[key]) {
594
+ const localVar = `_h${ctx.varCounter++}`
595
+ hoisted[key] = localVar
596
+ destructKeys.push(`${JSON.stringify(key)}:${localVar}`)
597
+ reqChecks.push(`${localVar}===undefined`)
598
+ } else {
599
+ // Required but no property schema — just check existence
600
+ reqChecks.push(`${v}[${JSON.stringify(key)}]===undefined`)
601
+ }
602
+ }
603
+ if (destructKeys.length > 0) {
604
+ lines.push(`const{${destructKeys.join(',')}}=${v}`)
605
+ }
606
+ if (reqChecks.length > 0) {
607
+ lines.push(`if(${reqChecks.join('||')})return false`)
608
+ }
609
+ } else if (schema.required) {
610
+ if (isObj) {
611
+ const checks = schema.required.map(key => `${v}[${JSON.stringify(key)}]===undefined`)
612
+ lines.push(`if(${checks.join('||')})return false`)
613
+ } else {
614
+ for (const key of schema.required) {
615
+ lines.push(`if(typeof ${v}!=='object'||${v}===null||!(${JSON.stringify(key)} in ${v}))return false`)
616
+ }
617
+ }
618
+ }
619
+
620
+ // numeric — skip type guard if known numeric
621
+ if (schema.minimum !== undefined) lines.push(isNum ? `if(${v}<${schema.minimum})return false` : `if(typeof ${v}==='number'&&${v}<${schema.minimum})return false`)
622
+ if (schema.maximum !== undefined) lines.push(isNum ? `if(${v}>${schema.maximum})return false` : `if(typeof ${v}==='number'&&${v}>${schema.maximum})return false`)
623
+ if (schema.exclusiveMinimum !== undefined) lines.push(isNum ? `if(${v}<=${schema.exclusiveMinimum})return false` : `if(typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum})return false`)
624
+ if (schema.exclusiveMaximum !== undefined) lines.push(isNum ? `if(${v}>=${schema.exclusiveMaximum})return false` : `if(typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum})return false`)
625
+ if (schema.multipleOf !== undefined) lines.push(isNum ? `if(${v}%${schema.multipleOf}!==0)return false` : `if(typeof ${v}==='number'&&${v}%${schema.multipleOf}!==0)return false`)
626
+
627
+ // string — skip type guard if known string
628
+ if (schema.minLength !== undefined) lines.push(isStr ? `if(${v}.length<${schema.minLength})return false` : `if(typeof ${v}==='string'&&${v}.length<${schema.minLength})return false`)
629
+ if (schema.maxLength !== undefined) lines.push(isStr ? `if(${v}.length>${schema.maxLength})return false` : `if(typeof ${v}==='string'&&${v}.length>${schema.maxLength})return false`)
630
+
631
+ // array size — skip guard if known array
632
+ if (schema.minItems !== undefined) lines.push(isArr ? `if(${v}.length<${schema.minItems})return false` : `if(Array.isArray(${v})&&${v}.length<${schema.minItems})return false`)
633
+ if (schema.maxItems !== undefined) lines.push(isArr ? `if(${v}.length>${schema.maxItems})return false` : `if(Array.isArray(${v})&&${v}.length>${schema.maxItems})return false`)
634
+
635
+ // object size
636
+ if (schema.minProperties !== undefined) lines.push(`if(${objGuard}Object.keys(${v}).length<${schema.minProperties})return false`)
637
+ if (schema.maxProperties !== undefined) lines.push(`if(${objGuard}Object.keys(${v}).length>${schema.maxProperties})return false`)
638
+
639
+ if (schema.pattern) {
640
+ // Use RegExp constructor via helper to avoid injection from untrusted patterns
641
+ const ri = ctx.varCounter++
642
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(schema.pattern)})`)
643
+ lines.push(isStr ? `if(!_re${ri}.test(${v}))return false` : `if(typeof ${v}==='string'&&!_re${ri}.test(${v}))return false`)
644
+ }
645
+
646
+ if (schema.format) {
647
+ const fc = FORMAT_CODEGEN[schema.format]
648
+ if (fc) lines.push(fc(v, isStr))
649
+ }
650
+
651
+ // uniqueItems — fast path for primitive arrays (no JSON.stringify)
652
+ if (schema.uniqueItems) {
653
+ const si = ctx.varCounter++
654
+ // If items schema is a primitive type, skip JSON.stringify entirely
655
+ const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
656
+ const isPrimItems = itemType === 'string' || itemType === 'number' || itemType === 'integer'
657
+ // For objects: use sorted-key JSON.stringify to handle key order correctly per spec
658
+ const inner = isPrimItems
659
+ ? `const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){if(_s${si}.has(${v}[_i]))return false;_s${si}.add(${v}[_i])}`
660
+ : `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);if(_s${si}.has(_k))return false;_s${si}.add(_k)}`
661
+ lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
662
+ }
663
+
664
+ // additionalProperties
665
+ if (schema.additionalProperties === false && schema.properties) {
666
+ const allowed = Object.keys(schema.properties).map(k => `'${esc(k)}'`).join(',')
667
+ const ci = ctx.varCounter++
668
+ const inner = `const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++)if(!_a${ci}.has(_k${ci}[_i]))return false`
669
+ lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
670
+ }
671
+
672
+ // dependentRequired
673
+ if (schema.dependentRequired) {
674
+ for (const [key, deps] of Object.entries(schema.dependentRequired)) {
675
+ const depChecks = deps.map(d => `!('${esc(d)}' in ${v})`).join('||')
676
+ lines.push(`if(${objGuard}'${esc(key)}' in ${v}&&(${depChecks}))return false`)
677
+ }
678
+ }
679
+
680
+ // properties — use hoisted vars for required props, hoist optional to locals too
681
+ if (schema.properties) {
682
+ for (const [key, prop] of Object.entries(schema.properties)) {
683
+ if (requiredSet.has(key) && isObj) {
684
+ // Required + type:object — property exists, use destructured local
685
+ genCode(prop, hoisted[key] || `${v}[${JSON.stringify(key)}]`, lines, ctx)
686
+ } else if (isObj) {
687
+ // Optional — hoist to local, check undefined
688
+ const oi = ctx.varCounter++
689
+ const local = `_o${oi}`
690
+ lines.push(`{const ${local}=${v}[${JSON.stringify(key)}];if(${local}!==undefined){`)
691
+ genCode(prop, local, lines, ctx)
692
+ lines.push(`}}`)
693
+ } else {
694
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}){`)
695
+ genCode(prop, `${v}[${JSON.stringify(key)}]`, lines, ctx)
696
+ lines.push(`}`)
697
+ }
698
+ }
699
+ }
700
+
701
+ // items — pass known type info to children
702
+ if (schema.items) {
703
+ const idx = `_j${ctx.varCounter}`
704
+ const elem = `_e${ctx.varCounter}`
705
+ ctx.varCounter++
706
+ lines.push(isArr
707
+ ? `for(let ${idx}=0;${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`
708
+ : `if(Array.isArray(${v})){for(let ${idx}=0;${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
709
+ genCode(schema.items, elem, lines, ctx)
710
+ lines.push(isArr ? `}` : `}}`)
711
+ }
712
+
713
+ // prefixItems
714
+ if (schema.prefixItems) {
715
+ for (let i = 0; i < schema.prefixItems.length; i++) {
716
+ const elem = `_p${ctx.varCounter}_${i}`
717
+ lines.push(isArr
718
+ ? `if(${v}.length>${i}){const ${elem}=${v}[${i}]`
719
+ : `if(Array.isArray(${v})&&${v}.length>${i}){const ${elem}=${v}[${i}]`)
720
+ genCode(schema.prefixItems[i], elem, lines, ctx)
721
+ lines.push(`}`)
722
+ }
723
+ ctx.varCounter++
724
+ }
725
+
726
+ // contains — use helper function to avoid try/catch overhead
727
+ if (schema.contains) {
728
+ const ci = ctx.varCounter++
729
+ const minC = schema.minContains !== undefined ? schema.minContains : 1
730
+ const maxC = schema.maxContains !== undefined ? schema.maxContains : Infinity
731
+ const subLines = []
732
+ genCode(schema.contains, `_cv`, subLines, ctx)
733
+ const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
734
+ const guard = isArr ? '' : `if(!Array.isArray(${v})){}else `
735
+ lines.push(`${guard}{const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0`)
736
+ lines.push(`for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
737
+ if (maxC === Infinity) {
738
+ lines.push(`if(_cc${ci}<${minC})return false}`)
739
+ } else {
740
+ lines.push(`if(_cc${ci}<${minC}||_cc${ci}>${maxC})return false}`)
741
+ }
742
+ }
743
+
744
+ // allOf — pass known type through
745
+ if (schema.allOf) {
746
+ for (const sub of schema.allOf) {
747
+ genCode(sub, v, lines, ctx, effectiveType)
748
+ }
749
+ }
750
+
751
+ // anyOf — need function wrappers since genCode uses return false
752
+ if (schema.anyOf) {
753
+ const fns = []
754
+ for (let i = 0; i < schema.anyOf.length; i++) {
755
+ const subLines = []
756
+ genCode(schema.anyOf[i], '_av', subLines, ctx)
757
+ if (subLines.length === 0) {
758
+ fns.push(`function(_av){return true}`)
759
+ } else {
760
+ fns.push(`function(_av){${subLines.join(';')};return true}`)
761
+ }
762
+ }
763
+ const fi = ctx.varCounter++
764
+ lines.push(`{const _af${fi}=[${fns.join(',')}];let _am${fi}=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am${fi}=true;break}}if(!_am${fi})return false}`)
765
+ }
766
+
767
+ // oneOf
768
+ if (schema.oneOf) {
769
+ const fns = []
770
+ for (let i = 0; i < schema.oneOf.length; i++) {
771
+ const subLines = []
772
+ genCode(schema.oneOf[i], '_ov', subLines, ctx)
773
+ if (subLines.length === 0) {
774
+ fns.push(`function(_ov){return true}`)
775
+ } else {
776
+ fns.push(`function(_ov){${subLines.join(';')};return true}`)
777
+ }
778
+ }
779
+ const fi = ctx.varCounter++
780
+ lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc${fi}=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc${fi}++;if(_oc${fi}>1)return false}if(_oc${fi}!==1)return false}`)
781
+ }
782
+
783
+ // not
784
+ if (schema.not) {
785
+ const subLines = []
786
+ genCode(schema.not, '_nv', subLines, ctx)
787
+ if (subLines.length === 0) {
788
+ lines.push(`return false`) // not:{} means nothing is valid
789
+ } else {
790
+ const fi = ctx.varCounter++
791
+ lines.push(`{const _nf${fi}=(function(_nv){${subLines.join(';')};return true});if(_nf${fi}(${v}))return false}`)
792
+ }
793
+ }
794
+
795
+ // if/then/else
796
+ if (schema.if) {
797
+ const ifLines = []
798
+ genCode(schema.if, '_iv', ifLines, ctx)
799
+ const fi = ctx.varCounter++
800
+ const ifFn = ifLines.length === 0
801
+ ? `function(_iv){return true}`
802
+ : `function(_iv){${ifLines.join(';')};return true}`
803
+
804
+ let thenFn = 'null', elseFn = 'null'
805
+ if (schema.then) {
806
+ const thenLines = []
807
+ genCode(schema.then, '_tv', thenLines, ctx)
808
+ thenFn = thenLines.length === 0
809
+ ? `function(_tv){return true}`
810
+ : `function(_tv){${thenLines.join(';')};return true}`
811
+ }
812
+ if (schema.else) {
813
+ const elseLines = []
814
+ genCode(schema.else, '_ev', elseLines, ctx)
815
+ elseFn = elseLines.length === 0
816
+ ? `function(_ev){return true}`
817
+ : `function(_ev){${elseLines.join(';')};return true}`
818
+ }
819
+ lines.push(`{const _if${fi}=${ifFn};const _th${fi}=${thenFn};const _el${fi}=${elseFn}`)
820
+ lines.push(`if(_if${fi}(${v})){if(_th${fi}&&!_th${fi}(${v}))return false}else{if(_el${fi}&&!_el${fi}(${v}))return false}}`)
821
+ }
822
+ }
823
+
824
+ const FORMAT_CODEGEN = {
825
+ email: (v, isStr) => {
826
+ const guard = isStr ? '' : `typeof ${v}==='string'&&`
827
+ return isStr
828
+ ? `{const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
829
+ : `if(typeof ${v}==='string'){const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
830
+ },
831
+ date: (v, isStr) => isStr
832
+ ? `if(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v}))return false`
833
+ : `if(typeof ${v}==='string'&&(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v})))return false`,
834
+ uuid: (v, isStr) => isStr
835
+ ? `if(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v}))return false`
836
+ : `if(typeof ${v}==='string'&&(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v})))return false`,
837
+ ipv4: (v, isStr) => isStr
838
+ ? `{const _p=${v}.split('.');if(_p.length!==4||!_p.every(function(n){var x=+n;return x>=0&&x<=255&&String(x)===n}))return false}`
839
+ : `if(typeof ${v}==='string'){const _p=${v}.split('.');if(_p.length!==4||!_p.every(function(n){var x=+n;return x>=0&&x<=255&&String(x)===n}))return false}`,
840
+ }
841
+
842
+ // Safe key escaping: use JSON.stringify to handle all special chars (newlines, null bytes, etc.)
843
+ function esc(s) { return JSON.stringify(s).slice(1, -1) }
844
+
845
+ module.exports = { compileToJS, compileToJSCodegen }