Skip to main content

alef_codegen/conversions/
core_to_binding.rs

1use ahash::AHashSet;
2use alef_core::ir::{CoreWrapper, PrimitiveType, TypeDef, TypeRef};
3use std::fmt::Write;
4
5use super::ConversionConfig;
6use super::binding_to_core::field_conversion_to_core;
7use super::helpers::is_newtype;
8use super::helpers::{binding_prim_str, core_type_path_remapped, needs_i64_cast};
9
10/// Generate `impl From<core::Type> for BindingType` (core -> binding).
11pub fn gen_from_core_to_binding(typ: &TypeDef, core_import: &str, opaque_types: &AHashSet<String>) -> String {
12    gen_from_core_to_binding_cfg(typ, core_import, opaque_types, &ConversionConfig::default())
13}
14
15/// Generate `impl From<core::Type> for BindingType` with backend-specific config.
16pub fn gen_from_core_to_binding_cfg(
17    typ: &TypeDef,
18    core_import: &str,
19    opaque_types: &AHashSet<String>,
20    config: &ConversionConfig,
21) -> String {
22    let core_path = core_type_path_remapped(typ, core_import, config.source_crate_remaps);
23    let binding_name = format!("{}{}", config.type_name_prefix, typ.name);
24    let mut out = String::with_capacity(256);
25    writeln!(out, "#[allow(clippy::redundant_closure, clippy::useless_conversion)]").ok();
26    writeln!(out, "impl From<{core_path}> for {binding_name} {{").ok();
27    writeln!(out, "    fn from(val: {core_path}) -> Self {{").ok();
28
29    // Newtype structs: extract inner value with val.0
30    if is_newtype(typ) {
31        let field = &typ.fields[0];
32        let inner_expr = match &field.ty {
33            TypeRef::Named(_) => "val.0.into()".to_string(),
34            TypeRef::Path => "val.0.to_string_lossy().to_string()".to_string(),
35            TypeRef::Duration => "val.0.as_millis() as u64".to_string(),
36            _ => "val.0".to_string(),
37        };
38        writeln!(out, "        Self {{ _0: {inner_expr} }}").ok();
39        writeln!(out, "    }}").ok();
40        write!(out, "}}").ok();
41        return out;
42    }
43
44    let optionalized = config.optionalize_defaults && typ.has_default;
45    writeln!(out, "        Self {{").ok();
46    for field in &typ.fields {
47        // Fields referencing excluded types are not present in the binding struct — skip
48        if !config.exclude_types.is_empty()
49            && super::helpers::field_references_excluded_type(&field.ty, config.exclude_types)
50        {
51            continue;
52        }
53        let base_conversion = field_conversion_from_core_cfg(
54            &field.name,
55            &field.ty,
56            field.optional,
57            field.sanitized,
58            opaque_types,
59            config,
60        );
61        // Box<T> fields: dereference before conversion.
62        let base_conversion = if field.is_boxed && matches!(&field.ty, TypeRef::Named(_)) {
63            if field.optional {
64                // Optional<Box<T>>: replace .map(Into::into) with .map(|v| (*v).into())
65                let src = format!("{}: val.{}.map(Into::into)", field.name, field.name);
66                let dst = format!("{}: val.{}.map(|v| (*v).into())", field.name, field.name);
67                if base_conversion == src { dst } else { base_conversion }
68            } else {
69                // Box<T>: replace `val.{name}` with `(*val.{name})`
70                base_conversion.replace(&format!("val.{}", field.name), &format!("(*val.{})", field.name))
71            }
72        } else {
73            base_conversion
74        };
75        // Newtype unwrapping: when the field was resolved from a newtype (e.g. NodeIndex → u32),
76        // unwrap the core newtype by accessing `.0`.
77        // e.g. `source: val.source` → `source: val.source.0`
78        //      `parent: val.parent` → `parent: val.parent.map(|v| v.0)`
79        //      `children: val.children` → `children: val.children.iter().map(|v| v.0).collect()`
80        let base_conversion = if field.newtype_wrapper.is_some() {
81            match &field.ty {
82                TypeRef::Optional(_) => {
83                    // Replace `val.{name}` with `val.{name}.map(|v| v.0)` in the generated expression
84                    base_conversion.replace(
85                        &format!("val.{}", field.name),
86                        &format!("val.{}.map(|v| v.0)", field.name),
87                    )
88                }
89                TypeRef::Vec(_) => {
90                    // Replace `val.{name}` with `val.{name}.iter().map(|v| v.0).collect()` in expression
91                    base_conversion.replace(
92                        &format!("val.{}", field.name),
93                        &format!("val.{}.iter().map(|v| v.0).collect::<Vec<_>>()", field.name),
94                    )
95                }
96                // When `optional=true` and `ty` is a plain Primitive (not TypeRef::Optional), the core
97                // field is actually `Option<NewtypeT>`, so we must use `.map(|v| v.0)` not `.0`.
98                _ if field.optional => base_conversion.replace(
99                    &format!("val.{}", field.name),
100                    &format!("val.{}.map(|v| v.0)", field.name),
101                ),
102                _ => {
103                    // Direct field: append `.0` to access the inner primitive
104                    base_conversion.replace(&format!("val.{}", field.name), &format!("val.{}.0", field.name))
105                }
106            }
107        } else {
108            base_conversion
109        };
110        // When field.optional=true AND field.ty=Optional(T), the binding struct flattens
111        // Option<Option<T>> to Option<T>. Core produces Option<Option<T>>, binding needs
112        // Option<T>. Generate the conversion by treating the pre-flattened field as Option<T>:
113        // call the standard conversion for the inner type T with optional=true, substituting
114        // val.{name}.flatten() for val.{name} so all cast/conversion logic applies to T.
115        let is_flattened_optional = field.optional && matches!(field.ty, TypeRef::Optional(_));
116        let base_conversion = if is_flattened_optional {
117            if let TypeRef::Optional(inner) = &field.ty {
118                // Produce the conversion as if the field is Option<inner> with value val.name.flatten()
119                let inner_conv = field_conversion_from_core_cfg(
120                    &field.name,
121                    inner.as_ref(),
122                    true,
123                    field.sanitized,
124                    opaque_types,
125                    config,
126                );
127                // inner_conv references val.{name}; replace with val.{name}.flatten()
128                inner_conv.replace(&format!("val.{}", field.name), &format!("val.{}.flatten()", field.name))
129            } else {
130                base_conversion
131            }
132        } else {
133            base_conversion
134        };
135        // Optionalized non-optional fields need Some() wrapping in core→binding direction.
136        // This covers both NAPI-style full optionalization and PyO3-style Duration optionalization.
137        // Flattened-optional fields are already handled above with the correct type.
138        let needs_some_wrap = !is_flattened_optional
139            && ((optionalized && !field.optional)
140                || (config.option_duration_on_defaults
141                    && typ.has_default
142                    && !field.optional
143                    && matches!(field.ty, TypeRef::Duration)));
144        let conversion = if needs_some_wrap {
145            // Extract the value expression after "name: " and wrap in Some()
146            if let Some(expr) = base_conversion.strip_prefix(&format!("{}: ", field.name)) {
147                format!("{}: Some({})", field.name, expr)
148            } else {
149                base_conversion
150            }
151        } else {
152            base_conversion
153        };
154        // CoreWrapper: unwrap Arc, convert Cow→String, Bytes→Vec<u8>
155        // For sanitized fields, still apply Cow→String conversion: Cow<'_, str> sanitizes to
156        // TypeRef::String and the Debug-formatted fallback produces quotes, but Cow implements
157        // Display so .to_string() (emitted by apply_core_wrapper_from_core for Cow) is correct.
158        // Other sanitized fields (unknown Named types) still fall through to Debug formatting.
159        let conversion = if !field.sanitized || field.core_wrapper == alef_core::ir::CoreWrapper::Cow {
160            apply_core_wrapper_from_core(
161                &conversion,
162                &field.name,
163                &field.core_wrapper,
164                &field.vec_inner_core_wrapper,
165                field.optional,
166            )
167        } else {
168            conversion
169        };
170        // Skip cfg-gated fields — they don't exist in the binding struct
171        if field.cfg.is_some() {
172            continue;
173        }
174        // In core→binding direction, the binding struct field may be keyword-escaped
175        // (e.g. `class_` for `class`). The generated conversion has `field.name: expr`
176        // on the left side — rename it to `binding_name: expr` when needed.
177        let binding_field = config.binding_field_name_owned(&typ.name, &field.name);
178        let conversion = if binding_field != field.name {
179            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
180                format!("{binding_field}: {expr}")
181            } else {
182                conversion
183            }
184        } else {
185            conversion
186        };
187        writeln!(out, "            {conversion},").ok();
188    }
189
190    writeln!(out, "        }}").ok();
191    writeln!(out, "    }}").ok();
192    write!(out, "}}").ok();
193    out
194}
195
196/// Same but for core -> binding direction.
197/// Some types are asymmetric (PathBuf→String, sanitized fields need .to_string()).
198pub fn field_conversion_from_core(
199    name: &str,
200    ty: &TypeRef,
201    optional: bool,
202    sanitized: bool,
203    opaque_types: &AHashSet<String>,
204) -> String {
205    // Sanitized fields: the binding type differs from core (e.g. Box<str>→String, Cow<str>→String).
206    // Box<str>, Cow<str>, and Arc<str> all implement Display, so use .to_string() not {:?}.
207    // {:?} on string-like types produces debug-escaped output with surrounding quotes.
208    if sanitized {
209        // Vec<Primitive>: sanitized from tuple types like (u32, u32) → Vec<u32>.
210        // Core has a tuple, binding expects Vec — destructure the tuple.
211        if let TypeRef::Vec(inner) = ty {
212            if matches!(inner.as_ref(), TypeRef::Primitive(_)) {
213                if optional {
214                    return format!(
215                        "{name}: val.{name}.map(|t| {{ let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect(); arr }})"
216                    );
217                }
218                return format!("{name}: vec![val.{name}.0 as _, val.{name}.1 as _]");
219            }
220        }
221        // Optional(Vec<Primitive>): sanitized from Option<(T, T)> → Option<Vec<T>>.
222        if let TypeRef::Optional(opt_inner) = ty {
223            if let TypeRef::Vec(vec_inner) = opt_inner.as_ref() {
224                if matches!(vec_inner.as_ref(), TypeRef::Primitive(_)) {
225                    return format!("{name}: val.{name}.map(|t| vec![t.0 as _, t.1 as _])");
226                }
227            }
228        }
229        // Map(String, String): sanitized from Map(Box<str>, Box<str>) etc.
230        if let TypeRef::Map(k, v) = ty {
231            if matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String) {
232                if optional {
233                    return format!(
234                        "{name}: val.{name}.as_ref().map(|m| m.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect())"
235                    );
236                }
237                return format!(
238                    "{name}: val.{name}.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()"
239                );
240            }
241        }
242        // Vec<String>: sanitized from Vec<Box<str>>, Vec<Cow<str>>, Vec<Named>, etc.
243        // Use Debug formatting — the original core type may not implement Display.
244        if let TypeRef::Vec(inner) = ty {
245            if matches!(inner.as_ref(), TypeRef::String) {
246                if optional {
247                    return format!(
248                        "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| format!(\"{{:?}}\", i)).collect())"
249                    );
250                }
251                return format!("{name}: val.{name}.iter().map(|i| format!(\"{{:?}}\", i)).collect()");
252            }
253        }
254        // Optional<Vec<String>>: sanitized from Optional<Vec<Box<str>>>, Optional<Vec<Cow<str>>>, etc.
255        if let TypeRef::Optional(opt_inner) = ty {
256            if let TypeRef::Vec(vec_inner) = opt_inner.as_ref() {
257                if matches!(vec_inner.as_ref(), TypeRef::String) {
258                    return format!(
259                        "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| format!(\"{{:?}}\", i)).collect())"
260                    );
261                }
262            }
263        }
264        // String: sanitized from Box<str>, Cow<str>, (u32, u32), etc.
265        // Use Debug formatting — it works for all types (including tuples) and avoids Display
266        // trait bound failures when the original core type doesn't implement Display.
267        // Note: Cow<str> is handled before this point via the CoreWrapper::Cow path above.
268        if matches!(ty, TypeRef::String) {
269            if optional {
270                return format!("{name}: val.{name}.as_ref().map(|v| format!(\"{{v:?}}\"))");
271            }
272            return format!("{name}: format!(\"{{:?}}\", val.{name})");
273        }
274        // Fallback for truly unknown sanitized types — the core type may not implement Display,
275        // so use Debug formatting which is always available (required by the sanitized field's derive).
276        if optional {
277            return format!("{name}: val.{name}.as_ref().map(|v| format!(\"{{v:?}}\"))");
278        }
279        return format!("{name}: format!(\"{{:?}}\", val.{name})");
280    }
281    match ty {
282        // Duration: core uses std::time::Duration, binding uses u64 (millis)
283        TypeRef::Duration => {
284            if optional {
285                return format!("{name}: val.{name}.map(|d| d.as_millis() as u64)");
286            }
287            format!("{name}: val.{name}.as_millis() as u64")
288        }
289        // Path: core uses PathBuf, binding uses String — PathBuf→String needs special handling
290        TypeRef::Path => {
291            if optional {
292                format!("{name}: val.{name}.map(|p| p.to_string_lossy().to_string())")
293            } else {
294                format!("{name}: val.{name}.to_string_lossy().to_string()")
295            }
296        }
297        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Path) => {
298            format!("{name}: val.{name}.map(|p| p.to_string_lossy().to_string())")
299        }
300        // Char: core uses char, binding uses String — convert char to string
301        TypeRef::Char => {
302            if optional {
303                format!("{name}: val.{name}.map(|c| c.to_string())")
304            } else {
305                format!("{name}: val.{name}.to_string()")
306            }
307        }
308        // Bytes: core uses bytes::Bytes, binding uses Vec<u8>
309        TypeRef::Bytes => {
310            if optional {
311                format!("{name}: val.{name}.map(|v| v.to_vec())")
312            } else {
313                format!("{name}: val.{name}.to_vec()")
314            }
315        }
316        // Opaque Named types: wrap in Arc to create the binding wrapper
317        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
318            if optional {
319                format!("{name}: val.{name}.map(|v| {n} {{ inner: Arc::new(v) }})")
320            } else {
321                format!("{name}: {n} {{ inner: Arc::new(val.{name}) }}")
322            }
323        }
324        // Json: core uses serde_json::Value, binding uses String — use .to_string()
325        TypeRef::Json => {
326            if optional {
327                format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
328            } else {
329                format!("{name}: val.{name}.to_string()")
330            }
331        }
332        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Json) => {
333            format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
334        }
335        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json) => {
336            if optional {
337                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| i.to_string()).collect())")
338            } else {
339                format!("{name}: val.{name}.iter().map(ToString::to_string).collect()")
340            }
341        }
342        // Vec<Optional<Json>>: each element is Option<Value> → Option<String>
343        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Optional(oi) if matches!(oi.as_ref(), TypeRef::Json)) => {
344            if optional {
345                format!(
346                    "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| i.as_ref().map(ToString::to_string)).collect())"
347                )
348            } else {
349                format!("{name}: val.{name}.iter().map(|i| i.as_ref().map(ToString::to_string)).collect()")
350            }
351        }
352        // Map with Json values: core uses HashMap<K, serde_json::Value>, binding uses HashMap<K, String>.
353        // Always emit `k.to_string()` so Cow<'_, str> / Box<str> / Arc<str> keys (which the type
354        // resolver normalizes to TypeRef::String) convert correctly. For an actual `String` key
355        // this is a clone, accepted under the existing `#[allow(clippy::useless_conversion)]`.
356        TypeRef::Map(_k, v) if matches!(v.as_ref(), TypeRef::Json) => {
357            if optional {
358                format!(
359                    "{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect())"
360                )
361            } else {
362                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()")
363            }
364        }
365        // Map with Json keys: core uses HashMap<serde_json::Value, V>, binding uses HashMap<String, V>
366        TypeRef::Map(k, _v) if matches!(k.as_ref(), TypeRef::Json) => {
367            if optional {
368                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.to_string(), v)).collect())")
369            } else {
370                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.to_string(), v)).collect()")
371            }
372        }
373        // Map<K, Named>: each value needs .into() to convert core→binding
374        TypeRef::Map(_k, v) if matches!(v.as_ref(), TypeRef::Named(_)) => {
375            if optional {
376                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())")
377            } else {
378                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k, v.into())).collect()")
379            }
380        }
381        // Optional(Map<K, Named>): same but wrapped in Option
382        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Map(_k, v) if matches!(v.as_ref(), TypeRef::Named(_))) =>
383        {
384            format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())")
385        }
386        // Vec<Named>: each element needs .into() to convert core→binding
387        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => {
388            if optional {
389                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
390            } else {
391                format!("{name}: val.{name}.into_iter().map(Into::into).collect()")
392            }
393        }
394        // Optional(Vec<Named>): same but wrapped in Option
395        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_))) =>
396        {
397            format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
398        }
399        // Everything else is symmetric
400        _ => field_conversion_to_core(name, ty, optional),
401    }
402}
403
404/// Core→binding field conversion with backend-specific config.
405pub fn field_conversion_from_core_cfg(
406    name: &str,
407    ty: &TypeRef,
408    optional: bool,
409    sanitized: bool,
410    opaque_types: &AHashSet<String>,
411    config: &ConversionConfig,
412) -> String {
413    // Sanitized fields: for WASM (map_uses_jsvalue), Map and Vec<Json> fields target JsValue
414    // and need serde_wasm_bindgen::to_value() instead of iterator-based .collect().
415    // Note: Vec<String> sanitized does NOT use the JsValue path because Vec<String> maps to
416    // Vec<String> in WASM (not JsValue) — use the normal sanitized iterator path instead.
417    if sanitized {
418        if config.map_uses_jsvalue {
419            // Map(String, String) sanitized → JsValue (HashMap maps to JsValue in WASM)
420            if let TypeRef::Map(k, v) = ty {
421                if matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String) {
422                    if optional {
423                        return format!(
424                            "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())"
425                        );
426                    }
427                    return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
428                }
429            }
430            // Vec<Json> sanitized → JsValue (Vec<Json> maps to JsValue in WASM via nested-vec path)
431            if let TypeRef::Vec(inner) = ty {
432                if matches!(inner.as_ref(), TypeRef::Json) {
433                    if optional {
434                        return format!(
435                            "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())"
436                        );
437                    }
438                    return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
439                }
440            }
441        }
442        return field_conversion_from_core(name, ty, optional, sanitized, opaque_types);
443    }
444
445    // Vec<Named>→String core→binding: binding holds JSON string, core has Vec<Named>.
446    // Only apply serde round-trip for Vec<Named> types (complex structs that can't cross FFI).
447    // Vec<String>, Vec<Primitive>, etc. stay as-is since they map directly.
448    if config.vec_named_to_string {
449        if let TypeRef::Vec(inner) = ty {
450            if matches!(inner.as_ref(), TypeRef::Named(_)) {
451                if optional {
452                    return format!("{name}: val.{name}.as_ref().and_then(|v| serde_json::to_string(v).ok())");
453                }
454                return format!("{name}: serde_json::to_string(&val.{name}).unwrap_or_default()");
455            }
456        }
457    }
458
459    // Map→String core→binding: binding holds Debug-formatted string, core has HashMap.
460    // Used by Rustler (Elixir NIFs) where HashMap cannot cross the NIF boundary directly.
461    if config.map_as_string && matches!(ty, TypeRef::Map(_, _)) {
462        if optional {
463            return format!("{name}: val.{name}.as_ref().map(|m| format!(\"{{m:?}}\"))");
464        }
465        return format!("{name}: format!(\"{{:?}}\", val.{name})");
466    }
467    if config.map_as_string {
468        if let TypeRef::Optional(inner) = ty {
469            if matches!(inner.as_ref(), TypeRef::Map(_, _)) {
470                return format!("{name}: val.{name}.as_ref().map(|m| format!(\"{{m:?}}\"))");
471            }
472        }
473    }
474
475    // WASM JsValue: use serde_wasm_bindgen for Map and nested Vec types
476    if config.map_uses_jsvalue {
477        let is_nested_vec = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Vec(_)));
478        let is_map = matches!(ty, TypeRef::Map(_, _));
479        if is_nested_vec || is_map {
480            if optional {
481                return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())");
482            }
483            return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
484        }
485        if let TypeRef::Optional(inner) = ty {
486            let is_inner_nested = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Vec(_)));
487            let is_inner_map = matches!(inner.as_ref(), TypeRef::Map(_, _));
488            if is_inner_nested || is_inner_map {
489                return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())");
490            }
491        }
492    }
493
494    let prefix = config.type_name_prefix;
495    let is_enum_string = |n: &str| -> bool { config.enum_string_names.as_ref().is_some_and(|names| names.contains(n)) };
496
497    match ty {
498        // i64 casting for large int primitives
499        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
500            let cast_to = binding_prim_str(p);
501            if optional {
502                format!("{name}: val.{name}.map(|v| v as {cast_to})")
503            } else {
504                format!("{name}: val.{name} as {cast_to}")
505            }
506        }
507        // Optional(large_int) with i64 casting
508        TypeRef::Optional(inner)
509            if config.cast_large_ints_to_i64
510                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
511        {
512            if let TypeRef::Primitive(p) = inner.as_ref() {
513                let cast_to = binding_prim_str(p);
514                format!("{name}: val.{name}.map(|v| v as {cast_to})")
515            } else {
516                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
517            }
518        }
519        // f32→f64 casting (NAPI only)
520        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
521            if optional {
522                format!("{name}: val.{name}.map(|v| v as f64)")
523            } else {
524                format!("{name}: val.{name} as f64")
525            }
526        }
527        // Duration with i64 casting
528        TypeRef::Duration if config.cast_large_ints_to_i64 => {
529            if optional {
530                format!("{name}: val.{name}.map(|d| d.as_millis() as u64 as i64)")
531            } else {
532                format!("{name}: val.{name}.as_millis() as u64 as i64")
533            }
534        }
535        // Opaque Named types with prefix: wrap in Arc with prefixed binding name
536        TypeRef::Named(n) if opaque_types.contains(n.as_str()) && !prefix.is_empty() => {
537            let prefixed = format!("{prefix}{n}");
538            if optional {
539                format!("{name}: val.{name}.map(|v| {prefixed} {{ inner: Arc::new(v) }})")
540            } else {
541                format!("{name}: {prefixed} {{ inner: Arc::new(val.{name}) }}")
542            }
543        }
544        // Enum-to-String Named types (PHP pattern)
545        TypeRef::Named(n) if is_enum_string(n) => {
546            // Use serde serialization to get the correct serde(rename) value, not Debug format.
547            // serde_json::to_value gives Value::String("auto") which we extract.
548            if optional {
549                format!(
550                    "{name}: val.{name}.as_ref().map(|v| serde_json::to_value(v).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default())"
551                )
552            } else {
553                format!(
554                    "{name}: serde_json::to_value(val.{name}).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default()"
555                )
556            }
557        }
558        // Vec<Enum-to-String> Named types: element-wise serde serialization
559        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(n) if is_enum_string(n)) => {
560            if optional {
561                format!(
562                    "{name}: val.{name}.as_ref().map(|v| v.iter().map(|x| serde_json::to_value(x).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default()).collect())"
563                )
564            } else {
565                format!(
566                    "{name}: val.{name}.iter().map(|v| serde_json::to_value(v).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default()).collect()"
567                )
568            }
569        }
570        // Optional(Vec<Enum-to-String>) Named types (PHP pattern)
571        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(n) if is_enum_string(n))) =>
572        {
573            format!(
574                "{name}: val.{name}.as_ref().map(|v| v.iter().map(|x| serde_json::to_value(x).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default()).collect())"
575            )
576        }
577        // Vec<f32> needs element-wise cast to f64 when f32→f64 mapping is active
578        TypeRef::Vec(inner)
579            if config.cast_f32_to_f64 && matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32)) =>
580        {
581            if optional {
582                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
583            } else {
584                format!("{name}: val.{name}.iter().map(|&v| v as f64).collect()")
585            }
586        }
587        // Optional(Vec(f32)) needs element-wise cast to f64
588        TypeRef::Optional(inner)
589            if config.cast_f32_to_f64
590                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
591        {
592            format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
593        }
594        // Optional(Vec(u64/usize/isize)) needs element-wise i64 casting
595        TypeRef::Optional(inner)
596            if config.cast_large_ints_to_i64
597                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p))) =>
598        {
599            if let TypeRef::Vec(vi) = inner.as_ref() {
600                if let TypeRef::Primitive(p) = vi.as_ref() {
601                    let cast_to = binding_prim_str(p);
602                    if sanitized {
603                        // Sanitized from Option<(T, T)> → Option<Vec<T>>: destructure tuple
604                        format!("{name}: val.{name}.map(|(a, b)| vec![a as {cast_to}, b as {cast_to}])")
605                    } else {
606                        format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as {cast_to}).collect())")
607                    }
608                } else {
609                    field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
610                }
611            } else {
612                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
613            }
614        }
615        // Vec<Vec<f32>> needs nested element-wise cast to f64 (for embeddings, etc.)
616        TypeRef::Vec(outer)
617            if config.cast_f32_to_f64
618                && matches!(outer.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
619        {
620            if optional {
621                format!(
622                    "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect())"
623                )
624            } else {
625                format!("{name}: val.{name}.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect()")
626            }
627        }
628        // Optional(Vec<Vec<f32>>) needs nested element-wise cast to f64
629        TypeRef::Optional(inner)
630            if config.cast_f32_to_f64
631                && matches!(inner.as_ref(), TypeRef::Vec(outer) if matches!(outer.as_ref(), TypeRef::Vec(prim) if matches!(prim.as_ref(), TypeRef::Primitive(PrimitiveType::F32)))) =>
632        {
633            format!(
634                "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect())"
635            )
636        }
637        // Optional with i64-cast inner
638        TypeRef::Optional(inner)
639            if config.cast_large_ints_to_i64
640                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
641        {
642            if let TypeRef::Primitive(p) = inner.as_ref() {
643                let cast_to = binding_prim_str(p);
644                format!("{name}: val.{name}.map(|v| v as {cast_to})")
645            } else {
646                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
647            }
648        }
649        // HashMap value type casting: when value type needs i64 casting
650        TypeRef::Map(_k, v)
651            if config.cast_large_ints_to_i64 && matches!(v.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
652        {
653            if let TypeRef::Primitive(p) = v.as_ref() {
654                let cast_to = binding_prim_str(p);
655                if optional {
656                    format!(
657                        "{name}: val.{name}.as_ref().map(|m| m.iter().map(|(k, v)| (k.clone(), *v as {cast_to})).collect())"
658                    )
659                } else {
660                    format!("{name}: val.{name}.iter().map(|(k, v)| (k.clone(), *v as {cast_to})).collect()")
661                }
662            } else {
663                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
664            }
665        }
666        // Vec<u64/usize/isize> needs element-wise i64 casting (core→binding)
667        TypeRef::Vec(inner)
668            if config.cast_large_ints_to_i64
669                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
670        {
671            if let TypeRef::Primitive(p) = inner.as_ref() {
672                let cast_to = binding_prim_str(p);
673                if sanitized {
674                    // Sanitized from tuple (T, T) → Vec<T>: destructure tuple into vec
675                    if optional {
676                        format!("{name}: val.{name}.map(|(a, b)| vec![a as {cast_to}, b as {cast_to}])")
677                    } else {
678                        format!("{name}: {{ let (a, b) = val.{name}; vec![a as {cast_to}, b as {cast_to}] }}")
679                    }
680                } else if optional {
681                    format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as {cast_to}).collect())")
682                } else {
683                    format!("{name}: val.{name}.iter().map(|&v| v as {cast_to}).collect()")
684                }
685            } else {
686                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
687            }
688        }
689        // Vec<Vec<u64/usize/isize>> needs nested element-wise i64 casting (core→binding)
690        TypeRef::Vec(outer)
691            if config.cast_large_ints_to_i64
692                && matches!(outer.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p))) =>
693        {
694            if let TypeRef::Vec(inner) = outer.as_ref() {
695                if let TypeRef::Primitive(p) = inner.as_ref() {
696                    let cast_to = binding_prim_str(p);
697                    if optional {
698                        format!(
699                            "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as {cast_to}).collect()).collect())"
700                        )
701                    } else {
702                        format!(
703                            "{name}: val.{name}.iter().map(|inner| inner.iter().map(|&x| x as {cast_to}).collect()).collect()"
704                        )
705                    }
706                } else {
707                    field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
708                }
709            } else {
710                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
711            }
712        }
713        // Json→String: core uses serde_json::Value, binding uses String (PHP)
714        TypeRef::Json if config.json_to_string => {
715            if optional {
716                format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
717            } else {
718                format!("{name}: val.{name}.to_string()")
719            }
720        }
721        // Json→JsValue: core uses serde_json::Value, binding uses JsValue (WASM)
722        TypeRef::Json if config.map_uses_jsvalue => {
723            if optional {
724                format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
725            } else {
726                format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)")
727            }
728        }
729        // Vec<Json>→JsValue: core uses Vec<serde_json::Value>, binding uses JsValue (WASM)
730        TypeRef::Vec(inner) if config.map_uses_jsvalue && matches!(inner.as_ref(), TypeRef::Json) => {
731            if optional {
732                format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
733            } else {
734                format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)")
735            }
736        }
737        // Optional(Vec<Json>)→JsValue (WASM)
738        TypeRef::Optional(inner)
739            if config.map_uses_jsvalue
740                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Json)) =>
741        {
742            format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
743        }
744        // Fall through to default (handles paths, opaque without prefix, etc.)
745        _ => field_conversion_from_core(name, ty, optional, sanitized, opaque_types),
746    }
747}
748
749/// Apply CoreWrapper transformations for core→binding direction.
750/// Unwraps Arc, converts Cow→String, Bytes→Vec<u8>.
751fn apply_core_wrapper_from_core(
752    conversion: &str,
753    name: &str,
754    core_wrapper: &CoreWrapper,
755    vec_inner_core_wrapper: &CoreWrapper,
756    optional: bool,
757) -> String {
758    // Handle Vec<Arc<T>>: unwrap Arc elements
759    if *vec_inner_core_wrapper == CoreWrapper::Arc {
760        return conversion
761            .replace(".map(Into::into).collect()", ".map(|v| (*v).clone().into()).collect()")
762            .replace(
763                "map(|v| v.into_iter().map(Into::into)",
764                "map(|v| v.into_iter().map(|v| (*v).clone().into())",
765            );
766    }
767
768    match core_wrapper {
769        CoreWrapper::None => conversion.to_string(),
770        CoreWrapper::Cow => {
771            // Cow<str> → String: core val.name is Cow<'static, str>, binding needs String.
772            // Always emit val.{name}.into_owned() regardless of what the base conversion emits.
773            // This handles both the normal path (base = "name: val.name") and the sanitized path
774            // (base = "name: format!(\"{:?}\", val.name)") which produces debug-escaped strings.
775            // When the binding has been optionalized (e.g. NAPI default-optional fields), the
776            // upstream pass already wrapped the conversion in Some(...) — preserve that wrap.
777            let prefix = format!("{name}: ");
778            let already_some_wrapped = conversion
779                .strip_prefix(&prefix)
780                .is_some_and(|expr| expr.starts_with("Some("));
781            if optional {
782                format!("{name}: val.{name}.as_ref().map(|v| v.to_string())")
783            } else if already_some_wrapped {
784                format!("{name}: Some(val.{name}.to_string())")
785            } else {
786                format!("{name}: val.{name}.to_string()")
787            }
788        }
789        CoreWrapper::Arc => {
790            // Arc<T> → T: unwrap via clone.
791            //
792            // Special case: opaque Named types build the binding wrapper with
793            // `{ inner: Arc::new(v) }` in the base conversion, but when the core
794            // field is `Arc<T>`, `v` IS already the `Arc<T>` — wrapping it again
795            // with `Arc::new` produces `Arc<Arc<T>>`.  Detect this pattern and
796            // replace `Arc::new(v)` with `v`, and `Arc::new(val.{name})` with
797            // `val.{name}`, then return without adding an extra unwrap chain.
798            if conversion.contains("{ inner: Arc::new(") {
799                return conversion.replace("{ inner: Arc::new(v) }", "{ inner: v }").replace(
800                    &format!("{{ inner: Arc::new(val.{name}) }}"),
801                    &format!("{{ inner: val.{name} }}"),
802                );
803            }
804            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
805                if optional {
806                    format!("{name}: {expr}.map(|v| (*v).clone().into())")
807                } else {
808                    let unwrapped = expr.replace(&format!("val.{name}"), &format!("(*val.{name}).clone()"));
809                    format!("{name}: {unwrapped}")
810                }
811            } else {
812                conversion.to_string()
813            }
814        }
815        CoreWrapper::Bytes => {
816            // Bytes → Vec<u8>: .to_vec()
817            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
818                if optional {
819                    format!("{name}: {expr}.map(|v| v.to_vec())")
820                } else if expr == format!("val.{name}") {
821                    format!("{name}: val.{name}.to_vec()")
822                } else {
823                    conversion.to_string()
824                }
825            } else {
826                conversion.to_string()
827            }
828        }
829        CoreWrapper::ArcMutex => {
830            // Arc<Mutex<T>> → T: lock and clone
831            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
832                if optional {
833                    format!("{name}: {expr}.map(|v| v.lock().unwrap().clone().into())")
834                } else if expr == format!("val.{name}") {
835                    format!("{name}: val.{name}.lock().unwrap().clone().into()")
836                } else {
837                    conversion.to_string()
838                }
839            } else {
840                conversion.to_string()
841            }
842        }
843    }
844}