Skip to main content

stackforge_core/layer/http/
builder.rs

1//! Fluent builders for serialising HTTP/1.x request and response messages.
2//!
3//! Both builders produce a complete, CRLF-terminated byte representation
4//! suitable for transmission over a TCP stream.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use stackforge_core::layer::http::builder::{HttpRequestBuilder, HttpResponseBuilder};
10//!
11//! // Build a simple GET request.
12//! let bytes = HttpRequestBuilder::new()
13//!     .method("GET")
14//!     .uri("/index.html")
15//!     .header("Host", "example.com")
16//!     .header("Accept", "*/*")
17//!     .build();
18//!
19//! assert!(bytes.starts_with(b"GET /index.html HTTP/1.1\r\n"));
20//!
21//! // Build a 200 OK response with a body.
22//! let body = b"Hello, World!";
23//! let bytes = HttpResponseBuilder::new()
24//!     .status(200, "OK")
25//!     .header("Content-Type", "text/plain")
26//!     .body(body.to_vec())
27//!     .build();
28//!
29//! assert!(bytes.starts_with(b"HTTP/1.1 200 OK\r\n"));
30//! ```
31
32// ---------------------------------------------------------------------------
33// HttpRequestBuilder
34// ---------------------------------------------------------------------------
35
36/// Builder for HTTP/1.x request messages.
37///
38/// Default values:
39/// - method : `GET`
40/// - URI    : `/`
41/// - version: `HTTP/1.1`
42/// - headers: empty
43/// - body   : empty
44#[derive(Debug, Clone)]
45pub struct HttpRequestBuilder {
46    method: String,
47    uri: String,
48    version: String,
49    headers: Vec<(String, String)>,
50    body: Vec<u8>,
51}
52
53impl Default for HttpRequestBuilder {
54    fn default() -> Self {
55        Self {
56            method: "GET".to_owned(),
57            uri: "/".to_owned(),
58            version: "HTTP/1.1".to_owned(),
59            headers: Vec::new(),
60            body: Vec::new(),
61        }
62    }
63}
64
65impl HttpRequestBuilder {
66    /// Create a new builder with default values (`GET / HTTP/1.1`).
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Set the HTTP method (e.g. `"GET"`, `"POST"`).
72    pub fn method(mut self, method: &str) -> Self {
73        self.method = method.to_owned();
74        self
75    }
76
77    /// Set the request-URI.
78    pub fn uri(mut self, uri: &str) -> Self {
79        self.uri = uri.to_owned();
80        self
81    }
82
83    /// Set the HTTP version string (e.g. `"HTTP/1.0"` or `"HTTP/1.1"`).
84    pub fn version(mut self, version: &str) -> Self {
85        self.version = version.to_owned();
86        self
87    }
88
89    /// Append a request header.
90    ///
91    /// Headers are written in the order they are added.  No deduplication is
92    /// performed.
93    pub fn header(mut self, name: &str, value: &str) -> Self {
94        self.headers.push((name.to_owned(), value.to_owned()));
95        self
96    }
97
98    /// Set the message body.
99    ///
100    /// Note: this method does **not** automatically add a `Content-Length`
101    /// header; callers should add it explicitly if required.
102    pub fn body(mut self, body: Vec<u8>) -> Self {
103        self.body = body;
104        self
105    }
106
107    /// Serialise the request to bytes.
108    ///
109    /// The output uses CRLF line endings and follows the standard HTTP/1.x
110    /// wire format:
111    ///
112    /// ```text
113    /// METHOD URI VERSION\r\n
114    /// Header-Name: header-value\r\n
115    /// ...
116    /// \r\n
117    /// [body]
118    /// ```
119    pub fn build(&self) -> Vec<u8> {
120        let mut out = Vec::new();
121
122        // Request line.
123        out.extend_from_slice(self.method.as_bytes());
124        out.push(b' ');
125        out.extend_from_slice(self.uri.as_bytes());
126        out.push(b' ');
127        out.extend_from_slice(self.version.as_bytes());
128        out.extend_from_slice(b"\r\n");
129
130        // Headers.
131        for (name, value) in &self.headers {
132            out.extend_from_slice(name.as_bytes());
133            out.extend_from_slice(b": ");
134            out.extend_from_slice(value.as_bytes());
135            out.extend_from_slice(b"\r\n");
136        }
137
138        // Blank line.
139        out.extend_from_slice(b"\r\n");
140
141        // Body (may be empty).
142        out.extend_from_slice(&self.body);
143
144        out
145    }
146}
147
148// ---------------------------------------------------------------------------
149// HttpResponseBuilder
150// ---------------------------------------------------------------------------
151
152/// Builder for HTTP/1.x response messages.
153///
154/// Default values:
155/// - version    : `HTTP/1.1`
156/// - status code: `200`
157/// - reason     : `OK`
158/// - headers    : empty
159/// - body       : empty
160#[derive(Debug, Clone)]
161pub struct HttpResponseBuilder {
162    version: String,
163    status_code: u16,
164    reason: String,
165    headers: Vec<(String, String)>,
166    body: Vec<u8>,
167}
168
169impl Default for HttpResponseBuilder {
170    fn default() -> Self {
171        Self {
172            version: "HTTP/1.1".to_owned(),
173            status_code: 200,
174            reason: "OK".to_owned(),
175            headers: Vec::new(),
176            body: Vec::new(),
177        }
178    }
179}
180
181impl HttpResponseBuilder {
182    /// Create a new builder with default values (`HTTP/1.1 200 OK`).
183    pub fn new() -> Self {
184        Self::default()
185    }
186
187    /// Set the HTTP version string.
188    pub fn version(mut self, version: &str) -> Self {
189        self.version = version.to_owned();
190        self
191    }
192
193    /// Set the status code and reason phrase together.
194    ///
195    /// # Example
196    ///
197    /// ```rust
198    /// use stackforge_core::layer::http::builder::HttpResponseBuilder;
199    ///
200    /// let bytes = HttpResponseBuilder::new()
201    ///     .status(404, "Not Found")
202    ///     .build();
203    ///
204    /// assert!(bytes.starts_with(b"HTTP/1.1 404 Not Found\r\n"));
205    /// ```
206    pub fn status(mut self, code: u16, reason: &str) -> Self {
207        self.status_code = code;
208        self.reason = reason.to_owned();
209        self
210    }
211
212    /// Append a response header.
213    ///
214    /// Headers are written in the order they are added.  No deduplication is
215    /// performed.
216    pub fn header(mut self, name: &str, value: &str) -> Self {
217        self.headers.push((name.to_owned(), value.to_owned()));
218        self
219    }
220
221    /// Set the message body.
222    ///
223    /// Note: this method does **not** automatically add a `Content-Length`
224    /// header; callers should add it explicitly if required.
225    pub fn body(mut self, body: Vec<u8>) -> Self {
226        self.body = body;
227        self
228    }
229
230    /// Serialise the response to bytes.
231    ///
232    /// The output uses CRLF line endings and follows the standard HTTP/1.x
233    /// wire format:
234    ///
235    /// ```text
236    /// VERSION STATUS_CODE REASON\r\n
237    /// Header-Name: header-value\r\n
238    /// ...
239    /// \r\n
240    /// [body]
241    /// ```
242    pub fn build(&self) -> Vec<u8> {
243        let mut out = Vec::new();
244
245        // Status line.
246        out.extend_from_slice(self.version.as_bytes());
247        out.push(b' ');
248        out.extend_from_slice(self.status_code.to_string().as_bytes());
249        out.push(b' ');
250        out.extend_from_slice(self.reason.as_bytes());
251        out.extend_from_slice(b"\r\n");
252
253        // Headers.
254        for (name, value) in &self.headers {
255            out.extend_from_slice(name.as_bytes());
256            out.extend_from_slice(b": ");
257            out.extend_from_slice(value.as_bytes());
258            out.extend_from_slice(b"\r\n");
259        }
260
261        // Blank line.
262        out.extend_from_slice(b"\r\n");
263
264        // Body (may be empty).
265        out.extend_from_slice(&self.body);
266
267        out
268    }
269}
270
271// ---------------------------------------------------------------------------
272// Tests
273// ---------------------------------------------------------------------------
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use crate::layer::http::request::HttpRequest;
279    use crate::layer::http::response::HttpResponse;
280
281    #[test]
282    fn test_request_builder_defaults() {
283        let bytes = HttpRequestBuilder::new().build();
284        assert!(bytes.starts_with(b"GET / HTTP/1.1\r\n"));
285        assert!(bytes.ends_with(b"\r\n\r\n"));
286    }
287
288    #[test]
289    fn test_request_builder_full() {
290        let body = b"key=value".to_vec();
291        let bytes = HttpRequestBuilder::new()
292            .method("POST")
293            .uri("/submit")
294            .version("HTTP/1.1")
295            .header("Host", "example.com")
296            .header("Content-Type", "application/x-www-form-urlencoded")
297            .header("Content-Length", &body.len().to_string())
298            .body(body.clone())
299            .build();
300
301        // Must be parseable by HttpRequest.
302        let req = HttpRequest::parse(&bytes).expect("should parse");
303        assert_eq!(req.method, "POST");
304        assert_eq!(req.uri, "/submit");
305        assert_eq!(req.version, "HTTP/1.1");
306        assert_eq!(req.headers.len(), 3);
307        let parsed_body = &bytes[req.body_offset..];
308        assert_eq!(parsed_body, body.as_slice());
309    }
310
311    #[test]
312    fn test_response_builder_defaults() {
313        let bytes = HttpResponseBuilder::new().build();
314        assert!(bytes.starts_with(b"HTTP/1.1 200 OK\r\n"));
315        assert!(bytes.ends_with(b"\r\n\r\n"));
316    }
317
318    #[test]
319    fn test_response_builder_full() {
320        let body = b"Hello, World!".to_vec();
321        let bytes = HttpResponseBuilder::new()
322            .status(200, "OK")
323            .header("Content-Type", "text/plain")
324            .header("Content-Length", &body.len().to_string())
325            .body(body.clone())
326            .build();
327
328        // Must be parseable by HttpResponse.
329        let resp = HttpResponse::parse(&bytes).expect("should parse");
330        assert_eq!(resp.status_code, 200);
331        assert_eq!(resp.reason, "OK");
332        assert_eq!(resp.headers.len(), 2);
333        let parsed_body = &bytes[resp.body_offset..];
334        assert_eq!(parsed_body, body.as_slice());
335    }
336
337    #[test]
338    fn test_response_builder_404() {
339        let bytes = HttpResponseBuilder::new().status(404, "Not Found").build();
340
341        assert!(bytes.starts_with(b"HTTP/1.1 404 Not Found\r\n"));
342        let resp = HttpResponse::parse(&bytes).unwrap();
343        assert_eq!(resp.status_code, 404);
344        assert_eq!(resp.reason, "Not Found");
345    }
346
347    #[test]
348    fn test_request_builder_http10() {
349        let bytes = HttpRequestBuilder::new()
350            .version("HTTP/1.0")
351            .uri("/old")
352            .build();
353        assert!(bytes.starts_with(b"GET /old HTTP/1.0\r\n"));
354    }
355
356    #[test]
357    fn test_multiple_headers_ordering() {
358        let bytes = HttpRequestBuilder::new()
359            .header("A", "1")
360            .header("B", "2")
361            .header("C", "3")
362            .build();
363
364        let req = HttpRequest::parse(&bytes).unwrap();
365        assert_eq!(req.headers[0], ("A", "1"));
366        assert_eq!(req.headers[1], ("B", "2"));
367        assert_eq!(req.headers[2], ("C", "3"));
368    }
369}