Skip to main content

alef_codegen/conversions/
binding_to_core.rs

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