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