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 tower_http::services::ServeDir;
8
9use crate::RequestEnvelope;
10use crate::rest_match::RestEndpoint;
11
12/// Discriminates between a plain static file mount and
13/// a mount that also performs SPA‑style fallback to index.html.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum MountMode {
16    Static,
17    Spa,
18}
19
20#[allow(dead_code)]
21pub struct StaticMount {
22    pub mount_path: String,
23    pub mode: MountMode,
24    pub dir: PathBuf,
25    pub cache_control: String,
26    pub error_pages: HashMap<u16, PathBuf>,
27    pub serve_dir: ServeDir,
28}
29
30impl std::fmt::Debug for StaticMount {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        f.debug_struct("StaticMount")
33            .field("mount_path", &self.mount_path)
34            .field("mode", &self.mode)
35            .field("dir", &self.dir)
36            .field("cache_control", &self.cache_control)
37            .field("error_pages", &self.error_pages)
38            .finish_non_exhaustive()
39    }
40}
41
42pub(crate) struct HttpRouteRegistryInner {
43    /// Legacy path-keyed API route registry. Used for `http:` routes
44    /// registered without an `httpMethod=` URI param. Multiple routes on
45    /// the same path overwrite each other here.
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}
69
70impl std::fmt::Debug for HttpRouteRegistry {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.debug_struct("HttpRouteRegistry").finish_non_exhaustive()
73    }
74}
75
76impl Default for HttpRouteRegistry {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl HttpRouteRegistry {
83    pub fn new() -> Self {
84        Self {
85            inner: Arc::new(RwLock::new(HttpRouteRegistryInner {
86                api_routes: HashMap::new(),
87                rest_endpoints: Vec::new(),
88                mounts: Vec::new(),
89            })),
90        }
91    }
92
93    pub async fn register_api_route(&self, path: String, sender: mpsc::Sender<RequestEnvelope>) {
94        let mut inner = self.inner.write().await;
95        inner.api_routes.insert(path, sender);
96    }
97
98    pub async fn unregister_api_route(&self, path: &str) {
99        let mut inner = self.inner.write().await;
100        inner.api_routes.remove(path);
101    }
102
103    /// Register a method-aware REST endpoint. Two endpoints on the same
104    /// path with different methods coexist; two endpoints with the same
105    /// `(method, path)` overwrite (last write wins), matching the
106    /// semantics expected for re-registration of the same logical route.
107    pub async fn register_rest_endpoint(
108        &self,
109        method: String,
110        segments: Vec<crate::rest_match::PathSegment>,
111        sender: mpsc::Sender<RequestEnvelope>,
112    ) {
113        let mut inner = self.inner.write().await;
114        // Drop any prior endpoint with the same (method, segments)
115        // signature so the new sender wins.
116        inner
117            .rest_endpoints
118            .retain(|ep| !(ep.method == method && ep.segments == segments));
119        inner.rest_endpoints.push(RestEndpoint {
120            method,
121            segments,
122            payload: sender,
123        });
124    }
125
126    /// Remove the REST endpoint matching `(method, path_template)`.
127    ///
128    /// Only the endpoint with the SAME method AND segments is removed —
129    /// other HTTP methods sharing the path template are preserved. Stopping
130    /// the `GET /users` consumer must not tear down the live `POST /users`
131    /// endpoint (the core REST multi-verb use case). Fixes review C1.
132    pub async fn unregister_rest_endpoint(&self, method: &str, path_template: &str) {
133        let target_segments = crate::rest_match::parse_path_template(path_template);
134        let mut inner = self.inner.write().await;
135        inner.rest_endpoints.retain(|ep| {
136            // Match BOTH method and segments: a sibling verb on the same
137            // path template stays registered.
138            !(ep.method == method && ep.segments == target_segments)
139        });
140    }
141
142    /// Register a static mount. Duplicate detection is by `mount_path`
143    /// only — every mount on a given port must have a unique prefix.
144    ///
145    /// A single SPA mount is still the convention, but it is no longer
146    /// enforced structurally; the dispatch loop treats all mounts
147    /// uniformly (sorted by longest prefix) and uses `mode` to decide
148    /// whether to attempt SPA-fallback after ServeDir fails.
149    #[allow(dead_code)]
150    pub async fn register_static_mount(&self, mount: StaticMount) -> Result<(), CamelError> {
151        let mut inner = self.inner.write().await;
152        if inner
153            .mounts
154            .iter()
155            .any(|m| m.mount_path == mount.mount_path)
156        {
157            return Err(CamelError::Config(format!(
158                "duplicate static mount path '{}' on this port",
159                mount.mount_path
160            )));
161        }
162        inner.mounts.push(mount);
163        Ok(())
164    }
165
166    /// Unregister a static mount by its unique `mount_path`.
167    #[allow(dead_code)]
168    pub async fn unregister_static_mount(&self, mount_path: &str) {
169        let mut inner = self.inner.write().await;
170        inner.mounts.retain(|m| m.mount_path != mount_path);
171    }
172}