@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,856 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python code emitter — Declaration IR → Python source code.
|
|
3
|
+
*
|
|
4
|
+
* Replaces `file.py.njk` + `_macros.njk` (~619 lines of Nunjucks templates)
|
|
5
|
+
* with a typed TypeScript function that walks the FileDecl tree.
|
|
6
|
+
*
|
|
7
|
+
* The emitter produces correctly-formatted Python 3.11+ code using modern
|
|
8
|
+
* `X | None` union syntax. Output should be byte-identical to the template
|
|
9
|
+
* output after ruff+black formatting.
|
|
10
|
+
*
|
|
11
|
+
* Structural blocks emitted (in order):
|
|
12
|
+
* 1. Header comment (auto-generated warning)
|
|
13
|
+
* 2. Imports (abc, dataclasses, typing, context, local types)
|
|
14
|
+
* 3. For each type in the file:
|
|
15
|
+
* a. @dataclass class definition with docstring
|
|
16
|
+
* b. _shorthand_property ClassVar
|
|
17
|
+
* c. Field declarations with type annotations + defaults
|
|
18
|
+
* d. load() static method
|
|
19
|
+
* e. Collection helpers (load_X / save_X)
|
|
20
|
+
* f. Polymorphic dispatch (load_<discriminator>)
|
|
21
|
+
* g. save() instance method
|
|
22
|
+
* h. to_yaml() and to_json() methods
|
|
23
|
+
* i. Factory classmethods
|
|
24
|
+
* j. Method stubs (as comments)
|
|
25
|
+
*/
|
|
26
|
+
import { toSnakeCase } from "../../ir/utilities.js";
|
|
27
|
+
/**
|
|
28
|
+
* Type mapping from TypeSpec scalar types to Python types.
|
|
29
|
+
*/
|
|
30
|
+
const TYPE_MAP = {
|
|
31
|
+
"string": "str",
|
|
32
|
+
"number": "float",
|
|
33
|
+
"array": "list",
|
|
34
|
+
"object": "dict",
|
|
35
|
+
"boolean": "bool",
|
|
36
|
+
"int64": "int",
|
|
37
|
+
"int32": "int",
|
|
38
|
+
"float64": "float",
|
|
39
|
+
"float32": "float",
|
|
40
|
+
"integer": "int",
|
|
41
|
+
"float": "float",
|
|
42
|
+
"numeric": "float",
|
|
43
|
+
"any": "Any",
|
|
44
|
+
"dictionary": "dict[str, Any]",
|
|
45
|
+
};
|
|
46
|
+
function paramType(typeStr) {
|
|
47
|
+
if (typeStr === "string")
|
|
48
|
+
return "str";
|
|
49
|
+
if (typeStr === "boolean")
|
|
50
|
+
return "bool";
|
|
51
|
+
if (typeStr === "integer" || typeStr === "int32" || typeStr === "int64")
|
|
52
|
+
return "int";
|
|
53
|
+
if (typeStr === "float" || typeStr === "float64" || typeStr === "float32")
|
|
54
|
+
return "float";
|
|
55
|
+
return "Any";
|
|
56
|
+
}
|
|
57
|
+
function returnType(typeStr) {
|
|
58
|
+
if (typeStr === "string")
|
|
59
|
+
return "str";
|
|
60
|
+
if (typeStr === "boolean")
|
|
61
|
+
return "bool";
|
|
62
|
+
if (typeStr === "integer" || typeStr === "int32" || typeStr === "int64")
|
|
63
|
+
return "int";
|
|
64
|
+
if (typeStr === "float" || typeStr === "float64" || typeStr === "float32")
|
|
65
|
+
return "float";
|
|
66
|
+
return typeStr;
|
|
67
|
+
}
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Main entry point
|
|
70
|
+
// ============================================================================
|
|
71
|
+
/**
|
|
72
|
+
* Emit a complete Python file from a FileDecl.
|
|
73
|
+
*/
|
|
74
|
+
export function emitPythonFile(decl, visitor, group = "") {
|
|
75
|
+
const lines = [];
|
|
76
|
+
// 1. Header
|
|
77
|
+
lines.push("##########################################");
|
|
78
|
+
lines.push("# WARNING: This is an auto-generated file.");
|
|
79
|
+
lines.push("# DO NOT EDIT THIS FILE DIRECTLY");
|
|
80
|
+
lines.push("# ANY EDITS WILL BE LOST");
|
|
81
|
+
lines.push("##########################################");
|
|
82
|
+
lines.push("");
|
|
83
|
+
// 2. Imports — sorted alphabetically as ruff/isort would
|
|
84
|
+
// Collect all import lines, then sort them
|
|
85
|
+
const stdlibImports = [];
|
|
86
|
+
const localImports = [];
|
|
87
|
+
const hasProtocol = decl.types.some(t => t.isProtocol);
|
|
88
|
+
const hasNonProtocol = decl.types.some(t => !t.isProtocol);
|
|
89
|
+
const hasMethodHelpers = decl.types.some(t => !t.isProtocol && t.methods.length > 0);
|
|
90
|
+
if (decl.containsAbstract) {
|
|
91
|
+
stdlibImports.push("from abc import ABC");
|
|
92
|
+
}
|
|
93
|
+
if (hasNonProtocol) {
|
|
94
|
+
stdlibImports.push("from dataclasses import dataclass, field");
|
|
95
|
+
}
|
|
96
|
+
const typingImports = ["Any"];
|
|
97
|
+
if (hasNonProtocol)
|
|
98
|
+
typingImports.push("ClassVar");
|
|
99
|
+
if (decl.enums.length > 0)
|
|
100
|
+
typingImports.push("Literal");
|
|
101
|
+
if (hasProtocol || hasMethodHelpers)
|
|
102
|
+
typingImports.push("Protocol", "runtime_checkable");
|
|
103
|
+
typingImports.sort();
|
|
104
|
+
stdlibImports.push(`from typing import ${typingImports.join(", ")}`);
|
|
105
|
+
// Context import — go up one level when inside a group subfolder
|
|
106
|
+
if (hasNonProtocol) {
|
|
107
|
+
const ctxPrefix = group ? ".." : ".";
|
|
108
|
+
localImports.push(`from ${ctxPrefix}_context import LoadContext, SaveContext`);
|
|
109
|
+
}
|
|
110
|
+
// Cross-group and same-group type imports
|
|
111
|
+
for (const imp of decl.imports) {
|
|
112
|
+
if (imp.group === group) {
|
|
113
|
+
// Same group (or both root): relative sibling import
|
|
114
|
+
localImports.push(`from ._${imp.module} import ${imp.names.join(", ")}`);
|
|
115
|
+
}
|
|
116
|
+
else if (imp.group) {
|
|
117
|
+
// Different non-empty group: go up to model root, then into the group subfolder
|
|
118
|
+
localImports.push(`from ..${imp.group}._${imp.module} import ${imp.names.join(", ")}`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// Imported module is at model root (no group): go up one level
|
|
122
|
+
localImports.push(`from .._${imp.module} import ${imp.names.join(", ")}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
localImports.sort();
|
|
126
|
+
for (const line of stdlibImports) {
|
|
127
|
+
lines.push(line);
|
|
128
|
+
}
|
|
129
|
+
lines.push("");
|
|
130
|
+
for (const line of localImports) {
|
|
131
|
+
lines.push(line);
|
|
132
|
+
}
|
|
133
|
+
// 2.5. Enum type aliases
|
|
134
|
+
if (decl.enums.length > 0) {
|
|
135
|
+
lines.push("");
|
|
136
|
+
for (const enumDef of decl.enums) {
|
|
137
|
+
const values = enumDef.values.map(v => `"${v}"`).join(", ");
|
|
138
|
+
if (enumDef.isOpen) {
|
|
139
|
+
// Open enum — Literal values or any str
|
|
140
|
+
lines.push(`${enumDef.name} = Literal[${values}] | str`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
lines.push(`${enumDef.name} = Literal[${values}]`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// 3. Emit each type
|
|
148
|
+
for (const type of decl.types) {
|
|
149
|
+
lines.push("");
|
|
150
|
+
lines.push("");
|
|
151
|
+
emitType(type, lines, visitor);
|
|
152
|
+
}
|
|
153
|
+
return lines.join("\n") + "\n";
|
|
154
|
+
}
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// Type emission
|
|
157
|
+
// ============================================================================
|
|
158
|
+
function emitType(type, lines, visitor) {
|
|
159
|
+
const name = type.typeName.name;
|
|
160
|
+
// Protocol types → emit as Protocol class with method signatures
|
|
161
|
+
if (type.isProtocol) {
|
|
162
|
+
emitProtocolClass(type, lines);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Class definition
|
|
166
|
+
lines.push("@dataclass");
|
|
167
|
+
const bases = [];
|
|
168
|
+
if (type.base)
|
|
169
|
+
bases.push(type.base.name);
|
|
170
|
+
if (type.isAbstract)
|
|
171
|
+
bases.push("ABC");
|
|
172
|
+
const baseSuffix = bases.length > 0 ? `(${bases.join(", ")})` : "";
|
|
173
|
+
lines.push(`class ${name}${baseSuffix}:`);
|
|
174
|
+
// Docstring
|
|
175
|
+
emitDocstring(type, lines);
|
|
176
|
+
// _shorthand_property ClassVar
|
|
177
|
+
lines.push("");
|
|
178
|
+
const shorthand = type.coercionProperty ? `"${type.coercionProperty}"` : "None";
|
|
179
|
+
lines.push(` _shorthand_property: ClassVar[str | None] = ${shorthand}`);
|
|
180
|
+
// Field declarations (blank line between _shorthand and first field)
|
|
181
|
+
lines.push("");
|
|
182
|
+
for (const field of type.fields) {
|
|
183
|
+
lines.push(` ${toSnakeCase(field.name)}: ${pythonTypeAnnotation(field)}${pythonDefaultValue(field)}`);
|
|
184
|
+
}
|
|
185
|
+
// load() method
|
|
186
|
+
lines.push("");
|
|
187
|
+
emitLoadMethod(type, lines);
|
|
188
|
+
// Collection helpers (between load and polymorphic dispatch)
|
|
189
|
+
for (const helper of type.collectionHelpers) {
|
|
190
|
+
emitCollectionLoadHelper(name, helper, lines);
|
|
191
|
+
lines.push("");
|
|
192
|
+
emitCollectionSaveHelper(name, helper, lines);
|
|
193
|
+
}
|
|
194
|
+
// Polymorphic dispatch
|
|
195
|
+
if (type.polymorphicDispatch) {
|
|
196
|
+
lines.push("");
|
|
197
|
+
lines.push("");
|
|
198
|
+
emitPolymorphicDispatch(name, type.polymorphicDispatch, type.isAbstract, lines);
|
|
199
|
+
}
|
|
200
|
+
// save() method
|
|
201
|
+
lines.push("");
|
|
202
|
+
emitSaveMethod(type, lines);
|
|
203
|
+
// to_wire() method (only when wire mappings exist)
|
|
204
|
+
if (type.wire) {
|
|
205
|
+
emitToWireMethod(type, lines);
|
|
206
|
+
}
|
|
207
|
+
// to_yaml() method
|
|
208
|
+
emitToYaml(name, lines);
|
|
209
|
+
// to_json() method
|
|
210
|
+
emitToJson(name, lines);
|
|
211
|
+
// Factory methods
|
|
212
|
+
if (type.factories.length > 0) {
|
|
213
|
+
const fieldNames = new Set(type.fields.map(f => f.name));
|
|
214
|
+
lines.push("");
|
|
215
|
+
for (const factory of type.factories) {
|
|
216
|
+
emitFactory(name, factory, visitor, fieldNames, lines);
|
|
217
|
+
lines.push("");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
lines.push("");
|
|
221
|
+
// Method stubs — emit as a sibling Protocol class outside the dataclass
|
|
222
|
+
if (type.methods.length > 0) {
|
|
223
|
+
emitMethodHelpersProtocol(type, lines);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Emit a typing.Protocol class named `<TypeName>Helpers` that declares the
|
|
228
|
+
* `@method` contract for a concrete type. The generated dataclass does NOT
|
|
229
|
+
* satisfy this Protocol on its own — runtime code must provide an
|
|
230
|
+
* implementation (either by attaching methods to the class or by providing
|
|
231
|
+
* a wrapper type). Using `@runtime_checkable` allows isinstance() checks.
|
|
232
|
+
*/
|
|
233
|
+
function emitMethodHelpersProtocol(type, lines) {
|
|
234
|
+
const name = type.typeName.name;
|
|
235
|
+
lines.push("");
|
|
236
|
+
lines.push("");
|
|
237
|
+
lines.push("@runtime_checkable");
|
|
238
|
+
lines.push(`class ${name}Helpers(Protocol):`);
|
|
239
|
+
lines.push(` """Helper contract for ${name}.`);
|
|
240
|
+
lines.push("");
|
|
241
|
+
lines.push(` Runtime implementations must provide these methods on every ${name}`);
|
|
242
|
+
lines.push(" instance (either by attaching them to the generated class or by wrapping it).");
|
|
243
|
+
lines.push(" The type checker can verify conformance by annotating against this Protocol");
|
|
244
|
+
lines.push(` or by calling isinstance(instance, ${name}Helpers) at runtime.`);
|
|
245
|
+
lines.push(` """`);
|
|
246
|
+
for (const m of type.methods) {
|
|
247
|
+
const params = Object.entries(m.params)
|
|
248
|
+
.map(([pName, pType]) => `${toSnakeCase(pName)}: ${protocolType(pType)}`)
|
|
249
|
+
.join(", ");
|
|
250
|
+
const paramList = params ? `, ${params}` : "";
|
|
251
|
+
const ret = protocolType(m.returns);
|
|
252
|
+
const snakeName = toSnakeCase(m.name);
|
|
253
|
+
// Zero-param, non-verb methods are emitted as @property — idiomatic Python
|
|
254
|
+
// for accessor-style helpers (matches the C# emitter's heuristic).
|
|
255
|
+
const isProperty = Object.keys(m.params).length === 0 && !isMethodStyle(m.name);
|
|
256
|
+
lines.push("");
|
|
257
|
+
if (isProperty) {
|
|
258
|
+
lines.push(" @property");
|
|
259
|
+
}
|
|
260
|
+
lines.push(` def ${snakeName}(self${paramList}) -> ${ret}:`);
|
|
261
|
+
if (m.description) {
|
|
262
|
+
lines.push(` """${m.description}"""`);
|
|
263
|
+
}
|
|
264
|
+
lines.push(" ...");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Verbs that indicate an action — zero-param @methods whose name starts with
|
|
269
|
+
* one of these are emitted as regular Python methods. Everything else becomes
|
|
270
|
+
* a ``@property``. Mirrors the C# emitter's heuristic so both runtimes expose
|
|
271
|
+
* ``message.text`` as a property and ``message.to_text_content()`` as a method.
|
|
272
|
+
*/
|
|
273
|
+
const PY_METHOD_VERB_PREFIXES = [
|
|
274
|
+
"to", "get", "set", "fetch", "compute", "make", "build", "create",
|
|
275
|
+
"load", "save", "convert", "parse", "format", "render", "serialize",
|
|
276
|
+
"deserialize", "find", "calculate", "invoke", "execute", "run",
|
|
277
|
+
];
|
|
278
|
+
function isMethodStyle(name) {
|
|
279
|
+
const lower = name.toLowerCase();
|
|
280
|
+
for (const prefix of PY_METHOD_VERB_PREFIXES) {
|
|
281
|
+
if (lower === prefix)
|
|
282
|
+
return true;
|
|
283
|
+
if (lower.startsWith(prefix) && name.length > prefix.length) {
|
|
284
|
+
const next = name[prefix.length];
|
|
285
|
+
if (next === next.toUpperCase() && next !== next.toLowerCase()) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
// ============================================================================
|
|
293
|
+
// Protocol class emission
|
|
294
|
+
// ============================================================================
|
|
295
|
+
/** Map a protocol type string to a Python type annotation. */
|
|
296
|
+
function protocolType(typeStr) {
|
|
297
|
+
// Handle nullable types
|
|
298
|
+
if (typeStr.endsWith("?")) {
|
|
299
|
+
const inner = typeStr.slice(0, -1);
|
|
300
|
+
return `${protocolType(inner)} | None`;
|
|
301
|
+
}
|
|
302
|
+
// Handle array types
|
|
303
|
+
if (typeStr.endsWith("[]")) {
|
|
304
|
+
const inner = typeStr.slice(0, -2);
|
|
305
|
+
return `list[${protocolType(inner)}]`;
|
|
306
|
+
}
|
|
307
|
+
// Handle Record/dict types
|
|
308
|
+
if (typeStr === "Record<unknown>" || typeStr === "dictionary")
|
|
309
|
+
return "dict[str, Any]";
|
|
310
|
+
if (typeStr === "unknown" || typeStr === "any")
|
|
311
|
+
return "Any";
|
|
312
|
+
// Scalar types
|
|
313
|
+
const mapped = TYPE_MAP[typeStr];
|
|
314
|
+
if (mapped)
|
|
315
|
+
return mapped;
|
|
316
|
+
return typeStr;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Emit a Python Protocol class for a protocol type.
|
|
320
|
+
* Uses typing.Protocol for structural subtyping.
|
|
321
|
+
*/
|
|
322
|
+
function emitProtocolClass(type, lines) {
|
|
323
|
+
const name = type.typeName.name;
|
|
324
|
+
lines.push("@runtime_checkable");
|
|
325
|
+
lines.push(`class ${name}(Protocol):`);
|
|
326
|
+
// Docstring
|
|
327
|
+
if (type.description) {
|
|
328
|
+
const descLines = type.description.split("\n");
|
|
329
|
+
lines.push(` """${descLines[0]}`);
|
|
330
|
+
for (let i = 1; i < descLines.length; i++) {
|
|
331
|
+
lines.push(` ${descLines[i]}`);
|
|
332
|
+
}
|
|
333
|
+
lines.push(` """`);
|
|
334
|
+
}
|
|
335
|
+
if (type.methods.length === 0) {
|
|
336
|
+
lines.push(" ...");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
for (const method of type.methods) {
|
|
340
|
+
const params = Object.entries(method.params)
|
|
341
|
+
.map(([pName, pType]) => `${toSnakeCase(pName)}: ${protocolType(pType)}`)
|
|
342
|
+
.join(", ");
|
|
343
|
+
const ret = protocolType(method.returns);
|
|
344
|
+
// Sync method
|
|
345
|
+
lines.push("");
|
|
346
|
+
if (method.description) {
|
|
347
|
+
lines.push(` def ${toSnakeCase(method.name)}(self, ${params}) -> ${ret}:`);
|
|
348
|
+
lines.push(` """${method.description}"""`);
|
|
349
|
+
if (method.optional) {
|
|
350
|
+
lines.push(" return None");
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
lines.push(" ...");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
if (method.optional) {
|
|
358
|
+
lines.push(` def ${toSnakeCase(method.name)}(self, ${params}) -> ${ret}:`);
|
|
359
|
+
lines.push(" return None");
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
lines.push(` def ${toSnakeCase(method.name)}(self, ${params}) -> ${ret}: ...`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Async variant (skip for sync-only methods)
|
|
366
|
+
if (!method.sync) {
|
|
367
|
+
lines.push("");
|
|
368
|
+
if (method.description) {
|
|
369
|
+
lines.push(` async def ${toSnakeCase(method.name)}_async(self, ${params}) -> ${ret}:`);
|
|
370
|
+
lines.push(` """${method.description} (async variant)"""`);
|
|
371
|
+
if (method.optional) {
|
|
372
|
+
lines.push(" return None");
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
lines.push(" ...");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
if (method.optional) {
|
|
380
|
+
lines.push(` async def ${toSnakeCase(method.name)}_async(self, ${params}) -> ${ret}:`);
|
|
381
|
+
lines.push(" return None");
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
lines.push(` async def ${toSnakeCase(method.name)}_async(self, ${params}) -> ${ret}: ...`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
lines.push("");
|
|
390
|
+
}
|
|
391
|
+
// ============================================================================
|
|
392
|
+
// Docstring
|
|
393
|
+
// ============================================================================
|
|
394
|
+
function emitDocstring(type, lines) {
|
|
395
|
+
const descLines = type.description.split("\n");
|
|
396
|
+
lines.push(` """${descLines[0]}`);
|
|
397
|
+
for (let i = 1; i < descLines.length; i++) {
|
|
398
|
+
lines.push(` ${descLines[i]}`);
|
|
399
|
+
}
|
|
400
|
+
if (type.fields.length > 0) {
|
|
401
|
+
lines.push(" ");
|
|
402
|
+
lines.push(" Attributes");
|
|
403
|
+
lines.push(" ----------");
|
|
404
|
+
for (const f of type.fields) {
|
|
405
|
+
lines.push(` ${toSnakeCase(f.name)} : ${pythonDocstringType(f)}`);
|
|
406
|
+
lines.push(` ${f.description}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
lines.push(` """`);
|
|
410
|
+
}
|
|
411
|
+
// ============================================================================
|
|
412
|
+
// Type annotations — uses modern X | None syntax
|
|
413
|
+
// ============================================================================
|
|
414
|
+
function pythonTypeAnnotation(f) {
|
|
415
|
+
// Named enum field — use the enum type alias
|
|
416
|
+
if (f.enumName && f.allowedValues.length > 0) {
|
|
417
|
+
return f.isOptional ? `${f.enumName} | None` : f.enumName;
|
|
418
|
+
}
|
|
419
|
+
const cat = f.category;
|
|
420
|
+
switch (cat.kind) {
|
|
421
|
+
case "dict":
|
|
422
|
+
return f.isOptional ? "dict[str, Any] | None" : "dict[str, Any]";
|
|
423
|
+
case "collection_scalar":
|
|
424
|
+
return `list[${TYPE_MAP[cat.scalarType] || "Any"}]`;
|
|
425
|
+
case "collection_complex":
|
|
426
|
+
return `list[${cat.typeName}]`;
|
|
427
|
+
case "scalar": {
|
|
428
|
+
const pyType = TYPE_MAP[cat.scalarType] || "Any";
|
|
429
|
+
return f.isOptional ? `${pyType} | None` : pyType;
|
|
430
|
+
}
|
|
431
|
+
case "complex":
|
|
432
|
+
return f.isOptional ? `${cat.typeName} | None` : cat.typeName;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function pythonDocstringType(f) {
|
|
436
|
+
const cat = f.category;
|
|
437
|
+
let baseType;
|
|
438
|
+
switch (cat.kind) {
|
|
439
|
+
case "dict":
|
|
440
|
+
baseType = "dict[str, Any]";
|
|
441
|
+
break;
|
|
442
|
+
case "collection_scalar":
|
|
443
|
+
baseType = `list[${TYPE_MAP[cat.scalarType] || "Any"}]`;
|
|
444
|
+
break;
|
|
445
|
+
case "collection_complex":
|
|
446
|
+
baseType = `list[${cat.typeName}]`;
|
|
447
|
+
break;
|
|
448
|
+
case "scalar":
|
|
449
|
+
baseType = TYPE_MAP[cat.scalarType] || "Any";
|
|
450
|
+
break;
|
|
451
|
+
case "complex":
|
|
452
|
+
baseType = cat.typeName;
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
if (f.isOptional)
|
|
456
|
+
return `Optional[${baseType}]`;
|
|
457
|
+
return baseType;
|
|
458
|
+
}
|
|
459
|
+
// ============================================================================
|
|
460
|
+
// Default values
|
|
461
|
+
// ============================================================================
|
|
462
|
+
function pythonDefaultValue(f) {
|
|
463
|
+
const cat = f.category;
|
|
464
|
+
if (cat.kind === "collection_scalar" || cat.kind === "collection_complex") {
|
|
465
|
+
return " = field(default_factory=list)";
|
|
466
|
+
}
|
|
467
|
+
if (f.isOptional) {
|
|
468
|
+
return " = None";
|
|
469
|
+
}
|
|
470
|
+
// Enum fields — use the field's default value or the first allowed value
|
|
471
|
+
if (f.enumName && f.allowedValues.length > 0) {
|
|
472
|
+
const dv = typeof f.defaultValue === "string" && f.allowedValues.includes(f.defaultValue)
|
|
473
|
+
? f.defaultValue
|
|
474
|
+
: f.allowedValues[0];
|
|
475
|
+
return ` = field(default="${dv}")`;
|
|
476
|
+
}
|
|
477
|
+
if (cat.kind === "scalar") {
|
|
478
|
+
const t = cat.scalarType;
|
|
479
|
+
if (t === "boolean") {
|
|
480
|
+
return ` = field(default=${f.defaultValue ? "True" : "False"})`;
|
|
481
|
+
}
|
|
482
|
+
if (t === "string") {
|
|
483
|
+
return ` = field(default="${f.defaultValue ?? ""}")`;
|
|
484
|
+
}
|
|
485
|
+
if (t === "number" || t === "numeric" || t === "float64" || t === "float32" || t === "float") {
|
|
486
|
+
return ` = field(default=${f.defaultValue ?? "0.0"})`;
|
|
487
|
+
}
|
|
488
|
+
if (t === "int64" || t === "int32" || t === "integer") {
|
|
489
|
+
return ` = field(default=${f.defaultValue ?? "0"})`;
|
|
490
|
+
}
|
|
491
|
+
if (t === "dictionary") {
|
|
492
|
+
return " = field(default_factory=dict)";
|
|
493
|
+
}
|
|
494
|
+
return ` = field(default=${f.defaultValue ?? "None"})`;
|
|
495
|
+
}
|
|
496
|
+
if (cat.kind === "dict") {
|
|
497
|
+
return " = field(default_factory=dict)";
|
|
498
|
+
}
|
|
499
|
+
if (cat.kind === "complex") {
|
|
500
|
+
return ` = field(default_factory=${f.typeName.name})`;
|
|
501
|
+
}
|
|
502
|
+
return " = None";
|
|
503
|
+
}
|
|
504
|
+
// ============================================================================
|
|
505
|
+
// load() method
|
|
506
|
+
// ============================================================================
|
|
507
|
+
function emitLoadMethod(type, lines) {
|
|
508
|
+
const name = type.typeName.name;
|
|
509
|
+
lines.push(" @staticmethod");
|
|
510
|
+
lines.push(` def load(data: Any, context: LoadContext | None = None) -> "${name}":`);
|
|
511
|
+
lines.push(` """Load a ${name} instance.`);
|
|
512
|
+
lines.push(" Args:");
|
|
513
|
+
lines.push(" data (Any): The data to load the instance from.");
|
|
514
|
+
lines.push(" context (Optional[LoadContext]): Optional context with pre/post processing callbacks.");
|
|
515
|
+
lines.push(" Returns:");
|
|
516
|
+
lines.push(` ${name}: The loaded ${name} instance.`);
|
|
517
|
+
lines.push("");
|
|
518
|
+
lines.push(` """`);
|
|
519
|
+
lines.push("");
|
|
520
|
+
lines.push(" if context is not None:");
|
|
521
|
+
lines.push(" data = context.process_input(data)");
|
|
522
|
+
// Coercion checks — direct property setting instead of dict construction
|
|
523
|
+
if (type.load.coercions.length > 0) {
|
|
524
|
+
lines.push(" ");
|
|
525
|
+
lines.push(" # handle alternate representations");
|
|
526
|
+
for (const c of type.load.coercions) {
|
|
527
|
+
emitCoercionBranch(name, c, type, lines);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
lines.push(" ");
|
|
531
|
+
lines.push(` if not isinstance(data, dict):`);
|
|
532
|
+
lines.push(` raise ValueError(f"Invalid data for ${name}: {data}")`);
|
|
533
|
+
// Create instance (polymorphic dispatch or direct)
|
|
534
|
+
if (type.load.hasPolymorphicDispatch && type.polymorphicDispatch) {
|
|
535
|
+
const discSnake = toSnakeCase(type.polymorphicDispatch.discriminatorField);
|
|
536
|
+
lines.push("");
|
|
537
|
+
lines.push(` # load polymorphic ${name} instance`);
|
|
538
|
+
lines.push(` instance = ${name}.load_${discSnake}(data, context)`);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
lines.push("");
|
|
542
|
+
lines.push(` # create new instance`);
|
|
543
|
+
lines.push(` instance = ${name}()`);
|
|
544
|
+
}
|
|
545
|
+
// Blank line(s) after instance creation
|
|
546
|
+
lines.push("");
|
|
547
|
+
if (type.load.hasPolymorphicDispatch) {
|
|
548
|
+
lines.push("");
|
|
549
|
+
}
|
|
550
|
+
// Per-property assignments
|
|
551
|
+
for (const a of type.load.assignments) {
|
|
552
|
+
lines.push(` if data is not None and "${a.sourceName}" in data:`);
|
|
553
|
+
lines.push(` ${emitLoadAssignment(a)}`);
|
|
554
|
+
}
|
|
555
|
+
// Context post-processing
|
|
556
|
+
lines.push(" if context is not None:");
|
|
557
|
+
lines.push(" instance = context.process_output(instance)");
|
|
558
|
+
lines.push(" return instance");
|
|
559
|
+
lines.push("");
|
|
560
|
+
lines.push("");
|
|
561
|
+
}
|
|
562
|
+
function emitLoadAssignment(a) {
|
|
563
|
+
const snake = toSnakeCase(a.fieldName);
|
|
564
|
+
const cat = a.category;
|
|
565
|
+
switch (cat.kind) {
|
|
566
|
+
case "scalar":
|
|
567
|
+
case "dict":
|
|
568
|
+
case "collection_scalar":
|
|
569
|
+
return `instance.${snake} = data["${a.sourceName}"]`;
|
|
570
|
+
case "collection_complex":
|
|
571
|
+
return `instance.${snake} = ${a.parentTypeName}.load_${snake}(data["${a.sourceName}"], context)`;
|
|
572
|
+
case "complex":
|
|
573
|
+
return `instance.${snake} = ${cat.typeName}.load(data["${a.sourceName}"], context)`;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Emit a single coercion branch — direct property setting with early return.
|
|
578
|
+
*
|
|
579
|
+
* For types where the coercion involves a dynamic discriminator with child
|
|
580
|
+
* variants, falls back to calling the dispatch method.
|
|
581
|
+
*/
|
|
582
|
+
function emitCoercionBranch(typeName, c, type, lines) {
|
|
583
|
+
const pyScalar = TYPE_MAP[c.scalarType] || c.scalarType;
|
|
584
|
+
lines.push(` if isinstance(data, ${pyScalar}):`);
|
|
585
|
+
if (c.needsDispatch && type.polymorphicDispatch) {
|
|
586
|
+
// Dynamic discriminator — must go through dispatch
|
|
587
|
+
const discSnake = toSnakeCase(type.polymorphicDispatch.discriminatorField);
|
|
588
|
+
// Build a minimal dict for the dispatch method
|
|
589
|
+
const dictFields = c.assignments.map(a => {
|
|
590
|
+
const val = a.isInput ? "data" : `"${a.literalValue}"`;
|
|
591
|
+
return `"${a.fieldName}": ${val}`;
|
|
592
|
+
});
|
|
593
|
+
lines.push(` return ${typeName}.load_${discSnake}({${dictFields.join(", ")}}, context)`);
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
// Direct property setting — no intermediate dict
|
|
597
|
+
lines.push(` instance = ${typeName}()`);
|
|
598
|
+
for (const a of c.assignments) {
|
|
599
|
+
const snake = toSnakeCase(a.fieldName);
|
|
600
|
+
if (a.isInput) {
|
|
601
|
+
lines.push(` instance.${snake} = data`);
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
lines.push(` instance.${snake} = "${a.literalValue}"`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
lines.push(" if context is not None:");
|
|
608
|
+
lines.push(" instance = context.process_output(instance)");
|
|
609
|
+
lines.push(" return instance");
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// ============================================================================
|
|
613
|
+
// Collection helpers (load_X / save_X)
|
|
614
|
+
// ============================================================================
|
|
615
|
+
function emitCollectionLoadHelper(parentName, helper, lines) {
|
|
616
|
+
const snake = toSnakeCase(helper.propertyName);
|
|
617
|
+
const elemName = helper.elementTypeName.name;
|
|
618
|
+
const firstInnerField = helper.innerFields[0] || "kind";
|
|
619
|
+
lines.push("");
|
|
620
|
+
lines.push(" @staticmethod");
|
|
621
|
+
lines.push(` def load_${snake}(data: dict | list, context: LoadContext | None) -> list[${elemName}]:`);
|
|
622
|
+
lines.push(" if isinstance(data, dict):");
|
|
623
|
+
lines.push(` # convert simple named ${helper.propertyName} to list of ${elemName}`);
|
|
624
|
+
lines.push(" result = []");
|
|
625
|
+
lines.push(" for k, v in data.items():");
|
|
626
|
+
lines.push(" if isinstance(v, dict):");
|
|
627
|
+
lines.push(" # value is an object, spread its properties");
|
|
628
|
+
lines.push(` result.append({"name": k, **v})`);
|
|
629
|
+
lines.push(" else:");
|
|
630
|
+
lines.push(" # value is a scalar, use it as the primary property");
|
|
631
|
+
lines.push(` result.append({"name": k, "${firstInnerField}": v})`);
|
|
632
|
+
lines.push(" data = result");
|
|
633
|
+
lines.push(` return [${elemName}.load(item, context) for item in data]`);
|
|
634
|
+
}
|
|
635
|
+
function emitCollectionSaveHelper(parentName, helper, lines) {
|
|
636
|
+
const snake = toSnakeCase(helper.propertyName);
|
|
637
|
+
const elemName = helper.elementTypeName.name;
|
|
638
|
+
lines.push(" @staticmethod");
|
|
639
|
+
lines.push(` def save_${snake}(items: list[${elemName}], context: SaveContext | None) -> dict[str, Any] | list[dict[str, Any]]:`);
|
|
640
|
+
lines.push(" if context is None:");
|
|
641
|
+
lines.push(" context = SaveContext()");
|
|
642
|
+
if (helper.hasNameProperty) {
|
|
643
|
+
lines.push("");
|
|
644
|
+
lines.push(' if context.collection_format == "array":');
|
|
645
|
+
lines.push(" return [item.save(context) for item in items]");
|
|
646
|
+
lines.push("");
|
|
647
|
+
lines.push(" # Object format: use name as key");
|
|
648
|
+
lines.push(" result: dict[str, Any] = {}");
|
|
649
|
+
lines.push(" for item in items:");
|
|
650
|
+
lines.push(" item_data = item.save(context)");
|
|
651
|
+
lines.push(' name = item_data.pop("name", None)');
|
|
652
|
+
lines.push(" if name:");
|
|
653
|
+
lines.push(" # Check if we can use shorthand (only primary property set)");
|
|
654
|
+
lines.push(" if context.use_shorthand and hasattr(item, '_shorthand_property'):");
|
|
655
|
+
lines.push(" shorthand_prop = item._shorthand_property");
|
|
656
|
+
lines.push(" if shorthand_prop and len(item_data) == 1 and shorthand_prop in item_data:");
|
|
657
|
+
lines.push(" result[name] = item_data[shorthand_prop]");
|
|
658
|
+
lines.push(" continue");
|
|
659
|
+
lines.push(" result[name] = item_data");
|
|
660
|
+
lines.push(" else:");
|
|
661
|
+
lines.push(' # No name, fall back to array format for this item');
|
|
662
|
+
lines.push(' if "_unnamed" not in result:');
|
|
663
|
+
lines.push(' result["_unnamed"] = []');
|
|
664
|
+
lines.push(' result["_unnamed"].append(item_data)');
|
|
665
|
+
lines.push(" return result");
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
lines.push("");
|
|
669
|
+
lines.push(" # This type doesn't have a 'name' property, so always use array format");
|
|
670
|
+
lines.push(" return [item.save(context) for item in items]");
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// ============================================================================
|
|
674
|
+
// Polymorphic dispatch
|
|
675
|
+
// ============================================================================
|
|
676
|
+
function emitPolymorphicDispatch(parentName, dispatch, isAbstract, lines) {
|
|
677
|
+
const discSnake = toSnakeCase(dispatch.discriminatorField);
|
|
678
|
+
lines.push(" @staticmethod");
|
|
679
|
+
lines.push(` def load_${discSnake}(data: dict, context: LoadContext | None) -> "${parentName}":`);
|
|
680
|
+
lines.push(` # load polymorphic ${parentName} instance`);
|
|
681
|
+
lines.push(` if data is not None and "${dispatch.discriminatorField}" in data:`);
|
|
682
|
+
lines.push(` discriminator_value = str(data["${dispatch.discriminatorField}"]).lower()`);
|
|
683
|
+
for (let i = 0; i < dispatch.variants.length; i++) {
|
|
684
|
+
const v = dispatch.variants[i];
|
|
685
|
+
const keyword = i === 0 ? "if" : "elif";
|
|
686
|
+
lines.push(` ${keyword} discriminator_value == "${v.value}":`);
|
|
687
|
+
lines.push(` return ${v.typeName.name}.load(data, context)`);
|
|
688
|
+
}
|
|
689
|
+
// Default handling — matches template whitespace exactly
|
|
690
|
+
if (dispatch.defaultVariant) {
|
|
691
|
+
lines.push("");
|
|
692
|
+
lines.push(" else:");
|
|
693
|
+
if (dispatch.defaultVariant.isSelfReference) {
|
|
694
|
+
lines.push(` # create new instance (stop recursion)`);
|
|
695
|
+
lines.push(` return ${parentName}()`);
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
lines.push("");
|
|
699
|
+
lines.push(` # load default instance`);
|
|
700
|
+
lines.push(` return ${dispatch.defaultVariant.typeName.name}.load(data, context)`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
lines.push("");
|
|
705
|
+
lines.push(" else:");
|
|
706
|
+
lines.push(` raise ValueError(f"Unknown ${parentName} discriminator value: {discriminator_value}")`);
|
|
707
|
+
}
|
|
708
|
+
lines.push(" else:");
|
|
709
|
+
if (isAbstract) {
|
|
710
|
+
lines.push("");
|
|
711
|
+
lines.push(` raise ValueError("Missing ${parentName} discriminator property: '${dispatch.discriminatorField}'")`);
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
lines.push(` # create new instance`);
|
|
715
|
+
lines.push(` return ${parentName}()`);
|
|
716
|
+
}
|
|
717
|
+
lines.push("");
|
|
718
|
+
}
|
|
719
|
+
// ============================================================================
|
|
720
|
+
// save() method
|
|
721
|
+
// ============================================================================
|
|
722
|
+
function emitSaveMethod(type, lines) {
|
|
723
|
+
const name = type.typeName.name;
|
|
724
|
+
lines.push(` def save(self, context: SaveContext | None = None) -> dict[str, Any]:`);
|
|
725
|
+
lines.push(` """Save the ${name} instance to a dictionary.`);
|
|
726
|
+
lines.push(" Args:");
|
|
727
|
+
lines.push(" context (Optional[SaveContext]): Optional context with pre/post processing callbacks.");
|
|
728
|
+
lines.push(" Returns:");
|
|
729
|
+
lines.push(" dict[str, Any]: The dictionary representation of this instance.");
|
|
730
|
+
lines.push("");
|
|
731
|
+
lines.push(` """`);
|
|
732
|
+
lines.push(" obj = self");
|
|
733
|
+
lines.push(" if context is not None:");
|
|
734
|
+
lines.push(" obj = context.process_object(obj)");
|
|
735
|
+
lines.push("");
|
|
736
|
+
if (type.save.hasBase) {
|
|
737
|
+
lines.push("");
|
|
738
|
+
lines.push(" # Start with parent class properties");
|
|
739
|
+
lines.push(" result = super().save(context)");
|
|
740
|
+
lines.push("");
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
lines.push("");
|
|
744
|
+
lines.push(" result: dict[str, Any] = {}");
|
|
745
|
+
}
|
|
746
|
+
// Per-property save assignments
|
|
747
|
+
lines.push("");
|
|
748
|
+
for (const a of type.save.assignments) {
|
|
749
|
+
const snake = toSnakeCase(a.fieldName);
|
|
750
|
+
lines.push(` if obj.${snake} is not None:`);
|
|
751
|
+
lines.push(` ${emitSaveAssignment(a)}`);
|
|
752
|
+
}
|
|
753
|
+
// Context post-processing (only for root types without base)
|
|
754
|
+
if (!type.save.hasBase) {
|
|
755
|
+
lines.push("");
|
|
756
|
+
lines.push(" if context is not None:");
|
|
757
|
+
lines.push(" result = context.process_dict(result)");
|
|
758
|
+
}
|
|
759
|
+
lines.push(" return result");
|
|
760
|
+
lines.push("");
|
|
761
|
+
}
|
|
762
|
+
function emitSaveAssignment(a) {
|
|
763
|
+
const snake = toSnakeCase(a.fieldName);
|
|
764
|
+
const cat = a.category;
|
|
765
|
+
switch (cat.kind) {
|
|
766
|
+
case "scalar":
|
|
767
|
+
case "dict":
|
|
768
|
+
case "collection_scalar":
|
|
769
|
+
return `result["${a.targetName}"] = obj.${snake}`;
|
|
770
|
+
case "collection_complex":
|
|
771
|
+
return `result["${a.targetName}"] = ${a.parentTypeName}.save_${snake}(obj.${snake}, context)`;
|
|
772
|
+
case "complex":
|
|
773
|
+
return `result["${a.targetName}"] = obj.${snake}.save(context)`;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
// ============================================================================
|
|
777
|
+
// to_wire() method
|
|
778
|
+
// ============================================================================
|
|
779
|
+
function emitToWireMethod(type, lines) {
|
|
780
|
+
const name = type.typeName.name;
|
|
781
|
+
const wire = type.wire;
|
|
782
|
+
lines.push(` def to_wire(self, provider: str) -> dict[str, Any]:`);
|
|
783
|
+
lines.push(` """Convert to provider-specific wire format.`);
|
|
784
|
+
lines.push(" Args:");
|
|
785
|
+
lines.push(" provider (str): The provider to convert to (e.g., \"openai\", \"anthropic\").");
|
|
786
|
+
lines.push(" Returns:");
|
|
787
|
+
lines.push(" dict[str, Any]: The wire-format dictionary with provider-specific field names.");
|
|
788
|
+
lines.push("");
|
|
789
|
+
lines.push(` """`);
|
|
790
|
+
lines.push(" data = self.save()");
|
|
791
|
+
lines.push(" result: dict[str, Any] = {}");
|
|
792
|
+
// Build the wire_map literal from the IR mappings
|
|
793
|
+
lines.push(" wire_map: dict[str, dict[str, str]] = {");
|
|
794
|
+
for (const mapping of wire.mappings) {
|
|
795
|
+
const entries = Object.entries(mapping.wireNames)
|
|
796
|
+
.map(([provider, wireName]) => `"${provider}": "${wireName}"`)
|
|
797
|
+
.join(", ");
|
|
798
|
+
lines.push(` "${mapping.fieldName}": {${entries}},`);
|
|
799
|
+
}
|
|
800
|
+
lines.push(" }");
|
|
801
|
+
lines.push(" for key, value in data.items():");
|
|
802
|
+
lines.push(" mapping = wire_map.get(key)");
|
|
803
|
+
lines.push(" if mapping is not None and provider in mapping:");
|
|
804
|
+
lines.push(" result[mapping[provider]] = value");
|
|
805
|
+
lines.push(" return result");
|
|
806
|
+
lines.push("");
|
|
807
|
+
}
|
|
808
|
+
// ============================================================================
|
|
809
|
+
// to_yaml() / to_json() methods
|
|
810
|
+
// ============================================================================
|
|
811
|
+
function emitToYaml(name, lines) {
|
|
812
|
+
lines.push(` def to_yaml(self, context: SaveContext | None = None) -> str:`);
|
|
813
|
+
lines.push(` """Convert the ${name} instance to a YAML string.`);
|
|
814
|
+
lines.push(" Args:");
|
|
815
|
+
lines.push(" context (Optional[SaveContext]): Optional context with pre/post processing callbacks.");
|
|
816
|
+
lines.push(" Returns:");
|
|
817
|
+
lines.push(" str: The YAML string representation of this instance.");
|
|
818
|
+
lines.push("");
|
|
819
|
+
lines.push(` """`);
|
|
820
|
+
lines.push(" if context is None:");
|
|
821
|
+
lines.push(" context = SaveContext()");
|
|
822
|
+
lines.push(" return context.to_yaml(self.save(context))");
|
|
823
|
+
lines.push("");
|
|
824
|
+
}
|
|
825
|
+
function emitToJson(name, lines) {
|
|
826
|
+
lines.push(` def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str:`);
|
|
827
|
+
lines.push(` """Convert the ${name} instance to a JSON string.`);
|
|
828
|
+
lines.push(" Args:");
|
|
829
|
+
lines.push(" context (Optional[SaveContext]): Optional context with pre/post processing callbacks.");
|
|
830
|
+
lines.push(" indent (int): Number of spaces for indentation. Defaults to 2.");
|
|
831
|
+
lines.push(" Returns:");
|
|
832
|
+
lines.push(" str: The JSON string representation of this instance.");
|
|
833
|
+
lines.push("");
|
|
834
|
+
lines.push(` """`);
|
|
835
|
+
lines.push(" if context is None:");
|
|
836
|
+
lines.push(" context = SaveContext()");
|
|
837
|
+
lines.push(" return context.to_json(self.save(context), indent)");
|
|
838
|
+
lines.push("");
|
|
839
|
+
}
|
|
840
|
+
// ============================================================================
|
|
841
|
+
// Factory methods
|
|
842
|
+
// ============================================================================
|
|
843
|
+
function emitFactory(parentName, factory, visitor, fieldNames, lines) {
|
|
844
|
+
const params = Object.entries(factory.params)
|
|
845
|
+
.map(([pName, pType]) => `${toSnakeCase(pName)}: ${paramType(pType)}`)
|
|
846
|
+
.join(", ");
|
|
847
|
+
const paramStr = params ? `, ${params}` : "";
|
|
848
|
+
// In Python, a @classmethod with the same name as a dataclass field shadows
|
|
849
|
+
// the field's default. Prefix with create_ to avoid the collision.
|
|
850
|
+
const methodName = fieldNames.has(factory.name) ? `create_${factory.name}` : factory.name;
|
|
851
|
+
lines.push(" @classmethod");
|
|
852
|
+
lines.push(` def ${methodName}(cls${paramStr}) -> "${parentName}":`);
|
|
853
|
+
lines.push(` """Create a ${parentName} with preset field values."""`);
|
|
854
|
+
lines.push(` return ${visitor.visitExpr(factory.body)}`);
|
|
855
|
+
}
|
|
856
|
+
//# sourceMappingURL=emitter.js.map
|