More simple clippy fixes

This commit is contained in:
James Musselman 2024-12-16 22:58:12 -06:00
parent 43aca1b492
commit 2b213ef635
Signed by: Musselman
GPG key ID: 1DAEFF35ECB5D6DB
2 changed files with 236 additions and 127 deletions

View file

@ -1,22 +1,22 @@
//! This module provides a local cache for CalDAV data //! This module provides a local cache for CalDAV data
use std::path::PathBuf;
use std::path::Path;
use std::error::Error;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, Mutex}; use std::error::Error;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::path::Path;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use async_trait::async_trait; use async_trait::async_trait;
use csscolorparser::Color; use csscolorparser::Color;
use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
use crate::traits::CalDavSource;
use crate::traits::BaseCalendar;
use crate::traits::CompleteCalendar;
use crate::calendar::cached_calendar::CachedCalendar; use crate::calendar::cached_calendar::CachedCalendar;
use crate::calendar::SupportedComponents; use crate::calendar::SupportedComponents;
use crate::traits::BaseCalendar;
use crate::traits::CalDavSource;
use crate::traits::CompleteCalendar;
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
use crate::mock_behaviour::MockBehaviour; use crate::mock_behaviour::MockBehaviour;
@ -52,10 +52,9 @@ impl Cache {
self.mock_behaviour = mock_behaviour; self.mock_behaviour = mock_behaviour;
} }
/// Get the path to the cache folder /// Get the path to the cache folder
pub fn cache_folder() -> PathBuf { pub fn cache_folder() -> PathBuf {
return PathBuf::from(String::from("~/.config/my-tasks/cache/")) PathBuf::from(String::from("~/.config/my-tasks/cache/"))
} }
/// Initialize a cache from the content of a valid backing folder if it exists. /// Initialize a cache from the content of a valid backing folder if it exists.
@ -66,7 +65,7 @@ impl Cache {
let mut data: CachedData = match std::fs::File::open(&main_file) { let mut data: CachedData = match std::fs::File::open(&main_file) {
Err(err) => { Err(err) => {
return Err(format!("Unable to open file {:?}: {}", main_file, err).into()); return Err(format!("Unable to open file {:?}: {}", main_file, err).into());
}, }
Ok(file) => serde_json::from_reader(file)?, Ok(file) => serde_json::from_reader(file)?,
}; };
@ -76,25 +75,30 @@ impl Cache {
Err(err) => { Err(err) => {
log::error!("Unable to read dir: {:?}", err); log::error!("Unable to read dir: {:?}", err);
continue; continue;
}, }
Ok(entry) => { Ok(entry) => {
let cal_path = entry.path(); let cal_path = entry.path();
log::debug!("Considering {:?}", cal_path); log::debug!("Considering {:?}", cal_path);
if cal_path.extension() == Some(OsStr::new("cal")) { if cal_path.extension() == Some(OsStr::new("cal")) {
match Self::load_calendar(&cal_path) { match Self::load_calendar(&cal_path) {
Err(err) => { Err(err) => {
log::error!("Unable to load calendar {:?} from cache: {:?}", cal_path, err); log::error!(
"Unable to load calendar {:?} from cache: {:?}",
cal_path,
err
);
continue; continue;
}, }
Ok(cal) => Ok(cal) => data
data.calendars.insert(cal.url().clone(), Arc::new(Mutex::new(cal))), .calendars
.insert(cal.url().clone(), Arc::new(Mutex::new(cal))),
}; };
} }
}, }
} }
} }
Ok(Self{ Ok(Self {
backing_folder: PathBuf::from(folder), backing_folder: PathBuf::from(folder),
data, data,
@ -104,13 +108,13 @@ impl Cache {
} }
fn load_calendar(path: &Path) -> Result<CachedCalendar, Box<dyn Error>> { fn load_calendar(path: &Path) -> Result<CachedCalendar, Box<dyn Error>> {
let file = std::fs::File::open(&path)?; let file = std::fs::File::open(path)?;
Ok(serde_json::from_reader(file)?) Ok(serde_json::from_reader(file)?)
} }
/// Initialize a cache with the default contents /// Initialize a cache with the default contents
pub fn new(folder_path: &Path) -> Self { pub fn new(folder_path: &Path) -> Self {
Self{ Self {
backing_folder: PathBuf::from(folder_path), backing_folder: PathBuf::from(folder_path),
data: CachedData::default(), data: CachedData::default(),
@ -143,16 +147,18 @@ impl Cache {
Ok(()) Ok(())
} }
/// Compares two Caches to check they have the same current content /// Compares two Caches to check they have the same current content
/// ///
/// This is not a complete equality test: some attributes (sync status...) may differ. This should mostly be used in tests /// This is not a complete equality test: some attributes (sync status...) may differ. This should mostly be used in tests
#[cfg(any(test, feature = "integration_tests"))] #[cfg(any(test, feature = "integration_tests"))]
pub async fn has_same_observable_content_as(&self, other: &Self) -> Result<bool, Box<dyn Error>> { pub async fn has_same_observable_content_as(
&self,
other: &Self,
) -> Result<bool, Box<dyn Error>> {
let calendars_l = self.get_calendars().await?; let calendars_l = self.get_calendars().await?;
let calendars_r = other.get_calendars().await?; let calendars_r = other.get_calendars().await?;
if crate::utils::keys_are_the_same(&calendars_l, &calendars_r) == false { if !crate::utils::keys_are_the_same(&calendars_l, &calendars_r) {
log::debug!("Different keys for calendars"); log::debug!("Different keys for calendars");
return Ok(false); return Ok(false);
} }
@ -166,11 +172,10 @@ impl Cache {
}; };
// TODO: check calendars have the same names/ID/whatever // TODO: check calendars have the same names/ID/whatever
if cal_l.has_same_observable_content_as(&cal_r).await? == false { if !(cal_l.has_same_observable_content_as(&cal_r).await?) {
log::debug!("Different calendars"); log::debug!("Different calendars");
return Ok(false) return Ok(false);
} }
} }
Ok(true) Ok(true)
} }
@ -179,32 +184,43 @@ impl Cache {
impl Drop for Cache { impl Drop for Cache {
fn drop(&mut self) { fn drop(&mut self) {
if let Err(err) = self.save_to_folder() { if let Err(err) = self.save_to_folder() {
log::error!("Unable to automatically save the cache when it's no longer required: {}", err); log::error!(
"Unable to automatically save the cache when it's no longer required: {}",
err
);
} }
} }
} }
impl Cache { 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<Url, 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(|(url, cal)| (url.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, url: &Url) -> Option<Arc<Mutex<CachedCalendar>>> { pub fn get_calendar_sync(&self, url: &Url) -> Option<Arc<Mutex<CachedCalendar>>> {
self.data.calendars.get(url).map(|arc| arc.clone()) self.data.calendars.get(url).cloned()
} }
} }
#[async_trait] #[async_trait]
impl CalDavSource<CachedCalendar> for Cache { impl CalDavSource<CachedCalendar> for Cache {
async fn get_calendars(&self) -> Result<HashMap<Url, 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()
} }
@ -212,21 +228,33 @@ impl CalDavSource<CachedCalendar> for Cache {
self.get_calendar_sync(url) self.get_calendar_sync(url)
} }
async fn create_calendar(&mut self, url: Url, 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 {}", url); 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, url.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")]
if let Some(behaviour) = &self.mock_behaviour { if let Some(behaviour) = &self.mock_behaviour {
arc.lock().unwrap().set_mock_behaviour(Some(Arc::clone(behaviour))); arc.lock()
.unwrap()
.set_mock_behaviour(Some(Arc::clone(behaviour)));
}; };
match self.data.calendars.insert(url, arc.clone()) { 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),
} }
} }
@ -236,38 +264,54 @@ impl CalDavSource<CachedCalendar> for Cache {
mod tests { mod tests {
use super::*; use super::*;
use url::Url;
use crate::calendar::SupportedComponents; use crate::calendar::SupportedComponents;
use crate::item::Item; use crate::item::Item;
use crate::task::Task; use crate::task::Task;
use url::Url;
async fn populate_cache(cache_path: &Path) -> Cache { async fn populate_cache(cache_path: &Path) -> Cache {
let mut cache = Cache::new(&cache_path); let mut cache = Cache::new(cache_path);
let _shopping_list = cache.create_calendar( let _shopping_list = cache
Url::parse("https://caldav.com/shopping").unwrap(), .create_calendar(
"My shopping list".to_string(), Url::parse("https://caldav.com/shopping").unwrap(),
SupportedComponents::TODO, "My shopping list".to_string(),
Some(csscolorparser::parse("lime").unwrap()), SupportedComponents::TODO,
).await.unwrap(); Some(csscolorparser::parse("lime").unwrap()),
)
.await
.unwrap();
let bucket_list = cache.create_calendar( let bucket_list = cache
Url::parse("https://caldav.com/bucket-list").unwrap(), .create_calendar(
"My bucket list".to_string(), Url::parse("https://caldav.com/bucket-list").unwrap(),
SupportedComponents::TODO, "My bucket list".to_string(),
Some(csscolorparser::parse("#ff8000").unwrap()), SupportedComponents::TODO,
).await.unwrap(); Some(csscolorparser::parse("#ff8000").unwrap()),
)
.await
.unwrap();
{ {
let mut bucket_list = bucket_list.lock().unwrap(); let mut bucket_list = bucket_list.lock().unwrap();
let cal_url = bucket_list.url().clone(); let cal_url = bucket_list.url().clone();
bucket_list.add_item(Item::Task(Task::new( bucket_list
String::from("Attend a concert of JS Bach"), false, &cal_url .add_item(Item::Task(Task::new(
))).await.unwrap(); String::from("Attend a concert of JS Bach"),
false,
&cal_url,
)))
.await
.unwrap();
bucket_list.add_item(Item::Task(Task::new( bucket_list
String::from("Climb the Lighthouse of Alexandria"), true, &cal_url .add_item(Item::Task(Task::new(
))).await.unwrap(); String::from("Climb the Lighthouse of Alexandria"),
true,
&cal_url,
)))
.await
.unwrap();
} }
cache cache
@ -285,7 +329,7 @@ mod tests {
assert_eq!(cache.backing_folder, retrieved_cache.backing_folder); assert_eq!(cache.backing_folder, retrieved_cache.backing_folder);
let test = cache.has_same_observable_content_as(&retrieved_cache).await; let test = cache.has_same_observable_content_as(&retrieved_cache).await;
println!("Equal? {:?}", test); println!("Equal? {:?}", test);
assert_eq!(test.unwrap(), true); assert!(test.unwrap(), true);
} }
#[tokio::test] #[tokio::test]
@ -295,12 +339,14 @@ mod 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 URL // 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
Url::parse("https://caldav.com/shopping").unwrap(), .create_calendar(
"My shopping list".to_string(), Url::parse("https://caldav.com/shopping").unwrap(),
SupportedComponents::TODO, "My shopping list".to_string(),
None, SupportedComponents::TODO,
).await; None,
)
.await;
assert!(second_addition_same_calendar.is_err()); assert!(second_addition_same_calendar.is_err());
} }
} }

