@typra/emitter 0.2.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/src/cleanup/generated-file.d.ts +6 -0
- package/dist/src/cleanup/generated-file.js +61 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +110 -0
- package/dist/src/decorators.d.ts +56 -0
- package/dist/src/decorators.js +177 -0
- package/dist/src/emitter.d.ts +13 -0
- package/dist/src/emitter.js +137 -0
- package/dist/src/generate.d.ts +86 -0
- package/dist/src/generate.js +104 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +5 -0
- package/dist/src/ir/ast.d.ts +235 -0
- package/dist/src/ir/ast.js +589 -0
- package/dist/src/ir/declarations.d.ts +364 -0
- package/dist/src/ir/declarations.js +23 -0
- package/dist/src/ir/expansion.d.ts +140 -0
- package/dist/src/ir/expansion.js +407 -0
- package/dist/src/ir/lower.d.ts +53 -0
- package/dist/src/ir/lower.js +480 -0
- package/dist/src/ir/utilities.d.ts +12 -0
- package/dist/src/ir/utilities.js +39 -0
- package/dist/src/ir/visitor.d.ts +29 -0
- package/dist/src/ir/visitor.js +48 -0
- package/dist/src/languages/csharp/driver.d.ts +5 -0
- package/dist/src/languages/csharp/driver.js +315 -0
- package/dist/src/languages/csharp/emitter.d.ts +33 -0
- package/dist/src/languages/csharp/emitter.js +1140 -0
- package/dist/src/languages/csharp/scaffolding.d.ts +18 -0
- package/dist/src/languages/csharp/scaffolding.js +591 -0
- package/dist/src/languages/csharp/test-emitter.d.ts +43 -0
- package/dist/src/languages/csharp/test-emitter.js +274 -0
- package/dist/src/languages/csharp/visitor.d.ts +14 -0
- package/dist/src/languages/csharp/visitor.js +79 -0
- package/dist/src/languages/go/driver.d.ts +12 -0
- package/dist/src/languages/go/driver.js +128 -0
- package/dist/src/languages/go/emitter.d.ts +33 -0
- package/dist/src/languages/go/emitter.js +879 -0
- package/dist/src/languages/go/scaffolding.d.ts +18 -0
- package/dist/src/languages/go/scaffolding.js +53 -0
- package/dist/src/languages/go/test-emitter.d.ts +20 -0
- package/dist/src/languages/go/test-emitter.js +300 -0
- package/dist/src/languages/go/visitor.d.ts +14 -0
- package/dist/src/languages/go/visitor.js +78 -0
- package/dist/src/languages/markdown/driver.d.ts +19 -0
- package/dist/src/languages/markdown/driver.js +408 -0
- package/dist/src/languages/python/driver.d.ts +14 -0
- package/dist/src/languages/python/driver.js +372 -0
- package/dist/src/languages/python/emitter.d.ts +31 -0
- package/dist/src/languages/python/emitter.js +856 -0
- package/dist/src/languages/python/scaffolding.d.ts +33 -0
- package/dist/src/languages/python/scaffolding.js +279 -0
- package/dist/src/languages/python/test-emitter.d.ts +29 -0
- package/dist/src/languages/python/test-emitter.js +388 -0
- package/dist/src/languages/python/visitor.d.ts +14 -0
- package/dist/src/languages/python/visitor.js +65 -0
- package/dist/src/languages/rust/driver.d.ts +13 -0
- package/dist/src/languages/rust/driver.js +624 -0
- package/dist/src/languages/rust/emitter.d.ts +45 -0
- package/dist/src/languages/rust/emitter.js +1596 -0
- package/dist/src/languages/rust/visitor.d.ts +25 -0
- package/dist/src/languages/rust/visitor.js +153 -0
- package/dist/src/languages/typescript/driver.d.ts +8 -0
- package/dist/src/languages/typescript/driver.js +209 -0
- package/dist/src/languages/typescript/emitter.d.ts +42 -0
- package/dist/src/languages/typescript/emitter.js +904 -0
- package/dist/src/languages/typescript/scaffolding.d.ts +32 -0
- package/dist/src/languages/typescript/scaffolding.js +303 -0
- package/dist/src/languages/typescript/test-emitter.d.ts +23 -0
- package/dist/src/languages/typescript/test-emitter.js +204 -0
- package/dist/src/languages/typescript/visitor.d.ts +14 -0
- package/dist/src/languages/typescript/visitor.js +64 -0
- package/dist/src/lib.d.ts +33 -0
- package/dist/src/lib.js +101 -0
- package/dist/src/testing/index.d.ts +2 -0
- package/dist/src/testing/index.js +8 -0
- package/dist/src/testing/test-context.d.ts +63 -0
- package/dist/src/testing/test-context.js +355 -0
- package/fixtures/shapes/main.tsp +43 -0
- package/fixtures/tspconfig.yaml +13 -0
- package/package.json +76 -0
- package/src/lib/main.tsp +110 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import { getDoc, getTypeName, isTemplateInstance, getNamespaceFullName, getDiscriminator, } from "@typespec/compiler";
|
|
2
|
+
import { getStateScalar, getStateValue } from "../decorators.js";
|
|
3
|
+
import { StateKeys } from "../lib.js";
|
|
4
|
+
const getModelType = (model, rootNamespace, rootAlias) => {
|
|
5
|
+
let namespace = model.namespace ? getNamespaceFullName(model.namespace) : rootNamespace || "";
|
|
6
|
+
if (rootNamespace.includes('.'))
|
|
7
|
+
namespace = rootNamespace;
|
|
8
|
+
else {
|
|
9
|
+
const parts = namespace.split(".");
|
|
10
|
+
parts[0] = rootNamespace;
|
|
11
|
+
namespace = parts.join(".");
|
|
12
|
+
}
|
|
13
|
+
const name = getTypeName(model, {
|
|
14
|
+
nameOnly: true,
|
|
15
|
+
printable: true,
|
|
16
|
+
});
|
|
17
|
+
if (rootAlias) {
|
|
18
|
+
const rootName = rootNamespace.split(".").at(-1) || rootNamespace;
|
|
19
|
+
return {
|
|
20
|
+
namespace: namespace,
|
|
21
|
+
name: name.replace(rootName, rootAlias)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
namespace: namespace,
|
|
26
|
+
name: name
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Walk up the AST parent chain from `node` to find the enclosing
|
|
31
|
+
* TypeSpecScriptNode (identified by presence of `file.path`), which
|
|
32
|
+
* carries the source file path. Returns `""` if not found.
|
|
33
|
+
*
|
|
34
|
+
* We use `any` because TypeSpec does not export `Node`, `SyntaxKind`,
|
|
35
|
+
* or `TypeSpecScriptNode` from its public API surface.
|
|
36
|
+
*/
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
function getNodeFilePath(node) {
|
|
39
|
+
let current = node;
|
|
40
|
+
while (current) {
|
|
41
|
+
if (current?.file?.path) {
|
|
42
|
+
return current.file.path;
|
|
43
|
+
}
|
|
44
|
+
current = current.parent;
|
|
45
|
+
}
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Extract the semantic group (subfolder) name from a TypeSpec source file path.
|
|
50
|
+
*
|
|
51
|
+
* The TSP model files are organised under `schema/model/{group}/{file}.tsp`.
|
|
52
|
+
* This function finds the first `/model/` segment and returns the folder name
|
|
53
|
+
* immediately after it (the group), or `""` if the file is at the model root.
|
|
54
|
+
*
|
|
55
|
+
* Examples:
|
|
56
|
+
* ".../schema/model/connection/connection.tsp" → "connection"
|
|
57
|
+
* ".../schema/model/model/model.tsp" → "model"
|
|
58
|
+
* ".../schema/model/main.tsp" → ""
|
|
59
|
+
*/
|
|
60
|
+
function extractGroup(sourcePath) {
|
|
61
|
+
const normalized = sourcePath.replace(/\\/g, "/");
|
|
62
|
+
const idx = normalized.indexOf("/model/");
|
|
63
|
+
if (idx < 0)
|
|
64
|
+
return "";
|
|
65
|
+
const afterModel = normalized.slice(idx + "/model/".length);
|
|
66
|
+
const slash = afterModel.indexOf("/");
|
|
67
|
+
return slash >= 0 ? afterModel.slice(0, slash) : "";
|
|
68
|
+
}
|
|
69
|
+
export class TypeNode {
|
|
70
|
+
model;
|
|
71
|
+
typeName = {
|
|
72
|
+
namespace: "",
|
|
73
|
+
name: ""
|
|
74
|
+
};
|
|
75
|
+
description;
|
|
76
|
+
base = null;
|
|
77
|
+
childTypes = [];
|
|
78
|
+
coercions = [];
|
|
79
|
+
properties = [];
|
|
80
|
+
isAbstract = false;
|
|
81
|
+
isProtocol = false;
|
|
82
|
+
discriminator = undefined;
|
|
83
|
+
factories = [];
|
|
84
|
+
methods = [];
|
|
85
|
+
/** Semantic group derived from the TSP source subfolder (e.g. "connection", "tools"). */
|
|
86
|
+
group = "";
|
|
87
|
+
constructor(model, description) {
|
|
88
|
+
this.model = model;
|
|
89
|
+
this.model = model;
|
|
90
|
+
this.description = description;
|
|
91
|
+
}
|
|
92
|
+
retrievePolymorphicTypes() {
|
|
93
|
+
let instances = [];
|
|
94
|
+
if (this.discriminator && this.childTypes.length > 0) {
|
|
95
|
+
instances = this.childTypes.map(child => ({
|
|
96
|
+
discriminator: this.discriminator,
|
|
97
|
+
value: child.properties.find(p => p.name === this.discriminator)?.defaultValue || "*",
|
|
98
|
+
instance: child,
|
|
99
|
+
}));
|
|
100
|
+
if (!this.isAbstract) {
|
|
101
|
+
instances = [...instances, { discriminator: this.discriminator, value: "*", instance: this }];
|
|
102
|
+
}
|
|
103
|
+
const filteredInstances = instances.filter(instance => instance.value !== "*");
|
|
104
|
+
const defaultInstance = instances.filter(i => i.value === "*")[0];
|
|
105
|
+
return {
|
|
106
|
+
types: filteredInstances,
|
|
107
|
+
default: defaultInstance,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
;
|
|
113
|
+
getSanitizedObject() {
|
|
114
|
+
return {
|
|
115
|
+
typeName: this.typeName,
|
|
116
|
+
description: this.description,
|
|
117
|
+
base: this.base || {},
|
|
118
|
+
isAbstract: this.isAbstract,
|
|
119
|
+
isProtocol: this.isProtocol,
|
|
120
|
+
discriminator: this.discriminator,
|
|
121
|
+
coercions: this.coercions,
|
|
122
|
+
factories: this.factories,
|
|
123
|
+
methods: this.methods,
|
|
124
|
+
childTypes: this.childTypes.map(ct => ct.getSanitizedObject()),
|
|
125
|
+
properties: this.properties.map(prop => prop.getSanitizedObject()),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export class PropertyNode {
|
|
130
|
+
name;
|
|
131
|
+
typeName = {
|
|
132
|
+
namespace: "",
|
|
133
|
+
name: ""
|
|
134
|
+
};
|
|
135
|
+
description;
|
|
136
|
+
samples = [];
|
|
137
|
+
knownAs = [];
|
|
138
|
+
defaultFor = [];
|
|
139
|
+
isScalar = false;
|
|
140
|
+
isOptional = false;
|
|
141
|
+
isCollection = false;
|
|
142
|
+
isAny = false;
|
|
143
|
+
isDict = false;
|
|
144
|
+
defaultValue = null;
|
|
145
|
+
allowedValues = [];
|
|
146
|
+
/** Name of the string-literal union alias (e.g., "Role"), null if unnamed or not an enum. */
|
|
147
|
+
enumName = null;
|
|
148
|
+
/** True when the union includes a bare `string` variant (open enum — accepts any string). */
|
|
149
|
+
isOpenEnum = false;
|
|
150
|
+
property;
|
|
151
|
+
type = undefined;
|
|
152
|
+
constructor(property, description) {
|
|
153
|
+
this.name = property.name;
|
|
154
|
+
this.description = description;
|
|
155
|
+
this.property = property;
|
|
156
|
+
}
|
|
157
|
+
getSanitizedObject() {
|
|
158
|
+
return {
|
|
159
|
+
name: this.name,
|
|
160
|
+
typeName: this.typeName,
|
|
161
|
+
description: this.description,
|
|
162
|
+
samples: this.samples,
|
|
163
|
+
knownAs: this.knownAs,
|
|
164
|
+
defaultFor: this.defaultFor,
|
|
165
|
+
isScalar: this.isScalar,
|
|
166
|
+
isOptional: this.isOptional,
|
|
167
|
+
isCollection: this.isCollection,
|
|
168
|
+
isAny: this.isAny,
|
|
169
|
+
isDict: this.isDict,
|
|
170
|
+
defaultValue: this.defaultValue || "null",
|
|
171
|
+
allowedValues: this.allowedValues,
|
|
172
|
+
enumName: this.enumName,
|
|
173
|
+
isOpenEnum: this.isOpenEnum,
|
|
174
|
+
type: this.type ? this.type.getSanitizedObject() : undefined,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export const enumerateTypes = function* (node, visited = new Set()) {
|
|
179
|
+
for (const prop of node.properties) {
|
|
180
|
+
if (prop.type) {
|
|
181
|
+
// enumerate
|
|
182
|
+
for (const subNode of enumerateTypes(prop.type, visited)) {
|
|
183
|
+
if (!visited.has(`${subNode.typeName.namespace}.${subNode.typeName.name}`)) {
|
|
184
|
+
yield subNode;
|
|
185
|
+
visited.add(`${subNode.typeName.namespace}.${subNode.typeName.name}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const child of prop.type.childTypes) {
|
|
189
|
+
for (const subNode of enumerateTypes(child, visited)) {
|
|
190
|
+
if (!visited.has(`${subNode.typeName.namespace}.${subNode.typeName.name}`)) {
|
|
191
|
+
yield subNode;
|
|
192
|
+
visited.add(`${subNode.typeName.namespace}.${subNode.typeName.name}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!visited.has(`${node.typeName.namespace}.${node.typeName.name}`)) {
|
|
199
|
+
yield node;
|
|
200
|
+
for (const child of node.childTypes) {
|
|
201
|
+
for (const subNode of enumerateTypes(child, visited)) {
|
|
202
|
+
if (!visited.has(`${subNode.typeName.namespace}.${subNode.typeName.name}`)) {
|
|
203
|
+
yield subNode;
|
|
204
|
+
visited.add(`${subNode.typeName.namespace}.${subNode.typeName.name}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
visited.add(`${node.typeName.namespace}.${node.typeName.name}`);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
export const resolveModel = (program, model, visited = new Set(), rootNamespace, rootAlias) => {
|
|
212
|
+
const node = new TypeNode(model, getDoc(program, model) || "");
|
|
213
|
+
if (model.name === "Named") {
|
|
214
|
+
// Use Named<T> for actual model props, but innerModel for naming and docs
|
|
215
|
+
const innerModel = getTemplateModel(model);
|
|
216
|
+
if (!innerModel || innerModel.kind !== "Model") {
|
|
217
|
+
throw new Error(`Invalid Named<T> model: ${model.name}`);
|
|
218
|
+
}
|
|
219
|
+
node.typeName = getModelType(innerModel, rootNamespace, rootAlias);
|
|
220
|
+
node.childTypes = resolveModelChildren(program, innerModel, visited, rootNamespace, rootAlias);
|
|
221
|
+
node.description = getDoc(program, innerModel) || "";
|
|
222
|
+
node.isAbstract = getStateScalar(program, StateKeys.abstracts, innerModel) || false;
|
|
223
|
+
node.isProtocol = getStateScalar(program, StateKeys.protocols, innerModel) || false;
|
|
224
|
+
const discriminator = getDiscriminator(program, innerModel);
|
|
225
|
+
node.discriminator = discriminator ? discriminator.propertyName : undefined;
|
|
226
|
+
// coercion .ctor
|
|
227
|
+
node.coercions = getStateValue(program, StateKeys.coercions, innerModel);
|
|
228
|
+
// factory and method stubs
|
|
229
|
+
node.factories = getStateValue(program, StateKeys.factories, innerModel);
|
|
230
|
+
node.methods = getStateValue(program, StateKeys.methods, innerModel);
|
|
231
|
+
node.group = extractGroup(getNodeFilePath(innerModel.node));
|
|
232
|
+
visited.add(innerModel.name);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
node.typeName = getModelType(model, rootNamespace, rootAlias);
|
|
236
|
+
node.childTypes = resolveModelChildren(program, model, visited, rootNamespace, rootAlias);
|
|
237
|
+
node.isAbstract = getStateScalar(program, StateKeys.abstracts, model) || false;
|
|
238
|
+
node.isProtocol = getStateScalar(program, StateKeys.protocols, model) || false;
|
|
239
|
+
const discriminator = getDiscriminator(program, model);
|
|
240
|
+
node.discriminator = discriminator ? discriminator.propertyName : undefined;
|
|
241
|
+
// coercion .ctor
|
|
242
|
+
node.coercions = getStateValue(program, StateKeys.coercions, model);
|
|
243
|
+
// factory and method stubs
|
|
244
|
+
node.factories = getStateValue(program, StateKeys.factories, model);
|
|
245
|
+
node.methods = getStateValue(program, StateKeys.methods, model);
|
|
246
|
+
node.group = extractGroup(getNodeFilePath(model.node));
|
|
247
|
+
visited.add(model.name);
|
|
248
|
+
}
|
|
249
|
+
if (model.baseModel) {
|
|
250
|
+
node.base = getModelType(model.baseModel, rootNamespace, rootAlias);
|
|
251
|
+
}
|
|
252
|
+
// resolve properties if model
|
|
253
|
+
if (model.kind === "Model") {
|
|
254
|
+
const properties = [];
|
|
255
|
+
for (const [_, value] of model.properties) {
|
|
256
|
+
const prop = resolveProperty(program, value, visited, rootNamespace, rootAlias);
|
|
257
|
+
// samples
|
|
258
|
+
prop.samples = getStateValue(program, StateKeys.samples, value);
|
|
259
|
+
// wire mappings
|
|
260
|
+
prop.knownAs = getStateValue(program, StateKeys.knownAs, value);
|
|
261
|
+
prop.defaultFor = getStateValue(program, StateKeys.defaultFor, value);
|
|
262
|
+
properties.push(prop);
|
|
263
|
+
}
|
|
264
|
+
node.properties = properties;
|
|
265
|
+
}
|
|
266
|
+
return node;
|
|
267
|
+
};
|
|
268
|
+
export const resolveModelChildren = (program, model, visited, rootNamespace, rootAlias) => {
|
|
269
|
+
return model.derivedModels.filter(derived => !visited.has(derived.name)).flatMap(derived => {
|
|
270
|
+
return [resolveModel(program, derived, visited, rootNamespace, rootAlias), ...resolveModelChildren(program, derived, visited, rootNamespace, rootAlias)];
|
|
271
|
+
});
|
|
272
|
+
};
|
|
273
|
+
export const resolveProperty = (program, property, visited, rootNamespace, rootAlias) => {
|
|
274
|
+
switch (property.type.kind) {
|
|
275
|
+
case "Scalar":
|
|
276
|
+
return resolveScalarProperty(program, property, property.type);
|
|
277
|
+
case "Model":
|
|
278
|
+
return resolveModelProperty(program, property, property.type, visited, rootNamespace, rootAlias);
|
|
279
|
+
case "Union":
|
|
280
|
+
return resolveUnionProperty(program, property, property.type, visited, rootNamespace, rootAlias);
|
|
281
|
+
case "Intrinsic":
|
|
282
|
+
return resolveIntrinsicProperty(program, property, property.type, visited);
|
|
283
|
+
case "String":
|
|
284
|
+
// this is for default values in discriminated types
|
|
285
|
+
const prop = new PropertyNode(property, getDoc(program, property) || "");
|
|
286
|
+
prop.defaultValue = property.type.value;
|
|
287
|
+
prop.typeName = {
|
|
288
|
+
namespace: "",
|
|
289
|
+
name: "string"
|
|
290
|
+
};
|
|
291
|
+
prop.isScalar = true;
|
|
292
|
+
prop.isAny = false;
|
|
293
|
+
prop.isOptional = property.optional;
|
|
294
|
+
prop.isCollection = false;
|
|
295
|
+
return prop;
|
|
296
|
+
default:
|
|
297
|
+
program.reportDiagnostic({
|
|
298
|
+
code: "typra-emitter-unsupported-property-type",
|
|
299
|
+
message: `Unsupported property type: ${property.type.kind}`,
|
|
300
|
+
severity: "error",
|
|
301
|
+
target: property
|
|
302
|
+
});
|
|
303
|
+
return new PropertyNode(property, getDoc(program, property) || "");
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
export const resolveScalarProperty = (program, property, scalar) => {
|
|
307
|
+
const prop = new PropertyNode(property, getDoc(program, property) || "");
|
|
308
|
+
prop.typeName = {
|
|
309
|
+
namespace: "",
|
|
310
|
+
name: getTypeName(scalar, { nameOnly: true })
|
|
311
|
+
};
|
|
312
|
+
prop.isScalar = true;
|
|
313
|
+
prop.isAny = false;
|
|
314
|
+
prop.isOptional = property.optional;
|
|
315
|
+
prop.isCollection = false;
|
|
316
|
+
// defaults
|
|
317
|
+
if (property.defaultValue) {
|
|
318
|
+
// only handle these things
|
|
319
|
+
switch (property.defaultValue.valueKind) {
|
|
320
|
+
case "StringValue":
|
|
321
|
+
prop.defaultValue = property.defaultValue.value;
|
|
322
|
+
break;
|
|
323
|
+
case "BooleanValue":
|
|
324
|
+
prop.defaultValue = property.defaultValue.value;
|
|
325
|
+
break;
|
|
326
|
+
case "NumericValue":
|
|
327
|
+
prop.defaultValue = property.defaultValue.value.asNumber();
|
|
328
|
+
break;
|
|
329
|
+
default:
|
|
330
|
+
prop.defaultValue = "unspecified";
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return prop;
|
|
335
|
+
};
|
|
336
|
+
export const resolveIntrinsicProperty = (program, property, intrinsic, visited) => {
|
|
337
|
+
const prop = new PropertyNode(property, getDoc(program, property) || "");
|
|
338
|
+
prop.typeName = {
|
|
339
|
+
namespace: "",
|
|
340
|
+
name: getTypeName(intrinsic, { nameOnly: true })
|
|
341
|
+
};
|
|
342
|
+
prop.isScalar = true;
|
|
343
|
+
prop.isAny = true;
|
|
344
|
+
prop.isOptional = property.optional;
|
|
345
|
+
prop.isCollection = prop.typeName.name.includes("[") && prop.typeName.name.includes("]");
|
|
346
|
+
// defaults
|
|
347
|
+
if (property.defaultValue) {
|
|
348
|
+
// only handle these things
|
|
349
|
+
switch (property.defaultValue.valueKind) {
|
|
350
|
+
case "StringValue":
|
|
351
|
+
prop.defaultValue = property.defaultValue.value;
|
|
352
|
+
break;
|
|
353
|
+
case "BooleanValue":
|
|
354
|
+
prop.defaultValue = property.defaultValue.value;
|
|
355
|
+
break;
|
|
356
|
+
case "NumericValue":
|
|
357
|
+
prop.defaultValue = property.defaultValue.value.asNumber();
|
|
358
|
+
break;
|
|
359
|
+
default:
|
|
360
|
+
prop.defaultValue = null;
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return prop;
|
|
365
|
+
};
|
|
366
|
+
export const resolveModelProperty = (program, property, model, visited, rootNamespace, rootAlias) => {
|
|
367
|
+
const prop = new PropertyNode(property, getDoc(program, property) || "");
|
|
368
|
+
if (model.name === "Array") {
|
|
369
|
+
const innerModel = getTemplateModel(model);
|
|
370
|
+
if (innerModel) {
|
|
371
|
+
// Use innerModel for naming and docs
|
|
372
|
+
if (innerModel.name === "Record") {
|
|
373
|
+
// Record situation -> treat as array of dictionary
|
|
374
|
+
prop.isScalar = false;
|
|
375
|
+
prop.isAny = false;
|
|
376
|
+
prop.isOptional = property.optional;
|
|
377
|
+
prop.isCollection = true;
|
|
378
|
+
prop.isDict = true;
|
|
379
|
+
prop.typeName = {
|
|
380
|
+
namespace: "",
|
|
381
|
+
name: "dictionary"
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
prop.isScalar = false;
|
|
386
|
+
prop.isAny = false;
|
|
387
|
+
prop.isOptional = property.optional;
|
|
388
|
+
prop.isCollection = true;
|
|
389
|
+
prop.typeName = getModelType(innerModel, rootNamespace, rootAlias);
|
|
390
|
+
if (!visited.has(model.name)) {
|
|
391
|
+
prop.type = resolveModel(program, innerModel, visited, rootNamespace, rootAlias);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
// check for Scalar Arrays
|
|
397
|
+
const innerType = getTemplateType(model);
|
|
398
|
+
if (innerType && innerType.kind === "Scalar") {
|
|
399
|
+
prop.isScalar = true;
|
|
400
|
+
prop.isAny = false;
|
|
401
|
+
prop.isOptional = property.optional;
|
|
402
|
+
prop.isCollection = true;
|
|
403
|
+
prop.typeName = {
|
|
404
|
+
namespace: "",
|
|
405
|
+
name: getTypeName(innerType, { nameOnly: true })
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
else if (innerType && innerType.kind === "Intrinsic") {
|
|
409
|
+
prop.isScalar = true;
|
|
410
|
+
prop.isAny = true;
|
|
411
|
+
prop.isOptional = property.optional;
|
|
412
|
+
prop.isCollection = true;
|
|
413
|
+
prop.typeName = {
|
|
414
|
+
namespace: "",
|
|
415
|
+
name: "unknown"
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
program.reportDiagnostic({
|
|
420
|
+
code: "typra-emitter-unsupported-array-type",
|
|
421
|
+
message: `Unsupported array type: ${getTypeName(model)}`,
|
|
422
|
+
severity: "error",
|
|
423
|
+
target: property
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
prop.isScalar = false;
|
|
430
|
+
prop.isAny = false;
|
|
431
|
+
prop.isOptional = property.optional;
|
|
432
|
+
prop.isCollection = false;
|
|
433
|
+
prop.typeName = getModelType(model, rootNamespace, rootAlias);
|
|
434
|
+
if (prop.typeName.name === "Record<unknown>") {
|
|
435
|
+
prop.isScalar = true;
|
|
436
|
+
prop.isDict = true;
|
|
437
|
+
prop.typeName = {
|
|
438
|
+
namespace: "",
|
|
439
|
+
name: "dictionary"
|
|
440
|
+
};
|
|
441
|
+
// need to clear this out as a model type
|
|
442
|
+
prop.type = undefined;
|
|
443
|
+
}
|
|
444
|
+
if (!visited.has(model.name) && prop.typeName.name !== "dictionary") {
|
|
445
|
+
prop.type = resolveModel(program, model, visited, rootNamespace, rootAlias);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return prop;
|
|
449
|
+
};
|
|
450
|
+
export const resolveUnionProperty = (program, property, union, visited, rootNamespace, rootAlias) => {
|
|
451
|
+
const prop = new PropertyNode(property, getDoc(program, property) || "");
|
|
452
|
+
prop.isScalar = false;
|
|
453
|
+
prop.isAny = false;
|
|
454
|
+
prop.isOptional = property.optional;
|
|
455
|
+
prop.isCollection = false;
|
|
456
|
+
const variants = Array.from(union.variants).map(([, v]) => v.type);
|
|
457
|
+
const models = variants.filter(v => v.kind === "Model");
|
|
458
|
+
if (models.length === 1) {
|
|
459
|
+
prop.typeName = getModelType(models[0], rootNamespace, rootAlias);
|
|
460
|
+
if (!visited.has(models[0].name)) {
|
|
461
|
+
prop.type = resolveModel(program, models[0], visited, rootNamespace, rootAlias);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
else if (models.length === 2) {
|
|
465
|
+
const modelNames = models.map(m => m.name);
|
|
466
|
+
// collection situation
|
|
467
|
+
if (modelNames.includes("Record") && modelNames.includes("Array")) {
|
|
468
|
+
// Should be Record<T> -> T
|
|
469
|
+
const recordType = getTemplateModel(models[modelNames.indexOf("Record")]);
|
|
470
|
+
// Should be Array<Named<T>> -> Named<T>
|
|
471
|
+
const namedType = getTemplateModel(models[modelNames.indexOf("Array")]);
|
|
472
|
+
// Should be Named<T> -> T
|
|
473
|
+
const arrayType = getTemplateModel(namedType);
|
|
474
|
+
if (recordType && arrayType && namedType && recordType.name === arrayType.name) {
|
|
475
|
+
prop.isCollection = true;
|
|
476
|
+
// Use T as actual class model for naming purposes
|
|
477
|
+
prop.typeName = getModelType(arrayType, rootNamespace, rootAlias);
|
|
478
|
+
// Use Named<T> for actual model props
|
|
479
|
+
if (!visited.has(arrayType.name)) {
|
|
480
|
+
prop.type = resolveModel(program, namedType, visited, rootNamespace, rootAlias);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
program.reportDiagnostic({
|
|
485
|
+
code: "typra-emitter-unsupported-union-types",
|
|
486
|
+
message: `Unsupported union types for Record/Array: ${recordType?.name} / ${arrayType?.name} - they should match.`,
|
|
487
|
+
severity: "error",
|
|
488
|
+
target: property
|
|
489
|
+
});
|
|
490
|
+
return prop;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else if (modelNames.includes("Named")) {
|
|
494
|
+
const namedIdx = modelNames.indexOf("Named");
|
|
495
|
+
const namedModel = getTemplateModel(models[namedIdx]);
|
|
496
|
+
const mainModel = models[(namedIdx + 1) % 2];
|
|
497
|
+
if (namedModel && namedModel.name === mainModel.name) {
|
|
498
|
+
prop.typeName = getModelType(namedModel, rootNamespace, rootAlias);
|
|
499
|
+
if (!visited.has(mainModel.name)) {
|
|
500
|
+
prop.type = resolveModel(program, namedModel, visited, rootNamespace, rootAlias);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
program.reportDiagnostic({
|
|
505
|
+
code: "typra-emitter-named-model-union-types",
|
|
506
|
+
message: `Named model union types must match! (${models.map(m => m.name).join(", ")})`,
|
|
507
|
+
severity: "error",
|
|
508
|
+
target: property
|
|
509
|
+
});
|
|
510
|
+
return prop;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
program.reportDiagnostic({
|
|
515
|
+
code: "typra-emitter-unsupported-union-types",
|
|
516
|
+
message: `Unsupported union type: ${union.kind}`,
|
|
517
|
+
severity: "error",
|
|
518
|
+
target: property
|
|
519
|
+
});
|
|
520
|
+
return prop;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
// string variants for `kind` scalar type
|
|
525
|
+
const acceptableVariants = variants.filter(v => v.kind === "String" || (v.kind === "Scalar" && v.name === "string")).length;
|
|
526
|
+
if (acceptableVariants === variants.length) {
|
|
527
|
+
prop.typeName = {
|
|
528
|
+
"namespace": "",
|
|
529
|
+
"name": "string"
|
|
530
|
+
};
|
|
531
|
+
prop.isScalar = true;
|
|
532
|
+
if (property.defaultValue && property.defaultValue.valueKind === "StringValue") {
|
|
533
|
+
prop.defaultValue = property.defaultValue?.value || null;
|
|
534
|
+
}
|
|
535
|
+
prop.allowedValues = variants.filter(v => v.kind === "String").map(v => v.value);
|
|
536
|
+
// Resolve enum type name from union.name (named unions) or alias name (alias statements)
|
|
537
|
+
let enumName = union.name;
|
|
538
|
+
if (!enumName && union.node) {
|
|
539
|
+
const node = union.node;
|
|
540
|
+
// For aliases: union.node is a UnionExpression whose parent is the AliasStatement
|
|
541
|
+
if (node.parent?.id?.sv && typeof node.parent.id.sv === "string") {
|
|
542
|
+
// Guard against parent.id being a file path (happens for named union declarations)
|
|
543
|
+
const parentId = node.parent.id.sv;
|
|
544
|
+
if (!parentId.includes("/") && !parentId.includes("\\")) {
|
|
545
|
+
enumName = parentId;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (enumName) {
|
|
550
|
+
prop.enumName = enumName;
|
|
551
|
+
}
|
|
552
|
+
// Check if the union includes a bare `string` scalar (open enum)
|
|
553
|
+
prop.isOpenEnum = variants.some(v => v.kind === "Scalar" && v.name === "string");
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
program.reportDiagnostic({
|
|
557
|
+
code: "typra-emitter-unsupported-union-types",
|
|
558
|
+
message: `Unable to resolve ${union.name} - too many variants: (${models.map(m => m.name).join(", ")})`,
|
|
559
|
+
severity: "error",
|
|
560
|
+
target: property
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return prop;
|
|
564
|
+
}
|
|
565
|
+
return prop;
|
|
566
|
+
};
|
|
567
|
+
const getTemplateModel = (type) => {
|
|
568
|
+
if (!type)
|
|
569
|
+
return undefined;
|
|
570
|
+
if (isTemplateInstance(type)) {
|
|
571
|
+
const t = type.templateMapper?.args.at(0);
|
|
572
|
+
if (t && t.entityKind === "Type" && t.kind === "Model") {
|
|
573
|
+
return t;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return undefined;
|
|
577
|
+
};
|
|
578
|
+
const getTemplateType = (type) => {
|
|
579
|
+
if (!type)
|
|
580
|
+
return undefined;
|
|
581
|
+
if (isTemplateInstance(type)) {
|
|
582
|
+
const t = type.templateMapper?.args.at(0);
|
|
583
|
+
if (t && t.entityKind === "Type") {
|
|
584
|
+
return t;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return undefined;
|
|
588
|
+
};
|
|
589
|
+
//# sourceMappingURL=ast.js.map
|