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 => 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 Ok(())
66 }
67 }
68 }
69
70 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#[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 pub fn get(&self, name: &str) -> Option<&RouteParam> {
105 self.params.get(name)
106 }
107
108 pub fn get_str(&self, name: &str) -> Option<&str> {
110 self.params.get(name).map(|p| p.value.as_str())
111 }
112
113 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 pub fn all(&self) -> &HashMap<String, RouteParam> {
126 &self.params
127 }
128
129 pub fn validate_all(&self) -> Result<(), ParamError> {
131 for param in self.params.values() {
132 param.validate()?;
133 }
134 Ok(())
135 }
136}
137
138impl<T> From<Path<T>> for PathParams
140where
141 T: DeserializeOwned + Send + 'static,
142{
143 fn from(_path: Path<T>) -> Self {
144 PathParams::new()
147 }
148}
149
150#[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 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 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 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}