Add output.rs module
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-01-31 11:41:09 +00:00
parent 45aaf9a67f
commit b0fe458ac4

107
src/output.rs Normal file
View File

@@ -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<String>,
pub description: String, pub confidence: f64, pub file_count: usize,
pub files: Vec<JsonFileChange>,
}
#[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<dyn OutputFormatter> {
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::<ShortFormatter>());
assert!(get_formatter(&OutputFormat::Json).is::<JsonFormatter>());
}
}