Skip to main content

mockforge_federation/
router.rs

1//! Federation router
2//!
3//! Routes requests to appropriate workspaces based on service boundaries.
4
5use crate::federation::Federation;
6use crate::service::ServiceBoundary;
7use chrono::Utc;
8use std::sync::Arc;
9use tracing::debug;
10use uuid::Uuid;
11
12/// Routing result
13#[derive(Debug, Clone)]
14pub struct RoutingResult {
15    /// Target workspace ID
16    pub workspace_id: Uuid,
17    /// Service boundary
18    pub service: Arc<ServiceBoundary>,
19    /// Service-specific path (path with `base_path` stripped)
20    pub service_path: String,
21}
22
23/// Federation router
24///
25/// Routes incoming requests to the appropriate workspace based on
26/// service boundaries defined in the federation.
27pub struct FederationRouter {
28    /// Active federation
29    federation: Arc<Federation>,
30}
31
32impl FederationRouter {
33    /// Create a new federation router
34    #[must_use]
35    pub const fn new(federation: Arc<Federation>) -> Self {
36        Self { federation }
37    }
38
39    /// Route a request to the appropriate workspace
40    ///
41    /// Returns the workspace ID and service path for the request.
42    pub fn route(&self, path: &str) -> Option<RoutingResult> {
43        debug!(path = %path, "Routing request in federation");
44
45        let service = self.federation.find_service_by_path(path)?;
46
47        let service_path = service.extract_service_path(path)?;
48
49        debug!(
50            path = %path,
51            service = %service.name,
52            workspace_id = %service.workspace_id,
53            service_path = %service_path,
54            "Routed request to service"
55        );
56
57        Some(RoutingResult {
58            workspace_id: service.workspace_id,
59            service: Arc::new(service.clone()),
60            service_path,
61        })
62    }
63
64    /// Get all services in the federation
65    #[must_use]
66    pub fn services(&self) -> &[ServiceBoundary] {
67        &self.federation.services
68    }
69
70    /// Get federation ID
71    #[must_use]
72    pub fn federation_id(&self) -> Uuid {
73        self.federation.id
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::service::ServiceRealityLevel;
81
82    fn create_test_federation() -> Arc<Federation> {
83        Arc::new(Federation {
84            id: Uuid::new_v4(),
85            name: "test".to_string(),
86            description: String::new(),
87            org_id: Uuid::new_v4(),
88            services: vec![
89                ServiceBoundary::new(
90                    "auth".to_string(),
91                    Uuid::new_v4(),
92                    "/auth".to_string(),
93                    ServiceRealityLevel::Real,
94                ),
95                ServiceBoundary::new(
96                    "payments".to_string(),
97                    Uuid::new_v4(),
98                    "/payments".to_string(),
99                    ServiceRealityLevel::MockV3,
100                ),
101            ],
102            created_at: Utc::now(),
103            updated_at: Utc::now(),
104        })
105    }
106
107    #[test]
108    fn test_router() {
109        let federation = create_test_federation();
110        let router = FederationRouter::new(federation);
111        let result = router.route("/auth/login");
112
113        assert!(result.is_some());
114        let routing = result.unwrap();
115        assert_eq!(routing.service_path, "/login");
116    }
117
118    #[test]
119    fn test_router_new() {
120        let federation = create_test_federation();
121        let router = FederationRouter::new(federation.clone());
122        assert_eq!(router.federation_id(), federation.id);
123    }
124
125    #[test]
126    fn test_router_route_exact_path() {
127        let federation = create_test_federation();
128        let router = FederationRouter::new(federation);
129
130        let result = router.route("/auth").unwrap();
131        assert_eq!(result.service_path, "/");
132        assert_eq!(result.service.name, "auth");
133    }
134
135    #[test]
136    fn test_router_route_nested_path() {
137        let federation = create_test_federation();
138        let router = FederationRouter::new(federation);
139
140        let result = router.route("/payments/process/order/123").unwrap();
141        assert_eq!(result.service_path, "/process/order/123");
142        assert_eq!(result.service.name, "payments");
143    }
144
145    #[test]
146    fn test_router_route_no_match() {
147        let federation = create_test_federation();
148        let router = FederationRouter::new(federation);
149
150        assert!(router.route("/unknown").is_none());
151        assert!(router.route("/api/users").is_none());
152        assert!(router.route("").is_none());
153    }
154
155    #[test]
156    fn test_router_services() {
157        let federation = create_test_federation();
158        let router = FederationRouter::new(federation);
159
160        let services = router.services();
161        assert_eq!(services.len(), 2);
162        assert!(services.iter().any(|s| s.name == "auth"));
163        assert!(services.iter().any(|s| s.name == "payments"));
164    }
165
166    #[test]
167    fn test_router_federation_id() {
168        let federation = create_test_federation();
169        let expected_id = federation.id;
170        let router = FederationRouter::new(federation);
171
172        assert_eq!(router.federation_id(), expected_id);
173    }
174
175    #[test]
176    fn test_routing_result_contains_workspace_id() {
177        let federation = create_test_federation();
178        let expected_workspace_id = federation.services[0].workspace_id;
179        let router = FederationRouter::new(federation);
180
181        let result = router.route("/auth/login").unwrap();
182        assert_eq!(result.workspace_id, expected_workspace_id);
183    }
184
185    #[test]
186    fn test_routing_result_debug() {
187        let federation = create_test_federation();
188        let router = FederationRouter::new(federation);
189
190        let result = router.route("/auth/login").unwrap();
191        let debug = format!("{result:?}");
192        assert!(debug.contains("RoutingResult"));
193        assert!(debug.contains("service_path"));
194    }
195
196    #[test]
197    fn test_routing_result_clone() {
198        let federation = create_test_federation();
199        let router = FederationRouter::new(federation);
200
201        let result = router.route("/auth/login").unwrap();
202        let cloned = result.clone();
203
204        assert_eq!(result.workspace_id, cloned.workspace_id);
205        assert_eq!(result.service_path, cloned.service_path);
206        assert_eq!(result.service.name, cloned.service.name);
207    }
208
209    #[test]
210    fn test_router_routes_to_different_services() {
211        let federation = create_test_federation();
212        let router = FederationRouter::new(federation);
213
214        let auth_result = router.route("/auth/login").unwrap();
215        let payment_result = router.route("/payments/process").unwrap();
216
217        assert_eq!(auth_result.service.name, "auth");
218        assert_eq!(payment_result.service.name, "payments");
219        assert_ne!(auth_result.workspace_id, payment_result.workspace_id);
220    }
221
222    #[test]
223    fn test_router_with_empty_services() {
224        let federation = Arc::new(Federation {
225            id: Uuid::new_v4(),
226            name: "empty".to_string(),
227            description: String::new(),
228            org_id: Uuid::new_v4(),
229            services: vec![],
230            created_at: Utc::now(),
231            updated_at: Utc::now(),
232        });
233
234        let router = FederationRouter::new(federation);
235        assert!(router.route("/any/path").is_none());
236        assert!(router.services().is_empty());
237    }
238}