commit_wizard/domain/
commit.rs

1use crate::domain::errors::DomainError;
2use std::fmt::{self, Display};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum CommitType {
6    Feat,
7    Fix,
8    Docs,
9    Chore,
10    Refactor,
11    Test,
12    Perf,
13    Build,
14    Ci,
15    Style,
16}
17
18impl CommitType {
19    pub fn as_str(&self) -> &'static str {
20        match self {
21            CommitType::Feat => "feat",
22            CommitType::Fix => "fix",
23            CommitType::Docs => "docs",
24            CommitType::Chore => "chore",
25            CommitType::Refactor => "refactor",
26            CommitType::Test => "test",
27            CommitType::Perf => "perf",
28            CommitType::Build => "build",
29            CommitType::Ci => "ci",
30            CommitType::Style => "style",
31        }
32    }
33}
34impl Display for CommitType {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        f.write_str(self.as_str())
37    }
38}
39
40#[derive(Debug, Clone)]
41pub struct CommitMessage {
42    r#type: CommitType,
43    scope: Option<String>,
44    summary: String,
45    body: Option<String>,
46    breaking: bool,
47}
48
49impl CommitMessage {
50    pub fn new(
51        r#type: CommitType,
52        scope: Option<String>,
53        summary: String,
54        body: Option<String>,
55        breaking: bool,
56    ) -> Result<Self, DomainError> {
57        if summary.trim().is_empty() {
58            return Err(DomainError::EmptySummary);
59        }
60        if summary.chars().count() > 72 {
61            return Err(DomainError::SummaryTooLong(72));
62        }
63        Ok(Self {
64            r#type,
65            scope: scope.and_then(|s| {
66                let t = s.trim().to_string();
67                if t.is_empty() { None } else { Some(t) }
68            }),
69            summary: summary.trim().to_string(),
70            body: body.and_then(|b| {
71                let t = b.trim().to_string();
72                if t.is_empty() { None } else { Some(t) }
73            }),
74            breaking,
75        })
76    }
77
78    pub fn render(&self) -> String {
79        let header = match &self.scope {
80            Some(s) => format!("{}({}): {}", self.r#type, s, self.summary),
81            None => format!("{}: {}", self.r#type, self.summary),
82        };
83
84        let mut sections = Vec::new();
85        if self.breaking {
86            sections.push(String::from("BREAKING CHANGE: yes"));
87        }
88        if let Some(b) = &self.body {
89            sections.push(b.clone());
90        }
91
92        if sections.is_empty() {
93            header
94        } else {
95            format!("{header}\n\n{}", sections.join("\n\n"))
96        }
97    }
98}