1use crate::error::{ApiError, ErrorResponse};
74use bytes::Bytes;
75use http::{header, HeaderMap, HeaderValue, StatusCode};
76use http_body_util::Full;
77use rustapi_openapi::{MediaType, Operation, ResponseModifier, ResponseSpec, Schema, SchemaRef};
78use serde::Serialize;
79use std::collections::HashMap;
80
81pub type Response = http::Response<Full<Bytes>>;
83
84pub trait IntoResponse {
86 fn into_response(self) -> Response;
88}
89
90impl IntoResponse for Response {
92 fn into_response(self) -> Response {
93 self
94 }
95}
96
97impl IntoResponse for () {
99 fn into_response(self) -> Response {
100 http::Response::builder()
101 .status(StatusCode::OK)
102 .body(Full::new(Bytes::new()))
103 .unwrap()
104 }
105}
106
107impl IntoResponse for &'static str {
109 fn into_response(self) -> Response {
110 http::Response::builder()
111 .status(StatusCode::OK)
112 .header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
113 .body(Full::new(Bytes::from(self)))
114 .unwrap()
115 }
116}
117
118impl IntoResponse for String {
120 fn into_response(self) -> Response {
121 http::Response::builder()
122 .status(StatusCode::OK)
123 .header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
124 .body(Full::new(Bytes::from(self)))
125 .unwrap()
126 }
127}
128
129impl IntoResponse for StatusCode {
131 fn into_response(self) -> Response {
132 http::Response::builder()
133 .status(self)
134 .body(Full::new(Bytes::new()))
135 .unwrap()
136 }
137}
138
139impl<R: IntoResponse> IntoResponse for (StatusCode, R) {
141 fn into_response(self) -> Response {
142 let mut response = self.1.into_response();
143 *response.status_mut() = self.0;
144 response
145 }
146}
147
148impl<R: IntoResponse> IntoResponse for (StatusCode, HeaderMap, R) {
150 fn into_response(self) -> Response {
151 let mut response = self.2.into_response();
152 *response.status_mut() = self.0;
153 response.headers_mut().extend(self.1);
154 response
155 }
156}
157
158impl<T: IntoResponse, E: IntoResponse> IntoResponse for Result<T, E> {
160 fn into_response(self) -> Response {
161 match self {
162 Ok(v) => v.into_response(),
163 Err(e) => e.into_response(),
164 }
165 }
166}
167
168impl IntoResponse for ApiError {
171 fn into_response(self) -> Response {
172 let status = self.status;
173 let error_response = ErrorResponse::from(self);
175 let body = serde_json::to_vec(&error_response).unwrap_or_else(|_| {
176 br#"{"error":{"type":"internal_error","message":"Failed to serialize error"}}"#.to_vec()
177 });
178
179 http::Response::builder()
180 .status(status)
181 .header(header::CONTENT_TYPE, "application/json")
182 .body(Full::new(Bytes::from(body)))
183 .unwrap()
184 }
185}
186
187impl ResponseModifier for ApiError {
188 fn update_response(op: &mut Operation) {
189 op.responses.insert(
192 "400".to_string(),
193 ResponseSpec {
194 description: "Bad Request".to_string(),
195 content: {
196 let mut map = HashMap::new();
197 map.insert(
198 "application/json".to_string(),
199 MediaType {
200 schema: SchemaRef::Ref {
201 reference: "#/components/schemas/ErrorSchema".to_string(),
202 },
203 },
204 );
205 Some(map)
206 },
207 },
208 );
209
210 op.responses.insert(
212 "500".to_string(),
213 ResponseSpec {
214 description: "Internal Server Error".to_string(),
215 content: {
216 let mut map = HashMap::new();
217 map.insert(
218 "application/json".to_string(),
219 MediaType {
220 schema: SchemaRef::Ref {
221 reference: "#/components/schemas/ErrorSchema".to_string(),
222 },
223 },
224 );
225 Some(map)
226 },
227 },
228 );
229 }
230}
231
232#[derive(Debug, Clone)]
245pub struct Created<T>(pub T);
246
247impl<T: Serialize> IntoResponse for Created<T> {
248 fn into_response(self) -> Response {
249 match serde_json::to_vec(&self.0) {
250 Ok(body) => http::Response::builder()
251 .status(StatusCode::CREATED)
252 .header(header::CONTENT_TYPE, "application/json")
253 .body(Full::new(Bytes::from(body)))
254 .unwrap(),
255 Err(err) => {
256 ApiError::internal(format!("Failed to serialize response: {}", err)).into_response()
257 }
258 }
259 }
260}
261
262impl<T: for<'a> Schema<'a>> ResponseModifier for Created<T> {
263 fn update_response(op: &mut Operation) {
264 let (name, _) = T::schema();
265
266 let schema_ref = SchemaRef::Ref {
267 reference: format!("#/components/schemas/{}", name),
268 };
269
270 op.responses.insert(
271 "201".to_string(),
272 ResponseSpec {
273 description: "Created".to_string(),
274 content: {
275 let mut map = HashMap::new();
276 map.insert(
277 "application/json".to_string(),
278 MediaType { schema: schema_ref },
279 );
280 Some(map)
281 },
282 },
283 );
284 }
285}
286
287#[derive(Debug, Clone, Copy)]
300pub struct NoContent;
301
302impl IntoResponse for NoContent {
303 fn into_response(self) -> Response {
304 http::Response::builder()
305 .status(StatusCode::NO_CONTENT)
306 .body(Full::new(Bytes::new()))
307 .unwrap()
308 }
309}
310
311impl ResponseModifier for NoContent {
312 fn update_response(op: &mut Operation) {
313 op.responses.insert(
314 "204".to_string(),
315 ResponseSpec {
316 description: "No Content".to_string(),
317 content: None,
318 },
319 );
320 }
321}
322
323#[derive(Debug, Clone)]
325pub struct Html<T>(pub T);
326
327impl<T: Into<String>> IntoResponse for Html<T> {
328 fn into_response(self) -> Response {
329 http::Response::builder()
330 .status(StatusCode::OK)
331 .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
332 .body(Full::new(Bytes::from(self.0.into())))
333 .unwrap()
334 }
335}
336
337impl<T> ResponseModifier for Html<T> {
338 fn update_response(op: &mut Operation) {
339 op.responses.insert(
340 "200".to_string(),
341 ResponseSpec {
342 description: "HTML Content".to_string(),
343 content: {
344 let mut map = HashMap::new();
345 map.insert(
346 "text/html".to_string(),
347 MediaType {
348 schema: SchemaRef::Inline(serde_json::json!({ "type": "string" })),
349 },
350 );
351 Some(map)
352 },
353 },
354 );
355 }
356}
357
358#[derive(Debug, Clone)]
360pub struct Redirect {
361 status: StatusCode,
362 location: HeaderValue,
363}
364
365impl Redirect {
366 pub fn to(uri: &str) -> Self {
368 Self {
369 status: StatusCode::FOUND,
370 location: HeaderValue::from_str(uri).expect("Invalid redirect URI"),
371 }
372 }
373
374 pub fn permanent(uri: &str) -> Self {
376 Self {
377 status: StatusCode::MOVED_PERMANENTLY,
378 location: HeaderValue::from_str(uri).expect("Invalid redirect URI"),
379 }
380 }
381
382 pub fn temporary(uri: &str) -> Self {
384 Self {
385 status: StatusCode::TEMPORARY_REDIRECT,
386 location: HeaderValue::from_str(uri).expect("Invalid redirect URI"),
387 }
388 }
389}
390
391impl IntoResponse for Redirect {
392 fn into_response(self) -> Response {
393 http::Response::builder()
394 .status(self.status)
395 .header(header::LOCATION, self.location)
396 .body(Full::new(Bytes::new()))
397 .unwrap()
398 }
399}
400
401impl ResponseModifier for Redirect {
402 fn update_response(op: &mut Operation) {
403 op.responses.insert(
406 "3xx".to_string(),
407 ResponseSpec {
408 description: "Redirection".to_string(),
409 content: None,
410 },
411 );
412 }
413}
414
415#[derive(Debug, Clone)]
433pub struct WithStatus<T, const CODE: u16>(pub T);
434
435impl<T: IntoResponse, const CODE: u16> IntoResponse for WithStatus<T, CODE> {
436 fn into_response(self) -> Response {
437 let mut response = self.0.into_response();
438 if let Ok(status) = StatusCode::from_u16(CODE) {
440 *response.status_mut() = status;
441 }
442 response
443 }
444}
445
446impl<T: for<'a> Schema<'a>, const CODE: u16> ResponseModifier for WithStatus<T, CODE> {
447 fn update_response(op: &mut Operation) {
448 let (name, _) = T::schema();
449
450 let schema_ref = SchemaRef::Ref {
451 reference: format!("#/components/schemas/{}", name),
452 };
453
454 op.responses.insert(
455 CODE.to_string(),
456 ResponseSpec {
457 description: format!("Response with status {}", CODE),
458 content: {
459 let mut map = HashMap::new();
460 map.insert(
461 "application/json".to_string(),
462 MediaType { schema: schema_ref },
463 );
464 Some(map)
465 },
466 },
467 );
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474 use http_body_util::BodyExt;
475 use proptest::prelude::*;
476
477 async fn body_to_bytes(body: Full<Bytes>) -> Bytes {
479 body.collect().await.unwrap().to_bytes()
480 }
481
482 proptest! {
489 #![proptest_config(ProptestConfig::with_cases(100))]
490
491 #[test]
492 fn prop_with_status_response_correctness(
493 body in "[a-zA-Z0-9 ]{0,100}",
494 ) {
495 let rt = tokio::runtime::Runtime::new().unwrap();
496 rt.block_on(async {
497 let response_200: Response = WithStatus::<_, 200>(body.clone()).into_response();
503 prop_assert_eq!(response_200.status().as_u16(), 200);
504
505 let response_201: Response = WithStatus::<_, 201>(body.clone()).into_response();
507 prop_assert_eq!(response_201.status().as_u16(), 201);
508
509 let response_202: Response = WithStatus::<_, 202>(body.clone()).into_response();
511 prop_assert_eq!(response_202.status().as_u16(), 202);
512
513 let response_204: Response = WithStatus::<_, 204>(body.clone()).into_response();
515 prop_assert_eq!(response_204.status().as_u16(), 204);
516
517 let response_400: Response = WithStatus::<_, 400>(body.clone()).into_response();
519 prop_assert_eq!(response_400.status().as_u16(), 400);
520
521 let response_404: Response = WithStatus::<_, 404>(body.clone()).into_response();
523 prop_assert_eq!(response_404.status().as_u16(), 404);
524
525 let response_418: Response = WithStatus::<_, 418>(body.clone()).into_response();
527 prop_assert_eq!(response_418.status().as_u16(), 418);
528
529 let response_500: Response = WithStatus::<_, 500>(body.clone()).into_response();
531 prop_assert_eq!(response_500.status().as_u16(), 500);
532
533 let response_503: Response = WithStatus::<_, 503>(body.clone()).into_response();
535 prop_assert_eq!(response_503.status().as_u16(), 503);
536
537 let response_for_body: Response = WithStatus::<_, 200>(body.clone()).into_response();
539 let body_bytes = body_to_bytes(response_for_body.into_body()).await;
540 let body_str = String::from_utf8_lossy(&body_bytes);
541 prop_assert_eq!(body_str.as_ref(), body.as_str());
542
543 Ok(())
544 })?;
545 }
546 }
547
548 #[tokio::test]
549 async fn test_with_status_preserves_content_type() {
550 let response: Response = WithStatus::<_, 202>("hello world").into_response();
552
553 assert_eq!(response.status().as_u16(), 202);
554 assert_eq!(
555 response.headers().get(header::CONTENT_TYPE).unwrap(),
556 "text/plain; charset=utf-8"
557 );
558 }
559
560 #[tokio::test]
561 async fn test_with_status_with_empty_body() {
562 let response: Response = WithStatus::<_, 204>(()).into_response();
563
564 assert_eq!(response.status().as_u16(), 204);
565 let body_bytes = body_to_bytes(response.into_body()).await;
567 assert!(body_bytes.is_empty());
568 }
569
570 #[test]
571 fn test_with_status_common_codes() {
572 assert_eq!(
574 WithStatus::<_, 100>("").into_response().status().as_u16(),
575 100
576 ); assert_eq!(
578 WithStatus::<_, 200>("").into_response().status().as_u16(),
579 200
580 ); assert_eq!(
582 WithStatus::<_, 201>("").into_response().status().as_u16(),
583 201
584 ); assert_eq!(
586 WithStatus::<_, 202>("").into_response().status().as_u16(),
587 202
588 ); assert_eq!(
590 WithStatus::<_, 204>("").into_response().status().as_u16(),
591 204
592 ); assert_eq!(
594 WithStatus::<_, 301>("").into_response().status().as_u16(),
595 301
596 ); assert_eq!(
598 WithStatus::<_, 302>("").into_response().status().as_u16(),
599 302
600 ); assert_eq!(
602 WithStatus::<_, 400>("").into_response().status().as_u16(),
603 400
604 ); assert_eq!(
606 WithStatus::<_, 401>("").into_response().status().as_u16(),
607 401
608 ); assert_eq!(
610 WithStatus::<_, 403>("").into_response().status().as_u16(),
611 403
612 ); assert_eq!(
614 WithStatus::<_, 404>("").into_response().status().as_u16(),
615 404
616 ); assert_eq!(
618 WithStatus::<_, 500>("").into_response().status().as_u16(),
619 500
620 ); assert_eq!(
622 WithStatus::<_, 502>("").into_response().status().as_u16(),
623 502
624 ); assert_eq!(
626 WithStatus::<_, 503>("").into_response().status().as_u16(),
627 503
628 ); }
630}