Added an iCal parser
This commit is contained in:
parent
f6d542460c
commit
b54fe5e228
6 changed files with 195 additions and 0 deletions
30
Cargo.lock
generated
30
Cargo.lock
generated
|
@ -303,6 +303,15 @@ dependencies = [
|
||||||
"tokio-native-tls",
|
"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]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
@ -423,6 +432,7 @@ dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"ical",
|
||||||
"log",
|
"log",
|
||||||
"minidom",
|
"minidom",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
@ -840,6 +850,26 @@ dependencies = [
|
||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
|
|
|
@ -23,3 +23,4 @@ serde_json = "1.0"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
uuid = { version = "0.8", features = ["v4"] }
|
uuid = { version = "0.8", features = ["v4"] }
|
||||||
sanitize-filename = "0.3"
|
sanitize-filename = "0.3"
|
||||||
|
ical = "0.7"
|
||||||
|
|
5
src/ical/builder.rs
Normal file
5
src/ical/builder.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
//! A module to build ICal files
|
||||||
|
|
||||||
|
pub fn build_from() {
|
||||||
|
|
||||||
|
}
|
8
src/ical/mod.rs
Normal file
8
src/ical/mod.rs
Normal file
|
@ -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;
|
150
src/ical/parser.rs
Normal file
150
src/ical/parser.rs
Normal file
|
@ -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<Item, Box<dyn Error>> {
|
||||||
|
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<CurrentType<'a>, Box<dyn Error>> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ pub use provider::Provider;
|
||||||
|
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
|
pub mod ical;
|
||||||
|
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
Loading…
Add table
Reference in a new issue