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    /// Set of opaque type names in the binding layer.
55    /// When a field has `CoreWrapper::Arc` and its type is an opaque Named type,
56    /// the binding wrapper holds `inner: Arc<CoreT>` and the conversion must extract
57    /// `.inner` directly instead of calling `.into()` + wrapping in `Arc::new`.
58    pub opaque_types: Option<&'a AHashSet<String>>,
59}
60
61// Re-export all public items so callers continue to use `conversions::foo`.
62pub use binding_to_core::{
63    field_conversion_to_core, field_conversion_to_core_cfg, gen_from_binding_to_core, gen_from_binding_to_core_cfg,
64};
65pub use core_to_binding::{
66    field_conversion_from_core, field_conversion_from_core_cfg, gen_from_core_to_binding, gen_from_core_to_binding_cfg,
67};
68pub use enums::{
69    gen_enum_from_binding_to_core, gen_enum_from_binding_to_core_cfg, gen_enum_from_core_to_binding,
70    gen_enum_from_core_to_binding_cfg,
71};
72pub use helpers::{
73    binding_to_core_match_arm, build_type_path_map, can_generate_conversion, can_generate_enum_conversion,
74    can_generate_enum_conversion_from_core, convertible_types, core_enum_path, core_to_binding_convertible_types,
75    core_to_binding_match_arm, core_type_path, field_references_excluded_type, has_sanitized_fields, input_type_names,
76    is_tuple_variant, resolve_named_path,
77};
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use alef_core::ir::*;
83
84    fn simple_type() -> TypeDef {
85        TypeDef {
86            name: "Config".to_string(),
87            rust_path: "my_crate::Config".to_string(),
88            original_rust_path: String::new(),
89            fields: vec![
90                FieldDef {
91                    name: "name".into(),
92                    ty: TypeRef::String,
93                    optional: false,
94                    default: None,
95                    doc: String::new(),
96                    sanitized: false,
97                    is_boxed: false,
98                    type_rust_path: None,
99                    cfg: None,
100                    typed_default: None,
101                    core_wrapper: CoreWrapper::None,
102                    vec_inner_core_wrapper: CoreWrapper::None,
103                    newtype_wrapper: None,
104                },
105                FieldDef {
106                    name: "timeout".into(),
107                    ty: TypeRef::Primitive(PrimitiveType::U64),
108                    optional: true,
109                    default: None,
110                    doc: String::new(),
111                    sanitized: false,
112                    is_boxed: false,
113                    type_rust_path: None,
114                    cfg: None,
115                    typed_default: None,
116                    core_wrapper: CoreWrapper::None,
117                    vec_inner_core_wrapper: CoreWrapper::None,
118                    newtype_wrapper: None,
119                },
120                FieldDef {
121                    name: "backend".into(),
122                    ty: TypeRef::Named("Backend".into()),
123                    optional: true,
124                    default: None,
125                    doc: String::new(),
126                    sanitized: false,
127                    is_boxed: false,
128                    type_rust_path: None,
129                    cfg: None,
130                    typed_default: None,
131                    core_wrapper: CoreWrapper::None,
132                    vec_inner_core_wrapper: CoreWrapper::None,
133                    newtype_wrapper: None,
134                },
135            ],
136            methods: vec![],
137            is_opaque: false,
138            is_clone: true,
139            is_trait: false,
140            has_default: false,
141            has_stripped_cfg_fields: false,
142            is_return_type: false,
143            serde_rename_all: None,
144            has_serde: false,
145            super_traits: vec![],
146            doc: String::new(),
147            cfg: None,
148        }
149    }
150
151    fn simple_enum() -> EnumDef {
152        EnumDef {
153            name: "Backend".to_string(),
154            rust_path: "my_crate::Backend".to_string(),
155            original_rust_path: String::new(),
156            variants: vec![
157                EnumVariant {
158                    name: "Cpu".into(),
159                    fields: vec![],
160                    doc: String::new(),
161                    is_default: false,
162                    serde_rename: None,
163                },
164                EnumVariant {
165                    name: "Gpu".into(),
166                    fields: vec![],
167                    doc: String::new(),
168                    is_default: false,
169                    serde_rename: None,
170                },
171            ],
172            doc: String::new(),
173            cfg: None,
174            serde_tag: None,
175            serde_rename_all: None,
176        }
177    }
178
179    #[test]
180    fn test_from_binding_to_core() {
181        let typ = simple_type();
182        let result = gen_from_binding_to_core(&typ, "my_crate");
183        assert!(result.contains("impl From<Config> for my_crate::Config"));
184        assert!(result.contains("name: val.name"));
185        assert!(result.contains("timeout: val.timeout"));
186        assert!(result.contains("backend: val.backend.map(Into::into)"));
187    }
188
189    #[test]
190    fn test_from_core_to_binding() {
191        let typ = simple_type();
192        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
193        assert!(result.contains("impl From<my_crate::Config> for Config"));
194    }
195
196    #[test]
197    fn test_enum_from_binding_to_core() {
198        let enum_def = simple_enum();
199        let result = gen_enum_from_binding_to_core(&enum_def, "my_crate");
200        assert!(result.contains("impl From<Backend> for my_crate::Backend"));
201        assert!(result.contains("Backend::Cpu => Self::Cpu"));
202        assert!(result.contains("Backend::Gpu => Self::Gpu"));
203    }
204
205    #[test]
206    fn test_enum_from_core_to_binding() {
207        let enum_def = simple_enum();
208        let result = gen_enum_from_core_to_binding(&enum_def, "my_crate");
209        assert!(result.contains("impl From<my_crate::Backend> for Backend"));
210        assert!(result.contains("my_crate::Backend::Cpu => Self::Cpu"));
211        assert!(result.contains("my_crate::Backend::Gpu => Self::Gpu"));
212    }
213
214    #[test]
215    fn test_from_binding_to_core_with_cfg_gated_field() {
216        // Create a type with a cfg-gated field
217        let mut typ = simple_type();
218        typ.has_stripped_cfg_fields = true;
219        typ.fields.push(FieldDef {
220            name: "layout".into(),
221            ty: TypeRef::String,
222            optional: false,
223            default: None,
224            doc: String::new(),
225            sanitized: false,
226            is_boxed: false,
227            type_rust_path: None,
228            cfg: Some("feature = \"layout-detection\"".into()),
229            typed_default: None,
230            core_wrapper: CoreWrapper::None,
231            vec_inner_core_wrapper: CoreWrapper::None,
232            newtype_wrapper: None,
233        });
234
235        let result = gen_from_binding_to_core(&typ, "my_crate");
236
237        // The impl should exist
238        assert!(result.contains("impl From<Config> for my_crate::Config"));
239        // Regular fields should be present
240        assert!(result.contains("name: val.name"));
241        assert!(result.contains("timeout: val.timeout"));
242        // cfg-gated field should NOT be accessed from val (it doesn't exist in binding struct)
243        assert!(!result.contains("layout: val.layout"));
244        // But ..Default::default() should be present to fill cfg-gated fields
245        assert!(result.contains("..Default::default()"));
246    }
247
248    #[test]
249    fn test_from_core_to_binding_with_cfg_gated_field() {
250        // Create a type with a cfg-gated field
251        let mut typ = simple_type();
252        typ.fields.push(FieldDef {
253            name: "layout".into(),
254            ty: TypeRef::String,
255            optional: false,
256            default: None,
257            doc: String::new(),
258            sanitized: false,
259            is_boxed: false,
260            type_rust_path: None,
261            cfg: Some("feature = \"layout-detection\"".into()),
262            typed_default: None,
263            core_wrapper: CoreWrapper::None,
264            vec_inner_core_wrapper: CoreWrapper::None,
265            newtype_wrapper: None,
266        });
267
268        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
269
270        // The impl should exist
271        assert!(result.contains("impl From<my_crate::Config> for Config"));
272        // Regular fields should be present
273        assert!(result.contains("name: val.name"));
274        // cfg-gated field should NOT be in the struct literal
275        assert!(!result.contains("layout:"));
276    }
277}