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}