Skip to main content

hyperlane/route/
impl.rs

1use crate::*;
2
3// Associate a plugin registry with the specified type.
4collect!(HookType);
5
6/// Provides a default implementation for RouteMatcher.
7impl Default for RouteMatcher {
8    /// Creates a new, empty RouteMatcher.
9    ///
10    /// # Returns
11    ///
12    /// - `RouteMatcher` - A new RouteMatcher with empty storage for static, dynamic, and regex route.
13    #[inline(always)]
14    fn default() -> Self {
15        Self {
16            static_route: hash_map_xx_hash3_64(),
17            dynamic_route: hash_map_xx_hash3_64(),
18            regex_route: hash_map_xx_hash3_64(),
19        }
20    }
21}
22
23/// Implements the `PartialEq` trait for `RoutePattern`.
24///
25/// This allows for comparing two `RoutePattern` instances for equality.
26impl PartialEq for RoutePattern {
27    /// Checks if two `RoutePattern` instances are equal.
28    ///
29    /// # Arguments
30    ///
31    /// - `&Self` - The other `RoutePattern` instance to compare against.
32    ///
33    /// # Returns
34    ///
35    /// - `bool`- `true` if the instances are equal, `false` otherwise.
36    #[inline(always)]
37    fn eq(&self, other: &Self) -> bool {
38        self.get_0() == other.get_0()
39    }
40}
41
42/// Implements the `Eq` trait for `RoutePattern`.
43///
44/// This indicates that `RoutePattern` has a total equality relation.
45impl Eq for RoutePattern {}
46
47/// Implements the `Hash` trait for `RoutePattern`.
48///
49/// This allows `RoutePattern` to be used as a key in hash-based collections.
50impl Hash for RoutePattern {
51    /// Hashes the `RoutePattern` instance.
52    ///
53    /// # Arguments
54    ///
55    /// - `&mut Hasher` - The hasher to use.
56    #[inline(always)]
57    fn hash<H: Hasher>(&self, state: &mut H) {
58        self.get_0().hash(state);
59    }
60}
61
62/// Implements the `PartialOrd` trait for `RoutePattern`.
63///
64/// This allows for partial ordering of `RoutePattern` instances.
65impl PartialOrd for RoutePattern {
66    /// Partially compares two `RoutePattern` instances.
67    ///
68    /// # Arguments
69    ///
70    /// - `&Self`- The other `RoutePattern` instance to compare against.
71    ///
72    /// # Returns
73    ///
74    /// - `Option<Ordering>`- The ordering of the two instances.
75    #[inline(always)]
76    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
77        Some(self.cmp(other))
78    }
79}
80
81/// Implements the `Ord` trait for `RoutePattern`.
82///
83/// This allows for total ordering of `RoutePattern` instances.
84impl Ord for RoutePattern {
85    /// Compares two `RoutePattern` instances.
86    ///
87    /// # Arguments
88    ///
89    /// - `&Self`- The other `RoutePattern` instance to compare against.
90    ///
91    /// # Returns
92    ///
93    /// - `Ordering`- The ordering of the two instances.
94    #[inline(always)]
95    fn cmp(&self, other: &Self) -> Ordering {
96        self.get_0().cmp(other.get_0())
97    }
98}
99
100/// Implements the `PartialEq` trait for `RouteMatcher`.
101///
102/// This allows for comparing two `RouteMatcher` instances for equality.
103impl PartialEq for RouteMatcher {
104    /// Checks if two `RouteMatcher` instances are equal.
105    ///
106    /// # Arguments
107    ///
108    /// - `&Self`- The other `RouteMatcher` instance to compare against.
109    ///
110    /// # Returns
111    ///
112    /// - `bool`- `true` if the instances are equal, `false` otherwise.
113    fn eq(&self, other: &Self) -> bool {
114        if self.get_static_route().len() != other.get_static_route().len() {
115            return false;
116        }
117        for key in self.get_static_route().keys() {
118            if !other.get_static_route().contains_key(key) {
119                return false;
120            }
121        }
122        if self.get_dynamic_route().len() != other.get_dynamic_route().len() {
123            return false;
124        }
125        for (segment_count, routes) in self.get_dynamic_route() {
126            match other.get_dynamic_route().get(segment_count) {
127                Some(other_routes) if routes.len() == other_routes.len() => {
128                    for (pattern, _) in routes {
129                        if !other_routes
130                            .iter()
131                            .any(|entry: &(RoutePattern, ServerHookHandler)| &entry.0 == pattern)
132                        {
133                            return false;
134                        }
135                    }
136                }
137                _ => return false,
138            }
139        }
140        if self.get_regex_route().len() != other.get_regex_route().len() {
141            return false;
142        }
143        for (segment_count, routes) in self.get_regex_route() {
144            match other.get_regex_route().get(segment_count) {
145                Some(other_routes) if routes.len() == other_routes.len() => {
146                    for (pattern, _) in routes {
147                        if !other_routes
148                            .iter()
149                            .any(|entry: &(RoutePattern, ServerHookHandler)| &entry.0 == pattern)
150                        {
151                            return false;
152                        }
153                    }
154                }
155                _ => return false,
156            }
157        }
158        true
159    }
160}
161
162/// Implements the `Eq` trait for `RouteMatcher`.
163///
164/// This indicates that `RouteMatcher` has a total equality relation.
165impl Eq for RouteMatcher {}
166
167/// Implements the `Eq` trait for `RouteSegment`.
168///
169/// This indicates that `RouteSegment` has a total equality relation.
170impl Eq for RouteSegment {}
171
172/// Implements the `PartialOrd` trait for `RouteSegment`.
173///
174/// This allows for partial ordering of `RouteSegment` instances.
175impl PartialOrd for RouteSegment {
176    /// Partially compares two `RouteSegment` instances.
177    ///
178    /// # Arguments
179    ///
180    /// - `&Self`- The other `RouteSegment` instance to compare against.
181    ///
182    /// # Returns
183    ///
184    /// - `Option<Ordering>`- The ordering of the two instances.
185    #[inline(always)]
186    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
187        Some(self.cmp(other))
188    }
189}
190
191/// Implements the `Ord` trait for `RouteSegment`.
192///
193/// This allows for total ordering of `RouteSegment` instances.
194impl Ord for RouteSegment {
195    /// Compares two `RouteSegment` instances.
196    ///
197    /// # Arguments
198    ///
199    /// - `&Self`- The other `RouteSegment` instance to compare against.
200    ///
201    /// # Returns
202    ///
203    /// - `Ordering`- The ordering of the two instances.
204    #[inline(always)]
205    fn cmp(&self, other: &Self) -> Ordering {
206        match (self, other) {
207            (Self::Static(left_static), Self::Static(right_static)) => {
208                left_static.cmp(right_static)
209            }
210            (Self::Dynamic(left_dynamic), Self::Dynamic(right_dynamic)) => {
211                left_dynamic.cmp(right_dynamic)
212            }
213            (Self::Regex(left_name, left_regex), Self::Regex(right_name, right_regex)) => left_name
214                .cmp(right_name)
215                .then_with(|| left_regex.as_str().cmp(right_regex.as_str())),
216            (Self::Static(_), _) => Ordering::Less,
217            (_, Self::Static(_)) => Ordering::Greater,
218            (Self::Dynamic(_), _) => Ordering::Less,
219            (_, Self::Dynamic(_)) => Ordering::Greater,
220        }
221    }
222}
223
224/// Implements the `PartialEq` trait for `RouteSegment`.
225///
226/// This allows for comparing two `RouteSegment` instances for equality.
227impl PartialEq for RouteSegment {
228    /// Checks if two `RouteSegment` instances are equal.
229    ///
230    /// # Arguments
231    ///
232    /// - `&Self`- The other `RouteSegment` instance to compare against.
233    ///
234    /// # Returns
235    ///
236    /// - `bool`- `true` if the instances are equal, `false` otherwise.
237    #[inline(always)]
238    fn eq(&self, other: &Self) -> bool {
239        match (self, other) {
240            (Self::Static(left_value), Self::Static(right_value)) => left_value == right_value,
241            (Self::Dynamic(left_value), Self::Dynamic(right_value)) => left_value == right_value,
242            (Self::Regex(left_name, left_regex), Self::Regex(right_name, right_regex)) => {
243                left_name == right_name && left_regex.as_str() == right_regex.as_str()
244            }
245            _ => false,
246        }
247    }
248}
249
250/// Implements the `Hash` trait for `RouteSegment`.
251///
252/// This allows `RouteSegment` to be used in hash-based collections.
253impl Hash for RouteSegment {
254    /// Hashes the `RouteSegment` instance.
255    ///
256    /// # Arguments
257    ///
258    /// - `&mut Hasher` - The hasher to use.
259    #[inline(always)]
260    fn hash<H: Hasher>(&self, state: &mut H) {
261        match self {
262            Self::Static(static_value) => {
263                0u8.hash(state);
264                static_value.hash(state);
265            }
266            Self::Dynamic(dynamic_value) => {
267                1u8.hash(state);
268                dynamic_value.hash(state);
269            }
270            Self::Regex(name, regex) => {
271                2u8.hash(state);
272                name.hash(state);
273                regex.as_str().hash(state);
274            }
275        }
276    }
277}
278
279/// Manages route patterns, including parsing and matching.
280///
281/// This struct is responsible for defining and validating route structures,
282/// supporting static, dynamic, and regex-based path matching.
283impl RoutePattern {
284    /// Creates a new RoutePattern by parsing a route string.
285    ///
286    /// # Arguments
287    ///
288    /// - `&str` - The raw route string to parse.
289    ///
290    /// # Returns
291    ///
292    /// - `Result<RoutePattern, RouteError>` - The parsed RoutePattern on success, or RouteError on failure.
293    pub(crate) fn new(route: &str) -> Result<RoutePattern, RouteError> {
294        Ok(Self(Self::parse_route(route)?))
295    }
296
297    /// Parses a raw route string into RouteSegments.
298    ///
299    /// This is the core logic for interpreting the route syntax.
300    ///
301    /// # Arguments
302    ///
303    /// - `&str` - The raw route string.
304    ///
305    /// # Returns
306    ///
307    /// - `Result<RouteSegmentList, RouteError>` - Vector of RouteSegments on success, or RouteError on failure.
308    fn parse_route(route: &str) -> Result<RouteSegmentList, RouteError> {
309        if route.is_empty() {
310            return Err(RouteError::EmptyPattern);
311        }
312        let route: &str = route.trim_start_matches(DEFAULT_HTTP_PATH);
313        if route.is_empty() {
314            return Ok(Vec::new());
315        }
316        let estimated_segments: usize = route.matches(DEFAULT_HTTP_PATH).count() + 1;
317        let mut segments: RouteSegmentList = Vec::with_capacity(estimated_segments);
318        for segment in route.split(DEFAULT_HTTP_PATH) {
319            if segment.starts_with(LEFT_BRACKET) && segment.ends_with(RIGHT_BRACKET) {
320                let content: &str = &segment[1..segment.len() - 1];
321                if let Some((name, pattern)) = content.split_once(COLON) {
322                    match Regex::new(pattern) {
323                        Ok(regex) => {
324                            segments.push(RouteSegment::Regex(name.to_owned(), regex));
325                        }
326                        Err(error) => {
327                            return Err(RouteError::InvalidRegexPattern(format!(
328                                "Invalid regex pattern '{}{}{}",
329                                pattern, COLON, error
330                            )));
331                        }
332                    }
333                } else {
334                    segments.push(RouteSegment::Dynamic(content.to_owned()));
335                }
336            } else {
337                segments.push(RouteSegment::Static(segment.to_owned()));
338            }
339        }
340        Ok(segments)
341    }
342
343    /// Matches this route pattern against a request path.
344    ///
345    /// If the pattern matches, extracts any dynamic or regex parameters.
346    ///
347    /// # Arguments
348    ///
349    /// - `&str` - The request path to match against.
350    ///
351    /// # Returns
352    ///
353    /// - `Option<RouteParams>` - Some with parameters if matched, None otherwise.
354    pub(crate) fn try_match_path(&self, path: &str) -> Option<RouteParams> {
355        let path: &str = path.trim_start_matches(DEFAULT_HTTP_PATH);
356        let route_segments_len: usize = self.get_0().len();
357        let is_tail_regex: bool = matches!(self.get_0().last(), Some(RouteSegment::Regex(_, _)));
358        if path.is_empty() {
359            if route_segments_len == 0 {
360                return Some(hash_map_xx_hash3_64());
361            }
362            return None;
363        }
364        let mut path_segments: PathComponentList = Vec::with_capacity(route_segments_len);
365        let path_bytes: &[u8] = path.as_bytes();
366        let path_separator_byte: u8 = DEFAULT_HTTP_PATH_BYTES[0];
367        let mut segment_start: usize = 0;
368        for (byte_index, &current_byte) in path_bytes.iter().enumerate() {
369            if current_byte == path_separator_byte {
370                if segment_start < byte_index {
371                    path_segments.push(&path[segment_start..byte_index]);
372                }
373                segment_start = byte_index + 1;
374            }
375        }
376        if segment_start < path.len() {
377            path_segments.push(&path[segment_start..]);
378        }
379        let path_segments_len: usize = path_segments.len();
380        if (!is_tail_regex && path_segments_len != route_segments_len)
381            || (is_tail_regex && path_segments_len < route_segments_len - 1)
382        {
383            return None;
384        }
385        let mut params: RouteParams = hash_map_xx_hash3_64();
386        for (idx, segment) in self.get_0().iter().enumerate() {
387            match segment {
388                RouteSegment::Static(expected_path) => {
389                    if path_segments.get(idx).copied() != Some(expected_path.as_str()) {
390                        return None;
391                    }
392                }
393                RouteSegment::Dynamic(param_name) => {
394                    params.insert(param_name.clone(), path_segments.get(idx)?.to_string());
395                }
396                RouteSegment::Regex(param_name, regex) => {
397                    let segment_value: String = if idx == route_segments_len - 1 {
398                        path_segments[idx..].join(DEFAULT_HTTP_PATH)
399                    } else {
400                        match path_segments.get(idx) {
401                            Some(val) => val.to_string(),
402                            None => return None,
403                        }
404                    };
405                    if let Some(mat) = regex.find(&segment_value) {
406                        if mat.start() != 0 || mat.end() != segment_value.len() {
407                            return None;
408                        }
409                    } else {
410                        return None;
411                    }
412                    params.insert(param_name.clone(), segment_value);
413                    if idx == route_segments_len - 1 {
414                        return Some(params);
415                    }
416                }
417            }
418        }
419        Some(params)
420    }
421
422    /// Checks if the route pattern is static.
423    ///
424    /// # Returns
425    ///
426    /// - `bool` - true if the pattern is static, false otherwise.
427    #[inline(always)]
428    pub(crate) fn is_static(&self) -> bool {
429        self.get_0()
430            .iter()
431            .all(|segment: &RouteSegment| matches!(segment, RouteSegment::Static(_)))
432    }
433
434    /// Checks if the route pattern is dynamic.
435    ///
436    /// # Returns
437    ///
438    /// - `bool` - true if the pattern is dynamic, false otherwise.
439    #[inline(always)]
440    pub(crate) fn is_dynamic(&self) -> bool {
441        self.get_0()
442            .iter()
443            .any(|segment: &RouteSegment| matches!(segment, RouteSegment::Dynamic(_)))
444            && self
445                .get_0()
446                .iter()
447                .all(|segment: &RouteSegment| !matches!(segment, RouteSegment::Regex(_, _)))
448    }
449
450    /// Gets the number of segments in this route pattern.
451    ///
452    /// # Returns
453    ///
454    /// - `usize` - The number of segments.
455    #[inline(always)]
456    pub(crate) fn segment_count(&self) -> usize {
457        self.get_0().len()
458    }
459
460    /// Checks if the last segment is a regex pattern.
461    ///
462    /// # Returns
463    ///
464    /// - `bool` - true if the last segment is a regex, false otherwise.
465    #[inline(always)]
466    pub(crate) fn has_tail_regex(&self) -> bool {
467        matches!(self.get_0().last(), Some(RouteSegment::Regex(_, _)))
468    }
469}
470
471/// Manages a collection of route, enabling efficient lookup and dispatch.
472///
473/// This struct stores route categorized by type (static, dynamic, regex)
474/// to quickly find the appropriate hook for incoming requests.
475impl RouteMatcher {
476    /// Creates a new, empty RouteMatcher.
477    ///
478    /// # Returns
479    ///
480    /// - `RouteMatcher` - A new RouteMatcher instance with empty route stores.
481    #[inline(always)]
482    pub(crate) fn new() -> Self {
483        Self::default()
484    }
485
486    /// Counts the number of segments in a path.
487    ///
488    /// # Arguments
489    ///
490    /// - `&str` - The path to count segments in.
491    ///
492    /// # Returns
493    ///
494    /// - `usize` - The number of segments.
495    #[inline(always)]
496    fn count_path_segments(path: &str) -> usize {
497        let path: &str = path.trim_start_matches(DEFAULT_HTTP_PATH);
498        if path.is_empty() {
499            return 0;
500        }
501        path.matches(DEFAULT_HTTP_PATH).count() + 1
502    }
503
504    /// Adds a new route and its hook to the matcher.
505    ///
506    /// Adds a route hook to the matcher.
507    ///
508    /// This method categorizes the route as static, dynamic, or regex based on its pattern
509    /// and stores it in the appropriate collection.
510    ///
511    /// # Arguments
512    ///
513    /// - `&str` - The route pattern string.
514    /// - `ServerHookHandler` - The boxed route hook.
515    ///
516    /// # Returns
517    ///
518    /// - `Result<(), RouteError>` - Ok on success, or RouteError if pattern is duplicate.
519    pub(crate) fn add(&mut self, pattern: &str, hook: ServerHookHandler) -> Result<(), RouteError> {
520        let route_pattern: RoutePattern = RoutePattern::new(pattern)?;
521        if route_pattern.is_static() {
522            if self.get_static_route().contains_key(pattern) {
523                return Err(RouteError::DuplicatePattern(pattern.to_owned()));
524            }
525            self.get_mut_static_route()
526                .insert(pattern.to_string(), hook);
527            return Ok(());
528        }
529        let target_map: &mut ServerHookPatternRoute = if route_pattern.is_dynamic() {
530            self.get_mut_dynamic_route()
531        } else {
532            self.get_mut_regex_route()
533        };
534        let segment_count: usize = route_pattern.segment_count();
535        let routes_for_count: &mut Vec<(RoutePattern, ServerHookHandler)> =
536            target_map.entry(segment_count).or_default();
537        match routes_for_count.binary_search_by(|entry: &(RoutePattern, ServerHookHandler)| {
538            entry.0.cmp(&route_pattern)
539        }) {
540            Ok(_) => return Err(RouteError::DuplicatePattern(pattern.to_owned())),
541            Err(pos) => routes_for_count.insert(pos, (route_pattern, hook)),
542        }
543        Ok(())
544    }
545
546    /// Resolves and executes a route hook.
547    ///
548    /// This method searches for a matching route and executes it if found.
549    /// Finds a matching route hook for the given path.
550    ///
551    /// # Arguments
552    ///
553    /// - `&mut Context` - The request context.
554    /// - `&str` - The request path to resolve.
555    ///
556    /// # Returns
557    ///
558    /// - `Option<ServerHookHandler>` - The matched route hook if found, None otherwise.
559    pub fn try_resolve_route(&self, ctx: &mut Context, path: &str) -> Option<ServerHookHandler> {
560        if let Some(hook) = self.get_static_route().get(path) {
561            ctx.set_route_params(RouteParams::default());
562            return Some(hook.clone());
563        }
564        let path_segment_count: usize = Self::count_path_segments(path);
565        if let Some(routes) = self.get_dynamic_route().get(&path_segment_count) {
566            for (pattern, hook) in routes {
567                if let Some(params) = pattern.try_match_path(path) {
568                    ctx.set_route_params(params);
569                    return Some(hook.clone());
570                }
571            }
572        }
573        if let Some(routes) = self.get_regex_route().get(&path_segment_count) {
574            for (pattern, hook) in routes {
575                if let Some(params) = pattern.try_match_path(path) {
576                    ctx.set_route_params(params);
577                    return Some(hook.clone());
578                }
579            }
580        }
581        for (&segment_count, routes) in self.get_regex_route() {
582            if segment_count == path_segment_count {
583                continue;
584            }
585            for (pattern, hook) in routes {
586                if pattern.has_tail_regex()
587                    && path_segment_count >= segment_count
588                    && let Some(params) = pattern.try_match_path(path)
589                {
590                    ctx.set_route_params(params);
591                    return Some(hook.clone());
592                }
593            }
594        }
595        None
596    }
597}