use std::collections::HashMap;
use axum::Router;
#[derive(Debug)]
pub struct AppEntry {
pub name: String,
pub router: Router,
pub domain: Option<String>,
}
#[derive(Debug, Default)]
pub struct MultiAppDispatcher {
apps: HashMap<String, AppEntry>,
}
impl MultiAppDispatcher {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, name: impl Into<String>, router: Router) {
let name = name.into();
self.apps.insert(
name.clone(),
AppEntry {
name,
router,
domain: None,
},
);
}
pub fn register_with_domain(
&mut self,
name: impl Into<String>,
domain: impl Into<String>,
router: Router,
) {
let name = name.into();
self.apps.insert(
name.clone(),
AppEntry {
name,
router,
domain: Some(domain.into()),
},
);
}
pub fn dispatch_by_path(&self, uri: &str) -> Option<&str> {
let path = uri.split('?').next().unwrap_or(uri);
let first = path
.trim_start_matches('/')
.split('/')
.next()
.filter(|s| !s.is_empty())?;
self.apps.get(first).map(|entry| entry.name.as_str())
}
pub fn dispatch_by_domain(&self, host: &str) -> Option<&str> {
let domain = host.split(':').next().unwrap_or(host);
self.apps
.values()
.find(|entry| entry.domain.as_deref() == Some(domain))
.map(|entry| entry.name.as_str())
}
pub fn get(&self, name: &str) -> Option<&AppEntry> {
self.apps.get(name)
}
pub fn len(&self) -> usize {
self.apps.len()
}
pub fn is_empty(&self) -> bool {
self.apps.is_empty()
}
pub fn app_names(&self) -> Vec<&str> {
self.apps.keys().map(|s| s.as_str()).collect()
}
pub fn build(&self) -> Router {
let mut main = Router::new();
for (name, entry) in &self.apps {
let prefix = format!("/{name}");
main = main.nest(&prefix, entry.router.clone());
}
main
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::{Method, Request, StatusCode};
use http_body_util::BodyExt;
use tower::ServiceExt;
fn make_router(body: &'static str) -> Router {
Router::new().route("/", axum::routing::get(move || async move { body }))
}
#[test]
fn test_register_and_get() {
let mut dispatcher = MultiAppDispatcher::new();
dispatcher.register("oapc", make_router("oapc"));
assert_eq!(dispatcher.len(), 1);
assert!(!dispatcher.is_empty());
assert!(dispatcher.get("oapc").is_some());
assert!(dispatcher.get("admin").is_none());
}
#[test]
fn test_register_with_domain() {
let mut dispatcher = MultiAppDispatcher::new();
dispatcher.register_with_domain("oapc", "oapc.example.com", make_router("oapc"));
let entry = dispatcher.get("oapc").unwrap();
assert_eq!(entry.domain.as_deref(), Some("oapc.example.com"));
}
#[test]
fn test_dispatch_by_path_seven_apps() {
let mut dispatcher = MultiAppDispatcher::new();
for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
dispatcher.register(app, make_router("ok"));
}
for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
assert_eq!(
dispatcher.dispatch_by_path(&format!("/{app}/customer/index")),
Some(app)
);
}
}
#[test]
fn test_dispatch_by_path_with_query_string() {
let mut dispatcher = MultiAppDispatcher::new();
dispatcher.register("oapc", make_router("ok"));
assert_eq!(
dispatcher.dispatch_by_path("/oapc/customer/index?id=1&page=2"),
Some("oapc")
);
}
#[test]
fn test_dispatch_by_path_root_returns_none() {
let mut dispatcher = MultiAppDispatcher::new();
dispatcher.register("oapc", make_router("ok"));
assert_eq!(dispatcher.dispatch_by_path("/"), None);
assert_eq!(dispatcher.dispatch_by_path(""), None);
}
#[test]
fn test_dispatch_by_path_unknown_app_returns_none() {
let mut dispatcher = MultiAppDispatcher::new();
dispatcher.register("oapc", make_router("ok"));
assert_eq!(dispatcher.dispatch_by_path("/unknown/foo/bar"), None);
assert_eq!(dispatcher.dispatch_by_path("/common/foo/bar"), None);
}
#[test]
fn test_dispatch_by_domain() {
let mut dispatcher = MultiAppDispatcher::new();
dispatcher.register_with_domain("oapc", "oapc.example.com", make_router("oapc"));
dispatcher.register_with_domain("admin", "admin.example.com", make_router("admin"));
assert_eq!(
dispatcher.dispatch_by_domain("oapc.example.com"),
Some("oapc")
);
assert_eq!(
dispatcher.dispatch_by_domain("admin.example.com"),
Some("admin")
);
assert_eq!(dispatcher.dispatch_by_domain("unknown.com"), None);
}
#[test]
fn test_dispatch_by_domain_with_port() {
let mut dispatcher = MultiAppDispatcher::new();
dispatcher.register_with_domain("oapc", "oapc.example.com", make_router("ok"));
assert_eq!(
dispatcher.dispatch_by_domain("oapc.example.com:8801"),
Some("oapc")
);
assert_eq!(
dispatcher.dispatch_by_domain("oapc.example.com:8443"),
Some("oapc")
);
}
#[test]
fn test_app_names() {
let mut dispatcher = MultiAppDispatcher::new();
dispatcher.register("oapc", make_router("a"));
dispatcher.register("admin", make_router("b"));
let mut names = dispatcher.app_names();
names.sort();
assert_eq!(names, vec!["admin", "oapc"]);
}
#[tokio::test]
async fn test_build_nests_all_apps() {
let mut dispatcher = MultiAppDispatcher::new();
dispatcher.register("oapc", make_router("oapc body"));
dispatcher.register("admin", make_router("admin body"));
let router = dispatcher.build();
let req = Request::builder()
.method(Method::GET)
.uri("/oapc")
.body(Body::empty())
.unwrap();
let resp = router.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
assert_eq!(&bytes[..], b"oapc body");
let req = Request::builder()
.method(Method::GET)
.uri("/admin")
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
assert_eq!(&bytes[..], b"admin body");
}
#[tokio::test]
async fn test_build_empty_dispatcher() {
let dispatcher = MultiAppDispatcher::new();
let router = dispatcher.build();
let req = Request::builder()
.method(Method::GET)
.uri("/oapc")
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_build_with_seven_apps() {
let mut dispatcher = MultiAppDispatcher::new();
for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
let body_str = Box::leak(format!("{app} body").into_boxed_str());
dispatcher.register(app, make_router(body_str));
}
let router = dispatcher.build();
for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
let req = Request::builder()
.method(Method::GET)
.uri(format!("/{app}"))
.body(Body::empty())
.unwrap();
let resp = router.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
}
#[test]
fn test_empty_dispatcher() {
let dispatcher = MultiAppDispatcher::new();
assert!(dispatcher.is_empty());
assert_eq!(dispatcher.len(), 0);
assert!(dispatcher.app_names().is_empty());
}
}