@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,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C# test emitter — TypeNode → xUnit test file.
|
|
3
|
+
*
|
|
4
|
+
* Replaces `test.cs.njk` Nunjucks template with a typed TypeScript function
|
|
5
|
+
* that produces a complete C# xUnit test class.
|
|
6
|
+
*
|
|
7
|
+
* Each TypeNode with samples/coercions/factories gets one test file
|
|
8
|
+
* containing LoadYaml, LoadJson, roundtrip, and validity tests.
|
|
9
|
+
*/
|
|
10
|
+
import { toPascalCase } from "../../ir/visitor.js";
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Main entry point
|
|
13
|
+
// ============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* Emit a complete C# xUnit test file for a type node.
|
|
16
|
+
*/
|
|
17
|
+
export function emitCSharpTest(ctx) {
|
|
18
|
+
const typeName = ctx.node.typeName.name;
|
|
19
|
+
const L = [];
|
|
20
|
+
L.push('using Xunit;');
|
|
21
|
+
L.push('');
|
|
22
|
+
L.push('#pragma warning disable IDE0130');
|
|
23
|
+
L.push(`namespace ${ctx.namespace};`);
|
|
24
|
+
L.push('#pragma warning restore IDE0130');
|
|
25
|
+
L.push('');
|
|
26
|
+
L.push('');
|
|
27
|
+
L.push(`public class ${typeName}ConversionTests`);
|
|
28
|
+
L.push('{');
|
|
29
|
+
// --- Example tests (6 per example) ---
|
|
30
|
+
ctx.examples.forEach((sample, i) => {
|
|
31
|
+
const suffix = i === 0 ? '' : `${i}`;
|
|
32
|
+
// LoadYamlInput
|
|
33
|
+
L.push(' [Fact]');
|
|
34
|
+
L.push(` public void LoadYamlInput${suffix}()`);
|
|
35
|
+
L.push(' {');
|
|
36
|
+
L.push(...emitRawStringLiteral('yamlData', sample.yaml));
|
|
37
|
+
L.push('');
|
|
38
|
+
L.push(` var instance = ${typeName}.FromYaml(yamlData);`);
|
|
39
|
+
L.push('');
|
|
40
|
+
L.push(' Assert.NotNull(instance);');
|
|
41
|
+
const yamlAssertions = emitExampleAssertions(sample.validations, 'instance');
|
|
42
|
+
if (yamlAssertions)
|
|
43
|
+
L.push(yamlAssertions);
|
|
44
|
+
L.push(' }');
|
|
45
|
+
L.push('');
|
|
46
|
+
// LoadJsonInput
|
|
47
|
+
L.push(' [Fact]');
|
|
48
|
+
L.push(` public void LoadJsonInput${suffix}()`);
|
|
49
|
+
L.push(' {');
|
|
50
|
+
L.push(...emitRawStringLiteral('jsonData', sample.json));
|
|
51
|
+
L.push('');
|
|
52
|
+
L.push(` var instance = ${typeName}.FromJson(jsonData);`);
|
|
53
|
+
L.push(' Assert.NotNull(instance);');
|
|
54
|
+
const jsonAssertions = emitExampleAssertions(sample.validations, 'instance');
|
|
55
|
+
if (jsonAssertions)
|
|
56
|
+
L.push(jsonAssertions);
|
|
57
|
+
L.push(' }');
|
|
58
|
+
L.push('');
|
|
59
|
+
// RoundtripJson
|
|
60
|
+
L.push(' [Fact]');
|
|
61
|
+
L.push(` public void RoundtripJson${suffix}()`);
|
|
62
|
+
L.push(' {');
|
|
63
|
+
L.push(' // Test that FromJson -> ToJson -> FromJson produces equivalent data');
|
|
64
|
+
L.push(...emitRawStringLiteral('jsonData', sample.json));
|
|
65
|
+
L.push('');
|
|
66
|
+
L.push(` var original = ${typeName}.FromJson(jsonData);`);
|
|
67
|
+
L.push(' Assert.NotNull(original);');
|
|
68
|
+
L.push('');
|
|
69
|
+
L.push(' var json = original.ToJson();');
|
|
70
|
+
L.push(' Assert.False(string.IsNullOrEmpty(json));');
|
|
71
|
+
L.push('');
|
|
72
|
+
L.push(` var reloaded = ${typeName}.FromJson(json);`);
|
|
73
|
+
L.push(' Assert.NotNull(reloaded);');
|
|
74
|
+
const rtJsonAssertions = emitExampleAssertions(sample.validations, 'reloaded');
|
|
75
|
+
if (rtJsonAssertions)
|
|
76
|
+
L.push(rtJsonAssertions);
|
|
77
|
+
L.push(' }');
|
|
78
|
+
L.push('');
|
|
79
|
+
// RoundtripYaml
|
|
80
|
+
L.push(' [Fact]');
|
|
81
|
+
L.push(` public void RoundtripYaml${suffix}()`);
|
|
82
|
+
L.push(' {');
|
|
83
|
+
L.push(' // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data');
|
|
84
|
+
L.push(...emitRawStringLiteral('yamlData', sample.yaml));
|
|
85
|
+
L.push('');
|
|
86
|
+
L.push(` var original = ${typeName}.FromYaml(yamlData);`);
|
|
87
|
+
L.push(' Assert.NotNull(original);');
|
|
88
|
+
L.push('');
|
|
89
|
+
L.push(' var yaml = original.ToYaml();');
|
|
90
|
+
L.push(' Assert.False(string.IsNullOrEmpty(yaml));');
|
|
91
|
+
L.push('');
|
|
92
|
+
L.push(` var reloaded = ${typeName}.FromYaml(yaml);`);
|
|
93
|
+
L.push(' Assert.NotNull(reloaded);');
|
|
94
|
+
const rtYamlAssertions = emitExampleAssertions(sample.validations, 'reloaded');
|
|
95
|
+
if (rtYamlAssertions)
|
|
96
|
+
L.push(rtYamlAssertions);
|
|
97
|
+
L.push(' }');
|
|
98
|
+
L.push('');
|
|
99
|
+
// ToJsonProducesValidJson
|
|
100
|
+
L.push(' [Fact]');
|
|
101
|
+
L.push(` public void ToJsonProducesValidJson${suffix}()`);
|
|
102
|
+
L.push(' {');
|
|
103
|
+
L.push(...emitRawStringLiteral('jsonData', sample.json));
|
|
104
|
+
L.push('');
|
|
105
|
+
L.push(` var instance = ${typeName}.FromJson(jsonData);`);
|
|
106
|
+
L.push(' var json = instance.ToJson();');
|
|
107
|
+
L.push('');
|
|
108
|
+
L.push(' // Verify it\'s valid JSON by parsing it');
|
|
109
|
+
L.push(' var parsed = System.Text.Json.JsonDocument.Parse(json);');
|
|
110
|
+
L.push(' Assert.NotNull(parsed);');
|
|
111
|
+
L.push(' }');
|
|
112
|
+
L.push('');
|
|
113
|
+
// ToYamlProducesValidYaml
|
|
114
|
+
L.push(' [Fact]');
|
|
115
|
+
L.push(` public void ToYamlProducesValidYaml${suffix}()`);
|
|
116
|
+
L.push(' {');
|
|
117
|
+
L.push(...emitRawStringLiteral('yamlData', sample.yaml));
|
|
118
|
+
L.push('');
|
|
119
|
+
L.push(` var instance = ${typeName}.FromYaml(yamlData);`);
|
|
120
|
+
L.push(' var yaml = instance.ToYaml();');
|
|
121
|
+
L.push('');
|
|
122
|
+
L.push(' // Verify it\'s valid YAML by parsing it');
|
|
123
|
+
L.push(' var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build();');
|
|
124
|
+
L.push(' var parsed = deserializer.Deserialize<object>(yaml);');
|
|
125
|
+
L.push(' Assert.NotNull(parsed);');
|
|
126
|
+
L.push(' }');
|
|
127
|
+
});
|
|
128
|
+
// --- Coercion tests (2 per coercion) ---
|
|
129
|
+
if (ctx.coercions.length > 0) {
|
|
130
|
+
for (const alt of ctx.coercions) {
|
|
131
|
+
const titleScalar = alt.scalar.charAt(0).toUpperCase() + alt.scalar.slice(1);
|
|
132
|
+
// Build the C# data literal
|
|
133
|
+
let dataLine;
|
|
134
|
+
if (alt.scalar === 'string') {
|
|
135
|
+
dataLine = ` var data = "${alt.value.toString().replace(/\\/g, '\\\\').replace(/"/g, '\\"')}";`;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
let dataValue;
|
|
139
|
+
if (alt.value.toString() === "True")
|
|
140
|
+
dataValue = "true";
|
|
141
|
+
else if (alt.value.toString() === "False")
|
|
142
|
+
dataValue = "false";
|
|
143
|
+
else
|
|
144
|
+
dataValue = alt.value.toString();
|
|
145
|
+
dataLine = ` var data = "${dataValue}";`;
|
|
146
|
+
}
|
|
147
|
+
// LoadJsonFrom{Scalar}
|
|
148
|
+
L.push('');
|
|
149
|
+
L.push(' [Fact]');
|
|
150
|
+
L.push(` public void LoadJsonFrom${titleScalar}()`);
|
|
151
|
+
L.push(' {');
|
|
152
|
+
L.push(` // alternate representation as ${alt.scalar}`);
|
|
153
|
+
L.push(dataLine);
|
|
154
|
+
L.push(` var instance = ${typeName}.FromJson(data);`);
|
|
155
|
+
L.push(' Assert.NotNull(instance);');
|
|
156
|
+
const jsonCoercionAssertions = emitCoercionAssertions(alt.validations);
|
|
157
|
+
if (jsonCoercionAssertions)
|
|
158
|
+
L.push(jsonCoercionAssertions);
|
|
159
|
+
L.push(' }');
|
|
160
|
+
// LoadYamlFrom{Scalar}
|
|
161
|
+
L.push('');
|
|
162
|
+
L.push(' [Fact]');
|
|
163
|
+
L.push(` public void LoadYamlFrom${titleScalar}()`);
|
|
164
|
+
L.push(' {');
|
|
165
|
+
L.push(` // alternate representation as ${alt.scalar}`);
|
|
166
|
+
L.push(dataLine);
|
|
167
|
+
L.push(` var instance = ${typeName}.FromYaml(data);`);
|
|
168
|
+
L.push(' Assert.NotNull(instance);');
|
|
169
|
+
L.push('');
|
|
170
|
+
const yamlCoercionAssertions = emitCoercionAssertions(alt.validations);
|
|
171
|
+
if (yamlCoercionAssertions)
|
|
172
|
+
L.push(yamlCoercionAssertions);
|
|
173
|
+
L.push(' }');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// --- Factory tests (1 per factory) ---
|
|
177
|
+
if (ctx.factories.length > 0) {
|
|
178
|
+
for (const factory of ctx.factories) {
|
|
179
|
+
const methodName = ctx.renderCsharpFactoryMethodName(factory.name);
|
|
180
|
+
const paramValues = Object.values(factory.params)
|
|
181
|
+
.map(pType => ctx.renderCsharpFactoryTestValue(pType))
|
|
182
|
+
.join(', ');
|
|
183
|
+
L.push('');
|
|
184
|
+
L.push(' [Fact]');
|
|
185
|
+
L.push(` public void Factory${methodName}()`);
|
|
186
|
+
L.push(' {');
|
|
187
|
+
L.push(` var instance = ${typeName}.${methodName}(${paramValues});`);
|
|
188
|
+
L.push(' Assert.NotNull(instance);');
|
|
189
|
+
for (const [propName, value] of Object.entries(factory.sets)) {
|
|
190
|
+
if (value === true) {
|
|
191
|
+
L.push(` Assert.True(instance.${ctx.renderName(propName)});`);
|
|
192
|
+
}
|
|
193
|
+
else if (value === false) {
|
|
194
|
+
L.push(` Assert.False(instance.${ctx.renderName(propName)});`);
|
|
195
|
+
}
|
|
196
|
+
else if (typeof value === 'number') {
|
|
197
|
+
L.push(` Assert.Equal(${value}, instance.${ctx.renderName(propName)});`);
|
|
198
|
+
}
|
|
199
|
+
else if (typeof value === 'string') {
|
|
200
|
+
// Check if this property is a closed enum (skip discriminator fields)
|
|
201
|
+
const prop = ctx.node.properties.find(p => p.name === propName);
|
|
202
|
+
const isDiscriminator = ctx.node.discriminator === propName;
|
|
203
|
+
if (prop && prop.enumName && !prop.isOpenEnum && !isDiscriminator) {
|
|
204
|
+
const csEnumName = toPascalCase(prop.enumName);
|
|
205
|
+
const memberName = toPascalCase(value);
|
|
206
|
+
L.push(` Assert.Equal(${csEnumName}.${memberName}, instance.${ctx.renderName(propName)});`);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
L.push(` Assert.Equal("${value}", instance.${ctx.renderName(propName)});`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
L.push(' }');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
L.push('}');
|
|
217
|
+
L.push('');
|
|
218
|
+
return L.join('\n');
|
|
219
|
+
}
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// Assertion helpers
|
|
222
|
+
// ============================================================================
|
|
223
|
+
/** Render Assert.Equal / Assert.True / Assert.False lines for example validations. */
|
|
224
|
+
function emitExampleAssertions(validations, varName) {
|
|
225
|
+
return validations.map(v => {
|
|
226
|
+
if (v.value === "True" || v.value === "False") {
|
|
227
|
+
return ` Assert.${v.value === "False" ? "False" : "True"}(${varName}.${v.key});`;
|
|
228
|
+
}
|
|
229
|
+
if (v.startDelim === '@"') {
|
|
230
|
+
return ` Assert.Equal(${v.startDelim}${v.value}${v.endDelim}.Replace("\\r\\n", "\\n"), ${varName}.${v.key});`;
|
|
231
|
+
}
|
|
232
|
+
return ` Assert.Equal(${v.startDelim}${v.value}${v.endDelim}, ${varName}.${v.key});`;
|
|
233
|
+
}).join('\n');
|
|
234
|
+
}
|
|
235
|
+
/** Render assertion lines for coercion validations (with isFloat / bool / normal dispatch). */
|
|
236
|
+
function emitCoercionAssertions(validations) {
|
|
237
|
+
return validations.map(v => {
|
|
238
|
+
const valueStr = v.value.toString();
|
|
239
|
+
if (valueStr === "True" || valueStr === "False") {
|
|
240
|
+
return [
|
|
241
|
+
` Assert.NotNull(instance.${v.key});`,
|
|
242
|
+
` Assert.IsType<bool>(instance.${v.key});`,
|
|
243
|
+
` Assert.${valueStr === "False" ? "False" : "True"}((bool)instance.${v.key});`,
|
|
244
|
+
].join('\n');
|
|
245
|
+
}
|
|
246
|
+
// Enum values (e.g., McpApprovalModeKind.Never) — emit direct assertion
|
|
247
|
+
if (v.delimiter === '' && /^[A-Z]/.test(valueStr)) {
|
|
248
|
+
return ` Assert.Equal(${v.value}, instance.${v.key});`;
|
|
249
|
+
}
|
|
250
|
+
// isFloat: value string contains '.'
|
|
251
|
+
if (valueStr.includes('.')) {
|
|
252
|
+
return [
|
|
253
|
+
` Assert.NotNull(instance.${v.key});`,
|
|
254
|
+
` Assert.True(instance.${v.key} is float || instance.${v.key} is double || instance.${v.key} is int || instance.${v.key} is long);`,
|
|
255
|
+
` Assert.Equal(${v.value}, Convert.ToDouble(instance.${v.key}), 5);`,
|
|
256
|
+
].join('\n');
|
|
257
|
+
}
|
|
258
|
+
return ` Assert.Equal(${v.delimiter}${v.value}${v.delimiter}, instance.${v.key});`;
|
|
259
|
+
}).join('\n');
|
|
260
|
+
}
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// Raw string literal helper
|
|
263
|
+
// ============================================================================
|
|
264
|
+
/** Emit a raw-string-literal block for C# (lines at column 0, delimiters on own lines). */
|
|
265
|
+
function emitRawStringLiteral(varName, dataLines) {
|
|
266
|
+
const lines = [];
|
|
267
|
+
lines.push(` string ${varName} = """`);
|
|
268
|
+
for (const line of dataLines) {
|
|
269
|
+
lines.push(line);
|
|
270
|
+
}
|
|
271
|
+
lines.push('""";');
|
|
272
|
+
return lines;
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=test-emitter.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C# expression visitor — Expr IR → C# source fragments.
|
|
3
|
+
*/
|
|
4
|
+
import { Expr, TypeRegistry } from "../../ir/expansion.js";
|
|
5
|
+
import { ExprVisitor } from "../../ir/visitor.js";
|
|
6
|
+
export declare class CSharpExprVisitor implements ExprVisitor {
|
|
7
|
+
registry?: TypeRegistry;
|
|
8
|
+
constructor(registry?: TypeRegistry);
|
|
9
|
+
visitExpr(expr: Expr): string;
|
|
10
|
+
private visitConstruct;
|
|
11
|
+
private visitVariant;
|
|
12
|
+
private visitArray;
|
|
13
|
+
private escapeString;
|
|
14
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C# expression visitor — Expr IR → C# source fragments.
|
|
3
|
+
*/
|
|
4
|
+
import { toPascalCase, assertNever } from "../../ir/visitor.js";
|
|
5
|
+
export class CSharpExprVisitor {
|
|
6
|
+
registry;
|
|
7
|
+
constructor(registry) {
|
|
8
|
+
this.registry = registry;
|
|
9
|
+
}
|
|
10
|
+
visitExpr(expr) {
|
|
11
|
+
switch (expr.kind) {
|
|
12
|
+
case "string":
|
|
13
|
+
return `"${this.escapeString(expr.value)}"`;
|
|
14
|
+
case "number":
|
|
15
|
+
return String(expr.value);
|
|
16
|
+
case "boolean":
|
|
17
|
+
return expr.value ? "true" : "false";
|
|
18
|
+
case "null":
|
|
19
|
+
return "null";
|
|
20
|
+
case "param":
|
|
21
|
+
return expr.name;
|
|
22
|
+
case "construct":
|
|
23
|
+
return this.visitConstruct(expr);
|
|
24
|
+
case "variant":
|
|
25
|
+
return this.visitVariant(expr);
|
|
26
|
+
case "array":
|
|
27
|
+
return this.visitArray(expr);
|
|
28
|
+
case "dict":
|
|
29
|
+
return `new Dictionary<string, object?> { ${expr.entries.map(e => `{ "${e.key}", ${this.visitExpr(e.value)} }`).join(", ")} }`;
|
|
30
|
+
case "field_read":
|
|
31
|
+
return `${expr.objectName}.${toPascalCase(expr.fieldName)}`;
|
|
32
|
+
default:
|
|
33
|
+
return assertNever(expr);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
visitConstruct(expr) {
|
|
37
|
+
const typeName = expr.typeName.name;
|
|
38
|
+
if (expr.fields.length === 0) {
|
|
39
|
+
return `new ${typeName}()`;
|
|
40
|
+
}
|
|
41
|
+
// C# uses object initializer syntax: new Type { Prop = value }
|
|
42
|
+
const fields = expr.fields.map(f => {
|
|
43
|
+
let val = this.visitExpr(f.value);
|
|
44
|
+
// For enum fields, convert string literals to EnumName.MemberName
|
|
45
|
+
if (f.value.kind === "string" && this.registry) {
|
|
46
|
+
const typeNode = this.registry.get(typeName);
|
|
47
|
+
if (typeNode) {
|
|
48
|
+
const prop = typeNode.properties.find(p => p.name === f.propertyName);
|
|
49
|
+
if (prop?.enumName) {
|
|
50
|
+
val = `${prop.enumName}.${toPascalCase(f.value.value)}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return `${toPascalCase(f.propertyName)} = ${val}`;
|
|
55
|
+
}).join(", ");
|
|
56
|
+
return `new ${typeName} { ${fields} }`;
|
|
57
|
+
}
|
|
58
|
+
visitVariant(expr) {
|
|
59
|
+
// In C#, polymorphic children are full classes
|
|
60
|
+
const variantName = expr.variantTypeName.name;
|
|
61
|
+
if (expr.fields.length === 0) {
|
|
62
|
+
return `new ${variantName}()`;
|
|
63
|
+
}
|
|
64
|
+
const fields = expr.fields.map(f => `${toPascalCase(f.propertyName)} = ${this.visitExpr(f.value)}`).join(", ");
|
|
65
|
+
return `new ${variantName} { ${fields} }`;
|
|
66
|
+
}
|
|
67
|
+
visitArray(expr) {
|
|
68
|
+
const elementType = expr.elementTypeName.name;
|
|
69
|
+
if (expr.items.length === 0) {
|
|
70
|
+
return `new List<${elementType}>()`;
|
|
71
|
+
}
|
|
72
|
+
const items = expr.items.map(i => this.visitExpr(i)).join(", ");
|
|
73
|
+
return `new List<${elementType}> { ${items} }`;
|
|
74
|
+
}
|
|
75
|
+
escapeString(s) {
|
|
76
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=visitor.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { EmitContext } from "@typespec/compiler";
|
|
2
|
+
import { EmitTarget, TypraEmitterOptions } from "../../lib.js";
|
|
3
|
+
import { TypeNode } from "../../ir/ast.js";
|
|
4
|
+
import { GeneratorOptions } from "../../emitter.js";
|
|
5
|
+
/**
|
|
6
|
+
* Type mapping from TypeSpec scalar types to Go types.
|
|
7
|
+
*/
|
|
8
|
+
export declare const goTypeMapper: Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* Main entry point for Go code generation.
|
|
11
|
+
*/
|
|
12
|
+
export declare const generateGo: (context: EmitContext<TypraEmitterOptions>, node: TypeNode, emitTarget: EmitTarget, options?: GeneratorOptions) => Promise<void>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { resolvePath } from "@typespec/compiler";
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { enumerateTypes, } from "../../ir/ast.js";
|
|
5
|
+
import { filterNodes } from "../../emitter.js";
|
|
6
|
+
import { buildBaseTestContext, goTestOptions } from "../../testing/test-context.js";
|
|
7
|
+
import { toSnakeCase } from "../../ir/utilities.js";
|
|
8
|
+
import { TypeRegistry } from "../../ir/expansion.js";
|
|
9
|
+
import { GoExprVisitor } from "./visitor.js";
|
|
10
|
+
import { lowerFile } from "../../ir/lower.js";
|
|
11
|
+
import { emitGoFileContent } from "./emitter.js";
|
|
12
|
+
import { emitGoContext } from "./scaffolding.js";
|
|
13
|
+
import { emitGoTest } from "./test-emitter.js";
|
|
14
|
+
import { emitGeneratedFile } from "../../cleanup/generated-file.js";
|
|
15
|
+
/**
|
|
16
|
+
* Type mapping from TypeSpec scalar types to Go types.
|
|
17
|
+
*/
|
|
18
|
+
export const goTypeMapper = {
|
|
19
|
+
"string": "string",
|
|
20
|
+
"number": "float64",
|
|
21
|
+
"array": "[]",
|
|
22
|
+
"object": "map[string]interface{}",
|
|
23
|
+
"boolean": "bool",
|
|
24
|
+
"int64": "int64",
|
|
25
|
+
"int32": "int32",
|
|
26
|
+
"float64": "float64",
|
|
27
|
+
"float32": "float32",
|
|
28
|
+
"integer": "int",
|
|
29
|
+
"float": "float64",
|
|
30
|
+
"numeric": "float64",
|
|
31
|
+
"any": "interface{}",
|
|
32
|
+
"dictionary": "map[string]interface{}",
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Main entry point for Go code generation.
|
|
36
|
+
*/
|
|
37
|
+
export const generateGo = async (context, node, emitTarget, options) => {
|
|
38
|
+
const allTypes = Array.from(enumerateTypes(node));
|
|
39
|
+
const nodes = filterNodes(allTypes, options);
|
|
40
|
+
// Build the expression IR infrastructure
|
|
41
|
+
const registry = TypeRegistry.fromTypeGraph(allTypes);
|
|
42
|
+
const visitor = new GoExprVisitor(registry);
|
|
43
|
+
// Determine package name from root node namespace (e.g., "Typra" -> "typra")
|
|
44
|
+
const packageName = node.typeName.namespace.toLowerCase().replace(/\./g, '');
|
|
45
|
+
// Collect all polymorphic type names across all nodes
|
|
46
|
+
const polymorphicTypeNames = new Set();
|
|
47
|
+
for (const n of nodes) {
|
|
48
|
+
const polyTypes = n.retrievePolymorphicTypes();
|
|
49
|
+
if (polyTypes) {
|
|
50
|
+
polymorphicTypeNames.add(n.typeName.name);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Emit context file (LoadContext/SaveContext utilities)
|
|
54
|
+
const contextContent = emitGoContext({ header: "Typra Context", packageName });
|
|
55
|
+
await emitGoFile(context, 'context.go', contextContent, emitTarget["output-dir"]);
|
|
56
|
+
// Emit each base type and its children as a single file (Go stays flat — no subfolders)
|
|
57
|
+
for (const n of nodes) {
|
|
58
|
+
// Skip child types - they're rendered with their parent
|
|
59
|
+
if (!n.base) {
|
|
60
|
+
const fileDecl = lowerFile(n, registry, polymorphicTypeNames);
|
|
61
|
+
// Go stays flat: pass group as a header comment only, no subfolder emission
|
|
62
|
+
const fileContent = emitGoFileContent(fileDecl.types, packageName, visitor, polymorphicTypeNames, fileDecl.enums, n.group || "");
|
|
63
|
+
const fileName = toSnakeCase(n.typeName.name) + '.go';
|
|
64
|
+
await emitGoFile(context, fileName, fileContent, emitTarget["output-dir"]);
|
|
65
|
+
}
|
|
66
|
+
// Emit test file for each type (skip protocols — they have no data to test)
|
|
67
|
+
if (emitTarget["test-dir"] && !n.isProtocol) {
|
|
68
|
+
const importPath = emitTarget["import-path"] || packageName;
|
|
69
|
+
const testContext = { ...buildTestContext(n, packageName), importPath };
|
|
70
|
+
const testContent = emitGoTest(testContext);
|
|
71
|
+
const testFileName = toSnakeCase(n.typeName.name) + '_test.go';
|
|
72
|
+
await emitGoFile(context, testFileName, testContent, emitTarget["test-dir"]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Format emitted files if format option is enabled (default: true)
|
|
76
|
+
if (emitTarget.format !== false) {
|
|
77
|
+
const outputDir = emitTarget["output-dir"]
|
|
78
|
+
? resolve(process.cwd(), emitTarget["output-dir"])
|
|
79
|
+
: context.emitterOutputDir;
|
|
80
|
+
const testDir = emitTarget["test-dir"]
|
|
81
|
+
? resolve(process.cwd(), emitTarget["test-dir"])
|
|
82
|
+
: undefined;
|
|
83
|
+
formatGoFiles(outputDir, testDir);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Format Go files using gofmt and goimports.
|
|
88
|
+
*/
|
|
89
|
+
function formatGoFiles(outputDir, testDir) {
|
|
90
|
+
const dirs = [outputDir, ...(testDir ? [testDir] : [])];
|
|
91
|
+
for (const dir of dirs) {
|
|
92
|
+
// Run gofmt — use execFileSync to avoid shell injection
|
|
93
|
+
try {
|
|
94
|
+
execFileSync("gofmt", ["-w", dir], {
|
|
95
|
+
stdio: 'pipe',
|
|
96
|
+
encoding: 'utf-8'
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
console.warn(`Warning: gofmt formatting failed for ${dir}. You may need to install Go.`);
|
|
101
|
+
}
|
|
102
|
+
// Run goimports if available
|
|
103
|
+
try {
|
|
104
|
+
execFileSync("goimports", ["-w", dir], {
|
|
105
|
+
stdio: 'pipe',
|
|
106
|
+
encoding: 'utf-8'
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
// goimports is optional, don't warn if not available
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build context for rendering a test file.
|
|
116
|
+
*/
|
|
117
|
+
function buildTestContext(node, packageName) {
|
|
118
|
+
return buildBaseTestContext(node, packageName, goTestOptions);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Write generated Go content to file.
|
|
122
|
+
*/
|
|
123
|
+
async function emitGoFile(context, filename, content, outputDir) {
|
|
124
|
+
outputDir = outputDir || `${context.emitterOutputDir}/go`;
|
|
125
|
+
const filePath = resolvePath(outputDir, filename);
|
|
126
|
+
await emitGeneratedFile(context, filePath, content);
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=driver.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Go code emitter — Declaration IR → Go source code.
|
|
3
|
+
*
|
|
4
|
+
* Replaces `file.go.njk` Nunjucks template with a typed TypeScript function
|
|
5
|
+
* that walks the TypeDecl tree and produces a complete Go source file.
|
|
6
|
+
*
|
|
7
|
+
* Each file contains one type hierarchy (parent + children).
|
|
8
|
+
* Go has no inheritance — child structs duplicate parent fields.
|
|
9
|
+
* Polymorphic types use interface{} and type switches.
|
|
10
|
+
*
|
|
11
|
+
* Structural blocks emitted per type (in order):
|
|
12
|
+
* 1. Description comment
|
|
13
|
+
* 2. Struct definition
|
|
14
|
+
* 3. Load function
|
|
15
|
+
* 4. Save method
|
|
16
|
+
* 5. ToJSON method
|
|
17
|
+
* 6. ToYAML method
|
|
18
|
+
* 7. FromJSON function
|
|
19
|
+
* 8. FromYAML function
|
|
20
|
+
*/
|
|
21
|
+
import { TypeDecl, EnumDef } from "../../ir/declarations.js";
|
|
22
|
+
import { ExprVisitor } from "../../ir/visitor.js";
|
|
23
|
+
/**
|
|
24
|
+
* Emit a complete Go source file for a type hierarchy.
|
|
25
|
+
*
|
|
26
|
+
* @param types - All TypeDecls in this file (parent first, then children)
|
|
27
|
+
* @param packageName - Go package name (lowercase)
|
|
28
|
+
* @param visitor - Expression visitor for coercion rendering
|
|
29
|
+
* @param polymorphicTypeNames - Set of type names that are polymorphic bases
|
|
30
|
+
* @param enums - Enum definitions used in this file
|
|
31
|
+
* @param group - Semantic group from TSP source subfolder (used as header comment only; Go stays flat)
|
|
32
|
+
*/
|
|
33
|
+
export declare function emitGoFileContent(types: TypeDecl[], packageName: string, visitor: ExprVisitor, polymorphicTypeNames: Set<string>, enums?: EnumDef[], group?: string): string;
|