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