@malloydata/motly-ts-parser 0.0.1 → 0.2.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/build/ast.d.ts +10 -2
- package/build/clone.d.ts +3 -0
- package/build/clone.js +35 -0
- package/build/index.d.ts +2 -1
- package/build/index.js +4 -1
- package/build/interpreter.d.ts +2 -2
- package/build/interpreter.js +213 -124
- package/build/parser.js +126 -132
- package/build/session.js +8 -35
- package/build/validate.js +51 -65
- package/package.json +1 -1
package/build/ast.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ export type ScalarValue = {
|
|
|
15
15
|
kind: "reference";
|
|
16
16
|
ups: number;
|
|
17
17
|
path: RefPathSegment[];
|
|
18
|
+
} | {
|
|
19
|
+
kind: "none";
|
|
20
|
+
} | {
|
|
21
|
+
kind: "env";
|
|
22
|
+
name: string;
|
|
18
23
|
};
|
|
19
24
|
/** A segment in a reference path: either a named property or an array index. */
|
|
20
25
|
export type RefPathSegment = {
|
|
@@ -43,12 +48,15 @@ export type Statement = {
|
|
|
43
48
|
path: string[];
|
|
44
49
|
value: TagValue;
|
|
45
50
|
properties: Statement[] | null;
|
|
46
|
-
|
|
51
|
+
} | {
|
|
52
|
+
kind: "assignBoth";
|
|
53
|
+
path: string[];
|
|
54
|
+
value: TagValue;
|
|
55
|
+
properties: Statement[] | null;
|
|
47
56
|
} | {
|
|
48
57
|
kind: "replaceProperties";
|
|
49
58
|
path: string[];
|
|
50
59
|
properties: Statement[];
|
|
51
|
-
preserveValue: boolean;
|
|
52
60
|
} | {
|
|
53
61
|
kind: "updateProperties";
|
|
54
62
|
path: string[];
|
package/build/clone.d.ts
ADDED
package/build/clone.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cloneValue = cloneValue;
|
|
4
|
+
const motly_ts_interface_1 = require("motly-ts-interface");
|
|
5
|
+
/** Deep clone a MOTLYValue. */
|
|
6
|
+
function cloneValue(value) {
|
|
7
|
+
const result = {};
|
|
8
|
+
if (value.deleted)
|
|
9
|
+
result.deleted = true;
|
|
10
|
+
if (value.eq !== undefined) {
|
|
11
|
+
if (value.eq instanceof Date) {
|
|
12
|
+
result.eq = new Date(value.eq.getTime());
|
|
13
|
+
}
|
|
14
|
+
else if (Array.isArray(value.eq)) {
|
|
15
|
+
result.eq = value.eq.map(cloneValue);
|
|
16
|
+
}
|
|
17
|
+
else if ((0, motly_ts_interface_1.isRef)(value.eq)) {
|
|
18
|
+
result.eq = { linkTo: value.eq.linkTo };
|
|
19
|
+
}
|
|
20
|
+
else if ((0, motly_ts_interface_1.isEnvRef)(value.eq)) {
|
|
21
|
+
result.eq = { env: value.eq.env };
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
result.eq = value.eq;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (value.properties) {
|
|
28
|
+
const props = {};
|
|
29
|
+
for (const key of Object.keys(value.properties)) {
|
|
30
|
+
props[key] = cloneValue(value.properties[key]);
|
|
31
|
+
}
|
|
32
|
+
result.properties = props;
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
package/build/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export type { MOTLYScalar, MOTLYRef, MOTLYValue, MOTLYNode, MOTLYError, MOTLYSchemaError, MOTLYValidationError, } from "motly-ts-interface";
|
|
1
|
+
export type { MOTLYScalar, MOTLYRef, MOTLYEnvRef, MOTLYValue, MOTLYNode, MOTLYError, MOTLYSchemaError, MOTLYValidationError, } from "motly-ts-interface";
|
|
2
|
+
export { isRef, isEnvRef } from "motly-ts-interface";
|
|
2
3
|
export { MOTLYSession } from "./session";
|
package/build/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MOTLYSession = void 0;
|
|
3
|
+
exports.MOTLYSession = exports.isEnvRef = exports.isRef = void 0;
|
|
4
|
+
var motly_ts_interface_1 = require("motly-ts-interface");
|
|
5
|
+
Object.defineProperty(exports, "isRef", { enumerable: true, get: function () { return motly_ts_interface_1.isRef; } });
|
|
6
|
+
Object.defineProperty(exports, "isEnvRef", { enumerable: true, get: function () { return motly_ts_interface_1.isEnvRef; } });
|
|
4
7
|
var session_1 = require("./session");
|
|
5
8
|
Object.defineProperty(exports, "MOTLYSession", { enumerable: true, get: function () { return session_1.MOTLYSession; } });
|
package/build/interpreter.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Statement } from "./ast";
|
|
2
|
-
import { MOTLYValue } from "motly-ts-interface";
|
|
2
|
+
import { MOTLYValue, MOTLYError } from "motly-ts-interface";
|
|
3
3
|
/** Execute a list of parsed statements against an existing MOTLYValue. */
|
|
4
|
-
export declare function execute(statements: Statement[], root: MOTLYValue):
|
|
4
|
+
export declare function execute(statements: Statement[], root: MOTLYValue): MOTLYError[];
|
package/build/interpreter.js
CHANGED
|
@@ -1,93 +1,123 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.execute = execute;
|
|
4
|
+
const motly_ts_interface_1 = require("motly-ts-interface");
|
|
5
|
+
const clone_1 = require("./clone");
|
|
4
6
|
/** Execute a list of parsed statements against an existing MOTLYValue. */
|
|
5
7
|
function execute(statements, root) {
|
|
8
|
+
const errors = [];
|
|
6
9
|
for (const stmt of statements) {
|
|
7
|
-
executeStatement(stmt, root);
|
|
10
|
+
executeStatement(stmt, root, errors);
|
|
8
11
|
}
|
|
9
|
-
return
|
|
12
|
+
return errors;
|
|
10
13
|
}
|
|
11
|
-
function executeStatement(stmt, node) {
|
|
14
|
+
function executeStatement(stmt, node, errors) {
|
|
12
15
|
switch (stmt.kind) {
|
|
13
16
|
case "setEq":
|
|
14
|
-
executeSetEq(node, stmt.path, stmt.value, stmt.properties,
|
|
17
|
+
executeSetEq(node, stmt.path, stmt.value, stmt.properties, errors);
|
|
18
|
+
break;
|
|
19
|
+
case "assignBoth":
|
|
20
|
+
executeAssignBoth(node, stmt.path, stmt.value, stmt.properties, errors);
|
|
15
21
|
break;
|
|
16
22
|
case "replaceProperties":
|
|
17
|
-
executeReplaceProperties(node, stmt.path, stmt.properties,
|
|
23
|
+
executeReplaceProperties(node, stmt.path, stmt.properties, errors);
|
|
18
24
|
break;
|
|
19
25
|
case "updateProperties":
|
|
20
|
-
executeUpdateProperties(node, stmt.path, stmt.properties);
|
|
26
|
+
executeUpdateProperties(node, stmt.path, stmt.properties, errors);
|
|
21
27
|
break;
|
|
22
28
|
case "define":
|
|
23
29
|
executeDefine(node, stmt.path, stmt.deleted);
|
|
24
30
|
break;
|
|
25
31
|
case "clearAll":
|
|
32
|
+
delete node.eq;
|
|
26
33
|
node.properties = {};
|
|
27
34
|
break;
|
|
28
35
|
}
|
|
29
36
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
!preserveProperties) {
|
|
36
|
-
const [writeKey, parent] = buildAccessPath(node, path);
|
|
37
|
-
const props = getOrCreateProperties(parent);
|
|
38
|
-
props[writeKey] = {
|
|
39
|
-
linkTo: formatRefString(value.value.ups, value.value.path),
|
|
40
|
-
};
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
37
|
+
/**
|
|
38
|
+
* `name = value` — set eq, preserve existing properties.
|
|
39
|
+
* `name = value { props }` — set eq, then merge properties.
|
|
40
|
+
*/
|
|
41
|
+
function executeSetEq(node, path, value, properties, errors) {
|
|
43
42
|
const [writeKey, parent] = buildAccessPath(node, path);
|
|
43
|
+
const props = getOrCreateProperties(parent);
|
|
44
|
+
// Get or create target (preserves existing node and its properties)
|
|
45
|
+
let target = props[writeKey];
|
|
46
|
+
if (target === undefined) {
|
|
47
|
+
target = {};
|
|
48
|
+
props[writeKey] = target;
|
|
49
|
+
}
|
|
50
|
+
// Set the value slot
|
|
51
|
+
setEqSlot(target, value);
|
|
52
|
+
// If properties block present, MERGE them
|
|
44
53
|
if (properties !== null) {
|
|
45
|
-
// name = value { new_properties } - set value and replace properties
|
|
46
|
-
const result = createValueNode(value);
|
|
47
54
|
for (const s of properties) {
|
|
48
|
-
executeStatement(s,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
executeStatement(s, target, errors);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `name := value` — assign value + clear properties.
|
|
61
|
+
* `name := value { props }` — assign value + replace properties.
|
|
62
|
+
* `name := $ref` — clone the referenced subtree.
|
|
63
|
+
* `name := $ref { props }` — clone + replace properties.
|
|
64
|
+
*/
|
|
65
|
+
function executeAssignBoth(node, path, value, properties, errors) {
|
|
66
|
+
if (value.kind === "scalar" &&
|
|
67
|
+
value.value.kind === "reference") {
|
|
68
|
+
// CLONE semantics: resolve + deep copy the target
|
|
69
|
+
let cloned;
|
|
70
|
+
try {
|
|
71
|
+
cloned = resolveAndClone(node, path, value.value.ups, value.value.path);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
75
|
+
errors.push(err);
|
|
61
76
|
}
|
|
62
|
-
|
|
77
|
+
return;
|
|
63
78
|
}
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
// Check for relative references that escape the clone boundary
|
|
80
|
+
sanitizeClonedRefs(cloned, 0, errors);
|
|
81
|
+
if (properties !== null) {
|
|
82
|
+
cloned.properties = {};
|
|
83
|
+
for (const s of properties) {
|
|
84
|
+
executeStatement(s, cloned, errors);
|
|
85
|
+
}
|
|
66
86
|
}
|
|
87
|
+
const [writeKey, parent] = buildAccessPath(node, path);
|
|
88
|
+
getOrCreateProperties(parent)[writeKey] = cloned;
|
|
67
89
|
}
|
|
68
90
|
else {
|
|
69
|
-
//
|
|
70
|
-
const
|
|
71
|
-
|
|
91
|
+
// Literal value: create fresh node (replaces everything)
|
|
92
|
+
const result = {};
|
|
93
|
+
setEqSlot(result, value);
|
|
94
|
+
if (properties !== null) {
|
|
95
|
+
for (const s of properties) {
|
|
96
|
+
executeStatement(s, result, errors);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const [writeKey, parent] = buildAccessPath(node, path);
|
|
100
|
+
getOrCreateProperties(parent)[writeKey] = result;
|
|
72
101
|
}
|
|
73
102
|
}
|
|
74
|
-
|
|
103
|
+
/**
|
|
104
|
+
* `name: { props }` — preserve existing value, replace properties.
|
|
105
|
+
*/
|
|
106
|
+
function executeReplaceProperties(node, path, properties, errors) {
|
|
75
107
|
const [writeKey, parent] = buildAccessPath(node, path);
|
|
76
108
|
const result = {};
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
109
|
+
// Always preserve the existing value
|
|
110
|
+
const parentProps = getOrCreateProperties(parent);
|
|
111
|
+
const existing = parentProps[writeKey];
|
|
112
|
+
if (existing !== undefined) {
|
|
113
|
+
result.eq = existing.eq;
|
|
83
114
|
}
|
|
84
115
|
for (const stmt of properties) {
|
|
85
|
-
executeStatement(stmt, result);
|
|
116
|
+
executeStatement(stmt, result, errors);
|
|
86
117
|
}
|
|
87
|
-
|
|
88
|
-
props[writeKey] = result;
|
|
118
|
+
parentProps[writeKey] = result;
|
|
89
119
|
}
|
|
90
|
-
function executeUpdateProperties(node, path, properties) {
|
|
120
|
+
function executeUpdateProperties(node, path, properties, errors) {
|
|
91
121
|
const [writeKey, parent] = buildAccessPath(node, path);
|
|
92
122
|
const props = getOrCreateProperties(parent);
|
|
93
123
|
let target = props[writeKey];
|
|
@@ -95,17 +125,8 @@ function executeUpdateProperties(node, path, properties) {
|
|
|
95
125
|
target = {};
|
|
96
126
|
props[writeKey] = target;
|
|
97
127
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
for (const stmt of properties) {
|
|
101
|
-
executeStatement(stmt, newNode);
|
|
102
|
-
}
|
|
103
|
-
props[writeKey] = newNode;
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
for (const stmt of properties) {
|
|
107
|
-
executeStatement(stmt, target);
|
|
108
|
-
}
|
|
128
|
+
for (const stmt of properties) {
|
|
129
|
+
executeStatement(stmt, target, errors);
|
|
109
130
|
}
|
|
110
131
|
}
|
|
111
132
|
function executeDefine(node, path, deleted) {
|
|
@@ -115,7 +136,10 @@ function executeDefine(node, path, deleted) {
|
|
|
115
136
|
props[writeKey] = { deleted: true };
|
|
116
137
|
}
|
|
117
138
|
else {
|
|
118
|
-
|
|
139
|
+
// Get-or-create: if node already exists, leave it alone
|
|
140
|
+
if (props[writeKey] === undefined) {
|
|
141
|
+
props[writeKey] = {};
|
|
142
|
+
}
|
|
119
143
|
}
|
|
120
144
|
}
|
|
121
145
|
/** Navigate to the parent of the final path segment, creating intermediate nodes. */
|
|
@@ -129,77 +153,54 @@ function buildAccessPath(node, path) {
|
|
|
129
153
|
entry = {};
|
|
130
154
|
props[segment] = entry;
|
|
131
155
|
}
|
|
132
|
-
if (isRef(entry)) {
|
|
133
|
-
entry = {};
|
|
134
|
-
props[segment] = entry;
|
|
135
|
-
}
|
|
136
156
|
current = entry;
|
|
137
157
|
}
|
|
138
158
|
return [path[path.length - 1], current];
|
|
139
159
|
}
|
|
140
|
-
/**
|
|
141
|
-
function
|
|
160
|
+
/** Set the eq slot on a target node from a TagValue. */
|
|
161
|
+
function setEqSlot(target, value, errors) {
|
|
142
162
|
if (value.kind === "array") {
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
target.eq = resolveArray(value.elements, errors ?? []);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const sv = value.value;
|
|
167
|
+
switch (sv.kind) {
|
|
168
|
+
case "string":
|
|
169
|
+
target.eq = sv.value;
|
|
170
|
+
break;
|
|
171
|
+
case "number":
|
|
172
|
+
target.eq = sv.value;
|
|
173
|
+
break;
|
|
174
|
+
case "boolean":
|
|
175
|
+
target.eq = sv.value;
|
|
176
|
+
break;
|
|
177
|
+
case "date":
|
|
178
|
+
target.eq = new Date(sv.value);
|
|
179
|
+
break;
|
|
180
|
+
case "reference":
|
|
181
|
+
target.eq = { linkTo: formatRefString(sv.ups, sv.path) };
|
|
182
|
+
break;
|
|
183
|
+
case "env":
|
|
184
|
+
target.eq = { env: sv.name };
|
|
185
|
+
break;
|
|
186
|
+
case "none":
|
|
187
|
+
delete target.eq;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
158
190
|
}
|
|
159
191
|
}
|
|
160
192
|
/** Resolve an array of AST elements to MOTLYNodes. */
|
|
161
|
-
function resolveArray(elements) {
|
|
162
|
-
return elements.map(resolveArrayElement);
|
|
163
|
-
}
|
|
164
|
-
function resolveArrayElement(el) {
|
|
165
|
-
// Reference without properties becomes a link
|
|
166
|
-
if (el.value !== null &&
|
|
167
|
-
el.value.kind === "scalar" &&
|
|
168
|
-
el.value.value.kind === "reference" &&
|
|
169
|
-
el.properties === null) {
|
|
170
|
-
return {
|
|
171
|
-
linkTo: formatRefString(el.value.value.ups, el.value.value.path),
|
|
172
|
-
};
|
|
173
|
-
}
|
|
193
|
+
function resolveArray(elements, errors) {
|
|
194
|
+
return elements.map((el) => resolveArrayElement(el, errors));
|
|
195
|
+
}
|
|
196
|
+
function resolveArrayElement(el, errors) {
|
|
174
197
|
const node = {};
|
|
175
198
|
if (el.value !== null) {
|
|
176
|
-
|
|
177
|
-
node.eq = resolveArray(el.value.elements);
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
const sv = el.value.value;
|
|
181
|
-
switch (sv.kind) {
|
|
182
|
-
case "string":
|
|
183
|
-
node.eq = sv.value;
|
|
184
|
-
break;
|
|
185
|
-
case "number":
|
|
186
|
-
node.eq = sv.value;
|
|
187
|
-
break;
|
|
188
|
-
case "boolean":
|
|
189
|
-
node.eq = sv.value;
|
|
190
|
-
break;
|
|
191
|
-
case "date":
|
|
192
|
-
node.eq = new Date(sv.value);
|
|
193
|
-
break;
|
|
194
|
-
case "reference":
|
|
195
|
-
// Reference with properties: ignore the reference value
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
+
setEqSlot(node, el.value);
|
|
199
200
|
}
|
|
200
201
|
if (el.properties !== null) {
|
|
201
202
|
for (const stmt of el.properties) {
|
|
202
|
-
executeStatement(stmt, node);
|
|
203
|
+
executeStatement(stmt, node, errors);
|
|
203
204
|
}
|
|
204
205
|
}
|
|
205
206
|
return node;
|
|
@@ -223,9 +224,97 @@ function formatRefString(ups, path) {
|
|
|
223
224
|
}
|
|
224
225
|
return s;
|
|
225
226
|
}
|
|
226
|
-
/**
|
|
227
|
-
function
|
|
228
|
-
|
|
227
|
+
/** Resolve a reference path in the tree and return a deep clone. */
|
|
228
|
+
function resolveAndClone(root, stmtPath, ups, refPath) {
|
|
229
|
+
const refStr = formatRefString(ups, refPath);
|
|
230
|
+
let start;
|
|
231
|
+
if (ups === 0) {
|
|
232
|
+
// Absolute reference: start at root
|
|
233
|
+
start = root;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// Relative reference: go up from the current context.
|
|
237
|
+
// stmtPath is the full write path (including the key being assigned to).
|
|
238
|
+
// Current context = parent of write target = stmtPath[0..len-2].
|
|
239
|
+
// Going up `ups` levels: stmtPath[0..len-2-ups].
|
|
240
|
+
const contextLen = stmtPath.length - 1 - ups;
|
|
241
|
+
if (contextLen < 0) {
|
|
242
|
+
throw cloneError(`Clone reference ${refStr} goes ${ups} level(s) up but only ${stmtPath.length - 1} ancestor(s) available`);
|
|
243
|
+
}
|
|
244
|
+
start = root;
|
|
245
|
+
for (let i = 0; i < contextLen; i++) {
|
|
246
|
+
if (!start.properties || !start.properties[stmtPath[i]]) {
|
|
247
|
+
throw cloneError(`Clone reference ${refStr} could not be resolved: path segment "${stmtPath[i]}" not found`);
|
|
248
|
+
}
|
|
249
|
+
start = start.properties[stmtPath[i]];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Follow refPath segments
|
|
253
|
+
let current = start;
|
|
254
|
+
for (const seg of refPath) {
|
|
255
|
+
if (seg.kind === "name") {
|
|
256
|
+
if (!current.properties || !current.properties[seg.name]) {
|
|
257
|
+
throw cloneError(`Clone reference ${refStr} could not be resolved: property "${seg.name}" not found`);
|
|
258
|
+
}
|
|
259
|
+
current = current.properties[seg.name];
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
if (!current.eq || !Array.isArray(current.eq)) {
|
|
263
|
+
throw cloneError(`Clone reference ${refStr} could not be resolved: index [${seg.index}] used on non-array`);
|
|
264
|
+
}
|
|
265
|
+
if (seg.index >= current.eq.length) {
|
|
266
|
+
throw cloneError(`Clone reference ${refStr} could not be resolved: index [${seg.index}] out of bounds (array length ${current.eq.length})`);
|
|
267
|
+
}
|
|
268
|
+
current = current.eq[seg.index];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return (0, clone_1.cloneValue)(current);
|
|
272
|
+
}
|
|
273
|
+
function cloneError(message) {
|
|
274
|
+
const zero = { line: 0, column: 0, offset: 0 };
|
|
275
|
+
return { code: "unresolved-clone-reference", message, begin: zero, end: zero };
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Walk a cloned subtree and null out any relative (^) references that
|
|
279
|
+
* escape the clone boundary. A reference at depth D with N ups escapes
|
|
280
|
+
* if N > D. Absolute references (ups=0) are left alone.
|
|
281
|
+
*/
|
|
282
|
+
function sanitizeClonedRefs(node, depth, errors) {
|
|
283
|
+
if ((0, motly_ts_interface_1.isRef)(node.eq)) {
|
|
284
|
+
const parsed = parseRefUps(node.eq.linkTo);
|
|
285
|
+
if (parsed.ups > 0 && parsed.ups > depth) {
|
|
286
|
+
const zero = { line: 0, column: 0, offset: 0 };
|
|
287
|
+
errors.push({
|
|
288
|
+
code: "clone-reference-out-of-scope",
|
|
289
|
+
message: `Cloned reference "${node.eq.linkTo}" escapes the clone boundary (${parsed.ups} level(s) up from depth ${depth})`,
|
|
290
|
+
begin: zero,
|
|
291
|
+
end: zero,
|
|
292
|
+
});
|
|
293
|
+
delete node.eq;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (node.eq !== undefined && Array.isArray(node.eq)) {
|
|
297
|
+
for (const elem of node.eq) {
|
|
298
|
+
sanitizeClonedRefs(elem, depth + 1, errors);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (node.properties) {
|
|
302
|
+
for (const key of Object.keys(node.properties)) {
|
|
303
|
+
sanitizeClonedRefs(node.properties[key], depth + 1, errors);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** Extract the ups count from a linkTo string like "$^^name". */
|
|
308
|
+
function parseRefUps(linkTo) {
|
|
309
|
+
let i = 0;
|
|
310
|
+
if (i < linkTo.length && linkTo[i] === "$")
|
|
311
|
+
i++;
|
|
312
|
+
let ups = 0;
|
|
313
|
+
while (i < linkTo.length && linkTo[i] === "^") {
|
|
314
|
+
ups++;
|
|
315
|
+
i++;
|
|
316
|
+
}
|
|
317
|
+
return { ups };
|
|
229
318
|
}
|
|
230
319
|
/** Get or create the properties object on a MOTLYValue. */
|
|
231
320
|
function getOrCreateProperties(node) {
|
package/build/parser.js
CHANGED
|
@@ -7,9 +7,6 @@ class Parser {
|
|
|
7
7
|
this.pos = 0;
|
|
8
8
|
}
|
|
9
9
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
10
|
-
remaining() {
|
|
11
|
-
return this.input.substring(this.pos);
|
|
12
|
-
}
|
|
13
10
|
peekChar() {
|
|
14
11
|
return this.pos < this.input.length ? this.input[this.pos] : undefined;
|
|
15
12
|
}
|
|
@@ -80,13 +77,25 @@ class Parser {
|
|
|
80
77
|
}
|
|
81
78
|
}
|
|
82
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Like `skipWs`, but also eats commas. Used in statement-list
|
|
82
|
+
* contexts (top-level document and properties blocks) so commas
|
|
83
|
+
* can serve as optional separators between statements.
|
|
84
|
+
*/
|
|
85
|
+
skipWsAndCommas() {
|
|
86
|
+
this.skipWs();
|
|
87
|
+
while (this.peekChar() === ",") {
|
|
88
|
+
this.advance(1);
|
|
89
|
+
this.skipWs();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
83
92
|
// ── Statement Dispatch ──────────────────────────────────────────
|
|
84
93
|
parseStatements() {
|
|
85
94
|
const statements = [];
|
|
86
|
-
this.
|
|
95
|
+
this.skipWsAndCommas();
|
|
87
96
|
while (this.pos < this.input.length) {
|
|
88
97
|
statements.push(this.parseStatement());
|
|
89
|
-
this.
|
|
98
|
+
this.skipWsAndCommas();
|
|
90
99
|
}
|
|
91
100
|
return statements;
|
|
92
101
|
}
|
|
@@ -106,87 +115,40 @@ class Parser {
|
|
|
106
115
|
const path = this.parsePropName();
|
|
107
116
|
this.skipWs();
|
|
108
117
|
const ch = this.peekChar();
|
|
118
|
+
// Check := first (MUST check before : alone)
|
|
119
|
+
if (ch === ":" && this.startsWith(":=")) {
|
|
120
|
+
this.advance(2);
|
|
121
|
+
this.skipWs();
|
|
122
|
+
const value = this.parseEqValue();
|
|
123
|
+
this.skipWs();
|
|
124
|
+
if (this.peekChar() === "{") {
|
|
125
|
+
const props = this.parsePropertiesBlock();
|
|
126
|
+
return { kind: "assignBoth", path, value, properties: props };
|
|
127
|
+
}
|
|
128
|
+
return { kind: "assignBoth", path, value, properties: null };
|
|
129
|
+
}
|
|
109
130
|
if (ch === "=") {
|
|
110
131
|
this.advance(1);
|
|
111
132
|
this.skipWs();
|
|
112
|
-
//
|
|
113
|
-
if (this.startsWith("...")) {
|
|
114
|
-
const saved = this.pos;
|
|
115
|
-
this.advance(3);
|
|
116
|
-
this.skipWs();
|
|
117
|
-
if (this.peekChar() === "{") {
|
|
118
|
-
const props = this.parsePropertiesBlock();
|
|
119
|
-
return {
|
|
120
|
-
kind: "replaceProperties",
|
|
121
|
-
path,
|
|
122
|
-
properties: props,
|
|
123
|
-
preserveValue: true,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
this.pos = saved;
|
|
127
|
-
}
|
|
128
|
-
// Check for `= {` (replaceProperties without preserveValue)
|
|
133
|
+
// = { is now a parse error (= requires a value)
|
|
129
134
|
if (this.peekChar() === "{") {
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
kind: "replaceProperties",
|
|
133
|
-
path,
|
|
134
|
-
properties: props,
|
|
135
|
-
preserveValue: false,
|
|
136
|
-
};
|
|
135
|
+
throw this.errorPoint("'=' requires a value; use ': { ... }' to replace properties");
|
|
137
136
|
}
|
|
138
|
-
//
|
|
137
|
+
// = value
|
|
139
138
|
const value = this.parseEqValue();
|
|
140
139
|
this.skipWs();
|
|
141
|
-
//
|
|
140
|
+
// Optional { props } block (MERGE semantics)
|
|
142
141
|
if (this.peekChar() === "{") {
|
|
143
|
-
const saved = this.pos;
|
|
144
|
-
this.advance(1);
|
|
145
|
-
this.skipWs();
|
|
146
|
-
if (this.startsWith("...")) {
|
|
147
|
-
const saved2 = this.pos;
|
|
148
|
-
this.advance(3);
|
|
149
|
-
this.skipWs();
|
|
150
|
-
if (this.peekChar() === "}") {
|
|
151
|
-
this.advance(1);
|
|
152
|
-
return {
|
|
153
|
-
kind: "setEq",
|
|
154
|
-
path,
|
|
155
|
-
value,
|
|
156
|
-
properties: null,
|
|
157
|
-
preserveProperties: true,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
this.pos = saved2;
|
|
161
|
-
}
|
|
162
|
-
this.pos = saved;
|
|
163
142
|
const props = this.parsePropertiesBlock();
|
|
164
|
-
return {
|
|
165
|
-
kind: "setEq",
|
|
166
|
-
path,
|
|
167
|
-
value,
|
|
168
|
-
properties: props,
|
|
169
|
-
preserveProperties: false,
|
|
170
|
-
};
|
|
143
|
+
return { kind: "setEq", path, value, properties: props };
|
|
171
144
|
}
|
|
172
|
-
return {
|
|
173
|
-
kind: "setEq",
|
|
174
|
-
path,
|
|
175
|
-
value,
|
|
176
|
-
properties: null,
|
|
177
|
-
preserveProperties: false,
|
|
178
|
-
};
|
|
145
|
+
return { kind: "setEq", path, value, properties: null };
|
|
179
146
|
}
|
|
180
147
|
if (ch === ":") {
|
|
181
148
|
this.advance(1);
|
|
182
149
|
this.skipWs();
|
|
183
150
|
const props = this.parsePropertiesBlock();
|
|
184
|
-
return {
|
|
185
|
-
kind: "replaceProperties",
|
|
186
|
-
path,
|
|
187
|
-
properties: props,
|
|
188
|
-
preserveValue: false,
|
|
189
|
-
};
|
|
151
|
+
return { kind: "replaceProperties", path, properties: props };
|
|
190
152
|
}
|
|
191
153
|
if (ch === "{") {
|
|
192
154
|
const props = this.parsePropertiesBlock();
|
|
@@ -211,55 +173,16 @@ class Parser {
|
|
|
211
173
|
return this.parseBareString();
|
|
212
174
|
}
|
|
213
175
|
// ── Values ──────────────────────────────────────────────────────
|
|
214
|
-
parseEqValue() {
|
|
176
|
+
parseEqValue(allowArrays = true) {
|
|
215
177
|
const ch = this.peekChar();
|
|
216
|
-
if (ch === "[")
|
|
178
|
+
if (allowArrays && ch === "[")
|
|
217
179
|
return { kind: "array", elements: this.parseArray() };
|
|
218
|
-
if (
|
|
219
|
-
return { kind: "scalar", value: this.parseAtValue() };
|
|
220
|
-
if (ch === "$")
|
|
221
|
-
return { kind: "scalar", value: this.parseReference() };
|
|
222
|
-
if (ch === '"') {
|
|
223
|
-
if (this.startsWith('"""')) {
|
|
224
|
-
return {
|
|
225
|
-
kind: "scalar",
|
|
226
|
-
value: { kind: "string", value: this.parseTripleString() },
|
|
227
|
-
};
|
|
228
|
-
}
|
|
180
|
+
if (this.startsWith("<<<")) {
|
|
229
181
|
return {
|
|
230
182
|
kind: "scalar",
|
|
231
|
-
value: { kind: "string", value: this.
|
|
183
|
+
value: { kind: "string", value: this.parseHeredoc() },
|
|
232
184
|
};
|
|
233
185
|
}
|
|
234
|
-
if (ch === "'") {
|
|
235
|
-
if (this.startsWith("'''")) {
|
|
236
|
-
return {
|
|
237
|
-
kind: "scalar",
|
|
238
|
-
value: {
|
|
239
|
-
kind: "string",
|
|
240
|
-
value: this.parseTripleSingleQuotedString(),
|
|
241
|
-
},
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
return {
|
|
245
|
-
kind: "scalar",
|
|
246
|
-
value: { kind: "string", value: this.parseSingleQuotedString() },
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
if (ch !== undefined &&
|
|
250
|
-
(ch === "-" || (ch >= "0" && ch <= "9") || ch === ".")) {
|
|
251
|
-
return this.parseNumberOrString();
|
|
252
|
-
}
|
|
253
|
-
if (ch !== undefined && isBareChar(ch)) {
|
|
254
|
-
return {
|
|
255
|
-
kind: "scalar",
|
|
256
|
-
value: { kind: "string", value: this.parseBareString() },
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
throw this.errorPoint("Expected a value");
|
|
260
|
-
}
|
|
261
|
-
parseScalarValue() {
|
|
262
|
-
const ch = this.peekChar();
|
|
263
186
|
if (ch === "@")
|
|
264
187
|
return { kind: "scalar", value: this.parseAtValue() };
|
|
265
188
|
if (ch === "$")
|
|
@@ -292,7 +215,7 @@ class Parser {
|
|
|
292
215
|
};
|
|
293
216
|
}
|
|
294
217
|
if (ch !== undefined &&
|
|
295
|
-
((ch >= "0" && ch <= "9") || ch === "."
|
|
218
|
+
(ch === "-" || (ch >= "0" && ch <= "9") || ch === ".")) {
|
|
296
219
|
return this.parseNumberOrString();
|
|
297
220
|
}
|
|
298
221
|
if (ch !== undefined && isBareChar(ch)) {
|
|
@@ -303,7 +226,7 @@ class Parser {
|
|
|
303
226
|
}
|
|
304
227
|
throw this.errorPoint("Expected a value");
|
|
305
228
|
}
|
|
306
|
-
/** Parse `@true`, `@false`, or `@date` */
|
|
229
|
+
/** Parse `@true`, `@false`, `@none`, `@env.NAME`, or `@date` */
|
|
307
230
|
parseAtValue() {
|
|
308
231
|
const begin = this.position();
|
|
309
232
|
this.expectChar("@");
|
|
@@ -315,6 +238,15 @@ class Parser {
|
|
|
315
238
|
this.advance(5);
|
|
316
239
|
return { kind: "boolean", value: false };
|
|
317
240
|
}
|
|
241
|
+
if (this.startsWith("none") && !this.isBareCharAt(4)) {
|
|
242
|
+
this.advance(4);
|
|
243
|
+
return { kind: "none" };
|
|
244
|
+
}
|
|
245
|
+
if (this.startsWith("env.")) {
|
|
246
|
+
this.advance(4);
|
|
247
|
+
const name = this.parseBareString();
|
|
248
|
+
return { kind: "env", name };
|
|
249
|
+
}
|
|
318
250
|
const ch = this.peekChar();
|
|
319
251
|
if (ch !== undefined && ch >= "0" && ch <= "9") {
|
|
320
252
|
return this.parseDate(begin);
|
|
@@ -325,11 +257,11 @@ class Parser {
|
|
|
325
257
|
this.pos++;
|
|
326
258
|
}
|
|
327
259
|
const token = this.pos > tokenStart ? this.input.substring(tokenStart, this.pos) : "";
|
|
328
|
-
throw this.errorSpan(`Illegal constant @${token}; expected @true, @false, or @date`, begin);
|
|
260
|
+
throw this.errorSpan(`Illegal constant @${token}; expected @true, @false, @none, @env.NAME, or @date`, begin);
|
|
329
261
|
}
|
|
330
262
|
isBareCharAt(offset) {
|
|
331
|
-
const
|
|
332
|
-
return
|
|
263
|
+
const absPos = this.pos + offset;
|
|
264
|
+
return absPos < this.input.length && isBareChar(this.input[absPos]);
|
|
333
265
|
}
|
|
334
266
|
parseDate(begin) {
|
|
335
267
|
const start = this.pos;
|
|
@@ -710,6 +642,79 @@ class Parser {
|
|
|
710
642
|
return ch;
|
|
711
643
|
}
|
|
712
644
|
}
|
|
645
|
+
// ── Heredoc ─────────────────────────────────────────────────────
|
|
646
|
+
parseHeredoc() {
|
|
647
|
+
const begin = this.position();
|
|
648
|
+
this.advance(3); // past <<<
|
|
649
|
+
// Skip spaces/tabs on the same line
|
|
650
|
+
while (this.pos < this.input.length) {
|
|
651
|
+
const ch = this.input[this.pos];
|
|
652
|
+
if (ch === " " || ch === "\t") {
|
|
653
|
+
this.pos++;
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Allow \r before \n
|
|
660
|
+
if (this.pos < this.input.length && this.input[this.pos] === "\r") {
|
|
661
|
+
this.advance(1);
|
|
662
|
+
}
|
|
663
|
+
// Expect newline
|
|
664
|
+
if (this.pos >= this.input.length || this.input[this.pos] !== "\n") {
|
|
665
|
+
throw this.errorSpan("Expected newline after <<<", begin);
|
|
666
|
+
}
|
|
667
|
+
this.advance(1);
|
|
668
|
+
// Collect lines until we find >>> on its own line
|
|
669
|
+
const lines = [];
|
|
670
|
+
let foundClose = false;
|
|
671
|
+
while (this.pos < this.input.length) {
|
|
672
|
+
// Read a line (break only on \n)
|
|
673
|
+
const lineStart = this.pos;
|
|
674
|
+
while (this.pos < this.input.length && this.input[this.pos] !== "\n") {
|
|
675
|
+
this.pos++;
|
|
676
|
+
}
|
|
677
|
+
// Strip trailing \r for CRLF compatibility
|
|
678
|
+
let lineContent = this.input.substring(lineStart, this.pos);
|
|
679
|
+
if (lineContent.endsWith("\r")) {
|
|
680
|
+
lineContent = lineContent.substring(0, lineContent.length - 1);
|
|
681
|
+
}
|
|
682
|
+
// Consume the \n
|
|
683
|
+
if (this.pos < this.input.length && this.input[this.pos] === "\n") {
|
|
684
|
+
this.advance(1);
|
|
685
|
+
}
|
|
686
|
+
// Check if this is the closing >>> line
|
|
687
|
+
if (lineContent.trim() === ">>>") {
|
|
688
|
+
foundClose = true;
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
lines.push(lineContent);
|
|
692
|
+
}
|
|
693
|
+
if (!foundClose) {
|
|
694
|
+
throw this.errorSpan("Unterminated heredoc (expected >>>)", begin);
|
|
695
|
+
}
|
|
696
|
+
if (lines.length === 0) {
|
|
697
|
+
return "";
|
|
698
|
+
}
|
|
699
|
+
// Determine strip amount from first line containing a non-space character
|
|
700
|
+
let strip = 0;
|
|
701
|
+
for (const line of lines) {
|
|
702
|
+
const trimmed = line.trimStart();
|
|
703
|
+
if (trimmed.length > 0) {
|
|
704
|
+
strip = line.length - trimmed.length;
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// Strip indentation and join; whitespace-only lines become empty
|
|
709
|
+
const stripped = lines.map((line) => {
|
|
710
|
+
if (line.trimStart().length === 0)
|
|
711
|
+
return "";
|
|
712
|
+
if (strip <= line.length)
|
|
713
|
+
return line.substring(strip);
|
|
714
|
+
return line;
|
|
715
|
+
});
|
|
716
|
+
return stripped.join("\n") + "\n";
|
|
717
|
+
}
|
|
713
718
|
// ── Arrays ──────────────────────────────────────────────────────
|
|
714
719
|
parseArray() {
|
|
715
720
|
const begin = this.position();
|
|
@@ -750,7 +755,7 @@ class Parser {
|
|
|
750
755
|
const elements = this.parseArray();
|
|
751
756
|
return { value: { kind: "array", elements }, properties: null };
|
|
752
757
|
}
|
|
753
|
-
const value = this.
|
|
758
|
+
const value = this.parseEqValue(false);
|
|
754
759
|
this.skipWs();
|
|
755
760
|
if (this.peekChar() === "{") {
|
|
756
761
|
const props = this.parsePropertiesBlock();
|
|
@@ -762,20 +767,9 @@ class Parser {
|
|
|
762
767
|
parsePropertiesBlock() {
|
|
763
768
|
const begin = this.position();
|
|
764
769
|
this.expectChar("{");
|
|
765
|
-
this.skipWs();
|
|
766
|
-
if (this.startsWith("...")) {
|
|
767
|
-
const saved = this.pos;
|
|
768
|
-
this.advance(3);
|
|
769
|
-
this.skipWs();
|
|
770
|
-
if (this.peekChar() === "}") {
|
|
771
|
-
this.advance(1);
|
|
772
|
-
return [];
|
|
773
|
-
}
|
|
774
|
-
this.pos = saved;
|
|
775
|
-
}
|
|
776
770
|
const stmts = [];
|
|
777
771
|
for (;;) {
|
|
778
|
-
this.
|
|
772
|
+
this.skipWsAndCommas();
|
|
779
773
|
if (this.eatChar("}"))
|
|
780
774
|
return stmts;
|
|
781
775
|
if (this.pos >= this.input.length) {
|
package/build/session.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.MOTLYSession = void 0;
|
|
|
4
4
|
const parser_1 = require("./parser");
|
|
5
5
|
const interpreter_1 = require("./interpreter");
|
|
6
6
|
const validate_1 = require("./validate");
|
|
7
|
+
const clone_1 = require("./clone");
|
|
7
8
|
/**
|
|
8
9
|
* A stateful MOTLY parsing session.
|
|
9
10
|
*
|
|
@@ -24,8 +25,8 @@ class MOTLYSession {
|
|
|
24
25
|
this.ensureAlive();
|
|
25
26
|
try {
|
|
26
27
|
const stmts = (0, parser_1.parse)(source);
|
|
27
|
-
|
|
28
|
-
return
|
|
28
|
+
const errors = (0, interpreter_1.execute)(stmts, this.value);
|
|
29
|
+
return errors;
|
|
29
30
|
}
|
|
30
31
|
catch (e) {
|
|
31
32
|
if (isMotlyError(e))
|
|
@@ -41,8 +42,10 @@ class MOTLYSession {
|
|
|
41
42
|
this.ensureAlive();
|
|
42
43
|
try {
|
|
43
44
|
const stmts = (0, parser_1.parse)(source);
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
const fresh = {};
|
|
46
|
+
const errors = (0, interpreter_1.execute)(stmts, fresh);
|
|
47
|
+
this.schema = fresh;
|
|
48
|
+
return errors;
|
|
46
49
|
}
|
|
47
50
|
catch (e) {
|
|
48
51
|
if (isMotlyError(e))
|
|
@@ -62,7 +65,7 @@ class MOTLYSession {
|
|
|
62
65
|
*/
|
|
63
66
|
getValue() {
|
|
64
67
|
this.ensureAlive();
|
|
65
|
-
return
|
|
68
|
+
return (0, clone_1.cloneValue)(this.value);
|
|
66
69
|
}
|
|
67
70
|
/**
|
|
68
71
|
* Validate the session's value against its stored schema.
|
|
@@ -103,33 +106,3 @@ function isMotlyError(e) {
|
|
|
103
106
|
"begin" in e &&
|
|
104
107
|
"end" in e);
|
|
105
108
|
}
|
|
106
|
-
function deepClone(value) {
|
|
107
|
-
const result = {};
|
|
108
|
-
if (value.deleted)
|
|
109
|
-
result.deleted = true;
|
|
110
|
-
if (value.eq !== undefined) {
|
|
111
|
-
if (value.eq instanceof Date) {
|
|
112
|
-
result.eq = new Date(value.eq.getTime());
|
|
113
|
-
}
|
|
114
|
-
else if (Array.isArray(value.eq)) {
|
|
115
|
-
result.eq = value.eq.map(cloneNode);
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
result.eq = value.eq;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (value.properties) {
|
|
122
|
-
const props = {};
|
|
123
|
-
for (const key of Object.keys(value.properties)) {
|
|
124
|
-
props[key] = cloneNode(value.properties[key]);
|
|
125
|
-
}
|
|
126
|
-
result.properties = props;
|
|
127
|
-
}
|
|
128
|
-
return result;
|
|
129
|
-
}
|
|
130
|
-
function cloneNode(node) {
|
|
131
|
-
if ("linkTo" in node) {
|
|
132
|
-
return { linkTo: node.linkTo };
|
|
133
|
-
}
|
|
134
|
-
return deepClone(node);
|
|
135
|
-
}
|
package/build/validate.js
CHANGED
|
@@ -2,15 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validateReferences = validateReferences;
|
|
4
4
|
exports.validateSchema = validateSchema;
|
|
5
|
-
|
|
6
|
-
function isRef(node) {
|
|
7
|
-
return "linkTo" in node;
|
|
8
|
-
}
|
|
5
|
+
const motly_ts_interface_1 = require("motly-ts-interface");
|
|
9
6
|
function getEqString(node) {
|
|
10
7
|
return typeof node.eq === "string" ? node.eq : undefined;
|
|
11
8
|
}
|
|
12
9
|
function valueEqString(node) {
|
|
13
|
-
if (isRef(node))
|
|
10
|
+
if ((0, motly_ts_interface_1.isRef)(node.eq) || (0, motly_ts_interface_1.isEnvRef)(node.eq))
|
|
14
11
|
return undefined;
|
|
15
12
|
return getEqString(node);
|
|
16
13
|
}
|
|
@@ -18,7 +15,7 @@ function extractSection(node, name) {
|
|
|
18
15
|
if (!node.properties)
|
|
19
16
|
return undefined;
|
|
20
17
|
const section = node.properties[name];
|
|
21
|
-
if (section === undefined || isRef(section))
|
|
18
|
+
if (section === undefined || (0, motly_ts_interface_1.isRef)(section.eq) || (0, motly_ts_interface_1.isEnvRef)(section.eq))
|
|
22
19
|
return undefined;
|
|
23
20
|
return section.properties;
|
|
24
21
|
}
|
|
@@ -37,10 +34,12 @@ function walkRefs(node, path, ancestors, root, errors) {
|
|
|
37
34
|
}
|
|
38
35
|
if (node.properties) {
|
|
39
36
|
for (const key of Object.keys(node.properties)) {
|
|
40
|
-
const
|
|
37
|
+
const child = node.properties[key];
|
|
41
38
|
path.push(key);
|
|
42
|
-
if (
|
|
43
|
-
|
|
39
|
+
// Check if child's eq is a reference (checked at property level
|
|
40
|
+
// to maintain correct ancestor depth for reference resolution)
|
|
41
|
+
if ((0, motly_ts_interface_1.isRef)(child.eq)) {
|
|
42
|
+
const errMsg = checkLink(child.eq, ancestors, root);
|
|
44
43
|
if (errMsg !== null) {
|
|
45
44
|
errors.push({
|
|
46
45
|
message: errMsg,
|
|
@@ -49,11 +48,10 @@ function walkRefs(node, path, ancestors, root, errors) {
|
|
|
49
48
|
});
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
51
|
+
// Recurse into child
|
|
52
|
+
ancestors.push(node);
|
|
53
|
+
walkRefs(child, path, ancestors, root, errors);
|
|
54
|
+
ancestors.pop();
|
|
57
55
|
path.pop();
|
|
58
56
|
}
|
|
59
57
|
}
|
|
@@ -63,8 +61,9 @@ function walkArrayRefs(arr, path, ancestors, parentNode, root, errors) {
|
|
|
63
61
|
const elem = arr[i];
|
|
64
62
|
const idxKey = `[${i}]`;
|
|
65
63
|
path.push(idxKey);
|
|
66
|
-
if
|
|
67
|
-
|
|
64
|
+
// Check if element's eq is a reference
|
|
65
|
+
if ((0, motly_ts_interface_1.isRef)(elem.eq)) {
|
|
66
|
+
const errMsg = checkLink(elem.eq, ancestors, root);
|
|
68
67
|
if (errMsg !== null) {
|
|
69
68
|
errors.push({
|
|
70
69
|
message: errMsg,
|
|
@@ -73,16 +72,17 @@ function walkArrayRefs(arr, path, ancestors, parentNode, root, errors) {
|
|
|
73
72
|
});
|
|
74
73
|
}
|
|
75
74
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
75
|
+
// Recurse into element
|
|
76
|
+
ancestors.push(parentNode);
|
|
77
|
+
walkRefs(elem, path, ancestors, root, errors);
|
|
78
|
+
ancestors.pop();
|
|
81
79
|
path.pop();
|
|
82
80
|
}
|
|
83
81
|
}
|
|
84
82
|
function checkLink(link, ancestors, root) {
|
|
85
|
-
const { ups, segments } = parseLinkString(link.linkTo);
|
|
83
|
+
const { ups, segments, error } = parseLinkString(link.linkTo);
|
|
84
|
+
if (error !== null)
|
|
85
|
+
return error;
|
|
86
86
|
let start;
|
|
87
87
|
if (ups === 0) {
|
|
88
88
|
start = root;
|
|
@@ -130,9 +130,10 @@ function parseLinkString(s) {
|
|
|
130
130
|
if (i < s.length)
|
|
131
131
|
i++; // skip ']'
|
|
132
132
|
const idx = parseInt(idxBuf, 10);
|
|
133
|
-
if (
|
|
134
|
-
|
|
133
|
+
if (isNaN(idx) || idx < 0) {
|
|
134
|
+
return { ups, segments, error: `Reference "${s}" has invalid array index [${idxBuf}]` };
|
|
135
135
|
}
|
|
136
|
+
segments.push({ kind: "index", index: idx });
|
|
136
137
|
}
|
|
137
138
|
else {
|
|
138
139
|
nameBuf += ch;
|
|
@@ -142,44 +143,29 @@ function parseLinkString(s) {
|
|
|
142
143
|
if (nameBuf.length > 0) {
|
|
143
144
|
segments.push({ kind: "name", name: nameBuf });
|
|
144
145
|
}
|
|
145
|
-
return { ups, segments };
|
|
146
|
+
return { ups, segments, error: null };
|
|
146
147
|
}
|
|
147
148
|
function resolvePath(start, segments, linkStr) {
|
|
148
|
-
let current =
|
|
149
|
+
let current = start;
|
|
149
150
|
for (const seg of segments) {
|
|
150
|
-
if (current.kind === "terminal") {
|
|
151
|
-
return `Reference "${linkStr}" could not be resolved: cannot follow path through a link`;
|
|
152
|
-
}
|
|
153
|
-
const node = current.node;
|
|
154
151
|
if (seg.kind === "name") {
|
|
155
|
-
if (!
|
|
152
|
+
if (!current.properties) {
|
|
156
153
|
return `Reference "${linkStr}" could not be resolved: property "${seg.name}" not found (node has no properties)`;
|
|
157
154
|
}
|
|
158
|
-
const child =
|
|
155
|
+
const child = current.properties[seg.name];
|
|
159
156
|
if (child === undefined) {
|
|
160
157
|
return `Reference "${linkStr}" could not be resolved: property "${seg.name}" not found`;
|
|
161
158
|
}
|
|
162
|
-
|
|
163
|
-
current = { kind: "terminal" };
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
current = { kind: "node", node: child };
|
|
167
|
-
}
|
|
159
|
+
current = child;
|
|
168
160
|
}
|
|
169
161
|
else {
|
|
170
|
-
if (
|
|
162
|
+
if (current.eq === undefined || !Array.isArray(current.eq)) {
|
|
171
163
|
return `Reference "${linkStr}" could not be resolved: index [${seg.index}] used on non-array`;
|
|
172
164
|
}
|
|
173
|
-
if (seg.index >=
|
|
174
|
-
return `Reference "${linkStr}" could not be resolved: index [${seg.index}] out of bounds (array length ${
|
|
175
|
-
}
|
|
176
|
-
const elem = node.eq[seg.index];
|
|
177
|
-
if (isRef(elem)) {
|
|
178
|
-
current = { kind: "terminal" };
|
|
179
|
-
}
|
|
180
|
-
else {
|
|
181
|
-
current = { kind: "node", node: elem };
|
|
165
|
+
if (seg.index >= current.eq.length) {
|
|
166
|
+
return `Reference "${linkStr}" could not be resolved: index [${seg.index}] out of bounds (array length ${current.eq.length})`;
|
|
182
167
|
}
|
|
168
|
+
current = current.eq[seg.index];
|
|
183
169
|
}
|
|
184
170
|
}
|
|
185
171
|
return null;
|
|
@@ -197,7 +183,7 @@ function getAdditionalPolicy(schema) {
|
|
|
197
183
|
const additional = schema.properties["Additional"];
|
|
198
184
|
if (additional === undefined)
|
|
199
185
|
return { kind: "reject" };
|
|
200
|
-
if (isRef(additional))
|
|
186
|
+
if ((0, motly_ts_interface_1.isRef)(additional.eq))
|
|
201
187
|
return { kind: "reject" };
|
|
202
188
|
const eqStr = getEqString(additional);
|
|
203
189
|
if (eqStr !== undefined) {
|
|
@@ -276,12 +262,12 @@ function makeTypeSpecNode(typeName) {
|
|
|
276
262
|
return { eq: typeName };
|
|
277
263
|
}
|
|
278
264
|
function validateValueType(value, typeSpec, types, path, errors) {
|
|
279
|
-
if (isRef(typeSpec))
|
|
265
|
+
if ((0, motly_ts_interface_1.isRef)(typeSpec.eq))
|
|
280
266
|
return;
|
|
281
267
|
// Check for union type (oneOf)
|
|
282
268
|
if (typeSpec.properties) {
|
|
283
269
|
const oneOf = typeSpec.properties["oneOf"];
|
|
284
|
-
if (oneOf !== undefined && !isRef(oneOf)) {
|
|
270
|
+
if (oneOf !== undefined && !(0, motly_ts_interface_1.isRef)(oneOf.eq)) {
|
|
285
271
|
validateUnion(value, oneOf, types, path, errors);
|
|
286
272
|
return;
|
|
287
273
|
}
|
|
@@ -289,14 +275,14 @@ function validateValueType(value, typeSpec, types, path, errors) {
|
|
|
289
275
|
// Check for enum (eq) or pattern (matches)
|
|
290
276
|
if (typeSpec.properties) {
|
|
291
277
|
const eqProp = typeSpec.properties["eq"];
|
|
292
|
-
if (eqProp !== undefined && !isRef(eqProp)) {
|
|
278
|
+
if (eqProp !== undefined && !(0, motly_ts_interface_1.isRef)(eqProp.eq)) {
|
|
293
279
|
if (Array.isArray(eqProp.eq)) {
|
|
294
280
|
validateEnum(value, eqProp.eq, path, errors);
|
|
295
281
|
return;
|
|
296
282
|
}
|
|
297
283
|
}
|
|
298
284
|
const matchesProp = typeSpec.properties["matches"];
|
|
299
|
-
if (matchesProp !== undefined && !isRef(matchesProp)) {
|
|
285
|
+
if (matchesProp !== undefined && !(0, motly_ts_interface_1.isRef)(matchesProp.eq)) {
|
|
300
286
|
const baseType = getEqString(typeSpec);
|
|
301
287
|
if (baseType !== undefined) {
|
|
302
288
|
validateBaseType(value, baseType, types, path, errors);
|
|
@@ -313,7 +299,7 @@ function validateValueType(value, typeSpec, types, path, errors) {
|
|
|
313
299
|
("Required" in typeSpec.properties ||
|
|
314
300
|
"Optional" in typeSpec.properties ||
|
|
315
301
|
"Additional" in typeSpec.properties)) {
|
|
316
|
-
if (isRef(value)) {
|
|
302
|
+
if ((0, motly_ts_interface_1.isRef)(value.eq)) {
|
|
317
303
|
errors.push({
|
|
318
304
|
message: "Expected a tag but found a link",
|
|
319
305
|
path: [...path],
|
|
@@ -382,7 +368,7 @@ function validateBaseType(value, typeName, types, path, errors) {
|
|
|
382
368
|
}
|
|
383
369
|
}
|
|
384
370
|
function validateTypeString(value, path, errors) {
|
|
385
|
-
if (isRef(value)) {
|
|
371
|
+
if ((0, motly_ts_interface_1.isRef)(value.eq)) {
|
|
386
372
|
errors.push({
|
|
387
373
|
message: 'Expected type "string" but found a link',
|
|
388
374
|
path: [...path],
|
|
@@ -399,7 +385,7 @@ function validateTypeString(value, path, errors) {
|
|
|
399
385
|
}
|
|
400
386
|
}
|
|
401
387
|
function validateTypeNumber(value, path, errors) {
|
|
402
|
-
if (isRef(value)) {
|
|
388
|
+
if ((0, motly_ts_interface_1.isRef)(value.eq)) {
|
|
403
389
|
errors.push({
|
|
404
390
|
message: 'Expected type "number" but found a link',
|
|
405
391
|
path: [...path],
|
|
@@ -416,7 +402,7 @@ function validateTypeNumber(value, path, errors) {
|
|
|
416
402
|
}
|
|
417
403
|
}
|
|
418
404
|
function validateTypeBoolean(value, path, errors) {
|
|
419
|
-
if (isRef(value)) {
|
|
405
|
+
if ((0, motly_ts_interface_1.isRef)(value.eq)) {
|
|
420
406
|
errors.push({
|
|
421
407
|
message: 'Expected type "boolean" but found a link',
|
|
422
408
|
path: [...path],
|
|
@@ -433,7 +419,7 @@ function validateTypeBoolean(value, path, errors) {
|
|
|
433
419
|
}
|
|
434
420
|
}
|
|
435
421
|
function validateTypeDate(value, path, errors) {
|
|
436
|
-
if (isRef(value)) {
|
|
422
|
+
if ((0, motly_ts_interface_1.isRef)(value.eq)) {
|
|
437
423
|
errors.push({
|
|
438
424
|
message: 'Expected type "date" but found a link',
|
|
439
425
|
path: [...path],
|
|
@@ -450,7 +436,7 @@ function validateTypeDate(value, path, errors) {
|
|
|
450
436
|
}
|
|
451
437
|
}
|
|
452
438
|
function validateTypeTag(value, path, errors) {
|
|
453
|
-
if (isRef(value)) {
|
|
439
|
+
if ((0, motly_ts_interface_1.isRef)(value.eq)) {
|
|
454
440
|
errors.push({
|
|
455
441
|
message: 'Expected type "tag" but found a link',
|
|
456
442
|
path: [...path],
|
|
@@ -459,7 +445,7 @@ function validateTypeTag(value, path, errors) {
|
|
|
459
445
|
}
|
|
460
446
|
}
|
|
461
447
|
function validateTypeFlag(value, path, errors) {
|
|
462
|
-
if (isRef(value)) {
|
|
448
|
+
if ((0, motly_ts_interface_1.isRef)(value.eq)) {
|
|
463
449
|
errors.push({
|
|
464
450
|
message: 'Expected type "flag" but found a link',
|
|
465
451
|
path: [...path],
|
|
@@ -468,7 +454,7 @@ function validateTypeFlag(value, path, errors) {
|
|
|
468
454
|
}
|
|
469
455
|
}
|
|
470
456
|
function validateArrayType(value, innerType, types, path, errors) {
|
|
471
|
-
if (isRef(value)) {
|
|
457
|
+
if ((0, motly_ts_interface_1.isRef)(value.eq)) {
|
|
472
458
|
errors.push({
|
|
473
459
|
message: `Expected type "${innerType}[]" but found a link`,
|
|
474
460
|
path: [...path],
|
|
@@ -490,7 +476,7 @@ function validateArrayType(value, innerType, types, path, errors) {
|
|
|
490
476
|
}
|
|
491
477
|
}
|
|
492
478
|
function validateEnum(value, allowed, path, errors) {
|
|
493
|
-
if (isRef(value)) {
|
|
479
|
+
if ((0, motly_ts_interface_1.isRef)(value.eq)) {
|
|
494
480
|
errors.push({
|
|
495
481
|
message: "Expected an enum value but found a link",
|
|
496
482
|
path: [...path],
|
|
@@ -512,7 +498,7 @@ function validateEnum(value, allowed, path, errors) {
|
|
|
512
498
|
return;
|
|
513
499
|
}
|
|
514
500
|
const matches = allowed.some((a) => {
|
|
515
|
-
if (isRef(a))
|
|
501
|
+
if ((0, motly_ts_interface_1.isRef)(a.eq))
|
|
516
502
|
return false;
|
|
517
503
|
const aeq = a.eq;
|
|
518
504
|
if (aeq instanceof Date && nodeEq instanceof Date) {
|
|
@@ -522,7 +508,7 @@ function validateEnum(value, allowed, path, errors) {
|
|
|
522
508
|
});
|
|
523
509
|
if (!matches) {
|
|
524
510
|
const allowedStrs = allowed
|
|
525
|
-
.filter((a) => !isRef(a))
|
|
511
|
+
.filter((a) => !(0, motly_ts_interface_1.isRef)(a.eq))
|
|
526
512
|
.map((a) => {
|
|
527
513
|
const aeq = a.eq;
|
|
528
514
|
return JSON.stringify(String(aeq));
|
|
@@ -538,7 +524,7 @@ function validatePattern(value, matchesNode, path, errors) {
|
|
|
538
524
|
const pattern = getEqString(matchesNode);
|
|
539
525
|
if (pattern === undefined)
|
|
540
526
|
return;
|
|
541
|
-
if (isRef(value)) {
|
|
527
|
+
if ((0, motly_ts_interface_1.isRef)(value.eq)) {
|
|
542
528
|
errors.push({
|
|
543
529
|
message: "Expected a value matching a pattern but found a link",
|
|
544
530
|
path: [...path],
|