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}