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    /// Per-field binding name overrides.  Key is `"TypeName.field_name"` (using the original
60    /// IR field name); value is the binding struct's actual Rust field name (e.g. `"class_"`).
61    /// Used when a field name is a reserved keyword in the target language and must be escaped
62    /// in the binding struct (e.g. `class` → `class_`).
63    ///
64    /// When present, `val.<binding_name>` is used for binding-side access and the original
65    /// `field_name` is used for core-side access (struct literal and assignment targets).
66    pub binding_field_renames: Option<&'a std::collections::HashMap<String, String>>,
67}
68
69impl<'a> ConversionConfig<'a> {
70    /// Look up the binding struct field name for a given type and IR field name.
71    ///
72    /// Returns the escaped name (e.g. `"class_"`) when the field was renamed due to a
73    /// reserved keyword conflict, or the original `field_name` when no rename applies.
74    pub fn binding_field_name<'b>(&self, type_name: &str, field_name: &'b str) -> &'b str
75    where
76        'a: 'b,
77    {
78        // &'b str: we return either the original (which has lifetime 'b from the parameter)
79        // or a &str from the HashMap (which would have lifetime 'a). Since 'a: 'b we can
80        // return either. But Rust's lifetime inference won't let us return `&'a str` from a
81        // `&'b str` parameter without unsafe. Use a helper that returns an owned String instead.
82        let _ = type_name;
83        field_name
84    }
85
86    /// Like `binding_field_name` but returns an owned `String`, suitable for use in
87    /// format strings and string interpolation.
88    pub fn binding_field_name_owned(&self, type_name: &str, field_name: &str) -> String {
89        if let Some(map) = self.binding_field_renames {
90            let key = format!("{type_name}.{field_name}");
91            if let Some(renamed) = map.get(&key) {
92                return renamed.clone();
93            }
94        }
95        field_name.to_string()
96    }
97}
98
99// Re-export all public items so callers continue to use `conversions::foo`.
100pub use binding_to_core::{
101    field_conversion_to_core, field_conversion_to_core_cfg, gen_from_binding_to_core, gen_from_binding_to_core_cfg,
102};
103pub use core_to_binding::{
104    field_conversion_from_core, field_conversion_from_core_cfg, gen_from_core_to_binding, gen_from_core_to_binding_cfg,
105};
106pub use enums::{
107    gen_enum_from_binding_to_core, gen_enum_from_binding_to_core_cfg, gen_enum_from_core_to_binding,
108    gen_enum_from_core_to_binding_cfg,
109};
110pub use helpers::{
111    binding_to_core_match_arm, build_type_path_map, can_generate_conversion, can_generate_enum_conversion,
112    can_generate_enum_conversion_from_core, convertible_types, core_enum_path, core_to_binding_convertible_types,
113    core_to_binding_match_arm, core_type_path, field_references_excluded_type, has_sanitized_fields, input_type_names,
114    is_tuple_variant, resolve_named_path,
115};
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use alef_core::ir::*;
121
122    fn simple_type() -> TypeDef {
123        TypeDef {
124            name: "Config".to_string(),
125            rust_path: "my_crate::Config".to_string(),
126            original_rust_path: String::new(),
127            fields: vec![
128                FieldDef {
129                    name: "name".into(),
130                    ty: TypeRef::String,
131                    optional: false,
132                    default: None,
133                    doc: String::new(),
134                    sanitized: false,
135                    is_boxed: false,
136                    type_rust_path: None,
137                    cfg: None,
138                    typed_default: None,
139                    core_wrapper: CoreWrapper::None,
140                    vec_inner_core_wrapper: CoreWrapper::None,
141                    newtype_wrapper: None,
142                },
143                FieldDef {
144                    name: "timeout".into(),
145                    ty: TypeRef::Primitive(PrimitiveType::U64),
146                    optional: true,
147                    default: None,
148                    doc: String::new(),
149                    sanitized: false,
150                    is_boxed: false,
151                    type_rust_path: None,
152                    cfg: None,
153                    typed_default: None,
154                    core_wrapper: CoreWrapper::None,
155                    vec_inner_core_wrapper: CoreWrapper::None,
156                    newtype_wrapper: None,
157                },
158                FieldDef {
159                    name: "backend".into(),
160                    ty: TypeRef::Named("Backend".into()),
161                    optional: true,
162                    default: None,
163                    doc: String::new(),
164                    sanitized: false,
165                    is_boxed: false,
166                    type_rust_path: None,
167                    cfg: None,
168                    typed_default: None,
169                    core_wrapper: CoreWrapper::None,
170                    vec_inner_core_wrapper: CoreWrapper::None,
171                    newtype_wrapper: None,
172                },
173            ],
174            methods: vec![],
175            is_opaque: false,
176            is_clone: true,
177            is_copy: false,
178            is_trait: false,
179            has_default: false,
180            has_stripped_cfg_fields: false,
181            is_return_type: false,
182            serde_rename_all: None,
183            has_serde: false,
184            super_traits: vec![],
185            doc: String::new(),
186            cfg: None,
187        }
188    }
189
190    fn simple_enum() -> EnumDef {
191        EnumDef {
192            name: "Backend".to_string(),
193            rust_path: "my_crate::Backend".to_string(),
194            original_rust_path: String::new(),
195            variants: vec![
196                EnumVariant {
197                    name: "Cpu".into(),
198                    fields: vec![],
199                    is_tuple: false,
200                    doc: String::new(),
201                    is_default: false,
202                    serde_rename: None,
203                },
204                EnumVariant {
205                    name: "Gpu".into(),
206                    fields: vec![],
207                    is_tuple: false,
208                    doc: String::new(),
209                    is_default: false,
210                    serde_rename: None,
211                },
212            ],
213            doc: String::new(),
214            cfg: None,
215            is_copy: false,
216            has_serde: false,
217            serde_tag: None,
218            serde_rename_all: None,
219        }
220    }
221
222    #[test]
223    fn test_from_binding_to_core() {
224        let typ = simple_type();
225        let result = gen_from_binding_to_core(&typ, "my_crate");
226        assert!(result.contains("impl From<Config> for my_crate::Config"));
227        assert!(result.contains("name: val.name"));
228        assert!(result.contains("timeout: val.timeout"));
229        assert!(result.contains("backend: val.backend.map(Into::into)"));
230    }
231
232    #[test]
233    fn test_from_core_to_binding() {
234        let typ = simple_type();
235        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
236        assert!(result.contains("impl From<my_crate::Config> for Config"));
237    }
238
239    #[test]
240    fn test_enum_from_binding_to_core() {
241        let enum_def = simple_enum();
242        let result = gen_enum_from_binding_to_core(&enum_def, "my_crate");
243        assert!(result.contains("impl From<Backend> for my_crate::Backend"));
244        assert!(result.contains("Backend::Cpu => Self::Cpu"));
245        assert!(result.contains("Backend::Gpu => Self::Gpu"));
246    }
247
248    #[test]
249    fn test_enum_from_core_to_binding() {
250        let enum_def = simple_enum();
251        let result = gen_enum_from_core_to_binding(&enum_def, "my_crate");
252        assert!(result.contains("impl From<my_crate::Backend> for Backend"));
253        assert!(result.contains("my_crate::Backend::Cpu => Self::Cpu"));
254        assert!(result.contains("my_crate::Backend::Gpu => Self::Gpu"));
255    }
256
257    #[test]
258    fn test_from_binding_to_core_with_cfg_gated_field() {
259        // Create a type with a cfg-gated field
260        let mut typ = simple_type();
261        typ.has_stripped_cfg_fields = true;
262        typ.fields.push(FieldDef {
263            name: "layout".into(),
264            ty: TypeRef::String,
265            optional: false,
266            default: None,
267            doc: String::new(),
268            sanitized: false,
269            is_boxed: false,
270            type_rust_path: None,
271            cfg: Some("feature = \"layout-detection\"".into()),
272            typed_default: None,
273            core_wrapper: CoreWrapper::None,
274            vec_inner_core_wrapper: CoreWrapper::None,
275            newtype_wrapper: None,
276        });
277
278        let result = gen_from_binding_to_core(&typ, "my_crate");
279
280        // The impl should exist
281        assert!(result.contains("impl From<Config> for my_crate::Config"));
282        // Regular fields should be present
283        assert!(result.contains("name: val.name"));
284        assert!(result.contains("timeout: val.timeout"));
285        // cfg-gated field should NOT be accessed from val (it doesn't exist in binding struct)
286        assert!(!result.contains("layout: val.layout"));
287        // But ..Default::default() should be present to fill cfg-gated fields
288        assert!(result.contains("..Default::default()"));
289    }
290
291    #[test]
292    fn test_from_core_to_binding_with_cfg_gated_field() {
293        // Create a type with a cfg-gated field
294        let mut typ = simple_type();
295        typ.fields.push(FieldDef {
296            name: "layout".into(),
297            ty: TypeRef::String,
298            optional: false,
299            default: None,
300            doc: String::new(),
301            sanitized: false,
302            is_boxed: false,
303            type_rust_path: None,
304            cfg: Some("feature = \"layout-detection\"".into()),
305            typed_default: None,
306            core_wrapper: CoreWrapper::None,
307            vec_inner_core_wrapper: CoreWrapper::None,
308            newtype_wrapper: None,
309        });
310
311        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
312
313        // The impl should exist
314        assert!(result.contains("impl From<my_crate::Config> for Config"));
315        // Regular fields should be present
316        assert!(result.contains("name: val.name"));
317        // cfg-gated field should NOT be in the struct literal
318        assert!(!result.contains("layout:"));
319    }
320}