Merge branch 'deprecate_id_for_uid'

This commit is contained in:
daladim 2021-11-16 00:11:33 +01:00
commit 38a44e2773
17 changed files with 375 additions and 440 deletions

View file

@ -3,13 +3,13 @@
use std::path::Path; use std::path::Path;
use chrono::{Utc}; use chrono::{Utc};
use url::Url;
use kitchen_fridge::{client::Client, traits::CalDavSource}; use kitchen_fridge::{client::Client, traits::CalDavSource};
use kitchen_fridge::calendar::{CalendarId, SupportedComponents}; use kitchen_fridge::calendar::SupportedComponents;
use kitchen_fridge::Item; use kitchen_fridge::Item;
use kitchen_fridge::Task; use kitchen_fridge::Task;
use kitchen_fridge::task::CompletionStatus; use kitchen_fridge::task::CompletionStatus;
use kitchen_fridge::item::ItemId;
use kitchen_fridge::cache::Cache; use kitchen_fridge::cache::Cache;
use kitchen_fridge::CalDavProvider; use kitchen_fridge::CalDavProvider;
use kitchen_fridge::traits::BaseCalendar; use kitchen_fridge::traits::BaseCalendar;
@ -81,27 +81,27 @@ async fn add_items_and_sync_again(provider: &mut CalDavProvider)
pause(); pause();
// Create a new calendar... // Create a new calendar...
let new_calendar_id: CalendarId = EXAMPLE_CREATED_CALENDAR_URL.parse().unwrap(); let new_calendar_url: Url = EXAMPLE_CREATED_CALENDAR_URL.parse().unwrap();
let new_calendar_name = "A brave new calendar".to_string(); let new_calendar_name = "A brave new calendar".to_string();
if let Err(_err) = provider.local_mut() if let Err(_err) = provider.local_mut()
.create_calendar(new_calendar_id.clone(), new_calendar_name.clone(), SupportedComponents::TODO, None) .create_calendar(new_calendar_url.clone(), new_calendar_name.clone(), SupportedComponents::TODO, None)
.await { .await {
println!("Unable to add calendar, maybe it exists already. We're not adding it after all."); println!("Unable to add calendar, maybe it exists already. We're not adding it after all.");
} }
// ...and add a task in it // ...and add a task in it
let new_name = "This is a new task in a new calendar"; let new_name = "This is a new task in a new calendar";
let new_task = Task::new(String::from(new_name), true, &new_calendar_id); let new_task = Task::new(String::from(new_name), true, &new_calendar_url);
provider.local().get_calendar(&new_calendar_id).await.unwrap() provider.local().get_calendar(&new_calendar_url).await.unwrap()
.lock().unwrap().add_item(Item::Task(new_task)).await.unwrap(); .lock().unwrap().add_item(Item::Task(new_task)).await.unwrap();
// Also create a task in a previously existing calendar // Also create a task in a previously existing calendar
let changed_calendar_id: CalendarId = EXAMPLE_EXISTING_CALENDAR_URL.parse().unwrap(); 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_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_id); let new_task = Task::new(String::from(new_task_name), false, &changed_calendar_url);
let new_id = new_task.id().clone(); let new_url = new_task.url().clone();
provider.local().get_calendar(&changed_calendar_id).await.unwrap() provider.local().get_calendar(&changed_calendar_url).await.unwrap()
.lock().unwrap().add_item(Item::Task(new_task)).await.unwrap(); .lock().unwrap().add_item(Item::Task(new_task)).await.unwrap();
@ -112,20 +112,20 @@ async fn add_items_and_sync_again(provider: &mut CalDavProvider)
} }
provider.local().save_to_folder().unwrap(); provider.local().save_to_folder().unwrap();
complete_item_and_sync_again(provider, &changed_calendar_id, &new_id).await; complete_item_and_sync_again(provider, &changed_calendar_url, &new_url).await;
} }
async fn complete_item_and_sync_again( async fn complete_item_and_sync_again(
provider: &mut CalDavProvider, provider: &mut CalDavProvider,
changed_calendar_id: &CalendarId, changed_calendar_url: &Url,
id_to_complete: &ItemId) url_to_complete: &Url)
{ {
println!("\nNow, we'll mark this last task as completed, and run the sync again."); println!("\nNow, we'll mark this last task as completed, and run the sync again.");
pause(); pause();
let completion_status = CompletionStatus::Completed(Some(Utc::now())); let completion_status = CompletionStatus::Completed(Some(Utc::now()));
provider.local().get_calendar(changed_calendar_id).await.unwrap() provider.local().get_calendar(changed_calendar_url).await.unwrap()
.lock().unwrap().get_item_by_id_mut(id_to_complete).await.unwrap() .lock().unwrap().get_item_by_url_mut(url_to_complete).await.unwrap()
.unwrap_task_mut() .unwrap_task_mut()
.set_completion_status(completion_status); .set_completion_status(completion_status);
@ -136,19 +136,19 @@ async fn complete_item_and_sync_again(
} }
provider.local().save_to_folder().unwrap(); provider.local().save_to_folder().unwrap();
remove_items_and_sync_again(provider, changed_calendar_id, id_to_complete).await; remove_items_and_sync_again(provider, changed_calendar_url, url_to_complete).await;
} }
async fn remove_items_and_sync_again( async fn remove_items_and_sync_again(
provider: &mut CalDavProvider, provider: &mut CalDavProvider,
changed_calendar_id: &CalendarId, changed_calendar_url: &Url,
id_to_remove: &ItemId) id_to_remove: &Url)
{ {
println!("\nNow, we'll delete this last task, and run the sync again."); println!("\nNow, we'll delete this last task, and run the sync again.");
pause(); pause();
// Remove the task we had created // Remove the task we had created
provider.local().get_calendar(changed_calendar_id).await.unwrap() provider.local().get_calendar(changed_calendar_url).await.unwrap()
.lock().unwrap() .lock().unwrap()
.mark_for_deletion(id_to_remove).await.unwrap(); .mark_for_deletion(id_to_remove).await.unwrap();

View file

@ -10,12 +10,12 @@ use std::ffi::OsStr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use async_trait::async_trait; use async_trait::async_trait;
use csscolorparser::Color; use csscolorparser::Color;
use url::Url;
use crate::traits::CalDavSource; use crate::traits::CalDavSource;
use crate::traits::BaseCalendar; use crate::traits::BaseCalendar;
use crate::traits::CompleteCalendar; use crate::traits::CompleteCalendar;
use crate::calendar::cached_calendar::CachedCalendar; use crate::calendar::cached_calendar::CachedCalendar;
use crate::calendar::CalendarId;
use crate::calendar::SupportedComponents; use crate::calendar::SupportedComponents;
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
@ -41,7 +41,7 @@ pub struct Cache {
#[derive(Default, Debug, Serialize, Deserialize)] #[derive(Default, Debug, Serialize, Deserialize)]
struct CachedData { struct CachedData {
#[serde(skip)] #[serde(skip)]
calendars: HashMap<CalendarId, Arc<Mutex<CachedCalendar>>>, calendars: HashMap<Url, Arc<Mutex<CachedCalendar>>>,
} }
impl Cache { impl Cache {
@ -86,7 +86,7 @@ impl Cache {
continue; continue;
}, },
Ok(cal) => Ok(cal) =>
data.calendars.insert(cal.id().clone(), Arc::new(Mutex::new(cal))), data.calendars.insert(cal.url().clone(), Arc::new(Mutex::new(cal))),
}; };
} }
}, },
@ -131,8 +131,8 @@ impl Cache {
serde_json::to_writer(file, &self.data)?; serde_json::to_writer(file, &self.data)?;
// Save each calendar // Save each calendar
for (cal_id, cal_mutex) in &self.data.calendars { for (cal_url, cal_mutex) in &self.data.calendars {
let file_name = sanitize_filename::sanitize(cal_id.as_str()) + ".cal"; let file_name = sanitize_filename::sanitize(cal_url.as_str()) + ".cal";
let cal_file = folder.join(file_name); let cal_file = folder.join(file_name);
let file = std::fs::File::create(&cal_file)?; let file = std::fs::File::create(&cal_file)?;
let cal = cal_mutex.lock().unwrap(); let cal = cal_mutex.lock().unwrap();
@ -156,10 +156,10 @@ impl Cache {
return Ok(false); return Ok(false);
} }
for (calendar_id, cal_l) in calendars_l { for (calendar_url, cal_l) in calendars_l {
log::debug!("Comparing calendars {}", calendar_id); log::debug!("Comparing calendars {}", calendar_url);
let cal_l = cal_l.lock().unwrap(); let cal_l = cal_l.lock().unwrap();
let cal_r = match calendars_r.get(&calendar_id) { let cal_r = match calendars_r.get(&calendar_url) {
Some(c) => c.lock().unwrap(), Some(c) => c.lock().unwrap(),
None => return Err("should not happen, we've just tested keys are the same".into()), None => return Err("should not happen, we've just tested keys are the same".into()),
}; };
@ -185,38 +185,38 @@ impl Drop for Cache {
impl Cache { impl Cache {
/// The non-async version of [`crate::traits::CalDavSource::get_calendars`] /// The non-async version of [`crate::traits::CalDavSource::get_calendars`]
pub fn get_calendars_sync(&self) -> Result<HashMap<CalendarId, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> { pub fn get_calendars_sync(&self) -> Result<HashMap<Url, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> {
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_calendars())?; self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_calendars())?;
Ok(self.data.calendars.iter() Ok(self.data.calendars.iter()
.map(|(id, cal)| (id.clone(), cal.clone())) .map(|(url, cal)| (url.clone(), cal.clone()))
.collect() .collect()
) )
} }
/// The non-async version of [`crate::traits::CalDavSource::get_calendar`] /// The non-async version of [`crate::traits::CalDavSource::get_calendar`]
pub fn get_calendar_sync(&self, id: &CalendarId) -> Option<Arc<Mutex<CachedCalendar>>> { pub fn get_calendar_sync(&self, url: &Url) -> Option<Arc<Mutex<CachedCalendar>>> {
self.data.calendars.get(id).map(|arc| arc.clone()) self.data.calendars.get(url).map(|arc| arc.clone())
} }
} }
#[async_trait] #[async_trait]
impl CalDavSource<CachedCalendar> for Cache { impl CalDavSource<CachedCalendar> for Cache {
async fn get_calendars(&self) -> Result<HashMap<CalendarId, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> { async fn get_calendars(&self) -> Result<HashMap<Url, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> {
self.get_calendars_sync() self.get_calendars_sync()
} }
async fn get_calendar(&self, id: &CalendarId) -> Option<Arc<Mutex<CachedCalendar>>> { async fn get_calendar(&self, url: &Url) -> Option<Arc<Mutex<CachedCalendar>>> {
self.get_calendar_sync(id) self.get_calendar_sync(url)
} }
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents, color: Option<Color>) -> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>> { async fn create_calendar(&mut self, url: Url, name: String, supported_components: SupportedComponents, color: Option<Color>) -> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>> {
log::debug!("Inserting local calendar {}", id); log::debug!("Inserting local calendar {}", url);
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_create_calendar())?; self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_create_calendar())?;
let new_calendar = CachedCalendar::new(name, id.clone(), supported_components, color); let new_calendar = CachedCalendar::new(name, url.clone(), supported_components, color);
let arc = Arc::new(Mutex::new(new_calendar)); let arc = Arc::new(Mutex::new(new_calendar));
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
@ -224,7 +224,7 @@ impl CalDavSource<CachedCalendar> for Cache {
arc.lock().unwrap().set_mock_behaviour(Some(Arc::clone(behaviour))); arc.lock().unwrap().set_mock_behaviour(Some(Arc::clone(behaviour)));
}; };
match self.data.calendars.insert(id, arc.clone()) { match self.data.calendars.insert(url, arc.clone()) {
Some(_) => Err("Attempt to insert calendar failed: there is alredy such a calendar.".into()), Some(_) => Err("Attempt to insert calendar failed: there is alredy such a calendar.".into()),
None => Ok(arc), None => Ok(arc),
} }
@ -259,13 +259,13 @@ mod tests {
{ {
let mut bucket_list = bucket_list.lock().unwrap(); let mut bucket_list = bucket_list.lock().unwrap();
let cal_id = bucket_list.id().clone(); let cal_url = bucket_list.url().clone();
bucket_list.add_item(Item::Task(Task::new( bucket_list.add_item(Item::Task(Task::new(
String::from("Attend a concert of JS Bach"), false, &cal_id String::from("Attend a concert of JS Bach"), false, &cal_url
))).await.unwrap(); ))).await.unwrap();
bucket_list.add_item(Item::Task(Task::new( bucket_list.add_item(Item::Task(Task::new(
String::from("Climb the Lighthouse of Alexandria"), true, &cal_id String::from("Climb the Lighthouse of Alexandria"), true, &cal_url
))).await.unwrap(); ))).await.unwrap();
} }
@ -293,7 +293,7 @@ mod tests {
let cache_path = PathBuf::from(String::from("test_cache/sanity_tests")); let cache_path = PathBuf::from(String::from("test_cache/sanity_tests"));
let mut cache = populate_cache(&cache_path).await; let mut cache = populate_cache(&cache_path).await;
// We should not be able to add a second calendar with the same id // We should not be able to add a second calendar with the same URL
let second_addition_same_calendar = cache.create_calendar( let second_addition_same_calendar = cache.create_calendar(
Url::parse("https://caldav.com/shopping").unwrap(), Url::parse("https://caldav.com/shopping").unwrap(),
"My shopping list".to_string(), "My shopping list".to_string(),

View file

@ -4,12 +4,12 @@ use std::error::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use async_trait::async_trait; use async_trait::async_trait;
use csscolorparser::Color; use csscolorparser::Color;
use url::Url;
use crate::item::SyncStatus; use crate::item::SyncStatus;
use crate::traits::{BaseCalendar, CompleteCalendar}; use crate::traits::{BaseCalendar, CompleteCalendar};
use crate::calendar::{CalendarId, SupportedComponents}; use crate::calendar::SupportedComponents;
use crate::Item; use crate::Item;
use crate::item::ItemId;
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -23,14 +23,14 @@ use crate::mock_behaviour::MockBehaviour;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CachedCalendar { pub struct CachedCalendar {
name: String, name: String,
id: CalendarId, url: Url,
supported_components: SupportedComponents, supported_components: SupportedComponents,
color: Option<Color>, color: Option<Color>,
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
#[serde(skip)] #[serde(skip)]
mock_behaviour: Option<Arc<Mutex<MockBehaviour>>>, mock_behaviour: Option<Arc<Mutex<MockBehaviour>>>,
items: HashMap<ItemId, Item>, items: HashMap<Url, Item>,
} }
impl CachedCalendar { impl CachedCalendar {
@ -65,7 +65,7 @@ impl CachedCalendar {
fn regular_add_or_update_item(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> { fn regular_add_or_update_item(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
let ss_clone = item.sync_status().clone(); let ss_clone = item.sync_status().clone();
log::debug!("Adding or updating an item with {:?}", ss_clone); log::debug!("Adding or updating an item with {:?}", ss_clone);
self.items.insert(item.id().clone(), item); self.items.insert(item.url().clone(), item);
Ok(ss_clone) Ok(ss_clone)
} }
@ -78,7 +78,7 @@ impl CachedCalendar {
_ => item.set_sync_status(SyncStatus::random_synced()), _ => item.set_sync_status(SyncStatus::random_synced()),
}; };
let ss_clone = item.sync_status().clone(); let ss_clone = item.sync_status().clone();
self.items.insert(item.id().clone(), item); self.items.insert(item.url().clone(), item);
Ok(ss_clone) Ok(ss_clone)
} }
@ -86,7 +86,7 @@ impl CachedCalendar {
#[cfg(any(test, feature = "integration_tests"))] #[cfg(any(test, feature = "integration_tests"))]
pub async fn has_same_observable_content_as(&self, other: &CachedCalendar) -> Result<bool, Box<dyn Error>> { pub async fn has_same_observable_content_as(&self, other: &CachedCalendar) -> Result<bool, Box<dyn Error>> {
if self.name != other.name if self.name != other.name
|| self.id != other.id || self.url != other.url
|| self.supported_components != other.supported_components || self.supported_components != other.supported_components
|| self.color != other.color || self.color != other.color
{ {
@ -102,13 +102,13 @@ impl CachedCalendar {
log::debug!("Different keys for items"); log::debug!("Different keys for items");
return Ok(false); return Ok(false);
} }
for (id_l, item_l) in items_l { for (url_l, item_l) in items_l {
let item_r = match items_r.get(&id_l) { let item_r = match items_r.get(&url_l) {
Some(c) => c, Some(c) => c,
None => return Err("should not happen, we've just tested keys are the same".into()), 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) == false {
log::debug!("Different items for id {}:", id_l); log::debug!("Different items for URL {}:", url_l);
log::debug!("{:#?}", item_l); log::debug!("{:#?}", item_l);
log::debug!("{:#?}", item_r); log::debug!("{:#?}", item_r);
return Ok(false); return Ok(false);
@ -118,36 +118,36 @@ impl CachedCalendar {
Ok(true) Ok(true)
} }
/// The non-async version of [`Self::get_item_ids`] /// The non-async version of [`Self::get_item_urls`]
pub fn get_item_ids_sync(&self) -> Result<HashSet<ItemId>, Box<dyn Error>> { pub fn get_item_urls_sync(&self) -> Result<HashSet<Url>, Box<dyn Error>> {
Ok(self.items.iter() Ok(self.items.iter()
.map(|(id, _)| id.clone()) .map(|(url, _)| url.clone())
.collect() .collect()
) )
} }
/// The non-async version of [`Self::get_items`] /// The non-async version of [`Self::get_items`]
pub fn get_items_sync(&self) -> Result<HashMap<ItemId, &Item>, Box<dyn Error>> { pub fn get_items_sync(&self) -> Result<HashMap<Url, &Item>, Box<dyn Error>> {
Ok(self.items.iter() Ok(self.items.iter()
.map(|(id, item)| (id.clone(), item)) .map(|(url, item)| (url.clone(), item))
.collect() .collect()
) )
} }
/// The non-async version of [`Self::get_item_by_id`] /// The non-async version of [`Self::get_item_by_url`]
pub fn get_item_by_id_sync<'a>(&'a self, id: &ItemId) -> Option<&'a Item> { pub fn get_item_by_url_sync<'a>(&'a self, url: &Url) -> Option<&'a Item> {
self.items.get(id) self.items.get(url)
} }
/// The non-async version of [`Self::get_item_by_id_mut`] /// The non-async version of [`Self::get_item_by_url_mut`]
pub fn get_item_by_id_mut_sync<'a>(&'a mut self, id: &ItemId) -> Option<&'a mut Item> { pub fn get_item_by_url_mut_sync<'a>(&'a mut self, url: &Url) -> Option<&'a mut Item> {
self.items.get_mut(id) self.items.get_mut(url)
} }
/// The non-async version of [`Self::add_item`] /// The non-async version of [`Self::add_item`]
pub fn add_item_sync(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> { pub fn add_item_sync(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
if self.items.contains_key(item.id()) { if self.items.contains_key(item.url()) {
return Err(format!("Item {:?} cannot be added, it exists already", item.id()).into()); return Err(format!("Item {:?} cannot be added, it exists already", item.url()).into());
} }
#[cfg(not(feature = "local_calendar_mocks_remote_calendars"))] #[cfg(not(feature = "local_calendar_mocks_remote_calendars"))]
return self.regular_add_or_update_item(item); return self.regular_add_or_update_item(item);
@ -158,8 +158,8 @@ impl CachedCalendar {
/// The non-async version of [`Self::update_item`] /// The non-async version of [`Self::update_item`]
pub fn update_item_sync(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> { pub fn update_item_sync(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
if self.items.contains_key(item.id()) == false { if self.items.contains_key(item.url()) == false {
return Err(format!("Item {:?} cannot be updated, it does not already exist", item.id()).into()); return Err(format!("Item {:?} cannot be updated, it does not already exist", item.url()).into());
} }
#[cfg(not(feature = "local_calendar_mocks_remote_calendars"))] #[cfg(not(feature = "local_calendar_mocks_remote_calendars"))]
return self.regular_add_or_update_item(item); return self.regular_add_or_update_item(item);
@ -169,8 +169,8 @@ impl CachedCalendar {
} }
/// The non-async version of [`Self::mark_for_deletion`] /// The non-async version of [`Self::mark_for_deletion`]
pub fn mark_for_deletion_sync(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> { pub fn mark_for_deletion_sync(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
match self.items.get_mut(item_id) { match self.items.get_mut(item_url) {
None => Err("no item for this key".into()), None => Err("no item for this key".into()),
Some(item) => { Some(item) => {
match item.sync_status() { match item.sync_status() {
@ -188,7 +188,7 @@ impl CachedCalendar {
}, },
SyncStatus::NotSynced => { SyncStatus::NotSynced => {
// This was never synced to the server, we can safely delete it as soon as now // This was never synced to the server, we can safely delete it as soon as now
self.items.remove(item_id); self.items.remove(item_url);
}, },
}; };
Ok(()) Ok(())
@ -197,9 +197,9 @@ impl CachedCalendar {
} }
/// The non-async version of [`Self::immediately_delete_item`] /// The non-async version of [`Self::immediately_delete_item`]
pub fn immediately_delete_item_sync(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> { pub fn immediately_delete_item_sync(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
match self.items.remove(item_id) { match self.items.remove(item_url) {
None => Err(format!("Item {} is absent from this calendar", item_id).into()), None => Err(format!("Item {} is absent from this calendar", item_url).into()),
Some(_) => Ok(()) Some(_) => Ok(())
} }
} }
@ -213,8 +213,8 @@ impl BaseCalendar for CachedCalendar {
&self.name &self.name
} }
fn id(&self) -> &CalendarId { fn url(&self) -> &Url {
&self.id &self.url
} }
fn supported_components(&self) -> SupportedComponents { fn supported_components(&self) -> SupportedComponents {
@ -236,37 +236,37 @@ impl BaseCalendar for CachedCalendar {
#[async_trait] #[async_trait]
impl CompleteCalendar for CachedCalendar { impl CompleteCalendar for CachedCalendar {
fn new(name: String, id: CalendarId, supported_components: SupportedComponents, color: Option<Color>) -> Self { fn new(name: String, url: Url, supported_components: SupportedComponents, color: Option<Color>) -> Self {
Self { Self {
name, id, supported_components, color, name, url, supported_components, color,
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
mock_behaviour: None, mock_behaviour: None,
items: HashMap::new(), items: HashMap::new(),
} }
} }
async fn get_item_ids(&self) -> Result<HashSet<ItemId>, Box<dyn Error>> { async fn get_item_urls(&self) -> Result<HashSet<Url>, Box<dyn Error>> {
self.get_item_ids_sync() self.get_item_urls_sync()
} }
async fn get_items(&self) -> Result<HashMap<ItemId, &Item>, Box<dyn Error>> { async fn get_items(&self) -> Result<HashMap<Url, &Item>, Box<dyn Error>> {
self.get_items_sync() self.get_items_sync()
} }
async fn get_item_by_id<'a>(&'a self, id: &ItemId) -> Option<&'a Item> { async fn get_item_by_url<'a>(&'a self, url: &Url) -> Option<&'a Item> {
self.get_item_by_id_sync(id) self.get_item_by_url_sync(url)
} }
async fn get_item_by_id_mut<'a>(&'a mut self, id: &ItemId) -> Option<&'a mut Item> { async fn get_item_by_url_mut<'a>(&'a mut self, url: &Url) -> Option<&'a mut Item> {
self.get_item_by_id_mut_sync(id) self.get_item_by_url_mut_sync(url)
} }
async fn mark_for_deletion(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> { async fn mark_for_deletion(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
self.mark_for_deletion_sync(item_id) self.mark_for_deletion_sync(item_url)
} }
async fn immediately_delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> { async fn immediately_delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
self.immediately_delete_item_sync(item_id) self.immediately_delete_item_sync(item_url)
} }
} }
@ -286,7 +286,7 @@ impl DavCalendar for CachedCalendar {
crate::traits::CompleteCalendar::new(name, resource.url().clone(), supported_components, color) crate::traits::CompleteCalendar::new(name, resource.url().clone(), supported_components, color)
} }
async fn get_item_version_tags(&self) -> Result<HashMap<ItemId, VersionTag>, Box<dyn Error>> { async fn get_item_version_tags(&self) -> Result<HashMap<Url, VersionTag>, Box<dyn Error>> {
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[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())?;
@ -294,30 +294,30 @@ impl DavCalendar for CachedCalendar {
let mut result = HashMap::new(); let mut result = HashMap::new();
for (id, item) in self.items.iter() { for (url, item) in self.items.iter() {
let vt = match item.sync_status() { let vt = match item.sync_status() {
SyncStatus::Synced(vt) => vt.clone(), 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(id.clone(), vt); result.insert(url.clone(), vt);
} }
Ok(result) Ok(result)
} }
async fn get_item_by_id(&self, id: &ItemId) -> Result<Option<Item>, Box<dyn Error>> { async fn get_item_by_url(&self, url: &Url) -> Result<Option<Item>, Box<dyn Error>> {
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_item_by_id())?; self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_item_by_url())?;
Ok(self.items.get(id).cloned()) Ok(self.items.get(url).cloned())
} }
async fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> { async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[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_id).await self.immediately_delete_item(item_url).await
} }
} }

View file

@ -77,6 +77,3 @@ impl Default for SearchFilter {
SearchFilter::All SearchFilter::All
} }
} }
pub type CalendarId = url::Url;

View file

@ -5,13 +5,12 @@ use std::sync::Mutex;
use async_trait::async_trait; use async_trait::async_trait;
use reqwest::{header::CONTENT_TYPE, header::CONTENT_LENGTH}; use reqwest::{header::CONTENT_TYPE, header::CONTENT_LENGTH};
use csscolorparser::Color; use csscolorparser::Color;
use url::Url;
use crate::traits::BaseCalendar; use crate::traits::BaseCalendar;
use crate::traits::DavCalendar; use crate::traits::DavCalendar;
use crate::calendar::SupportedComponents; use crate::calendar::SupportedComponents;
use crate::calendar::CalendarId;
use crate::item::Item; use crate::item::Item;
use crate::item::ItemId;
use crate::item::VersionTag; use crate::item::VersionTag;
use crate::item::SyncStatus; use crate::item::SyncStatus;
use crate::resource::Resource; use crate::resource::Resource;
@ -40,13 +39,13 @@ pub struct RemoteCalendar {
supported_components: SupportedComponents, supported_components: SupportedComponents,
color: Option<Color>, color: Option<Color>,
cached_version_tags: Mutex<Option<HashMap<ItemId, VersionTag>>>, cached_version_tags: Mutex<Option<HashMap<Url, VersionTag>>>,
} }
#[async_trait] #[async_trait]
impl BaseCalendar for RemoteCalendar { impl BaseCalendar for RemoteCalendar {
fn name(&self) -> &str { &self.name } fn name(&self) -> &str { &self.name }
fn id(&self) -> &CalendarId { &self.resource.url() } fn url(&self) -> &Url { &self.resource.url() }
fn supported_components(&self) -> crate::calendar::SupportedComponents { fn supported_components(&self) -> crate::calendar::SupportedComponents {
self.supported_components self.supported_components
} }
@ -58,7 +57,7 @@ impl BaseCalendar for RemoteCalendar {
let ical_text = crate::ical::build_from(&item)?; let ical_text = crate::ical::build_from(&item)?;
let response = reqwest::Client::new() let response = reqwest::Client::new()
.put(item.id().as_url().clone()) .put(item.url().clone())
.header("If-None-Match", "*") .header("If-None-Match", "*")
.header(CONTENT_TYPE, "text/calendar") .header(CONTENT_TYPE, "text/calendar")
.header(CONTENT_LENGTH, ical_text.len()) .header(CONTENT_LENGTH, ical_text.len())
@ -73,7 +72,7 @@ impl BaseCalendar for RemoteCalendar {
let reply_hdrs = response.headers(); let reply_hdrs = response.headers();
match reply_hdrs.get("ETag") { match reply_hdrs.get("ETag") {
None => Err(format!("No ETag in these response headers: {:?} (request was {:?})", reply_hdrs, item.id()).into()), None => Err(format!("No ETag in these response headers: {:?} (request was {:?})", reply_hdrs, item.url()).into()),
Some(etag) => { Some(etag) => {
let vtag_str = etag.to_str()?; let vtag_str = etag.to_str()?;
let vtag = VersionTag::from(String::from(vtag_str)); let vtag = VersionTag::from(String::from(vtag_str));
@ -92,7 +91,7 @@ impl BaseCalendar for RemoteCalendar {
let ical_text = crate::ical::build_from(&item)?; let ical_text = crate::ical::build_from(&item)?;
let request = reqwest::Client::new() let request = reqwest::Client::new()
.put(item.id().as_url().clone()) .put(item.url().clone())
.header("If-Match", old_etag.as_str()) .header("If-Match", old_etag.as_str())
.header(CONTENT_TYPE, "text/calendar") .header(CONTENT_TYPE, "text/calendar")
.header(CONTENT_LENGTH, ical_text.len()) .header(CONTENT_LENGTH, ical_text.len())
@ -107,7 +106,7 @@ impl BaseCalendar for RemoteCalendar {
let reply_hdrs = request.headers(); let reply_hdrs = request.headers();
match reply_hdrs.get("ETag") { match reply_hdrs.get("ETag") {
None => Err(format!("No ETag in these response headers: {:?} (request was {:?})", reply_hdrs, item.id()).into()), None => Err(format!("No ETag in these response headers: {:?} (request was {:?})", reply_hdrs, item.url()).into()),
Some(etag) => { Some(etag) => {
let vtag_str = etag.to_str()?; let vtag_str = etag.to_str()?;
let vtag = VersionTag::from(String::from(vtag_str)); let vtag = VersionTag::from(String::from(vtag_str));
@ -127,7 +126,7 @@ impl DavCalendar for RemoteCalendar {
} }
async fn get_item_version_tags(&self) -> Result<HashMap<ItemId, VersionTag>, Box<dyn Error>> { async fn get_item_version_tags(&self) -> Result<HashMap<Url, VersionTag>, Box<dyn Error>> {
if let Some(map) = &*self.cached_version_tags.lock().unwrap() { if let Some(map) = &*self.cached_version_tags.lock().unwrap() {
log::debug!("Version tags are already cached."); log::debug!("Version tags are already cached.");
return Ok(map.clone()); return Ok(map.clone());
@ -139,19 +138,19 @@ impl DavCalendar for RemoteCalendar {
for response in responses { for response in responses {
let item_url = crate::utils::find_elem(&response, "href") let item_url = crate::utils::find_elem(&response, "href")
.map(|elem| self.resource.combine(&elem.text())); .map(|elem| self.resource.combine(&elem.text()));
let item_id = match item_url { let item_url = match item_url {
None => { None => {
log::warn!("Unable to extract HREF"); log::warn!("Unable to extract HREF");
continue; continue;
}, },
Some(resource) => { Some(resource) => {
ItemId::from(&resource) resource.url().clone()
}, },
}; };
let version_tag = match crate::utils::find_elem(&response, "getetag") { let version_tag = match crate::utils::find_elem(&response, "getetag") {
None => { None => {
log::warn!("Unable to extract ETAG for item {}, ignoring it", item_id); log::warn!("Unable to extract ETAG for item {}, ignoring it", item_url);
continue; continue;
}, },
Some(etag) => { Some(etag) => {
@ -159,7 +158,7 @@ impl DavCalendar for RemoteCalendar {
} }
}; };
items.insert(item_id, version_tag); items.insert(item_url.clone(), version_tag);
} }
// Note: the mutex cannot be locked during this whole async function, but it can safely be re-entrant (this will just waste an unnecessary request) // Note: the mutex cannot be locked during this whole async function, but it can safely be re-entrant (this will just waste an unnecessary request)
@ -167,9 +166,9 @@ impl DavCalendar for RemoteCalendar {
Ok(items) Ok(items)
} }
async fn get_item_by_id(&self, id: &ItemId) -> Result<Option<Item>, Box<dyn Error>> { async fn get_item_by_url(&self, url: &Url) -> Result<Option<Item>, Box<dyn Error>> {
let res = reqwest::Client::new() let res = reqwest::Client::new()
.get(id.as_url().clone()) .get(url.clone())
.header(CONTENT_TYPE, "text/calendar") .header(CONTENT_TYPE, "text/calendar")
.basic_auth(self.resource.username(), Some(self.resource.password())) .basic_auth(self.resource.username(), Some(self.resource.password()))
.send() .send()
@ -183,18 +182,18 @@ impl DavCalendar for RemoteCalendar {
// This is supposed to be cached // This is supposed to be cached
let version_tags = self.get_item_version_tags().await?; let version_tags = self.get_item_version_tags().await?;
let vt = match version_tags.get(id) { let vt = match version_tags.get(url) {
None => return Err(format!("Inconsistent data: {} has no version tag", id).into()), None => return Err(format!("Inconsistent data: {} has no version tag", url).into()),
Some(vt) => vt, Some(vt) => vt,
}; };
let item = crate::ical::parse(&text, id.clone(), SyncStatus::Synced(vt.clone()))?; let item = crate::ical::parse(&text, url.clone(), SyncStatus::Synced(vt.clone()))?;
Ok(Some(item)) Ok(Some(item))
} }
async fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> { async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
let del_response = reqwest::Client::new() let del_response = reqwest::Client::new()
.delete(item_id.as_url().clone()) .delete(item_url.clone())
.basic_auth(self.resource.username(), Some(self.resource.password())) .basic_auth(self.resource.username(), Some(self.resource.password()))
.send() .send()
.await?; .await?;

View file

@ -15,7 +15,6 @@ use csscolorparser::Color;
use crate::resource::Resource; use crate::resource::Resource;
use crate::utils::{find_elem, find_elems}; use crate::utils::{find_elem, find_elems};
use crate::calendar::remote_calendar::RemoteCalendar; use crate::calendar::remote_calendar::RemoteCalendar;
use crate::calendar::CalendarId;
use crate::calendar::SupportedComponents; use crate::calendar::SupportedComponents;
use crate::traits::CalDavSource; use crate::traits::CalDavSource;
use crate::traits::BaseCalendar; use crate::traits::BaseCalendar;
@ -113,7 +112,7 @@ pub struct Client {
struct CachedReplies { struct CachedReplies {
principal: Option<Resource>, principal: Option<Resource>,
calendar_home_set: Option<Resource>, calendar_home_set: Option<Resource>,
calendars: Option<HashMap<CalendarId, Arc<Mutex<RemoteCalendar>>>>, calendars: Option<HashMap<Url, Arc<Mutex<RemoteCalendar>>>>,
} }
impl Client { impl Client {
@ -216,7 +215,7 @@ impl Client {
let this_calendar = RemoteCalendar::new(display_name, this_calendar_url, supported_components, this_calendar_color); let this_calendar = RemoteCalendar::new(display_name, this_calendar_url, supported_components, this_calendar_color);
log::info!("Found calendar {}", this_calendar.name()); log::info!("Found calendar {}", this_calendar.name());
calendars.insert(this_calendar.id().clone(), Arc::new(Mutex::new(this_calendar))); calendars.insert(this_calendar.url().clone(), Arc::new(Mutex::new(this_calendar)));
} }
let mut replies = self.cached_replies.lock().unwrap(); let mut replies = self.cached_replies.lock().unwrap();
@ -228,7 +227,7 @@ impl Client {
#[async_trait] #[async_trait]
impl CalDavSource<RemoteCalendar> for Client { impl CalDavSource<RemoteCalendar> for Client {
async fn get_calendars(&self) -> Result<HashMap<CalendarId, Arc<Mutex<RemoteCalendar>>>, Box<dyn Error>> { async fn get_calendars(&self) -> Result<HashMap<Url, Arc<Mutex<RemoteCalendar>>>, Box<dyn Error>> {
self.populate_calendars().await?; self.populate_calendars().await?;
match &self.cached_replies.lock().unwrap().calendars { match &self.cached_replies.lock().unwrap().calendars {
@ -239,7 +238,7 @@ impl CalDavSource<RemoteCalendar> for Client {
}; };
} }
async fn get_calendar(&self, id: &CalendarId) -> Option<Arc<Mutex<RemoteCalendar>>> { async fn get_calendar(&self, url: &Url) -> Option<Arc<Mutex<RemoteCalendar>>> {
if let Err(err) = self.populate_calendars().await { if let Err(err) = self.populate_calendars().await {
log::warn!("Unable to fetch calendars: {}", err); log::warn!("Unable to fetch calendars: {}", err);
return None; return None;
@ -248,17 +247,17 @@ impl CalDavSource<RemoteCalendar> for Client {
self.cached_replies.lock().unwrap() self.cached_replies.lock().unwrap()
.calendars .calendars
.as_ref() .as_ref()
.and_then(|cals| cals.get(id)) .and_then(|cals| cals.get(url))
.map(|cal| cal.clone()) .map(|cal| cal.clone())
} }
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents, color: Option<Color>) -> Result<Arc<Mutex<RemoteCalendar>>, Box<dyn Error>> { async fn create_calendar(&mut self, url: Url, name: String, supported_components: SupportedComponents, color: Option<Color>) -> Result<Arc<Mutex<RemoteCalendar>>, Box<dyn Error>> {
self.populate_calendars().await?; self.populate_calendars().await?;
match self.cached_replies.lock().unwrap().calendars.as_ref() { match self.cached_replies.lock().unwrap().calendars.as_ref() {
None => return Err("No calendars have been fetched".into()), None => return Err("No calendars have been fetched".into()),
Some(cals) => { Some(cals) => {
if cals.contains_key(&id) { if cals.contains_key(&url) {
return Err("This calendar already exists".into()); return Err("This calendar already exists".into());
} }
}, },
@ -267,7 +266,7 @@ impl CalDavSource<RemoteCalendar> for Client {
let creation_body = calendar_body(name, supported_components); let creation_body = calendar_body(name, supported_components);
let response = reqwest::Client::new() let response = reqwest::Client::new()
.request(Method::from_bytes(b"MKCALENDAR").unwrap(), id.clone()) .request(Method::from_bytes(b"MKCALENDAR").unwrap(), url.clone())
.header(CONTENT_TYPE, "application/xml") .header(CONTENT_TYPE, "application/xml")
.basic_auth(self.resource.username(), Some(self.resource.password())) .basic_auth(self.resource.username(), Some(self.resource.password()))
.body(creation_body) .body(creation_body)
@ -279,7 +278,7 @@ impl CalDavSource<RemoteCalendar> for Client {
return Err(format!("Unexpected HTTP status code. Expected CREATED, got {}", status.as_u16()).into()); return Err(format!("Unexpected HTTP status code. Expected CREATED, got {}", status.as_u16()).into());
} }
self.get_calendar(&id).await.ok_or(format!("Unable to insert calendar {:?}", id).into()) self.get_calendar(&url).await.ok_or(format!("Unable to insert calendar {:?}", url).into())
} }
} }

View file

@ -2,15 +2,15 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use url::Url;
use crate::item::ItemId;
use crate::item::SyncStatus; use crate::item::SyncStatus;
/// TODO: implement `Event` one day. /// TODO: implement `Event` one day.
/// This crate currently only supports tasks, not calendar events. /// This crate currently only supports tasks, not calendar events.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Event { pub struct Event {
id: ItemId, uid: String,
name: String, name: String,
sync_status: SyncStatus, sync_status: SyncStatus,
} }
@ -20,12 +20,12 @@ impl Event {
unimplemented!(); unimplemented!();
} }
pub fn id(&self) -> &ItemId { pub fn url(&self) -> &Url {
&self.id unimplemented!();
} }
pub fn uid(&self) -> &str { pub fn uid(&self) -> &str {
unimplemented!() &self.uid
} }
pub fn name(&self) -> &str { pub fn name(&self) -> &str {

View file

@ -130,12 +130,12 @@ mod tests {
} }
fn build_task(completed: bool) -> (String, String, String) { fn build_task(completed: bool) -> (String, String, String) {
let cal_id = "http://my.calend.ar/id".parse().unwrap(); let cal_url = "http://my.calend.ar/id".parse().unwrap();
let now = Utc::now(); let now = Utc::now();
let s_now = format_date_time(&now); let s_now = format_date_time(&now);
let task = Item::Task(Task::new( let task = Item::Task(Task::new(
String::from("This is a task with ÜTF-8 characters"), completed, &cal_id String::from("This is a task with ÜTF-8 characters"), completed, &cal_url
)); ));
let ical = build_from(&task).unwrap(); let ical = build_from(&task).unwrap();

View file

@ -4,22 +4,22 @@ use std::error::Error;
use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo}; use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo};
use chrono::{DateTime, TimeZone, Utc}; use chrono::{DateTime, TimeZone, Utc};
use url::Url;
use crate::Item; use crate::Item;
use crate::item::SyncStatus; use crate::item::SyncStatus;
use crate::item::ItemId;
use crate::Task; use crate::Task;
use crate::task::CompletionStatus; use crate::task::CompletionStatus;
use crate::Event; use crate::Event;
/// Parse an iCal file into the internal representation [`crate::Item`] /// Parse an iCal file into the internal representation [`crate::Item`]
pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<Item, Box<dyn Error>> { pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<Item, Box<dyn Error>> {
let mut reader = ical::IcalParser::new(content.as_bytes()); let mut reader = ical::IcalParser::new(content.as_bytes());
let parsed_item = match reader.next() { let parsed_item = match reader.next() {
None => return Err(format!("Invalid iCal data to parse for item {}", item_id).into()), None => return Err(format!("Invalid iCal data to parse for item {}", item_url).into()),
Some(item) => match item { Some(item) => match item {
Err(err) => return Err(format!("Unable to parse iCal data for item {}: {}", item_id, err).into()), Err(err) => return Err(format!("Unable to parse iCal data for item {}: {}", item_url, err).into()),
Ok(item) => item, Ok(item) => item,
} }
}; };
@ -80,15 +80,15 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
} }
let name = match name { let name = match name {
Some(name) => name, Some(name) => name,
None => return Err(format!("Missing name for item {}", item_id).into()), None => return Err(format!("Missing name for item {}", item_url).into()),
}; };
let uid = match uid { let uid = match uid {
Some(uid) => uid, Some(uid) => uid,
None => return Err(format!("Missing UID for item {}", item_id).into()), None => return Err(format!("Missing UID for item {}", item_url).into()),
}; };
let last_modified = match last_modified { let last_modified = match last_modified {
Some(dt) => dt, Some(dt) => dt,
None => return Err(format!("Missing DTSTAMP for item {}, but this is required by RFC5545", item_id).into()), None => return Err(format!("Missing DTSTAMP for item {}, but this is required by RFC5545", item_url).into()),
}; };
let completion_status = match completed { let completion_status = match completed {
false => { false => {
@ -100,7 +100,7 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
true => CompletionStatus::Completed(completion_date), true => CompletionStatus::Completed(completion_date),
}; };
Item::Task(Task::new_with_parameters(name, uid, item_id, 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))
}, },
}; };
@ -244,13 +244,13 @@ END:VCALENDAR
fn test_ical_parsing() { fn test_ical_parsing() {
let version_tag = VersionTag::from(String::from("test-tag")); let version_tag = VersionTag::from(String::from("test-tag"));
let sync_status = SyncStatus::Synced(version_tag); let sync_status = SyncStatus::Synced(version_tag);
let item_id: ItemId = "http://some.id/for/testing".parse().unwrap(); let item_url: Url = "http://some.id/for/testing".parse().unwrap();
let item = parse(EXAMPLE_ICAL, item_id.clone(), sync_status.clone()).unwrap(); let item = parse(EXAMPLE_ICAL, item_url.clone(), sync_status.clone()).unwrap();
let task = item.unwrap_task(); let task = item.unwrap_task();
assert_eq!(task.name(), "Do not forget to do this"); assert_eq!(task.name(), "Do not forget to do this");
assert_eq!(task.id(), &item_id); assert_eq!(task.url(), &item_url);
assert_eq!(task.uid(), "0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com"); assert_eq!(task.uid(), "0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com");
assert_eq!(task.completed(), false); assert_eq!(task.completed(), false);
assert_eq!(task.completion_status(), &CompletionStatus::Uncompleted); assert_eq!(task.completion_status(), &CompletionStatus::Uncompleted);
@ -262,9 +262,9 @@ END:VCALENDAR
fn test_completed_ical_parsing() { fn test_completed_ical_parsing() {
let version_tag = VersionTag::from(String::from("test-tag")); let version_tag = VersionTag::from(String::from("test-tag"));
let sync_status = SyncStatus::Synced(version_tag); let sync_status = SyncStatus::Synced(version_tag);
let item_id: ItemId = "http://some.id/for/testing".parse().unwrap(); let item_url: Url = "http://some.id/for/testing".parse().unwrap();
let item = parse(EXAMPLE_ICAL_COMPLETED, item_id.clone(), sync_status.clone()).unwrap(); let item = parse(EXAMPLE_ICAL_COMPLETED, item_url.clone(), sync_status.clone()).unwrap();
let task = item.unwrap_task(); let task = item.unwrap_task();
assert_eq!(task.completed(), true); assert_eq!(task.completed(), true);
@ -275,9 +275,9 @@ END:VCALENDAR
fn test_completed_without_date_ical_parsing() { fn test_completed_without_date_ical_parsing() {
let version_tag = VersionTag::from(String::from("test-tag")); let version_tag = VersionTag::from(String::from("test-tag"));
let sync_status = SyncStatus::Synced(version_tag); let sync_status = SyncStatus::Synced(version_tag);
let item_id: ItemId = "http://some.id/for/testing".parse().unwrap(); let item_url: Url = "http://some.id/for/testing".parse().unwrap();
let item = parse(EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE, item_id.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(); let task = item.unwrap_task();
assert_eq!(task.completed(), true); assert_eq!(task.completed(), true);
@ -288,9 +288,9 @@ END:VCALENDAR
fn test_multiple_items_in_ical() { fn test_multiple_items_in_ical() {
let version_tag = VersionTag::from(String::from("test-tag")); let version_tag = VersionTag::from(String::from("test-tag"));
let sync_status = SyncStatus::Synced(version_tag); let sync_status = SyncStatus::Synced(version_tag);
let item_id: ItemId = "http://some.id/for/testing".parse().unwrap(); let item_url: Url = "http://some.id/for/testing".parse().unwrap();
let item = parse(EXAMPLE_MULTIPLE_ICAL, item_id.clone(), sync_status.clone()); let item = parse(EXAMPLE_MULTIPLE_ICAL, item_url.clone(), sync_status.clone());
assert!(item.is_err()); assert!(item.is_err());
} }
} }

View file

@ -1,17 +1,10 @@
//! CalDAV items (todo, events, journals...) //! CalDAV items (todo, events, journals...)
// TODO: move Event and Task to nest them in crate::items::calendar::Calendar? // TODO: move Event and Task to nest them in crate::items::calendar::Calendar?
use std::fmt::{Display, Formatter}; use serde::{Deserialize, Serialize};
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use url::Url; use url::Url;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use crate::resource::Resource;
use crate::calendar::CalendarId;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Item { pub enum Item {
@ -32,7 +25,7 @@ macro_rules! synthetise_common_getter {
} }
impl Item { impl Item {
synthetise_common_getter!(id, &ItemId); synthetise_common_getter!(url, &Url);
synthetise_common_getter!(uid, &str); synthetise_common_getter!(uid, &str);
synthetise_common_getter!(name, &str); synthetise_common_getter!(name, &str);
synthetise_common_getter!(creation_date, Option<&DateTime<Utc>>); synthetise_common_getter!(creation_date, Option<&DateTime<Utc>>);
@ -94,67 +87,6 @@ impl Item {
} }
#[derive(Clone, Debug, PartialEq, Hash)]
pub struct ItemId {
content: Url,
}
impl ItemId{
/// Generate a random ItemId.
pub fn random(parent_calendar: &CalendarId) -> Self {
let random = uuid::Uuid::new_v4().to_hyphenated().to_string();
let u = parent_calendar.join(&random).unwrap(/* this cannot panic since we've just created a string that is a valid URL */);
Self { content:u }
}
pub fn as_url(&self) -> &Url {
&self.content
}
}
impl From<Url> for ItemId {
fn from(url: Url) -> Self {
Self { content: url }
}
}
impl From<&Resource> for ItemId {
fn from(resource: &Resource) -> Self {
Self { content: resource.url().clone() }
}
}
impl FromStr for ItemId {
type Err = url::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let u: Url = s.parse()?;
Ok(Self::from(u))
}
}
impl Eq for ItemId {}
impl Display for ItemId {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "{}", self.content)
}
}
/// Used to support serde
impl Serialize for ItemId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.content.as_str())
}
}
/// Used to support serde
impl<'de> Deserialize<'de> for ItemId {
fn deserialize<D>(deserializer: D) -> Result<ItemId, D::Error>
where
D: Deserializer<'de>,
{
let u = Url::deserialize(deserializer)?;
Ok(ItemId{ content: u })
}
}
/// A VersionTag is basically a CalDAV `ctag` or `etag`. Whenever it changes, this means the data has changed. /// A VersionTag is basically a CalDAV `ctag` or `etag`. Whenever it changes, this means the data has changed.

View file

@ -22,7 +22,7 @@ pub struct MockBehaviour {
// From the DavCalendar trait // From the DavCalendar trait
pub get_item_version_tags_behaviour: (u32, u32), pub get_item_version_tags_behaviour: (u32, u32),
pub get_item_by_id_behaviour: (u32, u32), pub get_item_by_url_behaviour: (u32, u32),
pub delete_item_behaviour: (u32, u32), pub delete_item_behaviour: (u32, u32),
} }
@ -41,7 +41,7 @@ impl MockBehaviour {
add_item_behaviour: (0, n_fails), add_item_behaviour: (0, n_fails),
update_item_behaviour: (0, n_fails), update_item_behaviour: (0, n_fails),
get_item_version_tags_behaviour: (0, n_fails), get_item_version_tags_behaviour: (0, n_fails),
get_item_by_id_behaviour: (0, n_fails), get_item_by_url_behaviour: (0, n_fails),
delete_item_behaviour: (0, n_fails), delete_item_behaviour: (0, n_fails),
} }
} }
@ -84,9 +84,9 @@ impl MockBehaviour {
if self.is_suspended { return Ok(()) } if self.is_suspended { return Ok(()) }
decrement(&mut self.get_item_version_tags_behaviour, "get_item_version_tags") decrement(&mut self.get_item_version_tags_behaviour, "get_item_version_tags")
} }
pub fn can_get_item_by_id(&mut self) -> Result<(), Box<dyn Error>> { pub fn can_get_item_by_url(&mut self) -> Result<(), Box<dyn Error>> {
if self.is_suspended { return Ok(()) } if self.is_suspended { return Ok(()) }
decrement(&mut self.get_item_by_id_behaviour, "get_item_by_id") decrement(&mut self.get_item_by_url_behaviour, "get_item_by_url")
} }
pub fn can_delete_item(&mut self) -> Result<(), Box<dyn Error>> { pub fn can_delete_item(&mut self) -> Result<(), Box<dyn Error>> {
if self.is_suspended { return Ok(()) } if self.is_suspended { return Ok(()) }

View file

@ -6,11 +6,11 @@ use std::error::Error;
use std::collections::HashSet; use std::collections::HashSet;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use url::Url;
use crate::traits::{BaseCalendar, CalDavSource, DavCalendar}; use crate::traits::{BaseCalendar, CalDavSource, DavCalendar};
use crate::traits::CompleteCalendar; use crate::traits::CompleteCalendar;
use crate::item::{ItemId, SyncStatus}; use crate::item::SyncStatus;
use crate::calendar::CalendarId;
pub mod sync_progress; pub mod sync_progress;
use sync_progress::SyncProgress; use sync_progress::SyncProgress;
@ -102,39 +102,39 @@ where
// Sync every remote calendar // Sync every remote calendar
let cals_remote = self.remote.get_calendars().await?; let cals_remote = self.remote.get_calendars().await?;
for (cal_id, cal_remote) in cals_remote { for (cal_url, cal_remote) in cals_remote {
let counterpart = match self.get_or_insert_local_counterpart_calendar(&cal_id, cal_remote.clone()).await { let counterpart = match self.get_or_insert_local_counterpart_calendar(&cal_url, cal_remote.clone()).await {
Err(err) => { Err(err) => {
progress.warn(&format!("Unable to get or insert local counterpart calendar for {} ({}). Skipping this time", cal_id, err)); progress.warn(&format!("Unable to get or insert local counterpart calendar for {} ({}). Skipping this time", cal_url, err));
continue; continue;
}, },
Ok(arc) => arc, Ok(arc) => arc,
}; };
if let Err(err) = Self::sync_calendar_pair(counterpart, cal_remote, progress).await { if let Err(err) = Self::sync_calendar_pair(counterpart, cal_remote, progress).await {
progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err)); progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_url, err));
continue; continue;
} }
handled_calendars.insert(cal_id); handled_calendars.insert(cal_url);
} }
// Sync every local calendar that would not be in the remote yet // Sync every local calendar that would not be in the remote yet
let cals_local = self.local.get_calendars().await?; let cals_local = self.local.get_calendars().await?;
for (cal_id, cal_local) in cals_local { for (cal_url, cal_local) in cals_local {
if handled_calendars.contains(&cal_id) { if handled_calendars.contains(&cal_url) {
continue; continue;
} }
let counterpart = match self.get_or_insert_remote_counterpart_calendar(&cal_id, cal_local.clone()).await { let counterpart = match self.get_or_insert_remote_counterpart_calendar(&cal_url, cal_local.clone()).await {
Err(err) => { Err(err) => {
progress.warn(&format!("Unable to get or insert remote counterpart calendar for {} ({}). Skipping this time", cal_id, err)); progress.warn(&format!("Unable to get or insert remote counterpart calendar for {} ({}). Skipping this time", cal_url, err));
continue; continue;
}, },
Ok(arc) => arc, Ok(arc) => arc,
}; };
if let Err(err) = Self::sync_calendar_pair(cal_local, counterpart, progress).await { if let Err(err) = Self::sync_calendar_pair(cal_local, counterpart, progress).await {
progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err)); progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_url, err));
continue; continue;
} }
} }
@ -145,11 +145,11 @@ where
} }
async fn get_or_insert_local_counterpart_calendar(&mut self, cal_id: &CalendarId, needle: Arc<Mutex<U>>) -> Result<Arc<Mutex<T>>, Box<dyn Error>> { async fn get_or_insert_local_counterpart_calendar(&mut self, cal_url: &Url, needle: Arc<Mutex<U>>) -> Result<Arc<Mutex<T>>, Box<dyn Error>> {
get_or_insert_counterpart_calendar("local", &mut self.local, cal_id, needle).await get_or_insert_counterpart_calendar("local", &mut self.local, cal_url, needle).await
} }
async fn get_or_insert_remote_counterpart_calendar(&mut self, cal_id: &CalendarId, needle: Arc<Mutex<T>>) -> Result<Arc<Mutex<U>>, Box<dyn Error>> { async fn get_or_insert_remote_counterpart_calendar(&mut self, cal_url: &Url, needle: Arc<Mutex<T>>) -> Result<Arc<Mutex<U>>, Box<dyn Error>> {
get_or_insert_counterpart_calendar("remote", &mut self.remote, cal_id, needle).await get_or_insert_counterpart_calendar("remote", &mut self.remote, cal_url, needle).await
} }
@ -179,52 +179,52 @@ where
details: format!("{} remote items", remote_items.len()), details: format!("{} remote items", remote_items.len()),
}); });
let mut local_items_to_handle = cal_local.get_item_ids().await?; let mut local_items_to_handle = cal_local.get_item_urls().await?;
for (id, remote_tag) in remote_items { for (url, remote_tag) in remote_items {
progress.trace(&format!("***** Considering remote item {}...", id)); progress.trace(&format!("***** Considering remote item {}...", url));
match cal_local.get_item_by_id(&id).await { match cal_local.get_item_by_url(&url).await {
None => { None => {
// This was created on the remote // This was created on the remote
progress.debug(&format!("* {} is a remote addition", id)); progress.debug(&format!("* {} is a remote addition", url));
remote_additions.insert(id); remote_additions.insert(url);
}, },
Some(local_item) => { Some(local_item) => {
if local_items_to_handle.remove(&id) == false { if local_items_to_handle.remove(&url) == false {
progress.error(&format!("Inconsistent state: missing task {} from the local tasks", id)); progress.error(&format!("Inconsistent state: missing task {} from the local tasks", url));
} }
match local_item.sync_status() { match local_item.sync_status() {
SyncStatus::NotSynced => { SyncStatus::NotSynced => {
progress.error(&format!("ID reuse between remote and local sources ({}). Ignoring this item in the sync", id)); progress.error(&format!("URL reuse between remote and local sources ({}). Ignoring this item in the sync", url));
continue; continue;
}, },
SyncStatus::Synced(local_tag) => { SyncStatus::Synced(local_tag) => {
if &remote_tag != local_tag { if &remote_tag != local_tag {
// This has been modified on the remote // This has been modified on the remote
progress.debug(&format!("* {} is a remote change", id)); progress.debug(&format!("* {} is a remote change", url));
remote_changes.insert(id); remote_changes.insert(url);
} }
}, },
SyncStatus::LocallyModified(local_tag) => { SyncStatus::LocallyModified(local_tag) => {
if &remote_tag == local_tag { if &remote_tag == local_tag {
// This has been changed locally // This has been changed locally
progress.debug(&format!("* {} is a local change", id)); progress.debug(&format!("* {} is a local change", url));
local_changes.insert(id); local_changes.insert(url);
} else { } else {
progress.info(&format!("Conflict: task {} has been modified in both sources. Using the remote version.", id)); progress.info(&format!("Conflict: task {} has been modified in both sources. Using the remote version.", url));
progress.debug(&format!("* {} is considered a remote change", id)); progress.debug(&format!("* {} is considered a remote change", url));
remote_changes.insert(id); remote_changes.insert(url);
} }
}, },
SyncStatus::LocallyDeleted(local_tag) => { SyncStatus::LocallyDeleted(local_tag) => {
if &remote_tag == local_tag { if &remote_tag == local_tag {
// This has been locally deleted // This has been locally deleted
progress.debug(&format!("* {} is a local deletion", id)); progress.debug(&format!("* {} is a local deletion", url));
local_del.insert(id); local_del.insert(url);
} else { } else {
progress.info(&format!("Conflict: task {} has been locally deleted and remotely modified. Reverting to the remote version.", id)); 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", id)); progress.debug(&format!("* {} is a considered a remote change", url));
remote_changes.insert(id); remote_changes.insert(url);
} }
}, },
} }
@ -233,11 +233,11 @@ where
} }
// Also iterate on the local tasks that are not on the remote // Also iterate on the local tasks that are not on the remote
for id in local_items_to_handle { for url in local_items_to_handle {
progress.trace(&format!("##### Considering local item {}...", id)); progress.trace(&format!("##### Considering local item {}...", url));
let local_item = match cal_local.get_item_by_id(&id).await { let local_item = match cal_local.get_item_by_url(&url).await {
None => { None => {
progress.error(&format!("Inconsistent state: missing task {} from the local tasks", id)); progress.error(&format!("Inconsistent state: missing task {} from the local tasks", url));
continue; continue;
}, },
Some(item) => item, Some(item) => item,
@ -246,22 +246,22 @@ where
match local_item.sync_status() { match local_item.sync_status() {
SyncStatus::Synced(_) => { SyncStatus::Synced(_) => {
// This item has been removed from the remote // This item has been removed from the remote
progress.debug(&format!("# {} is a deletion from the server", id)); progress.debug(&format!("# {} is a deletion from the server", url));
remote_del.insert(id); remote_del.insert(url);
}, },
SyncStatus::NotSynced => { SyncStatus::NotSynced => {
// This item has just been locally created // This item has just been locally created
progress.debug(&format!("# {} has been locally created", id)); progress.debug(&format!("# {} has been locally created", url));
local_additions.insert(id); local_additions.insert(url);
}, },
SyncStatus::LocallyDeleted(_) => { SyncStatus::LocallyDeleted(_) => {
// This item has been deleted from both sources // This item has been deleted from both sources
progress.debug(&format!("# {} has been deleted from both sources", id)); progress.debug(&format!("# {} has been deleted from both sources", url));
remote_del.insert(id); remote_del.insert(url);
}, },
SyncStatus::LocallyModified(_) => { SyncStatus::LocallyModified(_) => {
progress.info(&format!("Conflict: item {} has been deleted from the server and locally modified. Deleting the local copy", id)); progress.info(&format!("Conflict: item {} has been deleted from the server and locally modified. Deleting the local copy", url));
remote_del.insert(id); remote_del.insert(url);
}, },
} }
} }
@ -269,80 +269,80 @@ where
// Step 2 - commit changes // Step 2 - commit changes
progress.trace("Committing changes..."); progress.trace("Committing changes...");
for id_del in local_del { for url_del in local_del {
progress.debug(&format!("> Pushing local deletion {} to the server", id_del)); progress.debug(&format!("> Pushing local deletion {} to the server", url_del));
progress.feedback(SyncEvent::InProgress{ progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(), calendar: cal_name.clone(),
details: Self::item_name(&cal_local, &id_del).await, details: Self::item_name(&cal_local, &url_del).await,
}); });
match cal_remote.delete_item(&id_del).await { match cal_remote.delete_item(&url_del).await {
Err(err) => { Err(err) => {
progress.warn(&format!("Unable to delete remote item {}: {}", id_del, err)); progress.warn(&format!("Unable to delete remote item {}: {}", url_del, err));
}, },
Ok(()) => { Ok(()) => {
// Change the local copy from "marked to deletion" to "actually deleted" // Change the local copy from "marked to deletion" to "actually deleted"
if let Err(err) = cal_local.immediately_delete_item(&id_del).await { if let Err(err) = cal_local.immediately_delete_item(&url_del).await {
progress.error(&format!("Unable to permanently delete local item {}: {}", id_del, err)); progress.error(&format!("Unable to permanently delete local item {}: {}", url_del, err));
} }
}, },
} }
} }
for id_del in remote_del { for url_del in remote_del {
progress.debug(&format!("> Applying remote deletion {} locally", id_del)); progress.debug(&format!("> Applying remote deletion {} locally", url_del));
progress.feedback(SyncEvent::InProgress{ progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(), calendar: cal_name.clone(),
details: Self::item_name(&cal_local, &id_del).await, details: Self::item_name(&cal_local, &url_del).await,
}); });
if let Err(err) = cal_local.immediately_delete_item(&id_del).await { if let Err(err) = cal_local.immediately_delete_item(&url_del).await {
progress.warn(&format!("Unable to delete local item {}: {}", id_del, err)); progress.warn(&format!("Unable to delete local item {}: {}", url_del, err));
} }
} }
for id_add in remote_additions { for url_add in remote_additions {
progress.debug(&format!("> Applying remote addition {} locally", id_add)); progress.debug(&format!("> Applying remote addition {} locally", url_add));
progress.feedback(SyncEvent::InProgress{ progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(), calendar: cal_name.clone(),
details: Self::item_name(&cal_local, &id_add).await, details: Self::item_name(&cal_local, &url_add).await,
}); });
match cal_remote.get_item_by_id(&id_add).await { match cal_remote.get_item_by_url(&url_add).await {
Err(err) => { Err(err) => {
progress.warn(&format!("Unable to get remote item {}: {}. Skipping it.", id_add, err)); progress.warn(&format!("Unable to get remote item {}: {}. Skipping it.", url_add, err));
continue; continue;
}, },
Ok(item) => match item { Ok(item) => match item {
None => { None => {
progress.error(&format!("Inconsistency: new item {} has vanished from the remote end", id_add)); progress.error(&format!("Inconsistency: new item {} has vanished from the remote end", url_add));
continue; continue;
}, },
Some(new_item) => { Some(new_item) => {
if let Err(err) = cal_local.add_item(new_item.clone()).await { if let Err(err) = cal_local.add_item(new_item.clone()).await {
progress.error(&format!("Not able to add item {} to local calendar: {}", id_add, err)); progress.error(&format!("Not able to add item {} to local calendar: {}", url_add, err));
} }
}, },
}, },
} }
} }
for id_change in remote_changes { for url_change in remote_changes {
progress.debug(&format!("> Applying remote change {} locally", id_change)); progress.debug(&format!("> Applying remote change {} locally", url_change));
progress.feedback(SyncEvent::InProgress{ progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(), calendar: cal_name.clone(),
details: Self::item_name(&cal_local, &id_change).await, details: Self::item_name(&cal_local, &url_change).await,
}); });
match cal_remote.get_item_by_id(&id_change).await { match cal_remote.get_item_by_url(&url_change).await {
Err(err) => { Err(err) => {
progress.warn(&format!("Unable to get remote item {}: {}. Skipping it", id_change, err)); progress.warn(&format!("Unable to get remote item {}: {}. Skipping it", url_change, err));
continue; continue;
}, },
Ok(item) => match item { Ok(item) => match item {
None => { None => {
progress.error(&format!("Inconsistency: modified item {} has vanished from the remote end", id_change)); progress.error(&format!("Inconsistency: modified item {} has vanished from the remote end", url_change));
continue; continue;
}, },
Some(item) => { Some(item) => {
if let Err(err) = cal_local.update_item(item.clone()).await { if let Err(err) = cal_local.update_item(item.clone()).await {
progress.error(&format!("Unable to update item {} in local calendar: {}", id_change, err)); progress.error(&format!("Unable to update item {} in local calendar: {}", url_change, err));
} }
}, },
} }
@ -350,20 +350,20 @@ where
} }
for id_add in local_additions { for url_add in local_additions {
progress.debug(&format!("> Pushing local addition {} to the server", id_add)); progress.debug(&format!("> Pushing local addition {} to the server", url_add));
progress.feedback(SyncEvent::InProgress{ progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(), calendar: cal_name.clone(),
details: Self::item_name(&cal_local, &id_add).await, details: Self::item_name(&cal_local, &url_add).await,
}); });
match cal_local.get_item_by_id_mut(&id_add).await { match cal_local.get_item_by_url_mut(&url_add).await {
None => { None => {
progress.error(&format!("Inconsistency: created item {} has been marked for upload but is locally missing", id_add)); progress.error(&format!("Inconsistency: created item {} has been marked for upload but is locally missing", url_add));
continue; continue;
}, },
Some(item) => { Some(item) => {
match cal_remote.add_item(item.clone()).await { match cal_remote.add_item(item.clone()).await {
Err(err) => progress.error(&format!("Unable to add item {} to remote calendar: {}", id_add, err)), Err(err) => progress.error(&format!("Unable to add item {} to remote calendar: {}", url_add, err)),
Ok(new_ss) => { Ok(new_ss) => {
// Update local sync status // Update local sync status
item.set_sync_status(new_ss); item.set_sync_status(new_ss);
@ -373,20 +373,20 @@ where
}; };
} }
for id_change in local_changes { for url_change in local_changes {
progress.debug(&format!("> Pushing local change {} to the server", id_change)); progress.debug(&format!("> Pushing local change {} to the server", url_change));
progress.feedback(SyncEvent::InProgress{ progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(), calendar: cal_name.clone(),
details: Self::item_name(&cal_local, &id_change).await, details: Self::item_name(&cal_local, &url_change).await,
}); });
match cal_local.get_item_by_id_mut(&id_change).await { match cal_local.get_item_by_url_mut(&url_change).await {
None => { None => {
progress.error(&format!("Inconsistency: modified item {} has been marked for upload but is locally missing", id_change)); progress.error(&format!("Inconsistency: modified item {} has been marked for upload but is locally missing", url_change));
continue; continue;
}, },
Some(item) => { Some(item) => {
match cal_remote.update_item(item.clone()).await { match cal_remote.update_item(item.clone()).await {
Err(err) => progress.error(&format!("Unable to update item {} in remote calendar: {}", id_change, err)), Err(err) => progress.error(&format!("Unable to update item {} in remote calendar: {}", url_change, err)),
Ok(new_ss) => { Ok(new_ss) => {
// Update local sync status // Update local sync status
item.set_sync_status(new_ss); item.set_sync_status(new_ss);
@ -400,14 +400,14 @@ where
} }
async fn item_name(cal: &T, id: &ItemId) -> String { async fn item_name(cal: &T, url: &Url) -> String {
cal.get_item_by_id(id).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 get_or_insert_counterpart_calendar<H, N, I>(haystack_descr: &str, haystack: &mut H, cal_id: &CalendarId, needle: Arc<Mutex<N>>) async fn get_or_insert_counterpart_calendar<H, N, I>(haystack_descr: &str, haystack: &mut H, cal_url: &Url, needle: Arc<Mutex<N>>)
-> Result<Arc<Mutex<I>>, Box<dyn Error>> -> Result<Arc<Mutex<I>>, Box<dyn Error>>
where where
H: CalDavSource<I>, H: CalDavSource<I>,
@ -415,18 +415,18 @@ where
N: BaseCalendar, N: BaseCalendar,
{ {
loop { loop {
if let Some(cal) = haystack.get_calendar(&cal_id).await { if let Some(cal) = haystack.get_calendar(&cal_url).await {
break Ok(cal); break Ok(cal);
} }
// This calendar does not exist locally yet, let's add it // This calendar does not exist locally yet, let's add it
log::debug!("Adding a {} calendar {}", haystack_descr, cal_id); log::debug!("Adding a {} calendar {}", haystack_descr, cal_url);
let src = needle.lock().unwrap(); let src = needle.lock().unwrap();
let name = src.name().to_string(); let name = src.name().to_string();
let supported_comps = src.supported_components(); let supported_comps = src.supported_components();
let color = src.color(); let color = src.color();
if let Err(err) = haystack.create_calendar( if let Err(err) = haystack.create_calendar(
cal_id.clone(), cal_url.clone(),
name, name,
supported_comps, supported_comps,
color.cloned(), color.cloned(),

View file

@ -4,10 +4,10 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use ical::property::Property; use ical::property::Property;
use url::Url;
use crate::item::ItemId;
use crate::item::SyncStatus; use crate::item::SyncStatus;
use crate::calendar::CalendarId; use crate::utils::random_url;
/// RFC5545 defines the completion as several optional fields, yet some combinations make no sense. /// RFC5545 defines the completion as several optional fields, yet some combinations make no sense.
/// This enum provides an API that forbids such impossible combinations. /// This enum provides an API that forbids such impossible combinations.
@ -33,10 +33,11 @@ impl CompletionStatus {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Task { pub struct Task {
/// The task URL /// The task URL
id: ItemId, url: Url,
/// Persistent, globally unique identifier for the calendar component /// Persistent, globally unique identifier for the calendar component
/// The [RFC](https://tools.ietf.org/html/rfc5545#page-117) recommends concatenating a timestamp with the server's domain name, but UUID are even better /// The [RFC](https://tools.ietf.org/html/rfc5545#page-117) recommends concatenating a timestamp with the server's domain name.
/// UUID are even better so we'll generate them, but we have to support tasks from the server, that may have any arbitrary strings here.
uid: String, uid: String,
/// The sync status of this item /// The sync status of this item
@ -65,8 +66,8 @@ pub struct Task {
impl Task { impl Task {
/// Create a brand new Task that is not on a server yet. /// Create a brand new Task that is not on a server yet.
/// This will pick a new (random) task ID. /// This will pick a new (random) task ID.
pub fn new(name: String, completed: bool, parent_calendar_id: &CalendarId) -> Self { pub fn new(name: String, completed: bool, parent_calendar_url: &Url) -> Self {
let new_item_id = ItemId::random(parent_calendar_id); let new_url = random_url(parent_calendar_url);
let new_sync_status = SyncStatus::NotSynced; let new_sync_status = SyncStatus::NotSynced;
let new_uid = Uuid::new_v4().to_hyphenated().to_string(); let new_uid = Uuid::new_v4().to_hyphenated().to_string();
let new_creation_date = Some(Utc::now()); let new_creation_date = Some(Utc::now());
@ -76,18 +77,18 @@ impl Task {
} else { CompletionStatus::Uncompleted }; } else { CompletionStatus::Uncompleted };
let ical_prod_id = crate::ical::default_prod_id(); let ical_prod_id = crate::ical::default_prod_id();
let extra_parameters = Vec::new(); let extra_parameters = Vec::new();
Self::new_with_parameters(name, new_uid, new_item_id, 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 /// Create a new Task instance, that may be synced on the server already
pub fn new_with_parameters(name: String, uid: String, id: ItemId, pub fn new_with_parameters(name: String, uid: String, new_url: Url,
completion_status: CompletionStatus, completion_status: CompletionStatus,
sync_status: SyncStatus, creation_date: Option<DateTime<Utc>>, last_modified: DateTime<Utc>, sync_status: SyncStatus, creation_date: Option<DateTime<Utc>>, last_modified: DateTime<Utc>,
ical_prod_id: String, extra_parameters: Vec<Property>, ical_prod_id: String, extra_parameters: Vec<Property>,
) -> Self ) -> Self
{ {
Self { Self {
id, url: new_url,
uid, uid,
name, name,
completion_status, completion_status,
@ -99,7 +100,7 @@ impl Task {
} }
} }
pub fn id(&self) -> &ItemId { &self.id } pub fn url(&self) -> &Url { &self.url }
pub fn uid(&self) -> &str { &self.uid } pub fn uid(&self) -> &str { &self.uid }
pub fn name(&self) -> &str { &self.name } pub fn name(&self) -> &str { &self.name }
pub fn completed(&self) -> bool { self.completion_status.is_completed() } pub fn completed(&self) -> bool { self.completion_status.is_completed() }
@ -112,7 +113,8 @@ impl Task {
#[cfg(any(test, feature = "integration_tests"))] #[cfg(any(test, feature = "integration_tests"))]
pub fn has_same_observable_content_as(&self, other: &Task) -> bool { pub fn has_same_observable_content_as(&self, other: &Task) -> bool {
self.id == other.id self.url == other.url
&& self.uid == other.uid
&& self.name == other.name && self.name == other.name
// sync status must be the same variant, but we ignore its embedded version tag // sync status must be the same variant, but we ignore its embedded version tag
&& std::mem::discriminant(&self.sync_status) == std::mem::discriminant(&other.sync_status) && std::mem::discriminant(&self.sync_status) == std::mem::discriminant(&other.sync_status)

View file

@ -6,12 +6,11 @@ use std::sync::{Arc, Mutex};
use async_trait::async_trait; use async_trait::async_trait;
use csscolorparser::Color; use csscolorparser::Color;
use url::Url;
use crate::item::SyncStatus; use crate::item::SyncStatus;
use crate::item::Item; use crate::item::Item;
use crate::item::ItemId;
use crate::item::VersionTag; use crate::item::VersionTag;
use crate::calendar::CalendarId;
use crate::calendar::SupportedComponents; use crate::calendar::SupportedComponents;
use crate::resource::Resource; use crate::resource::Resource;
@ -22,11 +21,11 @@ use crate::resource::Resource;
pub trait CalDavSource<T: BaseCalendar> { pub trait CalDavSource<T: BaseCalendar> {
/// Returns the current calendars that this source contains /// Returns the current calendars that this source contains
/// This function may trigger an update (that can be a long process, or that can even fail, e.g. in case of a remote server) /// This function may trigger an update (that can be a long process, or that can even fail, e.g. in case of a remote server)
async fn get_calendars(&self) -> Result<HashMap<CalendarId, Arc<Mutex<T>>>, Box<dyn Error>>; async fn get_calendars(&self) -> Result<HashMap<Url, Arc<Mutex<T>>>, Box<dyn Error>>;
/// Returns the calendar matching the ID /// Returns the calendar matching the URL
async fn get_calendar(&self, id: &CalendarId) -> Option<Arc<Mutex<T>>>; async fn get_calendar(&self, url: &Url) -> Option<Arc<Mutex<T>>>;
/// Create a calendar if it did not exist, and return it /// Create a calendar if it did not exist, and return it
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents, color: Option<Color>) async fn create_calendar(&mut self, url: Url, name: String, supported_components: SupportedComponents, color: Option<Color>)
-> Result<Arc<Mutex<T>>, Box<dyn Error>>; -> Result<Arc<Mutex<T>>, Box<dyn Error>>;
// Removing a calendar is not supported yet // Removing a calendar is not supported yet
@ -40,8 +39,8 @@ pub trait BaseCalendar {
/// Returns the calendar name /// Returns the calendar name
fn name(&self) -> &str; fn name(&self) -> &str;
/// Returns the calendar unique ID /// Returns the calendar URL
fn id(&self) -> &CalendarId; fn url(&self) -> &Url;
/// Returns the supported kinds of components for this calendar /// Returns the supported kinds of components for this calendar
fn supported_components(&self) -> crate::calendar::SupportedComponents; fn supported_components(&self) -> crate::calendar::SupportedComponents;
@ -78,20 +77,20 @@ pub trait DavCalendar : BaseCalendar {
/// Create a new calendar /// Create a new calendar
fn new(name: String, resource: Resource, supported_components: SupportedComponents, color: Option<Color>) -> Self; fn new(name: String, resource: Resource, supported_components: SupportedComponents, color: Option<Color>) -> Self;
/// Get the IDs and the version tags of every item in this calendar /// Get the URLs and the version tags of every item in this calendar
async fn get_item_version_tags(&self) -> Result<HashMap<ItemId, VersionTag>, Box<dyn Error>>; async fn get_item_version_tags(&self) -> Result<HashMap<Url, VersionTag>, Box<dyn Error>>;
/// Returns a particular item /// Returns a particular item
async fn get_item_by_id(&self, id: &ItemId) -> Result<Option<Item>, Box<dyn Error>>; async fn get_item_by_url(&self, url: &Url) -> Result<Option<Item>, Box<dyn Error>>;
/// Delete an item /// Delete an item
async fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>>; async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>>;
/// Get the IDs of all current items in this calendar /// Get the URLs of all current items in this calendar
async fn get_item_ids(&self) -> Result<HashSet<ItemId>, Box<dyn Error>> { async fn get_item_urls(&self) -> Result<HashSet<Url>, Box<dyn Error>> {
let items = self.get_item_version_tags().await?; let items = self.get_item_version_tags().await?;
Ok(items.iter() Ok(items.iter()
.map(|(id, _tag)| id.clone()) .map(|(url, _tag)| url.clone())
.collect()) .collect())
} }
@ -108,25 +107,25 @@ pub trait DavCalendar : BaseCalendar {
#[async_trait] #[async_trait]
pub trait CompleteCalendar : BaseCalendar { pub trait CompleteCalendar : BaseCalendar {
/// Create a new calendar /// Create a new calendar
fn new(name: String, id: CalendarId, supported_components: SupportedComponents, color: Option<Color>) -> Self; fn new(name: String, url: Url, supported_components: SupportedComponents, color: Option<Color>) -> Self;
/// Get the IDs of all current items in this calendar /// Get the URLs of all current items in this calendar
async fn get_item_ids(&self) -> Result<HashSet<ItemId>, Box<dyn Error>>; async fn get_item_urls(&self) -> Result<HashSet<Url>, Box<dyn Error>>;
/// Returns all items that this calendar contains /// Returns all items that this calendar contains
async fn get_items(&self) -> Result<HashMap<ItemId, &Item>, Box<dyn Error>>; async fn get_items(&self) -> Result<HashMap<Url, &Item>, Box<dyn Error>>;
/// Returns a particular item /// Returns a particular item
async fn get_item_by_id<'a>(&'a self, id: &ItemId) -> Option<&'a Item>; async fn get_item_by_url<'a>(&'a self, url: &Url) -> Option<&'a Item>;
/// Returns a particular item /// Returns a particular item
async fn get_item_by_id_mut<'a>(&'a mut self, id: &ItemId) -> Option<&'a mut Item>; async fn get_item_by_url_mut<'a>(&'a mut self, url: &Url) -> Option<&'a mut Item>;
/// Mark an item for deletion. /// Mark an item for deletion.
/// This is required so that the upcoming sync will know it should also also delete this task from the server /// This is required so that the upcoming sync will know it should also also delete this task from the server
/// (and then call [`CompleteCalendar::immediately_delete_item`] once it has been successfully deleted on the server) /// (and then call [`CompleteCalendar::immediately_delete_item`] once it has been successfully deleted on the server)
async fn mark_for_deletion(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>>; async fn mark_for_deletion(&mut self, item_id: &Url) -> Result<(), Box<dyn Error>>;
/// Immediately remove an item. See [`CompleteCalendar::mark_for_deletion`] /// Immediately remove an item. See [`CompleteCalendar::mark_for_deletion`]
async fn immediately_delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>>; async fn immediately_delete_item(&mut self, item_id: &Url) -> Result<(), Box<dyn Error>>;
} }

View file

@ -6,10 +6,10 @@ use std::hash::Hash;
use std::io::{stdin, stdout, Read, Write}; use std::io::{stdin, stdout, Read, Write};
use minidom::Element; use minidom::Element;
use url::Url;
use crate::traits::CompleteCalendar; use crate::traits::CompleteCalendar;
use crate::traits::DavCalendar; use crate::traits::DavCalendar;
use crate::calendar::CalendarId;
use crate::Item; use crate::Item;
use crate::item::SyncStatus; use crate::item::SyncStatus;
@ -62,12 +62,12 @@ pub fn print_xml(element: &Element) {
} }
/// A debug utility that pretty-prints calendars /// A debug utility that pretty-prints calendars
pub async fn print_calendar_list<C>(cals: &HashMap<CalendarId, Arc<Mutex<C>>>) pub async fn print_calendar_list<C>(cals: &HashMap<Url, Arc<Mutex<C>>>)
where where
C: CompleteCalendar, C: CompleteCalendar,
{ {
for (id, cal) in cals { for (url, cal) in cals {
println!("CAL {} ({})", cal.lock().unwrap().name(), id); println!("CAL {} ({})", cal.lock().unwrap().name(), url);
match cal.lock().unwrap().get_items().await { match cal.lock().unwrap().get_items().await {
Err(_err) => continue, Err(_err) => continue,
Ok(map) => { Ok(map) => {
@ -80,17 +80,17 @@ where
} }
/// A debug utility that pretty-prints calendars /// A debug utility that pretty-prints calendars
pub async fn print_dav_calendar_list<C>(cals: &HashMap<CalendarId, Arc<Mutex<C>>>) pub async fn print_dav_calendar_list<C>(cals: &HashMap<Url, Arc<Mutex<C>>>)
where where
C: DavCalendar, C: DavCalendar,
{ {
for (id, cal) in cals { for (url, cal) in cals {
println!("CAL {} ({})", cal.lock().unwrap().name(), id); println!("CAL {} ({})", cal.lock().unwrap().name(), url);
match cal.lock().unwrap().get_item_version_tags().await { match cal.lock().unwrap().get_item_version_tags().await {
Err(_err) => continue, Err(_err) => continue,
Ok(map) => { Ok(map) => {
for (id, version_tag) in map { for (url, version_tag) in map {
println!(" * {} (version {:?})", id, version_tag); println!(" * {} (version {:?})", url, version_tag);
} }
}, },
} }
@ -107,7 +107,7 @@ pub fn print_task(item: &Item) {
SyncStatus::LocallyModified(_) => "~", SyncStatus::LocallyModified(_) => "~",
SyncStatus::LocallyDeleted(_) => "x", SyncStatus::LocallyDeleted(_) => "x",
}; };
println!(" {}{} {}\t{}", completion, sync, task.name(), task.id()); println!(" {}{} {}\t{}", completion, sync, task.name(), task.url());
}, },
_ => return, _ => return,
} }
@ -148,3 +148,10 @@ pub fn pause() {
stdout.flush().unwrap(); stdout.flush().unwrap();
stdin().read_exact(&mut [0]).unwrap(); 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();
parent_calendar.join(&random).unwrap(/* this cannot panic since we've just created a string that is a valid URL */)
}

View file

@ -11,10 +11,10 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::error::Error; use std::error::Error;
use url::Url;
use chrono::Utc; use chrono::Utc;
use kitchen_fridge::calendar::CalendarId;
use kitchen_fridge::calendar::SupportedComponents; use kitchen_fridge::calendar::SupportedComponents;
use kitchen_fridge::traits::CalDavSource; use kitchen_fridge::traits::CalDavSource;
use kitchen_fridge::traits::BaseCalendar; use kitchen_fridge::traits::BaseCalendar;
@ -22,13 +22,13 @@ use kitchen_fridge::traits::CompleteCalendar;
use kitchen_fridge::traits::DavCalendar; use kitchen_fridge::traits::DavCalendar;
use kitchen_fridge::cache::Cache; use kitchen_fridge::cache::Cache;
use kitchen_fridge::Item; use kitchen_fridge::Item;
use kitchen_fridge::item::ItemId;
use kitchen_fridge::item::SyncStatus; use kitchen_fridge::item::SyncStatus;
use kitchen_fridge::Task; use kitchen_fridge::Task;
use kitchen_fridge::task::CompletionStatus; use kitchen_fridge::task::CompletionStatus;
use kitchen_fridge::calendar::cached_calendar::CachedCalendar; use kitchen_fridge::calendar::cached_calendar::CachedCalendar;
use kitchen_fridge::provider::Provider; use kitchen_fridge::provider::Provider;
use kitchen_fridge::mock_behaviour::MockBehaviour; use kitchen_fridge::mock_behaviour::MockBehaviour;
use kitchen_fridge::utils::random_url;
pub enum LocatedState { pub enum LocatedState {
/// Item does not exist yet or does not exist anymore /// Item does not exist yet or does not exist anymore
@ -44,7 +44,7 @@ pub enum LocatedState {
pub struct ItemState { pub struct ItemState {
// TODO: if/when this crate supports Events as well, we could add such events here // TODO: if/when this crate supports Events as well, we could add such events here
/// The calendar it is in /// The calendar it is in
calendar: CalendarId, calendar: Url,
/// Its name /// Its name
name: String, name: String,
/// Its completion status /// Its completion status
@ -54,15 +54,15 @@ pub struct ItemState {
pub enum ChangeToApply { pub enum ChangeToApply {
Rename(String), Rename(String),
SetCompletion(bool), SetCompletion(bool),
Create(CalendarId, Item), Create(Url, Item),
/// "remove" means "mark for deletion" in the local calendar, or "immediately delete" on the remote calendar /// "remove" means "mark for deletion" in the local calendar, or "immediately delete" on the remote calendar
Remove, Remove,
// ChangeCalendar(CalendarId) is useless, as long as changing a calendar is implemented as "delete in one calendar and re-create it in another one" // 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 { pub struct ItemScenario {
id: ItemId, url: Url,
initial_state: LocatedState, initial_state: LocatedState,
local_changes_to_apply: Vec<ChangeToApply>, local_changes_to_apply: Vec<ChangeToApply>,
remote_changes_to_apply: Vec<ChangeToApply>, remote_changes_to_apply: Vec<ChangeToApply>,
@ -87,13 +87,13 @@ pub struct ItemScenario {
pub fn scenarii_basic() -> Vec<ItemScenario> { pub fn scenarii_basic() -> Vec<ItemScenario> {
let mut tasks = Vec::new(); let mut tasks = Vec::new();
let first_cal = CalendarId::from("https://some.calend.ar/calendar-1/".parse().unwrap()); let first_cal = Url::from("https://some.calend.ar/calendar-1/".parse().unwrap());
let second_cal = CalendarId::from("https://some.calend.ar/calendar-2/".parse().unwrap()); let second_cal = Url::from("https://some.calend.ar/calendar-2/".parse().unwrap());
let third_cal = CalendarId::from("https://some.calend.ar/calendar-3/".parse().unwrap()); let third_cal = Url::from("https://some.calend.ar/calendar-3/".parse().unwrap());
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&first_cal), url: random_url(&first_cal),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: first_cal.clone(), calendar: first_cal.clone(),
name: String::from("Task A"), name: String::from("Task A"),
@ -111,7 +111,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&first_cal), url: random_url(&first_cal),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: first_cal.clone(), calendar: first_cal.clone(),
name: String::from("Task B"), name: String::from("Task B"),
@ -125,7 +125,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&first_cal), url: random_url(&first_cal),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: first_cal.clone(), calendar: first_cal.clone(),
name: String::from("Task C"), name: String::from("Task C"),
@ -139,7 +139,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&first_cal), url: random_url(&first_cal),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: first_cal.clone(), calendar: first_cal.clone(),
name: String::from("Task D"), name: String::from("Task D"),
@ -157,7 +157,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&first_cal), url: random_url(&first_cal),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: first_cal.clone(), calendar: first_cal.clone(),
name: String::from("Task E"), name: String::from("Task E"),
@ -175,7 +175,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&first_cal), url: random_url(&first_cal),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: first_cal.clone(), calendar: first_cal.clone(),
name: String::from("Task F"), name: String::from("Task F"),
@ -194,7 +194,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&second_cal), url: random_url(&second_cal),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: second_cal.clone(), calendar: second_cal.clone(),
name: String::from("Task G"), name: String::from("Task G"),
@ -212,7 +212,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&second_cal), url: random_url(&second_cal),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: second_cal.clone(), calendar: second_cal.clone(),
name: String::from("Task H"), name: String::from("Task H"),
@ -230,7 +230,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&second_cal), url: random_url(&second_cal),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: second_cal.clone(), calendar: second_cal.clone(),
name: String::from("Task I"), name: String::from("Task I"),
@ -249,7 +249,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&second_cal), url: random_url(&second_cal),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: second_cal.clone(), calendar: second_cal.clone(),
name: String::from("Task J"), name: String::from("Task J"),
@ -263,7 +263,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&second_cal), url: random_url(&second_cal),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: second_cal.clone(), calendar: second_cal.clone(),
name: String::from("Task K"), name: String::from("Task K"),
@ -281,7 +281,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&second_cal), url: random_url(&second_cal),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: second_cal.clone(), calendar: second_cal.clone(),
name: String::from("Task L"), name: String::from("Task L"),
@ -295,7 +295,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&second_cal), url: random_url(&second_cal),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: second_cal.clone(), calendar: second_cal.clone(),
name: String::from("Task M"), name: String::from("Task M"),
@ -313,7 +313,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&third_cal), url: random_url(&third_cal),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: third_cal.clone(), calendar: third_cal.clone(),
name: String::from("Task N"), name: String::from("Task N"),
@ -331,7 +331,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&third_cal), url: random_url(&third_cal),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: third_cal.clone(), calendar: third_cal.clone(),
name: String::from("Task O"), name: String::from("Task O"),
@ -347,10 +347,10 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
} }
); );
let id_p = ItemId::random(&third_cal); let url_p = random_url(&third_cal);
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: id_p.clone(), url: url_p.clone(),
initial_state: LocatedState::BothSynced( ItemState{ initial_state: LocatedState::BothSynced( ItemState{
calendar: third_cal.clone(), calendar: third_cal.clone(),
name: String::from("Task P"), name: String::from("Task P"),
@ -369,16 +369,16 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
} }
); );
let id_q = ItemId::random(&third_cal); let url_q = random_url(&third_cal);
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: id_q.clone(), url: url_q.clone(),
initial_state: LocatedState::None, initial_state: LocatedState::None,
local_changes_to_apply: Vec::new(), local_changes_to_apply: Vec::new(),
remote_changes_to_apply: vec![ChangeToApply::Create(third_cal.clone(), Item::Task( remote_changes_to_apply: vec![ChangeToApply::Create(third_cal.clone(), Item::Task(
Task::new_with_parameters( Task::new_with_parameters(
String::from("Task Q, created on the server"), String::from("Task Q, created on the server"),
id_q.to_string(), id_q, url_q.to_string(), url_q,
CompletionStatus::Uncompleted, CompletionStatus::Uncompleted,
SyncStatus::random_synced(), Some(Utc::now()), Utc::now(), "prod_id".to_string(), Vec::new() ) SyncStatus::random_synced(), Some(Utc::now()), Utc::now(), "prod_id".to_string(), Vec::new() )
))], ))],
@ -390,15 +390,15 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
} }
); );
let id_r = ItemId::random(&third_cal); let url_r = random_url(&third_cal);
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: id_r.clone(), url: url_r.clone(),
initial_state: LocatedState::None, initial_state: LocatedState::None,
local_changes_to_apply: vec![ChangeToApply::Create(third_cal.clone(), Item::Task( local_changes_to_apply: vec![ChangeToApply::Create(third_cal.clone(), Item::Task(
Task::new_with_parameters( Task::new_with_parameters(
String::from("Task R, created locally"), String::from("Task R, created locally"),
id_r.to_string(), id_r, url_r.to_string(), url_r,
CompletionStatus::Uncompleted, CompletionStatus::Uncompleted,
SyncStatus::NotSynced, Some(Utc::now()), Utc::now(), "prod_id".to_string(), Vec::new() ) SyncStatus::NotSynced, Some(Utc::now()), Utc::now(), "prod_id".to_string(), Vec::new() )
))], ))],
@ -418,12 +418,12 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
pub fn scenarii_first_sync_to_local() -> Vec<ItemScenario> { pub fn scenarii_first_sync_to_local() -> Vec<ItemScenario> {
let mut tasks = Vec::new(); let mut tasks = Vec::new();
let cal1 = CalendarId::from("https://some.calend.ar/first/".parse().unwrap()); let cal1 = Url::from("https://some.calend.ar/first/".parse().unwrap());
let cal2 = CalendarId::from("https://some.calend.ar/second/".parse().unwrap()); let cal2 = Url::from("https://some.calend.ar/second/".parse().unwrap());
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&cal1), url: random_url(&cal1),
initial_state: LocatedState::Remote( ItemState{ initial_state: LocatedState::Remote( ItemState{
calendar: cal1.clone(), calendar: cal1.clone(),
name: String::from("Task A1"), name: String::from("Task A1"),
@ -441,7 +441,7 @@ pub fn scenarii_first_sync_to_local() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&cal2), url: random_url(&cal2),
initial_state: LocatedState::Remote( ItemState{ initial_state: LocatedState::Remote( ItemState{
calendar: cal2.clone(), calendar: cal2.clone(),
name: String::from("Task A2"), name: String::from("Task A2"),
@ -459,7 +459,7 @@ pub fn scenarii_first_sync_to_local() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&cal1), url: random_url(&cal1),
initial_state: LocatedState::Remote( ItemState{ initial_state: LocatedState::Remote( ItemState{
calendar: cal1.clone(), calendar: cal1.clone(),
name: String::from("Task B1"), name: String::from("Task B1"),
@ -482,12 +482,12 @@ pub fn scenarii_first_sync_to_local() -> Vec<ItemScenario> {
pub fn scenarii_first_sync_to_server() -> Vec<ItemScenario> { pub fn scenarii_first_sync_to_server() -> Vec<ItemScenario> {
let mut tasks = Vec::new(); let mut tasks = Vec::new();
let cal3 = CalendarId::from("https://some.calend.ar/third/".parse().unwrap()); let cal3 = Url::from("https://some.calend.ar/third/".parse().unwrap());
let cal4 = CalendarId::from("https://some.calend.ar/fourth/".parse().unwrap()); let cal4 = Url::from("https://some.calend.ar/fourth/".parse().unwrap());
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&cal3), url: random_url(&cal3),
initial_state: LocatedState::Local( ItemState{ initial_state: LocatedState::Local( ItemState{
calendar: cal3.clone(), calendar: cal3.clone(),
name: String::from("Task A3"), name: String::from("Task A3"),
@ -505,7 +505,7 @@ pub fn scenarii_first_sync_to_server() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&cal4), url: random_url(&cal4),
initial_state: LocatedState::Local( ItemState{ initial_state: LocatedState::Local( ItemState{
calendar: cal4.clone(), calendar: cal4.clone(),
name: String::from("Task A4"), name: String::from("Task A4"),
@ -523,7 +523,7 @@ pub fn scenarii_first_sync_to_server() -> Vec<ItemScenario> {
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&cal3), url: random_url(&cal3),
initial_state: LocatedState::Local( ItemState{ initial_state: LocatedState::Local( ItemState{
calendar: cal3.clone(), calendar: cal3.clone(),
name: String::from("Task B3"), name: String::from("Task B3"),
@ -547,11 +547,11 @@ pub fn scenarii_first_sync_to_server() -> Vec<ItemScenario> {
pub fn scenarii_transient_task() -> Vec<ItemScenario> { pub fn scenarii_transient_task() -> Vec<ItemScenario> {
let mut tasks = Vec::new(); let mut tasks = Vec::new();
let cal = CalendarId::from("https://some.calend.ar/transient/".parse().unwrap()); let cal = Url::from("https://some.calend.ar/transient/".parse().unwrap());
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: ItemId::random(&cal), url: random_url(&cal),
initial_state: LocatedState::Local( ItemState{ initial_state: LocatedState::Local( ItemState{
calendar: cal.clone(), calendar: cal.clone(),
name: String::from("A task, so that the calendar actually exists"), name: String::from("A task, so that the calendar actually exists"),
@ -567,16 +567,16 @@ pub fn scenarii_transient_task() -> Vec<ItemScenario> {
} }
); );
let id_transient = ItemId::random(&cal); let url_transient = random_url(&cal);
tasks.push( tasks.push(
ItemScenario { ItemScenario {
id: id_transient.clone(), url: url_transient.clone(),
initial_state: LocatedState::None, initial_state: LocatedState::None,
local_changes_to_apply: vec![ local_changes_to_apply: vec![
ChangeToApply::Create(cal, Item::Task( ChangeToApply::Create(cal, Item::Task(
Task::new_with_parameters( Task::new_with_parameters(
String::from("A transient task that will be deleted before the sync"), String::from("A transient task that will be deleted before the sync"),
id_transient.to_string(), id_transient, url_transient.to_string(), url_transient,
CompletionStatus::Uncompleted, CompletionStatus::Uncompleted,
SyncStatus::NotSynced, Some(Utc::now()), Utc::now(), SyncStatus::NotSynced, Some(Utc::now()), Utc::now(),
"prod_id".to_string(), Vec::new() ) "prod_id".to_string(), Vec::new() )
@ -637,8 +637,8 @@ async fn populate_test_provider(scenarii: &[ItemScenario], mock_behaviour: Arc<M
let new_item = Item::Task( let new_item = Item::Task(
Task::new_with_parameters( Task::new_with_parameters(
state.name.clone(), state.name.clone(),
item.id.to_string(), item.url.to_string(),
item.id.clone(), item.url.clone(),
completion_status, completion_status,
sync_status, sync_status,
Some(now), Some(now),
@ -667,37 +667,37 @@ async fn populate_test_provider(scenarii: &[ItemScenario], mock_behaviour: Arc<M
async fn apply_changes_on_provider(provider: &mut Provider<Cache, CachedCalendar, Cache, CachedCalendar>, scenarii: &[ItemScenario]) { async fn apply_changes_on_provider(provider: &mut Provider<Cache, CachedCalendar, Cache, CachedCalendar>, scenarii: &[ItemScenario]) {
// Apply changes to each item // Apply changes to each item
for item in scenarii { for item in scenarii {
let initial_calendar_id = match &item.initial_state { let initial_calendar_url = match &item.initial_state {
LocatedState::None => None, LocatedState::None => None,
LocatedState::Local(state) => Some(state.calendar.clone()), LocatedState::Local(state) => Some(state.calendar.clone()),
LocatedState::Remote(state) => Some(state.calendar.clone()), LocatedState::Remote(state) => Some(state.calendar.clone()),
LocatedState::BothSynced(state) => Some(state.calendar.clone()), LocatedState::BothSynced(state) => Some(state.calendar.clone()),
}; };
let mut calendar_id = initial_calendar_id.clone(); let mut calendar_url = initial_calendar_url.clone();
for local_change in &item.local_changes_to_apply { for local_change in &item.local_changes_to_apply {
calendar_id = Some(apply_change(provider.local(), calendar_id, &item.id, local_change, false).await); calendar_url = Some(apply_change(provider.local(), calendar_url, &item.url, local_change, false).await);
} }
let mut calendar_id = initial_calendar_id; let mut calendar_url = initial_calendar_url;
for remote_change in &item.remote_changes_to_apply { for remote_change in &item.remote_changes_to_apply {
calendar_id = Some(apply_change(provider.remote(), calendar_id, &item.id, remote_change, true).await); calendar_url = Some(apply_change(provider.remote(), calendar_url, &item.url, remote_change, true).await);
} }
} }
} }
async fn get_or_insert_calendar(source: &mut Cache, id: &CalendarId) async fn get_or_insert_calendar(source: &mut Cache, url: &Url)
-> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>> -> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>>
{ {
match source.get_calendar(id).await { match source.get_calendar(url).await {
Some(cal) => Ok(cal), Some(cal) => Ok(cal),
None => { None => {
let new_name = format!("Test calendar for ID {}", id); let new_name = format!("Test calendar for URL {}", url);
let supported_components = SupportedComponents::TODO; let supported_components = SupportedComponents::TODO;
let color = csscolorparser::parse("#ff8000"); // TODO: we should rather have specific colors, depending on the calendars let color = csscolorparser::parse("#ff8000"); // TODO: we should rather have specific colors, depending on the calendars
source.create_calendar( source.create_calendar(
id.clone(), url.clone(),
new_name.to_string(), new_name.to_string(),
supported_components, supported_components,
None, None,
@ -706,15 +706,15 @@ async fn get_or_insert_calendar(source: &mut Cache, id: &CalendarId)
} }
} }
/// Apply a single change on a given source, and returns the calendar ID that was modified /// Apply a single change on a given source, and returns the calendar URL that was modified
async fn apply_change<S, C>(source: &S, calendar_id: Option<CalendarId>, item_id: &ItemId, change: &ChangeToApply, is_remote: bool) -> CalendarId async fn apply_change<S, C>(source: &S, calendar_url: Option<Url>, item_url: &Url, change: &ChangeToApply, is_remote: bool) -> Url
where where
S: CalDavSource<C>, S: CalDavSource<C>,
C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds
{ {
match calendar_id { match calendar_url {
Some(cal) => { Some(cal) => {
apply_changes_on_an_existing_item(source, &cal, item_id, change, is_remote).await; apply_changes_on_an_existing_item(source, &cal, item_url, change, is_remote).await;
cal cal
}, },
None => { None => {
@ -723,14 +723,14 @@ where
} }
} }
async fn apply_changes_on_an_existing_item<S, C>(source: &S, calendar_id: &CalendarId, item_id: &ItemId, change: &ChangeToApply, is_remote: bool) async fn apply_changes_on_an_existing_item<S, C>(source: &S, calendar_url: &Url, item_url: &Url, change: &ChangeToApply, is_remote: bool)
where where
S: CalDavSource<C>, S: CalDavSource<C>,
C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds
{ {
let cal = source.get_calendar(calendar_id).await.unwrap(); let cal = source.get_calendar(calendar_url).await.unwrap();
let mut cal = cal.lock().unwrap(); let mut cal = cal.lock().unwrap();
let task = cal.get_item_by_id_mut(item_id).await.unwrap().unwrap_task_mut(); let task = cal.get_item_by_url_mut(item_url).await.unwrap().unwrap_task_mut();
match change { match change {
ChangeToApply::Rename(new_name) => { ChangeToApply::Rename(new_name) => {
@ -753,18 +753,18 @@ where
}, },
ChangeToApply::Remove => { ChangeToApply::Remove => {
match is_remote { match is_remote {
false => cal.mark_for_deletion(item_id).await.unwrap(), false => cal.mark_for_deletion(item_url).await.unwrap(),
true => cal.delete_item(item_id).await.unwrap(), true => cal.delete_item(item_url).await.unwrap(),
}; };
}, },
ChangeToApply::Create(_calendar_id, _item) => { ChangeToApply::Create(_calendar_url, _item) => {
panic!("This function only handles already existing items"); panic!("This function only handles already existing items");
}, },
} }
} }
/// Create an item, and returns the calendar ID it was inserted in /// Create an item, and returns the URL of the calendar it was inserted in
async fn create_test_item<S, C>(source: &S, change: &ChangeToApply) -> CalendarId async fn create_test_item<S, C>(source: &S, change: &ChangeToApply) -> Url
where where
S: CalDavSource<C>, S: CalDavSource<C>,
C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds
@ -775,10 +775,10 @@ where
ChangeToApply::Remove => { ChangeToApply::Remove => {
panic!("This function only creates items that do not exist yet"); panic!("This function only creates items that do not exist yet");
} }
ChangeToApply::Create(calendar_id, item) => { ChangeToApply::Create(calendar_url, item) => {
let cal = source.get_calendar(calendar_id).await.unwrap(); let cal = source.get_calendar(calendar_url).await.unwrap();
cal.lock().unwrap().add_item(item.clone()).await.unwrap(); cal.lock().unwrap().add_item(item.clone()).await.unwrap();
calendar_id.clone() calendar_url.clone()
}, },
} }
} }

View file

@ -124,7 +124,7 @@ impl TestFlavour {
Self { Self {
scenarii: scenarii::scenarii_basic(), scenarii: scenarii::scenarii_basic(),
mock_behaviour: Arc::new(Mutex::new(MockBehaviour{ mock_behaviour: Arc::new(Mutex::new(MockBehaviour{
get_item_by_id_behaviour: (3,2), get_item_by_url_behaviour: (3,2),
..MockBehaviour::default() ..MockBehaviour::default()
})), })),
} }
@ -145,7 +145,7 @@ impl TestFlavour {
scenarii: scenarii::scenarii_basic(), scenarii: scenarii::scenarii_basic(),
mock_behaviour: Arc::new(Mutex::new(MockBehaviour{ mock_behaviour: Arc::new(Mutex::new(MockBehaviour{
add_item_behaviour: (2,3), add_item_behaviour: (2,3),
get_item_by_id_behaviour: (1,4), get_item_by_url_behaviour: (1,4),
..MockBehaviour::default() ..MockBehaviour::default()
})), })),
} }
@ -183,7 +183,7 @@ impl TestFlavour {
delete_item_behaviour: (1,1), delete_item_behaviour: (1,1),
create_calendar_behaviour: (1,4), create_calendar_behaviour: (1,4),
get_item_version_tags_behaviour: (3,1), get_item_version_tags_behaviour: (3,1),
get_item_by_id_behaviour: (0,41), get_item_by_url_behaviour: (0,41),
..MockBehaviour::default() ..MockBehaviour::default()
})), })),
} }