View file

@ -1,25 +1,24 @@
//! This module provides a client to connect to a CalDAV server //! This module provides a client to connect to a CalDAV server
use std::error::Error;
use std::convert::TryFrom;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryFrom;
use std::error::Error;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use async_trait::async_trait; use async_trait::async_trait;
use reqwest::{Method, StatusCode};
use reqwest::header::CONTENT_TYPE;
use minidom::Element;
use url::Url;
use csscolorparser::Color; use csscolorparser::Color;
use minidom::Element;
use reqwest::header::CONTENT_TYPE;
use reqwest::{Method, StatusCode};
use url::Url;
use crate::resource::Resource;
use crate::utils::{find_elem, find_elems};
use crate::calendar::remote_calendar::RemoteCalendar; use crate::calendar::remote_calendar::RemoteCalendar;
use crate::calendar::SupportedComponents; use crate::calendar::SupportedComponents;
use crate::traits::CalDavSource; use crate::resource::Resource;
use crate::traits::BaseCalendar; use crate::traits::BaseCalendar;
use crate::traits::CalDavSource;
use crate::traits::DavCalendar; use crate::traits::DavCalendar;
use crate::utils::{find_elem, find_elems};
static DAVCLIENT_BODY: &str = r#" static DAVCLIENT_BODY: &str = r#"
<d:propfind xmlns:d="DAV:"> <d:propfind xmlns:d="DAV:">
@ -49,11 +48,13 @@ static CAL_BODY: &str = r#"
</d:propfind> </d:propfind>
"#; "#;
pub(crate) async fn sub_request(
resource: &Resource,
pub(crate) async fn sub_request(resource: &Resource, method: &str, body: String, depth: u32) -> Result<String, Box<dyn Error>> { method: &str,
let method = method.parse() body: String,
.expect("invalid method name"); depth: u32,
) -> Result<String, Box<dyn Error>> {
let method = method.parse().expect("invalid method name");
let res = reqwest::Client::new() let res = reqwest::Client::new()
.request(method, resource.url().clone()) .request(method, resource.url().clone())
@ -64,7 +65,7 @@ pub(crate) async fn sub_request(resource: &Resource, method: &str, body: String,
.send() .send()
.await?; .await?;
if res.status().is_success() == false { if !res.status().is_success() {
return Err(format!("Unexpected HTTP status code {:?}", res.status()).into()); return Err(format!("Unexpected HTTP status code {:?}", res.status()).into());
} }
@ -72,12 +73,16 @@ pub(crate) async fn sub_request(resource: &Resource, method: &str, body: String,
Ok(text) Ok(text)
} }
pub(crate) async fn sub_request_and_extract_elem(resource: &Resource, body: String, items: &[&str]) -> Result<String, Box<dyn Error>> { pub(crate) async fn sub_request_and_extract_elem(
resource: &Resource,
body: String,
items: &[&str],
) -> Result<String, Box<dyn Error>> {
let text = sub_request(resource, "PROPFIND", body, 0).await?; let text = sub_request(resource, "PROPFIND", body, 0).await?;
let mut current_element: &Element = &text.parse()?; let mut current_element: &Element = &text.parse()?;
for item in items { for item in items {
current_element = match find_elem(&current_element, item) { current_element = match find_elem(current_element, item) {
Some(elem) => elem, Some(elem) => elem,
None => return Err(format!("missing element {}", item).into()), None => return Err(format!("missing element {}", item).into()),
} }
@ -85,18 +90,21 @@ pub(crate) async fn sub_request_and_extract_elem(resource: &Resource, body: Stri
Ok(current_element.text()) Ok(current_element.text())
} }
pub(crate) async fn sub_request_and_extract_elems(resource: &Resource, method: &str, body: String, item: &str) -> Result<Vec<Element>, Box<dyn Error>> { pub(crate) async fn sub_request_and_extract_elems(
resource: &Resource,
method: &str,
body: String,
item: &str,
) -> Result<Vec<Element>, Box<dyn Error>> {
let text = sub_request(resource, method, body, 1).await?; let text = sub_request(resource, method, body, 1).await?;
let element: &Element = &text.parse()?; let element: &Element = &text.parse()?;
Ok(find_elems(&element, item) Ok(find_elems(element, item)
.iter() .iter()
.map(|elem| (*elem).clone()) .map(|elem| (*elem).clone())
.collect() .collect())
)
} }
/// A CalDAV data source that fetches its data from a CalDAV server /// A CalDAV data source that fetches its data from a CalDAV server
#[derive(Debug)] #[derive(Debug)]
pub struct Client { pub struct Client {
@ -107,7 +115,6 @@ pub struct Client {
cached_replies: Mutex<CachedReplies>, cached_replies: Mutex<CachedReplies>,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct CachedReplies { struct CachedReplies {
principal: Option<Resource>, principal: Option<Resource>,
@ -117,10 +124,14 @@ struct CachedReplies {
impl Client { impl Client {
/// Create a client. This does not start a connection /// Create a client. This does not start a connection
pub fn new<S: AsRef<str>, T: ToString, U: ToString>(url: S, username: T, password: U) -> Result<Self, Box<dyn Error>> { pub fn new<S: AsRef<str>, T: ToString, U: ToString>(
url: S,
username: T,
password: U,
) -> Result<Self, Box<dyn Error>> {
let url = Url::parse(url.as_ref())?; let url = Url::parse(url.as_ref())?;
Ok(Self{ Ok(Self {
resource: Resource::new(url, username.to_string(), password.to_string()), resource: Resource::new(url, username.to_string(), password.to_string()),
cached_replies: Mutex::new(CachedReplies::default()), cached_replies: Mutex::new(CachedReplies::default()),
}) })
@ -132,12 +143,17 @@ impl Client {
return Ok(p.clone()); return Ok(p.clone());
} }
let href = sub_request_and_extract_elem(&self.resource, DAVCLIENT_BODY.into(), &["current-user-principal", "href"]).await?; let href = sub_request_and_extract_elem(
&self.resource,
DAVCLIENT_BODY.into(),
&["current-user-principal", "href"],
)
.await?;
let principal_url = self.resource.combine(&href); let principal_url = self.resource.combine(&href);
self.cached_replies.lock().unwrap().principal = Some(principal_url.clone()); self.cached_replies.lock().unwrap().principal = Some(principal_url.clone());
log::debug!("Principal URL is {}", href); log::debug!("Principal URL is {}", href);
return Ok(principal_url); Ok(principal_url)
} }
/// Return the Homeset URL, or fetch it from server if not known yet /// Return the Homeset URL, or fetch it from server if not known yet
@ -147,7 +163,12 @@ impl Client {
} }
let principal_url = self.get_principal().await?; let principal_url = self.get_principal().await?;
let href = sub_request_and_extract_elem(&principal_url, HOMESET_BODY.into(), &["calendar-home-set", "href"]).await?; let href = sub_request_and_extract_elem(
&principal_url,
HOMESET_BODY.into(),
&["calendar-home-set", "href"],
)
.await?;
let chs_url = self.resource.combine(&href); let chs_url = self.resource.combine(&href);
self.cached_replies.lock().unwrap().calendar_home_set = Some(chs_url.clone()); self.cached_replies.lock().unwrap().calendar_home_set = Some(chs_url.clone());
log::debug!("Calendar home set URL is {:?}", href); log::debug!("Calendar home set URL is {:?}", href);
@ -158,10 +179,18 @@ impl Client {
async fn populate_calendars(&self) -> Result<(), Box<dyn Error>> { async fn populate_calendars(&self) -> Result<(), Box<dyn Error>> {
let cal_home_set = self.get_cal_home_set().await?; let cal_home_set = self.get_cal_home_set().await?;
let reps = sub_request_and_extract_elems(&cal_home_set, "PROPFIND", CAL_BODY.to_string(), "response").await?; let reps = sub_request_and_extract_elems(
&cal_home_set,
"PROPFIND",
CAL_BODY.to_string(),
"response",
)
.await?;
let mut calendars = HashMap::new(); let mut calendars = HashMap::new();
for rep in reps { for rep in reps {
let display_name = find_elem(&rep, "displayname").map(|e| e.text()).unwrap_or("<no name>".to_string()); let display_name = find_elem(&rep, "displayname")
.map(|e| e.text())
.unwrap_or("<no name>".to_string());
log::debug!("Considering calendar {}", display_name); log::debug!("Considering calendar {}", display_name);
// We filter out non-calendar items // We filter out non-calendar items
@ -176,7 +205,7 @@ impl Client {
break; break;
} }
} }
if found_calendar_type == false { if !found_calendar_type {
continue; continue;
} }
@ -193,48 +222,60 @@ impl Client {
None => { None => {
log::warn!("Calendar {} has no URL! Ignoring it.", display_name); log::warn!("Calendar {} has no URL! Ignoring it.", display_name);
continue; continue;
}, }
Some(h) => h.text(), Some(h) => h.text(),
}; };
let this_calendar_url = self.resource.combine(&calendar_href); let this_calendar_url = self.resource.combine(&calendar_href);
let supported_components = match crate::calendar::SupportedComponents::try_from(el_supported_comps.clone()) { let supported_components =
Err(err) => { match crate::calendar::SupportedComponents::try_from(el_supported_comps.clone()) {
log::warn!("Calendar {} has invalid supported components ({})! Ignoring it.", display_name, err); Err(err) => {
continue; log::warn!(
}, "Calendar {} has invalid supported components ({})! Ignoring it.",
Ok(sc) => sc, display_name,
}; err
);
continue;
}
Ok(sc) => sc,
};
let this_calendar_color = find_elem(&rep, "calendar-color") let this_calendar_color = find_elem(&rep, "calendar-color").and_then(|col| {
.and_then(|col| { col.texts()
col.texts().next() .next()
.and_then(|t| csscolorparser::parse(t).ok()) .and_then(|t| csscolorparser::parse(t).ok())
}); });
let this_calendar = RemoteCalendar::new(display_name, this_calendar_url, supported_components, this_calendar_color); let this_calendar = RemoteCalendar::new(
display_name,
this_calendar_url,
supported_components,
this_calendar_color,
);
log::info!("Found calendar {}", this_calendar.name()); log::info!("Found calendar {}", this_calendar.name());
calendars.insert(this_calendar.url().clone(), Arc::new(Mutex::new(this_calendar))); calendars.insert(
this_calendar.url().clone(),
Arc::new(Mutex::new(this_calendar)),
);
} }
let mut replies = self.cached_replies.lock().unwrap(); let mut replies = self.cached_replies.lock().unwrap();
replies.calendars = Some(calendars); replies.calendars = Some(calendars);
Ok(()) Ok(())
} }
} }
#[async_trait] #[async_trait]
impl CalDavSource<RemoteCalendar> for Client { impl CalDavSource<RemoteCalendar> for Client {
async fn get_calendars(&self) -> Result<HashMap<Url, 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 {
Some(cals) => { Some(cals) => return Ok(cals.clone()),
return Ok(cals.clone()) None => return Err("No calendars available".into()),
},
None => return Err("No calendars available".into())
}; };
} }
@ -244,14 +285,22 @@ impl CalDavSource<RemoteCalendar> for Client {
return None; return None;
} }
self.cached_replies.lock().unwrap() self.cached_replies
.lock()
.unwrap()
.calendars .calendars
.as_ref() .as_ref()
.and_then(|cals| cals.get(url)) .and_then(|cals| cals.get(url))
.map(|cal| cal.clone()) .cloned()
} }
async fn create_calendar(&mut self, url: Url, 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() {
@ -260,7 +309,7 @@ impl CalDavSource<RemoteCalendar> for Client {
if cals.contains_key(&url) { if cals.contains_key(&url) {
return Err("This calendar already exists".into()); return Err("This calendar already exists".into());
} }
}, }
} }
let creation_body = calendar_body(name, supported_components, color); let creation_body = calendar_body(name, supported_components, color);
@ -275,21 +324,35 @@ impl CalDavSource<RemoteCalendar> for Client {
let status = response.status(); let status = response.status();
if status != StatusCode::CREATED { if status != StatusCode::CREATED {
return Err(format!("Unexpected HTTP status code. Expected CREATED, got {}", status.as_u16()).into()); return Err(format!(
"Unexpected HTTP status code. Expected CREATED, got {}",
status.as_u16()
)
.into());
} }
self.get_calendar(&url).await.ok_or(format!("Unable to insert calendar {:?}", url).into()) self.get_calendar(&url)
.await
.ok_or(format!("Unable to insert calendar {:?}", url).into())
} }
} }
fn calendar_body(name: String, supported_components: SupportedComponents, color: Option<Color>) -> String { fn calendar_body(
name: String,
supported_components: SupportedComponents,
color: Option<Color>,
) -> String {
let color_property = match color { let color_property = match color {
None => "".to_string(), None => "".to_string(),
Some(color) => format!("<D:calendar-color xmlns:D=\"http://apple.com/ns/ical/\">{}FF</D:calendar-color>", color.to_hex_string().to_ascii_uppercase()), Some(color) => format!(
"<D:calendar-color xmlns:D=\"http://apple.com/ns/ical/\">{}FF</D:calendar-color>",
color.to_hex_string().to_ascii_uppercase()
),
}; };
// This is taken from https://tools.ietf.org/html/rfc4791#page-24 // This is taken from https://tools.ietf.org/html/rfc4791#page-24
format!(r#"<?xml version="1.0" encoding="utf-8" ?> format!(
r#"<?xml version="1.0" encoding="utf-8" ?>
<B:mkcalendar xmlns:B="urn:ietf:params:xml:ns:caldav"> <B:mkcalendar xmlns:B="urn:ietf:params:xml:ns:caldav">
<A:set xmlns:A="DAV:"> <A:set xmlns:A="DAV:">
<A:prop> <A:prop>