Skip to main content

arcly_http_core/web/
plugin_routes.rs

1//! Mounts plugin-registered routes as axum handlers.
2//!
3//! Lives in the `web` layer (not `core`) so the DI engine and plugin
4//! lifecycle stay HTTP-agnostic. Reuses `boundary::assemble_context` —
5//! plugin routes get exactly the same body cap, trace propagation, and
6//! credential extraction as macro routes, from the same code path.
7
8use axum::routing::{on, MethodFilter, MethodRouter};
9
10use crate::core::engine::FrozenDiContainer;
11use crate::core::plugins::PluginRoute;
12
13#[doc(hidden)]
14pub fn build_plugin_route(
15    container: std::sync::Arc<FrozenDiContainer>,
16    route: &PluginRoute,
17    globals: &'static [&'static dyn crate::web::interceptors::Interceptor],
18    filters: &'static [&'static dyn crate::web::boundary::BoundaryFilter],
19) -> MethodRouter {
20    let filter = MethodFilter::try_from(axum::http::Method::from(route.method))
21        .expect("supported HTTP method");
22    let chain = crate::web::interceptors::compose_chain(globals, route.handler.clone());
23    // Plugin paths are mounted verbatim (no params), so the path doubles as
24    // the metrics route-pattern label with no cardinality risk.
25    let pattern = route.path;
26
27    let handler = move |req: axum::extract::Request| {
28        let chain = chain.clone();
29        let container = container.clone();
30        async move {
31            let (parts, body) = req.into_parts();
32            crate::web::boundary::run_entry(
33                parts,
34                body,
35                Default::default(),
36                container,
37                pattern,
38                None,
39                filters,
40                &chain,
41            )
42            .await
43        }
44    };
45
46    on(filter, handler)
47}