@player-tools/xlr-sdk 0.9.0 → 0.9.1--canary.196.4186
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/cjs/index.cjs +116 -18
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.legacy-esm.js +115 -18
- package/dist/index.mjs +115 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/__snapshots__/sdk.test.ts.snap +12 -0
- package/src/__tests__/sdk.test.ts +150 -1
- package/src/types.ts +38 -5
- package/src/validator.ts +168 -18
- package/types/sdk.d.ts +2 -2
- package/types/types.d.ts +25 -5
- package/types/validator.d.ts +9 -3
package/src/validator.ts
CHANGED
|
@@ -16,16 +16,29 @@ import {
|
|
|
16
16
|
resolveReferenceNode,
|
|
17
17
|
computeEffectiveObject,
|
|
18
18
|
} from "@player-tools/xlr-utils";
|
|
19
|
-
import
|
|
19
|
+
import { ValidationSeverity } from "./types";
|
|
20
|
+
import type { ValidationMessage } from "./types";
|
|
21
|
+
|
|
22
|
+
export interface XLRValidatorConfig {
|
|
23
|
+
/** URL mapping for supplemental documentation */
|
|
24
|
+
urlMapping?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MAX_VALID_SHOWN = 20;
|
|
20
28
|
|
|
21
29
|
/**
|
|
22
30
|
* Validator for XLRs on JSON Nodes
|
|
23
31
|
*/
|
|
24
32
|
export class XLRValidator {
|
|
33
|
+
private config: XLRValidatorConfig;
|
|
25
34
|
private resolveType: (id: string) => NamedType<NodeType> | undefined;
|
|
26
35
|
private regexCache: Map<string, RegExp>;
|
|
27
36
|
|
|
28
|
-
constructor(
|
|
37
|
+
constructor(
|
|
38
|
+
resolveType: (id: string) => NamedType<NodeType> | undefined,
|
|
39
|
+
config?: XLRValidatorConfig
|
|
40
|
+
) {
|
|
41
|
+
this.config = config || {};
|
|
29
42
|
this.resolveType = resolveType;
|
|
30
43
|
this.regexCache = new Map();
|
|
31
44
|
}
|
|
@@ -34,8 +47,8 @@ export class XLRValidator {
|
|
|
34
47
|
public validateType(
|
|
35
48
|
rootNode: Node,
|
|
36
49
|
xlrNode: NodeType
|
|
37
|
-
): Array<
|
|
38
|
-
const validationIssues = new Array<
|
|
50
|
+
): Array<ValidationMessage> {
|
|
51
|
+
const validationIssues = new Array<ValidationMessage>();
|
|
39
52
|
if (xlrNode.type === "object") {
|
|
40
53
|
if (rootNode.type === "object") {
|
|
41
54
|
validationIssues.push(...this.validateObject(xlrNode, rootNode));
|
|
@@ -44,6 +57,7 @@ export class XLRValidator {
|
|
|
44
57
|
type: "type",
|
|
45
58
|
node: rootNode,
|
|
46
59
|
message: `Expected an object but got an "${rootNode.type}"`,
|
|
60
|
+
severity: ValidationSeverity.Error,
|
|
47
61
|
});
|
|
48
62
|
}
|
|
49
63
|
} else if (xlrNode.type === "array") {
|
|
@@ -54,6 +68,7 @@ export class XLRValidator {
|
|
|
54
68
|
type: "type",
|
|
55
69
|
node: rootNode,
|
|
56
70
|
message: `Expected an array but got an "${rootNode.type}"`,
|
|
71
|
+
severity: ValidationSeverity.Error,
|
|
57
72
|
});
|
|
58
73
|
}
|
|
59
74
|
} else if (xlrNode.type === "template") {
|
|
@@ -62,31 +77,58 @@ export class XLRValidator {
|
|
|
62
77
|
validationIssues.push(error);
|
|
63
78
|
}
|
|
64
79
|
} else if (xlrNode.type === "or") {
|
|
65
|
-
|
|
80
|
+
const potentialTypeErrors: Array<{
|
|
81
|
+
type: NodeType;
|
|
82
|
+
errors: Array<ValidationMessage>;
|
|
83
|
+
}> = [];
|
|
84
|
+
|
|
66
85
|
for (const potentialType of xlrNode.or) {
|
|
67
86
|
const potentialErrors = this.validateType(rootNode, potentialType);
|
|
87
|
+
|
|
68
88
|
if (potentialErrors.length === 0) {
|
|
69
89
|
return validationIssues;
|
|
70
90
|
}
|
|
91
|
+
|
|
92
|
+
potentialTypeErrors.push({
|
|
93
|
+
type: potentialType,
|
|
94
|
+
errors: potentialErrors,
|
|
95
|
+
});
|
|
71
96
|
}
|
|
72
97
|
|
|
73
98
|
let message: string;
|
|
99
|
+
const expectedTypes = xlrNode.or
|
|
100
|
+
.map((node) => node.name ?? node.title ?? node.type ?? "<unnamed type>")
|
|
101
|
+
.join(" | ");
|
|
74
102
|
|
|
75
103
|
if (xlrNode.name) {
|
|
76
104
|
message = `Does not match any of the expected types for type: '${xlrNode.name}'`;
|
|
77
105
|
} else if (xlrNode.title) {
|
|
78
106
|
message = `Does not match any of the expected types for property: '${xlrNode.title}'`;
|
|
79
107
|
} else {
|
|
80
|
-
message = `Does not match any of the types ${
|
|
81
|
-
.map((node) => node.name ?? node.title ?? "<unnamed type>")
|
|
82
|
-
.join(" | ")}`;
|
|
108
|
+
message = `Does not match any of the types: ${expectedTypes}`;
|
|
83
109
|
}
|
|
84
110
|
|
|
111
|
+
const { infoMessage } = this.generateNestedTypesInfo(
|
|
112
|
+
potentialTypeErrors,
|
|
113
|
+
xlrNode,
|
|
114
|
+
rootNode
|
|
115
|
+
);
|
|
116
|
+
|
|
85
117
|
validationIssues.push({
|
|
86
118
|
type: "value",
|
|
87
119
|
node: rootNode,
|
|
88
|
-
message,
|
|
120
|
+
message: message.trim(),
|
|
121
|
+
severity: ValidationSeverity.Error,
|
|
89
122
|
});
|
|
123
|
+
|
|
124
|
+
if (infoMessage) {
|
|
125
|
+
validationIssues.push({
|
|
126
|
+
type: "value",
|
|
127
|
+
node: rootNode,
|
|
128
|
+
message: infoMessage,
|
|
129
|
+
severity: ValidationSeverity.Info,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
90
132
|
} else if (xlrNode.type === "and") {
|
|
91
133
|
const effectiveType = {
|
|
92
134
|
...this.computeIntersectionType(xlrNode.and),
|
|
@@ -109,6 +151,7 @@ export class XLRValidator {
|
|
|
109
151
|
type: "unknown",
|
|
110
152
|
node: rootNode,
|
|
111
153
|
message: `Type "${xlrNode.ref}" is not defined in provided bundles`,
|
|
154
|
+
severity: ValidationSeverity.Error,
|
|
112
155
|
});
|
|
113
156
|
} else {
|
|
114
157
|
validationIssues.push(
|
|
@@ -127,12 +170,16 @@ export class XLRValidator {
|
|
|
127
170
|
type: "type",
|
|
128
171
|
node: rootNode.parent as Node,
|
|
129
172
|
message: `Expected "${xlrNode.const}" but got "${rootNode.value}"`,
|
|
173
|
+
expected: xlrNode.const,
|
|
174
|
+
severity: ValidationSeverity.Error,
|
|
130
175
|
});
|
|
131
176
|
} else {
|
|
132
177
|
validationIssues.push({
|
|
133
178
|
type: "type",
|
|
134
179
|
node: rootNode.parent as Node,
|
|
135
180
|
message: `Expected type "${xlrNode.type}" but got "${rootNode.type}"`,
|
|
181
|
+
expected: xlrNode.type,
|
|
182
|
+
severity: ValidationSeverity.Error,
|
|
136
183
|
});
|
|
137
184
|
}
|
|
138
185
|
}
|
|
@@ -173,15 +220,82 @@ export class XLRValidator {
|
|
|
173
220
|
return validationIssues;
|
|
174
221
|
}
|
|
175
222
|
|
|
223
|
+
private generateNestedTypesInfo(
|
|
224
|
+
potentialTypeErrors: Array<{
|
|
225
|
+
type: NodeType;
|
|
226
|
+
errors: Array<ValidationMessage>;
|
|
227
|
+
}>,
|
|
228
|
+
xlrNode: OrType,
|
|
229
|
+
rootNode: Node
|
|
230
|
+
): { nestedTypesList: string; infoMessage?: string } {
|
|
231
|
+
const nestedTypes = new Set<string>();
|
|
232
|
+
|
|
233
|
+
// TODO: Create a recursive function that returns value or xlrNode info
|
|
234
|
+
// First, try to extract types from potential type errors
|
|
235
|
+
potentialTypeErrors.forEach((typeError) => {
|
|
236
|
+
if (typeError.type.type !== "template") {
|
|
237
|
+
typeError.errors.forEach((error) => {
|
|
238
|
+
if (error.type === "type" && error.expected) {
|
|
239
|
+
// Split by separate types if union
|
|
240
|
+
String(error.expected)
|
|
241
|
+
.split(" | ")
|
|
242
|
+
.forEach((val) => nestedTypes.add(val.trim()));
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// If no types found from errors, try using type from xlrNode
|
|
249
|
+
if (nestedTypes.size === 0) {
|
|
250
|
+
xlrNode.or.forEach((type) => {
|
|
251
|
+
const typeName =
|
|
252
|
+
type.name ?? type.title ?? type.type ?? "<unnamed type>";
|
|
253
|
+
nestedTypes.add(typeName);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const nestedTypesArray = [...nestedTypes];
|
|
258
|
+
|
|
259
|
+
// Display list of expected types as a union
|
|
260
|
+
let nestedTypesList =
|
|
261
|
+
nestedTypesArray.slice(0, MAX_VALID_SHOWN).join(" | ") +
|
|
262
|
+
(nestedTypesArray.length > MAX_VALID_SHOWN
|
|
263
|
+
? ` | +${
|
|
264
|
+
nestedTypesArray.length - MAX_VALID_SHOWN
|
|
265
|
+
} ... ${nestedTypesArray.pop()}`
|
|
266
|
+
: "");
|
|
267
|
+
|
|
268
|
+
// TODO: Be able to pass the validator's config to the SDK
|
|
269
|
+
const docsURL = this.config.urlMapping;
|
|
270
|
+
|
|
271
|
+
// Support passing in a URL for matching type
|
|
272
|
+
if (docsURL && xlrNode.name && docsURL[xlrNode.name]) {
|
|
273
|
+
nestedTypesList = docsURL[xlrNode.name];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Support supplemental info message
|
|
277
|
+
let infoMessage;
|
|
278
|
+
|
|
279
|
+
if (rootNode.value !== undefined) {
|
|
280
|
+
infoMessage = `Got: ${rootNode.value} and expected: ${nestedTypesList}`;
|
|
281
|
+
} else if (nestedTypesList) {
|
|
282
|
+
infoMessage = `Expected: ${nestedTypesList}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return { nestedTypesList, infoMessage };
|
|
286
|
+
}
|
|
287
|
+
|
|
176
288
|
private validateTemplate(
|
|
177
289
|
node: Node,
|
|
178
290
|
xlrNode: TemplateLiteralType
|
|
179
|
-
):
|
|
291
|
+
): ValidationMessage | undefined {
|
|
180
292
|
if (node.type !== "string") {
|
|
181
293
|
return {
|
|
182
294
|
type: "type",
|
|
183
295
|
node: node.parent as Node,
|
|
184
296
|
message: `Expected type "${xlrNode.type}" but got "${typeof node}"`,
|
|
297
|
+
expected: xlrNode.type,
|
|
298
|
+
severity: ValidationSeverity.Error,
|
|
185
299
|
};
|
|
186
300
|
}
|
|
187
301
|
|
|
@@ -192,12 +306,14 @@ export class XLRValidator {
|
|
|
192
306
|
type: "value",
|
|
193
307
|
node: node.parent as Node,
|
|
194
308
|
message: `Does not match expected format: ${xlrNode.format}`,
|
|
309
|
+
expected: xlrNode.format,
|
|
310
|
+
severity: ValidationSeverity.Error,
|
|
195
311
|
};
|
|
196
312
|
}
|
|
197
313
|
}
|
|
198
314
|
|
|
199
315
|
private validateArray(rootNode: Node, xlrNode: ArrayType) {
|
|
200
|
-
const issues: Array<
|
|
316
|
+
const issues: Array<ValidationMessage> = [];
|
|
201
317
|
rootNode.children?.forEach((child) =>
|
|
202
318
|
issues.push(...this.validateType(child, xlrNode.elementType))
|
|
203
319
|
);
|
|
@@ -205,7 +321,7 @@ export class XLRValidator {
|
|
|
205
321
|
}
|
|
206
322
|
|
|
207
323
|
private validateObject(xlrNode: ObjectType, node: Node) {
|
|
208
|
-
const issues: Array<
|
|
324
|
+
const issues: Array<ValidationMessage> = [];
|
|
209
325
|
const objectProps = makePropertyMap(node);
|
|
210
326
|
|
|
211
327
|
// eslint-disable-next-line guard-for-in, no-restricted-syntax
|
|
@@ -217,6 +333,7 @@ export class XLRValidator {
|
|
|
217
333
|
type: "missing",
|
|
218
334
|
node,
|
|
219
335
|
message: `Property "${prop}" missing from type "${xlrNode.name}"`,
|
|
336
|
+
severity: ValidationSeverity.Error,
|
|
220
337
|
});
|
|
221
338
|
}
|
|
222
339
|
|
|
@@ -238,6 +355,7 @@ export class XLRValidator {
|
|
|
238
355
|
message: `Unexpected properties on "${xlrNode.name}": ${extraKeys.join(
|
|
239
356
|
", "
|
|
240
357
|
)}`,
|
|
358
|
+
severity: ValidationSeverity.Error,
|
|
241
359
|
});
|
|
242
360
|
} else {
|
|
243
361
|
issues.push(
|
|
@@ -316,6 +434,9 @@ export class XLRValidator {
|
|
|
316
434
|
let firstElement = types[0];
|
|
317
435
|
let effectiveType: ObjectType | OrType;
|
|
318
436
|
|
|
437
|
+
// Capture the original top-level type name if exists
|
|
438
|
+
const topLevelTypeName = types[0].name;
|
|
439
|
+
|
|
319
440
|
if (firstElement.type === "ref") {
|
|
320
441
|
firstElement = this.getRefType(firstElement);
|
|
321
442
|
}
|
|
@@ -361,18 +482,38 @@ export class XLRValidator {
|
|
|
361
482
|
} else {
|
|
362
483
|
effectiveType = {
|
|
363
484
|
...effectiveType,
|
|
364
|
-
or: effectiveType.or.map((y) =>
|
|
365
|
-
this.computeIntersectionType([
|
|
366
|
-
|
|
485
|
+
or: effectiveType.or.map((y) => {
|
|
486
|
+
const intersectedType = this.computeIntersectionType([
|
|
487
|
+
y,
|
|
488
|
+
typeToApply,
|
|
489
|
+
]);
|
|
490
|
+
|
|
491
|
+
// If the intersected type doesn't have a name, use the top-level type name
|
|
492
|
+
if (!intersectedType.name && topLevelTypeName) {
|
|
493
|
+
intersectedType.name = topLevelTypeName;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return intersectedType;
|
|
497
|
+
}),
|
|
367
498
|
};
|
|
368
499
|
}
|
|
369
500
|
} else if (typeToApply.type === "or") {
|
|
370
501
|
if (effectiveType.type === "object") {
|
|
371
502
|
effectiveType = {
|
|
372
503
|
...typeToApply,
|
|
373
|
-
or: typeToApply.or.map((y) =>
|
|
374
|
-
this.computeIntersectionType([
|
|
375
|
-
|
|
504
|
+
or: typeToApply.or.map((y) => {
|
|
505
|
+
const intersectedType = this.computeIntersectionType([
|
|
506
|
+
y,
|
|
507
|
+
effectiveType,
|
|
508
|
+
]);
|
|
509
|
+
|
|
510
|
+
// If the intersected type doesn't have a name, use the top-level type name
|
|
511
|
+
if (!intersectedType.name && topLevelTypeName) {
|
|
512
|
+
intersectedType.name = topLevelTypeName;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return intersectedType;
|
|
516
|
+
}),
|
|
376
517
|
};
|
|
377
518
|
} else {
|
|
378
519
|
throw new Error("unimplemented operation or x or projection");
|
|
@@ -384,6 +525,15 @@ export class XLRValidator {
|
|
|
384
525
|
}
|
|
385
526
|
});
|
|
386
527
|
|
|
528
|
+
// If the final effective type is an or type and doesn't have a name, use the top-level type name
|
|
529
|
+
if (
|
|
530
|
+
effectiveType.type === "or" &&
|
|
531
|
+
!effectiveType.name &&
|
|
532
|
+
topLevelTypeName
|
|
533
|
+
) {
|
|
534
|
+
effectiveType.name = topLevelTypeName;
|
|
535
|
+
}
|
|
536
|
+
|
|
387
537
|
return effectiveType;
|
|
388
538
|
}
|
|
389
539
|
}
|
package/types/sdk.d.ts
CHANGED
|
@@ -78,7 +78,7 @@ export declare class XLRSDK {
|
|
|
78
78
|
* @param rootNode - Node to validate
|
|
79
79
|
* @returns `Array<ValidationErrors>`
|
|
80
80
|
*/
|
|
81
|
-
validateByName(typeName: string, rootNode: Node): import("./types").
|
|
81
|
+
validateByName(typeName: string, rootNode: Node): import("./types").ValidationMessage[];
|
|
82
82
|
/**
|
|
83
83
|
* Validates if a JSONC Node follows the supplied XLR Type
|
|
84
84
|
*
|
|
@@ -86,7 +86,7 @@ export declare class XLRSDK {
|
|
|
86
86
|
* @param rootNode - Node to validate
|
|
87
87
|
* @returns `Array<ValidationErrors>`
|
|
88
88
|
*/
|
|
89
|
-
validateByType(type: NodeType, rootNode: Node): import("./types").
|
|
89
|
+
validateByType(type: NodeType, rootNode: Node): import("./types").ValidationMessage[];
|
|
90
90
|
/**
|
|
91
91
|
* Exports the types loaded into the registry to the specified format
|
|
92
92
|
*
|
package/types/types.d.ts
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import type { Node } from "jsonc-parser";
|
|
2
|
-
|
|
2
|
+
/** Support Export Formats */
|
|
3
|
+
export type ExportTypes = "TypeScript";
|
|
4
|
+
export interface BaseValidationMessage<ErrorType extends string = string> {
|
|
5
|
+
/** Validation Type */
|
|
6
|
+
type: ErrorType;
|
|
3
7
|
/** Error message text */
|
|
4
8
|
message: string;
|
|
5
9
|
/** JSONC node that the error originates from */
|
|
6
10
|
node: Node;
|
|
7
|
-
/**
|
|
8
|
-
|
|
11
|
+
/** Level of the message */
|
|
12
|
+
severity: ValidationSeverity;
|
|
13
|
+
}
|
|
14
|
+
export interface TypeValidationError extends BaseValidationMessage<"type"> {
|
|
15
|
+
/** Expected types */
|
|
16
|
+
expected?: string[] | string | number | boolean;
|
|
17
|
+
}
|
|
18
|
+
export type MissingValidationError = BaseValidationMessage<"missing">;
|
|
19
|
+
export type UnknownValidationError = BaseValidationMessage<"unknown">;
|
|
20
|
+
export interface ValueValidationError extends BaseValidationMessage<"value"> {
|
|
21
|
+
/** Expected value */
|
|
22
|
+
expected?: string;
|
|
23
|
+
}
|
|
24
|
+
export type UnexpectedValidationError = BaseValidationMessage<"unexpected">;
|
|
25
|
+
export type ValidationMessage = TypeValidationError | MissingValidationError | UnknownValidationError | ValueValidationError | UnexpectedValidationError;
|
|
26
|
+
export declare enum ValidationSeverity {
|
|
27
|
+
Error = 1,
|
|
28
|
+
Warning = 2,
|
|
29
|
+
Info = 3,
|
|
30
|
+
Trace = 4
|
|
9
31
|
}
|
|
10
|
-
/** Support Export Formats */
|
|
11
|
-
export type ExportTypes = "TypeScript";
|
|
12
32
|
//# sourceMappingURL=types.d.ts.map
|
package/types/validator.d.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import type { Node } from "jsonc-parser";
|
|
2
2
|
import type { NamedType, NodeType, ObjectType, OrType, RefType } from "@player-tools/xlr";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ValidationMessage } from "./types";
|
|
4
|
+
export interface XLRValidatorConfig {
|
|
5
|
+
/** URL mapping for supplemental documentation */
|
|
6
|
+
urlMapping?: Record<string, string>;
|
|
7
|
+
}
|
|
4
8
|
/**
|
|
5
9
|
* Validator for XLRs on JSON Nodes
|
|
6
10
|
*/
|
|
7
11
|
export declare class XLRValidator {
|
|
12
|
+
private config;
|
|
8
13
|
private resolveType;
|
|
9
14
|
private regexCache;
|
|
10
|
-
constructor(resolveType: (id: string) => NamedType<NodeType> | undefined);
|
|
15
|
+
constructor(resolveType: (id: string) => NamedType<NodeType> | undefined, config?: XLRValidatorConfig);
|
|
11
16
|
/** Main entrypoint for validation */
|
|
12
|
-
validateType(rootNode: Node, xlrNode: NodeType): Array<
|
|
17
|
+
validateType(rootNode: Node, xlrNode: NodeType): Array<ValidationMessage>;
|
|
18
|
+
private generateNestedTypesInfo;
|
|
13
19
|
private validateTemplate;
|
|
14
20
|
private validateArray;
|
|
15
21
|
private validateObject;
|