1use std::{
2 borrow::Cow,
3 fmt,
4 fmt::{Display, Formatter},
5 str::Chars,
6};
7
8use crate::{body::Body, fragment::Fragment};
9
10#[derive(Debug, PartialEq, Eq, Clone, Default)]
12pub struct Subject<'a> {
13 text: Cow<'a, str>,
14}
15
16impl Subject<'_> {
17 #[must_use]
28 pub fn len(&self) -> usize {
29 self.text.len()
30 }
31
32 #[must_use]
43 pub fn is_empty(&self) -> bool {
44 self.text.is_empty()
45 }
46
47 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 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}