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(feature = "askama")]
351pub struct TemplateResponse<T: askama::Template>(pub T);
352
353#[cfg(feature = "askama")]
354impl<T: askama::Template> IntoResponse for TemplateResponse<T> {
355 fn into_response(self) -> HttpResponse {
356 match self.0.render() {
357 Ok(html) => Response::builder()
358 .status(StatusCode::OK)
359 .header(CONTENT_TYPE, "text/html; charset=utf-8")
360 .body(
361 Full::new(Bytes::from(html))
362 .map_err(|never| match never {})
363 .boxed(),
364 )
365 .expect("valid HTTP response construction"),
366 Err(e) => json_error_response(
367 StatusCode::INTERNAL_SERVER_ERROR,
368 &format!("Template render error: {}", e),
369 ),
370 }
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use ranvier_core::Outcome;
378
379 #[test]
380 fn string_into_response_sets_200_and_text_body() {
381 let response = "hello".to_string().into_response();
382 assert_eq!(response.status(), StatusCode::OK);
383 }
384
385 #[test]
386 fn tuple_into_response_preserves_status_code() {
387 let response = (StatusCode::CREATED, "created").into_response();
388 assert_eq!(response.status(), StatusCode::CREATED);
389 }
390
391 #[test]
392 fn outcome_fault_maps_to_internal_server_error() {
393 let response = outcome_to_response::<String, &str>(Outcome::Fault("boom"));
394 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
395 }
396
397 #[test]
398 fn json_error_response_sets_json_content_type() {
399 let response = json_error_response(StatusCode::UNAUTHORIZED, "forbidden");
400 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
401 assert_eq!(
402 response
403 .headers()
404 .get(CONTENT_TYPE)
405 .and_then(|value| value.to_str().ok()),
406 Some("application/json")
407 );
408 }
409
410 #[test]
411 fn problem_detail_new_sets_defaults() {
412 let pd = ProblemDetail::new(404, "Not Found");
413 assert_eq!(pd.status, 404);
414 assert_eq!(pd.title, "Not Found");
415 assert_eq!(pd.type_uri, "about:blank");
416 assert!(pd.detail.is_none());
417 assert!(pd.instance.is_none());
418 assert!(pd.extensions.is_empty());
419 }
420
421 #[test]
422 fn problem_detail_builder_methods() {
423 let pd = ProblemDetail::new(400, "Bad Request")
424 .with_type_uri("https://ranvier.studio/errors/validation")
425 .with_detail("2 validation errors")
426 .with_instance("/api/todos")
427 .with_extension("trace_id", "abc123");
428 assert_eq!(pd.type_uri, "https://ranvier.studio/errors/validation");
429 assert_eq!(pd.detail.as_deref(), Some("2 validation errors"));
430 assert_eq!(pd.instance.as_deref(), Some("/api/todos"));
431 assert_eq!(pd.extensions.get("trace_id").unwrap(), "abc123");
432 }
433
434 #[test]
435 fn problem_detail_into_response_sets_problem_json_content_type() {
436 let pd = ProblemDetail::new(404, "Not Found");
437 let response = pd.into_response();
438 assert_eq!(response.status(), StatusCode::NOT_FOUND);
439 assert_eq!(
440 response
441 .headers()
442 .get(CONTENT_TYPE)
443 .and_then(|v| v.to_str().ok()),
444 Some("application/problem+json")
445 );
446 }
447
448 #[test]
449 fn problem_detail_serialization_roundtrip() {
450 let pd = ProblemDetail::new(500, "Internal Server Error")
451 .with_detail("Something went wrong")
452 .with_extension("transition", "GetUser");
453 let json = serde_json::to_string(&pd).unwrap();
454 let pd2: ProblemDetail = serde_json::from_str(&json).unwrap();
455 assert_eq!(pd2.status, 500);
456 assert_eq!(pd2.title, "Internal Server Error");
457 assert_eq!(pd2.detail.as_deref(), Some("Something went wrong"));
458 }
459
460 #[test]
461 fn outcome_to_problem_response_maps_fault_to_rfc7807() {
462 struct MyError;
463 impl IntoProblemDetail for MyError {
464 fn into_problem_detail(&self) -> ProblemDetail {
465 ProblemDetail::new(422, "Unprocessable Entity")
466 }
467 }
468 let outcome: Outcome<String, MyError> = Outcome::Fault(MyError);
469 let response = outcome_to_problem_response(outcome);
470 assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
471 }
472}