use crate::{
Depth, PropertyName,
dav::{FoundCollection, Propfind, WebDavError},
names,
requests::{DavRequest, ParseResponseError, PreparedRequest},
xmlutils::{get_normalised_href, validate_xml_response},
};
pub struct FindCollections<'a> {
propfind: Propfind<'a>,
collection_type: Option<&'a PropertyName<'a, 'a>>,
}
impl<'a> FindCollections<'a> {
#[must_use]
pub fn new(collection_home_set: &'a hyper::Uri) -> Self {
Self {
propfind: Propfind::new(collection_home_set.path())
.with_properties(&[
&names::RESOURCETYPE,
&names::GETETAG,
&names::SUPPORTED_REPORT_SET,
])
.with_depth(Depth::One),
collection_type: None,
}
}
#[must_use]
pub fn with_collection_type(mut self, collection_type: &'a PropertyName<'a, 'a>) -> Self {
self.collection_type = Some(collection_type);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FindCollectionsResponse {
pub collections: Vec<FoundCollection>,
}
impl DavRequest for FindCollections<'_> {
type Response = FindCollectionsResponse;
type ParseError = ParseResponseError;
type Error<E> = WebDavError<E>;
fn prepare_request(&self) -> Result<PreparedRequest, http::Error> {
self.propfind.prepare_request()
}
fn parse_response(
&self,
parts: &http::response::Parts,
body: &[u8],
) -> Result<Self::Response, ParseResponseError> {
let doc = validate_xml_response(parts, body)?;
let root = doc.root_element();
let responses = root
.descendants()
.filter(|node| node.tag_name() == names::RESPONSE);
let mut items = Vec::new();
for response in responses {
if let Some(collection_type) = self.collection_type {
if !response
.descendants()
.find(|node| node.tag_name() == names::RESOURCETYPE)
.is_some_and(|node| {
node.descendants()
.any(|node| node.tag_name() == *collection_type)
})
{
continue;
}
} else {
let resourcetype_node = response
.descendants()
.find(|node| node.tag_name() == names::RESOURCETYPE);
if let Some(rt) = resourcetype_node {
let has_collection = rt
.descendants()
.any(|node| node.tag_name() == names::COLLECTION);
let child_count = rt.children().filter(roxmltree::Node::is_element).count();
if !has_collection || child_count <= 1 {
continue;
}
} else {
continue;
}
}
let href = get_normalised_href(&response)?.to_string();
let etag = response
.descendants()
.find(|node| node.tag_name() == names::GETETAG)
.and_then(|node| node.text().map(str::to_string));
let supports_sync = response
.descendants()
.find(|node| node.tag_name() == names::SUPPORTED_REPORT_SET)
.is_some_and(|node| {
node.descendants()
.any(|node| node.tag_name() == names::SYNC_COLLECTION)
});
items.push(FoundCollection {
href,
etag,
supports_sync,
});
}
Ok(FindCollectionsResponse { collections: items })
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::StatusCode;
#[test]
fn test_parse_response_calendars() {
let home_set = hyper::Uri::from_static("/calendars/user/");
let req = FindCollections::new(&home_set).with_collection_type(&names::CALENDAR);
let response_xml = r#"<?xml version="1.0"?>
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<response>
<href>/calendars/user/</href>
<propstat>
<prop>
<resourcetype>
<collection/>
</resourcetype>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/calendars/user/work/</href>
<propstat>
<prop>
<resourcetype>
<collection/>
<C:calendar/>
</resourcetype>
<getetag>"abc123"</getetag>
<supported-report-set>
<supported-report>
<report>
<sync-collection/>
</report>
</supported-report>
</supported-report-set>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/calendars/user/personal/</href>
<propstat>
<prop>
<resourcetype>
<collection/>
<C:calendar/>
</resourcetype>
<getetag>"def456"</getetag>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, response_xml.as_bytes()).unwrap();
assert_eq!(result.collections.len(), 2);
assert_eq!(result.collections[0].href, "/calendars/user/work/");
assert_eq!(result.collections[0].etag, Some("\"abc123\"".to_string()));
assert!(result.collections[0].supports_sync);
assert_eq!(result.collections[1].href, "/calendars/user/personal/");
assert_eq!(result.collections[1].etag, Some("\"def456\"".to_string()));
assert!(!result.collections[1].supports_sync);
}
#[test]
fn test_parse_response_addressbooks() {
let home_set = hyper::Uri::from_static("/addressbooks/user/");
let req = FindCollections::new(&home_set).with_collection_type(&names::ADDRESSBOOK);
let response_xml = r#"<?xml version="1.0"?>
<multistatus xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<response>
<href>/addressbooks/user/contacts/</href>
<propstat>
<prop>
<resourcetype>
<collection/>
<CARD:addressbook/>
</resourcetype>
<getetag>"xyz789"</getetag>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, response_xml.as_bytes()).unwrap();
assert_eq!(result.collections.len(), 1);
assert_eq!(result.collections[0].href, "/addressbooks/user/contacts/");
assert_eq!(result.collections[0].etag, Some("\"xyz789\"".to_string()));
assert!(!result.collections[0].supports_sync);
}
#[test]
fn test_parse_response_empty() {
let home_set = hyper::Uri::from_static("/calendars/user/");
let req = FindCollections::new(&home_set).with_collection_type(&names::CALENDAR);
let response_xml = r#"<?xml version="1.0"?>
<multistatus xmlns="DAV:">
<response>
<href>/calendars/user/</href>
<propstat>
<prop>
<resourcetype>
<collection/>
</resourcetype>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, response_xml.as_bytes()).unwrap();
assert_eq!(result.collections.len(), 0);
}
#[test]
fn test_parse_response_bad_status() {
let home_set = hyper::Uri::from_static("/calendars/user/");
let req = FindCollections::new(&home_set).with_collection_type(&names::CALENDAR);
let response_xml = b"";
let response = http::Response::builder()
.status(StatusCode::NOT_FOUND)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, response_xml);
assert!(result.is_err());
}
#[test]
fn test_parse_response_all_collections() {
let home_set = hyper::Uri::from_static("/dav/user/");
let req = FindCollections::new(&home_set); let response_xml = r#"<?xml version="1.0"?>
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<response>
<href>/dav/user/</href>
<propstat>
<prop>
<resourcetype>
<collection/>
</resourcetype>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/dav/user/calendar/</href>
<propstat>
<prop>
<resourcetype>
<collection/>
<C:calendar/>
</resourcetype>
<getetag>"cal123"</getetag>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/dav/user/contacts/</href>
<propstat>
<prop>
<resourcetype>
<collection/>
<CARD:addressbook/>
</resourcetype>
<getetag>"addr456"</getetag>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, response_xml.as_bytes()).unwrap();
assert_eq!(result.collections.len(), 2);
assert_eq!(result.collections[0].href, "/dav/user/calendar/");
assert_eq!(result.collections[0].etag, Some("\"cal123\"".to_string()));
assert_eq!(result.collections[1].href, "/dav/user/contacts/");
assert_eq!(result.collections[1].etag, Some("\"addr456\"".to_string()));
}
}