1use bytes::Bytes;
10use http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
11use rainier_support::{Error, Extensions};
12use serde::Serialize;
13
14use crate::body::Body;
15use crate::cookie::Cookie;
16
17#[derive(Debug)]
19pub struct Response {
20 status: StatusCode,
21 headers: HeaderMap,
22 body: Body,
23 extensions: Extensions,
24}
25
26impl Response {
27 pub fn new(status: StatusCode) -> Self {
29 Self { status, headers: HeaderMap::new(), body: Body::Empty, extensions: Extensions::new() }
30 }
31
32 pub fn ok(body: impl Into<Body>) -> Self {
34 Self::new(StatusCode::OK).with_body(body)
35 }
36
37 pub fn no_content() -> Self {
39 Self::new(StatusCode::NO_CONTENT)
40 }
41
42 pub fn text(body: impl Into<String>) -> Self {
44 Self::ok(body.into()).with_content_type("text/plain; charset=utf-8")
45 }
46
47 pub fn html(body: impl Into<String>) -> Self {
49 Self::ok(body.into()).with_content_type("text/html; charset=utf-8")
50 }
51
52 pub fn json(value: &impl Serialize) -> Self {
58 match serde_json::to_vec(value) {
59 Ok(encoded) => Self::ok(encoded).with_content_type("application/json; charset=utf-8"),
60 Err(e) => Error::internal(format!("could not serialise the response: {e}")).into(),
61 }
62 }
63
64 pub fn created(location: impl AsRef<str>) -> Self {
66 Self::new(StatusCode::CREATED).with_header("location", location.as_ref())
67 }
68
69 pub fn download(bytes: impl Into<Bytes>, file_name: &str) -> Self {
71 let safe: String =
74 file_name.chars().filter(|c| *c != '"' && *c != '\r' && *c != '\n').collect();
75 Self::ok(bytes.into())
76 .with_content_type("application/octet-stream")
77 .with_header("content-disposition", &format!("attachment; filename=\"{safe}\""))
78 }
79
80 pub fn stream<S>(stream: S) -> Self
82 where
83 S: futures_core::Stream<Item = Result<Bytes, Error>> + Send + 'static,
84 {
85 Self::new(StatusCode::OK).with_body(Body::from_stream(stream))
86 }
87
88 pub fn status(&self) -> StatusCode {
92 self.status
93 }
94
95 pub fn headers(&self) -> &HeaderMap {
97 &self.headers
98 }
99
100 pub fn headers_mut(&mut self) -> &mut HeaderMap {
102 &mut self.headers
103 }
104
105 pub fn header(&self, name: &str) -> Option<&str> {
107 self.headers.get(name)?.to_str().ok()
108 }
109
110 pub fn body(&self) -> &Body {
112 &self.body
113 }
114
115 pub fn take_body(&mut self) -> Body {
117 std::mem::take(&mut self.body)
118 }
119
120 pub fn extensions(&self) -> &Extensions {
122 &self.extensions
123 }
124
125 pub fn extensions_mut(&mut self) -> &mut Extensions {
127 &mut self.extensions
128 }
129
130 pub fn is_successful(&self) -> bool {
132 self.status.is_success()
133 }
134
135 pub fn with_status(mut self, status: StatusCode) -> Self {
139 self.status = status;
140 self
141 }
142
143 pub fn with_body(mut self, body: impl Into<Body>) -> Self {
145 self.body = body.into();
146 self
147 }
148
149 pub fn with_header(mut self, name: &str, value: &str) -> Self {
155 if let (Ok(name), Ok(value)) = (HeaderName::try_from(name), HeaderValue::from_str(value)) {
156 self.headers.insert(name, value);
157 }
158 self
159 }
160
161 pub fn with_added_header(mut self, name: &str, value: &str) -> Self {
163 if let (Ok(name), Ok(value)) = (HeaderName::try_from(name), HeaderValue::from_str(value)) {
164 self.headers.append(name, value);
165 }
166 self
167 }
168
169 pub fn with_content_type(self, content_type: &str) -> Self {
171 self.with_header("content-type", content_type)
172 }
173
174 pub fn with_cookie(self, cookie: &Cookie) -> Self {
176 self.with_added_header("set-cookie", &cookie.to_set_cookie())
177 }
178
179 pub fn without_cookie(self, name: &str) -> Self {
181 self.with_cookie(&Cookie::removal(name))
182 }
183
184 pub fn into_http(self) -> http::Response<Body> {
186 let mut builder = http::Response::builder().status(self.status);
187 if let Some(headers) = builder.headers_mut() {
188 *headers = self.headers;
189 }
190 builder.body(self.body).unwrap_or_else(|_| {
191 http::Response::new(Body::Empty)
194 })
195 }
196}
197
198impl Default for Response {
199 fn default() -> Self {
200 Self::new(StatusCode::OK)
201 }
202}
203
204#[derive(Debug, Clone)]
206pub struct Redirect {
207 status: StatusCode,
208 location: String,
209}
210
211impl Redirect {
212 pub fn to(location: impl Into<String>) -> Self {
214 Self { status: StatusCode::FOUND, location: location.into() }
215 }
216
217 pub fn permanent(location: impl Into<String>) -> Self {
219 Self { status: StatusCode::MOVED_PERMANENTLY, location: location.into() }
220 }
221
222 pub fn see_other(location: impl Into<String>) -> Self {
225 Self { status: StatusCode::SEE_OTHER, location: location.into() }
226 }
227
228 pub fn temporary(location: impl Into<String>) -> Self {
230 Self { status: StatusCode::TEMPORARY_REDIRECT, location: location.into() }
231 }
232}
233
234#[derive(Debug, Clone)]
236pub struct Html<T>(pub T);
237
238#[derive(Debug, Clone)]
240pub struct Json<T>(pub T);
241
242pub trait IntoResponse {
244 fn into_response(self) -> Response;
246}
247
248impl IntoResponse for Response {
249 fn into_response(self) -> Response {
250 self
251 }
252}
253
254impl IntoResponse for () {
255 fn into_response(self) -> Response {
256 Response::no_content()
257 }
258}
259
260impl IntoResponse for StatusCode {
261 fn into_response(self) -> Response {
262 Response::new(self)
263 }
264}
265
266impl IntoResponse for String {
267 fn into_response(self) -> Response {
268 Response::text(self)
269 }
270}
271
272impl IntoResponse for &'static str {
273 fn into_response(self) -> Response {
274 Response::text(self)
275 }
276}
277
278impl IntoResponse for serde_json::Value {
279 fn into_response(self) -> Response {
280 Response::json(&self)
281 }
282}
283
284impl<T: Serialize> IntoResponse for Json<T> {
285 fn into_response(self) -> Response {
286 Response::json(&self.0)
287 }
288}
289
290impl<T: Into<String>> IntoResponse for Html<T> {
291 fn into_response(self) -> Response {
292 Response::html(self.0)
293 }
294}
295
296impl IntoResponse for Redirect {
297 fn into_response(self) -> Response {
298 Response::new(self.status).with_header("location", &self.location)
299 }
300}
301
302impl IntoResponse for Body {
303 fn into_response(self) -> Response {
304 Response::ok(self)
305 }
306}
307
308impl<T: IntoResponse> IntoResponse for (StatusCode, T) {
309 fn into_response(self) -> Response {
310 let (status, inner) = self;
311 inner.into_response().with_status(status)
312 }
313}
314
315impl<T: IntoResponse> IntoResponse for Option<T> {
316 fn into_response(self) -> Response {
319 match self {
320 Some(inner) => inner.into_response(),
321 None => Error::not_found("Not Found").into_response(),
322 }
323 }
324}
325
326impl<T, E> IntoResponse for Result<T, E>
327where
328 T: IntoResponse,
329 E: IntoResponse,
330{
331 fn into_response(self) -> Response {
332 match self {
333 Ok(inner) => inner.into_response(),
334 Err(e) => e.into_response(),
335 }
336 }
337}
338
339#[derive(Debug, Clone)]
347pub struct RenderedError {
348 pub status: u16,
350 pub message: String,
352 pub details: Option<serde_json::Value>,
354 pub disclosable: bool,
360}
361
362impl IntoResponse for Error {
368 fn into_response(self) -> Response {
369 let status =
370 StatusCode::from_u16(self.status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
371 let disclosable = !self.kind().is_server_error();
372
373 let mut payload = serde_json::Map::new();
374 payload.insert("message".into(), serde_json::Value::String(self.message().to_string()));
375 if let Some(details) = self.details() {
376 payload.insert("errors".into(), details.clone());
377 }
378
379 let mut response = Response::json(&serde_json::Value::Object(payload)).with_status(status);
380
381 response.extensions_mut().insert(RenderedError {
382 status: self.status(),
383 message: self.message().to_string(),
384 details: self.details().cloned(),
385 disclosable,
386 });
387 response
388 }
389}
390
391impl From<Error> for Response {
392 fn from(error: Error) -> Self {
393 error.into_response()
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use serde_json::json;
401
402 async fn body_of(response: Response) -> String {
403 String::from_utf8(response.body.collect().await.unwrap().to_vec()).unwrap()
404 }
405
406 #[tokio::test]
407 async fn builds_the_common_shapes() {
408 let text = Response::text("hi");
409 assert_eq!(text.status(), StatusCode::OK);
410 assert_eq!(text.header("content-type"), Some("text/plain; charset=utf-8"));
411 assert_eq!(body_of(text).await, "hi");
412
413 let html = Response::html("<p>hi</p>");
414 assert_eq!(html.header("content-type"), Some("text/html; charset=utf-8"));
415
416 let json = Response::json(&json!({ "a": 1 }));
417 assert_eq!(json.header("content-type"), Some("application/json; charset=utf-8"));
418 assert_eq!(body_of(json).await, r#"{"a":1}"#);
419
420 assert_eq!(Response::no_content().status(), StatusCode::NO_CONTENT);
421 assert_eq!(Response::created("/posts/1").header("location"), Some("/posts/1"));
422 }
423
424 #[test]
425 fn headers_replace_or_append() {
426 let response = Response::ok(()).with_header("x-a", "1").with_header("x-a", "2");
427 assert_eq!(response.headers().get_all("x-a").iter().count(), 1);
428 assert_eq!(response.header("x-a"), Some("2"));
429
430 let response = Response::ok(()).with_added_header("x-b", "1").with_added_header("x-b", "2");
431 assert_eq!(response.headers().get_all("x-b").iter().count(), 2);
432 }
433
434 #[test]
435 fn an_invalid_header_is_dropped_rather_than_panicking() {
436 let response = Response::ok(()).with_header("bad header name", "v");
437 assert!(response.headers().is_empty());
438
439 let response = Response::ok(()).with_header("x-ok", "line\nbreak");
440 assert!(response.headers().is_empty());
441 }
442
443 #[test]
444 fn cookies_stack_up_as_separate_headers() {
445 let response = Response::ok(())
446 .with_cookie(&Cookie::new("a", "1"))
447 .with_cookie(&Cookie::new("b", "2"))
448 .without_cookie("c");
449
450 assert_eq!(response.headers().get_all("set-cookie").iter().count(), 3);
451 }
452
453 #[test]
454 fn downloads_escape_the_file_name() {
455 let response = Response::download("data", "re\"port\r\n.csv");
456 assert_eq!(
457 response.header("content-disposition"),
458 Some("attachment; filename=\"report.csv\"")
459 );
460 }
461
462 #[tokio::test]
463 async fn into_response_covers_the_handler_return_types() {
464 assert_eq!(().into_response().status(), StatusCode::NO_CONTENT);
465 assert_eq!("hi".into_response().status(), StatusCode::OK);
466 assert_eq!(String::from("hi").into_response().status(), StatusCode::OK);
467 assert_eq!(StatusCode::IM_A_TEAPOT.into_response().status(), StatusCode::IM_A_TEAPOT);
468
469 let tuple = (StatusCode::ACCEPTED, "queued").into_response();
470 assert_eq!(tuple.status(), StatusCode::ACCEPTED);
471 assert_eq!(body_of(tuple).await, "queued");
472
473 let json = Json(json!({ "ok": true })).into_response();
474 assert_eq!(body_of(json).await, r#"{"ok":true}"#);
475
476 let html = Html("<b>x</b>").into_response();
477 assert_eq!(html.header("content-type"), Some("text/html; charset=utf-8"));
478 }
479
480 #[test]
481 fn options_and_results_carry_their_failure() {
482 let none: Option<&'static str> = None;
483 assert_eq!(none.into_response().status(), StatusCode::NOT_FOUND);
484 assert_eq!(Some("x").into_response().status(), StatusCode::OK);
485
486 let ok: Result<&'static str, Error> = Ok("x");
487 assert_eq!(ok.into_response().status(), StatusCode::OK);
488
489 let err: Result<&'static str, Error> = Err(Error::not_found("no post"));
490 assert_eq!(err.into_response().status(), StatusCode::NOT_FOUND);
491 }
492
493 #[tokio::test]
494 async fn errors_render_as_json_with_their_details() {
495 let error = Error::validation(json!({ "email": ["is required"] }));
496 let response = error.into_response();
497
498 assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
499 let body: serde_json::Value = serde_json::from_str(&body_of(response).await).unwrap();
500 assert_eq!(body["message"], "The given data was invalid.");
501 assert_eq!(body["errors"]["email"][0], "is required");
502 }
503
504 #[test]
505 fn redirects_set_the_location_and_status() {
506 assert_eq!(Redirect::to("/a").into_response().status(), StatusCode::FOUND);
507 assert_eq!(
508 Redirect::permanent("/a").into_response().status(),
509 StatusCode::MOVED_PERMANENTLY
510 );
511 assert_eq!(Redirect::see_other("/a").into_response().status(), StatusCode::SEE_OTHER);
512 assert_eq!(
513 Redirect::temporary("/a").into_response().status(),
514 StatusCode::TEMPORARY_REDIRECT
515 );
516 assert_eq!(Redirect::to("/dash").into_response().header("location"), Some("/dash"));
517 }
518
519 #[tokio::test]
520 async fn converts_into_an_http_response() {
521 let response = Response::text("body").with_header("x-a", "1").into_http();
522 assert_eq!(response.status(), StatusCode::OK);
523 assert_eq!(response.headers().get("x-a").unwrap(), "1");
524 }
525}