reinhardt_urls/routers/server_router/
builder.rs1use super::ServerRouter;
7use super::types::MiddlewareInfo;
8use crate::routers::UrlReverser;
9use matchit::Router as MatchitRouter;
10use reinhardt_di::InjectionContext;
11use reinhardt_http::ExcludeMiddleware;
12use reinhardt_middleware::Middleware;
13#[cfg(feature = "viewsets")]
14use std::collections::HashMap;
15use std::sync::{Arc, RwLock};
16
17impl ServerRouter {
18 fn validate_prefix(prefix: &str) {
44 if prefix.contains('{') || prefix.contains('}') {
49 panic!(
50 "`mount()` prefix `{prefix}` contains a path parameter placeholder (`{{...}}`); this is not supported.\nUse `route()` with the full path on the child router instead, or mount at a literal prefix."
51 );
52 }
53
54 if !prefix.ends_with('/') {
56 if prefix.is_empty() {
57 panic!(
58 "URL route prefix cannot be an empty string. \
59 Use '/' instead of ''. \
60 This follows Django URL configuration conventions."
61 );
62 } else {
63 panic!(
64 "URL route '{}' must end with a trailing slash '/'. \
65 Use '{}/' instead of '{}'. \
66 This follows Django URL configuration conventions.",
67 prefix, prefix, prefix,
68 );
69 }
70 }
71 }
72
73 pub fn new() -> Self {
83 Self {
84 prefix: String::new(),
85 namespace: None,
86 routes: Vec::new(),
87 #[cfg(feature = "viewsets")]
88 viewsets: HashMap::new(),
89 functions: Vec::new(),
90 views: Vec::new(),
91 children: Vec::new(),
92 di_context: None,
93 pending_middleware_di: reinhardt_di::DiRegistrationList::new(),
94 middleware: Vec::new(),
95 middleware_names: Vec::new(),
96 middleware_exclusions: Vec::new(),
97 reverser: UrlReverser::new(),
98 get_router: RwLock::new(MatchitRouter::new()),
99 post_router: RwLock::new(MatchitRouter::new()),
100 put_router: RwLock::new(MatchitRouter::new()),
101 delete_router: RwLock::new(MatchitRouter::new()),
102 patch_router: RwLock::new(MatchitRouter::new()),
103 head_router: RwLock::new(MatchitRouter::new()),
104 options_router: RwLock::new(MatchitRouter::new()),
105 routes_compiled: RwLock::new(false),
106 }
107 }
108
109 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
120 self.prefix = prefix.into();
121 self
122 }
123
124 pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
135 self.namespace = Some(namespace.into());
136 self
137 }
138
139 pub fn with_di_context(mut self, ctx: Arc<InjectionContext>) -> Self {
154 Self::adopt_di_context_recursive(&mut self, &ctx);
163 self.di_context = Some(ctx);
164 self
165 }
166
167 fn inherit_context_from_parent_if_any(parent: &ServerRouter, child: &mut ServerRouter) {
178 if child.di_context.is_none()
179 && let Some(parent_ctx) = parent.di_context.as_ref()
180 {
181 Self::adopt_di_context_recursive(child, parent_ctx);
182 child.di_context = Some(Arc::clone(parent_ctx));
183 }
184 }
185
186 fn adopt_di_context_recursive(router: &mut ServerRouter, ctx: &Arc<InjectionContext>) {
187 if !router.pending_middleware_di.is_empty() {
188 let pending = std::mem::take(&mut router.pending_middleware_di);
189 pending.apply_to(ctx.singleton_scope());
190 }
191 for child in router.children.iter_mut() {
192 if child.di_context.is_none() {
193 Self::adopt_di_context_recursive(child, ctx);
194 child.di_context = Some(Arc::clone(ctx));
195 }
196 }
197 }
198
199 pub(crate) fn di_context(&self) -> Option<&Arc<InjectionContext>> {
201 self.di_context.as_ref()
202 }
203
204 pub fn with_middleware<M: Middleware + 'static>(mut self, mw: M) -> Self {
216 let full_type_name = std::any::type_name::<M>().to_string();
217 let short_name = full_type_name
218 .rsplit("::")
219 .next()
220 .unwrap_or(&full_type_name)
221 .to_string();
222 let di_entries = mw.di_registrations();
234 if !di_entries.is_empty() {
235 if let Some(ctx) = self.di_context.as_ref() {
236 let scope = ctx.singleton_scope();
237 for (type_id, value) in di_entries {
238 scope.set_arc_any(type_id, value);
239 }
240 } else {
241 for (type_id, value) in di_entries {
242 self.pending_middleware_di.register_arc_any(type_id, value);
243 }
244 }
245 }
246 self.middleware_names.push(MiddlewareInfo {
247 name: short_name,
248 type_name: full_type_name,
249 });
250 self.middleware.push(Arc::new(mw));
251 self.middleware_exclusions.push(Vec::new());
252 self
253 }
254
255 pub fn exclude(mut self, pattern: &str) -> Self {
281 assert!(
282 !self.middleware_exclusions.is_empty(),
283 "exclude() called with no middleware. Call with_middleware() first."
284 );
285 self.middleware_exclusions
286 .last_mut()
287 .unwrap()
288 .push(pattern.to_string());
289 self
290 }
291
292 pub(crate) fn build_middleware_with_exclusions(&self) -> Vec<Arc<dyn Middleware>> {
294 let mut result: Vec<Arc<dyn Middleware>> = Vec::with_capacity(self.middleware.len());
295
296 for (mw, exclusions) in self
297 .middleware
298 .iter()
299 .zip(self.middleware_exclusions.iter())
300 {
301 if exclusions.is_empty() {
302 result.push(mw.clone());
303 } else {
304 let mut exclude_mw = ExcludeMiddleware::new(mw.clone());
305 for pattern in exclusions {
306 exclude_mw.add_exclusion_mut(pattern);
307 }
308 result.push(Arc::new(exclude_mw) as Arc<dyn Middleware>);
309 }
310 }
311
312 result
313 }
314
315 pub fn mount(mut self, prefix: &str, mut child: ServerRouter) -> Self {
353 Self::validate_prefix(prefix);
355
356 if child.prefix.is_empty() {
358 child.prefix = prefix.to_string();
359 }
360
361 Self::inherit_context_from_parent_if_any(&self, &mut child);
369
370 self.children.push(child);
371 self
372 }
373
374 pub fn mount_mut(&mut self, prefix: &str, mut child: ServerRouter) {
387 Self::validate_prefix(prefix);
389
390 if child.prefix.is_empty() {
391 child.prefix = prefix.to_string();
392 }
393 Self::inherit_context_from_parent_if_any(self, &mut child);
395 self.children.push(child);
396 }
397
398 pub fn group(mut self, routers: Vec<ServerRouter>) -> Self {
415 for mut router in routers {
416 Self::inherit_context_from_parent_if_any(&self, &mut router);
423 self.children.push(router);
424 }
425 self
426 }
427}
428
429#[cfg(test)]
430mod middleware_di_tests {
431 use super::*;
432 use async_trait::async_trait;
433 use reinhardt_core::exception::Result;
434 use reinhardt_di::{InjectionContext, SingletonScope};
435 use reinhardt_http::{Handler, Request, Response};
436 use rstest::rstest;
437 use std::any::TypeId;
438 use std::sync::Arc;
439
440 #[derive(Debug, PartialEq, Eq)]
441 struct DummyState(&'static str);
442
443 struct DummyMiddleware {
444 state: Arc<DummyState>,
445 }
446
447 #[async_trait]
448 impl Middleware for DummyMiddleware {
449 async fn process(&self, request: Request, handler: Arc<dyn Handler>) -> Result<Response> {
450 handler.handle(request).await
451 }
452
453 fn di_registrations(&self) -> Vec<reinhardt_http::MiddlewareDiRegistration> {
454 vec![(
455 TypeId::of::<DummyState>(),
456 Arc::clone(&self.state) as Arc<dyn std::any::Any + Send + Sync>,
457 )]
458 }
459 }
460
461 fn make_mw(tag: &'static str) -> DummyMiddleware {
462 DummyMiddleware {
463 state: Arc::new(DummyState(tag)),
464 }
465 }
466
467 #[rstest]
468 #[serial_test::serial(global_di)]
469 fn with_middleware_before_with_di_context_applies_to_context() {
470 let scope = Arc::new(SingletonScope::new());
473 let ctx = Arc::new(InjectionContext::builder(Arc::clone(&scope)).build());
474
475 let _router = ServerRouter::new()
477 .with_middleware(make_mw("before-context"))
478 .with_di_context(Arc::clone(&ctx));
479
480 let leaked = crate::routers::take_di_registrations();
482
483 let resolved = scope
486 .get::<DummyState>()
487 .expect("with_di_context must drain pending middleware DI into the new context");
488 assert_eq!(resolved.0, "before-context");
489 assert!(
490 leaked.is_none(),
491 "pending middleware DI must not leak into the global deferred list when a context is attached later"
492 );
493 }
494
495 #[rstest]
496 #[serial_test::serial(global_di)]
497 fn with_middleware_after_with_di_context_applies_to_context() {
498 let scope = Arc::new(SingletonScope::new());
500 let ctx = Arc::new(InjectionContext::builder(Arc::clone(&scope)).build());
501
502 let _router = ServerRouter::new()
504 .with_di_context(Arc::clone(&ctx))
505 .with_middleware(make_mw("after-context"));
506
507 let leaked = crate::routers::take_di_registrations();
508
509 let resolved = scope
511 .get::<DummyState>()
512 .expect("with_middleware after with_di_context must apply directly to context scope");
513 assert_eq!(resolved.0, "after-context");
514 assert!(leaked.is_none());
515 }
516
517 #[rstest]
518 #[serial_test::serial(global_di)]
519 fn with_middleware_without_context_flushes_to_global_on_register_all_routes() {
520 let _ = crate::routers::take_di_registrations(); let mut router = ServerRouter::new().with_middleware(make_mw("no-context"));
526 let _errors = router.register_all_routes();
527
528 let taken = crate::routers::take_di_registrations()
530 .expect("register_all_routes must flush pending middleware DI when no context is set");
531 let scope = SingletonScope::new();
532 taken.apply_to(&scope);
533 let resolved = scope.get::<DummyState>().expect(
534 "flushed registration must resolve from the global deferred list after apply_to",
535 );
536 assert_eq!(resolved.0, "no-context");
537 }
538
539 #[rstest]
540 #[serial_test::serial(global_di)]
541 fn group_drains_grouped_router_pending_into_parent_context() {
542 let scope = Arc::new(SingletonScope::new());
546 let ctx = Arc::new(InjectionContext::builder(Arc::clone(&scope)).build());
547 let users = ServerRouter::new()
548 .with_prefix("/users")
549 .with_middleware(make_mw("group-users"));
550 let posts_grandchild =
551 ServerRouter::new().with_middleware(make_mw("group-posts-grandchild"));
552 let posts = ServerRouter::new()
553 .with_prefix("/posts")
554 .mount("/comments/", posts_grandchild);
555
556 let _parent = ServerRouter::new()
558 .with_di_context(Arc::clone(&ctx))
559 .group(vec![users, posts]);
560
561 let leaked = crate::routers::take_di_registrations();
562
563 let resolved = scope.get::<DummyState>().expect(
567 "group must recursively drain grouped routers' pending middleware DI into the parent context",
568 );
569 assert!(matches!(
570 resolved.0,
571 "group-users" | "group-posts-grandchild"
572 ));
573 assert!(leaked.is_none());
574 }
575
576 #[rstest]
577 #[serial_test::serial(global_di)]
578 fn nested_grandchild_pending_drains_into_parent_context_on_mount() {
579 let scope = Arc::new(SingletonScope::new());
584 let ctx = Arc::new(InjectionContext::builder(Arc::clone(&scope)).build());
585 let grandchild = ServerRouter::new().with_middleware(make_mw("nested-grandchild"));
586 let child = ServerRouter::new().mount("/users/", grandchild);
587
588 let _parent = ServerRouter::new()
590 .with_di_context(Arc::clone(&ctx))
591 .mount("/api/", child);
592
593 let leaked = crate::routers::take_di_registrations();
594
595 let resolved = scope.get::<DummyState>().expect(
597 "mount must recursively drain grandchildren's pending middleware DI into the parent's context",
598 );
599 assert_eq!(resolved.0, "nested-grandchild");
600 assert!(leaked.is_none());
601 }
602
603 #[rstest]
604 #[serial_test::serial(global_di)]
605 fn child_pending_drains_when_context_attached_after_mount() {
606 let scope = Arc::new(SingletonScope::new());
610 let ctx = Arc::new(InjectionContext::builder(Arc::clone(&scope)).build());
611 let child = ServerRouter::new().with_middleware(make_mw("late-context-child"));
612
613 let _parent = ServerRouter::new()
615 .mount("/api/", child)
616 .with_di_context(Arc::clone(&ctx));
617
618 let leaked = crate::routers::take_di_registrations();
619
620 let resolved = scope.get::<DummyState>().expect(
622 "attaching a context after mounting a child with pending middleware DI must propagate into the child",
623 );
624 assert_eq!(resolved.0, "late-context-child");
625 assert!(leaked.is_none());
626 }
627
628 #[rstest]
629 #[serial_test::serial(global_di)]
630 fn child_pending_drains_into_parent_context_on_mount() {
631 let scope = Arc::new(SingletonScope::new());
634 let ctx = Arc::new(InjectionContext::builder(Arc::clone(&scope)).build());
635 let child = ServerRouter::new().with_middleware(make_mw("mounted-child"));
636
637 let _parent = ServerRouter::new()
639 .with_di_context(Arc::clone(&ctx))
640 .mount("/api/", child);
641
642 let leaked = crate::routers::take_di_registrations();
643
644 let resolved = scope.get::<DummyState>().expect(
646 "mounting a child with pending middleware DI into a context-bearing parent must drain into the parent's scope",
647 );
648 assert_eq!(resolved.0, "mounted-child");
649 assert!(leaked.is_none());
650 }
651}