Skip to main content

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}