Skip to content

Commit

Permalink
Add Uri new type.
Browse files Browse the repository at this point in the history
This allows us to implement FromStr for the new type and therefore use
serde_with::serde_as to serialize Uri values to strings.

The default serde implementation for uriparse is rather verbose and
unnecessary.

As we are using serde_as in conjunction with cfg_attr it is required to
add the cfg_eval dependency so the macros are expanded correctly.
  • Loading branch information
tmpfs committed Dec 11, 2024
1 parent 3149b42 commit 1db5a6d
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 101 deletions.
9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
20 changes: 20 additions & 0 deletions examples/serde.rs
Original file line number Diff line number Diff line change
@@ -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::<vcard4::Uri>()?;

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");
}
65 changes: 31 additions & 34 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -233,47 +232,47 @@ 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
}

// 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
}

// 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
}
Expand Down Expand Up @@ -302,7 +301,7 @@ mod tests {
// General
.source(
"http://directory.example.com/addressbooks/jdoe.vcf"
.try_into()
.parse()
.unwrap(),
)
// Identification
Expand All @@ -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(),
)
Expand All @@ -334,38 +333,36 @@ mod tests {
// Communication
.telephone("+10987654321".to_owned())
.email("[email protected]".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:[email protected]\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";
Expand All @@ -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());
Expand All @@ -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());
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ mod parser;
pub mod property;
#[cfg(feature = "serde")]
mod serde;
mod uri;
mod vcard;

pub use builder::VcardBuilder;
Expand All @@ -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<T> = std::result::Result<T, Error>;
Expand Down
18 changes: 14 additions & 4 deletions src/parameter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ use std::{
str::FromStr,
};
use time::UtcOffset;
use uriparse::uri::URI as Uri;

#[cfg(feature = "language-tags")]
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};

Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)]
Expand All @@ -427,16 +434,18 @@ 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),
}

/// 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")]
Expand Down Expand Up @@ -524,7 +533,8 @@ pub struct Parameters {
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub geo: Option<Uri<'static>>,
#[cfg_attr(feature = "serde", serde_as(as = "Option<DisplayFromStr>"))]
pub geo: Option<Uri>,
/// The TZ parameter.
#[cfg_attr(
feature = "serde",
Expand Down
Loading

0 comments on commit 1db5a6d

Please sign in to comment.