1use std::collections::HashMap;
51
52use axum::Router;
53
54#[derive(Debug)]
56pub struct AppEntry {
57 pub name: String,
59 pub router: Router,
61 pub domain: Option<String>,
63}
64
65#[derive(Debug, Default)]
69pub struct MultiAppDispatcher {
70 apps: HashMap<String, AppEntry>,
72}
73
74impl MultiAppDispatcher {
75 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn register(&mut self, name: impl Into<String>, router: Router) {
87 let name = name.into();
88 self.apps.insert(
89 name.clone(),
90 AppEntry {
91 name,
92 router,
93 domain: None,
94 },
95 );
96 }
97
98 pub fn register_with_domain(
108 &mut self,
109 name: impl Into<String>,
110 domain: impl Into<String>,
111 router: Router,
112 ) {
113 let name = name.into();
114 self.apps.insert(
115 name.clone(),
116 AppEntry {
117 name,
118 router,
119 domain: Some(domain.into()),
120 },
121 );
122 }
123
124 pub fn dispatch_by_path(&self, uri: &str) -> Option<&str> {
138 let path = uri.split('?').next().unwrap_or(uri);
139 let first = path
140 .trim_start_matches('/')
141 .split('/')
142 .next()
143 .filter(|s| !s.is_empty())?;
144
145 self.apps.get(first).map(|entry| entry.name.as_str())
146 }
147
148 pub fn dispatch_by_domain(&self, host: &str) -> Option<&str> {
162 let domain = host.split(':').next().unwrap_or(host);
164 self.apps
165 .values()
166 .find(|entry| entry.domain.as_deref() == Some(domain))
167 .map(|entry| entry.name.as_str())
168 }
169
170 pub fn get(&self, name: &str) -> Option<&AppEntry> {
172 self.apps.get(name)
173 }
174
175 pub fn len(&self) -> usize {
177 self.apps.len()
178 }
179
180 pub fn is_empty(&self) -> bool {
182 self.apps.is_empty()
183 }
184
185 pub fn app_names(&self) -> Vec<&str> {
187 self.apps.keys().map(|s| s.as_str()).collect()
188 }
189
190 pub fn build(&self) -> Router {
200 let mut main = Router::new();
201 for (name, entry) in &self.apps {
202 let prefix = format!("/{name}");
203 main = main.nest(&prefix, entry.router.clone());
204 }
205 main
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use axum::body::Body;
213 use axum::http::{Method, Request, StatusCode};
214 use http_body_util::BodyExt;
215 use tower::ServiceExt;
216
217 fn make_router(body: &'static str) -> Router {
218 Router::new().route("/", axum::routing::get(move || async move { body }))
219 }
220
221 #[test]
222 fn test_register_and_get() {
223 let mut dispatcher = MultiAppDispatcher::new();
224 dispatcher.register("oapc", make_router("oapc"));
225 assert_eq!(dispatcher.len(), 1);
226 assert!(!dispatcher.is_empty());
227 assert!(dispatcher.get("oapc").is_some());
228 assert!(dispatcher.get("admin").is_none());
229 }
230
231 #[test]
232 fn test_register_with_domain() {
233 let mut dispatcher = MultiAppDispatcher::new();
234 dispatcher.register_with_domain("oapc", "oapc.example.com", make_router("oapc"));
235 let entry = dispatcher.get("oapc").unwrap();
236 assert_eq!(entry.domain.as_deref(), Some("oapc.example.com"));
237 }
238
239 #[test]
240 fn test_dispatch_by_path_seven_apps() {
241 let mut dispatcher = MultiAppDispatcher::new();
242 for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
243 dispatcher.register(app, make_router("ok"));
244 }
245
246 for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
247 assert_eq!(
248 dispatcher.dispatch_by_path(&format!("/{app}/customer/index")),
249 Some(app)
250 );
251 }
252 }
253
254 #[test]
255 fn test_dispatch_by_path_with_query_string() {
256 let mut dispatcher = MultiAppDispatcher::new();
257 dispatcher.register("oapc", make_router("ok"));
258 assert_eq!(
259 dispatcher.dispatch_by_path("/oapc/customer/index?id=1&page=2"),
260 Some("oapc")
261 );
262 }
263
264 #[test]
265 fn test_dispatch_by_path_root_returns_none() {
266 let mut dispatcher = MultiAppDispatcher::new();
267 dispatcher.register("oapc", make_router("ok"));
268 assert_eq!(dispatcher.dispatch_by_path("/"), None);
269 assert_eq!(dispatcher.dispatch_by_path(""), None);
270 }
271
272 #[test]
273 fn test_dispatch_by_path_unknown_app_returns_none() {
274 let mut dispatcher = MultiAppDispatcher::new();
275 dispatcher.register("oapc", make_router("ok"));
276 assert_eq!(dispatcher.dispatch_by_path("/unknown/foo/bar"), None);
277 assert_eq!(dispatcher.dispatch_by_path("/common/foo/bar"), None);
279 }
280
281 #[test]
282 fn test_dispatch_by_domain() {
283 let mut dispatcher = MultiAppDispatcher::new();
284 dispatcher.register_with_domain("oapc", "oapc.example.com", make_router("oapc"));
285 dispatcher.register_with_domain("admin", "admin.example.com", make_router("admin"));
286
287 assert_eq!(
288 dispatcher.dispatch_by_domain("oapc.example.com"),
289 Some("oapc")
290 );
291 assert_eq!(
292 dispatcher.dispatch_by_domain("admin.example.com"),
293 Some("admin")
294 );
295 assert_eq!(dispatcher.dispatch_by_domain("unknown.com"), None);
296 }
297
298 #[test]
299 fn test_dispatch_by_domain_with_port() {
300 let mut dispatcher = MultiAppDispatcher::new();
301 dispatcher.register_with_domain("oapc", "oapc.example.com", make_router("ok"));
302
303 assert_eq!(
305 dispatcher.dispatch_by_domain("oapc.example.com:8801"),
306 Some("oapc")
307 );
308 assert_eq!(
309 dispatcher.dispatch_by_domain("oapc.example.com:8443"),
310 Some("oapc")
311 );
312 }
313
314 #[test]
315 fn test_app_names() {
316 let mut dispatcher = MultiAppDispatcher::new();
317 dispatcher.register("oapc", make_router("a"));
318 dispatcher.register("admin", make_router("b"));
319
320 let mut names = dispatcher.app_names();
321 names.sort();
322 assert_eq!(names, vec!["admin", "oapc"]);
323 }
324
325 #[tokio::test]
326 async fn test_build_nests_all_apps() {
327 let mut dispatcher = MultiAppDispatcher::new();
328 dispatcher.register("oapc", make_router("oapc body"));
329 dispatcher.register("admin", make_router("admin body"));
330
331 let router = dispatcher.build();
332
333 let req = Request::builder()
335 .method(Method::GET)
336 .uri("/oapc")
337 .body(Body::empty())
338 .unwrap();
339 let resp = router.clone().oneshot(req).await.unwrap();
340 assert_eq!(resp.status(), StatusCode::OK);
341 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
342 assert_eq!(&bytes[..], b"oapc body");
343
344 let req = Request::builder()
346 .method(Method::GET)
347 .uri("/admin")
348 .body(Body::empty())
349 .unwrap();
350 let resp = router.oneshot(req).await.unwrap();
351 assert_eq!(resp.status(), StatusCode::OK);
352 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
353 assert_eq!(&bytes[..], b"admin body");
354 }
355
356 #[tokio::test]
357 async fn test_build_empty_dispatcher() {
358 let dispatcher = MultiAppDispatcher::new();
359 let router = dispatcher.build();
360 let req = Request::builder()
362 .method(Method::GET)
363 .uri("/oapc")
364 .body(Body::empty())
365 .unwrap();
366 let resp = router.oneshot(req).await.unwrap();
367 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
368 }
369
370 #[tokio::test]
371 async fn test_build_with_seven_apps() {
372 let mut dispatcher = MultiAppDispatcher::new();
373 for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
374 let body_str = Box::leak(format!("{app} body").into_boxed_str());
375 dispatcher.register(app, make_router(body_str));
376 }
377 let router = dispatcher.build();
378
379 for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
380 let req = Request::builder()
381 .method(Method::GET)
382 .uri(format!("/{app}"))
383 .body(Body::empty())
384 .unwrap();
385 let resp = router.clone().oneshot(req).await.unwrap();
386 assert_eq!(resp.status(), StatusCode::OK);
387 }
388 }
389
390 #[test]
391 fn test_empty_dispatcher() {
392 let dispatcher = MultiAppDispatcher::new();
393 assert!(dispatcher.is_empty());
394 assert_eq!(dispatcher.len(), 0);
395 assert!(dispatcher.app_names().is_empty());
396 }
397}