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