yaml-dsl-spec 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -17
- package/index.js +1 -0
- package/lib/definition-reader.js +2 -1
- package/lib/dsl-definition.js +2 -0
- package/lib/function-table.js +7 -2
- package/lib/json-path.js +45 -29
- package/lib/problem-wording.js +223 -0
- package/lib/requirement-markers.js +128 -0
- package/lib/schema-validation.js +260 -229
- package/lib/validation-rules.js +123 -0
- package/package.json +1 -1
- package/schemas/x-yaml-dsl-functions-v1.1.schema.json +159 -0
- package/schemas/yaml-dsl-v1.2.schema.json +571 -0
package/lib/schema-validation.js
CHANGED
|
@@ -7,6 +7,24 @@ const AjvDraft202012 = require('ajv/dist/2020').default;
|
|
|
7
7
|
const { Diagnostic } = require('./diagnostic-report');
|
|
8
8
|
const { FunctionTable } = require('./function-table');
|
|
9
9
|
const { numberJson, integerOfText } = require('./yaml-node');
|
|
10
|
+
const { markedSchema } = require('./requirement-markers');
|
|
11
|
+
const {
|
|
12
|
+
fieldName,
|
|
13
|
+
numberText,
|
|
14
|
+
describedForm,
|
|
15
|
+
dependencyOf,
|
|
16
|
+
dependencyMessage,
|
|
17
|
+
duplicateItems,
|
|
18
|
+
containsMessage,
|
|
19
|
+
schemaNodeAt,
|
|
20
|
+
} = require('./problem-wording');
|
|
21
|
+
const {
|
|
22
|
+
ValidationRules,
|
|
23
|
+
childValue,
|
|
24
|
+
mergedOver,
|
|
25
|
+
copiedPointers,
|
|
26
|
+
escapePointer,
|
|
27
|
+
} = require('./validation-rules');
|
|
10
28
|
|
|
11
29
|
const VALIDATOR_OPTIONS = {
|
|
12
30
|
allErrors: true,
|
|
@@ -49,23 +67,22 @@ const READS_BELOW_KEYWORDS = new Set([
|
|
|
49
67
|
'uniqueItems',
|
|
50
68
|
]);
|
|
51
69
|
const REQUIRED_KEYWORDS = new Set(['required', 'dependentRequired', 'dependencies']);
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
maxProperties: ['has more than', 'property', 'properties'],
|
|
58
|
-
minProperties: ['has less than', 'property', 'properties'],
|
|
70
|
+
const BOUND_COMPARISONS = {
|
|
71
|
+
minimum: '>=',
|
|
72
|
+
maximum: '<=',
|
|
73
|
+
exclusiveMinimum: '>',
|
|
74
|
+
exclusiveMaximum: '<',
|
|
59
75
|
};
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
const LENGTH_LIMITS = {
|
|
77
|
+
minLength: ['fewer', 'characters'],
|
|
78
|
+
maxLength: ['more', 'characters'],
|
|
79
|
+
minItems: ['fewer', 'items'],
|
|
80
|
+
maxItems: ['more', 'items'],
|
|
81
|
+
minProperties: ['fewer', 'properties'],
|
|
82
|
+
maxProperties: ['more', 'properties'],
|
|
65
83
|
};
|
|
66
|
-
const KEYWORD_NAMES = { 'false schema': 'falseSchema'
|
|
84
|
+
const KEYWORD_NAMES = { 'false schema': 'falseSchema' };
|
|
67
85
|
const FALSE_SCHEMA_STEP = /\/false schema$/;
|
|
68
|
-
const SHOWN_ENUM_OPTIONS = 3;
|
|
69
86
|
|
|
70
87
|
const hasOwn = (object, field) => Object.prototype.hasOwnProperty.call(object, field);
|
|
71
88
|
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -88,10 +105,11 @@ function draftOfSchema(schema) {
|
|
|
88
105
|
|
|
89
106
|
/** A document's schema, as its caller hands it over: its function table, and the validation against it. */
|
|
90
107
|
class LoadedSchema {
|
|
91
|
-
constructor(table, validator, schema, name, pointersBySchemaObject) {
|
|
108
|
+
constructor(table, validator, schema, marked, name, pointersBySchemaObject) {
|
|
92
109
|
this.table = table;
|
|
93
110
|
this.validator = validator;
|
|
94
111
|
this.schema = schema;
|
|
112
|
+
this.marked = marked;
|
|
95
113
|
this.name = name;
|
|
96
114
|
this.pointersBySchemaObject = pointersBySchemaObject;
|
|
97
115
|
}
|
|
@@ -112,119 +130,234 @@ class LoadedSchema {
|
|
|
112
130
|
/** The schema `text` holds, named `name` in reports, or throws why it holds none. */
|
|
113
131
|
static parse(name, text) {
|
|
114
132
|
const failed = (error) => new Error(`schema ${name}: ${error.message}`);
|
|
115
|
-
let schema;
|
|
116
|
-
let table;
|
|
117
|
-
let validator;
|
|
118
133
|
try {
|
|
119
|
-
schema = JSON.parse(text);
|
|
120
|
-
table = FunctionTable.fromSchema(schema);
|
|
134
|
+
const schema = JSON.parse(text);
|
|
135
|
+
const table = FunctionTable.fromSchema(schema);
|
|
121
136
|
const draft = draftOfSchema(schema);
|
|
122
|
-
const
|
|
137
|
+
const declaresOlderDraft = isObject(schema)
|
|
138
|
+
&& schema.$schema !== undefined
|
|
139
|
+
&& OLDER_DRAFT_URIS.includes(schema.$schema);
|
|
140
|
+
const declared = declaresOlderDraft
|
|
123
141
|
? { ...schema, $schema: ASSUMED_DRAFT.schemaUris[0] }
|
|
124
142
|
: schema;
|
|
125
|
-
|
|
126
|
-
|
|
143
|
+
const marked = markedSchema(declared, draft.name);
|
|
144
|
+
const validator = new draft.ValidatorClass(VALIDATOR_OPTIONS).compile(marked);
|
|
145
|
+
const pointers = schemaObjectPointers(marked);
|
|
146
|
+
return new LoadedSchema(table, validator, schema, marked, name, pointers);
|
|
127
147
|
} catch (error) {
|
|
128
148
|
throw failed(error);
|
|
129
149
|
}
|
|
130
150
|
}
|
|
131
151
|
|
|
132
152
|
/**
|
|
133
|
-
* Validates `document`, a resolved value
|
|
134
|
-
* compiles, such as a call or a reference to a declaration,
|
|
153
|
+
* Validates `document`, a resolved value, read by `rules`. A value not known
|
|
154
|
+
* when the document compiles, such as a call or a reference to a declaration,
|
|
155
|
+
* is not checked.
|
|
135
156
|
*/
|
|
136
|
-
validate(document,
|
|
137
|
-
const instance = new InstanceRecord(
|
|
138
|
-
const json = instance.json(document, '', document.position);
|
|
157
|
+
validate(document, rules) {
|
|
158
|
+
const instance = new InstanceRecord(rules);
|
|
159
|
+
const json = this.withQuotedValuesRead(instance.json(document, '', document.position), rules);
|
|
139
160
|
this.validator(json);
|
|
140
|
-
const errors = reportedErrors(this.validator.errors || [], this.
|
|
161
|
+
const errors = reportedErrors(this.validator.errors || [], this.marked)
|
|
141
162
|
.filter((error) => !instance.readsUnknown(error.instancePath, error.keyword));
|
|
142
|
-
const
|
|
163
|
+
const reported = { dependencies: new Set(), itemLists: new Set(), propertyNames: new Set() };
|
|
164
|
+
const diagnostics = errors
|
|
165
|
+
.flatMap((error) => this.errorProblems(error, json, instance, reported))
|
|
166
|
+
.filter((problem) => !instance.isCopied(problem.subject))
|
|
167
|
+
.map((problem) => problem.diagnostic);
|
|
143
168
|
diagnostics.sort(compareDiagnostics);
|
|
144
169
|
return diagnostics.filter((diagnostic, index) =>
|
|
145
170
|
index === 0 || !sameDiagnostic(diagnostic, diagnostics[index - 1]));
|
|
146
171
|
}
|
|
147
172
|
|
|
148
173
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* breaks, and the allowed name closest to a misspelt one.
|
|
174
|
+
* `json` with each quoted value that spells a type the schema expects of it, and
|
|
175
|
+
* `rules` allow, read as that type, until no more are read.
|
|
152
176
|
*/
|
|
153
|
-
|
|
177
|
+
withQuotedValuesRead(json, rules) {
|
|
178
|
+
if (rules.quotedValueTypes.length === 0) return json;
|
|
179
|
+
let read = json;
|
|
180
|
+
for (;;) {
|
|
181
|
+
this.validator(read);
|
|
182
|
+
const readings = new Map();
|
|
183
|
+
for (const error of this.validator.errors || []) {
|
|
184
|
+
if (error.keyword !== 'type' || typeof error.data !== 'string') continue;
|
|
185
|
+
const reading = rules.quotedValue(error.data, [].concat(error.schema));
|
|
186
|
+
if (reading !== null && !readings.has(error.instancePath)) {
|
|
187
|
+
readings.set(error.instancePath, reading.value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (readings.size === 0) return read;
|
|
191
|
+
for (const [pointer, value] of readings) read = withValueAt(read, pointer, value);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* A schema error as the problems a reader fixes it from, `{ subject, diagnostic }`:
|
|
197
|
+
* placed at the value or the field that is wrong, worded by the standard,
|
|
198
|
+
* labeled with what is wrong there, naming the rule it breaks, and the allowed
|
|
199
|
+
* name closest to a misspelt one.
|
|
200
|
+
*/
|
|
201
|
+
errorProblems(error, json, instance, reported) {
|
|
154
202
|
const pointer = error.instancePath;
|
|
155
203
|
const keyword = keywordName(error);
|
|
156
204
|
const schemaPointer = this.keywordPointer(error);
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
205
|
+
const ruleAt = (name, at) => `${name}, at ${this.name}#${at}`;
|
|
206
|
+
const rule = ruleAt(keyword, schemaPointer);
|
|
207
|
+
const field = fieldName(json, pointer);
|
|
208
|
+
const missingField = REQUIRED_KEYWORDS.has(error.keyword)
|
|
209
|
+
? error.params.missingProperty
|
|
210
|
+
: null;
|
|
211
|
+
const dependency = error.propertyName === undefined
|
|
212
|
+
? dependencyOf(this.marked, schemaPointer, valueAt(json, pointer), missingField)
|
|
213
|
+
: null;
|
|
214
|
+
if (dependency !== null) {
|
|
215
|
+
const subject = `${pointer}/${escapePointer(dependency.key)}`;
|
|
216
|
+
if (reported.dependencies.has(subject)) return [];
|
|
217
|
+
reported.dependencies.add(subject);
|
|
218
|
+
const { keywordPointer } = dependency;
|
|
219
|
+
const dependencyKeyword = keywordPointer.slice(keywordPointer.lastIndexOf('/') + 1);
|
|
220
|
+
const at = `${dependency.keywordPointer}/${escapePointer(dependency.key)}`;
|
|
221
|
+
return [{
|
|
222
|
+
subject,
|
|
223
|
+
diagnostic: Diagnostic.at(
|
|
224
|
+
instance.position(subject),
|
|
225
|
+
dependencyMessage(this.marked, dependency.keywordPointer, dependency.key),
|
|
172
226
|
)
|
|
173
|
-
.withLabel(
|
|
174
|
-
.withRule(
|
|
175
|
-
}
|
|
227
|
+
.withLabel(`breaks \`${dependencyKeyword}\``)
|
|
228
|
+
.withRule(ruleAt(dependencyKeyword, at)),
|
|
229
|
+
}];
|
|
176
230
|
}
|
|
177
|
-
const step = lastStep(pointer);
|
|
178
|
-
const holder = step === null ? 'the document' : `\`${step}\``;
|
|
179
231
|
const suggestion = (name, candidates) => {
|
|
180
232
|
const closest = closestName(name, candidates);
|
|
181
233
|
return closest === null ? null : `did you mean \`${closest}\`?`;
|
|
182
234
|
};
|
|
235
|
+
const atValue = (message) => [{
|
|
236
|
+
subject: pointer,
|
|
237
|
+
diagnostic: Diagnostic.at(instance.valuePosition(pointer), message)
|
|
238
|
+
.withLabel(`breaks \`${keyword}\``)
|
|
239
|
+
.withRule(rule),
|
|
240
|
+
}];
|
|
241
|
+
if (error.propertyName !== undefined) {
|
|
242
|
+
const subject = `${pointer}/${escapePointer(error.propertyName)}`;
|
|
243
|
+
if (reported.propertyNames.has(subject)) return [];
|
|
244
|
+
reported.propertyNames.add(subject);
|
|
245
|
+
return [{
|
|
246
|
+
subject,
|
|
247
|
+
diagnostic: Diagnostic.at(
|
|
248
|
+
instance.position(subject),
|
|
249
|
+
`\`${error.propertyName}\` is not a valid key of ${field}`,
|
|
250
|
+
)
|
|
251
|
+
.withLabel('breaks `propertyNames`')
|
|
252
|
+
.withRule(rule),
|
|
253
|
+
}];
|
|
254
|
+
}
|
|
255
|
+
const itemsBeyondTuple = (error.keyword === 'items' && error.schema === false)
|
|
256
|
+
|| error.keyword === 'additionalItems';
|
|
257
|
+
if (itemsBeyondTuple) {
|
|
258
|
+
if (reported.itemLists.has(pointer)) return [];
|
|
259
|
+
reported.itemLists.add(pointer);
|
|
260
|
+
const listKeyword = schemaPointer.slice(schemaPointer.lastIndexOf('/') + 1);
|
|
261
|
+
return [{
|
|
262
|
+
subject: pointer,
|
|
263
|
+
diagnostic: Diagnostic.at(
|
|
264
|
+
instance.valuePosition(pointer),
|
|
265
|
+
`${field} must NOT have more than ${error.params.limit} items`,
|
|
266
|
+
)
|
|
267
|
+
.withLabel(`breaks \`${listKeyword}\``)
|
|
268
|
+
.withRule(ruleAt(listKeyword, schemaPointer)),
|
|
269
|
+
}];
|
|
270
|
+
}
|
|
183
271
|
if (error.keyword === 'additionalProperties' || error.keyword === 'unevaluatedProperties') {
|
|
184
272
|
const name = error.keyword === 'additionalProperties'
|
|
185
273
|
? error.params.additionalProperty
|
|
186
274
|
: error.params.unevaluatedProperty;
|
|
187
275
|
const allowed = allowedFields(this.schema, schemaPointer);
|
|
188
|
-
const
|
|
189
|
-
return [
|
|
190
|
-
|
|
191
|
-
|
|
276
|
+
const subject = `${pointer}/${escapePointer(name)}`;
|
|
277
|
+
return [{
|
|
278
|
+
subject,
|
|
279
|
+
diagnostic: Diagnostic.at(
|
|
280
|
+
instance.position(subject),
|
|
281
|
+
`\`${name}\` is not a field of ${field}`,
|
|
282
|
+
)
|
|
283
|
+
.withLabel(`${field} has no field \`${name}\``)
|
|
192
284
|
.withRule(rule)
|
|
193
285
|
.withHelp(suggestion(name, allowed)),
|
|
194
|
-
];
|
|
286
|
+
}];
|
|
195
287
|
}
|
|
196
288
|
if (error.keyword === 'enum') {
|
|
197
289
|
const options = Array.isArray(error.schema) ? error.schema : [];
|
|
198
290
|
const allowed = options.filter((option) => typeof option === 'string');
|
|
199
|
-
const
|
|
200
|
-
return [
|
|
201
|
-
|
|
291
|
+
const shown = options.map((option) => `\`${jsonText(option)}\``).join(', ');
|
|
292
|
+
return [{
|
|
293
|
+
subject: pointer,
|
|
294
|
+
diagnostic: Diagnostic.at(
|
|
295
|
+
instance.valuePosition(pointer),
|
|
296
|
+
`${field} must be one of ${shown}`,
|
|
297
|
+
)
|
|
202
298
|
.withLabel(`not one of the ${options.length} values \`enum\` allows`)
|
|
203
299
|
.withRule(rule)
|
|
204
|
-
.withHelp(typeof
|
|
205
|
-
];
|
|
300
|
+
.withHelp(typeof error.data === 'string' ? suggestion(error.data, allowed) : null),
|
|
301
|
+
}];
|
|
206
302
|
}
|
|
207
303
|
if (error.keyword === 'type') {
|
|
208
|
-
const expected =
|
|
209
|
-
return [
|
|
210
|
-
|
|
304
|
+
const expected = [].concat(error.schema);
|
|
305
|
+
return [{
|
|
306
|
+
subject: pointer,
|
|
307
|
+
diagnostic: Diagnostic.at(
|
|
308
|
+
instance.valuePosition(pointer),
|
|
309
|
+
`${field} must be ${expected.join(' or ')}`,
|
|
310
|
+
)
|
|
211
311
|
.withLabel(`is ${withArticle(instance.jsonType(pointer, error.data))}`)
|
|
212
312
|
.withRule(rule),
|
|
213
|
-
];
|
|
313
|
+
}];
|
|
214
314
|
}
|
|
215
315
|
if (REQUIRED_KEYWORDS.has(error.keyword)) {
|
|
216
316
|
const name = error.params.missingProperty;
|
|
217
|
-
return [
|
|
218
|
-
|
|
219
|
-
|
|
317
|
+
return [{
|
|
318
|
+
subject: pointer,
|
|
319
|
+
diagnostic: Diagnostic.at(instance.position(pointer), `\`${name}\` is required`)
|
|
320
|
+
.withLabel(`${field} has no field \`${name}\``)
|
|
220
321
|
.withRule(rule),
|
|
221
|
-
];
|
|
322
|
+
}];
|
|
323
|
+
}
|
|
324
|
+
if (ALTERNATIVE_KEYWORDS.has(error.keyword)) {
|
|
325
|
+
if (Array.isArray(error.params.passingSchemas)) {
|
|
326
|
+
return atValue(`${field} matches more than one of its allowed forms`);
|
|
327
|
+
}
|
|
328
|
+
const holderPointer = schemaPointer.slice(0, schemaPointer.lastIndexOf('/'));
|
|
329
|
+
const form = describedForm(this.schema, holderPointer);
|
|
330
|
+
return atValue(form === null
|
|
331
|
+
? `${field} matches none of its allowed forms`
|
|
332
|
+
: `${field} must be ${form}`);
|
|
333
|
+
}
|
|
334
|
+
if (hasOwn(BOUND_COMPARISONS, error.keyword)) {
|
|
335
|
+
const comparison = BOUND_COMPARISONS[error.keyword];
|
|
336
|
+
return atValue(`${field} must be ${comparison} ${numberText(error.schema)}`);
|
|
337
|
+
}
|
|
338
|
+
if (hasOwn(LENGTH_LIMITS, error.keyword)) {
|
|
339
|
+
const [comparison, unit] = LENGTH_LIMITS[error.keyword];
|
|
340
|
+
return atValue(`${field} must NOT have ${comparison} than ${error.params.limit} ${unit}`);
|
|
341
|
+
}
|
|
342
|
+
switch (error.keyword) {
|
|
343
|
+
case 'const': return atValue(`${field} must be equal to constant`);
|
|
344
|
+
case 'multipleOf':
|
|
345
|
+
return atValue(`${field} must be multiple of ${numberText(error.schema)}`);
|
|
346
|
+
case 'pattern': return atValue(`${field} must match pattern "${error.params.pattern}"`);
|
|
347
|
+
case 'uniqueItems': {
|
|
348
|
+
const items = Array.isArray(error.data) ? error.data : [];
|
|
349
|
+
const [later, earlier] = duplicateItems(items) || [0, 0];
|
|
350
|
+
return atValue(
|
|
351
|
+
`${field} must NOT have duplicate items (items ## ${later} and ${earlier} are identical)`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
case 'contains': return atValue(`${field} ${containsMessage(error.parentSchema)}`);
|
|
355
|
+
case 'not': return atValue(`${field} must NOT be valid`);
|
|
356
|
+
case 'false schema': return atValue(`${field} boolean schema is false`);
|
|
357
|
+
case 'unevaluatedItems':
|
|
358
|
+
return atValue(`${field} must NOT have more than ${error.params.len} items`);
|
|
359
|
+
default: return atValue(`${field} breaks \`${keyword}\``);
|
|
222
360
|
}
|
|
223
|
-
return [
|
|
224
|
-
Diagnostic.at(instance.valuePosition(pointer), `${displayPointer(pointer)}: ${errorText(error, instance)}`)
|
|
225
|
-
.withLabel(`breaks \`${keyword}\``)
|
|
226
|
-
.withRule(rule),
|
|
227
|
-
];
|
|
228
361
|
}
|
|
229
362
|
}
|
|
230
363
|
|
|
@@ -322,17 +455,6 @@ function pointerOfSchemaPath(path) {
|
|
|
322
455
|
}
|
|
323
456
|
}
|
|
324
457
|
|
|
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
458
|
/**
|
|
337
459
|
* The fields a schema node allows, by the `properties` of the node holding the
|
|
338
460
|
* keyword at `keywordPointer`, of the schemas it references and of those it
|
|
@@ -397,61 +519,10 @@ function editDistance(first, second) {
|
|
|
397
519
|
return previous[secondCharacters.length];
|
|
398
520
|
}
|
|
399
521
|
|
|
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
522
|
function withArticle(noun) {
|
|
428
523
|
return `${/^[aeiou]/.test(noun) ? 'an' : 'a'} ${noun}`;
|
|
429
524
|
}
|
|
430
525
|
|
|
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
526
|
/** A floating-point number as `serde_json` writes one. */
|
|
456
527
|
function floatText(number) {
|
|
457
528
|
if (Object.is(number, -0)) return '-0.0';
|
|
@@ -496,72 +567,6 @@ function nodeJsonText(node) {
|
|
|
496
567
|
return jsonText(node.toJson(), (at) => (numberTexts.has(at) ? numberTexts.get(at) : null));
|
|
497
568
|
}
|
|
498
569
|
|
|
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
570
|
function compareDiagnostics(first, second) {
|
|
566
571
|
const firstPlace = first.position ? [first.position.line, first.position.column] : [0, 0];
|
|
567
572
|
const secondPlace = second.position ? [second.position.line, second.position.column] : [0, 0];
|
|
@@ -573,14 +578,39 @@ function sameDiagnostic(first, second) {
|
|
|
573
578
|
return JSON.stringify(first) === JSON.stringify(second);
|
|
574
579
|
}
|
|
575
580
|
|
|
581
|
+
function pointerSteps(pointer) {
|
|
582
|
+
return pointer.slice(1).split('/')
|
|
583
|
+
.map((step) => step.replaceAll('~1', '/').replaceAll('~0', '~'));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** The value at a JSON pointer of `json`, `null` where it holds none. */
|
|
587
|
+
function valueAt(json, pointer) {
|
|
588
|
+
if (pointer === '') return json;
|
|
589
|
+
let node = json;
|
|
590
|
+
for (const step of pointerSteps(pointer)) {
|
|
591
|
+
if (node === null || typeof node !== 'object' || !hasOwn(node, step)) return null;
|
|
592
|
+
node = node[step];
|
|
593
|
+
}
|
|
594
|
+
return node;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** `json` with the value at a JSON pointer it holds replaced by `value`. */
|
|
598
|
+
function withValueAt(json, pointer, value) {
|
|
599
|
+
if (pointer === '') return value;
|
|
600
|
+
const holder = valueAt(json, pointer.slice(0, pointer.lastIndexOf('/')));
|
|
601
|
+
if (holder !== null && typeof holder === 'object') holder[pointerSteps(pointer).at(-1)] = value;
|
|
602
|
+
return json;
|
|
603
|
+
}
|
|
604
|
+
|
|
576
605
|
/** The document as JSON, and where each of its values was written. */
|
|
577
606
|
class InstanceRecord {
|
|
578
|
-
constructor(
|
|
579
|
-
this.
|
|
607
|
+
constructor(rules) {
|
|
608
|
+
this.rules = rules;
|
|
580
609
|
this.positions = new Map();
|
|
581
610
|
this.valuePositions = new Map();
|
|
582
611
|
this.numberTexts = new Map();
|
|
583
612
|
this.unknown = [];
|
|
613
|
+
this.copied = [];
|
|
584
614
|
}
|
|
585
615
|
|
|
586
616
|
json(value, pointer, keyPosition) {
|
|
@@ -604,6 +634,7 @@ class InstanceRecord {
|
|
|
604
634
|
if (isObject(json)) this.setChildValue(entry.key, json);
|
|
605
635
|
object[entry.key] = json;
|
|
606
636
|
}
|
|
637
|
+
this.mergeBaseKeys(object, pointer);
|
|
607
638
|
return object;
|
|
608
639
|
}
|
|
609
640
|
default:
|
|
@@ -614,10 +645,30 @@ class InstanceRecord {
|
|
|
614
645
|
|
|
615
646
|
/** A key such as `name mock-orders` sets the field `name` of the entry it keys, unless it writes the field. */
|
|
616
647
|
setChildValue(key, fields) {
|
|
617
|
-
const found = childValue(key
|
|
648
|
+
const found = this.rules.childValue(key);
|
|
618
649
|
if (found !== null && !hasOwn(fields, found.field)) fields[found.field] = found.value;
|
|
619
650
|
}
|
|
620
651
|
|
|
652
|
+
/** Merges the content of each merge base key `object` holds into each sibling map. */
|
|
653
|
+
mergeBaseKeys(object, pointer) {
|
|
654
|
+
const { mergeBaseKeys } = this.rules;
|
|
655
|
+
for (const baseKey of mergeBaseKeys.filter((key) => hasOwn(object, key))) {
|
|
656
|
+
const base = object[baseKey];
|
|
657
|
+
for (const [key, written] of Object.entries(object)) {
|
|
658
|
+
if (mergeBaseKeys.includes(key) || !isObject(written)) continue;
|
|
659
|
+
const entryPointer = `${pointer}/${escapePointer(key)}`;
|
|
660
|
+
const merged = mergedOver(base, written, entryPointer, (at) => this.isUnknown(at));
|
|
661
|
+
this.copied.push(...copiedPointers(merged, written, entryPointer));
|
|
662
|
+
object[key] = merged;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Whether `pointer` names a value a merge base key copied in, or one below it. */
|
|
668
|
+
isCopied(pointer) {
|
|
669
|
+
return this.copied.some((copied) => pointer === copied || pointer.startsWith(`${copied}/`));
|
|
670
|
+
}
|
|
671
|
+
|
|
621
672
|
/**
|
|
622
673
|
* Whether an error at `pointer` comes from a value not known when the document
|
|
623
674
|
* compiles: the value itself, or a check that reads a value below it through a
|
|
@@ -658,35 +709,15 @@ class InstanceRecord {
|
|
|
658
709
|
}
|
|
659
710
|
return 'object';
|
|
660
711
|
}
|
|
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
712
|
}
|
|
682
713
|
|
|
683
714
|
module.exports = {
|
|
684
715
|
LoadedSchema,
|
|
685
716
|
SchemaCache,
|
|
717
|
+
ValidationRules,
|
|
686
718
|
childValue,
|
|
687
719
|
closestName,
|
|
688
720
|
nodeJsonText,
|
|
689
721
|
draftOfSchema,
|
|
690
722
|
compareText,
|
|
691
|
-
debugText,
|
|
692
723
|
};
|