ata-validator 1.52.0 → 1.53.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/compat.js CHANGED
@@ -22,11 +22,26 @@ const { resolvePointer } = require('./lib/pointer.js');
22
22
 
23
23
  const { Validator } = require('./index');
24
24
  const { METASCHEMAS } = require('./lib/metaschemas');
25
+ const { normalizeExclusiveBounds } = require('./lib/draft7');
25
26
  const { createShaper, asReferenceKeyword } = require('./lib/compat-errors');
26
27
 
27
28
  const DRAFT7 = 'http://json-schema.org/draft-07/schema#';
28
29
  const DRAFT2020 = 'https://json-schema.org/draft/2020-12/schema';
29
30
 
31
+ // draft-04, which OpenAPI 3.0 builds on, is read as draft-07, the nearest
32
+ // dialect ata implements: its boolean exclusiveMinimum and exclusiveMaximum
33
+ // are normalized to the numeric form, and its `id` names the schema as `$id`
34
+ // does. The reference ships a separate class for it (ajv-draft-04).
35
+ const DRAFT4_IDS = new Set(['http://json-schema.org/draft-04/schema#', 'http://json-schema.org/draft-04/schema', 'https://json-schema.org/draft-04/schema#', 'https://json-schema.org/draft-04/schema']);
36
+ function isDraft4(schema) {
37
+ return schema !== null && typeof schema === 'object' && typeof schema.$schema === 'string' && DRAFT4_IDS.has(schema.$schema);
38
+ }
39
+ function draft4AsDraft7(schema) {
40
+ const out = { ...schema, $schema: DRAFT7 };
41
+ if (typeof schema.id === 'string' && schema.$id === undefined) { out.$id = schema.id; delete out.id; }
42
+ return out;
43
+ }
44
+
30
45
  function isDraft7Id(id) {
31
46
  return id === DRAFT7 || id === 'http://json-schema.org/draft-07/schema';
32
47
  }
@@ -58,17 +73,12 @@ class Ata {
58
73
  throw new Error('ata compat: $data references are not supported');
59
74
  }
60
75
  this.opts = { ...opts };
61
- // The formats plugin of the reference assigns into `opts.code.formats`
62
- // before registering anything; the formats it carries are built in here,
63
- // so say so at that exact point instead of failing further down.
64
- const code = { ...(opts.code || {}) };
65
- Object.defineProperty(code, 'formats', {
66
- get() { return undefined; },
67
- set() {
68
- throw new Error('ata compat: the formats plugin is not needed, formats such as email, uri, date-time and uuid are built in; remove addFormats()');
69
- },
70
- });
71
- this.opts.code = code;
76
+ // The formats plugin of the reference assigns into `opts.code.formats` and
77
+ // then registers each of its formats through addFormat, which is what it
78
+ // does here too. It is not needed (the formats are built in), but a
79
+ // library that calls it inside its own setup, as express-openapi-validator
80
+ // does, could not take this class at all while the call was refused.
81
+ this.opts.code = { ...(opts.code || {}) };
72
82
 
73
83
  this.errors = null;
74
84
  this._schemas = new Map(); // key or $id -> schema object
