pub mod utc {
use chrono::{DateTime, NaiveDateTime, Utc};
use serde::{self, Deserialize, Deserializer, Serializer};
pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&date.to_rfc3339())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
parse_flexible_datetime(&s).map_err(serde::de::Error::custom)
}
fn parse_flexible_datetime(s: &str) -> Result<DateTime<Utc>, String> {
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Ok(dt.with_timezone(&Utc));
}
if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f") {
return Ok(naive.and_utc());
}
if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
return Ok(naive.and_utc());
}
Err(format!(
"Failed to parse datetime '{}'. Expected RFC3339 format (e.g., '2024-01-15T09:30:00Z') \
or ISO 8601 format (e.g., '2024-01-15T09:30:00')",
s
))
}
pub mod option {
use super::*;
pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match date {
Some(dt) => serializer.serialize_some(&dt.to_rfc3339()),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<String> = Option::deserialize(deserializer)?;
match opt {
Some(s) => parse_flexible_datetime(&s)
.map(Some)
.map_err(serde::de::Error::custom),
None => Ok(None),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_rfc3339_with_z() {
let result = parse_flexible_datetime("2024-01-15T09:30:00Z");
assert!(result.is_ok());
}
#[test]
fn test_parse_rfc3339_with_offset() {
let result = parse_flexible_datetime("2024-01-15T09:30:00+00:00");
assert!(result.is_ok());
}
#[test]
fn test_parse_without_timezone() {
let result = parse_flexible_datetime("2024-01-15T09:30:00");
assert!(result.is_ok());
}
#[test]
fn test_parse_with_fractional_seconds() {
let result = parse_flexible_datetime("2024-01-15T09:30:00.123");
assert!(result.is_ok());
}
#[test]
fn test_parse_with_fractional_seconds_and_z() {
let result = parse_flexible_datetime("2024-01-15T09:30:00.123Z");
assert!(result.is_ok());
}
}
}
pub mod offset {
use chrono::{DateTime, FixedOffset, NaiveDateTime};
use serde::{self, Deserialize, Deserializer, Serializer};
pub fn serialize<S>(date: &DateTime<FixedOffset>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&date.to_rfc3339())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<FixedOffset>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
parse_flexible_datetime(&s).map_err(serde::de::Error::custom)
}
fn parse_flexible_datetime(s: &str) -> Result<DateTime<FixedOffset>, String> {
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Ok(dt);
}
let utc_offset = FixedOffset::east_opt(0).unwrap();
if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f") {
return Ok(naive.and_local_timezone(utc_offset).unwrap());
}
if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
return Ok(naive.and_local_timezone(utc_offset).unwrap());
}
Err(format!(
"Failed to parse datetime '{}'. Expected RFC3339 format (e.g., '2024-01-15T09:30:00Z') \
or ISO 8601 format (e.g., '2024-01-15T09:30:00')",
s
))
}
pub mod option {
use super::*;
pub fn serialize<S>(
date: &Option<DateTime<FixedOffset>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match date {
Some(dt) => serializer.serialize_some(&dt.to_rfc3339()),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(
deserializer: D,
) -> Result<Option<DateTime<FixedOffset>>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<String> = Option::deserialize(deserializer)?;
match opt {
Some(s) => parse_flexible_datetime(&s)
.map(Some)
.map_err(serde::de::Error::custom),
None => Ok(None),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_rfc3339_with_z() {
let result = parse_flexible_datetime("2024-01-15T09:30:00Z");
assert!(result.is_ok());
let dt = result.unwrap();
assert_eq!(dt.offset().local_minus_utc(), 0);
}
#[test]
fn test_parse_rfc3339_with_offset() {
let result = parse_flexible_datetime("2024-01-15T09:30:00-05:00");
assert!(result.is_ok());
let dt = result.unwrap();
assert_eq!(dt.offset().local_minus_utc(), -18000);
}
#[test]
fn test_parse_without_timezone() {
let result = parse_flexible_datetime("2024-01-15T09:30:00");
assert!(result.is_ok());
let dt = result.unwrap();
assert_eq!(dt.offset().local_minus_utc(), 0);
}
#[test]
fn test_parse_with_fractional_seconds() {
let result = parse_flexible_datetime("2024-01-15T09:30:00.123");
assert!(result.is_ok());
let dt = result.unwrap();
assert_eq!(dt.offset().local_minus_utc(), 0);
}
#[test]
fn test_parse_with_fractional_seconds_and_z() {
let result = parse_flexible_datetime("2024-01-15T09:30:00.123Z");
assert!(result.is_ok());
let dt = result.unwrap();
assert_eq!(dt.offset().local_minus_utc(), 0);
}
#[test]
fn test_preserves_positive_offset() {
let result = parse_flexible_datetime("2024-01-15T09:30:00+09:00");
assert!(result.is_ok());
let dt = result.unwrap();
assert_eq!(dt.offset().local_minus_utc(), 32400);
}
}
}