ata-validator 1.19.0 → 1.21.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.
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ const { collapseBranches } = require('./branch-collapse');
2
3
 
3
4
  // Interpreted draft 2020-12 engine. This is the correctness fallback for
4
5
  // schemas the JS codegen cannot represent when the native engine is absent
@@ -866,7 +867,7 @@ class Interpreter {
866
867
  valid = false;
867
868
  } else {
868
869
  const sub = collect ? { props: null, items: null } : DISCARD;
869
- if (!this.eval(child, data, refBase, dynScope, errors, instancePath, schemaPath + '/$ref', stack, sub)) valid = false;
870
+ if (!this.eval(child, data, refBase, dynScope, errors, instancePath, schemaPath, stack, sub)) valid = false;
870
871
  else if (collect) mergeAnnotations(local, sub);
871
872
  }
872
873
  }
@@ -905,7 +906,7 @@ class Interpreter {
905
906
  valid = false;
906
907
  } else {
907
908
  const sub = collect ? { props: null, items: null } : DISCARD;
908
- if (!this.eval(this.node(node), data, refBase, dynScope, errors, instancePath, schemaPath + '/$dynamicRef', stack, sub)) valid = false;
909
+ if (!this.eval(this.node(node), data, refBase, dynScope, errors, instancePath, schemaPath, stack, sub)) valid = false;
909
910
  else if (collect) mergeAnnotations(local, sub);
910
911
  }
911
912
  }
@@ -985,7 +986,7 @@ class Interpreter {
985
986
  }
986
987
  const minC = P.minContains !== undefined ? P.minContains : 1;
987
988
  if (matched.length < minC) { errors === NOERRORS || errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC }, `must contain at least ${minC} valid item(s)`)); valid = false; }
