Skip to main content

ranvier_http/
response.rs

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/// HTML response wrapper.
29///
30/// Wraps a string body with `Content-Type: text/html; charset=utf-8`.
31///
32/// # Example
33///
34/// ```rust,ignore
35/// Outcome::next(Html("<h1>Hello</h1>".to_string()))
36/// ```
37#[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}