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, with a per-field attribute callback.
27///
28/// `extra_field_attrs` is called for each field and returns additional `#[...]` attributes to
29/// prepend (beyond `cfg.field_attrs`). Pass `|_| vec![]` to use the default behaviour.
30pub fn gen_struct_with_per_field_attrs(
31    typ: &TypeDef,
32    mapper: &dyn TypeMapper,
33    cfg: &RustBindingConfig,
34    extra_field_attrs: impl Fn(&alef_core::ir::FieldDef) -> Vec<String>,
35) -> String {
36    let mut sb = StructBuilder::new(&typ.name);
37    for attr in cfg.struct_attrs {
38        sb.add_attr(attr);
39    }
40
41    // Check if struct has similar field names (e.g., sub_symbol and sup_symbol)
42    let field_names: Vec<_> = typ.fields.iter().filter(|f| f.cfg.is_none()).map(|f| &f.name).collect();
43    if has_similar_names(&field_names) {
44        sb.add_attr("allow(clippy::similar_names)");
45    }
46
47    for d in cfg.struct_derives {
48        sb.add_derive(d);
49    }
50    if typ.has_default {
51        sb.add_derive("Default");
52    }
53    if cfg.has_serde {
54        sb.add_derive("serde::Serialize");
55    }
56    for field in &typ.fields {
57        if field.cfg.is_some() {
58            continue;
59        }
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 mut attrs: Vec<String> = cfg.field_attrs.iter().map(|a| a.to_string()).collect();
70        attrs.extend(extra_field_attrs(field));
71        sb.add_field_with_doc(&field.name, &ty, attrs, &field.doc);
72    }
73    sb.build()
74}
75
76/// Generate a struct definition using the builder.
77pub fn gen_struct(typ: &TypeDef, mapper: &dyn TypeMapper, cfg: &RustBindingConfig) -> String {
78    let mut sb = StructBuilder::new(&typ.name);
79    for attr in cfg.struct_attrs {
80        sb.add_attr(attr);
81    }
82
83    // Check if struct has similar field names (e.g., sub_symbol and sup_symbol)
84    let field_names: Vec<_> = typ.fields.iter().filter(|f| f.cfg.is_none()).map(|f| &f.name).collect();
85    if has_similar_names(&field_names) {
86        sb.add_attr("allow(clippy::similar_names)");
87    }
88
89    for d in cfg.struct_derives {
90        sb.add_derive(d);
91    }
92    // Types with has_default get #[derive(Default)] — all fields use Default::default()
93    // so the manual impl is unnecessary and triggers clippy::derivable_impls.
94    if typ.has_default {
95        sb.add_derive("Default");
96    }
97    if cfg.has_serde {
98        sb.add_derive("serde::Serialize");
99    }
100    for field in &typ.fields {
101        // Skip cfg-gated fields — they depend on features that may not be enabled
102        // for this binding crate. Including them would require the binding struct to
103        // handle conditional compilation which struct literal initializers can't express.
104        if field.cfg.is_some() {
105            continue;
106        }
107        // When option_duration_on_defaults is set, wrap non-optional Duration fields in
108        // Option<u64> for has_default types so the binding constructor can accept None
109        // and the From conversion falls back to the core type's Default.
110        let force_optional = cfg.option_duration_on_defaults
111            && typ.has_default
112            && !field.optional
113            && matches!(field.ty, TypeRef::Duration);
114        let ty = if field.optional || force_optional {
115            mapper.optional(&mapper.map_type(&field.ty))
116        } else {
117            mapper.map_type(&field.ty)
118        };
119        let attrs: Vec<String> = cfg.field_attrs.iter().map(|a| a.to_string()).collect();
120        sb.add_field_with_doc(&field.name, &ty, attrs, &field.doc);
121    }
122    sb.build()
123}
124
125/// Generate a `Default` impl for a non-opaque binding struct with `has_default`.
126/// All fields use their type's Default::default().
127/// Optional fields use None instead of Default::default().
128/// This enables the struct to be used with `unwrap_or_default()` in config constructors.
129pub fn gen_struct_default_impl(typ: &TypeDef, name_prefix: &str) -> String {
130    let full_name = format!("{}{}", name_prefix, typ.name);
131    let mut out = String::with_capacity(256);
132    writeln!(out, "impl Default for {} {{", full_name).ok();
133    writeln!(out, "    fn default() -> Self {{").ok();
134    writeln!(out, "        Self {{").ok();
135    for field in &typ.fields {
136        if field.cfg.is_some() {
137            continue;
138        }
139        let default_val = match &field.ty {
140            TypeRef::Optional(_) => "None".to_string(),
141            _ => "Default::default()".to_string(),
142        };
143        writeln!(out, "            {}: {},", field.name, default_val).ok();
144    }
145    writeln!(out, "        }}").ok();
146    writeln!(out, "    }}").ok();
147    write!(out, "}}").ok();
148    out
149}
150
151/// Generate an opaque wrapper struct with `inner: Arc<core::Type>`.
152/// For trait types, uses `Arc<dyn Type + Send + Sync>`.
153pub fn gen_opaque_struct(typ: &TypeDef, cfg: &RustBindingConfig) -> String {
154    let mut out = String::with_capacity(512);
155    if !cfg.struct_derives.is_empty() {
156        writeln!(out, "#[derive(Clone)]").ok();
157    }
158    for attr in cfg.struct_attrs {
159        writeln!(out, "#[{attr}]").ok();
160    }
161    writeln!(out, "pub struct {} {{", typ.name).ok();
162    let core_path = typ.rust_path.replace('-', "_");
163    if typ.is_trait {
164        writeln!(out, "    inner: Arc<dyn {core_path} + Send + Sync>,").ok();
165    } else {
166        writeln!(out, "    inner: Arc<{core_path}>,").ok();
167    }
168    write!(out, "}}").ok();
169    out
170}
171
172/// Generate an opaque wrapper struct with `inner: Arc<core::Type>` and a `Js` prefix.
173pub fn gen_opaque_struct_prefixed(typ: &TypeDef, cfg: &RustBindingConfig, prefix: &str) -> String {
174    let mut out = String::with_capacity(512);
175    if !cfg.struct_derives.is_empty() {
176        writeln!(out, "#[derive(Clone)]").ok();
177    }
178    for attr in cfg.struct_attrs {
179        writeln!(out, "#[{attr}]").ok();
180    }
181    let core_path = typ.rust_path.replace('-', "_");
182    writeln!(out, "pub struct {}{} {{", prefix, typ.name).ok();
183    if typ.is_trait {
184        writeln!(out, "    inner: Arc<dyn {core_path} + Send + Sync>,").ok();
185    } else {
186        writeln!(out, "    inner: Arc<{core_path}>,").ok();
187    }
188    write!(out, "}}").ok();
189    out
190}