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        // Auto-generation notice comment
66        output.push_str("// DO NOT EDIT - Auto-generated by Spikard CLI\n");
67
68        if let Some(schema_file) = &metadata.schema_file {
69            output.push_str(&format!("// Schema: {schema_file}\n"));
70        }
71
72        if let Some(version) = &metadata.generator_version {
73            output.push_str(&format!("// Generator version: {version}\n"));
74        }
75
76        // Module-level rustdoc
77        output.push_str("\n//! Auto-generated module from Spikard code generation.\n");
78        if metadata.auto_generated {
79            output.push_str("//!\n");
80            output.push_str("//! This module was automatically generated and should not be manually edited.\n");
81            output.push_str("//! Regenerate from the source schema to incorporate changes.\n");
82        }
83
84        output
85    }
86
87    fn format_imports(&self, imports: &[Import]) -> String {
88        if imports.is_empty() {
89            return String::new();
90        }
91
92        // Classify imports into groups
93        let mut stdlib = Vec::new();
94        let mut external = Vec::new();
95        let mut internal = Vec::new();
96
97        for import in imports {
98            if import.module.starts_with("std::") || import.module == "std" {
99                stdlib.push(import.clone());
100            } else if import.module.starts_with("crate::") || import.module.starts_with("super::") {
101                internal.push(import.clone());
102            } else {
103                external.push(import.clone());
104            }
105        }
106
107        // Sort each group alphabetically
108        stdlib.sort_by(|a, b| a.module.cmp(&b.module));
109        external.sort_by(|a, b| a.module.cmp(&b.module));
110        internal.sort_by(|a, b| a.module.cmp(&b.module));
111
112        let mut output = String::new();
113
114        // Helper function to format import group
115        let format_group = |imports: &[Import]| -> String {
116            let mut group = String::new();
117            for import in imports {
118                if import.items.is_empty() {
119                    group.push_str(&format!("use {};\n", import.module));
120                } else {
121                    // Combine multiple items from same module
122                    let items = import.items.join(", ");
123                    group.push_str(&format!("use {}::{{{} }};\n", import.module, items));
124                }
125            }
126            group
127        };
128
129        // Add stdlib imports
130        if !stdlib.is_empty() {
131            output.push_str(&format_group(&stdlib));
132        }
133
134        // Add external imports
135        if !external.is_empty() {
136            if !stdlib.is_empty() {
137                output.push('\n');
138            }
139            output.push_str(&format_group(&external));
140        }
141
142        // Add internal imports
143        if !internal.is_empty() {
144            if !stdlib.is_empty() || !external.is_empty() {
145                output.push('\n');
146            }
147            output.push_str(&format_group(&internal));
148        }
149
150        output.trim_end().to_string()
151    }
152
153    fn format_docstring(&self, content: &str) -> String {
154        let lines: Vec<&str> = content.lines().collect();
155
156        if lines.is_empty() {
157            return String::new();
158        }
159
160        let mut output = String::new();
161
162        for line in lines {
163            let trimmed = line.trim();
164            if trimmed.is_empty() {
165                output.push_str("///\n");
166            } else if trimmed.starts_with("# ") || trimmed.starts_with("## ") || trimmed.starts_with("### ") {
167                // Markdown headers
168                output.push_str(&format!("/// {trimmed}\n"));
169            } else if trimmed.starts_with("```") {
170                // Code block markers
171                output.push_str(&format!("/// {trimmed}\n"));
172            } else {
173                // Regular documentation
174                output.push_str(&format!("/// {trimmed}\n"));
175            }
176        }
177
178        output.trim_end().to_string()
179    }
180
181    fn merge_sections(&self, sections: &[Section]) -> String {
182        let mut header_content = String::new();
183        let mut imports_content = String::new();
184        let mut body_content = String::new();
185
186        // Separate sections
187        for section in sections {
188            match section {
189                Section::Header(content) => header_content.push_str(content),
190                Section::Imports(content) => imports_content.push_str(content),
191                Section::Body(content) => body_content.push_str(content),
192            }
193        }
194
195        let mut output = String::new();
196
197        // Add header
198        if !header_content.is_empty() {
199            output.push_str(&header_content);
200            if !output.ends_with('\n') {
201                output.push('\n');
202            }
203        }
204
205        // Add imports with double blank line spacing
206        if !imports_content.is_empty() {
207            let imports_trimmed = imports_content.trim();
208            if !imports_trimmed.is_empty() {
209                output.push('\n');
210                output.push_str(imports_trimmed);
211            }
212        }
213
214        // Add body with double blank line spacing
215        if !body_content.is_empty() {
216            let body_trimmed = body_content.trim();
217            if !body_trimmed.is_empty() {
218                output.push('\n');
219                output.push('\n');
220                output.push_str(body_trimmed);
221            }
222        }
223
224        // Ensure trailing newline
225        if !output.ends_with('\n') {
226            output.push('\n');
227        }
228
229        output
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_format_header_contains_notice() {
239        let formatter = RustFormatter::new();
240        let metadata = HeaderMetadata {
241            auto_generated: true,
242            schema_file: None,
243            generator_version: None,
244        };
245        let header = formatter.format_header(&metadata);
246
247        assert!(header.contains("DO NOT EDIT"));
248        assert!(header.contains("//!"));
249    }
250
251    #[test]
252    fn test_format_header_with_metadata() {
253        let formatter = RustFormatter::new();
254        let metadata = HeaderMetadata {
255            auto_generated: true,
256            schema_file: Some("schema.graphql".to_string()),
257            generator_version: Some("0.6.2".to_string()),
258        };
259        let header = formatter.format_header(&metadata);
260
261        assert!(header.contains("schema.graphql"));
262        assert!(header.contains("0.6.2"));
263    }
264
265    #[test]
266    fn test_format_imports_empty() {
267        let formatter = RustFormatter::new();
268        let imports = vec![];
269        let result = formatter.format_imports(&imports);
270
271        assert!(result.is_empty());
272    }
273
274    #[test]
275    fn test_format_imports_grouped() {
276        let formatter = RustFormatter::new();
277        let imports = vec![
278            Import::new("crate::models"),
279            Import::new("std::collections"),
280            Import::new("serde"),
281        ];
282        let result = formatter.format_imports(&imports);
283
284        // Check that std comes before external which comes before crate
285        let std_pos = result.find("std::").unwrap();
286        let serde_pos = result.find("serde").unwrap();
287        let crate_pos = result.find("crate::").unwrap();
288
289        assert!(std_pos < serde_pos);
290        assert!(serde_pos < crate_pos);
291    }
292
293    #[test]
294    fn test_format_imports_with_items() {
295        let formatter = RustFormatter::new();
296        let imports = vec![Import::with_items("std::collections", vec!["HashMap", "BTreeMap"])];
297        let result = formatter.format_imports(&imports);
298
299        assert!(result.contains("use std::collections::{"));
300        assert!(result.contains("HashMap"));
301        assert!(result.contains("BTreeMap"));
302    }
303
304    #[test]
305    fn test_format_docstring() {
306        let formatter = RustFormatter::new();
307        let content = "This is documentation\nWith multiple lines";
308        let result = formatter.format_docstring(content);
309
310        assert!(result.contains("///"));
311        assert!(result.contains("This is documentation"));
312        assert!(result.contains("With multiple lines"));
313    }
314
315    #[test]
316    fn test_format_docstring_with_code() {
317        let formatter = RustFormatter::new();
318        let content = "Example usage:\n```rust\nlet x = 5;\n```";
319        let result = formatter.format_docstring(content);
320
321        assert!(result.contains("```rust"));
322        assert!(result.contains("let x = 5;"));
323    }
324
325    #[test]
326    fn test_merge_sections_proper_spacing() {
327        let formatter = RustFormatter::new();
328        let sections = vec![
329            Section::Header("// DO NOT EDIT\n".to_string()),
330            Section::Imports("use std::collections::HashMap;\n".to_string()),
331            Section::Body("fn main() {}".to_string()),
332        ];
333        let result = formatter.merge_sections(&sections);
334
335        // Should have proper blank line spacing
336        assert!(result.contains("// DO NOT EDIT"));
337        assert!(result.contains("use std::"));
338        assert!(result.contains("fn main"));
339        assert!(result.ends_with('\n'));
340    }
341
342    #[test]
343    fn test_merge_sections_ends_with_newline() {
344        let formatter = RustFormatter::new();
345        let sections = vec![Section::Body("fn test() {}".to_string())];
346        let result = formatter.merge_sections(&sections);
347
348        assert!(result.ends_with('\n'));
349    }
350
351    #[test]
352    fn test_merge_sections_combines_multiple_bodies() {
353        let formatter = RustFormatter::new();
354        let sections = vec![
355            Section::Body("struct MyStruct {}\n".to_string()),
356            Section::Body("fn my_function() {}".to_string()),
357        ];
358        let result = formatter.merge_sections(&sections);
359
360        assert!(result.contains("struct MyStruct"));
361        assert!(result.contains("fn my_function"));
362    }
363}