diff --git a/Cargo.toml b/Cargo.toml index cdfac36..df76334 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,23 +10,26 @@ license = "MIT OR Apache-2.0" [dependencies] thiserror = "1" logos = { version = "0.14", features = ["export_derive"] } -uriparse = "0.6.4" +uriparse = "0.6" time = { version = "0.3.37", features = ["parsing", "formatting"] } unicode-segmentation="1" aho-corasick = "0.7" +base64 = "0.21.0" serde = { version = "1", features = ["derive"], optional = true } +serde_with = { version = "3", optional = true } +cfg_eval = { version = "0.1", optional = true } zeroize = { version = "1.5", features = ["derive"], optional = true } mime = { version = "0.3", optional = true } language-tags = { version = "0.3", optional = true } -base64 = "0.21.0" [features] default = ["zeroize"] serde = [ "dep:serde", + "dep:serde_with", + "dep:cfg_eval", "time/serde-human-readable", "language-tags?/serde", - "uriparse/serde", ] zeroize = ["dep:zeroize"] mime = ["dep:mime"] diff --git a/examples/serde.rs b/examples/serde.rs new file mode 100644 index 0000000..7b8a2b1 --- /dev/null +++ b/examples/serde.rs @@ -0,0 +1,20 @@ +#[cfg(feature = "serde")] +pub fn main() -> anyhow::Result<()> { + use vcard4::parse; + + let uri = "file:///images/jdoe.jpeg"; + + let parsed = uri.parse::()?; + + const VCF: &str = include_str!("simon-perrault.vcf"); + + let cards = parse(VCF)?; + let card = cards.first().unwrap(); + print!("{}", serde_json::to_string_pretty(&card).unwrap()); + Ok(()) +} + +#[cfg(not(feature = "serde"))] +pub fn main() { + panic!("serde feature is required"); +} diff --git a/src/builder.rs b/src/builder.rs index e4d8aff..310e021 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -2,10 +2,9 @@ //! use crate::{ property::{DeliveryAddress, Gender, Kind, TextListProperty}, - Vcard, + Uri, Vcard, }; use time::{Date, OffsetDateTime}; -use uriparse::uri::URI as Uri; #[cfg(feature = "language-tags")] use language_tags::LanguageTag; @@ -43,7 +42,7 @@ impl VcardBuilder { } /// Add a source for the vCard. - pub fn source(mut self, value: Uri<'static>) -> Self { + pub fn source(mut self, value: Uri) -> Self { self.card.source.push(value.into()); self } @@ -79,7 +78,7 @@ impl VcardBuilder { } /// Add a photo to the vCard. - pub fn photo(mut self, value: Uri<'static>) -> Self { + pub fn photo(mut self, value: Uri) -> Self { self.card.photo.push(value.into()); self } @@ -131,7 +130,7 @@ impl VcardBuilder { } /// Add an instant messaging URI to the vCard. - pub fn impp(mut self, value: Uri<'static>) -> Self { + pub fn impp(mut self, value: Uri) -> Self { self.card.impp.push(value.into()); self } @@ -159,7 +158,7 @@ impl VcardBuilder { } /// Add a geographic location to the vCard. - pub fn geo(mut self, value: Uri<'static>) -> Self { + pub fn geo(mut self, value: Uri) -> Self { self.card.geo.push(value.into()); self } @@ -179,7 +178,7 @@ impl VcardBuilder { } /// Add logo to the vCard. - pub fn logo(mut self, value: Uri<'static>) -> Self { + pub fn logo(mut self, value: Uri) -> Self { self.card.logo.push(value.into()); self } @@ -193,13 +192,13 @@ impl VcardBuilder { /// Add a member to the vCard. /// /// The vCard should be of the group kind to be valid. - pub fn member(mut self, value: Uri<'static>) -> Self { + pub fn member(mut self, value: Uri) -> Self { self.card.member.push(value.into()); self } /// Add a related entry to the vCard. - pub fn related(mut self, value: Uri<'static>) -> Self { + pub fn related(mut self, value: Uri) -> Self { self.card.related.push(value.into()); self } @@ -233,19 +232,19 @@ impl VcardBuilder { } /// Add a sound to the vCard. - pub fn sound(mut self, value: Uri<'static>) -> Self { + pub fn sound(mut self, value: Uri) -> Self { self.card.sound.push(value.into()); self } /// Set the UID for the vCard. - pub fn uid(mut self, value: Uri<'static>) -> Self { + pub fn uid(mut self, value: Uri) -> Self { self.card.uid = Some(value.into()); self } /// Add a URL to the vCard. - pub fn url(mut self, value: Uri<'static>) -> Self { + pub fn url(mut self, value: Uri) -> Self { self.card.url.push(value.into()); self } @@ -253,7 +252,7 @@ impl VcardBuilder { // Security /// Add a key to the vCard. - pub fn key(mut self, value: Uri<'static>) -> Self { + pub fn key(mut self, value: Uri) -> Self { self.card.key.push(value.into()); self } @@ -261,19 +260,19 @@ impl VcardBuilder { // Calendar /// Add a fburl to the vCard. - pub fn fburl(mut self, value: Uri<'static>) -> Self { + pub fn fburl(mut self, value: Uri) -> Self { self.card.fburl.push(value.into()); self } /// Add a calendar address URI to the vCard. - pub fn cal_adr_uri(mut self, value: Uri<'static>) -> Self { + pub fn cal_adr_uri(mut self, value: Uri) -> Self { self.card.cal_adr_uri.push(value.into()); self } /// Add a calendar URI to the vCard. - pub fn cal_uri(mut self, value: Uri<'static>) -> Self { + pub fn cal_uri(mut self, value: Uri) -> Self { self.card.cal_uri.push(value.into()); self } @@ -302,7 +301,7 @@ mod tests { // General .source( "http://directory.example.com/addressbooks/jdoe.vcf" - .try_into() + .parse() .unwrap(), ) // Identification @@ -314,7 +313,7 @@ mod tests { "MS".to_owned(), ]) .nickname("JC".to_owned()) - .photo("file:///images/jdoe.jpeg".try_into().unwrap()) + .photo("file:///images/jdoe.jpeg".parse().unwrap()) .birthday( Date::from_calendar_date(1986, Month::February, 7).unwrap(), ) @@ -334,38 +333,36 @@ mod tests { // Communication .telephone("+10987654321".to_owned()) .email("janedoe@example.com".to_owned()) - .impp("im://example.com/messenger".try_into().unwrap()) + .impp("im://example.com/messenger".parse().unwrap()) // Geographical .timezone("Raleigh/North America".to_owned()) - .geo("geo:37.386013,-122.082932".try_into().unwrap()) + .geo("geo:37.386013,-122.082932".parse().unwrap()) // Organizational .org(vec!["Mock Hospital".to_owned(), "Surgery".to_owned()]) .title("Dr".to_owned()) .role("Master Surgeon".to_owned()) - .logo("https://example.com/mock.jpeg".try_into().unwrap()) - .related("https://example.com/johndoe".try_into().unwrap()) + .logo("https://example.com/mock.jpeg".parse().unwrap()) + .related("https://example.com/johndoe".parse().unwrap()) // Explanatory .categories(vec!["Medical".to_owned(), "Health".to_owned()]) .note("Saved my life!".to_owned()) .prod_id("Contact App v1".to_owned()) .rev(rev) - .sound("https://example.com/janedoe.wav".try_into().unwrap()) + .sound("https://example.com/janedoe.wav".parse().unwrap()) .uid( "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6" - .try_into() + .parse() .unwrap(), ) - .url("https://example.com/janedoe".try_into().unwrap()) + .url("https://example.com/janedoe".parse().unwrap()) // Security - .key("urn:eth:0x00".try_into().unwrap()) + .key("urn:eth:0x00".parse().unwrap()) // Calendar - .fburl("https://www.example.com/busy/janedoe".try_into().unwrap()) + .fburl("https://www.example.com/busy/janedoe".parse().unwrap()) .cal_adr_uri( - "https://www.example.com/calendar/janedoe" - .try_into() - .unwrap(), + "https://www.example.com/calendar/janedoe".parse().unwrap(), ) - .cal_uri("https://calendar.example.com".try_into().unwrap()) + .cal_uri("https://calendar.example.com".parse().unwrap()) .finish(); let expected = "BEGIN:VCARD\r\nVERSION:4.0\r\nSOURCE:http://directory.example.com/addressbooks/jdoe.vcf\r\nFN:Jane Doe\r\nN:Doe;Jane;Claire;Dr.;MS\r\nNICKNAME:JC\r\nPHOTO:file:///images/jdoe.jpeg\r\nBDAY:19860207\r\nANNIVERSARY:20020318\r\nGENDER:F\r\nURL:https://example.com/janedoe\r\nADR:;;123 Main Street;Mock City;Mock State;123;Mock Country\r\nTITLE:Dr\r\nROLE:Master Surgeon\r\nLOGO:https://example.com/mock.jpeg\r\nORG:Mock Hospital;Surgery\r\nRELATED:https://example.com/johndoe\r\nTEL:+10987654321\r\nEMAIL:janedoe@example.com\r\nIMPP:im://example.com/messenger\r\nTZ:Raleigh/North America\r\nGEO:geo:37.386013,-122.082932\r\nCATEGORIES:Medical,Health\r\nNOTE:Saved my life!\r\nPRODID:Contact App v1\r\nREV:20000103T000000Z\r\nSOUND:https://example.com/janedoe.wav\r\nUID:urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6\r\nKEY:urn:eth:0x00\r\nFBURL:https://www.example.com/busy/janedoe\r\nCALADRURI:https://www.example.com/calendar/janedoe\r\nCALURI:https://calendar.example.com/\r\nEND:VCARD\r\n"; @@ -378,8 +375,8 @@ mod tests { fn builder_member_group() { let card = VcardBuilder::new("Mock Company".to_owned()) .kind(Kind::Group) - .member("https://example.com/foo".try_into().unwrap()) - .member("https://example.com/bar".try_into().unwrap()) + .member("https://example.com/foo".parse().unwrap()) + .member("https://example.com/bar".parse().unwrap()) .finish(); assert_eq!(2, card.member.len()); assert!(card.validate().is_ok()); @@ -388,7 +385,7 @@ mod tests { #[test] fn builder_member_invalid() { let card = VcardBuilder::new("Mock Company".to_owned()) - .member("https://example.com/bar".try_into().unwrap()) + .member("https://example.com/bar".parse().unwrap()) .finish(); assert_eq!(1, card.member.len()); assert!(card.validate().is_err()); diff --git a/src/lib.rs b/src/lib.rs index 2a3a887..0192015 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,6 +104,7 @@ mod parser; pub mod property; #[cfg(feature = "serde")] mod serde; +mod uri; mod vcard; pub use builder::VcardBuilder; @@ -112,7 +113,7 @@ pub use iter::VcardIterator; pub use vcard::Vcard; pub use time; -pub use uriparse; +pub use uri::Uri; /// Result type for the vCard library. pub type Result = std::result::Result; diff --git a/src/parameter.rs b/src/parameter.rs index 94ebfff..1ad8b3e 100644 --- a/src/parameter.rs +++ b/src/parameter.rs @@ -5,7 +5,6 @@ use std::{ str::FromStr, }; use time::UtcOffset; -use uriparse::uri::URI as Uri; #[cfg(feature = "language-tags")] use language_tags::LanguageTag; @@ -13,6 +12,9 @@ use language_tags::LanguageTag; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "serde")] +use serde_with::{serde_as, DisplayFromStr}; + #[cfg(feature = "zeroize")] use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -22,7 +24,7 @@ use mime::Mime; use crate::{ helper::format_utc_offset, name::{HOME, WORK}, - Error, Result, + Error, Result, Uri, }; /// Names of properties that are allowed to specify a TYPE parameter. @@ -56,6 +58,10 @@ pub(crate) const TYPE_PROPERTIES: [&str; 23] = [ #[derive(Debug, Eq, PartialEq, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] +#[cfg_attr( + feature = "serde", + serde(rename_all = "lowercase", tag = "kind", content = "value") +)] pub enum TypeParameter { /// Related to a home environment. Home, @@ -419,6 +425,7 @@ impl FromStr for ValueType { /// create infinite type recursion in `Parameters` which would /// require us to wrap it in a `Box`. #[derive(Debug, Eq, PartialEq, Clone)] +#[cfg_attr(feature = "serde", cfg_eval::cfg_eval, serde_as)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] #[allow(clippy::large_enum_variant)] @@ -427,7 +434,7 @@ pub enum TimeZoneParameter { Text(String), /// Uri value. #[cfg_attr(feature = "zeroize", zeroize(skip))] - Uri(Uri<'static>), + Uri(#[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))] Uri), /// UTC offset value. #[cfg_attr(feature = "zeroize", zeroize(skip))] UtcOffset(UtcOffset), @@ -435,8 +442,10 @@ pub enum TimeZoneParameter { /// Parameters for a vCard property. #[derive(Debug, Default, Eq, PartialEq, Clone)] +#[cfg_attr(feature = "serde", cfg_eval::cfg_eval, serde_as)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] +#[cfg_attr(feature = "serde", serde(default))] pub struct Parameters { /// The LANGUAGE tag. #[cfg(feature = "language-tags")] @@ -524,7 +533,8 @@ pub struct Parameters { feature = "serde", serde(default, skip_serializing_if = "Option::is_none") )] - pub geo: Option>, + #[cfg_attr(feature = "serde", serde_as(as = "Option"))] + pub geo: Option, /// The TZ parameter. #[cfg_attr( feature = "serde", diff --git a/src/parser.rs b/src/parser.rs index fa854b9..e6b26fb 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,7 +2,6 @@ use logos::{Lexer, Logos}; use std::{borrow::Cow, ops::Range}; -use uriparse::uri::URI as Uri; #[cfg(feature = "language-tags")] use language_tags::LanguageTag; @@ -12,7 +11,7 @@ use mime::Mime; use crate::{ error::LexError, escape_control, helper::*, name::*, parameter::*, - property::*, unescape_value, Error, Result, Vcard, + property::*, unescape_value, Error, Result, Uri, Vcard, }; type LexResult = std::result::Result; @@ -346,13 +345,12 @@ impl<'s> VcardParser<'s> { property_upper_name, )); } - let geo = Uri::try_from(&value[..])?.into_owned(); + let geo = value.parse()?; params.geo = Some(geo); } TZ => { if quoted { - let value = - Uri::try_from(&value[..])?.into_owned(); + let value = value.parse()?; params.timezone = Some(TimeZoneParameter::Uri(value)); } else { @@ -519,7 +517,7 @@ impl<'s> VcardParser<'s> { // General properties // https://www.rfc-editor.org/rfc/rfc6350#section-6.1 SOURCE => { - let value = Uri::try_from(value.as_ref())?.into_owned(); + let value = value.as_ref().parse()?; card.source.push(UriProperty { value, parameters, @@ -576,9 +574,8 @@ impl<'s> VcardParser<'s> { group, }); } - PHOTO => match Uri::try_from(value.as_ref()) { - Ok(uri) => { - let value = uri.into_owned(); + PHOTO => match value.as_ref().parse::() { + Ok(value) => { card.photo.push(TextOrUriProperty::Uri(UriProperty { value, parameters, @@ -660,7 +657,7 @@ impl<'s> VcardParser<'s> { }); } IMPP => { - let value = Uri::try_from(value.as_ref())?.into_owned(); + let value = value.as_ref().parse()?; card.impp.push(UriProperty { value, parameters, @@ -696,8 +693,7 @@ impl<'s> VcardParser<'s> { .push(TimeZoneProperty::UtcOffset(value)); } ValueType::Uri => { - let value = - Uri::try_from(value.as_ref())?.into_owned(); + let value = value.parse()?; card.timezone.push(TimeZoneProperty::Uri( UriProperty { value, @@ -724,7 +720,7 @@ impl<'s> VcardParser<'s> { } } GEO => { - let value = Uri::try_from(value.as_ref())?.into_owned(); + let value = value.parse()?; card.geo.push(UriProperty { value, parameters, @@ -749,7 +745,7 @@ impl<'s> VcardParser<'s> { }); } LOGO => { - let value = Uri::try_from(value.as_ref())?.into_owned(); + let value = value.parse()?; card.logo.push(UriProperty { value, parameters, @@ -770,7 +766,7 @@ impl<'s> VcardParser<'s> { }); } MEMBER => { - let value = Uri::try_from(value.as_ref())?.into_owned(); + let value = value.parse()?; card.member.push(UriProperty { value, parameters, @@ -830,7 +826,7 @@ impl<'s> VcardParser<'s> { }); } SOUND => { - let value = Uri::try_from(value.as_ref())?.into_owned(); + let value = value.parse()?; card.sound.push(UriProperty { value, parameters, @@ -863,7 +859,7 @@ impl<'s> VcardParser<'s> { }); } URL => { - let value = Uri::try_from(value.as_ref())?.into_owned(); + let value = value.as_ref().parse()?; card.url.push(UriProperty { value, parameters, @@ -888,7 +884,7 @@ impl<'s> VcardParser<'s> { // Calendar // https://www.rfc-editor.org/rfc/rfc6350#section-6.9 FBURL => { - let value = Uri::try_from(value.as_ref())?.into_owned(); + let value = value.as_ref().parse()?; card.fburl.push(UriProperty { value, parameters, @@ -896,7 +892,7 @@ impl<'s> VcardParser<'s> { }); } CALADRURI => { - let value = Uri::try_from(value.as_ref())?.into_owned(); + let value = value.as_ref().parse()?; card.cal_adr_uri.push(UriProperty { value, parameters, @@ -904,7 +900,7 @@ impl<'s> VcardParser<'s> { }); } CALURI => { - let value = Uri::try_from(value.as_ref())?.into_owned(); + let value = value.parse()?; card.cal_uri.push(UriProperty { value, parameters, @@ -965,7 +961,7 @@ impl<'s> VcardParser<'s> { AnyProperty::UtcOffset(value) } ValueType::Uri => { - let value = Uri::try_from(value.as_ref())?.into_owned(); + let value = value.parse()?; AnyProperty::Uri(value) } } @@ -1076,7 +1072,7 @@ impl<'s> VcardParser<'s> { group, })) } else if let ValueType::Uri = value_type { - let value = Uri::try_from(value.as_ref())?.into_owned(); + let value = value.as_ref().parse()?; Ok(TextOrUriProperty::Uri(UriProperty { value, parameters, @@ -1086,9 +1082,9 @@ impl<'s> VcardParser<'s> { Err(Error::UnknownValueType(value_type.to_string())) } } else { - match Uri::try_from(value.as_ref()) { + match value.as_ref().parse::() { Ok(value) => Ok(TextOrUriProperty::Uri(UriProperty { - value: value.into_owned(), + value, parameters, group, })), diff --git a/src/property.rs b/src/property.rs index 244cc65..2c3725a 100644 --- a/src/property.rs +++ b/src/property.rs @@ -1,11 +1,11 @@ //! Types for properties. +use crate::Uri; use std::{ fmt::{self, Display}, str::FromStr, }; use time::{Date, OffsetDateTime, Time, UtcOffset}; -use uriparse::uri::URI as Uri; #[cfg(feature = "language-tags")] use language_tags::LanguageTag; @@ -13,6 +13,9 @@ use language_tags::LanguageTag; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "serde")] +use serde_with::{serde_as, DisplayFromStr}; + #[cfg(feature = "zeroize")] use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -44,23 +47,52 @@ pub trait Property: Display { } /// Delivery address for the ADR property. -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Default, Debug, Eq, PartialEq, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct DeliveryAddress { /// The post office box. + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none") + )] pub po_box: Option, /// The extended address (e.g: apartment or suite number). + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none") + )] pub extended_address: Option, /// The street address. + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none") + )] pub street_address: Option, /// The locality (e.g: city). + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none") + )] pub locality: Option, /// The region (e.g: state or province). + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none") + )] pub region: Option, /// The postal code. + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none") + )] pub postal_code: Option, /// The country name. + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none") + )] pub country_name: Option, } @@ -209,6 +241,7 @@ impl From for AddressProperty { /// Value for the CLIENTPIDMAP property. #[derive(Debug, Eq, PartialEq, Clone)] +#[cfg_attr(feature = "serde", cfg_eval::cfg_eval, serde_as)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] pub struct ClientPidMap { @@ -216,7 +249,8 @@ pub struct ClientPidMap { pub source: u64, /// The URI for the map. #[cfg_attr(feature = "zeroize", zeroize(skip))] - pub uri: Uri<'static>, + #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))] + pub uri: Uri, } impl fmt::Display for ClientPidMap { @@ -244,7 +278,7 @@ impl FromStr for ClientPidMap { return Err(Error::InvalidClientPidMap(s.to_string())); } - let uri = Uri::try_from(uri)?.into_owned(); + let uri = uri.parse()?; Ok(ClientPidMap { source, uri }) } } @@ -295,8 +329,10 @@ pub struct ExtensionProperty { /// Value for any property type. #[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "serde", cfg_eval::cfg_eval, serde_as)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] +#[cfg_attr(feature = "serde", serde(tag = "kind", rename_all = "camelCase"))] #[allow(clippy::large_enum_variant)] pub enum AnyProperty { /// Text property. @@ -325,7 +361,7 @@ pub enum AnyProperty { Timestamp(Vec), /// URI property. #[cfg_attr(feature = "zeroize", zeroize(skip))] - Uri(Uri<'static>), + Uri(#[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))] Uri), /// UTC offset property. #[cfg_attr(feature = "zeroize", zeroize(skip))] UtcOffset(UtcOffset), @@ -455,6 +491,7 @@ impl fmt::Display for DateTimeProperty { /// Date and or time. #[derive(Debug, Eq, PartialEq, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(untagged))] pub enum DateAndOrTime { /// Date value. Date(Date), @@ -581,6 +618,7 @@ impl fmt::Display for DateAndOrTimeProperty { #[derive(Debug, Eq, PartialEq, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] +#[cfg_attr(feature = "serde", serde(untagged))] #[allow(clippy::large_enum_variant)] pub enum TextOrUriProperty { /// Text value. @@ -595,8 +633,8 @@ impl From for TextOrUriProperty { } } -impl From> for TextOrUriProperty { - fn from(value: Uri<'static>) -> Self { +impl From for TextOrUriProperty { + fn from(value: Uri) -> Self { Self::Uri(value.into()) } } @@ -630,6 +668,7 @@ impl fmt::Display for TextOrUriProperty { #[derive(Debug, Eq, PartialEq, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] +#[cfg_attr(feature = "serde", serde(untagged))] pub enum DateTimeOrTextProperty { /// Date time value. #[cfg_attr(feature = "zeroize", zeroize(skip))] @@ -756,6 +795,7 @@ impl FromStr for UtcOffsetProperty { #[derive(Debug, Eq, PartialEq, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] +#[cfg_attr(feature = "serde", serde(untagged))] #[allow(clippy::large_enum_variant)] pub enum TimeZoneProperty { /// Text value. @@ -772,8 +812,8 @@ impl From for TimeZoneProperty { } } -impl From> for TimeZoneProperty { - fn from(value: Uri<'static>) -> Self { +impl From for TimeZoneProperty { + fn from(value: Uri) -> Self { Self::Uri(value.into()) } } @@ -814,7 +854,7 @@ impl fmt::Display for TimeZoneProperty { /// Text property value. #[derive(Debug, Eq, PartialEq, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] pub struct TextProperty { /// Group for this property. @@ -851,7 +891,7 @@ impl From for TextProperty { /// Delimiter used for a text list. #[derive(Debug, Eq, PartialEq, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] #[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))] pub enum TextListDelimiter { @@ -863,7 +903,7 @@ pub enum TextListDelimiter { /// Text list property value. #[derive(Debug, Eq, PartialEq, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] pub struct TextListProperty { /// Group for this property. @@ -929,6 +969,7 @@ impl fmt::Display for TextListProperty { /// Uri property value. #[derive(Debug, Eq, PartialEq, Clone)] +#[cfg_attr(feature = "serde", cfg_eval::cfg_eval, serde_as)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] pub struct UriProperty { @@ -940,7 +981,8 @@ pub struct UriProperty { pub group: Option, /// Value for this property. #[cfg_attr(feature = "zeroize", zeroize(skip))] - pub value: Uri<'static>, + #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))] + pub value: Uri, /// Parameters for this property. #[cfg_attr( feature = "serde", @@ -949,8 +991,8 @@ pub struct UriProperty { pub parameters: Option, } -impl From> for UriProperty { - fn from(value: Uri<'static>) -> Self { +impl From for UriProperty { + fn from(value: Uri) -> Self { Self { value, group: None, @@ -963,7 +1005,7 @@ impl TryFrom<&str> for UriProperty { type Error = Error; fn try_from(value: &str) -> Result { - let uri = Uri::try_from(value)?.into_owned(); + let uri: Uri = value.parse()?; Ok(uri.into()) } } @@ -1085,6 +1127,10 @@ pub struct Gender { /// The sex for the gender. pub sex: Sex, /// The identity text. + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none") + )] pub identity: Option, } diff --git a/src/uri.rs b/src/uri.rs new file mode 100644 index 0000000..1b94dfc --- /dev/null +++ b/src/uri.rs @@ -0,0 +1,20 @@ +use crate::Error; +use std::{fmt, str::FromStr}; +use uriparse::URI; + +/// URI type for the library. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Uri(URI<'static>); + +impl fmt::Display for Uri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} +impl FromStr for Uri { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(Self(URI::try_from(s)?.into_owned())) + } +} diff --git a/tests/extensions.rs b/tests/extensions.rs index edad984..2f0132c 100644 --- a/tests/extensions.rs +++ b/tests/extensions.rs @@ -2,7 +2,6 @@ mod test_helpers; use anyhow::Result; use test_helpers::{assert_language, assert_round_trip}; -use uriparse::uri::URI as Uri; use vcard4::{ helper::{ parse_date_list, parse_date_time_list, parse_time_list, @@ -81,9 +80,7 @@ END:VCARD"#; prop.parameters.as_ref().unwrap().value.as_ref().unwrap() ); assert_eq!( - &AnyProperty::Uri( - Uri::try_from("http://example.com/foo")?.into_owned() - ), + &AnyProperty::Uri("http://example.com/foo".parse()?), &prop.value ); diff --git a/tests/organizational.rs b/tests/organizational.rs index bfc210d..8881250 100644 --- a/tests/organizational.rs +++ b/tests/organizational.rs @@ -2,8 +2,7 @@ mod test_helpers; use anyhow::Result; use test_helpers::assert_round_trip; -use uriparse::uri::URI as Uri; -use vcard4::{parameter::TypeParameter, parse, property::*}; +use vcard4::{parameter::TypeParameter, parse, property::*, Uri}; #[test] fn organizational_title() -> Result<()> { @@ -126,11 +125,11 @@ END:VCARD"#; let card = vcards.remove(0); assert_eq!( - Uri::try_from("urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af")?, + "urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af".parse::()?, card.member.get(0).unwrap().value ); assert_eq!( - Uri::try_from("urn:uuid:b8767877-b4a1-4c70-9acc-505d3819e519")?, + "urn:uuid:b8767877-b4a1-4c70-9acc-505d3819e519".parse::()?, card.member.get(1).unwrap().value ); assert_round_trip(&card)?; @@ -140,7 +139,8 @@ END:VCARD"#; card.uid.as_ref().unwrap() { assert_eq!( - &Uri::try_from("urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af")?, + &"urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af" + .parse::()?, value ); } else { @@ -153,7 +153,8 @@ END:VCARD"#; card.uid.as_ref().unwrap() { assert_eq!( - &Uri::try_from("urn:uuid:b8767877-b4a1-4c70-9acc-505d3819e519")?, + &"urn:uuid:b8767877-b4a1-4c70-9acc-505d3819e519" + .parse::()?, value ); } else { @@ -163,19 +164,19 @@ END:VCARD"#; let card = vcards.remove(0); assert_eq!( - Uri::try_from("mailto:subscriber1@example.com")?, + "mailto:subscriber1@example.com".parse::()?, card.member.get(0).unwrap().value ); assert_eq!( - Uri::try_from("xmpp:subscriber2@example.com")?, + "xmpp:subscriber2@example.com".parse::()?, card.member.get(1).unwrap().value ); assert_eq!( - Uri::try_from("sip:subscriber3@example.com")?, + "sip:subscriber3@example.com".parse::()?, card.member.get(2).unwrap().value ); assert_eq!( - Uri::try_from("tel:+1-418-555-5555")?, + "tel:+1-418-555-5555".parse::()?, card.member.get(3).unwrap().value ); assert_round_trip(&card)?; @@ -199,7 +200,8 @@ END:VCARD"#; }) = card.related.get(0).unwrap() { assert_eq!( - &Uri::try_from("urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6")?, + &"urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6" + .parse::()?, value ); @@ -225,7 +227,7 @@ END:VCARD"#; }) = card.related.get(0).unwrap() { assert_eq!( - &Uri::try_from("http://example.com/directory/jdoe.vcf")?, + &"http://example.com/directory/jdoe.vcf".parse::()?, value ); diff --git a/tests/parameters.rs b/tests/parameters.rs index 5fe0836..f57ab1d 100644 --- a/tests/parameters.rs +++ b/tests/parameters.rs @@ -1,7 +1,6 @@ mod test_helpers; use anyhow::Result; -use uriparse::uri::URI as Uri; use vcard4::{ helper::parse_utc_offset, @@ -318,8 +317,7 @@ END:VCARD"#; let prop = card.formatted_name.get(0).unwrap(); assert_eq!( &TimeZoneParameter::Uri( - Uri::try_from("https://example.com/tz-database/acdt")? - .into_owned() + "https://example.com/tz-database/acdt".parse()? ), prop.parameters.as_ref().unwrap().timezone.as_ref().unwrap() );