use super::{Formatter, HeaderMetadata, Import, Section};
#[derive(Debug, Clone)]
pub struct RustFormatter;
impl RustFormatter {
#[must_use]
pub const fn new() -> Self {
Self
}
}
impl Default for RustFormatter {
fn default() -> Self {
Self::new()
}
}
impl Formatter for RustFormatter {
fn format_header(&self, metadata: &HeaderMetadata) -> String {
let mut output = String::new();
output.push_str("// DO NOT EDIT - Auto-generated by Spikard CLI\n");
if let Some(schema_file) = &metadata.schema_file {
output.push_str(&format!("// Schema: {schema_file}\n"));
}
if let Some(version) = &metadata.generator_version {
output.push_str(&format!("// Generator version: {version}\n"));
}
output.push_str("\n//! Auto-generated module from Spikard code generation.\n");
if metadata.auto_generated {
output.push_str("//!\n");
output.push_str("//! This module was automatically generated and should not be manually edited.\n");
output.push_str("//! Regenerate from the source schema to incorporate changes.\n");
}
output
}
fn format_imports(&self, imports: &[Import]) -> String {
if imports.is_empty() {
return String::new();
}
let mut stdlib = Vec::new();
let mut external = Vec::new();
let mut internal = Vec::new();
for import in imports {
if import.module.starts_with("std::") || import.module == "std" {
stdlib.push(import.clone());
} else if import.module.starts_with("crate::") || import.module.starts_with("super::") {
internal.push(import.clone());
} else {
external.push(import.clone());
}
}
stdlib.sort_by(|a, b| a.module.cmp(&b.module));
external.sort_by(|a, b| a.module.cmp(&b.module));
internal.sort_by(|a, b| a.module.cmp(&b.module));
let mut output = String::new();
let format_group = |imports: &[Import]| -> String {
let mut group = String::new();
for import in imports {
if import.items.is_empty() {
group.push_str(&format!("use {};\n", import.module));
} else {
let items = import.items.join(", ");
group.push_str(&format!("use {}::{{{} }};\n", import.module, items));
}
}
group
};
if !stdlib.is_empty() {
output.push_str(&format_group(&stdlib));
}
if !external.is_empty() {
if !stdlib.is_empty() {
output.push('\n');
}
output.push_str(&format_group(&external));
}
if !internal.is_empty() {
if !stdlib.is_empty() || !external.is_empty() {
output.push('\n');
}
output.push_str(&format_group(&internal));
}
output.trim_end().to_string()
}
fn format_docstring(&self, content: &str) -> String {
let lines: Vec<&str> = content.lines().collect();
if lines.is_empty() {
return String::new();
}
let mut output = String::new();
for line in lines {
let trimmed = line.trim();
if trimmed.is_empty() {
output.push_str("///\n");
} else if trimmed.starts_with("# ") || trimmed.starts_with("## ") || trimmed.starts_with("### ") {
output.push_str(&format!("/// {trimmed}\n"));
} else if trimmed.starts_with("```") {
output.push_str(&format!("/// {trimmed}\n"));
} else {
output.push_str(&format!("/// {trimmed}\n"));
}
}
output.trim_end().to_string()
}
fn merge_sections(&self, sections: &[Section]) -> String {
let mut header_content = String::new();
let mut imports_content = String::new();
let mut body_content = String::new();
for section in sections {
match section {
Section::Header(content) => header_content.push_str(content),
Section::Imports(content) => imports_content.push_str(content),
Section::Body(content) => body_content.push_str(content),
}
}
let mut output = String::new();
if !header_content.is_empty() {
output.push_str(&header_content);
if !output.ends_with('\n') {
output.push('\n');
}
}
if !imports_content.is_empty() {
let imports_trimmed = imports_content.trim();
if !imports_trimmed.is_empty() {
output.push('\n');
output.push_str(imports_trimmed);
}
}
if !body_content.is_empty() {
let body_trimmed = body_content.trim();
if !body_trimmed.is_empty() {
output.push('\n');
output.push('\n');
output.push_str(body_trimmed);
}
}
if !output.ends_with('\n') {
output.push('\n');
}
output
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_header_contains_notice() {
let formatter = RustFormatter::new();
let metadata = HeaderMetadata {
auto_generated: true,
schema_file: None,
generator_version: None,
};
let header = formatter.format_header(&metadata);
assert!(header.contains("DO NOT EDIT"));
assert!(header.contains("//!"));
}
#[test]
fn test_format_header_with_metadata() {
let formatter = RustFormatter::new();
let metadata = HeaderMetadata {
auto_generated: true,
schema_file: Some("schema.graphql".to_string()),
generator_version: Some("0.6.2".to_string()),
};
let header = formatter.format_header(&metadata);
assert!(header.contains("schema.graphql"));
assert!(header.contains("0.6.2"));
}
#[test]
fn test_format_imports_empty() {
let formatter = RustFormatter::new();
let imports = vec![];
let result = formatter.format_imports(&imports);
assert!(result.is_empty());
}
#[test]
fn test_format_imports_grouped() {
let formatter = RustFormatter::new();
let imports = vec![
Import::new("crate::models"),
Import::new("std::collections"),
Import::new("serde"),
];
let result = formatter.format_imports(&imports);
let std_pos = result.find("std::").unwrap();
let serde_pos = result.find("serde").unwrap();
let crate_pos = result.find("crate::").unwrap();
assert!(std_pos < serde_pos);
assert!(serde_pos < crate_pos);
}
#[test]
fn test_format_imports_with_items() {
let formatter = RustFormatter::new();
let imports = vec![Import::with_items("std::collections", vec!["HashMap", "BTreeMap"])];
let result = formatter.format_imports(&imports);
assert!(result.contains("use std::collections::{"));
assert!(result.contains("HashMap"));
assert!(result.contains("BTreeMap"));
}
#[test]
fn test_format_docstring() {
let formatter = RustFormatter::new();
let content = "This is documentation\nWith multiple lines";
let result = formatter.format_docstring(content);
assert!(result.contains("///"));
assert!(result.contains("This is documentation"));
assert!(result.contains("With multiple lines"));
}
#[test]
fn test_format_docstring_with_code() {
let formatter = RustFormatter::new();
let content = "Example usage:\n```rust\nlet x = 5;\n```";
let result = formatter.format_docstring(content);
assert!(result.contains("```rust"));
assert!(result.contains("let x = 5;"));
}
#[test]
fn test_merge_sections_proper_spacing() {
let formatter = RustFormatter::new();
let sections = vec![
Section::Header("// DO NOT EDIT\n".to_string()),
Section::Imports("use std::collections::HashMap;\n".to_string()),
Section::Body("fn main() {}".to_string()),
];
let result = formatter.merge_sections(§ions);
assert!(result.contains("// DO NOT EDIT"));
assert!(result.contains("use std::"));
assert!(result.contains("fn main"));
assert!(result.ends_with('\n'));
}
#[test]
fn test_merge_sections_ends_with_newline() {
let formatter = RustFormatter::new();
let sections = vec![Section::Body("fn test() {}".to_string())];
let result = formatter.merge_sections(§ions);
assert!(result.ends_with('\n'));
}
#[test]
fn test_merge_sections_combines_multiple_bodies() {
let formatter = RustFormatter::new();
let sections = vec![
Section::Body("struct MyStruct {}\n".to_string()),
Section::Body("fn my_function() {}".to_string()),
];
let result = formatter.merge_sections(§ions);
assert!(result.contains("struct MyStruct"));
assert!(result.contains("fn my_function"));
}
}