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