Skip to main content

hyperlane_core/route/
impl.rs

1use super::*;
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    /// Fast-path matcher for purely static route patterns.
344    ///
345    /// Walks the request path bytes once, comparing each segment against the
346    /// expected static literal in the pattern. Performs **no Vec allocation**
347    /// and returns an empty `RouteParams` on success.
348    ///
349    /// The caller must guarantee `self.is_purely_static()` is `true` and that
350    /// `path` is non-empty after the leading `/` has been trimmed.
351    ///
352    /// # Arguments
353    ///
354    /// - `&str` - The request path (already trimmed of the leading slash).
355    ///
356    /// # Returns
357    ///
358    /// - `Option<RouteParams>` - `Some` with an empty params map on match, `None` otherwise.
359    fn try_match_static_path(&self, path: &str) -> Option<RouteParams> {
360        let route_segments_len: usize = self.get_0().len();
361        let path_bytes: &[u8] = path.as_bytes();
362        let path_separator_byte: u8 = DEFAULT_HTTP_PATH_BYTES[0];
363        let mut segment_start: usize = 0;
364        let mut matched_segments: usize = 0;
365        let mut saw_content: bool = false;
366        for (byte_index, &current_byte) in path_bytes.iter().enumerate() {
367            if current_byte == path_separator_byte {
368                saw_content = true;
369                let expected: &str = match self.get_0().get(matched_segments) {
370                    Some(RouteSegment::Static(s)) => s.as_str(),
371                    Some(_) => return None,
372                    None => return None,
373                };
374                if &path[segment_start..byte_index] != expected {
375                    return None;
376                }
377                matched_segments += 1;
378                segment_start = byte_index + 1;
379            }
380        }
381        if segment_start < path.len() {
382            saw_content = true;
383            let expected: &str = match self.get_0().get(matched_segments) {
384                Some(RouteSegment::Static(s)) => s.as_str(),
385                Some(_) => return None,
386                None => return None,
387            };
388            if &path[segment_start..] != expected {
389                return None;
390            }
391            matched_segments += 1;
392        }
393        let all_matched: bool = matched_segments == route_segments_len;
394        let empty_single_match: bool = !saw_content && route_segments_len == 1 && path.is_empty();
395        if all_matched || empty_single_match {
396            Some(hash_map_xx_hash3_64())
397        } else {
398            None
399        }
400    }
401
402    /// Matches this route pattern against a request path.
403    ///
404    /// If the pattern matches, extracts any dynamic or regex parameters.
405    ///
406    /// # Arguments
407    ///
408    /// - `&str` - The request path to match against.
409    ///
410    /// # Returns
411    ///
412    /// - `Option<RouteParams>` - Some with parameters if matched, None otherwise.
413    pub(crate) fn try_match_path(&self, path: &str) -> Option<RouteParams> {
414        let path: &str = path.trim_start_matches(DEFAULT_HTTP_PATH);
415        let route_segments_len: usize = self.get_0().len();
416        let is_tail_regex: bool = matches!(self.get_0().last(), Some(RouteSegment::Regex(_, _)));
417        if path.is_empty() {
418            if route_segments_len == 0 {
419                return Some(hash_map_xx_hash3_64());
420            }
421            return None;
422        }
423        if self.is_static() {
424            return self.try_match_static_path(path);
425        }
426        let mut path_segments: PathComponentList = Vec::with_capacity(route_segments_len);
427        let path_bytes: &[u8] = path.as_bytes();
428        let path_separator_byte: u8 = DEFAULT_HTTP_PATH_BYTES[0];
429        let mut segment_start: usize = 0;
430        for (byte_index, &current_byte) in path_bytes.iter().enumerate() {
431            if current_byte == path_separator_byte {
432                if segment_start < byte_index {
433                    path_segments.push(&path[segment_start..byte_index]);
434                }
435                segment_start = byte_index + 1;
436            }
437        }
438        if segment_start < path.len() {
439            path_segments.push(&path[segment_start..]);
440        }
441        let path_segments_len: usize = path_segments.len();
442        if (!is_tail_regex && path_segments_len != route_segments_len)
443            || (is_tail_regex && path_segments_len < route_segments_len - 1)
444        {
445            return None;
446        }
447        let mut params: RouteParams = hash_map_xx_hash3_64();
448        for (idx, segment) in self.get_0().iter().enumerate() {
449            match segment {
450                RouteSegment::Static(expected_path) => {
451                    if path_segments.get(idx).copied() != Some(expected_path.as_str()) {
452                        return None;
453                    }
454                }
455                RouteSegment::Dynamic(param_name) => {
456                    params.insert(param_name.clone(), path_segments.get(idx)?.to_string());
457                }
458                RouteSegment::Regex(param_name, regex) => {
459                    let segment_value: String = if idx == route_segments_len - 1 {
460                        path_segments[idx..].join(DEFAULT_HTTP_PATH)
461                    } else {
462                        {
463                            let val = path_segments.get(idx)?;
464                            val.to_string()
465                        }
466                    };
467                    {
468                        let mat = regex.find(&segment_value)?;
469                        if mat.start() != 0 || mat.end() != segment_value.len() {
470                            return None;
471                        }
472                    }
473                    params.insert(param_name.clone(), segment_value);
474                    if idx == route_segments_len - 1 {
475                        return Some(params);
476                    }
477                }
478            }
479        }
480        Some(params)
481    }
482
483    /// Checks if the route pattern is static.
484    ///
485    /// # Returns
486    ///
487    /// - `bool` - true if the pattern is static, false otherwise.
488    #[inline(always)]
489    pub(crate) fn is_static(&self) -> bool {
490        self.get_0()
491            .iter()
492            .all(|segment: &RouteSegment| matches!(segment, RouteSegment::Static(_)))
493    }
494
495    /// Checks if the route pattern is dynamic.
496    ///
497    /// # Returns
498    ///
499    /// - `bool` - true if the pattern is dynamic, false otherwise.
500    #[inline(always)]
501    pub(crate) fn is_dynamic(&self) -> bool {
502        self.get_0()
503            .iter()
504            .any(|segment: &RouteSegment| matches!(segment, RouteSegment::Dynamic(_)))
505            && self
506                .get_0()
507                .iter()
508                .all(|segment: &RouteSegment| !matches!(segment, RouteSegment::Regex(_, _)))
509    }
510
511    /// Gets the number of segments in this route pattern.
512    ///
513    /// # Returns
514    ///
515    /// - `usize` - The number of segments.
516    #[inline(always)]
517    pub(crate) fn segment_count(&self) -> usize {
518        self.get_0().len()
519    }
520
521    /// Checks if the last segment is a regex pattern.
522    ///
523    /// # Returns
524    ///
525    /// - `bool` - true if the last segment is a regex, false otherwise.
526    #[inline(always)]
527    pub(crate) fn has_tail_regex(&self) -> bool {
528        matches!(self.get_0().last(), Some(RouteSegment::Regex(_, _)))
529    }
530}
531
532/// Manages a collection of route, enabling efficient lookup and dispatch.
533///
534/// This struct stores route categorized by type (static, dynamic, regex)
535/// to quickly find the appropriate hook for incoming requests.
536impl RouteMatcher {
537    /// Creates a new, empty RouteMatcher.
538    ///
539    /// # Returns
540    ///
541    /// - `RouteMatcher` - A new RouteMatcher instance with empty route stores.
542    #[inline(always)]
543    pub(crate) fn new() -> Self {
544        Self::default()
545    }
546
547    /// Counts the number of segments in a path.
548    ///
549    /// # Arguments
550    ///
551    /// - `&str` - The path to count segments in.
552    ///
553    /// # Returns
554    ///
555    /// - `usize` - The number of segments.
556    #[inline(always)]
557    fn count_path_segments(path: &str) -> usize {
558        let path: &str = path.trim_start_matches(DEFAULT_HTTP_PATH);
559        if path.is_empty() {
560            return 0;
561        }
562        path.matches(DEFAULT_HTTP_PATH).count() + 1
563    }
564
565    /// Adds a new route and its hook to the matcher.
566    ///
567    /// Adds a route hook to the matcher.
568    ///
569    /// This method categorizes the route as static, dynamic, or regex based on its pattern
570    /// and stores it in the appropriate collection.
571    ///
572    /// # Arguments
573    ///
574    /// - `&str` - The route pattern string.
575    /// - `ServerHookHandler` - The boxed route hook.
576    ///
577    /// # Returns
578    ///
579    /// - `Result<(), RouteError>` - Ok on success, or RouteError if pattern is duplicate.
580    pub(crate) fn add(&mut self, pattern: &str, hook: ServerHookHandler) -> Result<(), RouteError> {
581        let route_pattern: RoutePattern = RoutePattern::new(pattern)?;
582        if route_pattern.is_static() {
583            if self.get_static_route().contains_key(pattern) {
584                return Err(RouteError::DuplicatePattern(pattern.to_owned()));
585            }
586            self.get_mut_static_route()
587                .insert(pattern.to_string(), hook);
588            return Ok(());
589        }
590        let target_map: &mut ServerHookPatternRoute = if route_pattern.is_dynamic() {
591            self.get_mut_dynamic_route()
592        } else {
593            self.get_mut_regex_route()
594        };
595        let segment_count: usize = route_pattern.segment_count();
596        let routes_for_count: &mut Vec<(RoutePattern, ServerHookHandler)> =
597            target_map.entry(segment_count).or_default();
598        match routes_for_count.binary_search_by(|entry: &(RoutePattern, ServerHookHandler)| {
599            entry.0.cmp(&route_pattern)
600        }) {
601            Ok(_) => return Err(RouteError::DuplicatePattern(pattern.to_owned())),
602            Err(pos) => routes_for_count.insert(pos, (route_pattern, hook)),
603        }
604        Ok(())
605    }
606
607    /// Resolves a route hook by reference (no Arc::clone) for hot-path use.
608    ///
609    /// Returns a reference to the matched `ServerHookHandler` if any. The
610    /// caller must clone if it needs to retain the hook beyond the await point.
611    /// Returns `None` if no route matched.
612    ///
613    /// # Arguments
614    ///
615    /// - `&mut Context` - The request context (for storing route params).
616    /// - `&str` - The request path to resolve.
617    pub fn try_resolve_route<'a>(
618        &'a self,
619        ctx: &mut Context,
620        path: &str,
621    ) -> Option<&'a ServerHookHandler> {
622        if let Some(hook) = self.get_static_route().get(path) {
623            return Some(hook);
624        }
625        let path_segment_count: usize = Self::count_path_segments(path);
626        if let Some(routes) = self.get_dynamic_route().get(&path_segment_count) {
627            for (pattern, hook) in routes {
628                if let Some(params) = pattern.try_match_path(path) {
629                    ctx.set_route_params(params);
630                    return Some(hook);
631                }
632            }
633        }
634        if let Some(routes) = self.get_regex_route().get(&path_segment_count) {
635            for (pattern, hook) in routes {
636                if let Some(params) = pattern.try_match_path(path) {
637                    ctx.set_route_params(params);
638                    return Some(hook);
639                }
640            }
641        }
642        for (&segment_count, routes) in self.get_regex_route() {
643            if segment_count >= path_segment_count {
644                continue;
645            }
646            for (pattern, hook) in routes {
647                if pattern.has_tail_regex()
648                    && let Some(params) = pattern.try_match_path(path)
649                {
650                    ctx.set_route_params(params);
651                    return Some(hook);
652                }
653            }
654        }
655        None
656    }
657}