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 {
47 param: String,
48 constraint: String,
49 value: String,
50 },
51}
52
53#[derive(Debug, Clone)]
55pub struct ExtractedParams {
56 raw_params: HashMap<String, String>,
57 pattern: RoutePattern,
58}
59
60impl ExtractedParams {
61 pub fn from_route_match(
63 raw_params: HashMap<String, String>,
64 pattern: RoutePattern,
65 ) -> Result<Self, ExtractionError> {
66 let extracted = Self {
67 raw_params,
68 pattern,
69 };
70
71 extracted.validate_all()?;
73
74 Ok(extracted)
75 }
76
77 pub fn get_str(&self, name: &str) -> Option<&str> {
79 self.raw_params.get(name).map(|s| s.as_str())
80 }
81
82 pub fn get<T>(&self, name: &str) -> Result<T, ExtractionError>
87 where
88 T: FromStr,
89 T::Err: std::fmt::Display,
90 {
91 let value = self
92 .raw_params
93 .get(name)
94 .ok_or_else(|| ExtractionError::Missing(name.to_string()))?;
95
96 value
98 .parse::<T>()
99 .map_err(|e| ExtractionError::ConversionFailed {
100 param: name.to_string(),
101 error: e.to_string(),
102 })
103 }
104
105 pub fn get_int(&self, name: &str) -> Result<i64, ExtractionError> {
107 self.get::<i64>(name)
108 }
109
110 pub fn get_uuid(&self, name: &str) -> Result<Uuid, ExtractionError> {
112 self.get::<Uuid>(name)
113 }
114
115 pub fn get_or<T>(&self, name: &str, default: T) -> Result<T, ExtractionError>
117 where
118 T: FromStr,
119 T::Err: std::fmt::Display,
120 {
121 match self.get(name) {
122 Ok(value) => Ok(value),
123 Err(ExtractionError::Missing(_)) => Ok(default),
124 Err(e) => Err(e),
125 }
126 }
127
128 pub fn get_optional<T>(&self, name: &str) -> Result<Option<T>, ExtractionError>
130 where
131 T: FromStr,
132 T::Err: std::fmt::Display,
133 {
134 match self.get(name) {
135 Ok(value) => Ok(Some(value)),
136 Err(ExtractionError::Missing(_)) => Ok(None),
137 Err(e) => Err(e),
138 }
139 }
140
141 pub fn param_names(&self) -> Vec<&String> {
143 self.raw_params.keys().collect()
144 }
145
146 pub fn raw_params(&self) -> &HashMap<String, String> {
148 &self.raw_params
149 }
150
151 pub fn validate_all(&self) -> Result<(), ExtractionError> {
153 for (param_name, param_value) in &self.raw_params {
154 for segment in &self.pattern.segments {
156 match segment {
157 super::pattern::PathSegment::Parameter { name, constraint }
158 if name == param_name =>
159 {
160 if !constraint.validate(param_value) {
161 return Err(ExtractionError::ConstraintViolation {
162 param: param_name.clone(),
163 constraint: format!("{:?}", constraint),
164 value: param_value.clone(),
165 });
166 }
167 break;
168 }
169 super::pattern::PathSegment::CatchAll { name } if name == param_name => {
170 break;
172 }
173 _ => continue,
174 }
175 }
176 }
177 Ok(())
178 }
179}
180
181#[derive(Debug)]
183pub struct ParameterExtractor {
184 pattern: RoutePattern,
185}
186
187impl ParameterExtractor {
188 pub fn new(pattern: RoutePattern) -> Self {
190 Self { pattern }
191 }
192
193 pub fn extract_from_params(
195 &self,
196 raw_params: HashMap<String, String>,
197 ) -> Result<ExtractedParams, ExtractionError> {
198 ExtractedParams::from_route_match(raw_params, self.pattern.clone())
199 }
200
201 pub fn extract(&self, path: &str) -> Result<ExtractedParams, ExtractionError> {
206 if !self.pattern.matches(path) {
208 return Err(ExtractionError::ValidationFailed {
209 param: "path".to_string(),
210 reason: "Path does not match route pattern".to_string(),
211 });
212 }
213
214 let raw_params = self.pattern.extract_params(path);
216
217 self.extract_from_params(raw_params)
219 }
220
221 pub fn pattern(&self) -> &RoutePattern {
223 &self.pattern
224 }
225
226 pub fn param_names(&self) -> &[String] {
228 &self.pattern.param_names
229 }
230}
231
232#[derive(Debug)]
234pub struct TypedExtractorBuilder {
235 pattern: RoutePattern,
236 custom_constraints: HashMap<String, ParamConstraint>,
237}
238
239impl TypedExtractorBuilder {
240 pub fn new(pattern: RoutePattern) -> Self {
242 Self {
243 pattern,
244 custom_constraints: HashMap::new(),
245 }
246 }
247
248 pub fn constraint(mut self, param_name: &str, constraint: ParamConstraint) -> Self {
250 self.custom_constraints
251 .insert(param_name.to_string(), constraint);
252 self
253 }
254
255 pub fn int_param(self, param_name: &str) -> Self {
257 self.constraint(param_name, ParamConstraint::Int)
258 }
259
260 pub fn uuid_param(self, param_name: &str) -> Self {
262 self.constraint(param_name, ParamConstraint::Uuid)
263 }
264
265 pub fn alpha_param(self, param_name: &str) -> Self {
267 self.constraint(param_name, ParamConstraint::Alpha)
268 }
269
270 pub fn slug_param(self, param_name: &str) -> Self {
272 self.constraint(param_name, ParamConstraint::Slug)
273 }
274
275 pub fn build(mut self) -> ParameterExtractor {
277 for segment in &mut self.pattern.segments {
279 if let super::pattern::PathSegment::Parameter { name, constraint } = segment {
280 if let Some(custom_constraint) = self.custom_constraints.remove(name) {
281 *constraint = custom_constraint;
282 }
283 }
284 }
285
286 ParameterExtractor::new(self.pattern)
287 }
288}
289
290#[macro_export]
292macro_rules! extract_params {
293 ($extracted:expr, $($name:ident: $type:ty),+ $(,)?) => {
294 {
295 $(
296 let $name: $type = $extracted.get(stringify!($name))?;
297 )+
298 }
299 };
300}
301
302#[macro_export]
303macro_rules! extract_optional_params {
304 ($extracted:expr, $($name:ident: $type:ty),+ $(,)?) => {
305 {
306 $(
307 let $name: Option<$type> = $extracted.get_optional(stringify!($name))?;
308 )+
309 }
310 };
311}
312
313#[cfg(test)]
314mod tests {
315 use super::super::pattern::RoutePattern;
316 use super::*;
317
318 #[test]
319 fn test_basic_parameter_extraction() {
320 let pattern = RoutePattern::parse("/users/{id}/posts/{slug}").unwrap();
321 let extractor = ParameterExtractor::new(pattern);
322
323 let extracted = extractor.extract("/users/123/posts/hello-world").unwrap();
324
325 assert_eq!(extracted.get_str("id"), Some("123"));
326 assert_eq!(extracted.get_str("slug"), Some("hello-world"));
327 }
328
329 #[test]
330 fn test_efficient_parameter_extraction() {
331 let pattern = RoutePattern::parse("/users/{id:int}/posts/{slug}").unwrap();
333 let extractor = ParameterExtractor::new(pattern);
334
335 let mut raw_params = HashMap::new();
337 raw_params.insert("id".to_string(), "456".to_string());
338 raw_params.insert("slug".to_string(), "test-post".to_string());
339
340 let extracted = extractor.extract_from_params(raw_params).unwrap();
342
343 assert_eq!(extracted.get_str("id"), Some("456"));
344 assert_eq!(extracted.get_str("slug"), Some("test-post"));
345 assert_eq!(extracted.get_int("id").unwrap(), 456);
346 }
347
348 #[test]
349 fn test_typed_parameter_extraction() {
350 let pattern = RoutePattern::parse("/users/{id:int}/posts/{slug}").unwrap();
351 let extractor = ParameterExtractor::new(pattern);
352
353 let extracted = extractor.extract("/users/123/posts/hello-world").unwrap();
354
355 assert_eq!(extracted.get_int("id").unwrap(), 123);
357
358 assert_eq!(extracted.get::<String>("slug").unwrap(), "hello-world");
360 }
361
362 #[test]
363 fn test_uuid_parameter_extraction() {
364 let pattern = RoutePattern::parse("/users/{id:uuid}").unwrap();
365 let extractor = ParameterExtractor::new(pattern);
366
367 let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
368 let extracted = extractor.extract(&format!("/users/{}", uuid_str)).unwrap();
369
370 let uuid = extracted.get_uuid("id").unwrap();
371 assert_eq!(uuid.to_string(), uuid_str);
372 }
373
374 #[test]
375 fn test_constraint_violations() {
376 let pattern = RoutePattern::parse("/users/{id:int}").unwrap();
377 let extractor = ParameterExtractor::new(pattern);
378
379 let result = extractor.extract("/users/abc");
381 assert!(result.is_err());
382 assert!(matches!(
383 result.unwrap_err(),
384 ExtractionError::ValidationFailed { .. }
385 ));
386 }
387
388 #[test]
389 fn test_optional_parameters() {
390 let pattern = RoutePattern::parse("/users/{id}").unwrap();
391 let extractor = ParameterExtractor::new(pattern);
392
393 let extracted = extractor.extract("/users/123").unwrap();
394
395 let id: Option<i64> = extracted.get_optional("id").unwrap();
397 assert_eq!(id, Some(123));
398
399 let missing: Option<String> = extracted.get_optional("missing").unwrap();
401 assert_eq!(missing, None);
402 }
403
404 #[test]
405 fn test_parameter_with_defaults() {
406 let pattern = RoutePattern::parse("/users/{id}").unwrap();
407 let extractor = ParameterExtractor::new(pattern);
408
409 let extracted = extractor.extract("/users/123").unwrap();
410
411 let id = extracted.get_or("id", 0i64).unwrap();
413 assert_eq!(id, 123);
414
415 let page = extracted.get_or("page", 1i64).unwrap();
417 assert_eq!(page, 1);
418 }
419
420 #[test]
421 fn test_catch_all_parameter() {
422 let pattern = RoutePattern::parse("/files/*path").unwrap();
423 let extractor = ParameterExtractor::new(pattern);
424
425 let extracted = extractor.extract("/files/docs/images/logo.png").unwrap();
426
427 let path: String = extracted.get("path").unwrap();
428 assert_eq!(path, "docs/images/logo.png");
429 }
430
431 #[test]
432 fn test_typed_extractor_builder() {
433 let pattern = RoutePattern::parse("/api/{version}/users/{id}").unwrap();
434 let extractor = TypedExtractorBuilder::new(pattern)
435 .slug_param("version")
436 .int_param("id")
437 .build();
438
439 let extracted = extractor.extract("/api/v1/users/123").unwrap();
440
441 assert_eq!(extracted.get::<String>("version").unwrap(), "v1");
442 assert_eq!(extracted.get_int("id").unwrap(), 123);
443 }
444
445 #[test]
446 fn test_custom_regex_constraint() {
447 use regex::Regex;
448
449 let pattern = RoutePattern::parse("/posts/{slug}").unwrap();
450 let regex = Regex::new(r"^[a-z0-9-]+$").unwrap();
451
452 let extractor = TypedExtractorBuilder::new(pattern)
453 .constraint("slug", ParamConstraint::Custom(regex))
454 .build();
455
456 let result = extractor.extract("/posts/hello-world-123");
458 assert!(result.is_ok());
459
460 let result = extractor.extract("/posts/Hello_World!");
462 assert!(result.is_err());
463 }
464
465 #[test]
466 fn test_all_constraints() {
467 assert!(ParamConstraint::Int.validate("123"));
469 assert!(!ParamConstraint::Int.validate("abc"));
470
471 assert!(ParamConstraint::Alpha.validate("hello"));
472 assert!(!ParamConstraint::Alpha.validate("hello123"));
473
474 assert!(ParamConstraint::Slug.validate("hello-world_123"));
475 assert!(!ParamConstraint::Slug.validate("hello world!"));
476
477 let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
478 assert!(ParamConstraint::Uuid.validate(uuid_str));
479 assert!(!ParamConstraint::Uuid.validate("not-a-uuid"));
480
481 assert!(ParamConstraint::None.validate("anything"));
482 assert!(!ParamConstraint::None.validate("")); }
484
485 #[test]
486 fn test_error_types() {
487 let pattern = RoutePattern::parse("/users/{id:int}").unwrap();
488 let extractor = ParameterExtractor::new(pattern);
489
490 let extracted = extractor.extract("/users/123").unwrap();
491
492 let result: Result<i64, _> = extracted.get("missing");
494 assert!(matches!(result.unwrap_err(), ExtractionError::Missing(_)));
495
496 let pattern2 = RoutePattern::parse("/users/{name}").unwrap();
499 let extractor2 = ParameterExtractor::new(pattern2);
500 let extracted2 = extractor2.extract("/users/john").unwrap();
501
502 let result: Result<i64, _> = extracted2.get("name");
503 assert!(matches!(
504 result.unwrap_err(),
505 ExtractionError::ConversionFailed { .. }
506 ));
507 }
508
509 #[test]
510 fn test_parameter_access_performance() {
511 let pattern = RoutePattern::parse("/users/{id:int}/posts/{slug:alpha}").unwrap();
513
514 let mut raw_params = HashMap::new();
515 raw_params.insert("id".to_string(), "123".to_string());
516 raw_params.insert("slug".to_string(), "helloworld".to_string());
517
518 let extracted = ExtractedParams::from_route_match(raw_params, pattern).unwrap();
519
520 let start = std::time::Instant::now();
521
522 for _ in 0..10000 {
524 let id: i64 = extracted.get("id").unwrap();
525 let slug: String = extracted.get("slug").unwrap();
526 assert_eq!(id, 123);
527 assert_eq!(slug, "helloworld");
528 }
529
530 let elapsed = start.elapsed();
531
532 assert!(
534 elapsed.as_millis() < 50,
535 "Parameter access took too long: {}ms",
536 elapsed.as_millis()
537 );
538
539 println!(
540 "20,000 parameter accesses completed in {}μs",
541 elapsed.as_micros()
542 );
543 }
544
545 #[test]
546 fn test_integration_with_route_matcher() {
547 use super::super::{compiler::RouteCompilerBuilder, HttpMethod};
549
550 let compilation_result = RouteCompilerBuilder::new()
552 .get("users_show".to_string(), "/users/{id:int}".to_string())
553 .get(
554 "posts_show".to_string(),
555 "/posts/{slug:alpha}/comments/{id:uuid}".to_string(),
556 )
557 .build()
558 .unwrap();
559
560 let route_match = compilation_result
562 .matcher
563 .resolve(&HttpMethod::GET, "/users/123")
564 .unwrap();
565
566 assert_eq!(route_match.route_id, "users_show");
567 assert_eq!(route_match.params.get("id"), Some(&"123".to_string()));
568
569 let extractor = compilation_result.extractors.get("users_show").unwrap();
571 let extracted = extractor.extract_from_params(route_match.params).unwrap();
572
573 let user_id: i64 = extracted.get("id").unwrap();
575 assert_eq!(user_id, 123);
576
577 let route_match = compilation_result
580 .matcher
581 .resolve(
582 &HttpMethod::GET,
583 "/posts/helloworld/comments/550e8400-e29b-41d4-a716-446655440000",
584 )
585 .unwrap();
586
587 assert_eq!(route_match.route_id, "posts_show");
588
589 let extractor = compilation_result.extractors.get("posts_show").unwrap();
590 let extracted = extractor.extract_from_params(route_match.params).unwrap();
591
592 let slug: String = extracted.get("slug").unwrap();
593 let comment_id = extracted.get_uuid("id").unwrap();
594
595 assert_eq!(slug, "helloworld");
596 assert_eq!(
597 comment_id.to_string(),
598 "550e8400-e29b-41d4-a716-446655440000"
599 );
600 }
601}