ata-validator 0.5.0 → 0.6.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 +40 -25
- package/binding/ata_napi.cpp +13 -7
- package/compat.js +1 -9
- package/include/ata.h +4 -0
- package/index.d.ts +12 -4
- package/index.js +99 -14
- package/lib/js-compiler.js +951 -190
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/ata-validator.node +0 -0
- package/src/ata.cpp +327 -1
- package/prebuilds/linux-arm64/ata-validator.node +0 -0
- package/prebuilds/linux-x64/ata-validator.node +0 -0
package/lib/js-compiler.js
CHANGED
|
@@ -4,6 +4,34 @@
|
|
|
4
4
|
// Closure-based validator — no new Function() or eval().
|
|
5
5
|
// Returns null if the schema is too complex for JS compilation.
|
|
6
6
|
|
|
7
|
+
// AJV-compatible error message templates (compile-time, not runtime)
|
|
8
|
+
const AJV_MESSAGES = {
|
|
9
|
+
type: (p) => `must be ${p.type}`,
|
|
10
|
+
required: (p) => `must have required property '${p.missingProperty}'`,
|
|
11
|
+
additionalProperties: () => 'must NOT have additional properties',
|
|
12
|
+
enum: () => 'must be equal to one of the allowed values',
|
|
13
|
+
const: () => 'must be equal to constant',
|
|
14
|
+
minimum: (p) => `must be >= ${p.limit}`,
|
|
15
|
+
maximum: (p) => `must be <= ${p.limit}`,
|
|
16
|
+
exclusiveMinimum: (p) => `must be > ${p.limit}`,
|
|
17
|
+
exclusiveMaximum: (p) => `must be < ${p.limit}`,
|
|
18
|
+
minLength: (p) => `must NOT have fewer than ${p.limit} characters`,
|
|
19
|
+
maxLength: (p) => `must NOT have more than ${p.limit} characters`,
|
|
20
|
+
pattern: (p) => `must match pattern "${p.pattern}"`,
|
|
21
|
+
format: (p) => `must match format "${p.format}"`,
|
|
22
|
+
minItems: (p) => `must NOT have fewer than ${p.limit} items`,
|
|
23
|
+
maxItems: (p) => `must NOT have more than ${p.limit} items`,
|
|
24
|
+
uniqueItems: (p) => `must NOT have duplicate items (items ## ${p.j} and ${p.i} are identical)`,
|
|
25
|
+
minProperties: (p) => `must NOT have fewer than ${p.limit} properties`,
|
|
26
|
+
maxProperties: (p) => `must NOT have more than ${p.limit} properties`,
|
|
27
|
+
multipleOf: (p) => `must be multiple of ${p.multipleOf}`,
|
|
28
|
+
oneOf: () => 'must match exactly one schema in oneOf',
|
|
29
|
+
anyOf: () => 'must match a schema in anyOf',
|
|
30
|
+
allOf: () => 'must match all schemas in allOf',
|
|
31
|
+
not: () => 'must NOT be valid',
|
|
32
|
+
if: (p) => `must match "${p.failingKeyword}" schema`,
|
|
33
|
+
}
|
|
34
|
+
|
|
7
35
|
function compileToJS(schema, defs, schemaMap) {
|
|
8
36
|
if (typeof schema === 'boolean') {
|
|
9
37
|
return schema ? () => true : () => false
|
|
@@ -433,11 +461,13 @@ function codegenSafe(schema, schemaMap) {
|
|
|
433
461
|
if (typeof schema !== 'object' || schema === null) return true
|
|
434
462
|
|
|
435
463
|
// Boolean sub-schemas anywhere cause bail — codegen doesn't handle schema=false correctly
|
|
436
|
-
if (schema.items === false
|
|
464
|
+
if (schema.items === false) return false
|
|
465
|
+
if (schema.items === true && !schema.unevaluatedItems) return false
|
|
437
466
|
if (schema.additionalProperties === true) return true // permissive — fine
|
|
438
467
|
if (schema.properties) {
|
|
439
468
|
for (const v of Object.values(schema.properties)) {
|
|
440
|
-
if (
|
|
469
|
+
if (v === false) return false // property: false is complex
|
|
470
|
+
if (v === true) continue // property: true is always valid
|
|
441
471
|
if (!codegenSafe(v, schemaMap)) return false
|
|
442
472
|
}
|
|
443
473
|
}
|
|
@@ -463,9 +493,10 @@ function codegenSafe(schema, schemaMap) {
|
|
|
463
493
|
const isLocal = /^#\/(?:\$defs|definitions)\/[^/]+$/.test(schema.$ref)
|
|
464
494
|
const isResolvable = !isLocal && schemaMap && schemaMap.has(schema.$ref)
|
|
465
495
|
if (!isLocal && !isResolvable) return false
|
|
466
|
-
//
|
|
496
|
+
// In Draft 2020-12, $ref with siblings is allowed. Only bail if no unevaluated* keyword
|
|
497
|
+
// (unevaluated schemas need $ref + siblings to work properly)
|
|
467
498
|
const siblings = Object.keys(schema).filter(k => k !== '$ref' && k !== '$defs' && k !== 'definitions' && k !== '$schema' && k !== '$id')
|
|
468
|
-
if (siblings.length > 0) return false
|
|
499
|
+
if (siblings.length > 0 && schema.unevaluatedProperties === undefined && schema.unevaluatedItems === undefined) return false
|
|
469
500
|
}
|
|
470
501
|
|
|
471
502
|
// additionalProperties as schema — bail entirely, too many edge cases with allOf interaction
|
|
@@ -475,6 +506,19 @@ function codegenSafe(schema, schemaMap) {
|
|
|
475
506
|
// propertyNames: false — codegen doesn't handle this
|
|
476
507
|
if (schema.propertyNames === false) return false
|
|
477
508
|
|
|
509
|
+
// unevaluatedProperties: allow boolean and schema values
|
|
510
|
+
if (schema.unevaluatedProperties !== undefined) {
|
|
511
|
+
if (typeof schema.unevaluatedProperties === 'object' && schema.unevaluatedProperties !== null) {
|
|
512
|
+
if (!codegenSafe(schema.unevaluatedProperties, schemaMap)) return false
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// unevaluatedItems: allow boolean and schema values
|
|
516
|
+
if (schema.unevaluatedItems !== undefined) {
|
|
517
|
+
if (typeof schema.unevaluatedItems === 'object' && schema.unevaluatedItems !== null) {
|
|
518
|
+
if (!codegenSafe(schema.unevaluatedItems, schemaMap)) return false
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
478
522
|
// Check $defs: targets must be safe, names must be simple, no nested $ref chains
|
|
479
523
|
const defs = schema.$defs || schema.definitions
|
|
480
524
|
if (defs) {
|
|
@@ -501,7 +545,8 @@ function codegenSafe(schema, schemaMap) {
|
|
|
501
545
|
if (typeof schema.additionalProperties === 'object') subs.push(schema.additionalProperties)
|
|
502
546
|
for (const s of subs) {
|
|
503
547
|
if (s === undefined || s === null) continue
|
|
504
|
-
if (
|
|
548
|
+
if (s === false) return false // boolean false sub-schema — complex
|
|
549
|
+
if (s === true) continue // boolean true sub-schema — always valid, fine
|
|
505
550
|
if (!codegenSafe(s, schemaMap)) return false
|
|
506
551
|
}
|
|
507
552
|
|
|
@@ -548,13 +593,14 @@ function compileToJSCodegen(schema, schemaMap) {
|
|
|
548
593
|
const ctx = { varCounter: 0, helpers: [], helperCode: [], closureVars: [], closureVals: [], rootDefs, refStack: new Set(), schemaMap: schemaMap || null }
|
|
549
594
|
const lines = []
|
|
550
595
|
genCode(schema, 'd', lines, ctx)
|
|
551
|
-
if (lines.length === 0) return () => true
|
|
552
596
|
|
|
553
|
-
// Append deferred checks (additionalProperties) at the end
|
|
597
|
+
// Append deferred checks (additionalProperties, unevaluatedProperties) at the end
|
|
554
598
|
if (ctx.deferredChecks) {
|
|
555
599
|
for (const dc of ctx.deferredChecks) lines.push(dc)
|
|
556
600
|
}
|
|
557
601
|
|
|
602
|
+
if (lines.length === 0) return () => true
|
|
603
|
+
|
|
558
604
|
const checkStr = lines.join('\n ')
|
|
559
605
|
|
|
560
606
|
// Regex and helpers are passed as closure variables (not re-created per call)
|
|
@@ -641,25 +687,32 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
641
687
|
if (typeof schema !== 'object' || schema === null) return
|
|
642
688
|
|
|
643
689
|
// $ref — guard against circular references
|
|
690
|
+
// In 2020-12 with unevaluated*, $ref can coexist with siblings — don't early return
|
|
691
|
+
// Only when THIS schema has unevaluated keywords directly (not via $ref target)
|
|
692
|
+
const hasSiblings = schema.$ref && (schema.unevaluatedProperties !== undefined || schema.unevaluatedItems !== undefined)
|
|
644
693
|
if (schema.$ref) {
|
|
645
694
|
// 1. Local ref
|
|
646
695
|
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
647
696
|
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
648
|
-
if (ctx.refStack.has(schema.$ref)) return
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
|
|
656
|
-
|
|
657
|
-
ctx.refStack.
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
697
|
+
if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
|
|
698
|
+
else {
|
|
699
|
+
ctx.refStack.add(schema.$ref)
|
|
700
|
+
genCode(ctx.rootDefs[m[1]], v, lines, ctx, knownType)
|
|
701
|
+
ctx.refStack.delete(schema.$ref)
|
|
702
|
+
if (!hasSiblings) return
|
|
703
|
+
}
|
|
704
|
+
} else if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
|
|
705
|
+
// 2. Cross-schema ref
|
|
706
|
+
if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
|
|
707
|
+
else {
|
|
708
|
+
ctx.refStack.add(schema.$ref)
|
|
709
|
+
genCode(ctx.schemaMap.get(schema.$ref), v, lines, ctx, knownType)
|
|
710
|
+
ctx.refStack.delete(schema.$ref)
|
|
711
|
+
if (!hasSiblings) return
|
|
712
|
+
}
|
|
713
|
+
} else {
|
|
714
|
+
if (!hasSiblings) return
|
|
661
715
|
}
|
|
662
|
-
return
|
|
663
716
|
}
|
|
664
717
|
|
|
665
718
|
// Determine the single known type after this schema's type check
|
|
@@ -744,6 +797,27 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
744
797
|
}
|
|
745
798
|
}
|
|
746
799
|
|
|
800
|
+
// Early key count for unevaluatedProperties: false (before properties, 10% faster)
|
|
801
|
+
// V8 branch prediction benefits from for-in iteration before property access
|
|
802
|
+
if (schema.unevaluatedProperties === false && schema.properties && schema.required && isObj) {
|
|
803
|
+
const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
804
|
+
if (!evalResult.dynamic && !evalResult.allProps) {
|
|
805
|
+
const knownKeys = evalResult.props
|
|
806
|
+
const propCount = knownKeys.length
|
|
807
|
+
const allRequired = schema.required.length >= propCount &&
|
|
808
|
+
knownKeys.every(k => schema.required.includes(k))
|
|
809
|
+
if (allRequired && propCount > 0) {
|
|
810
|
+
// Adaptive: for-in for <=15 keys (V8 fast path), Object.keys for >15
|
|
811
|
+
if (propCount <= 15) {
|
|
812
|
+
lines.push(`var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`)
|
|
813
|
+
} else {
|
|
814
|
+
lines.push(`if(Object.keys(${v}).length!==${propCount})return false`)
|
|
815
|
+
}
|
|
816
|
+
ctx._earlyKeyCount = true // flag to skip deferred check
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
747
821
|
// numeric — skip type guard if known numeric
|
|
748
822
|
if (schema.minimum !== undefined) lines.push(isNum ? `if(${v}<${schema.minimum})return false` : `if(typeof ${v}==='number'&&${v}<${schema.minimum})return false`)
|
|
749
823
|
if (schema.maximum !== undefined) lines.push(isNum ? `if(${v}>${schema.maximum})return false` : `if(typeof ${v}==='number'&&${v}>${schema.maximum})return false`)
|
|
@@ -764,10 +838,15 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
764
838
|
if (schema.maxProperties !== undefined) lines.push(`if(${objGuard}Object.keys(${v}).length>${schema.maxProperties})return false`)
|
|
765
839
|
|
|
766
840
|
if (schema.pattern) {
|
|
767
|
-
//
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
841
|
+
// Try inline charCode compilation for simple patterns (avoids RegExp engine)
|
|
842
|
+
const inlineCheck = compilePatternInline(schema.pattern, v)
|
|
843
|
+
if (inlineCheck) {
|
|
844
|
+
lines.push(isStr ? `if(!(${inlineCheck}))return false` : `if(typeof ${v}==='string'&&!(${inlineCheck}))return false`)
|
|
845
|
+
} else {
|
|
846
|
+
const ri = ctx.varCounter++
|
|
847
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(schema.pattern)})`)
|
|
848
|
+
lines.push(isStr ? `if(!_re${ri}.test(${v}))return false` : `if(typeof ${v}==='string'&&!_re${ri}.test(${v}))return false`)
|
|
849
|
+
}
|
|
771
850
|
}
|
|
772
851
|
|
|
773
852
|
if (schema.format) {
|
|
@@ -798,7 +877,9 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
798
877
|
const propCount = Object.keys(schema.properties).length
|
|
799
878
|
const allRequired = schema.required && schema.required.length === propCount
|
|
800
879
|
const inner = allRequired
|
|
801
|
-
?
|
|
880
|
+
? (propCount <= 15
|
|
881
|
+
? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
|
|
882
|
+
: `if(Object.keys(${v}).length!==${propCount})return false`)
|
|
802
883
|
: `for(var _k in ${v})if(${Object.keys(schema.properties).map(k => `_k!==${JSON.stringify(k)}`).join('&&')})return false`
|
|
803
884
|
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
804
885
|
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
@@ -852,16 +933,6 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
852
933
|
ctx._ppHandledAdditional = true
|
|
853
934
|
ctx._ppHandledPropertyNames = !!pn
|
|
854
935
|
const propKeys = Object.keys(schema.properties)
|
|
855
|
-
// Inline key comparison instead of Set.has for small property counts (faster, no allocation)
|
|
856
|
-
const keyCheck = propKeys.length <= 8
|
|
857
|
-
? propKeys.map(k => `${kVar}===${JSON.stringify(k)}`).join('||')
|
|
858
|
-
: null
|
|
859
|
-
if (!keyCheck) {
|
|
860
|
-
const allowedSet = `_as${pi}`
|
|
861
|
-
ctx.closureVars.push(allowedSet)
|
|
862
|
-
ctx.closureVals.push(new Set(propKeys))
|
|
863
|
-
}
|
|
864
|
-
|
|
865
936
|
lines.push(`${guard}{for(const ${kVar} in ${v}){`)
|
|
866
937
|
// propertyNames checks (merged into same loop)
|
|
867
938
|
if (pn) {
|
|
@@ -886,14 +957,21 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
886
957
|
lines.push(`if(!_es${ei}.has(${kVar}))return false`)
|
|
887
958
|
}
|
|
888
959
|
}
|
|
889
|
-
// Check: is key
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
960
|
+
// Check: is key declared or matches a pattern?
|
|
961
|
+
// switch/case: V8 compiles string cases to jump table (faster than chained ===)
|
|
962
|
+
const switchCases = propKeys.map(k => `case ${JSON.stringify(k)}:`).join('')
|
|
963
|
+
lines.push(`switch(${kVar}){${switchCases}break;default:`)
|
|
964
|
+
// Default: key is not declared — must match a pattern
|
|
965
|
+
let patternChecks = []
|
|
893
966
|
for (let i = 0; i < ppEntries.length; i++) {
|
|
894
|
-
|
|
967
|
+
patternChecks.push(`if(${matchers[i].check}){if(!_ppf${pi}_${i}(${v}[${kVar}]))return false}else{return false}`)
|
|
895
968
|
}
|
|
896
|
-
|
|
969
|
+
if (patternChecks.length > 0) {
|
|
970
|
+
lines.push(patternChecks.join(''))
|
|
971
|
+
} else {
|
|
972
|
+
lines.push(`return false`)
|
|
973
|
+
}
|
|
974
|
+
lines.push(`}`) // end switch
|
|
897
975
|
lines.push(`}}`)
|
|
898
976
|
} else {
|
|
899
977
|
// No additionalProperties: validate matching keys + propertyNames
|
|
@@ -1040,7 +1118,8 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1040
1118
|
}
|
|
1041
1119
|
|
|
1042
1120
|
// anyOf — need function wrappers since genCode uses return false
|
|
1043
|
-
if (
|
|
1121
|
+
// Skip standard anyOf if unevaluatedProperties will handle it (single-pass optimization)
|
|
1122
|
+
if (schema.anyOf && schema.unevaluatedProperties === undefined) {
|
|
1044
1123
|
const fns = []
|
|
1045
1124
|
for (let i = 0; i < schema.anyOf.length; i++) {
|
|
1046
1125
|
const subLines = []
|
|
@@ -1110,6 +1189,389 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1110
1189
|
lines.push(`{const _if${fi}=${ifFn};const _th${fi}=${thenFn};const _el${fi}=${elseFn}`)
|
|
1111
1190
|
lines.push(`if(_if${fi}(${v})){if(_th${fi}&&!_th${fi}(${v}))return false}else{if(_el${fi}&&!_el${fi}(${v}))return false}}`)
|
|
1112
1191
|
}
|
|
1192
|
+
|
|
1193
|
+
// unevaluatedProperties
|
|
1194
|
+
if (schema.unevaluatedProperties !== undefined) {
|
|
1195
|
+
const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
1196
|
+
|
|
1197
|
+
if (evalResult.allProps || schema.unevaluatedProperties === true) {
|
|
1198
|
+
// All props evaluated or unevaluatedProperties:true — no-op
|
|
1199
|
+
} else if (!evalResult.dynamic) {
|
|
1200
|
+
// Tier 1-2: all evaluated props known at compile-time — ZERO COST
|
|
1201
|
+
const knownKeys = evalResult.props
|
|
1202
|
+
const propCount = knownKeys.length
|
|
1203
|
+
|
|
1204
|
+
if (schema.unevaluatedProperties === false) {
|
|
1205
|
+
const allRequired = schema.required && schema.required.length >= propCount &&
|
|
1206
|
+
knownKeys.every(k => schema.required.includes(k))
|
|
1207
|
+
|
|
1208
|
+
let inner
|
|
1209
|
+
if (allRequired && propCount > 0) {
|
|
1210
|
+
// TRICK 1: required covers all — key count check only
|
|
1211
|
+
if (!ctx._earlyKeyCount) {
|
|
1212
|
+
// Adaptive: for-in for <=15 keys, Object.keys for >15
|
|
1213
|
+
inner = propCount <= 15
|
|
1214
|
+
? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
|
|
1215
|
+
: `if(Object.keys(${v}).length!==${propCount})return false`
|
|
1216
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1217
|
+
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1218
|
+
}
|
|
1219
|
+
// else: already emitted early (before properties)
|
|
1220
|
+
} else if (propCount > 0) {
|
|
1221
|
+
// TRICK 3: charCodeAt switch tree
|
|
1222
|
+
inner = genCharCodeSwitch(knownKeys, v)
|
|
1223
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1224
|
+
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1225
|
+
} else {
|
|
1226
|
+
inner = `for(var _k in ${v})return false`
|
|
1227
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1228
|
+
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1229
|
+
}
|
|
1230
|
+
} else if (typeof schema.unevaluatedProperties === 'object') {
|
|
1231
|
+
// unevaluatedProperties: {schema} — validate unknown keys
|
|
1232
|
+
const ui = ctx.varCounter++
|
|
1233
|
+
const ukVar = `_uk${ui}`
|
|
1234
|
+
const subLines = []
|
|
1235
|
+
genCode(schema.unevaluatedProperties, `${v}[${ukVar}]`, subLines, ctx)
|
|
1236
|
+
if (subLines.length > 0) {
|
|
1237
|
+
const check = subLines.join(';')
|
|
1238
|
+
const keyChecks = knownKeys.map(k => `${ukVar}===${JSON.stringify(k)}`).join('||')
|
|
1239
|
+
const skipKnown = knownKeys.length > 0 ? `if(${keyChecks})continue;` : ''
|
|
1240
|
+
const inner = `for(var ${ukVar} in ${v}){${skipKnown}${check}}`
|
|
1241
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1242
|
+
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
} else {
|
|
1246
|
+
// Tier 2.5 / Tier 3: dynamic — runtime tracking needed
|
|
1247
|
+
// Compute base props: only unconditionally evaluated (properties, allOf-static, $ref)
|
|
1248
|
+
const baseResult = { props: [], items: null, allProps: false, allItems: false, dynamic: false }
|
|
1249
|
+
if (schema.properties) {
|
|
1250
|
+
for (const k of Object.keys(schema.properties)) {
|
|
1251
|
+
if (!baseResult.props.includes(k)) baseResult.props.push(k)
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
if (schema.allOf) {
|
|
1255
|
+
for (const sub of schema.allOf) {
|
|
1256
|
+
const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
|
|
1257
|
+
if (!subR.dynamic && subR.props) {
|
|
1258
|
+
for (const k of subR.props) {
|
|
1259
|
+
if (!baseResult.props.includes(k)) baseResult.props.push(k)
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
const baseProps = baseResult.props
|
|
1265
|
+
const branchKeyword = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
|
|
1266
|
+
|
|
1267
|
+
if (schema.unevaluatedProperties === false) {
|
|
1268
|
+
if (schema.if && (schema.then || schema.else) && !branchKeyword && !schema.patternProperties && !schema.dependentSchemas) {
|
|
1269
|
+
// Tier 2.5: if/then/else — re-emit if function + branch-inline duplication
|
|
1270
|
+
// Can't reuse _if from above (block-scoped), so regenerate
|
|
1271
|
+
const ifLines2 = []
|
|
1272
|
+
genCode(schema.if, '_iv2', ifLines2, ctx)
|
|
1273
|
+
const ufi = ctx.varCounter++
|
|
1274
|
+
const ifFn2 = ifLines2.length === 0
|
|
1275
|
+
? `function(_iv2){return true}`
|
|
1276
|
+
: `function(_iv2){${ifLines2.join(';')};return true}`
|
|
1277
|
+
|
|
1278
|
+
// if props are only evaluated when if matches (spec: failed applicators produce no annotations)
|
|
1279
|
+
const ifProps = []
|
|
1280
|
+
if (schema.if && schema.if.properties) ifProps.push(...Object.keys(schema.if.properties))
|
|
1281
|
+
const thenEval = schema.then ? collectEvaluated(schema.then, ctx.schemaMap, ctx.rootDefs) : { props: [] }
|
|
1282
|
+
const elseEval = schema.else ? collectEvaluated(schema.else, ctx.schemaMap, ctx.rootDefs) : { props: [] }
|
|
1283
|
+
const uniqueThen = [...new Set([...baseProps, ...ifProps, ...(thenEval.props || [])])]
|
|
1284
|
+
const uniqueElse = [...new Set([...baseProps, ...(elseEval.props || [])])]
|
|
1285
|
+
|
|
1286
|
+
const thenCheck = genCharCodeSwitch(uniqueThen, v)
|
|
1287
|
+
const elseCheck = genCharCodeSwitch(uniqueElse, v)
|
|
1288
|
+
const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
1289
|
+
lines.push(`${guard}{const _uif${ufi}=${ifFn2};if(_uif${ufi}(${v})){${thenCheck}}else{${elseCheck}}}`)
|
|
1290
|
+
} else if (branchKeyword) {
|
|
1291
|
+
// Tier 3: anyOf/oneOf — runtime tracking
|
|
1292
|
+
const branches = schema[branchKeyword]
|
|
1293
|
+
const branchProps = []
|
|
1294
|
+
for (const sub of branches) {
|
|
1295
|
+
const subResult = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
|
|
1296
|
+
branchProps.push(subResult.props || [])
|
|
1297
|
+
}
|
|
1298
|
+
const allDynamicKeys = [...new Set(branchProps.flat())]
|
|
1299
|
+
const dynamicOnly = allDynamicKeys.filter(k => !baseProps.includes(k))
|
|
1300
|
+
|
|
1301
|
+
if (dynamicOnly.length > 0 && dynamicOnly.length <= 32) {
|
|
1302
|
+
// TRICK 5: bit-packed evaluated set — SINGLE PASS (validation + tracking combined)
|
|
1303
|
+
const ei = ctx.varCounter++
|
|
1304
|
+
const evVar = `_ev${ei}`
|
|
1305
|
+
const bitMap = new Map()
|
|
1306
|
+
dynamicOnly.forEach((k, i) => bitMap.set(k, i))
|
|
1307
|
+
const branchMasks = branchProps.map(props => {
|
|
1308
|
+
let mask = 0
|
|
1309
|
+
for (const p of props) {
|
|
1310
|
+
if (bitMap.has(p)) mask |= (1 << bitMap.get(p))
|
|
1311
|
+
}
|
|
1312
|
+
return mask
|
|
1313
|
+
})
|
|
1314
|
+
|
|
1315
|
+
// TRICK 4: Direct function calls — no array, no loop, V8 can inline
|
|
1316
|
+
const bfi = ctx.varCounter++
|
|
1317
|
+
lines.push(`{let ${evVar}=0`)
|
|
1318
|
+
const fnVars = []
|
|
1319
|
+
for (let i = 0; i < branches.length; i++) {
|
|
1320
|
+
const subLines2 = []
|
|
1321
|
+
genCode(branches[i], '_bv', subLines2, ctx)
|
|
1322
|
+
const fnVar = `_bf${bfi}_${i}`
|
|
1323
|
+
fnVars.push(fnVar)
|
|
1324
|
+
const fnBody = subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`
|
|
1325
|
+
lines.push(`const ${fnVar}=${fnBody}`)
|
|
1326
|
+
}
|
|
1327
|
+
if (branchKeyword === 'oneOf') {
|
|
1328
|
+
// oneOf: exactly one must match — direct calls
|
|
1329
|
+
lines.push(`let _oc${bfi}=0`)
|
|
1330
|
+
for (let i = 0; i < branches.length; i++) {
|
|
1331
|
+
lines.push(`if(${fnVars[i]}(${v})){_oc${bfi}++;${evVar}=${branchMasks[i]};if(_oc${bfi}>1)return false}`)
|
|
1332
|
+
}
|
|
1333
|
+
lines.push(`if(_oc${bfi}!==1)return false`)
|
|
1334
|
+
} else {
|
|
1335
|
+
// anyOf: at least one must match — direct calls, collect all
|
|
1336
|
+
lines.push(`let _am${bfi}=false`)
|
|
1337
|
+
for (let i = 0; i < branches.length; i++) {
|
|
1338
|
+
lines.push(`if(${fnVars[i]}(${v})){_am${bfi}=true;${evVar}|=${branchMasks[i]}}`)
|
|
1339
|
+
}
|
|
1340
|
+
lines.push(`if(!_am${bfi})return false`)
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// Final check: static keys inline + dynamic keys via bitmask
|
|
1344
|
+
const staticCheck = baseProps.length > 0 ? baseProps.map(k => `_k===${JSON.stringify(k)}`).join('||') : ''
|
|
1345
|
+
const groups = new Map()
|
|
1346
|
+
for (const k of dynamicOnly) {
|
|
1347
|
+
const cc = k.charCodeAt(0)
|
|
1348
|
+
if (!groups.has(cc)) groups.set(cc, [])
|
|
1349
|
+
groups.get(cc).push(k)
|
|
1350
|
+
}
|
|
1351
|
+
let switchCases = ''
|
|
1352
|
+
for (const [cc, groupKeys] of groups) {
|
|
1353
|
+
const cond = groupKeys.map(k => `_k===${JSON.stringify(k)}&&(${evVar}&${1 << bitMap.get(k)})`).join('||')
|
|
1354
|
+
switchCases += `case ${cc}:if(${cond})continue;break;`
|
|
1355
|
+
}
|
|
1356
|
+
const dynamicCheck = `switch(_k.charCodeAt(0)){${switchCases}default:break}`
|
|
1357
|
+
const inner = staticCheck
|
|
1358
|
+
? `for(var _k in ${v}){if(${staticCheck})continue;${dynamicCheck}return false}`
|
|
1359
|
+
: `for(var _k in ${v}){${dynamicCheck}return false}`
|
|
1360
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1361
|
+
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1362
|
+
} else {
|
|
1363
|
+
// Fallback: plain object tracking
|
|
1364
|
+
const ei = ctx.varCounter++
|
|
1365
|
+
const evVar = `_ev${ei}`
|
|
1366
|
+
const fns = []
|
|
1367
|
+
for (let i = 0; i < branches.length; i++) {
|
|
1368
|
+
const subLines2 = []
|
|
1369
|
+
genCode(branches[i], '_bv', subLines2, ctx)
|
|
1370
|
+
fns.push(subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`)
|
|
1371
|
+
}
|
|
1372
|
+
const bfi = ctx.varCounter++
|
|
1373
|
+
ctx.closureVars.push(`_bk${bfi}`)
|
|
1374
|
+
ctx.closureVals.push(branchProps)
|
|
1375
|
+
lines.push(`{const ${evVar}={}`)
|
|
1376
|
+
for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
|
|
1377
|
+
lines.push(`const _bf${bfi}=[${fns.join(',')}]`)
|
|
1378
|
+
if (branchKeyword === 'oneOf') {
|
|
1379
|
+
// Single pass: validate oneOf (exactly one) + track evaluated
|
|
1380
|
+
lines.push(`let _oc${bfi}=0;for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){_oc${bfi}++;for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1;if(_oc${bfi}>1)return false}}if(_oc${bfi}!==1)return false`)
|
|
1381
|
+
} else {
|
|
1382
|
+
// Single pass: validate anyOf (at least one) + track all matching
|
|
1383
|
+
lines.push(`let _am${bfi}=false;for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){_am${bfi}=true;for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1}}if(!_am${bfi})return false`)
|
|
1384
|
+
}
|
|
1385
|
+
const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
|
|
1386
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1387
|
+
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1388
|
+
}
|
|
1389
|
+
} else if (schema.dependentSchemas) {
|
|
1390
|
+
// dependentSchemas: conditional merge at runtime
|
|
1391
|
+
const ei = ctx.varCounter++
|
|
1392
|
+
const evVar = `_ev${ei}`
|
|
1393
|
+
lines.push(`{const ${evVar}={}`)
|
|
1394
|
+
for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
|
|
1395
|
+
for (const [trigger, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
1396
|
+
const depResult = collectEvaluated(depSchema, ctx.schemaMap, ctx.rootDefs)
|
|
1397
|
+
if (depResult.props && depResult.props.length > 0) {
|
|
1398
|
+
lines.push(`if(${JSON.stringify(trigger)} in ${v}){${depResult.props.map(k => `${evVar}[${JSON.stringify(k)}]=1`).join(';')}}`)
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
|
|
1402
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1403
|
+
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1404
|
+
} else if (schema.patternProperties) {
|
|
1405
|
+
// patternProperties: runtime key matching
|
|
1406
|
+
const ei = ctx.varCounter++
|
|
1407
|
+
const evVar = `_ev${ei}`
|
|
1408
|
+
lines.push(`{const ${evVar}={}`)
|
|
1409
|
+
for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
|
|
1410
|
+
const patterns = Object.keys(schema.patternProperties)
|
|
1411
|
+
const reVars = []
|
|
1412
|
+
for (const pat of patterns) {
|
|
1413
|
+
const ri = ctx.varCounter++
|
|
1414
|
+
ctx.closureVars.push(`_ure${ri}`)
|
|
1415
|
+
ctx.closureVals.push(new RegExp(pat))
|
|
1416
|
+
reVars.push(`_ure${ri}`)
|
|
1417
|
+
}
|
|
1418
|
+
const inner = `for(var _k in ${v}){if(${evVar}[_k])continue;${reVars.map(rv => `if(${rv}.test(_k)){${evVar}[_k]=1;continue}`).join('')}return false}`
|
|
1419
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1420
|
+
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1421
|
+
}
|
|
1422
|
+
} else if (typeof schema.unevaluatedProperties === 'object') {
|
|
1423
|
+
// Tier 3 with schema: validate unknown keys against sub-schema
|
|
1424
|
+
const ei = ctx.varCounter++
|
|
1425
|
+
const evVar = `_ev${ei}`
|
|
1426
|
+
const ukVar = `_uk${ei}`
|
|
1427
|
+
lines.push(`{const ${evVar}={}`)
|
|
1428
|
+
for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
|
|
1429
|
+
|
|
1430
|
+
if (branchKeyword) {
|
|
1431
|
+
const branches = schema[branchKeyword]
|
|
1432
|
+
const branchProps = []
|
|
1433
|
+
for (const sub of branches) {
|
|
1434
|
+
const subResult = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
|
|
1435
|
+
branchProps.push(subResult.props || [])
|
|
1436
|
+
}
|
|
1437
|
+
const fns = []
|
|
1438
|
+
for (let i = 0; i < branches.length; i++) {
|
|
1439
|
+
const subLines2 = []
|
|
1440
|
+
genCode(branches[i], '_bv', subLines2, ctx)
|
|
1441
|
+
fns.push(subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`)
|
|
1442
|
+
}
|
|
1443
|
+
const bfi = ctx.varCounter++
|
|
1444
|
+
ctx.closureVars.push(`_bk${bfi}`)
|
|
1445
|
+
ctx.closureVals.push(branchProps)
|
|
1446
|
+
lines.push(`const _bf${bfi}=[${fns.join(',')}]`)
|
|
1447
|
+
if (branchKeyword === 'oneOf') {
|
|
1448
|
+
lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1;break}}`)
|
|
1449
|
+
} else {
|
|
1450
|
+
lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1}}`)
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
const subLines2 = []
|
|
1455
|
+
genCode(schema.unevaluatedProperties, `${v}[${ukVar}]`, subLines2, ctx)
|
|
1456
|
+
if (subLines2.length > 0) {
|
|
1457
|
+
const check = subLines2.join(';')
|
|
1458
|
+
const inner = `for(var ${ukVar} in ${v}){if(${evVar}[${ukVar}])continue;${check}}`
|
|
1459
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1460
|
+
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1461
|
+
} else {
|
|
1462
|
+
lines.push('}')
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// unevaluatedItems
|
|
1469
|
+
if (schema.unevaluatedItems !== undefined) {
|
|
1470
|
+
const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
1471
|
+
|
|
1472
|
+
if (evalResult.allItems || schema.unevaluatedItems === true) {
|
|
1473
|
+
// All items evaluated or unevaluatedItems:true — no-op
|
|
1474
|
+
} else if (!evalResult.dynamic) {
|
|
1475
|
+
// Static: all evaluated items known at compile-time
|
|
1476
|
+
if (schema.unevaluatedItems === false) {
|
|
1477
|
+
// TRICK 6: Array.length comparison only
|
|
1478
|
+
const maxIdx = evalResult.items || 0
|
|
1479
|
+
const inner = `if(${v}.length>${maxIdx})return false`
|
|
1480
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1481
|
+
ctx.deferredChecks.push(isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
1482
|
+
} else if (typeof schema.unevaluatedItems === 'object') {
|
|
1483
|
+
const maxIdx = evalResult.items || 0
|
|
1484
|
+
const ui = ctx.varCounter++
|
|
1485
|
+
const elemVar = `_ue${ui}`
|
|
1486
|
+
const idxVar = `_ui${ui}`
|
|
1487
|
+
const subLines = []
|
|
1488
|
+
genCode(schema.unevaluatedItems, elemVar, subLines, ctx)
|
|
1489
|
+
if (subLines.length > 0) {
|
|
1490
|
+
const check = subLines.join(';')
|
|
1491
|
+
const inner = `for(let ${idxVar}=${maxIdx};${idxVar}<${v}.length;${idxVar}++){const ${elemVar}=${v}[${idxVar}];${check}}`
|
|
1492
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1493
|
+
ctx.deferredChecks.push(isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
} else {
|
|
1497
|
+
// Dynamic: runtime tracking of max evaluated index
|
|
1498
|
+
const baseIdx = evalResult.items || 0
|
|
1499
|
+
const branchKeyword = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
|
|
1500
|
+
|
|
1501
|
+
if (branchKeyword && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
|
|
1502
|
+
// anyOf/oneOf: each branch may evaluate different number of items
|
|
1503
|
+
const branches = schema[branchKeyword]
|
|
1504
|
+
const branchMaxIdx = []
|
|
1505
|
+
for (const sub of branches) {
|
|
1506
|
+
const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
|
|
1507
|
+
branchMaxIdx.push(subR.items || 0)
|
|
1508
|
+
}
|
|
1509
|
+
// Runtime: find max evaluated index across all matching branches
|
|
1510
|
+
const fns = []
|
|
1511
|
+
for (let i = 0; i < branches.length; i++) {
|
|
1512
|
+
const subLines2 = []
|
|
1513
|
+
genCode(branches[i], '_bv', subLines2, ctx)
|
|
1514
|
+
fns.push(subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`)
|
|
1515
|
+
}
|
|
1516
|
+
const bfi = ctx.varCounter++
|
|
1517
|
+
const ei = ctx.varCounter++
|
|
1518
|
+
const evVar = `_eidx${ei}`
|
|
1519
|
+
lines.push(`{let ${evVar}=${baseIdx}`)
|
|
1520
|
+
lines.push(`const _bf${bfi}=[${fns.join(',')}]`)
|
|
1521
|
+
const maxExprs = branchMaxIdx.map((m, i) => `_bi===${i}?${Math.max(m, baseIdx)}`).join(':') + `:${baseIdx}`
|
|
1522
|
+
if (branchKeyword === 'oneOf') {
|
|
1523
|
+
lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){${evVar}=${maxExprs};break}}`)
|
|
1524
|
+
} else {
|
|
1525
|
+
lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){const _m=${maxExprs};if(_m>${evVar})${evVar}=_m}}`)
|
|
1526
|
+
}
|
|
1527
|
+
if (schema.unevaluatedItems === false) {
|
|
1528
|
+
const inner = `if(${v}.length>${evVar})return false`
|
|
1529
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1530
|
+
ctx.deferredChecks.push(isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
|
|
1531
|
+
} else {
|
|
1532
|
+
const ui = ctx.varCounter++
|
|
1533
|
+
const elemVar = `_ue${ui}`
|
|
1534
|
+
const idxVar = `_ui${ui}`
|
|
1535
|
+
const subLines = []
|
|
1536
|
+
genCode(schema.unevaluatedItems, elemVar, subLines, ctx)
|
|
1537
|
+
if (subLines.length > 0) {
|
|
1538
|
+
const check = subLines.join(';')
|
|
1539
|
+
const inner = `for(let ${idxVar}=${evVar};${idxVar}<${v}.length;${idxVar}++){const ${elemVar}=${v}[${idxVar}];${check}}`
|
|
1540
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1541
|
+
ctx.deferredChecks.push(isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
|
|
1542
|
+
} else {
|
|
1543
|
+
lines.push('}')
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
} else if (schema.if && (schema.then || schema.else) && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
|
|
1547
|
+
// if/then/else: branch-specific max index
|
|
1548
|
+
const ifEval = collectEvaluated(schema.if, ctx.schemaMap, ctx.rootDefs)
|
|
1549
|
+
const thenEval = schema.then ? collectEvaluated(schema.then, ctx.schemaMap, ctx.rootDefs) : { items: null }
|
|
1550
|
+
const elseEval = schema.else ? collectEvaluated(schema.else, ctx.schemaMap, ctx.rootDefs) : { items: null }
|
|
1551
|
+
const ifIdx = ifEval.items || 0
|
|
1552
|
+
const thenIdx = Math.max(baseIdx, ifIdx, thenEval.items || 0)
|
|
1553
|
+
const elseIdx = Math.max(baseIdx, elseEval.items || 0)
|
|
1554
|
+
|
|
1555
|
+
const ifLines2 = []
|
|
1556
|
+
genCode(schema.if, '_iv3', ifLines2, ctx)
|
|
1557
|
+
const ufi = ctx.varCounter++
|
|
1558
|
+
const ifFn3 = ifLines2.length === 0
|
|
1559
|
+
? `function(_iv3){return true}`
|
|
1560
|
+
: `function(_iv3){${ifLines2.join(';')};return true}`
|
|
1561
|
+
|
|
1562
|
+
if (schema.unevaluatedItems === false) {
|
|
1563
|
+
const guard = isArr ? '' : `if(Array.isArray(${v}))`
|
|
1564
|
+
lines.push(`${guard}{const _uif${ufi}=${ifFn3};if(_uif${ufi}(${v})){if(${v}.length>${thenIdx})return false}else{if(${v}.length>${elseIdx})return false}}`)
|
|
1565
|
+
}
|
|
1566
|
+
} else if (schema.unevaluatedItems === false) {
|
|
1567
|
+
// Fallback: use static base index (may not be fully correct for all dynamic cases)
|
|
1568
|
+
const maxIdx = evalResult.items || 0
|
|
1569
|
+
const inner = `if(${v}.length>${maxIdx})return false`
|
|
1570
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1571
|
+
ctx.deferredChecks.push(isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1113
1575
|
}
|
|
1114
1576
|
|
|
1115
1577
|
const FORMAT_CODEGEN = {
|
|
@@ -1154,6 +1616,79 @@ const FORMAT_CODEGEN = {
|
|
|
1154
1616
|
// Safe key escaping: use JSON.stringify to handle all special chars (newlines, null bytes, etc.)
|
|
1155
1617
|
function esc(s) { return JSON.stringify(s).slice(1, -1) }
|
|
1156
1618
|
|
|
1619
|
+
// Resolve child path at codegen time when parent is a static string literal.
|
|
1620
|
+
// This enables frozen pre-allocation for ALL nested error objects.
|
|
1621
|
+
function childPathExpr(parentExpr, suffix) {
|
|
1622
|
+
if (!parentExpr) return `'/${suffix}'`
|
|
1623
|
+
if (parentExpr.startsWith("'") && !parentExpr.includes('+')) {
|
|
1624
|
+
// Static parent: resolve at codegen time → '/parent/child' (single literal)
|
|
1625
|
+
return `'${parentExpr.slice(1, -1)}/${suffix}'`
|
|
1626
|
+
}
|
|
1627
|
+
// Dynamic parent: keep as concat expression
|
|
1628
|
+
return `${parentExpr}+'/${suffix}'`
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// Compile simple regex patterns to inline charCode checks — avoids RegExp engine overhead.
|
|
1632
|
+
// Returns null if pattern is too complex for inline compilation.
|
|
1633
|
+
// Handles: ^[charclass]{n}$, ^[charclass]+$, ^[charclass]*$, ^[charclass]{m,n}$
|
|
1634
|
+
function compilePatternInline(pattern, varName) {
|
|
1635
|
+
// Match: ^[chars]{exact}$ — e.g., ^[0-9]{5}$
|
|
1636
|
+
let m = pattern.match(/^\^(\[[\w\-]+\])\{(\d+)\}\$$/)
|
|
1637
|
+
if (m) {
|
|
1638
|
+
const rangeCheck = charClassToCheck(m[1], `${varName}.charCodeAt(_pi)`)
|
|
1639
|
+
if (!rangeCheck) return null
|
|
1640
|
+
const len = parseInt(m[2])
|
|
1641
|
+
return `${varName}.length===${len}&&(()=>{for(let _pi=0;_pi<${len};_pi++){if(!(${rangeCheck}))return false}return true})()`
|
|
1642
|
+
}
|
|
1643
|
+
// Match: ^[chars]+$ — e.g., ^[a-z]+$
|
|
1644
|
+
m = pattern.match(/^\^(\[[\w\-]+\])\+\$$/)
|
|
1645
|
+
if (m) {
|
|
1646
|
+
const rangeCheck = charClassToCheck(m[1], `${varName}.charCodeAt(_pi)`)
|
|
1647
|
+
if (!rangeCheck) return null
|
|
1648
|
+
return `${varName}.length>0&&(()=>{for(let _pi=0;_pi<${varName}.length;_pi++){if(!(${rangeCheck}))return false}return true})()`
|
|
1649
|
+
}
|
|
1650
|
+
// Match: ^[chars]{m,n}$ — e.g., ^[a-zA-Z]{2,50}$
|
|
1651
|
+
m = pattern.match(/^\^(\[[\w\-]+\])\{(\d+),(\d+)\}\$$/)
|
|
1652
|
+
if (m) {
|
|
1653
|
+
const rangeCheck = charClassToCheck(m[1], `${varName}.charCodeAt(_pi)`)
|
|
1654
|
+
if (!rangeCheck) return null
|
|
1655
|
+
const min = parseInt(m[2]), max = parseInt(m[3])
|
|
1656
|
+
return `${varName}.length>=${min}&&${varName}.length<=${max}&&(()=>{for(let _pi=0;_pi<${varName}.length;_pi++){if(!(${rangeCheck}))return false}return true})()`
|
|
1657
|
+
}
|
|
1658
|
+
return null
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// Convert [charclass] to charCode range check expression.
|
|
1662
|
+
// Supports: [0-9], [a-z], [A-Z], [a-zA-Z], [a-zA-Z0-9], [0-9a-f], etc.
|
|
1663
|
+
function charClassToCheck(charClass, codeExpr) {
|
|
1664
|
+
// Strip brackets
|
|
1665
|
+
const inner = charClass.slice(1, -1)
|
|
1666
|
+
// Parse ranges
|
|
1667
|
+
const ranges = []
|
|
1668
|
+
let i = 0
|
|
1669
|
+
while (i < inner.length) {
|
|
1670
|
+
if (i + 2 < inner.length && inner[i + 1] === '-') {
|
|
1671
|
+
ranges.push([inner.charCodeAt(i), inner.charCodeAt(i + 2)])
|
|
1672
|
+
i += 3
|
|
1673
|
+
} else {
|
|
1674
|
+
ranges.push([inner.charCodeAt(i), inner.charCodeAt(i)])
|
|
1675
|
+
i++
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
if (ranges.length === 0) return null
|
|
1679
|
+
// Generate check: (c >= 48 && c <= 57) || (c >= 65 && c <= 90)
|
|
1680
|
+
const checks = ranges.map(([lo, hi]) =>
|
|
1681
|
+
lo === hi ? `${codeExpr}===${lo}` : `(${codeExpr}>=${lo}&&${codeExpr}<=${hi})`
|
|
1682
|
+
)
|
|
1683
|
+
return checks.join('||')
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// Same but for dynamic segments (array indices)
|
|
1687
|
+
function childPathDynExpr(parentExpr, indexExpr) {
|
|
1688
|
+
if (!parentExpr) return `'/'+${indexExpr}`
|
|
1689
|
+
return `${parentExpr}+'/'+${indexExpr}`
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1157
1692
|
// Detect simple prefix patterns like "^x-", "^_", "^prefix" and generate fast charCodeAt checks
|
|
1158
1693
|
// Returns a JS expression string or null if pattern is too complex
|
|
1159
1694
|
function fastPrefixCheck(pattern, keyVar) {
|
|
@@ -1172,14 +1707,45 @@ function fastPrefixCheck(pattern, keyVar) {
|
|
|
1172
1707
|
return `${keyVar}.startsWith(${JSON.stringify(prefix)})`
|
|
1173
1708
|
}
|
|
1174
1709
|
|
|
1710
|
+
// Generate a charCodeAt(0)-based switch tree for fast key validation.
|
|
1711
|
+
// V8 compiles switch to jump tables — O(1) dispatch vs O(n) chain.
|
|
1712
|
+
function genCharCodeSwitch(keys, v) {
|
|
1713
|
+
if (keys.length === 0) return `for(var _k in ${v})return false`
|
|
1714
|
+
if (keys.length <= 3) {
|
|
1715
|
+
// Small set: simple chain is faster than switch overhead
|
|
1716
|
+
return `for(var _k in ${v})if(${keys.map(k => `_k!==${JSON.stringify(k)}`).join('&&')})return false`
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// Group keys by first charCode
|
|
1720
|
+
const groups = new Map()
|
|
1721
|
+
for (const k of keys) {
|
|
1722
|
+
const cc = k.charCodeAt(0)
|
|
1723
|
+
if (!groups.has(cc)) groups.set(cc, [])
|
|
1724
|
+
groups.get(cc).push(k)
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
let cases = ''
|
|
1728
|
+
for (const [cc, groupKeys] of groups) {
|
|
1729
|
+
const cond = groupKeys.map(k => `_k===${JSON.stringify(k)}`).join('||')
|
|
1730
|
+
cases += `case ${cc}:if(${cond})continue;break;`
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
return `for(var _k in ${v}){switch(_k.charCodeAt(0)){${cases}default:break}return false}`
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1175
1736
|
// --- Error-collecting codegen: same checks, but pushes errors instead of returning false ---
|
|
1176
1737
|
// Returns a function: (data, allErrors) => { valid, errors }
|
|
1177
1738
|
// Valid path is still fast — only error path does extra work.
|
|
1178
1739
|
function compileToJSCodegenWithErrors(schema, schemaMap) {
|
|
1740
|
+
// Bail on unevaluated keywords — error codegen doesn't support them yet
|
|
1741
|
+
if (typeof schema === 'object' && schema !== null) {
|
|
1742
|
+
const s = JSON.stringify(schema)
|
|
1743
|
+
if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
|
|
1744
|
+
}
|
|
1179
1745
|
if (typeof schema === 'boolean') {
|
|
1180
1746
|
return schema
|
|
1181
1747
|
? () => ({ valid: true, errors: [] })
|
|
1182
|
-
: () => ({ valid: false, errors: [{
|
|
1748
|
+
: () => ({ valid: false, errors: [{ keyword: 'false schema', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
|
|
1183
1749
|
}
|
|
1184
1750
|
if (typeof schema !== 'object' || schema === null) return null
|
|
1185
1751
|
if (!codegenSafe(schema, schemaMap)) return null
|
|
@@ -1206,7 +1772,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap) {
|
|
|
1206
1772
|
|
|
1207
1773
|
const ctx = { varCounter: 0, helperCode: [], rootDefs: schema.$defs || schema.definitions || null, refStack: new Set(), schemaMap: schemaMap || null }
|
|
1208
1774
|
const lines = []
|
|
1209
|
-
genCodeE(schema, 'd', '', lines, ctx)
|
|
1775
|
+
genCodeE(schema, 'd', '', lines, ctx, '#')
|
|
1210
1776
|
if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
|
|
1211
1777
|
|
|
1212
1778
|
const body = `const _e=[];\n ` +
|
|
@@ -1225,7 +1791,8 @@ function compileToJSCodegenWithErrors(schema, schemaMap) {
|
|
|
1225
1791
|
// Error-collecting code generator.
|
|
1226
1792
|
// Instead of `return false`, pushes to `_e` array and optionally early-returns.
|
|
1227
1793
|
// `_all` parameter: if falsy, return after first error.
|
|
1228
|
-
function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
1794
|
+
function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
1795
|
+
if (!schemaPrefix) schemaPrefix = '#'
|
|
1229
1796
|
if (typeof schema !== 'object' || schema === null) return
|
|
1230
1797
|
|
|
1231
1798
|
// $ref — resolve local and cross-schema refs
|
|
@@ -1234,14 +1801,14 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1234
1801
|
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
1235
1802
|
if (ctx.refStack.has(schema.$ref)) return
|
|
1236
1803
|
ctx.refStack.add(schema.$ref)
|
|
1237
|
-
genCodeE(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx)
|
|
1804
|
+
genCodeE(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx, schemaPrefix)
|
|
1238
1805
|
ctx.refStack.delete(schema.$ref)
|
|
1239
1806
|
return
|
|
1240
1807
|
}
|
|
1241
1808
|
if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
|
|
1242
1809
|
if (ctx.refStack.has(schema.$ref)) return
|
|
1243
1810
|
ctx.refStack.add(schema.$ref)
|
|
1244
|
-
genCodeE(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx)
|
|
1811
|
+
genCodeE(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx, schemaPrefix)
|
|
1245
1812
|
ctx.refStack.delete(schema.$ref)
|
|
1246
1813
|
return
|
|
1247
1814
|
}
|
|
@@ -1262,7 +1829,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1262
1829
|
}
|
|
1263
1830
|
})
|
|
1264
1831
|
const expected = types.join(', ')
|
|
1265
|
-
lines.push(`if(!(${conds.join('||')})){_e.push({
|
|
1832
|
+
lines.push(`if(!(${conds.join('||')})){_e.push({keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/type',params:{type:'${expected}'},message:'must be ${expected}'});if(!_all)return{valid:false,errors:_e}}`)
|
|
1266
1833
|
}
|
|
1267
1834
|
|
|
1268
1835
|
// In error mode, never assume type — always guard (data may have failed type check but allErrors continues)
|
|
@@ -1271,7 +1838,10 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1271
1838
|
const isStr = false
|
|
1272
1839
|
const isNum = false
|
|
1273
1840
|
|
|
1274
|
-
const fail = (
|
|
1841
|
+
const fail = (keyword, schemaSuffix, paramsCode, msgCode) => {
|
|
1842
|
+
const sp = schemaPrefix + '/' + schemaSuffix
|
|
1843
|
+
return `_e.push({keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}});if(!_all)return{valid:false,errors:_e}`
|
|
1844
|
+
}
|
|
1275
1845
|
|
|
1276
1846
|
// enum
|
|
1277
1847
|
if (schema.enum) {
|
|
@@ -1281,21 +1851,21 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1281
1851
|
const primChecks = primitives.map(p => `${v}===${JSON.stringify(p)}`).join('||')
|
|
1282
1852
|
const objChecks = objects.map(o => `JSON.stringify(${v})===${JSON.stringify(JSON.stringify(o))}`).join('||')
|
|
1283
1853
|
const allChecks = [primChecks, objChecks].filter(Boolean).join('||')
|
|
1284
|
-
lines.push(`if(!(${allChecks || 'false'})){${fail('
|
|
1854
|
+
lines.push(`if(!(${allChecks || 'false'})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
|
|
1285
1855
|
}
|
|
1286
1856
|
|
|
1287
1857
|
// const — use canonical (sorted-key) comparison for objects
|
|
1288
1858
|
if (schema.const !== undefined) {
|
|
1289
1859
|
const cv = schema.const
|
|
1290
1860
|
if (cv === null || typeof cv !== 'object') {
|
|
1291
|
-
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('
|
|
1861
|
+
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
|
|
1292
1862
|
} else {
|
|
1293
1863
|
// Pre-compute canonical form of const value
|
|
1294
1864
|
const ci = ctx.varCounter++
|
|
1295
1865
|
const canonFn = `_cnE${ci}`
|
|
1296
1866
|
ctx.helperCode.push(`const ${canonFn}=function(x){if(x===null||typeof x!=='object')return JSON.stringify(x);if(Array.isArray(x))return'['+x.map(${canonFn}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+${canonFn}(x[k])}).join(',')+'}'};`)
|
|
1297
1867
|
const expected = canonFn + '(' + JSON.stringify(cv) + ')'
|
|
1298
|
-
lines.push(`if(${canonFn}(${v})!==${expected}){${fail('
|
|
1868
|
+
lines.push(`if(${canonFn}(${v})!==${expected}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
|
|
1299
1869
|
}
|
|
1300
1870
|
}
|
|
1301
1871
|
|
|
@@ -1304,49 +1874,54 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1304
1874
|
const hoisted = {}
|
|
1305
1875
|
if (schema.required) {
|
|
1306
1876
|
for (const key of schema.required) {
|
|
1307
|
-
|
|
1308
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){_e.push({code:'required_missing',path:${p},message:'missing required: ${esc(key)}'});if(!_all)return{valid:false,errors:_e}}`)
|
|
1877
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){_e.push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required',params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"});if(!_all)return{valid:false,errors:_e}}`)
|
|
1309
1878
|
}
|
|
1310
1879
|
}
|
|
1311
1880
|
|
|
1312
1881
|
// numeric
|
|
1313
1882
|
if (schema.minimum !== undefined) {
|
|
1314
1883
|
const c = isNum ? `${v}<${schema.minimum}` : `typeof ${v}==='number'&&${v}<${schema.minimum}`
|
|
1315
|
-
lines.push(`if(${c}){${fail('
|
|
1884
|
+
lines.push(`if(${c}){${fail('minimum', 'minimum', `{comparison:'>=',limit:${schema.minimum}}`, `'must be >= ${schema.minimum}'`)}}`)
|
|
1316
1885
|
}
|
|
1317
1886
|
if (schema.maximum !== undefined) {
|
|
1318
1887
|
const c = isNum ? `${v}>${schema.maximum}` : `typeof ${v}==='number'&&${v}>${schema.maximum}`
|
|
1319
|
-
lines.push(`if(${c}){${fail('
|
|
1888
|
+
lines.push(`if(${c}){${fail('maximum', 'maximum', `{comparison:'<=',limit:${schema.maximum}}`, `'must be <= ${schema.maximum}'`)}}`)
|
|
1320
1889
|
}
|
|
1321
1890
|
if (schema.exclusiveMinimum !== undefined) {
|
|
1322
1891
|
const c = isNum ? `${v}<=${schema.exclusiveMinimum}` : `typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum}`
|
|
1323
|
-
lines.push(`if(${c}){${fail('
|
|
1892
|
+
lines.push(`if(${c}){${fail('exclusiveMinimum', 'exclusiveMinimum', `{comparison:'>',limit:${schema.exclusiveMinimum}}`, `'must be > ${schema.exclusiveMinimum}'`)}}`)
|
|
1324
1893
|
}
|
|
1325
1894
|
if (schema.exclusiveMaximum !== undefined) {
|
|
1326
1895
|
const c = isNum ? `${v}>=${schema.exclusiveMaximum}` : `typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum}`
|
|
1327
|
-
lines.push(`if(${c}){${fail('
|
|
1896
|
+
lines.push(`if(${c}){${fail('exclusiveMaximum', 'exclusiveMaximum', `{comparison:'<',limit:${schema.exclusiveMaximum}}`, `'must be < ${schema.exclusiveMaximum}'`)}}`)
|
|
1328
1897
|
}
|
|
1329
1898
|
if (schema.multipleOf !== undefined) {
|
|
1330
1899
|
const m = schema.multipleOf
|
|
1331
1900
|
const ci = ctx.varCounter++
|
|
1332
1901
|
// Use tolerance-based check for floating point (matches C++ behavior)
|
|
1333
|
-
lines.push(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('
|
|
1902
|
+
lines.push(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('multipleOf', 'multipleOf', `{multipleOf:${m}}`, `'must be multiple of ${m}'`)}}}`)
|
|
1334
1903
|
}
|
|
1335
1904
|
|
|
1336
1905
|
// string
|
|
1337
1906
|
if (schema.minLength !== undefined) {
|
|
1338
1907
|
const c = isStr ? `${v}.length<${schema.minLength}` : `typeof ${v}==='string'&&${v}.length<${schema.minLength}`
|
|
1339
|
-
lines.push(`if(${c}){${fail('
|
|
1908
|
+
lines.push(`if(${c}){${fail('minLength', 'minLength', `{limit:${schema.minLength}}`, `'must NOT have fewer than ${schema.minLength} characters'`)}}`)
|
|
1340
1909
|
}
|
|
1341
1910
|
if (schema.maxLength !== undefined) {
|
|
1342
1911
|
const c = isStr ? `${v}.length>${schema.maxLength}` : `typeof ${v}==='string'&&${v}.length>${schema.maxLength}`
|
|
1343
|
-
lines.push(`if(${c}){${fail('
|
|
1912
|
+
lines.push(`if(${c}){${fail('maxLength', 'maxLength', `{limit:${schema.maxLength}}`, `'must NOT have more than ${schema.maxLength} characters'`)}}`)
|
|
1344
1913
|
}
|
|
1345
1914
|
if (schema.pattern) {
|
|
1346
|
-
const
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1915
|
+
const inlineCheck = compilePatternInline(schema.pattern, v)
|
|
1916
|
+
if (inlineCheck) {
|
|
1917
|
+
const c = isStr ? `!(${inlineCheck})` : `typeof ${v}==='string'&&!(${inlineCheck})`
|
|
1918
|
+
lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, `'must match pattern "${schema.pattern}"'`)}}`)
|
|
1919
|
+
} else {
|
|
1920
|
+
const ri = ctx.varCounter++
|
|
1921
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(schema.pattern)})`)
|
|
1922
|
+
const c = isStr ? `!_re${ri}.test(${v})` : `typeof ${v}==='string'&&!_re${ri}.test(${v})`
|
|
1923
|
+
lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, `'must match pattern "${schema.pattern}"'`)}}`)
|
|
1924
|
+
}
|
|
1350
1925
|
}
|
|
1351
1926
|
if (schema.format) {
|
|
1352
1927
|
const fc = FORMAT_CODEGEN[schema.format]
|
|
@@ -1357,7 +1932,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1357
1932
|
boolLines.push(fc(v, isStr))
|
|
1358
1933
|
// Replace `return false` with error push in the format check
|
|
1359
1934
|
const fmtCode = boolLines.join(';').replace(/return false/g,
|
|
1360
|
-
`{_e.push({
|
|
1935
|
+
`{_e.push({keyword:'format',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/format',params:{format:'${esc(schema.format)}'},message:'must match format "${esc(schema.format)}"'});if(!_all)return{valid:false,errors:_e}}`)
|
|
1361
1936
|
lines.push(fmtCode)
|
|
1362
1937
|
}
|
|
1363
1938
|
}
|
|
@@ -1365,37 +1940,44 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1365
1940
|
// array size
|
|
1366
1941
|
if (schema.minItems !== undefined) {
|
|
1367
1942
|
const c = isArr ? `${v}.length<${schema.minItems}` : `Array.isArray(${v})&&${v}.length<${schema.minItems}`
|
|
1368
|
-
lines.push(`if(${c}){${fail('
|
|
1943
|
+
lines.push(`if(${c}){${fail('minItems', 'minItems', `{limit:${schema.minItems}}`, `'must NOT have fewer than ${schema.minItems} items'`)}}`)
|
|
1369
1944
|
}
|
|
1370
1945
|
if (schema.maxItems !== undefined) {
|
|
1371
1946
|
const c = isArr ? `${v}.length>${schema.maxItems}` : `Array.isArray(${v})&&${v}.length>${schema.maxItems}`
|
|
1372
|
-
lines.push(`if(${c}){${fail('
|
|
1947
|
+
lines.push(`if(${c}){${fail('maxItems', 'maxItems', `{limit:${schema.maxItems}}`, `'must NOT have more than ${schema.maxItems} items'`)}}`)
|
|
1373
1948
|
}
|
|
1374
1949
|
|
|
1375
|
-
// uniqueItems
|
|
1950
|
+
// uniqueItems — tiered: small primitive arrays use nested loop (no allocation)
|
|
1376
1951
|
if (schema.uniqueItems) {
|
|
1377
1952
|
const si = ctx.varCounter++
|
|
1378
1953
|
const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
|
|
1379
1954
|
const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1955
|
+
const maxItems = schema.maxItems
|
|
1956
|
+
const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
|
|
1957
|
+
let inner
|
|
1958
|
+
if (isPrim && maxItems && maxItems <= 16) {
|
|
1959
|
+
inner = `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k]){${failExpr('_k', '_i')};break}}}`
|
|
1960
|
+
} else if (isPrim) {
|
|
1961
|
+
inner = `const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _prev=_s${si}.get(${v}[_i]);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(${v}[_i],_i)}`
|
|
1962
|
+
} else {
|
|
1963
|
+
inner = `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 Map();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);const _prev=_s${si}.get(_k);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(_k,_i)}`
|
|
1964
|
+
}
|
|
1383
1965
|
lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
|
|
1384
1966
|
}
|
|
1385
1967
|
|
|
1386
1968
|
// object size
|
|
1387
1969
|
if (schema.minProperties !== undefined) {
|
|
1388
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length<${schema.minProperties}){${fail('
|
|
1970
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length<${schema.minProperties}){${fail('minProperties', 'minProperties', `{limit:${schema.minProperties}}`, `'must NOT have fewer than ${schema.minProperties} properties'`)}}`)
|
|
1389
1971
|
}
|
|
1390
1972
|
if (schema.maxProperties !== undefined) {
|
|
1391
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length>${schema.maxProperties}){${fail('
|
|
1973
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length>${schema.maxProperties}){${fail('maxProperties', 'maxProperties', `{limit:${schema.maxProperties}}`, `'must NOT have more than ${schema.maxProperties} properties'`)}}`)
|
|
1392
1974
|
}
|
|
1393
1975
|
|
|
1394
1976
|
// additionalProperties: false
|
|
1395
1977
|
if (schema.additionalProperties === false && schema.properties) {
|
|
1396
1978
|
const allowed = Object.keys(schema.properties).map(k => `${JSON.stringify(k)}`).join(',')
|
|
1397
1979
|
const ci = ctx.varCounter++
|
|
1398
|
-
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])){_e.push({
|
|
1980
|
+
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])){_e.push({keyword:'additionalProperties',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/additionalProperties',params:{additionalProperty:_k${ci}[_i]},message:'must NOT have additional properties'});if(!_all)return{valid:false,errors:_e}}}`
|
|
1399
1981
|
lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1400
1982
|
}
|
|
1401
1983
|
|
|
@@ -1403,8 +1985,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1403
1985
|
if (schema.dependentRequired) {
|
|
1404
1986
|
for (const [key, deps] of Object.entries(schema.dependentRequired)) {
|
|
1405
1987
|
for (const dep of deps) {
|
|
1406
|
-
|
|
1407
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){_e.push({code:'required_missing',path:${p},message:'${esc(key)} requires ${esc(dep)}'});if(!_all)return{valid:false,errors:_e}}`)
|
|
1988
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){_e.push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired',params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"});if(!_all)return{valid:false,errors:_e}}`)
|
|
1408
1989
|
}
|
|
1409
1990
|
}
|
|
1410
1991
|
}
|
|
@@ -1412,9 +1993,9 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1412
1993
|
// properties — always guard (error mode, data may not be an object or may be array)
|
|
1413
1994
|
if (schema.properties) {
|
|
1414
1995
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
1415
|
-
const childPath = pathExpr
|
|
1996
|
+
const childPath = childPathExpr(pathExpr, esc(key))
|
|
1416
1997
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
1417
|
-
genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx)
|
|
1998
|
+
genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
|
|
1418
1999
|
lines.push(`}`)
|
|
1419
2000
|
}
|
|
1420
2001
|
}
|
|
@@ -1427,7 +2008,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1427
2008
|
const ki = ctx.varCounter++
|
|
1428
2009
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){if(_re${ri}.test(_k${ki})){`)
|
|
1429
2010
|
const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
|
|
1430
|
-
genCodeE(sub, `${v}[_k${ki}]`, p, lines, ctx)
|
|
2011
|
+
genCodeE(sub, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix+'/patternProperties')
|
|
1431
2012
|
lines.push(`}}}`)
|
|
1432
2013
|
}
|
|
1433
2014
|
}
|
|
@@ -1436,7 +2017,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1436
2017
|
if (schema.dependentSchemas) {
|
|
1437
2018
|
for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
1438
2019
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
1439
|
-
genCodeE(depSchema, v, pathExpr, lines, ctx)
|
|
2020
|
+
genCodeE(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+key)
|
|
1440
2021
|
lines.push(`}`)
|
|
1441
2022
|
}
|
|
1442
2023
|
}
|
|
@@ -1447,23 +2028,23 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1447
2028
|
const ki = ctx.varCounter++
|
|
1448
2029
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
|
|
1449
2030
|
if (pn.minLength !== undefined) {
|
|
1450
|
-
lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('
|
|
2031
|
+
lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
|
|
1451
2032
|
}
|
|
1452
2033
|
if (pn.maxLength !== undefined) {
|
|
1453
|
-
lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('
|
|
2034
|
+
lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
|
|
1454
2035
|
}
|
|
1455
2036
|
if (pn.pattern) {
|
|
1456
2037
|
const ri = ctx.varCounter++
|
|
1457
2038
|
ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(pn.pattern)})`)
|
|
1458
|
-
lines.push(`if(!_re${ri}.test(_k${ki})){${fail('
|
|
2039
|
+
lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
|
|
1459
2040
|
}
|
|
1460
2041
|
if (pn.const !== undefined) {
|
|
1461
|
-
lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('
|
|
2042
|
+
lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
|
|
1462
2043
|
}
|
|
1463
2044
|
if (pn.enum) {
|
|
1464
2045
|
const ei = ctx.varCounter++
|
|
1465
2046
|
ctx.helperCode.push(`const _es${ei}=new Set(${JSON.stringify(pn.enum)})`)
|
|
1466
|
-
lines.push(`if(!_es${ei}.has(_k${ki})){${fail('
|
|
2047
|
+
lines.push(`if(!_es${ei}.has(_k${ki})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
|
|
1467
2048
|
}
|
|
1468
2049
|
lines.push(`}}`)
|
|
1469
2050
|
}
|
|
@@ -1474,18 +2055,18 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1474
2055
|
const idx = `_j${ctx.varCounter}`
|
|
1475
2056
|
const elem = `_ei${ctx.varCounter}`
|
|
1476
2057
|
ctx.varCounter++
|
|
1477
|
-
const childPath = pathExpr
|
|
2058
|
+
const childPath = childPathDynExpr(pathExpr, idx)
|
|
1478
2059
|
lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
|
|
1479
|
-
genCodeE(schema.items, elem, childPath, lines, ctx)
|
|
2060
|
+
genCodeE(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items')
|
|
1480
2061
|
lines.push(`}}`)
|
|
1481
2062
|
}
|
|
1482
2063
|
|
|
1483
2064
|
// prefixItems
|
|
1484
2065
|
if (schema.prefixItems) {
|
|
1485
2066
|
for (let i = 0; i < schema.prefixItems.length; i++) {
|
|
1486
|
-
const childPath = pathExpr
|
|
2067
|
+
const childPath = childPathExpr(pathExpr, String(i))
|
|
1487
2068
|
lines.push(`if(Array.isArray(${v})&&${v}.length>${i}){`)
|
|
1488
|
-
genCodeE(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx)
|
|
2069
|
+
genCodeE(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx, schemaPrefix+'/prefixItems/'+i)
|
|
1489
2070
|
lines.push(`}`)
|
|
1490
2071
|
}
|
|
1491
2072
|
}
|
|
@@ -1499,17 +2080,17 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1499
2080
|
const minC = schema.minContains !== undefined ? schema.minContains : 1
|
|
1500
2081
|
const maxC = schema.maxContains
|
|
1501
2082
|
lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
|
|
1502
|
-
lines.push(`if(_cc${ci}<${minC}){${fail('
|
|
2083
|
+
lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{limit:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
|
|
1503
2084
|
if (maxC !== undefined) {
|
|
1504
|
-
lines.push(`if(_cc${ci}>${maxC}){${fail('
|
|
2085
|
+
lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{limit:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
|
|
1505
2086
|
}
|
|
1506
2087
|
lines.push(`}`)
|
|
1507
2088
|
}
|
|
1508
2089
|
|
|
1509
2090
|
// allOf
|
|
1510
2091
|
if (schema.allOf) {
|
|
1511
|
-
for (
|
|
1512
|
-
genCodeE(
|
|
2092
|
+
for (let _ai = 0; _ai < schema.allOf.length; _ai++) {
|
|
2093
|
+
genCodeE(schema.allOf[_ai], v, pathExpr, lines, ctx, schemaPrefix+'/allOf/'+_ai)
|
|
1513
2094
|
}
|
|
1514
2095
|
}
|
|
1515
2096
|
|
|
@@ -1521,7 +2102,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1521
2102
|
genCode(sub, '_av', subLines, ctx)
|
|
1522
2103
|
return subLines.length === 0 ? `function(_av){return true}` : `function(_av){${subLines.join(';')};return true}`
|
|
1523
2104
|
})
|
|
1524
|
-
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}){${fail('
|
|
2105
|
+
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}){${fail('anyOf', 'anyOf', '{}', "'must match a schema in anyOf'")}}}`)
|
|
1525
2106
|
}
|
|
1526
2107
|
|
|
1527
2108
|
// oneOf
|
|
@@ -1532,7 +2113,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1532
2113
|
genCode(sub, '_ov', subLines, ctx)
|
|
1533
2114
|
return subLines.length === 0 ? `function(_ov){return true}` : `function(_ov){${subLines.join(';')};return true}`
|
|
1534
2115
|
})
|
|
1535
|
-
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)break}if(_oc${fi}!==1){${fail('
|
|
2116
|
+
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)break}if(_oc${fi}!==1){${fail('oneOf', 'oneOf', '{}', "'must match exactly one schema in oneOf'")}}}`)
|
|
1536
2117
|
}
|
|
1537
2118
|
|
|
1538
2119
|
// not
|
|
@@ -1541,7 +2122,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1541
2122
|
genCode(schema.not, '_nv', subLines, ctx)
|
|
1542
2123
|
const nfn = subLines.length === 0 ? `function(_nv){return true}` : `function(_nv){${subLines.join(';')};return true}`
|
|
1543
2124
|
const fi = ctx.varCounter++
|
|
1544
|
-
lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('
|
|
2125
|
+
lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not', 'not', '{}', "'must NOT be valid'")}}}`)
|
|
1545
2126
|
}
|
|
1546
2127
|
|
|
1547
2128
|
// if/then/else
|
|
@@ -1555,12 +2136,12 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1555
2136
|
lines.push(`{const _if${fi}=${ifFn}`)
|
|
1556
2137
|
if (schema.then) {
|
|
1557
2138
|
lines.push(`if(_if${fi}(${v})){`)
|
|
1558
|
-
genCodeE(schema.then, v, pathExpr, lines, ctx)
|
|
2139
|
+
genCodeE(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then')
|
|
1559
2140
|
lines.push(`}`)
|
|
1560
2141
|
}
|
|
1561
2142
|
if (schema.else) {
|
|
1562
2143
|
lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`)
|
|
1563
|
-
genCodeE(schema.else, v, pathExpr, lines, ctx)
|
|
2144
|
+
genCodeE(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else')
|
|
1564
2145
|
lines.push(`}`)
|
|
1565
2146
|
}
|
|
1566
2147
|
lines.push(`}`)
|
|
@@ -1572,10 +2153,15 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1572
2153
|
// Avoids double-pass (jsFn → false → errFn runs same checks again).
|
|
1573
2154
|
// Uses type-aware optimizations: after type check passes, skip guards.
|
|
1574
2155
|
function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
|
|
2156
|
+
// Bail on unevaluated keywords — combined codegen doesn't support them yet
|
|
2157
|
+
if (typeof schema === 'object' && schema !== null) {
|
|
2158
|
+
const s = JSON.stringify(schema)
|
|
2159
|
+
if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
|
|
2160
|
+
}
|
|
1575
2161
|
if (typeof schema === 'boolean') {
|
|
1576
2162
|
return schema
|
|
1577
2163
|
? () => VALID_RESULT
|
|
1578
|
-
: () => ({ valid: false, errors: [{
|
|
2164
|
+
: () => ({ valid: false, errors: [{ keyword: 'false schema', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
|
|
1579
2165
|
}
|
|
1580
2166
|
if (typeof schema !== 'object' || schema === null) return null
|
|
1581
2167
|
if (!codegenSafe(schema, schemaMap)) return null
|
|
@@ -1603,7 +2189,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
|
|
|
1603
2189
|
const ctx = { varCounter: 0, helperCode: [], closureVars: [], closureVals: [],
|
|
1604
2190
|
rootDefs: schema.$defs || schema.definitions || null, refStack: new Set(), schemaMap: schemaMap || null }
|
|
1605
2191
|
const lines = []
|
|
1606
|
-
genCodeC(schema, 'd', '', lines, ctx)
|
|
2192
|
+
genCodeC(schema, 'd', '', lines, ctx, '#')
|
|
1607
2193
|
if (lines.length === 0) return () => VALID_RESULT
|
|
1608
2194
|
|
|
1609
2195
|
// Use factory pattern: closure vars (regexes, etc.) created once, not per call
|
|
@@ -1615,6 +2201,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
|
|
|
1615
2201
|
`\n return _e?{valid:false,errors:_e}:R`
|
|
1616
2202
|
|
|
1617
2203
|
try {
|
|
2204
|
+
if (process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
|
|
1618
2205
|
const factory = new Function('R' + (closureParams ? ',' + closureParams : ''),
|
|
1619
2206
|
`return function(d){${inner}}`)
|
|
1620
2207
|
return factory(VALID_RESULT, ...ctx.closureVals)
|
|
@@ -1627,7 +2214,8 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
|
|
|
1627
2214
|
// Combined code generator: type-aware like genCode, error-collecting like genCodeE.
|
|
1628
2215
|
// After type check passes → use optimizations (destructuring, no guards).
|
|
1629
2216
|
// If type check fails → push error, skip property checks (they'd crash).
|
|
1630
|
-
function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
2217
|
+
function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
2218
|
+
if (!schemaPrefix) schemaPrefix = '#'
|
|
1631
2219
|
if (typeof schema !== 'object' || schema === null) return
|
|
1632
2220
|
|
|
1633
2221
|
// $ref — resolve local and cross-schema refs
|
|
@@ -1636,14 +2224,14 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
1636
2224
|
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
1637
2225
|
if (ctx.refStack.has(schema.$ref)) return
|
|
1638
2226
|
ctx.refStack.add(schema.$ref)
|
|
1639
|
-
genCodeC(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx)
|
|
2227
|
+
genCodeC(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx, schemaPrefix)
|
|
1640
2228
|
ctx.refStack.delete(schema.$ref)
|
|
1641
2229
|
return
|
|
1642
2230
|
}
|
|
1643
2231
|
if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
|
|
1644
2232
|
if (ctx.refStack.has(schema.$ref)) return
|
|
1645
2233
|
ctx.refStack.add(schema.$ref)
|
|
1646
|
-
genCodeC(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx)
|
|
2234
|
+
genCodeC(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx, schemaPrefix)
|
|
1647
2235
|
ctx.refStack.delete(schema.$ref)
|
|
1648
2236
|
return
|
|
1649
2237
|
}
|
|
@@ -1655,19 +2243,25 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
1655
2243
|
// Pre-allocate error objects as closure variables for static paths.
|
|
1656
2244
|
// This shrinks the generated function body → better V8 JIT on valid path.
|
|
1657
2245
|
const isStaticPath = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
|
|
1658
|
-
const fail = (
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
2246
|
+
const fail = (keyword, schemaSuffix, paramsCode, msgCode) => {
|
|
2247
|
+
const sp = schemaPrefix + '/' + schemaSuffix
|
|
2248
|
+
if (isStaticPath && msgCode.startsWith("'") && !msgCode.includes('+')) {
|
|
2249
|
+
// Try to evaluate paramsCode as a static constant
|
|
2250
|
+
let paramsVal
|
|
2251
|
+
try { paramsVal = Function('return ' + paramsCode)() } catch { /* dynamic params — fall through */ }
|
|
2252
|
+
if (paramsVal !== undefined) {
|
|
2253
|
+
// Static error: pre-allocate as frozen closure variable
|
|
2254
|
+
const ei = ctx.varCounter++
|
|
2255
|
+
const errVar = `_E${ei}`
|
|
2256
|
+
const pathVal = pathExpr ? pathExpr.slice(1, -1) : ''
|
|
2257
|
+
const msgVal = msgCode.slice(1, -1)
|
|
2258
|
+
ctx.closureVars.push(errVar)
|
|
2259
|
+
ctx.closureVals.push(Object.freeze({keyword, instancePath: pathVal, schemaPath: sp, params: Object.freeze(paramsVal), message: msgVal}))
|
|
2260
|
+
return `(_e||(_e=[])).push(${errVar})`
|
|
2261
|
+
}
|
|
1668
2262
|
}
|
|
1669
2263
|
// Dynamic path (e.g., array index): inline as before
|
|
1670
|
-
return `(_e||(_e=[])).push({
|
|
2264
|
+
return `(_e||(_e=[])).push({keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}})`
|
|
1671
2265
|
}
|
|
1672
2266
|
|
|
1673
2267
|
if (types) {
|
|
@@ -1687,7 +2281,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
1687
2281
|
// Type check: push error but continue — wrap remaining in type-success block
|
|
1688
2282
|
const typeOk = `_tok${ctx.varCounter++}`
|
|
1689
2283
|
lines.push(`const ${typeOk}=${conds.join('||')}`)
|
|
1690
|
-
lines.push(`if(!${typeOk}){${fail('
|
|
2284
|
+
lines.push(`if(!${typeOk}){${fail('type', 'type', `{type:'${expected}'}`, `'must be ${expected}'`)}}`)
|
|
1691
2285
|
// Subsequent optimized code runs inside if(typeOk){...}
|
|
1692
2286
|
if (types.length === 1) {
|
|
1693
2287
|
isObj = types[0] === 'object'
|
|
@@ -1706,19 +2300,19 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
1706
2300
|
const primChecks = primitives.map(p => `${v}===${JSON.stringify(p)}`).join('||')
|
|
1707
2301
|
const objChecks = objects.map(o => `JSON.stringify(${v})===${JSON.stringify(JSON.stringify(o))}`).join('||')
|
|
1708
2302
|
const allChecks = [primChecks, objChecks].filter(Boolean).join('||')
|
|
1709
|
-
lines.push(`if(!(${allChecks || 'false'})){${fail('
|
|
2303
|
+
lines.push(`if(!(${allChecks || 'false'})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
|
|
1710
2304
|
}
|
|
1711
2305
|
|
|
1712
2306
|
// const
|
|
1713
2307
|
if (schema.const !== undefined) {
|
|
1714
2308
|
const cv = schema.const
|
|
1715
2309
|
if (cv === null || typeof cv !== 'object') {
|
|
1716
|
-
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('
|
|
2310
|
+
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
|
|
1717
2311
|
} else {
|
|
1718
2312
|
const ci = ctx.varCounter++
|
|
1719
2313
|
const canonFn = `_cn${ci}`
|
|
1720
2314
|
ctx.helperCode.push(`const ${canonFn}=function(x){if(x===null||typeof x!=='object')return JSON.stringify(x);if(Array.isArray(x))return'['+x.map(${canonFn}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+${canonFn}(x[k])}).join(',')+'}'};`)
|
|
1721
|
-
lines.push(`if(${canonFn}(${v})!==${canonFn}(${JSON.stringify(cv)})){${fail('
|
|
2315
|
+
lines.push(`if(${canonFn}(${v})!==${canonFn}(${JSON.stringify(cv)})){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
|
|
1722
2316
|
}
|
|
1723
2317
|
}
|
|
1724
2318
|
|
|
@@ -1737,80 +2331,127 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
1737
2331
|
if (destructKeys.length > 0) lines.push(`const{${destructKeys.join(',')}}=${v}`)
|
|
1738
2332
|
for (const key of schema.required) {
|
|
1739
2333
|
const check = hoisted[key] ? `${hoisted[key]}===undefined` : `${v}[${JSON.stringify(key)}]===undefined`
|
|
1740
|
-
|
|
1741
|
-
|
|
2334
|
+
// Pre-allocate required errors as frozen closure variables
|
|
2335
|
+
const ei = ctx.varCounter++
|
|
2336
|
+
const errVar = `_E${ei}`
|
|
2337
|
+
const pathVal = pathExpr ? pathExpr.slice(1, -1) : ''
|
|
2338
|
+
ctx.closureVars.push(errVar)
|
|
2339
|
+
ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: `${schemaPrefix}/required`, params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}))
|
|
2340
|
+
lines.push(`if(${check}){(_e||(_e=[])).push(${errVar})}`)
|
|
1742
2341
|
}
|
|
1743
2342
|
} else if (schema.required) {
|
|
1744
2343
|
for (const key of schema.required) {
|
|
1745
|
-
const
|
|
1746
|
-
|
|
2344
|
+
const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
|
|
2345
|
+
if (isStatic) {
|
|
2346
|
+
const ei = ctx.varCounter++
|
|
2347
|
+
const errVar = `_E${ei}`
|
|
2348
|
+
const pathVal = pathExpr ? pathExpr.slice(1, -1) : ''
|
|
2349
|
+
ctx.closureVars.push(errVar)
|
|
2350
|
+
ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: `${schemaPrefix}/required`, params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}))
|
|
2351
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){(_e||(_e=[])).push(${errVar})}`)
|
|
2352
|
+
} else {
|
|
2353
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required',params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"})}`)
|
|
2354
|
+
}
|
|
1747
2355
|
}
|
|
1748
2356
|
}
|
|
1749
2357
|
|
|
1750
2358
|
// numeric — skip type guard if known
|
|
1751
|
-
if (schema.minimum !== undefined) { const c = isNum ? `${v}<${schema.minimum}` : `typeof ${v}==='number'&&${v}<${schema.minimum}`; lines.push(`if(${c}){${fail('
|
|
1752
|
-
if (schema.maximum !== undefined) { const c = isNum ? `${v}>${schema.maximum}` : `typeof ${v}==='number'&&${v}>${schema.maximum}`; lines.push(`if(${c}){${fail('
|
|
1753
|
-
if (schema.exclusiveMinimum !== undefined) { const c = isNum ? `${v}<=${schema.exclusiveMinimum}` : `typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum}`; lines.push(`if(${c}){${fail('
|
|
1754
|
-
if (schema.exclusiveMaximum !== undefined) { const c = isNum ? `${v}>=${schema.exclusiveMaximum}` : `typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum}`; lines.push(`if(${c}){${fail('
|
|
2359
|
+
if (schema.minimum !== undefined) { const c = isNum ? `${v}<${schema.minimum}` : `typeof ${v}==='number'&&${v}<${schema.minimum}`; lines.push(`if(${c}){${fail('minimum', 'minimum', `{comparison:'>=',limit:${schema.minimum}}`, `'must be >= ${schema.minimum}'`)}}`) }
|
|
2360
|
+
if (schema.maximum !== undefined) { const c = isNum ? `${v}>${schema.maximum}` : `typeof ${v}==='number'&&${v}>${schema.maximum}`; lines.push(`if(${c}){${fail('maximum', 'maximum', `{comparison:'<=',limit:${schema.maximum}}`, `'must be <= ${schema.maximum}'`)}}`) }
|
|
2361
|
+
if (schema.exclusiveMinimum !== undefined) { const c = isNum ? `${v}<=${schema.exclusiveMinimum}` : `typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum}`; lines.push(`if(${c}){${fail('exclusiveMinimum', 'exclusiveMinimum', `{comparison:'>',limit:${schema.exclusiveMinimum}}`, `'must be > ${schema.exclusiveMinimum}'`)}}`) }
|
|
2362
|
+
if (schema.exclusiveMaximum !== undefined) { const c = isNum ? `${v}>=${schema.exclusiveMaximum}` : `typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum}`; lines.push(`if(${c}){${fail('exclusiveMaximum', 'exclusiveMaximum', `{comparison:'<',limit:${schema.exclusiveMaximum}}`, `'must be < ${schema.exclusiveMaximum}'`)}}`) }
|
|
1755
2363
|
if (schema.multipleOf !== undefined) {
|
|
1756
2364
|
const m = schema.multipleOf
|
|
1757
2365
|
const ci = ctx.varCounter++
|
|
1758
|
-
lines.push(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('
|
|
2366
|
+
lines.push(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('multipleOf', 'multipleOf', `{multipleOf:${m}}`, `'must be multiple of ${m}'`)}}}`)
|
|
1759
2367
|
}
|
|
1760
2368
|
|
|
1761
2369
|
// string — skip guard if known
|
|
1762
|
-
if (schema.minLength !== undefined) { const c = isStr ? `${v}.length<${schema.minLength}` : `typeof ${v}==='string'&&${v}.length<${schema.minLength}`; lines.push(`if(${c}){${fail('
|
|
1763
|
-
if (schema.maxLength !== undefined) { const c = isStr ? `${v}.length>${schema.maxLength}` : `typeof ${v}==='string'&&${v}.length>${schema.maxLength}`; lines.push(`if(${c}){${fail('
|
|
2370
|
+
if (schema.minLength !== undefined) { const c = isStr ? `${v}.length<${schema.minLength}` : `typeof ${v}==='string'&&${v}.length<${schema.minLength}`; lines.push(`if(${c}){${fail('minLength', 'minLength', `{limit:${schema.minLength}}`, `'must NOT have fewer than ${schema.minLength} characters'`)}}`) }
|
|
2371
|
+
if (schema.maxLength !== undefined) { const c = isStr ? `${v}.length>${schema.maxLength}` : `typeof ${v}==='string'&&${v}.length>${schema.maxLength}`; lines.push(`if(${c}){${fail('maxLength', 'maxLength', `{limit:${schema.maxLength}}`, `'must NOT have more than ${schema.maxLength} characters'`)}}`) }
|
|
1764
2372
|
if (schema.pattern) {
|
|
1765
|
-
const
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
2373
|
+
const inlineCheck = compilePatternInline(schema.pattern, v)
|
|
2374
|
+
if (inlineCheck) {
|
|
2375
|
+
const c = isStr ? `!(${inlineCheck})` : `typeof ${v}==='string'&&!(${inlineCheck})`
|
|
2376
|
+
lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, `'must match pattern "${schema.pattern}"'`)}}`)
|
|
2377
|
+
} else {
|
|
2378
|
+
const ri = ctx.varCounter++
|
|
2379
|
+
const reVar = `_re${ri}`
|
|
2380
|
+
ctx.closureVars.push(reVar)
|
|
2381
|
+
ctx.closureVals.push(new RegExp(schema.pattern))
|
|
2382
|
+
const c = isStr ? `!${reVar}.test(${v})` : `typeof ${v}==='string'&&!${reVar}.test(${v})`
|
|
2383
|
+
lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, `'must match pattern "${schema.pattern}"'`)}}`)
|
|
2384
|
+
}
|
|
1771
2385
|
}
|
|
1772
2386
|
if (schema.format) {
|
|
1773
2387
|
const fc = FORMAT_CODEGEN[schema.format]
|
|
1774
2388
|
if (fc) {
|
|
1775
|
-
const code = fc(v, isStr).replace(/return false/g, `{${fail('
|
|
2389
|
+
const code = fc(v, isStr).replace(/return false/g, `{${fail('format', 'format', `{format:'${esc(schema.format)}'}`, `'must match format "${esc(schema.format)}"'`)}}`)
|
|
1776
2390
|
lines.push(code)
|
|
1777
2391
|
}
|
|
1778
2392
|
}
|
|
1779
2393
|
|
|
1780
2394
|
// array size
|
|
1781
|
-
if (schema.minItems !== undefined) { const c = isArr ? `${v}.length<${schema.minItems}` : `Array.isArray(${v})&&${v}.length<${schema.minItems}`; lines.push(`if(${c}){${fail('
|
|
1782
|
-
if (schema.maxItems !== undefined) { const c = isArr ? `${v}.length>${schema.maxItems}` : `Array.isArray(${v})&&${v}.length>${schema.maxItems}`; lines.push(`if(${c}){${fail('
|
|
2395
|
+
if (schema.minItems !== undefined) { const c = isArr ? `${v}.length<${schema.minItems}` : `Array.isArray(${v})&&${v}.length<${schema.minItems}`; lines.push(`if(${c}){${fail('minItems', 'minItems', `{limit:${schema.minItems}}`, `'must NOT have fewer than ${schema.minItems} items'`)}}`) }
|
|
2396
|
+
if (schema.maxItems !== undefined) { const c = isArr ? `${v}.length>${schema.maxItems}` : `Array.isArray(${v})&&${v}.length>${schema.maxItems}`; lines.push(`if(${c}){${fail('maxItems', 'maxItems', `{limit:${schema.maxItems}}`, `'must NOT have more than ${schema.maxItems} items'`)}}`) }
|
|
1783
2397
|
|
|
1784
|
-
// uniqueItems
|
|
2398
|
+
// uniqueItems — tiered: small primitive arrays use nested loop (no allocation)
|
|
1785
2399
|
if (schema.uniqueItems) {
|
|
1786
2400
|
const si = ctx.varCounter++
|
|
1787
2401
|
const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
|
|
1788
2402
|
const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
|
|
1789
|
-
const
|
|
1790
|
-
|
|
1791
|
-
|
|
2403
|
+
const maxItems = schema.maxItems
|
|
2404
|
+
const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
|
|
2405
|
+
let inner
|
|
2406
|
+
if (isPrim && maxItems && maxItems <= 16) {
|
|
2407
|
+
// Small primitive arrays: O(n²) nested loop, zero allocation
|
|
2408
|
+
inner = `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k]){${failExpr('_k', '_i')};break}}}`
|
|
2409
|
+
} else if (isPrim) {
|
|
2410
|
+
inner = `const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _prev=_s${si}.get(${v}[_i]);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(${v}[_i],_i)}`
|
|
2411
|
+
} else {
|
|
2412
|
+
inner = `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 Map();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);const _prev=_s${si}.get(_k);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(_k,_i)}`
|
|
2413
|
+
}
|
|
1792
2414
|
lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
|
|
1793
2415
|
}
|
|
1794
2416
|
|
|
1795
2417
|
// object size
|
|
1796
|
-
if (schema.minProperties !== undefined) lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length<${schema.minProperties}){${fail('
|
|
1797
|
-
if (schema.maxProperties !== undefined) lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length>${schema.maxProperties}){${fail('
|
|
2418
|
+
if (schema.minProperties !== undefined) lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length<${schema.minProperties}){${fail('minProperties', 'minProperties', `{limit:${schema.minProperties}}`, `'must NOT have fewer than ${schema.minProperties} properties'`)}}`)
|
|
2419
|
+
if (schema.maxProperties !== undefined) lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length>${schema.maxProperties}){${fail('maxProperties', 'maxProperties', `{limit:${schema.maxProperties}}`, `'must NOT have more than ${schema.maxProperties} properties'`)}}`)
|
|
1798
2420
|
|
|
1799
2421
|
// additionalProperties — skip if patternProperties present (handled in unified loop below)
|
|
2422
|
+
// Small property sets: direct === chain (no Set allocation)
|
|
1800
2423
|
if (schema.additionalProperties === false && schema.properties && !schema.patternProperties) {
|
|
1801
|
-
const
|
|
2424
|
+
const propKeys = Object.keys(schema.properties)
|
|
1802
2425
|
const ci = ctx.varCounter++
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2426
|
+
if (propKeys.length <= 8) {
|
|
2427
|
+
// Direct chain: no Set allocation for small schemas
|
|
2428
|
+
const checks = propKeys.map(k => `_k${ci}[_i]!==${JSON.stringify(k)}`).join('&&')
|
|
2429
|
+
lines.push(isObj
|
|
2430
|
+
? `{const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++)if(${checks}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`
|
|
2431
|
+
: `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++)if(${checks}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`)
|
|
2432
|
+
} else {
|
|
2433
|
+
const allowed = propKeys.map(k => JSON.stringify(k)).join(',')
|
|
2434
|
+
lines.push(isObj
|
|
2435
|
+
? `{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])){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`
|
|
2436
|
+
: `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){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])){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`)
|
|
2437
|
+
}
|
|
1806
2438
|
}
|
|
1807
2439
|
|
|
1808
2440
|
// dependentRequired
|
|
1809
2441
|
if (schema.dependentRequired) {
|
|
1810
2442
|
for (const [key, deps] of Object.entries(schema.dependentRequired)) {
|
|
1811
2443
|
for (const dep of deps) {
|
|
1812
|
-
const
|
|
1813
|
-
|
|
2444
|
+
const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
|
|
2445
|
+
if (isStatic) {
|
|
2446
|
+
const ei = ctx.varCounter++
|
|
2447
|
+
const errVar = `_E${ei}`
|
|
2448
|
+
const pathVal = pathExpr ? pathExpr.slice(1, -1) : ''
|
|
2449
|
+
ctx.closureVars.push(errVar)
|
|
2450
|
+
ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: `${schemaPrefix}/dependentRequired`, params: Object.freeze({missingProperty: dep}), message: `must have required property '${dep}'`}))
|
|
2451
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){(_e||(_e=[])).push(${errVar})}`)
|
|
2452
|
+
} else {
|
|
2453
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired',params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"})}`)
|
|
2454
|
+
}
|
|
1814
2455
|
}
|
|
1815
2456
|
}
|
|
1816
2457
|
}
|
|
@@ -1819,19 +2460,19 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
1819
2460
|
if (schema.properties) {
|
|
1820
2461
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
1821
2462
|
const pv = hoisted[key] || `${v}[${JSON.stringify(key)}]`
|
|
1822
|
-
const childPath = pathExpr
|
|
2463
|
+
const childPath = childPathExpr(pathExpr, esc(key))
|
|
1823
2464
|
if (requiredSet.has(key) && isObj) {
|
|
1824
2465
|
lines.push(`if(${pv}!==undefined){`)
|
|
1825
|
-
genCodeC(prop, pv, childPath, lines, ctx)
|
|
2466
|
+
genCodeC(prop, pv, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
|
|
1826
2467
|
lines.push(`}`)
|
|
1827
2468
|
} else if (isObj) {
|
|
1828
2469
|
const oi = ctx.varCounter++
|
|
1829
2470
|
lines.push(`{const _o${oi}=${v}[${JSON.stringify(key)}];if(_o${oi}!==undefined){`)
|
|
1830
|
-
genCodeC(prop, `_o${oi}`, childPath, lines, ctx)
|
|
2471
|
+
genCodeC(prop, `_o${oi}`, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
|
|
1831
2472
|
lines.push(`}}`)
|
|
1832
2473
|
} else {
|
|
1833
2474
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
1834
|
-
genCodeC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx)
|
|
2475
|
+
genCodeC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
|
|
1835
2476
|
lines.push(`}`)
|
|
1836
2477
|
}
|
|
1837
2478
|
}
|
|
@@ -1888,61 +2529,61 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
1888
2529
|
lines.push(`${guard}{for(const ${kVar} in ${v}){`)
|
|
1889
2530
|
// propertyNames checks (merged)
|
|
1890
2531
|
if (pn) {
|
|
1891
|
-
if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('
|
|
1892
|
-
if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('
|
|
2532
|
+
if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
|
|
2533
|
+
if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
|
|
1893
2534
|
if (pn.pattern) {
|
|
1894
2535
|
const fast = fastPrefixCheck(pn.pattern, kVar)
|
|
1895
2536
|
if (fast) {
|
|
1896
|
-
lines.push(`if(!(${fast})){${fail('
|
|
2537
|
+
lines.push(`if(!(${fast})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
|
|
1897
2538
|
} else {
|
|
1898
2539
|
const ri = ctx.varCounter++
|
|
1899
2540
|
ctx.closureVars.push(`_re${ri}`)
|
|
1900
2541
|
ctx.closureVals.push(new RegExp(pn.pattern))
|
|
1901
|
-
lines.push(`if(!_re${ri}.test(${kVar})){${fail('
|
|
2542
|
+
lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
|
|
1902
2543
|
}
|
|
1903
2544
|
}
|
|
1904
|
-
if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('
|
|
2545
|
+
if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
|
|
1905
2546
|
if (pn.enum) {
|
|
1906
2547
|
const ei = ctx.varCounter++
|
|
1907
2548
|
ctx.closureVars.push(`_es${ei}`)
|
|
1908
2549
|
ctx.closureVals.push(new Set(pn.enum))
|
|
1909
|
-
lines.push(`if(!_es${ei}.has(${kVar})){${fail('
|
|
2550
|
+
lines.push(`if(!_es${ei}.has(${kVar})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
|
|
1910
2551
|
}
|
|
1911
2552
|
}
|
|
1912
2553
|
const matchExpr = keyCheck || `_as${pi}.has(${kVar})`
|
|
1913
2554
|
lines.push(`let _m${pi}=${matchExpr}`)
|
|
1914
2555
|
for (let i = 0; i < ppEntries.length; i++) {
|
|
1915
|
-
lines.push(`if(${matchers[i].check}){_m${pi}=true;if(!_ppf${pi}_${i}(${v}[${kVar}])){${fail('
|
|
2556
|
+
lines.push(`if(${matchers[i].check}){_m${pi}=true;if(!_ppf${pi}_${i}(${v}[${kVar}])){${fail('pattern', 'patternProperties', `{pattern:'${ppEntries[i][0]}'}`, `'patternProperties: value invalid for key '+${kVar}`)}}}`)
|
|
1916
2557
|
}
|
|
1917
|
-
lines.push(`if(!_m${pi}){${fail('
|
|
2558
|
+
lines.push(`if(!_m${pi}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:${kVar}}`, "'must NOT have additional properties'")}}`)
|
|
1918
2559
|
lines.push(`}}`)
|
|
1919
2560
|
} else {
|
|
1920
2561
|
ctx._ppHandledPropertyNamesC = !!pn
|
|
1921
2562
|
lines.push(`${guard}{for(const ${kVar} in ${v}){`)
|
|
1922
2563
|
if (pn) {
|
|
1923
|
-
if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('
|
|
1924
|
-
if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('
|
|
2564
|
+
if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
|
|
2565
|
+
if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
|
|
1925
2566
|
if (pn.pattern) {
|
|
1926
2567
|
const fast = fastPrefixCheck(pn.pattern, kVar)
|
|
1927
2568
|
if (fast) {
|
|
1928
|
-
lines.push(`if(!(${fast})){${fail('
|
|
2569
|
+
lines.push(`if(!(${fast})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
|
|
1929
2570
|
} else {
|
|
1930
2571
|
const ri = ctx.varCounter++
|
|
1931
2572
|
ctx.closureVars.push(`_re${ri}`)
|
|
1932
2573
|
ctx.closureVals.push(new RegExp(pn.pattern))
|
|
1933
|
-
lines.push(`if(!_re${ri}.test(${kVar})){${fail('
|
|
2574
|
+
lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
|
|
1934
2575
|
}
|
|
1935
2576
|
}
|
|
1936
|
-
if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('
|
|
2577
|
+
if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
|
|
1937
2578
|
if (pn.enum) {
|
|
1938
2579
|
const ei = ctx.varCounter++
|
|
1939
2580
|
ctx.closureVars.push(`_es${ei}`)
|
|
1940
2581
|
ctx.closureVals.push(new Set(pn.enum))
|
|
1941
|
-
lines.push(`if(!_es${ei}.has(${kVar})){${fail('
|
|
2582
|
+
lines.push(`if(!_es${ei}.has(${kVar})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
|
|
1942
2583
|
}
|
|
1943
2584
|
}
|
|
1944
2585
|
for (let i = 0; i < ppEntries.length; i++) {
|
|
1945
|
-
lines.push(`if(${matchers[i].check}&&!_ppf${pi}_${i}(${v}[${kVar}])){${fail('
|
|
2586
|
+
lines.push(`if(${matchers[i].check}&&!_ppf${pi}_${i}(${v}[${kVar}])){${fail('pattern', 'patternProperties', `{pattern:'${ppEntries[i][0]}'}`, `'patternProperties: value invalid for key '+${kVar}`)}}`)
|
|
1946
2587
|
}
|
|
1947
2588
|
lines.push(`}}`)
|
|
1948
2589
|
}
|
|
@@ -1952,7 +2593,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
1952
2593
|
if (schema.dependentSchemas) {
|
|
1953
2594
|
for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
1954
2595
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
1955
|
-
genCodeC(depSchema, v, pathExpr, lines, ctx)
|
|
2596
|
+
genCodeC(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+key)
|
|
1956
2597
|
lines.push(`}`)
|
|
1957
2598
|
}
|
|
1958
2599
|
}
|
|
@@ -1963,25 +2604,25 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
1963
2604
|
const ki = ctx.varCounter++
|
|
1964
2605
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
|
|
1965
2606
|
if (pn.minLength !== undefined) {
|
|
1966
|
-
lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('
|
|
2607
|
+
lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
|
|
1967
2608
|
}
|
|
1968
2609
|
if (pn.maxLength !== undefined) {
|
|
1969
|
-
lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('
|
|
2610
|
+
lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
|
|
1970
2611
|
}
|
|
1971
2612
|
if (pn.pattern) {
|
|
1972
2613
|
const ri = ctx.varCounter++
|
|
1973
2614
|
ctx.closureVars.push(`_re${ri}`)
|
|
1974
2615
|
ctx.closureVals.push(new RegExp(pn.pattern))
|
|
1975
|
-
lines.push(`if(!_re${ri}.test(_k${ki})){${fail('
|
|
2616
|
+
lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
|
|
1976
2617
|
}
|
|
1977
2618
|
if (pn.const !== undefined) {
|
|
1978
|
-
lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('
|
|
2619
|
+
lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
|
|
1979
2620
|
}
|
|
1980
2621
|
if (pn.enum) {
|
|
1981
2622
|
const ei = ctx.varCounter++
|
|
1982
2623
|
ctx.closureVars.push(`_es${ei}`)
|
|
1983
2624
|
ctx.closureVals.push(new Set(pn.enum))
|
|
1984
|
-
lines.push(`if(!_es${ei}.has(_k${ki})){${fail('
|
|
2625
|
+
lines.push(`if(!_es${ei}.has(_k${ki})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
|
|
1985
2626
|
}
|
|
1986
2627
|
lines.push(`}}`)
|
|
1987
2628
|
}
|
|
@@ -1991,18 +2632,18 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
1991
2632
|
const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
|
|
1992
2633
|
const idx = `_j${ctx.varCounter}`, elem = `_ei${ctx.varCounter}`
|
|
1993
2634
|
ctx.varCounter++
|
|
1994
|
-
const childPath = pathExpr
|
|
2635
|
+
const childPath = childPathDynExpr(pathExpr, idx)
|
|
1995
2636
|
lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
|
|
1996
|
-
genCodeC(schema.items, elem, childPath, lines, ctx)
|
|
2637
|
+
genCodeC(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items')
|
|
1997
2638
|
lines.push(`}}`)
|
|
1998
2639
|
}
|
|
1999
2640
|
|
|
2000
2641
|
// prefixItems
|
|
2001
2642
|
if (schema.prefixItems) {
|
|
2002
2643
|
for (let i = 0; i < schema.prefixItems.length; i++) {
|
|
2003
|
-
const childPath = pathExpr
|
|
2644
|
+
const childPath = childPathExpr(pathExpr, String(i))
|
|
2004
2645
|
lines.push(`if(Array.isArray(${v})&&${v}.length>${i}){`)
|
|
2005
|
-
genCodeC(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx)
|
|
2646
|
+
genCodeC(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx, schemaPrefix+'/prefixItems/'+i)
|
|
2006
2647
|
lines.push(`}`)
|
|
2007
2648
|
}
|
|
2008
2649
|
}
|
|
@@ -2016,26 +2657,26 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
2016
2657
|
const minC = schema.minContains !== undefined ? schema.minContains : 1
|
|
2017
2658
|
const maxC = schema.maxContains
|
|
2018
2659
|
lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
|
|
2019
|
-
lines.push(`if(_cc${ci}<${minC}){${fail('
|
|
2020
|
-
if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('
|
|
2660
|
+
lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{limit:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
|
|
2661
|
+
if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{limit:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
|
|
2021
2662
|
lines.push(`}`)
|
|
2022
2663
|
}
|
|
2023
2664
|
|
|
2024
2665
|
// allOf
|
|
2025
|
-
if (schema.allOf) { for (
|
|
2666
|
+
if (schema.allOf) { for (let _ai = 0; _ai < schema.allOf.length; _ai++) genCodeC(schema.allOf[_ai], v, pathExpr, lines, ctx, schemaPrefix+'/allOf/'+_ai) }
|
|
2026
2667
|
|
|
2027
2668
|
// anyOf
|
|
2028
2669
|
if (schema.anyOf) {
|
|
2029
2670
|
const fi = ctx.varCounter++
|
|
2030
2671
|
const fns = schema.anyOf.map(sub => { const sl = []; genCode(sub, '_av', sl, ctx); return sl.length === 0 ? `function(_av){return true}` : `function(_av){${sl.join(';')};return true}` })
|
|
2031
|
-
lines.push(`{const _af${fi}=[${fns.join(',')}];let _am=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am=true;break}}if(!_am){${fail('
|
|
2672
|
+
lines.push(`{const _af${fi}=[${fns.join(',')}];let _am=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am=true;break}}if(!_am){${fail('anyOf', 'anyOf', '{}', "'must match a schema in anyOf'")}}}`)
|
|
2032
2673
|
}
|
|
2033
2674
|
|
|
2034
2675
|
// oneOf
|
|
2035
2676
|
if (schema.oneOf) {
|
|
2036
2677
|
const fi = ctx.varCounter++
|
|
2037
2678
|
const fns = schema.oneOf.map(sub => { const sl = []; genCode(sub, '_ov', sl, ctx); return sl.length === 0 ? `function(_ov){return true}` : `function(_ov){${sl.join(';')};return true}` })
|
|
2038
|
-
lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc++;if(_oc>1)break}if(_oc!==1){${fail('
|
|
2679
|
+
lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc++;if(_oc>1)break}if(_oc!==1){${fail('oneOf', 'oneOf', '{}', "'must match exactly one schema in oneOf'")}}}`)
|
|
2039
2680
|
}
|
|
2040
2681
|
|
|
2041
2682
|
// not
|
|
@@ -2043,7 +2684,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
2043
2684
|
const sl = []; genCode(schema.not, '_nv', sl, ctx)
|
|
2044
2685
|
const nfn = sl.length === 0 ? `function(_nv){return true}` : `function(_nv){${sl.join(';')};return true}`
|
|
2045
2686
|
const fi = ctx.varCounter++
|
|
2046
|
-
lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('
|
|
2687
|
+
lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not', 'not', '{}', "'must NOT be valid'")}}}`)
|
|
2047
2688
|
}
|
|
2048
2689
|
|
|
2049
2690
|
// if/then/else
|
|
@@ -2052,8 +2693,8 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
2052
2693
|
const fi = ctx.varCounter++
|
|
2053
2694
|
const ifFn = sl.length === 0 ? `function(_iv){return true}` : `function(_iv){${sl.join(';')};return true}`
|
|
2054
2695
|
lines.push(`{const _if${fi}=${ifFn}`)
|
|
2055
|
-
if (schema.then) { lines.push(`if(_if${fi}(${v})){`); genCodeC(schema.then, v, pathExpr, lines, ctx); lines.push(`}`) }
|
|
2056
|
-
if (schema.else) { lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`); genCodeC(schema.else, v, pathExpr, lines, ctx); lines.push(`}`) }
|
|
2696
|
+
if (schema.then) { lines.push(`if(_if${fi}(${v})){`); genCodeC(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then'); lines.push(`}`) }
|
|
2697
|
+
if (schema.else) { lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`); genCodeC(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else'); lines.push(`}`) }
|
|
2057
2698
|
lines.push(`}`)
|
|
2058
2699
|
}
|
|
2059
2700
|
|
|
@@ -2063,4 +2704,124 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
2063
2704
|
}
|
|
2064
2705
|
}
|
|
2065
2706
|
|
|
2066
|
-
|
|
2707
|
+
// Collect statically-known evaluated properties/items from a schema.
|
|
2708
|
+
// Returns { props: string[], items: number|null, allProps: bool, allItems: bool, dynamic: bool }
|
|
2709
|
+
function collectEvaluated(schema, schemaMap, rootDefs) {
|
|
2710
|
+
if (typeof schema !== 'object' || schema === null) return { props: [], items: null, allProps: false, allItems: false, dynamic: false }
|
|
2711
|
+
const defs = rootDefs || schema.$defs || schema.definitions || null
|
|
2712
|
+
const result = { props: [], items: null, allProps: false, allItems: false, dynamic: false }
|
|
2713
|
+
_collectEval(schema, result, defs, schemaMap, new Set(), true)
|
|
2714
|
+
return result
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
|
|
2718
|
+
if (typeof schema !== 'object' || schema === null) return
|
|
2719
|
+
if (result.allProps && result.allItems) return
|
|
2720
|
+
|
|
2721
|
+
// $ref — inline
|
|
2722
|
+
if (schema.$ref) {
|
|
2723
|
+
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
2724
|
+
if (m && defs && defs[m[1]]) {
|
|
2725
|
+
if (refStack.has(schema.$ref)) { result.dynamic = true; return }
|
|
2726
|
+
refStack.add(schema.$ref)
|
|
2727
|
+
_collectEval(defs[m[1]], result, defs, schemaMap, refStack)
|
|
2728
|
+
refStack.delete(schema.$ref)
|
|
2729
|
+
} else if (schemaMap && typeof schemaMap.get === 'function' && schemaMap.has(schema.$ref)) {
|
|
2730
|
+
if (refStack.has(schema.$ref)) { result.dynamic = true; return }
|
|
2731
|
+
refStack.add(schema.$ref)
|
|
2732
|
+
_collectEval(schemaMap.get(schema.$ref), result, defs, schemaMap, refStack)
|
|
2733
|
+
refStack.delete(schema.$ref)
|
|
2734
|
+
}
|
|
2735
|
+
// In 2020-12, $ref can coexist with siblings — don't return early if there are other keywords
|
|
2736
|
+
const hasOtherKeywords = Object.keys(schema).some(k => k !== '$ref' && k !== '$defs' && k !== 'definitions' && k !== '$schema' && k !== '$id')
|
|
2737
|
+
if (!hasOtherKeywords) return
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
// properties → static keys
|
|
2741
|
+
if (schema.properties) {
|
|
2742
|
+
for (const k of Object.keys(schema.properties)) {
|
|
2743
|
+
if (!result.props.includes(k)) result.props.push(k)
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
// additionalProperties: true/schema → all props evaluated
|
|
2748
|
+
if (schema.additionalProperties !== undefined && schema.additionalProperties !== false) {
|
|
2749
|
+
result.allProps = true
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
// patternProperties → dynamic
|
|
2753
|
+
if (schema.patternProperties) {
|
|
2754
|
+
result.dynamic = true
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
// prefixItems → max index
|
|
2758
|
+
if (schema.prefixItems) {
|
|
2759
|
+
const count = schema.prefixItems.length
|
|
2760
|
+
result.items = result.items === null ? count : Math.max(result.items, count)
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
// items: schema/true → all items evaluated
|
|
2764
|
+
if (schema.items && typeof schema.items === 'object') {
|
|
2765
|
+
result.allItems = true
|
|
2766
|
+
}
|
|
2767
|
+
if (schema.items === true) {
|
|
2768
|
+
result.allItems = true
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
// contains interaction with unevaluatedItems is complex
|
|
2772
|
+
// At root level: contains + unevaluatedItems needs dynamic tracking
|
|
2773
|
+
// In nested schemas: contains marks all items as evaluated
|
|
2774
|
+
if (schema.contains) {
|
|
2775
|
+
if (isRoot && (schema.unevaluatedItems !== undefined)) {
|
|
2776
|
+
result.dynamic = true
|
|
2777
|
+
} else {
|
|
2778
|
+
result.allItems = true
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// unevaluatedProperties: true/schema → all props evaluated (for nested schemas only)
|
|
2783
|
+
// At root level, unevaluatedProperties is what we're computing FOR, not a contributor
|
|
2784
|
+
if (!isRoot && (schema.unevaluatedProperties === true || (typeof schema.unevaluatedProperties === 'object' && schema.unevaluatedProperties !== null))) {
|
|
2785
|
+
result.allProps = true
|
|
2786
|
+
}
|
|
2787
|
+
// unevaluatedItems: true/schema → all items evaluated (for nested schemas only)
|
|
2788
|
+
if (!isRoot && (schema.unevaluatedItems === true || (typeof schema.unevaluatedItems === 'object' && schema.unevaluatedItems !== null))) {
|
|
2789
|
+
result.allItems = true
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
// allOf → merge all (unconditional)
|
|
2793
|
+
if (schema.allOf) {
|
|
2794
|
+
for (const sub of schema.allOf) {
|
|
2795
|
+
_collectEval(sub, result, defs, schemaMap, refStack)
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
// anyOf / oneOf → dynamic (conditional merge)
|
|
2800
|
+
if (schema.anyOf || schema.oneOf) {
|
|
2801
|
+
result.dynamic = true
|
|
2802
|
+
const branches = schema.anyOf || schema.oneOf
|
|
2803
|
+
for (const sub of branches) {
|
|
2804
|
+
_collectEval(sub, result, defs, schemaMap, refStack)
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
// if/then/else → dynamic (branch-dependent)
|
|
2809
|
+
if (schema.if && (schema.then || schema.else)) {
|
|
2810
|
+
result.dynamic = true
|
|
2811
|
+
_collectEval(schema.if, result, defs, schemaMap, refStack)
|
|
2812
|
+
if (schema.then) _collectEval(schema.then, result, defs, schemaMap, refStack)
|
|
2813
|
+
if (schema.else) _collectEval(schema.else, result, defs, schemaMap, refStack)
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
// dependentSchemas → dynamic
|
|
2817
|
+
if (schema.dependentSchemas) {
|
|
2818
|
+
result.dynamic = true
|
|
2819
|
+
for (const sub of Object.values(schema.dependentSchemas)) {
|
|
2820
|
+
_collectEval(sub, result, defs, schemaMap, refStack)
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
// not → contributes nothing (spec: annotations from not are discarded)
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, AJV_MESSAGES }
|