elif_http/routing/
params.rs

1//! Route parameter extraction and validation
2
3use axum::extract::{Path, FromRequestParts};
4use axum::http::request::Parts;
5use serde::{Deserialize, de::DeserializeOwned};
6use std::collections::HashMap;
7use thiserror::Error;
8
9/// Errors that can occur during parameter extraction
10#[derive(Error, Debug)]
11pub enum ParamError {
12    #[error("Missing parameter: {0}")]
13    Missing(String),
14    #[error("Invalid parameter format: {0}")]
15    InvalidFormat(String),
16    #[error("Parameter validation failed: {0}")]
17    ValidationFailed(String),
18    #[error("Deserialization error: {0}")]
19    DeserializationError(String),
20}
21
22/// A single route parameter with validation
23#[derive(Debug, Clone)]
24pub struct RouteParam {
25    pub name: String,
26    pub value: String,
27    pub param_type: ParamType,
28}
29
30/// Supported parameter types
31#[derive(Debug, Clone, PartialEq)]
32pub enum ParamType {
33    String,
34    Integer,
35    Uuid,
36    Custom(String), // For custom validation patterns
37}
38
39impl RouteParam {
40    pub fn new(name: String, value: String, param_type: ParamType) -> Self {
41        Self {
42            name,
43            value,
44            param_type,
45        }
46    }
47
48    /// Validate the parameter value against its type
49    pub fn validate(&self) -> Result<(), ParamError> {
50        match &self.param_type {
51            ParamType::String => Ok(()), // Strings are always valid
52            ParamType::Integer => {
53                self.value.parse::<i64>()
54                    .map(|_| ())
55                    .map_err(|_| ParamError::ValidationFailed(
56                        format!("Parameter '{}' must be an integer", self.name)
57                    ))
58            }
59            ParamType::Uuid => {
60                uuid::Uuid::parse_str(&self.value)
61                    .map(|_| ())
62                    .map_err(|_| ParamError::ValidationFailed(
63                        format!("Parameter '{}' must be a valid UUID", self.name)
64                    ))
65            }
66            ParamType::Custom(_pattern) => {
67                // TODO: Implement regex validation for custom patterns
68                Ok(())
69            }
70        }
71    }
72
73    /// Get the typed value as T
74    pub fn as_typed<T>(&self) -> Result<T, ParamError> 
75    where
76        T: std::str::FromStr,
77        T::Err: std::fmt::Display,
78    {
79        self.validate()?;
80        self.value.parse::<T>()
81            .map_err(|e| ParamError::InvalidFormat(format!("Cannot convert '{}' to target type: {}", self.value, e)))
82    }
83}
84
85/// Container for extracted path parameters
86#[derive(Debug, Default)]
87pub struct PathParams {
88    params: HashMap<String, RouteParam>,
89}
90
91impl PathParams {
92    pub fn new() -> Self {
93        Self {
94            params: HashMap::new(),
95        }
96    }
97
98    pub fn add_param(&mut self, param: RouteParam) {
99        self.params.insert(param.name.clone(), param);
100    }
101
102    /// Get a parameter by name
103    pub fn get(&self, name: &str) -> Option<&RouteParam> {
104        self.params.get(name)
105    }
106
107    /// Get parameter value as string
108    pub fn get_str(&self, name: &str) -> Option<&str> {
109        self.params.get(name).map(|p| p.value.as_str())
110    }
111
112    /// Get parameter value as typed value
113    pub fn get_typed<T>(&self, name: &str) -> Result<T, ParamError>
114    where
115        T: std::str::FromStr,
116        T::Err: std::fmt::Display,
117    {
118        self.get(name)
119            .ok_or_else(|| ParamError::Missing(name.to_string()))?
120            .as_typed()
121    }
122
123    /// Get all parameters
124    pub fn all(&self) -> &HashMap<String, RouteParam> {
125        &self.params
126    }
127
128    /// Validate all parameters
129    pub fn validate_all(&self) -> Result<(), ParamError> {
130        for param in self.params.values() {
131            param.validate()?;
132        }
133        Ok(())
134    }
135}
136
137/// Extract path parameters from axum Path
138impl<T> From<Path<T>> for PathParams 
139where
140    T: DeserializeOwned + Send + 'static,
141{
142    fn from(_path: Path<T>) -> Self {
143        // This is a placeholder implementation
144        // In practice, we'd need to work with the actual extracted values
145        PathParams::new()
146    }
147}
148
149/// Builder for creating typed parameter extractors
150#[derive(Debug)]
151pub struct ParamExtractor {
152    param_specs: HashMap<String, ParamType>,
153}
154
155impl ParamExtractor {
156    pub fn new() -> Self {
157        Self {
158            param_specs: HashMap::new(),
159        }
160    }
161
162    /// Specify a parameter type
163    pub fn param(mut self, name: &str, param_type: ParamType) -> Self {
164        self.param_specs.insert(name.to_string(), param_type);
165        self
166    }
167
168    /// Extract and validate parameters from a path
169    pub fn extract_from_path(&self, path: &str, route_pattern: &str) -> Result<PathParams, ParamError> {
170        let mut params = PathParams::new();
171        
172        // Parse route pattern to find parameter names
173        let pattern_parts: Vec<&str> = route_pattern.split('/').collect();
174        let path_parts: Vec<&str> = path.split('/').collect();
175        
176        if pattern_parts.len() != path_parts.len() {
177            return Err(ParamError::InvalidFormat("Path structure mismatch".to_string()));
178        }
179        
180        for (pattern_part, path_part) in pattern_parts.iter().zip(path_parts.iter()) {
181            if pattern_part.starts_with('{') && pattern_part.ends_with('}') {
182                let param_name = &pattern_part[1..pattern_part.len()-1];
183                let param_type = self.param_specs.get(param_name)
184                    .cloned()
185                    .unwrap_or(ParamType::String);
186                
187                let param = RouteParam::new(
188                    param_name.to_string(),
189                    path_part.to_string(),
190                    param_type,
191                );
192                
193                param.validate()?;
194                params.add_param(param);
195            }
196        }
197        
198        Ok(params)
199    }
200}
201
202impl Default for ParamExtractor {
203    fn default() -> Self {
204        Self::new()
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_route_param_validation() {
214        let param = RouteParam::new("id".to_string(), "123".to_string(), ParamType::Integer);
215        assert!(param.validate().is_ok());
216        
217        let invalid_param = RouteParam::new("id".to_string(), "abc".to_string(), ParamType::Integer);
218        assert!(invalid_param.validate().is_err());
219    }
220
221    #[test]
222    fn test_param_extractor() {
223        let extractor = ParamExtractor::new()
224            .param("id", ParamType::Integer)
225            .param("slug", ParamType::String);
226        
227        let params = extractor.extract_from_path("/users/123/posts/hello", "/users/{id}/posts/{slug}").unwrap();
228        
229        assert_eq!(params.get_str("id"), Some("123"));
230        assert_eq!(params.get_str("slug"), Some("hello"));
231        assert_eq!(params.get_typed::<i64>("id").unwrap(), 123);
232    }
233}