#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]
use crate::{
Error, HttpRequest,
from_request::{FromRequest, IntoHandlerError},
};
use serde::de::DeserializeOwned;
use std::{collections::BTreeMap, fmt, future::Ready};
#[derive(Debug)]
pub enum PathError {
EmptyPath,
InsufficientSegments {
found: usize,
expected: usize,
path: String,
},
DeserializationError {
source: String,
segments: Vec<String>,
target_type: &'static str,
},
InvalidSegment {
segment: String,
position: usize,
reason: String,
},
}
impl fmt::Display for PathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyPath => {
write!(f, "Path is empty or contains no extractable segments")
}
Self::InsufficientSegments {
found,
expected,
path,
} => {
write!(
f,
"Insufficient path segments: found {found}, expected {expected} in path '{path}'"
)
}
Self::DeserializationError {
source,
segments,
target_type,
} => {
write!(
f,
"Failed to deserialize path segments {segments:?} into type '{target_type}': {source}"
)
}
Self::InvalidSegment {
segment,
position,
reason,
} => {
write!(
f,
"Invalid path segment '{segment}' at position {position}: {reason}"
)
}
}
}
}
impl std::error::Error for PathError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
impl From<PathError> for Error {
fn from(err: PathError) -> Self {
Self::bad_request(err.to_string())
}
}
impl IntoHandlerError for PathError {
fn into_handler_error(self) -> Error {
self.into()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Path<T>(pub T);
impl<T> Path<T> {
#[must_use]
pub const fn new(value: T) -> Self {
Self(value)
}
#[must_use]
pub fn into_inner(self) -> T {
self.0
}
#[must_use]
pub const fn as_ref(&self) -> &T {
&self.0
}
}
impl<T> std::ops::Deref for Path<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> std::ops::DerefMut for Path<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
fn extract_path_segments(path: &str) -> Vec<String> {
path.split('/')
.filter(|s| !s.is_empty())
.map(|s| {
urlencoding::decode(s)
.unwrap_or_else(|_| s.into())
.into_owned()
})
.collect()
}
fn validate_segment(segment: &str, position: usize) -> Result<(), PathError> {
if segment.is_empty() {
return Err(PathError::InvalidSegment {
segment: segment.to_string(),
position,
reason: "segment is empty after URL decoding".to_string(),
});
}
if segment.contains('\0') {
return Err(PathError::InvalidSegment {
segment: segment.to_string(),
position,
reason: "segment contains null character".to_string(),
});
}
Ok(())
}
fn extract_single_param<T>(segments: &[String]) -> Result<T, PathError>
where
T: DeserializeOwned,
{
if segments.is_empty() {
return Err(PathError::EmptyPath);
}
let last_segment = segments.last().unwrap();
validate_segment(last_segment, segments.len() - 1)?;
let json_str = format!("\"{last_segment}\"");
serde_json::from_str::<T>(&json_str).map_or_else(
|_| {
match serde_json::from_str::<T>(last_segment) {
Ok(value) => Ok(value),
Err(err) => Err(PathError::DeserializationError {
source: err.to_string(),
segments: vec![last_segment.clone()],
target_type: std::any::type_name::<T>(),
}),
}
},
|value| Ok(value),
)
}
fn extract_tuple_params<T>(segments: &[String]) -> Result<T, PathError>
where
T: DeserializeOwned,
{
if segments.is_empty() {
return Err(PathError::EmptyPath);
}
for (i, segment) in segments.iter().enumerate() {
validate_segment(segment, i)?;
}
let json_array = format!(
"[{}]",
segments
.iter()
.map(|s| {
if s.parse::<f64>().is_ok() {
s.clone()
} else {
format!("\"{s}\"")
}
})
.collect::<Vec<_>>()
.join(",")
);
match serde_json::from_str::<T>(&json_array) {
Ok(value) => Ok(value),
Err(first_err) => {
for count in (1..=segments.len()).rev() {
let subset = &segments[segments.len() - count..];
let json_array = format!(
"[{}]",
subset
.iter()
.map(|s| {
if s.parse::<f64>().is_ok() {
s.clone()
} else {
format!("\"{s}\"")
}
})
.collect::<Vec<_>>()
.join(",")
);
if let Ok(value) = serde_json::from_str::<T>(&json_array) {
return Ok(value);
}
}
Err(PathError::DeserializationError {
source: first_err.to_string(),
segments: segments.to_vec(),
target_type: std::any::type_name::<T>(),
})
}
}
}
fn extract_struct_params<T>(segments: &[String]) -> Result<T, PathError>
where
T: DeserializeOwned,
{
if segments.is_empty() {
return Err(PathError::EmptyPath);
}
for (i, segment) in segments.iter().enumerate() {
validate_segment(segment, i)?;
}
let mut json_map = BTreeMap::new();
for (i, segment) in segments.iter().enumerate() {
let value = serde_json::Value::String(segment.clone());
json_map.insert(i.to_string(), value);
}
serde_json::from_value::<T>(serde_json::Value::Object(json_map.into_iter().collect()))
.map_or_else(
|_| {
let json_array = format!(
"[{}]",
segments
.iter()
.map(|s| {
if s.parse::<f64>().is_ok() {
s.clone()
} else {
format!("\"{s}\"")
}
})
.collect::<Vec<_>>()
.join(",")
);
match serde_json::from_str::<T>(&json_array) {
Ok(value) => Ok(value),
Err(err) => Err(PathError::DeserializationError {
source: err.to_string(),
segments: segments.to_vec(),
target_type: std::any::type_name::<T>(),
}),
}
},
|value| Ok(value),
)
}
impl<T> FromRequest for Path<T>
where
T: DeserializeOwned + Send + 'static,
{
type Error = PathError;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request_sync(req: &HttpRequest) -> Result<Self, Self::Error> {
let path = req.path();
let segments = extract_path_segments(path);
if segments.is_empty() {
return Err(PathError::EmptyPath);
}
let type_name = std::any::type_name::<T>();
let value = if type_name.starts_with('(') && type_name.ends_with(')') {
extract_tuple_params(&segments)?
} else if type_name.contains("::")
&& !type_name.starts_with("alloc::")
&& !type_name.starts_with("core::")
{
extract_struct_params(&segments)?
} else {
extract_single_param(&segments)?
};
Ok(Self(value))
}
fn from_request_async(req: HttpRequest) -> Self::Future {
std::future::ready(Self::from_request_sync(&req))
}
}
#[cfg(all(test, feature = "simulator"))]
mod tests {
use super::*;
use crate::HttpRequest;
use serde::Deserialize;
#[cfg(any(feature = "simulator", not(feature = "actix")))]
use crate::simulator::{SimulationRequest, SimulationStub};
fn create_test_request(path: &str) -> HttpRequest {
#[cfg(any(feature = "simulator", not(feature = "actix")))]
{
let sim_req = SimulationRequest::new(crate::Method::Get, path);
HttpRequest::new(SimulationStub::new(sim_req))
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
{
let _ = path;
HttpRequest::new(crate::EmptyRequest)
}
}
#[test]
fn test_single_string_parameter() {
let req = create_test_request("/users/john");
let result = Path::<String>::from_request_sync(&req);
assert!(result.is_ok());
assert_eq!(result.unwrap().0, "john");
}
#[test]
fn test_single_numeric_parameter() {
let req = create_test_request("/users/123");
let result = Path::<u32>::from_request_sync(&req);
assert!(result.is_ok());
assert_eq!(result.unwrap().0, 123);
}
#[test]
fn test_tuple_parameters() {
let req = create_test_request("/users/john/posts/456");
let result = Path::<(String, u32)>::from_request_sync(&req);
assert!(result.is_ok());
let (segment1, segment2) = result.unwrap().0;
assert_eq!(segment1, "posts");
assert_eq!(segment2, 456);
}
#[test]
fn test_triple_tuple_parameters() {
let req = create_test_request("/api/v1/users/john/posts/456");
let result = Path::<(String, String, u32)>::from_request_sync(&req);
assert!(result.is_ok());
let (a, b, c) = result.unwrap().0;
assert_eq!(a, "john");
assert_eq!(b, "posts");
assert_eq!(c, 456);
}
#[derive(Debug, Deserialize, PartialEq)]
struct UserParams {
username: String,
post_id: u32,
}
#[test]
fn test_struct_parameters() {
let req = create_test_request("/users/john/posts/456");
let result = Path::<UserParams>::from_request_sync(&req);
println!("Struct test result: {result:?}");
}
#[test]
fn test_empty_path() {
let req = create_test_request("/");
let result = Path::<String>::from_request_sync(&req);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), PathError::EmptyPath));
}
#[test]
fn test_url_encoded_segments() {
let req = create_test_request("/users/john%20doe");
let result = Path::<String>::from_request_sync(&req);
assert!(result.is_ok());
assert_eq!(result.unwrap().0, "john doe");
}
#[test]
fn test_invalid_numeric_conversion() {
let req = create_test_request("/users/not_a_number");
let result = Path::<u32>::from_request_sync(&req);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
PathError::DeserializationError { .. }
));
}
#[test]
fn test_path_error_display() {
let error = PathError::EmptyPath;
assert_eq!(
error.to_string(),
"Path is empty or contains no extractable segments"
);
let error = PathError::InsufficientSegments {
found: 1,
expected: 2,
path: "/users".to_string(),
};
assert_eq!(
error.to_string(),
"Insufficient path segments: found 1, expected 2 in path '/users'"
);
}
#[test]
fn test_path_wrapper_methods() {
let path = Path::new("test".to_string());
assert_eq!(path.as_ref(), "test");
assert_eq!(path.into_inner(), "test");
}
#[test]
fn test_path_deref() {
let path = Path::new("test".to_string());
assert_eq!(path.len(), 4); }
#[test]
fn test_validate_segment_with_null_character() {
let result = validate_segment("hello\0world", 0);
assert!(result.is_err());
match result.unwrap_err() {
PathError::InvalidSegment {
segment,
position,
reason,
} => {
assert_eq!(segment, "hello\0world");
assert_eq!(position, 0);
assert_eq!(reason, "segment contains null character");
}
_ => panic!("Expected InvalidSegment error"),
}
}
#[test]
fn test_validate_segment_empty() {
let result = validate_segment("", 2);
assert!(result.is_err());
match result.unwrap_err() {
PathError::InvalidSegment {
segment,
position,
reason,
} => {
assert!(segment.is_empty());
assert_eq!(position, 2);
assert_eq!(reason, "segment is empty after URL decoding");
}
_ => panic!("Expected InvalidSegment error"),
}
}
#[test]
fn test_validate_segment_valid() {
assert!(validate_segment("hello", 0).is_ok());
assert!(validate_segment("hello-world", 1).is_ok());
assert!(validate_segment("hello_world", 2).is_ok());
assert!(validate_segment("123", 3).is_ok());
assert!(validate_segment("hello world", 4).is_ok()); }
#[test]
fn test_path_error_display_invalid_segment() {
let error = PathError::InvalidSegment {
segment: "bad\0segment".to_string(),
position: 3,
reason: "segment contains null character".to_string(),
};
assert_eq!(
error.to_string(),
"Invalid path segment 'bad\0segment' at position 3: segment contains null character"
);
}
#[test]
fn test_path_error_display_deserialization_error() {
let error = PathError::DeserializationError {
source: "expected a number".to_string(),
segments: vec!["abc".to_string(), "def".to_string()],
target_type: "(u32, u32)",
};
let display = error.to_string();
assert!(display.contains("Failed to deserialize path segments"));
assert!(display.contains("[\"abc\", \"def\"]"));
assert!(display.contains("(u32, u32)"));
assert!(display.contains("expected a number"));
}
#[test]
fn test_path_deref_mut() {
let mut path = Path::new(vec![1, 2, 3]);
path.push(4); assert_eq!(*path, vec![1, 2, 3, 4]);
}
#[test]
fn test_extract_path_segments_multiple_slashes() {
let segments = extract_path_segments("//api//users//123//");
assert_eq!(segments, vec!["api", "users", "123"]);
}
#[test]
fn test_extract_path_segments_url_encoded_special_chars() {
let segments = extract_path_segments("/users/hello%20world");
assert_eq!(segments, vec!["users", "hello world"]);
let segments = extract_path_segments("/search/foo%26bar");
assert_eq!(segments, vec!["search", "foo&bar"]);
}
#[test]
fn test_path_error_source() {
let error = PathError::EmptyPath;
assert!(std::error::Error::source(&error).is_none());
let error = PathError::DeserializationError {
source: "test".to_string(),
segments: vec![],
target_type: "Test",
};
assert!(std::error::Error::source(&error).is_none());
}
}