988
- if (P.maxContains !== undefined && matched.length > P.maxContains) { errors === NOERRORS || errors.push(err('maxContains', 'maxContains', instancePath, schemaPath + '/maxContains', { limit: P.maxContains }, `must NOT contain more than ${P.maxContains} valid item(s)`)); valid = false; }
989
+ if (P.maxContains !== undefined && matched.length > P.maxContains) { errors === NOERRORS || errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC, maxContains: P.maxContains }, `must NOT contain more than ${P.maxContains} valid item(s)`)); valid = false; }
989
990
  if (collect && matched.length) {
990
991
  if (!local.items) local.items = new Set();
991
992
  for (const i of matched) local.items.add(i);
@@ -1016,7 +1017,7 @@ class Interpreter {
1016
1017
  }
1017
1018
  if (P.propertyNames !== undefined) {
1018
1019
  for (const key of keys) {
1019
- if (!this.eval(P.propertyNames, key, base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/propertyNames', stack, DISCARD)) valid = false;
1020
+ if (!this.eval(P.propertyNames, key, base, dynScope, errors, instancePath, schemaPath + '/propertyNames', stack, DISCARD)) valid = false;
1020
1021
  }
1021
1022
  }
1022
1023
  const props = P.properties;
@@ -1039,7 +1040,13 @@ class Interpreter {
1039
1040
  }
1040
1041
  }
1041
1042
  if (!evaluated && P.additionalProperties !== undefined) {
1042
- if (!this.eval(P.additionalProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack, DISCARD)) valid = false;
1043
+ // `additionalProperties: false` keeps its own keyword and names the
1044
+ // property it turned away. Running it as a plain false subschema
1045
+ // reported `not` at the key's path, which drops both.
1046
+ if (P.additionalProperties === false) {
1047
+ valid = false;
1048
+ if (errors !== NOERRORS) errors.push(err('additionalProperties', 'additionalProperties', instancePath, schemaPath + '/additionalProperties', { additionalProperty: key }, 'must NOT have additional properties'));
1049
+ } else if (!this.eval(P.additionalProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack, DISCARD)) valid = false;
1043
1050
  evaluated = true;
1044
1051
  }
1045
1052
  if (evaluated && collect) {
@@ -1098,16 +1105,24 @@ class Interpreter {
1098
1105
  if (P.anyOf !== null) {
1099
1106
  let any = false;
1100
1107
  const scratch = errors === NOERRORS ? NOERRORS : [];
1108
+ // Per-branch errors, taken as a slice of the shared scratch rather than
1109
+ // an array each, so a branch that passes costs nothing extra.
1110
+ const branches = errors === NOERRORS ? null : [];
1101
1111
  for (let i = 0; i < P.anyOf.length; i++) {
1102
1112
  const sub = collect ? { props: null, items: null } : DISCARD;
1103
- if (this.eval(P.anyOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, sub)) {
1113
+ const mark = branches === null ? 0 : scratch.length;
1114
+ const ok = this.eval(P.anyOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, sub);
1115
+ if (branches !== null) branches.push({ valid: ok, errors: scratch.slice(mark), title: branchTitle(P.anyOf[i]) });
1116
+ if (ok) {
1104
1117
  any = true;
1105
1118
  if (collect) mergeAnnotations(local, sub);
1106
1119
  }
1107
1120
  }
1108
1121
  if (!any) {
1109
- if (errors !== NOERRORS) for (const e of scratch) errors.push(e);
1110
- if (errors !== NOERRORS) errors.push(err('anyOf', 'anyOf', instancePath, schemaPath + '/anyOf', {}, 'must match a schema in anyOf'));
1122
+ if (branches !== null) {
1123
+ const collapsed = collapseBranches({ keyword: 'anyOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/anyOf' });
1124
+ if (collapsed !== null) errors.push(collapsed);
1125
+ }
1111
1126
  valid = false;
1112
1127
  }
1113
1128
  }
@@ -1115,17 +1130,25 @@ class Interpreter {
1115
1130
  let count = 0;
1116
1131
  let winner = null;
1117
1132
  const scratch = errors === NOERRORS ? NOERRORS : [];
1133
+ // Per-branch results, so a failing oneOf collapses to the branch that
1134
+ // came closest instead of every branch's complaints at once.
1135
+ const branches = errors === NOERRORS ? null : [];
1118
1136
  for (let i = 0; i < P.oneOf.length; i++) {
1119
1137
  const sub = collect ? { props: null, items: null } : DISCARD;
1120
- if (this.eval(P.oneOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, sub)) {
1138
+ const mark = branches === null ? 0 : scratch.length;
1139
+ const ok = this.eval(P.oneOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, sub);
1140
+ if (branches !== null) branches.push({ valid: ok, errors: scratch.slice(mark), title: branchTitle(P.oneOf[i]) });
1141
+ if (ok) {
1121
1142
  count++;
1122
1143
  winner = sub;
1123
1144
  }
1124
1145
  }
1125
1146
  if (count === 1) { if (collect) mergeAnnotations(local, winner); }
1126
1147
  else {
1127
- if (count === 0) for (const e of scratch) errors.push(e);
1128
- if (errors !== NOERRORS) errors.push(err('oneOf', 'oneOf', instancePath, schemaPath + '/oneOf', { passingSchemas: count }, 'must match exactly one schema in oneOf'));
1148
+ if (branches !== null) {
1149
+ const collapsed = collapseBranches({ keyword: 'oneOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/oneOf' });
1150
+ if (collapsed !== null) errors.push(collapsed);
1151
+ }
1129
1152
  valid = false;
1130
1153
  }
1131
1154
  }
@@ -1182,6 +1205,12 @@ class Interpreter {
1182
1205
  }
1183
1206
  }
1184
1207
 
1208
+ // One branch title, for the collapsed anyOf/oneOf error. The compiled node
1209
+ // keeps the schema it came from, which is where a `title` lives.
1210
+ function branchTitle (node) {
1211
+ return node && node.schema && typeof node.schema.title === 'string' ? node.schema.title : '';
1212
+ }
1213
+
1185
1214
  function escapePointer(s) {
1186
1215
  if (typeof s !== 'string') s = String(s);
1187
1216
  // Keys almost never contain `~` or `/`; skip the two regex passes when so.
@@ -25,6 +25,12 @@ const UQ_HELPERS = "function _cn(x){if(x===null||typeof x!=='object')return type
25
25
  function hoistOnce(ctx, key, code) {
26
26
  if (ctx[key]) return
27
27
  ctx[key] = true
28
+ // Recorded as shared: the text is a constant and the name it declares is
29
+ // fixed, so several compiled schemas in one bundle can use one copy. The
30
+ // rest of the preamble cannot move, because emitConstant and the generated
31
+ // branch checks name things per compilation (_kv0, _av3) and two schemas
32
+ // would collide.
33
+ if (ctx.shared) ctx.shared.push(code)
28
34
  if (ctx.preamble) ctx.preamble.push(code)
29
35
  else if (ctx.helperCode) ctx.helperCode.push(code)
30
36
  }
@@ -34,6 +40,18 @@ function emitDeq(ctx) {
34
40
  return '_deq'
35
41
  }
36
42
 
43
+ // RFC 6901: a property key reaching an instancePath has to have `~` and `/`
44
+ // escaped, or the pointer splits into the wrong segments. Static keys are
45
+ // escaped at compile time with ptrSeg; a key only known at run time needs
46
+ // this. It mirrors interpreter.js escapePointer, including skipping the two
47
+ // regex passes for the keys that need nothing, which is almost all of them.
48
+ const PTR_ESC_HELPER = "function _pe(s){for(let i=0;i<s.length;i++){const c=s.charCodeAt(i);if(c===126||c===47)return s.replace(/~/g,'~0').replace(/\\//g,'~1')}return s}"
49
+
50
+ function emitPtrEsc(ctx) {
51
+ hoistOnce(ctx, '_peHoisted', PTR_ESC_HELPER)
52
+ return '_pe'
53
+ }
54
+
37
55
  // `_uq` compares items with `_deq`, so it can never be hoisted alone.
38
56
  function emitUq(ctx) {
39
57
  emitDeq(ctx)
@@ -1474,7 +1492,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
1474
1492
  if (hasUnresolvableRef(schema, rootDefs, anchors, schemaMap, new Set())) return null
1475
1493
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
1476
1494
 
1477
- const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema, userFormats: userFormats || null }
1495
+ const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema, userFormats: userFormats || null }
1478
1496
  const lines = []
1479
1497
  genCode(schema, 'd', lines, ctx)
1480
1498
 
@@ -1556,6 +1574,9 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
1556
1574
  const helperStr = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
1557
1575
  boolFn._source = helperStr + body
1558
1576
  boolFn._preambleSource = preambleStr
1577
+ boolFn._preambleGuard = guardStr
1578
+ boolFn._preambleParts = ctx.preamble ? ctx.preamble.slice() : []
1579
+ boolFn._sharedHelpers = ctx.shared ? ctx.shared.slice() : []
1559
1580
  boolFn._hybridSource = helperStr + hybridBody
1560
1581
  boolFn._usesSafeRe = !!ctx.usesSafeRe
1561
1582
  // Custom-format closure entries that the bundle output needs to recreate.
@@ -3244,7 +3265,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
3244
3265
  if (hasUnresolvableRef(schema, eRootDefs, eAnchors, schemaMap, new Set())) return null
3245
3266
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
3246
3267
 
3247
- const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootSchema: schema, userFormats: userFormats || null,
3268
+ const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, shared: [], refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootSchema: schema, userFormats: userFormats || null,
3248
3269
  // Custom format checkers referenced by the body, bound as
3249
3270
  // closure parameters below the way the other entry points do.
3250
3271
  closureVars: [], closureVals: [],
@@ -3264,7 +3285,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
3264
3285
  // AOT output small for schemas that don't compose. Standalone modules can't
3265
3286
  // require('./branch-collapse'), so the helper body is embedded verbatim.
3266
3287
  if (ctx.usesBranchCollapse) {
3267
- ctx.helperCode.push("const __ATA_SEVERITY={type:10,const:8,enum:8,required:5,format:3,minLength:3,maxLength:3,minimum:3,maximum:3,pattern:3,additionalProperties:2,unevaluatedProperties:2,unevaluatedItems:2};function __ataScore(errs){if(!errs||!errs.length)return 0;let s=0;for(const e of errs)s+=__ATA_SEVERITY[e.keyword]||4;return errs.length*100+s}function __ataCollapse(kw,br,pp,sp,o){const pass=br.filter(b=>b.valid);if(kw==='oneOf'){if(pass.length===1)return null;if(pass.length>1)return{code:'ATA4002',keyword:'oneOf',instancePath:pp||'',path:pp||'',schemaPath:sp,_o:o,message:'value matched '+pass.length+' of '+br.length+' oneOf variants, expected exactly one',params:{matched:pass.length,total:br.length}}}else{if(pass.length>=1)return null}let bi=0,bs=Infinity;for(let i=0;i<br.length;i++){const s=__ataScore(br[i].errors);if(s<bs){bs=s;bi=i}}const code=kw==='oneOf'?'ATA4001':'ATA4003';const best=br[bi];return{code,keyword:kw,instancePath:pp||'',path:pp||'',schemaPath:sp,_o:o,message:'value matched 0 of '+br.length+' '+kw+' variants',params:{variants:br.length,closest:bi,closestName:best.title||('variant '+(bi+1))},branchErrors:best.errors}}")
3288
+ ctx.helperCode.push(require('./branch-collapse').embedSource())
3268
3289
  }
3269
3290
  if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
3270
3291
 
@@ -3314,6 +3335,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
3314
3335
  fn = new Function('d', '_all', body)
3315
3336
  }
3316
3337
  fn._errSource = body
3338
+ fn._sharedHelpers = ctx.shared ? ctx.shared.slice() : []
3317
3339
  fn._usesSafeRe = !!ctx.usesSafeRe
3318
3340
  if (cvars.length) {
3319
3341
  fn._formatClosures = cvars.map((name, i) => {
@@ -3490,10 +3512,11 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3490
3512
  }
3491
3513
  })
3492
3514
  const expected = types.join(', ')
3515
+ const expectedParam = types.length === 1 ? `'${types[0]}'` : JSON.stringify(types)
3493
3516
  {
3494
3517
  const typeSp = `${schemaPrefix}/type`
3495
3518
  const lit = buildErrorLiteral({ keyword: 'type', schemaPath: typeSp, sourceMap: ctx.sourceMap })
3496
- lines.push(`if(!(${conds.join('||')})){_e.push({code:'${lit.codeStr}',keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${typeSp}'${ordinalField(ctx, `${typeSp}`)},params:{type:'${expected}'},message:'must be ${expected}',docUrl:'${lit.docUrl}'${lit.frame}});if(!_all)return{valid:false,errors:_e}}`)
3519
+ lines.push(`if(!(${conds.join('||')})){_e.push({code:'${lit.codeStr}',keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${typeSp}'${ordinalField(ctx, `${typeSp}`)},params:{type:${expectedParam}},message:'must be ${expected}',docUrl:'${lit.docUrl}'${lit.frame}});if(!_all)return{valid:false,errors:_e}}`)
3497
3520
  }
3498
3521
  }
3499
3522
 
@@ -3703,9 +3726,9 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3703
3726
  // properties — always guard (error mode, data may not be an object or may be array)
3704
3727
  if (schema.properties) {
3705
3728
  for (const [key, prop] of Object.entries(schema.properties)) {
3706
- const childPath = childPathExpr(pathExpr, esc(key))
3729
+ const childPath = childPathExpr(pathExpr, ptrSeg(key))
3707
3730
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
3708
- genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
3731
+ genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
3709
3732
  lines.push(`}`)
3710
3733
  }
3711
3734
  }
@@ -3727,7 +3750,8 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3727
3750
  const ri = ctx.regExpMap.get(pattern);
3728
3751
  const ki = ctx.varCounter++
3729
3752
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){if(_re${ri}.test(_k${ki})){`)
3730
- const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
3753
+ const _peFn = emitPtrEsc(ctx)
3754
+ const p = pathExpr ? `${pathExpr}+'/'+${_peFn}(_k${ki})` : `'/'+${_peFn}(_k${ki})`
3731
3755
  // The real pointer, so source maps can locate the failing keyword.
3732
3756
  genCodeE(sub, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/patternProperties/' + pat.replace(/~/g, '~0').replace(/\//g, '~1'))
3733
3757
  lines.push(`}}}`)
@@ -3763,7 +3787,8 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3763
3787
  if (patTests.length) conds.push(`!(${patTests.join('||')})`)
3764
3788
  const keep = conds.length ? `if(${conds.join('&&')}){` : `{`
3765
3789
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${guard}for(const _k${ki} in ${v}){${keep}`)
3766
- const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
3790
+ const _peFn = emitPtrEsc(ctx)
3791
+ const p = pathExpr ? `${pathExpr}+'/'+${_peFn}(_k${ki})` : `'/'+${_peFn}(_k${ki})`
3767
3792
  genCodeE(schema.additionalProperties, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/additionalProperties')
3768
3793
  lines.push(`}}}`)
3769
3794
  }
@@ -3772,7 +3797,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3772
3797
  if (schema.dependentSchemas) {
3773
3798
  for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
3774
3799
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
3775
- genCodeE(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+key)
3800
+ genCodeE(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
3776
3801
  lines.push(`}`)
3777
3802
  }
3778
3803
  }
@@ -3781,7 +3806,10 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3781
3806
  // interpreter does (plan-compiler: instancePath + '/' + key).
3782
3807
  if (schema.propertyNames === false) {
3783
3808
  const ki = ctx.varCounter++
3784
- const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
3809
+ // The subschema is applied to the property NAME, so the failure belongs to
3810
+ // the object that carries the name, not to `/<key>`, which is where the
3811
+ // value lives. Same placement as the object-valued propertyNames branch.
3812
+ const p = pathExpr || '""'
3785
3813
  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'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'});if(!_all)return{valid:false,errors:_e}}}`)
3786
3814
  }
3787
3815
 
@@ -3853,9 +3881,9 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3853
3881
  const minC = schema.minContains !== undefined ? schema.minContains : 1
3854
3882
  const maxC = schema.maxContains
3855
3883
  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}++}`)
