Skip to main content

ranvier_http/
response.rs

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}