libdav 0.10.3

CalDAV and CardDAV client implementations.
Documentation
//! Find calendars under a calendar home set.

use crate::{
    dav::{FindCollections, FoundCollection, WebDavError},
    names,
    requests::{DavRequest, ParseResponseError, PreparedRequest},
};

/// Request to find calendars under a calendar home set.
///
/// Sends a PROPFIND request with depth 1 to discover calendars and their properties.
///
/// # Example
///
/// ```
/// # use libdav::caldav::FindCalendars;
/// # use libdav::CalDavClient;
/// # use tower_service::Service;
/// # use http::Uri;
/// # async fn example<C>(caldav: &CalDavClient<C>) -> Result<(), Box<dyn std::error::Error>>
/// # where
/// #     C: Service<http::Request<String>, Response = http::Response<hyper::body::Incoming>> + Send + Sync,
/// #     C::Error: std::error::Error + Send + Sync,
/// # {
/// let calendar_home_set = Uri::from_static("/calendars/user/");
/// let response = caldav.request(
///     FindCalendars::new(&calendar_home_set)
/// ).await?;
///
/// for calendar in response.calendars {
///     println!("Found calendar: {}", calendar.href);
///     println!("  Supports sync: {}", calendar.supports_sync);
/// }
/// # Ok(())
/// # }
/// ```
pub struct FindCalendars<'a> {
    inner: FindCollections<'a>,
}

impl<'a> FindCalendars<'a> {
    /// Create a new request to find calendars.
    ///
    /// `calendar_home_set` is URI of the calendar home set.
    #[must_use]
    pub fn new(calendar_home_set: &'a hyper::Uri) -> Self {
        let inner = FindCollections::new(calendar_home_set).with_collection_type(&names::CALENDAR);
        Self { inner }
    }
}

/// Response from a [`FindCalendars`] request.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FindCalendarsResponse {
    /// Calendars found.
    pub calendars: Vec<FoundCollection>,
}

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

    fn prepare_request(&self) -> Result<PreparedRequest, http::Error> {
        self.inner.prepare_request()
    }

    fn parse_response(
        &self,
        parts: &http::response::Parts,
        body: &[u8],
    ) -> Result<Self::Response, ParseResponseError> {
        let response = self.inner.parse_response(parts, body)?;
        Ok(FindCalendarsResponse {
            calendars: response.collections,
        })
    }
}

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

    #[test]
    fn test_prepare_request() {
        let home_set = hyper::Uri::from_static("/calendars/user/");
        let req = FindCalendars::new(&home_set);
        let prepared = req.prepare_request().unwrap();

        assert_eq!(prepared.path, "/calendars/user/");
        assert_eq!(
            prepared.headers,
            vec![
                ("Depth".to_string(), "1".to_string()),
                (
                    "Content-Type".to_string(),
                    "application/xml; charset=utf-8".to_string()
                )
            ]
        );
    }

    #[test]
    fn test_parse_response() {
        let home_set = hyper::Uri::from_static("/calendars/user/");
        let req = FindCalendars::new(&home_set);
        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>
                            <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>
            </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.calendars.len(), 1);
        assert_eq!(result.calendars[0].href, "/calendars/user/work/");
        assert_eq!(result.calendars[0].etag, Some("\"abc123\"".to_string()));
        assert!(result.calendars[0].supports_sync);
    }
}