knope_versioning/release_notes/
config.rs

1use std::{fmt, fmt::Display};
2
3use git_conventional::FooterToken;
4use serde::{Deserialize, Serialize};
5
6use crate::changes::ChangeType;
7
8/// Where a custom release section comes from, for example, the custom change type "extra" in
9/// a change file might correspond to a section called "Extras" in the changelog.
10#[derive(Clone, Debug, Hash, Eq, PartialEq)]
11pub enum SectionSource {
12    CommitFooter(CommitFooter),
13    CustomChangeType(CustomChangeType),
14}
15
16impl From<CommitFooter> for SectionSource {
17    fn from(footer: CommitFooter) -> Self {
18        Self::CommitFooter(footer)
19    }
20}
21
22impl From<CustomChangeType> for SectionSource {
23    fn from(change_type: CustomChangeType) -> Self {
24        Self::CustomChangeType(change_type)
25    }
26}
27
28impl Display for SectionSource {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            Self::CommitFooter(footer) => footer.fmt(f),
32            Self::CustomChangeType(change_type) => change_type.fmt(f),
33        }
34    }
35}
36
37/// A non-standard conventional commit type (e.g., the "doc" in "doc: some message")
38/// or a non-standard changeset type ([`changesets::ChangeType::Custom`]).
39#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
40#[serde(transparent)]
41pub struct CustomChangeType(String);
42
43impl PartialEq<str> for CustomChangeType {
44    fn eq(&self, other: &str) -> bool {
45        self.0 == other
46    }
47}
48
49#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
50#[serde(transparent)]
51pub struct CommitFooter(String);
52
53impl Display for CommitFooter {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        write!(f, "{}", self.0)
56    }
57}
58
59impl From<&str> for CommitFooter {
60    fn from(token: &str) -> Self {
61        Self(token.into())
62    }
63}
64
65impl From<FooterToken<'_>> for CommitFooter {
66    fn from(token: FooterToken) -> Self {
67        Self(token.as_str().into())
68    }
69}
70
71impl Display for CustomChangeType {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        write!(f, "{}", self.0)
74    }
75}
76
77impl From<String> for CustomChangeType {
78    fn from(token: String) -> Self {
79        Self(token)
80    }
81}
82
83impl From<&str> for CustomChangeType {
84    fn from(token: &str) -> Self {
85        Self(token.into())
86    }
87}
88
89impl From<CustomChangeType> for String {
90    fn from(custom: CustomChangeType) -> Self {
91        custom.0
92    }
93}
94
95#[derive(Clone, Debug, Eq, PartialEq)]
96pub struct Sections(pub Vec<(SectionName, Vec<ChangeType>)>);
97
98impl Sections {
99    #[must_use]
100    pub fn defaults() -> Vec<(SectionName, ChangeType)> {
101        vec![
102            (SectionName::from("Breaking Changes"), ChangeType::Breaking),
103            (SectionName::from("Features"), ChangeType::Feature),
104            (SectionName::from("Fixes"), ChangeType::Fix),
105            ("Notes".into(), CommitFooter::from("Changelog-Note").into()),
106        ]
107    }
108
109    pub fn iter(&self) -> impl Iterator<Item = &(SectionName, Vec<ChangeType>)> {
110        self.0.iter()
111    }
112
113    #[allow(dead_code)]
114    pub(crate) fn is_default(&self) -> bool {
115        let defaults = Self::defaults();
116        self.0.iter().enumerate().all(|(index, (name, sources))| {
117            if sources.len() != 1 {
118                return false;
119            }
120            sources.first().is_some_and(|source| {
121                defaults
122                    .get(index)
123                    .is_some_and(|(default_name, default_source)| {
124                        name == default_name && source == default_source
125                    })
126            })
127        })
128    }
129
130    pub(crate) fn contains_footer(&self, footer: &git_conventional::Footer) -> bool {
131        self.0.iter().any(|(_, sources)| {
132            sources.iter().any(|source| match source {
133                ChangeType::Custom(SectionSource::CommitFooter(footer_token)) => {
134                    footer_token.0.eq_ignore_ascii_case(footer.token().as_str())
135                }
136                _ => false,
137            })
138        })
139    }
140}
141
142impl Default for Sections {
143    fn default() -> Self {
144        Self(
145            Self::defaults()
146                .into_iter()
147                .map(|(name, source)| (name, vec![source]))
148                .collect(),
149        )
150    }
151}
152
153impl IntoIterator for Sections {
154    type Item = (SectionName, Vec<ChangeType>);
155    type IntoIter = std::vec::IntoIter<Self::Item>;
156
157    fn into_iter(self) -> Self::IntoIter {
158        self.0.into_iter()
159    }
160}
161
162#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
163#[serde(transparent)]
164pub struct SectionName(String);
165
166impl Display for SectionName {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(f, "{}", self.0)
169    }
170}
171
172impl From<&str> for SectionName {
173    fn from(token: &str) -> Self {
174        Self(token.into())
175    }
176}
177
178impl AsRef<str> for SectionName {
179    fn as_ref(&self) -> &str {
180        &self.0
181    }
182}