refactor(introspection): add GuardDetail enum and remove downcast_ref usage

- Added `GuardDetail` enum to encapsulate various introspection details of a guard.
- Refactored `HttpMethodsExtractor` implementation to use `GuardDetail` instead of `downcast_ref`.
This commit is contained in:
Guillermo Céspedes Tabárez 2025-03-05 02:34:39 -03:00
parent ee7621594c
commit ae08dcf6dc
1 changed files with 102 additions and 75 deletions

View File

@ -11,7 +11,7 @@
//! or handler. This interface is defined by the [`Guard`] trait. //! or handler. This interface is defined by the [`Guard`] trait.
//! //!
//! Commonly-used guards are provided in this module as well as a way of creating a guard from a //! Commonly-used guards are provided in this module as well as a way of creating a guard from a
//! closure ([`fn_guard`]). The [`Not`], [`Any()`], and [`All`] guards are noteworthy, as they can be //! closure ([`fn_guard`]). The [`Not()`], [`Any()`], and [`All()`] guards are noteworthy, as they can be
//! used to compose other guards in a more flexible and semantic way than calling `.guard(...)` on //! used to compose other guards in a more flexible and semantic way than calling `.guard(...)` on
//! services multiple times (which might have different combining behavior than you want). //! services multiple times (which might have different combining behavior than you want).
//! //!
@ -50,7 +50,6 @@
//! [`Route`]: crate::Route::guard() //! [`Route`]: crate::Route::guard()
use std::{ use std::{
any::Any,
cell::{Ref, RefMut}, cell::{Ref, RefMut},
rc::Rc, rc::Rc,
}; };
@ -67,6 +66,17 @@ pub use self::{
host::{Host, HostGuard}, host::{Host, HostGuard},
}; };
/// Enum to encapsulate various introspection details of a Guard.
#[derive(Debug, Clone)]
pub enum GuardDetail {
/// Detail associated with HTTP methods.
HttpMethods(Vec<String>),
/// Detail associated with headers (header, value).
Headers(Vec<(String, String)>),
/// Generic detail.
Generic(String),
}
/// Provides access to request parts that are useful during routing. /// Provides access to request parts that are useful during routing.
#[derive(Debug)] #[derive(Debug)]
pub struct GuardContext<'a> { pub struct GuardContext<'a> {
@ -122,15 +132,31 @@ impl<'a> GuardContext<'a> {
/// Interface for routing guards. /// Interface for routing guards.
/// ///
/// See [module level documentation](self) for more. /// See [module level documentation](self) for more.
pub trait Guard: AsAny { pub trait Guard {
/// Returns true if predicate condition is met for a given request. /// Returns true if predicate condition is met for a given request.
fn check(&self, ctx: &GuardContext<'_>) -> bool; fn check(&self, ctx: &GuardContext<'_>) -> bool;
/// Returns a nominal representation of the guard.
fn name(&self) -> String {
std::any::type_name::<Self>().to_string()
}
/// Returns detailed introspection information.
fn details(&self) -> Option<Vec<GuardDetail>> {
None
}
} }
impl Guard for Rc<dyn Guard> { impl Guard for Rc<dyn Guard> {
fn check(&self, ctx: &GuardContext<'_>) -> bool { fn check(&self, ctx: &GuardContext<'_>) -> bool {
(**self).check(ctx) (**self).check(ctx)
} }
fn name(&self) -> String {
(**self).name()
}
fn details(&self) -> Option<Vec<GuardDetail>> {
(**self).details()
}
} }
/// Creates a guard using the given function. /// Creates a guard using the given function.
@ -147,7 +173,7 @@ impl Guard for Rc<dyn Guard> {
/// ``` /// ```
pub fn fn_guard<F>(f: F) -> impl Guard pub fn fn_guard<F>(f: F) -> impl Guard
where where
F: Fn(&GuardContext<'_>) -> bool + 'static, F: Fn(&GuardContext<'_>) -> bool,
{ {
FnGuard(f) FnGuard(f)
} }
@ -156,7 +182,7 @@ struct FnGuard<F: Fn(&GuardContext<'_>) -> bool>(F);
impl<F> Guard for FnGuard<F> impl<F> Guard for FnGuard<F>
where where
F: Fn(&GuardContext<'_>) -> bool + 'static, F: Fn(&GuardContext<'_>) -> bool,
{ {
fn check(&self, ctx: &GuardContext<'_>) -> bool { fn check(&self, ctx: &GuardContext<'_>) -> bool {
(self.0)(ctx) (self.0)(ctx)
@ -165,7 +191,7 @@ where
impl<F> Guard for F impl<F> Guard for F
where where
F: for<'a> Fn(&'a GuardContext<'a>) -> bool + 'static, F: Fn(&GuardContext<'_>) -> bool,
{ {
fn check(&self, ctx: &GuardContext<'_>) -> bool { fn check(&self, ctx: &GuardContext<'_>) -> bool {
(self)(ctx) (self)(ctx)
@ -220,6 +246,24 @@ impl Guard for AnyGuard {
false false
} }
fn name(&self) -> String {
format!(
"AnyGuard({})",
self.guards
.iter()
.map(|g| g.name())
.collect::<Vec<_>>()
.join(", ")
)
}
fn details(&self) -> Option<Vec<GuardDetail>> {
Some(
self.guards
.iter()
.flat_map(|g| g.details().unwrap_or_default())
.collect(),
)
}
} }
/// Creates a guard that matches if all added guards match. /// Creates a guard that matches if all added guards match.
@ -248,7 +292,7 @@ pub fn All<F: Guard + 'static>(guard: F) -> AllGuard {
/// ///
/// That is, **all** contained guard needs to match in order for the aggregate guard to match. /// That is, **all** contained guard needs to match in order for the aggregate guard to match.
/// ///
/// Construct an `AllGuard` using [`All`]. /// Construct an `AllGuard` using [`All()`].
pub struct AllGuard { pub struct AllGuard {
guards: Vec<Box<dyn Guard>>, guards: Vec<Box<dyn Guard>>,
} }
@ -272,6 +316,24 @@ impl Guard for AllGuard {
true true
} }
fn name(&self) -> String {
format!(
"AllGuard({})",
self.guards
.iter()
.map(|g| g.name())
.collect::<Vec<_>>()
.join(", ")
)
}
fn details(&self) -> Option<Vec<GuardDetail>> {
Some(
self.guards
.iter()
.flat_map(|g| g.details().unwrap_or_default())
.collect(),
)
}
} }
/// Wraps a guard and inverts the outcome of its `Guard` implementation. /// Wraps a guard and inverts the outcome of its `Guard` implementation.
@ -285,13 +347,19 @@ impl Guard for AllGuard {
/// .guard(guard::Not(guard::Get())) /// .guard(guard::Not(guard::Get()))
/// .to(|| HttpResponse::Ok()); /// .to(|| HttpResponse::Ok());
/// ``` /// ```
pub struct Not<G: 'static>(pub G); pub struct Not<G>(pub G);
impl<G: Guard> Guard for Not<G> { impl<G: Guard> Guard for Not<G> {
#[inline] #[inline]
fn check(&self, ctx: &GuardContext<'_>) -> bool { fn check(&self, ctx: &GuardContext<'_>) -> bool {
!self.0.check(ctx) !self.0.check(ctx)
} }
fn name(&self) -> String {
format!("Not({})", self.0.name())
}
fn details(&self) -> Option<Vec<GuardDetail>> {
self.0.details()
}
} }
/// Creates a guard that matches a specified HTTP method. /// Creates a guard that matches a specified HTTP method.
@ -321,6 +389,12 @@ impl Guard for MethodGuard {
ctx.head().method == self.0 ctx.head().method == self.0
} }
fn name(&self) -> String {
self.0.to_string()
}
fn details(&self) -> Option<Vec<GuardDetail>> {
Some(vec![GuardDetail::HttpMethods(vec![self.0.to_string()])])
}
} }
#[cfg(feature = "resources-introspection")] #[cfg(feature = "resources-introspection")]
@ -331,70 +405,24 @@ pub trait HttpMethodsExtractor {
#[cfg(feature = "resources-introspection")] #[cfg(feature = "resources-introspection")]
impl HttpMethodsExtractor for dyn Guard { impl HttpMethodsExtractor for dyn Guard {
fn extract_http_methods(&self) -> Vec<String> { fn extract_http_methods(&self) -> Vec<String> {
if let Some(method_guard) = self.as_any().downcast_ref::<MethodGuard>() {
vec![method_guard.0.to_string()]
} else if let Some(any_guard) = self.as_any().downcast_ref::<AnyGuard>() {
any_guard
.guards
.iter()
.flat_map(|g| g.extract_http_methods())
.collect()
} else if let Some(all_guard) = self.as_any().downcast_ref::<AllGuard>() {
all_guard
.guards
.iter()
.flat_map(|g| g.extract_http_methods())
.collect()
} else {
vec!["UNKNOWN".to_string()]
}
}
}
#[cfg(feature = "resources-introspection")]
impl std::fmt::Display for MethodGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}]", self.0)
}
}
#[cfg(feature = "resources-introspection")]
impl std::fmt::Display for AllGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let methods: Vec<String> = self let methods: Vec<String> = self
.guards .details()
.unwrap_or_default()
.iter() .iter()
.filter_map(|guard| { .flat_map(|detail| {
guard if let GuardDetail::HttpMethods(methods) = detail {
.as_any() methods.clone()
.downcast_ref::<MethodGuard>()
.map(|method_guard| method_guard.0.to_string())
})
.collect();
write!(f, "[{}]", methods.join(", "))
}
}
#[cfg(feature = "resources-introspection")]
impl std::fmt::Display for AnyGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let methods: Vec<String> = self
.guards
.iter()
.map(|guard| {
let guard_ref = &**guard;
if let Some(method_guard) = guard_ref.as_any().downcast_ref::<MethodGuard>() {
method_guard.0.to_string()
} else if let Some(all_guard) = guard_ref.as_any().downcast_ref::<AllGuard>() {
all_guard.to_string()
} else { } else {
"UNKNOWN".to_string() vec!["UNKNOWN".to_string()]
} }
}) })
.collect(); .collect();
write!(f, "[{}]", methods.join(", ")) if methods.is_empty() {
vec!["UNKNOWN".to_string()]
} else {
methods
}
} }
} }
@ -458,15 +486,14 @@ impl Guard for HeaderGuard {
false false
} }
} fn name(&self) -> String {
format!("Header({}, {})", self.0, self.1.to_str().unwrap_or(""))
pub trait AsAny { }
fn as_any(&self) -> &dyn Any; fn details(&self) -> Option<Vec<GuardDetail>> {
} Some(vec![GuardDetail::Headers(vec![(
self.0.to_string(),
impl<T: Any> AsAny for T { self.1.to_str().unwrap_or("").to_string(),
fn as_any(&self) -> &dyn Any { )])])
self
} }
} }
@ -581,7 +608,7 @@ mod tests {
#[test] #[test]
fn function_guard() { fn function_guard() {
let domain = "rust-lang.org".to_owned(); let domain = "rust-lang.org".to_owned();
let guard = fn_guard(move |ctx| ctx.head().uri.host().unwrap().ends_with(&domain)); let guard = fn_guard(|ctx| ctx.head().uri.host().unwrap().ends_with(&domain));
let req = TestRequest::default() let req = TestRequest::default()
.uri("blog.rust-lang.org") .uri("blog.rust-lang.org")