mirror of https://github.com/fafhrd91/actix-web
feat(resources-introspection): add support for resource metadata retrieval
This commit is contained in:
parent
b8bdee0606
commit
0d87cc53a1
|
@ -128,6 +128,9 @@ compat = [
|
|||
# Opt-out forwards-compatibility for handler visibility inheritance fix.
|
||||
compat-routing-macros-force-pub = ["actix-web-codegen?/compat-routing-macros-force-pub"]
|
||||
|
||||
# Enabling the retrieval of metadata for initialized resources, including path and HTTP method.
|
||||
resources-introspection = []
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.5"
|
||||
actix-macros = { version = "0.2.3", optional = true }
|
||||
|
|
|
@ -82,6 +82,9 @@ where
|
|||
|
||||
let (config, services) = config.into_services();
|
||||
|
||||
#[cfg(feature = "resources-introspection")]
|
||||
let mut rdef_methods: Vec<(String, Vec<String>)> = Vec::new();
|
||||
|
||||
// complete pipeline creation.
|
||||
*self.factory_ref.borrow_mut() = Some(AppRoutingFactory {
|
||||
default,
|
||||
|
@ -89,6 +92,24 @@ where
|
|||
.into_iter()
|
||||
.map(|(mut rdef, srv, guards, nested)| {
|
||||
rmap.add(&mut rdef, nested);
|
||||
|
||||
#[cfg(feature = "resources-introspection")]
|
||||
{
|
||||
let http_methods: Vec<String> =
|
||||
guards.as_ref().map_or_else(Vec::new, |g| {
|
||||
g.iter()
|
||||
.flat_map(|g| {
|
||||
crate::guard::HttpMethodsExtractor::extract_http_methods(
|
||||
&**g,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
rdef_methods
|
||||
.push((rdef.pattern().unwrap_or_default().to_string(), http_methods));
|
||||
}
|
||||
|
||||
(rdef, srv, RefCell::new(guards))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
|
@ -105,6 +126,11 @@ where
|
|||
let rmap = Rc::new(rmap);
|
||||
ResourceMap::finish(&rmap);
|
||||
|
||||
#[cfg(feature = "resources-introspection")]
|
||||
{
|
||||
crate::introspection::process_introspection(Rc::clone(&rmap), rdef_methods);
|
||||
}
|
||||
|
||||
// construct all async data factory futures
|
||||
let factory_futs = join_all(self.async_data_factories.iter().map(|f| f()));
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
//! [`Route`]: crate::Route::guard()
|
||||
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::{Ref, RefMut},
|
||||
rc::Rc,
|
||||
};
|
||||
|
@ -121,7 +122,7 @@ impl<'a> GuardContext<'a> {
|
|||
/// Interface for routing guards.
|
||||
///
|
||||
/// See [module level documentation](self) for more.
|
||||
pub trait Guard {
|
||||
pub trait Guard: AsAny {
|
||||
/// Returns true if predicate condition is met for a given request.
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool;
|
||||
}
|
||||
|
@ -146,7 +147,7 @@ impl Guard for Rc<dyn Guard> {
|
|||
/// ```
|
||||
pub fn fn_guard<F>(f: F) -> impl Guard
|
||||
where
|
||||
F: Fn(&GuardContext<'_>) -> bool,
|
||||
F: Fn(&GuardContext<'_>) -> bool + 'static,
|
||||
{
|
||||
FnGuard(f)
|
||||
}
|
||||
|
@ -155,7 +156,7 @@ struct FnGuard<F: Fn(&GuardContext<'_>) -> bool>(F);
|
|||
|
||||
impl<F> Guard for FnGuard<F>
|
||||
where
|
||||
F: Fn(&GuardContext<'_>) -> bool,
|
||||
F: Fn(&GuardContext<'_>) -> bool + 'static,
|
||||
{
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
(self.0)(ctx)
|
||||
|
@ -164,7 +165,7 @@ where
|
|||
|
||||
impl<F> Guard for F
|
||||
where
|
||||
F: Fn(&GuardContext<'_>) -> bool,
|
||||
F: for<'a> Fn(&'a GuardContext<'a>) -> bool + 'static,
|
||||
{
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
(self)(ctx)
|
||||
|
@ -284,7 +285,7 @@ impl Guard for AllGuard {
|
|||
/// .guard(guard::Not(guard::Get()))
|
||||
/// .to(|| HttpResponse::Ok());
|
||||
/// ```
|
||||
pub struct Not<G>(pub G);
|
||||
pub struct Not<G: 'static>(pub G);
|
||||
|
||||
impl<G: Guard> Guard for Not<G> {
|
||||
#[inline]
|
||||
|
@ -322,6 +323,81 @@ impl Guard for MethodGuard {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "resources-introspection")]
|
||||
pub trait HttpMethodsExtractor {
|
||||
fn extract_http_methods(&self) -> Vec<String>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "resources-introspection")]
|
||||
impl HttpMethodsExtractor for dyn Guard {
|
||||
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
|
||||
.guards
|
||||
.iter()
|
||||
.filter_map(|guard| {
|
||||
guard
|
||||
.as_any()
|
||||
.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 {
|
||||
"UNKNOWN".to_string()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
write!(f, "[{}]", methods.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! method_guard {
|
||||
($method_fn:ident, $method_const:ident) => {
|
||||
#[doc = concat!("Creates a guard that matches the `", stringify!($method_const), "` request method.")]
|
||||
|
@ -384,6 +460,16 @@ impl Guard for HeaderGuard {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait AsAny {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
|
||||
impl<T: Any> AsAny for T {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_http::Method;
|
||||
|
@ -495,7 +581,7 @@ mod tests {
|
|||
#[test]
|
||||
fn function_guard() {
|
||||
let domain = "rust-lang.org".to_owned();
|
||||
let guard = fn_guard(|ctx| ctx.head().uri.host().unwrap().ends_with(&domain));
|
||||
let guard = fn_guard(move |ctx| ctx.head().uri.host().unwrap().ends_with(&domain));
|
||||
|
||||
let req = TestRequest::default()
|
||||
.uri("blog.rust-lang.org")
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
use std::rc::Rc;
|
||||
use std::sync::{OnceLock, RwLock};
|
||||
|
||||
use crate::dev::ResourceMap;
|
||||
|
||||
/// Represents an HTTP resource registered for introspection.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResourceIntrospection {
|
||||
/// HTTP method (e.g., "GET")
|
||||
pub method: String,
|
||||
/// Path (e.g., "/api/v1/test")
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Temporary registry for partial routes.
|
||||
static TEMP_REGISTRY: OnceLock<RwLock<Vec<ResourceIntrospection>>> = OnceLock::new();
|
||||
/// Final registry for complete routes.
|
||||
static FINAL_REGISTRY: OnceLock<RwLock<Vec<ResourceIntrospection>>> = OnceLock::new();
|
||||
|
||||
fn get_temp_registry() -> &'static RwLock<Vec<ResourceIntrospection>> {
|
||||
TEMP_REGISTRY.get_or_init(|| RwLock::new(Vec::new()))
|
||||
}
|
||||
|
||||
fn get_final_registry() -> &'static RwLock<Vec<ResourceIntrospection>> {
|
||||
FINAL_REGISTRY.get_or_init(|| RwLock::new(Vec::new()))
|
||||
}
|
||||
|
||||
/// Registers a resource.
|
||||
pub fn register_resource(resource: ResourceIntrospection, is_complete: bool) {
|
||||
let registry = if is_complete {
|
||||
get_final_registry()
|
||||
} else {
|
||||
get_temp_registry()
|
||||
};
|
||||
let mut reg = registry.write().expect("Failed to acquire lock");
|
||||
if !reg.iter().any(|r| r == &resource) {
|
||||
reg.push(resource);
|
||||
}
|
||||
}
|
||||
|
||||
/// Completes (moves to the final registry) partial routes that match the given marker,
|
||||
/// applying the prefix. Only affects routes whose path contains `marker`.
|
||||
pub fn complete_partial_routes_with_marker(marker: &str, prefix: &str) {
|
||||
let temp_registry = get_temp_registry();
|
||||
let mut temp = temp_registry
|
||||
.write()
|
||||
.expect("Failed to acquire lock TEMP_REGISTRY");
|
||||
let final_registry = get_final_registry();
|
||||
let mut final_reg = final_registry
|
||||
.write()
|
||||
.expect("Failed to acquire lock FINAL_REGISTRY");
|
||||
|
||||
let mut remaining = Vec::new();
|
||||
for resource in temp.drain(..) {
|
||||
if resource.path.contains(marker) {
|
||||
// Concatenate the prefix only if it is not already present.
|
||||
let full_path = if prefix.is_empty() {
|
||||
resource.path.clone()
|
||||
} else if prefix.ends_with("/") || resource.path.starts_with("/") {
|
||||
format!("{}{}", prefix, resource.path)
|
||||
} else {
|
||||
format!("{}/{}", prefix, resource.path)
|
||||
};
|
||||
let completed_resource = ResourceIntrospection {
|
||||
method: resource.method,
|
||||
path: full_path,
|
||||
};
|
||||
if !final_reg.iter().any(|r| r == &completed_resource) {
|
||||
final_reg.push(completed_resource);
|
||||
}
|
||||
} else {
|
||||
remaining.push(resource);
|
||||
}
|
||||
}
|
||||
*temp = remaining;
|
||||
}
|
||||
|
||||
/// Returns the complete list of registered resources.
|
||||
pub fn get_registered_resources() -> Vec<ResourceIntrospection> {
|
||||
let final_registry = get_final_registry();
|
||||
let final_reg = final_registry
|
||||
.read()
|
||||
.expect("Failed to acquire lock FINAL_REGISTRY");
|
||||
final_reg.clone()
|
||||
}
|
||||
|
||||
/// Processes introspection data for routes and methods.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `rmap`: A resource map that can be converted to a vector of full route strings.
|
||||
/// - `rdef_methods`: A vector of tuples `(sub_path, [methods])`.
|
||||
/// An entry with an empty methods vector is treated as a level marker, indicating that
|
||||
/// routes registered in a lower level (in TEMP_REGISTRY) should be "completed" (moved to the final registry)
|
||||
/// using the deduced prefix. For example, if a marker "/api" is found and the corresponding route is "/api/v1/item/{id}",
|
||||
/// the deduced prefix will be "" (if the marker starts at the beginning) or a non-empty string indicating a higher level.
|
||||
///
|
||||
/// # Processing Steps
|
||||
/// 1. **Marker Processing:**
|
||||
/// For each entry in `rdef_methods` with an empty methods vector, the function:
|
||||
/// - Searches `rmap_vec` for a route that contains the `sub_path` (the marker).
|
||||
/// - Deduces the prefix as the portion of the route before the marker.
|
||||
/// - Calls `complete_partial_routes_with_marker`, moving all partial routes that contain the marker
|
||||
/// from TEMP_REGISTRY to FINAL_REGISTRY, applying the deduced prefix.
|
||||
///
|
||||
/// 2. **Endpoint Registration:**
|
||||
/// For each entry in `rdef_methods` with assigned methods:
|
||||
/// - If `sub_path` is "/", an exact match is used; otherwise, routes ending with `sub_path` are considered.
|
||||
/// - Among the candidate routes from `rmap_vec`, the candidate that either starts with the deduced prefix
|
||||
/// (if non-empty) or the shortest candidate (if at root level or no prefix was deduced) is selected.
|
||||
/// - A single `ResourceIntrospection` is registered with the full route and all methods joined by commas.
|
||||
///
|
||||
/// Note: If multiple markers exist in the same block, only the last one processed (and stored in `deduced_prefix`)
|
||||
/// is used for selecting endpoint candidates. Consider refactoring if independent processing per level is desired.
|
||||
pub fn process_introspection(rmap: Rc<ResourceMap>, rdef_methods: Vec<(String, Vec<String>)>) {
|
||||
// Convert the ResourceMap to a vector for easier manipulation.
|
||||
let rmap_vec = rmap.to_vec();
|
||||
|
||||
// If there are no routes or methods, there is nothing to introspect.
|
||||
if rmap_vec.is_empty() && rdef_methods.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Variable to store the deduced prefix for this introspection (if there is a marker)
|
||||
let mut deduced_prefix: Option<String> = None;
|
||||
|
||||
// First, check the markers (entries with empty methods).
|
||||
for (sub_path, http_methods) in rdef_methods.iter() {
|
||||
if http_methods.is_empty() {
|
||||
if let Some(r) = rmap_vec.iter().find(|r| r.contains(sub_path)) {
|
||||
if let Some(pos) = r.find(sub_path) {
|
||||
let prefix = &r[..pos];
|
||||
deduced_prefix = Some(prefix.to_string());
|
||||
complete_partial_routes_with_marker(sub_path, prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then, process the endpoints with assigned methods.
|
||||
for (sub_path, http_methods) in rdef_methods.iter() {
|
||||
if !http_methods.is_empty() {
|
||||
// For the "/" subroute, do an exact match; otherwise, use ends_with.
|
||||
let candidates: Vec<&String> = if sub_path == "/" {
|
||||
rmap_vec.iter().filter(|r| r.as_str() == "/").collect()
|
||||
} else {
|
||||
rmap_vec.iter().filter(|r| r.ends_with(sub_path)).collect()
|
||||
};
|
||||
|
||||
if !candidates.is_empty() {
|
||||
let chosen = if let Some(prefix) = &deduced_prefix {
|
||||
if !prefix.is_empty() {
|
||||
candidates
|
||||
.iter()
|
||||
.find(|&&r| r.starts_with(prefix))
|
||||
.cloned()
|
||||
.or_else(|| candidates.iter().min_by_key(|&&r| r.len()).cloned())
|
||||
} else {
|
||||
// Root level: if sub_path is "/" we already filtered by equality.
|
||||
candidates.iter().min_by_key(|&&r| r.len()).cloned()
|
||||
}
|
||||
} else {
|
||||
candidates.iter().min_by_key(|&&r| r.len()).cloned()
|
||||
};
|
||||
if let Some(full_route) = chosen {
|
||||
// Register a single entry with all methods joined.
|
||||
register_resource(
|
||||
ResourceIntrospection {
|
||||
method: http_methods.join(","),
|
||||
path: full_route.clone(),
|
||||
},
|
||||
deduced_prefix.is_some(), // Mark as complete if any marker was detected.
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -108,6 +108,9 @@ mod thin_data;
|
|||
pub(crate) mod types;
|
||||
pub mod web;
|
||||
|
||||
#[cfg(feature = "resources-introspection")]
|
||||
pub mod introspection;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use crate::error::Result;
|
||||
pub use crate::{
|
||||
|
|
|
@ -433,6 +433,25 @@ where
|
|||
rdef.set_name(name);
|
||||
}
|
||||
|
||||
#[cfg(feature = "resources-introspection")]
|
||||
let mut rdef_methods: Vec<(String, Vec<String>)> = Vec::new();
|
||||
#[cfg(feature = "resources-introspection")]
|
||||
let mut rmap = crate::rmap::ResourceMap::new(ResourceDef::prefix(""));
|
||||
|
||||
#[cfg(feature = "resources-introspection")]
|
||||
{
|
||||
rmap.add(&mut rdef, None);
|
||||
|
||||
self.routes.iter_mut().for_each(|r| {
|
||||
r.take_guards().iter().for_each(|g| {
|
||||
let http_methods: Vec<String> =
|
||||
crate::guard::HttpMethodsExtractor::extract_http_methods(&**g);
|
||||
rdef_methods
|
||||
.push((rdef.pattern().unwrap_or_default().to_string(), http_methods));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
*self.factory_ref.borrow_mut() = Some(ResourceFactory {
|
||||
routes: self.routes,
|
||||
default: self.default,
|
||||
|
@ -451,6 +470,15 @@ where
|
|||
async { Ok(fut.await?.map_into_boxed_body()) }
|
||||
});
|
||||
|
||||
#[cfg(feature = "resources-introspection")]
|
||||
{
|
||||
println!("resources");
|
||||
crate::introspection::process_introspection(
|
||||
Rc::clone(&Rc::new(rmap.clone())),
|
||||
rdef_methods,
|
||||
);
|
||||
}
|
||||
|
||||
config.register_service(rdef, guards, endpoint, None)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,42 @@ impl ResourceMap {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "resources-introspection")]
|
||||
/// Returns a list of all paths in the resource map.
|
||||
pub fn to_vec(&self) -> Vec<String> {
|
||||
let mut paths = Vec::new();
|
||||
self.collect_full_paths(String::new(), &mut paths);
|
||||
paths
|
||||
}
|
||||
|
||||
#[cfg(feature = "resources-introspection")]
|
||||
/// Recursive function that accumulates the full path and adds it only if the node is an endpoint (leaf).
|
||||
fn collect_full_paths(&self, current_path: String, paths: &mut Vec<String>) {
|
||||
// Get the current segment of the pattern
|
||||
if let Some(segment) = self.pattern.pattern() {
|
||||
let mut new_path = current_path;
|
||||
// Add the '/' separator if necessary
|
||||
if !segment.is_empty() {
|
||||
if !new_path.ends_with('/') && !new_path.is_empty() && !segment.starts_with('/') {
|
||||
new_path.push('/');
|
||||
}
|
||||
new_path.push_str(segment);
|
||||
}
|
||||
|
||||
// If this node is an endpoint (has no children), add the full path
|
||||
if self.nodes.is_none() {
|
||||
paths.push(new_path.clone());
|
||||
}
|
||||
|
||||
// If it has children, iterate over each one
|
||||
if let Some(children) = &self.nodes {
|
||||
for child in children {
|
||||
child.collect_full_paths(new_path.clone(), paths);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Format resource map as tree structure (unfinished).
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn tree(&self) -> String {
|
||||
|
|
|
@ -395,6 +395,9 @@ where
|
|||
rmap.add(&mut rdef, None);
|
||||
}
|
||||
|
||||
#[cfg(feature = "resources-introspection")]
|
||||
let mut rdef_methods: Vec<(String, Vec<String>)> = Vec::new();
|
||||
|
||||
// complete scope pipeline creation
|
||||
*self.factory_ref.borrow_mut() = Some(ScopeFactory {
|
||||
default,
|
||||
|
@ -404,6 +407,24 @@ where
|
|||
.into_iter()
|
||||
.map(|(mut rdef, srv, guards, nested)| {
|
||||
rmap.add(&mut rdef, nested);
|
||||
|
||||
#[cfg(feature = "resources-introspection")]
|
||||
{
|
||||
let http_methods: Vec<String> =
|
||||
guards.as_ref().map_or_else(Vec::new, |g| {
|
||||
g.iter()
|
||||
.flat_map(|g| {
|
||||
crate::guard::HttpMethodsExtractor::extract_http_methods(
|
||||
&**g,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
rdef_methods
|
||||
.push((rdef.pattern().unwrap_or_default().to_string(), http_methods));
|
||||
}
|
||||
|
||||
(rdef, srv, RefCell::new(guards))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
|
@ -431,6 +452,14 @@ where
|
|||
async { Ok(fut.await?.map_into_boxed_body()) }
|
||||
});
|
||||
|
||||
#[cfg(feature = "resources-introspection")]
|
||||
{
|
||||
crate::introspection::process_introspection(
|
||||
Rc::clone(&Rc::new(rmap.clone())),
|
||||
rdef_methods,
|
||||
);
|
||||
}
|
||||
|
||||
// register final service
|
||||
config.register_service(
|
||||
ResourceDef::root_prefix(&self.rdef),
|
||||
|
|
Loading…
Reference in New Issue