diff options
-rw-r--r-- | .travis.yml | 4 | ||||
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | src/error.rs | 8 | ||||
-rw-r--r-- | src/icalendar.rs | 316 | ||||
-rw-r--r-- | src/lib.rs | 6 | ||||
-rw-r--r-- | src/util.rs | 101 | ||||
-rw-r--r-- | src/vcard.rs | 92 |
7 files changed, 440 insertions, 92 deletions
diff --git a/.travis.yml b/.travis.yml index 80ace8d..1dbeecc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,10 @@ rust: - stable - beta +script: + - cargo build --features timeconversions + - cargo test --features timeconversions + cache: cargo: true @@ -14,4 +14,9 @@ keywords = ["vobject", "icalendar", "calendar", "contacts"] [dependencies] error-chain = "0.11" +chrono = { version = "0.4", optional = true } + +[features] +default = [] +timeconversions = ["chrono"] diff --git a/src/error.rs b/src/error.rs index cf34910..22e64a8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,6 +5,10 @@ error_chain! { VObjectError, VObjectErrorKind, ResultExt, Result; } + foreign_links { + ChronoParseError(::chrono::format::ParseError) #[cfg(feature = "timeconversions")]; + } + errors { ParserError(desc: String) { description("Parser error") @@ -16,6 +20,10 @@ error_chain! { display("Passed content string is not a VCard") } + NotAnICalendar(content: String) { + description("Input is not a valid ICalendar") + display("Not an ICalendar: '{}'", content) + } } diff --git a/src/icalendar.rs b/src/icalendar.rs new file mode 100644 index 0000000..c9b8b94 --- /dev/null +++ b/src/icalendar.rs @@ -0,0 +1,316 @@ +use std::result::Result as RResult; + +use component::Component; +use component::parse_component; +use property::Property; +use error::*; +use util::*; + +#[cfg(feature = "timeconversions")] +use chrono::NaiveDateTime; + +#[cfg(feature = "timeconversions")] +use chrono::NaiveDate; + +/// An ICalendar representing type +#[derive(Debug)] +pub struct ICalendar(Component); + +impl ICalendar { + + /// Parse a string to a ICalendar object + /// + /// Returns an error if the parsed text is not a ICalendar (that means that an error is + /// returned also if this is a valid Vcard!) + /// + pub fn build(s: &str) -> Result<ICalendar> { + let c = parse_component(s)?; + Self::from_component(c) + .map_err(|_| { + let kind = VObjectErrorKind::NotAnICalendar(s.to_owned()); + VObjectError::from_kind(kind) + }) + } + + /// Wrap a Component into a Vcard object, or don't do it if the Component is not a Vcard. + pub fn from_component(c: Component)-> RResult<ICalendar, Component> { + if c.name == "VCALENDAR" { + Ok(ICalendar(c)) + } else { + Err(c) + } + } + + /// Get an iterator over the events in this calendar + /// + /// The iterator creates Ok(&Event) instances on the fly, or Err(&Component) instances if the + /// item cannot be parsed as an Event, not forgetting any data. + /// + /// # Getting actual objects + /// + /// For getting a Event-instance iterator from this, one can use this as follows: + /// + /// ``` + /// # use std::collections::BTreeMap; + /// # use vobject::component::Component; + /// # use vobject::icalendar::Event; + /// # use vobject::icalendar::ICalendar; + /// # let icalendar = ICalendar::from_component(Component { + /// # name: "VCALENDAR".to_owned(), + /// # props: BTreeMap::new(), + /// # subcomponents: vec![] + /// # }).unwrap(); + /// icalendar + /// .events() + /// .filter_map(Result::ok) + /// .map(|ev| ev.clone()) + /// .collect::<Vec<Event>>(); + /// ``` + /// + pub fn events<'a>(&'a self) -> EventIterator<'a> { + EventIterator::new(self.0.subcomponents.iter()) + } + + make_getter_function_for_optional!(get_version, "VERSION", Version); + make_getter_function_for_optional!(get_prodid, "PRODID", Prodid); +} + +create_data_type!(Version); +create_data_type!(Prodid); + +pub struct EventIterator<'a>(::std::slice::Iter<'a, Component>); + +impl<'a> EventIterator<'a> { + fn new(i: ::std::slice::Iter<'a, Component>) -> EventIterator<'a> { + EventIterator(i) + } +} + +impl<'a> Iterator for EventIterator<'a> { + type Item = RResult<Event<'a>, &'a Component>; + + fn next(&mut self) -> Option<Self::Item> { + self.0.next().map(Event::from_component) + } + +} + +#[derive(Debug, Clone)] +pub struct Event<'a>(&'a Component); + +impl<'a> Event<'a> { + fn from_component(c: &'a Component) -> RResult<Event<'a>, &'a Component> { + if c.name == "VEVENT" { + Ok(Event(c)) + } else { + Err(c) + } + } + + make_getter_function_for_optional!(get_dtend , "DTEND" , Dtend); + make_getter_function_for_optional!(get_dtstart , "DTSTART" , Dtstart); + make_getter_function_for_optional!(get_dtstamp , "DTSTAMP" , Dtstamp); + make_getter_function_for_optional!(get_uid , "UID" , Uid); + make_getter_function_for_optional!(get_description , "DESCRIPTION" , Description); + make_getter_function_for_optional!(get_summary , "SUMMARY" , Summary); + make_getter_function_for_optional!(get_url , "URL" , Url); + make_getter_function_for_optional!(get_location , "LOCATION" , Location); + make_getter_function_for_optional!(get_class , "CLASS" , Class); + make_getter_function_for_optional!(get_categories , "CATEGORIES" , Categories); + make_getter_function_for_optional!(get_transp , "TRANSP" , Transp); + make_getter_function_for_optional!(get_rrule , "RRULE" , Rrule); +} + +create_data_type!(Dtend); +create_data_type!(Dtstart); +create_data_type!(Dtstamp); +create_data_type!(Uid); +create_data_type!(Description); +create_data_type!(Summary); +create_data_type!(Url); +create_data_type!(Location); +create_data_type!(Class); +create_data_type!(Categories); +create_data_type!(Transp); +create_data_type!(Rrule); + +#[cfg(feature = "timeconversions")] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] +pub enum Time { + Date(NaiveDate), + DateTime(NaiveDateTime), +} + +#[cfg(feature = "timeconversions")] +pub trait AsDateTime { + fn as_datetime(&self) -> Result<Time>; +} + +#[cfg(feature = "timeconversions")] +impl AsDateTime for Dtend { + + fn as_datetime(&self) -> Result<Time> { + match NaiveDateTime::parse_from_str(&self.0, DATE_TIME_FMT) { + Ok(dt) => Ok(Time::DateTime(dt)), + Err(_) => NaiveDate::parse_from_str(&self.0, DATE_FMT) + .map(Time::Date) + .map_err(From::from), + } + } + +} + +#[cfg(feature = "timeconversions")] +impl AsDateTime for Dtstart { + + fn as_datetime(&self) -> Result<Time> { + match NaiveDateTime::parse_from_str(&self.0, DATE_TIME_FMT) { + Ok(dt) => Ok(Time::DateTime(dt)), + Err(_) => NaiveDate::parse_from_str(&self.0, DATE_FMT) + .map(Time::Date) + .map_err(From::from), + } + } + +} + +#[cfg(feature = "timeconversions")] +impl AsDateTime for Dtstamp { + + fn as_datetime(&self) -> Result<Time> { + match NaiveDateTime::parse_from_str(&self.0, DATE_TIME_FMT) { + Ok(dt) => Ok(Time::DateTime(dt)), + Err(_) => NaiveDate::parse_from_str(&self.0, DATE_FMT) + .map(Time::Date) + .map_err(From::from), + } + } + +} + +#[cfg(all(test, feature = "timeconversions"))] +mod tests { + use chrono::NaiveDate; + use chrono::NaiveDateTime; + use util::*; + + use super::*; + + const TEST_ENTRY : &'static str = + "BEGIN:VCALENDAR\n\ + VERSION:2.0\n\ + PRODID:http://www.example.com/calendarapplication/\n\ + METHOD:PUBLISH\n\ + BEGIN:VEVENT\n\ + UID:461092315540@example.com\n\ + ORGANIZER;CN=\"Alice Balder, Example Inc.\":MAILTO:alice@example.com\n\ + LOCATION:Somewhere\n\ + SUMMARY:Eine Kurzinfo\n\ + DESCRIPTION:Beschreibung des Termines\n\ + CLASS:PUBLIC\n\ + DTSTART:20060910T220000Z\n\ + DTEND:20060919T215900Z\n\ + DTSTAMP:20060812T125900Z\n\ + END:VEVENT\n\ + END:VCALENDAR\n"; + + const TEST_ENTRY_OC : &'static str = // Lets see how owncloud foo works here + "BEGIN:VCALENDAR\n\ + VERSION:2.0\n\ + PRODID:ownCloud Calendar\n\ + CALSCALE:GREGORIAN\n\ + BEGIN:VEVENT\n\ + UID:ff411055a5\n\ + DTSTAMP:20160128T223013Z\n\ + CREATED:20160128T223013Z\n\ + LAST-MODIFIED:20160128T223013Z\n\ + SUMMARY:Amon Amarth - Jomsviking\n\ + DTSTART;VALUE=DATE:20160325\n\ + DTEND;VALUE=DATE:20160326\n\ + LOCATION:\n\ + DESCRIPTION:\n\ + CATEGORIES:\n\ + END:VEVENT\n\ + END:VCALENDAR\n\ + "; + + #[test] + fn test_parse() { + let cal = ICalendar::build(TEST_ENTRY); + assert!(cal.is_ok(), "Not okay: {:?}\n in '{}'", cal, TEST_ENTRY); + } + + #[test] + fn test_iter() { + let ical = ICalendar::build(TEST_ENTRY).unwrap(); + assert_eq!(ical.events().count(), 1); + } + + #[test] + fn test_icalendar_attributes() { + let ical = ICalendar::build(TEST_ENTRY).unwrap(); + assert_eq!(ical.get_version().unwrap().raw(), "2.0"); + assert_eq!(ical.get_prodid().unwrap().raw(), "http://www.example.com/calendarapplication/"); + } + + #[test] + fn test_event_attributes() { + let ical = ICalendar::build(TEST_ENTRY).unwrap(); + let ev = ical.events().next().unwrap().unwrap(); + assert_eq!(ev.get_dtend().map(|e| e.raw().clone()) , Some("20060919T215900Z".to_owned())); + assert_eq!(ev.get_dtstart().map(|e| e.raw().clone()) , Some("20060910T220000Z".to_owned())); + assert_eq!(ev.get_dtstamp().map(|e| e.raw().clone()) , Some("20060812T125900Z".to_owned())); + assert_eq!(ev.get_uid().map(|e| e.raw().clone()) , Some("461092315540@example.com".to_owned())); + assert_eq!(ev.get_description().map(|e| e.raw().clone()) , Some("Beschreibung des Termines".to_owned())); + assert_eq!(ev.get_summary().map(|e| e.raw().clone()) , Some("Eine Kurzinfo".to_owned())); + assert_eq!(ev.get_url() , None); + assert_eq!(ev.get_location().map(|e| e.raw().clone()) , Some("Somewhere".to_owned())); + assert_eq!(ev.get_class().map(|e| e.raw().clone()) , Some("PUBLIC".to_owned())); + assert_eq!(ev.get_categories() , None); + assert_eq!(ev.get_transp() , None); + assert_eq!(ev.get_rrule() , None); + } + + #[test] + fn test_event_attributes_oc() { + let ical = ICalendar::build(TEST_ENTRY_OC).unwrap(); + assert_eq!(ical.get_version().unwrap().raw(), "2.0"); + assert_eq!(ical.get_prodid().unwrap().raw(), "ownCloud Calendar"); + let ev = ical.events().next().unwrap().unwrap(); + assert_eq!(ev.get_dtend().map(|e| e.raw().clone()) , Some("20160326".to_owned())); + assert_eq!(ev.get_dtstart().map(|e| e.raw().clone()) , Some("20160325".to_owned())); + assert_eq!(ev.get_dtstamp().map(|e| e.raw().clone()) , Some("20160128T223013Z".to_owned())); + assert_eq!(ev.get_uid().map(|e| e.raw().clone()) , Some("ff411055a5".to_owned())); + assert_eq!(ev.get_description().map(|e| e.raw().clone()) , Some("".to_owned())); + assert_eq!(ev.get_summary().map(|e| e.raw().clone()) , Some("Amon Amarth - Jomsviking".to_owned())); + assert_eq!(ev.get_url() , None); + assert_eq!(ev.get_location().map(|e| e.raw().clone()) , Some("".to_owned())); + assert_eq!(ev.get_class().map(|e| e.raw().clone()) , None); + assert_eq!(ev.get_categories().map(|e| e.raw().clone()) , Some("".to_owned())); + assert_eq!(ev.get_transp() , None); + assert_eq!(ev.get_rrule() , None); + } + + #[cfg(feature = "timeconversions")] + #[test] + fn test_event_attributes_with_conversions() { + let ical = ICalendar::build(TEST_ENTRY).unwrap(); + let ev = ical.events().next().unwrap().unwrap(); + assert_eq!(ev.get_dtend().map(|e| e.as_datetime().unwrap()).unwrap(), Time::DateTime(NaiveDateTime::parse_from_str("20060919T215900Z", DATE_TIME_FMT).unwrap())); + assert_eq!(ev.get_dtstart().map(|e| e.as_datetime().unwrap()).unwrap(), Time::DateTime(NaiveDateTime::parse_from_str("20060910T220000Z", DATE_TIME_FMT).unwrap())); + assert_eq!(ev.get_dtstamp().map(|e| e.as_datetime().unwrap()).unwrap(), Time::DateTime(NaiveDateTime::parse_from_str("20060812T125900Z", DATE_TIME_FMT).unwrap())); + } + + #[cfg(feature = "timeconversions")] + #[test] + fn test_event_attributes_oc_with_conversions() { + let ical = ICalendar::build(TEST_ENTRY_OC).unwrap(); + assert_eq!(ical.get_version().unwrap().raw(), "2.0"); + assert_eq!(ical.get_prodid().unwrap().raw(), "ownCloud Calendar"); + let ev = ical.events().next().unwrap().unwrap(); + assert_eq!(ev.get_dtend().map(|e| e.as_datetime().unwrap()).unwrap(), Time::Date(NaiveDate::parse_from_str("20160326", DATE_FMT).unwrap())); + assert_eq!(ev.get_dtstart().map(|e| e.as_datetime().unwrap()).unwrap(), Time::Date(NaiveDate::parse_from_str("20160325", DATE_FMT).unwrap())); + assert_eq!(ev.get_dtstamp().map(|e| e.as_datetime().unwrap()).unwrap(), Time::DateTime(NaiveDateTime::parse_from_str("20160128T223013Z", DATE_TIME_FMT).unwrap())); + } + +} @@ -3,12 +3,18 @@ #[macro_use] extern crate error_chain; +#[cfg(feature = "timeconversions")] +extern crate chrono; + #[macro_use] pub mod param; +#[macro_use] mod util; + pub mod component; pub mod error; mod parser; pub mod property; pub mod vcard; +pub mod icalendar; pub use component::Component; pub use component::parse_component; diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..f414ed7 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,101 @@ +#[macro_export] +macro_rules! make_getter_function_for_optional { + ($fnname:ident, $name:expr, $mapper:ty) => { + pub fn $fnname(&self) -> Option<$mapper> { + self.0.get_only($name).cloned().map(From::from) + } + } +} + +#[macro_export] +macro_rules! make_getter_function_for_values { + ($fnname:ident, $name:expr, $mapper:ty) => { + pub fn $fnname(&self) -> Vec<$mapper> { + self.0 + .get_all($name) + .iter() + .map(Clone::clone) + .map(From::from) + .collect() + } + } +} + +#[macro_export] +macro_rules! create_data_type { + ( $name:ident ) => { + #[derive(Eq, PartialEq, Debug)] + pub struct $name(String, $crate::param::Parameters); + + impl $name { + fn new(raw: String, params: $crate::param::Parameters) -> $name { + $name(raw, params) + } + + pub fn raw(&self) -> &String { + &self.0 + } + } + + impl From<Property> for $name { + fn from(p: Property) -> $name { + $name::new(p.raw_value, p.params) + } + } + } +} + +macro_rules! make_builder_fn { + ( + fn $fnname:ident building $property_name:tt with_params, + $mapfn:expr => $( $arg_name:ident : $arg_type:ty ),* + ) => { + pub fn $fnname(mut self, params: $crate::param::Parameters, $( $arg_name : $arg_type ),*) -> Self { + let raw_value = vec![ $( $arg_name ),* ] + .into_iter() + .map($mapfn) + .collect::<Vec<_>>() + .join(";"); + + let prop = Property { + name: String::from($property_name), + params: params, + raw_value: raw_value, + prop_group: None + }; + + self.0.props.entry(String::from($property_name)).or_insert(vec![]).push(prop); + self + } + }; + + ( + fn $fnname:ident building $property_name:tt, + $mapfn:expr => $( $arg_name:ident : $arg_type:ty ),* + ) => { + pub fn $fnname(mut self, $( $arg_name : $arg_type ),*) -> Self { + let raw_value = vec![ $( $arg_name ),* ] + .into_iter() + .map($mapfn) + .collect::<Vec<_>>() + .join(";"); + + + let prop = Property { + name: String::from($property_name), + params: BTreeMap::new(), + raw_value: raw_value, + prop_group: None + }; + self.0.props.entry(String::from($property_name)).or_insert(vec![]).push(prop); + self + } + } +} + +#[cfg(feature = "timeconversions")] +pub const DATE_TIME_FMT : &'static str = "%Y%m%dT%H%M%SZ"; + +#[cfg(feature = "timeconversions")] +pub const DATE_FMT : &'static str = "%Y%m%d"; + diff --git a/src/vcard.rs b/src/vcard.rs index 05d7498..fece8a2 100644 --- a/src/vcard.rs +++ b/src/vcard.rs @@ -7,78 +7,9 @@ use property::Property; use std::result::Result as RResult; use error::*; -use param::Parameters; pub struct Vcard(Component); -macro_rules! make_getter_function_for_optional { - ($fnname:ident, $name:expr, $mapper:ty) => { - pub fn $fnname(&self) -> Option<$mapper> { - self.0.get_only($name).cloned().map(From::from) - } - } -} - -macro_rules! make_getter_function_for_values { - ($fnname:ident, $name:expr, $mapper:ty) => { - pub fn $fnname(&self) -> Vec<$mapper> { - self.0 - .get_all($name) - .iter() - .map(Clone::clone) - .map(From::from) - .collect() - } - } -} - -macro_rules! make_builder_fn { - ( - fn $fnname:ident building $property_name:tt with_params, - $mapfn:expr => $( $arg_name:ident : $arg_type:ty ),* - ) => { - pub fn $fnname(mut self, params: Parameters, $( $arg_name : $arg_type ),*) -> Self { - let raw_value = vec![ $( $arg_name ),* ] - .into_iter() - .map($mapfn) - .collect::<Vec<_>>() - .join(";"); - - let prop = Property { - name: String::from($property_name), - params: params, - raw_value: raw_value, - prop_group: None - }; - self.0.props.entry(String::from($property_name)).or_insert(vec![]).push(prop); - self - } - }; - - ( - fn $fnname:ident building $property_name:tt, - $mapfn:expr => $( $arg_name:ident : $arg_type:ty ),* - ) => { - pub fn $fnname(mut self, $( $arg_name : $arg_type ),*) -> Self { - let raw_value = vec![ $( $arg_name ),* ] - .into_iter() - .map($mapfn) - .collect::<Vec<_>>() - .join(";"); - - let prop = Property { - name: String::from($property_name), - params: BTreeMap::new(), - raw_value: raw_value, - prop_group: None - }; - self.0.props.entry(String::from($property_name)).or_insert(vec![]).push(prop); - self - } - } -} - - /// The Vcard object. /// /// This type simply holds data and offers functions to access this data. It does not compute @@ -205,29 +136,6 @@ impl Deref for Vcard { } } -macro_rules! create_data_type { - ( $name:ident ) => { - #[derive(Eq, PartialEq)] - pub struct $name(String, Parameters); - - impl $name { - fn new(raw: String, params: Parameters) -> $name { - $name(raw, params) - } - - pub fn raw(&self) -> &String { - &self.0 - } - } - - impl From<Property> for $name { - fn from(p: Property) -> $name { - $name::new(p.raw_value, p.params) - } - } - } -} - create_data_type!(Adr); create_data_type!(Anniversary); create_data_type!(BDay); |