@malloydata/motly-ts-parser 0.0.1
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 +62 -0
- package/build/ast.js +2 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +5 -0
- package/build/interpreter.d.ts +4 -0
- package/build/interpreter.js +236 -0
- package/build/parser.d.ts +3 -0
- package/build/parser.js +861 -0
- package/build/session.d.ts +45 -0
- package/build/session.js +135 -0
- package/build/validate.d.ts +3 -0
- package/build/validate.js +596 -0
- package/package.json +31 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateReferences = validateReferences;
|
|
4
|
+
exports.validateSchema = validateSchema;
|
|
5
|
+
// ── Helpers ─────────────────────────────────────────────────────
|
|
6
|
+
function isRef(node) {
|
|
7
|
+
return "linkTo" in node;
|
|
8
|
+
}
|
|
9
|
+
function getEqString(node) {
|
|
10
|
+
return typeof node.eq === "string" ? node.eq : undefined;
|
|
11
|
+
}
|
|
12
|
+
function valueEqString(node) {
|
|
13
|
+
if (isRef(node))
|
|
14
|
+
return undefined;
|
|
15
|
+
return getEqString(node);
|
|
16
|
+
}
|
|
17
|
+
function extractSection(node, name) {
|
|
18
|
+
if (!node.properties)
|
|
19
|
+
return undefined;
|
|
20
|
+
const section = node.properties[name];
|
|
21
|
+
if (section === undefined || isRef(section))
|
|
22
|
+
return undefined;
|
|
23
|
+
return section.properties;
|
|
24
|
+
}
|
|
25
|
+
// ── Reference Validation ────────────────────────────────────────
|
|
26
|
+
function validateReferences(root) {
|
|
27
|
+
const errors = [];
|
|
28
|
+
const path = [];
|
|
29
|
+
const ancestors = [root];
|
|
30
|
+
walkRefs(root, path, ancestors, root, errors);
|
|
31
|
+
return errors;
|
|
32
|
+
}
|
|
33
|
+
function walkRefs(node, path, ancestors, root, errors) {
|
|
34
|
+
// Check array elements in eq
|
|
35
|
+
if (node.eq !== undefined && Array.isArray(node.eq)) {
|
|
36
|
+
walkArrayRefs(node.eq, path, ancestors, node, root, errors);
|
|
37
|
+
}
|
|
38
|
+
if (node.properties) {
|
|
39
|
+
for (const key of Object.keys(node.properties)) {
|
|
40
|
+
const value = node.properties[key];
|
|
41
|
+
path.push(key);
|
|
42
|
+
if (isRef(value)) {
|
|
43
|
+
const errMsg = checkLink(value, ancestors, root);
|
|
44
|
+
if (errMsg !== null) {
|
|
45
|
+
errors.push({
|
|
46
|
+
message: errMsg,
|
|
47
|
+
path: [...path],
|
|
48
|
+
code: "unresolved-reference",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
ancestors.push(node);
|
|
54
|
+
walkRefs(value, path, ancestors, root, errors);
|
|
55
|
+
ancestors.pop();
|
|
56
|
+
}
|
|
57
|
+
path.pop();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function walkArrayRefs(arr, path, ancestors, parentNode, root, errors) {
|
|
62
|
+
for (let i = 0; i < arr.length; i++) {
|
|
63
|
+
const elem = arr[i];
|
|
64
|
+
const idxKey = `[${i}]`;
|
|
65
|
+
path.push(idxKey);
|
|
66
|
+
if (isRef(elem)) {
|
|
67
|
+
const errMsg = checkLink(elem, ancestors, root);
|
|
68
|
+
if (errMsg !== null) {
|
|
69
|
+
errors.push({
|
|
70
|
+
message: errMsg,
|
|
71
|
+
path: [...path],
|
|
72
|
+
code: "unresolved-reference",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
ancestors.push(parentNode);
|
|
78
|
+
walkRefs(elem, path, ancestors, root, errors);
|
|
79
|
+
ancestors.pop();
|
|
80
|
+
}
|
|
81
|
+
path.pop();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function checkLink(link, ancestors, root) {
|
|
85
|
+
const { ups, segments } = parseLinkString(link.linkTo);
|
|
86
|
+
let start;
|
|
87
|
+
if (ups === 0) {
|
|
88
|
+
start = root;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const idx = ancestors.length - ups;
|
|
92
|
+
if (idx < 0 || idx >= ancestors.length) {
|
|
93
|
+
return `Reference "${link.linkTo}" goes ${ups} level(s) up but only ${ancestors.length} ancestor(s) available`;
|
|
94
|
+
}
|
|
95
|
+
start = ancestors[idx];
|
|
96
|
+
}
|
|
97
|
+
return resolvePath(start, segments, link.linkTo);
|
|
98
|
+
}
|
|
99
|
+
function parseLinkString(s) {
|
|
100
|
+
let i = 0;
|
|
101
|
+
if (i < s.length && s[i] === "$")
|
|
102
|
+
i++;
|
|
103
|
+
let ups = 0;
|
|
104
|
+
while (i < s.length && s[i] === "^") {
|
|
105
|
+
ups++;
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
const segments = [];
|
|
109
|
+
let nameBuf = "";
|
|
110
|
+
while (i < s.length) {
|
|
111
|
+
const ch = s[i];
|
|
112
|
+
if (ch === ".") {
|
|
113
|
+
if (nameBuf.length > 0) {
|
|
114
|
+
segments.push({ kind: "name", name: nameBuf });
|
|
115
|
+
nameBuf = "";
|
|
116
|
+
}
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
else if (ch === "[") {
|
|
120
|
+
if (nameBuf.length > 0) {
|
|
121
|
+
segments.push({ kind: "name", name: nameBuf });
|
|
122
|
+
nameBuf = "";
|
|
123
|
+
}
|
|
124
|
+
i++;
|
|
125
|
+
let idxBuf = "";
|
|
126
|
+
while (i < s.length && s[i] !== "]") {
|
|
127
|
+
idxBuf += s[i];
|
|
128
|
+
i++;
|
|
129
|
+
}
|
|
130
|
+
if (i < s.length)
|
|
131
|
+
i++; // skip ']'
|
|
132
|
+
const idx = parseInt(idxBuf, 10);
|
|
133
|
+
if (!isNaN(idx) && idx >= 0) {
|
|
134
|
+
segments.push({ kind: "index", index: idx });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
nameBuf += ch;
|
|
139
|
+
i++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (nameBuf.length > 0) {
|
|
143
|
+
segments.push({ kind: "name", name: nameBuf });
|
|
144
|
+
}
|
|
145
|
+
return { ups, segments };
|
|
146
|
+
}
|
|
147
|
+
function resolvePath(start, segments, linkStr) {
|
|
148
|
+
let current = { kind: "node", node: start };
|
|
149
|
+
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
|
+
if (seg.kind === "name") {
|
|
155
|
+
if (!node.properties) {
|
|
156
|
+
return `Reference "${linkStr}" could not be resolved: property "${seg.name}" not found (node has no properties)`;
|
|
157
|
+
}
|
|
158
|
+
const child = node.properties[seg.name];
|
|
159
|
+
if (child === undefined) {
|
|
160
|
+
return `Reference "${linkStr}" could not be resolved: property "${seg.name}" not found`;
|
|
161
|
+
}
|
|
162
|
+
if (isRef(child)) {
|
|
163
|
+
current = { kind: "terminal" };
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
current = { kind: "node", node: child };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
if (node.eq === undefined || !Array.isArray(node.eq)) {
|
|
171
|
+
return `Reference "${linkStr}" could not be resolved: index [${seg.index}] used on non-array`;
|
|
172
|
+
}
|
|
173
|
+
if (seg.index >= node.eq.length) {
|
|
174
|
+
return `Reference "${linkStr}" could not be resolved: index [${seg.index}] out of bounds (array length ${node.eq.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 };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
// ── Schema Validation ───────────────────────────────────────────
|
|
188
|
+
function validateSchema(tag, schema) {
|
|
189
|
+
const errors = [];
|
|
190
|
+
const types = extractSection(schema, "Types");
|
|
191
|
+
validateNodeAgainstSchema(tag, schema, types, [], errors);
|
|
192
|
+
return errors;
|
|
193
|
+
}
|
|
194
|
+
function getAdditionalPolicy(schema) {
|
|
195
|
+
if (!schema.properties)
|
|
196
|
+
return { kind: "reject" };
|
|
197
|
+
const additional = schema.properties["Additional"];
|
|
198
|
+
if (additional === undefined)
|
|
199
|
+
return { kind: "reject" };
|
|
200
|
+
if (isRef(additional))
|
|
201
|
+
return { kind: "reject" };
|
|
202
|
+
const eqStr = getEqString(additional);
|
|
203
|
+
if (eqStr !== undefined) {
|
|
204
|
+
if (eqStr === "allow")
|
|
205
|
+
return { kind: "allow" };
|
|
206
|
+
if (eqStr === "reject")
|
|
207
|
+
return { kind: "reject" };
|
|
208
|
+
return { kind: "validateAs", typeName: eqStr };
|
|
209
|
+
}
|
|
210
|
+
return { kind: "allow" };
|
|
211
|
+
}
|
|
212
|
+
function validateNodeAgainstSchema(tag, schema, types, path, errors) {
|
|
213
|
+
const required = extractSection(schema, "Required");
|
|
214
|
+
const optional = extractSection(schema, "Optional");
|
|
215
|
+
const additional = getAdditionalPolicy(schema);
|
|
216
|
+
const tagProps = tag.properties;
|
|
217
|
+
// Check required properties
|
|
218
|
+
if (required) {
|
|
219
|
+
for (const key of Object.keys(required)) {
|
|
220
|
+
const propPath = [...path, key];
|
|
221
|
+
const tagValue = tagProps ? tagProps[key] : undefined;
|
|
222
|
+
if (tagValue === undefined) {
|
|
223
|
+
errors.push({
|
|
224
|
+
message: `Missing required property "${key}"`,
|
|
225
|
+
path: propPath,
|
|
226
|
+
code: "missing-required",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
validateValueType(tagValue, required[key], types, propPath, errors);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Check optional properties that exist
|
|
235
|
+
if (optional && tagProps) {
|
|
236
|
+
for (const key of Object.keys(optional)) {
|
|
237
|
+
const tagValue = tagProps[key];
|
|
238
|
+
if (tagValue !== undefined) {
|
|
239
|
+
validateValueType(tagValue, optional[key], types, [...path, key], errors);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Check for unknown properties
|
|
244
|
+
if (tagProps) {
|
|
245
|
+
const knownKeys = new Set();
|
|
246
|
+
if (required)
|
|
247
|
+
for (const k of Object.keys(required))
|
|
248
|
+
knownKeys.add(k);
|
|
249
|
+
if (optional)
|
|
250
|
+
for (const k of Object.keys(optional))
|
|
251
|
+
knownKeys.add(k);
|
|
252
|
+
for (const key of Object.keys(tagProps)) {
|
|
253
|
+
if (knownKeys.has(key))
|
|
254
|
+
continue;
|
|
255
|
+
const propPath = [...path, key];
|
|
256
|
+
switch (additional.kind) {
|
|
257
|
+
case "reject":
|
|
258
|
+
errors.push({
|
|
259
|
+
message: `Unknown property "${key}"`,
|
|
260
|
+
path: propPath,
|
|
261
|
+
code: "unknown-property",
|
|
262
|
+
});
|
|
263
|
+
break;
|
|
264
|
+
case "allow":
|
|
265
|
+
break;
|
|
266
|
+
case "validateAs": {
|
|
267
|
+
const synthetic = makeTypeSpecNode(additional.typeName);
|
|
268
|
+
validateValueType(tagProps[key], synthetic, types, propPath, errors);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function makeTypeSpecNode(typeName) {
|
|
276
|
+
return { eq: typeName };
|
|
277
|
+
}
|
|
278
|
+
function validateValueType(value, typeSpec, types, path, errors) {
|
|
279
|
+
if (isRef(typeSpec))
|
|
280
|
+
return;
|
|
281
|
+
// Check for union type (oneOf)
|
|
282
|
+
if (typeSpec.properties) {
|
|
283
|
+
const oneOf = typeSpec.properties["oneOf"];
|
|
284
|
+
if (oneOf !== undefined && !isRef(oneOf)) {
|
|
285
|
+
validateUnion(value, oneOf, types, path, errors);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Check for enum (eq) or pattern (matches)
|
|
290
|
+
if (typeSpec.properties) {
|
|
291
|
+
const eqProp = typeSpec.properties["eq"];
|
|
292
|
+
if (eqProp !== undefined && !isRef(eqProp)) {
|
|
293
|
+
if (Array.isArray(eqProp.eq)) {
|
|
294
|
+
validateEnum(value, eqProp.eq, path, errors);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const matchesProp = typeSpec.properties["matches"];
|
|
299
|
+
if (matchesProp !== undefined && !isRef(matchesProp)) {
|
|
300
|
+
const baseType = getEqString(typeSpec);
|
|
301
|
+
if (baseType !== undefined) {
|
|
302
|
+
validateBaseType(value, baseType, types, path, errors);
|
|
303
|
+
}
|
|
304
|
+
validatePattern(value, matchesProp, path, errors);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Get the type name from the spec's eq value
|
|
309
|
+
const typeName = getEqString(typeSpec);
|
|
310
|
+
if (typeName === undefined) {
|
|
311
|
+
// Nested schema (has Required/Optional/Additional)
|
|
312
|
+
if (typeSpec.properties &&
|
|
313
|
+
("Required" in typeSpec.properties ||
|
|
314
|
+
"Optional" in typeSpec.properties ||
|
|
315
|
+
"Additional" in typeSpec.properties)) {
|
|
316
|
+
if (isRef(value)) {
|
|
317
|
+
errors.push({
|
|
318
|
+
message: "Expected a tag but found a link",
|
|
319
|
+
path: [...path],
|
|
320
|
+
code: "wrong-type",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
validateNodeAgainstSchema(value, typeSpec, types, path, errors);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
validateBaseType(value, typeName, types, path, errors);
|
|
330
|
+
}
|
|
331
|
+
function validateBaseType(value, typeName, types, path, errors) {
|
|
332
|
+
// Array types: "string[]", "number[]", etc.
|
|
333
|
+
if (typeName.endsWith("[]")) {
|
|
334
|
+
const innerType = typeName.slice(0, -2);
|
|
335
|
+
validateArrayType(value, innerType, types, path, errors);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
switch (typeName) {
|
|
339
|
+
case "string":
|
|
340
|
+
validateTypeString(value, path, errors);
|
|
341
|
+
break;
|
|
342
|
+
case "number":
|
|
343
|
+
validateTypeNumber(value, path, errors);
|
|
344
|
+
break;
|
|
345
|
+
case "boolean":
|
|
346
|
+
validateTypeBoolean(value, path, errors);
|
|
347
|
+
break;
|
|
348
|
+
case "date":
|
|
349
|
+
validateTypeDate(value, path, errors);
|
|
350
|
+
break;
|
|
351
|
+
case "tag":
|
|
352
|
+
validateTypeTag(value, path, errors);
|
|
353
|
+
break;
|
|
354
|
+
case "flag":
|
|
355
|
+
validateTypeFlag(value, path, errors);
|
|
356
|
+
break;
|
|
357
|
+
case "any":
|
|
358
|
+
break;
|
|
359
|
+
default: {
|
|
360
|
+
// Custom type
|
|
361
|
+
if (types) {
|
|
362
|
+
const typeDef = types[typeName];
|
|
363
|
+
if (typeDef !== undefined) {
|
|
364
|
+
validateValueType(value, typeDef, types, path, errors);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
errors.push({
|
|
368
|
+
message: `Unknown type "${typeName}" in schema`,
|
|
369
|
+
path: [...path],
|
|
370
|
+
code: "invalid-schema",
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
errors.push({
|
|
376
|
+
message: `Unknown type "${typeName}" (no Types section in schema)`,
|
|
377
|
+
path: [...path],
|
|
378
|
+
code: "invalid-schema",
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function validateTypeString(value, path, errors) {
|
|
385
|
+
if (isRef(value)) {
|
|
386
|
+
errors.push({
|
|
387
|
+
message: 'Expected type "string" but found a link',
|
|
388
|
+
path: [...path],
|
|
389
|
+
code: "wrong-type",
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (typeof value.eq !== "string") {
|
|
394
|
+
errors.push({
|
|
395
|
+
message: 'Expected type "string"',
|
|
396
|
+
path: [...path],
|
|
397
|
+
code: "wrong-type",
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function validateTypeNumber(value, path, errors) {
|
|
402
|
+
if (isRef(value)) {
|
|
403
|
+
errors.push({
|
|
404
|
+
message: 'Expected type "number" but found a link',
|
|
405
|
+
path: [...path],
|
|
406
|
+
code: "wrong-type",
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (typeof value.eq !== "number") {
|
|
411
|
+
errors.push({
|
|
412
|
+
message: 'Expected type "number"',
|
|
413
|
+
path: [...path],
|
|
414
|
+
code: "wrong-type",
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function validateTypeBoolean(value, path, errors) {
|
|
419
|
+
if (isRef(value)) {
|
|
420
|
+
errors.push({
|
|
421
|
+
message: 'Expected type "boolean" but found a link',
|
|
422
|
+
path: [...path],
|
|
423
|
+
code: "wrong-type",
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (typeof value.eq !== "boolean") {
|
|
428
|
+
errors.push({
|
|
429
|
+
message: 'Expected type "boolean"',
|
|
430
|
+
path: [...path],
|
|
431
|
+
code: "wrong-type",
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function validateTypeDate(value, path, errors) {
|
|
436
|
+
if (isRef(value)) {
|
|
437
|
+
errors.push({
|
|
438
|
+
message: 'Expected type "date" but found a link',
|
|
439
|
+
path: [...path],
|
|
440
|
+
code: "wrong-type",
|
|
441
|
+
});
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (!(value.eq instanceof Date)) {
|
|
445
|
+
errors.push({
|
|
446
|
+
message: 'Expected type "date"',
|
|
447
|
+
path: [...path],
|
|
448
|
+
code: "wrong-type",
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function validateTypeTag(value, path, errors) {
|
|
453
|
+
if (isRef(value)) {
|
|
454
|
+
errors.push({
|
|
455
|
+
message: 'Expected type "tag" but found a link',
|
|
456
|
+
path: [...path],
|
|
457
|
+
code: "wrong-type",
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function validateTypeFlag(value, path, errors) {
|
|
462
|
+
if (isRef(value)) {
|
|
463
|
+
errors.push({
|
|
464
|
+
message: 'Expected type "flag" but found a link',
|
|
465
|
+
path: [...path],
|
|
466
|
+
code: "wrong-type",
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function validateArrayType(value, innerType, types, path, errors) {
|
|
471
|
+
if (isRef(value)) {
|
|
472
|
+
errors.push({
|
|
473
|
+
message: `Expected type "${innerType}[]" but found a link`,
|
|
474
|
+
path: [...path],
|
|
475
|
+
code: "wrong-type",
|
|
476
|
+
});
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (!Array.isArray(value.eq)) {
|
|
480
|
+
errors.push({
|
|
481
|
+
message: `Expected type "${innerType}[]" but value is not an array`,
|
|
482
|
+
path: [...path],
|
|
483
|
+
code: "wrong-type",
|
|
484
|
+
});
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
for (let i = 0; i < value.eq.length; i++) {
|
|
488
|
+
const elemPath = [...path, `[${i}]`];
|
|
489
|
+
validateBaseType(value.eq[i], innerType, types, elemPath, errors);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function validateEnum(value, allowed, path, errors) {
|
|
493
|
+
if (isRef(value)) {
|
|
494
|
+
errors.push({
|
|
495
|
+
message: "Expected an enum value but found a link",
|
|
496
|
+
path: [...path],
|
|
497
|
+
code: "wrong-type",
|
|
498
|
+
});
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const nodeEq = value.eq;
|
|
502
|
+
if (nodeEq === undefined ||
|
|
503
|
+
(typeof nodeEq !== "string" &&
|
|
504
|
+
typeof nodeEq !== "number" &&
|
|
505
|
+
typeof nodeEq !== "boolean" &&
|
|
506
|
+
!(nodeEq instanceof Date))) {
|
|
507
|
+
errors.push({
|
|
508
|
+
message: "Expected an enum value",
|
|
509
|
+
path: [...path],
|
|
510
|
+
code: "invalid-enum-value",
|
|
511
|
+
});
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const matches = allowed.some((a) => {
|
|
515
|
+
if (isRef(a))
|
|
516
|
+
return false;
|
|
517
|
+
const aeq = a.eq;
|
|
518
|
+
if (aeq instanceof Date && nodeEq instanceof Date) {
|
|
519
|
+
return aeq.getTime() === nodeEq.getTime();
|
|
520
|
+
}
|
|
521
|
+
return aeq === nodeEq;
|
|
522
|
+
});
|
|
523
|
+
if (!matches) {
|
|
524
|
+
const allowedStrs = allowed
|
|
525
|
+
.filter((a) => !isRef(a))
|
|
526
|
+
.map((a) => {
|
|
527
|
+
const aeq = a.eq;
|
|
528
|
+
return JSON.stringify(String(aeq));
|
|
529
|
+
});
|
|
530
|
+
errors.push({
|
|
531
|
+
message: `Value does not match any allowed enum value. Allowed: [${allowedStrs.join(", ")}]`,
|
|
532
|
+
path: [...path],
|
|
533
|
+
code: "invalid-enum-value",
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function validatePattern(value, matchesNode, path, errors) {
|
|
538
|
+
const pattern = getEqString(matchesNode);
|
|
539
|
+
if (pattern === undefined)
|
|
540
|
+
return;
|
|
541
|
+
if (isRef(value)) {
|
|
542
|
+
errors.push({
|
|
543
|
+
message: "Expected a value matching a pattern but found a link",
|
|
544
|
+
path: [...path],
|
|
545
|
+
code: "wrong-type",
|
|
546
|
+
});
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (typeof value.eq !== "string") {
|
|
550
|
+
errors.push({
|
|
551
|
+
message: `Expected a string matching pattern "${pattern}"`,
|
|
552
|
+
path: [...path],
|
|
553
|
+
code: "pattern-mismatch",
|
|
554
|
+
});
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
try {
|
|
558
|
+
const re = new RegExp(pattern);
|
|
559
|
+
if (!re.test(value.eq)) {
|
|
560
|
+
errors.push({
|
|
561
|
+
message: `Value "${value.eq}" does not match pattern "${pattern}"`,
|
|
562
|
+
path: [...path],
|
|
563
|
+
code: "pattern-mismatch",
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch (e) {
|
|
568
|
+
errors.push({
|
|
569
|
+
message: `Invalid regex pattern "${pattern}": ${e}`,
|
|
570
|
+
path: [...path],
|
|
571
|
+
code: "invalid-schema",
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function validateUnion(value, oneOfNode, types, path, errors) {
|
|
576
|
+
if (!Array.isArray(oneOfNode.eq))
|
|
577
|
+
return;
|
|
578
|
+
for (const typeVal of oneOfNode.eq) {
|
|
579
|
+
const typeName = valueEqString(typeVal);
|
|
580
|
+
if (typeName === undefined)
|
|
581
|
+
continue;
|
|
582
|
+
const trialErrors = [];
|
|
583
|
+
const synthetic = makeTypeSpecNode(typeName);
|
|
584
|
+
validateValueType(value, synthetic, types, path, trialErrors);
|
|
585
|
+
if (trialErrors.length === 0)
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const typeStrs = oneOfNode.eq
|
|
589
|
+
.map((v) => valueEqString(v))
|
|
590
|
+
.filter((s) => s !== undefined);
|
|
591
|
+
errors.push({
|
|
592
|
+
message: `Value does not match any type in oneOf: [${typeStrs.join(", ")}]`,
|
|
593
|
+
path: [...path],
|
|
594
|
+
code: "wrong-type",
|
|
595
|
+
});
|
|
596
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@malloydata/motly-ts-parser",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "MOTLY text to wire format parser — pure TypeScript",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "build/index.js",
|
|
7
|
+
"types": "build/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"build/"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/malloydata/motly.git",
|
|
14
|
+
"directory": "bindings/typescript/parser"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"build:test": "tsc -p test/tsconfig.json",
|
|
22
|
+
"test": "npm run build && npm run build:test && node --test build-test/test.js",
|
|
23
|
+
"clean": "rm -rf build build-test",
|
|
24
|
+
"pack": "npm run build && npm pack"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^25.2.2",
|
|
28
|
+
"motly-ts-interface": "file:../interface",
|
|
29
|
+
"typescript": "^5.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|