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::Cookie;
10use crate::exception::{
11 ForbiddenException, HttpException, InternalServerErrorException, NotFoundException,
12};
13use crate::result::Result;
14
15pub type BoxBodyStream = Pin<Box<dyn Stream<Item = Result<Bytes>> + Send>>;
17
18pub enum ResponseBody {
20 Buffered(Vec<u8>),
22 Streaming(BoxBodyStream),
24}
25
26impl ResponseBody {
27 pub fn as_buffered(&self) -> Option<&[u8]> {
29 match self {
30 ResponseBody::Buffered(bytes) => Some(bytes.as_slice()),
31 ResponseBody::Streaming(_) => None,
32 }
33 }
34
35 pub fn as_buffered_mut(&mut self) -> Option<&mut Vec<u8>> {
37 match self {
38 ResponseBody::Buffered(bytes) => Some(bytes),
39 ResponseBody::Streaming(_) => None,
40 }
41 }
42
43 pub fn is_streaming(&self) -> bool {
45 matches!(self, ResponseBody::Streaming(_))
46 }
47
48 pub fn is_empty(&self) -> bool {
50 match self {
51 ResponseBody::Buffered(bytes) => bytes.is_empty(),
52 ResponseBody::Streaming(_) => false,
53 }
54 }
55}
56
57pub struct HttpResponse {
59 pub status: StatusCode,
61 pub body: ResponseBody,
63 pub content_type: &'static str,
65 pub headers: Vec<(String, String)>,
71 pub cookies: Vec<Cookie>,
73}
74
75impl HttpResponse {
76 pub fn new(status: StatusCode, body: Vec<u8>, content_type: &'static str) -> Self {
78 Self {
79 status,
80 body: ResponseBody::Buffered(body),
81 content_type,
82 headers: Vec::new(),
83 cookies: Vec::new(),
84 }
85 }
86
87 pub fn json(status: StatusCode, body: impl serde::Serialize) -> Self {
90 match serde_json::to_vec(&body) {
91 Ok(body) => Self::new(status, body, "application/json"),
92 Err(_) => json_serialization_error_response(),
93 }
94 }
95
96 pub fn text(status: StatusCode, body: impl Into<String>) -> Self {
98 Self::new(status, body.into().into_bytes(), "text/plain")
99 }
100
101 pub fn bytes(status: StatusCode, body: impl Into<Vec<u8>>) -> Self {
103 Self::new(status, body.into(), "application/octet-stream")
104 }
105
106 pub fn body_bytes(&self) -> Option<&[u8]> {
108 self.body.as_buffered()
109 }
110
111 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
114 self.insert_header(name, value);
115 self
116 }
117
118 pub fn insert_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
120 self.headers.push((name.into(), value.into()));
121 }
122
123 pub fn with_cookie(mut self, cookie: Cookie) -> Self {
125 self.insert_cookie(cookie);
126 self
127 }
128
129 pub fn insert_cookie(&mut self, cookie: Cookie) {
131 self.cookies.push(cookie);
132 }
133}
134
135pub trait IntoCaelixResponse {
137 fn into_response(self) -> HttpResponse;
139}
140
141pub enum Body {
145 Json(Vec<u8>),
147 Text(String),
149 Bytes(Vec<u8>),
151}
152
153impl Body {
154 fn respond_with(self, status: StatusCode) -> HttpResponse {
158 match self {
159 Body::Json(bytes) => HttpResponse::new(status, bytes, "application/json"),
160 Body::Text(text) => HttpResponse::text(status, text),
161 Body::Bytes(bytes) => HttpResponse::bytes(status, bytes),
162 }
163 }
164}
165
166pub enum Response<T> {
168 Body(T),
170 WithStatus(StatusCode, T),
172 Raw(StatusCode, Body),
174 Empty,
176 WithCookies(Box<Response<T>>, Vec<Cookie>),
178}
179
180impl<T> Response<T> {
181 pub fn with_cookie(self, cookie: Cookie) -> Self {
183 match self {
184 Response::WithCookies(response, mut cookies) => {
185 cookies.push(cookie);
186 Response::WithCookies(response, cookies)
187 }
188 response => Response::WithCookies(Box::new(response), vec![cookie]),
189 }
190 }
191}
192
193impl Response<()> {
194 pub fn no_content() -> Self {
196 Response::Empty
197 }
198
199 pub fn text(status: StatusCode, value: impl Into<String>) -> Self {
201 Response::Raw(status, Body::Text(value.into()))
202 }
203
204 pub fn bytes(status: StatusCode, value: impl Into<Vec<u8>>) -> Self {
206 Response::Raw(status, Body::Bytes(value.into()))
207 }
208
209 pub fn json(status: StatusCode, value: impl serde::Serialize) -> Self {
211 let bytes = match serde_json::to_vec(&value) {
212 Ok(bytes) => bytes,
213 Err(_) => {
214 return Response::Raw(
215 StatusCode::INTERNAL_SERVER_ERROR,
216 Body::Json(json_serialization_error_body()),
217 );
218 }
219 };
220 Response::Raw(status, Body::Json(bytes))
221 }
222
223 pub fn stream(
226 content_type: &'static str,
227 stream: impl Stream<Item = Result<Bytes>> + Send + 'static,
228 ) -> HttpResponse {
229 HttpResponse {
230 status: StatusCode::OK,
231 body: ResponseBody::Streaming(Box::pin(stream)),
232 content_type,
233 headers: Vec::new(),
234 cookies: Vec::new(),
235 }
236 }
237
238 pub fn sse<T>(stream: impl Stream<Item = Result<T>> + Send + 'static) -> HttpResponse
245 where
246 T: serde::Serialize + 'static,
247 {
248 let framed = stream.map(|item| {
249 item.and_then(|value| {
250 let json = serde_json::to_string(&value).map_err(|err| {
251 InternalServerErrorException::new(anyhow::anyhow!(
252 "failed to serialize SSE event: {err}"
253 ))
254 })?;
255 Ok(Bytes::from(format!("data: {json}\n\n")))
256 })
257 });
258 Response::stream("text/event-stream", framed)
259 .with_header("Cache-Control", "no-cache")
260 .with_header("X-Accel-Buffering", "no")
261 }
262
263 pub async fn file(
267 path: impl AsRef<std::path::Path>,
268 content_type: &'static str,
269 ) -> Result<HttpResponse> {
270 let file = tokio::fs::File::open(path)
271 .await
272 .map_err(map_file_open_error)?;
273
274 let stream = ReaderStream::new(file).map(|chunk| {
275 chunk
276 .map(Bytes::from)
277 .map_err(|err| InternalServerErrorException::new(err))
278 });
279
280 Ok(Response::stream(content_type, stream))
281 }
282}
283
284pub(crate) fn map_file_open_error(err: std::io::Error) -> HttpException {
285 match err.kind() {
286 std::io::ErrorKind::NotFound => NotFoundException::new("file not found"),
287 std::io::ErrorKind::PermissionDenied => ForbiddenException::new("permission denied"),
288 _ => InternalServerErrorException::new(err),
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use std::io::{Error, ErrorKind};
296
297 #[test]
298 fn map_file_open_error_classifies_io_kinds() {
299 let not_found = map_file_open_error(Error::new(ErrorKind::NotFound, "gone"));
300 assert_eq!(not_found.status, StatusCode::NOT_FOUND);
301
302 let denied = map_file_open_error(Error::new(ErrorKind::PermissionDenied, "nope"));
303 assert_eq!(denied.status, StatusCode::FORBIDDEN);
304
305 let other = map_file_open_error(Error::new(ErrorKind::Other, "disk failed"));
306 assert_eq!(other.status, StatusCode::INTERNAL_SERVER_ERROR);
307 }
308
309 #[test]
310 fn with_header_accepts_dynamic_values() {
311 let filename = format!("report-{}.csv", 123);
312 let response = HttpResponse::text(StatusCode::OK, "a,b\n").with_header(
313 "Content-Disposition",
314 format!("attachment; filename=\"{filename}\""),
315 );
316
317 assert_eq!(
318 response.headers,
319 vec![(
320 "Content-Disposition".to_string(),
321 "attachment; filename=\"report-123.csv\"".to_string()
322 )]
323 );
324 }
325
326 #[test]
327 fn insert_header_mutates_in_place() {
328 let mut response = HttpResponse::text(StatusCode::OK, "ok");
329 response.insert_header("X-Request-Id", "abc-123");
330 assert_eq!(
331 response.headers,
332 vec![("X-Request-Id".to_string(), "abc-123".to_string())]
333 );
334 }
335
336 #[test]
337 fn response_cookie_calls_append_in_order() {
338 let response = Response::Body("ok")
339 .with_cookie(Cookie::new("session", "a b"))
340 .with_cookie(Cookie::new("preference", "dark"))
341 .into_response();
342 assert_eq!(response.cookies.len(), 2);
343 assert_eq!(response.cookies[0].name(), "session");
344 assert_eq!(response.cookies[1].name(), "preference");
345 assert!(
346 response.cookies[0]
347 .to_header_value()
348 .starts_with("session=a%20b")
349 );
350 }
351}
352
353fn json_serialization_error_response() -> HttpResponse {
354 HttpResponse::new(
355 StatusCode::INTERNAL_SERVER_ERROR,
356 json_serialization_error_body(),
357 "application/json",
358 )
359}
360
361fn json_serialization_error_body() -> Vec<u8> {
362 br#"{"status":500,"error":"Internal Server Error","message":"Internal Server Error"}"#.to_vec()
363}
364
365impl IntoCaelixResponse for HttpResponse {
366 fn into_response(self) -> HttpResponse {
367 self
368 }
369}
370
371impl IntoCaelixResponse for String {
372 fn into_response(self) -> HttpResponse {
373 HttpResponse::text(StatusCode::OK, self)
374 }
375}
376
377impl IntoCaelixResponse for &'static str {
378 fn into_response(self) -> HttpResponse {
379 HttpResponse::text(StatusCode::OK, self)
380 }
381}
382
383impl IntoCaelixResponse for HttpException {
384 fn into_response(self) -> HttpResponse {
385 #[derive(serde::Serialize)]
386 struct ErrorBody {
387 status: u16,
388 error: &'static str,
389 message: String,
390 #[serde(skip_serializing_if = "Option::is_none")]
391 errors: Option<std::collections::BTreeMap<String, Vec<String>>>,
392 }
393
394 let (message, errors) = if self.status.is_server_error() {
395 ("Internal Server Error".to_string(), None)
396 } else {
397 (self.message, self.errors)
398 };
399
400 HttpResponse::json(
401 self.status,
402 ErrorBody {
403 status: self.status.as_u16(),
404 error: self.error,
405 message,
406 errors,
407 },
408 )
409 }
410}
411
412impl<T: serde::Serialize> IntoCaelixResponse for Response<T> {
413 fn into_response(self) -> HttpResponse {
414 match self {
415 Response::Body(value) => HttpResponse::json(StatusCode::OK, value),
416 Response::WithStatus(status, value) => HttpResponse::json(status, value),
417 Response::Raw(status, body) => body.respond_with(status),
418 Response::Empty => {
419 HttpResponse::new(StatusCode::NO_CONTENT, Vec::new(), "application/json")
420 }
421 Response::WithCookies(response, cookies) => {
422 let mut response = response.into_response();
423 response.cookies.extend(cookies);
424 response
425 }
426 }
427 }
428}