Skip to main content

clasp_federation/
namespace.rs

1//! Namespace management for federation
2//!
3//! Tracks which peer routers own which address patterns,
4//! enabling intelligent message forwarding and loop prevention.
5
6use std::collections::HashMap;
7
8/// Manages namespace ownership across federated peers.
9///
10/// Each peer declares the address patterns it owns. When a message arrives,
11/// the namespace manager determines which peers should receive it.
12#[derive(Debug)]
13pub struct NamespaceManager {
14    /// Map of peer router ID -> owned patterns
15    peer_namespaces: HashMap<String, Vec<String>>,
16    /// Local router's owned patterns
17    local_namespaces: Vec<String>,
18}
19
20impl NamespaceManager {
21    /// Create a new namespace manager with local patterns
22    pub fn new(local_namespaces: Vec<String>) -> Self {
23        Self {
24            peer_namespaces: HashMap::new(),
25            local_namespaces,
26        }
27    }
28
29    /// Register a peer's namespace patterns
30    pub fn register_peer(&mut self, router_id: &str, patterns: Vec<String>) {
31        self.peer_namespaces.insert(router_id.to_string(), patterns);
32    }
33
34    /// Remove a peer's namespace registrations
35    pub fn remove_peer(&mut self, router_id: &str) {
36        self.peer_namespaces.remove(router_id);
37    }
38
39    /// Get all peers that should receive a message for the given address.
40    ///
41    /// Returns peer router IDs whose namespace patterns match the address.
42    /// Excludes the origin peer to prevent loops.
43    pub fn peers_for_address(&self, address: &str, exclude_origin: Option<&str>) -> Vec<String> {
44        self.peer_namespaces
45            .iter()
46            .filter(|(router_id, patterns)| {
47                // Exclude origin to prevent loops
48                if let Some(origin) = exclude_origin {
49                    if router_id.as_str() == origin {
50                        return false;
51                    }
52                }
53                // Check if any of the peer's patterns match this address
54                patterns
55                    .iter()
56                    .any(|p| clasp_core::address::glob_match(p, address))
57            })
58            .map(|(id, _)| id.clone())
59            .collect()
60    }
61
62    /// Check if an address belongs to the local router's namespace
63    pub fn is_local(&self, address: &str) -> bool {
64        self.local_namespaces
65            .iter()
66            .any(|p| clasp_core::address::glob_match(p, address))
67    }
68
69    /// Check if an address belongs to any peer's namespace
70    pub fn is_remote(&self, address: &str) -> bool {
71        self.peer_namespaces.values().any(|patterns| {
72            patterns
73                .iter()
74                .any(|p| clasp_core::address::glob_match(p, address))
75        })
76    }
77
78    /// Check for namespace conflicts between peers.
79    ///
80    /// Returns pairs of (pattern, peer_a, peer_b) for overlapping namespaces.
81    pub fn find_conflicts(&self) -> Vec<(String, String, String)> {
82        let mut conflicts = Vec::new();
83        let peers: Vec<_> = self.peer_namespaces.iter().collect();
84
85        for i in 0..peers.len() {
86            for j in (i + 1)..peers.len() {
87                let (id_a, patterns_a) = peers[i];
88                let (id_b, patterns_b) = peers[j];
89
90                for pa in patterns_a {
91                    for pb in patterns_b {
92                        if patterns_overlap(pa, pb) {
93                            conflicts.push((
94                                format!("{} <-> {}", pa, pb),
95                                id_a.clone(),
96                                id_b.clone(),
97                            ));
98                        }
99                    }
100                }
101            }
102        }
103
104        conflicts
105    }
106
107    /// Get a peer's registered namespaces
108    pub fn peer_patterns(&self, router_id: &str) -> Option<&Vec<String>> {
109        self.peer_namespaces.get(router_id)
110    }
111
112    /// Get all registered peers
113    pub fn peers(&self) -> Vec<String> {
114        self.peer_namespaces.keys().cloned().collect()
115    }
116
117    /// Get local namespaces
118    pub fn local_patterns(&self) -> &[String] {
119        &self.local_namespaces
120    }
121
122    /// Number of registered peers
123    pub fn peer_count(&self) -> usize {
124        self.peer_namespaces.len()
125    }
126}
127
128/// Check if two glob patterns can potentially match the same address.
129///
130/// This is a conservative check -- it may return true for patterns that
131/// don't actually overlap, but will never return false for patterns that do.
132fn patterns_overlap(a: &str, b: &str) -> bool {
133    // If either pattern is "/**" or "**", they overlap with everything
134    if a == "/**" || a == "**" || b == "/**" || b == "**" {
135        return true;
136    }
137
138    // Check if one pattern could match a prefix of the other
139    // Simple heuristic: split by '/' and compare non-wildcard segments
140    let parts_a: Vec<&str> = a.split('/').filter(|s| !s.is_empty()).collect();
141    let parts_b: Vec<&str> = b.split('/').filter(|s| !s.is_empty()).collect();
142
143    let min_len = parts_a.len().min(parts_b.len());
144    for i in 0..min_len {
145        let pa = parts_a[i];
146        let pb = parts_b[i];
147
148        // If either segment is a wildcard, they could overlap
149        if pa == "*" || pa == "**" || pb == "*" || pb == "**" {
150            return true;
151        }
152
153        // If both are literal and different, no overlap
154        if pa != pb {
155            return false;
156        }
157    }
158
159    // If we got here, all compared segments match.
160    // They overlap if either has a ** or they're the same length.
161    true
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_namespace_basics() {
170        let mut ns = NamespaceManager::new(vec!["/local/**".to_string()]);
171
172        ns.register_peer("peer-a", vec!["/site-a/**".to_string()]);
173        ns.register_peer("peer-b", vec!["/site-b/**".to_string()]);
174
175        assert!(ns.is_local("/local/foo"));
176        assert!(!ns.is_local("/site-a/foo"));
177
178        assert!(ns.is_remote("/site-a/foo"));
179        assert!(ns.is_remote("/site-b/foo"));
180        assert!(!ns.is_remote("/local/foo"));
181    }
182
183    #[test]
184    fn test_peers_for_address() {
185        let mut ns = NamespaceManager::new(vec!["/local/**".to_string()]);
186        ns.register_peer("peer-a", vec!["/shared/**".to_string()]);
187        ns.register_peer("peer-b", vec!["/shared/**".to_string()]);
188
189        let peers = ns.peers_for_address("/shared/foo", None);
190        assert_eq!(peers.len(), 2);
191
192        // Exclude origin
193        let peers = ns.peers_for_address("/shared/foo", Some("peer-a"));
194        assert_eq!(peers.len(), 1);
195        assert_eq!(peers[0], "peer-b");
196    }
197
198    #[test]
199    fn test_remove_peer() {
200        let mut ns = NamespaceManager::new(vec![]);
201        ns.register_peer("peer-a", vec!["/a/**".to_string()]);
202        assert_eq!(ns.peer_count(), 1);
203
204        ns.remove_peer("peer-a");
205        assert_eq!(ns.peer_count(), 0);
206        assert!(!ns.is_remote("/a/foo"));
207    }
208
209    #[test]
210    fn test_conflict_detection() {
211        let mut ns = NamespaceManager::new(vec![]);
212        ns.register_peer("peer-a", vec!["/shared/**".to_string()]);
213        ns.register_peer("peer-b", vec!["/shared/**".to_string()]);
214
215        let conflicts = ns.find_conflicts();
216        assert_eq!(conflicts.len(), 1);
217    }
218
219    #[test]
220    fn test_no_conflict_disjoint() {
221        let mut ns = NamespaceManager::new(vec![]);
222        ns.register_peer("peer-a", vec!["/site-a/**".to_string()]);
223        ns.register_peer("peer-b", vec!["/site-b/**".to_string()]);
224
225        let conflicts = ns.find_conflicts();
226        assert!(conflicts.is_empty());
227    }
228
229    #[test]
230    fn test_patterns_overlap() {
231        assert!(patterns_overlap("/**", "/anything"));
232        assert!(patterns_overlap("/a/**", "/a/b/c"));
233        assert!(!patterns_overlap("/a/**", "/b/**"));
234        assert!(patterns_overlap("/shared/**", "/shared/**"));
235        assert!(!patterns_overlap("/site-a/data", "/site-b/data"));
236    }
237}