use std::sync::Arc;
use crate::runtime::ServiceRuntime;
#[derive(Debug)]
pub struct Router {
services: Vec<Arc<ServiceRuntime>>,
}
impl Router {
#[must_use]
pub fn new(services: Vec<Arc<ServiceRuntime>>) -> Self {
Self { services }
}
#[must_use]
pub fn services(&self) -> &[Arc<ServiceRuntime>] {
&self.services
}
#[must_use]
pub fn route(&self, host: &str, path: &str) -> Option<Arc<ServiceRuntime>> {
let host = normalize_host(host);
let path = path_only(path);
let mut best: Option<(&Arc<ServiceRuntime>, u32, usize)> = None;
for service in &self.services {
let cfg = service.config();
let Some(specificity) = host_specificity(&cfg.host, host) else {
continue;
};
let prefix = cfg.path_prefix.as_deref().unwrap_or("");
if !path_matches_prefix(path, prefix) {
continue;
}
let score = (specificity, prefix.len());
let better = match best {
None => true,
Some((_, best_spec, best_len)) => score > (best_spec, best_len),
};
if better {
best = Some((service, specificity, prefix.len()));
}
}
best.map(|(service, _, _)| Arc::clone(service))
}
}
fn normalize_host(host: &str) -> &str {
host.split(':').next().unwrap_or(host).trim()
}
fn path_only(path: &str) -> &str {
path.split(['?', '#']).next().unwrap_or(path)
}
fn host_specificity(pattern: &str, host: &str) -> Option<u32> {
if pattern.eq_ignore_ascii_case(host) {
return Some(2);
}
if let Some(suffix) = pattern.strip_prefix("*.") {
let dotted = format!(".{suffix}");
if host.len() > dotted.len() && host.to_ascii_lowercase().ends_with(&dotted.to_ascii_lowercase())
{
return Some(1);
}
}
None
}
fn path_matches_prefix(path: &str, prefix: &str) -> bool {
if prefix.is_empty() || prefix == "/" {
return true;
}
let prefix = prefix.strip_suffix('/').unwrap_or(prefix);
if !path.starts_with(prefix) {
return false;
}
match path.as_bytes().get(prefix.len()) {
None => true,
Some(&b) => b == b'/',
}
}