summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias Beyer <mail@beyermatthias.de>2017-11-21 13:15:00 +0100
committerMarkus Unterwaditzer <markus@unterwaditzer.net>2017-11-21 13:15:00 +0100
commitad934d47c97c933eebb038816803af70a27bb536 (patch)
tree3fc5ca2bf55e191f1a2e7d5f090828f98b7545ba
parenta2f6ad5215e892d9243d75c8fdfaf61f8da99a05 (diff)
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
-rw-r--r--.travis.yml4
-rw-r--r--Cargo.toml5
-rw-r--r--src/error.rs8
-rw-r--r--src/icalendar.rs316
-rw-r--r--src/lib.rs6
-rw-r--r--src/util.rs101
-rw-r--r--src/vcard.rs92
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
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<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()));
+ }
+
+}
diff --git a/src/lib.rs b/src/lib.rs
index 39a0362..38ea59a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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);