@malloydata/motly-ts-parser 0.2.1 → 0.3.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.
@@ -8,24 +8,28 @@ export interface MOTLYRef {
8
8
  export interface MOTLYEnvRef {
9
9
  env: string;
10
10
  }
11
+ /** What goes to the right of = (the eq slot). */
12
+ export type MOTLYValue = MOTLYScalar | MOTLYEnvRef | MOTLYPropertyValue[];
11
13
  /**
12
- * A value node in the MOTLY tree.
14
+ * A node in the MOTLY tree.
13
15
  *
14
- * - `eq` — the node's assigned value: a scalar, a reference ({@link MOTLYRef}),
15
- * or an array of child nodes
16
- * - `properties` — named child nodes (the node's "tags")
16
+ * - `eq` — the node's assigned value: a scalar, an env ref ({@link MOTLYEnvRef}),
17
+ * or an array of property values
18
+ * - `properties` — named child property values
17
19
  * - `deleted` — true if this node was explicitly deleted with `-name`
18
20
  */
19
- export interface MOTLYValue {
20
- eq?: MOTLYScalar | MOTLYRef | MOTLYEnvRef | MOTLYNode[];
21
- properties?: Record<string, MOTLYNode>;
21
+ export interface MOTLYNode {
22
+ eq?: MOTLYValue;
23
+ properties?: Record<string, MOTLYPropertyValue>;
22
24
  deleted?: boolean;
23
25
  }
24
26
  /**
25
- * A node in the MOTLY tree. Every node is a {@link MOTLYValue}.
26
- * References are represented as `eq: { linkTo: "..." }` inside a value node.
27
+ * What a property or array element leads to: either a node or a link reference.
28
+ *
29
+ * A `MOTLYRef` means "this IS that other node" — no own value, no own properties.
30
+ * A `MOTLYNode` is a full node with optional eq, properties, and deleted flag.
27
31
  */
28
- export type MOTLYNode = MOTLYValue;
32
+ export type MOTLYPropertyValue = MOTLYNode | MOTLYRef;
29
33
  /** A parse error with source location span. */
30
34
  export interface MOTLYError {
31
35
  /** Machine-readable error code (e.g. `"tag-parse-syntax-error"`). */
@@ -54,10 +58,10 @@ export interface MOTLYSchemaError {
54
58
  /** Path to the offending node (e.g. `["metadata", "name"]`). */
55
59
  path: string[];
56
60
  }
57
- /** Type guard: is this eq value a link reference? */
58
- export declare function isRef(eq: MOTLYValue["eq"]): eq is MOTLYRef;
61
+ /** Type guard: is this property value a link reference? */
62
+ export declare function isRef(pv: MOTLYPropertyValue | undefined): pv is MOTLYRef;
59
63
  /** Type guard: is this eq value an env reference? */
60
- export declare function isEnvRef(eq: MOTLYValue["eq"]): eq is MOTLYEnvRef;
64
+ export declare function isEnvRef(eq: MOTLYNode["eq"]): eq is MOTLYEnvRef;
61
65
  /** An error from reference validation. */
62
66
  export interface MOTLYValidationError {
63
67
  /** Machine-readable error code (e.g. `"unresolved-reference"`). */
@@ -2,9 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isRef = isRef;
4
4
  exports.isEnvRef = isEnvRef;
5
- /** Type guard: is this eq value a link reference? */
6
- function isRef(eq) {
7
- return typeof eq === "object" && eq !== null && "linkTo" in eq && !Array.isArray(eq) && !(eq instanceof Date);
5
+ /** Type guard: is this property value a link reference? */
6
+ function isRef(pv) {
7
+ return typeof pv === "object" && pv !== null && "linkTo" in pv && !Array.isArray(pv) && !(pv instanceof Date);
8
8
  }
9
9
  /** Type guard: is this eq value an env reference? */
10
10
  function isEnvRef(eq) {
@@ -1,3 +1,7 @@
1
- import { MOTLYValue } from "../../interface/src/types";
2
- /** Deep clone a MOTLYValue. */
3
- export declare function cloneValue(value: MOTLYValue): MOTLYValue;
1
+ import { MOTLYNode, MOTLYPropertyValue } from "../../interface/src/types";
2
+ /** Deep clone a MOTLYNode. */
3
+ export declare function cloneNode(value: MOTLYNode): MOTLYNode;
4
+ /** Deep clone a MOTLYPropertyValue (either a node or a ref). */
5
+ export declare function clonePropertyValue(pv: MOTLYPropertyValue): MOTLYPropertyValue;
6
+ /** @deprecated Use cloneNode instead. */
7
+ export declare const cloneValue: typeof cloneNode;
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.cloneValue = cloneValue;
3
+ exports.cloneValue = void 0;
4
+ exports.cloneNode = cloneNode;
5
+ exports.clonePropertyValue = clonePropertyValue;
4
6
  const types_1 = require("../../interface/src/types");
5
- /** Deep clone a MOTLYValue. */
6
- function cloneValue(value) {
7
+ /** Deep clone a MOTLYNode. */
8
+ function cloneNode(value) {
7
9
  const result = {};
8
10
  if (value.deleted)
9
11
  result.deleted = true;
@@ -12,10 +14,7 @@ function cloneValue(value) {
12
14
  result.eq = new Date(value.eq.getTime());
13
15
  }
14
16
  else if (Array.isArray(value.eq)) {
15
- result.eq = value.eq.map(cloneValue);
16
- }
17
- else if ((0, types_1.isRef)(value.eq)) {
18
- result.eq = { linkTo: value.eq.linkTo };
17
+ result.eq = value.eq.map(clonePropertyValue);
19
18
  }
20
19
  else if ((0, types_1.isEnvRef)(value.eq)) {
21
20
  result.eq = { env: value.eq.env };
@@ -27,9 +26,18 @@ function cloneValue(value) {
27
26
  if (value.properties) {
28
27
  const props = {};
29
28
  for (const key of Object.keys(value.properties)) {
30
- props[key] = cloneValue(value.properties[key]);
29
+ props[key] = clonePropertyValue(value.properties[key]);
31
30
  }
32
31
  result.properties = props;
33
32
  }
34
33
  return result;
35
34
  }
35
+ /** Deep clone a MOTLYPropertyValue (either a node or a ref). */
36
+ function clonePropertyValue(pv) {
37
+ if ((0, types_1.isRef)(pv)) {
38
+ return { linkTo: pv.linkTo };
39
+ }
40
+ return cloneNode(pv);
41
+ }
42
+ /** @deprecated Use cloneNode instead. */
43
+ exports.cloneValue = cloneNode;
@@ -1,3 +1,3 @@
1
- export type { MOTLYScalar, MOTLYRef, MOTLYEnvRef, MOTLYValue, MOTLYNode, MOTLYError, MOTLYSchemaError, MOTLYValidationError, } from "../../interface/src/types";
1
+ export type { MOTLYScalar, MOTLYRef, MOTLYEnvRef, MOTLYValue, MOTLYNode, MOTLYPropertyValue, MOTLYError, MOTLYSchemaError, MOTLYValidationError, } from "../../interface/src/types";
2
2
  export { isRef, isEnvRef } from "../../interface/src/types";
3
3
  export { MOTLYSession } from "./session";
@@ -1,4 +1,4 @@
1
1
  import { Statement } from "./ast";
2
- import { MOTLYValue, MOTLYError } from "../../interface/src/types";
3
- /** Execute a list of parsed statements against an existing MOTLYValue. */
4
- export declare function execute(statements: Statement[], root: MOTLYValue): MOTLYError[];
2
+ import { MOTLYNode, MOTLYError } from "../../interface/src/types";
3
+ /** Execute a list of parsed statements against an existing MOTLYNode. */
4
+ export declare function execute(statements: Statement[], root: MOTLYNode): MOTLYError[];
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.execute = execute;
4
4
  const types_1 = require("../../interface/src/types");
5
5
  const clone_1 = require("./clone");
6
- /** Execute a list of parsed statements against an existing MOTLYValue. */
6
+ /** Execute a list of parsed statements against an existing MOTLYNode. */
7
7
  function execute(statements, root) {
8
8
  const errors = [];
9
9
  for (const stmt of statements) {
@@ -37,16 +37,37 @@ function executeStatement(stmt, node, errors) {
37
37
  /**
38
38
  * `name = value` — set eq, preserve existing properties.
39
39
  * `name = value { props }` — set eq, then merge properties.
40
+ *
41
+ * Special case: `name = $ref` inserts a MOTLYRef directly.
42
+ * `name = $ref { props }` produces a non-fatal error (ref created, props ignored).
40
43
  */
41
44
  function executeSetEq(node, path, value, properties, errors) {
45
+ // Special case: reference value → insert as MOTLYRef
46
+ if (value.kind === "scalar" && value.value.kind === "reference") {
47
+ const refStr = formatRefString(value.value.ups, value.value.path);
48
+ if (properties !== null) {
49
+ const zero = { line: 0, column: 0, offset: 0 };
50
+ errors.push({
51
+ code: "ref-with-properties",
52
+ message: "Cannot add properties to a reference. Did you mean := (clone)?",
53
+ begin: zero,
54
+ end: zero,
55
+ });
56
+ }
57
+ const [writeKey, parent] = buildAccessPath(node, path);
58
+ getOrCreateProperties(parent)[writeKey] = { linkTo: refStr };
59
+ return;
60
+ }
42
61
  const [writeKey, parent] = buildAccessPath(node, path);
43
62
  const props = getOrCreateProperties(parent);
44
63
  // 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;
64
+ let targetPv = props[writeKey];
65
+ if (targetPv === undefined) {
66
+ targetPv = {};
67
+ props[writeKey] = targetPv;
49
68
  }
69
+ // If it was a ref, convert to empty node
70
+ const target = ensureNode(props, writeKey);
50
71
  // Set the value slot
51
72
  setEqSlot(target, value);
52
73
  // If properties block present, MERGE them
@@ -106,10 +127,10 @@ function executeAssignBoth(node, path, value, properties, errors) {
106
127
  function executeReplaceProperties(node, path, properties, errors) {
107
128
  const [writeKey, parent] = buildAccessPath(node, path);
108
129
  const result = {};
109
- // Always preserve the existing value
130
+ // Always preserve the existing value (if it's a node, not a ref)
110
131
  const parentProps = getOrCreateProperties(parent);
111
132
  const existing = parentProps[writeKey];
112
- if (existing !== undefined) {
133
+ if (existing !== undefined && !(0, types_1.isRef)(existing)) {
113
134
  result.eq = existing.eq;
114
135
  }
115
136
  for (const stmt of properties) {
@@ -120,11 +141,11 @@ function executeReplaceProperties(node, path, properties, errors) {
120
141
  function executeUpdateProperties(node, path, properties, errors) {
121
142
  const [writeKey, parent] = buildAccessPath(node, path);
122
143
  const props = getOrCreateProperties(parent);
123
- let target = props[writeKey];
124
- if (target === undefined) {
125
- target = {};
126
- props[writeKey] = target;
144
+ // Get or create the target node (merging semantics - preserves existing)
145
+ if (props[writeKey] === undefined) {
146
+ props[writeKey] = {};
127
147
  }
148
+ const target = ensureNode(props, writeKey);
128
149
  for (const stmt of properties) {
129
150
  executeStatement(stmt, target, errors);
130
151
  }
@@ -148,19 +169,17 @@ function buildAccessPath(node, path) {
148
169
  for (let i = 0; i < path.length - 1; i++) {
149
170
  const segment = path[i];
150
171
  const props = getOrCreateProperties(current);
151
- let entry = props[segment];
152
- if (entry === undefined) {
153
- entry = {};
154
- props[segment] = entry;
172
+ if (props[segment] === undefined) {
173
+ props[segment] = {};
155
174
  }
156
- current = entry;
175
+ current = ensureNode(props, segment);
157
176
  }
158
177
  return [path[path.length - 1], current];
159
178
  }
160
179
  /** Set the eq slot on a target node from a TagValue. */
161
- function setEqSlot(target, value, errors) {
180
+ function setEqSlot(target, value) {
162
181
  if (value.kind === "array") {
163
- target.eq = resolveArray(value.elements, errors ?? []);
182
+ target.eq = resolveArray(value.elements, []);
164
183
  }
165
184
  else {
166
185
  const sv = value.value;
@@ -178,8 +197,8 @@ function setEqSlot(target, value, errors) {
178
197
  target.eq = new Date(sv.value);
179
198
  break;
180
199
  case "reference":
181
- target.eq = { linkTo: formatRefString(sv.ups, sv.path) };
182
- break;
200
+ // References are handled by the caller — should not reach here
201
+ throw new Error("References should be handled before calling setEqSlot");
183
202
  case "env":
184
203
  target.eq = { env: sv.name };
185
204
  break;
@@ -189,11 +208,25 @@ function setEqSlot(target, value, errors) {
189
208
  }
190
209
  }
191
210
  }
192
- /** Resolve an array of AST elements to MOTLYNodes. */
211
+ /** Resolve an array of AST elements to MOTLYPropertyValues. */
193
212
  function resolveArray(elements, errors) {
194
213
  return elements.map((el) => resolveArrayElement(el, errors));
195
214
  }
196
215
  function resolveArrayElement(el, errors) {
216
+ // Check if the element value is a reference → becomes MOTLYRef
217
+ if (el.value !== null && el.value.kind === "scalar" && el.value.value.kind === "reference") {
218
+ const refStr = formatRefString(el.value.value.ups, el.value.value.path);
219
+ if (el.properties !== null) {
220
+ const zero = { line: 0, column: 0, offset: 0 };
221
+ errors.push({
222
+ code: "ref-with-properties",
223
+ message: "Cannot add properties to a reference. Did you mean := (clone)?",
224
+ begin: zero,
225
+ end: zero,
226
+ });
227
+ }
228
+ return { linkTo: refStr };
229
+ }
197
230
  const node = {};
198
231
  if (el.value !== null) {
199
232
  setEqSlot(node, el.value);
@@ -233,30 +266,40 @@ function resolveAndClone(root, stmtPath, ups, refPath) {
233
266
  start = root;
234
267
  }
235
268
  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
269
  const contextLen = stmtPath.length - 1 - ups;
241
270
  if (contextLen < 0) {
242
271
  throw cloneError(`Clone reference ${refStr} goes ${ups} level(s) up but only ${stmtPath.length - 1} ancestor(s) available`);
243
272
  }
244
273
  start = root;
245
274
  for (let i = 0; i < contextLen; i++) {
246
- if (!start.properties || !start.properties[stmtPath[i]]) {
275
+ if (!start.properties) {
247
276
  throw cloneError(`Clone reference ${refStr} could not be resolved: path segment "${stmtPath[i]}" not found`);
248
277
  }
249
- start = start.properties[stmtPath[i]];
278
+ const pv = start.properties[stmtPath[i]];
279
+ if (pv === undefined) {
280
+ throw cloneError(`Clone reference ${refStr} could not be resolved: path segment "${stmtPath[i]}" not found`);
281
+ }
282
+ if ((0, types_1.isRef)(pv)) {
283
+ throw cloneError(`Clone reference ${refStr} could not be resolved: path segment "${stmtPath[i]}" is a link reference`);
284
+ }
285
+ start = pv;
250
286
  }
251
287
  }
252
288
  // Follow refPath segments
253
289
  let current = start;
254
290
  for (const seg of refPath) {
255
291
  if (seg.kind === "name") {
256
- if (!current.properties || !current.properties[seg.name]) {
292
+ if (!current.properties) {
257
293
  throw cloneError(`Clone reference ${refStr} could not be resolved: property "${seg.name}" not found`);
258
294
  }
259
- current = current.properties[seg.name];
295
+ const pv = current.properties[seg.name];
296
+ if (pv === undefined) {
297
+ throw cloneError(`Clone reference ${refStr} could not be resolved: property "${seg.name}" not found`);
298
+ }
299
+ if ((0, types_1.isRef)(pv)) {
300
+ throw cloneError(`Clone reference ${refStr} could not be resolved: property "${seg.name}" is a link reference`);
301
+ }
302
+ current = pv;
260
303
  }
261
304
  else {
262
305
  if (!current.eq || !Array.isArray(current.eq)) {
@@ -265,10 +308,14 @@ function resolveAndClone(root, stmtPath, ups, refPath) {
265
308
  if (seg.index >= current.eq.length) {
266
309
  throw cloneError(`Clone reference ${refStr} could not be resolved: index [${seg.index}] out of bounds (array length ${current.eq.length})`);
267
310
  }
268
- current = current.eq[seg.index];
311
+ const elemPv = current.eq[seg.index];
312
+ if ((0, types_1.isRef)(elemPv)) {
313
+ throw cloneError(`Clone reference ${refStr} could not be resolved: index [${seg.index}] is a link reference`);
314
+ }
315
+ current = elemPv;
269
316
  }
270
317
  }
271
- return (0, clone_1.cloneValue)(current);
318
+ return (0, clone_1.cloneNode)(current);
272
319
  }
273
320
  function cloneError(message) {
274
321
  const zero = { line: 0, column: 0, offset: 0 };
@@ -280,29 +327,60 @@ function cloneError(message) {
280
327
  * if N > D. Absolute references (ups=0) are left alone.
281
328
  */
282
329
  function sanitizeClonedRefs(node, depth, errors) {
283
- if ((0, types_1.isRef)(node.eq)) {
284
- const parsed = parseRefUps(node.eq.linkTo);
330
+ // Check array elements
331
+ if (node.eq !== undefined && Array.isArray(node.eq)) {
332
+ for (let i = 0; i < node.eq.length; i++) {
333
+ sanitizeClonedPv(node.eq, i, depth + 1, errors);
334
+ }
335
+ }
336
+ // Check properties
337
+ if (node.properties) {
338
+ for (const key of Object.keys(node.properties)) {
339
+ sanitizeClonedPvInProps(node.properties, key, depth + 1, errors);
340
+ }
341
+ }
342
+ }
343
+ /** Sanitize a single property value within a cloned subtree (in an array context). */
344
+ function sanitizeClonedPv(arr, index, depth, errors) {
345
+ const pv = arr[index];
346
+ if ((0, types_1.isRef)(pv)) {
347
+ const parsed = parseRefUps(pv.linkTo);
285
348
  if (parsed.ups > 0 && parsed.ups > depth) {
286
349
  const zero = { line: 0, column: 0, offset: 0 };
287
350
  errors.push({
288
351
  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})`,
352
+ message: `Cloned reference "${pv.linkTo}" escapes the clone boundary (${parsed.ups} level(s) up from depth ${depth})`,
290
353
  begin: zero,
291
354
  end: zero,
292
355
  });
293
- delete node.eq;
356
+ // Convert to empty node
357
+ arr[index] = {};
294
358
  }
295
359
  }
296
- if (node.eq !== undefined && Array.isArray(node.eq)) {
297
- for (const elem of node.eq) {
298
- sanitizeClonedRefs(elem, depth + 1, errors);
299
- }
360
+ else {
361
+ sanitizeClonedRefs(pv, depth, errors);
300
362
  }
301
- if (node.properties) {
302
- for (const key of Object.keys(node.properties)) {
303
- sanitizeClonedRefs(node.properties[key], depth + 1, errors);
363
+ }
364
+ /** Sanitize a single property value within a cloned subtree (in a properties context). */
365
+ function sanitizeClonedPvInProps(props, key, depth, errors) {
366
+ const pv = props[key];
367
+ if ((0, types_1.isRef)(pv)) {
368
+ const parsed = parseRefUps(pv.linkTo);
369
+ if (parsed.ups > 0 && parsed.ups > depth) {
370
+ const zero = { line: 0, column: 0, offset: 0 };
371
+ errors.push({
372
+ code: "clone-reference-out-of-scope",
373
+ message: `Cloned reference "${pv.linkTo}" escapes the clone boundary (${parsed.ups} level(s) up from depth ${depth})`,
374
+ begin: zero,
375
+ end: zero,
376
+ });
377
+ // Convert to empty node
378
+ props[key] = {};
304
379
  }
305
380
  }
381
+ else {
382
+ sanitizeClonedRefs(pv, depth, errors);
383
+ }
306
384
  }
307
385
  /** Extract the ups count from a linkTo string like "$^^name". */
308
386
  function parseRefUps(linkTo) {
@@ -316,10 +394,24 @@ function parseRefUps(linkTo) {
316
394
  }
317
395
  return { ups };
318
396
  }
319
- /** Get or create the properties object on a MOTLYValue. */
397
+ /** Get or create the properties object on a MOTLYNode. */
320
398
  function getOrCreateProperties(node) {
321
399
  if (!node.properties) {
322
400
  node.properties = {};
323
401
  }
324
402
  return node.properties;
325
403
  }
404
+ /**
405
+ * Ensure the property value at props[key] is a MOTLYNode (not a MOTLYRef).
406
+ * If it's a ref, replace it with an empty node.
407
+ * Returns a mutable reference to the node.
408
+ */
409
+ function ensureNode(props, key) {
410
+ const pv = props[key];
411
+ if ((0, types_1.isRef)(pv)) {
412
+ const node = {};
413
+ props[key] = node;
414
+ return node;
415
+ }
416
+ return pv;
417
+ }
@@ -1,4 +1,4 @@
1
- import { MOTLYValue, MOTLYError, MOTLYSchemaError, MOTLYValidationError } from "../../interface/src/types";
1
+ import { MOTLYNode, MOTLYError, MOTLYSchemaError, MOTLYValidationError } from "../../interface/src/types";
2
2
  /**
3
3
  * A stateful MOTLY parsing session.
4
4
  *
@@ -26,7 +26,7 @@ export declare class MOTLYSession {
26
26
  /**
27
27
  * Return a deep clone of the session's current value.
28
28
  */
29
- getValue(): MOTLYValue;
29
+ getValue(): MOTLYNode;
30
30
  /**
31
31
  * Validate the session's value against its stored schema.
32
32
  * Returns an empty array if no schema has been set.
@@ -65,7 +65,7 @@ class MOTLYSession {
65
65
  */
66
66
  getValue() {
67
67
  this.ensureAlive();
68
- return (0, clone_1.cloneValue)(this.value);
68
+ return (0, clone_1.cloneNode)(this.value);
69
69
  }
70
70
  /**
71
71
  * Validate the session's value against its stored schema.
@@ -1,3 +1,3 @@
1
- import { MOTLYValue, MOTLYSchemaError, MOTLYValidationError } from "../../interface/src/types";
2
- export declare function validateReferences(root: MOTLYValue): MOTLYValidationError[];
3
- export declare function validateSchema(tag: MOTLYValue, schema: MOTLYValue): MOTLYSchemaError[];
1
+ import { MOTLYNode, MOTLYSchemaError, MOTLYValidationError } from "../../interface/src/types";
2
+ export declare function validateReferences(root: MOTLYNode): MOTLYValidationError[];
3
+ export declare function validateSchema(tag: MOTLYNode, schema: MOTLYNode): MOTLYSchemaError[];
@@ -6,18 +6,18 @@ const types_1 = require("../../interface/src/types");
6
6
  function getEqString(node) {
7
7
  return typeof node.eq === "string" ? node.eq : undefined;
8
8
  }
9
- function valueEqString(node) {
10
- if ((0, types_1.isRef)(node.eq) || (0, types_1.isEnvRef)(node.eq))
9
+ function pvEqString(pv) {
10
+ if ((0, types_1.isRef)(pv))
11
11
  return undefined;
12
- return getEqString(node);
12
+ return getEqString(pv);
13
13
  }
14
14
  function extractSection(node, name) {
15
15
  if (!node.properties)
16
16
  return undefined;
17
- const section = node.properties[name];
18
- if (section === undefined || (0, types_1.isRef)(section.eq) || (0, types_1.isEnvRef)(section.eq))
17
+ const pv = node.properties[name];
18
+ if (pv === undefined || (0, types_1.isRef)(pv))
19
19
  return undefined;
20
- return section.properties;
20
+ return pv.properties;
21
21
  }
22
22
  // ── Reference Validation ────────────────────────────────────────
23
23
  function validateReferences(root) {
@@ -34,12 +34,11 @@ function walkRefs(node, path, ancestors, root, errors) {
34
34
  }
35
35
  if (node.properties) {
36
36
  for (const key of Object.keys(node.properties)) {
37
- const child = node.properties[key];
37
+ const childPv = node.properties[key];
38
38
  path.push(key);
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, types_1.isRef)(child.eq)) {
42
- const errMsg = checkLink(child.eq, ancestors, root);
39
+ if ((0, types_1.isRef)(childPv)) {
40
+ // This property is a reference check it
41
+ const errMsg = checkLink(childPv.linkTo, ancestors, root);
43
42
  if (errMsg !== null) {
44
43
  errors.push({
45
44
  message: errMsg,
@@ -48,22 +47,23 @@ function walkRefs(node, path, ancestors, root, errors) {
48
47
  });
49
48
  }
50
49
  }
51
- // Recurse into child
52
- ancestors.push(node);
53
- walkRefs(child, path, ancestors, root, errors);
54
- ancestors.pop();
50
+ else {
51
+ // Recurse into child node
52
+ ancestors.push(node);
53
+ walkRefs(childPv, path, ancestors, root, errors);
54
+ ancestors.pop();
55
+ }
55
56
  path.pop();
56
57
  }
57
58
  }
58
59
  }
59
60
  function walkArrayRefs(arr, path, ancestors, parentNode, root, errors) {
60
61
  for (let i = 0; i < arr.length; i++) {
61
- const elem = arr[i];
62
+ const elemPv = arr[i];
62
63
  const idxKey = `[${i}]`;
63
64
  path.push(idxKey);
64
- // Check if element's eq is a reference
65
- if ((0, types_1.isRef)(elem.eq)) {
66
- const errMsg = checkLink(elem.eq, ancestors, root);
65
+ if ((0, types_1.isRef)(elemPv)) {
66
+ const errMsg = checkLink(elemPv.linkTo, ancestors, root);
67
67
  if (errMsg !== null) {
68
68
  errors.push({
69
69
  message: errMsg,
@@ -72,15 +72,17 @@ function walkArrayRefs(arr, path, ancestors, parentNode, root, errors) {
72
72
  });
73
73
  }
74
74
  }
75
- // Recurse into element
76
- ancestors.push(parentNode);
77
- walkRefs(elem, path, ancestors, root, errors);
78
- ancestors.pop();
75
+ else {
76
+ // Recurse into element node
77
+ ancestors.push(parentNode);
78
+ walkRefs(elemPv, path, ancestors, root, errors);
79
+ ancestors.pop();
80
+ }
79
81
  path.pop();
80
82
  }
81
83
  }
82
- function checkLink(link, ancestors, root) {
83
- const { ups, segments, error } = parseLinkString(link.linkTo);
84
+ function checkLink(linkTo, ancestors, root) {
85
+ const { ups, segments, error } = parseLinkString(linkTo);
84
86
  if (error !== null)
85
87
  return error;
86
88
  let start;
@@ -90,11 +92,11 @@ function checkLink(link, ancestors, root) {
90
92
  else {
91
93
  const idx = ancestors.length - ups;
92
94
  if (idx < 0 || idx >= ancestors.length) {
93
- return `Reference "${link.linkTo}" goes ${ups} level(s) up but only ${ancestors.length} ancestor(s) available`;
95
+ return `Reference "${linkTo}" goes ${ups} level(s) up but only ${ancestors.length} ancestor(s) available`;
94
96
  }
95
97
  start = ancestors[idx];
96
98
  }
97
- return resolvePath(start, segments, link.linkTo);
99
+ return resolvePath(start, segments, linkTo);
98
100
  }
99
101
  function parseLinkString(s) {
100
102
  let i = 0;
@@ -148,15 +150,23 @@ function parseLinkString(s) {
148
150
  function resolvePath(start, segments, linkStr) {
149
151
  let current = start;
150
152
  for (const seg of segments) {
153
+ if (current === "terminal") {
154
+ return `Reference "${linkStr}" could not be resolved: cannot follow path through a link`;
155
+ }
151
156
  if (seg.kind === "name") {
152
157
  if (!current.properties) {
153
158
  return `Reference "${linkStr}" could not be resolved: property "${seg.name}" not found (node has no properties)`;
154
159
  }
155
- const child = current.properties[seg.name];
156
- if (child === undefined) {
160
+ const childPv = current.properties[seg.name];
161
+ if (childPv === undefined) {
157
162
  return `Reference "${linkStr}" could not be resolved: property "${seg.name}" not found`;
158
163
  }
159
- current = child;
164
+ if ((0, types_1.isRef)(childPv)) {
165
+ current = "terminal";
166
+ }
167
+ else {
168
+ current = childPv;
169
+ }
160
170
  }
161
171
  else {
162
172
  if (current.eq === undefined || !Array.isArray(current.eq)) {
@@ -165,7 +175,13 @@ function resolvePath(start, segments, linkStr) {
165
175
  if (seg.index >= current.eq.length) {
166
176
  return `Reference "${linkStr}" could not be resolved: index [${seg.index}] out of bounds (array length ${current.eq.length})`;
167
177
  }
168
- current = current.eq[seg.index];
178
+ const elemPv = current.eq[seg.index];
179
+ if ((0, types_1.isRef)(elemPv)) {
180
+ current = "terminal";
181
+ }
182
+ else {
183
+ current = elemPv;
184
+ }
169
185
  }
170
186
  }
171
187
  return null;
@@ -180,12 +196,10 @@ function validateSchema(tag, schema) {
180
196
  function getAdditionalPolicy(schema) {
181
197
  if (!schema.properties)
182
198
  return { kind: "reject" };
183
- const additional = schema.properties["Additional"];
184
- if (additional === undefined)
185
- return { kind: "reject" };
186
- if ((0, types_1.isRef)(additional.eq))
199
+ const additionalPv = schema.properties["Additional"];
200
+ if (additionalPv === undefined || (0, types_1.isRef)(additionalPv))
187
201
  return { kind: "reject" };
188
- const eqStr = getEqString(additional);
202
+ const eqStr = getEqString(additionalPv);
189
203
  if (eqStr !== undefined) {
190
204
  if (eqStr === "allow")
191
205
  return { kind: "allow" };
@@ -204,8 +218,8 @@ function validateNodeAgainstSchema(tag, schema, types, path, errors) {
204
218
  if (required) {
205
219
  for (const key of Object.keys(required)) {
206
220
  const propPath = [...path, key];
207
- const tagValue = tagProps ? tagProps[key] : undefined;
208
- if (tagValue === undefined) {
221
+ const tagValuePv = tagProps ? tagProps[key] : undefined;
222
+ if (tagValuePv === undefined) {
209
223
  errors.push({
210
224
  message: `Missing required property "${key}"`,
211
225
  path: propPath,
@@ -213,16 +227,16 @@ function validateNodeAgainstSchema(tag, schema, types, path, errors) {
213
227
  });
214
228
  }
215
229
  else {
216
- validateValueType(tagValue, required[key], types, propPath, errors);
230
+ validateValueType(tagValuePv, required[key], types, propPath, errors);
217
231
  }
218
232
  }
219
233
  }
220
234
  // Check optional properties that exist
221
235
  if (optional && tagProps) {
222
236
  for (const key of Object.keys(optional)) {
223
- const tagValue = tagProps[key];
224
- if (tagValue !== undefined) {
225
- validateValueType(tagValue, optional[key], types, [...path, key], errors);
237
+ const tagValuePv = tagProps[key];
238
+ if (tagValuePv !== undefined) {
239
+ validateValueType(tagValuePv, optional[key], types, [...path, key], errors);
226
240
  }
227
241
  }
228
242
  }
@@ -261,29 +275,83 @@ function validateNodeAgainstSchema(tag, schema, types, path, errors) {
261
275
  function makeTypeSpecNode(typeName) {
262
276
  return { eq: typeName };
263
277
  }
264
- function validateValueType(value, typeSpec, types, path, errors) {
265
- if ((0, types_1.isRef)(typeSpec.eq))
278
+ function validateValueType(valuePv, typeSpecPv, types, path, errors) {
279
+ // Skip ref type specs in schema
280
+ if ((0, types_1.isRef)(typeSpecPv))
266
281
  return;
282
+ const specNode = typeSpecPv;
283
+ // If value is a ref, generate appropriate "found a link" error
284
+ if ((0, types_1.isRef)(valuePv)) {
285
+ pushRefTypeError(specNode, path, errors);
286
+ return;
287
+ }
288
+ // Value is a node
289
+ const value = valuePv;
290
+ validateNodeAgainstTypeSpec(value, specNode, types, path, errors);
291
+ }
292
+ function pushRefTypeError(specNode, path, errors) {
293
+ // Check for enum
294
+ if (specNode.properties) {
295
+ const eqProp = specNode.properties["eq"];
296
+ if (eqProp !== undefined && !(0, types_1.isRef)(eqProp)) {
297
+ if (Array.isArray(eqProp.eq)) {
298
+ errors.push({
299
+ message: "Expected an enum value but found a link",
300
+ path: [...path],
301
+ code: "wrong-type",
302
+ });
303
+ return;
304
+ }
305
+ }
306
+ if (specNode.properties["matches"] !== undefined) {
307
+ errors.push({
308
+ message: "Expected a value matching a pattern but found a link",
309
+ path: [...path],
310
+ code: "wrong-type",
311
+ });
312
+ return;
313
+ }
314
+ }
315
+ const typeName = getEqString(specNode);
316
+ if (typeName !== undefined) {
317
+ errors.push({
318
+ message: `Expected type "${typeName}" but found a link`,
319
+ path: [...path],
320
+ code: "wrong-type",
321
+ });
322
+ }
323
+ else if (specNode.properties &&
324
+ ("Required" in specNode.properties ||
325
+ "Optional" in specNode.properties ||
326
+ "Additional" in specNode.properties)) {
327
+ errors.push({
328
+ message: "Expected a tag but found a link",
329
+ path: [...path],
330
+ code: "wrong-type",
331
+ });
332
+ }
333
+ }
334
+ function validateNodeAgainstTypeSpec(value, specNode, types, path, errors) {
267
335
  // Check for union type (oneOf)
268
- if (typeSpec.properties) {
269
- const oneOf = typeSpec.properties["oneOf"];
270
- if (oneOf !== undefined && !(0, types_1.isRef)(oneOf.eq)) {
271
- validateUnion(value, oneOf, types, path, errors);
336
+ if (specNode.properties) {
337
+ const oneOfPv = specNode.properties["oneOf"];
338
+ if (oneOfPv !== undefined && !(0, types_1.isRef)(oneOfPv)) {
339
+ validateUnion(value, oneOfPv, types, path, errors);
272
340
  return;
273
341
  }
274
342
  }
275
343
  // Check for enum (eq) or pattern (matches)
276
- if (typeSpec.properties) {
277
- const eqProp = typeSpec.properties["eq"];
278
- if (eqProp !== undefined && !(0, types_1.isRef)(eqProp.eq)) {
344
+ if (specNode.properties) {
345
+ const eqProp = specNode.properties["eq"];
346
+ if (eqProp !== undefined && !(0, types_1.isRef)(eqProp)) {
279
347
  if (Array.isArray(eqProp.eq)) {
280
348
  validateEnum(value, eqProp.eq, path, errors);
281
349
  return;
282
350
  }
283
351
  }
284
- const matchesProp = typeSpec.properties["matches"];
285
- if (matchesProp !== undefined && !(0, types_1.isRef)(matchesProp.eq)) {
286
- const baseType = getEqString(typeSpec);
352
+ const matchesProp = specNode.properties["matches"];
353
+ if (matchesProp !== undefined && !(0, types_1.isRef)(matchesProp)) {
354
+ const baseType = getEqString(specNode);
287
355
  if (baseType !== undefined) {
288
356
  validateBaseType(value, baseType, types, path, errors);
289
357
  }
@@ -292,23 +360,14 @@ function validateValueType(value, typeSpec, types, path, errors) {
292
360
  }
293
361
  }
294
362
  // Get the type name from the spec's eq value
295
- const typeName = getEqString(typeSpec);
363
+ const typeName = getEqString(specNode);
296
364
  if (typeName === undefined) {
297
365
  // Nested schema (has Required/Optional/Additional)
298
- if (typeSpec.properties &&
299
- ("Required" in typeSpec.properties ||
300
- "Optional" in typeSpec.properties ||
301
- "Additional" in typeSpec.properties)) {
302
- if ((0, types_1.isRef)(value.eq)) {
303
- errors.push({
304
- message: "Expected a tag but found a link",
305
- path: [...path],
306
- code: "wrong-type",
307
- });
308
- }
309
- else {
310
- validateNodeAgainstSchema(value, typeSpec, types, path, errors);
311
- }
366
+ if (specNode.properties &&
367
+ ("Required" in specNode.properties ||
368
+ "Optional" in specNode.properties ||
369
+ "Additional" in specNode.properties)) {
370
+ validateNodeAgainstSchema(value, specNode, types, path, errors);
312
371
  }
313
372
  return;
314
373
  }
@@ -335,19 +394,22 @@ function validateBaseType(value, typeName, types, path, errors) {
335
394
  validateTypeDate(value, path, errors);
336
395
  break;
337
396
  case "tag":
338
- validateTypeTag(value, path, errors);
339
- break;
397
+ break; // tag — node exists, always valid for a non-ref
340
398
  case "flag":
341
- validateTypeFlag(value, path, errors);
342
- break;
399
+ break; // flag — presence-only, always valid for a non-ref
343
400
  case "any":
344
- break;
401
+ break; // any — always valid
345
402
  default: {
346
403
  // Custom type
347
404
  if (types) {
348
- const typeDef = types[typeName];
349
- if (typeDef !== undefined) {
350
- validateValueType(value, typeDef, types, path, errors);
405
+ const typeDefPv = types[typeName];
406
+ if (typeDefPv !== undefined) {
407
+ if ((0, types_1.isRef)(typeDefPv)) {
408
+ // Type definition is a ref — skip
409
+ }
410
+ else {
411
+ validateNodeAgainstTypeSpec(value, typeDefPv, types, path, errors);
412
+ }
351
413
  }
352
414
  else {
353
415
  errors.push({
@@ -368,14 +430,6 @@ function validateBaseType(value, typeName, types, path, errors) {
368
430
  }
369
431
  }
370
432
  function validateTypeString(value, path, errors) {
371
- if ((0, types_1.isRef)(value.eq)) {
372
- errors.push({
373
- message: 'Expected type "string" but found a link',
374
- path: [...path],
375
- code: "wrong-type",
376
- });
377
- return;
378
- }
379
433
  if (typeof value.eq !== "string") {
380
434
  errors.push({
381
435
  message: 'Expected type "string"',
@@ -385,14 +439,6 @@ function validateTypeString(value, path, errors) {
385
439
  }
386
440
  }
387
441
  function validateTypeNumber(value, path, errors) {
388
- if ((0, types_1.isRef)(value.eq)) {
389
- errors.push({
390
- message: 'Expected type "number" but found a link',
391
- path: [...path],
392
- code: "wrong-type",
393
- });
394
- return;
395
- }
396
442
  if (typeof value.eq !== "number") {
397
443
  errors.push({
398
444
  message: 'Expected type "number"',
@@ -402,14 +448,6 @@ function validateTypeNumber(value, path, errors) {
402
448
  }
403
449
  }
404
450
  function validateTypeBoolean(value, path, errors) {
405
- if ((0, types_1.isRef)(value.eq)) {
406
- errors.push({
407
- message: 'Expected type "boolean" but found a link',
408
- path: [...path],
409
- code: "wrong-type",
410
- });
411
- return;
412
- }
413
451
  if (typeof value.eq !== "boolean") {
414
452
  errors.push({
415
453
  message: 'Expected type "boolean"',
@@ -419,14 +457,6 @@ function validateTypeBoolean(value, path, errors) {
419
457
  }
420
458
  }
421
459
  function validateTypeDate(value, path, errors) {
422
- if ((0, types_1.isRef)(value.eq)) {
423
- errors.push({
424
- message: 'Expected type "date" but found a link',
425
- path: [...path],
426
- code: "wrong-type",
427
- });
428
- return;
429
- }
430
460
  if (!(value.eq instanceof Date)) {
431
461
  errors.push({
432
462
  message: 'Expected type "date"',
@@ -435,33 +465,7 @@ function validateTypeDate(value, path, errors) {
435
465
  });
436
466
  }
437
467
  }
438
- function validateTypeTag(value, path, errors) {
439
- if ((0, types_1.isRef)(value.eq)) {
440
- errors.push({
441
- message: 'Expected type "tag" but found a link',
442
- path: [...path],
443
- code: "wrong-type",
444
- });
445
- }
446
- }
447
- function validateTypeFlag(value, path, errors) {
448
- if ((0, types_1.isRef)(value.eq)) {
449
- errors.push({
450
- message: 'Expected type "flag" but found a link',
451
- path: [...path],
452
- code: "wrong-type",
453
- });
454
- }
455
- }
456
468
  function validateArrayType(value, innerType, types, path, errors) {
457
- if ((0, types_1.isRef)(value.eq)) {
458
- errors.push({
459
- message: `Expected type "${innerType}[]" but found a link`,
460
- path: [...path],
461
- code: "wrong-type",
462
- });
463
- return;
464
- }
465
469
  if (!Array.isArray(value.eq)) {
466
470
  errors.push({
467
471
  message: `Expected type "${innerType}[]" but value is not an array`,
@@ -472,18 +476,20 @@ function validateArrayType(value, innerType, types, path, errors) {
472
476
  }
473
477
  for (let i = 0; i < value.eq.length; i++) {
474
478
  const elemPath = [...path, `[${i}]`];
475
- validateBaseType(value.eq[i], innerType, types, elemPath, errors);
479
+ const elemPv = value.eq[i];
480
+ if ((0, types_1.isRef)(elemPv)) {
481
+ errors.push({
482
+ message: `Expected type "${innerType}" but found a link`,
483
+ path: elemPath,
484
+ code: "wrong-type",
485
+ });
486
+ }
487
+ else {
488
+ validateBaseType(elemPv, innerType, types, elemPath, errors);
489
+ }
476
490
  }
477
491
  }
478
492
  function validateEnum(value, allowed, path, errors) {
479
- if ((0, types_1.isRef)(value.eq)) {
480
- errors.push({
481
- message: "Expected an enum value but found a link",
482
- path: [...path],
483
- code: "wrong-type",
484
- });
485
- return;
486
- }
487
493
  const nodeEq = value.eq;
488
494
  if (nodeEq === undefined ||
489
495
  (typeof nodeEq !== "string" &&
@@ -498,7 +504,7 @@ function validateEnum(value, allowed, path, errors) {
498
504
  return;
499
505
  }
500
506
  const matches = allowed.some((a) => {
501
- if ((0, types_1.isRef)(a.eq))
507
+ if ((0, types_1.isRef)(a))
502
508
  return false;
503
509
  const aeq = a.eq;
504
510
  if (aeq instanceof Date && nodeEq instanceof Date) {
@@ -508,7 +514,7 @@ function validateEnum(value, allowed, path, errors) {
508
514
  });
509
515
  if (!matches) {
510
516
  const allowedStrs = allowed
511
- .filter((a) => !(0, types_1.isRef)(a.eq))
517
+ .filter((a) => !(0, types_1.isRef)(a))
512
518
  .map((a) => {
513
519
  const aeq = a.eq;
514
520
  return JSON.stringify(String(aeq));
@@ -524,14 +530,6 @@ function validatePattern(value, matchesNode, path, errors) {
524
530
  const pattern = getEqString(matchesNode);
525
531
  if (pattern === undefined)
526
532
  return;
527
- if ((0, types_1.isRef)(value.eq)) {
528
- errors.push({
529
- message: "Expected a value matching a pattern but found a link",
530
- path: [...path],
531
- code: "wrong-type",
532
- });
533
- return;
534
- }
535
533
  if (typeof value.eq !== "string") {
536
534
  errors.push({
537
535
  message: `Expected a string matching pattern "${pattern}"`,
@@ -561,18 +559,20 @@ function validatePattern(value, matchesNode, path, errors) {
561
559
  function validateUnion(value, oneOfNode, types, path, errors) {
562
560
  if (!Array.isArray(oneOfNode.eq))
563
561
  return;
564
- for (const typeVal of oneOfNode.eq) {
565
- const typeName = valueEqString(typeVal);
562
+ // We need to wrap value as a MOTLYPropertyValue for validate_value_type
563
+ const valuePv = value;
564
+ for (const typePv of oneOfNode.eq) {
565
+ const typeName = pvEqString(typePv);
566
566
  if (typeName === undefined)
567
567
  continue;
568
568
  const trialErrors = [];
569
569
  const synthetic = makeTypeSpecNode(typeName);
570
- validateValueType(value, synthetic, types, path, trialErrors);
570
+ validateValueType(valuePv, synthetic, types, path, trialErrors);
571
571
  if (trialErrors.length === 0)
572
572
  return;
573
573
  }
574
574
  const typeStrs = oneOfNode.eq
575
- .map((v) => valueEqString(v))
575
+ .map((v) => pvEqString(v))
576
576
  .filter((s) => s !== undefined);
577
577
  errors.push({
578
578
  message: `Value does not match any type in oneOf: [${typeStrs.join(", ")}]`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/motly-ts-parser",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "MOTLY text to wire format parser — pure TypeScript",
5
5
  "license": "MIT",
6
6
  "main": "build/parser/src/index.js",