3856
- lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{limit:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
3884
+ lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{minContains:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
3857
3885
  if (maxC !== undefined) {
3858
- lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{limit:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
3886
+ lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{minContains:${minC},maxContains:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
3859
3887
  }
3860
3888
  lines.push(`}`)
3861
3889
  }
@@ -3991,7 +4019,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
3991
4019
  if (hasUnresolvableRef(schema, cRootDefs, cAnchors, schemaMap, new Set())) return null
3992
4020
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
3993
4021
 
3994
- const ctx = { varCounter: 0, helperCode: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
4022
+ const ctx = { varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
3995
4023
  rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: schema, userFormats: userFormats || null }
3996
4024
  const lines = []
3997
4025
  genCodeC(schema, 'd', '', lines, ctx, '#')
@@ -4137,10 +4165,11 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4137
4165
  }
4138
4166
  })
4139
4167
  const expected = types.join(', ')
4168
+ const expectedParam = types.length === 1 ? `'${types[0]}'` : JSON.stringify(types)
4140
4169
  // Type check: push error but continue — wrap remaining in type-success block
4141
4170
  const typeOk = `_tok${ctx.varCounter++}`
4142
4171
  lines.push(`const ${typeOk}=${conds.join('||')}`)
4143
- lines.push(`if(!${typeOk}){${fail('type', 'type', `{type:'${expected}'}`, `'must be ${expected}'`)}}`)
4172
+ lines.push(`if(!${typeOk}){${fail('type', 'type', `{type:${expectedParam}}`, `'must be ${expected}'`)}}`)
4144
4173
  // Subsequent optimized code runs inside if(typeOk){...}
4145
4174
  if (types.length === 1) {
4146
4175
  isObj = types[0] === 'object'
@@ -4335,19 +4364,19 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4335
4364
  if (schema.properties) {
4336
4365
  for (const [key, prop] of Object.entries(schema.properties)) {
4337
4366
  const pv = hoisted[key] || `${v}[${JSON.stringify(key)}]`
4338
- const childPath = childPathExpr(pathExpr, esc(key))
4367
+ const childPath = childPathExpr(pathExpr, ptrSeg(key))
4339
4368
  if (requiredSet.has(key) && isObj) {
4340
4369
  lines.push(`if(${pv}!==undefined){`)
4341
- genCodeC(prop, pv, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
4370
+ genCodeC(prop, pv, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
4342
4371
  lines.push(`}`)
4343
4372
  } else if (isObj) {
4344
4373
  const oi = ctx.varCounter++
4345
4374
  lines.push(`{const _o${oi}=${v}[${JSON.stringify(key)}];if(_o${oi}!==undefined){`)
4346
- genCodeC(prop, `_o${oi}`, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
4375
+ genCodeC(prop, `_o${oi}`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
4347
4376
  lines.push(`}}`)
4348
4377
  } else {
4349
4378
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
4350
- genCodeC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
4379
+ genCodeC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
4351
4380
  lines.push(`}`)
4352
4381
  }
4353
4382
  }
@@ -4420,7 +4449,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4420
4449
  // The subschema is generated in place so its own keywords report
4421
4450
  // the real instance path and schema pointer.
4422
4451
  lines.push(`if(${matchers[i].check}){_m${pi}=true;{const _ppv${pi}_${i}=${v}[${kVar}]`)
4423
- genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, kVar), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
4452
+ genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, `${emitPtrEsc(ctx)}(${kVar})`), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
4424
4453
  lines.push(`}}`)
4425
4454
  }
4426
4455
  lines.push(`if(!_m${pi}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:${kVar}}`, "'must NOT have additional properties'")}}`)
@@ -4452,7 +4481,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4452
4481
  }
4453
4482
  for (let i = 0; i < ppEntries.length; i++) {
4454
4483
  lines.push(`if(${matchers[i].check}){const _ppv${pi}_${i}=${v}[${kVar}]`)
4455
- genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, kVar), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
4484
+ genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, `${emitPtrEsc(ctx)}(${kVar})`), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
4456
4485
  lines.push(`}`)
4457
4486
  }
4458
4487
  lines.push(`}}`)
@@ -4463,14 +4492,14 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4463
4492
  if (schema.dependentSchemas) {
4464
4493
  for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
4465
4494
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
4466
- genCodeC(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+key)
4495
+ genCodeC(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
4467
4496
  lines.push(`}`)
4468
4497
  }
4469
4498
  }
4470
4499
 
4471
4500
  if (schema.propertyNames === false) {
4472
4501
  const ki = ctx.varCounter++
4473
- const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
4502
+ const p = pathExpr || '""'
4474
4503
  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'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'})}}`)