@@ -97,9 +107,13 @@ class Ata {
97
107
  const o = this.opts;
98
108
  const out = {
99
109
  useDefaults: !!o.useDefaults,
100
- coerceTypes: !!o.coerceTypes,
110
+ coerceTypes: o.coerceTypes === 'array' ? 'array' : !!o.coerceTypes,
101
111
  removeAdditional: !!o.removeAdditional,
102
112
  verbose: !!o.verbose,
113
+ // The errors are reshaped into the reference's own; the fields only
114
+ // rich errors carry (code, received, suggestion, docUrl) are dropped on
115
+ // the way, so they are not built at all.
116
+ richErrors: false,
103
117
  };
104
118
  // `strict` and `strictSchema`: the unknown-keyword and dangling-local-$ref
105
119
  // checks are enforced; `strictTypes`, `strictTuples` and `strictRequired`
@@ -112,11 +126,9 @@ class Ata {
112
126
  const formatNames = Object.keys(this._formats);
113
127
  if (formatNames.length > 0) out.formats = { ...this._formats };
114
128
  const keywordNames = Object.keys(this._keywords);
115
- if (keywordNames.length > 0) {
116
- const kws = {};
117
- for (const name of keywordNames) if (this._keywords[name] !== true) kws[name] = this._keywords[name];
118
- if (Object.keys(kws).length > 0) out.keywords = kws;
119
- }
129
+ const kws = {};
130
+ for (const name of keywordNames) if (this._keywords[name] !== true) kws[name] = this._keywords[name];
131
+ if (Object.keys(kws).length > 0) out.keywords = kws;
120
132
  const registry = this._registrySnapshot();
121
133
  if (registry) out.schemas = registry;
122
134
  return out;
@@ -126,23 +138,111 @@ class Ata {
126
138
  if (this._schemas.size === 0) return null;
127
139
  if (this._registry === null) {
128
140
  const map = {};
129
- for (const [key, schema] of this._schemas) map[key] = this._withDialect(schema);
141
+ for (const [key, schema] of this._schemas) map[fragmentKey(key)] = this._fragmentRefs(this._withDialect(schema));
130
142
  this._registry = map;
131
143
  }
132
144
  return this._registry;
133
145
  }
134
146
 
147
+ // A schema registered under a key that is only a fragment
148
+ // (`addSchema(s, '#/components/schemas/Pet')`, as OpenAPI tools do) is
149
+ // found by a `$ref` of that exact string that does not resolve in its own
150
+ // document: the reference looks it up among the registered keys then. ata
151
+ // resolves a fragment in the document alone, so such a `$ref` is pointed at
152
+ // the key's entry in the registry (see fragmentKey).
153
+ _fragmentRefs(schema) {
154
+ if (schema === null || typeof schema !== 'object') return schema;
155
+ let keys = this._fragmentKeys;
156
+ if (keys === undefined || this._fragmentStale) {
157
+ // The copies stand while the fragment-only keys are the same ones.
158
+ const now = new Set();
159
+ for (const key of this._schemas.keys()) if (key.startsWith('#')) now.add(key);
160
+ this._fragmentStale = false;
161
+ if (keys === undefined || keys.size !== now.size || [...now].some((k) => !keys.has(k))) {
162
+ // Each key's pointer, split and decoded once.
163
+ keys = this._fragmentKeys = new Map();
164
+ for (const key of now) {
165
+ let segs = null;
166
+ if (key.startsWith('#/')) {
167
+ try { segs = key.slice(2).split('/').map((raw) => decodeURIComponent(raw.replace(/~1/g, '/').replace(/~0/g, '~'))); } catch { segs = null; }
168
+ }
169
+ keys.set(key, segs);
170
+ }
171
+ this._fragmentCopies = new WeakMap();
172
+ }
173
+ }
174
+ if (keys.size === 0) return schema;
175
+ const hit = this._fragmentCopies.get(schema);
176
+ if (hit !== undefined) return hit;
177
+ if ((features(schema) & F_REF) === 0) { this._fragmentCopies.set(schema, schema); return schema; }
178
+ const resolves = (key, segs) => {
179
+ if (key === '#') return true;
180
+ if (segs === null) return !key.startsWith('#/');
181
+ let node = schema;
182
+ for (let i = 0; i < segs.length; i++) {
183
+ if (node === null || typeof node !== 'object' || !Object.prototype.hasOwnProperty.call(node, segs[i])) return false;
184
+ node = node[segs[i]];
185
+ }
186
+ return true;
187
+ };
188
+ // Only a key that does not resolve in this document can need pointing
189
+ // elsewhere. A document that holds every registered fragment itself, as
190
+ // an OpenAPI tool's schema carrying the whole `components` does, is left
191
+ // as it is without a walk.
192
+ const pointed = new Set();
193
+ for (const [key, segs] of keys) if (!resolves(key, segs)) pointed.add(key);
194
+ if (pointed.size === 0) { this._fragmentCopies.set(schema, schema); return schema; }
195
+ let changed = false;
196
+ const copy = (n, data) => {
197
+ if (n === null || typeof n !== 'object') return n;
198
+ if (Array.isArray(n)) return n.map((x) => copy(x, data));
199
+ const out = {};
200
+ for (const k of Object.keys(n)) {
201
+ let v;
202
+ if (!data && k === '$ref' && typeof n[k] === 'string' && pointed.has(n[k])) { v = fragmentKey(n[k]); changed = true; }
203
+ else v = copy(n[k], !data && (k === 'const' || k === 'enum' || k === 'default' || k === 'examples' || k === 'example'));
204
+ if (k === '__proto__') Object.defineProperty(out, k, { value: v, writable: true, enumerable: true, configurable: true });
205
+ else out[k] = v;
206
+ }
207
+ return out;
208
+ };
209
+ const out = copy(schema, false);
210
+ const result = changed ? out : schema;
211
+ this._fragmentCopies.set(schema, result);
212
+ return result;
213
+ }
214
+
135
215
  _touchRegistry() {
216
+ this._fragmentStale = true;
136
217
  this._registry = null;
137
218
  this._registryEpoch++;
138
219
  }
139
220
 
140
221
  // A schema with no `$schema` is read as draft-07, which is what the
141
- // reference class does. The copy is shallow: only the root changes.
222
+ // reference class does, or as `defaultMeta` when that names a dialect: the
223
+ // reference's 2020-12 class reads one as 2020-12, which is how an OpenAPI
224
+ // 3.1 document's schemas are meant. The copy is shallow: only the root
225
+ // changes.
226
+ // The copy is kept per schema object, so a schema compiled again or
227
+ // registered again is the same object to everything that remembers work
228
+ // per object below (the scan, normalization, the fragment copies).
142
229
  _withDialect(schema) {
143
230
  if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) return schema;
231
+ if (this._dialectCopies === undefined) this._dialectCopies = new WeakMap();
232
+ let out = this._dialectCopies.get(schema);
233
+ if (out === undefined) {
234
+ out = this._withDialectCopy(schema);
235
+ this._dialectCopies.set(schema, out);
236
+ }
237
+ return out;
238
+ }
239
+
240
+ _withDialectCopy(schema) {
241
+ if (isDraft4(schema)) return draft4AsDraft7(schema);
144
242
  if (typeof schema.$schema === 'string') return schema;
145
- return { $schema: DRAFT7, ...schema };
243
+ const meta = typeof this.opts.defaultMeta === 'string' ? this.opts.defaultMeta : DRAFT7;
244
+ if (DRAFT4_IDS.has(meta)) return draft4AsDraft7({ $schema: meta, ...schema });
245
+ return { $schema: meta, ...schema };
146
246
  }
147
247
 
148
248
  // ----- compile / validate --------------------------------------------
@@ -163,9 +263,20 @@ class Ata {
163
263
  }
164
264
  }
