1use bytes::Bytes;
2use std::collections::BTreeSet;
3use std::sync::Arc;
4
5use crate::pipe::handler::PipeHandle;
6use crate::pipe::path_pattern::PathPattern;
7use crate::pipe::request::Request;
8
9#[derive(Clone)]
13pub struct Mount<Handle = PipeHandle> {
14 pub path: PathPattern,
15 pub pipe: Handle,
16 pub methods: MethodFilter,
17 pub host: HostFilter,
18 pub label: Arc<[u8]>,
19 pub pipe_name: Arc<str>,
25}
26
27#[derive(Debug, Clone, Default)]
31pub struct HostFilter {
32 allowed: Option<Vec<String>>,
33}
34
35impl HostFilter {
36 #[must_use]
37 pub fn any() -> Self {
38 Self { allowed: None }
39 }
40
41 #[must_use]
42 pub fn only<I: IntoIterator<Item = String>>(hosts: I) -> Self {
43 let collected: Vec<String> = hosts
44 .into_iter()
45 .map(|host| host.to_ascii_lowercase())
46 .collect();
47 if collected.is_empty() {
48 Self::any()
49 } else {
50 Self {
51 allowed: Some(collected),
52 }
53 }
54 }
55
56 #[must_use]
57 pub fn matches(&self, host_header: Option<&str>) -> bool {
58 let Some(allowed) = self.allowed.as_ref() else {
59 return true;
60 };
61 let Some(host) = host_header else {
62 return false;
63 };
64 let host_no_port = host
66 .split_once(':')
67 .map_or(host, |(name, _)| name)
68 .to_ascii_lowercase();
69 allowed.iter().any(|candidate| candidate == &host_no_port)
70 }
71}
72
73#[derive(Debug, Clone)]
74pub struct MethodFilter {
75 allowed: Option<BTreeSet<String>>,
76}
77
78impl MethodFilter {
79 #[must_use]
80 pub fn any() -> Self {
81 Self { allowed: None }
82 }
83
84 #[must_use]
85 pub fn only<I: IntoIterator<Item = String>>(methods: I) -> Self {
86 Self {
87 allowed: Some(
88 methods
89 .into_iter()
90 .map(|method| method.to_uppercase())
91 .collect(),
92 ),
93 }
94 }
95
96 #[must_use]
97 pub fn matches(&self, method: &[u8]) -> bool {
98 match &self.allowed {
99 Some(set) => set
100 .iter()
101 .any(|allowed| allowed.as_bytes().eq_ignore_ascii_case(method)),
102 None => true,
103 }
104 }
105}
106
107impl<Handle> std::fmt::Debug for Mount<Handle> {
111 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 formatter
113 .debug_struct("Mount")
114 .field("path", &self.path.raw())
115 .field("pipe", &self.pipe_name)
116 .field("methods", &self.methods)
117 .field("label", &self.label)
118 .finish()
119 }
120}
121
122impl<Handle> std::fmt::Debug for Router<Handle> {
123 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 formatter
125 .debug_struct("Router")
126 .field("mounts", &self.mounts)
127 .finish()
128 }
129}
130
131impl<Handle> Mount<Handle> {
132 #[must_use]
133 pub fn new(pattern: &str, pipe: Handle) -> Self {
134 Self {
135 path: PathPattern::parse(pattern),
136 pipe,
137 methods: MethodFilter::any(),
138 host: HostFilter::any(),
139 label: Arc::from(pattern.as_bytes()),
140 pipe_name: Arc::from("anonymous"),
141 }
142 }
143
144 #[must_use]
145 pub fn with_methods(mut self, methods: MethodFilter) -> Self {
146 self.methods = methods;
147 self
148 }
149
150 #[must_use]
151 pub fn with_host(mut self, host: HostFilter) -> Self {
152 self.host = host;
153 self
154 }
155
156 #[must_use]
160 pub fn named(mut self, name: impl Into<Arc<str>>) -> Self {
161 self.pipe_name = name.into();
162 self
163 }
164
165 #[must_use]
166 pub fn matches(&self, request: &Request<Bytes>) -> bool {
167 self.matches_with_params(request).is_some()
168 }
169
170 #[must_use]
173 pub fn matches_with_params(
174 &self,
175 request: &Request<Bytes>,
176 ) -> Option<std::collections::BTreeMap<String, String>> {
177 if !self.methods.matches(request.method.as_bytes()) {
178 return None;
179 }
180 let host_header = request
181 .metadata
182 .iter()
183 .find(|(name, _)| name.eq_ignore_ascii_case(b"host"))
184 .and_then(|(_, value)| std::str::from_utf8(value).ok());
185 if !self.host.matches(host_header) {
186 return None;
187 }
188 let path_view = std::str::from_utf8(&request.path).unwrap_or("");
189 self.path.matches(path_view)
190 }
191}
192
193#[derive(Clone)]
194pub struct Router<Handle = PipeHandle> {
195 mounts: Vec<Mount<Handle>>,
196}
197
198impl<Handle> Default for Router<Handle> {
199 fn default() -> Self {
200 Self { mounts: Vec::new() }
201 }
202}
203
204impl<Handle> Router<Handle> {
205 #[must_use]
206 pub fn new() -> Self {
207 Self::default()
208 }
209
210 #[must_use]
211 pub fn with_capacity(capacity: usize) -> Self {
212 Self {
213 mounts: Vec::with_capacity(capacity),
214 }
215 }
216
217 pub fn add(&mut self, mount: Mount<Handle>) {
218 self.mounts.push(mount);
219 }
220
221 pub fn remove(&mut self, pattern: &str) -> usize {
222 let before = self.mounts.len();
223 let needle = pattern.as_bytes();
224 self.mounts.retain(|mount| mount.label.as_ref() != needle);
225 before - self.mounts.len()
226 }
227
228 pub fn replace(&mut self, pattern: &str, mount: Mount<Handle>) -> bool {
229 let removed = self.remove(pattern);
230 self.mounts.push(mount);
231 removed > 0
232 }
233
234 #[must_use]
235 pub fn route(&self, request: &Request<Bytes>) -> Option<&Mount<Handle>> {
236 self.mounts.iter().find(|mount| mount.matches(request))
237 }
238
239 pub fn route_with_params(
241 &self,
242 request: &Request<Bytes>,
243 ) -> Option<(&Mount<Handle>, std::collections::BTreeMap<String, String>)> {
244 for mount in &self.mounts {
245 if let Some(params) = mount.matches_with_params(request) {
246 return Some((mount, params));
247 }
248 }
249 None
250 }
251
252 #[must_use]
253 pub fn mounts(&self) -> &[Mount<Handle>] {
254 &self.mounts
255 }
256}
257
258use core::future::Future;
259
260pub struct RoutingPipe<Handle = PipeHandle> {
265 router: Router<Handle>,
266 label: String,
267 fallback: Option<Handle>,
268}
269
270impl<Handle> RoutingPipe<Handle> {
271 #[must_use]
272 pub fn new(label: impl Into<String>) -> Self {
273 Self {
274 router: Router::new(),
275 label: label.into(),
276 fallback: None,
277 }
278 }
279
280 #[must_use]
281 pub fn route(mut self, pattern: &str, pipe: Handle) -> Self {
282 self.router.add(Mount::new(pattern, pipe));
283 self
284 }
285
286 #[must_use]
287 pub fn route_with_methods(
288 mut self,
289 pattern: &str,
290 pipe: Handle,
291 methods: MethodFilter,
292 ) -> Self {
293 self.router
294 .add(Mount::new(pattern, pipe).with_methods(methods));
295 self
296 }
297
298 #[must_use]
299 pub fn fallback(mut self, pipe: Handle) -> Self {
300 self.fallback = Some(pipe);
301 self
302 }
303
304 #[must_use]
307 pub fn label(&self) -> &str {
308 &self.label
309 }
310}
311
312impl<Handle> crate::pipe::primitives::SendPipe for RoutingPipe<Handle>
313where
314 Handle: crate::pipe::handler::Handler + Clone + Send + Sync + 'static,
315{
316 type In = Request<Bytes>;
317 type Out = crate::pipe::request::Response<Bytes>;
318 type Err = crate::pipe::ProximaError;
319
320 fn call(
321 &self,
322 request: Request<Bytes>,
323 ) -> impl Future<
324 Output = Result<crate::pipe::request::Response<Bytes>, crate::pipe::ProximaError>,
325 > + Send {
326 let routed = self.router.route(&request).map(|mount| mount.pipe.clone());
327 let fallback = self.fallback.clone();
328 async move {
329 if let Some(pipe) = routed {
330 return crate::pipe::primitives::SendPipe::call(&pipe, request).await;
331 }
332 if let Some(pipe) = fallback {
333 return crate::pipe::primitives::SendPipe::call(&pipe, request).await;
334 }
335 Ok(crate::pipe::request::Response::not_found().with_body("no route matched"))
336 }
337 }
338}
339
340impl crate::pipe::primitives::Pipe for RoutingPipe<crate::pipe::handler::ThreadLocalPipeHandle> {
341 type In = Request<Bytes>;
342 type Out = crate::pipe::request::Response<Bytes>;
343 type Err = crate::pipe::ProximaError;
344
345 fn call(
346 &self,
347 request: Request<Bytes>,
348 ) -> impl Future<Output = Result<crate::pipe::request::Response<Bytes>, crate::pipe::ProximaError>>
349 {
350 let routed = self.router.route(&request).map(|mount| mount.pipe.clone());
351 let fallback = self.fallback.clone();
352 async move {
353 if let Some(pipe) = routed {
354 return crate::pipe::primitives::Pipe::call(&pipe, request).await;
355 }
356 if let Some(pipe) = fallback {
357 return crate::pipe::primitives::Pipe::call(&pipe, request).await;
358 }
359 Ok(crate::pipe::request::Response::not_found().with_body("no route matched"))
360 }
361 }
362}
363
364#[cfg(all(test, not(loom)))]
369#[allow(clippy::unwrap_used, clippy::expect_used)]
370mod routing_pipe_tests {
371 use super::*;
372 use crate::pipe::SendPipe;
373 use crate::pipe::handler::into_handle;
374 use crate::pipe::request::Response;
375
376 struct Static(&'static str);
377
378 impl SendPipe for Static {
379 type In = Request<Bytes>;
380 type Out = Response<Bytes>;
381 type Err = crate::pipe::ProximaError;
382
383 fn call(
384 &self,
385 _request: Request<Bytes>,
386 ) -> impl Future<Output = Result<Response<Bytes>, crate::pipe::ProximaError>> + Send
387 {
388 let body = self.0;
389 async move { Ok(Response::ok(bytes::Bytes::from_static(body.as_bytes()))) }
390 }
391 }
392
393 fn build_request(method: &str, path: &str) -> Request<Bytes> {
394 Request::builder()
395 .method(method)
396 .path(path)
397 .build()
398 .expect("builder")
399 }
400
401 #[proxima::test]
402 async fn route_dispatches_to_matching_pipe() {
403 let svc = RoutingPipe::new("api")
404 .route("/users/{id}", into_handle(Static("users")))
405 .route("/posts/{id}", into_handle(Static("posts")));
406
407 let response = SendPipe::call(&svc, build_request("GET", "/users/42"))
408 .await
409 .expect("call");
410 let body = response.collect_body().await.expect("collect");
411 assert_eq!(&body[..], b"users");
412 }
413
414 #[proxima::test]
415 async fn unmatched_path_falls_through_to_fallback() {
416 let svc = RoutingPipe::new("api")
417 .route("/known", into_handle(Static("known")))
418 .fallback(into_handle(Static("fallback")));
419 let response = SendPipe::call(&svc, build_request("GET", "/anything-else"))
420 .await
421 .expect("call");
422 let body = response.collect_body().await.expect("collect");
423 assert_eq!(&body[..], b"fallback");
424 }
425
426 #[proxima::test]
427 async fn unmatched_path_without_fallback_returns_404() {
428 let svc = RoutingPipe::new("api").route("/known", into_handle(Static("known")));
429 let response = SendPipe::call(&svc, build_request("GET", "/missing"))
430 .await
431 .expect("call");
432 assert_eq!(response.status, 404);
433 }
434
435 #[proxima::test]
436 async fn method_filter_excludes_unmatched_method() {
437 let svc = RoutingPipe::new("api").route_with_methods(
438 "/users",
439 into_handle(Static("users")),
440 MethodFilter::only(["GET".into()]),
441 );
442 let response = SendPipe::call(&svc, build_request("DELETE", "/users"))
443 .await
444 .expect("call");
445 assert_eq!(response.status, 404);
446 }
447}
448
449#[cfg(all(test, not(loom)))]
454#[allow(clippy::unwrap_used, clippy::expect_used)]
455mod tests {
456 use super::*;
457 use crate::pipe::ProximaError;
458 use crate::pipe::SendPipe;
459 use crate::pipe::handler::into_handle;
460 use crate::pipe::request::Response;
461 use std::future::Future;
462
463 struct StubPipe(&'static str);
464
465 impl SendPipe for StubPipe {
466 type In = Request<Bytes>;
467 type Out = Response<Bytes>;
468 type Err = ProximaError;
469
470 fn call(
471 &self,
472 _request: Request<Bytes>,
473 ) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send {
474 let body = self.0;
475 async move { Ok(Response::ok(body)) }
476 }
477 }
478
479 fn build_request(method: &str, path: &str) -> Request<Bytes> {
480 Request::builder()
481 .method(method)
482 .path(path)
483 .build()
484 .expect("builder should succeed")
485 }
486
487 #[test]
488 fn router_returns_first_matching_mount() {
489 let mut router = Router::new();
490 router.add(Mount::new("/users/{id}", into_handle(StubPipe("users"))).named("users"));
491 router.add(Mount::new("/posts/{id}", into_handle(StubPipe("posts"))).named("posts"));
492 let request = build_request("GET", "/users/42");
493 let mount = router.route(&request).expect("should match");
494 assert_eq!(mount.pipe_name.as_ref(), "users");
495 }
496
497 #[test]
498 fn method_filter_excludes_unlisted() {
499 let mut router = Router::new();
500 let mount = Mount::new("/users", into_handle(StubPipe("users")))
501 .with_methods(MethodFilter::only(["GET".into(), "POST".into()]));
502 router.add(mount);
503 assert!(router.route(&build_request("GET", "/users")).is_some());
504 assert!(router.route(&build_request("POST", "/users")).is_some());
505 assert!(router.route(&build_request("DELETE", "/users")).is_none());
506 }
507
508 #[test]
509 fn no_match_returns_none() {
510 let router: Router = Router::new();
511 assert!(router.route(&build_request("GET", "/missing")).is_none());
512 }
513
514 #[test]
515 fn unnamed_mount_defaults_to_anonymous() {
516 let mount = Mount::new("/x", into_handle(StubPipe("x")));
517 assert_eq!(mount.pipe_name.as_ref(), "anonymous");
518 }
519}