dbrest_core/routing.rs
1//! Namespace routing abstractions.
2//!
3//! Defines the [`Router`] trait for deciding whether a namespace should be
4//! served locally or forwarded to another instance. The default
5//! [`LocalRouter`] always serves locally (single-instance mode).
6
7use std::sync::Arc;
8
9// ---------------------------------------------------------------------------
10// NamespaceId
11// ---------------------------------------------------------------------------
12
13/// A namespace identifier. Cheaply cloneable.
14#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
15pub struct NamespaceId(Arc<str>);
16
17impl NamespaceId {
18 pub fn new(s: impl Into<Arc<str>>) -> Self {
19 Self(s.into())
20 }
21
22 pub fn as_str(&self) -> &str {
23 &self.0
24 }
25}
26
27impl std::fmt::Display for NamespaceId {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 self.0.fmt(f)
30 }
31}
32
33// ---------------------------------------------------------------------------
34// RoutingError
35// ---------------------------------------------------------------------------
36
37/// Errors that can occur during namespace routing.
38#[derive(Debug, thiserror::Error)]
39pub enum RoutingError {
40 #[error("routing failed: {0}")]
41 RoutingFailed(String),
42}
43
44// ---------------------------------------------------------------------------
45// Route & Router trait
46// ---------------------------------------------------------------------------
47
48/// The outcome of a routing decision.
49#[derive(Debug, Clone)]
50pub enum Route {
51 /// Serve from this instance.
52 Local,
53 // Remote(InstanceAddr) — added later for multi-instance
54}
55
56/// Decides whether a namespace should be served locally or remotely.
57///
58/// The default [`LocalRouter`] always returns [`Route::Local`].
59/// Custom implementations can check a shared registry, raft state,
60/// or any other logic to decide routing.
61pub trait Router: Send + Sync {
62 fn route(&self, ns: &NamespaceId) -> Result<Route, RoutingError>;
63}
64
65// ---------------------------------------------------------------------------
66// LocalRouter (default)
67// ---------------------------------------------------------------------------
68
69/// Default router — always serves locally.
70///
71/// Used for single-instance deployments where no routing is needed.
72pub struct LocalRouter;
73
74impl Router for LocalRouter {
75 fn route(&self, _ns: &NamespaceId) -> Result<Route, RoutingError> {
76 Ok(Route::Local)
77 }
78}
79
80// ---------------------------------------------------------------------------
81// Tests
82// ---------------------------------------------------------------------------
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use std::collections::HashSet;
88
89 #[test]
90 fn test_namespace_id_new_and_display() {
91 let ns = NamespaceId::new("tenant_a");
92 assert_eq!(ns.as_str(), "tenant_a");
93 assert_eq!(ns.to_string(), "tenant_a");
94 }
95
96 #[test]
97 fn test_namespace_id_clone_is_cheap() {
98 let ns = NamespaceId::new("tenant_b");
99 let ns2 = ns.clone();
100 assert_eq!(ns, ns2);
101 }
102
103 #[test]
104 fn test_namespace_id_hash_eq() {
105 let mut set = HashSet::new();
106 set.insert(NamespaceId::new("a"));
107 set.insert(NamespaceId::new("a"));
108 assert_eq!(set.len(), 1);
109 }
110
111 #[test]
112 fn test_local_router_returns_local() {
113 let router = LocalRouter;
114 let ns = NamespaceId::new("any_namespace");
115 let result = router.route(&ns).unwrap();
116 assert!(matches!(result, Route::Local));
117 }
118
119 #[test]
120 fn test_local_router_returns_local_for_different_namespaces() {
121 let router = LocalRouter;
122 for name in ["ns1", "ns2", "tenant_abc", ""] {
123 let ns = NamespaceId::new(name);
124 let result = router.route(&ns).unwrap();
125 assert!(matches!(result, Route::Local));
126 }
127 }
128}