#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::presets::SortPreset;
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum LabelPreset {
#[default]
Alpha,
Din,
Ams,
}
#[derive(Debug, Clone)]
pub struct LabelParams {
pub single_author_chars: u8,
pub multi_author_chars: u8,
pub et_al_min: u8,
pub et_al_marker: String,
pub et_al_names: u8,
pub year_digits: u8,
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct LabelConfig {
#[serde(default)]
pub preset: LabelPreset,
#[serde(skip_serializing_if = "Option::is_none")]
pub single_author_chars: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub multi_author_chars: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub et_al_min: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub et_al_marker: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub et_al_names: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub year_digits: Option<u8>,
}
impl LabelConfig {
pub fn effective_params(&self) -> LabelParams {
let (
default_single_author_chars,
default_multi_author_chars,
default_et_al_min,
default_marker,
default_et_al_names,
) = match self.preset {
LabelPreset::Alpha => (3u8, 1u8, 4u8, "+".to_string(), 3u8),
LabelPreset::Ams => (3u8, 1u8, 4u8, "+".to_string(), 4u8),
LabelPreset::Din => (4u8, 1u8, 3u8, String::new(), 3u8),
};
LabelParams {
single_author_chars: self
.single_author_chars
.unwrap_or(default_single_author_chars),
multi_author_chars: self
.multi_author_chars
.unwrap_or(default_multi_author_chars),
et_al_min: self.et_al_min.unwrap_or(default_et_al_min),
et_al_marker: self.et_al_marker.clone().unwrap_or(default_marker),
et_al_names: self.et_al_names.unwrap_or(default_et_al_names),
year_digits: self.year_digits.unwrap_or(2),
}
}
}
#[derive(Debug, Default, PartialEq, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "schema", schemars(rename_all = "kebab-case"))]
#[non_exhaustive]
pub enum Processing {
#[default]
AuthorDate,
Numeric,
Note,
Label(LabelConfig),
Custom(ProcessingCustom),
}
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum CitationSortPolicy {
ExplicitOnly,
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct ProcessingCustom {
#[serde(skip_serializing_if = "Option::is_none")]
pub sort: Option<SortEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<Group>,
#[serde(skip_serializing_if = "Option::is_none")]
pub disambiguate: Option<Disambiguation>,
}
impl Processing {
pub fn default_bibliography_sort(&self) -> Option<SortPreset> {
match self {
Processing::AuthorDate => Some(SortPreset::AuthorDateTitle),
Processing::Numeric => None,
Processing::Note => Some(SortPreset::AuthorTitleDate),
Processing::Label(_) => Some(SortPreset::AuthorDateTitle),
Processing::Custom(_) => None,
}
}
pub fn default_citation_sort_policy(&self) -> CitationSortPolicy {
CitationSortPolicy::ExplicitOnly
}
pub fn config(&self) -> ProcessingCustom {
match self {
Processing::AuthorDate => ProcessingCustom {
sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
group: Some(Group {
template: vec![SortKey::Author, SortKey::Year],
}),
disambiguate: Some(Disambiguation {
names: true,
add_givenname: true,
givenname_rule: GivennameRule::default(),
year_suffix: true,
}),
},
Processing::Numeric => ProcessingCustom {
sort: None,
group: None,
disambiguate: None,
},
Processing::Note => ProcessingCustom {
sort: Some(SortEntry::Preset(SortPreset::AuthorTitleDate)),
group: None,
disambiguate: Some(Disambiguation {
names: true,
add_givenname: false,
givenname_rule: GivennameRule::default(),
year_suffix: false,
}),
},
Processing::Label(_) => ProcessingCustom {
sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
group: None,
disambiguate: Some(Disambiguation {
names: false,
add_givenname: false,
givenname_rule: GivennameRule::default(),
year_suffix: true,
}),
},
Processing::Custom(custom) => custom.clone(),
}
}
}
impl Serialize for Processing {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Processing::AuthorDate => serializer.serialize_str("author-date"),
Processing::Numeric => serializer.serialize_str("numeric"),
Processing::Note => serializer.serialize_str("note"),
Processing::Label(config) => {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("label", config)?;
map.end()
}
Processing::Custom(custom) => custom.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for Processing {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, MapAccess, Visitor};
struct ProcessingVisitor;
impl<'de> Visitor<'de> for ProcessingVisitor {
type Value = Processing;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a processing mode string or map")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Processing, E> {
match v {
"author-date" => Ok(Processing::AuthorDate),
"numeric" => Ok(Processing::Numeric),
"note" => Ok(Processing::Note),
"label" => Ok(Processing::Label(LabelConfig::default())),
other => Err(E::unknown_variant(
other,
&["author-date", "numeric", "note", "label"],
)),
}
}
fn visit_enum<A: de::EnumAccess<'de>>(self, data: A) -> Result<Processing, A::Error> {
use serde::de::VariantAccess;
let (variant, access) = data.variant::<String>()?;
match variant.as_str() {
"custom" => {
let custom: ProcessingCustom = access.newtype_variant()?;
Ok(Processing::Custom(custom))
}
other => Err(de::Error::unknown_variant(
other,
&["author-date", "numeric", "note", "label", "custom"],
)),
}
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Processing, A::Error> {
let key: String = map
.next_key()?
.ok_or_else(|| de::Error::invalid_length(0, &"1"))?;
match key.as_str() {
"label" => {
let config: LabelConfig = map.next_value()?;
Ok(Processing::Label(config))
}
"sort" | "group" | "disambiguate" => {
let mut sort = None;
let mut group = None;
let mut disambiguate = None;
match key.as_str() {
"sort" => sort = Some(map.next_value()?),
"group" => group = Some(map.next_value()?),
"disambiguate" => disambiguate = Some(map.next_value()?),
_ => {
return Err(de::Error::unknown_field(
&key,
&["sort", "group", "disambiguate"],
));
}
}
while let Some(k) = map.next_key::<String>()? {
match k.as_str() {
"sort" => sort = Some(map.next_value()?),
"group" => group = Some(map.next_value()?),
"disambiguate" => disambiguate = Some(map.next_value()?),
other => {
return Err(de::Error::unknown_field(
other,
&["sort", "group", "disambiguate"],
));
}
}
}
Ok(Processing::Custom(ProcessingCustom {
sort,
group,
disambiguate,
}))
}
other => Err(de::Error::unknown_field(
other,
&["label", "sort", "group", "disambiguate"],
)),
}
}
}
deserializer.deserialize_any(ProcessingVisitor)
}
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum GivennameRule {
#[default]
ByCite,
AllNames,
AllNamesWithInitials,
PrimaryName,
PrimaryNameWithInitials,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct Disambiguation {
pub names: bool,
#[serde(default)]
pub add_givenname: bool,
#[serde(default)]
pub givenname_rule: GivennameRule,
pub year_suffix: bool,
}
impl Default for Disambiguation {
fn default() -> Self {
Self {
names: true,
add_givenname: false,
givenname_rule: GivennameRule::default(),
year_suffix: false,
}
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct Sort {
#[serde(default)]
pub shorten_names: bool,
#[serde(default)]
pub render_substitutions: bool,
pub template: Vec<SortSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum SortEntry {
Preset(crate::presets::SortPreset),
Explicit(Sort),
}
impl SortEntry {
pub fn resolve(&self) -> Sort {
match self {
SortEntry::Preset(preset) => preset.sort(),
SortEntry::Explicit(sort) => sort.clone(),
}
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct SortSpec {
pub key: SortKey,
#[serde(default = "default_ascending")]
pub ascending: bool,
}
fn default_ascending() -> bool {
true
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum SortKey {
#[default]
Author,
Year,
Title,
CitationNumber,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct Group {
pub template: Vec<SortKey>,
}
#[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_label_config_alpha_preset_defaults() {
let config = LabelConfig {
preset: LabelPreset::Alpha,
single_author_chars: None,
multi_author_chars: None,
et_al_min: None,
et_al_marker: None,
et_al_names: None,
year_digits: None,
};
let params = config.effective_params();
assert_eq!(params.single_author_chars, 3);
assert_eq!(params.multi_author_chars, 1);
assert_eq!(params.et_al_min, 4);
assert_eq!(params.et_al_marker, "+");
assert_eq!(params.et_al_names, 3);
assert_eq!(params.year_digits, 2);
}
#[test]
fn test_label_config_alpha_with_overrides() {
let config = LabelConfig {
preset: LabelPreset::Alpha,
single_author_chars: Some(5),
multi_author_chars: Some(2),
et_al_min: Some(5),
et_al_marker: Some("*".to_string()),
et_al_names: Some(4),
year_digits: Some(4),
};
let params = config.effective_params();
assert_eq!(params.single_author_chars, 5);
assert_eq!(params.multi_author_chars, 2);
assert_eq!(params.et_al_min, 5);
assert_eq!(params.et_al_marker, "*");
assert_eq!(params.et_al_names, 4);
assert_eq!(params.year_digits, 4);
}
#[test]
fn test_label_config_din_preset_defaults() {
let config = LabelConfig {
preset: LabelPreset::Din,
single_author_chars: None,
multi_author_chars: None,
et_al_min: None,
et_al_marker: None,
et_al_names: None,
year_digits: None,
};
let params = config.effective_params();
assert_eq!(params.single_author_chars, 4);
assert_eq!(params.multi_author_chars, 1);
assert_eq!(params.et_al_min, 3);
assert_eq!(params.et_al_marker, "");
assert_eq!(params.et_al_names, 3);
assert_eq!(params.year_digits, 2);
}
#[test]
fn test_processing_author_date_default_bibliography_sort() {
let processing = Processing::AuthorDate;
let sort = processing.default_bibliography_sort();
assert_eq!(sort, Some(SortPreset::AuthorDateTitle));
}
#[test]
fn test_processing_numeric_default_bibliography_sort() {
let processing = Processing::Numeric;
let sort = processing.default_bibliography_sort();
assert_eq!(sort, None);
}
#[test]
fn test_processing_note_default_bibliography_sort() {
let processing = Processing::Note;
let sort = processing.default_bibliography_sort();
assert_eq!(sort, Some(SortPreset::AuthorTitleDate));
}
#[test]
fn test_processing_citation_sort_policy() {
let modes = vec![
Processing::AuthorDate,
Processing::Numeric,
Processing::Note,
Processing::Label(LabelConfig::default()),
Processing::Custom(ProcessingCustom::default()),
];
for mode in modes {
assert_eq!(
mode.default_citation_sort_policy(),
CitationSortPolicy::ExplicitOnly
);
}
}
#[test]
fn test_processing_author_date_config() {
let processing = Processing::AuthorDate;
let config = processing.config();
assert!(config.sort.is_some());
assert!(config.group.is_some());
assert!(config.disambiguate.is_some());
let disambig = config.disambiguate.unwrap();
assert!(disambig.names);
assert!(disambig.add_givenname);
assert!(disambig.year_suffix);
}
#[test]
fn test_disambiguation_defaults() {
let disambig = Disambiguation::default();
assert!(disambig.names);
assert!(!disambig.add_givenname);
assert_eq!(disambig.givenname_rule, GivennameRule::ByCite);
assert!(!disambig.year_suffix);
}
#[test]
fn test_sort_entry_resolve_preset() {
let entry = SortEntry::Preset(SortPreset::AuthorDateTitle);
let sort = entry.resolve();
assert!(!sort.template.is_empty());
}
#[test]
fn test_sort_entry_resolve_explicit() {
let explicit = Sort {
shorten_names: true,
render_substitutions: false,
template: vec![SortSpec {
key: SortKey::Title,
ascending: false,
}],
};
let entry = SortEntry::Explicit(explicit.clone());
let resolved = entry.resolve();
assert!(resolved.shorten_names);
assert!(!resolved.render_substitutions);
assert_eq!(resolved.template.len(), 1);
assert_eq!(resolved.template[0].key, SortKey::Title);
assert!(!resolved.template[0].ascending);
}
}