diff --git a/examples/provider-sync.rs b/examples/provider-sync.rs index a117272..06b2efe 100644 --- a/examples/provider-sync.rs +++ b/examples/provider-sync.rs @@ -1,38 +1,45 @@ //! This is an example of how kitchen-fridge can be used -use chrono::{Utc}; +use chrono::Utc; use url::Url; -use kitchen_fridge::traits::CalDavSource; 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::CalDavSource; use kitchen_fridge::traits::CompleteCalendar; use kitchen_fridge::utils::pause; +use kitchen_fridge::CalDavProvider; +use kitchen_fridge::Item; +use kitchen_fridge::Task; mod shared; use shared::initial_sync; -use shared::{URL, USERNAME, EXAMPLE_EXISTING_CALENDAR_URL, EXAMPLE_CREATED_CALENDAR_URL}; +use shared::{EXAMPLE_CREATED_CALENDAR_URL, EXAMPLE_EXISTING_CALENDAR_URL, URL, USERNAME}; 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!( + "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; @@ -47,32 +54,56 @@ 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) { 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(); @@ -82,14 +113,22 @@ 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); @@ -106,15 +145,22 @@ 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) { 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."); diff --git a/src/calendar/cached_calendar.rs b/src/calendar/cached_calendar.rs index 6d665c6..0254e03 100644 --- a/src/calendar/cached_calendar.rs +++ b/src/calendar/cached_calendar.rs @@ -1,21 +1,20 @@ 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 std::sync::{Arc, Mutex}; #[cfg(feature = "local_calendar_mocks_remote_calendars")] use crate::mock_behaviour::MockBehaviour; - +#[cfg(feature = "local_calendar_mocks_remote_calendars")] +use std::sync::{Arc, Mutex}; /// A calendar used by the [`cache`](crate::cache) module /// @@ -41,11 +40,12 @@ 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,7 +55,9 @@ 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) @@ -72,7 +74,10 @@ 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(_) => (), @@ -85,21 +90,23 @@ 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) == false { + if !crate::utils::keys_are_the_same(&items_l, &items_r) { log::debug!("Different keys for items"); return Ok(false); } @@ -108,7 +115,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) == false { + if !item_l.has_same_observable_content_as(item_r) { log::debug!("Different items for URL {}:", url_l); log::debug!("{:#?}", item_l); log::debug!("{:#?}", item_r); @@ -121,26 +128,25 @@ impl CachedCalendar { /// The non-async version of [`Self::get_item_urls`] pub fn get_item_urls_sync(&self) -> Result, Box> { - Ok(self.items.iter() - .map(|(url, _)| url.clone()) - .collect() - ) + Ok(self.items.keys().cloned().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`] @@ -167,8 +173,12 @@ 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()) == false { - return Err(format!("Item {:?} cannot be updated, it does not already exist", item.url()).into()); + if !self.items.contains_key(item.url()) { + 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); @@ -185,20 +195,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(()) } @@ -209,13 +219,11 @@ 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 { @@ -245,9 +253,17 @@ 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(), @@ -283,25 +299,33 @@ 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, - traits::DavCalendar, - resource::Resource}; +use crate::{item::VersionTag, resource::Resource, traits::DavCalendar}; #[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; @@ -311,7 +335,10 @@ 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); @@ -322,7 +349,9 @@ 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()) } @@ -337,7 +366,9 @@ 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/event.rs b/src/event.rs index 2821242..2d55f9a 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,7 +1,7 @@ //! Calendar events (iCal `VEVENT` items) -use serde::{Deserialize, Serialize}; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; use url::Url; use crate::item::SyncStatus; @@ -56,3 +56,9 @@ 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 acf408a..8894cd9 100644 --- a/src/ical/parser.rs +++ b/src/ical/parser.rs @@ -2,36 +2,41 @@ use std::error::Error; -use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo}; use chrono::{DateTime, TimeZone, Utc}; +use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo}; 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; @@ -44,8 +49,8 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result { 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 @@ -53,7 +58,7 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result { // The property can be specified once, but is not mandatory // "This property specifies the date and time that the information associated with @@ -66,11 +71,11 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result { // 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. @@ -97,7 +102,13 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result 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 => { @@ -105,15 +116,24 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result 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()); @@ -123,33 +143,30 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result 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_ref().map(|s| s.as_str()) + return prop.value.as_deref(); } } None } - enum CurrentType<'a> { Event(&'a IcalEvent), Todo(&'a IcalTodo), @@ -176,10 +193,9 @@ fn assert_single_type<'a>(item: &'a IcalCalendar) -> Result, Box } } - return Err("Only a single TODO or a single EVENT is supported".into()); + Err("Only a single TODO or a single EVENT is supported".into()) } - #[cfg(test)] mod test { const EXAMPLE_ICAL: &str = r#"BEGIN:VCALENDAR @@ -195,7 +211,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 @@ -211,7 +227,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 @@ -261,11 +277,17 @@ 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_eq!(task.completed(), false); + assert_eq!( + task.uid(), + "0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com" + ); + assert!(!task.completed()); 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] @@ -274,11 +296,19 @@ 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_eq!(task.completed(), true); - assert_eq!(task.completion_status(), &CompletionStatus::Completed(Some(Utc.ymd(2021, 04, 02).and_hms(8, 15, 57)))); + assert!(task.completed()); + assert_eq!( + task.completion_status(), + &CompletionStatus::Completed(Some(Utc.ymd(2021, 04, 02).and_hms(8, 15, 57))) + ); } #[test] @@ -287,10 +317,15 @@ 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_eq!(task.completed(), true); + assert!(task.completed()); assert_eq!(task.completion_status(), &CompletionStatus::Completed(None)); } diff --git a/src/item.rs b/src/item.rs index 8d457d0..18348f5 100644 --- a/src/item.rs +++ b/src/item.rs @@ -1,10 +1,9 @@ //! 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 { @@ -21,7 +20,7 @@ macro_rules! synthetise_common_getter { Item::Task(t) => t.$property_name(), } } - } + }; } impl Item { @@ -41,17 +40,11 @@ impl Item { } pub fn is_event(&self) -> bool { - match &self { - Item::Event(_) => true, - _ => false, - } + matches!(self, Item::Event(_)) } pub fn is_task(&self) -> bool { - match &self { - Item::Task(_) => true, - _ => false, - } + matches!(self, Item::Task(_)) } /// Returns a mutable reference to the inner Task @@ -80,19 +73,16 @@ 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 { @@ -115,8 +105,6 @@ 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 c5c8bd1..070436a 100644 --- a/src/mock_behaviour.rs +++ b/src/mock_behaviour.rs @@ -61,7 +61,9 @@ 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> { @@ -69,50 +71,66 @@ 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 = value.0 - 1; + 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 { - 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(()) - } + log::debug!("Mock behaviour: allowing a {} ({:?})", descr, value); + Ok(()) } } @@ -140,9 +158,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 c73b503..324aac3 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::error::Error; use std::collections::HashSet; +use std::error::Error; +use std::fmt::{Display, Formatter}; use std::marker::PhantomData; use std::sync::{Arc, Mutex}; -use std::fmt::{Display, Formatter}; -use url::Url; use itertools::Itertools; +use url::Url; -use crate::traits::{BaseCalendar, CalDavSource, DavCalendar}; -use crate::traits::CompleteCalendar; use crate::item::SyncStatus; +use crate::traits::CompleteCalendar; +use crate::traits::{BaseCalendar, CalDavSource, DavCalendar}; pub mod sync_progress; use sync_progress::SyncProgress; @@ -42,7 +42,6 @@ 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`. \ @@ -76,21 +75,30 @@ 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. /// @@ -117,7 +125,9 @@ 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() } @@ -130,16 +140,22 @@ 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); @@ -152,16 +168,22 @@ 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; } } @@ -171,26 +193,36 @@ 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 @@ -203,7 +235,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()), @@ -217,24 +249,27 @@ 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) == false { - progress.error(&format!("Inconsistent state: missing task {} from the local tasks", url)); + if !local_items_to_handle.remove(&url) { + 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 @@ -242,10 +277,11 @@ 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 @@ -253,10 +289,11 @@ 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); } - }, + } } } } @@ -267,9 +304,12 @@ 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, }; @@ -278,31 +318,33 @@ 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, @@ -310,21 +352,27 @@ 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, @@ -339,22 +387,26 @@ 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, @@ -363,23 +415,29 @@ 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, @@ -388,14 +446,17 @@ 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); - }, + } }; } }; @@ -404,9 +465,12 @@ 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( @@ -414,10 +478,22 @@ 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; } } @@ -426,10 +502,22 @@ 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; } } @@ -439,60 +527,74 @@ 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(&format!("Inconsistency: an item from the batch has vanished from the remote end")); + progress.error("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.get(0) { - Some(url) => Self::item_name(&cal_local, &url).await, + let one_item_name = match list_of_additions.first() { + 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); } @@ -502,14 +604,11 @@ 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 a7ef98f..6599617 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -10,17 +10,27 @@ 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 2545abb..a1ec877 100644 --- a/src/task.rs +++ b/src/task.rs @@ -1,20 +1,24 @@ //! 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>), @@ -22,10 +26,7 @@ pub enum CompletionStatus { } impl CompletionStatus { pub fn is_completed(&self) -> bool { - match self { - CompletionStatus::Completed(_) => true, - _ => false, - } + matches!(self, CompletionStatus::Completed(_)) } } @@ -53,7 +54,6 @@ pub struct Task { /// The display name of the task name: String, - /// The PRODID, as defined in iCal files ical_prod_id: String, @@ -62,7 +62,6 @@ 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. @@ -73,20 +72,37 @@ 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, @@ -100,20 +116,40 @@ 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 @@ -129,15 +165,13 @@ impl Task { fn update_sync_status(&mut self) { match &self.sync_status { - SyncStatus::NotSynced => return, - SyncStatus::LocallyModified(_) => return, + SyncStatus::NotSynced | SyncStatus::LocallyModified(_) => (), 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; - }, + } } } @@ -145,7 +179,6 @@ 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) { @@ -169,7 +202,10 @@ 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 3bcffe3..7e33304 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,17 +1,17 @@ //! Traits used by multiple structs in this crate -use std::error::Error; use std::collections::{HashMap, HashSet}; +use std::error::Error; use std::sync::{Arc, Mutex}; use async_trait::async_trait; use csscolorparser::Color; use url::Url; -use crate::item::SyncStatus; -use crate::item::Item; -use crate::item::VersionTag; use crate::calendar::SupportedComponents; +use crate::item::Item; +use crate::item::SyncStatus; +use crate::item::VersionTag; use crate::resource::Resource; /// This trait must be implemented by data sources (either local caches or remote CalDAV clients) @@ -25,8 +25,13 @@ 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 } @@ -59,23 +64,29 @@ 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>; @@ -93,25 +104,27 @@ 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.iter() - .map(|(url, _tag)| url.clone()) - .collect()) + Ok(items.keys().cloned().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 5f917ae..3ce99e7 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,14 +49,10 @@ 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]); } @@ -74,7 +70,7 @@ where for (_, item) in map { print_task(item); } - }, + } } } } @@ -92,28 +88,24 @@ where for (url, version_tag) in map { println!(" * {} (version {:?})", url, version_tag); } - }, + } } } } pub fn print_task(item: &Item) { - 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, + 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()); } } - /// Compare keys of two hashmaps for equality pub fn keys_are_the_same(left: &HashMap, right: &HashMap) -> bool where @@ -127,7 +119,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 == false { + if !result { log::debug!("Keys of a map mismatch"); for key in keys_l { log::debug!(" left: {}", key); @@ -140,7 +132,6 @@ where result } - /// Wait for the user to press enter pub fn pause() { let mut stdout = stdout(); @@ -149,7 +140,6 @@ 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 c8a0e9a..c62526e 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::traits::CalDavSource; +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::CompleteCalendar; use kitchen_fridge::traits::DavCalendar; -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; +use kitchen_fridge::Item; +use kitchen_fridge::Task; pub enum LocatedState { /// Item does not exist yet or does not exist anymore @@ -60,11 +60,10 @@ 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, } @@ -87,329 +86,317 @@ pub struct ItemScenario { pub fn scenarii_basic() -> Vec { let mut tasks = Vec::new(); - 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()); + 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(); - 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 } @@ -418,62 +405,56 @@ pub fn scenarii_basic() -> Vec { pub fn scenarii_first_sync_to_local() -> Vec { let mut tasks = Vec::new(); - let cal1 = Url::from("https://some.calend.ar/first/".parse().unwrap()); - let cal2 = Url::from("https://some.calend.ar/second/".parse().unwrap()); + let cal1 = "https://some.calend.ar/first/".parse().unwrap(); + let cal2 = "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 } @@ -482,147 +463,160 @@ pub fn scenarii_first_sync_to_local() -> Vec { pub fn scenarii_first_sync_to_server() -> Vec { let mut tasks = Vec::new(); - let cal3 = Url::from("https://some.calend.ar/third/".parse().unwrap()); - let cal4 = Url::from("https://some.calend.ar/fourth/".parse().unwrap()); + let cal3 = "https://some.calend.ar/third/".parse().unwrap(); + let cal4 = "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 = Url::from("https://some.calend.ar/transient/".parse().unwrap()); + let cal = "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 == false, "You are not supposed to expect an item in this state after sync"); + assert!( + !populate_for_final_state, + "You are not supposed to expect an item in this state after sync" + ); (s, SyncStatus::NotSynced) - }, + } LocatedState::Remote(s) => { - assert!(populate_for_final_state == false, "You are not supposed to expect an item in this state after sync"); + assert!( + !populate_for_final_state, + "You are not supposed to expect an item in this state after sync" + ); (s, SyncStatus::random_synced()) } LocatedState::BothSynced(s) => (s, SyncStatus::random_synced()), @@ -634,37 +628,68 @@ async fn populate_test_provider(scenarii: &[ItemScenario], mock_behaviour: Arc 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 { @@ -676,19 +701,38 @@ async fn apply_changes_on_provider(provider: &mut Provider 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 => { @@ -696,18 +740,26 @@ async fn get_or_insert_calendar(source: &mut Cache, url: &Url) 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 @@ -716,21 +768,28 @@ 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) => { @@ -739,7 +798,7 @@ where } else { task.set_name(new_name.clone()); } - }, + } ChangeToApply::SetCompletion(new_status) => { let completion_status = match new_status { false => CompletionStatus::Uncompleted, @@ -750,16 +809,16 @@ where } 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"); - }, + } } } @@ -770,15 +829,13 @@ 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 cc46469..54fa8c4 100644 --- a/tests/sync.rs +++ b/tests/sync.rs @@ -6,8 +6,6 @@ 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 { @@ -19,22 +17,54 @@ 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"); @@ -81,9 +111,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() })), } @@ -92,9 +122,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() })), } @@ -103,8 +133,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() })), } @@ -113,8 +143,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() })), } @@ -123,8 +153,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() })), } @@ -133,8 +163,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() })), } @@ -143,9 +173,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() })), } @@ -154,9 +184,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() })), } @@ -165,11 +195,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() })), } @@ -178,12 +208,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() })), } @@ -192,26 +222,29 @@ 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 == true { + if provider.sync().await { 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(); @@ -219,136 +252,160 @@ 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::{traits::CalDavSource, - provider::Provider, - cache::Cache, - calendar::cached_calendar::CachedCalendar, +use kitchen_fridge::{ + cache::Cache, calendar::cached_calendar::CachedCalendar, provider::Provider, + traits::CalDavSource, }; /// 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;