#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
crate::str_enum! {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TermForm {
Long = "long",
Short = "short",
Verb = "verb",
VerbShort = "verb-short",
Symbol = "symbol"
}
}
crate::str_enum! {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum GrammaticalGender {
Masculine = "masculine",
Feminine = "feminine",
Neuter = "neuter",
Common = "common"
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum MaybeGendered<T> {
Plain(T),
Gendered {
#[serde(skip_serializing_if = "Option::is_none")]
masculine: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
feminine: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
neuter: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
common: Option<T>,
},
}
impl<T: Default> Default for MaybeGendered<T> {
fn default() -> Self {
Self::Plain(T::default())
}
}
impl<T> From<T> for MaybeGendered<T> {
fn from(value: T) -> Self {
Self::Plain(value)
}
}
impl From<&str> for MaybeGendered<String> {
fn from(value: &str) -> Self {
Self::Plain(value.to_string())
}
}
impl<T> MaybeGendered<T> {
fn by_gender(&self, requested: GrammaticalGender) -> Option<&T> {
match self {
Self::Plain(value) => Some(value),
Self::Gendered {
masculine,
feminine,
neuter,
common,
} => match requested {
GrammaticalGender::Masculine => masculine.as_ref(),
GrammaticalGender::Feminine => feminine.as_ref(),
GrammaticalGender::Neuter => neuter.as_ref(),
GrammaticalGender::Common => common.as_ref(),
_ => None,
},
}
}
pub fn resolve_strict(&self, requested: Option<GrammaticalGender>) -> Option<&T> {
match self {
Self::Plain(value) => Some(value),
Self::Gendered { .. } => requested.and_then(|gender| self.by_gender(gender)),
}
}
pub fn resolve_with_fallback(&self, requested: Option<GrammaticalGender>) -> Option<&T> {
match self {
Self::Plain(value) => Some(value),
Self::Gendered {
masculine,
feminine,
neuter,
common,
} => requested
.and_then(|gender| self.by_gender(gender))
.or(common.as_ref())
.or(masculine.as_ref())
.or(feminine.as_ref())
.or(neuter.as_ref()),
}
}
pub fn resolve_neutral(&self) -> Option<&T> {
match self {
Self::Plain(value) => Some(value),
Self::Gendered { common, .. } => common.as_ref(),
}
}
}
impl MaybeGendered<String> {
pub fn as_default_str(&self) -> &str {
self.resolve_with_fallback(None)
.map(String::as_str)
.unwrap_or("")
}
pub fn is_empty(&self) -> bool {
self.as_default_str().is_empty()
}
pub fn as_str(&self) -> &str {
self.as_default_str()
}
pub fn to_lowercase(&self) -> String {
self.as_default_str().to_lowercase()
}
}
crate::str_enum! {
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub enum GeneralTerm {
#[default]
In = "in",
Accessed = "accessed",
Retrieved = "retrieved",
At = "at",
From = "from",
Of = "of",
To = "to",
By = "by",
NoDate = "no-date",
Anonymous = "anonymous",
Circa = "circa",
AvailableAt = "available-at",
Ibid = "ibid",
And = "and",
EtAl = "et-al",
AndOthers = "and-others",
Forthcoming = "forthcoming",
Online = "online",
Here = "here",
Deposited = "deposited",
ReviewOf = "review-of",
OriginalWorkPublished = "original-work-published",
Patent = "patent",
Volume = "volume",
Issue = "issue",
Page = "page",
Chapter = "chapter",
Edition = "edition",
Section = "section",
PersonalCommunication = "personal-communication"
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct Terms {
pub and: Option<String>,
pub and_symbol: Option<String>,
pub and_others: Option<String>,
#[serde(default)]
pub anonymous: SimpleTerm,
pub at: Option<String>,
pub accessed: Option<String>,
pub available_at: Option<String>,
pub by: Option<String>,
#[serde(default)]
pub circa: SimpleTerm,
pub et_al: Option<String>,
pub from: Option<String>,
pub ibid: Option<String>,
pub in_: Option<String>,
#[serde(skip_serializing)]
pub no_date: Option<String>,
pub retrieved: Option<String>,
#[serde(flatten, default)]
pub general: std::collections::HashMap<GeneralTerm, SimpleTerm>,
}
impl Terms {
pub fn en_us() -> Self {
Self {
and: Some("and".into()),
and_symbol: Some("&".into()),
and_others: Some("and others".into()),
anonymous: SimpleTerm {
long: "anonymous".into(),
short: "anon.".into(),
},
at: Some("at".into()),
accessed: Some("accessed".into()),
available_at: Some("available at".into()),
by: Some("by".into()),
circa: SimpleTerm {
long: "circa".into(),
short: "c.".into(),
},
et_al: Some("et al.".into()),
from: Some("from".into()),
ibid: Some("ibid.".into()),
in_: Some("in".into()),
no_date: Some("n.d.".into()),
retrieved: Some("retrieved".into()),
general: std::collections::HashMap::from([
(
GeneralTerm::NoDate,
SimpleTerm {
long: "no date".into(),
short: "n.d.".into(),
},
),
(
GeneralTerm::PersonalCommunication,
SimpleTerm {
long: "personal communication".into(),
short: "pers. comm.".into(),
},
),
]),
}
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct SimpleTerm {
pub long: MaybeGendered<String>,
pub short: MaybeGendered<String>,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct ContributorTerm {
pub singular: SimpleTerm,
pub plural: SimpleTerm,
pub verb: SimpleTerm,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct LocatorTerm {
#[serde(default)]
pub long: Option<SingularPlural>,
#[serde(default)]
pub short: Option<SingularPlural>,
#[serde(default)]
pub symbol: Option<SingularPlural>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gender: Option<GrammaticalGender>,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct SingularPlural {
pub singular: MaybeGendered<String>,
pub plural: MaybeGendered<String>,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct DateTerms {
#[serde(default)]
pub months: MonthNames,
#[serde(default)]
pub seasons: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uncertainty_term: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub open_ended_term: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub am: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pm: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timezone_utc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before_era: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ad: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bce: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ce: Option<String>,
}
impl DateTerms {
pub fn en_us() -> Self {
Self {
months: MonthNames::en_us(),
seasons: vec![
"Spring".into(),
"Summer".into(),
"Autumn".into(),
"Winter".into(),
],
uncertainty_term: Some("uncertain".into()),
open_ended_term: Some("present".into()),
am: Some("AM".into()),
pm: Some("PM".into()),
timezone_utc: Some("UTC".into()),
before_era: Some("BC".into()),
ad: Some("AD".into()),
bc: Some("BC".into()),
bce: Some("BCE".into()),
ce: Some("CE".into()),
}
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct MonthNames {
pub long: Vec<String>,
pub short: Vec<String>,
}
impl MonthNames {
pub fn en_us() -> Self {
Self {
long: vec![
"January".into(),
"February".into(),
"March".into(),
"April".into(),
"May".into(),
"June".into(),
"July".into(),
"August".into(),
"September".into(),
"October".into(),
"November".into(),
"December".into(),
],
short: vec![
"Jan.".into(),
"Feb.".into(),
"Mar.".into(),
"Apr.".into(),
"May".into(),
"June".into(),
"July".into(),
"Aug.".into(),
"Sept.".into(),
"Oct.".into(),
"Nov.".into(),
"Dec.".into(),
],
}
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct NumberFormats {
#[serde(default = "default_decimal_separator")]
pub decimal_separator: String,
#[serde(default = "default_thousands_separator")]
pub thousands_separator: String,
#[serde(default = "default_minimum_digits")]
pub minimum_digits: u8,
}
fn default_decimal_separator() -> String {
".".into()
}
fn default_thousands_separator() -> String {
",".into()
}
fn default_minimum_digits() -> u8 {
1
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct GrammarOptions {
#[serde(default)]
pub punctuation_in_quote: bool,
#[serde(default)]
pub nbsp_before_colon: bool,
#[serde(default = "default_open_quote")]
pub open_quote: String,
#[serde(default = "default_close_quote")]
pub close_quote: String,
#[serde(default = "default_open_inner_quote")]
pub open_inner_quote: String,
#[serde(default = "default_close_inner_quote")]
pub close_inner_quote: String,
#[serde(default)]
pub serial_comma: bool,
#[serde(default = "default_page_range_delimiter")]
pub page_range_delimiter: String,
}
impl Default for GrammarOptions {
fn default() -> Self {
Self {
punctuation_in_quote: false,
nbsp_before_colon: false,
open_quote: default_open_quote(),
close_quote: default_close_quote(),
open_inner_quote: default_open_inner_quote(),
close_inner_quote: default_close_inner_quote(),
serial_comma: false,
page_range_delimiter: default_page_range_delimiter(),
}
}
}
fn default_open_quote() -> String {
"\u{201C}".into()
}
fn default_close_quote() -> String {
"\u{201D}".into()
}
fn default_open_inner_quote() -> String {
"\u{2018}".into()
}
fn default_close_inner_quote() -> String {
"\u{2019}".into()
}
fn default_page_range_delimiter() -> String {
"\u{2013}".into()
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum MessageSyntax {
#[default]
Static,
Mf2,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct EvaluationConfig {
#[serde(default)]
pub message_syntax: MessageSyntax,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct VocabMap {
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub genre: HashMap<String, String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub medium: HashMap<String, String>,
}
impl VocabMap {
pub fn is_empty(&self) -> bool {
self.genre.is_empty() && self.medium.is_empty()
}
}
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case", default)]
pub struct LocaleOverride {
pub messages: std::collections::HashMap<String, String>,
pub grammar_options: Option<GrammarOptions>,
pub legacy_term_aliases: std::collections::HashMap<String, 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::*;
#[test]
fn test_general_term_deserialization() {
let json_tests = vec![
(r#""in""#, GeneralTerm::In),
(r#""accessed""#, GeneralTerm::Accessed),
(r#""retrieved""#, GeneralTerm::Retrieved),
(r#""at""#, GeneralTerm::At),
(r#""from""#, GeneralTerm::From),
(r#""of""#, GeneralTerm::Of),
(r#""to""#, GeneralTerm::To),
(r#""by""#, GeneralTerm::By),
(r#""no-date""#, GeneralTerm::NoDate),
(r#""anonymous""#, GeneralTerm::Anonymous),
(r#""circa""#, GeneralTerm::Circa),
(r#""available-at""#, GeneralTerm::AvailableAt),
(r#""ibid""#, GeneralTerm::Ibid),
(r#""and""#, GeneralTerm::And),
(r#""et-al""#, GeneralTerm::EtAl),
(r#""and-others""#, GeneralTerm::AndOthers),
(r#""forthcoming""#, GeneralTerm::Forthcoming),
(r#""online""#, GeneralTerm::Online),
(r#""here""#, GeneralTerm::Here),
(r#""deposited""#, GeneralTerm::Deposited),
(r#""review-of""#, GeneralTerm::ReviewOf),
(
r#""original-work-published""#,
GeneralTerm::OriginalWorkPublished,
),
(r#""patent""#, GeneralTerm::Patent),
(r#""volume""#, GeneralTerm::Volume),
(r#""issue""#, GeneralTerm::Issue),
(r#""page""#, GeneralTerm::Page),
(r#""chapter""#, GeneralTerm::Chapter),
(r#""edition""#, GeneralTerm::Edition),
(r#""section""#, GeneralTerm::Section),
];
for (json_str, expected) in json_tests {
let result: GeneralTerm = serde_json::from_str(json_str)
.unwrap_or_else(|e| panic!("Failed to deserialize {}: {}", json_str, e));
assert_eq!(result, expected, "Mismatch for {}", json_str);
}
}
#[test]
fn test_term_form_deserialization() {
let form_long: TermForm = serde_json::from_str(r#""long""#).unwrap();
assert_eq!(form_long, TermForm::Long);
let form_short: TermForm = serde_json::from_str(r#""short""#).unwrap();
assert_eq!(form_short, TermForm::Short);
let form_verb: TermForm = serde_json::from_str(r#""verb""#).unwrap();
assert_eq!(form_verb, TermForm::Verb);
let form_verb_short: TermForm = serde_json::from_str(r#""verb-short""#).unwrap();
assert_eq!(form_verb_short, TermForm::VerbShort);
let form_symbol: TermForm = serde_json::from_str(r#""symbol""#).unwrap();
assert_eq!(form_symbol, TermForm::Symbol);
}
#[test]
fn test_simple_term_construction() {
let term = SimpleTerm {
long: "anonymous".into(),
short: "anon.".into(),
};
assert_eq!(term.long, MaybeGendered::Plain("anonymous".to_string()));
assert_eq!(term.short, MaybeGendered::Plain("anon.".to_string()));
}
#[test]
fn test_singular_plural_construction() {
let term = SingularPlural {
singular: "page".into(),
plural: "pages".into(),
};
assert_eq!(term.singular, MaybeGendered::Plain("page".to_string()));
assert_eq!(term.plural, MaybeGendered::Plain("pages".to_string()));
}
#[test]
fn test_terms_en_us_defaults() {
let terms = Terms::en_us();
assert_eq!(terms.and, Some("and".to_string()));
assert_eq!(terms.and_symbol, Some("&".to_string()));
assert_eq!(terms.and_others, Some("and others".to_string()));
assert_eq!(terms.by, Some("by".to_string()));
assert_eq!(terms.from, Some("from".to_string()));
assert_eq!(terms.et_al, Some("et al.".to_string()));
assert_eq!(terms.in_, Some("in".to_string()));
assert_eq!(terms.no_date, Some("n.d.".to_string()));
assert_eq!(terms.ibid, Some("ibid.".to_string()));
assert_eq!(
terms.anonymous.long,
MaybeGendered::Plain("anonymous".to_string())
);
assert_eq!(
terms.anonymous.short,
MaybeGendered::Plain("anon.".to_string())
);
assert_eq!(terms.circa.long, MaybeGendered::Plain("circa".to_string()));
assert_eq!(terms.circa.short, MaybeGendered::Plain("c.".to_string()));
}
#[test]
fn test_terms_en_us_serializes_single_no_date_entry() {
let terms = Terms::en_us();
let value = serde_json::to_value(&terms).unwrap();
let object = value.as_object().unwrap();
assert_eq!(
object.get("no-date"),
Some(&serde_json::json!({
"long": "no date",
"short": "n.d."
}))
);
assert_eq!(object.get("no_date"), None);
}
#[test]
fn test_date_terms_en_us_months() {
let date_terms = DateTerms::en_us();
assert_eq!(date_terms.months.long.len(), 12);
assert_eq!(date_terms.months.short.len(), 12);
assert_eq!(date_terms.months.long[0], "January");
assert_eq!(date_terms.months.short[0], "Jan.");
assert_eq!(date_terms.months.long[11], "December");
assert_eq!(date_terms.months.short[11], "Dec.");
}
#[test]
fn test_date_terms_en_us_seasons() {
let date_terms = DateTerms::en_us();
assert_eq!(date_terms.seasons.len(), 4);
assert_eq!(date_terms.seasons[0], "Spring");
assert_eq!(date_terms.seasons[1], "Summer");
assert_eq!(date_terms.seasons[2], "Autumn");
assert_eq!(date_terms.seasons[3], "Winter");
}
#[test]
fn test_date_terms_en_us_before_era() {
let date_terms = DateTerms::en_us();
assert_eq!(date_terms.before_era.as_deref(), Some("BC"));
assert_eq!(date_terms.ad.as_deref(), Some("AD"));
assert_eq!(date_terms.bc.as_deref(), Some("BC"));
assert_eq!(date_terms.bce.as_deref(), Some("BCE"));
assert_eq!(date_terms.ce.as_deref(), Some("CE"));
}
#[test]
fn test_month_names_en_us() {
let months = MonthNames::en_us();
assert_eq!(months.long.len(), 12);
assert_eq!(months.short.len(), 12);
assert_eq!(months.long[5], "June");
assert_eq!(months.short[5], "June");
assert_eq!(months.long[8], "September");
assert_eq!(months.short[8], "Sept.");
}
}