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
177#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
192pub struct ProblemDetail {
193 #[serde(rename = "type")]
195 pub type_uri: String,
196 pub title: String,
198 pub status: u16,
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub detail: Option<String>,
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub instance: Option<String>,
206 #[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
208 pub extensions: std::collections::HashMap<String, serde_json::Value>,
209}
210
211impl ProblemDetail {
212 pub fn new(status: u16, title: impl Into<String>) -> Self {
214 Self {
215 type_uri: "about:blank".to_string(),
216 title: title.into(),
217 status,
218 detail: None,
219 instance: None,
220 extensions: std::collections::HashMap::new(),
221 }
222 }
223
224 pub fn with_type_uri(mut self, uri: impl Into<String>) -> Self {
226 self.type_uri = uri.into();
227 self
228 }
229
230 pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
232 self.detail = Some(detail.into());
233 self
234 }
235
236 pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
238 self.instance = Some(instance.into());
239 self
240 }
241
242 pub fn with_extension(mut self, key: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
244 self.extensions.insert(key.into(), value.into());
245 self
246 }
247}
248
249impl IntoResponse for ProblemDetail {
250 fn into_response(self) -> HttpResponse {
251 let status = StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
252 let body = serde_json::to_string(&self).unwrap_or_default();
253 Response::builder()
254 .status(status)
255 .header(CONTENT_TYPE, "application/problem+json")
256 .body(
257 Full::new(Bytes::from(body))
258 .map_err(|never| match never {})
259 .boxed(),
260 )
261 .expect("response builder should be infallible")
262 }
263}
264
265pub trait IntoProblemDetail {
270 fn into_problem_detail(&self) -> ProblemDetail;
271}
272
273pub fn outcome_to_problem_response<Out, E>(outcome: Outcome<Out, E>) -> HttpResponse
275where
276 Out: IntoResponse,
277 E: IntoProblemDetail,
278{
279 match outcome {
280 Outcome::Next(output) => output.into_response(),
281 Outcome::Fault(error) => error.into_problem_detail().into_response(),
282 _ => "OK".into_response(),
283 }
284}
285
286pub fn outcome_to_response<Out, E>(outcome: Outcome<Out, E>) -> HttpResponse
287where
288 Out: IntoResponse,
289 E: std::fmt::Debug,
290{
291 outcome_to_response_with_error(outcome, |error| {
292 (
293 StatusCode::INTERNAL_SERVER_ERROR,
294 format!("Error: {:?}", error),
295 )
296 .into_response()
297 })
298}
299
300pub fn outcome_to_response_with_error<Out, E, F>(
301 outcome: Outcome<Out, E>,
302 on_fault: F,
303) -> HttpResponse
304where
305 Out: IntoResponse,
306 F: FnOnce(&E) -> HttpResponse,
307{
308 match outcome {
309 Outcome::Next(output) => output.into_response(),
310 Outcome::Fault(error) => on_fault(&error),
311 _ => "OK".into_response(),
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use ranvier_core::Outcome;
319
320 #[test]
321 fn string_into_response_sets_200_and_text_body() {
322 let response = "hello".to_string().into_response();
323 assert_eq!(response.status(), StatusCode::OK);
324 }
325
326 #[test]
327 fn tuple_into_response_preserves_status_code() {
328 let response = (StatusCode::CREATED, "created").into_response();
329 assert_eq!(response.status(), StatusCode::CREATED);
330 }
331
332 #[test]
333 fn outcome_fault_maps_to_internal_server_error() {
334 let response = outcome_to_response::<String, &str>(Outcome::Fault("boom"));
335 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
336 }
337
338 #[test]
339 fn json_error_response_sets_json_content_type() {
340 let response = json_error_response(StatusCode::UNAUTHORIZED, "forbidden");
341 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
342 assert_eq!(
343 response
344 .headers()
345 .get(CONTENT_TYPE)
346 .and_then(|value| value.to_str().ok()),
347 Some("application/json")
348 );
349 }
350
351 #[test]
352 fn problem_detail_new_sets_defaults() {
353 let pd = ProblemDetail::new(404, "Not Found");
354 assert_eq!(pd.status, 404);
355 assert_eq!(pd.title, "Not Found");
356 assert_eq!(pd.type_uri, "about:blank");
357 assert!(pd.detail.is_none());
358 assert!(pd.instance.is_none());
359 assert!(pd.extensions.is_empty());
360 }
361
362 #[test]
363 fn problem_detail_builder_methods() {
364 let pd = ProblemDetail::new(400, "Bad Request")
365 .with_type_uri("https://ranvier.studio/errors/validation")
366 .with_detail("2 validation errors")
367 .with_instance("/api/todos")
368 .with_extension("trace_id", "abc123");
369 assert_eq!(pd.type_uri, "https://ranvier.studio/errors/validation");
370 assert_eq!(pd.detail.as_deref(), Some("2 validation errors"));
371 assert_eq!(pd.instance.as_deref(), Some("/api/todos"));
372 assert_eq!(pd.extensions.get("trace_id").unwrap(), "abc123");
373 }
374
375 #[test]
376 fn problem_detail_into_response_sets_problem_json_content_type() {
377 let pd = ProblemDetail::new(404, "Not Found");
378 let response = pd.into_response();
379 assert_eq!(response.status(), StatusCode::NOT_FOUND);
380 assert_eq!(
381 response
382 .headers()
383 .get(CONTENT_TYPE)
384 .and_then(|v| v.to_str().ok()),
385 Some("application/problem+json")
386 );
387 }
388
389 #[test]
390 fn problem_detail_serialization_roundtrip() {
391 let pd = ProblemDetail::new(500, "Internal Server Error")
392 .with_detail("Something went wrong")
393 .with_extension("transition", "GetUser");
394 let json = serde_json::to_string(&pd).unwrap();
395 let pd2: ProblemDetail = serde_json::from_str(&json).unwrap();
396 assert_eq!(pd2.status, 500);
397 assert_eq!(pd2.title, "Internal Server Error");
398 assert_eq!(pd2.detail.as_deref(), Some("Something went wrong"));
399 }
400
401 #[test]
402 fn outcome_to_problem_response_maps_fault_to_rfc7807() {
403 struct MyError;
404 impl IntoProblemDetail for MyError {
405 fn into_problem_detail(&self) -> ProblemDetail {
406 ProblemDetail::new(422, "Unprocessable Entity")
407 }
408 }
409 let outcome: Outcome<String, MyError> = Outcome::Fault(MyError);
410 let response = outcome_to_problem_response(outcome);
411 assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
412 }
413}