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.chars().count()
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_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}