Skip to main content

libdav/
requests.rs

1//! Request/response pattern for WebDAV operations.
2//!
3//! Idiomatic API to the method-based approach, using a request/response pattern.
4//!
5//! # Example
6//!
7//! ```
8//! use libdav::dav::GetEtag;
9//! # use libdav::CalDavClient;
10//! # use tower_service::Service;
11//! # async fn example<C>(caldav: &CalDavClient<C>) -> Result<(), Box<dyn std::error::Error>>
12//! # where
13//! #     C: Service<http::Request<String>, Response = http::Response<hyper::body::Incoming>> + Send + Sync,
14//! #     C::Error: std::error::Error + Send + Sync,
15//! # {
16//! let response = caldav.request(
17//!     GetEtag::new("/calendar/event.ics")
18//! ).await?;
19//! println!("Etag: {}", response.etag);
20//! # Ok(())
21//! # }
22//! ```
23
24use crate::dav::WebDavError;
25use crate::encoding::NormalisationError;
26use http::{Method, StatusCode, response::Parts, status::InvalidStatusCode};
27
28/// Error when parsing a WebDAV responses.
29///
30/// This error type contains only the variants that are feasible during response parsing,
31/// excluding errors related to HTTP client operations or request building.
32#[derive(thiserror::Error, Debug)]
33pub enum ParseResponseError {
34    /// The server returned an invalid status code.
35    #[error("invalid status code in response: {0}")]
36    InvalidStatusCode(#[from] InvalidStatusCode),
37
38    /// Error parsing the XML response.
39    #[error("parsing XML response: {0}")]
40    Xml(#[from] roxmltree::Error),
41
42    /// The server returned an unexpected status code.
43    #[error("http request returned {0}")]
44    BadStatusCode(StatusCode),
45
46    /// The server returned a response that did not contain valid data.
47    #[error("invalid response: {0}")]
48    InvalidResponse(String),
49
50    /// The response is not valid UTF-8.
51    ///
52    /// At this time, other encodings are not supported.
53    #[error("decoding response as utf-8: {0}")]
54    NotUtf8(#[from] std::str::Utf8Error),
55}
56
57impl From<StatusCode> for ParseResponseError {
58    fn from(status: StatusCode) -> Self {
59        ParseResponseError::BadStatusCode(status)
60    }
61}
62
63impl From<NormalisationError> for ParseResponseError {
64    fn from(error: NormalisationError) -> Self {
65        // FIXME: hack
66        ParseResponseError::InvalidResponse(error.to_string())
67    }
68}
69
70impl<E> From<ParseResponseError> for WebDavError<E> {
71    fn from(error: ParseResponseError) -> Self {
72        match error {
73            ParseResponseError::InvalidStatusCode(e) => WebDavError::InvalidStatusCode(e),
74            ParseResponseError::Xml(e) => WebDavError::Xml(e),
75            ParseResponseError::BadStatusCode(s) => WebDavError::BadStatusCode(s),
76            ParseResponseError::InvalidResponse(msg) => WebDavError::InvalidResponse(msg.into()),
77            ParseResponseError::NotUtf8(e) => WebDavError::NotUtf8(e),
78        }
79    }
80}
81
82impl<E> From<crate::dav::PutResourceParseError> for WebDavError<E> {
83    fn from(error: crate::dav::PutResourceParseError) -> Self {
84        match error {
85            crate::dav::PutResourceParseError::BadStatusCode(s) => WebDavError::BadStatusCode(s),
86            crate::dav::PutResourceParseError::NotUtf8(e) => WebDavError::NotUtf8(e),
87            crate::dav::PutResourceParseError::PreconditionFailed(p) => {
88                WebDavError::PreconditionFailed(p)
89            }
90        }
91    }
92}
93
94/// A prepared HTTP request ready to be sent to a WebDAV server.
95///
96/// This struct contains all the information needed to execute an HTTP request,
97/// including the method, path, body, and any additional headers.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct PreparedRequest {
100    /// The HTTP method (e.g., PROPFIND, MKCOL, GET, PUT).
101    pub method: Method,
102    /// The path to the resource (must be percent-encoded by the client).
103    pub path: String,
104    /// The request body (typically XML for WebDAV requests).
105    pub body: String,
106    /// Additional HTTP headers to include in the request.
107    pub headers: Vec<(String, String)>,
108}
109
110/// Returns the HTTP header tuple for XML content with UTF-8 encoding.
111///
112/// Convenience helper to avoid copy-pasting the same block in a million places.
113#[must_use]
114pub fn xml_content_type_header() -> (String, String) {
115    (
116        "Content-Type".to_string(),
117        "application/xml; charset=utf-8".to_string(),
118    )
119}
120
121/// A WebDAV request that can be prepared and executed.
122///
123/// A type-safe WebDAV operation. Each implementation knows how to:
124///
125/// - Serialise itself into an HTTP request (`prepare_request`)
126/// - Parse the HTTP response into a typed response (`parse_response`)
127///
128/// # Example
129///
130/// ```
131/// use libdav::requests::{DavRequest, ParseResponseError, PreparedRequest};
132/// use http::{Method, StatusCode};
133///
134/// struct MyRequest {
135///     path: String,
136/// }
137///
138/// struct MyResponse {
139///     success: bool,
140/// }
141///
142/// impl DavRequest for MyRequest {
143///     type Response = MyResponse;
144///     type ParseError = ParseResponseError;
145///     type Error<E> = libdav::dav::WebDavError<E>;
146///
147///     fn prepare_request(&self) -> Result<PreparedRequest, http::Error> {
148///         Ok(PreparedRequest {
149///             method: Method::from_bytes(b"PROPFIND")?,
150///             path: self.path.clone(),
151///             body: String::from(r#"<propfind xmlns="DAV:"><prop><displayname/></prop></propfind>"#),
152///             headers: vec![("Depth".to_string(), "0".to_string())],
153///         })
154///     }
155///
156///     fn parse_response(&self, parts: &http::response::Parts, body: &[u8])
157///         -> Result<Self::Response, Self::ParseError>
158///     {
159///         Ok(MyResponse {
160///             success: parts.status.is_success(),
161///         })
162///     }
163/// }
164/// ```
165pub trait DavRequest {
166    /// The response type expected when parsing the response of this request.
167    type Response;
168
169    /// The error type returned when parsing the response of this request.
170    ///
171    /// Most implementations use [`ParseResponseError`], which would be the default is Rust
172    /// supported default values for associated types.
173    type ParseError;
174
175    /// The complete error type for this request operation.
176    ///
177    /// This is generic over the HTTP client's error type `E`. Most implementations use
178    /// [`crate::dav::WebDavError<E>`], which would be the default is Rust
179    /// supported default values for associated types.
180    ///
181    /// This error type must be convertible from:
182    /// - [`http::Error`] (URL building errors)
183    /// - [`crate::dav::RequestError<E>`] (HTTP request errors)
184    /// - [`Self::ParseError`] (response parsing errors)
185    type Error<E>;
186
187    /// Prepare the HTTP request.
188    ///
189    /// This method serialises the request into an HTTP method, path, body, and headers.
190    /// The path should not be percent-encoded; the client shall handle encoding.
191    ///
192    /// # Errors
193    ///
194    /// Returns an error if the request cannot be prepared (e.g., invalid parameters).
195    fn prepare_request(&self) -> Result<PreparedRequest, http::Error>;
196
197    /// Parse the HTTP response.
198    ///
199    /// This method deserialises the HTTP response into a typed response object.
200    ///
201    /// # Errors
202    ///
203    /// Returns an error if the response cannot be parsed (e.g., malformed XML, unexpected status).
204    fn parse_response(
205        &self,
206        parts: &Parts,
207        body: &[u8],
208    ) -> Result<Self::Response, Self::ParseError>;
209}