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 a type's fields can all be safely defaulted.
8/// Primitives, strings, collections, Options, and Duration all have Default impls.
9/// Named types (custom structs) only have Default if explicitly marked with `has_default=true`.
10/// If any field is a Named type without `has_default`, returning true would generate
11/// code that calls `Default::default()` on a type that doesn't implement it.
12pub fn can_generate_default_impl(typ: &TypeDef, known_default_types: &std::collections::HashSet<&str>) -> bool {
13    for field in &typ.fields {
14        if field.cfg.is_some() {
15            continue; // Skip cfg-gated fields
16        }
17        if !field_type_has_default(&field.ty, known_default_types) {
18            return false;
19        }
20    }
21    true
22}
23
24/// Check if a specific TypeRef can be safely defaulted.
25fn field_type_has_default(ty: &TypeRef, known_default_types: &std::collections::HashSet<&str>) -> bool {
26    match ty {
27        TypeRef::Primitive(_)
28        | TypeRef::String
29        | TypeRef::Char
30        | TypeRef::Bytes
31        | TypeRef::Path
32        | TypeRef::Unit
33        | TypeRef::Duration
34        | TypeRef::Json => true,
35        // Optional<T> defaults to None regardless of T
36        TypeRef::Optional(inner) => field_type_has_default(inner, known_default_types),
37        // Vec<T> defaults to empty vec regardless of T
38        TypeRef::Vec(inner) => field_type_has_default(inner, known_default_types),
39        // Map<K, V> defaults to empty map regardless of K/V
40        TypeRef::Map(k, v) => {
41            field_type_has_default(k, known_default_types) && field_type_has_default(v, known_default_types)
42        }
43        // Named types only have Default if marked with has_default=true
44        TypeRef::Named(name) => known_default_types.contains(name.as_str()),
45    }
46}
47
48/// Check if any two field names are similar enough to trigger clippy::similar_names.
49/// This detects patterns like "sub_symbol" and "sup_symbol" (differ by 1-2 chars).
50fn has_similar_names(names: &[&String]) -> bool {
51    for (i, &name1) in names.iter().enumerate() {
52        for &name2 in &names[i + 1..] {
53            // Simple heuristic: if names differ by <= 2 characters and have same length, flag it
54            if name1.len() == name2.len() && diff_count(name1, name2) <= 2 {
55                return true;
56            }
57        }
58    }
59    false
60}
61
62/// Count how many characters differ between two strings of equal length.
63fn diff_count(s1: &str, s2: &str) -> usize {
64    s1.chars().zip(s2.chars()).filter(|(c1, c2)| c1 != c2).count()
65}
66
67/// Generate a struct definition using the builder, with a per-field attribute callback.
68///
69/// `extra_field_attrs` is called for each field and returns additional `#[...]` attributes to
70/// prepend (beyond `cfg.field_attrs`). Pass `|_| vec![]` to use the default behaviour.
71pub fn gen_struct_with_per_field_attrs(
72    typ: &TypeDef,
73    mapper: &dyn TypeMapper,
74    cfg: &RustBindingConfig,
75    extra_field_attrs: impl Fn(&alef_core::ir::FieldDef) -> Vec<String>,
76) -> String {
77    let mut sb = StructBuilder::new(&typ.name);
78    for attr in cfg.struct_attrs {
79        sb.add_attr(attr);
80    }
81
82    // Check if struct has similar field names (e.g., sub_symbol and sup_symbol)
83    let field_names: Vec<_> = typ.fields.iter().filter(|f| f.cfg.is_none()).map(|f| &f.name).collect();
84    if has_similar_names(&field_names) {
85        sb.add_attr("allow(clippy::similar_names)");
86    }
87
88    for d in cfg.struct_derives {
89        sb.add_derive(d);
90    }
91    // Binding types always derive Default, Serialize, and Deserialize.
92    // Default: enables using unwrap_or_default() in constructors for types with has_default.
93    // Serialize/Deserialize: required for FFI/type conversion across binding boundaries.
94    sb.add_derive("Default");
95    sb.add_derive("serde::Serialize");
96    sb.add_derive("serde::Deserialize");
97    for field in &typ.fields {
98        if field.cfg.is_some() {
99            continue;
100        }
101        let force_optional = cfg.option_duration_on_defaults
102            && typ.has_default
103            && !field.optional
104            && matches!(field.ty, TypeRef::Duration);
105        let ty = if (field.optional || force_optional) && !matches!(field.ty, TypeRef::Optional(_)) {
106            mapper.optional(&mapper.map_type(&field.ty))
107        } else {
108            // field.ty is already Optional(T) — mapped type is already Option<T>, don't double-wrap
109            mapper.map_type(&field.ty)
110        };
111        let mut attrs: Vec<String> = cfg.field_attrs.iter().map(|a| a.to_string()).collect();
112        attrs.extend(extra_field_attrs(field));
113        sb.add_field_with_doc(&field.name, &ty, attrs, &field.doc);
114    }
115    sb.build()
116}
117
118/// Generate a struct definition using the builder.
119pub fn gen_struct(typ: &TypeDef, mapper: &dyn TypeMapper, cfg: &RustBindingConfig) -> String {
120    let mut sb = StructBuilder::new(&typ.name);
121    for attr in cfg.struct_attrs {
122        sb.add_attr(attr);
123    }
124
125    // Check if struct has similar field names (e.g., sub_symbol and sup_symbol)
126    let field_names: Vec<_> = typ.fields.iter().filter(|f| f.cfg.is_none()).map(|f| &f.name).collect();
127    if has_similar_names(&field_names) {
128        sb.add_attr("allow(clippy::similar_names)");
129    }
130
131    for d in cfg.struct_derives {
132        sb.add_derive(d);
133    }
134    // Binding types always derive Default, Serialize, and Deserialize.
135    // Default: enables using unwrap_or_default() in constructors for types with has_default.
136    // Serialize/Deserialize: required for FFI/type conversion across binding boundaries.
137    sb.add_derive("Default");
138    sb.add_derive("serde::Serialize");
139    sb.add_derive("serde::Deserialize");
140    for field in &typ.fields {
141        // Skip cfg-gated fields — they depend on features that may not be enabled
142        // for this binding crate. Including them would require the binding struct to
143        // handle conditional compilation which struct literal initializers can't express.
144        if field.cfg.is_some() {
145            continue;
146        }
147        // When option_duration_on_defaults is set, wrap non-optional Duration fields in
148        // Option<u64> for has_default types so the binding constructor can accept None
149        // and the From conversion falls back to the core type's Default.
150        let force_optional = cfg.option_duration_on_defaults
151            && typ.has_default
152            && !field.optional
153            && matches!(field.ty, TypeRef::Duration);
154        let ty = if (field.optional || force_optional) && !matches!(field.ty, TypeRef::Optional(_)) {
155            mapper.optional(&mapper.map_type(&field.ty))
156        } else {
157            // field.ty is already Optional(T) — mapped type is already Option<T>, don't double-wrap
158            mapper.map_type(&field.ty)
159        };
160        let attrs: Vec<String> = cfg.field_attrs.iter().map(|a| a.to_string()).collect();
161        sb.add_field_with_doc(&field.name, &ty, attrs, &field.doc);
162    }
163    sb.build()
164}
165
166/// Generate a `Default` impl for a non-opaque binding struct with `has_default`.
167/// All fields use their type's Default::default().
168/// Optional fields use None instead of Default::default().
169/// This enables the struct to be used with `unwrap_or_default()` in config constructors.
170///
171/// WARNING: This assumes all field types implement Default. If a Named field type
172/// doesn't implement Default, this impl will fail to compile. Callers should verify
173/// that the struct's fields can be safely defaulted before calling this function.
174pub fn gen_struct_default_impl(typ: &TypeDef, name_prefix: &str) -> String {
175    let full_name = format!("{}{}", name_prefix, typ.name);
176    let mut out = String::with_capacity(256);
177    writeln!(out, "impl Default for {} {{", full_name).ok();
178    writeln!(out, "    fn default() -> Self {{").ok();
179    writeln!(out, "        Self {{").ok();
180    for field in &typ.fields {
181        if field.cfg.is_some() {
182            continue;
183        }
184        let default_val = match &field.ty {
185            TypeRef::Optional(_) => "None".to_string(),
186            _ => "Default::default()".to_string(),
187        };
188        writeln!(out, "            {}: {},", field.name, default_val).ok();
189    }
190    writeln!(out, "        }}").ok();
191    writeln!(out, "    }}").ok();
192    write!(out, "}}").ok();
193    out
194}
195
196/// Generate an opaque wrapper struct with `inner: Arc<core::Type>`.
197/// For trait types, uses `Arc<dyn Type + Send + Sync>`.
198pub fn gen_opaque_struct(typ: &TypeDef, cfg: &RustBindingConfig) -> String {
199    let mut out = String::with_capacity(512);
200    if !cfg.struct_derives.is_empty() {
201        writeln!(out, "#[derive(Clone)]").ok();
202    }
203    for attr in cfg.struct_attrs {
204        writeln!(out, "#[{attr}]").ok();
205    }
206    writeln!(out, "pub struct {} {{", typ.name).ok();
207    let core_path = typ.rust_path.replace('-', "_");
208    if typ.is_trait {
209        writeln!(out, "    inner: Arc<dyn {core_path} + Send + Sync>,").ok();
210    } else {
211        writeln!(out, "    inner: Arc<{core_path}>,").ok();
212    }
213    write!(out, "}}").ok();
214    out
215}
216
217/// Generate an opaque wrapper struct with `inner: Arc<core::Type>` and a `Js` prefix.
218pub fn gen_opaque_struct_prefixed(typ: &TypeDef, cfg: &RustBindingConfig, prefix: &str) -> String {
219    let mut out = String::with_capacity(512);
220    if !cfg.struct_derives.is_empty() {
221        writeln!(out, "#[derive(Clone)]").ok();
222    }
223    for attr in cfg.struct_attrs {
224        writeln!(out, "#[{attr}]").ok();
225    }
226    let core_path = typ.rust_path.replace('-', "_");
227    writeln!(out, "pub struct {}{} {{", prefix, typ.name).ok();
228    if typ.is_trait {
229        writeln!(out, "    inner: Arc<dyn {core_path} + Send + Sync>,").ok();
230    } else {
231        writeln!(out, "    inner: Arc<{core_path}>,").ok();
232    }
233    write!(out, "}}").ok();
234    out
235}