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;
13use std::collections::HashMap;
14use std::sync::{Arc, RwLock};
15
16impl ServerRouter {
17	/// Validate that a prefix for `mount`/`include` follows Django URL conventions.
18	///
19	/// # Panics
20	///
21	/// Panics if the prefix doesn't end with "/".
22	/// This matches Django's behavior where URL patterns must end with a trailing slash.
23	/// Use "/" for root mounting instead of an empty string "".
24	///
25	/// # Examples
26	///
27	/// ```should_panic
28	/// use reinhardt_urls::routers::ServerRouter;
29	///
30	/// // This will panic because "api" doesn't end with "/"
31	/// let router = ServerRouter::new()
32	///     .mount("api", ServerRouter::new());
33	/// ```
34	///
35	/// ```should_panic
36	/// use reinhardt_urls::routers::ServerRouter;
37	///
38	/// // This will panic because "" is not allowed, use "/" instead
39	/// let router = ServerRouter::new()
40	///     .mount("", ServerRouter::new());
41	/// ```
42	fn validate_prefix(prefix: &str) {
43		// Prefix must not contain path parameter placeholders.
44		// Mount prefixes are matched as literal strings, so a placeholder like
45		// `{org}` would never match an actual path segment and all child routes
46		// would silently 404. Fail early at construction time instead.
47		if prefix.contains('{') || prefix.contains('}') {
48			panic!(
49				"`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."
50			);
51		}
52
53		// Prefix must end with "/"
54		if !prefix.ends_with('/') {
55			if prefix.is_empty() {
56				panic!(
57					"URL route prefix cannot be an empty string. \
58					 Use '/' instead of ''. \
59					 This follows Django URL configuration conventions."
60				);
61			} else {
62				panic!(
63					"URL route '{}' must end with a trailing slash '/'. \
64					 Use '{}/' instead of '{}'. \
65					 This follows Django URL configuration conventions.",
66					prefix, prefix, prefix,
67				);
68			}
69		}
70	}
71
72	/// Create a new ServerRouter
73	///
74	/// # Examples
75	///
76	/// ```
77	/// use reinhardt_urls::routers::ServerRouter;
78	///
79	/// let router = ServerRouter::new();
80	/// ```
81	pub fn new() -> Self {
82		Self {
83			prefix: String::new(),
84			namespace: None,
85			routes: Vec::new(),
86			viewsets: HashMap::new(),
87			functions: Vec::new(),
88			views: Vec::new(),
89			children: Vec::new(),
90			di_context: None,
91			pending_middleware_di: reinhardt_di::DiRegistrationList::new(),
92			middleware: Vec::new(),
93			middleware_names: Vec::new(),
94			middleware_exclusions: Vec::new(),
95			reverser: UrlReverser::new(),
96			get_router: RwLock::new(MatchitRouter::new()),
97			post_router: RwLock::new(MatchitRouter::new()),
98			put_router: RwLock::new(MatchitRouter::new()),
99			delete_router: RwLock::new(MatchitRouter::new()),
100			patch_router: RwLock::new(MatchitRouter::new()),
101			head_router: RwLock::new(MatchitRouter::new()),
102			options_router: RwLock::new(MatchitRouter::new()),
103			routes_compiled: RwLock::new(false),
104		}
105	}
106
107	/// Set the prefix for this router
108	///
109	/// # Examples
110	///
111	/// ```
112	/// use reinhardt_urls::routers::ServerRouter;
113	///
114	/// let router = ServerRouter::new()
115	///     .with_prefix("/api/v1");
116	/// ```
117	pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
118		self.prefix = prefix.into();
119		self
120	}
121
122	/// Set the namespace for this router
123	///
124	/// # Examples
125	///
126	/// ```
127	/// use reinhardt_urls::routers::ServerRouter;
128	///
129	/// let router = ServerRouter::new()
130	///     .with_namespace("v1");
131	/// ```
132	pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
133		self.namespace = Some(namespace.into());
134		self
135	}
136
137	/// Set the DI context for this router
138	///
139	/// # Examples
140	///
141	/// ```rust,no_run
142	/// use reinhardt_urls::routers::ServerRouter;
143	/// use reinhardt_di::{InjectionContext, SingletonScope};
144	/// use std::sync::Arc;
145	///
146	/// let singleton_scope = Arc::new(SingletonScope::new());
147	/// let di_ctx = Arc::new(InjectionContext::builder(singleton_scope).build());
148	/// let router = ServerRouter::new()
149	///     .with_di_context(di_ctx);
150	/// ```
151	pub fn with_di_context(mut self, ctx: Arc<InjectionContext>) -> Self {
152		// Drain any middleware-contributed DI registrations that were harvested
153		// before `with_di_context` was called, walking children that have not
154		// already been bound to their own context (e.g., a child mounted
155		// before `with_di_context` was attached). Without this, registrations
156		// staged by an earlier `with_middleware` on this router or any
157		// not-yet-bound child would never reach this context's
158		// `SingletonScope` (startup's `take_di_registrations()` path is
159		// skipped whenever a user-supplied context exists). See #4426.
160		Self::adopt_di_context_recursive(&mut self, &ctx);
161		self.di_context = Some(ctx);
162		self
163	}
164
165	/// Propagate a newly attached `InjectionContext` into every descendant
166	/// that has no context of its own, draining each router's pending
167	/// middleware DI registrations into the context's `SingletonScope` along
168	/// the way. Descendants that already own a different context are left
169	/// untouched. See #4426.
170	/// If `parent` owns an `InjectionContext` and `child` does not, recursively
171	/// adopt the parent's context into the child's subtree (draining any
172	/// pending middleware DI registrations along the way) and attach the
173	/// context to the child itself. Used by `mount`, `mount_mut`, and `group`
174	/// to keep the DI propagation behavior in one place. See #4426.
175	fn inherit_context_from_parent_if_any(parent: &ServerRouter, child: &mut ServerRouter) {
176		if child.di_context.is_none()
177			&& let Some(parent_ctx) = parent.di_context.as_ref()
178		{
179			Self::adopt_di_context_recursive(child, parent_ctx);
180			child.di_context = Some(Arc::clone(parent_ctx));
181		}
182	}
183
184	fn adopt_di_context_recursive(router: &mut ServerRouter, ctx: &Arc<InjectionContext>) {
185		if !router.pending_middleware_di.is_empty() {
186			let pending = std::mem::take(&mut router.pending_middleware_di);
187			pending.apply_to(ctx.singleton_scope());
188		}
189		for child in router.children.iter_mut() {
190			if child.di_context.is_none() {
191				Self::adopt_di_context_recursive(child, ctx);
192				child.di_context = Some(Arc::clone(ctx));
193			}
194		}
195	}
196
197	/// Returns a reference to the DI context, if set.
198	pub(crate) fn di_context(&self) -> Option<&Arc<InjectionContext>> {
199		self.di_context.as_ref()
200	}
201
202	/// Add middleware to this router
203	///
204	/// # Examples
205	///
206	/// ```rust,no_run
207	/// use reinhardt_urls::routers::ServerRouter;
208	/// use reinhardt_middleware::LoggingMiddleware;
209	///
210	/// let router = ServerRouter::new()
211	///     .with_middleware(LoggingMiddleware::new());
212	/// ```
213	pub fn with_middleware<M: Middleware + 'static>(mut self, mw: M) -> Self {
214		let full_type_name = std::any::type_name::<M>().to_string();
215		let short_name = full_type_name
216			.rsplit("::")
217			.next()
218			.unwrap_or(&full_type_name)
219			.to_string();
220		// Harvest middleware-contributed DI singleton registrations. Decision
221		// is order-independent across `with_middleware` / `with_di_context`:
222		//   - If a DI context is already attached, apply directly to its
223		//     `SingletonScope` so handlers resolved through it see the value.
224		//   - Otherwise, stage into `pending_middleware_di`. A later
225		//     `with_di_context` will drain it into the new context; if no
226		//     context is ever attached, `register_all_routes` flushes it to
227		//     the global deferred list (the path startup consumes when no
228		//     user-supplied context exists). This eliminates both the silent
229		//     drop on `with_middleware` → `with_di_context` ordering and the
230		//     global-list leak that would otherwise occur. See #4426.
231		let di_entries = mw.di_registrations();
232		if !di_entries.is_empty() {
233			if let Some(ctx) = self.di_context.as_ref() {
234				let scope = ctx.singleton_scope();
235				for (type_id, value) in di_entries {
236					scope.set_arc_any(type_id, value);
237				}
238			} else {
239				for (type_id, value) in di_entries {
240					self.pending_middleware_di.register_arc_any(type_id, value);
241				}
242			}
243		}
244		self.middleware_names.push(MiddlewareInfo {
245			name: short_name,
246			type_name: full_type_name,
247		});
248		self.middleware.push(Arc::new(mw));
249		self.middleware_exclusions.push(Vec::new());
250		self
251	}
252
253	/// Exclude a URL path from the most recently added middleware.
254	///
255	/// Paths ending with `/` are treated as prefix matches: any request
256	/// path starting with the given prefix will skip the middleware.
257	/// Paths without trailing `/` require an exact match.
258	///
259	/// This method operates on the **last middleware** added via
260	/// [`with_middleware()`](Self::with_middleware). Multiple `.exclude()`
261	/// calls accumulate exclusions on the same middleware.
262	///
263	/// # Panics
264	///
265	/// Panics if no middleware has been added yet.
266	///
267	/// # Examples
268	///
269	/// ```rust,no_run
270	/// use reinhardt_urls::routers::ServerRouter;
271	/// use reinhardt_middleware::LoggingMiddleware;
272	///
273	/// let router = ServerRouter::new()
274	///     .with_middleware(LoggingMiddleware::new())
275	///         .exclude("/api/auth/")    // prefix: skips /api/auth/*
276	///         .exclude("/health");      // exact: skips only /health
277	/// ```
278	pub fn exclude(mut self, pattern: &str) -> Self {
279		assert!(
280			!self.middleware_exclusions.is_empty(),
281			"exclude() called with no middleware. Call with_middleware() first."
282		);
283		self.middleware_exclusions
284			.last_mut()
285			.unwrap()
286			.push(pattern.to_string());
287		self
288	}
289
290	/// Build middleware list, wrapping any with exclusions in `ExcludeMiddleware`.
291	pub(crate) fn build_middleware_with_exclusions(&self) -> Vec<Arc<dyn Middleware>> {
292		let mut result: Vec<Arc<dyn Middleware>> = Vec::with_capacity(self.middleware.len());
293
294		for (mw, exclusions) in self
295			.middleware
296			.iter()
297			.zip(self.middleware_exclusions.iter())
298		{
299			if exclusions.is_empty() {
300				result.push(mw.clone());
301			} else {
302				let mut exclude_mw = ExcludeMiddleware::new(mw.clone());
303				for pattern in exclusions {
304					exclude_mw.add_exclusion_mut(pattern);
305				}
306				result.push(Arc::new(exclude_mw) as Arc<dyn Middleware>);
307			}
308		}
309
310		result
311	}
312
313	/// Mount a child router at the given prefix
314	///
315	/// # Panics
316	///
317	/// Panics if the prefix is non-empty, not "/" and doesn't end with "/".
318	/// This follows Django's URL configuration conventions.
319	///
320	/// Also panics if the prefix contains a path parameter placeholder
321	/// (e.g. `/orgs/{org}/`). Mount prefixes are matched as literal strings,
322	/// so placeholders would silently cause all child routes to return 404.
323	/// Use `route()` with the full path on the child router instead, or
324	/// mount at a literal prefix.
325	///
326	/// # Examples
327	///
328	/// ```rust
329	/// use reinhardt_urls::routers::ServerRouter;
330	///
331	/// let users_router = ServerRouter::new()
332	///     .with_namespace("users");
333	///
334	/// let router = ServerRouter::new()
335	///     .with_prefix("/api")
336	///     .mount("/users/", users_router);  // Note: trailing slash required
337	///
338	/// // Verify the router was created successfully
339	/// assert_eq!(router.prefix(), "/api");
340	/// ```
341	///
342	/// Using "/" for root mounting is also valid:
343	///
344	/// ```rust
345	/// use reinhardt_urls::routers::ServerRouter;
346	///
347	/// let app_router = ServerRouter::new();
348	/// let router = ServerRouter::new().mount("/", app_router);
349	/// ```
350	pub fn mount(mut self, prefix: &str, mut child: ServerRouter) -> Self {
351		// Validate prefix follows Django URL conventions
352		Self::validate_prefix(prefix);
353
354		// Set prefix if not already set
355		if child.prefix.is_empty() {
356			child.prefix = prefix.to_string();
357		}
358
359		// Inherit DI context if child doesn't have one. When inheriting,
360		// recursively drain pending middleware DI from the entire subtree
361		// (the child itself AND any grandchildren that also lack a context),
362		// mirroring `with_di_context`. A non-recursive drain would leave
363		// nested grandchildren's staged registrations stranded; later
364		// `register_all_routes` would push them to the global list, which
365		// startup skips when the top router owns a context. See #4426.
366		Self::inherit_context_from_parent_if_any(&self, &mut child);
367
368		self.children.push(child);
369		self
370	}
371
372	/// Mount a child router (mutable version)
373	///
374	/// # Examples
375	///
376	/// ```rust,no_run
377	/// use reinhardt_urls::routers::ServerRouter;
378	///
379	/// let mut router = ServerRouter::new();
380	/// let users_router = ServerRouter::new();
381	///
382	/// router.mount_mut("/users/", users_router);
383	/// ```
384	pub fn mount_mut(&mut self, prefix: &str, mut child: ServerRouter) {
385		// Validate prefix follows Django URL conventions
386		Self::validate_prefix(prefix);
387
388		if child.prefix.is_empty() {
389			child.prefix = prefix.to_string();
390		}
391		// See `mount` for the rationale on recursive subtree adoption.
392		Self::inherit_context_from_parent_if_any(self, &mut child);
393		self.children.push(child);
394	}
395
396	/// Add multiple child routers at once
397	///
398	/// # Examples
399	///
400	/// ```rust
401	/// use reinhardt_urls::routers::ServerRouter;
402	///
403	/// let users = ServerRouter::new().with_prefix("/users");
404	/// let posts = ServerRouter::new().with_prefix("/posts");
405	///
406	/// let router = ServerRouter::new()
407	///     .group(vec![users, posts]);
408	///
409	/// // Verify the router was created successfully
410	/// assert_eq!(router.prefix(), "");
411	/// ```
412	pub fn group(mut self, routers: Vec<ServerRouter>) -> Self {
413		for mut router in routers {
414			// Mirror `mount`: when this router owns a context, recursively
415			// adopt the grouped subtree into it so any pending middleware DI
416			// staged before grouping is drained. Otherwise the grouped
417			// subtree's pending lists would later be pushed onto the global
418			// deferred list, which startup skips when the parent owns a
419			// context. See #4426.
420			Self::inherit_context_from_parent_if_any(&self, &mut router);
421			self.children.push(router);
422		}
423		self
424	}
425}
426
427#[cfg(test)]
428mod middleware_di_tests {
429	use super::*;
430	use async_trait::async_trait;
431	use reinhardt_core::exception::Result;
432	use reinhardt_di::{InjectionContext, SingletonScope};
433	use reinhardt_http::{Handler, Request, Response};
434	use rstest::rstest;
435	use std::any::TypeId;
436	use std::sync::Arc;
437
438	#[derive(Debug, PartialEq, Eq)]
439	struct DummyState(&'static str);
440
441	struct DummyMiddleware {
442		state: Arc<DummyState>,
443	}
444
445	#[async_trait]
446	impl Middleware for DummyMiddleware {
447		async fn process(&self, request: Request, handler: Arc<dyn Handler>) -> Result<Response> {
448			handler.handle(request).await
449		}
450
451		fn di_registrations(&self) -> Vec<reinhardt_http::MiddlewareDiRegistration> {
452			vec![(
453				TypeId::of::<DummyState>(),
454				Arc::clone(&self.state) as Arc<dyn std::any::Any + Send + Sync>,
455			)]
456		}
457	}
458
459	fn make_mw(tag: &'static str) -> DummyMiddleware {
460		DummyMiddleware {
461			state: Arc::new(DummyState(tag)),
462		}
463	}
464
465	#[rstest]
466	#[serial_test::serial(global_di)]
467	fn with_middleware_before_with_di_context_applies_to_context() {
468		// Arrange: builder calls in the order that previously dropped the
469		// registration (with_middleware first, with_di_context later).
470		let scope = Arc::new(SingletonScope::new());
471		let ctx = Arc::new(InjectionContext::builder(Arc::clone(&scope)).build());
472
473		// Act
474		let _router = ServerRouter::new()
475			.with_middleware(make_mw("before-context"))
476			.with_di_context(Arc::clone(&ctx));
477
478		// Drain the global list to assert nothing leaked there.
479		let leaked = crate::routers::take_di_registrations();
480
481		// Assert: scope resolves the middleware-owned singleton, and the
482		// global deferred list received nothing.
483		let resolved = scope
484			.get::<DummyState>()
485			.expect("with_di_context must drain pending middleware DI into the new context");
486		assert_eq!(resolved.0, "before-context");
487		assert!(
488			leaked.is_none(),
489			"pending middleware DI must not leak into the global deferred list when a context is attached later"
490		);
491	}
492
493	#[rstest]
494	#[serial_test::serial(global_di)]
495	fn with_middleware_after_with_di_context_applies_to_context() {
496		// Arrange
497		let scope = Arc::new(SingletonScope::new());
498		let ctx = Arc::new(InjectionContext::builder(Arc::clone(&scope)).build());
499
500		// Act: reverse order — context first, then middleware.
501		let _router = ServerRouter::new()
502			.with_di_context(Arc::clone(&ctx))
503			.with_middleware(make_mw("after-context"));
504
505		let leaked = crate::routers::take_di_registrations();
506
507		// Assert
508		let resolved = scope
509			.get::<DummyState>()
510			.expect("with_middleware after with_di_context must apply directly to context scope");
511		assert_eq!(resolved.0, "after-context");
512		assert!(leaked.is_none());
513	}
514
515	#[rstest]
516	#[serial_test::serial(global_di)]
517	fn with_middleware_without_context_flushes_to_global_on_register_all_routes() {
518		// Arrange: no DI context ever attached. Pending must flush to global
519		// on `register_all_routes`.
520		let _ = crate::routers::take_di_registrations(); // clear any leftover
521
522		// Act
523		let mut router = ServerRouter::new().with_middleware(make_mw("no-context"));
524		let _errors = router.register_all_routes();
525
526		// Assert: global deferred list now contains the registration.
527		let taken = crate::routers::take_di_registrations()
528			.expect("register_all_routes must flush pending middleware DI when no context is set");
529		let scope = SingletonScope::new();
530		taken.apply_to(&scope);
531		let resolved = scope.get::<DummyState>().expect(
532			"flushed registration must resolve from the global deferred list after apply_to",
533		);
534		assert_eq!(resolved.0, "no-context");
535	}
536
537	#[rstest]
538	#[serial_test::serial(global_di)]
539	fn group_drains_grouped_router_pending_into_parent_context() {
540		// Arrange: each grouped child stages its own middleware DI; one of
541		// them also has a nested grandchild with pending DI to verify the
542		// recursive walk through `group`.
543		let scope = Arc::new(SingletonScope::new());
544		let ctx = Arc::new(InjectionContext::builder(Arc::clone(&scope)).build());
545		let users = ServerRouter::new()
546			.with_prefix("/users")
547			.with_middleware(make_mw("group-users"));
548		let posts_grandchild =
549			ServerRouter::new().with_middleware(make_mw("group-posts-grandchild"));
550		let posts = ServerRouter::new()
551			.with_prefix("/posts")
552			.mount("/comments/", posts_grandchild);
553
554		// Act: group both routers under a context-owning parent.
555		let _parent = ServerRouter::new()
556			.with_di_context(Arc::clone(&ctx))
557			.group(vec![users, posts]);
558
559		let leaked = crate::routers::take_di_registrations();
560
561		// Assert: both staged values reach the parent's scope; the second
562		// `set_arc_any` overwrites the first under the same `DummyState`
563		// `TypeId`, so we only verify presence and absence of global leak.
564		let resolved = scope.get::<DummyState>().expect(
565			"group must recursively drain grouped routers' pending middleware DI into the parent context",
566		);
567		assert!(matches!(
568			resolved.0,
569			"group-users" | "group-posts-grandchild"
570		));
571		assert!(leaked.is_none());
572	}
573
574	#[rstest]
575	#[serial_test::serial(global_di)]
576	fn nested_grandchild_pending_drains_into_parent_context_on_mount() {
577		// Arrange: build a grandchild with pending middleware DI, nest it
578		// inside a child (neither has a context yet), then mount the whole
579		// subtree under a parent that already owns a context. `mount` must
580		// recursively drain the grandchild — not just the immediate child.
581		let scope = Arc::new(SingletonScope::new());
582		let ctx = Arc::new(InjectionContext::builder(Arc::clone(&scope)).build());
583		let grandchild = ServerRouter::new().with_middleware(make_mw("nested-grandchild"));
584		let child = ServerRouter::new().mount("/users/", grandchild);
585
586		// Act
587		let _parent = ServerRouter::new()
588			.with_di_context(Arc::clone(&ctx))
589			.mount("/api/", child);
590
591		let leaked = crate::routers::take_di_registrations();
592
593		// Assert
594		let resolved = scope.get::<DummyState>().expect(
595			"mount must recursively drain grandchildren's pending middleware DI into the parent's context",
596		);
597		assert_eq!(resolved.0, "nested-grandchild");
598		assert!(leaked.is_none());
599	}
600
601	#[rstest]
602	#[serial_test::serial(global_di)]
603	fn child_pending_drains_when_context_attached_after_mount() {
604		// Arrange: child is mounted BEFORE the parent has a DI context, so
605		// `mount` cannot drain. Then `with_di_context` runs on the parent and
606		// must propagate into the already-mounted child.
607		let scope = Arc::new(SingletonScope::new());
608		let ctx = Arc::new(InjectionContext::builder(Arc::clone(&scope)).build());
609		let child = ServerRouter::new().with_middleware(make_mw("late-context-child"));
610
611		// Act: mount first, then attach context.
612		let _parent = ServerRouter::new()
613			.mount("/api/", child)
614			.with_di_context(Arc::clone(&ctx));
615
616		let leaked = crate::routers::take_di_registrations();
617
618		// Assert
619		let resolved = scope.get::<DummyState>().expect(
620			"attaching a context after mounting a child with pending middleware DI must propagate into the child",
621		);
622		assert_eq!(resolved.0, "late-context-child");
623		assert!(leaked.is_none());
624	}
625
626	#[rstest]
627	#[serial_test::serial(global_di)]
628	fn child_pending_drains_into_parent_context_on_mount() {
629		// Arrange: parent has a context; child staged a middleware DI before
630		// being mounted under the parent.
631		let scope = Arc::new(SingletonScope::new());
632		let ctx = Arc::new(InjectionContext::builder(Arc::clone(&scope)).build());
633		let child = ServerRouter::new().with_middleware(make_mw("mounted-child"));
634
635		// Act
636		let _parent = ServerRouter::new()
637			.with_di_context(Arc::clone(&ctx))
638			.mount("/api/", child);
639
640		let leaked = crate::routers::take_di_registrations();
641
642		// Assert
643		let resolved = scope.get::<DummyState>().expect(
644			"mounting a child with pending middleware DI into a context-bearing parent must drain into the parent's scope",
645		);
646		assert_eq!(resolved.0, "mounted-child");
647		assert!(leaked.is_none());
648	}
649}