use std::borrow::Cow;
use crate::catalog::normalization::{
NormalizationError, capitalize_ascii, normalize_mac, to_ascii_alphabetic,
};
#[derive(Debug, Clone, serde::Deserialize)]
pub struct Author {
#[serde(default)]
forename: Option<String>,
#[serde(default)]
surname: Option<String>,
}
impl Author {
#[must_use]
pub fn new(forename: impl Into<String>, surname: impl Into<String>) -> Self {
Self {
forename: Some(forename.into()),
surname: Some(surname.into()),
}
}
#[must_use]
pub fn surname_only(surname: impl Into<String>) -> Self {
Self {
forename: None,
surname: Some(surname.into()),
}
}
#[must_use]
pub fn forename_only(forename: impl Into<String>) -> Self {
Self {
forename: Some(forename.into()),
surname: None,
}
}
#[inline]
#[must_use]
pub fn forename(&self) -> Option<&str> {
self.forename.as_deref()
}
#[inline]
#[must_use]
pub fn surname(&self) -> Option<&str> {
self.surname.as_deref()
}
pub fn forename_normalized(&self) -> Result<Option<Cow<'_, str>>, NormalizationError> {
self.forename().map(Author::normalize_name).transpose()
}
pub fn surname_normalized(&self) -> Result<Option<Cow<'_, str>>, NormalizationError> {
self.surname().map(Author::normalize_name).transpose()
}
pub fn normalize_name(input: &str) -> Result<Cow<'_, str>, NormalizationError> {
let norm_mac = normalize_mac(Cow::Borrowed(input));
let normalized = to_ascii_alphabetic(norm_mac)?;
Ok(capitalize_ascii(normalized))
}
pub fn composed_normalized<T, U>(given: T, family: U) -> Result<String, NormalizationError>
where
T: AsRef<str>,
U: AsRef<str>,
{
let given = Author::normalize_name(given.as_ref())?;
let family = Author::normalize_name(family.as_ref())?;
Ok(Author::compose_parts(&family, &given))
}
#[must_use]
pub(crate) fn compose_parts(surname_normalized: &str, forename_normalized: &str) -> String {
match surname_normalized {
"Saint" | "Sainte" | "San" => format!("{surname_normalized} {forename_normalized}"),
"St" => format!("Saint {forename_normalized}"),
"Ste" => format!("Sainte {forename_normalized}"),
_ => format!("{surname_normalized}, {forename_normalized}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::catalog::normalization::NormalizationError;
use std::borrow::Cow;
mod constructor {
use super::*;
#[test]
fn new_with_full_name() {
let author = Author::new("John", "Doe");
assert_eq!(author.forename(), Some("John"));
assert_eq!(author.surname(), Some("Doe"));
}
#[test]
fn new_with_no_forename() {
let author = Author::surname_only("Plato");
assert_eq!(author.forename(), None);
assert_eq!(author.surname(), Some("Plato"));
}
#[test]
fn new_with_no_surname() {
let author = Author::forename_only("Prince");
assert_eq!(author.forename(), Some("Prince"));
assert_eq!(author.surname(), None);
}
#[test]
fn new_with_owned_strings() {
let f = String::from("John");
let l = String::from("Doe");
let author = Author::new(f, l);
assert_eq!(author.forename(), Some("John"));
assert_eq!(author.surname(), Some("Doe"));
}
#[test]
fn deserialized_empty_author_is_representable() {
let author: Author = serde_json::from_str("{}").unwrap();
assert_eq!(author.forename(), None);
assert_eq!(author.surname(), None);
}
}
mod instance_normalization {
use super::*;
#[test]
fn forename_normalized() {
let author = Author::new(" john ", "Doe");
let result = author.forename_normalized().unwrap();
assert_eq!(result, Some(Cow::Borrowed("John")));
}
#[test]
fn forename_none() {
let author = Author::surname_only("Doe");
let result = author.forename_normalized().unwrap();
assert_eq!(result, None);
}
#[test]
fn surname_normalized_standard() {
let author = Author::new("John", " doe ");
let result = author.surname_normalized().unwrap();
assert_eq!(result.as_deref(), Some("Doe"));
assert!(matches!(result, Some(Cow::Owned(_))));
}
#[test]
fn surname_none_returns_none() {
let author = Author::forename_only("Prince");
let result = author.surname_normalized().unwrap();
assert_eq!(result, None);
}
#[test]
fn mac_expansion_survives_untrimmed_input() {
let author = Author::new("Ronald", " mcdonald ");
let result = author.surname_normalized().unwrap();
assert_eq!(result.as_deref(), Some("Macdonald"));
}
}
mod static_composed {
use super::*;
#[test]
fn standard_composition() {
let res = Author::composed_normalized("john", "doe").unwrap();
assert_eq!(res, "Doe, John");
}
#[test]
fn trimming_and_casing() {
let res = Author::composed_normalized(" ALICE ", " wONDERLAND ").unwrap();
assert_eq!(res, "Wonderland, Alice");
}
#[test]
fn saint_formatting() {
let res = Author::composed_normalized("Paul", "Saint").unwrap();
assert_eq!(res, "Saint Paul");
}
#[test]
fn sainte_formatting() {
let res = Author::composed_normalized("Beuve", "Sainte").unwrap();
assert_eq!(res, "Sainte Beuve");
}
#[test]
fn san_formatting() {
let res = Author::composed_normalized("Francisco", "San").unwrap();
assert_eq!(res, "San Francisco");
}
#[test]
fn st_expansion() {
let res = Author::composed_normalized("Patrick", "st").unwrap();
assert_eq!(res, "Saint Patrick");
}
#[test]
fn ste_expansion() {
let res = Author::composed_normalized("Marie", "Ste.").unwrap();
assert_eq!(res, "Sainte Marie");
}
#[test]
fn composed_with_bad_input() {
let res = Author::composed_normalized("", "Doe");
assert!(matches!(res, Err(NormalizationError::EmptyString)));
}
}
}