@malloydata/malloy-tag 0.0.339 → 0.0.340

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.
Files changed (45) hide show
  1. package/dist/index.d.ts +1 -3
  2. package/dist/index.js +4 -5
  3. package/dist/index.js.map +1 -1
  4. package/dist/{peggy/index.d.ts → parser.d.ts} +13 -4
  5. package/dist/parser.js +181 -0
  6. package/dist/parser.js.map +1 -0
  7. package/package.json +13 -6
  8. package/src/index.ts +1 -3
  9. package/src/parser.ts +203 -0
  10. package/CONTEXT.md +0 -173
  11. package/README.md +0 -0
  12. package/dist/peggy/dist/peg-tag-parser.d.ts +0 -11
  13. package/dist/peggy/dist/peg-tag-parser.js +0 -3130
  14. package/dist/peggy/dist/peg-tag-parser.js.map +0 -1
  15. package/dist/peggy/index.js +0 -117
  16. package/dist/peggy/index.js.map +0 -1
  17. package/dist/peggy/interpreter.d.ts +0 -32
  18. package/dist/peggy/interpreter.js +0 -208
  19. package/dist/peggy/interpreter.js.map +0 -1
  20. package/dist/peggy/statements.d.ts +0 -51
  21. package/dist/peggy/statements.js +0 -7
  22. package/dist/peggy/statements.js.map +0 -1
  23. package/dist/schema.d.ts +0 -41
  24. package/dist/schema.js +0 -573
  25. package/dist/schema.js.map +0 -1
  26. package/dist/schema.spec.d.ts +0 -1
  27. package/dist/schema.spec.js +0 -980
  28. package/dist/schema.spec.js.map +0 -1
  29. package/dist/tags.spec.d.ts +0 -8
  30. package/dist/tags.spec.js +0 -884
  31. package/dist/tags.spec.js.map +0 -1
  32. package/dist/util.spec.d.ts +0 -1
  33. package/dist/util.spec.js +0 -43
  34. package/dist/util.spec.js.map +0 -1
  35. package/src/motly-schema.motly +0 -52
  36. package/src/peggy/dist/peg-tag-parser.js +0 -2790
  37. package/src/peggy/index.ts +0 -89
  38. package/src/peggy/interpreter.ts +0 -265
  39. package/src/peggy/malloy-tag.peggy +0 -224
  40. package/src/peggy/statements.ts +0 -49
  41. package/src/schema.spec.ts +0 -1280
  42. package/src/schema.ts +0 -852
  43. package/src/tags.spec.ts +0 -967
  44. package/src/util.spec.ts +0 -43
  45. package/tsconfig.json +0 -12
