elif_http/routing/
params.rs

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