Skip to main content

alef_codegen/conversions/
binding_to_core.rs

1use alef_core::ir::{CoreWrapper, PrimitiveType, TypeDef, TypeRef};
2
3use super::ConversionConfig;
4use super::helpers::{
5    core_prim_str, core_type_path_remapped, is_newtype, is_tuple_type_name, needs_f64_cast, needs_i32_cast,
6    needs_i64_cast,
7};
8
9/// Generate `impl From<BindingType> for core::Type` (binding -> core).
10/// Sanitized fields use `Default::default()` unless the sanitizer only removed a
11/// core wrapper that can be reconstructed losslessly from the binding value.
12pub fn gen_from_binding_to_core(typ: &TypeDef, core_import: &str) -> String {
13    gen_from_binding_to_core_cfg(typ, core_import, &ConversionConfig::default())
14}
15
16/// Generate `impl From<BindingType> for core::Type` with backend-specific config.
17pub fn gen_from_binding_to_core_cfg(typ: &TypeDef, core_import: &str, config: &ConversionConfig) -> String {
18    let core_path = core_type_path_remapped(typ, core_import, config.source_crate_remaps);
19    let binding_name = format!("{}{}", config.type_name_prefix, typ.name);
20
21    // Newtype structs: generate tuple constructor Self(val._0)
22    if is_newtype(typ) {
23        let field = &typ.fields[0];
24        let newtype_inner_expr = match &field.ty {
25            TypeRef::Named(_) => "val._0.into()".to_string(),
26            TypeRef::Path => "val._0.into()".to_string(),
27            TypeRef::Duration => "std::time::Duration::from_millis(val._0)".to_string(),
28            _ => "val._0".to_string(),
29        };
30        return crate::template_env::render(
31            "conversions/binding_to_core_impl",
32            minijinja::context! {
33                core_path => core_path,
34                binding_name => binding_name,
35                is_newtype => true,
36                newtype_inner_expr => newtype_inner_expr,
37                builder_mode => false,
38                uses_builder_pattern => false,
39                has_stripped_cfg_fields => typ.has_stripped_cfg_fields,
40                statements => vec![] as Vec<String>,
41                fields => vec![] as Vec<String>,
42            },
43        );
44    }
45
46    // Determine if we're using the builder pattern
47    let uses_builder_pattern = (config.option_duration_on_defaults
48        && typ.has_default
49        && typ
50            .fields
51            .iter()
52            .any(|f| !f.optional && matches!(f.ty, TypeRef::Duration)))
53        || (config.optionalize_defaults && typ.has_default);
54
55    // When option_duration_on_defaults is set for a has_default type, non-optional Duration
56    // fields are stored as Option<u64> in the binding struct.  We use the builder pattern
57    // so that None falls back to the core type's Default (giving the real field default,
58    // e.g. Duration::from_millis(30000)) rather than Duration::ZERO.
59    let has_optionalized_duration = config.option_duration_on_defaults
60        && typ.has_default
61        && typ
62            .fields
63            .iter()
64            .any(|f| !f.optional && matches!(f.ty, TypeRef::Duration));
65
66    if has_optionalized_duration {
67        // Builder pattern: start from core default, override explicitly-set fields.
68        let optionalized = config.optionalize_defaults && typ.has_default;
69        let mut statements = Vec::new();
70        for field in &typ.fields {
71            if field.binding_excluded {
72                continue;
73            }
74            if field.sanitized && field.core_wrapper != CoreWrapper::Cow {
75                // sanitized fields keep the default value — skip
76                continue;
77            }
78            // Fields referencing excluded types keep their default value — skip
79            if !config.exclude_types.is_empty()
80                && super::helpers::field_references_excluded_type(&field.ty, config.exclude_types)
81            {
82                continue;
83            }
84            // Duration field stored as Option<u64/i64>: only override when Some
85            let binding_name_field = config.binding_field_name_owned(&typ.name, &field.name);
86            if !field.optional && matches!(field.ty, TypeRef::Duration) {
87                let cast = if config.cast_large_ints_to_i64 { " as u64" } else { "" };
88                statements.push(format!(
89                    "if let Some(__v) = val.{binding_name_field} {{ __result.{} = std::time::Duration::from_millis(__v{cast}); }}",
90                    field.name
91                ));
92                continue;
93            }
94            let conversion = if optionalized && !field.optional {
95                // Field was Option-wrapped in the binding for ergonomics; core expects T.
96                // Use unwrap_or_default to peel the binding-side Option.
97                gen_optionalized_field_to_core(&field.name, &field.ty, config, false)
98            } else {
99                // Genuinely-optional IR field (binding: Option<T>, core: Option<T>) or required
100                // field (`!field.optional` with optionalize_defaults=false). Both cases are
101                // handled correctly by `field_conversion_to_core_cfg`. Routing genuinely-optional
102                // fields through `gen_optionalized_field_to_core` would emit `.unwrap_or_default()`
103                // for primitives/String/Path/Duration and break the `Option<T>` destination.
104                field_conversion_to_core_cfg(&field.name, &field.ty, field.optional, config)
105            };
106            // Apply binding field name substitution for keyword-escaped fields.
107            let conversion = if binding_name_field != field.name {
108                conversion.replace(&format!("val.{}", field.name), &format!("val.{binding_name_field}"))
109            } else {
110                conversion
111            };
112            // Strip the "name: " prefix to get just the expression, then assign
113            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
114                statements.push(format!("__result.{} = {};", field.name, expr));
115            }
116        }
117
118        return crate::template_env::render(
119            "conversions/binding_to_core_impl",
120            minijinja::context! {
121                core_path => core_path,
122                binding_name => binding_name,
123                is_newtype => false,
124                newtype_inner_expr => "",
125                builder_mode => true,
126                uses_builder_pattern => uses_builder_pattern,
127                has_stripped_cfg_fields => typ.has_stripped_cfg_fields,
128                statements => statements,
129                fields => vec![] as Vec<String>,
130            },
131        );
132    }
133
134    let optionalized = config.optionalize_defaults && typ.has_default;
135
136    // Pre-compute all fields
137    let mut fields = Vec::new();
138    let mut statements = Vec::new();
139
140    for field in &typ.fields {
141        if field.binding_excluded {
142            if field.cfg.is_some()
143                && !config.never_skip_cfg_field_names.contains(&field.name)
144                && (typ.has_stripped_cfg_fields || config.strip_cfg_fields_from_binding_struct)
145            {
146                continue;
147            }
148            fields.push(format!("{}: Default::default()", field.name));
149            continue;
150        }
151        // Cfg-gated fields: emit the assignment with `#[cfg(...)]` so it only applies when
152        // the same feature is enabled on the binding crate. Force-restored (never_skip) fields
153        // skip the gate — they're always emitted (used by trait-bridge bind_via = "options_field").
154        // Pre-stripped types still have the field in IR; we just don't emit the cfg gate here
155        // since the binding struct definition has already been gated.
156        // Fields referencing excluded types don't exist in the binding struct.
157        // When the type has stripped cfg-gated fields, these fields may also be
158        // cfg-gated and absent from the core struct — skip them entirely and let
159        // ..Default::default() fill them in.
160        // Otherwise, use Default::default() to fill them in the core type.
161        // Sanitized fields also use Default::default() (lossy but functional).
162        let references_excluded = !config.exclude_types.is_empty()
163            && super::helpers::field_references_excluded_type(&field.ty, config.exclude_types);
164        if references_excluded && typ.has_stripped_cfg_fields {
165            continue;
166        }
167        // When the binding crate strips cfg-gated fields from the struct
168        // (typically because the backend doesn't carry feature gates into the binding
169        // crate's Cargo.toml — e.g. extendr), the From impl cannot reference
170        // val.<field> because the field doesn't exist in the binding struct.
171        // Skip these entirely; ..Default::default() in the template handles them.
172        if field.cfg.is_some()
173            && !config.never_skip_cfg_field_names.contains(&field.name)
174            && config.strip_cfg_fields_from_binding_struct
175        {
176            continue;
177        }
178        if optionalized && ((field.sanitized && field.core_wrapper != CoreWrapper::Cow) || references_excluded) {
179            continue;
180        }
181        let field_was_optionalized = optionalized && !field.optional;
182        let conversion = if (field.sanitized && field.core_wrapper != CoreWrapper::Cow) || references_excluded {
183            format!("{}: Default::default()", field.name)
184        } else if field_was_optionalized {
185            // Field was wrapped in Option<T> for JS ergonomics but core expects T.
186            // Convert the supplied value as T; omitted fields keep the core type's Default value.
187            field_conversion_to_core_cfg(&field.name, &field.ty, false, config)
188        } else {
189            field_conversion_to_core_cfg(&field.name, &field.ty, field.optional, config)
190        };
191        // Newtype wrapping: when the field was resolved from a newtype (e.g. NodeIndex → u32),
192        // wrap the binding value back into the newtype for the core struct.
193        // e.g. `source: val.source` → `source: kreuzberg::NodeIndex(val.source)`
194        //      `parent: val.parent` → `parent: val.parent.map(kreuzberg::NodeIndex)`
195        //      `children: val.children` → `children: val.children.into_iter().map(kreuzberg::NodeIndex).collect()`
196        let conversion = if let Some(newtype_path) = &field.newtype_wrapper {
197            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
198                // When `optional=true` and `ty` is a plain Primitive (not TypeRef::Optional), the core
199                // field is actually `Option<NewtypeT>`, so we must use `.map(NewtypeT)` not `NewtypeT(...)`.
200                match &field.ty {
201                    TypeRef::Optional(_) => format!("{}: ({expr}).map({newtype_path})", field.name),
202                    TypeRef::Vec(_) => {
203                        // When the inner expr already ends with .collect() (e.g. because of a
204                        // primitive cast), the compiler cannot infer the intermediate Vec type
205                        // without an explicit type annotation. Use collect::<Vec<_>>() to make
206                        // the intermediate collection type unambiguous before mapping to newtype.
207                        let inner_expr = if let Some(prefix) = expr.strip_suffix(".collect()") {
208                            format!("{prefix}.collect::<Vec<_>>()")
209                        } else {
210                            expr.to_string()
211                        };
212                        format!(
213                            "{}: ({inner_expr}).into_iter().map({newtype_path}).collect()",
214                            field.name
215                        )
216                    }
217                    _ if field.optional => format!("{}: ({expr}).map({newtype_path})", field.name),
218                    _ => format!("{}: {newtype_path}({expr})", field.name),
219                }
220            } else {
221                conversion
222            }
223        } else {
224            conversion
225        };
226        // Box<T> fields: wrap the converted value in Box::new()
227        let conversion = if field.is_boxed && matches!(&field.ty, TypeRef::Named(_)) {
228            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
229                if field.optional {
230                    // Option<Box<T>> field: map inside the Option
231                    format!("{}: {}.map(Box::new)", field.name, expr)
232                } else {
233                    format!("{}: Box::new({})", field.name, expr)
234                }
235            } else {
236                conversion
237            }
238        } else {
239            conversion
240        };
241        // CoreWrapper: apply Cow/Arc/Bytes wrapping for binding→core direction.
242        //
243        // Special case: opaque Named field with CoreWrapper::Arc.
244        // The binding wrapper already holds `inner: Arc<CoreT>`, so the correct
245        // conversion is to extract `.inner` directly rather than calling `.into()`
246        // (which requires `From<BindingType> for CoreT`, a non-existent impl) and
247        // then wrapping in `Arc::new` (which would double-wrap the Arc).
248        let is_opaque_arc_field = field.core_wrapper == CoreWrapper::Arc
249            && matches!(&field.ty, TypeRef::Named(n) if config
250                .opaque_types
251                .is_some_and(|opaque| opaque.contains(n.as_str())));
252        // Opaque Named fields without CoreWrapper::Arc (e.g. visitor: Object<'static>) cannot be
253        // auto-converted via Into — the binding stores a raw JS object that needs a bridge.
254        // Emit Default::default() and let the caller (e.g. the convert function) set it separately.
255        let is_opaque_no_wrapper_field = field.core_wrapper == CoreWrapper::None
256            && matches!(&field.ty, TypeRef::Named(n) if config
257                .opaque_types
258                .is_some_and(|opaque| opaque.contains(n.as_str())));
259        let conversion = if is_opaque_arc_field {
260            if field.optional {
261                format!("{}: val.{}.map(|v| v.inner)", field.name, field.name)
262            } else {
263                format!("{}: val.{}.inner", field.name, field.name)
264            }
265        } else if is_opaque_no_wrapper_field {
266            // Trait-bridge OptionsField fields: the binding wrapper holds `inner: Arc<core::T>`.
267            // Clone out of the Arc so the visitor (or other bridge handle) is forwarded instead
268            // of silently dropped. Fall back to Default::default() when no Arc wrapper is present.
269            if config.trait_bridge_field_is_arc_wrapper(&field.name) {
270                if field.optional {
271                    format!("{}: val.{}.map(|v| (*v.inner).clone())", field.name, field.name)
272                } else {
273                    format!("{}: (*val.{}.inner).clone()", field.name, field.name)
274                }
275            } else {
276                format!("{}: Default::default()", field.name)
277            }
278        } else {
279            apply_core_wrapper_to_core(
280                &conversion,
281                &field.name,
282                &field.core_wrapper,
283                &field.vec_inner_core_wrapper,
284                field.optional,
285            )
286        };
287        // When the binding struct uses a keyword-escaped field name (e.g. `class_` for `class`),
288        // replace `val.{field.name}` access patterns in the conversion expression with
289        // `val.{binding_name}` so the generated From impl compiles.
290        let binding_name_field = config.binding_field_name_owned(&typ.name, &field.name);
291        let conversion = if binding_name_field != field.name {
292            conversion.replace(&format!("val.{}", field.name), &format!("val.{binding_name_field}"))
293        } else {
294            conversion
295        };
296        if optionalized {
297            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
298                if field_was_optionalized {
299                    statements.push(format!(
300                        "if let Some(__v) = val.{binding_name_field} {{ __result.{} = {}; }}",
301                        field.name,
302                        expr.replace(&format!("val.{binding_name_field}"), "__v")
303                    ));
304                } else {
305                    statements.push(format!("__result.{} = {};", field.name, expr));
306                }
307            }
308        } else {
309            fields.push(conversion);
310        }
311    }
312
313    // Note: ..Default::default() for cfg-gated fields is emitted by the template
314    // via the has_stripped_cfg_fields context variable — do not push it here.
315
316    crate::template_env::render(
317        "conversions/binding_to_core_impl",
318        minijinja::context! {
319            core_path => core_path,
320            binding_name => binding_name,
321            is_newtype => false,
322            newtype_inner_expr => "",
323            builder_mode => optionalized,
324            uses_builder_pattern => uses_builder_pattern,
325            has_stripped_cfg_fields => typ.has_stripped_cfg_fields,
326            statements => statements,
327            fields => fields,
328        },
329    )
330}
331
332/// Generate field conversion for a field that was optionalized (wrapped in `Option<T>`) in the
333/// binding struct for JS ergonomics (`optionalize_defaults`). When `field_is_ir_optional` is
334/// `true`, the field is genuinely `Option<T>` in the IR and the `Option` layer must be preserved
335/// in the output expression (use `.map(|m| …)` rather than `unwrap_or_default()`).
336pub(super) fn gen_optionalized_field_to_core(
337    name: &str,
338    ty: &TypeRef,
339    config: &ConversionConfig,
340    field_is_ir_optional: bool,
341) -> String {
342    match ty {
343        TypeRef::Json if config.json_as_value => {
344            format!("{name}: val.{name}.unwrap_or_default()")
345        }
346        TypeRef::Json => {
347            format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or_default()")
348        }
349        TypeRef::Named(_) => {
350            // Named type: unwrap Option, convert via .into(), or use Default
351            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
352        }
353        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
354            format!("{name}: val.{name}.map(|v| v as f32).unwrap_or(0.0)")
355        }
356        TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => {
357            format!("{name}: val.{name}.unwrap_or(0.0)")
358        }
359        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
360            let core_ty = core_prim_str(p);
361            format!("{name}: val.{name}.map(|v| v as {core_ty}).unwrap_or_default()")
362        }
363        TypeRef::Optional(inner)
364            if config.cast_large_ints_to_i64
365                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
366        {
367            if let TypeRef::Primitive(p) = inner.as_ref() {
368                let core_ty = core_prim_str(p);
369                format!("{name}: val.{name}.map(|v| v as {core_ty})")
370            } else {
371                field_conversion_to_core(name, ty, false)
372            }
373        }
374        TypeRef::Duration if config.cast_large_ints_to_i64 => {
375            format!("{name}: val.{name}.map(|v| std::time::Duration::from_millis(v as u64)).unwrap_or_default()")
376        }
377        TypeRef::Duration => {
378            format!("{name}: val.{name}.map(std::time::Duration::from_millis).unwrap_or_default()")
379        }
380        TypeRef::Path => {
381            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
382        }
383        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Path) => {
384            // Binding has Option<String>, core has Option<PathBuf>
385            format!("{name}: val.{name}.map(|s| std::path::PathBuf::from(s))")
386        }
387        TypeRef::Optional(_) => {
388            // Field was flattened from Option<Option<T>> to Option<T> in the binding struct.
389            // Core expects Option<Option<T>>, so wrap with .map(Some) to reconstruct.
390            format!("{name}: val.{name}.map(Some)")
391        }
392        // Char: binding uses Option<String>, core uses char
393        TypeRef::Char => {
394            format!("{name}: val.{name}.and_then(|s| s.chars().next()).unwrap_or('*')")
395        }
396        TypeRef::Vec(inner) => match inner.as_ref() {
397            TypeRef::Json => {
398                format!(
399                    "{name}: val.{name}.map(|v| v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()).unwrap_or_default()"
400                )
401            }
402            TypeRef::Named(_) => {
403                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect()).unwrap_or_default()")
404            }
405            TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
406                let core_ty = core_prim_str(p);
407                format!(
408                    "{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect()).unwrap_or_default()"
409                )
410            }
411            _ => format!("{name}: val.{name}.unwrap_or_default()"),
412        },
413        TypeRef::Map(k, v) if matches!(v.as_ref(), TypeRef::Json) => {
414            // Map with Json values: binding uses HashMap<K, String>, core uses HashMap<K, serde_json::Value>.
415            // Use `k.into()` for non-Json keys so String→String is a no-op while still converting
416            // String→Cow<'_, str>/Box<str>/Arc<str> when the core type uses one of those wrappers.
417            let k_is_json = matches!(k.as_ref(), TypeRef::Json);
418            let k_expr = if k_is_json {
419                "serde_json::from_str(&k).unwrap_or_default()"
420            } else {
421                "k.into()"
422            };
423            format!(
424                "{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| ({k_expr}, serde_json::from_str(&v).unwrap_or(serde_json::json!(v)))).collect()"
425            )
426        }
427        TypeRef::Map(k, _v) if matches!(k.as_ref(), TypeRef::Json) => {
428            // Map with Json keys: binding uses HashMap<String, V>, core uses HashMap<serde_json::Value, V>
429            format!(
430                "{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| (serde_json::from_str(&k).unwrap_or_default(), v)).collect()"
431            )
432        }
433        TypeRef::Map(k, v) => {
434            // Map with Named values need .into() conversion on each value.
435            let has_named_val = matches!(v.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
436            let has_named_key = matches!(k.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
437            let val_is_string_enum = matches!(v.as_ref(), TypeRef::Named(n)
438                if config.enum_string_names.as_ref().is_some_and(|names| names.contains(n)));
439            if field_is_ir_optional {
440                // Genuinely optional field: preserve the Option layer using .map(|m| …).
441                if val_is_string_enum {
442                    format!(
443                        "{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, serde_json::from_str(&v).unwrap_or_default())).collect())"
444                    )
445                } else if has_named_val {
446                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())")
447                } else if has_named_key {
448                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.into(), v)).collect())")
449                } else {
450                    format!("{name}: val.{name}.map(|m| m.into_iter().collect())")
451                }
452            } else if val_is_string_enum {
453                format!(
454                    "{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| (k, serde_json::from_str(&v).unwrap_or_default())).collect()"
455                )
456            } else if has_named_val {
457                format!("{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| (k, v.into())).collect()")
458            } else if has_named_key {
459                format!("{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| (k.into(), v)).collect()")
460            } else {
461                format!("{name}: val.{name}.unwrap_or_default().into_iter().collect()")
462            }
463        }
464        _ => {
465            // Simple types (primitives, String, etc): unwrap_or_default()
466            format!("{name}: val.{name}.unwrap_or_default()")
467        }
468    }
469}
470
471/// Determine the field conversion expression for binding -> core.
472pub fn field_conversion_to_core(name: &str, ty: &TypeRef, optional: bool) -> String {
473    match ty {
474        // Primitives, String, Unit -- direct assignment
475        TypeRef::Primitive(_) | TypeRef::String | TypeRef::Unit => {
476            format!("{name}: val.{name}")
477        }
478        // Bytes: binding may use Vec<u8> or napi `Buffer`; core uses `bytes::Bytes`
479        // (or `Vec<u8>` for some targets). `.to_vec().into()` works in all cases:
480        // Buffer → Vec<u8> via `From<Buffer> for Vec<u8>`, then `Vec<u8> → Bytes`
481        // via `From<Vec<u8>> for Bytes` (or identity From for Vec<u8>→Vec<u8>).
482        TypeRef::Bytes => {
483            if optional {
484                format!("{name}: val.{name}.map(|v| v.to_vec().into())")
485            } else {
486                format!("{name}: val.{name}.to_vec().into()")
487            }
488        }
489        // Json: binding uses String, core uses serde_json::Value — parse or default
490        TypeRef::Json => {
491            if optional {
492                format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())")
493            } else {
494                format!("{name}: serde_json::from_str(&val.{name}).unwrap_or_default()")
495            }
496        }
497        // Char: binding uses String, core uses char — convert first character
498        TypeRef::Char => {
499            if optional {
500                format!("{name}: val.{name}.and_then(|s| s.chars().next())")
501            } else {
502                format!("{name}: val.{name}.chars().next().unwrap_or('*')")
503            }
504        }
505        // Duration: binding uses u64 (millis), core uses std::time::Duration
506        TypeRef::Duration => {
507            if optional {
508                format!("{name}: val.{name}.map(std::time::Duration::from_millis)")
509            } else {
510                format!("{name}: std::time::Duration::from_millis(val.{name})")
511            }
512        }
513        // Path needs .into() — binding uses String, core uses PathBuf
514        TypeRef::Path => {
515            if optional {
516                format!("{name}: val.{name}.map(Into::into)")
517            } else {
518                format!("{name}: val.{name}.into()")
519            }
520        }
521        // Named type -- needs .into() to convert between binding and core types
522        // Tuple types (e.g., "(String, String)") are passthrough — no conversion needed
523        TypeRef::Named(type_name) if is_tuple_type_name(type_name) => {
524            format!("{name}: val.{name}")
525        }
526        TypeRef::Named(_) => {
527            if optional {
528                format!("{name}: val.{name}.map(Into::into)")
529            } else {
530                format!("{name}: val.{name}.into()")
531            }
532        }
533        // Map with Json value type: binding uses HashMap<K, String>, core uses HashMap<K, Value>.
534        // Use `k.into()` for non-Json keys so String→String is a no-op while still converting
535        // String→Cow<'_, str>/Box<str>/Arc<str> when the core type uses one of those wrappers.
536        TypeRef::Map(k, v) if matches!(v.as_ref(), TypeRef::Json) => {
537            let k_expr = if matches!(k.as_ref(), TypeRef::Json) {
538                "serde_json::from_str(&k).unwrap_or_default()"
539            } else {
540                "k.into()"
541            };
542            if optional {
543                format!(
544                    "{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, serde_json::from_str(&v).unwrap_or_default())).collect())"
545                )
546            } else {
547                format!(
548                    "{name}: val.{name}.into_iter().map(|(k, v)| ({k_expr}, serde_json::from_str(&v).unwrap_or_default())).collect()"
549                )
550            }
551        }
552        // Map<K, Bytes>: binding uses Vec<u8> or napi Buffer, core uses bytes::Bytes (or Vec<u8>).
553        // `.to_vec().into()` converts Buffer→Vec<u8> (napi) or is identity for Vec<u8>→Vec<u8>.
554        TypeRef::Map(_k, v) if matches!(v.as_ref(), TypeRef::Bytes) => {
555            if optional {
556                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v.to_vec().into())).collect())")
557            } else {
558                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k, v.to_vec().into())).collect()")
559            }
560        }
561        // Optional with inner
562        TypeRef::Optional(inner) => match inner.as_ref() {
563            TypeRef::Json => format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())"),
564            TypeRef::Named(_) | TypeRef::Path => format!("{name}: val.{name}.map(Into::into)"),
565            TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_)) => {
566                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
567            }
568            TypeRef::Map(k, v) if matches!(v.as_ref(), TypeRef::Json) => {
569                let k_expr = if matches!(k.as_ref(), TypeRef::Json) {
570                    "serde_json::from_str(&k).unwrap_or_default()"
571                } else {
572                    "k.into()"
573                };
574                format!(
575                    "{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, serde_json::from_str(&v).unwrap_or_default())).collect())"
576                )
577            }
578            _ => format!("{name}: val.{name}"),
579        },
580        // Vec of named or Json types -- map each element
581        TypeRef::Vec(inner) => match inner.as_ref() {
582            TypeRef::Json => {
583                if optional {
584                    format!(
585                        "{name}: val.{name}.map(|v| v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect())"
586                    )
587                } else {
588                    format!("{name}: val.{name}.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()")
589                }
590            }
591            // Vec<(T1, T2)> — tuples are passthrough
592            TypeRef::Named(type_name) if is_tuple_type_name(type_name) => {
593                format!("{name}: val.{name}")
594            }
595            TypeRef::Named(_) => {
596                if optional {
597                    format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
598                } else {
599                    format!("{name}: val.{name}.into_iter().map(Into::into).collect()")
600                }
601            }
602            _ => format!("{name}: val.{name}"),
603        },
604        // Map -- collect to handle HashMap↔BTreeMap conversion;
605        // additionally convert Named keys/values via Into, Json values via serde.
606        TypeRef::Map(k, v) => {
607            let has_named_key = matches!(k.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
608            let has_named_val = matches!(v.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
609            let has_json_val = matches!(v.as_ref(), TypeRef::Json);
610            let has_json_key = matches!(k.as_ref(), TypeRef::Json);
611            // Vec<Named> values: each vector element needs Into conversion.
612            let has_vec_named_val = matches!(v.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n)));
613            // Vec<Json> values: each element needs serde deserialization.
614            let has_vec_json_val = matches!(v.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json));
615            if has_json_val || has_json_key || has_named_key || has_named_val || has_vec_named_val || has_vec_json_val {
616                // `k.into()` is a no-op for `String`→`String` and the canonical conversion for
617                // wrapped string keys (`Cow`, `Box<str>`, `Arc<str>`) which the type resolver
618                // collapses to `TypeRef::String`.
619                let k_expr = if has_json_key {
620                    "serde_json::from_str(&k).unwrap_or(serde_json::Value::String(k))"
621                } else {
622                    "k.into()"
623                };
624                let v_expr = if has_json_val {
625                    "serde_json::from_str(&v).unwrap_or(serde_json::Value::String(v))"
626                } else if has_named_val {
627                    "v.into()"
628                } else if has_vec_named_val {
629                    "v.into_iter().map(Into::into).collect()"
630                } else if has_vec_json_val {
631                    "v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()"
632                } else {
633                    "v"
634                };
635                if optional {
636                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect())")
637                } else {
638                    format!("{name}: val.{name}.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect()")
639                }
640            } else {
641                // Map<String, String>: binding may have String keys/values, core may have Box<str>/Cow<str>.
642                // Emit .map(|(k, v)| (k.into(), v.into())) which is a no-op when both sides are String.
643                // This handles cases like HashMap<String, String> (binding) → HashMap<Box<str>, Box<str>> (core).
644                let is_string_map = matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String);
645                if is_string_map {
646                    if optional {
647                        format!(
648                            "{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect())"
649                        )
650                    } else {
651                        format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.into(), v.into())).collect()")
652                    }
653                } else {
654                    // No conversion needed for keys/values — just collect for potential
655                    // HashMap↔BTreeMap type change. Still apply per-value .into() when the value
656                    // type is a Named wrapper that requires conversion (e.g. a binding-side newtype).
657                    if optional {
658                        if has_named_val {
659                            format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())")
660                        } else {
661                            format!("{name}: val.{name}.map(|m| m.into_iter().collect())")
662                        }
663                    } else {
664                        format!("{name}: val.{name}.into_iter().collect()")
665                    }
666                }
667            }
668        }
669    }
670}
671
672/// Binding→core field conversion with backend-specific config (i64 casts, etc.).
673pub fn field_conversion_to_core_cfg(name: &str, ty: &TypeRef, optional: bool, config: &ConversionConfig) -> String {
674    // When optional=true and ty=Optional(T), the binding field was flattened from
675    // Option<Option<T>> to Option<T>. Core expects Option<Option<T>>, so wrap with .map(Some).
676    // This applies regardless of cast config; handle before any other dispatch.
677    if optional && matches!(ty, TypeRef::Optional(_)) {
678        // Delegate to get the inner Optional(T) → Option<T> conversion (with optional=false,
679        // since the outer Option is handled by the .map(Some) we add here).
680        let inner_expr = field_conversion_to_core_cfg(name, ty, false, config);
681        // inner_expr is "name: <expr-for-Option<T>>"; wrap it with .map(Some)
682        if let Some(expr) = inner_expr.strip_prefix(&format!("{name}: ")) {
683            return format!("{name}: ({expr}).map(Some)");
684        }
685        return inner_expr;
686    }
687
688    // WASM JsValue: use serde_wasm_bindgen for Map, nested Vec, and Vec<Json> types
689    if config.map_uses_jsvalue {
690        let is_nested_vec = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Vec(_)));
691        let is_vec_json = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json));
692        let is_map = matches!(ty, TypeRef::Map(_, _));
693        if is_nested_vec || is_map || is_vec_json {
694            if optional {
695                return format!(
696                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
697                );
698            }
699            return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
700        }
701        if let TypeRef::Optional(inner) = ty {
702            let is_inner_nested = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Vec(_)));
703            let is_inner_vec_json = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Json));
704            let is_inner_map = matches!(inner.as_ref(), TypeRef::Map(_, _));
705            if is_inner_nested || is_inner_map || is_inner_vec_json {
706                return format!(
707                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
708                );
709            }
710        }
711    }
712
713    // Vec<Named>→String binding→core: binding holds JSON string, core expects Vec<Named>.
714    // Only apply serde round-trip for Vec<Named> types (complex structs that can't cross FFI).
715    // Vec<String>, Vec<Primitive>, etc. stay as-is since they map directly.
716    if config.vec_named_to_string {
717        if let TypeRef::Vec(inner) = ty {
718            if matches!(inner.as_ref(), TypeRef::Named(_)) {
719                if optional {
720                    return format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())");
721                }
722                return format!("{name}: serde_json::from_str(&val.{name}).unwrap_or_default()");
723            }
724        }
725    }
726    // Map→String binding→core: use Default::default() (lossy — can't reconstruct HashMap from Debug string)
727    if config.map_as_string && matches!(ty, TypeRef::Map(_, _)) {
728        return format!("{name}: Default::default()");
729    }
730    if config.map_as_string {
731        if let TypeRef::Optional(inner) = ty {
732            if matches!(inner.as_ref(), TypeRef::Map(_, _)) {
733                return format!("{name}: Default::default()");
734            }
735        }
736    }
737    // Tagged-data enum field (WASM only; binding holds JsValue / Option<JsValue>, core holds the
738    // typed enum). Handles bare Named, Option<Named>, Vec<Named>, and Option<Vec<Named>> shapes.
739    // All four shapes are stored as JsValue or Option<JsValue> in the binding struct so that
740    // callers can pass plain JS objects without constructing explicit wasm-bindgen class instances.
741    if config.map_uses_jsvalue {
742        if let Some(tagged_names) = config.tagged_data_enum_names {
743            let bare_named = matches!(ty, TypeRef::Named(n) if tagged_names.contains(n));
744            let optional_named = matches!(ty, TypeRef::Optional(inner)
745                if matches!(inner.as_ref(), TypeRef::Named(n) if tagged_names.contains(n)));
746            let vec_named = matches!(ty, TypeRef::Vec(inner)
747                if matches!(inner.as_ref(), TypeRef::Named(n) if tagged_names.contains(n)));
748            let optional_vec_named = matches!(ty, TypeRef::Optional(outer)
749                if matches!(outer.as_ref(), TypeRef::Vec(inner)
750                    if matches!(inner.as_ref(), TypeRef::Named(n) if tagged_names.contains(n))));
751            if bare_named {
752                if optional {
753                    // Optional bare TaggedDataEnum (field.optional=true, ty=Named): binding holds
754                    // Option<JsValue>; core expects Option<T>.
755                    return format!(
756                        "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
757                    );
758                }
759                // Required bare TaggedDataEnum stored as JsValue: deserialize directly.
760                return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
761            }
762            if optional_named {
763                // Option<TaggedDataEnum> (ty=Optional(Named)) stored as Option<JsValue>: deserialize when Some.
764                return format!(
765                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
766                );
767            }
768            if vec_named {
769                return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
770            }
771            if optional_vec_named {
772                return format!(
773                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
774                );
775            }
776        }
777    }
778
779    // Untagged data enum field (binding holds serde_json::Value, core holds the typed enum):
780    // convert via serde_json::from_value.  Handles direct, Optional, and Vec wrappings.
781    if let Some(untagged_names) = config.untagged_data_enum_names {
782        let direct_named = matches!(ty, TypeRef::Named(n) if untagged_names.contains(n));
783        let optional_named = matches!(ty, TypeRef::Optional(inner)
784            if matches!(inner.as_ref(), TypeRef::Named(n) if untagged_names.contains(n)));
785        let vec_named = matches!(ty, TypeRef::Vec(inner)
786            if matches!(inner.as_ref(), TypeRef::Named(n) if untagged_names.contains(n)));
787        let optional_vec_named = matches!(ty, TypeRef::Optional(outer)
788            if matches!(outer.as_ref(), TypeRef::Vec(inner)
789                if matches!(inner.as_ref(), TypeRef::Named(n) if untagged_names.contains(n))));
790        if direct_named {
791            if optional {
792                return format!("{name}: val.{name}.and_then(|v| serde_json::from_value(v).ok())");
793            }
794            return format!("{name}: serde_json::from_value(val.{name}).unwrap_or_default()");
795        }
796        if optional_named {
797            return format!("{name}: val.{name}.and_then(|v| serde_json::from_value(v).ok())");
798        }
799        if vec_named {
800            if optional {
801                return format!(
802                    "{name}: val.{name}.map(|v| v.into_iter().filter_map(|x| serde_json::from_value(x).ok()).collect())"
803                );
804            }
805            return format!("{name}: val.{name}.into_iter().filter_map(|x| serde_json::from_value(x).ok()).collect()");
806        }
807        if optional_vec_named {
808            return format!(
809                "{name}: val.{name}.map(|v| v.into_iter().filter_map(|x| serde_json::from_value(x).ok()).collect())"
810            );
811        }
812    }
813    // Json→String binding→core: use Default::default() (lossy — can't parse String back)
814    if config.json_to_string && matches!(ty, TypeRef::Json) {
815        return format!("{name}: Default::default()");
816    }
817    // Json stays as serde_json::Value: identity passthrough.
818    if config.json_as_value && matches!(ty, TypeRef::Json) {
819        return format!("{name}: val.{name}");
820    }
821    if config.json_as_value {
822        if let TypeRef::Optional(inner) = ty {
823            if matches!(inner.as_ref(), TypeRef::Json) {
824                return format!("{name}: val.{name}");
825            }
826        }
827        if let TypeRef::Vec(inner) = ty {
828            if matches!(inner.as_ref(), TypeRef::Json) {
829                if optional {
830                    return format!("{name}: val.{name}.unwrap_or_default()");
831                }
832                return format!("{name}: val.{name}");
833            }
834        }
835        if let TypeRef::Map(_k, v) = ty {
836            if matches!(v.as_ref(), TypeRef::Json) {
837                if optional {
838                    return format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.into(), v)).collect())");
839                }
840                return format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.into(), v)).collect()");
841            }
842        }
843    }
844    // Json→JsValue binding→core: use serde_wasm_bindgen to convert (WASM)
845    if config.map_uses_jsvalue && matches!(ty, TypeRef::Json) {
846        if optional {
847            return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())");
848        }
849        return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
850    }
851    if !config.cast_large_ints_to_i64
852        && !config.cast_large_ints_to_f64
853        && !config.cast_uints_to_i32
854        && !config.cast_f32_to_f64
855        && !config.json_to_string
856        && !config.vec_named_to_string
857        && !config.map_as_string
858        && config.from_binding_skip_types.is_empty()
859    {
860        return field_conversion_to_core(name, ty, optional);
861    }
862    // Cast mode: handle primitives and Duration differently
863    match ty {
864        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
865            let core_ty = core_prim_str(p);
866            if optional {
867                format!("{name}: val.{name}.map(|v| v as {core_ty})")
868            } else {
869                format!("{name}: val.{name} as {core_ty}")
870            }
871        }
872        // f64→f32 cast (NAPI binding f64 → core f32)
873        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
874            if optional {
875                format!("{name}: val.{name}.map(|v| v as f32)")
876            } else {
877                format!("{name}: val.{name} as f32")
878            }
879        }
880        TypeRef::Duration if config.cast_large_ints_to_i64 => {
881            if optional {
882                format!("{name}: val.{name}.map(|v| std::time::Duration::from_millis(v as u64))")
883            } else {
884                format!("{name}: std::time::Duration::from_millis(val.{name} as u64)")
885            }
886        }
887        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) => {
888            if let TypeRef::Primitive(p) = inner.as_ref() {
889                let core_ty = core_prim_str(p);
890                format!("{name}: val.{name}.map(|v| v as {core_ty})")
891            } else {
892                field_conversion_to_core(name, ty, optional)
893            }
894        }
895        // Vec<u64/usize/isize> needs element-wise i64→core casting
896        TypeRef::Vec(inner)
897            if config.cast_large_ints_to_i64
898                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
899        {
900            if let TypeRef::Primitive(p) = inner.as_ref() {
901                let core_ty = core_prim_str(p);
902                if optional {
903                    format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
904                } else {
905                    format!("{name}: val.{name}.into_iter().map(|v| v as {core_ty}).collect()")
906                }
907            } else {
908                field_conversion_to_core(name, ty, optional)
909            }
910        }
911        // HashMap value type casting: when value type needs i64→core casting
912        TypeRef::Map(_k, v)
913            if config.cast_large_ints_to_i64 && matches!(v.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
914        {
915            if let TypeRef::Primitive(p) = v.as_ref() {
916                let core_ty = core_prim_str(p);
917                if optional {
918                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v as {core_ty})).collect())")
919                } else {
920                    format!("{name}: val.{name}.into_iter().map(|(k, v)| (k, v as {core_ty})).collect()")
921                }
922            } else {
923                field_conversion_to_core(name, ty, optional)
924            }
925        }
926        // Vec<f32> needs element-wise cast when f32→f64 mapping is active (NAPI)
927        TypeRef::Vec(inner)
928            if config.cast_f32_to_f64 && matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32)) =>
929        {
930            if optional {
931                format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
932            } else {
933                format!("{name}: val.{name}.into_iter().map(|v| v as f32).collect()")
934            }
935        }
936        // Optional(Vec(f32)) needs element-wise cast (NAPI only)
937        TypeRef::Optional(inner)
938            if config.cast_f32_to_f64
939                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
940        {
941            format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
942        }
943        // i32→u8/u16/u32/i8/i16 casts (extendr — R maps small ints to i32)
944        TypeRef::Primitive(p) if config.cast_uints_to_i32 && needs_i32_cast(p) => {
945            let core_ty = core_prim_str(p);
946            if optional {
947                format!("{name}: val.{name}.map(|v| v as {core_ty})")
948            } else {
949                format!("{name}: val.{name} as {core_ty}")
950            }
951        }
952        // Optional(i32-needs-cast) with cast_uints_to_i32
953        TypeRef::Optional(inner)
954            if config.cast_uints_to_i32 && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i32_cast(p)) =>
955        {
956            if let TypeRef::Primitive(p) = inner.as_ref() {
957                let core_ty = core_prim_str(p);
958                format!("{name}: val.{name}.map(|v| v as {core_ty})")
959            } else {
960                field_conversion_to_core(name, ty, optional)
961            }
962        }
963        // Vec<u8/u16/u32/i8/i16> needs element-wise i32→core casting
964        TypeRef::Vec(inner)
965            if config.cast_uints_to_i32 && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i32_cast(p)) =>
966        {
967            if let TypeRef::Primitive(p) = inner.as_ref() {
968                let core_ty = core_prim_str(p);
969                if optional {
970                    format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
971                } else {
972                    format!("{name}: val.{name}.into_iter().map(|v| v as {core_ty}).collect()")
973                }
974            } else {
975                field_conversion_to_core(name, ty, optional)
976            }
977        }
978        // f64→u64/usize/isize casts (extendr — R maps large ints to f64)
979        TypeRef::Primitive(p) if config.cast_large_ints_to_f64 && needs_f64_cast(p) => {
980            let core_ty = core_prim_str(p);
981            if optional {
982                format!("{name}: val.{name}.map(|v| v as {core_ty})")
983            } else {
984                format!("{name}: val.{name} as {core_ty}")
985            }
986        }
987        // Optional(f64-needs-cast) with cast_large_ints_to_f64
988        TypeRef::Optional(inner)
989            if config.cast_large_ints_to_f64
990                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)) =>
991        {
992            if let TypeRef::Primitive(p) = inner.as_ref() {
993                let core_ty = core_prim_str(p);
994                format!("{name}: val.{name}.map(|v| v as {core_ty})")
995            } else {
996                field_conversion_to_core(name, ty, optional)
997            }
998        }
999        // Vec<u64/usize/isize> needs element-wise f64→core casting
1000        TypeRef::Vec(inner)
1001            if config.cast_large_ints_to_f64
1002                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)) =>
1003        {
1004            if let TypeRef::Primitive(p) = inner.as_ref() {
1005                let core_ty = core_prim_str(p);
1006                if optional {
1007                    format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
1008                } else {
1009                    format!("{name}: val.{name}.into_iter().map(|v| v as {core_ty}).collect()")
1010                }
1011            } else {
1012                field_conversion_to_core(name, ty, optional)
1013            }
1014        }
1015        // Map<K, usize/u64/i64/isize/f32> needs value-wise f64→core casting (extendr)
1016        TypeRef::Map(_k, v)
1017            if config.cast_large_ints_to_f64 && matches!(v.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)) =>
1018        {
1019            if let TypeRef::Primitive(p) = v.as_ref() {
1020                let core_ty = core_prim_str(p);
1021                if optional {
1022                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v as {core_ty})).collect())")
1023                } else {
1024                    format!("{name}: val.{name}.into_iter().map(|(k, v)| (k, v as {core_ty})).collect()")
1025                }
1026            } else {
1027                field_conversion_to_core(name, ty, optional)
1028            }
1029        }
1030        // Skip-type: Named types that can't be auto-converted via Into in the binding→core From
1031        // impl (e.g. PHP VisitorHandle which is handled separately by bridge machinery).
1032        TypeRef::Named(n) if config.from_binding_skip_types.iter().any(|s| s == n) => {
1033            format!("{name}: Default::default()")
1034        }
1035        TypeRef::Optional(inner) => match inner.as_ref() {
1036            TypeRef::Named(n) if config.from_binding_skip_types.iter().any(|s| s == n) => {
1037                format!("{name}: Default::default()")
1038            }
1039            _ => field_conversion_to_core(name, ty, optional),
1040        },
1041        // Fall through to default for everything else
1042        _ => field_conversion_to_core(name, ty, optional),
1043    }
1044}
1045
1046/// Apply CoreWrapper transformations to a binding→core conversion expression.
1047/// Wraps the value expression with Arc::new(), .into() for Cow, etc.
1048pub fn apply_core_wrapper_to_core(
1049    conversion: &str,
1050    name: &str,
1051    core_wrapper: &CoreWrapper,
1052    vec_inner_core_wrapper: &CoreWrapper,
1053    optional: bool,
1054) -> String {
1055    // Handle Vec<Arc<T>>: replace .map(Into::into) with .map(|v| std::sync::Arc::new(v.into()))
1056    if *vec_inner_core_wrapper == CoreWrapper::Arc {
1057        return conversion
1058            .replace(
1059                ".map(Into::into).collect()",
1060                ".map(|v| std::sync::Arc::new(v.into())).collect()",
1061            )
1062            .replace(
1063                "map(|v| v.into_iter().map(Into::into)",
1064                "map(|v| v.into_iter().map(|v| std::sync::Arc::new(v.into()))",
1065            );
1066    }
1067
1068    match core_wrapper {
1069        CoreWrapper::None => conversion.to_string(),
1070        CoreWrapper::Cow => {
1071            // Cow<str>: binding String → core Cow via .into()
1072            // The field_conversion already emits "name: val.name" for strings,
1073            // we need to add .into() to convert String → Cow<'static, str>
1074            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
1075                if optional {
1076                    format!("{name}: {expr}.map(Into::into)")
1077                } else if expr == format!("val.{name}") {
1078                    format!("{name}: val.{name}.into()")
1079                } else if expr == "Default::default()" {
1080                    // Sanitized field: Default::default() already resolves to the correct core type
1081                    // (e.g. Cow<'static, str> — adding .into() breaks type inference).
1082                    conversion.to_string()
1083                } else {
1084                    format!("{name}: ({expr}).into()")
1085                }
1086            } else {
1087                conversion.to_string()
1088            }
1089        }
1090        CoreWrapper::Arc => {
1091            // Arc<T>: wrap with Arc::new()
1092            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
1093                if expr == "Default::default()" {
1094                    // Sanitized field: Default::default() resolves to the correct core type;
1095                    // wrapping in Arc::new() would change the type.
1096                    conversion.to_string()
1097                } else if optional {
1098                    format!("{name}: {expr}.map(|v| std::sync::Arc::new(v))")
1099                } else {
1100                    format!("{name}: std::sync::Arc::new({expr})")
1101                }
1102            } else {
1103                conversion.to_string()
1104            }
1105        }
1106        CoreWrapper::Bytes => {
1107            // Bytes: binding Vec<u8> → core bytes::Bytes via .into().
1108            // When TypeRef::Bytes already emitted a conversion (e.g. `val.{name}.into()` or
1109            // `val.{name}.map(Into::into)`), applying another .into() creates an ambiguous
1110            // double-into chain. Detect and dedup: use the already-generated expression as-is
1111            // when it fully covers the conversion, or emit a fresh single .into() for bare fields.
1112            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
1113                let already_converted_non_opt =
1114                    expr == format!("val.{name}.into()") || expr == format!("val.{name}.to_vec().into()");
1115                let already_converted_opt = expr
1116                    .strip_prefix(&format!("val.{name}"))
1117                    .map(|s| s == ".map(Into::into)" || s == ".map(|v| v.to_vec().into())")
1118                    .unwrap_or(false);
1119                if already_converted_non_opt || already_converted_opt {
1120                    // The base conversion already handles Bytes — pass through unchanged.
1121                    conversion.to_string()
1122                } else if optional {
1123                    format!("{name}: {expr}.map(Into::into)")
1124                } else if expr == format!("val.{name}") {
1125                    format!("{name}: val.{name}.into()")
1126                } else if expr == "Default::default()" {
1127                    // Sanitized field: Default::default() already resolves to the correct core type
1128                    // (e.g. bytes::Bytes — adding .into() breaks type inference).
1129                    conversion.to_string()
1130                } else {
1131                    format!("{name}: ({expr}).into()")
1132                }
1133            } else {
1134                conversion.to_string()
1135            }
1136        }
1137        CoreWrapper::ArcMutex => {
1138            // ArcMutex: binding T → core Arc<Mutex<T>> via Arc::new(Mutex::new())
1139            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
1140                if optional {
1141                    format!("{name}: {expr}.map(|v| std::sync::Arc::new(std::sync::Mutex::new(v.into())))")
1142                } else if expr == format!("val.{name}") {
1143                    format!("{name}: std::sync::Arc::new(std::sync::Mutex::new(val.{name}.into()))")
1144                } else {
1145                    format!("{name}: std::sync::Arc::new(std::sync::Mutex::new(({expr}).into()))")
1146                }
1147            } else {
1148                conversion.to_string()
1149            }
1150        }
1151    }
1152}
1153
1154#[cfg(test)]
1155mod tests {
1156    use super::gen_from_binding_to_core;
1157    use super::gen_from_binding_to_core_cfg;
1158    use crate::conversions::ConversionConfig;
1159    use ahash::AHashSet;
1160    use alef_core::ir::{CoreWrapper, DefaultValue, FieldDef, TypeDef, TypeRef};
1161
1162    fn type_with_field(field: FieldDef) -> TypeDef {
1163        TypeDef {
1164            name: "ProcessConfig".to_string(),
1165            rust_path: "crate::ProcessConfig".to_string(),
1166            original_rust_path: String::new(),
1167            fields: vec![field],
1168            methods: vec![],
1169            is_opaque: false,
1170            is_clone: true,
1171            is_copy: false,
1172            doc: String::new(),
1173            cfg: None,
1174            is_trait: false,
1175            has_default: true,
1176            has_stripped_cfg_fields: false,
1177            is_return_type: false,
1178            serde_rename_all: None,
1179            has_serde: true,
1180            super_traits: vec![],
1181            binding_excluded: false,
1182            binding_exclusion_reason: None,
1183        }
1184    }
1185
1186    #[test]
1187    fn sanitized_cow_string_field_converts_to_core() {
1188        let field = FieldDef {
1189            name: "language".to_string(),
1190            ty: TypeRef::String,
1191            optional: false,
1192            default: None,
1193            doc: String::new(),
1194            sanitized: true,
1195            is_boxed: false,
1196            type_rust_path: None,
1197            cfg: None,
1198            typed_default: Some(DefaultValue::Empty),
1199            core_wrapper: CoreWrapper::Cow,
1200            vec_inner_core_wrapper: CoreWrapper::None,
1201            newtype_wrapper: None,
1202            serde_rename: None,
1203            serde_flatten: false,
1204            binding_excluded: false,
1205            binding_exclusion_reason: None,
1206        };
1207
1208        let out = gen_from_binding_to_core(&type_with_field(field), "crate");
1209
1210        assert!(out.contains("language: val.language.into()"));
1211        assert!(!out.contains("language: Default::default()"));
1212    }
1213
1214    #[test]
1215    fn binding_excluded_cfg_field_is_not_emitted_into_core_literal() {
1216        let field = FieldDef {
1217            name: "di_container".to_string(),
1218            ty: TypeRef::String,
1219            optional: true,
1220            default: None,
1221            doc: String::new(),
1222            sanitized: false,
1223            is_boxed: false,
1224            type_rust_path: None,
1225            cfg: Some("feature = \"di\"".to_string()),
1226            typed_default: None,
1227            core_wrapper: CoreWrapper::None,
1228            vec_inner_core_wrapper: CoreWrapper::None,
1229            newtype_wrapper: None,
1230            serde_rename: None,
1231            serde_flatten: false,
1232            binding_excluded: true,
1233            binding_exclusion_reason: Some("internal implementation detail".to_string()),
1234        };
1235        let mut typ = type_with_field(field);
1236        typ.has_stripped_cfg_fields = true;
1237
1238        let out = gen_from_binding_to_core(&typ, "crate");
1239
1240        assert!(
1241            !out.contains("di_container:"),
1242            "cfg-gated binding-excluded fields may not exist in the core struct; got:\n{out}"
1243        );
1244        assert!(
1245            out.contains("..Default::default()"),
1246            "stripped cfg fields should be filled by the default update; got:\n{out}"
1247        );
1248    }
1249
1250    /// Trait-bridge OptionsField field with Arc wrapper: the binding→core From impl must
1251    /// emit `val.visitor.map(|v| (*v.inner).clone())` and must NOT fall back to
1252    /// `visitor: Default::default()`, which would silently drop the visitor handle.
1253    #[test]
1254    fn trait_bridge_arc_wrapper_field_forwards_value_not_default() {
1255        let opaque_type_name = "VisitorHandle".to_string();
1256        let mut opaque_set = AHashSet::new();
1257        opaque_set.insert(opaque_type_name.clone());
1258
1259        let field = FieldDef {
1260            name: "visitor".to_string(),
1261            ty: TypeRef::Named(opaque_type_name.clone()),
1262            optional: true,
1263            default: None,
1264            doc: String::new(),
1265            sanitized: false,
1266            is_boxed: false,
1267            type_rust_path: None,
1268            cfg: Some("feature = \"visitor\"".to_string()),
1269            typed_default: None,
1270            core_wrapper: CoreWrapper::None,
1271            vec_inner_core_wrapper: CoreWrapper::None,
1272            newtype_wrapper: None,
1273            serde_rename: None,
1274            serde_flatten: false,
1275            binding_excluded: false,
1276            binding_exclusion_reason: None,
1277        };
1278
1279        let never_skip = vec!["visitor".to_string()];
1280        let arc_wrapper = vec!["visitor".to_string()];
1281
1282        let config = ConversionConfig {
1283            opaque_types: Some(&opaque_set),
1284            never_skip_cfg_field_names: &never_skip,
1285            trait_bridge_arc_wrapper_field_names: &arc_wrapper,
1286            ..ConversionConfig::default()
1287        };
1288
1289        let out = gen_from_binding_to_core_cfg(&type_with_field(field), "crate", &config);
1290
1291        assert!(
1292            out.contains("val.visitor.map(|v| (*v.inner).clone())"),
1293            "expected arc-wrapper clone forwarding, got:\n{out}"
1294        );
1295        assert!(
1296            !out.contains("visitor: Default::default()"),
1297            "must not emit Default::default() for arc-wrapper trait-bridge field, got:\n{out}"
1298        );
1299    }
1300
1301    /// When `trait_bridge_arc_wrapper_field_names` is empty (default), the old
1302    /// `Default::default()` fallback is preserved for opaque-no-wrapper fields.
1303    #[test]
1304    fn opaque_no_wrapper_field_without_arc_flag_emits_default() {
1305        let opaque_type_name = "OpaqueHandle".to_string();
1306        let mut opaque_set = AHashSet::new();
1307        opaque_set.insert(opaque_type_name.clone());
1308
1309        let field = FieldDef {
1310            name: "handle".to_string(),
1311            ty: TypeRef::Named(opaque_type_name.clone()),
1312            optional: false,
1313            default: None,
1314            doc: String::new(),
1315            sanitized: false,
1316            is_boxed: false,
1317            type_rust_path: None,
1318            cfg: None,
1319            typed_default: None,
1320            core_wrapper: CoreWrapper::None,
1321            vec_inner_core_wrapper: CoreWrapper::None,
1322            newtype_wrapper: None,
1323            serde_rename: None,
1324            serde_flatten: false,
1325            binding_excluded: false,
1326            binding_exclusion_reason: None,
1327        };
1328
1329        let config = ConversionConfig {
1330            opaque_types: Some(&opaque_set),
1331            // trait_bridge_arc_wrapper_field_names left empty (default)
1332            ..ConversionConfig::default()
1333        };
1334
1335        let out = gen_from_binding_to_core_cfg(&type_with_field(field), "crate", &config);
1336
1337        assert!(
1338            out.contains("handle: Default::default()"),
1339            "expected Default::default() for non-arc-wrapper opaque field, got:\n{out}"
1340        );
1341        assert!(
1342            !out.contains("(*val.handle.inner).clone()"),
1343            "must not emit arc-clone for non-arc-wrapper opaque field, got:\n{out}"
1344        );
1345    }
1346}