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. `mylib_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(("mylib_core", "mylib_http"))` rewrites
69    /// `mylib_core::Method` → `mylib_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    /// Field names that must always emit `Default::default()` in both binding→core and
80    /// core→binding `From` implementations, regardless of the field's IR type.
81    ///
82    /// Used by NAPI (and other backends) for bridge fields: the visitor field on
83    /// `ConversionOptions` is typed as `Object<'static>` in the binding struct but the
84    /// core type is an opaque handle.  The `From` impl must leave the field at `None`
85    /// (i.e. `Default::default()`) in both directions; the actual bridge setup is performed
86    /// by the hand-generated `gen_bridge_field_function` wrapper.
87    pub force_default_fields: &'a [&'a str],
88}
89
90impl<'a> ConversionConfig<'a> {
91    /// Look up the binding struct field name for a given type and IR field name.
92    ///
93    /// Returns the escaped name (e.g. `"class_"`) when the field was renamed due to a
94    /// reserved keyword conflict, or the original `field_name` when no rename applies.
95    pub fn binding_field_name<'b>(&self, type_name: &str, field_name: &'b str) -> &'b str
96    where
97        'a: 'b,
98    {
99        // &'b str: we return either the original (which has lifetime 'b from the parameter)
100        // or a &str from the HashMap (which would have lifetime 'a). Since 'a: 'b we can
101        // return either. But Rust's lifetime inference won't let us return `&'a str` from a
102        // `&'b str` parameter without unsafe. Use a helper that returns an owned String instead.
103        let _ = type_name;
104        field_name
105    }
106
107    /// Like `binding_field_name` but returns an owned `String`, suitable for use in
108    /// format strings and string interpolation.
109    pub fn binding_field_name_owned(&self, type_name: &str, field_name: &str) -> String {
110        if let Some(map) = self.binding_field_renames {
111            let key = format!("{type_name}.{field_name}");
112            if let Some(renamed) = map.get(&key) {
113                return renamed.clone();
114            }
115        }
116        field_name.to_string()
117    }
118}
119
120// Re-export all public items so callers continue to use `conversions::foo`.
121pub use binding_to_core::{
122    field_conversion_to_core, field_conversion_to_core_cfg, gen_from_binding_to_core, gen_from_binding_to_core_cfg,
123};
124pub use core_to_binding::{
125    field_conversion_from_core, field_conversion_from_core_cfg, gen_from_core_to_binding, gen_from_core_to_binding_cfg,
126};
127pub use enums::{
128    gen_enum_from_binding_to_core, gen_enum_from_binding_to_core_cfg, gen_enum_from_core_to_binding,
129    gen_enum_from_core_to_binding_cfg,
130};
131pub use helpers::{
132    apply_crate_remaps, binding_to_core_match_arm, build_type_path_map, can_generate_conversion,
133    can_generate_enum_conversion, can_generate_enum_conversion_from_core, convertible_types, core_enum_path,
134    core_enum_path_remapped, core_to_binding_convertible_types, core_to_binding_match_arm, core_type_path,
135    core_type_path_remapped, field_references_excluded_type, has_sanitized_fields, input_type_names, is_tuple_variant,
136    resolve_named_path,
137};
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use alef_core::ir::*;
143
144    fn simple_type() -> TypeDef {
145        TypeDef {
146            name: "Config".to_string(),
147            rust_path: "my_crate::Config".to_string(),
148            original_rust_path: String::new(),
149            fields: vec![
150                FieldDef {
151                    name: "name".into(),
152                    ty: TypeRef::String,
153                    optional: false,
154                    default: None,
155                    doc: String::new(),
156                    sanitized: false,
157                    is_boxed: false,
158                    type_rust_path: None,
159                    cfg: None,
160                    typed_default: None,
161                    core_wrapper: CoreWrapper::None,
162                    vec_inner_core_wrapper: CoreWrapper::None,
163                    newtype_wrapper: None,
164                },
165                FieldDef {
166                    name: "timeout".into(),
167                    ty: TypeRef::Primitive(PrimitiveType::U64),
168                    optional: true,
169                    default: None,
170                    doc: String::new(),
171                    sanitized: false,
172                    is_boxed: false,
173                    type_rust_path: None,
174                    cfg: None,
175                    typed_default: None,
176                    core_wrapper: CoreWrapper::None,
177                    vec_inner_core_wrapper: CoreWrapper::None,
178                    newtype_wrapper: None,
179                },
180                FieldDef {
181                    name: "backend".into(),
182                    ty: TypeRef::Named("Backend".into()),
183                    optional: true,
184                    default: None,
185                    doc: String::new(),
186                    sanitized: false,
187                    is_boxed: false,
188                    type_rust_path: None,
189                    cfg: None,
190                    typed_default: None,
191                    core_wrapper: CoreWrapper::None,
192                    vec_inner_core_wrapper: CoreWrapper::None,
193                    newtype_wrapper: None,
194                },
195            ],
196            methods: vec![],
197            is_opaque: false,
198            is_clone: true,
199            is_copy: false,
200            is_trait: false,
201            has_default: false,
202            has_stripped_cfg_fields: false,
203            is_return_type: false,
204            serde_rename_all: None,
205            has_serde: false,
206            super_traits: vec![],
207            doc: String::new(),
208            cfg: None,
209        }
210    }
211
212    fn simple_enum() -> EnumDef {
213        EnumDef {
214            name: "Backend".to_string(),
215            rust_path: "my_crate::Backend".to_string(),
216            original_rust_path: String::new(),
217            variants: vec![
218                EnumVariant {
219                    name: "Cpu".into(),
220                    fields: vec![],
221                    is_tuple: false,
222                    doc: String::new(),
223                    is_default: false,
224                    serde_rename: None,
225                },
226                EnumVariant {
227                    name: "Gpu".into(),
228                    fields: vec![],
229                    is_tuple: false,
230                    doc: String::new(),
231                    is_default: false,
232                    serde_rename: None,
233                },
234            ],
235            doc: String::new(),
236            cfg: None,
237            is_copy: false,
238            has_serde: false,
239            serde_tag: None,
240            serde_rename_all: None,
241        }
242    }
243
244    #[test]
245    fn test_from_binding_to_core() {
246        let typ = simple_type();
247        let result = gen_from_binding_to_core(&typ, "my_crate");
248        assert!(result.contains("impl From<Config> for my_crate::Config"));
249        assert!(result.contains("name: val.name"));
250        assert!(result.contains("timeout: val.timeout"));
251        assert!(result.contains("backend: val.backend.map(Into::into)"));
252    }
253
254    #[test]
255    fn test_from_core_to_binding() {
256        let typ = simple_type();
257        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
258        assert!(result.contains("impl From<my_crate::Config> for Config"));
259    }
260
261    #[test]
262    fn test_enum_from_binding_to_core() {
263        let enum_def = simple_enum();
264        let result = gen_enum_from_binding_to_core(&enum_def, "my_crate");
265        assert!(result.contains("impl From<Backend> for my_crate::Backend"));
266        assert!(result.contains("Backend::Cpu => Self::Cpu"));
267        assert!(result.contains("Backend::Gpu => Self::Gpu"));
268    }
269
270    #[test]
271    fn test_enum_from_core_to_binding() {
272        let enum_def = simple_enum();
273        let result = gen_enum_from_core_to_binding(&enum_def, "my_crate");
274        assert!(result.contains("impl From<my_crate::Backend> for Backend"));
275        assert!(result.contains("my_crate::Backend::Cpu => Self::Cpu"));
276        assert!(result.contains("my_crate::Backend::Gpu => Self::Gpu"));
277    }
278
279    #[test]
280    fn test_from_binding_to_core_with_cfg_gated_field() {
281        // Create a type with a cfg-gated field
282        let mut typ = simple_type();
283        typ.has_stripped_cfg_fields = true;
284        typ.fields.push(FieldDef {
285            name: "layout".into(),
286            ty: TypeRef::String,
287            optional: false,
288            default: None,
289            doc: String::new(),
290            sanitized: false,
291            is_boxed: false,
292            type_rust_path: None,
293            cfg: Some("feature = \"layout-detection\"".into()),
294            typed_default: None,
295            core_wrapper: CoreWrapper::None,
296            vec_inner_core_wrapper: CoreWrapper::None,
297            newtype_wrapper: None,
298        });
299
300        let result = gen_from_binding_to_core(&typ, "my_crate");
301
302        // The impl should exist
303        assert!(result.contains("impl From<Config> for my_crate::Config"));
304        // Regular fields should be present
305        assert!(result.contains("name: val.name"));
306        assert!(result.contains("timeout: val.timeout"));
307        // cfg-gated field should NOT be accessed from val (it doesn't exist in binding struct)
308        assert!(!result.contains("layout: val.layout"));
309        // But ..Default::default() should be present to fill cfg-gated fields
310        assert!(result.contains("..Default::default()"));
311    }
312
313    #[test]
314    fn test_from_core_to_binding_with_cfg_gated_field() {
315        // Create a type with a cfg-gated field
316        let mut typ = simple_type();
317        typ.fields.push(FieldDef {
318            name: "layout".into(),
319            ty: TypeRef::String,
320            optional: false,
321            default: None,
322            doc: String::new(),
323            sanitized: false,
324            is_boxed: false,
325            type_rust_path: None,
326            cfg: Some("feature = \"layout-detection\"".into()),
327            typed_default: None,
328            core_wrapper: CoreWrapper::None,
329            vec_inner_core_wrapper: CoreWrapper::None,
330            newtype_wrapper: None,
331        });
332
333        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
334
335        // The impl should exist
336        assert!(result.contains("impl From<my_crate::Config> for Config"));
337        // Regular fields should be present
338        assert!(result.contains("name: val.name"));
339        // cfg-gated field should NOT be in the struct literal
340        assert!(!result.contains("layout:"));
341    }
342
343    #[test]
344    fn test_field_conversion_from_core_map_named_non_optional() {
345        // Map<K, Named> non-optional: each value needs .into() core→binding
346        let result = field_conversion_from_core(
347            "tags",
348            &TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Named("Tag".into()))),
349            false,
350            false,
351            &AHashSet::new(),
352        );
353        assert_eq!(
354            result,
355            "tags: val.tags.into_iter().map(|(k, v)| (k, v.into())).collect()"
356        );
357    }
358
359    #[test]
360    fn test_field_conversion_from_core_option_map_named() {
361        // Option<Map<K, Named>>: .map() wrapper + per-element .into()
362        let result = field_conversion_from_core(
363            "tags",
364            &TypeRef::Optional(Box::new(TypeRef::Map(
365                Box::new(TypeRef::String),
366                Box::new(TypeRef::Named("Tag".into())),
367            ))),
368            false,
369            false,
370            &AHashSet::new(),
371        );
372        assert_eq!(
373            result,
374            "tags: val.tags.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
375        );
376    }
377
378    #[test]
379    fn test_field_conversion_from_core_vec_named_non_optional() {
380        // Vec<Named> non-optional: each element needs .into() core→binding
381        let result = field_conversion_from_core(
382            "items",
383            &TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))),
384            false,
385            false,
386            &AHashSet::new(),
387        );
388        assert_eq!(result, "items: val.items.into_iter().map(Into::into).collect()");
389    }
390
391    #[test]
392    fn test_field_conversion_from_core_option_vec_named() {
393        // Option<Vec<Named>>: .map() wrapper + per-element .into()
394        let result = field_conversion_from_core(
395            "items",
396            &TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))))),
397            false,
398            false,
399            &AHashSet::new(),
400        );
401        assert_eq!(
402            result,
403            "items: val.items.map(|v| v.into_iter().map(Into::into).collect())"
404        );
405    }
406
407    #[test]
408    fn test_field_conversion_to_core_option_map_named_applies_per_value_into() {
409        // Bug A1 regression: Option<Map<K, Named>> must apply per-value .into() so that
410        // binding-side wrapper types (e.g. PyO3 / Magnus structs) are converted correctly.
411        let result = field_conversion_to_core(
412            "patterns",
413            &TypeRef::Map(
414                Box::new(TypeRef::String),
415                Box::new(TypeRef::Named("ExtractionPattern".into())),
416            ),
417            true,
418        );
419        assert!(
420            result.contains("m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()"),
421            "expected per-value v.into() in optional Map<Named> conversion, got: {result}"
422        );
423        assert_eq!(
424            result,
425            "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect())"
426        );
427    }
428
429    #[test]
430    fn test_gen_optionalized_field_to_core_ir_optional_map_named_preserves_option() {
431        // Bug A3 regression: when field_is_ir_optional=true, gen_optionalized_field_to_core must
432        // preserve the Option layer via .map(|m| …) instead of dropping it with unwrap_or_default().
433        use super::binding_to_core::gen_optionalized_field_to_core;
434        let config = ConversionConfig::default();
435        let result = gen_optionalized_field_to_core(
436            "patterns",
437            &TypeRef::Map(
438                Box::new(TypeRef::String),
439                Box::new(TypeRef::Named("ExtractionPattern".into())),
440            ),
441            &config,
442            true,
443        );
444        assert!(
445            result.contains("m.into_iter().map(|(k, v)| (k, v.into())).collect()"),
446            "expected per-value v.into() in ir-optional Map<Named> conversion, got: {result}"
447        );
448        assert_eq!(
449            result,
450            "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
451        );
452    }
453
454    fn arc_field_type(field: FieldDef) -> TypeDef {
455        TypeDef {
456            name: "State".to_string(),
457            rust_path: "my_crate::State".to_string(),
458            original_rust_path: String::new(),
459            fields: vec![field],
460            methods: vec![],
461            is_opaque: false,
462            is_clone: true,
463            is_copy: false,
464            is_trait: false,
465            has_default: false,
466            has_stripped_cfg_fields: false,
467            is_return_type: false,
468            serde_rename_all: None,
469            has_serde: false,
470            super_traits: vec![],
471            doc: String::new(),
472            cfg: None,
473        }
474    }
475
476    fn arc_field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
477        FieldDef {
478            name: name.into(),
479            ty,
480            optional,
481            default: None,
482            doc: String::new(),
483            sanitized: false,
484            is_boxed: false,
485            type_rust_path: None,
486            cfg: None,
487            typed_default: None,
488            core_wrapper: CoreWrapper::Arc,
489            vec_inner_core_wrapper: CoreWrapper::None,
490            newtype_wrapper: None,
491        }
492    }
493
494    /// Regression: Option<Arc<serde_json::Value>> must not chain `(*v).clone().into()`
495    /// on top of `as_ref().map(ToString::to_string)`, which would emit invalid
496    /// `(*String).clone()` (str: !Clone).
497    #[test]
498    fn test_arc_json_option_field_no_double_chain() {
499        let typ = arc_field_type(arc_field("registered_spec", TypeRef::Json, true));
500        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
501        assert!(
502            result.contains("val.registered_spec.as_ref().map(ToString::to_string)"),
503            "expected as_ref().map(ToString::to_string) for Option<Arc<Value>>, got: {result}"
504        );
505        assert!(
506            !result.contains("map(ToString::to_string).map("),
507            "must not chain a second map() on top of ToString::to_string, got: {result}"
508        );
509    }
510
511    /// Non-optional Arc<Value>: `(*val.X).clone().to_string()` is valid (Value: Clone).
512    #[test]
513    fn test_arc_json_non_optional_field() {
514        let typ = arc_field_type(arc_field("spec", TypeRef::Json, false));
515        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
516        assert!(
517            result.contains("(*val.spec).clone().to_string()"),
518            "expected (*val.spec).clone().to_string() for Arc<Value>, got: {result}"
519        );
520    }
521
522    /// Option<Arc<String>>: simple passthrough → `.map(|v| (*v).clone().into())` is valid
523    /// (String: Clone). Verifies the simple_passthrough branch is preserved.
524    #[test]
525    fn test_arc_string_option_field_passthrough() {
526        let typ = arc_field_type(arc_field("label", TypeRef::String, true));
527        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
528        assert!(
529            result.contains("val.label.map(|v| (*v).clone().into())"),
530            "expected .map(|v| (*v).clone().into()) for Option<Arc<String>>, got: {result}"
531        );
532    }
533}