use std::borrow::Cow;
use std::{collections::BTreeMap, str::FromStr};
use serde::de::Visitor;
use serde::{Deserialize, Serialize};
use unscanny::Scanner;
use crate::taxonomy::Season;
#[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone)]
#[serde(transparent)]
pub struct Item(pub BTreeMap<String, Value>);
impl Item {
pub fn id(&self) -> Option<Cow<'_, str>> {
self.0.get("id")?.to_str()
}
pub fn type_(&self) -> Option<Cow<'_, str>> {
self.0.get("type")?.to_str()
}
pub fn has_html(&self) -> bool {
self.0.values().any(|v| v.has_html())
}
pub fn may_have_hack(&self) -> bool {
self.0.contains_key("note")
}
}
#[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone)]
#[serde(untagged)]
pub enum Value {
String(String),
Number(i64),
Names(Vec<NameValue>),
Date(DateValue),
}
impl Value {
pub fn to_str(&self) -> Option<Cow<'_, str>> {
match self {
Value::String(s) => Some(s.as_str().into()),
Value::Number(n) => Some(n.to_string().into()),
Value::Date(_) => None,
Value::Names(_) => None,
}
}
pub fn has_html(&self) -> bool {
match self {
Value::String(s) => s.contains('<'),
Value::Number(_) => false,
Value::Date(_) => false,
Value::Names(_) => false,
}
}
}
#[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone)]
#[serde(untagged)]
pub enum NameValue {
Literal(LiteralName),
Item(NameItem),
}
#[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct NameItem {
#[serde(default)]
pub family: String,
pub given: Option<String>,
pub non_dropping_particle: Option<String>,
pub dropping_particle: Option<String>,
pub suffix: Option<String>,
pub comma_suffix: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone)]
pub struct LiteralName {
pub literal: String,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum DateValue {
Raw {
raw: FixedDateRange,
literal: Option<String>,
season: Option<String>,
},
DateParts {
date_parts: VecDateRange,
literal: Option<String>,
season: Option<String>,
circa: bool,
},
}
impl DateValue {
pub fn is_approx(&self) -> bool {
match self {
DateValue::Raw { raw, .. } => {
raw.start.circa || raw.end.map(|d| d.circa).unwrap_or_default()
}
DateValue::DateParts { circa, .. } => *circa,
}
}
}
impl TryFrom<DateValue> for FixedDateRange {
type Error = ();
fn try_from(value: DateValue) -> Result<Self, Self::Error> {
let (mut fixed, season) = match value {
DateValue::Raw { raw, season, .. } => (raw, season),
DateValue::DateParts { date_parts, season, circa, .. } => {
let mut res: FixedDateRange = date_parts.try_into()?;
res.start.circa = circa;
(res, season)
}
};
fixed.start.season = season
.and_then(|s| s.parse::<u8>().ok())
.and_then(|u| Season::try_from_csl_number(u).ok());
Ok(fixed)
}
}
impl From<DateValue> for VecDateRange {
fn from(value: DateValue) -> Self {
match value {
DateValue::Raw { raw, .. } => raw.into(),
DateValue::DateParts { date_parts, .. } => date_parts,
}
}
}
enum BooleanLike {
String(String),
Bool(bool),
Number(u8),
}
impl BooleanLike {
fn is_true(&self) -> bool {
match self {
BooleanLike::String(s) => s == "true",
BooleanLike::Bool(b) => *b,
BooleanLike::Number(n) => *n == 1,
}
}
}
impl<'de> Deserialize<'de> for BooleanLike {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct ValueVisitor;
impl<'de> Visitor<'de> for ValueVisitor {
type Value = BooleanLike;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("boolean, string, or unsigned, small number")
}
#[inline]
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(BooleanLike::Bool(v))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(BooleanLike::String(String::from(v)))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(BooleanLike::Number(v as u8))
}
}
deserializer.deserialize_any(ValueVisitor)
}
}
impl<'de> Deserialize<'de> for DateValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case", untagged)]
enum DateReprRaw {
Raw {
raw: FixedDateRange,
literal: Option<String>,
season: Option<NumberOrString>,
},
DateParts {
#[serde(rename = "date-parts")]
date_parts: VecDateRange,
literal: Option<String>,
season: Option<NumberOrString>,
circa: Option<BooleanLike>,
},
}
let raw = DateReprRaw::deserialize(deserializer)?;
Ok(match raw {
DateReprRaw::Raw { raw, literal, season } => DateValue::Raw {
raw,
literal,
season: season.map(NumberOrString::into_string),
},
DateReprRaw::DateParts { date_parts, literal, season, circa } => {
DateValue::DateParts {
date_parts,
literal,
season: season.map(NumberOrString::into_string),
circa: circa.as_ref().map(BooleanLike::is_true).unwrap_or_default(),
}
}
})
}
}
impl Serialize for DateValue {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
DateValue::Raw { raw, .. } => VecDateRange::from(*raw).serialize(serializer),
DateValue::DateParts { date_parts, .. } => date_parts.serialize(serializer),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
#[serde(transparent)]
pub struct VecDateRange(pub Vec<VecDate>);
impl From<FixedDateRange> for VecDateRange {
fn from(value: FixedDateRange) -> Self {
let mut v = Vec::new();
v.push(value.start.into());
if let Some(end) = value.end {
v.push(end.into());
}
VecDateRange(v)
}
}
#[derive(Clone, Debug, Serialize, Hash, PartialEq, Eq)]
#[serde(transparent)]
pub struct VecDate(pub Vec<i16>);
impl From<FixedDate> for VecDate {
fn from(value: FixedDate) -> Self {
let mut v = Vec::new();
v.push(value.year);
if let Some(month) = value.month {
v.push(month as i16);
if let Some(day) = value.day {
v.push(day as i16);
}
}
VecDate(v)
}
}
impl<'de> Deserialize<'de> for VecDate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let v = Vec::<NumberOrString>::deserialize(deserializer)?;
Ok(VecDate(
v.into_iter()
.filter_map(|v| match v {
NumberOrString::Number(n) => Some(Ok(n)),
NumberOrString::String(s) if s.is_empty() => None,
NumberOrString::String(s) => Some(s.parse().map_err(|_| {
serde::de::Error::custom(format!("invalid number: {}", s))
})),
})
.collect::<Result<_, _>>()?,
))
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct FixedDateRange {
pub start: FixedDate,
pub end: Option<FixedDate>,
}
impl TryFrom<VecDateRange> for FixedDateRange {
type Error = ();
fn try_from(value: VecDateRange) -> Result<Self, Self::Error> {
let mut v = value.0.into_iter();
let start = v.next().ok_or(())?.into();
let end = v.next().map(|v| v.into());
if v.next().is_some() {
return Err(());
}
Ok(FixedDateRange { start, end })
}
}
impl FromStr for FixedDateRange {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut s = Scanner::new(s);
let start = parse_date(&mut s).ok_or(())?;
let end =
if s.eat() == Some('/') { Some(parse_date(&mut s).ok_or(())?) } else { None };
Ok(FixedDateRange { start, end })
}
}
impl<'de> Deserialize<'de> for FixedDateRange {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(|_| serde::de::Error::custom("invalid date"))
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[allow(missing_docs)]
pub struct FixedDate {
pub year: i16,
pub month: Option<u8>,
pub day: Option<u8>,
pub season: Option<Season>,
pub circa: bool,
}
impl From<VecDate> for FixedDate {
fn from(value: VecDate) -> Self {
let mut v = value.0.into_iter();
let year = v.next().unwrap();
let month = v.next().map(|v| (v - 1) as u8);
let day = v.next().map(|v| (v - 1) as u8);
FixedDate { year, month, day, season: None, circa: false }
}
}
impl FromStr for FixedDate {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut s = Scanner::new(s);
parse_date(&mut s).ok_or(())
}
}
impl<'de> Deserialize<'de> for FixedDate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(|_| serde::de::Error::custom("invalid date"))
}
}
fn parse_date(s: &mut Scanner<'_>) -> Option<FixedDate> {
let year = s.eat_while(char::is_ascii_digit);
let year = year.parse().ok()?;
if s.peek() != Some('-') {
return Some(FixedDate {
year,
month: None,
day: None,
season: None,
circa: matches!(s.peek(), Some('~')),
});
}
s.eat();
let month = s.eat_while(char::is_ascii_digit);
let month = month.parse::<u8>().ok()? - 1;
if month > 11 {
return None;
}
if s.peek() != Some('-') {
return Some(FixedDate {
year,
month: Some(month),
day: None,
season: None,
circa: matches!(s.peek(), Some('~')),
});
}
s.eat();
let day = s.eat_while(char::is_ascii_digit);
let day = day.parse::<u8>().ok()? - 1;
if day > 31 {
return None;
}
Some(FixedDate {
year,
month: Some(month),
day: Some(day),
season: None,
circa: matches!(s.peek(), Some('~')),
})
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Citation {
pub citation_id: String,
pub citation_items: Vec<CitationItem>,
pub properties: CitationProperties,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct CitationItem {
pub id: String,
pub locator: Option<String>,
pub label: Option<String>,
#[serde(default)]
pub suppress_author: bool,
pub prefix: Option<String>,
pub suffix: Option<String>,
pub position: Option<u8>,
pub near_note: Option<bool>,
}
impl<'de> Deserialize<'de> for CitationItem {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
struct CitationItemRaw {
id: NumberOrString,
locator: Option<NumberOrString>,
label: Option<String>,
#[serde(default)]
suppress_author: bool,
prefix: Option<String>,
suffix: Option<String>,
position: Option<u8>,
near_note: Option<bool>,
}
let raw = CitationItemRaw::deserialize(deserializer)?;
Ok(CitationItem {
id: raw.id.into_string(),
locator: raw.locator.map(NumberOrString::into_string),
label: raw.label,
suppress_author: raw.suppress_author,
prefix: raw.prefix,
suffix: raw.suffix,
position: raw.position,
near_note: raw.near_note,
})
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CitationProperties {
note_index: Option<u32>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum NumberOrString {
Number(i16),
String(String),
}
impl NumberOrString {
fn into_string(self) -> String {
match self {
NumberOrString::Number(n) => n.to_string(),
NumberOrString::String(s) => s,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialize() {
let mut map = BTreeMap::new();
map.insert("title".to_string(), Value::String("The Title".to_string()));
map.insert(
"author".to_string(),
Value::Names(vec![NameValue::Item(NameItem {
family: "Doe".to_string(),
given: Some("John".to_string()),
non_dropping_particle: None,
dropping_particle: None,
suffix: None,
comma_suffix: None,
})]),
);
map.insert(
"date".to_string(),
Value::Date(DateValue::Raw {
raw: FixedDateRange::from_str("2021-09-10/2022-01-01").unwrap(),
literal: None,
season: None,
}),
);
let item = Item(map);
println!("{}", serde_json::to_string_pretty(&item).unwrap());
}
#[test]
fn test_approximate() {
let d: DateValue = serde_json::from_str(r#"{"raw": "2025-09~"}"#).unwrap();
assert!(d.is_approx());
let d: DateValue = serde_json::from_str(
r#"{
"circa": "true",
"date-parts": [
[
2005,
12,
15
]
]}"#,
)
.unwrap();
assert!(d.is_approx());
let d: DateValue = serde_json::from_str(
r#"{
"circa": true,
"date-parts": [
[
2005,
12,
15
]
]}"#,
)
.unwrap();
assert!(d.is_approx());
let d: DateValue = serde_json::from_str(
r#"{
"circa": 1,
"date-parts": [
[
2005,
12,
15
]
]}"#,
)
.unwrap();
assert!(d.is_approx());
}
}