@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.
- package/dist/index.d.ts +1 -3
- package/dist/index.js +4 -5
- package/dist/index.js.map +1 -1
- package/dist/{peggy/index.d.ts → parser.d.ts} +13 -4
- package/dist/parser.js +181 -0
- package/dist/parser.js.map +1 -0
- package/package.json +13 -6
- package/src/index.ts +1 -3
- package/src/parser.ts +203 -0
- package/CONTEXT.md +0 -173
- package/README.md +0 -0
- package/dist/peggy/dist/peg-tag-parser.d.ts +0 -11
- package/dist/peggy/dist/peg-tag-parser.js +0 -3130
- package/dist/peggy/dist/peg-tag-parser.js.map +0 -1
- package/dist/peggy/index.js +0 -117
- package/dist/peggy/index.js.map +0 -1
- package/dist/peggy/interpreter.d.ts +0 -32
- package/dist/peggy/interpreter.js +0 -208
- package/dist/peggy/interpreter.js.map +0 -1
- package/dist/peggy/statements.d.ts +0 -51
- package/dist/peggy/statements.js +0 -7
- package/dist/peggy/statements.js.map +0 -1
- package/dist/schema.d.ts +0 -41
- package/dist/schema.js +0 -573
- package/dist/schema.js.map +0 -1
- package/dist/schema.spec.d.ts +0 -1
- package/dist/schema.spec.js +0 -980
- package/dist/schema.spec.js.map +0 -1
- package/dist/tags.spec.d.ts +0 -8
- package/dist/tags.spec.js +0 -884
- package/dist/tags.spec.js.map +0 -1
- package/dist/util.spec.d.ts +0 -1
- package/dist/util.spec.js +0 -43
- package/dist/util.spec.js.map +0 -1
- package/src/motly-schema.motly +0 -52
- package/src/peggy/dist/peg-tag-parser.js +0 -2790
- package/src/peggy/index.ts +0 -89
- package/src/peggy/interpreter.ts +0 -265
- package/src/peggy/malloy-tag.peggy +0 -224
- package/src/peggy/statements.ts +0 -49
- package/src/schema.spec.ts +0 -1280
- package/src/schema.ts +0 -852
- package/src/tags.spec.ts +0 -967
- package/src/util.spec.ts +0 -43
- 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
|
-
}
|