use crate::reference::contributor::Contributor;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[cfg(feature = "bindings")]
use specta::Type;
use std::borrow::Borrow;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::str::FromStr;
use url::Url;
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(transparent)]
pub struct RefID(pub String);
impl RefID {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for RefID {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for RefID {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Borrow<str> for RefID {
fn borrow(&self) -> &str {
self.as_str()
}
}
impl Deref for RefID {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl From<String> for RefID {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<&str> for RefID {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}
impl From<RefID> for String {
fn from(value: RefID) -> Self {
value.0
}
}
impl FromStr for RefID {
type Err = std::convert::Infallible;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(Self::from(value))
}
}
impl PartialEq<&str> for RefID {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<str> for RefID {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<String> for RefID {
fn eq(&self, other: &String) -> bool {
self.as_str() == other
}
}
impl PartialEq<RefID> for &str {
fn eq(&self, other: &RefID) -> bool {
*self == other.as_str()
}
}
impl PartialEq<RefID> for String {
fn eq(&self, other: &RefID) -> bool {
self.as_str() == other.as_str()
}
}
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(transparent)]
pub struct Place(
pub String,
);
impl Place {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for Place {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for Place {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Borrow<str> for Place {
fn borrow(&self) -> &str {
self.as_str()
}
}
impl Deref for Place {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl From<String> for Place {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<&str> for Place {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}
impl From<Place> for String {
fn from(value: Place) -> Self {
value.0
}
}
impl FromStr for Place {
type Err = std::convert::Infallible;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(Self::from(value))
}
}
impl PartialEq<&str> for Place {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<str> for Place {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<String> for Place {
fn eq(&self, other: &String) -> bool {
self.as_str() == other
}
}
impl PartialEq<Place> for &str {
fn eq(&self, other: &Place) -> bool {
*self == other.as_str()
}
}
impl PartialEq<Place> for String {
fn eq(&self, other: &Place) -> bool {
self.as_str() == other.as_str()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(untagged)]
pub enum RichText {
Plain(String),
Djot {
djot: String,
},
}
impl Default for RichText {
fn default() -> Self {
RichText::Plain(String::new())
}
}
impl RichText {
pub fn raw(&self) -> &str {
match self {
RichText::Plain(s) | RichText::Djot { djot: s } => s,
}
}
pub fn is_empty(&self) -> bool {
self.raw().is_empty()
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
pub struct Numbering {
pub r#type: NumberingType,
pub value: String,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "bindings", derive(Type))]
#[non_exhaustive]
pub enum NumberingType {
Volume,
Issue,
Number,
Report,
Part,
Supplement,
Printing,
Chapter,
Section,
Edition,
Season,
Episode,
Custom(String),
}
impl NumberingType {
#[must_use]
pub fn as_key(&self) -> Cow<'_, str> {
match self {
Self::Volume => Cow::Borrowed("volume"),
Self::Issue => Cow::Borrowed("issue"),
Self::Number => Cow::Borrowed("number"),
Self::Report => Cow::Borrowed("report"),
Self::Part => Cow::Borrowed("part"),
Self::Supplement => Cow::Borrowed("supplement"),
Self::Printing => Cow::Borrowed("printing"),
Self::Chapter => Cow::Borrowed("chapter"),
Self::Section => Cow::Borrowed("section"),
Self::Edition => Cow::Borrowed("edition"),
Self::Season => Cow::Borrowed("season"),
Self::Episode => Cow::Borrowed("episode"),
Self::Custom(value) => normalize_kind_key(value)
.map(Cow::Owned)
.unwrap_or_else(|| Cow::Borrowed(value.as_str())),
}
}
pub fn from_key(value: &str) -> Result<Self, String> {
let canonical = normalize_kind_key(value)
.ok_or_else(|| "numbering kind must not be empty".to_string())?;
Ok(match canonical.as_str() {
"volume" => Self::Volume,
"issue" => Self::Issue,
"number" => Self::Number,
"report" => Self::Report,
"part" => Self::Part,
"supplement" => Self::Supplement,
"printing" => Self::Printing,
"chapter" => Self::Chapter,
"section" => Self::Section,
"edition" => Self::Edition,
"season" => Self::Season,
"episode" => Self::Episode,
_ => Self::Custom(canonical),
})
}
}
impl PartialEq for NumberingType {
fn eq(&self, other: &Self) -> bool {
self.as_key().as_ref() == other.as_key().as_ref()
}
}
impl Eq for NumberingType {}
impl Hash for NumberingType {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_key().as_ref().hash(state);
}
}
impl Serialize for NumberingType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_key().as_ref())
}
}
impl<'de> Deserialize<'de> for NumberingType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::from_key(&value).map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "schema")]
impl JsonSchema for NumberingType {
fn schema_name() -> std::borrow::Cow<'static, str> {
"NumberingType".into()
}
fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"description": "Known numbering kind keyword or custom kebab-case identifier."
})
}
}
pub(crate) trait HasNumbering {
fn numbering(&self) -> &[Numbering];
fn find_numbering(&self, numbering_type: NumberingType) -> Option<String> {
self.numbering()
.iter()
.find(|numbering| numbering.r#type == numbering_type)
.map(|numbering| numbering.value.clone())
}
}
pub(crate) trait NormalizeNumbering {
fn numbering_mut(&mut self) -> &mut Vec<Numbering>;
fn volume_mut(&mut self) -> &mut Option<String>;
fn issue_mut(&mut self) -> &mut Option<String>;
fn edition_mut(&mut self) -> &mut Option<String>;
fn number_mut(&mut self) -> &mut Option<String>;
fn part_number_mut(&mut self) -> &mut Option<String>;
fn supplement_number_mut(&mut self) -> &mut Option<String>;
fn printing_number_mut(&mut self) -> &mut Option<String>;
fn normalize_numbering(&mut self) {
let volume = self.volume_mut().take();
let issue = self.issue_mut().take();
let edition = self.edition_mut().take();
let number = self.number_mut().take();
let part_number = self.part_number_mut().take();
let supplement_number = self.supplement_number_mut().take();
let printing_number = self.printing_number_mut().take();
let has_volume = volume.is_some();
let has_issue = issue.is_some();
let has_edition = edition.is_some();
let has_number = number.is_some();
let has_part = part_number.is_some();
let has_supplement = supplement_number.is_some();
let has_printing = printing_number.is_some();
let existing = std::mem::take(self.numbering_mut());
let mut normalized = Vec::with_capacity(existing.len() + 7);
let mut push_shorthand = |r#type, value: Option<String>| {
if let Some(value) = value {
normalized.push(Numbering { r#type, value });
}
};
push_shorthand(NumberingType::Volume, volume);
push_shorthand(NumberingType::Issue, issue);
push_shorthand(NumberingType::Edition, edition);
push_shorthand(NumberingType::Number, number);
push_shorthand(NumberingType::Part, part_number);
push_shorthand(NumberingType::Supplement, supplement_number);
push_shorthand(NumberingType::Printing, printing_number);
normalized.extend(existing.into_iter().filter(|entry| match entry.r#type {
NumberingType::Volume => !has_volume,
NumberingType::Issue => !has_issue,
NumberingType::Edition => !has_edition,
NumberingType::Number => !has_number,
NumberingType::Part => !has_part,
NumberingType::Supplement => !has_supplement,
NumberingType::Printing => !has_printing,
_ => true,
}));
*self.numbering_mut() = normalized;
}
}
fn normalize_kind_key(value: &str) -> Option<String> {
let mut normalized = String::new();
let mut pending_dash = false;
for ch in value.trim().chars() {
if ch.is_ascii_alphanumeric() {
if pending_dash && !normalized.is_empty() {
normalized.push('-');
}
normalized.push(ch.to_ascii_lowercase());
pending_dash = false;
} else if !normalized.is_empty() {
pending_dash = true;
}
}
if normalized.is_empty() {
None
} else {
Some(normalized)
}
}
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(transparent)]
pub struct LangID(pub String);
impl LangID {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for LangID {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for LangID {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Borrow<str> for LangID {
fn borrow(&self) -> &str {
self.as_str()
}
}
impl Deref for LangID {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl From<String> for LangID {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<&str> for LangID {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}
impl From<LangID> for String {
fn from(value: LangID) -> Self {
value.0
}
}
impl FromStr for LangID {
type Err = std::convert::Infallible;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(Self::from(value))
}
}
impl PartialEq<&str> for LangID {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<str> for LangID {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<String> for LangID {
fn eq(&self, other: &String) -> bool {
self.as_str() == other
}
}
impl PartialEq<LangID> for &str {
fn eq(&self, other: &LangID) -> bool {
*self == other.as_str()
}
}
impl PartialEq<LangID> for String {
fn eq(&self, other: &LangID) -> bool {
self.as_str() == other.as_str()
}
}
pub type FieldLanguageMap = HashMap<String, LangID>;
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(untagged)]
pub enum NumOrStr {
Number(i64),
Str(String),
}
impl Display for NumOrStr {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
Self::Number(i) => write!(f, "{}", i),
Self::Str(s) => write!(f, "{}", s),
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(untagged)]
pub enum MultilingualString {
Simple(String),
Complex(MultilingualComplex),
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
pub struct MultilingualComplex {
pub original: String,
pub lang: Option<LangID>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub transliterations: HashMap<String, String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub translations: HashMap<LangID, String>,
}
impl From<String> for MultilingualString {
fn from(s: String) -> Self {
Self::Simple(s)
}
}
impl From<&str> for MultilingualString {
fn from(s: &str) -> Self {
Self::Simple(s.to_string())
}
}
impl Display for MultilingualString {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
Self::Simple(s) => write!(f, "{}", s),
Self::Complex(c) => write!(f, "{}", c.original),
}
}
}
impl Default for MultilingualString {
fn default() -> Self {
Self::Simple(String::new())
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
pub struct ArchiveInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<MultilingualString>,
#[serde(skip_serializing_if = "Option::is_none")]
pub place: Option<Place>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collection: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collection_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub series: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub r#box: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub folder: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub item: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<Url>,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
pub struct EprintInfo {
pub id: String,
pub server: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub class: Option<String>,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
#[serde(from = "PublisherCompat")]
pub struct Publisher {
pub name: MultilingualString,
#[serde(skip_serializing_if = "Option::is_none")]
pub place: Option<Place>,
}
#[derive(Debug, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(untagged)]
enum PublisherCompat {
PublisherObject {
name: MultilingualString,
place: Option<Place>,
},
Contributor(Box<Contributor>),
Name(MultilingualString),
String(String),
}
impl From<PublisherCompat> for Publisher {
fn from(value: PublisherCompat) -> Self {
match value {
PublisherCompat::PublisherObject { name, place } => Self { name, place },
PublisherCompat::Contributor(contributor) => match *contributor {
Contributor::SimpleName(name) => Self {
name: name.name,
place: name.location,
},
Contributor::StructuredName(name) => Self {
name: format!("{} {}", name.given, name.family).into(),
place: None,
},
Contributor::Multilingual(name) => Self {
name: format!("{} {}", name.original.given, name.original.family).into(),
place: None,
},
Contributor::ContributorList(list) => Self {
name: list.to_string().into(),
place: None,
},
},
PublisherCompat::Name(name) => Self { name, place: None },
PublisherCompat::String(name) => Self {
name: name.into(),
place: None,
},
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(untagged)]
pub enum Title {
Single(String),
Structured(StructuredTitle),
Multilingual(MultilingualComplex),
Multi(Vec<(LangID, String)>),
MultiStructured(Vec<(LangID, StructuredTitle)>),
Shorthand(String, String),
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
pub struct StructuredTitle {
#[serde(skip_serializing_if = "Option::is_none")]
pub full: Option<String>,
pub main: String,
pub sub: Subtitle,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(untagged)]
pub enum Subtitle {
String(String),
Vector(Vec<String>),
}
impl fmt::Display for Title {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Title::Single(s) => write!(f, "{}", s),
Title::Multi(_m) => write!(f, "[multilingual title]"),
Title::Multilingual(m) => write!(f, "{}", m.original),
Title::Structured(s) => {
let subtitle = match &s.sub {
Subtitle::String(s) => s.clone(),
Subtitle::Vector(v) => v.join(", "),
};
write!(f, "{}: {}", s.main, subtitle)
}
Title::MultiStructured(_m) => write!(f, "[multilingual structured title]"),
Title::Shorthand(s, t) => write!(f, "{} ({})", s, t),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum RefDate {
Edtf(citum_edtf::Edtf),
Literal(String),
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::todo,
clippy::unimplemented,
clippy::unreachable,
clippy::get_unwrap,
reason = "Panicking is acceptable and often desired in tests."
)]
mod tests {
use super::{LangID, RefID};
#[cfg(feature = "schema")]
use schemars::schema_for;
use std::collections::HashMap;
#[test]
fn ref_id_round_trips_as_a_scalar_string() {
let id = RefID::from("kuhn1962");
let serialized = serde_json::to_string(&id).expect("ref id should serialize");
let deserialized: RefID =
serde_json::from_str(&serialized).expect("ref id should deserialize");
assert_eq!(serialized, "\"kuhn1962\"");
assert_eq!(deserialized, "kuhn1962");
assert_eq!(deserialized.as_ref(), "kuhn1962");
assert_eq!(String::from(deserialized), "kuhn1962");
}
#[test]
fn lang_id_supports_string_lookup_and_parsing() {
let lang = "en-US".parse::<LangID>().expect("lang id should parse");
let mut translations = HashMap::new();
translations.insert(lang.clone(), "English".to_string());
assert_eq!(lang, "en-US");
assert_eq!(
translations.get("en-US").map(String::as_str),
Some("English")
);
}
#[cfg(feature = "schema")]
#[test]
fn newtypes_stay_string_shaped_in_json_schema() {
let ref_schema = schema_for!(RefID);
let lang_schema = schema_for!(LangID);
let place_schema = schema_for!(super::Place);
assert_eq!(ref_schema.to_value()["type"], "string");
assert_eq!(lang_schema.to_value()["type"], "string");
assert_eq!(place_schema.to_value()["type"], "string");
}
}