From 694b5597ea9606d44a7f275f5d73c0904c0a3084 Mon Sep 17 00:00:00 2001 From: muji Date: Thu, 12 Dec 2024 10:35:35 +0800 Subject: [PATCH] Serde format datetime and date as rfc 3339 (#18) * Use RFC 3339 date formats for serde. * Bump minor version. --- Cargo.toml | 4 +- src/builder.rs | 15 ++++--- src/date_time.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++ src/helper.rs | 35 +++++++++-------- src/lib.rs | 2 + src/parser.rs | 9 +++-- src/property.rs | 31 +++++++-------- tests/explanatory.rs | 2 +- tests/extensions.rs | 5 ++- tests/types.rs | 32 +++++++-------- 10 files changed, 166 insertions(+), 62 deletions(-) create mode 100644 src/date_time.rs diff --git a/Cargo.toml b/Cargo.toml index a65e3f0..6beb906 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vcard4" -version = "0.6.2" +version = "0.7.0" edition = "2021" description = "Fast and correct vCard parser for RFC6350" repository = "https://github.com/tmpfs/vcard4" @@ -28,7 +28,7 @@ serde = [ "dep:serde", "dep:serde_with", "dep:cfg_eval", - "time/serde-human-readable", + "time/serde", "language-tags?/serde", ] zeroize = ["dep:zeroize"] diff --git a/src/builder.rs b/src/builder.rs index 310e021..1bb3196 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -2,9 +2,8 @@ //! use crate::{ property::{DeliveryAddress, Gender, Kind, TextListProperty}, - Uri, Vcard, + Date, DateTime, Uri, Vcard, }; -use time::{Date, OffsetDateTime}; #[cfg(feature = "language-tags")] use language_tags::LanguageTag; @@ -226,7 +225,7 @@ impl VcardBuilder { } /// Set the revision of the vCard. - pub fn rev(mut self, value: OffsetDateTime) -> Self { + pub fn rev(mut self, value: DateTime) -> Self { self.card.rev = Some(value.into()); self } @@ -315,10 +314,14 @@ mod tests { .nickname("JC".to_owned()) .photo("file:///images/jdoe.jpeg".parse().unwrap()) .birthday( - Date::from_calendar_date(1986, Month::February, 7).unwrap(), + Date::from_calendar_date(1986, Month::February, 7) + .unwrap() + .into(), ) .anniversary( - Date::from_calendar_date(2002, Month::March, 18).unwrap(), + Date::from_calendar_date(2002, Month::March, 18) + .unwrap() + .into(), ) .gender("F") .address(DeliveryAddress { @@ -347,7 +350,7 @@ mod tests { .categories(vec!["Medical".to_owned(), "Health".to_owned()]) .note("Saved my life!".to_owned()) .prod_id("Contact App v1".to_owned()) - .rev(rev) + .rev(rev.into()) .sound("https://example.com/janedoe.wav".parse().unwrap()) .uid( "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6" diff --git a/src/date_time.rs b/src/date_time.rs new file mode 100644 index 0000000..bba8be8 --- /dev/null +++ b/src/date_time.rs @@ -0,0 +1,93 @@ +use crate::Error; +use std::{fmt, str::FromStr}; +use time::{format_description::well_known::Rfc3339, OffsetDateTime}; + +#[cfg(feature = "serde")] +use serde_with::{serde_as, DeserializeFromStr, SerializeDisplay}; + +/// Date and time that serializes to and from RFC3339. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", cfg_eval::cfg_eval, serde_as)] +#[cfg_attr(feature = "serde", derive(DeserializeFromStr, SerializeDisplay))] +pub struct DateTime(OffsetDateTime); + +impl DateTime { + /// Create UTC date and time. + pub fn now_utc() -> Self { + Self(OffsetDateTime::now_utc()) + } +} + +impl From for DateTime { + fn from(value: OffsetDateTime) -> Self { + Self(value) + } +} + +impl From for OffsetDateTime { + fn from(value: DateTime) -> Self { + value.0 + } +} + +impl AsRef for DateTime { + fn as_ref(&self) -> &OffsetDateTime { + &self.0 + } +} + +impl fmt::Display for DateTime { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + self.0.format(&Rfc3339).map_err(|_| fmt::Error::default())? + ) + } +} + +impl FromStr for DateTime { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(Self(OffsetDateTime::parse(s, &Rfc3339)?)) + } +} + +/// Date that serializes to and from RFC3339. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", cfg_eval::cfg_eval, serde_as)] +#[cfg_attr(feature = "serde", derive(DeserializeFromStr, SerializeDisplay))] +pub struct Date(time::Date); + +impl From for Date { + fn from(value: time::Date) -> Self { + Self(value) + } +} + +impl From for time::Date { + fn from(value: Date) -> Self { + value.0 + } +} + +impl AsRef for Date { + fn as_ref(&self) -> &time::Date { + &self.0 + } +} + +impl fmt::Display for Date { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.to_string(),) + } +} + +impl FromStr for Date { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(Self(OffsetDateTime::parse(s, &Rfc3339)?.date())) + } +} diff --git a/src/helper.rs b/src/helper.rs index c5a3dbc..3abd0ad 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -5,7 +5,7 @@ use time::{ Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, }; -use crate::{property::DateAndOrTime, Error, Result}; +use crate::{property::DateAndOrTime, DateTime, Error, Result}; // UTC OFFSET @@ -196,14 +196,14 @@ fn do_parse_date(s: &str) -> Result { } } -pub(crate) fn format_date(value: &Date) -> Result { +pub(crate) fn format_date(value: &crate::Date) -> Result { let date = format_description::parse("[year][month][day]")?; - Ok(value.format(&date)?) + Ok(value.as_ref().format(&date)?) } pub(crate) fn format_date_list( f: &mut fmt::Formatter<'_>, - val: &[Date], + val: &[crate::Date], ) -> fmt::Result { for (index, item) in val.iter().enumerate() { write!(f, "{}", &format_date(item).map_err(|_| fmt::Error)?)?; @@ -217,7 +217,7 @@ pub(crate) fn format_date_list( // DATETIME /// Parse a list of date times separated by a comma. -pub fn parse_date_time_list(value: &str) -> Result> { +pub fn parse_date_time_list(value: &str) -> Result> { let mut values = Vec::new(); for value in value.split(',') { values.push(parse_date_time(value)?); @@ -226,7 +226,7 @@ pub fn parse_date_time_list(value: &str) -> Result> { } /// Parse a date time. -pub fn parse_date_time(value: &str) -> Result { +pub fn parse_date_time(value: &str) -> Result { let mut it = value.splitn(2, 'T'); let date = it .next() @@ -242,10 +242,11 @@ pub fn parse_date_time(value: &str) -> Result { .replace_date(date) .replace_time(time) .replace_offset(offset); - Ok(utc) + Ok(utc.into()) } -pub(crate) fn format_date_time(d: &OffsetDateTime) -> Result { +pub(crate) fn format_date_time(d: &DateTime) -> Result { + let d = d.as_ref(); let offset = (*d).offset(); let format = if offset == UtcOffset::UTC { @@ -263,7 +264,7 @@ pub(crate) fn format_date_time(d: &OffsetDateTime) -> Result { pub(crate) fn format_date_time_list( f: &mut fmt::Formatter<'_>, - val: &[OffsetDateTime], + val: &[DateTime], ) -> fmt::Result { for (index, item) in val.iter().enumerate() { write!(f, "{}", &format_date_time(item).map_err(|_| fmt::Error)?)?; @@ -277,7 +278,7 @@ pub(crate) fn format_date_time_list( // TIMESTAMP /// Parse a timestamp. -pub fn parse_timestamp(value: &str) -> Result { +pub fn parse_timestamp(value: &str) -> Result { let offset_format = format_description::parse( "[year][month][day]T[hour][minute][second][offset_hour sign:mandatory][offset_minute]", )?; @@ -292,24 +293,24 @@ pub fn parse_timestamp(value: &str) -> Result { )?; if let Ok(result) = OffsetDateTime::parse(value, &offset_format) { - Ok(result) + Ok(result.into()) } else if let Ok(result) = - OffsetDateTime::parse(value, &offset_format_hours) + OffsetDateTime::parse(value, &offset_format_hours).into() { - Ok(result) + Ok(result.into()) } else if let Ok(result) = PrimitiveDateTime::parse(value, &utc_format) { let result = OffsetDateTime::now_utc().replace_date_time(result); - Ok(result) + Ok(result.into()) } else { let result = PrimitiveDateTime::parse(value, &implicit_utc_format)?; let result = OffsetDateTime::now_utc().replace_date_time(result); - Ok(result) + Ok(result.into()) } } pub(crate) fn format_timestamp_list( f: &mut fmt::Formatter<'_>, - val: &[OffsetDateTime], + val: &[DateTime], ) -> fmt::Result { for (index, item) in val.iter().enumerate() { write!(f, "{}", &format_date_time(item).map_err(|_| fmt::Error)?)?; @@ -321,7 +322,7 @@ pub(crate) fn format_timestamp_list( } /// Parse a list of date and or time types possibly separated by a comma. -pub fn parse_timestamp_list(value: &str) -> Result> { +pub fn parse_timestamp_list(value: &str) -> Result> { let mut values = Vec::new(); for value in value.split(',') { values.push(parse_timestamp(value)?); diff --git a/src/lib.rs b/src/lib.rs index b3fda61..5518dd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,6 +95,7 @@ //! mod builder; +mod date_time; mod error; pub mod helper; mod iter; @@ -112,6 +113,7 @@ pub use error::Error; pub use iter::VcardIterator; pub use vcard::Vcard; +pub use date_time::{Date, DateTime}; pub use time; pub use uri::Uri; diff --git a/src/parser.rs b/src/parser.rs index e6b26fb..3987b33 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -938,9 +938,12 @@ impl<'s> VcardParser<'s> { ValueType::Boolean => { AnyProperty::Boolean(parse_boolean(value.as_ref())?) } - ValueType::Date => { - AnyProperty::Date(parse_date_list(value.as_ref())?) - } + ValueType::Date => AnyProperty::Date( + parse_date_list(value.as_ref())? + .into_iter() + .map(crate::Date::from) + .collect(), + ), ValueType::DateTime => AnyProperty::DateTime( parse_date_time_list(value.as_ref())?, ), diff --git a/src/property.rs b/src/property.rs index 2c3725a..aa2415f 100644 --- a/src/property.rs +++ b/src/property.rs @@ -1,11 +1,10 @@ //! Types for properties. -use crate::Uri; use std::{ fmt::{self, Display}, str::FromStr, }; -use time::{Date, OffsetDateTime, Time, UtcOffset}; +use time::{Time, UtcOffset}; #[cfg(feature = "language-tags")] use language_tags::LanguageTag; @@ -29,7 +28,7 @@ use crate::{ parse_date_time, parse_time, parse_utc_offset, }, parameter::Parameters, - Error, Result, + Date, DateTime, Error, Result, Uri, }; const INDIVIDUAL: &str = "individual"; @@ -349,7 +348,7 @@ pub enum AnyProperty { Date(Vec), /// Date and time value. #[cfg_attr(feature = "zeroize", zeroize(skip))] - DateTime(Vec), + DateTime(Vec), /// Time value. #[cfg_attr(feature = "zeroize", zeroize(skip))] Time(Vec<(Time, UtcOffset)>), @@ -358,7 +357,7 @@ pub enum AnyProperty { DateAndOrTime(Vec), /// Timetamp value. #[cfg_attr(feature = "zeroize", zeroize(skip))] - Timestamp(Vec), + Timestamp(Vec), /// URI property. #[cfg_attr(feature = "zeroize", zeroize(skip))] Uri(#[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))] Uri), @@ -459,7 +458,7 @@ pub struct DateTimeProperty { pub group: Option, /// The value for the property. #[cfg_attr(feature = "zeroize", zeroize(skip))] - pub value: OffsetDateTime, + pub value: DateTime, /// The property parameters. #[cfg_attr( feature = "serde", @@ -468,8 +467,8 @@ pub struct DateTimeProperty { pub parameters: Option, } -impl From for DateTimeProperty { - fn from(value: OffsetDateTime) -> Self { +impl From for DateTimeProperty { + fn from(value: DateTime) -> Self { Self { value, group: None, @@ -496,7 +495,7 @@ pub enum DateAndOrTime { /// Date value. Date(Date), /// Date and time value. - DateTime(OffsetDateTime), + DateTime(DateTime), /// Time value. Time((Time, UtcOffset)), } @@ -507,8 +506,8 @@ impl From for DateAndOrTime { } } -impl From for DateAndOrTime { - fn from(value: OffsetDateTime) -> Self { +impl From for DateAndOrTime { + fn from(value: DateTime) -> Self { Self::DateTime(value) } } @@ -548,7 +547,7 @@ impl FromStr for DateAndOrTime { match parse_date_time(s) { Ok(value) => Ok(Self::DateTime(value)), Err(_) => match parse_date(s) { - Ok(value) => Ok(Self::Date(value)), + Ok(value) => Ok(Self::Date(value.into())), Err(_) => match parse_time(s) { Ok(val) => Ok(Self::Time(val)), Err(e) => Err(e), @@ -588,8 +587,8 @@ impl From for DateAndOrTimeProperty { } } -impl From for DateAndOrTimeProperty { - fn from(value: OffsetDateTime) -> Self { +impl From for DateAndOrTimeProperty { + fn from(value: DateTime) -> Self { Self { value: vec![value.into()], group: None, @@ -689,8 +688,8 @@ impl From for DateTimeOrTextProperty { } } -impl From for DateTimeOrTextProperty { - fn from(value: OffsetDateTime) -> Self { +impl From for DateTimeOrTextProperty { + fn from(value: DateTime) -> Self { Self::DateTime(value.into()) } } diff --git a/tests/explanatory.rs b/tests/explanatory.rs index 2ebcd75..26e6323 100644 --- a/tests/explanatory.rs +++ b/tests/explanatory.rs @@ -60,7 +60,7 @@ END:VCARD"#; let card = vcards.remove(0); let prop = card.rev.as_ref().unwrap(); - assert_eq!("1995-10-31 22:27:10.0 +00:00:00", &prop.value.to_string()); + assert_eq!("1995-10-31T22:27:10Z", &prop.value.to_string()); assert_round_trip(&card)?; Ok(()) } diff --git a/tests/extensions.rs b/tests/extensions.rs index 2f0132c..e11226a 100644 --- a/tests/extensions.rs +++ b/tests/extensions.rs @@ -108,7 +108,10 @@ END:VCARD"#; prop.parameters.as_ref().unwrap().value.as_ref().unwrap() ); - let expected = parse_date_list("20221107")?; + let expected = parse_date_list("20221107")? + .into_iter() + .map(vcard4::Date::from) + .collect(); assert_eq!(&AnyProperty::Date(expected), &prop.value); assert_round_trip(&card)?; diff --git a/tests/types.rs b/tests/types.rs index 899d075..da4e6c7 100644 --- a/tests/types.rs +++ b/tests/types.rs @@ -59,22 +59,22 @@ fn types_date_only() -> Result<()> { #[test] fn types_date_time() -> Result<()> { let date_time = parse_date_time("20090808T1430-0500")?; - assert_eq!("2009-08-08 14:30:00.0 -05:00:00", &date_time.to_string()); + assert_eq!("2009-08-08T14:30:00-05:00", &date_time.to_string()); let date_time = parse_date_time("19961022T140000Z")?; - assert_eq!("1996-10-22 14:00:00.0 +00:00:00", &date_time.to_string()); + assert_eq!("1996-10-22T14:00:00Z", &date_time.to_string()); let date_time = parse_date_time("19961022T140000+0800")?; - assert_eq!("1996-10-22 14:00:00.0 +08:00:00", &date_time.to_string()); + assert_eq!("1996-10-22T14:00:00+08:00", &date_time.to_string()); let date_time = parse_date_time("19961022T140000")?; - assert_eq!("1996-10-22 14:00:00.0 +00:00:00", &date_time.to_string()); + assert_eq!("1996-10-22T14:00:00Z", &date_time.to_string()); let date_time = parse_date_time("--1022T1400")?; - assert_eq!("0000-10-22 14:00:00.0 +00:00:00", &date_time.to_string()); + assert_eq!("0000-10-22T14:00:00Z", &date_time.to_string()); let date_time = parse_date_time("---22T14")?; - assert_eq!("0000-01-22 14:00:00.0 +00:00:00", &date_time.to_string()); + assert_eq!("0000-01-22T14:00:00Z", &date_time.to_string()); Ok(()) } @@ -84,7 +84,7 @@ fn types_date_and_or_time() -> Result<()> { let value: DateAndOrTime = "19961022T140000".parse()?; if let DateAndOrTime::DateTime(value) = value { //let value = value.get(0).unwrap(); - assert_eq!("1996-10-22 14:00:00.0 +00:00:00", &value.to_string()); + assert_eq!("1996-10-22T14:00:00Z", &value.to_string()); } else { panic!("expecting DateTime variant"); } @@ -92,7 +92,7 @@ fn types_date_and_or_time() -> Result<()> { let value: DateAndOrTime = "--1022T1400".parse()?; if let DateAndOrTime::DateTime(value) = value { //let value = value.get(0).unwrap(); - assert_eq!("0000-10-22 14:00:00.0 +00:00:00", &value.to_string()); + assert_eq!("0000-10-22T14:00:00Z", &value.to_string()); } else { panic!("expecting DateTime variant"); } @@ -100,7 +100,7 @@ fn types_date_and_or_time() -> Result<()> { let value: DateAndOrTime = "---22T14".parse()?; if let DateAndOrTime::DateTime(value) = value { //let value = value.get(0).unwrap(); - assert_eq!("0000-01-22 14:00:00.0 +00:00:00", &value.to_string()); + assert_eq!("0000-01-22T14:00:00Z", &value.to_string()); } else { panic!("expecting DateTime variant"); } @@ -208,16 +208,16 @@ fn types_date_and_or_time() -> Result<()> { #[test] fn types_timestamp() -> Result<()> { let timestamp = parse_timestamp("19961022T140000")?; - assert_eq!("1996-10-22 14:00:00.0 +00:00:00", ×tamp.to_string()); + assert_eq!("1996-10-22T14:00:00Z", ×tamp.to_string()); let timestamp = parse_timestamp("19961022T140000Z")?; - assert_eq!("1996-10-22 14:00:00.0 +00:00:00", ×tamp.to_string()); + assert_eq!("1996-10-22T14:00:00Z", ×tamp.to_string()); let timestamp = parse_timestamp("19961022T140000-05")?; - assert_eq!("1996-10-22 14:00:00.0 -05:00:00", ×tamp.to_string()); + assert_eq!("1996-10-22T14:00:00-05:00", ×tamp.to_string()); let timestamp = parse_timestamp("19961022T140000-0500")?; - assert_eq!("1996-10-22 14:00:00.0 -05:00:00", ×tamp.to_string()); + assert_eq!("1996-10-22T14:00:00-05:00", ×tamp.to_string()); Ok(()) } @@ -341,10 +341,10 @@ proptest! { "{:04}{:02}{:02}T{:02}{:02}{:02}", y, m, d, h, mi, s); let date_time = parse_date_time(&value).unwrap(); - let m2: u8 = date_time.month().try_into().unwrap(); - let (y2, d2) = (date_time.year(), date_time.day()); + let m2: u8 = date_time.as_ref().month().try_into().unwrap(); + let (y2, d2) = (date_time.as_ref().year(), date_time.as_ref().day()); let (h2, mi2, s2) = ( - date_time.hour(), date_time.minute(), date_time.second()); + date_time.as_ref().hour(), date_time.as_ref().minute(), date_time.as_ref().second()); prop_assert_eq!((y, m, d, h, mi, s), (y2, m2, d2, h2, mi2, s2)); // TODO: test negative offset