Skip to main content

alef_codegen/conversions/
binding_to_core.rs

1use alef_core::ir::{CoreWrapper, PrimitiveType, TypeDef, TypeRef};
2use std::fmt::Write;
3
4use super::ConversionConfig;
5use super::helpers::{core_prim_str, core_type_path, is_newtype, is_tuple_type_name, needs_i64_cast};
6
7/// Generate `impl From<BindingType> for core::Type` (binding -> core).
8/// Sanitized fields use `Default::default()` (lossy but functional).
9pub fn gen_from_binding_to_core(typ: &TypeDef, core_import: &str) -> String {
10    gen_from_binding_to_core_cfg(typ, core_import, &ConversionConfig::default())
11}
12
13/// Generate `impl From<BindingType> for core::Type` with backend-specific config.
14pub fn gen_from_binding_to_core_cfg(typ: &TypeDef, core_import: &str, config: &ConversionConfig) -> String {
15    let core_path = core_type_path(typ, core_import);
16    let binding_name = format!("{}{}", config.type_name_prefix, typ.name);
17    let mut out = String::with_capacity(256);
18    // When cfg-gated fields exist, ..Default::default() fills them when the feature is enabled.
19    // When disabled, all fields are already specified and the update has no effect — suppress lint.
20    if typ.has_stripped_cfg_fields {
21        writeln!(out, "#[allow(clippy::needless_update)]").ok();
22    }
23    // Suppress clippy when we use the builder pattern (Default + field reassignment)
24    let uses_builder_pattern = config.option_duration_on_defaults
25        && typ.has_default
26        && typ
27            .fields
28            .iter()
29            .any(|f| !f.optional && matches!(f.ty, TypeRef::Duration));
30    if uses_builder_pattern {
31        writeln!(out, "#[allow(clippy::field_reassign_with_default)]").ok();
32    }
33    writeln!(out, "impl From<{binding_name}> for {core_path} {{").ok();
34    writeln!(out, "    fn from(val: {binding_name}) -> Self {{").ok();
35
36    // Newtype structs: generate tuple constructor Self(val._0)
37    if is_newtype(typ) {
38        let field = &typ.fields[0];
39        let inner_expr = match &field.ty {
40            TypeRef::Named(_) => "val._0.into()".to_string(),
41            TypeRef::Path => "val._0.into()".to_string(),
42            TypeRef::Duration => "std::time::Duration::from_millis(val._0)".to_string(),
43            _ => "val._0".to_string(),
44        };
45        writeln!(out, "        Self({inner_expr})").ok();
46        writeln!(out, "    }}").ok();
47        write!(out, "}}").ok();
48        return out;
49    }
50
51    // When option_duration_on_defaults is set for a has_default type, non-optional Duration
52    // fields are stored as Option<u64> in the binding struct.  We use the builder pattern
53    // so that None falls back to the core type's Default (giving the real field default,
54    // e.g. Duration::from_millis(30000)) rather than Duration::ZERO.
55    let has_optionalized_duration = config.option_duration_on_defaults
56        && typ.has_default
57        && typ
58            .fields
59            .iter()
60            .any(|f| !f.optional && matches!(f.ty, TypeRef::Duration));
61
62    if has_optionalized_duration {
63        // Builder pattern: start from core default, override explicitly-set fields.
64        writeln!(out, "        let mut __result = {core_path}::default();").ok();
65        let optionalized = config.optionalize_defaults && typ.has_default;
66        for field in &typ.fields {
67            if field.sanitized {
68                // sanitized fields keep the default value — skip
69                continue;
70            }
71            // Fields referencing excluded types keep their default value — skip
72            if !config.exclude_types.is_empty()
73                && super::helpers::field_references_excluded_type(&field.ty, config.exclude_types)
74            {
75                continue;
76            }
77            // Duration field stored as Option<u64/i64>: only override when Some
78            if !field.optional && matches!(field.ty, TypeRef::Duration) {
79                let cast = if config.cast_large_ints_to_i64 { " as u64" } else { "" };
80                writeln!(
81                    out,
82                    "        if let Some(__v) = val.{} {{ __result.{} = std::time::Duration::from_millis(__v{cast}); }}",
83                    field.name, field.name
84                )
85                .ok();
86                continue;
87            }
88            let conversion = if optionalized && !field.optional {
89                gen_optionalized_field_to_core(&field.name, &field.ty, config)
90            } else {
91                field_conversion_to_core_cfg(&field.name, &field.ty, field.optional, config)
92            };
93            // Strip the "name: " prefix to get just the expression, then assign
94            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
95                writeln!(out, "        __result.{} = {};", field.name, expr).ok();
96            }
97        }
98        writeln!(out, "        __result").ok();
99        writeln!(out, "    }}").ok();
100        write!(out, "}}").ok();
101        return out;
102    }
103
104    writeln!(out, "        Self {{").ok();
105    let optionalized = config.optionalize_defaults && typ.has_default;
106    for field in &typ.fields {
107        // Fields referencing excluded types don't exist in the binding struct;
108        // use Default::default() to fill them in the core type.
109        // Sanitized fields also use Default::default() (lossy but functional).
110        let conversion = if field.sanitized
111            || (!config.exclude_types.is_empty()
112                && super::helpers::field_references_excluded_type(&field.ty, config.exclude_types))
113        {
114            format!("{}: Default::default()", field.name)
115        } else if optionalized && !field.optional {
116            // Field was wrapped in Option<T> for JS ergonomics but core expects T.
117            // Use unwrap_or_default() for simple types, unwrap_or_default() + into for Named.
118            gen_optionalized_field_to_core(&field.name, &field.ty, config)
119        } else {
120            field_conversion_to_core_cfg(&field.name, &field.ty, field.optional, config)
121        };
122        // Newtype wrapping: when the field was resolved from a newtype (e.g. NodeIndex → u32),
123        // wrap the binding value back into the newtype for the core struct.
124        // e.g. `source: val.source` → `source: kreuzberg::NodeIndex(val.source)`
125        //      `parent: val.parent` → `parent: val.parent.map(kreuzberg::NodeIndex)`
126        //      `children: val.children` → `children: val.children.into_iter().map(kreuzberg::NodeIndex).collect()`
127        let conversion = if let Some(newtype_path) = &field.newtype_wrapper {
128            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
129                // When `optional=true` and `ty` is a plain Primitive (not TypeRef::Optional), the core
130                // field is actually `Option<NewtypeT>`, so we must use `.map(NewtypeT)` not `NewtypeT(...)`.
131                match &field.ty {
132                    TypeRef::Optional(_) => format!("{}: ({expr}).map({newtype_path})", field.name),
133                    TypeRef::Vec(_) => {
134                        format!("{}: ({expr}).into_iter().map({newtype_path}).collect()", field.name)
135                    }
136                    _ if field.optional => format!("{}: ({expr}).map({newtype_path})", field.name),
137                    _ => format!("{}: {newtype_path}({expr})", field.name),
138                }
139            } else {
140                conversion
141            }
142        } else {
143            conversion
144        };
145        // Box<T> fields: wrap the converted value in Box::new()
146        let conversion = if field.is_boxed && matches!(&field.ty, TypeRef::Named(_)) {
147            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
148                if field.optional {
149                    // Option<Box<T>> field: map inside the Option
150                    format!("{}: {}.map(Box::new)", field.name, expr)
151                } else {
152                    format!("{}: Box::new({})", field.name, expr)
153                }
154            } else {
155                conversion
156            }
157        } else {
158            conversion
159        };
160        // CoreWrapper: apply Cow/Arc/Bytes wrapping for binding→core direction
161        let conversion = apply_core_wrapper_to_core(
162            &conversion,
163            &field.name,
164            &field.core_wrapper,
165            &field.vec_inner_core_wrapper,
166            field.optional,
167        );
168        writeln!(out, "            {conversion},").ok();
169    }
170    // Use ..Default::default() to fill cfg-gated fields stripped from the IR
171    if typ.has_stripped_cfg_fields {
172        writeln!(out, "            ..Default::default()").ok();
173    }
174    writeln!(out, "        }}").ok();
175    writeln!(out, "    }}").ok();
176    write!(out, "}}").ok();
177    out
178}
179
180/// Generate field conversion for a non-optional field that was optionalized
181/// (wrapped in Option<T>) in the binding struct for JS ergonomics.
182pub(super) fn gen_optionalized_field_to_core(name: &str, ty: &TypeRef, config: &ConversionConfig) -> String {
183    match ty {
184        TypeRef::Json => {
185            format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or_default()")
186        }
187        TypeRef::Named(_) => {
188            // Named type: unwrap Option, convert via .into(), or use Default
189            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
190        }
191        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
192            format!("{name}: val.{name}.map(|v| v as f32).unwrap_or(0.0)")
193        }
194        TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => {
195            format!("{name}: val.{name}.unwrap_or(0.0)")
196        }
197        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
198            let core_ty = core_prim_str(p);
199            format!("{name}: val.{name}.map(|v| v as {core_ty}).unwrap_or_default()")
200        }
201        TypeRef::Duration if config.cast_large_ints_to_i64 => {
202            format!("{name}: val.{name}.map(|v| std::time::Duration::from_millis(v as u64)).unwrap_or_default()")
203        }
204        TypeRef::Duration => {
205            format!("{name}: val.{name}.map(std::time::Duration::from_millis).unwrap_or_default()")
206        }
207        TypeRef::Path => {
208            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
209        }
210        // Char: binding uses Option<String>, core uses char
211        TypeRef::Char => {
212            format!("{name}: val.{name}.and_then(|s| s.chars().next()).unwrap_or('*')")
213        }
214        TypeRef::Vec(inner) => match inner.as_ref() {
215            TypeRef::Json => {
216                format!(
217                    "{name}: val.{name}.map(|v| v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()).unwrap_or_default()"
218                )
219            }
220            TypeRef::Named(_) => {
221                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect()).unwrap_or_default()")
222            }
223            TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
224                let core_ty = core_prim_str(p);
225                format!(
226                    "{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect()).unwrap_or_default()"
227                )
228            }
229            _ => format!("{name}: val.{name}.unwrap_or_default()"),
230        },
231        TypeRef::Map(_, _) => {
232            // Collect to handle HashMap↔BTreeMap conversion
233            format!("{name}: val.{name}.unwrap_or_default().into_iter().collect()")
234        }
235        _ => {
236            // Simple types (primitives, String, etc): unwrap_or_default()
237            format!("{name}: val.{name}.unwrap_or_default()")
238        }
239    }
240}
241
242/// Determine the field conversion expression for binding -> core.
243pub fn field_conversion_to_core(name: &str, ty: &TypeRef, optional: bool) -> String {
244    match ty {
245        // Primitives, String, Bytes, Unit -- direct assignment
246        TypeRef::Primitive(_) | TypeRef::String | TypeRef::Bytes | TypeRef::Unit => {
247            format!("{name}: val.{name}")
248        }
249        // Json: binding uses String, core uses serde_json::Value — parse or default
250        TypeRef::Json => {
251            if optional {
252                format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())")
253            } else {
254                format!("{name}: serde_json::from_str(&val.{name}).unwrap_or_default()")
255            }
256        }
257        // Char: binding uses String, core uses char — convert first character
258        TypeRef::Char => {
259            if optional {
260                format!("{name}: val.{name}.and_then(|s| s.chars().next())")
261            } else {
262                format!("{name}: val.{name}.chars().next().unwrap_or('*')")
263            }
264        }
265        // Duration: binding uses u64 (millis), core uses std::time::Duration
266        TypeRef::Duration => {
267            if optional {
268                format!("{name}: val.{name}.map(std::time::Duration::from_millis)")
269            } else {
270                format!("{name}: std::time::Duration::from_millis(val.{name})")
271            }
272        }
273        // Path needs .into() — binding uses String, core uses PathBuf
274        TypeRef::Path => {
275            if optional {
276                format!("{name}: val.{name}.map(Into::into)")
277            } else {
278                format!("{name}: val.{name}.into()")
279            }
280        }
281        // Named type -- needs .into() to convert between binding and core types
282        // Tuple types (e.g., "(String, String)") are passthrough — no conversion needed
283        TypeRef::Named(type_name) if is_tuple_type_name(type_name) => {
284            format!("{name}: val.{name}")
285        }
286        TypeRef::Named(_) => {
287            if optional {
288                format!("{name}: val.{name}.map(Into::into)")
289            } else {
290                format!("{name}: val.{name}.into()")
291            }
292        }
293        // Optional with inner
294        TypeRef::Optional(inner) => match inner.as_ref() {
295            TypeRef::Json => format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())"),
296            TypeRef::Named(_) | TypeRef::Path => format!("{name}: val.{name}.map(Into::into)"),
297            TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_)) => {
298                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
299            }
300            _ => format!("{name}: val.{name}"),
301        },
302        // Vec of named or Json types -- map each element
303        TypeRef::Vec(inner) => match inner.as_ref() {
304            TypeRef::Json => {
305                if optional {
306                    format!(
307                        "{name}: val.{name}.map(|v| v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect())"
308                    )
309                } else {
310                    format!("{name}: val.{name}.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()")
311                }
312            }
313            // Vec<(T1, T2)> — tuples are passthrough
314            TypeRef::Named(type_name) if is_tuple_type_name(type_name) => {
315                format!("{name}: val.{name}")
316            }
317            TypeRef::Named(_) => {
318                if optional {
319                    format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
320                } else {
321                    format!("{name}: val.{name}.into_iter().map(Into::into).collect()")
322                }
323            }
324            _ => format!("{name}: val.{name}"),
325        },
326        // Map -- collect to handle HashMap↔BTreeMap conversion;
327        // additionally convert Named keys/values via Into, Json values via serde.
328        TypeRef::Map(k, v) => {
329            let has_named_key = matches!(k.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
330            let has_named_val = matches!(v.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
331            let has_json_val = matches!(v.as_ref(), TypeRef::Json);
332            let has_json_key = matches!(k.as_ref(), TypeRef::Json);
333            if has_json_val || has_json_key || has_named_key || has_named_val {
334                let k_expr = if has_json_key {
335                    "serde_json::from_str(&k).unwrap_or(serde_json::Value::String(k))"
336                } else if has_named_key {
337                    "k.into()"
338                } else {
339                    "k"
340                };
341                let v_expr = if has_json_val {
342                    "serde_json::from_str(&v).unwrap_or(serde_json::Value::String(v))"
343                } else if has_named_val {
344                    "v.into()"
345                } else {
346                    "v"
347                };
348                if optional {
349                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect())")
350                } else {
351                    format!("{name}: val.{name}.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect()")
352                }
353            } else {
354                // No conversion needed — just collect for potential HashMap↔BTreeMap type change
355                if optional {
356                    format!("{name}: val.{name}.map(|m| m.into_iter().collect())")
357                } else {
358                    format!("{name}: val.{name}.into_iter().collect()")
359                }
360            }
361        }
362    }
363}
364
365/// Binding→core field conversion with backend-specific config (i64 casts, etc.).
366pub fn field_conversion_to_core_cfg(name: &str, ty: &TypeRef, optional: bool, config: &ConversionConfig) -> String {
367    // WASM JsValue: use serde_wasm_bindgen for Map, nested Vec, and Vec<Json> types
368    if config.map_uses_jsvalue {
369        let is_nested_vec = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Vec(_)));
370        let is_vec_json = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json));
371        let is_map = matches!(ty, TypeRef::Map(_, _));
372        if is_nested_vec || is_map || is_vec_json {
373            if optional {
374                return format!(
375                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
376                );
377            }
378            return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
379        }
380        if let TypeRef::Optional(inner) = ty {
381            let is_inner_nested = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Vec(_)));
382            let is_inner_vec_json = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Json));
383            let is_inner_map = matches!(inner.as_ref(), TypeRef::Map(_, _));
384            if is_inner_nested || is_inner_map || is_inner_vec_json {
385                return format!(
386                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
387                );
388            }
389        }
390    }
391
392    // Json→String binding→core: use Default::default() (lossy — can't parse String back)
393    if config.json_to_string && matches!(ty, TypeRef::Json) {
394        return format!("{name}: Default::default()");
395    }
396    // Json→JsValue binding→core: use serde_wasm_bindgen to convert (WASM)
397    if config.map_uses_jsvalue && matches!(ty, TypeRef::Json) {
398        if optional {
399            return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())");
400        }
401        return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
402    }
403    if !config.cast_large_ints_to_i64 && !config.cast_f32_to_f64 && !config.json_to_string {
404        return field_conversion_to_core(name, ty, optional);
405    }
406    // Cast mode: handle primitives and Duration differently
407    match ty {
408        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
409            let core_ty = core_prim_str(p);
410            if optional {
411                format!("{name}: val.{name}.map(|v| v as {core_ty})")
412            } else {
413                format!("{name}: val.{name} as {core_ty}")
414            }
415        }
416        // f64→f32 cast (NAPI binding f64 → core f32)
417        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
418            if optional {
419                format!("{name}: val.{name}.map(|v| v as f32)")
420            } else {
421                format!("{name}: val.{name} as f32")
422            }
423        }
424        TypeRef::Duration if config.cast_large_ints_to_i64 => {
425            if optional {
426                format!("{name}: val.{name}.map(|v| std::time::Duration::from_millis(v as u64))")
427            } else {
428                format!("{name}: std::time::Duration::from_millis(val.{name} as u64)")
429            }
430        }
431        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) => {
432            if let TypeRef::Primitive(p) = inner.as_ref() {
433                let core_ty = core_prim_str(p);
434                format!("{name}: val.{name}.map(|v| v as {core_ty})")
435            } else {
436                field_conversion_to_core(name, ty, optional)
437            }
438        }
439        // Vec<u64/usize/isize> needs element-wise i64→core casting
440        TypeRef::Vec(inner)
441            if config.cast_large_ints_to_i64
442                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
443        {
444            if let TypeRef::Primitive(p) = inner.as_ref() {
445                let core_ty = core_prim_str(p);
446                if optional {
447                    format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
448                } else {
449                    format!("{name}: val.{name}.into_iter().map(|v| v as {core_ty}).collect()")
450                }
451            } else {
452                field_conversion_to_core(name, ty, optional)
453            }
454        }
455        // Vec<f32> needs element-wise cast when f32→f64 mapping is active (NAPI)
456        TypeRef::Vec(inner)
457            if config.cast_f32_to_f64 && matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32)) =>
458        {
459            if optional {
460                format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
461            } else {
462                format!("{name}: val.{name}.into_iter().map(|v| v as f32).collect()")
463            }
464        }
465        // Optional(Vec(f32)) needs element-wise cast (NAPI only)
466        TypeRef::Optional(inner)
467            if config.cast_f32_to_f64
468                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
469        {
470            format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
471        }
472        // Fall through to default for everything else
473        _ => field_conversion_to_core(name, ty, optional),
474    }
475}
476
477/// Apply CoreWrapper transformations to a binding→core conversion expression.
478/// Wraps the value expression with Arc::new(), .into() for Cow, etc.
479fn apply_core_wrapper_to_core(
480    conversion: &str,
481    name: &str,
482    core_wrapper: &CoreWrapper,
483    vec_inner_core_wrapper: &CoreWrapper,
484    optional: bool,
485) -> String {
486    // Handle Vec<Arc<T>>: replace .map(Into::into) with .map(|v| std::sync::Arc::new(v.into()))
487    if *vec_inner_core_wrapper == CoreWrapper::Arc {
488        return conversion
489            .replace(
490                ".map(Into::into).collect()",
491                ".map(|v| std::sync::Arc::new(v.into())).collect()",
492            )
493            .replace(
494                "map(|v| v.into_iter().map(Into::into)",
495                "map(|v| v.into_iter().map(|v| std::sync::Arc::new(v.into()))",
496            );
497    }
498
499    match core_wrapper {
500        CoreWrapper::None => conversion.to_string(),
501        CoreWrapper::Cow => {
502            // Cow<str>: binding String → core Cow via .into()
503            // The field_conversion already emits "name: val.name" for strings,
504            // we need to add .into() to convert String → Cow<'static, str>
505            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
506                if optional {
507                    format!("{name}: {expr}.map(Into::into)")
508                } else if expr == format!("val.{name}") {
509                    format!("{name}: val.{name}.into()")
510                } else if expr == "Default::default()" {
511                    // Sanitized field: Default::default() already resolves to the correct core type
512                    // (e.g. Cow<'static, str> — adding .into() breaks type inference).
513                    conversion.to_string()
514                } else {
515                    format!("{name}: ({expr}).into()")
516                }
517            } else {
518                conversion.to_string()
519            }
520        }
521        CoreWrapper::Arc => {
522            // Arc<T>: wrap with Arc::new()
523            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
524                if expr == "Default::default()" {
525                    // Sanitized field: Default::default() resolves to the correct core type;
526                    // wrapping in Arc::new() would change the type.
527                    conversion.to_string()
528                } else if optional {
529                    format!("{name}: {expr}.map(|v| std::sync::Arc::new(v))")
530                } else {
531                    format!("{name}: std::sync::Arc::new({expr})")
532                }
533            } else {
534                conversion.to_string()
535            }
536        }
537        CoreWrapper::Bytes => {
538            // Bytes: binding Vec<u8> → core Bytes via .into()
539            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
540                if optional {
541                    format!("{name}: {expr}.map(Into::into)")
542                } else if expr == format!("val.{name}") {
543                    format!("{name}: val.{name}.into()")
544                } else if expr == "Default::default()" {
545                    // Sanitized field: Default::default() already resolves to the correct core type
546                    // (e.g. bytes::Bytes — adding .into() breaks type inference).
547                    conversion.to_string()
548                } else {
549                    format!("{name}: ({expr}).into()")
550                }
551            } else {
552                conversion.to_string()
553            }
554        }
555    }
556}