use super::pattern::{ParamConstraint, RoutePattern};
use std::collections::HashMap;
use std::str::FromStr;
use thiserror::Error;
use uuid::Uuid;
#[derive(Error, Debug)]
pub enum ExtractionError {
#[error("Missing parameter: {0}")]
Missing(String),
#[error("Parameter validation failed for '{param}': {reason}")]
ValidationFailed { param: String, reason: String },
#[error("Type conversion failed for parameter '{param}': {error}")]
ConversionFailed { param: String, error: String },
#[error("Constraint violation for parameter '{param}': expected {constraint}, got '{value}'")]
ConstraintViolation {
param: String,
constraint: String,
value: String,
},
}
#[derive(Debug, Clone)]
pub struct ExtractedParams {
raw_params: HashMap<String, String>,
pattern: RoutePattern,
}
impl ExtractedParams {
pub fn from_route_match(
raw_params: HashMap<String, String>,
pattern: RoutePattern,
) -> Result<Self, ExtractionError> {
let extracted = Self {
raw_params,
pattern,
};
extracted.validate_all()?;
Ok(extracted)
}
pub fn get_str(&self, name: &str) -> Option<&str> {
self.raw_params.get(name).map(|s| s.as_str())
}
pub fn get<T>(&self, name: &str) -> Result<T, ExtractionError>
where
T: FromStr,
T::Err: std::fmt::Display,
{
let value = self
.raw_params
.get(name)
.ok_or_else(|| ExtractionError::Missing(name.to_string()))?;
value
.parse::<T>()
.map_err(|e| ExtractionError::ConversionFailed {
param: name.to_string(),
error: e.to_string(),
})
}
pub fn get_int(&self, name: &str) -> Result<i64, ExtractionError> {
self.get::<i64>(name)
}
pub fn get_uuid(&self, name: &str) -> Result<Uuid, ExtractionError> {
self.get::<Uuid>(name)
}
pub fn get_or<T>(&self, name: &str, default: T) -> Result<T, ExtractionError>
where
T: FromStr,
T::Err: std::fmt::Display,
{
match self.get(name) {
Ok(value) => Ok(value),
Err(ExtractionError::Missing(_)) => Ok(default),
Err(e) => Err(e),
}
}
pub fn get_optional<T>(&self, name: &str) -> Result<Option<T>, ExtractionError>
where
T: FromStr,
T::Err: std::fmt::Display,
{
match self.get(name) {
Ok(value) => Ok(Some(value)),
Err(ExtractionError::Missing(_)) => Ok(None),
Err(e) => Err(e),
}
}
pub fn param_names(&self) -> Vec<&String> {
self.raw_params.keys().collect()
}
pub fn raw_params(&self) -> &HashMap<String, String> {
&self.raw_params
}
pub fn validate_all(&self) -> Result<(), ExtractionError> {
for (param_name, param_value) in &self.raw_params {
for segment in &self.pattern.segments {
match segment {
super::pattern::PathSegment::Parameter { name, constraint }
if name == param_name =>
{
if !constraint.validate(param_value) {
return Err(ExtractionError::ConstraintViolation {
param: param_name.clone(),
constraint: format!("{:?}", constraint),
value: param_value.clone(),
});
}
break;
}
super::pattern::PathSegment::CatchAll { name } if name == param_name => {
break;
}
_ => continue,
}
}
}
Ok(())
}
}
#[derive(Debug)]
pub struct ParameterExtractor {
pattern: RoutePattern,
}
impl ParameterExtractor {
pub fn new(pattern: RoutePattern) -> Self {
Self { pattern }
}
pub fn extract_from_params(
&self,
raw_params: HashMap<String, String>,
) -> Result<ExtractedParams, ExtractionError> {
ExtractedParams::from_route_match(raw_params, self.pattern.clone())
}
pub fn extract(&self, path: &str) -> Result<ExtractedParams, ExtractionError> {
if !self.pattern.matches(path) {
return Err(ExtractionError::ValidationFailed {
param: "path".to_string(),
reason: "Path does not match route pattern".to_string(),
});
}
let raw_params = self.pattern.extract_params(path);
self.extract_from_params(raw_params)
}
pub fn pattern(&self) -> &RoutePattern {
&self.pattern
}
pub fn param_names(&self) -> &[String] {
&self.pattern.param_names
}
}
#[derive(Debug)]
pub struct TypedExtractorBuilder {
pattern: RoutePattern,
custom_constraints: HashMap<String, ParamConstraint>,
}
impl TypedExtractorBuilder {
pub fn new(pattern: RoutePattern) -> Self {
Self {
pattern,
custom_constraints: HashMap::new(),
}
}
pub fn constraint(mut self, param_name: &str, constraint: ParamConstraint) -> Self {
self.custom_constraints
.insert(param_name.to_string(), constraint);
self
}
pub fn int_param(self, param_name: &str) -> Self {
self.constraint(param_name, ParamConstraint::Int)
}
pub fn uuid_param(self, param_name: &str) -> Self {
self.constraint(param_name, ParamConstraint::Uuid)
}
pub fn alpha_param(self, param_name: &str) -> Self {
self.constraint(param_name, ParamConstraint::Alpha)
}
pub fn slug_param(self, param_name: &str) -> Self {
self.constraint(param_name, ParamConstraint::Slug)
}
pub fn build(mut self) -> ParameterExtractor {
for segment in &mut self.pattern.segments {
if let super::pattern::PathSegment::Parameter { name, constraint } = segment {
if let Some(custom_constraint) = self.custom_constraints.remove(name) {
*constraint = custom_constraint;
}
}
}
ParameterExtractor::new(self.pattern)
}
}
#[macro_export]
macro_rules! extract_params {
($extracted:expr, $($name:ident: $type:ty),+ $(,)?) => {
{
$(
let $name: $type = $extracted.get(stringify!($name))?;
)+
}
};
}
#[macro_export]
macro_rules! extract_optional_params {
($extracted:expr, $($name:ident: $type:ty),+ $(,)?) => {
{
$(
let $name: Option<$type> = $extracted.get_optional(stringify!($name))?;
)+
}
};
}
#[cfg(test)]
mod tests {
use super::super::pattern::RoutePattern;
use super::*;
#[test]
fn test_basic_parameter_extraction() {
let pattern = RoutePattern::parse("/users/{id}/posts/{slug}").unwrap();
let extractor = ParameterExtractor::new(pattern);
let extracted = extractor.extract("/users/123/posts/hello-world").unwrap();
assert_eq!(extracted.get_str("id"), Some("123"));
assert_eq!(extracted.get_str("slug"), Some("hello-world"));
}
#[test]
fn test_efficient_parameter_extraction() {
let pattern = RoutePattern::parse("/users/{id:int}/posts/{slug}").unwrap();
let extractor = ParameterExtractor::new(pattern);
let mut raw_params = HashMap::new();
raw_params.insert("id".to_string(), "456".to_string());
raw_params.insert("slug".to_string(), "test-post".to_string());
let extracted = extractor.extract_from_params(raw_params).unwrap();
assert_eq!(extracted.get_str("id"), Some("456"));
assert_eq!(extracted.get_str("slug"), Some("test-post"));
assert_eq!(extracted.get_int("id").unwrap(), 456);
}
#[test]
fn test_typed_parameter_extraction() {
let pattern = RoutePattern::parse("/users/{id:int}/posts/{slug}").unwrap();
let extractor = ParameterExtractor::new(pattern);
let extracted = extractor.extract("/users/123/posts/hello-world").unwrap();
assert_eq!(extracted.get_int("id").unwrap(), 123);
assert_eq!(extracted.get::<String>("slug").unwrap(), "hello-world");
}
#[test]
fn test_uuid_parameter_extraction() {
let pattern = RoutePattern::parse("/users/{id:uuid}").unwrap();
let extractor = ParameterExtractor::new(pattern);
let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
let extracted = extractor.extract(&format!("/users/{}", uuid_str)).unwrap();
let uuid = extracted.get_uuid("id").unwrap();
assert_eq!(uuid.to_string(), uuid_str);
}
#[test]
fn test_constraint_violations() {
let pattern = RoutePattern::parse("/users/{id:int}").unwrap();
let extractor = ParameterExtractor::new(pattern);
let result = extractor.extract("/users/abc");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ExtractionError::ValidationFailed { .. }
));
}
#[test]
fn test_optional_parameters() {
let pattern = RoutePattern::parse("/users/{id}").unwrap();
let extractor = ParameterExtractor::new(pattern);
let extracted = extractor.extract("/users/123").unwrap();
let id: Option<i64> = extracted.get_optional("id").unwrap();
assert_eq!(id, Some(123));
let missing: Option<String> = extracted.get_optional("missing").unwrap();
assert_eq!(missing, None);
}
#[test]
fn test_parameter_with_defaults() {
let pattern = RoutePattern::parse("/users/{id}").unwrap();
let extractor = ParameterExtractor::new(pattern);
let extracted = extractor.extract("/users/123").unwrap();
let id = extracted.get_or("id", 0i64).unwrap();
assert_eq!(id, 123);
let page = extracted.get_or("page", 1i64).unwrap();
assert_eq!(page, 1);
}
#[test]
fn test_catch_all_parameter() {
let pattern = RoutePattern::parse("/files/*path").unwrap();
let extractor = ParameterExtractor::new(pattern);
let extracted = extractor.extract("/files/docs/images/logo.png").unwrap();
let path: String = extracted.get("path").unwrap();
assert_eq!(path, "docs/images/logo.png");
}
#[test]
fn test_typed_extractor_builder() {
let pattern = RoutePattern::parse("/api/{version}/users/{id}").unwrap();
let extractor = TypedExtractorBuilder::new(pattern)
.slug_param("version")
.int_param("id")
.build();
let extracted = extractor.extract("/api/v1/users/123").unwrap();
assert_eq!(extracted.get::<String>("version").unwrap(), "v1");
assert_eq!(extracted.get_int("id").unwrap(), 123);
}
#[test]
fn test_custom_regex_constraint() {
use regex::Regex;
let pattern = RoutePattern::parse("/posts/{slug}").unwrap();
let regex = Regex::new(r"^[a-z0-9-]+$").unwrap();
let extractor = TypedExtractorBuilder::new(pattern)
.constraint("slug", ParamConstraint::Custom(regex))
.build();
let result = extractor.extract("/posts/hello-world-123");
assert!(result.is_ok());
let result = extractor.extract("/posts/Hello_World!");
assert!(result.is_err());
}
#[test]
fn test_all_constraints() {
assert!(ParamConstraint::Int.validate("123"));
assert!(!ParamConstraint::Int.validate("abc"));
assert!(ParamConstraint::Alpha.validate("hello"));
assert!(!ParamConstraint::Alpha.validate("hello123"));
assert!(ParamConstraint::Slug.validate("hello-world_123"));
assert!(!ParamConstraint::Slug.validate("hello world!"));
let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
assert!(ParamConstraint::Uuid.validate(uuid_str));
assert!(!ParamConstraint::Uuid.validate("not-a-uuid"));
assert!(ParamConstraint::None.validate("anything"));
assert!(!ParamConstraint::None.validate("")); }
#[test]
fn test_error_types() {
let pattern = RoutePattern::parse("/users/{id:int}").unwrap();
let extractor = ParameterExtractor::new(pattern);
let extracted = extractor.extract("/users/123").unwrap();
let result: Result<i64, _> = extracted.get("missing");
assert!(matches!(result.unwrap_err(), ExtractionError::Missing(_)));
let pattern2 = RoutePattern::parse("/users/{name}").unwrap();
let extractor2 = ParameterExtractor::new(pattern2);
let extracted2 = extractor2.extract("/users/john").unwrap();
let result: Result<i64, _> = extracted2.get("name");
assert!(matches!(
result.unwrap_err(),
ExtractionError::ConversionFailed { .. }
));
}
#[test]
fn test_parameter_access_performance() {
let pattern = RoutePattern::parse("/users/{id:int}/posts/{slug:alpha}").unwrap();
let mut raw_params = HashMap::new();
raw_params.insert("id".to_string(), "123".to_string());
raw_params.insert("slug".to_string(), "helloworld".to_string());
let extracted = ExtractedParams::from_route_match(raw_params, pattern).unwrap();
let start = std::time::Instant::now();
for _ in 0..10000 {
let id: i64 = extracted.get("id").unwrap();
let slug: String = extracted.get("slug").unwrap();
assert_eq!(id, 123);
assert_eq!(slug, "helloworld");
}
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < 50,
"Parameter access took too long: {}ms",
elapsed.as_millis()
);
println!(
"20,000 parameter accesses completed in {}μs",
elapsed.as_micros()
);
}
#[test]
fn test_integration_with_route_matcher() {
use super::super::{compiler::RouteCompilerBuilder, HttpMethod};
let compilation_result = RouteCompilerBuilder::new()
.get("users_show".to_string(), "/users/{id:int}".to_string())
.get(
"posts_show".to_string(),
"/posts/{slug:alpha}/comments/{id:uuid}".to_string(),
)
.build()
.unwrap();
let route_match = compilation_result
.matcher
.resolve(&HttpMethod::GET, "/users/123")
.unwrap();
assert_eq!(route_match.route_id, "users_show");
assert_eq!(route_match.params.get("id"), Some(&"123".to_string()));
let extractor = compilation_result.extractors.get("users_show").unwrap();
let extracted = extractor.extract_from_params(route_match.params).unwrap();
let user_id: i64 = extracted.get("id").unwrap();
assert_eq!(user_id, 123);
let route_match = compilation_result
.matcher
.resolve(
&HttpMethod::GET,
"/posts/helloworld/comments/550e8400-e29b-41d4-a716-446655440000",
)
.unwrap();
assert_eq!(route_match.route_id, "posts_show");
let extractor = compilation_result.extractors.get("posts_show").unwrap();
let extracted = extractor.extract_from_params(route_match.params).unwrap();
let slug: String = extracted.get("slug").unwrap();
let comment_id = extracted.get_uuid("id").unwrap();
assert_eq!(slug, "helloworld");
assert_eq!(
comment_id.to_string(),
"550e8400-e29b-41d4-a716-446655440000"
);
}
}