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
296where
297 Out: IntoResponse,
298 E: std::fmt::Debug,
299{
300 outcome_to_response_with_error(outcome, |error| {
301 if cfg!(debug_assertions) {
302 (
303 StatusCode::INTERNAL_SERVER_ERROR,
304 format!("Error: {:?}", error),
305 )
306 .into_response()
307 } else {
308 json_error_response(
309 StatusCode::INTERNAL_SERVER_ERROR,
310 "Internal server error",
311 )
312 }
313 })
314}
315
316pub fn outcome_to_response_with_error<Out, E, F>(
317 outcome: Outcome<Out, E>,
318 on_fault: F,
319) -> HttpResponse
320where
321 Out: IntoResponse,
322 F: FnOnce(&E) -> HttpResponse,
323{
324 match outcome {
325 Outcome::Next(output) => output.into_response(),
326 Outcome::Fault(error) => on_fault(&error),
327 _ => "OK".into_response(),
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use ranvier_core::Outcome;
335
336 #[test]
337 fn string_into_response_sets_200_and_text_body() {
338 let response = "hello".to_string().into_response();
339 assert_eq!(response.status(), StatusCode::OK);
340 }
341
342 #[test]
343 fn tuple_into_response_preserves_status_code() {
344 let response = (StatusCode::CREATED, "created").into_response();
345 assert_eq!(response.status(), StatusCode::CREATED);
346 }
347
348 #[test]
349 fn outcome_fault_maps_to_internal_server_error() {
350 let response = outcome_to_response::<String, &str>(Outcome::Fault("boom"));
351 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
352 }
353
354 #[test]
355 fn json_error_response_sets_json_content_type() {
356 let response = json_error_response(StatusCode::UNAUTHORIZED, "forbidden");
357 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
358 assert_eq!(
359 response
360 .headers()
361 .get(CONTENT_TYPE)
362 .and_then(|value| value.to_str().ok()),
363 Some("application/json")
364 );
365 }
366
367 #[test]
368 fn problem_detail_new_sets_defaults() {
369 let pd = ProblemDetail::new(404, "Not Found");
370 assert_eq!(pd.status, 404);
371 assert_eq!(pd.title, "Not Found");
372 assert_eq!(pd.type_uri, "about:blank");
373 assert!(pd.detail.is_none());
374 assert!(pd.instance.is_none());
375 assert!(pd.extensions.is_empty());
376 }
377
378 #[test]
379 fn problem_detail_builder_methods() {
380 let pd = ProblemDetail::new(400, "Bad Request")
381 .with_type_uri("https://ranvier.studio/errors/validation")
382 .with_detail("2 validation errors")
383 .with_instance("/api/todos")
384 .with_extension("trace_id", "abc123");
385 assert_eq!(pd.type_uri, "https://ranvier.studio/errors/validation");
386 assert_eq!(pd.detail.as_deref(), Some("2 validation errors"));
387 assert_eq!(pd.instance.as_deref(), Some("/api/todos"));
388 assert_eq!(pd.extensions.get("trace_id").unwrap(), "abc123");
389 }
390
391 #[test]
392 fn problem_detail_into_response_sets_problem_json_content_type() {
393 let pd = ProblemDetail::new(404, "Not Found");
394 let response = pd.into_response();
395 assert_eq!(response.status(), StatusCode::NOT_FOUND);
396 assert_eq!(
397 response
398 .headers()
399 .get(CONTENT_TYPE)
400 .and_then(|v| v.to_str().ok()),
401 Some("application/problem+json")
402 );
403 }
404
405 #[test]
406 fn problem_detail_serialization_roundtrip() {
407 let pd = ProblemDetail::new(500, "Internal Server Error")
408 .with_detail("Something went wrong")
409 .with_extension("transition", "GetUser");
410 let json = serde_json::to_string(&pd).unwrap();
411 let pd2: ProblemDetail = serde_json::from_str(&json).unwrap();
412 assert_eq!(pd2.status, 500);
413 assert_eq!(pd2.title, "Internal Server Error");
414 assert_eq!(pd2.detail.as_deref(), Some("Something went wrong"));
415 }
416
417 #[test]
418 fn outcome_to_problem_response_maps_fault_to_rfc7807() {
419 struct MyError;
420 impl IntoProblemDetail for MyError {
421 fn into_problem_detail(&self) -> ProblemDetail {
422 ProblemDetail::new(422, "Unprocessable Entity")
423 }
424 }
425 let outcome: Outcome<String, MyError> = Outcome::Fault(MyError);
426 let response = outcome_to_problem_response(outcome);
427 assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
428 }
429}