Skip to main content

alef_codegen/generators/
structs.rs

1use crate::builder::StructBuilder;
2use crate::generators::RustBindingConfig;
3use crate::type_mapper::TypeMapper;
4use alef_core::ir::{TypeDef, TypeRef};
5use std::fmt::Write;
6
7/// Check if any two field names are similar enough to trigger clippy::similar_names.
8/// This detects patterns like "sub_symbol" and "sup_symbol" (differ by 1-2 chars).
9fn has_similar_names(names: &[&String]) -> bool {
10    for (i, &name1) in names.iter().enumerate() {
11        for &name2 in &names[i + 1..] {
12            // Simple heuristic: if names differ by <= 2 characters and have same length, flag it
13            if name1.len() == name2.len() && diff_count(name1, name2) <= 2 {
14                return true;
15            }
16        }
17    }
18    false
19}
20
21/// Count how many characters differ between two strings of equal length.
22fn diff_count(s1: &str, s2: &str) -> usize {
23    s1.chars().zip(s2.chars()).filter(|(c1, c2)| c1 != c2).count()
24}
25
26/// Generate a struct definition using the builder.
27pub fn gen_struct(typ: &TypeDef, mapper: &dyn TypeMapper, cfg: &RustBindingConfig) -> String {
28    let mut sb = StructBuilder::new(&typ.name);
29    for attr in cfg.struct_attrs {
30        sb.add_attr(attr);
31    }
32
33    // Check if struct has similar field names (e.g., sub_symbol and sup_symbol)
34    let field_names: Vec<_> = typ.fields.iter().filter(|f| f.cfg.is_none()).map(|f| &f.name).collect();
35    if has_similar_names(&field_names) {
36        sb.add_attr("allow(clippy::similar_names)");
37    }
38
39    for d in cfg.struct_derives {
40        sb.add_derive(d);
41    }
42    // Types with has_default get #[derive(Default)] — all fields use Default::default()
43    // so the manual impl is unnecessary and triggers clippy::derivable_impls.
44    if typ.has_default {
45        sb.add_derive("Default");
46    }
47    if cfg.has_serde {
48        sb.add_derive("serde::Serialize");
49    }
50    for field in &typ.fields {
51        // Skip cfg-gated fields — they depend on features that may not be enabled
52        // for this binding crate. Including them would require the binding struct to
53        // handle conditional compilation which struct literal initializers can't express.
54        if field.cfg.is_some() {
55            continue;
56        }
57        // When option_duration_on_defaults is set, wrap non-optional Duration fields in
58        // Option<u64> for has_default types so the binding constructor can accept None
59        // and the From conversion falls back to the core type's Default.
60        let force_optional = cfg.option_duration_on_defaults
61            && typ.has_default
62            && !field.optional
63            && matches!(field.ty, TypeRef::Duration);
64        let ty = if field.optional || force_optional {
65            mapper.optional(&mapper.map_type(&field.ty))
66        } else {
67            mapper.map_type(&field.ty)
68        };
69        let attrs: Vec<String> = cfg.field_attrs.iter().map(|a| a.to_string()).collect();
70        sb.add_field_with_doc(&field.name, &ty, attrs, &field.doc);
71    }
72    sb.build()
73}
74
75/// Generate a `Default` impl for a non-opaque binding struct with `has_default`.
76/// All fields use their type's Default::default().
77/// Optional fields use None instead of Default::default().
78/// This enables the struct to be used with `unwrap_or_default()` in config constructors.
79pub fn gen_struct_default_impl(typ: &TypeDef, name_prefix: &str) -> String {
80    let full_name = format!("{}{}", name_prefix, typ.name);
81    let mut out = String::with_capacity(256);
82    writeln!(out, "impl Default for {} {{", full_name).ok();
83    writeln!(out, "    fn default() -> Self {{").ok();
84    writeln!(out, "        Self {{").ok();
85    for field in &typ.fields {
86        if field.cfg.is_some() {
87            continue;
88        }
89        let default_val = match &field.ty {
90            TypeRef::Optional(_) => "None".to_string(),
91            _ => "Default::default()".to_string(),
92        };
93        writeln!(out, "            {}: {},", field.name, default_val).ok();
94    }
95    writeln!(out, "        }}").ok();
96    writeln!(out, "    }}").ok();
97    write!(out, "}}").ok();
98    out
99}
100
101/// Generate an opaque wrapper struct with `inner: Arc<core::Type>`.
102/// For trait types, uses `Arc<dyn Type + Send + Sync>`.
103pub fn gen_opaque_struct(typ: &TypeDef, cfg: &RustBindingConfig) -> String {
104    let mut out = String::with_capacity(512);
105    if !cfg.struct_derives.is_empty() {
106        writeln!(out, "#[derive(Clone)]").ok();
107    }
108    for attr in cfg.struct_attrs {
109        writeln!(out, "#[{attr}]").ok();
110    }
111    writeln!(out, "pub struct {} {{", typ.name).ok();
112    let core_path = typ.rust_path.replace('-', "_");
113    if typ.is_trait {
114        writeln!(out, "    inner: Arc<dyn {core_path} + Send + Sync>,").ok();
115    } else {
116        writeln!(out, "    inner: Arc<{core_path}>,").ok();
117    }
118    write!(out, "}}").ok();
119    out
120}
121
122/// Generate an opaque wrapper struct with `inner: Arc<core::Type>` and a `Js` prefix.
123pub fn gen_opaque_struct_prefixed(typ: &TypeDef, cfg: &RustBindingConfig, prefix: &str) -> String {
124    let mut out = String::with_capacity(512);
125    if !cfg.struct_derives.is_empty() {
126        writeln!(out, "#[derive(Clone)]").ok();
127    }
128    for attr in cfg.struct_attrs {
129        writeln!(out, "#[{attr}]").ok();
130    }
131    let core_path = typ.rust_path.replace('-', "_");
132    writeln!(out, "pub struct {}{} {{", prefix, typ.name).ok();
133    if typ.is_trait {
134        writeln!(out, "    inner: Arc<dyn {core_path} + Send + Sync>,").ok();
135    } else {
136        writeln!(out, "    inner: Arc<{core_path}>,").ok();
137    }
138    write!(out, "}}").ok();
139    out
140}