mod rule;
pub mod tcp;
pub use rule::Rule;
use crate::config::RouterConfig;
use crate::error::{GatewayError, Result};
use http::HeaderMap;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ResolvedRoute {
pub router_name: String,
pub service_name: String,
pub middlewares: Vec<String>,
}
pub struct RouterTable {
routes: Vec<CompiledRoute>,
}
struct CompiledRoute {
name: String,
rule_expr: String,
rule: Rule,
service: String,
entrypoints: Vec<String>,
middlewares: Vec<String>,
priority: i32,
}
impl RouterTable {
pub fn from_config(routers: &HashMap<String, RouterConfig>) -> Result<Self> {
let mut routes: Vec<CompiledRoute> = Vec::new();
for (name, config) in routers {
let rule = Rule::parse(&config.rule).map_err(|e| {
GatewayError::Config(format!(
"Router '{}': invalid rule '{}': {}",
name, config.rule, e
))
})?;
routes.push(CompiledRoute {
name: name.clone(),
rule_expr: config.rule.clone(),
rule,
service: config.service.clone(),
entrypoints: config.entrypoints.clone(),
middlewares: config.middlewares.clone(),
priority: config.priority,
});
}
routes.sort_by_key(|r| r.priority);
Ok(Self { routes })
}
pub fn match_request(
&self,
host: Option<&str>,
path: &str,
method: &str,
headers: &HeaderMap,
entrypoint: &str,
) -> Option<ResolvedRoute> {
for route in &self.routes {
if !route.entrypoints.is_empty() && !route.entrypoints.iter().any(|ep| ep == entrypoint)
{
continue;
}
if route.rule.matches(host, path, method, headers) {
return Some(ResolvedRoute {
router_name: route.name.clone(),
service_name: route.service.clone(),
middlewares: route.middlewares.clone(),
});
}
}
None
}
pub fn len(&self) -> usize {
self.routes.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.routes.is_empty()
}
pub fn routes_info(&self) -> Vec<RouteInfoSnapshot> {
self.routes
.iter()
.map(|r| RouteInfoSnapshot {
name: r.name.clone(),
rule: r.rule_expr.clone(),
service: r.service.clone(),
entrypoints: r.entrypoints.clone(),
middlewares: r.middlewares.clone(),
priority: r.priority,
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct RouteInfoSnapshot {
pub name: String,
pub rule: String,
pub service: String,
pub entrypoints: Vec<String>,
pub middlewares: Vec<String>,
pub priority: i32,
}
#[cfg(test)]
mod tests {
use super::*;
fn make_routers() -> HashMap<String, RouterConfig> {
let mut routers = HashMap::new();
routers.insert(
"api".to_string(),
RouterConfig {
rule: "PathPrefix(`/api`)".to_string(),
service: "backend".to_string(),
entrypoints: vec!["web".to_string()],
middlewares: vec!["auth".to_string()],
priority: 0,
},
);
routers.insert(
"health".to_string(),
RouterConfig {
rule: "Path(`/health`)".to_string(),
service: "health-svc".to_string(),
entrypoints: vec![],
middlewares: vec![],
priority: -1, },
);
routers
}
#[test]
fn test_router_table_build() {
let routers = make_routers();
let table = RouterTable::from_config(&routers).unwrap();
assert_eq!(table.len(), 2);
}
#[test]
fn test_router_table_match_path() {
let routers = make_routers();
let table = RouterTable::from_config(&routers).unwrap();
let headers = http::HeaderMap::new();
let result = table.match_request(None, "/api/users", "GET", &headers, "web");
assert!(result.is_some());
let route = result.unwrap();
assert_eq!(route.service_name, "backend");
assert_eq!(route.middlewares, vec!["auth"]);
}
#[test]
fn test_router_table_match_exact_path() {
let routers = make_routers();
let table = RouterTable::from_config(&routers).unwrap();
let headers = http::HeaderMap::new();
let result = table.match_request(None, "/health", "GET", &headers, "web");
assert!(result.is_some());
assert_eq!(result.unwrap().service_name, "health-svc");
}
#[test]
fn test_router_table_no_match() {
let routers = make_routers();
let table = RouterTable::from_config(&routers).unwrap();
let headers = http::HeaderMap::new();
let result = table.match_request(None, "/unknown", "GET", &headers, "web");
assert!(result.is_none());
}
#[test]
fn test_router_table_entrypoint_filter() {
let routers = make_routers();
let table = RouterTable::from_config(&routers).unwrap();
let headers = http::HeaderMap::new();
let result = table.match_request(None, "/api/users", "GET", &headers, "other");
assert!(result.is_none());
}
#[test]
fn test_router_table_priority_order() {
let routers = make_routers();
let table = RouterTable::from_config(&routers).unwrap();
let headers = http::HeaderMap::new();
let result = table.match_request(None, "/health", "GET", &headers, "web");
assert!(result.is_some());
assert_eq!(result.unwrap().router_name, "health");
}
#[test]
fn test_router_table_empty() {
let routers = HashMap::new();
let table = RouterTable::from_config(&routers).unwrap();
assert!(table.is_empty());
}
#[test]
fn test_router_table_invalid_rule() {
let mut routers = HashMap::new();
routers.insert(
"bad".to_string(),
RouterConfig {
rule: "InvalidMatcher(`test`)".to_string(),
service: "svc".to_string(),
entrypoints: vec![],
middlewares: vec![],
priority: 0,
},
);
let result = RouterTable::from_config(&routers);
assert!(result.is_err());
}
}