#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::presets::SortPreset;
const PROCESSING_STRING_VARIANTS: &[&str] = &[
"author-date",
"author-date-givenname",
"author-date-names",
"author-date-full",
"numeric",
"note",
"label",
];
#[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 => (4u8, 1u8, 5u8, String::new(), 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,
AuthorDateGivenname,
AuthorDateNames,
AuthorDateFull,
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, PartialEq, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum ProcessingBase {
AuthorDate,
AuthorDateGivenname,
AuthorDateNames,
AuthorDateFull,
Numeric,
Note,
Label,
}
impl ProcessingBase {
pub fn processing(&self) -> Processing {
match self {
Self::AuthorDate => Processing::AuthorDate,
Self::AuthorDateGivenname => Processing::AuthorDateGivenname,
Self::AuthorDateNames => Processing::AuthorDateNames,
Self::AuthorDateFull => Processing::AuthorDateFull,
Self::Numeric => Processing::Numeric,
Self::Note => Processing::Note,
Self::Label => Processing::Label(LabelConfig::default()),
}
}
}
#[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 base: Option<ProcessingBase>,
#[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 ProcessingCustom {
#[must_use]
pub fn resolved(&self) -> ProcessingCustom {
let mut config = match self.base {
Some(base) => base.processing().config(),
None => ProcessingCustom::default(),
};
config.base = None;
if self.sort.is_some() {
config.sort = self.sort.clone();
}
if self.group.is_some() {
config.group = self.group.clone();
}
if self.disambiguate.is_some() {
config.disambiguate = self.disambiguate.clone();
}
config
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RegimeFamily {
AuthorDate,
Numeric,
Note,
Label,
Custom,
}
fn author_date_config(
names: bool,
add_givenname: bool,
givenname_rule: GivennameRule,
) -> ProcessingCustom {
ProcessingCustom {
base: None,
sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
group: Some(Group {
template: vec![SortKey::Author, SortKey::Year],
}),
disambiguate: Some(Disambiguation {
names,
add_givenname,
givenname_rule,
year_suffix: true,
}),
}
}
impl Processing {
pub fn default_bibliography_sort(&self) -> Option<SortPreset> {
match self {
Processing::AuthorDate
| Processing::AuthorDateGivenname
| Processing::AuthorDateNames
| Processing::AuthorDateFull => Some(SortPreset::AuthorDateTitle),
Processing::Numeric => None,
Processing::Note => Some(SortPreset::AuthorTitleDate),
Processing::Label(_) => Some(SortPreset::AuthorDateTitle),
Processing::Custom(custom) => match (custom.base, custom.sort.as_ref()) {
(Some(base), None) => base.processing().default_bibliography_sort(),
_ => None,
},
}
}
pub fn is_author_date_family(&self) -> bool {
self.regime_family() == RegimeFamily::AuthorDate
}
pub fn regime_family(&self) -> RegimeFamily {
match self {
Self::AuthorDate
| Self::AuthorDateGivenname
| Self::AuthorDateNames
| Self::AuthorDateFull => RegimeFamily::AuthorDate,
Self::Numeric => RegimeFamily::Numeric,
Self::Note => RegimeFamily::Note,
Self::Label(_) => RegimeFamily::Label,
Self::Custom(custom) => match custom.base {
Some(base) => base.processing().regime_family(),
None => RegimeFamily::Custom,
},
}
}
pub fn default_citation_sort_policy(&self) -> CitationSortPolicy {
CitationSortPolicy::ExplicitOnly
}
pub fn config(&self) -> ProcessingCustom {
match self {
Processing::AuthorDate => author_date_config(false, false, GivennameRule::ByCite),
Processing::AuthorDateGivenname => {
author_date_config(false, true, GivennameRule::ByCite)
}
Processing::AuthorDateNames => author_date_config(true, false, GivennameRule::ByCite),
Processing::AuthorDateFull => {
author_date_config(true, true, GivennameRule::PrimaryName)
}
Processing::Numeric => ProcessingCustom::default(),
Processing::Note => ProcessingCustom {
base: None,
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 {
base: None,
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.resolved(),
}
}
}
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::AuthorDateGivenname => serializer.serialize_str("author-date-givenname"),
Processing::AuthorDateNames => serializer.serialize_str("author-date-names"),
Processing::AuthorDateFull => serializer.serialize_str("author-date-full"),
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),
"author-date-givenname" => Ok(Processing::AuthorDateGivenname),
"author-date-names" => Ok(Processing::AuthorDateNames),
"author-date-full" => Ok(Processing::AuthorDateFull),
"numeric" => Ok(Processing::Numeric),
"note" => Ok(Processing::Note),
"label" => Ok(Processing::Label(LabelConfig::default())),
other => Err(E::unknown_variant(other, PROCESSING_STRING_VARIANTS)),
}
}
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, &["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))
}
"base" | "sort" | "group" | "disambiguate" => {
let mut base = None;
let mut sort = None;
let mut group = None;
let mut disambiguate = None;
match key.as_str() {
"base" => base = map.next_value()?,
"sort" => sort = map.next_value()?,
"group" => group = map.next_value()?,
"disambiguate" => disambiguate = map.next_value()?,
_ => {
return Err(de::Error::unknown_field(
&key,
&["base", "sort", "group", "disambiguate"],
));
}
}
while let Some(k) = map.next_key::<String>()? {
match k.as_str() {
"base" => base = map.next_value()?,
"sort" => sort = map.next_value()?,
"group" => group = map.next_value()?,
"disambiguate" => disambiguate = map.next_value()?,
other => {
return Err(de::Error::unknown_field(
other,
&["base", "sort", "group", "disambiguate"],
));
}
}
}
Ok(Processing::Custom(ProcessingCustom {
base,
sort,
group,
disambiguate,
}))
}
other => Err(de::Error::unknown_field(
other,
&["label", "base", "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(),
}
}
}
impl Sort {
pub fn group_sort(&self) -> crate::grouping::GroupSort {
let template = self
.template
.iter()
.filter_map(|sort| {
let key = match sort.key {
SortKey::Author => crate::grouping::SortKey::Author,
SortKey::Year => crate::grouping::SortKey::Issued,
SortKey::Title => crate::grouping::SortKey::Title,
SortKey::CitationNumber => return None,
};
Some(crate::grouping::GroupSortKey {
key,
ascending: sort.ascending,
order: None,
sort_order: None,
})
})
.collect();
crate::grouping::GroupSort { template }
}
}
#[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_label_config_ams_preset_defaults() {
let config = LabelConfig {
preset: LabelPreset::Ams,
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, 5);
assert_eq!(params.et_al_marker, "");
assert_eq!(params.et_al_names, 4);
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::AuthorDateGivenname,
Processing::AuthorDateNames,
Processing::AuthorDateFull,
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_variant_configs() {
let cases = [
(Processing::AuthorDate, false, false, GivennameRule::ByCite),
(
Processing::AuthorDateGivenname,
false,
true,
GivennameRule::ByCite,
),
(
Processing::AuthorDateNames,
true,
false,
GivennameRule::ByCite,
),
(
Processing::AuthorDateFull,
true,
true,
GivennameRule::PrimaryName,
),
];
for (processing, names, add_givenname, expected_rule) in cases {
let config = processing.config();
assert_eq!(
config.sort,
Some(SortEntry::Preset(SortPreset::AuthorDateTitle))
);
assert_eq!(
config.group,
Some(Group {
template: vec![SortKey::Author, SortKey::Year],
})
);
let disambig = config.disambiguate.unwrap();
assert_eq!(disambig.names, names);
assert_eq!(disambig.add_givenname, add_givenname);
assert_eq!(disambig.givenname_rule, expected_rule);
assert!(disambig.year_suffix);
}
}
#[test]
fn test_processing_author_date_variant_names() {
let cases = [
(Processing::AuthorDate, "author-date"),
(Processing::AuthorDateGivenname, "author-date-givenname"),
(Processing::AuthorDateNames, "author-date-names"),
(Processing::AuthorDateFull, "author-date-full"),
];
for (processing, name) in cases {
let serialized = serde_yaml::to_string(&processing).unwrap();
assert_eq!(serialized.trim(), name);
let deserialized: Processing = serde_yaml::from_str(name).unwrap();
assert_eq!(deserialized, processing);
}
}
#[test]
fn test_processing_custom_base_round_trip() {
let processing = Processing::Custom(ProcessingCustom {
base: Some(ProcessingBase::AuthorDate),
sort: Some(SortEntry::Preset(SortPreset::AuthorTitleDate)),
group: None,
disambiguate: None,
});
let yaml = serde_yaml::to_string(&processing).unwrap();
let parsed: Processing = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(yaml.trim(), "base: author-date\nsort: author-title-date");
assert_eq!(parsed, processing);
}
#[test]
fn test_processing_custom_base_only_resolves_to_preset_config() {
let parsed: Processing = serde_yaml::from_str("base: author-date-full").unwrap();
assert_eq!(
parsed,
Processing::Custom(ProcessingCustom {
base: Some(ProcessingBase::AuthorDateFull),
sort: None,
group: None,
disambiguate: None,
})
);
assert_eq!(parsed.config(), Processing::AuthorDateFull.config());
}
#[test]
fn test_processing_custom_resolved_overlay_semantics() {
let custom = ProcessingCustom {
base: Some(ProcessingBase::AuthorDate),
sort: Some(SortEntry::Preset(SortPreset::AuthorTitleDate)),
group: None,
disambiguate: None,
};
let resolved = custom.resolved();
let base_config = Processing::AuthorDate.config();
assert_eq!(resolved.base, None);
assert_eq!(
resolved.sort,
Some(SortEntry::Preset(SortPreset::AuthorTitleDate))
);
assert_eq!(resolved.group, base_config.group);
assert_eq!(resolved.disambiguate, base_config.disambiguate);
}
#[test]
fn test_processing_custom_resolved_without_base_is_identity() {
let custom = ProcessingCustom {
base: None,
sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
group: None,
disambiguate: None,
};
assert_eq!(custom.resolved(), custom);
}
#[test]
fn test_processing_custom_base_family_delegation() {
let with_base = Processing::Custom(ProcessingCustom {
base: Some(ProcessingBase::AuthorDate),
..Default::default()
});
let without_base = Processing::Custom(ProcessingCustom::default());
assert_eq!(with_base.regime_family(), RegimeFamily::AuthorDate);
assert!(with_base.is_author_date_family());
assert_eq!(without_base.regime_family(), RegimeFamily::Custom);
assert!(!without_base.is_author_date_family());
let numeric_base = Processing::Custom(ProcessingCustom {
base: Some(ProcessingBase::Numeric),
..Default::default()
});
assert_eq!(numeric_base.regime_family(), RegimeFamily::Numeric);
assert!(!numeric_base.is_author_date_family());
}
#[test]
fn test_processing_custom_base_default_bibliography_sort() {
let inherited = Processing::Custom(ProcessingCustom {
base: Some(ProcessingBase::AuthorDate),
..Default::default()
});
assert_eq!(
inherited.default_bibliography_sort(),
Some(SortPreset::AuthorDateTitle)
);
let overridden = Processing::Custom(ProcessingCustom {
base: Some(ProcessingBase::AuthorDate),
sort: Some(SortEntry::Preset(SortPreset::AuthorTitleDate)),
..Default::default()
});
assert_eq!(overridden.default_bibliography_sort(), None);
}
#[test]
fn test_processing_custom_map_accepts_explicit_nulls() {
let parsed: Processing =
serde_yaml::from_str("base: ~\nsort: author-title-date\ndisambiguate: null").unwrap();
assert_eq!(
parsed,
Processing::Custom(ProcessingCustom {
base: None,
sort: Some(SortEntry::Preset(SortPreset::AuthorTitleDate)),
group: None,
disambiguate: None,
})
);
}
#[test]
fn test_processing_custom_base_rejects_invalid_values() {
let nested = serde_yaml::from_str::<Processing>("base: { sort: author-date-title }");
let unknown = serde_yaml::from_str::<Processing>("base: fancy-date");
assert!(nested.is_err());
assert!(unknown.is_err());
}
#[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_group_sort_maps_keys_and_skips_citation_number() {
let sort = Sort {
shorten_names: false,
render_substitutions: false,
template: vec![
SortSpec {
key: SortKey::Author,
ascending: true,
},
SortSpec {
key: SortKey::Year,
ascending: false,
},
SortSpec {
key: SortKey::Title,
ascending: true,
},
SortSpec {
key: SortKey::CitationNumber,
ascending: true,
},
],
};
let group_sort = sort.group_sort();
assert_eq!(group_sort.template.len(), 3);
assert_eq!(group_sort.template[0].key, crate::grouping::SortKey::Author);
assert!(group_sort.template[0].ascending);
assert_eq!(group_sort.template[1].key, crate::grouping::SortKey::Issued);
assert!(!group_sort.template[1].ascending);
assert_eq!(group_sort.template[2].key, crate::grouping::SortKey::Title);
assert!(group_sort.template[2].ascending);
}
#[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);
}
}