spikard_cli/codegen/formatters/
rust_lang.rs1use super::{Formatter, HeaderMetadata, Import, Section};
38
39#[derive(Debug, Clone)]
45pub struct RustFormatter;
46
47impl RustFormatter {
48 #[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(§ions);
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(§ions);
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(§ions);
340
341 assert!(result.contains("struct MyStruct"));
342 assert!(result.contains("fn my_function"));
343 }
344}