From ad934d47c97c933eebb038816803af70a27bb536 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Tue, 21 Nov 2017 13:15:00 +0100 Subject: Icalendar highlevel interface (#19) * Add error kind for "not an icalendar" * Move helper macros to utils * Add optional date/datetime conversions * Add optional dependency: chrono * Add error types for converting from parser error from chrono * Add AsDateTime for icalendar times * Add travis build script with all features tested * Add tests * Add tests with simple test entry * Add test for owncloud-generated cal entry * Add conversions-testing for entries * Use container type for returning either Date or DateTime * fixup! Move helper macros to utils * Fix to use list syntax * Capitalize consistently * Use ? instead of callback chaining * Remove all unneeded imports --- .travis.yml | 4 + Cargo.toml | 5 + src/error.rs | 8 ++ src/icalendar.rs | 316 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 6 ++ src/util.rs | 101 ++++++++++++++++++ src/vcard.rs | 92 ---------------- 7 files changed, 440 insertions(+), 92 deletions(-) create mode 100644 src/icalendar.rs create mode 100644 src/util.rs 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 diff --git a/Cargo.toml b/Cargo.toml index 61686e2..f3840fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 { + 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 { + 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::>(); + /// ``` + /// + 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, &'a Component>; + + fn next(&mut self) -> Option { + 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, &'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