toml_datetime_compat/
lib.rs

1//! Adds a functionality to easily convert between [toml_datetime]'s and
2//! [chrono](::chrono)'s/[time](::time)'s types.
3//!
4//! # Features
5//! - `chrono` enables [chrono](::chrono) conversions
6//! - `time` enables [time](::time) conversions
7//! - `serde_with` enables [`TomlDateTime`] to use with
8//!   [serde_with](::serde_with)
9//!
10//! # Using [`serde`] derive macros
11//! This crate can be used with
12//! [`#[serde(with="toml_datetime_compat")]`](https://serde.rs/field-attrs.html#with),
13//! but the functions [`deserialize`] and [`serialize`] can also be used on
14//! their own to (de)serialize [`chrono`](::chrono) and [`time`](::time) types.
15//!
16//! Meaning this struct
17//! ```
18//! # use serde::{Deserialize, Serialize};
19//! #[derive(Deserialize, Serialize)]
20//! struct SomeDateTimes {
21#![cfg_attr(
22    feature = "chrono",
23    doc = r#"
24    #[serde(with = "toml_datetime_compat")]
25    chrono_naive_date: chrono::NaiveDate,
26    #[serde(with = "toml_datetime_compat")]
27    chrono_naive_time: chrono::NaiveTime,
28    #[serde(with = "toml_datetime_compat")]
29    chrono_naive_date_time: chrono::NaiveDateTime,
30    #[serde(with = "toml_datetime_compat")]
31    chrono_date_time_utc: chrono::DateTime<chrono::Utc>,
32    #[serde(with = "toml_datetime_compat")]
33    chrono_date_time_offset: chrono::DateTime<chrono::FixedOffset>,
34    // Options work with any other supported type, too
35    #[serde(with = "toml_datetime_compat", default)]
36    chrono_date_time_utc_optional_present: Option<chrono::DateTime<chrono::Utc>>,
37    #[serde(with = "toml_datetime_compat", default)]
38    chrono_date_time_utc_optional_nonpresent: Option<chrono::DateTime<chrono::Utc>>,"#
39)]
40#![cfg_attr(
41    feature = "time",
42    doc = r#"
43    #[serde(with = "toml_datetime_compat")]
44    time_date: time::Date,
45    #[serde(with = "toml_datetime_compat")]
46    time_time: time::Time,
47    #[serde(with = "toml_datetime_compat")]
48    time_primitive_date_time: time::PrimitiveDateTime,
49    #[serde(with = "toml_datetime_compat")]
50    time_offset_date_time: time::OffsetDateTime,
51    // Options work with any other supported type, too
52    #[serde(with = "toml_datetime_compat", default)]
53    time_primitive_date_time_optional_present: Option<time::PrimitiveDateTime>,
54    #[serde(with = "toml_datetime_compat", default)]
55    time_primitive_date_time_optional_nonpresent: Option<time::PrimitiveDateTime>,"#
56)]
57//! }
58//! ```
59//! will (de)serialize from/to
60//! ```toml
61#![cfg_attr(
62    feature = "time",
63    doc = r"chrono_naive_date = 1523-08-20
64chrono_naive_time = 23:54:33.000011235
65chrono_naive_date_time = 1523-08-20T23:54:33.000011235
66chrono_date_time_utc = 1523-08-20T23:54:33.000011235Z
67chrono_date_time_offset = 1523-08-20T23:54:33.000011235+04:30
68chrono_date_time_utc_optional_present = 1523-08-20T23:54:33.000011235Z"
69)]
70#![cfg_attr(
71    feature = "time",
72    doc = r"time_date = 1523-08-20
73time_time = 23:54:33.000011235
74time_primitive_date_time = 1523-08-20T23:54:33.000011235
75time_offset_date_time = 1523-08-20T23:54:33.000011235+04:30
76time_primitive_date_time_optional_present = 1523-08-20T23:54:33.000011235"
77)]
78//! ```
79//!
80#![cfg_attr(
81    feature = "time",
82    doc = r"# Using [serde_with](::serde_with)
83
84It is also possible to use [serde_with](::serde_with) using the [`TomlDateTime`]
85converter.
86
87This is especially helpful to deserialize optional date time values (due to
88[serde-rs/serde#723](https://github.com/serde-rs/serde/issues/723)) if the
89existing support for `Option` is insufficient.
90
91"
92)]
93//! # Using [`FromToTomlDateTime`]
94//!
95//! And by introducing a new trait [`FromToTomlDateTime`] that adds
96//! [`to_toml`](FromToTomlDateTime::to_toml) and
97//! [`from_toml`](FromToTomlDateTime::from_toml) functions to the relevant
98//! structs from [`chrono`](::chrono) and [`time`](::time).
99#![warn(clippy::pedantic, missing_docs)]
100#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
101use std::result::Result as StdResult;
102
103use serde::{de::Error as _, ser::Error as _, Deserialize, Deserializer, Serialize, Serializer};
104use toml_datetime::Datetime as TomlDatetime;
105#[cfg(any(feature = "chrono", feature = "time"))]
106use toml_datetime::{Date as TomlDate, Offset as TomlOffset, Time as TomlTime};
107
108#[cfg(feature = "serde_with")]
109pub use crate::serde_with::TomlDateTime;
110
111/// Function that can be used with
112/// [`#[serde(deserialize_with="toml_datetime_compat::deserialize")]`](https://serde.rs/field-attrs.html#deserialize_with)
113#[allow(clippy::missing_errors_doc)]
114pub fn deserialize<'de, D: Deserializer<'de>, T: TomlDateTimeSerde>(
115    deserializer: D,
116) -> StdResult<T, D::Error> {
117    T::deserialize(deserializer)
118}
119
120/// Function that can be used with
121/// [`#[serde(serialize_with="toml_datetime_compat::serialize")]`](https://serde.rs/field-attrs.html#serialize_with)
122#[allow(clippy::missing_errors_doc)]
123pub fn serialize<S: Serializer, T: TomlDateTimeSerde>(
124    value: &T,
125    serializer: S,
126) -> StdResult<S::Ok, S::Error> {
127    T::serialize(value, serializer)
128}
129
130#[cfg(feature = "serde_with")]
131mod serde_with {
132    use serde::{Deserializer, Serializer};
133    use serde_with::{DeserializeAs, SerializeAs};
134
135    use crate::FromToTomlDateTime;
136
137    /// Struct to allow the integration into the [`serde_with`](::serde_with)
138    /// ecosystem
139    #[cfg_attr(
140        any(feature = "time", feature = "chrono"),
141        doc = r#"```
142# use serde::{Deserialize, Serialize};
143use serde_with::serde_as;
144
145#[serde_as]
146#[derive(Serialize, Deserialize)]
147struct OptionalDateTimes {
148    #[serde_as(as = "Option<toml_datetime_compat::TomlDateTime>")]"#
149    )]
150    #[cfg_attr(feature = "time", doc = "    value: Option<time::Date>")]
151    #[cfg_attr(
152        all(not(feature = "time"), feature = "chrono"),
153        doc = "    value: Option<chrono::NaiveDate>"
154    )]
155    #[cfg_attr(
156        any(feature = "time", feature = "chrono"),
157        doc = "}
158```"
159    )]
160    pub struct TomlDateTime;
161
162    impl<'de, T: FromToTomlDateTime> DeserializeAs<'de, T> for TomlDateTime {
163        fn deserialize_as<D: Deserializer<'de>>(deserializer: D) -> Result<T, D::Error> {
164            crate::deserialize(deserializer)
165        }
166    }
167    impl<T: FromToTomlDateTime> SerializeAs<T> for TomlDateTime {
168        fn serialize_as<S: Serializer>(source: &T, serializer: S) -> Result<S::Ok, S::Error> {
169            crate::serialize(source, serializer)
170        }
171    }
172}
173
174/// Error that can occur while transforming [`TomlDatetime`] from and to
175/// [`chrono`](::chrono) and [`time`](::time) types
176#[derive(thiserror::Error, Debug)]
177pub enum Error {
178    /// Caused by years that cannot be represented in [`TomlDate::year`]
179    #[error("year out of range for toml")]
180    InvalidYear,
181    /// Caused by converting a [`TomlDatetime`] without a date to a type
182    /// requiring a date component
183    #[error("expected date")]
184    ExpectedDate,
185    /// Caused by converting a [`TomlDatetime`] with a date to a type
186    /// without a date component
187    #[error("unexpected date")]
188    UnexpectedDate,
189    /// Caused by converting a [`TomlDatetime`] without a time to a type
190    /// requiring a time component
191    #[error("expected time")]
192    ExpectedTime,
193    /// Caused by converting a [`TomlDatetime`] with a time to a type
194    /// without a time component
195    #[error("unexpected time")]
196    UnexpectedTime,
197    /// Caused by converting a [`TomlDatetime`] without a time zone to a type
198    /// requiring a time zone component
199    #[error("expected time zone")]
200    ExpectedTimeZone,
201    /// Caused by converting a [`TomlDatetime`] with a time zone to a type
202    /// without a time zone component
203    #[error("unexpected offset")]
204    UnexpectedTimeZone,
205    /// Caused by converting a [`TomlDatetime`] without the UTC time zone to a
206    /// type requiring UTC time zone
207    #[error("expected UTC date time (either `Z` or +00:00)")]
208    ExpectedUtcTimeZone,
209    /// Creating rust type failed due to the date time parsed by
210    /// [`TomlDatetime`] being invalid
211    ///
212    /// [`toml_datetime`] should already validate this
213    #[error("unable to create rust type from toml type")]
214    UnableToCreateRustType,
215}
216
217type Result<T> = StdResult<T, Error>;
218
219/// Used to implement serialization atop [`FromToTomlDateTime`] for
220/// [`TomlDatetime`] and various container types.
221pub trait TomlDateTimeSerde {
222    /// Deserializes into a `Self`.
223    fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error>
224    where
225        Self: Sized;
226    /// Serializes a `Self`.
227    fn serialize<S: Serializer>(value: &Self, serializer: S) -> StdResult<S::Ok, S::Error>;
228}
229impl<T: FromToTomlDateTime> TomlDateTimeSerde for T {
230    fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
231        FromToTomlDateTime::from_toml(TomlDatetime::deserialize(deserializer)?)
232            .map_err(D::Error::custom)
233    }
234
235    fn serialize<S: Serializer>(value: &Self, serializer: S) -> StdResult<S::Ok, S::Error> {
236        value
237            .to_toml()
238            .map_err(S::Error::custom)?
239            .serialize(serializer)
240    }
241}
242impl<T: FromToTomlDateTime> TomlDateTimeSerde for Option<T> {
243    fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
244        use serde::de;
245        struct OptionVisitor<T>(std::marker::PhantomData<T>);
246        impl<'de, T: FromToTomlDateTime> de::Visitor<'de> for OptionVisitor<T> {
247            type Value = Option<T>;
248
249            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
250                formatter.write_str("an optional date time")
251            }
252
253            fn visit_none<E: de::Error>(self) -> StdResult<Self::Value, E> {
254                Ok(None)
255            }
256
257            fn visit_some<D: Deserializer<'de>>(
258                self,
259                deserializer: D,
260            ) -> StdResult<Self::Value, D::Error> {
261                T::deserialize(deserializer).map(Some)
262            }
263        }
264        deserializer.deserialize_option(OptionVisitor(std::marker::PhantomData::<T>))
265    }
266
267    fn serialize<S: Serializer>(value: &Self, serializer: S) -> StdResult<S::Ok, S::Error> {
268        match value {
269            Some(value) => serializer.serialize_some(&value.to_toml().map_err(S::Error::custom)?),
270            None => serializer.serialize_none(),
271        }
272    }
273}
274
275/// Trait that allows easy conversion between [`TomlDatetime`] and
276/// [`chrono`'s](::chrono)/[`time`'s](::time) types
277pub trait FromToTomlDateTime: Sized {
278    /// Converts from a [`TomlDatetime`]
279    ///
280    /// # Errors
281    /// Fails when the [`TomlDatetime`] contains data not representable by
282    /// [`Self`] or is missing data required by [`Self`]
283    fn from_toml(value: TomlDatetime) -> Result<Self>;
284    /// Converts to a [`TomlDatetime`]
285    ///
286    /// # Errors
287    /// Fails when the [`Self`] is not representable by [`TomlDatetime`] mainly
288    /// due to a negative year
289    fn to_toml(&self) -> Result<TomlDatetime>;
290}
291
292#[cfg(feature = "chrono")]
293mod chrono {
294    use chrono::{
295        DateTime, Datelike, Duration, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Offset,
296        Timelike, Utc,
297    };
298
299    use crate::{Error, FromToTomlDateTime, Result, TomlDate, TomlDatetime, TomlOffset, TomlTime};
300
301    impl FromToTomlDateTime for NaiveDate {
302        fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
303            if time.is_some() {
304                return Err(Error::UnexpectedTime);
305            }
306            if offset.is_some() {
307                return Err(Error::UnexpectedTimeZone);
308            }
309            let TomlDate { year, month, day } = date.ok_or(Error::ExpectedDate)?;
310            NaiveDate::from_ymd_opt(year.into(), month.into(), day.into())
311                .ok_or(Error::UnableToCreateRustType)
312        }
313
314        fn to_toml(&self) -> Result<TomlDatetime> {
315            Ok(TomlDatetime {
316                date: Some(TomlDate {
317                    year: self.year().try_into().map_err(|_| Error::InvalidYear)?,
318                    month: self.month() as u8,
319                    day: self.day() as u8,
320                }),
321                time: None,
322                offset: None,
323            })
324        }
325    }
326
327    impl FromToTomlDateTime for NaiveTime {
328        fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
329            if date.is_some() {
330                return Err(Error::UnexpectedDate);
331            }
332            if offset.is_some() {
333                return Err(Error::UnexpectedTimeZone);
334            }
335            let TomlTime {
336                hour,
337                minute,
338                second,
339                nanosecond,
340            } = time.ok_or(Error::ExpectedTime)?;
341            NaiveTime::from_hms_nano_opt(hour.into(), minute.into(), second.into(), nanosecond)
342                .ok_or(Error::UnableToCreateRustType)
343        }
344
345        fn to_toml(&self) -> Result<TomlDatetime> {
346            Ok(TomlDatetime {
347                date: None,
348                time: Some(TomlTime {
349                    hour: self.hour() as u8,
350                    minute: self.minute() as u8,
351                    second: self.second() as u8,
352                    nanosecond: self.nanosecond(),
353                }),
354                offset: None,
355            })
356        }
357    }
358
359    impl FromToTomlDateTime for NaiveDateTime {
360        fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
361            let date = NaiveDate::from_toml(TomlDatetime {
362                date,
363                time: None,
364                offset,
365            })?;
366            Ok(if time.is_some() {
367                NaiveDateTime::new(
368                    date,
369                    NaiveTime::from_toml(TomlDatetime {
370                        date: None,
371                        time,
372                        offset,
373                    })?,
374                )
375            } else {
376                date.and_hms_opt(0, 0, 0).expect("00:00:00 is a valid time")
377            })
378        }
379
380        fn to_toml(&self) -> Result<TomlDatetime> {
381            Ok(TomlDatetime {
382                date: self.date().to_toml()?.date,
383                time: self.time().to_toml()?.time,
384                offset: None,
385            })
386        }
387    }
388
389    impl FromToTomlDateTime for DateTime<Utc> {
390        fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
391            match offset {
392                Some(
393                    TomlOffset::Z
394                    | TomlOffset::Custom {
395                        hours: 0,
396                        minutes: 0,
397                    },
398                ) => {
399                    let date = NaiveDateTime::from_toml(TomlDatetime {
400                        date,
401                        time,
402                        offset: None,
403                    })?;
404                    Ok(DateTime::from_utc(date, Utc))
405                }
406                _ => Err(Error::ExpectedUtcTimeZone),
407            }
408        }
409
410        fn to_toml(&self) -> Result<TomlDatetime> {
411            let date_time = self.naive_local().to_toml()?;
412            Ok(TomlDatetime {
413                offset: Some(TomlOffset::Z),
414                ..date_time
415            })
416        }
417    }
418
419    impl FromToTomlDateTime for DateTime<FixedOffset> {
420        fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
421            match offset {
422                Some(offset) => {
423                    let date = NaiveDateTime::from_toml(TomlDatetime {
424                        date,
425                        time,
426                        offset: None,
427                    })?;
428                    Ok(DateTime::from_local(date, match offset {
429                        TomlOffset::Z => {
430                            FixedOffset::east_opt(0).expect("00:00 is a valid time zone offset")
431                        }
432                        TomlOffset::Custom { hours, minutes } => FixedOffset::east_opt(
433                            i32::from(hours) * 60 * 60
434                                + i32::from(minutes)
435                                    * 60
436                                    * if hours.is_positive() { 1 } else { -1 },
437                        )
438                        .ok_or(Error::UnableToCreateRustType)?,
439                    }))
440                }
441                _ => Err(Error::ExpectedTimeZone),
442            }
443        }
444
445        fn to_toml(&self) -> Result<TomlDatetime> {
446            let timezone = Duration::seconds(self.timezone().fix().local_minus_utc().into());
447            let hours = timezone.num_hours();
448            let minutes = timezone.num_minutes() - hours * 60;
449            let date_time = self.naive_local().to_toml()?;
450            Ok(TomlDatetime {
451                offset: Some(TomlOffset::Custom {
452                    hours: hours as i8,
453                    minutes: minutes as u8,
454                }),
455                ..date_time
456            })
457        }
458    }
459}
460
461#[cfg(feature = "time")]
462mod time {
463    use time::{error::ComponentRange, Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset};
464
465    use crate::{Error, FromToTomlDateTime, Result, TomlDate, TomlDatetime, TomlOffset, TomlTime};
466
467    impl From<ComponentRange> for Error {
468        fn from(_: ComponentRange) -> Self {
469            Self::UnableToCreateRustType
470        }
471    }
472
473    impl FromToTomlDateTime for Date {
474        fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
475            if time.is_some() {
476                return Err(Error::UnexpectedTime);
477            }
478            if offset.is_some() {
479                return Err(Error::UnexpectedTimeZone);
480            }
481            let TomlDate { year, month, day } = date.ok_or(Error::ExpectedDate)?;
482            Date::from_calendar_date(year.into(), month.try_into()?, day).map_err(From::from)
483        }
484
485        fn to_toml(&self) -> Result<TomlDatetime> {
486            Ok(TomlDatetime {
487                date: Some(TomlDate {
488                    year: self.year().try_into().map_err(|_| Error::InvalidYear)?,
489                    month: self.month() as u8,
490                    day: self.day(),
491                }),
492                time: None,
493                offset: None,
494            })
495        }
496    }
497
498    impl FromToTomlDateTime for Time {
499        fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
500            if date.is_some() {
501                return Err(Error::UnexpectedDate);
502            }
503            if offset.is_some() {
504                return Err(Error::UnexpectedTimeZone);
505            }
506            let TomlTime {
507                hour,
508                minute,
509                second,
510                nanosecond,
511            } = time.ok_or(Error::ExpectedTime)?;
512            Time::from_hms_nano(hour, minute, second, nanosecond).map_err(From::from)
513        }
514
515        fn to_toml(&self) -> Result<TomlDatetime> {
516            Ok(TomlDatetime {
517                date: None,
518                time: Some(TomlTime {
519                    hour: self.hour(),
520                    minute: self.minute(),
521                    second: self.second(),
522                    nanosecond: self.nanosecond(),
523                }),
524                offset: None,
525            })
526        }
527    }
528
529    impl FromToTomlDateTime for PrimitiveDateTime {
530        fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
531            let date = Date::from_toml(TomlDatetime {
532                date,
533                time: None,
534                offset,
535            })?;
536            Ok(if time.is_some() {
537                PrimitiveDateTime::new(
538                    date,
539                    Time::from_toml(TomlDatetime {
540                        date: None,
541                        time,
542                        offset,
543                    })?,
544                )
545            } else {
546                date.midnight()
547            })
548        }
549
550        fn to_toml(&self) -> Result<TomlDatetime> {
551            Ok(TomlDatetime {
552                date: self.date().to_toml()?.date,
553                time: self.time().to_toml()?.time,
554                offset: None,
555            })
556        }
557    }
558
559    impl FromToTomlDateTime for OffsetDateTime {
560        fn from_toml(TomlDatetime { date, time, offset }: TomlDatetime) -> Result<Self> {
561            match offset {
562                Some(offset) => {
563                    let date = PrimitiveDateTime::from_toml(TomlDatetime {
564                        date,
565                        time,
566                        offset: None,
567                    })?;
568                    Ok(date.assume_offset(match offset {
569                        TomlOffset::Z => UtcOffset::UTC,
570                        TomlOffset::Custom { hours, minutes } => UtcOffset::from_hms(
571                            hours,
572                            minutes
573                                .try_into()
574                                .map_err(|_| Error::UnableToCreateRustType)?,
575                            0,
576                        )
577                        .map_err(|_| Error::UnableToCreateRustType)?,
578                    }))
579                }
580                _ => Err(Error::ExpectedTimeZone),
581            }
582        }
583
584        fn to_toml(&self) -> Result<TomlDatetime> {
585            Ok(TomlDatetime {
586                date: self.date().to_toml()?.date,
587                time: self.time().to_toml()?.time,
588                offset: Some(TomlOffset::Custom {
589                    hours: self.offset().whole_hours(),
590                    minutes: self.offset().minutes_past_hour().unsigned_abs(),
591                }),
592            })
593        }
594    }
595}
596
597#[test]
598#[cfg(feature = "chrono")]
599fn chrono() {
600    use ::chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc};
601    use indoc::formatdoc;
602    use pretty_assertions::assert_eq;
603    use serde::{Deserialize, Serialize};
604
605    const Y: i32 = 1523;
606    const M: u32 = 8;
607    const D: u32 = 20;
608    const H: u32 = 23;
609    const MIN: u32 = 54;
610    const S: u32 = 33;
611    const NS: u32 = 11_235;
612    const OH: i32 = 4;
613    const OM: i32 = 30;
614
615    #[derive(Serialize, Deserialize, Debug, PartialEq)]
616    struct Test {
617        #[serde(with = "crate")]
618        naive_date: NaiveDate,
619        #[serde(with = "crate")]
620        naive_time: NaiveTime,
621        #[serde(with = "crate")]
622        naive_date_time: NaiveDateTime,
623        #[serde(with = "crate")]
624        date_time_utc: DateTime<Utc>,
625        #[serde(with = "crate")]
626        date_time_offset: DateTime<FixedOffset>,
627        #[serde(with = "crate", default)]
628        date_time_utc_optional_present: Option<DateTime<Utc>>,
629        #[serde(with = "crate", default)]
630        date_time_utc_optional_nonpresent: Option<DateTime<Utc>>,
631    }
632
633    let naive_date = NaiveDate::from_ymd_opt(Y, M, D).unwrap();
634    let naive_time = NaiveTime::from_hms_nano_opt(H, MIN, S, NS).unwrap();
635    let naive_date_time = NaiveDateTime::new(naive_date, naive_time);
636
637    let input = Test {
638        naive_date,
639        naive_time,
640        naive_date_time,
641        date_time_utc: DateTime::from_utc(naive_date_time, Utc),
642        date_time_offset: DateTime::from_local(
643            naive_date_time,
644            FixedOffset::east_opt((OH * 60 + OM) * 60).unwrap(),
645        ),
646        date_time_utc_optional_present: Some(DateTime::from_utc(naive_date_time, Utc)),
647        date_time_utc_optional_nonpresent: None,
648    };
649
650    let serialized = toml::to_string(&input).unwrap();
651
652    assert_eq!(
653        serialized,
654        dbg!(formatdoc! {"
655            naive_date = {Y:04}-{M:02}-{D:02}
656            naive_time = {H:02}:{MIN:02}:{S:02}.{NS:09}
657            naive_date_time = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}
658            date_time_utc = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}Z
659            date_time_offset = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}+{OH:02}:{OM:02}
660            date_time_utc_optional_present = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}Z
661            "})
662    );
663
664    assert_eq!(toml::from_str::<Test>(&serialized).unwrap(), input);
665}
666
667#[cfg(all(feature = "time", test))]
668mod time_test {
669    use ::time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset};
670    use indoc::formatdoc;
671    use pretty_assertions::assert_eq;
672    use serde::{Deserialize, Serialize};
673
674    const Y: i32 = 1523;
675    const M: u8 = 8;
676    const D: u8 = 20;
677    const H: u8 = 23;
678    const MIN: u8 = 54;
679    const S: u8 = 33;
680    const NS: u32 = 11_235;
681    const OH: i8 = 4;
682    const OM: i8 = 30;
683
684    #[test]
685    fn time() {
686        #[derive(Serialize, Deserialize, Debug, PartialEq)]
687        struct Test {
688            #[serde(with = "crate")]
689            date: Date,
690            #[serde(with = "crate")]
691            time: Time,
692            #[serde(with = "crate")]
693            primitive_date_time: PrimitiveDateTime,
694            #[serde(with = "crate")]
695            offset_date_time: OffsetDateTime,
696            #[serde(with = "crate", default)]
697            primitive_date_time_optional_present: Option<PrimitiveDateTime>,
698            #[serde(with = "crate", default)]
699            primitive_date_time_optional_nonpresent: Option<PrimitiveDateTime>,
700        }
701
702        let date = Date::from_calendar_date(Y, Month::try_from(M).unwrap(), D).unwrap();
703        let time = Time::from_hms_nano(H, MIN, S, NS).unwrap();
704        let primitive_date_time = PrimitiveDateTime::new(date, time);
705
706        let input = Test {
707            date,
708            time,
709            primitive_date_time,
710            offset_date_time: primitive_date_time
711                .assume_offset(UtcOffset::from_hms(OH, OM, 0).unwrap()),
712            primitive_date_time_optional_present: Some(primitive_date_time),
713            primitive_date_time_optional_nonpresent: None,
714        };
715
716        let serialized = toml::to_string(&input).unwrap();
717
718        assert_eq!(
719            serialized,
720            dbg!(formatdoc! {"
721            date = {Y:04}-{M:02}-{D:02}
722            time = {H:02}:{MIN:02}:{S:02}.{NS:09}
723            primitive_date_time = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}
724            offset_date_time = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}+{OH:02}:{OM:02}
725            primitive_date_time_optional_present = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}
726            "})
727        );
728
729        assert_eq!(toml::from_str::<Test>(&serialized).unwrap(), input);
730    }
731
732    #[test]
733    #[cfg(feature = "serde_with")]
734    fn serde_with() {
735        use serde_with::serde_as;
736
737        use crate::TomlDateTime;
738
739        #[serde_as]
740        #[derive(Serialize, Deserialize, Debug, PartialEq)]
741        struct Test {
742            #[serde_as(as = "Option<TomlDateTime>")]
743            optional_date_time: Option<OffsetDateTime>,
744        }
745
746        let input = Test {
747            optional_date_time: Some(
748                PrimitiveDateTime::new(
749                    Date::from_calendar_date(Y, Month::try_from(M).unwrap(), D).unwrap(),
750                    Time::from_hms_nano(H, MIN, S, NS).unwrap(),
751                )
752                .assume_offset(UtcOffset::from_hms(OH, OM, 0).unwrap()),
753            ),
754        };
755
756        let serialized = toml::to_string(&input).unwrap();
757
758        assert_eq!(
759            serialized,
760            dbg!(formatdoc! {"
761            optional_date_time = {Y:04}-{M:02}-{D:02}T{H:02}:{MIN:02}:{S:02}.{NS:09}+{OH:02}:{OM:02}
762            "})
763        );
764
765        assert_eq!(toml::from_str::<Test>(&serialized).unwrap(), input);
766
767        let input = Test {
768            optional_date_time: None,
769        };
770
771        let serialized = toml::to_string(&input).unwrap();
772
773        assert!(serialized.trim().is_empty());
774
775        assert_eq!(toml::from_str::<Test>(&serialized).unwrap(), input);
776    }
777}