#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::Template;
use crate::locale::{GeneralTerm, TermForm};
use crate::presets::SortPreset;
use crate::template::TypeSelector;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct BibliographyGroup {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub heading: Option<GroupHeading>,
pub selector: GroupSelector,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort: Option<GroupSortEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<Template>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disambiguate: Option<DisambiguationScope>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case", untagged)]
pub enum GroupHeading {
Literal {
literal: String,
},
Term {
term: GeneralTerm,
#[serde(skip_serializing_if = "Option::is_none")]
form: Option<TermForm>,
},
Localized {
localized: HashMap<String, String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum DisambiguationScope {
#[default]
Globally,
Locally,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct GroupSelector {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "type")]
pub ref_type: Option<TypeSelector>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cited: Option<CitedStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub field: Option<HashMap<String, FieldMatcher>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub not: Option<Box<GroupSelector>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum CitedStatus {
Visible,
Any,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum FieldMatcher {
Exact(String),
Multiple(Vec<String>),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum GroupSortEntry {
Preset(SortPreset),
Explicit(GroupSort),
}
impl GroupSortEntry {
pub fn resolve(&self) -> GroupSort {
match self {
GroupSortEntry::Preset(preset) => preset.group_sort(),
GroupSortEntry::Explicit(sort) => sort.clone(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct GroupSort {
pub template: Vec<GroupSortKey>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct GroupSortKey {
pub key: SortKey,
#[serde(default = "default_true")]
pub ascending: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub order: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_order: Option<NameSortOrder>,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum SortKey {
#[serde(rename = "type")]
RefType,
Author,
Title,
Issued,
Field(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum NameSortOrder {
FamilyGiven,
GivenFamily,
}
#[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_group_selector_type_single() {
let yaml = r#"
type: legal-case
"#;
let selector: GroupSelector = serde_yaml::from_str(yaml).unwrap();
assert!(selector.ref_type.is_some());
match selector.ref_type.unwrap() {
TypeSelector::Single(t) => assert_eq!(t, "legal-case"),
_ => panic!("Expected Single"),
}
}
#[test]
fn test_group_selector_type_multiple() {
let yaml = r#"
type: [legal-case, statute, treaty]
"#;
let selector: GroupSelector = serde_yaml::from_str(yaml).unwrap();
match selector.ref_type.unwrap() {
TypeSelector::Multiple(types) => {
assert_eq!(types, vec!["legal-case", "statute", "treaty"]);
}
_ => panic!("Expected Multiple"),
}
}
#[test]
fn test_group_selector_field_exact() {
let yaml = r#"
field:
language: vi
"#;
let selector: GroupSelector = serde_yaml::from_str(yaml).unwrap();
let fields = selector.field.unwrap();
match fields.get("language").unwrap() {
FieldMatcher::Exact(lang) => assert_eq!(lang, "vi"),
_ => panic!("Expected Exact"),
}
}
#[test]
fn test_group_selector_negation() {
let yaml = r#"
not:
type: legal-case
"#;
let selector: GroupSelector = serde_yaml::from_str(yaml).unwrap();
let negated = selector.not.unwrap();
assert!(negated.ref_type.is_some());
}
#[test]
fn test_bibliography_group_minimal() {
let yaml = r#"
id: cases
selector:
type: legal-case
"#;
let group: BibliographyGroup = serde_yaml::from_str(yaml).unwrap();
assert_eq!(group.id, "cases");
assert!(group.heading.is_none());
assert!(group.sort.is_none());
}
#[test]
fn test_bibliography_group_full() {
let yaml = r#"
id: vietnamese
heading:
localized:
vi: "Tài liệu tiếng Việt"
en-US: "Vietnamese Sources"
selector:
field:
language: vi
sort:
template:
- key: author
sort-order: given-family
- key: issued
ascending: false
"#;
let group: BibliographyGroup = serde_yaml::from_str(yaml).unwrap();
assert_eq!(group.id, "vietnamese");
match group.heading.unwrap() {
GroupHeading::Localized { localized } => {
assert_eq!(localized.get("vi").unwrap(), "Tài liệu tiếng Việt");
assert_eq!(localized.get("en-US").unwrap(), "Vietnamese Sources");
}
_ => panic!("Expected localized heading"),
}
let sort = group.sort.unwrap().resolve();
assert_eq!(sort.template.len(), 2);
match &sort.template[0].key {
SortKey::Author => {}
_ => panic!("Expected Author"),
}
assert_eq!(
sort.template[0].sort_order,
Some(NameSortOrder::GivenFamily)
);
match &sort.template[1].key {
SortKey::Issued => {}
_ => panic!("Expected Issued"),
}
assert!(!sort.template[1].ascending);
}
#[test]
fn test_type_order_sorting() {
let yaml = r#"
template:
- key: type
order: [legal-case, statute, treaty]
"#;
let sort: GroupSort = serde_yaml::from_str(yaml).unwrap();
assert_eq!(sort.template.len(), 1);
let order = sort.template[0].order.as_ref().unwrap();
assert_eq!(order, &vec!["legal-case", "statute", "treaty"]);
}
#[test]
fn test_group_heading_literal() {
let yaml = r#"
literal: "Primary Sources"
"#;
let heading: GroupHeading = serde_yaml::from_str(yaml).unwrap();
match heading {
GroupHeading::Literal { literal } => assert_eq!(literal, "Primary Sources"),
_ => panic!("Expected literal heading"),
}
}
#[test]
fn test_group_heading_term() {
let yaml = r#"
term: no-date
form: short
"#;
let heading: GroupHeading = serde_yaml::from_str(yaml).unwrap();
match heading {
GroupHeading::Term { term, form } => {
assert_eq!(term, GeneralTerm::NoDate);
assert_eq!(form, Some(TermForm::Short));
}
_ => panic!("Expected term heading"),
}
}
}