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    /// When true, all Map(K, V) fields are stored as a plain `String` in the binding layer.
55    /// Core→binding uses `format!("{:?}", val.field)`, binding→core uses `Default::default()` (lossy).
56    /// Used by Rustler (Elixir NIFs) where `HashMap` cannot cross the NIF boundary directly.
57    pub map_as_string: bool,
58    /// Set of opaque type names in the binding layer.
59    /// When a field has `CoreWrapper::Arc` and its type is an opaque Named type,
60    /// the binding wrapper holds `inner: Arc<CoreT>` and the conversion must extract
61    /// `.inner` directly instead of calling `.into()` + wrapping in `Arc::new`.
62    pub opaque_types: Option<&'a AHashSet<String>>,
63    /// When `core_crate_override` is set for a language, the IR's `rust_path` values
64    /// still contain the original source crate prefix (e.g. `spikard_core::Method`).
65    /// This field remaps those paths: `(original_crate_name, override_crate_name)`.
66    /// When set, any `rust_path` whose leading crate segment equals `original_crate_name`
67    /// is rewritten to use `override_crate_name` instead.
68    /// Example: `Some(("spikard_core", "spikard_http"))` rewrites
69    /// `spikard_core::Method` → `spikard_http::Method`.
70    pub source_crate_remaps: &'a [(&'a str, &'a str)],
71    /// Per-field binding name overrides.  Key is `"TypeName.field_name"` (using the original
72    /// IR field name); value is the binding struct's actual Rust field name (e.g. `"class_"`).
73    /// Used when a field name is a reserved keyword in the target language and must be escaped
74    /// in the binding struct (e.g. `class` → `class_`).
75    ///
76    /// When present, `val.<binding_name>` is used for binding-side access and the original
77    /// `field_name` is used for core-side access (struct literal and assignment targets).
78    pub binding_field_renames: Option<&'a std::collections::HashMap<String, String>>,
79}
80
81impl<'a> ConversionConfig<'a> {
82    /// Look up the binding struct field name for a given type and IR field name.
83    ///
84    /// Returns the escaped name (e.g. `"class_"`) when the field was renamed due to a
85    /// reserved keyword conflict, or the original `field_name` when no rename applies.
86    pub fn binding_field_name<'b>(&self, type_name: &str, field_name: &'b str) -> &'b str
87    where
88        'a: 'b,
89    {
90        // &'b str: we return either the original (which has lifetime 'b from the parameter)
91        // or a &str from the HashMap (which would have lifetime 'a). Since 'a: 'b we can
92        // return either. But Rust's lifetime inference won't let us return `&'a str` from a
93        // `&'b str` parameter without unsafe. Use a helper that returns an owned String instead.
94        let _ = type_name;
95        field_name
96    }
97
98    /// Like `binding_field_name` but returns an owned `String`, suitable for use in
99    /// format strings and string interpolation.
100    pub fn binding_field_name_owned(&self, type_name: &str, field_name: &str) -> String {
101        if let Some(map) = self.binding_field_renames {
102            let key = format!("{type_name}.{field_name}");
103            if let Some(renamed) = map.get(&key) {
104                return renamed.clone();
105            }
106        }
107        field_name.to_string()
108    }
109}
110
111// Re-export all public items so callers continue to use `conversions::foo`.
112pub use binding_to_core::{
113    field_conversion_to_core, field_conversion_to_core_cfg, gen_from_binding_to_core, gen_from_binding_to_core_cfg,
114};
115pub use core_to_binding::{
116    field_conversion_from_core, field_conversion_from_core_cfg, gen_from_core_to_binding, gen_from_core_to_binding_cfg,
117};
118pub use enums::{
119    gen_enum_from_binding_to_core, gen_enum_from_binding_to_core_cfg, gen_enum_from_core_to_binding,
120    gen_enum_from_core_to_binding_cfg,
121};
122pub use helpers::{
123    apply_crate_remaps, binding_to_core_match_arm, build_type_path_map, can_generate_conversion,
124    can_generate_enum_conversion, can_generate_enum_conversion_from_core, convertible_types, core_enum_path,
125    core_enum_path_remapped, core_to_binding_convertible_types, core_to_binding_match_arm, core_type_path,
126    core_type_path_remapped, field_references_excluded_type, has_sanitized_fields, input_type_names, is_tuple_variant,
127    resolve_named_path,
128};
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use alef_core::ir::*;
134
135    fn simple_type() -> TypeDef {
136        TypeDef {
137            name: "Config".to_string(),
138            rust_path: "my_crate::Config".to_string(),
139            original_rust_path: String::new(),
140            fields: vec![
141                FieldDef {
142                    name: "name".into(),
143                    ty: TypeRef::String,
144                    optional: false,
145                    default: None,
146                    doc: String::new(),
147                    sanitized: false,
148                    is_boxed: false,
149                    type_rust_path: None,
150                    cfg: None,
151                    typed_default: None,
152                    core_wrapper: CoreWrapper::None,
153                    vec_inner_core_wrapper: CoreWrapper::None,
154                    newtype_wrapper: None,
155                },
156                FieldDef {
157                    name: "timeout".into(),
158                    ty: TypeRef::Primitive(PrimitiveType::U64),
159                    optional: true,
160                    default: None,
161                    doc: String::new(),
162                    sanitized: false,
163                    is_boxed: false,
164                    type_rust_path: None,
165                    cfg: None,
166                    typed_default: None,
167                    core_wrapper: CoreWrapper::None,
168                    vec_inner_core_wrapper: CoreWrapper::None,
169                    newtype_wrapper: None,
170                },
171                FieldDef {
172                    name: "backend".into(),
173                    ty: TypeRef::Named("Backend".into()),
174                    optional: true,
175                    default: None,
176                    doc: String::new(),
177                    sanitized: false,
178                    is_boxed: false,
179                    type_rust_path: None,
180                    cfg: None,
181                    typed_default: None,
182                    core_wrapper: CoreWrapper::None,
183                    vec_inner_core_wrapper: CoreWrapper::None,
184                    newtype_wrapper: None,
185                },
186            ],
187            methods: vec![],
188            is_opaque: false,
189            is_clone: true,
190            is_copy: false,
191            is_trait: false,
192            has_default: false,
193            has_stripped_cfg_fields: false,
194            is_return_type: false,
195            serde_rename_all: None,
196            has_serde: false,
197            super_traits: vec![],
198            doc: String::new(),
199            cfg: None,
200        }
201    }
202
203    fn simple_enum() -> EnumDef {
204        EnumDef {
205            name: "Backend".to_string(),
206            rust_path: "my_crate::Backend".to_string(),
207            original_rust_path: String::new(),
208            variants: vec![
209                EnumVariant {
210                    name: "Cpu".into(),
211                    fields: vec![],
212                    is_tuple: false,
213                    doc: String::new(),
214                    is_default: false,
215                    serde_rename: None,
216                },
217                EnumVariant {
218                    name: "Gpu".into(),
219                    fields: vec![],
220                    is_tuple: false,
221                    doc: String::new(),
222                    is_default: false,
223                    serde_rename: None,
224                },
225            ],
226            doc: String::new(),
227            cfg: None,
228            is_copy: false,
229            has_serde: false,
230            serde_tag: None,
231            serde_rename_all: None,
232        }
233    }
234
235    #[test]
236    fn test_from_binding_to_core() {
237        let typ = simple_type();
238        let result = gen_from_binding_to_core(&typ, "my_crate");
239        assert!(result.contains("impl From<Config> for my_crate::Config"));
240        assert!(result.contains("name: val.name"));
241        assert!(result.contains("timeout: val.timeout"));
242        assert!(result.contains("backend: val.backend.map(Into::into)"));
243    }
244
245    #[test]
246    fn test_from_core_to_binding() {
247        let typ = simple_type();
248        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
249        assert!(result.contains("impl From<my_crate::Config> for Config"));
250    }
251
252    #[test]
253    fn test_enum_from_binding_to_core() {
254        let enum_def = simple_enum();
255        let result = gen_enum_from_binding_to_core(&enum_def, "my_crate");
256        assert!(result.contains("impl From<Backend> for my_crate::Backend"));
257        assert!(result.contains("Backend::Cpu => Self::Cpu"));
258        assert!(result.contains("Backend::Gpu => Self::Gpu"));
259    }
260
261    #[test]
262    fn test_enum_from_core_to_binding() {
263        let enum_def = simple_enum();
264        let result = gen_enum_from_core_to_binding(&enum_def, "my_crate");
265        assert!(result.contains("impl From<my_crate::Backend> for Backend"));
266        assert!(result.contains("my_crate::Backend::Cpu => Self::Cpu"));
267        assert!(result.contains("my_crate::Backend::Gpu => Self::Gpu"));
268    }
269
270    #[test]
271    fn test_from_binding_to_core_with_cfg_gated_field() {
272        // Create a type with a cfg-gated field
273        let mut typ = simple_type();
274        typ.has_stripped_cfg_fields = true;
275        typ.fields.push(FieldDef {
276            name: "layout".into(),
277            ty: TypeRef::String,
278            optional: false,
279            default: None,
280            doc: String::new(),
281            sanitized: false,
282            is_boxed: false,
283            type_rust_path: None,
284            cfg: Some("feature = \"layout-detection\"".into()),
285            typed_default: None,
286            core_wrapper: CoreWrapper::None,
287            vec_inner_core_wrapper: CoreWrapper::None,
288            newtype_wrapper: None,
289        });
290
291        let result = gen_from_binding_to_core(&typ, "my_crate");
292
293        // The impl should exist
294        assert!(result.contains("impl From<Config> for my_crate::Config"));
295        // Regular fields should be present
296        assert!(result.contains("name: val.name"));
297        assert!(result.contains("timeout: val.timeout"));
298        // cfg-gated field should NOT be accessed from val (it doesn't exist in binding struct)
299        assert!(!result.contains("layout: val.layout"));
300        // But ..Default::default() should be present to fill cfg-gated fields
301        assert!(result.contains("..Default::default()"));
302    }
303
304    #[test]
305    fn test_from_core_to_binding_with_cfg_gated_field() {
306        // Create a type with a cfg-gated field
307        let mut typ = simple_type();
308        typ.fields.push(FieldDef {
309            name: "layout".into(),
310            ty: TypeRef::String,
311            optional: false,
312            default: None,
313            doc: String::new(),
314            sanitized: false,
315            is_boxed: false,
316            type_rust_path: None,
317            cfg: Some("feature = \"layout-detection\"".into()),
318            typed_default: None,
319            core_wrapper: CoreWrapper::None,
320            vec_inner_core_wrapper: CoreWrapper::None,
321            newtype_wrapper: None,
322        });
323
324        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
325
326        // The impl should exist
327        assert!(result.contains("impl From<my_crate::Config> for Config"));
328        // Regular fields should be present
329        assert!(result.contains("name: val.name"));
330        // cfg-gated field should NOT be in the struct literal
331        assert!(!result.contains("layout:"));
332    }
333
334    #[test]
335    fn test_field_conversion_from_core_map_named_non_optional() {
336        // Map<K, Named> non-optional: each value needs .into() core→binding
337        let result = field_conversion_from_core(
338            "tags",
339            &TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Named("Tag".into()))),
340            false,
341            false,
342            &AHashSet::new(),
343        );
344        assert_eq!(
345            result,
346            "tags: val.tags.into_iter().map(|(k, v)| (k, v.into())).collect()"
347        );
348    }
349
350    #[test]
351    fn test_field_conversion_from_core_option_map_named() {
352        // Option<Map<K, Named>>: .map() wrapper + per-element .into()
353        let result = field_conversion_from_core(
354            "tags",
355            &TypeRef::Optional(Box::new(TypeRef::Map(
356                Box::new(TypeRef::String),
357                Box::new(TypeRef::Named("Tag".into())),
358            ))),
359            false,
360            false,
361            &AHashSet::new(),
362        );
363        assert_eq!(
364            result,
365            "tags: val.tags.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
366        );
367    }
368
369    #[test]
370    fn test_field_conversion_from_core_vec_named_non_optional() {
371        // Vec<Named> non-optional: each element needs .into() core→binding
372        let result = field_conversion_from_core(
373            "items",
374            &TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))),
375            false,
376            false,
377            &AHashSet::new(),
378        );
379        assert_eq!(result, "items: val.items.into_iter().map(Into::into).collect()");
380    }
381
382    #[test]
383    fn test_field_conversion_from_core_option_vec_named() {
384        // Option<Vec<Named>>: .map() wrapper + per-element .into()
385        let result = field_conversion_from_core(
386            "items",
387            &TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))))),
388            false,
389            false,
390            &AHashSet::new(),
391        );
392        assert_eq!(
393            result,
394            "items: val.items.map(|v| v.into_iter().map(Into::into).collect())"
395        );
396    }
397
398    #[test]
399    fn test_field_conversion_to_core_option_map_named_applies_per_value_into() {
400        // Bug A1 regression: Option<Map<K, Named>> must apply per-value .into() so that
401        // binding-side wrapper types (e.g. PyO3 / Magnus structs) are converted correctly.
402        let result = field_conversion_to_core(
403            "patterns",
404            &TypeRef::Map(
405                Box::new(TypeRef::String),
406                Box::new(TypeRef::Named("ExtractionPattern".into())),
407            ),
408            true,
409        );
410        assert!(
411            result.contains("m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()"),
412            "expected per-value v.into() in optional Map<Named> conversion, got: {result}"
413        );
414        assert_eq!(
415            result,
416            "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect())"
417        );
418    }
419
420    #[test]
421    fn test_gen_optionalized_field_to_core_ir_optional_map_named_preserves_option() {
422        // Bug A3 regression: when field_is_ir_optional=true, gen_optionalized_field_to_core must
423        // preserve the Option layer via .map(|m| …) instead of dropping it with unwrap_or_default().
424        use super::binding_to_core::gen_optionalized_field_to_core;
425        let config = ConversionConfig::default();
426        let result = gen_optionalized_field_to_core(
427            "patterns",
428            &TypeRef::Map(
429                Box::new(TypeRef::String),
430                Box::new(TypeRef::Named("ExtractionPattern".into())),
431            ),
432            &config,
433            true,
434        );
435        assert!(
436            result.contains("m.into_iter().map(|(k, v)| (k, v.into())).collect()"),
437            "expected per-value v.into() in ir-optional Map<Named> conversion, got: {result}"
438        );
439        assert_eq!(
440            result,
441            "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
442        );
443    }
444}