Skip to main content

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}