csan_core/catalog/models/
entry.rs1use crate::catalog::language::Language;
4use crate::catalog::models::{author::Author, opus::Opus};
5
6#[non_exhaustive]
18pub enum Attribution {
19 Both {
21 author: Author,
23 opus: Opus,
25 },
26 AuthorOnly(Author),
28 OpusOnly(Opus),
30}
31
32impl Attribution {
33 #[must_use]
55 pub fn from_parts(
56 forename: Option<&str>,
57 surname: Option<&str>,
58 title: Option<&str>,
59 ) -> Option<Self> {
60 let given = forename.map(str::trim).filter(|n| !n.is_empty());
61 let family = surname.map(str::trim).filter(|n| !n.is_empty());
62 let author = match (given, family) {
63 (Some(g), Some(f)) => Some(Author::new(g, f)),
64 (None, Some(f)) => Some(Author::surname_only(f)),
65 (Some(g), None) => Some(Author::forename_only(g)),
66 (None, None) => None,
67 };
68 let opus = title
69 .map(str::trim)
70 .filter(|t| !t.is_empty())
71 .map(Opus::new);
72
73 match (author, opus) {
74 (Some(author), Some(opus)) => Some(Attribution::Both { author, opus }),
75 (Some(author), None) => Some(Attribution::AuthorOnly(author)),
76 (None, Some(opus)) => Some(Attribution::OpusOnly(opus)),
77 (None, None) => None,
78 }
79 }
80
81 #[must_use]
101 pub fn with_article_language(self, language: Option<Language>) -> Self {
102 match self {
103 Attribution::Both { author, opus } => Attribution::Both {
104 author,
105 opus: opus.with_article_language(language),
106 },
107 Attribution::OpusOnly(opus) => {
108 Attribution::OpusOnly(opus.with_article_language(language))
109 }
110 Attribution::AuthorOnly(author) => Attribution::AuthorOnly(author),
111 }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn from_parts_selects_the_right_variant() {
121 assert!(matches!(
122 Attribution::from_parts(Some("Charles"), Some("Dickens"), Some("Oliver Twist")),
123 Some(Attribution::Both { .. })
124 ));
125 assert!(matches!(
126 Attribution::from_parts(None, Some("Dickens"), None),
127 Some(Attribution::AuthorOnly(_))
128 ));
129 assert!(matches!(
130 Attribution::from_parts(None, None, Some("Bear")),
131 Some(Attribution::OpusOnly(_))
132 ));
133 assert!(Attribution::from_parts(None, None, None).is_none());
134 }
135
136 #[test]
137 fn blank_parts_are_absent() {
138 let Some(Attribution::AuthorOnly(author)) =
140 Attribution::from_parts(Some("Prince"), Some(" "), None)
141 else {
142 panic!("expected an author-only attribution");
143 };
144 assert_eq!(author.forename(), Some("Prince"));
145 assert_eq!(author.surname(), None);
146
147 assert!(Attribution::from_parts(Some(""), Some(" "), Some("\t")).is_none());
148 }
149
150 #[test]
151 fn parts_are_trimmed() {
152 let Some(Attribution::Both { author, opus }) =
153 Attribution::from_parts(Some(" Charles "), Some(" Dickens"), Some(" Oliver "))
154 else {
155 panic!("expected a both attribution");
156 };
157 assert_eq!(author.forename(), Some("Charles"));
158 assert_eq!(author.surname(), Some("Dickens"));
159 assert_eq!(opus.title(), "Oliver");
160 }
161
162 fn article_language(entry: &Attribution) -> Option<Language> {
164 match entry {
165 Attribution::Both { opus, .. } | Attribution::OpusOnly(opus) => opus.article_language(),
166 Attribution::AuthorOnly(_) => None,
167 }
168 }
169
170 #[test]
171 fn article_language_reaches_the_title() {
172 let english = Some(Language::English);
173 for entry in [
174 Attribution::from_parts(None, Some("Dickens"), Some("A Christmas Carol")),
175 Attribution::from_parts(None, None, Some("A Christmas Carol")),
176 ] {
177 let entry = entry.map(|entry| entry.with_article_language(english));
178 let Some(Attribution::Both { opus, .. } | Attribution::OpusOnly(opus)) = entry else {
179 panic!("expected a title");
180 };
181 assert_eq!(opus.article_language(), english);
182 assert_eq!(opus.filing_title(), "Christmas Carol");
183 }
184 }
185
186 #[test]
187 fn last_article_language_wins() {
188 for entry in [
189 Attribution::Both {
190 author: Author::surname_only("Faulkner"),
191 opus: Opus::new("The Bear"),
192 },
193 Attribution::OpusOnly(Opus::new("The Bear")),
194 ] {
195 let entry = entry
196 .with_article_language(Some(Language::French))
197 .with_article_language(Some(Language::English));
198 assert_eq!(article_language(&entry), Some(Language::English));
199 let entry = entry.with_article_language(None);
200 assert_eq!(article_language(&entry), None);
201 }
202 }
203
204 #[test]
205 fn author_names_are_never_stripped() {
206 let entry =
208 Attribution::from_parts(Some("Jean de"), Some("La Fontaine"), Some("Les Fables"))
209 .map(|entry| entry.with_article_language(Some(Language::French)));
210 let Some(Attribution::Both { author, opus }) = entry else {
211 panic!("expected a both attribution");
212 };
213 assert_eq!(author.forename(), Some("Jean de"));
214 assert_eq!(author.surname(), Some("La Fontaine"));
215 assert_eq!(opus.filing_title(), "Fables");
216
217 let author_only = Attribution::AuthorOnly(Author::surname_only("La Fontaine"))
218 .with_article_language(Some(Language::French));
219 let Attribution::AuthorOnly(kept) = author_only else {
220 panic!("expected an author-only attribution");
221 };
222 assert_eq!(kept.surname(), Some("La Fontaine"));
223 }
224}