ddns_a/webhook/
http.rs

1//! HTTP request/response types and client trait.
2
3use super::HttpError;
4
5/// An HTTP request to be sent.
6///
7/// This is a value type that can be constructed and passed to any
8/// [`HttpClient`] implementation. It uses standard `http` crate types
9/// for method and headers, ensuring compatibility with the broader ecosystem.
10#[derive(Debug, Clone)]
11pub struct HttpRequest {
12    /// HTTP method (GET, POST, PUT, DELETE, etc.)
13    pub method: http::Method,
14    /// Target URL
15    pub url: url::Url,
16    /// HTTP headers to send
17    pub headers: http::HeaderMap,
18    /// Optional request body
19    pub body: Option<Vec<u8>>,
20}
21
22impl HttpRequest {
23    /// Creates a new HTTP request with the given method and URL.
24    ///
25    /// Headers are initialized to an empty map and body is `None`.
26    #[must_use]
27    pub fn new(method: http::Method, url: url::Url) -> Self {
28        Self {
29            method,
30            url,
31            headers: http::HeaderMap::new(),
32            body: None,
33        }
34    }
35
36    /// Creates a GET request to the given URL.
37    #[must_use]
38    pub fn get(url: url::Url) -> Self {
39        Self::new(http::Method::GET, url)
40    }
41
42    /// Creates a POST request to the given URL.
43    #[must_use]
44    pub fn post(url: url::Url) -> Self {
45        Self::new(http::Method::POST, url)
46    }
47
48    /// Sets the request body.
49    #[must_use]
50    pub fn with_body(mut self, body: Vec<u8>) -> Self {
51        self.body = Some(body);
52        self
53    }
54
55    /// Adds a header to the request.
56    ///
57    /// If the header name already exists, the value is appended
58    /// (HTTP headers can have multiple values).
59    #[must_use]
60    pub fn with_header(mut self, name: http::HeaderName, value: http::HeaderValue) -> Self {
61        self.headers.append(name, value);
62        self
63    }
64}
65
66/// An HTTP response received from a server.
67///
68/// Contains the status code, headers, and body of the response.
69/// The body is fully buffered into memory.
70#[derive(Debug, Clone)]
71pub struct HttpResponse {
72    /// HTTP status code
73    pub status: http::StatusCode,
74    /// Response headers
75    pub headers: http::HeaderMap,
76    /// Response body (fully buffered)
77    pub body: Vec<u8>,
78}
79
80impl HttpResponse {
81    /// Creates a new HTTP response.
82    #[must_use]
83    pub const fn new(status: http::StatusCode, headers: http::HeaderMap, body: Vec<u8>) -> Self {
84        Self {
85            status,
86            headers,
87            body,
88        }
89    }
90
91    /// Returns true if the status code indicates success (2xx).
92    #[must_use]
93    pub fn is_success(&self) -> bool {
94        self.status.is_success()
95    }
96
97    /// Returns the body as a UTF-8 string, if valid.
98    #[must_use]
99    pub fn body_text(&self) -> Option<&str> {
100        std::str::from_utf8(&self.body).ok()
101    }
102}
103
104/// Trait for making HTTP requests.
105///
106/// # Design
107///
108/// This trait abstracts the HTTP client implementation, enabling:
109/// - Dependency injection for testing with mock clients
110/// - Swapping HTTP libraries without changing calling code
111/// - Adding cross-cutting concerns (logging, metrics) via decorators
112///
113/// # Example
114///
115/// ```ignore
116/// use ddns_a::webhook::{HttpClient, HttpRequest, HttpResponse, HttpError};
117///
118/// struct MockClient {
119///     response: HttpResponse,
120/// }
121///
122/// impl HttpClient for MockClient {
123///     async fn request(&self, _req: HttpRequest) -> Result<HttpResponse, HttpError> {
124///         Ok(self.response.clone())
125///     }
126/// }
127/// ```
128pub trait HttpClient: Send + Sync {
129    /// Sends an HTTP request and returns the response.
130    ///
131    /// # Arguments
132    ///
133    /// * `req` - The HTTP request to send
134    ///
135    /// # Returns
136    ///
137    /// The HTTP response on success, or an [`HttpError`] on failure.
138    ///
139    /// # Errors
140    ///
141    /// Returns [`HttpError`] when:
142    /// - Network connection fails ([`HttpError::Connection`])
143    /// - Request times out ([`HttpError::Timeout`])
144    /// - URL is invalid ([`HttpError::InvalidUrl`])
145    fn request(
146        &self,
147        req: HttpRequest,
148    ) -> impl std::future::Future<Output = Result<HttpResponse, HttpError>> + Send;
149}