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