yaml-dsl-spec 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,692 @@
1
+ 'use strict';
2
+
3
+ const AjvDraft07 = require('ajv').default;
4
+ const AjvDraft201909 = require('ajv/dist/2019').default;
5
+ const AjvDraft202012 = require('ajv/dist/2020').default;
6
+
7
+ const { Diagnostic } = require('./diagnostic-report');
8
+ const { FunctionTable } = require('./function-table');
9
+ const { numberJson, integerOfText } = require('./yaml-node');
10
+
11
+ const VALIDATOR_OPTIONS = {
12
+ allErrors: true,
13
+ strict: false,
14
+ unicodeRegExp: true,
15
+ validateFormats: false,
16
+ verbose: true,
17
+ logger: false,
18
+ };
19
+ const DRAFTS = [
20
+ {
21
+ name: 'draft-07',
22
+ ValidatorClass: AjvDraft07,
23
+ schemaUris: ['http://json-schema.org/draft-07/schema#', 'http://json-schema.org/draft-07/schema'],
24
+ },
25
+ {
26
+ name: 'draft 2019-09',
27
+ ValidatorClass: AjvDraft201909,
28
+ schemaUris: ['https://json-schema.org/draft/2019-09/schema', 'https://json-schema.org/draft/2019-09/schema#'],
29
+ },
30
+ {
31
+ name: 'draft 2020-12',
32
+ ValidatorClass: AjvDraft202012,
33
+ schemaUris: ['https://json-schema.org/draft/2020-12/schema', 'https://json-schema.org/draft/2020-12/schema#'],
34
+ },
35
+ ];
36
+ const [ASSUMED_DRAFT] = DRAFTS;
37
+ const OLDER_DRAFT_URIS = ['draft-03', 'draft-04', 'draft-06'].flatMap((name) => [
38
+ `http://json-schema.org/${name}/schema#`,
39
+ `http://json-schema.org/${name}/schema`,
40
+ ]);
41
+ const ALTERNATIVE_KEYWORDS = new Set(['anyOf', 'oneOf']);
42
+ const READS_BELOW_KEYWORDS = new Set([
43
+ 'anyOf',
44
+ 'oneOf',
45
+ 'not',
46
+ 'contains',
47
+ 'unevaluatedItems',
48
+ 'unevaluatedProperties',
49
+ 'uniqueItems',
50
+ ]);
51
+ const REQUIRED_KEYWORDS = new Set(['required', 'dependentRequired', 'dependencies']);
52
+ const LIMIT_PLURALS = {
53
+ maxLength: ['is longer than', 'character', 'characters'],
54
+ minLength: ['is shorter than', 'character', 'characters'],
55
+ maxItems: ['has more than', 'item', 'items'],
56
+ minItems: ['has less than', 'item', 'items'],
57
+ maxProperties: ['has more than', 'property', 'properties'],
58
+ minProperties: ['has less than', 'property', 'properties'],
59
+ };
60
+ const BOUNDS = {
61
+ maximum: 'is greater than the maximum of',
62
+ minimum: 'is less than the minimum of',
63
+ exclusiveMaximum: 'is greater than or equal to the maximum of',
64
+ exclusiveMinimum: 'is less than or equal to the minimum of',
65
+ };
66
+ const KEYWORD_NAMES = { 'false schema': 'falseSchema', items: 'additionalItems' };
67
+ const FALSE_SCHEMA_STEP = /\/false schema$/;
68
+ const SHOWN_ENUM_OPTIONS = 3;
69
+
70
+ const hasOwn = (object, field) => Object.prototype.hasOwnProperty.call(object, field);
71
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
72
+
73
+ /** Orders texts by their UTF-8 bytes, as the crate orders strings. */
74
+ function compareText(first, second) {
75
+ return Buffer.compare(Buffer.from(first, 'utf8'), Buffer.from(second, 'utf8'));
76
+ }
77
+
78
+ /** The draft the standard reads a document schema in, by its root `$schema`. */
79
+ function draftOfSchema(schema) {
80
+ if (!isObject(schema) || schema.$schema === undefined || OLDER_DRAFT_URIS.includes(schema.$schema)) {
81
+ return ASSUMED_DRAFT;
82
+ }
83
+ const draft = DRAFTS.find((known) => known.schemaUris.includes(schema.$schema));
84
+ if (draft) return draft;
85
+ const readDrafts = DRAFTS.map((known) => known.name).join(', ');
86
+ throw new Error(`declares ${jsonText(schema.$schema)}, a draft this reader does not read; it reads ${readDrafts}`);
87
+ }
88
+
89
+ /** A document's schema, as its caller hands it over: its function table, and the validation against it. */
90
+ class LoadedSchema {
91
+ constructor(table, validator, schema, name, pointersBySchemaObject) {
92
+ this.table = table;
93
+ this.validator = validator;
94
+ this.schema = schema;
95
+ this.name = name;
96
+ this.pointersBySchemaObject = pointersBySchemaObject;
97
+ }
98
+
99
+ /**
100
+ * Where the schema holds the keyword an error breaks, as a JSON pointer: by the
101
+ * schema object holding it, since Ajv restarts a path inside a reference it
102
+ * does not inline.
103
+ */
104
+ keywordPointer(error) {
105
+ const written = pointerOfSchemaPath(error.schemaPath);
106
+ if (FALSE_SCHEMA_STEP.test(written)) return written.replace(FALSE_SCHEMA_STEP, '');
107
+ const holder = this.pointersBySchemaObject.get(error.parentSchema);
108
+ if (holder === undefined) return written;
109
+ return `${holder}${written.slice(written.lastIndexOf('/'))}`;
110
+ }
111
+
112
+ /** The schema `text` holds, named `name` in reports, or throws why it holds none. */
113
+ static parse(name, text) {
114
+ const failed = (error) => new Error(`schema ${name}: ${error.message}`);
115
+ let schema;
116
+ let table;
117
+ let validator;
118
+ try {
119
+ schema = JSON.parse(text);
120
+ table = FunctionTable.fromSchema(schema);
121
+ const draft = draftOfSchema(schema);
122
+ const compiled = isObject(schema) && schema.$schema !== undefined && OLDER_DRAFT_URIS.includes(schema.$schema)
123
+ ? { ...schema, $schema: ASSUMED_DRAFT.schemaUris[0] }
124
+ : schema;
125
+ validator = new draft.ValidatorClass(VALIDATOR_OPTIONS).compile(compiled);
126
+ return new LoadedSchema(table, validator, schema, name, schemaObjectPointers(compiled));
127
+ } catch (error) {
128
+ throw failed(error);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Validates `document`, a resolved value. A value not known when the document
134
+ * compiles, such as a call or a reference to a declaration, is not checked.
135
+ */
136
+ validate(document, childValuePatterns = []) {
137
+ const instance = new InstanceRecord(childValuePatterns);
138
+ const json = instance.json(document, '', document.position);
139
+ this.validator(json);
140
+ const errors = reportedErrors(this.validator.errors || [], this.schema)
141
+ .filter((error) => !instance.readsUnknown(error.instancePath, error.keyword));
142
+ const diagnostics = errors.flatMap((error) => this.errorDiagnostics(error, instance));
143
+ diagnostics.sort(compareDiagnostics);
144
+ return diagnostics.filter((diagnostic, index) =>
145
+ index === 0 || !sameDiagnostic(diagnostic, diagnostics[index - 1]));
146
+ }
147
+
148
+ /**
149
+ * A schema error as the report a reader fixes it from: placed at the value or
150
+ * the field that is wrong, labeled with what is wrong there, naming the rule it
151
+ * breaks, and the allowed name closest to a misspelt one.
152
+ */
153
+ errorDiagnostics(error, instance) {
154
+ const pointer = error.instancePath;
155
+ const keyword = keywordName(error);
156
+ const schemaPointer = this.keywordPointer(error);
157
+ const rule = `${keyword}, at ${this.name}#${schemaPointer}`;
158
+ if (error.propertyName !== undefined) {
159
+ return [
160
+ Diagnostic.at(instance.valuePosition(pointer), `${displayPointer(pointer)}: ${errorText(error, instance)}`)
161
+ .withLabel(`breaks \`${keyword}\``)
162
+ .withRule(rule),
163
+ ];
164
+ }
165
+ if (error.keyword === 'items' && error.schema === false) {
166
+ const extra = Array.isArray(error.data) ? error.data.slice(error.params.limit) : [];
167
+ return extra.map((item, index) => {
168
+ const itemPointer = `${pointer}/${error.params.limit + index}`;
169
+ return Diagnostic.at(
170
+ instance.valuePosition(itemPointer),
171
+ `${displayPointer(itemPointer)}: False schema does not allow ${instance.jsonText(item, itemPointer)}`,
172
+ )
173
+ .withLabel('breaks `falseSchema`')
174
+ .withRule(`falseSchema, at ${this.name}#${schemaPointer}`);
175
+ });
176
+ }
177
+ const step = lastStep(pointer);
178
+ const holder = step === null ? 'the document' : `\`${step}\``;
179
+ const suggestion = (name, candidates) => {
180
+ const closest = closestName(name, candidates);
181
+ return closest === null ? null : `did you mean \`${closest}\`?`;
182
+ };
183
+ if (error.keyword === 'additionalProperties' || error.keyword === 'unevaluatedProperties') {
184
+ const name = error.keyword === 'additionalProperties'
185
+ ? error.params.additionalProperty
186
+ : error.params.unevaluatedProperty;
187
+ const allowed = allowedFields(this.schema, schemaPointer);
188
+ const field = `${pointer}/${escapePointer(name)}`;
189
+ return [
190
+ Diagnostic.at(instance.position(field), `field \`${name}\` is not allowed`)
191
+ .withLabel(`${holder} has no field \`${name}\``)
192
+ .withRule(rule)
193
+ .withHelp(suggestion(name, allowed)),
194
+ ];
195
+ }
196
+ if (error.keyword === 'enum') {
197
+ const options = Array.isArray(error.schema) ? error.schema : [];
198
+ const allowed = options.filter((option) => typeof option === 'string');
199
+ const value = error.data;
200
+ return [
201
+ Diagnostic.at(instance.valuePosition(pointer), `\`${shownValue(value, pointer, instance)}\` is not an allowed value`)
202
+ .withLabel(`not one of the ${options.length} values \`enum\` allows`)
203
+ .withRule(rule)
204
+ .withHelp(typeof value === 'string' ? suggestion(value, allowed) : null),
205
+ ];
206
+ }
207
+ if (error.keyword === 'type') {
208
+ const expected = typesOf(error.schema);
209
+ return [
210
+ Diagnostic.at(instance.valuePosition(pointer), `${holder} is not ${withArticle(expected.join(' or '))}`)
211
+ .withLabel(`is ${withArticle(instance.jsonType(pointer, error.data))}`)
212
+ .withRule(rule),
213
+ ];
214
+ }
215
+ if (REQUIRED_KEYWORDS.has(error.keyword)) {
216
+ const name = error.params.missingProperty;
217
+ return [
218
+ Diagnostic.at(instance.position(pointer), `field \`${name}\` is required`)
219
+ .withLabel(`${holder} has no field \`${name}\``)
220
+ .withRule(rule),
221
+ ];
222
+ }
223
+ return [
224
+ Diagnostic.at(instance.valuePosition(pointer), `${displayPointer(pointer)}: ${errorText(error, instance)}`)
225
+ .withLabel(`breaks \`${keyword}\``)
226
+ .withRule(rule),
227
+ ];
228
+ }
229
+ }
230
+
231
+ /** The JSON pointer of every object of a schema, by the object. */
232
+ function schemaObjectPointers(schema) {
233
+ const pointers = new Map();
234
+ const pending = [[schema, '']];
235
+ while (pending.length > 0) {
236
+ const [node, pointer] = pending.pop();
237
+ if (node === null || typeof node !== 'object' || pointers.has(node)) continue;
238
+ pointers.set(node, pointer);
239
+ for (const [key, child] of Object.entries(node)) pending.push([child, `${pointer}/${escapePointer(key)}`]);
240
+ }
241
+ return pointers;
242
+ }
243
+
244
+ /** Schemas parsed once and shared by every unit that reads them, by name and text. */
245
+ class SchemaCache {
246
+ constructor() {
247
+ this.loaded = new Map();
248
+ }
249
+
250
+ /** The schema named `name` whose text is `text`, or throws why it holds none. */
251
+ load(name, text) {
252
+ const key = JSON.stringify([name, text]);
253
+ if (!this.loaded.has(key)) {
254
+ try {
255
+ this.loaded.set(key, { schema: LoadedSchema.parse(name, text) });
256
+ } catch (error) {
257
+ this.loaded.set(key, { error });
258
+ }
259
+ }
260
+ const loaded = this.loaded.get(key);
261
+ if (loaded.error) throw loaded.error;
262
+ return loaded.schema;
263
+ }
264
+ }
265
+
266
+ /** The keyword a report names: the crate's name for what Ajv reports. */
267
+ function keywordName(error) {
268
+ if (error.propertyName !== undefined) return 'propertyNames';
269
+ if (error.keyword === 'dependentRequired' || error.keyword === 'dependencies') return 'required';
270
+ return hasOwn(KEYWORD_NAMES, error.keyword) ? KEYWORD_NAMES[error.keyword] : error.keyword;
271
+ }
272
+
273
+ /**
274
+ * The errors a reader reports: those of a check made on the document, never the
275
+ * errors inside a branch of `anyOf`, `oneOf` or `contains` that only explain the
276
+ * check's failure, and never an `if`. A property name's failure is reported by the
277
+ * check the name fails, never again as the `propertyNames` it fails through.
278
+ */
279
+ function reportedErrors(errors, schema) {
280
+ const reachable = new Map();
281
+ const reaches = (start, target) => {
282
+ if (!reachable.has(start)) reachable.set(start, reachableSchemaObjects(start, schema));
283
+ return reachable.get(start).has(target);
284
+ };
285
+ const explainers = errors.filter((error) =>
286
+ ALTERNATIVE_KEYWORDS.has(error.keyword) || error.keyword === 'contains');
287
+ const explains = (outer, inner) => {
288
+ if (outer === inner) return false;
289
+ const below = inner.instancePath === outer.instancePath
290
+ || inner.instancePath.startsWith(`${outer.instancePath}/`);
291
+ if (outer.keyword === 'contains' && inner.instancePath === outer.instancePath) return false;
292
+ return below && reaches(outer.schema, inner.parentSchema);
293
+ };
294
+ return errors.filter((error) => error.keyword !== 'if'
295
+ && !(error.keyword === 'propertyNames' && error.propertyName === undefined)
296
+ && !explainers.some((outer) => explains(outer, error)));
297
+ }
298
+
299
+ /** Every schema object reachable from `start`, through local references. */
300
+ function reachableSchemaObjects(start, root) {
301
+ const reached = new Set();
302
+ const pending = [start];
303
+ while (pending.length > 0) {
304
+ const node = pending.pop();
305
+ if (node === null || typeof node !== 'object' || reached.has(node)) continue;
306
+ reached.add(node);
307
+ if (!Array.isArray(node) && typeof node.$ref === 'string' && node.$ref.startsWith('#')) {
308
+ pending.push(schemaNodeAt(root, pointerOfSchemaPath(node.$ref)));
309
+ }
310
+ pending.push(...Object.values(node));
311
+ }
312
+ return reached;
313
+ }
314
+
315
+ /** A schema path or a local reference as a JSON pointer, without its `#`. */
316
+ function pointerOfSchemaPath(path) {
317
+ const pointer = path.startsWith('#') ? path.slice(1) : path;
318
+ try {
319
+ return decodeURIComponent(pointer);
320
+ } catch {
321
+ return pointer;
322
+ }
323
+ }
324
+
325
+ function schemaNodeAt(root, pointer) {
326
+ if (pointer === '') return root;
327
+ let node = root;
328
+ for (const step of pointer.slice(1).split('/')) {
329
+ const key = step.replaceAll('~1', '/').replaceAll('~0', '~');
330
+ if (node === null || typeof node !== 'object' || !hasOwn(node, key)) return undefined;
331
+ node = node[key];
332
+ }
333
+ return node;
334
+ }
335
+
336
+ /**
337
+ * The fields a schema node allows, by the `properties` of the node holding the
338
+ * keyword at `keywordPointer`, of the schemas it references and of those it
339
+ * combines.
340
+ */
341
+ function allowedFields(schema, keywordPointer) {
342
+ const holderPointer = keywordPointer.includes('/')
343
+ ? keywordPointer.slice(0, keywordPointer.lastIndexOf('/'))
344
+ : '';
345
+ const fields = [];
346
+ const holder = schemaNodeAt(schema, holderPointer);
347
+ if (holder !== undefined) collectFields(schema, holder, fields, new Set());
348
+ return fields;
349
+ }
350
+
351
+ function collectFields(schema, node, fields, visited) {
352
+ if (!isObject(node)) return;
353
+ if (isObject(node.properties)) {
354
+ for (const name of Object.keys(node.properties)) if (!fields.includes(name)) fields.push(name);
355
+ }
356
+ if (typeof node.$ref === 'string' && node.$ref.startsWith('#')) {
357
+ const pointer = node.$ref.slice(1);
358
+ const target = visited.has(pointer) ? undefined : schemaNodeAt(schema, pointer);
359
+ visited.add(pointer);
360
+ if (target !== undefined) collectFields(schema, target, fields, visited);
361
+ }
362
+ for (const combinator of ['allOf', 'anyOf', 'oneOf']) {
363
+ for (const branch of Array.isArray(node[combinator]) ? node[combinator] : []) {
364
+ collectFields(schema, branch, fields, visited);
365
+ }
366
+ }
367
+ }
368
+
369
+ /**
370
+ * The candidate closest in spelling to `name`, where one is close enough to be a
371
+ * likely fix and no other is as close; else `null`.
372
+ */
373
+ function closestName(name, candidates) {
374
+ const ranked = candidates
375
+ .filter((candidate) => candidate !== name)
376
+ .map((candidate) => [editDistance(name, candidate), candidate])
377
+ .sort(([firstDistance, first], [secondDistance, second]) =>
378
+ firstDistance - secondDistance || compareText(first, second));
379
+ const limit = Math.max(Math.floor(Array.from(name).length / 3), 1);
380
+ if (ranked.length === 0) return null;
381
+ const [[distance, best], next] = ranked;
382
+ return distance <= limit && (next === undefined || next[0] > distance) ? best : null;
383
+ }
384
+
385
+ /** The fewest single-character insertions, deletions and substitutions turning one text into the other. */
386
+ function editDistance(first, second) {
387
+ const secondCharacters = Array.from(second);
388
+ let previous = Array.from({ length: secondCharacters.length + 1 }, (_, index) => index);
389
+ Array.from(first).forEach((firstCharacter, row) => {
390
+ const current = [row + 1];
391
+ secondCharacters.forEach((secondCharacter, column) => {
392
+ const substitution = previous[column] + (firstCharacter === secondCharacter ? 0 : 1);
393
+ current.push(Math.min(substitution, previous[column + 1] + 1, current[column] + 1));
394
+ });
395
+ previous = current;
396
+ });
397
+ return previous[secondCharacters.length];
398
+ }
399
+
400
+ /**
401
+ * What a JSON pointer names, as a report shows it: its last key, or for a list
402
+ * item, the list's key and the item's index, such as `dsls[0]`; `null` for the
403
+ * document's root.
404
+ */
405
+ function lastStep(pointer) {
406
+ const slash = pointer.lastIndexOf('/');
407
+ if (slash === -1) return null;
408
+ const parent = pointer.slice(0, slash);
409
+ const step = pointer.slice(slash + 1);
410
+ if (/^\d+$/.test(step)) {
411
+ const list = lastStep(parent);
412
+ if (list !== null) return `${list}[${step}]`;
413
+ }
414
+ return step.replaceAll('~1', '/').replaceAll('~0', '~');
415
+ }
416
+
417
+ function escapePointer(key) {
418
+ return key.replaceAll('~', '~0').replaceAll('/', '~1');
419
+ }
420
+
421
+ function displayPointer(pointer) {
422
+ if (pointer === '') return 'the document';
423
+ const steps = pointer.slice(1).split('/').map((step) => step.replaceAll('~1', '/').replaceAll('~0', '~'));
424
+ return `\`${steps.join('.')}\``;
425
+ }
426
+
427
+ function withArticle(noun) {
428
+ return `${/^[aeiou]/.test(noun) ? 'an' : 'a'} ${noun}`;
429
+ }
430
+
431
+ /** A schema's `type` as the crate lists several: in its fixed order of JSON types. */
432
+ function typesOf(typeKeyword) {
433
+ if (!Array.isArray(typeKeyword)) return [String(typeKeyword)];
434
+ const order = ['null', 'boolean', 'integer', 'number', 'string', 'array', 'object'];
435
+ return order.filter((type) => typeKeyword.includes(type));
436
+ }
437
+
438
+ /** A string as Rust's `Debug` writes it. */
439
+ function debugText(text) {
440
+ let written = '"';
441
+ for (const character of text) {
442
+ const code = character.codePointAt(0);
443
+ if (character === '"') written += '\\"';
444
+ else if (character === '\\') written += '\\\\';
445
+ else if (character === '\n') written += '\\n';
446
+ else if (character === '\r') written += '\\r';
447
+ else if (character === '\t') written += '\\t';
448
+ else if (character === '\0') written += '\\0';
449
+ else if (code < 0x20 || code === 0x7f) written += `\\u{${code.toString(16)}}`;
450
+ else written += character;
451
+ }
452
+ return `${written}"`;
453
+ }
454
+
455
+ /** A floating-point number as `serde_json` writes one. */
456
+ function floatText(number) {
457
+ if (Object.is(number, -0)) return '-0.0';
458
+ const exponent = Number(number.toExponential().split('e')[1]);
459
+ if (exponent < -5 || exponent >= 16) return number.toExponential().replace('e+', 'e');
460
+ const decimal = String(number);
461
+ return decimal.includes('.') ? decimal : `${decimal}.0`;
462
+ }
463
+
464
+ /** A JSON value as `serde_json` writes it, compact. */
465
+ function jsonText(value, numberTextAt = () => null, pointer = '') {
466
+ if (value === null) return 'null';
467
+ if (typeof value === 'boolean') return String(value);
468
+ if (typeof value === 'string') return JSON.stringify(value);
469
+ if (typeof value === 'number') {
470
+ const written = numberTextAt(pointer);
471
+ if (written !== null) {
472
+ const integer = integerOfText(written);
473
+ if (integer !== null) return integer.toString();
474
+ }
475
+ return Number.isInteger(value) && written === null ? String(value) : floatText(value);
476
+ }
477
+ if (Array.isArray(value)) {
478
+ return `[${value.map((item, index) => jsonText(item, numberTextAt, `${pointer}/${index}`)).join(',')}]`;
479
+ }
480
+ return `{${Object.entries(value)
481
+ .map(([key, item]) => `${JSON.stringify(key)}:${jsonText(item, numberTextAt, `${pointer}/${escapePointer(key)}`)}`)
482
+ .join(',')}}`;
483
+ }
484
+
485
+ /** A node as `serde_json` writes the JSON it reads to, each number as it is written. */
486
+ function nodeJsonText(node) {
487
+ const numberTexts = new Map();
488
+ const collect = (current, pointer) => {
489
+ if (current.kind === 'number') numberTexts.set(pointer, current.value);
490
+ if (current.kind === 'list') current.value.forEach((item, index) => collect(item, `${pointer}/${index}`));
491
+ if (current.kind === 'map') {
492
+ for (const entry of current.value) collect(entry.value, `${pointer}/${escapePointer(entry.key)}`);
493
+ }
494
+ };
495
+ collect(node, '');
496
+ return jsonText(node.toJson(), (at) => (numberTexts.has(at) ? numberTexts.get(at) : null));
497
+ }
498
+
499
+ function shownValue(value, pointer, instance) {
500
+ return typeof value === 'string' ? debugText(value) : instance.jsonText(value, pointer);
501
+ }
502
+
503
+ function plural(count, words) {
504
+ return count === 1 ? words[1] : words[2];
505
+ }
506
+
507
+ /** An error's text as the crate's validator words it. */
508
+ function errorText(error, instance) {
509
+ const shown = instance.jsonText(error.data, error.instancePath);
510
+ const { keyword, params } = error;
511
+ if (hasOwn(LIMIT_PLURALS, keyword)) {
512
+ const words = LIMIT_PLURALS[keyword];
513
+ return `${shown} ${words[0]} ${params.limit} ${plural(params.limit, words)}`;
514
+ }
515
+ if (hasOwn(BOUNDS, keyword)) return `${shown} ${BOUNDS[keyword]} ${jsonText(params.limit)}`;
516
+ switch (keyword) {
517
+ case 'enum': return `${shown} is not one of ${enumOptionsText(Array.isArray(error.schema) ? error.schema : [])}`;
518
+ case 'type': {
519
+ const types = typesOf(error.schema);
520
+ return types.length === 1
521
+ ? `${shown} is not of type ${JSON.stringify(types[0])}`
522
+ : `${shown} is not of types ${types.map((type) => JSON.stringify(type)).join(', ')}`;
523
+ }
524
+ case 'const': return `${jsonText(error.schema)} was expected`;
525
+ case 'pattern': return `${shown} does not match "${params.pattern}"`;
526
+ case 'multipleOf': return `${shown} is not a multiple of ${params.multipleOf}`;
527
+ case 'uniqueItems': return `${shown} has non-unique elements`;
528
+ case 'not': return `${jsonText(error.schema)} is not allowed for ${shown}`;
529
+ case 'anyOf': return `${shown} is not valid under any of the schemas listed in the 'anyOf' keyword`;
530
+ case 'oneOf':
531
+ return params.passingSchemas
532
+ ? `${shown} is valid under more than one of the schemas listed in the 'oneOf' keyword`
533
+ : `${shown} is not valid under any of the schemas listed in the 'oneOf' keyword`;
534
+ case 'contains': return `None of ${shown} are valid under the given schema`;
535
+ case 'false schema': return `False schema does not allow ${shown}`;
536
+ case 'propertyNames': return `${JSON.stringify(params.propertyName)} is not valid under the property names schema`;
537
+ case 'items':
538
+ case 'additionalItems': {
539
+ const extra = Array.isArray(error.data) ? error.data.slice(params.limit) : [];
540
+ const listed = extra.map((item, index) =>
541
+ instance.jsonText(item, `${error.instancePath}/${params.limit + index}`));
542
+ return `Additional items are not allowed (${listed.join(', ')} ${extra.length === 1 ? 'was' : 'were'} unexpected)`;
543
+ }
544
+ case 'unevaluatedItems': {
545
+ const extra = Array.isArray(error.data) ? error.data.slice(params.len) : [];
546
+ const listed = extra.map((item, index) =>
547
+ `'${instance.jsonText(item, `${error.instancePath}/${params.len + index}`)}'`);
548
+ return `Unevaluated items are not allowed (${listed.join(', ')} ${extra.length === 1 ? 'was' : 'were'} unexpected)`;
549
+ }
550
+ default: return error.message;
551
+ }
552
+ }
553
+
554
+ /** An enum's options as the crate's validator lists them: a few, then how many more. */
555
+ function enumOptionsText(options) {
556
+ const written = options.map((option) => jsonText(option));
557
+ if (written.length <= SHOWN_ENUM_OPTIONS) {
558
+ if (written.length <= 1) return written.join('');
559
+ return `${written.slice(0, -1).join(', ')} or ${written.at(-1)}`;
560
+ }
561
+ const shown = written.slice(0, SHOWN_ENUM_OPTIONS - 1);
562
+ return `${shown.join(', ')} or ${written.length - shown.length} other candidates`;
563
+ }
564
+
565
+ function compareDiagnostics(first, second) {
566
+ const firstPlace = first.position ? [first.position.line, first.position.column] : [0, 0];
567
+ const secondPlace = second.position ? [second.position.line, second.position.column] : [0, 0];
568
+ return firstPlace[0] - secondPlace[0] || firstPlace[1] - secondPlace[1]
569
+ || compareText(first.message, second.message);
570
+ }
571
+
572
+ function sameDiagnostic(first, second) {
573
+ return JSON.stringify(first) === JSON.stringify(second);
574
+ }
575
+
576
+ /** The document as JSON, and where each of its values was written. */
577
+ class InstanceRecord {
578
+ constructor(childValuePatterns) {
579
+ this.childValuePatterns = childValuePatterns;
580
+ this.positions = new Map();
581
+ this.valuePositions = new Map();
582
+ this.numberTexts = new Map();
583
+ this.unknown = [];
584
+ }
585
+
586
+ json(value, pointer, keyPosition) {
587
+ this.positions.set(pointer, keyPosition);
588
+ this.valuePositions.set(pointer, value.position);
589
+ const local = value.localValue();
590
+ switch (local.kind) {
591
+ case 'null': return null;
592
+ case 'bool': return local.value;
593
+ case 'number':
594
+ this.numberTexts.set(pointer, local.value);
595
+ return numberJson(local.value);
596
+ case 'string': return local.value;
597
+ case 'list':
598
+ return local.value.map((item, index) => this.json(item, `${pointer}/${index}`, item.position));
599
+ case 'map': {
600
+ const object = {};
601
+ for (const entry of local.value) {
602
+ const child = `${pointer}/${escapePointer(entry.key)}`;
603
+ const json = this.json(entry.value, child, entry.keyPosition);
604
+ if (isObject(json)) this.setChildValue(entry.key, json);
605
+ object[entry.key] = json;
606
+ }
607
+ return object;
608
+ }
609
+ default:
610
+ this.unknown.push(pointer);
611
+ return null;
612
+ }
613
+ }
614
+
615
+ /** A key such as `name mock-orders` sets the field `name` of the entry it keys, unless it writes the field. */
616
+ setChildValue(key, fields) {
617
+ const found = childValue(key, this.childValuePatterns);
618
+ if (found !== null && !hasOwn(fields, found.field)) fields[found.field] = found.value;
619
+ }
620
+
621
+ /**
622
+ * Whether an error at `pointer` comes from a value not known when the document
623
+ * compiles: the value itself, or a check that reads a value below it through a
624
+ * subschema, or compares the values below it.
625
+ */
626
+ readsUnknown(pointer, keyword) {
627
+ if (this.isUnknown(pointer)) return true;
628
+ const below = `${pointer}/`;
629
+ return READS_BELOW_KEYWORDS.has(keyword) && this.unknown.some((unknown) => unknown.startsWith(below));
630
+ }
631
+
632
+ isUnknown(pointer) {
633
+ return this.unknown.some((unknown) => pointer === unknown || pointer.startsWith(`${unknown}/`));
634
+ }
635
+
636
+ valuePosition(pointer) {
637
+ return this.valuePositions.has(pointer) ? this.valuePositions.get(pointer) : this.position(pointer);
638
+ }
639
+
640
+ position(pointer) {
641
+ let current = pointer;
642
+ for (;;) {
643
+ if (this.positions.has(current)) return this.positions.get(current);
644
+ current = current.includes('/') ? current.slice(0, current.lastIndexOf('/')) : '';
645
+ }
646
+ }
647
+
648
+ /** The JSON type of the value at `pointer`, an integer as written told from a number. */
649
+ jsonType(pointer, value) {
650
+ if (value === null) return 'null';
651
+ if (Array.isArray(value)) return 'array';
652
+ if (typeof value === 'boolean') return 'boolean';
653
+ if (typeof value === 'string') return 'string';
654
+ if (typeof value === 'number') {
655
+ const written = this.numberTexts.get(pointer);
656
+ if (written !== undefined) return integerOfText(written) !== null ? 'integer' : 'number';
657
+ return Number.isInteger(value) ? 'integer' : 'number';
658
+ }
659
+ return 'object';
660
+ }
661
+
662
+ jsonText(value, pointer) {
663
+ return jsonText(value, (at) => (this.numberTexts.has(at) ? this.numberTexts.get(at) : null), pointer);
664
+ }
665
+ }
666
+
667
+ /**
668
+ * The field a key such as `name mock-orders` sets on the entry it keys, and the
669
+ * value it sets, `{ field, value }`, by the first of `patterns` with `field` and
670
+ * `value` groups that matches; else `null`.
671
+ */
672
+ function childValue(key, patterns) {
673
+ for (const pattern of patterns) {
674
+ const match = pattern.exec(key);
675
+ const groups = match && match.groups;
676
+ if (groups && groups.field !== undefined && groups.value !== undefined) {
677
+ return { field: groups.field, value: groups.value };
678
+ }
679
+ }
680
+ return null;
681
+ }
682
+
683
+ module.exports = {
684
+ LoadedSchema,
685
+ SchemaCache,
686
+ childValue,
687
+ closestName,
688
+ nodeJsonText,
689
+ draftOfSchema,
690
+ compareText,
691
+ debugText,
692
+ };