Skip to content

Commit

Permalink
Serde format datetime and date as rfc 3339 (#18)
Browse files Browse the repository at this point in the history
* Use RFC 3339 date formats for serde.

* Bump minor version.
  • Loading branch information
tmpfs authored Dec 12, 2024
1 parent d8e43e5 commit 694b559
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 62 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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"]
Expand Down
15 changes: 9 additions & 6 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"
Expand Down
93 changes: 93 additions & 0 deletions src/date_time.rs
Original file line number Diff line number Diff line change
@@ -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<OffsetDateTime> for DateTime {
fn from(value: OffsetDateTime) -> Self {
Self(value)
}
}

impl From<DateTime> for OffsetDateTime {
fn from(value: DateTime) -> Self {
value.0
}
}

impl AsRef<OffsetDateTime> 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<Self, Self::Err> {
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<time::Date> for Date {
fn from(value: time::Date) -> Self {
Self(value)
}
}

impl From<Date> for time::Date {
fn from(value: Date) -> Self {
value.0
}
}

impl AsRef<time::Date> 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<Self, Self::Err> {
Ok(Self(OffsetDateTime::parse(s, &Rfc3339)?.date()))
}
}
35 changes: 18 additions & 17 deletions src/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -196,14 +196,14 @@ fn do_parse_date(s: &str) -> Result<Date> {
}
}

pub(crate) fn format_date(value: &Date) -> Result<String> {
pub(crate) fn format_date(value: &crate::Date) -> Result<String> {
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)?)?;
Expand All @@ -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<Vec<OffsetDateTime>> {
pub fn parse_date_time_list(value: &str) -> Result<Vec<DateTime>> {
let mut values = Vec::new();
for value in value.split(',') {
values.push(parse_date_time(value)?);
Expand All @@ -226,7 +226,7 @@ pub fn parse_date_time_list(value: &str) -> Result<Vec<OffsetDateTime>> {
}

/// Parse a date time.
pub fn parse_date_time(value: &str) -> Result<OffsetDateTime> {
pub fn parse_date_time(value: &str) -> Result<DateTime> {
let mut it = value.splitn(2, 'T');
let date = it
.next()
Expand All @@ -242,10 +242,11 @@ pub fn parse_date_time(value: &str) -> Result<OffsetDateTime> {
.replace_date(date)
.replace_time(time)
.replace_offset(offset);
Ok(utc)
Ok(utc.into())
}

pub(crate) fn format_date_time(d: &OffsetDateTime) -> Result<String> {
pub(crate) fn format_date_time(d: &DateTime) -> Result<String> {
let d = d.as_ref();
let offset = (*d).offset();

let format = if offset == UtcOffset::UTC {
Expand All @@ -263,7 +264,7 @@ pub(crate) fn format_date_time(d: &OffsetDateTime) -> Result<String> {

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)?)?;
Expand All @@ -277,7 +278,7 @@ pub(crate) fn format_date_time_list(
// TIMESTAMP

/// Parse a timestamp.
pub fn parse_timestamp(value: &str) -> Result<OffsetDateTime> {
pub fn parse_timestamp(value: &str) -> Result<DateTime> {
let offset_format = format_description::parse(
"[year][month][day]T[hour][minute][second][offset_hour sign:mandatory][offset_minute]",
)?;
Expand All @@ -292,24 +293,24 @@ pub fn parse_timestamp(value: &str) -> Result<OffsetDateTime> {
)?;

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)?)?;
Expand All @@ -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<Vec<OffsetDateTime>> {
pub fn parse_timestamp_list(value: &str) -> Result<Vec<DateTime>> {
let mut values = Vec::new();
for value in value.split(',') {
values.push(parse_timestamp(value)?);
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
//!
mod builder;
mod date_time;
mod error;
pub mod helper;
mod iter;
Expand All @@ -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;

Expand Down
9 changes: 6 additions & 3 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())?,
),
Expand Down
Loading

0 comments on commit 694b559

Please sign in to comment.