@@ -1,1280 +0,0 @@
1
- /*
2
- * Copyright Contributors to the Malloy project
3
- * SPDX-License-Identifier: MIT
4
- */
5
-
6
- import * as fs from 'fs';
7
- import * as path from 'path';
8
- import {parseTag} from './peggy';
9
- import {validateTag} from './schema';
10
-
11
- // Load the meta-schema from file
12
- const metaSchemaPath = path.join(__dirname, 'motly-schema.motly');
13
- const metaSchemaSource = fs.readFileSync(metaSchemaPath, 'utf-8');
14
- const {tag: metaSchema, log: metaSchemaLog} = parseTag(metaSchemaSource);
15
-
16
- // Fail fast if meta-schema has parse errors
17
- if (metaSchemaLog.length > 0) {
18
- throw new Error(
19
- `Meta-schema failed to parse:\n${metaSchemaLog.map(e => e.message).join('\n')}`
20
- );
21
- }
22
-
23
- describe('schema validation', () => {
24
- describe('missing required properties', () => {
25
- test('errors when required property is missing', () => {
26
- const {tag} = parseTag('');
27
- const {tag: schema} = parseTag('Required: { name=string }');
28
-
29
- const errors = validateTag(tag, schema);
30
-
31
- expect(errors).toHaveLength(1);
32
- expect(errors[0]).toEqual({
33
- message: "Missing required property 'name'",
34
- path: ['name'],
35
- code: 'missing-required',
36
- });
37
- });
38
-
39
- test('errors for multiple missing required properties', () => {
40
- const {tag} = parseTag('');
41
- const {tag: schema} = parseTag('Required: { name=string age=number }');
42
-
43
- const errors = validateTag(tag, schema);
44
-
45
- expect(errors).toHaveLength(2);
46
- expect(errors.map(e => e.path)).toEqual([['name'], ['age']]);
47
- });
48
-
49
- test('errors for missing nested required property', () => {
50
- const {tag} = parseTag('size { width=10 }');
51
- const {tag: schema} = parseTag(
52
- 'Required: { size: { Required: { width=number height=number } } }'
53
- );
54
-
55
- const errors = validateTag(tag, schema);
56
-
57
- expect(errors).toHaveLength(1);
58
- expect(errors[0]).toEqual({
59
- message: "Missing required property 'height'",
60
- path: ['size', 'height'],
61
- code: 'missing-required',
62
- });
63
- });
64
- });
65
-
66
- describe('wrong type errors', () => {
67
- test('errors when string expected but number provided', () => {
68
- const {tag} = parseTag('name=42');
69
- const {tag: schema} = parseTag('Required: { name=string }');
70
-
71
- const errors = validateTag(tag, schema);
72
-
73
- expect(errors).toHaveLength(1);
74
- expect(errors[0]).toEqual({
75
- message:
76
- "Property 'name' has wrong type: expected 'string', got 'number'",
77
- path: ['name'],
78
- code: 'wrong-type',
79
- });
80
- });
81
-
82
- test('errors when number expected but string provided', () => {
83
- const {tag} = parseTag('age=hello');
84
- const {tag: schema} = parseTag('Required: { age=number }');
85
-
86
- const errors = validateTag(tag, schema);
87
-
88
- expect(errors).toHaveLength(1);
89
- expect(errors[0]).toEqual({
90
- message:
91
- "Property 'age' has wrong type: expected 'number', got 'string'",
92
- path: ['age'],
93
- code: 'wrong-type',
94
- });
95
- });
96
-
97
- test('errors when boolean expected but string provided', () => {
98
- const {tag} = parseTag('enabled=yes');
99
- const {tag: schema} = parseTag('Required: { enabled=boolean }');
100
-
101
- const errors = validateTag(tag, schema);
102
-
103
- expect(errors).toHaveLength(1);
104
- expect(errors[0].code).toBe('wrong-type');
105
- expect(errors[0].message).toContain("expected 'boolean'");
106
- });
107
-
108
- test('errors when date expected but string provided', () => {
109
- const {tag} = parseTag('created=yesterday');
110
- const {tag: schema} = parseTag('Required: { created=date }');
111
-
112
- const errors = validateTag(tag, schema);
113
-
114
- expect(errors).toHaveLength(1);
115
- expect(errors[0].code).toBe('wrong-type');
116
- expect(errors[0].message).toContain("expected 'date'");
117
- });
118
-
119
- test('errors when tag expected but scalar provided', () => {
120
- const {tag} = parseTag('config=value');
121
- const {tag: schema} = parseTag('Required: { config=tag }');
122
-
123
- const errors = validateTag(tag, schema);
124
-
125
- expect(errors).toHaveLength(1);
126
- expect(errors[0].code).toBe('wrong-type');
127
- expect(errors[0].message).toContain("expected 'tag'");
128
- });
129
-
130
- test('validates tag type correctly', () => {
131
- const {tag} = parseTag('config { a=1 b=2 }');
132
- const {tag: schema} = parseTag('Required: { config=tag }');
133
-
134
- const errors = validateTag(tag, schema);
135
-
136
- expect(errors).toHaveLength(0);
137
- });
138
-
139
- test('validates flag type correctly', () => {
140
- const {tag} = parseTag('hidden readonly');
141
- const {tag: schema} = parseTag('Required: { hidden=flag readonly=flag }');
142
-
143
- const errors = validateTag(tag, schema);
144
-
145
- expect(errors).toHaveLength(0);
146
- });
147
-
148
- test('errors when flag expected but scalar provided', () => {
149
- const {tag} = parseTag('hidden=@true');
150
- const {tag: schema} = parseTag('Required: { hidden=flag }');
151
-
152
- const errors = validateTag(tag, schema);
153
-
154
- expect(errors).toHaveLength(1);
155
- expect(errors[0].code).toBe('wrong-type');
156
- expect(errors[0].message).toContain("expected 'flag'");
157
- expect(errors[0].message).toContain("got 'boolean'");
158
- });
159
-
160
- test('errors when flag expected but tag with properties provided', () => {
161
- const {tag} = parseTag('hidden { x=1 }');
162
- const {tag: schema} = parseTag('Required: { hidden=flag }');
163
-
164
- const errors = validateTag(tag, schema);
165
-
166
- expect(errors).toHaveLength(1);
167
- expect(errors[0].code).toBe('wrong-type');
168
- expect(errors[0].message).toContain("expected 'flag'");
169
- expect(errors[0].message).toContain("got 'tag'");
170
- });
171
-
172
- test('optional flag works when present', () => {
173
- const {tag} = parseTag('name=test deprecated');
174
- const {tag: schema} = parseTag(`
175
- Required: { name=string }
176
- Optional: { deprecated=flag }
177
- `);
178
-
179
- const errors = validateTag(tag, schema);
180
-
181
- expect(errors).toHaveLength(0);
182
- });
183
-
184
- test('optional flag works when absent', () => {
185
- const {tag} = parseTag('name=test');
186
- const {tag: schema} = parseTag(`
187
- Required: { name=string }
188
- Optional: { deprecated=flag }
189
- `);
190
-
191
- const errors = validateTag(tag, schema);
192
-
193
- expect(errors).toHaveLength(0);
194
- });
195
- });
196
-
197
- describe('unknown property errors', () => {
198
- test('errors on unknown property', () => {
199
- const {tag} = parseTag('name=alice unknown=value');
200
- const {tag: schema} = parseTag('Required: { name=string }');
201
-
202
- const errors = validateTag(tag, schema);
203
-
204
- expect(errors).toHaveLength(1);
205
- expect(errors[0]).toEqual({
206
- message: "Unknown property 'unknown'",
207
- path: ['unknown'],
208
- code: 'unknown-property',
209
- });
210
- });
211
-
212
- test('allows unknown properties with Additional flag', () => {
213
- const {tag} = parseTag('name=alice extra=value');
214
- const {tag: schema} = parseTag('Additional Required: { name=string }');
215
-
216
- const errors = validateTag(tag, schema);
217
-
218
- expect(errors).toHaveLength(0);
219
- });
220
-
221
- test('errors on unknown nested property', () => {
222
- const {tag} = parseTag('size { width=10 extra=5 }');
223
- const {tag: schema} = parseTag(
224
- 'Required: { size: { Required: { width=number } } }'
225
- );
226
-
227
- const errors = validateTag(tag, schema);
228
-
229
- expect(errors).toHaveLength(1);
230
- expect(errors[0]).toEqual({
231
- message: "Unknown property 'extra'",
232
- path: ['size', 'extra'],
233
- code: 'unknown-property',
234
- });
235
- });
236
- });
237
-
238
- describe('valid tags pass validation', () => {
239
- test('valid tag with required properties', () => {
240
- const {tag} = parseTag('name=alice age=30');
241
- const {tag: schema} = parseTag('Required: { name=string age=number }');
242
-
243
- const errors = validateTag(tag, schema);
244
-
245
- expect(errors).toHaveLength(0);
246
- });
247
-
248
- test('valid tag with optional properties', () => {
249
- const {tag} = parseTag('name=alice');
250
- const {tag: schema} = parseTag(
251
- 'Required: { name=string } Optional: { age=number }'
252
- );
253
-
254
- const errors = validateTag(tag, schema);
255
-
256
- expect(errors).toHaveLength(0);
257
- });
258
-
259
- test('valid tag with optional property present', () => {
260
- const {tag} = parseTag('name=alice age=30');
261
- const {tag: schema} = parseTag(
262
- 'Required: { name=string } Optional: { age=number }'
263
- );
264
-
265
- const errors = validateTag(tag, schema);
266
-
267
- expect(errors).toHaveLength(0);
268
- });
269
-
270
- test('valid nested properties', () => {
271
- const {tag} = parseTag('size { width=100 height=200 }');
272
- const {tag: schema} = parseTag(
273
- 'Required: { size: { Required: { width=number height=number } } }'
274
- );
275
-
276
- const errors = validateTag(tag, schema);
277
-
278
- expect(errors).toHaveLength(0);
279
- });
280
- });
281
-
282
- describe('array type validation', () => {
283
- test('validates string[] type', () => {
284
- const {tag} = parseTag('colors=[red, green, blue]');
285
- const {tag: schema} = parseTag('Required: { colors="string[]" }');
286
-
287
- const errors = validateTag(tag, schema);
288
-
289
- expect(errors).toHaveLength(0);
290
- });
291
-
292
- test('errors when string[] expected but number[] provided', () => {
293
- const {tag} = parseTag('values=[1, 2, 3]');
294
- const {tag: schema} = parseTag('Required: { values="string[]" }');
295
-
296
- const errors = validateTag(tag, schema);
297
-
298
- expect(errors).toHaveLength(1);
299
- expect(errors[0].code).toBe('wrong-type');
300
- expect(errors[0].message).toContain("expected 'string[]'");
301
- expect(errors[0].message).toContain("got 'number[]'");
302
- });
303
-
304
- test('validates number[] type', () => {
305
- const {tag} = parseTag('counts=[1, 2, 3]');
306
- const {tag: schema} = parseTag('Required: { counts="number[]" }');
307
-
308
- const errors = validateTag(tag, schema);
309
-
310
- expect(errors).toHaveLength(0);
311
- });
312
-
313
- test('validates boolean[] type', () => {
314
- const {tag} = parseTag('flags=[@true, @false, @true]');
315
- const {tag: schema} = parseTag('Required: { flags="boolean[]" }');
316
-
317
- const errors = validateTag(tag, schema);
318
-
319
- expect(errors).toHaveLength(0);
320
- });
321
-
322
- test('validates date[] type', () => {
323
- const {tag} = parseTag('dates=[@2024-01-01, @2024-02-01]');
324
- const {tag: schema} = parseTag('Required: { dates="date[]" }');
325
-
326
- const errors = validateTag(tag, schema);
327
-
328
- expect(errors).toHaveLength(0);
329
- });
330
-
331
- test('any[] accepts any array', () => {
332
- const {tag} = parseTag('items=[1, 2, 3]');
333
- const {tag: schema} = parseTag('Required: { items="any[]" }');
334
-
335
- const errors = validateTag(tag, schema);
336
-
337
- expect(errors).toHaveLength(0);
338
- });
339
-
340
- test('errors when array expected but scalar provided', () => {
341
- const {tag} = parseTag('colors=red');
342
- const {tag: schema} = parseTag('Required: { colors="string[]" }');
343
-
344
- const errors = validateTag(tag, schema);
345
-
346
- expect(errors).toHaveLength(1);
347
- expect(errors[0].code).toBe('wrong-type');
348
- });
349
-
350
- test('empty array matches any array type', () => {
351
- const {tag} = parseTag('items=[]');
352
- const {tag: schema} = parseTag('Required: { items="string[]" }');
353
-
354
- const errors = validateTag(tag, schema);
355
-
356
- expect(errors).toHaveLength(0);
357
- });
358
- });
359
-
360
- describe('any type validation', () => {
361
- test('any accepts string', () => {
362
- const {tag} = parseTag('value=hello');
363
- const {tag: schema} = parseTag('Required: { value=any }');
364
-
365
- const errors = validateTag(tag, schema);
366
-
367
- expect(errors).toHaveLength(0);
368
- });
369
-
370
- test('any accepts number', () => {
371
- const {tag} = parseTag('value=42');
372
- const {tag: schema} = parseTag('Required: { value=any }');
373
-
374
- const errors = validateTag(tag, schema);
375
-
376
- expect(errors).toHaveLength(0);
377
- });
378
-
379
- test('any accepts boolean', () => {
380
- const {tag} = parseTag('value=@true');
381
- const {tag: schema} = parseTag('Required: { value=any }');
382
-
383
- const errors = validateTag(tag, schema);
384
-
385
- expect(errors).toHaveLength(0);
386
- });
387
-
388
- test('any accepts date', () => {
389
- const {tag} = parseTag('value=@2024-01-15');
390
- const {tag: schema} = parseTag('Required: { value=any }');
391
-
392
- const errors = validateTag(tag, schema);
393
-
394
- expect(errors).toHaveLength(0);
395
- });
396
-
397
- test('any accepts tag', () => {
398
- const {tag} = parseTag('value { a=1 }');
399
- const {tag: schema} = parseTag('Required: { value=any }');
400
-
401
- const errors = validateTag(tag, schema);
402
-
403
- expect(errors).toHaveLength(0);
404
- });
405
-
406
- test('any accepts array', () => {
407
- const {tag} = parseTag('value=[1, 2, 3]');
408
- const {tag: schema} = parseTag('Required: { value=any }');
409
-
410
- const errors = validateTag(tag, schema);
411
-
412
- expect(errors).toHaveLength(0);
413
- });
414
-
415
- test('any accepts flag', () => {
416
- const {tag} = parseTag('value');
417
- const {tag: schema} = parseTag('Required: { value=any }');
418
-
419
- const errors = validateTag(tag, schema);
420
-
421
- expect(errors).toHaveLength(0);
422
- });
423
- });
424
-
425
- describe('full form type syntax', () => {
426
- test('validates with type in full form', () => {
427
- const {tag} = parseTag('name=alice');
428
- const {tag: schema} = parseTag('Required: { name: { Type=string } }');
429
-
430
- const errors = validateTag(tag, schema);
431
-
432
- expect(errors).toHaveLength(0);
433
- });
434
-
435
- test('validates nested with full form', () => {
436
- const {tag} = parseTag('person { name=alice age=30 }');
437
- const {tag: schema} = parseTag(`
438
- Required: {
439
- person: {
440
- Type=tag
441
- Required: {
442
- name=string
443
- age=number
444
- }
445
- }
446
- }
447
- `);
448
-
449
- const errors = validateTag(tag, schema);
450
-
451
- expect(errors).toHaveLength(0);
452
- });
453
-
454
- test('errors on type mismatch with full form', () => {
455
- const {tag} = parseTag('name=42');
456
- const {tag: schema} = parseTag('Required: { name: { Type=string } }');
457
-
458
- const errors = validateTag(tag, schema);
459
-
460
- expect(errors).toHaveLength(1);
461
- expect(errors[0].code).toBe('wrong-type');
462
- });
463
- });
464
-
465
- describe('optional property type validation', () => {
466
- test('validates type of optional property when present', () => {
467
- const {tag} = parseTag('name=alice age=notanumber');
468
- const {tag: schema} = parseTag(
469
- 'Required: { name=string } Optional: { age=number }'
470
- );
471
-
472
- const errors = validateTag(tag, schema);
473
-
474
- expect(errors).toHaveLength(1);
475
- expect(errors[0]).toEqual({
476
- message:
477
- "Property 'age' has wrong type: expected 'number', got 'string'",
478
- path: ['age'],
479
- code: 'wrong-type',
480
- });
481
- });
482
- });
483
-
484
- describe('complex schemas', () => {
485
- test('validates complex nested schema', () => {
486
- const {tag} = parseTag(`
487
- config {
488
- database {
489
- host=localhost
490
- port=5432
491
- }
492
- features {
493
- enabled=@true
494
- flags=[@true, @false]
495
- }
496
- }
497
- `);
498
- const {tag: schema} = parseTag(`
499
- Required: {
500
- config: {
501
- Required: {
502
- database: {
503
- Required: {
504
- host=string
505
- port=number
506
- }
507
- }
508
- features: {
509
- Required: {
510
- enabled=boolean
511
- }
512
- Optional: {
513
- flags="boolean[]"
514
- }
515
- }
516
- }
517
- }
518
- }
519
- `);
520
-
521
- const errors = validateTag(tag, schema);
522
-
523
- expect(errors).toHaveLength(0);
524
- });
525
-
526
- test('collects multiple errors from complex schema', () => {
527
- const {tag} = parseTag(`
528
- config {
529
- database {
530
- host=123
531
- }
532
- extra=bad
533
- }
534
- `);
535
- const {tag: schema} = parseTag(`
536
- Required: {
537
- config: {
538
- Required: {
539
- database: {
540
- Required: {
541
- host=string
542
- port=number
543
- }
544
- }
545
- }
546
- }
547
- }
548
- `);
549
-
550
- const errors = validateTag(tag, schema);
551
-
552
- // Should have: wrong type for host, missing port, unknown extra
553
- expect(errors.length).toBeGreaterThanOrEqual(3);
554
- expect(errors.map(e => e.code)).toContain('wrong-type');
555
- expect(errors.map(e => e.code)).toContain('missing-required');
556
- expect(errors.map(e => e.code)).toContain('unknown-property');
557
- });
558
- });
559
-
560
- describe('edge cases', () => {
561
- test('empty tag against schema with only optional properties', () => {
562
- const {tag} = parseTag('');
563
- const {tag: schema} = parseTag('Optional: { name=string }');
564
-
565
- const errors = validateTag(tag, schema);
566
-
567
- expect(errors).toHaveLength(0);
568
- });
569
-
570
- test('empty schema passes any tag', () => {
571
- const {tag} = parseTag('anything=goes here=too');
572
- const {tag: schema} = parseTag('');
573
-
574
- const errors = validateTag(tag, schema);
575
-
576
- // Empty schema = no rules = all lawful
577
- expect(errors).toHaveLength(0);
578
- });
579
-
580
- test('empty schema with Additional passes any tag', () => {
581
- const {tag} = parseTag('anything=goes here=too');
582
- const {tag: schema} = parseTag('Additional');
583
-
584
- const errors = validateTag(tag, schema);
585
-
586
- expect(errors).toHaveLength(0);
587
- });
588
-
589
- test('property with no type in schema allows any value', () => {
590
- const {tag} = parseTag('name=42');
591
- const {tag: schema} = parseTag('Required: { name }');
592
-
593
- const errors = validateTag(tag, schema);
594
-
595
- // No type specified, so any value is OK
596
- expect(errors).toHaveLength(0);
597
- });
598
-
599
- test('mixed array reports as mixed type', () => {
600
- const {tag} = parseTag('items=[1, hello, @true]');
601
- const {tag: schema} = parseTag('Required: { items="string[]" }');
602
-
603
- const errors = validateTag(tag, schema);
604
-
605
- expect(errors).toHaveLength(1);
606
- expect(errors[0].code).toBe('wrong-type');
607
- expect(errors[0].message).toContain("got 'mixed[]'");
608
- });
609
-
610
- test('any[] accepts mixed arrays', () => {
611
- const {tag} = parseTag('items=[1, hello, @true]');
612
- const {tag: schema} = parseTag('Required: { items="any[]" }');
613
-
614
- const errors = validateTag(tag, schema);
615
-
616
- expect(errors).toHaveLength(0);
617
- });
618
-
619
- test('value with properties validates only value type', () => {
620
- const {tag} = parseTag('name=alice { extra=1 }');
621
- const {tag: schema} = parseTag('Required: { name=string }');
622
-
623
- const errors = validateTag(tag, schema);
624
-
625
- // Only value type is checked; nested properties aren't validated
626
- // unless schema has required/optional sections
627
- expect(errors).toHaveLength(0);
628
- });
629
-
630
- test('value with properties and nested schema (full form)', () => {
631
- const {tag} = parseTag('name=alice { extra=1 }');
632
- const {tag: schema} = parseTag(`
633
- Required: {
634
- name: {
635
- Type=string
636
- Optional: { extra=number }
637
- }
638
- }
639
- `);
640
-
641
- const errors = validateTag(tag, schema);
642
-
643
- expect(errors).toHaveLength(0);
644
- });
645
-
646
- test('value with properties and nested schema (shorthand)', () => {
647
- const {tag} = parseTag('name=alice { extra=1 }');
648
- const {tag: schema} = parseTag(`
649
- Required: {
650
- name=string { Optional: { extra=number } }
651
- }
652
- `);
653
-
654
- const errors = validateTag(tag, schema);
655
-
656
- expect(errors).toHaveLength(0);
657
- });
658
-
659
- test('arrays of objects have tag element type', () => {
660
- const {tag} = parseTag('items=[{a=1}, {b=2}]');
661
- const {tag: schema} = parseTag('Required: { items="string[]" }');
662
-
663
- const errors = validateTag(tag, schema);
664
-
665
- expect(errors).toHaveLength(1);
666
- expect(errors[0].code).toBe('wrong-type');
667
- expect(errors[0].message).toContain("got 'tag[]'");
668
- });
669
-
670
- test('tag[] validates arrays of objects', () => {
671
- const {tag} = parseTag('items=[{a=1}, {b=2}]');
672
- const {tag: schema} = parseTag('Required: { items="tag[]" }');
673
-
674
- const errors = validateTag(tag, schema);
675
-
676
- expect(errors).toHaveLength(0);
677
- });
678
-
679
- test('tag[] rejects arrays of scalars', () => {
680
- const {tag} = parseTag('items=[1, 2, 3]');
681
- const {tag: schema} = parseTag('Required: { items="tag[]" }');
682
-
683
- const errors = validateTag(tag, schema);
684
-
685
- expect(errors).toHaveLength(1);
686
- expect(errors[0].code).toBe('wrong-type');
687
- expect(errors[0].message).toContain("expected 'tag[]'");
688
- });
689
-
690
- test('tag[] with element schema validates each element', () => {
691
- const {tag} = parseTag(
692
- 'items=[{size=10 color=red}, {size=20 color=blue}]'
693
- );
694
- const {tag: schema} = parseTag(
695
- 'Required: { items="tag[]" { Required: { size=number color=string } } }'
696
- );
697
-
698
- const errors = validateTag(tag, schema);
699
-
700
- expect(errors).toHaveLength(0);
701
- });
702
-
703
- test('tag[] element schema reports errors with index in path', () => {
704
- const {tag} = parseTag(
705
- 'items=[{size=10 color=red}, {size=bad color=blue}]'
706
- );
707
- const {tag: schema} = parseTag(
708
- 'Required: { items="tag[]" { Required: { size=number color=string } } }'
709
- );
710
-
711
- const errors = validateTag(tag, schema);
712
-
713
- expect(errors).toHaveLength(1);
714
- expect(errors[0].code).toBe('wrong-type');
715
- expect(errors[0].path).toEqual(['items', '1', 'size']);
716
- });
717
-
718
- test('tag[] element schema catches missing required', () => {
719
- const {tag} = parseTag('items=[{size=10}, {color=blue}]');
720
- const {tag: schema} = parseTag(
721
- 'Required: { items="tag[]" { Required: { size=number color=string } } }'
722
- );
723
-
724
- const errors = validateTag(tag, schema);
725
-
726
- expect(errors).toHaveLength(2);
727
- expect(errors[0].path).toEqual(['items', '0', 'color']);
728
- expect(errors[1].path).toEqual(['items', '1', 'size']);
729
- });
730
-
731
- test('tag[] element schema catches unknown properties', () => {
732
- const {tag} = parseTag('items=[{size=10 color=red extra=bad}]');
733
- const {tag: schema} = parseTag(
734
- 'Required: { items="tag[]" { Required: { size=number color=string } } }'
735
- );
736
-
737
- const errors = validateTag(tag, schema);
738
-
739
- expect(errors).toHaveLength(1);
740
- expect(errors[0].code).toBe('unknown-property');
741
- expect(errors[0].path).toEqual(['items', '0', 'extra']);
742
- });
743
-
744
- test('misspelled type in schema reports invalid-schema error', () => {
745
- const {tag} = parseTag('name=42');
746
- const {tag: schema} = parseTag('Required: { name=stirng }'); // typo
747
-
748
- const errors = validateTag(tag, schema);
749
-
750
- expect(errors).toHaveLength(1);
751
- expect(errors[0].code).toBe('invalid-schema');
752
- expect(errors[0].message).toContain("Invalid type 'stirng'");
753
- });
754
-
755
- test('invalid type in full form reports invalid-schema error', () => {
756
- const {tag} = parseTag('name=42');
757
- const {tag: schema} = parseTag('Required: { name: { Type=nubmer } }'); // typo
758
-
759
- const errors = validateTag(tag, schema);
760
-
761
- expect(errors).toHaveLength(1);
762
- expect(errors[0].code).toBe('invalid-schema');
763
- expect(errors[0].message).toContain("Invalid type 'nubmer'");
764
- });
765
- });
766
-
767
- describe('custom types', () => {
768
- test('validates using custom type reference', () => {
769
- const {tag} = parseTag('person { name=alice age=30 }');
770
- const {tag: schema} = parseTag(`
771
- Types: {
772
- personType: {
773
- Required: { name=string age=number }
774
- }
775
- }
776
- Required: { person=personType }
777
- `);
778
-
779
- const errors = validateTag(tag, schema);
780
-
781
- expect(errors).toHaveLength(0);
782
- });
783
-
784
- test('reports error when custom type validation fails', () => {
785
- const {tag} = parseTag('person { name=alice }');
786
- const {tag: schema} = parseTag(`
787
- Types: {
788
- personType: {
789
- Required: { name=string age=number }
790
- }
791
- }
792
- Required: { person=personType }
793
- `);
794
-
795
- const errors = validateTag(tag, schema);
796
-
797
- expect(errors).toHaveLength(1);
798
- expect(errors[0].code).toBe('missing-required');
799
- expect(errors[0].path).toEqual(['person', 'age']);
800
- });
801
-
802
- test('validates array of custom type', () => {
803
- const {tag} = parseTag('people=[{name=alice age=30}, {name=bob age=25}]');
804
- const {tag: schema} = parseTag(
805
- 'Types: { personType: { Required: { name=string age=number } } } Required: { people="personType[]" }'
806
- );
807
-
808
- const errors = validateTag(tag, schema);
809
-
810
- expect(errors).toHaveLength(0);
811
- });
812
-
813
- test('reports error in array of custom type with index', () => {
814
- const {tag} = parseTag('people=[{name=alice age=30}, {name=bob}]');
815
- const {tag: schema} = parseTag(
816
- 'Types: { personType: { Required: { name=string age=number } } } Required: { people="personType[]" }'
817
- );
818
-
819
- const errors = validateTag(tag, schema);
820
-
821
- expect(errors).toHaveLength(1);
822
- expect(errors[0].code).toBe('missing-required');
823
- expect(errors[0].path).toEqual(['people', '1', 'age']);
824
- });
825
-
826
- test('custom type array rejects non-array', () => {
827
- const {tag} = parseTag('person { name=alice age=30 }');
828
- const {tag: schema} = parseTag(
829
- 'Types: { personType: { Required: { name=string age=number } } } Required: { person="personType[]" }'
830
- );
831
-
832
- const errors = validateTag(tag, schema);
833
-
834
- expect(errors).toHaveLength(1);
835
- expect(errors[0].code).toBe('wrong-type');
836
- expect(errors[0].message).toContain("expected 'personType[]'");
837
- });
838
-
839
- test('recursive type validates nested structure', () => {
840
- const {tag} = parseTag(
841
- 'node { value=1 children=[{ value=2 }, { value=3 children=[{ value=4 }] }] }'
842
- );
843
- const {tag: schema} = parseTag(
844
- 'Types: { treeNode: { Required: { value=number } Optional: { children="treeNode[]" } } } Required: { node=treeNode }'
845
- );
846
-
847
- const errors = validateTag(tag, schema);
848
-
849
- expect(errors).toHaveLength(0);
850
- });
851
-
852
- test('recursive type catches deep errors', () => {
853
- const {tag} = parseTag(
854
- 'node { value=1 children=[{ value=2 }, { value=bad }] }'
855
- );
856
- const {tag: schema} = parseTag(
857
- 'Types: { treeNode: { Required: { value=number } Optional: { children="treeNode[]" } } } Required: { node=treeNode }'
858
- );
859
-
860
- const errors = validateTag(tag, schema);
861
-
862
- expect(errors).toHaveLength(1);
863
- expect(errors[0].code).toBe('wrong-type');
864
- expect(errors[0].path).toEqual(['node', 'children', '1', 'value']);
865
- });
866
-
867
- test('multiple custom types in same schema', () => {
868
- const {tag} = parseTag(`
869
- user { name=alice }
870
- address { city=boston }
871
- `);
872
- const {tag: schema} = parseTag(`
873
- Types: {
874
- userType: { Required: { name=string } }
875
- addressType: { Required: { city=string } }
876
- }
877
- Required: {
878
- user=userType
879
- address=addressType
880
- }
881
- `);
882
-
883
- const errors = validateTag(tag, schema);
884
-
885
- expect(errors).toHaveLength(0);
886
- });
887
-
888
- test('custom type with type constraint on value', () => {
889
- const {tag} = parseTag('config=settings { debug=@true }');
890
- const {tag: schema} = parseTag(`
891
- Types: {
892
- configType: {
893
- Type=string
894
- Optional: { debug=boolean }
895
- }
896
- }
897
- Required: { config=configType }
898
- `);
899
-
900
- const errors = validateTag(tag, schema);
901
-
902
- expect(errors).toHaveLength(0);
903
- });
904
-
905
- test('meta-schema validates itself', () => {
906
- // The meta-schema loaded from motly-schema.motly should validate against itself
907
- const errors = validateTag(metaSchema, metaSchema);
908
-
909
- expect(errors).toHaveLength(0);
910
- });
911
-
912
- test('meta-schema validates a simple schema', () => {
913
- const {tag: simpleSchema} = parseTag(`
914
- Required: { name=string age=number }
915
- Optional: { email=string }
916
- `);
917
-
918
- const errors = validateTag(simpleSchema, metaSchema);
919
-
920
- expect(errors).toHaveLength(0);
921
- });
922
-
923
- test('meta-schema validates schema with custom types', () => {
924
- const {tag: schema} = parseTag(`
925
- Types: {
926
- PersonType: {
927
- Required: { name=string }
928
- Optional: { age=number }
929
- }
930
- }
931
- Required: { person=PersonType }
932
- `);
933
-
934
- const errors = validateTag(schema, metaSchema);
935
-
936
- expect(errors).toHaveLength(0);
937
- });
938
- });
939
-
940
- describe('enum types', () => {
941
- test('validates string enum', () => {
942
- const {tag} = parseTag('status=active');
943
- const {tag: schema} = parseTag(`
944
- Types: { statusType = [pending, active, completed] }
945
- Required: { status=statusType }
946
- `);
947
-
948
- const errors = validateTag(tag, schema);
949
-
950
- expect(errors).toHaveLength(0);
951
- });
952
-
953
- test('rejects invalid enum value', () => {
954
- const {tag} = parseTag('status=unknown');
955
- const {tag: schema} = parseTag(`
956
- Types: { statusType = [pending, active, completed] }
957
- Required: { status=statusType }
958
- `);
959
-
960
- const errors = validateTag(tag, schema);
961
-
962
- expect(errors).toHaveLength(1);
963
- expect(errors[0].code).toBe('invalid-enum-value');
964
- expect(errors[0].message).toContain('unknown');
965
- expect(errors[0].message).toContain('pending, active, completed');
966
- });
967
-
968
- test('validates numeric enum', () => {
969
- const {tag} = parseTag('level=2');
970
- const {tag: schema} = parseTag(`
971
- Types: { levelType = [1, 2, 3] }
972
- Required: { level=levelType }
973
- `);
974
-
975
- const errors = validateTag(tag, schema);
976
-
977
- expect(errors).toHaveLength(0);
978
- });
979
-
980
- test('rejects invalid numeric enum value', () => {
981
- const {tag} = parseTag('level=5');
982
- const {tag: schema} = parseTag(`
983
- Types: { levelType = [1, 2, 3] }
984
- Required: { level=levelType }
985
- `);
986
-
987
- const errors = validateTag(tag, schema);
988
-
989
- expect(errors).toHaveLength(1);
990
- expect(errors[0].code).toBe('invalid-enum-value');
991
- });
992
-
993
- test('validates array of enum type', () => {
994
- const {tag} = parseTag('statuses=[active, pending]');
995
- const {tag: schema} = parseTag(`
996
- Types: { statusType = [pending, active, completed] }
997
- Required: { statuses="statusType[]" }
998
- `);
999
-
1000
- const errors = validateTag(tag, schema);
1001
-
1002
- expect(errors).toHaveLength(0);
1003
- });
1004
-
1005
- test('rejects invalid value in enum array', () => {
1006
- const {tag} = parseTag('statuses=[active, invalid]');
1007
- const {tag: schema} = parseTag(`
1008
- Types: { statusType = [pending, active, completed] }
1009
- Required: { statuses="statusType[]" }
1010
- `);
1011
-
1012
- const errors = validateTag(tag, schema);
1013
-
1014
- expect(errors).toHaveLength(1);
1015
- expect(errors[0].code).toBe('invalid-enum-value');
1016
- expect(errors[0].path).toEqual(['statuses', '1']);
1017
- });
1018
-
1019
- test('rejects empty enum type', () => {
1020
- const {tag} = parseTag('status=active');
1021
- const {tag: schema} = parseTag(`
1022
- Types: { statusType = [] }
1023
- Required: { status=statusType }
1024
- `);
1025
-
1026
- const errors = validateTag(tag, schema);
1027
-
1028
- expect(errors).toHaveLength(1);
1029
- expect(errors[0].code).toBe('invalid-schema');
1030
- expect(errors[0].message).toContain('no values');
1031
- });
1032
-
1033
- test('rejects mixed type enum', () => {
1034
- const {tag} = parseTag('value=1');
1035
- const {tag: schema} = parseTag(`
1036
- Types: { mixedType = [1, two, 3] }
1037
- Required: { value=mixedType }
1038
- `);
1039
-
1040
- const errors = validateTag(tag, schema);
1041
-
1042
- expect(errors).toHaveLength(1);
1043
- expect(errors[0].code).toBe('invalid-schema');
1044
- expect(errors[0].message).toContain('mixed types');
1045
- });
1046
- });
1047
-
1048
- describe('pattern types', () => {
1049
- test('validates string matching pattern', () => {
1050
- const {tag} = parseTag('email="test@example.com"');
1051
- const {tag: schema} = parseTag(`
1052
- Types: { emailType.matches = "^[^@]+@[^@]+$" }
1053
- Required: { email=emailType }
1054
- `);
1055
-
1056
- const errors = validateTag(tag, schema);
1057
-
1058
- expect(errors).toHaveLength(0);
1059
- });
1060
-
1061
- test('rejects string not matching pattern', () => {
1062
- const {tag} = parseTag('email="not-an-email"');
1063
- const {tag: schema} = parseTag(`
1064
- Types: { emailType.matches = "^[^@]+@[^@]+$" }
1065
- Required: { email=emailType }
1066
- `);
1067
-
1068
- const errors = validateTag(tag, schema);
1069
-
1070
- expect(errors).toHaveLength(1);
1071
- expect(errors[0].code).toBe('pattern-mismatch');
1072
- });
1073
-
1074
- test('rejects non-string for pattern type', () => {
1075
- const {tag} = parseTag('email=123');
1076
- const {tag: schema} = parseTag(`
1077
- Types: { emailType.matches = ".*" }
1078
- Required: { email=emailType }
1079
- `);
1080
-
1081
- const errors = validateTag(tag, schema);
1082
-
1083
- expect(errors).toHaveLength(1);
1084
- expect(errors[0].code).toBe('wrong-type');
1085
- });
1086
-
1087
- test('validates array of pattern type', () => {
1088
- const {tag} = parseTag('emails=["a@b.com", "c@d.org"]');
1089
- const {tag: schema} = parseTag(`
1090
- Types: { emailType.matches = "^[^@]+@[^@]+$" }
1091
- Required: { emails="emailType[]" }
1092
- `);
1093
-
1094
- const errors = validateTag(tag, schema);
1095
-
1096
- expect(errors).toHaveLength(0);
1097
- });
1098
-
1099
- test('rejects invalid value in pattern array', () => {
1100
- const {tag} = parseTag('emails=["a@b.com", "invalid"]');
1101
- const {tag: schema} = parseTag(`
1102
- Types: { emailType.matches = "^[^@]+@[^@]+$" }
1103
- Required: { emails="emailType[]" }
1104
- `);
1105
-
1106
- const errors = validateTag(tag, schema);
1107
-
1108
- expect(errors).toHaveLength(1);
1109
- expect(errors[0].code).toBe('pattern-mismatch');
1110
- expect(errors[0].path).toEqual(['emails', '1']);
1111
- });
1112
-
1113
- test('reports error for invalid regex in schema', () => {
1114
- const {tag} = parseTag('value=test');
1115
- const {tag: schema} = parseTag(`
1116
- Types: { badPattern.matches = "[invalid" }
1117
- Required: { value=badPattern }
1118
- `);
1119
-
1120
- const errors = validateTag(tag, schema);
1121
-
1122
- expect(errors).toHaveLength(1);
1123
- expect(errors[0].code).toBe('invalid-schema');
1124
- });
1125
- });
1126
-
1127
- describe('oneOf union types', () => {
1128
- test('validates value matching first type in oneOf', () => {
1129
- const {tag} = parseTag('value=hello');
1130
- const {tag: schema} = parseTag(`
1131
- Types: { StringOrNumber.oneOf = [string, number] }
1132
- Required: { value=StringOrNumber }
1133
- `);
1134
-
1135
- const errors = validateTag(tag, schema);
1136
-
1137
- expect(errors).toHaveLength(0);
1138
- });
1139
-
1140
- test('validates value matching second type in oneOf', () => {
1141
- const {tag} = parseTag('value=42');
1142
- const {tag: schema} = parseTag(`
1143
- Types: { StringOrNumber.oneOf = [string, number] }
1144
- Required: { value=StringOrNumber }
1145
- `);
1146
-
1147
- const errors = validateTag(tag, schema);
1148
-
1149
- expect(errors).toHaveLength(0);
1150
- });
1151
-
1152
- test('rejects value not matching any type in oneOf', () => {
1153
- const {tag} = parseTag('value=@true');
1154
- const {tag: schema} = parseTag(`
1155
- Types: { StringOrNumber.oneOf = [string, number] }
1156
- Required: { value=StringOrNumber }
1157
- `);
1158
-
1159
- const errors = validateTag(tag, schema);
1160
-
1161
- expect(errors).toHaveLength(1);
1162
- expect(errors[0].code).toBe('wrong-type');
1163
- expect(errors[0].message).toContain('oneOf');
1164
- });
1165
-
1166
- test('oneOf with custom structural types', () => {
1167
- const {tag} = parseTag('item { name=test }');
1168
- const {tag: schema} = parseTag(`
1169
- Types: {
1170
- TypeA: { Required: { name=string } }
1171
- TypeB: { Required: { count=number } }
1172
- AorB.oneOf = [TypeA, TypeB]
1173
- }
1174
- Required: { item=AorB }
1175
- `);
1176
-
1177
- const errors = validateTag(tag, schema);
1178
-
1179
- expect(errors).toHaveLength(0);
1180
- });
1181
-
1182
- test('oneOf with enum type', () => {
1183
- const {tag} = parseTag('value=active');
1184
- const {tag: schema} = parseTag(`
1185
- Types: {
1186
- StatusEnum = [pending, active, completed]
1187
- StringOrStatus.oneOf = [number, StatusEnum]
1188
- }
1189
- Required: { value=StringOrStatus }
1190
- `);
1191
-
1192
- const errors = validateTag(tag, schema);
1193
-
1194
- expect(errors).toHaveLength(0);
1195
- });
1196
-
1197
- test('array of oneOf type', () => {
1198
- const {tag} = parseTag('values=[hello, 42, world]');
1199
- const {tag: schema} = parseTag(`
1200
- Types: { StringOrNumber.oneOf = [string, number] }
1201
- Required: { values="StringOrNumber[]" }
1202
- `);
1203
-
1204
- const errors = validateTag(tag, schema);
1205
-
1206
- expect(errors).toHaveLength(0);
1207
- });
1208
- });
1209
-
1210
- describe('typed Additional', () => {
1211
- test('Additional with type validates unknown properties', () => {
1212
- const {tag} = parseTag('name=alice extra1=hello extra2=world');
1213
- const {tag: schema} = parseTag(`
1214
- Required: { name=string }
1215
- Additional=string
1216
- `);
1217
-
1218
- const errors = validateTag(tag, schema);
1219
-
1220
- expect(errors).toHaveLength(0);
1221
- });
1222
-
1223
- test('Additional with type rejects invalid unknown properties', () => {
1224
- const {tag} = parseTag('name=alice extra=42');
1225
- const {tag: schema} = parseTag(`
1226
- Required: { name=string }
1227
- Additional=string
1228
- `);
1229
-
1230
- const errors = validateTag(tag, schema);
1231
-
1232
- expect(errors).toHaveLength(1);
1233
- expect(errors[0].code).toBe('wrong-type');
1234
- expect(errors[0].path).toEqual(['extra']);
1235
- });
1236
-
1237
- test('Additional with custom type', () => {
1238
- const {tag} = parseTag('name=test prop1 { x=1 } prop2 { x=2 }');
1239
- const {tag: schema} = parseTag(`
1240
- Types: {
1241
- PropType: { Required: { x=number } }
1242
- }
1243
- Required: { name=string }
1244
- Additional=PropType
1245
- `);
1246
-
1247
- const errors = validateTag(tag, schema);
1248
-
1249
- expect(errors).toHaveLength(0);
1250
- });
1251
-
1252
- test('Additional with custom type catches errors', () => {
1253
- const {tag} = parseTag('name=test prop1 { x=bad }');
1254
- const {tag: schema} = parseTag(`
1255
- Types: {
1256
- PropType: { Required: { x=number } }
1257
- }
1258
- Required: { name=string }
1259
- Additional=PropType
1260
- `);
1261
-
1262
- const errors = validateTag(tag, schema);
1263
-
1264
- expect(errors).toHaveLength(1);
1265
- expect(errors[0].path).toEqual(['prop1', 'x']);
1266
- });
1267
-
1268
- test('Additional flag is same as Additional=any', () => {
1269
- const {tag} = parseTag('name=alice anything=goes here=too');
1270
- const {tag: schema} = parseTag(`
1271
- Required: { name=string }
1272
- Additional
1273
- `);
1274
-
1275
- const errors = validateTag(tag, schema);
1276
-
1277
- expect(errors).toHaveLength(0);
1278
- });
1279
- });
1280
- });