@xlr-lib/xlr-utils 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.
@@ -0,0 +1,130 @@
1
+ import type {
2
+ NamedType,
3
+ NamedTypeWithGenerics,
4
+ NodeType,
5
+ NodeTypeWithGenerics,
6
+ PrimitiveTypes,
7
+ StringType,
8
+ NumberType,
9
+ BooleanType,
10
+ ObjectType,
11
+ ArrayType,
12
+ RefType,
13
+ OrType,
14
+ AndType,
15
+ RecordType,
16
+ } from "@xlr-lib/xlr";
17
+
18
+ /**
19
+ * Returns if the NodeType has generic tokens
20
+ */
21
+ export function isGenericNodeType<T extends NodeType = NodeType>(
22
+ nt: NodeType,
23
+ ): nt is NodeTypeWithGenerics<T> {
24
+ return (nt as NodeTypeWithGenerics).genericTokens?.length > 0;
25
+ }
26
+
27
+ /**
28
+ * Returns if the named type has generic tokens
29
+ */
30
+ export function isGenericNamedType<T extends NamedType = NamedType>(
31
+ nt: NodeType,
32
+ ): nt is NamedTypeWithGenerics<T> {
33
+ return (nt as NamedTypeWithGenerics).genericTokens?.length > 0;
34
+ }
35
+
36
+ /**
37
+ * Returns if the node is a `PrimitiveTypes`
38
+ */
39
+ export function isPrimitiveTypeNode(node: NodeType): node is PrimitiveTypes {
40
+ return (
41
+ node.type === "string" ||
42
+ node.type === "number" ||
43
+ node.type === "boolean" ||
44
+ node.type === "null" ||
45
+ node.type === "any" ||
46
+ node.type === "never" ||
47
+ node.type === "undefined" ||
48
+ node.type === "unknown" ||
49
+ node.type === "void"
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Type Guard for non-null values
55
+ */
56
+ export function isNonNullable<T>(a: T | null | undefined): a is NonNullable<T> {
57
+ return a !== null || a !== undefined;
58
+ }
59
+
60
+ /**
61
+ * Type guard for string type nodes
62
+ */
63
+ export function isStringType(node: NodeType): node is StringType {
64
+ return node.type === "string";
65
+ }
66
+
67
+ /**
68
+ * Type guard for number type nodes
69
+ */
70
+ export function isNumberType(node: NodeType): node is NumberType {
71
+ return node.type === "number";
72
+ }
73
+
74
+ /**
75
+ * Type guard for boolean type nodes
76
+ */
77
+ export function isBooleanType(node: NodeType): node is BooleanType {
78
+ return node.type === "boolean";
79
+ }
80
+
81
+ /**
82
+ * Type guard for object type nodes
83
+ */
84
+ export function isObjectType(node: NodeType): node is ObjectType {
85
+ return node.type === "object";
86
+ }
87
+
88
+ /**
89
+ * Type guard for array type nodes
90
+ */
91
+ export function isArrayType(node: NodeType): node is ArrayType {
92
+ return node.type === "array";
93
+ }
94
+
95
+ /**
96
+ * Type guard for ref type nodes
97
+ */
98
+ export function isRefType(node: NodeType): node is RefType {
99
+ return node.type === "ref";
100
+ }
101
+
102
+ /**
103
+ * Type guard for or (union) type nodes
104
+ */
105
+ export function isOrType(node: NodeType): node is OrType {
106
+ return node.type === "or";
107
+ }
108
+
109
+ /**
110
+ * Type guard for and (intersection) type nodes
111
+ */
112
+ export function isAndType(node: NodeType): node is AndType {
113
+ return node.type === "and";
114
+ }
115
+
116
+ /**
117
+ * Type guard for record type nodes
118
+ */
119
+ export function isRecordType(node: NodeType): node is RecordType {
120
+ return node.type === "record";
121
+ }
122
+
123
+ /**
124
+ * Type guard for named types (have name and source)
125
+ */
126
+ export function isNamedType<T extends NodeType = NodeType>(
127
+ node: NodeType,
128
+ ): node is NamedType<T> {
129
+ return "name" in node && "source" in node && typeof node.name === "string";
130
+ }
@@ -0,0 +1,246 @@
1
+ import type { Node } from "jsonc-parser";
2
+ import type {
3
+ ConditionalType,
4
+ NodeType,
5
+ ObjectType,
6
+ RefNode,
7
+ } from "@xlr-lib/xlr";
8
+ import { isGenericNodeType, isPrimitiveTypeNode } from "./type-checks";
9
+ import { fillInGenerics } from "./xlr-helpers";
10
+
11
+ export interface PropertyNode {
12
+ /** Equivalent Property Name */
13
+ key: string;
14
+
15
+ /** Equivalent Property Value */
16
+ value: Node;
17
+ }
18
+
19
+ /**
20
+ * Takes a property node and returns the underlying Key/Value Pairs
21
+ */
22
+ export function propertyToTuple(node: Node): PropertyNode {
23
+ return {
24
+ key: node.children?.[0].value as string,
25
+ value: node.children?.[1] as Node,
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Turns a node's children into a map for easy access
31
+ */
32
+ export function makePropertyMap(node: Node): Map<string, Node> {
33
+ const m = new Map();
34
+ node.children?.forEach((child) => {
35
+ const property = propertyToTuple(child);
36
+ m.set(property.key, property.value);
37
+ });
38
+ return m;
39
+ }
40
+
41
+ /**
42
+ * Checks if property is a leaf node or another node
43
+ */
44
+ export function isNode(obj: Node | string | number | boolean): obj is Node {
45
+ return (
46
+ typeof obj !== "string" ||
47
+ typeof obj !== "number" ||
48
+ typeof obj !== "boolean"
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Computes if the first arg extends the second arg
54
+ */
55
+ export function computeExtends(a: NodeType, b: NodeType): boolean {
56
+ // special case for any/unknown being functionally the same
57
+ if (
58
+ (a.type === "any" || a.type === "unknown") &&
59
+ (b.type === "any" || b.type === "unknown")
60
+ ) {
61
+ return true;
62
+ }
63
+
64
+ // special case for null/undefined being functionally the same
65
+ if (
66
+ (a.type === "null" || a.type === "undefined") &&
67
+ (b.type === "null" || b.type === "undefined")
68
+ ) {
69
+ return true;
70
+ }
71
+
72
+ // check simple case of equal types
73
+ if (a.type === b.type) {
74
+ if (isPrimitiveTypeNode(a) && isPrimitiveTypeNode(b)) {
75
+ if (a.const && b.const) {
76
+ if (a.const === b.const) {
77
+ return true;
78
+ }
79
+ } else {
80
+ return true;
81
+ }
82
+ }
83
+
84
+ if (a.type === "ref" && b.type === "ref") {
85
+ return a.ref === b.ref;
86
+ }
87
+
88
+ if (a.type === "object" && b.type === "object") {
89
+ for (const property in b.properties) {
90
+ const propertyNode = b.properties[property];
91
+ if (
92
+ !a.properties[property] ||
93
+ !computeExtends(a.properties[property].node, propertyNode.node)
94
+ ) {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ return true;
100
+ }
101
+ }
102
+
103
+ if (isPrimitiveTypeNode(a) && b.type === "or") {
104
+ return b.or.every((member) => computeExtends(a, member));
105
+ }
106
+
107
+ if (isPrimitiveTypeNode(b) && a.type === "or") {
108
+ return a.or.every((member) => computeExtends(b, member));
109
+ }
110
+
111
+ if (a.type === "or" && b.type === "or") {
112
+ return a.or.every((x) => b.or.some((y) => computeExtends(x, y)));
113
+ }
114
+
115
+ return false;
116
+ }
117
+
118
+ /**
119
+ * Attempts to resolve a conditional type
120
+ */
121
+ export function resolveConditional(conditional: ConditionalType): NodeType {
122
+ const { left, right } = conditional.check;
123
+ const conditionalResult = computeExtends(left, right)
124
+ ? conditional.value.true
125
+ : conditional.value.false;
126
+
127
+ // Compose first level generics here since `conditionalResult` won't have them
128
+ if (isGenericNodeType(conditional)) {
129
+ const genericMap: Map<string, NodeType> = new Map();
130
+ conditional.genericTokens.forEach((token) => {
131
+ genericMap.set(
132
+ token.symbol,
133
+ token.default ?? token.constraints ?? { type: "any" },
134
+ );
135
+ });
136
+
137
+ return fillInGenerics(conditionalResult, genericMap);
138
+ }
139
+
140
+ return conditionalResult;
141
+ }
142
+
143
+ /**
144
+ * Resolve referenced node with potential generic arguments
145
+ */
146
+ export function resolveReferenceNode(
147
+ genericReference: RefNode,
148
+ typeToFill: NodeType,
149
+ ): NodeType {
150
+ const genericArgs = genericReference.genericArguments;
151
+ const genericMap: Map<string, NodeType> = new Map();
152
+
153
+ // Compose first level generics here from `genericReference`
154
+ if (genericArgs && isGenericNodeType(typeToFill)) {
155
+ typeToFill.genericTokens.forEach((token, index) => {
156
+ genericMap.set(
157
+ token.symbol,
158
+ genericArgs[index] ?? token.default ?? token.constraints,
159
+ );
160
+ });
161
+ }
162
+
163
+ // Fill in generics
164
+ const filledInNode = fillInGenerics(typeToFill, genericMap);
165
+
166
+ // Remove generic tokens that were resolve
167
+ if (isGenericNodeType(filledInNode) && genericArgs?.length) {
168
+ if (genericArgs.length < filledInNode.genericTokens.length) {
169
+ filledInNode.genericTokens = filledInNode.genericTokens.slice(
170
+ genericArgs?.length,
171
+ );
172
+ } else if (genericArgs.length === filledInNode.genericTokens.length) {
173
+ filledInNode.genericTokens = [];
174
+ }
175
+ }
176
+
177
+ // Resolve index access
178
+ if (genericReference.property && filledInNode.type === "object") {
179
+ return (
180
+ filledInNode.properties[genericReference.property]?.node ??
181
+ filledInNode.additionalProperties ?? { type: "undefined" }
182
+ );
183
+ }
184
+
185
+ return filledInNode;
186
+ }
187
+
188
+ /**
189
+ * Combines two ObjectType objects to get a representation of the effective TypeScript interface of `base` extending `operand`
190
+ - * @param base The base interface
191
+ - * @param operand The interface that is extended
192
+ - * @param errorOnOverlap whether or not conflicting properties should throw an error or use the property from operand
193
+ - * @returns `ObjectType`
194
+ */
195
+ export function computeEffectiveObject(
196
+ base: ObjectType,
197
+ operand: ObjectType,
198
+ errorOnOverlap = true,
199
+ ): ObjectType {
200
+ const baseObjectName = base.name ?? base.title ?? "object literal";
201
+ const operandObjectName = operand.name ?? operand.title ?? "object literal";
202
+ const newObject = {
203
+ ...JSON.parse(JSON.stringify(base)),
204
+ name: `${baseObjectName} & ${operandObjectName}`,
205
+ description: `Effective type combining ${baseObjectName} and ${operandObjectName}`,
206
+ genericTokens: [
207
+ ...(isGenericNodeType(base) ? base.genericTokens : []),
208
+ ...(isGenericNodeType(operand) ? operand.genericTokens : []),
209
+ ],
210
+ };
211
+ // TODO this check needs to account for primitive -> primitive generic overlap
212
+
213
+ for (const property in operand.properties) {
214
+ if (newObject.properties[property] !== undefined && errorOnOverlap) {
215
+ if (
216
+ !computeExtends(
217
+ newObject.properties[property].node,
218
+ operand.properties[property].node,
219
+ )
220
+ ) {
221
+ throw new Error(
222
+ `Can't compute effective type for ${baseObjectName} and ${operandObjectName} because of conflicting properties ${property}`,
223
+ );
224
+ }
225
+ }
226
+
227
+ newObject.properties[property] = operand.properties[property];
228
+ }
229
+
230
+ if (newObject.additionalProperties && operand.additionalProperties) {
231
+ if (
232
+ !isPrimitiveTypeNode(newObject.additionalProperties) ||
233
+ !isPrimitiveTypeNode(operand.additionalProperties) ||
234
+ newObject.additionalProperties.type !== operand.additionalProperties.type
235
+ ) {
236
+ newObject.additionalProperties = {
237
+ type: "and",
238
+ and: [newObject.additionalProperties, operand.additionalProperties],
239
+ };
240
+ }
241
+ } else if (operand.additionalProperties) {
242
+ newObject.additionalProperties = operand.additionalProperties;
243
+ }
244
+
245
+ return newObject;
246
+ }
@@ -0,0 +1,340 @@
1
+ import type {
2
+ NamedType,
3
+ NodeType,
4
+ ObjectProperty,
5
+ ObjectType,
6
+ OrType,
7
+ RefNode,
8
+ } from "@xlr-lib/xlr";
9
+ import { computeExtends, resolveConditional } from "./validation-helpers";
10
+ import { isGenericNamedType, isGenericNodeType } from "./type-checks";
11
+
12
+ /**
13
+ * Walks generics to fill in values from a combination of the default, constraint, and passed in map values
14
+ * TODO convert this to use simpleTransformGenerator
15
+ */
16
+ export function fillInGenerics(
17
+ xlrNode: NodeType,
18
+ generics?: Map<string, NodeType>,
19
+ preferLocalGenerics = false,
20
+ ): NodeType {
21
+ // Need to make sure not to set generics in passed in map to avoid using generics outside of tree
22
+ let localGenerics: Map<string, NodeType>;
23
+
24
+ if (generics) {
25
+ localGenerics = new Map(generics);
26
+ if (preferLocalGenerics && isGenericNodeType(xlrNode)) {
27
+ xlrNode.genericTokens?.forEach((token) => {
28
+ const genericValue = (token.default ?? token.constraints) as NodeType;
29
+ localGenerics.set(
30
+ token.symbol,
31
+ fillInGenerics(genericValue, localGenerics, preferLocalGenerics),
32
+ );
33
+ });
34
+ }
35
+ } else {
36
+ localGenerics = new Map();
37
+ if (isGenericNodeType(xlrNode)) {
38
+ xlrNode.genericTokens?.forEach((token) => {
39
+ const genericValue = (token.default ?? token.constraints) as NodeType;
40
+ localGenerics.set(
41
+ token.symbol,
42
+ fillInGenerics(genericValue, localGenerics, preferLocalGenerics),
43
+ );
44
+ });
45
+ }
46
+ }
47
+
48
+ if (xlrNode.type === "ref") {
49
+ if (localGenerics.has(xlrNode.ref)) {
50
+ return {
51
+ ...(localGenerics.get(xlrNode.ref) as NodeType),
52
+ ...(xlrNode.genericArguments
53
+ ? {
54
+ genericArguments: xlrNode.genericArguments.map((ga) =>
55
+ fillInGenerics(ga, localGenerics, preferLocalGenerics),
56
+ ),
57
+ }
58
+ : {}),
59
+ ...(xlrNode.title ? { title: xlrNode.title } : {}),
60
+ ...(xlrNode.name ? { name: xlrNode.name } : {}),
61
+ ...(xlrNode.description ? { description: xlrNode.description } : {}),
62
+ ...(xlrNode.comment ? { comment: xlrNode.comment } : {}),
63
+ };
64
+ }
65
+
66
+ return {
67
+ ...xlrNode,
68
+ ...(xlrNode.genericArguments
69
+ ? {
70
+ genericArguments: xlrNode.genericArguments.map((ga) =>
71
+ fillInGenerics(ga, localGenerics, preferLocalGenerics),
72
+ ),
73
+ }
74
+ : {}),
75
+ };
76
+ }
77
+
78
+ if (xlrNode.type === "object") {
79
+ const newProperties: { [name: string]: ObjectProperty } = {};
80
+ Object.getOwnPropertyNames(xlrNode.properties).forEach((propName) => {
81
+ const prop = xlrNode.properties[propName];
82
+ newProperties[propName] = {
83
+ required: prop.required,
84
+ node: fillInGenerics(prop.node, localGenerics, preferLocalGenerics),
85
+ };
86
+ });
87
+
88
+ return {
89
+ ...xlrNode,
90
+ properties: newProperties,
91
+ ...(isGenericNamedType(xlrNode)
92
+ ? {
93
+ genericTokens: xlrNode.genericTokens.map((token) => {
94
+ return {
95
+ ...token,
96
+ constraints: token.constraints
97
+ ? fillInGenerics(
98
+ token.constraints,
99
+ localGenerics,
100
+ preferLocalGenerics,
101
+ )
102
+ : undefined,
103
+ default: token.default
104
+ ? fillInGenerics(
105
+ token.default,
106
+ localGenerics,
107
+ preferLocalGenerics,
108
+ )
109
+ : undefined,
110
+ };
111
+ }),
112
+ }
113
+ : {}),
114
+ extends: xlrNode.extends
115
+ ? (fillInGenerics(
116
+ xlrNode.extends,
117
+ localGenerics,
118
+ preferLocalGenerics,
119
+ ) as RefNode)
120
+ : undefined,
121
+ additionalProperties: xlrNode.additionalProperties
122
+ ? fillInGenerics(
123
+ xlrNode.additionalProperties,
124
+ localGenerics,
125
+ preferLocalGenerics,
126
+ )
127
+ : false,
128
+ };
129
+ }
130
+
131
+ if (xlrNode.type === "array") {
132
+ return {
133
+ ...xlrNode,
134
+ elementType: fillInGenerics(
135
+ xlrNode.elementType,
136
+ localGenerics,
137
+ preferLocalGenerics,
138
+ ),
139
+ };
140
+ } else if (xlrNode.type === "or" || xlrNode.type === "and") {
141
+ let pointer;
142
+ if (xlrNode.type === "or") {
143
+ pointer = xlrNode.or;
144
+ } else {
145
+ pointer = xlrNode.and;
146
+ }
147
+
148
+ return {
149
+ ...xlrNode,
150
+ [xlrNode.type]: pointer.map((prop) => {
151
+ return fillInGenerics(prop, localGenerics, preferLocalGenerics);
152
+ }),
153
+ };
154
+ } else if (xlrNode.type === "record") {
155
+ return {
156
+ ...xlrNode,
157
+ keyType: fillInGenerics(
158
+ xlrNode.keyType,
159
+ localGenerics,
160
+ preferLocalGenerics,
161
+ ),
162
+ valueType: fillInGenerics(
163
+ xlrNode.valueType,
164
+ localGenerics,
165
+ preferLocalGenerics,
166
+ ),
167
+ };
168
+ } else if (xlrNode.type === "conditional") {
169
+ const filledInConditional = {
170
+ ...xlrNode,
171
+ check: {
172
+ left: fillInGenerics(
173
+ xlrNode.check.left,
174
+ localGenerics,
175
+ preferLocalGenerics,
176
+ ),
177
+ right: fillInGenerics(
178
+ xlrNode.check.right,
179
+ localGenerics,
180
+ preferLocalGenerics,
181
+ ),
182
+ },
183
+ value: {
184
+ true: fillInGenerics(
185
+ xlrNode.value.true,
186
+ localGenerics,
187
+ preferLocalGenerics,
188
+ ),
189
+ false: fillInGenerics(
190
+ xlrNode.value.false,
191
+ localGenerics,
192
+ preferLocalGenerics,
193
+ ),
194
+ },
195
+ };
196
+
197
+ // Check to see if we have enough information to resolve this conditional
198
+ if (
199
+ filledInConditional.check.left.type !== "ref" &&
200
+ filledInConditional.check.right.type !== "ref"
201
+ ) {
202
+ return {
203
+ name: xlrNode.name,
204
+ title: xlrNode.title,
205
+ ...resolveConditional(filledInConditional),
206
+ } as NamedType;
207
+ }
208
+
209
+ return filledInConditional;
210
+ }
211
+
212
+ return xlrNode;
213
+ }
214
+
215
+ /** Applies the TS `Pick` or `Omit` type to an interface/union/intersection */
216
+ export function applyPickOrOmitToNodeType(
217
+ baseObject: NodeType,
218
+ operation: "Pick" | "Omit",
219
+ properties: Set<string>,
220
+ ): NodeType | undefined {
221
+ if (baseObject.type === "object") {
222
+ const newObject = { ...baseObject };
223
+ Object.keys(baseObject.properties).forEach((key) => {
224
+ if (
225
+ (operation === "Omit" && properties.has(key)) ||
226
+ (operation === "Pick" && !properties.has(key))
227
+ ) {
228
+ delete newObject.properties[key];
229
+ }
230
+ });
231
+
232
+ /**
233
+ * Filter out objects in cases:
234
+ * - A Pick operation and there are no properties left
235
+ * - An Omit operation and there are no properties left and no additional properties allowed
236
+ */
237
+ if (
238
+ Object.keys(newObject.properties).length === 0 &&
239
+ (operation !== "Omit" || newObject.additionalProperties === false)
240
+ ) {
241
+ return undefined;
242
+ }
243
+
244
+ return newObject;
245
+ }
246
+
247
+ let pointer;
248
+ if (baseObject.type === "and") {
249
+ pointer = baseObject.and;
250
+ } else if (baseObject.type === "or") {
251
+ pointer = baseObject.or;
252
+ } else {
253
+ throw new Error(
254
+ `Error: Can not apply ${operation} to type ${baseObject.type}`,
255
+ );
256
+ }
257
+
258
+ const pickedTypes = pointer
259
+ .map((type) => {
260
+ const node = applyPickOrOmitToNodeType(type, operation, properties);
261
+ if (node === undefined) {
262
+ return undefined;
263
+ }
264
+
265
+ return { ...node, additionalProperties: false } as ObjectType;
266
+ })
267
+ .filter((type) => type !== undefined) as NodeType[];
268
+
269
+ if (pickedTypes.length === 0) {
270
+ return undefined;
271
+ }
272
+
273
+ if (pickedTypes.length === 1) {
274
+ return pickedTypes[0];
275
+ }
276
+
277
+ if (baseObject.type === "and") {
278
+ return { ...baseObject, and: pickedTypes };
279
+ }
280
+
281
+ return { ...baseObject, or: pickedTypes };
282
+ }
283
+
284
+ /** Applies the TS `Partial` or `Required` type to an interface/union/intersection */
285
+ export function applyPartialOrRequiredToNodeType(
286
+ baseObject: NodeType,
287
+ modifier: boolean,
288
+ ): NodeType {
289
+ if (baseObject.type === "object") {
290
+ const newObject = { ...baseObject };
291
+ Object.keys(baseObject.properties).forEach((key) => {
292
+ newObject.properties[key].required = modifier;
293
+ });
294
+
295
+ return newObject;
296
+ }
297
+
298
+ if (baseObject.type === "and") {
299
+ const pickedTypes = baseObject.and.map((type) =>
300
+ applyPartialOrRequiredToNodeType(type, modifier),
301
+ );
302
+ return { ...baseObject, and: pickedTypes };
303
+ }
304
+
305
+ if (baseObject.type === "or") {
306
+ const pickedTypes = baseObject.or.map((type) =>
307
+ applyPartialOrRequiredToNodeType(type, modifier),
308
+ );
309
+ return { ...baseObject, or: pickedTypes };
310
+ }
311
+
312
+ throw new Error(
313
+ `Error: Can not apply ${modifier ? "Required" : "Partial"} to type ${
314
+ baseObject.type
315
+ }`,
316
+ );
317
+ }
318
+
319
+ /** Applies the TS `Exclude` type to a union */
320
+ export function applyExcludeToNodeType(
321
+ baseObject: OrType,
322
+ filters: NodeType | OrType,
323
+ ): NodeType {
324
+ const remainingMembers = baseObject.or.filter((type) => {
325
+ if (filters.type === "or") {
326
+ return !filters.or.some((filter) => computeExtends(type, filter));
327
+ }
328
+
329
+ return !computeExtends(type, filters);
330
+ });
331
+
332
+ if (remainingMembers.length === 1) {
333
+ return remainingMembers[0];
334
+ }
335
+
336
+ return {
337
+ ...baseObject,
338
+ or: remainingMembers,
339
+ };
340
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./xlr-helpers";
2
+ export * from "./type-checks";
3
+ export * from "./validation-helpers";
4
+ //# sourceMappingURL=index.d.ts.map