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.
package/README.md CHANGED
@@ -1,15 +1,28 @@
1
1
  # yaml-dsl-spec
2
2
 
3
- The YAML DSL standard in JavaScript, from [yaml-dsl-spec](https://github.com/litebitus/yaml-dsl-spec).
3
+ The YAML DSL standard in JavaScript, from [yaml-dsl-spec](https://github.com/litebitus/yaml-dsl-spec). It reads as the Rust crate does: both pass the repo's conformance cases.
4
4
 
5
5
  ```js
6
- const { yamlDslSchemas, functionTableSchemas } = require('yaml-dsl-spec');
7
- const schema = yamlDslSchemas['1'];
6
+ const { readDslDefinitions } = require('yaml-dsl-spec');
7
+ const read = readDslDefinitions('yaml-dsl.yml', text);
8
+ if (read.problems) for (const problem of read.problems) console.error(String(problem));
9
+ else for (const { definition } of read.dsls) console.log(definition.id);
8
10
  ```
9
11
 
10
12
  | Export | Is |
11
13
  |---|---|
12
- | `yamlDslSchemas` | the JSON Schemas of `yaml-dsl.yml`, by version |
13
- | `functionTableSchemas` | the JSON Schemas of a DSL's function table, `x-yaml-dsl-functions`, by version |
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 |
14
27
 
15
28
  A version is a minor, such as `1.0`, or a major, such as `1`, which is its newest minor in the package.
package/index.js CHANGED
@@ -1,19 +1,54 @@
1
1
  'use strict';
2
2
 
3
- const { packagedVersionFiles } = require('./schema-versions');
3
+ /*
4
+ * The YAML DSL standard in JavaScript: the JSON Schemas of `yaml-dsl.yml` and of a
5
+ * DSL's function table at each of their versions, a reader that returns a
6
+ * `yaml-dsl.yml`'s DSL definitions fully validated, and the YAML reading,
7
+ * diagnostics and schema validation it and every reader of a DSL stand on.
8
+ *
9
+ * A version is a minor, such as `1.0`, or a major, such as `1`, which stands for
10
+ * the newest minor of that major the package carries.
11
+ */
4
12
 
5
- /** Each schema family's export, by the family's file stem. */
6
- const FAMILIES = {
7
- yamlDslSchemas: 'yaml-dsl',
8
- functionTableSchemas: 'x-yaml-dsl-functions',
9
- };
10
-
11
- /** A family's schemas by version: a minor such as `1.0`, or a major such as `1`, its newest minor. */
12
- function schemasOf(family) {
13
- return Object.fromEntries(
14
- packagedVersionFiles(family).map(([version, file]) => [version, require(file)]));
15
- }
13
+ const schemaCatalog = require('./lib/schema-catalog');
14
+ const { readDslDefinitions } = require('./lib/definition-reader');
15
+ const { Diagnostic } = require('./lib/diagnostic-report');
16
+ const { Position } = require('./lib/source-position');
17
+ const { Node, numberJson } = require('./lib/yaml-node');
18
+ const functionTable = require('./lib/function-table');
19
+ const dslDefinition = require('./lib/dsl-definition');
20
+ const jsonPath = require('./lib/json-path');
21
+ const resolved = require('./lib/resolved-value');
22
+ const schemaValidation = require('./lib/schema-validation');
23
+ const yamlReader = require('./lib/yaml-reader');
16
24
 
17
- module.exports = Object.fromEntries(
18
- Object.entries(FAMILIES).map(([name, family]) => [name, schemasOf(family)]));
19
- module.exports.FAMILIES = FAMILIES;
25
+ module.exports = {
26
+ readDslDefinitions,
27
+ yamlDslSchema: schemaCatalog.yamlDslSchema,
28
+ functionTableSchema: schemaCatalog.functionTableSchema,
29
+ YAML_DSL_VERSIONS: schemaCatalog.YAML_DSL_VERSIONS,
30
+ FUNCTION_TABLE_VERSIONS: schemaCatalog.FUNCTION_TABLE_VERSIONS,
31
+ yamlDslSchemas: schemaCatalog.yamlDslSchemas,
32
+ functionTableSchemas: schemaCatalog.functionTableSchemas,
33
+ FAMILIES: schemaCatalog.FAMILIES,
34
+ Diagnostic,
35
+ Position,
36
+ Node,
37
+ numberJson,
38
+ parseCallKey: functionTable.parseCallKey,
39
+ closestName: schemaValidation.closestName,
40
+ yamlReader: { parseYaml: yamlReader.parseYaml },
41
+ dslDefinition: { dslDefinitionFromJson: dslDefinition.dslDefinitionFromJson },
42
+ jsonPath: { PathPattern: jsonPath.PathPattern, PathSet: jsonPath.PathSet },
43
+ resolved: { Value: resolved.Value, reference: resolved.reference, call: resolved.call },
44
+ functionTable: {
45
+ FUNCTION_TABLE_POINTER: functionTable.FUNCTION_TABLE_POINTER,
46
+ FunctionTable: functionTable.FunctionTable,
47
+ MacroEntry: functionTable.MacroEntry,
48
+ },
49
+ schemaValidation: {
50
+ SchemaCache: schemaValidation.SchemaCache,
51
+ LoadedSchema: schemaValidation.LoadedSchema,
52
+ childValue: schemaValidation.childValue,
53
+ },
54
+ };
@@ -0,0 +1,397 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * Reads a `yaml-dsl.yml`: checks its `version`, validates it against that
5
+ * version's schema, checks what the schema cannot state, and returns each DSL's
6
+ * definition, or every problem at the field it is about.
7
+ */
8
+
9
+ const { Diagnostic } = require('./diagnostic-report');
10
+ const { dslDefinitionFromJson } = require('./dsl-definition');
11
+ const { PathPattern } = require('./json-path');
12
+ const { compilePattern, groupNamesOf } = require('./pattern-dialect');
13
+ const { Position } = require('./source-position');
14
+ const { Value } = require('./resolved-value');
15
+ const { SchemaCache, closestName, nodeJsonText } = require('./schema-validation');
16
+ const { parseYaml } = require('./yaml-reader');
17
+ const { YAML_DSL_VERSIONS, yamlDslSchemaFile } = require('./schema-catalog');
18
+
19
+ /** The text in a `common_layer_files` name that stands for a concern. */
20
+ const CONCERN_TOKEN = '{concern}';
21
+ const PATH_ENTRY_FIELDS = [['placeholder', 'unscanned_paths'], ['function', 'calls_not_allowed_at'], ['key_sort_orders']];
22
+
23
+ /**
24
+ * Each DSL of the `yaml-dsl.yml` at `file`, whose text is `text`, validated, as
25
+ * `{ dsls }`, each `{ definition, block }`; or every problem found, each at the
26
+ * field it is about, as `{ problems }`.
27
+ */
28
+ function readDslDefinitions(file, text) {
29
+ const parsed = parseYaml(file, text, []);
30
+ if (parsed.problem) return { problems: [parsed.problem] };
31
+ const root = parsed.node;
32
+ const version = versionSchema(root, file);
33
+ if (version.problems) return version;
34
+ let schema;
35
+ try {
36
+ schema = new SchemaCache().load(version.file, version.text);
37
+ } catch (error) {
38
+ return { problems: [Diagnostic.unplaced(error.message)] };
39
+ }
40
+ const schemaProblems = schema.validate(Value.fromNode(root), []);
41
+ if (schemaProblems.length > 0) return { problems: schemaProblems };
42
+ const blocks = (root.get('dsls') && root.get('dsls').items()) || [];
43
+ const problems = [];
44
+ const dsls = [];
45
+ blocks.forEach((block, index) => {
46
+ const repeated = repeatedId(block, blocks.slice(0, index));
47
+ if (repeated) problems.push(repeated);
48
+ const blockProblems = new BlockChecks(block).problems();
49
+ if (blockProblems.length > 0) {
50
+ problems.push(...blockProblems);
51
+ return;
52
+ }
53
+ try {
54
+ dsls.push({ definition: dslDefinitionFromJson(block.toJson()), block });
55
+ } catch (error) {
56
+ problems.push(Diagnostic.at(block.position, error.message));
57
+ }
58
+ });
59
+ return problems.length === 0 ? { dsls } : { problems };
60
+ }
61
+
62
+ /** The file name and text of the schema the file's `version` names, or its problem. */
63
+ function versionSchema(root, file) {
64
+ const listed = YAML_DSL_VERSIONS.map((version) => `\`"${version}"\``).join(', ');
65
+ const versionNode = root.get('version');
66
+ if (versionNode === null) {
67
+ return {
68
+ problems: [
69
+ Diagnostic.at(Position.fileStart(file), 'field `version` is required')
70
+ .withLabel(`\`version\` is one of ${listed}`),
71
+ ],
72
+ };
73
+ }
74
+ const version = versionNode.asStr() || '';
75
+ const found = yamlDslSchemaFile(version);
76
+ if (found) return found;
77
+ const shown = versionNode.asStr() === null
78
+ ? `\`${nodeJsonText(versionNode)}\``
79
+ : `\`${versionNode.asStr()}\``;
80
+ const close = closestName(version, YAML_DSL_VERSIONS);
81
+ return {
82
+ problems: [
83
+ Diagnostic.at(versionNode.position, `${shown} is not a version this reader reads`)
84
+ .withLabel(`\`version\` is one of ${listed}`)
85
+ .withHelp(close === null ? null : `did you mean \`"${close}"\`?`),
86
+ ],
87
+ };
88
+ }
89
+
90
+ /** A second DSL of the file with the same `id`. */
91
+ function repeatedId(block, earlier) {
92
+ const idNode = block.get('id');
93
+ const id = idNode && idNode.asStr();
94
+ if (id === null || id === undefined) return null;
95
+ const repeated = earlier.some((other) => other.get('id') && other.get('id').asStr() === id);
96
+ return repeated
97
+ ? Diagnostic.at(idNode.position, `DSL \`${id}\` is declared more than once`)
98
+ .withLabel('an earlier DSL of this file has this `id`')
99
+ : null;
100
+ }
101
+
102
+ /** Whether a field is written `true`. */
103
+ function isTrue(node) {
104
+ return node !== null && node.kind === 'bool' && node.value === true;
105
+ }
106
+
107
+ /** The text of the field `field` of `node`, `null` where it holds none. */
108
+ function textAt(node, field) {
109
+ const found = node.get(field);
110
+ return found === null ? null : found.asStr();
111
+ }
112
+
113
+ /** The node at `steps` of map keys below `node`, or `null`. */
114
+ function nodeAt(node, steps) {
115
+ let current = node;
116
+ for (const step of steps) {
117
+ current = current.get(step);
118
+ if (current === null) return null;
119
+ }
120
+ return current;
121
+ }
122
+
123
+ /** The items of the list at `steps`, none where there is no list. */
124
+ function listAt(node, steps) {
125
+ const found = nodeAt(node, steps);
126
+ return (found && found.items()) || [];
127
+ }
128
+
129
+ /** The values of the map at `steps`, none where there is no map. */
130
+ function mapValuesAt(node, steps) {
131
+ const found = nodeAt(node, steps);
132
+ return ((found && found.entries()) || []).map((entry) => entry.value);
133
+ }
134
+
135
+ /**
136
+ * The checks of one DSL block that its schema cannot state. The block has
137
+ * already passed the schema, so every field has its shape.
138
+ */
139
+ class BlockChecks {
140
+ constructor(block) {
141
+ this.block = block;
142
+ this.found = [];
143
+ }
144
+
145
+ problems() {
146
+ this.patterns();
147
+ this.paths();
148
+ this.repeatedPathEntries();
149
+ this.keySortOrders();
150
+ this.commonLayerFiles();
151
+ this.scopeNames();
152
+ this.localsAndLoops();
153
+ this.tagKeys();
154
+ return this.found;
155
+ }
156
+
157
+ report(node, message, label) {
158
+ this.found.push(Diagnostic.at(node.position, message).withLabel(label));
159
+ }
160
+
161
+ /** Each regular expression compiles, and holds the named groups its rule reads. */
162
+ patterns() {
163
+ const placeholder = nodeAt(this.block, ['placeholder', 'pattern']);
164
+ if (placeholder) this.pattern(placeholder, [['body', "a placeholder's body"]]);
165
+ for (const rule of listAt(this.block, ['references'])) {
166
+ const pattern = rule.get('pattern');
167
+ if (!pattern) continue;
168
+ const groups = [];
169
+ const name = textAt(rule, 'name_group');
170
+ if (name !== null) groups.push([name, 'the name, by `name_group`']);
171
+ const scope = textAt(rule, 'scope_group');
172
+ if (scope !== null) groups.push([scope, 'the scope, by `scope_group`']);
173
+ this.pattern(pattern, groups);
174
+ }
175
+ for (const rule of listAt(this.block, ['validator', 'child_values_from_parent_key'])) {
176
+ const pattern = rule.get('pattern');
177
+ if (pattern) this.pattern(pattern, [['field', 'the child field'], ['value', 'its value']]);
178
+ }
179
+ }
180
+
181
+ pattern(node, groups) {
182
+ const text = node.asStr();
183
+ if (text === null) return;
184
+ const compiled = compilePattern(text);
185
+ if (compiled === null) {
186
+ this.report(node, 'the pattern is not a regular expression', 'a regular expression the reader compiles');
187
+ return;
188
+ }
189
+ const names = groupNamesOf(compiled);
190
+ for (const [group, holds] of groups) {
191
+ if (!names.includes(group)) {
192
+ this.report(node, `the pattern has no \`(?<${group}>...)\` group`, `the group holds ${holds}`);
193
+ }
194
+ }
195
+ }
196
+
197
+ /** Each JSONPath is one of the subset the standard reads. */
198
+ paths() {
199
+ const nodes = [];
200
+ for (const rule of listAt(this.block, ['declarations'])) if (rule.get('path')) nodes.push(rule.get('path'));
201
+ for (const scope of mapValuesAt(this.block, ['scopes'])) nodes.push(...listAt(scope, ['regions']));
202
+ for (const field of PATH_ENTRY_FIELDS) {
203
+ for (const entry of listAt(this.block, field)) if (entry.get('path')) nodes.push(entry.get('path'));
204
+ }
205
+ for (const include of listAt(this.block, ['layers', 'overlay_includes'])) {
206
+ nodes.push(...listAt(include, ['key_paths']));
207
+ if (include.get('location_path')) nodes.push(include.get('location_path'));
208
+ }
209
+ for (const node of nodes) {
210
+ const text = node.asStr();
211
+ if (text === null) continue;
212
+ try {
213
+ PathPattern.parse(text, []);
214
+ } catch (error) {
215
+ this.report(node, error.message, "a path of the standard's JSONPath subset");
216
+ }
217
+ }
218
+ }
219
+
220
+ /** No list of path entries holds one path twice. */
221
+ repeatedPathEntries() {
222
+ for (const field of PATH_ENTRY_FIELDS) {
223
+ const entries = listAt(this.block, field);
224
+ entries.forEach((entry, index) => {
225
+ const path = entry.get('path');
226
+ if (!path) return;
227
+ const repeated = entries.slice(0, index).some((earlier) =>
228
+ (earlier.get('path') ? earlier.get('path').asStr() : null) === path.asStr());
229
+ if (repeated) {
230
+ this.report(
231
+ path,
232
+ `path \`${path.asStr() || ''}\` is listed more than once`,
233
+ `an earlier entry of \`${field.join('.')}\` has this path`,
234
+ );
235
+ }
236
+ });
237
+ }
238
+ }
239
+
240
+ /** An order's first and last keys are distinct, and alphabetical order names none. */
241
+ keySortOrders() {
242
+ for (const entry of listAt(this.block, ['key_sort_orders'])) {
243
+ const alphabetical = entry.get('order') !== null && entry.get('order').asStr() === 'alphabetical';
244
+ for (const field of ['first_keys', 'last_keys']) {
245
+ const keys = listAt(entry, [field]);
246
+ if (alphabetical && keys.length > 0) {
247
+ this.report(entry.get(field) || entry, `\`${field}\` is not read with alphabetical order`, '`[]` under alphabetical order');
248
+ }
249
+ keys.forEach((key, index) => {
250
+ if (keys.slice(0, index).some((earlier) => earlier.asStr() === key.asStr())) {
251
+ this.report(key, `\`${field}\` lists a key more than once`, 'an earlier item is this key');
252
+ }
253
+ });
254
+ }
255
+ const last = listAt(entry, ['last_keys']);
256
+ for (const key of listAt(entry, ['first_keys'])) {
257
+ if (last.some((other) => other.asStr() === key.asStr())) {
258
+ this.report(key, 'a key is in both `first_keys` and `last_keys`', '`last_keys` lists it too');
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ /**
265
+ * The names a layer file's common layer may take: each a file name, each once,
266
+ * `{concern}` at most once in a name, one without it, and none with it where
267
+ * only an ancestor reaches a concern below its overlays.
268
+ */
269
+ commonLayerFiles() {
270
+ const discovery = nodeAt(this.block, ['layers', 'common_layer_discovery']);
271
+ const parentDiscovery = discovery !== null && discovery.asStr() === 'parent';
272
+ for (const files of mapValuesAt(this.block, ['layers', 'common_layer_files'])) {
273
+ const names = files.items() || [];
274
+ names.forEach((name, index) => {
275
+ const text = name.asStr() || '';
276
+ if (text === '' || text === '.' || text === '..' || text.includes('/') || text.includes('\\')) {
277
+ this.report(name, `\`${text}\` is not a file name`, 'a file name, with no folder');
278
+ }
279
+ if (names.slice(0, index).some((earlier) => earlier.asStr() === name.asStr())) {
280
+ this.report(name, `\`${text}\` is listed more than once`, 'an earlier item is this name');
281
+ }
282
+ if (text.split(CONCERN_TOKEN).length - 1 > 1) {
283
+ this.report(name, `\`${text}\` holds \`${CONCERN_TOKEN}\` more than once`, `a name holds \`${CONCERN_TOKEN}\` at most once`);
284
+ }
285
+ if (parentDiscovery && text.includes(CONCERN_TOKEN)) {
286
+ this.report(
287
+ name,
288
+ `\`${text}\` names a concern's common layer`,
289
+ 'only `common_layer_discovery: ancestor` reaches a concern below its overlays',
290
+ );
291
+ }
292
+ });
293
+ const allConcern = names.length > 0 && names.every((name) => (name.asStr() || '').includes(CONCERN_TOKEN));
294
+ if (allConcern) {
295
+ this.report(
296
+ files,
297
+ `every name holds \`${CONCERN_TOKEN}\``,
298
+ 'a name without it is the common layer of a concern above its overlays',
299
+ );
300
+ }
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Every scope a rule names is declared, and a reference names its scope by a
306
+ * group exactly when the scope is named by a parent key.
307
+ */
308
+ scopeNames() {
309
+ const scopes = this.scopes();
310
+ const declared = scopes.map(([name]) => name);
311
+ const named = [];
312
+ for (const rule of [...listAt(this.block, ['declarations']), ...listAt(this.block, ['references'])]) {
313
+ if (rule.get('scope_name')) named.push(rule.get('scope_name'));
314
+ }
315
+ for (const holder of ['locals', 'loops']) {
316
+ const scope = nodeAt(this.block, [holder, 'scope_name']);
317
+ if (scope) named.push(scope);
318
+ }
319
+ for (const node of named) {
320
+ const name = node.asStr() || '';
321
+ if (!declared.includes(name)) {
322
+ const close = closestName(name, declared);
323
+ this.found.push(Diagnostic.at(node.position, `scope \`${name}\` is not declared`)
324
+ .withLabel('`scopes` declares no scope of this name')
325
+ .withHelp(close === null ? null : `did you mean \`${close}\`?`));
326
+ }
327
+ }
328
+ for (const rule of listAt(this.block, ['references'])) {
329
+ const name = textAt(rule, 'scope_name');
330
+ if (name === null) continue;
331
+ const scope = scopes.find(([declaredName]) => declaredName === name);
332
+ if (!scope) continue;
333
+ const [, namedByParentKey] = scope;
334
+ const group = rule.get('scope_group');
335
+ const hasGroup = group !== null && group.asStr() !== null;
336
+ if (namedByParentKey && !hasGroup) {
337
+ this.report(group || rule, '`scope_group` is required', `scope \`${name}\` is named by a parent key`);
338
+ }
339
+ if (!namedByParentKey && hasGroup) {
340
+ this.report(group || rule, '`scope_group` must be null', `scope \`${name}\` is not named by a parent key`);
341
+ }
342
+ }
343
+ }
344
+
345
+ /** Locals have a rule declaring their names from keys and one reading them; loop variables have a rule reading them. */
346
+ localsAndLoops() {
347
+ const locals = nodeAt(this.block, ['locals', 'scope_name']);
348
+ if (locals) {
349
+ const name = locals.asStr() || '';
350
+ const declaredByKey = listAt(this.block, ['declarations']).some((rule) =>
351
+ rule.get('scope_name') !== null && rule.get('scope_name').asStr() === name
352
+ && rule.get('name_source') !== null && rule.get('name_source').asStr() === 'key'
353
+ && !isTrue(rule.get('declares_every_name')));
354
+ if (!declaredByKey) {
355
+ this.report(locals, `no declaration rule reads \`${name}\` names from keys`, 'a local is declared by its key');
356
+ }
357
+ this.readByReferenceRule(locals, name, 'a local');
358
+ }
359
+ const loops = nodeAt(this.block, ['loops', 'scope_name']);
360
+ if (loops) this.readByReferenceRule(loops, loops.asStr() || '', 'a loop variable');
361
+ }
362
+
363
+ readByReferenceRule(scope, name, what) {
364
+ const read = listAt(this.block, ['references']).some((rule) =>
365
+ rule.get('scope_name') !== null && rule.get('scope_name').asStr() === name);
366
+ if (!read) this.report(scope, `no reference rule reads \`${name}\``, `${what} is read by a reference rule of its scope`);
367
+ }
368
+
369
+ /** No tag, and no key, is listed twice. */
370
+ tagKeys() {
371
+ const entries = listAt(this.block, ['tag_keys']);
372
+ entries.forEach((entry, index) => {
373
+ for (const field of ['tag', 'key']) {
374
+ const node = entry.get(field);
375
+ if (!node) continue;
376
+ const repeated = entries.slice(0, index).some((earlier) =>
377
+ (earlier.get(field) ? earlier.get(field).asStr() : null) === node.asStr());
378
+ if (repeated) {
379
+ this.report(
380
+ node,
381
+ `${field} \`${node.asStr() || ''}\` is listed more than once`,
382
+ `an earlier entry of \`tag_keys\` has this ${field}`,
383
+ );
384
+ }
385
+ }
386
+ });
387
+ }
388
+
389
+ /** Each declared scope's name, and whether it is named by a parent key. */
390
+ scopes() {
391
+ const scopes = this.block.get('scopes');
392
+ return ((scopes && scopes.entries()) || []).map((entry) =>
393
+ [entry.key, isTrue(entry.value.get('named_by_parent_key'))]);
394
+ }
395
+ }
396
+
397
+ module.exports = { readDslDefinitions };
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ /** A problem found in a file, at the value it is about where there is one. */
4
+ class Diagnostic {
5
+ constructor(position, message) {
6
+ this.position = position;
7
+ this.message = message;
8
+ this.notes = null;
9
+ }
10
+
11
+ static at(position, message) {
12
+ return new Diagnostic(position, message);
13
+ }
14
+
15
+ static unplaced(message) {
16
+ return new Diagnostic(null, message);
17
+ }
18
+
19
+ /** What is wrong with the text at the position. */
20
+ withLabel(label) {
21
+ this.writableNotes().label = label;
22
+ return this;
23
+ }
24
+
25
+ /** The schema rule the document breaks: its keyword and where the schema holds it. */
26
+ withRule(rule) {
27
+ this.writableNotes().rule = rule;
28
+ return this;
29
+ }
30
+
31
+ /** How the problem may be fixed; nothing is noted for `null`. */
32
+ withHelp(help) {
33
+ if (help !== null && help !== undefined) this.writableNotes().help = help;
34
+ return this;
35
+ }
36
+
37
+ writableNotes() {
38
+ if (this.notes === null) this.notes = { label: null, rule: null, help: null };
39
+ return this.notes;
40
+ }
41
+
42
+ toString() {
43
+ return this.position === null ? this.message : `${this.position}: ${this.message}`;
44
+ }
45
+ }
46
+
47
+ module.exports = { Diagnostic };
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * A DSL definition: the shape of a DSL block of `yaml-dsl.yml`, its fields named
5
+ * as the standard names them. The fields read here mean what the specification
6
+ * says they mean; fields that only shape editing are not part of it, and are
7
+ * left out when a block holding them is read.
8
+ */
9
+
10
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
11
+
12
+ /** Reads one struct of the definition from `source`, by each field's reader. */
13
+ function readFields(source, fields) {
14
+ if (!isObject(source)) throw new Error('invalid type: expected a map');
15
+ const read = {};
16
+ for (const [field, readField] of Object.entries(fields)) read[field] = readField(source[field], field);
17
+ return read;
18
+ }
19
+
20
+ const required = (readValue) => (value, field) => {
21
+ if (value === undefined) throw new Error(`missing field \`${field}\``);
22
+ return readValue(value, field);
23
+ };
24
+ const optional = (readValue) => (value, field) =>
25
+ (value === undefined || value === null ? null : readValue(value, field));
26
+ const withDefault = (readValue, fallback) => (value, field) =>
27
+ (value === undefined ? fallback() : readValue(value, field));
28
+
29
+ function text(value, field) {
30
+ if (typeof value !== 'string') throw new Error(`invalid type for \`${field}\`: expected a string`);
31
+ return value;
32
+ }
33
+
34
+ function flag(value, field) {
35
+ if (typeof value !== 'boolean') throw new Error(`invalid type for \`${field}\`: expected a boolean`);
36
+ return value;
37
+ }
38
+
39
+ const listOf = (readItem) => (value, field) => {
40
+ if (!Array.isArray(value)) throw new Error(`invalid type for \`${field}\`: expected a sequence`);
41
+ return value.map((item) => readItem(item, field));
42
+ };
43
+
44
+ const mapOf = (readItem) => (value, field) => {
45
+ if (!isObject(value)) throw new Error(`invalid type for \`${field}\`: expected a map`);
46
+ const keys = Object.keys(value).sort((first, second) =>
47
+ Buffer.compare(Buffer.from(first, 'utf8'), Buffer.from(second, 'utf8')));
48
+ return Object.fromEntries(keys.map((key) => [key, readItem(value[key], field)]));
49
+ };
50
+
51
+ const oneOf = (variants) => (value, field) => {
52
+ if (!variants.includes(value)) {
53
+ throw new Error(`unknown variant \`${value}\` for \`${field}\`, expected one of ${variants.map((variant) => `\`${variant}\``).join(', ')}`);
54
+ }
55
+ return value;
56
+ };
57
+
58
+ const struct = (fields) => (value) => readFields(value, fields);
59
+ const emptyList = () => [];
60
+ const emptyMap = () => ({});
61
+
62
+ const PATH_ENTRY = struct({
63
+ path: required(text),
64
+ skip_keys: withDefault(listOf(text), emptyList),
65
+ });
66
+
67
+ const DSL_FIELDS = {
68
+ id: required(text),
69
+ file_includes: required(listOf(text)),
70
+ file_excludes: withDefault(listOf(text), emptyList),
71
+ schema_search_paths: withDefault(listOf(text), emptyList),
72
+ layers: optional(struct({
73
+ overlay_folders: withDefault(listOf(text), emptyList),
74
+ common_layer_discovery: required(oneOf(['parent', 'ancestor', 'overlay_includes'])),
75
+ common_layer_files: withDefault(mapOf(listOf(text)), emptyMap),
76
+ })),
77
+ placeholder: optional(struct({
78
+ pattern: required(text),
79
+ escape_prefix: withDefault(optional(text), () => null),
80
+ unscanned_paths: withDefault(listOf(PATH_ENTRY), emptyList),
81
+ })),
82
+ function: optional(struct({
83
+ definitions: withDefault(listOf(text), emptyList),
84
+ calls_not_allowed_at: withDefault(listOf(PATH_ENTRY), emptyList),
85
+ marker_function: optional(struct({
86
+ call_marker: required(text),
87
+ splat_operator: optional(text),
88
+ calls_without_name_allowed: required(flag),
89
+ })),
90
+ })),
91
+ locals: optional(struct({
92
+ scope_name: required(text),
93
+ path_separator: optional(text),
94
+ })),
95
+ loops: optional(struct({ scope_name: required(text) })),
96
+ scopes: withDefault(mapOf(struct({
97
+ regions: withDefault(listOf(text), emptyList),
98
+ later_items_of_declaring_list: withDefault(flag, () => false),
99
+ named_by_parent_key: withDefault(flag, () => false),
100
+ builtin_names: withDefault(listOf(text), emptyList),
101
+ })), emptyMap),
102
+ declarations: withDefault(listOf(struct({
103
+ path: required(text),
104
+ skip_keys: withDefault(listOf(text), emptyList),
105
+ exclude_candidates: withDefault(listOf(text), emptyList),
106
+ name_source: required(oneOf(['key', 'value', 'meta_argument'])),
107
+ key_token: optional(oneOf(['all_words', 'first_word', 'last_word'])),
108
+ name_spelling: required(oneOf(['as_written', 'dashes_as_underscores'])),
109
+ meta_argument_name: optional(text),
110
+ declares_every_name: withDefault(flag, () => false),
111
+ scope_name: required(text),
112
+ })), emptyList),
113
+ references: withDefault(listOf(struct({
114
+ pattern: required(text),
115
+ positions: required(listOf(oneOf([
116
+ 'whole_scalar',
117
+ 'whole_placeholder',
118
+ 'placeholder_in_text',
119
+ 'anywhere_in_scalar',
120
+ ]))),
121
+ text_after_name_allowed: required(flag),
122
+ scope_name: required(text),
123
+ scope_group: optional(text),
124
+ name_group: required(text),
125
+ })), emptyList),
126
+ tag_keys: withDefault(listOf(struct({ tag: required(text), key: required(text) })), emptyList),
127
+ validator: optional(struct({
128
+ child_values_from_parent_key: withDefault(listOf(struct({ pattern: required(text) })), emptyList),
129
+ })),
130
+ };
131
+
132
+ /** Reads a DSL block, such as one of a parsed `yaml-dsl.yml`, or throws why it is none. */
133
+ function dslDefinitionFromJson(block) {
134
+ return readFields(block, DSL_FIELDS);
135
+ }
136
+
137
+ module.exports = { dslDefinitionFromJson };