Skip to main content

alef_codegen/conversions/
mod.rs

1mod binding_to_core;
2mod core_to_binding;
3mod enums;
4pub(crate) mod helpers;
5
6use ahash::AHashSet;
7
8/// Backend-specific configuration for From/field conversion generation.
9/// Enables shared code to handle all backend differences via parameters.
10#[derive(Default, Clone)]
11pub struct ConversionConfig<'a> {
12    /// Prefix for binding type names ("Js" for NAPI/WASM, "" for others).
13    pub type_name_prefix: &'a str,
14    /// U64/Usize/Isize need `as i64` casts (NAPI, PHP — JS/PHP lack native u64).
15    pub cast_large_ints_to_i64: bool,
16    /// Enum names mapped to String in the binding layer (PHP only).
17    /// Named fields referencing these use `format!("{:?}")` in core→binding.
18    pub enum_string_names: Option<&'a AHashSet<String>>,
19    /// Map types use JsValue in the binding layer (WASM only).
20    /// When true, Map fields use `serde_wasm_bindgen` for conversion instead of
21    /// iterator-based collect patterns (JsValue is not iterable).
22    pub map_uses_jsvalue: bool,
23    /// When true, f32 is mapped to f64 (NAPI only — JS has no f32).
24    pub cast_f32_to_f64: bool,
25    /// When true, non-optional fields on defaultable types are wrapped in Option<T>
26    /// in the binding struct and need `.unwrap_or_default()` in binding→core From.
27    /// Used by NAPI to make JS-facing structs fully optional.
28    pub optionalize_defaults: bool,
29    /// When true, Json (serde_json::Value) fields are mapped to String in the binding layer.
30    /// Core→binding uses `.to_string()`, binding→core uses `Default::default()` (lossy).
31    /// Used by PHP where serde_json::Value can't cross the extension boundary.
32    pub json_to_string: bool,
33    /// When true, Json fields stay as `serde_json::Value` in the binding layer (no wrapping).
34    /// Core↔binding conversions are identity since both sides hold the same type.
35    /// Used by NAPI (with `serde-json` feature) so JS callers can pass arbitrary objects
36    /// directly without first stringifying them.
37    pub json_as_value: bool,
38    /// When true, add synthetic metadata field conversion for ConversionResult.
39    /// Only NAPI backend sets this (it adds metadata field to the struct).
40    pub include_cfg_metadata: bool,
41    /// When true, non-optional Duration fields on `has_default` types are stored as
42    /// `Option<u64>` in the binding struct.  The From conversion uses the builder
43    /// pattern so that `None` falls back to the core type's `Default` implementation
44    /// (giving the real default, e.g. `Duration::from_secs(30)`) instead of `Duration::ZERO`.
45    /// Used by PyO3 to prevent validation failures when `request_timeout` is unset.
46    pub option_duration_on_defaults: bool,
47    /// When true, binding enums include data variant fields (Magnus).
48    /// When false (default), binding enums are unit-only and data is lost in conversion.
49    pub binding_enums_have_data: bool,
50    /// Type names excluded from the binding layer. Fields referencing these types
51    /// are skipped in the binding struct and defaulted in From conversions.
52    /// Used by WASM to handle types excluded due to native dependency requirements.
53    pub exclude_types: &'a [String],
54    /// When true, Vec<Named> fields are stored as JSON strings in the binding layer.
55    /// Core→binding uses `serde_json::to_string`, binding→core uses `serde_json::from_str`.
56    /// Used by Magnus (Ruby) where Vec<Named> cannot cross the FFI boundary directly and
57    /// is collapsed to String by `field_type_for_serde`'s catch-all arm.
58    pub vec_named_to_string: bool,
59    /// When true, all Map(K, V) fields are stored as a plain `String` in the binding layer.
60    /// Core→binding uses `format!("{:?}", val.field)`, binding→core uses `Default::default()` (lossy).
61    /// Used by Rustler (Elixir NIFs) where `HashMap` cannot cross the NIF boundary directly.
62    pub map_as_string: bool,
63    /// Set of opaque type names in the binding layer.
64    /// When a field has `CoreWrapper::Arc` and its type is an opaque Named type,
65    /// the binding wrapper holds `inner: Arc<CoreT>` and the conversion must extract
66    /// `.inner` directly instead of calling `.into()` + wrapping in `Arc::new`.
67    pub opaque_types: Option<&'a AHashSet<String>>,
68    /// Type names that should use `Default::default()` in the binding→core From impl.
69    /// Used by PHP to skip bridge type fields (e.g., VisitorHandle) that can't be
70    /// auto-converted via Into and are always handled by the bridge machinery instead.
71    pub from_binding_skip_types: &'a [String],
72    /// When `core_crate_override` is set for a language, the IR's `rust_path` values
73    /// still contain the original source crate prefix (e.g. `mylib_core::Method`).
74    /// This field remaps those paths: `(original_crate_name, override_crate_name)`.
75    /// When set, any `rust_path` whose leading crate segment equals `original_crate_name`
76    /// is rewritten to use `override_crate_name` instead.
77    /// Example: `Some(("mylib_core", "mylib_http"))` rewrites
78    /// `mylib_core::Method` → `mylib_http::Method`.
79    pub source_crate_remaps: &'a [(&'a str, &'a str)],
80    /// Per-field binding name overrides.  Key is `"TypeName.field_name"` (using the original
81    /// IR field name); value is the binding struct's actual Rust field name (e.g. `"class_"`).
82    /// Used when a field name is a reserved keyword in the target language and must be escaped
83    /// in the binding struct (e.g. `class` → `class_`).
84    ///
85    /// When present, `val.<binding_name>` is used for binding-side access and the original
86    /// `field_name` is used for core-side access (struct literal and assignment targets).
87    pub binding_field_renames: Option<&'a std::collections::HashMap<String, String>>,
88    /// When true, U8/U16/U32 (and their signed counterparts I8/I16) need `as i32` casts.
89    /// extendr maps all small integers to R's native integer type (i32), so binding→core
90    /// conversions must cast back to the original unsigned/narrow types.
91    pub cast_uints_to_i32: bool,
92    /// When true, U64/Usize/Isize are mapped to f64 (R's native double type) rather than i64.
93    /// extendr uses f64 for large integers because R has no native 64-bit integer type.
94    /// Binding→core: `as usize`/`as u64` casts; core→binding: `as f64` casts.
95    pub cast_large_ints_to_f64: bool,
96    /// Names of untagged data enums (`#[serde(untagged)]` with at least one data variant —
97    /// e.g. `Single(String) | Multiple(Vec<String>)`). Fields referencing these types are
98    /// stored as `serde_json::Value` in the binding struct (the wire JSON shape varies per
99    /// variant, so we accept any value at the boundary). Used by the PHP backend; ext-php-rs
100    /// has no `FromZval`/`IntoZval` for typed Rust enums with mixed-shape variants, and the
101    /// only safe wire format is JSON-via-Value. Conversions:
102    ///
103    ///   - core→binding: `serde_json::to_value(val.<name>).unwrap_or_default()`
104    ///   - binding→core: `serde_json::from_value(val.<name>).unwrap_or_default()`
105    pub untagged_data_enum_names: Option<&'a AHashSet<String>>,
106    /// Names of tagged-data enums (`#[serde(tag = "...")]` with at least one data variant).
107    /// Fields referencing these types (or `Vec` of these types) are stored as `JsValue` in the
108    /// wasm binding struct so that plain JS objects `{ role: "user", content: "..." }` can be
109    /// passed without being wrapped in an explicit binding-class instance.
110    ///
111    /// Used by the WASM backend only; `map_uses_jsvalue` must also be `true`.
112    ///
113    /// Conversions:
114    ///   - core→binding: `serde_wasm_bindgen::to_value(&val.<name>).unwrap_or(JsValue::NULL)`
115    ///   - binding→core: `serde_wasm_bindgen::from_value(val.<name>.clone()).unwrap_or_default()`
116    pub tagged_data_enum_names: Option<&'a AHashSet<String>>,
117    /// Names of cfg-gated fields that must NOT be skipped in conversions because the binding
118    /// emits them (via [`super::generators::RustBindingConfig::never_skip_cfg_field_names`]).
119    /// Empty by default; backends populate from trait-bridge `bind_via = "options_field"` config.
120    pub never_skip_cfg_field_names: &'a [String],
121    /// Names of trait-bridge OptionsField fields whose binding wrapper holds the core value
122    /// as `inner: Arc<core::T>` (the standard codegen layout for every OptionsField bridge).
123    /// When a field matches both `is_opaque_no_wrapper_field` and this list, the binding→core
124    /// From impl emits `(*v.inner).clone()` instead of `Default::default()`, so the visitor
125    /// (or other bridge handle) is forwarded rather than silently dropped.
126    pub trait_bridge_arc_wrapper_field_names: &'a [String],
127    /// When true, cfg-gated fields (not listed in `never_skip_cfg_field_names`) are
128    /// stripped from the binding struct entirely (no field at all in the struct body).
129    /// Conversions must then skip those fields and rely on `..Default::default()` in
130    /// the template to fill the core struct slot.
131    ///
132    /// Set to `true` for backends whose binding crate does not carry feature gates into
133    /// its own Cargo.toml — e.g. extendr (R), where the binding struct is uniform across
134    /// all feature combinations.  PyO3/NAPI/PHP/etc keep cfg-gated fields in the binding
135    /// struct (decorated with `#[cfg(...)]`) and want them included in conversions.
136    pub strip_cfg_fields_from_binding_struct: bool,
137}
138
139impl<'a> ConversionConfig<'a> {
140    /// Look up the binding struct field name for a given type and IR field name.
141    ///
142    /// Returns the escaped name (e.g. `"class_"`) when the field was renamed due to a
143    /// reserved keyword conflict, or the original `field_name` when no rename applies.
144    pub fn binding_field_name<'b>(&self, type_name: &str, field_name: &'b str) -> &'b str
145    where
146        'a: 'b,
147    {
148        // &'b str: we return either the original (which has lifetime 'b from the parameter)
149        // or a &str from the HashMap (which would have lifetime 'a). Since 'a: 'b we can
150        // return either. But Rust's lifetime inference won't let us return `&'a str` from a
151        // `&'b str` parameter without unsafe. Use a helper that returns an owned String instead.
152        let _ = type_name;
153        field_name
154    }
155
156    /// Returns `true` when `field_name` is a trait-bridge OptionsField whose binding wrapper
157    /// stores the core value as `inner: Arc<core::T>`. Used by `gen_from_binding_to_core_cfg`
158    /// to emit `(*v.inner).clone()` instead of `Default::default()` for opaque-no-wrapper fields.
159    pub fn trait_bridge_field_is_arc_wrapper(&self, field_name: &str) -> bool {
160        self.trait_bridge_arc_wrapper_field_names
161            .iter()
162            .any(|n| n == field_name)
163    }
164
165    /// Like `binding_field_name` but returns an owned `String`, suitable for use in
166    /// format strings and string interpolation.
167    pub fn binding_field_name_owned(&self, type_name: &str, field_name: &str) -> String {
168        if let Some(map) = self.binding_field_renames {
169            let key = format!("{type_name}.{field_name}");
170            if let Some(renamed) = map.get(&key) {
171                return renamed.clone();
172            }
173        }
174        field_name.to_string()
175    }
176}
177
178// Re-export all public items so callers continue to use `conversions::foo`.
179pub use binding_to_core::{
180    apply_core_wrapper_to_core, field_conversion_to_core, field_conversion_to_core_cfg, gen_from_binding_to_core,
181    gen_from_binding_to_core_cfg,
182};
183pub use core_to_binding::{
184    field_conversion_from_core, field_conversion_from_core_cfg, gen_from_core_to_binding, gen_from_core_to_binding_cfg,
185};
186pub use enums::{
187    gen_enum_from_binding_to_core, gen_enum_from_binding_to_core_cfg, gen_enum_from_core_to_binding,
188    gen_enum_from_core_to_binding_cfg,
189};
190pub use helpers::{
191    apply_crate_remaps, binding_to_core_match_arm, build_type_path_map, can_generate_conversion,
192    can_generate_enum_conversion, can_generate_enum_conversion_from_core, convertible_types, core_enum_path,
193    core_enum_path_remapped, core_to_binding_convertible_types, core_to_binding_match_arm, core_type_path,
194    core_type_path_remapped, field_references_excluded_type, has_sanitized_fields, input_type_names, is_tuple_variant,
195    resolve_named_path,
196};
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use alef_core::ir::*;
202
203    fn simple_type() -> TypeDef {
204        TypeDef {
205            name: "Config".to_string(),
206            rust_path: "my_crate::Config".to_string(),
207            original_rust_path: String::new(),
208            fields: vec![
209                FieldDef {
210                    name: "name".into(),
211                    ty: TypeRef::String,
212                    optional: false,
213                    default: None,
214                    doc: String::new(),
215                    sanitized: false,
216                    is_boxed: false,
217                    type_rust_path: None,
218                    cfg: None,
219                    typed_default: None,
220                    core_wrapper: CoreWrapper::None,
221                    vec_inner_core_wrapper: CoreWrapper::None,
222                    newtype_wrapper: None,
223                    serde_rename: None,
224                    serde_flatten: false,
225                    binding_excluded: false,
226                    binding_exclusion_reason: None,
227                    original_type: None,
228                },
229                FieldDef {
230                    name: "timeout".into(),
231                    ty: TypeRef::Primitive(PrimitiveType::U64),
232                    optional: true,
233                    default: None,
234                    doc: String::new(),
235                    sanitized: false,
236                    is_boxed: false,
237                    type_rust_path: None,
238                    cfg: None,
239                    typed_default: None,
240                    core_wrapper: CoreWrapper::None,
241                    vec_inner_core_wrapper: CoreWrapper::None,
242                    newtype_wrapper: None,
243                    serde_rename: None,
244                    serde_flatten: false,
245                    binding_excluded: false,
246                    binding_exclusion_reason: None,
247                    original_type: None,
248                },
249                FieldDef {
250                    name: "backend".into(),
251                    ty: TypeRef::Named("Backend".into()),
252                    optional: true,
253                    default: None,
254                    doc: String::new(),
255                    sanitized: false,
256                    is_boxed: false,
257                    type_rust_path: None,
258                    cfg: None,
259                    typed_default: None,
260                    core_wrapper: CoreWrapper::None,
261                    vec_inner_core_wrapper: CoreWrapper::None,
262                    newtype_wrapper: None,
263                    serde_rename: None,
264                    serde_flatten: false,
265                    binding_excluded: false,
266                    binding_exclusion_reason: None,
267                    original_type: None,
268                },
269            ],
270            methods: vec![],
271            is_opaque: false,
272            is_clone: true,
273            is_copy: false,
274            is_trait: false,
275            has_default: false,
276            has_stripped_cfg_fields: false,
277            is_return_type: false,
278            serde_rename_all: None,
279            has_serde: false,
280            super_traits: vec![],
281            doc: String::new(),
282            cfg: None,
283            binding_excluded: false,
284            binding_exclusion_reason: None,
285        }
286    }
287
288    fn simple_enum() -> EnumDef {
289        EnumDef {
290            name: "Backend".to_string(),
291            rust_path: "my_crate::Backend".to_string(),
292            original_rust_path: String::new(),
293            variants: vec![
294                EnumVariant {
295                    name: "Cpu".into(),
296                    fields: vec![],
297                    is_tuple: false,
298                    doc: String::new(),
299                    is_default: false,
300                    serde_rename: None,
301                },
302                EnumVariant {
303                    name: "Gpu".into(),
304                    fields: vec![],
305                    is_tuple: false,
306                    doc: String::new(),
307                    is_default: false,
308                    serde_rename: None,
309                },
310            ],
311            doc: String::new(),
312            cfg: None,
313            is_copy: false,
314            has_serde: false,
315            serde_tag: None,
316            serde_untagged: false,
317            serde_rename_all: None,
318            binding_excluded: false,
319            binding_exclusion_reason: None,
320        }
321    }
322
323    #[test]
324    fn test_from_binding_to_core() {
325        let typ = simple_type();
326        let result = gen_from_binding_to_core(&typ, "my_crate");
327        assert!(result.contains("impl From<Config> for my_crate::Config"));
328        assert!(result.contains("name: val.name"));
329        assert!(result.contains("timeout: val.timeout"));
330        assert!(result.contains("backend: val.backend.map(Into::into)"));
331    }
332
333    #[test]
334    fn test_from_core_to_binding() {
335        let typ = simple_type();
336        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
337        assert!(result.contains("impl From<my_crate::Config> for Config"));
338    }
339
340    #[test]
341    fn test_enum_from_binding_to_core() {
342        let enum_def = simple_enum();
343        let result = gen_enum_from_binding_to_core(&enum_def, "my_crate");
344        assert!(result.contains("impl From<Backend> for my_crate::Backend"));
345        assert!(result.contains("Backend::Cpu => Self::Cpu"));
346        assert!(result.contains("Backend::Gpu => Self::Gpu"));
347    }
348
349    #[test]
350    fn test_enum_from_core_to_binding() {
351        let enum_def = simple_enum();
352        let result = gen_enum_from_core_to_binding(&enum_def, "my_crate");
353        assert!(result.contains("impl From<my_crate::Backend> for Backend"));
354        assert!(result.contains("my_crate::Backend::Cpu => Self::Cpu"));
355        assert!(result.contains("my_crate::Backend::Gpu => Self::Gpu"));
356    }
357
358    #[test]
359    fn test_from_binding_to_core_with_cfg_gated_field() {
360        // Create a type with a cfg-gated field
361        let mut typ = simple_type();
362        typ.has_stripped_cfg_fields = true;
363        typ.fields.push(FieldDef {
364            name: "layout".into(),
365            ty: TypeRef::String,
366            optional: false,
367            default: None,
368            doc: String::new(),
369            sanitized: false,
370            is_boxed: false,
371            type_rust_path: None,
372            cfg: Some("feature = \"layout-detection\"".into()),
373            typed_default: None,
374            core_wrapper: CoreWrapper::None,
375            vec_inner_core_wrapper: CoreWrapper::None,
376            newtype_wrapper: None,
377            serde_rename: None,
378            serde_flatten: false,
379            binding_excluded: false,
380            binding_exclusion_reason: None,
381            original_type: None,
382        });
383
384        let result = gen_from_binding_to_core(&typ, "my_crate");
385
386        // The impl should exist
387        assert!(result.contains("impl From<Config> for my_crate::Config"));
388        // Regular fields should be present
389        assert!(result.contains("name: val.name"));
390        assert!(result.contains("timeout: val.timeout"));
391        // Cfg-gated fields are now preserved on the binding struct, so the conversion
392        // accesses them directly rather than padding with ..Default::default().
393        assert!(result.contains("layout: val.layout"));
394    }
395
396    #[test]
397    fn test_from_core_to_binding_with_cfg_gated_field() {
398        // Create a type with a cfg-gated field
399        let mut typ = simple_type();
400        typ.fields.push(FieldDef {
401            name: "layout".into(),
402            ty: TypeRef::String,
403            optional: false,
404            default: None,
405            doc: String::new(),
406            sanitized: false,
407            is_boxed: false,
408            type_rust_path: None,
409            cfg: Some("feature = \"layout-detection\"".into()),
410            typed_default: None,
411            core_wrapper: CoreWrapper::None,
412            vec_inner_core_wrapper: CoreWrapper::None,
413            newtype_wrapper: None,
414            serde_rename: None,
415            serde_flatten: false,
416            binding_excluded: false,
417            binding_exclusion_reason: None,
418            original_type: None,
419        });
420
421        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
422
423        // The impl should exist
424        assert!(result.contains("impl From<my_crate::Config> for Config"));
425        // Regular fields should be present
426        assert!(result.contains("name: val.name"));
427        // Cfg-gated fields are now preserved on the binding struct and round-tripped.
428        assert!(result.contains("layout: val.layout"));
429    }
430
431    #[test]
432    fn test_field_conversion_from_core_map_named_non_optional() {
433        // Map<K, Named> non-optional: each value needs .into() core→binding
434        let result = field_conversion_from_core(
435            "tags",
436            &TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Named("Tag".into()))),
437            false,
438            false,
439            &AHashSet::new(),
440        );
441        assert_eq!(
442            result,
443            "tags: val.tags.into_iter().map(|(k, v)| (k, v.into())).collect()"
444        );
445    }
446
447    #[test]
448    fn test_field_conversion_from_core_option_map_named() {
449        // Option<Map<K, Named>>: .map() wrapper + per-element .into()
450        let result = field_conversion_from_core(
451            "tags",
452            &TypeRef::Optional(Box::new(TypeRef::Map(
453                Box::new(TypeRef::String),
454                Box::new(TypeRef::Named("Tag".into())),
455            ))),
456            false,
457            false,
458            &AHashSet::new(),
459        );
460        assert_eq!(
461            result,
462            "tags: val.tags.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
463        );
464    }
465
466    #[test]
467    fn test_field_conversion_from_core_vec_named_non_optional() {
468        // Vec<Named> non-optional: each element needs .into() core→binding
469        let result = field_conversion_from_core(
470            "items",
471            &TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))),
472            false,
473            false,
474            &AHashSet::new(),
475        );
476        assert_eq!(result, "items: val.items.into_iter().map(Into::into).collect()");
477    }
478
479    #[test]
480    fn test_field_conversion_from_core_option_vec_named() {
481        // Option<Vec<Named>>: .map() wrapper + per-element .into()
482        let result = field_conversion_from_core(
483            "items",
484            &TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))))),
485            false,
486            false,
487            &AHashSet::new(),
488        );
489        assert_eq!(
490            result,
491            "items: val.items.map(|v| v.into_iter().map(Into::into).collect())"
492        );
493    }
494
495    #[test]
496    fn test_field_conversion_to_core_option_map_named_applies_per_value_into() {
497        // Bug A1 regression: Option<Map<K, Named>> must apply per-value .into() so that
498        // binding-side wrapper types (e.g. PyO3 / Magnus structs) are converted correctly.
499        let result = field_conversion_to_core(
500            "patterns",
501            &TypeRef::Map(
502                Box::new(TypeRef::String),
503                Box::new(TypeRef::Named("ExtractionPattern".into())),
504            ),
505            true,
506        );
507        assert!(
508            result.contains("m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()"),
509            "expected per-value v.into() in optional Map<Named> conversion, got: {result}"
510        );
511        assert_eq!(
512            result,
513            "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect())"
514        );
515    }
516
517    #[test]
518    fn test_gen_optionalized_field_to_core_ir_optional_map_named_preserves_option() {
519        // Bug A3 regression: when field_is_ir_optional=true, gen_optionalized_field_to_core must
520        // preserve the Option layer via .map(|m| …) instead of dropping it with unwrap_or_default().
521        use super::binding_to_core::gen_optionalized_field_to_core;
522        let config = ConversionConfig::default();
523        let result = gen_optionalized_field_to_core(
524            "patterns",
525            &TypeRef::Map(
526                Box::new(TypeRef::String),
527                Box::new(TypeRef::Named("ExtractionPattern".into())),
528            ),
529            &config,
530            true,
531        );
532        assert!(
533            result.contains("m.into_iter().map(|(k, v)| (k, v.into())).collect()"),
534            "expected per-value v.into() in ir-optional Map<Named> conversion, got: {result}"
535        );
536        assert_eq!(
537            result,
538            "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
539        );
540    }
541
542    #[test]
543    fn test_optionalized_defaultable_struct_uses_core_default_as_base() {
544        let mut typ = simple_type();
545        typ.has_default = true;
546        typ.fields = vec![
547            FieldDef {
548                name: "language".into(),
549                ty: TypeRef::String,
550                optional: false,
551                default: None,
552                doc: String::new(),
553                sanitized: false,
554                is_boxed: false,
555                type_rust_path: None,
556                cfg: None,
557                typed_default: None,
558                core_wrapper: CoreWrapper::Cow,
559                vec_inner_core_wrapper: CoreWrapper::None,
560                newtype_wrapper: None,
561                serde_rename: None,
562                serde_flatten: false,
563                binding_excluded: false,
564                binding_exclusion_reason: None,
565                original_type: None,
566            },
567            FieldDef {
568                name: "structure".into(),
569                ty: TypeRef::Primitive(PrimitiveType::Bool),
570                optional: false,
571                default: None,
572                doc: String::new(),
573                sanitized: false,
574                is_boxed: false,
575                type_rust_path: None,
576                cfg: None,
577                typed_default: None,
578                core_wrapper: CoreWrapper::None,
579                vec_inner_core_wrapper: CoreWrapper::None,
580                newtype_wrapper: None,
581                serde_rename: None,
582                serde_flatten: false,
583                binding_excluded: false,
584                binding_exclusion_reason: None,
585                original_type: None,
586            },
587        ];
588        let config = ConversionConfig {
589            type_name_prefix: "Js",
590            optionalize_defaults: true,
591            ..ConversionConfig::default()
592        };
593
594        let result = gen_from_binding_to_core_cfg(&typ, "my_crate", &config);
595
596        assert!(result.contains("let mut __result = my_crate::Config::default();"));
597        assert!(result.contains("if let Some(__v) = val.language { __result.language = __v.into(); }"));
598        assert!(result.contains("if let Some(__v) = val.structure { __result.structure = __v; }"));
599        assert!(!result.contains("unwrap_or_default()"));
600    }
601
602    fn arc_field_type(field: FieldDef) -> TypeDef {
603        TypeDef {
604            name: "State".to_string(),
605            rust_path: "my_crate::State".to_string(),
606            original_rust_path: String::new(),
607            fields: vec![field],
608            methods: vec![],
609            is_opaque: false,
610            is_clone: true,
611            is_copy: false,
612            is_trait: false,
613            has_default: false,
614            has_stripped_cfg_fields: false,
615            is_return_type: false,
616            serde_rename_all: None,
617            has_serde: false,
618            super_traits: vec![],
619            doc: String::new(),
620            cfg: None,
621            binding_excluded: false,
622            binding_exclusion_reason: None,
623        }
624    }
625
626    fn arc_field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
627        FieldDef {
628            name: name.into(),
629            ty,
630            optional,
631            default: None,
632            doc: String::new(),
633            sanitized: false,
634            is_boxed: false,
635            type_rust_path: None,
636            cfg: None,
637            typed_default: None,
638            core_wrapper: CoreWrapper::Arc,
639            vec_inner_core_wrapper: CoreWrapper::None,
640            newtype_wrapper: None,
641            serde_rename: None,
642            serde_flatten: false,
643            binding_excluded: false,
644            binding_exclusion_reason: None,
645            original_type: None,
646        }
647    }
648
649    /// Regression: Option<Arc<serde_json::Value>> must not chain `(*v).clone().into()`
650    /// on top of `as_ref().map(ToString::to_string)`, which would emit invalid
651    /// `(*String).clone()` (str: !Clone).
652    #[test]
653    fn test_arc_json_option_field_no_double_chain() {
654        let typ = arc_field_type(arc_field("registered_spec", TypeRef::Json, true));
655        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
656        assert!(
657            result.contains("val.registered_spec.as_ref().map(ToString::to_string)"),
658            "expected as_ref().map(ToString::to_string) for Option<Arc<Value>>, got: {result}"
659        );
660        assert!(
661            !result.contains("map(ToString::to_string).map("),
662            "must not chain a second map() on top of ToString::to_string, got: {result}"
663        );
664    }
665
666    /// Non-optional Arc<Value>: `(*val.X).clone().to_string()` is valid (Value: Clone).
667    #[test]
668    fn test_arc_json_non_optional_field() {
669        let typ = arc_field_type(arc_field("spec", TypeRef::Json, false));
670        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
671        assert!(
672            result.contains("(*val.spec).clone().to_string()"),
673            "expected (*val.spec).clone().to_string() for Arc<Value>, got: {result}"
674        );
675    }
676
677    /// Option<Arc<String>>: simple passthrough → `.map(|v| (*v).clone().into())` is valid
678    /// (String: Clone). Verifies the simple_passthrough branch is preserved.
679    #[test]
680    fn test_arc_string_option_field_passthrough() {
681        let typ = arc_field_type(arc_field("label", TypeRef::String, true));
682        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
683        assert!(
684            result.contains("val.label.map(|v| (*v).clone().into())"),
685            "expected .map(|v| (*v).clone().into()) for Option<Arc<String>>, got: {result}"
686        );
687    }
688}