1#![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#![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#
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#![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#[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#[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 #[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#[derive(thiserror::Error, Debug)]
177pub enum Error {
178 #[error("year out of range for toml")]
180 InvalidYear,
181 #[error("expected date")]
184 ExpectedDate,
185 #[error("unexpected date")]
188 UnexpectedDate,
189 #[error("expected time")]
192 ExpectedTime,
193 #[error("unexpected time")]
196 UnexpectedTime,
197 #[error("expected time zone")]
200 ExpectedTimeZone,
201 #[error("unexpected offset")]
204 UnexpectedTimeZone,
205 #[error("expected UTC date time (either `Z` or +00:00)")]
208 ExpectedUtcTimeZone,
209 #[error("unable to create rust type from toml type")]
214 UnableToCreateRustType,
215}
216
217type Result<T> = StdResult<T, Error>;
218
219pub trait TomlDateTimeSerde {
222 fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error>
224 where
225 Self: Sized;
226 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
275pub trait FromToTomlDateTime: Sized {
278 fn from_toml(value: TomlDatetime) -> Result<Self>;
284 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}