spikard-cli 0.16.1

Command-line interface for building and validating Spikard applications
Documentation
//! Rust code formatter for generated code output
//!
//! Formats Rust code according to Rust 2024 edition standards with support for:
//! - Auto-generation notices and module-level rustdoc
//! - Organized imports grouped by stdlib, external crates, and internal crates
//! - Rustdoc documentation with proper markdown formatting and examples
//! - Proper item ordering: imports → types → functions → tests
//! - Rustfmt-compatible spacing and formatting
//!
//! # Design
//!
//! This formatter ensures generated Rust code maintains consistency with:
//! - Clear auto-generation markers for tooling integration
//! - Crate-level attributes for allow/deny rules
//! - Module-level rustdoc with `//!` format
//! - Item documentation with `///` format
//! - Alphabetically sorted imports within groups
//! - Single blank line between items, double blank lines between major sections
//!
//! # Example
//!
//! ```no_run
//! use spikard_cli::codegen::formatters::{Formatter, Import, HeaderMetadata, RustFormatter};
//!
//! 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("DO NOT EDIT"));
//! assert!(header.contains("//!"));
//! ```

use super::{Formatter, HeaderMetadata, Import, Section};

/// Rust code formatter implementing Rust 2024 edition standards
///
/// This formatter generates Rust code that adheres to the latest Rust edition,
/// ensuring consistency across the spikard toolkit. It handles proper import
/// organization, rustdoc comments, and item ordering according to Rust conventions.
#[derive(Debug, Clone)]
pub struct RustFormatter;

impl RustFormatter {
    /// Create a new Rust formatter instance
    #[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();

        // Auto-generation notice comment
        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"));
        }

        // Module-level rustdoc
        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();
        }

        // Classify imports into groups
        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());
            }
        }

        // Sort each group alphabetically
        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();

        // Helper function to format import group
        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 {
                    // Combine multiple items from same module
                    let items = import.items.join(", ");
                    group.push_str(&format!("use {}::{{{} }};\n", import.module, items));
                }
            }
            group
        };

        // Add stdlib imports
        if !stdlib.is_empty() {
            output.push_str(&format_group(&stdlib));
        }

        // Add external imports
        if !external.is_empty() {
            if !stdlib.is_empty() {
                output.push('\n');
            }
            output.push_str(&format_group(&external));
        }

        // Add internal imports
        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("### ") {
                // Markdown headers
                output.push_str(&format!("/// {trimmed}\n"));
            } else if trimmed.starts_with("```") {
                // Code block markers
                output.push_str(&format!("/// {trimmed}\n"));
            } else {
                // Regular documentation
                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();

        // Separate sections
        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();

        // Add header
        if !header_content.is_empty() {
            output.push_str(&header_content);
            if !output.ends_with('\n') {
                output.push('\n');
            }
        }

        // Add imports with double blank line spacing
        if !imports_content.is_empty() {
            let imports_trimmed = imports_content.trim();
            if !imports_trimmed.is_empty() {
                output.push('\n');
                output.push_str(imports_trimmed);
            }
        }

        // Add body with double blank line spacing
        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);
            }
        }

        // Ensure trailing newline
        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);

        // Check that std comes before external which comes before crate
        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(&sections);

        // Should have proper blank line spacing
        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(&sections);

        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(&sections);

        assert!(result.contains("struct MyStruct"));
        assert!(result.contains("fn my_function"));
    }
}