libdav 0.10.3

CalDAV and CardDAV client implementations.
Documentation
//! Request to fetch the `supported-calendar-component-set` property from a calendar collection.
//
//! Returns the list of supported iCalendar component types (VEVENT, VTODO, VJOURNAL, VFREEBUSY,
//! VAVAILABILITY, and any non-standard components).
//!
//! This request issues a PROPFIND for the `supported-calendar-component-set` property and
//! parses the `<C:comp name="..."/>` children into the library's `CalendarComponent` enum.

use crate::{
    Depth,
    caldav::CalendarComponent,
    dav::{Propfind, WebDavError},
    names,
    requests::{DavRequest, ParseResponseError, PreparedRequest},
    xmlutils::{check_multistatus, validate_xml_response},
};

/// Request to fetch the supported calendar components of a collection.
pub struct GetSupportedComponents<'a> {
    propfind: Propfind<'a>,
}

impl<'a> GetSupportedComponents<'a> {
    /// Create a new request for the given collection href.
    #[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),
        }
    }
}

/// Response containing supported calendar components.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GetSupportedComponentsResponse {
    /// The list of supported component types.
    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();

        // Ensure there are no error propstat entries.
        check_multistatus(root)?;

        // Find the supported-calendar-component-set element.
        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 each <comp name="..."/> child, read the name and map it to CalendarComponent.
            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()))
        );
    }
}