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