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}