lmrc_proxy/
routing.rs

1//! Routing and subdomain handling
2//!
3//! Provides traits and utilities for routing requests based on subdomains or other criteria.
4
5use async_trait::async_trait;
6use std::collections::HashMap;
7
8/// Route resolver trait
9///
10/// Implement this trait to provide custom routing logic.
11///
12/// ## Example
13///
14/// ```rust
15/// use lmrc_proxy::routing::RouteResolver;
16/// use async_trait::async_trait;
17/// use std::collections::HashMap;
18///
19/// struct StaticRouter {
20///     routes: HashMap<String, String>,
21/// }
22///
23/// #[async_trait]
24/// impl RouteResolver for StaticRouter {
25///     async fn resolve(&self, key: &str) -> Option<String> {
26///         self.routes.get(key).cloned()
27///     }
28/// }
29/// ```
30#[async_trait]
31pub trait RouteResolver: Send + Sync {
32    /// Resolve a routing key (e.g., subdomain) to a backend URL
33    async fn resolve(&self, key: &str) -> Option<String>;
34}
35
36/// Static route resolver using a HashMap
37pub struct StaticRouteResolver {
38    routes: HashMap<String, String>,
39}
40
41impl StaticRouteResolver {
42    /// Create a new static route resolver
43    pub fn new() -> Self {
44        Self {
45            routes: HashMap::new(),
46        }
47    }
48
49    /// Add a route
50    pub fn add_route(mut self, key: impl Into<String>, backend_url: impl Into<String>) -> Self {
51        self.routes.insert(key.into(), backend_url.into());
52        self
53    }
54
55    /// Add routes from a HashMap
56    pub fn with_routes(mut self, routes: HashMap<String, String>) -> Self {
57        self.routes = routes;
58        self
59    }
60}
61
62impl Default for StaticRouteResolver {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68#[async_trait]
69impl RouteResolver for StaticRouteResolver {
70    async fn resolve(&self, key: &str) -> Option<String> {
71        self.routes.get(key).cloned()
72    }
73}
74
75/// Extract subdomain from Host header
76///
77/// ## Examples
78///
79/// ```rust
80/// use lmrc_proxy::routing::extract_subdomain;
81///
82/// assert_eq!(extract_subdomain("api.example.com"), Some("api".to_string()));
83/// assert_eq!(extract_subdomain("infra.example.com:8080"), Some("infra".to_string()));
84/// assert_eq!(extract_subdomain("example.com"), None);
85/// assert_eq!(extract_subdomain("localhost"), Some("infra".to_string())); // Development default
86/// ```
87pub fn extract_subdomain(host: &str) -> Option<String> {
88    // Remove port if present
89    let host = host.split(':').next().unwrap_or(host);
90
91    // Split by dots
92    let parts: Vec<&str> = host.split('.').collect();
93
94    // If we have at least 3 parts (subdomain.domain.tld), extract subdomain
95    if parts.len() >= 3 {
96        Some(parts[0].to_string())
97    } else if parts.len() == 2 {
98        // Could be domain.tld or localhost:port
99        None
100    } else if parts.len() == 1 && parts[0] == "localhost" {
101        // Development: treat localhost as infra subdomain
102        Some("infra".to_string())
103    } else {
104        None
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_extract_subdomain() {
114        assert_eq!(
115            extract_subdomain("infra.example.com"),
116            Some("infra".to_string())
117        );
118        assert_eq!(
119            extract_subdomain("api.example.com"),
120            Some("api".to_string())
121        );
122        assert_eq!(extract_subdomain("example.com"), None);
123        assert_eq!(extract_subdomain("localhost"), Some("infra".to_string()));
124        assert_eq!(
125            extract_subdomain("infra.example.com:8080"),
126            Some("infra".to_string())
127        );
128        assert_eq!(
129            extract_subdomain("my-service.example.com"),
130            Some("my-service".to_string())
131        );
132    }
133
134    #[tokio::test]
135    async fn test_static_route_resolver() {
136        let resolver = StaticRouteResolver::new()
137            .add_route("api", "http://api-backend:8080")
138            .add_route("admin", "http://admin-backend:9000");
139
140        assert_eq!(
141            resolver.resolve("api").await,
142            Some("http://api-backend:8080".to_string())
143        );
144        assert_eq!(
145            resolver.resolve("admin").await,
146            Some("http://admin-backend:9000".to_string())
147        );
148        assert_eq!(resolver.resolve("unknown").await, None);
149    }
150}