Skip to main content

alef_codegen/
builder.rs

1/// Builder for constructing Rust source files.
2#[derive(Debug, Default)]
3pub struct RustFileBuilder {
4    doc_header: Option<String>,
5    imports: Vec<String>,
6    items: Vec<String>,
7}
8
9impl RustFileBuilder {
10    pub fn new() -> Self {
11        Self::default()
12    }
13
14    /// Add a "DO NOT EDIT" header.
15    pub fn with_generated_header(mut self) -> Self {
16        self.doc_header = Some(
17            "// This file is auto-generated by alef. DO NOT EDIT.\n\
18             // Re-generate with: alef generate\n"
19                .to_string(),
20        );
21        self
22    }
23
24    /// Add a crate-level inner attribute (e.g., `#![allow(clippy::...)]`).
25    /// These are placed at the very top of the file, before imports.
26    pub fn add_inner_attribute(&mut self, attr: &str) {
27        // Append to doc_header since it comes first
28        let rendered = crate::template_env::render(
29            "builders/inner_attribute.jinja",
30            minijinja::context! {
31                attr => attr,
32            },
33        );
34        if let Some(ref mut header) = self.doc_header {
35            header.push_str(&rendered);
36        } else {
37            self.doc_header = Some(rendered);
38        }
39    }
40
41    /// Add a use import line.
42    /// Single-component imports (e.g. `use serde_json;`) are skipped since they are
43    /// redundant in Rust 2018+ where extern crates are automatically in scope.
44    pub fn add_import(&mut self, import: &str) {
45        // Skip single-component path imports — they trigger clippy::single_component_path_imports
46        // and are redundant in edition 2018+. A single component has no `::`, `*`, or `{`.
47        if !import.contains("::") && !import.contains('*') && !import.contains('{') {
48            return;
49        }
50        if !self.imports.iter().any(|i| i == import) {
51            self.imports.push(import.to_string());
52        }
53    }
54
55    /// Add a raw code item (struct, impl, function, etc.).
56    pub fn add_item(&mut self, item: &str) {
57        self.items.push(item.to_string());
58    }
59
60    /// Build the final source string.
61    pub fn build(&self) -> String {
62        let mut capacity = 256; // base header + imports + newlines
63        if let Some(header) = &self.doc_header {
64            capacity += header.len() + 1;
65        }
66        capacity += self.imports.iter().map(|i| i.len() + 6).sum::<usize>(); // "use ...;\n"
67        capacity += self.items.iter().map(|i| i.len() + 2).sum::<usize>(); // item + "\n\n"
68
69        let mut out = String::with_capacity(capacity);
70
71        if let Some(header) = &self.doc_header {
72            out.push_str(header);
73            out.push('\n');
74        }
75
76        if !self.imports.is_empty() {
77            for import in &self.imports {
78                out.push_str(&crate::template_env::render(
79                    "builders/use_import.jinja",
80                    minijinja::context! {
81                        import => import,
82                    },
83                ));
84            }
85            out.push('\n');
86        }
87
88        for (i, item) in self.items.iter().enumerate() {
89            out.push_str(item);
90            if i < self.items.len() - 1 {
91                out.push_str("\n\n");
92            }
93        }
94
95        if !out.ends_with('\n') {
96            out.push('\n');
97        }
98
99        out
100    }
101}
102
103/// Helper to build a struct with attributes.
104pub struct StructBuilder {
105    attrs: Vec<String>,
106    visibility: String,
107    name: String,
108    derives: Vec<String>,
109    fields: Vec<(String, String, Vec<String>, String)>, // (name, type, field_attrs, doc)
110}
111
112impl StructBuilder {
113    pub fn new(name: &str) -> Self {
114        Self {
115            attrs: vec![],
116            visibility: String::from("pub"),
117            name: name.to_string(),
118            derives: vec![],
119            fields: vec![],
120        }
121    }
122
123    pub fn add_attr(&mut self, attr: &str) -> &mut Self {
124        self.attrs.push(attr.to_string());
125        self
126    }
127
128    pub fn add_derive(&mut self, derive: &str) -> &mut Self {
129        if !self.derives.iter().any(|d| d == derive) {
130            self.derives.push(derive.to_string());
131        }
132        self
133    }
134
135    pub fn add_field(&mut self, name: &str, ty: &str, attrs: Vec<String>) -> &mut Self {
136        self.add_field_with_doc(name, ty, attrs, "")
137    }
138
139    pub fn add_field_with_doc(&mut self, name: &str, ty: &str, attrs: Vec<String>, doc: &str) -> &mut Self {
140        self.fields
141            .push((name.to_string(), ty.to_string(), attrs, doc.to_string()));
142        self
143    }
144
145    pub fn build(&self) -> String {
146        let mut capacity = 128; // structural overhead
147        capacity += self.derives.iter().map(|d| d.len() + 2).sum::<usize>(); // derive + comma
148        capacity += self.attrs.iter().map(|a| a.len() + 5).sum::<usize>(); // #[...]\n
149        capacity += self.name.len() + self.visibility.len() + 16; // struct declaration
150        capacity += self
151            .fields
152            .iter()
153            .map(|(n, t, attrs, doc)| {
154                n.len() + t.len() + 12 + doc.len() + 8 + attrs.iter().map(|a| a.len() + 5).sum::<usize>()
155            })
156            .sum::<usize>();
157
158        let mut out = String::with_capacity(capacity);
159
160        if !self.derives.is_empty() {
161            let rendered = crate::template_env::render(
162                "builders/derive_attr.jinja",
163                minijinja::context! {
164                    derives => self.derives.join(", "),
165                },
166            );
167            out.push_str(&rendered);
168            if !rendered.ends_with('\n') {
169                out.push('\n');
170            }
171        }
172
173        for attr in &self.attrs {
174            let rendered = crate::template_env::render(
175                "builders/generic_attr.jinja",
176                minijinja::context! {
177                    attr => attr,
178                },
179            );
180            out.push_str(&rendered);
181            if !rendered.ends_with('\n') {
182                out.push('\n');
183            }
184        }
185
186        let rendered_header = crate::template_env::render(
187            "builders/struct_header.jinja",
188            minijinja::context! {
189                visibility => &self.visibility,
190                name => &self.name,
191            },
192        );
193        out.push_str(&rendered_header);
194        if !rendered_header.ends_with('\n') {
195            out.push('\n');
196        }
197
198        for (name, ty, attrs, doc) in &self.fields {
199            if !doc.is_empty() {
200                for line in doc.lines() {
201                    let rendered = crate::template_env::render(
202                        "builders/doc_line.jinja",
203                        minijinja::context! {
204                            line => line,
205                        },
206                    );
207                    out.push_str(&rendered);
208                    if !rendered.ends_with('\n') {
209                        out.push('\n');
210                    }
211                }
212            }
213            for attr in attrs {
214                let rendered = crate::template_env::render(
215                    "builders/generic_attr.jinja",
216                    minijinja::context! {
217                        attr => attr,
218                    },
219                );
220                out.push_str(&rendered);
221                if !rendered.ends_with('\n') {
222                    out.push('\n');
223                }
224            }
225            let rendered_field = crate::template_env::render(
226                "builders/struct_field.jinja",
227                minijinja::context! {
228                    name => name,
229                    ty => ty,
230                },
231            );
232            out.push_str(&rendered_field);
233            if !rendered_field.ends_with('\n') {
234                out.push('\n');
235            }
236        }
237
238        out.push('}');
239        out
240    }
241}
242
243/// Helper to build an impl block.
244pub struct ImplBuilder {
245    target: String,
246    attrs: Vec<String>,
247    methods: Vec<String>,
248}
249
250impl ImplBuilder {
251    pub fn new(target: &str) -> Self {
252        Self {
253            target: target.to_string(),
254            attrs: vec![],
255            methods: vec![],
256        }
257    }
258
259    pub fn add_attr(&mut self, attr: &str) -> &mut Self {
260        self.attrs.push(attr.to_string());
261        self
262    }
263
264    pub fn add_method(&mut self, method: &str) -> &mut Self {
265        self.methods.push(method.to_string());
266        self
267    }
268
269    pub fn build(&self) -> String {
270        let mut capacity = 128; // structural overhead
271        capacity += self.target.len() + 10; // impl ... {}
272        capacity += self.attrs.iter().map(|a| a.len() + 5).sum::<usize>(); // #[...]\n
273        capacity += self.methods.iter().map(|m| m.len() + 4).sum::<usize>(); // indented + newlines
274
275        let mut out = String::with_capacity(capacity);
276
277        for attr in &self.attrs {
278            let rendered = crate::template_env::render(
279                "builders/generic_attr.jinja",
280                minijinja::context! {
281                    attr => attr,
282                },
283            );
284            out.push_str(&rendered);
285            if !rendered.ends_with('\n') {
286                out.push('\n');
287            }
288        }
289
290        let rendered_header = crate::template_env::render(
291            "builders/impl_header.jinja",
292            minijinja::context! {
293                target => &self.target,
294            },
295        );
296        out.push_str(&rendered_header);
297        if !rendered_header.ends_with('\n') {
298            out.push('\n');
299        }
300
301        for (i, method) in self.methods.iter().enumerate() {
302            // Indent each line of the method
303            for line in method.lines() {
304                if line.is_empty() {
305                    out.push('\n');
306                } else {
307                    let rendered = crate::template_env::render(
308                        "builders/indented_line.jinja",
309                        minijinja::context! {
310                            line => line,
311                        },
312                    );
313                    out.push_str(&rendered);
314                    if !rendered.ends_with('\n') {
315                        out.push('\n');
316                    }
317                }
318            }
319            if i < self.methods.len() - 1 {
320                out.push('\n');
321            }
322        }
323
324        out.push('}');
325        out
326    }
327}