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, add synthetic metadata field conversion for ConversionResult.
34    /// Only NAPI backend sets this (it adds metadata field to the struct).
35    pub include_cfg_metadata: bool,
36    /// When true, non-optional Duration fields on `has_default` types are stored as
37    /// `Option<u64>` in the binding struct.  The From conversion uses the builder
38    /// pattern so that `None` falls back to the core type's `Default` implementation
39    /// (giving the real default, e.g. `Duration::from_secs(30)`) instead of `Duration::ZERO`.
40    /// Used by PyO3 to prevent validation failures when `request_timeout` is unset.
41    pub option_duration_on_defaults: bool,
42    /// When true, binding enums include data variant fields (Magnus).
43    /// When false (default), binding enums are unit-only and data is lost in conversion.
44    pub binding_enums_have_data: bool,
45    /// Type names excluded from the binding layer. Fields referencing these types
46    /// are skipped in the binding struct and defaulted in From conversions.
47    /// Used by WASM to handle types excluded due to native dependency requirements.
48    pub exclude_types: &'a [String],
49    /// When true, Vec<Named> fields are stored as JSON strings in the binding layer.
50    /// Core→binding uses `serde_json::to_string`, binding→core uses `serde_json::from_str`.
51    /// Used by Magnus (Ruby) where Vec<Named> cannot cross the FFI boundary directly and
52    /// is collapsed to String by `field_type_for_serde`'s catch-all arm.
53    pub vec_named_to_string: bool,
54}
55
56// Re-export all public items so callers continue to use `conversions::foo`.
57pub use binding_to_core::{
58    field_conversion_to_core, field_conversion_to_core_cfg, gen_from_binding_to_core, gen_from_binding_to_core_cfg,
59};
60pub use core_to_binding::{
61    field_conversion_from_core, field_conversion_from_core_cfg, gen_from_core_to_binding, gen_from_core_to_binding_cfg,
62};
63pub use enums::{
64    gen_enum_from_binding_to_core, gen_enum_from_binding_to_core_cfg, gen_enum_from_core_to_binding,
65    gen_enum_from_core_to_binding_cfg,
66};
67pub use helpers::{
68    binding_to_core_match_arm, build_type_path_map, can_generate_conversion, can_generate_enum_conversion,
69    can_generate_enum_conversion_from_core, convertible_types, core_enum_path, core_to_binding_convertible_types,
70    core_to_binding_match_arm, core_type_path, field_references_excluded_type, has_sanitized_fields, input_type_names,
71    is_tuple_variant, resolve_named_path,
72};
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use alef_core::ir::*;
78
79    // -----------------------------------------------------------------------
80    // Shared test helpers
81    // -----------------------------------------------------------------------
82
83    fn make_field(name: &str, ty: TypeRef) -> FieldDef {
84        FieldDef {
85            name: name.into(),
86            ty,
87            optional: false,
88            default: None,
89            doc: String::new(),
90            sanitized: false,
91            is_boxed: false,
92            type_rust_path: None,
93            cfg: None,
94            typed_default: None,
95            core_wrapper: CoreWrapper::None,
96            vec_inner_core_wrapper: CoreWrapper::None,
97            newtype_wrapper: None,
98        }
99    }
100
101    fn make_opt_field(name: &str, ty: TypeRef) -> FieldDef {
102        FieldDef {
103            optional: true,
104            ..make_field(name, ty)
105        }
106    }
107
108    fn make_type(name: &str, rust_path: &str, fields: Vec<FieldDef>) -> TypeDef {
109        TypeDef {
110            name: name.into(),
111            rust_path: rust_path.into(),
112            original_rust_path: String::new(),
113            fields,
114            methods: vec![],
115            is_opaque: false,
116            is_clone: true,
117            is_trait: false,
118            has_default: false,
119            has_stripped_cfg_fields: false,
120            is_return_type: false,
121            serde_rename_all: None,
122            has_serde: false,
123            super_traits: vec![],
124            doc: String::new(),
125            cfg: None,
126        }
127    }
128
129    fn make_enum(name: &str, rust_path: &str, variants: &[&str]) -> EnumDef {
130        EnumDef {
131            name: name.into(),
132            rust_path: rust_path.into(),
133            original_rust_path: String::new(),
134            variants: variants
135                .iter()
136                .map(|v| EnumVariant {
137                    name: (*v).into(),
138                    fields: vec![],
139                    doc: String::new(),
140                    is_default: false,
141                    serde_rename: None,
142                })
143                .collect(),
144            doc: String::new(),
145            cfg: None,
146            serde_tag: None,
147            serde_rename_all: None,
148        }
149    }
150
151    fn no_opaques() -> AHashSet<String> {
152        AHashSet::new()
153    }
154
155    fn simple_type() -> TypeDef {
156        TypeDef {
157            name: "Config".to_string(),
158            rust_path: "my_crate::Config".to_string(),
159            original_rust_path: String::new(),
160            fields: vec![
161                FieldDef {
162                    name: "name".into(),
163                    ty: TypeRef::String,
164                    optional: false,
165                    default: None,
166                    doc: String::new(),
167                    sanitized: false,
168                    is_boxed: false,
169                    type_rust_path: None,
170                    cfg: None,
171                    typed_default: None,
172                    core_wrapper: CoreWrapper::None,
173                    vec_inner_core_wrapper: CoreWrapper::None,
174                    newtype_wrapper: None,
175                },
176                FieldDef {
177                    name: "timeout".into(),
178                    ty: TypeRef::Primitive(PrimitiveType::U64),
179                    optional: true,
180                    default: None,
181                    doc: String::new(),
182                    sanitized: false,
183                    is_boxed: false,
184                    type_rust_path: None,
185                    cfg: None,
186                    typed_default: None,
187                    core_wrapper: CoreWrapper::None,
188                    vec_inner_core_wrapper: CoreWrapper::None,
189                    newtype_wrapper: None,
190                },
191                FieldDef {
192                    name: "backend".into(),
193                    ty: TypeRef::Named("Backend".into()),
194                    optional: true,
195                    default: None,
196                    doc: String::new(),
197                    sanitized: false,
198                    is_boxed: false,
199                    type_rust_path: None,
200                    cfg: None,
201                    typed_default: None,
202                    core_wrapper: CoreWrapper::None,
203                    vec_inner_core_wrapper: CoreWrapper::None,
204                    newtype_wrapper: None,
205                },
206            ],
207            methods: vec![],
208            is_opaque: false,
209            is_clone: true,
210            is_trait: false,
211            has_default: false,
212            has_stripped_cfg_fields: false,
213            is_return_type: false,
214            serde_rename_all: None,
215            has_serde: false,
216            super_traits: vec![],
217            doc: String::new(),
218            cfg: None,
219        }
220    }
221
222    fn simple_enum() -> EnumDef {
223        EnumDef {
224            name: "Backend".to_string(),
225            rust_path: "my_crate::Backend".to_string(),
226            original_rust_path: String::new(),
227            variants: vec![
228                EnumVariant {
229                    name: "Cpu".into(),
230                    fields: vec![],
231                    doc: String::new(),
232                    is_default: false,
233                    serde_rename: None,
234                },
235                EnumVariant {
236                    name: "Gpu".into(),
237                    fields: vec![],
238                    doc: String::new(),
239                    is_default: false,
240                    serde_rename: None,
241                },
242            ],
243            doc: String::new(),
244            cfg: None,
245            serde_tag: None,
246            serde_rename_all: None,
247        }
248    }
249
250    #[test]
251    fn test_from_binding_to_core() {
252        let typ = simple_type();
253        let result = gen_from_binding_to_core(&typ, "my_crate");
254        assert!(result.contains("impl From<Config> for my_crate::Config"));
255        assert!(result.contains("name: val.name"));
256        assert!(result.contains("timeout: val.timeout"));
257        assert!(result.contains("backend: val.backend.map(Into::into)"));
258    }
259
260    #[test]
261    fn test_from_core_to_binding() {
262        let typ = simple_type();
263        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
264        assert!(result.contains("impl From<my_crate::Config> for Config"));
265    }
266
267    #[test]
268    fn test_enum_from_binding_to_core() {
269        let enum_def = simple_enum();
270        let result = gen_enum_from_binding_to_core(&enum_def, "my_crate");
271        assert!(result.contains("impl From<Backend> for my_crate::Backend"));
272        assert!(result.contains("Backend::Cpu => Self::Cpu"));
273        assert!(result.contains("Backend::Gpu => Self::Gpu"));
274    }
275
276    #[test]
277    fn test_enum_from_core_to_binding() {
278        let enum_def = simple_enum();
279        let result = gen_enum_from_core_to_binding(&enum_def, "my_crate");
280        assert!(result.contains("impl From<my_crate::Backend> for Backend"));
281        assert!(result.contains("my_crate::Backend::Cpu => Self::Cpu"));
282        assert!(result.contains("my_crate::Backend::Gpu => Self::Gpu"));
283    }
284
285    #[test]
286    fn test_from_binding_to_core_with_cfg_gated_field() {
287        // Create a type with a cfg-gated field
288        let mut typ = simple_type();
289        typ.has_stripped_cfg_fields = true;
290        typ.fields.push(FieldDef {
291            name: "layout".into(),
292            ty: TypeRef::String,
293            optional: false,
294            default: None,
295            doc: String::new(),
296            sanitized: false,
297            is_boxed: false,
298            type_rust_path: None,
299            cfg: Some("feature = \"layout-detection\"".into()),
300            typed_default: None,
301            core_wrapper: CoreWrapper::None,
302            vec_inner_core_wrapper: CoreWrapper::None,
303            newtype_wrapper: None,
304        });
305
306        let result = gen_from_binding_to_core(&typ, "my_crate");
307
308        // The impl should exist
309        assert!(result.contains("impl From<Config> for my_crate::Config"));
310        // Regular fields should be present
311        assert!(result.contains("name: val.name"));
312        assert!(result.contains("timeout: val.timeout"));
313        // cfg-gated field should NOT be accessed from val (it doesn't exist in binding struct)
314        assert!(!result.contains("layout: val.layout"));
315        // But ..Default::default() should be present to fill cfg-gated fields
316        assert!(result.contains("..Default::default()"));
317    }
318
319    #[test]
320    fn test_from_core_to_binding_with_cfg_gated_field() {
321        // Create a type with a cfg-gated field
322        let mut typ = simple_type();
323        typ.fields.push(FieldDef {
324            name: "layout".into(),
325            ty: TypeRef::String,
326            optional: false,
327            default: None,
328            doc: String::new(),
329            sanitized: false,
330            is_boxed: false,
331            type_rust_path: None,
332            cfg: Some("feature = \"layout-detection\"".into()),
333            typed_default: None,
334            core_wrapper: CoreWrapper::None,
335            vec_inner_core_wrapper: CoreWrapper::None,
336            newtype_wrapper: None,
337        });
338
339        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
340
341        // The impl should exist
342        assert!(result.contains("impl From<my_crate::Config> for Config"));
343        // Regular fields should be present
344        assert!(result.contains("name: val.name"));
345        // cfg-gated field should NOT be in the struct literal
346        assert!(!result.contains("layout:"));
347    }
348
349    // -----------------------------------------------------------------------
350    // helpers.rs: field_conversion_to_core (binding → core)
351    // -----------------------------------------------------------------------
352
353    #[test]
354    fn test_field_conversion_to_core_string() {
355        let result = field_conversion_to_core("label", &TypeRef::String, false);
356        assert_eq!(result, "label: val.label");
357    }
358
359    #[test]
360    fn test_field_conversion_to_core_primitive() {
361        let result = field_conversion_to_core("count", &TypeRef::Primitive(PrimitiveType::I32), false);
362        assert_eq!(result, "count: val.count");
363    }
364
365    #[test]
366    fn test_field_conversion_to_core_bytes() {
367        let result = field_conversion_to_core("data", &TypeRef::Bytes, false);
368        assert_eq!(result, "data: val.data");
369    }
370
371    #[test]
372    fn test_field_conversion_to_core_unit() {
373        let result = field_conversion_to_core("nothing", &TypeRef::Unit, false);
374        assert_eq!(result, "nothing: val.nothing");
375    }
376
377    #[test]
378    fn test_field_conversion_to_core_duration_non_optional() {
379        let result = field_conversion_to_core("timeout", &TypeRef::Duration, false);
380        assert_eq!(result, "timeout: std::time::Duration::from_millis(val.timeout)");
381    }
382
383    #[test]
384    fn test_field_conversion_to_core_duration_optional() {
385        let result = field_conversion_to_core("timeout", &TypeRef::Duration, true);
386        assert_eq!(result, "timeout: val.timeout.map(std::time::Duration::from_millis)");
387    }
388
389    #[test]
390    fn test_field_conversion_to_core_path_non_optional() {
391        let result = field_conversion_to_core("file", &TypeRef::Path, false);
392        assert_eq!(result, "file: val.file.into()");
393    }
394
395    #[test]
396    fn test_field_conversion_to_core_path_optional() {
397        let result = field_conversion_to_core("file", &TypeRef::Path, true);
398        assert_eq!(result, "file: val.file.map(Into::into)");
399    }
400
401    #[test]
402    fn test_field_conversion_to_core_json_non_optional() {
403        let result = field_conversion_to_core("meta", &TypeRef::Json, false);
404        assert_eq!(result, "meta: serde_json::from_str(&val.meta).unwrap_or_default()");
405    }
406
407    #[test]
408    fn test_field_conversion_to_core_json_optional() {
409        let result = field_conversion_to_core("meta", &TypeRef::Json, true);
410        assert_eq!(
411            result,
412            "meta: val.meta.as_ref().and_then(|s| serde_json::from_str(s).ok())"
413        );
414    }
415
416    #[test]
417    fn test_field_conversion_to_core_char_non_optional() {
418        let result = field_conversion_to_core("sep", &TypeRef::Char, false);
419        assert_eq!(result, "sep: val.sep.chars().next().unwrap_or('*')");
420    }
421
422    #[test]
423    fn test_field_conversion_to_core_char_optional() {
424        let result = field_conversion_to_core("sep", &TypeRef::Char, true);
425        assert_eq!(result, "sep: val.sep.and_then(|s| s.chars().next())");
426    }
427
428    #[test]
429    fn test_field_conversion_to_core_named_non_optional() {
430        let result = field_conversion_to_core("backend", &TypeRef::Named("Backend".into()), false);
431        assert_eq!(result, "backend: val.backend.into()");
432    }
433
434    #[test]
435    fn test_field_conversion_to_core_named_optional() {
436        let result = field_conversion_to_core("backend", &TypeRef::Named("Backend".into()), true);
437        assert_eq!(result, "backend: val.backend.map(Into::into)");
438    }
439
440    #[test]
441    fn test_field_conversion_to_core_named_tuple_type_is_passthrough() {
442        // Tuple type names (starting with '(') are passthrough — no conversion
443        let result = field_conversion_to_core("pair", &TypeRef::Named("(String, u32)".into()), false);
444        assert_eq!(result, "pair: val.pair");
445    }
446
447    #[test]
448    fn test_field_conversion_to_core_vec_named() {
449        let ty = TypeRef::Vec(Box::new(TypeRef::Named("Item".into())));
450        let result = field_conversion_to_core("items", &ty, false);
451        assert_eq!(result, "items: val.items.into_iter().map(Into::into).collect()");
452    }
453
454    #[test]
455    fn test_field_conversion_to_core_vec_named_optional() {
456        let ty = TypeRef::Vec(Box::new(TypeRef::Named("Item".into())));
457        let result = field_conversion_to_core("items", &ty, true);
458        assert_eq!(
459            result,
460            "items: val.items.map(|v| v.into_iter().map(Into::into).collect())"
461        );
462    }
463
464    #[test]
465    fn test_field_conversion_to_core_vec_tuple_passthrough() {
466        let ty = TypeRef::Vec(Box::new(TypeRef::Named("(u32, u32)".into())));
467        let result = field_conversion_to_core("pairs", &ty, false);
468        assert_eq!(result, "pairs: val.pairs");
469    }
470
471    #[test]
472    fn test_field_conversion_to_core_vec_json() {
473        let ty = TypeRef::Vec(Box::new(TypeRef::Json));
474        let result = field_conversion_to_core("items", &ty, false);
475        assert_eq!(
476            result,
477            "items: val.items.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()"
478        );
479    }
480
481    #[test]
482    fn test_field_conversion_to_core_optional_named() {
483        let ty = TypeRef::Optional(Box::new(TypeRef::Named("Config".into())));
484        let result = field_conversion_to_core("config", &ty, false);
485        assert_eq!(result, "config: val.config.map(Into::into)");
486    }
487
488    #[test]
489    fn test_field_conversion_to_core_optional_vec_named() {
490        let ty = TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Item".into())))));
491        let result = field_conversion_to_core("items", &ty, false);
492        assert_eq!(
493            result,
494            "items: val.items.map(|v| v.into_iter().map(Into::into).collect())"
495        );
496    }
497
498    #[test]
499    fn test_field_conversion_to_core_map_string_string() {
500        let ty = TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String));
501        let result = field_conversion_to_core("map", &ty, false);
502        assert_eq!(result, "map: val.map.into_iter().collect()");
503    }
504
505    #[test]
506    fn test_field_conversion_to_core_map_string_json() {
507        let ty = TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Json));
508        let result = field_conversion_to_core("map", &ty, false);
509        assert!(result.contains("serde_json::from_str(&v)"));
510    }
511
512    #[test]
513    fn test_field_conversion_to_core_map_named_values() {
514        let ty = TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Named("Val".into())));
515        let result = field_conversion_to_core("map", &ty, false);
516        assert!(result.contains("v.into()"));
517    }
518
519    // -----------------------------------------------------------------------
520    // helpers.rs: field_conversion_from_core (core → binding)
521    // -----------------------------------------------------------------------
522
523    #[test]
524    fn test_field_conversion_from_core_string() {
525        let result = field_conversion_from_core("label", &TypeRef::String, false, false, &no_opaques());
526        assert_eq!(result, "label: val.label");
527    }
528
529    #[test]
530    fn test_field_conversion_from_core_duration_non_optional() {
531        let result = field_conversion_from_core("timeout", &TypeRef::Duration, false, false, &no_opaques());
532        assert_eq!(result, "timeout: val.timeout.as_millis() as u64");
533    }
534
535    #[test]
536    fn test_field_conversion_from_core_duration_optional() {
537        let result = field_conversion_from_core("timeout", &TypeRef::Duration, true, false, &no_opaques());
538        assert_eq!(result, "timeout: val.timeout.map(|d| d.as_millis() as u64)");
539    }
540
541    #[test]
542    fn test_field_conversion_from_core_path_non_optional() {
543        let result = field_conversion_from_core("file", &TypeRef::Path, false, false, &no_opaques());
544        assert_eq!(result, "file: val.file.to_string_lossy().to_string()");
545    }
546
547    #[test]
548    fn test_field_conversion_from_core_path_optional() {
549        let result = field_conversion_from_core("file", &TypeRef::Path, true, false, &no_opaques());
550        assert_eq!(result, "file: val.file.map(|p| p.to_string_lossy().to_string())");
551    }
552
553    #[test]
554    fn test_field_conversion_from_core_char_non_optional() {
555        let result = field_conversion_from_core("sep", &TypeRef::Char, false, false, &no_opaques());
556        assert_eq!(result, "sep: val.sep.to_string()");
557    }
558
559    #[test]
560    fn test_field_conversion_from_core_char_optional() {
561        let result = field_conversion_from_core("sep", &TypeRef::Char, true, false, &no_opaques());
562        assert_eq!(result, "sep: val.sep.map(|c| c.to_string())");
563    }
564
565    #[test]
566    fn test_field_conversion_from_core_bytes_non_optional() {
567        let result = field_conversion_from_core("data", &TypeRef::Bytes, false, false, &no_opaques());
568        assert_eq!(result, "data: val.data.to_vec()");
569    }
570
571    #[test]
572    fn test_field_conversion_from_core_bytes_optional() {
573        let result = field_conversion_from_core("data", &TypeRef::Bytes, true, false, &no_opaques());
574        assert_eq!(result, "data: val.data.map(|v| v.to_vec())");
575    }
576
577    #[test]
578    fn test_field_conversion_from_core_json_non_optional() {
579        let result = field_conversion_from_core("meta", &TypeRef::Json, false, false, &no_opaques());
580        assert_eq!(result, "meta: val.meta.to_string()");
581    }
582
583    #[test]
584    fn test_field_conversion_from_core_json_optional() {
585        let result = field_conversion_from_core("meta", &TypeRef::Json, true, false, &no_opaques());
586        assert_eq!(result, "meta: val.meta.as_ref().map(ToString::to_string)");
587    }
588
589    #[test]
590    fn test_field_conversion_from_core_named_non_opaque() {
591        // Non-opaque Named uses .into() (symmetric with binding_to_core)
592        let result = field_conversion_from_core(
593            "backend",
594            &TypeRef::Named("Backend".into()),
595            false,
596            false,
597            &no_opaques(),
598        );
599        assert_eq!(result, "backend: val.backend.into()");
600    }
601
602    #[test]
603    fn test_field_conversion_from_core_opaque_non_optional() {
604        let mut opaques = AHashSet::new();
605        opaques.insert("Client".to_string());
606        let result = field_conversion_from_core("client", &TypeRef::Named("Client".into()), false, false, &opaques);
607        assert_eq!(result, "client: Client { inner: Arc::new(val.client) }");
608    }
609
610    #[test]
611    fn test_field_conversion_from_core_opaque_optional() {
612        let mut opaques = AHashSet::new();
613        opaques.insert("Client".to_string());
614        let result = field_conversion_from_core("client", &TypeRef::Named("Client".into()), true, false, &opaques);
615        assert_eq!(result, "client: val.client.map(|v| Client { inner: Arc::new(v) })");
616    }
617
618    #[test]
619    fn test_field_conversion_from_core_vec_json() {
620        let ty = TypeRef::Vec(Box::new(TypeRef::Json));
621        let result = field_conversion_from_core("items", &ty, false, false, &no_opaques());
622        assert_eq!(result, "items: val.items.iter().map(ToString::to_string).collect()");
623    }
624
625    #[test]
626    fn test_field_conversion_from_core_map_json_values() {
627        let ty = TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Json));
628        let result = field_conversion_from_core("map", &ty, false, false, &no_opaques());
629        assert!(result.contains("v.to_string()"));
630    }
631
632    #[test]
633    fn test_field_conversion_from_core_sanitized_string() {
634        // Sanitized String field uses Debug formatting
635        let result = field_conversion_from_core("value", &TypeRef::String, false, true, &no_opaques());
636        assert_eq!(result, "value: format!(\"{:?}\", val.value)");
637    }
638
639    #[test]
640    fn test_field_conversion_from_core_sanitized_string_optional() {
641        let result = field_conversion_from_core("value", &TypeRef::String, true, true, &no_opaques());
642        assert_eq!(result, "value: val.value.as_ref().map(|v| format!(\"{v:?}\"))");
643    }
644
645    #[test]
646    fn test_field_conversion_from_core_sanitized_named_non_optional() {
647        // Sanitized Named non-optional → empty String (type may not have Debug)
648        let result = field_conversion_from_core("obj", &TypeRef::Named("Opaque".into()), false, true, &no_opaques());
649        assert_eq!(result, "obj: String::new()");
650    }
651
652    #[test]
653    fn test_field_conversion_from_core_sanitized_named_optional() {
654        // Sanitized Named optional → None
655        let result = field_conversion_from_core("obj", &TypeRef::Named("Opaque".into()), true, true, &no_opaques());
656        assert_eq!(result, "obj: None");
657    }
658
659    #[test]
660    fn test_field_conversion_from_core_sanitized_vec_string() {
661        let ty = TypeRef::Vec(Box::new(TypeRef::String));
662        let result = field_conversion_from_core("tags", &ty, false, true, &no_opaques());
663        // Generated code contains a format!("{:?}", i) expression inside .map()
664        assert!(result.contains(r#"format!("{:?}", i)"#));
665        assert!(result.contains(".iter().map("));
666    }
667
668    #[test]
669    fn test_field_conversion_from_core_sanitized_map_string_string() {
670        let ty = TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String));
671        let result = field_conversion_from_core("headers", &ty, false, true, &no_opaques());
672        assert!(result.contains("k.to_string()"));
673        assert!(result.contains("v.to_string()"));
674    }
675
676    // -----------------------------------------------------------------------
677    // helpers.rs: is_tuple_variant / is_newtype / is_tuple_type_name
678    // -----------------------------------------------------------------------
679
680    #[test]
681    fn test_is_tuple_variant_true_for_positional_fields() {
682        let fields = vec![
683            make_field("_0", TypeRef::String),
684            make_field("_1", TypeRef::Primitive(PrimitiveType::I32)),
685        ];
686        assert!(is_tuple_variant(&fields));
687    }
688
689    #[test]
690    fn test_is_tuple_variant_false_for_named_fields() {
691        let fields = vec![make_field("name", TypeRef::String)];
692        assert!(!is_tuple_variant(&fields));
693    }
694
695    #[test]
696    fn test_is_tuple_variant_false_for_empty_fields() {
697        assert!(!is_tuple_variant(&[]));
698    }
699
700    #[test]
701    fn test_is_tuple_type_name_true() {
702        assert!(helpers::is_tuple_type_name("(String, u32)"));
703    }
704
705    #[test]
706    fn test_is_tuple_type_name_false() {
707        assert!(!helpers::is_tuple_type_name("Config"));
708    }
709
710    // -----------------------------------------------------------------------
711    // helpers.rs: field_references_excluded_type
712    // -----------------------------------------------------------------------
713
714    #[test]
715    fn test_field_references_excluded_type_direct_match() {
716        let ty = TypeRef::Named("JsValue".into());
717        assert!(field_references_excluded_type(&ty, &["JsValue".to_string()]));
718    }
719
720    #[test]
721    fn test_field_references_excluded_type_no_match() {
722        let ty = TypeRef::Named("Config".into());
723        assert!(!field_references_excluded_type(&ty, &["JsValue".to_string()]));
724    }
725
726    #[test]
727    fn test_field_references_excluded_type_inside_optional() {
728        let ty = TypeRef::Optional(Box::new(TypeRef::Named("Excluded".into())));
729        assert!(field_references_excluded_type(&ty, &["Excluded".to_string()]));
730    }
731
732    #[test]
733    fn test_field_references_excluded_type_inside_vec() {
734        let ty = TypeRef::Vec(Box::new(TypeRef::Named("Excluded".into())));
735        assert!(field_references_excluded_type(&ty, &["Excluded".to_string()]));
736    }
737
738    #[test]
739    fn test_field_references_excluded_type_inside_map_key() {
740        let ty = TypeRef::Map(Box::new(TypeRef::Named("Excluded".into())), Box::new(TypeRef::String));
741        assert!(field_references_excluded_type(&ty, &["Excluded".to_string()]));
742    }
743
744    #[test]
745    fn test_field_references_excluded_type_inside_map_value() {
746        let ty = TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Named("Excluded".into())));
747        assert!(field_references_excluded_type(&ty, &["Excluded".to_string()]));
748    }
749
750    #[test]
751    fn test_field_references_excluded_type_primitive_not_excluded() {
752        let ty = TypeRef::Primitive(PrimitiveType::I32);
753        assert!(!field_references_excluded_type(&ty, &["JsValue".to_string()]));
754    }
755
756    // -----------------------------------------------------------------------
757    // helpers.rs: can_generate_enum_conversion / can_generate_enum_conversion_from_core
758    // -----------------------------------------------------------------------
759
760    #[test]
761    fn test_can_generate_enum_conversion_with_variants() {
762        let e = make_enum("Color", "crate::Color", &["Red", "Green"]);
763        assert!(can_generate_enum_conversion(&e));
764    }
765
766    #[test]
767    fn test_can_generate_enum_conversion_empty_variants() {
768        let e = make_enum("Empty", "crate::Empty", &[]);
769        assert!(!can_generate_enum_conversion(&e));
770    }
771
772    #[test]
773    fn test_can_generate_enum_conversion_from_core_with_variants() {
774        let e = make_enum("Color", "crate::Color", &["Red"]);
775        assert!(can_generate_enum_conversion_from_core(&e));
776    }
777
778    // -----------------------------------------------------------------------
779    // helpers.rs: core_type_path / core_enum_path
780    // -----------------------------------------------------------------------
781
782    #[test]
783    fn test_core_type_path_with_full_path() {
784        let typ = make_type("Config", "my_crate::types::Config", vec![]);
785        assert_eq!(core_type_path(&typ, "my_crate"), "my_crate::types::Config");
786    }
787
788    #[test]
789    fn test_core_type_path_with_bare_name() {
790        // When rust_path has no "::", prefix with core_import::name
791        let typ = make_type("Config", "Config", vec![]);
792        assert_eq!(core_type_path(&typ, "my_crate"), "my_crate::Config");
793    }
794
795    #[test]
796    fn test_core_type_path_normalizes_hyphens() {
797        let typ = make_type("Config", "my-crate::Config", vec![]);
798        assert_eq!(core_type_path(&typ, "my_crate"), "my_crate::Config");
799    }
800
801    #[test]
802    fn test_core_enum_path_with_full_path() {
803        let e = make_enum("Backend", "my_crate::Backend", &[]);
804        assert_eq!(core_enum_path(&e, "my_crate"), "my_crate::Backend");
805    }
806
807    #[test]
808    fn test_core_enum_path_bare_name_gets_prefixed() {
809        let e = make_enum("Backend", "Backend", &[]);
810        assert_eq!(core_enum_path(&e, "my_crate"), "my_crate::Backend");
811    }
812
813    // -----------------------------------------------------------------------
814    // helpers.rs: build_type_path_map / resolve_named_path
815    // -----------------------------------------------------------------------
816
817    #[test]
818    fn test_build_type_path_map_includes_types_and_enums() {
819        let surface = ApiSurface {
820            crate_name: "my_crate".into(),
821            version: "1.0.0".into(),
822            types: vec![make_type("Config", "my_crate::Config", vec![])],
823            functions: vec![],
824            enums: vec![make_enum("Mode", "my_crate::Mode", &["A"])],
825            errors: vec![],
826        };
827        let map = build_type_path_map(&surface, "my_crate");
828        assert_eq!(map.get("Config").map(String::as_str), Some("my_crate::Config"));
829        assert_eq!(map.get("Mode").map(String::as_str), Some("my_crate::Mode"));
830    }
831
832    #[test]
833    fn test_resolve_named_path_found_in_map() {
834        let mut map = ahash::AHashMap::new();
835        map.insert("Config".to_string(), "my_crate::types::Config".to_string());
836        assert_eq!(
837            resolve_named_path("Config", "my_crate", &map),
838            "my_crate::types::Config"
839        );
840    }
841
842    #[test]
843    fn test_resolve_named_path_not_found_falls_back() {
844        let map = ahash::AHashMap::new();
845        assert_eq!(resolve_named_path("Unknown", "my_crate", &map), "my_crate::Unknown");
846    }
847
848    // -----------------------------------------------------------------------
849    // helpers.rs: input_type_names
850    // -----------------------------------------------------------------------
851
852    #[test]
853    fn test_input_type_names_from_function_params() {
854        let surface = ApiSurface {
855            crate_name: "my_crate".into(),
856            version: "1.0.0".into(),
857            types: vec![],
858            functions: vec![FunctionDef {
859                name: "process".into(),
860                rust_path: "my_crate::process".into(),
861                original_rust_path: String::new(),
862                params: vec![ParamDef {
863                    name: "config".into(),
864                    ty: TypeRef::Named("Config".into()),
865                    optional: false,
866                    default: None,
867                    sanitized: false,
868                    typed_default: None,
869                    is_ref: false,
870                    is_mut: false,
871                    newtype_wrapper: None,
872                }],
873                return_type: TypeRef::Unit,
874                is_async: false,
875                error_type: None,
876                doc: String::new(),
877                cfg: None,
878                sanitized: false,
879                returns_ref: false,
880                returns_cow: false,
881                return_newtype_wrapper: None,
882            }],
883            enums: vec![],
884            errors: vec![],
885        };
886        let names = input_type_names(&surface);
887        assert!(names.contains("Config"));
888    }
889
890    #[test]
891    fn test_input_type_names_from_return_types() {
892        let surface = ApiSurface {
893            crate_name: "my_crate".into(),
894            version: "1.0.0".into(),
895            types: vec![],
896            functions: vec![FunctionDef {
897                name: "get_result".into(),
898                rust_path: "my_crate::get_result".into(),
899                original_rust_path: String::new(),
900                params: vec![],
901                return_type: TypeRef::Named("Result".into()),
902                is_async: false,
903                error_type: None,
904                doc: String::new(),
905                cfg: None,
906                sanitized: false,
907                returns_ref: false,
908                returns_cow: false,
909                return_newtype_wrapper: None,
910            }],
911            enums: vec![],
912            errors: vec![],
913        };
914        let names = input_type_names(&surface);
915        assert!(names.contains("Result"));
916    }
917
918    // -----------------------------------------------------------------------
919    // helpers.rs: binding_to_core_match_arm / core_to_binding_match_arm
920    // -----------------------------------------------------------------------
921
922    #[test]
923    fn test_binding_to_core_match_arm_unit_variant() {
924        let result = binding_to_core_match_arm("MyEnum", "Foo", &[]);
925        assert_eq!(result, "MyEnum::Foo => Self::Foo,");
926    }
927
928    #[test]
929    fn test_binding_to_core_match_arm_data_variant_no_binding_data() {
930        // When binding is unit-only and core has named fields, use Default
931        let fields = vec![make_field("value", TypeRef::String)];
932        let result = helpers::binding_to_core_match_arm_ext("MyEnum", "Foo", &fields, false);
933        assert!(result.contains("value: Default::default()"));
934    }
935
936    #[test]
937    fn test_binding_to_core_match_arm_tuple_variant_no_binding_data() {
938        let fields = vec![make_field("_0", TypeRef::String)];
939        let result = helpers::binding_to_core_match_arm_ext("MyEnum", "Foo", &fields, false);
940        assert!(result.contains("Default::default()"));
941        assert!(result.contains("Self::Foo("));
942    }
943
944    #[test]
945    fn test_binding_to_core_match_arm_data_variant_with_binding_data_named() {
946        // Binding has data — destructure and convert
947        let fields = vec![make_field("value", TypeRef::Named("Inner".into()))];
948        let result = helpers::binding_to_core_match_arm_ext("MyEnum", "Foo", &fields, true);
949        assert!(result.contains("value: value.into()"));
950    }
951
952    #[test]
953    fn test_binding_to_core_match_arm_data_variant_with_binding_data_tuple() {
954        let fields = vec![make_field("_0", TypeRef::Named("Inner".into()))];
955        let result = helpers::binding_to_core_match_arm_ext("MyEnum", "Foo", &fields, true);
956        assert!(result.contains("_0.into()"));
957    }
958
959    #[test]
960    fn test_core_to_binding_match_arm_unit_variant() {
961        let result = core_to_binding_match_arm("CoreEnum", "Bar", &[]);
962        assert_eq!(result, "CoreEnum::Bar => Self::Bar,");
963    }
964
965    #[test]
966    fn test_core_to_binding_match_arm_data_named_no_binding_data() {
967        let fields = vec![make_field("x", TypeRef::Primitive(PrimitiveType::I32))];
968        let result = helpers::core_to_binding_match_arm_ext("CoreEnum", "Bar", &fields, false);
969        assert!(result.contains("{ .. }"));
970        assert!(result.contains("Self::Bar"));
971    }
972
973    #[test]
974    fn test_core_to_binding_match_arm_data_tuple_no_binding_data() {
975        let fields = vec![make_field("_0", TypeRef::Primitive(PrimitiveType::I32))];
976        let result = helpers::core_to_binding_match_arm_ext("CoreEnum", "Bar", &fields, false);
977        assert!(result.contains("(..)"));
978        assert!(result.contains("Self::Bar"));
979    }
980
981    // -----------------------------------------------------------------------
982    // helpers.rs: has_sanitized_fields
983    // -----------------------------------------------------------------------
984
985    #[test]
986    fn test_has_sanitized_fields_false() {
987        let typ = make_type("Foo", "crate::Foo", vec![make_field("x", TypeRef::String)]);
988        assert!(!has_sanitized_fields(&typ));
989    }
990
991    #[test]
992    fn test_has_sanitized_fields_true() {
993        let mut field = make_field("x", TypeRef::String);
994        field.sanitized = true;
995        let typ = make_type("Foo", "crate::Foo", vec![field]);
996        assert!(has_sanitized_fields(&typ));
997    }
998
999    // -----------------------------------------------------------------------
1000    // binding_to_core.rs: gen_from_binding_to_core — various field types
1001    // -----------------------------------------------------------------------
1002
1003    #[test]
1004    fn test_gen_from_binding_to_core_string_field() {
1005        let typ = make_type("S", "c::S", vec![make_field("title", TypeRef::String)]);
1006        let result = gen_from_binding_to_core(&typ, "c");
1007        assert!(result.contains("impl From<S> for c::S"));
1008        assert!(result.contains("title: val.title"));
1009    }
1010
1011    #[test]
1012    fn test_gen_from_binding_to_core_duration_field() {
1013        let typ = make_type("S", "c::S", vec![make_field("timeout", TypeRef::Duration)]);
1014        let result = gen_from_binding_to_core(&typ, "c");
1015        assert!(result.contains("std::time::Duration::from_millis(val.timeout)"));
1016    }
1017
1018    #[test]
1019    fn test_gen_from_binding_to_core_optional_named_field() {
1020        let field = make_opt_field("backend", TypeRef::Named("Backend".into()));
1021        let typ = make_type("S", "c::S", vec![field]);
1022        let result = gen_from_binding_to_core(&typ, "c");
1023        assert!(result.contains("backend: val.backend.map(Into::into)"));
1024    }
1025
1026    #[test]
1027    fn test_gen_from_binding_to_core_vec_named_field() {
1028        let field = make_field("items", TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))));
1029        let typ = make_type("S", "c::S", vec![field]);
1030        let result = gen_from_binding_to_core(&typ, "c");
1031        assert!(result.contains("into_iter().map(Into::into).collect()"));
1032    }
1033
1034    #[test]
1035    fn test_gen_from_binding_to_core_sanitized_field_uses_default() {
1036        let mut field = make_field("complex", TypeRef::String);
1037        field.sanitized = true;
1038        let typ = make_type("S", "c::S", vec![field]);
1039        let result = gen_from_binding_to_core(&typ, "c");
1040        assert!(result.contains("complex: Default::default()"));
1041    }
1042
1043    #[test]
1044    fn test_gen_from_binding_to_core_with_stripped_cfg_fields_uses_default_update() {
1045        let mut typ = make_type("S", "c::S", vec![make_field("x", TypeRef::String)]);
1046        typ.has_stripped_cfg_fields = true;
1047        let result = gen_from_binding_to_core(&typ, "c");
1048        assert!(result.contains("..Default::default()"));
1049        assert!(result.contains("#[allow(clippy::needless_update)]"));
1050    }
1051
1052    #[test]
1053    fn test_gen_from_binding_to_core_with_type_name_prefix() {
1054        let typ = make_type("Config", "c::Config", vec![make_field("x", TypeRef::String)]);
1055        let config = ConversionConfig {
1056            type_name_prefix: "Js",
1057            ..Default::default()
1058        };
1059        let result = gen_from_binding_to_core_cfg(&typ, "c", &config);
1060        assert!(result.contains("impl From<JsConfig> for c::Config"));
1061    }
1062
1063    #[test]
1064    fn test_gen_from_binding_to_core_path_field() {
1065        let field = make_field("file", TypeRef::Path);
1066        let typ = make_type("S", "c::S", vec![field]);
1067        let result = gen_from_binding_to_core(&typ, "c");
1068        assert!(result.contains("file: val.file.into()"));
1069    }
1070
1071    #[test]
1072    fn test_gen_from_binding_to_core_json_field() {
1073        let field = make_field("meta", TypeRef::Json);
1074        let typ = make_type("S", "c::S", vec![field]);
1075        let result = gen_from_binding_to_core(&typ, "c");
1076        assert!(result.contains("serde_json::from_str(&val.meta).unwrap_or_default()"));
1077    }
1078
1079    #[test]
1080    fn test_gen_from_binding_to_core_newtype_struct() {
1081        // A newtype is a struct with single field named "_0"
1082        let field = make_field("_0", TypeRef::Primitive(PrimitiveType::U32));
1083        let typ = make_type("NodeIndex", "c::NodeIndex", vec![field]);
1084        let result = gen_from_binding_to_core(&typ, "c");
1085        assert!(result.contains("Self(val._0)"));
1086    }
1087
1088    #[test]
1089    fn test_gen_from_binding_to_core_newtype_named_field() {
1090        let field = make_field("_0", TypeRef::Named("Inner".into()));
1091        let typ = make_type("Wrapper", "c::Wrapper", vec![field]);
1092        let result = gen_from_binding_to_core(&typ, "c");
1093        assert!(result.contains("Self(val._0.into())"));
1094    }
1095
1096    #[test]
1097    fn test_gen_from_binding_to_core_newtype_path_field() {
1098        let field = make_field("_0", TypeRef::Path);
1099        let typ = make_type("PathWrapper", "c::PathWrapper", vec![field]);
1100        let result = gen_from_binding_to_core(&typ, "c");
1101        assert!(result.contains("Self(val._0.into())"));
1102    }
1103
1104    #[test]
1105    fn test_gen_from_binding_to_core_newtype_duration_field() {
1106        let field = make_field("_0", TypeRef::Duration);
1107        let typ = make_type("DurWrapper", "c::DurWrapper", vec![field]);
1108        let result = gen_from_binding_to_core(&typ, "c");
1109        assert!(result.contains("Self(std::time::Duration::from_millis(val._0))"));
1110    }
1111
1112    #[test]
1113    fn test_gen_from_binding_to_core_boxed_named_field() {
1114        let mut field = make_field("child", TypeRef::Named("Child".into()));
1115        field.is_boxed = true;
1116        let typ = make_type("S", "c::S", vec![field]);
1117        let result = gen_from_binding_to_core(&typ, "c");
1118        assert!(result.contains("Box::new("));
1119    }
1120
1121    #[test]
1122    fn test_gen_from_binding_to_core_cast_large_ints_to_i64() {
1123        let field = make_field("count", TypeRef::Primitive(PrimitiveType::U64));
1124        let typ = make_type("S", "c::S", vec![field]);
1125        let config = ConversionConfig {
1126            cast_large_ints_to_i64: true,
1127            ..Default::default()
1128        };
1129        let result = gen_from_binding_to_core_cfg(&typ, "c", &config);
1130        assert!(result.contains("val.count as u64"));
1131    }
1132
1133    #[test]
1134    fn test_gen_from_binding_to_core_exclude_types_skips_field() {
1135        let field = make_field("js_val", TypeRef::Named("JsValue".into()));
1136        let typ = make_type("S", "c::S", vec![field]);
1137        let excluded = vec!["JsValue".to_string()];
1138        let config = ConversionConfig {
1139            exclude_types: &excluded,
1140            ..Default::default()
1141        };
1142        let result = gen_from_binding_to_core_cfg(&typ, "c", &config);
1143        assert!(result.contains("js_val: Default::default()"));
1144    }
1145
1146    // -----------------------------------------------------------------------
1147    // core_to_binding.rs: gen_from_core_to_binding — various field types
1148    // -----------------------------------------------------------------------
1149
1150    #[test]
1151    fn test_gen_from_core_to_binding_string_field() {
1152        let typ = make_type("S", "c::S", vec![make_field("title", TypeRef::String)]);
1153        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1154        assert!(result.contains("impl From<c::S> for S"));
1155        assert!(result.contains("title: val.title"));
1156    }
1157
1158    #[test]
1159    fn test_gen_from_core_to_binding_duration_field() {
1160        let field = make_field("timeout", TypeRef::Duration);
1161        let typ = make_type("S", "c::S", vec![field]);
1162        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1163        assert!(result.contains("val.timeout.as_millis() as u64"));
1164    }
1165
1166    #[test]
1167    fn test_gen_from_core_to_binding_path_field() {
1168        let field = make_field("path", TypeRef::Path);
1169        let typ = make_type("S", "c::S", vec![field]);
1170        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1171        assert!(result.contains("to_string_lossy().to_string()"));
1172    }
1173
1174    #[test]
1175    fn test_gen_from_core_to_binding_json_field() {
1176        let field = make_field("meta", TypeRef::Json);
1177        let typ = make_type("S", "c::S", vec![field]);
1178        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1179        assert!(result.contains("val.meta.to_string()"));
1180    }
1181
1182    #[test]
1183    fn test_gen_from_core_to_binding_opaque_field() {
1184        let mut opaques = AHashSet::new();
1185        opaques.insert("Client".to_string());
1186        let field = make_field("client", TypeRef::Named("Client".into()));
1187        let typ = make_type("S", "c::S", vec![field]);
1188        let result = gen_from_core_to_binding(&typ, "c", &opaques);
1189        assert!(result.contains("Arc::new(val.client)"));
1190        assert!(result.contains("Client { inner:"));
1191    }
1192
1193    #[test]
1194    fn test_gen_from_core_to_binding_with_type_name_prefix_opaque() {
1195        let mut opaques = AHashSet::new();
1196        opaques.insert("Client".to_string());
1197        let field = make_field("client", TypeRef::Named("Client".into()));
1198        let typ = make_type("S", "c::S", vec![field]);
1199        let config = ConversionConfig {
1200            type_name_prefix: "Js",
1201            ..Default::default()
1202        };
1203        let result = gen_from_core_to_binding_cfg(&typ, "c", &opaques, &config);
1204        assert!(result.contains("JsClient { inner: Arc::new(val.client) }"));
1205    }
1206
1207    #[test]
1208    fn test_gen_from_core_to_binding_sanitized_field() {
1209        let mut field = make_field("complex", TypeRef::String);
1210        field.sanitized = true;
1211        let typ = make_type("S", "c::S", vec![field]);
1212        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1213        assert!(result.contains("format!("));
1214    }
1215
1216    #[test]
1217    fn test_gen_from_core_to_binding_newtype_struct() {
1218        let field = make_field("_0", TypeRef::Primitive(PrimitiveType::U32));
1219        let typ = make_type("NodeIndex", "c::NodeIndex", vec![field]);
1220        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1221        assert!(result.contains("Self { _0: val.0 }"));
1222    }
1223
1224    #[test]
1225    fn test_gen_from_core_to_binding_newtype_path_field() {
1226        let field = make_field("_0", TypeRef::Path);
1227        let typ = make_type("PathWrapper", "c::PathWrapper", vec![field]);
1228        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1229        assert!(result.contains("val.0.to_string_lossy().to_string()"));
1230    }
1231
1232    #[test]
1233    fn test_gen_from_core_to_binding_newtype_duration_field() {
1234        let field = make_field("_0", TypeRef::Duration);
1235        let typ = make_type("DurWrapper", "c::DurWrapper", vec![field]);
1236        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1237        assert!(result.contains("val.0.as_millis() as u64"));
1238    }
1239
1240    #[test]
1241    fn test_gen_from_core_to_binding_newtype_named_field() {
1242        let field = make_field("_0", TypeRef::Named("Inner".into()));
1243        let typ = make_type("Wrapper", "c::Wrapper", vec![field]);
1244        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1245        assert!(result.contains("val.0.into()"));
1246    }
1247
1248    #[test]
1249    fn test_gen_from_core_to_binding_cast_large_ints_to_i64() {
1250        let field = make_field("count", TypeRef::Primitive(PrimitiveType::U64));
1251        let typ = make_type("S", "c::S", vec![field]);
1252        let config = ConversionConfig {
1253            cast_large_ints_to_i64: true,
1254            ..Default::default()
1255        };
1256        let result = gen_from_core_to_binding_cfg(&typ, "c", &no_opaques(), &config);
1257        assert!(result.contains("val.count as i64"));
1258    }
1259
1260    #[test]
1261    fn test_gen_from_core_to_binding_cast_f32_to_f64() {
1262        let field = make_field("score", TypeRef::Primitive(PrimitiveType::F32));
1263        let typ = make_type("S", "c::S", vec![field]);
1264        let config = ConversionConfig {
1265            cast_f32_to_f64: true,
1266            ..Default::default()
1267        };
1268        let result = gen_from_core_to_binding_cfg(&typ, "c", &no_opaques(), &config);
1269        assert!(result.contains("val.score as f64"));
1270    }
1271
1272    #[test]
1273    fn test_gen_from_core_to_binding_exclude_types_skips_field() {
1274        let field = make_field("js_val", TypeRef::Named("JsValue".into()));
1275        let typ = make_type("S", "c::S", vec![field]);
1276        let excluded = vec!["JsValue".to_string()];
1277        let config = ConversionConfig {
1278            exclude_types: &excluded,
1279            ..Default::default()
1280        };
1281        let result = gen_from_core_to_binding_cfg(&typ, "c", &no_opaques(), &config);
1282        // The field must be absent from the generated struct literal
1283        assert!(!result.contains("js_val:"));
1284    }
1285
1286    #[test]
1287    fn test_gen_from_core_to_binding_vec_json_field() {
1288        let field = make_field("items", TypeRef::Vec(Box::new(TypeRef::Json)));
1289        let typ = make_type("S", "c::S", vec![field]);
1290        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1291        assert!(result.contains("map(ToString::to_string)"));
1292    }
1293
1294    #[test]
1295    fn test_gen_from_core_to_binding_char_field() {
1296        let field = make_field("sep", TypeRef::Char);
1297        let typ = make_type("S", "c::S", vec![field]);
1298        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1299        assert!(result.contains("val.sep.to_string()"));
1300    }
1301
1302    #[test]
1303    fn test_gen_from_core_to_binding_bytes_field() {
1304        let field = make_field("data", TypeRef::Bytes);
1305        let typ = make_type("S", "c::S", vec![field]);
1306        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1307        assert!(result.contains("val.data.to_vec()"));
1308    }
1309
1310    // -----------------------------------------------------------------------
1311    // cfg_field: field_conversion_to_core_cfg — backend-specific variations
1312    // -----------------------------------------------------------------------
1313
1314    #[test]
1315    fn test_field_conversion_to_core_cfg_no_flags_delegates_to_base() {
1316        let config = ConversionConfig::default();
1317        let result = field_conversion_to_core_cfg("x", &TypeRef::String, false, &config);
1318        assert_eq!(result, "x: val.x");
1319    }
1320
1321    #[test]
1322    fn test_field_conversion_to_core_cfg_cast_u64_to_i64() {
1323        let config = ConversionConfig {
1324            cast_large_ints_to_i64: true,
1325            ..Default::default()
1326        };
1327        let result = field_conversion_to_core_cfg("n", &TypeRef::Primitive(PrimitiveType::U64), false, &config);
1328        assert_eq!(result, "n: val.n as u64");
1329    }
1330
1331    #[test]
1332    fn test_field_conversion_to_core_cfg_cast_usize_to_i64() {
1333        let config = ConversionConfig {
1334            cast_large_ints_to_i64: true,
1335            ..Default::default()
1336        };
1337        let result = field_conversion_to_core_cfg("n", &TypeRef::Primitive(PrimitiveType::Usize), false, &config);
1338        assert_eq!(result, "n: val.n as usize");
1339    }
1340
1341    #[test]
1342    fn test_field_conversion_to_core_cfg_cast_isize_to_i64() {
1343        let config = ConversionConfig {
1344            cast_large_ints_to_i64: true,
1345            ..Default::default()
1346        };
1347        let result = field_conversion_to_core_cfg("n", &TypeRef::Primitive(PrimitiveType::Isize), false, &config);
1348        assert_eq!(result, "n: val.n as isize");
1349    }
1350
1351    #[test]
1352    fn test_field_conversion_to_core_cfg_f32_cast() {
1353        let config = ConversionConfig {
1354            cast_f32_to_f64: true,
1355            ..Default::default()
1356        };
1357        let result = field_conversion_to_core_cfg("s", &TypeRef::Primitive(PrimitiveType::F32), false, &config);
1358        assert_eq!(result, "s: val.s as f32");
1359    }
1360
1361    #[test]
1362    fn test_field_conversion_to_core_cfg_duration_cast() {
1363        let config = ConversionConfig {
1364            cast_large_ints_to_i64: true,
1365            ..Default::default()
1366        };
1367        let result = field_conversion_to_core_cfg("t", &TypeRef::Duration, false, &config);
1368        assert_eq!(result, "t: std::time::Duration::from_millis(val.t as u64)");
1369    }
1370
1371    #[test]
1372    fn test_field_conversion_to_core_cfg_json_to_string() {
1373        let config = ConversionConfig {
1374            json_to_string: true,
1375            ..Default::default()
1376        };
1377        let result = field_conversion_to_core_cfg("m", &TypeRef::Json, false, &config);
1378        assert_eq!(result, "m: Default::default()");
1379    }
1380
1381    #[test]
1382    fn test_field_conversion_to_core_cfg_vec_named_to_string() {
1383        let config = ConversionConfig {
1384            vec_named_to_string: true,
1385            ..Default::default()
1386        };
1387        let ty = TypeRef::Vec(Box::new(TypeRef::Named("Item".into())));
1388        let result = field_conversion_to_core_cfg("items", &ty, false, &config);
1389        assert_eq!(result, "items: serde_json::from_str(&val.items).unwrap_or_default()");
1390    }
1391
1392    // -----------------------------------------------------------------------
1393    // field_conversion_from_core_cfg — backend-specific variations
1394    // -----------------------------------------------------------------------
1395
1396    #[test]
1397    fn test_field_conversion_from_core_cfg_no_flags_delegates_to_base() {
1398        let config = ConversionConfig::default();
1399        let result = field_conversion_from_core_cfg("x", &TypeRef::String, false, false, &no_opaques(), &config);
1400        assert_eq!(result, "x: val.x");
1401    }
1402
1403    #[test]
1404    fn test_field_conversion_from_core_cfg_cast_u64_to_i64() {
1405        let config = ConversionConfig {
1406            cast_large_ints_to_i64: true,
1407            ..Default::default()
1408        };
1409        let result = field_conversion_from_core_cfg(
1410            "n",
1411            &TypeRef::Primitive(PrimitiveType::U64),
1412            false,
1413            false,
1414            &no_opaques(),
1415            &config,
1416        );
1417        assert_eq!(result, "n: val.n as i64");
1418    }
1419
1420    #[test]
1421    fn test_field_conversion_from_core_cfg_cast_f32_to_f64() {
1422        let config = ConversionConfig {
1423            cast_f32_to_f64: true,
1424            ..Default::default()
1425        };
1426        let result = field_conversion_from_core_cfg(
1427            "s",
1428            &TypeRef::Primitive(PrimitiveType::F32),
1429            false,
1430            false,
1431            &no_opaques(),
1432            &config,
1433        );
1434        assert_eq!(result, "s: val.s as f64");
1435    }
1436
1437    #[test]
1438    fn test_field_conversion_from_core_cfg_duration_cast_to_i64() {
1439        let config = ConversionConfig {
1440            cast_large_ints_to_i64: true,
1441            ..Default::default()
1442        };
1443        let result = field_conversion_from_core_cfg("t", &TypeRef::Duration, false, false, &no_opaques(), &config);
1444        assert_eq!(result, "t: val.t.as_millis() as u64 as i64");
1445    }
1446
1447    #[test]
1448    fn test_field_conversion_from_core_cfg_json_to_string() {
1449        let config = ConversionConfig {
1450            json_to_string: true,
1451            ..Default::default()
1452        };
1453        let result = field_conversion_from_core_cfg("m", &TypeRef::Json, false, false, &no_opaques(), &config);
1454        assert_eq!(result, "m: val.m.to_string()");
1455    }
1456
1457    #[test]
1458    fn test_field_conversion_from_core_cfg_vec_named_to_string() {
1459        let config = ConversionConfig {
1460            vec_named_to_string: true,
1461            ..Default::default()
1462        };
1463        let ty = TypeRef::Vec(Box::new(TypeRef::Named("Item".into())));
1464        let result = field_conversion_from_core_cfg("items", &ty, false, false, &no_opaques(), &config);
1465        assert_eq!(result, "items: serde_json::to_string(&val.items).unwrap_or_default()");
1466    }
1467
1468    #[test]
1469    fn test_field_conversion_from_core_cfg_vec_u64_cast() {
1470        let config = ConversionConfig {
1471            cast_large_ints_to_i64: true,
1472            ..Default::default()
1473        };
1474        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::U64)));
1475        let result = field_conversion_from_core_cfg("ids", &ty, false, false, &no_opaques(), &config);
1476        assert!(result.contains("as i64"));
1477    }
1478
1479    #[test]
1480    fn test_field_conversion_from_core_cfg_vec_f32_cast() {
1481        let config = ConversionConfig {
1482            cast_f32_to_f64: true,
1483            ..Default::default()
1484        };
1485        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::F32)));
1486        let result = field_conversion_from_core_cfg("scores", &ty, false, false, &no_opaques(), &config);
1487        assert!(result.contains("as f64"));
1488    }
1489
1490    #[test]
1491    fn test_field_conversion_from_core_cfg_map_u64_values_cast() {
1492        let config = ConversionConfig {
1493            cast_large_ints_to_i64: true,
1494            ..Default::default()
1495        };
1496        let ty = TypeRef::Map(
1497            Box::new(TypeRef::String),
1498            Box::new(TypeRef::Primitive(PrimitiveType::U64)),
1499        );
1500        let result = field_conversion_from_core_cfg("map", &ty, false, false, &no_opaques(), &config);
1501        assert!(result.contains("as i64"));
1502    }
1503
1504    // -----------------------------------------------------------------------
1505    // convertible_types / core_to_binding_convertible_types
1506    // -----------------------------------------------------------------------
1507
1508    #[test]
1509    fn test_convertible_types_simple_struct() {
1510        let surface = ApiSurface {
1511            crate_name: "c".into(),
1512            version: "1.0".into(),
1513            types: vec![make_type("Config", "c::Config", vec![make_field("x", TypeRef::String)])],
1514            functions: vec![],
1515            enums: vec![],
1516            errors: vec![],
1517        };
1518        let result = convertible_types(&surface);
1519        assert!(result.contains("Config"));
1520    }
1521
1522    #[test]
1523    fn test_convertible_types_excludes_type_with_unconvertible_named_field() {
1524        // "Unknown" is not in the surface — types referencing it are removed
1525        let field = make_field("inner", TypeRef::Named("Unknown".into()));
1526        let surface = ApiSurface {
1527            crate_name: "c".into(),
1528            version: "1.0".into(),
1529            types: vec![make_type("Wrapper", "c::Wrapper", vec![field])],
1530            functions: vec![],
1531            enums: vec![],
1532            errors: vec![],
1533        };
1534        let result = convertible_types(&surface);
1535        assert!(!result.contains("Wrapper"));
1536    }
1537
1538    #[test]
1539    fn test_core_to_binding_convertible_types_simple() {
1540        let surface = ApiSurface {
1541            crate_name: "c".into(),
1542            version: "1.0".into(),
1543            types: vec![make_type("Config", "c::Config", vec![make_field("x", TypeRef::String)])],
1544            functions: vec![],
1545            enums: vec![],
1546            errors: vec![],
1547        };
1548        let result = core_to_binding_convertible_types(&surface);
1549        assert!(result.contains("Config"));
1550    }
1551
1552    #[test]
1553    fn test_can_generate_conversion_true_when_in_set() {
1554        let mut set = AHashSet::new();
1555        set.insert("Config".to_string());
1556        let typ = make_type("Config", "c::Config", vec![]);
1557        assert!(can_generate_conversion(&typ, &set));
1558    }
1559
1560    #[test]
1561    fn test_can_generate_conversion_false_when_absent() {
1562        let set = AHashSet::new();
1563        let typ = make_type("Config", "c::Config", vec![]);
1564        assert!(!can_generate_conversion(&typ, &set));
1565    }
1566
1567    // -----------------------------------------------------------------------
1568    // field_conversion_to_core_cfg — optional variants of cast flags
1569    // -----------------------------------------------------------------------
1570
1571    #[test]
1572    fn test_field_conversion_to_core_cfg_cast_u64_optional() {
1573        let config = ConversionConfig {
1574            cast_large_ints_to_i64: true,
1575            ..Default::default()
1576        };
1577        let result = field_conversion_to_core_cfg("n", &TypeRef::Primitive(PrimitiveType::U64), true, &config);
1578        assert_eq!(result, "n: val.n.map(|v| v as u64)");
1579    }
1580
1581    #[test]
1582    fn test_field_conversion_to_core_cfg_cast_usize_optional() {
1583        let config = ConversionConfig {
1584            cast_large_ints_to_i64: true,
1585            ..Default::default()
1586        };
1587        let result = field_conversion_to_core_cfg("n", &TypeRef::Primitive(PrimitiveType::Usize), true, &config);
1588        assert_eq!(result, "n: val.n.map(|v| v as usize)");
1589    }
1590
1591    #[test]
1592    fn test_field_conversion_to_core_cfg_cast_isize_optional() {
1593        let config = ConversionConfig {
1594            cast_large_ints_to_i64: true,
1595            ..Default::default()
1596        };
1597        let result = field_conversion_to_core_cfg("n", &TypeRef::Primitive(PrimitiveType::Isize), true, &config);
1598        assert_eq!(result, "n: val.n.map(|v| v as isize)");
1599    }
1600
1601    #[test]
1602    fn test_field_conversion_to_core_cfg_cast_f32_optional() {
1603        let config = ConversionConfig {
1604            cast_f32_to_f64: true,
1605            ..Default::default()
1606        };
1607        let result = field_conversion_to_core_cfg("s", &TypeRef::Primitive(PrimitiveType::F32), true, &config);
1608        assert_eq!(result, "s: val.s.map(|v| v as f32)");
1609    }
1610
1611    #[test]
1612    fn test_field_conversion_to_core_cfg_duration_cast_optional() {
1613        let config = ConversionConfig {
1614            cast_large_ints_to_i64: true,
1615            ..Default::default()
1616        };
1617        let result = field_conversion_to_core_cfg("t", &TypeRef::Duration, true, &config);
1618        assert_eq!(result, "t: val.t.map(|v| std::time::Duration::from_millis(v as u64))");
1619    }
1620
1621    #[test]
1622    fn test_field_conversion_to_core_cfg_json_to_string_optional() {
1623        let config = ConversionConfig {
1624            json_to_string: true,
1625            ..Default::default()
1626        };
1627        let result = field_conversion_to_core_cfg("m", &TypeRef::Json, true, &config);
1628        // json_to_string is lossy; optional still falls through to Default::default() path
1629        assert_eq!(result, "m: Default::default()");
1630    }
1631
1632    #[test]
1633    fn test_field_conversion_to_core_cfg_vec_named_to_string_optional() {
1634        let config = ConversionConfig {
1635            vec_named_to_string: true,
1636            ..Default::default()
1637        };
1638        let ty = TypeRef::Vec(Box::new(TypeRef::Named("Item".into())));
1639        let result = field_conversion_to_core_cfg("items", &ty, true, &config);
1640        assert_eq!(
1641            result,
1642            "items: val.items.as_ref().and_then(|s| serde_json::from_str(s).ok())"
1643        );
1644    }
1645
1646    #[test]
1647    fn test_field_conversion_to_core_cfg_vec_u64_cast_optional() {
1648        let config = ConversionConfig {
1649            cast_large_ints_to_i64: true,
1650            ..Default::default()
1651        };
1652        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::U64)));
1653        let result = field_conversion_to_core_cfg("ids", &ty, true, &config);
1654        assert!(result.contains("as u64"));
1655    }
1656
1657    #[test]
1658    fn test_field_conversion_to_core_cfg_vec_f32_cast_optional() {
1659        let config = ConversionConfig {
1660            cast_f32_to_f64: true,
1661            ..Default::default()
1662        };
1663        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::F32)));
1664        let result = field_conversion_to_core_cfg("scores", &ty, true, &config);
1665        assert!(result.contains("as f32"));
1666    }
1667
1668    #[test]
1669    fn test_field_conversion_to_core_cfg_map_u64_values_cast_optional() {
1670        let config = ConversionConfig {
1671            cast_large_ints_to_i64: true,
1672            ..Default::default()
1673        };
1674        let ty = TypeRef::Map(
1675            Box::new(TypeRef::String),
1676            Box::new(TypeRef::Primitive(PrimitiveType::U64)),
1677        );
1678        let result = field_conversion_to_core_cfg("map", &ty, true, &config);
1679        assert!(result.contains("as u64"));
1680    }
1681
1682    #[test]
1683    fn test_field_conversion_to_core_cfg_optional_inner_u64_cast() {
1684        // Optional(Primitive(U64)) should map element with cast
1685        let config = ConversionConfig {
1686            cast_large_ints_to_i64: true,
1687            ..Default::default()
1688        };
1689        let ty = TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::U64)));
1690        let result = field_conversion_to_core_cfg("n", &ty, false, &config);
1691        assert!(result.contains("as u64"));
1692    }
1693
1694    // -----------------------------------------------------------------------
1695    // field_conversion_from_core_cfg — optional variants and missing branches
1696    // -----------------------------------------------------------------------
1697
1698    #[test]
1699    fn test_field_conversion_from_core_cfg_cast_u64_optional() {
1700        let config = ConversionConfig {
1701            cast_large_ints_to_i64: true,
1702            ..Default::default()
1703        };
1704        let result = field_conversion_from_core_cfg(
1705            "n",
1706            &TypeRef::Primitive(PrimitiveType::U64),
1707            true,
1708            false,
1709            &no_opaques(),
1710            &config,
1711        );
1712        assert_eq!(result, "n: val.n.map(|v| v as i64)");
1713    }
1714
1715    #[test]
1716    fn test_field_conversion_from_core_cfg_cast_usize_optional() {
1717        let config = ConversionConfig {
1718            cast_large_ints_to_i64: true,
1719            ..Default::default()
1720        };
1721        let result = field_conversion_from_core_cfg(
1722            "n",
1723            &TypeRef::Primitive(PrimitiveType::Usize),
1724            true,
1725            false,
1726            &no_opaques(),
1727            &config,
1728        );
1729        assert_eq!(result, "n: val.n.map(|v| v as i64)");
1730    }
1731
1732    #[test]
1733    fn test_field_conversion_from_core_cfg_cast_isize_optional() {
1734        let config = ConversionConfig {
1735            cast_large_ints_to_i64: true,
1736            ..Default::default()
1737        };
1738        let result = field_conversion_from_core_cfg(
1739            "n",
1740            &TypeRef::Primitive(PrimitiveType::Isize),
1741            true,
1742            false,
1743            &no_opaques(),
1744            &config,
1745        );
1746        assert_eq!(result, "n: val.n.map(|v| v as i64)");
1747    }
1748
1749    #[test]
1750    fn test_field_conversion_from_core_cfg_cast_f32_optional() {
1751        let config = ConversionConfig {
1752            cast_f32_to_f64: true,
1753            ..Default::default()
1754        };
1755        let result = field_conversion_from_core_cfg(
1756            "s",
1757            &TypeRef::Primitive(PrimitiveType::F32),
1758            true,
1759            false,
1760            &no_opaques(),
1761            &config,
1762        );
1763        assert_eq!(result, "s: val.s.map(|v| v as f64)");
1764    }
1765
1766    #[test]
1767    fn test_field_conversion_from_core_cfg_duration_cast_optional() {
1768        let config = ConversionConfig {
1769            cast_large_ints_to_i64: true,
1770            ..Default::default()
1771        };
1772        let result = field_conversion_from_core_cfg("t", &TypeRef::Duration, true, false, &no_opaques(), &config);
1773        assert_eq!(result, "t: val.t.map(|d| d.as_millis() as u64 as i64)");
1774    }
1775
1776    #[test]
1777    fn test_field_conversion_from_core_cfg_json_to_string_optional() {
1778        let config = ConversionConfig {
1779            json_to_string: true,
1780            ..Default::default()
1781        };
1782        let result = field_conversion_from_core_cfg("m", &TypeRef::Json, true, false, &no_opaques(), &config);
1783        assert_eq!(result, "m: val.m.as_ref().map(ToString::to_string)");
1784    }
1785
1786    #[test]
1787    fn test_field_conversion_from_core_cfg_vec_named_to_string_optional() {
1788        let config = ConversionConfig {
1789            vec_named_to_string: true,
1790            ..Default::default()
1791        };
1792        let ty = TypeRef::Vec(Box::new(TypeRef::Named("Item".into())));
1793        let result = field_conversion_from_core_cfg("items", &ty, true, false, &no_opaques(), &config);
1794        assert_eq!(
1795            result,
1796            "items: val.items.as_ref().and_then(|v| serde_json::to_string(v).ok())"
1797        );
1798    }
1799
1800    #[test]
1801    fn test_field_conversion_from_core_cfg_vec_u64_cast_optional() {
1802        let config = ConversionConfig {
1803            cast_large_ints_to_i64: true,
1804            ..Default::default()
1805        };
1806        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::U64)));
1807        let result = field_conversion_from_core_cfg("ids", &ty, true, false, &no_opaques(), &config);
1808        assert!(result.contains("as i64"));
1809    }
1810
1811    #[test]
1812    fn test_field_conversion_from_core_cfg_vec_f32_cast_optional() {
1813        let config = ConversionConfig {
1814            cast_f32_to_f64: true,
1815            ..Default::default()
1816        };
1817        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::F32)));
1818        let result = field_conversion_from_core_cfg("scores", &ty, true, false, &no_opaques(), &config);
1819        assert!(result.contains("as f64"));
1820    }
1821
1822    #[test]
1823    fn test_field_conversion_from_core_cfg_optional_inner_u64_cast() {
1824        // Optional(Primitive(U64)) with cast should map element
1825        let config = ConversionConfig {
1826            cast_large_ints_to_i64: true,
1827            ..Default::default()
1828        };
1829        let ty = TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::U64)));
1830        let result = field_conversion_from_core_cfg("n", &ty, false, false, &no_opaques(), &config);
1831        assert!(result.contains("as i64"));
1832    }
1833
1834    #[test]
1835    fn test_field_conversion_from_core_cfg_map_u64_values_cast_optional() {
1836        let config = ConversionConfig {
1837            cast_large_ints_to_i64: true,
1838            ..Default::default()
1839        };
1840        let ty = TypeRef::Map(
1841            Box::new(TypeRef::String),
1842            Box::new(TypeRef::Primitive(PrimitiveType::U64)),
1843        );
1844        let result = field_conversion_from_core_cfg("map", &ty, true, false, &no_opaques(), &config);
1845        assert!(result.contains("as i64"));
1846    }
1847
1848    // -----------------------------------------------------------------------
1849    // Complex nested types: Optional<Vec<Named>>, Map<String, Optional<Named>>,
1850    // Vec<Map<String, String>>
1851    // -----------------------------------------------------------------------
1852
1853    #[test]
1854    fn test_field_conversion_to_core_optional_vec_string() {
1855        // Optional(Vec(String)) — passthrough
1856        let ty = TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::String))));
1857        let result = field_conversion_to_core("items", &ty, false);
1858        assert_eq!(result, "items: val.items");
1859    }
1860
1861    #[test]
1862    fn test_field_conversion_to_core_optional_vec_named_inner() {
1863        // Optional(Vec(Named)) — map into
1864        let ty = TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Item".into())))));
1865        let result = field_conversion_to_core("items", &ty, false);
1866        assert_eq!(
1867            result,
1868            "items: val.items.map(|v| v.into_iter().map(Into::into).collect())"
1869        );
1870    }
1871
1872    #[test]
1873    fn test_field_conversion_to_core_map_string_optional_named() {
1874        // Map(String, Optional(Named)) — named value uses .into()
1875        let ty = TypeRef::Map(
1876            Box::new(TypeRef::String),
1877            Box::new(TypeRef::Optional(Box::new(TypeRef::Named("Val".into())))),
1878        );
1879        // Optional inner Named doesn't trigger `has_named_val` (only TypeRef::Named at top), so falls
1880        // through to plain collect (there's no special map for Optional-value Named).
1881        let result = field_conversion_to_core("map", &ty, false);
1882        assert_eq!(result, "map: val.map.into_iter().collect()");
1883    }
1884
1885    #[test]
1886    fn test_field_conversion_to_core_map_string_vec_named_value() {
1887        // Map(String, Vec(Named)) — Vec<Named> values need per-vector Into mapping
1888        let ty = TypeRef::Map(
1889            Box::new(TypeRef::String),
1890            Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Item".into())))),
1891        );
1892        let result = field_conversion_to_core("map", &ty, false);
1893        assert!(result.contains("v.into_iter().map(Into::into).collect()"));
1894    }
1895
1896    #[test]
1897    fn test_field_conversion_to_core_map_string_vec_json_value() {
1898        // Map(String, Vec(Json)) — Vec<Json> values need per-vector serde deserialization
1899        let ty = TypeRef::Map(
1900            Box::new(TypeRef::String),
1901            Box::new(TypeRef::Vec(Box::new(TypeRef::Json))),
1902        );
1903        let result = field_conversion_to_core("map", &ty, false);
1904        assert!(result.contains("filter_map(|s| serde_json::from_str(&s).ok()).collect()"));
1905    }
1906
1907    #[test]
1908    fn test_field_conversion_to_core_map_string_string_optional() {
1909        // Map(String, String) with optional=true
1910        let ty = TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String));
1911        let result = field_conversion_to_core("map", &ty, true);
1912        assert_eq!(result, "map: val.map.map(|m| m.into_iter().collect())");
1913    }
1914
1915    #[test]
1916    fn test_field_conversion_from_core_optional_vec_named() {
1917        // Optional(Vec(Named)) — per-element .into() mapping
1918        let ty = TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Item".into())))));
1919        let result = field_conversion_from_core("items", &ty, false, false, &no_opaques());
1920        // falls through to field_conversion_to_core (symmetric case)
1921        assert!(result.contains("map(Into::into)") || result.contains("into_iter().map(Into::into)"));
1922    }
1923
1924    #[test]
1925    fn test_field_conversion_from_core_map_string_named_values() {
1926        // Map(String, Named) — Named values need .into()
1927        let ty = TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Named("Val".into())));
1928        let result = field_conversion_from_core("map", &ty, false, false, &no_opaques());
1929        // No asymmetric logic for Named map values in from_core — falls through to to_core symmetric
1930        assert!(result.contains("v.into()"));
1931    }
1932
1933    #[test]
1934    fn test_field_conversion_to_core_vec_string_passthrough() {
1935        // Vec(String) — passthrough (not a special case)
1936        let ty = TypeRef::Vec(Box::new(TypeRef::String));
1937        let result = field_conversion_to_core("tags", &ty, false);
1938        assert_eq!(result, "tags: val.tags");
1939    }
1940
1941    #[test]
1942    fn test_field_conversion_to_core_vec_string_optional() {
1943        // Vec(String) optional
1944        let ty = TypeRef::Vec(Box::new(TypeRef::String));
1945        let result = field_conversion_to_core("tags", &ty, true);
1946        assert_eq!(result, "tags: val.tags");
1947    }
1948
1949    #[test]
1950    fn test_field_conversion_to_core_map_named_key() {
1951        // Map(Named, String) — named key needs .into()
1952        let ty = TypeRef::Map(Box::new(TypeRef::Named("Key".into())), Box::new(TypeRef::String));
1953        let result = field_conversion_to_core("map", &ty, false);
1954        assert!(result.contains("k.into()"));
1955    }
1956
1957    #[test]
1958    fn test_field_conversion_to_core_map_json_key() {
1959        // Map(Json, String) — Json key gets deserialized
1960        let ty = TypeRef::Map(Box::new(TypeRef::Json), Box::new(TypeRef::String));
1961        let result = field_conversion_to_core("map", &ty, false);
1962        assert!(result.contains("serde_json::from_str(&k)"));
1963    }
1964
1965    #[test]
1966    fn test_field_conversion_from_core_optional_json_inner() {
1967        // Optional(Json) — binding uses Option<String> via .to_string()
1968        let ty = TypeRef::Optional(Box::new(TypeRef::Json));
1969        let result = field_conversion_from_core("meta", &ty, false, false, &no_opaques());
1970        assert_eq!(result, "meta: val.meta.as_ref().map(ToString::to_string)");
1971    }
1972
1973    #[test]
1974    fn test_field_conversion_from_core_optional_path_inner() {
1975        // Optional(Path) — binding uses to_string_lossy
1976        let ty = TypeRef::Optional(Box::new(TypeRef::Path));
1977        let result = field_conversion_from_core("file", &ty, false, false, &no_opaques());
1978        assert_eq!(result, "file: val.file.map(|p| p.to_string_lossy().to_string())");
1979    }
1980
1981    #[test]
1982    fn test_field_conversion_from_core_map_json_keys() {
1983        // Map(Json, String) — Json key gets .to_string()
1984        let ty = TypeRef::Map(Box::new(TypeRef::Json), Box::new(TypeRef::String));
1985        let result = field_conversion_from_core("map", &ty, false, false, &no_opaques());
1986        assert!(result.contains("k.to_string()"));
1987    }
1988
1989    // -----------------------------------------------------------------------
1990    // is_tuple_variant edge cases
1991    // -----------------------------------------------------------------------
1992
1993    #[test]
1994    fn test_is_tuple_variant_single_positional_field() {
1995        let fields = vec![make_field("_0", TypeRef::String)];
1996        assert!(is_tuple_variant(&fields));
1997    }
1998
1999    #[test]
2000    fn test_is_tuple_variant_true_for_underscore_only() {
2001        // "_".strip_prefix('_') == Some("") and "".chars().all(is_ascii_digit) is vacuously true
2002        let fields = vec![make_field("_", TypeRef::String)];
2003        assert!(is_tuple_variant(&fields));
2004    }
2005
2006    #[test]
2007    fn test_is_tuple_variant_false_for_field_starting_with_underscore_then_alpha() {
2008        // "_foo" — digits check fails
2009        let fields = vec![make_field("_foo", TypeRef::String)];
2010        assert!(!is_tuple_variant(&fields));
2011    }
2012
2013    #[test]
2014    fn test_is_tuple_variant_three_positional_fields() {
2015        let fields = vec![
2016            make_field("_0", TypeRef::String),
2017            make_field("_1", TypeRef::Primitive(PrimitiveType::I32)),
2018            make_field("_2", TypeRef::Primitive(PrimitiveType::F64)),
2019        ];
2020        assert!(is_tuple_variant(&fields));
2021    }
2022
2023    #[test]
2024    fn test_is_tuple_type_name_empty_string_is_false() {
2025        assert!(!helpers::is_tuple_type_name(""));
2026    }
2027
2028    #[test]
2029    fn test_is_tuple_type_name_space_is_false() {
2030        assert!(!helpers::is_tuple_type_name("String"));
2031    }
2032
2033    // -----------------------------------------------------------------------
2034    // core_type_path edge cases
2035    // -----------------------------------------------------------------------
2036
2037    #[test]
2038    fn test_core_type_path_with_hyphen_and_double_colon() {
2039        // Hyphens in path should be replaced; path still contains "::" so used verbatim
2040        let typ = make_type("Config", "my-crate::module::Config", vec![]);
2041        assert_eq!(core_type_path(&typ, "my_crate"), "my_crate::module::Config");
2042    }
2043
2044    #[test]
2045    fn test_core_type_path_rust_path_matches_core_import_prefix() {
2046        // Path already starts with core_import → used verbatim
2047        let typ = make_type("Config", "my_crate::Config", vec![]);
2048        assert_eq!(core_type_path(&typ, "my_crate"), "my_crate::Config");
2049    }
2050
2051    // -----------------------------------------------------------------------
2052    // build_type_path_map — multiple types, hyphens, enums
2053    // -----------------------------------------------------------------------
2054
2055    #[test]
2056    fn test_build_type_path_map_multiple_types() {
2057        let surface = ApiSurface {
2058            crate_name: "my_crate".into(),
2059            version: "1.0.0".into(),
2060            types: vec![
2061                make_type("Config", "my_crate::Config", vec![]),
2062                make_type("Result", "my_crate::types::Result", vec![]),
2063            ],
2064            functions: vec![],
2065            enums: vec![
2066                make_enum("Mode", "my_crate::Mode", &["A"]),
2067                make_enum("Status", "Status", &["Ok"]),
2068            ],
2069            errors: vec![],
2070        };
2071        let map = build_type_path_map(&surface, "my_crate");
2072        assert_eq!(map.get("Config").map(String::as_str), Some("my_crate::Config"));
2073        assert_eq!(map.get("Result").map(String::as_str), Some("my_crate::types::Result"));
2074        assert_eq!(map.get("Mode").map(String::as_str), Some("my_crate::Mode"));
2075        // "Status" path has no "::" and doesn't start with core_import → prefixed
2076        assert_eq!(map.get("Status").map(String::as_str), Some("my_crate::Status"));
2077    }
2078
2079    #[test]
2080    fn test_build_type_path_map_normalizes_hyphens() {
2081        let surface = ApiSurface {
2082            crate_name: "my_crate".into(),
2083            version: "1.0.0".into(),
2084            types: vec![make_type("Config", "my-crate::Config", vec![])],
2085            functions: vec![],
2086            enums: vec![],
2087            errors: vec![],
2088        };
2089        let map = build_type_path_map(&surface, "my_crate");
2090        assert_eq!(map.get("Config").map(String::as_str), Some("my_crate::Config"));
2091    }
2092
2093    #[test]
2094    fn test_build_type_path_map_empty_surface() {
2095        let surface = ApiSurface {
2096            crate_name: "c".into(),
2097            version: "1.0".into(),
2098            types: vec![],
2099            functions: vec![],
2100            enums: vec![],
2101            errors: vec![],
2102        };
2103        let map = build_type_path_map(&surface, "c");
2104        assert!(map.is_empty());
2105    }
2106
2107    // -----------------------------------------------------------------------
2108    // Edge cases: empty fields, single-field structs, all-optional fields
2109    // -----------------------------------------------------------------------
2110
2111    #[test]
2112    fn test_gen_from_binding_to_core_empty_fields() {
2113        let typ = make_type("Empty", "c::Empty", vec![]);
2114        let result = gen_from_binding_to_core(&typ, "c");
2115        assert!(result.contains("impl From<Empty> for c::Empty"));
2116        assert!(result.contains("Self {"));
2117    }
2118
2119    #[test]
2120    fn test_gen_from_core_to_binding_empty_fields() {
2121        let typ = make_type("Empty", "c::Empty", vec![]);
2122        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
2123        assert!(result.contains("impl From<c::Empty> for Empty"));
2124        assert!(result.contains("Self {"));
2125    }
2126
2127    #[test]
2128    fn test_gen_from_binding_to_core_all_optional_fields() {
2129        let typ = make_type(
2130            "Config",
2131            "c::Config",
2132            vec![
2133                make_opt_field("name", TypeRef::String),
2134                make_opt_field("count", TypeRef::Primitive(PrimitiveType::I32)),
2135            ],
2136        );
2137        let result = gen_from_binding_to_core(&typ, "c");
2138        assert!(result.contains("name: val.name"));
2139        assert!(result.contains("count: val.count"));
2140    }
2141
2142    #[test]
2143    fn test_gen_from_binding_to_core_single_string_field() {
2144        let typ = make_type("S", "c::S", vec![make_field("value", TypeRef::String)]);
2145        let result = gen_from_binding_to_core(&typ, "c");
2146        assert!(result.contains("value: val.value"));
2147    }
2148
2149    #[test]
2150    fn test_gen_from_core_to_binding_single_optional_named_field() {
2151        let field = make_opt_field("inner", TypeRef::Named("Inner".into()));
2152        let typ = make_type("Wrapper", "c::Wrapper", vec![field]);
2153        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
2154        assert!(result.contains("inner: val.inner.map(Into::into)"));
2155    }
2156
2157    // -----------------------------------------------------------------------
2158    // binding_to_core_match_arm_ext_cfg — config-aware match arms
2159    // -----------------------------------------------------------------------
2160
2161    #[test]
2162    fn test_binding_to_core_match_arm_ext_cfg_unit_variant() {
2163        let config = ConversionConfig::default();
2164        let result = helpers::binding_to_core_match_arm_ext_cfg("MyEnum", "Foo", &[], false, &config);
2165        assert_eq!(result, "MyEnum::Foo => Self::Foo,");
2166    }
2167
2168    #[test]
2169    fn test_binding_to_core_match_arm_ext_cfg_no_binding_data_named_fields() {
2170        let config = ConversionConfig::default();
2171        let fields = vec![make_field("value", TypeRef::String)];
2172        let result = helpers::binding_to_core_match_arm_ext_cfg("MyEnum", "Bar", &fields, false, &config);
2173        assert!(result.contains("value: Default::default()"));
2174    }
2175
2176    #[test]
2177    fn test_binding_to_core_match_arm_ext_cfg_no_binding_data_tuple_fields() {
2178        let config = ConversionConfig::default();
2179        let fields = vec![make_field("_0", TypeRef::String)];
2180        let result = helpers::binding_to_core_match_arm_ext_cfg("MyEnum", "Bar", &fields, false, &config);
2181        assert!(result.contains("Default::default()"));
2182        assert!(result.contains("Self::Bar("));
2183    }
2184
2185    #[test]
2186    fn test_binding_to_core_match_arm_ext_cfg_with_binding_data_named() {
2187        let config = ConversionConfig::default();
2188        let fields = vec![make_field("value", TypeRef::Named("Inner".into()))];
2189        let result = helpers::binding_to_core_match_arm_ext_cfg("MyEnum", "Bar", &fields, true, &config);
2190        assert!(result.contains("value: value.into()"));
2191    }
2192
2193    #[test]
2194    fn test_binding_to_core_match_arm_ext_cfg_with_binding_data_tuple() {
2195        let config = ConversionConfig::default();
2196        let fields = vec![make_field("_0", TypeRef::Named("Inner".into()))];
2197        let result = helpers::binding_to_core_match_arm_ext_cfg("MyEnum", "Bar", &fields, true, &config);
2198        assert!(result.contains("_0.into()"));
2199    }
2200
2201    #[test]
2202    fn test_binding_to_core_match_arm_ext_cfg_cast_u64_field() {
2203        let config = ConversionConfig {
2204            cast_large_ints_to_i64: true,
2205            ..Default::default()
2206        };
2207        let fields = vec![make_field("count", TypeRef::Primitive(PrimitiveType::U64))];
2208        let result = helpers::binding_to_core_match_arm_ext_cfg("MyEnum", "Bar", &fields, true, &config);
2209        assert!(result.contains("as u64"));
2210    }
2211
2212    // -----------------------------------------------------------------------
2213    // core_to_binding_match_arm_ext_cfg — config-aware match arms
2214    // -----------------------------------------------------------------------
2215
2216    #[test]
2217    fn test_core_to_binding_match_arm_ext_cfg_unit_variant() {
2218        let config = ConversionConfig::default();
2219        let result = helpers::core_to_binding_match_arm_ext_cfg("CoreEnum", "Foo", &[], false, &config);
2220        assert_eq!(result, "CoreEnum::Foo => Self::Foo,");
2221    }
2222
2223    #[test]
2224    fn test_core_to_binding_match_arm_ext_cfg_no_binding_data_named() {
2225        let config = ConversionConfig::default();
2226        let fields = vec![make_field("x", TypeRef::Primitive(PrimitiveType::I32))];
2227        let result = helpers::core_to_binding_match_arm_ext_cfg("CoreEnum", "Foo", &fields, false, &config);
2228        assert!(result.contains("{ .. }"));
2229        assert!(result.contains("Self::Foo"));
2230    }
2231
2232    #[test]
2233    fn test_core_to_binding_match_arm_ext_cfg_no_binding_data_tuple() {
2234        let config = ConversionConfig::default();
2235        let fields = vec![make_field("_0", TypeRef::Primitive(PrimitiveType::I32))];
2236        let result = helpers::core_to_binding_match_arm_ext_cfg("CoreEnum", "Foo", &fields, false, &config);
2237        assert!(result.contains("(..)"));
2238        assert!(result.contains("Self::Foo"));
2239    }
2240
2241    #[test]
2242    fn test_core_to_binding_match_arm_ext_cfg_with_binding_data_named_fields() {
2243        let config = ConversionConfig::default();
2244        let fields = vec![make_field("value", TypeRef::Named("Inner".into()))];
2245        let result = helpers::core_to_binding_match_arm_ext_cfg("CoreEnum", "Foo", &fields, true, &config);
2246        assert!(result.contains("value: value.into()"));
2247    }
2248
2249    #[test]
2250    fn test_core_to_binding_match_arm_ext_cfg_with_binding_data_tuple() {
2251        let config = ConversionConfig::default();
2252        let fields = vec![make_field("_0", TypeRef::Named("Inner".into()))];
2253        let result = helpers::core_to_binding_match_arm_ext_cfg("CoreEnum", "Foo", &fields, true, &config);
2254        assert!(result.contains("_0: _0.into()"));
2255    }
2256
2257    #[test]
2258    fn test_core_to_binding_match_arm_ext_cfg_cast_u64_field() {
2259        let config = ConversionConfig {
2260            cast_large_ints_to_i64: true,
2261            ..Default::default()
2262        };
2263        let fields = vec![make_field("count", TypeRef::Primitive(PrimitiveType::U64))];
2264        let result = helpers::core_to_binding_match_arm_ext_cfg("CoreEnum", "Bar", &fields, true, &config);
2265        assert!(result.contains("as i64"));
2266    }
2267
2268    // -----------------------------------------------------------------------
2269    // core_to_binding_match_arm_ext with binding_has_data=true
2270    // -----------------------------------------------------------------------
2271
2272    #[test]
2273    fn test_core_to_binding_match_arm_ext_binding_has_data_named_fields() {
2274        let fields = vec![make_field("value", TypeRef::Named("Inner".into()))];
2275        let result = helpers::core_to_binding_match_arm_ext("CoreEnum", "Bar", &fields, true);
2276        assert!(result.contains("value: value.into()"));
2277    }
2278
2279    #[test]
2280    fn test_core_to_binding_match_arm_ext_binding_has_data_tuple_fields() {
2281        let fields = vec![make_field("_0", TypeRef::Named("Inner".into()))];
2282        let result = helpers::core_to_binding_match_arm_ext("CoreEnum", "Bar", &fields, true);
2283        assert!(result.contains("_0: _0.into()"));
2284    }
2285
2286    #[test]
2287    fn test_core_to_binding_match_arm_ext_binding_has_data_plain_field() {
2288        let fields = vec![make_field("x", TypeRef::Primitive(PrimitiveType::I32))];
2289        let result = helpers::core_to_binding_match_arm_ext("CoreEnum", "Bar", &fields, true);
2290        assert!(result.contains("x: x"));
2291    }
2292
2293    #[test]
2294    fn test_core_to_binding_match_arm_ext_binding_has_data_sanitized_field() {
2295        let mut field = make_field("complex", TypeRef::String);
2296        field.sanitized = true;
2297        let result = helpers::core_to_binding_match_arm_ext("CoreEnum", "Bar", &[field], true);
2298        assert!(result.contains("serde_json::to_string("));
2299    }
2300
2301    // -----------------------------------------------------------------------
2302    // input_type_names — method params and transitive closure
2303    // -----------------------------------------------------------------------
2304
2305    #[test]
2306    fn test_input_type_names_from_method_params() {
2307        let surface = ApiSurface {
2308            crate_name: "my_crate".into(),
2309            version: "1.0.0".into(),
2310            types: vec![TypeDef {
2311                name: "Client".into(),
2312                rust_path: "my_crate::Client".into(),
2313                original_rust_path: String::new(),
2314                fields: vec![],
2315                methods: vec![MethodDef {
2316                    name: "process".into(),
2317                    params: vec![ParamDef {
2318                        name: "config".into(),
2319                        ty: TypeRef::Named("Config".into()),
2320                        optional: false,
2321                        default: None,
2322                        sanitized: false,
2323                        typed_default: None,
2324                        is_ref: false,
2325                        is_mut: false,
2326                        newtype_wrapper: None,
2327                    }],
2328                    return_type: TypeRef::Unit,
2329                    is_async: false,
2330                    is_static: false,
2331                    error_type: None,
2332                    doc: String::new(),
2333                    receiver: None,
2334                    sanitized: false,
2335                    trait_source: None,
2336                    returns_ref: false,
2337                    returns_cow: false,
2338                    return_newtype_wrapper: None,
2339                    has_default_impl: false,
2340                }],
2341                is_opaque: false,
2342                is_clone: true,
2343                is_trait: false,
2344                has_default: false,
2345                has_stripped_cfg_fields: false,
2346                is_return_type: false,
2347                serde_rename_all: None,
2348                has_serde: false,
2349                super_traits: vec![],
2350                doc: String::new(),
2351                cfg: None,
2352            }],
2353            functions: vec![],
2354            enums: vec![],
2355            errors: vec![],
2356        };
2357        let names = input_type_names(&surface);
2358        assert!(names.contains("Config"));
2359    }
2360
2361    #[test]
2362    fn test_input_type_names_transitive_closure() {
2363        // Config has a field of type Backend — Backend should also be in input_type_names
2364        let config_type = make_type(
2365            "Config",
2366            "c::Config",
2367            vec![make_field("backend", TypeRef::Named("Backend".into()))],
2368        );
2369        let surface = ApiSurface {
2370            crate_name: "c".into(),
2371            version: "1.0".into(),
2372            types: vec![config_type],
2373            functions: vec![FunctionDef {
2374                name: "run".into(),
2375                rust_path: "c::run".into(),
2376                original_rust_path: String::new(),
2377                params: vec![ParamDef {
2378                    name: "config".into(),
2379                    ty: TypeRef::Named("Config".into()),
2380                    optional: false,
2381                    default: None,
2382                    sanitized: false,
2383                    typed_default: None,
2384                    is_ref: false,
2385                    is_mut: false,
2386                    newtype_wrapper: None,
2387                }],
2388                return_type: TypeRef::Unit,
2389                is_async: false,
2390                error_type: None,
2391                doc: String::new(),
2392                cfg: None,
2393                sanitized: false,
2394                returns_ref: false,
2395                returns_cow: false,
2396                return_newtype_wrapper: None,
2397            }],
2398            enums: vec![],
2399            errors: vec![],
2400        };
2401        let names = input_type_names(&surface);
2402        assert!(names.contains("Config"));
2403        assert!(names.contains("Backend"));
2404    }
2405
2406    // -----------------------------------------------------------------------
2407    // convertible_types — sanitized fields with/without has_default
2408    // -----------------------------------------------------------------------
2409
2410    #[test]
2411    fn test_convertible_types_sanitized_field_with_has_default() {
2412        let mut field = make_field("complex", TypeRef::String);
2413        field.sanitized = true;
2414        let mut typ = make_type("Config", "c::Config", vec![field]);
2415        typ.has_default = true;
2416        let surface = ApiSurface {
2417            crate_name: "c".into(),
2418            version: "1.0".into(),
2419            types: vec![typ],
2420            functions: vec![],
2421            enums: vec![],
2422            errors: vec![],
2423        };
2424        // String has Default::default() — convertible
2425        let result = convertible_types(&surface);
2426        assert!(result.contains("Config"));
2427    }
2428
2429    #[test]
2430    fn test_convertible_types_opaque_type_excluded() {
2431        let mut typ = make_type("Client", "c::Client", vec![]);
2432        typ.is_opaque = true;
2433        let surface = ApiSurface {
2434            crate_name: "c".into(),
2435            version: "1.0".into(),
2436            types: vec![typ],
2437            functions: vec![],
2438            enums: vec![],
2439            errors: vec![],
2440        };
2441        // Opaque types are not in the candidate set initially
2442        let result = convertible_types(&surface);
2443        assert!(!result.contains("Client"));
2444    }
2445
2446    #[test]
2447    fn test_convertible_types_type_with_named_field_in_surface() {
2448        // Both Config (with Backend field) and Backend present — both convertible
2449        let config_field = make_field("backend", TypeRef::Named("Backend".into()));
2450        let config = make_type("Config", "c::Config", vec![config_field]);
2451        let backend = make_type("Backend", "c::Backend", vec![]);
2452        let surface = ApiSurface {
2453            crate_name: "c".into(),
2454            version: "1.0".into(),
2455            types: vec![config, backend],
2456            functions: vec![],
2457            enums: vec![],
2458            errors: vec![],
2459        };
2460        let result = convertible_types(&surface);
2461        assert!(result.contains("Config"));
2462        assert!(result.contains("Backend"));
2463    }
2464
2465    // -----------------------------------------------------------------------
2466    // core_enum_path edge cases
2467    // -----------------------------------------------------------------------
2468
2469    #[test]
2470    fn test_core_enum_path_with_hyphen_normalization() {
2471        let e = make_enum("Status", "my-crate::Status", &[]);
2472        assert_eq!(core_enum_path(&e, "my_crate"), "my_crate::Status");
2473    }
2474
2475    #[test]
2476    fn test_core_enum_path_already_starts_with_core_import() {
2477        // When path already starts with core_import, use verbatim
2478        let e = make_enum("Mode", "my_crate::inner::Mode", &[]);
2479        assert_eq!(core_enum_path(&e, "my_crate"), "my_crate::inner::Mode");
2480    }
2481
2482    // -----------------------------------------------------------------------
2483    // needs_i64_cast / core_prim_str / binding_prim_str (helper coverage)
2484    // -----------------------------------------------------------------------
2485
2486    #[test]
2487    fn test_needs_i64_cast_true_for_large_ints() {
2488        use super::helpers::*;
2489        assert!(needs_i64_cast(&PrimitiveType::U64));
2490        assert!(needs_i64_cast(&PrimitiveType::Usize));
2491        assert!(needs_i64_cast(&PrimitiveType::Isize));
2492    }
2493
2494    #[test]
2495    fn test_needs_i64_cast_false_for_small_ints() {
2496        use super::helpers::*;
2497        assert!(!needs_i64_cast(&PrimitiveType::I32));
2498        assert!(!needs_i64_cast(&PrimitiveType::U32));
2499        assert!(!needs_i64_cast(&PrimitiveType::F64));
2500    }
2501
2502    #[test]
2503    fn test_core_prim_str_all_variants() {
2504        use super::helpers::core_prim_str;
2505        assert_eq!(core_prim_str(&PrimitiveType::U64), "u64");
2506        assert_eq!(core_prim_str(&PrimitiveType::Usize), "usize");
2507        assert_eq!(core_prim_str(&PrimitiveType::Isize), "isize");
2508        assert_eq!(core_prim_str(&PrimitiveType::F32), "f32");
2509        assert_eq!(core_prim_str(&PrimitiveType::Bool), "bool");
2510        assert_eq!(core_prim_str(&PrimitiveType::U8), "u8");
2511        assert_eq!(core_prim_str(&PrimitiveType::U16), "u16");
2512        assert_eq!(core_prim_str(&PrimitiveType::U32), "u32");
2513        assert_eq!(core_prim_str(&PrimitiveType::I8), "i8");
2514        assert_eq!(core_prim_str(&PrimitiveType::I16), "i16");
2515        assert_eq!(core_prim_str(&PrimitiveType::I32), "i32");
2516        assert_eq!(core_prim_str(&PrimitiveType::I64), "i64");
2517        assert_eq!(core_prim_str(&PrimitiveType::F64), "f64");
2518    }
2519
2520    #[test]
2521    fn test_binding_prim_str_large_ints_map_to_i64() {
2522        use super::helpers::binding_prim_str;
2523        assert_eq!(binding_prim_str(&PrimitiveType::U64), "i64");
2524        assert_eq!(binding_prim_str(&PrimitiveType::Usize), "i64");
2525        assert_eq!(binding_prim_str(&PrimitiveType::Isize), "i64");
2526    }
2527
2528    #[test]
2529    fn test_binding_prim_str_small_ints_map_to_i32() {
2530        use super::helpers::binding_prim_str;
2531        assert_eq!(binding_prim_str(&PrimitiveType::U8), "i32");
2532        assert_eq!(binding_prim_str(&PrimitiveType::U16), "i32");
2533        assert_eq!(binding_prim_str(&PrimitiveType::U32), "i32");
2534        assert_eq!(binding_prim_str(&PrimitiveType::I8), "i32");
2535        assert_eq!(binding_prim_str(&PrimitiveType::I16), "i32");
2536        assert_eq!(binding_prim_str(&PrimitiveType::I32), "i32");
2537    }
2538
2539    #[test]
2540    fn test_binding_prim_str_float_and_i64() {
2541        use super::helpers::binding_prim_str;
2542        assert_eq!(binding_prim_str(&PrimitiveType::F32), "f64");
2543        assert_eq!(binding_prim_str(&PrimitiveType::F64), "f64");
2544        assert_eq!(binding_prim_str(&PrimitiveType::I64), "i64");
2545        assert_eq!(binding_prim_str(&PrimitiveType::Bool), "bool");
2546    }
2547}