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 (e.g. Box<str>→String, Cow<str>→String).
90    // Use .to_string() for String targets, proper iteration for Vec/Map, format!("{:?}") as last resort.
91    if sanitized {
92        // Map(String, String): sanitized from Map(Box<str>, Box<str>) etc.
93        if let TypeRef::Map(k, v) = ty {
94            if matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String) {
95                if optional {
96                    return format!(
97                        "{name}: val.{name}.as_ref().map(|m| m.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect())"
98                    );
99                }
100                return format!(
101                    "{name}: val.{name}.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()"
102                );
103            }
104        }
105        // Vec<String>: sanitized from Vec<Box<str>> etc.
106        if let TypeRef::Vec(inner) = ty {
107            if matches!(inner.as_ref(), TypeRef::String) {
108                if optional {
109                    return format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| i.to_string()).collect())");
110                }
111                return format!("{name}: val.{name}.iter().map(ToString::to_string).collect()");
112            }
113        }
114        // Optional<Vec<String>>: sanitized from Optional<Vec<Box<str>>> etc.
115        if let TypeRef::Optional(opt_inner) = ty {
116            if let TypeRef::Vec(vec_inner) = opt_inner.as_ref() {
117                if matches!(vec_inner.as_ref(), TypeRef::String) {
118                    return format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| i.to_string()).collect())");
119                }
120            }
121        }
122        // String: sanitized from Box<str>, Cow<str>, etc. — use .to_string()
123        if matches!(ty, TypeRef::String) {
124            if optional {
125                return format!("{name}: val.{name}.as_ref().map(ToString::to_string)");
126            }
127            return format!("{name}: val.{name}.to_string()");
128        }
129        // Fallback for truly unknown sanitized types
130        if optional {
131            return format!("{name}: val.{name}.as_ref().map(|v| format!(\"{{:?}}\", v))");
132        }
133        return format!("{name}: format!(\"{{:?}}\", val.{name})");
134    }
135    match ty {
136        // Duration: core uses std::time::Duration, binding uses u64 (secs)
137        TypeRef::Duration => {
138            if optional {
139                return format!("{name}: val.{name}.map(|d| d.as_secs())");
140            }
141            format!("{name}: val.{name}.as_secs()")
142        }
143        // Path: core uses PathBuf, binding uses String — PathBuf→String needs special handling
144        TypeRef::Path => {
145            if optional {
146                format!("{name}: val.{name}.map(|p| p.to_string_lossy().to_string())")
147            } else {
148                format!("{name}: val.{name}.to_string_lossy().to_string()")
149            }
150        }
151        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Path) => {
152            format!("{name}: val.{name}.map(|p| p.to_string_lossy().to_string())")
153        }
154        // Char: core uses char, binding uses String — convert char to string
155        TypeRef::Char => {
156            if optional {
157                format!("{name}: val.{name}.map(|c| c.to_string())")
158            } else {
159                format!("{name}: val.{name}.to_string()")
160            }
161        }
162        // Bytes: core uses bytes::Bytes, binding uses Vec<u8>
163        TypeRef::Bytes => {
164            if optional {
165                format!("{name}: val.{name}.map(|v| v.to_vec())")
166            } else {
167                format!("{name}: val.{name}.to_vec()")
168            }
169        }
170        // Opaque Named types: wrap in Arc to create the binding wrapper
171        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
172            if optional {
173                format!("{name}: val.{name}.map(|v| {n} {{ inner: Arc::new(v) }})")
174            } else {
175                format!("{name}: {n} {{ inner: Arc::new(val.{name}) }}")
176            }
177        }
178        // Json: core uses serde_json::Value, binding uses String — use .to_string()
179        TypeRef::Json => {
180            if optional {
181                format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
182            } else {
183                format!("{name}: val.{name}.to_string()")
184            }
185        }
186        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Json) => {
187            format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
188        }
189        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json) => {
190            if optional {
191                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| i.to_string()).collect())")
192            } else {
193                format!("{name}: val.{name}.iter().map(ToString::to_string).collect()")
194            }
195        }
196        // Everything else is symmetric
197        _ => field_conversion_to_core(name, ty, optional),
198    }
199}
200
201/// Core→binding field conversion with backend-specific config.
202pub fn field_conversion_from_core_cfg(
203    name: &str,
204    ty: &TypeRef,
205    optional: bool,
206    sanitized: bool,
207    opaque_types: &AHashSet<String>,
208    config: &ConversionConfig,
209) -> String {
210    // Sanitized fields: for WASM (map_uses_jsvalue), Map and Vec<String> fields target JsValue
211    // and need serde_wasm_bindgen::to_value() instead of iterator-based .collect().
212    if sanitized {
213        if config.map_uses_jsvalue {
214            // Map(String, String) sanitized → JsValue
215            if let TypeRef::Map(k, v) = ty {
216                if matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String) {
217                    if optional {
218                        return format!(
219                            "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())"
220                        );
221                    }
222                    return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
223                }
224            }
225            // Vec<String> sanitized → JsValue
226            if let TypeRef::Vec(inner) = ty {
227                if matches!(inner.as_ref(), TypeRef::String) {
228                    if optional {
229                        return format!(
230                            "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())"
231                        );
232                    }
233                    return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
234                }
235            }
236            // Vec<Json> sanitized → JsValue
237            if let TypeRef::Vec(inner) = ty {
238                if matches!(inner.as_ref(), TypeRef::Json) {
239                    if optional {
240                        return format!(
241                            "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())"
242                        );
243                    }
244                    return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
245                }
246            }
247        }
248        return field_conversion_from_core(name, ty, optional, sanitized, opaque_types);
249    }
250
251    // WASM JsValue: use serde_wasm_bindgen for Map and nested Vec types
252    if config.map_uses_jsvalue {
253        let is_nested_vec = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Vec(_)));
254        let is_map = matches!(ty, TypeRef::Map(_, _));
255        if is_nested_vec || is_map {
256            if optional {
257                return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())");
258            }
259            return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
260        }
261        if let TypeRef::Optional(inner) = ty {
262            let is_inner_nested = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Vec(_)));
263            let is_inner_map = matches!(inner.as_ref(), TypeRef::Map(_, _));
264            if is_inner_nested || is_inner_map {
265                return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())");
266            }
267        }
268    }
269
270    let prefix = config.type_name_prefix;
271    let is_enum_string = |n: &str| -> bool { config.enum_string_names.as_ref().is_some_and(|names| names.contains(n)) };
272
273    match ty {
274        // i64 casting for large int primitives
275        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
276            let cast_to = binding_prim_str(p);
277            if optional {
278                format!("{name}: val.{name}.map(|v| v as {cast_to})")
279            } else {
280                format!("{name}: val.{name} as {cast_to}")
281            }
282        }
283        // f32→f64 casting (NAPI only)
284        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
285            if optional {
286                format!("{name}: val.{name}.map(|v| v as f64)")
287            } else {
288                format!("{name}: val.{name} as f64")
289            }
290        }
291        // Duration with i64 casting
292        TypeRef::Duration if config.cast_large_ints_to_i64 => {
293            if optional {
294                format!("{name}: val.{name}.map(|d| d.as_secs() as i64)")
295            } else {
296                format!("{name}: val.{name}.as_secs() as i64")
297            }
298        }
299        // Opaque Named types with prefix: wrap in Arc with prefixed binding name
300        TypeRef::Named(n) if opaque_types.contains(n.as_str()) && !prefix.is_empty() => {
301            let prefixed = format!("{prefix}{n}");
302            if optional {
303                format!("{name}: val.{name}.map(|v| {prefixed} {{ inner: Arc::new(v) }})")
304            } else {
305                format!("{name}: {prefixed} {{ inner: Arc::new(val.{name}) }}")
306            }
307        }
308        // Enum-to-String Named types (PHP pattern)
309        TypeRef::Named(n) if is_enum_string(n) => {
310            if optional {
311                format!("{name}: val.{name}.as_ref().map(|v| format!(\"{{:?}}\", v))")
312            } else {
313                format!("{name}: format!(\"{{:?}}\", val.{name})")
314            }
315        }
316        // Vec<Enum-to-String> Named types (PHP pattern): element-wise format!("{:?}")
317        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(n) if is_enum_string(n)) => {
318            if optional {
319                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|x| format!(\"{{:?}}\", x)).collect())")
320            } else {
321                format!("{name}: val.{name}.iter().map(|v| format!(\"{{:?}}\", v)).collect()")
322            }
323        }
324        // Optional(Vec<Enum-to-String>) Named types (PHP pattern)
325        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(n) if is_enum_string(n))) =>
326        {
327            format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|x| format!(\"{{:?}}\", x)).collect())")
328        }
329        // Vec<f32> needs element-wise cast to f64 when f32→f64 mapping is active
330        TypeRef::Vec(inner)
331            if config.cast_f32_to_f64 && matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32)) =>
332        {
333            if optional {
334                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
335            } else {
336                format!("{name}: val.{name}.iter().map(|&v| v as f64).collect()")
337            }
338        }
339        // Optional(Vec(f32)) needs element-wise cast to f64
340        TypeRef::Optional(inner)
341            if config.cast_f32_to_f64
342                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
343        {
344            format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
345        }
346        // Optional with i64-cast inner
347        TypeRef::Optional(inner)
348            if config.cast_large_ints_to_i64
349                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
350        {
351            if let TypeRef::Primitive(p) = inner.as_ref() {
352                let cast_to = binding_prim_str(p);
353                format!("{name}: val.{name}.map(|v| v as {cast_to})")
354            } else {
355                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
356            }
357        }
358        // Vec<u64/usize/isize> needs element-wise i64 casting (core→binding)
359        TypeRef::Vec(inner)
360            if config.cast_large_ints_to_i64
361                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
362        {
363            if let TypeRef::Primitive(p) = inner.as_ref() {
364                let cast_to = binding_prim_str(p);
365                if optional {
366                    format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as {cast_to}).collect())")
367                } else {
368                    format!("{name}: val.{name}.iter().map(|&v| v as {cast_to}).collect()")
369                }
370            } else {
371                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
372            }
373        }
374        // Json→String: core uses serde_json::Value, binding uses String (PHP)
375        TypeRef::Json if config.json_to_string => {
376            if optional {
377                format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
378            } else {
379                format!("{name}: val.{name}.to_string()")
380            }
381        }
382        // Json→JsValue: core uses serde_json::Value, binding uses JsValue (WASM)
383        TypeRef::Json if config.map_uses_jsvalue => {
384            if optional {
385                format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
386            } else {
387                format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)")
388            }
389        }
390        // Vec<Json>→JsValue: core uses Vec<serde_json::Value>, binding uses JsValue (WASM)
391        TypeRef::Vec(inner) if config.map_uses_jsvalue && matches!(inner.as_ref(), TypeRef::Json) => {
392            if optional {
393                format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
394            } else {
395                format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)")
396            }
397        }
398        // Optional(Vec<Json>)→JsValue (WASM)
399        TypeRef::Optional(inner)
400            if config.map_uses_jsvalue
401                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Json)) =>
402        {
403            format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
404        }
405        // Fall through to default (handles paths, opaque without prefix, etc.)
406        _ => field_conversion_from_core(name, ty, optional, sanitized, opaque_types),
407    }
408}