Skip to main content

reifydb_value/value/
iso.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use 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}