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