1use std::collections::HashMap;
41use std::sync::Arc;
42
43use rainier_container::Container;
44use rainier_http::{IntoResponse, Method, Request, Response};
45use rainier_middleware::{
46 ConcreteMiddleware, Destination, IntoMiddlewareStack, Middleware, MiddlewareStack, Pipeline,
47 ReadyPipeline,
48};
49use rainier_support::{BoxedFuture, Error, Result};
50
51use crate::handler::{IntoRouteHandler, RouteHandler};
52use crate::route::{normalise_uri, ParamConstraint, Route};
53
54#[derive(Debug, Clone)]
57pub struct MatchedRoute {
58 pub name: Option<String>,
60 pub uri: String,
62 pub methods: Vec<Method>,
64}
65
66#[derive(Debug, Default, Clone)]
69pub struct GroupAttributes {
70 prefix: String,
71 name_prefix: String,
72 middleware: MiddlewareStack,
73 constraints: HashMap<String, ParamConstraint>,
74}
75
76impl GroupAttributes {
77 pub fn new() -> Self {
79 Self::default()
80 }
81
82 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
84 self.prefix = prefix.into();
85 self
86 }
87
88 pub fn name(mut self, prefix: impl Into<String>) -> Self {
91 self.name_prefix = prefix.into();
92 self
93 }
94
95 pub fn middleware(mut self, middleware: impl IntoMiddlewareStack) -> Self {
106 self.middleware = self.middleware.with_stack(middleware.into_middleware_stack());
107 self
108 }
109
110 pub fn where_param(mut self, param: impl Into<String>, constraint: ParamConstraint) -> Self {
113 self.constraints.insert(param.into(), constraint);
114 self
115 }
116}
117
118#[derive(Default)]
120pub struct Router {
121 routes: Vec<Route>,
122 fallback: Option<Arc<dyn RouteHandler>>,
123}
124
125impl Router {
126 pub fn new() -> Self {
128 Self::default()
129 }
130
131 pub fn add<H, Args>(
135 &mut self,
136 methods: Vec<Method>,
137 uri: impl Into<String>,
138 handler: H,
139 ) -> &mut Route
140 where
141 H: IntoRouteHandler<Args>,
142 {
143 self.add_erased(methods, uri, handler.into_route_handler())
144 }
145
146 pub fn add_erased(
152 &mut self,
153 methods: Vec<Method>,
154 uri: impl Into<String>,
155 handler: Arc<dyn RouteHandler>,
156 ) -> &mut Route {
157 self.routes.push(Route::new(methods, uri, handler));
158 self.routes.last_mut().expect("just pushed")
159 }
160
161 pub fn get<H, Args>(&mut self, uri: impl Into<String>, handler: H) -> &mut Route
163 where
164 H: IntoRouteHandler<Args>,
165 {
166 self.add(vec![Method::GET], uri, handler)
167 }
168
169 pub fn post<H, Args>(&mut self, uri: impl Into<String>, handler: H) -> &mut Route
171 where
172 H: IntoRouteHandler<Args>,
173 {
174 self.add(vec![Method::POST], uri, handler)
175 }
176
177 pub fn put<H, Args>(&mut self, uri: impl Into<String>, handler: H) -> &mut Route
179 where
180 H: IntoRouteHandler<Args>,
181 {
182 self.add(vec![Method::PUT], uri, handler)
183 }
184
185 pub fn patch<H, Args>(&mut self, uri: impl Into<String>, handler: H) -> &mut Route
187 where
188 H: IntoRouteHandler<Args>,
189 {
190 self.add(vec![Method::PATCH], uri, handler)
191 }
192
193 pub fn delete<H, Args>(&mut self, uri: impl Into<String>, handler: H) -> &mut Route
195 where
196 H: IntoRouteHandler<Args>,
197 {
198 self.add(vec![Method::DELETE], uri, handler)
199 }
200
201 pub fn options<H, Args>(&mut self, uri: impl Into<String>, handler: H) -> &mut Route
203 where
204 H: IntoRouteHandler<Args>,
205 {
206 self.add(vec![Method::OPTIONS], uri, handler)
207 }
208
209 pub fn any<H, Args>(&mut self, uri: impl Into<String>, handler: H) -> &mut Route
211 where
212 H: IntoRouteHandler<Args>,
213 {
214 self.add(
215 vec![
216 Method::GET,
217 Method::POST,
218 Method::PUT,
219 Method::PATCH,
220 Method::DELETE,
221 Method::OPTIONS,
222 ],
223 uri,
224 handler,
225 )
226 }
227
228 pub fn redirect(&mut self, from: impl Into<String>, to: impl Into<String>) -> &mut Route {
230 let to = to.into();
231 self.get(from, move || {
232 let to = to.clone();
233 async move { rainier_http::Redirect::permanent(to) }
234 })
235 }
236
237 pub fn fallback<H, Args>(&mut self, handler: H)
241 where
242 H: IntoRouteHandler<Args>,
243 {
244 self.fallback = Some(handler.into_route_handler());
245 }
246
247 pub fn group(&mut self, attributes: GroupAttributes, declare: impl FnOnce(&mut Router)) {
256 let start = self.routes.len();
257 declare(self);
258
259 for route in &mut self.routes[start..] {
260 route.prefix_with(&attributes.prefix);
261 route.prepend_middleware(&attributes.middleware);
262 route.prepend_name(&attributes.name_prefix);
263 route.add_constraints(&attributes.constraints);
264 }
265 }
266
267 pub fn merge(&mut self, other: Router) {
269 self.routes.extend(other.routes);
270 if self.fallback.is_none() {
271 self.fallback = other.fallback;
272 }
273 }
274
275 pub fn routes(&self) -> &[Route] {
279 &self.routes
280 }
281
282 pub fn routes_mut(&mut self) -> &mut [Route] {
284 &mut self.routes
285 }
286
287 pub fn len(&self) -> usize {
289 self.routes.len()
290 }
291
292 pub fn is_empty(&self) -> bool {
294 self.routes.is_empty()
295 }
296
297 pub fn uri_for(&self, name: &str) -> Option<&str> {
299 self.routes.iter().find(|route| route.route_name() == Some(name)).map(|route| route.uri())
300 }
301
302 pub fn named_routes(&self) -> Vec<(&str, &str)> {
304 self.routes
305 .iter()
306 .filter_map(|route| route.route_name().map(|name| (name, route.uri())))
307 .collect()
308 }
309
310 pub fn describe(&self) -> Vec<RouteSummary> {
324 self.routes
325 .iter()
326 .map(|route| RouteSummary {
327 methods: route.methods().iter().map(|m| m.to_string()).collect(),
328 uri: route.uri().to_string(),
329 name: route.route_name().map(str::to_string),
330 middleware: route.middleware_stack().labels(),
331 })
332 .collect()
333 }
334
335 pub fn compile(self, container: &Container) -> Result<CompiledRouter> {
343 let mut duplicate_names: HashMap<&str, usize> = HashMap::new();
344 for route in &self.routes {
345 if let Some(name) = route.route_name() {
346 *duplicate_names.entry(name).or_insert(0) += 1;
347 }
348 }
349 if let Some((name, _)) = duplicate_names.into_iter().find(|(_, count)| *count > 1) {
350 return Err(Error::internal(format!(
351 "two routes are both named `{name}` — route names must be unique for URL \
352 generation to be unambiguous"
353 )));
354 }
355
356 let mut compiled = Vec::with_capacity(self.routes.len());
357 for route in self.routes {
358 let pipeline = build_pipeline(&route, container)?;
359 compiled.push(CompiledRoute { route, pipeline });
360 }
361
362 let fallback = self.fallback.map(|handler| {
363 let destination: Arc<dyn Destination> = Arc::new(HandlerDestination(handler));
364 Pipeline::new().then_arc(destination)
365 });
366
367 Ok(CompiledRouter { routes: compiled, fallback })
368 }
369}
370
371fn build_pipeline(route: &Route, container: &Container) -> Result<ReadyPipeline> {
372 let resolved = route.middleware_stack().resolve(container).map_err(|e| {
376 Error::internal(format!(
377 "route `{} {}`: {e}",
378 route.methods().first().map(Method::as_str).unwrap_or("?"),
379 route.uri()
380 ))
381 })?;
382
383 let excluded = route.excluded_middleware();
386 let resolved: Vec<Arc<dyn Middleware>> = resolved
387 .into_iter()
388 .filter(|stage| !excluded.contains(&ConcreteMiddleware::concrete_type_id(&**stage)))
389 .collect();
390
391 let destination: Arc<dyn Destination> =
392 Arc::new(HandlerDestination(Arc::clone(route.handler())));
393 Ok(Pipeline::new().through_all(resolved).then_arc(destination))
394}
395
396struct HandlerDestination(Arc<dyn RouteHandler>);
398
399impl Destination for HandlerDestination {
400 fn call(&self, request: Request) -> BoxedFuture<Response> {
401 self.0.call(request)
402 }
403}
404
405struct CompiledRoute {
406 route: Route,
407 pipeline: ReadyPipeline,
408}
409
410pub struct CompiledRouter {
412 routes: Vec<CompiledRoute>,
413 fallback: Option<ReadyPipeline>,
414}
415
416impl CompiledRouter {
417 pub async fn dispatch(&self, mut request: Request) -> Response {
424 let path = request.path().to_string();
425 let mut allowed: Vec<Method> = Vec::new();
426
427 for compiled in &self.routes {
428 let Some(params) = compiled.route.match_path(&path) else {
429 continue;
430 };
431
432 if !compiled.route.accepts(request.method()) {
433 for method in compiled.route.methods() {
434 if !allowed.contains(method) {
435 allowed.push(method.clone());
436 }
437 }
438 continue;
439 }
440
441 request.set_route_params(params);
445 request.extensions_mut().insert(MatchedRoute {
446 name: compiled.route.route_name().map(str::to_string),
447 uri: compiled.route.uri().to_string(),
448 methods: compiled.route.methods().to_vec(),
449 });
450
451 return compiled.pipeline.run(request).await;
452 }
453
454 if !allowed.is_empty() {
455 let allow = allowed.iter().map(Method::as_str).collect::<Vec<_>>().join(", ");
456 return Error::new(
457 rainier_support::ErrorKind::MethodNotAllowed,
458 format!("The {} method is not supported for this route.", request.method()),
459 )
460 .into_response()
461 .with_header("allow", &allow);
462 }
463
464 match &self.fallback {
465 Some(fallback) => fallback.run(request).await,
466 None => Error::not_found(format!("No route matches {path}")).into_response(),
467 }
468 }
469
470 pub fn describe(&self) -> Vec<RouteSummary> {
472 self.routes
473 .iter()
474 .map(|compiled| RouteSummary {
475 methods: compiled.route.methods().iter().map(|m| m.to_string()).collect(),
476 uri: compiled.route.uri().to_string(),
477 name: compiled.route.route_name().map(str::to_string),
478 middleware: compiled.pipeline.stage_names(),
479 })
480 .collect()
481 }
482
483 pub fn uri_for(&self, name: &str) -> Option<&str> {
485 self.routes
486 .iter()
487 .find(|compiled| compiled.route.route_name() == Some(name))
488 .map(|compiled| compiled.route.uri())
489 }
490
491 pub fn named_routes(&self) -> Vec<(String, String)> {
494 self.routes
495 .iter()
496 .filter_map(|compiled| {
497 compiled
498 .route
499 .route_name()
500 .map(|name| (name.to_string(), compiled.route.uri().to_string()))
501 })
502 .collect()
503 }
504
505 pub fn len(&self) -> usize {
507 self.routes.len()
508 }
509
510 pub fn is_empty(&self) -> bool {
512 self.routes.is_empty()
513 }
514}
515
516impl std::fmt::Debug for CompiledRouter {
517 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
518 f.debug_struct("CompiledRouter")
519 .field("routes", &self.len())
520 .field("fallback", &self.fallback.is_some())
521 .finish()
522 }
523}
524
525#[derive(Debug, Clone, PartialEq, Eq)]
527pub struct RouteSummary {
528 pub methods: Vec<String>,
530 pub uri: String,
532 pub name: Option<String>,
534 pub middleware: Vec<&'static str>,
536}
537
538pub async fn not_found() -> Response {
540 Error::not_found("Not Found").into_response()
541}
542
543pub fn normalise(uri: &str) -> String {
546 normalise_uri(uri)
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552 use crate::handler::Req;
553 use rainier_http::StatusCode;
554 use rainier_middleware::{Middleware, MiddlewareStack, Next};
555
556 struct Unbound;
558
559 struct Nothing;
562
563 #[async_trait::async_trait]
564 impl Middleware for Nothing {
565 async fn handle(&self, request: Request, next: Next) -> Response {
566 next.run(request).await
567 }
568 }
569
570 async fn index() -> &'static str {
571 "index"
572 }
573 async fn show(request: Req) -> String {
574 format!("show:{}", request.route_param("post").unwrap_or("?"))
575 }
576 async fn store() -> &'static str {
577 "store"
578 }
579
580 async fn body_of(response: Response) -> String {
581 String::from_utf8(response.into_http().into_body().collect().await.unwrap().to_vec())
582 .unwrap()
583 }
584
585 fn get(uri: &str) -> Request {
586 Request::builder().method(Method::GET).uri(uri).build()
587 }
588
589 fn compile(router: Router) -> CompiledRouter {
590 router.compile(&Container::new()).expect("compiles")
591 }
592
593 #[tokio::test]
594 async fn dispatches_to_a_static_route() {
595 let mut router = Router::new();
596 router.get("/posts", index);
597
598 let response = compile(router).dispatch(get("/posts")).await;
599 assert_eq!(body_of(response).await, "index");
600 }
601
602 #[tokio::test]
603 async fn captures_route_parameters() {
604 let mut router = Router::new();
605 router.get("/posts/{post}", show);
606
607 let response = compile(router).dispatch(get("/posts/7")).await;
608 assert_eq!(body_of(response).await, "show:7");
609 }
610
611 #[tokio::test]
612 async fn an_unmatched_path_is_a_404() {
613 let mut router = Router::new();
614 router.get("/posts", index);
615
616 let response = compile(router).dispatch(get("/nope")).await;
617 assert_eq!(response.status(), StatusCode::NOT_FOUND);
618 }
619
620 #[tokio::test]
621 async fn a_matched_path_with_the_wrong_method_is_a_405_listing_what_is_allowed() {
622 let mut router = Router::new();
623 router.get("/posts", index);
624 router.post("/posts", store);
625
626 let request = Request::builder().method(Method::DELETE).uri("/posts").build();
627 let response = compile(router).dispatch(request).await;
628
629 assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
630 let allow = response.header("allow").unwrap().to_string();
631 assert!(allow.contains("GET"), "{allow}");
632 assert!(allow.contains("POST"), "{allow}");
633 }
634
635 #[tokio::test]
636 async fn the_first_declared_route_wins() {
637 let mut router = Router::new();
638 router.get("/posts/create", || async { "create form" });
639 router.get("/posts/{post}", show);
640
641 let compiled = compile(router);
642 assert_eq!(body_of(compiled.dispatch(get("/posts/create")).await).await, "create form");
643 assert_eq!(body_of(compiled.dispatch(get("/posts/9")).await).await, "show:9");
644 }
645
646 #[tokio::test]
647 async fn a_constraint_lets_a_later_route_win() {
648 let mut router = Router::new();
649 router.get("/posts/{post}", show).where_number("post");
650 router.get("/posts/create", || async { "create form" });
651
652 let compiled = compile(router);
653 assert_eq!(body_of(compiled.dispatch(get("/posts/create")).await).await, "create form");
655 assert_eq!(body_of(compiled.dispatch(get("/posts/9")).await).await, "show:9");
656 }
657
658 #[tokio::test]
659 async fn the_fallback_catches_everything_unmatched() {
660 let mut router = Router::new();
661 router.get("/posts", index);
662 router.fallback(|| async { "nothing here" });
663
664 let response = compile(router).dispatch(get("/anywhere/else")).await;
665 assert_eq!(body_of(response).await, "nothing here");
666 }
667
668 #[tokio::test]
669 async fn groups_apply_a_prefix_and_a_name() {
670 let mut router = Router::new();
671 router.group(GroupAttributes::new().prefix("api/v1").name("api."), |router| {
672 router.get("/posts", index).name("posts.index");
673 });
674
675 let compiled = compile(router);
676 assert_eq!(compiled.uri_for("api.posts.index"), Some("/api/v1/posts"));
677 assert_eq!(compiled.dispatch(get("/api/v1/posts")).await.status(), StatusCode::OK);
678 assert_eq!(compiled.dispatch(get("/posts")).await.status(), StatusCode::NOT_FOUND);
679 }
680
681 #[tokio::test]
682 async fn groups_nest_outermost_first() {
683 let mut router = Router::new();
684 router.group(GroupAttributes::new().prefix("api").name("api."), |router| {
685 router.group(GroupAttributes::new().prefix("v1").name("v1."), |router| {
686 router.get("/posts", index).name("posts.index");
687 });
688 });
689
690 let compiled = compile(router);
691 assert_eq!(compiled.uri_for("api.v1.posts.index"), Some("/api/v1/posts"));
692 }
693
694 #[tokio::test]
695 async fn group_constraints_apply_to_every_route_that_lacks_its_own() {
696 let mut router = Router::new();
697 router.group(
698 GroupAttributes::new().where_param("post", ParamConstraint::Number),
699 |router| {
700 router.get("/posts/{post}", show);
701 },
702 );
703
704 let compiled = compile(router);
705 assert_eq!(compiled.dispatch(get("/posts/7")).await.status(), StatusCode::OK);
706 assert_eq!(compiled.dispatch(get("/posts/abc")).await.status(), StatusCode::NOT_FOUND);
707 }
708
709 struct Tag(&'static str);
710
711 #[async_trait::async_trait]
712 impl Middleware for Tag {
713 async fn handle(&self, request: Request, next: Next) -> Response {
714 next.run(request).await.with_added_header("x-tag", self.0)
715 }
716 fn name(&self) -> &'static str {
717 "Tag"
718 }
719 }
720
721 struct Audit;
723
724 #[async_trait::async_trait]
725 impl Middleware for Audit {
726 async fn handle(&self, request: Request, next: Next) -> Response {
727 next.run(request).await.with_added_header("x-tag", "audit")
728 }
729 }
730
731 #[tokio::test]
732 async fn route_and_group_middleware_both_run() {
733 let mut router = Router::new();
734 router.group(GroupAttributes::new().middleware(Tag("group")), |router| {
735 router.get("/posts", index).middleware(Tag("route"));
736 });
737
738 let compiled = router.compile(&Container::new()).unwrap();
739 let response = compiled.dispatch(get("/posts")).await;
740 let tags: Vec<_> =
741 response.headers().get_all("x-tag").iter().map(|v| v.to_str().unwrap()).collect();
742 assert_eq!(tags.len(), 2);
743 }
744
745 #[tokio::test]
746 async fn a_tuple_attaches_several_in_order() {
747 let mut router = Router::new();
748 router.get("/posts", index).middleware((Tag("first"), Audit));
749
750 let compiled = router.compile(&Container::new()).unwrap();
751 let response = compiled.dispatch(get("/posts")).await;
752 let tags: Vec<_> =
753 response.headers().get_all("x-tag").iter().map(|v| v.to_str().unwrap()).collect();
754 assert_eq!(tags, vec!["audit", "first"], "the outermost runs last on the way out");
755 }
756
757 #[tokio::test]
758 async fn a_route_can_opt_out_of_group_middleware_by_type() {
759 let mut router = Router::new();
760 router.group(GroupAttributes::new().middleware((Tag("group"), Audit)), |router| {
761 router.get("/open", index).without_middleware::<Audit>();
762 router.get("/closed", index);
763 });
764
765 let compiled = router.compile(&Container::new()).unwrap();
766
767 let open = compiled.dispatch(get("/open")).await;
768 let open_tags: Vec<_> =
769 open.headers().get_all("x-tag").iter().map(|v| v.to_str().unwrap()).collect();
770 assert_eq!(open_tags, vec!["group"], "only the excluded type is dropped");
771
772 let closed = compiled.dispatch(get("/closed")).await;
773 assert_eq!(closed.headers().get_all("x-tag").iter().count(), 2);
774 }
775
776 #[test]
777 fn a_deferred_stage_that_cannot_be_built_names_the_route() {
778 let mut router = Router::new();
782 router.get("/posts", index).middleware(
783 MiddlewareStack::new()
784 .deferred(|_| Err::<Audit, _>(Error::internal("AuthManager is not bound"))),
785 );
786
787 let err = router.compile(&Container::new()).err().expect("compilation should fail");
788 assert!(err.message().contains("/posts"), "{}", err.message());
789 assert!(err.message().contains("Audit"), "{}", err.message());
790 assert!(err.message().contains("AuthManager"), "{}", err.message());
791 }
792
793 #[test]
794 fn duplicate_route_names_are_rejected() {
795 let mut router = Router::new();
796 router.get("/a", index).name("same");
797 router.get("/b", index).name("same");
798
799 let err = router.compile(&Container::new()).err().expect("should fail");
800 assert!(err.message().contains("`same`"), "{}", err.message());
801 }
802
803 #[tokio::test]
804 async fn the_matched_route_is_readable_from_the_request() {
805 async fn reveal(request: Req) -> String {
806 request
807 .extension::<MatchedRoute>()
808 .map(|route| format!("{}|{:?}", route.uri, route.name))
809 .unwrap_or_default()
810 }
811
812 let mut router = Router::new();
813 router.get("/posts/{post}", reveal).name("posts.show");
814
815 let response = compile(router).dispatch(get("/posts/1")).await;
816 assert_eq!(body_of(response).await, "/posts/{post}|Some(\"posts.show\")");
817 }
818
819 #[tokio::test]
820 async fn redirect_routes_send_a_location() {
821 let mut router = Router::new();
822 router.redirect("/old", "/new");
823
824 let response = compile(router).dispatch(get("/old")).await;
825 assert_eq!(response.status(), StatusCode::MOVED_PERMANENTLY);
826 assert_eq!(response.header("location"), Some("/new"));
827 }
828
829 #[tokio::test]
830 async fn merging_keeps_both_route_sets() {
831 let mut web = Router::new();
832 web.get("/", index).name("home");
833
834 let mut api = Router::new();
835 api.get("/api/posts", index).name("api.posts");
836
837 web.merge(api);
838 let compiled = compile(web);
839 assert_eq!(compiled.len(), 2);
840 assert_eq!(compiled.dispatch(get("/api/posts")).await.status(), StatusCode::OK);
841 }
842
843 #[tokio::test]
844 async fn describe_reports_the_table() {
845 let mut router = Router::new();
846 router.get("/posts", index).name("posts.index").middleware(Tag("route"));
847
848 let compiled = router.compile(&Container::new()).unwrap();
849 let rows = compiled.describe();
850 assert_eq!(rows.len(), 1);
851 assert_eq!(rows[0].uri, "/posts");
852 assert_eq!(rows[0].name.as_deref(), Some("posts.index"));
853 assert!(rows[0].methods.contains(&"GET".to_string()));
854 assert_eq!(rows[0].middleware, vec!["Tag"]);
855 }
856
857 #[tokio::test]
858 async fn head_is_served_wherever_get_is() {
859 let mut router = Router::new();
860 router.get("/posts", index);
861
862 let request = Request::builder().method(Method::HEAD).uri("/posts").build();
863 assert_eq!(compile(router).dispatch(request).await.status(), StatusCode::OK);
864 }
865
866 #[test]
867 fn an_uncompiled_router_can_still_describe_itself() {
868 let mut router = Router::new();
870 router.get("/", || async { "home" }).name("home");
871 router.post("/posts", || async { "create" }).name("posts.store").middleware(
872 MiddlewareStack::new().resolved(|_: Arc<Unbound>| Nothing),
875 );
876
877 let table = router.describe();
878
879 assert_eq!(table.len(), 2);
880 assert_eq!(table[0].uri, "/");
881 assert_eq!(table[0].name.as_deref(), Some("home"));
882 assert_eq!(table[1].methods, vec!["POST"]);
883 assert_eq!(table[1].middleware.len(), 1);
884
885 assert!(router.compile(&Container::new()).is_err());
887 }
888
889 #[tokio::test]
890 async fn describing_uncompiled_matches_describing_compiled() {
891 let mut router = Router::new();
894 router.get("/", || async { "home" }).name("home");
895 router.put("/posts/{post}", || async { "update" }).name("posts.update");
896
897 let before = router.describe();
898 let after = router.compile(&Container::new()).unwrap().describe();
899
900 assert_eq!(before.len(), after.len());
901 for (before, after) in before.iter().zip(after.iter()) {
902 assert_eq!(before.methods, after.methods);
903 assert_eq!(before.uri, after.uri);
904 assert_eq!(before.name, after.name);
905 }
906 }
907}