6 changed files with 195 additions and 0 deletions
@ -0,0 +1,5 @@
|
||||
//! A module to build ICal files
|
||||
|
||||
pub fn build_from() { |
||||
|
||||
} |
||||
@ -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; |
||||
@ -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()); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue