rama_http/service/web/endpoint/extract/body/
text.rs1use super::BytesRejection;
2use crate::Request;
3use crate::body::util::BodyExt;
4use crate::service::web::extract::{FromRequest, FromRequestBody};
5use crate::utils::macros::{composite_http_rejection, define_http_rejection};
6use rama_utils::macros::impl_deref;
7
8#[derive(Debug, Clone)]
10pub struct Text(pub String);
11
12impl_deref!(Text: String);
13
14define_http_rejection! {
15 #[status = UNSUPPORTED_MEDIA_TYPE]
16 #[body = "Text requests must have `Content-Type: text/plain`"]
17 pub struct InvalidTextContentType;
21}
22
23define_http_rejection! {
24 #[status = BAD_REQUEST]
25 #[body = "Failed to decode text payload"]
26 pub struct InvalidUtf8Text(Error);
29}
30
31composite_http_rejection! {
32 pub enum TextRejection {
37 InvalidTextContentType,
38 InvalidUtf8Text,
39 BytesRejection,
40 }
41}
42
43impl FromRequest for Text {
44 type Rejection = TextRejection;
45
46 async fn from_request(req: Request) -> Result<Self, Self::Rejection> {
47 let (parts, body) = req.into_parts();
48 let future = Self::from_request_body(&parts, body);
49 future.await
50 }
51}
52
53impl FromRequestBody for Text {
54 fn from_request_body(
55 parts: &crate::request::Parts,
56 body: crate::Body,
57 ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send + 'static {
58 let has_valid_content_type = crate::service::web::extract::has_any_content_type(
59 &parts.headers,
60 &[&crate::mime::TEXT_PLAIN],
61 );
62
63 async move {
64 if !has_valid_content_type {
65 return Err(InvalidTextContentType.into());
66 }
67
68 match body.collect().await {
69 Ok(c) => match String::from_utf8(c.to_bytes().to_vec()) {
70 Ok(s) => Ok(Self(s)),
71 Err(err) => Err(InvalidUtf8Text::from_err(err).into()),
72 },
73 Err(err) => Err(BytesRejection::from_err(err).into()),
74 }
75 }
76 }
77}
78
79#[cfg(test)]
80mod test {
81 use super::*;
82 use crate::service::web::WebService;
83 use crate::{Method, Request, StatusCode, header};
84 use rama_core::Service;
85
86 #[tokio::test]
87 async fn test_text() {
88 let service = WebService::default().with_post("/", async |Text(body): Text| {
89 assert_eq!(body, "test");
90 });
91
92 let req = Request::builder()
93 .method(Method::POST)
94 .header(header::CONTENT_TYPE, "text/plain")
95 .body("test".into())
96 .unwrap();
97 let resp = service.serve(req).await.unwrap();
98 assert_eq!(resp.status(), StatusCode::OK);
99 }
100
101 #[tokio::test]
102 async fn test_text_missing_content_type() {
103 let service = WebService::default().with_post("/", async |Text(_): Text| StatusCode::OK);
104
105 let req = Request::builder()
106 .method(Method::POST)
107 .body("test".into())
108 .unwrap();
109 let resp = service.serve(req).await.unwrap();
110 assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
111 }
112
113 #[tokio::test]
114 async fn test_text_incorrect_content_type() {
115 let service = WebService::default().with_post("/", async |Text(_): Text| StatusCode::OK);
116
117 let req = Request::builder()
118 .method(Method::POST)
119 .header(header::CONTENT_TYPE, "application/json")
120 .body("test".into())
121 .unwrap();
122 let resp = service.serve(req).await.unwrap();
123 assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
124 }
125
126 #[tokio::test]
127 async fn test_text_invalid_utf8() {
128 let service = WebService::default().with_post("/", async |Text(_): Text| StatusCode::OK);
129
130 let req = Request::builder()
131 .method(Method::POST)
132 .header(header::CONTENT_TYPE, "text/plain")
133 .body(vec![0, 159, 146, 150].into())
134 .unwrap();
135 let resp = service.serve(req).await.unwrap();
136 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
137 }
138}