diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..1accdd2 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,107 @@ +use serde::Serialize; +use crate::convention::CommitSuggestion; +use crate::analyzer::ChangedFile; + +#[derive(Debug, Clone, Serialize)] +pub enum OutputFormat { Short, Verbose, Json, Compact } + +#[derive(Debug, Clone, Serialize)] +pub struct JsonOutput { + pub prefix: String, pub commit_type: String, pub scope: Option, + pub description: String, pub confidence: f64, pub file_count: usize, + pub files: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct JsonFileChange { pub path: String, pub status: String, pub additions: usize, pub deletions: usize; } + +pub trait OutputFormatter { fn format(&self, suggestion: &CommitSuggestion, files: &[ChangedFile]) -> String; } + +pub struct ShortFormatter; +impl OutputFormatter for ShortFormatter { + fn format(&self, suggestion: &CommitSuggestion, _files: &[ChangedFile]) -> String { + match &suggestion.scope { + Some(scope) => format!("{}({}): {}", suggestion.commit_type, scope, suggestion.description), + None => format!("{}: {}", suggestion.commit_type, suggestion.description), + } + } +} + +pub struct VerboseFormatter; +impl OutputFormatter for VerboseFormatter { + fn format(&self, suggestion: &CommitSuggestion, files: &[ChangedFile]) -> String { + let mut output = String::new(); + output.push_str(&format!("Commit Type: {}\n", suggestion.commit_type)); + if let Some(scope) = &suggestion.scope { output.push_str(&format!("Scope: {}\n", scope)); } + output.push_str(&format!("Description: {}\n", suggestion.description)); + output.push_str(&format!("Confidence: {:.0}%\n", suggestion.confidence * 100.0)); + output.push_str(&format!("Files Changed: {}\n", suggestion.file_count)); + output.push_str("\nChanged Files:\n"); + for file in files { output.push_str(&format!(" [{}] {}\n", file.status_short(), file.path)); } + output + } +} + +pub struct CompactFormatter; +impl OutputFormatter for CompactFormatter { + fn format(&self, suggestion: &CommitSuggestion, _files: &[ChangedFile]) -> String { + match &suggestion.scope { + Some(scope) => format!("{}{}{}", suggestion.commit_type, scope, suggestion.description), + None => format!("{}{}", suggestion.commit_type, suggestion.description), + } + } +} + +pub struct JsonFormatter; +impl OutputFormatter for JsonFormatter { + fn format(&self, suggestion: &CommitSuggestion, files: &[ChangedFile]) -> String { + let json_output = JsonOutput { + prefix: match &suggestion.scope { + Some(scope) => format!("{}({}): {}", suggestion.commit_type, scope, suggestion.description), + None => format!("{}: {}", suggestion.commit_type, suggestion.description), + }, + commit_type: suggestion.commit_type.to_string(), + scope: suggestion.scope.as_ref().map(|s| s.name.clone()), + description: suggestion.description.clone(), confidence: suggestion.confidence, + file_count: suggestion.file_count, + files: files.iter().map(|f| JsonFileChange { path: f.path.clone(), status: f.status_short().to_string(), additions: f.additions, deletions: f.deletions }).collect(), + }; + serde_json::to_string_pretty(&json_output).unwrap_or_else(|_| String::from("{}")) + } +} + +pub fn get_formatter(format: &OutputFormat) -> Box { + match format { + OutputFormat::Short => Box::new(ShortFormatter), + OutputFormat::Verbose => Box::new(VerboseFormatter), + OutputFormat::Compact => Box::new(CompactFormatter), + OutputFormat::Json => Box::new(JsonFormatter), + } +} + +impl ChangedFile { + pub fn status_short(&self) -> char { + match self.status { + super::analyzer::ChangeStatus::Added => 'A', super::analyzer::ChangeStatus::Deleted => 'D', + super::analyzer::ChangeStatus::Modified => 'M', super::analyzer::ChangeStatus::Renamed => 'R', + super::analyzer::ChangeStatus::Copied => 'C', super::analyzer::ChangeStatus::TypeChanged => 'T', + super::analyzer::ChangeStatus::Untracked => '?', + } + } +} + +#[cfg(test)] mod tests { + use super::*; use crate::convention::{Scope, CommitSuggestion}; use crate::analyzer::ChangeStatus; + #[test] fn test_short_formatter() { + let suggestion = CommitSuggestion { commit_type: crate::convention::CommitType::Feat, scope: Some(Scope::new("auth".to_string())), description: "add login".to_string(), confidence: 0.9, file_count: 2 }; + assert_eq!(ShortFormatter.format(&suggestion, &[]), "feat(auth): add login"); + } + #[test] fn test_short_formatter_no_scope() { + let suggestion = CommitSuggestion { commit_type: crate::convention::CommitType::Fix, scope: None, description: "fix bug".to_string(), confidence: 0.9, file_count: 1 }; + assert_eq!(ShortFormatter.format(&suggestion, &[]), "fix: fix bug"); + } + #[test] fn test_get_formatter() { + assert!(get_formatter(&OutputFormat::Short).is::()); + assert!(get_formatter(&OutputFormat::Json).is::()); + } +}