1use std::{mem::take, ops::Deref};
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
7
8use crate::value::{
9 Value,
10 date::Date,
11 datetime::DateTime,
12 duration::Duration,
13 time::Time,
14 try_from::{FromValueError, TryFromValue},
15};
16
17macro_rules! iso_wrapper {
18 ($iso:ident, $inner:ty) => {
19 #[repr(transparent)]
20 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
21 pub struct $iso(pub $inner);
22
23 impl From<$inner> for $iso {
24 fn from(value: $inner) -> Self {
25 Self(value)
26 }
27 }
28
29 impl From<$iso> for $inner {
30 fn from(value: $iso) -> Self {
31 value.0
32 }
33 }
34
35 impl Deref for $iso {
36 type Target = $inner;
37
38 fn deref(&self) -> &Self::Target {
39 &self.0
40 }
41 }
42
43 impl TryFromValue for $iso {
44 fn try_from_value(value: &Value) -> Result<Self, FromValueError> {
45 <$inner>::try_from_value(value).map($iso)
46 }
47 }
48 };
49}
50
51iso_wrapper!(IsoDate, Date);
52iso_wrapper!(IsoTime, Time);
53iso_wrapper!(IsoDateTime, DateTime);
54iso_wrapper!(IsoDuration, Duration);
55
56impl Serialize for IsoDate {
57 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
58 serializer.serialize_str(&self.0.to_string())
59 }
60}
61
62impl Serialize for IsoTime {
63 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
64 serializer.serialize_str(&self.0.to_string())
65 }
66}
67
68impl Serialize for IsoDateTime {
69 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
70 serializer.serialize_str(&self.0.to_string())
71 }
72}
73
74impl Serialize for IsoDuration {
75 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
76 serializer.serialize_str(&self.0.to_iso_string())
77 }
78}
79
80impl<'de> Deserialize<'de> for IsoDate {
81 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
82 let raw = String::deserialize(deserializer)?;
83 parse_date(&raw).map(IsoDate).map_err(de::Error::custom)
84 }
85}
86
87impl<'de> Deserialize<'de> for IsoTime {
88 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
89 let raw = String::deserialize(deserializer)?;
90 parse_time(&raw).map(IsoTime).map_err(de::Error::custom)
91 }
92}
93
94impl<'de> Deserialize<'de> for IsoDateTime {
95 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
96 let raw = String::deserialize(deserializer)?;
97 parse_datetime(&raw).map(IsoDateTime).map_err(de::Error::custom)
98 }
99}
100
101impl<'de> Deserialize<'de> for IsoDuration {
102 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
103 let raw = String::deserialize(deserializer)?;
104 parse_duration(&raw).map(IsoDuration).map_err(de::Error::custom)
105 }
106}
107
108fn parse_date_parts(s: &str) -> Result<(i32, u32, u32), String> {
109 let (sign, rest) = match s.strip_prefix('-') {
110 Some(rest) => (-1, rest),
111 None => (1, s),
112 };
113 let mut parts = rest.split('-');
114 let year = parts.next().unwrap_or_default();
115 let month = parts.next().ok_or_else(|| format!("missing month in date '{s}'"))?;
116 let day = parts.next().ok_or_else(|| format!("missing day in date '{s}'"))?;
117 if parts.next().is_some() {
118 return Err(format!("unexpected extra component in date '{s}'"));
119 }
120 let year: i32 = year.parse().map_err(|_| format!("invalid year in date '{s}'"))?;
121 let month: u32 = month.parse().map_err(|_| format!("invalid month in date '{s}'"))?;
122 let day: u32 = day.parse().map_err(|_| format!("invalid day in date '{s}'"))?;
123 Ok((sign * year, month, day))
124}
125
126fn parse_time_parts(s: &str) -> Result<(u32, u32, u32, u32), String> {
127 let mut parts = s.split(':');
128 let hour = parts.next().unwrap_or_default();
129 let minute = parts.next().ok_or_else(|| format!("missing minute in time '{s}'"))?;
130 let second = parts.next().ok_or_else(|| format!("missing second in time '{s}'"))?;
131 if parts.next().is_some() {
132 return Err(format!("unexpected extra component in time '{s}'"));
133 }
134 let hour: u32 = hour.parse().map_err(|_| format!("invalid hour in time '{s}'"))?;
135 let minute: u32 = minute.parse().map_err(|_| format!("invalid minute in time '{s}'"))?;
136 let (second_str, fraction) = match second.split_once('.') {
137 Some((a, b)) => (a, b),
138 None => (second, ""),
139 };
140 let second: u32 = second_str.parse().map_err(|_| format!("invalid second in time '{s}'"))?;
141 let nano = parse_fraction_nanos(fraction).ok_or_else(|| format!("invalid fractional second in time '{s}'"))?;
142 Ok((hour, minute, second, nano))
143}
144
145fn parse_fraction_nanos(fraction: &str) -> Option<u32> {
146 if fraction.is_empty() {
147 return Some(0);
148 }
149 if !fraction.bytes().all(|b| b.is_ascii_digit()) {
150 return None;
151 }
152 let mut padded = fraction.to_string();
153 padded.truncate(9);
154 while padded.len() < 9 {
155 padded.push('0');
156 }
157 padded.parse().ok()
158}
159
160fn parse_date(s: &str) -> Result<Date, String> {
161 let (year, month, day) = parse_date_parts(s)?;
162 Date::from_ymd(year, month, day).map_err(|e| e.to_string())
163}
164
165fn parse_time(s: &str) -> Result<Time, String> {
166 let (hour, minute, second, nano) = parse_time_parts(s)?;
167 Time::from_hms_nano(hour, minute, second, nano).map_err(|e| e.to_string())
168}
169
170fn parse_datetime(s: &str) -> Result<DateTime, String> {
171 let body = s.strip_suffix('Z').unwrap_or(s);
172 let (date_part, time_part) =
173 body.split_once('T').ok_or_else(|| format!("missing 'T' separator in datetime '{s}'"))?;
174 let (year, month, day) = parse_date_parts(date_part)?;
175 let (hour, minute, second, nano) = parse_time_parts(time_part)?;
176 DateTime::new(year, month, day, hour, minute, second, nano).ok_or_else(|| format!("invalid datetime '{s}'"))
177}
178
179fn parse_duration(s: &str) -> Result<Duration, String> {
180 let rest = s.strip_prefix('P').ok_or_else(|| format!("duration must start with 'P': '{s}'"))?;
181 let (date_part, time_part) = match rest.split_once('T') {
182 Some((date, time)) => (date, time),
183 None => (rest, ""),
184 };
185
186 let mut years = 0i64;
187 let mut months = 0i64;
188 let mut days = 0i64;
189 for (number, unit) in split_components(date_part)? {
190 let value: i64 = number.parse().map_err(|_| format!("invalid duration number '{number}'"))?;
191 match unit {
192 'Y' => years = value,
193 'M' => months = value,
194 'D' => days = value,
195 other => return Err(format!("invalid duration date unit '{other}'")),
196 }
197 }
198
199 let mut hours = 0i64;
200 let mut minutes = 0i64;
201 let mut seconds = 0i64;
202 let mut fraction_nanos = 0i64;
203 for (number, unit) in split_components(time_part)? {
204 match unit {
205 'H' => hours = number.parse().map_err(|_| format!("invalid duration hours '{number}'"))?,
206 'M' => minutes = number.parse().map_err(|_| format!("invalid duration minutes '{number}'"))?,
207 'S' => {
208 let negative = number.starts_with('-');
209 let (second_str, fraction) = match number.split_once('.') {
210 Some((a, b)) => (a, b),
211 None => (number.as_str(), ""),
212 };
213 seconds = second_str
214 .parse()
215 .map_err(|_| format!("invalid duration seconds '{number}'"))?;
216 let magnitude = parse_fraction_nanos(fraction)
217 .ok_or_else(|| format!("invalid duration fraction '{number}'"))?
218 as i64;
219 fraction_nanos = if negative {
220 -magnitude
221 } else {
222 magnitude
223 };
224 }
225 other => return Err(format!("invalid duration time unit '{other}'")),
226 }
227 }
228
229 let total_months =
230 i32::try_from(years * 12 + months).map_err(|_| format!("duration months out of range in '{s}'"))?;
231 let total_days = i32::try_from(days).map_err(|_| format!("duration days out of range in '{s}'"))?;
232 let nanos = (hours * 3600 + minutes * 60 + seconds) * 1_000_000_000 + fraction_nanos;
233 Duration::new(total_months, total_days, nanos).map_err(|e| e.to_string())
234}
235
236fn split_components(s: &str) -> Result<Vec<(String, char)>, String> {
237 let mut components = Vec::new();
238 let mut number = String::new();
239 for ch in s.chars() {
240 if ch.is_ascii_digit() || ch == '-' || ch == '.' {
241 number.push(ch);
242 } else if ch.is_ascii_alphabetic() {
243 if number.is_empty() {
244 return Err(format!("missing number before '{ch}'"));
245 }
246 components.push((take(&mut number), ch));
247 } else {
248 return Err(format!("unexpected character '{ch}' in duration"));
249 }
250 }
251 if !number.is_empty() {
252 return Err("trailing number without unit in duration".to_string());
253 }
254 Ok(components)
255}
256
257#[cfg(test)]
258mod tests {
259 use serde_json::{from_value, json, to_value};
260
261 use super::*;
262
263 fn datetime() -> DateTime {
264 DateTime::from_timestamp(1_700_000_000).unwrap()
265 }
266
267 #[test]
268 fn serializes_each_wrapper_as_iso8601() {
269 let date = IsoDate(Date::from_ymd(2024, 3, 15).unwrap());
270 assert_eq!(to_value(date).unwrap(), json!("2024-03-15"));
271
272 let time = IsoTime(Time::from_hms_nano(14, 30, 15, 123_456_789).unwrap());
273 assert_eq!(to_value(time).unwrap(), json!("14:30:15.123456789"));
274
275 let dt = IsoDateTime(datetime());
276 let json = to_value(dt).unwrap();
277 let rendered = json.as_str().unwrap();
278 assert!(rendered.contains('T') && rendered.ends_with('Z'), "got {rendered}");
279
280 assert_eq!(to_value(IsoDuration(Duration::from_seconds(90).unwrap())).unwrap(), json!("PT1M30S"));
281 assert_eq!(
282 to_value(IsoDuration(Duration::new(14, 3, 3_661_000_000_000).unwrap())).unwrap(),
283 json!("P1Y2M3DT1H1M1S")
284 );
285 assert_eq!(to_value(IsoDuration(Duration::zero())).unwrap(), json!("PT0S"));
286 }
287
288 #[test]
289 fn round_trips_through_serde() {
290 let values: Vec<IsoDate> = vec![IsoDate(Date::from_ymd(2024, 3, 15).unwrap())];
291 for v in values {
292 let json = to_value(v).unwrap();
293 assert_eq!(from_value::<IsoDate>(json).unwrap(), v);
294 }
295
296 let time = IsoTime(Time::from_hms_nano(14, 30, 15, 123_456_789).unwrap());
297 assert_eq!(from_value::<IsoTime>(to_value(time).unwrap()).unwrap(), time);
298
299 let dt = IsoDateTime(datetime());
300 assert_eq!(from_value::<IsoDateTime>(to_value(dt).unwrap()).unwrap(), dt);
301
302 for d in [
303 IsoDuration(Duration::from_seconds(90).unwrap()),
304 IsoDuration(Duration::new(14, 3, 3_661_000_000_000).unwrap()),
305 IsoDuration(Duration::zero()),
306 ] {
307 assert_eq!(from_value::<IsoDuration>(to_value(d).unwrap()).unwrap(), d);
308 }
309 }
310
311 #[test]
312 fn try_from_value_delegates_to_inner_and_rejects_mismatch() {
313 let dt = datetime();
314 assert_eq!(IsoDateTime::try_from_value(&Value::DateTime(dt)).unwrap(), IsoDateTime(dt));
315
316 let err = IsoDateTime::try_from_value(&Value::Date(Date::from_ymd(2024, 3, 15).unwrap())).unwrap_err();
317 assert!(matches!(err, FromValueError::TypeMismatch { .. }), "a Date is not a DateTime");
318 }
319
320 #[test]
321 fn deserialize_rejects_malformed_input_without_panicking() {
322 assert!(from_value::<IsoDate>(json!("not-a-date")).is_err());
323 assert!(from_value::<IsoDuration>(json!("90s")).is_err());
324 assert!(from_value::<IsoDateTime>(json!("2024-03-15")).is_err());
325 }
326}