Skip to main content

churust_core/
guard.rs

1//! Route guards: predicates that decide whether a registered route may serve a
2//! request, beyond matching its method and path.
3//!
4//! Several routes may share a `(method, path)` as long as guards distinguish
5//! them. Resolution takes the first whose guards all pass, in registration
6//! order, so an unguarded route registered last acts as the fallback.
7//!
8//! ```
9//! use churust_core::{guard, Call, Churust, TestClient};
10//! # tokio::runtime::Runtime::new().unwrap().block_on(async {
11//! let app = Churust::server()
12//!     .routing(|r| {
13//!         r.get("/", |_c: Call| async { "beta" })
14//!             .guard(guard::header("x-beta", "1"));
15//!         r.get("/", |_c: Call| async { "stable" });
16//!     })
17//!     .build();
18//!
19//! let client = TestClient::new(app);
20//! assert_eq!(client.get("/").header("x-beta", "1").send().await.text(), "beta");
21//! assert_eq!(client.get("/").send().await.text(), "stable");
22//! # });
23//! ```
24
25use crate::call::Call;
26use std::sync::Arc;
27
28/// A predicate over the request. See the [module docs](self).
29pub trait Guard: Send + Sync + 'static {
30    /// Whether this route may serve the request.
31    fn check(&self, call: &Call) -> bool;
32}
33
34/// A boxed guard, as stored by the router.
35pub type BoxGuard = Arc<dyn Guard>;
36
37struct HeaderGuard {
38    name: String,
39    value: String,
40}
41
42impl Guard for HeaderGuard {
43    fn check(&self, call: &Call) -> bool {
44        call.header(&self.name) == Some(self.value.as_str())
45    }
46}
47
48/// Passes when the request carries `name` with exactly `value`.
49pub fn header(name: impl Into<String>, value: impl Into<String>) -> BoxGuard {
50    Arc::new(HeaderGuard {
51        name: name.into(),
52        value: value.into(),
53    })
54}
55
56struct HostGuard(String);
57
58impl Guard for HostGuard {
59    fn check(&self, call: &Call) -> bool {
60        // `Call::host` reads the `Host` header *and* the URI authority. HTTP/2
61        // dropped the header in favour of `:authority`, so reading only the
62        // header meant every host-guarded route silently stopped matching over
63        // h2 — which ALPN prefers — and traffic fell through to whatever
64        // unguarded sibling was registered as the fallback. The port is not
65        // part of the identity the caller asked about, and `host` strips it.
66        call.host().is_some_and(|h| h.eq_ignore_ascii_case(&self.0))
67    }
68}
69
70/// Passes when the request's host matches, ignoring any port and case.
71///
72/// Reads the `Host` header over HTTP/1.1 and the `:authority` pseudo-header
73/// over HTTP/2, so a vhost-scoped route matches on both protocols. See
74/// [`Call::host`](crate::Call::host).
75pub fn host(name: impl Into<String>) -> BoxGuard {
76    Arc::new(HostGuard(name.into()))
77}
78
79struct FnGuard<F>(F);
80
81impl<F: Fn(&Call) -> bool + Send + Sync + 'static> Guard for FnGuard<F> {
82    fn check(&self, call: &Call) -> bool {
83        (self.0)(call)
84    }
85}
86
87/// Passes when the closure returns `true`.
88pub fn fn_guard<F: Fn(&Call) -> bool + Send + Sync + 'static>(f: F) -> BoxGuard {
89    Arc::new(FnGuard(f))
90}
91
92struct All(Vec<BoxGuard>);
93
94impl Guard for All {
95    fn check(&self, call: &Call) -> bool {
96        self.0.iter().all(|g| g.check(call))
97    }
98}
99
100/// Passes only when every inner guard passes. An empty list passes.
101pub fn all(guards: Vec<BoxGuard>) -> BoxGuard {
102    Arc::new(All(guards))
103}
104
105struct Any(Vec<BoxGuard>);
106
107impl Guard for Any {
108    fn check(&self, call: &Call) -> bool {
109        self.0.iter().any(|g| g.check(call))
110    }
111}
112
113/// Passes when at least one inner guard passes. An empty list never passes.
114pub fn any(guards: Vec<BoxGuard>) -> BoxGuard {
115    Arc::new(Any(guards))
116}
117
118struct Not(BoxGuard);
119
120impl Guard for Not {
121    fn check(&self, call: &Call) -> bool {
122        !self.0.check(call)
123    }
124}
125
126/// Inverts a guard.
127pub fn not(g: BoxGuard) -> BoxGuard {
128    Arc::new(Not(g))
129}