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