1use bytes::Bytes;
2use http::header::CONTENT_TYPE;
3use http::{Response, StatusCode};
4use http_body_util::{BodyExt, Full};
5use ranvier_core::Outcome;
6use std::convert::Infallible;
7use http_body_util::combinators::BoxBody;
8
9pub type HttpResponse = Response<BoxBody<Bytes, Infallible>>;
10
11pub trait IntoResponse {
12 fn into_response(self) -> HttpResponse;
13}
14
15pub fn json_error_response(status: StatusCode, message: impl Into<String>) -> HttpResponse {
16 let payload = serde_json::json!({ "error": message.into() });
17 Response::builder()
18 .status(status)
19 .header(CONTENT_TYPE, "application/json")
20 .body(Full::new(Bytes::from(payload.to_string())).map_err(|never| match never {}).boxed())
21 .expect("response builder should be infallible")
22}
23
24impl IntoResponse for HttpResponse {
25 fn into_response(self) -> HttpResponse {
26 self
27 }
28}
29
30impl IntoResponse for String {
31 fn into_response(self) -> HttpResponse {
32 Response::builder()
33 .status(StatusCode::OK)
34 .header(CONTENT_TYPE, "text/plain; charset=utf-8")
35 .body(Full::new(Bytes::from(self)).map_err(|never| match never {}).boxed())
36 .expect("response builder should be infallible")
37 }
38}
39
40impl IntoResponse for &'static str {
41 fn into_response(self) -> HttpResponse {
42 Response::builder()
43 .status(StatusCode::OK)
44 .header(CONTENT_TYPE, "text/plain; charset=utf-8")
45 .body(Full::new(Bytes::from(self)).map_err(|never| match never {}).boxed())
46 .expect("response builder should be infallible")
47 }
48}
49
50impl IntoResponse for Bytes {
51 fn into_response(self) -> HttpResponse {
52 Response::builder()
53 .status(StatusCode::OK)
54 .header(CONTENT_TYPE, "application/octet-stream")
55 .body(Full::new(self).map_err(|never| match never {}).boxed())
56 .expect("response builder should be infallible")
57 }
58}
59
60impl IntoResponse for serde_json::Value {
61 fn into_response(self) -> HttpResponse {
62 Response::builder()
63 .status(StatusCode::OK)
64 .header(CONTENT_TYPE, "application/json")
65 .body(Full::new(Bytes::from(self.to_string())).map_err(|never| match never {}).boxed())
66 .expect("response builder should be infallible")
67 }
68}
69
70impl IntoResponse for () {
71 fn into_response(self) -> HttpResponse {
72 Response::builder()
73 .status(StatusCode::NO_CONTENT)
74 .body(Full::new(Bytes::new()).map_err(|never| match never {}).boxed())
75 .expect("response builder should be infallible")
76 }
77}
78
79impl IntoResponse for (StatusCode, String) {
80 fn into_response(self) -> HttpResponse {
81 Response::builder()
82 .status(self.0)
83 .header(CONTENT_TYPE, "text/plain; charset=utf-8")
84 .body(Full::new(Bytes::from(self.1)).map_err(|never| match never {}).boxed())
85 .expect("response builder should be infallible")
86 }
87}
88
89impl IntoResponse for (StatusCode, &'static str) {
90 fn into_response(self) -> HttpResponse {
91 Response::builder()
92 .status(self.0)
93 .header(CONTENT_TYPE, "text/plain; charset=utf-8")
94 .body(Full::new(Bytes::from(self.1)).map_err(|never| match never {}).boxed())
95 .expect("response builder should be infallible")
96 }
97}
98
99impl IntoResponse for (StatusCode, Bytes) {
100 fn into_response(self) -> HttpResponse {
101 Response::builder()
102 .status(self.0)
103 .header(CONTENT_TYPE, "application/octet-stream")
104 .body(Full::new(self.1).map_err(|never| match never {}).boxed())
105 .expect("response builder should be infallible")
106 }
107}
108
109pub fn outcome_to_response<Out, E>(outcome: Outcome<Out, E>) -> HttpResponse
110where
111 Out: IntoResponse,
112 E: std::fmt::Debug,
113{
114 outcome_to_response_with_error(outcome, |error| {
115 (
116 StatusCode::INTERNAL_SERVER_ERROR,
117 format!("Error: {:?}", error),
118 )
119 .into_response()
120 })
121}
122
123pub fn outcome_to_response_with_error<Out, E, F>(
124 outcome: Outcome<Out, E>,
125 on_fault: F,
126) -> HttpResponse
127where
128 Out: IntoResponse,
129 F: FnOnce(&E) -> HttpResponse,
130{
131 match outcome {
132 Outcome::Next(output) => output.into_response(),
133 Outcome::Fault(error) => on_fault(&error),
134 _ => "OK".into_response(),
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use ranvier_core::Outcome;
142
143 #[test]
144 fn string_into_response_sets_200_and_text_body() {
145 let response = "hello".to_string().into_response();
146 assert_eq!(response.status(), StatusCode::OK);
147 }
148
149 #[test]
150 fn tuple_into_response_preserves_status_code() {
151 let response = (StatusCode::CREATED, "created").into_response();
152 assert_eq!(response.status(), StatusCode::CREATED);
153 }
154
155 #[test]
156 fn outcome_fault_maps_to_internal_server_error() {
157 let response = outcome_to_response::<String, &str>(Outcome::Fault("boom"));
158 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
159 }
160
161 #[test]
162 fn json_error_response_sets_json_content_type() {
163 let response = json_error_response(StatusCode::UNAUTHORIZED, "forbidden");
164 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
165 assert_eq!(
166 response
167 .headers()
168 .get(CONTENT_TYPE)
169 .and_then(|value| value.to_str().ok()),
170 Some("application/json")
171 );
172 }
173}