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,129 @@
1
+ 'use strict';
2
+
3
+ const FUNCTION_TABLE_POINTER = '/x-yaml-dsl-functions';
4
+ const FUNCTION_TABLE_FIELD = 'x-yaml-dsl-functions';
5
+ const MACRO_KINDS = new Set(['function_call', 'merge', 'loop', 'transform']);
6
+ const MERGE_BEHAVIORS = new Set(['shallow', 'deep', 'join']);
7
+ const ROLE_NAMES = ['callee', 'callee_arguments', 'content', 'loop_variables', 'collection', 'body'];
8
+ const FUNCTION_NAME_START = /^[A-Za-z_]/;
9
+ const FUNCTION_NAME_CHARACTERS = /^[A-Za-z0-9_:-]*$/;
10
+
11
+ const hasOwn = (object, field) => Object.prototype.hasOwnProperty.call(object, field);
12
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
13
+
14
+ /** A macro of the table: its `kind`, `mergeBehavior`, `nameSuffix` and its arguments by role. */
15
+ class MacroEntry {
16
+ constructor(kind, mergeBehavior, nameSuffix, roles) {
17
+ this.kind = kind;
18
+ this.mergeBehavior = mergeBehavior;
19
+ this.nameSuffix = nameSuffix;
20
+ this.roles = roles;
21
+ }
22
+
23
+ /** The name of the entry's argument that plays `role`. */
24
+ role(role) {
25
+ return hasOwn(this.roles, role) ? this.roles[role] : null;
26
+ }
27
+ }
28
+
29
+ /** The functions and macros a schema publishes at `#/x-yaml-dsl-functions`. */
30
+ class FunctionTable {
31
+ constructor(entries = new Map()) {
32
+ this.entries = entries;
33
+ }
34
+
35
+ /** The table `schema` publishes, or throws a message naming what is wrong with it. */
36
+ static fromSchema(schema) {
37
+ if (!isObject(schema) || !hasOwn(schema, FUNCTION_TABLE_FIELD)) return new FunctionTable();
38
+ const table = schema[FUNCTION_TABLE_FIELD];
39
+ if (!isObject(table)) {
40
+ throw new Error(`${FUNCTION_TABLE_POINTER} is not a mapping of function names`);
41
+ }
42
+ const entries = new Map(Object.entries(table)
43
+ .sort(([first], [second]) => compareText(first, second))
44
+ .map(([name, entry]) => [name, functionEntry(name, entry)]));
45
+ return new FunctionTable(entries);
46
+ }
47
+
48
+ /** The entry a call's function name calls, a suffixed macro name included. */
49
+ entry(functionName) {
50
+ const direct = this.entries.get(functionName);
51
+ if (direct && (!direct.macroEntry || !direct.macroEntry.nameSuffix)) return direct;
52
+ for (const [name, entry] of this.entries) {
53
+ const suffixPattern = entry.macroEntry && entry.macroEntry.nameSuffix;
54
+ if (!suffixPattern || !functionName.startsWith(name)) continue;
55
+ if (suffixPattern.test(functionName.slice(name.length))) return entry;
56
+ }
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /** Orders names by their UTF-8 bytes, as a sorted map of the crate holds them. */
62
+ function compareText(first, second) {
63
+ return Buffer.compare(Buffer.from(first, 'utf8'), Buffer.from(second, 'utf8'));
64
+ }
65
+
66
+ function functionEntry(name, entry) {
67
+ const listed = isObject(entry) ? entry.arguments : undefined;
68
+ if (!Array.isArray(listed)) throw new Error(`function \`${name}\` has no arguments list`);
69
+ const functionArguments = listed.map((argument) => {
70
+ const argumentName = isObject(argument) ? argument.name : undefined;
71
+ if (typeof argumentName !== 'string') throw new Error(`an argument of \`${name}\` has no name`);
72
+ return {
73
+ name: argumentName,
74
+ repeated: typeof argument.repeated === 'boolean' ? argument.repeated : false,
75
+ default: hasOwn(argument, 'default') ? argument.default : null,
76
+ };
77
+ });
78
+ const macroEntry = hasOwn(entry, 'macro') ? macroEntryOf(name, entry.macro) : null;
79
+ return { arguments: functionArguments, macroEntry };
80
+ }
81
+
82
+ function macroEntryOf(name, value) {
83
+ const kind = isObject(value) ? value.kind : undefined;
84
+ if (!MACRO_KINDS.has(kind)) {
85
+ const shown = typeof kind === 'string' ? `Some(${JSON.stringify(kind)})` : 'None';
86
+ throw new Error(`macro \`${name}\` has an unknown kind ${shown}`);
87
+ }
88
+ const written = typeof value.merge_behavior === 'string' ? value.merge_behavior : null;
89
+ if (written !== null && !MERGE_BEHAVIORS.has(written)) {
90
+ throw new Error(`macro \`${name}\` has an unknown merge_behavior \`${written}\``);
91
+ }
92
+ let nameSuffix = null;
93
+ if (typeof value.name_suffix_pattern === 'string') {
94
+ try {
95
+ nameSuffix = new RegExp(`^(?:${value.name_suffix_pattern})$`, 'u');
96
+ } catch (error) {
97
+ throw new Error(`macro \`${name}\`: ${error.message}`);
98
+ }
99
+ }
100
+ const roles = {};
101
+ for (const role of ROLE_NAMES) {
102
+ if (typeof value[role] === 'string') roles[role] = value[role];
103
+ }
104
+ return new MacroEntry(kind, written || 'shallow', nameSuffix, roles);
105
+ }
106
+
107
+ /**
108
+ * A call written in a key, `<name> <marker><function>[splat]` or the marker alone,
109
+ * as `{ name, function, splat }`; `null` for a key that writes none.
110
+ */
111
+ function parseCallKey(key, marker) {
112
+ const trimmed = key.trimEnd();
113
+ const space = trimmed.lastIndexOf(' ');
114
+ const callStart = space === -1 ? 0 : space + 1;
115
+ const callText = trimmed.slice(callStart);
116
+ if (!callText.startsWith(marker.call_marker)) return null;
117
+ const rest = callText.slice(marker.call_marker.length);
118
+ const splatOperator = marker.splat_operator;
119
+ const splat = typeof splatOperator === 'string' && rest.endsWith(splatOperator);
120
+ const functionName = splat ? rest.slice(0, rest.length - splatOperator.length) : rest;
121
+ if (!FUNCTION_NAME_START.test(functionName) || !FUNCTION_NAME_CHARACTERS.test(functionName)) return null;
122
+ return {
123
+ name: callStart > 0 ? trimmed.slice(0, callStart).trim() : null,
124
+ function: functionName,
125
+ splat,
126
+ };
127
+ }
128
+
129
+ module.exports = { FUNCTION_TABLE_POINTER, FunctionTable, MacroEntry, parseCallKey };
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+
3
+ const { Diagnostic } = require('./diagnostic-report');
4
+
5
+ /** One step of a concrete path into a document: `{ key }` or `{ index }`. */
6
+ function keyStep(key) {
7
+ return { key };
8
+ }
9
+
10
+ function indexStep(index) {
11
+ return { index };
12
+ }
13
+
14
+ /** A parsed path and the keys its wildcard steps do not select or descend into. */
15
+ class PathPattern {
16
+ constructor(selectors, skipKeys) {
17
+ this.selectors = selectors;
18
+ this.skipKeys = skipKeys;
19
+ }
20
+
21
+ /** The pattern `text` writes, or throws a `Diagnostic` naming why it is none. */
22
+ static parse(text, skipKeys = []) {
23
+ const invalid = () =>
24
+ Diagnostic.unplaced(`\`${text}\` is not a path of the supported JSONPath subset`);
25
+ if (!text.startsWith('$')) throw invalid();
26
+ const rest = text.slice(1);
27
+ const selectors = [];
28
+ let index = 0;
29
+ while (index < rest.length) {
30
+ const descendant = rest.startsWith('..', index);
31
+ if (descendant) index += 2;
32
+ else if (rest[index] === '.') index += 1;
33
+ let name;
34
+ let consumed;
35
+ if (rest.startsWith("['", index)) {
36
+ const close = rest.indexOf("']", index + 2);
37
+ if (close === -1) throw invalid();
38
+ name = rest.slice(index + 2, close);
39
+ consumed = close - index + 2;
40
+ } else if (rest.startsWith('[*]', index)) {
41
+ name = null;
42
+ consumed = 3;
43
+ } else if (rest.startsWith('*', index)) {
44
+ name = null;
45
+ consumed = 1;
46
+ } else {
47
+ const ends = [rest.indexOf('.', index), rest.indexOf('[', index)].filter((end) => end !== -1);
48
+ const end = ends.length > 0 ? Math.min(...ends) : rest.length;
49
+ if (end === index) throw invalid();
50
+ name = rest.slice(index, end);
51
+ consumed = end - index;
52
+ }
53
+ index += consumed;
54
+ if (!descendant) selectors.push(name === null ? { kind: 'wildcard' } : { kind: 'name', name });
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();
58
+ }
59
+ return new PathPattern(selectors, [...skipKeys]);
60
+ }
61
+
62
+ /** Every node of `document` the pattern selects, `{ path, value }`, in document order. */
63
+ select(document) {
64
+ const selected = [];
65
+ this.collect(document, [], selected);
66
+ return selected;
67
+ }
68
+
69
+ collect(value, path, selected) {
70
+ if (this.selects(path)) selected.push({ path: [...path], value });
71
+ if (value.kind === 'map') {
72
+ for (const entry of value.value) {
73
+ path.push(keyStep(entry.key));
74
+ this.collect(entry.value, path, selected);
75
+ path.pop();
76
+ }
77
+ } else if (value.kind === 'list') {
78
+ value.value.forEach((item, itemIndex) => {
79
+ path.push(indexStep(itemIndex));
80
+ this.collect(item, path, selected);
81
+ path.pop();
82
+ });
83
+ }
84
+ }
85
+
86
+ /** Whether the pattern selects the node at `path`. */
87
+ selects(path) {
88
+ return this.matchesFrom(0, path);
89
+ }
90
+
91
+ /** Whether `path` is the node a step selects or anywhere below it. */
92
+ covers(path) {
93
+ for (let length = 0; length <= path.length; length += 1) {
94
+ if (this.selects(path.slice(0, length))) return true;
95
+ }
96
+ return false;
97
+ }
98
+
99
+ skipped(step) {
100
+ return 'key' in step && this.skipKeys.includes(step.key);
101
+ }
102
+
103
+ matchesFrom(selectorIndex, path) {
104
+ const selector = this.selectors[selectorIndex];
105
+ if (!selector) return path.length === 0;
106
+ const [first] = path;
107
+ switch (selector.kind) {
108
+ case 'name':
109
+ return first !== undefined && first.key === selector.name
110
+ && this.matchesFrom(selectorIndex + 1, path.slice(1));
111
+ case 'wildcard':
112
+ return first !== undefined && !this.skipped(first)
113
+ && this.matchesFrom(selectorIndex + 1, path.slice(1));
114
+ case 'descendant_name':
115
+ for (let depth = 0; depth < path.length; depth += 1) {
116
+ if (path.slice(0, depth).every((step) => !this.skipped(step))
117
+ && path[depth].key === selector.name
118
+ && this.matchesFrom(selectorIndex + 1, path.slice(depth + 1))) {
119
+ return true;
120
+ }
121
+ }
122
+ return false;
123
+ default:
124
+ return path.length > 0 && path.every((step) => !this.skipped(step));
125
+ }
126
+ }
127
+ }
128
+
129
+ /** Several path entries; a node is selected when any of them selects it. */
130
+ class PathSet {
131
+ constructor(patterns = []) {
132
+ this.patterns = patterns;
133
+ }
134
+
135
+ /** The set of `entries`, each `{ path, skipKeys }`, or throws the first path's `Diagnostic`. */
136
+ static parse(entries) {
137
+ return new PathSet(entries.map(({ path, skipKeys }) => PathPattern.parse(path, skipKeys || [])));
138
+ }
139
+
140
+ selects(path) {
141
+ return this.patterns.some((pattern) => pattern.selects(path));
142
+ }
143
+
144
+ covers(path) {
145
+ return this.patterns.some((pattern) => pattern.covers(path));
146
+ }
147
+ }
148
+
149
+ module.exports = { PathPattern, PathSet, keyStep, indexStep };
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ /** The regular expression a `yaml-dsl.yml` pattern writes, or `null` where it writes none. */
4
+ function compilePattern(text) {
5
+ try {
6
+ return new RegExp(text, 'u');
7
+ } catch {
8
+ return null;
9
+ }
10
+ }
11
+
12
+ /** The named groups of a compiled pattern, in the order the pattern writes them. */
13
+ function groupNamesOf(pattern) {
14
+ const anyText = new RegExp(`${pattern.source}|`, 'u');
15
+ return Object.keys(anyText.exec('').groups || {});
16
+ }
17
+
18
+ module.exports = { compilePattern, groupNamesOf };
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const { scalarTextOf } = require('./yaml-node');
4
+
5
+ /**
6
+ * A value of the folded, expanded document, with where it was written. `kind` is
7
+ * a node's kind, or `reference`, `call` or `template`; `value` is what a node of
8
+ * that kind holds, a list of values and of map entries `{ key, keyPosition, value }`
9
+ * included, or the reference, the call, or the template's parts.
10
+ */
11
+ class Value {
12
+ constructor(kind, value, position) {
13
+ this.kind = kind;
14
+ this.value = value;
15
+ this.position = position;
16
+ }
17
+
18
+ static string(text, position) {
19
+ return new Value('string', text, position);
20
+ }
21
+
22
+ /** A node as written, each value with its position: nothing resolved. */
23
+ static fromNode(node) {
24
+ let value;
25
+ if (node.kind === 'list') value = node.value.map((item) => Value.fromNode(item));
26
+ else if (node.kind === 'map') {
27
+ value = node.value.map((entry) => ({
28
+ key: entry.key,
29
+ keyPosition: entry.keyPosition,
30
+ value: Value.fromNode(entry.value),
31
+ }));
32
+ } else value = node.value;
33
+ return new Value(node.kind, value, node.position);
34
+ }
35
+
36
+ entries() {
37
+ return this.kind === 'map' ? this.value : null;
38
+ }
39
+
40
+ items() {
41
+ return this.kind === 'list' ? this.value : null;
42
+ }
43
+
44
+ get(key) {
45
+ const found = (this.entries() || []).find((entry) => entry.key === key);
46
+ return found ? found.value : null;
47
+ }
48
+
49
+ asStr() {
50
+ return this.kind === 'string' ? this.value : null;
51
+ }
52
+
53
+ /** A scalar's text, for splicing into a longer string. */
54
+ scalarText() {
55
+ if (this.kind === 'reference' && this.value.target.kind === 'local') {
56
+ return this.value.target.value.scalarText();
57
+ }
58
+ return scalarTextOf(this.kind, this.value);
59
+ }
60
+
61
+ /** The value with every local reference replaced by the local's value. */
62
+ localValue() {
63
+ if (this.kind === 'reference' && this.value.target.kind === 'local') {
64
+ return this.value.target.value.localValue();
65
+ }
66
+ return this;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * A name read by one of the definition's reference rules: `{ scope, scopeKey,
72
+ * name, path, text, target }`. A target is `{ kind: 'local', value }`, or a
73
+ * `kind` of `local_unknown`, `builtin`, `declaration` with its `position`, or
74
+ * `undeclared`.
75
+ */
76
+ function reference(fields) {
77
+ return { scopeKey: null, ...fields };
78
+ }
79
+
80
+ /** A call left for the target to evaluate: `{ function, arguments, expandFinalArgument }`. */
81
+ function call(functionName, callArguments) {
82
+ return { function: functionName, arguments: callArguments, expandFinalArgument: false };
83
+ }
84
+
85
+ module.exports = { Value, reference, call };
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const { packagedVersionFiles } = require('../schema-versions');
7
+
8
+ /** Each schema family the package carries, by its file stem. */
9
+ const FAMILIES = {
10
+ yamlDslSchemas: 'yaml-dsl',
11
+ functionTableSchemas: 'x-yaml-dsl-functions',
12
+ };
13
+
14
+ /** A family's schema files by version: a minor such as `1.0`, or a major such as `1`, its newest minor. */
15
+ function filesOf(family) {
16
+ return packagedVersionFiles(family).map(([version, file]) => ({
17
+ version,
18
+ file: path.basename(file),
19
+ text: fs.readFileSync(file, 'utf8'),
20
+ }));
21
+ }
22
+
23
+ const YAML_DSL_FILES = filesOf(FAMILIES.yamlDslSchemas);
24
+ const FUNCTION_TABLE_FILES = filesOf(FAMILIES.functionTableSchemas);
25
+
26
+ /** The versions the package reads, each major before its minors. */
27
+ const YAML_DSL_VERSIONS = YAML_DSL_FILES.map(({ version }) => version);
28
+ const FUNCTION_TABLE_VERSIONS = FUNCTION_TABLE_FILES.map(({ version }) => version);
29
+
30
+ function schemasByVersion(files) {
31
+ return Object.fromEntries(files.map(({ version, text }) => [version, JSON.parse(text)]));
32
+ }
33
+
34
+ const yamlDslSchemas = schemasByVersion(YAML_DSL_FILES);
35
+ const functionTableSchemas = schemasByVersion(FUNCTION_TABLE_FILES);
36
+ for (const schemas of [yamlDslSchemas, functionTableSchemas]) {
37
+ const byFile = new Map();
38
+ for (const [version, schema] of Object.entries(schemas)) {
39
+ const key = JSON.stringify(schema);
40
+ if (byFile.has(key)) schemas[version] = byFile.get(key);
41
+ else byFile.set(key, schema);
42
+ }
43
+ }
44
+
45
+ /** The JSON Schema of `yaml-dsl.yml` at `version`, or `null`. */
46
+ function yamlDslSchema(version) {
47
+ return Object.prototype.hasOwnProperty.call(yamlDslSchemas, version) ? yamlDslSchemas[version] : null;
48
+ }
49
+
50
+ /** The JSON Schema of a DSL's function table, a document schema's `x-yaml-dsl-functions`, at `version`, or `null`. */
51
+ function functionTableSchema(version) {
52
+ return Object.prototype.hasOwnProperty.call(functionTableSchemas, version) ? functionTableSchemas[version] : null;
53
+ }
54
+
55
+ /** The file name and text of the `yaml-dsl.yml` schema at `version`, or `null`. */
56
+ function yamlDslSchemaFile(version) {
57
+ const found = YAML_DSL_FILES.find((entry) => entry.version === version);
58
+ return found ? { file: found.file, text: found.text } : null;
59
+ }
60
+
61
+ module.exports = {
62
+ FAMILIES,
63
+ YAML_DSL_VERSIONS,
64
+ FUNCTION_TABLE_VERSIONS,
65
+ yamlDslSchemas,
66
+ functionTableSchemas,
67
+ yamlDslSchema,
68
+ functionTableSchema,
69
+ yamlDslSchemaFile,
70
+ };