Skip to main content

camel_component_http/
registry.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use camel_component_api::CamelError;
6use tokio::sync::{RwLock, mpsc};
7use tokio_util::sync::CancellationToken;
8use tower_http::services::ServeDir;
9
10use crate::RequestEnvelope;
11use crate::rest_match::RestEndpoint;
12
13/// Discriminates between a plain static file mount and
14/// a mount that also performs SPA‑style fallback to index.html.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum MountMode {
17    Static,
18    Spa,
19}
20
21#[allow(dead_code)]
22pub struct StaticMount {
23    pub mount_path: String,
24    pub mode: MountMode,
25    pub dir: PathBuf,
26    pub cache_control: String,
27    pub error_pages: HashMap<u16, PathBuf>,
28    pub serve_dir: ServeDir,
29}
30
31impl std::fmt::Debug for StaticMount {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.debug_struct("StaticMount")
34            .field("mount_path", &self.mount_path)
35            .field("mode", &self.mode)
36            .field("dir", &self.dir)
37            .field("cache_control", &self.cache_control)
38            .field("error_pages", &self.error_pages)
39            .finish_non_exhaustive()
40    }
41}
42
43pub(crate) struct HttpRouteRegistryInner {
44    /// Legacy path-keyed API route registry. Used for `http:` routes
45    /// registered without an `httpMethod=` URI param. Paths must be unique.
46    pub api_routes: HashMap<String, mpsc::Sender<RequestEnvelope>>,
47    /// Method-aware REST endpoint registry. Populated by REST-lowered
48    /// `http:` routes (those whose URI carries `httpMethod=...`). Allows
49    /// GET and POST on the same path to coexist, and supports path
50    /// templates like `/users/{id}`. Per plan expert guidance E1.
51    pub rest_endpoints: Vec<RestEndpoint<mpsc::Sender<RequestEnvelope>>>,
52    pub mounts: Vec<StaticMount>,
53}
54
55impl std::fmt::Debug for HttpRouteRegistryInner {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        f.debug_struct("HttpRouteRegistryInner")
58            .field("api_routes", &self.api_routes.keys())
59            .field("rest_endpoints", &self.rest_endpoints.len())
60            .field("mounts", &self.mounts.len())
61            .finish()
62    }
63}
64
65#[derive(Clone)]
66pub struct HttpRouteRegistry {
67    pub(crate) inner: Arc<RwLock<HttpRouteRegistryInner>>,
68    /// Cancelled by `monitor_axum_task` when the shared Axum server task
69    /// backing this registry exits unexpectedly (panic or abort). Every
70    /// `HttpConsumer` hosted on that server selects on this token in its
71    /// `start()` loop; on cancellation the consumer returns `Err`, which
72    /// camel-core's consumer watcher converts into a per-route
73    /// `CrashNotification` → `FailRoute` → supervision backoff restart
74    /// (ADR-0007 route-supervised contract — shared transports must fail
75    /// their hosted routes like per-route transports do).
76    ///
77    /// Registries constructed directly via [`HttpRouteRegistry::new`]
78    /// (tests, embedding) carry a token that is never cancelled — their
79    /// consumers keep the pre-fix semantics.
80    pub(crate) server_exited: CancellationToken,
81}
82
83impl std::fmt::Debug for HttpRouteRegistry {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        f.debug_struct("HttpRouteRegistry").finish_non_exhaustive()
86    }
87}
88
89impl Default for HttpRouteRegistry {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95impl HttpRouteRegistry {
96    pub fn new() -> Self {
97        Self::new_with_server_exited(CancellationToken::new())
98    }
99
100    /// Crate-private constructor used by `spawn_entry`: binds the registry
101    /// to the death signal of the shared server that serves it.
102    pub(crate) fn new_with_server_exited(server_exited: CancellationToken) -> Self {
103        Self {
104            inner: Arc::new(RwLock::new(HttpRouteRegistryInner {
105                api_routes: HashMap::new(),
106                rest_endpoints: Vec::new(),
107                mounts: Vec::new(),
108            })),
109            server_exited,
110        }
111    }
112
113    pub async fn register_api_route(
114        &self,
115        path: String,
116        sender: mpsc::Sender<RequestEnvelope>,
117    ) -> Result<(), CamelError> {
118        let mut inner = self.inner.write().await;
119        if inner.api_routes.contains_key(&path) {
120            return Err(CamelError::Config(format!(
121                "duplicate HTTP consumer path '{path}' on this listener"
122            )));
123        }
124        inner.api_routes.insert(path, sender);
125        Ok(())
126    }
127
128    pub async fn unregister_api_route(&self, path: &str) {
129        let mut inner = self.inner.write().await;
130        inner.api_routes.remove(path);
131    }
132
133    /// Register a method-aware REST endpoint. Two endpoints on the same
134    /// path with different methods coexist; duplicate `(method, segments)`
135    /// keys are rejected without replacing the original sender.
136    pub async fn register_rest_endpoint(
137        &self,
138        method: String,
139        segments: Vec<crate::rest_match::PathSegment>,
140        sender: mpsc::Sender<RequestEnvelope>,
141    ) -> Result<(), CamelError> {
142        let mut inner = self.inner.write().await;
143        if inner
144            .rest_endpoints
145            .iter()
146            .any(|ep| ep.method == method && ep.segments == segments)
147        {
148            let template = segments
149                .iter()
150                .map(|segment| match segment {
151                    crate::rest_match::PathSegment::Literal(value) => value.clone(),
152                    crate::rest_match::PathSegment::Param(name) => format!("{{{name}}}"),
153                })
154                .collect::<Vec<_>>()
155                .join("/");
156            return Err(CamelError::Config(format!(
157                "duplicate HTTP consumer endpoint '{method} /{template}' on this listener"
158            )));
159        }
160        inner.rest_endpoints.push(RestEndpoint {
161            method,
162            segments,
163            payload: sender,
164        });
165        Ok(())
166    }
167
168    /// Remove the REST endpoint matching `(method, path_template)`.
169    ///
170    /// Only the endpoint with the SAME method AND segments is removed —
171    /// other HTTP methods sharing the path template are preserved. Stopping
172    /// the `GET /users` consumer must not tear down the live `POST /users`
173    /// endpoint (the core REST multi-verb use case). Fixes review C1.
174    pub async fn unregister_rest_endpoint(&self, method: &str, path_template: &str) {
175        let target_segments = crate::rest_match::parse_path_template(path_template);
176        let mut inner = self.inner.write().await;
177        inner.rest_endpoints.retain(|ep| {
178            // Match BOTH method and segments: a sibling verb on the same
179            // path template stays registered.
180            !(ep.method == method && ep.segments == target_segments)
181        });
182    }
183
184    /// Register a static mount. Duplicate detection is by `mount_path`
185    /// only — every mount on a given port must have a unique prefix.
186    ///
187    /// A single SPA mount is still the convention, but it is no longer
188    /// enforced structurally; the dispatch loop treats all mounts
189    /// uniformly (sorted by longest prefix) and uses `mode` to decide
190    /// whether to attempt SPA-fallback after ServeDir fails.
191    #[allow(dead_code)]
192    pub async fn register_static_mount(&self, mount: StaticMount) -> Result<(), CamelError> {
193        let mut inner = self.inner.write().await;
194        if inner
195            .mounts
196            .iter()
197            .any(|m| m.mount_path == mount.mount_path)
198        {
199            return Err(CamelError::Config(format!(
200                "duplicate static mount path '{}' on this port",
201                mount.mount_path
202            )));
203        }
204        inner.mounts.push(mount);
205        Ok(())
206    }
207
208    /// Unregister a static mount by its unique `mount_path`.
209    #[allow(dead_code)]
210    pub async fn unregister_static_mount(&self, mount_path: &str) {
211        let mut inner = self.inner.write().await;
212        inner.mounts.retain(|m| m.mount_path != mount_path);
213    }
214}