@player-tools/xlr-sdk 0.0.2-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/README.md +10 -0
- package/dist/index.cjs.js +509 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.esm.js +480 -0
- package/package.json +40 -0
- package/src/index.ts +3 -0
- package/src/registry/basic-registry.ts +83 -0
- package/src/registry/index.ts +2 -0
- package/src/registry/types.ts +28 -0
- package/src/sdk.ts +229 -0
- package/src/types.ts +15 -0
- package/src/validator.ts +398 -0
package/src/sdk.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Manifest,
|
|
3
|
+
NamedType,
|
|
4
|
+
NodeType,
|
|
5
|
+
TransformFunction,
|
|
6
|
+
TSManifest,
|
|
7
|
+
} from '@player-tools/xlr';
|
|
8
|
+
import type { TopLevelDeclaration } from '@player-tools/xlr-utils';
|
|
9
|
+
import { fillInGenerics } from '@player-tools/xlr-utils';
|
|
10
|
+
import type { Node } from 'jsonc-parser';
|
|
11
|
+
import { TSWriter } from '@player-tools/xlr-converters';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import ts from 'typescript';
|
|
15
|
+
|
|
16
|
+
import type { XLRRegistry, Filters } from './registry';
|
|
17
|
+
import { BasicXLRRegistry } from './registry';
|
|
18
|
+
import type { ExportTypes } from './types';
|
|
19
|
+
import { XLRValidator } from './validator';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Abstraction for interfacing with XLRs making it more approachable to use without understanding the inner workings of the types and how they are packaged
|
|
23
|
+
*/
|
|
24
|
+
export class XLRSDK {
|
|
25
|
+
private registry: XLRRegistry;
|
|
26
|
+
private validator: XLRValidator;
|
|
27
|
+
private tsWriter: TSWriter;
|
|
28
|
+
|
|
29
|
+
constructor(customRegistry?: XLRRegistry) {
|
|
30
|
+
this.registry = customRegistry ?? new BasicXLRRegistry();
|
|
31
|
+
this.validator = new XLRValidator(this.registry);
|
|
32
|
+
this.tsWriter = new TSWriter();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public loadDefinitionsFromDisk(
|
|
36
|
+
inputPath: string,
|
|
37
|
+
filters?: Omit<Filters, 'pluginFilter'>,
|
|
38
|
+
transforms?: Array<TransformFunction>
|
|
39
|
+
) {
|
|
40
|
+
const manifest = JSON.parse(
|
|
41
|
+
fs.readFileSync(path.join(inputPath, 'xlr', 'manifest.json')).toString(),
|
|
42
|
+
(key: unknown, value: unknown) => {
|
|
43
|
+
// Custom parser because JSON objects -> JS Objects, not maps
|
|
44
|
+
if (typeof value === 'object' && value !== null) {
|
|
45
|
+
if (key === 'capabilities') {
|
|
46
|
+
return new Map(Object.entries(value));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
) as Manifest;
|
|
53
|
+
|
|
54
|
+
manifest.capabilities?.forEach((capabilityList, capabilityName) => {
|
|
55
|
+
if (
|
|
56
|
+
filters?.capabilityFilter &&
|
|
57
|
+
capabilityName.match(filters?.capabilityFilter)
|
|
58
|
+
)
|
|
59
|
+
return;
|
|
60
|
+
capabilityList.forEach((extensionName) => {
|
|
61
|
+
if (!filters?.typeFilter || !extensionName.match(filters?.typeFilter)) {
|
|
62
|
+
const cType: NamedType<NodeType> = JSON.parse(
|
|
63
|
+
fs
|
|
64
|
+
.readFileSync(
|
|
65
|
+
path.join(inputPath, 'xlr', `${extensionName}.json`)
|
|
66
|
+
)
|
|
67
|
+
.toString()
|
|
68
|
+
);
|
|
69
|
+
transforms?.forEach((transform) => transform(cType, capabilityName));
|
|
70
|
+
const resolvedType = fillInGenerics(cType) as NamedType<NodeType>;
|
|
71
|
+
this.registry.add(resolvedType, manifest.pluginName, capabilityName);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async loadDefinitionsFromModule(
|
|
78
|
+
inputPath: string,
|
|
79
|
+
filters?: Omit<Filters, 'pluginFilter'>,
|
|
80
|
+
transforms?: Array<TransformFunction>
|
|
81
|
+
) {
|
|
82
|
+
const importManifest = await import(
|
|
83
|
+
path.join(inputPath, 'xlr', 'manifest.js')
|
|
84
|
+
);
|
|
85
|
+
const manifest = importManifest.default as TSManifest;
|
|
86
|
+
|
|
87
|
+
Object.keys(manifest.capabilities)?.forEach((capabilityName) => {
|
|
88
|
+
if (
|
|
89
|
+
filters?.capabilityFilter &&
|
|
90
|
+
capabilityName.match(filters?.capabilityFilter)
|
|
91
|
+
)
|
|
92
|
+
return;
|
|
93
|
+
const capabilityList = manifest.capabilities[capabilityName];
|
|
94
|
+
capabilityList.forEach((extension) => {
|
|
95
|
+
if (
|
|
96
|
+
!filters?.typeFilter ||
|
|
97
|
+
!extension.name.match(filters?.typeFilter)
|
|
98
|
+
) {
|
|
99
|
+
transforms?.forEach((transform) =>
|
|
100
|
+
transform(extension, extension.name)
|
|
101
|
+
);
|
|
102
|
+
const resolvedType = fillInGenerics(extension) as NamedType<NodeType>;
|
|
103
|
+
this.registry.add(resolvedType, manifest.pluginName, extension.name);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public getType(id: string) {
|
|
110
|
+
return this.registry.get(id);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public hasType(id: string) {
|
|
114
|
+
return this.registry.has(id);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public listTypes(filters?: Filters) {
|
|
118
|
+
return this.registry.list(filters);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public validate(typeName: string, rootNode: Node) {
|
|
122
|
+
const xlr = this.registry.get(typeName);
|
|
123
|
+
if (!xlr) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Type ${typeName} does not exist in registry, can't validate`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return this.validator.validateType(rootNode, xlr);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Exports the types loaded into the registry to the specified format
|
|
134
|
+
*
|
|
135
|
+
* @param exportType - what format to export as
|
|
136
|
+
* @param importMap - a map of primitive packages to types exported from that package to add import statements
|
|
137
|
+
* @param filters - filter out plugins/capabilities/types you don't want to export
|
|
138
|
+
* @param transforms - transforms to apply to types before exporting them
|
|
139
|
+
* @returns [filename, content][] - Tuples of filenames and content to write
|
|
140
|
+
*/
|
|
141
|
+
public exportRegistry(
|
|
142
|
+
exportType: ExportTypes,
|
|
143
|
+
importMap: Map<string, string[]>,
|
|
144
|
+
filters?: Filters,
|
|
145
|
+
transforms?: Array<TransformFunction>
|
|
146
|
+
): [string, string][] {
|
|
147
|
+
const typesToExport = this.registry.list(filters).map((type) => {
|
|
148
|
+
transforms?.forEach((transformFunction) =>
|
|
149
|
+
transformFunction(
|
|
150
|
+
type,
|
|
151
|
+
this.registry.info(type.name)?.capability as string
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
return type;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (exportType === 'TypeScript') {
|
|
158
|
+
const outputString = this.exportToTypeScript(typesToExport, importMap);
|
|
159
|
+
return [['out.d.ts', outputString]];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
throw new Error(`Unknown export format ${exportType}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private exportToTypeScript(
|
|
166
|
+
typesToExport: NamedType[],
|
|
167
|
+
importMap: Map<string, string[]>
|
|
168
|
+
): string {
|
|
169
|
+
const referencedImports: Set<string> = new Set();
|
|
170
|
+
const exportedTypes: Map<string, TopLevelDeclaration> = new Map();
|
|
171
|
+
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
172
|
+
|
|
173
|
+
let resultFile = ts.createSourceFile(
|
|
174
|
+
'output.d.ts',
|
|
175
|
+
'',
|
|
176
|
+
ts.ScriptTarget.ES2017,
|
|
177
|
+
false, // setParentNodes
|
|
178
|
+
ts.ScriptKind.TS
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
typesToExport.forEach((typeNode) => {
|
|
182
|
+
const { type, referencedTypes, additionalTypes } =
|
|
183
|
+
this.tsWriter.convertNamedType(typeNode);
|
|
184
|
+
exportedTypes.set(typeNode.name, type);
|
|
185
|
+
additionalTypes?.forEach((additionalType, name) =>
|
|
186
|
+
exportedTypes.set(name, additionalType)
|
|
187
|
+
);
|
|
188
|
+
referencedTypes?.forEach((referencedType) =>
|
|
189
|
+
referencedImports.add(referencedType)
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const typesToPrint: Array<string> = [];
|
|
194
|
+
|
|
195
|
+
exportedTypes.forEach((type) =>
|
|
196
|
+
typesToPrint.push(
|
|
197
|
+
printer.printNode(ts.EmitHint.Unspecified, type, resultFile)
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
importMap.forEach((imports, packageName) => {
|
|
202
|
+
const applicableImports = imports.filter((i) => referencedImports.has(i));
|
|
203
|
+
resultFile = ts.factory.updateSourceFile(resultFile, [
|
|
204
|
+
ts.factory.createImportDeclaration(
|
|
205
|
+
/* decorators */ undefined,
|
|
206
|
+
/* modifiers */ undefined,
|
|
207
|
+
ts.factory.createImportClause(
|
|
208
|
+
false,
|
|
209
|
+
undefined,
|
|
210
|
+
ts.factory.createNamedImports(
|
|
211
|
+
applicableImports.map((i) =>
|
|
212
|
+
ts.factory.createImportSpecifier(
|
|
213
|
+
undefined,
|
|
214
|
+
ts.factory.createIdentifier(i)
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
),
|
|
219
|
+
ts.factory.createStringLiteral(packageName)
|
|
220
|
+
),
|
|
221
|
+
...resultFile.statements,
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const headerText = printer.printFile(resultFile);
|
|
226
|
+
const nodeText = typesToPrint.join('\n');
|
|
227
|
+
return `${headerText}\n${nodeText}`;
|
|
228
|
+
}
|
|
229
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Node } from 'jsonc-parser';
|
|
2
|
+
|
|
3
|
+
export interface ValidationError {
|
|
4
|
+
/** Error message text */
|
|
5
|
+
message: string;
|
|
6
|
+
|
|
7
|
+
/** JSONC node that the error originates from */
|
|
8
|
+
node: Node;
|
|
9
|
+
|
|
10
|
+
/** Rough categorization of the error type */
|
|
11
|
+
type: 'type' | 'missing' | 'unknown' | 'value' | 'unexpected';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Support Export Formats */
|
|
15
|
+
export type ExportTypes = 'TypeScript';
|
package/src/validator.ts
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import type { Node } from 'jsonc-parser';
|
|
2
|
+
import type {
|
|
3
|
+
ArrayType,
|
|
4
|
+
NamedType,
|
|
5
|
+
NodeType,
|
|
6
|
+
ObjectType,
|
|
7
|
+
OrType,
|
|
8
|
+
PrimitiveTypes,
|
|
9
|
+
RefType,
|
|
10
|
+
TemplateLiteralType,
|
|
11
|
+
} from '@player-tools/xlr';
|
|
12
|
+
import {
|
|
13
|
+
makePropertyMap,
|
|
14
|
+
resolveConditional,
|
|
15
|
+
isPrimitiveTypeNode,
|
|
16
|
+
fillInGenerics,
|
|
17
|
+
isGenericNodeType,
|
|
18
|
+
} from '@player-tools/xlr-utils';
|
|
19
|
+
import type { ValidationError } from './types';
|
|
20
|
+
import type { XLRRegistry } from './registry';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validator for XLRs on JSON Nodes
|
|
24
|
+
*/
|
|
25
|
+
export class XLRValidator {
|
|
26
|
+
private typeMap: XLRRegistry;
|
|
27
|
+
private regexCache: Map<string, RegExp>;
|
|
28
|
+
|
|
29
|
+
constructor(typeMap: XLRRegistry) {
|
|
30
|
+
this.typeMap = typeMap;
|
|
31
|
+
this.regexCache = new Map();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Main entrypoint for validation */
|
|
35
|
+
public validateType(
|
|
36
|
+
rootNode: Node,
|
|
37
|
+
xlrNode: NodeType
|
|
38
|
+
): Array<ValidationError> {
|
|
39
|
+
const validationIssues = new Array<ValidationError>();
|
|
40
|
+
if (xlrNode.type === 'object') {
|
|
41
|
+
if (rootNode.type === 'object') {
|
|
42
|
+
validationIssues.push(...this.validateObject(xlrNode, rootNode));
|
|
43
|
+
} else {
|
|
44
|
+
validationIssues.push({
|
|
45
|
+
type: 'type',
|
|
46
|
+
node: rootNode,
|
|
47
|
+
message: `Expected an object but got an '${rootNode.type}'`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
} else if (xlrNode.type === 'array') {
|
|
51
|
+
if (rootNode.type === 'array') {
|
|
52
|
+
validationIssues.push(...this.validateArray(rootNode, xlrNode));
|
|
53
|
+
} else {
|
|
54
|
+
validationIssues.push({
|
|
55
|
+
type: 'type',
|
|
56
|
+
node: rootNode,
|
|
57
|
+
message: `Expected an array but got an '${rootNode.type}'`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
} else if (xlrNode.type === 'template') {
|
|
61
|
+
this.validateTemplate(rootNode, xlrNode);
|
|
62
|
+
} else if (xlrNode.type === 'or') {
|
|
63
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
64
|
+
for (const potentialType of xlrNode.or) {
|
|
65
|
+
const potentialErrors = this.validateType(rootNode, potentialType);
|
|
66
|
+
if (potentialErrors.length === 0) {
|
|
67
|
+
return validationIssues;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
validationIssues.push({
|
|
72
|
+
type: 'value',
|
|
73
|
+
node: rootNode,
|
|
74
|
+
message: `Does not match any of the expected types for type: '${xlrNode.name}'`,
|
|
75
|
+
});
|
|
76
|
+
} else if (xlrNode.type === 'and') {
|
|
77
|
+
const effectiveType = this.computeIntersectionType(xlrNode.and);
|
|
78
|
+
validationIssues.push(...this.validateType(rootNode, effectiveType));
|
|
79
|
+
} else if (xlrNode.type === 'record') {
|
|
80
|
+
rootNode.children?.forEach((child) => {
|
|
81
|
+
validationIssues.push(
|
|
82
|
+
...this.validateType(child.children?.[0] as Node, xlrNode.keyType)
|
|
83
|
+
);
|
|
84
|
+
validationIssues.push(
|
|
85
|
+
...this.validateType(child.children?.[1] as Node, xlrNode.valueType)
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
} else if (xlrNode.type === 'ref') {
|
|
89
|
+
const refType = this.getRefType(xlrNode);
|
|
90
|
+
if (refType === undefined) {
|
|
91
|
+
validationIssues.push({
|
|
92
|
+
type: 'unknown',
|
|
93
|
+
node: rootNode,
|
|
94
|
+
message: `Type '${xlrNode.ref}' is not defined in provided bundles`,
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
validationIssues.push(
|
|
98
|
+
...this.validateType(rootNode, refType as NamedType)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
} else if (isPrimitiveTypeNode(xlrNode)) {
|
|
102
|
+
if (!this.validateLiteralType(xlrNode, rootNode)) {
|
|
103
|
+
if (
|
|
104
|
+
(xlrNode.type === 'string' ||
|
|
105
|
+
xlrNode.type === 'number' ||
|
|
106
|
+
xlrNode.type === 'boolean') &&
|
|
107
|
+
xlrNode.const
|
|
108
|
+
) {
|
|
109
|
+
validationIssues.push({
|
|
110
|
+
type: 'type',
|
|
111
|
+
node: rootNode.parent as Node,
|
|
112
|
+
message: `Expected '${xlrNode.const}' but got '${rootNode.value}'`,
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
validationIssues.push({
|
|
116
|
+
type: 'type',
|
|
117
|
+
node: rootNode.parent as Node,
|
|
118
|
+
message: `Expected type '${xlrNode.type}' but got '${rootNode.type}'`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else if (xlrNode.type === 'conditional') {
|
|
123
|
+
const resolvedType = resolveConditional(xlrNode);
|
|
124
|
+
if (resolvedType === xlrNode) {
|
|
125
|
+
throw Error(
|
|
126
|
+
`Unable to resolve conditional type at runtime: ${xlrNode.name}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
validationIssues.push(...this.validateType(rootNode, resolvedType));
|
|
131
|
+
} else {
|
|
132
|
+
throw Error(`Unknown type ${xlrNode.type}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return validationIssues;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private validateTemplate(
|
|
139
|
+
node: Node,
|
|
140
|
+
xlrNode: TemplateLiteralType
|
|
141
|
+
): ValidationError | undefined {
|
|
142
|
+
if (node.type !== 'string') {
|
|
143
|
+
return {
|
|
144
|
+
type: 'type',
|
|
145
|
+
node: node.parent as Node,
|
|
146
|
+
message: `Expected type '${xlrNode.type}' but got '${typeof node}'`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const regex = this.getRegex(xlrNode.format);
|
|
151
|
+
const valid = regex.exec(node.value);
|
|
152
|
+
if (!valid) {
|
|
153
|
+
return {
|
|
154
|
+
type: 'value',
|
|
155
|
+
node: node.parent as Node,
|
|
156
|
+
message: `Does not match expected format: ${xlrNode.format}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private validateArray(rootNode: Node, xlrNode: ArrayType) {
|
|
162
|
+
const issues: Array<ValidationError> = [];
|
|
163
|
+
rootNode.children?.forEach((child) =>
|
|
164
|
+
issues.push(...this.validateType(child, xlrNode.elementType))
|
|
165
|
+
);
|
|
166
|
+
return issues;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private validateObject(xlrNode: ObjectType, node: Node) {
|
|
170
|
+
const issues: Array<ValidationError> = [];
|
|
171
|
+
const objectProps = makePropertyMap(node);
|
|
172
|
+
// eslint-disable-next-line guard-for-in, no-restricted-syntax
|
|
173
|
+
for (const prop in xlrNode.properties) {
|
|
174
|
+
const expectedType = xlrNode.properties[prop];
|
|
175
|
+
const valueNode = objectProps.get(prop);
|
|
176
|
+
if (expectedType.required && valueNode === undefined) {
|
|
177
|
+
issues.push({
|
|
178
|
+
type: 'missing',
|
|
179
|
+
node,
|
|
180
|
+
message: `Property '${prop}' missing from type '${xlrNode.name}'`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (valueNode) {
|
|
185
|
+
issues.push(
|
|
186
|
+
...this.validateType(valueNode, expectedType.node as NamedType)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check if unknown keys are allowed and if they are - do the violate the constraint
|
|
192
|
+
const extraKeys = Array.from(objectProps.keys()).filter(
|
|
193
|
+
(key) => xlrNode.properties[key] === undefined
|
|
194
|
+
);
|
|
195
|
+
if (xlrNode.additionalProperties === false && extraKeys.length > 0) {
|
|
196
|
+
issues.push({
|
|
197
|
+
type: 'value',
|
|
198
|
+
node,
|
|
199
|
+
message: `Unexpected properties on '${xlrNode.name}': ${extraKeys.join(
|
|
200
|
+
', '
|
|
201
|
+
)}`,
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
issues.push(
|
|
205
|
+
...extraKeys.flatMap((key) =>
|
|
206
|
+
this.validateType(
|
|
207
|
+
objectProps.get(key) as Node,
|
|
208
|
+
xlrNode.additionalProperties as NodeType
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return issues;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private validateLiteralType(expectedType: PrimitiveTypes, literalType: Node) {
|
|
218
|
+
switch (expectedType.type) {
|
|
219
|
+
case 'boolean':
|
|
220
|
+
if (expectedType.const) {
|
|
221
|
+
return expectedType.const === literalType.value;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return typeof literalType.value === 'boolean';
|
|
225
|
+
break;
|
|
226
|
+
case 'number':
|
|
227
|
+
if (expectedType.const) {
|
|
228
|
+
return expectedType.const === literalType.value;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return typeof literalType.value === 'number';
|
|
232
|
+
break;
|
|
233
|
+
case 'string':
|
|
234
|
+
if (expectedType.const) {
|
|
235
|
+
return expectedType.const === literalType.value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return typeof literalType.value === 'string';
|
|
239
|
+
break;
|
|
240
|
+
case 'null':
|
|
241
|
+
return literalType.value === 'null';
|
|
242
|
+
break;
|
|
243
|
+
case 'never':
|
|
244
|
+
return literalType === undefined;
|
|
245
|
+
break;
|
|
246
|
+
case 'any':
|
|
247
|
+
return literalType !== undefined;
|
|
248
|
+
case 'unknown':
|
|
249
|
+
return literalType !== undefined;
|
|
250
|
+
case 'undefined':
|
|
251
|
+
return true;
|
|
252
|
+
default:
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private getRefType(ref: RefType): NodeType {
|
|
258
|
+
let refName = ref.ref;
|
|
259
|
+
const { genericArguments } = ref;
|
|
260
|
+
|
|
261
|
+
if (refName.indexOf('<') > 0) {
|
|
262
|
+
[refName] = refName.split('<');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const actualType = this.typeMap.get(refName) as NodeType;
|
|
266
|
+
const genericMap: Map<string, NodeType> = new Map();
|
|
267
|
+
|
|
268
|
+
// Compose first level generics here since `fillInGenerics` won't process them if a map is passed in
|
|
269
|
+
if (genericArguments && isGenericNodeType(actualType)) {
|
|
270
|
+
actualType.genericTokens.forEach((token, index) => {
|
|
271
|
+
genericMap.set(
|
|
272
|
+
token.symbol,
|
|
273
|
+
genericArguments[index] ?? token.default ?? token.constraints
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Fill in generics
|
|
279
|
+
return fillInGenerics(actualType, genericMap);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private getRegex(expString: string): RegExp {
|
|
283
|
+
if (this.regexCache.has(expString)) {
|
|
284
|
+
return this.regexCache.get(expString) as RegExp;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const exp = new RegExp(expString);
|
|
288
|
+
this.regexCache.set(expString, exp);
|
|
289
|
+
return exp;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private computeIntersectionType(types: Array<NodeType>): ObjectType | OrType {
|
|
293
|
+
let firstElement = types[0];
|
|
294
|
+
let effectiveType: ObjectType | OrType;
|
|
295
|
+
|
|
296
|
+
if (firstElement.type === 'ref') {
|
|
297
|
+
firstElement = this.getRefType(firstElement);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (firstElement.type === 'and') {
|
|
301
|
+
effectiveType = this.computeIntersectionType(firstElement.and);
|
|
302
|
+
} else if (firstElement.type !== 'or' && firstElement.type !== 'object') {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`Can't compute a union with a non-object type ${firstElement.type} (${firstElement.name})`
|
|
305
|
+
);
|
|
306
|
+
} else {
|
|
307
|
+
effectiveType = firstElement;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
types.slice(1).forEach((type) => {
|
|
311
|
+
let typeToApply = type;
|
|
312
|
+
|
|
313
|
+
if (type.type === 'ref') {
|
|
314
|
+
typeToApply = this.getRefType(type);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (typeToApply.type === 'and') {
|
|
318
|
+
typeToApply = this.computeIntersectionType([type, effectiveType]);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (typeToApply.type === 'object') {
|
|
322
|
+
if (effectiveType.type === 'object') {
|
|
323
|
+
effectiveType = this.computeEffectiveObject(
|
|
324
|
+
effectiveType,
|
|
325
|
+
typeToApply
|
|
326
|
+
);
|
|
327
|
+
} else {
|
|
328
|
+
effectiveType = {
|
|
329
|
+
...effectiveType,
|
|
330
|
+
or: effectiveType.or.map((y) =>
|
|
331
|
+
this.computeIntersectionType([y, typeToApply])
|
|
332
|
+
),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
} else if (typeToApply.type === 'or') {
|
|
336
|
+
if (effectiveType.type === 'object') {
|
|
337
|
+
effectiveType = {
|
|
338
|
+
...typeToApply,
|
|
339
|
+
or: typeToApply.or.map((y) =>
|
|
340
|
+
this.computeIntersectionType([y, effectiveType])
|
|
341
|
+
),
|
|
342
|
+
};
|
|
343
|
+
} else {
|
|
344
|
+
throw new Error('unimplemented operation or x or projection');
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`Can't compute a union with a non-object type ${typeToApply.type} (${typeToApply.name})`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
return effectiveType;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private computeEffectiveObject(
|
|
357
|
+
base: ObjectType,
|
|
358
|
+
operand: ObjectType,
|
|
359
|
+
errorOnOverlap = true
|
|
360
|
+
): ObjectType {
|
|
361
|
+
const newObject = {
|
|
362
|
+
...base,
|
|
363
|
+
name: `${base.name} & ${operand.name}`,
|
|
364
|
+
description: `Effective type combining ${base.name} and ${operand.name}`,
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// eslint-disable-next-line no-restricted-syntax, guard-for-in
|
|
368
|
+
for (const property in operand.properties) {
|
|
369
|
+
if (
|
|
370
|
+
newObject.properties[property] !== undefined &&
|
|
371
|
+
newObject.properties[property].node.type !==
|
|
372
|
+
operand.properties[property].node.type &&
|
|
373
|
+
errorOnOverlap
|
|
374
|
+
) {
|
|
375
|
+
throw new Error(
|
|
376
|
+
`Can't compute effective type for ${
|
|
377
|
+
base.name ?? 'object literal'
|
|
378
|
+
} and ${
|
|
379
|
+
operand.name ?? 'object literal'
|
|
380
|
+
} because of conflicting properties ${property}`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
newObject.properties[property] = operand.properties[property];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (newObject.additionalProperties && operand.additionalProperties) {
|
|
388
|
+
newObject.additionalProperties = {
|
|
389
|
+
type: 'and',
|
|
390
|
+
and: [newObject.additionalProperties, operand.additionalProperties],
|
|
391
|
+
};
|
|
392
|
+
} else if (operand.additionalProperties) {
|
|
393
|
+
newObject.additionalProperties = operand.additionalProperties;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return newObject;
|
|
397
|
+
}
|
|
398
|
+
}
|