Skip to main content

foxtive_ntex/http/extractors/
string_body.rs

1use crate::error::HttpError;
2use foxtive::prelude::{AppMessage, AppResult};
3use ntex::http::Payload;
4use ntex::util::BytesMut;
5use ntex::web::{FromRequest, HttpRequest};
6use tracing::debug;
7
8/// Extractor for reading the request body as a plain UTF-8 string.
9///
10/// # Example
11/// ```
12/// use foxtive_ntex::http::extractors::StringBody;
13///
14/// async fn handler(body: StringBody) -> String {
15///     format!("Received: {}", body.body())
16/// }
17/// ```
18pub struct StringBody {
19    body: String,
20}
21
22impl StringBody {
23    /// Returns a reference to the underlying string body.
24    pub fn body(&self) -> &String {
25        &self.body
26    }
27
28    /// Consumes the `StringBody`, returning the inner string.
29    pub fn into_body(self) -> String {
30        self.body
31    }
32
33    /// Returns the length of the string body in bytes.
34    pub fn len(&self) -> usize {
35        self.body.len()
36    }
37
38    /// Returns true if the string body is empty.
39    pub fn is_empty(&self) -> bool {
40        self.body.is_empty()
41    }
42
43    /// Tries to parse the body to a specific type that implements `FromStr`.
44    /// Returns an application-level result or an error if parsing fails.
45    pub fn parse<T: std::str::FromStr>(&self) -> AppResult<T>
46    where
47        <T as std::str::FromStr>::Err: ToString,
48    {
49        self.body.parse::<T>().map_err(|e| {
50            HttpError::AppMessage(AppMessage::WarningMessageString(e.to_string())).into_app_error()
51        })
52    }
53}
54
55impl From<String> for StringBody {
56    fn from(body: String) -> Self {
57        Self { body }
58    }
59}
60
61impl From<&str> for StringBody {
62    fn from(body: &str) -> Self {
63        Self {
64            body: body.to_owned(),
65        }
66    }
67}
68
69impl<Err> FromRequest<Err> for StringBody {
70    type Error = HttpError;
71
72    async fn from_request(_req: &HttpRequest, payload: &mut Payload) -> Result<Self, Self::Error> {
73        let mut bytes = BytesMut::new();
74        while let Some(chunk) = ntex::util::stream_recv(payload).await {
75            bytes.extend_from_slice(&chunk?);
76        }
77
78        let raw = String::from_utf8(bytes.to_vec())?;
79        debug!("[string-body] {raw}");
80        Ok(Self { body: raw })
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use ntex::http::StatusCode;
88    use ntex::web::WebResponseError;
89
90    #[test]
91    fn test_body_and_into_body() {
92        let data = "hello string body".to_string();
93        let sb = StringBody::from(data.clone());
94        assert_eq!(sb.body(), &data);
95
96        let sb = StringBody::from(&data[..]);
97        assert_eq!(sb.body(), &data);
98
99        let moved = sb.into_body();
100        assert_eq!(moved, data);
101    }
102
103    #[test]
104    fn test_len_and_is_empty() {
105        let empty = StringBody::from("");
106        assert!(empty.is_empty());
107        assert_eq!(empty.len(), 0);
108
109        let s = StringBody::from("abcde");
110        assert!(!s.is_empty());
111        assert_eq!(s.len(), 5);
112    }
113
114    #[test]
115    #[allow(clippy::approx_constant)]
116    fn test_parse_success() {
117        let s = StringBody::from("42");
118        let val: i32 = s.parse().unwrap();
119        assert_eq!(val, 42);
120
121        let s = StringBody::from("3.1415");
122        let val: f64 = s.parse().unwrap();
123        assert!((val - 3.1415).abs() < 1e-6);
124    }
125
126    #[test]
127    fn test_parse_failure() {
128        let s = StringBody::from("not_a_number");
129        let result: AppResult<i32> = s.parse();
130        assert!(result.is_err());
131        let err = result.unwrap_err().downcast::<HttpError>().unwrap();
132        assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
133        // Message should include 'invalid digit' for i32::FromStr
134        assert!(err.to_string().to_lowercase().contains("invalid"));
135    }
136
137    #[test]
138    fn test_deprecated_raw() {
139        let data = "raw string body".to_string();
140        let sb = StringBody::from(data.clone());
141        assert_eq!(sb.body(), &data);
142    }
143}