use crate::reference::types::RefDate;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "bindings", derive(specta::Type))]
pub struct DateValue {
pub value: String,
#[cfg_attr(feature = "bindings", specta(optional))]
pub note: Option<String>,
}
impl DateValue {
pub fn new(value: impl Into<String>) -> Self {
Self {
value: value.into(),
note: None,
}
}
pub fn is_empty(&self) -> bool {
self.value.is_empty()
}
pub fn parse(&self) -> RefDate {
match self.value.parse::<citum_edtf::Edtf>() {
Ok(edtf) => RefDate::Edtf(edtf),
Err(_) => RefDate::Literal(self.value.clone()),
}
}
pub fn year(&self) -> String {
match self.parse() {
RefDate::Edtf(edtf) => edtf.year().to_string(),
RefDate::Literal(_) => String::new(),
}
}
pub fn day(&self) -> Option<u32> {
match self.parse() {
RefDate::Edtf(edtf) => edtf.day().filter(|&d| d > 0),
RefDate::Literal(_) => None,
}
}
pub fn is_uncertain(&self) -> bool {
self.value.contains('?')
}
pub fn is_approximate(&self) -> bool {
self.value.contains('~')
}
pub fn is_range(&self) -> bool {
matches!(self.parse(), RefDate::Edtf(edtf) if edtf.is_range())
}
pub fn is_open_range(&self) -> bool {
matches!(self.parse(), RefDate::Edtf(edtf) if edtf.is_open_range())
}
pub fn time(&self) -> Option<citum_edtf::Time> {
match self.parse() {
RefDate::Edtf(edtf) => edtf.time(),
_ => None,
}
}
pub fn has_time(&self) -> bool {
self.time().is_some()
}
}
impl fmt::Display for DateValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.value)
}
}
impl From<String> for DateValue {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl From<&str> for DateValue {
fn from(value: &str) -> Self {
Self::new(value)
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct DateValueStructured {
value: String,
#[serde(default)]
note: Option<String>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum DateValueRepr {
Scalar(String),
Structured(DateValueStructured),
}
impl<'de> Deserialize<'de> for DateValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(match DateValueRepr::deserialize(deserializer)? {
DateValueRepr::Scalar(value) => DateValue { value, note: None },
DateValueRepr::Structured(DateValueStructured { value, note }) => {
DateValue { value, note }
}
})
}
}
impl Serialize for DateValue {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match &self.note {
None => serializer.serialize_str(&self.value),
Some(note) => {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("value", &self.value)?;
map.serialize_entry("note", note)?;
map.end()
}
}
}
}
#[cfg(feature = "schema")]
impl JsonSchema for DateValue {
fn schema_name() -> std::borrow::Cow<'static, str> {
"DateValue".into()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let scalar_schema = generator.subschema_for::<String>();
let structured_schema = schemars::json_schema!({
"type": "object",
"properties": {
"value": generator.subschema_for::<String>(),
"note": generator.subschema_for::<Option<String>>()
},
"required": ["value"],
"additionalProperties": false
});
schemars::json_schema!({
"oneOf": [scalar_schema, structured_schema]
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, reason = "Panicking is acceptable in tests.")]
mod tests {
use super::*;
#[test]
fn scalar_input_round_trips_byte_identically() {
let json = r#""1947""#;
let date: DateValue = serde_json::from_str(json).unwrap();
assert_eq!(date, DateValue::new("1947"));
assert_eq!(serde_json::to_string(&date).unwrap(), json);
}
#[test]
fn mapping_form_parses_value_and_note() {
let json = r#"{"value":"1947","note":"民国三十六年"}"#;
let date: DateValue = serde_json::from_str(json).unwrap();
assert_eq!(date.value, "1947");
assert_eq!(date.note.as_deref(), Some("民国三十六年"));
}
#[test]
fn mapping_form_without_note_defaults_to_none() {
let json = r#"{"value":"1947"}"#;
let date: DateValue = serde_json::from_str(json).unwrap();
assert_eq!(date, DateValue::new("1947"));
}
#[test]
fn mapping_form_rejects_unknown_fields() {
let json = r#"{"value":"1947","note":"民国三十六年","calendar":"minguo"}"#;
let err = serde_json::from_str::<DateValue>(json).unwrap_err();
assert!(
err.to_string()
.contains("did not match any variant of untagged enum")
);
}
#[test]
fn mapping_form_requires_value() {
let json = r#"{"note":"民国三十六年"}"#;
assert!(serde_json::from_str::<DateValue>(json).is_err());
}
#[test]
fn note_present_serializes_as_mapping_not_scalar() {
let date = DateValue {
value: "1947".to_string(),
note: Some("民国三十六年".to_string()),
};
let json = serde_json::to_string(&date).unwrap();
assert_eq!(json, r#"{"value":"1947","note":"民国三十六年"}"#);
let round_tripped: DateValue = serde_json::from_str(&json).unwrap();
assert_eq!(round_tripped, date);
}
#[test]
fn note_is_ignored_by_value_oriented_accessors() {
let annotated = DateValue {
value: "1947".to_string(),
note: Some("民国三十六年".to_string()),
};
let plain = DateValue::new("1947");
assert_eq!(annotated.value, plain.value);
assert_eq!(annotated.year(), plain.year());
assert_eq!(annotated.is_empty(), plain.is_empty());
}
}