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