1use std::pin::Pin;
2
3use bytes::Bytes;
4use futures_core::Stream;
5use futures_util::StreamExt;
6use http::StatusCode;
7use tokio_util::io::ReaderStream;
8
9use crate::exception::{
10 ForbiddenException, HttpException, InternalServerErrorException, NotFoundException,
11};
12use crate::result::Result;
13
14pub type BoxBodyStream = Pin<Box<dyn Stream<Item = Result<Bytes>> + Send>>;
16
17pub enum ResponseBody {
19 Buffered(Vec<u8>),
20 Streaming(BoxBodyStream),
21}
22
23impl ResponseBody {
24 pub fn as_buffered(&self) -> Option<&[u8]> {
25 match self {
26 ResponseBody::Buffered(bytes) => Some(bytes.as_slice()),
27 ResponseBody::Streaming(_) => None,
28 }
29 }
30
31 pub fn as_buffered_mut(&mut self) -> Option<&mut Vec<u8>> {
32 match self {
33 ResponseBody::Buffered(bytes) => Some(bytes),
34 ResponseBody::Streaming(_) => None,
35 }
36 }
37
38 pub fn is_streaming(&self) -> bool {
39 matches!(self, ResponseBody::Streaming(_))
40 }
41
42 pub fn is_empty(&self) -> bool {
43 match self {
44 ResponseBody::Buffered(bytes) => bytes.is_empty(),
45 ResponseBody::Streaming(_) => false,
46 }
47 }
48}
49
50pub struct HttpResponse {
51 pub status: StatusCode,
52 pub body: ResponseBody,
53 pub content_type: &'static str,
54 pub headers: Vec<(String, String)>,
60}
61
62impl HttpResponse {
63 pub fn new(status: StatusCode, body: Vec<u8>, content_type: &'static str) -> Self {
64 Self {
65 status,
66 body: ResponseBody::Buffered(body),
67 content_type,
68 headers: Vec::new(),
69 }
70 }
71
72 pub fn json(status: StatusCode, body: impl serde::Serialize) -> Self {
75 match serde_json::to_vec(&body) {
76 Ok(body) => Self::new(status, body, "application/json"),
77 Err(_) => json_serialization_error_response(),
78 }
79 }
80
81 pub fn text(status: StatusCode, body: impl Into<String>) -> Self {
82 Self::new(status, body.into().into_bytes(), "text/plain")
83 }
84
85 pub fn bytes(status: StatusCode, body: impl Into<Vec<u8>>) -> Self {
86 Self::new(status, body.into(), "application/octet-stream")
87 }
88
89 pub fn body_bytes(&self) -> Option<&[u8]> {
91 self.body.as_buffered()
92 }
93
94 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
97 self.insert_header(name, value);
98 self
99 }
100
101 pub fn insert_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
103 self.headers.push((name.into(), value.into()));
104 }
105}
106
107pub trait IntoCaelixResponse {
108 fn into_response(self) -> HttpResponse;
109}
110
111pub enum Body {
115 Json(Vec<u8>),
116 Text(String),
117 Bytes(Vec<u8>),
118}
119
120impl Body {
121 fn respond_with(self, status: StatusCode) -> HttpResponse {
125 match self {
126 Body::Json(bytes) => HttpResponse::new(status, bytes, "application/json"),
127 Body::Text(text) => HttpResponse::text(status, text),
128 Body::Bytes(bytes) => HttpResponse::bytes(status, bytes),
129 }
130 }
131}
132
133pub enum Response<T> {
134 Body(T),
135 WithStatus(StatusCode, T),
136 Raw(StatusCode, Body),
137 Empty,
138}
139
140impl Response<()> {
141 pub fn no_content() -> Self {
142 Response::Empty
143 }
144
145 pub fn text(status: StatusCode, value: impl Into<String>) -> Self {
146 Response::Raw(status, Body::Text(value.into()))
147 }
148
149 pub fn bytes(status: StatusCode, value: impl Into<Vec<u8>>) -> Self {
150 Response::Raw(status, Body::Bytes(value.into()))
151 }
152
153 pub fn json(status: StatusCode, value: impl serde::Serialize) -> Self {
154 let bytes = match serde_json::to_vec(&value) {
155 Ok(bytes) => bytes,
156 Err(_) => {
157 return Response::Raw(
158 StatusCode::INTERNAL_SERVER_ERROR,
159 Body::Json(json_serialization_error_body()),
160 );
161 }
162 };
163 Response::Raw(status, Body::Json(bytes))
164 }
165
166 pub fn stream(
169 content_type: &'static str,
170 stream: impl Stream<Item = Result<Bytes>> + Send + 'static,
171 ) -> HttpResponse {
172 HttpResponse {
173 status: StatusCode::OK,
174 body: ResponseBody::Streaming(Box::pin(stream)),
175 content_type,
176 headers: Vec::new(),
177 }
178 }
179
180 pub fn sse<T>(stream: impl Stream<Item = Result<T>> + Send + 'static) -> HttpResponse
187 where
188 T: serde::Serialize + 'static,
189 {
190 let framed = stream.map(|item| {
191 item.and_then(|value| {
192 let json = serde_json::to_string(&value).map_err(|err| {
193 InternalServerErrorException::new(anyhow::anyhow!(
194 "failed to serialize SSE event: {err}"
195 ))
196 })?;
197 Ok(Bytes::from(format!("data: {json}\n\n")))
198 })
199 });
200 Response::stream("text/event-stream", framed)
201 .with_header("Cache-Control", "no-cache")
202 .with_header("X-Accel-Buffering", "no")
203 }
204
205 pub async fn file(
209 path: impl AsRef<std::path::Path>,
210 content_type: &'static str,
211 ) -> Result<HttpResponse> {
212 let file = tokio::fs::File::open(path)
213 .await
214 .map_err(map_file_open_error)?;
215
216 let stream = ReaderStream::new(file).map(|chunk| {
217 chunk
218 .map(Bytes::from)
219 .map_err(|err| InternalServerErrorException::new(err))
220 });
221
222 Ok(Response::stream(content_type, stream))
223 }
224}
225
226pub(crate) fn map_file_open_error(err: std::io::Error) -> HttpException {
227 match err.kind() {
228 std::io::ErrorKind::NotFound => NotFoundException::new("file not found"),
229 std::io::ErrorKind::PermissionDenied => ForbiddenException::new("permission denied"),
230 _ => InternalServerErrorException::new(err),
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use std::io::{Error, ErrorKind};
238
239 #[test]
240 fn map_file_open_error_classifies_io_kinds() {
241 let not_found = map_file_open_error(Error::new(ErrorKind::NotFound, "gone"));
242 assert_eq!(not_found.status, StatusCode::NOT_FOUND);
243
244 let denied = map_file_open_error(Error::new(ErrorKind::PermissionDenied, "nope"));
245 assert_eq!(denied.status, StatusCode::FORBIDDEN);
246
247 let other = map_file_open_error(Error::new(ErrorKind::Other, "disk failed"));
248 assert_eq!(other.status, StatusCode::INTERNAL_SERVER_ERROR);
249 }
250
251 #[test]
252 fn with_header_accepts_dynamic_values() {
253 let filename = format!("report-{}.csv", 123);
254 let response = HttpResponse::text(StatusCode::OK, "a,b\n").with_header(
255 "Content-Disposition",
256 format!("attachment; filename=\"{filename}\""),
257 );
258
259 assert_eq!(
260 response.headers,
261 vec![(
262 "Content-Disposition".to_string(),
263 "attachment; filename=\"report-123.csv\"".to_string()
264 )]
265 );
266 }
267
268 #[test]
269 fn insert_header_mutates_in_place() {
270 let mut response = HttpResponse::text(StatusCode::OK, "ok");
271 response.insert_header("X-Request-Id", "abc-123");
272 assert_eq!(
273 response.headers,
274 vec![("X-Request-Id".to_string(), "abc-123".to_string())]
275 );
276 }
277}
278
279fn json_serialization_error_response() -> HttpResponse {
280 HttpResponse::new(
281 StatusCode::INTERNAL_SERVER_ERROR,
282 json_serialization_error_body(),
283 "application/json",
284 )
285}
286
287fn json_serialization_error_body() -> Vec<u8> {
288 br#"{"status":500,"error":"Internal Server Error","message":"Internal Server Error"}"#.to_vec()
289}
290
291impl IntoCaelixResponse for HttpResponse {
292 fn into_response(self) -> HttpResponse {
293 self
294 }
295}
296
297impl IntoCaelixResponse for String {
298 fn into_response(self) -> HttpResponse {
299 HttpResponse::text(StatusCode::OK, self)
300 }
301}
302
303impl IntoCaelixResponse for &'static str {
304 fn into_response(self) -> HttpResponse {
305 HttpResponse::text(StatusCode::OK, self)
306 }
307}
308
309impl IntoCaelixResponse for HttpException {
310 fn into_response(self) -> HttpResponse {
311 #[derive(serde::Serialize)]
312 struct ErrorBody {
313 status: u16,
314 error: &'static str,
315 message: String,
316 #[serde(skip_serializing_if = "Option::is_none")]
317 errors: Option<std::collections::BTreeMap<String, Vec<String>>>,
318 }
319
320 let (message, errors) = if self.status.is_server_error() {
321 ("Internal Server Error".to_string(), None)
322 } else {
323 (self.message, self.errors)
324 };
325
326 HttpResponse::json(
327 self.status,
328 ErrorBody {
329 status: self.status.as_u16(),
330 error: self.error,
331 message,
332 errors,
333 },
334 )
335 }
336}
337
338impl<T: serde::Serialize> IntoCaelixResponse for Response<T> {
339 fn into_response(self) -> HttpResponse {
340 match self {
341 Response::Body(value) => HttpResponse::json(StatusCode::OK, value),
342 Response::WithStatus(status, value) => HttpResponse::json(status, value),
343 Response::Raw(status, body) => body.respond_with(status),
344 Response::Empty => {
345 HttpResponse::new(StatusCode::NO_CONTENT, Vec::new(), "application/json")
346 }
347 }
348 }
349}