libdav 0.10.3

CalDAV and CardDAV client implementations.
Documentation
//! Request to fetch the `supported-calendar-data` property from a calendar collection.

// FIXME: Having separate request types for each property like this doesn't scale at all.
// FIXME: This forces a separate request for each property too, which is senseless.
// FIXME: We _could_ return a light wrapper over XML, but that leaves parsing to the caller.

use http::Method;

use crate::{
    Depth,
    dav::WebDavError,
    names,
    requests::{DavRequest, ParseResponseError, PreparedRequest, xml_content_type_header},
    xmlutils::{XmlNode, validate_xml_response},
};

/// Request to fetch supported calendar data types of a collection.
pub struct GetSupportedCalendarData<'a> {
    href: &'a str,
}

impl<'a> GetSupportedCalendarData<'a> {
    /// Create a new request for the given collection href.
    #[must_use]
    pub fn new(href: &'a str) -> Self {
        Self { href }
    }
}

/// Calendar data type with content-type and version.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CalendarDataType {
    /// The MIME type (e.g.: "text/calendar").
    pub content_type: String,
    /// The version (e.g.: "2.0").
    pub version: Option<String>,
}

/// Response containing supported calendar data types.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GetSupportedCalendarDataResponse {
    /// Supported calendar data types.
    pub data_types: Vec<CalendarDataType>,
}

impl DavRequest for GetSupportedCalendarData<'_> {
    type Response = GetSupportedCalendarDataResponse;
    type ParseError = ParseResponseError;
    type Error<E> = WebDavError<E>;

    fn prepare_request(&self) -> Result<PreparedRequest, http::Error> {
        let mut prop = XmlNode::new(&names::PROP);
        prop.children = vec![XmlNode::new(&names::SUPPORTED_CALENDAR_DATA)];

        let mut propfind = XmlNode::new(&names::PROPFIND);
        propfind.children = vec![prop];

        Ok(PreparedRequest {
            method: Method::from_bytes(b"PROPFIND")?,
            path: self.href.to_string(),
            body: propfind.render_node(),
            headers: vec![
                ("Depth".to_string(), Depth::Zero.to_string()),
                xml_content_type_header(),
            ],
        })
    }

    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 mut data_types = Vec::new();

        if let Some(prop) = root
            .descendants()
            .find(|node| node.tag_name() == names::SUPPORTED_CALENDAR_DATA)
        {
            for elem in prop
                .descendants()
                .filter(|node| node.tag_name().name() == "calendar-data")
            {
                if let Some(content_type) = elem.attribute("content-type") {
                    let version = elem.attribute("version").map(str::to_string);
                    data_types.push(CalendarDataType {
                        content_type: content_type.to_string(),
                        version,
                    });
                }
            }
        }

        Ok(GetSupportedCalendarDataResponse { data_types })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use http::StatusCode;

    #[test]
    fn test_parse_response() {
        let req = GetSupportedCalendarData::new("/calendars/user/work/");
        let response_xml = r#"<?xml version="1.0"?>
            <multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
                <response>
                    <href>/calendars/user/work/</href>
                    <propstat>
                        <prop>
                            <C:supported-calendar-data>
                                <C:calendar-data content-type="text/calendar" version="2.0"/>
                            </C:supported-calendar-data>
                        </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.data_types.len(), 1);
        assert_eq!(result.data_types[0].content_type, "text/calendar");
        assert_eq!(result.data_types[0].version, Some("2.0".to_string()));
    }

    #[test]
    fn test_parse_response_multiple_types() {
        let req = GetSupportedCalendarData::new("/calendars/user/work/");
        let response_xml = r#"<?xml version="1.0"?>
            <multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
                <response>
                    <href>/calendars/user/work/</href>
                    <propstat>
                        <prop>
                            <C:supported-calendar-data>
                                <C:calendar-data content-type="text/calendar" version="2.0"/>
                                <C:calendar-data content-type="text/calendar" version="1.0"/>
                            </C:supported-calendar-data>
                        </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.data_types.len(), 2);
        assert_eq!(result.data_types[0].version, Some("2.0".to_string()));
        assert_eq!(result.data_types[1].version, Some("1.0".to_string()));
    }
}