@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
package/src/schema.ts DELETED
@@ -1,852 +0,0 @@
1
- /*
2
- * Copyright Contributors to the Malloy project
3
- * SPDX-License-Identifier: MIT
4
- */
5
-
6
- import type {Tag} from './tags';
7
-
8
- export interface SchemaError {
9
- message: string;
10
- path: string[];
11
- code:
12
- | 'missing-required'
13
- | 'wrong-type'
14
- | 'unknown-property'
15
- | 'invalid-schema'
16
- | 'invalid-enum-value'
17
- | 'pattern-mismatch';
18
- }
19
-
20
- type SchemaType =
21
- | 'string'
22
- | 'number'
23
- | 'boolean'
24
- | 'date'
25
- | 'tag'
26
- | 'flag'
27
- | 'any'
28
- | 'string[]'
29
- | 'number[]'
30
- | 'boolean[]'
31
- | 'date[]'
32
- | 'tag[]'
33
- | 'any[]';
34
-
35
- const VALID_TYPES: SchemaType[] = [
36
- 'string',
37
- 'number',
38
- 'boolean',
39
- 'date',
40
- 'tag',
41
- 'flag',
42
- 'any',
43
- 'string[]',
44
- 'number[]',
45
- 'boolean[]',
46
- 'date[]',
47
- 'tag[]',
48
- 'any[]',
49
- ];
50
-
51
- function isValidSchemaType(value: string): value is SchemaType {
52
- return VALID_TYPES.includes(value as SchemaType);
53
- }
54
-
55
- function isArrayType(type: SchemaType): boolean {
56
- return type.endsWith('[]');
57
- }
58
-
59
- function getArrayElementType(
60
- type: SchemaType
61
- ): 'string' | 'number' | 'boolean' | 'date' | 'tag' | 'any' {
62
- return type.slice(0, -2) as
63
- | 'string'
64
- | 'number'
65
- | 'boolean'
66
- | 'date'
67
- | 'tag'
68
- | 'any';
69
- }
70
-
71
- interface TypeResult {
72
- type?: SchemaType;
73
- typeRef?: string;
74
- typeRefArray?: boolean;
75
- invalidType?: string;
76
- }
77
-
78
- type TypesMap = Record<string, Tag>;
79
-
80
- function parseTypeSpecifier(value: string, customTypes: TypesMap): TypeResult {
81
- // Check built-in types first (including built-in array types)
82
- if (isValidSchemaType(value)) {
83
- return {type: value};
84
- }
85
-
86
- // Check for array suffix for custom types
87
- const isArray = value.endsWith('[]');
88
- const baseName = isArray ? value.slice(0, -2) : value;
89
-
90
- // Check if it's a custom type reference
91
- if (baseName in customTypes) {
92
- return {typeRef: baseName, typeRefArray: isArray};
93
- }
94
-
95
- // Unknown type
96
- return {invalidType: value};
97
- }
98
-
99
- function getExpectedType(schemaProp: Tag, customTypes: TypesMap): TypeResult {
100
- // Check for shorthand: prop=string, prop=number, prop=customType, etc.
101
- const eqValue = schemaProp.text();
102
- if (eqValue !== undefined) {
103
- return parseTypeSpecifier(eqValue, customTypes);
104
- }
105
-
106
- // Check for full form: prop: { type=string }
107
- const typeValue = schemaProp.text('Type');
108
- if (typeValue !== undefined) {
109
- return parseTypeSpecifier(typeValue, customTypes);
110
- }
111
-
112
- return {};
113
- }
114
-
115
- function getActualType(tag: Tag): string {
116
- const eq = tag.eq;
117
-
118
- if (eq === undefined) {
119
- // No value - check if it has properties
120
- if (!tag.hasProperties()) {
121
- // No value and no properties - it's a flag (presence-only)
122
- return 'flag';
123
- }
124
- // Has properties but no value - it's a "tag" type
125
- return 'tag';
126
- }
127
-
128
- if (Array.isArray(eq)) {
129
- // Check element types in array
130
- if (eq.length === 0) {
131
- return 'any[]'; // Empty array, could be any type
132
- }
133
-
134
- const elementTypes = eq.map(el => {
135
- if (el.eq === undefined) {
136
- return 'tag';
137
- }
138
- if (typeof el.eq === 'string') return 'string';
139
- if (typeof el.eq === 'number') return 'number';
140
- if (typeof el.eq === 'boolean') return 'boolean';
141
- if (el.eq instanceof Date) return 'date';
142
- return 'unknown';
143
- });
144
-
145
- // Check if all elements are the same type
146
- const firstType = elementTypes[0];
147
- const allSame = elementTypes.every(t => t === firstType);
148
-
149
- if (allSame && firstType !== 'unknown') {
150
- return `${firstType}[]`;
151
- }
152
-
153
- return 'mixed[]';
154
- }
155
-
156
- if (typeof eq === 'string') return 'string';
157
- if (typeof eq === 'number') return 'number';
158
- if (typeof eq === 'boolean') return 'boolean';
159
- if (eq instanceof Date) return 'date';
160
-
161
- return 'unknown';
162
- }
163
-
164
- function typeMatches(actualType: string, expectedType: SchemaType): boolean {
165
- if (expectedType === 'any') {
166
- return true;
167
- }
168
-
169
- if (expectedType === 'tag') {
170
- return actualType === 'tag';
171
- }
172
-
173
- if (isArrayType(expectedType)) {
174
- const elementType = getArrayElementType(expectedType);
175
-
176
- // Check if actual is an array type
177
- if (!actualType.endsWith('[]')) {
178
- return false;
179
- }
180
-
181
- if (elementType === 'any') {
182
- return true; // any[] matches any array
183
- }
184
-
185
- // Extract actual element type
186
- const actualElementType = actualType.slice(0, -2);
187
-
188
- // Empty arrays (any[]) match any array type
189
- if (actualElementType === 'any') {
190
- return true;
191
- }
192
-
193
- return actualElementType === elementType;
194
- }
195
-
196
- return actualType === expectedType;
197
- }
198
-
199
- interface AdditionalConfig {
200
- allow: boolean;
201
- typeRef?: string;
202
- }
203
-
204
- function getAdditionalConfig(
205
- schema: Tag,
206
- customTypes: TypesMap
207
- ): AdditionalConfig {
208
- const additional = schema.tag('Additional');
209
- if (additional === undefined) {
210
- return {allow: false};
211
- }
212
-
213
- // Additional present - check if it has a type value
214
- const typeValue = additional.text();
215
- if (typeValue === undefined) {
216
- // Flag form: Additional (no value) = allow any
217
- return {allow: true};
218
- }
219
-
220
- // Check if it's "any" explicitly
221
- if (typeValue === 'any') {
222
- return {allow: true};
223
- }
224
-
225
- // It's a type reference
226
- if (typeValue in customTypes || isValidSchemaType(typeValue)) {
227
- return {allow: true, typeRef: typeValue};
228
- }
229
-
230
- // Unknown type - treat as allow (will error on validation if type is bad)
231
- return {allow: true, typeRef: typeValue};
232
- }
233
-
234
- function validateProperties(
235
- tag: Tag,
236
- schema: Tag,
237
- path: string[],
238
- errors: SchemaError[],
239
- additionalConfig: AdditionalConfig,
240
- customTypes: TypesMap
241
- ): void {
242
- const requiredSection = schema.tag('Required');
243
- const optionalSection = schema.tag('Optional');
244
- const knownProps = new Set<string>();
245
-
246
- // Check required properties
247
- if (requiredSection) {
248
- for (const [propName, schemaProp] of requiredSection.entries()) {
249
- knownProps.add(propName);
250
- const propTag = tag.tag(propName);
251
- if (propTag === undefined) {
252
- errors.push({
253
- message: `Missing required property '${propName}'`,
254
- path: [...path, propName],
255
- code: 'missing-required',
256
- });
257
- continue;
258
- }
259
- validateProperty(
260
- propTag,
261
- schemaProp,
262
- propName,
263
- path,
264
- errors,
265
- additionalConfig,
266
- customTypes
267
- );
268
- }
269
- }
270
-
271
- // Check optional properties that exist
272
- if (optionalSection) {
273
- for (const [propName, schemaProp] of optionalSection.entries()) {
274
- knownProps.add(propName);
275
- const propTag = tag.tag(propName);
276
- if (propTag === undefined) {
277
- continue; // Optional, so OK if missing
278
- }
279
- validateProperty(
280
- propTag,
281
- schemaProp,
282
- propName,
283
- path,
284
- errors,
285
- additionalConfig,
286
- customTypes
287
- );
288
- }
289
- }
290
-
291
- // Check for unknown properties (only if schema defines any)
292
- if (knownProps.size > 0) {
293
- for (const [propName, propTag] of tag.entries()) {
294
- if (!knownProps.has(propName)) {
295
- if (!additionalConfig.allow) {
296
- errors.push({
297
- message: `Unknown property '${propName}'`,
298
- path: [...path, propName],
299
- code: 'unknown-property',
300
- });
301
- } else if (additionalConfig.typeRef !== undefined) {
302
- // Validate unknown property against the Additional type
303
- validatePropertyAgainstType(
304
- propTag,
305
- additionalConfig.typeRef,
306
- propName,
307
- path,
308
- errors,
309
- additionalConfig,
310
- customTypes
311
- );
312
- }
313
- // else: allow any, no validation needed
314
- }
315
- }
316
- }
317
- }
318
-
319
- interface EnumInfo {
320
- kind: 'string' | 'number';
321
- values: string[] | number[];
322
- }
323
-
324
- function getEnumInfo(refSchema: Tag, typeName: string): EnumInfo | SchemaError {
325
- const array = refSchema.array();
326
- if (!array || array.length === 0) {
327
- return {
328
- message: `Enum type '${typeName}' has no values`,
329
- path: [],
330
- code: 'invalid-schema',
331
- };
332
- }
333
-
334
- // Check what types are in the array
335
- let hasStrings = false;
336
- let hasNumbers = false;
337
- const stringValues: string[] = [];
338
- const numberValues: number[] = [];
339
-
340
- for (const el of array) {
341
- const val = el.eq;
342
- if (typeof val === 'string') {
343
- hasStrings = true;
344
- stringValues.push(val);
345
- } else if (typeof val === 'number') {
346
- hasNumbers = true;
347
- numberValues.push(val);
348
- }
349
- }
350
-
351
- // Check for mixed types
352
- if (hasStrings && hasNumbers) {
353
- return {
354
- message: `Enum type '${typeName}' has mixed types (must be all strings or all numbers)`,
355
- path: [],
356
- code: 'invalid-schema',
357
- };
358
- }
359
-
360
- if (hasStrings) {
361
- return {kind: 'string', values: stringValues};
362
- }
363
- if (hasNumbers) {
364
- return {kind: 'number', values: numberValues};
365
- }
366
-
367
- return {
368
- message: `Enum type '${typeName}' has no valid values (must be strings or numbers)`,
369
- path: [],
370
- code: 'invalid-schema',
371
- };
372
- }
373
-
374
- function isSchemaError(x: EnumInfo | SchemaError): x is SchemaError {
375
- return 'code' in x;
376
- }
377
-
378
- function validateEnumValue(
379
- tag: Tag,
380
- enumInfo: EnumInfo,
381
- typeName: string,
382
- path: string[],
383
- errors: SchemaError[]
384
- ): void {
385
- const actualValue = tag.eq;
386
-
387
- if (enumInfo.kind === 'string') {
388
- if (
389
- typeof actualValue === 'string' &&
390
- (enumInfo.values as string[]).includes(actualValue)
391
- ) {
392
- return;
393
- }
394
- } else {
395
- if (
396
- typeof actualValue === 'number' &&
397
- (enumInfo.values as number[]).includes(actualValue)
398
- ) {
399
- return;
400
- }
401
- }
402
-
403
- const allowedStr = enumInfo.values.map(v => String(v)).join(', ');
404
- errors.push({
405
- message: `Value '${actualValue}' is not a valid ${typeName}. Allowed values: [${allowedStr}]`,
406
- path,
407
- code: 'invalid-enum-value',
408
- });
409
- }
410
-
411
- function validatePattern(
412
- tag: Tag,
413
- pattern: string,
414
- typeName: string,
415
- path: string[],
416
- errors: SchemaError[]
417
- ): void {
418
- const actualValue = tag.eq;
419
-
420
- // Pattern only applies to strings
421
- if (typeof actualValue !== 'string') {
422
- errors.push({
423
- message: `Value must be a string to match pattern for type '${typeName}', got ${typeof actualValue}`,
424
- path,
425
- code: 'wrong-type',
426
- });
427
- return;
428
- }
429
-
430
- try {
431
- const regex = new RegExp(pattern);
432
- if (!regex.test(actualValue)) {
433
- errors.push({
434
- message: `Value '${actualValue}' does not match pattern for type '${typeName}'`,
435
- path,
436
- code: 'pattern-mismatch',
437
- });
438
- }
439
- } catch {
440
- errors.push({
441
- message: `Invalid regex pattern '${pattern}' in type '${typeName}'`,
442
- path,
443
- code: 'invalid-schema',
444
- });
445
- }
446
- }
447
-
448
- function validateEachElement(
449
- propTag: Tag,
450
- isArray: boolean | undefined,
451
- typeName: string,
452
- path: string[],
453
- errors: SchemaError[],
454
- validate: (el: {tag: Tag; path: string[]}) => void
455
- ): void {
456
- if (isArray === true) {
457
- const array = propTag.array();
458
- if (!array) {
459
- const actualType = getActualType(propTag);
460
- errors.push({
461
- message: `Expected '${typeName}[]', got '${actualType}'`,
462
- path,
463
- code: 'wrong-type',
464
- });
465
- return;
466
- }
467
- for (let i = 0; i < array.length; i++) {
468
- validate({tag: array[i], path: [...path, String(i)]});
469
- }
470
- } else {
471
- validate({tag: propTag, path});
472
- }
473
- }
474
-
475
- function validateAgainstOneOf(
476
- propTag: Tag,
477
- oneOfTypes: string[],
478
- propName: string,
479
- path: string[],
480
- errors: SchemaError[],
481
- additionalConfig: AdditionalConfig,
482
- customTypes: TypesMap
483
- ): boolean {
484
- // Try each type in the oneOf list
485
- for (const typeName of oneOfTypes) {
486
- const testErrors: SchemaError[] = [];
487
-
488
- if (isValidSchemaType(typeName)) {
489
- // Built-in type
490
- const actualType = getActualType(propTag);
491
- if (typeMatches(actualType, typeName)) {
492
- return true; // Match found
493
- }
494
- } else if (typeName in customTypes) {
495
- // Custom type reference
496
- const refSchema = customTypes[typeName];
497
- const refAdditionalConfig = getAdditionalConfig(refSchema, customTypes);
498
-
499
- // Check if it's an enum type
500
- if (Array.isArray(refSchema.eq)) {
501
- const enumInfo = getEnumInfo(refSchema, typeName);
502
- if (!isSchemaError(enumInfo)) {
503
- validateEnumValue(propTag, enumInfo, typeName, path, testErrors);
504
- if (testErrors.length === 0) {
505
- return true; // Match found
506
- }
507
- }
508
- continue;
509
- }
510
-
511
- // Check if it's a pattern type
512
- const pattern = refSchema.text('matches');
513
- if (pattern !== undefined) {
514
- validatePattern(propTag, pattern, typeName, path, testErrors);
515
- if (testErrors.length === 0) {
516
- return true; // Match found
517
- }
518
- continue;
519
- }
520
-
521
- // Check if it's a oneOf type (nested)
522
- const nestedOneOf = refSchema.textArray('oneOf');
523
- if (nestedOneOf !== undefined && nestedOneOf.length > 0) {
524
- if (
525
- validateAgainstOneOf(
526
- propTag,
527
- nestedOneOf,
528
- propName,
529
- path,
530
- testErrors,
531
- additionalConfig,
532
- customTypes
533
- )
534
- ) {
535
- return true; // Match found
536
- }
537
- continue;
538
- }
539
-
540
- // Structural type - validate properties
541
- validateProperties(
542
- propTag,
543
- refSchema,
544
- path,
545
- testErrors,
546
- refAdditionalConfig,
547
- customTypes
548
- );
549
- if (testErrors.length === 0) {
550
- return true; // Match found
551
- }
552
- }
553
- }
554
-
555
- // No match found - report error
556
- errors.push({
557
- message: `Property '${propName}' does not match any type in oneOf: [${oneOfTypes.join(', ')}]`,
558
- path,
559
- code: 'wrong-type',
560
- });
561
- return false;
562
- }
563
-
564
- function validatePropertyAgainstType(
565
- propTag: Tag,
566
- typeName: string,
567
- propName: string,
568
- parentPath: string[],
569
- errors: SchemaError[],
570
- additionalConfig: AdditionalConfig,
571
- customTypes: TypesMap
572
- ): void {
573
- const path = [...parentPath, propName];
574
-
575
- // Check if it's a built-in type
576
- if (isValidSchemaType(typeName)) {
577
- const actualType = getActualType(propTag);
578
- if (!typeMatches(actualType, typeName)) {
579
- errors.push({
580
- message: `Property '${propName}' has wrong type: expected '${typeName}', got '${actualType}'`,
581
- path,
582
- code: 'wrong-type',
583
- });
584
- }
585
- return;
586
- }
587
-
588
- // Check if it's a custom type
589
- if (!(typeName in customTypes)) {
590
- errors.push({
591
- message: `Invalid type '${typeName}' in schema for '${propName}'`,
592
- path,
593
- code: 'invalid-schema',
594
- });
595
- return;
596
- }
597
-
598
- const refSchema = customTypes[typeName];
599
- const refAdditionalConfig = getAdditionalConfig(refSchema, customTypes);
600
-
601
- // Check if this is an enum type
602
- if (Array.isArray(refSchema.eq)) {
603
- const enumInfo = getEnumInfo(refSchema, typeName);
604
- if (isSchemaError(enumInfo)) {
605
- errors.push({...enumInfo, path});
606
- return;
607
- }
608
- validateEnumValue(propTag, enumInfo, typeName, path, errors);
609
- return;
610
- }
611
-
612
- // Check if this is a pattern type
613
- const pattern = refSchema.text('matches');
614
- if (pattern !== undefined) {
615
- validatePattern(propTag, pattern, typeName, path, errors);
616
- return;
617
- }
618
-
619
- // Check if this is a oneOf type
620
- const oneOfTypes = refSchema.textArray('oneOf');
621
- if (oneOfTypes !== undefined && oneOfTypes.length > 0) {
622
- validateAgainstOneOf(
623
- propTag,
624
- oneOfTypes,
625
- propName,
626
- path,
627
- errors,
628
- additionalConfig,
629
- customTypes
630
- );
631
- return;
632
- }
633
-
634
- // Structural type - validate properties
635
- validateProperties(
636
- propTag,
637
- refSchema,
638
- path,
639
- errors,
640
- refAdditionalConfig,
641
- customTypes
642
- );
643
- }
644
-
645
- function validateProperty(
646
- propTag: Tag,
647
- schemaProp: Tag,
648
- propName: string,
649
- parentPath: string[],
650
- errors: SchemaError[],
651
- additionalConfig: AdditionalConfig,
652
- customTypes: TypesMap
653
- ): void {
654
- const path = [...parentPath, propName];
655
- const {
656
- type: expectedType,
657
- typeRef,
658
- typeRefArray,
659
- invalidType,
660
- } = getExpectedType(schemaProp, customTypes);
661
-
662
- if (invalidType !== undefined) {
663
- errors.push({
664
- message: `Invalid type '${invalidType}' in schema for '${propName}'`,
665
- path,
666
- code: 'invalid-schema',
667
- });
668
- return; // Don't continue validation with invalid schema
669
- }
670
-
671
- // Handle custom type reference
672
- if (typeRef !== undefined) {
673
- const refSchema = customTypes[typeRef];
674
- const refAdditionalConfig = getAdditionalConfig(refSchema, customTypes);
675
-
676
- // Check if this is an enum type (custom type value is an array)
677
- if (Array.isArray(refSchema.eq)) {
678
- const enumInfo = getEnumInfo(refSchema, typeRef);
679
- if (isSchemaError(enumInfo)) {
680
- errors.push({...enumInfo, path});
681
- return;
682
- }
683
- validateEachElement(propTag, typeRefArray, typeRef, path, errors, el =>
684
- validateEnumValue(el.tag, enumInfo, typeRef, el.path, errors)
685
- );
686
- return;
687
- }
688
-
689
- // Check if this is a pattern type (custom type has 'matches' property)
690
- const pattern = refSchema.text('matches');
691
- if (pattern !== undefined) {
692
- validateEachElement(propTag, typeRefArray, typeRef, path, errors, el =>
693
- validatePattern(el.tag, pattern, typeRef, el.path, errors)
694
- );
695
- return;
696
- }
697
-
698
- // Check if this is a oneOf type
699
- const oneOfTypes = refSchema.textArray('oneOf');
700
- if (oneOfTypes !== undefined && oneOfTypes.length > 0) {
701
- validateEachElement(propTag, typeRefArray, typeRef, path, errors, el =>
702
- validateAgainstOneOf(
703
- el.tag,
704
- oneOfTypes,
705
- propName,
706
- el.path,
707
- errors,
708
- additionalConfig,
709
- customTypes
710
- )
711
- );
712
- return;
713
- }
714
-
715
- // Regular custom type - validate properties
716
- if (typeRefArray) {
717
- // Validate as array of custom type
718
- const actualType = getActualType(propTag);
719
- if (!actualType.endsWith('[]')) {
720
- errors.push({
721
- message: `Property '${propName}' has wrong type: expected '${typeRef}[]', got '${actualType}'`,
722
- path,
723
- code: 'wrong-type',
724
- });
725
- return;
726
- }
727
-
728
- const array = propTag.array();
729
- if (array) {
730
- for (let i = 0; i < array.length; i++) {
731
- validateProperties(
732
- array[i],
733
- refSchema,
734
- [...path, String(i)],
735
- errors,
736
- refAdditionalConfig,
737
- customTypes
738
- );
739
- }
740
- }
741
- } else {
742
- // Validate against referenced type schema
743
- validateProperties(
744
- propTag,
745
- refSchema,
746
- path,
747
- errors,
748
- refAdditionalConfig,
749
- customTypes
750
- );
751
- }
752
- return;
753
- }
754
-
755
- if (expectedType !== undefined) {
756
- const actualType = getActualType(propTag);
757
-
758
- if (!typeMatches(actualType, expectedType)) {
759
- errors.push({
760
- message: `Property '${propName}' has wrong type: expected '${expectedType}', got '${actualType}'`,
761
- path,
762
- code: 'wrong-type',
763
- });
764
- return; // Don't validate nested if type is wrong
765
- }
766
- }
767
-
768
- // Check for nested Required/Optional sections in schema
769
- const nestedRequired = schemaProp.tag('Required');
770
- const nestedOptional = schemaProp.tag('Optional');
771
-
772
- if (nestedRequired !== undefined || nestedOptional !== undefined) {
773
- const nestedAdditionalConfig = getAdditionalConfig(schemaProp, customTypes);
774
- // If this is an array type, validate each element against the nested schema
775
- if (expectedType !== undefined && isArrayType(expectedType)) {
776
- const array = propTag.array();
777
- if (array) {
778
- for (let i = 0; i < array.length; i++) {
779
- validateProperties(
780
- array[i],
781
- schemaProp,
782
- [...path, String(i)],
783
- errors,
784
- nestedAdditionalConfig,
785
- customTypes
786
- );
787
- }
788
- }
789
- } else {
790
- validateProperties(
791
- propTag,
792
- schemaProp,
793
- path,
794
- errors,
795
- nestedAdditionalConfig,
796
- customTypes
797
- );
798
- }
799
- }
800
- }
801
-
802
- /**
803
- * Validate a tag against a schema.
804
- *
805
- * The schema is itself a Tag that defines Required and Optional properties:
806
- *
807
- * ```motly
808
- * Types: {
809
- * ItemType: {
810
- * Required: { name=string price=number }
811
- * }
812
- * }
813
- * Required: {
814
- * color=string
815
- * items="ItemType[]"
816
- * }
817
- * Optional: {
818
- * border=number
819
- * }
820
- * ```
821
- *
822
- * Type specifiers: string, number, boolean, date, tag, any
823
- * Array types: string[], number[], boolean[], date[], tag[], any[]
824
- * Custom types: defined in `Types` section, referenced by name or name[]
825
- * Union types: TypeName.oneOf = [type1, type2, ...]
826
- *
827
- * Additional properties:
828
- * - No `Additional`: reject unknown properties
829
- * - `Additional`: allow any additional properties (same as `Additional = any`)
830
- * - `Additional = TypeName`: validate additional properties against type
831
- *
832
- * @param tag The tag to validate
833
- * @param schema The schema to validate against (as a Tag)
834
- * @returns Array of schema errors, empty if valid
835
- */
836
- export function validateTag(tag: Tag, schema: Tag): SchemaError[] {
837
- const errors: SchemaError[] = [];
838
-
839
- // Extract custom types from schema
840
- const typesSection = schema.tag('Types');
841
- const customTypes: TypesMap = {};
842
- if (typesSection) {
843
- for (const [name, typeDef] of typesSection.entries()) {
844
- customTypes[name] = typeDef;
845
- }
846
- }
847
-
848
- const additionalConfig = getAdditionalConfig(schema, customTypes);
849
-
850
- validateProperties(tag, schema, [], errors, additionalConfig, customTypes);
851
- return errors;
852
- }