use crate::{
Depth,
caldav::CalendarComponent,
dav::{Propfind, WebDavError},
names,
requests::{DavRequest, ParseResponseError, PreparedRequest},
xmlutils::{check_multistatus, validate_xml_response},
};
pub struct GetSupportedComponents<'a> {
propfind: Propfind<'a>,
}
impl<'a> GetSupportedComponents<'a> {
#[must_use]
pub fn new(href: &'a str) -> Self {
Self {
propfind: Propfind::new(href)
.with_properties(&[&names::SUPPORTED_CALENDAR_COMPONENT_SET])
.with_depth(Depth::Zero),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GetSupportedComponentsResponse {
pub components: Vec<CalendarComponent>,
}
impl DavRequest for GetSupportedComponents<'_> {
type Response = GetSupportedComponentsResponse;
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, Self::ParseError> {
let doc = validate_xml_response(parts, body)?;
let root = doc.root_element();
check_multistatus(root)?;
let prop_node = root
.descendants()
.find(|node| node.tag_name() == names::SUPPORTED_CALENDAR_COMPONENT_SET);
let mut components = Vec::new();
if let Some(set_node) = prop_node {
for comp in set_node
.descendants()
.filter(|n| n.tag_name() == names::COMP)
{
if let Some(name) = comp.attribute("name") {
let component = match name.to_uppercase().as_str() {
"VEVENT" => CalendarComponent::VEvent,
"VTODO" => CalendarComponent::VTodo,
"VJOURNAL" => CalendarComponent::VJournal,
"VFREEBUSY" => CalendarComponent::VFreeBusy,
"VAVAILABILITY" => CalendarComponent::VAvailability,
other => CalendarComponent::Other(other.to_string()),
};
components.push(component);
}
}
}
Ok(GetSupportedComponentsResponse { components })
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::{Method, StatusCode};
#[test]
fn test_prepare_request() {
let req = GetSupportedComponents::new("/calendars/personal/");
let prepared = req.prepare_request().unwrap();
assert_eq!(prepared.method, Method::from_bytes(b"PROPFIND").unwrap());
assert_eq!(prepared.path, "/calendars/personal/");
assert!(prepared.body.contains("supported-calendar-component-set"));
assert!(prepared.body.contains(r#"xmlns:D="DAV:""#));
}
#[test]
fn test_parse_response_with_components() {
let req = GetSupportedComponents::new("/calendars/personal/");
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<response>
<href>/calendars/personal/</href>
<propstat>
<prop>
<C:supported-calendar-component-set>
<C:comp name="VEVENT"/>
<C:comp name="VTODO"/>
</C:supported-calendar-component-set>
</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, body).unwrap();
assert_eq!(result.components.len(), 2);
assert!(result.components.contains(&CalendarComponent::VEvent));
assert!(result.components.contains(&CalendarComponent::VTodo));
}
#[test]
fn test_parse_response_empty() {
let req = GetSupportedComponents::new("/calendars/personal/");
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<response>
<href>/calendars/personal/</href>
<propstat>
<prop>
<C:supported-calendar-component-set/>
</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, body).unwrap();
assert!(result.components.is_empty());
}
#[test]
fn test_parse_response_with_freebusy_and_availability() {
let req = GetSupportedComponents::new("/calendars/personal/");
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<response>
<href>/calendars/personal/</href>
<propstat>
<prop>
<C:supported-calendar-component-set>
<C:comp name="VEVENT"/>
<C:comp name="VFREEBUSY"/>
<C:comp name="VAVAILABILITY"/>
</C:supported-calendar-component-set>
</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, body).unwrap();
assert_eq!(result.components.len(), 3);
assert!(result.components.contains(&CalendarComponent::VEvent));
assert!(result.components.contains(&CalendarComponent::VFreeBusy));
assert!(
result
.components
.contains(&CalendarComponent::VAvailability)
);
}
#[test]
fn test_parse_response_with_non_standard_component() {
let req = GetSupportedComponents::new("/calendars/personal/");
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<response>
<href>/calendars/personal/</href>
<propstat>
<prop>
<C:supported-calendar-component-set>
<C:comp name="VEVENT"/>
<C:comp name="VPOLL"/>
</C:supported-calendar-component-set>
</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, body).unwrap();
assert_eq!(result.components.len(), 2);
assert!(result.components.contains(&CalendarComponent::VEvent));
assert!(
result
.components
.contains(&CalendarComponent::Other("VPOLL".to_string()))
);
}
}