arcly_http_core/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 #[doc(hidden)]
48 pub fn new() -> Self {
49 Self {
50 table: ArcSwap::from_pointee(Table::new()),
51 globals: std::sync::OnceLock::new(),
52 }
53 }
54
55 #[doc(hidden)]
58 pub fn set_globals(
59 &self,
60 globals: &'static [&'static dyn crate::web::interceptors::Interceptor],
61 ) {
62 let _ = self.globals.set(globals);
63 }
64
65 pub fn mount<F, Fut>(&self, method: Method, path: &str, handler: F) -> Option<PluginHandler>
69 where
70 F: Fn(RequestContext) -> Fut + Send + Sync + 'static,
71 Fut: std::future::Future<Output = Response> + Send + 'static,
72 {
73 let full = format!("{DYNAMIC_PREFIX}{path}");
74 let raw: PluginHandler = Arc::new(move |ctx| Box::pin(handler(ctx)));
75 let h: PluginHandler = match self.globals.get() {
78 Some(globals) => crate::web::interceptors::compose_chain(globals, raw),
79 None => raw,
80 };
81 let key = (method, full);
82 let mut replaced = None;
83 self.table.rcu(|cur| {
84 let mut next: Table = (**cur).clone();
85 replaced = next.insert(key.clone(), h.clone());
86 next
87 });
88 replaced
89 }
90
91 pub fn unmount(&self, method: Method, path: &str) -> bool {
93 let key = (method, format!("{DYNAMIC_PREFIX}{path}"));
94 let mut removed = false;
95 self.table.rcu(|cur| {
96 let mut next: Table = (**cur).clone();
97 removed = next.remove(&key).is_some();
98 next
99 });
100 removed
101 }
102
103 fn lookup(&self, method: &Method, path: &str) -> Option<PluginHandler> {
105 self.table
106 .load()
107 .get(&(method.clone(), path.to_owned()))
108 .cloned()
109 }
110}
111
112#[doc(hidden)]
115pub fn dynamic_dispatch_route(
116 container: std::sync::Arc<FrozenDiContainer>,
117 filters: &'static [&'static dyn BoundaryFilter],
118) -> axum::routing::MethodRouter {
119 let handler = move |req: axum::extract::Request| {
120 let container = container.clone();
121 async move {
122 let (parts, body) = req.into_parts();
123 let table = container.get::<DynamicRouteTable>();
124 let Some(h) = table.lookup(&parts.method, parts.uri.path()) else {
125 return Response::builder()
126 .status(404)
127 .body(axum::body::Body::from("dynamic route not found"))
128 .expect("static 404");
129 };
130 crate::web::boundary::run_entry(
133 parts,
134 body,
135 Default::default(),
136 container,
137 "/_plugins/*",
138 None,
139 filters,
140 &h,
141 )
142 .await
143 }
144 };
145
146 any(handler)
147}