@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,1596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rust code emitter — Declaration IR → Rust source code.
|
|
3
|
+
*
|
|
4
|
+
* Replaces `file.rs.njk` + `_macros.njk` (~1,072 lines of Nunjucks templates)
|
|
5
|
+
* with a typed TypeScript function that walks the FileDecl tree.
|
|
6
|
+
*
|
|
7
|
+
* The emitter produces Rust code using the struct + enum pattern for
|
|
8
|
+
* polymorphic types. Output is post-processed by `cargo fmt`.
|
|
9
|
+
*
|
|
10
|
+
* Key Rust-specific patterns:
|
|
11
|
+
* - Polymorphic types use struct + XxxKind enum (not inheritance)
|
|
12
|
+
* - Variant-specific fields live on enum variants, not child classes
|
|
13
|
+
* - Polymorphic single-ref fields → serde_json::Value
|
|
14
|
+
* - Ownership: String, Option<T>, Vec<T>
|
|
15
|
+
* - Pattern matching for load/save of variant fields
|
|
16
|
+
*
|
|
17
|
+
* Structural blocks emitted (in order):
|
|
18
|
+
* 1. Header comment (auto-generated warning)
|
|
19
|
+
* 2. Imports (context, referenced types)
|
|
20
|
+
* 3. For each polymorphic type:
|
|
21
|
+
* a. XxxKind enum with inline struct variants
|
|
22
|
+
* b. impl Default for XxxKind
|
|
23
|
+
* 4. For each type:
|
|
24
|
+
* a. Struct definition with #[derive(Debug, Clone, Default)]
|
|
25
|
+
* b. impl block:
|
|
26
|
+
* - new(), from_json(), from_yaml()
|
|
27
|
+
* - load_from_value()
|
|
28
|
+
* - kind_str() (polymorphic only)
|
|
29
|
+
* - to_value(), to_json(), to_yaml()
|
|
30
|
+
* - to_wire() (when wire mappings exist)
|
|
31
|
+
* - Collection helpers
|
|
32
|
+
* - Factory methods
|
|
33
|
+
* - Method stubs (as trait)
|
|
34
|
+
*/
|
|
35
|
+
import { toSnakeCase } from "../../ir/utilities.js";
|
|
36
|
+
/**
|
|
37
|
+
* Emit a description as a single-line `///` doc comment.
|
|
38
|
+
* Multi-line descriptions are collapsed to a single line.
|
|
39
|
+
*/
|
|
40
|
+
function emitDocComment(description, indent, lines) {
|
|
41
|
+
// Collapse multi-line descriptions to a single line
|
|
42
|
+
const oneLine = description.replace(/\r?\n/g, " ").replace(/\s+/g, " ").trim();
|
|
43
|
+
lines.push(`${indent}/// ${oneLine}`);
|
|
44
|
+
}
|
|
45
|
+
function renderLines(lines) {
|
|
46
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
47
|
+
lines.pop();
|
|
48
|
+
}
|
|
49
|
+
return `${lines.join("\n")}\n`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Convert a string literal value to a PascalCase Rust variant name.
|
|
53
|
+
* e.g., "system" → "System", "tool" → "Tool", "text" → "Text"
|
|
54
|
+
*/
|
|
55
|
+
function toPascalCase(s) {
|
|
56
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
57
|
+
}
|
|
58
|
+
const RUST_KEYWORDS = new Set([
|
|
59
|
+
"as", "break", "const", "continue", "crate", "else", "enum", "extern",
|
|
60
|
+
"false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod",
|
|
61
|
+
"move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct",
|
|
62
|
+
"super", "trait", "true", "type", "unsafe", "use", "where", "while",
|
|
63
|
+
"async", "await", "dyn",
|
|
64
|
+
]);
|
|
65
|
+
function rustFieldName(name) {
|
|
66
|
+
const snake = toSnakeCase(name);
|
|
67
|
+
return RUST_KEYWORDS.has(snake) ? `r#${snake}` : snake;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Emit a Rust enum for a named string-literal type.
|
|
71
|
+
* Uses serde rename for string ↔ enum round-tripping.
|
|
72
|
+
*/
|
|
73
|
+
function emitStringEnum(enumDef, lines) {
|
|
74
|
+
const firstVariant = toPascalCase(enumDef.values[0]);
|
|
75
|
+
if (enumDef.isOpen) {
|
|
76
|
+
// Open enums carry String data in Other variant — no Copy
|
|
77
|
+
lines.push("#[derive(Debug, Clone, PartialEq, Eq, Hash)]");
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
lines.push("#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]");
|
|
81
|
+
}
|
|
82
|
+
lines.push(`pub enum ${enumDef.name} {`);
|
|
83
|
+
for (const value of enumDef.values) {
|
|
84
|
+
const variant = toPascalCase(value);
|
|
85
|
+
lines.push(` ${variant},`);
|
|
86
|
+
}
|
|
87
|
+
if (enumDef.isOpen) {
|
|
88
|
+
lines.push(" /// Unknown variant (open enum — accepts any string).");
|
|
89
|
+
lines.push(" Other(String),");
|
|
90
|
+
}
|
|
91
|
+
lines.push("}");
|
|
92
|
+
lines.push("");
|
|
93
|
+
// impl Default — first variant (only for closed enums; open enums use first variant too)
|
|
94
|
+
lines.push(`impl Default for ${enumDef.name} {`);
|
|
95
|
+
lines.push(" fn default() -> Self {");
|
|
96
|
+
lines.push(` Self::${firstVariant}`);
|
|
97
|
+
lines.push(" }");
|
|
98
|
+
lines.push("}");
|
|
99
|
+
lines.push("");
|
|
100
|
+
// impl Display
|
|
101
|
+
lines.push(`impl std::fmt::Display for ${enumDef.name} {`);
|
|
102
|
+
lines.push(" fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {");
|
|
103
|
+
lines.push(" match self {");
|
|
104
|
+
for (const value of enumDef.values) {
|
|
105
|
+
lines.push(` Self::${toPascalCase(value)} => write!(f, "${value}"),`);
|
|
106
|
+
}
|
|
107
|
+
if (enumDef.isOpen) {
|
|
108
|
+
lines.push(" Self::Other(s) => write!(f, \"{}\", s),");
|
|
109
|
+
}
|
|
110
|
+
lines.push(" }");
|
|
111
|
+
lines.push(" }");
|
|
112
|
+
lines.push("}");
|
|
113
|
+
lines.push("");
|
|
114
|
+
// impl From<&str>
|
|
115
|
+
lines.push(`impl ${enumDef.name} {`);
|
|
116
|
+
lines.push(" pub fn from_str_opt(s: &str) -> Option<Self> {");
|
|
117
|
+
lines.push(" match s {");
|
|
118
|
+
for (const value of enumDef.values) {
|
|
119
|
+
lines.push(` "${value}" => Some(Self::${toPascalCase(value)}),`);
|
|
120
|
+
}
|
|
121
|
+
if (enumDef.isOpen) {
|
|
122
|
+
lines.push(" other => Some(Self::Other(other.to_string())),");
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
lines.push(" _ => None,");
|
|
126
|
+
}
|
|
127
|
+
lines.push(" }");
|
|
128
|
+
lines.push(" }");
|
|
129
|
+
lines.push("");
|
|
130
|
+
lines.push(" pub fn as_str(&self) -> &str {");
|
|
131
|
+
lines.push(" match self {");
|
|
132
|
+
for (const value of enumDef.values) {
|
|
133
|
+
lines.push(` Self::${toPascalCase(value)} => "${value}",`);
|
|
134
|
+
}
|
|
135
|
+
if (enumDef.isOpen) {
|
|
136
|
+
lines.push(" Self::Other(s) => s.as_str(),");
|
|
137
|
+
}
|
|
138
|
+
lines.push(" }");
|
|
139
|
+
lines.push(" }");
|
|
140
|
+
lines.push("}");
|
|
141
|
+
lines.push("");
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Type mapping from TypeSpec scalar types to Rust types.
|
|
145
|
+
*/
|
|
146
|
+
const RUST_TYPE_MAP = {
|
|
147
|
+
string: "String",
|
|
148
|
+
boolean: "bool",
|
|
149
|
+
int32: "i32",
|
|
150
|
+
int64: "i64",
|
|
151
|
+
float32: "f32",
|
|
152
|
+
float64: "f64",
|
|
153
|
+
integer: "i64",
|
|
154
|
+
float: "f64",
|
|
155
|
+
numeric: "f64",
|
|
156
|
+
number: "f64",
|
|
157
|
+
any: "serde_json::Value",
|
|
158
|
+
unknown: "serde_json::Value",
|
|
159
|
+
object: "serde_json::Value",
|
|
160
|
+
dictionary: "serde_json::Value",
|
|
161
|
+
array: "Vec<serde_json::Value>",
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Emit a complete Rust source file from a FileDecl.
|
|
165
|
+
*
|
|
166
|
+
* @param file - File declaration to emit
|
|
167
|
+
* @param visitor - Expression visitor
|
|
168
|
+
* @param polymorphicTypeNames - Set of type names that have polymorphic dispatch
|
|
169
|
+
* @param childToParent - Map from child variant name to parent type name
|
|
170
|
+
*/
|
|
171
|
+
export function emitRustFile(file, visitor, polymorphicTypeNames, childToParent = new Map()) {
|
|
172
|
+
const lines = [];
|
|
173
|
+
const hasNonProtocol = file.types.some(t => !t.isProtocol);
|
|
174
|
+
const group = file.group || "";
|
|
175
|
+
// Header
|
|
176
|
+
lines.push("// Code generated by Typra emitter; DO NOT EDIT.");
|
|
177
|
+
lines.push("");
|
|
178
|
+
lines.push("#![allow(unused_imports, dead_code, non_camel_case_types, unused_variables, clippy::all)]");
|
|
179
|
+
lines.push("");
|
|
180
|
+
if (hasNonProtocol) {
|
|
181
|
+
// Context is always at the model root.
|
|
182
|
+
// From inside a group subfolder, we need to go up two levels: group → model root.
|
|
183
|
+
const contextPath = group ? "super::super::context" : "super::context";
|
|
184
|
+
lines.push(`use ${contextPath}::{LoadContext, SaveContext};`);
|
|
185
|
+
}
|
|
186
|
+
// Imports — post-process for Rust specifics
|
|
187
|
+
// In Rust, polymorphic child types are enum variants (not standalone structs),
|
|
188
|
+
// so we import ParentKind instead.
|
|
189
|
+
for (const imp of file.imports) {
|
|
190
|
+
const processedNames = [];
|
|
191
|
+
for (const name of imp.names) {
|
|
192
|
+
// Check if this name is a polymorphic child → import ParentKind
|
|
193
|
+
const parentName = childToParent.get(name);
|
|
194
|
+
if (parentName) {
|
|
195
|
+
const kindName = parentName + "Kind";
|
|
196
|
+
if (!processedNames.includes(kindName)) {
|
|
197
|
+
processedNames.push(kindName);
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
processedNames.push(name);
|
|
202
|
+
}
|
|
203
|
+
if (processedNames.length === 0)
|
|
204
|
+
continue;
|
|
205
|
+
lines.push("");
|
|
206
|
+
const names = processedNames.sort().join(", ");
|
|
207
|
+
// Build the Rust module path based on group relationship
|
|
208
|
+
let modPath;
|
|
209
|
+
if (imp.group === group) {
|
|
210
|
+
// Same group (or both root): sibling module via super
|
|
211
|
+
modPath = `super::${toSnakeCase(imp.module)}`;
|
|
212
|
+
}
|
|
213
|
+
else if (imp.group) {
|
|
214
|
+
// Different non-empty group: go up to model root, then into the group
|
|
215
|
+
modPath = `super::super::${imp.group}::${toSnakeCase(imp.module)}`;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// Root-level module accessed from inside a group subfolder
|
|
219
|
+
modPath = `super::super::${toSnakeCase(imp.module)}`;
|
|
220
|
+
}
|
|
221
|
+
if (processedNames.length === 1) {
|
|
222
|
+
lines.push(`use ${modPath}::${names};`);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
lines.push(`use ${modPath}::{${names}};`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
lines.push("");
|
|
229
|
+
// String-literal enum types
|
|
230
|
+
for (const enumDef of file.enums) {
|
|
231
|
+
emitStringEnum(enumDef, lines);
|
|
232
|
+
}
|
|
233
|
+
// Find the base type (the one that owns the polymorphic dispatch)
|
|
234
|
+
const baseType = file.types.find(t => t.polymorphicDispatch) || file.types[0];
|
|
235
|
+
const childTypes = file.types.filter(t => t !== baseType);
|
|
236
|
+
// Protocol types → emit as trait (no struct, no impl)
|
|
237
|
+
if (baseType.isProtocol) {
|
|
238
|
+
emitProtocolTrait(baseType, lines);
|
|
239
|
+
lines.push("");
|
|
240
|
+
return renderLines(lines);
|
|
241
|
+
}
|
|
242
|
+
// Collect base field names for variant field extraction
|
|
243
|
+
const baseFieldNames = new Set(baseType.fields.map(f => f.name));
|
|
244
|
+
// Emit enum for polymorphic types
|
|
245
|
+
if (baseType.polymorphicDispatch) {
|
|
246
|
+
emitKindEnum(baseType, childTypes, baseFieldNames, polymorphicTypeNames, lines);
|
|
247
|
+
}
|
|
248
|
+
// Emit struct + impl for the base type (only one struct per file in Rust)
|
|
249
|
+
emitStruct(baseType, polymorphicTypeNames, lines);
|
|
250
|
+
emitImpl(baseType, childTypes, baseFieldNames, visitor, polymorphicTypeNames, lines);
|
|
251
|
+
// Method stubs as trait
|
|
252
|
+
if (baseType.methods.length > 0) {
|
|
253
|
+
emitMethodTrait(baseType, lines);
|
|
254
|
+
}
|
|
255
|
+
// Trailing newlines for child types that were folded into the enum
|
|
256
|
+
for (const _ of childTypes) {
|
|
257
|
+
lines.push("");
|
|
258
|
+
}
|
|
259
|
+
lines.push("");
|
|
260
|
+
return renderLines(lines);
|
|
261
|
+
}
|
|
262
|
+
// ============================================================================
|
|
263
|
+
// Kind Enum
|
|
264
|
+
// ============================================================================
|
|
265
|
+
function emitKindEnum(baseType, childTypes, baseFieldNames, polymorphicTypeNames, lines) {
|
|
266
|
+
const dispatch = baseType.polymorphicDispatch;
|
|
267
|
+
const enumName = baseType.typeName.name + "Kind";
|
|
268
|
+
lines.push("");
|
|
269
|
+
lines.push(`/// Variant-specific data for [\`${baseType.typeName.name}\`], discriminated by \`${dispatch.discriminatorField}\`.`);
|
|
270
|
+
lines.push("#[derive(Debug, Clone)]");
|
|
271
|
+
lines.push(`pub enum ${enumName} {`);
|
|
272
|
+
// Named variants
|
|
273
|
+
for (const variant of dispatch.variants) {
|
|
274
|
+
const childType = childTypes.find(ct => ct.typeName.name === variant.typeName.name);
|
|
275
|
+
const variantName = variant.typeName.name.replace(baseType.typeName.name, "") || variant.typeName.name;
|
|
276
|
+
const variantFields = childType
|
|
277
|
+
? childType.fields.filter(f => f.name !== dispatch.discriminatorField && !baseFieldNames.has(f.name))
|
|
278
|
+
: [];
|
|
279
|
+
lines.push(` /// \`${dispatch.discriminatorField}\` = \`"${variant.value}"\``);
|
|
280
|
+
if (variantFields.length === 0) {
|
|
281
|
+
lines.push(` ${variantName},`);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
lines.push(` ${variantName} {`);
|
|
285
|
+
for (const field of variantFields) {
|
|
286
|
+
if (field.description) {
|
|
287
|
+
emitDocComment(field.description, " ", lines);
|
|
288
|
+
}
|
|
289
|
+
lines.push(` ${rustFieldName(field.name)}: ${fieldType(field, polymorphicTypeNames)},`);
|
|
290
|
+
}
|
|
291
|
+
lines.push(` },`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Wildcard/default variant
|
|
295
|
+
if (dispatch.defaultVariant) {
|
|
296
|
+
const defaultType = childTypes.find(ct => ct.typeName.name === dispatch.defaultVariant.typeName.name);
|
|
297
|
+
const isSelfRef = dispatch.defaultVariant.isSelfReference;
|
|
298
|
+
const variantName = isSelfRef
|
|
299
|
+
? "Custom"
|
|
300
|
+
: (dispatch.defaultVariant.typeName.name.replace(baseType.typeName.name, "") || "Custom");
|
|
301
|
+
const variantFields = isSelfRef
|
|
302
|
+
? []
|
|
303
|
+
: (defaultType
|
|
304
|
+
? defaultType.fields.filter(f => f.name !== dispatch.discriminatorField && !baseFieldNames.has(f.name))
|
|
305
|
+
: []);
|
|
306
|
+
lines.push(` /// Wildcard / catch-all variant for unrecognized \`${dispatch.discriminatorField}\` values.`);
|
|
307
|
+
// Wildcard always has kind_name field
|
|
308
|
+
if (variantFields.length === 0) {
|
|
309
|
+
lines.push(` ${variantName} {`);
|
|
310
|
+
lines.push(` /// The raw \`${dispatch.discriminatorField}\` string for this unknown variant.`);
|
|
311
|
+
lines.push(` ${toSnakeCase(dispatch.discriminatorField)}_name: String,`);
|
|
312
|
+
lines.push(` },`);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
lines.push(` ${variantName} {`);
|
|
316
|
+
for (const field of variantFields) {
|
|
317
|
+
if (field.description) {
|
|
318
|
+
emitDocComment(field.description, " ", lines);
|
|
319
|
+
}
|
|
320
|
+
lines.push(` ${rustFieldName(field.name)}: ${fieldType(field, polymorphicTypeNames)},`);
|
|
321
|
+
}
|
|
322
|
+
lines.push(` /// The raw \`${dispatch.discriminatorField}\` string for this unknown variant.`);
|
|
323
|
+
lines.push(` ${toSnakeCase(dispatch.discriminatorField)}_name: String,`);
|
|
324
|
+
lines.push(` },`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
lines.push("}");
|
|
328
|
+
lines.push("");
|
|
329
|
+
// impl Default for the enum
|
|
330
|
+
emitEnumDefault(baseType, childTypes, baseFieldNames, dispatch, polymorphicTypeNames, lines);
|
|
331
|
+
}
|
|
332
|
+
function emitEnumDefault(baseType, childTypes, baseFieldNames, dispatch, polymorphicTypeNames, lines) {
|
|
333
|
+
const enumName = baseType.typeName.name + "Kind";
|
|
334
|
+
lines.push(`impl Default for ${enumName} {`);
|
|
335
|
+
lines.push(" fn default() -> Self {");
|
|
336
|
+
if (dispatch.defaultVariant) {
|
|
337
|
+
// Default to the wildcard/custom variant
|
|
338
|
+
const isSelfRef = dispatch.defaultVariant.isSelfReference;
|
|
339
|
+
const variantName = isSelfRef
|
|
340
|
+
? "Custom"
|
|
341
|
+
: (dispatch.defaultVariant.typeName.name.replace(baseType.typeName.name, "") || "Custom");
|
|
342
|
+
const defaultType = childTypes.find(ct => ct.typeName.name === dispatch.defaultVariant.typeName.name);
|
|
343
|
+
const variantFields = isSelfRef
|
|
344
|
+
? []
|
|
345
|
+
: (defaultType
|
|
346
|
+
? defaultType.fields.filter(f => f.name !== dispatch.discriminatorField && !baseFieldNames.has(f.name))
|
|
347
|
+
: []);
|
|
348
|
+
if (variantFields.length === 0) {
|
|
349
|
+
lines.push(` ${enumName}::${variantName} {`);
|
|
350
|
+
lines.push(` ${toSnakeCase(dispatch.discriminatorField)}_name: String::new(),`);
|
|
351
|
+
lines.push(" }");
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
lines.push(` ${enumName}::${variantName} {`);
|
|
355
|
+
for (const field of variantFields) {
|
|
356
|
+
lines.push(` ${rustFieldName(field.name)}: ${fieldDefault(field, polymorphicTypeNames)},`);
|
|
357
|
+
}
|
|
358
|
+
lines.push(` ${toSnakeCase(dispatch.discriminatorField)}_name: String::new(),`);
|
|
359
|
+
lines.push(" }");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
else if (dispatch.variants.length > 0) {
|
|
363
|
+
// Default to the first named variant
|
|
364
|
+
const firstVariant = dispatch.variants[0];
|
|
365
|
+
const childType = childTypes.find(ct => ct.typeName.name === firstVariant.typeName.name);
|
|
366
|
+
const variantName = firstVariant.typeName.name.replace(baseType.typeName.name, "") || firstVariant.typeName.name;
|
|
367
|
+
const variantFields = childType
|
|
368
|
+
? childType.fields.filter(f => f.name !== dispatch.discriminatorField && !baseFieldNames.has(f.name))
|
|
369
|
+
: [];
|
|
370
|
+
if (variantFields.length === 0) {
|
|
371
|
+
lines.push(` ${enumName}::${variantName}`);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
lines.push(` ${enumName}::${variantName} {`);
|
|
375
|
+
for (const field of variantFields) {
|
|
376
|
+
lines.push(` ${rustFieldName(field.name)}: ${fieldDefault(field, polymorphicTypeNames)},`);
|
|
377
|
+
}
|
|
378
|
+
lines.push(" }");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
lines.push(" }");
|
|
382
|
+
lines.push("}");
|
|
383
|
+
}
|
|
384
|
+
// ============================================================================
|
|
385
|
+
// Struct Definition
|
|
386
|
+
// ============================================================================
|
|
387
|
+
function emitStruct(type, polymorphicTypeNames, lines) {
|
|
388
|
+
if (type.description) {
|
|
389
|
+
emitDocComment(type.description, "", lines);
|
|
390
|
+
}
|
|
391
|
+
if (type.wire) {
|
|
392
|
+
lines.push("#[derive(Debug, Clone, Default, serde::Serialize)]");
|
|
393
|
+
lines.push('#[serde(rename_all = "camelCase")]');
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
lines.push("#[derive(Debug, Clone, Default)]");
|
|
397
|
+
}
|
|
398
|
+
lines.push(`pub struct ${type.typeName.name} {`);
|
|
399
|
+
for (const field of type.fields) {
|
|
400
|
+
// Skip discriminator field — it's represented by the XxxKind enum field
|
|
401
|
+
if (type.polymorphicDispatch && field.name === type.polymorphicDispatch.discriminatorField)
|
|
402
|
+
continue;
|
|
403
|
+
if (field.description) {
|
|
404
|
+
emitDocComment(field.description, " ", lines);
|
|
405
|
+
}
|
|
406
|
+
lines.push(` pub ${rustFieldName(field.name)}: ${fieldType(field, polymorphicTypeNames)},`);
|
|
407
|
+
}
|
|
408
|
+
// Add kind field for polymorphic types
|
|
409
|
+
if (type.polymorphicDispatch) {
|
|
410
|
+
lines.push(` /// Variant-specific data, discriminated by \`${type.polymorphicDispatch.discriminatorField}\`.`);
|
|
411
|
+
lines.push(` pub ${rustFieldName(type.polymorphicDispatch.discriminatorField)}: ${type.typeName.name}Kind,`);
|
|
412
|
+
}
|
|
413
|
+
lines.push("}");
|
|
414
|
+
}
|
|
415
|
+
// ============================================================================
|
|
416
|
+
// Impl Block
|
|
417
|
+
// ============================================================================
|
|
418
|
+
function emitImpl(type, childTypes, baseFieldNames, visitor, polymorphicTypeNames, lines) {
|
|
419
|
+
const name = type.typeName.name;
|
|
420
|
+
lines.push("");
|
|
421
|
+
lines.push(`impl ${name} {`);
|
|
422
|
+
// new()
|
|
423
|
+
lines.push(` /// Create a new ${name} with default values.`);
|
|
424
|
+
lines.push(" pub fn new() -> Self {");
|
|
425
|
+
lines.push(" Self::default()");
|
|
426
|
+
lines.push(" }");
|
|
427
|
+
lines.push("");
|
|
428
|
+
// from_json()
|
|
429
|
+
emitFromJson(name, lines);
|
|
430
|
+
// from_yaml()
|
|
431
|
+
emitFromYaml(name, lines);
|
|
432
|
+
// load_from_value()
|
|
433
|
+
emitLoadFromValue(name, type, childTypes, baseFieldNames, polymorphicTypeNames, lines);
|
|
434
|
+
// kind_str() for polymorphic types
|
|
435
|
+
if (type.polymorphicDispatch) {
|
|
436
|
+
emitKindStr(type, childTypes, lines);
|
|
437
|
+
}
|
|
438
|
+
// to_value()
|
|
439
|
+
emitToValue(name, type, childTypes, baseFieldNames, polymorphicTypeNames, lines);
|
|
440
|
+
// to_json() / to_yaml()
|
|
441
|
+
emitToJson(name, lines);
|
|
442
|
+
emitToYaml(name, lines);
|
|
443
|
+
// to_wire() (only when wire mappings exist)
|
|
444
|
+
if (type.wire) {
|
|
445
|
+
emitToWireMethod(type, lines);
|
|
446
|
+
}
|
|
447
|
+
// Dict accessor helpers (only for dict-category fields, not scalar value types)
|
|
448
|
+
for (const field of type.fields) {
|
|
449
|
+
if (field.category.kind === "dict") {
|
|
450
|
+
emitDictAccessor(field, lines);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Collection helpers — include both base type and child type helpers
|
|
454
|
+
// (in Rust, child types are enum variants, so their helpers live on the parent impl)
|
|
455
|
+
const emittedHelpers = new Set();
|
|
456
|
+
const allHelpers = [...type.collectionHelpers];
|
|
457
|
+
for (const child of childTypes) {
|
|
458
|
+
for (const h of child.collectionHelpers) {
|
|
459
|
+
if (!emittedHelpers.has(h.propertyName)) {
|
|
460
|
+
allHelpers.push(h);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
for (const helper of allHelpers) {
|
|
465
|
+
if (emittedHelpers.has(helper.propertyName))
|
|
466
|
+
continue;
|
|
467
|
+
emittedHelpers.add(helper.propertyName);
|
|
468
|
+
emitCollectionLoadHelper(helper, polymorphicTypeNames, lines);
|
|
469
|
+
emitCollectionSaveHelper(helper, lines);
|
|
470
|
+
}
|
|
471
|
+
// Factories
|
|
472
|
+
for (const factory of type.factories) {
|
|
473
|
+
emitFactory(name, factory, visitor, lines);
|
|
474
|
+
}
|
|
475
|
+
lines.push("}");
|
|
476
|
+
// Method trait stubs (handled separately)
|
|
477
|
+
}
|
|
478
|
+
// ============================================================================
|
|
479
|
+
// from_json / from_yaml
|
|
480
|
+
// ============================================================================
|
|
481
|
+
function emitFromJson(name, lines) {
|
|
482
|
+
lines.push(` /// Load ${name} from a JSON string.`);
|
|
483
|
+
lines.push(` pub fn from_json(json: &str, ctx: &LoadContext) -> Result<Self, serde_json::Error> {`);
|
|
484
|
+
lines.push(" let value: serde_json::Value = serde_json::from_str(json)?;");
|
|
485
|
+
lines.push(" Ok(Self::load_from_value(&value, ctx))");
|
|
486
|
+
lines.push(" }");
|
|
487
|
+
lines.push("");
|
|
488
|
+
}
|
|
489
|
+
function emitFromYaml(name, lines) {
|
|
490
|
+
lines.push(` /// Load ${name} from a YAML string.`);
|
|
491
|
+
lines.push(` pub fn from_yaml(yaml: &str, ctx: &LoadContext) -> Result<Self, serde_yaml::Error> {`);
|
|
492
|
+
lines.push(" let value: serde_json::Value = serde_yaml::from_str(yaml)?;");
|
|
493
|
+
lines.push(" Ok(Self::load_from_value(&value, ctx))");
|
|
494
|
+
lines.push(" }");
|
|
495
|
+
lines.push("");
|
|
496
|
+
}
|
|
497
|
+
// ============================================================================
|
|
498
|
+
// load_from_value
|
|
499
|
+
// ============================================================================
|
|
500
|
+
function emitLoadFromValue(name, type, childTypes, baseFieldNames, polymorphicTypeNames, lines) {
|
|
501
|
+
lines.push(` /// Load ${name} from a \`serde_json::Value\`.`);
|
|
502
|
+
lines.push(" ///");
|
|
503
|
+
lines.push(" /// Calls `ctx.process_input` before field extraction.");
|
|
504
|
+
lines.push(" pub fn load_from_value(value: &serde_json::Value, ctx: &LoadContext) -> Self {");
|
|
505
|
+
lines.push(" let value = ctx.process_input(value.clone());");
|
|
506
|
+
// Coercions
|
|
507
|
+
for (const c of type.load.coercions) {
|
|
508
|
+
emitCoercionBranch(name, c, type, childTypes, baseFieldNames, polymorphicTypeNames, lines);
|
|
509
|
+
}
|
|
510
|
+
// Polymorphic dispatch
|
|
511
|
+
if (type.polymorphicDispatch) {
|
|
512
|
+
emitPolymorphicLoad(name, type, childTypes, baseFieldNames, polymorphicTypeNames, lines);
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
// Simple struct construction
|
|
516
|
+
lines.push(" Self {");
|
|
517
|
+
for (const a of type.load.assignments) {
|
|
518
|
+
lines.push(` ${rustFieldName(a.fieldName)}: ${loadExpr(a, polymorphicTypeNames)},`);
|
|
519
|
+
}
|
|
520
|
+
lines.push(" }");
|
|
521
|
+
}
|
|
522
|
+
lines.push(" }");
|
|
523
|
+
lines.push("");
|
|
524
|
+
}
|
|
525
|
+
function emitCoercionBranch(typeName, c, type, childTypes, baseFieldNames, polymorphicTypeNames, lines) {
|
|
526
|
+
const check = rustCoercionCheck(c.scalarType);
|
|
527
|
+
if (!check)
|
|
528
|
+
return;
|
|
529
|
+
lines.push(` ${check.ifLet} {`);
|
|
530
|
+
// For string coercions, rebind `s` → `value` so downstream references work
|
|
531
|
+
if (c.scalarType === "string") {
|
|
532
|
+
lines.push(" let value = s.to_string();");
|
|
533
|
+
}
|
|
534
|
+
const dispatch = type.polymorphicDispatch;
|
|
535
|
+
const discField = dispatch?.discriminatorField;
|
|
536
|
+
const enumName = typeName + "Kind";
|
|
537
|
+
// Build field assignments, handling discriminator → enum variant construction
|
|
538
|
+
const fieldAssignments = c.assignments.map(a => {
|
|
539
|
+
const snake = rustFieldName(a.fieldName);
|
|
540
|
+
if (a.isInput) {
|
|
541
|
+
// Check if the target field is optional or an enum
|
|
542
|
+
const targetField = type.fields.find(f => f.name === a.fieldName);
|
|
543
|
+
const isOptional = targetField?.isOptional ?? false;
|
|
544
|
+
// If target field is a named enum, use from_str_opt instead of .into()
|
|
545
|
+
if (targetField?.enumName && targetField.allowedValues.length > 0) {
|
|
546
|
+
const defaultVal = String(targetField.defaultValue || targetField.allowedValues[0]);
|
|
547
|
+
const enumExpr = `${targetField.enumName}::from_str_opt(&value).unwrap_or(${targetField.enumName}::${toPascalCase(defaultVal)})`;
|
|
548
|
+
return isOptional ? `${snake}: Some(${enumExpr})` : `${snake}: ${enumExpr}`;
|
|
549
|
+
}
|
|
550
|
+
const expr = isOptional ? "Some(value.into())" : "value.into()";
|
|
551
|
+
return `${snake}: ${expr}`;
|
|
552
|
+
}
|
|
553
|
+
// Check if this field is the discriminator — must construct enum variant
|
|
554
|
+
if (dispatch && a.fieldName === discField) {
|
|
555
|
+
const discSnake = toSnakeCase(discField);
|
|
556
|
+
// Find if the literal matches a named variant
|
|
557
|
+
const matchingVariant = dispatch.variants.find(v => v.value === a.literalValue);
|
|
558
|
+
if (matchingVariant) {
|
|
559
|
+
const variantName = matchingVariant.typeName.name.replace(typeName, "") || matchingVariant.typeName.name;
|
|
560
|
+
// Check if variant has fields — unit variants don't get braces
|
|
561
|
+
const childType = childTypes.find(ct => ct.typeName.name === matchingVariant.typeName.name);
|
|
562
|
+
const variantFields = childType
|
|
563
|
+
? childType.fields.filter(f => f.name !== dispatch.discriminatorField && !baseFieldNames.has(f.name))
|
|
564
|
+
: [];
|
|
565
|
+
if (variantFields.length === 0) {
|
|
566
|
+
return `${discSnake}: ${enumName}::${variantName}`;
|
|
567
|
+
}
|
|
568
|
+
return `${discSnake}: ${enumName}::${variantName} { ..Default::default() }`;
|
|
569
|
+
}
|
|
570
|
+
// Otherwise use the wildcard/custom variant
|
|
571
|
+
if (dispatch.defaultVariant) {
|
|
572
|
+
const dvName = dispatch.defaultVariant.isSelfReference
|
|
573
|
+
? "Custom"
|
|
574
|
+
: (dispatch.defaultVariant.typeName.name.replace(typeName, "") || "Custom");
|
|
575
|
+
return `${discSnake}: ${enumName}::${dvName} { ${discSnake}_name: "${a.literalValue}".to_string() }`;
|
|
576
|
+
}
|
|
577
|
+
return `${discSnake}: ${enumName}::default()`;
|
|
578
|
+
}
|
|
579
|
+
return `${snake}: "${a.literalValue}".to_string()`;
|
|
580
|
+
});
|
|
581
|
+
lines.push(` return ${typeName} { ${fieldAssignments.join(", ")}, ..Default::default() };`);
|
|
582
|
+
lines.push(" }");
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Get the Rust coercion check pattern for a scalar type.
|
|
586
|
+
*/
|
|
587
|
+
function rustCoercionCheck(scalarType) {
|
|
588
|
+
switch (scalarType) {
|
|
589
|
+
case "string":
|
|
590
|
+
return { ifLet: "if let Some(s) = value.as_str()" };
|
|
591
|
+
case "boolean":
|
|
592
|
+
return { ifLet: "if let Some(value) = value.as_bool()" };
|
|
593
|
+
case "float64":
|
|
594
|
+
case "float":
|
|
595
|
+
case "number":
|
|
596
|
+
case "numeric":
|
|
597
|
+
return { ifLet: "if let Some(value) = value.as_f64()" };
|
|
598
|
+
case "int32":
|
|
599
|
+
case "int64":
|
|
600
|
+
case "integer":
|
|
601
|
+
return { ifLet: "if let Some(value) = value.as_i64()" };
|
|
602
|
+
default:
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Get the Rust expression to convert a coercion value to the target field type.
|
|
608
|
+
*/
|
|
609
|
+
function coercionIntoValue(scalarType) {
|
|
610
|
+
// `value` is already bound in the coercion branch
|
|
611
|
+
// For string coercions: value is String, .into() for String → String
|
|
612
|
+
// For numeric/bool: value is the extracted primitive
|
|
613
|
+
return "value.into()";
|
|
614
|
+
}
|
|
615
|
+
function emitPolymorphicLoad(name, type, childTypes, baseFieldNames, polymorphicTypeNames, lines) {
|
|
616
|
+
const dispatch = type.polymorphicDispatch;
|
|
617
|
+
const discSnake = rustFieldName(dispatch.discriminatorField);
|
|
618
|
+
const enumName = name + "Kind";
|
|
619
|
+
lines.push(` let ${discSnake}_str = value.get("${dispatch.discriminatorField}").and_then(|v| v.as_str()).unwrap_or("");`);
|
|
620
|
+
lines.push(` let ${discSnake} = match ${discSnake}_str {`);
|
|
621
|
+
// Named variants
|
|
622
|
+
for (const variant of dispatch.variants) {
|
|
623
|
+
const childType = childTypes.find(ct => ct.typeName.name === variant.typeName.name);
|
|
624
|
+
const variantName = variant.typeName.name.replace(name, "") || variant.typeName.name;
|
|
625
|
+
const variantFields = childType
|
|
626
|
+
? childType.fields.filter(f => f.name !== dispatch.discriminatorField && !baseFieldNames.has(f.name))
|
|
627
|
+
: [];
|
|
628
|
+
if (variantFields.length === 0) {
|
|
629
|
+
lines.push(` "${variant.value}" => ${enumName}::${variantName},`);
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
lines.push(` "${variant.value}" => ${enumName}::${variantName} {`);
|
|
633
|
+
for (const field of variantFields) {
|
|
634
|
+
const assignment = variantLoadExpr(field, name, polymorphicTypeNames);
|
|
635
|
+
lines.push(` ${rustFieldName(field.name)}: ${assignment},`);
|
|
636
|
+
}
|
|
637
|
+
lines.push(` },`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// Default / wildcard
|
|
641
|
+
if (dispatch.defaultVariant) {
|
|
642
|
+
const isSelfRef = dispatch.defaultVariant.isSelfReference;
|
|
643
|
+
const variantName = isSelfRef
|
|
644
|
+
? "Custom"
|
|
645
|
+
: (dispatch.defaultVariant.typeName.name.replace(name, "") || "Custom");
|
|
646
|
+
const defaultType = childTypes.find(ct => ct.typeName.name === dispatch.defaultVariant.typeName.name);
|
|
647
|
+
const variantFields = isSelfRef
|
|
648
|
+
? []
|
|
649
|
+
: (defaultType
|
|
650
|
+
? defaultType.fields.filter(f => f.name !== dispatch.discriminatorField && !baseFieldNames.has(f.name))
|
|
651
|
+
: []);
|
|
652
|
+
if (variantFields.length === 0) {
|
|
653
|
+
lines.push(` _ => ${enumName}::${variantName} {`);
|
|
654
|
+
lines.push(` ${discSnake}_name: ${discSnake}_str.to_string(),`);
|
|
655
|
+
lines.push(` },`);
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
lines.push(` _ => ${enumName}::${variantName} {`);
|
|
659
|
+
for (const field of variantFields) {
|
|
660
|
+
const assignment = variantLoadExpr(field, name, polymorphicTypeNames);
|
|
661
|
+
lines.push(` ${rustFieldName(field.name)}: ${assignment},`);
|
|
662
|
+
}
|
|
663
|
+
lines.push(` ${discSnake}_name: ${discSnake}_str.to_string(),`);
|
|
664
|
+
lines.push(` },`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
lines.push(` _ => ${enumName}::default(),`);
|
|
669
|
+
}
|
|
670
|
+
lines.push(" };");
|
|
671
|
+
// Construct the struct with base fields + kind
|
|
672
|
+
lines.push(" Self {");
|
|
673
|
+
for (const a of type.load.assignments) {
|
|
674
|
+
// Skip discriminator — it's stored in the enum field
|
|
675
|
+
if (a.fieldName === dispatch.discriminatorField)
|
|
676
|
+
continue;
|
|
677
|
+
lines.push(` ${rustFieldName(a.fieldName)}: ${loadExpr(a, polymorphicTypeNames)},`);
|
|
678
|
+
}
|
|
679
|
+
lines.push(` ${discSnake}: ${discSnake},`);
|
|
680
|
+
lines.push(" }");
|
|
681
|
+
}
|
|
682
|
+
// ============================================================================
|
|
683
|
+
// kind_str
|
|
684
|
+
// ============================================================================
|
|
685
|
+
function emitKindStr(type, childTypes, lines) {
|
|
686
|
+
const dispatch = type.polymorphicDispatch;
|
|
687
|
+
const baseFieldNames = new Set(type.fields.map(f => f.name));
|
|
688
|
+
const enumName = type.typeName.name + "Kind";
|
|
689
|
+
const discSnake = rustFieldName(dispatch.discriminatorField);
|
|
690
|
+
lines.push(` /// Returns the \`${dispatch.discriminatorField}\` discriminator string for this instance.`);
|
|
691
|
+
lines.push(` pub fn ${discSnake}_str(&self) -> &str {`);
|
|
692
|
+
lines.push(` match &self.${discSnake} {`);
|
|
693
|
+
for (const variant of dispatch.variants) {
|
|
694
|
+
const variantName = variant.typeName.name.replace(type.typeName.name, "") || variant.typeName.name;
|
|
695
|
+
const childType = childTypes.find(ct => ct.typeName.name === variant.typeName.name);
|
|
696
|
+
const variantFields = childType
|
|
697
|
+
? childType.fields.filter(f => f.name !== dispatch.discriminatorField && !baseFieldNames.has(f.name))
|
|
698
|
+
: [];
|
|
699
|
+
if (variantFields.length === 0) {
|
|
700
|
+
lines.push(` ${enumName}::${variantName} => "${variant.value}",`);
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
lines.push(` ${enumName}::${variantName} { .. } => "${variant.value}",`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (dispatch.defaultVariant) {
|
|
707
|
+
const isSelfRef = dispatch.defaultVariant.isSelfReference;
|
|
708
|
+
const variantName = isSelfRef
|
|
709
|
+
? "Custom"
|
|
710
|
+
: (dispatch.defaultVariant.typeName.name.replace(type.typeName.name, "") || "Custom");
|
|
711
|
+
lines.push(` ${enumName}::${variantName} { ${discSnake}_name, .. } => ${discSnake}_name.as_str(),`);
|
|
712
|
+
}
|
|
713
|
+
lines.push(" }");
|
|
714
|
+
lines.push(" }");
|
|
715
|
+
lines.push("");
|
|
716
|
+
}
|
|
717
|
+
// ============================================================================
|
|
718
|
+
// to_value
|
|
719
|
+
// ============================================================================
|
|
720
|
+
function emitToValue(name, type, childTypes, baseFieldNames, polymorphicTypeNames, lines) {
|
|
721
|
+
lines.push(` /// Serialize ${name} to a \`serde_json::Value\`.`);
|
|
722
|
+
lines.push(" ///");
|
|
723
|
+
lines.push(" /// Calls `ctx.process_dict` after serialization.");
|
|
724
|
+
lines.push(" pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value {");
|
|
725
|
+
lines.push(" let mut result = serde_json::Map::new();");
|
|
726
|
+
// Write discriminator first
|
|
727
|
+
if (type.polymorphicDispatch) {
|
|
728
|
+
const discSnake = rustFieldName(type.polymorphicDispatch.discriminatorField);
|
|
729
|
+
lines.push(` // Write the discriminator`);
|
|
730
|
+
lines.push(` result.insert("${type.polymorphicDispatch.discriminatorField}".to_string(), serde_json::Value::String(self.${discSnake}_str().to_string()));`);
|
|
731
|
+
}
|
|
732
|
+
// Write base fields
|
|
733
|
+
if (type.save.assignments.length > 0) {
|
|
734
|
+
lines.push(` // Write base fields`);
|
|
735
|
+
}
|
|
736
|
+
for (const a of type.save.assignments) {
|
|
737
|
+
// Skip discriminator — it's written explicitly above from kind_str()
|
|
738
|
+
if (type.polymorphicDispatch && a.fieldName === type.polymorphicDispatch.discriminatorField)
|
|
739
|
+
continue;
|
|
740
|
+
emitSaveField(a, "self.", polymorphicTypeNames, lines, " ");
|
|
741
|
+
}
|
|
742
|
+
// Write variant-specific fields
|
|
743
|
+
if (type.polymorphicDispatch) {
|
|
744
|
+
emitVariantSave(type, childTypes, baseFieldNames, polymorphicTypeNames, lines);
|
|
745
|
+
}
|
|
746
|
+
lines.push(" ctx.process_dict(serde_json::Value::Object(result))");
|
|
747
|
+
lines.push(" }");
|
|
748
|
+
lines.push("");
|
|
749
|
+
}
|
|
750
|
+
function emitVariantSave(type, childTypes, baseFieldNames, polymorphicTypeNames, lines) {
|
|
751
|
+
const dispatch = type.polymorphicDispatch;
|
|
752
|
+
const enumName = type.typeName.name + "Kind";
|
|
753
|
+
const discSnake = rustFieldName(dispatch.discriminatorField);
|
|
754
|
+
lines.push(" // Write variant-specific fields");
|
|
755
|
+
lines.push(` match &self.${discSnake} {`);
|
|
756
|
+
for (const variant of dispatch.variants) {
|
|
757
|
+
const childType = childTypes.find(ct => ct.typeName.name === variant.typeName.name);
|
|
758
|
+
const variantName = variant.typeName.name.replace(type.typeName.name, "") || variant.typeName.name;
|
|
759
|
+
const variantFields = childType
|
|
760
|
+
? childType.fields.filter(f => f.name !== dispatch.discriminatorField && !baseFieldNames.has(f.name))
|
|
761
|
+
: [];
|
|
762
|
+
if (variantFields.length === 0) {
|
|
763
|
+
lines.push(` ${enumName}::${variantName} => {`);
|
|
764
|
+
lines.push(" }");
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
const destructure = variantFields.map(f => rustFieldName(f.name)).join(", ");
|
|
768
|
+
lines.push(` ${enumName}::${variantName} { ${destructure}, .. } => {`);
|
|
769
|
+
for (const field of variantFields) {
|
|
770
|
+
emitVariantSaveField(field, polymorphicTypeNames, type.typeName.name, lines);
|
|
771
|
+
}
|
|
772
|
+
lines.push(" }");
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
// Wildcard variant
|
|
776
|
+
if (dispatch.defaultVariant) {
|
|
777
|
+
const isSelfRef = dispatch.defaultVariant.isSelfReference;
|
|
778
|
+
const variantName = isSelfRef
|
|
779
|
+
? "Custom"
|
|
780
|
+
: (dispatch.defaultVariant.typeName.name.replace(type.typeName.name, "") || "Custom");
|
|
781
|
+
const defaultType = childTypes.find(ct => ct.typeName.name === dispatch.defaultVariant.typeName.name);
|
|
782
|
+
const variantFields = isSelfRef
|
|
783
|
+
? []
|
|
784
|
+
: (defaultType
|
|
785
|
+
? defaultType.fields.filter(f => f.name !== dispatch.discriminatorField && !baseFieldNames.has(f.name))
|
|
786
|
+
: []);
|
|
787
|
+
if (variantFields.length === 0) {
|
|
788
|
+
lines.push(` ${enumName}::${variantName} { ${discSnake}_name: _, .. } => {`);
|
|
789
|
+
lines.push(" }");
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
const destructure = variantFields.map(f => rustFieldName(f.name)).join(", ");
|
|
793
|
+
lines.push(` ${enumName}::${variantName} { ${destructure}, ${discSnake}_name: _, .. } => {`);
|
|
794
|
+
for (const field of variantFields) {
|
|
795
|
+
emitVariantSaveField(field, polymorphicTypeNames, type.typeName.name, lines);
|
|
796
|
+
}
|
|
797
|
+
lines.push(" }");
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
lines.push(" }");
|
|
801
|
+
}
|
|
802
|
+
// ============================================================================
|
|
803
|
+
// to_json / to_yaml
|
|
804
|
+
// ============================================================================
|
|
805
|
+
function emitToJson(name, lines) {
|
|
806
|
+
lines.push(` /// Serialize ${name} to a JSON string.`);
|
|
807
|
+
lines.push(` pub fn to_json(&self, ctx: &SaveContext) -> Result<String, serde_json::Error> {`);
|
|
808
|
+
lines.push(" serde_json::to_string_pretty(&self.to_value(ctx))");
|
|
809
|
+
lines.push(" }");
|
|
810
|
+
lines.push("");
|
|
811
|
+
}
|
|
812
|
+
function emitToYaml(name, lines) {
|
|
813
|
+
lines.push(` /// Serialize ${name} to a YAML string.`);
|
|
814
|
+
lines.push(` pub fn to_yaml(&self, ctx: &SaveContext) -> Result<String, serde_yaml::Error> {`);
|
|
815
|
+
lines.push(" serde_yaml::to_string(&self.to_value(ctx))");
|
|
816
|
+
lines.push(" }");
|
|
817
|
+
}
|
|
818
|
+
// ============================================================================
|
|
819
|
+
// to_wire — provider-specific wire format conversion
|
|
820
|
+
// ============================================================================
|
|
821
|
+
/**
|
|
822
|
+
* Emit a `to_wire(&self, provider: &str) -> serde_json::Value` method that
|
|
823
|
+
* serializes the struct via `serde_json::to_value`, then remaps field names
|
|
824
|
+
* according to provider-specific wire mappings.
|
|
825
|
+
*
|
|
826
|
+
* Only emitted when `type.wire` is non-null.
|
|
827
|
+
*/
|
|
828
|
+
function emitToWireMethod(type, lines) {
|
|
829
|
+
const wire = type.wire;
|
|
830
|
+
const name = type.typeName.name;
|
|
831
|
+
lines.push("");
|
|
832
|
+
lines.push(` /// Convert to provider-specific wire format.`);
|
|
833
|
+
lines.push(` pub fn to_wire(&self, provider: &str) -> serde_json::Value {`);
|
|
834
|
+
lines.push(` let data = serde_json::to_value(self).unwrap_or_default();`);
|
|
835
|
+
lines.push(` let mut result = serde_json::Map::new();`);
|
|
836
|
+
// Build the wire_map HashMap literal
|
|
837
|
+
lines.push(` let wire_map: std::collections::HashMap<&str, std::collections::HashMap<&str, &str>> = std::collections::HashMap::from([`);
|
|
838
|
+
for (const mapping of wire.mappings) {
|
|
839
|
+
const entries = Object.entries(mapping.wireNames);
|
|
840
|
+
const inner = entries.map(([provider, wireName]) => `("${provider}", "${wireName}")`).join(", ");
|
|
841
|
+
lines.push(` ("${mapping.fieldName}", std::collections::HashMap::from([${inner}])),`);
|
|
842
|
+
}
|
|
843
|
+
lines.push(` ]);`);
|
|
844
|
+
// Iterate over serialized keys and remap (strict: only emit mapped fields)
|
|
845
|
+
lines.push(` if let serde_json::Value::Object(map) = data {`);
|
|
846
|
+
lines.push(` for (key, value) in map {`);
|
|
847
|
+
lines.push(` if let Some(mapping) = wire_map.get(key.as_str()) {`);
|
|
848
|
+
lines.push(` if let Some(wire_name) = mapping.get(provider) {`);
|
|
849
|
+
lines.push(` result.insert(wire_name.to_string(), value);`);
|
|
850
|
+
lines.push(` }`);
|
|
851
|
+
lines.push(` }`);
|
|
852
|
+
lines.push(` }`);
|
|
853
|
+
lines.push(` }`);
|
|
854
|
+
lines.push(` serde_json::Value::Object(result)`);
|
|
855
|
+
lines.push(` }`);
|
|
856
|
+
}
|
|
857
|
+
// ============================================================================
|
|
858
|
+
// Dict accessor helpers
|
|
859
|
+
// ============================================================================
|
|
860
|
+
function emitDictAccessor(field, lines) {
|
|
861
|
+
const snake = rustFieldName(field.name);
|
|
862
|
+
lines.push(` /// Returns typed reference to the map if the field is an object.`);
|
|
863
|
+
lines.push(` /// Returns \`None\` if the field is null or not an object.`);
|
|
864
|
+
lines.push(` pub fn as_${snake}_dict(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {`);
|
|
865
|
+
lines.push(` self.${snake}.as_object()`);
|
|
866
|
+
lines.push(" }");
|
|
867
|
+
lines.push("");
|
|
868
|
+
}
|
|
869
|
+
// ============================================================================
|
|
870
|
+
// Collection helpers
|
|
871
|
+
// ============================================================================
|
|
872
|
+
function emitCollectionLoadHelper(helper, polymorphicTypeNames, lines) {
|
|
873
|
+
const fnName = `load_${toSnakeCase(helper.propertyName)}`;
|
|
874
|
+
const elemType = helper.elementTypeName.name;
|
|
875
|
+
lines.push("");
|
|
876
|
+
lines.push(` /// Load a collection of ${elemType} from a JSON value.`);
|
|
877
|
+
if (helper.hasNameProperty) {
|
|
878
|
+
lines.push(` /// Handles both array format \`[{...}]\` and dict format \`{"name": {...}}\`.`);
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
lines.push(` /// Handles both array format \`[{...}]\`.`);
|
|
882
|
+
}
|
|
883
|
+
lines.push(` fn ${fnName}(data: &serde_json::Value, ctx: &LoadContext) -> Vec<${elemType}> {`);
|
|
884
|
+
lines.push(" match data {");
|
|
885
|
+
lines.push(" serde_json::Value::Array(arr) => {");
|
|
886
|
+
lines.push(` arr.iter().map(|v| ${elemType}::load_from_value(v, ctx)).collect()`);
|
|
887
|
+
lines.push(" }");
|
|
888
|
+
lines.push("");
|
|
889
|
+
if (helper.hasNameProperty) {
|
|
890
|
+
// Dict format with name keys
|
|
891
|
+
const shorthandField = helper.innerFields.length > 0 ? helper.innerFields[0] : "value";
|
|
892
|
+
lines.push(" serde_json::Value::Object(obj) => {");
|
|
893
|
+
lines.push(" obj.iter()");
|
|
894
|
+
lines.push(" .filter_map(|(name, value)| {");
|
|
895
|
+
lines.push(" if value.is_array() {");
|
|
896
|
+
lines.push(" return None;");
|
|
897
|
+
lines.push(" }");
|
|
898
|
+
lines.push(" let mut v = if value.is_object() {");
|
|
899
|
+
lines.push(" value.clone()");
|
|
900
|
+
lines.push(" } else {");
|
|
901
|
+
lines.push(` serde_json::json!({ "${shorthandField}": value })`);
|
|
902
|
+
lines.push(" };");
|
|
903
|
+
lines.push(' if let serde_json::Value::Object(ref mut m) = v {');
|
|
904
|
+
lines.push(' m.entry("name".to_string()).or_insert_with(|| serde_json::Value::String(name.clone()));');
|
|
905
|
+
lines.push(" }");
|
|
906
|
+
lines.push(` Some(${elemType}::load_from_value(&v, ctx))`);
|
|
907
|
+
lines.push(" })");
|
|
908
|
+
lines.push(" .collect()");
|
|
909
|
+
lines.push(" }");
|
|
910
|
+
}
|
|
911
|
+
lines.push(" _ => Vec::new(),");
|
|
912
|
+
lines.push("");
|
|
913
|
+
lines.push(" }");
|
|
914
|
+
lines.push(" }");
|
|
915
|
+
lines.push("");
|
|
916
|
+
// Save helper
|
|
917
|
+
}
|
|
918
|
+
function emitCollectionSaveHelper(helper, lines) {
|
|
919
|
+
const fnName = `save_${toSnakeCase(helper.propertyName)}`;
|
|
920
|
+
const elemType = helper.elementTypeName.name;
|
|
921
|
+
lines.push(` /// Save a collection of ${elemType} to a JSON value.`);
|
|
922
|
+
lines.push(` fn ${fnName}(items: &[${elemType}], ctx: &SaveContext) -> serde_json::Value {`);
|
|
923
|
+
if (helper.hasNameProperty) {
|
|
924
|
+
lines.push("");
|
|
925
|
+
lines.push(' if ctx.collection_format == "array" {');
|
|
926
|
+
lines.push(" return serde_json::Value::Array(items.iter().map(|item| item.to_value(ctx)).collect::<Vec<_>>());");
|
|
927
|
+
lines.push(" }");
|
|
928
|
+
lines.push(" // Object format: use name as key");
|
|
929
|
+
lines.push(" let mut result = serde_json::Map::new();");
|
|
930
|
+
lines.push(" for item in items {");
|
|
931
|
+
lines.push(" let mut item_data = match item.to_value(ctx) {");
|
|
932
|
+
lines.push(' serde_json::Value::Object(m) => m,');
|
|
933
|
+
lines.push(' other => { let mut m = serde_json::Map::new(); m.insert("value".to_string(), other); m },');
|
|
934
|
+
lines.push(" };");
|
|
935
|
+
lines.push(' if let Some(serde_json::Value::String(name)) = item_data.remove("name") {');
|
|
936
|
+
lines.push(" result.insert(name, serde_json::Value::Object(item_data));");
|
|
937
|
+
lines.push(" }");
|
|
938
|
+
lines.push(" }");
|
|
939
|
+
lines.push(" serde_json::Value::Object(result)");
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
lines.push("");
|
|
943
|
+
lines.push(" serde_json::Value::Array(items.iter().map(|item| item.to_value(ctx)).collect::<Vec<_>>())");
|
|
944
|
+
}
|
|
945
|
+
lines.push("");
|
|
946
|
+
lines.push(" }");
|
|
947
|
+
}
|
|
948
|
+
// ============================================================================
|
|
949
|
+
// Factory methods
|
|
950
|
+
// ============================================================================
|
|
951
|
+
function emitFactory(parentName, factory, visitor, lines) {
|
|
952
|
+
const params = Object.entries(factory.params)
|
|
953
|
+
.map(([pName, pType]) => `${toSnakeCase(pName)}: ${factoryParamType(pType)}`)
|
|
954
|
+
.join(", ");
|
|
955
|
+
lines.push(` /// Create a ${parentName} with preset field values.`);
|
|
956
|
+
lines.push(` pub fn ${factory.name}(${params}) -> Self {`);
|
|
957
|
+
lines.push(` ${visitor.visitExpr(factory.body)}`);
|
|
958
|
+
lines.push(" }");
|
|
959
|
+
}
|
|
960
|
+
function factoryParamType(typeStr) {
|
|
961
|
+
switch (typeStr) {
|
|
962
|
+
case "string":
|
|
963
|
+
return "impl Into<String>";
|
|
964
|
+
case "unknown":
|
|
965
|
+
case "any":
|
|
966
|
+
return "impl Into<serde_json::Value>";
|
|
967
|
+
case "boolean":
|
|
968
|
+
return "bool";
|
|
969
|
+
case "int32":
|
|
970
|
+
return "i32";
|
|
971
|
+
case "int64":
|
|
972
|
+
case "integer":
|
|
973
|
+
return "i64";
|
|
974
|
+
case "float32":
|
|
975
|
+
return "f32";
|
|
976
|
+
case "float64":
|
|
977
|
+
case "float":
|
|
978
|
+
return "f64";
|
|
979
|
+
default:
|
|
980
|
+
return `impl Into<${typeStr}>`;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
// ============================================================================
|
|
984
|
+
// Method stub trait
|
|
985
|
+
// ============================================================================
|
|
986
|
+
function emitMethodTrait(type, lines) {
|
|
987
|
+
lines.push(`/// Helpers for [\`${type.typeName.name}\`]. Implement in a separate file.`);
|
|
988
|
+
lines.push(`pub trait ${type.typeName.name}Helpers {`);
|
|
989
|
+
for (const method of type.methods) {
|
|
990
|
+
if (method.description) {
|
|
991
|
+
emitDocComment(method.description, " ", lines);
|
|
992
|
+
}
|
|
993
|
+
lines.push(` fn ${toSnakeCase(method.name)}(&self) -> ${methodReturnType(method)};`);
|
|
994
|
+
}
|
|
995
|
+
lines.push("}");
|
|
996
|
+
}
|
|
997
|
+
function methodReturnType(method) {
|
|
998
|
+
if (method.returns === "string")
|
|
999
|
+
return "String";
|
|
1000
|
+
return RUST_TYPE_MAP[method.returns] || method.returns;
|
|
1001
|
+
}
|
|
1002
|
+
// ============================================================================
|
|
1003
|
+
// Protocol trait emission
|
|
1004
|
+
// ============================================================================
|
|
1005
|
+
/** Map a protocol type string to a Rust type. */
|
|
1006
|
+
function protocolRustType(typeStr) {
|
|
1007
|
+
// Handle nullable types
|
|
1008
|
+
if (typeStr.endsWith("?")) {
|
|
1009
|
+
const inner = typeStr.slice(0, -1);
|
|
1010
|
+
return `Option<${protocolRustType(inner)}>`;
|
|
1011
|
+
}
|
|
1012
|
+
// Handle array types
|
|
1013
|
+
if (typeStr.endsWith("[]")) {
|
|
1014
|
+
const inner = typeStr.slice(0, -2);
|
|
1015
|
+
return `Vec<${protocolRustType(inner)}>`;
|
|
1016
|
+
}
|
|
1017
|
+
if (typeStr === "Record<unknown>" || typeStr === "dictionary")
|
|
1018
|
+
return "serde_json::Value";
|
|
1019
|
+
if (typeStr === "unknown" || typeStr === "any")
|
|
1020
|
+
return "serde_json::Value";
|
|
1021
|
+
if (typeStr === "string")
|
|
1022
|
+
return "String";
|
|
1023
|
+
return RUST_TYPE_MAP[typeStr] || typeStr;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Emit a Rust trait for a protocol type.
|
|
1027
|
+
* Uses #[async_trait] for async method support.
|
|
1028
|
+
*/
|
|
1029
|
+
function emitProtocolTrait(type, lines) {
|
|
1030
|
+
const name = type.typeName.name;
|
|
1031
|
+
if (type.description) {
|
|
1032
|
+
emitDocComment(type.description, "", lines);
|
|
1033
|
+
}
|
|
1034
|
+
lines.push("#[async_trait::async_trait]");
|
|
1035
|
+
lines.push(`pub trait ${name}: Send + Sync {`);
|
|
1036
|
+
for (const method of type.methods) {
|
|
1037
|
+
if (method.description) {
|
|
1038
|
+
emitDocComment(method.description, " ", lines);
|
|
1039
|
+
}
|
|
1040
|
+
const params = Object.entries(method.params)
|
|
1041
|
+
.map(([pName, pType]) => `${toSnakeCase(pName)}: &${protocolRustType(pType)}`)
|
|
1042
|
+
.join(", ");
|
|
1043
|
+
const ret = protocolRustType(method.returns);
|
|
1044
|
+
if (method.sync) {
|
|
1045
|
+
// Synchronous method
|
|
1046
|
+
if (method.optional) {
|
|
1047
|
+
// Return type already includes nullability from ? suffix — don't double-wrap
|
|
1048
|
+
lines.push(` fn ${toSnakeCase(method.name)}(&self, ${params}) -> ${ret} {`);
|
|
1049
|
+
lines.push(" None");
|
|
1050
|
+
lines.push(" }");
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
lines.push(` fn ${toSnakeCase(method.name)}(&self, ${params}) -> ${ret};`);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
else {
|
|
1057
|
+
// Async method
|
|
1058
|
+
if (method.optional) {
|
|
1059
|
+
// Default implementation returns an error — providers override with real streaming
|
|
1060
|
+
lines.push(` async fn ${toSnakeCase(method.name)}(&self, ${params}) -> Result<${ret}, Box<dyn std::error::Error + Send + Sync>> {`);
|
|
1061
|
+
lines.push(` Err("not supported".into())`);
|
|
1062
|
+
lines.push(" }");
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
lines.push(` async fn ${toSnakeCase(method.name)}(&self, ${params}) -> Result<${ret}, Box<dyn std::error::Error + Send + Sync>>;`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
lines.push("}");
|
|
1070
|
+
}
|
|
1071
|
+
// ============================================================================
|
|
1072
|
+
// Field type rendering
|
|
1073
|
+
// ============================================================================
|
|
1074
|
+
function fieldType(field, polymorphicTypeNames) {
|
|
1075
|
+
// Named enum field — use the enum type
|
|
1076
|
+
if (field.enumName && field.allowedValues.length > 0) {
|
|
1077
|
+
return field.isOptional ? `Option<${field.enumName}>` : field.enumName;
|
|
1078
|
+
}
|
|
1079
|
+
const cat = field.category;
|
|
1080
|
+
switch (cat.kind) {
|
|
1081
|
+
case "scalar": {
|
|
1082
|
+
const rustType = RUST_TYPE_MAP[cat.scalarType] || cat.scalarType;
|
|
1083
|
+
// dict category handles always-Value separately; for scalar value types
|
|
1084
|
+
// (any, object, unknown), optional fields are Option<Value>, required are Value.
|
|
1085
|
+
// dictionary scalar is always Value (never Option) since it's a map type.
|
|
1086
|
+
if (cat.scalarType === "dictionary") {
|
|
1087
|
+
return "serde_json::Value";
|
|
1088
|
+
}
|
|
1089
|
+
return field.isOptional ? `Option<${rustType}>` : rustType;
|
|
1090
|
+
}
|
|
1091
|
+
case "complex": {
|
|
1092
|
+
// Polymorphic references and "unknown" become serde_json::Value
|
|
1093
|
+
// Always non-optional — Value::Null serves as the "absent" sentinel
|
|
1094
|
+
if (polymorphicTypeNames.has(cat.typeName) || cat.typeName === "unknown") {
|
|
1095
|
+
return "serde_json::Value";
|
|
1096
|
+
}
|
|
1097
|
+
return field.isOptional ? `Option<${cat.typeName}>` : cat.typeName;
|
|
1098
|
+
}
|
|
1099
|
+
case "collection_scalar": {
|
|
1100
|
+
const elemType = RUST_TYPE_MAP[cat.scalarType] || cat.scalarType;
|
|
1101
|
+
if (isValueType(cat.scalarType)) {
|
|
1102
|
+
return field.isOptional ? "Option<Vec<serde_json::Value>>" : "Vec<serde_json::Value>";
|
|
1103
|
+
}
|
|
1104
|
+
return field.isOptional ? `Option<Vec<${elemType}>>` : `Vec<${elemType}>`;
|
|
1105
|
+
}
|
|
1106
|
+
case "collection_complex": {
|
|
1107
|
+
if (cat.typeName === "unknown") {
|
|
1108
|
+
return "Vec<serde_json::Value>";
|
|
1109
|
+
}
|
|
1110
|
+
return `Vec<${cat.typeName}>`;
|
|
1111
|
+
}
|
|
1112
|
+
case "dict": {
|
|
1113
|
+
return "serde_json::Value";
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function isValueType(scalarType) {
|
|
1118
|
+
return scalarType === "any" || scalarType === "object" || scalarType === "dictionary" || scalarType === "unknown";
|
|
1119
|
+
}
|
|
1120
|
+
function fieldDefault(field, polymorphicTypeNames) {
|
|
1121
|
+
// Named enum — default to field default value or first variant
|
|
1122
|
+
if (field.enumName && field.allowedValues.length > 0) {
|
|
1123
|
+
if (field.isOptional)
|
|
1124
|
+
return "None";
|
|
1125
|
+
const dv = typeof field.defaultValue === "string" ? field.defaultValue : null;
|
|
1126
|
+
const defaultVal = dv && field.allowedValues.includes(dv) ? dv : field.allowedValues[0];
|
|
1127
|
+
return `${field.enumName}::${toPascalCase(defaultVal)}`;
|
|
1128
|
+
}
|
|
1129
|
+
const cat = field.category;
|
|
1130
|
+
switch (cat.kind) {
|
|
1131
|
+
case "scalar": {
|
|
1132
|
+
if (isValueType(cat.scalarType)) {
|
|
1133
|
+
// dictionary is always Value (never Option); other value types may be optional
|
|
1134
|
+
if (cat.scalarType !== "dictionary" && field.isOptional)
|
|
1135
|
+
return "None";
|
|
1136
|
+
return "serde_json::Value::Null";
|
|
1137
|
+
}
|
|
1138
|
+
if (field.isOptional)
|
|
1139
|
+
return "None";
|
|
1140
|
+
const rustType = RUST_TYPE_MAP[cat.scalarType] || cat.scalarType;
|
|
1141
|
+
if (rustType === "String")
|
|
1142
|
+
return 'String::from("")';
|
|
1143
|
+
if (rustType === "bool")
|
|
1144
|
+
return "false";
|
|
1145
|
+
if (rustType === "i32" || rustType === "i64")
|
|
1146
|
+
return "0";
|
|
1147
|
+
if (rustType === "f32" || rustType === "f64")
|
|
1148
|
+
return "0.0";
|
|
1149
|
+
return "Default::default()";
|
|
1150
|
+
}
|
|
1151
|
+
case "complex": {
|
|
1152
|
+
// Polymorphic refs are always serde_json::Value, Null is the "absent" sentinel
|
|
1153
|
+
if (polymorphicTypeNames.has(cat.typeName))
|
|
1154
|
+
return "serde_json::Value::Null";
|
|
1155
|
+
return field.isOptional ? "None" : "Default::default()";
|
|
1156
|
+
}
|
|
1157
|
+
case "collection_scalar":
|
|
1158
|
+
case "collection_complex":
|
|
1159
|
+
return field.isOptional ? "None" : "Vec::new()";
|
|
1160
|
+
case "dict":
|
|
1161
|
+
return "serde_json::Value::Null";
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
// ============================================================================
|
|
1165
|
+
// Load expression rendering (per-field)
|
|
1166
|
+
// ============================================================================
|
|
1167
|
+
function loadExpr(a, polymorphicTypeNames) {
|
|
1168
|
+
const key = a.sourceName;
|
|
1169
|
+
const cat = a.category;
|
|
1170
|
+
// Named enum — parse from string via from_str_opt
|
|
1171
|
+
if (a.enumName && a.allowedValues.length > 0) {
|
|
1172
|
+
const dv = typeof a.defaultValue === "string" ? a.defaultValue : null;
|
|
1173
|
+
const defaultVal = dv && a.allowedValues.includes(dv) ? dv : a.allowedValues[0];
|
|
1174
|
+
if (a.isOptional) {
|
|
1175
|
+
return `value.get("${key}").and_then(|v| v.as_str()).and_then(|s| ${a.enumName}::from_str_opt(s))`;
|
|
1176
|
+
}
|
|
1177
|
+
return `value.get("${key}").and_then(|v| v.as_str()).and_then(|s| ${a.enumName}::from_str_opt(s)).unwrap_or(${a.enumName}::${toPascalCase(defaultVal)})`;
|
|
1178
|
+
}
|
|
1179
|
+
switch (cat.kind) {
|
|
1180
|
+
case "scalar":
|
|
1181
|
+
return scalarLoadExpr(key, cat.scalarType, a.isOptional);
|
|
1182
|
+
case "complex": {
|
|
1183
|
+
if (polymorphicTypeNames.has(cat.typeName)) {
|
|
1184
|
+
// Polymorphic ref → always serde_json::Value, Null is the "absent" sentinel
|
|
1185
|
+
return `value.get("${key}").cloned().unwrap_or(serde_json::Value::Null)`;
|
|
1186
|
+
}
|
|
1187
|
+
if (a.isOptional) {
|
|
1188
|
+
return `value.get("${key}").filter(|v| v.is_object() || v.is_array() || v.is_string()).map(|v| ${cat.typeName}::load_from_value(v, ctx))`;
|
|
1189
|
+
}
|
|
1190
|
+
return `value.get("${key}").filter(|v| v.is_object() || v.is_array() || v.is_string()).map(|v| ${cat.typeName}::load_from_value(v, ctx)).unwrap_or_default()`;
|
|
1191
|
+
}
|
|
1192
|
+
case "collection_scalar":
|
|
1193
|
+
return collectionScalarLoadExpr(key, cat.scalarType, a.isOptional);
|
|
1194
|
+
case "collection_complex":
|
|
1195
|
+
return `value.get("${key}").map(|v| Self::load_${toSnakeCase(a.fieldName)}(v, ctx)).unwrap_or_default()`;
|
|
1196
|
+
case "dict":
|
|
1197
|
+
return `value.get("${key}").cloned().unwrap_or(serde_json::Value::Null)`;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
function scalarLoadExpr(key, scalarType, isOptional) {
|
|
1201
|
+
if (isValueType(scalarType)) {
|
|
1202
|
+
// dictionary is never optional; any/object/unknown may be optional
|
|
1203
|
+
if (scalarType === "dictionary") {
|
|
1204
|
+
return `value.get("${key}").cloned().unwrap_or(serde_json::Value::Null)`;
|
|
1205
|
+
}
|
|
1206
|
+
return isOptional
|
|
1207
|
+
? `value.get("${key}").cloned()`
|
|
1208
|
+
: `value.get("${key}").cloned().unwrap_or(serde_json::Value::Null)`;
|
|
1209
|
+
}
|
|
1210
|
+
switch (scalarType) {
|
|
1211
|
+
case "string":
|
|
1212
|
+
return isOptional
|
|
1213
|
+
? `value.get("${key}").and_then(|v| v.as_str()).map(|s| s.to_string())`
|
|
1214
|
+
: `value.get("${key}").and_then(|v| v.as_str()).unwrap_or_default().to_string()`;
|
|
1215
|
+
case "boolean":
|
|
1216
|
+
return isOptional
|
|
1217
|
+
? `value.get("${key}").and_then(|v| v.as_bool())`
|
|
1218
|
+
: `value.get("${key}").and_then(|v| v.as_bool()).unwrap_or(false)`;
|
|
1219
|
+
case "int32":
|
|
1220
|
+
return isOptional
|
|
1221
|
+
? `value.get("${key}").and_then(|v| v.as_i64()).map(|v| v as i32)`
|
|
1222
|
+
: `value.get("${key}").and_then(|v| v.as_i64()).unwrap_or(0) as i32`;
|
|
1223
|
+
case "int64":
|
|
1224
|
+
case "integer":
|
|
1225
|
+
return isOptional
|
|
1226
|
+
? `value.get("${key}").and_then(|v| v.as_i64())`
|
|
1227
|
+
: `value.get("${key}").and_then(|v| v.as_i64()).unwrap_or(0)`;
|
|
1228
|
+
case "float64":
|
|
1229
|
+
case "float":
|
|
1230
|
+
case "number":
|
|
1231
|
+
case "numeric":
|
|
1232
|
+
return isOptional
|
|
1233
|
+
? `value.get("${key}").and_then(|v| v.as_f64())`
|
|
1234
|
+
: `value.get("${key}").and_then(|v| v.as_f64()).unwrap_or(0.0)`;
|
|
1235
|
+
case "float32":
|
|
1236
|
+
return isOptional
|
|
1237
|
+
? `value.get("${key}").and_then(|v| v.as_f64()).map(|v| v as f32)`
|
|
1238
|
+
: `value.get("${key}").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32`;
|
|
1239
|
+
default:
|
|
1240
|
+
return `value.get("${key}").cloned().unwrap_or(serde_json::Value::Null)`;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
function collectionScalarLoadExpr(key, scalarType, isOptional) {
|
|
1244
|
+
if (isValueType(scalarType)) {
|
|
1245
|
+
return isOptional
|
|
1246
|
+
? `value.get("${key}").and_then(|v| v.as_array()).map(|arr| arr.to_vec())`
|
|
1247
|
+
: `value.get("${key}").and_then(|v| v.as_array()).map(|arr| arr.to_vec()).unwrap_or_default()`;
|
|
1248
|
+
}
|
|
1249
|
+
switch (scalarType) {
|
|
1250
|
+
case "string":
|
|
1251
|
+
return isOptional
|
|
1252
|
+
? `value.get("${key}").and_then(|v| v.as_array()).map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())`
|
|
1253
|
+
: `value.get("${key}").and_then(|v| v.as_array()).map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()).unwrap_or_default()`;
|
|
1254
|
+
default:
|
|
1255
|
+
return isOptional
|
|
1256
|
+
? `value.get("${key}").and_then(|v| v.as_array()).map(|arr| arr.to_vec())`
|
|
1257
|
+
: `value.get("${key}").and_then(|v| v.as_array()).map(|arr| arr.to_vec()).unwrap_or_default()`;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Load expression for a variant field (used inside match arms).
|
|
1262
|
+
*/
|
|
1263
|
+
function variantLoadExpr(field, parentTypeName, polymorphicTypeNames) {
|
|
1264
|
+
// Delegate to the same patterns as base field loading, but with "value" as the source
|
|
1265
|
+
const key = field.name;
|
|
1266
|
+
const cat = field.category;
|
|
1267
|
+
// Named enum — parse from string via from_str_opt
|
|
1268
|
+
if (field.enumName && field.allowedValues.length > 0) {
|
|
1269
|
+
const defaultVal = String(field.defaultValue || field.allowedValues[0]);
|
|
1270
|
+
if (field.isOptional) {
|
|
1271
|
+
return `value.get("${key}").and_then(|v| v.as_str()).and_then(|s| ${field.enumName}::from_str_opt(s))`;
|
|
1272
|
+
}
|
|
1273
|
+
return `value.get("${key}").and_then(|v| v.as_str()).and_then(|s| ${field.enumName}::from_str_opt(s)).unwrap_or(${field.enumName}::${toPascalCase(defaultVal)})`;
|
|
1274
|
+
}
|
|
1275
|
+
switch (cat.kind) {
|
|
1276
|
+
case "scalar":
|
|
1277
|
+
return scalarLoadExpr(key, cat.scalarType, field.isOptional);
|
|
1278
|
+
case "complex": {
|
|
1279
|
+
if (polymorphicTypeNames.has(cat.typeName)) {
|
|
1280
|
+
return field.isOptional
|
|
1281
|
+
? `value.get("${key}").cloned()`
|
|
1282
|
+
: `value.get("${key}").cloned().unwrap_or(serde_json::Value::Null)`;
|
|
1283
|
+
}
|
|
1284
|
+
if (field.isOptional) {
|
|
1285
|
+
return `value.get("${key}").filter(|v| v.is_object() || v.is_array() || v.is_string()).map(|v| ${cat.typeName}::load_from_value(v, ctx))`;
|
|
1286
|
+
}
|
|
1287
|
+
return `value.get("${key}").filter(|v| v.is_object() || v.is_array() || v.is_string()).map(|v| ${cat.typeName}::load_from_value(v, ctx)).unwrap_or_default()`;
|
|
1288
|
+
}
|
|
1289
|
+
case "collection_scalar":
|
|
1290
|
+
return collectionScalarLoadExpr(key, cat.scalarType, field.isOptional);
|
|
1291
|
+
case "collection_complex":
|
|
1292
|
+
// Collection in a variant — use the parent type's helper
|
|
1293
|
+
return `value.get("${key}").map(|v| Self::load_${toSnakeCase(field.name)}(v, ctx)).unwrap_or_default()`;
|
|
1294
|
+
case "dict":
|
|
1295
|
+
return `value.get("${key}").cloned().unwrap_or(serde_json::Value::Null)`;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
// ============================================================================
|
|
1299
|
+
// Save expression rendering (per-field)
|
|
1300
|
+
// ============================================================================
|
|
1301
|
+
function emitSaveField(a, prefix, polymorphicTypeNames, lines, indent) {
|
|
1302
|
+
const key = a.targetName;
|
|
1303
|
+
const fieldRef = `${prefix}${rustFieldName(a.fieldName)}`;
|
|
1304
|
+
const cat = a.category;
|
|
1305
|
+
// Named enum — serialize via .to_string()
|
|
1306
|
+
if (a.enumName) {
|
|
1307
|
+
if (a.isOptional) {
|
|
1308
|
+
lines.push(`${indent}if let Some(ref val) = ${fieldRef} {`);
|
|
1309
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Value::String(val.to_string()));`);
|
|
1310
|
+
lines.push(`${indent}}`);
|
|
1311
|
+
}
|
|
1312
|
+
else {
|
|
1313
|
+
lines.push(`${indent}result.insert("${key}".to_string(), serde_json::Value::String(${fieldRef}.to_string()));`);
|
|
1314
|
+
}
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
switch (cat.kind) {
|
|
1318
|
+
case "scalar":
|
|
1319
|
+
emitScalarSave(key, fieldRef, cat.scalarType, a.isOptional, lines, indent);
|
|
1320
|
+
return;
|
|
1321
|
+
case "complex": {
|
|
1322
|
+
if (polymorphicTypeNames.has(cat.typeName)) {
|
|
1323
|
+
// Polymorphic ref → always serde_json::Value, check .is_null()
|
|
1324
|
+
lines.push(`${indent}if !${fieldRef}.is_null() {`);
|
|
1325
|
+
lines.push(`${indent} result.insert("${key}".to_string(), ${fieldRef}.clone());`);
|
|
1326
|
+
lines.push(`${indent}}`);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
if (a.isOptional) {
|
|
1330
|
+
lines.push(`${indent}if let Some(ref val) = ${fieldRef} {`);
|
|
1331
|
+
lines.push(`${indent} let nested = val.to_value(ctx);`);
|
|
1332
|
+
lines.push(`${indent} if !nested.is_null() {`);
|
|
1333
|
+
lines.push(`${indent} result.insert("${key}".to_string(), nested);`);
|
|
1334
|
+
lines.push(`${indent} }`);
|
|
1335
|
+
lines.push(`${indent}}`);
|
|
1336
|
+
}
|
|
1337
|
+
else {
|
|
1338
|
+
lines.push(`${indent}{`);
|
|
1339
|
+
lines.push(`${indent} let nested = ${fieldRef}.to_value(ctx);`);
|
|
1340
|
+
lines.push(`${indent} if !nested.is_null() {`);
|
|
1341
|
+
lines.push(`${indent} result.insert("${key}".to_string(), nested);`);
|
|
1342
|
+
lines.push(`${indent} }`);
|
|
1343
|
+
lines.push(`${indent}}`);
|
|
1344
|
+
}
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
case "collection_scalar": {
|
|
1348
|
+
if (a.isOptional) {
|
|
1349
|
+
lines.push(`${indent}if let Some(ref items) = ${fieldRef} {`);
|
|
1350
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::to_value(items).unwrap_or(serde_json::Value::Null));`);
|
|
1351
|
+
lines.push(`${indent}}`);
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
lines.push(`${indent}if !${fieldRef}.is_empty() {`);
|
|
1355
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::to_value(&${fieldRef}).unwrap_or(serde_json::Value::Null));`);
|
|
1356
|
+
lines.push(`${indent}}`);
|
|
1357
|
+
}
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
case "collection_complex": {
|
|
1361
|
+
lines.push(`${indent}if !${fieldRef}.is_empty() {`);
|
|
1362
|
+
lines.push(`${indent} result.insert("${key}".to_string(), Self::save_${toSnakeCase(a.fieldName)}(&${fieldRef}, ctx));`);
|
|
1363
|
+
lines.push(`${indent}}`);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
case "dict": {
|
|
1367
|
+
lines.push(`${indent}if !${fieldRef}.is_null() {`);
|
|
1368
|
+
lines.push(`${indent} result.insert("${key}".to_string(), ${fieldRef}.clone());`);
|
|
1369
|
+
lines.push(`${indent}}`);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
function emitScalarSave(key, fieldRef, scalarType, isOptional, lines, indent) {
|
|
1375
|
+
if (isValueType(scalarType)) {
|
|
1376
|
+
if (scalarType !== "dictionary" && isOptional) {
|
|
1377
|
+
// Optional value types (any, object, unknown) are Option<Value>
|
|
1378
|
+
lines.push(`${indent}if let Some(ref val) = ${fieldRef} {`);
|
|
1379
|
+
lines.push(`${indent} result.insert("${key}".to_string(), val.clone());`);
|
|
1380
|
+
lines.push(`${indent}}`);
|
|
1381
|
+
}
|
|
1382
|
+
else {
|
|
1383
|
+
// Required value types or dictionary (always Value, never Option)
|
|
1384
|
+
lines.push(`${indent}if !${fieldRef}.is_null() {`);
|
|
1385
|
+
lines.push(`${indent} result.insert("${key}".to_string(), ${fieldRef}.clone());`);
|
|
1386
|
+
lines.push(`${indent}}`);
|
|
1387
|
+
}
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
switch (scalarType) {
|
|
1391
|
+
case "string":
|
|
1392
|
+
if (isOptional) {
|
|
1393
|
+
lines.push(`${indent}if let Some(ref val) = ${fieldRef} {`);
|
|
1394
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Value::String(val.clone()));`);
|
|
1395
|
+
lines.push(`${indent}}`);
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
lines.push(`${indent}if !${fieldRef}.is_empty() {`);
|
|
1399
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Value::String(${fieldRef}.clone()));`);
|
|
1400
|
+
lines.push(`${indent}}`);
|
|
1401
|
+
}
|
|
1402
|
+
return;
|
|
1403
|
+
case "boolean":
|
|
1404
|
+
if (isOptional) {
|
|
1405
|
+
lines.push(`${indent}if let Some(val) = ${fieldRef} {`);
|
|
1406
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Value::Bool(val));`);
|
|
1407
|
+
lines.push(`${indent}}`);
|
|
1408
|
+
}
|
|
1409
|
+
else {
|
|
1410
|
+
// Always write booleans
|
|
1411
|
+
lines.push(`${indent}result.insert("${key}".to_string(), serde_json::Value::Bool(${fieldRef}));`);
|
|
1412
|
+
}
|
|
1413
|
+
return;
|
|
1414
|
+
case "int32":
|
|
1415
|
+
case "int64":
|
|
1416
|
+
case "integer":
|
|
1417
|
+
if (isOptional) {
|
|
1418
|
+
lines.push(`${indent}if let Some(val) = ${fieldRef} {`);
|
|
1419
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Value::Number(serde_json::Number::from(val)));`);
|
|
1420
|
+
lines.push(`${indent}}`);
|
|
1421
|
+
}
|
|
1422
|
+
else {
|
|
1423
|
+
lines.push(`${indent}if ${fieldRef} != 0 {`);
|
|
1424
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Value::Number(serde_json::Number::from(${fieldRef})));`);
|
|
1425
|
+
lines.push(`${indent}}`);
|
|
1426
|
+
}
|
|
1427
|
+
return;
|
|
1428
|
+
case "float32":
|
|
1429
|
+
case "float64":
|
|
1430
|
+
case "float":
|
|
1431
|
+
case "number":
|
|
1432
|
+
case "numeric":
|
|
1433
|
+
if (isOptional) {
|
|
1434
|
+
lines.push(`${indent}if let Some(val) = ${fieldRef} {`);
|
|
1435
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Number::from_f64(val as f64).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null));`);
|
|
1436
|
+
lines.push(`${indent}}`);
|
|
1437
|
+
}
|
|
1438
|
+
else {
|
|
1439
|
+
lines.push(`${indent}if ${fieldRef} != 0.0 {`);
|
|
1440
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Number::from_f64(${fieldRef} as f64).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null));`);
|
|
1441
|
+
lines.push(`${indent}}`);
|
|
1442
|
+
}
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Emit a save expression for a variant field (destructured in a match arm).
|
|
1448
|
+
*/
|
|
1449
|
+
function emitVariantSaveField(field, polymorphicTypeNames, parentTypeName, lines) {
|
|
1450
|
+
const key = field.name;
|
|
1451
|
+
const fieldRef = rustFieldName(field.name);
|
|
1452
|
+
const cat = field.category;
|
|
1453
|
+
const indent = " ";
|
|
1454
|
+
// Named enum — serialize via .to_string()
|
|
1455
|
+
if (field.enumName && field.allowedValues.length > 0) {
|
|
1456
|
+
if (field.isOptional) {
|
|
1457
|
+
lines.push(`${indent}if let Some(ref val) = ${fieldRef} {`);
|
|
1458
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Value::String(val.to_string()));`);
|
|
1459
|
+
lines.push(`${indent}}`);
|
|
1460
|
+
}
|
|
1461
|
+
else {
|
|
1462
|
+
lines.push(`${indent}result.insert("${key}".to_string(), serde_json::Value::String(${fieldRef}.to_string()));`);
|
|
1463
|
+
}
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
switch (cat.kind) {
|
|
1467
|
+
case "scalar":
|
|
1468
|
+
emitVariantScalarSave(key, fieldRef, cat.scalarType, field.isOptional, lines, indent);
|
|
1469
|
+
return;
|
|
1470
|
+
case "complex": {
|
|
1471
|
+
if (polymorphicTypeNames.has(cat.typeName)) {
|
|
1472
|
+
if (field.isOptional) {
|
|
1473
|
+
lines.push(`${indent}if let Some(val) = ${fieldRef} {`);
|
|
1474
|
+
lines.push(`${indent} result.insert("${key}".to_string(), val.clone());`);
|
|
1475
|
+
lines.push(`${indent}}`);
|
|
1476
|
+
}
|
|
1477
|
+
else {
|
|
1478
|
+
lines.push(`${indent}if !${fieldRef}.is_null() {`);
|
|
1479
|
+
lines.push(`${indent} result.insert("${key}".to_string(), ${fieldRef}.clone());`);
|
|
1480
|
+
lines.push(`${indent}}`);
|
|
1481
|
+
}
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
if (field.isOptional) {
|
|
1485
|
+
lines.push(`${indent}if let Some(val) = ${fieldRef} {`);
|
|
1486
|
+
lines.push(`${indent} result.insert("${key}".to_string(), val.to_value(ctx));`);
|
|
1487
|
+
lines.push(`${indent}}`);
|
|
1488
|
+
}
|
|
1489
|
+
else {
|
|
1490
|
+
lines.push(`${indent}{`);
|
|
1491
|
+
lines.push(`${indent} let nested = ${fieldRef}.to_value(ctx);`);
|
|
1492
|
+
lines.push(`${indent} if !nested.is_null() {`);
|
|
1493
|
+
lines.push(`${indent} result.insert("${key}".to_string(), nested);`);
|
|
1494
|
+
lines.push(`${indent} }`);
|
|
1495
|
+
lines.push(`${indent}}`);
|
|
1496
|
+
}
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
case "collection_scalar": {
|
|
1500
|
+
if (field.isOptional) {
|
|
1501
|
+
lines.push(`${indent}if let Some(items) = ${fieldRef} {`);
|
|
1502
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::to_value(items).unwrap_or(serde_json::Value::Null));`);
|
|
1503
|
+
lines.push(`${indent}}`);
|
|
1504
|
+
}
|
|
1505
|
+
else {
|
|
1506
|
+
lines.push(`${indent}if !${fieldRef}.is_empty() {`);
|
|
1507
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::to_value(${fieldRef}).unwrap_or(serde_json::Value::Null));`);
|
|
1508
|
+
lines.push(`${indent}}`);
|
|
1509
|
+
}
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
case "collection_complex": {
|
|
1513
|
+
lines.push(`${indent}if !${fieldRef}.is_empty() {`);
|
|
1514
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Value::Array(${fieldRef}.iter().map(|item| item.to_value(ctx)).collect()));`);
|
|
1515
|
+
lines.push(`${indent}}`);
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
case "dict": {
|
|
1519
|
+
lines.push(`${indent}if !${fieldRef}.is_null() {`);
|
|
1520
|
+
lines.push(`${indent} result.insert("${key}".to_string(), ${fieldRef}.clone());`);
|
|
1521
|
+
lines.push(`${indent}}`);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function emitVariantScalarSave(key, fieldRef, scalarType, isOptional, lines, indent) {
|
|
1527
|
+
if (isValueType(scalarType)) {
|
|
1528
|
+
if (scalarType !== "dictionary" && isOptional) {
|
|
1529
|
+
// Optional value types — variant fields are references, use direct pattern
|
|
1530
|
+
lines.push(`${indent}if let Some(val) = ${fieldRef} {`);
|
|
1531
|
+
lines.push(`${indent} result.insert("${key}".to_string(), val.clone());`);
|
|
1532
|
+
lines.push(`${indent}}`);
|
|
1533
|
+
}
|
|
1534
|
+
else {
|
|
1535
|
+
lines.push(`${indent}if !${fieldRef}.is_null() {`);
|
|
1536
|
+
lines.push(`${indent} result.insert("${key}".to_string(), ${fieldRef}.clone());`);
|
|
1537
|
+
lines.push(`${indent}}`);
|
|
1538
|
+
}
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
switch (scalarType) {
|
|
1542
|
+
case "string":
|
|
1543
|
+
if (isOptional) {
|
|
1544
|
+
lines.push(`${indent}if let Some(val) = ${fieldRef} {`);
|
|
1545
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Value::String(val.clone()));`);
|
|
1546
|
+
lines.push(`${indent}}`);
|
|
1547
|
+
}
|
|
1548
|
+
else {
|
|
1549
|
+
lines.push(`${indent}if !${fieldRef}.is_empty() {`);
|
|
1550
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Value::String(${fieldRef}.clone()));`);
|
|
1551
|
+
lines.push(`${indent}}`);
|
|
1552
|
+
}
|
|
1553
|
+
return;
|
|
1554
|
+
case "boolean":
|
|
1555
|
+
if (isOptional) {
|
|
1556
|
+
lines.push(`${indent}if let Some(val) = ${fieldRef} {`);
|
|
1557
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Value::Bool(*val));`);
|
|
1558
|
+
lines.push(`${indent}}`);
|
|
1559
|
+
}
|
|
1560
|
+
else {
|
|
1561
|
+
lines.push(`${indent}result.insert("${key}".to_string(), serde_json::Value::Bool(*${fieldRef}));`);
|
|
1562
|
+
}
|
|
1563
|
+
return;
|
|
1564
|
+
case "int32":
|
|
1565
|
+
case "int64":
|
|
1566
|
+
case "integer":
|
|
1567
|
+
if (isOptional) {
|
|
1568
|
+
lines.push(`${indent}if let Some(val) = ${fieldRef} {`);
|
|
1569
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Value::Number(serde_json::Number::from(*val)));`);
|
|
1570
|
+
lines.push(`${indent}}`);
|
|
1571
|
+
}
|
|
1572
|
+
else {
|
|
1573
|
+
lines.push(`${indent}if *${fieldRef} != 0 {`);
|
|
1574
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Value::Number(serde_json::Number::from(*${fieldRef})));`);
|
|
1575
|
+
lines.push(`${indent}}`);
|
|
1576
|
+
}
|
|
1577
|
+
return;
|
|
1578
|
+
case "float32":
|
|
1579
|
+
case "float64":
|
|
1580
|
+
case "float":
|
|
1581
|
+
case "number":
|
|
1582
|
+
case "numeric":
|
|
1583
|
+
if (isOptional) {
|
|
1584
|
+
lines.push(`${indent}if let Some(val) = ${fieldRef} {`);
|
|
1585
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Number::from_f64(*val as f64).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null));`);
|
|
1586
|
+
lines.push(`${indent}}`);
|
|
1587
|
+
}
|
|
1588
|
+
else {
|
|
1589
|
+
lines.push(`${indent}if *${fieldRef} != 0.0 {`);
|
|
1590
|
+
lines.push(`${indent} result.insert("${key}".to_string(), serde_json::Number::from_f64(*${fieldRef} as f64).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null));`);
|
|
1591
|
+
lines.push(`${indent}}`);
|
|
1592
|
+
}
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
//# sourceMappingURL=emitter.js.map
|