Skip to main content

alef_codegen/conversions/
binding_to_core.rs

1use alef_core::ir::{PrimitiveType, TypeDef, TypeRef};
2use std::fmt::Write;
3
4use super::ConversionConfig;
5use super::helpers::{core_prim_str, core_type_path, needs_i64_cast};
6
7/// Generate `impl From<BindingType> for core::Type` (binding -> core).
8/// Sanitized fields use `Default::default()` (lossy but functional).
9pub fn gen_from_binding_to_core(typ: &TypeDef, core_import: &str) -> String {
10    gen_from_binding_to_core_cfg(typ, core_import, &ConversionConfig::default())
11}
12
13/// Generate `impl From<BindingType> for core::Type` with backend-specific config.
14pub fn gen_from_binding_to_core_cfg(typ: &TypeDef, core_import: &str, config: &ConversionConfig) -> String {
15    let core_path = core_type_path(typ, core_import);
16    let binding_name = format!("{}{}", config.type_name_prefix, typ.name);
17    let mut out = String::with_capacity(256);
18    // When cfg-gated fields exist, ..Default::default() fills them when the feature is enabled.
19    // When disabled, all fields are already specified and the update has no effect — suppress lint.
20    if typ.has_stripped_cfg_fields {
21        writeln!(out, "#[allow(clippy::needless_update)]").ok();
22    }
23    writeln!(out, "impl From<{binding_name}> for {core_path} {{").ok();
24    writeln!(out, "    fn from(val: {binding_name}) -> Self {{").ok();
25    writeln!(out, "        Self {{").ok();
26    let optionalized = config.optionalize_defaults && typ.has_default;
27    for field in &typ.fields {
28        let conversion = if field.sanitized {
29            format!("{}: Default::default()", field.name)
30        } else if optionalized && !field.optional {
31            // Field was wrapped in Option<T> for JS ergonomics but core expects T.
32            // Use unwrap_or_default() for simple types, unwrap_or_default() + into for Named.
33            gen_optionalized_field_to_core(&field.name, &field.ty, config)
34        } else {
35            field_conversion_to_core_cfg(&field.name, &field.ty, field.optional, config)
36        };
37        // Box<T> fields: wrap the converted value in Box::new()
38        let conversion = if field.is_boxed && matches!(&field.ty, TypeRef::Named(_)) {
39            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
40                format!("{}: Box::new({})", field.name, expr)
41            } else {
42                conversion
43            }
44        } else {
45            conversion
46        };
47        writeln!(out, "            {conversion},").ok();
48    }
49    // Use ..Default::default() to fill cfg-gated fields stripped from the IR
50    if typ.has_stripped_cfg_fields {
51        writeln!(out, "            ..Default::default()").ok();
52    }
53    writeln!(out, "        }}").ok();
54    writeln!(out, "    }}").ok();
55    write!(out, "}}").ok();
56    out
57}
58
59/// Generate field conversion for a non-optional field that was optionalized
60/// (wrapped in Option<T>) in the binding struct for JS ergonomics.
61pub(super) fn gen_optionalized_field_to_core(name: &str, ty: &TypeRef, config: &ConversionConfig) -> String {
62    match ty {
63        TypeRef::Json => {
64            format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or_default()")
65        }
66        TypeRef::Named(_) => {
67            // Named type: unwrap Option, convert via .into(), or use Default
68            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
69        }
70        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
71            let core_ty = core_prim_str(p);
72            format!("{name}: val.{name}.map(|v| v as {core_ty}).unwrap_or_default()")
73        }
74        TypeRef::Duration if config.cast_large_ints_to_i64 => {
75            format!("{name}: val.{name}.map(|v| std::time::Duration::from_secs(v as u64)).unwrap_or_default()")
76        }
77        TypeRef::Duration => {
78            format!("{name}: val.{name}.map(std::time::Duration::from_secs).unwrap_or_default()")
79        }
80        TypeRef::Path => {
81            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
82        }
83        // Char: binding uses Option<String>, core uses char
84        TypeRef::Char => {
85            format!("{name}: val.{name}.and_then(|s| s.chars().next()).unwrap_or('*')")
86        }
87        TypeRef::Vec(inner) => match inner.as_ref() {
88            TypeRef::Json => {
89                format!(
90                    "{name}: val.{name}.map(|v| v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()).unwrap_or_default()"
91                )
92            }
93            TypeRef::Named(_) => {
94                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect()).unwrap_or_default()")
95            }
96            TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
97                let core_ty = core_prim_str(p);
98                format!(
99                    "{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect()).unwrap_or_default()"
100                )
101            }
102            _ => format!("{name}: val.{name}.unwrap_or_default()"),
103        },
104        TypeRef::Map(_, _) => {
105            // Collect to handle HashMap↔BTreeMap conversion
106            format!("{name}: val.{name}.unwrap_or_default().into_iter().collect()")
107        }
108        _ => {
109            // Simple types (primitives, String, etc): unwrap_or_default()
110            format!("{name}: val.{name}.unwrap_or_default()")
111        }
112    }
113}
114
115/// Determine the field conversion expression for binding -> core.
116pub fn field_conversion_to_core(name: &str, ty: &TypeRef, optional: bool) -> String {
117    match ty {
118        // Primitives, String, Bytes, Unit -- direct assignment
119        TypeRef::Primitive(_) | TypeRef::String | TypeRef::Bytes | TypeRef::Unit => {
120            format!("{name}: val.{name}")
121        }
122        // Json: binding uses String, core uses serde_json::Value — parse or default
123        TypeRef::Json => {
124            if optional {
125                format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())")
126            } else {
127                format!("{name}: serde_json::from_str(&val.{name}).unwrap_or_default()")
128            }
129        }
130        // Char: binding uses String, core uses char — convert first character
131        TypeRef::Char => {
132            if optional {
133                format!("{name}: val.{name}.and_then(|s| s.chars().next())")
134            } else {
135                format!("{name}: val.{name}.chars().next().unwrap_or('*')")
136            }
137        }
138        // Duration: binding uses u64 (secs), core uses std::time::Duration
139        TypeRef::Duration => {
140            if optional {
141                format!("{name}: val.{name}.map(std::time::Duration::from_secs)")
142            } else {
143                format!("{name}: std::time::Duration::from_secs(val.{name})")
144            }
145        }
146        // Path needs .into() — binding uses String, core uses PathBuf
147        TypeRef::Path => {
148            if optional {
149                format!("{name}: val.{name}.map(Into::into)")
150            } else {
151                format!("{name}: val.{name}.into()")
152            }
153        }
154        // Named type -- needs .into() to convert between binding and core types
155        TypeRef::Named(_) => {
156            if optional {
157                format!("{name}: val.{name}.map(Into::into)")
158            } else {
159                format!("{name}: val.{name}.into()")
160            }
161        }
162        // Optional with inner
163        TypeRef::Optional(inner) => match inner.as_ref() {
164            TypeRef::Json => format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())"),
165            TypeRef::Named(_) | TypeRef::Path => format!("{name}: val.{name}.map(Into::into)"),
166            TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_)) => {
167                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
168            }
169            _ => format!("{name}: val.{name}"),
170        },
171        // Vec of named or Json types -- map each element
172        TypeRef::Vec(inner) => match inner.as_ref() {
173            TypeRef::Json => {
174                if optional {
175                    format!(
176                        "{name}: val.{name}.map(|v| v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect())"
177                    )
178                } else {
179                    format!("{name}: val.{name}.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()")
180                }
181            }
182            TypeRef::Named(_) => {
183                if optional {
184                    format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
185                } else {
186                    format!("{name}: val.{name}.into_iter().map(Into::into).collect()")
187                }
188            }
189            _ => format!("{name}: val.{name}"),
190        },
191        // Map -- collect to handle HashMap↔BTreeMap conversion;
192        // additionally convert Named keys/values via Into.
193        // Skip .map() when neither key nor value needs conversion (avoids clippy::map_identity).
194        TypeRef::Map(k, v) => {
195            let has_named_key = matches!(k.as_ref(), TypeRef::Named(_));
196            let has_named_val = matches!(v.as_ref(), TypeRef::Named(_));
197            if has_named_key || has_named_val {
198                let k_expr = if has_named_key { "k.into()" } else { "k" };
199                let v_expr = if has_named_val { "v.into()" } else { "v" };
200                if optional {
201                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect())")
202                } else {
203                    format!("{name}: val.{name}.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect()")
204                }
205            } else {
206                // No conversion needed — just collect for potential HashMap↔BTreeMap type change
207                if optional {
208                    format!("{name}: val.{name}.map(|m| m.into_iter().collect())")
209                } else {
210                    format!("{name}: val.{name}.into_iter().collect()")
211                }
212            }
213        }
214    }
215}
216
217/// Binding→core field conversion with backend-specific config (i64 casts, etc.).
218pub fn field_conversion_to_core_cfg(name: &str, ty: &TypeRef, optional: bool, config: &ConversionConfig) -> String {
219    // WASM JsValue: use serde_wasm_bindgen for Map, nested Vec, and Vec<Json> types
220    if config.map_uses_jsvalue {
221        let is_nested_vec = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Vec(_)));
222        let is_vec_json = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json));
223        let is_map = matches!(ty, TypeRef::Map(_, _));
224        if is_nested_vec || is_map || is_vec_json {
225            if optional {
226                return format!(
227                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
228                );
229            }
230            return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
231        }
232        if let TypeRef::Optional(inner) = ty {
233            let is_inner_nested = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Vec(_)));
234            let is_inner_vec_json = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Json));
235            let is_inner_map = matches!(inner.as_ref(), TypeRef::Map(_, _));
236            if is_inner_nested || is_inner_map || is_inner_vec_json {
237                return format!(
238                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
239                );
240            }
241        }
242    }
243
244    // Json→String binding→core: use Default::default() (lossy — can't parse String back)
245    if config.json_to_string && matches!(ty, TypeRef::Json) {
246        return format!("{name}: Default::default()");
247    }
248    // Json→JsValue binding→core: use serde_wasm_bindgen to convert (WASM)
249    if config.map_uses_jsvalue && matches!(ty, TypeRef::Json) {
250        if optional {
251            return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())");
252        }
253        return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
254    }
255    if !config.cast_large_ints_to_i64 && !config.cast_f32_to_f64 && !config.json_to_string {
256        return field_conversion_to_core(name, ty, optional);
257    }
258    // Cast mode: handle primitives and Duration differently
259    match ty {
260        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
261            let core_ty = core_prim_str(p);
262            if optional {
263                format!("{name}: val.{name}.map(|v| v as {core_ty})")
264            } else {
265                format!("{name}: val.{name} as {core_ty}")
266            }
267        }
268        // f64→f32 cast (NAPI binding f64 → core f32)
269        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
270            if optional {
271                format!("{name}: val.{name}.map(|v| v as f32)")
272            } else {
273                format!("{name}: val.{name} as f32")
274            }
275        }
276        TypeRef::Duration if config.cast_large_ints_to_i64 => {
277            if optional {
278                format!("{name}: val.{name}.map(|v| std::time::Duration::from_secs(v as u64))")
279            } else {
280                format!("{name}: std::time::Duration::from_secs(val.{name} as u64)")
281            }
282        }
283        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) => {
284            if let TypeRef::Primitive(p) = inner.as_ref() {
285                let core_ty = core_prim_str(p);
286                format!("{name}: val.{name}.map(|v| v as {core_ty})")
287            } else {
288                field_conversion_to_core(name, ty, optional)
289            }
290        }
291        // Vec<u64/usize/isize> needs element-wise i64→core casting
292        TypeRef::Vec(inner)
293            if config.cast_large_ints_to_i64
294                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
295        {
296            if let TypeRef::Primitive(p) = inner.as_ref() {
297                let core_ty = core_prim_str(p);
298                if optional {
299                    format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
300                } else {
301                    format!("{name}: val.{name}.into_iter().map(|v| v as {core_ty}).collect()")
302                }
303            } else {
304                field_conversion_to_core(name, ty, optional)
305            }
306        }
307        // Vec<f32> needs element-wise cast when f32→f64 mapping is active (NAPI)
308        TypeRef::Vec(inner)
309            if config.cast_f32_to_f64 && matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32)) =>
310        {
311            if optional {
312                format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
313            } else {
314                format!("{name}: val.{name}.into_iter().map(|v| v as f32).collect()")
315            }
316        }
317        // Optional(Vec(f32)) needs element-wise cast (NAPI only)
318        TypeRef::Optional(inner)
319            if config.cast_f32_to_f64
320                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
321        {
322            format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
323        }
324        // Fall through to default for everything else
325        _ => field_conversion_to_core(name, ty, optional),
326    }
327}