@typra/emitter 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cleanup/generated-file.d.ts +6 -0
- package/dist/src/cleanup/generated-file.js +61 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +110 -0
- package/dist/src/decorators.d.ts +56 -0
- package/dist/src/decorators.js +177 -0
- package/dist/src/emitter.d.ts +13 -0
- package/dist/src/emitter.js +137 -0
- package/dist/src/generate.d.ts +86 -0
- package/dist/src/generate.js +104 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +5 -0
- package/dist/src/ir/ast.d.ts +235 -0
- package/dist/src/ir/ast.js +589 -0
- package/dist/src/ir/declarations.d.ts +364 -0
- package/dist/src/ir/declarations.js +23 -0
- package/dist/src/ir/expansion.d.ts +140 -0
- package/dist/src/ir/expansion.js +407 -0
- package/dist/src/ir/lower.d.ts +53 -0
- package/dist/src/ir/lower.js +480 -0
- package/dist/src/ir/utilities.d.ts +12 -0
- package/dist/src/ir/utilities.js +39 -0
- package/dist/src/ir/visitor.d.ts +29 -0
- package/dist/src/ir/visitor.js +48 -0
- package/dist/src/languages/csharp/driver.d.ts +5 -0
- package/dist/src/languages/csharp/driver.js +315 -0
- package/dist/src/languages/csharp/emitter.d.ts +33 -0
- package/dist/src/languages/csharp/emitter.js +1140 -0
- package/dist/src/languages/csharp/scaffolding.d.ts +18 -0
- package/dist/src/languages/csharp/scaffolding.js +591 -0
- package/dist/src/languages/csharp/test-emitter.d.ts +43 -0
- package/dist/src/languages/csharp/test-emitter.js +274 -0
- package/dist/src/languages/csharp/visitor.d.ts +14 -0
- package/dist/src/languages/csharp/visitor.js +79 -0
- package/dist/src/languages/go/driver.d.ts +12 -0
- package/dist/src/languages/go/driver.js +128 -0
- package/dist/src/languages/go/emitter.d.ts +33 -0
- package/dist/src/languages/go/emitter.js +879 -0
- package/dist/src/languages/go/scaffolding.d.ts +18 -0
- package/dist/src/languages/go/scaffolding.js +53 -0
- package/dist/src/languages/go/test-emitter.d.ts +20 -0
- package/dist/src/languages/go/test-emitter.js +300 -0
- package/dist/src/languages/go/visitor.d.ts +14 -0
- package/dist/src/languages/go/visitor.js +78 -0
- package/dist/src/languages/markdown/driver.d.ts +19 -0
- package/dist/src/languages/markdown/driver.js +408 -0
- package/dist/src/languages/python/driver.d.ts +14 -0
- package/dist/src/languages/python/driver.js +372 -0
- package/dist/src/languages/python/emitter.d.ts +31 -0
- package/dist/src/languages/python/emitter.js +856 -0
- package/dist/src/languages/python/scaffolding.d.ts +33 -0
- package/dist/src/languages/python/scaffolding.js +279 -0
- package/dist/src/languages/python/test-emitter.d.ts +29 -0
- package/dist/src/languages/python/test-emitter.js +388 -0
- package/dist/src/languages/python/visitor.d.ts +14 -0
- package/dist/src/languages/python/visitor.js +65 -0
- package/dist/src/languages/rust/driver.d.ts +13 -0
- package/dist/src/languages/rust/driver.js +624 -0
- package/dist/src/languages/rust/emitter.d.ts +45 -0
- package/dist/src/languages/rust/emitter.js +1596 -0
- package/dist/src/languages/rust/visitor.d.ts +25 -0
- package/dist/src/languages/rust/visitor.js +153 -0
- package/dist/src/languages/typescript/driver.d.ts +8 -0
- package/dist/src/languages/typescript/driver.js +209 -0
- package/dist/src/languages/typescript/emitter.d.ts +42 -0
- package/dist/src/languages/typescript/emitter.js +904 -0
- package/dist/src/languages/typescript/scaffolding.d.ts +32 -0
- package/dist/src/languages/typescript/scaffolding.js +303 -0
- package/dist/src/languages/typescript/test-emitter.d.ts +23 -0
- package/dist/src/languages/typescript/test-emitter.js +204 -0
- package/dist/src/languages/typescript/visitor.d.ts +14 -0
- package/dist/src/languages/typescript/visitor.js +64 -0
- package/dist/src/lib.d.ts +33 -0
- package/dist/src/lib.js +101 -0
- package/dist/src/testing/index.d.ts +2 -0
- package/dist/src/testing/index.js +8 -0
- package/dist/src/testing/test-context.d.ts +63 -0
- package/dist/src/testing/test-context.js +355 -0
- package/fixtures/shapes/main.tsp +43 -0
- package/fixtures/tspconfig.yaml +13 -0
- package/package.json +76 -0
- package/src/lib/main.tsp +110 -0
|
@@ -0,0 +1,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
|