fastapi_router/match.rs
1//! Route matching result.
2
3use crate::trie::Route;
4use fastapi_core::Method;
5use std::num::{ParseFloatError, ParseIntError};
6
7/// A matched route with extracted parameters.
8#[derive(Debug)]
9pub struct RouteMatch<'a> {
10 /// The matched route.
11 pub route: &'a Route,
12 /// Extracted path parameters.
13 pub params: Vec<(&'a str, &'a str)>,
14}
15
16impl<'a> RouteMatch<'a> {
17 /// Get a parameter value by name as a string slice.
18 #[must_use]
19 pub fn get_param(&self, name: &str) -> Option<&str> {
20 self.params
21 .iter()
22 .find(|(n, _)| *n == name)
23 .map(|(_, v)| *v)
24 }
25
26 /// Get a parameter value parsed as an i64 integer.
27 ///
28 /// Returns `None` if the parameter doesn't exist.
29 /// Returns `Some(Err(_))` if the parameter exists but can't be parsed as i64.
30 ///
31 /// # Example
32 ///
33 /// ```ignore
34 /// // Route: /users/{id:int}
35 /// if let Some(Ok(id)) = route_match.get_param_int("id") {
36 /// println!("User ID: {id}");
37 /// }
38 /// ```
39 #[must_use]
40 pub fn get_param_int(&self, name: &str) -> Option<Result<i64, ParseIntError>> {
41 self.get_param(name).map(str::parse)
42 }
43
44 /// Get a parameter value parsed as an i32 integer.
45 ///
46 /// Returns `None` if the parameter doesn't exist.
47 /// Returns `Some(Err(_))` if the parameter exists but can't be parsed as i32.
48 #[must_use]
49 pub fn get_param_i32(&self, name: &str) -> Option<Result<i32, ParseIntError>> {
50 self.get_param(name).map(str::parse)
51 }
52
53 /// Get a parameter value parsed as a u64 unsigned integer.
54 ///
55 /// Returns `None` if the parameter doesn't exist.
56 /// Returns `Some(Err(_))` if the parameter exists but can't be parsed as u64.
57 #[must_use]
58 pub fn get_param_u64(&self, name: &str) -> Option<Result<u64, ParseIntError>> {
59 self.get_param(name).map(str::parse)
60 }
61
62 /// Get a parameter value parsed as a u32 unsigned integer.
63 ///
64 /// Returns `None` if the parameter doesn't exist.
65 /// Returns `Some(Err(_))` if the parameter exists but can't be parsed as u32.
66 #[must_use]
67 pub fn get_param_u32(&self, name: &str) -> Option<Result<u32, ParseIntError>> {
68 self.get_param(name).map(str::parse)
69 }
70
71 /// Get a parameter value parsed as an f64 float.
72 ///
73 /// Returns `None` if the parameter doesn't exist.
74 /// Returns `Some(Err(_))` if the parameter exists but can't be parsed as f64.
75 ///
76 /// # Example
77 ///
78 /// ```ignore
79 /// // Route: /values/{val:float}
80 /// if let Some(Ok(val)) = route_match.get_param_float("val") {
81 /// println!("Value: {val}");
82 /// }
83 /// ```
84 #[must_use]
85 pub fn get_param_float(&self, name: &str) -> Option<Result<f64, ParseFloatError>> {
86 self.get_param(name).map(str::parse)
87 }
88
89 /// Get a parameter value parsed as an f32 float.
90 ///
91 /// Returns `None` if the parameter doesn't exist.
92 /// Returns `Some(Err(_))` if the parameter exists but can't be parsed as f32.
93 #[must_use]
94 pub fn get_param_f32(&self, name: &str) -> Option<Result<f32, ParseFloatError>> {
95 self.get_param(name).map(str::parse)
96 }
97
98 /// Check if a parameter value is a valid UUID format.
99 ///
100 /// Returns `None` if the parameter doesn't exist.
101 /// Returns `Some(true)` if the parameter is a valid UUID.
102 /// Returns `Some(false)` if the parameter exists but isn't a valid UUID.
103 #[must_use]
104 pub fn is_param_uuid(&self, name: &str) -> Option<bool> {
105 self.get_param(name).map(is_valid_uuid)
106 }
107
108 /// Get parameter count.
109 #[must_use]
110 pub fn param_count(&self) -> usize {
111 self.params.len()
112 }
113
114 /// Check if there are no parameters.
115 #[must_use]
116 pub fn is_empty(&self) -> bool {
117 self.params.is_empty()
118 }
119
120 /// Iterate over all parameters as (name, value) pairs.
121 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
122 self.params.iter().map(|(n, v)| (*n, *v))
123 }
124}
125
126/// Check if a string is a valid UUID format (8-4-4-4-12 hex digits).
127fn is_valid_uuid(s: &str) -> bool {
128 if s.len() != 36 {
129 return false;
130 }
131 let parts: Vec<_> = s.split('-').collect();
132 if parts.len() != 5 {
133 return false;
134 }
135 parts[0].len() == 8
136 && parts[1].len() == 4
137 && parts[2].len() == 4
138 && parts[3].len() == 4
139 && parts[4].len() == 12
140 && parts
141 .iter()
142 .all(|p| p.chars().all(|c| c.is_ascii_hexdigit()))
143}
144
145/// Result of attempting to locate a route by path and method.
146#[derive(Debug)]
147pub enum RouteLookup<'a> {
148 /// A route matched by path and method.
149 Match(RouteMatch<'a>),
150 /// Path matched, but method is not allowed.
151 MethodNotAllowed { allowed: AllowedMethods },
152 /// No route matched the path.
153 NotFound,
154}
155
156/// Allowed methods for a matched path.
157#[derive(Debug, Clone)]
158pub struct AllowedMethods {
159 methods: Vec<Method>,
160}
161
162impl AllowedMethods {
163 /// Create a normalized allow list.
164 ///
165 /// - Adds `HEAD` if `GET` is present.
166 /// - Sorts and de-duplicates for stable output.
167 #[must_use]
168 pub fn new(mut methods: Vec<Method>) -> Self {
169 if methods.contains(&Method::Get) && !methods.contains(&Method::Head) {
170 methods.push(Method::Head);
171 }
172 methods.sort_by_key(method_order);
173 methods.dedup();
174 Self { methods }
175 }
176
177 /// Access the normalized methods.
178 #[must_use]
179 pub fn methods(&self) -> &[Method] {
180 &self.methods
181 }
182
183 /// Check whether a method is allowed.
184 #[must_use]
185 pub fn contains(&self, method: Method) -> bool {
186 self.methods.contains(&method)
187 }
188
189 /// Format as an HTTP Allow header value.
190 #[must_use]
191 pub fn header_value(&self) -> String {
192 let mut out = String::new();
193 for (idx, method) in self.methods.iter().enumerate() {
194 if idx > 0 {
195 out.push_str(", ");
196 }
197 out.push_str(method.as_str());
198 }
199 out
200 }
201}
202
203fn method_order(method: &Method) -> u8 {
204 match *method {
205 Method::Get => 0,
206 Method::Head => 1,
207 Method::Post => 2,
208 Method::Put => 3,
209 Method::Delete => 4,
210 Method::Patch => 5,
211 Method::Options => 6,
212 Method::Trace => 7,
213 }
214}