Skip to main content

spikard_cli/codegen/formatters/
rust_lang.rs

1//! Rust code formatter for generated code output
2//!
3//! Formats Rust code according to Rust 2024 edition standards with support for:
4//! - Auto-generation notices and module-level rustdoc
5//! - Organized imports grouped by stdlib, external crates, and internal crates
6//! - Rustdoc documentation with proper markdown formatting and examples
7//! - Proper item ordering: imports → types → functions → tests
8//! - Rustfmt-compatible spacing and formatting
9//!
10//! # Design
11//!
12//! This formatter ensures generated Rust code maintains consistency with:
13//! - Clear auto-generation markers for tooling integration
14//! - Crate-level attributes for allow/deny rules
15//! - Module-level rustdoc with `//!` format
16//! - Item documentation with `///` format
17//! - Alphabetically sorted imports within groups
18//! - Single blank line between items, double blank lines between major sections
19//!
20//! # Example
21//!
22//! ```no_run
23//! use spikard_cli::codegen::formatters::{Formatter, Import, HeaderMetadata, RustFormatter};
24//!
25//! let formatter = RustFormatter::new();
26//! let metadata = HeaderMetadata {
27//!     auto_generated: true,
28//!     schema_file: Some("schema.graphql".to_string()),
29//!     generator_version: Some("0.6.2".to_string()),
30//! };
31//!
32//! let header = formatter.format_header(&metadata);
33//! assert!(header.contains("DO NOT EDIT"));
34//! assert!(header.contains("//!"));
35//! ```
36
37use super::{Formatter, HeaderMetadata, Import, Section};
38
39/// Rust code formatter implementing Rust 2024 edition standards
40///
41/// This formatter generates Rust code that adheres to the latest Rust edition,
42/// ensuring consistency across the spikard toolkit. It handles proper import
43/// organization, rustdoc comments, and item ordering according to Rust conventions.
44#[derive(Debug, Clone)]
45pub struct RustFormatter;
46
47impl RustFormatter {
48    /// Create a new Rust formatter instance
49    #[must_use]
50    pub const fn new() -> Self {
51        Self
52    }
53}
54
55impl Default for RustFormatter {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl Formatter for RustFormatter {
62    fn format_header(&self, metadata: &HeaderMetadata) -> String {
63        let mut output = String::new();
64
65        output.push_str("// DO NOT EDIT - Auto-generated by Spikard CLI\n");
66
67        if let Some(schema_file) = &metadata.schema_file {
68            output.push_str(&format!("// Schema: {schema_file}\n"));
69        }
70
71        if let Some(version) = &metadata.generator_version {
72            output.push_str(&format!("// Generator version: {version}\n"));
73        }
74
75        output.push_str("\n//! Auto-generated module from Spikard code generation.\n");
76        if metadata.auto_generated {
77            output.push_str("//!\n");
78            output.push_str("//! This module was automatically generated and should not be manually edited.\n");
79            output.push_str("//! Regenerate from the source schema to incorporate changes.\n");
80        }
81
82        output
83    }
84
85    fn format_imports(&self, imports: &[Import]) -> String {
86        if imports.is_empty() {
87            return String::new();
88        }
89
90        let mut stdlib = Vec::new();
91        let mut external = Vec::new();
92        let mut internal = Vec::new();
93
94        for import in imports {
95            if import.module.starts_with("std::") || import.module == "std" {
96                stdlib.push(import.clone());
97            } else if import.module.starts_with("crate::") || import.module.starts_with("super::") {
98                internal.push(import.clone());
99            } else {
100                external.push(import.clone());
101            }
102        }
103
104        stdlib.sort_by(|a, b| a.module.cmp(&b.module));
105        external.sort_by(|a, b| a.module.cmp(&b.module));
106        internal.sort_by(|a, b| a.module.cmp(&b.module));
107
108        let mut output = String::new();
109
110        let format_group = |imports: &[Import]| -> String {
111            let mut group = String::new();
112            for import in imports {
113                if import.items.is_empty() {
114                    group.push_str(&format!("use {};\n", import.module));
115                } else {
116                    let items = import.items.join(", ");
117                    group.push_str(&format!("use {}::{{{} }};\n", import.module, items));
118                }
119            }
120            group
121        };
122
123        if !stdlib.is_empty() {
124            output.push_str(&format_group(&stdlib));
125        }
126
127        if !external.is_empty() {
128            if !stdlib.is_empty() {
129                output.push('\n');
130            }
131            output.push_str(&format_group(&external));
132        }
133
134        if !internal.is_empty() {
135            if !stdlib.is_empty() || !external.is_empty() {
136                output.push('\n');
137            }
138            output.push_str(&format_group(&internal));
139        }
140
141        output.trim_end().to_string()
142    }
143
144    fn format_docstring(&self, content: &str) -> String {
145        let lines: Vec<&str> = content.lines().collect();
146
147        if lines.is_empty() {
148            return String::new();
149        }
150
151        let mut output = String::new();
152
153        for line in lines {
154            let trimmed = line.trim();
155            if trimmed.is_empty() {
156                output.push_str("///\n");
157            } else if trimmed.starts_with("# ") || trimmed.starts_with("## ") || trimmed.starts_with("### ") {
158                output.push_str(&format!("/// {trimmed}\n"));
159            } else if trimmed.starts_with("```") {
160                output.push_str(&format!("/// {trimmed}\n"));
161            } else {
162                output.push_str(&format!("/// {trimmed}\n"));
163            }
164        }
165
166        output.trim_end().to_string()
167    }
168
169    fn merge_sections(&self, sections: &[Section]) -> String {
170        let mut header_content = String::new();
171        let mut imports_content = String::new();
172        let mut body_content = String::new();
173
174        for section in sections {
175            match section {
176                Section::Header(content) => header_content.push_str(content),
177                Section::Imports(content) => imports_content.push_str(content),
178                Section::Body(content) => body_content.push_str(content),
179            }
180        }
181
182        let mut output = String::new();
183
184        if !header_content.is_empty() {
185            output.push_str(&header_content);
186            if !output.ends_with('\n') {
187                output.push('\n');
188            }
189        }
190
191        if !imports_content.is_empty() {
192            let imports_trimmed = imports_content.trim();
193            if !imports_trimmed.is_empty() {
194                output.push('\n');
195                output.push_str(imports_trimmed);
196            }
197        }
198
199        if !body_content.is_empty() {
200            let body_trimmed = body_content.trim();
201            if !body_trimmed.is_empty() {
202                output.push('\n');
203                output.push('\n');
204                output.push_str(body_trimmed);
205            }
206        }
207
208        if !output.ends_with('\n') {
209            output.push('\n');
210        }
211
212        output
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_format_header_contains_notice() {
222        let formatter = RustFormatter::new();
223        let metadata = HeaderMetadata {
224            auto_generated: true,
225            schema_file: None,
226            generator_version: None,
227        };
228        let header = formatter.format_header(&metadata);
229
230        assert!(header.contains("DO NOT EDIT"));
231        assert!(header.contains("//!"));
232    }
233
234    #[test]
235    fn test_format_header_with_metadata() {
236        let formatter = RustFormatter::new();
237        let metadata = HeaderMetadata {
238            auto_generated: true,
239            schema_file: Some("schema.graphql".to_string()),
240            generator_version: Some("0.6.2".to_string()),
241        };
242        let header = formatter.format_header(&metadata);
243
244        assert!(header.contains("schema.graphql"));
245        assert!(header.contains("0.6.2"));
246    }
247
248    #[test]
249    fn test_format_imports_empty() {
250        let formatter = RustFormatter::new();
251        let imports = vec![];
252        let result = formatter.format_imports(&imports);
253
254        assert!(result.is_empty());
255    }
256
257    #[test]
258    fn test_format_imports_grouped() {
259        let formatter = RustFormatter::new();
260        let imports = vec![
261            Import::new("crate::models"),
262            Import::new("std::collections"),
263            Import::new("serde"),
264        ];
265        let result = formatter.format_imports(&imports);
266
267        let std_pos = result.find("std::").unwrap();
268        let serde_pos = result.find("serde").unwrap();
269        let crate_pos = result.find("crate::").unwrap();
270
271        assert!(std_pos < serde_pos);
272        assert!(serde_pos < crate_pos);
273    }
274
275    #[test]
276    fn test_format_imports_with_items() {
277        let formatter = RustFormatter::new();
278        let imports = vec![Import::with_items("std::collections", vec!["HashMap", "BTreeMap"])];
279        let result = formatter.format_imports(&imports);
280
281        assert!(result.contains("use std::collections::{"));
282        assert!(result.contains("HashMap"));
283        assert!(result.contains("BTreeMap"));
284    }
285
286    #[test]
287    fn test_format_docstring() {
288        let formatter = RustFormatter::new();
289        let content = "This is documentation\nWith multiple lines";
290        let result = formatter.format_docstring(content);
291
292        assert!(result.contains("///"));
293        assert!(result.contains("This is documentation"));
294        assert!(result.contains("With multiple lines"));
295    }
296
297    #[test]
298    fn test_format_docstring_with_code() {
299        let formatter = RustFormatter::new();
300        let content = "Example usage:\n```rust\nlet x = 5;\n```";
301        let result = formatter.format_docstring(content);
302
303        assert!(result.contains("```rust"));
304        assert!(result.contains("let x = 5;"));
305    }
306
307    #[test]
308    fn test_merge_sections_proper_spacing() {
309        let formatter = RustFormatter::new();
310        let sections = vec![
311            Section::Header("// DO NOT EDIT\n".to_string()),
312            Section::Imports("use std::collections::HashMap;\n".to_string()),
313            Section::Body("fn main() {}".to_string()),
314        ];
315        let result = formatter.merge_sections(&sections);
316
317        assert!(result.contains("// DO NOT EDIT"));
318        assert!(result.contains("use std::"));
319        assert!(result.contains("fn main"));
320        assert!(result.ends_with('\n'));
321    }
322
323    #[test]
324    fn test_merge_sections_ends_with_newline() {
325        let formatter = RustFormatter::new();
326        let sections = vec![Section::Body("fn test() {}".to_string())];
327        let result = formatter.merge_sections(&sections);
328
329        assert!(result.ends_with('\n'));
330    }
331
332    #[test]
333    fn test_merge_sections_combines_multiple_bodies() {
334        let formatter = RustFormatter::new();
335        let sections = vec![
336            Section::Body("struct MyStruct {}\n".to_string()),
337            Section::Body("fn my_function() {}".to_string()),
338        ];
339        let result = formatter.merge_sections(&sections);
340
341        assert!(result.contains("struct MyStruct"));
342        assert!(result.contains("fn my_function"));
343    }
344}