Skip to main content

opendev_models/
datetime_compat.rs

1//! Flexible datetime (de)serialization for Python compatibility.
2//!
3//! Python's `datetime.isoformat()` produces strings like `2024-06-15T10:30:00`
4//! (no timezone), while `chrono::DateTime<Utc>` expects RFC3339 with `Z` or offset.
5//! This module handles both formats.
6
7use chrono::{DateTime, NaiveDateTime, Utc};
8use serde::{self, Deserialize, Deserializer, Serializer};
9
10const FORMAT: &str = "%Y-%m-%dT%H:%M:%S";
11const FORMAT_WITH_FRAC: &str = "%Y-%m-%dT%H:%M:%S%.f";
12
13/// Serialize a `DateTime<Utc>` as ISO 8601 string.
14pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
15where
16    S: Serializer,
17{
18    serializer.serialize_str(&date.to_rfc3339())
19}
20
21/// Deserialize a datetime string that may or may not have timezone info.
22pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
23where
24    D: Deserializer<'de>,
25{
26    let s = String::deserialize(deserializer)?;
27    parse_flexible_datetime(&s).map_err(serde::de::Error::custom)
28}
29
30/// Parse a datetime string flexibly, handling:
31/// - RFC3339 with Z: `2024-06-15T10:30:00Z`
32/// - RFC3339 with offset: `2024-06-15T10:30:00+00:00`
33/// - Naive (no timezone): `2024-06-15T10:30:00` (assumed UTC)
34/// - With fractional seconds: `2024-06-15T10:30:00.123456`
35pub fn parse_flexible_datetime(s: &str) -> Result<DateTime<Utc>, String> {
36    // Try RFC3339 first (has timezone)
37    if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
38        return Ok(dt.with_timezone(&Utc));
39    }
40
41    // Try naive datetime with fractional seconds
42    if let Ok(naive) = NaiveDateTime::parse_from_str(s, FORMAT_WITH_FRAC) {
43        return Ok(naive.and_utc());
44    }
45
46    // Try naive datetime without fractional seconds
47    if let Ok(naive) = NaiveDateTime::parse_from_str(s, FORMAT) {
48        return Ok(naive.and_utc());
49    }
50
51    Err(format!("Cannot parse datetime: {s}"))
52}
53
54/// Module for optional datetime fields.
55pub mod option {
56    use chrono::{DateTime, Utc};
57    use serde::{self, Deserialize, Deserializer, Serializer};
58
59    pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
60    where
61        S: Serializer,
62    {
63        match date {
64            Some(dt) => serializer.serialize_str(&dt.to_rfc3339()),
65            None => serializer.serialize_none(),
66        }
67    }
68
69    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
70    where
71        D: Deserializer<'de>,
72    {
73        let opt: Option<String> = Option::deserialize(deserializer)?;
74        match opt {
75            Some(s) => super::parse_flexible_datetime(&s)
76                .map(Some)
77                .map_err(serde::de::Error::custom),
78            None => Ok(None),
79        }
80    }
81}
82
83#[cfg(test)]
84#[path = "datetime_compat_tests.rs"]
85mod tests;