1use bytes::Bytes;
2use http::header::CONTENT_TYPE;
3use http::{Response, StatusCode};
4use http_body_util::combinators::BoxBody;
5use http_body_util::{BodyExt, Full};
6use ranvier_core::Outcome;
7use std::convert::Infallible;
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(
21 Full::new(Bytes::from(payload.to_string()))
22 .map_err(|never| match never {})
23 .boxed(),
24 )
25 .expect("response builder should be infallible")
26}
27
28#[derive(Debug, Clone)]
38pub struct Html(pub String);
39
40impl IntoResponse for Html {
41 fn into_response(self) -> HttpResponse {
42 Response::builder()
43 .status(StatusCode::OK)
44 .header(CONTENT_TYPE, "text/html; charset=utf-8")
45 .body(
46 Full::new(Bytes::from(self.0))
47 .map_err(|never| match never {})
48 .boxed(),
49 )
50 .expect("response builder should be infallible")
51 }
52}
53
54impl IntoResponse for (StatusCode, Html) {
55 fn into_response(self) -> HttpResponse {
56 Response::builder()
57 .status(self.0)
58 .header(CONTENT_TYPE, "text/html; charset=utf-8")
59 .body(
60 Full::new(Bytes::from((self.1).0))
61 .map_err(|never| match never {})
62 .boxed(),
63 )
64 .expect("response builder should be infallible")
65 }
66}
67
68impl IntoResponse for HttpResponse {
69 fn into_response(self) -> HttpResponse {
70 self
71 }
72}
73
74impl IntoResponse for String {
75 fn into_response(self) -> HttpResponse {
76 Response::builder()
77 .status(StatusCode::OK)
78 .header(CONTENT_TYPE, "text/plain; charset=utf-8")
79 .body(
80 Full::new(Bytes::from(self))
81 .map_err(|never| match never {})
82 .boxed(),
83 )
84 .expect("response builder should be infallible")
85 }
86}
87
88impl IntoResponse for &'static str {
89 fn into_response(self) -> HttpResponse {
90 Response::builder()
91 .status(StatusCode::OK)
92 .header(CONTENT_TYPE, "text/plain; charset=utf-8")
93 .body(
94 Full::new(Bytes::from(self))
95 .map_err(|never| match never {})
96 .boxed(),
97 )
98 .expect("response builder should be infallible")
99 }
100}
101
102impl IntoResponse for Bytes {
103 fn into_response(self) -> HttpResponse {
104 Response::builder()
105 .status(StatusCode::OK)
106 .header(CONTENT_TYPE, "application/octet-stream")
107 .body(Full::new(self).map_err(|never| match never {}).boxed())
108 .expect("response builder should be infallible")
109 }
110}
111
112impl IntoResponse for serde_json::Value {
113 fn into_response(self) -> HttpResponse {
114 Response::builder()
115 .status(StatusCode::OK)
116 .header(CONTENT_TYPE, "application/json")
117 .body(
118 Full::new(Bytes::from(self.to_string()))
119 .map_err(|never| match never {})
120 .boxed(),
121 )
122 .expect("response builder should be infallible")
123 }
124}
125
126impl IntoResponse for () {
127 fn into_response(self) -> HttpResponse {
128 Response::builder()
129 .status(StatusCode::NO_CONTENT)
130 .body(
131 Full::new(Bytes::new())
132 .map_err(|never| match never {})
133 .boxed(),
134 )
135 .expect("response builder should be infallible")
136 }
137}
138
139impl IntoResponse for (StatusCode, String) {
140 fn into_response(self) -> HttpResponse {
141 Response::builder()
142 .status(self.0)
143 .header(CONTENT_TYPE, "text/plain; charset=utf-8")
144 .body(
145 Full::new(Bytes::from(self.1))
146 .map_err(|never| match never {})
147 .boxed(),
148 )
149 .expect("response builder should be infallible")
150 }
151}
152
153impl IntoResponse for (StatusCode, &'static str) {
154 fn into_response(self) -> HttpResponse {
155 Response::builder()
156 .status(self.0)
157 .header(CONTENT_TYPE, "text/plain; charset=utf-8")
158 .body(
159 Full::new(Bytes::from(self.1))
160 .map_err(|never| match never {})
161 .boxed(),
162 )
163 .expect("response builder should be infallible")
164 }
165}
166
167impl IntoResponse for (StatusCode, Bytes) {
168 fn into_response(self) -> HttpResponse {
169 Response::builder()
170 .status(self.0)
171 .header(CONTENT_TYPE, "application/octet-stream")
172 .body(Full::new(self.1).map_err(|never| match never {}).boxed())
173 .expect("response builder should be infallible")
174 }
175}
176
177pub fn outcome_to_response<Out, E>(outcome: Outcome<Out, E>) -> HttpResponse
178where
179 Out: IntoResponse,
180 E: std::fmt::Debug,
181{
182 outcome_to_response_with_error(outcome, |error| {
183 (
184 StatusCode::INTERNAL_SERVER_ERROR,
185 format!("Error: {:?}", error),
186 )
187 .into_response()
188 })
189}
190
191pub fn outcome_to_response_with_error<Out, E, F>(
192 outcome: Outcome<Out, E>,
193 on_fault: F,
194) -> HttpResponse
195where
196 Out: IntoResponse,
197 F: FnOnce(&E) -> HttpResponse,
198{
199 match outcome {
200 Outcome::Next(output) => output.into_response(),
201 Outcome::Fault(error) => on_fault(&error),
202 _ => "OK".into_response(),
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use ranvier_core::Outcome;
210
211 #[test]
212 fn string_into_response_sets_200_and_text_body() {
213 let response = "hello".to_string().into_response();
214 assert_eq!(response.status(), StatusCode::OK);
215 }
216
217 #[test]
218 fn tuple_into_response_preserves_status_code() {
219 let response = (StatusCode::CREATED, "created").into_response();
220 assert_eq!(response.status(), StatusCode::CREATED);
221 }
222
223 #[test]
224 fn outcome_fault_maps_to_internal_server_error() {
225 let response = outcome_to_response::<String, &str>(Outcome::Fault("boom"));
226 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
227 }
228
229 #[test]
230 fn json_error_response_sets_json_content_type() {
231 let response = json_error_response(StatusCode::UNAUTHORIZED, "forbidden");
232 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
233 assert_eq!(
234 response
235 .headers()
236 .get(CONTENT_TYPE)
237 .and_then(|value| value.to_str().ok()),
238 Some("application/json")
239 );
240 }
241}