From b54fe5e228a9441d63198113ea80df2a657350c5 Mon Sep 17 00:00:00 2001 From: daladim Date: Mon, 29 Mar 2021 09:31:22 +0200 Subject: [PATCH] Added an iCal parser --- Cargo.lock | 30 +++++++++ Cargo.toml | 1 + src/ical/builder.rs | 5 ++ src/ical/mod.rs | 8 +++ src/ical/parser.rs | 150 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 6 files changed, 195 insertions(+) create mode 100644 src/ical/builder.rs create mode 100644 src/ical/mod.rs create mode 100644 src/ical/parser.rs diff --git a/Cargo.lock b/Cargo.lock index ec0b120..6bce024 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,6 +303,15 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "ical" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9f7215ad0d77e69644570dee000c7678a47ba7441062c1b5f918adde0d73cf" +dependencies = [ + "thiserror", +] + [[package]] name = "idna" version = "0.2.2" @@ -423,6 +432,7 @@ dependencies = [ "async-trait", "bitflags", "env_logger", + "ical", "log", "minidom", "reqwest", @@ -840,6 +850,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.3" diff --git a/Cargo.toml b/Cargo.toml index ed62b13..29a1581 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,4 @@ serde_json = "1.0" async-trait = "0.1" uuid = { version = "0.8", features = ["v4"] } sanitize-filename = "0.3" +ical = "0.7" diff --git a/src/ical/builder.rs b/src/ical/builder.rs new file mode 100644 index 0000000..b4f6329 --- /dev/null +++ b/src/ical/builder.rs @@ -0,0 +1,5 @@ +//! A module to build ICal files + +pub fn build_from() { + +} diff --git a/src/ical/mod.rs b/src/ical/mod.rs new file mode 100644 index 0000000..b8a520e --- /dev/null +++ b/src/ical/mod.rs @@ -0,0 +1,8 @@ +//! This module handles conversion between iCal files and internal representations +//! +//! It is a wrapper around different Rust third-party libraries, since I haven't find any complete library that is able to parse _and_ generate iCal files + +mod parser; +pub use parser::parse; +mod builder; +pub use builder::build_from; diff --git a/src/ical/parser.rs b/src/ical/parser.rs new file mode 100644 index 0000000..c60c7c1 --- /dev/null +++ b/src/ical/parser.rs @@ -0,0 +1,150 @@ +//! A module to parse ICal files + +use std::error::Error; + +use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo}; + +use crate::Item; +use crate::item::SyncStatus; +use crate::item::ItemId; +use crate::Task; +use crate::Event; + + +/// Parse an iCal file into the internal representation [`crate::Item`] +pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result> { + let mut reader = ical::IcalParser::new(content.as_bytes()); + let parsed_item = match reader.next() { + None => return Err(format!("Invalid uCal data to parse for item {}", item_id).into()), + Some(item) => match item { + Err(err) => return Err(format!("Unable to parse uCal data for item {}: {}", item_id, err).into()), + Ok(item) => item, + } + }; + + let item = match assert_single_type(&parsed_item)? { + CurrentType::Event(_) => { + Item::Event(Event::new()) + }, + + CurrentType::Todo(todo) => { + let mut name = None; + for prop in &todo.properties { + if prop.name == "SUMMARY" { + name = prop.value.clone(); + break; + } + } + let name = match name { + Some(name) => name, + None => return Err(format!("Missing name for item {}", item_id).into()), + }; + + Item::Task(Task::new(name, item_id, sync_status)) + }, + }; + + + // What to do with multiple items? + if reader.next().map(|r| r.is_ok()) == Some(true) { + return Err("Parsing multiple items are not supported".into()); + } + + Ok(item) +} + +enum CurrentType<'a> { + Event(&'a IcalEvent), + Todo(&'a IcalTodo), +} + +fn assert_single_type<'a>(item: &'a IcalCalendar) -> Result, Box> { + let n_events = item.events.len(); + let n_todos = item.todos.len(); + let n_journals = item.journals.len(); + + if n_events == 1 { + if n_todos != 0 || n_journals != 0 { + return Err("Only a single TODO or a single EVENT is supported".into()); + } else { + return Ok(CurrentType::Event(&item.events[0])); + } + } + + if n_todos == 1 { + if n_events != 0 || n_journals != 0 { + return Err("Only a single TODO or a single EVENT is supported".into()); + } else { + return Ok(CurrentType::Todo(&item.todos[0])); + } + } + + return Err("Only a single TODO or a single EVENT is supported".into()); +} + + +#[cfg(test)] +mod test { + const EXAMPLE_ICAL: &str = r#"BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Nextcloud Tasks v0.13.6 +BEGIN:VTODO +UID:0633de27-8c32-42be-bcb8-63bc879c6185 +CREATED:20210321T001600 +LAST-MODIFIED:20210321T001600 +DTSTAMP:20210321T001600 +SUMMARY:Do not forget to do this +END:VTODO +END:VCALENDAR +"#; + + const EXAMPLE_MULTIPLE_ICAL: &str = r#"BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Nextcloud Tasks v0.13.6 +BEGIN:VTODO +UID:0633de27-8c32-42be-bcb8-63bc879c6185 +CREATED:20210321T001600 +LAST-MODIFIED:20210321T001600 +DTSTAMP:20210321T001600 +SUMMARY:Call Mom +END:VTODO +END:VCALENDAR +BEGIN:VCALENDAR +BEGIN:VTODO +UID:0633de27-8c32-42be-bcb8-63bc879c6185 +CREATED:20210321T001600 +LAST-MODIFIED:20210321T001600 +DTSTAMP:20210321T001600 +SUMMARY:Buy a gift for Mom +END:VTODO +END:VCALENDAR +"#; + + use super::*; + use crate::item::VersionTag; + + #[test] + fn test_ical_parsing() { + let version_tag = VersionTag::from(String::from("test-tag")); + let sync_status = SyncStatus::Synced(version_tag); + let item_id: ItemId = "http://some.id/for/testing".parse().unwrap(); + + let item = parse(EXAMPLE_ICAL, item_id.clone(), sync_status.clone()).unwrap(); + let task = item.unwrap_task(); + + assert_eq!(task.name(), "Do not forget to do this"); + assert_eq!(task.id(), &item_id); + assert_eq!(task.completed(), false); + assert_eq!(task.sync_status(), &sync_status); + } + + #[test] + fn test_multiple_items_in_ical() { + let version_tag = VersionTag::from(String::from("test-tag")); + let sync_status = SyncStatus::Synced(version_tag); + let item_id: ItemId = "http://some.id/for/testing".parse().unwrap(); + + let item = parse(EXAMPLE_MULTIPLE_ICAL, item_id.clone(), sync_status.clone()); + assert!(item.is_err()); + } +} diff --git a/src/lib.rs b/src/lib.rs index c671e53..a7a398e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,7 @@ pub use provider::Provider; pub mod client; pub mod cache; +pub mod ical; pub mod settings; pub mod utils;