@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/src/validator.ts CHANGED
@@ -16,16 +16,29 @@ import {
16
16
  resolveReferenceNode,
17
17
  computeEffectiveObject,
18
18
  } from "@player-tools/xlr-utils";
19
- import type { ValidationError } from "./types";
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(resolveType: (id: string) => NamedType<NodeType> | undefined) {
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<ValidationError> {
38
- const validationIssues = new Array<ValidationError>();
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
- // eslint-disable-next-line no-restricted-syntax
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 ${xlrNode.or
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
- ): ValidationError | undefined {
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<ValidationError> = [];
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<ValidationError> = [];
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([y, typeToApply])
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([y, effectiveType])
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").ValidationError[];
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").ValidationError[];
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
- export interface ValidationError {
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
- /** Rough categorization of the error type */
8
- type: "type" | "missing" | "unknown" | "value" | "unexpected";
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
@@ -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 { ValidationError } from "./types";
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<ValidationError>;
17
+ validateType(rootNode: Node, xlrNode: NodeType): Array<ValidationMessage>;
18
+ private generateNestedTypesInfo;
13
19
  private validateTemplate;
14
20
  private validateArray;
15
21
  private validateObject;