Skip to main content

alef_codegen/conversions/
core_to_binding.rs

1use ahash::AHashSet;
2use alef_core::ir::{PrimitiveType, TypeDef, TypeRef};
3use std::fmt::Write;
4
5use super::ConversionConfig;
6use super::binding_to_core::field_conversion_to_core;
7use super::helpers::{binding_prim_str, core_type_path, needs_i64_cast};
8
9/// Generate `impl From<core::Type> for BindingType` (core -> binding).
10pub fn gen_from_core_to_binding(typ: &TypeDef, core_import: &str, opaque_types: &AHashSet<String>) -> String {
11    gen_from_core_to_binding_cfg(typ, core_import, opaque_types, &ConversionConfig::default())
12}
13
14/// Generate `impl From<core::Type> for BindingType` with backend-specific config.
15pub fn gen_from_core_to_binding_cfg(
16    typ: &TypeDef,
17    core_import: &str,
18    opaque_types: &AHashSet<String>,
19    config: &ConversionConfig,
20) -> String {
21    let core_path = core_type_path(typ, core_import);
22    let binding_name = format!("{}{}", config.type_name_prefix, typ.name);
23    let mut out = String::with_capacity(256);
24    writeln!(out, "impl From<{core_path}> for {binding_name} {{").ok();
25    writeln!(out, "    fn from(val: {core_path}) -> Self {{").ok();
26    let optionalized = config.optionalize_defaults && typ.has_default;
27    writeln!(out, "        Self {{").ok();
28    for field in &typ.fields {
29        let base_conversion = field_conversion_from_core_cfg(
30            &field.name,
31            &field.ty,
32            field.optional,
33            field.sanitized,
34            opaque_types,
35            config,
36        );
37        // Box<T> fields: dereference before conversion.
38        let base_conversion = if field.is_boxed && matches!(&field.ty, TypeRef::Named(_)) {
39            if field.optional {
40                // Optional<Box<T>>: replace .map(Into::into) with .map(|v| (*v).into())
41                let src = format!("{}: val.{}.map(Into::into)", field.name, field.name);
42                let dst = format!("{}: val.{}.map(|v| (*v).into())", field.name, field.name);
43                if base_conversion == src { dst } else { base_conversion }
44            } else {
45                // Box<T>: replace `val.{name}` with `(*val.{name})`
46                base_conversion.replace(&format!("val.{}", field.name), &format!("(*val.{})", field.name))
47            }
48        } else {
49            base_conversion
50        };
51        // Optionalized non-optional fields need Some() wrapping in core→binding direction
52        let conversion = if optionalized && !field.optional {
53            // Extract the value expression after "name: " and wrap in Some()
54            if let Some(expr) = base_conversion.strip_prefix(&format!("{}: ", field.name)) {
55                format!("{}: Some({})", field.name, expr)
56            } else {
57                base_conversion
58            }
59        } else {
60            base_conversion
61        };
62        // Skip cfg-gated fields — they don't exist in the binding struct
63        if field.cfg.is_some() {
64            continue;
65        }
66        writeln!(out, "            {conversion},").ok();
67    }
68
69    // Synthetic field conversion for cfg-gated metadata field (NAPI only).
70    if config.include_cfg_metadata && typ.has_stripped_cfg_fields && typ.name == "ConversionResult" {
71        writeln!(out, "            metadata: Some(val.metadata.into()),").ok();
72    }
73
74    writeln!(out, "        }}").ok();
75    writeln!(out, "    }}").ok();
76    write!(out, "}}").ok();
77    out
78}
79
80/// Same but for core -> binding direction.
81/// Some types are asymmetric (PathBuf→String, sanitized fields need .to_string()).
82pub fn field_conversion_from_core(
83    name: &str,
84    ty: &TypeRef,
85    optional: bool,
86    sanitized: bool,
87    opaque_types: &AHashSet<String>,
88) -> String {
89    // Sanitized fields: the binding type differs from core. Use format!("{:?}") to convert.
90    // When the binding type is Vec<String> (sanitized from Vec<Unknown>), map each element.
91    if sanitized {
92        // Check if binding type is Vec<String> (inner was sanitized from Named→String)
93        if let TypeRef::Vec(inner) = ty {
94            if matches!(inner.as_ref(), TypeRef::String) {
95                if optional {
96                    return format!(
97                        "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| format!(\"{{:?}}\", i)).collect())"
98                    );
99                }
100                return format!("{name}: val.{name}.iter().map(|v| format!(\"{{:?}}\", v)).collect()");
101            }
102        }
103        // Check if binding type is Optional<Vec<String>> (sanitized from Optional<Vec<Unknown>>)
104        if let TypeRef::Optional(opt_inner) = ty {
105            if let TypeRef::Vec(vec_inner) = opt_inner.as_ref() {
106                if matches!(vec_inner.as_ref(), TypeRef::String) {
107                    return format!(
108                        "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| format!(\"{{:?}}\", i)).collect())"
109                    );
110                }
111            }
112        }
113        if optional {
114            return format!("{name}: val.{name}.as_ref().map(|v| format!(\"{{:?}}\", v))");
115        }
116        return format!("{name}: format!(\"{{:?}}\", val.{name})");
117    }
118    match ty {
119        // Duration: core uses std::time::Duration, binding uses u64 (secs)
120        TypeRef::Duration => {
121            if optional {
122                return format!("{name}: val.{name}.map(|d| d.as_secs())");
123            }
124            format!("{name}: val.{name}.as_secs()")
125        }
126        // Path: core uses PathBuf, binding uses String — PathBuf→String needs special handling
127        TypeRef::Path => {
128            if optional {
129                format!("{name}: val.{name}.map(|p| p.to_string_lossy().to_string())")
130            } else {
131                format!("{name}: val.{name}.to_string_lossy().to_string()")
132            }
133        }
134        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Path) => {
135            format!("{name}: val.{name}.map(|p| p.to_string_lossy().to_string())")
136        }
137        // Char: core uses char, binding uses String — convert char to string
138        TypeRef::Char => {
139            if optional {
140                format!("{name}: val.{name}.map(|c| c.to_string())")
141            } else {
142                format!("{name}: val.{name}.to_string()")
143            }
144        }
145        // Bytes: core uses bytes::Bytes, binding uses Vec<u8>
146        TypeRef::Bytes => {
147            if optional {
148                format!("{name}: val.{name}.map(|v| v.to_vec())")
149            } else {
150                format!("{name}: val.{name}.to_vec()")
151            }
152        }
153        // Opaque Named types: wrap in Arc to create the binding wrapper
154        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
155            if optional {
156                format!("{name}: val.{name}.map(|v| {n} {{ inner: Arc::new(v) }})")
157            } else {
158                format!("{name}: {n} {{ inner: Arc::new(val.{name}) }}")
159            }
160        }
161        // Everything else is symmetric
162        _ => field_conversion_to_core(name, ty, optional),
163    }
164}
165
166/// Core→binding field conversion with backend-specific config.
167pub fn field_conversion_from_core_cfg(
168    name: &str,
169    ty: &TypeRef,
170    optional: bool,
171    sanitized: bool,
172    opaque_types: &AHashSet<String>,
173    config: &ConversionConfig,
174) -> String {
175    // Sanitized fields handled the same regardless of config
176    if sanitized {
177        return field_conversion_from_core(name, ty, optional, sanitized, opaque_types);
178    }
179
180    // WASM JsValue: use serde_wasm_bindgen for Map and nested Vec types
181    if config.map_uses_jsvalue {
182        let is_nested_vec = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Vec(_)));
183        let is_map = matches!(ty, TypeRef::Map(_, _));
184        if is_nested_vec || is_map {
185            if optional {
186                return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())");
187            }
188            return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
189        }
190        if let TypeRef::Optional(inner) = ty {
191            let is_inner_nested = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Vec(_)));
192            let is_inner_map = matches!(inner.as_ref(), TypeRef::Map(_, _));
193            if is_inner_nested || is_inner_map {
194                return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())");
195            }
196        }
197    }
198
199    let prefix = config.type_name_prefix;
200    let is_enum_string = |n: &str| -> bool { config.enum_string_names.as_ref().is_some_and(|names| names.contains(n)) };
201
202    match ty {
203        // i64 casting for large int primitives
204        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
205            let cast_to = binding_prim_str(p);
206            if optional {
207                format!("{name}: val.{name}.map(|v| v as {cast_to})")
208            } else {
209                format!("{name}: val.{name} as {cast_to}")
210            }
211        }
212        // f32→f64 casting (NAPI only)
213        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
214            if optional {
215                format!("{name}: val.{name}.map(|v| v as f64)")
216            } else {
217                format!("{name}: val.{name} as f64")
218            }
219        }
220        // Duration with i64 casting
221        TypeRef::Duration if config.cast_large_ints_to_i64 => {
222            if optional {
223                format!("{name}: val.{name}.map(|d| d.as_secs() as i64)")
224            } else {
225                format!("{name}: val.{name}.as_secs() as i64")
226            }
227        }
228        // Opaque Named types with prefix: wrap in Arc with prefixed binding name
229        TypeRef::Named(n) if opaque_types.contains(n.as_str()) && !prefix.is_empty() => {
230            let prefixed = format!("{prefix}{n}");
231            if optional {
232                format!("{name}: val.{name}.map(|v| {prefixed} {{ inner: Arc::new(v) }})")
233            } else {
234                format!("{name}: {prefixed} {{ inner: Arc::new(val.{name}) }}")
235            }
236        }
237        // Enum-to-String Named types (PHP pattern)
238        TypeRef::Named(n) if is_enum_string(n) => {
239            if optional {
240                format!("{name}: val.{name}.as_ref().map(|v| format!(\"{{:?}}\", v))")
241            } else {
242                format!("{name}: format!(\"{{:?}}\", val.{name})")
243            }
244        }
245        // Vec<f32> needs element-wise cast to f64 when f32→f64 mapping is active
246        TypeRef::Vec(inner)
247            if config.cast_f32_to_f64 && matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32)) =>
248        {
249            if optional {
250                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
251            } else {
252                format!("{name}: val.{name}.iter().map(|&v| v as f64).collect()")
253            }
254        }
255        // Optional(Vec(f32)) needs element-wise cast to f64
256        TypeRef::Optional(inner)
257            if config.cast_f32_to_f64
258                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
259        {
260            format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
261        }
262        // Optional with i64-cast inner
263        TypeRef::Optional(inner)
264            if config.cast_large_ints_to_i64
265                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
266        {
267            if let TypeRef::Primitive(p) = inner.as_ref() {
268                let cast_to = binding_prim_str(p);
269                format!("{name}: val.{name}.map(|v| v as {cast_to})")
270            } else {
271                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
272            }
273        }
274        // Vec<u64/usize/isize> needs element-wise i64 casting (core→binding)
275        TypeRef::Vec(inner)
276            if config.cast_large_ints_to_i64
277                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
278        {
279            if let TypeRef::Primitive(p) = inner.as_ref() {
280                let cast_to = binding_prim_str(p);
281                if optional {
282                    format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as {cast_to}).collect())")
283                } else {
284                    format!("{name}: val.{name}.iter().map(|&v| v as {cast_to}).collect()")
285                }
286            } else {
287                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
288            }
289        }
290        // Json→String: core uses serde_json::Value, binding uses String (PHP)
291        TypeRef::Json if config.json_to_string => {
292            if optional {
293                format!("{name}: val.{name}.as_ref().map(|v| v.to_string())")
294            } else {
295                format!("{name}: val.{name}.to_string()")
296            }
297        }
298        // Json→JsValue: core uses serde_json::Value, binding uses JsValue (WASM)
299        TypeRef::Json if config.map_uses_jsvalue => {
300            if optional {
301                format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
302            } else {
303                format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)")
304            }
305        }
306        // Fall through to default (handles paths, opaque without prefix, etc.)
307        _ => field_conversion_from_core(name, ty, optional, sanitized, opaque_types),
308    }
309}