#![cfg_attr(
feature = "chrono",
doc = r#"
#[serde(with = "toml_datetime_compat")]
chrono_naive_date: chrono::NaiveDate,
#[serde(with = "toml_datetime_compat")]
chrono_naive_time: chrono::NaiveTime,
#[serde(with = "toml_datetime_compat")]
chrono_naive_date_time: chrono::NaiveDateTime,
#[serde(with = "toml_datetime_compat")]
chrono_date_time_utc: chrono::DateTime<chrono::Utc>,
#[serde(with = "toml_datetime_compat")]
chrono_date_time_offset: chrono::DateTime<chrono::FixedOffset>,
// Options work with any other supported type, too
#[serde(with = "toml_datetime_compat", default)]
chrono_date_time_utc_optional_present: Option<chrono::DateTime<chrono::Utc>>,
#[serde(with = "toml_datetime_compat", default)]
chrono_date_time_utc_optional_nonpresent: Option<chrono::DateTime<chrono::Utc>>,"#
)]
#![cfg_attr(
feature = "time",
doc = r#"
#[serde(with = "toml_datetime_compat")]
time_date: time::Date,
#[serde(with = "toml_datetime_compat")]
time_time: time::Time,
#[serde(with = "toml_datetime_compat")]
time_primitive_date_time: time::PrimitiveDateTime,
#[serde(with = "toml_datetime_compat")]
time_offset_date_time: time::OffsetDateTime,
// Options work with any other supported type, too
#[serde(with = "toml_datetime_compat", default)]
time_primitive_date_time_optional_present: Option<time::PrimitiveDateTime>,
#[serde(with = "toml_datetime_compat", default)]
time_primitive_date_time_optional_nonpresent: Option<time::PrimitiveDateTime>,"#
)]
#![cfg_attr(
feature = "time",
doc = r"chrono_naive_date = 1523-08-20
chrono_naive_time = 23:54:33.000011235
chrono_naive_date_time = 1523-08-20T23:54:33.000011235
chrono_date_time_utc = 1523-08-20T23:54:33.000011235Z
chrono_date_time_offset = 1523-08-20T23:54:33.000011235+04:30
chrono_date_time_utc_optional_present = 1523-08-20T23:54:33.000011235Z"
)]
#![cfg_attr(
feature = "time",
doc = r"time_date = 1523-08-20
time_time = 23:54:33.000011235
time_primitive_date_time = 1523-08-20T23:54:33.000011235
time_offset_date_time = 1523-08-20T23:54:33.000011235+04:30
time_primitive_date_time_optional_present = 1523-08-20T23:54:33.000011235"
)]
#![cfg_attr(
feature = "time",
doc = r"# Using [serde_with](::serde_with)
It is also possible to use [serde_with](::serde_with) using the [`TomlDateTime`]
converter.
This is especially helpful to deserialize optional date time values (due to
[serde-rs/serde#723](https://github.com/serde-rs/serde/issues/723)) if the
existing support for `Option` is insufficient.
"
)]
#![warn(clippy::pedantic, missing_docs)]
#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
use std::result::Result as StdResult;
use serde::{de::Error as _, ser::Error as _, Deserialize, Deserializer, Serialize, Serializer};
use toml_datetime::Datetime as TomlDatetime;
#[cfg(any(feature = "chrono", feature = "time"))]
use toml_datetime::{Date as TomlDate, Offset as TomlOffset, Time as TomlTime};
#[cfg(feature = "serde_with")]
pub use crate::serde_with::TomlDateTime;
#[allow(clippy::missing_errors_doc)]
pub fn deserialize<'de, D: Deserializer<'de>, T: TomlDateTimeSerde>(
deserializer: D,
) -> StdResult<T, D::Error> {
T::deserialize(deserializer)
}
#[allow(clippy::missing_errors_doc)]
pub fn serialize<S: Serializer, T: TomlDateTimeSerde>(
value: &T,
serializer: S,
) -> StdResult<S::Ok, S::Error> {
T::serialize(value, serializer)
}
#[cfg(feature = "serde_with")]
mod serde_with {
use serde::{Deserializer, Serializer};
use serde_with::{DeserializeAs, SerializeAs};
use crate::FromToTomlDateTime;
#[cfg_attr(
any(feature = "time", feature = "chrono"),
doc = r#"```
# use serde::{Deserialize, Serialize};
use serde_with::serde_as;
#[serde_as]
#[derive(Serialize, Deserialize)]
struct OptionalDateTimes {
#[serde_as(as = "Option<toml_datetime_compat::TomlDateTime>")]"#
)]
#[cfg_attr(feature = "time", doc = " value: Option<time::Date>")]
#[cfg_attr(
all(not(feature = "time"), feature = "chrono"),
doc = " value: Option<chrono::NaiveDate>"
)]
#[cfg_attr(
any(feature = "time", feature = "chrono"),
doc = "}
```"
)]
pub struct TomlDateTime;
impl<'de, T: FromToTomlDateTime> DeserializeAs<'de, T> for TomlDateTime {
fn deserialize_as<D: Deserializer<'de>>(deserializer: D) -> Result<T, D::Error> {
crate::deserialize(deserializer)
}
}
impl<T: FromToTomlDateTime> SerializeAs<T> for TomlDateTime {
fn serialize_as<S: Serializer>(source: &T, serializer: S) -> Result<S::Ok, S::Error> {
crate::serialize(source, serializer)
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("year out of range for toml")]
InvalidYear,
#[error("expected date")]
ExpectedDate,
#[error("unexpected date")]
UnexpectedDate,
#[error("expected time")]
ExpectedTime,
#[error("unexpected time")]
UnexpectedTime,
#[error("expected time zone")]
ExpectedTimeZone,
#[error("unexpected offset")]
UnexpectedTimeZone,
#[error("expected UTC date time (either `Z` or +00:00)")]
ExpectedUtcTimeZone,
#[error("unable to create rust type from toml type")]
UnableToCreateRustType,
}
type Result<T> = StdResult<T, Error>;
pub trait TomlDateTimeSerde {
fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error>
where
Self: Sized;
fn serialize<S: Serializer>(value: &Self, serializer: S) -> StdResult<S::Ok, S::Error>;
}
impl<T: FromToTomlDateTime> TomlDateTimeSerde for T {
fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
FromToTomlDateTime::from_toml(TomlDatetime::deserialize(deserializer)?)
.map_err(D::Error::custom)
}
fn serialize<S: Serializer>(value: &Self, serializer: S) -> StdResult<S::Ok, S::Error> {
value
.to_toml()
.map_err(S::Error::custom)?
.serialize(serializer)
}
}
impl<T: FromToTomlDateTime> TomlDateTimeSerde for Option<T> {
fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
use serde::de;
struct OptionVisitor<T>(std::marker::PhantomData<T>);
impl<'de, T: FromToTomlDateTime> de::Visitor<'de> for OptionVisitor<T> {
type Value = Option<T>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("an optional date time")
}
fn visit_none<E: de::Error>(self) -> StdResult<Self::Value, E> {
Ok(None)
}
fn visit_some<D: Deserializer<'de>>(
self,
deserializer: D,
) -> StdResult<Self::Value, D::Error> {
T::deserialize(deserializer).map(Some)
}
}
deserializer.deserialize_option(OptionVisitor(std::marker::PhantomData::<T>))
}
fn serialize<S: Serializer>(value: &Self, serializer: S) -> StdResult<S::Ok, S::Error> {
match value {
Some(value) => serializer.serialize_some(&value.to_toml().map_err(S::Error::custom)?),
None => serializer.serialize_none(),
}
}
}
pub trait FromToTomlDateTime: Sized {
fn from_toml(value: TomlDatetime) -> Result<Self>;
fn to_toml(&self) -> Result<TomlDatetime>;
}
#[cfg(feature = "chrono")]
mod chrono {
use chrono::{
DateTime, Datelike, Duration, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Offset,
Timelike, Utc,
};
use crate::{Error, FromToTomlDateTime, Result, TomlDate, TomlDatetime, TomlOffset, TomlTime};
impl FromToTomlDateTime for NaiveDate {
fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
if time.is_some() {
return Err(Error::UnexpectedTime);
}
if offset.is_some() {
return Err(Error::UnexpectedTimeZone);
}
let TomlDate { year, month, day } = date.ok_or(Error::ExpectedDate)?;
NaiveDate::from_ymd_opt(year.into(), month.into(), day.into())
.ok_or(Error::UnableToCreateRustType)
}
fn to_toml(&self) -> Result<TomlDatetime> {
Ok(TomlDatetime {
date: Some(TomlDate {
year: self.year().try_into().map_err(|_| Error::InvalidYear)?,
month: self.month() as u8,
day: self.day() as u8,
}),
time: None,
offset: None,
})
}
}
impl FromToTomlDateTime for NaiveTime {
fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
if date.is_some() {
return Err(Error::UnexpectedDate);
}
if offset.is_some() {
return Err(Error::UnexpectedTimeZone);
}
let TomlTime {
hour,
minute,
second,
nanosecond,
} = time.ok_or(Error::ExpectedTime)?;
NaiveTime::from_hms_nano_opt(hour.into(), minute.into(), second.into(), nanosecond)
.ok_or(Error::UnableToCreateRustType)
}
fn to_toml(&self) -> Result<TomlDatetime> {
Ok(TomlDatetime {
date: None,
time: Some(TomlTime {
hour: self.hour() as u8,
minute: self.minute() as u8,
second: self.second() as u8,
nanosecond: self.nanosecond(),
}),
offset: None,
})
}
}
impl FromToTomlDateTime for NaiveDateTime {
fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
let date = NaiveDate::from_toml(TomlDatetime {
date,
time: None,
offset,
})?;
Ok(if time.is_some() {
NaiveDateTime::new(
date,
NaiveTime::from_toml(TomlDatetime {
date: None,
time,
offset,
})?,
)
} else {
date.and_hms_opt(0, 0, 0).expect("00:00:00 is a valid time")
})
}
fn to_toml(&self) -> Result<TomlDatetime> {
Ok(TomlDatetime {
date: self.date().to_toml()?.date,
time: self.time().to_toml()?.time,
offset: None,
})
}
}
impl FromToTomlDateTime for DateTime<Utc> {
fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
match offset {
Some(
TomlOffset::Z
| TomlOffset::Custom {
hours: 0,
minutes: 0,
},
) => {
let date = NaiveDateTime::from_toml(TomlDatetime {
date,
time,
offset: None,
})?;
Ok(DateTime::from_utc(date, Utc))
}
_ => Err(Error::ExpectedUtcTimeZone),
}
}
fn to_toml(&self) -> Result<TomlDatetime> {
let date_time = self.naive_local().to_toml()?;
Ok(TomlDatetime {
offset: Some(TomlOffset::Z),
..date_time
})
}
}
impl FromToTomlDateTime for DateTime<FixedOffset> {
fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
match offset {
Some(offset) => {
let date = NaiveDateTime::from_toml(TomlDatetime {
date,
time,
offset: None,
})?;
Ok(DateTime::from_local(date, match offset {
TomlOffset::Z => {
FixedOffset::east_opt(0).expect("00:00 is a valid time zone offset")
}
TomlOffset::Custom { hours, minutes } => FixedOffset::east_opt(
i32::from(hours) * 60 * 60
+ i32::from(minutes)
* 60
* if hours.is_positive() { 1 } else { -1 },
)
.ok_or(Error::UnableToCreateRustType)?,
}))
}
_ => Err(Error::ExpectedTimeZone),
}
}
fn to_toml(&self) -> Result<TomlDatetime> {
let timezone = Duration::seconds(self.timezone().fix().local_minus_utc().into());
let hours = timezone.num_hours();
let minutes = timezone.num_minutes() - hours * 60;
let date_time = self.naive_local().to_toml()?;
Ok(TomlDatetime {
offset: Some(TomlOffset::Custom {
hours: hours as i8,
minutes: minutes as u8,
}),
..date_time
})
}
}
}
#[cfg(feature = "time")]
mod time {
use time::{error::ComponentRange, Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset};
use crate::{Error, FromToTomlDateTime, Result, TomlDate, TomlDatetime, TomlOffset, TomlTime};
impl From<ComponentRange> for Error {
fn from(_: ComponentRange) -> Self {
Self::UnableToCreateRustType
}
}
impl FromToTomlDateTime for Date {
fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
if time.is_some() {
return Err(Error::UnexpectedTime);
}
if offset.is_some() {
return Err(Error::UnexpectedTimeZone);
}
let TomlDate { year, month, day } = date.ok_or(Error::ExpectedDate)?;
Date::from_calendar_date(year.into(), month.try_into()?, day).map_err(From::from)
}
fn to_toml(&self) -> Result<TomlDatetime> {
Ok(TomlDatetime {
date: Some(TomlDate {
year: self.year().try_into().map_err(|_| Error::InvalidYear)?,
month: self.month() as u8,
day: self.day(),
}),
time: None,
offset: None,
})
}
}
impl FromToTomlDateTime for Time {
fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
if date.is_some() {
return Err(Error::UnexpectedDate);
}
if offset.is_some() {
return Err(Error::UnexpectedTimeZone);
}
let TomlTime {
hour,
minute,
second,
nanosecond,
} = time.ok_or(Error::ExpectedTime)?;
Time::from_hms_nano(hour, minute, second, nanosecond).map_err(From::from)
}
fn to_toml(&self) -> Result<TomlDatetime> {
Ok(TomlDatetime {
date: None,
time: Some(TomlTime {
hour: self.hour(),
minute: self.minute(),
second: self.second(),
nanosecond: self.nanosecond(),
}),
offset: None,
})
}
}
impl FromToTomlDateTime for PrimitiveDateTime {
fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
let date = Date::from_toml(TomlDatetime {
date,
time: None,
offset,
})?;
Ok(if time.is_some() {
PrimitiveDateTime::new(
date,
Time::from_toml(TomlDatetime {
date: None,
time,
offset,
})?,
)
} else {
date.midnight()
})
}
fn to_toml(&self) -> Result<TomlDatetime> {
Ok(TomlDatetime {
date: self.date().to_toml()?.date,
time: self.time().to_toml()?.time,
offset: None,
})
}
}
impl FromToTomlDateTime for OffsetDateTime {
fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
match offset {
Some(offset) => {
let date = PrimitiveDateTime::from_toml(TomlDatetime {
date,
time,
offset: None,
})?;
Ok(date.assume_offset(match offset {
TomlOffset::Z => UtcOffset::UTC,
TomlOffset::Custom { hours, minutes } => UtcOffset::from_hms(
hours,
minutes
.try_into()
.map_err(|_| Error::UnableToCreateRustType)?,
0,
)
.map_err(|_| Error::UnableToCreateRustType)?,
}))
}
_ => Err(Error::ExpectedTimeZone),
}
}
fn to_toml(&self) -> Result<TomlDatetime> {
Ok(TomlDatetime {
date: self.date().to_toml()?.date,
time: self.time().to_toml()?.time,
offset: Some(TomlOffset::Custom {
hours: self.offset().whole_hours(),
minutes: self.offset().minutes_past_hour().unsigned_abs(),
}),
})
}
}
}
#[test]
#[cfg(feature = "chrono")]
fn chrono() {
use ::chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use indoc::formatdoc;
use pretty_assertions::assert_eq;
use serde::{Deserialize, Serialize};
const Y: i32 = 1523;
const M: u32 = 8;
const D: u32 = 20;
const H: u32 = 23;
const MIN: u32 = 54;
const S: u32 = 33;
const NS: u32 = 11_235;
const OH: i32 = 4;
const OM: i32 = 30;
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Test {
#[serde(with = "crate")]
naive_date: NaiveDate,
#[serde(with = "crate")]
naive_time: NaiveTime,
#[serde(with = "crate")]
naive_date_time: NaiveDateTime,
#[serde(with = "crate")]
date_time_utc: DateTime<Utc>,
#[serde(with = "crate")]
date_time_offset: DateTime<FixedOffset>,
#[serde(with = "crate", default)]
date_time_utc_optional_present: Option<DateTime<Utc>>,
#[serde(with = "crate", default)]
date_time_utc_optional_nonpresent: Option<DateTime<Utc>>,
}
let naive_date = NaiveDate::from_ymd_opt(Y, M, D).unwrap();
let naive_time = NaiveTime::from_hms_nano_opt(H, MIN, S, NS).unwrap();
let naive_date_time = NaiveDateTime::new(naive_date, naive_time);
let input = Test {
naive_date,
naive_time,
naive_date_time,
date_time_utc: DateTime::from_utc(naive_date_time, Utc),
date_time_offset: DateTime::from_local(
naive_date_time,
FixedOffset::east_opt((OH * 60 + OM) * 60).unwrap(),
),
date_time_utc_optional_present: Some(DateTime::from_utc(naive_date_time, Utc)),
date_time_utc_optional_nonpresent: None,
};
let serialized = toml::to_string(&input).unwrap();
assert_eq!(
serialized,
dbg!(formatdoc! {"
naive_date = {Y:04}-{M:02}-{D:02}
naive_time = {H:02}:{MIN:02}:{S:02}.{NS:09}
naive_date_time = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}
date_time_utc = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}Z
date_time_offset = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}+{OH:02}:{OM:02}
date_time_utc_optional_present = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}Z
"})
);
assert_eq!(toml::from_str::<Test>(&serialized).unwrap(), input);
}
#[cfg(all(feature = "time", test))]
mod time_test {
use ::time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset};
use indoc::formatdoc;
use pretty_assertions::assert_eq;
use serde::{Deserialize, Serialize};
const Y: i32 = 1523;
const M: u8 = 8;
const D: u8 = 20;
const H: u8 = 23;
const MIN: u8 = 54;
const S: u8 = 33;
const NS: u32 = 11_235;
const OH: i8 = 4;
const OM: i8 = 30;
#[test]
fn time() {
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Test {
#[serde(with = "crate")]
date: Date,
#[serde(with = "crate")]
time: Time,
#[serde(with = "crate")]
primitive_date_time: PrimitiveDateTime,
#[serde(with = "crate")]
offset_date_time: OffsetDateTime,
#[serde(with = "crate", default)]
primitive_date_time_optional_present: Option<PrimitiveDateTime>,
#[serde(with = "crate", default)]
primitive_date_time_optional_nonpresent: Option<PrimitiveDateTime>,
}
let date = Date::from_calendar_date(Y, Month::try_from(M).unwrap(), D).unwrap();
let time = Time::from_hms_nano(H, MIN, S, NS).unwrap();
let primitive_date_time = PrimitiveDateTime::new(date, time);
let input = Test {
date,
time,
primitive_date_time,
offset_date_time: primitive_date_time
.assume_offset(UtcOffset::from_hms(OH, OM, 0).unwrap()),
primitive_date_time_optional_present: Some(primitive_date_time),
primitive_date_time_optional_nonpresent: None,
};
let serialized = toml::to_string(&input).unwrap();
assert_eq!(
serialized,
dbg!(formatdoc! {"
date = {Y:04}-{M:02}-{D:02}
time = {H:02}:{MIN:02}:{S:02}.{NS:09}
primitive_date_time = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}
offset_date_time = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}+{OH:02}:{OM:02}
primitive_date_time_optional_present = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}
"})
);
assert_eq!(toml::from_str::<Test>(&serialized).unwrap(), input);
}
#[test]
#[cfg(feature = "serde_with")]
fn serde_with() {
use serde_with::serde_as;
use crate::TomlDateTime;
#[serde_as]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Test {
#[serde_as(as = "Option<TomlDateTime>")]
optional_date_time: Option<OffsetDateTime>,
}
let input = Test {
optional_date_time: Some(
PrimitiveDateTime::new(
Date::from_calendar_date(Y, Month::try_from(M).unwrap(), D).unwrap(),
Time::from_hms_nano(H, MIN, S, NS).unwrap(),
)
.assume_offset(UtcOffset::from_hms(OH, OM, 0).unwrap()),
),
};
let serialized = toml::to_string(&input).unwrap();
assert_eq!(
serialized,
dbg!(formatdoc! {"
optional_date_time = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}+{OH:02}:{OM:02}
"})
);
assert_eq!(toml::from_str::<Test>(&serialized).unwrap(), input);
let input = Test {
optional_date_time: None,
};
let serialized = toml::to_string(&input).unwrap();
assert!(serialized.trim().is_empty());
assert_eq!(toml::from_str::<Test>(&serialized).unwrap(), input);
}
}