libdav 0.10.3

CalDAV and CardDAV client implementations.
Documentation
//! Request/response pattern for WebDAV operations.
//!
//! Idiomatic API to the method-based approach, using a request/response pattern.
//!
//! # Example
//!
//! ```
//! use libdav::dav::GetEtag;
//! # use libdav::CalDavClient;
//! # use tower_service::Service;
//! # 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 response = caldav.request(
//!     GetEtag::new("/calendar/event.ics")
//! ).await?;
//! println!("Etag: {}", response.etag);
//! # Ok(())
//! # }
//! ```

use http::{Method, StatusCode, response::Parts, status::InvalidStatusCode};

use crate::{
    dav::{PutResourceParseError, WebDavError},
    encoding::NormalisationError,
};

/// Error when parsing a WebDAV responses.
///
/// Contains only the variants that are feasible during response parsing,
/// excluding errors related to HTTP client operations or request building.
#[derive(thiserror::Error, Debug)]
pub enum ParseResponseError {
    /// The server returned an invalid status code.
    #[error("invalid status code in response: {0}")]
    InvalidStatusCode(#[from] InvalidStatusCode),

    /// Error parsing the XML response.
    #[error("parsing XML response: {0}")]
    Xml(#[from] roxmltree::Error),

    /// The server returned an unexpected status code.
    #[error("http request returned {0}")]
    BadStatusCode(StatusCode),

    /// The server returned a response that did not contain valid data.
    #[error("invalid response: {0}")]
    InvalidResponse(String),

    /// The response is not valid UTF-8.
    ///
    /// At this time, other encodings are not supported.
    #[error("decoding response as utf-8: {0}")]
    NotUtf8(#[from] std::str::Utf8Error),
}

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

impl From<NormalisationError> for ParseResponseError {
    fn from(error: NormalisationError) -> Self {
        // FIXME: hack
        ParseResponseError::InvalidResponse(error.to_string())
    }
}

impl<E> From<ParseResponseError> for WebDavError<E> {
    fn from(error: ParseResponseError) -> Self {
        match error {
            ParseResponseError::InvalidStatusCode(e) => WebDavError::InvalidStatusCode(e),
            ParseResponseError::Xml(e) => WebDavError::Xml(e),
            ParseResponseError::BadStatusCode(s) => WebDavError::BadStatusCode(s),
            ParseResponseError::InvalidResponse(msg) => WebDavError::InvalidResponse(msg.into()),
            ParseResponseError::NotUtf8(e) => WebDavError::NotUtf8(e),
        }
    }
}

impl<E> From<PutResourceParseError> for WebDavError<E> {
    fn from(error: PutResourceParseError) -> Self {
        match error {
            PutResourceParseError::BadStatusCode(s) => WebDavError::BadStatusCode(s),
            PutResourceParseError::NotUtf8(e) => WebDavError::NotUtf8(e),
            PutResourceParseError::PreconditionFailed(p) => WebDavError::PreconditionFailed(p),
        }
    }
}

/// A prepared HTTP request ready to be sent to a WebDAV server.
///
/// Contains all the information needed to execute an HTTP request,
/// including the method, path, body, and any additional headers.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PreparedRequest {
    /// The HTTP method (e.g., PROPFIND, MKCOL, GET, PUT).
    pub method: Method,
    /// The path to the resource (must be percent-encoded by the client).
    pub path: String,
    /// The request body (typically XML for WebDAV requests).
    pub body: String,
    /// Additional HTTP headers to include in the request.
    pub headers: Vec<(String, String)>,
}

/// Returns the HTTP header tuple for XML content with UTF-8 encoding.
///
/// Convenience helper to avoid copy-pasting the same block in a million places.
#[must_use]
pub fn xml_content_type_header() -> (String, String) {
    (
        "Content-Type".to_string(),
        "application/xml; charset=utf-8".to_string(),
    )
}

/// A WebDAV request that can be prepared and executed.
///
/// A type-safe WebDAV operation. Each implementation knows how to:
///
/// - Serialise itself into an HTTP request (`prepare_request`)
/// - Parse the HTTP response into a typed response (`parse_response`)
///
/// # Example
///
/// ```
/// use libdav::requests::{DavRequest, ParseResponseError, PreparedRequest};
/// use http::{Method, StatusCode};
///
/// struct MyRequest {
///     path: String,
/// }
///
/// struct MyResponse {
///     success: bool,
/// }
///
/// impl DavRequest for MyRequest {
///     type Response = MyResponse;
///     type ParseError = ParseResponseError;
///     type Error<E> = libdav::dav::WebDavError<E>;
///
///     fn prepare_request(&self) -> Result<PreparedRequest, http::Error> {
///         Ok(PreparedRequest {
///             method: Method::from_bytes(b"PROPFIND")?,
///             path: self.path.clone(),
///             body: String::from(r#"<propfind xmlns="DAV:"><prop><displayname/></prop></propfind>"#),
///             headers: vec![("Depth".to_string(), "0".to_string())],
///         })
///     }
///
///     fn parse_response(&self, parts: &http::response::Parts, body: &[u8])
///         -> Result<Self::Response, Self::ParseError>
///     {
///         Ok(MyResponse {
///             success: parts.status.is_success(),
///         })
///     }
/// }
/// ```
pub trait DavRequest {
    /// The response type expected when parsing the response of this request.
    type Response;

    /// The error type returned when parsing the response of this request.
    ///
    /// Most implementations use [`ParseResponseError`], which would be the default is Rust
    /// supported default values for associated types.
    type ParseError;

    /// The complete error type for this request operation.
    ///
    /// Generic over the HTTP client's error type `E`. Most implementations use
    /// [`crate::dav::WebDavError<E>`], which would be the default is Rust
    /// supported default values for associated types.
    ///
    /// This error type must be convertible from:
    /// - [`http::Error`] (URL building errors)
    /// - [`crate::dav::RequestError<E>`] (HTTP request errors)
    /// - [`Self::ParseError`] (response parsing errors)
    type Error<E>;

    /// Prepare the HTTP request.
    ///
    /// Serialises the request into an HTTP method, path, body, and headers.
    /// The path should not be percent-encoded; the client shall handle encoding.
    ///
    /// # Errors
    ///
    /// Returns an error if the request cannot be prepared (e.g., invalid parameters).
    fn prepare_request(&self) -> Result<PreparedRequest, http::Error>;

    /// Parse the HTTP response.
    ///
    /// Deserialises the HTTP response into a typed response object.
    ///
    /// # Errors
    ///
    /// Returns an error if the response cannot be parsed (e.g., malformed XML, unexpected status).
    fn parse_response(
        &self,
        parts: &Parts,
        body: &[u8],
    ) -> Result<Self::Response, Self::ParseError>;
}