@player-tools/xlr-utils 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 +3 -0
- package/dist/index.cjs.js +526 -0
- package/dist/index.d.ts +126 -0
- package/dist/index.esm.js +477 -0
- package/package.json +35 -0
- package/src/annotations.ts +230 -0
- package/src/index.ts +5 -0
- package/src/test-helpers.ts +72 -0
- package/src/ts-helpers.ts +356 -0
- package/src/type-checks.ts +106 -0
- package/src/validation-helpers.ts +88 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import type { Annotations } from '@player-tools/xlr';
|
|
3
|
+
|
|
4
|
+
interface JSDocContainer {
|
|
5
|
+
/** */
|
|
6
|
+
jsDoc: Array<ts.JSDoc>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
function extractDescription(text: string | undefined): Annotations {
|
|
13
|
+
if (!text) {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return { description: text };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Checks if the parent node is a non-object type
|
|
22
|
+
*/
|
|
23
|
+
function parentIsNonObjectPath(node: ts.Node) {
|
|
24
|
+
return (
|
|
25
|
+
node.parent &&
|
|
26
|
+
(ts.isArrayTypeNode(node.parent) ||
|
|
27
|
+
ts.isTupleTypeNode(node.parent) ||
|
|
28
|
+
ts.isOptionalTypeNode(node.parent) ||
|
|
29
|
+
ts.isRestTypeNode(node.parent) ||
|
|
30
|
+
ts.isUnionTypeNode(node.parent))
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Traverses up the node tree to build the title path down to the initial node
|
|
36
|
+
*/
|
|
37
|
+
function recurseTypeChain(
|
|
38
|
+
node: ts.Node,
|
|
39
|
+
child: ts.Node | undefined
|
|
40
|
+
): Array<string> {
|
|
41
|
+
if (!node) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
ts.isArrayTypeNode(node) &&
|
|
47
|
+
node.parent &&
|
|
48
|
+
ts.isRestTypeNode(node.parent)
|
|
49
|
+
) {
|
|
50
|
+
return recurseTypeChain(node.parent, node);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (ts.isRestTypeNode(node)) {
|
|
54
|
+
return recurseTypeChain(node.parent, node);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (ts.isOptionalTypeNode(node)) {
|
|
58
|
+
return recurseTypeChain(node.parent, node);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (ts.isUnionTypeNode(node)) {
|
|
62
|
+
return recurseTypeChain(node.parent, node);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (ts.isParenthesizedTypeNode(node)) {
|
|
66
|
+
return recurseTypeChain(node.parent, node);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (ts.isTypeLiteralNode(node)) {
|
|
70
|
+
return recurseTypeChain(node.parent, node);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (ts.isArrayTypeNode(node)) {
|
|
74
|
+
return ['[]', ...recurseTypeChain(node.parent, node)];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (ts.isTupleTypeNode(node)) {
|
|
78
|
+
const pos = node.elements.indexOf(child as any);
|
|
79
|
+
return [
|
|
80
|
+
...(pos === -1 ? [] : [`${pos}`]),
|
|
81
|
+
...recurseTypeChain(node.parent, node),
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
ts.isTypeAliasDeclaration(node) ||
|
|
87
|
+
ts.isInterfaceDeclaration(node) ||
|
|
88
|
+
ts.isPropertySignature(node)
|
|
89
|
+
) {
|
|
90
|
+
return [node.name.getText(), ...recurseTypeChain(node.parent, node)];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (parentIsNonObjectPath(node)) {
|
|
94
|
+
return recurseTypeChain(node.parent, node);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Builds the `Title` property by traversing up and noting the named types in the tree
|
|
102
|
+
*/
|
|
103
|
+
function extractTitle(node: ts.Node): Annotations {
|
|
104
|
+
const typeNames = recurseTypeChain(node, undefined).reverse().join('.');
|
|
105
|
+
|
|
106
|
+
if (!typeNames.length) {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { title: typeNames };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
*
|
|
115
|
+
*/
|
|
116
|
+
function stringifyDoc(
|
|
117
|
+
docString: undefined | string | ts.NodeArray<ts.JSDocComment>
|
|
118
|
+
): string | undefined {
|
|
119
|
+
if (typeof docString === 'undefined' || typeof docString === 'string') {
|
|
120
|
+
return docString;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return docString.map(({ text }) => text).join(' ');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Extracts JSDoc tags to strings
|
|
128
|
+
*/
|
|
129
|
+
function extractTags(tags: ReadonlyArray<ts.JSDocTag>): Annotations {
|
|
130
|
+
const descriptions: Array<string> = [];
|
|
131
|
+
const examples: Array<string> = [];
|
|
132
|
+
const _default: Array<string> = [];
|
|
133
|
+
const see: Array<string> = [];
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
*
|
|
137
|
+
*/
|
|
138
|
+
const extractSee = (tag: ts.JSDocSeeTag) => {
|
|
139
|
+
return `${tag.tagName ? `${tag.tagName?.getText()} ` : ''}${
|
|
140
|
+
stringifyDoc(tag.comment)?.trim() ?? ''
|
|
141
|
+
}`;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
tags.forEach((tag) => {
|
|
145
|
+
if (!tag.comment) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (tag.tagName.text === 'example') {
|
|
150
|
+
examples.push(stringifyDoc(tag.comment)?.trim() ?? '');
|
|
151
|
+
} else if (tag.tagName.text === 'default') {
|
|
152
|
+
_default.push(stringifyDoc(tag.comment)?.trim() ?? '');
|
|
153
|
+
} else if (tag.tagName.text === 'see') {
|
|
154
|
+
see.push(extractSee(tag as ts.JSDocSeeTag));
|
|
155
|
+
} else {
|
|
156
|
+
const text = stringifyDoc(tag.comment)?.trim() ?? '';
|
|
157
|
+
descriptions.push(`@${tag.tagName.text} ${text}`);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
...(descriptions.length === 0
|
|
163
|
+
? {}
|
|
164
|
+
: { description: descriptions.join('\n') }),
|
|
165
|
+
...(examples.length === 0 ? {} : { examples }),
|
|
166
|
+
...(_default.length === 0 ? {} : { default: _default.join('\n') }),
|
|
167
|
+
...(see.length === 0 ? {} : { see }),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Joins Arrays of maybe strings with a given separator
|
|
173
|
+
*/
|
|
174
|
+
function join(t: Array<string | undefined>, separator = '\n') {
|
|
175
|
+
const unique = new Set(t).values();
|
|
176
|
+
return Array.from(unique)
|
|
177
|
+
.filter((s) => s !== undefined)
|
|
178
|
+
.join(separator)
|
|
179
|
+
.trim();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Merges Annotation nodes for various nodes
|
|
184
|
+
*/
|
|
185
|
+
function mergeAnnotations(nodes: Array<Annotations>): Annotations {
|
|
186
|
+
const name = nodes.find((n) => n.name)?.name;
|
|
187
|
+
const title = join(
|
|
188
|
+
nodes.map((n) => n.title),
|
|
189
|
+
', '
|
|
190
|
+
);
|
|
191
|
+
const description = join(nodes.map((n) => n.description));
|
|
192
|
+
const _default = join(nodes.map((n) => n.default));
|
|
193
|
+
const comment = join(nodes.map((n) => n.comment));
|
|
194
|
+
const examples = join(
|
|
195
|
+
nodes.map((n) =>
|
|
196
|
+
Array.isArray(n.examples) ? join(n.examples) : n.examples
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
const see = join(
|
|
200
|
+
nodes.map((n) => (Array.isArray(n.see) ? join(n.see) : n.see))
|
|
201
|
+
);
|
|
202
|
+
return {
|
|
203
|
+
...(name ? { name } : {}),
|
|
204
|
+
...(title ? { title } : {}),
|
|
205
|
+
...(description ? { description } : {}),
|
|
206
|
+
...(examples ? { examples } : {}),
|
|
207
|
+
...(_default ? { default: _default } : {}),
|
|
208
|
+
...(see ? { see } : {}),
|
|
209
|
+
...(comment ? { comment } : {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Converts JSDoc comments to strings
|
|
215
|
+
*/
|
|
216
|
+
export function decorateNode(node: ts.Node): Annotations {
|
|
217
|
+
const { jsDoc } = node as unknown as JSDocContainer;
|
|
218
|
+
const titleAnnotation = extractTitle(node);
|
|
219
|
+
|
|
220
|
+
if (jsDoc && jsDoc.length) {
|
|
221
|
+
const first = jsDoc[0];
|
|
222
|
+
return mergeAnnotations([
|
|
223
|
+
extractDescription(stringifyDoc(first.comment)),
|
|
224
|
+
titleAnnotation,
|
|
225
|
+
extractTags(first.tags ?? []),
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return titleAnnotation;
|
|
230
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
export interface SetupReturnType {
|
|
4
|
+
/**
|
|
5
|
+
* Virtual source file containing the passed in text
|
|
6
|
+
*/
|
|
7
|
+
sf: ts.SourceFile;
|
|
8
|
+
/**
|
|
9
|
+
* Type checker for the virtual program
|
|
10
|
+
*/
|
|
11
|
+
tc: ts.TypeChecker;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Setups a virtual TS environment for tests
|
|
16
|
+
*/
|
|
17
|
+
export function setupTestEnv(sourceCode: string, mockFileName = 'filename.ts') {
|
|
18
|
+
const sourceFile = ts.createSourceFile(
|
|
19
|
+
mockFileName,
|
|
20
|
+
sourceCode,
|
|
21
|
+
ts.ScriptTarget.Latest,
|
|
22
|
+
/* setParentNodes */ true
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const outputs = [];
|
|
26
|
+
|
|
27
|
+
const compilerHost = {
|
|
28
|
+
getSourceFile(filename: any) {
|
|
29
|
+
if (filename === mockFileName) {
|
|
30
|
+
return sourceFile;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return undefined;
|
|
34
|
+
},
|
|
35
|
+
writeFile(name: any, text: any, writeByteOrderMark: any) {
|
|
36
|
+
outputs.push({ name, text, writeByteOrderMark });
|
|
37
|
+
},
|
|
38
|
+
getDefaultLibFileName() {
|
|
39
|
+
return 'lib.d.ts';
|
|
40
|
+
},
|
|
41
|
+
useCaseSensitiveFileNames() {
|
|
42
|
+
return false;
|
|
43
|
+
},
|
|
44
|
+
getCanonicalFileName(filename: any) {
|
|
45
|
+
return filename;
|
|
46
|
+
},
|
|
47
|
+
getCurrentDirectory() {
|
|
48
|
+
return '';
|
|
49
|
+
},
|
|
50
|
+
getNewLine() {
|
|
51
|
+
return '\n';
|
|
52
|
+
},
|
|
53
|
+
fileExists(fileName: string) {
|
|
54
|
+
if (fileName === mockFileName) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return false;
|
|
59
|
+
},
|
|
60
|
+
readFile(fileName: string) {
|
|
61
|
+
if (fileName === mockFileName) {
|
|
62
|
+
return sourceCode;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return undefined;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const program = ts.createProgram([mockFileName], {}, compilerHost);
|
|
70
|
+
|
|
71
|
+
return { sf: sourceFile, tc: program.getTypeChecker() };
|
|
72
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/* eslint-disable no-bitwise */
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
import type {
|
|
4
|
+
NamedType,
|
|
5
|
+
NodeType,
|
|
6
|
+
ObjectProperty,
|
|
7
|
+
ObjectType,
|
|
8
|
+
} from '@player-tools/xlr';
|
|
9
|
+
import { resolveConditional } from './validation-helpers';
|
|
10
|
+
import { isGenericNodeType } from './type-checks';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns the required type or the optionally required type
|
|
14
|
+
*/
|
|
15
|
+
export function tsStripOptionalType(node: ts.TypeNode): ts.TypeNode {
|
|
16
|
+
return ts.isOptionalTypeNode(node) ? node.type : node;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns if the top level declaration is exported
|
|
21
|
+
*/
|
|
22
|
+
export function isExportedDeclaration(node: ts.Statement) {
|
|
23
|
+
return !!node.modifiers?.some(
|
|
24
|
+
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns if the node is exported from the source file
|
|
30
|
+
*/
|
|
31
|
+
export function isNodeExported(node: ts.Node): boolean {
|
|
32
|
+
return (
|
|
33
|
+
(ts.getCombinedModifierFlags(node as ts.Declaration) &
|
|
34
|
+
ts.ModifierFlags.Export) !==
|
|
35
|
+
0 ||
|
|
36
|
+
(!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns the actual type and will following import chains if needed
|
|
42
|
+
*/
|
|
43
|
+
export function getReferencedType(
|
|
44
|
+
node: ts.TypeReferenceNode,
|
|
45
|
+
typeChecker: ts.TypeChecker
|
|
46
|
+
) {
|
|
47
|
+
let symbol = typeChecker.getSymbolAtLocation(node.typeName);
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
symbol &&
|
|
51
|
+
(symbol.flags & ts.SymbolFlags.Alias) === ts.SymbolFlags.Alias
|
|
52
|
+
) {
|
|
53
|
+
// follow alias if it is a symbol
|
|
54
|
+
symbol = typeChecker.getAliasedSymbol(symbol);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const varDecl = symbol?.declarations?.[0];
|
|
58
|
+
if (
|
|
59
|
+
varDecl &&
|
|
60
|
+
(ts.isInterfaceDeclaration(varDecl) || ts.isTypeAliasDeclaration(varDecl))
|
|
61
|
+
) {
|
|
62
|
+
return { declaration: varDecl, exported: isNodeExported(varDecl) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns list of string literals from potential union of strings
|
|
68
|
+
*/
|
|
69
|
+
export function getStringLiteralsFromUnion(node: ts.Node): Set<string> {
|
|
70
|
+
if (ts.isUnionTypeNode(node)) {
|
|
71
|
+
return new Set(
|
|
72
|
+
node.types.map((type) => {
|
|
73
|
+
if (ts.isLiteralTypeNode(type) && ts.isStringLiteral(type.literal)) {
|
|
74
|
+
return type.literal.text;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return '';
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) {
|
|
83
|
+
return new Set([node.literal.text]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return new Set();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Converts a format string into a regex that can be used to validate a given string matches the template
|
|
91
|
+
*/
|
|
92
|
+
export function buildTemplateRegex(
|
|
93
|
+
node: ts.TemplateLiteralTypeNode,
|
|
94
|
+
typeChecker: ts.TypeChecker
|
|
95
|
+
): string {
|
|
96
|
+
let regex = node.head.text;
|
|
97
|
+
node.templateSpans.forEach((span) => {
|
|
98
|
+
// process template tag
|
|
99
|
+
let type = span.type.kind;
|
|
100
|
+
if (ts.isTypeReferenceNode(span.type)) {
|
|
101
|
+
let symbol = typeChecker.getSymbolAtLocation(
|
|
102
|
+
span.type.typeName
|
|
103
|
+
) as ts.Symbol;
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
symbol &&
|
|
107
|
+
(symbol.flags & ts.SymbolFlags.Alias) === ts.SymbolFlags.Alias
|
|
108
|
+
) {
|
|
109
|
+
// follow alias if it is a symbol
|
|
110
|
+
symbol = typeChecker.getAliasedSymbol(symbol);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type = (symbol?.declarations?.[0] as ts.TypeAliasDeclaration).type.kind;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (type === ts.SyntaxKind.StringKeyword) {
|
|
117
|
+
regex += '.*';
|
|
118
|
+
} else if (type === ts.SyntaxKind.NumberKeyword) {
|
|
119
|
+
regex += '[0-9]*';
|
|
120
|
+
} else if (type === ts.SyntaxKind.BooleanKeyword) {
|
|
121
|
+
regex += 'true|false';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// add non-tag element
|
|
125
|
+
regex += span.literal.text;
|
|
126
|
+
});
|
|
127
|
+
return regex;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Walks generics to fill in values from a combination of the default, constraint, and passed in map values
|
|
132
|
+
*/
|
|
133
|
+
export function fillInGenerics(
|
|
134
|
+
xlrNode: NodeType,
|
|
135
|
+
generics?: Map<string, NodeType>
|
|
136
|
+
): NodeType {
|
|
137
|
+
// Need to make sure not to set generics in passed in map to avoid using generics outside of tree
|
|
138
|
+
let localGenerics: Map<string, NodeType>;
|
|
139
|
+
|
|
140
|
+
if (generics) {
|
|
141
|
+
localGenerics = new Map(generics);
|
|
142
|
+
} else {
|
|
143
|
+
localGenerics = new Map();
|
|
144
|
+
if (isGenericNodeType(xlrNode)) {
|
|
145
|
+
xlrNode.genericTokens?.forEach((token) => {
|
|
146
|
+
localGenerics.set(
|
|
147
|
+
token.symbol,
|
|
148
|
+
(token.default ?? token.constraints) as NodeType
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (xlrNode.type === 'ref') {
|
|
155
|
+
if (localGenerics.has(xlrNode.ref)) {
|
|
156
|
+
return {
|
|
157
|
+
...(localGenerics.get(xlrNode.ref) as NodeType),
|
|
158
|
+
...(xlrNode.genericArguments
|
|
159
|
+
? {
|
|
160
|
+
genericArguments: xlrNode.genericArguments.map((ga) =>
|
|
161
|
+
fillInGenerics(ga, localGenerics)
|
|
162
|
+
),
|
|
163
|
+
}
|
|
164
|
+
: {}),
|
|
165
|
+
...(xlrNode.title ? { title: xlrNode.title } : {}),
|
|
166
|
+
...(xlrNode.name ? { name: xlrNode.name } : {}),
|
|
167
|
+
...(xlrNode.description ? { description: xlrNode.description } : {}),
|
|
168
|
+
...(xlrNode.comment ? { comment: xlrNode.comment } : {}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
...xlrNode,
|
|
174
|
+
...(xlrNode.genericArguments
|
|
175
|
+
? {
|
|
176
|
+
genericArguments: xlrNode.genericArguments.map((ga) =>
|
|
177
|
+
fillInGenerics(ga, localGenerics)
|
|
178
|
+
),
|
|
179
|
+
}
|
|
180
|
+
: {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (xlrNode.type === 'object') {
|
|
185
|
+
const newProperties: { [name: string]: ObjectProperty } = {};
|
|
186
|
+
Object.getOwnPropertyNames(xlrNode.properties).forEach((propName) => {
|
|
187
|
+
const prop = xlrNode.properties[propName];
|
|
188
|
+
newProperties[propName] = {
|
|
189
|
+
required: prop.required,
|
|
190
|
+
node: fillInGenerics(prop.node, localGenerics),
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
...xlrNode,
|
|
196
|
+
properties: newProperties,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (xlrNode.type === 'array') {
|
|
201
|
+
// eslint-disable-next-line no-param-reassign
|
|
202
|
+
xlrNode.elementType = fillInGenerics(xlrNode.elementType, localGenerics);
|
|
203
|
+
} else if (xlrNode.type === 'or' || xlrNode.type === 'and') {
|
|
204
|
+
let pointer;
|
|
205
|
+
if (xlrNode.type === 'or') {
|
|
206
|
+
pointer = xlrNode.or;
|
|
207
|
+
} else {
|
|
208
|
+
pointer = xlrNode.and;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
...xlrNode,
|
|
213
|
+
[xlrNode.type]: pointer.map((prop) => {
|
|
214
|
+
return fillInGenerics(prop, localGenerics);
|
|
215
|
+
}),
|
|
216
|
+
};
|
|
217
|
+
} else if (xlrNode.type === 'record') {
|
|
218
|
+
return {
|
|
219
|
+
...xlrNode,
|
|
220
|
+
keyType: fillInGenerics(xlrNode.keyType, localGenerics),
|
|
221
|
+
valueType: fillInGenerics(xlrNode.valueType, localGenerics),
|
|
222
|
+
};
|
|
223
|
+
} else if (xlrNode.type === 'conditional') {
|
|
224
|
+
const filledInConditional = {
|
|
225
|
+
...xlrNode,
|
|
226
|
+
check: {
|
|
227
|
+
left: fillInGenerics(xlrNode.check.left, localGenerics),
|
|
228
|
+
right: fillInGenerics(xlrNode.check.right, localGenerics),
|
|
229
|
+
},
|
|
230
|
+
value: {
|
|
231
|
+
true: fillInGenerics(xlrNode.value.true, localGenerics),
|
|
232
|
+
false: fillInGenerics(xlrNode.value.false, localGenerics),
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Check to see if we have enough information to resolve this conditional
|
|
237
|
+
if (
|
|
238
|
+
filledInConditional.check.left.type !== 'ref' &&
|
|
239
|
+
filledInConditional.check.right.type !== 'ref'
|
|
240
|
+
) {
|
|
241
|
+
return {
|
|
242
|
+
name: xlrNode.name,
|
|
243
|
+
title: xlrNode.title,
|
|
244
|
+
...resolveConditional(filledInConditional),
|
|
245
|
+
} as NamedType;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return filledInConditional;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return xlrNode;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Applies the TS `Pick` type to an interface/union/intersection */
|
|
255
|
+
export function applyPickOrOmitToNodeType(
|
|
256
|
+
baseObject: NodeType,
|
|
257
|
+
operation: 'Pick' | 'Omit',
|
|
258
|
+
properties: Set<string>
|
|
259
|
+
): NodeType | undefined {
|
|
260
|
+
if (baseObject.type === 'object') {
|
|
261
|
+
const newObject = { ...baseObject };
|
|
262
|
+
Object.keys(baseObject.properties).forEach((key) => {
|
|
263
|
+
if (
|
|
264
|
+
(operation === 'Omit' && properties.has(key)) ||
|
|
265
|
+
(operation === 'Pick' && !properties.has(key))
|
|
266
|
+
) {
|
|
267
|
+
delete newObject.properties[key];
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Filter out objects in cases:
|
|
273
|
+
* - A Pick operation and there are no properties left
|
|
274
|
+
* - An Omit operation and there are no properties left and no additional properties allowed
|
|
275
|
+
*/
|
|
276
|
+
if (
|
|
277
|
+
Object.keys(newObject.properties).length === 0 &&
|
|
278
|
+
(operation !== 'Omit' || newObject.additionalProperties === false)
|
|
279
|
+
) {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return newObject;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let pointer;
|
|
287
|
+
if (baseObject.type === 'and') {
|
|
288
|
+
pointer = baseObject.and;
|
|
289
|
+
} else if (baseObject.type === 'or') {
|
|
290
|
+
pointer = baseObject.or;
|
|
291
|
+
} else {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Error: Can not apply ${operation} to type ${baseObject.type}`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const pickedTypes = pointer
|
|
298
|
+
.map((type) => {
|
|
299
|
+
const node = applyPickOrOmitToNodeType(type, operation, properties);
|
|
300
|
+
if (node === undefined) {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return { ...node, additionalProperties: false } as ObjectType;
|
|
305
|
+
})
|
|
306
|
+
.filter((type) => type !== undefined) as NodeType[];
|
|
307
|
+
|
|
308
|
+
if (pickedTypes.length === 0) {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (pickedTypes.length === 1) {
|
|
313
|
+
return pickedTypes[0];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (baseObject.type === 'and') {
|
|
317
|
+
return { ...baseObject, and: pickedTypes };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { ...baseObject, or: pickedTypes };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Applies the TS `Omit` type to an interface/union/intersection */
|
|
324
|
+
export function applyPartialOrRequiredToNodeType(
|
|
325
|
+
baseObject: NodeType,
|
|
326
|
+
modifier: boolean
|
|
327
|
+
): NodeType {
|
|
328
|
+
if (baseObject.type === 'object') {
|
|
329
|
+
const newObject = { ...baseObject };
|
|
330
|
+
Object.keys(baseObject.properties).forEach((key) => {
|
|
331
|
+
newObject.properties[key].required = modifier;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return newObject;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (baseObject.type === 'and') {
|
|
338
|
+
const pickedTypes = baseObject.and.map((type) =>
|
|
339
|
+
applyPartialOrRequiredToNodeType(type, modifier)
|
|
340
|
+
);
|
|
341
|
+
return { ...baseObject, and: pickedTypes };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (baseObject.type === 'or') {
|
|
345
|
+
const pickedTypes = baseObject.or.map((type) =>
|
|
346
|
+
applyPartialOrRequiredToNodeType(type, modifier)
|
|
347
|
+
);
|
|
348
|
+
return { ...baseObject, or: pickedTypes };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
throw new Error(
|
|
352
|
+
`Error: Can not apply ${modifier ? 'Required' : 'Partial'} to type ${
|
|
353
|
+
baseObject.type
|
|
354
|
+
}`
|
|
355
|
+
);
|
|
356
|
+
}
|