4475
4504
  }
4476
4505
 
@@ -4533,8 +4562,8 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4533
4562
  const minC = schema.minContains !== undefined ? schema.minContains : 1
4534
4563
  const maxC = schema.maxContains
4535
4564
  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}++}`)
4536
- lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{limit:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
4537
- if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{limit:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
4565
+ lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{minContains:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
4566
+ if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{minContains:${minC},maxContains:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
4538
4567
  lines.push(`}`)
4539
4568
  }
4540
4569
 
@@ -0,0 +1,140 @@
1
+ 'use strict';
2
+
3
+ // The standardised output format from draft 2019-09 and 2020-12.
4
+ //
5
+ // ata reports errors in its own shape, the one consumers and the compat entry
6
+ // expect. The specification also defines an output format of its own, with
7
+ // `keywordLocation`, `absoluteKeywordLocation` and `instanceLocation`, and the
8
+ // official test suite has a small set of tests for it under `output-tests/`.
9
+ // This produces that shape from a validation, so those tests can be run and so
10
+ // a caller that wants spec output can have it.
11
+ //
12
+ // `flag` and `basic` are implemented. `detailed` and `verbose` are not: they
13
+ // need the full applicator tree, including the subschemas that passed, which
14
+ // the engines do not keep. The v1 `list` format is not implemented either; it
15
+ // is still moving, and the suite's own v1 output schema does not yet define it.
16
+ //
17
+ // Annotations are collected only when validation succeeded. That is not a
18
+ // shortcut: a subschema that fails contributes no annotations, and the output
19
+ // is rooted at the root schema, so a failing root drops all of them.
20
+
21
+ // Keywords whose value is the annotation they produce.
22
+ const METADATA_KEYWORDS = [
23
+ 'title', 'description', 'default', 'deprecated', 'readOnly', 'writeOnly',
24
+ 'examples', 'format', 'contentEncoding', 'contentMediaType', 'contentSchema',
25
+ ];
26
+
27
+ function pointerSegment (s) {
28
+ return String(s).replace(/~/g, '~0').replace(/\//g, '~1');
29
+ }
30
+
31
+ // The root $id, which absoluteKeywordLocation is resolved against.
32
+ function baseUri (schema) {
33
+ if (schema && typeof schema === 'object' && typeof schema.$id === 'string') {
34
+ return schema.$id.split('#')[0];
35
+ }
36
+ return null;
37
+ }
38
+
39
+ function unitFor (error, base) {
40
+ const keywordLocation = typeof error.schemaPath === 'string' && error.schemaPath.charAt(0) === '#'
41
+ ? error.schemaPath.slice(1)
42
+ : (error.schemaPath || '');
43
+ const unit = {
44
+ valid: false,
45
+ keywordLocation,
46
+ instanceLocation: error.instancePath || '',
47
+ };
48
+ if (base) unit.absoluteKeywordLocation = base + '#' + keywordLocation;
49
+ if (typeof error.message === 'string') unit.error = error.message;
50
+ return unit;
51
+ }
52
+
53
+ // Walk the schema beside the instance, collecting metadata annotations from
54
+ // the subschemas that applied. Only reached when the whole validation passed,
55
+ // so every applicator followed here is one that succeeded, except the
56
+ // alternatives of anyOf/oneOf/if, which are checked before being followed.
57
+ function collectAnnotations (schema, data, keywordLocation, instanceLocation, base, out, isValid, depth) {
58
+ if (schema === true || schema === false || schema === null || typeof schema !== 'object' || depth > 64) return;
59
+
60
+ for (const kw of METADATA_KEYWORDS) {
61
+ if (Object.prototype.hasOwnProperty.call(schema, kw)) {
62
+ const unit = {
63
+ valid: true,
64
+ keywordLocation: keywordLocation + '/' + kw,
65
+ instanceLocation,
66
+ annotation: schema[kw],
67
+ };
68
+ if (base) unit.absoluteKeywordLocation = base + '#' + keywordLocation + '/' + kw;
69
+ out.push(unit);
70
+ }
71
+ }
72
+
73
+ const down = (sub, kwSeg, value, instSeg) => collectAnnotations(
74
+ sub, value, keywordLocation + kwSeg, instanceLocation + (instSeg || ''), base, out, isValid, depth + 1,
75
+ );
76
+
77
+ if (schema.properties && data !== null && typeof data === 'object' && !Array.isArray(data)) {
78
+ for (const key of Object.keys(schema.properties)) {
79
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
80
+ down(schema.properties[key], '/properties/' + pointerSegment(key), data[key], '/' + pointerSegment(key));
81
+ }
82
+ }
83
+ }
84
+ if (Array.isArray(schema.prefixItems) && Array.isArray(data)) {
85
+ for (let i = 0; i < schema.prefixItems.length && i < data.length; i++) {
86
+ down(schema.prefixItems[i], '/prefixItems/' + i, data[i], '/' + i);
87
+ }
88
+ }
89
+ if (schema.items && typeof schema.items === 'object' && Array.isArray(data)) {
90
+ const start = Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0;
91
+ for (let i = start; i < data.length; i++) down(schema.items, '/items', data[i], '/' + i);
92
+ }
93
+ if (Array.isArray(schema.allOf)) {
94
+ for (let i = 0; i < schema.allOf.length; i++) down(schema.allOf[i], '/allOf/' + i, data, '');
95
+ }
96
+ for (const kw of ['anyOf', 'oneOf']) {
97
+ if (Array.isArray(schema[kw])) {
98
+ for (let i = 0; i < schema[kw].length; i++) {
99
+ if (isValid(schema[kw][i], data)) down(schema[kw][i], '/' + kw + '/' + i, data, '');
100
+ }
101
+ }
102
+ }
103
+ if (schema.if !== undefined) {
104
+ const taken = isValid(schema.if, data);
105
+ if (taken && schema.then !== undefined) down(schema.then, '/then', data, '');
106
+ if (!taken && schema.else !== undefined) down(schema.else, '/else', data, '');
107
+ }
108
+ }
109
+
110
+ // Produce spec output for `data` under `validator`.
111
+ // format: 'flag' | 'basic' (default 'basic')
112
+ function toOutput (validator, data, opts) {
113
+ const format = (opts && opts.format) || 'basic';
114
+ const result = validator.validate(data);
115
+ if (format === 'flag') return { valid: result.valid };
116
+ if (format !== 'basic') {
117
+ throw new Error(`toOutput: unsupported format "${format}". Supported: "flag", "basic".`);
118
+ }
119
+
120
+ const schema = validator._schemaObj !== undefined ? validator._schemaObj : validator._rawSchema;
121
+ const base = baseUri(schema);
122
+ const out = { valid: result.valid };
123
+
124
+ if (!result.valid) {
125
+ const errors = [];
126
+ for (const e of result.errors) errors.push(unitFor(e, base));
127
+ out.errors = errors;
128
+ return out;
129
+ }
130
+
131
+ const annotations = [];
132
+ const isValid = (sub, value) => {
133
+ try { return new validator.constructor(sub).isValidObject(value); } catch { return false; }
134
+ };
135
+ collectAnnotations(schema, data, '', '', base, annotations, isValid, 0);
136
+ if (annotations.length) out.annotations = annotations;
137
+ return out;
138
+ }
139
+
140
+ module.exports = { toOutput, METADATA_KEYWORDS };