citum_schema_data/reference/
date.rs1use crate::reference::types::RefDate;
7#[cfg(feature = "schema")]
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12#[derive(Debug, Clone, Default, PartialEq)]
27#[cfg_attr(feature = "bindings", derive(specta::Type))]
28pub struct DateValue {
29 pub value: String,
31 #[cfg_attr(feature = "bindings", specta(optional))]
34 pub note: Option<String>,
35}
36
37impl DateValue {
38 pub fn new(value: impl Into<String>) -> Self {
40 Self {
41 value: value.into(),
42 note: None,
43 }
44 }
45
46 pub fn is_empty(&self) -> bool {
48 self.value.is_empty()
49 }
50
51 pub fn parse(&self) -> RefDate {
53 match self.value.parse::<citum_edtf::Edtf>() {
54 Ok(edtf) => RefDate::Edtf(edtf),
55 Err(_) => RefDate::Literal(self.value.clone()),
56 }
57 }
58
59 pub fn year(&self) -> String {
61 match self.parse() {
62 RefDate::Edtf(edtf) => edtf.year().to_string(),
63 RefDate::Literal(_) => String::new(),
64 }
65 }
66
67 pub fn day(&self) -> Option<u32> {
69 match self.parse() {
70 RefDate::Edtf(edtf) => edtf.day().filter(|&d| d > 0),
71 RefDate::Literal(_) => None,
72 }
73 }
74
75 pub fn is_uncertain(&self) -> bool {
77 self.value.contains('?')
78 }
79
80 pub fn is_approximate(&self) -> bool {
82 self.value.contains('~')
83 }
84
85 pub fn is_range(&self) -> bool {
87 matches!(self.parse(), RefDate::Edtf(edtf) if edtf.is_range())
88 }
89
90 pub fn is_open_range(&self) -> bool {
92 matches!(self.parse(), RefDate::Edtf(edtf) if edtf.is_open_range())
93 }
94
95 pub fn time(&self) -> Option<citum_edtf::Time> {
97 match self.parse() {
98 RefDate::Edtf(edtf) => edtf.time(),
99 _ => None,
100 }
101 }
102
103 pub fn has_time(&self) -> bool {
105 self.time().is_some()
106 }
107}
108
109impl fmt::Display for DateValue {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 write!(f, "{}", self.value)
112 }
113}
114
115impl From<String> for DateValue {
116 fn from(value: String) -> Self {
117 Self::new(value)
118 }
119}
120
121impl From<&str> for DateValue {
122 fn from(value: &str) -> Self {
123 Self::new(value)
124 }
125}
126
127#[derive(Deserialize)]
131#[serde(deny_unknown_fields)]
132struct DateValueStructured {
133 value: String,
134 #[serde(default)]
135 note: Option<String>,
136}
137
138#[derive(Deserialize)]
139#[serde(untagged)]
140enum DateValueRepr {
141 Scalar(String),
142 Structured(DateValueStructured),
143}
144
145impl<'de> Deserialize<'de> for DateValue {
146 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
147 where
148 D: serde::Deserializer<'de>,
149 {
150 Ok(match DateValueRepr::deserialize(deserializer)? {
151 DateValueRepr::Scalar(value) => DateValue { value, note: None },
152 DateValueRepr::Structured(DateValueStructured { value, note }) => {
153 DateValue { value, note }
154 }
155 })
156 }
157}
158
159impl Serialize for DateValue {
160 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
161 where
162 S: serde::Serializer,
163 {
164 match &self.note {
165 None => serializer.serialize_str(&self.value),
166 Some(note) => {
167 use serde::ser::SerializeMap;
168 let mut map = serializer.serialize_map(Some(2))?;
169 map.serialize_entry("value", &self.value)?;
170 map.serialize_entry("note", note)?;
171 map.end()
172 }
173 }
174 }
175}
176
177#[cfg(feature = "schema")]
178impl JsonSchema for DateValue {
179 fn schema_name() -> std::borrow::Cow<'static, str> {
180 "DateValue".into()
181 }
182
183 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
184 let scalar_schema = generator.subschema_for::<String>();
185 let structured_schema = schemars::json_schema!({
186 "type": "object",
187 "properties": {
188 "value": generator.subschema_for::<String>(),
189 "note": generator.subschema_for::<Option<String>>()
190 },
191 "required": ["value"],
192 "additionalProperties": false
193 });
194 schemars::json_schema!({
195 "oneOf": [scalar_schema, structured_schema]
196 })
197 }
198}
199
200#[cfg(test)]
201#[allow(clippy::unwrap_used, reason = "Panicking is acceptable in tests.")]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn scalar_input_round_trips_byte_identically() {
207 let json = r#""1947""#;
208 let date: DateValue = serde_json::from_str(json).unwrap();
209 assert_eq!(date, DateValue::new("1947"));
210 assert_eq!(serde_json::to_string(&date).unwrap(), json);
211 }
212
213 #[test]
214 fn mapping_form_parses_value_and_note() {
215 let json = r#"{"value":"1947","note":"民国三十六年"}"#;
216 let date: DateValue = serde_json::from_str(json).unwrap();
217 assert_eq!(date.value, "1947");
218 assert_eq!(date.note.as_deref(), Some("民国三十六年"));
219 }
220
221 #[test]
222 fn mapping_form_without_note_defaults_to_none() {
223 let json = r#"{"value":"1947"}"#;
224 let date: DateValue = serde_json::from_str(json).unwrap();
225 assert_eq!(date, DateValue::new("1947"));
226 }
227
228 #[test]
229 fn mapping_form_rejects_unknown_fields() {
230 let json = r#"{"value":"1947","note":"民国三十六年","calendar":"minguo"}"#;
231 let err = serde_json::from_str::<DateValue>(json).unwrap_err();
232 assert!(
233 err.to_string()
234 .contains("did not match any variant of untagged enum")
235 );
236 }
237
238 #[test]
239 fn mapping_form_requires_value() {
240 let json = r#"{"note":"民国三十六年"}"#;
241 assert!(serde_json::from_str::<DateValue>(json).is_err());
242 }
243
244 #[test]
245 fn note_present_serializes_as_mapping_not_scalar() {
246 let date = DateValue {
247 value: "1947".to_string(),
248 note: Some("民国三十六年".to_string()),
249 };
250 let json = serde_json::to_string(&date).unwrap();
251 assert_eq!(json, r#"{"value":"1947","note":"民国三十六年"}"#);
252
253 let round_tripped: DateValue = serde_json::from_str(&json).unwrap();
254 assert_eq!(round_tripped, date);
255 }
256
257 #[test]
258 fn note_is_ignored_by_value_oriented_accessors() {
259 let annotated = DateValue {
263 value: "1947".to_string(),
264 note: Some("民国三十六年".to_string()),
265 };
266 let plain = DateValue::new("1947");
267 assert_eq!(annotated.value, plain.value);
268 assert_eq!(annotated.year(), plain.year());
269 assert_eq!(annotated.is_empty(), plain.is_empty());
270 }
271}