Skip to main content

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.chars().count()
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_subject_length_counts_unicode_characters_not_bytes() {
160        assert_eq!(
161            Subject::from("café").len(),
162            4,
163            "Subject length should count Unicode characters, not bytes"
164        );
165        assert_eq!(
166            Subject::from("über").len(),
167            4,
168            "Subject length should count Unicode characters, not bytes"
169        );
170        assert_eq!(
171            Subject::from("日本語").len(),
172            3,
173            "Subject length should count Unicode characters, not bytes"
174        );
175    }
176
177    #[test]
178    fn test_chars_iterator_returns_correct_unicode_characters() {
179        let subject = Subject::from("y\u{306}");
180
181        let mut chars = subject.chars();
182
183        assert_eq!(Some('y'), chars.next(), "First character should be 'y'");
184        assert_eq!(
185            Some('\u{0306}'),
186            chars.next(),
187            "Second character should be the combining breve (U+0306)"
188        );
189        assert_eq!(
190            None,
191            chars.next(),
192            "Iterator should be exhausted after two characters"
193        );
194    }
195
196    #[test]
197    fn test_is_empty_returns_correct_boolean_value() {
198        assert!(
199            !Subject::from("hello, world!").is_empty(),
200            "Non-empty subject should return false for is_empty()"
201        );
202        assert!(
203            Subject::from("").is_empty(),
204            "Empty subject should return true for is_empty()"
205        );
206    }
207
208    #[test]
209    fn test_display_trait_formats_subject_correctly() {
210        let _subject = String::from(Subject::from("hello, world!"));
211
212        assert_eq!(
213            format!("{}", Subject::from("hello, world!")),
214            String::from("hello, world!"),
215            "Display implementation should format the subject as a plain string"
216        );
217    }
218
219    #[test]
220    fn test_from_str_creates_valid_subject() {
221        let subject = String::from(Subject::from("hello, world!"));
222
223        assert_eq!(
224            subject,
225            String::from("hello, world!"),
226            "Subject created from &str should convert back to the original string"
227        );
228    }
229
230    #[test]
231    fn test_from_string_creates_valid_subject() {
232        let subject = String::from(Subject::from(String::from("hello, world!")));
233
234        assert_eq!(
235            subject,
236            String::from("hello, world!"),
237            "Subject created from String should convert back to the original string"
238        );
239    }
240
241    #[test]
242    fn test_from_body_creates_equivalent_subject() {
243        let subject = Subject::from(Body::from("hello, world!"));
244
245        assert_eq!(
246            subject,
247            Subject::from("hello, world!"),
248            "Subject created from Body should be equivalent to Subject created from the same string"
249        );
250    }
251
252    #[test]
253    fn test_from_fragments_extracts_first_body_as_subject() {
254        let subject = Subject::from(vec![Fragment::Body(Body::from("hello, world!"))]);
255
256        assert_eq!(
257            subject,
258            Subject::from("hello, world!"),
259            "Subject created from fragments should extract the first Body fragment"
260        );
261    }
262
263    #[test]
264    fn test_from_cow_creates_valid_subject() {
265        let subject = Subject::from(Cow::from("hello, world!"));
266
267        assert_eq!(
268            subject,
269            Subject::from("hello, world!"),
270            "Subject created from Cow should be equivalent to Subject created from the same string"
271        );
272    }
273
274    #[test]
275    fn test_from_fragments_skips_comments_when_extracting_subject() {
276        let subject = Subject::from(vec![
277            Fragment::Comment(Comment::from("# Important Comment")),
278            Fragment::Body(Body::from("hello, world!")),
279        ]);
280
281        assert_eq!(
282            subject,
283            Subject::from("hello, world!"),
284            "Subject created from fragments should skip Comment fragments and use the first Body fragment"
285        );
286    }
287}