From caaddf910c9bb24ee3d23a9dfd8f5242db738beb Mon Sep 17 00:00:00 2001 From: daladim Date: Mon, 1 Mar 2021 23:39:16 +0100 Subject: [PATCH] Major overhaul: more generics! --- src/bin/dummy.rs | 34 +++++++-- src/cache.rs | 21 +++--- src/calendar/cached_calendar.rs | 124 ++++++++++++++++++++++++++++++++ src/calendar/mod.rs | 115 +---------------------------- src/calendar/remote_calendar.rs | 38 ++++++++++ src/client.rs | 13 ++-- src/lib.rs | 2 +- src/provider.rs | 40 +++++++---- src/traits.rs | 73 +++++++++++++++++-- src/utils.rs | 4 +- tests/caldav_client.rs | 6 +- tests/sync.rs | 7 +- 12 files changed, 314 insertions(+), 163 deletions(-) create mode 100644 src/calendar/cached_calendar.rs create mode 100644 src/calendar/remote_calendar.rs diff --git a/src/bin/dummy.rs b/src/bin/dummy.rs index eabd92e..4c5d09d 100644 --- a/src/bin/dummy.rs +++ b/src/bin/dummy.rs @@ -1,17 +1,37 @@ -use my_tasks::client::Client; +use std::path::Path; + +use my_tasks::{client::Client, traits::CalDavSource}; +use my_tasks::cache::Cache; +use my_tasks::Provider; use my_tasks::settings::URL; use my_tasks::settings::USERNAME; use my_tasks::settings::PASSWORD; +const CACHE_FILE: &str = "caldav_cache.json"; + #[tokio::main] async fn main() { - // This is just a function to silence "unused function" warning + /* + let cache_path = Path::new(CACHE_FILE); let mut client = Client::new(URL, USERNAME, PASSWORD).unwrap(); - let calendars = client.get_calendars().await.unwrap(); - let _ = calendars.iter() - .map(|cal| println!(" {}\t{}", cal.name(), cal.url().as_str())) - .collect::<()>(); - let _ = client.get_tasks(&calendars[3].url()).await; + let mut cache = match Cache::from_file(&cache_path) { + Ok(cache) => cache, + Err(err) => { + log::warn!("Invalid cache file: {}. Using a default cache", err); + Cache::new(&cache_path) + } + }; + let provider = Provider::new(client, cache); + + let cals = provider.local().get_calendars().await.unwrap(); + println!("---- before sync -----"); + my_tasks::utils::print_calendar_list(cals); + + provider.sync(); + println!("---- after sync -----"); + my_tasks::utils::print_calendar_list(cals); + */ + } diff --git a/src/cache.rs b/src/cache.rs index 53a0ee6..987a949 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -11,7 +11,8 @@ use chrono::{DateTime, Utc}; use crate::traits::CalDavSource; use crate::traits::SyncSlave; -use crate::Calendar; +use crate::traits::PartialCalendar; +use crate::calendar::cached_calendar::CachedCalendar; /// A CalDAV source that stores its item in a local file @@ -23,7 +24,7 @@ pub struct Cache { #[derive(Default, Debug, PartialEq, Serialize, Deserialize)] struct CachedData { - calendars: Vec, + calendars: Vec, last_sync: Option>, } @@ -76,33 +77,33 @@ impl Cache { } - pub fn add_calendar(&mut self, calendar: Calendar) { + pub fn add_calendar(&mut self, calendar: CachedCalendar) { self.data.calendars.push(calendar); } } #[async_trait] -impl CalDavSource for Cache { - async fn get_calendars(&self) -> Result<&Vec, Box> { +impl CalDavSource for Cache { + async fn get_calendars(&self) -> Result<&Vec, Box> { Ok(&self.data.calendars) } - async fn get_calendars_mut(&mut self) -> Result, Box> { + async fn get_calendars_mut(&mut self) -> Result, Box> { Ok( self.data.calendars.iter_mut() .collect() ) } - async fn get_calendar(&self, url: Url) -> Option<&Calendar> { + async fn get_calendar(&self, url: Url) -> Option<&CachedCalendar> { for cal in &self.data.calendars { if cal.url() == &url { return Some(cal); -} + } } return None; } - async fn get_calendar_mut(&mut self, url: Url) -> Option<&mut Calendar> { + async fn get_calendar_mut(&mut self, url: Url) -> Option<&mut CachedCalendar> { for cal in &mut self.data.calendars { if cal.url() == &url { return Some(cal); @@ -135,7 +136,7 @@ mod tests { let mut cache = Cache::new(&cache_path); - let cal1 = Calendar::new("shopping list".to_string(), + let cal1 = CachedCalendar::new("shopping list".to_string(), Url::parse("https://caldav.com/shopping").unwrap(), SupportedComponents::TODO); cache.add_calendar(cal1); diff --git a/src/calendar/cached_calendar.rs b/src/calendar/cached_calendar.rs new file mode 100644 index 0000000..d9f9ade --- /dev/null +++ b/src/calendar/cached_calendar.rs @@ -0,0 +1,124 @@ +use std::convert::TryFrom; +use std::error::Error; +use std::collections::HashMap; +use std::collections::BTreeMap; + +use url::Url; +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; + +use crate::traits::{PartialCalendar, CompleteCalendar}; +use crate::calendar::{SupportedComponents, SearchFilter}; +use crate::Item; +use crate::item::ItemId; + + +/// A calendar used by the [`cache`](crate::cache) module +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct CachedCalendar { + name: String, + url: Url, + supported_components: SupportedComponents, + + items: Vec, + deleted_items: BTreeMap, ItemId>, +} + +impl CachedCalendar { + /// Create a new calendar + pub fn new(name: String, url: Url, supported_components: SupportedComponents) -> Self { + Self { + name, url, supported_components, + items: Vec::new(), + deleted_items: BTreeMap::new(), + } + } + + /// Returns the list of tasks that this calendar contains + pub fn get_tasks(&self) -> HashMap { + self.get_tasks_modified_since(None) + } + /// Returns the tasks that have been last-modified after `since` + pub fn get_tasks_modified_since(&self, since: Option>) -> HashMap { + self.get_items_modified_since(since, Some(SearchFilter::Tasks)) + } +} + +impl PartialCalendar for CachedCalendar { + fn name(&self) -> &str { + &self.name + } + + fn url(&self) -> &Url { + &self.url + } + + fn supported_components(&self) -> SupportedComponents { + self.supported_components + } + + fn add_item(&mut self, item: Item) { + self.items.push(item); + } + + fn delete_item(&mut self, item_id: &ItemId) { + self.items.retain(|i| i.id() != item_id); + self.deleted_items.insert(Utc::now(), item_id.clone()); + } + + fn get_items_modified_since(&self, since: Option>, filter: Option) -> HashMap { + let filter = filter.unwrap_or_default(); + + let mut map = HashMap::new(); + + for item in &self.items { + match since { + None => (), + Some(since) => if item.last_modified() < since { + continue; + }, + } + + match filter { + SearchFilter::Tasks => { + if item.is_task() == false { + continue; + } + }, + _ => (), + } + + map.insert(item.id().clone(), item); + } + + map + } + + fn get_item_by_id_mut(&mut self, id: &ItemId) -> Option<&mut Item> { + for item in &mut self.items { + if item.id() == id { + return Some(item); + } + } + return None; + } + + fn find_missing_items_compared_to(&self, other: &dyn PartialCalendar) -> Vec { + unimplemented!("todo"); + } +} + +impl CompleteCalendar for CachedCalendar { + /// Returns the items that have been deleted after `since` + fn get_items_deleted_since(&self, since: DateTime) -> Vec { + self.deleted_items.range(since..) + .map(|(_key, value)| value.clone()) + .collect() + } + + /// Returns the list of items that this calendar contains + fn get_items(&self) -> HashMap { + self.get_items_modified_since(None, None) + } + +} diff --git a/src/calendar/mod.rs b/src/calendar/mod.rs index de74c9b..768819d 100644 --- a/src/calendar/mod.rs +++ b/src/calendar/mod.rs @@ -1,3 +1,5 @@ +pub mod cached_calendar; + use std::convert::TryFrom; use std::error::Error; use std::collections::HashMap; @@ -66,116 +68,3 @@ impl Default for SearchFilter { SearchFilter::All } } - -/// A Caldav Calendar -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Calendar { - name: String, - url: Url, - supported_components: SupportedComponents, - - items: Vec, - deleted_items: BTreeMap, ItemId>, -} - -impl Calendar { - /// Create a new calendar - pub fn new(name: String, url: Url, supported_components: SupportedComponents) -> Self { - Self { - name, url, supported_components, - items: Vec::new(), - deleted_items: BTreeMap::new(), - } - } - - /// Returns the calendar name - pub fn name(&self) -> &str { - &self.name - } - - /// Returns the calendar URL - pub fn url(&self) -> &Url { - &self.url - } - - /// Returns whether this calDAV calendar supports to-do items - pub fn supports_todo(&self) -> bool { - self.supported_components.contains(SupportedComponents::TODO) - } - - /// Returns whether this calDAV calendar supports calendar items - pub fn supports_events(&self) -> bool { - self.supported_components.contains(SupportedComponents::EVENT) - } - - /// Add an item into this calendar - pub fn add_item(&mut self, item: Item) { - self.items.push(item); - } - - /// Remove an item from this calendar - pub fn delete_item(&mut self, item_id: &ItemId) { - self.items.retain(|i| i.id() != item_id); - self.deleted_items.insert(Utc::now(), item_id.clone()); - } - - /// Returns the list of items that this calendar contains - pub fn get_items(&self) -> HashMap { - self.get_items_modified_since(None, None) - } - /// Returns the items that have been last-modified after `since` - pub fn get_items_modified_since(&self, since: Option>, filter: Option) -> HashMap { - let filter = filter.unwrap_or_default(); - - let mut map = HashMap::new(); - - for item in &self.items { - match since { - None => (), - Some(since) => if item.last_modified() < since { - continue; - }, - } - - match filter { - SearchFilter::Tasks => { - if item.is_task() == false { - continue; - } - }, - _ => (), - } - - map.insert(item.id().clone(), item); - } - - map - } - - /// Returns the items that have been deleted after `since` - pub fn get_items_deleted_since(&self, since: DateTime) -> Vec { - self.deleted_items.range(since..) - .map(|(_key, value)| value.clone()) - .collect() - } - - /// Returns a particular item - pub fn get_item_by_id_mut(&mut self, id: &ItemId) -> Option<&mut Item> { - for item in &mut self.items { - if item.id() == id { - return Some(item); - } - } - return None; - } - - - /// Returns the list of tasks that this calendar contains - pub fn get_tasks(&self) -> HashMap { - self.get_tasks_modified_since(None) - } - /// Returns the tasks that have been last-modified after `since` - pub fn get_tasks_modified_since(&self, since: Option>) -> HashMap { - self.get_items_modified_since(since, Some(SearchFilter::Tasks)) - } -} diff --git a/src/calendar/remote_calendar.rs b/src/calendar/remote_calendar.rs new file mode 100644 index 0000000..2459a45 --- /dev/null +++ b/src/calendar/remote_calendar.rs @@ -0,0 +1,38 @@ +use crate::traits::PartialCalendar; + +/// A CalDAV calendar created by a [`Client`](crate::client::Client). +pub struct RemoteCalendar { + name: String, + url: Url, + supported_components: SupportedComponents +} + +impl PartialCalendar for RemoteCalendar { + fn name(&self) -> &str { + &self.name + } + + fn supported_components(&self) -> crate::calendar::SupportedComponents { + self.supported_components + } + + fn get_items_modified_since(&self, since: Option>, filter: Option) + -> HashMap + { + log::error!("Not implemented"); + HashMap::new() + } + + fn get_item_by_id_mut(&mut self, id: &ItemId) -> Option<&mut Item> { + log::error!("Not implemented"); + None + } + + fn add_item(&mut self, item: Item) { + log::error!("Not implemented"); + } + + fn delete_item(&mut self, item_id: &ItemId) { + log::error!("Not implemented"); + } +} diff --git a/src/client.rs b/src/client.rs index a35018c..4ab8570 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,14 +2,19 @@ use std::error::Error; use std::convert::TryFrom; +use std::sync::Mutex; use reqwest::Method; use reqwest::header::CONTENT_TYPE; use minidom::Element; use url::Url; +use async_trait::async_trait; use crate::utils::{find_elem, find_elems}; -use crate::calendar::Calendar; +use crate::calendar::cached_calendar::CachedCalendar; +use crate::traits::PartialCalendar; +use crate::traits::CalDavSource; + static DAVCLIENT_BODY: &str = r#" @@ -69,7 +74,7 @@ pub struct Client { principal: Option, calendar_home_set: Option, - calendars: Option>, + calendars: Option>, } impl Client { @@ -148,7 +153,7 @@ impl Client { } /// Return the list of calendars, or fetch from server if not known yet - pub async fn get_calendars(&mut self) -> Result, Box> { + pub async fn get_calendars(&mut self) -> Result, Box> { if let Some(c) = &self.calendars { return Ok(c.to_vec()); } @@ -206,7 +211,7 @@ impl Client { }, Ok(sc) => sc, }; - let this_calendar = Calendar::new(display_name, this_calendar_url, supported_components); + let this_calendar = CachedCalendar::new(display_name, this_calendar_url, supported_components); log::info!("Found calendar {}", this_calendar.name()); calendars.push(this_calendar); } diff --git a/src/lib.rs b/src/lib.rs index 8937952..e7f6644 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ pub mod traits; pub mod calendar; -pub use calendar::Calendar; +pub use calendar::cached_calendar::CachedCalendar; mod item; pub use item::Item; mod task; diff --git a/src/provider.rs b/src/provider.rs index 3579587..4b0657d 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -1,39 +1,49 @@ //! This modules abstracts data sources and merges them in a single virtual one -use std::error::Error; +use std::{error::Error, marker::PhantomData}; use chrono::{DateTime, Utc}; -use crate::traits::CalDavSource; +use crate::traits::{CalDavSource, CompleteCalendar}; use crate::traits::SyncSlave; -use crate::Calendar; +use crate::traits::PartialCalendar; +use crate::calendar::cached_calendar::CachedCalendar; use crate::Item; use crate::item::ItemId; /// A data source that combines two `CalDavSources` (usually a server and a local cache), which is able to sync both sources. -pub struct Provider +pub struct Provider where - S: CalDavSource, - L: CalDavSource + SyncSlave, + L: CalDavSource + SyncSlave, + T: CompleteCalendar, + S: CalDavSource, + U: PartialCalendar, { /// The remote server server: S, /// The local cache local: L, + + phantom_t: PhantomData, + phantom_u: PhantomData, } -impl Provider +impl Provider where - S: CalDavSource, - L: CalDavSource + SyncSlave, + L: CalDavSource + SyncSlave, + T: CompleteCalendar, + S: CalDavSource, + U: PartialCalendar, { /// Create a provider. /// /// `server` is usually a [`Client`](crate::client::Client), `local` is usually a [`Cache`](crate::cache::Cache). /// However, both can be interchangeable. The only difference is that `server` always wins in case of a sync conflict pub fn new(server: S, local: L) -> Self { - Self { server, local } + Self { server, local, + phantom_t: PhantomData, phantom_u: PhantomData, + } } /// Returns the data source described as the `server` @@ -62,9 +72,9 @@ where Some(cal) => cal, }; - let server_mod = cal_server.get_tasks_modified_since(last_sync); + let server_mod = cal_server.get_items_modified_since(last_sync, None); let server_del = match last_sync { - Some(date) => cal_server.get_items_deleted_since(date), + Some(date) => cal_server.find_missing_items_compared_to(cal_local), None => Vec::new(), }; let local_del = match last_sync { @@ -91,7 +101,7 @@ where // Push local changes to the server - let local_mod = cal_local.get_tasks_modified_since(last_sync); + let local_mod = cal_local.get_items_modified_since(last_sync, None); let mut tasks_to_add_to_server = Vec::new(); let mut tasks_id_to_remove_from_server = Vec::new(); @@ -122,14 +132,14 @@ where } -fn move_to_calendar(items: &mut Vec, calendar: &mut Calendar) { +fn move_to_calendar(items: &mut Vec, calendar: &mut C) { while items.len() > 0 { let item = items.remove(0); calendar.add_item(item); } } -fn remove_from_calendar(ids: &Vec, calendar: &mut Calendar) { +fn remove_from_calendar(ids: &Vec, calendar: &mut C) { for id in ids { log::info!(" Removing {:?} from local calendar", id); calendar.delete_item(id); diff --git a/src/traits.rs b/src/traits.rs index 9938a33..027378b 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,19 +1,22 @@ use std::error::Error; +use std::collections::HashMap; use async_trait::async_trait; use url::Url; use chrono::{DateTime, Utc}; -use crate::Calendar; +use crate::calendar::cached_calendar::CachedCalendar; +use crate::item::Item; +use crate::item::ItemId; #[async_trait] -pub trait CalDavSource { +pub trait CalDavSource { /// Returns the current calendars that this source contains /// This function may trigger an update (that can be a long process, or that can even fail, e.g. in case of a remote server) - async fn get_calendars(&self) -> Result<&Vec, Box>; + async fn get_calendars(&self) -> Result<&Vec, Box>; /// Returns the current calendars that this source contains /// This function may trigger an update (that can be a long process, or that can even fail, e.g. in case of a remote server) - async fn get_calendars_mut(&mut self) -> Result, Box>; + async fn get_calendars_mut(&mut self) -> Result, Box>; // // @@ -21,9 +24,9 @@ pub trait CalDavSource { // TODO: search key should be a reference // /// Returns the calendar matching the URL - async fn get_calendar(&self, url: Url) -> Option<&Calendar>; + async fn get_calendar(&self, url: Url) -> Option<&T>; /// Returns the calendar matching the URL - async fn get_calendar_mut(&mut self, url: Url) -> Option<&mut Calendar>; + async fn get_calendar_mut(&mut self, url: Url) -> Option<&mut T>; } @@ -34,3 +37,61 @@ pub trait SyncSlave { /// Update the last sync timestamp to now, or to a custom time in case `timepoint` is `Some` fn update_last_sync(&mut self, timepoint: Option>); } + +/// A calendar we have a partial knowledge of. +/// +/// Usually, this is a calendar from a remote source, that is synced to a CompleteCalendar +pub trait PartialCalendar { + /// Returns the calendar name + fn name(&self) -> &str; + + /// Returns the calendar URL + fn url(&self) -> &Url; + + /// Returns the supported kinds of components for this calendar + fn supported_components(&self) -> crate::calendar::SupportedComponents; + + /// Returns the items that have been last-modified after `since` + fn get_items_modified_since(&self, since: Option>, filter: Option) + -> HashMap; + + /// Returns a particular item + fn get_item_by_id_mut(&mut self, id: &ItemId) -> Option<&mut Item>; + + /// Add an item into this calendar + fn add_item(&mut self, item: Item); + + /// Remove an item from this calendar + fn delete_item(&mut self, item_id: &ItemId); + + /// Compares with another calendar and lists missing items + /// This function is a sort of replacement for `get_items_deleted_since`, that is not available on PartialCalendars + fn find_missing_items_compared_to(&self, other: &dyn PartialCalendar) -> Vec; +} + +/// A calendar we always know everything about. +/// +/// Usually, this is a calendar fully stored on a local disk +pub trait CompleteCalendar : PartialCalendar { + /// Returns the items that have been deleted after `since` + /// + /// See also [`PartialCalendar::get_items_deleted_since`] + fn get_items_deleted_since(&self, since: DateTime) -> Vec; + + /// Returns the list of items that this calendar contains + fn get_items(&self) -> HashMap; +} + + + +impl PartialCalendar { + /// Returns whether this calDAV calendar supports to-do items + pub fn supports_todo(&self) -> bool { + self.supported_components().contains(crate::calendar::SupportedComponents::TODO) + } + + /// Returns whether this calDAV calendar supports calendar items + pub fn supports_events(&self) -> bool { + self.supported_components().contains(crate::calendar::SupportedComponents::EVENT) + } +} diff --git a/src/utils.rs b/src/utils.rs index d46b600..badc5e1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,7 @@ use minidom::Element; -use crate::Calendar; +use crate::traits::CompleteCalendar; /// Walks an XML tree and returns every element that has the given name pub fn find_elems>(root: &Element, searched_name: S) -> Vec<&Element> { @@ -53,7 +53,7 @@ pub fn print_xml(element: &Element) { } /// A debug utility that pretty-prints calendars -pub fn print_calendar_list(cals: &Vec) { +pub fn print_calendar_list(cals: &Vec) { for cal in cals { println!("CAL {}", cal.url()); for (_, item) in cal.get_items() { diff --git a/tests/caldav_client.rs b/tests/caldav_client.rs index 314fd0c..defd6fb 100644 --- a/tests/caldav_client.rs +++ b/tests/caldav_client.rs @@ -7,6 +7,8 @@ use minidom::Element; use url::Url; use my_tasks::client::Client; +use my_tasks::traits::PartialCalendar; + use my_tasks::settings::URL; use my_tasks::settings::USERNAME; use my_tasks::settings::PASSWORD; @@ -24,7 +26,7 @@ static EXAMPLE_TASKS_BODY_LAST_MODIFIED: &str = r#" - @@ -45,7 +47,7 @@ async fn test_client() { .map(|cal| println!(" {}\t{}", cal.name(), cal.url().as_str())) .collect::<()>(); - let _ = client.get_tasks(&calendars[3].url()).await; + let _ = client.get_tasks(&calendars[0].url()).await; } #[tokio::test] diff --git a/tests/sync.rs b/tests/sync.rs index 6886bf4..fac1423 100644 --- a/tests/sync.rs +++ b/tests/sync.rs @@ -4,10 +4,11 @@ use chrono::{Utc, TimeZone}; use url::Url; use my_tasks::traits::CalDavSource; +use my_tasks::traits::{PartialCalendar, CompleteCalendar}; use my_tasks::cache::Cache; use my_tasks::Item; use my_tasks::Task; -use my_tasks::Calendar; +use my_tasks::calendar::cached_calendar::CachedCalendar; use my_tasks::Provider; #[tokio::test] @@ -44,7 +45,7 @@ async fn test_sync() { /// * X': name has been modified since the last sync /// * F'/F'': name conflict /// * G✓: task has been marked as completed -async fn populate_test_provider() -> Provider { +async fn populate_test_provider() -> Provider { let mut server = Cache::new(&PathBuf::from(String::from("server.json"))); let mut local = Cache::new(&PathBuf::from(String::from("local.json"))); @@ -80,7 +81,7 @@ async fn populate_test_provider() -> Provider { // Step 1 // Build the calendar as it was at the time of the sync - let mut calendar = Calendar::new("a list".into(), Url::parse("http://todo.list/cal").unwrap(), my_tasks::calendar::SupportedComponents::TODO); + let mut calendar = CachedCalendar::new("a list".into(), Url::parse("http://todo.list/cal").unwrap(), my_tasks::calendar::SupportedComponents::TODO); calendar.add_item(task_a); calendar.add_item(task_b); calendar.add_item(task_c);