use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Bibliography {
#[serde(default)]
pub style: CitationStyle,
pub entries: Vec<BibliographyEntry>,
}
impl Bibliography {
#[must_use]
pub fn new(style: CitationStyle) -> Self {
Self {
style,
entries: Vec::new(),
}
}
pub fn add_entry(&mut self, entry: BibliographyEntry) {
self.entries.push(entry);
}
#[must_use]
pub fn get(&self, id: &str) -> Option<&BibliographyEntry> {
self.entries.iter().find(|e| e.id == id)
}
#[must_use]
pub fn contains(&self, id: &str) -> bool {
self.entries.iter().any(|e| e.id == id)
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl Default for Bibliography {
fn default() -> Self {
Self::new(CitationStyle::default())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, strum::Display)]
#[serde(rename_all = "lowercase")]
pub enum CitationStyle {
#[default]
#[strum(serialize = "APA")]
Apa,
#[strum(serialize = "MLA")]
Mla,
Chicago,
#[strum(serialize = "IEEE")]
Ieee,
Harvard,
Vancouver,
#[strum(serialize = "ACM")]
Acm,
Custom,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BibliographyEntry {
pub id: String,
pub entry_type: EntryType,
pub title: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authors: Vec<Author>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issued: Option<PartialDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub container_title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub volume: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issue: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doi: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub isbn: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issn: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub publisher: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub publisher_place: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edition: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub editors: Vec<Author>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub abstract_text: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub keywords: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accessed: Option<PartialDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
impl BibliographyEntry {
#[must_use]
pub fn new(id: impl Into<String>, entry_type: EntryType, title: impl Into<String>) -> Self {
Self {
id: id.into(),
entry_type,
title: title.into(),
authors: Vec::new(),
issued: None,
container_title: None,
volume: None,
issue: None,
page: None,
doi: None,
url: None,
isbn: None,
issn: None,
publisher: None,
publisher_place: None,
edition: None,
editors: Vec::new(),
abstract_text: None,
keywords: Vec::new(),
language: None,
accessed: None,
note: None,
}
}
#[must_use]
pub fn with_author(mut self, author: Author) -> Self {
self.authors.push(author);
self
}
#[must_use]
pub fn with_authors(mut self, authors: Vec<Author>) -> Self {
self.authors = authors;
self
}
#[must_use]
pub fn with_issued(mut self, date: PartialDate) -> Self {
self.issued = Some(date);
self
}
#[must_use]
pub fn with_container(mut self, container: impl Into<String>) -> Self {
self.container_title = Some(container.into());
self
}
#[must_use]
pub fn with_volume_issue(mut self, volume: impl Into<String>, issue: Option<String>) -> Self {
self.volume = Some(volume.into());
self.issue = issue;
self
}
#[must_use]
pub fn with_pages(mut self, pages: impl Into<String>) -> Self {
self.page = Some(pages.into());
self
}
#[must_use]
pub fn with_doi(mut self, doi: impl Into<String>) -> Self {
self.doi = Some(doi.into());
self
}
#[must_use]
pub fn with_url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
#[must_use]
pub fn with_publisher(mut self, publisher: impl Into<String>, place: Option<String>) -> Self {
self.publisher = Some(publisher.into());
self.publisher_place = place;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
#[serde(rename_all = "camelCase")]
#[strum(serialize_all = "lowercase")]
pub enum EntryType {
Article,
Book,
Chapter,
Conference,
Thesis,
Report,
Webpage,
Patent,
Dataset,
Software,
#[strum(serialize = "legal-case")]
LegalCase,
Legislation,
Personal,
Manuscript,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Author {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub given: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub family: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub literal: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub orcid: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub affiliation: Option<String>,
}
impl Author {
#[must_use]
pub fn new(given: impl Into<String>, family: impl Into<String>) -> Self {
Self {
given: Some(given.into()),
family: Some(family.into()),
literal: None,
orcid: None,
affiliation: None,
}
}
#[must_use]
pub fn literal(name: impl Into<String>) -> Self {
Self {
given: None,
family: None,
literal: Some(name.into()),
orcid: None,
affiliation: None,
}
}
#[must_use]
pub fn with_orcid(mut self, orcid: impl Into<String>) -> Self {
self.orcid = Some(orcid.into());
self
}
#[must_use]
pub fn with_affiliation(mut self, affiliation: impl Into<String>) -> Self {
self.affiliation = Some(affiliation.into());
self
}
#[must_use]
pub fn display_name(&self) -> String {
if let Some(literal) = &self.literal {
return literal.clone();
}
match (&self.family, &self.given) {
(Some(family), Some(given)) => format!("{family}, {given}"),
(Some(family), None) => family.clone(),
(None, Some(given)) => given.clone(),
(None, None) => String::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PartialDate {
pub year: i32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub month: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub day: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub season: Option<String>,
}
impl PartialDate {
#[must_use]
pub const fn year(year: i32) -> Self {
Self {
year,
month: None,
day: None,
season: None,
}
}
#[must_use]
pub const fn year_month(year: i32, month: u8) -> Self {
Self {
year,
month: Some(month),
day: None,
season: None,
}
}
pub fn try_year_month(year: i32, month: u8) -> Result<Self, String> {
if !(1..=12).contains(&month) {
return Err(format!("month must be 1-12, got {month}"));
}
Ok(Self::year_month(year, month))
}
#[must_use]
pub const fn full(year: i32, month: u8, day: u8) -> Self {
Self {
year,
month: Some(month),
day: Some(day),
season: None,
}
}
pub fn try_full(year: i32, month: u8, day: u8) -> Result<Self, String> {
if !(1..=12).contains(&month) {
return Err(format!("month must be 1-12, got {month}"));
}
if !(1..=31).contains(&day) {
return Err(format!("day must be 1-31, got {day}"));
}
Ok(Self::full(year, month, day))
}
#[must_use]
pub fn seasonal(year: i32, season: impl Into<String>) -> Self {
Self {
year,
month: None,
day: None,
season: Some(season.into()),
}
}
}
impl<'de> Deserialize<'de> for PartialDate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Raw {
year: i32,
#[serde(default)]
month: Option<u8>,
#[serde(default)]
day: Option<u8>,
#[serde(default)]
season: Option<String>,
}
let raw = Raw::deserialize(deserializer)?;
if let Some(m) = raw.month {
if !(1..=12).contains(&m) {
return Err(serde::de::Error::custom(format!(
"month must be 1-12, got {m}"
)));
}
}
if let Some(d) = raw.day {
if !(1..=31).contains(&d) {
return Err(serde::de::Error::custom(format!(
"day must be 1-31, got {d}"
)));
}
}
Ok(PartialDate {
year: raw.year,
month: raw.month,
day: raw.day,
season: raw.season,
})
}
}
impl std::fmt::Display for PartialDate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(season) = &self.season {
return write!(f, "{} {}", season, self.year);
}
match (self.month, self.day) {
(Some(month), Some(day)) => write!(f, "{}-{:02}-{:02}", self.year, month, day),
(Some(month), None) => write!(f, "{}-{:02}", self.year, month),
_ => write!(f, "{}", self.year),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_try_year_month_valid() {
assert!(PartialDate::try_year_month(2024, 1).is_ok());
assert!(PartialDate::try_year_month(2024, 12).is_ok());
}
#[test]
fn test_try_year_month_invalid() {
assert!(PartialDate::try_year_month(2024, 0).is_err());
assert!(PartialDate::try_year_month(2024, 13).is_err());
}
#[test]
fn test_try_full_valid() {
assert!(PartialDate::try_full(2024, 6, 15).is_ok());
}
#[test]
fn test_try_full_invalid() {
assert!(PartialDate::try_full(2024, 0, 15).is_err());
assert!(PartialDate::try_full(2024, 6, 0).is_err());
assert!(PartialDate::try_full(2024, 6, 32).is_err());
}
#[test]
fn test_partial_date_deser_rejects_invalid_month() {
let json = r#"{"year":2024,"month":13}"#;
let result: Result<PartialDate, _> = serde_json::from_str(json);
assert!(result.is_err());
}
#[test]
fn test_partial_date_deser_rejects_invalid_day() {
let json = r#"{"year":2024,"month":6,"day":32}"#;
let result: Result<PartialDate, _> = serde_json::from_str(json);
assert!(result.is_err());
}
#[test]
fn test_partial_date_deser_accepts_valid() {
let json = r#"{"year":2024,"month":6,"day":15}"#;
let result: PartialDate = serde_json::from_str(json).unwrap();
assert_eq!(result.year, 2024);
assert_eq!(result.month, Some(6));
assert_eq!(result.day, Some(15));
}
}