ata-validator 1.9.0 → 1.11.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/CHANGELOG.md +35 -0
- package/index.js +180 -101
- package/lib/enrich-error.js +10 -3
- package/lib/error-codes.js +54 -1
- package/lib/formats.js +262 -0
- package/lib/interpreter.js +6 -5
- package/lib/js-compiler.js +354 -152
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/js-compiler.js
CHANGED
|
@@ -193,11 +193,13 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
193
193
|
// Collect $defs early so sub-schemas can resolve $ref
|
|
194
194
|
const rootDefs = defs || collectDefs(schema)
|
|
195
195
|
|
|
196
|
-
// Bail on features
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
schema.
|
|
196
|
+
// Bail on features this path has no emitter for. Tested for presence, not
|
|
197
|
+
// truthiness: `propertyNames: false` is falsy, passed this bail, and then
|
|
198
|
+
// validated nothing, which the entry-point agreement test caught.
|
|
199
|
+
if (schema.patternProperties !== undefined ||
|
|
200
|
+
schema.dependentSchemas !== undefined ||
|
|
201
|
+
schema.propertyDependencies !== undefined ||
|
|
202
|
+
schema.propertyNames !== undefined) {
|
|
201
203
|
return null
|
|
202
204
|
}
|
|
203
205
|
|
|
@@ -328,7 +330,7 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
328
330
|
}
|
|
329
331
|
|
|
330
332
|
// items, applied after the prefixItems positions
|
|
331
|
-
if (schema.items) {
|
|
333
|
+
if (schema.items !== undefined && schema.items !== true) {
|
|
332
334
|
const itemCheck = compileToJS(schema.items, rootDefs)
|
|
333
335
|
if (!itemCheck) return null
|
|
334
336
|
const start = Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0
|
|
@@ -359,7 +361,7 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
359
361
|
}
|
|
360
362
|
|
|
361
363
|
// contains
|
|
362
|
-
if (schema.contains) {
|
|
364
|
+
if (schema.contains !== undefined) {
|
|
363
365
|
const containsCheck = compileToJS(schema.contains, rootDefs)
|
|
364
366
|
if (!containsCheck) return null
|
|
365
367
|
const minC = schema.minContains !== undefined ? schema.minContains : 1
|
|
@@ -517,20 +519,20 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
517
519
|
}
|
|
518
520
|
|
|
519
521
|
// not
|
|
520
|
-
if (schema.not) {
|
|
522
|
+
if (schema.not !== undefined) {
|
|
521
523
|
const notFn = compileToJS(schema.not, rootDefs)
|
|
522
524
|
if (!notFn) return null
|
|
523
525
|
checks.push((d) => !notFn(d))
|
|
524
526
|
}
|
|
525
527
|
|
|
526
528
|
// if/then/else
|
|
527
|
-
if (schema.if) {
|
|
529
|
+
if (schema.if !== undefined) {
|
|
528
530
|
const ifFn = compileToJS(schema.if, rootDefs)
|
|
529
531
|
if (!ifFn) return null
|
|
530
|
-
const thenFn = schema.then ? compileToJS(schema.then, rootDefs) : null
|
|
531
|
-
const elseFn = schema.else ? compileToJS(schema.else, rootDefs) : null
|
|
532
|
-
if (schema.then && !thenFn) return null
|
|
533
|
-
if (schema.else && !elseFn) return null
|
|
532
|
+
const thenFn = schema.then !== undefined ? compileToJS(schema.then, rootDefs) : null
|
|
533
|
+
const elseFn = schema.else !== undefined ? compileToJS(schema.else, rootDefs) : null
|
|
534
|
+
if (schema.then !== undefined && !thenFn) return null
|
|
535
|
+
if (schema.else !== undefined && !elseFn) return null
|
|
534
536
|
checks.push((d) => {
|
|
535
537
|
if (ifFn(d)) {
|
|
536
538
|
return thenFn ? thenFn(d) : true
|
|
@@ -756,12 +758,13 @@ function _canonical(x) {
|
|
|
756
758
|
return '{' + Object.keys(x).sort().map((k) => JSON.stringify(k) + ':' + _canonical(x[k])).join(',') + '}'
|
|
757
759
|
}
|
|
758
760
|
|
|
761
|
+
const _formats = require('./formats')
|
|
759
762
|
const FORMAT_CHECKS = {
|
|
760
763
|
email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1 },
|
|
761
|
-
date:
|
|
764
|
+
date: _formats.date,
|
|
762
765
|
uuid: (s) => s.length === 36 && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s),
|
|
763
|
-
ipv4:
|
|
764
|
-
hostname:
|
|
766
|
+
ipv4: _formats.ipv4,
|
|
767
|
+
hostname: _formats.hostname,
|
|
765
768
|
}
|
|
766
769
|
|
|
767
770
|
// Dangerous JS property names that exist on Object.prototype
|
|
@@ -842,6 +845,94 @@ function hasNestedIdScope(node, isRoot = true) {
|
|
|
842
845
|
|
|
843
846
|
// Recursively check if a schema can be safely compiled to JS codegen.
|
|
844
847
|
// Returns false if any sub-schema contains features codegen gets wrong.
|
|
848
|
+
// Names of the defs that sit on a $ref cycle among defs. Such a def compiles
|
|
849
|
+
// as a named function rather than being inlined, so recursion terminates in
|
|
850
|
+
// the generated code the same way `$ref: "#"` does at the root. A plain
|
|
851
|
+
// cross-reference (A -> B where B does not refer back) is not on a cycle and
|
|
852
|
+
// still inlines.
|
|
853
|
+
// Data can point back at itself, and a schema that references itself would
|
|
854
|
+
// then recurse until the stack runs out. Validation runs in two passes: the
|
|
855
|
+
// fast one counts depth and gives up past CYCLE_DEPTH, the second re-runs with
|
|
856
|
+
// a set of the values currently under check, where a value met twice is a
|
|
857
|
+
// fixed point and counts as satisfied. That is what the interpreted engine
|
|
858
|
+
// does, so both engines answer the same on cyclic input. Legitimate documents
|
|
859
|
+
// never reach the threshold, so they only pay the counter.
|
|
860
|
+
// Raised by a generator that meets a construct it cannot express. The entry
|
|
861
|
+
// point turns it into a decline, so the caller falls back to an engine that
|
|
862
|
+
// can. Emitting nothing would mean "everything is valid", which is the failure
|
|
863
|
+
// mode this project has shipped three times in $ref handling; declining is the
|
|
864
|
+
// only safe way out.
|
|
865
|
+
const DECLINE = Symbol('ata.decline')
|
|
866
|
+
|
|
867
|
+
const CYCLE_DEPTH = 512
|
|
868
|
+
|
|
869
|
+
// Declarations the guarded functions close over. Emitted once, ahead of them.
|
|
870
|
+
function emitGuardState () {
|
|
871
|
+
return 'const _CYC={};const _stk=new Set();let _sd=0,_sg=false\n '
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Wraps a self-recursive root: `name` is the guarded entry, `name`_b the body.
|
|
875
|
+
function emitRootGuard (name, args, onRepeat) {
|
|
876
|
+
const call = `${name}_b(${args})`
|
|
877
|
+
return (
|
|
878
|
+
`function ${name}(${args}){\n ` +
|
|
879
|
+
`if(_sg){if(typeof d!=='object'||d===null)return ${call};if(_stk.has(d))return ${onRepeat};_stk.add(d);try{return ${call}}finally{_stk.delete(d)}}\n ` +
|
|
880
|
+
`if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${call}\n _sd--\n return _r\n }\n `
|
|
881
|
+
)
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Runs `attempt`, and on the depth signal runs it again under the guard.
|
|
885
|
+
// `reset` clears anything the abandoned pass left behind.
|
|
886
|
+
function emitGuardedRun (attempt, reset, defSets) {
|
|
887
|
+
const clears = ['_stk.clear()'].concat(defSets.map((n) => `${n}_s.clear()`)).join(';')
|
|
888
|
+
return (
|
|
889
|
+
`_sd=0\n try{${attempt}}catch(_err){\n ` +
|
|
890
|
+
`if(_err!==_CYC)throw _err\n ` +
|
|
891
|
+
`${reset}_sg=true;${clears}\n ` +
|
|
892
|
+
`try{${attempt}}finally{_sg=false}\n }\n `
|
|
893
|
+
)
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function cyclicDefNames (defs) {
|
|
897
|
+
const out = new Set()
|
|
898
|
+
if (!defs || typeof defs !== 'object') return out
|
|
899
|
+
const defNames = new Set(Object.keys(defs))
|
|
900
|
+
const defRefs = {}
|
|
901
|
+
for (const [name, def] of Object.entries(defs)) {
|
|
902
|
+
const refs = []
|
|
903
|
+
const seen = new WeakSet()
|
|
904
|
+
const collectRefs = (node) => {
|
|
905
|
+
if (typeof node !== 'object' || node === null) return
|
|
906
|
+
if (seen.has(node)) return
|
|
907
|
+
seen.add(node)
|
|
908
|
+
if (node.$ref) {
|
|
909
|
+
const m = /^#\/(?:\$defs|definitions)\/([^/]+)$/.exec(node.$ref)
|
|
910
|
+
if (m && defNames.has(m[1])) refs.push(m[1])
|
|
911
|
+
}
|
|
912
|
+
for (const val of Object.values(node)) {
|
|
913
|
+
if (typeof val === 'object' && val !== null) {
|
|
914
|
+
if (Array.isArray(val)) { for (const item of val) collectRefs(item) } else collectRefs(val)
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
collectRefs(def)
|
|
919
|
+
defRefs[name] = refs
|
|
920
|
+
}
|
|
921
|
+
const visited = new Set()
|
|
922
|
+
const inStack = []
|
|
923
|
+
const dfs = (node) => {
|
|
924
|
+
const at = inStack.indexOf(node)
|
|
925
|
+
if (at !== -1) { for (let i = at; i < inStack.length; i++) out.add(inStack[i]); return }
|
|
926
|
+
if (visited.has(node)) return
|
|
927
|
+
visited.add(node)
|
|
928
|
+
inStack.push(node)
|
|
929
|
+
for (const neighbor of (defRefs[node] || [])) dfs(neighbor)
|
|
930
|
+
inStack.pop()
|
|
931
|
+
}
|
|
932
|
+
for (const name of Object.keys(defRefs)) dfs(name)
|
|
933
|
+
return out
|
|
934
|
+
}
|
|
935
|
+
|
|
845
936
|
function codegenSafe(schema, schemaMap) {
|
|
846
937
|
if (typeof schema === 'boolean') return true
|
|
847
938
|
if (typeof schema !== 'object' || schema === null) return true
|
|
@@ -855,13 +946,12 @@ function codegenSafe(schema, schemaMap) {
|
|
|
855
946
|
if (schema.$dynamicRef && !schema.$dynamicRef.startsWith('#')) return false
|
|
856
947
|
|
|
857
948
|
// Boolean sub-schemas anywhere cause bail — codegen doesn't handle schema=false correctly
|
|
858
|
-
|
|
859
|
-
|
|
949
|
+
// items: false and items: true are constants; every generator handles a
|
|
950
|
+
// boolean child at its head, so neither falls through as "nothing".
|
|
860
951
|
if (schema.additionalProperties === true) return true // permissive — fine
|
|
861
952
|
if (schema.properties) {
|
|
862
953
|
for (const v of Object.values(schema.properties)) {
|
|
863
|
-
if (v ===
|
|
864
|
-
if (v === true) continue // property: true is always valid
|
|
954
|
+
if (typeof v === 'boolean') continue // constants; every generator handles them at its head
|
|
865
955
|
if (!codegenSafe(v, schemaMap)) return false
|
|
866
956
|
}
|
|
867
957
|
}
|
|
@@ -926,15 +1016,17 @@ function codegenSafe(schema, schemaMap) {
|
|
|
926
1016
|
// additionalProperties as schema — supported when no composition (allOf/oneOf/anyOf)
|
|
927
1017
|
// is in play, and no patternProperties (unified loop does not emit the
|
|
928
1018
|
// per-key sub-schema walk yet). Closure path handles those cases.
|
|
1019
|
+
// additionalProperties as a schema: emitted by the per-key walk, or by the
|
|
1020
|
+
// unified key loop when patternProperties is present. Composition alongside
|
|
1021
|
+
// it is fine, since additionalProperties only looks at this schema's own
|
|
1022
|
+
// properties and patternProperties.
|
|
929
1023
|
if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) {
|
|
930
|
-
if (schema.allOf || schema.oneOf || schema.anyOf) return false
|
|
931
|
-
if (schema.patternProperties) return false
|
|
932
1024
|
if (!codegenSafe(schema.additionalProperties, schemaMap)) return false
|
|
933
1025
|
}
|
|
934
1026
|
if (schema.additionalProperties === false && !schema.properties) return false
|
|
935
1027
|
|
|
936
1028
|
// propertyNames: false — codegen doesn't handle this
|
|
937
|
-
|
|
1029
|
+
// propertyNames: false is emitted as a per-key rejection by every generator.
|
|
938
1030
|
|
|
939
1031
|
// unevaluatedProperties: allow boolean and schema values
|
|
940
1032
|
if (schema.unevaluatedProperties !== undefined) {
|
|
@@ -967,53 +1059,8 @@ function codegenSafe(schema, schemaMap) {
|
|
|
967
1059
|
const defs = schema.$defs || schema.definitions
|
|
968
1060
|
if (defs) {
|
|
969
1061
|
const defNames = new Set(Object.keys(defs))
|
|
970
|
-
//
|
|
971
|
-
//
|
|
972
|
-
// refer back to A) is fine; only true cycles (A -> B -> A) must be rejected.
|
|
973
|
-
// The JS codegen returns (() => true) on cycle (permissive), so cyclic schemas
|
|
974
|
-
// must route to the interpreted engine instead.
|
|
975
|
-
const defRefs = {}
|
|
976
|
-
for (const [name, def] of Object.entries(defs)) {
|
|
977
|
-
defRefs[name] = []
|
|
978
|
-
if (typeof def === 'object' && def !== null) {
|
|
979
|
-
const refs = []
|
|
980
|
-
const seen = new WeakSet()
|
|
981
|
-
const collectRefs = (node) => {
|
|
982
|
-
if (typeof node !== 'object' || node === null) return
|
|
983
|
-
if (seen.has(node)) return
|
|
984
|
-
seen.add(node)
|
|
985
|
-
if (node.$ref) {
|
|
986
|
-
const m = /^#\/(?:\$defs|definitions)\/([^/]+)$/.exec(node.$ref)
|
|
987
|
-
if (m && defNames.has(m[1])) refs.push(m[1])
|
|
988
|
-
}
|
|
989
|
-
for (const val of Object.values(node)) {
|
|
990
|
-
if (typeof val === 'object' && val !== null) {
|
|
991
|
-
if (Array.isArray(val)) { for (const item of val) collectRefs(item) }
|
|
992
|
-
else collectRefs(val)
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
collectRefs(def)
|
|
997
|
-
defRefs[name] = refs
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
const hasCycle = (() => {
|
|
1001
|
-
const visited = new Set()
|
|
1002
|
-
const inStack = new Set()
|
|
1003
|
-
const dfs = (node) => {
|
|
1004
|
-
if (inStack.has(node)) return true
|
|
1005
|
-
if (visited.has(node)) return false
|
|
1006
|
-
visited.add(node)
|
|
1007
|
-
inStack.add(node)
|
|
1008
|
-
for (const neighbor of (defRefs[node] || [])) {
|
|
1009
|
-
if (dfs(neighbor)) return true
|
|
1010
|
-
}
|
|
1011
|
-
inStack.delete(node)
|
|
1012
|
-
return false
|
|
1013
|
-
}
|
|
1014
|
-
return Object.keys(defRefs).some(dfs)
|
|
1015
|
-
})()
|
|
1016
|
-
if (hasCycle) return false
|
|
1062
|
+
// Defs on a $ref cycle compile as named functions (see genCode); nothing
|
|
1063
|
+
// to decline here. cyclicDefNames() is the walk that used to bail.
|
|
1017
1064
|
for (const [name, def] of Object.entries(defs)) {
|
|
1018
1065
|
if (/[~/"']/.test(name)) return false // special chars in def name
|
|
1019
1066
|
if (typeof def === 'boolean') return false
|
|
@@ -1030,6 +1077,10 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1030
1077
|
}
|
|
1031
1078
|
|
|
1032
1079
|
// Recurse into sub-schemas — bail on boolean schemas in any position
|
|
1080
|
+
// A boolean subschema is a constant that every generator emits at its
|
|
1081
|
+
// head with the child's own path. The emitters below test for presence,
|
|
1082
|
+
// not truthiness, because `false` is falsy and a truthiness test would
|
|
1083
|
+
// skip it and validate nothing.
|
|
1033
1084
|
const subs = [
|
|
1034
1085
|
schema.items, schema.contains, schema.not,
|
|
1035
1086
|
schema.if, schema.then, schema.else,
|
|
@@ -1040,9 +1091,7 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1040
1091
|
]
|
|
1041
1092
|
if (typeof schema.additionalProperties === 'object') subs.push(schema.additionalProperties)
|
|
1042
1093
|
for (const s of subs) {
|
|
1043
|
-
if (s === undefined || s === null) continue
|
|
1044
|
-
if (s === false) return false // boolean false sub-schema — complex
|
|
1045
|
-
if (s === true) continue // boolean true sub-schema — always valid, fine
|
|
1094
|
+
if (s === undefined || s === null || typeof s === 'boolean') continue
|
|
1046
1095
|
if (!codegenSafe(s, schemaMap)) return false
|
|
1047
1096
|
}
|
|
1048
1097
|
|
|
@@ -1330,7 +1379,6 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
1330
1379
|
// patternProperties: bail only on boolean sub-schemas or unicode property escapes
|
|
1331
1380
|
if (schema.patternProperties) {
|
|
1332
1381
|
for (const [pat, sub] of Object.entries(schema.patternProperties)) {
|
|
1333
|
-
if (typeof sub === 'boolean') return null
|
|
1334
1382
|
if (/\\[pP]\{/.test(pat)) return null
|
|
1335
1383
|
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
1336
1384
|
}
|
|
@@ -1338,13 +1386,11 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
1338
1386
|
// dependentSchemas: bail on boolean sub-schemas
|
|
1339
1387
|
if (schema.dependentSchemas) {
|
|
1340
1388
|
for (const sub of Object.values(schema.dependentSchemas)) {
|
|
1341
|
-
if (typeof sub === 'boolean') return null
|
|
1342
1389
|
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
1343
1390
|
}
|
|
1344
1391
|
}
|
|
1345
1392
|
// propertyNames: only codegen simple string constraints
|
|
1346
1393
|
if (schema.propertyNames) {
|
|
1347
|
-
if (typeof schema.propertyNames === 'boolean') return null
|
|
1348
1394
|
const pn = schema.propertyNames
|
|
1349
1395
|
const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
|
|
1350
1396
|
const keys = Object.keys(pn).filter(k => k !== '$schema')
|
|
@@ -1414,18 +1460,33 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
1414
1460
|
}
|
|
1415
1461
|
}
|
|
1416
1462
|
|
|
1463
|
+
const defSets = ctx.defFns ? Array.from(ctx.defFns.values()) : []
|
|
1464
|
+
const needsGuard = ctx.usesRecursion || defSets.length > 0
|
|
1465
|
+
|
|
1417
1466
|
let body, hybridBody
|
|
1418
1467
|
if (ctx.usesRecursion) {
|
|
1419
|
-
// Self-recursive:
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1468
|
+
// Self-recursive: a named body reached through the cycle guard, so data
|
|
1469
|
+
// that points back at itself settles instead of exhausting the stack.
|
|
1470
|
+
const decl = emitRootGuard('_validate', 'd', 'true') +
|
|
1471
|
+
`function _validate_b(d){\n ${checkStr}\n return true\n }\n `
|
|
1472
|
+
const run = emitGuardedRun('return _validate(d)', '', defSets)
|
|
1473
|
+
body = `${decl}function _run(d){\n ${run}\n }\n return _run(d)`
|
|
1474
|
+
// Hybrid: keep the boolean answer, wrap only the outer call
|
|
1475
|
+
hybridBody = `${decl}function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
|
|
1476
|
+
} else if (defSets.length > 0) {
|
|
1477
|
+
// No self-reference at the root, but a def on a cycle can still be
|
|
1478
|
+
// re-entered with the same value; the guarded functions need their state.
|
|
1479
|
+
const run = emitGuardedRun(`return _body(d)`, '', defSets)
|
|
1480
|
+
body = `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)`
|
|
1481
|
+
hybridBody = `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
|
|
1423
1482
|
} else {
|
|
1424
1483
|
body = checkStr + '\n return true'
|
|
1425
1484
|
hybridBody = replaceTopLevel(checkStr + '\n return R')
|
|
1426
1485
|
}
|
|
1427
1486
|
|
|
1428
|
-
const
|
|
1487
|
+
const guardStr = needsGuard ? emitGuardState() : ''
|
|
1488
|
+
const preambleStr = guardStr +
|
|
1489
|
+
(ctx.preamble && ctx.preamble.length ? ctx.preamble.join('\n ') + '\n ' : '')
|
|
1429
1490
|
|
|
1430
1491
|
try {
|
|
1431
1492
|
let boolFn
|
|
@@ -1654,6 +1715,11 @@ function _deferOrInline(ctx, lines, v, check) {
|
|
|
1654
1715
|
// 'array' = we know v is an array
|
|
1655
1716
|
// 'string' / 'number' / 'integer' = we know the primitive type
|
|
1656
1717
|
function genCode(schema, v, lines, ctx, knownType) {
|
|
1718
|
+
// A boolean schema is a constant. false rejects whatever reached it; true
|
|
1719
|
+
// constrains nothing and emits nothing, which is correct only because the
|
|
1720
|
+
// caller's own checks still stand around this call.
|
|
1721
|
+
if (schema === false) { lines.push('return false'); return }
|
|
1722
|
+
if (schema === true) return
|
|
1657
1723
|
if (typeof schema !== 'object' || schema === null) return
|
|
1658
1724
|
if (!ctx.regExpMap) {
|
|
1659
1725
|
ctx.regExpMap = new Map();
|
|
@@ -1673,10 +1739,37 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1673
1739
|
// 1. Local ref
|
|
1674
1740
|
const m = schema.$ref !== '#' && schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
1675
1741
|
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
1676
|
-
|
|
1742
|
+
const defName = m[1]
|
|
1743
|
+
if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
|
|
1744
|
+
if (ctx.cyclicDefs.has(defName) && ctx.preamble) {
|
|
1745
|
+
// A def on a cycle is a named function in the preamble, emitted once
|
|
1746
|
+
// and called by name, so recursion terminates the way `$ref: "#"`
|
|
1747
|
+
// does at the root. The name is registered before the body is
|
|
1748
|
+
// generated so a self-reference inside it becomes a call.
|
|
1749
|
+
if (!ctx.defFns) ctx.defFns = new Map()
|
|
1750
|
+
let fnName = ctx.defFns.get(defName)
|
|
1751
|
+
if (!fnName) {
|
|
1752
|
+
fnName = '_def' + ctx.defFns.size + '_' + defName.replace(/[^A-Za-z0-9_]/g, '_')
|
|
1753
|
+
ctx.defFns.set(defName, fnName)
|
|
1754
|
+
const bodyLines = []
|
|
1755
|
+
genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)
|
|
1756
|
+
// Entered through the cycle guard: see emitGuardState. In the fast
|
|
1757
|
+
// pass the wrapper only counts depth; the set is used by the second,
|
|
1758
|
+
// guarded pass that runs when data turns out to point back at
|
|
1759
|
+
// itself.
|
|
1760
|
+
ctx.preamble.push(
|
|
1761
|
+
`const ${fnName}_s=new Set()\n function ${fnName}(d){\n ` +
|
|
1762
|
+
`if(_sg){if(typeof d!=='object'||d===null)return ${fnName}_b(d);if(${fnName}_s.has(d))return true;${fnName}_s.add(d);try{return ${fnName}_b(d)}finally{${fnName}_s.delete(d)}}\n ` +
|
|
1763
|
+
`if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(d)\n _sd--\n return _r\n }\n ` +
|
|
1764
|
+
`function ${fnName}_b(d){${bodyLines.join('\n ')}\n return true}`,
|
|
1765
|
+
)
|
|
1766
|
+
}
|
|
1767
|
+
lines.push(`if(!${fnName}(${v}))return false`)
|
|
1768
|
+
if (!hasSiblings) return
|
|
1769
|
+
} else if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
|
|
1677
1770
|
else {
|
|
1678
1771
|
ctx.refStack.add(schema.$ref)
|
|
1679
|
-
genCode(ctx.rootDefs[
|
|
1772
|
+
genCode(ctx.rootDefs[defName], v, lines, ctx, knownType)
|
|
1680
1773
|
ctx.refStack.delete(schema.$ref)
|
|
1681
1774
|
if (!hasSiblings) return
|
|
1682
1775
|
}
|
|
@@ -2048,11 +2141,23 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2048
2141
|
|
|
2049
2142
|
const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
2050
2143
|
|
|
2051
|
-
|
|
2052
|
-
|
|
2144
|
+
const apSchema = typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null ? schema.additionalProperties : null
|
|
2145
|
+
if ((schema.additionalProperties === false && schema.properties) || apSchema) {
|
|
2146
|
+
// Unified loop: properties + patterns + propertyNames + additionalProperties.
|
|
2147
|
+
// A key that is neither declared nor matched by any pattern is
|
|
2148
|
+
// additional: rejected outright, or run through the additionalProperties
|
|
2149
|
+
// schema when there is one.
|
|
2053
2150
|
ctx._ppHandledAdditional = true
|
|
2054
2151
|
ctx._ppHandledPropertyNames = !!pn
|
|
2055
|
-
const propKeys = Object.keys(schema.properties)
|
|
2152
|
+
const propKeys = Object.keys(schema.properties || {})
|
|
2153
|
+
let apFn = null
|
|
2154
|
+
if (apSchema) {
|
|
2155
|
+
const apLines = []
|
|
2156
|
+
genCode(apSchema, '_apv', apLines, ctx)
|
|
2157
|
+
apFn = `_apf${pi}`
|
|
2158
|
+
ctx.closureVars.push(apFn)
|
|
2159
|
+
ctx.closureVals.push(new Function('_apv', apLines.length === 0 ? 'return true' : `${apLines.join(';')};return true`))
|
|
2160
|
+
}
|
|
2056
2161
|
lines.push(`${guard}{for(const ${kVar} in ${v}){`)
|
|
2057
2162
|
// propertyNames checks (merged into same loop)
|
|
2058
2163
|
if (pn) {
|
|
@@ -2077,21 +2182,29 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2077
2182
|
lines.push(`if(!_es${ei}.has(${kVar}))return false`)
|
|
2078
2183
|
}
|
|
2079
2184
|
}
|
|
2080
|
-
//
|
|
2081
|
-
//
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
//
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2185
|
+
// Every matching pattern applies to every key, declared or not: a
|
|
2186
|
+
// declared key that also matches a pattern is validated by both (the
|
|
2187
|
+
// properties emitter handles its own subschema). A flag rather than an
|
|
2188
|
+
// if/else chain, because a chain returned false at the first pattern
|
|
2189
|
+
// that missed and wrongly rejected a key only a later pattern matched.
|
|
2190
|
+
if (ppEntries.length > 0) {
|
|
2191
|
+
lines.push(`let _pm${pi}=false`)
|
|
2192
|
+
for (let i = 0; i < ppEntries.length; i++) {
|
|
2193
|
+
lines.push(`if(${matchers[i].check}){_pm${pi}=true;if(!_ppf${pi}_${i}(${v}[${kVar}]))return false}`)
|
|
2194
|
+
}
|
|
2088
2195
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2196
|
+
// A key that is neither declared nor matched is additional. switch on
|
|
2197
|
+
// the declared names (V8 compiles string cases to a jump table); no
|
|
2198
|
+
// switch at all when nothing is declared, since a switch with no case
|
|
2199
|
+
// clause is a syntax error.
|
|
2200
|
+
const additional = apFn ? `if(!${apFn}(${v}[${kVar}]))return false` : `return false`
|
|
2201
|
+
const notMatched = ppEntries.length > 0 ? `if(!_pm${pi}){${additional}}` : additional
|
|
2202
|
+
if (propKeys.length) {
|
|
2203
|
+
const switchCases = propKeys.map(k => `case ${JSON.stringify(k)}:`).join('')
|
|
2204
|
+
lines.push(`switch(${kVar}){${switchCases}break;default:{${notMatched}}}`)
|
|
2091
2205
|
} else {
|
|
2092
|
-
lines.push(
|
|
2206
|
+
lines.push(notMatched)
|
|
2093
2207
|
}
|
|
2094
|
-
lines.push(`}`) // end switch
|
|
2095
2208
|
lines.push(`}}`)
|
|
2096
2209
|
} else {
|
|
2097
2210
|
// No additionalProperties: validate matching keys + propertyNames
|
|
@@ -2139,6 +2252,13 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2139
2252
|
}
|
|
2140
2253
|
}
|
|
2141
2254
|
|
|
2255
|
+
// propertyNames: false rejects any key at all; true constrains nothing.
|
|
2256
|
+
if (schema.propertyNames === false) {
|
|
2257
|
+
lines.push(isObj
|
|
2258
|
+
? `for(const _k in ${v})return false`
|
|
2259
|
+
: `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k in ${v})return false}`)
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2142
2262
|
// propertyNames — only emit if not already merged into patternProperties loop
|
|
2143
2263
|
if (schema.propertyNames && typeof schema.propertyNames === 'object' && !ctx._ppHandledPropertyNames) {
|
|
2144
2264
|
const pn = schema.propertyNames
|
|
@@ -2207,7 +2327,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2207
2327
|
}
|
|
2208
2328
|
|
|
2209
2329
|
// items — pass known type info to children
|
|
2210
|
-
if (schema.items) {
|
|
2330
|
+
if (schema.items !== undefined && schema.items !== true) {
|
|
2211
2331
|
const idx = `_j${ctx.varCounter}`
|
|
2212
2332
|
const elem = `_e${ctx.varCounter}`
|
|
2213
2333
|
ctx.varCounter++
|
|
@@ -2234,7 +2354,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2234
2354
|
}
|
|
2235
2355
|
|
|
2236
2356
|
// contains — use helper function to avoid try/catch overhead
|
|
2237
|
-
if (schema.contains) {
|
|
2357
|
+
if (schema.contains !== undefined) {
|
|
2238
2358
|
const ci = ctx.varCounter++
|
|
2239
2359
|
const minC = schema.minContains !== undefined ? schema.minContains : 1
|
|
2240
2360
|
const maxC = schema.maxContains !== undefined ? schema.maxContains : Infinity
|
|
@@ -2314,7 +2434,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2314
2434
|
}
|
|
2315
2435
|
|
|
2316
2436
|
// not
|
|
2317
|
-
if (schema.not) {
|
|
2437
|
+
if (schema.not !== undefined) {
|
|
2318
2438
|
const subLines = []
|
|
2319
2439
|
genCode(schema.not, '_nv', subLines, ctx)
|
|
2320
2440
|
if (subLines.length === 0) {
|
|
@@ -2326,7 +2446,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2326
2446
|
}
|
|
2327
2447
|
|
|
2328
2448
|
// if/then/else
|
|
2329
|
-
if (schema.if) {
|
|
2449
|
+
if (schema.if !== undefined) {
|
|
2330
2450
|
const ifLines = []
|
|
2331
2451
|
genCode(schema.if, '_iv', ifLines, ctx)
|
|
2332
2452
|
const fi = ctx.varCounter++
|
|
@@ -2335,14 +2455,14 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2335
2455
|
: `function(_iv){${ifLines.join(';')};return true}`
|
|
2336
2456
|
|
|
2337
2457
|
let thenFn = 'null', elseFn = 'null'
|
|
2338
|
-
if (schema.then) {
|
|
2458
|
+
if (schema.then !== undefined) {
|
|
2339
2459
|
const thenLines = []
|
|
2340
2460
|
genCode(schema.then, '_tv', thenLines, ctx)
|
|
2341
2461
|
thenFn = thenLines.length === 0
|
|
2342
2462
|
? `function(_tv){return true}`
|
|
2343
2463
|
: `function(_tv){${thenLines.join(';')};return true}`
|
|
2344
2464
|
}
|
|
2345
|
-
if (schema.else) {
|
|
2465
|
+
if (schema.else !== undefined) {
|
|
2346
2466
|
const elseLines = []
|
|
2347
2467
|
genCode(schema.else, '_ev', elseLines, ctx)
|
|
2348
2468
|
elseFn = elseLines.length === 0
|
|
@@ -2846,15 +2966,11 @@ const FORMAT_CODEGEN = {
|
|
|
2846
2966
|
? `{const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
|
|
2847
2967
|
: `if(typeof ${v}==='string'){const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
|
|
2848
2968
|
},
|
|
2849
|
-
date:
|
|
2850
|
-
? `{if(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v}))return false;const _dm=+${v}.slice(5,7),_dd=+${v}.slice(8,10);if(_dm<1||_dm>12||_dd<1||_dd>31)return false}`
|
|
2851
|
-
: `if(typeof ${v}==='string'){if(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v}))return false;const _dm=+${v}.slice(5,7),_dd=+${v}.slice(8,10);if(_dm<1||_dm>12||_dd<1||_dd>31)return false}`,
|
|
2969
|
+
date: _formats.dateSource,
|
|
2852
2970
|
uuid: (v, isStr) => isStr
|
|
2853
2971
|
? `if(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v}))return false`
|
|
2854
2972
|
: `if(typeof ${v}==='string'&&(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v})))return false`,
|
|
2855
|
-
'date-time':
|
|
2856
|
-
? `if(!/^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?([Zz]|[+-]\\d{2}:\\d{2})$/.test(${v})||isNaN(Date.parse(${v})))return false`
|
|
2857
|
-
: `if(typeof ${v}==='string'&&(!/^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?([Zz]|[+-]\\d{2}:\\d{2})$/.test(${v})||isNaN(Date.parse(${v}))))return false`,
|
|
2973
|
+
'date-time': _formats.dateTimeSource,
|
|
2858
2974
|
time: (v, isStr) => isStr
|
|
2859
2975
|
? `if(!/^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$/.test(${v}))return false`
|
|
2860
2976
|
: `if(typeof ${v}==='string'&&!/^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$/.test(${v}))return false`,
|
|
@@ -2867,15 +2983,9 @@ const FORMAT_CODEGEN = {
|
|
|
2867
2983
|
'uri-reference': (v, isStr) => isStr
|
|
2868
2984
|
? `if(/[\\s\\u0000-\\u001f\\u007f]/.test(${v}))return false`
|
|
2869
2985
|
: `if(typeof ${v}==='string'&&/[\\s\\u0000-\\u001f\\u007f]/.test(${v}))return false`,
|
|
2870
|
-
ipv4:
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
ipv6: (v, isStr) => isStr
|
|
2874
|
-
? `{const _s=${v};if(_s===''||!/^[0-9a-fA-F:]+$/.test(_s)||_s.split(':').length<3||_s.split(':').length>8)return false}`
|
|
2875
|
-
: `if(typeof ${v}==='string'){const _s=${v};if(_s===''||!/^[0-9a-fA-F:]+$/.test(_s)||_s.split(':').length<3||_s.split(':').length>8)return false}`,
|
|
2876
|
-
hostname: (v, isStr) => isStr
|
|
2877
|
-
? `if(!/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(${v}))return false`
|
|
2878
|
-
: `if(typeof ${v}==='string'&&!/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(${v}))return false`,
|
|
2986
|
+
ipv4: _formats.ipv4Source,
|
|
2987
|
+
ipv6: _formats.ipv6Source,
|
|
2988
|
+
hostname: _formats.hostnameSource,
|
|
2879
2989
|
}
|
|
2880
2990
|
|
|
2881
2991
|
// Safe key escaping: use JSON.stringify to handle all special chars (newlines, null bytes, etc.)
|
|
@@ -3020,8 +3130,6 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
3020
3130
|
if (typeof schema === 'object' && schema !== null) {
|
|
3021
3131
|
const s = JSON.stringify(schema)
|
|
3022
3132
|
if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
|
|
3023
|
-
// Bail on self-referencing schemas — error codegen doesn't support recursion
|
|
3024
|
-
if (s.includes('"$ref":"#"')) return null
|
|
3025
3133
|
}
|
|
3026
3134
|
if (typeof schema === 'boolean') {
|
|
3027
3135
|
return schema
|
|
@@ -3032,19 +3140,16 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
3032
3140
|
if (!sharedCodegenGate(schema, schemaMap)) return null
|
|
3033
3141
|
if (schema.patternProperties) {
|
|
3034
3142
|
for (const [pat, sub] of Object.entries(schema.patternProperties)) {
|
|
3035
|
-
if (typeof sub === 'boolean') return null
|
|
3036
3143
|
if (/\\[pP]\{/.test(pat)) return null
|
|
3037
3144
|
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
3038
3145
|
}
|
|
3039
3146
|
}
|
|
3040
3147
|
if (schema.dependentSchemas) {
|
|
3041
3148
|
for (const sub of Object.values(schema.dependentSchemas)) {
|
|
3042
|
-
if (typeof sub === 'boolean') return null
|
|
3043
3149
|
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
3044
3150
|
}
|
|
3045
3151
|
}
|
|
3046
3152
|
if (schema.propertyNames) {
|
|
3047
|
-
if (typeof schema.propertyNames === 'boolean') return null
|
|
3048
3153
|
const pn = schema.propertyNames
|
|
3049
3154
|
const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
|
|
3050
3155
|
const keys = Object.keys(pn).filter(k => k !== '$schema')
|
|
@@ -3087,7 +3192,12 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
3087
3192
|
: null }
|
|
3088
3193
|
ctx.helperCode.push('const _cpLen=s=>{let n=0;for(const _ of s)n++;return n}')
|
|
3089
3194
|
const lines = []
|
|
3090
|
-
|
|
3195
|
+
try {
|
|
3196
|
+
genCodeE(schema, 'd', '', lines, ctx, '#')
|
|
3197
|
+
} catch (e) {
|
|
3198
|
+
if (e === DECLINE) return null
|
|
3199
|
+
throw e
|
|
3200
|
+
}
|
|
3091
3201
|
// Inline branch-collapse helper only when the schema actually used a
|
|
3092
3202
|
// oneOf/anyOf branch path (tracked via ctx.usesBranchCollapse). Keeps the
|
|
3093
3203
|
// AOT output small for schemas that don't compose. Standalone modules can't
|
|
@@ -3098,16 +3208,28 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
3098
3208
|
if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
|
|
3099
3209
|
|
|
3100
3210
|
const checkStr = lines.join('\n ')
|
|
3211
|
+
const defSetsE = ctx.defFns ? Array.from(ctx.defFns.values()) : []
|
|
3212
|
+
const needsGuardE = ctx.usesRecursion || defSetsE.length > 0
|
|
3213
|
+
const guardStrE = needsGuardE ? emitGuardState() : ''
|
|
3214
|
+
const helpersE = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
|
|
3215
|
+
|
|
3101
3216
|
let body
|
|
3102
3217
|
if (ctx.usesRecursion) {
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3218
|
+
// The abandoned first pass may have pushed errors, so the retry starts
|
|
3219
|
+
// from an empty list and the reported errors come from the guarded run
|
|
3220
|
+
// alone.
|
|
3221
|
+
body = `const _e=[];\n ` + guardStrE + helpersE +
|
|
3222
|
+
emitRootGuard('_validateE', 'd,_all,_e', '') +
|
|
3223
|
+
`function _validateE_b(d,_all,_e){\n ${checkStr}\n }\n ` +
|
|
3224
|
+
emitGuardedRun('_validateE(d,_all,_e)', '_e.length=0;', defSetsE) +
|
|
3225
|
+
`return{valid:_e.length===0,errors:_e}`
|
|
3226
|
+
} else if (defSetsE.length > 0) {
|
|
3227
|
+
body = `const _e=[];\n ` + guardStrE + helpersE +
|
|
3228
|
+
`function _bodyE(d,_all,_e){\n ${checkStr}\n }\n ` +
|
|
3229
|
+
emitGuardedRun('_bodyE(d,_all,_e)', '_e.length=0;', defSetsE) +
|
|
3106
3230
|
`return{valid:_e.length===0,errors:_e}`
|
|
3107
3231
|
} else {
|
|
3108
|
-
body = `const _e=[];\n ` +
|
|
3109
|
-
(ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
|
|
3110
|
-
checkStr +
|
|
3232
|
+
body = `const _e=[];\n ` + helpersE + checkStr +
|
|
3111
3233
|
`\n return{valid:_e.length===0,errors:_e}`
|
|
3112
3234
|
}
|
|
3113
3235
|
try {
|
|
@@ -3145,6 +3267,10 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
3145
3267
|
const subSp = branchSp + '/' + i
|
|
3146
3268
|
if (typeof sub === 'object' && sub !== null) {
|
|
3147
3269
|
genCodeE(sub, '_bv', pathExpr, subLines, ctx, subSp)
|
|
3270
|
+
} else if (sub === false) {
|
|
3271
|
+
// A false branch never matches; it reports the same error the
|
|
3272
|
+
// interpreter attaches to a false schema.
|
|
3273
|
+
subLines.push(`_e.push({keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${subSp}',params:{},message:'boolean schema is false'})`)
|
|
3148
3274
|
}
|
|
3149
3275
|
const title = (sub && typeof sub === 'object' && typeof sub.title === 'string') ? sub.title : ''
|
|
3150
3276
|
titles.push(title)
|
|
@@ -3166,19 +3292,54 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
3166
3292
|
// `_all` parameter: if falsy, return after first error.
|
|
3167
3293
|
function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
3168
3294
|
if (!schemaPrefix) schemaPrefix = '#'
|
|
3295
|
+
// A false schema reports one error at the value that reached it, with the
|
|
3296
|
+
// keyword the interpreter has always used for it, `not` (lib/plan-compiler.js
|
|
3297
|
+
// line 58; its `err()` discards the 'false schema' label). Other validators
|
|
3298
|
+
// name this keyword `false schema`; changing it here would change output
|
|
3299
|
+
// the interpreter path already produces, so both engines say `not`.
|
|
3300
|
+
if (schema === false) {
|
|
3301
|
+
lines.push(`;_e.push({keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}',params:{},message:'boolean schema is false'});if(!_all)return{valid:false,errors:_e}`)
|
|
3302
|
+
return
|
|
3303
|
+
}
|
|
3304
|
+
if (schema === true) return
|
|
3169
3305
|
if (typeof schema !== 'object' || schema === null) return
|
|
3170
3306
|
if (!ctx.regExpMap) {
|
|
3171
3307
|
ctx.regExpMap = new Map();
|
|
3172
3308
|
}
|
|
3173
3309
|
// $ref — resolve local and cross-schema refs
|
|
3174
3310
|
if (schema.$ref) {
|
|
3175
|
-
//
|
|
3176
|
-
|
|
3311
|
+
// A self-reference cannot be expressed here yet: the root helper takes no
|
|
3312
|
+
// path argument, so errors raised through it would carry the path of the
|
|
3313
|
+
// root rather than of the value that failed. This used to fall through as
|
|
3314
|
+
// a permissive no-op, which accepted whatever the reference guarded.
|
|
3315
|
+
if (schema.$ref === '#') throw DECLINE
|
|
3177
3316
|
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
3178
3317
|
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
3318
|
+
const defName = m[1]
|
|
3319
|
+
if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
|
|
3320
|
+
if (ctx.cyclicDefs.has(defName)) {
|
|
3321
|
+
// Named helper for a def on a cycle; the path comes in as an
|
|
3322
|
+
// argument because every call site has its own.
|
|
3323
|
+
if (!ctx.defFns) ctx.defFns = new Map()
|
|
3324
|
+
let fnName = ctx.defFns.get(defName)
|
|
3325
|
+
if (!fnName) {
|
|
3326
|
+
fnName = '_defE' + ctx.defFns.size + '_' + defName.replace(/[^A-Za-z0-9_]/g, '_')
|
|
3327
|
+
ctx.defFns.set(defName, fnName)
|
|
3328
|
+
const bodyLines = []
|
|
3329
|
+
genCodeE(ctx.rootDefs[defName], 'd', '_p', bodyLines, ctx, '#/$defs/' + defName)
|
|
3330
|
+
ctx.helperCode.push(
|
|
3331
|
+
`const ${fnName}_s=new Set()\n function ${fnName}(d,_p,_all,_e){\n ` +
|
|
3332
|
+
`if(_sg){if(typeof d!=='object'||d===null)return ${fnName}_b(d,_p,_all,_e);if(${fnName}_s.has(d))return;${fnName}_s.add(d);try{return ${fnName}_b(d,_p,_all,_e)}finally{${fnName}_s.delete(d)}}\n ` +
|
|
3333
|
+
`if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(d,_p,_all,_e)\n _sd--\n return _r\n }\n ` +
|
|
3334
|
+
`function ${fnName}_b(d,_p,_all,_e){${bodyLines.join('\n ')}}`,
|
|
3335
|
+
)
|
|
3336
|
+
}
|
|
3337
|
+
lines.push(`${fnName}(${v},${pathExpr || '""'},_all,_e);if(!_all&&_e.length)return{valid:false,errors:_e}`)
|
|
3338
|
+
return
|
|
3339
|
+
}
|
|
3179
3340
|
if (ctx.refStack.has(schema.$ref)) return
|
|
3180
3341
|
ctx.refStack.add(schema.$ref)
|
|
3181
|
-
genCodeE(ctx.rootDefs[
|
|
3342
|
+
genCodeE(ctx.rootDefs[defName], v, pathExpr, lines, ctx, schemaPrefix)
|
|
3182
3343
|
ctx.refStack.delete(schema.$ref)
|
|
3183
3344
|
return
|
|
3184
3345
|
}
|
|
@@ -3496,7 +3657,26 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3496
3657
|
const ki = ctx.varCounter++
|
|
3497
3658
|
const known = Object.keys(schema.properties || {})
|
|
3498
3659
|
const guard = known.length ? `const _ak${ki}=new Set([${known.map((k) => JSON.stringify(k)).join(',')}]);` : ''
|
|
3499
|
-
|
|
3660
|
+
// A key matched by any patternProperties entry is not additional.
|
|
3661
|
+
const patTests = []
|
|
3662
|
+
for (const pat of Object.keys(schema.patternProperties || {})) {
|
|
3663
|
+
const pattern = JSON.stringify(pat)
|
|
3664
|
+
if (!ctx.regExpMap.has(pattern)) {
|
|
3665
|
+
const ri = ctx.varCounter++
|
|
3666
|
+
ctx.regExpMap.set(pattern, ri)
|
|
3667
|
+
if (patternIsSafe(pat)) {
|
|
3668
|
+
ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
|
|
3669
|
+
ctx.usesSafeRe = true
|
|
3670
|
+
} else {
|
|
3671
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
patTests.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ki})`)
|
|
3675
|
+
}
|
|
3676
|
+
const conds = []
|
|
3677
|
+
if (known.length) conds.push(`!_ak${ki}.has(_k${ki})`)
|
|
3678
|
+
if (patTests.length) conds.push(`!(${patTests.join('||')})`)
|
|
3679
|
+
const keep = conds.length ? `if(${conds.join('&&')}){` : `{`
|
|
3500
3680
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${guard}for(const _k${ki} in ${v}){${keep}`)
|
|
3501
3681
|
const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
|
|
3502
3682
|
genCodeE(schema.additionalProperties, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/additionalProperties')
|
|
@@ -3512,6 +3692,14 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3512
3692
|
}
|
|
3513
3693
|
}
|
|
3514
3694
|
|
|
3695
|
+
// propertyNames: false reports every key, at the key's own path, the way the
|
|
3696
|
+
// interpreter does (plan-compiler: instancePath + '/' + key).
|
|
3697
|
+
if (schema.propertyNames === false) {
|
|
3698
|
+
const ki = ctx.varCounter++
|
|
3699
|
+
const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
|
|
3700
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){_e.push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames',params:{},message:'boolean schema is false'});if(!_all)return{valid:false,errors:_e}}}`)
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3515
3703
|
// propertyNames
|
|
3516
3704
|
if (schema.propertyNames && typeof schema.propertyNames === 'object') {
|
|
3517
3705
|
const pn = schema.propertyNames
|
|
@@ -3550,7 +3738,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3550
3738
|
}
|
|
3551
3739
|
|
|
3552
3740
|
// items — starts after prefixItems (Draft 2020-12 semantics)
|
|
3553
|
-
if (schema.items) {
|
|
3741
|
+
if (schema.items !== undefined && schema.items !== true) {
|
|
3554
3742
|
const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
|
|
3555
3743
|
const idx = `_j${ctx.varCounter}`
|
|
3556
3744
|
const elem = `_ei${ctx.varCounter}`
|
|
@@ -3572,7 +3760,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3572
3760
|
}
|
|
3573
3761
|
|
|
3574
3762
|
// contains
|
|
3575
|
-
if (schema.contains) {
|
|
3763
|
+
if (schema.contains !== undefined) {
|
|
3576
3764
|
const ci = ctx.varCounter++
|
|
3577
3765
|
const subLines = []
|
|
3578
3766
|
genCode(schema.contains, `_cv`, subLines, ctx)
|
|
@@ -3605,7 +3793,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3605
3793
|
}
|
|
3606
3794
|
|
|
3607
3795
|
// not
|
|
3608
|
-
if (schema.not) {
|
|
3796
|
+
if (schema.not !== undefined) {
|
|
3609
3797
|
const subLines = []
|
|
3610
3798
|
genCode(schema.not, '_nv', subLines, ctx)
|
|
3611
3799
|
const nfn = subLines.length === 0 ? `function(_nv){return true}` : `function(_nv){${subLines.join(';')};return true}`
|
|
@@ -3614,7 +3802,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3614
3802
|
}
|
|
3615
3803
|
|
|
3616
3804
|
// if/then/else
|
|
3617
|
-
if (schema.if) {
|
|
3805
|
+
if (schema.if !== undefined) {
|
|
3618
3806
|
const ifLines = []
|
|
3619
3807
|
genCode(schema.if, '_iv', ifLines, ctx)
|
|
3620
3808
|
const fi = ctx.varCounter++
|
|
@@ -3622,13 +3810,13 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3622
3810
|
? `function(_iv){return true}`
|
|
3623
3811
|
: `function(_iv){${ifLines.join(';')};return true}`
|
|
3624
3812
|
lines.push(`{const _if${fi}=${ifFn}`)
|
|
3625
|
-
if (schema.then) {
|
|
3813
|
+
if (schema.then !== undefined) {
|
|
3626
3814
|
lines.push(`if(_if${fi}(${v})){`)
|
|
3627
3815
|
genCodeE(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then')
|
|
3628
3816
|
lines.push(`}`)
|
|
3629
3817
|
}
|
|
3630
|
-
if (schema.else) {
|
|
3631
|
-
lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`)
|
|
3818
|
+
if (schema.else !== undefined) {
|
|
3819
|
+
lines.push(`${schema.then !== undefined ? 'else' : `if(!_if${fi}(${v}))`}{`)
|
|
3632
3820
|
genCodeE(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else')
|
|
3633
3821
|
lines.push(`}`)
|
|
3634
3822
|
}
|
|
@@ -3647,6 +3835,10 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
|
|
|
3647
3835
|
if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
|
|
3648
3836
|
// Bail on self-referencing schemas — combined codegen doesn't support recursion
|
|
3649
3837
|
if (s.includes('"$ref":"#"')) return null
|
|
3838
|
+
// Defs on a $ref cycle compile as named functions in the boolean and
|
|
3839
|
+
// errors generators; this generator has no preamble for them, so it
|
|
3840
|
+
// declines and the router uses the errors generator.
|
|
3841
|
+
if (cyclicDefNames(schema.$defs || schema.definitions).size) return null
|
|
3650
3842
|
// Bail on additionalProperties as a schema — combined path doesn't emit the
|
|
3651
3843
|
// per-key sub-schema loop yet. The jsFn + errFn fallback handles it correctly.
|
|
3652
3844
|
if (hasAdditionalPropertiesSchema(schema)) return null
|
|
@@ -3665,19 +3857,16 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
|
|
|
3665
3857
|
if (!sharedCodegenGate(schema, schemaMap)) return null
|
|
3666
3858
|
if (schema.patternProperties) {
|
|
3667
3859
|
for (const [pat, sub] of Object.entries(schema.patternProperties)) {
|
|
3668
|
-
if (typeof sub === 'boolean') return null
|
|
3669
3860
|
if (/\\[pP]\{/.test(pat)) return null
|
|
3670
3861
|
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
3671
3862
|
}
|
|
3672
3863
|
}
|
|
3673
3864
|
if (schema.dependentSchemas) {
|
|
3674
3865
|
for (const sub of Object.values(schema.dependentSchemas)) {
|
|
3675
|
-
if (typeof sub === 'boolean') return null
|
|
3676
3866
|
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
3677
3867
|
}
|
|
3678
3868
|
}
|
|
3679
3869
|
if (schema.propertyNames) {
|
|
3680
|
-
if (typeof schema.propertyNames === 'boolean') return null
|
|
3681
3870
|
const pn = schema.propertyNames
|
|
3682
3871
|
const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
|
|
3683
3872
|
const keys = Object.keys(pn).filter(k => k !== '$schema')
|
|
@@ -3747,6 +3936,13 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
|
|
|
3747
3936
|
// If type check fails → push error, skip property checks (they'd crash).
|
|
3748
3937
|
function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
3749
3938
|
if (!schemaPrefix) schemaPrefix = '#'
|
|
3939
|
+
if (schema === false) {
|
|
3940
|
+
// Leading semicolon: the previous generated line may end in an
|
|
3941
|
+
// expression, and a line starting with `(` would be parsed as a call.
|
|
3942
|
+
lines.push(`;(_e||(_e=[])).push({keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}',params:{},message:'boolean schema is false'})`)
|
|
3943
|
+
return
|
|
3944
|
+
}
|
|
3945
|
+
if (schema === true) return
|
|
3750
3946
|
if (typeof schema !== 'object' || schema === null) return
|
|
3751
3947
|
|
|
3752
3948
|
// $ref — resolve local, anchor, and cross-schema refs
|
|
@@ -4181,6 +4377,12 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4181
4377
|
}
|
|
4182
4378
|
}
|
|
4183
4379
|
|
|
4380
|
+
if (schema.propertyNames === false) {
|
|
4381
|
+
const ki = ctx.varCounter++
|
|
4382
|
+
const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
|
|
4383
|
+
lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){(_e||(_e=[])).push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames',params:{},message:'boolean schema is false'})}}`)
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4184
4386
|
// propertyNames — skip if already merged into patternProperties loop
|
|
4185
4387
|
if (schema.propertyNames && typeof schema.propertyNames === 'object' && !ctx._ppHandledPropertyNamesC) {
|
|
4186
4388
|
const pn = schema.propertyNames
|
|
@@ -4211,7 +4413,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4211
4413
|
}
|
|
4212
4414
|
|
|
4213
4415
|
// items
|
|
4214
|
-
if (schema.items) {
|
|
4416
|
+
if (schema.items !== undefined && schema.items !== true) {
|
|
4215
4417
|
const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
|
|
4216
4418
|
const idx = `_j${ctx.varCounter}`, elem = `_ei${ctx.varCounter}`
|
|
4217
4419
|
ctx.varCounter++
|
|
@@ -4232,7 +4434,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4232
4434
|
}
|
|
4233
4435
|
|
|
4234
4436
|
// contains
|
|
4235
|
-
if (schema.contains) {
|
|
4437
|
+
if (schema.contains !== undefined) {
|
|
4236
4438
|
const ci = ctx.varCounter++
|
|
4237
4439
|
const subLines = []
|
|
4238
4440
|
genCode(schema.contains, `_cv`, subLines, ctx)
|
|
@@ -4263,7 +4465,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4263
4465
|
}
|
|
4264
4466
|
|
|
4265
4467
|
// not
|
|
4266
|
-
if (schema.not) {
|
|
4468
|
+
if (schema.not !== undefined) {
|
|
4267
4469
|
const sl = []; genCode(schema.not, '_nv', sl, ctx)
|
|
4268
4470
|
const nfn = sl.length === 0 ? `function(_nv){return true}` : `function(_nv){${sl.join(';')};return true}`
|
|
4269
4471
|
const fi = ctx.varCounter++
|
|
@@ -4271,13 +4473,13 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4271
4473
|
}
|
|
4272
4474
|
|
|
4273
4475
|
// if/then/else
|
|
4274
|
-
if (schema.if) {
|
|
4476
|
+
if (schema.if !== undefined) {
|
|
4275
4477
|
const sl = []; genCode(schema.if, '_iv', sl, ctx)
|
|
4276
4478
|
const fi = ctx.varCounter++
|
|
4277
4479
|
const ifFn = sl.length === 0 ? `function(_iv){return true}` : `function(_iv){${sl.join(';')};return true}`
|
|
4278
4480
|
lines.push(`{const _if${fi}=${ifFn}`)
|
|
4279
|
-
if (schema.then) { lines.push(`if(_if${fi}(${v})){`); genCodeC(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then'); lines.push(`}`) }
|
|
4280
|
-
if (schema.else) { lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`); genCodeC(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else'); lines.push(`}`) }
|
|
4481
|
+
if (schema.then !== undefined) { lines.push(`if(_if${fi}(${v})){`); genCodeC(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then'); lines.push(`}`) }
|
|
4482
|
+
if (schema.else !== undefined) { lines.push(`${schema.then !== undefined ? 'else' : `if(!_if${fi}(${v}))`}{`); genCodeC(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else'); lines.push(`}`) }
|
|
4281
4483
|
lines.push(`}`)
|
|
4282
4484
|
}
|
|
4283
4485
|
|
|
@@ -4362,7 +4564,7 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
|
|
|
4362
4564
|
|
|
4363
4565
|
// contains: marks matching items as evaluated (not ALL items)
|
|
4364
4566
|
// Always set dynamic since which items match depends on the data
|
|
4365
|
-
if (schema.contains) {
|
|
4567
|
+
if (schema.contains !== undefined) {
|
|
4366
4568
|
result.dynamic = true
|
|
4367
4569
|
}
|
|
4368
4570
|
|