@xlr-lib/xlr-sdk 0.1.1--canary.9.190

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/sdk.ts ADDED
@@ -0,0 +1,351 @@
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 {
12
+ computeEffectiveObject,
13
+ resolveConditional,
14
+ resolveReferenceNode,
15
+ } from "@xlr-lib/xlr-utils";
16
+ import { fillInGenerics } from "@xlr-lib/xlr-utils";
17
+ import type { Node } from "jsonc-parser";
18
+ import fs from "fs";
19
+ import path from "path";
20
+
21
+ import type { XLRRegistry, Filters } from "./registry";
22
+ import { BasicXLRRegistry } from "./registry";
23
+ import { XLRValidator } from "./validator";
24
+ import { TransformFunctionMap, xlrTransformWalker } from "./utils";
25
+
26
+ export interface GetTypeOptions {
27
+ /** Resolves `extends` fields in objects */
28
+ getRawType?: boolean;
29
+ /** Perform optimizations to resolve all references, type intersections, and conditionals */
30
+ optimize?: boolean;
31
+ }
32
+
33
+ /**
34
+ * Abstraction for interfacing with XLRs making it more approachable to use without understanding the inner workings of the types and how they are packaged
35
+ */
36
+ export class XLRSDK {
37
+ private registry: XLRRegistry;
38
+ private validator: XLRValidator;
39
+ private computedNodeCache: Map<string, NodeType>;
40
+ private externalTransformFunctions: Map<string, TransformFunction>;
41
+
42
+ constructor(customRegistry?: XLRRegistry) {
43
+ this.registry = customRegistry ?? new BasicXLRRegistry();
44
+ this.validator = new XLRValidator(this.getType.bind(this));
45
+ this.computedNodeCache = new Map();
46
+ this.externalTransformFunctions = new Map();
47
+ }
48
+
49
+ /**
50
+ * Loads definitions from a path on the filesystem
51
+ *
52
+ * @param inputPath - path to the directory to load (above the xlr folder)
53
+ * @param filters - Any filters to apply when loading the types (a positive match will omit)
54
+ * @param transforms - any transforms to apply to the types being loaded
55
+ */
56
+ public loadDefinitionsFromDisk(
57
+ inputPath: string,
58
+ filters?: Omit<Filters, "pluginFilter">,
59
+ transforms?: Array<TransformFunction>
60
+ ) {
61
+ this.computedNodeCache.clear();
62
+
63
+ const transformsToRun = [
64
+ ...this.externalTransformFunctions.values(),
65
+ ...(transforms ?? []),
66
+ ];
67
+
68
+ const manifest = JSON.parse(
69
+ fs.readFileSync(path.join(inputPath, "xlr", "manifest.json")).toString(),
70
+ (key: unknown, value: unknown) => {
71
+ // Custom parser because JSON objects -> JS Objects, not maps
72
+ if (typeof value === "object" && value !== null) {
73
+ if (key === "capabilities") {
74
+ return new Map(Object.entries(value));
75
+ }
76
+ }
77
+
78
+ return value;
79
+ }
80
+ ) as Manifest;
81
+
82
+ manifest.capabilities?.forEach((capabilityList, capabilityName) => {
83
+ if (
84
+ filters?.capabilityFilter &&
85
+ capabilityName.match(filters?.capabilityFilter)
86
+ )
87
+ return;
88
+ capabilityList.forEach((extensionName) => {
89
+ if (!filters?.typeFilter || !extensionName.match(filters?.typeFilter)) {
90
+ const cType: NamedType<NodeType> = JSON.parse(
91
+ fs
92
+ .readFileSync(
93
+ path.join(inputPath, "xlr", `${extensionName}.json`)
94
+ )
95
+ .toString()
96
+ );
97
+ const effectiveType =
98
+ transformsToRun?.reduce(
99
+ (typeAccumulator: NamedType<NodeType>, transformFn) =>
100
+ transformFn(
101
+ typeAccumulator,
102
+ capabilityName
103
+ ) as NamedType<NodeType>,
104
+ cType
105
+ ) ?? cType;
106
+
107
+ this.registry.add(effectiveType, manifest.pluginName, capabilityName);
108
+ }
109
+ });
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Load definitions from a js/ts file in memory
115
+ *
116
+ * @param manifest - The imported XLR manifest module
117
+ * @param filters - Any filters to apply when loading the types (a positive match will omit)
118
+ * @param transforms - any transforms to apply to the types being loaded
119
+ */
120
+ public async loadDefinitionsFromModule(
121
+ manifest: TSManifest,
122
+ filters?: Omit<Filters, "pluginFilter">,
123
+ transforms?: Array<TransformFunction>
124
+ ) {
125
+ this.computedNodeCache.clear();
126
+
127
+ const transformsToRun = [
128
+ ...this.externalTransformFunctions.values(),
129
+ ...(transforms ?? []),
130
+ ];
131
+
132
+ Object.keys(manifest.capabilities)?.forEach((capabilityName) => {
133
+ if (
134
+ filters?.capabilityFilter &&
135
+ capabilityName.match(filters?.capabilityFilter)
136
+ )
137
+ return;
138
+ const capabilityList = manifest.capabilities[capabilityName];
139
+ capabilityList.forEach((extension) => {
140
+ if (
141
+ !filters?.typeFilter ||
142
+ !extension.name.match(filters?.typeFilter)
143
+ ) {
144
+ const effectiveType =
145
+ transformsToRun?.reduce(
146
+ (typeAccumulator: NamedType<NodeType>, transformFn) =>
147
+ transformFn(
148
+ typeAccumulator,
149
+ capabilityName
150
+ ) as NamedType<NodeType>,
151
+ extension
152
+ ) ?? extension;
153
+
154
+ this.registry.add(effectiveType, manifest.pluginName, capabilityName);
155
+ }
156
+ });
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Statically load transform function that should be applied to every XLR bundle that is imported
162
+ */
163
+ public addTransformFunction(name: string, fn: TransformFunction): void {
164
+ this.externalTransformFunctions.set(name, fn);
165
+ }
166
+
167
+ /**
168
+ * Remove any transform function loaded via the `addTransformFunction` method by name
169
+ */
170
+ public removeTransformFunction(name: string): void {
171
+ this.externalTransformFunctions.delete(name);
172
+ }
173
+
174
+ /**
175
+ * Returns a Type that has been previously loaded
176
+ *
177
+ * @param id - Type to retrieve
178
+ * @param options - `GetTypeOptions`
179
+ * @returns `NamedType<NodeType>` | `undefined`
180
+ */
181
+ public getType(
182
+ id: string,
183
+ options?: GetTypeOptions
184
+ ): NamedType<NodeType> | undefined {
185
+ let type = this.registry.get(id);
186
+ if (options?.getRawType === true || !type) {
187
+ return type;
188
+ }
189
+
190
+ if (this.computedNodeCache.has(id)) {
191
+ return JSON.parse(JSON.stringify(this.computedNodeCache.get(id))) as
192
+ | NamedType<NodeType>
193
+ | undefined;
194
+ }
195
+
196
+ type = this.resolveType(type, options?.optimize)
197
+
198
+ this.computedNodeCache.set(id, type);
199
+
200
+ return type;
201
+ }
202
+
203
+ /**
204
+ * Returns if a Type with `id` has been loaded into the DSK
205
+ *
206
+ * @param id - Type to retrieve
207
+ * @returns `boolean`
208
+ */
209
+ public hasType(id: string) {
210
+ return this.registry.has(id);
211
+ }
212
+
213
+ /**
214
+ * Lists types that have been loaded into the SDK
215
+ *
216
+ * @param filters - Any filters to apply to the types returned (a positive match will omit)
217
+ * @returns `Array<NamedTypes>`
218
+ */
219
+ public listTypes(filters?: Filters) {
220
+ return this.registry.list(filters);
221
+ }
222
+
223
+ /**
224
+ * Returns meta information around a registered type
225
+ *
226
+ * @param id - Name of Type to retrieve
227
+ * @returns `TypeMetaData` | `undefined`
228
+ */
229
+ public getTypeInfo(id: string) {
230
+ return this.registry.info(id);
231
+ }
232
+
233
+ /**
234
+ * Validates if a JSONC Node follows the XLR Type registered under the `typeName` specified
235
+ *
236
+ * @param typeName - Registered XLR Type to use for validation
237
+ * @param rootNode - Node to validate
238
+ * @returns `Array<ValidationErrors>`
239
+ */
240
+ public validateByName(typeName: string, rootNode: Node) {
241
+ const xlr = this.getType(typeName);
242
+ if (!xlr) {
243
+ throw new Error(
244
+ `Type ${typeName} does not exist in registry, can't validate`
245
+ );
246
+ }
247
+
248
+ return this.validator.validateType(rootNode, xlr);
249
+ }
250
+
251
+ /**
252
+ * Validates if a JSONC Node follows the supplied XLR Type
253
+ *
254
+ * @param type - Type to validate against
255
+ * @param rootNode - Node to validate
256
+ * @returns `Array<ValidationErrors>`
257
+ */
258
+ public validateByType(type: NodeType, rootNode: Node) {
259
+ return this.validator.validateType(rootNode, type);
260
+ }
261
+
262
+ /**
263
+ * Transforms a generated XLR node into its final representation by resolving all `extends` properties.
264
+ * If `optimize` is set to true the following operations are also performed:
265
+ * - Solving any conditional types
266
+ * - Computing the effective types of any union elements
267
+ * - Resolving any ref nodes
268
+ * - filing in any remaining generics with their default value
269
+ */
270
+ private resolveType(type: NamedType, optimize = true): NamedType {
271
+ const resolvedObject = fillInGenerics(type, new Map(), true);
272
+
273
+ let transformMap: TransformFunctionMap = {
274
+ object: [(objectNode: ObjectType) => {
275
+ if (objectNode.extends) {
276
+ const refName = objectNode.extends.ref.split("<")[0];
277
+ let extendedType = this.getType(refName, {getRawType: true});
278
+ if (!extendedType) {
279
+ throw new Error(
280
+ `Error resolving ${objectNode.name}: can't find extended type ${refName}`
281
+ );
282
+ }
283
+
284
+ extendedType = resolveReferenceNode(
285
+ objectNode.extends,
286
+ extendedType as NamedType<ObjectType>
287
+ ) as NamedType;
288
+ if (extendedType.type === "object") {
289
+ return {
290
+ ...computeEffectiveObject(
291
+ extendedType as ObjectType,
292
+ objectNode as ObjectType,
293
+ false
294
+ ),
295
+ name: objectNode.name,
296
+ description: objectNode.description,
297
+ };
298
+ }
299
+
300
+ if( extendedType.type === "or"){
301
+ return {
302
+ ...this.validator.computeIntersectionType([
303
+ objectNode,
304
+ extendedType
305
+ ]
306
+ ),
307
+ name: objectNode.name,
308
+ description: objectNode.description,
309
+ } as any;
310
+ }
311
+
312
+ // if the merge isn't straightforward, defer until validation time for now
313
+ return {
314
+ name: objectNode.name,
315
+ type: "and",
316
+ and: [
317
+ {
318
+ ...objectNode,
319
+ extends: undefined,
320
+ },
321
+ extendedType,
322
+ ],
323
+ } as unknown as ObjectNode;
324
+ }
325
+
326
+ return objectNode;
327
+ }],
328
+ }
329
+
330
+ if(optimize){
331
+ transformMap = {
332
+ ...transformMap,
333
+ conditional: [(node) => {
334
+ return resolveConditional(node) as any
335
+ }],
336
+ and: [(node) => {
337
+ return {
338
+ ...this.validator.computeIntersectionType(node.and),
339
+ ...(node.name ? { name: node.name } : {}),
340
+ } as any
341
+ }],
342
+ ref: [(refNode) => {
343
+ return this.validator.getRefType(refNode) as any
344
+ }]
345
+ }
346
+ }
347
+
348
+ return xlrTransformWalker(transformMap)(resolvedObject) as NamedType
349
+ }
350
+
351
+ }
package/src/types.ts ADDED
@@ -0,0 +1,45 @@
1
+ import type { Node } from "jsonc-parser";
2
+
3
+ export interface BaseValidationMessage<ErrorType extends string = string> {
4
+ /** Validation Type */
5
+ type: ErrorType;
6
+
7
+ /** Error message text */
8
+ message: string;
9
+
10
+ /** JSONC node that the error originates from */
11
+ node: Node;
12
+
13
+ /** Level of the message */
14
+ severity: ValidationSeverity;
15
+ }
16
+
17
+ export interface TypeValidationError extends BaseValidationMessage<"type"> {
18
+ /** Expected types */
19
+ expected?: string[] | string | number | boolean;
20
+ }
21
+
22
+ export type MissingValidationError = BaseValidationMessage<"missing">;
23
+
24
+ export type UnknownValidationError = BaseValidationMessage<"unknown">;
25
+
26
+ export interface ValueValidationError extends BaseValidationMessage<"value"> {
27
+ /** Expected value */
28
+ expected?: string;
29
+ }
30
+
31
+ export type UnexpectedValidationError = BaseValidationMessage<"unexpected">;
32
+
33
+ export type ValidationMessage =
34
+ | TypeValidationError
35
+ | MissingValidationError
36
+ | UnknownValidationError
37
+ | ValueValidationError
38
+ | UnexpectedValidationError;
39
+
40
+ export enum ValidationSeverity {
41
+ Error = 1,
42
+ Warning = 2,
43
+ Info = 3,
44
+ Trace = 4,
45
+ }
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
+ }