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        // When field.optional=true AND field.ty=Optional(T), the binding struct flattens
110        // Option<Option<T>> to Option<T>. Core produces Option<Option<T>>, binding needs
111        // Option<T>. Generate the conversion by treating the pre-flattened field as Option<T>:
112        // call the standard conversion for the inner type T with optional=true, substituting
113        // val.{name}.flatten() for val.{name} so all cast/conversion logic applies to T.
114        let is_flattened_optional = field.optional && matches!(field.ty, TypeRef::Optional(_));
115        let base_conversion = if is_flattened_optional {
116            if let TypeRef::Optional(inner) = &field.ty {
117                // Produce the conversion as if the field is Option<inner> with value val.name.flatten()
118                let inner_conv = field_conversion_from_core_cfg(
119                    &field.name,
120                    inner.as_ref(),
121                    true,
122                    field.sanitized,
123                    opaque_types,
124                    config,
125                );
126                // inner_conv references val.{name}; replace with val.{name}.flatten()
127                inner_conv.replace(&format!("val.{}", field.name), &format!("val.{}.flatten()", field.name))
128            } else {
129                base_conversion
130            }
131        } else {
132            base_conversion
133        };
134        // Optionalized non-optional fields need Some() wrapping in core→binding direction.
135        // This covers both NAPI-style full optionalization and PyO3-style Duration optionalization.
136        // Flattened-optional fields are already handled above with the correct type.
137        let needs_some_wrap = !is_flattened_optional
138            && ((optionalized && !field.optional)
139                || (config.option_duration_on_defaults
140                    && typ.has_default
141                    && !field.optional
142                    && matches!(field.ty, TypeRef::Duration)));
143        let conversion = if needs_some_wrap {
144            // Extract the value expression after "name: " and wrap in Some()
145            if let Some(expr) = base_conversion.strip_prefix(&format!("{}: ", field.name)) {
146                format!("{}: Some({})", field.name, expr)
147            } else {
148                base_conversion
149            }
150        } else {
151            base_conversion
152        };
153        // CoreWrapper: unwrap Arc, convert Cow→String, Bytes→Vec<u8>
154        // Skip for sanitized fields since their conversion already handles the type mismatch via format!("{:?}", ...)
155        let conversion = if !field.sanitized {
156            apply_core_wrapper_from_core(
157                &conversion,
158                &field.name,
159                &field.core_wrapper,
160                &field.vec_inner_core_wrapper,
161                field.optional,
162            )
163        } else {
164            conversion
165        };
166        // Skip cfg-gated fields — they don't exist in the binding struct
167        if field.cfg.is_some() {
168            continue;
169        }
170        writeln!(out, "            {conversion},").ok();
171    }
172
173    writeln!(out, "        }}").ok();
174    writeln!(out, "    }}").ok();
175    write!(out, "}}").ok();
176    out
177}
178
179/// Same but for core -> binding direction.
180/// Some types are asymmetric (PathBuf→String, sanitized fields need .to_string()).
181pub fn field_conversion_from_core(
182    name: &str,
183    ty: &TypeRef,
184    optional: bool,
185    sanitized: bool,
186    opaque_types: &AHashSet<String>,
187) -> String {
188    // Sanitized fields: the binding type differs from core (e.g. Box<str>→String, Cow<str>→String).
189    // Use .to_string() for String targets, proper iteration for Vec/Map, format!("{:?}") as last resort.
190    if sanitized {
191        // Map(String, String): sanitized from Map(Box<str>, Box<str>) etc.
192        if let TypeRef::Map(k, v) = ty {
193            if matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String) {
194                if optional {
195                    return format!(
196                        "{name}: val.{name}.as_ref().map(|m| m.iter().map(|(k, v)| (format!(\"{{:?}}\", k), format!(\"{{:?}}\", v))).collect())"
197                    );
198                }
199                return format!(
200                    "{name}: val.{name}.into_iter().map(|(k, v)| (format!(\"{{:?}}\", k), format!(\"{{:?}}\", v))).collect()"
201                );
202            }
203        }
204        // Vec<String>: sanitized from Vec<Box<str>>, Vec<(T, U)>, etc.
205        if let TypeRef::Vec(inner) = ty {
206            if matches!(inner.as_ref(), TypeRef::String) {
207                if optional {
208                    return format!(
209                        "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| format!(\"{{:?}}\", i)).collect())"
210                    );
211                }
212                return format!("{name}: val.{name}.iter().map(|i| format!(\"{{:?}}\", i)).collect()");
213            }
214        }
215        // Optional<Vec<String>>: sanitized from Optional<Vec<Box<str>>>, Optional<Vec<(T, U)>>, etc.
216        // Use format!("{:?}", i) because source elements may not impl Display (e.g. tuples).
217        if let TypeRef::Optional(opt_inner) = ty {
218            if let TypeRef::Vec(vec_inner) = opt_inner.as_ref() {
219                if matches!(vec_inner.as_ref(), TypeRef::String) {
220                    return format!(
221                        "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| format!(\"{{:?}}\", i)).collect())"
222                    );
223                }
224            }
225        }
226        // String: sanitized from Box<str>, Cow<str>, tuple, etc.
227        // Use format!("{:?}") since the source type may not impl Display (e.g., tuples).
228        if matches!(ty, TypeRef::String) {
229            if optional {
230                return format!("{name}: val.{name}.as_ref().map(|v| format!(\"{{:?}}\", v))");
231            }
232            return format!("{name}: format!(\"{{:?}}\", val.{name})");
233        }
234        // Fallback for truly unknown sanitized types
235        if optional {
236            return format!("{name}: val.{name}.as_ref().map(|v| format!(\"{{:?}}\", v))");
237        }
238        return format!("{name}: format!(\"{{:?}}\", val.{name})");
239    }
240    match ty {
241        // Duration: core uses std::time::Duration, binding uses u64 (millis)
242        TypeRef::Duration => {
243            if optional {
244                return format!("{name}: val.{name}.map(|d| d.as_millis() as u64)");
245            }
246            format!("{name}: val.{name}.as_millis() as u64")
247        }
248        // Path: core uses PathBuf, binding uses String — PathBuf→String needs special handling
249        TypeRef::Path => {
250            if optional {
251                format!("{name}: val.{name}.map(|p| p.to_string_lossy().to_string())")
252            } else {
253                format!("{name}: val.{name}.to_string_lossy().to_string()")
254            }
255        }
256        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Path) => {
257            format!("{name}: val.{name}.map(|p| p.to_string_lossy().to_string())")
258        }
259        // Char: core uses char, binding uses String — convert char to string
260        TypeRef::Char => {
261            if optional {
262                format!("{name}: val.{name}.map(|c| c.to_string())")
263            } else {
264                format!("{name}: val.{name}.to_string()")
265            }
266        }
267        // Bytes: core uses bytes::Bytes, binding uses Vec<u8>
268        TypeRef::Bytes => {
269            if optional {
270                format!("{name}: val.{name}.map(|v| v.to_vec())")
271            } else {
272                format!("{name}: val.{name}.to_vec()")
273            }
274        }
275        // Opaque Named types: wrap in Arc to create the binding wrapper
276        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
277            if optional {
278                format!("{name}: val.{name}.map(|v| {n} {{ inner: Arc::new(v) }})")
279            } else {
280                format!("{name}: {n} {{ inner: Arc::new(val.{name}) }}")
281            }
282        }
283        // Json: core uses serde_json::Value, binding uses String — use .to_string()
284        TypeRef::Json => {
285            if optional {
286                format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
287            } else {
288                format!("{name}: val.{name}.to_string()")
289            }
290        }
291        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Json) => {
292            format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
293        }
294        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json) => {
295            if optional {
296                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| i.to_string()).collect())")
297            } else {
298                format!("{name}: val.{name}.iter().map(ToString::to_string).collect()")
299            }
300        }
301        // Vec<Optional<Json>>: each element is Option<Value> → Option<String>
302        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Optional(oi) if matches!(oi.as_ref(), TypeRef::Json)) => {
303            if optional {
304                format!(
305                    "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| i.as_ref().map(ToString::to_string)).collect())"
306                )
307            } else {
308                format!("{name}: val.{name}.iter().map(|i| i.as_ref().map(ToString::to_string)).collect()")
309            }
310        }
311        // Map with Json values: core uses HashMap<K, serde_json::Value>, binding uses HashMap<K, String>
312        TypeRef::Map(k, v) if matches!(v.as_ref(), TypeRef::Json) => {
313            let k_is_json = matches!(k.as_ref(), TypeRef::Json);
314            let k_expr = if k_is_json { "k.to_string()" } else { "k" };
315            if optional {
316                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, v.to_string())).collect())")
317            } else {
318                format!("{name}: val.{name}.into_iter().map(|(k, v)| ({k_expr}, v.to_string())).collect()")
319            }
320        }
321        // Map with Json keys: core uses HashMap<serde_json::Value, V>, binding uses HashMap<String, V>
322        TypeRef::Map(k, _v) if matches!(k.as_ref(), TypeRef::Json) => {
323            if optional {
324                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.to_string(), v)).collect())")
325            } else {
326                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.to_string(), v)).collect()")
327            }
328        }
329        // Everything else is symmetric
330        _ => field_conversion_to_core(name, ty, optional),
331    }
332}
333
334/// Core→binding field conversion with backend-specific config.
335pub fn field_conversion_from_core_cfg(
336    name: &str,
337    ty: &TypeRef,
338    optional: bool,
339    sanitized: bool,
340    opaque_types: &AHashSet<String>,
341    config: &ConversionConfig,
342) -> String {
343    // Sanitized fields: for WASM (map_uses_jsvalue), Map and Vec<Json> fields target JsValue
344    // and need serde_wasm_bindgen::to_value() instead of iterator-based .collect().
345    // Note: Vec<String> sanitized does NOT use the JsValue path because Vec<String> maps to
346    // Vec<String> in WASM (not JsValue) — use the normal sanitized iterator path instead.
347    if sanitized {
348        if config.map_uses_jsvalue {
349            // Map(String, String) sanitized → JsValue (HashMap maps to JsValue in WASM)
350            if let TypeRef::Map(k, v) = ty {
351                if matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String) {
352                    if optional {
353                        return format!(
354                            "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())"
355                        );
356                    }
357                    return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
358                }
359            }
360            // Vec<Json> sanitized → JsValue (Vec<Json> maps to JsValue in WASM via nested-vec path)
361            if let TypeRef::Vec(inner) = ty {
362                if matches!(inner.as_ref(), TypeRef::Json) {
363                    if optional {
364                        return format!(
365                            "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())"
366                        );
367                    }
368                    return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
369                }
370            }
371        }
372        return field_conversion_from_core(name, ty, optional, sanitized, opaque_types);
373    }
374
375    // WASM JsValue: use serde_wasm_bindgen for Map and nested Vec types
376    if config.map_uses_jsvalue {
377        let is_nested_vec = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Vec(_)));
378        let is_map = matches!(ty, TypeRef::Map(_, _));
379        if is_nested_vec || is_map {
380            if optional {
381                return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())");
382            }
383            return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
384        }
385        if let TypeRef::Optional(inner) = ty {
386            let is_inner_nested = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Vec(_)));
387            let is_inner_map = matches!(inner.as_ref(), TypeRef::Map(_, _));
388            if is_inner_nested || is_inner_map {
389                return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())");
390            }
391        }
392    }
393
394    let prefix = config.type_name_prefix;
395    let is_enum_string = |n: &str| -> bool { config.enum_string_names.as_ref().is_some_and(|names| names.contains(n)) };
396
397    match ty {
398        // i64 casting for large int primitives
399        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
400            let cast_to = binding_prim_str(p);
401            if optional {
402                format!("{name}: val.{name}.map(|v| v as {cast_to})")
403            } else {
404                format!("{name}: val.{name} as {cast_to}")
405            }
406        }
407        // Optional(large_int) with i64 casting
408        TypeRef::Optional(inner)
409            if config.cast_large_ints_to_i64
410                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
411        {
412            if let TypeRef::Primitive(p) = inner.as_ref() {
413                let cast_to = binding_prim_str(p);
414                format!("{name}: val.{name}.map(|v| v as {cast_to})")
415            } else {
416                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
417            }
418        }
419        // f32→f64 casting (NAPI only)
420        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
421            if optional {
422                format!("{name}: val.{name}.map(|v| v as f64)")
423            } else {
424                format!("{name}: val.{name} as f64")
425            }
426        }
427        // Duration with i64 casting
428        TypeRef::Duration if config.cast_large_ints_to_i64 => {
429            if optional {
430                format!("{name}: val.{name}.map(|d| d.as_millis() as u64 as i64)")
431            } else {
432                format!("{name}: val.{name}.as_millis() as u64 as i64")
433            }
434        }
435        // Opaque Named types with prefix: wrap in Arc with prefixed binding name
436        TypeRef::Named(n) if opaque_types.contains(n.as_str()) && !prefix.is_empty() => {
437            let prefixed = format!("{prefix}{n}");
438            if optional {
439                format!("{name}: val.{name}.map(|v| {prefixed} {{ inner: Arc::new(v) }})")
440            } else {
441                format!("{name}: {prefixed} {{ inner: Arc::new(val.{name}) }}")
442            }
443        }
444        // Enum-to-String Named types (PHP pattern)
445        TypeRef::Named(n) if is_enum_string(n) => {
446            // Use serde serialization to get the correct serde(rename) value, not Debug format.
447            // serde_json::to_value gives Value::String("auto") which we extract.
448            if optional {
449                format!(
450                    "{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())"
451                )
452            } else {
453                format!(
454                    "{name}: serde_json::to_value(val.{name}).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default()"
455                )
456            }
457        }
458        // Vec<Enum-to-String> Named types: element-wise serde serialization
459        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(n) if is_enum_string(n)) => {
460            if optional {
461                format!(
462                    "{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())"
463                )
464            } else {
465                format!(
466                    "{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()"
467                )
468            }
469        }
470        // Optional(Vec<Enum-to-String>) Named types (PHP pattern)
471        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(n) if is_enum_string(n))) =>
472        {
473            format!(
474                "{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())"
475            )
476        }
477        // Vec<f32> needs element-wise cast to f64 when f32→f64 mapping is active
478        TypeRef::Vec(inner)
479            if config.cast_f32_to_f64 && matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32)) =>
480        {
481            if optional {
482                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
483            } else {
484                format!("{name}: val.{name}.iter().map(|&v| v as f64).collect()")
485            }
486        }
487        // Optional(Vec(f32)) needs element-wise cast to f64
488        TypeRef::Optional(inner)
489            if config.cast_f32_to_f64
490                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
491        {
492            format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
493        }
494        // Vec<Vec<f32>> needs nested element-wise cast to f64 (for embeddings, etc.)
495        TypeRef::Vec(outer)
496            if config.cast_f32_to_f64
497                && matches!(outer.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
498        {
499            if optional {
500                format!(
501                    "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect())"
502                )
503            } else {
504                format!("{name}: val.{name}.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect()")
505            }
506        }
507        // Optional(Vec<Vec<f32>>) needs nested element-wise cast to f64
508        TypeRef::Optional(inner)
509            if config.cast_f32_to_f64
510                && matches!(inner.as_ref(), TypeRef::Vec(outer) if matches!(outer.as_ref(), TypeRef::Vec(prim) if matches!(prim.as_ref(), TypeRef::Primitive(PrimitiveType::F32)))) =>
511        {
512            format!(
513                "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect())"
514            )
515        }
516        // Optional with i64-cast inner
517        TypeRef::Optional(inner)
518            if config.cast_large_ints_to_i64
519                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
520        {
521            if let TypeRef::Primitive(p) = inner.as_ref() {
522                let cast_to = binding_prim_str(p);
523                format!("{name}: val.{name}.map(|v| v as {cast_to})")
524            } else {
525                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
526            }
527        }
528        // HashMap value type casting: when value type needs i64 casting
529        TypeRef::Map(k, v)
530            if config.cast_large_ints_to_i64 && matches!(v.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
531        {
532            if let TypeRef::Primitive(p) = v.as_ref() {
533                let cast_to = binding_prim_str(p);
534                if optional {
535                    format!(
536                        "{name}: val.{name}.as_ref().map(|m| m.iter().map(|(k, v)| (k.clone(), *v as {cast_to})).collect())"
537                    )
538                } else {
539                    format!("{name}: val.{name}.iter().map(|(k, v)| (k.clone(), *v as {cast_to})).collect()")
540                }
541            } else {
542                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
543            }
544        }
545        // Vec<u64/usize/isize> needs element-wise i64 casting (core→binding)
546        TypeRef::Vec(inner)
547            if config.cast_large_ints_to_i64
548                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
549        {
550            if let TypeRef::Primitive(p) = inner.as_ref() {
551                let cast_to = binding_prim_str(p);
552                if optional {
553                    format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as {cast_to}).collect())")
554                } else {
555                    format!("{name}: val.{name}.iter().map(|&v| v as {cast_to}).collect()")
556                }
557            } else {
558                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
559            }
560        }
561        // Vec<Vec<u64/usize/isize>> needs nested element-wise i64 casting (core→binding)
562        TypeRef::Vec(outer)
563            if config.cast_large_ints_to_i64
564                && matches!(outer.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p))) =>
565        {
566            if let TypeRef::Vec(inner) = outer.as_ref() {
567                if let TypeRef::Primitive(p) = inner.as_ref() {
568                    let cast_to = binding_prim_str(p);
569                    if optional {
570                        format!(
571                            "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as {cast_to}).collect()).collect())"
572                        )
573                    } else {
574                        format!(
575                            "{name}: val.{name}.iter().map(|inner| inner.iter().map(|&x| x as {cast_to}).collect()).collect()"
576                        )
577                    }
578                } else {
579                    field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
580                }
581            } else {
582                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
583            }
584        }
585        // Json→String: core uses serde_json::Value, binding uses String (PHP)
586        TypeRef::Json if config.json_to_string => {
587            if optional {
588                format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
589            } else {
590                format!("{name}: val.{name}.to_string()")
591            }
592        }
593        // Json→JsValue: core uses serde_json::Value, binding uses JsValue (WASM)
594        TypeRef::Json if config.map_uses_jsvalue => {
595            if optional {
596                format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
597            } else {
598                format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)")
599            }
600        }
601        // Vec<Json>→JsValue: core uses Vec<serde_json::Value>, binding uses JsValue (WASM)
602        TypeRef::Vec(inner) if config.map_uses_jsvalue && matches!(inner.as_ref(), TypeRef::Json) => {
603            if optional {
604                format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
605            } else {
606                format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)")
607            }
608        }
609        // Optional(Vec<Json>)→JsValue (WASM)
610        TypeRef::Optional(inner)
611            if config.map_uses_jsvalue
612                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Json)) =>
613        {
614            format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
615        }
616        // Fall through to default (handles paths, opaque without prefix, etc.)
617        _ => field_conversion_from_core(name, ty, optional, sanitized, opaque_types),
618    }
619}
620
621/// Apply CoreWrapper transformations for core→binding direction.
622/// Unwraps Arc, converts Cow→String, Bytes→Vec<u8>.
623fn apply_core_wrapper_from_core(
624    conversion: &str,
625    name: &str,
626    core_wrapper: &CoreWrapper,
627    vec_inner_core_wrapper: &CoreWrapper,
628    optional: bool,
629) -> String {
630    // Handle Vec<Arc<T>>: unwrap Arc elements
631    if *vec_inner_core_wrapper == CoreWrapper::Arc {
632        return conversion
633            .replace(".map(Into::into).collect()", ".map(|v| (*v).clone().into()).collect()")
634            .replace(
635                "map(|v| v.into_iter().map(Into::into)",
636                "map(|v| v.into_iter().map(|v| (*v).clone().into())",
637            );
638    }
639
640    match core_wrapper {
641        CoreWrapper::None => conversion.to_string(),
642        CoreWrapper::Cow => {
643            // Cow<str> → String: core val.name is Cow, binding needs String
644            // The conversion already emits "name: val.name" for strings which works
645            // since Cow<str> derefs to &str and String: From<Cow<str>> exists.
646            // But if it's "val.name" directly, add .into_owned() or .to_string()
647            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
648                if optional {
649                    // Already handled by map
650                    conversion.to_string()
651                } else if expr == format!("val.{name}") {
652                    format!("{name}: val.{name}.into_owned()")
653                } else {
654                    conversion.to_string()
655                }
656            } else {
657                conversion.to_string()
658            }
659        }
660        CoreWrapper::Arc => {
661            // Arc<T> → T: unwrap via clone
662            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
663                if optional {
664                    format!("{name}: {expr}.map(|v| (*v).clone().into())")
665                } else {
666                    let unwrapped = expr.replace(&format!("val.{name}"), &format!("(*val.{name}).clone()"));
667                    format!("{name}: {unwrapped}")
668                }
669            } else {
670                conversion.to_string()
671            }
672        }
673        CoreWrapper::Bytes => {
674            // Bytes → Vec<u8>: .to_vec()
675            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
676                if optional {
677                    format!("{name}: {expr}.map(|v| v.to_vec())")
678                } else if expr == format!("val.{name}") {
679                    format!("{name}: val.{name}.to_vec()")
680                } else {
681                    conversion.to_string()
682                }
683            } else {
684                conversion.to_string()
685            }
686        }
687    }
688}