1use ic_http_certification::{HttpRequest, HttpResponse, Method};
2use std::collections::HashMap;
3
4use crate::middleware::MiddlewareFn;
5use crate::route_config::RouteConfig;
6
7pub type RouteParams = HashMap<String, String>;
14
15pub type HandlerFn = fn(HttpRequest, RouteParams) -> HttpResponse<'static>;
21
22type MatchResult<'a> = (
27 &'a HashMap<Method, HandlerFn>,
28 &'a HashMap<Method, HandlerResultFn>,
29 RouteParams,
30 String,
31);
32
33pub type HandlerResultFn = fn(HttpRequest, RouteParams) -> HandlerResult;
55
56pub enum HandlerResult {
77 Response(HttpResponse<'static>),
79
80 NotModified,
83}
84
85impl From<HttpResponse<'static>> for HandlerResult {
86 fn from(resp: HttpResponse<'static>) -> Self {
87 HandlerResult::Response(resp)
88 }
89}
90
91#[derive(Debug, PartialEq, Eq)]
97pub enum NodeType {
98 Static(String),
100 Param(String),
103 Wildcard,
106}
107
108pub enum RouteResult {
113 Found(HandlerFn, RouteParams, Option<HandlerResultFn>, String),
121 MethodNotAllowed(Vec<Method>),
124 NotFound,
126}
127
128pub struct RouteNode {
138 pub node_type: NodeType,
140 pub static_children: HashMap<String, RouteNode>,
143 pub param_child: Option<Box<RouteNode>>,
147 pub wildcard_child: Option<Box<RouteNode>>,
150 pub handlers: HashMap<Method, HandlerFn>,
153 result_handlers: HashMap<Method, HandlerResultFn>,
158 middlewares: Vec<(String, MiddlewareFn)>,
163 not_found_handler: Option<HandlerFn>,
167 route_configs: HashMap<String, RouteConfig>,
172}
173
174impl RouteNode {
175 pub fn new(node_type: NodeType) -> Self {
177 Self {
178 node_type,
179 static_children: HashMap::new(),
180 param_child: None,
181 wildcard_child: None,
182 handlers: HashMap::new(),
183 result_handlers: HashMap::new(),
184 middlewares: Vec::new(),
185 not_found_handler: None,
186 route_configs: HashMap::new(),
187 }
188 }
189
190 pub fn set_middleware(&mut self, prefix: &str, mw: MiddlewareFn) {
199 let normalized = normalize_prefix(prefix);
200 if let Some(entry) = self.middlewares.iter_mut().find(|(p, _)| *p == normalized) {
201 entry.1 = mw;
202 } else {
203 self.middlewares.push((normalized, mw));
204 }
205 self.middlewares.sort_by_key(|(p, _)| segment_count(p));
207 }
208
209 pub fn set_not_found(&mut self, handler: HandlerFn) {
218 self.not_found_handler = Some(handler);
219 }
220
221 pub fn not_found_handler(&self) -> Option<HandlerFn> {
223 self.not_found_handler
224 }
225
226 pub fn set_route_config(&mut self, path: &str, config: RouteConfig) {
234 self.route_configs.insert(path.to_string(), config);
235 }
236
237 pub fn get_route_config(&self, path: &str) -> Option<&RouteConfig> {
242 self.route_configs.get(path)
243 }
244
245 pub fn skip_certified_paths(&self) -> Vec<String> {
252 self.route_configs
253 .iter()
254 .filter(|(_, config)| {
255 matches!(
256 config.certification,
257 crate::certification::CertificationMode::Skip
258 )
259 })
260 .map(|(path, _)| path.clone())
261 .collect()
262 }
263
264 pub fn insert(&mut self, path: &str, method: Method, handler: HandlerFn) {
270 let node = self.get_or_create_node(path);
271 node.handlers.insert(method, handler);
272 }
273
274 pub fn insert_result(&mut self, path: &str, method: Method, handler: HandlerResultFn) {
284 let node = self.get_or_create_node(path);
285 node.result_handlers.insert(method, handler);
286 }
287
288 fn get_or_create_node(&mut self, path: &str) -> &mut RouteNode {
298 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
299 let mut current = self;
300 for seg in segments {
301 match seg {
302 "*" => {
303 if current.wildcard_child.is_none() {
304 current.wildcard_child = Some(Box::new(RouteNode::new(NodeType::Wildcard)));
305 }
306 current = current.wildcard_child.as_mut().unwrap();
307 }
308 s if s.starts_with(':') => {
309 let name = s[1..].to_string();
310 if current.param_child.is_none() {
311 current.param_child = Some(Box::new(RouteNode::new(NodeType::Param(name))));
312 }
313 current = current.param_child.as_mut().unwrap();
314 }
315 s => {
316 current = current
317 .static_children
318 .entry(s.to_string())
319 .or_insert_with(|| RouteNode::new(NodeType::Static(s.to_string())));
320 }
321 }
322 }
323 current
324 }
325
326 pub fn execute_with_middleware(
333 &self,
334 path: &str,
335 handler: HandlerFn,
336 req: HttpRequest,
337 params: RouteParams,
338 ) -> HttpResponse<'static> {
339 let matching: Vec<MiddlewareFn> = self
340 .middlewares
341 .iter()
342 .filter(|(prefix, _)| path_matches_prefix(path, prefix))
343 .map(|(_, mw)| *mw)
344 .collect();
345
346 if matching.is_empty() {
347 return handler(req, params);
348 }
349
350 build_chain(&matching, handler, req, ¶ms)
355 }
356
357 pub fn execute_not_found_with_middleware(
364 &self,
365 path: &str,
366 req: HttpRequest,
367 ) -> Option<HttpResponse<'static>> {
368 let handler = self.not_found_handler?;
369 let params = RouteParams::new();
370 Some(self.execute_with_middleware(path, handler, req, params))
371 }
372
373 pub fn resolve(&self, path: &str, method: &Method) -> RouteResult {
379 let segments: Vec<_> = path.split('/').filter(|s| !s.is_empty()).collect();
380 match self._match(&segments) {
381 Some((handlers, result_handlers, params, pattern)) => {
382 if let Some(&handler) = handlers.get(method) {
383 let result_handler = result_handlers.get(method).copied();
384 RouteResult::Found(handler, params, result_handler, pattern)
385 } else {
386 let allowed: Vec<Method> = handlers.keys().cloned().collect();
387 RouteResult::MethodNotAllowed(allowed)
388 }
389 }
390 None => RouteResult::NotFound,
391 }
392 }
393
394 pub fn match_path(&self, path: &str) -> Option<MatchResult<'_>> {
399 let segments: Vec<_> = path.split('/').filter(|s| !s.is_empty()).collect();
400 self._match(&segments)
401 }
402
403 fn _match(&self, segments: &[&str]) -> Option<MatchResult<'_>> {
404 if segments.is_empty() {
405 if !self.handlers.is_empty() {
406 return Some((
407 &self.handlers,
408 &self.result_handlers,
409 HashMap::new(),
410 "/".to_string(),
411 ));
412 }
413 if let Some(ref wc) = self.wildcard_child {
415 if !wc.handlers.is_empty() {
416 let mut params = HashMap::new();
417 params.insert("*".to_string(), String::new());
418 return Some((&wc.handlers, &wc.result_handlers, params, "/*".to_string()));
419 }
420 }
421 return None;
422 }
423
424 let head = segments[0];
425 let tail = &segments[1..];
426
427 debug_log!("head: {:?}", head);
428
429 if let Some(child) = self.static_children.get(head) {
431 if let Some((h, rh, p, pattern)) = child._match(tail) {
432 debug_log!("Static match: {:?}", segments);
433 let full_pattern = if pattern == "/" {
434 format!("/{head}")
435 } else {
436 format!("/{head}{pattern}")
437 };
438 return Some((h, rh, p, full_pattern));
439 }
440 }
441
442 if let Some(ref child) = self.param_child {
444 if let NodeType::Param(ref name) = child.node_type {
445 if let Some((h, rh, mut p, pattern)) = child._match(tail) {
446 p.insert(name.clone(), head.to_string());
447 debug_log!("Param match: {:?}", segments);
448 let full_pattern = if pattern == "/" {
449 format!("/:{name}")
450 } else {
451 format!("/:{name}{pattern}")
452 };
453 return Some((h, rh, p, full_pattern));
454 }
455 }
456 }
457
458 if let Some(ref child) = self.wildcard_child {
460 if !segments.is_empty() && !child.handlers.is_empty() {
461 debug_log!("Wildcard match: {:?}", segments);
462 let remaining = segments.join("/");
463 let mut params = HashMap::new();
464 params.insert("*".to_string(), remaining);
465 return Some((
466 &child.handlers,
467 &child.result_handlers,
468 params,
469 "/*".to_string(),
470 ));
471 }
472 }
473
474 None
475 }
476}
477
478fn path_matches_prefix(path: &str, prefix: &str) -> bool {
483 if prefix == "/" {
484 return true;
485 }
486 path == prefix || path.starts_with(&format!("{prefix}/"))
487}
488
489fn build_chain(
495 middlewares: &[MiddlewareFn],
496 handler: HandlerFn,
497 req: HttpRequest,
498 params: &RouteParams,
499) -> HttpResponse<'static> {
500 match middlewares.split_first() {
501 None => handler(req, params.clone()),
502 Some((&mw, rest)) => {
503 let next =
504 |inner_req: HttpRequest, inner_params: &RouteParams| -> HttpResponse<'static> {
505 build_chain(rest, handler, inner_req, inner_params)
506 };
507 mw(req, params, &next)
508 }
509 }
510}
511
512fn normalize_prefix(prefix: &str) -> String {
515 let trimmed = prefix.trim_matches('/');
516 if trimmed.is_empty() {
517 "/".to_string()
518 } else {
519 format!("/{trimmed}")
520 }
521}
522
523fn segment_count(prefix: &str) -> usize {
526 prefix.split('/').filter(|s| !s.is_empty()).count()
527}
528
529#[cfg(test)]
549mod tests {
550 use super::*;
551 use ic_http_certification::{Method, StatusCode};
552 use std::{borrow::Cow, str};
553
554 fn test_request(path: &str) -> HttpRequest<'_> {
555 HttpRequest::builder()
556 .with_method(Method::GET)
557 .with_url(path)
558 .build()
559 }
560
561 fn response_with_text(text: &str) -> HttpResponse<'static> {
562 HttpResponse::builder()
563 .with_body(Cow::Owned(text.as_bytes().to_vec()))
564 .with_status_code(StatusCode::OK)
565 .build()
566 }
567
568 fn resolve_get(root: &RouteNode, path: &str) -> (HandlerFn, RouteParams) {
570 match root.resolve(path, &Method::GET) {
571 RouteResult::Found(h, p, _, _) => (h, p),
572 other => panic!(
573 "expected Found for GET {path}, got {}",
574 route_result_name(&other)
575 ),
576 }
577 }
578
579 fn route_result_name(r: &RouteResult) -> &'static str {
580 match r {
581 RouteResult::Found(_, _, _, _) => "Found",
582 RouteResult::MethodNotAllowed(_) => "MethodNotAllowed",
583 RouteResult::NotFound => "NotFound",
584 }
585 }
586
587 fn matched_root(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
588 response_with_text("root")
589 }
590
591 fn matched_404(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
592 response_with_text("404")
593 }
594
595 fn matched_index2(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
596 response_with_text("index2")
597 }
598
599 fn matched_about(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
600 response_with_text("about")
601 }
602
603 fn matched_deep(_: HttpRequest, params: RouteParams) -> HttpResponse<'static> {
604 response_with_text(&format!("deep: {params:?}"))
605 }
606
607 fn matched_folder(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
608 response_with_text("folder")
609 }
610
611 fn setup_router() -> RouteNode {
612 let mut root = RouteNode::new(NodeType::Static("".into()));
613 root.insert("/", Method::GET, matched_root);
614 root.insert("/*", Method::GET, matched_404);
615 root.insert("/index2", Method::GET, matched_index2);
616 root.insert("/about", Method::GET, matched_about);
617 root.insert("/deep/:pageId", Method::GET, matched_deep);
618 root.insert("/deep/:pageId/:subpageId", Method::GET, matched_deep);
619 root.insert("/alsodeep/:pageId/edit", Method::GET, matched_deep);
620 root.insert("/folder/*", Method::GET, matched_folder);
621 root
622 }
623
624 fn body_str(resp: HttpResponse<'static>) -> String {
625 str::from_utf8(resp.body())
626 .unwrap_or("<invalid utf-8>")
627 .to_string()
628 }
629
630 #[test]
633 fn test_root_match() {
634 let root = setup_router();
635 let (handler, params) = resolve_get(&root, "/");
636 assert_eq!(body_str(handler(test_request("/"), params)), "root");
637 }
638
639 #[test]
640 fn test_404_match() {
641 let root = setup_router();
642 let (handler, _) = resolve_get(&root, "/nonexistent");
643 assert_eq!(
644 body_str(handler(test_request("/nonexistent"), HashMap::new())),
645 "404"
646 );
647 }
648
649 #[test]
650 fn test_exact_match() {
651 let root = setup_router();
652 let (handler, params) = resolve_get(&root, "/index2");
653 assert_eq!(body_str(handler(test_request("/index2"), params)), "index2");
654 }
655
656 #[test]
657 fn test_pathless_layout_route_a() {
658 let mut root = RouteNode::new(NodeType::Static("".into()));
659 root.insert("/about", Method::GET, matched_about);
660 let (handler, params) = resolve_get(&root, "/about");
661 assert_eq!(body_str(handler(test_request("/about"), params)), "about");
662 }
663
664 #[test]
665 fn test_dynamic_match() {
666 let root = setup_router();
667 let (handler, params) = resolve_get(&root, "/deep/page1");
668 let body = body_str(handler(test_request("/deep/page1"), params));
669 assert!(body.contains("page1"));
670 }
671
672 #[test]
673 fn test_posts_postid_edit() {
674 let root = setup_router();
675 let (handler, params) = resolve_get(&root, "/alsodeep/page1/edit");
676 let body = body_str(handler(test_request("/alsodeep/page1/edit"), params));
677 assert!(body.contains("page1"));
678 }
679
680 #[test]
681 fn test_nested_dynamic_match() {
682 let root = setup_router();
683 let (handler, params) = resolve_get(&root, "/deep/page2/subpage1");
684 let body = body_str(handler(test_request("/deep/page2/subpage1"), params));
685 assert!(body.contains("page2"));
686 assert!(body.contains("subpage1"));
687 }
688
689 #[test]
690 fn test_wildcard_match() {
691 let root = setup_router();
692 let (handler, _) = resolve_get(&root, "/folder/anything");
693 assert_eq!(
694 body_str(handler(test_request("/folder/anything"), HashMap::new())),
695 "folder"
696 );
697 }
698
699 #[test]
700 fn test_folder_root_wildcard_match() {
701 let root = setup_router();
702 let (handler, _) = resolve_get(&root, "/folder/any");
703 assert_eq!(
704 body_str(handler(test_request("/folder/any"), HashMap::new())),
705 "folder"
706 );
707 }
708
709 #[test]
710 fn test_deep_wildcard_multi_segments() {
711 let root = setup_router();
712 let (handler, _) = resolve_get(&root, "/folder/a/b/c/d");
713 assert_eq!(
714 body_str(handler(test_request("/folder/a/b/c/d"), HashMap::new())),
715 "folder"
716 );
717 }
718
719 #[test]
720 fn test_trailing_slash_static_match() {
721 let root = setup_router();
722 let (handler, _) = resolve_get(&root, "/index2/");
723 assert_eq!(
724 body_str(handler(test_request("/index2/"), HashMap::new())),
725 "index2"
726 );
727 }
728
729 #[test]
730 fn test_double_slash_matches_normalized() {
731 let root = setup_router();
732 let (handler, _) = resolve_get(&root, "//index2");
733 assert_eq!(
734 body_str(handler(test_request("//index2"), HashMap::new())),
735 "index2"
736 );
737 }
738
739 #[test]
740 fn test_root_wildcard_captures_full_path() {
741 let root = setup_router();
742 let (_, params) = resolve_get(&root, "/a/b/c");
743 assert_eq!(params.get("*").unwrap(), "a/b/c");
744 }
745
746 #[test]
747 fn test_folder_wildcard_captures_tail() {
748 let root = setup_router();
749 let (handler, params) = resolve_get(&root, "/folder/docs/report.pdf");
750 assert_eq!(params.get("*").unwrap(), "docs/report.pdf");
751 assert_eq!(
752 body_str(handler(
753 test_request("/folder/docs/report.pdf"),
754 params.clone()
755 )),
756 "folder"
757 );
758 }
759
760 fn matched_user_files(_: HttpRequest, params: RouteParams) -> HttpResponse<'static> {
761 response_with_text(&format!("user_files: {params:?}"))
762 }
763
764 #[test]
765 fn test_mixed_params_and_wildcard() {
766 let mut root = RouteNode::new(NodeType::Static("".into()));
767 root.insert("/users/:id/files/*", Method::GET, matched_user_files);
768 let (_, params) = resolve_get(&root, "/users/42/files/docs/report.pdf");
769 assert_eq!(params.get("id").unwrap(), "42");
770 assert_eq!(params.get("*").unwrap(), "docs/report.pdf");
771 }
772
773 #[test]
774 fn test_empty_wildcard_match() {
775 let mut root = RouteNode::new(NodeType::Static("".into()));
776 root.insert("/files/*", Method::GET, matched_folder);
777 let (handler, params) = resolve_get(&root, "/files/");
778 assert_eq!(params.get("*").unwrap(), "");
779 assert_eq!(
780 body_str(handler(test_request("/files/"), params.clone())),
781 "folder"
782 );
783 }
784
785 fn matched_post_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
788 response_with_text("post_handler")
789 }
790
791 fn matched_get_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
792 response_with_text("get_handler")
793 }
794
795 #[test]
797 fn test_method_dispatch_get_and_post() {
798 let mut root = RouteNode::new(NodeType::Static("".into()));
799 root.insert("/api/users", Method::GET, matched_get_handler);
800 root.insert("/api/users", Method::POST, matched_post_handler);
801
802 match root.resolve("/api/users", &Method::GET) {
804 RouteResult::Found(handler, params, _, _) => {
805 assert_eq!(
806 body_str(handler(test_request("/api/users"), params)),
807 "get_handler"
808 );
809 }
810 other => panic!("expected Found, got {}", route_result_name(&other)),
811 }
812
813 match root.resolve("/api/users", &Method::POST) {
815 RouteResult::Found(handler, params, _, _) => {
816 let req = HttpRequest::builder()
817 .with_method(Method::POST)
818 .with_url("/api/users")
819 .build();
820 assert_eq!(body_str(handler(req, params)), "post_handler");
821 }
822 other => panic!("expected Found, got {}", route_result_name(&other)),
823 }
824 }
825
826 #[test]
828 fn test_method_not_allowed() {
829 let mut root = RouteNode::new(NodeType::Static("".into()));
830 root.insert("/api/users", Method::GET, matched_get_handler);
831 root.insert("/api/users", Method::POST, matched_post_handler);
832
833 match root.resolve("/api/users", &Method::PUT) {
834 RouteResult::MethodNotAllowed(allowed) => {
835 let mut names: Vec<&str> = allowed.iter().map(|m| m.as_str()).collect();
836 names.sort();
837 assert_eq!(names, vec!["GET", "POST"]);
838 }
839 other => panic!(
840 "expected MethodNotAllowed, got {}",
841 route_result_name(&other)
842 ),
843 }
844 }
845
846 #[test]
848 fn test_unknown_path_returns_not_found() {
849 let mut root = RouteNode::new(NodeType::Static("".into()));
850 root.insert("/api/users", Method::GET, matched_get_handler);
851
852 assert!(matches!(
853 root.resolve("/api/nonexistent", &Method::GET),
854 RouteResult::NotFound
855 ));
856 }
857
858 #[test]
860 fn test_all_seven_methods() {
861 let methods = [
862 Method::GET,
863 Method::POST,
864 Method::PUT,
865 Method::PATCH,
866 Method::DELETE,
867 Method::HEAD,
868 Method::OPTIONS,
869 ];
870
871 let mut root = RouteNode::new(NodeType::Static("".into()));
872 for method in &methods {
873 root.insert("/test", method.clone(), matched_get_handler);
874 }
875
876 for method in &methods {
878 match root.resolve("/test", method) {
879 RouteResult::Found(_, _, _, _) => {}
880 other => panic!(
881 "expected Found for method {}, got {}",
882 method.as_str(),
883 route_result_name(&other)
884 ),
885 }
886 }
887 }
888
889 use std::cell::RefCell;
892
893 thread_local! {
894 static LOG: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
895 }
896
897 fn clear_log() {
898 LOG.with(|l| l.borrow_mut().clear());
899 }
900
901 fn get_log() -> Vec<String> {
902 LOG.with(|l| l.borrow().clone())
903 }
904
905 fn log_entry(msg: &str) {
906 LOG.with(|l| l.borrow_mut().push(msg.to_string()));
907 }
908
909 fn logging_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
910 log_entry("handler");
911 response_with_text("handler_response")
912 }
913
914 fn root_middleware(
915 req: HttpRequest,
916 params: &RouteParams,
917 next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
918 ) -> HttpResponse<'static> {
919 log_entry("root_mw_before");
920 let resp = next(req, params);
921 log_entry("root_mw_after");
922 resp
923 }
924
925 fn api_middleware(
926 req: HttpRequest,
927 params: &RouteParams,
928 next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
929 ) -> HttpResponse<'static> {
930 log_entry("api_mw_before");
931 let resp = next(req, params);
932 log_entry("api_mw_after");
933 resp
934 }
935
936 fn api_v2_middleware(
937 req: HttpRequest,
938 params: &RouteParams,
939 next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
940 ) -> HttpResponse<'static> {
941 log_entry("api_v2_mw_before");
942 let resp = next(req, params);
943 log_entry("api_v2_mw_after");
944 resp
945 }
946
947 #[test]
949 fn test_root_middleware_runs_on_all_requests() {
950 clear_log();
951 let mut root = RouteNode::new(NodeType::Static("".into()));
952 root.insert("/", Method::GET, logging_handler);
953 root.insert("/about", Method::GET, logging_handler);
954 root.insert("/api/users", Method::GET, logging_handler);
955 root.set_middleware("/", root_middleware);
956
957 let (handler, params) = resolve_get(&root, "/");
959 root.execute_with_middleware("/", handler, test_request("/"), params);
960 assert!(get_log().contains(&"root_mw_before".to_string()));
961 assert!(get_log().contains(&"handler".to_string()));
962 assert!(get_log().contains(&"root_mw_after".to_string()));
963
964 clear_log();
966 let (handler, params) = resolve_get(&root, "/about");
967 root.execute_with_middleware("/about", handler, test_request("/about"), params);
968 assert!(get_log().contains(&"root_mw_before".to_string()));
969 assert!(get_log().contains(&"handler".to_string()));
970
971 clear_log();
973 let (handler, params) = resolve_get(&root, "/api/users");
974 root.execute_with_middleware("/api/users", handler, test_request("/api/users"), params);
975 assert!(get_log().contains(&"root_mw_before".to_string()));
976 assert!(get_log().contains(&"handler".to_string()));
977 }
978
979 #[test]
981 fn test_scoped_middleware_only_matching_prefix() {
982 clear_log();
983 let mut root = RouteNode::new(NodeType::Static("".into()));
984 root.insert("/api/users", Method::GET, logging_handler);
985 root.insert("/pages/home", Method::GET, logging_handler);
986 root.set_middleware("/api", api_middleware);
987
988 let (handler, params) = resolve_get(&root, "/api/users");
990 root.execute_with_middleware("/api/users", handler, test_request("/api/users"), params);
991 assert!(get_log().contains(&"api_mw_before".to_string()));
992 assert!(get_log().contains(&"handler".to_string()));
993
994 clear_log();
996 let (handler, params) = resolve_get(&root, "/pages/home");
997 root.execute_with_middleware("/pages/home", handler, test_request("/pages/home"), params);
998 assert!(!get_log().contains(&"api_mw_before".to_string()));
999 assert!(get_log().contains(&"handler".to_string()));
1000 }
1001
1002 #[test]
1004 fn test_middleware_chain_order() {
1005 clear_log();
1006 let mut root = RouteNode::new(NodeType::Static("".into()));
1007 root.insert("/api/v2/data", Method::GET, logging_handler);
1008 root.set_middleware("/", root_middleware);
1009 root.set_middleware("/api", api_middleware);
1010 root.set_middleware("/api/v2", api_v2_middleware);
1011
1012 let (handler, params) = resolve_get(&root, "/api/v2/data");
1013 root.execute_with_middleware(
1014 "/api/v2/data",
1015 handler,
1016 test_request("/api/v2/data"),
1017 params,
1018 );
1019
1020 let log = get_log();
1021 assert_eq!(
1022 log,
1023 vec![
1024 "root_mw_before",
1025 "api_mw_before",
1026 "api_v2_mw_before",
1027 "handler",
1028 "api_v2_mw_after",
1029 "api_mw_after",
1030 "root_mw_after",
1031 ]
1032 );
1033 }
1034
1035 #[test]
1037 fn test_middleware_short_circuit() {
1038 fn auth_middleware(
1039 _req: HttpRequest,
1040 _params: &RouteParams,
1041 _next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
1042 ) -> HttpResponse<'static> {
1043 log_entry("auth_reject");
1044 HttpResponse::builder()
1045 .with_status_code(StatusCode::UNAUTHORIZED)
1046 .with_body(Cow::Owned(b"Unauthorized".to_vec()))
1047 .build()
1048 }
1049
1050 clear_log();
1051 let mut root = RouteNode::new(NodeType::Static("".into()));
1052 root.insert("/secret", Method::GET, logging_handler);
1053 root.set_middleware("/", auth_middleware);
1054
1055 let (handler, params) = resolve_get(&root, "/secret");
1056 let resp =
1057 root.execute_with_middleware("/secret", handler, test_request("/secret"), params);
1058
1059 assert_eq!(resp.status_code(), StatusCode::UNAUTHORIZED);
1060 let log = get_log();
1061 assert!(log.contains(&"auth_reject".to_string()));
1062 assert!(!log.contains(&"handler".to_string()));
1063 }
1064
1065 #[test]
1067 fn test_middleware_modifies_response() {
1068 fn header_middleware(
1069 req: HttpRequest,
1070 params: &RouteParams,
1071 next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
1072 ) -> HttpResponse<'static> {
1073 let resp = next(req, params);
1074 let mut headers = resp.headers().to_vec();
1076 headers.push(("x-custom".to_string(), "injected".to_string()));
1077 HttpResponse::builder()
1078 .with_status_code(resp.status_code())
1079 .with_headers(headers)
1080 .with_body(resp.body().to_vec())
1081 .build()
1082 }
1083
1084 let mut root = RouteNode::new(NodeType::Static("".into()));
1085 root.insert("/test", Method::GET, logging_handler);
1086 root.set_middleware("/", header_middleware);
1087
1088 let (handler, params) = resolve_get(&root, "/test");
1089 let resp = root.execute_with_middleware("/test", handler, test_request("/test"), params);
1090
1091 let custom_header = resp
1092 .headers()
1093 .iter()
1094 .find(|(k, _)| k == "x-custom")
1095 .map(|(_, v)| v.clone());
1096 assert_eq!(custom_header, Some("injected".to_string()));
1097 assert_eq!(body_str(resp), "handler_response");
1098 }
1099
1100 #[test]
1102 fn test_set_middleware_replaces_previous() {
1103 fn mw_a(
1104 req: HttpRequest,
1105 params: &RouteParams,
1106 next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
1107 ) -> HttpResponse<'static> {
1108 log_entry("mw_a");
1109 next(req, params)
1110 }
1111 fn mw_b(
1112 req: HttpRequest,
1113 params: &RouteParams,
1114 next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
1115 ) -> HttpResponse<'static> {
1116 log_entry("mw_b");
1117 next(req, params)
1118 }
1119
1120 clear_log();
1121 let mut root = RouteNode::new(NodeType::Static("".into()));
1122 root.insert("/test", Method::GET, logging_handler);
1123 root.set_middleware("/", mw_a);
1124 root.set_middleware("/", mw_b); let (handler, params) = resolve_get(&root, "/test");
1127 root.execute_with_middleware("/test", handler, test_request("/test"), params);
1128
1129 let log = get_log();
1130 assert!(!log.contains(&"mw_a".to_string()));
1131 assert!(log.contains(&"mw_b".to_string()));
1132 }
1133
1134 #[test]
1138 fn test_middleware_works_in_both_paths() {
1139 clear_log();
1140 let mut root = RouteNode::new(NodeType::Static("".into()));
1141
1142 fn post_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1143 log_entry("post_handler");
1144 response_with_text("posted")
1145 }
1146
1147 root.insert("/api/data", Method::GET, logging_handler);
1148 root.insert("/api/data", Method::POST, post_handler);
1149 root.set_middleware("/api", api_middleware);
1150
1151 let (handler, params) = resolve_get(&root, "/api/data");
1153 let resp =
1154 root.execute_with_middleware("/api/data", handler, test_request("/api/data"), params);
1155 assert_eq!(body_str(resp), "handler_response");
1156 assert!(get_log().contains(&"api_mw_before".to_string()));
1157
1158 clear_log();
1160 match root.resolve("/api/data", &Method::POST) {
1161 RouteResult::Found(handler, params, _, _) => {
1162 let req = HttpRequest::builder()
1163 .with_method(Method::POST)
1164 .with_url("/api/data")
1165 .build();
1166 let resp = root.execute_with_middleware("/api/data", handler, req, params);
1167 assert_eq!(body_str(resp), "posted");
1168 assert!(get_log().contains(&"api_mw_before".to_string()));
1169 assert!(get_log().contains(&"post_handler".to_string()));
1170 }
1171 other => panic!("expected Found, got {}", route_result_name(&other)),
1172 }
1173 }
1174
1175 fn custom_404_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1178 HttpResponse::builder()
1179 .with_status_code(StatusCode::NOT_FOUND)
1180 .with_headers(vec![("content-type".to_string(), "text/html".to_string())])
1181 .with_body(Cow::Owned(b"<h1>Custom Not Found</h1>".to_vec()))
1182 .build()
1183 }
1184
1185 #[test]
1187 fn test_custom_404_returns_custom_response() {
1188 let mut root = RouteNode::new(NodeType::Static("".into()));
1189 root.insert("/exists", Method::GET, matched_get_handler);
1190 root.set_not_found(custom_404_handler);
1191
1192 let resp = root
1194 .execute_not_found_with_middleware("/nonexistent", test_request("/nonexistent"))
1195 .expect("expected custom 404 response");
1196 assert_eq!(resp.status_code(), StatusCode::NOT_FOUND);
1197 assert_eq!(body_str(resp), "<h1>Custom Not Found</h1>");
1198 }
1199
1200 #[test]
1202 fn test_default_404_without_custom_handler() {
1203 let mut root = RouteNode::new(NodeType::Static("".into()));
1204 root.insert("/exists", Method::GET, matched_get_handler);
1205 let resp =
1209 root.execute_not_found_with_middleware("/nonexistent", test_request("/nonexistent"));
1210 assert!(resp.is_none(), "expected None when no custom 404 is set");
1211 }
1212
1213 #[test]
1215 fn test_custom_404_receives_full_request() {
1216 fn inspecting_404(req: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1217 let url = req.url().to_string();
1219 response_with_text(&format!("404 for: {url}"))
1220 }
1221
1222 let mut root = RouteNode::new(NodeType::Static("".into()));
1223 root.set_not_found(inspecting_404);
1224
1225 let req = HttpRequest::builder()
1226 .with_method(Method::GET)
1227 .with_url("/some/missing/path")
1228 .build();
1229 let resp = root
1230 .execute_not_found_with_middleware("/some/missing/path", req)
1231 .expect("expected custom 404 response");
1232 let body = body_str(resp);
1233 assert!(
1234 body.contains("/some/missing/path"),
1235 "expected URL in response body, got: {body}"
1236 );
1237 }
1238
1239 #[test]
1241 fn test_custom_404_json_content_type() {
1242 fn json_404(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1243 HttpResponse::builder()
1244 .with_status_code(StatusCode::NOT_FOUND)
1245 .with_headers(vec![(
1246 "content-type".to_string(),
1247 "application/json".to_string(),
1248 )])
1249 .with_body(Cow::Owned(br#"{"error":"not found"}"#.to_vec()))
1250 .build()
1251 }
1252
1253 let mut root = RouteNode::new(NodeType::Static("".into()));
1254 root.set_not_found(json_404);
1255
1256 let resp = root
1257 .execute_not_found_with_middleware("/api/missing", test_request("/api/missing"))
1258 .expect("expected custom 404 response");
1259 assert_eq!(resp.status_code(), StatusCode::NOT_FOUND);
1260 let ct = resp
1261 .headers()
1262 .iter()
1263 .find(|(k, _)| k == "content-type")
1264 .map(|(_, v)| v.clone());
1265 assert_eq!(ct, Some("application/json".to_string()));
1266 assert_eq!(body_str(resp), r#"{"error":"not found"}"#);
1267 }
1268
1269 #[test]
1271 fn test_root_middleware_runs_before_custom_404() {
1272 fn logging_404(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1273 log_entry("custom_404");
1274 response_with_text("custom 404")
1275 }
1276
1277 clear_log();
1278 let mut root = RouteNode::new(NodeType::Static("".into()));
1279 root.insert("/exists", Method::GET, logging_handler);
1280 root.set_middleware("/", root_middleware);
1281 root.set_not_found(logging_404);
1282
1283 let resp = root
1284 .execute_not_found_with_middleware("/nonexistent", test_request("/nonexistent"))
1285 .expect("expected custom 404 response");
1286
1287 let log = get_log();
1288 assert_eq!(
1289 log,
1290 vec!["root_mw_before", "custom_404", "root_mw_after"],
1291 "middleware should wrap the custom 404 handler"
1292 );
1293 assert_eq!(body_str(resp), "custom 404");
1294 }
1295
1296 #[test]
1299 fn from_http_response_for_handler_result() {
1300 let response = HttpResponse::builder()
1301 .with_status_code(StatusCode::OK)
1302 .with_body(Cow::Owned(b"hello".to_vec()))
1303 .build();
1304
1305 let result: HandlerResult = response.into();
1306
1307 match result {
1308 HandlerResult::Response(resp) => {
1309 assert_eq!(resp.status_code(), StatusCode::OK);
1310 assert_eq!(resp.body(), b"hello");
1311 }
1312 HandlerResult::NotModified => panic!("expected Response, got NotModified"),
1313 }
1314 }
1315
1316 #[test]
1320 fn test_empty_segments_ignored() {
1321 let root = setup_router();
1322 let (handler, _) = resolve_get(&root, "/about///");
1324 assert_eq!(
1325 body_str(handler(test_request("/about"), HashMap::new())),
1326 "about"
1327 );
1328 }
1329
1330 #[test]
1333 fn test_url_encoded_characters_in_static_path() {
1334 let mut root = RouteNode::new(NodeType::Static("".into()));
1335 root.insert("/hello%20world", Method::GET, matched_about);
1337 let (handler, params) = resolve_get(&root, "/hello%20world");
1338 assert_eq!(
1339 body_str(handler(test_request("/hello%20world"), params)),
1340 "about"
1341 );
1342 }
1343
1344 #[test]
1346 fn test_url_encoded_characters_in_param() {
1347 let mut root = RouteNode::new(NodeType::Static("".into()));
1348 root.insert("/posts/:id", Method::GET, matched_deep);
1349 let (_, params) = resolve_get(&root, "/posts/hello%20world");
1350 assert_eq!(params.get("id").unwrap(), "hello%20world");
1351 }
1352
1353 #[test]
1355 fn test_very_long_path() {
1356 let mut root = RouteNode::new(NodeType::Static("".into()));
1357 let segments: Vec<String> = (0..100).map(|i| format!("s{i}")).collect();
1359 let path = format!("/{}", segments.join("/"));
1360 root.insert(&path, Method::GET, matched_about);
1361
1362 let (handler, params) = resolve_get(&root, &path);
1363 assert_eq!(body_str(handler(test_request(&path), params)), "about");
1364 }
1365
1366 #[test]
1368 fn test_very_long_path_not_found() {
1369 let root = RouteNode::new(NodeType::Static("".into()));
1370 let segments: Vec<String> = (0..100).map(|i| format!("s{i}")).collect();
1371 let path = format!("/{}", segments.join("/"));
1372 assert!(matches!(
1373 root.resolve(&path, &Method::GET),
1374 RouteResult::NotFound
1375 ));
1376 }
1377
1378 #[test]
1380 fn test_many_parameters() {
1381 fn many_param_handler(_: HttpRequest, params: RouteParams) -> HttpResponse<'static> {
1382 response_with_text(&format!(
1383 "{}/{}/{}/{}",
1384 params.get("a").unwrap(),
1385 params.get("b").unwrap(),
1386 params.get("c").unwrap(),
1387 params.get("d").unwrap(),
1388 ))
1389 }
1390
1391 let mut root = RouteNode::new(NodeType::Static("".into()));
1392 root.insert("/:a/:b/:c/:d", Method::GET, many_param_handler);
1393
1394 let (handler, params) = resolve_get(&root, "/w/x/y/z");
1395 assert_eq!(params.get("a").unwrap(), "w");
1396 assert_eq!(params.get("b").unwrap(), "x");
1397 assert_eq!(params.get("c").unwrap(), "y");
1398 assert_eq!(params.get("d").unwrap(), "z");
1399 assert_eq!(
1400 body_str(handler(test_request("/w/x/y/z"), params)),
1401 "w/x/y/z"
1402 );
1403 }
1404
1405 #[test]
1407 fn test_static_precedence_over_param() {
1408 fn static_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1409 response_with_text("static")
1410 }
1411 fn param_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1412 response_with_text("param")
1413 }
1414
1415 let mut root = RouteNode::new(NodeType::Static("".into()));
1416 root.insert("/items/special", Method::GET, static_handler);
1417 root.insert("/items/:id", Method::GET, param_handler);
1418
1419 let (handler, _) = resolve_get(&root, "/items/special");
1421 assert_eq!(
1422 body_str(handler(test_request("/items/special"), HashMap::new())),
1423 "static"
1424 );
1425
1426 let (handler, params) = resolve_get(&root, "/items/other");
1428 assert_eq!(
1429 body_str(handler(test_request("/items/other"), params)),
1430 "param"
1431 );
1432 }
1433
1434 #[test]
1436 fn test_param_precedence_over_wildcard() {
1437 fn param_handler(_: HttpRequest, params: RouteParams) -> HttpResponse<'static> {
1438 response_with_text(&format!("param:{}", params.get("id").unwrap()))
1439 }
1440 fn wildcard_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1441 response_with_text("wildcard")
1442 }
1443
1444 let mut root = RouteNode::new(NodeType::Static("".into()));
1445 root.insert("/items/:id", Method::GET, param_handler);
1446 root.insert("/items/*", Method::GET, wildcard_handler);
1447
1448 let (handler, params) = resolve_get(&root, "/items/42");
1450 assert_eq!(
1451 body_str(handler(test_request("/items/42"), params.clone())),
1452 "param:42"
1453 );
1454
1455 let (handler, params) = resolve_get(&root, "/items/42/extra");
1457 assert_eq!(params.get("*").unwrap(), "42/extra");
1458 assert_eq!(
1459 body_str(handler(test_request("/items/42/extra"), params)),
1460 "wildcard"
1461 );
1462 }
1463
1464 #[test]
1466 fn test_root_not_found_when_only_nested() {
1467 let mut root = RouteNode::new(NodeType::Static("".into()));
1468 root.insert("/api/data", Method::GET, matched_about);
1469 assert!(matches!(
1470 root.resolve("/", &Method::GET),
1471 RouteResult::NotFound
1472 ));
1473 }
1474
1475 #[test]
1477 fn test_insert_result_and_resolve() {
1478 fn handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1479 response_with_text("ok")
1480 }
1481 fn result_handler(_: HttpRequest, _: RouteParams) -> HandlerResult {
1482 HandlerResult::NotModified
1483 }
1484
1485 let mut root = RouteNode::new(NodeType::Static("".into()));
1486 root.insert("/test", Method::GET, handler);
1487 root.insert_result("/test", Method::GET, result_handler);
1488
1489 match root.resolve("/test", &Method::GET) {
1490 RouteResult::Found(_, _, Some(rh), _) => {
1491 let result = rh(test_request("/test"), HashMap::new());
1493 assert!(matches!(result, HandlerResult::NotModified));
1494 }
1495 RouteResult::Found(_, _, None, _) => panic!("expected result handler to be present"),
1496 other => panic!("expected Found, got {}", route_result_name(&other)),
1497 }
1498 }
1499
1500 #[test]
1502 fn test_match_path_returns_handlers() {
1503 let mut root = RouteNode::new(NodeType::Static("".into()));
1504 root.insert("/items/:id", Method::GET, matched_get_handler);
1505 root.insert("/items/:id", Method::POST, matched_post_handler);
1506
1507 let (handlers, _, params, _) = root.match_path("/items/42").expect("should match");
1508 assert_eq!(params.get("id").unwrap(), "42");
1509 assert!(handlers.contains_key(&Method::GET));
1510 assert!(handlers.contains_key(&Method::POST));
1511 assert_eq!(handlers.len(), 2);
1512 }
1513
1514 #[test]
1516 fn test_match_path_returns_none() {
1517 let root = RouteNode::new(NodeType::Static("".into()));
1518 assert!(root.match_path("/nonexistent").is_none());
1519 }
1520
1521 #[test]
1526 fn test_middleware_modifies_request_before_handler() {
1527 fn inject_header_mw(
1528 req: HttpRequest,
1529 params: &RouteParams,
1530 next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
1531 ) -> HttpResponse<'static> {
1532 let mut headers = req.headers().to_vec();
1534 headers.push(("x-injected".to_string(), "mw-value".to_string()));
1535 let modified = HttpRequest::builder()
1536 .with_method(req.method().clone())
1537 .with_url(req.url())
1538 .with_headers(headers)
1539 .with_body(req.body().to_vec())
1540 .build();
1541 next(modified, params)
1542 }
1543
1544 fn header_checking_handler(req: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1545 let has_header = req
1546 .headers()
1547 .iter()
1548 .any(|(k, v)| k == "x-injected" && v == "mw-value");
1549 if has_header {
1550 response_with_text("header_present")
1551 } else {
1552 response_with_text("header_missing")
1553 }
1554 }
1555
1556 let mut root = RouteNode::new(NodeType::Static("".into()));
1557 root.insert("/check", Method::GET, header_checking_handler);
1558 root.set_middleware("/", inject_header_mw);
1559
1560 let (handler, params) = resolve_get(&root, "/check");
1561 let resp = root.execute_with_middleware("/check", handler, test_request("/check"), params);
1562 assert_eq!(body_str(resp), "header_present");
1563 }
1564
1565 #[test]
1567 fn test_multiple_middleware_on_not_found() {
1568 fn nf_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1569 log_entry("not_found_handler");
1570 response_with_text("not found")
1571 }
1572
1573 clear_log();
1574 let mut root = RouteNode::new(NodeType::Static("".into()));
1575 root.insert("/api/data", Method::GET, logging_handler);
1576 root.set_middleware("/", root_middleware);
1577 root.set_middleware("/api", api_middleware);
1578 root.set_not_found(nf_handler);
1579
1580 let resp = root
1582 .execute_not_found_with_middleware("/api/missing", test_request("/api/missing"))
1583 .expect("expected not-found response");
1584
1585 let log = get_log();
1586 assert_eq!(
1587 log,
1588 vec![
1589 "root_mw_before",
1590 "api_mw_before",
1591 "not_found_handler",
1592 "api_mw_after",
1593 "root_mw_after",
1594 ],
1595 "both root and /api middleware should wrap the not-found handler"
1596 );
1597 assert_eq!(body_str(resp), "not found");
1598 }
1599
1600 #[test]
1602 fn test_not_found_only_root_middleware_for_non_api() {
1603 fn nf_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1604 log_entry("not_found_handler");
1605 response_with_text("not found")
1606 }
1607
1608 clear_log();
1609 let mut root = RouteNode::new(NodeType::Static("".into()));
1610 root.insert("/api/data", Method::GET, logging_handler);
1611 root.set_middleware("/", root_middleware);
1612 root.set_middleware("/api", api_middleware);
1613 root.set_not_found(nf_handler);
1614
1615 let resp = root
1617 .execute_not_found_with_middleware("/other/missing", test_request("/other/missing"))
1618 .expect("expected not-found response");
1619
1620 let log = get_log();
1621 assert_eq!(
1622 log,
1623 vec!["root_mw_before", "not_found_handler", "root_mw_after"],
1624 "/api middleware should NOT fire for /other/missing"
1625 );
1626 assert_eq!(body_str(resp), "not found");
1627 }
1628
1629 #[test]
1632 fn test_middleware_ordering_independent_of_registration_order() {
1633 clear_log();
1634 let mut root = RouteNode::new(NodeType::Static("".into()));
1635 root.insert("/api/v2/data", Method::GET, logging_handler);
1636
1637 root.set_middleware("/api/v2", api_v2_middleware);
1639 root.set_middleware("/api", api_middleware);
1640 root.set_middleware("/", root_middleware);
1641
1642 let (handler, params) = resolve_get(&root, "/api/v2/data");
1643 root.execute_with_middleware(
1644 "/api/v2/data",
1645 handler,
1646 test_request("/api/v2/data"),
1647 params,
1648 );
1649
1650 let log = get_log();
1651 assert_eq!(
1652 log,
1653 vec![
1654 "root_mw_before",
1655 "api_mw_before",
1656 "api_v2_mw_before",
1657 "handler",
1658 "api_v2_mw_after",
1659 "api_mw_after",
1660 "root_mw_after",
1661 ],
1662 "order should be root→api→api_v2 regardless of registration order"
1663 );
1664 }
1665
1666 #[test]
1668 fn test_no_middleware_handler_runs_directly() {
1669 clear_log();
1670 let mut root = RouteNode::new(NodeType::Static("".into()));
1671 root.insert("/test", Method::GET, logging_handler);
1672 let (handler, params) = resolve_get(&root, "/test");
1675 let resp = root.execute_with_middleware("/test", handler, test_request("/test"), params);
1676
1677 let log = get_log();
1678 assert_eq!(log, vec!["handler"]);
1679 assert_eq!(body_str(resp), "handler_response");
1680 }
1681
1682 #[test]
1684 fn test_normalize_prefix_canonical() {
1685 assert_eq!(normalize_prefix("/"), "/");
1686 assert_eq!(normalize_prefix(""), "/");
1687 assert_eq!(normalize_prefix("/api"), "/api");
1688 assert_eq!(normalize_prefix("/api/"), "/api");
1689 assert_eq!(normalize_prefix("api"), "/api");
1690 assert_eq!(normalize_prefix("api/v2/"), "/api/v2");
1691 }
1692
1693 #[test]
1695 fn test_segment_count() {
1696 assert_eq!(segment_count("/"), 0);
1697 assert_eq!(segment_count("/api"), 1);
1698 assert_eq!(segment_count("/api/v2"), 2);
1699 assert_eq!(segment_count("/api/v2/data"), 3);
1700 }
1701
1702 #[test]
1704 fn test_path_matches_prefix() {
1705 assert!(path_matches_prefix("/api/data", "/"));
1707 assert!(path_matches_prefix("/", "/"));
1708
1709 assert!(path_matches_prefix("/api", "/api"));
1711
1712 assert!(path_matches_prefix("/api/data", "/api"));
1714 assert!(path_matches_prefix("/api/v2/data", "/api"));
1715
1716 assert!(!path_matches_prefix("/api-v2", "/api"));
1718 assert!(!path_matches_prefix("/apidata", "/api"));
1719
1720 assert!(!path_matches_prefix("/other", "/api"));
1722 }
1723
1724 mod proptests {
1727 use super::*;
1728 use proptest::prelude::*;
1729
1730 fn dummy_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1731 response_with_text("dummy")
1732 }
1733
1734 proptest! {
1735 #[test]
1738 fn inserted_routes_are_always_found(path in "/[a-z]{1,5}(/[a-z]{1,5}){0,4}") {
1739 let mut root = RouteNode::new(NodeType::Static("".into()));
1740 root.insert(&path, Method::GET, dummy_handler);
1741 match root.resolve(&path, &Method::GET) {
1742 RouteResult::Found(_, _, _, _) => {},
1743 _ => panic!("expected Found for inserted path: {path}"),
1744 }
1745 }
1746
1747 #[test]
1750 fn non_inserted_routes_are_not_found(
1751 inserted in "/[a-z]{1,10}",
1752 queried in "/[a-z]{1,10}"
1753 ) {
1754 prop_assume!(inserted != queried);
1755 let mut root = RouteNode::new(NodeType::Static("".into()));
1756 root.insert(&inserted, Method::GET, dummy_handler);
1757 match root.resolve(&queried, &Method::GET) {
1758 RouteResult::NotFound => {},
1759 _ => panic!("expected NotFound for non-inserted route: {queried} (inserted: {inserted})"),
1760 }
1761 }
1762
1763 #[test]
1765 fn param_routes_capture_any_segment(
1766 prefix in "/[a-z]{1,5}",
1767 value in "[a-z0-9]{1,20}"
1768 ) {
1769 let route = format!("{prefix}/:id");
1770 let path = format!("{prefix}/{value}");
1771 let mut root = RouteNode::new(NodeType::Static("".into()));
1772 root.insert(&route, Method::GET, dummy_handler);
1773 match root.resolve(&path, &Method::GET) {
1774 RouteResult::Found(_, params, _, _) => {
1775 prop_assert_eq!(params.get("id").map(|s| s.as_str()), Some(value.as_str()));
1776 },
1777 other => panic!("expected Found, got {}", route_result_name(&other)),
1778 }
1779 }
1780
1781 #[test]
1783 fn wildcard_routes_capture_remaining_path(
1784 prefix in "/[a-z]{1,5}",
1785 tail in "[a-z0-9]{1,5}(/[a-z0-9]{1,5}){0,3}"
1786 ) {
1787 let route = format!("{prefix}/*");
1788 let path = format!("{prefix}/{tail}");
1789 let mut root = RouteNode::new(NodeType::Static("".into()));
1790 root.insert(&route, Method::GET, dummy_handler);
1791 match root.resolve(&path, &Method::GET) {
1792 RouteResult::Found(_, params, _, _) => {
1793 prop_assert_eq!(params.get("*").map(|s| s.as_str()), Some(tail.as_str()));
1794 },
1795 other => panic!("expected Found, got {}", route_result_name(&other)),
1796 }
1797 }
1798
1799 #[test]
1802 fn wrong_method_returns_method_not_allowed(path in "/[a-z]{1,5}(/[a-z]{1,5}){0,3}") {
1803 let mut root = RouteNode::new(NodeType::Static("".into()));
1804 root.insert(&path, Method::GET, dummy_handler);
1805 match root.resolve(&path, &Method::POST) {
1806 RouteResult::MethodNotAllowed(allowed) => {
1807 prop_assert!(allowed.contains(&Method::GET));
1808 },
1809 other => panic!("expected MethodNotAllowed, got {}", route_result_name(&other)),
1810 }
1811 }
1812
1813 #[test]
1815 fn multi_param_routes_capture_all(
1816 a in "[a-z0-9]{1,10}",
1817 b in "[a-z0-9]{1,10}"
1818 ) {
1819 let mut root = RouteNode::new(NodeType::Static("".into()));
1820 root.insert("/x/:first/:second", Method::GET, dummy_handler);
1821 let path = format!("/x/{a}/{b}");
1822 match root.resolve(&path, &Method::GET) {
1823 RouteResult::Found(_, params, _, _) => {
1824 prop_assert_eq!(params.get("first").map(|s| s.as_str()), Some(a.as_str()));
1825 prop_assert_eq!(params.get("second").map(|s| s.as_str()), Some(b.as_str()));
1826 },
1827 other => panic!("expected Found, got {}", route_result_name(&other)),
1828 }
1829 }
1830 }
1831 }
1832
1833 #[test]
1836 fn set_and_get_route_config() {
1837 let mut root = RouteNode::new(NodeType::Static("".into()));
1838 root.insert("/api/users", Method::GET, matched_get_handler);
1839
1840 let config = RouteConfig {
1841 certification: crate::certification::CertificationMode::skip(),
1842 ttl: Some(std::time::Duration::from_secs(60)),
1843 headers: vec![],
1844 };
1845 root.set_route_config("/api/users", config);
1846
1847 let rc = root
1848 .get_route_config("/api/users")
1849 .expect("should find config");
1850 assert!(matches!(
1851 rc.certification,
1852 crate::certification::CertificationMode::Skip
1853 ));
1854 assert_eq!(rc.ttl, Some(std::time::Duration::from_secs(60)));
1855 }
1856
1857 #[test]
1858 fn get_route_config_returns_none_for_unknown() {
1859 let root = RouteNode::new(NodeType::Static("".into()));
1860 assert!(root.get_route_config("/nonexistent").is_none());
1861 }
1862
1863 #[test]
1864 fn set_route_config_replaces_existing() {
1865 let mut root = RouteNode::new(NodeType::Static("".into()));
1866 root.insert("/test", Method::GET, matched_get_handler);
1867
1868 let config1 = RouteConfig {
1869 certification: crate::certification::CertificationMode::skip(),
1870 ttl: None,
1871 headers: vec![],
1872 };
1873 root.set_route_config("/test", config1);
1874
1875 let config2 = RouteConfig {
1876 certification: crate::certification::CertificationMode::authenticated(),
1877 ttl: Some(std::time::Duration::from_secs(300)),
1878 headers: vec![],
1879 };
1880 root.set_route_config("/test", config2);
1881
1882 let rc = root.get_route_config("/test").expect("should find config");
1883 assert!(matches!(
1884 rc.certification,
1885 crate::certification::CertificationMode::Full(_)
1886 ));
1887 assert_eq!(rc.ttl, Some(std::time::Duration::from_secs(300)));
1888 }
1889
1890 #[test]
1891 fn routes_without_config_default_to_response_only() {
1892 let mut root = RouteNode::new(NodeType::Static("".into()));
1893 root.insert("/page", Method::GET, matched_get_handler);
1894
1895 assert!(root.get_route_config("/page").is_none());
1898 }
1899
1900 #[test]
1901 fn resolve_returns_correct_pattern_for_static_route() {
1902 let mut root = RouteNode::new(NodeType::Static("".into()));
1903 root.insert("/api/users", Method::GET, matched_get_handler);
1904
1905 match root.resolve("/api/users", &Method::GET) {
1906 RouteResult::Found(_, _, _, pattern) => {
1907 assert_eq!(pattern, "/api/users");
1908 }
1909 other => panic!("expected Found, got {}", route_result_name(&other)),
1910 }
1911 }
1912
1913 #[test]
1914 fn resolve_returns_correct_pattern_for_param_route() {
1915 let mut root = RouteNode::new(NodeType::Static("".into()));
1916 root.insert("/users/:id", Method::GET, matched_get_handler);
1917
1918 match root.resolve("/users/42", &Method::GET) {
1919 RouteResult::Found(_, params, _, pattern) => {
1920 assert_eq!(pattern, "/users/:id");
1921 assert_eq!(params.get("id").unwrap(), "42");
1922 }
1923 other => panic!("expected Found, got {}", route_result_name(&other)),
1924 }
1925 }
1926
1927 #[test]
1928 fn resolve_returns_correct_pattern_for_wildcard_route() {
1929 let mut root = RouteNode::new(NodeType::Static("".into()));
1930 root.insert("/files/*", Method::GET, matched_get_handler);
1931
1932 match root.resolve("/files/a/b/c", &Method::GET) {
1933 RouteResult::Found(_, params, _, pattern) => {
1934 assert_eq!(pattern, "/files/*");
1935 assert_eq!(params.get("*").unwrap(), "a/b/c");
1936 }
1937 other => panic!("expected Found, got {}", route_result_name(&other)),
1938 }
1939 }
1940
1941 #[test]
1942 fn resolve_returns_correct_pattern_for_root_route() {
1943 let mut root = RouteNode::new(NodeType::Static("".into()));
1944 root.insert("/", Method::GET, matched_get_handler);
1945
1946 match root.resolve("/", &Method::GET) {
1947 RouteResult::Found(_, _, _, pattern) => {
1948 assert_eq!(pattern, "/");
1949 }
1950 other => panic!("expected Found, got {}", route_result_name(&other)),
1951 }
1952 }
1953
1954 #[test]
1955 fn resolve_returns_correct_pattern_for_nested_param() {
1956 let mut root = RouteNode::new(NodeType::Static("".into()));
1957 root.insert(
1958 "/posts/:postId/comments/:commentId",
1959 Method::GET,
1960 matched_get_handler,
1961 );
1962
1963 match root.resolve("/posts/10/comments/20", &Method::GET) {
1964 RouteResult::Found(_, params, _, pattern) => {
1965 assert_eq!(pattern, "/posts/:postId/comments/:commentId");
1966 assert_eq!(params.get("postId").unwrap(), "10");
1967 assert_eq!(params.get("commentId").unwrap(), "20");
1968 }
1969 other => panic!("expected Found, got {}", route_result_name(&other)),
1970 }
1971 }
1972
1973 #[test]
1974 fn route_config_lookup_via_pattern() {
1975 let mut root = RouteNode::new(NodeType::Static("".into()));
1976 root.insert("/users/:id", Method::GET, matched_get_handler);
1977
1978 let config = RouteConfig {
1979 certification: crate::certification::CertificationMode::skip(),
1980 ttl: None,
1981 headers: vec![],
1982 };
1983 root.set_route_config("/users/:id", config);
1984
1985 match root.resolve("/users/42", &Method::GET) {
1987 RouteResult::Found(_, _, _, pattern) => {
1988 let rc = root.get_route_config(&pattern).expect("should find config");
1989 assert!(matches!(
1990 rc.certification,
1991 crate::certification::CertificationMode::Skip
1992 ));
1993 }
1994 other => panic!("expected Found, got {}", route_result_name(&other)),
1995 }
1996 }
1997
1998 #[test]
2002 fn test_get_or_create_node_creates_intermediate_nodes() {
2003 let mut root = RouteNode::new(NodeType::Static("".into()));
2004 let _node = root.get_or_create_node("/api/v2/data");
2005
2006 assert_eq!(root.static_children.len(), 1);
2008 let api = root.static_children.get("api").expect("api child");
2009 assert_eq!(api.node_type, NodeType::Static("api".into()));
2010
2011 assert_eq!(api.static_children.len(), 1);
2013 let v2 = api.static_children.get("v2").expect("v2 child");
2014 assert_eq!(v2.node_type, NodeType::Static("v2".into()));
2015
2016 assert_eq!(v2.static_children.len(), 1);
2018 let data = v2.static_children.get("data").expect("data child");
2019 assert_eq!(data.node_type, NodeType::Static("data".into()));
2020 }
2021
2022 #[test]
2025 fn test_get_or_create_node_idempotent() {
2026 let mut root = RouteNode::new(NodeType::Static("".into()));
2027
2028 let node = root.get_or_create_node("/api/users");
2030 node.handlers.insert(Method::GET, matched_get_handler);
2031
2032 let node2 = root.get_or_create_node("/api/users");
2034 assert!(
2035 node2.handlers.contains_key(&Method::GET),
2036 "second call should return the same node with the handler intact"
2037 );
2038
2039 assert_eq!(root.static_children.len(), 1);
2041 let api = root.static_children.get("api").expect("api child");
2042 assert_eq!(api.static_children.len(), 1);
2043 }
2044
2045 #[test]
2047 fn test_get_or_create_node_root_path() {
2048 let mut root = RouteNode::new(NodeType::Static("".into()));
2049
2050 let node = root.get_or_create_node("/");
2051 node.handlers.insert(Method::GET, matched_get_handler);
2052
2053 assert!(root.handlers.contains_key(&Method::GET));
2055 assert!(root.static_children.is_empty());
2057 assert!(root.param_child.is_none());
2058 assert!(root.wildcard_child.is_none());
2059 }
2060
2061 #[test]
2063 fn test_get_or_create_node_param_and_wildcard() {
2064 let mut root = RouteNode::new(NodeType::Static("".into()));
2065
2066 let _node = root.get_or_create_node("/users/:id/files/*");
2067
2068 assert_eq!(root.static_children.len(), 1);
2069 let users = root.static_children.get("users").expect("users child");
2070 assert_eq!(users.node_type, NodeType::Static("users".into()));
2071
2072 let param = users.param_child.as_ref().expect("param child");
2073 assert_eq!(param.node_type, NodeType::Param("id".into()));
2074
2075 assert_eq!(param.static_children.len(), 1);
2076 let files = param.static_children.get("files").expect("files child");
2077 assert_eq!(files.node_type, NodeType::Static("files".into()));
2078
2079 let wc = files.wildcard_child.as_ref().expect("wildcard child");
2080 assert_eq!(wc.node_type, NodeType::Wildcard);
2081 }
2082
2083 #[test]
2089 fn test_param_with_percent_encoded_space_resolves_raw() {
2090 let mut root = RouteNode::new(NodeType::Static("".into()));
2091 root.insert("/posts/:id", Method::GET, matched_deep);
2092 let (_, params) = resolve_get(&root, "/posts/hello%20world");
2093 assert_eq!(
2094 params.get("id").unwrap(),
2095 "hello%20world",
2096 "trie should store the raw percent-encoded value; decoding happens in generated code"
2097 );
2098 }
2099
2100 #[test]
2102 fn test_wildcard_with_percent_encoded_space_resolves_raw() {
2103 let mut root = RouteNode::new(NodeType::Static("".into()));
2104 root.insert("/files/*", Method::GET, matched_folder);
2105 let (_, params) = resolve_get(&root, "/files/hello%20world/doc.pdf");
2106 assert_eq!(
2107 params.get("*").unwrap(),
2108 "hello%20world/doc.pdf",
2109 "trie should store the raw percent-encoded wildcard value"
2110 );
2111 }
2112
2113 #[test]
2119 fn multiple_param_children_first_wins() {
2120 fn handler_a(_: HttpRequest, params: RouteParams) -> HttpResponse<'static> {
2121 response_with_text(&format!("a:{}", params.get("a").unwrap_or(&String::new())))
2122 }
2123 fn handler_b(_: HttpRequest, params: RouteParams) -> HttpResponse<'static> {
2124 response_with_text(&format!("b:{}", params.get("b").unwrap_or(&String::new())))
2125 }
2126
2127 let mut root = RouteNode::new(NodeType::Static("".into()));
2128 root.insert("/items/:a", Method::GET, handler_a);
2129 root.insert("/items/:b", Method::POST, handler_b);
2132
2133 let (handler, params) = resolve_get(&root, "/items/42");
2135 assert_eq!(params.get("a"), Some(&"42".to_string()));
2136 assert_eq!(params.get("b"), None);
2137 assert_eq!(body_str(handler(test_request("/items/42"), params)), "a:42");
2138
2139 match root.resolve("/items/99", &Method::POST) {
2142 RouteResult::Found(handler, params, _, _) => {
2143 assert_eq!(params.get("a"), Some(&"99".to_string()));
2144 assert_eq!(params.get("b"), None);
2145 assert_eq!(body_str(handler(test_request("/items/99"), params)), "b:");
2147 }
2148 other => panic!("expected Found, got {}", route_result_name(&other)),
2149 }
2150 }
2151
2152 #[test]
2154 fn wildcard_consumes_remaining_segments() {
2155 let mut root = RouteNode::new(NodeType::Static("".into()));
2156 root.insert("/files/*", Method::GET, matched_folder);
2157
2158 let (_, params) = resolve_get(&root, "/files/a");
2160 assert_eq!(params.get("*").unwrap(), "a");
2161
2162 let (_, params) = resolve_get(&root, "/files/a/b/c");
2164 assert_eq!(params.get("*").unwrap(), "a/b/c");
2165
2166 let (_, params) = resolve_get(&root, "/files/a/b/c/d/e");
2168 assert_eq!(params.get("*").unwrap(), "a/b/c/d/e");
2169 }
2170
2171 #[test]
2175 fn post_wildcard_segments_unreachable() {
2176 fn edit_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
2177 response_with_text("edit")
2178 }
2179
2180 let mut root = RouteNode::new(NodeType::Static("".into()));
2181 root.insert("/files/*", Method::GET, matched_folder);
2182 root.insert("/files/*/edit", Method::GET, edit_handler);
2185
2186 let (handler, params) = resolve_get(&root, "/files/something/edit");
2188 assert_eq!(params.get("*").unwrap(), "something/edit");
2189 assert_eq!(
2190 body_str(handler(test_request("/files/something/edit"), params)),
2191 "folder"
2192 );
2193 }
2194
2195 #[test]
2197 fn empty_path_resolves_to_root() {
2198 let mut root = RouteNode::new(NodeType::Static("".into()));
2199 root.insert("/", Method::GET, matched_root);
2200
2201 let (handler, params) = resolve_get(&root, "/");
2202 assert!(params.is_empty());
2203 assert_eq!(body_str(handler(test_request("/"), params)), "root");
2204 }
2205
2206 #[test]
2208 fn trailing_slash_normalization() {
2209 let mut root = RouteNode::new(NodeType::Static("".into()));
2210 root.insert("/about", Method::GET, matched_about);
2211
2212 let (handler, _) = resolve_get(&root, "/about");
2214 assert_eq!(
2215 body_str(handler(test_request("/about"), HashMap::new())),
2216 "about"
2217 );
2218
2219 let (handler, _) = resolve_get(&root, "/about/");
2221 assert_eq!(
2222 body_str(handler(test_request("/about/"), HashMap::new())),
2223 "about"
2224 );
2225 }
2226}