diff --git a/examples/provider-sync.rs b/examples/provider-sync.rs index 06b2efe..4e6cd3c 100644 --- a/examples/provider-sync.rs +++ b/examples/provider-sync.rs @@ -1,45 +1,38 @@ //! This is an example of how kitchen-fridge can be used -use chrono::Utc; +use chrono::{Utc}; use url::Url; -use kitchen_fridge::calendar::SupportedComponents; -use kitchen_fridge::task::CompletionStatus; -use kitchen_fridge::traits::BaseCalendar; use kitchen_fridge::traits::CalDavSource; -use kitchen_fridge::traits::CompleteCalendar; -use kitchen_fridge::utils::pause; -use kitchen_fridge::CalDavProvider; +use kitchen_fridge::calendar::SupportedComponents; use kitchen_fridge::Item; use kitchen_fridge::Task; +use kitchen_fridge::task::CompletionStatus; +use kitchen_fridge::CalDavProvider; +use kitchen_fridge::traits::BaseCalendar; +use kitchen_fridge::traits::CompleteCalendar; +use kitchen_fridge::utils::pause; mod shared; use shared::initial_sync; -use shared::{EXAMPLE_CREATED_CALENDAR_URL, EXAMPLE_EXISTING_CALENDAR_URL, URL, USERNAME}; +use shared::{URL, USERNAME, EXAMPLE_EXISTING_CALENDAR_URL, EXAMPLE_CREATED_CALENDAR_URL}; const CACHE_FOLDER: &str = "test_cache/provider_sync"; + #[tokio::main] async fn main() { env_logger::init(); println!("This example show how to sync a remote server with a local cache, using a Provider."); println!("Make sure you have edited the constants in the 'shared.rs' file to include correct URLs and credentials."); - println!( - "You can also set the RUST_LOG environment variable to display more info about the sync." - ); - println!(); + println!("You can also set the RUST_LOG environment variable to display more info about the sync."); + println!(""); println!("This will use the following settings:"); println!(" * URL = {}", URL); println!(" * USERNAME = {}", USERNAME); - println!( - " * EXAMPLE_EXISTING_CALENDAR_URL = {}", - EXAMPLE_EXISTING_CALENDAR_URL - ); - println!( - " * EXAMPLE_CREATED_CALENDAR_URL = {}", - EXAMPLE_CREATED_CALENDAR_URL - ); + println!(" * EXAMPLE_EXISTING_CALENDAR_URL = {}", EXAMPLE_EXISTING_CALENDAR_URL); + println!(" * EXAMPLE_CREATED_CALENDAR_URL = {}", EXAMPLE_CREATED_CALENDAR_URL); pause(); let mut provider = initial_sync(CACHE_FOLDER).await; @@ -54,56 +47,32 @@ async fn add_items_and_sync_again(provider: &mut CalDavProvider) { // Create a new calendar... let new_calendar_url: Url = EXAMPLE_CREATED_CALENDAR_URL.parse().unwrap(); let new_calendar_name = "A brave new calendar".to_string(); - if let Err(_err) = provider - .local_mut() - .create_calendar( - new_calendar_url.clone(), - new_calendar_name.clone(), - SupportedComponents::TODO, - Some("#ff8000".parse().unwrap()), - ) - .await - { - println!("Unable to add calendar, maybe it exists already. We're not adding it after all."); + if let Err(_err) = provider.local_mut() + .create_calendar(new_calendar_url.clone(), new_calendar_name.clone(), SupportedComponents::TODO, Some("#ff8000".parse().unwrap())) + .await { + println!("Unable to add calendar, maybe it exists already. We're not adding it after all."); } // ...and add a task in it let new_name = "This is a new task in a new calendar"; let new_task = Task::new(String::from(new_name), true, &new_calendar_url); - provider - .local() - .get_calendar(&new_calendar_url) - .await - .unwrap() - .lock() - .unwrap() - .add_item(Item::Task(new_task)) - .await - .unwrap(); + provider.local().get_calendar(&new_calendar_url).await.unwrap() + .lock().unwrap().add_item(Item::Task(new_task)).await.unwrap(); + // Also create a task in a previously existing calendar let changed_calendar_url: Url = EXAMPLE_EXISTING_CALENDAR_URL.parse().unwrap(); let new_task_name = "This is a new task we're adding as an example, with ÜTF-8 characters"; let new_task = Task::new(String::from(new_task_name), false, &changed_calendar_url); let new_url = new_task.url().clone(); - provider - .local() - .get_calendar(&changed_calendar_url) - .await - .unwrap() - .lock() - .unwrap() - .add_item(Item::Task(new_task)) - .await - .unwrap(); + provider.local().get_calendar(&changed_calendar_url).await.unwrap() + .lock().unwrap().add_item(Item::Task(new_task)).await.unwrap(); - if !(provider.sync().await) { + + if provider.sync().await == false { log::warn!("Sync did not complete, see the previous log lines for more info. You can safely start a new sync. The new task may not have been synced."); } else { - println!( - "Done syncing the new task '{}' and the new calendar '{}'", - new_task_name, new_calendar_name - ); + println!("Done syncing the new task '{}' and the new calendar '{}'", new_task_name, new_calendar_name); } provider.local().save_to_folder().unwrap(); @@ -113,26 +82,18 @@ async fn add_items_and_sync_again(provider: &mut CalDavProvider) { async fn complete_item_and_sync_again( provider: &mut CalDavProvider, changed_calendar_url: &Url, - url_to_complete: &Url, -) { + url_to_complete: &Url) +{ println!("\nNow, we'll mark this last task as completed, and run the sync again."); pause(); let completion_status = CompletionStatus::Completed(Some(Utc::now())); - provider - .local() - .get_calendar(changed_calendar_url) - .await - .unwrap() - .lock() - .unwrap() - .get_item_by_url_mut(url_to_complete) - .await - .unwrap() + provider.local().get_calendar(changed_calendar_url).await.unwrap() + .lock().unwrap().get_item_by_url_mut(url_to_complete).await.unwrap() .unwrap_task_mut() .set_completion_status(completion_status); - if !(provider.sync().await) { + if provider.sync().await == false { log::warn!("Sync did not complete, see the previous log lines for more info. You can safely start a new sync. The new task may not have been synced."); } else { println!("Done syncing the completed task"); @@ -145,24 +106,17 @@ async fn complete_item_and_sync_again( async fn remove_items_and_sync_again( provider: &mut CalDavProvider, changed_calendar_url: &Url, - id_to_remove: &Url, -) { + id_to_remove: &Url) +{ println!("\nNow, we'll delete this last task, and run the sync again."); pause(); // Remove the task we had created - provider - .local() - .get_calendar(changed_calendar_url) - .await - .unwrap() - .lock() - .unwrap() - .mark_for_deletion(id_to_remove) - .await - .unwrap(); + provider.local().get_calendar(changed_calendar_url).await.unwrap() + .lock().unwrap() + .mark_for_deletion(id_to_remove).await.unwrap(); - if !(provider.sync().await) { + if provider.sync().await == false { log::warn!("Sync did not complete, see the previous log lines for more info. You can safely start a new sync. The new task may not have been synced."); } else { println!("Done syncing the deleted task"); diff --git a/examples/shared.rs b/examples/shared.rs index 74aa2d8..3cb7f2e 100644 --- a/examples/shared.rs +++ b/examples/shared.rs @@ -1,48 +1,47 @@ use std::path::Path; -use kitchen_fridge::cache::Cache; use kitchen_fridge::client::Client; use kitchen_fridge::traits::CalDavSource; use kitchen_fridge::CalDavProvider; +use kitchen_fridge::cache::Cache; + // TODO: change these values with yours pub const URL: &str = "https://my.server.com/remote.php/dav/files/john"; pub const USERNAME: &str = "username"; pub const PASSWORD: &str = "secret_password"; -pub const EXAMPLE_EXISTING_CALENDAR_URL: &str = - "https://my.server.com/remote.php/dav/calendars/john/a_calendar_name/"; -pub const EXAMPLE_CREATED_CALENDAR_URL: &str = - "https://my.server.com/remote.php/dav/calendars/john/a_calendar_that_we_have_created/"; +pub const EXAMPLE_EXISTING_CALENDAR_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/a_calendar_name/"; +pub const EXAMPLE_CREATED_CALENDAR_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/a_calendar_that_we_have_created/"; fn main() { panic!("This file is not supposed to be executed"); } + /// Initializes a Provider, and run an initial sync from the server pub async fn initial_sync(cache_folder: &str) -> CalDavProvider { let cache_path = Path::new(cache_folder); let client = Client::new(URL, USERNAME, PASSWORD).unwrap(); - let cache = match Cache::from_folder(cache_path) { + let cache = match Cache::from_folder(&cache_path) { Ok(cache) => cache, Err(err) => { log::warn!("Invalid cache file: {}. Using a default cache", err); - Cache::new(cache_path) + Cache::new(&cache_path) } }; let mut provider = CalDavProvider::new(client, cache); + let cals = provider.local().get_calendars().await.unwrap(); println!("---- Local items, before sync -----"); kitchen_fridge::utils::print_calendar_list(&cals).await; println!("Starting a sync..."); - println!( - "Depending on your RUST_LOG value, you may see more or less details about the progress." - ); + println!("Depending on your RUST_LOG value, you may see more or less details about the progress."); // Note that we could use sync_with_feedback() to have better and formatted feedback - if !(provider.sync().await) { + if provider.sync().await == false { log::warn!("Sync did not complete, see the previous log lines for more info. You can safely start a new sync."); } provider.local().save_to_folder().unwrap(); diff --git a/examples/toggle-completions.rs b/examples/toggle-completions.rs index 1f042cc..592f608 100644 --- a/examples/toggle-completions.rs +++ b/examples/toggle-completions.rs @@ -23,7 +23,7 @@ async fn main() { println!("This example show how to sync a remote server with a local cache, using a Provider."); println!("Make sure you have edited the constants in the 'shared.rs' file to include correct URLs and credentials."); println!("You can also set the RUST_LOG environment variable to display more info about the sync."); - println!(); + println!(""); println!("This will use the following settings:"); println!(" * URL = {}", URL); println!(" * USERNAME = {}", USERNAME); diff --git a/src/cache.rs b/src/cache.rs index 5e2c298..a398d6d 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,22 +1,22 @@ //! This module provides a local cache for CalDAV data -use std::collections::HashMap; -use std::error::Error; -use std::ffi::OsStr; -use std::path::Path; use std::path::PathBuf; +use std::path::Path; +use std::error::Error; +use std::collections::HashMap; use std::sync::{Arc, Mutex}; +use std::ffi::OsStr; +use serde::{Deserialize, Serialize}; use async_trait::async_trait; use csscolorparser::Color; -use serde::{Deserialize, Serialize}; use url::Url; +use crate::traits::CalDavSource; +use crate::traits::BaseCalendar; +use crate::traits::CompleteCalendar; use crate::calendar::cached_calendar::CachedCalendar; use crate::calendar::SupportedComponents; -use crate::traits::BaseCalendar; -use crate::traits::CalDavSource; -use crate::traits::CompleteCalendar; #[cfg(feature = "local_calendar_mocks_remote_calendars")] use crate::mock_behaviour::MockBehaviour; @@ -52,9 +52,10 @@ impl Cache { self.mock_behaviour = mock_behaviour; } + /// Get the path to the cache folder pub fn cache_folder() -> PathBuf { - PathBuf::from(String::from("~/.config/my-tasks/cache/")) + return PathBuf::from(String::from("~/.config/my-tasks/cache/")) } /// Initialize a cache from the content of a valid backing folder if it exists. @@ -65,7 +66,7 @@ impl Cache { let mut data: CachedData = match std::fs::File::open(&main_file) { Err(err) => { return Err(format!("Unable to open file {:?}: {}", main_file, err).into()); - } + }, Ok(file) => serde_json::from_reader(file)?, }; @@ -75,30 +76,25 @@ impl Cache { Err(err) => { log::error!("Unable to read dir: {:?}", err); continue; - } + }, Ok(entry) => { let cal_path = entry.path(); log::debug!("Considering {:?}", cal_path); if cal_path.extension() == Some(OsStr::new("cal")) { match Self::load_calendar(&cal_path) { Err(err) => { - log::error!( - "Unable to load calendar {:?} from cache: {:?}", - cal_path, - err - ); + log::error!("Unable to load calendar {:?} from cache: {:?}", cal_path, err); continue; - } - Ok(cal) => data - .calendars - .insert(cal.url().clone(), Arc::new(Mutex::new(cal))), + }, + Ok(cal) => + data.calendars.insert(cal.url().clone(), Arc::new(Mutex::new(cal))), }; } - } + }, } } - Ok(Self { + Ok(Self{ backing_folder: PathBuf::from(folder), data, @@ -108,13 +104,13 @@ impl Cache { } fn load_calendar(path: &Path) -> Result> { - let file = std::fs::File::open(path)?; + let file = std::fs::File::open(&path)?; Ok(serde_json::from_reader(file)?) } /// Initialize a cache with the default contents pub fn new(folder_path: &Path) -> Self { - Self { + Self{ backing_folder: PathBuf::from(folder_path), data: CachedData::default(), @@ -147,18 +143,16 @@ impl Cache { Ok(()) } + /// Compares two Caches to check they have the same current content /// /// This is not a complete equality test: some attributes (sync status...) may differ. This should mostly be used in tests #[cfg(any(test, feature = "integration_tests"))] - pub async fn has_same_observable_content_as( - &self, - other: &Self, - ) -> Result> { + pub async fn has_same_observable_content_as(&self, other: &Self) -> Result> { let calendars_l = self.get_calendars().await?; let calendars_r = other.get_calendars().await?; - if !crate::utils::keys_are_the_same(&calendars_l, &calendars_r) { + if crate::utils::keys_are_the_same(&calendars_l, &calendars_r) == false { log::debug!("Different keys for calendars"); return Ok(false); } @@ -172,10 +166,11 @@ impl Cache { }; // TODO: check calendars have the same names/ID/whatever - if !(cal_l.has_same_observable_content_as(&cal_r).await?) { + if cal_l.has_same_observable_content_as(&cal_r).await? == false { log::debug!("Different calendars"); - return Ok(false); + return Ok(false) } + } Ok(true) } @@ -184,43 +179,32 @@ impl Cache { impl Drop for Cache { fn drop(&mut self) { if let Err(err) = self.save_to_folder() { - log::error!( - "Unable to automatically save the cache when it's no longer required: {}", - err - ); + log::error!("Unable to automatically save the cache when it's no longer required: {}", err); } } } impl Cache { /// The non-async version of [`crate::traits::CalDavSource::get_calendars`] - pub fn get_calendars_sync( - &self, - ) -> Result>>, Box> { + pub fn get_calendars_sync(&self) -> Result>>, Box> { #[cfg(feature = "local_calendar_mocks_remote_calendars")] - self.mock_behaviour - .as_ref() - .map_or(Ok(()), |b| b.lock().unwrap().can_get_calendars())?; + self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_calendars())?; - Ok(self - .data - .calendars - .iter() + Ok(self.data.calendars.iter() .map(|(url, cal)| (url.clone(), cal.clone())) - .collect()) + .collect() + ) } /// The non-async version of [`crate::traits::CalDavSource::get_calendar`] pub fn get_calendar_sync(&self, url: &Url) -> Option>> { - self.data.calendars.get(url).cloned() + self.data.calendars.get(url).map(|arc| arc.clone()) } } #[async_trait] impl CalDavSource for Cache { - async fn get_calendars( - &self, - ) -> Result>>, Box> { + async fn get_calendars(&self) -> Result>>, Box> { self.get_calendars_sync() } @@ -228,33 +212,21 @@ impl CalDavSource for Cache { self.get_calendar_sync(url) } - async fn create_calendar( - &mut self, - url: Url, - name: String, - supported_components: SupportedComponents, - color: Option, - ) -> Result>, Box> { + async fn create_calendar(&mut self, url: Url, name: String, supported_components: SupportedComponents, color: Option) -> Result>, Box> { log::debug!("Inserting local calendar {}", url); #[cfg(feature = "local_calendar_mocks_remote_calendars")] - self.mock_behaviour - .as_ref() - .map_or(Ok(()), |b| b.lock().unwrap().can_create_calendar())?; + self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_create_calendar())?; let new_calendar = CachedCalendar::new(name, url.clone(), supported_components, color); let arc = Arc::new(Mutex::new(new_calendar)); #[cfg(feature = "local_calendar_mocks_remote_calendars")] if let Some(behaviour) = &self.mock_behaviour { - arc.lock() - .unwrap() - .set_mock_behaviour(Some(Arc::clone(behaviour))); + arc.lock().unwrap().set_mock_behaviour(Some(Arc::clone(behaviour))); }; match self.data.calendars.insert(url, arc.clone()) { - Some(_) => { - Err("Attempt to insert calendar failed: there is alredy such a calendar.".into()) - } + Some(_) => Err("Attempt to insert calendar failed: there is alredy such a calendar.".into()), None => Ok(arc), } } @@ -264,54 +236,38 @@ impl CalDavSource for Cache { mod tests { use super::*; + use url::Url; use crate::calendar::SupportedComponents; use crate::item::Item; use crate::task::Task; - use url::Url; async fn populate_cache(cache_path: &Path) -> Cache { - let mut cache = Cache::new(cache_path); + let mut cache = Cache::new(&cache_path); - let _shopping_list = cache - .create_calendar( - Url::parse("https://caldav.com/shopping").unwrap(), - "My shopping list".to_string(), - SupportedComponents::TODO, - Some(csscolorparser::parse("lime").unwrap()), - ) - .await - .unwrap(); + let _shopping_list = cache.create_calendar( + Url::parse("https://caldav.com/shopping").unwrap(), + "My shopping list".to_string(), + SupportedComponents::TODO, + Some(csscolorparser::parse("lime").unwrap()), + ).await.unwrap(); - let bucket_list = cache - .create_calendar( - Url::parse("https://caldav.com/bucket-list").unwrap(), - "My bucket list".to_string(), - SupportedComponents::TODO, - Some(csscolorparser::parse("#ff8000").unwrap()), - ) - .await - .unwrap(); + let bucket_list = cache.create_calendar( + Url::parse("https://caldav.com/bucket-list").unwrap(), + "My bucket list".to_string(), + SupportedComponents::TODO, + Some(csscolorparser::parse("#ff8000").unwrap()), + ).await.unwrap(); { let mut bucket_list = bucket_list.lock().unwrap(); let cal_url = bucket_list.url().clone(); - bucket_list - .add_item(Item::Task(Task::new( - String::from("Attend a concert of JS Bach"), - false, - &cal_url, - ))) - .await - .unwrap(); + bucket_list.add_item(Item::Task(Task::new( + String::from("Attend a concert of JS Bach"), false, &cal_url + ))).await.unwrap(); - bucket_list - .add_item(Item::Task(Task::new( - String::from("Climb the Lighthouse of Alexandria"), - true, - &cal_url, - ))) - .await - .unwrap(); + bucket_list.add_item(Item::Task(Task::new( + String::from("Climb the Lighthouse of Alexandria"), true, &cal_url + ))).await.unwrap(); } cache @@ -329,7 +285,7 @@ mod tests { assert_eq!(cache.backing_folder, retrieved_cache.backing_folder); let test = cache.has_same_observable_content_as(&retrieved_cache).await; println!("Equal? {:?}", test); - assert!(test.unwrap(), "{}", true); + assert_eq!(test.unwrap(), true); } #[tokio::test] @@ -339,14 +295,12 @@ mod tests { let mut cache = populate_cache(&cache_path).await; // We should not be able to add a second calendar with the same URL - let second_addition_same_calendar = cache - .create_calendar( - Url::parse("https://caldav.com/shopping").unwrap(), - "My shopping list".to_string(), - SupportedComponents::TODO, - None, - ) - .await; + let second_addition_same_calendar = cache.create_calendar( + Url::parse("https://caldav.com/shopping").unwrap(), + "My shopping list".to_string(), + SupportedComponents::TODO, + None, + ).await; assert!(second_addition_same_calendar.is_err()); } } diff --git a/src/calendar/cached_calendar.rs b/src/calendar/cached_calendar.rs index 0254e03..6d665c6 100644 --- a/src/calendar/cached_calendar.rs +++ b/src/calendar/cached_calendar.rs @@ -1,20 +1,21 @@ use std::collections::{HashMap, HashSet}; use std::error::Error; +use serde::{Deserialize, Serialize}; use async_trait::async_trait; use csscolorparser::Color; -use serde::{Deserialize, Serialize}; use url::Url; -use crate::calendar::SupportedComponents; use crate::item::SyncStatus; use crate::traits::{BaseCalendar, CompleteCalendar}; +use crate::calendar::SupportedComponents; use crate::Item; -#[cfg(feature = "local_calendar_mocks_remote_calendars")] -use crate::mock_behaviour::MockBehaviour; #[cfg(feature = "local_calendar_mocks_remote_calendars")] use std::sync::{Arc, Mutex}; +#[cfg(feature = "local_calendar_mocks_remote_calendars")] +use crate::mock_behaviour::MockBehaviour; + /// A calendar used by the [`cache`](crate::cache) module /// @@ -40,12 +41,11 @@ impl CachedCalendar { self.mock_behaviour = mock_behaviour; } + #[cfg(feature = "local_calendar_mocks_remote_calendars")] fn add_item_maybe_mocked(&mut self, item: Item) -> Result> { if self.mock_behaviour.is_some() { - self.mock_behaviour - .as_ref() - .map_or(Ok(()), |b| b.lock().unwrap().can_add_item())?; + self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_add_item())?; self.add_or_update_item_force_synced(item) } else { self.regular_add_or_update_item(item) @@ -55,9 +55,7 @@ impl CachedCalendar { #[cfg(feature = "local_calendar_mocks_remote_calendars")] fn update_item_maybe_mocked(&mut self, item: Item) -> Result> { if self.mock_behaviour.is_some() { - self.mock_behaviour - .as_ref() - .map_or(Ok(()), |b| b.lock().unwrap().can_update_item())?; + self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_update_item())?; self.add_or_update_item_force_synced(item) } else { self.regular_add_or_update_item(item) @@ -74,10 +72,7 @@ impl CachedCalendar { /// Add or update an item, but force a "synced" SyncStatus. This is the normal behaviour that would happen on a server #[cfg(feature = "local_calendar_mocks_remote_calendars")] - fn add_or_update_item_force_synced( - &mut self, - mut item: Item, - ) -> Result> { + fn add_or_update_item_force_synced(&mut self, mut item: Item) -> Result> { log::debug!("Adding or updating an item, but forces a synced SyncStatus"); match item.sync_status() { SyncStatus::Synced(_) => (), @@ -90,23 +85,21 @@ impl CachedCalendar { /// Some kind of equality check #[cfg(any(test, feature = "integration_tests"))] - pub async fn has_same_observable_content_as( - &self, - other: &CachedCalendar, - ) -> Result> { + pub async fn has_same_observable_content_as(&self, other: &CachedCalendar) -> Result> { if self.name != other.name - || self.url != other.url - || self.supported_components != other.supported_components - || self.color != other.color + || self.url != other.url + || self.supported_components != other.supported_components + || self.color != other.color { log::debug!("Calendar properties mismatch"); return Ok(false); } + let items_l = self.get_items().await?; let items_r = other.get_items().await?; - if !crate::utils::keys_are_the_same(&items_l, &items_r) { + if crate::utils::keys_are_the_same(&items_l, &items_r) == false { log::debug!("Different keys for items"); return Ok(false); } @@ -115,7 +108,7 @@ impl CachedCalendar { Some(c) => c, None => return Err("should not happen, we've just tested keys are the same".into()), }; - if !item_l.has_same_observable_content_as(item_r) { + if item_l.has_same_observable_content_as(&item_r) == false { log::debug!("Different items for URL {}:", url_l); log::debug!("{:#?}", item_l); log::debug!("{:#?}", item_r); @@ -128,25 +121,26 @@ impl CachedCalendar { /// The non-async version of [`Self::get_item_urls`] pub fn get_item_urls_sync(&self) -> Result, Box> { - Ok(self.items.keys().cloned().collect()) + Ok(self.items.iter() + .map(|(url, _)| url.clone()) + .collect() + ) } /// The non-async version of [`Self::get_items`] pub fn get_items_sync(&self) -> Result, Box> { - Ok(self - .items - .iter() + Ok(self.items.iter() .map(|(url, item)| (url.clone(), item)) - .collect()) + .collect() + ) } /// The non-async version of [`Self::get_items_mut`] pub fn get_items_mut_sync(&mut self) -> Result, Box> { - Ok(self - .items - .iter_mut() + Ok(self.items.iter_mut() .map(|(url, item)| (url.clone(), item)) - .collect()) + .collect() + ) } /// The non-async version of [`Self::get_item_by_url`] @@ -173,12 +167,8 @@ impl CachedCalendar { /// The non-async version of [`Self::update_item`] pub fn update_item_sync(&mut self, item: Item) -> Result> { - if !self.items.contains_key(item.url()) { - return Err(format!( - "Item {:?} cannot be updated, it does not already exist", - item.url() - ) - .into()); + if self.items.contains_key(item.url()) == false { + return Err(format!("Item {:?} cannot be updated, it does not already exist", item.url()).into()); } #[cfg(not(feature = "local_calendar_mocks_remote_calendars"))] return self.regular_add_or_update_item(item); @@ -195,20 +185,20 @@ impl CachedCalendar { match item.sync_status() { SyncStatus::Synced(prev_ss) => { let prev_ss = prev_ss.clone(); - item.set_sync_status(SyncStatus::LocallyDeleted(prev_ss)); - } + item.set_sync_status( SyncStatus::LocallyDeleted(prev_ss)); + }, SyncStatus::LocallyModified(prev_ss) => { let prev_ss = prev_ss.clone(); - item.set_sync_status(SyncStatus::LocallyDeleted(prev_ss)); - } + item.set_sync_status( SyncStatus::LocallyDeleted(prev_ss)); + }, SyncStatus::LocallyDeleted(prev_ss) => { let prev_ss = prev_ss.clone(); - item.set_sync_status(SyncStatus::LocallyDeleted(prev_ss)); - } + item.set_sync_status( SyncStatus::LocallyDeleted(prev_ss)); + }, SyncStatus::NotSynced => { // This was never synced to the server, we can safely delete it as soon as now self.items.remove(item_url); - } + }, }; Ok(()) } @@ -219,11 +209,13 @@ impl CachedCalendar { pub fn immediately_delete_item_sync(&mut self, item_url: &Url) -> Result<(), Box> { match self.items.remove(item_url) { None => Err(format!("Item {} is absent from this calendar", item_url).into()), - Some(_) => Ok(()), + Some(_) => Ok(()) } } + } + #[async_trait] impl BaseCalendar for CachedCalendar { fn name(&self) -> &str { @@ -253,17 +245,9 @@ impl BaseCalendar for CachedCalendar { #[async_trait] impl CompleteCalendar for CachedCalendar { - fn new( - name: String, - url: Url, - supported_components: SupportedComponents, - color: Option, - ) -> Self { + fn new(name: String, url: Url, supported_components: SupportedComponents, color: Option) -> Self { Self { - name, - url, - supported_components, - color, + name, url, supported_components, color, #[cfg(feature = "local_calendar_mocks_remote_calendars")] mock_behaviour: None, items: HashMap::new(), @@ -299,33 +283,25 @@ impl CompleteCalendar for CachedCalendar { } } + + // This class can be used to mock a remote calendar for integration tests #[cfg(feature = "local_calendar_mocks_remote_calendars")] -use crate::{item::VersionTag, resource::Resource, traits::DavCalendar}; +use crate::{item::VersionTag, + traits::DavCalendar, + resource::Resource}; #[cfg(feature = "local_calendar_mocks_remote_calendars")] #[async_trait] impl DavCalendar for CachedCalendar { - fn new( - name: String, - resource: Resource, - supported_components: SupportedComponents, - color: Option, - ) -> Self { - crate::traits::CompleteCalendar::new( - name, - resource.url().clone(), - supported_components, - color, - ) + fn new(name: String, resource: Resource, supported_components: SupportedComponents, color: Option) -> Self { + crate::traits::CompleteCalendar::new(name, resource.url().clone(), supported_components, color) } async fn get_item_version_tags(&self) -> Result, Box> { #[cfg(feature = "local_calendar_mocks_remote_calendars")] - self.mock_behaviour - .as_ref() - .map_or(Ok(()), |b| b.lock().unwrap().can_get_item_version_tags())?; + self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_item_version_tags())?; use crate::item::SyncStatus; @@ -335,10 +311,7 @@ impl DavCalendar for CachedCalendar { let vt = match item.sync_status() { SyncStatus::Synced(vt) => vt.clone(), _ => { - panic!( - "Mock calendars must contain only SyncStatus::Synced. Got {:?}", - item - ); + panic!("Mock calendars must contain only SyncStatus::Synced. Got {:?}", item); } }; result.insert(url.clone(), vt); @@ -349,9 +322,7 @@ impl DavCalendar for CachedCalendar { async fn get_item_by_url(&self, url: &Url) -> Result, Box> { #[cfg(feature = "local_calendar_mocks_remote_calendars")] - self.mock_behaviour - .as_ref() - .map_or(Ok(()), |b| b.lock().unwrap().can_get_item_by_url())?; + self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_item_by_url())?; Ok(self.items.get(url).cloned()) } @@ -366,9 +337,7 @@ impl DavCalendar for CachedCalendar { async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box> { #[cfg(feature = "local_calendar_mocks_remote_calendars")] - self.mock_behaviour - .as_ref() - .map_or(Ok(()), |b| b.lock().unwrap().can_delete_item())?; + self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_delete_item())?; self.immediately_delete_item(item_url).await } diff --git a/src/client.rs b/src/client.rs index 97cbb88..993bec1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,24 +1,25 @@ //! This module provides a client to connect to a CalDAV server -use std::collections::HashMap; -use std::convert::TryFrom; use std::error::Error; +use std::convert::TryFrom; +use std::collections::HashMap; use std::sync::{Arc, Mutex}; use async_trait::async_trait; -use csscolorparser::Color; -use minidom::Element; -use reqwest::header::CONTENT_TYPE; use reqwest::{Method, StatusCode}; +use reqwest::header::CONTENT_TYPE; +use minidom::Element; use url::Url; +use csscolorparser::Color; +use crate::resource::Resource; +use crate::utils::{find_elem, find_elems}; use crate::calendar::remote_calendar::RemoteCalendar; use crate::calendar::SupportedComponents; -use crate::resource::Resource; -use crate::traits::BaseCalendar; use crate::traits::CalDavSource; +use crate::traits::BaseCalendar; use crate::traits::DavCalendar; -use crate::utils::{find_elem, find_elems}; + static DAVCLIENT_BODY: &str = r#" @@ -48,13 +49,11 @@ static CAL_BODY: &str = r#" "#; -pub(crate) async fn sub_request( - resource: &Resource, - method: &str, - body: String, - depth: u32, -) -> Result> { - let method = method.parse().expect("invalid method name"); + + +pub(crate) async fn sub_request(resource: &Resource, method: &str, body: String, depth: u32) -> Result> { + let method = method.parse() + .expect("invalid method name"); let res = reqwest::Client::new() .request(method, resource.url().clone()) @@ -65,7 +64,7 @@ pub(crate) async fn sub_request( .send() .await?; - if !res.status().is_success() { + if res.status().is_success() == false { return Err(format!("Unexpected HTTP status code {:?}", res.status()).into()); } @@ -73,16 +72,12 @@ pub(crate) async fn sub_request( Ok(text) } -pub(crate) async fn sub_request_and_extract_elem( - resource: &Resource, - body: String, - items: &[&str], -) -> Result> { +pub(crate) async fn sub_request_and_extract_elem(resource: &Resource, body: String, items: &[&str]) -> Result> { let text = sub_request(resource, "PROPFIND", body, 0).await?; let mut current_element: &Element = &text.parse()?; for item in items { - current_element = match find_elem(current_element, item) { + current_element = match find_elem(¤t_element, item) { Some(elem) => elem, None => return Err(format!("missing element {}", item).into()), } @@ -90,21 +85,18 @@ pub(crate) async fn sub_request_and_extract_elem( Ok(current_element.text()) } -pub(crate) async fn sub_request_and_extract_elems( - resource: &Resource, - method: &str, - body: String, - item: &str, -) -> Result, Box> { +pub(crate) async fn sub_request_and_extract_elems(resource: &Resource, method: &str, body: String, item: &str) -> Result, Box> { let text = sub_request(resource, method, body, 1).await?; let element: &Element = &text.parse()?; - Ok(find_elems(element, item) + Ok(find_elems(&element, item) .iter() .map(|elem| (*elem).clone()) - .collect()) + .collect() + ) } + /// A CalDAV data source that fetches its data from a CalDAV server #[derive(Debug)] pub struct Client { @@ -115,6 +107,7 @@ pub struct Client { cached_replies: Mutex, } + #[derive(Debug, Default)] struct CachedReplies { principal: Option, @@ -124,14 +117,10 @@ struct CachedReplies { impl Client { /// Create a client. This does not start a connection - pub fn new, T: ToString, U: ToString>( - url: S, - username: T, - password: U, - ) -> Result> { + pub fn new, T: ToString, U: ToString>(url: S, username: T, password: U) -> Result> { let url = Url::parse(url.as_ref())?; - Ok(Self { + Ok(Self{ resource: Resource::new(url, username.to_string(), password.to_string()), cached_replies: Mutex::new(CachedReplies::default()), }) @@ -143,17 +132,12 @@ impl Client { return Ok(p.clone()); } - let href = sub_request_and_extract_elem( - &self.resource, - DAVCLIENT_BODY.into(), - &["current-user-principal", "href"], - ) - .await?; + let href = sub_request_and_extract_elem(&self.resource, DAVCLIENT_BODY.into(), &["current-user-principal", "href"]).await?; let principal_url = self.resource.combine(&href); self.cached_replies.lock().unwrap().principal = Some(principal_url.clone()); log::debug!("Principal URL is {}", href); - Ok(principal_url) + return Ok(principal_url); } /// Return the Homeset URL, or fetch it from server if not known yet @@ -163,12 +147,7 @@ impl Client { } let principal_url = self.get_principal().await?; - let href = sub_request_and_extract_elem( - &principal_url, - HOMESET_BODY.into(), - &["calendar-home-set", "href"], - ) - .await?; + let href = sub_request_and_extract_elem(&principal_url, HOMESET_BODY.into(), &["calendar-home-set", "href"]).await?; let chs_url = self.resource.combine(&href); self.cached_replies.lock().unwrap().calendar_home_set = Some(chs_url.clone()); log::debug!("Calendar home set URL is {:?}", href); @@ -179,18 +158,10 @@ impl Client { async fn populate_calendars(&self) -> Result<(), Box> { let cal_home_set = self.get_cal_home_set().await?; - let reps = sub_request_and_extract_elems( - &cal_home_set, - "PROPFIND", - CAL_BODY.to_string(), - "response", - ) - .await?; + let reps = sub_request_and_extract_elems(&cal_home_set, "PROPFIND", CAL_BODY.to_string(), "response").await?; let mut calendars = HashMap::new(); for rep in reps { - let display_name = find_elem(&rep, "displayname") - .map(|e| e.text()) - .unwrap_or("".to_string()); + let display_name = find_elem(&rep, "displayname").map(|e| e.text()).unwrap_or("".to_string()); log::debug!("Considering calendar {}", display_name); // We filter out non-calendar items @@ -205,7 +176,7 @@ impl Client { break; } } - if !found_calendar_type { + if found_calendar_type == false { continue; } @@ -222,60 +193,48 @@ impl Client { None => { log::warn!("Calendar {} has no URL! Ignoring it.", display_name); continue; - } + }, Some(h) => h.text(), }; let this_calendar_url = self.resource.combine(&calendar_href); - let supported_components = - match crate::calendar::SupportedComponents::try_from(el_supported_comps.clone()) { - Err(err) => { - log::warn!( - "Calendar {} has invalid supported components ({})! Ignoring it.", - display_name, - err - ); - continue; - } - Ok(sc) => sc, - }; + let supported_components = match crate::calendar::SupportedComponents::try_from(el_supported_comps.clone()) { + Err(err) => { + log::warn!("Calendar {} has invalid supported components ({})! Ignoring it.", display_name, err); + continue; + }, + Ok(sc) => sc, + }; - let this_calendar_color = find_elem(&rep, "calendar-color").and_then(|col| { - col.texts() - .next() - .and_then(|t| csscolorparser::parse(t).ok()) - }); + let this_calendar_color = find_elem(&rep, "calendar-color") + .and_then(|col| { + col.texts().next() + .and_then(|t| csscolorparser::parse(t).ok()) + }); - let this_calendar = RemoteCalendar::new( - display_name, - this_calendar_url, - supported_components, - this_calendar_color, - ); + let this_calendar = RemoteCalendar::new(display_name, this_calendar_url, supported_components, this_calendar_color); log::info!("Found calendar {}", this_calendar.name()); - calendars.insert( - this_calendar.url().clone(), - Arc::new(Mutex::new(this_calendar)), - ); + calendars.insert(this_calendar.url().clone(), Arc::new(Mutex::new(this_calendar))); } let mut replies = self.cached_replies.lock().unwrap(); replies.calendars = Some(calendars); Ok(()) } + } #[async_trait] impl CalDavSource for Client { - async fn get_calendars( - &self, - ) -> Result>>, Box> { + async fn get_calendars(&self) -> Result>>, Box> { self.populate_calendars().await?; match &self.cached_replies.lock().unwrap().calendars { - Some(cals) => return Ok(cals.clone()), - None => return Err("No calendars available".into()), + Some(cals) => { + return Ok(cals.clone()) + }, + None => return Err("No calendars available".into()) }; } @@ -285,22 +244,14 @@ impl CalDavSource for Client { return None; } - self.cached_replies - .lock() - .unwrap() + self.cached_replies.lock().unwrap() .calendars .as_ref() .and_then(|cals| cals.get(url)) - .cloned() + .map(|cal| cal.clone()) } - async fn create_calendar( - &mut self, - url: Url, - name: String, - supported_components: SupportedComponents, - color: Option, - ) -> Result>, Box> { + async fn create_calendar(&mut self, url: Url, name: String, supported_components: SupportedComponents, color: Option) -> Result>, Box> { self.populate_calendars().await?; match self.cached_replies.lock().unwrap().calendars.as_ref() { @@ -309,7 +260,7 @@ impl CalDavSource for Client { if cals.contains_key(&url) { return Err("This calendar already exists".into()); } - } + }, } let creation_body = calendar_body(name, supported_components, color); @@ -324,35 +275,21 @@ impl CalDavSource for Client { let status = response.status(); if status != StatusCode::CREATED { - return Err(format!( - "Unexpected HTTP status code. Expected CREATED, got {}", - status.as_u16() - ) - .into()); + return Err(format!("Unexpected HTTP status code. Expected CREATED, got {}", status.as_u16()).into()); } - self.get_calendar(&url) - .await - .ok_or(format!("Unable to insert calendar {:?}", url).into()) + self.get_calendar(&url).await.ok_or(format!("Unable to insert calendar {:?}", url).into()) } } -fn calendar_body( - name: String, - supported_components: SupportedComponents, - color: Option, -) -> String { +fn calendar_body(name: String, supported_components: SupportedComponents, color: Option) -> String { let color_property = match color { None => "".to_string(), - Some(color) => format!( - "{}FF", - color.to_hex_string().to_ascii_uppercase() - ), + Some(color) => format!("{}FF", color.to_hex_string().to_ascii_uppercase()), }; // This is taken from https://tools.ietf.org/html/rfc4791#page-24 - format!( - r#" + format!(r#" diff --git a/src/event.rs b/src/event.rs index 2d55f9a..2821242 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,7 +1,7 @@ //! Calendar events (iCal `VEVENT` items) -use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; use url::Url; use crate::item::SyncStatus; @@ -56,9 +56,3 @@ impl Event { unimplemented!(); } } - -impl Default for Event { - fn default() -> Self { - Self::new() - } -} diff --git a/src/ical/parser.rs b/src/ical/parser.rs index 8894cd9..acf408a 100644 --- a/src/ical/parser.rs +++ b/src/ical/parser.rs @@ -2,41 +2,36 @@ use std::error::Error; -use chrono::{DateTime, TimeZone, Utc}; use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo}; +use chrono::{DateTime, TimeZone, Utc}; use url::Url; +use crate::Item; use crate::item::SyncStatus; +use crate::Task; use crate::task::CompletionStatus; use crate::Event; -use crate::Item; -use crate::Task; + /// Parse an iCal file into the internal representation [`crate::Item`] -pub fn parse( - content: &str, - item_url: Url, - sync_status: SyncStatus, -) -> Result> { +pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result> { let mut reader = ical::IcalParser::new(content.as_bytes()); let parsed_item = match reader.next() { None => return Err(format!("Invalid iCal data to parse for item {}", item_url).into()), Some(item) => match item { - Err(err) => { - return Err( - format!("Unable to parse iCal data for item {}: {}", item_url, err).into(), - ) - } + Err(err) => return Err(format!("Unable to parse iCal data for item {}: {}", item_url, err).into()), Ok(item) => item, - }, + } }; let ical_prod_id = extract_ical_prod_id(&parsed_item) .map(|s| s.to_string()) - .unwrap_or_else(super::default_prod_id); + .unwrap_or_else(|| super::default_prod_id()); let item = match assert_single_type(&parsed_item)? { - CurrentType::Event(_) => Item::Event(Event::new()), + CurrentType::Event(_) => { + Item::Event(Event::new()) + }, CurrentType::Todo(todo) => { let mut name = None; @@ -49,8 +44,8 @@ pub fn parse( for prop in &todo.properties { match prop.name.as_str() { - "SUMMARY" => name = prop.value.clone(), - "UID" => uid = prop.value.clone(), + "SUMMARY" => { name = prop.value.clone() }, + "UID" => { uid = prop.value.clone() }, "DTSTAMP" => { // The property can be specified once, but is not mandatory // "This property specifies the date and time that the information associated with @@ -58,7 +53,7 @@ pub fn parse( // "In the case of an iCalendar object that doesn't specify a "METHOD" // property [e.g.: VTODO and VEVENT], this property is equivalent to the "LAST-MODIFIED" property". last_modified = parse_date_time_from_property(&prop.value); - } + }, "LAST-MODIFIED" => { // The property can be specified once, but is not mandatory // "This property specifies the date and time that the information associated with @@ -71,11 +66,11 @@ pub fn parse( // "This property defines the date and time that a to-do was // actually completed." completion_date = parse_date_time_from_property(&prop.value) - } + }, "CREATED" => { // The property can be specified once, but is not mandatory creation_date = parse_date_time_from_property(&prop.value) - } + }, "STATUS" => { // Possible values: // "NEEDS-ACTION" ;Indicates to-do needs action. @@ -102,13 +97,7 @@ pub fn parse( }; let last_modified = match last_modified { Some(dt) => dt, - None => { - return Err(format!( - "Missing DTSTAMP for item {}, but this is required by RFC5545", - item_url - ) - .into()) - } + None => return Err(format!("Missing DTSTAMP for item {}, but this is required by RFC5545", item_url).into()), }; let completion_status = match completed { false => { @@ -116,24 +105,15 @@ pub fn parse( log::warn!("Task {:?} has an inconsistent content: its STATUS is not completed, yet it has a COMPLETED timestamp at {:?}", uid, completion_date); } CompletionStatus::Uncompleted - } + }, true => CompletionStatus::Completed(completion_date), }; - Item::Task(Task::new_with_parameters( - name, - uid, - item_url, - completion_status, - sync_status, - creation_date, - last_modified, - ical_prod_id, - extra_parameters, - )) - } + Item::Task(Task::new_with_parameters(name, uid, item_url, completion_status, sync_status, creation_date, last_modified, ical_prod_id, extra_parameters)) + }, }; + // 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()); @@ -143,30 +123,33 @@ pub fn parse( } fn parse_date_time(dt: &str) -> Result, chrono::format::ParseError> { - Utc.datetime_from_str(dt, "%Y%m%dT%H%M%SZ") - .or_else(|_err| Utc.datetime_from_str(dt, "%Y%m%dT%H%M%S")) + Utc.datetime_from_str(dt, "%Y%m%dT%H%M%SZ") + .or_else(|_err| Utc.datetime_from_str(dt, "%Y%m%dT%H%M%S") ) } fn parse_date_time_from_property(value: &Option) -> Option> { - value.as_ref().and_then(|s| { - parse_date_time(s) + value.as_ref() + .and_then(|s| { + parse_date_time(s) .map_err(|err| { log::warn!("Invalid timestamp: {}", s); err }) .ok() - }) + }) } + fn extract_ical_prod_id(item: &IcalCalendar) -> Option<&str> { for prop in &item.properties { if &prop.name == "PRODID" { - return prop.value.as_deref(); + return prop.value.as_ref().map(|s| s.as_str()) } } None } + enum CurrentType<'a> { Event(&'a IcalEvent), Todo(&'a IcalTodo), @@ -193,9 +176,10 @@ fn assert_single_type<'a>(item: &'a IcalCalendar) -> Result, Box } } - Err("Only a single TODO or a single EVENT is supported".into()) + return Err("Only a single TODO or a single EVENT is supported".into()); } + #[cfg(test)] mod test { const EXAMPLE_ICAL: &str = r#"BEGIN:VCALENDAR @@ -211,7 +195,7 @@ END:VTODO END:VCALENDAR "#; - const EXAMPLE_ICAL_COMPLETED: &str = r#"BEGIN:VCALENDAR +const EXAMPLE_ICAL_COMPLETED: &str = r#"BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Nextcloud Tasks v0.13.6 BEGIN:VTODO @@ -227,7 +211,7 @@ END:VTODO END:VCALENDAR "#; - const EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE: &str = r#"BEGIN:VCALENDAR +const EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE: &str = r#"BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Nextcloud Tasks v0.13.6 BEGIN:VTODO @@ -277,17 +261,11 @@ END:VCALENDAR assert_eq!(task.name(), "Do not forget to do this"); assert_eq!(task.url(), &item_url); - assert_eq!( - task.uid(), - "0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com" - ); - assert!(!task.completed()); + assert_eq!(task.uid(), "0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com"); + assert_eq!(task.completed(), false); assert_eq!(task.completion_status(), &CompletionStatus::Uncompleted); assert_eq!(task.sync_status(), &sync_status); - assert_eq!( - task.last_modified(), - &Utc.ymd(2021, 03, 21).and_hms(0, 16, 0) - ); + assert_eq!(task.last_modified(), &Utc.ymd(2021, 03, 21).and_hms(0, 16, 0)); } #[test] @@ -296,19 +274,11 @@ END:VCALENDAR let sync_status = SyncStatus::Synced(version_tag); let item_url: Url = "http://some.id/for/testing".parse().unwrap(); - let item = parse( - EXAMPLE_ICAL_COMPLETED, - item_url.clone(), - sync_status.clone(), - ) - .unwrap(); + let item = parse(EXAMPLE_ICAL_COMPLETED, item_url.clone(), sync_status.clone()).unwrap(); let task = item.unwrap_task(); - assert!(task.completed()); - assert_eq!( - task.completion_status(), - &CompletionStatus::Completed(Some(Utc.ymd(2021, 04, 02).and_hms(8, 15, 57))) - ); + assert_eq!(task.completed(), true); + assert_eq!(task.completion_status(), &CompletionStatus::Completed(Some(Utc.ymd(2021, 04, 02).and_hms(8, 15, 57)))); } #[test] @@ -317,15 +287,10 @@ END:VCALENDAR let sync_status = SyncStatus::Synced(version_tag); let item_url: Url = "http://some.id/for/testing".parse().unwrap(); - let item = parse( - EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE, - item_url.clone(), - sync_status.clone(), - ) - .unwrap(); + let item = parse(EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE, item_url.clone(), sync_status.clone()).unwrap(); let task = item.unwrap_task(); - assert!(task.completed()); + assert_eq!(task.completed(), true); assert_eq!(task.completion_status(), &CompletionStatus::Completed(None)); } diff --git a/src/item.rs b/src/item.rs index 18348f5..8d457d0 100644 --- a/src/item.rs +++ b/src/item.rs @@ -1,9 +1,10 @@ //! CalDAV items (todo, events, journals...) // TODO: move Event and Task to nest them in crate::items::calendar::Calendar? -use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use url::Url; +use chrono::{DateTime, Utc}; + #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Item { @@ -20,7 +21,7 @@ macro_rules! synthetise_common_getter { Item::Task(t) => t.$property_name(), } } - }; + } } impl Item { @@ -40,11 +41,17 @@ impl Item { } pub fn is_event(&self) -> bool { - matches!(self, Item::Event(_)) + match &self { + Item::Event(_) => true, + _ => false, + } } pub fn is_task(&self) -> bool { - matches!(self, Item::Task(_)) + match &self { + Item::Task(_) => true, + _ => false, + } } /// Returns a mutable reference to the inner Task @@ -73,16 +80,19 @@ impl Item { pub fn has_same_observable_content_as(&self, other: &Item) -> bool { match (self, other) { (Item::Event(s), Item::Event(o)) => s.has_same_observable_content_as(o), - (Item::Task(s), Item::Task(o)) => s.has_same_observable_content_as(o), + (Item::Task(s), Item::Task(o)) => s.has_same_observable_content_as(o), _ => false, } } } + + + /// A VersionTag is basically a CalDAV `ctag` or `etag`. Whenever it changes, this means the data has changed. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct VersionTag { - tag: String, + tag: String } impl From for VersionTag { @@ -105,6 +115,8 @@ impl VersionTag { } } + + /// Describes whether this item has been synced already, or modified since the last time it was synced #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum SyncStatus { diff --git a/src/mock_behaviour.rs b/src/mock_behaviour.rs index 070436a..c5c8bd1 100644 --- a/src/mock_behaviour.rs +++ b/src/mock_behaviour.rs @@ -61,9 +61,7 @@ impl MockBehaviour { } pub fn can_get_calendars(&mut self) -> Result<(), Box> { - if self.is_suspended { - return Ok(()); - } + if self.is_suspended { return Ok(()) } decrement(&mut self.get_calendars_behaviour, "get_calendars") } // pub fn can_get_calendar(&mut self) -> Result<(), Box> { @@ -71,66 +69,50 @@ impl MockBehaviour { // decrement(&mut self.get_calendar_behaviour, "get_calendar") // } pub fn can_create_calendar(&mut self) -> Result<(), Box> { - if self.is_suspended { - return Ok(()); - } + if self.is_suspended { return Ok(()) } decrement(&mut self.create_calendar_behaviour, "create_calendar") } pub fn can_add_item(&mut self) -> Result<(), Box> { - if self.is_suspended { - return Ok(()); - } + if self.is_suspended { return Ok(()) } decrement(&mut self.add_item_behaviour, "add_item") } pub fn can_update_item(&mut self) -> Result<(), Box> { - if self.is_suspended { - return Ok(()); - } + if self.is_suspended { return Ok(()) } decrement(&mut self.update_item_behaviour, "update_item") } pub fn can_get_item_version_tags(&mut self) -> Result<(), Box> { - if self.is_suspended { - return Ok(()); - } - decrement( - &mut self.get_item_version_tags_behaviour, - "get_item_version_tags", - ) + if self.is_suspended { return Ok(()) } + decrement(&mut self.get_item_version_tags_behaviour, "get_item_version_tags") } pub fn can_get_item_by_url(&mut self) -> Result<(), Box> { - if self.is_suspended { - return Ok(()); - } + if self.is_suspended { return Ok(()) } decrement(&mut self.get_item_by_url_behaviour, "get_item_by_url") } pub fn can_delete_item(&mut self) -> Result<(), Box> { - if self.is_suspended { - return Ok(()); - } + if self.is_suspended { return Ok(()) } decrement(&mut self.delete_item_behaviour, "delete_item") } } + /// Return Ok(()) in case the value is `(1+, _)` or `(_, 0)`, or return Err and decrement otherwise fn decrement(value: &mut (u32, u32), descr: &str) -> Result<(), Box> { let remaining_successes = value.0; let remaining_failures = value.1; if remaining_successes > 0 { - value.0 -= 1; + value.0 = value.0 - 1; log::debug!("Mock behaviour: allowing a {} ({:?})", descr, value); Ok(()) - } else if remaining_failures > 0 { - value.1 -= 1; - log::debug!("Mock behaviour: failing a {} ({:?})", descr, value); - Err(format!( - "Mocked behaviour requires this {} to fail this time. ({:?})", - descr, value - ) - .into()) } else { - log::debug!("Mock behaviour: allowing a {} ({:?})", descr, value); - Ok(()) + if remaining_failures > 0 { + value.1 = value.1 - 1; + log::debug!("Mock behaviour: failing a {} ({:?})", descr, value); + Err(format!("Mocked behaviour requires this {} to fail this time. ({:?})", descr, value).into()) + } else { + log::debug!("Mock behaviour: allowing a {} ({:?})", descr, value); + Ok(()) + } } } @@ -158,9 +140,9 @@ mod test { assert!(now.can_get_calendars().is_ok()); assert!(now.can_create_calendar().is_ok()); - let mut custom = MockBehaviour { - get_calendars_behaviour: (0, 1), - create_calendar_behaviour: (1, 3), + let mut custom = MockBehaviour{ + get_calendars_behaviour: (0,1), + create_calendar_behaviour: (1,3), ..MockBehaviour::default() }; assert!(custom.can_get_calendars().is_err()); diff --git a/src/provider/mod.rs b/src/provider/mod.rs index 324aac3..c73b503 100644 --- a/src/provider/mod.rs +++ b/src/provider/mod.rs @@ -2,18 +2,18 @@ //! //! It is also responsible for syncing them together -use std::collections::HashSet; use std::error::Error; -use std::fmt::{Display, Formatter}; +use std::collections::HashSet; use std::marker::PhantomData; use std::sync::{Arc, Mutex}; +use std::fmt::{Display, Formatter}; -use itertools::Itertools; use url::Url; +use itertools::Itertools; -use crate::item::SyncStatus; -use crate::traits::CompleteCalendar; use crate::traits::{BaseCalendar, CalDavSource, DavCalendar}; +use crate::traits::CompleteCalendar; +use crate::item::SyncStatus; pub mod sync_progress; use sync_progress::SyncProgress; @@ -42,6 +42,7 @@ impl Display for BatchDownloadType { } } + /// A data source that combines two `CalDavSource`s, which is able to sync both sources. /// /// Usually, you will only need to use a provider between a server and a local cache, that is to say a [`CalDavProvider`](crate::CalDavProvider), i.e. a `Provider`. \ @@ -75,30 +76,21 @@ where /// `remote` 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 `remote` always wins in case of a sync conflict pub fn new(remote: R, local: L) -> Self { - Self { - remote, - local, - phantom_t: PhantomData, - phantom_u: PhantomData, + Self { remote, local, + phantom_t: PhantomData, phantom_u: PhantomData, } } /// Returns the data source described as `local` - pub fn local(&self) -> &L { - &self.local - } + pub fn local(&self) -> &L { &self.local } /// Returns the data source described as `local` - pub fn local_mut(&mut self) -> &mut L { - &mut self.local - } + pub fn local_mut(&mut self) -> &mut L { &mut self.local } /// Returns the data source described as `remote`. /// /// Apart from tests, there are very few (if any) reasons to access `remote` directly. /// Usually, you should rather use the `local` source, which (usually) is a much faster local cache. /// To be sure `local` accurately mirrors the `remote` source, you can run [`Provider::sync`] - pub fn remote(&self) -> &R { - &self.remote - } + pub fn remote(&self) -> &R { &self.remote } /// Performs a synchronisation between `local` and `remote`, and provide feeedback to the user about the progress. /// @@ -125,9 +117,7 @@ where if let Err(err) = self.run_sync_inner(progress).await { progress.error(&format!("Sync terminated because of an error: {}", err)); } - progress.feedback(SyncEvent::Finished { - success: progress.is_success(), - }); + progress.feedback(SyncEvent::Finished{ success: progress.is_success() }); progress.is_success() } @@ -140,22 +130,16 @@ where // Sync every remote calendar let cals_remote = self.remote.get_calendars().await?; for (cal_url, cal_remote) in cals_remote { - let counterpart = match self - .get_or_insert_local_counterpart_calendar(&cal_url, cal_remote.clone()) - .await - { + let counterpart = match self.get_or_insert_local_counterpart_calendar(&cal_url, cal_remote.clone()).await { Err(err) => { progress.warn(&format!("Unable to get or insert local counterpart calendar for {} ({}). Skipping this time", cal_url, err)); continue; - } + }, Ok(arc) => arc, }; if let Err(err) = Self::sync_calendar_pair(counterpart, cal_remote, progress).await { - progress.warn(&format!( - "Unable to sync calendar {}: {}, skipping this time.", - cal_url, err - )); + progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_url, err)); continue; } handled_calendars.insert(cal_url); @@ -168,22 +152,16 @@ where continue; } - let counterpart = match self - .get_or_insert_remote_counterpart_calendar(&cal_url, cal_local.clone()) - .await - { + let counterpart = match self.get_or_insert_remote_counterpart_calendar(&cal_url, cal_local.clone()).await { Err(err) => { progress.warn(&format!("Unable to get or insert remote counterpart calendar for {} ({}). Skipping this time", cal_url, err)); continue; - } + }, Ok(arc) => arc, }; if let Err(err) = Self::sync_calendar_pair(cal_local, counterpart, progress).await { - progress.warn(&format!( - "Unable to sync calendar {}: {}, skipping this time.", - cal_url, err - )); + progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_url, err)); continue; } } @@ -193,36 +171,26 @@ where Ok(()) } - async fn get_or_insert_local_counterpart_calendar( - &mut self, - cal_url: &Url, - needle: Arc>, - ) -> Result>, Box> { + + async fn get_or_insert_local_counterpart_calendar(&mut self, cal_url: &Url, needle: Arc>) -> Result>, Box> { get_or_insert_counterpart_calendar("local", &mut self.local, cal_url, needle).await } - async fn get_or_insert_remote_counterpart_calendar( - &mut self, - cal_url: &Url, - needle: Arc>, - ) -> Result>, Box> { + async fn get_or_insert_remote_counterpart_calendar(&mut self, cal_url: &Url, needle: Arc>) -> Result>, Box> { get_or_insert_counterpart_calendar("remote", &mut self.remote, cal_url, needle).await } - async fn sync_calendar_pair( - cal_local: Arc>, - cal_remote: Arc>, - progress: &mut SyncProgress, - ) -> Result<(), Box> { + + async fn sync_calendar_pair(cal_local: Arc>, cal_remote: Arc>, progress: &mut SyncProgress) -> Result<(), Box> { let mut cal_remote = cal_remote.lock().unwrap(); let mut cal_local = cal_local.lock().unwrap(); let cal_name = cal_local.name().to_string(); progress.info(&format!("Syncing calendar {}", cal_name)); progress.reset_counter(); - progress.feedback(SyncEvent::InProgress { + progress.feedback(SyncEvent::InProgress{ calendar: cal_name.clone(), items_done_already: 0, - details: "started".to_string(), + details: "started".to_string() }); // Step 1 - find the differences @@ -235,7 +203,7 @@ where let mut remote_additions = HashSet::new(); let remote_items = cal_remote.get_item_version_tags().await?; - progress.feedback(SyncEvent::InProgress { + progress.feedback(SyncEvent::InProgress{ calendar: cal_name.clone(), items_done_already: 0, details: format!("{} remote items", remote_items.len()), @@ -249,27 +217,24 @@ where // This was created on the remote progress.debug(&format!("* {} is a remote addition", url)); remote_additions.insert(url); - } + }, Some(local_item) => { - if !local_items_to_handle.remove(&url) { - progress.error(&format!( - "Inconsistent state: missing task {} from the local tasks", - url - )); + if local_items_to_handle.remove(&url) == false { + progress.error(&format!("Inconsistent state: missing task {} from the local tasks", url)); } match local_item.sync_status() { SyncStatus::NotSynced => { progress.error(&format!("URL reuse between remote and local sources ({}). Ignoring this item in the sync", url)); continue; - } + }, SyncStatus::Synced(local_tag) => { if &remote_tag != local_tag { // This has been modified on the remote progress.debug(&format!("* {} is a remote change", url)); remote_changes.insert(url); } - } + }, SyncStatus::LocallyModified(local_tag) => { if &remote_tag == local_tag { // This has been changed locally @@ -277,11 +242,10 @@ where local_changes.insert(url); } else { progress.info(&format!("Conflict: task {} has been modified in both sources. Using the remote version.", url)); - progress - .debug(&format!("* {} is considered a remote change", url)); + progress.debug(&format!("* {} is considered a remote change", url)); remote_changes.insert(url); } - } + }, SyncStatus::LocallyDeleted(local_tag) => { if &remote_tag == local_tag { // This has been locally deleted @@ -289,11 +253,10 @@ where local_del.insert(url); } else { progress.info(&format!("Conflict: task {} has been locally deleted and remotely modified. Reverting to the remote version.", url)); - progress - .debug(&format!("* {} is a considered a remote change", url)); + progress.debug(&format!("* {} is a considered a remote change", url)); remote_changes.insert(url); } - } + }, } } } @@ -304,12 +267,9 @@ where progress.trace(&format!("##### Considering local item {}...", url)); let local_item = match cal_local.get_item_by_url(&url).await { None => { - progress.error(&format!( - "Inconsistent state: missing task {} from the local tasks", - url - )); + progress.error(&format!("Inconsistent state: missing task {} from the local tasks", url)); continue; - } + }, Some(item) => item, }; @@ -318,33 +278,31 @@ where // This item has been removed from the remote progress.debug(&format!("# {} is a deletion from the server", url)); remote_del.insert(url); - } + }, SyncStatus::NotSynced => { // This item has just been locally created progress.debug(&format!("# {} has been locally created", url)); local_additions.insert(url); - } + }, SyncStatus::LocallyDeleted(_) => { // This item has been deleted from both sources progress.debug(&format!("# {} has been deleted from both sources", url)); remote_del.insert(url); - } + }, SyncStatus::LocallyModified(_) => { progress.info(&format!("Conflict: item {} has been deleted from the server and locally modified. Deleting the local copy", url)); remote_del.insert(url); - } + }, } } + // Step 2 - commit changes progress.trace("Committing changes..."); for url_del in local_del { - progress.debug(&format!( - "> Pushing local deletion {} to the server", - url_del - )); + progress.debug(&format!("> Pushing local deletion {} to the server", url_del)); progress.increment_counter(1); - progress.feedback(SyncEvent::InProgress { + progress.feedback(SyncEvent::InProgress{ calendar: cal_name.clone(), items_done_already: progress.counter(), details: Self::item_name(&cal_local, &url_del).await, @@ -352,27 +310,21 @@ where match cal_remote.delete_item(&url_del).await { Err(err) => { - progress.warn(&format!( - "Unable to delete remote item {}: {}", - url_del, err - )); - } + progress.warn(&format!("Unable to delete remote item {}: {}", url_del, err)); + }, Ok(()) => { // Change the local copy from "marked to deletion" to "actually deleted" if let Err(err) = cal_local.immediately_delete_item(&url_del).await { - progress.error(&format!( - "Unable to permanently delete local item {}: {}", - url_del, err - )); + progress.error(&format!("Unable to permanently delete local item {}: {}", url_del, err)); } - } + }, } } for url_del in remote_del { progress.debug(&format!("> Applying remote deletion {} locally", url_del)); progress.increment_counter(1); - progress.feedback(SyncEvent::InProgress { + progress.feedback(SyncEvent::InProgress{ calendar: cal_name.clone(), items_done_already: progress.counter(), details: Self::item_name(&cal_local, &url_del).await, @@ -387,26 +339,22 @@ where &mut *cal_local, &mut *cal_remote, progress, - &cal_name, - ) - .await; + &cal_name + ).await; Self::apply_remote_changes( remote_changes, &mut *cal_local, &mut *cal_remote, progress, - &cal_name, - ) - .await; + &cal_name + ).await; + for url_add in local_additions { - progress.debug(&format!( - "> Pushing local addition {} to the server", - url_add - )); + progress.debug(&format!("> Pushing local addition {} to the server", url_add)); progress.increment_counter(1); - progress.feedback(SyncEvent::InProgress { + progress.feedback(SyncEvent::InProgress{ calendar: cal_name.clone(), items_done_already: progress.counter(), details: Self::item_name(&cal_local, &url_add).await, @@ -415,29 +363,23 @@ where None => { progress.error(&format!("Inconsistency: created item {} has been marked for upload but is locally missing", url_add)); continue; - } + }, Some(item) => { match cal_remote.add_item(item.clone()).await { - Err(err) => progress.error(&format!( - "Unable to add item {} to remote calendar: {}", - url_add, err - )), + Err(err) => progress.error(&format!("Unable to add item {} to remote calendar: {}", url_add, err)), Ok(new_ss) => { // Update local sync status item.set_sync_status(new_ss); - } + }, } - } + }, }; } for url_change in local_changes { - progress.debug(&format!( - "> Pushing local change {} to the server", - url_change - )); + progress.debug(&format!("> Pushing local change {} to the server", url_change)); progress.increment_counter(1); - progress.feedback(SyncEvent::InProgress { + progress.feedback(SyncEvent::InProgress{ calendar: cal_name.clone(), items_done_already: progress.counter(), details: Self::item_name(&cal_local, &url_change).await, @@ -446,17 +388,14 @@ where None => { progress.error(&format!("Inconsistency: modified item {} has been marked for upload but is locally missing", url_change)); continue; - } + }, Some(item) => { match cal_remote.update_item(item.clone()).await { - Err(err) => progress.error(&format!( - "Unable to update item {} in remote calendar: {}", - url_change, err - )), + Err(err) => progress.error(&format!("Unable to update item {} in remote calendar: {}", url_change, err)), Ok(new_ss) => { // Update local sync status item.set_sync_status(new_ss); - } + }, }; } }; @@ -465,12 +404,9 @@ where Ok(()) } + async fn item_name(cal: &T, url: &Url) -> String { - cal.get_item_by_url(url) - .await - .map(|item| item.name()) - .unwrap_or_default() - .to_string() + cal.get_item_by_url(url).await.map(|item| item.name()).unwrap_or_default().to_string() } async fn apply_remote_additions( @@ -478,22 +414,10 @@ where cal_local: &mut T, cal_remote: &mut U, progress: &mut SyncProgress, - cal_name: &str, + cal_name: &str ) { - for batch in remote_additions - .drain() - .chunks(DOWNLOAD_BATCH_SIZE) - .into_iter() - { - Self::fetch_batch_and_apply( - BatchDownloadType::RemoteAdditions, - batch, - cal_local, - cal_remote, - progress, - cal_name, - ) - .await; + for batch in remote_additions.drain().chunks(DOWNLOAD_BATCH_SIZE).into_iter() { + Self::fetch_batch_and_apply(BatchDownloadType::RemoteAdditions, batch, cal_local, cal_remote, progress, cal_name).await; } } @@ -502,22 +426,10 @@ where cal_local: &mut T, cal_remote: &mut U, progress: &mut SyncProgress, - cal_name: &str, + cal_name: &str ) { - for batch in remote_changes - .drain() - .chunks(DOWNLOAD_BATCH_SIZE) - .into_iter() - { - Self::fetch_batch_and_apply( - BatchDownloadType::RemoteChanges, - batch, - cal_local, - cal_remote, - progress, - cal_name, - ) - .await; + for batch in remote_changes.drain().chunks(DOWNLOAD_BATCH_SIZE).into_iter() { + Self::fetch_batch_and_apply(BatchDownloadType::RemoteChanges, batch, cal_local, cal_remote, progress, cal_name).await; } } @@ -527,74 +439,60 @@ where cal_local: &mut T, cal_remote: &mut U, progress: &mut SyncProgress, - cal_name: &str, + cal_name: &str ) { progress.debug(&format!("> Applying a batch of {} locally", batch_type) /* too bad Chunks does not implement ExactSizeIterator, that could provide useful debug info. See https://github.com/rust-itertools/itertools/issues/171 */); let list_of_additions: Vec = remote_additions.map(|url| url.clone()).collect(); match cal_remote.get_items_by_url(&list_of_additions).await { Err(err) => { - progress.warn(&format!( - "Unable to get the batch of {} {:?}: {}. Skipping them.", - batch_type, list_of_additions, err - )); - } + progress.warn(&format!("Unable to get the batch of {} {:?}: {}. Skipping them.", batch_type, list_of_additions, err)); + }, Ok(items) => { for item in items { match item { None => { - progress.error("Inconsistency: an item from the batch has vanished from the remote end"); + progress.error(&format!("Inconsistency: an item from the batch has vanished from the remote end")); continue; - } + }, Some(new_item) => { let local_update_result = match batch_type { - BatchDownloadType::RemoteAdditions => { - cal_local.add_item(new_item.clone()).await - } - BatchDownloadType::RemoteChanges => { - cal_local.update_item(new_item.clone()).await - } + BatchDownloadType::RemoteAdditions => cal_local.add_item(new_item.clone()).await, + BatchDownloadType::RemoteChanges => cal_local.update_item(new_item.clone()).await, }; if let Err(err) = local_update_result { - progress.error(&format!( - "Not able to add item {} to local calendar: {}", - new_item.url(), - err - )); + progress.error(&format!("Not able to add item {} to local calendar: {}", new_item.url(), err)); } - } + }, } } // Notifying every item at the same time would not make sense. Let's notify only one of them - let one_item_name = match list_of_additions.first() { - Some(url) => Self::item_name(cal_local, url).await, + let one_item_name = match list_of_additions.get(0) { + Some(url) => Self::item_name(&cal_local, &url).await, None => String::from(""), }; progress.increment_counter(list_of_additions.len()); - progress.feedback(SyncEvent::InProgress { + progress.feedback(SyncEvent::InProgress{ calendar: cal_name.to_string(), items_done_already: progress.counter(), details: one_item_name, }); - } + }, } } } -async fn get_or_insert_counterpart_calendar( - haystack_descr: &str, - haystack: &mut H, - cal_url: &Url, - needle: Arc>, -) -> Result>, Box> + +async fn get_or_insert_counterpart_calendar(haystack_descr: &str, haystack: &mut H, cal_url: &Url, needle: Arc>) + -> Result>, Box> where H: CalDavSource, I: BaseCalendar, N: BaseCalendar, { loop { - if let Some(cal) = haystack.get_calendar(cal_url).await { + if let Some(cal) = haystack.get_calendar(&cal_url).await { break Ok(cal); } @@ -604,11 +502,14 @@ where let name = src.name().to_string(); let supported_comps = src.supported_components(); let color = src.color(); - if let Err(err) = haystack - .create_calendar(cal_url.clone(), name, supported_comps, color.cloned()) - .await - { + if let Err(err) = haystack.create_calendar( + cal_url.clone(), + name, + supported_comps, + color.cloned(), + ).await{ return Err(err); } } } + diff --git a/src/resource.rs b/src/resource.rs index 6599617..a7ef98f 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -10,27 +10,17 @@ pub struct Resource { impl Resource { pub fn new(url: Url, username: String, password: String) -> Self { - Self { - url, - username, - password, - } + Self { url, username, password } } - pub fn url(&self) -> &Url { - &self.url - } - pub fn username(&self) -> &String { - &self.username - } - pub fn password(&self) -> &String { - &self.password - } + pub fn url(&self) -> &Url { &self.url } + pub fn username(&self) -> &String { &self.username } + pub fn password(&self) -> &String { &self.password } /// Build a new Resource by keeping the same credentials, scheme and server from `base` but changing the path part pub fn combine(&self, new_path: &str) -> Resource { let mut built = (*self).clone(); - built.url.set_path(new_path); + built.url.set_path(&new_path); built } } diff --git a/src/task.rs b/src/task.rs index a1ec877..2545abb 100644 --- a/src/task.rs +++ b/src/task.rs @@ -1,24 +1,20 @@ //! To-do tasks (iCal `VTODO` item) +use serde::{Deserialize, Serialize}; +use uuid::Uuid; use chrono::{DateTime, Utc}; use ical::property::Property; -use serde::{Deserialize, Serialize}; use url::Url; -use uuid::Uuid; use crate::item::SyncStatus; use crate::utils::random_url; -/** -RFC5545 defines the completion as several optional fields, yet some combinations make no sense. -This enum provides an API that forbids such impossible combinations. - -* `COMPLETED` is an optional timestamp that tells whether this task is completed -* `STATUS` is an optional field, that can be set to `NEEDS-ACTION`, `COMPLETED`, or others. - -Even though having a `COMPLETED` date but a `STATUS:NEEDS-ACTION` is theorically possible, it obviously makes no sense. This API ensures this cannot happen - -*/ +/// RFC5545 defines the completion as several optional fields, yet some combinations make no sense. +/// This enum provides an API that forbids such impossible combinations. +/// +/// * `COMPLETED` is an optional timestamp that tells whether this task is completed +/// * `STATUS` is an optional field, that can be set to `NEEDS-ACTION`, `COMPLETED`, or others. +/// Even though having a `COMPLETED` date but a `STATUS:NEEDS-ACTION` is theorically possible, it obviously makes no sense. This API ensures this cannot happen #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum CompletionStatus { Completed(Option>), @@ -26,7 +22,10 @@ pub enum CompletionStatus { } impl CompletionStatus { pub fn is_completed(&self) -> bool { - matches!(self, CompletionStatus::Completed(_)) + match self { + CompletionStatus::Completed(_) => true, + _ => false, + } } } @@ -54,6 +53,7 @@ pub struct Task { /// The display name of the task name: String, + /// The PRODID, as defined in iCal files ical_prod_id: String, @@ -62,6 +62,7 @@ pub struct Task { extra_parameters: Vec, } + impl Task { /// Create a brand new Task that is not on a server yet. /// This will pick a new (random) task ID. @@ -72,37 +73,20 @@ impl Task { let new_creation_date = Some(Utc::now()); let new_last_modified = Utc::now(); let new_completion_status = if completed { - CompletionStatus::Completed(Some(Utc::now())) - } else { - CompletionStatus::Uncompleted - }; + CompletionStatus::Completed(Some(Utc::now())) + } else { CompletionStatus::Uncompleted }; let ical_prod_id = crate::ical::default_prod_id(); let extra_parameters = Vec::new(); - Self::new_with_parameters( - name, - new_uid, - new_url, - new_completion_status, - new_sync_status, - new_creation_date, - new_last_modified, - ical_prod_id, - extra_parameters, - ) + Self::new_with_parameters(name, new_uid, new_url, new_completion_status, new_sync_status, new_creation_date, new_last_modified, ical_prod_id, extra_parameters) } /// Create a new Task instance, that may be synced on the server already - pub fn new_with_parameters( - name: String, - uid: String, - new_url: Url, - completion_status: CompletionStatus, - sync_status: SyncStatus, - creation_date: Option>, - last_modified: DateTime, - ical_prod_id: String, - extra_parameters: Vec, - ) -> Self { + pub fn new_with_parameters(name: String, uid: String, new_url: Url, + completion_status: CompletionStatus, + sync_status: SyncStatus, creation_date: Option>, last_modified: DateTime, + ical_prod_id: String, extra_parameters: Vec, + ) -> Self + { Self { url: new_url, uid, @@ -116,40 +100,20 @@ impl Task { } } - pub fn url(&self) -> &Url { - &self.url - } - pub fn uid(&self) -> &str { - &self.uid - } - pub fn name(&self) -> &str { - &self.name - } - pub fn completed(&self) -> bool { - self.completion_status.is_completed() - } - pub fn ical_prod_id(&self) -> &str { - &self.ical_prod_id - } - pub fn sync_status(&self) -> &SyncStatus { - &self.sync_status - } - pub fn last_modified(&self) -> &DateTime { - &self.last_modified - } - pub fn creation_date(&self) -> Option<&DateTime> { - self.creation_date.as_ref() - } - pub fn completion_status(&self) -> &CompletionStatus { - &self.completion_status - } - pub fn extra_parameters(&self) -> &[Property] { - &self.extra_parameters - } + pub fn url(&self) -> &Url { &self.url } + pub fn uid(&self) -> &str { &self.uid } + pub fn name(&self) -> &str { &self.name } + pub fn completed(&self) -> bool { self.completion_status.is_completed() } + pub fn ical_prod_id(&self) -> &str { &self.ical_prod_id } + pub fn sync_status(&self) -> &SyncStatus { &self.sync_status } + pub fn last_modified(&self) -> &DateTime { &self.last_modified } + pub fn creation_date(&self) -> Option<&DateTime> { self.creation_date.as_ref() } + pub fn completion_status(&self) -> &CompletionStatus { &self.completion_status } + pub fn extra_parameters(&self) -> &[Property] { &self.extra_parameters } #[cfg(any(test, feature = "integration_tests"))] pub fn has_same_observable_content_as(&self, other: &Task) -> bool { - self.url == other.url + self.url == other.url && self.uid == other.uid && self.name == other.name // sync status must be the same variant, but we ignore its embedded version tag @@ -165,13 +129,15 @@ impl Task { fn update_sync_status(&mut self) { match &self.sync_status { - SyncStatus::NotSynced | SyncStatus::LocallyModified(_) => (), + SyncStatus::NotSynced => return, + SyncStatus::LocallyModified(_) => return, SyncStatus::Synced(prev_vt) => { self.sync_status = SyncStatus::LocallyModified(prev_vt.clone()); } SyncStatus::LocallyDeleted(_) => { log::warn!("Trying to update an item that has previously been deleted. These changes will probably be ignored at next sync."); - } + return; + }, } } @@ -179,6 +145,7 @@ impl Task { self.last_modified = Utc::now(); } + /// Rename a task. /// This updates its "last modified" field pub fn set_name(&mut self, new_name: String) { @@ -202,10 +169,7 @@ impl Task { } #[cfg(feature = "local_calendar_mocks_remote_calendars")] /// Set the completion status, but forces a "master" SyncStatus, just like CalDAV servers are always "masters" - pub fn mock_remote_calendar_set_completion_status( - &mut self, - new_completion_status: CompletionStatus, - ) { + pub fn mock_remote_calendar_set_completion_status(&mut self, new_completion_status: CompletionStatus) { self.sync_status = SyncStatus::random_synced(); self.completion_status = new_completion_status; } diff --git a/src/traits.rs b/src/traits.rs index 7e33304..3bcffe3 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,17 +1,17 @@ //! Traits used by multiple structs in this crate -use std::collections::{HashMap, HashSet}; use std::error::Error; +use std::collections::{HashMap, HashSet}; use std::sync::{Arc, Mutex}; use async_trait::async_trait; use csscolorparser::Color; use url::Url; -use crate::calendar::SupportedComponents; -use crate::item::Item; use crate::item::SyncStatus; +use crate::item::Item; use crate::item::VersionTag; +use crate::calendar::SupportedComponents; use crate::resource::Resource; /// This trait must be implemented by data sources (either local caches or remote CalDAV clients) @@ -25,13 +25,8 @@ pub trait CalDavSource { /// Returns the calendar matching the URL async fn get_calendar(&self, url: &Url) -> Option>>; /// Create a calendar if it did not exist, and return it - async fn create_calendar( - &mut self, - url: Url, - name: String, - supported_components: SupportedComponents, - color: Option, - ) -> Result>, Box>; + async fn create_calendar(&mut self, url: Url, name: String, supported_components: SupportedComponents, color: Option) + -> Result>, Box>; // Removing a calendar is not supported yet } @@ -64,29 +59,23 @@ pub trait BaseCalendar { /// Returns whether this calDAV calendar supports to-do items fn supports_todo(&self) -> bool { - self.supported_components() - .contains(crate::calendar::SupportedComponents::TODO) + self.supported_components().contains(crate::calendar::SupportedComponents::TODO) } /// Returns whether this calDAV calendar supports calendar items fn supports_events(&self) -> bool { - self.supported_components() - .contains(crate::calendar::SupportedComponents::EVENT) + self.supported_components().contains(crate::calendar::SupportedComponents::EVENT) } } + /// Functions availabe for calendars that are backed by a CalDAV server /// /// Note that some concrete types (e.g. [`crate::calendar::cached_calendar::CachedCalendar`]) can also provide non-async versions of these functions #[async_trait] -pub trait DavCalendar: BaseCalendar { +pub trait DavCalendar : BaseCalendar { /// Create a new calendar - fn new( - name: String, - resource: Resource, - supported_components: SupportedComponents, - color: Option, - ) -> Self; + fn new(name: String, resource: Resource, supported_components: SupportedComponents, color: Option) -> Self; /// Get the URLs and the version tags of every item in this calendar async fn get_item_version_tags(&self) -> Result, Box>; @@ -104,27 +93,25 @@ pub trait DavCalendar: BaseCalendar { /// Get the URLs of all current items in this calendar async fn get_item_urls(&self) -> Result, Box> { let items = self.get_item_version_tags().await?; - Ok(items.keys().cloned().collect()) + Ok(items.iter() + .map(|(url, _tag)| url.clone()) + .collect()) } // Note: the CalDAV protocol could also enable to do this: // fn get_current_version(&self) -> CTag } + /// Functions availabe for calendars we have full knowledge of /// /// Usually, these are local calendars fully backed by a local folder /// /// Note that some concrete types (e.g. [`crate::calendar::cached_calendar::CachedCalendar`]) can also provide non-async versions of these functions #[async_trait] -pub trait CompleteCalendar: BaseCalendar { +pub trait CompleteCalendar : BaseCalendar { /// Create a new calendar - fn new( - name: String, - url: Url, - supported_components: SupportedComponents, - color: Option, - ) -> Self; + fn new(name: String, url: Url, supported_components: SupportedComponents, color: Option) -> Self; /// Get the URLs of all current items in this calendar async fn get_item_urls(&self) -> Result, Box>; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 3ce99e7..5f917ae 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,17 +1,17 @@ //! Some utility functions use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, Mutex}; use std::hash::Hash; use std::io::{stdin, stdout, Read, Write}; -use std::sync::{Arc, Mutex}; use minidom::Element; use url::Url; -use crate::item::SyncStatus; use crate::traits::CompleteCalendar; use crate::traits::DavCalendar; use crate::Item; +use crate::item::SyncStatus; /// Walks an XML tree and returns every element that has the given name pub fn find_elems>(root: &Element, searched_name: S) -> Vec<&Element> { @@ -49,10 +49,14 @@ pub fn find_elem>(root: &Element, searched_name: S) -> Option<&Ele None } + pub fn print_xml(element: &Element) { let mut writer = std::io::stdout(); - let mut xml_writer = minidom::quick_xml::Writer::new_with_indent(std::io::stdout(), 0x20, 4); + let mut xml_writer = minidom::quick_xml::Writer::new_with_indent( + std::io::stdout(), + 0x20, 4 + ); let _ = element.to_writer(&mut xml_writer); let _ = writer.write(&[0x0a]); } @@ -70,7 +74,7 @@ where for (_, item) in map { print_task(item); } - } + }, } } } @@ -88,24 +92,28 @@ where for (url, version_tag) in map { println!(" * {} (version {:?})", url, version_tag); } - } + }, } } } pub fn print_task(item: &Item) { - if let Item::Task(task) = item { - let completion = if task.completed() { "✓" } else { " " }; - let sync = match task.sync_status() { - SyncStatus::NotSynced => ".", - SyncStatus::Synced(_) => "=", - SyncStatus::LocallyModified(_) => "~", - SyncStatus::LocallyDeleted(_) => "x", - }; - println!(" {}{} {}\t{}", completion, sync, task.name(), task.url()); + match item { + Item::Task(task) => { + let completion = if task.completed() { "✓" } else { " " }; + let sync = match task.sync_status() { + SyncStatus::NotSynced => ".", + SyncStatus::Synced(_) => "=", + SyncStatus::LocallyModified(_) => "~", + SyncStatus::LocallyDeleted(_) => "x", + }; + println!(" {}{} {}\t{}", completion, sync, task.name(), task.url()); + }, + _ => return, } } + /// Compare keys of two hashmaps for equality pub fn keys_are_the_same(left: &HashMap, right: &HashMap) -> bool where @@ -119,7 +127,7 @@ where let keys_l: HashSet = left.keys().cloned().collect(); let keys_r: HashSet = right.keys().cloned().collect(); let result = keys_l == keys_r; - if !result { + if result == false { log::debug!("Keys of a map mismatch"); for key in keys_l { log::debug!(" left: {}", key); @@ -132,6 +140,7 @@ where result } + /// Wait for the user to press enter pub fn pause() { let mut stdout = stdout(); @@ -140,6 +149,7 @@ pub fn pause() { stdin().read_exact(&mut [0]).unwrap(); } + /// Generate a random URL with a given prefix pub fn random_url(parent_calendar: &Url) -> Url { let random = uuid::Uuid::new_v4().to_hyphenated().to_string(); diff --git a/tests/scenarii.rs b/tests/scenarii.rs index c62526e..c8a0e9a 100644 --- a/tests/scenarii.rs +++ b/tests/scenarii.rs @@ -8,27 +8,27 @@ //! This module can also check the sources after a sync contain the actual data we expect #![cfg(feature = "local_calendar_mocks_remote_calendars")] -use std::error::Error; use std::path::PathBuf; use std::sync::{Arc, Mutex}; +use std::error::Error; use url::Url; use chrono::Utc; -use kitchen_fridge::cache::Cache; -use kitchen_fridge::calendar::cached_calendar::CachedCalendar; use kitchen_fridge::calendar::SupportedComponents; -use kitchen_fridge::item::SyncStatus; -use kitchen_fridge::mock_behaviour::MockBehaviour; -use kitchen_fridge::provider::Provider; -use kitchen_fridge::task::CompletionStatus; -use kitchen_fridge::traits::BaseCalendar; use kitchen_fridge::traits::CalDavSource; +use kitchen_fridge::traits::BaseCalendar; use kitchen_fridge::traits::CompleteCalendar; use kitchen_fridge::traits::DavCalendar; -use kitchen_fridge::utils::random_url; +use kitchen_fridge::cache::Cache; use kitchen_fridge::Item; +use kitchen_fridge::item::SyncStatus; use kitchen_fridge::Task; +use kitchen_fridge::task::CompletionStatus; +use kitchen_fridge::calendar::cached_calendar::CachedCalendar; +use kitchen_fridge::provider::Provider; +use kitchen_fridge::mock_behaviour::MockBehaviour; +use kitchen_fridge::utils::random_url; pub enum LocatedState { /// Item does not exist yet or does not exist anymore @@ -60,10 +60,11 @@ pub enum ChangeToApply { // ChangeCalendar(Url) is useless, as long as changing a calendar is implemented as "delete in one calendar and re-create it in another one" } + pub struct ItemScenario { url: Url, initial_state: LocatedState, - local_changes_to_apply: Vec, + local_changes_to_apply: Vec, remote_changes_to_apply: Vec, after_sync: LocatedState, } @@ -86,317 +87,329 @@ pub struct ItemScenario { pub fn scenarii_basic() -> Vec { let mut tasks = Vec::new(); - let first_cal = "https://some.calend.ar/calendar-1/".parse().unwrap(); - let second_cal = "https://some.calend.ar/calendar-2/".parse().unwrap(); - let third_cal = "https://some.calend.ar/calendar-3/".parse().unwrap(); + let first_cal = Url::from("https://some.calend.ar/calendar-1/".parse().unwrap()); + let second_cal = Url::from("https://some.calend.ar/calendar-2/".parse().unwrap()); + let third_cal = Url::from("https://some.calend.ar/calendar-3/".parse().unwrap()); - tasks.push(ItemScenario { - url: random_url(&first_cal), - initial_state: LocatedState::BothSynced(ItemState { - calendar: first_cal.clone(), - name: String::from("Task A"), - completed: false, - }), - local_changes_to_apply: Vec::new(), - remote_changes_to_apply: Vec::new(), - after_sync: LocatedState::BothSynced(ItemState { - calendar: first_cal.clone(), - name: String::from("Task A"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&first_cal), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: first_cal.clone(), + name: String::from("Task A"), + completed: false, + }), + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: first_cal.clone(), + name: String::from("Task A"), + completed: false, + }), + } + ); - tasks.push(ItemScenario { - url: random_url(&first_cal), - initial_state: LocatedState::BothSynced(ItemState { - calendar: first_cal.clone(), - name: String::from("Task B"), - completed: false, - }), - local_changes_to_apply: Vec::new(), - remote_changes_to_apply: vec![ChangeToApply::Remove], - after_sync: LocatedState::None, - }); + tasks.push( + ItemScenario { + url: random_url(&first_cal), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: first_cal.clone(), + name: String::from("Task B"), + completed: false, + }), + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: vec![ChangeToApply::Remove], + after_sync: LocatedState::None, + } + ); - tasks.push(ItemScenario { - url: random_url(&first_cal), - initial_state: LocatedState::BothSynced(ItemState { - calendar: first_cal.clone(), - name: String::from("Task C"), - completed: false, - }), - local_changes_to_apply: vec![ChangeToApply::Remove], - remote_changes_to_apply: Vec::new(), - after_sync: LocatedState::None, - }); + tasks.push( + ItemScenario { + url: random_url(&first_cal), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: first_cal.clone(), + name: String::from("Task C"), + completed: false, + }), + local_changes_to_apply: vec![ChangeToApply::Remove], + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::None, + } + ); - tasks.push(ItemScenario { - url: random_url(&first_cal), - initial_state: LocatedState::BothSynced(ItemState { - calendar: first_cal.clone(), - name: String::from("Task D"), - completed: false, - }), - local_changes_to_apply: vec![ChangeToApply::Rename(String::from( - "Task D, locally renamed", - ))], - remote_changes_to_apply: Vec::new(), - after_sync: LocatedState::BothSynced(ItemState { - calendar: first_cal.clone(), - name: String::from("Task D, locally renamed"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&first_cal), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: first_cal.clone(), + name: String::from("Task D"), + completed: false, + }), + local_changes_to_apply: vec![ChangeToApply::Rename(String::from("Task D, locally renamed"))], + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: first_cal.clone(), + name: String::from("Task D, locally renamed"), + completed: false, + }), + } + ); - tasks.push(ItemScenario { - url: random_url(&first_cal), - initial_state: LocatedState::BothSynced(ItemState { - calendar: first_cal.clone(), - name: String::from("Task E"), - completed: false, - }), - local_changes_to_apply: Vec::new(), - remote_changes_to_apply: vec![ChangeToApply::Rename(String::from( - "Task E, remotely renamed", - ))], - after_sync: LocatedState::BothSynced(ItemState { - calendar: first_cal.clone(), - name: String::from("Task E, remotely renamed"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&first_cal), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: first_cal.clone(), + name: String::from("Task E"), + completed: false, + }), + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: vec![ChangeToApply::Rename(String::from("Task E, remotely renamed"))], + after_sync: LocatedState::BothSynced( ItemState{ + calendar: first_cal.clone(), + name: String::from("Task E, remotely renamed"), + completed: false, + }), + } + ); - tasks.push(ItemScenario { - url: random_url(&first_cal), - initial_state: LocatedState::BothSynced(ItemState { - calendar: first_cal.clone(), - name: String::from("Task F"), - completed: false, - }), - local_changes_to_apply: vec![ChangeToApply::Rename(String::from( - "Task F, locally renamed", - ))], - remote_changes_to_apply: vec![ChangeToApply::Rename(String::from( - "Task F, remotely renamed", - ))], - // Conflict: the server wins - after_sync: LocatedState::BothSynced(ItemState { - calendar: first_cal.clone(), - name: String::from("Task F, remotely renamed"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&first_cal), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: first_cal.clone(), + name: String::from("Task F"), + completed: false, + }), + local_changes_to_apply: vec![ChangeToApply::Rename(String::from("Task F, locally renamed"))], + remote_changes_to_apply: vec![ChangeToApply::Rename(String::from("Task F, remotely renamed"))], + // Conflict: the server wins + after_sync: LocatedState::BothSynced( ItemState{ + calendar: first_cal.clone(), + name: String::from("Task F, remotely renamed"), + completed: false, + }), + } + ); - tasks.push(ItemScenario { - url: random_url(&second_cal), - initial_state: LocatedState::BothSynced(ItemState { - calendar: second_cal.clone(), - name: String::from("Task G"), - completed: false, - }), - local_changes_to_apply: Vec::new(), - remote_changes_to_apply: vec![ChangeToApply::SetCompletion(true)], - after_sync: LocatedState::BothSynced(ItemState { - calendar: second_cal.clone(), - name: String::from("Task G"), - completed: true, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&second_cal), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: second_cal.clone(), + name: String::from("Task G"), + completed: false, + }), + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: vec![ChangeToApply::SetCompletion(true)], + after_sync: LocatedState::BothSynced( ItemState{ + calendar: second_cal.clone(), + name: String::from("Task G"), + completed: true, + }), + } + ); - tasks.push(ItemScenario { - url: random_url(&second_cal), - initial_state: LocatedState::BothSynced(ItemState { - calendar: second_cal.clone(), - name: String::from("Task H"), - completed: false, - }), - local_changes_to_apply: vec![ChangeToApply::SetCompletion(true)], - remote_changes_to_apply: Vec::new(), - after_sync: LocatedState::BothSynced(ItemState { - calendar: second_cal.clone(), - name: String::from("Task H"), - completed: true, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&second_cal), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: second_cal.clone(), + name: String::from("Task H"), + completed: false, + }), + local_changes_to_apply: vec![ChangeToApply::SetCompletion(true)], + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: second_cal.clone(), + name: String::from("Task H"), + completed: true, + }), + } + ); - tasks.push(ItemScenario { - url: random_url(&second_cal), - initial_state: LocatedState::BothSynced(ItemState { - calendar: second_cal.clone(), - name: String::from("Task I"), - completed: false, - }), - local_changes_to_apply: vec![ChangeToApply::SetCompletion(true)], - remote_changes_to_apply: vec![ChangeToApply::Rename(String::from( - "Task I, remotely renamed", - ))], - // Conflict, the server wins - after_sync: LocatedState::BothSynced(ItemState { - calendar: second_cal.clone(), - name: String::from("Task I, remotely renamed"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&second_cal), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: second_cal.clone(), + name: String::from("Task I"), + completed: false, + }), + local_changes_to_apply: vec![ChangeToApply::SetCompletion(true)], + remote_changes_to_apply: vec![ChangeToApply::Rename(String::from("Task I, remotely renamed"))], + // Conflict, the server wins + after_sync: LocatedState::BothSynced( ItemState{ + calendar: second_cal.clone(), + name: String::from("Task I, remotely renamed"), + completed: false, + }), + } + ); - tasks.push(ItemScenario { - url: random_url(&second_cal), - initial_state: LocatedState::BothSynced(ItemState { - calendar: second_cal.clone(), - name: String::from("Task J"), - completed: false, - }), - local_changes_to_apply: vec![ChangeToApply::SetCompletion(true)], - remote_changes_to_apply: vec![ChangeToApply::Remove], - after_sync: LocatedState::None, - }); + tasks.push( + ItemScenario { + url: random_url(&second_cal), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: second_cal.clone(), + name: String::from("Task J"), + completed: false, + }), + local_changes_to_apply: vec![ChangeToApply::SetCompletion(true)], + remote_changes_to_apply: vec![ChangeToApply::Remove], + after_sync: LocatedState::None, + } + ); - tasks.push(ItemScenario { - url: random_url(&second_cal), - initial_state: LocatedState::BothSynced(ItemState { - calendar: second_cal.clone(), - name: String::from("Task K"), - completed: false, - }), - local_changes_to_apply: vec![ChangeToApply::Remove], - remote_changes_to_apply: vec![ChangeToApply::SetCompletion(true)], - after_sync: LocatedState::BothSynced(ItemState { - calendar: second_cal.clone(), - name: String::from("Task K"), - completed: true, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&second_cal), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: second_cal.clone(), + name: String::from("Task K"), + completed: false, + }), + local_changes_to_apply: vec![ChangeToApply::Remove], + remote_changes_to_apply: vec![ChangeToApply::SetCompletion(true)], + after_sync: LocatedState::BothSynced( ItemState{ + calendar: second_cal.clone(), + name: String::from("Task K"), + completed: true, + }), + } + ); - tasks.push(ItemScenario { - url: random_url(&second_cal), - initial_state: LocatedState::BothSynced(ItemState { - calendar: second_cal.clone(), - name: String::from("Task L"), - completed: false, - }), - local_changes_to_apply: vec![ChangeToApply::Remove], - remote_changes_to_apply: vec![ChangeToApply::Remove], - after_sync: LocatedState::None, - }); + tasks.push( + ItemScenario { + url: random_url(&second_cal), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: second_cal.clone(), + name: String::from("Task L"), + completed: false, + }), + local_changes_to_apply: vec![ChangeToApply::Remove], + remote_changes_to_apply: vec![ChangeToApply::Remove], + after_sync: LocatedState::None, + } + ); - tasks.push(ItemScenario { - url: random_url(&second_cal), - initial_state: LocatedState::BothSynced(ItemState { - calendar: second_cal.clone(), - name: String::from("Task M"), - completed: true, - }), - local_changes_to_apply: vec![ChangeToApply::SetCompletion(false)], - remote_changes_to_apply: Vec::new(), - after_sync: LocatedState::BothSynced(ItemState { - calendar: second_cal.clone(), - name: String::from("Task M"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&second_cal), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: second_cal.clone(), + name: String::from("Task M"), + completed: true, + }), + local_changes_to_apply: vec![ChangeToApply::SetCompletion(false)], + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: second_cal.clone(), + name: String::from("Task M"), + completed: false, + }), + } + ); - tasks.push(ItemScenario { - url: random_url(&third_cal), - initial_state: LocatedState::BothSynced(ItemState { - calendar: third_cal.clone(), - name: String::from("Task N"), - completed: true, - }), - local_changes_to_apply: Vec::new(), - remote_changes_to_apply: vec![ChangeToApply::SetCompletion(false)], - after_sync: LocatedState::BothSynced(ItemState { - calendar: third_cal.clone(), - name: String::from("Task N"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&third_cal), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: third_cal.clone(), + name: String::from("Task N"), + completed: true, + }), + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: vec![ChangeToApply::SetCompletion(false)], + after_sync: LocatedState::BothSynced( ItemState{ + calendar: third_cal.clone(), + name: String::from("Task N"), + completed: false, + }), + } + ); - tasks.push(ItemScenario { - url: random_url(&third_cal), - initial_state: LocatedState::BothSynced(ItemState { - calendar: third_cal.clone(), - name: String::from("Task O"), - completed: true, - }), - local_changes_to_apply: vec![ChangeToApply::SetCompletion(false)], - remote_changes_to_apply: vec![ChangeToApply::SetCompletion(false)], - after_sync: LocatedState::BothSynced(ItemState { - calendar: third_cal.clone(), - name: String::from("Task O"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&third_cal), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: third_cal.clone(), + name: String::from("Task O"), + completed: true, + }), + local_changes_to_apply: vec![ChangeToApply::SetCompletion(false)], + remote_changes_to_apply: vec![ChangeToApply::SetCompletion(false)], + after_sync: LocatedState::BothSynced( ItemState{ + calendar: third_cal.clone(), + name: String::from("Task O"), + completed: false, + }), + } + ); let url_p = random_url(&third_cal); - tasks.push(ItemScenario { - url: url_p.clone(), - initial_state: LocatedState::BothSynced(ItemState { - calendar: third_cal.clone(), - name: String::from("Task P"), - completed: true, - }), - local_changes_to_apply: vec![ - ChangeToApply::Rename(String::from("Task P, locally renamed and un-completed")), - ChangeToApply::SetCompletion(false), - ], - remote_changes_to_apply: Vec::new(), - after_sync: LocatedState::BothSynced(ItemState { - calendar: third_cal.clone(), - name: String::from("Task P, locally renamed and un-completed"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: url_p.clone(), + initial_state: LocatedState::BothSynced( ItemState{ + calendar: third_cal.clone(), + name: String::from("Task P"), + completed: true, + }), + local_changes_to_apply: vec![ + ChangeToApply::Rename(String::from("Task P, locally renamed and un-completed")), + ChangeToApply::SetCompletion(false), + ], + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: third_cal.clone(), + name: String::from("Task P, locally renamed and un-completed"), + completed: false, + }), + } + ); let url_q = random_url(&third_cal); - tasks.push(ItemScenario { - url: url_q.clone(), - initial_state: LocatedState::None, - local_changes_to_apply: Vec::new(), - remote_changes_to_apply: vec![ChangeToApply::Create( - third_cal.clone(), - Item::Task(Task::new_with_parameters( - String::from("Task Q, created on the server"), - url_q.to_string(), - url_q, - CompletionStatus::Uncompleted, - SyncStatus::random_synced(), - Some(Utc::now()), - Utc::now(), - "prod_id".to_string(), - Vec::new(), - )), - )], - after_sync: LocatedState::BothSynced(ItemState { - calendar: third_cal.clone(), - name: String::from("Task Q, created on the server"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: url_q.clone(), + initial_state: LocatedState::None, + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: vec![ChangeToApply::Create(third_cal.clone(), Item::Task( + Task::new_with_parameters( + String::from("Task Q, created on the server"), + url_q.to_string(), url_q, + CompletionStatus::Uncompleted, + SyncStatus::random_synced(), Some(Utc::now()), Utc::now(), "prod_id".to_string(), Vec::new() ) + ))], + after_sync: LocatedState::BothSynced( ItemState{ + calendar: third_cal.clone(), + name: String::from("Task Q, created on the server"), + completed: false, + }), + } + ); let url_r = random_url(&third_cal); - tasks.push(ItemScenario { - url: url_r.clone(), - initial_state: LocatedState::None, - local_changes_to_apply: vec![ChangeToApply::Create( - third_cal.clone(), - Item::Task(Task::new_with_parameters( - String::from("Task R, created locally"), - url_r.to_string(), - url_r, - CompletionStatus::Uncompleted, - SyncStatus::NotSynced, - Some(Utc::now()), - Utc::now(), - "prod_id".to_string(), - Vec::new(), - )), - )], - remote_changes_to_apply: Vec::new(), - after_sync: LocatedState::BothSynced(ItemState { - calendar: third_cal.clone(), - name: String::from("Task R, created locally"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: url_r.clone(), + initial_state: LocatedState::None, + local_changes_to_apply: vec![ChangeToApply::Create(third_cal.clone(), Item::Task( + Task::new_with_parameters( + String::from("Task R, created locally"), + url_r.to_string(), url_r, + CompletionStatus::Uncompleted, + SyncStatus::NotSynced, Some(Utc::now()), Utc::now(), "prod_id".to_string(), Vec::new() ) + ))], + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: third_cal.clone(), + name: String::from("Task R, created locally"), + completed: false, + }), + } + ); tasks } @@ -405,56 +418,62 @@ pub fn scenarii_basic() -> Vec { pub fn scenarii_first_sync_to_local() -> Vec { let mut tasks = Vec::new(); - let cal1 = "https://some.calend.ar/first/".parse().unwrap(); - let cal2 = "https://some.calend.ar/second/".parse().unwrap(); + let cal1 = Url::from("https://some.calend.ar/first/".parse().unwrap()); + let cal2 = Url::from("https://some.calend.ar/second/".parse().unwrap()); - tasks.push(ItemScenario { - url: random_url(&cal1), - initial_state: LocatedState::Remote(ItemState { - calendar: cal1.clone(), - name: String::from("Task A1"), - completed: false, - }), - local_changes_to_apply: Vec::new(), - remote_changes_to_apply: Vec::new(), - after_sync: LocatedState::BothSynced(ItemState { - calendar: cal1.clone(), - name: String::from("Task A1"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&cal1), + initial_state: LocatedState::Remote( ItemState{ + calendar: cal1.clone(), + name: String::from("Task A1"), + completed: false, + }), + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: cal1.clone(), + name: String::from("Task A1"), + completed: false, + }), + } + ); - tasks.push(ItemScenario { - url: random_url(&cal2), - initial_state: LocatedState::Remote(ItemState { - calendar: cal2.clone(), - name: String::from("Task A2"), - completed: false, - }), - local_changes_to_apply: Vec::new(), - remote_changes_to_apply: Vec::new(), - after_sync: LocatedState::BothSynced(ItemState { - calendar: cal2.clone(), - name: String::from("Task A2"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&cal2), + initial_state: LocatedState::Remote( ItemState{ + calendar: cal2.clone(), + name: String::from("Task A2"), + completed: false, + }), + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: cal2.clone(), + name: String::from("Task A2"), + completed: false, + }), + } + ); - tasks.push(ItemScenario { - url: random_url(&cal1), - initial_state: LocatedState::Remote(ItemState { - calendar: cal1.clone(), - name: String::from("Task B1"), - completed: false, - }), - local_changes_to_apply: Vec::new(), - remote_changes_to_apply: Vec::new(), - after_sync: LocatedState::BothSynced(ItemState { - calendar: cal1.clone(), - name: String::from("Task B1"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&cal1), + initial_state: LocatedState::Remote( ItemState{ + calendar: cal1.clone(), + name: String::from("Task B1"), + completed: false, + }), + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: cal1.clone(), + name: String::from("Task B1"), + completed: false, + }), + } + ); tasks } @@ -463,160 +482,147 @@ pub fn scenarii_first_sync_to_local() -> Vec { pub fn scenarii_first_sync_to_server() -> Vec { let mut tasks = Vec::new(); - let cal3 = "https://some.calend.ar/third/".parse().unwrap(); - let cal4 = "https://some.calend.ar/fourth/".parse().unwrap(); + let cal3 = Url::from("https://some.calend.ar/third/".parse().unwrap()); + let cal4 = Url::from("https://some.calend.ar/fourth/".parse().unwrap()); - tasks.push(ItemScenario { - url: random_url(&cal3), - initial_state: LocatedState::Local(ItemState { - calendar: cal3.clone(), - name: String::from("Task A3"), - completed: false, - }), - local_changes_to_apply: Vec::new(), - remote_changes_to_apply: Vec::new(), - after_sync: LocatedState::BothSynced(ItemState { - calendar: cal3.clone(), - name: String::from("Task A3"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&cal3), + initial_state: LocatedState::Local( ItemState{ + calendar: cal3.clone(), + name: String::from("Task A3"), + completed: false, + }), + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: cal3.clone(), + name: String::from("Task A3"), + completed: false, + }), + } + ); - tasks.push(ItemScenario { - url: random_url(&cal4), - initial_state: LocatedState::Local(ItemState { - calendar: cal4.clone(), - name: String::from("Task A4"), - completed: false, - }), - local_changes_to_apply: Vec::new(), - remote_changes_to_apply: Vec::new(), - after_sync: LocatedState::BothSynced(ItemState { - calendar: cal4.clone(), - name: String::from("Task A4"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&cal4), + initial_state: LocatedState::Local( ItemState{ + calendar: cal4.clone(), + name: String::from("Task A4"), + completed: false, + }), + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: cal4.clone(), + name: String::from("Task A4"), + completed: false, + }), + } + ); - tasks.push(ItemScenario { - url: random_url(&cal3), - initial_state: LocatedState::Local(ItemState { - calendar: cal3.clone(), - name: String::from("Task B3"), - completed: false, - }), - local_changes_to_apply: Vec::new(), - remote_changes_to_apply: Vec::new(), - after_sync: LocatedState::BothSynced(ItemState { - calendar: cal3.clone(), - name: String::from("Task B3"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&cal3), + initial_state: LocatedState::Local( ItemState{ + calendar: cal3.clone(), + name: String::from("Task B3"), + completed: false, + }), + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: cal3.clone(), + name: String::from("Task B3"), + completed: false, + }), + } + ); tasks } + /// This scenario tests a task added and deleted before a sync happens pub fn scenarii_transient_task() -> Vec { let mut tasks = Vec::new(); - let cal = "https://some.calend.ar/transient/".parse().unwrap(); + let cal = Url::from("https://some.calend.ar/transient/".parse().unwrap()); - tasks.push(ItemScenario { - url: random_url(&cal), - initial_state: LocatedState::Local(ItemState { - calendar: cal.clone(), - name: String::from("A task, so that the calendar actually exists"), - completed: false, - }), - local_changes_to_apply: Vec::new(), - remote_changes_to_apply: Vec::new(), - after_sync: LocatedState::BothSynced(ItemState { - calendar: cal.clone(), - name: String::from("A task, so that the calendar actually exists"), - completed: false, - }), - }); + tasks.push( + ItemScenario { + url: random_url(&cal), + initial_state: LocatedState::Local( ItemState{ + calendar: cal.clone(), + name: String::from("A task, so that the calendar actually exists"), + completed: false, + }), + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: cal.clone(), + name: String::from("A task, so that the calendar actually exists"), + completed: false, + }), + } + ); let url_transient = random_url(&cal); - tasks.push(ItemScenario { - url: url_transient.clone(), - initial_state: LocatedState::None, - local_changes_to_apply: vec![ - ChangeToApply::Create( - cal, - Item::Task(Task::new_with_parameters( - String::from("A transient task that will be deleted before the sync"), - url_transient.to_string(), - url_transient, - CompletionStatus::Uncompleted, - SyncStatus::NotSynced, - Some(Utc::now()), - Utc::now(), - "prod_id".to_string(), - Vec::new(), + tasks.push( + ItemScenario { + url: url_transient.clone(), + initial_state: LocatedState::None, + local_changes_to_apply: vec![ + ChangeToApply::Create(cal, Item::Task( + Task::new_with_parameters( + String::from("A transient task that will be deleted before the sync"), + url_transient.to_string(), url_transient, + CompletionStatus::Uncompleted, + SyncStatus::NotSynced, Some(Utc::now()), Utc::now(), + "prod_id".to_string(), Vec::new() ) )), - ), - ChangeToApply::Rename(String::from("A new name")), - ChangeToApply::SetCompletion(true), - ChangeToApply::Remove, - ], - remote_changes_to_apply: Vec::new(), - after_sync: LocatedState::None, - }); + + ChangeToApply::Rename(String::from("A new name")), + ChangeToApply::SetCompletion(true), + ChangeToApply::Remove, + ], + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::None, + } + ); tasks } + /// Build a `Provider` that contains the data (defined in the given scenarii) before sync -pub async fn populate_test_provider_before_sync( - scenarii: &[ItemScenario], - mock_behaviour: Arc>, -) -> Provider { +pub async fn populate_test_provider_before_sync(scenarii: &[ItemScenario], mock_behaviour: Arc>) -> Provider { let mut provider = populate_test_provider(scenarii, mock_behaviour, false).await; apply_changes_on_provider(&mut provider, scenarii).await; provider } /// Build a `Provider` that contains the data (defined in the given scenarii) after sync -pub async fn populate_test_provider_after_sync( - scenarii: &[ItemScenario], - mock_behaviour: Arc>, -) -> Provider { +pub async fn populate_test_provider_after_sync(scenarii: &[ItemScenario], mock_behaviour: Arc>) -> Provider { populate_test_provider(scenarii, mock_behaviour, true).await } -async fn populate_test_provider( - scenarii: &[ItemScenario], - mock_behaviour: Arc>, - populate_for_final_state: bool, -) -> Provider { +async fn populate_test_provider(scenarii: &[ItemScenario], mock_behaviour: Arc>, populate_for_final_state: bool) -> Provider { let mut local = Cache::new(&PathBuf::from(String::from("test_cache/local/"))); let mut remote = Cache::new(&PathBuf::from(String::from("test_cache/remote/"))); remote.set_mock_behaviour(Some(mock_behaviour)); // Create the initial state, as if we synced both sources in a given state for item in scenarii { - let required_state = if populate_for_final_state { - &item.after_sync - } else { - &item.initial_state - }; + let required_state = if populate_for_final_state { &item.after_sync } else { &item.initial_state }; let (state, sync_status) = match required_state { LocatedState::None => continue, LocatedState::Local(s) => { - assert!( - !populate_for_final_state, - "You are not supposed to expect an item in this state after sync" - ); + assert!(populate_for_final_state == false, "You are not supposed to expect an item in this state after sync"); (s, SyncStatus::NotSynced) - } + }, LocatedState::Remote(s) => { - assert!( - !populate_for_final_state, - "You are not supposed to expect an item in this state after sync" - ); + assert!(populate_for_final_state == false, "You are not supposed to expect an item in this state after sync"); (s, SyncStatus::random_synced()) } LocatedState::BothSynced(s) => (s, SyncStatus::random_synced()), @@ -628,68 +634,37 @@ async fn populate_test_provider( true => CompletionStatus::Completed(Some(now)), }; - let new_item = Item::Task(Task::new_with_parameters( - state.name.clone(), - item.url.to_string(), - item.url.clone(), - completion_status, - sync_status, - Some(now), - now, - "prod_id".to_string(), - Vec::new(), - )); + let new_item = Item::Task( + Task::new_with_parameters( + state.name.clone(), + item.url.to_string(), + item.url.clone(), + completion_status, + sync_status, + Some(now), + now, + "prod_id".to_string(), Vec::new(), + )); match required_state { LocatedState::None => panic!("Should not happen, we've continued already"), LocatedState::Local(s) => { - get_or_insert_calendar(&mut local, &s.calendar) - .await - .unwrap() - .lock() - .unwrap() - .add_item(new_item) - .await - .unwrap(); - } + get_or_insert_calendar(&mut local, &s.calendar).await.unwrap().lock().unwrap().add_item(new_item).await.unwrap(); + }, LocatedState::Remote(s) => { - get_or_insert_calendar(&mut remote, &s.calendar) - .await - .unwrap() - .lock() - .unwrap() - .add_item(new_item) - .await - .unwrap(); - } + get_or_insert_calendar(&mut remote, &s.calendar).await.unwrap().lock().unwrap().add_item(new_item).await.unwrap(); + }, LocatedState::BothSynced(s) => { - get_or_insert_calendar(&mut local, &s.calendar) - .await - .unwrap() - .lock() - .unwrap() - .add_item(new_item.clone()) - .await - .unwrap(); - get_or_insert_calendar(&mut remote, &s.calendar) - .await - .unwrap() - .lock() - .unwrap() - .add_item(new_item) - .await - .unwrap(); - } + get_or_insert_calendar(&mut local, &s.calendar).await.unwrap().lock().unwrap().add_item(new_item.clone()).await.unwrap(); + get_or_insert_calendar(&mut remote, &s.calendar).await.unwrap().lock().unwrap().add_item(new_item).await.unwrap(); + }, } } Provider::new(remote, local) } /// Apply `local_changes_to_apply` and `remote_changes_to_apply` to a provider that contains data before sync -async fn apply_changes_on_provider( - provider: &mut Provider, - scenarii: &[ItemScenario], -) { +async fn apply_changes_on_provider(provider: &mut Provider, scenarii: &[ItemScenario]) { // Apply changes to each item for item in scenarii { let initial_calendar_url = match &item.initial_state { @@ -701,38 +676,19 @@ async fn apply_changes_on_provider( let mut calendar_url = initial_calendar_url.clone(); for local_change in &item.local_changes_to_apply { - calendar_url = Some( - apply_change( - provider.local(), - calendar_url, - &item.url, - local_change, - false, - ) - .await, - ); + calendar_url = Some(apply_change(provider.local(), calendar_url, &item.url, local_change, false).await); } let mut calendar_url = initial_calendar_url; for remote_change in &item.remote_changes_to_apply { - calendar_url = Some( - apply_change( - provider.remote(), - calendar_url, - &item.url, - remote_change, - true, - ) - .await, - ); + calendar_url = Some(apply_change(provider.remote(), calendar_url, &item.url, remote_change, true).await); } } } -async fn get_or_insert_calendar( - source: &mut Cache, - url: &Url, -) -> Result>, Box> { +async fn get_or_insert_calendar(source: &mut Cache, url: &Url) + -> Result>, Box> +{ match source.get_calendar(url).await { Some(cal) => Ok(cal), None => { @@ -740,26 +696,18 @@ async fn get_or_insert_calendar( let supported_components = SupportedComponents::TODO; let color = csscolorparser::parse("#ff8000").unwrap(); // TODO: we should rather have specific colors, depending on the calendars - source - .create_calendar( - url.clone(), - new_name.to_string(), - supported_components, - Some(color), - ) - .await + source.create_calendar( + url.clone(), + new_name.to_string(), + supported_components, + Some(color), + ).await } } } /// Apply a single change on a given source, and returns the calendar URL that was modified -async fn apply_change( - source: &S, - calendar_url: Option, - item_url: &Url, - change: &ChangeToApply, - is_remote: bool, -) -> Url +async fn apply_change(source: &S, calendar_url: Option, item_url: &Url, change: &ChangeToApply, is_remote: bool) -> Url where S: CalDavSource, C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds @@ -768,28 +716,21 @@ where Some(cal) => { apply_changes_on_an_existing_item(source, &cal, item_url, change, is_remote).await; cal - } - None => create_test_item(source, change).await, + }, + None => { + create_test_item(source, change).await + }, } } -async fn apply_changes_on_an_existing_item( - source: &S, - calendar_url: &Url, - item_url: &Url, - change: &ChangeToApply, - is_remote: bool, -) where +async fn apply_changes_on_an_existing_item(source: &S, calendar_url: &Url, item_url: &Url, change: &ChangeToApply, is_remote: bool) +where S: CalDavSource, C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds { let cal = source.get_calendar(calendar_url).await.unwrap(); let mut cal = cal.lock().unwrap(); - let task = cal - .get_item_by_url_mut(item_url) - .await - .unwrap() - .unwrap_task_mut(); + let task = cal.get_item_by_url_mut(item_url).await.unwrap().unwrap_task_mut(); match change { ChangeToApply::Rename(new_name) => { @@ -798,7 +739,7 @@ async fn apply_changes_on_an_existing_item( } else { task.set_name(new_name.clone()); } - } + }, ChangeToApply::SetCompletion(new_status) => { let completion_status = match new_status { false => CompletionStatus::Uncompleted, @@ -809,16 +750,16 @@ async fn apply_changes_on_an_existing_item( } else { task.set_completion_status(completion_status); } - } + }, ChangeToApply::Remove => { match is_remote { false => cal.mark_for_deletion(item_url).await.unwrap(), true => cal.delete_item(item_url).await.unwrap(), }; - } + }, ChangeToApply::Create(_calendar_url, _item) => { panic!("This function only handles already existing items"); - } + }, } } @@ -829,13 +770,15 @@ where C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds { match change { - ChangeToApply::Rename(_) | ChangeToApply::SetCompletion(_) | ChangeToApply::Remove => { + ChangeToApply::Rename(_) | + ChangeToApply::SetCompletion(_) | + ChangeToApply::Remove => { panic!("This function only creates items that do not exist yet"); } ChangeToApply::Create(calendar_url, item) => { let cal = source.get_calendar(calendar_url).await.unwrap(); cal.lock().unwrap().add_item(item.clone()).await.unwrap(); calendar_url.clone() - } + }, } } diff --git a/tests/sync.rs b/tests/sync.rs index 54fa8c4..cc46469 100644 --- a/tests/sync.rs +++ b/tests/sync.rs @@ -6,6 +6,8 @@ use std::sync::{Arc, Mutex}; #[cfg(feature = "local_calendar_mocks_remote_calendars")] use kitchen_fridge::mock_behaviour::MockBehaviour; + + /// A test that simulates a regular synchronisation between a local cache and a server. /// Note that this uses a second cache to "mock" a server. struct TestFlavour { @@ -17,54 +19,22 @@ struct TestFlavour { #[cfg(not(feature = "local_calendar_mocks_remote_calendars"))] impl TestFlavour { - pub fn normal() -> Self { - Self {} - } - pub fn first_sync_to_local() -> Self { - Self {} - } - pub fn first_sync_to_server() -> Self { - Self {} - } - pub fn transient_task() -> Self { - Self {} - } - pub fn normal_with_errors1() -> Self { - Self {} - } - pub fn normal_with_errors2() -> Self { - Self {} - } - pub fn normal_with_errors3() -> Self { - Self {} - } - pub fn normal_with_errors4() -> Self { - Self {} - } - pub fn normal_with_errors5() -> Self { - Self {} - } - pub fn normal_with_errors6() -> Self { - Self {} - } - pub fn normal_with_errors7() -> Self { - Self {} - } - pub fn normal_with_errors8() -> Self { - Self {} - } - pub fn normal_with_errors9() -> Self { - Self {} - } - pub fn normal_with_errors10() -> Self { - Self {} - } - pub fn normal_with_errors11() -> Self { - Self {} - } - pub fn normal_with_errors12() -> Self { - Self {} - } + pub fn normal() -> Self { Self{} } + pub fn first_sync_to_local() -> Self { Self{} } + pub fn first_sync_to_server() -> Self { Self{} } + pub fn transient_task() -> Self { Self{} } + pub fn normal_with_errors1() -> Self { Self{} } + pub fn normal_with_errors2() -> Self { Self{} } + pub fn normal_with_errors3() -> Self { Self{} } + pub fn normal_with_errors4() -> Self { Self{} } + pub fn normal_with_errors5() -> Self { Self{} } + pub fn normal_with_errors6() -> Self { Self{} } + pub fn normal_with_errors7() -> Self { Self{} } + pub fn normal_with_errors8() -> Self { Self{} } + pub fn normal_with_errors9() -> Self { Self{} } + pub fn normal_with_errors10() -> Self { Self{} } + pub fn normal_with_errors11() -> Self { Self{} } + pub fn normal_with_errors12() -> Self { Self{} } pub async fn run(&self, _max_attempts: u32) { panic!("WARNING: This test required the \"integration_tests\" Cargo feature"); @@ -111,9 +81,9 @@ impl TestFlavour { pub fn normal_with_errors2() -> Self { Self { scenarii: scenarii::scenarii_basic(), - mock_behaviour: Arc::new(Mutex::new(MockBehaviour { - get_calendars_behaviour: (0, 1), - create_calendar_behaviour: (2, 2), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour{ + get_calendars_behaviour: (0,1), + create_calendar_behaviour: (2,2), ..MockBehaviour::default() })), } @@ -122,9 +92,9 @@ impl TestFlavour { pub fn normal_with_errors3() -> Self { Self { scenarii: scenarii::scenarii_first_sync_to_server(), - mock_behaviour: Arc::new(Mutex::new(MockBehaviour { - get_calendars_behaviour: (1, 6), - create_calendar_behaviour: (0, 1), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour{ + get_calendars_behaviour: (1,6), + create_calendar_behaviour: (0,1), ..MockBehaviour::default() })), } @@ -133,8 +103,8 @@ impl TestFlavour { pub fn normal_with_errors4() -> Self { Self { scenarii: scenarii::scenarii_first_sync_to_server(), - mock_behaviour: Arc::new(Mutex::new(MockBehaviour { - add_item_behaviour: (1, 3), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour{ + add_item_behaviour: (1,3), ..MockBehaviour::default() })), } @@ -143,8 +113,8 @@ impl TestFlavour { pub fn normal_with_errors5() -> Self { Self { scenarii: scenarii::scenarii_basic(), - mock_behaviour: Arc::new(Mutex::new(MockBehaviour { - get_item_version_tags_behaviour: (0, 1), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour{ + get_item_version_tags_behaviour: (0,1), ..MockBehaviour::default() })), } @@ -153,8 +123,8 @@ impl TestFlavour { pub fn normal_with_errors6() -> Self { Self { scenarii: scenarii::scenarii_basic(), - mock_behaviour: Arc::new(Mutex::new(MockBehaviour { - get_item_by_url_behaviour: (3, 2), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour{ + get_item_by_url_behaviour: (3,2), ..MockBehaviour::default() })), } @@ -163,8 +133,8 @@ impl TestFlavour { pub fn normal_with_errors7() -> Self { Self { scenarii: scenarii::scenarii_basic(), - mock_behaviour: Arc::new(Mutex::new(MockBehaviour { - delete_item_behaviour: (0, 2), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour{ + delete_item_behaviour: (0,2), ..MockBehaviour::default() })), } @@ -173,9 +143,9 @@ impl TestFlavour { pub fn normal_with_errors8() -> Self { Self { scenarii: scenarii::scenarii_basic(), - mock_behaviour: Arc::new(Mutex::new(MockBehaviour { - add_item_behaviour: (2, 3), - get_item_by_url_behaviour: (1, 12), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour{ + add_item_behaviour: (2,3), + get_item_by_url_behaviour: (1,12), ..MockBehaviour::default() })), } @@ -184,9 +154,9 @@ impl TestFlavour { pub fn normal_with_errors9() -> Self { Self { scenarii: scenarii::scenarii_basic(), - mock_behaviour: Arc::new(Mutex::new(MockBehaviour { - get_calendars_behaviour: (0, 8), - delete_item_behaviour: (1, 1), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour{ + get_calendars_behaviour: (0,8), + delete_item_behaviour: (1,1), ..MockBehaviour::default() })), } @@ -195,11 +165,11 @@ impl TestFlavour { pub fn normal_with_errors10() -> Self { Self { scenarii: scenarii::scenarii_first_sync_to_server(), - mock_behaviour: Arc::new(Mutex::new(MockBehaviour { - get_calendars_behaviour: (0, 8), - delete_item_behaviour: (1, 1), - create_calendar_behaviour: (1, 4), - get_item_version_tags_behaviour: (3, 1), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour{ + get_calendars_behaviour: (0,8), + delete_item_behaviour: (1,1), + create_calendar_behaviour: (1,4), + get_item_version_tags_behaviour: (3,1), ..MockBehaviour::default() })), } @@ -208,12 +178,12 @@ impl TestFlavour { pub fn normal_with_errors11() -> Self { Self { scenarii: scenarii::scenarii_basic(), - mock_behaviour: Arc::new(Mutex::new(MockBehaviour { - get_calendars_behaviour: (0, 8), - delete_item_behaviour: (1, 1), - create_calendar_behaviour: (1, 4), - get_item_version_tags_behaviour: (3, 1), - get_item_by_url_behaviour: (0, 41), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour{ + get_calendars_behaviour: (0,8), + delete_item_behaviour: (1,1), + create_calendar_behaviour: (1,4), + get_item_version_tags_behaviour: (3,1), + get_item_by_url_behaviour: (0,41), ..MockBehaviour::default() })), } @@ -222,29 +192,26 @@ impl TestFlavour { pub fn normal_with_errors12() -> Self { Self { scenarii: scenarii::scenarii_basic(), - mock_behaviour: Arc::new(Mutex::new(MockBehaviour { - update_item_behaviour: (0, 3), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour{ + update_item_behaviour: (0,3), ..MockBehaviour::default() })), } } + pub async fn run(&self, max_attempts: u32) { self.mock_behaviour.lock().unwrap().suspend(); - let mut provider = scenarii::populate_test_provider_before_sync( - &self.scenarii, - Arc::clone(&self.mock_behaviour), - ) - .await; + let mut provider = scenarii::populate_test_provider_before_sync(&self.scenarii, Arc::clone(&self.mock_behaviour)).await; print_provider(&provider, "before sync").await; self.mock_behaviour.lock().unwrap().resume(); for attempt in 0..max_attempts { println!("\nSyncing...\n"); - if provider.sync().await { + if provider.sync().await == true { println!("Sync complete after {} attempts (multiple attempts are due to forced errors in mocked behaviour)", attempt+1); - break; + break } } self.mock_behaviour.lock().unwrap().suspend(); @@ -252,160 +219,136 @@ impl TestFlavour { print_provider(&provider, "after sync").await; // Check the contents of both sources are the same after sync - assert!(provider - .remote() - .has_same_observable_content_as(provider.local()) - .await - .unwrap()); + assert!(provider.remote().has_same_observable_content_as(provider.local()).await.unwrap()); // But also explicitely check that every item is expected - let expected_provider = scenarii::populate_test_provider_after_sync( - &self.scenarii, - Arc::clone(&self.mock_behaviour), - ) - .await; + let expected_provider = scenarii::populate_test_provider_after_sync(&self.scenarii, Arc::clone(&self.mock_behaviour)).await; - assert!(provider - .local() - .has_same_observable_content_as(expected_provider.local()) - .await - .unwrap()); - assert!(provider - .remote() - .has_same_observable_content_as(expected_provider.remote()) - .await - .unwrap()); + assert!(provider.local() .has_same_observable_content_as(expected_provider.local() ).await.unwrap()); + assert!(provider.remote().has_same_observable_content_as(expected_provider.remote()).await.unwrap()); // Perform a second sync, even if no change has happened, just to check println!("Syncing again"); provider.sync().await; - assert!(provider - .local() - .has_same_observable_content_as(expected_provider.local()) - .await - .unwrap()); - assert!(provider - .remote() - .has_same_observable_content_as(expected_provider.remote()) - .await - .unwrap()); + assert!(provider.local() .has_same_observable_content_as(expected_provider.local() ).await.unwrap()); + assert!(provider.remote().has_same_observable_content_as(expected_provider.remote()).await.unwrap()); } } + + async fn run_flavour(flavour: TestFlavour, max_attempts: u32) { let _ = env_logger::builder().is_test(true).try_init(); flavour.run(max_attempts).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_regular_sync() { run_flavour(TestFlavour::normal(), 1).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_sync_empty_initial_local() { run_flavour(TestFlavour::first_sync_to_local(), 1).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_sync_empty_initial_server() { run_flavour(TestFlavour::first_sync_to_server(), 1).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_sync_transient_task() { run_flavour(TestFlavour::transient_task(), 1).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_errors_in_regular_sync1() { run_flavour(TestFlavour::normal_with_errors1(), 100).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_errors_in_regular_sync2() { run_flavour(TestFlavour::normal_with_errors2(), 100).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_errors_in_regular_sync3() { run_flavour(TestFlavour::normal_with_errors3(), 100).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_errors_in_regular_sync4() { run_flavour(TestFlavour::normal_with_errors4(), 100).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_errors_in_regular_sync5() { run_flavour(TestFlavour::normal_with_errors5(), 100).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_errors_in_regular_sync6() { run_flavour(TestFlavour::normal_with_errors6(), 100).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_errors_in_regular_sync7() { run_flavour(TestFlavour::normal_with_errors7(), 100).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_errors_in_regular_sync8() { run_flavour(TestFlavour::normal_with_errors8(), 100).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_errors_in_regular_sync9() { run_flavour(TestFlavour::normal_with_errors9(), 100).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_errors_in_regular_sync10() { run_flavour(TestFlavour::normal_with_errors10(), 100).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_errors_in_regular_sync11() { run_flavour(TestFlavour::normal_with_errors11(), 100).await; } #[tokio::test] -#[cfg_attr(not(feature = "integration_tests"), ignore)] +#[cfg_attr(not(feature="integration_tests"), ignore)] async fn test_errors_in_regular_sync12() { run_flavour(TestFlavour::normal_with_errors12(), 100).await; } #[cfg(feature = "integration_tests")] -use kitchen_fridge::{ - cache::Cache, calendar::cached_calendar::CachedCalendar, provider::Provider, - traits::CalDavSource, +use kitchen_fridge::{traits::CalDavSource, + provider::Provider, + cache::Cache, + calendar::cached_calendar::CachedCalendar, }; /// Print the contents of the provider. This is usually used for debugging #[allow(dead_code)] #[cfg(feature = "integration_tests")] -async fn print_provider( - provider: &Provider, - title: &str, -) { +async fn print_provider(provider: &Provider, title: &str) { let cals_server = provider.remote().get_calendars().await.unwrap(); println!("----Server, {}-------", title); kitchen_fridge::utils::print_calendar_list(&cals_server).await;