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            // Skip cfg-gated fields — they don't exist in the binding struct.
68            if field.cfg.is_some() {
69                continue;
70            }
71            if field.sanitized {
72                // sanitized fields keep the default value — skip
73                continue;
74            }
75            // Fields referencing excluded types keep their default value — skip
76            if !config.exclude_types.is_empty()
77                && super::helpers::field_references_excluded_type(&field.ty, config.exclude_types)
78            {
79                continue;
80            }
81            // Duration field stored as Option<u64/i64>: only override when Some
82            if !field.optional && matches!(field.ty, TypeRef::Duration) {
83                let cast = if config.cast_large_ints_to_i64 { " as u64" } else { "" };
84                writeln!(
85                    out,
86                    "        if let Some(__v) = val.{} {{ __result.{} = std::time::Duration::from_millis(__v{cast}); }}",
87                    field.name, field.name
88                )
89                .ok();
90                continue;
91            }
92            let conversion = if optionalized && !field.optional {
93                gen_optionalized_field_to_core(&field.name, &field.ty, config)
94            } else {
95                field_conversion_to_core_cfg(&field.name, &field.ty, field.optional, config)
96            };
97            // Strip the "name: " prefix to get just the expression, then assign
98            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
99                writeln!(out, "        __result.{} = {};", field.name, expr).ok();
100            }
101        }
102        writeln!(out, "        __result").ok();
103        writeln!(out, "    }}").ok();
104        write!(out, "}}").ok();
105        return out;
106    }
107
108    writeln!(out, "        Self {{").ok();
109    let optionalized = config.optionalize_defaults && typ.has_default;
110    for field in &typ.fields {
111        // Skip cfg-gated fields — they don't exist in the binding struct.
112        // When the binding is compiled, these fields are absent, and accessing them would fail.
113        // The ..Default::default() at the end fills in these fields when the core type is compiled
114        // with the required feature enabled.
115        if field.cfg.is_some() {
116            continue;
117        }
118        // Fields referencing excluded types don't exist in the binding struct.
119        // When the type has stripped cfg-gated fields, these fields may also be
120        // cfg-gated and absent from the core struct — skip them entirely and let
121        // ..Default::default() fill them in.
122        // Otherwise, use Default::default() to fill them in the core type.
123        // Sanitized fields also use Default::default() (lossy but functional).
124        let references_excluded = !config.exclude_types.is_empty()
125            && super::helpers::field_references_excluded_type(&field.ty, config.exclude_types);
126        if references_excluded && typ.has_stripped_cfg_fields {
127            continue;
128        }
129        let conversion = if field.sanitized || references_excluded {
130            // Sanitized Vec<Primitive> fields come from homogeneous numeric tuples (e.g. (u32,u32)→Vec<u32>).
131            // Use serde round-trip instead of Default::default() so the binding value flows through
132            // correctly: Vec<u32> serializes as a JSON array, which tuples also deserialize from.
133            if !references_excluded && is_sanitized_vec_primitive(&field.ty) {
134                gen_sanitized_vec_primitive_to_core(&field.name, &field.ty, field.optional)
135            } else {
136                format!("{}: Default::default()", field.name)
137            }
138        } else if optionalized && !field.optional {
139            // Field was wrapped in Option<T> for JS ergonomics but core expects T.
140            // Use unwrap_or_default() for simple types, unwrap_or_default() + into for Named.
141            gen_optionalized_field_to_core(&field.name, &field.ty, config)
142        } else {
143            field_conversion_to_core_cfg(&field.name, &field.ty, field.optional, config)
144        };
145        // Newtype wrapping: when the field was resolved from a newtype (e.g. NodeIndex → u32),
146        // wrap the binding value back into the newtype for the core struct.
147        // e.g. `source: val.source` → `source: kreuzberg::NodeIndex(val.source)`
148        //      `parent: val.parent` → `parent: val.parent.map(kreuzberg::NodeIndex)`
149        //      `children: val.children` → `children: val.children.into_iter().map(kreuzberg::NodeIndex).collect()`
150        let conversion = if let Some(newtype_path) = &field.newtype_wrapper {
151            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
152                // When `optional=true` and `ty` is a plain Primitive (not TypeRef::Optional), the core
153                // field is actually `Option<NewtypeT>`, so we must use `.map(NewtypeT)` not `NewtypeT(...)`.
154                match &field.ty {
155                    TypeRef::Optional(_) => format!("{}: ({expr}).map({newtype_path})", field.name),
156                    TypeRef::Vec(_) => {
157                        format!("{}: ({expr}).into_iter().map({newtype_path}).collect()", field.name)
158                    }
159                    _ if field.optional => format!("{}: ({expr}).map({newtype_path})", field.name),
160                    _ => format!("{}: {newtype_path}({expr})", field.name),
161                }
162            } else {
163                conversion
164            }
165        } else {
166            conversion
167        };
168        // Box<T> fields: wrap the converted value in Box::new()
169        let conversion = if field.is_boxed && matches!(&field.ty, TypeRef::Named(_)) {
170            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
171                if field.optional {
172                    // Option<Box<T>> field: map inside the Option
173                    format!("{}: {}.map(Box::new)", field.name, expr)
174                } else {
175                    format!("{}: Box::new({})", field.name, expr)
176                }
177            } else {
178                conversion
179            }
180        } else {
181            conversion
182        };
183        // CoreWrapper: apply Cow/Arc/Bytes wrapping for binding→core direction
184        let conversion = apply_core_wrapper_to_core(
185            &conversion,
186            &field.name,
187            &field.core_wrapper,
188            &field.vec_inner_core_wrapper,
189            field.optional,
190        );
191        writeln!(out, "            {conversion},").ok();
192    }
193    // Use ..Default::default() to fill cfg-gated fields stripped from the IR
194    if typ.has_stripped_cfg_fields {
195        writeln!(out, "            ..Default::default()").ok();
196    }
197    writeln!(out, "        }}").ok();
198    writeln!(out, "    }}").ok();
199    write!(out, "}}").ok();
200    out
201}
202
203/// Generate field conversion for a non-optional field that was optionalized
204/// (wrapped in Option<T>) in the binding struct for JS ergonomics.
205pub(super) fn gen_optionalized_field_to_core(name: &str, ty: &TypeRef, config: &ConversionConfig) -> String {
206    match ty {
207        TypeRef::Json => {
208            format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or_default()")
209        }
210        TypeRef::Named(_) => {
211            // Named type: unwrap Option, convert via .into(), or use Default
212            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
213        }
214        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
215            format!("{name}: val.{name}.map(|v| v as f32).unwrap_or(0.0)")
216        }
217        TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => {
218            format!("{name}: val.{name}.unwrap_or(0.0)")
219        }
220        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
221            let core_ty = core_prim_str(p);
222            format!("{name}: val.{name}.map(|v| v as {core_ty}).unwrap_or_default()")
223        }
224        TypeRef::Optional(inner)
225            if config.cast_large_ints_to_i64
226                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
227        {
228            if let TypeRef::Primitive(p) = inner.as_ref() {
229                let core_ty = core_prim_str(p);
230                format!("{name}: val.{name}.map(|v| v as {core_ty})")
231            } else {
232                field_conversion_to_core(name, ty, false)
233            }
234        }
235        TypeRef::Duration if config.cast_large_ints_to_i64 => {
236            format!("{name}: val.{name}.map(|v| std::time::Duration::from_millis(v as u64)).unwrap_or_default()")
237        }
238        TypeRef::Duration => {
239            format!("{name}: val.{name}.map(std::time::Duration::from_millis).unwrap_or_default()")
240        }
241        TypeRef::Path => {
242            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
243        }
244        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Path) => {
245            // Binding has Option<String>, core has Option<PathBuf>
246            format!("{name}: val.{name}.map(|s| std::path::PathBuf::from(s))")
247        }
248        TypeRef::Optional(_) => {
249            // Field was flattened from Option<Option<T>> to Option<T> in the binding struct.
250            // Core expects Option<Option<T>>, so wrap with .map(Some) to reconstruct.
251            format!("{name}: val.{name}.map(Some)")
252        }
253        // Char: binding uses Option<String>, core uses char
254        TypeRef::Char => {
255            format!("{name}: val.{name}.and_then(|s| s.chars().next()).unwrap_or('*')")
256        }
257        TypeRef::Vec(inner) => match inner.as_ref() {
258            TypeRef::Json => {
259                format!(
260                    "{name}: val.{name}.map(|v| v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()).unwrap_or_default()"
261                )
262            }
263            TypeRef::Named(_) => {
264                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect()).unwrap_or_default()")
265            }
266            TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
267                let core_ty = core_prim_str(p);
268                format!(
269                    "{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect()).unwrap_or_default()"
270                )
271            }
272            _ => format!("{name}: val.{name}.unwrap_or_default()"),
273        },
274        TypeRef::Map(k, v) if matches!(v.as_ref(), TypeRef::Json) => {
275            // Map with Json values: binding uses HashMap<K, String>, core uses HashMap<K, serde_json::Value>
276            let k_is_json = matches!(k.as_ref(), TypeRef::Json);
277            let k_expr = if k_is_json {
278                "serde_json::from_str(&k).unwrap_or_default()"
279            } else {
280                "k"
281            };
282            format!(
283                "{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()"
284            )
285        }
286        TypeRef::Map(k, _v) if matches!(k.as_ref(), TypeRef::Json) => {
287            // Map with Json keys: binding uses HashMap<String, V>, core uses HashMap<serde_json::Value, V>
288            format!(
289                "{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| (serde_json::from_str(&k).unwrap_or_default(), v)).collect()"
290            )
291        }
292        TypeRef::Map(k, v) => {
293            // Collect to handle HashMap↔BTreeMap conversion + value type conversion
294            let has_named_val = matches!(v.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
295            if has_named_val {
296                format!("{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| (k, v.into())).collect()")
297            } else if matches!(k.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n)) {
298                format!("{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| (k.into(), v)).collect()")
299            } else {
300                format!("{name}: val.{name}.unwrap_or_default().into_iter().collect()")
301            }
302        }
303        _ => {
304            // Simple types (primitives, String, etc): unwrap_or_default()
305            format!("{name}: val.{name}.unwrap_or_default()")
306        }
307    }
308}
309
310/// Determine the field conversion expression for binding -> core.
311pub fn field_conversion_to_core(name: &str, ty: &TypeRef, optional: bool) -> String {
312    match ty {
313        // Primitives, String, Bytes, Unit -- direct assignment
314        TypeRef::Primitive(_) | TypeRef::String | TypeRef::Bytes | TypeRef::Unit => {
315            format!("{name}: val.{name}")
316        }
317        // Json: binding uses String, core uses serde_json::Value — parse or default
318        TypeRef::Json => {
319            if optional {
320                format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())")
321            } else {
322                format!("{name}: serde_json::from_str(&val.{name}).unwrap_or_default()")
323            }
324        }
325        // Char: binding uses String, core uses char — convert first character
326        TypeRef::Char => {
327            if optional {
328                format!("{name}: val.{name}.and_then(|s| s.chars().next())")
329            } else {
330                format!("{name}: val.{name}.chars().next().unwrap_or('*')")
331            }
332        }
333        // Duration: binding uses u64 (millis), core uses std::time::Duration
334        TypeRef::Duration => {
335            if optional {
336                format!("{name}: val.{name}.map(std::time::Duration::from_millis)")
337            } else {
338                format!("{name}: std::time::Duration::from_millis(val.{name})")
339            }
340        }
341        // Path needs .into() — binding uses String, core uses PathBuf
342        TypeRef::Path => {
343            if optional {
344                format!("{name}: val.{name}.map(Into::into)")
345            } else {
346                format!("{name}: val.{name}.into()")
347            }
348        }
349        // Named type -- needs .into() to convert between binding and core types
350        // Tuple types (e.g., "(String, String)") are passthrough — no conversion needed
351        TypeRef::Named(type_name) if is_tuple_type_name(type_name) => {
352            format!("{name}: val.{name}")
353        }
354        TypeRef::Named(_) => {
355            if optional {
356                format!("{name}: val.{name}.map(Into::into)")
357            } else {
358                format!("{name}: val.{name}.into()")
359            }
360        }
361        // Map with Json value type: binding uses HashMap<K, String>, core uses HashMap<K, Value>
362        TypeRef::Map(k, v) if matches!(v.as_ref(), TypeRef::Json) => {
363            let k_expr = if matches!(k.as_ref(), TypeRef::Json) {
364                "serde_json::from_str(&k).unwrap_or_default()"
365            } else {
366                "k"
367            };
368            if optional {
369                format!(
370                    "{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, serde_json::from_str(&v).unwrap_or_default())).collect())"
371                )
372            } else {
373                format!(
374                    "{name}: val.{name}.into_iter().map(|(k, v)| ({k_expr}, serde_json::from_str(&v).unwrap_or_default())).collect()"
375                )
376            }
377        }
378        // Optional with inner
379        TypeRef::Optional(inner) => match inner.as_ref() {
380            TypeRef::Json => format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())"),
381            TypeRef::Named(_) | TypeRef::Path => format!("{name}: val.{name}.map(Into::into)"),
382            TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_)) => {
383                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
384            }
385            _ => format!("{name}: val.{name}"),
386        },
387        // Vec of named or Json types -- map each element
388        TypeRef::Vec(inner) => match inner.as_ref() {
389            TypeRef::Json => {
390                if optional {
391                    format!(
392                        "{name}: val.{name}.map(|v| v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect())"
393                    )
394                } else {
395                    format!("{name}: val.{name}.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()")
396                }
397            }
398            // Vec<(T1, T2)> — tuples are passthrough
399            TypeRef::Named(type_name) if is_tuple_type_name(type_name) => {
400                format!("{name}: val.{name}")
401            }
402            TypeRef::Named(_) => {
403                if optional {
404                    format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
405                } else {
406                    format!("{name}: val.{name}.into_iter().map(Into::into).collect()")
407                }
408            }
409            _ => format!("{name}: val.{name}"),
410        },
411        // Map -- collect to handle HashMap↔BTreeMap conversion;
412        // additionally convert Named keys/values via Into, Json values via serde.
413        TypeRef::Map(k, v) => {
414            let has_named_key = matches!(k.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
415            let has_named_val = matches!(v.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
416            let has_json_val = matches!(v.as_ref(), TypeRef::Json);
417            let has_json_key = matches!(k.as_ref(), TypeRef::Json);
418            // Vec<Named> values: each vector element needs Into conversion.
419            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)));
420            // Vec<Json> values: each element needs serde deserialization.
421            let has_vec_json_val = matches!(v.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json));
422            if has_json_val || has_json_key || has_named_key || has_named_val || has_vec_named_val || has_vec_json_val {
423                let k_expr = if has_json_key {
424                    "serde_json::from_str(&k).unwrap_or(serde_json::Value::String(k))"
425                } else if has_named_key {
426                    "k.into()"
427                } else {
428                    "k"
429                };
430                let v_expr = if has_json_val {
431                    "serde_json::from_str(&v).unwrap_or(serde_json::Value::String(v))"
432                } else if has_named_val {
433                    "v.into()"
434                } else if has_vec_named_val {
435                    "v.into_iter().map(Into::into).collect()"
436                } else if has_vec_json_val {
437                    "v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()"
438                } else {
439                    "v"
440                };
441                if optional {
442                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect())")
443                } else {
444                    format!("{name}: val.{name}.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect()")
445                }
446            } else {
447                // No conversion needed — just collect for potential HashMap↔BTreeMap type change
448                if optional {
449                    format!("{name}: val.{name}.map(|m| m.into_iter().collect())")
450                } else {
451                    format!("{name}: val.{name}.into_iter().collect()")
452                }
453            }
454        }
455    }
456}
457
458/// Binding→core field conversion with backend-specific config (i64 casts, etc.).
459pub fn field_conversion_to_core_cfg(name: &str, ty: &TypeRef, optional: bool, config: &ConversionConfig) -> String {
460    // When optional=true and ty=Optional(T), the binding field was flattened from
461    // Option<Option<T>> to Option<T>. Core expects Option<Option<T>>, so wrap with .map(Some).
462    // This applies regardless of cast config; handle before any other dispatch.
463    if optional && matches!(ty, TypeRef::Optional(_)) {
464        // Delegate to get the inner Optional(T) → Option<T> conversion (with optional=false,
465        // since the outer Option is handled by the .map(Some) we add here).
466        let inner_expr = field_conversion_to_core_cfg(name, ty, false, config);
467        // inner_expr is "name: <expr-for-Option<T>>"; wrap it with .map(Some)
468        if let Some(expr) = inner_expr.strip_prefix(&format!("{name}: ")) {
469            return format!("{name}: ({expr}).map(Some)");
470        }
471        return inner_expr;
472    }
473
474    // WASM JsValue: use serde_wasm_bindgen for Map, nested Vec, and Vec<Json> types
475    if config.map_uses_jsvalue {
476        let is_nested_vec = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Vec(_)));
477        let is_vec_json = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json));
478        let is_map = matches!(ty, TypeRef::Map(_, _));
479        if is_nested_vec || is_map || is_vec_json {
480            if optional {
481                return format!(
482                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
483                );
484            }
485            return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
486        }
487        if let TypeRef::Optional(inner) = ty {
488            let is_inner_nested = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Vec(_)));
489            let is_inner_vec_json = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Json));
490            let is_inner_map = matches!(inner.as_ref(), TypeRef::Map(_, _));
491            if is_inner_nested || is_inner_map || is_inner_vec_json {
492                return format!(
493                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
494                );
495            }
496        }
497    }
498
499    // Vec<T>→String binding→core: binding holds JSON string, core expects Vec<T>.
500    // field_type_for_serde collapses any Vec<T> not explicitly handled (including Vec<String>,
501    // Vec<Named>, etc.) to String. Apply serde round-trip for all Vec types.
502    if config.vec_named_to_string {
503        if let TypeRef::Vec(_) = ty {
504            if optional {
505                return format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())");
506            }
507            return format!("{name}: serde_json::from_str(&val.{name}).unwrap_or_default()");
508        }
509    }
510    // Json→String binding→core: use Default::default() (lossy — can't parse String back)
511    if config.json_to_string && matches!(ty, TypeRef::Json) {
512        return format!("{name}: Default::default()");
513    }
514    // Json→JsValue binding→core: use serde_wasm_bindgen to convert (WASM)
515    if config.map_uses_jsvalue && matches!(ty, TypeRef::Json) {
516        if optional {
517            return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())");
518        }
519        return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
520    }
521    if !config.cast_large_ints_to_i64
522        && !config.cast_f32_to_f64
523        && !config.json_to_string
524        && !config.vec_named_to_string
525    {
526        return field_conversion_to_core(name, ty, optional);
527    }
528    // Cast mode: handle primitives and Duration differently
529    match ty {
530        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
531            let core_ty = core_prim_str(p);
532            if optional {
533                format!("{name}: val.{name}.map(|v| v as {core_ty})")
534            } else {
535                format!("{name}: val.{name} as {core_ty}")
536            }
537        }
538        // f64→f32 cast (NAPI binding f64 → core f32)
539        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
540            if optional {
541                format!("{name}: val.{name}.map(|v| v as f32)")
542            } else {
543                format!("{name}: val.{name} as f32")
544            }
545        }
546        TypeRef::Duration if config.cast_large_ints_to_i64 => {
547            if optional {
548                format!("{name}: val.{name}.map(|v| std::time::Duration::from_millis(v as u64))")
549            } else {
550                format!("{name}: std::time::Duration::from_millis(val.{name} as u64)")
551            }
552        }
553        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) => {
554            if let TypeRef::Primitive(p) = inner.as_ref() {
555                let core_ty = core_prim_str(p);
556                format!("{name}: val.{name}.map(|v| v as {core_ty})")
557            } else {
558                field_conversion_to_core(name, ty, optional)
559            }
560        }
561        // Vec<u64/usize/isize> needs element-wise i64→core casting
562        TypeRef::Vec(inner)
563            if config.cast_large_ints_to_i64
564                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
565        {
566            if let TypeRef::Primitive(p) = inner.as_ref() {
567                let core_ty = core_prim_str(p);
568                if optional {
569                    format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
570                } else {
571                    format!("{name}: val.{name}.into_iter().map(|v| v as {core_ty}).collect()")
572                }
573            } else {
574                field_conversion_to_core(name, ty, optional)
575            }
576        }
577        // HashMap value type casting: when value type needs i64→core casting
578        TypeRef::Map(_k, v)
579            if config.cast_large_ints_to_i64 && matches!(v.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
580        {
581            if let TypeRef::Primitive(p) = v.as_ref() {
582                let core_ty = core_prim_str(p);
583                if optional {
584                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v as {core_ty})).collect())")
585                } else {
586                    format!("{name}: val.{name}.into_iter().map(|(k, v)| (k, v as {core_ty})).collect()")
587                }
588            } else {
589                field_conversion_to_core(name, ty, optional)
590            }
591        }
592        // Vec<f32> needs element-wise cast when f32→f64 mapping is active (NAPI)
593        TypeRef::Vec(inner)
594            if config.cast_f32_to_f64 && matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32)) =>
595        {
596            if optional {
597                format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
598            } else {
599                format!("{name}: val.{name}.into_iter().map(|v| v as f32).collect()")
600            }
601        }
602        // Optional(Vec(f32)) needs element-wise cast (NAPI only)
603        TypeRef::Optional(inner)
604            if config.cast_f32_to_f64
605                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
606        {
607            format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
608        }
609        // Fall through to default for everything else
610        _ => field_conversion_to_core(name, ty, optional),
611    }
612}
613
614/// Apply CoreWrapper transformations to a binding→core conversion expression.
615/// Wraps the value expression with Arc::new(), .into() for Cow, etc.
616fn apply_core_wrapper_to_core(
617    conversion: &str,
618    name: &str,
619    core_wrapper: &CoreWrapper,
620    vec_inner_core_wrapper: &CoreWrapper,
621    optional: bool,
622) -> String {
623    // Handle Vec<Arc<T>>: replace .map(Into::into) with .map(|v| std::sync::Arc::new(v.into()))
624    if *vec_inner_core_wrapper == CoreWrapper::Arc {
625        return conversion
626            .replace(
627                ".map(Into::into).collect()",
628                ".map(|v| std::sync::Arc::new(v.into())).collect()",
629            )
630            .replace(
631                "map(|v| v.into_iter().map(Into::into)",
632                "map(|v| v.into_iter().map(|v| std::sync::Arc::new(v.into()))",
633            );
634    }
635
636    match core_wrapper {
637        CoreWrapper::None => conversion.to_string(),
638        CoreWrapper::Cow => {
639            // Cow<str>: binding String → core Cow via .into()
640            // The field_conversion already emits "name: val.name" for strings,
641            // we need to add .into() to convert String → Cow<'static, str>
642            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
643                if optional {
644                    format!("{name}: {expr}.map(Into::into)")
645                } else if expr == format!("val.{name}") {
646                    format!("{name}: val.{name}.into()")
647                } else if expr == "Default::default()" {
648                    // Sanitized field: Default::default() already resolves to the correct core type
649                    // (e.g. Cow<'static, str> — adding .into() breaks type inference).
650                    conversion.to_string()
651                } else {
652                    format!("{name}: ({expr}).into()")
653                }
654            } else {
655                conversion.to_string()
656            }
657        }
658        CoreWrapper::Arc => {
659            // Arc<T>: wrap with Arc::new()
660            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
661                if expr == "Default::default()" {
662                    // Sanitized field: Default::default() resolves to the correct core type;
663                    // wrapping in Arc::new() would change the type.
664                    conversion.to_string()
665                } else if optional {
666                    format!("{name}: {expr}.map(|v| std::sync::Arc::new(v))")
667                } else {
668                    format!("{name}: std::sync::Arc::new({expr})")
669                }
670            } else {
671                conversion.to_string()
672            }
673        }
674        CoreWrapper::Bytes => {
675            // Bytes: binding Vec<u8> → core Bytes via .into()
676            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
677                if optional {
678                    format!("{name}: {expr}.map(Into::into)")
679                } else if expr == format!("val.{name}") {
680                    format!("{name}: val.{name}.into()")
681                } else if expr == "Default::default()" {
682                    // Sanitized field: Default::default() already resolves to the correct core type
683                    // (e.g. bytes::Bytes — adding .into() breaks type inference).
684                    conversion.to_string()
685                } else {
686                    format!("{name}: ({expr}).into()")
687                }
688            } else {
689                conversion.to_string()
690            }
691        }
692        CoreWrapper::ArcMutex => {
693            // ArcMutex: binding T → core Arc<Mutex<T>> via Arc::new(Mutex::new())
694            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
695                if optional {
696                    format!("{name}: {expr}.map(|v| std::sync::Arc::new(std::sync::Mutex::new(v.into())))")
697                } else if expr == format!("val.{name}") {
698                    format!("{name}: std::sync::Arc::new(std::sync::Mutex::new(val.{name}.into()))")
699                } else {
700                    format!("{name}: std::sync::Arc::new(std::sync::Mutex::new(({expr}).into()))")
701                }
702            } else {
703                conversion.to_string()
704            }
705        }
706    }
707}
708
709/// Returns true if this is a sanitized tuple-to-vec field: `Vec(Primitive(_))` or
710/// `Optional(Vec(Primitive(_)))`. These come from homogeneous numeric tuples such as
711/// `(u32, u32)` which `sanitize_type_ref` maps to `Vec<u32>`.
712fn is_sanitized_vec_primitive(ty: &TypeRef) -> bool {
713    match ty {
714        TypeRef::Vec(inner) => matches!(inner.as_ref(), TypeRef::Primitive(_)),
715        TypeRef::Optional(inner) => {
716            matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(_)))
717        }
718        _ => false,
719    }
720}
721
722/// Generate binding→core conversion for a sanitized Vec<Primitive> field (tuple round-trip).
723/// Uses serde JSON round-trip: Vec<P> serializes as a JSON array, which tuples also parse from.
724fn gen_sanitized_vec_primitive_to_core(name: &str, ty: &TypeRef, optional: bool) -> String {
725    // Optional(Vec(Primitive)): binding holds Option<Vec<P>>, core expects Option<(P, P, ...)>
726    let is_option_vec = matches!(ty, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(_))));
727    if is_option_vec || optional {
728        return format!(
729            "{name}: val.{name}.as_ref().and_then(|v| serde_json::to_value(v).ok()).and_then(|v| serde_json::from_value(v).ok())"
730        );
731    }
732    // Non-optional Vec<Primitive>: binding holds Vec<P>, core expects (P, P, ...)
733    format!(
734        "{name}: serde_json::to_value(&val.{name}).ok().and_then(|v| serde_json::from_value(v).ok()).unwrap_or_default()"
735    )
736}