libdav 0.10.3

CalDAV and CardDAV client implementations.
Documentation
//! Check if server advertises support for a specific DAV extension.

use http::{Method, StatusCode};

use crate::{
    CheckSupportError,
    requests::{DavRequest, PreparedRequest},
};

/// Error when parsing a response from a `CheckSupport` request.
///
/// Contains only the variants that can occur during response parsing
/// for capability checking.
#[derive(thiserror::Error, Debug)]
pub enum CheckSupportParseError {
    /// The `DAV` header was missing from the response received.
    #[error("DAV header missing from the response")]
    MissingHeader,

    /// The server does not advertise the queried capability.
    #[error("requested support not advertised by the server")]
    NotAdvertised,

    /// Failed to parse the `DAV` header as a UTF-8 string.
    #[error("DAV header is not a valid string: {0}")]
    HeaderNotAscii(#[from] http::header::ToStrError),

    /// Server returned a non-success status code.
    #[error("http request returned {0}")]
    BadStatusCode(StatusCode),
}

impl From<StatusCode> for CheckSupportParseError {
    fn from(status: StatusCode) -> Self {
        CheckSupportParseError::BadStatusCode(status)
    }
}

/// Request to check if a server advertises support for a specific protocol extension.
///
/// Sends an OPTIONS request to the specified URI and checks if the `DAV:` header
/// contains the expected value.
///
/// # Example
///
/// ```
/// # use libdav::dav::CheckSupport;
/// # use libdav::dav::WebDavClient;
/// # use tower_service::Service;
/// # use http::Uri;
/// # async fn example<C>(webdav: &WebDavClient<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 uri = Uri::from_static("/");
/// let response = webdav.request(
///     CheckSupport::new(&uri, "calendar-access")
/// ).await?;
/// # Ok(())
/// # }
/// ```
pub struct CheckSupport<'a> {
    uri: &'a hyper::Uri,
    expectation: &'a str,
}

impl<'a> CheckSupport<'a> {
    /// Create a new `CheckSupport` request.
    ///
    /// `uri` is the URI to check.
    /// `expectation` is the value that must be present in the DAV header.
    #[must_use]
    pub fn new(uri: &'a hyper::Uri, expectation: &'a str) -> Self {
        Self { uri, expectation }
    }

    /// Create a request to check if a server advertises CalDAV support.
    ///
    /// Checks if the `DAV:` header contains "calendar-access".
    ///
    /// See: <https://www.rfc-editor.org/rfc/rfc4791#section-5.1>
    #[must_use]
    pub fn caldav(uri: &'a hyper::Uri) -> Self {
        Self::new(uri, "calendar-access")
    }

    /// Create a request to check if a server advertises CardDAV support.
    ///
    /// Checks if the `DAV:` header contains "addressbook".
    ///
    /// See: <https://www.rfc-editor.org/rfc/rfc6352#section-6.1>
    #[must_use]
    pub fn carddav(uri: &'a hyper::Uri) -> Self {
        Self::new(uri, "addressbook")
    }
}

/// Response from a `CheckSupport` request.
///
/// Contains no data; since the request either succeeds (support is advertised)
/// or fails (support is not advertised or an error occurred).
pub type CheckSupportResponse = ();

impl DavRequest for CheckSupport<'_> {
    type Response = CheckSupportResponse;
    type ParseError = CheckSupportParseError;
    type Error<E> = CheckSupportError<E>;

    fn prepare_request(&self) -> Result<PreparedRequest, http::Error> {
        Ok(PreparedRequest {
            method: Method::OPTIONS,
            path: self.uri.path().to_string(),
            body: String::new(),
            headers: vec![],
        })
    }

    fn parse_response(
        &self,
        parts: &http::response::Parts,
        _body: &[u8],
    ) -> Result<Self::Response, Self::ParseError> {
        if !parts.status.is_success() {
            return Err(CheckSupportParseError::BadStatusCode(parts.status));
        }

        let header = parts
            .headers
            .get("DAV")
            .ok_or(CheckSupportParseError::MissingHeader)?
            .to_str()?;

        log::debug!("DAV header: '{header}'");
        if header
            .split(',')
            .any(|part| part.trim() == self.expectation)
        {
            Ok(())
        } else {
            Err(CheckSupportParseError::NotAdvertised)
        }
    }
}

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

    #[test]
    fn test_prepare_request() {
        let uri = hyper::Uri::from_static("/dav/");
        let req = CheckSupport::new(&uri, "calendar-access");
        let prepared = req.prepare_request().unwrap();

        assert_eq!(prepared.method, Method::OPTIONS);
        assert_eq!(prepared.path, "/dav/");
        assert_eq!(prepared.body, "");
        assert_eq!(prepared.headers, vec![]);
    }

    #[test]
    fn test_parse_response_success() {
        let uri = hyper::Uri::from_static("/");
        let req = CheckSupport::new(&uri, "calendar-access");

        let response = http::Response::builder()
            .status(StatusCode::OK)
            .header("DAV", "1, 2, calendar-access")
            .body(())
            .unwrap();
        let (parts, ()) = response.into_parts();
        let result = req.parse_response(&parts, b"");

        assert!(result.is_ok());
    }

    #[test]
    fn test_parse_response_missing_header() {
        let uri = hyper::Uri::from_static("/");
        let req = CheckSupport::new(&uri, "calendar-access");

        let response = http::Response::builder()
            .status(StatusCode::OK)
            .body(())
            .unwrap();
        let (parts, ()) = response.into_parts();
        let result = req.parse_response(&parts, b"");

        assert!(result.is_err());
        assert!(matches!(result, Err(CheckSupportParseError::MissingHeader)));
    }

    #[test]
    fn test_parse_response_not_advertised() {
        let uri = hyper::Uri::from_static("/");
        let req = CheckSupport::new(&uri, "calendar-access");

        let response = http::Response::builder()
            .status(StatusCode::OK)
            .header("DAV", "1, 2, 3")
            .body(())
            .unwrap();
        let (parts, ()) = response.into_parts();
        let result = req.parse_response(&parts, b"");

        assert!(result.is_err());
        assert!(matches!(result, Err(CheckSupportParseError::NotAdvertised)));
    }

    #[test]
    fn test_parse_response_bad_status() {
        let uri = hyper::Uri::from_static("/");
        let req = CheckSupport::new(&uri, "calendar-access");

        let response = http::Response::builder()
            .status(StatusCode::NOT_FOUND)
            .body(())
            .unwrap();
        let (parts, ()) = response.into_parts();
        let result = req.parse_response(&parts, b"");

        assert!(result.is_err());
    }
}