@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,1140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C# code emitter — Declaration IR → C# source code.
|
|
3
|
+
*
|
|
4
|
+
* Replaces `file.cs.njk` Nunjucks template with a typed TypeScript function
|
|
5
|
+
* that walks the TypeDecl tree and produces a complete C# class file.
|
|
6
|
+
*
|
|
7
|
+
* Each TypeDecl becomes one C# file (one class per file).
|
|
8
|
+
* Polymorphic types use abstract class + child : Base inheritance.
|
|
9
|
+
*
|
|
10
|
+
* Structural blocks emitted (in order):
|
|
11
|
+
* 1. Copyright + using directives
|
|
12
|
+
* 2. Namespace
|
|
13
|
+
* 3. XML doc comment
|
|
14
|
+
* 4. Class declaration
|
|
15
|
+
* 5. ShorthandProperty
|
|
16
|
+
* 6. Constructor
|
|
17
|
+
* 7. Properties
|
|
18
|
+
* 8. #region Load Methods
|
|
19
|
+
* 9. #region Save Methods
|
|
20
|
+
* 10. #region Factory Methods (if any)
|
|
21
|
+
* 11. #region Helpers (if any)
|
|
22
|
+
*/
|
|
23
|
+
import { toPascalCase } from "../../ir/visitor.js";
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Type maps
|
|
26
|
+
// ============================================================================
|
|
27
|
+
const CSHARP_TYPE_MAP = {
|
|
28
|
+
string: "string",
|
|
29
|
+
number: "float",
|
|
30
|
+
boolean: "bool",
|
|
31
|
+
int32: "int",
|
|
32
|
+
int64: "long",
|
|
33
|
+
float32: "float",
|
|
34
|
+
float64: "double",
|
|
35
|
+
integer: "int",
|
|
36
|
+
object: "object",
|
|
37
|
+
unknown: "object",
|
|
38
|
+
any: "object",
|
|
39
|
+
dictionary: "IDictionary<string, object>",
|
|
40
|
+
array: "IList<object>",
|
|
41
|
+
};
|
|
42
|
+
const CONVERT_MAP = {
|
|
43
|
+
bool: "Boolean",
|
|
44
|
+
int: "Int32",
|
|
45
|
+
long: "Int64",
|
|
46
|
+
float: "Single",
|
|
47
|
+
double: "Double",
|
|
48
|
+
};
|
|
49
|
+
const NON_NULLABLE_VALUE_TYPES = new Set(["bool", "int", "long", "float", "double"]);
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Main entry point
|
|
52
|
+
// ============================================================================
|
|
53
|
+
/**
|
|
54
|
+
* Emit a complete C# class file for a single TypeDecl.
|
|
55
|
+
*/
|
|
56
|
+
export function emitCSharpClass(type, namespace, visitor, allTypes, findType) {
|
|
57
|
+
const lines = [];
|
|
58
|
+
// Protocol types → emit as interface
|
|
59
|
+
if (type.isProtocol) {
|
|
60
|
+
return emitCSharpInterface(type, namespace, lines);
|
|
61
|
+
}
|
|
62
|
+
// Header
|
|
63
|
+
emitHeader(lines, namespace);
|
|
64
|
+
// Class doc comment
|
|
65
|
+
emitXmlDocComment(type.description, " ", lines);
|
|
66
|
+
// Class declaration
|
|
67
|
+
emitClassDeclaration(type, lines);
|
|
68
|
+
// ShorthandProperty
|
|
69
|
+
emitShorthandProperty(type, lines);
|
|
70
|
+
// Constructor
|
|
71
|
+
emitConstructor(type, lines);
|
|
72
|
+
// Properties
|
|
73
|
+
emitProperties(type, allTypes, findType, lines);
|
|
74
|
+
// Load region
|
|
75
|
+
emitLoadRegion(type, allTypes, findType, lines);
|
|
76
|
+
// Save region
|
|
77
|
+
emitSaveRegion(type, allTypes, findType, lines);
|
|
78
|
+
// Factory methods
|
|
79
|
+
if (type.factories.length > 0) {
|
|
80
|
+
emitFactoryRegion(type, visitor, lines);
|
|
81
|
+
}
|
|
82
|
+
// Helper stubs
|
|
83
|
+
if (type.methods.length > 0) {
|
|
84
|
+
emitHelperRegion(type, lines);
|
|
85
|
+
}
|
|
86
|
+
// Close class
|
|
87
|
+
lines.push("}");
|
|
88
|
+
// Emit I<Name>Helpers interface after the class
|
|
89
|
+
if (type.methods.length > 0) {
|
|
90
|
+
emitHelperInterface(type, lines);
|
|
91
|
+
}
|
|
92
|
+
lines.push("");
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Emit a C# enum file for a named string-literal type.
|
|
97
|
+
* Uses JsonStringEnumConverter for JSON serialization.
|
|
98
|
+
*/
|
|
99
|
+
export function emitCSharpEnum(enumDef, namespace) {
|
|
100
|
+
const lines = [];
|
|
101
|
+
// Header
|
|
102
|
+
lines.push("// <auto-generated/>");
|
|
103
|
+
lines.push("// Code generated by Typra emitter; DO NOT EDIT.");
|
|
104
|
+
lines.push("");
|
|
105
|
+
lines.push("using System.Text.Json.Serialization;");
|
|
106
|
+
lines.push("");
|
|
107
|
+
lines.push(`namespace ${namespace};`);
|
|
108
|
+
lines.push("");
|
|
109
|
+
// Use string converter for JSON round-tripping
|
|
110
|
+
const csEnumName = toPascalCase(enumDef.name);
|
|
111
|
+
lines.push("[JsonConverter(typeof(JsonStringEnumConverter))]");
|
|
112
|
+
lines.push(`public enum ${csEnumName}`);
|
|
113
|
+
lines.push("{");
|
|
114
|
+
for (const value of enumDef.values) {
|
|
115
|
+
const memberName = toPascalCase(value);
|
|
116
|
+
// Add EnumMember attribute if the wire value doesn't match the member name exactly
|
|
117
|
+
lines.push(` [JsonPropertyName("${value}")]`);
|
|
118
|
+
lines.push(` ${memberName},`);
|
|
119
|
+
lines.push("");
|
|
120
|
+
}
|
|
121
|
+
lines.push("}");
|
|
122
|
+
lines.push("");
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
}
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// Protocol interface emission
|
|
127
|
+
// ============================================================================
|
|
128
|
+
/** Map a protocol type string to a C# type. */
|
|
129
|
+
function protocolCSharpType(typeStr) {
|
|
130
|
+
// Handle array types
|
|
131
|
+
if (typeStr.endsWith("[]")) {
|
|
132
|
+
const inner = typeStr.slice(0, -2);
|
|
133
|
+
return `List<${protocolCSharpType(inner)}>`;
|
|
134
|
+
}
|
|
135
|
+
if (typeStr === "Record<unknown>" || typeStr === "dictionary")
|
|
136
|
+
return "Dictionary<string, object?>";
|
|
137
|
+
if (typeStr === "unknown" || typeStr === "any")
|
|
138
|
+
return "object";
|
|
139
|
+
// Handle nullable types (e.g., "string?")
|
|
140
|
+
if (typeStr.endsWith("?")) {
|
|
141
|
+
const inner = typeStr.slice(0, -1);
|
|
142
|
+
return `${protocolCSharpType(inner)}?`;
|
|
143
|
+
}
|
|
144
|
+
return CSHARP_TYPE_MAP[typeStr] || typeStr;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Emit a C# interface for a protocol type.
|
|
148
|
+
*/
|
|
149
|
+
function emitCSharpInterface(type, namespace, lines) {
|
|
150
|
+
const name = type.typeName.name;
|
|
151
|
+
// Header (simplified — no YAML/JSON imports needed for interfaces)
|
|
152
|
+
lines.push("// Copyright (c) Microsoft. All rights reserved.");
|
|
153
|
+
lines.push("");
|
|
154
|
+
lines.push("#pragma warning disable IDE0130");
|
|
155
|
+
lines.push(`namespace ${namespace};`);
|
|
156
|
+
lines.push("#pragma warning restore IDE0130");
|
|
157
|
+
lines.push("");
|
|
158
|
+
// XML doc
|
|
159
|
+
if (type.description) {
|
|
160
|
+
emitXmlDocComment(type.description, " ", lines);
|
|
161
|
+
}
|
|
162
|
+
lines.push(` public interface I${name}`);
|
|
163
|
+
lines.push(" {");
|
|
164
|
+
for (const method of type.methods) {
|
|
165
|
+
if (method.description) {
|
|
166
|
+
emitXmlDocComment(method.description, " ", lines);
|
|
167
|
+
}
|
|
168
|
+
const params = Object.entries(method.params)
|
|
169
|
+
.map(([pName, pType]) => `${protocolCSharpType(pType)} ${pName}`)
|
|
170
|
+
.join(", ");
|
|
171
|
+
const ret = protocolCSharpType(method.returns);
|
|
172
|
+
if (method.sync) {
|
|
173
|
+
// Synchronous method
|
|
174
|
+
if (method.optional) {
|
|
175
|
+
// Return type already includes nullability — provide default body
|
|
176
|
+
lines.push(` ${ret} ${toPascalCase(method.name)}(${params}) => default!;`);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
lines.push(` ${ret} ${toPascalCase(method.name)}(${params});`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// Async method
|
|
184
|
+
if (method.optional) {
|
|
185
|
+
lines.push(` Task<${ret}> ${toPascalCase(method.name)}Async(${params}) => Task.FromResult<${ret}>(default!);`);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
lines.push(` Task<${ret}> ${toPascalCase(method.name)}Async(${params});`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
lines.push(" }");
|
|
193
|
+
lines.push("");
|
|
194
|
+
return lines.join("\n");
|
|
195
|
+
}
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// Header & namespace
|
|
198
|
+
// ============================================================================
|
|
199
|
+
function emitHeader(lines, namespace) {
|
|
200
|
+
lines.push("// Copyright (c) Microsoft. All rights reserved.");
|
|
201
|
+
lines.push("using System.Text.Json;");
|
|
202
|
+
lines.push("using YamlDotNet.Serialization;");
|
|
203
|
+
lines.push("");
|
|
204
|
+
lines.push("#pragma warning disable IDE0130");
|
|
205
|
+
lines.push(`namespace ${namespace};`);
|
|
206
|
+
lines.push("#pragma warning restore IDE0130");
|
|
207
|
+
lines.push("");
|
|
208
|
+
}
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// XML doc comment
|
|
211
|
+
// ============================================================================
|
|
212
|
+
function emitXmlDocComment(description, indent, lines) {
|
|
213
|
+
lines.push(`${indent}/// <summary>`);
|
|
214
|
+
// Split multi-line descriptions into separate /// lines
|
|
215
|
+
const descLines = description.split(/\r?\n/).map(l => l.trim()).filter(l => l.length > 0);
|
|
216
|
+
for (let i = 0; i < descLines.length; i++) {
|
|
217
|
+
lines.push(`${indent}/// ${descLines[i]}`);
|
|
218
|
+
// Add empty /// line between paragraphs (if multiple lines and not the last)
|
|
219
|
+
if (descLines.length > 1 && i < descLines.length - 1) {
|
|
220
|
+
lines.push(`${indent}///`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
lines.push(`${indent}/// </summary>`);
|
|
224
|
+
}
|
|
225
|
+
// ============================================================================
|
|
226
|
+
// Class declaration
|
|
227
|
+
// ============================================================================
|
|
228
|
+
function emitClassDeclaration(type, lines) {
|
|
229
|
+
const abstract_ = type.isAbstract ? "abstract " : "";
|
|
230
|
+
const bases = [];
|
|
231
|
+
if (type.base)
|
|
232
|
+
bases.push(type.base.name);
|
|
233
|
+
if (type.methods.length > 0)
|
|
234
|
+
bases.push(`I${type.typeName.name}Helpers`);
|
|
235
|
+
const baseSuffix = bases.length > 0 ? ` : ${bases.join(", ")}` : "";
|
|
236
|
+
lines.push(`public ${abstract_}partial class ${type.typeName.name}${baseSuffix}`);
|
|
237
|
+
lines.push("{");
|
|
238
|
+
}
|
|
239
|
+
// ============================================================================
|
|
240
|
+
// ShorthandProperty
|
|
241
|
+
// ============================================================================
|
|
242
|
+
function emitShorthandProperty(type, lines) {
|
|
243
|
+
const new_ = type.base ? "new " : "";
|
|
244
|
+
const value = type.coercionProperty ? `"${type.coercionProperty}"` : "null";
|
|
245
|
+
lines.push(" /// <summary>");
|
|
246
|
+
lines.push(" /// The shorthand property name for this type, if any.");
|
|
247
|
+
lines.push(" /// </summary>");
|
|
248
|
+
lines.push(` public ${new_}static string? ShorthandProperty => ${value};`);
|
|
249
|
+
lines.push("");
|
|
250
|
+
}
|
|
251
|
+
// ============================================================================
|
|
252
|
+
// Constructor
|
|
253
|
+
// ============================================================================
|
|
254
|
+
function emitConstructor(type, lines) {
|
|
255
|
+
const access = type.isAbstract ? "protected" : "public";
|
|
256
|
+
lines.push(" /// <summary>");
|
|
257
|
+
lines.push(` /// Initializes a new instance of <see cref="${type.typeName.name}"/>.`);
|
|
258
|
+
lines.push(" /// </summary>");
|
|
259
|
+
lines.push("#pragma warning disable CS8618");
|
|
260
|
+
lines.push(` ${access} ${type.typeName.name}()`);
|
|
261
|
+
lines.push(" {");
|
|
262
|
+
lines.push(" }");
|
|
263
|
+
lines.push("#pragma warning restore CS8618");
|
|
264
|
+
lines.push("");
|
|
265
|
+
}
|
|
266
|
+
// ============================================================================
|
|
267
|
+
// Properties
|
|
268
|
+
// ============================================================================
|
|
269
|
+
function emitProperties(type, allTypes, findType, lines) {
|
|
270
|
+
for (const field of type.fields) {
|
|
271
|
+
const modifier = getPropertyModifier(field, type, allTypes, findType);
|
|
272
|
+
const csType = getCSharpType(field.category, field.isOptional, field.enumName, field.isOpenEnum);
|
|
273
|
+
const propName = toPascalCase(field.name);
|
|
274
|
+
const default_ = getPropertyDefault(field);
|
|
275
|
+
emitXmlDocComment(field.description || propName, " ", lines);
|
|
276
|
+
lines.push(` public ${modifier}${csType} ${propName} { get; set; }${default_}`);
|
|
277
|
+
lines.push("");
|
|
278
|
+
}
|
|
279
|
+
lines.push("");
|
|
280
|
+
}
|
|
281
|
+
function getPropertyModifier(field, type, allTypes, findType) {
|
|
282
|
+
// Check if any child type has a field with same name → virtual
|
|
283
|
+
if (type.polymorphicDispatch || type.isAbstract) {
|
|
284
|
+
const childHasSameField = allTypes.some(t => t.base?.name === type.typeName.name &&
|
|
285
|
+
t.fields.some(f => f.name === field.name));
|
|
286
|
+
if (childHasSameField)
|
|
287
|
+
return "virtual ";
|
|
288
|
+
}
|
|
289
|
+
// Check if parent has same field → override
|
|
290
|
+
if (type.base) {
|
|
291
|
+
const parent = findType(type.base.name);
|
|
292
|
+
if (parent?.fields.some(f => f.name === field.name)) {
|
|
293
|
+
return "override ";
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return "";
|
|
297
|
+
}
|
|
298
|
+
function getCSharpType(category, isOptional, enumName, isOpenEnum) {
|
|
299
|
+
// Named enum field — use the PascalCase enum type name (except open enums which stay as string)
|
|
300
|
+
if (enumName && !isOpenEnum) {
|
|
301
|
+
const csName = toPascalCase(enumName);
|
|
302
|
+
return isOptional ? `${csName}?` : csName;
|
|
303
|
+
}
|
|
304
|
+
let baseType;
|
|
305
|
+
switch (category.kind) {
|
|
306
|
+
case "scalar":
|
|
307
|
+
baseType = CSHARP_TYPE_MAP[category.scalarType] || "object";
|
|
308
|
+
break;
|
|
309
|
+
case "complex":
|
|
310
|
+
baseType = category.typeName;
|
|
311
|
+
break;
|
|
312
|
+
case "collection_scalar": {
|
|
313
|
+
const inner = CSHARP_TYPE_MAP[category.scalarType] || "object";
|
|
314
|
+
baseType = `IList<${inner}>`;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
case "collection_complex":
|
|
318
|
+
baseType = `IList<${category.typeName}>`;
|
|
319
|
+
break;
|
|
320
|
+
case "dict":
|
|
321
|
+
baseType = "IDictionary<string, object>";
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
return isOptional ? `${baseType}?` : baseType;
|
|
325
|
+
}
|
|
326
|
+
function getPropertyDefault(field) {
|
|
327
|
+
if (field.isOptional)
|
|
328
|
+
return "";
|
|
329
|
+
// Enum fields use EnumName.MemberName syntax (skip open enums — they use string)
|
|
330
|
+
if (field.enumName && !field.isOpenEnum && field.allowedValues.length > 0) {
|
|
331
|
+
const csEnumName = toPascalCase(field.enumName);
|
|
332
|
+
const dv = typeof field.defaultValue === "string" && field.defaultValue !== "*"
|
|
333
|
+
? field.defaultValue
|
|
334
|
+
: field.allowedValues[0];
|
|
335
|
+
const memberName = toPascalCase(dv);
|
|
336
|
+
return ` = ${csEnumName}.${memberName};`;
|
|
337
|
+
}
|
|
338
|
+
const cat = field.category;
|
|
339
|
+
switch (cat.kind) {
|
|
340
|
+
case "collection_scalar":
|
|
341
|
+
case "collection_complex":
|
|
342
|
+
return " = [];";
|
|
343
|
+
case "dict":
|
|
344
|
+
return " = new Dictionary<string, object>();";
|
|
345
|
+
case "scalar": {
|
|
346
|
+
const csType = CSHARP_TYPE_MAP[cat.scalarType] || "object";
|
|
347
|
+
if (csType === "string") {
|
|
348
|
+
if (field.defaultValue && field.defaultValue !== "*") {
|
|
349
|
+
return ` = "${field.defaultValue}";`;
|
|
350
|
+
}
|
|
351
|
+
return " = string.Empty;";
|
|
352
|
+
}
|
|
353
|
+
if (csType === "bool") {
|
|
354
|
+
return field.defaultValue != null ? ` = ${field.defaultValue};` : " = false;";
|
|
355
|
+
}
|
|
356
|
+
if (csType === "object") {
|
|
357
|
+
return " = new object();";
|
|
358
|
+
}
|
|
359
|
+
// number types: only emit default if there is one
|
|
360
|
+
if (field.defaultValue != null) {
|
|
361
|
+
return ` = ${field.defaultValue};`;
|
|
362
|
+
}
|
|
363
|
+
return "";
|
|
364
|
+
}
|
|
365
|
+
case "complex":
|
|
366
|
+
return "";
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// ============================================================================
|
|
370
|
+
// Load region
|
|
371
|
+
// ============================================================================
|
|
372
|
+
function emitLoadRegion(type, allTypes, findType, lines) {
|
|
373
|
+
lines.push("");
|
|
374
|
+
lines.push(" #region Load Methods");
|
|
375
|
+
lines.push("");
|
|
376
|
+
// Main Load method
|
|
377
|
+
emitLoadMethod(type, allTypes, findType, lines);
|
|
378
|
+
// Collection load helpers
|
|
379
|
+
for (const helper of type.collectionHelpers) {
|
|
380
|
+
emitCollectionLoadHelper(helper, findType, lines);
|
|
381
|
+
}
|
|
382
|
+
// Polymorphic dispatch (LoadKind)
|
|
383
|
+
if (type.polymorphicDispatch) {
|
|
384
|
+
emitLoadKind(type, lines);
|
|
385
|
+
}
|
|
386
|
+
lines.push("");
|
|
387
|
+
lines.push(" #endregion");
|
|
388
|
+
lines.push("");
|
|
389
|
+
}
|
|
390
|
+
function emitLoadMethod(type, allTypes, findType, lines) {
|
|
391
|
+
const typeName = type.typeName.name;
|
|
392
|
+
const new_ = type.base ? "new " : "";
|
|
393
|
+
const hasCoercions = type.load.coercions.length > 0 || type.coercionProperty;
|
|
394
|
+
lines.push(" /// <summary>");
|
|
395
|
+
lines.push(` /// Load a ${typeName} instance from a dictionary.`);
|
|
396
|
+
lines.push(" /// </summary>");
|
|
397
|
+
lines.push(' /// <param name="data">The dictionary containing the data.</param>');
|
|
398
|
+
lines.push(' /// <param name="context">Optional context with pre/post processing callbacks.</param>');
|
|
399
|
+
lines.push(` /// <returns>The loaded ${typeName} instance.</returns>`);
|
|
400
|
+
lines.push(` public ${new_}static ${typeName} Load(Dictionary<string, object?> data, LoadContext? context = null)`);
|
|
401
|
+
lines.push(" {");
|
|
402
|
+
// ProcessInput
|
|
403
|
+
lines.push(" if (context is not null)");
|
|
404
|
+
lines.push(" {");
|
|
405
|
+
lines.push(" data = context.ProcessInput(data);");
|
|
406
|
+
lines.push(" }");
|
|
407
|
+
lines.push("");
|
|
408
|
+
// Note about alternate representations
|
|
409
|
+
if (hasCoercions) {
|
|
410
|
+
lines.push(" // Note: Alternate (shorthand) representations are handled by the converter");
|
|
411
|
+
}
|
|
412
|
+
lines.push("");
|
|
413
|
+
// Instance creation: polymorphic dispatch or direct
|
|
414
|
+
if (type.polymorphicDispatch) {
|
|
415
|
+
lines.push(` // Load polymorphic ${typeName} instance`);
|
|
416
|
+
lines.push(" var instance = LoadKind(data, context);");
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
lines.push(" // Create new instance");
|
|
420
|
+
lines.push(` var instance = new ${typeName}();`);
|
|
421
|
+
}
|
|
422
|
+
lines.push("");
|
|
423
|
+
lines.push("");
|
|
424
|
+
// Per-field assignments
|
|
425
|
+
for (const assign of type.load.assignments) {
|
|
426
|
+
emitLoadAssignment(assign, findType, lines);
|
|
427
|
+
}
|
|
428
|
+
// ProcessOutput
|
|
429
|
+
lines.push(" if (context is not null)");
|
|
430
|
+
lines.push(" {");
|
|
431
|
+
lines.push(` instance = context.ProcessOutput(instance);`);
|
|
432
|
+
lines.push(" }");
|
|
433
|
+
lines.push(" return instance;");
|
|
434
|
+
lines.push(" }");
|
|
435
|
+
lines.push("");
|
|
436
|
+
}
|
|
437
|
+
function emitLoadAssignment(assign, findType, lines) {
|
|
438
|
+
const propName = toPascalCase(assign.fieldName);
|
|
439
|
+
const varName = `${assign.sourceName}Value`;
|
|
440
|
+
lines.push(` if (data.TryGetValue("${assign.sourceName}", out var ${varName}) && ${varName} is not null)`);
|
|
441
|
+
lines.push(" {");
|
|
442
|
+
lines.push(` ${getLoadExpression(assign, propName, varName, findType)}`);
|
|
443
|
+
lines.push(" }");
|
|
444
|
+
lines.push("");
|
|
445
|
+
}
|
|
446
|
+
function getLoadExpression(assign, propName, varName, findType) {
|
|
447
|
+
// Named enum — parse from string (skip open enums — they use string)
|
|
448
|
+
if (assign.enumName && !assign.isOpenEnum) {
|
|
449
|
+
const csEnumName = toPascalCase(assign.enumName);
|
|
450
|
+
return `instance.${propName} = Enum.Parse<${csEnumName}>(${varName}?.ToString()!, true);`;
|
|
451
|
+
}
|
|
452
|
+
const cat = assign.category;
|
|
453
|
+
switch (cat.kind) {
|
|
454
|
+
case "scalar": {
|
|
455
|
+
const csType = CSHARP_TYPE_MAP[cat.scalarType] || "object";
|
|
456
|
+
if (csType === "string") {
|
|
457
|
+
return `instance.${propName} = ${varName}?.ToString()!;`;
|
|
458
|
+
}
|
|
459
|
+
if (NON_NULLABLE_VALUE_TYPES.has(csType)) {
|
|
460
|
+
const convertMethod = CONVERT_MAP[csType];
|
|
461
|
+
return `instance.${propName} = Convert.To${convertMethod}(${varName});`;
|
|
462
|
+
}
|
|
463
|
+
// object/unknown
|
|
464
|
+
return `instance.${propName} = ${varName};`;
|
|
465
|
+
}
|
|
466
|
+
case "dict":
|
|
467
|
+
return `instance.${propName} = ${varName}.GetDictionary()!;`;
|
|
468
|
+
case "complex":
|
|
469
|
+
return `instance.${propName} = ${cat.typeName}.Load(${varName}.GetDictionary(${cat.typeName}.ShorthandProperty), context);`;
|
|
470
|
+
case "collection_complex":
|
|
471
|
+
return `instance.${propName} = Load${propName}(${varName}, context);`;
|
|
472
|
+
case "collection_scalar": {
|
|
473
|
+
const csType = CSHARP_TYPE_MAP[cat.scalarType] || "object";
|
|
474
|
+
if (csType === "string") {
|
|
475
|
+
return `instance.${propName} = (${varName} as IEnumerable<object>)?.Select(x => x?.ToString()!).ToList() ?? [];`;
|
|
476
|
+
}
|
|
477
|
+
if (NON_NULLABLE_VALUE_TYPES.has(csType)) {
|
|
478
|
+
const convertMethod = CONVERT_MAP[csType];
|
|
479
|
+
return `instance.${propName} = (${varName} as IEnumerable<object>)?.Select(x => Convert.To${convertMethod}(x)).ToList() ?? [];`;
|
|
480
|
+
}
|
|
481
|
+
return `instance.${propName} = (${varName} as IEnumerable<object>)?.ToList() ?? [];`;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// ============================================================================
|
|
486
|
+
// Collection Load Helper
|
|
487
|
+
// ============================================================================
|
|
488
|
+
function emitCollectionLoadHelper(helper, findType, lines) {
|
|
489
|
+
const propName = toPascalCase(helper.propertyName);
|
|
490
|
+
const elemType = helper.elementTypeName.name;
|
|
491
|
+
// Determine primary property for scalar shorthand in dict format
|
|
492
|
+
const elemTypeDecl = findType(elemType);
|
|
493
|
+
let primaryProp = null;
|
|
494
|
+
if (elemTypeDecl?.coercionProperty) {
|
|
495
|
+
primaryProp = elemTypeDecl.coercionProperty;
|
|
496
|
+
}
|
|
497
|
+
else if (helper.innerFields.length > 0) {
|
|
498
|
+
primaryProp = helper.innerFields[0];
|
|
499
|
+
}
|
|
500
|
+
lines.push("");
|
|
501
|
+
lines.push(" /// <summary>");
|
|
502
|
+
lines.push(` /// Load a list of ${elemType} from a dictionary or list.`);
|
|
503
|
+
lines.push(" /// </summary>");
|
|
504
|
+
lines.push(` public static IList<${elemType}> Load${propName}(object data, LoadContext? context)`);
|
|
505
|
+
lines.push(" {");
|
|
506
|
+
lines.push(` var result = new List<${elemType}>();`);
|
|
507
|
+
lines.push("");
|
|
508
|
+
lines.push(" if (data is Dictionary<string, object?> dict)");
|
|
509
|
+
lines.push(" {");
|
|
510
|
+
lines.push(" // Convert named dictionary to list");
|
|
511
|
+
lines.push(" foreach (var kvp in dict)");
|
|
512
|
+
lines.push(" {");
|
|
513
|
+
lines.push(" if (kvp.Value is IEnumerable<object>)");
|
|
514
|
+
lines.push(" {");
|
|
515
|
+
lines.push(" throw new ArgumentException(");
|
|
516
|
+
lines.push(` $"Invalid '${helper.propertyName}' format: key '{kvp.Key}' has an array value. " +`);
|
|
517
|
+
lines.push(` $"'${helper.propertyName}' must be a flat list of objects or a name-keyed dict — " +`);
|
|
518
|
+
lines.push(` "not a nested {" + kvp.Key + ": [...]} structure.");`);
|
|
519
|
+
lines.push(" }");
|
|
520
|
+
lines.push(" var itemDict = kvp.Value.GetDictionary();");
|
|
521
|
+
lines.push(" if (itemDict.Count > 0)");
|
|
522
|
+
lines.push(" {");
|
|
523
|
+
lines.push(" // Value is an object, add name to it");
|
|
524
|
+
lines.push(' itemDict["name"] = kvp.Key;');
|
|
525
|
+
lines.push(` result.Add(${elemType}.Load(itemDict, context));`);
|
|
526
|
+
lines.push(" }");
|
|
527
|
+
lines.push(" else");
|
|
528
|
+
lines.push(" {");
|
|
529
|
+
lines.push(" // Value is a scalar, use it as the primary property");
|
|
530
|
+
lines.push(" var newDict = new Dictionary<string, object?>");
|
|
531
|
+
lines.push(" {");
|
|
532
|
+
lines.push(' ["name"] = kvp.Key,');
|
|
533
|
+
lines.push(` ["${primaryProp || ""}"] = kvp.Value`);
|
|
534
|
+
lines.push(" };");
|
|
535
|
+
lines.push(` result.Add(${elemType}.Load(newDict, context));`);
|
|
536
|
+
lines.push(" }");
|
|
537
|
+
lines.push(" }");
|
|
538
|
+
lines.push(" }");
|
|
539
|
+
lines.push(" else if (data is IEnumerable<object> list)");
|
|
540
|
+
lines.push(" {");
|
|
541
|
+
lines.push(" foreach (var item in list)");
|
|
542
|
+
lines.push(" {");
|
|
543
|
+
lines.push(` var itemDict = item.GetDictionary(${elemType}.ShorthandProperty);`);
|
|
544
|
+
lines.push(" if (itemDict.Count > 0)");
|
|
545
|
+
lines.push(" {");
|
|
546
|
+
lines.push(` result.Add(${elemType}.Load(itemDict, context));`);
|
|
547
|
+
lines.push(" }");
|
|
548
|
+
lines.push(" }");
|
|
549
|
+
lines.push(" }");
|
|
550
|
+
lines.push("");
|
|
551
|
+
lines.push(" return result;");
|
|
552
|
+
lines.push(" }");
|
|
553
|
+
lines.push("");
|
|
554
|
+
}
|
|
555
|
+
// ============================================================================
|
|
556
|
+
// LoadKind (polymorphic dispatch)
|
|
557
|
+
// ============================================================================
|
|
558
|
+
function emitLoadKind(type, lines) {
|
|
559
|
+
const dispatch = type.polymorphicDispatch;
|
|
560
|
+
const typeName = type.typeName.name;
|
|
561
|
+
lines.push("");
|
|
562
|
+
lines.push(" /// <summary>");
|
|
563
|
+
lines.push(` /// Load polymorphic ${typeName} based on discriminator.`);
|
|
564
|
+
lines.push(" /// </summary>");
|
|
565
|
+
lines.push(` private static ${typeName} LoadKind(Dictionary<string, object?> data, LoadContext? context)`);
|
|
566
|
+
lines.push(" {");
|
|
567
|
+
lines.push(` if (data.TryGetValue("${dispatch.discriminatorField}", out var discriminatorValue) && discriminatorValue is not null)`);
|
|
568
|
+
lines.push(" {");
|
|
569
|
+
lines.push(" var discriminator = discriminatorValue.ToString()?.ToLowerInvariant();");
|
|
570
|
+
lines.push(" return discriminator switch");
|
|
571
|
+
lines.push(" {");
|
|
572
|
+
for (const variant of dispatch.variants) {
|
|
573
|
+
lines.push(` "${variant.value}" => ${variant.typeName.name}.Load(data, context),`);
|
|
574
|
+
}
|
|
575
|
+
// Default handling
|
|
576
|
+
if (dispatch.defaultVariant) {
|
|
577
|
+
if (dispatch.defaultVariant.isSelfReference) {
|
|
578
|
+
lines.push(` _ => new ${typeName}(),`);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
lines.push(` _ => ${dispatch.defaultVariant.typeName.name}.Load(data, context),`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
else if (dispatch.isAbstract) {
|
|
585
|
+
lines.push(` _ => throw new ArgumentException($"Unknown ${typeName} discriminator value: {discriminator}"),`);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
lines.push(` _ => new ${typeName}(),`);
|
|
589
|
+
}
|
|
590
|
+
lines.push(" };");
|
|
591
|
+
lines.push(" }");
|
|
592
|
+
lines.push("");
|
|
593
|
+
// Fallback when discriminator property is missing
|
|
594
|
+
if (dispatch.isAbstract && !dispatch.defaultVariant) {
|
|
595
|
+
lines.push(` throw new ArgumentException("Missing ${typeName} discriminator property: '${dispatch.discriminatorField}'");`);
|
|
596
|
+
}
|
|
597
|
+
else if (dispatch.defaultVariant && !dispatch.defaultVariant.isSelfReference) {
|
|
598
|
+
lines.push(` throw new ArgumentException("Missing ${typeName} discriminator property: '${dispatch.discriminatorField}'");`);
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
lines.push(` return new ${typeName}();`);
|
|
602
|
+
}
|
|
603
|
+
lines.push("");
|
|
604
|
+
lines.push(" }");
|
|
605
|
+
lines.push("");
|
|
606
|
+
}
|
|
607
|
+
// ============================================================================
|
|
608
|
+
// Save region
|
|
609
|
+
// ============================================================================
|
|
610
|
+
function emitSaveRegion(type, allTypes, findType, lines) {
|
|
611
|
+
lines.push(" #region Save Methods");
|
|
612
|
+
lines.push("");
|
|
613
|
+
// Main Save method
|
|
614
|
+
emitSaveMethod(type, allTypes, lines);
|
|
615
|
+
// ToWire method (only when wire mappings exist)
|
|
616
|
+
if (type.wire) {
|
|
617
|
+
emitToWireMethod(type, lines);
|
|
618
|
+
}
|
|
619
|
+
// Collection save helpers
|
|
620
|
+
for (const helper of type.collectionHelpers) {
|
|
621
|
+
emitCollectionSaveHelper(helper, lines);
|
|
622
|
+
}
|
|
623
|
+
// ToYaml, ToJson, FromJson, FromYaml
|
|
624
|
+
emitSerializationMethods(type, lines);
|
|
625
|
+
lines.push(" #endregion");
|
|
626
|
+
}
|
|
627
|
+
function emitSaveMethod(type, allTypes, lines) {
|
|
628
|
+
const typeName = type.typeName.name;
|
|
629
|
+
const hasBase = type.save.hasBase;
|
|
630
|
+
const hasChildren = type.polymorphicDispatch !== null || type.isAbstract;
|
|
631
|
+
// virtual if has children, override if has base
|
|
632
|
+
let modifier = "";
|
|
633
|
+
if (hasBase) {
|
|
634
|
+
modifier = "override ";
|
|
635
|
+
}
|
|
636
|
+
else if (hasChildren) {
|
|
637
|
+
modifier = "virtual ";
|
|
638
|
+
}
|
|
639
|
+
lines.push(" /// <summary>");
|
|
640
|
+
lines.push(` /// Save the ${typeName} instance to a dictionary.`);
|
|
641
|
+
lines.push(" /// </summary>");
|
|
642
|
+
lines.push(' /// <param name="context">Optional context with pre/post processing callbacks.</param>');
|
|
643
|
+
lines.push(" /// <returns>The dictionary representation of this instance.</returns>");
|
|
644
|
+
lines.push(` public ${modifier}Dictionary<string, object?> Save(SaveContext? context = null)`);
|
|
645
|
+
lines.push(" {");
|
|
646
|
+
lines.push(" var obj = this;");
|
|
647
|
+
lines.push(" if (context is not null)");
|
|
648
|
+
lines.push(" {");
|
|
649
|
+
lines.push(" obj = context.ProcessObject(obj);");
|
|
650
|
+
lines.push(" }");
|
|
651
|
+
lines.push("");
|
|
652
|
+
lines.push("");
|
|
653
|
+
if (hasBase) {
|
|
654
|
+
lines.push(" // Start with parent class properties");
|
|
655
|
+
lines.push(" var result = base.Save(context);");
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
lines.push(" var result = new Dictionary<string, object?>();");
|
|
659
|
+
}
|
|
660
|
+
lines.push("");
|
|
661
|
+
// Per-field saves
|
|
662
|
+
for (const assign of type.save.assignments) {
|
|
663
|
+
emitSaveAssignment(assign, lines);
|
|
664
|
+
}
|
|
665
|
+
// ProcessDict only if no base
|
|
666
|
+
if (!hasBase) {
|
|
667
|
+
lines.push("");
|
|
668
|
+
lines.push(" if (context is not null)");
|
|
669
|
+
lines.push(" {");
|
|
670
|
+
lines.push(" result = context.ProcessDict(result);");
|
|
671
|
+
lines.push(" }");
|
|
672
|
+
}
|
|
673
|
+
lines.push("");
|
|
674
|
+
lines.push(" return result;");
|
|
675
|
+
lines.push(" }");
|
|
676
|
+
lines.push("");
|
|
677
|
+
}
|
|
678
|
+
function emitToWireMethod(type, lines) {
|
|
679
|
+
const wire = type.wire;
|
|
680
|
+
lines.push(" /// <summary>");
|
|
681
|
+
lines.push(` /// Convert this instance to a provider-specific wire-format dictionary.`);
|
|
682
|
+
lines.push(" /// </summary>");
|
|
683
|
+
lines.push(' /// <param name="provider">The provider name (e.g., "openai", "anthropic").</param>');
|
|
684
|
+
lines.push(" /// <returns>A dictionary with provider-specific field names.</returns>");
|
|
685
|
+
lines.push(" public Dictionary<string, object?> ToWire(string provider)");
|
|
686
|
+
lines.push(" {");
|
|
687
|
+
lines.push(" var data = Save();");
|
|
688
|
+
lines.push(" var result = new Dictionary<string, object?>();");
|
|
689
|
+
lines.push(" var wireMap = new Dictionary<string, Dictionary<string, string>>");
|
|
690
|
+
lines.push(" {");
|
|
691
|
+
for (const mapping of wire.mappings) {
|
|
692
|
+
const entries = Object.entries(mapping.wireNames);
|
|
693
|
+
const inner = entries
|
|
694
|
+
.map(([provider, wireName]) => `["${provider}"] = "${wireName}"`)
|
|
695
|
+
.join(", ");
|
|
696
|
+
lines.push(` ["${mapping.fieldName}"] = new Dictionary<string, string> { ${inner} },`);
|
|
697
|
+
}
|
|
698
|
+
lines.push(" };");
|
|
699
|
+
lines.push(" foreach (var (key, value) in data)");
|
|
700
|
+
lines.push(" {");
|
|
701
|
+
lines.push(" if (wireMap.TryGetValue(key, out var mapping) && mapping.TryGetValue(provider, out var wireName))");
|
|
702
|
+
lines.push(" result[wireName] = value;");
|
|
703
|
+
lines.push(" }");
|
|
704
|
+
lines.push(" return result;");
|
|
705
|
+
lines.push(" }");
|
|
706
|
+
lines.push("");
|
|
707
|
+
}
|
|
708
|
+
function emitSaveAssignment(assign, lines) {
|
|
709
|
+
const propName = toPascalCase(assign.fieldName);
|
|
710
|
+
const cat = assign.category;
|
|
711
|
+
lines.push("");
|
|
712
|
+
if (assign.isOptional) {
|
|
713
|
+
lines.push(` if (obj.${propName} is not null)`);
|
|
714
|
+
lines.push(" {");
|
|
715
|
+
lines.push(` ${getSaveExpression(assign, propName)}`);
|
|
716
|
+
lines.push(" }");
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
lines.push(` ${getSaveExpression(assign, propName)}`);
|
|
720
|
+
}
|
|
721
|
+
lines.push("");
|
|
722
|
+
}
|
|
723
|
+
function getSaveExpression(assign, propName) {
|
|
724
|
+
// Named enum — serialize as string (skip open enums — they're already string)
|
|
725
|
+
if (assign.enumName && !assign.isOpenEnum) {
|
|
726
|
+
const valueExpr = assign.isOptional ? `obj.${propName}.Value` : `obj.${propName}`;
|
|
727
|
+
return `result["${assign.targetName}"] = ${valueExpr}.ToString().ToLowerInvariant();`;
|
|
728
|
+
}
|
|
729
|
+
const cat = assign.category;
|
|
730
|
+
switch (cat.kind) {
|
|
731
|
+
case "scalar":
|
|
732
|
+
case "dict":
|
|
733
|
+
return `result["${assign.targetName}"] = obj.${propName};`;
|
|
734
|
+
case "complex":
|
|
735
|
+
return `result["${assign.targetName}"] = obj.${propName}?.Save(context);`;
|
|
736
|
+
case "collection_complex":
|
|
737
|
+
return `result["${assign.targetName}"] = Save${propName}(obj.${propName}, context);`;
|
|
738
|
+
case "collection_scalar":
|
|
739
|
+
return `result["${assign.targetName}"] = obj.${propName};`;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
// ============================================================================
|
|
743
|
+
// Collection Save Helper
|
|
744
|
+
// ============================================================================
|
|
745
|
+
function emitCollectionSaveHelper(helper, lines) {
|
|
746
|
+
const propName = toPascalCase(helper.propertyName);
|
|
747
|
+
const elemType = helper.elementTypeName.name;
|
|
748
|
+
lines.push("");
|
|
749
|
+
lines.push(" /// <summary>");
|
|
750
|
+
lines.push(` /// Save a list of ${elemType} to object or array format.`);
|
|
751
|
+
lines.push(" /// </summary>");
|
|
752
|
+
lines.push(` public static object Save${propName}(IList<${elemType}> items, SaveContext? context)`);
|
|
753
|
+
lines.push(" {");
|
|
754
|
+
lines.push(" context ??= new SaveContext();");
|
|
755
|
+
lines.push("");
|
|
756
|
+
if (helper.hasNameProperty) {
|
|
757
|
+
lines.push("");
|
|
758
|
+
lines.push(' if (context.CollectionFormat == "array")');
|
|
759
|
+
lines.push(" {");
|
|
760
|
+
lines.push(" return items.Select(item => item.Save(context)).ToList();");
|
|
761
|
+
lines.push(" }");
|
|
762
|
+
lines.push("");
|
|
763
|
+
lines.push(" // Object format: use name as key");
|
|
764
|
+
lines.push(" var result = new Dictionary<string, object?>();");
|
|
765
|
+
lines.push(" foreach (var item in items)");
|
|
766
|
+
lines.push(" {");
|
|
767
|
+
lines.push(" var itemData = item.Save(context);");
|
|
768
|
+
lines.push(' if (itemData.TryGetValue("name", out var nameValue) && nameValue is string name)');
|
|
769
|
+
lines.push(" {");
|
|
770
|
+
lines.push(' itemData.Remove("name");');
|
|
771
|
+
lines.push("");
|
|
772
|
+
lines.push(" // Check if we can use shorthand");
|
|
773
|
+
lines.push(` if (context.UseShorthand && ${elemType}.ShorthandProperty is string shorthandProp)`);
|
|
774
|
+
lines.push(" {");
|
|
775
|
+
lines.push(" if (itemData.Count == 1 && itemData.ContainsKey(shorthandProp))");
|
|
776
|
+
lines.push(" {");
|
|
777
|
+
lines.push(" result[name] = itemData[shorthandProp];");
|
|
778
|
+
lines.push(" continue;");
|
|
779
|
+
lines.push(" }");
|
|
780
|
+
lines.push(" }");
|
|
781
|
+
lines.push(" result[name] = itemData;");
|
|
782
|
+
lines.push(" }");
|
|
783
|
+
lines.push(" else");
|
|
784
|
+
lines.push(" {");
|
|
785
|
+
lines.push(' // No name, can\'t use object format for this item');
|
|
786
|
+
lines.push(' throw new InvalidOperationException("Cannot save item in object format: missing \'name\' property");');
|
|
787
|
+
lines.push(" }");
|
|
788
|
+
lines.push(" }");
|
|
789
|
+
lines.push(" return result;");
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
lines.push(" // This collection type does not have a 'name' property, only array format is supported");
|
|
793
|
+
lines.push(" return items.Select(item => item.Save(context)).ToList();");
|
|
794
|
+
}
|
|
795
|
+
lines.push("");
|
|
796
|
+
lines.push(" }");
|
|
797
|
+
lines.push("");
|
|
798
|
+
}
|
|
799
|
+
// ============================================================================
|
|
800
|
+
// Serialization methods (ToYaml, ToJson, FromJson, FromYaml)
|
|
801
|
+
// ============================================================================
|
|
802
|
+
function emitSerializationMethods(type, lines) {
|
|
803
|
+
const typeName = type.typeName.name;
|
|
804
|
+
const new_ = type.base ? "new " : "";
|
|
805
|
+
// ToYaml
|
|
806
|
+
lines.push("");
|
|
807
|
+
lines.push(" /// <summary>");
|
|
808
|
+
lines.push(` /// Convert the ${typeName} instance to a YAML string.`);
|
|
809
|
+
lines.push(" /// </summary>");
|
|
810
|
+
lines.push(' /// <param name="context">Optional context with pre/post processing callbacks.</param>');
|
|
811
|
+
lines.push(" /// <returns>The YAML string representation of this instance.</returns>");
|
|
812
|
+
lines.push(` public ${new_}string ToYaml(SaveContext? context = null)`);
|
|
813
|
+
lines.push(" {");
|
|
814
|
+
lines.push(" context ??= new SaveContext();");
|
|
815
|
+
lines.push(" return context.ToYaml(Save(context));");
|
|
816
|
+
lines.push(" }");
|
|
817
|
+
lines.push("");
|
|
818
|
+
// ToJson
|
|
819
|
+
lines.push(" /// <summary>");
|
|
820
|
+
lines.push(` /// Convert the ${typeName} instance to a JSON string.`);
|
|
821
|
+
lines.push(" /// </summary>");
|
|
822
|
+
lines.push(' /// <param name="context">Optional context with pre/post processing callbacks.</param>');
|
|
823
|
+
lines.push(' /// <param name="indent">Whether to indent the output. Defaults to true.</param>');
|
|
824
|
+
lines.push(" /// <returns>The JSON string representation of this instance.</returns>");
|
|
825
|
+
lines.push(` public ${new_}string ToJson(SaveContext? context = null, bool indent = true)`);
|
|
826
|
+
lines.push(" {");
|
|
827
|
+
lines.push(" context ??= new SaveContext();");
|
|
828
|
+
lines.push(" return context.ToJson(Save(context), indent);");
|
|
829
|
+
lines.push(" }");
|
|
830
|
+
lines.push("");
|
|
831
|
+
// FromJson
|
|
832
|
+
emitFromJson(type, lines);
|
|
833
|
+
lines.push("");
|
|
834
|
+
// FromYaml
|
|
835
|
+
emitFromYaml(type, lines);
|
|
836
|
+
lines.push("");
|
|
837
|
+
}
|
|
838
|
+
// ============================================================================
|
|
839
|
+
// FromJson
|
|
840
|
+
// ============================================================================
|
|
841
|
+
function emitFromJson(type, lines) {
|
|
842
|
+
const typeName = type.typeName.name;
|
|
843
|
+
const new_ = type.base ? "new " : "";
|
|
844
|
+
const hasCoercions = type.load.coercions.length > 0;
|
|
845
|
+
const hasCoercionProp = type.coercionProperty !== null;
|
|
846
|
+
lines.push(" /// <summary>");
|
|
847
|
+
lines.push(` /// Load a ${typeName} instance from a JSON string.`);
|
|
848
|
+
lines.push(" /// </summary>");
|
|
849
|
+
lines.push(' /// <param name="json">The JSON string to parse.</param>');
|
|
850
|
+
lines.push(' /// <param name="context">Optional context with pre/post processing callbacks.</param>');
|
|
851
|
+
lines.push(` /// <returns>The loaded ${typeName} instance.</returns>`);
|
|
852
|
+
lines.push(` public ${new_}static ${typeName} FromJson(string json, LoadContext? context = null)`);
|
|
853
|
+
lines.push(" {");
|
|
854
|
+
lines.push(" using var doc = JsonDocument.Parse(json);");
|
|
855
|
+
lines.push(" Dictionary<string, object?> dict;");
|
|
856
|
+
if (hasCoercions || hasCoercionProp) {
|
|
857
|
+
lines.push(" // Handle alternate representations");
|
|
858
|
+
lines.push(" if (doc.RootElement.ValueKind != JsonValueKind.Object)");
|
|
859
|
+
lines.push(" {");
|
|
860
|
+
lines.push(" var value = JsonUtils.GetJsonElementValue(doc.RootElement);");
|
|
861
|
+
if (hasCoercions) {
|
|
862
|
+
lines.push(" dict = value switch");
|
|
863
|
+
lines.push(" {");
|
|
864
|
+
emitCoercionSwitchArms(type.load.coercions, type.coercionProperty, " ", "value", lines);
|
|
865
|
+
lines.push(" };");
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
// Only coercionProperty, no typed coercions
|
|
869
|
+
lines.push(` dict = new Dictionary<string, object?>`);
|
|
870
|
+
lines.push(" {");
|
|
871
|
+
lines.push(` ["${type.coercionProperty}"] = value`);
|
|
872
|
+
lines.push(" };");
|
|
873
|
+
}
|
|
874
|
+
lines.push(" }");
|
|
875
|
+
lines.push(" else");
|
|
876
|
+
lines.push(" {");
|
|
877
|
+
lines.push(" dict = JsonSerializer.Deserialize<Dictionary<string, object?>>(json, JsonUtils.Options)");
|
|
878
|
+
lines.push(' ?? throw new ArgumentException("Failed to parse JSON as dictionary");');
|
|
879
|
+
lines.push(" }");
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
lines.push(" dict = JsonSerializer.Deserialize<Dictionary<string, object?>>(json, JsonUtils.Options)");
|
|
883
|
+
lines.push(' ?? throw new ArgumentException("Failed to parse JSON as dictionary");');
|
|
884
|
+
}
|
|
885
|
+
lines.push("");
|
|
886
|
+
lines.push(" return Load(dict, context);");
|
|
887
|
+
lines.push(" }");
|
|
888
|
+
}
|
|
889
|
+
// ============================================================================
|
|
890
|
+
// FromYaml
|
|
891
|
+
// ============================================================================
|
|
892
|
+
function emitFromYaml(type, lines) {
|
|
893
|
+
const typeName = type.typeName.name;
|
|
894
|
+
const new_ = type.base ? "new " : "";
|
|
895
|
+
const hasCoercions = type.load.coercions.length > 0;
|
|
896
|
+
const hasCoercionProp = type.coercionProperty !== null;
|
|
897
|
+
lines.push(" /// <summary>");
|
|
898
|
+
lines.push(` /// Load a ${typeName} instance from a YAML string.`);
|
|
899
|
+
lines.push(" /// </summary>");
|
|
900
|
+
lines.push(' /// <param name="yaml">The YAML string to parse.</param>');
|
|
901
|
+
lines.push(' /// <param name="context">Optional context with pre/post processing callbacks.</param>');
|
|
902
|
+
lines.push(` /// <returns>The loaded ${typeName} instance.</returns>`);
|
|
903
|
+
lines.push(` public ${new_}static ${typeName} FromYaml(string yaml, LoadContext? context = null)`);
|
|
904
|
+
lines.push(" {");
|
|
905
|
+
if (hasCoercions || hasCoercionProp) {
|
|
906
|
+
lines.push(" // Handle alternate representations - try object first, fall back to scalar");
|
|
907
|
+
lines.push(" Dictionary<string, object?>? dictResult = null;");
|
|
908
|
+
lines.push(" try");
|
|
909
|
+
lines.push(" {");
|
|
910
|
+
lines.push(" dictResult = YamlUtils.Deserializer.Deserialize<Dictionary<string, object?>>(yaml);");
|
|
911
|
+
lines.push(" }");
|
|
912
|
+
lines.push(" catch (YamlDotNet.Core.YamlException)");
|
|
913
|
+
lines.push(" {");
|
|
914
|
+
lines.push(" // Not a dictionary, will be handled as scalar below");
|
|
915
|
+
lines.push(" }");
|
|
916
|
+
lines.push("");
|
|
917
|
+
lines.push(" Dictionary<string, object?> dict;");
|
|
918
|
+
lines.push(" if (dictResult is not null)");
|
|
919
|
+
lines.push(" {");
|
|
920
|
+
lines.push(" dict = dictResult;");
|
|
921
|
+
lines.push(" }");
|
|
922
|
+
lines.push(" else");
|
|
923
|
+
lines.push(" {");
|
|
924
|
+
lines.push(" // Parse as scalar with proper type inference");
|
|
925
|
+
lines.push(" var parsed = YamlUtils.ParseScalar(yaml);");
|
|
926
|
+
if (hasCoercions) {
|
|
927
|
+
lines.push(" dict = parsed switch");
|
|
928
|
+
lines.push(" {");
|
|
929
|
+
emitCoercionSwitchArms(type.load.coercions, type.coercionProperty, " ", "parsed", lines);
|
|
930
|
+
lines.push(" };");
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
lines.push(` dict = new Dictionary<string, object?>`);
|
|
934
|
+
lines.push(" {");
|
|
935
|
+
lines.push(` ["${type.coercionProperty}"] = parsed`);
|
|
936
|
+
lines.push(" };");
|
|
937
|
+
}
|
|
938
|
+
lines.push(" }");
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
lines.push(" var dict = YamlUtils.Deserializer.Deserialize<Dictionary<string, object?>>(yaml)");
|
|
942
|
+
lines.push(' ?? throw new ArgumentException("Failed to parse YAML as dictionary");');
|
|
943
|
+
}
|
|
944
|
+
lines.push("");
|
|
945
|
+
lines.push(" return Load(dict, context);");
|
|
946
|
+
lines.push(" }");
|
|
947
|
+
}
|
|
948
|
+
// ============================================================================
|
|
949
|
+
// Coercion switch arms (shared between FromJson and FromYaml)
|
|
950
|
+
// ============================================================================
|
|
951
|
+
function emitCoercionSwitchArms(coercions, coercionProperty, indent, switchVarName, lines) {
|
|
952
|
+
// Track emitted types to avoid duplicates
|
|
953
|
+
const emittedTypes = new Set();
|
|
954
|
+
for (const coercion of coercions) {
|
|
955
|
+
const csharpType = CSHARP_TYPE_MAP[coercion.scalarType] || "object";
|
|
956
|
+
const varName = `${csharpType}Value`;
|
|
957
|
+
if (emittedTypes.has(csharpType))
|
|
958
|
+
continue;
|
|
959
|
+
emittedTypes.add(csharpType);
|
|
960
|
+
// Emit the main type arm
|
|
961
|
+
lines.push(`${indent}${csharpType} ${varName} => new Dictionary<string, object?>`);
|
|
962
|
+
lines.push(`${indent}{`);
|
|
963
|
+
for (const assign of coercion.assignments) {
|
|
964
|
+
if (assign.isInput) {
|
|
965
|
+
lines.push(`${indent} ["${assign.fieldName}"] = ${varName},`);
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
lines.push(`${indent} ["${assign.fieldName}"] = "${assign.literalValue}",`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
lines.push(`${indent}},`);
|
|
972
|
+
// Numeric widening: int → long, float → double
|
|
973
|
+
if (csharpType === "int" && !emittedTypes.has("long")) {
|
|
974
|
+
emittedTypes.add("long");
|
|
975
|
+
lines.push(`${indent}long longValue => new Dictionary<string, object?>`);
|
|
976
|
+
lines.push(`${indent}{`);
|
|
977
|
+
for (const assign of coercion.assignments) {
|
|
978
|
+
if (assign.isInput) {
|
|
979
|
+
lines.push(`${indent} ["${assign.fieldName}"] = longValue,`);
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
lines.push(`${indent} ["${assign.fieldName}"] = "${assign.literalValue}",`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
lines.push(`${indent}},`);
|
|
986
|
+
}
|
|
987
|
+
if (csharpType === "float" && !emittedTypes.has("double")) {
|
|
988
|
+
emittedTypes.add("double");
|
|
989
|
+
lines.push(`${indent}double doubleValue => new Dictionary<string, object?>`);
|
|
990
|
+
lines.push(`${indent}{`);
|
|
991
|
+
for (const assign of coercion.assignments) {
|
|
992
|
+
if (assign.isInput) {
|
|
993
|
+
lines.push(`${indent} ["${assign.fieldName}"] = doubleValue,`);
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
lines.push(`${indent} ["${assign.fieldName}"] = "${assign.literalValue}",`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
lines.push(`${indent}},`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
// Default arm
|
|
1003
|
+
if (coercionProperty) {
|
|
1004
|
+
lines.push(`${indent}_ => new Dictionary<string, object?>`);
|
|
1005
|
+
lines.push(`${indent}{`);
|
|
1006
|
+
lines.push(`${indent} ["${coercionProperty}"] = ${switchVarName}`);
|
|
1007
|
+
lines.push(`${indent}}`);
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
lines.push(`${indent}_ => throw new ArgumentException($"Unsupported scalar type")`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
// ============================================================================
|
|
1014
|
+
// Factory methods
|
|
1015
|
+
// ============================================================================
|
|
1016
|
+
function emitFactoryRegion(type, visitor, lines) {
|
|
1017
|
+
lines.push("");
|
|
1018
|
+
lines.push(" #region Factory Methods");
|
|
1019
|
+
for (const factory of type.factories) {
|
|
1020
|
+
emitFactoryMethod(factory, type, visitor, lines);
|
|
1021
|
+
}
|
|
1022
|
+
lines.push("");
|
|
1023
|
+
lines.push(" #endregion");
|
|
1024
|
+
}
|
|
1025
|
+
function emitFactoryMethod(factory, type, visitor, lines) {
|
|
1026
|
+
const methodName = getCSharpFactoryMethodName(factory.name, type);
|
|
1027
|
+
const params = Object.entries(factory.params).map(([name, typeStr]) => `${getCSharpFactoryParamType(typeStr)} ${name}`).join(", ");
|
|
1028
|
+
const body = visitor.visitExpr(factory.body);
|
|
1029
|
+
lines.push("");
|
|
1030
|
+
lines.push(" /// <summary>");
|
|
1031
|
+
lines.push(` /// Create a ${type.typeName.name} with preset field values.`);
|
|
1032
|
+
lines.push(" /// </summary>");
|
|
1033
|
+
lines.push(` public static ${type.typeName.name} ${methodName}(${params})`);
|
|
1034
|
+
lines.push(" {");
|
|
1035
|
+
lines.push(` return ${body};`);
|
|
1036
|
+
lines.push(" }");
|
|
1037
|
+
}
|
|
1038
|
+
function getCSharpFactoryMethodName(factoryName, type) {
|
|
1039
|
+
const methodName = factoryName.charAt(0).toUpperCase() + factoryName.slice(1);
|
|
1040
|
+
const propertyNames = type.fields.map(f => toPascalCase(f.name));
|
|
1041
|
+
// Also consider @method stubs that emit as properties (zero-param, non-verb names)
|
|
1042
|
+
for (const m of type.methods) {
|
|
1043
|
+
if (!isMethodStyle(m.name) && !m.params?.length) {
|
|
1044
|
+
propertyNames.push(toPascalCase(m.name));
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (propertyNames.includes(methodName)) {
|
|
1048
|
+
return `Create${methodName}`;
|
|
1049
|
+
}
|
|
1050
|
+
return methodName;
|
|
1051
|
+
}
|
|
1052
|
+
function getCSharpFactoryParamType(typeStr) {
|
|
1053
|
+
switch (typeStr) {
|
|
1054
|
+
case "string": return "string";
|
|
1055
|
+
case "boolean": return "bool";
|
|
1056
|
+
case "integer":
|
|
1057
|
+
case "int32": return "int";
|
|
1058
|
+
case "int64": return "long";
|
|
1059
|
+
case "float":
|
|
1060
|
+
case "float32": return "float";
|
|
1061
|
+
case "float64": return "double";
|
|
1062
|
+
case "unknown": return "object?";
|
|
1063
|
+
default: return "object?";
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
// ============================================================================
|
|
1067
|
+
// Helper interface (contract for @method declarations)
|
|
1068
|
+
// ============================================================================
|
|
1069
|
+
/**
|
|
1070
|
+
* Verbs that indicate an action — zero-param @methods whose name starts with
|
|
1071
|
+
* one of these get emitted as C# methods. Everything else becomes a read-only
|
|
1072
|
+
* property. This preserves idiomatic C# naming (``message.Text`` is a property,
|
|
1073
|
+
* ``message.ToTextContent()`` is a method).
|
|
1074
|
+
*/
|
|
1075
|
+
const METHOD_VERB_PREFIXES = [
|
|
1076
|
+
"to", "get", "set", "fetch", "compute", "make", "build", "create",
|
|
1077
|
+
"load", "save", "convert", "parse", "format", "render", "serialize",
|
|
1078
|
+
"deserialize", "find", "calculate", "invoke", "execute", "run",
|
|
1079
|
+
];
|
|
1080
|
+
function isMethodStyle(name) {
|
|
1081
|
+
const lower = name.toLowerCase();
|
|
1082
|
+
for (const prefix of METHOD_VERB_PREFIXES) {
|
|
1083
|
+
if (lower === prefix)
|
|
1084
|
+
return true;
|
|
1085
|
+
if (lower.startsWith(prefix) && name.length > prefix.length) {
|
|
1086
|
+
const next = name[prefix.length];
|
|
1087
|
+
if (next === next.toUpperCase() && next !== next.toLowerCase()) {
|
|
1088
|
+
return true;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Emit a C# interface ``I<TypeName>Helpers`` that declares the @method
|
|
1096
|
+
* contract. The generated partial class declares ``: I<TypeName>Helpers``,
|
|
1097
|
+
* so the C# compiler will error if a hand-written partial does not provide
|
|
1098
|
+
* every contract member. Zero-param queries emit as properties, everything
|
|
1099
|
+
* else as methods.
|
|
1100
|
+
*/
|
|
1101
|
+
function emitHelperInterface(type, lines) {
|
|
1102
|
+
const name = type.typeName.name;
|
|
1103
|
+
lines.push("");
|
|
1104
|
+
lines.push("/// <summary>");
|
|
1105
|
+
lines.push(`/// Helper contract for <see cref="${name}"/>.`);
|
|
1106
|
+
lines.push("///");
|
|
1107
|
+
lines.push(`/// Runtime implementations must provide these members on ${name} (via a`);
|
|
1108
|
+
lines.push(`/// hand-written partial class). The C# compiler enforces conformance`);
|
|
1109
|
+
lines.push(`/// because ${name} declares : I${name}Helpers.`);
|
|
1110
|
+
lines.push("/// </summary>");
|
|
1111
|
+
lines.push(`public partial interface I${name}Helpers`);
|
|
1112
|
+
lines.push("{");
|
|
1113
|
+
for (const m of type.methods) {
|
|
1114
|
+
const pascalName = toPascalCase(m.name);
|
|
1115
|
+
const ret = protocolCSharpType(m.returns);
|
|
1116
|
+
if (m.description) {
|
|
1117
|
+
emitXmlDocComment(m.description, " ", lines);
|
|
1118
|
+
}
|
|
1119
|
+
const paramEntries = Object.entries(m.params);
|
|
1120
|
+
if (paramEntries.length === 0 && !isMethodStyle(m.name)) {
|
|
1121
|
+
// Property-style: ``T Foo { get; }``
|
|
1122
|
+
lines.push(` ${ret} ${pascalName} { get; }`);
|
|
1123
|
+
}
|
|
1124
|
+
else {
|
|
1125
|
+
const params = paramEntries
|
|
1126
|
+
.map(([pName, pType]) => `${protocolCSharpType(pType)} ${pName}`)
|
|
1127
|
+
.join(", ");
|
|
1128
|
+
lines.push(` ${ret} ${pascalName}(${params});`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
lines.push("}");
|
|
1132
|
+
}
|
|
1133
|
+
function emitHelperRegion(type, lines) {
|
|
1134
|
+
// Intentionally empty — the body of the partial class no longer contains
|
|
1135
|
+
// comments about helpers. The contract is expressed via the sibling
|
|
1136
|
+
// I<TypeName>Helpers interface emitted after the class.
|
|
1137
|
+
void type;
|
|
1138
|
+
void lines;
|
|
1139
|
+
}
|
|
1140
|
+
//# sourceMappingURL=emitter.js.map
|