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::web::boundary::BoundaryFilter;
27use crate::web::context::RequestContext;
28
29pub const DYNAMIC_PREFIX: &str = "/_plugins";
31
32type Table = HashMap<(Method, String), PluginHandler>;
33
34pub struct DynamicRouteTable {
38 table: ArcSwap<Table>,
39 globals: std::sync::OnceLock<&'static [&'static dyn crate::web::interceptors::Interceptor]>,
44}
45
46impl DynamicRouteTable {
47 pub(crate) fn new() -> Self {
48 Self {
49 table: ArcSwap::from_pointee(Table::new()),
50 globals: std::sync::OnceLock::new(),
51 }
52 }
53
54 pub(crate) fn set_globals(
57 &self,
58 globals: &'static [&'static dyn crate::web::interceptors::Interceptor],
59 ) {
60 let _ = self.globals.set(globals);
61 }
62
63 pub fn mount<F, Fut>(&self, method: Method, path: &str, handler: F) -> Option<PluginHandler>
67 where
68 F: Fn(RequestContext) -> Fut + Send + Sync + 'static,
69 Fut: std::future::Future<Output = Response> + Send + 'static,
70 {
71 let full = format!("{DYNAMIC_PREFIX}{path}");
72 let raw: PluginHandler = Arc::new(move |ctx| Box::pin(handler(ctx)));
73 let h: PluginHandler = match self.globals.get() {
76 Some(globals) => crate::web::interceptors::compose_chain(globals, raw),
77 None => raw,
78 };
79 let key = (method, full);
80 let mut replaced = None;
81 self.table.rcu(|cur| {
82 let mut next: Table = (**cur).clone();
83 replaced = next.insert(key.clone(), h.clone());
84 next
85 });
86 replaced
87 }
88
89 pub fn unmount(&self, method: Method, path: &str) -> bool {
91 let key = (method, format!("{DYNAMIC_PREFIX}{path}"));
92 let mut removed = false;
93 self.table.rcu(|cur| {
94 let mut next: Table = (**cur).clone();
95 removed = next.remove(&key).is_some();
96 next
97 });
98 removed
99 }
100
101 fn lookup(&self, method: &Method, path: &str) -> Option<PluginHandler> {
103 self.table
104 .load()
105 .get(&(method.clone(), path.to_owned()))
106 .cloned()
107 }
108}
109
110pub(crate) fn dynamic_dispatch_route(
113 container: &'static FrozenDiContainer,
114 filters: &'static [&'static dyn BoundaryFilter],
115) -> axum::routing::MethodRouter {
116 let handler = move |req: axum::extract::Request| async move {
117 let (parts, body) = req.into_parts();
118 let table = container.get::<DynamicRouteTable>();
119 let Some(h) = table.lookup(&parts.method, parts.uri.path()) else {
120 return Response::builder()
121 .status(404)
122 .body(axum::body::Body::from("dynamic route not found"))
123 .expect("static 404");
124 };
125 crate::web::boundary::run_entry(
128 parts,
129 body,
130 Default::default(),
131 container,
132 "/_plugins/*",
133 None,
134 filters,
135 &h,
136 )
137 .await
138 };
139
140 any(handler)
141}