Skip to main content

hooch_http/
response.rs

1//! HTTP Response Building and Serialization Module
2//!
3//! This module provides a minimal `HttpResponseBuilder` utility for constructing HTTP
4//! responses, along with associated types like `HttpStatus`, `HeaderKey`, and `HeaderValue`.
5//! Responses can be serialized into byte buffers for sending over a network.
6
7use std::collections::HashMap;
8use std::io::Write;
9
10use crate::shared::HttpVersion;
11
12/// Common HTTP status codes.
13#[derive(Debug, Copy, Clone)]
14pub enum HttpStatus {
15    Ok,
16    Created,
17    NoContent,
18    BadRequest,
19    Unauthorized,
20    Forbidden,
21    NotFound,
22    InternalServerError,
23    BadGateway,
24    ServiceUnavailable,
25}
26
27/// Convert an `HttpStatus` to its numeric status code.
28impl From<HttpStatus> for u16 {
29    fn from(status: HttpStatus) -> u16 {
30        match status {
31            HttpStatus::Ok => 200,
32            HttpStatus::Created => 201,
33            HttpStatus::NoContent => 204,
34            HttpStatus::BadRequest => 400,
35            HttpStatus::Unauthorized => 401,
36            HttpStatus::Forbidden => 403,
37            HttpStatus::NotFound => 404,
38            HttpStatus::InternalServerError => 500,
39            HttpStatus::BadGateway => 502,
40            HttpStatus::ServiceUnavailable => 503,
41        }
42    }
43}
44
45/// Convert an `HttpStatus` to its standard reason phrase.
46impl From<HttpStatus> for &'static str {
47    fn from(status: HttpStatus) -> &'static str {
48        match status {
49            HttpStatus::Ok => "OK",
50            HttpStatus::Created => "Created",
51            HttpStatus::NoContent => "No Content",
52            HttpStatus::BadRequest => "Bad Request",
53            HttpStatus::Unauthorized => "Unauthorized",
54            HttpStatus::Forbidden => "Forbidden",
55            HttpStatus::NotFound => "Not Found",
56            HttpStatus::InternalServerError => "Internal Server Error",
57            HttpStatus::BadGateway => "Bad Gateway",
58            HttpStatus::ServiceUnavailable => "Service Unavailable",
59        }
60    }
61}
62
63/// Wrapper for HTTP header keys.
64#[derive(Debug)]
65pub struct HeaderKey(String);
66
67impl AsRef<str> for HeaderKey {
68    fn as_ref(&self) -> &str {
69        &self.0
70    }
71}
72
73impl AsMut<String> for HeaderKey {
74    fn as_mut(&mut self) -> &mut String {
75        &mut self.0
76    }
77}
78
79impl From<String> for HeaderKey {
80    fn from(value: String) -> Self {
81        Self(value)
82    }
83}
84
85/// Wrapper for HTTP header values.
86#[derive(Debug)]
87pub struct HeaderValue(String);
88
89impl AsRef<str> for HeaderValue {
90    fn as_ref(&self) -> &str {
91        &self.0
92    }
93}
94
95impl AsMut<String> for HeaderValue {
96    fn as_mut(&mut self) -> &mut String {
97        &mut self.0
98    }
99}
100
101impl From<String> for HeaderValue {
102    fn from(value: String) -> Self {
103        Self(value)
104    }
105}
106
107/// Builder struct for constructing an HTTP response.
108#[derive(Debug)]
109pub struct HttpResponseBuilder {
110    status: HttpStatus,
111    protocal: Option<HttpVersion>,
112    headers: Option<HashMap<HeaderKey, HeaderValue>>,
113    body: Option<String>,
114}
115
116impl HttpResponseBuilder {
117    /// Create a builder with a custom status.
118    pub fn new(status: HttpStatus) -> Self {
119        Self {
120            status,
121            protocal: None,
122            headers: None,
123            body: None,
124        }
125    }
126
127    /// Shortcut for 200 OK.
128    pub fn ok() -> Self {
129        Self::new(HttpStatus::Ok)
130    }
131
132    /// Shortcut for 201 Created.
133    pub fn created() -> Self {
134        Self::new(HttpStatus::Created)
135    }
136
137    /// Shortcut for 204 No Content.
138    pub fn no_content() -> Self {
139        Self::new(HttpStatus::NoContent)
140    }
141
142    /// Shortcut for 400 Bad Request.
143    pub fn bad_request() -> Self {
144        Self::new(HttpStatus::BadRequest)
145    }
146
147    /// Shortcut for 401 Unauthorized.
148    pub fn unauthorized() -> Self {
149        Self::new(HttpStatus::Unauthorized)
150    }
151
152    /// Shortcut for 403 Forbidden.
153    pub fn forbidden() -> Self {
154        Self::new(HttpStatus::Forbidden)
155    }
156
157    /// Shortcut for 404 Not Found.
158    pub fn not_found() -> Self {
159        Self::new(HttpStatus::NotFound)
160    }
161
162    /// Shortcut for 500 Internal Server Error.
163    pub fn internal_server_error() -> Self {
164        Self::new(HttpStatus::InternalServerError)
165    }
166
167    /// Shortcut for 502 Bad Gateway.
168    pub fn bad_gateway() -> Self {
169        Self::new(HttpStatus::BadGateway)
170    }
171
172    /// Shortcut for 503 Service Unavailable.
173    pub fn service_unavailable() -> Self {
174        Self::new(HttpStatus::ServiceUnavailable)
175    }
176
177    /// Set the HTTP protocol version (defaults to 1.1).
178    pub fn protocal(mut self, protocal: HttpVersion) -> Self {
179        self.protocal = Some(protocal);
180        self
181    }
182
183    /// Set the response headers.
184    pub fn headers(mut self, headers: HashMap<HeaderKey, HeaderValue>) -> Self {
185        self.headers = Some(headers);
186        self
187    }
188
189    /// Get a mutable reference to the headers (if present).
190    pub fn get_mut_headers(&mut self) -> Option<&mut HashMap<HeaderKey, HeaderValue>> {
191        self.headers.as_mut()
192    }
193
194    /// Set the response body.
195    pub fn body(mut self, body: String) -> Self {
196        self.body = Some(body);
197        self
198    }
199
200    /// Finalize the builder and return a constructed `HttpResponse`.
201    pub fn build(self) -> HttpResponse {
202        HttpResponse {
203            status: self.status,
204            protocal: self.protocal.unwrap_or(HttpVersion::OnePointOne),
205            headers: self.headers,
206            body: self.body,
207        }
208    }
209}
210
211/// Represents a fully built HTTP response.
212#[derive(Debug)]
213pub struct HttpResponse {
214    status: HttpStatus,
215    protocal: HttpVersion,
216    headers: Option<HashMap<HeaderKey, HeaderValue>>,
217    body: Option<String>,
218}
219
220impl HttpResponse {
221    /// Serialize the HTTP response to a byte buffer, suitable for sending over the network.
222    pub fn serialize(self, mut buffer: Vec<u8>) -> Vec<u8> {
223        // Write status line
224        write!(
225            &mut buffer,
226            "{} {}\r\n",
227            <&str>::from(self.protocal),
228            format!("{} {}", u16::from(self.status), <&str>::from(self.status))
229        )
230        .unwrap();
231
232        // Write headers
233        if let Some(headers) = self.headers {
234            headers.iter().for_each(|(key, value)| {
235                write!(&mut buffer, "{}: {}\r\n", key.as_ref(), value.as_ref()).unwrap();
236            });
237        }
238
239        // End of headers
240        write!(&mut buffer, "\r\n").unwrap();
241
242        // Write body if present
243        if let Some(body) = self.body {
244            write!(&mut buffer, "{}", body).unwrap();
245        }
246
247        buffer
248    }
249}