#[cfg(native)]
use crate::routers::server_router::ServerRouter;
#[cfg(feature = "client-router")]
use crate::routers::client_router::ClientRouter;
#[cfg(native)]
use hyper::Method;
#[cfg(native)]
use reinhardt_core::exception::Result;
#[cfg(native)]
use reinhardt_di::InjectionContext;
#[cfg(native)]
use reinhardt_http::{Request, Response};
#[cfg(native)]
use reinhardt_middleware::Middleware;
#[cfg(native)]
use std::future::Future;
#[cfg(native)]
use std::sync::Arc;
#[cfg(all(feature = "client-router", native))]
pub struct UnifiedRouter {
server: ServerRouter,
client: ClientRouter,
pub websocket: reinhardt_core::ws::WebSocketRouter,
di_registrations: reinhardt_di::DiRegistrationList,
#[cfg(feature = "streaming")]
streaming_handlers: Vec<reinhardt_streaming::StreamingHandlerRegistration>,
}
#[cfg(all(feature = "client-router", native))]
impl UnifiedRouter {
pub fn new() -> Self {
Self {
server: ServerRouter::new(),
client: ClientRouter::new(),
websocket: reinhardt_core::ws::WebSocketRouter::new(),
di_registrations: reinhardt_di::DiRegistrationList::new(),
#[cfg(feature = "streaming")]
streaming_handlers: Vec::new(),
}
}
pub fn server<F>(mut self, f: F) -> Self
where
F: FnOnce(ServerRouter) -> ServerRouter,
{
self.server = f(self.server);
self
}
pub fn client<F>(mut self, f: F) -> Self
where
F: FnOnce(ClientRouter) -> ClientRouter,
{
self.client = f(self.client);
self
}
pub fn server_ref(&self) -> &ServerRouter {
&self.server
}
pub fn server_mut(&mut self) -> &mut ServerRouter {
&mut self.server
}
pub fn client_ref(&self) -> &ClientRouter {
&self.client
}
pub fn client_mut(&mut self) -> &mut ClientRouter {
&mut self.client
}
pub fn websocket<F>(mut self, f: F) -> Self
where
F: FnOnce(reinhardt_core::ws::WebSocketRouter) -> reinhardt_core::ws::WebSocketRouter,
{
self.websocket = f(self.websocket);
self
}
pub fn websocket_ref(&self) -> &reinhardt_core::ws::WebSocketRouter {
&self.websocket
}
fn flush_di_registrations(&mut self) {
if self.di_registrations.is_empty() {
return;
}
let registrations = std::mem::take(&mut self.di_registrations);
match self
.server
.di_context()
.map(|ctx| Arc::clone(ctx.singleton_scope()))
{
Some(scope) => registrations.apply_to(&scope),
None => crate::routers::register_di_registrations(registrations),
}
}
pub fn into_server(mut self) -> ServerRouter {
self.flush_di_registrations();
let errors = self.server.register_all_routes();
for error in &errors {
tracing::warn!("{}", error);
}
self.server
}
pub fn into_client(mut self) -> ClientRouter {
self.flush_di_registrations();
self.client
}
pub fn into_parts(mut self) -> (ServerRouter, ClientRouter) {
self.flush_di_registrations();
let errors = self.server.register_all_routes();
for error in &errors {
tracing::warn!("{}", error);
}
(self.server, self.client)
}
pub fn register_globally(self) -> ClientRouter {
let (server, client) = self.into_parts();
crate::routers::register_router(server);
client
}
pub fn with_di_registrations(mut self, list: reinhardt_di::DiRegistrationList) -> Self {
self.di_registrations.merge(list);
self
}
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.server = self.server.with_prefix(prefix);
self
}
pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
let ns: String = namespace.into();
self.client = self.client.with_namespace(&ns);
self.server = self.server.with_namespace(ns);
self
}
pub fn with_di_context(mut self, ctx: Arc<InjectionContext>) -> Self {
self.server = self.server.with_di_context(ctx);
self
}
pub fn with_middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
for (type_id, value) in middleware.di_registrations() {
self.di_registrations.register_arc_any(type_id, value);
}
self.server = self.server.with_middleware(middleware);
self
}
pub fn exclude(mut self, pattern: &str) -> Self {
self.server = self.server.exclude(pattern);
self
}
pub fn mount(mut self, prefix: &str, child: ServerRouter) -> Self {
self.server = self.server.mount(prefix, child);
self
}
pub fn mount_unified(mut self, prefix: &str, child: UnifiedRouter) -> Self {
self.client = self.client.merge(child.client);
self.mount(prefix, child.server)
}
#[cfg(feature = "streaming")]
pub fn mount_streaming(mut self, router: reinhardt_streaming::StreamingRouter) -> Self {
self.streaming_handlers.extend(router.into_handlers());
self
}
pub fn endpoint<F, E>(mut self, f: F) -> Self
where
F: FnOnce() -> E,
E: reinhardt_core::endpoint::EndpointInfo + reinhardt_http::Handler + 'static,
{
self.server = self.server.endpoint(f);
self
}
pub fn function<F, Fut>(mut self, path: &str, method: Method, func: F) -> Self
where
F: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Response>> + Send + 'static,
{
self.server = self.server.function(path, method, func);
self
}
#[deprecated(
since = "0.2.0",
note = "Use `#[get(\"/path\", name = \"name\")]` + `.endpoint()` instead"
)]
pub fn function_named<F, Fut>(mut self, path: &str, method: Method, name: &str, func: F) -> Self
where
F: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Response>> + Send + 'static,
{
#[allow(deprecated)]
{
self.server = self.server.function_named(path, method, name, func);
}
self
}
}
#[cfg(all(feature = "client-router", native))]
impl std::fmt::Debug for UnifiedRouter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UnifiedRouter")
.field("server", &self.server)
.field("client", &self.client)
.field("di_registrations", &self.di_registrations)
.finish()
}
}
#[cfg(all(feature = "client-router", native))]
impl Default for UnifiedRouter {
fn default() -> Self {
Self::new()
}
}
#[cfg(all(native, feature = "client-router"))]
#[doc(hidden)]
const _: fn() = || {
fn delegate(s: ServerRouter) -> ServerRouter {
s
}
let _ = UnifiedRouter::new().server(delegate).client(|c| c);
};
#[cfg(not(feature = "client-router"))]
pub struct UnifiedRouter {
server: ServerRouter,
di_registrations: reinhardt_di::DiRegistrationList,
#[cfg(feature = "streaming")]
streaming_handlers: Vec<reinhardt_streaming::StreamingHandlerRegistration>,
}
#[cfg(not(feature = "client-router"))]
impl UnifiedRouter {
pub fn new() -> Self {
Self {
server: ServerRouter::new(),
di_registrations: reinhardt_di::DiRegistrationList::new(),
#[cfg(feature = "streaming")]
streaming_handlers: Vec::new(),
}
}
pub fn server<F>(mut self, f: F) -> Self
where
F: FnOnce(ServerRouter) -> ServerRouter,
{
self.server = f(self.server);
self
}
pub fn server_ref(&self) -> &ServerRouter {
&self.server
}
pub fn server_mut(&mut self) -> &mut ServerRouter {
&mut self.server
}
fn flush_di_registrations(&mut self) {
if self.di_registrations.is_empty() {
return;
}
let registrations = std::mem::take(&mut self.di_registrations);
match self
.server
.di_context()
.map(|ctx| Arc::clone(ctx.singleton_scope()))
{
Some(scope) => registrations.apply_to(&scope),
None => crate::routers::register_di_registrations(registrations),
}
}
pub fn into_server(mut self) -> ServerRouter {
self.flush_di_registrations();
let errors = self.server.register_all_routes();
for error in &errors {
tracing::warn!("{}", error);
}
self.server
}
pub fn register_globally(mut self) {
self.flush_di_registrations();
crate::routers::register_router(self.server);
}
pub fn with_di_registrations(mut self, list: reinhardt_di::DiRegistrationList) -> Self {
self.di_registrations.merge(list);
self
}
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.server = self.server.with_prefix(prefix);
self
}
pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
self.server = self.server.with_namespace(namespace);
self
}
pub fn with_di_context(mut self, ctx: Arc<InjectionContext>) -> Self {
self.server = self.server.with_di_context(ctx);
self
}
pub fn with_middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
for (type_id, value) in middleware.di_registrations() {
self.di_registrations.register_arc_any(type_id, value);
}
self.server = self.server.with_middleware(middleware);
self
}
pub fn mount(mut self, prefix: &str, child: ServerRouter) -> Self {
self.server = self.server.mount(prefix, child);
self
}
pub fn mount_unified(self, prefix: &str, child: UnifiedRouter) -> Self {
self.mount(prefix, child.server)
}
#[cfg(feature = "streaming")]
pub fn mount_streaming(mut self, router: reinhardt_streaming::StreamingRouter) -> Self {
self.streaming_handlers.extend(router.into_handlers());
self
}
pub fn endpoint<F, E>(mut self, f: F) -> Self
where
F: FnOnce() -> E,
E: reinhardt_core::endpoint::EndpointInfo + reinhardt_http::Handler + 'static,
{
self.server = self.server.endpoint(f);
self
}
pub fn function<F, Fut>(mut self, path: &str, method: Method, func: F) -> Self
where
F: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Response>> + Send + 'static,
{
self.server = self.server.function(path, method, func);
self
}
#[deprecated(
since = "0.2.0",
note = "Use `#[get(\"/path\", name = \"name\")]` + `.endpoint()` instead"
)]
pub fn function_named<F, Fut>(mut self, path: &str, method: Method, name: &str, func: F) -> Self
where
F: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Response>> + Send + 'static,
{
#[allow(deprecated)]
{
self.server = self.server.function_named(path, method, name, func);
}
self
}
}
#[cfg(not(feature = "client-router"))]
impl std::fmt::Debug for UnifiedRouter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UnifiedRouter")
.field("server", &self.server)
.field("di_registrations", &self.di_registrations)
.finish()
}
}
#[cfg(not(feature = "client-router"))]
impl Default for UnifiedRouter {
fn default() -> Self {
Self::new()
}
}
#[cfg(not(feature = "client-router"))]
#[async_trait::async_trait]
impl reinhardt_http::Handler for UnifiedRouter {
async fn handle(&self, request: Request) -> Result<Response> {
self.server.handle(request).await
}
}
#[cfg(all(wasm, feature = "client-router"))]
pub struct ServerRouter;
#[cfg(all(wasm, feature = "client-router"))]
impl ServerRouter {
pub fn new() -> Self {
Self
}
pub fn with_prefix(self, _prefix: impl Into<String>) -> Self {
self
}
pub fn with_namespace(self, _namespace: impl Into<String>) -> Self {
self
}
pub fn with_di_context<C>(self, _ctx: C) -> Self {
self
}
pub fn with_middleware<M>(self, _middleware: M) -> Self {
self
}
pub fn with_route_middleware<M>(self, _middleware: M) -> Self {
self
}
pub fn exclude(self, _pattern: &str) -> Self {
self
}
pub fn mount<R>(self, _prefix: &str, _child: R) -> Self {
self
}
pub fn group<R>(self, _routers: Vec<R>) -> Self {
self
}
pub fn function<M, F>(self, _path: &str, _method: M, _func: F) -> Self {
self
}
pub fn function_named<M, F>(self, _path: &str, _method: M, _name: &str, _func: F) -> Self {
self
}
pub fn route<M, F>(self, _path: &str, _method: M, _func: F) -> Self {
self
}
pub fn route_named<M, F>(self, _path: &str, _method: M, _name: &str, _func: F) -> Self {
self
}
pub fn handler<H>(self, _path: &str, _handler: H) -> Self {
self
}
pub fn handler_arc<H>(self, _path: &str, _handler: H) -> Self {
self
}
pub fn handler_with_method<M, H>(self, _path: &str, _method: M, _handler: H) -> Self {
self
}
pub fn handler_with_method_named<M, H>(
self,
_path: &str,
_method: M,
_name: &str,
_handler: H,
) -> Self {
self
}
pub fn view<V>(self, _path: &str, _view: V) -> Self {
self
}
pub fn view_named<V>(self, _path: &str, _name: &str, _view: V) -> Self {
self
}
pub fn viewset<V>(self, _prefix: &str, _viewset: V) -> Self {
self
}
pub fn endpoint<F>(self, _f: F) -> Self {
self
}
pub fn server_fn<S>(self, _marker: S) -> Self {
self
}
}
#[cfg(all(wasm, feature = "client-router"))]
impl Default for ServerRouter {
fn default() -> Self {
Self::new()
}
}
#[cfg(all(wasm, feature = "client-router"))]
#[doc(hidden)]
const _: fn() = || {
let _ = UnifiedRouter::new()
.server(|s| {
s.with_prefix("/api")
.with_namespace("api")
.with_di_context(())
.with_middleware(())
.with_route_middleware(())
.exclude("/internal")
.mount("/v1/", ServerRouter::new())
.group(Vec::<ServerRouter>::new())
.function("/f", (), || ())
.function_named("/f", (), "f", || ())
.route("/r", (), || ())
.route_named("/r", (), "r", || ())
.handler("/h", ())
.handler_arc("/h", ())
.handler_with_method("/h", (), ())
.handler_with_method_named("/h", (), "h", ())
.view("/v", ())
.view_named("/v", "v", ())
.viewset("/vs/", ())
.endpoint(|| ())
.server_fn(())
})
.client(|c| c);
};
#[cfg(all(wasm, feature = "client-router"))]
#[doc(hidden)]
const _: fn() = || {
fn delegate(s: ServerRouter) -> ServerRouter {
s
}
let _ = UnifiedRouter::new().server(delegate).client(|c| c);
};
#[cfg(all(wasm, feature = "client-router"))]
pub struct UnifiedRouter {
client: ClientRouter,
}
#[cfg(all(wasm, feature = "client-router"))]
impl UnifiedRouter {
pub fn new() -> Self {
Self {
client: ClientRouter::new(),
}
}
pub fn server<F>(self, _f: F) -> Self
where
F: FnOnce(ServerRouter) -> ServerRouter,
{
self
}
pub fn client<F>(mut self, f: F) -> Self
where
F: FnOnce(ClientRouter) -> ClientRouter,
{
self.client = f(self.client);
self
}
pub fn client_ref(&self) -> &ClientRouter {
&self.client
}
pub fn client_mut(&mut self) -> &mut ClientRouter {
&mut self.client
}
pub fn into_client(self) -> ClientRouter {
self.client
}
pub fn register_globally(self) -> ClientRouter {
self.client
}
pub fn mount_unified(mut self, _prefix: &str, child: UnifiedRouter) -> Self {
self.client = self.client.merge(child.client);
self
}
#[cfg(feature = "streaming")]
pub fn mount_streaming(self, _router: reinhardt_streaming::StreamingRouter) -> Self {
self
}
pub fn with_prefix(self, _prefix: impl Into<String>) -> Self {
self
}
pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
let ns: String = namespace.into();
self.client = self.client.with_namespace(&ns);
self
}
}
#[cfg(all(wasm, feature = "client-router"))]
impl std::fmt::Debug for UnifiedRouter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UnifiedRouter")
.field("client", &self.client)
.finish()
}
}
#[cfg(all(wasm, feature = "client-router"))]
impl Default for UnifiedRouter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use super::*;
#[cfg(feature = "client-router")]
use reinhardt_core::page::Page;
#[test]
fn test_unified_router_new() {
let router = UnifiedRouter::new();
assert_eq!(router.server_ref().prefix(), "");
}
#[test]
fn test_unified_router_server_closure() {
let router = UnifiedRouter::new().server(|s| s.with_prefix("/api").with_namespace("v1"));
assert_eq!(router.server_ref().prefix(), "/api");
assert_eq!(router.server_ref().namespace(), Some("v1"));
}
#[test]
fn test_unified_router_convenience_methods() {
let router = UnifiedRouter::new()
.with_prefix("/api")
.with_namespace("v1");
assert_eq!(router.server_ref().prefix(), "/api");
assert_eq!(router.server_ref().namespace(), Some("v1"));
}
#[cfg(feature = "client-router")]
#[test]
fn test_unified_router_client_closure() {
let router = UnifiedRouter::new().client(|c| c.route("home", "/", || Page::Empty));
assert_eq!(router.client_ref().route_count(), 1);
}
#[cfg(all(feature = "client-router", native))]
#[test]
fn unified_with_namespace_propagates_to_client() {
let router = UnifiedRouter::new()
.client(|c| c.route("login", "/login/", || Page::Empty))
.with_namespace("app");
assert!(
router.client_ref().has_route("app:login"),
"client-side named route should be namespaced by UnifiedRouter::with_namespace"
);
assert!(
!router.client_ref().has_route("login"),
"unprefixed name should no longer resolve after with_namespace"
);
}
#[cfg(all(feature = "client-router", native))]
#[test]
fn mount_unified_merges_client_routes_on_native() {
let child =
UnifiedRouter::new().client(|c| c.route("login_page", "/login/", || Page::Empty));
let parent = UnifiedRouter::new().client(|c| c.route("home", "/", || Page::Empty));
let merged = parent.mount_unified("/", child);
assert!(merged.client_ref().has_route("home"));
assert!(
merged.client_ref().has_route("login_page"),
"native mount_unified must merge child client routes (#4076)"
);
assert_eq!(merged.client_ref().route_count(), 2);
assert_eq!(
merged.client_ref().reverse("login_page", &[]).ok(),
Some("/login/".to_string()),
"merged client routes must be resolvable on native"
);
}
#[cfg(all(feature = "client-router", native))]
#[test]
fn mount_unified_merges_namespaced_client_routes_on_native() {
let child = UnifiedRouter::new()
.client(|c| c.route("login_page", "/login/", || Page::Empty))
.with_namespace("auth");
let parent = UnifiedRouter::new();
let merged = parent.mount_unified("/", child);
assert!(
merged.client_ref().has_route("auth:login_page"),
"namespaced child client routes must survive native mount_unified"
);
assert_eq!(
merged.client_ref().reverse("auth:login_page", &[]).ok(),
Some("/login/".to_string())
);
}
#[cfg(all(wasm, feature = "client-router"))]
#[test]
fn unified_wasm_with_namespace_propagates_to_client() {
let router = UnifiedRouter::new()
.client(|c| c.route("login", "/login/", || Page::Empty))
.with_namespace("app");
assert!(
router.client_ref().has_route("app:login"),
"WASM UnifiedRouter::with_namespace must propagate to ClientRouter"
);
assert!(
!router.client_ref().has_route("login"),
"unprefixed name should no longer resolve after with_namespace on WASM"
);
}
#[cfg(feature = "client-router")]
#[test]
fn test_unified_router_into_parts() {
let router = UnifiedRouter::new()
.server(|s| s.with_prefix("/api"))
.client(|c| c.route("home", "/", || Page::Empty));
let (server, client) = router.into_parts();
assert_eq!(server.prefix(), "/api");
assert_eq!(client.route_count(), 1);
}
#[cfg(feature = "client-router")]
#[test]
fn test_unified_router_into_server() {
let router = UnifiedRouter::new().server(|s| s.with_prefix("/api"));
let server = router.into_server();
assert_eq!(server.prefix(), "/api");
}
#[cfg(feature = "client-router")]
#[test]
fn test_unified_router_into_client() {
let router = UnifiedRouter::new().client(|c| c.route("home", "/", || Page::Empty));
let client = router.into_client();
assert_eq!(client.route_count(), 1);
}
mod flush_di_registrations {
use super::*;
use reinhardt_di::{DiRegistrationList, InjectionContext, SingletonScope};
use rstest::rstest;
use std::sync::Arc;
#[rstest]
fn applies_registrations_to_di_context_singleton_scope() {
let singleton_scope = Arc::new(SingletonScope::new());
let di_ctx = Arc::new(InjectionContext::builder(Arc::clone(&singleton_scope)).build());
let mut registrations = DiRegistrationList::new();
registrations.register(42i32);
let _server = UnifiedRouter::new()
.with_di_registrations(registrations)
.with_di_context(di_ctx)
.into_server();
let value = singleton_scope
.get::<i32>()
.expect("i32 should be registered");
assert_eq!(*value, 42);
}
#[rstest]
fn applies_registrations_regardless_of_builder_order() {
let singleton_scope = Arc::new(SingletonScope::new());
let di_ctx = Arc::new(InjectionContext::builder(Arc::clone(&singleton_scope)).build());
let mut registrations = DiRegistrationList::new();
registrations.register(99u64);
let _server = UnifiedRouter::new()
.with_di_context(di_ctx)
.with_di_registrations(registrations)
.into_server();
let value = singleton_scope
.get::<u64>()
.expect("u64 should be registered");
assert_eq!(*value, 99);
}
#[rstest]
#[serial_test::serial(global_di)]
fn stashes_globally_when_no_di_context() {
let mut registrations = DiRegistrationList::new();
registrations.register(7u8);
let _server = UnifiedRouter::new()
.with_di_registrations(registrations)
.into_server();
let taken = crate::routers::take_di_registrations();
assert!(taken.is_some(), "registrations should be stashed globally");
}
}
mod debug_impl {
use super::*;
use rstest::rstest;
use std::sync::Arc;
#[rstest]
fn unified_router_implements_debug() {
let router = UnifiedRouter::new().with_prefix("/api");
let debug_output = format!("{:?}", router);
assert!(debug_output.contains("UnifiedRouter"));
assert!(debug_output.contains("ServerRouter"));
}
#[rstest]
fn arc_try_unwrap_with_expect() {
let router = Arc::new(UnifiedRouter::new());
let unwrapped = Arc::try_unwrap(router).expect("should have single ref");
assert_eq!(unwrapped.server_ref().prefix(), "");
}
}
mod route_registration {
use super::*;
use hyper::Method;
use reinhardt_http::{Request, Response, Result};
use rstest::rstest;
async fn dummy_handler(_req: Request) -> Result<Response> {
Ok(Response::ok())
}
#[rstest]
fn into_server_registers_routes_for_reverse() {
let router = UnifiedRouter::new().server(|s| {
s.with_namespace("api").function_named(
"/health",
Method::GET,
"health",
dummy_handler,
)
});
let server = router.into_server();
let url = server.reverse("api:health", &[]);
assert_eq!(url, Some("/health".to_string()));
}
#[cfg(feature = "client-router")]
#[rstest]
fn into_parts_registers_routes_for_reverse() {
let router = UnifiedRouter::new().server(|s| {
s.with_namespace("api").function_named(
"/health",
Method::GET,
"health",
dummy_handler,
)
});
let (server, _client) = router.into_parts();
let url = server.reverse("api:health", &[]);
assert_eq!(url, Some("/health".to_string()));
}
}
}