165
265
  }
166
- const rootDoc = this._withDialect(schema);
266
+ if (this._codeOnly && typeof schema === 'object' && schema !== null) {
267
+ const { schemaUsesKeywords } = require('./lib/keywords');
268
+ for (const name of Object.keys(this._codeOnly)) {
269
+ if (schemaUsesKeywords(schema, { [name]: this._codeOnly[name] }) || [...this._schemas.values()].some((s) => schemaUsesKeywords(s, { [name]: this._codeOnly[name] }))) {
270
+ throw new Error(`keyword "${name}": code-generating keywords are not supported; define it with validate, compile or macro`);
271
+ }
272
+ }
273
+ }
274
+ const rootDoc = this._fragmentRefs(this._withDialect(schema));
167
275
  const options = this._validatorOptions();
168
276
  const allErrors = !!this.opts.allErrors;
277
+ // The reference's `discriminator` option, for a schema that has one.
278
+ const discriminated = this.opts.discriminator === true &&
279
+ ((features(rootDoc) & F_DISCR) !== 0 || Object.values(options.schemas || {}).some((d) => features(d) & F_DISCR));
169
280
  let shaper = null;
170
281
  if (typeof rootDoc === 'object' && rootDoc !== null) {
171
282
  // The root is registered under a private id so the error shaper can
@@ -174,18 +285,47 @@ class Ata {
174
285
  const rootId = 'ata-compat:root';
175
286
  options.schemas = { ...(options.schemas || {}), [rootId]: rootDoc };
176
287
  shaper = createShaper({
177
- Validator, rootId, rootDoc, options, allErrors,
288
+ Validator, rootId, rootDoc, options, allErrors, schemas: options.schemas, discriminator: discriminated,
178
289
  macroKeywords: Object.keys(this._keywords).filter((k) => this._keywords[k] !== true && this._keywords[k].macro),
179
290
  });
180
291
  }
292
+ // coerceTypes reaching a value behind allOf, anyOf, oneOf, $ref and the
293
+ // rest, where ata's coercion pass, which runs before validation and
294
+ // follows `properties` and `items`, does not: the interpreted engine then
295
+ // coerces where each `type` is checked, as the reference does.
296
+ if (options.coerceTypes && (features(rootDoc) & F_BEYOND) !== 0) {
297
+ options._coerceInWalk = this.opts.coerceTypes === 'array' ? 'array' : true;
298
+ options.coerceTypes = false;
299
+ options.engine = 'interpreter';
300
+ }
301
+ // The reference's `discriminator` option: the interpreted engine reads the
302
+ // keyword (see discriminate in lib/interpreter-core.js).
303
+ // A format registered for numbers, for a schema that names a format.
304
+ if (this._numFormats && Object.keys(this._numFormats).length > 0 &&
305
+ ((features(rootDoc) & F_FORMAT) !== 0 || Object.values(options.schemas || {}).some((d) => features(d) & F_FORMAT))) {
306
+ options._numberFormats = { ...this._numFormats };
307
+ options.engine = 'interpreter';
308
+ }
309
+ if (discriminated) {
310
+ options._discriminator = true;
311
+ options.engine = 'interpreter';
312
+ }
181
313
  const v = new Validator(rootDoc, options);
314
+ const defaults = this.opts.useDefaults ? referencedDefaults(rootDoc, options.schemas) : null;
315
+ // With keywords that read their data's context, the shaper is given the
316
+ // errors as the validator made them: those carry each anyOf and oneOf
317
+ // branch's errors (see keepBranches in lib/interpreter-core.js), which the
318
+ // copies handed out do not.
319
+ const ctxKeywords = options.keywords !== undefined && Object.values(options.keywords).some((k) => k && k.dataContext === true);
182
320
  const validate = (data) => {
321
+ if (defaults !== null) defaults(data);
183
322
  const result = v.validate(data);
184
323
  if (result.valid) {
185
324
  validate.errors = null;
186
325
  return true;
187
326
  }
188
- let errors = shaper === null ? result.errors.map(asReferenceKeyword) : shaper(result.errors, data);
327
+ const raw = ctxKeywords && shaper !== null && typeof result._ataRaw === 'function' ? result._ataRaw() : result.errors;
328
+ let errors = shaper === null ? result.errors.map(asReferenceKeyword) : shaper(raw, data);
189
329
  // Errors the shaper synthesises, the `if` wrapper among them, are built
190
330
  // from the group rather than carried up from the validator, so verbose
191
331
  // mode has to fill in the value they point at here or they would be the
@@ -195,7 +335,7 @@ class Ata {
195
335
  ? { ...e, data: resolvePointer(data, e.instancePath || '', undefined) }
196
336
  : e));
197
337
  }
