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/README.md
CHANGED
|
@@ -9,20 +9,4 @@ if (read.problems) for (const problem of read.problems) console.error(String(pro
|
|
|
9
9
|
else for (const { definition } of read.dsls) console.log(definition.id);
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|---|---|
|
|
14
|
-
| `readDslDefinitions(file, text)` | `{ dsls }`, each DSL of a `yaml-dsl.yml`, `{ definition, block }`, validated against its version's schema and the checks the schema cannot state; or `{ problems }`, every problem at the field it is about |
|
|
15
|
-
| `Diagnostic`, `Position` | a problem at its place: `message`, `position` and `notes` (`label`, `rule`, `help`) |
|
|
16
|
-
| `yamlReader.parseYaml(file, text, tagKeys)` | `{ node }`, YAML read with each value's place, or `{ problem }` |
|
|
17
|
-
| `Node`, `numberJson` | a YAML value as written, and a number's text as JSON |
|
|
18
|
-
| `dslDefinition.dslDefinitionFromJson(block)` | a DSL block with its defaults, its fields named as the standard names them |
|
|
19
|
-
| `jsonPath` | `PathPattern` and `PathSet`, the standard's JSONPath subset |
|
|
20
|
-
| `resolved` | `Value`, a resolved document's value with its place |
|
|
21
|
-
| `functionTable` | `FunctionTable`, a schema's `x-yaml-dsl-functions` |
|
|
22
|
-
| `schemaValidation` | `SchemaCache` and `LoadedSchema`, a document validated against its schema, each problem at its place |
|
|
23
|
-
| `parseCallKey`, `closestName` | a call written in a key, and the allowed name closest to a misspelt one |
|
|
24
|
-
| `yamlDslSchema(version)`, `yamlDslSchemas` | the JSON Schema of `yaml-dsl.yml`, at a version, or by version |
|
|
25
|
-
| `functionTableSchema(version)`, `functionTableSchemas` | the JSON Schema of a DSL's function table, `x-yaml-dsl-functions`, at a version, or by version |
|
|
26
|
-
| `YAML_DSL_VERSIONS`, `FUNCTION_TABLE_VERSIONS` | the versions the package reads |
|
|
27
|
-
|
|
28
|
-
A version is a minor, such as `1.0`, or a major, such as `1`, which is its newest minor in the package.
|
|
12
|
+
Its API is the core's, designed in the [package design](https://github.com/litebitus/yaml-dsl-spec/blob/main/docs/package-design.md).
|
package/index.js
CHANGED
package/lib/definition-reader.js
CHANGED
|
@@ -13,6 +13,7 @@ const { compilePattern, groupNamesOf } = require('./pattern-dialect');
|
|
|
13
13
|
const { Position } = require('./source-position');
|
|
14
14
|
const { Value } = require('./resolved-value');
|
|
15
15
|
const { SchemaCache, closestName, nodeJsonText } = require('./schema-validation');
|
|
16
|
+
const { ValidationRules } = require('./validation-rules');
|
|
16
17
|
const { parseYaml } = require('./yaml-reader');
|
|
17
18
|
const { YAML_DSL_VERSIONS, yamlDslSchemaFile } = require('./schema-catalog');
|
|
18
19
|
|
|
@@ -37,7 +38,7 @@ function readDslDefinitions(file, text) {
|
|
|
37
38
|
} catch (error) {
|
|
38
39
|
return { problems: [Diagnostic.unplaced(error.message)] };
|
|
39
40
|
}
|
|
40
|
-
const schemaProblems = schema.validate(Value.fromNode(root),
|
|
41
|
+
const schemaProblems = schema.validate(Value.fromNode(root), new ValidationRules());
|
|
41
42
|
if (schemaProblems.length > 0) return { problems: schemaProblems };
|
|
42
43
|
const blocks = (root.get('dsls') && root.get('dsls').items()) || [];
|
|
43
44
|
const problems = [];
|
package/lib/dsl-definition.js
CHANGED
|
@@ -125,7 +125,9 @@ const DSL_FIELDS = {
|
|
|
125
125
|
})), emptyList),
|
|
126
126
|
tag_keys: withDefault(listOf(struct({ tag: required(text), key: required(text) })), emptyList),
|
|
127
127
|
validator: optional(struct({
|
|
128
|
+
merge_base_keys: withDefault(listOf(text), emptyList),
|
|
128
129
|
child_values_from_parent_key: withDefault(listOf(struct({ pattern: required(text) })), emptyList),
|
|
130
|
+
allow_quoted_values: withDefault(listOf(oneOf(['boolean', 'integer', 'number'])), emptyList),
|
|
129
131
|
})),
|
|
130
132
|
};
|
|
131
133
|
|
package/lib/function-table.js
CHANGED
|
@@ -64,7 +64,7 @@ function compareText(first, second) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function functionEntry(name, entry) {
|
|
67
|
-
const listed = isObject(entry) ? entry.arguments : undefined;
|
|
67
|
+
const listed = Array.isArray(entry) ? entry : isObject(entry) ? entry.arguments : undefined;
|
|
68
68
|
if (!Array.isArray(listed)) throw new Error(`function \`${name}\` has no arguments list`);
|
|
69
69
|
const functionArguments = listed.map((argument) => {
|
|
70
70
|
const argumentName = isObject(argument) ? argument.name : undefined;
|
|
@@ -75,7 +75,9 @@ function functionEntry(name, entry) {
|
|
|
75
75
|
default: hasOwn(argument, 'default') ? argument.default : null,
|
|
76
76
|
};
|
|
77
77
|
});
|
|
78
|
-
const macroEntry =
|
|
78
|
+
const macroEntry = isObject(entry) && hasOwn(entry, 'macro')
|
|
79
|
+
? macroEntryOf(name, entry.macro)
|
|
80
|
+
: null;
|
|
79
81
|
return { arguments: functionArguments, macroEntry };
|
|
80
82
|
}
|
|
81
83
|
|
|
@@ -101,6 +103,9 @@ function macroEntryOf(name, value) {
|
|
|
101
103
|
for (const role of ROLE_NAMES) {
|
|
102
104
|
if (typeof value[role] === 'string') roles[role] = value[role];
|
|
103
105
|
}
|
|
106
|
+
if (!hasOwn(roles, 'loop_variables') && typeof value.loop_variable === 'string') {
|
|
107
|
+
roles.loop_variables = value.loop_variable;
|
|
108
|
+
}
|
|
104
109
|
return new MacroEntry(kind, written || 'shallow', nameSuffix, roles);
|
|
105
110
|
}
|
|
106
111
|
|
package/lib/json-path.js
CHANGED
|
@@ -11,6 +11,23 @@ function indexStep(index) {
|
|
|
11
11
|
return { index };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** A `['name']` step's name and the text after the step, or `null`. */
|
|
15
|
+
function quotedName(text) {
|
|
16
|
+
if (!text.startsWith("['")) return null;
|
|
17
|
+
const close = text.indexOf("'", 2);
|
|
18
|
+
if (close <= 2 || text[close + 1] !== ']') return null;
|
|
19
|
+
return { name: text.slice(2, close), remaining: text.slice(close + 2) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A dotted step's name, which runs to the next `.` or `[`, and the text after it, or `null`. */
|
|
23
|
+
function dottedName(text) {
|
|
24
|
+
if (text.startsWith('*')) return null;
|
|
25
|
+
const stepEnd = text.search(/[.[]/);
|
|
26
|
+
const end = stepEnd === -1 ? text.length : stepEnd;
|
|
27
|
+
if (end === 0) return null;
|
|
28
|
+
return { name: text.slice(0, end), remaining: text.slice(end) };
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
/** A parsed path and the keys its wildcard steps do not select or descend into. */
|
|
15
32
|
class PathPattern {
|
|
16
33
|
constructor(selectors, skipKeys) {
|
|
@@ -23,38 +40,37 @@ class PathPattern {
|
|
|
23
40
|
const invalid = () =>
|
|
24
41
|
Diagnostic.unplaced(`\`${text}\` is not a path of the supported JSONPath subset`);
|
|
25
42
|
if (!text.startsWith('$')) throw invalid();
|
|
26
|
-
|
|
43
|
+
let rest = text.slice(1);
|
|
27
44
|
const selectors = [];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
while (rest.length > 0) {
|
|
46
|
+
let step;
|
|
47
|
+
if (rest.startsWith('..')) {
|
|
48
|
+
const after = rest.slice(2);
|
|
49
|
+
if (after === '*') {
|
|
50
|
+
step = { selector: { kind: 'descendant_all' }, remaining: '' };
|
|
51
|
+
} else {
|
|
52
|
+
const named = quotedName(after) || dottedName(after);
|
|
53
|
+
if (!named) throw invalid();
|
|
54
|
+
step = {
|
|
55
|
+
selector: { kind: 'descendant_name', name: named.name },
|
|
56
|
+
remaining: named.remaining,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
} else if (rest.startsWith('.*')) {
|
|
60
|
+
step = { selector: { kind: 'wildcard' }, remaining: rest.slice(2) };
|
|
61
|
+
} else if (rest.startsWith('.')) {
|
|
62
|
+
const named = dottedName(rest.slice(1));
|
|
63
|
+
if (!named) throw invalid();
|
|
64
|
+
step = { selector: { kind: 'name', name: named.name }, remaining: named.remaining };
|
|
65
|
+
} else if (rest.startsWith('[*]')) {
|
|
66
|
+
step = { selector: { kind: 'wildcard' }, remaining: rest.slice(3) };
|
|
46
67
|
} else {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
name = rest.slice(index, end);
|
|
51
|
-
consumed = end - index;
|
|
68
|
+
const named = quotedName(rest);
|
|
69
|
+
if (!named) throw invalid();
|
|
70
|
+
step = { selector: { kind: 'name', name: named.name }, remaining: named.remaining };
|
|
52
71
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
else if (name !== null) selectors.push({ kind: 'descendant_name', name });
|
|
56
|
-
else if (index === rest.length) selectors.push({ kind: 'descendant_all' });
|
|
57
|
-
else throw invalid();
|
|
72
|
+
selectors.push(step.selector);
|
|
73
|
+
rest = step.remaining;
|
|
58
74
|
}
|
|
59
75
|
return new PathPattern(selectors, [...skipKeys]);
|
|
60
76
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* How a schema problem is worded: the field it names and the sentence about it. */
|
|
4
|
+
|
|
5
|
+
const { descriptionText } = require('./requirement-markers');
|
|
6
|
+
|
|
7
|
+
const DEPENDENCY_KEYWORDS = ['dependencies', 'dependentRequired', 'dependentSchemas'];
|
|
8
|
+
const SCHEMA_MAP_KEYWORDS = [
|
|
9
|
+
'$defs',
|
|
10
|
+
'definitions',
|
|
11
|
+
'dependencies',
|
|
12
|
+
'dependentSchemas',
|
|
13
|
+
'patternProperties',
|
|
14
|
+
'properties',
|
|
15
|
+
];
|
|
16
|
+
const DEFINITION_KEYWORDS = ['$defs', 'definitions'];
|
|
17
|
+
const UNSTATED_CONDITION = 'a condition the schema states';
|
|
18
|
+
|
|
19
|
+
const hasOwn = (object, field) => Object.prototype.hasOwnProperty.call(object, field);
|
|
20
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
21
|
+
|
|
22
|
+
function unescapePointer(step) {
|
|
23
|
+
return step.replaceAll('~1', '/').replaceAll('~0', '~');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function escapePointer(key) {
|
|
27
|
+
return key.replaceAll('~', '~0').replaceAll('/', '~1');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function schemaNodeAt(root, pointer) {
|
|
31
|
+
if (pointer === '') return root;
|
|
32
|
+
let node = root;
|
|
33
|
+
for (const step of pointer.slice(1).split('/')) {
|
|
34
|
+
const key = unescapePointer(step);
|
|
35
|
+
if (node === null || typeof node !== 'object' || !hasOwn(node, key)) return undefined;
|
|
36
|
+
node = node[key];
|
|
37
|
+
}
|
|
38
|
+
return node;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The field a problem names: the key of the value at `pointer` in `document`, then
|
|
43
|
+
* `[<index>]` for each list index below it; the root is `the document`.
|
|
44
|
+
*/
|
|
45
|
+
function fieldName(document, pointer) {
|
|
46
|
+
if (pointer === '') return 'the document';
|
|
47
|
+
const steps = [];
|
|
48
|
+
let node = document;
|
|
49
|
+
for (const step of pointer.slice(1).split('/')) {
|
|
50
|
+
const key = unescapePointer(step);
|
|
51
|
+
const inList = Array.isArray(node);
|
|
52
|
+
if (inList) node = node[Number(key)];
|
|
53
|
+
else node = isObject(node) && hasOwn(node, key) ? node[key] : undefined;
|
|
54
|
+
steps.push({ key, inList });
|
|
55
|
+
}
|
|
56
|
+
let keyAt = steps.length - 1;
|
|
57
|
+
while (keyAt >= 0 && steps[keyAt].inList) keyAt -= 1;
|
|
58
|
+
const key = keyAt >= 0 ? steps[keyAt].key : '';
|
|
59
|
+
const indexes = steps.slice(keyAt + 1).map((step) => `[${step.key}]`).join('');
|
|
60
|
+
return `\`${key}${indexes}\``;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A JSON number as a report writes it. */
|
|
64
|
+
function numberText(number) {
|
|
65
|
+
return String(number);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The sentence of an alternative none of whose forms holds: the first sentence of
|
|
70
|
+
* the definition holding the alternatives, where they are a described
|
|
71
|
+
* definition's, `#/$defs/<name>` or `#/definitions/<name>`; else `null`.
|
|
72
|
+
*/
|
|
73
|
+
function describedForm(schema, holderPointer) {
|
|
74
|
+
const steps = holderPointer.split('/').slice(1);
|
|
75
|
+
if (steps.length !== 2 || !DEFINITION_KEYWORDS.includes(steps[0])) return null;
|
|
76
|
+
const description = descriptionText(schemaNodeAt(schema, holderPointer));
|
|
77
|
+
if (!description) return null;
|
|
78
|
+
const sentence = description.split(/(?<=\.)\s/u)[0];
|
|
79
|
+
const firstSentence = sentence.endsWith('.') ? sentence.slice(0, -1) : sentence;
|
|
80
|
+
const leadingWord = /^[A-Za-z0-9]*/u.exec(firstSentence)[0];
|
|
81
|
+
if (leadingWord.length >= 2 && /^[A-Z]+$/u.test(leadingWord)) return firstSentence;
|
|
82
|
+
const [first, ...rest] = Array.from(firstSentence);
|
|
83
|
+
return first === undefined ? null : `${first.toLowerCase()}${rest.join('')}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The dependency keyword's pointer and the key it names, `{ keywordPointer, key }`,
|
|
88
|
+
* where `keywordPointer` lies inside a dependency: the key read from the pointer,
|
|
89
|
+
* or, for a dependency listing fields, from the field `holder` misses; else `null`.
|
|
90
|
+
*/
|
|
91
|
+
function dependencyOf(schema, keywordPointer, holder, missingField) {
|
|
92
|
+
const steps = keywordPointer.split('/').slice(1).map(unescapePointer);
|
|
93
|
+
let at = steps.length - 1;
|
|
94
|
+
while (at >= 0 && !(DEPENDENCY_KEYWORDS.includes(steps[at])
|
|
95
|
+
&& (at === 0 || !SCHEMA_MAP_KEYWORDS.includes(steps[at - 1])))) at -= 1;
|
|
96
|
+
if (at < 0) return null;
|
|
97
|
+
const dependencyPointer = steps.slice(0, at + 1)
|
|
98
|
+
.map((step) => `/${escapePointer(step)}`)
|
|
99
|
+
.join('');
|
|
100
|
+
let key = steps[at + 1];
|
|
101
|
+
if (key === undefined) {
|
|
102
|
+
const listed = schemaNodeAt(schema, dependencyPointer);
|
|
103
|
+
if (typeof missingField !== 'string' || !isObject(listed)) return null;
|
|
104
|
+
key = Object.keys(listed).find((candidate) => isObject(holder) && hasOwn(holder, candidate)
|
|
105
|
+
&& Array.isArray(listed[candidate]) && listed[candidate].includes(missingField));
|
|
106
|
+
if (key === undefined) return null;
|
|
107
|
+
}
|
|
108
|
+
if (!isObject(holder) || !hasOwn(holder, key)) return null;
|
|
109
|
+
return { keywordPointer: dependencyPointer, key };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* `` `key` needs <what> ``: what each dependency of the node holding
|
|
114
|
+
* `keywordPointer` states for `key`.
|
|
115
|
+
*/
|
|
116
|
+
function dependencyMessage(schema, keywordPointer, key) {
|
|
117
|
+
const holder = schemaNodeAt(schema, keywordPointer.slice(0, keywordPointer.lastIndexOf('/')));
|
|
118
|
+
const statesKey = (keyword) => isObject(holder)
|
|
119
|
+
&& isObject(holder[keyword])
|
|
120
|
+
&& hasOwn(holder[keyword], key);
|
|
121
|
+
const neededTexts = DEPENDENCY_KEYWORDS
|
|
122
|
+
.filter(statesKey)
|
|
123
|
+
.map((keyword) => neededText(holder[keyword][key]));
|
|
124
|
+
return `\`${key}\` needs ${neededTexts.length > 0 ? neededTexts.join('; ') : UNSTATED_CONDITION}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function neededText(needed) {
|
|
128
|
+
if (Array.isArray(needed)) return keyList(needed);
|
|
129
|
+
return consequenceText(needed) || UNSTATED_CONDITION;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function consequenceText(node) {
|
|
133
|
+
if (!isObject(node)) return null;
|
|
134
|
+
const parts = [];
|
|
135
|
+
if (Array.isArray(node.required) && node.required.length > 0) parts.push(keyList(node.required));
|
|
136
|
+
if (isObject(node.not) && Array.isArray(node.not.required) && node.not.required.length > 0) {
|
|
137
|
+
parts.push(`${keyList(node.not.required)} must not be set`);
|
|
138
|
+
}
|
|
139
|
+
const properties = isObject(node.properties) ? node.properties : {};
|
|
140
|
+
for (const [field, fieldSchema] of Object.entries(properties)) {
|
|
141
|
+
if (isObject(fieldSchema) && typeof fieldSchema.$ref === 'string') {
|
|
142
|
+
parts.push(`\`${field}\` is a \`${referenceName(fieldSchema.$ref)}\``);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const part of Array.isArray(node.allOf) ? node.allOf : []) {
|
|
146
|
+
const partText = consequenceText(part);
|
|
147
|
+
if (partText) parts.push(partText);
|
|
148
|
+
}
|
|
149
|
+
const alternatives = ['anyOf', 'oneOf']
|
|
150
|
+
.flatMap((keyword) => (Array.isArray(node[keyword]) ? node[keyword] : []));
|
|
151
|
+
if (alternatives.length > 0) {
|
|
152
|
+
parts.push(alternatives
|
|
153
|
+
.map((alternative) => consequenceText(alternative) || UNSTATED_CONDITION)
|
|
154
|
+
.join(', or '));
|
|
155
|
+
}
|
|
156
|
+
if (parts.length === 0 && typeof node.$ref === 'string') {
|
|
157
|
+
parts.push(`a \`${referenceName(node.$ref)}\``);
|
|
158
|
+
}
|
|
159
|
+
return parts.length > 0 ? parts.join('; ') : null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function referenceName(reference) {
|
|
163
|
+
return reference.slice(reference.lastIndexOf('/') + 1).replace(/^#/u, '');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function keyList(keys) {
|
|
167
|
+
const spans = keys.map((key) => `\`${typeof key === 'string' ? key : JSON.stringify(key)}\``);
|
|
168
|
+
return spans.length > 1 ? `${spans.slice(0, -1).join(', ')} and ${spans.at(-1)}` : spans.join('');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The indexes of the duplicate items a list reports, later first, `[later, earlier]`:
|
|
173
|
+
* from the last item back, the first one equal to an item after it, and the
|
|
174
|
+
* nearest such item; `null` for a list without duplicates.
|
|
175
|
+
*/
|
|
176
|
+
function duplicateItems(items) {
|
|
177
|
+
for (let earlier = items.length - 1; earlier >= 0; earlier -= 1) {
|
|
178
|
+
for (let later = earlier + 1; later < items.length; later += 1) {
|
|
179
|
+
if (jsonEqual(items[earlier], items[later])) return [later, earlier];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function jsonEqual(first, second) {
|
|
186
|
+
if (Array.isArray(first) && Array.isArray(second)) {
|
|
187
|
+
return first.length === second.length
|
|
188
|
+
&& first.every((item, index) => jsonEqual(item, second[index]));
|
|
189
|
+
}
|
|
190
|
+
if (isObject(first) && isObject(second)) {
|
|
191
|
+
const keys = Object.keys(first);
|
|
192
|
+
return keys.length === Object.keys(second).length
|
|
193
|
+
&& keys.every((key) => hasOwn(second, key) && jsonEqual(first[key], second[key]));
|
|
194
|
+
}
|
|
195
|
+
return first === second;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* `must contain at least <min> valid item(s)`, with `and no more than <max>`
|
|
200
|
+
* where `holder` bounds it.
|
|
201
|
+
*/
|
|
202
|
+
function containsMessage(holder) {
|
|
203
|
+
const bound = (keyword) => (isObject(holder) && typeof holder[keyword] === 'number'
|
|
204
|
+
? numberText(holder[keyword])
|
|
205
|
+
: null);
|
|
206
|
+
const minimum = bound('minContains') || '1';
|
|
207
|
+
const maximum = bound('maxContains');
|
|
208
|
+
return maximum === null
|
|
209
|
+
? `must contain at least ${minimum} valid item(s)`
|
|
210
|
+
: `must contain at least ${minimum} and no more than ${maximum} valid item(s)`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
fieldName,
|
|
215
|
+
numberText,
|
|
216
|
+
describedForm,
|
|
217
|
+
dependencyOf,
|
|
218
|
+
dependencyMessage,
|
|
219
|
+
duplicateItems,
|
|
220
|
+
containsMessage,
|
|
221
|
+
schemaNodeAt,
|
|
222
|
+
escapePointer,
|
|
223
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Requirement markers: a field's `description` opening with `[required]`,
|
|
5
|
+
* `[~required]` or `[optional]` decides whether the field is required, over its
|
|
6
|
+
* parent's `required` array.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const REQUIRED_MARKER = '[required]';
|
|
10
|
+
const REQUIREMENT_MARKERS = [REQUIRED_MARKER, '[~required]', '[optional]'];
|
|
11
|
+
const CONDITION_KEYWORDS = new Set(['if', 'not']);
|
|
12
|
+
const FORM_SELECTING_KEYWORDS = new Set(['anyOf', 'oneOf']);
|
|
13
|
+
const SAME_INSTANCE_SCHEMA_KEYWORDS = new Set(['else', 'if', 'not', 'then']);
|
|
14
|
+
const SAME_INSTANCE_LIST_KEYWORDS = new Set(['allOf', 'anyOf', 'oneOf']);
|
|
15
|
+
const SCHEMA_MAP_KEYWORDS = new Set([
|
|
16
|
+
'$defs',
|
|
17
|
+
'definitions',
|
|
18
|
+
'dependentSchemas',
|
|
19
|
+
'patternProperties',
|
|
20
|
+
'properties',
|
|
21
|
+
]);
|
|
22
|
+
const DRAFT_KEYWORDS = {
|
|
23
|
+
'draft-07': {
|
|
24
|
+
childSchema: new Set([
|
|
25
|
+
'additionalItems',
|
|
26
|
+
'additionalProperties',
|
|
27
|
+
'contains',
|
|
28
|
+
'items',
|
|
29
|
+
'propertyNames',
|
|
30
|
+
]),
|
|
31
|
+
schemaMap: new Set([
|
|
32
|
+
'$defs',
|
|
33
|
+
'definitions',
|
|
34
|
+
'dependencies',
|
|
35
|
+
'patternProperties',
|
|
36
|
+
'properties',
|
|
37
|
+
]),
|
|
38
|
+
},
|
|
39
|
+
'draft 2019-09': {
|
|
40
|
+
childSchema: new Set([
|
|
41
|
+
'additionalItems',
|
|
42
|
+
'additionalProperties',
|
|
43
|
+
'contains',
|
|
44
|
+
'contentSchema',
|
|
45
|
+
'items',
|
|
46
|
+
'propertyNames',
|
|
47
|
+
'unevaluatedItems',
|
|
48
|
+
'unevaluatedProperties',
|
|
49
|
+
]),
|
|
50
|
+
schemaMap: SCHEMA_MAP_KEYWORDS,
|
|
51
|
+
},
|
|
52
|
+
'draft 2020-12': {
|
|
53
|
+
childSchema: new Set([
|
|
54
|
+
'additionalProperties',
|
|
55
|
+
'contains',
|
|
56
|
+
'contentSchema',
|
|
57
|
+
'items',
|
|
58
|
+
'prefixItems',
|
|
59
|
+
'propertyNames',
|
|
60
|
+
'unevaluatedItems',
|
|
61
|
+
'unevaluatedProperties',
|
|
62
|
+
]),
|
|
63
|
+
schemaMap: SCHEMA_MAP_KEYWORDS,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
68
|
+
|
|
69
|
+
/** The marker a schema node's `description` opens with, or `null`. */
|
|
70
|
+
function requirementMarker(node) {
|
|
71
|
+
if (!isObject(node) || typeof node.description !== 'string') return null;
|
|
72
|
+
const description = node.description.trimStart();
|
|
73
|
+
return REQUIREMENT_MARKERS.find((marker) => description.startsWith(marker)) || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A node's description without the marker it opens with, or `null`. */
|
|
77
|
+
function descriptionText(node) {
|
|
78
|
+
if (!isObject(node) || typeof node.description !== 'string') return null;
|
|
79
|
+
const marker = requirementMarker(node);
|
|
80
|
+
const text = marker ? node.description.trimStart().slice(marker.length) : node.description;
|
|
81
|
+
return text.trim() || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `schema`, read in the draft named `draftName`, with each `required` array decided by markers. */
|
|
85
|
+
function markedSchema(schema, draftName) {
|
|
86
|
+
return markedNode(schema, null, DRAFT_KEYWORDS[draftName]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function markersOfFields(properties) {
|
|
90
|
+
const markers = new Map();
|
|
91
|
+
for (const [key, field] of Object.entries(properties)) {
|
|
92
|
+
const marker = requirementMarker(field);
|
|
93
|
+
if (marker) markers.set(key, marker);
|
|
94
|
+
}
|
|
95
|
+
return markers;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function markedNode(node, inheritedMarkers, keywords) {
|
|
99
|
+
if (!isObject(node)) return node;
|
|
100
|
+
const ownMarkers = isObject(node.properties) ? markersOfFields(node.properties) : null;
|
|
101
|
+
const markers = ownMarkers || inheritedMarkers;
|
|
102
|
+
const marked = {};
|
|
103
|
+
const markedChild = (child) => markedNode(child, null, keywords);
|
|
104
|
+
for (const [keyword, value] of Object.entries(node)) {
|
|
105
|
+
if (keyword === 'required') continue;
|
|
106
|
+
if (CONDITION_KEYWORDS.has(keyword)) marked[keyword] = value;
|
|
107
|
+
else if (SAME_INSTANCE_SCHEMA_KEYWORDS.has(keyword)) {
|
|
108
|
+
marked[keyword] = markedNode(value, markers, keywords);
|
|
109
|
+
} else if (SAME_INSTANCE_LIST_KEYWORDS.has(keyword) && Array.isArray(value)) {
|
|
110
|
+
const branchMarkers = FORM_SELECTING_KEYWORDS.has(keyword) ? null : markers;
|
|
111
|
+
marked[keyword] = value.map((branch) => markedNode(branch, branchMarkers, keywords));
|
|
112
|
+
} else if (keywords.childSchema.has(keyword)) {
|
|
113
|
+
marked[keyword] = Array.isArray(value) ? value.map(markedChild) : markedChild(value);
|
|
114
|
+
} else if (keywords.schemaMap.has(keyword) && isObject(value)) {
|
|
115
|
+
marked[keyword] = Object.fromEntries(Object.entries(value)
|
|
116
|
+
.map(([name, schema]) => [name, markedChild(schema)]));
|
|
117
|
+
} else {
|
|
118
|
+
marked[keyword] = value;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const listed = Array.isArray(node.required) ? node.required : [];
|
|
122
|
+
const required = listed.filter((key) => !markers || !markers.has(key));
|
|
123
|
+
for (const [key, marker] of ownMarkers || []) if (marker === REQUIRED_MARKER) required.push(key);
|
|
124
|
+
if (required.length > 0) marked.required = required;
|
|
125
|
+
return marked;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { requirementMarker, descriptionText, markedSchema };
|