Skip to main content

alef_codegen/conversions/
core_to_binding.rs

1use ahash::AHashSet;
2use alef_core::ir::{CoreWrapper, PrimitiveType, TypeDef, TypeRef};
3use std::fmt::Write;
4
5use super::ConversionConfig;
6use super::binding_to_core::field_conversion_to_core;
7use super::helpers::is_newtype;
8use super::helpers::{binding_prim_str, core_type_path, needs_i64_cast};
9
10/// Generate `impl From<core::Type> for BindingType` (core -> binding).
11pub fn gen_from_core_to_binding(typ: &TypeDef, core_import: &str, opaque_types: &AHashSet<String>) -> String {
12    gen_from_core_to_binding_cfg(typ, core_import, opaque_types, &ConversionConfig::default())
13}
14
15/// Generate `impl From<core::Type> for BindingType` with backend-specific config.
16pub fn gen_from_core_to_binding_cfg(
17    typ: &TypeDef,
18    core_import: &str,
19    opaque_types: &AHashSet<String>,
20    config: &ConversionConfig,
21) -> String {
22    let core_path = core_type_path(typ, core_import);
23    let binding_name = format!("{}{}", config.type_name_prefix, typ.name);
24    let mut out = String::with_capacity(256);
25    writeln!(out, "impl From<{core_path}> for {binding_name} {{").ok();
26    writeln!(out, "    fn from(val: {core_path}) -> Self {{").ok();
27
28    // Newtype structs: extract inner value with val.0
29    if is_newtype(typ) {
30        let field = &typ.fields[0];
31        let inner_expr = match &field.ty {
32            TypeRef::Named(_) => "val.0.into()".to_string(),
33            TypeRef::Path => "val.0.to_string_lossy().to_string()".to_string(),
34            TypeRef::Duration => "val.0.as_millis() as u64".to_string(),
35            _ => "val.0".to_string(),
36        };
37        writeln!(out, "        Self {{ _0: {inner_expr} }}").ok();
38        writeln!(out, "    }}").ok();
39        write!(out, "}}").ok();
40        return out;
41    }
42
43    let optionalized = config.optionalize_defaults && typ.has_default;
44    writeln!(out, "        Self {{").ok();
45    for field in &typ.fields {
46        // Fields referencing excluded types are not present in the binding struct — skip
47        if !config.exclude_types.is_empty()
48            && super::helpers::field_references_excluded_type(&field.ty, config.exclude_types)
49        {
50            continue;
51        }
52        let base_conversion = field_conversion_from_core_cfg(
53            &field.name,
54            &field.ty,
55            field.optional,
56            field.sanitized,
57            opaque_types,
58            config,
59        );
60        // Box<T> fields: dereference before conversion.
61        let base_conversion = if field.is_boxed && matches!(&field.ty, TypeRef::Named(_)) {
62            if field.optional {
63                // Optional<Box<T>>: replace .map(Into::into) with .map(|v| (*v).into())
64                let src = format!("{}: val.{}.map(Into::into)", field.name, field.name);
65                let dst = format!("{}: val.{}.map(|v| (*v).into())", field.name, field.name);
66                if base_conversion == src { dst } else { base_conversion }
67            } else {
68                // Box<T>: replace `val.{name}` with `(*val.{name})`
69                base_conversion.replace(&format!("val.{}", field.name), &format!("(*val.{})", field.name))
70            }
71        } else {
72            base_conversion
73        };
74        // Newtype unwrapping: when the field was resolved from a newtype (e.g. NodeIndex → u32),
75        // unwrap the core newtype by accessing `.0`.
76        // e.g. `source: val.source` → `source: val.source.0`
77        //      `parent: val.parent` → `parent: val.parent.map(|v| v.0)`
78        //      `children: val.children` → `children: val.children.iter().map(|v| v.0).collect()`
79        let base_conversion = if field.newtype_wrapper.is_some() {
80            match &field.ty {
81                TypeRef::Optional(_) => {
82                    // Replace `val.{name}` with `val.{name}.map(|v| v.0)` in the generated expression
83                    base_conversion.replace(
84                        &format!("val.{}", field.name),
85                        &format!("val.{}.map(|v| v.0)", field.name),
86                    )
87                }
88                TypeRef::Vec(_) => {
89                    // Replace `val.{name}` with `val.{name}.iter().map(|v| v.0).collect()` in expression
90                    base_conversion.replace(
91                        &format!("val.{}", field.name),
92                        &format!("val.{}.iter().map(|v| v.0).collect::<Vec<_>>()", field.name),
93                    )
94                }
95                // When `optional=true` and `ty` is a plain Primitive (not TypeRef::Optional), the core
96                // field is actually `Option<NewtypeT>`, so we must use `.map(|v| v.0)` not `.0`.
97                _ if field.optional => base_conversion.replace(
98                    &format!("val.{}", field.name),
99                    &format!("val.{}.map(|v| v.0)", field.name),
100                ),
101                _ => {
102                    // Direct field: append `.0` to access the inner primitive
103                    base_conversion.replace(&format!("val.{}", field.name), &format!("val.{}.0", field.name))
104                }
105            }
106        } else {
107            base_conversion
108        };
109        // Optionalized non-optional fields need Some() wrapping in core→binding direction.
110        // This covers both NAPI-style full optionalization and PyO3-style Duration optionalization.
111        let needs_some_wrap = (optionalized && !field.optional)
112            || (config.option_duration_on_defaults
113                && typ.has_default
114                && !field.optional
115                && matches!(field.ty, TypeRef::Duration));
116        let conversion = if needs_some_wrap {
117            // Extract the value expression after "name: " and wrap in Some()
118            if let Some(expr) = base_conversion.strip_prefix(&format!("{}: ", field.name)) {
119                format!("{}: Some({})", field.name, expr)
120            } else {
121                base_conversion
122            }
123        } else {
124            base_conversion
125        };
126        // CoreWrapper: unwrap Arc, convert Cow→String, Bytes→Vec<u8>
127        let conversion = apply_core_wrapper_from_core(
128            &conversion,
129            &field.name,
130            &field.core_wrapper,
131            &field.vec_inner_core_wrapper,
132            field.optional,
133        );
134        // Skip cfg-gated fields — they don't exist in the binding struct
135        if field.cfg.is_some() {
136            continue;
137        }
138        writeln!(out, "            {conversion},").ok();
139    }
140
141    writeln!(out, "        }}").ok();
142    writeln!(out, "    }}").ok();
143    write!(out, "}}").ok();
144    out
145}
146
147/// Same but for core -> binding direction.
148/// Some types are asymmetric (PathBuf→String, sanitized fields need .to_string()).
149pub fn field_conversion_from_core(
150    name: &str,
151    ty: &TypeRef,
152    optional: bool,
153    sanitized: bool,
154    opaque_types: &AHashSet<String>,
155) -> String {
156    // Sanitized fields: the binding type differs from core (e.g. Box<str>→String, Cow<str>→String).
157    // Use .to_string() for String targets, proper iteration for Vec/Map, format!("{:?}") as last resort.
158    if sanitized {
159        // Map(String, String): sanitized from Map(Box<str>, Box<str>) etc.
160        if let TypeRef::Map(k, v) = ty {
161            if matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String) {
162                if optional {
163                    return format!(
164                        "{name}: val.{name}.as_ref().map(|m| m.iter().map(|(k, v)| (format!(\"{{:?}}\", k), format!(\"{{:?}}\", v))).collect())"
165                    );
166                }
167                return format!(
168                    "{name}: val.{name}.into_iter().map(|(k, v)| (format!(\"{{:?}}\", k), format!(\"{{:?}}\", v))).collect()"
169                );
170            }
171        }
172        // Vec<String>: sanitized from Vec<Box<str>>, Vec<(T, U)>, etc.
173        if let TypeRef::Vec(inner) = ty {
174            if matches!(inner.as_ref(), TypeRef::String) {
175                if optional {
176                    return format!(
177                        "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| format!(\"{{:?}}\", i)).collect())"
178                    );
179                }
180                return format!("{name}: val.{name}.iter().map(|i| format!(\"{{:?}}\", i)).collect()");
181            }
182        }
183        // Optional<Vec<String>>: sanitized from Optional<Vec<Box<str>>>, Optional<Vec<(T, U)>>, etc.
184        // Use format!("{:?}", i) because source elements may not impl Display (e.g. tuples).
185        if let TypeRef::Optional(opt_inner) = ty {
186            if let TypeRef::Vec(vec_inner) = opt_inner.as_ref() {
187                if matches!(vec_inner.as_ref(), TypeRef::String) {
188                    return format!(
189                        "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| format!(\"{{:?}}\", i)).collect())"
190                    );
191                }
192            }
193        }
194        // String: sanitized from Box<str>, Cow<str>, tuple, etc.
195        // Use format!("{:?}") since the source type may not impl Display (e.g., tuples).
196        if matches!(ty, TypeRef::String) {
197            if optional {
198                return format!("{name}: val.{name}.as_ref().map(|v| format!(\"{{:?}}\", v))");
199            }
200            return format!("{name}: format!(\"{{:?}}\", val.{name})");
201        }
202        // Fallback for truly unknown sanitized types
203        if optional {
204            return format!("{name}: val.{name}.as_ref().map(|v| format!(\"{{:?}}\", v))");
205        }
206        return format!("{name}: format!(\"{{:?}}\", val.{name})");
207    }
208    match ty {
209        // Duration: core uses std::time::Duration, binding uses u64 (millis)
210        TypeRef::Duration => {
211            if optional {
212                return format!("{name}: val.{name}.map(|d| d.as_millis() as u64)");
213            }
214            format!("{name}: val.{name}.as_millis() as u64")
215        }
216        // Path: core uses PathBuf, binding uses String — PathBuf→String needs special handling
217        TypeRef::Path => {
218            if optional {
219                format!("{name}: val.{name}.map(|p| p.to_string_lossy().to_string())")
220            } else {
221                format!("{name}: val.{name}.to_string_lossy().to_string()")
222            }
223        }
224        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Path) => {
225            format!("{name}: val.{name}.map(|p| p.to_string_lossy().to_string())")
226        }
227        // Char: core uses char, binding uses String — convert char to string
228        TypeRef::Char => {
229            if optional {
230                format!("{name}: val.{name}.map(|c| c.to_string())")
231            } else {
232                format!("{name}: val.{name}.to_string()")
233            }
234        }
235        // Bytes: core uses bytes::Bytes, binding uses Vec<u8>
236        TypeRef::Bytes => {
237            if optional {
238                format!("{name}: val.{name}.map(|v| v.to_vec())")
239            } else {
240                format!("{name}: val.{name}.to_vec()")
241            }
242        }
243        // Opaque Named types: wrap in Arc to create the binding wrapper
244        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
245            if optional {
246                format!("{name}: val.{name}.map(|v| {n} {{ inner: Arc::new(v) }})")
247            } else {
248                format!("{name}: {n} {{ inner: Arc::new(val.{name}) }}")
249            }
250        }
251        // Json: core uses serde_json::Value, binding uses String — use .to_string()
252        TypeRef::Json => {
253            if optional {
254                format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
255            } else {
256                format!("{name}: val.{name}.to_string()")
257            }
258        }
259        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Json) => {
260            format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
261        }
262        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json) => {
263            if optional {
264                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| i.to_string()).collect())")
265            } else {
266                format!("{name}: val.{name}.iter().map(ToString::to_string).collect()")
267            }
268        }
269        // Map with Json values: core uses HashMap<K, serde_json::Value>, binding uses HashMap<K, String>
270        TypeRef::Map(k, v) if matches!(v.as_ref(), TypeRef::Json) => {
271            let k_is_json = matches!(k.as_ref(), TypeRef::Json);
272            let k_expr = if k_is_json { "k.to_string()" } else { "k" };
273            if optional {
274                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, v.to_string())).collect())")
275            } else {
276                format!("{name}: val.{name}.into_iter().map(|(k, v)| ({k_expr}, v.to_string())).collect()")
277            }
278        }
279        // Map with Json keys: core uses HashMap<serde_json::Value, V>, binding uses HashMap<String, V>
280        TypeRef::Map(k, _v) if matches!(k.as_ref(), TypeRef::Json) => {
281            if optional {
282                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.to_string(), v)).collect())")
283            } else {
284                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.to_string(), v)).collect()")
285            }
286        }
287        // Everything else is symmetric
288        _ => field_conversion_to_core(name, ty, optional),
289    }
290}
291
292/// Core→binding field conversion with backend-specific config.
293pub fn field_conversion_from_core_cfg(
294    name: &str,
295    ty: &TypeRef,
296    optional: bool,
297    sanitized: bool,
298    opaque_types: &AHashSet<String>,
299    config: &ConversionConfig,
300) -> String {
301    // Sanitized fields: for WASM (map_uses_jsvalue), Map and Vec<Json> fields target JsValue
302    // and need serde_wasm_bindgen::to_value() instead of iterator-based .collect().
303    // Note: Vec<String> sanitized does NOT use the JsValue path because Vec<String> maps to
304    // Vec<String> in WASM (not JsValue) — use the normal sanitized iterator path instead.
305    if sanitized {
306        if config.map_uses_jsvalue {
307            // Map(String, String) sanitized → JsValue (HashMap maps to JsValue in WASM)
308            if let TypeRef::Map(k, v) = ty {
309                if matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String) {
310                    if optional {
311                        return format!(
312                            "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())"
313                        );
314                    }
315                    return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
316                }
317            }
318            // Vec<Json> sanitized → JsValue (Vec<Json> maps to JsValue in WASM via nested-vec path)
319            if let TypeRef::Vec(inner) = ty {
320                if matches!(inner.as_ref(), TypeRef::Json) {
321                    if optional {
322                        return format!(
323                            "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())"
324                        );
325                    }
326                    return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
327                }
328            }
329        }
330        return field_conversion_from_core(name, ty, optional, sanitized, opaque_types);
331    }
332
333    // WASM JsValue: use serde_wasm_bindgen for Map and nested Vec types
334    if config.map_uses_jsvalue {
335        let is_nested_vec = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Vec(_)));
336        let is_map = matches!(ty, TypeRef::Map(_, _));
337        if is_nested_vec || is_map {
338            if optional {
339                return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())");
340            }
341            return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
342        }
343        if let TypeRef::Optional(inner) = ty {
344            let is_inner_nested = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Vec(_)));
345            let is_inner_map = matches!(inner.as_ref(), TypeRef::Map(_, _));
346            if is_inner_nested || is_inner_map {
347                return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())");
348            }
349        }
350    }
351
352    let prefix = config.type_name_prefix;
353    let is_enum_string = |n: &str| -> bool { config.enum_string_names.as_ref().is_some_and(|names| names.contains(n)) };
354
355    match ty {
356        // i64 casting for large int primitives
357        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
358            let cast_to = binding_prim_str(p);
359            if optional {
360                format!("{name}: val.{name}.map(|v| v as {cast_to})")
361            } else {
362                format!("{name}: val.{name} as {cast_to}")
363            }
364        }
365        // f32→f64 casting (NAPI only)
366        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
367            if optional {
368                format!("{name}: val.{name}.map(|v| v as f64)")
369            } else {
370                format!("{name}: val.{name} as f64")
371            }
372        }
373        // Duration with i64 casting
374        TypeRef::Duration if config.cast_large_ints_to_i64 => {
375            if optional {
376                format!("{name}: val.{name}.map(|d| d.as_millis() as u64 as i64)")
377            } else {
378                format!("{name}: val.{name}.as_millis() as u64 as i64")
379            }
380        }
381        // Opaque Named types with prefix: wrap in Arc with prefixed binding name
382        TypeRef::Named(n) if opaque_types.contains(n.as_str()) && !prefix.is_empty() => {
383            let prefixed = format!("{prefix}{n}");
384            if optional {
385                format!("{name}: val.{name}.map(|v| {prefixed} {{ inner: Arc::new(v) }})")
386            } else {
387                format!("{name}: {prefixed} {{ inner: Arc::new(val.{name}) }}")
388            }
389        }
390        // Enum-to-String Named types (PHP pattern)
391        TypeRef::Named(n) if is_enum_string(n) => {
392            // Use serde serialization to get the correct serde(rename) value, not Debug format.
393            // serde_json::to_value gives Value::String("auto") which we extract.
394            if optional {
395                format!(
396                    "{name}: val.{name}.as_ref().map(|v| serde_json::to_value(v).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default())"
397                )
398            } else {
399                format!(
400                    "{name}: serde_json::to_value(val.{name}).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default()"
401                )
402            }
403        }
404        // Vec<Enum-to-String> Named types: element-wise serde serialization
405        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(n) if is_enum_string(n)) => {
406            if optional {
407                format!(
408                    "{name}: val.{name}.as_ref().map(|v| v.iter().map(|x| serde_json::to_value(x).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default()).collect())"
409                )
410            } else {
411                format!(
412                    "{name}: val.{name}.iter().map(|v| serde_json::to_value(v).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default()).collect()"
413                )
414            }
415        }
416        // Optional(Vec<Enum-to-String>) Named types (PHP pattern)
417        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(n) if is_enum_string(n))) =>
418        {
419            format!(
420                "{name}: val.{name}.as_ref().map(|v| v.iter().map(|x| serde_json::to_value(x).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default()).collect())"
421            )
422        }
423        // Vec<f32> needs element-wise cast to f64 when f32→f64 mapping is active
424        TypeRef::Vec(inner)
425            if config.cast_f32_to_f64 && matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32)) =>
426        {
427            if optional {
428                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
429            } else {
430                format!("{name}: val.{name}.iter().map(|&v| v as f64).collect()")
431            }
432        }
433        // Optional(Vec(f32)) needs element-wise cast to f64
434        TypeRef::Optional(inner)
435            if config.cast_f32_to_f64
436                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
437        {
438            format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
439        }
440        // Optional with i64-cast inner
441        TypeRef::Optional(inner)
442            if config.cast_large_ints_to_i64
443                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
444        {
445            if let TypeRef::Primitive(p) = inner.as_ref() {
446                let cast_to = binding_prim_str(p);
447                format!("{name}: val.{name}.map(|v| v as {cast_to})")
448            } else {
449                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
450            }
451        }
452        // Vec<u64/usize/isize> needs element-wise i64 casting (core→binding)
453        TypeRef::Vec(inner)
454            if config.cast_large_ints_to_i64
455                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
456        {
457            if let TypeRef::Primitive(p) = inner.as_ref() {
458                let cast_to = binding_prim_str(p);
459                if optional {
460                    format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as {cast_to}).collect())")
461                } else {
462                    format!("{name}: val.{name}.iter().map(|&v| v as {cast_to}).collect()")
463                }
464            } else {
465                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
466            }
467        }
468        // Json→String: core uses serde_json::Value, binding uses String (PHP)
469        TypeRef::Json if config.json_to_string => {
470            if optional {
471                format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
472            } else {
473                format!("{name}: val.{name}.to_string()")
474            }
475        }
476        // Json→JsValue: core uses serde_json::Value, binding uses JsValue (WASM)
477        TypeRef::Json if config.map_uses_jsvalue => {
478            if optional {
479                format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
480            } else {
481                format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)")
482            }
483        }
484        // Vec<Json>→JsValue: core uses Vec<serde_json::Value>, binding uses JsValue (WASM)
485        TypeRef::Vec(inner) if config.map_uses_jsvalue && matches!(inner.as_ref(), TypeRef::Json) => {
486            if optional {
487                format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
488            } else {
489                format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)")
490            }
491        }
492        // Optional(Vec<Json>)→JsValue (WASM)
493        TypeRef::Optional(inner)
494            if config.map_uses_jsvalue
495                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Json)) =>
496        {
497            format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
498        }
499        // Fall through to default (handles paths, opaque without prefix, etc.)
500        _ => field_conversion_from_core(name, ty, optional, sanitized, opaque_types),
501    }
502}
503
504/// Apply CoreWrapper transformations for core→binding direction.
505/// Unwraps Arc, converts Cow→String, Bytes→Vec<u8>.
506fn apply_core_wrapper_from_core(
507    conversion: &str,
508    name: &str,
509    core_wrapper: &CoreWrapper,
510    vec_inner_core_wrapper: &CoreWrapper,
511    optional: bool,
512) -> String {
513    // Handle Vec<Arc<T>>: unwrap Arc elements
514    if *vec_inner_core_wrapper == CoreWrapper::Arc {
515        return conversion
516            .replace(".map(Into::into).collect()", ".map(|v| (*v).clone().into()).collect()")
517            .replace(
518                "map(|v| v.into_iter().map(Into::into)",
519                "map(|v| v.into_iter().map(|v| (*v).clone().into())",
520            );
521    }
522
523    match core_wrapper {
524        CoreWrapper::None => conversion.to_string(),
525        CoreWrapper::Cow => {
526            // Cow<str> → String: core val.name is Cow, binding needs String
527            // The conversion already emits "name: val.name" for strings which works
528            // since Cow<str> derefs to &str and String: From<Cow<str>> exists.
529            // But if it's "val.name" directly, add .into_owned() or .to_string()
530            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
531                if optional {
532                    // Already handled by map
533                    conversion.to_string()
534                } else if expr == format!("val.{name}") {
535                    format!("{name}: val.{name}.into_owned()")
536                } else {
537                    conversion.to_string()
538                }
539            } else {
540                conversion.to_string()
541            }
542        }
543        CoreWrapper::Arc => {
544            // Arc<T> → T: unwrap via clone
545            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
546                if optional {
547                    format!("{name}: {expr}.map(|v| (*v).clone().into())")
548                } else {
549                    let unwrapped = expr.replace(&format!("val.{name}"), &format!("(*val.{name}).clone()"));
550                    format!("{name}: {unwrapped}")
551                }
552            } else {
553                conversion.to_string()
554            }
555        }
556        CoreWrapper::Bytes => {
557            // Bytes → Vec<u8>: .to_vec()
558            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
559                if optional {
560                    format!("{name}: {expr}.map(|v| v.to_vec())")
561                } else if expr == format!("val.{name}") {
562                    format!("{name}: val.{name}.to_vec()")
563                } else {
564                    conversion.to_string()
565                }
566            } else {
567                conversion.to_string()
568            }
569        }
570    }
571}