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>),
21 Streaming(BoxBodyStream),
23}
24
25impl ResponseBody {
26 pub fn as_buffered(&self) -> Option<&[u8]> {
28 match self {
29 ResponseBody::Buffered(bytes) => Some(bytes.as_slice()),
30 ResponseBody::Streaming(_) => None,
31 }
32 }
33
34 pub fn as_buffered_mut(&mut self) -> Option<&mut Vec<u8>> {
36 match self {
37 ResponseBody::Buffered(bytes) => Some(bytes),
38 ResponseBody::Streaming(_) => None,
39 }
40 }
41
42 pub fn is_streaming(&self) -> bool {
44 matches!(self, ResponseBody::Streaming(_))
45 }
46
47 pub fn is_empty(&self) -> bool {
49 match self {
50 ResponseBody::Buffered(bytes) => bytes.is_empty(),
51 ResponseBody::Streaming(_) => false,
52 }
53 }
54}
55
56pub struct HttpResponse {
58 pub status: StatusCode,
60 pub body: ResponseBody,
62 pub content_type: &'static str,
64 pub headers: Vec<(String, String)>,
70}
71
72impl HttpResponse {
73 pub fn new(status: StatusCode, body: Vec<u8>, content_type: &'static str) -> Self {
75 Self {
76 status,
77 body: ResponseBody::Buffered(body),
78 content_type,
79 headers: Vec::new(),
80 }
81 }
82
83 pub fn json(status: StatusCode, body: impl serde::Serialize) -> Self {
86 match serde_json::to_vec(&body) {
87 Ok(body) => Self::new(status, body, "application/json"),
88 Err(_) => json_serialization_error_response(),
89 }
90 }
91
92 pub fn text(status: StatusCode, body: impl Into<String>) -> Self {
94 Self::new(status, body.into().into_bytes(), "text/plain")
95 }
96
97 pub fn bytes(status: StatusCode, body: impl Into<Vec<u8>>) -> Self {
99 Self::new(status, body.into(), "application/octet-stream")
100 }
101
102 pub fn body_bytes(&self) -> Option<&[u8]> {
104 self.body.as_buffered()
105 }
106
107 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
110 self.insert_header(name, value);
111 self
112 }
113
114 pub fn insert_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
116 self.headers.push((name.into(), value.into()));
117 }
118}
119
120pub trait IntoCaelixResponse {
122 fn into_response(self) -> HttpResponse;
124}
125
126pub enum Body {
130 Json(Vec<u8>),
132 Text(String),
134 Bytes(Vec<u8>),
136}
137
138impl Body {
139 fn respond_with(self, status: StatusCode) -> HttpResponse {
143 match self {
144 Body::Json(bytes) => HttpResponse::new(status, bytes, "application/json"),
145 Body::Text(text) => HttpResponse::text(status, text),
146 Body::Bytes(bytes) => HttpResponse::bytes(status, bytes),
147 }
148 }
149}
150
151pub enum Response<T> {
153 Body(T),
155 WithStatus(StatusCode, T),
157 Raw(StatusCode, Body),
159 Empty,
161}
162
163impl Response<()> {
164 pub fn no_content() -> Self {
166 Response::Empty
167 }
168
169 pub fn text(status: StatusCode, value: impl Into<String>) -> Self {
171 Response::Raw(status, Body::Text(value.into()))
172 }
173
174 pub fn bytes(status: StatusCode, value: impl Into<Vec<u8>>) -> Self {
176 Response::Raw(status, Body::Bytes(value.into()))
177 }
178
179 pub fn json(status: StatusCode, value: impl serde::Serialize) -> Self {
181 let bytes = match serde_json::to_vec(&value) {
182 Ok(bytes) => bytes,
183 Err(_) => {
184 return Response::Raw(
185 StatusCode::INTERNAL_SERVER_ERROR,
186 Body::Json(json_serialization_error_body()),
187 );
188 }
189 };
190 Response::Raw(status, Body::Json(bytes))
191 }
192
193 pub fn stream(
196 content_type: &'static str,
197 stream: impl Stream<Item = Result<Bytes>> + Send + 'static,
198 ) -> HttpResponse {
199 HttpResponse {
200 status: StatusCode::OK,
201 body: ResponseBody::Streaming(Box::pin(stream)),
202 content_type,
203 headers: Vec::new(),
204 }
205 }
206
207 pub fn sse<T>(stream: impl Stream<Item = Result<T>> + Send + 'static) -> HttpResponse
214 where
215 T: serde::Serialize + 'static,
216 {
217 let framed = stream.map(|item| {
218 item.and_then(|value| {
219 let json = serde_json::to_string(&value).map_err(|err| {
220 InternalServerErrorException::new(anyhow::anyhow!(
221 "failed to serialize SSE event: {err}"
222 ))
223 })?;
224 Ok(Bytes::from(format!("data: {json}\n\n")))
225 })
226 });
227 Response::stream("text/event-stream", framed)
228 .with_header("Cache-Control", "no-cache")
229 .with_header("X-Accel-Buffering", "no")
230 }
231
232 pub async fn file(
236 path: impl AsRef<std::path::Path>,
237 content_type: &'static str,
238 ) -> Result<HttpResponse> {
239 let file = tokio::fs::File::open(path)
240 .await
241 .map_err(map_file_open_error)?;
242
243 let stream = ReaderStream::new(file).map(|chunk| {
244 chunk
245 .map(Bytes::from)
246 .map_err(|err| InternalServerErrorException::new(err))
247 });
248
249 Ok(Response::stream(content_type, stream))
250 }
251}
252
253pub(crate) fn map_file_open_error(err: std::io::Error) -> HttpException {
254 match err.kind() {
255 std::io::ErrorKind::NotFound => NotFoundException::new("file not found"),
256 std::io::ErrorKind::PermissionDenied => ForbiddenException::new("permission denied"),
257 _ => InternalServerErrorException::new(err),
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use std::io::{Error, ErrorKind};
265
266 #[test]
267 fn map_file_open_error_classifies_io_kinds() {
268 let not_found = map_file_open_error(Error::new(ErrorKind::NotFound, "gone"));
269 assert_eq!(not_found.status, StatusCode::NOT_FOUND);
270
271 let denied = map_file_open_error(Error::new(ErrorKind::PermissionDenied, "nope"));
272 assert_eq!(denied.status, StatusCode::FORBIDDEN);
273
274 let other = map_file_open_error(Error::new(ErrorKind::Other, "disk failed"));
275 assert_eq!(other.status, StatusCode::INTERNAL_SERVER_ERROR);
276 }
277
278 #[test]
279 fn with_header_accepts_dynamic_values() {
280 let filename = format!("report-{}.csv", 123);
281 let response = HttpResponse::text(StatusCode::OK, "a,b\n").with_header(
282 "Content-Disposition",
283 format!("attachment; filename=\"{filename}\""),
284 );
285
286 assert_eq!(
287 response.headers,
288 vec![(
289 "Content-Disposition".to_string(),
290 "attachment; filename=\"report-123.csv\"".to_string()
291 )]
292 );
293 }
294
295 #[test]
296 fn insert_header_mutates_in_place() {
297 let mut response = HttpResponse::text(StatusCode::OK, "ok");
298 response.insert_header("X-Request-Id", "abc-123");
299 assert_eq!(
300 response.headers,
301 vec![("X-Request-Id".to_string(), "abc-123".to_string())]
302 );
303 }
304}
305
306fn json_serialization_error_response() -> HttpResponse {
307 HttpResponse::new(
308 StatusCode::INTERNAL_SERVER_ERROR,
309 json_serialization_error_body(),
310 "application/json",
311 )
312}
313
314fn json_serialization_error_body() -> Vec<u8> {
315 br#"{"status":500,"error":"Internal Server Error","message":"Internal Server Error"}"#.to_vec()
316}
317
318impl IntoCaelixResponse for HttpResponse {
319 fn into_response(self) -> HttpResponse {
320 self
321 }
322}
323
324impl IntoCaelixResponse for String {
325 fn into_response(self) -> HttpResponse {
326 HttpResponse::text(StatusCode::OK, self)
327 }
328}
329
330impl IntoCaelixResponse for &'static str {
331 fn into_response(self) -> HttpResponse {
332 HttpResponse::text(StatusCode::OK, self)
333 }
334}
335
336impl IntoCaelixResponse for HttpException {
337 fn into_response(self) -> HttpResponse {
338 #[derive(serde::Serialize)]
339 struct ErrorBody {
340 status: u16,
341 error: &'static str,
342 message: String,
343 #[serde(skip_serializing_if = "Option::is_none")]
344 errors: Option<std::collections::BTreeMap<String, Vec<String>>>,
345 }
346
347 let (message, errors) = if self.status.is_server_error() {
348 ("Internal Server Error".to_string(), None)
349 } else {
350 (self.message, self.errors)
351 };
352
353 HttpResponse::json(
354 self.status,
355 ErrorBody {
356 status: self.status.as_u16(),
357 error: self.error,
358 message,
359 errors,
360 },
361 )
362 }
363}
364
365impl<T: serde::Serialize> IntoCaelixResponse for Response<T> {
366 fn into_response(self) -> HttpResponse {
367 match self {
368 Response::Body(value) => HttpResponse::json(StatusCode::OK, value),
369 Response::WithStatus(status, value) => HttpResponse::json(status, value),
370 Response::Raw(status, body) => body.respond_with(status),
371 Response::Empty => {
372 HttpResponse::new(StatusCode::NO_CONTENT, Vec::new(), "application/json")
373 }
374 }
375 }
376}