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");
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 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 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 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 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 let items = import.items.join(", ");
123 group.push_str(&format!("use {}::{{{} }};\n", import.module, items));
124 }
125 }
126 group
127 };
128
129 if !stdlib.is_empty() {
131 output.push_str(&format_group(&stdlib));
132 }
133
134 if !external.is_empty() {
136 if !stdlib.is_empty() {
137 output.push('\n');
138 }
139 output.push_str(&format_group(&external));
140 }
141
142 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 output.push_str(&format!("/// {trimmed}\n"));
169 } else if trimmed.starts_with("```") {
170 output.push_str(&format!("/// {trimmed}\n"));
172 } else {
173 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 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 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 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 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 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 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(§ions);
334
335 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(§ions);
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(§ions);
359
360 assert!(result.contains("struct MyStruct"));
361 assert!(result.contains("fn my_function"));
362 }
363}