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