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