Skip to main content

reinhardt_urls/routers/server_router/
builder.rs

1//! Builder methods for [`ServerRouter`].
2//!
3//! Holds the constructor, prefix/namespace/DI configuration, middleware
4//! registration, and child router composition (`mount`, `group`).
5
6use 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	/// Validate that a prefix for `mount`/`include` follows Django URL conventions.
19	///
20	/// # Panics
21	///
22	/// Panics if the prefix doesn't end with "/".
23	/// This matches Django's behavior where URL patterns must end with a trailing slash.
24	/// Use "/" for root mounting instead of an empty string "".
25	///
26	/// # Examples
27	///
28	/// ```should_panic
29	/// use reinhardt_urls::routers::ServerRouter;
30	///
31	/// // This will panic because "api" doesn't end with "/"
32	/// let router = ServerRouter::new()
33	///     .mount("api", ServerRouter::new());
34	/// ```
35	///
36	/// ```should_panic
37	/// use reinhardt_urls::routers::ServerRouter;
38	///
39	/// // This will panic because "" is not allowed, use "/" instead
40	/// let router = ServerRouter::new()
41	///     .mount("", ServerRouter::new());
42	/// ```
43	fn validate_prefix(prefix: &str) {
44		// Prefix must not contain path parameter placeholders.
45		// Mount prefixes are matched as literal strings, so a placeholder like
46		// `{org}` would never match an actual path segment and all child routes
47		// would silently 404. Fail early at construction time instead.
48		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		// Prefix must end with "/"
55		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	/// Create a new ServerRouter
74	///
75	/// # Examples
76	///
77	/// ```
78	/// use reinhardt_urls::routers::ServerRouter;
79	///
80	/// let router = ServerRouter::new();
81	/// ```
82	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	/// Set the prefix for this router
110	///
111	/// # Examples
112	///
113	/// ```
114	/// use reinhardt_urls::routers::ServerRouter;
115	///
116	/// let router = ServerRouter::new()
117	///     .with_prefix("/api/v1");
118	/// ```
119	pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
120		self.prefix = prefix.into();
121		self
122	}
123
124	/// Set the namespace for this router
125	///
126	/// # Examples
127	///
128	/// ```
129	/// use reinhardt_urls::routers::ServerRouter;
130	///
131	/// let router = ServerRouter::new()
132	///     .with_namespace("v1");
133	/// ```
134	pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
135		self.namespace = Some(namespace.into());
136		self
137	}
138
139	/// Set the DI context for this router
140	///
141	/// # Examples
142	///
143	/// ```rust,no_run
144	/// use reinhardt_urls::routers::ServerRouter;
145	/// use reinhardt_di::{InjectionContext, SingletonScope};
146	/// use std::sync::Arc;
147	///
148	/// let singleton_scope = Arc::new(SingletonScope::new());
149	/// let di_ctx = Arc::new(InjectionContext::builder(singleton_scope).build());
150	/// let router = ServerRouter::new()
151	///     .with_di_context(di_ctx);
152	/// ```
153	pub fn with_di_context(mut self, ctx: Arc<InjectionContext>) -> Self {
154		// Drain any middleware-contributed DI registrations that were harvested
155		// before `with_di_context` was called, walking children that have not
156		// already been bound to their own context (e.g., a child mounted
157		// before `with_di_context` was attached). Without this, registrations
158		// staged by an earlier `with_middleware` on this router or any
159		// not-yet-bound child would never reach this context's
160		// `SingletonScope` (startup's `take_di_registrations()` path is
161		// skipped whenever a user-supplied context exists). See #4426.
162		Self::adopt_di_context_recursive(&mut self, &ctx);
163		self.di_context = Some(ctx);
164		self
165	}
166
167	/// Propagate a newly attached `InjectionContext` into every descendant
168	/// that has no context of its own, draining each router's pending
169	/// middleware DI registrations into the context's `SingletonScope` along
170	/// the way. Descendants that already own a different context are left
171	/// untouched. See #4426.
172	/// If `parent` owns an `InjectionContext` and `child` does not, recursively
173	/// adopt the parent's context into the child's subtree (draining any
174	/// pending middleware DI registrations along the way) and attach the
175	/// context to the child itself. Used by `mount`, `mount_mut`, and `group`
176	/// to keep the DI propagation behavior in one place. See #4426.
177	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	/// Returns a reference to the DI context, if set.
200	pub(crate) fn di_context(&self) -> Option<&Arc<InjectionContext>> {
201		self.di_context.as_ref()
202	}
203
204	/// Add middleware to this router
205	///
206	/// # Examples
207	///
208	/// ```rust,no_run
209	/// use reinhardt_urls::routers::ServerRouter;
210	/// use reinhardt_middleware::LoggingMiddleware;
211	///
212	/// let router = ServerRouter::new()
213	///     .with_middleware(LoggingMiddleware::new());
214	/// ```
215	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		// Harvest middleware-contributed DI singleton registrations. Decision
223		// is order-independent across `with_middleware` / `with_di_context`:
224		//   - If a DI context is already attached, apply directly to its
225		//     `SingletonScope` so handlers resolved through it see the value.
226		//   - Otherwise, stage into `pending_middleware_di`. A later
227		//     `with_di_context` will drain it into the new context; if no
228		//     context is ever attached, `register_all_routes` flushes it to
229		//     the global deferred list (the path startup consumes when no
230		//     user-supplied context exists). This eliminates both the silent
231		//     drop on `with_middleware` → `with_di_context` ordering and the
232		//     global-list leak that would otherwise occur. See #4426.
233		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	/// Exclude a URL path from the most recently added middleware.
256	///
257	/// Paths ending with `/` are treated as prefix matches: any request
258	/// path starting with the given prefix will skip the middleware.
259	/// Paths without trailing `/` require an exact match.
260	///
261	/// This method operates on the **last middleware** added via
262	/// [`with_middleware()`](Self::with_middleware). Multiple `.exclude()`
263	/// calls accumulate exclusions on the same middleware.
264	///
265	/// # Panics
266	///
267	/// Panics if no middleware has been added yet.
268	///
269	/// # Examples
270	///
271	/// ```rust,no_run
272	/// use reinhardt_urls::routers::ServerRouter;
273	/// use reinhardt_middleware::LoggingMiddleware;
274	///
275	/// let router = ServerRouter::new()
276	///     .with_middleware(LoggingMiddleware::new())
277	///         .exclude("/api/auth/")    // prefix: skips /api/auth/*
278	///         .exclude("/health");      // exact: skips only /health
279	/// ```
280	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	/// Build middleware list, wrapping any with exclusions in `ExcludeMiddleware`.
293	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	/// Mount a child router at the given prefix
316	///
317	/// # Panics
318	///
319	/// Panics if the prefix is non-empty, not "/" and doesn't end with "/".
320	/// This follows Django's URL configuration conventions.
321	///
322	/// Also panics if the prefix contains a path parameter placeholder
323	/// (e.g. `/orgs/{org}/`). Mount prefixes are matched as literal strings,
324	/// so placeholders would silently cause all child routes to return 404.
325	/// Use `route()` with the full path on the child router instead, or
326	/// mount at a literal prefix.
327	///
328	/// # Examples
329	///
330	/// ```rust
331	/// use reinhardt_urls::routers::ServerRouter;
332	///
333	/// let users_router = ServerRouter::new()
334	///     .with_namespace("users");
335	///
336	/// let router = ServerRouter::new()
337	///     .with_prefix("/api")
338	///     .mount("/users/", users_router);  // Note: trailing slash required
339	///
340	/// // Verify the router was created successfully
341	/// assert_eq!(router.prefix(), "/api");
342	/// ```
343	///
344	/// Using "/" for root mounting is also valid:
345	///
346	/// ```rust
347	/// use reinhardt_urls::routers::ServerRouter;
348	///
349	/// let app_router = ServerRouter::new();
350	/// let router = ServerRouter::new().mount("/", app_router);
351	/// ```
352	pub fn mount(mut self, prefix: &str, mut child: ServerRouter) -> Self {
353		// Validate prefix follows Django URL conventions
354		Self::validate_prefix(prefix);
355
356		// Set prefix if not already set
357		if child.prefix.is_empty() {
358			child.prefix = prefix.to_string();
359		}
360
361		// Inherit DI context if child doesn't have one. When inheriting,
362		// recursively drain pending middleware DI from the entire subtree
363		// (the child itself AND any grandchildren that also lack a context),
364		// mirroring `with_di_context`. A non-recursive drain would leave
365		// nested grandchildren's staged registrations stranded; later
366		// `register_all_routes` would push them to the global list, which
367		// startup skips when the top router owns a context. See #4426.
368		Self::inherit_context_from_parent_if_any(&self, &mut child);
369
370		self.children.push(child);
371		self
372	}
373
374	/// Mount a child router (mutable version)
375	///
376	/// # Examples
377	///
378	/// ```rust,no_run
379	/// use reinhardt_urls::routers::ServerRouter;
380	///
381	/// let mut router = ServerRouter::new();
382	/// let users_router = ServerRouter::new();
383	///
384	/// router.mount_mut("/users/", users_router);
385	/// ```
386	pub fn mount_mut(&mut self, prefix: &str, mut child: ServerRouter) {
387		// Validate prefix follows Django URL conventions
388		Self::validate_prefix(prefix);
389
390		if child.prefix.is_empty() {
391			child.prefix = prefix.to_string();
392		}
393		// See `mount` for the rationale on recursive subtree adoption.
394		Self::inherit_context_from_parent_if_any(self, &mut child);
395		self.children.push(child);
396	}
397
398	/// Add multiple child routers at once
399	///
400	/// # Examples
401	///
402	/// ```rust
403	/// use reinhardt_urls::routers::ServerRouter;
404	///
405	/// let users = ServerRouter::new().with_prefix("/users");
406	/// let posts = ServerRouter::new().with_prefix("/posts");
407	///
408	/// let router = ServerRouter::new()
409	///     .group(vec![users, posts]);
410	///
411	/// // Verify the router was created successfully
412	/// assert_eq!(router.prefix(), "");
413	/// ```
414	pub fn group(mut self, routers: Vec<ServerRouter>) -> Self {
415		for mut router in routers {
416			// Mirror `mount`: when this router owns a context, recursively
417			// adopt the grouped subtree into it so any pending middleware DI
418			// staged before grouping is drained. Otherwise the grouped
419			// subtree's pending lists would later be pushed onto the global
420			// deferred list, which startup skips when the parent owns a
421			// context. See #4426.
422			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		// Arrange: builder calls in the order that previously dropped the
471		// registration (with_middleware first, with_di_context later).
472		let scope = Arc::new(SingletonScope::new());
473		let ctx = Arc::new(InjectionContext::builder(Arc::clone(&scope)).build());
474
475		// Act
476		let _router = ServerRouter::new()
477			.with_middleware(make_mw("before-context"))
478			.with_di_context(Arc::clone(&ctx));
479
480		// Drain the global list to assert nothing leaked there.
481		let leaked = crate::routers::take_di_registrations();
482
483		// Assert: scope resolves the middleware-owned singleton, and the
484		// global deferred list received nothing.
485		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		// Arrange
499		let scope = Arc::new(SingletonScope::new());
500		let ctx = Arc::new(InjectionContext::builder(Arc::clone(&scope)).build());
501
502		// Act: reverse order — context first, then middleware.
503		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		// Assert
510		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		// Arrange: no DI context ever attached. Pending must flush to global
521		// on `register_all_routes`.
522		let _ = crate::routers::take_di_registrations(); // clear any leftover
523
524		// Act
525		let mut router = ServerRouter::new().with_middleware(make_mw("no-context"));
526		let _errors = router.register_all_routes();
527
528		// Assert: global deferred list now contains the registration.
529		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		// Arrange: each grouped child stages its own middleware DI; one of
543		// them also has a nested grandchild with pending DI to verify the
544		// recursive walk through `group`.
545		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		// Act: group both routers under a context-owning parent.
557		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		// Assert: both staged values reach the parent's scope; the second
564		// `set_arc_any` overwrites the first under the same `DummyState`
565		// `TypeId`, so we only verify presence and absence of global leak.
566		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		// Arrange: build a grandchild with pending middleware DI, nest it
580		// inside a child (neither has a context yet), then mount the whole
581		// subtree under a parent that already owns a context. `mount` must
582		// recursively drain the grandchild — not just the immediate child.
583		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		// Act
589		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		// Assert
596		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		// Arrange: child is mounted BEFORE the parent has a DI context, so
607		// `mount` cannot drain. Then `with_di_context` runs on the parent and
608		// must propagate into the already-mounted child.
609		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		// Act: mount first, then attach context.
614		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		// Assert
621		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		// Arrange: parent has a context; child staged a middleware DI before
632		// being mounted under the parent.
633		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		// Act
638		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		// Assert
645		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}