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