mit_commit/
subject.rs

1use std::{
2    borrow::Cow,
3    fmt,
4    fmt::{Display, Formatter},
5    str::Chars,
6};
7
8use crate::{body::Body, fragment::Fragment};
9
10/// The [`Subject`] from the [`crate::CommitMessage`]
11#[derive(Debug, PartialEq, Eq, Clone, Default)]
12pub struct Subject<'a> {
13    text: Cow<'a, str>,
14}
15
16impl Subject<'_> {
17    /// Count characters in [`Self`]
18    ///
19    /// # Examples
20    ///
21    /// ```
22    /// use mit_commit::Subject;
23    ///
24    /// assert_eq!(Subject::from("hello, world!").len(), 13);
25    /// assert_eq!(Subject::from("goodbye").len(), 7)
26    /// ```
27    #[must_use]
28    pub fn len(&self) -> usize {
29        self.text.len()
30    }
31
32    /// Is the [`Self`] empty
33    ///
34    /// # Examples
35    ///
36    /// ```
37    /// use mit_commit::Subject;
38    ///
39    /// assert_eq!(Subject::from("hello, world!").is_empty(), false);
40    /// assert_eq!(Subject::from("").is_empty(), true)
41    /// ```
42    #[must_use]
43    pub fn is_empty(&self) -> bool {
44        self.text.is_empty()
45    }
46
47    /// Convert the [`Self`] into chars
48    ///
49    /// # Examples
50    ///
51    /// ```
52    /// use mit_commit::Subject;
53    ///
54    /// let subject = Subject::from("y\u{306}");
55    ///
56    /// let mut chars = subject.chars();
57    ///
58    /// assert_eq!(Some('y'), chars.next());
59    /// assert_eq!(Some('\u{0306}'), chars.next());
60    ///
61    /// assert_eq!(None, chars.next());
62    /// ```
63    pub fn chars(&self) -> Chars<'_> {
64        self.text.chars()
65    }
66}
67
68impl<'a> From<&'a str> for Subject<'a> {
69    fn from(subject: &'a str) -> Self {
70        Self {
71            text: subject.into(),
72        }
73    }
74}
75
76impl From<String> for Subject<'_> {
77    /// Convert from an owned string
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use mit_commit::Subject;
83    ///
84    /// let subject = Subject::from("y\u{306}".to_string());
85    ///
86    /// let mut chars = subject.chars();
87    ///
88    /// assert_eq!(Some('y'), chars.next());
89    /// assert_eq!(Some('\u{0306}'), chars.next());
90    /// assert_eq!(None, chars.next());
91    /// ```
92    fn from(subject: String) -> Self {
93        Self {
94            text: subject.into(),
95        }
96    }
97}
98
99impl<'a> From<Cow<'a, str>> for Subject<'a> {
100    fn from(subject: Cow<'a, str>) -> Self {
101        Self { text: subject }
102    }
103}
104
105impl From<Subject<'_>> for String {
106    fn from(subject: Subject<'_>) -> Self {
107        subject.text.into_owned()
108    }
109}
110
111impl Display for Subject<'_> {
112    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
113        write!(f, "{}", String::from(self.clone()))
114    }
115}
116
117impl<'a> From<Body<'a>> for Subject<'a> {
118    fn from(body: Body<'_>) -> Self {
119        Self::from(String::from(body))
120    }
121}
122
123impl<'a> From<Vec<Fragment<'a>>> for Subject<'a> {
124    fn from(ast: Vec<Fragment<'a>>) -> Self {
125        ast.iter()
126            .find_map(|values| {
127                if let Fragment::Body(body) = values {
128                    Some(Self::from(body.clone()))
129                } else {
130                    None
131                }
132            })
133            .unwrap_or_default()
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use std::borrow::Cow;
140
141    use super::Subject;
142    use crate::{Comment, body::Body, fragment::Fragment};
143
144    #[test]
145    fn test_subject_length_returns_correct_character_count() {
146        assert_eq!(
147            Subject::from("hello, world!").len(),
148            13,
149            "Subject length should count all characters correctly"
150        );
151        assert_eq!(
152            Subject::from("goodbye").len(),
153            7,
154            "Subject length should count all characters correctly"
155        );
156    }
157
158    #[test]
159    fn test_chars_iterator_returns_correct_unicode_characters() {
160        let subject = Subject::from("y\u{306}");
161
162        let mut chars = subject.chars();
163
164        assert_eq!(Some('y'), chars.next(), "First character should be 'y'");
165        assert_eq!(
166            Some('\u{0306}'),
167            chars.next(),
168            "Second character should be the combining breve (U+0306)"
169        );
170        assert_eq!(
171            None,
172            chars.next(),
173            "Iterator should be exhausted after two characters"
174        );
175    }
176
177    #[test]
178    fn test_is_empty_returns_correct_boolean_value() {
179        assert!(
180            !Subject::from("hello, world!").is_empty(),
181            "Non-empty subject should return false for is_empty()"
182        );
183        assert!(
184            Subject::from("").is_empty(),
185            "Empty subject should return true for is_empty()"
186        );
187    }
188
189    #[test]
190    fn test_display_trait_formats_subject_correctly() {
191        let _subject = String::from(Subject::from("hello, world!"));
192
193        assert_eq!(
194            format!("{}", Subject::from("hello, world!")),
195            String::from("hello, world!"),
196            "Display implementation should format the subject as a plain string"
197        );
198    }
199
200    #[test]
201    fn test_from_str_creates_valid_subject() {
202        let subject = String::from(Subject::from("hello, world!"));
203
204        assert_eq!(
205            subject,
206            String::from("hello, world!"),
207            "Subject created from &str should convert back to the original string"
208        );
209    }
210
211    #[test]
212    fn test_from_string_creates_valid_subject() {
213        let subject = String::from(Subject::from(String::from("hello, world!")));
214
215        assert_eq!(
216            subject,
217            String::from("hello, world!"),
218            "Subject created from String should convert back to the original string"
219        );
220    }
221
222    #[test]
223    fn test_from_body_creates_equivalent_subject() {
224        let subject = Subject::from(Body::from("hello, world!"));
225
226        assert_eq!(
227            subject,
228            Subject::from("hello, world!"),
229            "Subject created from Body should be equivalent to Subject created from the same string"
230        );
231    }
232
233    #[test]
234    fn test_from_fragments_extracts_first_body_as_subject() {
235        let subject = Subject::from(vec![Fragment::Body(Body::from("hello, world!"))]);
236
237        assert_eq!(
238            subject,
239            Subject::from("hello, world!"),
240            "Subject created from fragments should extract the first Body fragment"
241        );
242    }
243
244    #[test]
245    fn test_from_cow_creates_valid_subject() {
246        let subject = Subject::from(Cow::from("hello, world!"));
247
248        assert_eq!(
249            subject,
250            Subject::from("hello, world!"),
251            "Subject created from Cow should be equivalent to Subject created from the same string"
252        );
253    }
254
255    #[test]
256    fn test_from_fragments_skips_comments_when_extracting_subject() {
257        let subject = Subject::from(vec![
258            Fragment::Comment(Comment::from("# Important Comment")),
259            Fragment::Body(Body::from("hello, world!")),
260        ]);
261
262        assert_eq!(
263            subject,
264            Subject::from("hello, world!"),
265            "Subject created from fragments should skip Comment fragments and use the first Body fragment"
266        );
267    }
268}