@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.
Files changed (82) hide show
  1. package/dist/src/cleanup/generated-file.d.ts +6 -0
  2. package/dist/src/cleanup/generated-file.js +61 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +110 -0
  5. package/dist/src/decorators.d.ts +56 -0
  6. package/dist/src/decorators.js +177 -0
  7. package/dist/src/emitter.d.ts +13 -0
  8. package/dist/src/emitter.js +137 -0
  9. package/dist/src/generate.d.ts +86 -0
  10. package/dist/src/generate.js +104 -0
  11. package/dist/src/index.d.ts +4 -0
  12. package/dist/src/index.js +5 -0
  13. package/dist/src/ir/ast.d.ts +235 -0
  14. package/dist/src/ir/ast.js +589 -0
  15. package/dist/src/ir/declarations.d.ts +364 -0
  16. package/dist/src/ir/declarations.js +23 -0
  17. package/dist/src/ir/expansion.d.ts +140 -0
  18. package/dist/src/ir/expansion.js +407 -0
  19. package/dist/src/ir/lower.d.ts +53 -0
  20. package/dist/src/ir/lower.js +480 -0
  21. package/dist/src/ir/utilities.d.ts +12 -0
  22. package/dist/src/ir/utilities.js +39 -0
  23. package/dist/src/ir/visitor.d.ts +29 -0
  24. package/dist/src/ir/visitor.js +48 -0
  25. package/dist/src/languages/csharp/driver.d.ts +5 -0
  26. package/dist/src/languages/csharp/driver.js +315 -0
  27. package/dist/src/languages/csharp/emitter.d.ts +33 -0
  28. package/dist/src/languages/csharp/emitter.js +1140 -0
  29. package/dist/src/languages/csharp/scaffolding.d.ts +18 -0
  30. package/dist/src/languages/csharp/scaffolding.js +591 -0
  31. package/dist/src/languages/csharp/test-emitter.d.ts +43 -0
  32. package/dist/src/languages/csharp/test-emitter.js +274 -0
  33. package/dist/src/languages/csharp/visitor.d.ts +14 -0
  34. package/dist/src/languages/csharp/visitor.js +79 -0
  35. package/dist/src/languages/go/driver.d.ts +12 -0
  36. package/dist/src/languages/go/driver.js +128 -0
  37. package/dist/src/languages/go/emitter.d.ts +33 -0
  38. package/dist/src/languages/go/emitter.js +879 -0
  39. package/dist/src/languages/go/scaffolding.d.ts +18 -0
  40. package/dist/src/languages/go/scaffolding.js +53 -0
  41. package/dist/src/languages/go/test-emitter.d.ts +20 -0
  42. package/dist/src/languages/go/test-emitter.js +300 -0
  43. package/dist/src/languages/go/visitor.d.ts +14 -0
  44. package/dist/src/languages/go/visitor.js +78 -0
  45. package/dist/src/languages/markdown/driver.d.ts +19 -0
  46. package/dist/src/languages/markdown/driver.js +408 -0
  47. package/dist/src/languages/python/driver.d.ts +14 -0
  48. package/dist/src/languages/python/driver.js +372 -0
  49. package/dist/src/languages/python/emitter.d.ts +31 -0
  50. package/dist/src/languages/python/emitter.js +856 -0
  51. package/dist/src/languages/python/scaffolding.d.ts +33 -0
  52. package/dist/src/languages/python/scaffolding.js +279 -0
  53. package/dist/src/languages/python/test-emitter.d.ts +29 -0
  54. package/dist/src/languages/python/test-emitter.js +388 -0
  55. package/dist/src/languages/python/visitor.d.ts +14 -0
  56. package/dist/src/languages/python/visitor.js +65 -0
  57. package/dist/src/languages/rust/driver.d.ts +13 -0
  58. package/dist/src/languages/rust/driver.js +624 -0
  59. package/dist/src/languages/rust/emitter.d.ts +45 -0
  60. package/dist/src/languages/rust/emitter.js +1596 -0
  61. package/dist/src/languages/rust/visitor.d.ts +25 -0
  62. package/dist/src/languages/rust/visitor.js +153 -0
  63. package/dist/src/languages/typescript/driver.d.ts +8 -0
  64. package/dist/src/languages/typescript/driver.js +209 -0
  65. package/dist/src/languages/typescript/emitter.d.ts +42 -0
  66. package/dist/src/languages/typescript/emitter.js +904 -0
  67. package/dist/src/languages/typescript/scaffolding.d.ts +32 -0
  68. package/dist/src/languages/typescript/scaffolding.js +303 -0
  69. package/dist/src/languages/typescript/test-emitter.d.ts +23 -0
  70. package/dist/src/languages/typescript/test-emitter.js +204 -0
  71. package/dist/src/languages/typescript/visitor.d.ts +14 -0
  72. package/dist/src/languages/typescript/visitor.js +64 -0
  73. package/dist/src/lib.d.ts +33 -0
  74. package/dist/src/lib.js +101 -0
  75. package/dist/src/testing/index.d.ts +2 -0
  76. package/dist/src/testing/index.js +8 -0
  77. package/dist/src/testing/test-context.d.ts +63 -0
  78. package/dist/src/testing/test-context.js +355 -0
  79. package/fixtures/shapes/main.tsp +43 -0
  80. package/fixtures/tspconfig.yaml +13 -0
  81. package/package.json +76 -0
  82. 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