Skip to main content

ranvier_http/
response.rs

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