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