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::{
6    core_prim_str, core_type_path_remapped, is_newtype, is_tuple_type_name, needs_f64_cast, needs_i32_cast,
7    needs_i64_cast,
8};
9
10/// Generate `impl From<BindingType> for core::Type` (binding -> core).
11/// Sanitized fields use `Default::default()` unless the sanitizer only removed a
12/// core wrapper that can be reconstructed losslessly from the binding value.
13pub fn gen_from_binding_to_core(typ: &TypeDef, core_import: &str) -> String {
14    gen_from_binding_to_core_cfg(typ, core_import, &ConversionConfig::default())
15}
16
17/// Generate `impl From<BindingType> for core::Type` with backend-specific config.
18pub fn gen_from_binding_to_core_cfg(typ: &TypeDef, core_import: &str, config: &ConversionConfig) -> String {
19    let core_path = core_type_path_remapped(typ, core_import, config.source_crate_remaps);
20    let binding_name = format!("{}{}", config.type_name_prefix, typ.name);
21    let mut out = String::with_capacity(256);
22    // When cfg-gated fields exist, ..Default::default() fills them when the feature is enabled.
23    // When disabled, all fields are already specified and the update has no effect — suppress lint.
24    if typ.has_stripped_cfg_fields {
25        writeln!(out, "#[allow(clippy::needless_update)]").ok();
26    }
27    // Suppress clippy when we use the builder pattern (Default + field reassignment).
28    // Two paths use this pattern:
29    //   1. option_duration_on_defaults: non-optional Duration fields stored as Option<u64>
30    //   2. optionalize_defaults: all fields of has_default types wrapped in Option<T>
31    let uses_builder_pattern = (config.option_duration_on_defaults
32        && typ.has_default
33        && typ
34            .fields
35            .iter()
36            .any(|f| !f.optional && matches!(f.ty, TypeRef::Duration)))
37        || (config.optionalize_defaults && typ.has_default);
38    if uses_builder_pattern {
39        writeln!(
40            out,
41            "#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]"
42        )
43        .ok();
44    }
45    writeln!(out, "#[allow(clippy::redundant_closure, clippy::useless_conversion)]").ok();
46    writeln!(out, "impl From<{binding_name}> for {core_path} {{").ok();
47    writeln!(out, "    fn from(val: {binding_name}) -> Self {{").ok();
48
49    // Newtype structs: generate tuple constructor Self(val._0)
50    if is_newtype(typ) {
51        let field = &typ.fields[0];
52        let inner_expr = match &field.ty {
53            TypeRef::Named(_) => "val._0.into()".to_string(),
54            TypeRef::Path => "val._0.into()".to_string(),
55            TypeRef::Duration => "std::time::Duration::from_millis(val._0)".to_string(),
56            _ => "val._0".to_string(),
57        };
58        writeln!(out, "        Self({inner_expr})").ok();
59        writeln!(out, "    }}").ok();
60        write!(out, "}}").ok();
61        return out;
62    }
63
64    // When option_duration_on_defaults is set for a has_default type, non-optional Duration
65    // fields are stored as Option<u64> in the binding struct.  We use the builder pattern
66    // so that None falls back to the core type's Default (giving the real field default,
67    // e.g. Duration::from_millis(30000)) rather than Duration::ZERO.
68    let has_optionalized_duration = config.option_duration_on_defaults
69        && typ.has_default
70        && typ
71            .fields
72            .iter()
73            .any(|f| !f.optional && matches!(f.ty, TypeRef::Duration));
74
75    if has_optionalized_duration {
76        // Builder pattern: start from core default, override explicitly-set fields.
77        writeln!(out, "        let mut __result = {core_path}::default();").ok();
78        let optionalized = config.optionalize_defaults && typ.has_default;
79        for field in &typ.fields {
80            // Skip cfg-gated fields — they don't exist in the binding struct.
81            if field.cfg.is_some() {
82                continue;
83            }
84            if field.sanitized && field.core_wrapper != CoreWrapper::Cow {
85                // sanitized fields keep the default value — skip
86                continue;
87            }
88            // Fields referencing excluded types keep their default value — skip
89            if !config.exclude_types.is_empty()
90                && super::helpers::field_references_excluded_type(&field.ty, config.exclude_types)
91            {
92                continue;
93            }
94            // Duration field stored as Option<u64/i64>: only override when Some
95            let binding_name = config.binding_field_name_owned(&typ.name, &field.name);
96            if !field.optional && matches!(field.ty, TypeRef::Duration) {
97                let cast = if config.cast_large_ints_to_i64 { " as u64" } else { "" };
98                writeln!(
99                    out,
100                    "        if let Some(__v) = val.{binding_name} {{ __result.{} = std::time::Duration::from_millis(__v{cast}); }}",
101                    field.name
102                )
103                .ok();
104                continue;
105            }
106            let conversion = if optionalized && !field.optional {
107                gen_optionalized_field_to_core(&field.name, &field.ty, config, false)
108            } else {
109                field_conversion_to_core_cfg(&field.name, &field.ty, field.optional, config)
110            };
111            // Apply binding field name substitution for keyword-escaped fields.
112            let conversion = if binding_name != field.name {
113                conversion.replace(&format!("val.{}", field.name), &format!("val.{binding_name}"))
114            } else {
115                conversion
116            };
117            // Strip the "name: " prefix to get just the expression, then assign
118            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
119                writeln!(out, "        __result.{} = {};", field.name, expr).ok();
120            }
121        }
122        writeln!(out, "        __result").ok();
123        writeln!(out, "    }}").ok();
124        write!(out, "}}").ok();
125        return out;
126    }
127
128    let optionalized = config.optionalize_defaults && typ.has_default;
129    if optionalized {
130        writeln!(out, "        let mut __result = {core_path}::default();").ok();
131    } else {
132        writeln!(out, "        Self {{").ok();
133    }
134    for field in &typ.fields {
135        // Skip cfg-gated fields — they don't exist in the binding struct.
136        // When the binding is compiled, these fields are absent, and accessing them would fail.
137        // The ..Default::default() at the end fills in these fields when the core type is compiled
138        // with the required feature enabled.
139        if field.cfg.is_some() {
140            continue;
141        }
142        // Fields referencing excluded types don't exist in the binding struct.
143        // When the type has stripped cfg-gated fields, these fields may also be
144        // cfg-gated and absent from the core struct — skip them entirely and let
145        // ..Default::default() fill them in.
146        // Otherwise, use Default::default() to fill them in the core type.
147        // Sanitized fields also use Default::default() (lossy but functional).
148        let references_excluded = !config.exclude_types.is_empty()
149            && super::helpers::field_references_excluded_type(&field.ty, config.exclude_types);
150        if references_excluded && typ.has_stripped_cfg_fields {
151            continue;
152        }
153        if optionalized && ((field.sanitized && field.core_wrapper != CoreWrapper::Cow) || references_excluded) {
154            continue;
155        }
156        let field_was_optionalized = optionalized && !field.optional;
157        let conversion = if (field.sanitized && field.core_wrapper != CoreWrapper::Cow) || references_excluded {
158            format!("{}: Default::default()", field.name)
159        } else if field_was_optionalized {
160            // Field was wrapped in Option<T> for JS ergonomics but core expects T.
161            // Convert the supplied value as T; omitted fields keep the core type's Default value.
162            field_conversion_to_core_cfg(&field.name, &field.ty, false, config)
163        } else {
164            field_conversion_to_core_cfg(&field.name, &field.ty, field.optional, config)
165        };
166        // Newtype wrapping: when the field was resolved from a newtype (e.g. NodeIndex → u32),
167        // wrap the binding value back into the newtype for the core struct.
168        // e.g. `source: val.source` → `source: kreuzberg::NodeIndex(val.source)`
169        //      `parent: val.parent` → `parent: val.parent.map(kreuzberg::NodeIndex)`
170        //      `children: val.children` → `children: val.children.into_iter().map(kreuzberg::NodeIndex).collect()`
171        let conversion = if let Some(newtype_path) = &field.newtype_wrapper {
172            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
173                // When `optional=true` and `ty` is a plain Primitive (not TypeRef::Optional), the core
174                // field is actually `Option<NewtypeT>`, so we must use `.map(NewtypeT)` not `NewtypeT(...)`.
175                match &field.ty {
176                    TypeRef::Optional(_) => format!("{}: ({expr}).map({newtype_path})", field.name),
177                    TypeRef::Vec(_) => {
178                        // When the inner expr already ends with .collect() (e.g. because of a
179                        // primitive cast), the compiler cannot infer the intermediate Vec type
180                        // without an explicit type annotation. Use collect::<Vec<_>>() to make
181                        // the intermediate collection type unambiguous before mapping to newtype.
182                        let inner_expr = if let Some(prefix) = expr.strip_suffix(".collect()") {
183                            format!("{prefix}.collect::<Vec<_>>()")
184                        } else {
185                            expr.to_string()
186                        };
187                        format!(
188                            "{}: ({inner_expr}).into_iter().map({newtype_path}).collect()",
189                            field.name
190                        )
191                    }
192                    _ if field.optional => format!("{}: ({expr}).map({newtype_path})", field.name),
193                    _ => format!("{}: {newtype_path}({expr})", field.name),
194                }
195            } else {
196                conversion
197            }
198        } else {
199            conversion
200        };
201        // Box<T> fields: wrap the converted value in Box::new()
202        let conversion = if field.is_boxed && matches!(&field.ty, TypeRef::Named(_)) {
203            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
204                if field.optional {
205                    // Option<Box<T>> field: map inside the Option
206                    format!("{}: {}.map(Box::new)", field.name, expr)
207                } else {
208                    format!("{}: Box::new({})", field.name, expr)
209                }
210            } else {
211                conversion
212            }
213        } else {
214            conversion
215        };
216        // CoreWrapper: apply Cow/Arc/Bytes wrapping for binding→core direction.
217        //
218        // Special case: opaque Named field with CoreWrapper::Arc.
219        // The binding wrapper already holds `inner: Arc<CoreT>`, so the correct
220        // conversion is to extract `.inner` directly rather than calling `.into()`
221        // (which requires `From<BindingType> for CoreT`, a non-existent impl) and
222        // then wrapping in `Arc::new` (which would double-wrap the Arc).
223        let is_opaque_arc_field = field.core_wrapper == CoreWrapper::Arc
224            && matches!(&field.ty, TypeRef::Named(n) if config
225                .opaque_types
226                .is_some_and(|opaque| opaque.contains(n.as_str())));
227        // Opaque Named fields without CoreWrapper::Arc (e.g. visitor: Object<'static>) cannot be
228        // auto-converted via Into — the binding stores a raw JS object that needs a bridge.
229        // Emit Default::default() and let the caller (e.g. the convert function) set it separately.
230        let is_opaque_no_wrapper_field = field.core_wrapper == CoreWrapper::None
231            && matches!(&field.ty, TypeRef::Named(n) if config
232                .opaque_types
233                .is_some_and(|opaque| opaque.contains(n.as_str())));
234        let conversion = if is_opaque_arc_field {
235            if field.optional {
236                format!("{}: val.{}.map(|v| v.inner)", field.name, field.name)
237            } else {
238                format!("{}: val.{}.inner", field.name, field.name)
239            }
240        } else if is_opaque_no_wrapper_field {
241            format!("{}: Default::default()", field.name)
242        } else {
243            apply_core_wrapper_to_core(
244                &conversion,
245                &field.name,
246                &field.core_wrapper,
247                &field.vec_inner_core_wrapper,
248                field.optional,
249            )
250        };
251        // When the binding struct uses a keyword-escaped field name (e.g. `class_` for `class`),
252        // replace `val.{field.name}` access patterns in the conversion expression with
253        // `val.{binding_name}` so the generated From impl compiles.
254        let binding_name = config.binding_field_name_owned(&typ.name, &field.name);
255        let conversion = if binding_name != field.name {
256            conversion.replace(&format!("val.{}", field.name), &format!("val.{binding_name}"))
257        } else {
258            conversion
259        };
260        if optionalized {
261            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
262                if field_was_optionalized {
263                    writeln!(
264                        out,
265                        "        if let Some(__v) = val.{binding_name} {{ __result.{} = {}; }}",
266                        field.name,
267                        expr.replace(&format!("val.{binding_name}"), "__v")
268                    )
269                    .ok();
270                } else {
271                    writeln!(out, "        __result.{} = {};", field.name, expr).ok();
272                }
273            }
274        } else {
275            writeln!(out, "            {conversion},").ok();
276        }
277    }
278    // Use ..Default::default() to fill cfg-gated fields stripped from the IR
279    if typ.has_stripped_cfg_fields && !optionalized {
280        writeln!(out, "            ..Default::default()").ok();
281    }
282    if optionalized {
283        writeln!(out, "        __result").ok();
284    } else {
285        writeln!(out, "        }}").ok();
286    }
287    writeln!(out, "    }}").ok();
288    write!(out, "}}").ok();
289    out
290}
291
292/// Generate field conversion for a field that was optionalized (wrapped in `Option<T>`) in the
293/// binding struct for JS ergonomics (`optionalize_defaults`). When `field_is_ir_optional` is
294/// `true`, the field is genuinely `Option<T>` in the IR and the `Option` layer must be preserved
295/// in the output expression (use `.map(|m| …)` rather than `unwrap_or_default()`).
296pub(super) fn gen_optionalized_field_to_core(
297    name: &str,
298    ty: &TypeRef,
299    config: &ConversionConfig,
300    field_is_ir_optional: bool,
301) -> String {
302    match ty {
303        TypeRef::Json => {
304            format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or_default()")
305        }
306        TypeRef::Named(_) => {
307            // Named type: unwrap Option, convert via .into(), or use Default
308            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
309        }
310        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
311            format!("{name}: val.{name}.map(|v| v as f32).unwrap_or(0.0)")
312        }
313        TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => {
314            format!("{name}: val.{name}.unwrap_or(0.0)")
315        }
316        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
317            let core_ty = core_prim_str(p);
318            format!("{name}: val.{name}.map(|v| v as {core_ty}).unwrap_or_default()")
319        }
320        TypeRef::Optional(inner)
321            if config.cast_large_ints_to_i64
322                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
323        {
324            if let TypeRef::Primitive(p) = inner.as_ref() {
325                let core_ty = core_prim_str(p);
326                format!("{name}: val.{name}.map(|v| v as {core_ty})")
327            } else {
328                field_conversion_to_core(name, ty, false)
329            }
330        }
331        TypeRef::Duration if config.cast_large_ints_to_i64 => {
332            format!("{name}: val.{name}.map(|v| std::time::Duration::from_millis(v as u64)).unwrap_or_default()")
333        }
334        TypeRef::Duration => {
335            format!("{name}: val.{name}.map(std::time::Duration::from_millis).unwrap_or_default()")
336        }
337        TypeRef::Path => {
338            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
339        }
340        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Path) => {
341            // Binding has Option<String>, core has Option<PathBuf>
342            format!("{name}: val.{name}.map(|s| std::path::PathBuf::from(s))")
343        }
344        TypeRef::Optional(_) => {
345            // Field was flattened from Option<Option<T>> to Option<T> in the binding struct.
346            // Core expects Option<Option<T>>, so wrap with .map(Some) to reconstruct.
347            format!("{name}: val.{name}.map(Some)")
348        }
349        // Char: binding uses Option<String>, core uses char
350        TypeRef::Char => {
351            format!("{name}: val.{name}.and_then(|s| s.chars().next()).unwrap_or('*')")
352        }
353        TypeRef::Vec(inner) => match inner.as_ref() {
354            TypeRef::Json => {
355                format!(
356                    "{name}: val.{name}.map(|v| v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()).unwrap_or_default()"
357                )
358            }
359            TypeRef::Named(_) => {
360                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect()).unwrap_or_default()")
361            }
362            TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
363                let core_ty = core_prim_str(p);
364                format!(
365                    "{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect()).unwrap_or_default()"
366                )
367            }
368            _ => format!("{name}: val.{name}.unwrap_or_default()"),
369        },
370        TypeRef::Map(k, v) if matches!(v.as_ref(), TypeRef::Json) => {
371            // Map with Json values: binding uses HashMap<K, String>, core uses HashMap<K, serde_json::Value>.
372            // Use `k.into()` for non-Json keys so String→String is a no-op while still converting
373            // String→Cow<'_, str>/Box<str>/Arc<str> when the core type uses one of those wrappers.
374            let k_is_json = matches!(k.as_ref(), TypeRef::Json);
375            let k_expr = if k_is_json {
376                "serde_json::from_str(&k).unwrap_or_default()"
377            } else {
378                "k.into()"
379            };
380            format!(
381                "{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()"
382            )
383        }
384        TypeRef::Map(k, _v) if matches!(k.as_ref(), TypeRef::Json) => {
385            // Map with Json keys: binding uses HashMap<String, V>, core uses HashMap<serde_json::Value, V>
386            format!(
387                "{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| (serde_json::from_str(&k).unwrap_or_default(), v)).collect()"
388            )
389        }
390        TypeRef::Map(k, v) => {
391            // Map with Named values need .into() conversion on each value.
392            let has_named_val = matches!(v.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
393            let has_named_key = matches!(k.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
394            let val_is_string_enum = matches!(v.as_ref(), TypeRef::Named(n)
395                if config.enum_string_names.as_ref().is_some_and(|names| names.contains(n)));
396            if field_is_ir_optional {
397                // Genuinely optional field: preserve the Option layer using .map(|m| …).
398                if val_is_string_enum {
399                    format!(
400                        "{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, serde_json::from_str(&v).unwrap_or_default())).collect())"
401                    )
402                } else if has_named_val {
403                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())")
404                } else if has_named_key {
405                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.into(), v)).collect())")
406                } else {
407                    format!("{name}: val.{name}.map(|m| m.into_iter().collect())")
408                }
409            } else if val_is_string_enum {
410                format!(
411                    "{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| (k, serde_json::from_str(&v).unwrap_or_default())).collect()"
412                )
413            } else if has_named_val {
414                format!("{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| (k, v.into())).collect()")
415            } else if has_named_key {
416                format!("{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| (k.into(), v)).collect()")
417            } else {
418                format!("{name}: val.{name}.unwrap_or_default().into_iter().collect()")
419            }
420        }
421        _ => {
422            // Simple types (primitives, String, etc): unwrap_or_default()
423            format!("{name}: val.{name}.unwrap_or_default()")
424        }
425    }
426}
427
428/// Determine the field conversion expression for binding -> core.
429pub fn field_conversion_to_core(name: &str, ty: &TypeRef, optional: bool) -> String {
430    match ty {
431        // Primitives, String, Unit -- direct assignment
432        TypeRef::Primitive(_) | TypeRef::String | TypeRef::Unit => {
433            format!("{name}: val.{name}")
434        }
435        // Bytes: binding may use Vec<u8> or napi `Buffer`; core uses `bytes::Bytes`
436        // (or `Vec<u8>` for some targets). `.to_vec().into()` works in all cases:
437        // Buffer → Vec<u8> via `From<Buffer> for Vec<u8>`, then `Vec<u8> → Bytes`
438        // via `From<Vec<u8>> for Bytes` (or identity From for Vec<u8>→Vec<u8>).
439        TypeRef::Bytes => {
440            if optional {
441                format!("{name}: val.{name}.map(|v| v.to_vec().into())")
442            } else {
443                format!("{name}: val.{name}.to_vec().into()")
444            }
445        }
446        // Json: binding uses String, core uses serde_json::Value — parse or default
447        TypeRef::Json => {
448            if optional {
449                format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())")
450            } else {
451                format!("{name}: serde_json::from_str(&val.{name}).unwrap_or_default()")
452            }
453        }
454        // Char: binding uses String, core uses char — convert first character
455        TypeRef::Char => {
456            if optional {
457                format!("{name}: val.{name}.and_then(|s| s.chars().next())")
458            } else {
459                format!("{name}: val.{name}.chars().next().unwrap_or('*')")
460            }
461        }
462        // Duration: binding uses u64 (millis), core uses std::time::Duration
463        TypeRef::Duration => {
464            if optional {
465                format!("{name}: val.{name}.map(std::time::Duration::from_millis)")
466            } else {
467                format!("{name}: std::time::Duration::from_millis(val.{name})")
468            }
469        }
470        // Path needs .into() — binding uses String, core uses PathBuf
471        TypeRef::Path => {
472            if optional {
473                format!("{name}: val.{name}.map(Into::into)")
474            } else {
475                format!("{name}: val.{name}.into()")
476            }
477        }
478        // Named type -- needs .into() to convert between binding and core types
479        // Tuple types (e.g., "(String, String)") are passthrough — no conversion needed
480        TypeRef::Named(type_name) if is_tuple_type_name(type_name) => {
481            format!("{name}: val.{name}")
482        }
483        TypeRef::Named(_) => {
484            if optional {
485                format!("{name}: val.{name}.map(Into::into)")
486            } else {
487                format!("{name}: val.{name}.into()")
488            }
489        }
490        // Map with Json value type: binding uses HashMap<K, String>, core uses HashMap<K, Value>.
491        // Use `k.into()` for non-Json keys so String→String is a no-op while still converting
492        // String→Cow<'_, str>/Box<str>/Arc<str> when the core type uses one of those wrappers.
493        TypeRef::Map(k, v) if matches!(v.as_ref(), TypeRef::Json) => {
494            let k_expr = if matches!(k.as_ref(), TypeRef::Json) {
495                "serde_json::from_str(&k).unwrap_or_default()"
496            } else {
497                "k.into()"
498            };
499            if optional {
500                format!(
501                    "{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, serde_json::from_str(&v).unwrap_or_default())).collect())"
502                )
503            } else {
504                format!(
505                    "{name}: val.{name}.into_iter().map(|(k, v)| ({k_expr}, serde_json::from_str(&v).unwrap_or_default())).collect()"
506                )
507            }
508        }
509        // Optional with inner
510        TypeRef::Optional(inner) => match inner.as_ref() {
511            TypeRef::Json => format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())"),
512            TypeRef::Named(_) | TypeRef::Path => format!("{name}: val.{name}.map(Into::into)"),
513            TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_)) => {
514                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
515            }
516            _ => format!("{name}: val.{name}"),
517        },
518        // Vec of named or Json types -- map each element
519        TypeRef::Vec(inner) => match inner.as_ref() {
520            TypeRef::Json => {
521                if optional {
522                    format!(
523                        "{name}: val.{name}.map(|v| v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect())"
524                    )
525                } else {
526                    format!("{name}: val.{name}.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()")
527                }
528            }
529            // Vec<(T1, T2)> — tuples are passthrough
530            TypeRef::Named(type_name) if is_tuple_type_name(type_name) => {
531                format!("{name}: val.{name}")
532            }
533            TypeRef::Named(_) => {
534                if optional {
535                    format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
536                } else {
537                    format!("{name}: val.{name}.into_iter().map(Into::into).collect()")
538                }
539            }
540            _ => format!("{name}: val.{name}"),
541        },
542        // Map -- collect to handle HashMap↔BTreeMap conversion;
543        // additionally convert Named keys/values via Into, Json values via serde.
544        TypeRef::Map(k, v) => {
545            let has_named_key = matches!(k.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
546            let has_named_val = matches!(v.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
547            let has_json_val = matches!(v.as_ref(), TypeRef::Json);
548            let has_json_key = matches!(k.as_ref(), TypeRef::Json);
549            // Vec<Named> values: each vector element needs Into conversion.
550            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)));
551            // Vec<Json> values: each element needs serde deserialization.
552            let has_vec_json_val = matches!(v.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json));
553            if has_json_val || has_json_key || has_named_key || has_named_val || has_vec_named_val || has_vec_json_val {
554                // `k.into()` is a no-op for `String`→`String` and the canonical conversion for
555                // wrapped string keys (`Cow`, `Box<str>`, `Arc<str>`) which the type resolver
556                // collapses to `TypeRef::String`.
557                let k_expr = if has_json_key {
558                    "serde_json::from_str(&k).unwrap_or(serde_json::Value::String(k))"
559                } else {
560                    "k.into()"
561                };
562                let v_expr = if has_json_val {
563                    "serde_json::from_str(&v).unwrap_or(serde_json::Value::String(v))"
564                } else if has_named_val {
565                    "v.into()"
566                } else if has_vec_named_val {
567                    "v.into_iter().map(Into::into).collect()"
568                } else if has_vec_json_val {
569                    "v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()"
570                } else {
571                    "v"
572                };
573                if optional {
574                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect())")
575                } else {
576                    format!("{name}: val.{name}.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect()")
577                }
578            } else {
579                // Map<String, String>: binding may have String keys/values, core may have Box<str>/Cow<str>.
580                // Emit .map(|(k, v)| (k.into(), v.into())) which is a no-op when both sides are String.
581                // This handles cases like HashMap<String, String> (binding) → HashMap<Box<str>, Box<str>> (core).
582                let is_string_map = matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String);
583                if is_string_map {
584                    if optional {
585                        format!(
586                            "{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect())"
587                        )
588                    } else {
589                        format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.into(), v.into())).collect()")
590                    }
591                } else {
592                    // No conversion needed for keys/values — just collect for potential
593                    // HashMap↔BTreeMap type change. Still apply per-value .into() when the value
594                    // type is a Named wrapper that requires conversion (e.g. a binding-side newtype).
595                    if optional {
596                        if has_named_val {
597                            format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())")
598                        } else {
599                            format!("{name}: val.{name}.map(|m| m.into_iter().collect())")
600                        }
601                    } else {
602                        format!("{name}: val.{name}.into_iter().collect()")
603                    }
604                }
605            }
606        }
607    }
608}
609
610/// Binding→core field conversion with backend-specific config (i64 casts, etc.).
611pub fn field_conversion_to_core_cfg(name: &str, ty: &TypeRef, optional: bool, config: &ConversionConfig) -> String {
612    // When optional=true and ty=Optional(T), the binding field was flattened from
613    // Option<Option<T>> to Option<T>. Core expects Option<Option<T>>, so wrap with .map(Some).
614    // This applies regardless of cast config; handle before any other dispatch.
615    if optional && matches!(ty, TypeRef::Optional(_)) {
616        // Delegate to get the inner Optional(T) → Option<T> conversion (with optional=false,
617        // since the outer Option is handled by the .map(Some) we add here).
618        let inner_expr = field_conversion_to_core_cfg(name, ty, false, config);
619        // inner_expr is "name: <expr-for-Option<T>>"; wrap it with .map(Some)
620        if let Some(expr) = inner_expr.strip_prefix(&format!("{name}: ")) {
621            return format!("{name}: ({expr}).map(Some)");
622        }
623        return inner_expr;
624    }
625
626    // WASM JsValue: use serde_wasm_bindgen for Map, nested Vec, and Vec<Json> types
627    if config.map_uses_jsvalue {
628        let is_nested_vec = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Vec(_)));
629        let is_vec_json = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json));
630        let is_map = matches!(ty, TypeRef::Map(_, _));
631        if is_nested_vec || is_map || is_vec_json {
632            if optional {
633                return format!(
634                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
635                );
636            }
637            return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
638        }
639        if let TypeRef::Optional(inner) = ty {
640            let is_inner_nested = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Vec(_)));
641            let is_inner_vec_json = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Json));
642            let is_inner_map = matches!(inner.as_ref(), TypeRef::Map(_, _));
643            if is_inner_nested || is_inner_map || is_inner_vec_json {
644                return format!(
645                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
646                );
647            }
648        }
649    }
650
651    // Vec<Named>→String binding→core: binding holds JSON string, core expects Vec<Named>.
652    // Only apply serde round-trip for Vec<Named> types (complex structs that can't cross FFI).
653    // Vec<String>, Vec<Primitive>, etc. stay as-is since they map directly.
654    if config.vec_named_to_string {
655        if let TypeRef::Vec(inner) = ty {
656            if matches!(inner.as_ref(), TypeRef::Named(_)) {
657                if optional {
658                    return format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())");
659                }
660                return format!("{name}: serde_json::from_str(&val.{name}).unwrap_or_default()");
661            }
662        }
663    }
664    // Map→String binding→core: use Default::default() (lossy — can't reconstruct HashMap from Debug string)
665    if config.map_as_string && matches!(ty, TypeRef::Map(_, _)) {
666        return format!("{name}: Default::default()");
667    }
668    if config.map_as_string {
669        if let TypeRef::Optional(inner) = ty {
670            if matches!(inner.as_ref(), TypeRef::Map(_, _)) {
671                return format!("{name}: Default::default()");
672            }
673        }
674    }
675    // Json→String binding→core: use Default::default() (lossy — can't parse String back)
676    if config.json_to_string && matches!(ty, TypeRef::Json) {
677        return format!("{name}: Default::default()");
678    }
679    // Json→JsValue binding→core: use serde_wasm_bindgen to convert (WASM)
680    if config.map_uses_jsvalue && matches!(ty, TypeRef::Json) {
681        if optional {
682            return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())");
683        }
684        return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
685    }
686    if !config.cast_large_ints_to_i64
687        && !config.cast_large_ints_to_f64
688        && !config.cast_uints_to_i32
689        && !config.cast_f32_to_f64
690        && !config.json_to_string
691        && !config.vec_named_to_string
692        && !config.map_as_string
693        && config.from_binding_skip_types.is_empty()
694    {
695        return field_conversion_to_core(name, ty, optional);
696    }
697    // Cast mode: handle primitives and Duration differently
698    match ty {
699        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
700            let core_ty = core_prim_str(p);
701            if optional {
702                format!("{name}: val.{name}.map(|v| v as {core_ty})")
703            } else {
704                format!("{name}: val.{name} as {core_ty}")
705            }
706        }
707        // f64→f32 cast (NAPI binding f64 → core f32)
708        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
709            if optional {
710                format!("{name}: val.{name}.map(|v| v as f32)")
711            } else {
712                format!("{name}: val.{name} as f32")
713            }
714        }
715        TypeRef::Duration if config.cast_large_ints_to_i64 => {
716            if optional {
717                format!("{name}: val.{name}.map(|v| std::time::Duration::from_millis(v as u64))")
718            } else {
719                format!("{name}: std::time::Duration::from_millis(val.{name} as u64)")
720            }
721        }
722        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) => {
723            if let TypeRef::Primitive(p) = inner.as_ref() {
724                let core_ty = core_prim_str(p);
725                format!("{name}: val.{name}.map(|v| v as {core_ty})")
726            } else {
727                field_conversion_to_core(name, ty, optional)
728            }
729        }
730        // Vec<u64/usize/isize> needs element-wise i64→core casting
731        TypeRef::Vec(inner)
732            if config.cast_large_ints_to_i64
733                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
734        {
735            if let TypeRef::Primitive(p) = inner.as_ref() {
736                let core_ty = core_prim_str(p);
737                if optional {
738                    format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
739                } else {
740                    format!("{name}: val.{name}.into_iter().map(|v| v as {core_ty}).collect()")
741                }
742            } else {
743                field_conversion_to_core(name, ty, optional)
744            }
745        }
746        // HashMap value type casting: when value type needs i64→core casting
747        TypeRef::Map(_k, v)
748            if config.cast_large_ints_to_i64 && matches!(v.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
749        {
750            if let TypeRef::Primitive(p) = v.as_ref() {
751                let core_ty = core_prim_str(p);
752                if optional {
753                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v as {core_ty})).collect())")
754                } else {
755                    format!("{name}: val.{name}.into_iter().map(|(k, v)| (k, v as {core_ty})).collect()")
756                }
757            } else {
758                field_conversion_to_core(name, ty, optional)
759            }
760        }
761        // Vec<f32> needs element-wise cast when f32→f64 mapping is active (NAPI)
762        TypeRef::Vec(inner)
763            if config.cast_f32_to_f64 && matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32)) =>
764        {
765            if optional {
766                format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
767            } else {
768                format!("{name}: val.{name}.into_iter().map(|v| v as f32).collect()")
769            }
770        }
771        // Optional(Vec(f32)) needs element-wise cast (NAPI only)
772        TypeRef::Optional(inner)
773            if config.cast_f32_to_f64
774                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
775        {
776            format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
777        }
778        // i32→u8/u16/u32/i8/i16 casts (extendr — R maps small ints to i32)
779        TypeRef::Primitive(p) if config.cast_uints_to_i32 && needs_i32_cast(p) => {
780            let core_ty = core_prim_str(p);
781            if optional {
782                format!("{name}: val.{name}.map(|v| v as {core_ty})")
783            } else {
784                format!("{name}: val.{name} as {core_ty}")
785            }
786        }
787        // Optional(i32-needs-cast) with cast_uints_to_i32
788        TypeRef::Optional(inner)
789            if config.cast_uints_to_i32 && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i32_cast(p)) =>
790        {
791            if let TypeRef::Primitive(p) = inner.as_ref() {
792                let core_ty = core_prim_str(p);
793                format!("{name}: val.{name}.map(|v| v as {core_ty})")
794            } else {
795                field_conversion_to_core(name, ty, optional)
796            }
797        }
798        // Vec<u8/u16/u32/i8/i16> needs element-wise i32→core casting
799        TypeRef::Vec(inner)
800            if config.cast_uints_to_i32 && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i32_cast(p)) =>
801        {
802            if let TypeRef::Primitive(p) = inner.as_ref() {
803                let core_ty = core_prim_str(p);
804                if optional {
805                    format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
806                } else {
807                    format!("{name}: val.{name}.into_iter().map(|v| v as {core_ty}).collect()")
808                }
809            } else {
810                field_conversion_to_core(name, ty, optional)
811            }
812        }
813        // f64→u64/usize/isize casts (extendr — R maps large ints to f64)
814        TypeRef::Primitive(p) if config.cast_large_ints_to_f64 && needs_f64_cast(p) => {
815            let core_ty = core_prim_str(p);
816            if optional {
817                format!("{name}: val.{name}.map(|v| v as {core_ty})")
818            } else {
819                format!("{name}: val.{name} as {core_ty}")
820            }
821        }
822        // Optional(f64-needs-cast) with cast_large_ints_to_f64
823        TypeRef::Optional(inner)
824            if config.cast_large_ints_to_f64
825                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)) =>
826        {
827            if let TypeRef::Primitive(p) = inner.as_ref() {
828                let core_ty = core_prim_str(p);
829                format!("{name}: val.{name}.map(|v| v as {core_ty})")
830            } else {
831                field_conversion_to_core(name, ty, optional)
832            }
833        }
834        // Vec<u64/usize/isize> needs element-wise f64→core casting
835        TypeRef::Vec(inner)
836            if config.cast_large_ints_to_f64
837                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)) =>
838        {
839            if let TypeRef::Primitive(p) = inner.as_ref() {
840                let core_ty = core_prim_str(p);
841                if optional {
842                    format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
843                } else {
844                    format!("{name}: val.{name}.into_iter().map(|v| v as {core_ty}).collect()")
845                }
846            } else {
847                field_conversion_to_core(name, ty, optional)
848            }
849        }
850        // Map<K, usize/u64/i64/isize/f32> needs value-wise f64→core casting (extendr)
851        TypeRef::Map(_k, v)
852            if config.cast_large_ints_to_f64 && matches!(v.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)) =>
853        {
854            if let TypeRef::Primitive(p) = v.as_ref() {
855                let core_ty = core_prim_str(p);
856                if optional {
857                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v as {core_ty})).collect())")
858                } else {
859                    format!("{name}: val.{name}.into_iter().map(|(k, v)| (k, v as {core_ty})).collect()")
860                }
861            } else {
862                field_conversion_to_core(name, ty, optional)
863            }
864        }
865        // Skip-type: Named types that can't be auto-converted via Into in the binding→core From
866        // impl (e.g. PHP VisitorHandle which is handled separately by bridge machinery).
867        TypeRef::Named(n) if config.from_binding_skip_types.iter().any(|s| s == n) => {
868            format!("{name}: Default::default()")
869        }
870        TypeRef::Optional(inner) => match inner.as_ref() {
871            TypeRef::Named(n) if config.from_binding_skip_types.iter().any(|s| s == n) => {
872                format!("{name}: Default::default()")
873            }
874            _ => field_conversion_to_core(name, ty, optional),
875        },
876        // Fall through to default for everything else
877        _ => field_conversion_to_core(name, ty, optional),
878    }
879}
880
881/// Apply CoreWrapper transformations to a binding→core conversion expression.
882/// Wraps the value expression with Arc::new(), .into() for Cow, etc.
883pub fn apply_core_wrapper_to_core(
884    conversion: &str,
885    name: &str,
886    core_wrapper: &CoreWrapper,
887    vec_inner_core_wrapper: &CoreWrapper,
888    optional: bool,
889) -> String {
890    // Handle Vec<Arc<T>>: replace .map(Into::into) with .map(|v| std::sync::Arc::new(v.into()))
891    if *vec_inner_core_wrapper == CoreWrapper::Arc {
892        return conversion
893            .replace(
894                ".map(Into::into).collect()",
895                ".map(|v| std::sync::Arc::new(v.into())).collect()",
896            )
897            .replace(
898                "map(|v| v.into_iter().map(Into::into)",
899                "map(|v| v.into_iter().map(|v| std::sync::Arc::new(v.into()))",
900            );
901    }
902
903    match core_wrapper {
904        CoreWrapper::None => conversion.to_string(),
905        CoreWrapper::Cow => {
906            // Cow<str>: binding String → core Cow via .into()
907            // The field_conversion already emits "name: val.name" for strings,
908            // we need to add .into() to convert String → Cow<'static, str>
909            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
910                if optional {
911                    format!("{name}: {expr}.map(Into::into)")
912                } else if expr == format!("val.{name}") {
913                    format!("{name}: val.{name}.into()")
914                } else if expr == "Default::default()" {
915                    // Sanitized field: Default::default() already resolves to the correct core type
916                    // (e.g. Cow<'static, str> — adding .into() breaks type inference).
917                    conversion.to_string()
918                } else {
919                    format!("{name}: ({expr}).into()")
920                }
921            } else {
922                conversion.to_string()
923            }
924        }
925        CoreWrapper::Arc => {
926            // Arc<T>: wrap with Arc::new()
927            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
928                if expr == "Default::default()" {
929                    // Sanitized field: Default::default() resolves to the correct core type;
930                    // wrapping in Arc::new() would change the type.
931                    conversion.to_string()
932                } else if optional {
933                    format!("{name}: {expr}.map(|v| std::sync::Arc::new(v))")
934                } else {
935                    format!("{name}: std::sync::Arc::new({expr})")
936                }
937            } else {
938                conversion.to_string()
939            }
940        }
941        CoreWrapper::Bytes => {
942            // Bytes: binding Vec<u8> → core bytes::Bytes via .into().
943            // When TypeRef::Bytes already emitted a conversion (e.g. `val.{name}.into()` or
944            // `val.{name}.map(Into::into)`), applying another .into() creates an ambiguous
945            // double-into chain. Detect and dedup: use the already-generated expression as-is
946            // when it fully covers the conversion, or emit a fresh single .into() for bare fields.
947            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
948                let already_converted_non_opt =
949                    expr == format!("val.{name}.into()") || expr == format!("val.{name}.to_vec().into()");
950                let already_converted_opt = expr
951                    .strip_prefix(&format!("val.{name}"))
952                    .map(|s| s == ".map(Into::into)" || s == ".map(|v| v.to_vec().into())")
953                    .unwrap_or(false);
954                if already_converted_non_opt || already_converted_opt {
955                    // The base conversion already handles Bytes — pass through unchanged.
956                    conversion.to_string()
957                } else if optional {
958                    format!("{name}: {expr}.map(Into::into)")
959                } else if expr == format!("val.{name}") {
960                    format!("{name}: val.{name}.into()")
961                } else if expr == "Default::default()" {
962                    // Sanitized field: Default::default() already resolves to the correct core type
963                    // (e.g. bytes::Bytes — adding .into() breaks type inference).
964                    conversion.to_string()
965                } else {
966                    format!("{name}: ({expr}).into()")
967                }
968            } else {
969                conversion.to_string()
970            }
971        }
972        CoreWrapper::ArcMutex => {
973            // ArcMutex: binding T → core Arc<Mutex<T>> via Arc::new(Mutex::new())
974            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
975                if optional {
976                    format!("{name}: {expr}.map(|v| std::sync::Arc::new(std::sync::Mutex::new(v.into())))")
977                } else if expr == format!("val.{name}") {
978                    format!("{name}: std::sync::Arc::new(std::sync::Mutex::new(val.{name}.into()))")
979                } else {
980                    format!("{name}: std::sync::Arc::new(std::sync::Mutex::new(({expr}).into()))")
981                }
982            } else {
983                conversion.to_string()
984            }
985        }
986    }
987}
988
989#[cfg(test)]
990mod tests {
991    use super::gen_from_binding_to_core;
992    use alef_core::ir::{CoreWrapper, DefaultValue, FieldDef, TypeDef, TypeRef};
993
994    fn type_with_field(field: FieldDef) -> TypeDef {
995        TypeDef {
996            name: "ProcessConfig".to_string(),
997            rust_path: "crate::ProcessConfig".to_string(),
998            original_rust_path: String::new(),
999            fields: vec![field],
1000            methods: vec![],
1001            is_opaque: false,
1002            is_clone: true,
1003            is_copy: false,
1004            doc: String::new(),
1005            cfg: None,
1006            is_trait: false,
1007            has_default: true,
1008            has_stripped_cfg_fields: false,
1009            is_return_type: false,
1010            serde_rename_all: None,
1011            has_serde: true,
1012            super_traits: vec![],
1013        }
1014    }
1015
1016    #[test]
1017    fn sanitized_cow_string_field_converts_to_core() {
1018        let field = FieldDef {
1019            name: "language".to_string(),
1020            ty: TypeRef::String,
1021            optional: false,
1022            default: None,
1023            doc: String::new(),
1024            sanitized: true,
1025            is_boxed: false,
1026            type_rust_path: None,
1027            cfg: None,
1028            typed_default: Some(DefaultValue::Empty),
1029            core_wrapper: CoreWrapper::Cow,
1030            vec_inner_core_wrapper: CoreWrapper::None,
1031            newtype_wrapper: None,
1032        };
1033
1034        let out = gen_from_binding_to_core(&type_with_field(field), "crate");
1035
1036        assert!(out.contains("language: val.language.into()"));
1037        assert!(!out.contains("language: Default::default()"));
1038    }
1039}