diff --git a/examples/provider-sync.rs b/examples/provider-sync.rs index 8bbe9d0..a93dbf5 100644 --- a/examples/provider-sync.rs +++ b/examples/provider-sync.rs @@ -3,13 +3,13 @@ use std::path::Path; use chrono::{Utc}; +use url::Url; use kitchen_fridge::{client::Client, traits::CalDavSource}; use kitchen_fridge::calendar::{CalendarId, SupportedComponents}; use kitchen_fridge::Item; use kitchen_fridge::Task; use kitchen_fridge::task::CompletionStatus; -use kitchen_fridge::item::ItemId; use kitchen_fridge::cache::Cache; use kitchen_fridge::CalDavProvider; use kitchen_fridge::traits::BaseCalendar; @@ -100,7 +100,7 @@ async fn add_items_and_sync_again(provider: &mut CalDavProvider) let changed_calendar_id: CalendarId = EXAMPLE_EXISTING_CALENDAR_URL.parse().unwrap(); let new_task_name = "This is a new task we're adding as an example, with ÜTF-8 characters"; let new_task = Task::new(String::from(new_task_name), false, &changed_calendar_id); - let new_id = new_task.id().clone(); + let new_url = new_task.url().clone(); provider.local().get_calendar(&changed_calendar_id).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(); - complete_item_and_sync_again(provider, &changed_calendar_id, &new_id).await; + complete_item_and_sync_again(provider, &changed_calendar_id, &new_url).await; } async fn complete_item_and_sync_again( provider: &mut CalDavProvider, changed_calendar_id: &CalendarId, - id_to_complete: &ItemId) + url_to_complete: &Url) { println!("\nNow, we'll mark this last task as completed, and run the sync again."); pause(); let completion_status = CompletionStatus::Completed(Some(Utc::now())); provider.local().get_calendar(changed_calendar_id).await.unwrap() - .lock().unwrap().get_item_by_id_mut(id_to_complete).await.unwrap() + .lock().unwrap().get_item_by_id_mut(url_to_complete).await.unwrap() .unwrap_task_mut() .set_completion_status(completion_status); @@ -136,13 +136,13 @@ async fn complete_item_and_sync_again( } 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_id, url_to_complete).await; } async fn remove_items_and_sync_again( provider: &mut CalDavProvider, changed_calendar_id: &CalendarId, - id_to_remove: &ItemId) + id_to_remove: &Url) { println!("\nNow, we'll delete this last task, and run the sync again."); pause(); diff --git a/src/cache.rs b/src/cache.rs index bc12968..926ec43 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -86,7 +86,7 @@ impl Cache { continue; }, Ok(cal) => - data.calendars.insert(cal.id().clone(), Arc::new(Mutex::new(cal))), + data.calendars.insert(cal.url().clone(), Arc::new(Mutex::new(cal))), }; } }, @@ -259,13 +259,13 @@ mod tests { { 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( - String::from("Attend a concert of JS Bach"), false, &cal_id + String::from("Attend a concert of JS Bach"), false, &cal_url ))).await.unwrap(); bucket_list.add_item(Item::Task(Task::new( - String::from("Climb the Lighthouse of Alexandria"), true, &cal_id + String::from("Climb the Lighthouse of Alexandria"), true, &cal_url ))).await.unwrap(); } diff --git a/src/calendar/cached_calendar.rs b/src/calendar/cached_calendar.rs index ec4f8a0..aa14db8 100644 --- a/src/calendar/cached_calendar.rs +++ b/src/calendar/cached_calendar.rs @@ -4,12 +4,12 @@ use std::error::Error; use serde::{Deserialize, Serialize}; use async_trait::async_trait; use csscolorparser::Color; +use url::Url; use crate::item::SyncStatus; use crate::traits::{BaseCalendar, CompleteCalendar}; use crate::calendar::{CalendarId, SupportedComponents}; use crate::Item; -use crate::item::ItemId; #[cfg(feature = "local_calendar_mocks_remote_calendars")] use std::sync::{Arc, Mutex}; @@ -23,14 +23,14 @@ use crate::mock_behaviour::MockBehaviour; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CachedCalendar { name: String, - id: CalendarId, + url: Url, supported_components: SupportedComponents, color: Option, #[cfg(feature = "local_calendar_mocks_remote_calendars")] #[serde(skip)] mock_behaviour: Option>>, - items: HashMap, + items: HashMap, } impl CachedCalendar { @@ -65,7 +65,7 @@ impl CachedCalendar { fn regular_add_or_update_item(&mut self, item: Item) -> Result> { let ss_clone = item.sync_status().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) } @@ -78,7 +78,7 @@ impl CachedCalendar { _ => item.set_sync_status(SyncStatus::random_synced()), }; let ss_clone = item.sync_status().clone(); - self.items.insert(item.id().clone(), item); + self.items.insert(item.url().clone(), item); Ok(ss_clone) } @@ -86,7 +86,7 @@ impl CachedCalendar { #[cfg(any(test, feature = "integration_tests"))] pub async fn has_same_observable_content_as(&self, other: &CachedCalendar) -> Result> { if self.name != other.name - || self.id != other.id + || self.url != other.url || self.supported_components != other.supported_components || self.color != other.color { @@ -119,35 +119,35 @@ impl CachedCalendar { } /// The non-async version of [`Self::get_item_ids`] - pub fn get_item_ids_sync(&self) -> Result, Box> { + pub fn get_item_ids_sync(&self) -> Result, Box> { Ok(self.items.iter() - .map(|(id, _)| id.clone()) + .map(|(url, _)| url.clone()) .collect() ) } /// The non-async version of [`Self::get_items`] - pub fn get_items_sync(&self) -> Result, Box> { + pub fn get_items_sync(&self) -> Result, Box> { Ok(self.items.iter() - .map(|(id, item)| (id.clone(), item)) + .map(|(url, item)| (url.clone(), item)) .collect() ) } /// The non-async version of [`Self::get_item_by_id`] - pub fn get_item_by_id_sync<'a>(&'a self, id: &ItemId) -> Option<&'a Item> { + pub fn get_item_by_id_sync<'a>(&'a self, id: &Url) -> Option<&'a Item> { self.items.get(id) } /// The non-async version of [`Self::get_item_by_id_mut`] - pub fn get_item_by_id_mut_sync<'a>(&'a mut self, id: &ItemId) -> Option<&'a mut Item> { + pub fn get_item_by_id_mut_sync<'a>(&'a mut self, id: &Url) -> Option<&'a mut Item> { self.items.get_mut(id) } /// The non-async version of [`Self::add_item`] pub fn add_item_sync(&mut self, item: Item) -> Result> { - if self.items.contains_key(item.id()) { - return Err(format!("Item {:?} cannot be added, it exists already", item.id()).into()); + if self.items.contains_key(item.url()) { + return Err(format!("Item {:?} cannot be added, it exists already", item.url()).into()); } #[cfg(not(feature = "local_calendar_mocks_remote_calendars"))] return self.regular_add_or_update_item(item); @@ -158,8 +158,8 @@ impl CachedCalendar { /// The non-async version of [`Self::update_item`] pub fn update_item_sync(&mut self, item: Item) -> Result> { - if self.items.contains_key(item.id()) == false { - return Err(format!("Item {:?} cannot be updated, it does not already exist", item.id()).into()); + if self.items.contains_key(item.url()) == false { + return Err(format!("Item {:?} cannot be updated, it does not already exist", item.url()).into()); } #[cfg(not(feature = "local_calendar_mocks_remote_calendars"))] return self.regular_add_or_update_item(item); @@ -169,7 +169,7 @@ impl CachedCalendar { } /// The non-async version of [`Self::mark_for_deletion`] - pub fn mark_for_deletion_sync(&mut self, item_id: &ItemId) -> Result<(), Box> { + pub fn mark_for_deletion_sync(&mut self, item_id: &Url) -> Result<(), Box> { match self.items.get_mut(item_id) { None => Err("no item for this key".into()), Some(item) => { @@ -197,7 +197,7 @@ impl CachedCalendar { } /// The non-async version of [`Self::immediately_delete_item`] - pub fn immediately_delete_item_sync(&mut self, item_id: &ItemId) -> Result<(), Box> { + pub fn immediately_delete_item_sync(&mut self, item_id: &Url) -> Result<(), Box> { match self.items.remove(item_id) { None => Err(format!("Item {} is absent from this calendar", item_id).into()), Some(_) => Ok(()) @@ -213,8 +213,8 @@ impl BaseCalendar for CachedCalendar { &self.name } - fn id(&self) -> &CalendarId { - &self.id + fn url(&self) -> &Url { + &self.url } fn supported_components(&self) -> SupportedComponents { @@ -236,36 +236,36 @@ impl BaseCalendar for CachedCalendar { #[async_trait] impl CompleteCalendar for CachedCalendar { - fn new(name: String, id: CalendarId, supported_components: SupportedComponents, color: Option) -> Self { + fn new(name: String, url: CalendarId, supported_components: SupportedComponents, color: Option) -> Self { Self { - name, id, supported_components, color, + name, url, supported_components, color, #[cfg(feature = "local_calendar_mocks_remote_calendars")] mock_behaviour: None, items: HashMap::new(), } } - async fn get_item_ids(&self) -> Result, Box> { + async fn get_item_ids(&self) -> Result, Box> { self.get_item_ids_sync() } - async fn get_items(&self) -> Result, Box> { + async fn get_items(&self) -> Result, Box> { self.get_items_sync() } - async fn get_item_by_id<'a>(&'a self, id: &ItemId) -> Option<&'a Item> { + async fn get_item_by_id<'a>(&'a self, id: &Url) -> Option<&'a Item> { self.get_item_by_id_sync(id) } - async fn get_item_by_id_mut<'a>(&'a mut self, id: &ItemId) -> Option<&'a mut Item> { + async fn get_item_by_id_mut<'a>(&'a mut self, id: &Url) -> Option<&'a mut Item> { self.get_item_by_id_mut_sync(id) } - async fn mark_for_deletion(&mut self, item_id: &ItemId) -> Result<(), Box> { + async fn mark_for_deletion(&mut self, item_id: &Url) -> Result<(), Box> { self.mark_for_deletion_sync(item_id) } - async fn immediately_delete_item(&mut self, item_id: &ItemId) -> Result<(), Box> { + async fn immediately_delete_item(&mut self, item_id: &Url) -> Result<(), Box> { self.immediately_delete_item_sync(item_id) } } @@ -286,7 +286,7 @@ impl DavCalendar for CachedCalendar { crate::traits::CompleteCalendar::new(name, resource.url().clone(), supported_components, color) } - async fn get_item_version_tags(&self) -> Result, Box> { + async fn get_item_version_tags(&self) -> Result, Box> { #[cfg(feature = "local_calendar_mocks_remote_calendars")] self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_item_version_tags())?; @@ -307,14 +307,14 @@ impl DavCalendar for CachedCalendar { Ok(result) } - async fn get_item_by_id(&self, id: &ItemId) -> Result, Box> { + async fn get_item_by_id(&self, id: &Url) -> Result, Box> { #[cfg(feature = "local_calendar_mocks_remote_calendars")] self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_item_by_id())?; Ok(self.items.get(id).cloned()) } - async fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box> { + async fn delete_item(&mut self, item_id: &Url) -> Result<(), Box> { #[cfg(feature = "local_calendar_mocks_remote_calendars")] self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_delete_item())?; diff --git a/src/calendar/remote_calendar.rs b/src/calendar/remote_calendar.rs index 14912d4..050c78a 100644 --- a/src/calendar/remote_calendar.rs +++ b/src/calendar/remote_calendar.rs @@ -5,13 +5,12 @@ use std::sync::Mutex; use async_trait::async_trait; use reqwest::{header::CONTENT_TYPE, header::CONTENT_LENGTH}; use csscolorparser::Color; +use url::Url; use crate::traits::BaseCalendar; use crate::traits::DavCalendar; use crate::calendar::SupportedComponents; -use crate::calendar::CalendarId; use crate::item::Item; -use crate::item::ItemId; use crate::item::VersionTag; use crate::item::SyncStatus; use crate::resource::Resource; @@ -40,13 +39,13 @@ pub struct RemoteCalendar { supported_components: SupportedComponents, color: Option, - cached_version_tags: Mutex>>, + cached_version_tags: Mutex>>, } #[async_trait] impl BaseCalendar for RemoteCalendar { 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 { self.supported_components } @@ -58,7 +57,7 @@ impl BaseCalendar for RemoteCalendar { let ical_text = crate::ical::build_from(&item)?; let response = reqwest::Client::new() - .put(item.id().as_url().clone()) + .put(item.url().clone()) .header("If-None-Match", "*") .header(CONTENT_TYPE, "text/calendar") .header(CONTENT_LENGTH, ical_text.len()) @@ -73,7 +72,7 @@ impl BaseCalendar for RemoteCalendar { let reply_hdrs = response.headers(); 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) => { let vtag_str = etag.to_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 request = reqwest::Client::new() - .put(item.id().as_url().clone()) + .put(item.url().clone()) .header("If-Match", old_etag.as_str()) .header(CONTENT_TYPE, "text/calendar") .header(CONTENT_LENGTH, ical_text.len()) @@ -107,7 +106,7 @@ impl BaseCalendar for RemoteCalendar { let reply_hdrs = request.headers(); 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) => { let vtag_str = etag.to_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, Box> { + async fn get_item_version_tags(&self) -> Result, Box> { if let Some(map) = &*self.cached_version_tags.lock().unwrap() { log::debug!("Version tags are already cached."); return Ok(map.clone()); @@ -145,7 +144,7 @@ impl DavCalendar for RemoteCalendar { continue; }, Some(resource) => { - ItemId::from(&resource) + resource.url().clone() }, }; @@ -159,7 +158,7 @@ impl DavCalendar for RemoteCalendar { } }; - items.insert(item_id, version_tag); + items.insert(item_id.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) @@ -167,9 +166,9 @@ impl DavCalendar for RemoteCalendar { Ok(items) } - async fn get_item_by_id(&self, id: &ItemId) -> Result, Box> { + async fn get_item_by_id(&self, id: &Url) -> Result, Box> { let res = reqwest::Client::new() - .get(id.as_url().clone()) + .get(id.clone()) .header(CONTENT_TYPE, "text/calendar") .basic_auth(self.resource.username(), Some(self.resource.password())) .send() @@ -192,9 +191,9 @@ impl DavCalendar for RemoteCalendar { Ok(Some(item)) } - async fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box> { + async fn delete_item(&mut self, item_id: &Url) -> Result<(), Box> { let del_response = reqwest::Client::new() - .delete(item_id.as_url().clone()) + .delete(item_id.clone()) .basic_auth(self.resource.username(), Some(self.resource.password())) .send() .await?; diff --git a/src/client.rs b/src/client.rs index 112fdc0..242a1a1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -216,7 +216,7 @@ impl Client { let this_calendar = RemoteCalendar::new(display_name, this_calendar_url, supported_components, this_calendar_color); log::info!("Found calendar {}", this_calendar.name()); - calendars.insert(this_calendar.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(); diff --git a/src/event.rs b/src/event.rs index c2149ca..2821242 100644 --- a/src/event.rs +++ b/src/event.rs @@ -2,15 +2,15 @@ use serde::{Deserialize, Serialize}; use chrono::{DateTime, Utc}; +use url::Url; -use crate::item::ItemId; use crate::item::SyncStatus; /// TODO: implement `Event` one day. /// This crate currently only supports tasks, not calendar events. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Event { - id: ItemId, + uid: String, name: String, sync_status: SyncStatus, } @@ -20,12 +20,12 @@ impl Event { unimplemented!(); } - pub fn id(&self) -> &ItemId { - &self.id + pub fn url(&self) -> &Url { + unimplemented!(); } pub fn uid(&self) -> &str { - unimplemented!() + &self.uid } pub fn name(&self) -> &str { diff --git a/src/ical/parser.rs b/src/ical/parser.rs index 8be5da5..3bdeadd 100644 --- a/src/ical/parser.rs +++ b/src/ical/parser.rs @@ -4,22 +4,22 @@ use std::error::Error; use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo}; use chrono::{DateTime, TimeZone, Utc}; +use url::Url; use crate::Item; use crate::item::SyncStatus; -use crate::item::ItemId; use crate::Task; use crate::task::CompletionStatus; use crate::Event; /// Parse an iCal file into the internal representation [`crate::Item`] -pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result> { +pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result> { let mut reader = ical::IcalParser::new(content.as_bytes()); let parsed_item = match reader.next() { - None => return Err(format!("Invalid iCal data to parse for item {}", item_id).into()), + None => return Err(format!("Invalid iCal data to parse for item {}", item_url).into()), Some(item) => match item { - Err(err) => return Err(format!("Unable to parse iCal data for item {}: {}", item_id, err).into()), + Err(err) => return Err(format!("Unable to parse iCal data for item {}: {}", item_url, err).into()), Ok(item) => item, } }; @@ -80,15 +80,15 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result< } let name = match 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 { 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 { 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 { false => { @@ -100,7 +100,7 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result< 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() { let version_tag = VersionTag::from(String::from("test-tag")); let sync_status = SyncStatus::Synced(version_tag); - let item_id: ItemId = "http://some.id/for/testing".parse().unwrap(); + let item_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(); 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.completed(), false); assert_eq!(task.completion_status(), &CompletionStatus::Uncompleted); @@ -262,9 +262,9 @@ END:VCALENDAR fn test_completed_ical_parsing() { let version_tag = VersionTag::from(String::from("test-tag")); let sync_status = SyncStatus::Synced(version_tag); - let item_id: ItemId = "http://some.id/for/testing".parse().unwrap(); + let item_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(); assert_eq!(task.completed(), true); @@ -275,9 +275,9 @@ END:VCALENDAR fn test_completed_without_date_ical_parsing() { let version_tag = VersionTag::from(String::from("test-tag")); let sync_status = SyncStatus::Synced(version_tag); - let item_id: ItemId = "http://some.id/for/testing".parse().unwrap(); + let item_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(); assert_eq!(task.completed(), true); @@ -288,9 +288,9 @@ END:VCALENDAR fn test_multiple_items_in_ical() { let version_tag = VersionTag::from(String::from("test-tag")); let sync_status = SyncStatus::Synced(version_tag); - let item_id: ItemId = "http://some.id/for/testing".parse().unwrap(); + let item_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()); } } diff --git a/src/item.rs b/src/item.rs index 9c45056..8d457d0 100644 --- a/src/item.rs +++ b/src/item.rs @@ -1,17 +1,10 @@ //! CalDAV items (todo, events, journals...) // TODO: move Event and Task to nest them in crate::items::calendar::Calendar? -use std::fmt::{Display, Formatter}; -use std::str::FromStr; - -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Serialize}; use url::Url; use chrono::{DateTime, Utc}; -use crate::resource::Resource; -use crate::calendar::CalendarId; - - #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Item { @@ -32,7 +25,7 @@ macro_rules! synthetise_common_getter { } impl Item { - synthetise_common_getter!(id, &ItemId); + synthetise_common_getter!(url, &Url); synthetise_common_getter!(uid, &str); synthetise_common_getter!(name, &str); synthetise_common_getter!(creation_date, Option<&DateTime>); @@ -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 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 { - 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(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(self.content.as_str()) - } -} -/// Used to support serde -impl<'de> Deserialize<'de> for ItemId { - fn deserialize(deserializer: D) -> Result - 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. diff --git a/src/provider/mod.rs b/src/provider/mod.rs index 6bb9a5e..2e9fb84 100644 --- a/src/provider/mod.rs +++ b/src/provider/mod.rs @@ -6,10 +6,11 @@ use std::error::Error; use std::collections::HashSet; use std::marker::PhantomData; use std::sync::{Arc, Mutex}; +use url::Url; use crate::traits::{BaseCalendar, CalDavSource, DavCalendar}; use crate::traits::CompleteCalendar; -use crate::item::{ItemId, SyncStatus}; +use crate::item::SyncStatus; use crate::calendar::CalendarId; pub mod sync_progress; @@ -400,7 +401,7 @@ where } - async fn item_name(cal: &T, id: &ItemId) -> String { + async fn item_name(cal: &T, id: &Url) -> String { cal.get_item_by_id(id).await.map(|item| item.name()).unwrap_or_default().to_string() } diff --git a/src/task.rs b/src/task.rs index 91e180a..4da9d93 100644 --- a/src/task.rs +++ b/src/task.rs @@ -4,10 +4,11 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use chrono::{DateTime, Utc}; use ical::property::Property; +use url::Url; -use crate::item::ItemId; 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. /// This enum provides an API that forbids such impossible combinations. @@ -33,10 +34,11 @@ impl CompletionStatus { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Task { /// The task URL - id: ItemId, + url: Url, /// 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, /// The sync status of this item @@ -65,8 +67,8 @@ pub struct Task { impl Task { /// Create a brand new Task that is not on a server yet. /// This will pick a new (random) task ID. - pub fn new(name: String, completed: bool, parent_calendar_id: &CalendarId) -> Self { - let new_item_id = ItemId::random(parent_calendar_id); + pub fn new(name: String, completed: bool, parent_calendar_url: &CalendarId) -> Self { + let new_url = random_url(parent_calendar_url); let new_sync_status = SyncStatus::NotSynced; let new_uid = Uuid::new_v4().to_hyphenated().to_string(); let new_creation_date = Some(Utc::now()); @@ -76,18 +78,18 @@ impl Task { } else { CompletionStatus::Uncompleted }; let ical_prod_id = crate::ical::default_prod_id(); let extra_parameters = Vec::new(); - Self::new_with_parameters(name, new_uid, new_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 - 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, sync_status: SyncStatus, creation_date: Option>, last_modified: DateTime, ical_prod_id: String, extra_parameters: Vec, ) -> Self { Self { - id, + url: new_url, uid, name, completion_status, @@ -99,7 +101,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 name(&self) -> &str { &self.name } pub fn completed(&self) -> bool { self.completion_status.is_completed() } @@ -112,7 +114,8 @@ impl Task { #[cfg(any(test, feature = "integration_tests"))] 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 // 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) diff --git a/src/traits.rs b/src/traits.rs index 5dc1bd4..66c1b43 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -6,10 +6,10 @@ use std::sync::{Arc, Mutex}; use async_trait::async_trait; use csscolorparser::Color; +use url::Url; use crate::item::SyncStatus; use crate::item::Item; -use crate::item::ItemId; use crate::item::VersionTag; use crate::calendar::CalendarId; use crate::calendar::SupportedComponents; @@ -40,8 +40,8 @@ pub trait BaseCalendar { /// Returns the calendar name fn name(&self) -> &str; - /// Returns the calendar unique ID - fn id(&self) -> &CalendarId; + /// Returns the calendar URL + fn url(&self) -> &Url; /// Returns the supported kinds of components for this calendar fn supported_components(&self) -> crate::calendar::SupportedComponents; @@ -79,16 +79,16 @@ pub trait DavCalendar : BaseCalendar { fn new(name: String, resource: Resource, supported_components: SupportedComponents, color: Option) -> Self; /// Get the IDs and the version tags of every item in this calendar - async fn get_item_version_tags(&self) -> Result, Box>; + async fn get_item_version_tags(&self) -> Result, Box>; /// Returns a particular item - async fn get_item_by_id(&self, id: &ItemId) -> Result, Box>; + async fn get_item_by_id(&self, id: &Url) -> Result, Box>; /// Delete an item - async fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box>; + async fn delete_item(&mut self, item_id: &Url) -> Result<(), Box>; /// Get the IDs of all current items in this calendar - async fn get_item_ids(&self) -> Result, Box> { + async fn get_item_ids(&self) -> Result, Box> { let items = self.get_item_version_tags().await?; Ok(items.iter() .map(|(id, _tag)| id.clone()) @@ -111,22 +111,22 @@ pub trait CompleteCalendar : BaseCalendar { fn new(name: String, id: CalendarId, supported_components: SupportedComponents, color: Option) -> Self; /// Get the IDs of all current items in this calendar - async fn get_item_ids(&self) -> Result, Box>; + async fn get_item_ids(&self) -> Result, Box>; /// Returns all items that this calendar contains - async fn get_items(&self) -> Result, Box>; + async fn get_items(&self) -> Result, Box>; /// Returns a particular item - async fn get_item_by_id<'a>(&'a self, id: &ItemId) -> Option<&'a Item>; + async fn get_item_by_id<'a>(&'a self, id: &Url) -> Option<&'a 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_id_mut<'a>(&'a mut self, id: &Url) -> Option<&'a mut Item>; /// 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 /// (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>; + async fn mark_for_deletion(&mut self, item_id: &Url) -> Result<(), Box>; /// Immediately remove an item. See [`CompleteCalendar::mark_for_deletion`] - async fn immediately_delete_item(&mut self, item_id: &ItemId) -> Result<(), Box>; + async fn immediately_delete_item(&mut self, item_id: &Url) -> Result<(), Box>; } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 9aba3a5..68f1c69 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -6,6 +6,7 @@ use std::hash::Hash; use std::io::{stdin, stdout, Read, Write}; use minidom::Element; +use url::Url; use crate::traits::CompleteCalendar; use crate::traits::DavCalendar; @@ -107,7 +108,7 @@ pub fn print_task(item: &Item) { SyncStatus::LocallyModified(_) => "~", SyncStatus::LocallyDeleted(_) => "x", }; - println!(" {}{} {}\t{}", completion, sync, task.name(), task.id()); + println!(" {}{} {}\t{}", completion, sync, task.name(), task.url()); }, _ => return, } @@ -148,3 +149,10 @@ pub fn pause() { stdout.flush().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 */) +} diff --git a/tests/scenarii.rs b/tests/scenarii.rs index 5888997..8987c8b 100644 --- a/tests/scenarii.rs +++ b/tests/scenarii.rs @@ -11,6 +11,7 @@ use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::error::Error; +use url::Url; use chrono::Utc; @@ -22,13 +23,13 @@ use kitchen_fridge::traits::CompleteCalendar; use kitchen_fridge::traits::DavCalendar; use kitchen_fridge::cache::Cache; use kitchen_fridge::Item; -use kitchen_fridge::item::ItemId; use kitchen_fridge::item::SyncStatus; use kitchen_fridge::Task; use kitchen_fridge::task::CompletionStatus; use kitchen_fridge::calendar::cached_calendar::CachedCalendar; use kitchen_fridge::provider::Provider; use kitchen_fridge::mock_behaviour::MockBehaviour; +use kitchen_fridge::utils::random_url; pub enum LocatedState { /// Item does not exist yet or does not exist anymore @@ -62,7 +63,7 @@ pub enum ChangeToApply { pub struct ItemScenario { - id: ItemId, + url: Url, initial_state: LocatedState, local_changes_to_apply: Vec, remote_changes_to_apply: Vec, @@ -93,7 +94,7 @@ pub fn scenarii_basic() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&first_cal), + url: random_url(&first_cal), initial_state: LocatedState::BothSynced( ItemState{ calendar: first_cal.clone(), name: String::from("Task A"), @@ -111,7 +112,7 @@ pub fn scenarii_basic() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&first_cal), + url: random_url(&first_cal), initial_state: LocatedState::BothSynced( ItemState{ calendar: first_cal.clone(), name: String::from("Task B"), @@ -125,7 +126,7 @@ pub fn scenarii_basic() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&first_cal), + url: random_url(&first_cal), initial_state: LocatedState::BothSynced( ItemState{ calendar: first_cal.clone(), name: String::from("Task C"), @@ -139,7 +140,7 @@ pub fn scenarii_basic() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&first_cal), + url: random_url(&first_cal), initial_state: LocatedState::BothSynced( ItemState{ calendar: first_cal.clone(), name: String::from("Task D"), @@ -157,7 +158,7 @@ pub fn scenarii_basic() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&first_cal), + url: random_url(&first_cal), initial_state: LocatedState::BothSynced( ItemState{ calendar: first_cal.clone(), name: String::from("Task E"), @@ -175,7 +176,7 @@ pub fn scenarii_basic() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&first_cal), + url: random_url(&first_cal), initial_state: LocatedState::BothSynced( ItemState{ calendar: first_cal.clone(), name: String::from("Task F"), @@ -194,7 +195,7 @@ pub fn scenarii_basic() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&second_cal), + url: random_url(&second_cal), initial_state: LocatedState::BothSynced( ItemState{ calendar: second_cal.clone(), name: String::from("Task G"), @@ -212,7 +213,7 @@ pub fn scenarii_basic() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&second_cal), + url: random_url(&second_cal), initial_state: LocatedState::BothSynced( ItemState{ calendar: second_cal.clone(), name: String::from("Task H"), @@ -230,7 +231,7 @@ pub fn scenarii_basic() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&second_cal), + url: random_url(&second_cal), initial_state: LocatedState::BothSynced( ItemState{ calendar: second_cal.clone(), name: String::from("Task I"), @@ -249,7 +250,7 @@ pub fn scenarii_basic() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&second_cal), + url: random_url(&second_cal), initial_state: LocatedState::BothSynced( ItemState{ calendar: second_cal.clone(), name: String::from("Task J"), @@ -263,7 +264,7 @@ pub fn scenarii_basic() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&second_cal), + url: random_url(&second_cal), initial_state: LocatedState::BothSynced( ItemState{ calendar: second_cal.clone(), name: String::from("Task K"), @@ -281,7 +282,7 @@ pub fn scenarii_basic() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&second_cal), + url: random_url(&second_cal), initial_state: LocatedState::BothSynced( ItemState{ calendar: second_cal.clone(), name: String::from("Task L"), @@ -295,7 +296,7 @@ pub fn scenarii_basic() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&second_cal), + url: random_url(&second_cal), initial_state: LocatedState::BothSynced( ItemState{ calendar: second_cal.clone(), name: String::from("Task M"), @@ -313,7 +314,7 @@ pub fn scenarii_basic() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&third_cal), + url: random_url(&third_cal), initial_state: LocatedState::BothSynced( ItemState{ calendar: third_cal.clone(), name: String::from("Task N"), @@ -331,7 +332,7 @@ pub fn scenarii_basic() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&third_cal), + url: random_url(&third_cal), initial_state: LocatedState::BothSynced( ItemState{ calendar: third_cal.clone(), name: String::from("Task O"), @@ -347,10 +348,10 @@ pub fn scenarii_basic() -> Vec { } ); - let id_p = ItemId::random(&third_cal); + let url_p = random_url(&third_cal); tasks.push( ItemScenario { - id: id_p.clone(), + url: url_p.clone(), initial_state: LocatedState::BothSynced( ItemState{ calendar: third_cal.clone(), name: String::from("Task P"), @@ -369,16 +370,16 @@ pub fn scenarii_basic() -> Vec { } ); - let id_q = ItemId::random(&third_cal); + let url_q = random_url(&third_cal); tasks.push( ItemScenario { - id: id_q.clone(), + url: url_q.clone(), initial_state: LocatedState::None, local_changes_to_apply: Vec::new(), remote_changes_to_apply: vec![ChangeToApply::Create(third_cal.clone(), Item::Task( Task::new_with_parameters( String::from("Task Q, created on the server"), - id_q.to_string(), id_q, + url_q.to_string(), url_q, CompletionStatus::Uncompleted, SyncStatus::random_synced(), Some(Utc::now()), Utc::now(), "prod_id".to_string(), Vec::new() ) ))], @@ -390,15 +391,15 @@ pub fn scenarii_basic() -> Vec { } ); - let id_r = ItemId::random(&third_cal); + let url_r = random_url(&third_cal); tasks.push( ItemScenario { - id: id_r.clone(), + url: url_r.clone(), initial_state: LocatedState::None, local_changes_to_apply: vec![ChangeToApply::Create(third_cal.clone(), Item::Task( Task::new_with_parameters( String::from("Task R, created locally"), - id_r.to_string(), id_r, + url_r.to_string(), url_r, CompletionStatus::Uncompleted, SyncStatus::NotSynced, Some(Utc::now()), Utc::now(), "prod_id".to_string(), Vec::new() ) ))], @@ -423,7 +424,7 @@ pub fn scenarii_first_sync_to_local() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&cal1), + url: random_url(&cal1), initial_state: LocatedState::Remote( ItemState{ calendar: cal1.clone(), name: String::from("Task A1"), @@ -441,7 +442,7 @@ pub fn scenarii_first_sync_to_local() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&cal2), + url: random_url(&cal2), initial_state: LocatedState::Remote( ItemState{ calendar: cal2.clone(), name: String::from("Task A2"), @@ -459,7 +460,7 @@ pub fn scenarii_first_sync_to_local() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&cal1), + url: random_url(&cal1), initial_state: LocatedState::Remote( ItemState{ calendar: cal1.clone(), name: String::from("Task B1"), @@ -487,7 +488,7 @@ pub fn scenarii_first_sync_to_server() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&cal3), + url: random_url(&cal3), initial_state: LocatedState::Local( ItemState{ calendar: cal3.clone(), name: String::from("Task A3"), @@ -505,7 +506,7 @@ pub fn scenarii_first_sync_to_server() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&cal4), + url: random_url(&cal4), initial_state: LocatedState::Local( ItemState{ calendar: cal4.clone(), name: String::from("Task A4"), @@ -523,7 +524,7 @@ pub fn scenarii_first_sync_to_server() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&cal3), + url: random_url(&cal3), initial_state: LocatedState::Local( ItemState{ calendar: cal3.clone(), name: String::from("Task B3"), @@ -551,7 +552,7 @@ pub fn scenarii_transient_task() -> Vec { tasks.push( ItemScenario { - id: ItemId::random(&cal), + url: random_url(&cal), initial_state: LocatedState::Local( ItemState{ calendar: cal.clone(), name: String::from("A task, so that the calendar actually exists"), @@ -567,16 +568,16 @@ pub fn scenarii_transient_task() -> Vec { } ); - let id_transient = ItemId::random(&cal); + let url_transient = random_url(&cal); tasks.push( ItemScenario { - id: id_transient.clone(), + url: url_transient.clone(), initial_state: LocatedState::None, local_changes_to_apply: vec![ ChangeToApply::Create(cal, Item::Task( Task::new_with_parameters( String::from("A transient task that will be deleted before the sync"), - id_transient.to_string(), id_transient, + url_transient.to_string(), url_transient, CompletionStatus::Uncompleted, SyncStatus::NotSynced, Some(Utc::now()), Utc::now(), "prod_id".to_string(), Vec::new() ) @@ -637,8 +638,8 @@ async fn populate_test_provider(scenarii: &[ItemScenario], mock_behaviour: Arc(source: &S, calendar_id: Option, item_id: &ItemId, change: &ChangeToApply, is_remote: bool) -> CalendarId +async fn apply_change(source: &S, calendar_id: Option, item_url: &Url, change: &ChangeToApply, is_remote: bool) -> CalendarId where S: CalDavSource, C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds { match calendar_id { 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 }, None => { @@ -723,14 +724,14 @@ where } } -async fn apply_changes_on_an_existing_item(source: &S, calendar_id: &CalendarId, item_id: &ItemId, change: &ChangeToApply, is_remote: bool) +async fn apply_changes_on_an_existing_item(source: &S, calendar_id: &CalendarId, item_url: &Url, change: &ChangeToApply, is_remote: bool) where S: CalDavSource, C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds { let cal = source.get_calendar(calendar_id).await.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_id_mut(item_url).await.unwrap().unwrap_task_mut(); match change { ChangeToApply::Rename(new_name) => { @@ -753,8 +754,8 @@ where }, ChangeToApply::Remove => { match is_remote { - false => cal.mark_for_deletion(item_id).await.unwrap(), - true => cal.delete_item(item_id).await.unwrap(), + false => cal.mark_for_deletion(item_url).await.unwrap(), + true => cal.delete_item(item_url).await.unwrap(), }; }, ChangeToApply::Create(_calendar_id, _item) => {