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, ¤t_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}