use crate::catalog::language::Language;
use crate::catalog::models::{author::Author, opus::Opus};
#[non_exhaustive]
pub enum Attribution {
Both {
author: Author,
opus: Opus,
},
AuthorOnly(Author),
OpusOnly(Opus),
}
impl Attribution {
#[must_use]
pub fn from_parts(
forename: Option<&str>,
surname: Option<&str>,
title: Option<&str>,
) -> Option<Self> {
let given = forename.map(str::trim).filter(|n| !n.is_empty());
let family = surname.map(str::trim).filter(|n| !n.is_empty());
let author = match (given, family) {
(Some(g), Some(f)) => Some(Author::new(g, f)),
(None, Some(f)) => Some(Author::surname_only(f)),
(Some(g), None) => Some(Author::forename_only(g)),
(None, None) => None,
};
let opus = title
.map(str::trim)
.filter(|t| !t.is_empty())
.map(Opus::new);
match (author, opus) {
(Some(author), Some(opus)) => Some(Attribution::Both { author, opus }),
(Some(author), None) => Some(Attribution::AuthorOnly(author)),
(None, Some(opus)) => Some(Attribution::OpusOnly(opus)),
(None, None) => None,
}
}
#[must_use]
pub fn with_article_language(self, language: Option<Language>) -> Self {
match self {
Attribution::Both { author, opus } => Attribution::Both {
author,
opus: opus.with_article_language(language),
},
Attribution::OpusOnly(opus) => {
Attribution::OpusOnly(opus.with_article_language(language))
}
Attribution::AuthorOnly(author) => Attribution::AuthorOnly(author),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_parts_selects_the_right_variant() {
assert!(matches!(
Attribution::from_parts(Some("Charles"), Some("Dickens"), Some("Oliver Twist")),
Some(Attribution::Both { .. })
));
assert!(matches!(
Attribution::from_parts(None, Some("Dickens"), None),
Some(Attribution::AuthorOnly(_))
));
assert!(matches!(
Attribution::from_parts(None, None, Some("Bear")),
Some(Attribution::OpusOnly(_))
));
assert!(Attribution::from_parts(None, None, None).is_none());
}
#[test]
fn blank_parts_are_absent() {
let Some(Attribution::AuthorOnly(author)) =
Attribution::from_parts(Some("Prince"), Some(" "), None)
else {
panic!("expected an author-only attribution");
};
assert_eq!(author.forename(), Some("Prince"));
assert_eq!(author.surname(), None);
assert!(Attribution::from_parts(Some(""), Some(" "), Some("\t")).is_none());
}
#[test]
fn parts_are_trimmed() {
let Some(Attribution::Both { author, opus }) =
Attribution::from_parts(Some(" Charles "), Some(" Dickens"), Some(" Oliver "))
else {
panic!("expected a both attribution");
};
assert_eq!(author.forename(), Some("Charles"));
assert_eq!(author.surname(), Some("Dickens"));
assert_eq!(opus.title(), "Oliver");
}
fn article_language(entry: &Attribution) -> Option<Language> {
match entry {
Attribution::Both { opus, .. } | Attribution::OpusOnly(opus) => opus.article_language(),
Attribution::AuthorOnly(_) => None,
}
}
#[test]
fn article_language_reaches_the_title() {
let english = Some(Language::English);
for entry in [
Attribution::from_parts(None, Some("Dickens"), Some("A Christmas Carol")),
Attribution::from_parts(None, None, Some("A Christmas Carol")),
] {
let entry = entry.map(|entry| entry.with_article_language(english));
let Some(Attribution::Both { opus, .. } | Attribution::OpusOnly(opus)) = entry else {
panic!("expected a title");
};
assert_eq!(opus.article_language(), english);
assert_eq!(opus.filing_title(), "Christmas Carol");
}
}
#[test]
fn last_article_language_wins() {
for entry in [
Attribution::Both {
author: Author::surname_only("Faulkner"),
opus: Opus::new("The Bear"),
},
Attribution::OpusOnly(Opus::new("The Bear")),
] {
let entry = entry
.with_article_language(Some(Language::French))
.with_article_language(Some(Language::English));
assert_eq!(article_language(&entry), Some(Language::English));
let entry = entry.with_article_language(None);
assert_eq!(article_language(&entry), None);
}
}
#[test]
fn author_names_are_never_stripped() {
let entry =
Attribution::from_parts(Some("Jean de"), Some("La Fontaine"), Some("Les Fables"))
.map(|entry| entry.with_article_language(Some(Language::French)));
let Some(Attribution::Both { author, opus }) = entry else {
panic!("expected a both attribution");
};
assert_eq!(author.forename(), Some("Jean de"));
assert_eq!(author.surname(), Some("La Fontaine"));
assert_eq!(opus.filing_title(), "Fables");
let author_only = Attribution::AuthorOnly(Author::surname_only("La Fontaine"))
.with_article_language(Some(Language::French));
let Attribution::AuthorOnly(kept) = author_only else {
panic!("expected an author-only attribution");
};
assert_eq!(kept.surname(), Some("La Fontaine"));
}
}