elif_http/routing/
params.rs1use axum::extract::Path;
4use serde::de::DeserializeOwned;
5use std::collections::HashMap;
6use thiserror::Error;
7
8#[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#[derive(Debug, Clone)]
23pub struct RouteParam {
24 pub name: String,
25 pub value: String,
26 pub param_type: ParamType,
27}
28
29#[derive(Debug, Clone, PartialEq)]
31pub enum ParamType {
32 String,
33 Integer,
34 Uuid,
35 Custom(String), }
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 pub fn validate(&self) -> Result<(), ParamError> {
49 match &self.param_type {
50 ParamType::String => Ok(()), 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 Ok(())
68 }
69 }
70 }
71
72 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#[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 pub fn get(&self, name: &str) -> Option<&RouteParam> {
103 self.params.get(name)
104 }
105
106 pub fn get_str(&self, name: &str) -> Option<&str> {
108 self.params.get(name).map(|p| p.value.as_str())
109 }
110
111 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 pub fn all(&self) -> &HashMap<String, RouteParam> {
124 &self.params
125 }
126
127 pub fn validate_all(&self) -> Result<(), ParamError> {
129 for param in self.params.values() {
130 param.validate()?;
131 }
132 Ok(())
133 }
134}
135
136impl<T> From<Path<T>> for PathParams
138where
139 T: DeserializeOwned + Send + 'static,
140{
141 fn from(_path: Path<T>) -> Self {
142 PathParams::new()
145 }
146}
147
148#[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 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 pub fn extract_from_path(&self, path: &str, route_pattern: &str) -> Result<PathParams, ParamError> {
169 let mut params = PathParams::new();
170
171 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}