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