Skip to main content

alef_codegen/conversions/
core_to_binding.rs

1use ahash::AHashSet;
2use alef_core::ir::{CoreWrapper, PrimitiveType, TypeDef, TypeRef};
3
4use super::ConversionConfig;
5use super::binding_to_core::field_conversion_to_core;
6use super::helpers::is_newtype;
7use super::helpers::{binding_prim_str, core_type_path_remapped, needs_f64_cast, needs_i32_cast, 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_remapped(typ, core_import, config.source_crate_remaps);
22    let binding_name = format!("{}{}", config.type_name_prefix, typ.name);
23
24    // Newtype structs: extract inner value with val.0
25    if is_newtype(typ) {
26        let field = &typ.fields[0];
27        let newtype_inner_expr = match &field.ty {
28            TypeRef::Named(_) => "val.0.into()".to_string(),
29            TypeRef::Path => "val.0.to_string_lossy().to_string()".to_string(),
30            TypeRef::Duration => "val.0.as_millis() as u64".to_string(),
31            _ => "val.0".to_string(),
32        };
33        return crate::template_env::render(
34            "conversions/core_to_binding_impl",
35            minijinja::context! {
36                core_path => core_path,
37                binding_name => binding_name,
38                is_newtype => true,
39                newtype_inner_expr => newtype_inner_expr,
40                fields => vec![] as Vec<String>,
41            },
42        );
43    }
44
45    let optionalized = config.optionalize_defaults && typ.has_default;
46
47    // Pre-compute all field conversions
48    let mut fields = Vec::new();
49    for field in &typ.fields {
50        // Fields referencing excluded types are not present in the binding struct — skip
51        if !config.exclude_types.is_empty()
52            && super::helpers::field_references_excluded_type(&field.ty, config.exclude_types)
53        {
54            continue;
55        }
56        // When the binding crate strips cfg-gated fields from the struct
57        // (typically because the backend doesn't carry feature gates into the binding
58        // crate's Cargo.toml — e.g. extendr), the From impl cannot assign
59        // <field>: val.<field> because the binding struct has no slot for it.
60        if field.cfg.is_some()
61            && !config.never_skip_cfg_field_names.contains(&field.name)
62            && config.strip_cfg_fields_from_binding_struct
63        {
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        // Opaque Named fields without CoreWrapper::Arc (e.g. visitor: Object<'static>) cannot be
168        // auto-converted via Arc::new — the binding stores a raw host object that needs a bridge.
169        // Emit Default::default() and let the caller (e.g. the convert function) set it separately.
170        let is_opaque_no_wrapper_field = field.core_wrapper == CoreWrapper::None
171            && matches!(&field.ty, TypeRef::Named(n) if config
172                .opaque_types
173                .is_some_and(|opaque| opaque.contains(n.as_str())));
174        // CoreWrapper: unwrap Arc, convert Cow→String, Bytes→Vec<u8>
175        // For sanitized fields, still apply Cow→String conversion: Cow<'_, str> sanitizes to
176        // TypeRef::String and the Debug-formatted fallback produces quotes, but Cow implements
177        // Display so .to_string() (emitted by apply_core_wrapper_from_core for Cow) is correct.
178        // Other sanitized fields (unknown Named types) still fall through to Debug formatting.
179        let conversion = if is_opaque_no_wrapper_field {
180            format!("{}: Default::default()", field.name)
181        } else if !field.sanitized || field.core_wrapper == alef_core::ir::CoreWrapper::Cow {
182            apply_core_wrapper_from_core(
183                &conversion,
184                &field.name,
185                &field.core_wrapper,
186                &field.vec_inner_core_wrapper,
187                field.optional,
188            )
189        } else {
190            conversion
191        };
192        // In core→binding direction, the binding struct field may be keyword-escaped
193        // (e.g. `class_` for `class`). The generated conversion has `field.name: expr`
194        // on the left side — rename it to `binding_name: expr` when needed.
195        let binding_field = config.binding_field_name_owned(&typ.name, &field.name);
196        let conversion = if binding_field != field.name {
197            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
198                format!("{binding_field}: {expr}")
199            } else {
200                conversion
201            }
202        } else {
203            conversion
204        };
205        // Cfg-gated trait-bridge fields (visitor handles wrapped as opaque types like
206        // PyVisitorRef) have no auto-conversion from core — force Default::default() and
207        // let the caller wire the bridge separately.  Other cfg-gated fields with
208        // concretely convertible types (e.g. `metadata: HtmlMetadata`) flow through
209        // the normal `val.field.into()` conversion.
210        let field_is_trait_bridge = field.cfg.is_some() && config.never_skip_cfg_field_names.contains(&field.name) && {
211            let opaque_vec: Vec<String> = opaque_types.iter().cloned().collect();
212            crate::generators::structs::field_references_opaque_type(&field.ty, &opaque_vec)
213        };
214        let conversion = if field_is_trait_bridge {
215            format!("{}: Default::default()", binding_field)
216        } else {
217            conversion
218        };
219        fields.push(conversion);
220    }
221
222    crate::template_env::render(
223        "conversions/core_to_binding_impl",
224        minijinja::context! {
225            core_path => core_path,
226            binding_name => binding_name,
227            is_newtype => false,
228            newtype_inner_expr => "",
229            fields => fields,
230        },
231    )
232}
233
234/// Same but for core -> binding direction.
235/// Some types are asymmetric (PathBuf→String, sanitized fields need .to_string()).
236pub fn field_conversion_from_core(
237    name: &str,
238    ty: &TypeRef,
239    optional: bool,
240    sanitized: bool,
241    opaque_types: &AHashSet<String>,
242) -> String {
243    // Sanitized fields: the binding type differs from core (e.g. Box<str>→String, Cow<str>→String).
244    // Box<str>, Cow<str>, and Arc<str> all implement Display, so use .to_string() not {:?}.
245    // {:?} on string-like types produces debug-escaped output with surrounding quotes.
246    if sanitized {
247        // Vec<Primitive>: sanitized from tuple types like (u32, u32) → Vec<u32>.
248        // Core has a tuple, binding expects Vec — destructure the tuple.
249        if let TypeRef::Vec(inner) = ty {
250            if matches!(inner.as_ref(), TypeRef::Primitive(_)) {
251                if optional {
252                    return format!(
253                        "{name}: val.{name}.map(|t| {{ let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect(); arr }})"
254                    );
255                }
256                return format!("{name}: vec![val.{name}.0 as _, val.{name}.1 as _]");
257            }
258        }
259        // Optional(Vec<Primitive>): sanitized from Option<(T, T)> → Option<Vec<T>>.
260        if let TypeRef::Optional(opt_inner) = ty {
261            if let TypeRef::Vec(vec_inner) = opt_inner.as_ref() {
262                if matches!(vec_inner.as_ref(), TypeRef::Primitive(_)) {
263                    return format!("{name}: val.{name}.map(|t| vec![t.0 as _, t.1 as _])");
264                }
265            }
266        }
267        // Map(String, String): sanitized from Map(Box<str>, Box<str>) etc.
268        if let TypeRef::Map(k, v) = ty {
269            if matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String) {
270                if optional {
271                    return format!(
272                        "{name}: val.{name}.as_ref().map(|m| m.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect())"
273                    );
274                }
275                return format!(
276                    "{name}: val.{name}.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()"
277                );
278            }
279        }
280        // Vec<String>: sanitized from Vec<Box<str>>, Vec<Cow<str>>, Vec<Named>, etc.
281        // Use Debug formatting — the original core type may not implement Display.
282        if let TypeRef::Vec(inner) = ty {
283            if matches!(inner.as_ref(), TypeRef::String) {
284                if optional {
285                    return format!(
286                        "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| format!(\"{{:?}}\", i)).collect())"
287                    );
288                }
289                return format!("{name}: val.{name}.iter().map(|i| format!(\"{{:?}}\", i)).collect()");
290            }
291        }
292        // Optional<Vec<String>>: sanitized from Optional<Vec<Box<str>>>, Optional<Vec<Cow<str>>>, etc.
293        if let TypeRef::Optional(opt_inner) = ty {
294            if let TypeRef::Vec(vec_inner) = opt_inner.as_ref() {
295                if matches!(vec_inner.as_ref(), TypeRef::String) {
296                    return format!(
297                        "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| format!(\"{{:?}}\", i)).collect())"
298                    );
299                }
300            }
301        }
302        // String: sanitized from Box<str>, Cow<str>, (u32, u32), etc.
303        // Use Debug formatting — it works for all types (including tuples) and avoids Display
304        // trait bound failures when the original core type doesn't implement Display.
305        // Note: Cow<str> is handled before this point via the CoreWrapper::Cow path above.
306        if matches!(ty, TypeRef::String) {
307            if optional {
308                return format!("{name}: val.{name}.as_ref().map(|v| format!(\"{{v:?}}\"))");
309            }
310            return format!("{name}: format!(\"{{:?}}\", val.{name})");
311        }
312        // Fallback for truly unknown sanitized types — the core type may not implement Display,
313        // so use Debug formatting which is always available (required by the sanitized field's derive).
314        if optional {
315            return format!("{name}: val.{name}.as_ref().map(|v| format!(\"{{v:?}}\"))");
316        }
317        return format!("{name}: format!(\"{{:?}}\", val.{name})");
318    }
319    match ty {
320        // Duration: core uses std::time::Duration, binding uses u64 (millis)
321        TypeRef::Duration => {
322            if optional {
323                return format!("{name}: val.{name}.map(|d| d.as_millis() as u64)");
324            }
325            format!("{name}: val.{name}.as_millis() as u64")
326        }
327        // Path: core uses PathBuf, binding uses String — PathBuf→String needs special handling
328        TypeRef::Path => {
329            if optional {
330                format!("{name}: val.{name}.map(|p| p.to_string_lossy().to_string())")
331            } else {
332                format!("{name}: val.{name}.to_string_lossy().to_string()")
333            }
334        }
335        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Path) => {
336            format!("{name}: val.{name}.map(|p| p.to_string_lossy().to_string())")
337        }
338        // Char: core uses char, binding uses String — convert char to string
339        TypeRef::Char => {
340            if optional {
341                format!("{name}: val.{name}.map(|c| c.to_string())")
342            } else {
343                format!("{name}: val.{name}.to_string()")
344            }
345        }
346        // Bytes: core uses bytes::Bytes, binding uses Vec<u8> or napi `Buffer`.
347        // `.into()` is a no-op when destination is Vec<u8> (identity From) and
348        // a Vec→Buffer wrap when destination is `napi::bindgen_prelude::Buffer`.
349        TypeRef::Bytes => {
350            if optional {
351                format!("{name}: val.{name}.map(|v| v.to_vec().into())")
352            } else {
353                format!("{name}: val.{name}.to_vec().into()")
354            }
355        }
356        // Opaque Named types: wrap in Arc to create the binding wrapper
357        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
358            if optional {
359                format!("{name}: val.{name}.map(|v| {n} {{ inner: Arc::new(v) }})")
360            } else {
361                format!("{name}: {n} {{ inner: Arc::new(val.{name}) }}")
362            }
363        }
364        // Json: core uses serde_json::Value, binding uses String — use .to_string()
365        TypeRef::Json => {
366            if optional {
367                format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
368            } else {
369                format!("{name}: val.{name}.to_string()")
370            }
371        }
372        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Json) => {
373            format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
374        }
375        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json) => {
376            if optional {
377                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| i.to_string()).collect())")
378            } else {
379                format!("{name}: val.{name}.iter().map(ToString::to_string).collect()")
380            }
381        }
382        // Vec<Optional<Json>>: each element is Option<Value> → Option<String>
383        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Optional(oi) if matches!(oi.as_ref(), TypeRef::Json)) => {
384            if optional {
385                format!(
386                    "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| i.as_ref().map(ToString::to_string)).collect())"
387                )
388            } else {
389                format!("{name}: val.{name}.iter().map(|i| i.as_ref().map(ToString::to_string)).collect()")
390            }
391        }
392        // Map with Json values: core uses HashMap<K, serde_json::Value>, binding uses HashMap<K, String>.
393        // Always emit `k.to_string()` so Cow<'_, str> / Box<str> / Arc<str> keys (which the type
394        // resolver normalizes to TypeRef::String) convert correctly. For an actual `String` key
395        // this is a clone, accepted under the existing `#[allow(clippy::useless_conversion)]`.
396        TypeRef::Map(_k, v) if matches!(v.as_ref(), TypeRef::Json) => {
397            if optional {
398                format!(
399                    "{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect())"
400                )
401            } else {
402                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()")
403            }
404        }
405        // Map with Json keys: core uses HashMap<serde_json::Value, V>, binding uses HashMap<String, V>
406        TypeRef::Map(k, _v) if matches!(k.as_ref(), TypeRef::Json) => {
407            if optional {
408                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.to_string(), v)).collect())")
409            } else {
410                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.to_string(), v)).collect()")
411            }
412        }
413        // Map<String, String>: core may have Box<str> keys/values, binding has String keys/values.
414        // Emit .map() with .into() conversions, which are no-ops when both sides are String.
415        // This handles cases like HashMap<Box<str>, Box<str>> (core) → HashMap<String, String> (binding).
416        TypeRef::Map(k, v) if matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String) => {
417            if optional {
418                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect())")
419            } else {
420                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.into(), v.into())).collect()")
421            }
422        }
423        // Map<K, Bytes>: core uses bytes::Bytes (or Vec<u8>), binding uses Vec<u8> or napi Buffer.
424        // `.to_vec().into()` converts Bytes→Vec<u8> (identity for Vec<u8>) or Bytes→Buffer (napi).
425        TypeRef::Map(_k, v) if matches!(v.as_ref(), TypeRef::Bytes) => {
426            if optional {
427                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v.to_vec().into())).collect())")
428            } else {
429                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k, v.to_vec().into())).collect()")
430            }
431        }
432        // Map<K, Named>: each value needs .into() to convert core→binding
433        TypeRef::Map(_k, v) if matches!(v.as_ref(), TypeRef::Named(_)) => {
434            if optional {
435                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())")
436            } else {
437                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k, v.into())).collect()")
438            }
439        }
440        // Optional(Map<K, Named>): same but wrapped in Option
441        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Map(_k, v) if matches!(v.as_ref(), TypeRef::Named(_))) =>
442        {
443            format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())")
444        }
445        // Vec<Named>: each element needs .into() to convert core→binding
446        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => {
447            if optional {
448                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
449            } else {
450                format!("{name}: val.{name}.into_iter().map(Into::into).collect()")
451            }
452        }
453        // Optional(Vec<Named>): same but wrapped in Option
454        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_))) =>
455        {
456            format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
457        }
458        // Everything else is symmetric
459        _ => field_conversion_to_core(name, ty, optional),
460    }
461}
462
463/// Core→binding field conversion with backend-specific config.
464pub fn field_conversion_from_core_cfg(
465    name: &str,
466    ty: &TypeRef,
467    optional: bool,
468    sanitized: bool,
469    opaque_types: &AHashSet<String>,
470    config: &ConversionConfig,
471) -> String {
472    // Sanitized fields: for WASM (map_uses_jsvalue), Map and Vec<Json> fields target JsValue
473    // and need serde_wasm_bindgen::to_value() instead of iterator-based .collect().
474    // Note: Vec<String> sanitized does NOT use the JsValue path because Vec<String> maps to
475    // Vec<String> in WASM (not JsValue) — use the normal sanitized iterator path instead.
476    if sanitized {
477        if config.map_uses_jsvalue {
478            // Map(String, String) sanitized → JsValue (HashMap maps to JsValue in WASM)
479            // Use js_sys::JSON::parse(json_str) to get a plain JS object (not ES6 Map).
480            if let TypeRef::Map(k, v) = ty {
481                if matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String) {
482                    if optional {
483                        return format!(
484                            "{name}: val.{name}.as_ref().and_then(|v| serde_json::to_string(v).ok()).and_then(|s| js_sys::JSON::parse(&s).ok())"
485                        );
486                    }
487                    return format!(
488                        "{name}: js_sys::JSON::parse(&serde_json::to_string(&val.{name}).unwrap_or_default()).unwrap_or(JsValue::NULL)"
489                    );
490                }
491            }
492            // Vec<Json> sanitized → JsValue (Vec<Json> maps to JsValue in WASM via nested-vec path)
493            if let TypeRef::Vec(inner) = ty {
494                if matches!(inner.as_ref(), TypeRef::Json) {
495                    if optional {
496                        return format!(
497                            "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())"
498                        );
499                    }
500                    return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
501                }
502            }
503        }
504        return field_conversion_from_core(name, ty, optional, sanitized, opaque_types);
505    }
506
507    // Untagged data enum field (core holds the typed enum, binding holds serde_json::Value):
508    // serialize via serde_json::to_value.  Handles direct, Optional, and Vec wrappings.
509    if let Some(untagged_names) = config.untagged_data_enum_names {
510        let direct_named = matches!(ty, TypeRef::Named(n) if untagged_names.contains(n));
511        let optional_named = matches!(ty, TypeRef::Optional(inner)
512            if matches!(inner.as_ref(), TypeRef::Named(n) if untagged_names.contains(n)));
513        let vec_named = matches!(ty, TypeRef::Vec(inner)
514            if matches!(inner.as_ref(), TypeRef::Named(n) if untagged_names.contains(n)));
515        let optional_vec_named = matches!(ty, TypeRef::Optional(outer)
516            if matches!(outer.as_ref(), TypeRef::Vec(inner)
517                if matches!(inner.as_ref(), TypeRef::Named(n) if untagged_names.contains(n))));
518        if direct_named {
519            if optional {
520                return format!("{name}: val.{name}.as_ref().and_then(|v| serde_json::to_value(v).ok())");
521            }
522            return format!("{name}: serde_json::to_value(&val.{name}).unwrap_or(serde_json::Value::Null)");
523        }
524        if optional_named {
525            return format!("{name}: val.{name}.as_ref().and_then(|v| serde_json::to_value(v).ok())");
526        }
527        if vec_named {
528            if optional {
529                return format!(
530                    "{name}: val.{name}.as_ref().map(|v| v.iter().filter_map(|x| serde_json::to_value(x).ok()).collect())"
531                );
532            }
533            return format!("{name}: val.{name}.iter().filter_map(|x| serde_json::to_value(x).ok()).collect()");
534        }
535        if optional_vec_named {
536            return format!(
537                "{name}: val.{name}.as_ref().map(|v| v.iter().filter_map(|x| serde_json::to_value(x).ok()).collect())"
538            );
539        }
540    }
541
542    // Vec<Named>→String core→binding: binding holds JSON string, core has Vec<Named>.
543    // Only apply serde round-trip for Vec<Named> types (complex structs that can't cross FFI).
544    // Vec<String>, Vec<Primitive>, etc. stay as-is since they map directly.
545    if config.vec_named_to_string {
546        if let TypeRef::Vec(inner) = ty {
547            if matches!(inner.as_ref(), TypeRef::Named(_)) {
548                if optional {
549                    return format!("{name}: val.{name}.as_ref().and_then(|v| serde_json::to_string(v).ok())");
550                }
551                return format!("{name}: serde_json::to_string(&val.{name}).unwrap_or_default()");
552            }
553        }
554    }
555
556    // Map→String core→binding: binding holds Debug-formatted string, core has HashMap.
557    // Used by Rustler (Elixir NIFs) where HashMap cannot cross the NIF boundary directly.
558    if config.map_as_string && matches!(ty, TypeRef::Map(_, _)) {
559        if optional {
560            return format!("{name}: val.{name}.as_ref().map(|m| format!(\"{{m:?}}\"))");
561        }
562        return format!("{name}: format!(\"{{:?}}\", val.{name})");
563    }
564    if config.map_as_string {
565        if let TypeRef::Optional(inner) = ty {
566            if matches!(inner.as_ref(), TypeRef::Map(_, _)) {
567                return format!("{name}: val.{name}.as_ref().map(|m| format!(\"{{m:?}}\"))");
568            }
569        }
570    }
571
572    // WASM JsValue: use js_sys::JSON::parse for Map types (produces plain JS objects, not ES6
573    // Maps which serde_wasm_bindgen would produce for serialize_map calls). Use
574    // serde_wasm_bindgen for nested Vec types.
575    if config.map_uses_jsvalue {
576        let is_nested_vec = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Vec(_)));
577        let is_map = matches!(ty, TypeRef::Map(_, _));
578        if is_map {
579            if optional {
580                return format!(
581                    "{name}: val.{name}.as_ref().and_then(|v| serde_json::to_string(v).ok()).and_then(|s| js_sys::JSON::parse(&s).ok())"
582                );
583            }
584            return format!(
585                "{name}: js_sys::JSON::parse(&serde_json::to_string(&val.{name}).unwrap_or_default()).unwrap_or(JsValue::NULL)"
586            );
587        }
588        if is_nested_vec {
589            if optional {
590                return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())");
591            }
592            return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
593        }
594        if let TypeRef::Optional(inner) = ty {
595            let is_inner_nested = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Vec(_)));
596            let is_inner_map = matches!(inner.as_ref(), TypeRef::Map(_, _));
597            if is_inner_map {
598                return format!(
599                    "{name}: val.{name}.as_ref().and_then(|v| serde_json::to_string(v).ok()).and_then(|s| js_sys::JSON::parse(&s).ok())"
600                );
601            }
602            if is_inner_nested {
603                return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())");
604            }
605        }
606    }
607
608    let prefix = config.type_name_prefix;
609    let is_enum_string = |n: &str| -> bool { config.enum_string_names.as_ref().is_some_and(|names| names.contains(n)) };
610
611    match ty {
612        // i64 casting for large int primitives
613        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
614            let cast_to = binding_prim_str(p);
615            if optional {
616                format!("{name}: val.{name}.map(|v| v as {cast_to})")
617            } else {
618                format!("{name}: val.{name} as {cast_to}")
619            }
620        }
621        // Optional(large_int) with i64 casting
622        TypeRef::Optional(inner)
623            if config.cast_large_ints_to_i64
624                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
625        {
626            if let TypeRef::Primitive(p) = inner.as_ref() {
627                let cast_to = binding_prim_str(p);
628                format!("{name}: val.{name}.map(|v| v as {cast_to})")
629            } else {
630                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
631            }
632        }
633        // i32 casting for small uint primitives (extendr/R only)
634        TypeRef::Primitive(p) if config.cast_uints_to_i32 && needs_i32_cast(p) => {
635            if optional {
636                format!("{name}: val.{name}.map(|v| v as i32)")
637            } else {
638                format!("{name}: val.{name} as i32")
639            }
640        }
641        // Optional(small_uint) with i32 casting
642        TypeRef::Optional(inner)
643            if config.cast_uints_to_i32 && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i32_cast(p)) =>
644        {
645            format!("{name}: val.{name}.map(|v| v as i32)")
646        }
647        // Vec<u8/u16/u32/i8/i16> needs element-wise core→i32 casting (extendr/R only)
648        TypeRef::Vec(inner)
649            if config.cast_uints_to_i32 && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i32_cast(p)) =>
650        {
651            if let TypeRef::Primitive(_p) = inner.as_ref() {
652                if optional {
653                    format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as i32).collect())")
654                } else {
655                    format!("{name}: val.{name}.iter().map(|&v| v as i32).collect()")
656                }
657            } else {
658                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
659            }
660        }
661        // f64 casting for large int primitives (extendr/R only)
662        TypeRef::Primitive(p) if config.cast_large_ints_to_f64 && needs_f64_cast(p) => {
663            if optional {
664                format!("{name}: val.{name}.map(|v| v as f64)")
665            } else {
666                format!("{name}: val.{name} as f64")
667            }
668        }
669        // Optional(large_int) with f64 casting
670        TypeRef::Optional(inner)
671            if config.cast_large_ints_to_f64
672                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)) =>
673        {
674            format!("{name}: val.{name}.map(|v| v as f64)")
675        }
676        // Vec<usize/u64/i64/isize/f32> needs element-wise f64 cast for extendr/R backend
677        TypeRef::Vec(inner)
678            if config.cast_large_ints_to_f64
679                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)) =>
680        {
681            if optional {
682                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
683            } else {
684                format!("{name}: val.{name}.iter().map(|&v| v as f64).collect()")
685            }
686        }
687        // Optional(Vec(usize/u64/i64/isize/f32)) needs element-wise f64 cast
688        TypeRef::Optional(inner)
689            if config.cast_large_ints_to_f64
690                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p))) =>
691        {
692            format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
693        }
694        // Vec<Vec<usize/u64/i64/isize/f32>> needs nested element-wise f64 cast (embeddings)
695        TypeRef::Vec(outer)
696            if config.cast_large_ints_to_f64
697                && matches!(outer.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p))) =>
698        {
699            if optional {
700                format!(
701                    "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect())"
702                )
703            } else {
704                format!("{name}: val.{name}.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect()")
705            }
706        }
707        // Optional(Vec<Vec<usize/u64/i64/isize/f32>>) needs nested element-wise f64 cast
708        TypeRef::Optional(inner)
709            if config.cast_large_ints_to_f64
710                && matches!(inner.as_ref(), TypeRef::Vec(outer) if matches!(outer.as_ref(), TypeRef::Vec(prim) if matches!(prim.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)))) =>
711        {
712            format!(
713                "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect())"
714            )
715        }
716        // Map values that are usize/u64/i64/isize/f32 stored as f64 in binding → cast when reading core
717        TypeRef::Map(_k, v)
718            if config.cast_large_ints_to_f64 && matches!(v.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)) =>
719        {
720            if optional {
721                format!("{name}: val.{name}.as_ref().map(|m| m.iter().map(|(k, v)| (k.clone(), *v as f64)).collect())")
722            } else {
723                format!("{name}: val.{name}.iter().map(|(k, v)| (k.clone(), *v as f64)).collect()")
724            }
725        }
726        // Duration with f64 casting (R: no u64, use f64 millis)
727        TypeRef::Duration if config.cast_large_ints_to_f64 => {
728            if optional {
729                format!("{name}: val.{name}.map(|d| d.as_millis() as f64)")
730            } else {
731                format!("{name}: val.{name}.as_millis() as f64")
732            }
733        }
734        // f32→f64 casting (NAPI only)
735        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
736            if optional {
737                format!("{name}: val.{name}.map(|v| v as f64)")
738            } else {
739                format!("{name}: val.{name} as f64")
740            }
741        }
742        // Duration with i64 casting
743        TypeRef::Duration if config.cast_large_ints_to_i64 => {
744            if optional {
745                format!("{name}: val.{name}.map(|d| d.as_millis() as u64 as i64)")
746            } else {
747                format!("{name}: val.{name}.as_millis() as u64 as i64")
748            }
749        }
750        // Opaque Named types with prefix: wrap in Arc with prefixed binding name
751        TypeRef::Named(n) if opaque_types.contains(n.as_str()) && !prefix.is_empty() => {
752            let prefixed = format!("{prefix}{n}");
753            if optional {
754                format!("{name}: val.{name}.map(|v| {prefixed} {{ inner: Arc::new(v) }})")
755            } else {
756                format!("{name}: {prefixed} {{ inner: Arc::new(val.{name}) }}")
757            }
758        }
759        // Enum-to-String Named types (PHP pattern)
760        TypeRef::Named(n) if is_enum_string(n) => {
761            // Use serde serialization to get the correct serde(rename) value, not Debug format.
762            // serde_json::to_value gives Value::String("auto") which we extract.
763            if optional {
764                format!(
765                    "{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())"
766                )
767            } else {
768                format!(
769                    "{name}: serde_json::to_value(val.{name}).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default()"
770                )
771            }
772        }
773        // Vec<Enum-to-String> Named types: element-wise serde serialization
774        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(n) if is_enum_string(n)) => {
775            if optional {
776                format!(
777                    "{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())"
778                )
779            } else {
780                format!(
781                    "{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()"
782                )
783            }
784        }
785        // Optional(Vec<Enum-to-String>) Named types (PHP pattern)
786        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(n) if is_enum_string(n))) =>
787        {
788            format!(
789                "{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())"
790            )
791        }
792        // Vec<f32> needs element-wise cast to f64 when f32→f64 mapping is active
793        TypeRef::Vec(inner)
794            if config.cast_f32_to_f64 && matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32)) =>
795        {
796            if optional {
797                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
798            } else {
799                format!("{name}: val.{name}.iter().map(|&v| v as f64).collect()")
800            }
801        }
802        // Optional(Vec(f32)) needs element-wise cast to f64
803        TypeRef::Optional(inner)
804            if config.cast_f32_to_f64
805                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
806        {
807            format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
808        }
809        // Optional(Vec(u64/usize/isize)) needs element-wise i64 casting
810        TypeRef::Optional(inner)
811            if config.cast_large_ints_to_i64
812                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p))) =>
813        {
814            if let TypeRef::Vec(vi) = inner.as_ref() {
815                if let TypeRef::Primitive(p) = vi.as_ref() {
816                    let cast_to = binding_prim_str(p);
817                    if sanitized {
818                        // Sanitized from Option<(T, T)> → Option<Vec<T>>: destructure tuple
819                        format!("{name}: val.{name}.map(|(a, b)| vec![a as {cast_to}, b as {cast_to}])")
820                    } else {
821                        format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as {cast_to}).collect())")
822                    }
823                } else {
824                    field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
825                }
826            } else {
827                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
828            }
829        }
830        // Vec<Vec<f32>> needs nested element-wise cast to f64 (for embeddings, etc.)
831        TypeRef::Vec(outer)
832            if config.cast_f32_to_f64
833                && matches!(outer.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
834        {
835            if optional {
836                format!(
837                    "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect())"
838                )
839            } else {
840                format!("{name}: val.{name}.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect()")
841            }
842        }
843        // Optional(Vec<Vec<f32>>) needs nested element-wise cast to f64
844        TypeRef::Optional(inner)
845            if config.cast_f32_to_f64
846                && matches!(inner.as_ref(), TypeRef::Vec(outer) if matches!(outer.as_ref(), TypeRef::Vec(prim) if matches!(prim.as_ref(), TypeRef::Primitive(PrimitiveType::F32)))) =>
847        {
848            format!(
849                "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect())"
850            )
851        }
852        // Optional with i64-cast inner
853        TypeRef::Optional(inner)
854            if config.cast_large_ints_to_i64
855                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
856        {
857            if let TypeRef::Primitive(p) = inner.as_ref() {
858                let cast_to = binding_prim_str(p);
859                format!("{name}: val.{name}.map(|v| v as {cast_to})")
860            } else {
861                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
862            }
863        }
864        // HashMap value type casting: when value type needs i64 casting
865        TypeRef::Map(_k, v)
866            if config.cast_large_ints_to_i64 && matches!(v.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
867        {
868            if let TypeRef::Primitive(p) = v.as_ref() {
869                let cast_to = binding_prim_str(p);
870                if optional {
871                    format!(
872                        "{name}: val.{name}.as_ref().map(|m| m.iter().map(|(k, v)| (k.clone(), *v as {cast_to})).collect())"
873                    )
874                } else {
875                    format!("{name}: val.{name}.iter().map(|(k, v)| (k.clone(), *v as {cast_to})).collect()")
876                }
877            } else {
878                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
879            }
880        }
881        // Vec<u64/usize/isize> needs element-wise i64 casting (core→binding)
882        TypeRef::Vec(inner)
883            if config.cast_large_ints_to_i64
884                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
885        {
886            if let TypeRef::Primitive(p) = inner.as_ref() {
887                let cast_to = binding_prim_str(p);
888                if sanitized {
889                    // Sanitized from tuple (T, T) → Vec<T>: destructure tuple into vec
890                    if optional {
891                        format!("{name}: val.{name}.map(|(a, b)| vec![a as {cast_to}, b as {cast_to}])")
892                    } else {
893                        format!("{name}: {{ let (a, b) = val.{name}; vec![a as {cast_to}, b as {cast_to}] }}")
894                    }
895                } else if optional {
896                    format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as {cast_to}).collect())")
897                } else {
898                    format!("{name}: val.{name}.iter().map(|&v| v as {cast_to}).collect()")
899                }
900            } else {
901                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
902            }
903        }
904        // Vec<Vec<u64/usize/isize>> needs nested element-wise i64 casting (core→binding)
905        TypeRef::Vec(outer)
906            if config.cast_large_ints_to_i64
907                && matches!(outer.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p))) =>
908        {
909            if let TypeRef::Vec(inner) = outer.as_ref() {
910                if let TypeRef::Primitive(p) = inner.as_ref() {
911                    let cast_to = binding_prim_str(p);
912                    if optional {
913                        format!(
914                            "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as {cast_to}).collect()).collect())"
915                        )
916                    } else {
917                        format!(
918                            "{name}: val.{name}.iter().map(|inner| inner.iter().map(|&x| x as {cast_to}).collect()).collect()"
919                        )
920                    }
921                } else {
922                    field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
923                }
924            } else {
925                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
926            }
927        }
928        // Json→String: core uses serde_json::Value, binding uses String (PHP)
929        TypeRef::Json if config.json_to_string => {
930            if optional {
931                format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
932            } else {
933                format!("{name}: val.{name}.to_string()")
934            }
935        }
936        // Json stays as serde_json::Value: identity passthrough.
937        TypeRef::Json if config.json_as_value => {
938            format!("{name}: val.{name}")
939        }
940        TypeRef::Optional(inner) if config.json_as_value && matches!(inner.as_ref(), TypeRef::Json) => {
941            format!("{name}: val.{name}")
942        }
943        TypeRef::Vec(inner) if config.json_as_value && matches!(inner.as_ref(), TypeRef::Json) => {
944            if optional {
945                format!("{name}: Some(val.{name})")
946            } else {
947                format!("{name}: val.{name}")
948            }
949        }
950        TypeRef::Map(_k, v) if config.json_as_value && matches!(v.as_ref(), TypeRef::Json) => {
951            if optional {
952                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.into(), v)).collect())")
953            } else {
954                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.into(), v)).collect()")
955            }
956        }
957        // Json→JsValue: core uses serde_json::Value, binding uses JsValue (WASM)
958        TypeRef::Json if config.map_uses_jsvalue => {
959            if optional {
960                format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
961            } else {
962                format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)")
963            }
964        }
965        // Vec<Json>→JsValue: core uses Vec<serde_json::Value>, binding uses JsValue (WASM)
966        TypeRef::Vec(inner) if config.map_uses_jsvalue && matches!(inner.as_ref(), TypeRef::Json) => {
967            if optional {
968                format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
969            } else {
970                format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)")
971            }
972        }
973        // Optional(Vec<Json>)→JsValue (WASM)
974        TypeRef::Optional(inner)
975            if config.map_uses_jsvalue
976                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Json)) =>
977        {
978            format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
979        }
980        // Fall through to default (handles paths, opaque without prefix, etc.)
981        _ => field_conversion_from_core(name, ty, optional, sanitized, opaque_types),
982    }
983}
984
985/// Apply CoreWrapper transformations for core→binding direction.
986/// Unwraps Arc, converts Cow→String, Bytes→Vec<u8>.
987fn apply_core_wrapper_from_core(
988    conversion: &str,
989    name: &str,
990    core_wrapper: &CoreWrapper,
991    vec_inner_core_wrapper: &CoreWrapper,
992    optional: bool,
993) -> String {
994    // Handle Vec<Arc<T>>: unwrap Arc elements
995    if *vec_inner_core_wrapper == CoreWrapper::Arc {
996        return conversion
997            .replace(".map(Into::into).collect()", ".map(|v| (*v).clone().into()).collect()")
998            .replace(
999                "map(|v| v.into_iter().map(Into::into)",
1000                "map(|v| v.into_iter().map(|v| (*v).clone().into())",
1001            );
1002    }
1003
1004    match core_wrapper {
1005        CoreWrapper::None => conversion.to_string(),
1006        CoreWrapper::Cow => {
1007            // Cow<str> → String: core val.name is Cow<'static, str>, binding needs String.
1008            // Always emit val.{name}.into_owned() regardless of what the base conversion emits.
1009            // This handles both the normal path (base = "name: val.name") and the sanitized path
1010            // (base = "name: format!(\"{:?}\", val.name)") which produces debug-escaped strings.
1011            // When the binding has been optionalized (e.g. NAPI default-optional fields), the
1012            // upstream pass already wrapped the conversion in Some(...) — preserve that wrap.
1013            let prefix = format!("{name}: ");
1014            let already_some_wrapped = conversion
1015                .strip_prefix(&prefix)
1016                .is_some_and(|expr| expr.starts_with("Some("));
1017            if optional {
1018                format!("{name}: val.{name}.as_ref().map(|v| v.to_string())")
1019            } else if already_some_wrapped {
1020                format!("{name}: Some(val.{name}.to_string())")
1021            } else {
1022                format!("{name}: val.{name}.to_string()")
1023            }
1024        }
1025        CoreWrapper::Arc => {
1026            // Arc<T> → T: unwrap via clone.
1027            //
1028            // Special case: opaque Named types build the binding wrapper with
1029            // `{ inner: Arc::new(v) }` in the base conversion, but when the core
1030            // field is `Arc<T>`, `v` IS already the `Arc<T>` — wrapping it again
1031            // with `Arc::new` produces `Arc<Arc<T>>`.  Detect this pattern and
1032            // replace `Arc::new(v)` with `v`, and `Arc::new(val.{name})` with
1033            // `val.{name}`, then return without adding an extra unwrap chain.
1034            if conversion.contains("{ inner: Arc::new(") {
1035                return conversion.replace("{ inner: Arc::new(v) }", "{ inner: v }").replace(
1036                    &format!("{{ inner: Arc::new(val.{name}) }}"),
1037                    &format!("{{ inner: val.{name} }}"),
1038                );
1039            }
1040            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
1041                if optional {
1042                    // When the base conversion is the simple passthrough `val.{name}`,
1043                    // the Option carries Arc<T> elements; deref-clone each.
1044                    // When the base is already a complex expression (e.g.
1045                    // `val.{name}.as_ref().map(ToString::to_string)` for Json fields),
1046                    // the Arc is transparently handled via Display/Deref coercion;
1047                    // chaining another `.map(|v| (*v).clone().into())` would operate
1048                    // on the already-converted value (e.g. String) and emit invalid
1049                    // codegen such as `(*String).clone()` (since str: !Clone).
1050                    let simple_passthrough = format!("val.{name}");
1051                    if expr == simple_passthrough {
1052                        format!("{name}: {expr}.map(|v| (*v).clone().into())")
1053                    } else {
1054                        format!("{name}: {expr}")
1055                    }
1056                } else {
1057                    let unwrapped = expr.replace(&format!("val.{name}"), &format!("(*val.{name}).clone()"));
1058                    format!("{name}: {unwrapped}")
1059                }
1060            } else {
1061                conversion.to_string()
1062            }
1063        }
1064        CoreWrapper::Bytes => {
1065            // Bytes → Vec<u8> (or napi Buffer via From<Vec<u8>>): .to_vec().into()
1066            // The TypeRef::Bytes field_conversion already emits the correct expression
1067            // (`.to_vec().into()` non-optional, `.map(|v| v.to_vec().into())` optional).
1068            // Detect those forms and pass through unchanged to avoid double conversion.
1069            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
1070                let already_converted_non_opt = expr == format!("val.{name}.to_vec().into()");
1071                let already_converted_opt = expr == format!("val.{name}.map(|v| v.to_vec().into())");
1072                if already_converted_non_opt || already_converted_opt {
1073                    conversion.to_string()
1074                } else if optional {
1075                    format!("{name}: {expr}.map(|v| v.to_vec().into())")
1076                } else if expr == format!("val.{name}") {
1077                    format!("{name}: val.{name}.to_vec().into()")
1078                } else {
1079                    conversion.to_string()
1080                }
1081            } else {
1082                conversion.to_string()
1083            }
1084        }
1085        CoreWrapper::ArcMutex => {
1086            // Arc<Mutex<T>> → T: lock and clone
1087            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
1088                if optional {
1089                    format!("{name}: {expr}.map(|v| v.lock().unwrap().clone().into())")
1090                } else if expr == format!("val.{name}") {
1091                    format!("{name}: val.{name}.lock().unwrap().clone().into()")
1092                } else {
1093                    conversion.to_string()
1094                }
1095            } else {
1096                conversion.to_string()
1097            }
1098        }
1099    }
1100}