use crate::error::HttpError;
use crate::{FOXTIVE_NTEX, FoxtiveNtexExt};
use foxtive::prelude::{AppMessage, AppResult};
use ntex::http::Payload;
use ntex::util::BytesMut;
use ntex::web::{FromRequest, HttpRequest};
use tracing::debug;
pub struct StringBody {
body: String,
}
impl StringBody {
pub fn body(&self) -> &String {
&self.body
}
pub fn into_body(self) -> String {
self.body
}
pub fn len(&self) -> usize {
self.body.len()
}
pub fn is_empty(&self) -> bool {
self.body.is_empty()
}
pub fn parse<T: std::str::FromStr>(&self) -> AppResult<T>
where
<T as std::str::FromStr>::Err: ToString,
{
self.body
.parse::<T>()
.map_err(|e| HttpError::AppMessage(AppMessage::invalid(e.to_string())).into_app_error())
}
}
impl From<String> for StringBody {
fn from(body: String) -> Self {
Self { body }
}
}
impl From<&str> for StringBody {
fn from(body: &str) -> Self {
Self {
body: body.to_owned(),
}
}
}
impl<Err> FromRequest<Err> for StringBody {
type Error = HttpError;
async fn from_request(_req: &HttpRequest, payload: &mut Payload) -> Result<Self, Self::Error> {
let max_size = FOXTIVE_NTEX.app().body_config.string_limit;
let mut bytes = BytesMut::new();
let mut total_size = 0;
while let Some(chunk) = ntex::util::stream_recv(payload).await {
let chunk = chunk?;
total_size += chunk.len();
if total_size > max_size {
return Err(HttpError::AppMessage(AppMessage::invalid(
"String body exceeded maximum size",
)));
}
bytes.extend_from_slice(&chunk);
}
let raw = String::from_utf8(bytes.to_vec())?;
debug!("[string-body] {raw}");
Ok(Self { body: raw })
}
}
#[cfg(test)]
mod tests {
use super::*;
use ntex::http::StatusCode;
use ntex::web::WebResponseError;
#[test]
fn test_body_and_into_body() {
let data = "hello string body".to_string();
let sb = StringBody::from(data.clone());
assert_eq!(sb.body(), &data);
let sb = StringBody::from(&data[..]);
assert_eq!(sb.body(), &data);
let moved = sb.into_body();
assert_eq!(moved, data);
}
#[test]
fn test_len_and_is_empty() {
let empty = StringBody::from("");
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
let s = StringBody::from("abcde");
assert!(!s.is_empty());
assert_eq!(s.len(), 5);
}
#[test]
#[allow(clippy::approx_constant)]
fn test_parse_success() {
let s = StringBody::from("42");
let val: i32 = s.parse().unwrap();
assert_eq!(val, 42);
let s = StringBody::from("3.1415");
let val: f64 = s.parse().unwrap();
assert!((val - 3.1415).abs() < 1e-6);
}
#[test]
fn test_parse_failure() {
let s = StringBody::from("not_a_number");
let result: AppResult<i32> = s.parse();
assert!(result.is_err());
let err = result.unwrap_err().downcast::<HttpError>().unwrap();
assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
assert!(err.to_string().to_lowercase().contains("invalid"));
}
#[test]
fn test_deprecated_raw() {
let data = "raw string body".to_string();
let sb = StringBody::from(data.clone());
assert_eq!(sb.body(), &data);
}
}