@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,904 @@
1
+ /**
2
+ * TypeScript code emitter — Declaration IR → TypeScript source code.
3
+ *
4
+ * Replaces `file.ts.njk` (~414 lines of Nunjucks templates)
5
+ * with a typed TypeScript function that walks the FileDecl tree.
6
+ *
7
+ * The emitter produces correctly-formatted TypeScript code using modern
8
+ * class patterns with `Partial<T>` constructors. Output is post-processed
9
+ * by prettier + eslint, so exact whitespace is not critical.
10
+ *
11
+ * Structural blocks emitted (in order):
12
+ * 1. Header comment (auto-generated warning)
13
+ * 2. Imports (LoadContext/SaveContext, referenced types)
14
+ * 3. For each type in the file:
15
+ * a. [abstract] class definition with extends
16
+ * b. shorthandProperty static ClassVar
17
+ * c. Field declarations with type annotations + defaults
18
+ * d. Constructor with Partial<T>
19
+ * e. //#region Load Methods
20
+ * - load() static method
21
+ * - loadKind() for polymorphic dispatch (private static)
22
+ * - Collection helpers (loadX / saveX)
23
+ * f. //#endregion
24
+ * g. //#region Save Methods
25
+ * - save() instance method
26
+ * - toYaml() / toJson()
27
+ * - fromJson() / fromYaml() static methods
28
+ * h. //#endregion
29
+ * i. Factory static methods
30
+ * j. Method stubs (as comments)
31
+ */
32
+ import { toKebabCase } from "../../ir/utilities.js";
33
+ /**
34
+ * Type mapping from TypeSpec scalar types to TypeScript types.
35
+ */
36
+ const TYPE_MAP = {
37
+ string: "string",
38
+ boolean: "boolean",
39
+ int32: "number",
40
+ int64: "number",
41
+ float32: "number",
42
+ float64: "number",
43
+ number: "number",
44
+ integer: "number",
45
+ numeric: "number",
46
+ float: "number",
47
+ plainDate: "Date",
48
+ plainTime: "Date",
49
+ utcDateTime: "Date",
50
+ bytes: "Uint8Array",
51
+ url: "string",
52
+ unknown: "unknown",
53
+ any: "unknown",
54
+ dictionary: "Record<string, unknown>",
55
+ };
56
+ /**
57
+ * Map a TypeSpec scalar to its typeof check string for coercion guards.
58
+ */
59
+ function typeofCheck(scalarType) {
60
+ switch (scalarType) {
61
+ case "string":
62
+ return "string";
63
+ case "boolean":
64
+ return "boolean";
65
+ case "int32":
66
+ case "int64":
67
+ case "float32":
68
+ case "float64":
69
+ case "number":
70
+ case "integer":
71
+ case "numeric":
72
+ case "float":
73
+ return "number";
74
+ default:
75
+ return null;
76
+ }
77
+ }
78
+ function paramType(typeStr) {
79
+ return TYPE_MAP[typeStr] || typeStr;
80
+ }
81
+ function returnType(typeStr) {
82
+ if (typeStr.endsWith("?")) {
83
+ const inner = typeStr.slice(0, -1);
84
+ return `${returnType(inner)} | null`;
85
+ }
86
+ if (typeStr === "Record<unknown>")
87
+ return "Record<string, unknown>";
88
+ return TYPE_MAP[typeStr] || typeStr;
89
+ }
90
+ // ============================================================================
91
+ // Main entry point
92
+ // ============================================================================
93
+ /**
94
+ * Emit a complete TypeScript file from a FileDecl.
95
+ *
96
+ * @param decl - The file declaration to emit
97
+ * @param visitor - Expression visitor for rendering expressions
98
+ * @param namespace - Optional namespace override
99
+ * @param group - Semantic group folder this file lives in (e.g. "connection"). Empty string = root.
100
+ */
101
+ export function emitTypeScriptFile(decl, visitor, namespace, group = "") {
102
+ const lines = [];
103
+ // 1. Header
104
+ lines.push("// Copyright (c) Microsoft. All rights reserved.");
105
+ lines.push("// WARNING: This is an auto-generated file. DO NOT EDIT THIS FILE DIRECTLY.");
106
+ lines.push("");
107
+ // 2. Imports
108
+ // Protocol-only files don't need LoadContext/SaveContext
109
+ const hasNonProtocol = decl.types.some(t => !t.isProtocol);
110
+ if (hasNonProtocol) {
111
+ // Context file is always at the model root — go up one level when inside a group subfolder
112
+ const contextPath = group ? "../context" : "./context";
113
+ lines.push(`import { LoadContext, SaveContext } from "${contextPath}";`);
114
+ }
115
+ for (const imp of decl.imports) {
116
+ const kebab = toKebabCase(imp.module);
117
+ if (imp.group === group) {
118
+ // Same group (or both root): relative sibling
119
+ lines.push(`import { ${imp.names.join(", ")} } from "./${kebab}";`);
120
+ }
121
+ else if (imp.group) {
122
+ // Different non-empty group: go up to model root, then into the group subfolder
123
+ lines.push(`import { ${imp.names.join(", ")} } from "../${imp.group}/${kebab}";`);
124
+ }
125
+ else {
126
+ // Root-level module: go up one level from group subfolder
127
+ lines.push(`import { ${imp.names.join(", ")} } from "../${kebab}";`);
128
+ }
129
+ }
130
+ lines.push("");
131
+ // 2.5. Enum type aliases
132
+ for (const enumDef of decl.enums) {
133
+ const values = enumDef.values.map(v => `"${v}"`).join(" | ");
134
+ if (enumDef.isOpen) {
135
+ // Open enum — known values + any string
136
+ lines.push(`export type ${enumDef.name} = ${values} | (string & {});`);
137
+ }
138
+ else {
139
+ lines.push(`export type ${enumDef.name} = ${values};`);
140
+ }
141
+ }
142
+ if (decl.enums.length > 0) {
143
+ lines.push("");
144
+ }
145
+ // 3. Emit each type
146
+ for (const type of decl.types) {
147
+ emitType(type, lines, visitor);
148
+ lines.push("");
149
+ }
150
+ return lines.join("\n") + "\n";
151
+ }
152
+ // ============================================================================
153
+ // Type emission
154
+ // ============================================================================
155
+ function emitType(type, lines, visitor) {
156
+ const name = type.typeName.name;
157
+ // Protocol types → emit as interface with method signatures
158
+ if (type.isProtocol) {
159
+ emitProtocolInterface(type, lines);
160
+ return;
161
+ }
162
+ // Class definition
163
+ const abstractPrefix = type.isAbstract ? "abstract " : "";
164
+ const extendsClause = type.base ? ` extends ${type.base.name}` : "";
165
+ lines.push(`export ${abstractPrefix}class ${name}${extendsClause} {`);
166
+ // shorthandProperty static
167
+ const shorthand = type.coercionProperty ? `"${type.coercionProperty}"` : "undefined";
168
+ lines.push(` static readonly shorthandProperty: string | undefined = ${shorthand};`);
169
+ lines.push("");
170
+ // Field declarations
171
+ for (const field of type.fields) {
172
+ const annotation = tsTypeAnnotation(field);
173
+ const defaultVal = tsDefaultValue(field);
174
+ if (field.isOptional) {
175
+ lines.push(` ${field.name}?: ${annotation}${defaultVal};`);
176
+ }
177
+ else if (defaultVal === "" && field.category.kind === "complex") {
178
+ // Required complex types without defaults need definite assignment assertion
179
+ lines.push(` ${field.name}!: ${annotation};`);
180
+ }
181
+ else {
182
+ lines.push(` ${field.name}: ${annotation}${defaultVal};`);
183
+ }
184
+ }
185
+ lines.push("");
186
+ // Constructor
187
+ emitConstructor(type, lines);
188
+ // //#region Load Methods
189
+ lines.push(" //#region Load Methods");
190
+ lines.push("");
191
+ // load() static method
192
+ emitLoadMethod(type, lines);
193
+ // Polymorphic dispatch
194
+ if (type.polymorphicDispatch) {
195
+ emitPolymorphicDispatch(name, type.polymorphicDispatch, lines);
196
+ }
197
+ // Collection helpers
198
+ for (const helper of type.collectionHelpers) {
199
+ emitCollectionLoadHelper(name, helper, lines);
200
+ emitCollectionSaveHelper(name, helper, lines);
201
+ }
202
+ lines.push(" //#endregion");
203
+ lines.push("");
204
+ // //#region Save Methods
205
+ lines.push(" //#region Save Methods");
206
+ lines.push("");
207
+ // save() instance method
208
+ emitSaveMethod(type, lines);
209
+ // toWire() method (only if wire mappings exist)
210
+ if (type.wire !== null) {
211
+ emitToWireMethod(type, lines);
212
+ }
213
+ // toYaml / toJson
214
+ emitToYaml(name, lines);
215
+ emitToJson(name, lines);
216
+ // fromJson / fromYaml
217
+ emitFromJson(name, type, lines);
218
+ emitFromYaml(name, type, lines);
219
+ lines.push(" //#endregion");
220
+ // Factory methods
221
+ if (type.factories.length > 0) {
222
+ lines.push("");
223
+ for (const factory of type.factories) {
224
+ emitFactory(name, factory, visitor, lines);
225
+ }
226
+ }
227
+ // Method stubs — emitted as a sibling interface below the class
228
+ lines.push("}");
229
+ // Emit MessageHelpers-style interface after the class
230
+ if (type.methods.length > 0) {
231
+ emitMethodHelpersInterface(type, lines);
232
+ }
233
+ }
234
+ /**
235
+ * Emit a TypeScript interface named `<TypeName>Helpers` declaring the
236
+ * `@method` contract for a concrete type. The generated class does NOT
237
+ * implement this interface on its own — runtime code must provide an
238
+ * implementation. The type checker verifies conformance via structural typing.
239
+ */
240
+ function emitMethodHelpersInterface(type, lines) {
241
+ const name = type.typeName.name;
242
+ lines.push("");
243
+ lines.push(`/**`);
244
+ lines.push(` * Helper contract for \`${name}\`.`);
245
+ lines.push(` *`);
246
+ lines.push(` * Runtime implementations must provide these methods on every ${name}`);
247
+ lines.push(` * instance (either on the generated class or on a wrapper type). The`);
248
+ lines.push(` * TypeScript compiler enforces conformance wherever a value is typed as`);
249
+ lines.push(` * \`${name}Helpers\`.`);
250
+ lines.push(` */`);
251
+ lines.push(`export interface ${name}Helpers {`);
252
+ for (const m of type.methods) {
253
+ if (m.description) {
254
+ lines.push(` /** ${m.description} */`);
255
+ }
256
+ const params = Object.entries(m.params)
257
+ .map(([pName, pType]) => `${pName}: ${returnType(pType)}`)
258
+ .join(", ");
259
+ const ret = returnType(m.returns);
260
+ // Zero-param non-verb methods are emitted as getter-style readonly
261
+ // properties (matches C#/Python emitters). Callers access them without
262
+ // parens, e.g. ``msg.text``.
263
+ const isGetter = Object.keys(m.params).length === 0 && !isMethodStyle(m.name);
264
+ if (isGetter) {
265
+ lines.push(` readonly ${m.name}: ${ret};`);
266
+ }
267
+ else {
268
+ lines.push(` ${m.name}(${params}): ${ret};`);
269
+ }
270
+ }
271
+ lines.push("}");
272
+ }
273
+ /**
274
+ * Verbs that indicate an action — zero-param @methods whose name starts with
275
+ * one of these are emitted as regular TypeScript methods. Everything else
276
+ * becomes a ``readonly`` getter-style property. Mirrors the C#/Python emitter
277
+ * heuristic so that ``message.text`` stays a property and
278
+ * ``message.toTextContent()`` stays a method in every runtime.
279
+ */
280
+ const TS_METHOD_VERB_PREFIXES = [
281
+ "to", "get", "set", "fetch", "compute", "make", "build", "create",
282
+ "load", "save", "convert", "parse", "format", "render", "serialize",
283
+ "deserialize", "find", "calculate", "invoke", "execute", "run",
284
+ ];
285
+ function isMethodStyle(name) {
286
+ const lower = name.toLowerCase();
287
+ for (const prefix of TS_METHOD_VERB_PREFIXES) {
288
+ if (lower === prefix)
289
+ return true;
290
+ if (lower.startsWith(prefix) && name.length > prefix.length) {
291
+ const next = name[prefix.length];
292
+ if (next === next.toUpperCase() && next !== next.toLowerCase()) {
293
+ return true;
294
+ }
295
+ }
296
+ }
297
+ return false;
298
+ }
299
+ // ============================================================================
300
+ // Protocol interface emission
301
+ // ============================================================================
302
+ /**
303
+ * Emit a TypeScript interface for a protocol type.
304
+ * Protocols have methods but no data fields, load/save, or serialization.
305
+ */
306
+ function emitProtocolInterface(type, lines) {
307
+ const name = type.typeName.name;
308
+ if (type.description) {
309
+ lines.push(`/** ${type.description} */`);
310
+ }
311
+ lines.push(`export interface ${name} {`);
312
+ for (const method of type.methods) {
313
+ if (method.description) {
314
+ lines.push(` /** ${method.description} */`);
315
+ }
316
+ const params = Object.entries(method.params)
317
+ .map(([pName, pType]) => `${pName}: ${returnType(pType)}`)
318
+ .join(", ");
319
+ const ret = returnType(method.returns);
320
+ const optMark = method.optional ? "?" : "";
321
+ if (method.sync) {
322
+ lines.push(` ${method.name}${optMark}(${params}): ${ret};`);
323
+ }
324
+ else {
325
+ lines.push(` ${method.name}${optMark}(${params}): Promise<${ret}>;`);
326
+ }
327
+ }
328
+ lines.push("}");
329
+ }
330
+ function tsTypeAnnotation(f) {
331
+ // Named enum field — use the enum type alias
332
+ if (f.enumName && f.allowedValues.length > 0) {
333
+ return f.isOptional ? `${f.enumName} | undefined` : f.enumName;
334
+ }
335
+ const cat = f.category;
336
+ switch (cat.kind) {
337
+ case "dict":
338
+ return f.isOptional ? "Record<string, unknown> | undefined" : "Record<string, unknown>";
339
+ case "collection_scalar":
340
+ return `${TYPE_MAP[cat.scalarType] || "unknown"}[]`;
341
+ case "collection_complex":
342
+ return `${cat.typeName}[]`;
343
+ case "scalar": {
344
+ const tsType = TYPE_MAP[cat.scalarType] || "unknown";
345
+ return f.isOptional ? `${tsType} | undefined` : tsType;
346
+ }
347
+ case "complex":
348
+ return f.isOptional ? `${cat.typeName} | undefined` : cat.typeName;
349
+ }
350
+ }
351
+ // ============================================================================
352
+ // Default values
353
+ // ============================================================================
354
+ function tsDefaultValue(f) {
355
+ const cat = f.category;
356
+ if (cat.kind === "collection_scalar" || cat.kind === "collection_complex") {
357
+ return " = []";
358
+ }
359
+ if (f.isOptional) {
360
+ return "";
361
+ }
362
+ // Enum fields — use the field's default value or the first allowed value
363
+ if (f.enumName && f.allowedValues.length > 0) {
364
+ const dv = typeof f.defaultValue === "string" && f.allowedValues.includes(f.defaultValue)
365
+ ? f.defaultValue
366
+ : f.allowedValues[0];
367
+ return ` = "${dv}"`;
368
+ }
369
+ if (cat.kind === "dict") {
370
+ return " = {}";
371
+ }
372
+ if (cat.kind === "scalar") {
373
+ return tsScalarDefault(cat.scalarType, f.defaultValue);
374
+ }
375
+ // complex required types — no inline default, handled by constructor
376
+ return "";
377
+ }
378
+ function tsScalarDefault(scalarType, defaultValue) {
379
+ if (defaultValue !== undefined && defaultValue !== null) {
380
+ if (typeof defaultValue === "string") {
381
+ return ` = "${defaultValue}"`;
382
+ }
383
+ return ` = ${defaultValue}`;
384
+ }
385
+ switch (scalarType) {
386
+ case "string":
387
+ return ' = ""';
388
+ case "boolean":
389
+ return " = false";
390
+ case "number":
391
+ case "int32":
392
+ case "int64":
393
+ case "float32":
394
+ case "float64":
395
+ case "integer":
396
+ case "numeric":
397
+ case "float":
398
+ return " = 0";
399
+ default:
400
+ return "";
401
+ }
402
+ }
403
+ // ============================================================================
404
+ // Constructor
405
+ // ============================================================================
406
+ function emitConstructor(type, lines) {
407
+ const name = type.typeName.name;
408
+ const hasBase = type.base !== null;
409
+ lines.push(` constructor(init?: Partial<${name}>) {`);
410
+ if (hasBase) {
411
+ lines.push(" super(init);");
412
+ }
413
+ for (const field of type.fields) {
414
+ const cat = field.category;
415
+ if (field.isOptional) {
416
+ lines.push(` if (init?.${field.name} !== undefined) {`);
417
+ lines.push(` this.${field.name} = init.${field.name};`);
418
+ lines.push(" }");
419
+ }
420
+ else if (cat.kind === "collection_scalar" || cat.kind === "collection_complex") {
421
+ lines.push(` this.${field.name} = init?.${field.name} ?? [];`);
422
+ }
423
+ else if (cat.kind === "dict") {
424
+ lines.push(` this.${field.name} = init?.${field.name} ?? {};`);
425
+ }
426
+ else if (field.enumName && field.allowedValues.length > 0 && !field.isOptional) {
427
+ const dv = typeof field.defaultValue === "string" && field.allowedValues.includes(field.defaultValue)
428
+ ? field.defaultValue
429
+ : field.allowedValues[0];
430
+ lines.push(` this.${field.name} = init?.${field.name} ?? "${dv}";`);
431
+ }
432
+ else if (cat.kind === "scalar") {
433
+ const def = tsConstructorDefault(cat.scalarType, field.defaultValue);
434
+ lines.push(` this.${field.name} = init?.${field.name} ?? ${def};`);
435
+ }
436
+ else {
437
+ // complex required — use init value or construct a new default
438
+ lines.push(` if (init?.${field.name} !== undefined) {`);
439
+ lines.push(` this.${field.name} = init.${field.name};`);
440
+ lines.push(" }");
441
+ }
442
+ }
443
+ lines.push(" }");
444
+ lines.push("");
445
+ }
446
+ function tsConstructorDefault(scalarType, defaultValue) {
447
+ if (defaultValue !== undefined && defaultValue !== null) {
448
+ if (typeof defaultValue === "string") {
449
+ return `"${defaultValue}"`;
450
+ }
451
+ return String(defaultValue);
452
+ }
453
+ switch (scalarType) {
454
+ case "string":
455
+ return '""';
456
+ case "boolean":
457
+ return "false";
458
+ case "number":
459
+ case "int32":
460
+ case "int64":
461
+ case "float32":
462
+ case "float64":
463
+ case "integer":
464
+ case "numeric":
465
+ case "float":
466
+ return "0";
467
+ default:
468
+ return '""';
469
+ }
470
+ }
471
+ // ============================================================================
472
+ // load() method
473
+ // ============================================================================
474
+ function emitLoadMethod(type, lines) {
475
+ const name = type.typeName.name;
476
+ lines.push(` static load(data: Record<string, unknown>, context?: LoadContext): ${name} {`);
477
+ // Context pre-processing
478
+ lines.push(" if (context) {");
479
+ lines.push(" data = context.processInput(data) as Record<string, unknown>;");
480
+ lines.push(" }");
481
+ // Coercion checks — group by JS typeof to handle int/float distinction
482
+ if (type.load.coercions.length > 0) {
483
+ lines.push("");
484
+ lines.push(" // Handle alternate representations");
485
+ emitCoercionBranches(name, type.load.coercions, type, lines);
486
+ }
487
+ lines.push("");
488
+ // Create instance (polymorphic dispatch or direct)
489
+ if (type.load.hasPolymorphicDispatch && type.polymorphicDispatch) {
490
+ lines.push(` // Load polymorphic ${name} instance`);
491
+ lines.push(` const instance = ${name}.loadKind(data, context);`);
492
+ }
493
+ else {
494
+ lines.push(` const instance = new ${name}();`);
495
+ }
496
+ lines.push("");
497
+ // Per-property assignments
498
+ for (const a of type.load.assignments) {
499
+ lines.push(` if (data["${a.sourceName}"] !== undefined && data["${a.sourceName}"] !== null) {`);
500
+ lines.push(` ${emitLoadAssignment(a)}`);
501
+ lines.push(" }");
502
+ }
503
+ // Context post-processing
504
+ lines.push("");
505
+ lines.push(" if (context) {");
506
+ lines.push(` return context.processOutput(instance) as ${name};`);
507
+ lines.push(" }");
508
+ lines.push(" return instance;");
509
+ lines.push(" }");
510
+ lines.push("");
511
+ }
512
+ function emitLoadAssignment(a) {
513
+ // Named enum — cast string to enum type alias
514
+ if (a.enumName && a.allowedValues.length > 0) {
515
+ return `instance.${a.fieldName} = String(data["${a.sourceName}"]) as ${a.enumName};`;
516
+ }
517
+ const cat = a.category;
518
+ switch (cat.kind) {
519
+ case "scalar": {
520
+ const tsType = TYPE_MAP[cat.scalarType];
521
+ switch (tsType) {
522
+ case "string":
523
+ return `instance.${a.fieldName} = String(data["${a.sourceName}"]);`;
524
+ case "number":
525
+ return `instance.${a.fieldName} = Number(data["${a.sourceName}"]);`;
526
+ case "boolean":
527
+ return `instance.${a.fieldName} = Boolean(data["${a.sourceName}"]);`;
528
+ default:
529
+ return `instance.${a.fieldName} = data["${a.sourceName}"] as ${tsType};`;
530
+ }
531
+ }
532
+ case "complex":
533
+ return `instance.${a.fieldName} = ${cat.typeName}.load(data["${a.sourceName}"] as Record<string, unknown>, context);`;
534
+ case "collection_scalar": {
535
+ const tsType = TYPE_MAP[cat.scalarType];
536
+ switch (tsType) {
537
+ case "string":
538
+ return `instance.${a.fieldName} = (data["${a.sourceName}"] as unknown[]).map(v => String(v));`;
539
+ case "number":
540
+ return `instance.${a.fieldName} = (data["${a.sourceName}"] as unknown[]).map(v => Number(v));`;
541
+ case "boolean":
542
+ return `instance.${a.fieldName} = (data["${a.sourceName}"] as unknown[]).map(v => Boolean(v));`;
543
+ default:
544
+ return `instance.${a.fieldName} = data["${a.sourceName}"] as ${tsType}[];`;
545
+ }
546
+ }
547
+ case "collection_complex":
548
+ return `instance.${a.fieldName} = ${a.parentTypeName}.load${capitalize(a.fieldName)}(data["${a.sourceName}"] as unknown[], context);`;
549
+ case "dict":
550
+ return `instance.${a.fieldName} = data["${a.sourceName}"] as Record<string, unknown>;`;
551
+ }
552
+ }
553
+ /**
554
+ * Emit coercion branches, grouping coercions that share the same JS typeof.
555
+ *
556
+ * In TypeScript, int32 and float64 both map to `typeof data === "number"`.
557
+ * When multiple coercions share a typeof, we emit a single outer `if` with
558
+ * `Number.isInteger()` sub-branches to disambiguate.
559
+ */
560
+ function emitCoercionBranches(typeName, coercions, type, lines) {
561
+ // Group coercions by JS typeof check
562
+ const groups = new Map();
563
+ for (const c of coercions) {
564
+ const jsType = typeofCheck(c.scalarType);
565
+ if (!jsType)
566
+ continue;
567
+ if (!groups.has(jsType))
568
+ groups.set(jsType, []);
569
+ groups.get(jsType).push(c);
570
+ }
571
+ for (const [jsType, group] of groups) {
572
+ if (group.length === 1) {
573
+ // Single coercion for this typeof — emit simple branch
574
+ emitCoercionBranch(typeName, group[0], type, lines);
575
+ }
576
+ else {
577
+ // Multiple coercions sharing the same typeof (e.g., int32 + float64 → "number")
578
+ lines.push(` if (typeof data === "${jsType}") {`);
579
+ // Find integer and float coercions
580
+ const intCoercion = group.find(c => isIntegerScalar(c.scalarType));
581
+ const floatCoercion = group.find(c => !isIntegerScalar(c.scalarType));
582
+ if (intCoercion && floatCoercion) {
583
+ // Use Number.isInteger to disambiguate
584
+ lines.push(" if (Number.isInteger(data)) {");
585
+ emitCoercionBody(typeName, intCoercion, type, lines, " ");
586
+ lines.push(" } else {");
587
+ emitCoercionBody(typeName, floatCoercion, type, lines, " ");
588
+ lines.push(" }");
589
+ }
590
+ else {
591
+ // Fallback: just emit them sequentially (shouldn't happen)
592
+ for (const c of group) {
593
+ emitCoercionBody(typeName, c, type, lines, " ");
594
+ }
595
+ }
596
+ lines.push(" }");
597
+ }
598
+ }
599
+ }
600
+ function isIntegerScalar(scalarType) {
601
+ return scalarType === "int32" || scalarType === "int64" || scalarType === "integer";
602
+ }
603
+ /**
604
+ * Emit a single coercion branch — typeof check with early return.
605
+ */
606
+ function emitCoercionBranch(typeName, c, type, lines) {
607
+ const jsType = typeofCheck(c.scalarType);
608
+ if (!jsType)
609
+ return;
610
+ lines.push(` if (typeof data === "${jsType}") {`);
611
+ emitCoercionBody(typeName, c, type, lines, " ");
612
+ lines.push(" }");
613
+ }
614
+ /**
615
+ * Emit the body of a coercion branch (instance creation + property assignment).
616
+ */
617
+ function emitCoercionBody(typeName, c, type, lines, indent) {
618
+ if (c.needsDispatch && type.polymorphicDispatch) {
619
+ // Dynamic discriminator — must go through dispatch
620
+ const dictFields = c.assignments.map(a => {
621
+ const val = a.isInput ? "data" : `"${a.literalValue}"`;
622
+ return `"${a.fieldName}": ${val}`;
623
+ });
624
+ lines.push(`${indent}return ${typeName}.loadKind({ ${dictFields.join(", ")} } as Record<string, unknown>, context);`);
625
+ }
626
+ else {
627
+ // Direct property setting
628
+ lines.push(`${indent}const instance = new ${typeName}();`);
629
+ for (const a of c.assignments) {
630
+ if (a.isInput) {
631
+ // Check if the target field is an enum — cast to enum type instead of scalar
632
+ const targetField = type.fields.find(f => f.name === a.fieldName);
633
+ const castType = targetField?.enumName && targetField.allowedValues.length > 0
634
+ ? targetField.enumName
635
+ : (TYPE_MAP[c.scalarType] || "unknown");
636
+ lines.push(`${indent}instance.${a.fieldName} = data as ${castType};`);
637
+ }
638
+ else {
639
+ lines.push(`${indent}instance.${a.fieldName} = "${a.literalValue}";`);
640
+ }
641
+ }
642
+ lines.push(`${indent}if (context) {`);
643
+ lines.push(`${indent} return context.processOutput(instance) as ${typeName};`);
644
+ lines.push(`${indent}}`);
645
+ lines.push(`${indent}return instance;`);
646
+ }
647
+ }
648
+ // ============================================================================
649
+ // Polymorphic dispatch (loadKind)
650
+ // ============================================================================
651
+ function emitPolymorphicDispatch(parentName, dispatch, lines) {
652
+ lines.push(` private static loadKind(data: Record<string, unknown>, context?: LoadContext): ${parentName} {`);
653
+ lines.push(` const discriminatorValue = data["${dispatch.discriminatorField}"];`);
654
+ lines.push(" if (discriminatorValue !== undefined && discriminatorValue !== null) {");
655
+ lines.push(" const discriminator = String(discriminatorValue).toLowerCase();");
656
+ lines.push(" switch (discriminator) {");
657
+ for (const v of dispatch.variants) {
658
+ lines.push(` case "${v.value}":`);
659
+ lines.push(` return ${v.typeName.name}.load(data, context);`);
660
+ }
661
+ // Default handling
662
+ if (dispatch.defaultVariant) {
663
+ lines.push(" default:");
664
+ if (dispatch.defaultVariant.isSelfReference) {
665
+ lines.push(` return new ${parentName}();`);
666
+ }
667
+ else {
668
+ lines.push(` return ${dispatch.defaultVariant.typeName.name}.load(data, context);`);
669
+ }
670
+ }
671
+ else {
672
+ lines.push(" default:");
673
+ lines.push(` throw new Error(\`Unknown ${parentName} discriminator value: \${discriminator}\`);`);
674
+ }
675
+ lines.push(" }");
676
+ lines.push(" }");
677
+ // Missing discriminator
678
+ if (dispatch.isAbstract) {
679
+ lines.push(` throw new Error("Missing ${parentName} discriminator property: '${dispatch.discriminatorField}'");`);
680
+ }
681
+ else {
682
+ lines.push(` return new ${parentName}();`);
683
+ }
684
+ lines.push(" }");
685
+ lines.push("");
686
+ }
687
+ // ============================================================================
688
+ // Collection helpers (loadX / saveX)
689
+ // ============================================================================
690
+ function emitCollectionLoadHelper(parentName, helper, lines) {
691
+ const methodName = `load${capitalize(helper.propertyName)}`;
692
+ const elemName = helper.elementTypeName.name;
693
+ const firstInnerField = helper.innerFields[0] || "kind";
694
+ lines.push(` static ${methodName}(data: Record<string, unknown>[] | unknown[], context?: LoadContext): ${elemName}[] {`);
695
+ lines.push(" if (!Array.isArray(data)) {");
696
+ lines.push(" // Convert dict/object format to array format");
697
+ lines.push(" const result: Record<string, unknown>[] = [];");
698
+ lines.push(" for (const [k, v] of Object.entries(data)) {");
699
+ lines.push(" if (typeof v === \"object\" && v !== null && !Array.isArray(v)) {");
700
+ lines.push(` result.push({ name: k, ...(v as Record<string, unknown>) });`);
701
+ lines.push(" } else {");
702
+ lines.push(` result.push({ name: k, "${firstInnerField}": v });`);
703
+ lines.push(" }");
704
+ lines.push(" }");
705
+ lines.push(" data = result;");
706
+ lines.push(" }");
707
+ lines.push(` return data.map(item => ${elemName}.load(item as Record<string, unknown>, context));`);
708
+ lines.push(" }");
709
+ lines.push("");
710
+ }
711
+ function emitCollectionSaveHelper(parentName, helper, lines) {
712
+ const loadMethodName = `save${capitalize(helper.propertyName)}`;
713
+ const elemName = helper.elementTypeName.name;
714
+ lines.push(` static ${loadMethodName}(items: ${elemName}[], context?: SaveContext): Record<string, unknown>[] | Record<string, unknown> {`);
715
+ lines.push(" if (!context) {");
716
+ lines.push(" context = new SaveContext();");
717
+ lines.push(" }");
718
+ if (helper.hasNameProperty) {
719
+ lines.push("");
720
+ lines.push(' if (context.collectionFormat === "array") {');
721
+ lines.push(" return items.map(item => item.save(context));");
722
+ lines.push(" }");
723
+ lines.push("");
724
+ lines.push(" // Object format: use name as key");
725
+ lines.push(" const result: Record<string, unknown> = {};");
726
+ lines.push(" for (const item of items) {");
727
+ lines.push(" const itemData = item.save(context) as Record<string, unknown>;");
728
+ lines.push(' const name = itemData["name"] as string | undefined;');
729
+ lines.push(' delete itemData["name"];');
730
+ lines.push(" if (name) {");
731
+ lines.push(" // Check if we can use shorthand (only primary property set)");
732
+ lines.push(` const shorthand = (item.constructor as typeof ${elemName}).shorthandProperty;`);
733
+ lines.push(" if (context.useShorthand && shorthand && Object.keys(itemData).length === 1 && shorthand in itemData) {");
734
+ lines.push(" result[name] = itemData[shorthand];");
735
+ lines.push(" continue;");
736
+ lines.push(" }");
737
+ lines.push(" result[name] = itemData;");
738
+ lines.push(" } else {");
739
+ lines.push(" // No name, fall back to array format for this item");
740
+ lines.push(' if (!result["_unnamed"]) {');
741
+ lines.push(' result["_unnamed"] = [];');
742
+ lines.push(" }");
743
+ lines.push(' (result["_unnamed"] as unknown[]).push(itemData);');
744
+ lines.push(" }");
745
+ lines.push(" }");
746
+ lines.push(" return result;");
747
+ }
748
+ else {
749
+ lines.push("");
750
+ lines.push(" // This type doesn't have a 'name' property, so always use array format");
751
+ lines.push(" return items.map(item => item.save(context));");
752
+ }
753
+ lines.push(" }");
754
+ lines.push("");
755
+ }
756
+ // ============================================================================
757
+ // save() method
758
+ // ============================================================================
759
+ function emitSaveMethod(type, lines) {
760
+ const name = type.typeName.name;
761
+ lines.push(" save(context?: SaveContext): Record<string, unknown> {");
762
+ lines.push(" let obj: this = this;");
763
+ lines.push(" if (context) {");
764
+ lines.push(` obj = context.processObject(obj) as this;`);
765
+ lines.push(" }");
766
+ lines.push("");
767
+ if (type.save.hasBase) {
768
+ lines.push(" // Start with parent class properties");
769
+ lines.push(" const result = super.save(context);");
770
+ }
771
+ else {
772
+ lines.push(" const result: Record<string, unknown> = {};");
773
+ }
774
+ lines.push("");
775
+ // Per-property save assignments
776
+ for (const a of type.save.assignments) {
777
+ if (a.isOptional) {
778
+ lines.push(` if (obj.${a.fieldName} !== undefined && obj.${a.fieldName} !== null) {`);
779
+ lines.push(` ${emitSaveAssignment(a)}`);
780
+ lines.push(" }");
781
+ }
782
+ else {
783
+ lines.push(` if (obj.${a.fieldName} !== undefined && obj.${a.fieldName} !== null) {`);
784
+ lines.push(` ${emitSaveAssignment(a)}`);
785
+ lines.push(" }");
786
+ }
787
+ }
788
+ // Context post-processing (only for root types without base)
789
+ if (!type.save.hasBase) {
790
+ lines.push("");
791
+ lines.push(" if (context) {");
792
+ lines.push(" return context.processDict(result);");
793
+ lines.push(" }");
794
+ }
795
+ lines.push(" return result;");
796
+ lines.push(" }");
797
+ lines.push("");
798
+ }
799
+ function emitSaveAssignment(a) {
800
+ const cat = a.category;
801
+ switch (cat.kind) {
802
+ case "scalar":
803
+ case "dict":
804
+ case "collection_scalar":
805
+ return `result["${a.targetName}"] = obj.${a.fieldName};`;
806
+ case "collection_complex":
807
+ return `result["${a.targetName}"] = ${a.parentTypeName}.save${capitalize(a.fieldName)}(obj.${a.fieldName}, context);`;
808
+ case "complex":
809
+ return `result["${a.targetName}"] = obj.${a.fieldName}.save(context);`;
810
+ }
811
+ }
812
+ // ============================================================================
813
+ // toWire() method
814
+ // ============================================================================
815
+ function emitToWireMethod(type, lines) {
816
+ const wire = type.wire;
817
+ lines.push(" toWire(provider: string): Record<string, unknown> {");
818
+ lines.push(" const data = this.save();");
819
+ lines.push(" const result: Record<string, unknown> = {};");
820
+ lines.push(" const wireMap: Record<string, Record<string, string>> = {");
821
+ for (const mapping of wire.mappings) {
822
+ const entries = Object.entries(mapping.wireNames)
823
+ .map(([prov, wireName]) => `"${prov}": "${wireName}"`)
824
+ .join(", ");
825
+ lines.push(` "${mapping.fieldName}": { ${entries} },`);
826
+ }
827
+ lines.push(" };");
828
+ lines.push(" for (const [key, value] of Object.entries(data)) {");
829
+ lines.push(" const mapping = wireMap[key];");
830
+ lines.push(" if (mapping?.[provider]) {");
831
+ lines.push(" result[mapping[provider]] = value;");
832
+ lines.push(" }");
833
+ lines.push(" }");
834
+ lines.push(" return result;");
835
+ lines.push(" }");
836
+ lines.push("");
837
+ }
838
+ // ============================================================================
839
+ // toYaml() / toJson()
840
+ // ============================================================================
841
+ function emitToYaml(name, lines) {
842
+ lines.push(" toYaml(context?: SaveContext): string {");
843
+ lines.push(" context = context ?? new SaveContext();");
844
+ lines.push(" return context.toYaml(this.save(context));");
845
+ lines.push(" }");
846
+ lines.push("");
847
+ }
848
+ function emitToJson(name, lines) {
849
+ lines.push(" toJson(context?: SaveContext, indent: number = 2): string {");
850
+ lines.push(" context = context ?? new SaveContext();");
851
+ lines.push(" return context.toJson(this.save(context), indent);");
852
+ lines.push(" }");
853
+ lines.push("");
854
+ }
855
+ // ============================================================================
856
+ // fromJson() / fromYaml()
857
+ // ============================================================================
858
+ function emitFromJson(name, type, lines) {
859
+ lines.push(` static fromJson(json: string, context?: LoadContext): ${name} {`);
860
+ lines.push(" const data = JSON.parse(json);");
861
+ if (type.load.coercions.length > 0) {
862
+ // Pass raw parsed value directly to load() — it handles typeof coercions
863
+ lines.push(` return ${name}.load(data as Record<string, unknown>, context);`);
864
+ }
865
+ else {
866
+ lines.push(` return ${name}.load(data as Record<string, unknown>, context);`);
867
+ }
868
+ lines.push(" }");
869
+ lines.push("");
870
+ }
871
+ function emitFromYaml(name, type, lines) {
872
+ lines.push(` static fromYaml(yaml: string, context?: LoadContext): ${name} {`);
873
+ lines.push(' const { parse } = require("yaml");');
874
+ lines.push(" const data = parse(yaml);");
875
+ if (type.load.coercions.length > 0) {
876
+ // Pass raw parsed value directly to load() — it handles typeof coercions
877
+ lines.push(` return ${name}.load(data as Record<string, unknown>, context);`);
878
+ }
879
+ else {
880
+ lines.push(` return ${name}.load(data as Record<string, unknown>, context);`);
881
+ }
882
+ lines.push(" }");
883
+ lines.push("");
884
+ }
885
+ // ============================================================================
886
+ // Factory methods
887
+ // ============================================================================
888
+ function emitFactory(parentName, factory, visitor, lines) {
889
+ const params = Object.entries(factory.params)
890
+ .map(([pName, pType]) => `${pName}: ${paramType(pType)}`)
891
+ .join(", ");
892
+ lines.push(` static ${factory.name}(${params}): ${parentName} {`);
893
+ lines.push(` return ${visitor.visitExpr(factory.body)};`);
894
+ lines.push(" }");
895
+ lines.push("");
896
+ }
897
+ // ============================================================================
898
+ // Helpers
899
+ // ============================================================================
900
+ /** Capitalize first letter (camelCase → PascalCase for method names). */
901
+ function capitalize(s) {
902
+ return s.charAt(0).toUpperCase() + s.slice(1);
903
+ }
904
+ //# sourceMappingURL=emitter.js.map