1use crate::error::{ApiError, ErrorResponse};
74use bytes::Bytes;
75use futures_util::StreamExt;
76use http::{header, HeaderMap, HeaderValue, StatusCode};
77use http_body_util::Full;
78use rustapi_openapi::schema::{RustApiSchema, SchemaCtx};
79use rustapi_openapi::{MediaType, Operation, ResponseModifier, ResponseSpec, SchemaRef};
80use serde::Serialize;
81use std::collections::BTreeMap;
82use std::pin::Pin;
83use std::task::{Context, Poll};
84
85pub enum Body {
87 Full(Full<Bytes>),
89 Streaming(Pin<Box<dyn http_body::Body<Data = Bytes, Error = ApiError> + Send + 'static>>),
91}
92
93impl Body {
94 pub fn new(bytes: Bytes) -> Self {
96 Self::Full(Full::new(bytes))
97 }
98
99 pub fn empty() -> Self {
101 Self::Full(Full::new(Bytes::new()))
102 }
103
104 pub fn from_stream<S, E>(stream: S) -> Self
106 where
107 S: futures_util::Stream<Item = Result<Bytes, E>> + Send + 'static,
108 E: Into<ApiError> + 'static,
109 {
110 let body = http_body_util::StreamBody::new(
111 stream.map(|res| res.map_err(|e| e.into()).map(http_body::Frame::data)),
112 );
113 Self::Streaming(Box::pin(body))
114 }
115}
116
117impl Default for Body {
118 fn default() -> Self {
119 Self::empty()
120 }
121}
122
123impl http_body::Body for Body {
124 type Data = Bytes;
125 type Error = ApiError;
126
127 fn poll_frame(
128 self: Pin<&mut Self>,
129 cx: &mut Context<'_>,
130 ) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
131 match self.get_mut() {
132 Body::Full(b) => Pin::new(b)
133 .poll_frame(cx)
134 .map_err(|_| ApiError::internal("Infallible error")),
135 Body::Streaming(b) => b.as_mut().poll_frame(cx),
136 }
137 }
138
139 fn is_end_stream(&self) -> bool {
140 match self {
141 Body::Full(b) => b.is_end_stream(),
142 Body::Streaming(b) => b.is_end_stream(),
143 }
144 }
145
146 fn size_hint(&self) -> http_body::SizeHint {
147 match self {
148 Body::Full(b) => b.size_hint(),
149 Body::Streaming(b) => b.size_hint(),
150 }
151 }
152}
153
154impl From<Bytes> for Body {
155 fn from(bytes: Bytes) -> Self {
156 Self::new(bytes)
157 }
158}
159
160impl From<String> for Body {
161 fn from(s: String) -> Self {
162 Self::new(Bytes::from(s))
163 }
164}
165
166impl From<&'static str> for Body {
167 fn from(s: &'static str) -> Self {
168 Self::new(Bytes::from(s))
169 }
170}
171
172impl From<Vec<u8>> for Body {
173 fn from(v: Vec<u8>) -> Self {
174 Self::new(Bytes::from(v))
175 }
176}
177
178pub type Response = http::Response<Body>;
180
181pub trait IntoResponse {
183 fn into_response(self) -> Response;
185}
186
187impl IntoResponse for Response {
189 fn into_response(self) -> Response {
190 self
191 }
192}
193
194impl IntoResponse for () {
196 fn into_response(self) -> Response {
197 http::Response::builder()
198 .status(StatusCode::OK)
199 .body(Body::empty())
200 .unwrap()
201 }
202}
203
204impl IntoResponse for &'static str {
206 fn into_response(self) -> Response {
207 http::Response::builder()
208 .status(StatusCode::OK)
209 .header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
210 .body(Body::from(self))
211 .unwrap()
212 }
213}
214
215impl IntoResponse for String {
217 fn into_response(self) -> Response {
218 http::Response::builder()
219 .status(StatusCode::OK)
220 .header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
221 .body(Body::from(self))
222 .unwrap()
223 }
224}
225
226impl IntoResponse for StatusCode {
228 fn into_response(self) -> Response {
229 http::Response::builder()
230 .status(self)
231 .body(Body::empty())
232 .unwrap()
233 }
234}
235
236impl<R: IntoResponse> IntoResponse for (StatusCode, R) {
238 fn into_response(self) -> Response {
239 let mut response = self.1.into_response();
240 *response.status_mut() = self.0;
241 response
242 }
243}
244
245impl<R: IntoResponse> IntoResponse for (StatusCode, HeaderMap, R) {
247 fn into_response(self) -> Response {
248 let mut response = self.2.into_response();
249 *response.status_mut() = self.0;
250 response.headers_mut().extend(self.1);
251 response
252 }
253}
254
255impl<T: IntoResponse, E: IntoResponse> IntoResponse for Result<T, E> {
257 fn into_response(self) -> Response {
258 match self {
259 Ok(v) => v.into_response(),
260 Err(e) => e.into_response(),
261 }
262 }
263}
264
265impl IntoResponse for ApiError {
268 fn into_response(self) -> Response {
269 let status = self.status;
270 let error_response = ErrorResponse::from(self);
272 let body = serde_json::to_vec(&error_response).unwrap_or_else(|_| {
273 br#"{"error":{"type":"internal_error","message":"Failed to serialize error"}}"#.to_vec()
274 });
275
276 http::Response::builder()
277 .status(status)
278 .header(header::CONTENT_TYPE, "application/json")
279 .body(Body::from(body))
280 .unwrap()
281 }
282}
283
284impl ResponseModifier for ApiError {
285 fn update_response(op: &mut Operation) {
286 op.responses.insert(
289 "400".to_string(),
290 ResponseSpec {
291 description: "Bad Request".to_string(),
292 content: {
293 let mut map = BTreeMap::new();
294 map.insert(
295 "application/json".to_string(),
296 MediaType {
297 schema: Some(SchemaRef::Ref {
298 reference: "#/components/schemas/ErrorSchema".to_string(),
299 }),
300 example: None,
301 },
302 );
303 map
304 },
305 headers: BTreeMap::new(),
306 },
307 );
308
309 op.responses.insert(
311 "500".to_string(),
312 ResponseSpec {
313 description: "Internal Server Error".to_string(),
314 content: {
315 let mut map = BTreeMap::new();
316 map.insert(
317 "application/json".to_string(),
318 MediaType {
319 schema: Some(SchemaRef::Ref {
320 reference: "#/components/schemas/ErrorSchema".to_string(),
321 }),
322 example: None,
323 },
324 );
325 map
326 },
327 headers: BTreeMap::new(),
328 },
329 );
330 }
331
332 fn register_components(spec: &mut rustapi_openapi::OpenApiSpec) {
333 spec.register_in_place::<rustapi_openapi::ErrorSchema>();
334 spec.register_in_place::<rustapi_openapi::ErrorBodySchema>();
335 spec.register_in_place::<rustapi_openapi::ValidationErrorSchema>();
336 spec.register_in_place::<rustapi_openapi::ValidationErrorBodySchema>();
337 spec.register_in_place::<rustapi_openapi::FieldErrorSchema>();
338 }
339}
340
341#[derive(Debug, Clone)]
354pub struct Created<T>(pub T);
355
356impl<T: Serialize> IntoResponse for Created<T> {
357 fn into_response(self) -> Response {
358 match serde_json::to_vec(&self.0) {
359 Ok(body) => http::Response::builder()
360 .status(StatusCode::CREATED)
361 .header(header::CONTENT_TYPE, "application/json")
362 .body(Body::from(body))
363 .unwrap(),
364 Err(err) => {
365 ApiError::internal(format!("Failed to serialize response: {}", err)).into_response()
366 }
367 }
368 }
369}
370
371impl<T: RustApiSchema> ResponseModifier for Created<T> {
372 fn update_response(op: &mut Operation) {
373 let mut ctx = SchemaCtx::new();
374 let schema_ref = T::schema(&mut ctx);
375
376 op.responses.insert(
377 "201".to_string(),
378 ResponseSpec {
379 description: "Created".to_string(),
380 content: {
381 let mut map = BTreeMap::new();
382 map.insert(
383 "application/json".to_string(),
384 MediaType {
385 schema: Some(schema_ref),
386 example: None,
387 },
388 );
389 map
390 },
391 headers: BTreeMap::new(),
392 },
393 );
394 }
395
396 fn register_components(spec: &mut rustapi_openapi::OpenApiSpec) {
397 spec.register_in_place::<T>();
398 }
399}
400
401#[derive(Debug, Clone, Copy)]
414pub struct NoContent;
415
416impl IntoResponse for NoContent {
417 fn into_response(self) -> Response {
418 http::Response::builder()
419 .status(StatusCode::NO_CONTENT)
420 .body(Body::empty())
421 .unwrap()
422 }
423}
424
425impl ResponseModifier for NoContent {
426 fn update_response(op: &mut Operation) {
427 op.responses.insert(
428 "204".to_string(),
429 ResponseSpec {
430 description: "No Content".to_string(),
431 content: BTreeMap::new(),
432 headers: BTreeMap::new(),
433 },
434 );
435 }
436}
437
438#[derive(Debug, Clone)]
440pub struct Html<T>(pub T);
441
442impl<T: Into<String>> IntoResponse for Html<T> {
443 fn into_response(self) -> Response {
444 http::Response::builder()
445 .status(StatusCode::OK)
446 .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
447 .body(Body::from(self.0.into()))
448 .unwrap()
449 }
450}
451
452impl<T> ResponseModifier for Html<T> {
453 fn update_response(op: &mut Operation) {
454 op.responses.insert(
455 "200".to_string(),
456 ResponseSpec {
457 description: "HTML Content".to_string(),
458 content: {
459 let mut map = BTreeMap::new();
460 map.insert(
461 "text/html".to_string(),
462 MediaType {
463 schema: Some(SchemaRef::Inline(
464 serde_json::json!({ "type": "string" }),
465 )),
466 example: None,
467 },
468 );
469 map
470 },
471 headers: BTreeMap::new(),
472 },
473 );
474 }
475}
476
477#[derive(Debug, Clone)]
479pub struct Redirect {
480 status: StatusCode,
481 location: HeaderValue,
482}
483
484impl Redirect {
485 pub fn to(uri: &str) -> Self {
487 Self {
488 status: StatusCode::FOUND,
489 location: HeaderValue::from_str(uri).expect("Invalid redirect URI"),
490 }
491 }
492
493 pub fn permanent(uri: &str) -> Self {
495 Self {
496 status: StatusCode::MOVED_PERMANENTLY,
497 location: HeaderValue::from_str(uri).expect("Invalid redirect URI"),
498 }
499 }
500
501 pub fn temporary(uri: &str) -> Self {
503 Self {
504 status: StatusCode::TEMPORARY_REDIRECT,
505 location: HeaderValue::from_str(uri).expect("Invalid redirect URI"),
506 }
507 }
508}
509
510impl IntoResponse for Redirect {
511 fn into_response(self) -> Response {
512 http::Response::builder()
513 .status(self.status)
514 .header(header::LOCATION, self.location)
515 .body(Body::empty())
516 .unwrap()
517 }
518}
519
520impl ResponseModifier for Redirect {
521 fn update_response(op: &mut Operation) {
522 op.responses.insert(
525 "3xx".to_string(),
526 ResponseSpec {
527 description: "Redirection".to_string(),
528 content: BTreeMap::new(),
529 headers: BTreeMap::new(),
530 },
531 );
532 }
533}
534
535#[derive(Debug, Clone)]
553pub struct WithStatus<T, const CODE: u16>(pub T);
554
555impl<T: IntoResponse, const CODE: u16> IntoResponse for WithStatus<T, CODE> {
556 fn into_response(self) -> Response {
557 let mut response = self.0.into_response();
558 if let Ok(status) = StatusCode::from_u16(CODE) {
560 *response.status_mut() = status;
561 }
562 response
563 }
564}
565
566impl<T: RustApiSchema, const CODE: u16> ResponseModifier for WithStatus<T, CODE> {
567 fn update_response(op: &mut Operation) {
568 let mut ctx = SchemaCtx::new();
569 let schema_ref = T::schema(&mut ctx);
570
571 op.responses.insert(
572 CODE.to_string(),
573 ResponseSpec {
574 description: format!("Response with status {}", CODE),
575 content: {
576 let mut map = BTreeMap::new();
577 map.insert(
578 "application/json".to_string(),
579 MediaType {
580 schema: Some(schema_ref),
581 example: None,
582 },
583 );
584 map
585 },
586 headers: BTreeMap::new(),
587 },
588 );
589 }
590
591 fn register_components(spec: &mut rustapi_openapi::OpenApiSpec) {
592 spec.register_in_place::<T>();
593 }
594}
595
596#[cfg(test)]
597mod tests {
598 use super::*;
599 use proptest::prelude::*;
600
601 async fn body_to_bytes(body: Body) -> Bytes {
603 use http_body_util::BodyExt;
604 body.collect().await.unwrap().to_bytes()
605 }
606
607 proptest! {
614 #![proptest_config(ProptestConfig::with_cases(100))]
615
616 #[test]
617 fn prop_with_status_response_correctness(
618 body in "[a-zA-Z0-9 ]{0,100}",
619 ) {
620 let rt = tokio::runtime::Runtime::new().unwrap();
621 rt.block_on(async {
622 let response_200: Response = WithStatus::<_, 200>(body.clone()).into_response();
628 prop_assert_eq!(response_200.status().as_u16(), 200);
629
630 let response_201: Response = WithStatus::<_, 201>(body.clone()).into_response();
632 prop_assert_eq!(response_201.status().as_u16(), 201);
633
634 let response_202: Response = WithStatus::<_, 202>(body.clone()).into_response();
636 prop_assert_eq!(response_202.status().as_u16(), 202);
637
638 let response_204: Response = WithStatus::<_, 204>(body.clone()).into_response();
640 prop_assert_eq!(response_204.status().as_u16(), 204);
641
642 let response_400: Response = WithStatus::<_, 400>(body.clone()).into_response();
644 prop_assert_eq!(response_400.status().as_u16(), 400);
645
646 let response_404: Response = WithStatus::<_, 404>(body.clone()).into_response();
648 prop_assert_eq!(response_404.status().as_u16(), 404);
649
650 let response_418: Response = WithStatus::<_, 418>(body.clone()).into_response();
652 prop_assert_eq!(response_418.status().as_u16(), 418);
653
654 let response_500: Response = WithStatus::<_, 500>(body.clone()).into_response();
656 prop_assert_eq!(response_500.status().as_u16(), 500);
657
658 let response_503: Response = WithStatus::<_, 503>(body.clone()).into_response();
660 prop_assert_eq!(response_503.status().as_u16(), 503);
661
662 let response_for_body: Response = WithStatus::<_, 200>(body.clone()).into_response();
664 let body_bytes = body_to_bytes(response_for_body.into_body()).await;
665 let body_str = String::from_utf8_lossy(&body_bytes);
666 prop_assert_eq!(body_str.as_ref(), body.as_str());
667
668 Ok(())
669 })?;
670 }
671 }
672
673 #[tokio::test]
674 async fn test_with_status_preserves_content_type() {
675 let response: Response = WithStatus::<_, 202>("hello world").into_response();
677
678 assert_eq!(response.status().as_u16(), 202);
679 assert_eq!(
680 response.headers().get(header::CONTENT_TYPE).unwrap(),
681 "text/plain; charset=utf-8"
682 );
683 }
684
685 #[tokio::test]
686 async fn test_with_status_with_empty_body() {
687 let response: Response = WithStatus::<_, 204>(()).into_response();
688
689 assert_eq!(response.status().as_u16(), 204);
690 let body_bytes = body_to_bytes(response.into_body()).await;
692 assert!(body_bytes.is_empty());
693 }
694
695 #[test]
696 fn test_with_status_common_codes() {
697 assert_eq!(
699 WithStatus::<_, 100>("").into_response().status().as_u16(),
700 100
701 ); assert_eq!(
703 WithStatus::<_, 200>("").into_response().status().as_u16(),
704 200
705 ); assert_eq!(
707 WithStatus::<_, 201>("").into_response().status().as_u16(),
708 201
709 ); assert_eq!(
711 WithStatus::<_, 202>("").into_response().status().as_u16(),
712 202
713 ); assert_eq!(
715 WithStatus::<_, 204>("").into_response().status().as_u16(),
716 204
717 ); assert_eq!(
719 WithStatus::<_, 301>("").into_response().status().as_u16(),
720 301
721 ); assert_eq!(
723 WithStatus::<_, 302>("").into_response().status().as_u16(),
724 302
725 ); assert_eq!(
727 WithStatus::<_, 400>("").into_response().status().as_u16(),
728 400
729 ); assert_eq!(
731 WithStatus::<_, 401>("").into_response().status().as_u16(),
732 401
733 ); assert_eq!(
735 WithStatus::<_, 403>("").into_response().status().as_u16(),
736 403
737 ); assert_eq!(
739 WithStatus::<_, 404>("").into_response().status().as_u16(),
740 404
741 ); assert_eq!(
743 WithStatus::<_, 500>("").into_response().status().as_u16(),
744 500
745 ); assert_eq!(
747 WithStatus::<_, 502>("").into_response().status().as_u16(),
748 502
749 ); assert_eq!(
751 WithStatus::<_, 503>("").into_response().status().as_u16(),
752 503
753 ); }
755}