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