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    #[test]
677    fn test_field_conversion_from_core_sanitized_vec_u32_non_optional() {
678        // Sanitized Vec<u32> (from homogeneous tuple like (u32, u32)) uses serde round-trip
679        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::U32)));
680        let result = field_conversion_from_core("dimensions", &ty, false, true, &no_opaques());
681        assert!(
682            result.contains("serde_json::to_value"),
683            "should use serde_json::to_value for tuple-to-vec conversion, got: {result}"
684        );
685        assert!(
686            result.contains("serde_json::from_value"),
687            "should use serde_json::from_value for tuple-to-vec conversion, got: {result}"
688        );
689        assert!(
690            !result.contains("format!"),
691            "should NOT use format! debug for Vec<Primitive> sanitized field, got: {result}"
692        );
693    }
694
695    #[test]
696    fn test_field_conversion_from_core_sanitized_vec_u32_optional() {
697        // Optional sanitized Vec<u32> (from Option<(u32, u32)>) uses serde round-trip
698        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::U32)));
699        let result = field_conversion_from_core("dimensions", &ty, true, true, &no_opaques());
700        assert!(
701            result.contains("serde_json::to_value"),
702            "should use serde_json::to_value for optional tuple-to-vec conversion, got: {result}"
703        );
704        assert!(
705            result.contains("serde_json::from_value"),
706            "should use serde_json::from_value for optional tuple-to-vec conversion, got: {result}"
707        );
708        assert!(
709            !result.contains("format!"),
710            "should NOT use format! debug for optional Vec<Primitive> sanitized field, got: {result}"
711        );
712    }
713
714    #[test]
715    fn test_field_conversion_from_core_sanitized_optional_vec_u32() {
716        // Optional(Vec(u32)): from the IR Optional wrapper around a tuple-to-vec sanitized type
717        let ty = TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::U32)))));
718        let result = field_conversion_from_core("dimensions", &ty, false, true, &no_opaques());
719        assert!(
720            result.contains("serde_json::to_value"),
721            "should use serde_json::to_value for Optional(Vec(Primitive)) sanitized field, got: {result}"
722        );
723        assert!(
724            result.contains("serde_json::from_value"),
725            "should use serde_json::from_value for Optional(Vec(Primitive)) sanitized field, got: {result}"
726        );
727        assert!(
728            !result.contains("format!"),
729            "should NOT use format! debug for Optional(Vec(Primitive)) sanitized field, got: {result}"
730        );
731    }
732
733    #[test]
734    fn test_gen_from_binding_to_core_sanitized_vec_u32_uses_serde() {
735        // Sanitized Vec<u32> field (from tuple) should use serde round-trip in binding→core
736        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::U32)));
737        let mut field = make_opt_field("dimensions", ty);
738        field.sanitized = true;
739        let typ = make_type("S", "c::S", vec![field]);
740        let result = gen_from_binding_to_core(&typ, "c");
741        assert!(
742            result.contains("serde_json::to_value"),
743            "binding→core for sanitized Vec<Primitive> should use serde_json::to_value, got:\n{result}"
744        );
745        assert!(
746            result.contains("serde_json::from_value"),
747            "binding→core for sanitized Vec<Primitive> should use serde_json::from_value, got:\n{result}"
748        );
749        assert!(
750            !result.contains("Default::default()"),
751            "binding→core for sanitized Vec<Primitive> should NOT use Default::default(), got:\n{result}"
752        );
753    }
754
755    // -----------------------------------------------------------------------
756    // helpers.rs: is_tuple_variant / is_newtype / is_tuple_type_name
757    // -----------------------------------------------------------------------
758
759    #[test]
760    fn test_is_tuple_variant_true_for_positional_fields() {
761        let fields = vec![
762            make_field("_0", TypeRef::String),
763            make_field("_1", TypeRef::Primitive(PrimitiveType::I32)),
764        ];
765        assert!(is_tuple_variant(&fields));
766    }
767
768    #[test]
769    fn test_is_tuple_variant_false_for_named_fields() {
770        let fields = vec![make_field("name", TypeRef::String)];
771        assert!(!is_tuple_variant(&fields));
772    }
773
774    #[test]
775    fn test_is_tuple_variant_false_for_empty_fields() {
776        assert!(!is_tuple_variant(&[]));
777    }
778
779    #[test]
780    fn test_is_tuple_type_name_true() {
781        assert!(helpers::is_tuple_type_name("(String, u32)"));
782    }
783
784    #[test]
785    fn test_is_tuple_type_name_false() {
786        assert!(!helpers::is_tuple_type_name("Config"));
787    }
788
789    // -----------------------------------------------------------------------
790    // helpers.rs: field_references_excluded_type
791    // -----------------------------------------------------------------------
792
793    #[test]
794    fn test_field_references_excluded_type_direct_match() {
795        let ty = TypeRef::Named("JsValue".into());
796        assert!(field_references_excluded_type(&ty, &["JsValue".to_string()]));
797    }
798
799    #[test]
800    fn test_field_references_excluded_type_no_match() {
801        let ty = TypeRef::Named("Config".into());
802        assert!(!field_references_excluded_type(&ty, &["JsValue".to_string()]));
803    }
804
805    #[test]
806    fn test_field_references_excluded_type_inside_optional() {
807        let ty = TypeRef::Optional(Box::new(TypeRef::Named("Excluded".into())));
808        assert!(field_references_excluded_type(&ty, &["Excluded".to_string()]));
809    }
810
811    #[test]
812    fn test_field_references_excluded_type_inside_vec() {
813        let ty = TypeRef::Vec(Box::new(TypeRef::Named("Excluded".into())));
814        assert!(field_references_excluded_type(&ty, &["Excluded".to_string()]));
815    }
816
817    #[test]
818    fn test_field_references_excluded_type_inside_map_key() {
819        let ty = TypeRef::Map(Box::new(TypeRef::Named("Excluded".into())), Box::new(TypeRef::String));
820        assert!(field_references_excluded_type(&ty, &["Excluded".to_string()]));
821    }
822
823    #[test]
824    fn test_field_references_excluded_type_inside_map_value() {
825        let ty = TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Named("Excluded".into())));
826        assert!(field_references_excluded_type(&ty, &["Excluded".to_string()]));
827    }
828
829    #[test]
830    fn test_field_references_excluded_type_primitive_not_excluded() {
831        let ty = TypeRef::Primitive(PrimitiveType::I32);
832        assert!(!field_references_excluded_type(&ty, &["JsValue".to_string()]));
833    }
834
835    // -----------------------------------------------------------------------
836    // helpers.rs: can_generate_enum_conversion / can_generate_enum_conversion_from_core
837    // -----------------------------------------------------------------------
838
839    #[test]
840    fn test_can_generate_enum_conversion_with_variants() {
841        let e = make_enum("Color", "crate::Color", &["Red", "Green"]);
842        assert!(can_generate_enum_conversion(&e));
843    }
844
845    #[test]
846    fn test_can_generate_enum_conversion_empty_variants() {
847        let e = make_enum("Empty", "crate::Empty", &[]);
848        assert!(!can_generate_enum_conversion(&e));
849    }
850
851    #[test]
852    fn test_can_generate_enum_conversion_from_core_with_variants() {
853        let e = make_enum("Color", "crate::Color", &["Red"]);
854        assert!(can_generate_enum_conversion_from_core(&e));
855    }
856
857    // -----------------------------------------------------------------------
858    // helpers.rs: core_type_path / core_enum_path
859    // -----------------------------------------------------------------------
860
861    #[test]
862    fn test_core_type_path_with_full_path() {
863        let typ = make_type("Config", "my_crate::types::Config", vec![]);
864        assert_eq!(core_type_path(&typ, "my_crate"), "my_crate::types::Config");
865    }
866
867    #[test]
868    fn test_core_type_path_with_bare_name() {
869        // When rust_path has no "::", prefix with core_import::name
870        let typ = make_type("Config", "Config", vec![]);
871        assert_eq!(core_type_path(&typ, "my_crate"), "my_crate::Config");
872    }
873
874    #[test]
875    fn test_core_type_path_normalizes_hyphens() {
876        let typ = make_type("Config", "my-crate::Config", vec![]);
877        assert_eq!(core_type_path(&typ, "my_crate"), "my_crate::Config");
878    }
879
880    #[test]
881    fn test_core_enum_path_with_full_path() {
882        let e = make_enum("Backend", "my_crate::Backend", &[]);
883        assert_eq!(core_enum_path(&e, "my_crate"), "my_crate::Backend");
884    }
885
886    #[test]
887    fn test_core_enum_path_bare_name_gets_prefixed() {
888        let e = make_enum("Backend", "Backend", &[]);
889        assert_eq!(core_enum_path(&e, "my_crate"), "my_crate::Backend");
890    }
891
892    // -----------------------------------------------------------------------
893    // helpers.rs: build_type_path_map / resolve_named_path
894    // -----------------------------------------------------------------------
895
896    #[test]
897    fn test_build_type_path_map_includes_types_and_enums() {
898        let surface = ApiSurface {
899            crate_name: "my_crate".into(),
900            version: "1.0.0".into(),
901            types: vec![make_type("Config", "my_crate::Config", vec![])],
902            functions: vec![],
903            enums: vec![make_enum("Mode", "my_crate::Mode", &["A"])],
904            errors: vec![],
905        };
906        let map = build_type_path_map(&surface, "my_crate");
907        assert_eq!(map.get("Config").map(String::as_str), Some("my_crate::Config"));
908        assert_eq!(map.get("Mode").map(String::as_str), Some("my_crate::Mode"));
909    }
910
911    #[test]
912    fn test_resolve_named_path_found_in_map() {
913        let mut map = ahash::AHashMap::new();
914        map.insert("Config".to_string(), "my_crate::types::Config".to_string());
915        assert_eq!(
916            resolve_named_path("Config", "my_crate", &map),
917            "my_crate::types::Config"
918        );
919    }
920
921    #[test]
922    fn test_resolve_named_path_not_found_falls_back() {
923        let map = ahash::AHashMap::new();
924        assert_eq!(resolve_named_path("Unknown", "my_crate", &map), "my_crate::Unknown");
925    }
926
927    // -----------------------------------------------------------------------
928    // helpers.rs: input_type_names
929    // -----------------------------------------------------------------------
930
931    #[test]
932    fn test_input_type_names_from_function_params() {
933        let surface = ApiSurface {
934            crate_name: "my_crate".into(),
935            version: "1.0.0".into(),
936            types: vec![],
937            functions: vec![FunctionDef {
938                name: "process".into(),
939                rust_path: "my_crate::process".into(),
940                original_rust_path: String::new(),
941                params: vec![ParamDef {
942                    name: "config".into(),
943                    ty: TypeRef::Named("Config".into()),
944                    optional: false,
945                    default: None,
946                    sanitized: false,
947                    typed_default: None,
948                    is_ref: false,
949                    is_mut: false,
950                    newtype_wrapper: None,
951                    original_type: None,
952                }],
953                return_type: TypeRef::Unit,
954                is_async: false,
955                error_type: None,
956                doc: String::new(),
957                cfg: None,
958                sanitized: false,
959                returns_ref: false,
960                returns_cow: false,
961                return_newtype_wrapper: None,
962            }],
963            enums: vec![],
964            errors: vec![],
965        };
966        let names = input_type_names(&surface);
967        assert!(names.contains("Config"));
968    }
969
970    #[test]
971    fn test_input_type_names_from_return_types() {
972        let surface = ApiSurface {
973            crate_name: "my_crate".into(),
974            version: "1.0.0".into(),
975            types: vec![],
976            functions: vec![FunctionDef {
977                name: "get_result".into(),
978                rust_path: "my_crate::get_result".into(),
979                original_rust_path: String::new(),
980                params: vec![],
981                return_type: TypeRef::Named("Result".into()),
982                is_async: false,
983                error_type: None,
984                doc: String::new(),
985                cfg: None,
986                sanitized: false,
987                returns_ref: false,
988                returns_cow: false,
989                return_newtype_wrapper: None,
990            }],
991            enums: vec![],
992            errors: vec![],
993        };
994        let names = input_type_names(&surface);
995        assert!(names.contains("Result"));
996    }
997
998    // -----------------------------------------------------------------------
999    // helpers.rs: binding_to_core_match_arm / core_to_binding_match_arm
1000    // -----------------------------------------------------------------------
1001
1002    #[test]
1003    fn test_binding_to_core_match_arm_unit_variant() {
1004        let result = binding_to_core_match_arm("MyEnum", "Foo", &[]);
1005        assert_eq!(result, "MyEnum::Foo => Self::Foo,");
1006    }
1007
1008    #[test]
1009    fn test_binding_to_core_match_arm_data_variant_no_binding_data() {
1010        // When binding is unit-only and core has named fields, use Default
1011        let fields = vec![make_field("value", TypeRef::String)];
1012        let result = helpers::binding_to_core_match_arm_ext("MyEnum", "Foo", &fields, false);
1013        assert!(result.contains("value: Default::default()"));
1014    }
1015
1016    #[test]
1017    fn test_binding_to_core_match_arm_tuple_variant_no_binding_data() {
1018        let fields = vec![make_field("_0", TypeRef::String)];
1019        let result = helpers::binding_to_core_match_arm_ext("MyEnum", "Foo", &fields, false);
1020        assert!(result.contains("Default::default()"));
1021        assert!(result.contains("Self::Foo("));
1022    }
1023
1024    #[test]
1025    fn test_binding_to_core_match_arm_data_variant_with_binding_data_named() {
1026        // Binding has data — destructure and convert
1027        let fields = vec![make_field("value", TypeRef::Named("Inner".into()))];
1028        let result = helpers::binding_to_core_match_arm_ext("MyEnum", "Foo", &fields, true);
1029        assert!(result.contains("value: value.into()"));
1030    }
1031
1032    #[test]
1033    fn test_binding_to_core_match_arm_data_variant_with_binding_data_tuple() {
1034        let fields = vec![make_field("_0", TypeRef::Named("Inner".into()))];
1035        let result = helpers::binding_to_core_match_arm_ext("MyEnum", "Foo", &fields, true);
1036        assert!(result.contains("_0.into()"));
1037    }
1038
1039    #[test]
1040    fn test_core_to_binding_match_arm_unit_variant() {
1041        let result = core_to_binding_match_arm("CoreEnum", "Bar", &[]);
1042        assert_eq!(result, "CoreEnum::Bar => Self::Bar,");
1043    }
1044
1045    #[test]
1046    fn test_core_to_binding_match_arm_data_named_no_binding_data() {
1047        let fields = vec![make_field("x", TypeRef::Primitive(PrimitiveType::I32))];
1048        let result = helpers::core_to_binding_match_arm_ext("CoreEnum", "Bar", &fields, false);
1049        assert!(result.contains("{ .. }"));
1050        assert!(result.contains("Self::Bar"));
1051    }
1052
1053    #[test]
1054    fn test_core_to_binding_match_arm_data_tuple_no_binding_data() {
1055        let fields = vec![make_field("_0", TypeRef::Primitive(PrimitiveType::I32))];
1056        let result = helpers::core_to_binding_match_arm_ext("CoreEnum", "Bar", &fields, false);
1057        assert!(result.contains("(..)"));
1058        assert!(result.contains("Self::Bar"));
1059    }
1060
1061    // -----------------------------------------------------------------------
1062    // helpers.rs: has_sanitized_fields
1063    // -----------------------------------------------------------------------
1064
1065    #[test]
1066    fn test_has_sanitized_fields_false() {
1067        let typ = make_type("Foo", "crate::Foo", vec![make_field("x", TypeRef::String)]);
1068        assert!(!has_sanitized_fields(&typ));
1069    }
1070
1071    #[test]
1072    fn test_has_sanitized_fields_true() {
1073        let mut field = make_field("x", TypeRef::String);
1074        field.sanitized = true;
1075        let typ = make_type("Foo", "crate::Foo", vec![field]);
1076        assert!(has_sanitized_fields(&typ));
1077    }
1078
1079    // -----------------------------------------------------------------------
1080    // binding_to_core.rs: gen_from_binding_to_core — various field types
1081    // -----------------------------------------------------------------------
1082
1083    #[test]
1084    fn test_gen_from_binding_to_core_string_field() {
1085        let typ = make_type("S", "c::S", vec![make_field("title", TypeRef::String)]);
1086        let result = gen_from_binding_to_core(&typ, "c");
1087        assert!(result.contains("impl From<S> for c::S"));
1088        assert!(result.contains("title: val.title"));
1089    }
1090
1091    #[test]
1092    fn test_gen_from_binding_to_core_duration_field() {
1093        let typ = make_type("S", "c::S", vec![make_field("timeout", TypeRef::Duration)]);
1094        let result = gen_from_binding_to_core(&typ, "c");
1095        assert!(result.contains("std::time::Duration::from_millis(val.timeout)"));
1096    }
1097
1098    #[test]
1099    fn test_gen_from_binding_to_core_optional_named_field() {
1100        let field = make_opt_field("backend", TypeRef::Named("Backend".into()));
1101        let typ = make_type("S", "c::S", vec![field]);
1102        let result = gen_from_binding_to_core(&typ, "c");
1103        assert!(result.contains("backend: val.backend.map(Into::into)"));
1104    }
1105
1106    #[test]
1107    fn test_gen_from_binding_to_core_vec_named_field() {
1108        let field = make_field("items", TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))));
1109        let typ = make_type("S", "c::S", vec![field]);
1110        let result = gen_from_binding_to_core(&typ, "c");
1111        assert!(result.contains("into_iter().map(Into::into).collect()"));
1112    }
1113
1114    #[test]
1115    fn test_gen_from_binding_to_core_sanitized_field_uses_default() {
1116        let mut field = make_field("complex", TypeRef::String);
1117        field.sanitized = true;
1118        let typ = make_type("S", "c::S", vec![field]);
1119        let result = gen_from_binding_to_core(&typ, "c");
1120        assert!(result.contains("complex: Default::default()"));
1121    }
1122
1123    #[test]
1124    fn test_gen_from_binding_to_core_with_stripped_cfg_fields_uses_default_update() {
1125        let mut typ = make_type("S", "c::S", vec![make_field("x", TypeRef::String)]);
1126        typ.has_stripped_cfg_fields = true;
1127        let result = gen_from_binding_to_core(&typ, "c");
1128        assert!(result.contains("..Default::default()"));
1129        assert!(result.contains("#[allow(clippy::needless_update)]"));
1130    }
1131
1132    #[test]
1133    fn test_gen_from_binding_to_core_with_type_name_prefix() {
1134        let typ = make_type("Config", "c::Config", vec![make_field("x", TypeRef::String)]);
1135        let config = ConversionConfig {
1136            type_name_prefix: "Js",
1137            ..Default::default()
1138        };
1139        let result = gen_from_binding_to_core_cfg(&typ, "c", &config);
1140        assert!(result.contains("impl From<JsConfig> for c::Config"));
1141    }
1142
1143    #[test]
1144    fn test_gen_from_binding_to_core_path_field() {
1145        let field = make_field("file", TypeRef::Path);
1146        let typ = make_type("S", "c::S", vec![field]);
1147        let result = gen_from_binding_to_core(&typ, "c");
1148        assert!(result.contains("file: val.file.into()"));
1149    }
1150
1151    #[test]
1152    fn test_gen_from_binding_to_core_json_field() {
1153        let field = make_field("meta", TypeRef::Json);
1154        let typ = make_type("S", "c::S", vec![field]);
1155        let result = gen_from_binding_to_core(&typ, "c");
1156        assert!(result.contains("serde_json::from_str(&val.meta).unwrap_or_default()"));
1157    }
1158
1159    #[test]
1160    fn test_gen_from_binding_to_core_newtype_struct() {
1161        // A newtype is a struct with single field named "_0"
1162        let field = make_field("_0", TypeRef::Primitive(PrimitiveType::U32));
1163        let typ = make_type("NodeIndex", "c::NodeIndex", vec![field]);
1164        let result = gen_from_binding_to_core(&typ, "c");
1165        assert!(result.contains("Self(val._0)"));
1166    }
1167
1168    #[test]
1169    fn test_gen_from_binding_to_core_newtype_named_field() {
1170        let field = make_field("_0", TypeRef::Named("Inner".into()));
1171        let typ = make_type("Wrapper", "c::Wrapper", vec![field]);
1172        let result = gen_from_binding_to_core(&typ, "c");
1173        assert!(result.contains("Self(val._0.into())"));
1174    }
1175
1176    #[test]
1177    fn test_gen_from_binding_to_core_newtype_path_field() {
1178        let field = make_field("_0", TypeRef::Path);
1179        let typ = make_type("PathWrapper", "c::PathWrapper", vec![field]);
1180        let result = gen_from_binding_to_core(&typ, "c");
1181        assert!(result.contains("Self(val._0.into())"));
1182    }
1183
1184    #[test]
1185    fn test_gen_from_binding_to_core_newtype_duration_field() {
1186        let field = make_field("_0", TypeRef::Duration);
1187        let typ = make_type("DurWrapper", "c::DurWrapper", vec![field]);
1188        let result = gen_from_binding_to_core(&typ, "c");
1189        assert!(result.contains("Self(std::time::Duration::from_millis(val._0))"));
1190    }
1191
1192    #[test]
1193    fn test_gen_from_binding_to_core_boxed_named_field() {
1194        let mut field = make_field("child", TypeRef::Named("Child".into()));
1195        field.is_boxed = true;
1196        let typ = make_type("S", "c::S", vec![field]);
1197        let result = gen_from_binding_to_core(&typ, "c");
1198        assert!(result.contains("Box::new("));
1199    }
1200
1201    #[test]
1202    fn test_gen_from_binding_to_core_cast_large_ints_to_i64() {
1203        let field = make_field("count", TypeRef::Primitive(PrimitiveType::U64));
1204        let typ = make_type("S", "c::S", vec![field]);
1205        let config = ConversionConfig {
1206            cast_large_ints_to_i64: true,
1207            ..Default::default()
1208        };
1209        let result = gen_from_binding_to_core_cfg(&typ, "c", &config);
1210        assert!(result.contains("val.count as u64"));
1211    }
1212
1213    #[test]
1214    fn test_gen_from_binding_to_core_exclude_types_skips_field() {
1215        let field = make_field("js_val", TypeRef::Named("JsValue".into()));
1216        let typ = make_type("S", "c::S", vec![field]);
1217        let excluded = vec!["JsValue".to_string()];
1218        let config = ConversionConfig {
1219            exclude_types: &excluded,
1220            ..Default::default()
1221        };
1222        let result = gen_from_binding_to_core_cfg(&typ, "c", &config);
1223        assert!(result.contains("js_val: Default::default()"));
1224    }
1225
1226    // -----------------------------------------------------------------------
1227    // core_to_binding.rs: gen_from_core_to_binding — various field types
1228    // -----------------------------------------------------------------------
1229
1230    #[test]
1231    fn test_gen_from_core_to_binding_string_field() {
1232        let typ = make_type("S", "c::S", vec![make_field("title", TypeRef::String)]);
1233        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1234        assert!(result.contains("impl From<c::S> for S"));
1235        assert!(result.contains("title: val.title"));
1236    }
1237
1238    #[test]
1239    fn test_gen_from_core_to_binding_duration_field() {
1240        let field = make_field("timeout", TypeRef::Duration);
1241        let typ = make_type("S", "c::S", vec![field]);
1242        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1243        assert!(result.contains("val.timeout.as_millis() as u64"));
1244    }
1245
1246    #[test]
1247    fn test_gen_from_core_to_binding_path_field() {
1248        let field = make_field("path", TypeRef::Path);
1249        let typ = make_type("S", "c::S", vec![field]);
1250        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1251        assert!(result.contains("to_string_lossy().to_string()"));
1252    }
1253
1254    #[test]
1255    fn test_gen_from_core_to_binding_json_field() {
1256        let field = make_field("meta", TypeRef::Json);
1257        let typ = make_type("S", "c::S", vec![field]);
1258        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1259        assert!(result.contains("val.meta.to_string()"));
1260    }
1261
1262    #[test]
1263    fn test_gen_from_core_to_binding_opaque_field() {
1264        let mut opaques = AHashSet::new();
1265        opaques.insert("Client".to_string());
1266        let field = make_field("client", TypeRef::Named("Client".into()));
1267        let typ = make_type("S", "c::S", vec![field]);
1268        let result = gen_from_core_to_binding(&typ, "c", &opaques);
1269        assert!(result.contains("Arc::new(val.client)"));
1270        assert!(result.contains("Client { inner:"));
1271    }
1272
1273    #[test]
1274    fn test_gen_from_core_to_binding_with_type_name_prefix_opaque() {
1275        let mut opaques = AHashSet::new();
1276        opaques.insert("Client".to_string());
1277        let field = make_field("client", TypeRef::Named("Client".into()));
1278        let typ = make_type("S", "c::S", vec![field]);
1279        let config = ConversionConfig {
1280            type_name_prefix: "Js",
1281            ..Default::default()
1282        };
1283        let result = gen_from_core_to_binding_cfg(&typ, "c", &opaques, &config);
1284        assert!(result.contains("JsClient { inner: Arc::new(val.client) }"));
1285    }
1286
1287    #[test]
1288    fn test_gen_from_core_to_binding_sanitized_field() {
1289        let mut field = make_field("complex", TypeRef::String);
1290        field.sanitized = true;
1291        let typ = make_type("S", "c::S", vec![field]);
1292        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1293        assert!(result.contains("format!("));
1294    }
1295
1296    #[test]
1297    fn test_gen_from_core_to_binding_newtype_struct() {
1298        let field = make_field("_0", TypeRef::Primitive(PrimitiveType::U32));
1299        let typ = make_type("NodeIndex", "c::NodeIndex", vec![field]);
1300        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1301        assert!(result.contains("Self { _0: val.0 }"));
1302    }
1303
1304    #[test]
1305    fn test_gen_from_core_to_binding_newtype_path_field() {
1306        let field = make_field("_0", TypeRef::Path);
1307        let typ = make_type("PathWrapper", "c::PathWrapper", vec![field]);
1308        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1309        assert!(result.contains("val.0.to_string_lossy().to_string()"));
1310    }
1311
1312    #[test]
1313    fn test_gen_from_core_to_binding_newtype_duration_field() {
1314        let field = make_field("_0", TypeRef::Duration);
1315        let typ = make_type("DurWrapper", "c::DurWrapper", vec![field]);
1316        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1317        assert!(result.contains("val.0.as_millis() as u64"));
1318    }
1319
1320    #[test]
1321    fn test_gen_from_core_to_binding_newtype_named_field() {
1322        let field = make_field("_0", TypeRef::Named("Inner".into()));
1323        let typ = make_type("Wrapper", "c::Wrapper", vec![field]);
1324        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1325        assert!(result.contains("val.0.into()"));
1326    }
1327
1328    #[test]
1329    fn test_gen_from_core_to_binding_cast_large_ints_to_i64() {
1330        let field = make_field("count", TypeRef::Primitive(PrimitiveType::U64));
1331        let typ = make_type("S", "c::S", vec![field]);
1332        let config = ConversionConfig {
1333            cast_large_ints_to_i64: true,
1334            ..Default::default()
1335        };
1336        let result = gen_from_core_to_binding_cfg(&typ, "c", &no_opaques(), &config);
1337        assert!(result.contains("val.count as i64"));
1338    }
1339
1340    #[test]
1341    fn test_gen_from_core_to_binding_cast_f32_to_f64() {
1342        let field = make_field("score", TypeRef::Primitive(PrimitiveType::F32));
1343        let typ = make_type("S", "c::S", vec![field]);
1344        let config = ConversionConfig {
1345            cast_f32_to_f64: true,
1346            ..Default::default()
1347        };
1348        let result = gen_from_core_to_binding_cfg(&typ, "c", &no_opaques(), &config);
1349        assert!(result.contains("val.score as f64"));
1350    }
1351
1352    #[test]
1353    fn test_gen_from_core_to_binding_exclude_types_skips_field() {
1354        let field = make_field("js_val", TypeRef::Named("JsValue".into()));
1355        let typ = make_type("S", "c::S", vec![field]);
1356        let excluded = vec!["JsValue".to_string()];
1357        let config = ConversionConfig {
1358            exclude_types: &excluded,
1359            ..Default::default()
1360        };
1361        let result = gen_from_core_to_binding_cfg(&typ, "c", &no_opaques(), &config);
1362        // The field must be absent from the generated struct literal
1363        assert!(!result.contains("js_val:"));
1364    }
1365
1366    #[test]
1367    fn test_gen_from_core_to_binding_vec_json_field() {
1368        let field = make_field("items", TypeRef::Vec(Box::new(TypeRef::Json)));
1369        let typ = make_type("S", "c::S", vec![field]);
1370        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1371        assert!(result.contains("map(ToString::to_string)"));
1372    }
1373
1374    #[test]
1375    fn test_gen_from_core_to_binding_char_field() {
1376        let field = make_field("sep", TypeRef::Char);
1377        let typ = make_type("S", "c::S", vec![field]);
1378        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1379        assert!(result.contains("val.sep.to_string()"));
1380    }
1381
1382    #[test]
1383    fn test_gen_from_core_to_binding_bytes_field() {
1384        let field = make_field("data", TypeRef::Bytes);
1385        let typ = make_type("S", "c::S", vec![field]);
1386        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
1387        assert!(result.contains("val.data.to_vec()"));
1388    }
1389
1390    // -----------------------------------------------------------------------
1391    // cfg_field: field_conversion_to_core_cfg — backend-specific variations
1392    // -----------------------------------------------------------------------
1393
1394    #[test]
1395    fn test_field_conversion_to_core_cfg_no_flags_delegates_to_base() {
1396        let config = ConversionConfig::default();
1397        let result = field_conversion_to_core_cfg("x", &TypeRef::String, false, &config);
1398        assert_eq!(result, "x: val.x");
1399    }
1400
1401    #[test]
1402    fn test_field_conversion_to_core_cfg_cast_u64_to_i64() {
1403        let config = ConversionConfig {
1404            cast_large_ints_to_i64: true,
1405            ..Default::default()
1406        };
1407        let result = field_conversion_to_core_cfg("n", &TypeRef::Primitive(PrimitiveType::U64), false, &config);
1408        assert_eq!(result, "n: val.n as u64");
1409    }
1410
1411    #[test]
1412    fn test_field_conversion_to_core_cfg_cast_usize_to_i64() {
1413        let config = ConversionConfig {
1414            cast_large_ints_to_i64: true,
1415            ..Default::default()
1416        };
1417        let result = field_conversion_to_core_cfg("n", &TypeRef::Primitive(PrimitiveType::Usize), false, &config);
1418        assert_eq!(result, "n: val.n as usize");
1419    }
1420
1421    #[test]
1422    fn test_field_conversion_to_core_cfg_cast_isize_to_i64() {
1423        let config = ConversionConfig {
1424            cast_large_ints_to_i64: true,
1425            ..Default::default()
1426        };
1427        let result = field_conversion_to_core_cfg("n", &TypeRef::Primitive(PrimitiveType::Isize), false, &config);
1428        assert_eq!(result, "n: val.n as isize");
1429    }
1430
1431    #[test]
1432    fn test_field_conversion_to_core_cfg_f32_cast() {
1433        let config = ConversionConfig {
1434            cast_f32_to_f64: true,
1435            ..Default::default()
1436        };
1437        let result = field_conversion_to_core_cfg("s", &TypeRef::Primitive(PrimitiveType::F32), false, &config);
1438        assert_eq!(result, "s: val.s as f32");
1439    }
1440
1441    #[test]
1442    fn test_field_conversion_to_core_cfg_duration_cast() {
1443        let config = ConversionConfig {
1444            cast_large_ints_to_i64: true,
1445            ..Default::default()
1446        };
1447        let result = field_conversion_to_core_cfg("t", &TypeRef::Duration, false, &config);
1448        assert_eq!(result, "t: std::time::Duration::from_millis(val.t as u64)");
1449    }
1450
1451    #[test]
1452    fn test_field_conversion_to_core_cfg_json_to_string() {
1453        let config = ConversionConfig {
1454            json_to_string: true,
1455            ..Default::default()
1456        };
1457        let result = field_conversion_to_core_cfg("m", &TypeRef::Json, false, &config);
1458        assert_eq!(result, "m: Default::default()");
1459    }
1460
1461    #[test]
1462    fn test_field_conversion_to_core_cfg_vec_named_to_string() {
1463        let config = ConversionConfig {
1464            vec_named_to_string: true,
1465            ..Default::default()
1466        };
1467        let ty = TypeRef::Vec(Box::new(TypeRef::Named("Item".into())));
1468        let result = field_conversion_to_core_cfg("items", &ty, false, &config);
1469        assert_eq!(result, "items: serde_json::from_str(&val.items).unwrap_or_default()");
1470    }
1471
1472    // -----------------------------------------------------------------------
1473    // field_conversion_from_core_cfg — backend-specific variations
1474    // -----------------------------------------------------------------------
1475
1476    #[test]
1477    fn test_field_conversion_from_core_cfg_no_flags_delegates_to_base() {
1478        let config = ConversionConfig::default();
1479        let result = field_conversion_from_core_cfg("x", &TypeRef::String, false, false, &no_opaques(), &config);
1480        assert_eq!(result, "x: val.x");
1481    }
1482
1483    #[test]
1484    fn test_field_conversion_from_core_cfg_cast_u64_to_i64() {
1485        let config = ConversionConfig {
1486            cast_large_ints_to_i64: true,
1487            ..Default::default()
1488        };
1489        let result = field_conversion_from_core_cfg(
1490            "n",
1491            &TypeRef::Primitive(PrimitiveType::U64),
1492            false,
1493            false,
1494            &no_opaques(),
1495            &config,
1496        );
1497        assert_eq!(result, "n: val.n as i64");
1498    }
1499
1500    #[test]
1501    fn test_field_conversion_from_core_cfg_cast_f32_to_f64() {
1502        let config = ConversionConfig {
1503            cast_f32_to_f64: true,
1504            ..Default::default()
1505        };
1506        let result = field_conversion_from_core_cfg(
1507            "s",
1508            &TypeRef::Primitive(PrimitiveType::F32),
1509            false,
1510            false,
1511            &no_opaques(),
1512            &config,
1513        );
1514        assert_eq!(result, "s: val.s as f64");
1515    }
1516
1517    #[test]
1518    fn test_field_conversion_from_core_cfg_duration_cast_to_i64() {
1519        let config = ConversionConfig {
1520            cast_large_ints_to_i64: true,
1521            ..Default::default()
1522        };
1523        let result = field_conversion_from_core_cfg("t", &TypeRef::Duration, false, false, &no_opaques(), &config);
1524        assert_eq!(result, "t: val.t.as_millis() as u64 as i64");
1525    }
1526
1527    #[test]
1528    fn test_field_conversion_from_core_cfg_json_to_string() {
1529        let config = ConversionConfig {
1530            json_to_string: true,
1531            ..Default::default()
1532        };
1533        let result = field_conversion_from_core_cfg("m", &TypeRef::Json, false, false, &no_opaques(), &config);
1534        assert_eq!(result, "m: val.m.to_string()");
1535    }
1536
1537    #[test]
1538    fn test_field_conversion_from_core_cfg_vec_named_to_string() {
1539        let config = ConversionConfig {
1540            vec_named_to_string: true,
1541            ..Default::default()
1542        };
1543        let ty = TypeRef::Vec(Box::new(TypeRef::Named("Item".into())));
1544        let result = field_conversion_from_core_cfg("items", &ty, false, false, &no_opaques(), &config);
1545        assert_eq!(result, "items: serde_json::to_string(&val.items).unwrap_or_default()");
1546    }
1547
1548    #[test]
1549    fn test_field_conversion_from_core_cfg_vec_u64_cast() {
1550        let config = ConversionConfig {
1551            cast_large_ints_to_i64: true,
1552            ..Default::default()
1553        };
1554        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::U64)));
1555        let result = field_conversion_from_core_cfg("ids", &ty, false, false, &no_opaques(), &config);
1556        assert!(result.contains("as i64"));
1557    }
1558
1559    #[test]
1560    fn test_field_conversion_from_core_cfg_vec_f32_cast() {
1561        let config = ConversionConfig {
1562            cast_f32_to_f64: true,
1563            ..Default::default()
1564        };
1565        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::F32)));
1566        let result = field_conversion_from_core_cfg("scores", &ty, false, false, &no_opaques(), &config);
1567        assert!(result.contains("as f64"));
1568    }
1569
1570    #[test]
1571    fn test_field_conversion_from_core_cfg_map_u64_values_cast() {
1572        let config = ConversionConfig {
1573            cast_large_ints_to_i64: true,
1574            ..Default::default()
1575        };
1576        let ty = TypeRef::Map(
1577            Box::new(TypeRef::String),
1578            Box::new(TypeRef::Primitive(PrimitiveType::U64)),
1579        );
1580        let result = field_conversion_from_core_cfg("map", &ty, false, false, &no_opaques(), &config);
1581        assert!(result.contains("as i64"));
1582    }
1583
1584    // -----------------------------------------------------------------------
1585    // convertible_types / core_to_binding_convertible_types
1586    // -----------------------------------------------------------------------
1587
1588    #[test]
1589    fn test_convertible_types_simple_struct() {
1590        let surface = ApiSurface {
1591            crate_name: "c".into(),
1592            version: "1.0".into(),
1593            types: vec![make_type("Config", "c::Config", vec![make_field("x", TypeRef::String)])],
1594            functions: vec![],
1595            enums: vec![],
1596            errors: vec![],
1597        };
1598        let result = convertible_types(&surface);
1599        assert!(result.contains("Config"));
1600    }
1601
1602    #[test]
1603    fn test_convertible_types_excludes_type_with_unconvertible_named_field() {
1604        // "Unknown" is not in the surface — types referencing it are removed
1605        let field = make_field("inner", TypeRef::Named("Unknown".into()));
1606        let surface = ApiSurface {
1607            crate_name: "c".into(),
1608            version: "1.0".into(),
1609            types: vec![make_type("Wrapper", "c::Wrapper", vec![field])],
1610            functions: vec![],
1611            enums: vec![],
1612            errors: vec![],
1613        };
1614        let result = convertible_types(&surface);
1615        assert!(!result.contains("Wrapper"));
1616    }
1617
1618    #[test]
1619    fn test_core_to_binding_convertible_types_simple() {
1620        let surface = ApiSurface {
1621            crate_name: "c".into(),
1622            version: "1.0".into(),
1623            types: vec![make_type("Config", "c::Config", vec![make_field("x", TypeRef::String)])],
1624            functions: vec![],
1625            enums: vec![],
1626            errors: vec![],
1627        };
1628        let result = core_to_binding_convertible_types(&surface);
1629        assert!(result.contains("Config"));
1630    }
1631
1632    #[test]
1633    fn test_can_generate_conversion_true_when_in_set() {
1634        let mut set = AHashSet::new();
1635        set.insert("Config".to_string());
1636        let typ = make_type("Config", "c::Config", vec![]);
1637        assert!(can_generate_conversion(&typ, &set));
1638    }
1639
1640    #[test]
1641    fn test_can_generate_conversion_false_when_absent() {
1642        let set = AHashSet::new();
1643        let typ = make_type("Config", "c::Config", vec![]);
1644        assert!(!can_generate_conversion(&typ, &set));
1645    }
1646
1647    // -----------------------------------------------------------------------
1648    // field_conversion_to_core_cfg — optional variants of cast flags
1649    // -----------------------------------------------------------------------
1650
1651    #[test]
1652    fn test_field_conversion_to_core_cfg_cast_u64_optional() {
1653        let config = ConversionConfig {
1654            cast_large_ints_to_i64: true,
1655            ..Default::default()
1656        };
1657        let result = field_conversion_to_core_cfg("n", &TypeRef::Primitive(PrimitiveType::U64), true, &config);
1658        assert_eq!(result, "n: val.n.map(|v| v as u64)");
1659    }
1660
1661    #[test]
1662    fn test_field_conversion_to_core_cfg_cast_usize_optional() {
1663        let config = ConversionConfig {
1664            cast_large_ints_to_i64: true,
1665            ..Default::default()
1666        };
1667        let result = field_conversion_to_core_cfg("n", &TypeRef::Primitive(PrimitiveType::Usize), true, &config);
1668        assert_eq!(result, "n: val.n.map(|v| v as usize)");
1669    }
1670
1671    #[test]
1672    fn test_field_conversion_to_core_cfg_cast_isize_optional() {
1673        let config = ConversionConfig {
1674            cast_large_ints_to_i64: true,
1675            ..Default::default()
1676        };
1677        let result = field_conversion_to_core_cfg("n", &TypeRef::Primitive(PrimitiveType::Isize), true, &config);
1678        assert_eq!(result, "n: val.n.map(|v| v as isize)");
1679    }
1680
1681    #[test]
1682    fn test_field_conversion_to_core_cfg_cast_f32_optional() {
1683        let config = ConversionConfig {
1684            cast_f32_to_f64: true,
1685            ..Default::default()
1686        };
1687        let result = field_conversion_to_core_cfg("s", &TypeRef::Primitive(PrimitiveType::F32), true, &config);
1688        assert_eq!(result, "s: val.s.map(|v| v as f32)");
1689    }
1690
1691    #[test]
1692    fn test_field_conversion_to_core_cfg_duration_cast_optional() {
1693        let config = ConversionConfig {
1694            cast_large_ints_to_i64: true,
1695            ..Default::default()
1696        };
1697        let result = field_conversion_to_core_cfg("t", &TypeRef::Duration, true, &config);
1698        assert_eq!(result, "t: val.t.map(|v| std::time::Duration::from_millis(v as u64))");
1699    }
1700
1701    #[test]
1702    fn test_field_conversion_to_core_cfg_json_to_string_optional() {
1703        let config = ConversionConfig {
1704            json_to_string: true,
1705            ..Default::default()
1706        };
1707        let result = field_conversion_to_core_cfg("m", &TypeRef::Json, true, &config);
1708        // json_to_string is lossy; optional still falls through to Default::default() path
1709        assert_eq!(result, "m: Default::default()");
1710    }
1711
1712    #[test]
1713    fn test_field_conversion_to_core_cfg_vec_named_to_string_optional() {
1714        let config = ConversionConfig {
1715            vec_named_to_string: true,
1716            ..Default::default()
1717        };
1718        let ty = TypeRef::Vec(Box::new(TypeRef::Named("Item".into())));
1719        let result = field_conversion_to_core_cfg("items", &ty, true, &config);
1720        assert_eq!(
1721            result,
1722            "items: val.items.as_ref().and_then(|s| serde_json::from_str(s).ok())"
1723        );
1724    }
1725
1726    #[test]
1727    fn test_field_conversion_to_core_cfg_vec_u64_cast_optional() {
1728        let config = ConversionConfig {
1729            cast_large_ints_to_i64: true,
1730            ..Default::default()
1731        };
1732        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::U64)));
1733        let result = field_conversion_to_core_cfg("ids", &ty, true, &config);
1734        assert!(result.contains("as u64"));
1735    }
1736
1737    #[test]
1738    fn test_field_conversion_to_core_cfg_vec_f32_cast_optional() {
1739        let config = ConversionConfig {
1740            cast_f32_to_f64: true,
1741            ..Default::default()
1742        };
1743        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::F32)));
1744        let result = field_conversion_to_core_cfg("scores", &ty, true, &config);
1745        assert!(result.contains("as f32"));
1746    }
1747
1748    #[test]
1749    fn test_field_conversion_to_core_cfg_map_u64_values_cast_optional() {
1750        let config = ConversionConfig {
1751            cast_large_ints_to_i64: true,
1752            ..Default::default()
1753        };
1754        let ty = TypeRef::Map(
1755            Box::new(TypeRef::String),
1756            Box::new(TypeRef::Primitive(PrimitiveType::U64)),
1757        );
1758        let result = field_conversion_to_core_cfg("map", &ty, true, &config);
1759        assert!(result.contains("as u64"));
1760    }
1761
1762    #[test]
1763    fn test_field_conversion_to_core_cfg_optional_inner_u64_cast() {
1764        // Optional(Primitive(U64)) should map element with cast
1765        let config = ConversionConfig {
1766            cast_large_ints_to_i64: true,
1767            ..Default::default()
1768        };
1769        let ty = TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::U64)));
1770        let result = field_conversion_to_core_cfg("n", &ty, false, &config);
1771        assert!(result.contains("as u64"));
1772    }
1773
1774    // -----------------------------------------------------------------------
1775    // field_conversion_from_core_cfg — optional variants and missing branches
1776    // -----------------------------------------------------------------------
1777
1778    #[test]
1779    fn test_field_conversion_from_core_cfg_cast_u64_optional() {
1780        let config = ConversionConfig {
1781            cast_large_ints_to_i64: true,
1782            ..Default::default()
1783        };
1784        let result = field_conversion_from_core_cfg(
1785            "n",
1786            &TypeRef::Primitive(PrimitiveType::U64),
1787            true,
1788            false,
1789            &no_opaques(),
1790            &config,
1791        );
1792        assert_eq!(result, "n: val.n.map(|v| v as i64)");
1793    }
1794
1795    #[test]
1796    fn test_field_conversion_from_core_cfg_cast_usize_optional() {
1797        let config = ConversionConfig {
1798            cast_large_ints_to_i64: true,
1799            ..Default::default()
1800        };
1801        let result = field_conversion_from_core_cfg(
1802            "n",
1803            &TypeRef::Primitive(PrimitiveType::Usize),
1804            true,
1805            false,
1806            &no_opaques(),
1807            &config,
1808        );
1809        assert_eq!(result, "n: val.n.map(|v| v as i64)");
1810    }
1811
1812    #[test]
1813    fn test_field_conversion_from_core_cfg_cast_isize_optional() {
1814        let config = ConversionConfig {
1815            cast_large_ints_to_i64: true,
1816            ..Default::default()
1817        };
1818        let result = field_conversion_from_core_cfg(
1819            "n",
1820            &TypeRef::Primitive(PrimitiveType::Isize),
1821            true,
1822            false,
1823            &no_opaques(),
1824            &config,
1825        );
1826        assert_eq!(result, "n: val.n.map(|v| v as i64)");
1827    }
1828
1829    #[test]
1830    fn test_field_conversion_from_core_cfg_cast_f32_optional() {
1831        let config = ConversionConfig {
1832            cast_f32_to_f64: true,
1833            ..Default::default()
1834        };
1835        let result = field_conversion_from_core_cfg(
1836            "s",
1837            &TypeRef::Primitive(PrimitiveType::F32),
1838            true,
1839            false,
1840            &no_opaques(),
1841            &config,
1842        );
1843        assert_eq!(result, "s: val.s.map(|v| v as f64)");
1844    }
1845
1846    #[test]
1847    fn test_field_conversion_from_core_cfg_duration_cast_optional() {
1848        let config = ConversionConfig {
1849            cast_large_ints_to_i64: true,
1850            ..Default::default()
1851        };
1852        let result = field_conversion_from_core_cfg("t", &TypeRef::Duration, true, false, &no_opaques(), &config);
1853        assert_eq!(result, "t: val.t.map(|d| d.as_millis() as u64 as i64)");
1854    }
1855
1856    #[test]
1857    fn test_field_conversion_from_core_cfg_json_to_string_optional() {
1858        let config = ConversionConfig {
1859            json_to_string: true,
1860            ..Default::default()
1861        };
1862        let result = field_conversion_from_core_cfg("m", &TypeRef::Json, true, false, &no_opaques(), &config);
1863        assert_eq!(result, "m: val.m.as_ref().map(ToString::to_string)");
1864    }
1865
1866    #[test]
1867    fn test_field_conversion_from_core_cfg_vec_named_to_string_optional() {
1868        let config = ConversionConfig {
1869            vec_named_to_string: true,
1870            ..Default::default()
1871        };
1872        let ty = TypeRef::Vec(Box::new(TypeRef::Named("Item".into())));
1873        let result = field_conversion_from_core_cfg("items", &ty, true, false, &no_opaques(), &config);
1874        assert_eq!(
1875            result,
1876            "items: val.items.as_ref().and_then(|v| serde_json::to_string(v).ok())"
1877        );
1878    }
1879
1880    #[test]
1881    fn test_field_conversion_from_core_cfg_vec_u64_cast_optional() {
1882        let config = ConversionConfig {
1883            cast_large_ints_to_i64: true,
1884            ..Default::default()
1885        };
1886        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::U64)));
1887        let result = field_conversion_from_core_cfg("ids", &ty, true, false, &no_opaques(), &config);
1888        assert!(result.contains("as i64"));
1889    }
1890
1891    #[test]
1892    fn test_field_conversion_from_core_cfg_vec_f32_cast_optional() {
1893        let config = ConversionConfig {
1894            cast_f32_to_f64: true,
1895            ..Default::default()
1896        };
1897        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::F32)));
1898        let result = field_conversion_from_core_cfg("scores", &ty, true, false, &no_opaques(), &config);
1899        assert!(result.contains("as f64"));
1900    }
1901
1902    #[test]
1903    fn test_field_conversion_from_core_cfg_optional_inner_u64_cast() {
1904        // Optional(Primitive(U64)) with cast should map element
1905        let config = ConversionConfig {
1906            cast_large_ints_to_i64: true,
1907            ..Default::default()
1908        };
1909        let ty = TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::U64)));
1910        let result = field_conversion_from_core_cfg("n", &ty, false, false, &no_opaques(), &config);
1911        assert!(result.contains("as i64"));
1912    }
1913
1914    #[test]
1915    fn test_field_conversion_from_core_cfg_map_u64_values_cast_optional() {
1916        let config = ConversionConfig {
1917            cast_large_ints_to_i64: true,
1918            ..Default::default()
1919        };
1920        let ty = TypeRef::Map(
1921            Box::new(TypeRef::String),
1922            Box::new(TypeRef::Primitive(PrimitiveType::U64)),
1923        );
1924        let result = field_conversion_from_core_cfg("map", &ty, true, false, &no_opaques(), &config);
1925        assert!(result.contains("as i64"));
1926    }
1927
1928    // -----------------------------------------------------------------------
1929    // Complex nested types: Optional<Vec<Named>>, Map<String, Optional<Named>>,
1930    // Vec<Map<String, String>>
1931    // -----------------------------------------------------------------------
1932
1933    #[test]
1934    fn test_field_conversion_to_core_optional_vec_string() {
1935        // Optional(Vec(String)) — passthrough
1936        let ty = TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::String))));
1937        let result = field_conversion_to_core("items", &ty, false);
1938        assert_eq!(result, "items: val.items");
1939    }
1940
1941    #[test]
1942    fn test_field_conversion_to_core_optional_vec_named_inner() {
1943        // Optional(Vec(Named)) — map into
1944        let ty = TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Item".into())))));
1945        let result = field_conversion_to_core("items", &ty, false);
1946        assert_eq!(
1947            result,
1948            "items: val.items.map(|v| v.into_iter().map(Into::into).collect())"
1949        );
1950    }
1951
1952    #[test]
1953    fn test_field_conversion_to_core_map_string_optional_named() {
1954        // Map(String, Optional(Named)) — named value uses .into()
1955        let ty = TypeRef::Map(
1956            Box::new(TypeRef::String),
1957            Box::new(TypeRef::Optional(Box::new(TypeRef::Named("Val".into())))),
1958        );
1959        // Optional inner Named doesn't trigger `has_named_val` (only TypeRef::Named at top), so falls
1960        // through to plain collect (there's no special map for Optional-value Named).
1961        let result = field_conversion_to_core("map", &ty, false);
1962        assert_eq!(result, "map: val.map.into_iter().collect()");
1963    }
1964
1965    #[test]
1966    fn test_field_conversion_to_core_map_string_vec_named_value() {
1967        // Map(String, Vec(Named)) — Vec<Named> values need per-vector Into mapping
1968        let ty = TypeRef::Map(
1969            Box::new(TypeRef::String),
1970            Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Item".into())))),
1971        );
1972        let result = field_conversion_to_core("map", &ty, false);
1973        assert!(result.contains("v.into_iter().map(Into::into).collect()"));
1974    }
1975
1976    #[test]
1977    fn test_field_conversion_to_core_map_string_vec_json_value() {
1978        // Map(String, Vec(Json)) — Vec<Json> values need per-vector serde deserialization
1979        let ty = TypeRef::Map(
1980            Box::new(TypeRef::String),
1981            Box::new(TypeRef::Vec(Box::new(TypeRef::Json))),
1982        );
1983        let result = field_conversion_to_core("map", &ty, false);
1984        assert!(result.contains("filter_map(|s| serde_json::from_str(&s).ok()).collect()"));
1985    }
1986
1987    #[test]
1988    fn test_field_conversion_to_core_map_string_string_optional() {
1989        // Map(String, String) with optional=true
1990        let ty = TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String));
1991        let result = field_conversion_to_core("map", &ty, true);
1992        assert_eq!(result, "map: val.map.map(|m| m.into_iter().collect())");
1993    }
1994
1995    #[test]
1996    fn test_field_conversion_from_core_optional_vec_named() {
1997        // Optional(Vec(Named)) — per-element .into() mapping
1998        let ty = TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Item".into())))));
1999        let result = field_conversion_from_core("items", &ty, false, false, &no_opaques());
2000        // falls through to field_conversion_to_core (symmetric case)
2001        assert!(result.contains("map(Into::into)") || result.contains("into_iter().map(Into::into)"));
2002    }
2003
2004    #[test]
2005    fn test_field_conversion_from_core_map_string_named_values() {
2006        // Map(String, Named) — Named values need .into()
2007        let ty = TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Named("Val".into())));
2008        let result = field_conversion_from_core("map", &ty, false, false, &no_opaques());
2009        // No asymmetric logic for Named map values in from_core — falls through to to_core symmetric
2010        assert!(result.contains("v.into()"));
2011    }
2012
2013    #[test]
2014    fn test_field_conversion_to_core_vec_string_passthrough() {
2015        // Vec(String) — passthrough (not a special case)
2016        let ty = TypeRef::Vec(Box::new(TypeRef::String));
2017        let result = field_conversion_to_core("tags", &ty, false);
2018        assert_eq!(result, "tags: val.tags");
2019    }
2020
2021    #[test]
2022    fn test_field_conversion_to_core_vec_string_optional() {
2023        // Vec(String) optional
2024        let ty = TypeRef::Vec(Box::new(TypeRef::String));
2025        let result = field_conversion_to_core("tags", &ty, true);
2026        assert_eq!(result, "tags: val.tags");
2027    }
2028
2029    #[test]
2030    fn test_field_conversion_to_core_map_named_key() {
2031        // Map(Named, String) — named key needs .into()
2032        let ty = TypeRef::Map(Box::new(TypeRef::Named("Key".into())), Box::new(TypeRef::String));
2033        let result = field_conversion_to_core("map", &ty, false);
2034        assert!(result.contains("k.into()"));
2035    }
2036
2037    #[test]
2038    fn test_field_conversion_to_core_map_json_key() {
2039        // Map(Json, String) — Json key gets deserialized
2040        let ty = TypeRef::Map(Box::new(TypeRef::Json), Box::new(TypeRef::String));
2041        let result = field_conversion_to_core("map", &ty, false);
2042        assert!(result.contains("serde_json::from_str(&k)"));
2043    }
2044
2045    #[test]
2046    fn test_field_conversion_from_core_optional_json_inner() {
2047        // Optional(Json) — binding uses Option<String> via .to_string()
2048        let ty = TypeRef::Optional(Box::new(TypeRef::Json));
2049        let result = field_conversion_from_core("meta", &ty, false, false, &no_opaques());
2050        assert_eq!(result, "meta: val.meta.as_ref().map(ToString::to_string)");
2051    }
2052
2053    #[test]
2054    fn test_field_conversion_from_core_optional_path_inner() {
2055        // Optional(Path) — binding uses to_string_lossy
2056        let ty = TypeRef::Optional(Box::new(TypeRef::Path));
2057        let result = field_conversion_from_core("file", &ty, false, false, &no_opaques());
2058        assert_eq!(result, "file: val.file.map(|p| p.to_string_lossy().to_string())");
2059    }
2060
2061    #[test]
2062    fn test_field_conversion_from_core_map_json_keys() {
2063        // Map(Json, String) — Json key gets .to_string()
2064        let ty = TypeRef::Map(Box::new(TypeRef::Json), Box::new(TypeRef::String));
2065        let result = field_conversion_from_core("map", &ty, false, false, &no_opaques());
2066        assert!(result.contains("k.to_string()"));
2067    }
2068
2069    // -----------------------------------------------------------------------
2070    // is_tuple_variant edge cases
2071    // -----------------------------------------------------------------------
2072
2073    #[test]
2074    fn test_is_tuple_variant_single_positional_field() {
2075        let fields = vec![make_field("_0", TypeRef::String)];
2076        assert!(is_tuple_variant(&fields));
2077    }
2078
2079    #[test]
2080    fn test_is_tuple_variant_true_for_underscore_only() {
2081        // "_".strip_prefix('_') == Some("") and "".chars().all(is_ascii_digit) is vacuously true
2082        let fields = vec![make_field("_", TypeRef::String)];
2083        assert!(is_tuple_variant(&fields));
2084    }
2085
2086    #[test]
2087    fn test_is_tuple_variant_false_for_field_starting_with_underscore_then_alpha() {
2088        // "_foo" — digits check fails
2089        let fields = vec![make_field("_foo", TypeRef::String)];
2090        assert!(!is_tuple_variant(&fields));
2091    }
2092
2093    #[test]
2094    fn test_is_tuple_variant_three_positional_fields() {
2095        let fields = vec![
2096            make_field("_0", TypeRef::String),
2097            make_field("_1", TypeRef::Primitive(PrimitiveType::I32)),
2098            make_field("_2", TypeRef::Primitive(PrimitiveType::F64)),
2099        ];
2100        assert!(is_tuple_variant(&fields));
2101    }
2102
2103    #[test]
2104    fn test_is_tuple_type_name_empty_string_is_false() {
2105        assert!(!helpers::is_tuple_type_name(""));
2106    }
2107
2108    #[test]
2109    fn test_is_tuple_type_name_space_is_false() {
2110        assert!(!helpers::is_tuple_type_name("String"));
2111    }
2112
2113    // -----------------------------------------------------------------------
2114    // core_type_path edge cases
2115    // -----------------------------------------------------------------------
2116
2117    #[test]
2118    fn test_core_type_path_with_hyphen_and_double_colon() {
2119        // Hyphens in path should be replaced; path still contains "::" so used verbatim
2120        let typ = make_type("Config", "my-crate::module::Config", vec![]);
2121        assert_eq!(core_type_path(&typ, "my_crate"), "my_crate::module::Config");
2122    }
2123
2124    #[test]
2125    fn test_core_type_path_rust_path_matches_core_import_prefix() {
2126        // Path already starts with core_import → used verbatim
2127        let typ = make_type("Config", "my_crate::Config", vec![]);
2128        assert_eq!(core_type_path(&typ, "my_crate"), "my_crate::Config");
2129    }
2130
2131    // -----------------------------------------------------------------------
2132    // build_type_path_map — multiple types, hyphens, enums
2133    // -----------------------------------------------------------------------
2134
2135    #[test]
2136    fn test_build_type_path_map_multiple_types() {
2137        let surface = ApiSurface {
2138            crate_name: "my_crate".into(),
2139            version: "1.0.0".into(),
2140            types: vec![
2141                make_type("Config", "my_crate::Config", vec![]),
2142                make_type("Result", "my_crate::types::Result", vec![]),
2143            ],
2144            functions: vec![],
2145            enums: vec![
2146                make_enum("Mode", "my_crate::Mode", &["A"]),
2147                make_enum("Status", "Status", &["Ok"]),
2148            ],
2149            errors: vec![],
2150        };
2151        let map = build_type_path_map(&surface, "my_crate");
2152        assert_eq!(map.get("Config").map(String::as_str), Some("my_crate::Config"));
2153        assert_eq!(map.get("Result").map(String::as_str), Some("my_crate::types::Result"));
2154        assert_eq!(map.get("Mode").map(String::as_str), Some("my_crate::Mode"));
2155        // "Status" path has no "::" and doesn't start with core_import → prefixed
2156        assert_eq!(map.get("Status").map(String::as_str), Some("my_crate::Status"));
2157    }
2158
2159    #[test]
2160    fn test_build_type_path_map_normalizes_hyphens() {
2161        let surface = ApiSurface {
2162            crate_name: "my_crate".into(),
2163            version: "1.0.0".into(),
2164            types: vec![make_type("Config", "my-crate::Config", vec![])],
2165            functions: vec![],
2166            enums: vec![],
2167            errors: vec![],
2168        };
2169        let map = build_type_path_map(&surface, "my_crate");
2170        assert_eq!(map.get("Config").map(String::as_str), Some("my_crate::Config"));
2171    }
2172
2173    #[test]
2174    fn test_build_type_path_map_empty_surface() {
2175        let surface = ApiSurface {
2176            crate_name: "c".into(),
2177            version: "1.0".into(),
2178            types: vec![],
2179            functions: vec![],
2180            enums: vec![],
2181            errors: vec![],
2182        };
2183        let map = build_type_path_map(&surface, "c");
2184        assert!(map.is_empty());
2185    }
2186
2187    // -----------------------------------------------------------------------
2188    // Edge cases: empty fields, single-field structs, all-optional fields
2189    // -----------------------------------------------------------------------
2190
2191    #[test]
2192    fn test_gen_from_binding_to_core_empty_fields() {
2193        let typ = make_type("Empty", "c::Empty", vec![]);
2194        let result = gen_from_binding_to_core(&typ, "c");
2195        assert!(result.contains("impl From<Empty> for c::Empty"));
2196        assert!(result.contains("Self {"));
2197    }
2198
2199    #[test]
2200    fn test_gen_from_core_to_binding_empty_fields() {
2201        let typ = make_type("Empty", "c::Empty", vec![]);
2202        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
2203        assert!(result.contains("impl From<c::Empty> for Empty"));
2204        assert!(result.contains("Self {"));
2205    }
2206
2207    #[test]
2208    fn test_gen_from_binding_to_core_all_optional_fields() {
2209        let typ = make_type(
2210            "Config",
2211            "c::Config",
2212            vec![
2213                make_opt_field("name", TypeRef::String),
2214                make_opt_field("count", TypeRef::Primitive(PrimitiveType::I32)),
2215            ],
2216        );
2217        let result = gen_from_binding_to_core(&typ, "c");
2218        assert!(result.contains("name: val.name"));
2219        assert!(result.contains("count: val.count"));
2220    }
2221
2222    #[test]
2223    fn test_gen_from_binding_to_core_single_string_field() {
2224        let typ = make_type("S", "c::S", vec![make_field("value", TypeRef::String)]);
2225        let result = gen_from_binding_to_core(&typ, "c");
2226        assert!(result.contains("value: val.value"));
2227    }
2228
2229    #[test]
2230    fn test_gen_from_core_to_binding_single_optional_named_field() {
2231        let field = make_opt_field("inner", TypeRef::Named("Inner".into()));
2232        let typ = make_type("Wrapper", "c::Wrapper", vec![field]);
2233        let result = gen_from_core_to_binding(&typ, "c", &no_opaques());
2234        assert!(result.contains("inner: val.inner.map(Into::into)"));
2235    }
2236
2237    // -----------------------------------------------------------------------
2238    // binding_to_core_match_arm_ext_cfg — config-aware match arms
2239    // -----------------------------------------------------------------------
2240
2241    #[test]
2242    fn test_binding_to_core_match_arm_ext_cfg_unit_variant() {
2243        let config = ConversionConfig::default();
2244        let result = helpers::binding_to_core_match_arm_ext_cfg("MyEnum", "Foo", &[], false, &config);
2245        assert_eq!(result, "MyEnum::Foo => Self::Foo,");
2246    }
2247
2248    #[test]
2249    fn test_binding_to_core_match_arm_ext_cfg_no_binding_data_named_fields() {
2250        let config = ConversionConfig::default();
2251        let fields = vec![make_field("value", TypeRef::String)];
2252        let result = helpers::binding_to_core_match_arm_ext_cfg("MyEnum", "Bar", &fields, false, &config);
2253        assert!(result.contains("value: Default::default()"));
2254    }
2255
2256    #[test]
2257    fn test_binding_to_core_match_arm_ext_cfg_no_binding_data_tuple_fields() {
2258        let config = ConversionConfig::default();
2259        let fields = vec![make_field("_0", TypeRef::String)];
2260        let result = helpers::binding_to_core_match_arm_ext_cfg("MyEnum", "Bar", &fields, false, &config);
2261        assert!(result.contains("Default::default()"));
2262        assert!(result.contains("Self::Bar("));
2263    }
2264
2265    #[test]
2266    fn test_binding_to_core_match_arm_ext_cfg_with_binding_data_named() {
2267        let config = ConversionConfig::default();
2268        let fields = vec![make_field("value", TypeRef::Named("Inner".into()))];
2269        let result = helpers::binding_to_core_match_arm_ext_cfg("MyEnum", "Bar", &fields, true, &config);
2270        assert!(result.contains("value: value.into()"));
2271    }
2272
2273    #[test]
2274    fn test_binding_to_core_match_arm_ext_cfg_with_binding_data_tuple() {
2275        let config = ConversionConfig::default();
2276        let fields = vec![make_field("_0", TypeRef::Named("Inner".into()))];
2277        let result = helpers::binding_to_core_match_arm_ext_cfg("MyEnum", "Bar", &fields, true, &config);
2278        assert!(result.contains("_0.into()"));
2279    }
2280
2281    #[test]
2282    fn test_binding_to_core_match_arm_ext_cfg_cast_u64_field() {
2283        let config = ConversionConfig {
2284            cast_large_ints_to_i64: true,
2285            ..Default::default()
2286        };
2287        let fields = vec![make_field("count", TypeRef::Primitive(PrimitiveType::U64))];
2288        let result = helpers::binding_to_core_match_arm_ext_cfg("MyEnum", "Bar", &fields, true, &config);
2289        assert!(result.contains("as u64"));
2290    }
2291
2292    // -----------------------------------------------------------------------
2293    // core_to_binding_match_arm_ext_cfg — config-aware match arms
2294    // -----------------------------------------------------------------------
2295
2296    #[test]
2297    fn test_core_to_binding_match_arm_ext_cfg_unit_variant() {
2298        let config = ConversionConfig::default();
2299        let result = helpers::core_to_binding_match_arm_ext_cfg("CoreEnum", "Foo", &[], false, &config);
2300        assert_eq!(result, "CoreEnum::Foo => Self::Foo,");
2301    }
2302
2303    #[test]
2304    fn test_core_to_binding_match_arm_ext_cfg_no_binding_data_named() {
2305        let config = ConversionConfig::default();
2306        let fields = vec![make_field("x", TypeRef::Primitive(PrimitiveType::I32))];
2307        let result = helpers::core_to_binding_match_arm_ext_cfg("CoreEnum", "Foo", &fields, false, &config);
2308        assert!(result.contains("{ .. }"));
2309        assert!(result.contains("Self::Foo"));
2310    }
2311
2312    #[test]
2313    fn test_core_to_binding_match_arm_ext_cfg_no_binding_data_tuple() {
2314        let config = ConversionConfig::default();
2315        let fields = vec![make_field("_0", TypeRef::Primitive(PrimitiveType::I32))];
2316        let result = helpers::core_to_binding_match_arm_ext_cfg("CoreEnum", "Foo", &fields, false, &config);
2317        assert!(result.contains("(..)"));
2318        assert!(result.contains("Self::Foo"));
2319    }
2320
2321    #[test]
2322    fn test_core_to_binding_match_arm_ext_cfg_with_binding_data_named_fields() {
2323        let config = ConversionConfig::default();
2324        let fields = vec![make_field("value", TypeRef::Named("Inner".into()))];
2325        let result = helpers::core_to_binding_match_arm_ext_cfg("CoreEnum", "Foo", &fields, true, &config);
2326        assert!(result.contains("value: value.into()"));
2327    }
2328
2329    #[test]
2330    fn test_core_to_binding_match_arm_ext_cfg_with_binding_data_tuple() {
2331        let config = ConversionConfig::default();
2332        let fields = vec![make_field("_0", TypeRef::Named("Inner".into()))];
2333        let result = helpers::core_to_binding_match_arm_ext_cfg("CoreEnum", "Foo", &fields, true, &config);
2334        assert!(result.contains("_0: _0.into()"));
2335    }
2336
2337    #[test]
2338    fn test_core_to_binding_match_arm_ext_cfg_cast_u64_field() {
2339        let config = ConversionConfig {
2340            cast_large_ints_to_i64: true,
2341            ..Default::default()
2342        };
2343        let fields = vec![make_field("count", TypeRef::Primitive(PrimitiveType::U64))];
2344        let result = helpers::core_to_binding_match_arm_ext_cfg("CoreEnum", "Bar", &fields, true, &config);
2345        assert!(result.contains("as i64"));
2346    }
2347
2348    // -----------------------------------------------------------------------
2349    // core_to_binding_match_arm_ext with binding_has_data=true
2350    // -----------------------------------------------------------------------
2351
2352    #[test]
2353    fn test_core_to_binding_match_arm_ext_binding_has_data_named_fields() {
2354        let fields = vec![make_field("value", TypeRef::Named("Inner".into()))];
2355        let result = helpers::core_to_binding_match_arm_ext("CoreEnum", "Bar", &fields, true);
2356        assert!(result.contains("value: value.into()"));
2357    }
2358
2359    #[test]
2360    fn test_core_to_binding_match_arm_ext_binding_has_data_tuple_fields() {
2361        let fields = vec![make_field("_0", TypeRef::Named("Inner".into()))];
2362        let result = helpers::core_to_binding_match_arm_ext("CoreEnum", "Bar", &fields, true);
2363        assert!(result.contains("_0: _0.into()"));
2364    }
2365
2366    #[test]
2367    fn test_core_to_binding_match_arm_ext_binding_has_data_plain_field() {
2368        let fields = vec![make_field("x", TypeRef::Primitive(PrimitiveType::I32))];
2369        let result = helpers::core_to_binding_match_arm_ext("CoreEnum", "Bar", &fields, true);
2370        assert!(result.contains("x: x"));
2371    }
2372
2373    #[test]
2374    fn test_core_to_binding_match_arm_ext_binding_has_data_sanitized_field() {
2375        let mut field = make_field("complex", TypeRef::String);
2376        field.sanitized = true;
2377        let result = helpers::core_to_binding_match_arm_ext("CoreEnum", "Bar", &[field], true);
2378        assert!(result.contains("serde_json::to_string("));
2379    }
2380
2381    // -----------------------------------------------------------------------
2382    // input_type_names — method params and transitive closure
2383    // -----------------------------------------------------------------------
2384
2385    #[test]
2386    fn test_input_type_names_from_method_params() {
2387        let surface = ApiSurface {
2388            crate_name: "my_crate".into(),
2389            version: "1.0.0".into(),
2390            types: vec![TypeDef {
2391                name: "Client".into(),
2392                rust_path: "my_crate::Client".into(),
2393                original_rust_path: String::new(),
2394                fields: vec![],
2395                methods: vec![MethodDef {
2396                    name: "process".into(),
2397                    params: vec![ParamDef {
2398                        name: "config".into(),
2399                        ty: TypeRef::Named("Config".into()),
2400                        optional: false,
2401                        default: None,
2402                        sanitized: false,
2403                        typed_default: None,
2404                        is_ref: false,
2405                        is_mut: false,
2406                        newtype_wrapper: None,
2407                        original_type: None,
2408                    }],
2409                    return_type: TypeRef::Unit,
2410                    is_async: false,
2411                    is_static: false,
2412                    error_type: None,
2413                    doc: String::new(),
2414                    receiver: None,
2415                    sanitized: false,
2416                    trait_source: None,
2417                    returns_ref: false,
2418                    returns_cow: false,
2419                    return_newtype_wrapper: None,
2420                    has_default_impl: false,
2421                }],
2422                is_opaque: false,
2423                is_clone: true,
2424                is_trait: false,
2425                has_default: false,
2426                has_stripped_cfg_fields: false,
2427                is_return_type: false,
2428                serde_rename_all: None,
2429                has_serde: false,
2430                super_traits: vec![],
2431                doc: String::new(),
2432                cfg: None,
2433            }],
2434            functions: vec![],
2435            enums: vec![],
2436            errors: vec![],
2437        };
2438        let names = input_type_names(&surface);
2439        assert!(names.contains("Config"));
2440    }
2441
2442    #[test]
2443    fn test_input_type_names_transitive_closure() {
2444        // Config has a field of type Backend — Backend should also be in input_type_names
2445        let config_type = make_type(
2446            "Config",
2447            "c::Config",
2448            vec![make_field("backend", TypeRef::Named("Backend".into()))],
2449        );
2450        let surface = ApiSurface {
2451            crate_name: "c".into(),
2452            version: "1.0".into(),
2453            types: vec![config_type],
2454            functions: vec![FunctionDef {
2455                name: "run".into(),
2456                rust_path: "c::run".into(),
2457                original_rust_path: String::new(),
2458                params: vec![ParamDef {
2459                    name: "config".into(),
2460                    ty: TypeRef::Named("Config".into()),
2461                    optional: false,
2462                    default: None,
2463                    sanitized: false,
2464                    typed_default: None,
2465                    is_ref: false,
2466                    is_mut: false,
2467                    newtype_wrapper: None,
2468                    original_type: None,
2469                }],
2470                return_type: TypeRef::Unit,
2471                is_async: false,
2472                error_type: None,
2473                doc: String::new(),
2474                cfg: None,
2475                sanitized: false,
2476                returns_ref: false,
2477                returns_cow: false,
2478                return_newtype_wrapper: None,
2479            }],
2480            enums: vec![],
2481            errors: vec![],
2482        };
2483        let names = input_type_names(&surface);
2484        assert!(names.contains("Config"));
2485        assert!(names.contains("Backend"));
2486    }
2487
2488    // -----------------------------------------------------------------------
2489    // convertible_types — sanitized fields with/without has_default
2490    // -----------------------------------------------------------------------
2491
2492    #[test]
2493    fn test_convertible_types_sanitized_field_with_has_default() {
2494        let mut field = make_field("complex", TypeRef::String);
2495        field.sanitized = true;
2496        let mut typ = make_type("Config", "c::Config", vec![field]);
2497        typ.has_default = true;
2498        let surface = ApiSurface {
2499            crate_name: "c".into(),
2500            version: "1.0".into(),
2501            types: vec![typ],
2502            functions: vec![],
2503            enums: vec![],
2504            errors: vec![],
2505        };
2506        // String has Default::default() — convertible
2507        let result = convertible_types(&surface);
2508        assert!(result.contains("Config"));
2509    }
2510
2511    #[test]
2512    fn test_convertible_types_opaque_type_excluded() {
2513        let mut typ = make_type("Client", "c::Client", vec![]);
2514        typ.is_opaque = true;
2515        let surface = ApiSurface {
2516            crate_name: "c".into(),
2517            version: "1.0".into(),
2518            types: vec![typ],
2519            functions: vec![],
2520            enums: vec![],
2521            errors: vec![],
2522        };
2523        // Opaque types are not in the candidate set initially
2524        let result = convertible_types(&surface);
2525        assert!(!result.contains("Client"));
2526    }
2527
2528    #[test]
2529    fn test_convertible_types_type_with_named_field_in_surface() {
2530        // Both Config (with Backend field) and Backend present — both convertible
2531        let config_field = make_field("backend", TypeRef::Named("Backend".into()));
2532        let config = make_type("Config", "c::Config", vec![config_field]);
2533        let backend = make_type("Backend", "c::Backend", vec![]);
2534        let surface = ApiSurface {
2535            crate_name: "c".into(),
2536            version: "1.0".into(),
2537            types: vec![config, backend],
2538            functions: vec![],
2539            enums: vec![],
2540            errors: vec![],
2541        };
2542        let result = convertible_types(&surface);
2543        assert!(result.contains("Config"));
2544        assert!(result.contains("Backend"));
2545    }
2546
2547    // -----------------------------------------------------------------------
2548    // core_enum_path edge cases
2549    // -----------------------------------------------------------------------
2550
2551    #[test]
2552    fn test_core_enum_path_with_hyphen_normalization() {
2553        let e = make_enum("Status", "my-crate::Status", &[]);
2554        assert_eq!(core_enum_path(&e, "my_crate"), "my_crate::Status");
2555    }
2556
2557    #[test]
2558    fn test_core_enum_path_already_starts_with_core_import() {
2559        // When path already starts with core_import, use verbatim
2560        let e = make_enum("Mode", "my_crate::inner::Mode", &[]);
2561        assert_eq!(core_enum_path(&e, "my_crate"), "my_crate::inner::Mode");
2562    }
2563
2564    // -----------------------------------------------------------------------
2565    // needs_i64_cast / core_prim_str / binding_prim_str (helper coverage)
2566    // -----------------------------------------------------------------------
2567
2568    #[test]
2569    fn test_needs_i64_cast_true_for_large_ints() {
2570        use super::helpers::*;
2571        assert!(needs_i64_cast(&PrimitiveType::U64));
2572        assert!(needs_i64_cast(&PrimitiveType::Usize));
2573        assert!(needs_i64_cast(&PrimitiveType::Isize));
2574    }
2575
2576    #[test]
2577    fn test_needs_i64_cast_false_for_small_ints() {
2578        use super::helpers::*;
2579        assert!(!needs_i64_cast(&PrimitiveType::I32));
2580        assert!(!needs_i64_cast(&PrimitiveType::U32));
2581        assert!(!needs_i64_cast(&PrimitiveType::F64));
2582    }
2583
2584    #[test]
2585    fn test_core_prim_str_all_variants() {
2586        use super::helpers::core_prim_str;
2587        assert_eq!(core_prim_str(&PrimitiveType::U64), "u64");
2588        assert_eq!(core_prim_str(&PrimitiveType::Usize), "usize");
2589        assert_eq!(core_prim_str(&PrimitiveType::Isize), "isize");
2590        assert_eq!(core_prim_str(&PrimitiveType::F32), "f32");
2591        assert_eq!(core_prim_str(&PrimitiveType::Bool), "bool");
2592        assert_eq!(core_prim_str(&PrimitiveType::U8), "u8");
2593        assert_eq!(core_prim_str(&PrimitiveType::U16), "u16");
2594        assert_eq!(core_prim_str(&PrimitiveType::U32), "u32");
2595        assert_eq!(core_prim_str(&PrimitiveType::I8), "i8");
2596        assert_eq!(core_prim_str(&PrimitiveType::I16), "i16");
2597        assert_eq!(core_prim_str(&PrimitiveType::I32), "i32");
2598        assert_eq!(core_prim_str(&PrimitiveType::I64), "i64");
2599        assert_eq!(core_prim_str(&PrimitiveType::F64), "f64");
2600    }
2601
2602    #[test]
2603    fn test_binding_prim_str_large_ints_map_to_i64() {
2604        use super::helpers::binding_prim_str;
2605        assert_eq!(binding_prim_str(&PrimitiveType::U64), "i64");
2606        assert_eq!(binding_prim_str(&PrimitiveType::Usize), "i64");
2607        assert_eq!(binding_prim_str(&PrimitiveType::Isize), "i64");
2608    }
2609
2610    #[test]
2611    fn test_binding_prim_str_small_ints_map_to_i32() {
2612        use super::helpers::binding_prim_str;
2613        assert_eq!(binding_prim_str(&PrimitiveType::U8), "i32");
2614        assert_eq!(binding_prim_str(&PrimitiveType::U16), "i32");
2615        assert_eq!(binding_prim_str(&PrimitiveType::U32), "i32");
2616        assert_eq!(binding_prim_str(&PrimitiveType::I8), "i32");
2617        assert_eq!(binding_prim_str(&PrimitiveType::I16), "i32");
2618        assert_eq!(binding_prim_str(&PrimitiveType::I32), "i32");
2619    }
2620
2621    #[test]
2622    fn test_binding_prim_str_float_and_i64() {
2623        use super::helpers::binding_prim_str;
2624        assert_eq!(binding_prim_str(&PrimitiveType::F32), "f64");
2625        assert_eq!(binding_prim_str(&PrimitiveType::F64), "f64");
2626        assert_eq!(binding_prim_str(&PrimitiveType::I64), "i64");
2627        assert_eq!(binding_prim_str(&PrimitiveType::Bool), "bool");
2628    }
2629}