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