arcly_http/web/
dynamic.rs1use std::collections::HashMap;
17use std::sync::Arc;
18
19use arc_swap::ArcSwap;
20use axum::http::Method;
21use axum::routing::any;
22
23use crate::core::engine::FrozenDiContainer;
24use crate::core::plugins::PluginHandler;
25use crate::http::Response;
26use crate::observability::lean_telemetry::on_request_start;
27use crate::web::boundary::{assemble_context, BoundaryFilter, InFlightGuard};
28use crate::web::context::RequestContext;
29
30pub const DYNAMIC_PREFIX: &str = "/_plugins";
32
33type Table = HashMap<(Method, String), PluginHandler>;
34
35pub struct DynamicRouteTable {
39 table: ArcSwap<Table>,
40}
41
42impl DynamicRouteTable {
43 pub(crate) fn new() -> Self {
44 Self {
45 table: ArcSwap::from_pointee(Table::new()),
46 }
47 }
48
49 pub fn mount<F, Fut>(&self, method: Method, path: &str, handler: F) -> Option<PluginHandler>
53 where
54 F: Fn(RequestContext) -> Fut + Send + Sync + 'static,
55 Fut: std::future::Future<Output = Response> + Send + 'static,
56 {
57 let full = format!("{DYNAMIC_PREFIX}{path}");
58 let h: PluginHandler = Arc::new(move |ctx| Box::pin(handler(ctx)));
59 let key = (method, full);
60 let mut replaced = None;
61 self.table.rcu(|cur| {
62 let mut next: Table = (**cur).clone();
63 replaced = next.insert(key.clone(), h.clone());
64 next
65 });
66 replaced
67 }
68
69 pub fn unmount(&self, method: Method, path: &str) -> bool {
71 let key = (method, format!("{DYNAMIC_PREFIX}{path}"));
72 let mut removed = false;
73 self.table.rcu(|cur| {
74 let mut next: Table = (**cur).clone();
75 removed = next.remove(&key).is_some();
76 next
77 });
78 removed
79 }
80
81 fn lookup(&self, method: &Method, path: &str) -> Option<PluginHandler> {
83 self.table
84 .load()
85 .get(&(method.clone(), path.to_owned()))
86 .cloned()
87 }
88}
89
90pub(crate) fn dynamic_dispatch_route(
93 container: &'static FrozenDiContainer,
94 globals: &'static [&'static dyn crate::web::interceptors::Interceptor],
95 filters: &'static [&'static dyn BoundaryFilter],
96) -> axum::routing::MethodRouter {
97 let handler = move |req: axum::extract::Request| async move {
98 let (parts, body) = req.into_parts();
99 if let Some(reject) = crate::web::boundary::run_boundary_filters(filters, &parts) {
100 return reject;
101 }
102 let table = container.get::<DynamicRouteTable>();
103 let Some(h) = table.lookup(&parts.method, parts.uri.path()) else {
104 return Response::builder()
105 .status(404)
106 .body(axum::body::Body::from("dynamic route not found"))
107 .expect("static 404");
108 };
109 let ctx = assemble_context(
112 parts,
113 body,
114 Default::default(),
115 container,
116 "/_plugins/*",
117 None,
118 )
119 .await;
120
121 let _guard = on_request_start();
122 let _in_flight = InFlightGuard::new();
123 let chain = crate::web::interceptors::compose_chain(globals, h);
124 chain(ctx).await
125 };
126
127 any(handler)
128}