1use super::pattern::{ParamConstraint, RoutePattern};
31use std::collections::HashMap;
32use std::str::FromStr;
33use thiserror::Error;
34use uuid::Uuid;
35
36#[derive(Error, Debug)]
38pub enum ExtractionError {
39 #[error("Missing parameter: {0}")]
40 Missing(String),
41 #[error("Parameter validation failed for '{param}': {reason}")]
42 ValidationFailed { param: String, reason: String },
43 #[error("Type conversion failed for parameter '{param}': {error}")]
44 ConversionFailed { param: String, error: String },
45 #[error("Constraint violation for parameter '{param}': expected {constraint}, got '{value}'")]
46 ConstraintViolation { param: String, constraint: String, value: String },
47}
48
49#[derive(Debug, Clone)]
51pub struct ExtractedParams {
52 raw_params: HashMap<String, String>,
53 pattern: RoutePattern,
54}
55
56impl ExtractedParams {
57 pub fn from_route_match(
59 raw_params: HashMap<String, String>,
60 pattern: RoutePattern
61 ) -> Result<Self, ExtractionError> {
62 let extracted = Self { raw_params, pattern };
63
64 extracted.validate_all()?;
66
67 Ok(extracted)
68 }
69
70
71 pub fn get_str(&self, name: &str) -> Option<&str> {
73 self.raw_params.get(name).map(|s| s.as_str())
74 }
75
76 pub fn get<T>(&self, name: &str) -> Result<T, ExtractionError>
81 where
82 T: FromStr,
83 T::Err: std::fmt::Display,
84 {
85 let value = self.raw_params
86 .get(name)
87 .ok_or_else(|| ExtractionError::Missing(name.to_string()))?;
88
89 value.parse::<T>().map_err(|e| ExtractionError::ConversionFailed {
91 param: name.to_string(),
92 error: e.to_string(),
93 })
94 }
95
96 pub fn get_int(&self, name: &str) -> Result<i64, ExtractionError> {
98 self.get::<i64>(name)
99 }
100
101 pub fn get_uuid(&self, name: &str) -> Result<Uuid, ExtractionError> {
103 self.get::<Uuid>(name)
104 }
105
106 pub fn get_or<T>(&self, name: &str, default: T) -> Result<T, ExtractionError>
108 where
109 T: FromStr,
110 T::Err: std::fmt::Display,
111 {
112 match self.get(name) {
113 Ok(value) => Ok(value),
114 Err(ExtractionError::Missing(_)) => Ok(default),
115 Err(e) => Err(e),
116 }
117 }
118
119 pub fn get_optional<T>(&self, name: &str) -> Result<Option<T>, ExtractionError>
121 where
122 T: FromStr,
123 T::Err: std::fmt::Display,
124 {
125 match self.get(name) {
126 Ok(value) => Ok(Some(value)),
127 Err(ExtractionError::Missing(_)) => Ok(None),
128 Err(e) => Err(e),
129 }
130 }
131
132 pub fn param_names(&self) -> Vec<&String> {
134 self.raw_params.keys().collect()
135 }
136
137 pub fn raw_params(&self) -> &HashMap<String, String> {
139 &self.raw_params
140 }
141
142 pub fn validate_all(&self) -> Result<(), ExtractionError> {
144 for (param_name, param_value) in &self.raw_params {
145 for segment in &self.pattern.segments {
147 match segment {
148 super::pattern::PathSegment::Parameter { name, constraint } if name == param_name => {
149 if !constraint.validate(param_value) {
150 return Err(ExtractionError::ConstraintViolation {
151 param: param_name.clone(),
152 constraint: format!("{:?}", constraint),
153 value: param_value.clone(),
154 });
155 }
156 break;
157 }
158 super::pattern::PathSegment::CatchAll { name } if name == param_name => {
159 break;
161 }
162 _ => continue,
163 }
164 }
165 }
166 Ok(())
167 }
168
169}
170
171#[derive(Debug)]
173pub struct ParameterExtractor {
174 pattern: RoutePattern,
175}
176
177impl ParameterExtractor {
178 pub fn new(pattern: RoutePattern) -> Self {
180 Self { pattern }
181 }
182
183 pub fn extract_from_params(&self, raw_params: HashMap<String, String>) -> Result<ExtractedParams, ExtractionError> {
185 ExtractedParams::from_route_match(raw_params, self.pattern.clone())
186 }
187
188 pub fn extract(&self, path: &str) -> Result<ExtractedParams, ExtractionError> {
193 if !self.pattern.matches(path) {
195 return Err(ExtractionError::ValidationFailed {
196 param: "path".to_string(),
197 reason: "Path does not match route pattern".to_string(),
198 });
199 }
200
201 let raw_params = self.pattern.extract_params(path);
203
204 self.extract_from_params(raw_params)
206 }
207
208 pub fn pattern(&self) -> &RoutePattern {
210 &self.pattern
211 }
212
213 pub fn param_names(&self) -> &[String] {
215 &self.pattern.param_names
216 }
217}
218
219#[derive(Debug)]
221pub struct TypedExtractorBuilder {
222 pattern: RoutePattern,
223 custom_constraints: HashMap<String, ParamConstraint>,
224}
225
226impl TypedExtractorBuilder {
227 pub fn new(pattern: RoutePattern) -> Self {
229 Self {
230 pattern,
231 custom_constraints: HashMap::new(),
232 }
233 }
234
235 pub fn constraint(mut self, param_name: &str, constraint: ParamConstraint) -> Self {
237 self.custom_constraints.insert(param_name.to_string(), constraint);
238 self
239 }
240
241 pub fn int_param(self, param_name: &str) -> Self {
243 self.constraint(param_name, ParamConstraint::Int)
244 }
245
246 pub fn uuid_param(self, param_name: &str) -> Self {
248 self.constraint(param_name, ParamConstraint::Uuid)
249 }
250
251 pub fn alpha_param(self, param_name: &str) -> Self {
253 self.constraint(param_name, ParamConstraint::Alpha)
254 }
255
256 pub fn slug_param(self, param_name: &str) -> Self {
258 self.constraint(param_name, ParamConstraint::Slug)
259 }
260
261 pub fn build(mut self) -> ParameterExtractor {
263 for segment in &mut self.pattern.segments {
265 if let super::pattern::PathSegment::Parameter { name, constraint } = segment {
266 if let Some(custom_constraint) = self.custom_constraints.remove(name) {
267 *constraint = custom_constraint;
268 }
269 }
270 }
271
272 ParameterExtractor::new(self.pattern)
273 }
274}
275
276#[macro_export]
278macro_rules! extract_params {
279 ($extracted:expr, $($name:ident: $type:ty),+ $(,)?) => {
280 {
281 $(
282 let $name: $type = $extracted.get(stringify!($name))?;
283 )+
284 }
285 };
286}
287
288#[macro_export]
289macro_rules! extract_optional_params {
290 ($extracted:expr, $($name:ident: $type:ty),+ $(,)?) => {
291 {
292 $(
293 let $name: Option<$type> = $extracted.get_optional(stringify!($name))?;
294 )+
295 }
296 };
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use super::super::pattern::RoutePattern;
303
304 #[test]
305 fn test_basic_parameter_extraction() {
306 let pattern = RoutePattern::parse("/users/{id}/posts/{slug}").unwrap();
307 let extractor = ParameterExtractor::new(pattern);
308
309 let extracted = extractor.extract("/users/123/posts/hello-world").unwrap();
310
311 assert_eq!(extracted.get_str("id"), Some("123"));
312 assert_eq!(extracted.get_str("slug"), Some("hello-world"));
313 }
314
315 #[test]
316 fn test_efficient_parameter_extraction() {
317 let pattern = RoutePattern::parse("/users/{id:int}/posts/{slug}").unwrap();
319 let extractor = ParameterExtractor::new(pattern);
320
321 let mut raw_params = HashMap::new();
323 raw_params.insert("id".to_string(), "456".to_string());
324 raw_params.insert("slug".to_string(), "test-post".to_string());
325
326 let extracted = extractor.extract_from_params(raw_params).unwrap();
328
329 assert_eq!(extracted.get_str("id"), Some("456"));
330 assert_eq!(extracted.get_str("slug"), Some("test-post"));
331 assert_eq!(extracted.get_int("id").unwrap(), 456);
332 }
333
334 #[test]
335 fn test_typed_parameter_extraction() {
336 let pattern = RoutePattern::parse("/users/{id:int}/posts/{slug}").unwrap();
337 let extractor = ParameterExtractor::new(pattern);
338
339 let extracted = extractor.extract("/users/123/posts/hello-world").unwrap();
340
341 assert_eq!(extracted.get_int("id").unwrap(), 123);
343
344 assert_eq!(extracted.get::<String>("slug").unwrap(), "hello-world");
346 }
347
348 #[test]
349 fn test_uuid_parameter_extraction() {
350 let pattern = RoutePattern::parse("/users/{id:uuid}").unwrap();
351 let extractor = ParameterExtractor::new(pattern);
352
353 let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
354 let extracted = extractor.extract(&format!("/users/{}", uuid_str)).unwrap();
355
356 let uuid = extracted.get_uuid("id").unwrap();
357 assert_eq!(uuid.to_string(), uuid_str);
358 }
359
360 #[test]
361 fn test_constraint_violations() {
362 let pattern = RoutePattern::parse("/users/{id:int}").unwrap();
363 let extractor = ParameterExtractor::new(pattern);
364
365 let result = extractor.extract("/users/abc");
367 assert!(result.is_err());
368 assert!(matches!(result.unwrap_err(), ExtractionError::ValidationFailed { .. }));
369 }
370
371 #[test]
372 fn test_optional_parameters() {
373 let pattern = RoutePattern::parse("/users/{id}").unwrap();
374 let extractor = ParameterExtractor::new(pattern);
375
376 let extracted = extractor.extract("/users/123").unwrap();
377
378 let id: Option<i64> = extracted.get_optional("id").unwrap();
380 assert_eq!(id, Some(123));
381
382 let missing: Option<String> = extracted.get_optional("missing").unwrap();
384 assert_eq!(missing, None);
385 }
386
387 #[test]
388 fn test_parameter_with_defaults() {
389 let pattern = RoutePattern::parse("/users/{id}").unwrap();
390 let extractor = ParameterExtractor::new(pattern);
391
392 let extracted = extractor.extract("/users/123").unwrap();
393
394 let id = extracted.get_or("id", 0i64).unwrap();
396 assert_eq!(id, 123);
397
398 let page = extracted.get_or("page", 1i64).unwrap();
400 assert_eq!(page, 1);
401 }
402
403 #[test]
404 fn test_catch_all_parameter() {
405 let pattern = RoutePattern::parse("/files/*path").unwrap();
406 let extractor = ParameterExtractor::new(pattern);
407
408 let extracted = extractor.extract("/files/docs/images/logo.png").unwrap();
409
410 let path: String = extracted.get("path").unwrap();
411 assert_eq!(path, "docs/images/logo.png");
412 }
413
414 #[test]
415 fn test_typed_extractor_builder() {
416 let pattern = RoutePattern::parse("/api/{version}/users/{id}").unwrap();
417 let extractor = TypedExtractorBuilder::new(pattern)
418 .slug_param("version")
419 .int_param("id")
420 .build();
421
422 let extracted = extractor.extract("/api/v1/users/123").unwrap();
423
424 assert_eq!(extracted.get::<String>("version").unwrap(), "v1");
425 assert_eq!(extracted.get_int("id").unwrap(), 123);
426 }
427
428 #[test]
429 fn test_custom_regex_constraint() {
430 use regex::Regex;
431
432 let pattern = RoutePattern::parse("/posts/{slug}").unwrap();
433 let regex = Regex::new(r"^[a-z0-9-]+$").unwrap();
434
435 let extractor = TypedExtractorBuilder::new(pattern)
436 .constraint("slug", ParamConstraint::Custom(regex))
437 .build();
438
439 let result = extractor.extract("/posts/hello-world-123");
441 assert!(result.is_ok());
442
443 let result = extractor.extract("/posts/Hello_World!");
445 assert!(result.is_err());
446 }
447
448 #[test]
449 fn test_all_constraints() {
450 assert!(ParamConstraint::Int.validate("123"));
452 assert!(!ParamConstraint::Int.validate("abc"));
453
454 assert!(ParamConstraint::Alpha.validate("hello"));
455 assert!(!ParamConstraint::Alpha.validate("hello123"));
456
457 assert!(ParamConstraint::Slug.validate("hello-world_123"));
458 assert!(!ParamConstraint::Slug.validate("hello world!"));
459
460 let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
461 assert!(ParamConstraint::Uuid.validate(uuid_str));
462 assert!(!ParamConstraint::Uuid.validate("not-a-uuid"));
463
464 assert!(ParamConstraint::None.validate("anything"));
465 assert!(!ParamConstraint::None.validate("")); }
467
468 #[test]
469 fn test_error_types() {
470 let pattern = RoutePattern::parse("/users/{id:int}").unwrap();
471 let extractor = ParameterExtractor::new(pattern);
472
473 let extracted = extractor.extract("/users/123").unwrap();
474
475 let result: Result<i64, _> = extracted.get("missing");
477 assert!(matches!(result.unwrap_err(), ExtractionError::Missing(_)));
478
479 let pattern2 = RoutePattern::parse("/users/{name}").unwrap();
482 let extractor2 = ParameterExtractor::new(pattern2);
483 let extracted2 = extractor2.extract("/users/john").unwrap();
484
485 let result: Result<i64, _> = extracted2.get("name");
486 assert!(matches!(result.unwrap_err(), ExtractionError::ConversionFailed { .. }));
487 }
488
489 #[test]
490 fn test_parameter_access_performance() {
491 let pattern = RoutePattern::parse("/users/{id:int}/posts/{slug:alpha}").unwrap();
493
494 let mut raw_params = HashMap::new();
495 raw_params.insert("id".to_string(), "123".to_string());
496 raw_params.insert("slug".to_string(), "helloworld".to_string());
497
498 let extracted = ExtractedParams::from_route_match(raw_params, pattern).unwrap();
499
500 let start = std::time::Instant::now();
501
502 for _ in 0..10000 {
504 let id: i64 = extracted.get("id").unwrap();
505 let slug: String = extracted.get("slug").unwrap();
506 assert_eq!(id, 123);
507 assert_eq!(slug, "helloworld");
508 }
509
510 let elapsed = start.elapsed();
511
512 assert!(elapsed.as_millis() < 50, "Parameter access took too long: {}ms", elapsed.as_millis());
514
515 println!("20,000 parameter accesses completed in {}μs", elapsed.as_micros());
516 }
517
518 #[test]
519 fn test_integration_with_route_matcher() {
520 use super::super::{HttpMethod, compiler::RouteCompilerBuilder};
522
523 let compilation_result = RouteCompilerBuilder::new()
525 .get("users_show".to_string(), "/users/{id:int}".to_string())
526 .get("posts_show".to_string(), "/posts/{slug:alpha}/comments/{id:uuid}".to_string())
527 .build()
528 .unwrap();
529
530 let route_match = compilation_result.matcher
532 .resolve(&HttpMethod::GET, "/users/123")
533 .unwrap();
534
535 assert_eq!(route_match.route_id, "users_show");
536 assert_eq!(route_match.params.get("id"), Some(&"123".to_string()));
537
538 let extractor = compilation_result.extractors.get("users_show").unwrap();
540 let extracted = extractor.extract_from_params(route_match.params).unwrap();
541
542 let user_id: i64 = extracted.get("id").unwrap();
544 assert_eq!(user_id, 123);
545
546 let route_match = compilation_result.matcher
549 .resolve(&HttpMethod::GET, "/posts/helloworld/comments/550e8400-e29b-41d4-a716-446655440000")
550 .unwrap();
551
552 assert_eq!(route_match.route_id, "posts_show");
553
554 let extractor = compilation_result.extractors.get("posts_show").unwrap();
555 let extracted = extractor.extract_from_params(route_match.params).unwrap();
556
557 let slug: String = extracted.get("slug").unwrap();
558 let comment_id = extracted.get_uuid("id").unwrap();
559
560 assert_eq!(slug, "helloworld");
561 assert_eq!(comment_id.to_string(), "550e8400-e29b-41d4-a716-446655440000");
562 }
563}