198
- validate.errors = shaper === null && !allErrors ? errors.slice(0, 1) : errors;
338
+ validate.errors = inReferenceOrder(shaper === null && !allErrors ? errors.slice(0, 1) : errors);
199
339
  return false;
200
340
  };
201
341
  validate.errors = null;
@@ -231,7 +371,8 @@ class Ata {
231
371
  for (const s of schema) this.addSchema(s);
232
372
  return this;
233
373
  }
234
- const id = schema && typeof schema === 'object' && typeof schema.$id === 'string' ? schema.$id : undefined;
374
+ const id = schema && typeof schema === 'object' && typeof schema.$id === 'string' ? schema.$id
375
+ : isDraft4(schema) && typeof schema.id === 'string' ? schema.id : undefined;
235
376
  const k = key !== undefined ? String(key) : id;
236
377
  if (k === undefined) throw new Error('schema with key or id "undefined" cannot be added: no key and no $id');
237
378
  if (this._schemas.has(k) && this._schemas.get(k) !== schema) {
@@ -308,7 +449,11 @@ class Ata {
308
449
  this.errors = [{ keyword: 'type', instancePath: '', schemaPath: '#/type', params: { type: 'object' }, message: 'must be object,boolean' }];
309
450
  return false;
310
451
  }
311
- const declared = typeof schema.$schema === 'string' ? schema.$schema : DRAFT7;
452
+ // A draft-04 schema is checked as the draft-07 schema it is read as: the
453
+ // boolean exclusive bounds in their numeric form, against draft-07's
454
+ // meta-schema (ata carries no draft-04 meta-schema).
455
+ if (isDraft4(schema)) schema = normalizeExclusiveBounds(structuredClone(draft4AsDraft7(schema)));
456
+ const declared = typeof schema.$schema === 'string' ? schema.$schema : typeof this.opts.defaultMeta === 'string' && !DRAFT4_IDS.has(this.opts.defaultMeta) ? this.opts.defaultMeta : DRAFT7;
312
457
  const metaId = METASCHEMAS.has(declared) ? declared : isDraft7Id(declared) ? DRAFT7 : DRAFT2020;
313
458
  const meta = metaValidator(metaId);
314
459
  const result = meta.validate(schema);
@@ -330,6 +475,15 @@ class Ata {
330
475
 
331
476
  addFormat(name, format) {
332
477
  this._formats[name] = formatToFunction(name, format);
478
+ // The reference applies a format declared `type: 'number'` to numbers,
479
+ // where JSON Schema, and ata, apply formats to strings only. Such a
480
+ // format is also checked on numbers here, by the interpreted engine (see
481
+ // compile).
482
+ if (format && typeof format === 'object' && format.type === 'number') {
483
+ this._numFormats = this._numFormats || Object.create(null);
484
+ this._numFormats[name] = this._formats[name];
485
+ } else if (this._numFormats) delete this._numFormats[name];
486
+ this._touchRegistry();
333
487
  this._compiled = new WeakMap();
334
488
  return this;
335
489
  }
@@ -346,6 +500,12 @@ class Ata {
346
500
  keyword = kwdOrDef.keyword;
347
501
  definition = kwdOrDef;
348
502
  }
503
+ // The reference takes a list of names for one definition (the formats
504
+ // plugin registers formatMinimum and its kin that way).
505
+ if (Array.isArray(keyword)) {
506
+ for (const k of keyword) this.addKeyword({ ...definition, keyword: k });
507
+ return this;
508
+ }
349
509
  if (typeof keyword !== 'string' || keyword === '') {
350
510
  throw new Error('addKeyword: keyword must be a non-empty string');
351
511
  }
@@ -358,14 +518,28 @@ class Ata {
358
518
  return this;
359
519
  }
360
520
  if (typeof definition.code === 'function' && !definition.validate && !definition.compile && !definition.macro) {
361
- throw new Error(`keyword "${keyword}": code-generating keywords are not supported; define it with validate, compile or macro`);
521
+ // A keyword defined only as generated code cannot run here. It is
522
+ // registered as known and refused when a schema uses it (see compile),
523
+ // so a library that defines such keywords for options it may never use
524
+ // can still take this class, and a schema that relies on one is never
525
+ // accepted without the check.
526
+ this._codeOnly = this._codeOnly || Object.create(null);
527
+ this._codeOnly[keyword] = { name: keyword, types: null, validate: null, compile: null, macro: null };
528
+ this._compiled = new WeakMap();
529
+ return this;
362
530
  }
363
531
  const mapped = {};
364
532
  if (definition.type !== undefined) mapped.type = definition.type;
365
533
  if (typeof definition.validate === 'function') mapped.validate = definition.validate;
366
- if (typeof definition.compile === 'function') mapped.compile = definition.compile;
534
+ if (typeof definition.compile === 'function') mapped.compile = compileWithContext(definition.compile);
367
535
  if (typeof definition.macro === 'function') mapped.macro = definition.macro;
536
+ if (typeof definition.before === 'string') mapped.before = definition.before;
537
+ if (definition.post === true) mapped.post = true;
368
538
  if (mapped.validate && mapped.compile) delete mapped.validate;
539
+ // The reference hands every keyword function the data's context
540
+ // (parentData, parentDataProperty, instancePath, rootData), and keywords
541
+ // that remove or rewrite the value they check rely on it.
542
+ if (mapped.validate || mapped.compile) mapped.dataContext = true;
369
543
  this._keywords[keyword] = mapped;
370
544
  this._keywordDefs = this._keywordDefs || Object.create(null);
371
545
  this._keywordDefs[keyword] = definition;
@@ -386,6 +560,7 @@ class Ata {
386
560
 
387
561
  removeKeyword(keyword) {
388
562
  delete this._keywords[keyword];
563
+ if (this._codeOnly) delete this._codeOnly[keyword];
389
564
  if (this._keywordDefs) delete this._keywordDefs[keyword];
390
565
  this._compiled = new WeakMap();
391
566
  return this;
@@ -399,11 +574,121 @@ class Ata {
399
574
  }
400
575
  }
401
576
 
577
+ // A reference keyword's compile(schema, parentSchema, it) returns a function
578
+ // of (data, dataCxt). `it.schemaPath.str` is read by keywords that build their
579
+ // own error objects, at the time they fail: it names the schema the check
580
+ // sits in, which is known per call here, so it is read from the context the
581
+ // call was given.
582
+ function compileWithContext(compile) {
583
+ return (value, parentSchema) => {
584
+ let current = '#';
585
+ const it = { get schemaPath() { return { str: current }; } };
586
+ const inner = compile(value, parentSchema, it);
587
+ if (typeof inner !== 'function') return inner;
588
+ const wrapped = (data, ctx) => {
589
+ if (ctx && typeof ctx.schemaPath === 'string') current = ctx.schemaPath.slice(0, ctx.schemaPath.lastIndexOf('/')) || '#';
590
+ return inner(data, ctx);
591
+ };
592
+ // Errors the keyword writes land on its own function, read from here.
593
+ wrapped.source = inner;
594
+ return wrapped;
595
+ };
596
+ }
597
+
598
+ // The reference writes an error's fields as instancePath, schemaPath,
599
+ // keyword, params, message; code that serializes errors sees the order. The
600
+ // errors a custom keyword reports come out in another, and are put in this one.
601
+ const REFERENCE_FIELDS = ['instancePath', 'schemaPath', 'keyword', 'params', 'message'];
602
+ function inReferenceOrder(errors) {
603
+ for (let i = 0; i < errors.length; i++) {
604
+ const e = errors[i];
605
+ if (e === null || typeof e !== 'object') continue;
606
+ const keys = Object.keys(e);
607
+ let ordered = true;
608
+ for (let j = 0; j < REFERENCE_FIELDS.length && j < keys.length; j++) if (keys[j] !== REFERENCE_FIELDS[j]) { ordered = false; break; }
609
+ if (ordered) continue;
610
+ const out = {};
611
+ for (const k of REFERENCE_FIELDS) if (k in e) out[k] = e[k];
612
+ for (const k of keys) if (!(k in out)) out[k] = e[k];
613
+ errors[i] = out;
614
+ }
615
+ return errors;
616
+ }
617
+
618
+ // useDefaults the way the reference applies it: in the properties of every
619
+ // subschema the data is validated against outside anyOf, oneOf, not and if,
620
+ // which includes those behind `allOf` and a `$ref`. ata's own pass fills the
621
+ // `properties` it can reach by nesting alone; this one, run before it, also
622
+ // goes through `allOf` and `$ref`. Both fill only a key the object does not
623
+ // have, so what one fills the other leaves. Null when the schema has no
624
+ // default anywhere such a walk could reach that ata's own pass would not.
625
+ function referencedDefaults(rootDoc, registry) {
626
+ if (rootDoc === null || typeof rootDoc !== 'object') return null;
627
+ const f = features(rootDoc);
628
+ if ((f & F_DEFAULT) === 0 && !Object.values(registry || {}).some((d) => features(d) & F_DEFAULT)) return null;
629
+ if ((f & (F_REF | F_ALLOF)) === 0) return null;
630
+ const resolve = (ref) => {
631
+ const hash = ref.indexOf('#');
632
+ const base = hash < 0 ? ref : ref.slice(0, hash);
633
+ const doc = base === '' ? rootDoc : registry && registry[base];
634
+ if (doc === undefined || doc === null) return null;
635
+ const frag = hash < 0 ? '' : ref.slice(hash + 1);
636
+ if (frag !== '' && !frag.startsWith('/')) return null;
637
+ let node = doc;
638
+ for (const raw of frag === '' ? [] : frag.slice(1).split('/')) {
639
+ let seg = raw.replace(/~1/g, '/').replace(/~0/g, '~');
640
+ try { seg = decodeURIComponent(seg); } catch { return null; }
641
+ if (node === null || typeof node !== 'object' || !Object.prototype.hasOwnProperty.call(node, seg)) return null;
642
+ node = node[seg];
643
+ }
644
+ return node;
645
+ };
646
+ const fill = (schema, data, active) => {
647
+ if (schema === null || typeof schema !== 'object' || data === null || typeof data !== 'object' || active.has(schema)) return;
648
+ active.add(schema);
649
+ if (typeof schema.$ref === 'string') fill(resolve(schema.$ref), data, active);
650
+ if (Array.isArray(schema.allOf)) for (const sub of schema.allOf) fill(sub, data, active);
651
+ const props = schema.properties;
652
+ if (props !== null && typeof props === 'object' && !Array.isArray(data)) {
653
+ for (const key of Object.keys(props)) {
654
+ const prop = props[key];
655
+ if (prop === null || typeof prop !== 'object') continue;
656
+ if (prop.default !== undefined && !Object.hasOwn(data, key)) {
657
+ const value = prop.default !== null && typeof prop.default === 'object' ? JSON.parse(JSON.stringify(prop.default)) : prop.default;
658
+ Object.defineProperty(data, key, { value, writable: true, enumerable: true, configurable: true });
659
+ }
660
+ // Into the value with a fresh set: the same schema may apply again
661
+ // one level down, in a recursive one.
662
+ if (Object.hasOwn(data, key)) fill(prop, data[key], new Set());
663
+ }
664
+ }
665
+ if (Array.isArray(data) && schema.items !== null && typeof schema.items === 'object' && !Array.isArray(schema.items)) {
666
+ for (const item of data) fill(schema.items, item, new Set());
667
+ }
668
+ active.delete(schema);
669
+ };
670
+ return (data) => fill(rootDoc, data, new Set());
671
+ }
672
+
673
+ // The registry id a fragment-only key is kept under: a `#...` string is not an
674
+ // id a document can be found by, so it is given one.
675
+ function fragmentKey(key) {
676
+ return key.startsWith('#') ? 'ata-compat:fragment:' + encodeURIComponent(key.slice(1)) : key;
677
+ }
678
+
679
+
680
+ // What a schema holds anywhere below it, read from the walk normalization
681
+ // makes of it anyway (lib/schema-scan.js), which remembers it per object.
682
+ const _scan = require('./lib/schema-scan');
683
+ const F_BEYOND = _scan.BEYOND_ITEMS, F_DISCR = _scan.DISCRIMINATOR, F_DEFAULT = _scan.HAS_DEFAULT;
684
+ const F_REF = _scan.HAS_REF, F_ALLOF = _scan.HAS_ALLOF, F_FORMAT = _scan.HAS_FORMAT;
685
+ const features = _scan.scan;
686
+
402
687
  const _metaValidators = new Map();
403
688
  function metaValidator(id) {
404
689
  let v = _metaValidators.get(id);
405
690
  if (!v) {
406
- v = new Validator({ $ref: id }, { useDefaults: false });
691
+ v = new Validator({ $ref: id }, { useDefaults: false, engine: 'interpreter' });
407
692
  _metaValidators.set(id, v);
408
693
  }
409
694
  return v;