@xlr-lib/xlr-sdk 0.1.1-next.0
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 +992 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/index.legacy-esm.js +961 -0
- package/dist/index.mjs +961 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +38 -0
- package/src/__tests__/__snapshots__/sdk.test.ts.snap +3167 -0
- package/src/__tests__/sdk.test.ts +392 -0
- package/src/__tests__/utils.test.ts +0 -0
- package/src/index.ts +4 -0
- package/src/registry/basic-registry.ts +82 -0
- package/src/registry/index.ts +2 -0
- package/src/registry/types.ts +28 -0
- package/src/sdk.ts +458 -0
- package/src/types.ts +48 -0
- package/src/utils.ts +191 -0
- package/src/validator.ts +542 -0
- package/types/index.d.ts +5 -0
- package/types/registry/basic-registry.d.ts +18 -0
- package/types/registry/index.d.ts +3 -0
- package/types/registry/types.d.ts +23 -0
- package/types/sdk.d.ts +111 -0
- package/types/types.d.ts +32 -0
- package/types/utils.d.ts +13 -0
- package/types/validator.d.ts +27 -0
package/src/sdk.ts
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/* eslint-disable prettier/prettier */
|
|
2
|
+
import type {
|
|
3
|
+
Manifest,
|
|
4
|
+
NamedType,
|
|
5
|
+
NodeType,
|
|
6
|
+
ObjectNode,
|
|
7
|
+
ObjectType,
|
|
8
|
+
TransformFunction,
|
|
9
|
+
TSManifest,
|
|
10
|
+
} from "@xlr-lib/xlr";
|
|
11
|
+
import type { TopLevelDeclaration } from "@xlr-lib/xlr-utils";
|
|
12
|
+
import {
|
|
13
|
+
computeEffectiveObject,
|
|
14
|
+
resolveConditional,
|
|
15
|
+
resolveReferenceNode,
|
|
16
|
+
} from "@xlr-lib/xlr-utils";
|
|
17
|
+
import { fillInGenerics } from "@xlr-lib/xlr-utils";
|
|
18
|
+
import type { Node } from "jsonc-parser";
|
|
19
|
+
import { TSWriter } from "@xlr-lib/xlr-converters";
|
|
20
|
+
import fs from "fs";
|
|
21
|
+
import path from "path";
|
|
22
|
+
import ts from "typescript";
|
|
23
|
+
|
|
24
|
+
import type { XLRRegistry, Filters } from "./registry";
|
|
25
|
+
import { BasicXLRRegistry } from "./registry";
|
|
26
|
+
import type { ExportTypes } from "./types";
|
|
27
|
+
import { XLRValidator } from "./validator";
|
|
28
|
+
import { TransformFunctionMap, xlrTransformWalker } from "./utils";
|
|
29
|
+
|
|
30
|
+
export interface GetTypeOptions {
|
|
31
|
+
/** Resolves `extends` fields in objects */
|
|
32
|
+
getRawType?: boolean;
|
|
33
|
+
/** Perform optimizations to resolve all references, type intersections, and conditionals */
|
|
34
|
+
optimize?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Abstraction for interfacing with XLRs making it more approachable to use without understanding the inner workings of the types and how they are packaged
|
|
39
|
+
*/
|
|
40
|
+
export class XLRSDK {
|
|
41
|
+
private registry: XLRRegistry;
|
|
42
|
+
private validator: XLRValidator;
|
|
43
|
+
private tsWriter: TSWriter;
|
|
44
|
+
private computedNodeCache: Map<string, NodeType>;
|
|
45
|
+
private externalTransformFunctions: Map<string, TransformFunction>;
|
|
46
|
+
|
|
47
|
+
constructor(customRegistry?: XLRRegistry) {
|
|
48
|
+
this.registry = customRegistry ?? new BasicXLRRegistry();
|
|
49
|
+
this.validator = new XLRValidator(this.getType.bind(this));
|
|
50
|
+
this.tsWriter = new TSWriter();
|
|
51
|
+
this.computedNodeCache = new Map();
|
|
52
|
+
this.externalTransformFunctions = new Map();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Loads definitions from a path on the filesystem
|
|
57
|
+
*
|
|
58
|
+
* @param inputPath - path to the directory to load (above the xlr folder)
|
|
59
|
+
* @param filters - Any filters to apply when loading the types (a positive match will omit)
|
|
60
|
+
* @param transforms - any transforms to apply to the types being loaded
|
|
61
|
+
*/
|
|
62
|
+
public loadDefinitionsFromDisk(
|
|
63
|
+
inputPath: string,
|
|
64
|
+
filters?: Omit<Filters, "pluginFilter">,
|
|
65
|
+
transforms?: Array<TransformFunction>
|
|
66
|
+
) {
|
|
67
|
+
this.computedNodeCache.clear();
|
|
68
|
+
|
|
69
|
+
const transformsToRun = [
|
|
70
|
+
...this.externalTransformFunctions.values(),
|
|
71
|
+
...(transforms ?? []),
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const manifest = JSON.parse(
|
|
75
|
+
fs.readFileSync(path.join(inputPath, "xlr", "manifest.json")).toString(),
|
|
76
|
+
(key: unknown, value: unknown) => {
|
|
77
|
+
// Custom parser because JSON objects -> JS Objects, not maps
|
|
78
|
+
if (typeof value === "object" && value !== null) {
|
|
79
|
+
if (key === "capabilities") {
|
|
80
|
+
return new Map(Object.entries(value));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
) as Manifest;
|
|
87
|
+
|
|
88
|
+
manifest.capabilities?.forEach((capabilityList, capabilityName) => {
|
|
89
|
+
if (
|
|
90
|
+
filters?.capabilityFilter &&
|
|
91
|
+
capabilityName.match(filters?.capabilityFilter)
|
|
92
|
+
)
|
|
93
|
+
return;
|
|
94
|
+
capabilityList.forEach((extensionName) => {
|
|
95
|
+
if (!filters?.typeFilter || !extensionName.match(filters?.typeFilter)) {
|
|
96
|
+
const cType: NamedType<NodeType> = JSON.parse(
|
|
97
|
+
fs
|
|
98
|
+
.readFileSync(
|
|
99
|
+
path.join(inputPath, "xlr", `${extensionName}.json`)
|
|
100
|
+
)
|
|
101
|
+
.toString()
|
|
102
|
+
);
|
|
103
|
+
const effectiveType =
|
|
104
|
+
transformsToRun?.reduce(
|
|
105
|
+
(typeAccumulator: NamedType<NodeType>, transformFn) =>
|
|
106
|
+
transformFn(
|
|
107
|
+
typeAccumulator,
|
|
108
|
+
capabilityName
|
|
109
|
+
) as NamedType<NodeType>,
|
|
110
|
+
cType
|
|
111
|
+
) ?? cType;
|
|
112
|
+
|
|
113
|
+
this.registry.add(effectiveType, manifest.pluginName, capabilityName);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Load definitions from a js/ts file in memory
|
|
121
|
+
*
|
|
122
|
+
* @param manifest - The imported XLR manifest module
|
|
123
|
+
* @param filters - Any filters to apply when loading the types (a positive match will omit)
|
|
124
|
+
* @param transforms - any transforms to apply to the types being loaded
|
|
125
|
+
*/
|
|
126
|
+
public async loadDefinitionsFromModule(
|
|
127
|
+
manifest: TSManifest,
|
|
128
|
+
filters?: Omit<Filters, "pluginFilter">,
|
|
129
|
+
transforms?: Array<TransformFunction>
|
|
130
|
+
) {
|
|
131
|
+
this.computedNodeCache.clear();
|
|
132
|
+
|
|
133
|
+
const transformsToRun = [
|
|
134
|
+
...this.externalTransformFunctions.values(),
|
|
135
|
+
...(transforms ?? []),
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
Object.keys(manifest.capabilities)?.forEach((capabilityName) => {
|
|
139
|
+
if (
|
|
140
|
+
filters?.capabilityFilter &&
|
|
141
|
+
capabilityName.match(filters?.capabilityFilter)
|
|
142
|
+
)
|
|
143
|
+
return;
|
|
144
|
+
const capabilityList = manifest.capabilities[capabilityName];
|
|
145
|
+
capabilityList.forEach((extension) => {
|
|
146
|
+
if (
|
|
147
|
+
!filters?.typeFilter ||
|
|
148
|
+
!extension.name.match(filters?.typeFilter)
|
|
149
|
+
) {
|
|
150
|
+
const effectiveType =
|
|
151
|
+
transformsToRun?.reduce(
|
|
152
|
+
(typeAccumulator: NamedType<NodeType>, transformFn) =>
|
|
153
|
+
transformFn(
|
|
154
|
+
typeAccumulator,
|
|
155
|
+
capabilityName
|
|
156
|
+
) as NamedType<NodeType>,
|
|
157
|
+
extension
|
|
158
|
+
) ?? extension;
|
|
159
|
+
|
|
160
|
+
this.registry.add(effectiveType, manifest.pluginName, capabilityName);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Statically load transform function that should be applied to every XLR bundle that is imported
|
|
168
|
+
*/
|
|
169
|
+
public addTransformFunction(name: string, fn: TransformFunction): void {
|
|
170
|
+
this.externalTransformFunctions.set(name, fn);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Remove any transform function loaded via the `addTransformFunction` method by name
|
|
175
|
+
*/
|
|
176
|
+
public removeTransformFunction(name: string): void {
|
|
177
|
+
this.externalTransformFunctions.delete(name);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Returns a Type that has been previously loaded
|
|
182
|
+
*
|
|
183
|
+
* @param id - Type to retrieve
|
|
184
|
+
* @param options - `GetTypeOptions`
|
|
185
|
+
* @returns `NamedType<NodeType>` | `undefined`
|
|
186
|
+
*/
|
|
187
|
+
public getType(
|
|
188
|
+
id: string,
|
|
189
|
+
options?: GetTypeOptions
|
|
190
|
+
): NamedType<NodeType> | undefined {
|
|
191
|
+
let type = this.registry.get(id);
|
|
192
|
+
if (options?.getRawType === true || !type) {
|
|
193
|
+
return type;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (this.computedNodeCache.has(id)) {
|
|
197
|
+
return JSON.parse(JSON.stringify(this.computedNodeCache.get(id))) as
|
|
198
|
+
| NamedType<NodeType>
|
|
199
|
+
| undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
type = this.resolveType(type, options?.optimize)
|
|
203
|
+
|
|
204
|
+
this.computedNodeCache.set(id, type);
|
|
205
|
+
|
|
206
|
+
return type;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Returns if a Type with `id` has been loaded into the DSK
|
|
211
|
+
*
|
|
212
|
+
* @param id - Type to retrieve
|
|
213
|
+
* @returns `boolean`
|
|
214
|
+
*/
|
|
215
|
+
public hasType(id: string) {
|
|
216
|
+
return this.registry.has(id);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Lists types that have been loaded into the SDK
|
|
221
|
+
*
|
|
222
|
+
* @param filters - Any filters to apply to the types returned (a positive match will omit)
|
|
223
|
+
* @returns `Array<NamedTypes>`
|
|
224
|
+
*/
|
|
225
|
+
public listTypes(filters?: Filters) {
|
|
226
|
+
return this.registry.list(filters);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Returns meta information around a registered type
|
|
231
|
+
*
|
|
232
|
+
* @param id - Name of Type to retrieve
|
|
233
|
+
* @returns `TypeMetaData` | `undefined`
|
|
234
|
+
*/
|
|
235
|
+
public getTypeInfo(id: string) {
|
|
236
|
+
return this.registry.info(id);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Validates if a JSONC Node follows the XLR Type registered under the `typeName` specified
|
|
241
|
+
*
|
|
242
|
+
* @param typeName - Registered XLR Type to use for validation
|
|
243
|
+
* @param rootNode - Node to validate
|
|
244
|
+
* @returns `Array<ValidationErrors>`
|
|
245
|
+
*/
|
|
246
|
+
public validateByName(typeName: string, rootNode: Node) {
|
|
247
|
+
const xlr = this.getType(typeName);
|
|
248
|
+
if (!xlr) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Type ${typeName} does not exist in registry, can't validate`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return this.validator.validateType(rootNode, xlr);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Validates if a JSONC Node follows the supplied XLR Type
|
|
259
|
+
*
|
|
260
|
+
* @param type - Type to validate against
|
|
261
|
+
* @param rootNode - Node to validate
|
|
262
|
+
* @returns `Array<ValidationErrors>`
|
|
263
|
+
*/
|
|
264
|
+
public validateByType(type: NodeType, rootNode: Node) {
|
|
265
|
+
return this.validator.validateType(rootNode, type);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Exports the types loaded into the registry to the specified format
|
|
270
|
+
*
|
|
271
|
+
* @param exportType - what format to export as
|
|
272
|
+
* @param importMap - a map of primitive packages to types exported from that package to add import statements
|
|
273
|
+
* @param filters - filter out plugins/capabilities/types you don't want to export
|
|
274
|
+
* @param transforms - transforms to apply to types before exporting them
|
|
275
|
+
* @returns [filename, content][] - Tuples of filenames and content to write
|
|
276
|
+
*/
|
|
277
|
+
public exportRegistry(
|
|
278
|
+
exportType: ExportTypes,
|
|
279
|
+
importMap: Map<string, string[]>,
|
|
280
|
+
filters?: Filters,
|
|
281
|
+
transforms?: Array<TransformFunction>
|
|
282
|
+
): [string, string][] {
|
|
283
|
+
const typesToExport = this.registry.list(filters).map((type) => {
|
|
284
|
+
const effectiveType =
|
|
285
|
+
transforms?.reduce(
|
|
286
|
+
(typeAccumulator: NamedType<NodeType>, transformFn) =>
|
|
287
|
+
transformFn(
|
|
288
|
+
typeAccumulator,
|
|
289
|
+
this.registry.info(type.name)?.capability as string
|
|
290
|
+
) as NamedType<NodeType>,
|
|
291
|
+
type
|
|
292
|
+
) ?? type;
|
|
293
|
+
|
|
294
|
+
return effectiveType;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (exportType === "TypeScript") {
|
|
298
|
+
const outputString = this.exportToTypeScript(typesToExport, importMap);
|
|
299
|
+
return [["out.d.ts", outputString]];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
throw new Error(`Unknown export format ${exportType}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Transforms a generated XLR node into its final representation by resolving all `extends` properties.
|
|
307
|
+
* If `optimize` is set to true the following operations are also performed:
|
|
308
|
+
* - Solving any conditional types
|
|
309
|
+
* - Computing the effective types of any union elements
|
|
310
|
+
* - Resolving any ref nodes
|
|
311
|
+
* - filing in any remaining generics with their default value
|
|
312
|
+
*/
|
|
313
|
+
private resolveType(type: NamedType, optimize = true): NamedType {
|
|
314
|
+
const resolvedObject = fillInGenerics(type);
|
|
315
|
+
|
|
316
|
+
let transformMap: TransformFunctionMap = {
|
|
317
|
+
object: [(objectNode: ObjectType) => {
|
|
318
|
+
if (objectNode.extends) {
|
|
319
|
+
const refName = objectNode.extends.ref.split("<")[0];
|
|
320
|
+
let extendedType = this.getType(refName, {getRawType: true});
|
|
321
|
+
if (!extendedType) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Error resolving ${objectNode.name}: can't find extended type ${refName}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
extendedType = resolveReferenceNode(
|
|
328
|
+
objectNode.extends,
|
|
329
|
+
extendedType as NamedType<ObjectType>
|
|
330
|
+
) as NamedType;
|
|
331
|
+
if (extendedType.type === "object") {
|
|
332
|
+
return {
|
|
333
|
+
...computeEffectiveObject(
|
|
334
|
+
extendedType as ObjectType,
|
|
335
|
+
objectNode as ObjectType,
|
|
336
|
+
false
|
|
337
|
+
),
|
|
338
|
+
name: objectNode.name,
|
|
339
|
+
description: objectNode.description,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if( extendedType.type === "or"){
|
|
344
|
+
return {
|
|
345
|
+
...this.validator.computeIntersectionType([
|
|
346
|
+
objectNode,
|
|
347
|
+
extendedType
|
|
348
|
+
]
|
|
349
|
+
),
|
|
350
|
+
name: objectNode.name,
|
|
351
|
+
description: objectNode.description,
|
|
352
|
+
} as any;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// if the merge isn't straightforward, defer until validation time for now
|
|
356
|
+
return {
|
|
357
|
+
name: objectNode.name,
|
|
358
|
+
type: "and",
|
|
359
|
+
and: [
|
|
360
|
+
{
|
|
361
|
+
...objectNode,
|
|
362
|
+
extends: undefined,
|
|
363
|
+
},
|
|
364
|
+
extendedType,
|
|
365
|
+
],
|
|
366
|
+
} as unknown as ObjectNode;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return objectNode;
|
|
370
|
+
}],
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if(optimize){
|
|
374
|
+
transformMap = {
|
|
375
|
+
...transformMap,
|
|
376
|
+
conditional: [(node) => {
|
|
377
|
+
return resolveConditional(node) as any
|
|
378
|
+
}],
|
|
379
|
+
and: [(node) => {
|
|
380
|
+
return {
|
|
381
|
+
...this.validator.computeIntersectionType(node.and),
|
|
382
|
+
...(node.name ? { name: node.name } : {}),
|
|
383
|
+
} as any
|
|
384
|
+
}],
|
|
385
|
+
ref: [(refNode) => {
|
|
386
|
+
return this.validator.getRefType(refNode) as any
|
|
387
|
+
}]
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return xlrTransformWalker(transformMap)(resolvedObject) as NamedType
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private exportToTypeScript(
|
|
395
|
+
typesToExport: NamedType[],
|
|
396
|
+
importMap: Map<string, string[]>
|
|
397
|
+
): string {
|
|
398
|
+
const referencedImports: Set<string> = new Set();
|
|
399
|
+
const exportedTypes: Map<string, TopLevelDeclaration> = new Map();
|
|
400
|
+
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
401
|
+
|
|
402
|
+
let resultFile = ts.createSourceFile(
|
|
403
|
+
"output.d.ts",
|
|
404
|
+
"",
|
|
405
|
+
ts.ScriptTarget.ES2017,
|
|
406
|
+
false, // setParentNodes
|
|
407
|
+
ts.ScriptKind.TS
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
typesToExport.forEach((typeNode) => {
|
|
411
|
+
const { type, referencedTypes, additionalTypes } =
|
|
412
|
+
this.tsWriter.convertNamedType(typeNode);
|
|
413
|
+
exportedTypes.set(typeNode.name, type);
|
|
414
|
+
additionalTypes?.forEach((additionalType, name) =>
|
|
415
|
+
exportedTypes.set(name, additionalType)
|
|
416
|
+
);
|
|
417
|
+
referencedTypes?.forEach((referencedType) =>
|
|
418
|
+
referencedImports.add(referencedType)
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const typesToPrint: Array<string> = [];
|
|
423
|
+
|
|
424
|
+
exportedTypes.forEach((type) =>
|
|
425
|
+
typesToPrint.push(
|
|
426
|
+
printer.printNode(ts.EmitHint.Unspecified, type, resultFile)
|
|
427
|
+
)
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
importMap.forEach((imports, packageName) => {
|
|
431
|
+
const applicableImports = imports.filter((i) => referencedImports.has(i));
|
|
432
|
+
resultFile = ts.factory.updateSourceFile(resultFile, [
|
|
433
|
+
ts.factory.createImportDeclaration(
|
|
434
|
+
/* modifiers */ undefined,
|
|
435
|
+
ts.factory.createImportClause(
|
|
436
|
+
false,
|
|
437
|
+
undefined,
|
|
438
|
+
ts.factory.createNamedImports(
|
|
439
|
+
applicableImports.map((i) =>
|
|
440
|
+
ts.factory.createImportSpecifier(
|
|
441
|
+
false,
|
|
442
|
+
undefined,
|
|
443
|
+
ts.factory.createIdentifier(i)
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
),
|
|
448
|
+
ts.factory.createStringLiteral(packageName)
|
|
449
|
+
),
|
|
450
|
+
...resultFile.statements,
|
|
451
|
+
]);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const headerText = printer.printFile(resultFile);
|
|
455
|
+
const nodeText = typesToPrint.join("\n");
|
|
456
|
+
return `${headerText}\n${nodeText}`;
|
|
457
|
+
}
|
|
458
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Node } from "jsonc-parser";
|
|
2
|
+
|
|
3
|
+
/** Support Export Formats */
|
|
4
|
+
export type ExportTypes = "TypeScript";
|
|
5
|
+
|
|
6
|
+
export interface BaseValidationMessage<ErrorType extends string = string> {
|
|
7
|
+
/** Validation Type */
|
|
8
|
+
type: ErrorType;
|
|
9
|
+
|
|
10
|
+
/** Error message text */
|
|
11
|
+
message: string;
|
|
12
|
+
|
|
13
|
+
/** JSONC node that the error originates from */
|
|
14
|
+
node: Node;
|
|
15
|
+
|
|
16
|
+
/** Level of the message */
|
|
17
|
+
severity: ValidationSeverity;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TypeValidationError extends BaseValidationMessage<"type"> {
|
|
21
|
+
/** Expected types */
|
|
22
|
+
expected?: string[] | string | number | boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type MissingValidationError = BaseValidationMessage<"missing">;
|
|
26
|
+
|
|
27
|
+
export type UnknownValidationError = BaseValidationMessage<"unknown">;
|
|
28
|
+
|
|
29
|
+
export interface ValueValidationError extends BaseValidationMessage<"value"> {
|
|
30
|
+
/** Expected value */
|
|
31
|
+
expected?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type UnexpectedValidationError = BaseValidationMessage<"unexpected">;
|
|
35
|
+
|
|
36
|
+
export type ValidationMessage =
|
|
37
|
+
| TypeValidationError
|
|
38
|
+
| MissingValidationError
|
|
39
|
+
| UnknownValidationError
|
|
40
|
+
| ValueValidationError
|
|
41
|
+
| UnexpectedValidationError;
|
|
42
|
+
|
|
43
|
+
export enum ValidationSeverity {
|
|
44
|
+
Error = 1,
|
|
45
|
+
Warning = 2,
|
|
46
|
+
Info = 3,
|
|
47
|
+
Trace = 4,
|
|
48
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NamedType,
|
|
3
|
+
NodeTypeStrings,
|
|
4
|
+
NodeTypeMap,
|
|
5
|
+
TransformFunction,
|
|
6
|
+
NodeType,
|
|
7
|
+
ObjectProperty,
|
|
8
|
+
RefNode,
|
|
9
|
+
} from "@xlr-lib/xlr";
|
|
10
|
+
import { isGenericNamedType } from "@xlr-lib/xlr-utils";
|
|
11
|
+
|
|
12
|
+
type TypedTransformFunction<T extends NodeTypeStrings = NodeTypeStrings> = (
|
|
13
|
+
input: NodeTypeMap[T],
|
|
14
|
+
) => NodeTypeMap[T];
|
|
15
|
+
|
|
16
|
+
export type TransformFunctionMap = {
|
|
17
|
+
[nodeType in NodeTypeStrings]?: Array<TypedTransformFunction<nodeType>>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const isMatchingCapability = (
|
|
21
|
+
capability: string,
|
|
22
|
+
capabilitiesToMatch: string | Array<string>,
|
|
23
|
+
): boolean => {
|
|
24
|
+
if (Array.isArray(capabilitiesToMatch)) {
|
|
25
|
+
return capabilitiesToMatch.includes(capability);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return capability === capabilitiesToMatch;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function xlrTransformWalker(
|
|
32
|
+
transformMap: TransformFunctionMap,
|
|
33
|
+
): (node: NodeType) => NodeType {
|
|
34
|
+
const walker = (n: NamedType | NodeType): NodeType => {
|
|
35
|
+
let node = { ...n };
|
|
36
|
+
const transformFunctions = transformMap[node.type] as unknown as Array<
|
|
37
|
+
TypedTransformFunction<typeof node.type>
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
for (const transformFn of transformFunctions ?? []) {
|
|
41
|
+
node = transformFn(node);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (node.type === "object") {
|
|
45
|
+
const newObjectProperties: Record<string, ObjectProperty> = {};
|
|
46
|
+
|
|
47
|
+
for (const key in node.properties) {
|
|
48
|
+
const value = node.properties[key];
|
|
49
|
+
newObjectProperties[key] = {
|
|
50
|
+
required: value.required,
|
|
51
|
+
node: walker(value.node),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// need to walk generic tokens
|
|
56
|
+
return {
|
|
57
|
+
...node,
|
|
58
|
+
properties: { ...newObjectProperties },
|
|
59
|
+
...(isGenericNamedType(node)
|
|
60
|
+
? {
|
|
61
|
+
genericTokens: node.genericTokens.map((token) => {
|
|
62
|
+
return {
|
|
63
|
+
...token,
|
|
64
|
+
constraints: token.constraints
|
|
65
|
+
? walker(token.constraints)
|
|
66
|
+
: undefined,
|
|
67
|
+
default: token.default ? walker(token.default) : undefined,
|
|
68
|
+
};
|
|
69
|
+
}),
|
|
70
|
+
}
|
|
71
|
+
: {}),
|
|
72
|
+
extends: node.extends ? (walker(node.extends) as RefNode) : undefined,
|
|
73
|
+
additionalProperties: node.additionalProperties
|
|
74
|
+
? walker(node.additionalProperties)
|
|
75
|
+
: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (node.type === "array") {
|
|
80
|
+
return {
|
|
81
|
+
...node,
|
|
82
|
+
elementType: walker(node.elementType),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (node.type === "and") {
|
|
87
|
+
return {
|
|
88
|
+
...node,
|
|
89
|
+
and: node.and.map((element) => walker(element)),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (node.type === "or") {
|
|
94
|
+
return {
|
|
95
|
+
...node,
|
|
96
|
+
or: node.or.map((element) => walker(element)),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (node.type === "ref") {
|
|
101
|
+
return {
|
|
102
|
+
...node,
|
|
103
|
+
...(node.genericArguments
|
|
104
|
+
? {
|
|
105
|
+
genericArguments: node.genericArguments?.map((arg) =>
|
|
106
|
+
walker(arg),
|
|
107
|
+
),
|
|
108
|
+
}
|
|
109
|
+
: {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (node.type === "tuple") {
|
|
114
|
+
return {
|
|
115
|
+
...node,
|
|
116
|
+
elementTypes: node.elementTypes.map((element) => {
|
|
117
|
+
return {
|
|
118
|
+
name: element.name,
|
|
119
|
+
type: walker(element.type),
|
|
120
|
+
optional: element.optional,
|
|
121
|
+
};
|
|
122
|
+
}),
|
|
123
|
+
additionalItems: node.additionalItems
|
|
124
|
+
? walker(node.additionalItems)
|
|
125
|
+
: false,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (node.type === "function") {
|
|
130
|
+
return {
|
|
131
|
+
...node,
|
|
132
|
+
parameters: node.parameters.map((param) => {
|
|
133
|
+
return {
|
|
134
|
+
...param,
|
|
135
|
+
type: walker(param.type),
|
|
136
|
+
default: param.default ? walker(param.default) : undefined,
|
|
137
|
+
};
|
|
138
|
+
}),
|
|
139
|
+
returnType: node.returnType ? walker(node.returnType) : undefined,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (node.type === "record") {
|
|
144
|
+
return {
|
|
145
|
+
...node,
|
|
146
|
+
keyType: walker(node.keyType),
|
|
147
|
+
valueType: walker(node.valueType),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (node.type === "conditional") {
|
|
152
|
+
return {
|
|
153
|
+
...node,
|
|
154
|
+
check: {
|
|
155
|
+
left: walker(node.check.left),
|
|
156
|
+
right: walker(node.check.left),
|
|
157
|
+
},
|
|
158
|
+
value: {
|
|
159
|
+
true: walker(node.value.true),
|
|
160
|
+
false: walker(node.value.false),
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return node;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return walker;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Helper function for simple transforms
|
|
173
|
+
* Walks an XLR tree looking for the specified node type calls the supplied function when called
|
|
174
|
+
*/
|
|
175
|
+
export function simpleTransformGenerator<
|
|
176
|
+
T extends NodeTypeStrings = NodeTypeStrings,
|
|
177
|
+
>(
|
|
178
|
+
typeToTransform: T,
|
|
179
|
+
capabilityToTransform: string | Array<string>,
|
|
180
|
+
functionToRun: TypedTransformFunction<T>,
|
|
181
|
+
): TransformFunction {
|
|
182
|
+
/** walker for an XLR tree to touch every node */
|
|
183
|
+
return (n: NamedType | NodeType, capability: string) => {
|
|
184
|
+
// Run transform on base node before running on children
|
|
185
|
+
if (isMatchingCapability(capability, capabilityToTransform)) {
|
|
186
|
+
return xlrTransformWalker({ [typeToTransform]: [functionToRun] })(n);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return n;
|
|
190
|
+
};
|
|
191
|
+
}
|