@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,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