Skip to main content

goose_http/response/
mod.rs

1//! HTTP response representation.
2//!
3//! Provides the shell for building status lines, headers, and bodies. Detailed
4//! semantics (validators, range headers, etc.) will be filled in future tasks.
5
6use std::{fmt, io};
7
8use bytes::Bytes;
9use futures_core::Stream;
10
11use crate::{
12    common::{HttpVersion, StatusCode},
13    headers::{HeaderName, Headers, header_keys},
14};
15
16/// Alias for boxed streaming response bodies.
17pub type BoxBodyStream = Box<dyn Stream<Item = io::Result<Bytes>> + Send + Unpin>;
18
19/// Represents the body of an HTTP response.
20pub enum ResponseBody {
21    /// No body content is present.
22    Empty,
23    /// Entire payload is buffered in memory.
24    Full(Bytes),
25    /// Payload will be produced lazily via chunked transfer encoding.
26    Stream(BoxBodyStream),
27}
28
29impl fmt::Debug for ResponseBody {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self {
32            ResponseBody::Empty => f.write_str("ResponseBody::Empty"),
33            ResponseBody::Full(bytes) => f.debug_tuple("ResponseBody::Full").field(bytes).finish(),
34            ResponseBody::Stream(_) => f.write_str("ResponseBody::Stream(<stream>)"),
35        }
36    }
37}
38
39impl ResponseBody {
40    /// Returns true if the body is empty.
41    pub fn is_empty(&self) -> bool {
42        matches!(self, ResponseBody::Empty)
43    }
44
45    /// Returns the known byte length when determinable.
46    pub fn len(&self) -> Option<usize> {
47        match self {
48            ResponseBody::Empty => Some(0),
49            ResponseBody::Full(bytes) => Some(bytes.len()),
50            ResponseBody::Stream(_) => None,
51        }
52    }
53
54    /// Indicates whether the body requires streaming.
55    pub fn is_stream(&self) -> bool {
56        matches!(self, ResponseBody::Stream(_))
57    }
58}
59
60/// Represents an HTTP response message.
61#[derive(Debug)]
62pub struct Response {
63    version: HttpVersion,
64    status: StatusCode,
65    reason: Option<String>,
66    headers: Headers,
67    body: ResponseBody,
68    trailers: Option<Headers>,
69}
70
71impl Response {
72    /// Create a new response with the provided status code and canonical reason.
73    pub fn new(status: StatusCode) -> Self {
74        Self {
75            version: HttpVersion::HTTP_1_1,
76            reason: None,
77            status,
78            headers: Headers::new(),
79            body: ResponseBody::Empty,
80            trailers: None,
81        }
82    }
83
84    /// Construct a builder for the response.
85    pub fn builder(status: StatusCode) -> ResponseBuilder {
86        ResponseBuilder::new(status)
87    }
88
89    /// Return the HTTP version of the response.
90    pub fn version(&self) -> HttpVersion {
91        self.version
92    }
93
94    /// Return the status code.
95    pub fn status(&self) -> StatusCode {
96        self.status
97    }
98
99    /// Access the reason phrase used for the status line.
100    pub fn reason_phrase(&self) -> &str {
101        self.reason
102            .as_deref()
103            .or_else(|| self.status.canonical_reason())
104            .unwrap_or("")
105    }
106
107    /// Borrow header map immutably.
108    pub fn headers(&self) -> &Headers {
109        &self.headers
110    }
111
112    /// Borrow header map mutably.
113    pub fn headers_mut(&mut self) -> &mut Headers {
114        &mut self.headers
115    }
116
117    /// Borrow the body representation.
118    pub fn body(&self) -> &ResponseBody {
119        &self.body
120    }
121
122    /// Override the status code.
123    pub fn set_status(&mut self, status: StatusCode) {
124        self.status = status;
125    }
126
127    /// Remove and return the body, leaving an empty placeholder behind.
128    pub fn take_body(&mut self) -> ResponseBody {
129        std::mem::replace(&mut self.body, ResponseBody::Empty)
130    }
131
132    /// Replace the body with the provided representation.
133    pub fn set_body(&mut self, body: ResponseBody) {
134        self.body = body;
135    }
136
137    /// Convenience helper for replacing the body with fully buffered bytes.
138    pub fn set_body_bytes(&mut self, bytes: impl Into<Bytes>) {
139        self.body = ResponseBody::Full(bytes.into());
140    }
141
142    /// Replace the body with a static byte slice without copying.
143    pub fn set_body_static(&mut self, bytes: &'static [u8]) {
144        self.body = ResponseBody::Full(Bytes::from_static(bytes));
145    }
146
147    /// Replace the body with a static UTF-8 string without copying.
148    pub fn set_body_text_static(&mut self, text: &'static str) {
149        self.set_body_static(text.as_bytes());
150    }
151
152    /// Prepare the response for a HEAD reply by stripping the body while
153    /// preserving the advertised payload length when known.
154    pub fn strip_body_for_head(&mut self) {
155        if let ResponseBody::Full(bytes) = &self.body {
156            if !self.headers.contains(header_keys::CONTENT_LENGTH) {
157                self.headers
158                    .insert(header_keys::CONTENT_LENGTH, bytes.len().to_string());
159            }
160        }
161        self.body = ResponseBody::Empty;
162    }
163
164    /// Borrow the trailers if present.
165    pub fn trailers(&self) -> Option<&Headers> {
166        self.trailers.as_ref()
167    }
168
169    /// Replace the trailers with the provided header set.
170    pub fn set_trailers(&mut self, trailers: Headers) {
171        self.trailers = Some(trailers);
172    }
173
174    /// Remove any trailers from the response and return them.
175    pub fn take_trailers(&mut self) -> Option<Headers> {
176        self.trailers.take()
177    }
178
179    /// Borrow the body mutably.
180    pub fn body_mut(&mut self) -> &mut ResponseBody {
181        &mut self.body
182    }
183
184    /// Override the HTTP version.
185    pub fn set_version(&mut self, version: HttpVersion) {
186        self.version = version;
187    }
188
189    /// Override the reason phrase.
190    pub fn set_reason(&mut self, reason: impl Into<String>) {
191        self.reason = Some(reason.into());
192    }
193}
194
195/// Builder pattern for constructing responses.
196#[derive(Debug)]
197pub struct ResponseBuilder {
198    version: HttpVersion,
199    status: StatusCode,
200    reason: Option<String>,
201    headers: Headers,
202    body: ResponseBody,
203    trailers: Option<Headers>,
204}
205
206impl ResponseBuilder {
207    fn new(status: StatusCode) -> Self {
208        Self {
209            version: HttpVersion::HTTP_1_1,
210            status,
211            reason: None,
212            headers: Headers::new(),
213            body: ResponseBody::Empty,
214            trailers: None,
215        }
216    }
217
218    /// Override the HTTP version.
219    pub fn version(mut self, version: HttpVersion) -> Self {
220        self.version = version;
221        self
222    }
223
224    /// Override the reason phrase.
225    pub fn reason(mut self, reason: impl Into<String>) -> Self {
226        self.reason = Some(reason.into());
227        self
228    }
229
230    /// Append a header value.
231    pub fn header(mut self, name: impl Into<HeaderName>, value: impl Into<String>) -> Self {
232        self.headers.insert(name, value);
233        self
234    }
235
236    /// Set the response body representation.
237    pub fn body(mut self, body: ResponseBody) -> Self {
238        self.body = body;
239        self
240    }
241
242    /// Provide a fully buffered body payload.
243    pub fn body_bytes(mut self, bytes: impl Into<Bytes>) -> Self {
244        self.body = ResponseBody::Full(bytes.into());
245        self
246    }
247
248    /// Provide a static byte slice body payload without copying.
249    pub fn body_static(mut self, bytes: &'static [u8]) -> Self {
250        self.body = ResponseBody::Full(Bytes::from_static(bytes));
251        self
252    }
253
254    /// Provide a static UTF-8 string body payload without copying.
255    pub fn text_static(self, text: &'static str) -> Self {
256        self.body_static(text.as_bytes())
257    }
258
259    /// Attach response trailers that will be emitted after the payload.
260    pub fn trailers(mut self, trailers: Headers) -> Self {
261        self.trailers = Some(trailers);
262        self
263    }
264
265    /// Build the response.
266    pub fn build(self) -> Response {
267        Response {
268            version: self.version,
269            status: self.status,
270            reason: self.reason,
271            headers: self.headers,
272            body: self.body,
273            trailers: self.trailers,
274        }
275    }
276}