From 0d87cc53a1c92ff2c9ca3743816ab6d13453026c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= <1295883+dertin@users.noreply.github.com> Date: Mon, 3 Mar 2025 05:27:52 -0300 Subject: [PATCH 01/18] feat(resources-introspection): add support for resource metadata retrieval --- actix-web/Cargo.toml | 3 + actix-web/src/app_service.rs | 26 +++++ actix-web/src/guard/mod.rs | 98 ++++++++++++++++-- actix-web/src/introspection.rs | 177 +++++++++++++++++++++++++++++++++ actix-web/src/lib.rs | 3 + actix-web/src/resource.rs | 28 ++++++ actix-web/src/rmap.rs | 36 +++++++ actix-web/src/scope.rs | 29 ++++++ 8 files changed, 394 insertions(+), 6 deletions(-) create mode 100644 actix-web/src/introspection.rs diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index d6fd8f5af..219449dc5 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -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 } diff --git a/actix-web/src/app_service.rs b/actix-web/src/app_service.rs index 7aa16b790..a935c1354 100644 --- a/actix-web/src/app_service.rs +++ b/actix-web/src/app_service.rs @@ -82,6 +82,9 @@ where let (config, services) = config.into_services(); + #[cfg(feature = "resources-introspection")] + let mut rdef_methods: Vec<(String, Vec)> = 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 = + guards.as_ref().map_or_else(Vec::new, |g| { + g.iter() + .flat_map(|g| { + crate::guard::HttpMethodsExtractor::extract_http_methods( + &**g, + ) + }) + .collect::>() + }); + + rdef_methods + .push((rdef.pattern().unwrap_or_default().to_string(), http_methods)); + } + (rdef, srv, RefCell::new(guards)) }) .collect::>() @@ -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())); diff --git a/actix-web/src/guard/mod.rs b/actix-web/src/guard/mod.rs index 41609953a..93aeefe3c 100644 --- a/actix-web/src/guard/mod.rs +++ b/actix-web/src/guard/mod.rs @@ -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 { /// ``` pub fn fn_guard(f: F) -> impl Guard where - F: Fn(&GuardContext<'_>) -> bool, + F: Fn(&GuardContext<'_>) -> bool + 'static, { FnGuard(f) } @@ -155,7 +156,7 @@ struct FnGuard) -> bool>(F); impl Guard for FnGuard 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 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(pub G); +pub struct Not(pub G); impl Guard for Not { #[inline] @@ -322,6 +323,81 @@ impl Guard for MethodGuard { } } +#[cfg(feature = "resources-introspection")] +pub trait HttpMethodsExtractor { + fn extract_http_methods(&self) -> Vec; +} + +#[cfg(feature = "resources-introspection")] +impl HttpMethodsExtractor for dyn Guard { + fn extract_http_methods(&self) -> Vec { + if let Some(method_guard) = self.as_any().downcast_ref::() { + vec![method_guard.0.to_string()] + } else if let Some(any_guard) = self.as_any().downcast_ref::() { + any_guard + .guards + .iter() + .flat_map(|g| g.extract_http_methods()) + .collect() + } else if let Some(all_guard) = self.as_any().downcast_ref::() { + 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 = self + .guards + .iter() + .filter_map(|guard| { + guard + .as_any() + .downcast_ref::() + .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 = self + .guards + .iter() + .map(|guard| { + let guard_ref = &**guard; + if let Some(method_guard) = guard_ref.as_any().downcast_ref::() { + method_guard.0.to_string() + } else if let Some(all_guard) = guard_ref.as_any().downcast_ref::() { + 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 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") diff --git a/actix-web/src/introspection.rs b/actix-web/src/introspection.rs new file mode 100644 index 000000000..d0b439f29 --- /dev/null +++ b/actix-web/src/introspection.rs @@ -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>> = OnceLock::new(); +/// Final registry for complete routes. +static FINAL_REGISTRY: OnceLock>> = OnceLock::new(); + +fn get_temp_registry() -> &'static RwLock> { + TEMP_REGISTRY.get_or_init(|| RwLock::new(Vec::new())) +} + +fn get_final_registry() -> &'static RwLock> { + 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 { + 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, rdef_methods: Vec<(String, Vec)>) { + // 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 = 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. + ); + } + } + } + } +} diff --git a/actix-web/src/lib.rs b/actix-web/src/lib.rs index e2a8e2275..e26e76243 100644 --- a/actix-web/src/lib.rs +++ b/actix-web/src/lib.rs @@ -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::{ diff --git a/actix-web/src/resource.rs b/actix-web/src/resource.rs index aee0dff93..0fb2cd234 100644 --- a/actix-web/src/resource.rs +++ b/actix-web/src/resource.rs @@ -433,6 +433,25 @@ where rdef.set_name(name); } + #[cfg(feature = "resources-introspection")] + let mut rdef_methods: Vec<(String, Vec)> = 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 = + 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) } } diff --git a/actix-web/src/rmap.rs b/actix-web/src/rmap.rs index b445687ac..2e4451b38 100644 --- a/actix-web/src/rmap.rs +++ b/actix-web/src/rmap.rs @@ -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 { + 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) { + // 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 { diff --git a/actix-web/src/scope.rs b/actix-web/src/scope.rs index e317349da..0f2b87c86 100644 --- a/actix-web/src/scope.rs +++ b/actix-web/src/scope.rs @@ -395,6 +395,9 @@ where rmap.add(&mut rdef, None); } + #[cfg(feature = "resources-introspection")] + let mut rdef_methods: Vec<(String, Vec)> = 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 = + guards.as_ref().map_or_else(Vec::new, |g| { + g.iter() + .flat_map(|g| { + crate::guard::HttpMethodsExtractor::extract_http_methods( + &**g, + ) + }) + .collect::>() + }); + + rdef_methods + .push((rdef.pattern().unwrap_or_default().to_string(), http_methods)); + } + (rdef, srv, RefCell::new(guards)) }) .collect::>() @@ -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), From 041322ef9cae0ebeca64fa093ce23ec87945d8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= <1295883+dertin@users.noreply.github.com> Date: Mon, 3 Mar 2025 06:10:45 -0300 Subject: [PATCH 02/18] misc: remove debug print --- actix-web/src/resource.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/actix-web/src/resource.rs b/actix-web/src/resource.rs index 0fb2cd234..104adabf5 100644 --- a/actix-web/src/resource.rs +++ b/actix-web/src/resource.rs @@ -472,7 +472,6 @@ where #[cfg(feature = "resources-introspection")] { - println!("resources"); crate::introspection::process_introspection( Rc::clone(&Rc::new(rmap.clone())), rdef_methods, From 819ee93e9b81c2f4a71ea637df1125682704a0e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= <1295883+dertin@users.noreply.github.com> Date: Mon, 3 Mar 2025 06:46:52 -0300 Subject: [PATCH 03/18] style: cargo fmt --- actix-web/src/introspection.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/actix-web/src/introspection.rs b/actix-web/src/introspection.rs index d0b439f29..f683f61ed 100644 --- a/actix-web/src/introspection.rs +++ b/actix-web/src/introspection.rs @@ -1,5 +1,7 @@ -use std::rc::Rc; -use std::sync::{OnceLock, RwLock}; +use std::{ + rc::Rc, + sync::{OnceLock, RwLock}, +}; use crate::dev::ResourceMap; From 91fa813f0e316448eb9fc5afec1946ceb2205a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= <1295883+dertin@users.noreply.github.com> Date: Mon, 3 Mar 2025 15:31:20 -0300 Subject: [PATCH 04/18] fix(guards): replace take_guards with get_guards to prevent guard removal and fix test failures --- actix-web/src/guard/mod.rs | 2 +- actix-web/src/resource.rs | 4 ++-- actix-web/src/route.rs | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/actix-web/src/guard/mod.rs b/actix-web/src/guard/mod.rs index 93aeefe3c..1f660d174 100644 --- a/actix-web/src/guard/mod.rs +++ b/actix-web/src/guard/mod.rs @@ -11,7 +11,7 @@ //! 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 -//! 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 //! services multiple times (which might have different combining behavior than you want). //! diff --git a/actix-web/src/resource.rs b/actix-web/src/resource.rs index 104adabf5..e5016d240 100644 --- a/actix-web/src/resource.rs +++ b/actix-web/src/resource.rs @@ -442,8 +442,8 @@ where { rmap.add(&mut rdef, None); - self.routes.iter_mut().for_each(|r| { - r.take_guards().iter().for_each(|g| { + self.routes.iter().for_each(|r| { + r.get_guards().iter().for_each(|g| { let http_methods: Vec = crate::guard::HttpMethodsExtractor::extract_http_methods(&**g); rdef_methods diff --git a/actix-web/src/route.rs b/actix-web/src/route.rs index e05e6be52..bcbfb9042 100644 --- a/actix-web/src/route.rs +++ b/actix-web/src/route.rs @@ -65,6 +65,11 @@ impl Route { pub(crate) fn take_guards(&mut self) -> Vec> { mem::take(Rc::get_mut(&mut self.guards).unwrap()) } + + #[cfg(feature = "resources-introspection")] + pub(crate) fn get_guards(&self) -> &Vec> { + &self.guards + } } impl ServiceFactory for Route { From 176ea5da779d5fb48957bfbc96bb324e9ed4016f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= <1295883+dertin@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:05:01 -0300 Subject: [PATCH 05/18] ci: downgrade for msrv litemap to version 0.7.4 in justfile --- justfile | 1 + 1 file changed, 1 insertion(+) diff --git a/justfile b/justfile index 3a5e417fd..7e63e037c 100644 --- a/justfile +++ b/justfile @@ -13,6 +13,7 @@ downgrade-for-msrv: cargo update -p=parse-size --precise=1.0.0 cargo update -p=clap --precise=4.4.18 cargo update -p=divan --precise=0.1.15 + cargo update -p=litemap --precise=0.7.4 msrv := ``` cargo metadata --format-version=1 \ From acd7c58097b932c4e5b0f9994c15edf91aa0f285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= <1295883+dertin@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:12:31 -0300 Subject: [PATCH 06/18] chore: update changelog and fix docs for CI --- actix-web/CHANGES.md | 1 + actix-web/src/guard/mod.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index edbdc6982..030c71d1d 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -2,6 +2,7 @@ ## Unreleased +- Add `resources-introspection` feature for retrieving configured route paths and HTTP methods. - Implement `Responder` for `Result<(), E: Into>`. Returning `Ok(())` responds with HTTP 204 No Content. - On Windows, an error is now returned from `HttpServer::bind()` (or TLS variants) when binding to a socket that's already in use. - Update `brotli` dependency to `7`. diff --git a/actix-web/src/guard/mod.rs b/actix-web/src/guard/mod.rs index 1f660d174..8f1021f76 100644 --- a/actix-web/src/guard/mod.rs +++ b/actix-web/src/guard/mod.rs @@ -196,7 +196,7 @@ pub fn Any(guard: F) -> AnyGuard { /// /// That is, only one contained guard needs to match in order for the aggregate guard to match. /// -/// Construct an `AnyGuard` using [`Any`]. +/// Construct an `AnyGuard` using [`Any()`]. pub struct AnyGuard { guards: Vec>, } From c4be19942b2c2e41813242f1b42b2fa908ef991f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= <1295883+dertin@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:25:50 -0300 Subject: [PATCH 07/18] ci: downgrade for msrv zerofrom to version 0.1.5 in justfile --- justfile | 1 + 1 file changed, 1 insertion(+) diff --git a/justfile b/justfile index 7e63e037c..2b6d04d18 100644 --- a/justfile +++ b/justfile @@ -14,6 +14,7 @@ downgrade-for-msrv: cargo update -p=clap --precise=4.4.18 cargo update -p=divan --precise=0.1.15 cargo update -p=litemap --precise=0.7.4 + cargo update -p=zerofrom --precise=0.1.5 msrv := ``` cargo metadata --format-version=1 \ From a79dc9dc79370e766d4f7a4fb3bc673d9b07c784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= <1295883+dertin@users.noreply.github.com> Date: Tue, 4 Mar 2025 05:09:18 -0300 Subject: [PATCH 08/18] refactor: improve thread safety and add unit tests for introspection process --- actix-web/src/introspection.rs | 486 +++++++++++++++++++++++++++------ 1 file changed, 395 insertions(+), 91 deletions(-) diff --git a/actix-web/src/introspection.rs b/actix-web/src/introspection.rs index f683f61ed..75f5dd395 100644 --- a/actix-web/src/introspection.rs +++ b/actix-web/src/introspection.rs @@ -1,153 +1,142 @@ use std::{ rc::Rc, sync::{OnceLock, RwLock}, + thread, }; -use crate::dev::ResourceMap; +use crate::rmap::ResourceMap; /// Represents an HTTP resource registered for introspection. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ResourceIntrospection { - /// HTTP method (e.g., "GET") + /// HTTP method (e.g., "GET"). pub method: String, - /// Path (e.g., "/api/v1/test") + /// Route path (e.g., "/api/v1/test"). pub path: String, } -/// Temporary registry for partial routes. -static TEMP_REGISTRY: OnceLock>> = OnceLock::new(); -/// Final registry for complete routes. -static FINAL_REGISTRY: OnceLock>> = OnceLock::new(); +/// A global registry of listed resources for introspection. +/// Only the designated thread can modify it. +static RESOURCE_REGISTRY: RwLock> = RwLock::new(Vec::new()); -fn get_temp_registry() -> &'static RwLock> { - TEMP_REGISTRY.get_or_init(|| RwLock::new(Vec::new())) -} +/// Stores the thread ID of the designated thread (the first to call `process_introspection`). +/// Any other thread will immediately return without updating the global registry. +static DESIGNATED_THREAD: OnceLock = OnceLock::new(); -fn get_final_registry() -> &'static RwLock> { - 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); +/// Inserts a resource into the global registry, avoiding duplicates. +pub fn register_resource(resource: ResourceIntrospection) { + let mut global = RESOURCE_REGISTRY.write().unwrap(); + if !global.iter().any(|r| r == &resource) { + global.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`. +/// Completes (updates) partial routes in the global registry whose path contains `marker`, +/// by applying the specified `prefix`. 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 global = RESOURCE_REGISTRY.write().unwrap(); + let mut updated = Vec::new(); let mut remaining = Vec::new(); - for resource in temp.drain(..) { + + // Move all items out of the current registry. + for resource in global.drain(..) { if resource.path.contains(marker) { - // Concatenate the prefix only if it is not already present. + // Build the full path by applying the prefix if needed. let full_path = if prefix.is_empty() { resource.path.clone() - } else if prefix.ends_with("/") || resource.path.starts_with("/") { + } else if prefix.ends_with('/') || resource.path.starts_with('/') { format!("{}{}", prefix, resource.path) } else { format!("{}/{}", prefix, resource.path) }; - let completed_resource = ResourceIntrospection { + + let completed = ResourceIntrospection { method: resource.method, path: full_path, }; - if !final_reg.iter().any(|r| r == &completed_resource) { - final_reg.push(completed_resource); + + // Add to `updated` if it's not already in there. + if !updated.iter().any(|r| r == &completed) { + updated.push(completed); } } else { + // Keep this resource as-is. remaining.push(resource); } } - *temp = remaining; + + // Merge updated items back with the remaining ones. + remaining.extend(updated); + *global = remaining; } -/// Returns the complete list of registered resources. +/// Returns a **copy** of the global registry (safe to call from any thread). pub fn get_registered_resources() -> Vec { - let final_registry = get_final_registry(); - let final_reg = final_registry - .read() - .expect("Failed to acquire lock FINAL_REGISTRY"); - final_reg.clone() + RESOURCE_REGISTRY.read().unwrap().clone() } /// Processes introspection data for routes and methods. +/// Only the **first thread** that calls this function (the "designated" one) may update +/// the global resource registry. Any other thread will immediately return without updating it. /// /// # 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. +/// - `rmap`: A resource map convertible to a vector of route strings. +/// - `rdef_methods`: A vector of `(sub_path, [methods])`. +/// - A tuple with an **empty** methods vector is treated as a "marker" (a partial route) +/// for which we try to deduce a prefix by finding `sub_path` in a route, then calling +/// `complete_partial_routes_with_marker`. +/// - A tuple with one or more methods registers a resource with `register_resource`. pub fn process_introspection(rmap: Rc, rdef_methods: Vec<(String, Vec)>) { - // Convert the ResourceMap to a vector for easier manipulation. + // Determine the designated thread: if none is set yet, assign the current thread's ID. + // This ensures that the first thread to call this function becomes the designated thread. + let current_id = thread::current().id(); + DESIGNATED_THREAD.get_or_init(|| current_id); + + // If the current thread is not the designated one, return immediately. + // This ensures that only the designated thread updates the global registry, + // avoiding any interleaving or inconsistent updates from other threads. + if *DESIGNATED_THREAD.get().unwrap() != current_id { + return; + } + let rmap_vec = rmap.to_vec(); - // If there are no routes or methods, there is nothing to introspect. + // If there is no data, nothing to process. + // Avoid unnecessary work. if rmap_vec.is_empty() && rdef_methods.is_empty() { return; } - // Variable to store the deduced prefix for this introspection (if there is a marker) + // Keep track of the deduced prefix for partial routes. let mut deduced_prefix: Option = None; - // First, check the markers (entries with empty methods). - for (sub_path, http_methods) in rdef_methods.iter() { + // 1. Handle "marker" entries (where methods is empty). + for (sub_path, http_methods) in &rdef_methods { 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]; + // Find any route that contains sub_path and use it to deduce a prefix. + if let Some(route) = rmap_vec.iter().find(|r| r.contains(sub_path)) { + if let Some(pos) = route.find(sub_path) { + let prefix = &route[..pos]; deduced_prefix = Some(prefix.to_string()); + // Complete partial routes in the global registry using this prefix. complete_partial_routes_with_marker(sub_path, prefix); } } } } - // Then, process the endpoints with assigned methods. - for (sub_path, http_methods) in rdef_methods.iter() { + // 2. Handle endpoint entries (where methods is non-empty). + for (sub_path, http_methods) in &rdef_methods { if !http_methods.is_empty() { - // For the "/" subroute, do an exact match; otherwise, use ends_with. + // Identify candidate routes that end with sub_path (or exactly match "/" if sub_path == "/"). 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 we found any candidates, pick the best match. if !candidates.is_empty() { let chosen = if let Some(prefix) = &deduced_prefix { if !prefix.is_empty() { @@ -157,23 +146,338 @@ pub fn process_introspection(rmap: Rc, rdef_methods: Vec<(String, V .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. - ); + // Register the endpoint in the global resource registry. + register_resource(ResourceIntrospection { + method: http_methods.join(","), + path: full_route.clone(), + }); } } } } } + +#[cfg(test)] +mod tests { + use std::{num::NonZeroUsize, rc::Rc}; + + use actix_router::ResourceDef; + use tokio::sync::oneshot; + + use super::*; + use crate::rmap::ResourceMap; + + /// Helper function to create a ResourceMap from a list of route strings. + /// It creates a root ResourceMap with an empty prefix and adds each route as a leaf. + fn create_resource_map(routes: Vec<&str>) -> Rc { + // Create a root node with an empty prefix. + let mut root = ResourceMap::new(ResourceDef::root_prefix("")); + // For each route, create a ResourceDef and add it as a leaf (nested = None). + for route in routes { + let mut def = ResourceDef::new(route); + root.add(&mut def, None); + } + Rc::new(root) + } + + // Helper function to run the full introspection flow. + // It processes introspection data for multiple blocks, each with a different set of routes and methods. + fn run_full_introspection_flow() { + // Block 1: + // rmap_vec: ["/item/{id}"] + // rdef_methods: [] + process_introspection(create_resource_map(vec!["/item/{id}"]), vec![]); + + // Block 2: + // rmap_vec: ["/info"] + // rdef_methods: [] + process_introspection(create_resource_map(vec!["/info"]), vec![]); + + // Block 3: + // rmap_vec: ["/guarded"] + // rdef_methods: [] + process_introspection(create_resource_map(vec!["/guarded"]), vec![]); + + // Block 4: + // rmap_vec: ["/v1/item/{id}", "/v1/info", "/v1/guarded"] + // rdef_methods: [("/item/{id}", ["GET"]), ("/info", ["POST"]), ("/guarded", ["UNKNOWN"])] + process_introspection( + create_resource_map(vec!["/v1/item/{id}", "/v1/info", "/v1/guarded"]), + vec![ + ("/item/{id}".to_string(), vec!["GET".to_string()]), + ("/info".to_string(), vec!["POST".to_string()]), + ("/guarded".to_string(), vec!["UNKNOWN".to_string()]), + ], + ); + + // Block 5: + // rmap_vec: ["/hello"] + // rdef_methods: [] + process_introspection(create_resource_map(vec!["/hello"]), vec![]); + + // Block 6: + // rmap_vec: ["/v2/hello"] + // rdef_methods: [("/hello", ["GET"])] + process_introspection( + create_resource_map(vec!["/v2/hello"]), + vec![("/hello".to_string(), vec!["GET".to_string()])], + ); + + // Block 7: + // rmap_vec: ["/api/v1/item/{id}", "/api/v1/info", "/api/v1/guarded", "/api/v2/hello"] + // rdef_methods: [("/v1", []), ("/v2", [])] + process_introspection( + create_resource_map(vec![ + "/api/v1/item/{id}", + "/api/v1/info", + "/api/v1/guarded", + "/api/v2/hello", + ]), + vec![("/v1".to_string(), vec![]), ("/v2".to_string(), vec![])], + ); + + // Block 8: + // rmap_vec: ["/dashboard"] + // rdef_methods: [] + process_introspection(create_resource_map(vec!["/dashboard"]), vec![]); + + // Block 9: + // rmap_vec: ["/settings"] + // rdef_methods: [("/settings", ["GET"]), ("/settings", ["POST"])] + process_introspection( + create_resource_map(vec!["/settings"]), + vec![ + ("/settings".to_string(), vec!["GET".to_string()]), + ("/settings".to_string(), vec!["POST".to_string()]), + ], + ); + + // Block 10: + // rmap_vec: ["/admin/dashboard", "/admin/settings"] + // rdef_methods: [("/dashboard", ["GET"]), ("/settings", [])] + process_introspection( + create_resource_map(vec!["/admin/dashboard", "/admin/settings"]), + vec![ + ("/dashboard".to_string(), vec!["GET".to_string()]), + ("/settings".to_string(), vec![]), + ], + ); + + // Block 11: + // rmap_vec: ["/"] + // rdef_methods: [("/", ["GET"]), ("/", ["POST"])] + process_introspection( + create_resource_map(vec!["/"]), + vec![ + ("/".to_string(), vec!["GET".to_string()]), + ("/".to_string(), vec!["POST".to_string()]), + ], + ); + + // Block 12: + // rmap_vec: ["/ping"] + // rdef_methods: [] + process_introspection(create_resource_map(vec!["/ping"]), vec![]); + + // Block 13: + // rmap_vec: ["/multi"] + // rdef_methods: [("/multi", ["GET"]), ("/multi", ["POST"])] + process_introspection( + create_resource_map(vec!["/multi"]), + vec![ + ("/multi".to_string(), vec!["GET".to_string()]), + ("/multi".to_string(), vec!["POST".to_string()]), + ], + ); + + // Block 14: + // rmap_vec: ["/extra/ping", "/extra/multi"] + // rdef_methods: [("/ping", ["GET"]), ("/multi", [])] + process_introspection( + create_resource_map(vec!["/extra/ping", "/extra/multi"]), + vec![ + ("/ping".to_string(), vec!["GET".to_string()]), + ("/multi".to_string(), vec![]), + ], + ); + + // Block 15: + // rmap_vec: ["/other_guard"] + // rdef_methods: [] + process_introspection(create_resource_map(vec!["/other_guard"]), vec![]); + + // Block 16: + // rmap_vec: ["/all_guard"] + // rdef_methods: [] + process_introspection(create_resource_map(vec!["/all_guard"]), vec![]); + + // Block 17: + // rmap_vec: ["/api/v1/item/{id}", "/api/v1/info", "/api/v1/guarded", "/api/v2/hello", + // "/admin/dashboard", "/admin/settings", "/", "/extra/ping", "/extra/multi", + // "/other_guard", "/all_guard"] + // rdef_methods: [("/api", []), ("/admin", []), ("/", []), ("/extra", []), + // ("/other_guard", ["UNKNOWN"]), ("/all_guard", ["GET", "UNKNOWN", "POST"])] + process_introspection( + create_resource_map(vec![ + "/api/v1/item/{id}", + "/api/v1/info", + "/api/v1/guarded", + "/api/v2/hello", + "/admin/dashboard", + "/admin/settings", + "/", + "/extra/ping", + "/extra/multi", + "/other_guard", + "/all_guard", + ]), + vec![ + ("/api".to_string(), vec![]), + ("/admin".to_string(), vec![]), + ("/".to_string(), vec![]), + ("/extra".to_string(), vec![]), + ("/other_guard".to_string(), vec!["UNKNOWN".to_string()]), + ( + "/all_guard".to_string(), + vec!["GET".to_string(), "UNKNOWN".to_string(), "POST".to_string()], + ), + ], + ); + } + + /// This test spawns multiple tasks that run the full introspection flow concurrently. + /// Only the designated task (the first one to call process_introspection) updates the global registry, + /// ensuring that the internal order remains consistent. Finally, we verify that get_registered_resources() + /// returns the expected set of listed resources. + /// Using a dedicated arbiter for each task ensures that the global registry is thread-safe. + #[actix_rt::test] + async fn test_introspection() { + // Number of tasks to spawn. + const NUM_TASKS: usize = 4; + let mut completion_receivers = Vec::with_capacity(NUM_TASKS); + + // Check that the registry is initially empty. + let registered_resources = get_registered_resources(); + + assert_eq!( + registered_resources.len(), + 0, + "Expected 0 registered resources, found: {:?}", + registered_resources + ); + + // Determine parallelism and max blocking threads. + let parallelism = std::thread::available_parallelism().map_or(2, NonZeroUsize::get); + let max_blocking_threads = std::cmp::max(512 / parallelism, 1); + + // Spawn tasks on the arbiter. Each task runs the full introspection flow and then signals completion. + for _ in 0..NUM_TASKS { + let (tx, rx) = oneshot::channel(); + // Create an Arbiter with a dedicated Tokio runtime. + let arbiter = actix_rt::Arbiter::with_tokio_rt(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .max_blocking_threads(max_blocking_threads) + .build() + .unwrap() + }); + // Spawn the task on the arbiter. + arbiter.spawn(async move { + run_full_introspection_flow(); + // Signal that this task has finished. + let _ = tx.send(()); + }); + completion_receivers.push(rx); + } + + // Wait for all spawned tasks to complete. + for rx in completion_receivers { + let _ = rx.await; + } + + // After all blocks, we expect the final registry to contain 14 entries. + let registered_resources = get_registered_resources(); + + assert_eq!( + registered_resources.len(), + 14, + "Expected 14 registered resources, found: {:?}", + registered_resources + ); + + // List of expected resources + let expected_resources = vec![ + ResourceIntrospection { + method: "GET".to_string(), + path: "/api/v1/item/{id}".to_string(), + }, + ResourceIntrospection { + method: "POST".to_string(), + path: "/api/v1/info".to_string(), + }, + ResourceIntrospection { + method: "UNKNOWN".to_string(), + path: "/api/v1/guarded".to_string(), + }, + ResourceIntrospection { + method: "GET".to_string(), + path: "/api/v2/hello".to_string(), + }, + ResourceIntrospection { + method: "GET".to_string(), + path: "/admin/settings".to_string(), + }, + ResourceIntrospection { + method: "POST".to_string(), + path: "/admin/settings".to_string(), + }, + ResourceIntrospection { + method: "GET".to_string(), + path: "/admin/dashboard".to_string(), + }, + ResourceIntrospection { + method: "GET".to_string(), + path: "/extra/multi".to_string(), + }, + ResourceIntrospection { + method: "POST".to_string(), + path: "/extra/multi".to_string(), + }, + ResourceIntrospection { + method: "GET".to_string(), + path: "/extra/ping".to_string(), + }, + ResourceIntrospection { + method: "GET".to_string(), + path: "/".to_string(), + }, + ResourceIntrospection { + method: "POST".to_string(), + path: "/".to_string(), + }, + ResourceIntrospection { + method: "UNKNOWN".to_string(), + path: "/other_guard".to_string(), + }, + ResourceIntrospection { + method: "GET,UNKNOWN,POST".to_string(), + path: "/all_guard".to_string(), + }, + ]; + + for exp in expected_resources { + assert!( + registered_resources.contains(&exp), + "Expected resource not found: {:?}", + exp + ); + } + } +} From 2bb774a52983db3466ed344e7be6ec8bb08eae97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= <1295883+dertin@users.noreply.github.com> Date: Tue, 4 Mar 2025 05:30:49 -0300 Subject: [PATCH 09/18] fix(introspection): add conditional arbiter creation for io-uring support --- actix-web/src/introspection.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/actix-web/src/introspection.rs b/actix-web/src/introspection.rs index 75f5dd395..7fb0fb4e4 100644 --- a/actix-web/src/introspection.rs +++ b/actix-web/src/introspection.rs @@ -380,14 +380,25 @@ mod tests { // Spawn tasks on the arbiter. Each task runs the full introspection flow and then signals completion. for _ in 0..NUM_TASKS { let (tx, rx) = oneshot::channel(); - // Create an Arbiter with a dedicated Tokio runtime. + + #[cfg(all(target_os = "linux", feature = "experimental-io-uring"))] + let arbiter = { + // TODO: pass max blocking thread config when tokio-uring enable configuration + // on building runtime. + let _ = config.max_blocking_threads; + Arbiter::new() + }; + + #[cfg(not(all(target_os = "linux", feature = "experimental-io-uring")))] let arbiter = actix_rt::Arbiter::with_tokio_rt(move || { + // Create an Arbiter with a dedicated Tokio runtime. tokio::runtime::Builder::new_current_thread() .enable_all() .max_blocking_threads(max_blocking_threads) .build() .unwrap() }); + // Spawn the task on the arbiter. arbiter.spawn(async move { run_full_introspection_flow(); From 3b99f86e89fb120f9cf46d59ad637ff66b38d098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= <1295883+dertin@users.noreply.github.com> Date: Tue, 4 Mar 2025 05:30:49 -0300 Subject: [PATCH 10/18] fix(introspection): add conditional arbiter creation for io-uring support --- actix-web/src/introspection.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/actix-web/src/introspection.rs b/actix-web/src/introspection.rs index 75f5dd395..6074f2691 100644 --- a/actix-web/src/introspection.rs +++ b/actix-web/src/introspection.rs @@ -380,14 +380,25 @@ mod tests { // Spawn tasks on the arbiter. Each task runs the full introspection flow and then signals completion. for _ in 0..NUM_TASKS { let (tx, rx) = oneshot::channel(); - // Create an Arbiter with a dedicated Tokio runtime. + + #[cfg(all(target_os = "linux", feature = "experimental-io-uring"))] + let arbiter = { + // TODO: pass max blocking thread config when tokio-uring enable configuration + // on building runtime. + let _ = config.max_blocking_threads; + actix_rt::Arbiter::new() + }; + + #[cfg(not(all(target_os = "linux", feature = "experimental-io-uring")))] let arbiter = actix_rt::Arbiter::with_tokio_rt(move || { + // Create an Arbiter with a dedicated Tokio runtime. tokio::runtime::Builder::new_current_thread() .enable_all() .max_blocking_threads(max_blocking_threads) .build() .unwrap() }); + // Spawn the task on the arbiter. arbiter.spawn(async move { run_full_introspection_flow(); From ae08dcf6dc578d8ca15d69a84470dedb603147f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= <1295883+dertin@users.noreply.github.com> Date: Wed, 5 Mar 2025 02:34:39 -0300 Subject: [PATCH 11/18] 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`. --- actix-web/src/guard/mod.rs | 177 +++++++++++++++++++++---------------- 1 file changed, 102 insertions(+), 75 deletions(-) diff --git a/actix-web/src/guard/mod.rs b/actix-web/src/guard/mod.rs index 8f1021f76..03a8f370d 100644 --- a/actix-web/src/guard/mod.rs +++ b/actix-web/src/guard/mod.rs @@ -11,7 +11,7 @@ //! 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 -//! 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 //! services multiple times (which might have different combining behavior than you want). //! @@ -50,7 +50,6 @@ //! [`Route`]: crate::Route::guard() use std::{ - any::Any, cell::{Ref, RefMut}, rc::Rc, }; @@ -67,6 +66,17 @@ pub use self::{ 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), + /// Detail associated with headers (header, value). + Headers(Vec<(String, String)>), + /// Generic detail. + Generic(String), +} + /// Provides access to request parts that are useful during routing. #[derive(Debug)] pub struct GuardContext<'a> { @@ -122,15 +132,31 @@ impl<'a> GuardContext<'a> { /// Interface for routing guards. /// /// 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. fn check(&self, ctx: &GuardContext<'_>) -> bool; + + /// Returns a nominal representation of the guard. + fn name(&self) -> String { + std::any::type_name::().to_string() + } + + /// Returns detailed introspection information. + fn details(&self) -> Option> { + None + } } impl Guard for Rc { fn check(&self, ctx: &GuardContext<'_>) -> bool { (**self).check(ctx) } + fn name(&self) -> String { + (**self).name() + } + fn details(&self) -> Option> { + (**self).details() + } } /// Creates a guard using the given function. @@ -147,7 +173,7 @@ impl Guard for Rc { /// ``` pub fn fn_guard(f: F) -> impl Guard where - F: Fn(&GuardContext<'_>) -> bool + 'static, + F: Fn(&GuardContext<'_>) -> bool, { FnGuard(f) } @@ -156,7 +182,7 @@ struct FnGuard) -> bool>(F); impl Guard for FnGuard where - F: Fn(&GuardContext<'_>) -> bool + 'static, + F: Fn(&GuardContext<'_>) -> bool, { fn check(&self, ctx: &GuardContext<'_>) -> bool { (self.0)(ctx) @@ -165,7 +191,7 @@ where impl Guard for F where - F: for<'a> Fn(&'a GuardContext<'a>) -> bool + 'static, + F: Fn(&GuardContext<'_>) -> bool, { fn check(&self, ctx: &GuardContext<'_>) -> bool { (self)(ctx) @@ -220,6 +246,24 @@ impl Guard for AnyGuard { false } + fn name(&self) -> String { + format!( + "AnyGuard({})", + self.guards + .iter() + .map(|g| g.name()) + .collect::>() + .join(", ") + ) + } + fn details(&self) -> Option> { + Some( + self.guards + .iter() + .flat_map(|g| g.details().unwrap_or_default()) + .collect(), + ) + } } /// Creates a guard that matches if all added guards match. @@ -248,7 +292,7 @@ pub fn All(guard: F) -> AllGuard { /// /// 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 { guards: Vec>, } @@ -272,6 +316,24 @@ impl Guard for AllGuard { true } + fn name(&self) -> String { + format!( + "AllGuard({})", + self.guards + .iter() + .map(|g| g.name()) + .collect::>() + .join(", ") + ) + } + fn details(&self) -> Option> { + Some( + self.guards + .iter() + .flat_map(|g| g.details().unwrap_or_default()) + .collect(), + ) + } } /// Wraps a guard and inverts the outcome of its `Guard` implementation. @@ -285,13 +347,19 @@ impl Guard for AllGuard { /// .guard(guard::Not(guard::Get())) /// .to(|| HttpResponse::Ok()); /// ``` -pub struct Not(pub G); +pub struct Not(pub G); impl Guard for Not { #[inline] fn check(&self, ctx: &GuardContext<'_>) -> bool { !self.0.check(ctx) } + fn name(&self) -> String { + format!("Not({})", self.0.name()) + } + fn details(&self) -> Option> { + self.0.details() + } } /// Creates a guard that matches a specified HTTP method. @@ -321,6 +389,12 @@ impl Guard for MethodGuard { ctx.head().method == self.0 } + fn name(&self) -> String { + self.0.to_string() + } + fn details(&self) -> Option> { + Some(vec![GuardDetail::HttpMethods(vec![self.0.to_string()])]) + } } #[cfg(feature = "resources-introspection")] @@ -331,70 +405,24 @@ pub trait HttpMethodsExtractor { #[cfg(feature = "resources-introspection")] impl HttpMethodsExtractor for dyn Guard { fn extract_http_methods(&self) -> Vec { - if let Some(method_guard) = self.as_any().downcast_ref::() { - vec![method_guard.0.to_string()] - } else if let Some(any_guard) = self.as_any().downcast_ref::() { - any_guard - .guards - .iter() - .flat_map(|g| g.extract_http_methods()) - .collect() - } else if let Some(all_guard) = self.as_any().downcast_ref::() { - 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 = self - .guards + .details() + .unwrap_or_default() .iter() - .filter_map(|guard| { - guard - .as_any() - .downcast_ref::() - .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 = self - .guards - .iter() - .map(|guard| { - let guard_ref = &**guard; - if let Some(method_guard) = guard_ref.as_any().downcast_ref::() { - method_guard.0.to_string() - } else if let Some(all_guard) = guard_ref.as_any().downcast_ref::() { - all_guard.to_string() + .flat_map(|detail| { + if let GuardDetail::HttpMethods(methods) = detail { + methods.clone() } else { - "UNKNOWN".to_string() + vec!["UNKNOWN".to_string()] } }) .collect(); - write!(f, "[{}]", methods.join(", ")) + if methods.is_empty() { + vec!["UNKNOWN".to_string()] + } else { + methods + } } } @@ -458,15 +486,14 @@ impl Guard for HeaderGuard { false } -} - -pub trait AsAny { - fn as_any(&self) -> &dyn Any; -} - -impl AsAny for T { - fn as_any(&self) -> &dyn Any { - self + fn name(&self) -> String { + format!("Header({}, {})", self.0, self.1.to_str().unwrap_or("")) + } + fn details(&self) -> Option> { + Some(vec![GuardDetail::Headers(vec![( + self.0.to_string(), + self.1.to_str().unwrap_or("").to_string(), + )])]) } } @@ -581,7 +608,7 @@ mod tests { #[test] fn function_guard() { 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() .uri("blog.rust-lang.org") From aebab17c1ecee50f8dc6f65b93258189538630ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= <1295883+dertin@users.noreply.github.com> Date: Wed, 5 Mar 2025 02:34:39 -0300 Subject: [PATCH 12/18] 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`. --- actix-web/src/guard/mod.rs | 177 +++++++++++++++++++++---------------- 1 file changed, 102 insertions(+), 75 deletions(-) diff --git a/actix-web/src/guard/mod.rs b/actix-web/src/guard/mod.rs index 8f1021f76..acdd86e81 100644 --- a/actix-web/src/guard/mod.rs +++ b/actix-web/src/guard/mod.rs @@ -11,7 +11,7 @@ //! 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 -//! 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 //! services multiple times (which might have different combining behavior than you want). //! @@ -50,7 +50,6 @@ //! [`Route`]: crate::Route::guard() use std::{ - any::Any, cell::{Ref, RefMut}, rc::Rc, }; @@ -67,6 +66,17 @@ pub use self::{ 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), + /// Detail associated with headers (header, value). + Headers(Vec<(String, String)>), + /// Generic detail. + Generic(String), +} + /// Provides access to request parts that are useful during routing. #[derive(Debug)] pub struct GuardContext<'a> { @@ -122,15 +132,31 @@ impl<'a> GuardContext<'a> { /// Interface for routing guards. /// /// 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. fn check(&self, ctx: &GuardContext<'_>) -> bool; + + /// Returns a nominal representation of the guard. + fn name(&self) -> String { + std::any::type_name::().to_string() + } + + /// Returns detailed introspection information. + fn details(&self) -> Option> { + None + } } impl Guard for Rc { fn check(&self, ctx: &GuardContext<'_>) -> bool { (**self).check(ctx) } + fn name(&self) -> String { + (**self).name() + } + fn details(&self) -> Option> { + (**self).details() + } } /// Creates a guard using the given function. @@ -147,7 +173,7 @@ impl Guard for Rc { /// ``` pub fn fn_guard(f: F) -> impl Guard where - F: Fn(&GuardContext<'_>) -> bool + 'static, + F: Fn(&GuardContext<'_>) -> bool, { FnGuard(f) } @@ -156,7 +182,7 @@ struct FnGuard) -> bool>(F); impl Guard for FnGuard where - F: Fn(&GuardContext<'_>) -> bool + 'static, + F: Fn(&GuardContext<'_>) -> bool, { fn check(&self, ctx: &GuardContext<'_>) -> bool { (self.0)(ctx) @@ -165,7 +191,7 @@ where impl Guard for F where - F: for<'a> Fn(&'a GuardContext<'a>) -> bool + 'static, + F: Fn(&GuardContext<'_>) -> bool, { fn check(&self, ctx: &GuardContext<'_>) -> bool { (self)(ctx) @@ -220,6 +246,24 @@ impl Guard for AnyGuard { false } + fn name(&self) -> String { + format!( + "AnyGuard({})", + self.guards + .iter() + .map(|g| g.name()) + .collect::>() + .join(", ") + ) + } + fn details(&self) -> Option> { + Some( + self.guards + .iter() + .flat_map(|g| g.details().unwrap_or_default()) + .collect(), + ) + } } /// Creates a guard that matches if all added guards match. @@ -248,7 +292,7 @@ pub fn All(guard: F) -> AllGuard { /// /// 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 { guards: Vec>, } @@ -272,6 +316,24 @@ impl Guard for AllGuard { true } + fn name(&self) -> String { + format!( + "AllGuard({})", + self.guards + .iter() + .map(|g| g.name()) + .collect::>() + .join(", ") + ) + } + fn details(&self) -> Option> { + Some( + self.guards + .iter() + .flat_map(|g| g.details().unwrap_or_default()) + .collect(), + ) + } } /// Wraps a guard and inverts the outcome of its `Guard` implementation. @@ -285,13 +347,19 @@ impl Guard for AllGuard { /// .guard(guard::Not(guard::Get())) /// .to(|| HttpResponse::Ok()); /// ``` -pub struct Not(pub G); +pub struct Not(pub G); impl Guard for Not { #[inline] fn check(&self, ctx: &GuardContext<'_>) -> bool { !self.0.check(ctx) } + fn name(&self) -> String { + format!("Not({})", self.0.name()) + } + fn details(&self) -> Option> { + self.0.details() + } } /// Creates a guard that matches a specified HTTP method. @@ -321,6 +389,12 @@ impl Guard for MethodGuard { ctx.head().method == self.0 } + fn name(&self) -> String { + self.0.to_string() + } + fn details(&self) -> Option> { + Some(vec![GuardDetail::HttpMethods(vec![self.0.to_string()])]) + } } #[cfg(feature = "resources-introspection")] @@ -331,70 +405,24 @@ pub trait HttpMethodsExtractor { #[cfg(feature = "resources-introspection")] impl HttpMethodsExtractor for dyn Guard { fn extract_http_methods(&self) -> Vec { - if let Some(method_guard) = self.as_any().downcast_ref::() { - vec![method_guard.0.to_string()] - } else if let Some(any_guard) = self.as_any().downcast_ref::() { - any_guard - .guards - .iter() - .flat_map(|g| g.extract_http_methods()) - .collect() - } else if let Some(all_guard) = self.as_any().downcast_ref::() { - 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 = self - .guards + .details() + .unwrap_or_default() .iter() - .filter_map(|guard| { - guard - .as_any() - .downcast_ref::() - .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 = self - .guards - .iter() - .map(|guard| { - let guard_ref = &**guard; - if let Some(method_guard) = guard_ref.as_any().downcast_ref::() { - method_guard.0.to_string() - } else if let Some(all_guard) = guard_ref.as_any().downcast_ref::() { - all_guard.to_string() + .flat_map(|detail| { + if let GuardDetail::HttpMethods(methods) = detail { + methods.clone() } else { - "UNKNOWN".to_string() + vec!["UNKNOWN".to_string()] } }) .collect(); - write!(f, "[{}]", methods.join(", ")) + if methods.is_empty() { + vec!["UNKNOWN".to_string()] + } else { + methods + } } } @@ -458,15 +486,14 @@ impl Guard for HeaderGuard { false } -} - -pub trait AsAny { - fn as_any(&self) -> &dyn Any; -} - -impl AsAny for T { - fn as_any(&self) -> &dyn Any { - self + fn name(&self) -> String { + format!("Header({}, {})", self.0, self.1.to_str().unwrap_or("")) + } + fn details(&self) -> Option> { + Some(vec![GuardDetail::Headers(vec![( + self.0.to_string(), + self.1.to_str().unwrap_or("").to_string(), + )])]) } } @@ -581,7 +608,7 @@ mod tests { #[test] fn function_guard() { 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() .uri("blog.rust-lang.org") From d5011026103f8df456382d1e3cdb97866a762d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= Date: Mon, 12 May 2025 02:29:11 -0300 Subject: [PATCH 13/18] feat(introspection): rename feature from `resources-introspection` to `experimental-introspection` - Refactored introspection logic. - Enhanced route introspection to register HTTP methods and guard names. - Added example for testing the experimental introspection feature. --- Cargo.lock | 443 +++++++++++---------- actix-web/CHANGES.md | 2 +- actix-web/Cargo.toml | 2 +- actix-web/examples/introspection.rs | 206 ++++++++++ actix-web/src/app_service.rs | 56 +-- actix-web/src/guard/mod.rs | 29 -- actix-web/src/introspection.rs | 598 +++++++--------------------- actix-web/src/lib.rs | 2 +- actix-web/src/resource.rs | 45 +-- actix-web/src/rmap.rs | 43 +- actix-web/src/route.rs | 29 +- actix-web/src/scope.rs | 54 +-- 12 files changed, 697 insertions(+), 812 deletions(-) create mode 100644 actix-web/examples/introspection.rs diff --git a/Cargo.lock b/Cargo.lock index 96cd56efb..9722accfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,7 +112,7 @@ dependencies = [ "rand 0.9.1", "rcgen", "regex", - "rustls 0.23.25", + "rustls 0.23.27", "rustls-pemfile", "rustversion", "serde", @@ -251,9 +251,9 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.5.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6398974fd4284f4768af07965701efbbb5fdc0616bff20cade1bb14b77675e24" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" dependencies = [ "actix-rt", "actix-service", @@ -296,7 +296,7 @@ dependencies = [ "rustls 0.20.9", "rustls 0.21.12", "rustls 0.22.4", - "rustls 0.23.25", + "rustls 0.23.27", "serde", "serde_json", "serde_urlencoded", @@ -330,7 +330,7 @@ dependencies = [ "tracing", "webpki-roots 0.22.6", "webpki-roots 0.25.4", - "webpki-roots 0.26.8", + "webpki-roots 0.26.11", ] [[package]] @@ -387,7 +387,7 @@ dependencies = [ "rcgen", "regex", "regex-lite", - "rustls 0.23.25", + "rustls 0.23.27", "rustls-pemfile", "serde", "serde_json", @@ -655,7 +655,7 @@ dependencies = [ "rustls 0.20.9", "rustls 0.21.12", "rustls 0.22.4", - "rustls 0.23.25", + "rustls 0.23.27", "rustls-pemfile", "serde", "serde_json", @@ -668,9 +668,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878" +checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" dependencies = [ "aws-lc-sys", "zeroize", @@ -678,9 +678,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f7720b74ed28ca77f90769a71fd8c637a0137f6fae4ae947e1050229cff57f" +checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" dependencies = [ "bindgen", "cc", @@ -691,9 +691,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -773,9 +773,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.2" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" +checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -816,9 +816,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.18" +version = "1.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" +checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" dependencies = [ "jobserver", "libc", @@ -890,18 +890,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.35" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" +checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.35" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" +checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" dependencies = [ "anstyle", "clap_lex", @@ -1153,9 +1153,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "deranged" @@ -1211,9 +1211,9 @@ dependencies = [ [[package]] name = "divan" -version = "0.1.18" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009c56317fe2bd3b5eebe3aa888828c62ed1b085d26c1ef2079a60369795765e" +checksum = "a405457ec78b8fe08b0e32b4a3570ab5dff6dd16eb9e76a5ee0a9d9cbd898933" dependencies = [ "cfg-if", "clap", @@ -1225,9 +1225,9 @@ dependencies = [ [[package]] name = "divan-macros" -version = "0.1.18" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4de9827ae754db91aedec0277381f5a2d8e2f801564c8d774acfe1ab1b045e" +checksum = "9556bc800956545d6420a640173e5ba7dfa82f38d3ea5a167eb555bc69ac3323" dependencies = [ "proc-macro2", "quote", @@ -1463,9 +1463,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -1474,9 +1474,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", @@ -1537,9 +1537,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" [[package]] name = "heck" @@ -1555,9 +1555,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hermit-abi" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" [[package]] name = "hkdf" @@ -1586,17 +1586,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "hostname" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" -dependencies = [ - "cfg-if", - "libc", - "windows-link", -] - [[package]] name = "http" version = "0.2.12" @@ -1639,21 +1628,22 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -1662,31 +1652,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -1694,67 +1664,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -1784,9 +1741,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1851,7 +1808,7 @@ version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.5.0", + "hermit-abi 0.5.1", "libc", "windows-sys 0.59.0", ] @@ -1888,9 +1845,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.6" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f33145a5cbea837164362c7bd596106eb7c5198f97d1ba6f6ebb3223952e488" +checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" dependencies = [ "jiff-static", "log", @@ -1901,9 +1858,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.6" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43ce13c40ec6956157a3635d97a1ee2df323b263f09ea14165131289cb0f5c19" +checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48" dependencies = [ "proc-macro2", "quote", @@ -1916,7 +1873,7 @@ version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", "libc", ] @@ -1950,18 +1907,18 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.171" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.0", ] [[package]] @@ -1984,9 +1941,9 @@ checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "local-channel" @@ -2192,9 +2149,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.107" +version = "0.9.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" dependencies = [ "cc", "libc", @@ -2334,6 +2291,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2430,7 +2396,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -2439,7 +2405,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", ] [[package]] @@ -2477,9 +2443,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ "bitflags 2.9.0", ] @@ -2521,12 +2487,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "resolv-conf" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48375394603e3dd4b2d64371f7148fd8c7baa2680e28741f2cb8d23b59e3d4c4" -dependencies = [ - "hostname", -] +checksum = "fc7c8f7f733062b66dc1c63f9db168ac0b97a9210e247fa90fdc9ad08f51b302" [[package]] name = "ring" @@ -2551,7 +2514,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted 0.9.0", "windows-sys 0.52.0", @@ -2584,9 +2547,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.5" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ "bitflags 2.9.0", "errno", @@ -2635,15 +2598,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.25" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "aws-lc-rs", "log", "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.1", + "rustls-webpki 0.103.3", "subtle", "zeroize", ] @@ -2672,9 +2635,12 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] [[package]] name = "rustls-webpki" @@ -2699,9 +2665,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.1" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "aws-lc-rs", "ring 0.17.14", @@ -2859,9 +2825,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -2876,9 +2842,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -2956,9 +2922,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -2967,9 +2933,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -2984,14 +2950,14 @@ checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790" [[package]] name = "tempfile" -version = "3.19.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.3", "once_cell", - "rustix 1.0.5", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -3010,7 +2976,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "rustix 1.0.5", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -3067,9 +3033,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -3102,9 +3068,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.2" +version = "1.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" dependencies = [ "backtrace", "bytes", @@ -3178,7 +3144,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.25", + "rustls 0.23.27", "tokio", ] @@ -3210,9 +3176,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -3223,9 +3189,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" dependencies = [ "serde", "serde_spanned", @@ -3235,26 +3201,33 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + [[package]] name = "tracing" version = "0.1.41" @@ -3335,9 +3308,9 @@ dependencies = [ [[package]] name = "trybuild" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ae08be68c056db96f0e6c6dd820727cca756ced9e1f4cc7fdd20e2a55e23898" +checksum = "1c9bf9513a2f4aeef5fdac8677d7d349c79fdbcc03b9c86da6e9d254f1e43be2" dependencies = [ "glob", "serde", @@ -3420,12 +3393,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -3576,9 +3543,18 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.8" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.0", +] + +[[package]] +name = "webpki-roots" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" dependencies = [ "rustls-pki-types", ] @@ -3632,12 +3608,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-link" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" - [[package]] name = "windows-sys" version = "0.48.0" @@ -3689,13 +3659,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3708,6 +3694,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3720,6 +3712,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3732,12 +3730,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3750,6 +3760,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3762,6 +3778,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3774,6 +3796,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3787,10 +3815,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "0.7.6" +name = "windows_x86_64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] @@ -3814,17 +3848,11 @@ dependencies = [ "bitflags 2.9.0", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "yasna" @@ -3837,9 +3865,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -3849,9 +3877,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", @@ -3861,18 +3889,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", @@ -3907,10 +3935,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] -name = "zerovec" -version = "0.10.4" +name = "zerotrie" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -3919,9 +3958,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index 4ae5a09d3..f45604b79 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -2,7 +2,7 @@ ## Unreleased -- Add `resources-introspection` feature for retrieving configured route paths and HTTP methods. +- Add `experimental-introspection` feature for retrieving configured route paths and HTTP methods. ## 4.10.2 diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index 76ecffd38..39b3f8e62 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -129,7 +129,7 @@ compat = [ 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 = [] +experimental-introspection = [] [dependencies] actix-codec = "0.5" diff --git a/actix-web/examples/introspection.rs b/actix-web/examples/introspection.rs new file mode 100644 index 000000000..1de9cfec6 --- /dev/null +++ b/actix-web/examples/introspection.rs @@ -0,0 +1,206 @@ +// NOTE: This is a work-in-progress example being used to test the new implementation +// of the experimental introspection feature. +// `cargo run --features experimental-introspection --example introspection` + +use actix_web::{dev::Service, guard, web, App, HttpResponse, HttpServer, Responder}; +use serde::Deserialize; +// Custom guard that checks if the Content-Type header is present. +struct ContentTypeGuard; + +impl guard::Guard for ContentTypeGuard { + fn check(&self, req: &guard::GuardContext<'_>) -> bool { + req.head() + .headers() + .contains_key(actix_web::http::header::CONTENT_TYPE) + } +} + +// Data structure for endpoints that receive JSON. +#[derive(Deserialize)] +struct UserInfo { + username: String, + age: u8, +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let server = HttpServer::new(|| { + let app = App::new() + .service( + web::scope("/api") + .service( + web::scope("/v1") + // GET /api/v1/item/{id}: returns the item id from the path. + .service(get_item) + // POST /api/v1/info: accepts JSON and returns user info. + .service(post_user_info) + // /api/v1/guarded: only accessible if Content-Type header is present. + .route( + "/guarded", + web::route().guard(ContentTypeGuard).to(guarded_handler), + ), + ) + // API scope /api/v2: additional endpoint. + .service(web::scope("/v2").route("/hello", web::get().to(hello_v2))), + ) + // Scope /v1 outside /api: exposes only GET /v1/item/{id}. + .service(web::scope("/v1").service(get_item)) + // Scope /admin: admin endpoints with different HTTP methods. + .service( + web::scope("/admin") + .route("/dashboard", web::get().to(admin_dashboard)) + // Single route handling multiple methods using separate handlers. + .service( + web::resource("/settings") + .route(web::get().to(get_settings)) + .route(web::post().to(update_settings)), + ), + ) + // Root resource: supports GET and POST on "/". + .service( + web::resource("/") + .route(web::get().to(root_index)) + .route(web::post().to(root_index)), + ) + // Additional endpoints configured in a separate function. + .configure(extra_endpoints) + // Endpoint that rejects GET on /not_guard (allows other methods). + .route( + "/not_guard", + web::route() + .guard(guard::Not(guard::Get())) + .to(HttpResponse::MethodNotAllowed), + ) + // Endpoint that requires GET, content-type: plain/text header, and/or POST on /all_guard. + .route( + "/all_guard", + web::route() + .guard( + guard::All(guard::Get()) + .and(guard::Header("content-type", "plain/text")) + .and(guard::Any(guard::Post())), + ) + .to(HttpResponse::MethodNotAllowed), + ); + + /*#[cfg(feature = "experimental-introspection")] + { + actix_web::introspection::introspect(); + }*/ + // TODO: Enable introspection without the feature flag. + app + }) + .workers(5) + .bind("127.0.0.1:8080")?; + + server.run().await +} + +// GET /api/v1/item/{id} and GET /v1/item/{id} +// Returns a message with the provided id. +#[actix_web::get("/item/{id:\\d+}")] +async fn get_item(path: web::Path) -> impl Responder { + let id = path.into_inner(); + HttpResponse::Ok().body(format!("Requested item with id: {}", id)) +} + +// POST /api/v1/info +// Expects JSON and responds with the received user info. +#[actix_web::post("/info")] +async fn post_user_info(info: web::Json) -> impl Responder { + HttpResponse::Ok().json(format!( + "User {} with age {} received", + info.username, info.age + )) +} + +// /api/v1/guarded +// Uses a custom guard that requires the Content-Type header. +async fn guarded_handler() -> impl Responder { + HttpResponse::Ok().body("Passed the Content-Type guard!") +} + +// GET /api/v2/hello +// Simple greeting endpoint. +async fn hello_v2() -> impl Responder { + HttpResponse::Ok().body("Hello from API v2!") +} + +// GET /admin/dashboard +// Returns a message for the admin dashboard. +async fn admin_dashboard() -> impl Responder { + HttpResponse::Ok().body("Welcome to the Admin Dashboard!") +} + +// GET /admin/settings +// Returns the current admin settings. +async fn get_settings() -> impl Responder { + HttpResponse::Ok().body("Current settings: ...") +} + +// POST /admin/settings +// Updates the admin settings. +async fn update_settings() -> impl Responder { + HttpResponse::Ok().body("Settings have been updated!") +} + +// GET and POST on / +// Generic root endpoint. +async fn root_index() -> impl Responder { + HttpResponse::Ok().body("Welcome to the Root Endpoint!") +} + +// Additional endpoints configured in a separate function. +fn extra_endpoints(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/extra") + // GET /extra/ping: simple ping endpoint. + .route( + "/ping", + web::get().to(|| async { HttpResponse::Ok().body("pong") }), + ) + // /extra/multi: resource that supports GET and POST. + .service( + web::resource("/multi") + .route( + web::get().to(|| async { + HttpResponse::Ok().body("GET response from /extra/multi") + }), + ) + .route(web::post().to(|| async { + HttpResponse::Ok().body("POST response from /extra/multi") + })), + ) + // /extra/{entities_id}/secure: nested scope with GET and POST, prints the received id. + .service( + web::scope("{entities_id:\\d+}") + .service( + web::scope("/secure") + .route( + "", + web::get().to(|| async { + HttpResponse::Ok().body("GET response from /extra/secure") + }), + ) + .route( + "", + web::post().to(|| async { + HttpResponse::Ok().body("POST response from /extra/secure") + }), + ), + ) + // Middleware that prints the id received in the route. + .wrap_fn(|req, srv| { + println!( + "Request to /extra/secure with id: {}", + req.match_info().get("entities_id").unwrap() + ); + let fut = srv.call(req); + async move { + let res = fut.await?; + Ok(res) + } + }), + ), + ); +} diff --git a/actix-web/src/app_service.rs b/actix-web/src/app_service.rs index a935c1354..dd76c3d2d 100644 --- a/actix-web/src/app_service.rs +++ b/actix-web/src/app_service.rs @@ -82,9 +82,6 @@ where let (config, services) = config.into_services(); - #[cfg(feature = "resources-introspection")] - let mut rdef_methods: Vec<(String, Vec)> = Vec::new(); - // complete pipeline creation. *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { default, @@ -92,24 +89,35 @@ where .into_iter() .map(|(mut rdef, srv, guards, nested)| { rmap.add(&mut rdef, nested); - - #[cfg(feature = "resources-introspection")] + #[cfg(feature = "experimental-introspection")] { - let http_methods: Vec = - guards.as_ref().map_or_else(Vec::new, |g| { - g.iter() - .flat_map(|g| { - crate::guard::HttpMethodsExtractor::extract_http_methods( - &**g, - ) - }) - .collect::>() - }); - - rdef_methods - .push((rdef.pattern().unwrap_or_default().to_string(), http_methods)); + use std::borrow::Borrow; + let pat = rdef.pattern().unwrap_or("").to_string(); + let mut methods = Vec::new(); + let mut guard_names = Vec::new(); + if let Some(gs) = guards.borrow().as_ref() { + for g in gs.iter() { + let name = g.name().to_string(); + if !guard_names.contains(&name) { + guard_names.push(name.clone()); + } + if let Some(details) = g.details() { + for d in details { + if let crate::guard::GuardDetail::HttpMethods(v) = d { + for s in v { + if let Ok(m) = s.parse() { + if !methods.contains(&m) { + methods.push(m); + } + } + } + } + } + } + } + } + crate::introspection::register_pattern_detail(pat, methods, guard_names); } - (rdef, srv, RefCell::new(guards)) }) .collect::>() @@ -126,11 +134,6 @@ 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())); @@ -156,6 +159,11 @@ where factory.create(&mut app_data); } + #[cfg(feature = "experimental-introspection")] + { + crate::introspection::register_rmap(&rmap); + } + Ok(AppInitService { service, app_data: Rc::new(app_data), diff --git a/actix-web/src/guard/mod.rs b/actix-web/src/guard/mod.rs index acdd86e81..97b0f80b6 100644 --- a/actix-web/src/guard/mod.rs +++ b/actix-web/src/guard/mod.rs @@ -397,35 +397,6 @@ impl Guard for MethodGuard { } } -#[cfg(feature = "resources-introspection")] -pub trait HttpMethodsExtractor { - fn extract_http_methods(&self) -> Vec; -} - -#[cfg(feature = "resources-introspection")] -impl HttpMethodsExtractor for dyn Guard { - fn extract_http_methods(&self) -> Vec { - let methods: Vec = self - .details() - .unwrap_or_default() - .iter() - .flat_map(|detail| { - if let GuardDetail::HttpMethods(methods) = detail { - methods.clone() - } else { - vec!["UNKNOWN".to_string()] - } - }) - .collect(); - - if methods.is_empty() { - vec!["UNKNOWN".to_string()] - } else { - methods - } - } -} - macro_rules! method_guard { ($method_fn:ident, $method_const:ident) => { #[doc = concat!("Creates a guard that matches the `", stringify!($method_const), "` request method.")] diff --git a/actix-web/src/introspection.rs b/actix-web/src/introspection.rs index 0059ed215..71e61eccd 100644 --- a/actix-web/src/introspection.rs +++ b/actix-web/src/introspection.rs @@ -1,494 +1,172 @@ use std::{ - rc::Rc, - sync::{OnceLock, RwLock}, + collections::HashMap, + sync::{ + atomic::{AtomicBool, Ordering}, + Mutex, OnceLock, + }, thread, }; -use crate::rmap::ResourceMap; +use crate::{http::Method, rmap::ResourceMap}; -/// Represents an HTTP resource registered for introspection. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ResourceIntrospection { - /// HTTP method (e.g., "GET"). - pub method: String, - /// Route path (e.g., "/api/v1/test"). - pub path: String, -} - -/// A global registry of listed resources for introspection. -/// Only the designated thread can modify it. -static RESOURCE_REGISTRY: RwLock> = RwLock::new(Vec::new()); - -/// Stores the thread ID of the designated thread (the first to call `process_introspection`). -/// Any other thread will immediately return without updating the global registry. +static REGISTRY: OnceLock> = OnceLock::new(); +static DETAIL_REGISTRY: OnceLock>> = OnceLock::new(); static DESIGNATED_THREAD: OnceLock = OnceLock::new(); +static IS_INITIALIZED: AtomicBool = AtomicBool::new(false); -/// Inserts a resource into the global registry, avoiding duplicates. -pub fn register_resource(resource: ResourceIntrospection) { - let mut global = RESOURCE_REGISTRY.write().unwrap(); - if !global.iter().any(|r| r == &resource) { - global.push(resource); - } +pub fn initialize_registry() { + REGISTRY.get_or_init(|| Mutex::new(IntrospectionNode::new(ResourceType::App, "".into()))); } -/// Completes (updates) partial routes in the global registry whose path contains `marker`, -/// by applying the specified `prefix`. -pub fn complete_partial_routes_with_marker(marker: &str, prefix: &str) { - let mut global = RESOURCE_REGISTRY.write().unwrap(); +pub fn get_registry() -> &'static Mutex { + REGISTRY.get().expect("Registry not initialized") +} - let mut updated = Vec::new(); - let mut remaining = Vec::new(); +pub fn initialize_detail_registry() { + DETAIL_REGISTRY.get_or_init(|| Mutex::new(HashMap::new())); +} - // Move all items out of the current registry. - for resource in global.drain(..) { - if resource.path.contains(marker) { - // Build the full path by applying the prefix if needed. - 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) - }; +pub fn get_detail_registry() -> &'static Mutex> { + DETAIL_REGISTRY + .get() + .expect("Detail registry not initialized") +} - let completed = ResourceIntrospection { - method: resource.method, - path: full_path, - }; +#[derive(Clone)] +pub struct RouteDetail { + methods: Vec, + guards: Vec, +} - // Add to `updated` if it's not already in there. - if !updated.iter().any(|r| r == &completed) { - updated.push(completed); - } +#[derive(Debug, Clone, Copy)] +pub enum ResourceType { + App, + Scope, + Resource, +} + +#[derive(Debug, Clone)] +pub struct IntrospectionNode { + pub kind: ResourceType, + pub pattern: String, + pub methods: Vec, + pub guards: Vec, + pub children: Vec, +} + +impl IntrospectionNode { + pub fn new(kind: ResourceType, pattern: String) -> Self { + IntrospectionNode { + kind, + pattern, + methods: Vec::new(), + guards: Vec::new(), + children: Vec::new(), + } + } + + pub fn display(&self, indent: usize, parent_path: &str) { + let full_path = if parent_path.is_empty() { + self.pattern.clone() } else { - // Keep this resource as-is. - remaining.push(resource); - } - } + format!( + "{}/{}", + parent_path.trim_end_matches('/'), + self.pattern.trim_start_matches('/') + ) + }; - // Merge updated items back with the remaining ones. - remaining.extend(updated); - *global = remaining; -} - -/// Returns a **copy** of the global registry (safe to call from any thread). -pub fn get_registered_resources() -> Vec { - RESOURCE_REGISTRY.read().unwrap().clone() -} - -/// Processes introspection data for routes and methods. -/// Only the **first thread** that calls this function (the "designated" one) may update -/// the global resource registry. Any other thread will immediately return without updating it. -/// -/// # Parameters -/// - `rmap`: A resource map convertible to a vector of route strings. -/// - `rdef_methods`: A vector of `(sub_path, [methods])`. -/// - A tuple with an **empty** methods vector is treated as a "marker" (a partial route) -/// for which we try to deduce a prefix by finding `sub_path` in a route, then calling -/// `complete_partial_routes_with_marker`. -/// - A tuple with one or more methods registers a resource with `register_resource`. -pub fn process_introspection(rmap: Rc, rdef_methods: Vec<(String, Vec)>) { - // Determine the designated thread: if none is set yet, assign the current thread's ID. - // This ensures that the first thread to call this function becomes the designated thread. - let current_id = thread::current().id(); - DESIGNATED_THREAD.get_or_init(|| current_id); - - // If the current thread is not the designated one, return immediately. - // This ensures that only the designated thread updates the global registry, - // avoiding any interleaving or inconsistent updates from other threads. - if *DESIGNATED_THREAD.get().unwrap() != current_id { - return; - } - - let rmap_vec = rmap.to_vec(); - - // If there is no data, nothing to process. - // Avoid unnecessary work. - if rmap_vec.is_empty() && rdef_methods.is_empty() { - return; - } - - // Keep track of the deduced prefix for partial routes. - let mut deduced_prefix: Option = None; - - // 1. Handle "marker" entries (where methods is empty). - for (sub_path, http_methods) in &rdef_methods { - if http_methods.is_empty() { - // Find any route that contains sub_path and use it to deduce a prefix. - if let Some(route) = rmap_vec.iter().find(|r| r.contains(sub_path)) { - if let Some(pos) = route.find(sub_path) { - let prefix = &route[..pos]; - deduced_prefix = Some(prefix.to_string()); - // Complete partial routes in the global registry using this prefix. - complete_partial_routes_with_marker(sub_path, prefix); - } - } - } - } - - // 2. Handle endpoint entries (where methods is non-empty). - for (sub_path, http_methods) in &rdef_methods { - if !http_methods.is_empty() { - // Identify candidate routes that end with sub_path (or exactly match "/" if sub_path == "/"). - let candidates: Vec<&String> = if sub_path == "/" { - rmap_vec.iter().filter(|r| r.as_str() == "/").collect() + if !self.methods.is_empty() || !self.guards.is_empty() { + let methods = if self.methods.is_empty() { + "".to_string() } else { - rmap_vec.iter().filter(|r| r.ends_with(sub_path)).collect() + format!(" Methods: {:?}", self.methods) + }; + let guards = if self.guards.is_empty() { + "".to_string() + } else { + format!(" Guards: {:?}", self.guards) }; - // If we found any candidates, pick the best match. - 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 { - candidates.iter().min_by_key(|&&r| r.len()).cloned() - } - } else { - candidates.iter().min_by_key(|&&r| r.len()).cloned() - }; + println!("{}{}{}{}", " ".repeat(indent), full_path, methods, guards); + } - if let Some(full_route) = chosen { - // Register the endpoint in the global resource registry. - register_resource(ResourceIntrospection { - method: http_methods.join(","), - path: full_route.clone(), - }); + for child in &self.children { + child.display(indent, &full_path); + } + } +} + +fn build_tree(node: &mut IntrospectionNode, rmap: &ResourceMap) { + initialize_detail_registry(); + let detail_registry = get_detail_registry(); + if let Some(ref children) = rmap.nodes { + for child_rc in children { + let child = child_rc; + let pat = child.pattern.pattern().unwrap_or("").to_string(); + let kind = if child.nodes.is_some() { + ResourceType::Scope + } else { + ResourceType::Resource + }; + let mut new_node = IntrospectionNode::new(kind, pat.clone()); + + if let ResourceType::Resource = new_node.kind { + if let Some(d) = detail_registry.lock().unwrap().get(&pat) { + new_node.methods = d.methods.clone(); + new_node.guards = d.guards.clone(); } } + + build_tree(&mut new_node, child); + node.children.push(new_node); } } } -#[cfg(test)] -mod tests { - use std::{num::NonZeroUsize, rc::Rc}; +fn is_designated_thread() -> bool { + let current_id = thread::current().id(); + DESIGNATED_THREAD.get_or_init(|| { + IS_INITIALIZED.store(true, Ordering::SeqCst); + current_id // Assign the first thread that calls this function + }); - use actix_router::ResourceDef; - use tokio::sync::oneshot; + *DESIGNATED_THREAD.get().unwrap() == current_id +} - use super::*; - use crate::rmap::ResourceMap; - - /// Helper function to create a ResourceMap from a list of route strings. - /// It creates a root ResourceMap with an empty prefix and adds each route as a leaf. - fn create_resource_map(routes: Vec<&str>) -> Rc { - // Create a root node with an empty prefix. - let mut root = ResourceMap::new(ResourceDef::root_prefix("")); - // For each route, create a ResourceDef and add it as a leaf (nested = None). - for route in routes { - let mut def = ResourceDef::new(route); - root.add(&mut def, None); - } - Rc::new(root) +pub fn register_rmap(rmap: &ResourceMap) { + if !is_designated_thread() { + return; } - // Helper function to run the full introspection flow. - // It processes introspection data for multiple blocks, each with a different set of routes and methods. - fn run_full_introspection_flow() { - // Block 1: - // rmap_vec: ["/item/{id}"] - // rdef_methods: [] - process_introspection(create_resource_map(vec!["/item/{id}"]), vec![]); + initialize_registry(); + let mut root = IntrospectionNode::new(ResourceType::App, "".into()); + build_tree(&mut root, rmap); + *get_registry().lock().unwrap() = root; - // Block 2: - // rmap_vec: ["/info"] - // rdef_methods: [] - process_introspection(create_resource_map(vec!["/info"]), vec![]); + // WIP. Display the introspection tree + let reg = get_registry().lock().unwrap(); + reg.display(0, ""); +} - // Block 3: - // rmap_vec: ["/guarded"] - // rdef_methods: [] - process_introspection(create_resource_map(vec!["/guarded"]), vec![]); - - // Block 4: - // rmap_vec: ["/v1/item/{id}", "/v1/info", "/v1/guarded"] - // rdef_methods: [("/item/{id}", ["GET"]), ("/info", ["POST"]), ("/guarded", ["UNKNOWN"])] - process_introspection( - create_resource_map(vec!["/v1/item/{id}", "/v1/info", "/v1/guarded"]), - vec![ - ("/item/{id}".to_string(), vec!["GET".to_string()]), - ("/info".to_string(), vec!["POST".to_string()]), - ("/guarded".to_string(), vec!["UNKNOWN".to_string()]), - ], - ); - - // Block 5: - // rmap_vec: ["/hello"] - // rdef_methods: [] - process_introspection(create_resource_map(vec!["/hello"]), vec![]); - - // Block 6: - // rmap_vec: ["/v2/hello"] - // rdef_methods: [("/hello", ["GET"])] - process_introspection( - create_resource_map(vec!["/v2/hello"]), - vec![("/hello".to_string(), vec!["GET".to_string()])], - ); - - // Block 7: - // rmap_vec: ["/api/v1/item/{id}", "/api/v1/info", "/api/v1/guarded", "/api/v2/hello"] - // rdef_methods: [("/v1", []), ("/v2", [])] - process_introspection( - create_resource_map(vec![ - "/api/v1/item/{id}", - "/api/v1/info", - "/api/v1/guarded", - "/api/v2/hello", - ]), - vec![("/v1".to_string(), vec![]), ("/v2".to_string(), vec![])], - ); - - // Block 8: - // rmap_vec: ["/dashboard"] - // rdef_methods: [] - process_introspection(create_resource_map(vec!["/dashboard"]), vec![]); - - // Block 9: - // rmap_vec: ["/settings"] - // rdef_methods: [("/settings", ["GET"]), ("/settings", ["POST"])] - process_introspection( - create_resource_map(vec!["/settings"]), - vec![ - ("/settings".to_string(), vec!["GET".to_string()]), - ("/settings".to_string(), vec!["POST".to_string()]), - ], - ); - - // Block 10: - // rmap_vec: ["/admin/dashboard", "/admin/settings"] - // rdef_methods: [("/dashboard", ["GET"]), ("/settings", [])] - process_introspection( - create_resource_map(vec!["/admin/dashboard", "/admin/settings"]), - vec![ - ("/dashboard".to_string(), vec!["GET".to_string()]), - ("/settings".to_string(), vec![]), - ], - ); - - // Block 11: - // rmap_vec: ["/"] - // rdef_methods: [("/", ["GET"]), ("/", ["POST"])] - process_introspection( - create_resource_map(vec!["/"]), - vec![ - ("/".to_string(), vec!["GET".to_string()]), - ("/".to_string(), vec!["POST".to_string()]), - ], - ); - - // Block 12: - // rmap_vec: ["/ping"] - // rdef_methods: [] - process_introspection(create_resource_map(vec!["/ping"]), vec![]); - - // Block 13: - // rmap_vec: ["/multi"] - // rdef_methods: [("/multi", ["GET"]), ("/multi", ["POST"])] - process_introspection( - create_resource_map(vec!["/multi"]), - vec![ - ("/multi".to_string(), vec!["GET".to_string()]), - ("/multi".to_string(), vec!["POST".to_string()]), - ], - ); - - // Block 14: - // rmap_vec: ["/extra/ping", "/extra/multi"] - // rdef_methods: [("/ping", ["GET"]), ("/multi", [])] - process_introspection( - create_resource_map(vec!["/extra/ping", "/extra/multi"]), - vec![ - ("/ping".to_string(), vec!["GET".to_string()]), - ("/multi".to_string(), vec![]), - ], - ); - - // Block 15: - // rmap_vec: ["/other_guard"] - // rdef_methods: [] - process_introspection(create_resource_map(vec!["/other_guard"]), vec![]); - - // Block 16: - // rmap_vec: ["/all_guard"] - // rdef_methods: [] - process_introspection(create_resource_map(vec!["/all_guard"]), vec![]); - - // Block 17: - // rmap_vec: ["/api/v1/item/{id}", "/api/v1/info", "/api/v1/guarded", "/api/v2/hello", - // "/admin/dashboard", "/admin/settings", "/", "/extra/ping", "/extra/multi", - // "/other_guard", "/all_guard"] - // rdef_methods: [("/api", []), ("/admin", []), ("/", []), ("/extra", []), - // ("/other_guard", ["UNKNOWN"]), ("/all_guard", ["GET", "UNKNOWN", "POST"])] - process_introspection( - create_resource_map(vec![ - "/api/v1/item/{id}", - "/api/v1/info", - "/api/v1/guarded", - "/api/v2/hello", - "/admin/dashboard", - "/admin/settings", - "/", - "/extra/ping", - "/extra/multi", - "/other_guard", - "/all_guard", - ]), - vec![ - ("/api".to_string(), vec![]), - ("/admin".to_string(), vec![]), - ("/".to_string(), vec![]), - ("/extra".to_string(), vec![]), - ("/other_guard".to_string(), vec!["UNKNOWN".to_string()]), - ( - "/all_guard".to_string(), - vec!["GET".to_string(), "UNKNOWN".to_string(), "POST".to_string()], - ), - ], - ); - } - - /// This test spawns multiple tasks that run the full introspection flow concurrently. - /// Only the designated task (the first one to call process_introspection) updates the global registry, - /// ensuring that the internal order remains consistent. Finally, we verify that get_registered_resources() - /// returns the expected set of listed resources. - /// Using a dedicated arbiter for each task ensures that the global registry is thread-safe. - #[actix_rt::test] - async fn test_introspection() { - // Number of tasks to spawn. - const NUM_TASKS: usize = 4; - let mut completion_receivers = Vec::with_capacity(NUM_TASKS); - - // Check that the registry is initially empty. - let registered_resources = get_registered_resources(); - - assert_eq!( - registered_resources.len(), - 0, - "Expected 0 registered resources, found: {:?}", - registered_resources - ); - - // Determine parallelism and max blocking threads. - let parallelism = std::thread::available_parallelism().map_or(2, NonZeroUsize::get); - let max_blocking_threads = std::cmp::max(512 / parallelism, 1); - - // Spawn tasks on the arbiter. Each task runs the full introspection flow and then signals completion. - for _ in 0..NUM_TASKS { - let (tx, rx) = oneshot::channel(); - - #[cfg(all(target_os = "linux", feature = "experimental-io-uring"))] - let arbiter = { - // TODO: pass max blocking thread config when tokio-uring enable configuration - // on building runtime. - let _ = max_blocking_threads; - actix_rt::Arbiter::new() - }; - - #[cfg(not(all(target_os = "linux", feature = "experimental-io-uring")))] - let arbiter = actix_rt::Arbiter::with_tokio_rt(move || { - // Create an Arbiter with a dedicated Tokio runtime. - tokio::runtime::Builder::new_current_thread() - .enable_all() - .max_blocking_threads(max_blocking_threads) - .build() - .unwrap() - }); - - // Spawn the task on the arbiter. - arbiter.spawn(async move { - run_full_introspection_flow(); - // Signal that this task has finished. - let _ = tx.send(()); - }); - completion_receivers.push(rx); - } - - // Wait for all spawned tasks to complete. - for rx in completion_receivers { - let _ = rx.await; - } - - // After all blocks, we expect the final registry to contain 14 entries. - let registered_resources = get_registered_resources(); - - assert_eq!( - registered_resources.len(), - 14, - "Expected 14 registered resources, found: {:?}", - registered_resources - ); - - // List of expected resources - let expected_resources = vec![ - ResourceIntrospection { - method: "GET".to_string(), - path: "/api/v1/item/{id}".to_string(), - }, - ResourceIntrospection { - method: "POST".to_string(), - path: "/api/v1/info".to_string(), - }, - ResourceIntrospection { - method: "UNKNOWN".to_string(), - path: "/api/v1/guarded".to_string(), - }, - ResourceIntrospection { - method: "GET".to_string(), - path: "/api/v2/hello".to_string(), - }, - ResourceIntrospection { - method: "GET".to_string(), - path: "/admin/settings".to_string(), - }, - ResourceIntrospection { - method: "POST".to_string(), - path: "/admin/settings".to_string(), - }, - ResourceIntrospection { - method: "GET".to_string(), - path: "/admin/dashboard".to_string(), - }, - ResourceIntrospection { - method: "GET".to_string(), - path: "/extra/multi".to_string(), - }, - ResourceIntrospection { - method: "POST".to_string(), - path: "/extra/multi".to_string(), - }, - ResourceIntrospection { - method: "GET".to_string(), - path: "/extra/ping".to_string(), - }, - ResourceIntrospection { - method: "GET".to_string(), - path: "/".to_string(), - }, - ResourceIntrospection { - method: "POST".to_string(), - path: "/".to_string(), - }, - ResourceIntrospection { - method: "UNKNOWN".to_string(), - path: "/other_guard".to_string(), - }, - ResourceIntrospection { - method: "GET,UNKNOWN,POST".to_string(), - path: "/all_guard".to_string(), - }, - ]; - - for exp in expected_resources { - assert!( - registered_resources.contains(&exp), - "Expected resource not found: {:?}", - exp - ); +fn update_unique(existing: &mut Vec, new_items: &[T]) { + for item in new_items { + if !existing.contains(item) { + existing.push(item.clone()); } } } + +pub fn register_pattern_detail(pattern: String, methods: Vec, guards: Vec) { + if !is_designated_thread() { + return; + } + initialize_detail_registry(); + let mut reg = get_detail_registry().lock().unwrap(); + reg.entry(pattern) + .and_modify(|d| { + update_unique(&mut d.methods, &methods); + update_unique(&mut d.guards, &guards); + }) + .or_insert(RouteDetail { methods, guards }); +} diff --git a/actix-web/src/lib.rs b/actix-web/src/lib.rs index e26e76243..4dac84962 100644 --- a/actix-web/src/lib.rs +++ b/actix-web/src/lib.rs @@ -108,7 +108,7 @@ mod thin_data; pub(crate) mod types; pub mod web; -#[cfg(feature = "resources-introspection")] +#[cfg(feature = "experimental-introspection")] pub mod introspection; #[doc(inline)] diff --git a/actix-web/src/resource.rs b/actix-web/src/resource.rs index e5016d240..e6f3c0933 100644 --- a/actix-web/src/resource.rs +++ b/actix-web/src/resource.rs @@ -417,6 +417,8 @@ where B: MessageBody + 'static, { fn register(mut self, config: &mut AppService) { + let routes = std::mem::take(&mut self.routes); + let guards = if self.guards.is_empty() { None } else { @@ -433,27 +435,28 @@ where rdef.set_name(name); } - #[cfg(feature = "resources-introspection")] - let mut rdef_methods: Vec<(String, Vec)> = Vec::new(); - #[cfg(feature = "resources-introspection")] - let mut rmap = crate::rmap::ResourceMap::new(ResourceDef::prefix("")); - - #[cfg(feature = "resources-introspection")] + #[cfg(feature = "experimental-introspection")] { - rmap.add(&mut rdef, None); - - self.routes.iter().for_each(|r| { - r.get_guards().iter().for_each(|g| { - let http_methods: Vec = - crate::guard::HttpMethodsExtractor::extract_http_methods(&**g); - rdef_methods - .push((rdef.pattern().unwrap_or_default().to_string(), http_methods)); - }); - }); + let pat = rdef.pattern().unwrap_or("").to_string(); + let mut methods = Vec::new(); + let mut guard_names = Vec::new(); + for route in &routes { + if let Some(m) = route.get_method() { + if !methods.contains(&m) { + methods.push(m); + } + } + for name in route.guard_names() { + if !guard_names.contains(&name) { + guard_names.push(name.clone()); + } + } + } + crate::introspection::register_pattern_detail(pat, methods, guard_names); } *self.factory_ref.borrow_mut() = Some(ResourceFactory { - routes: self.routes, + routes, default: self.default, }); @@ -470,14 +473,6 @@ 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, - ); - } - config.register_service(rdef, guards, endpoint, None) } } diff --git a/actix-web/src/rmap.rs b/actix-web/src/rmap.rs index 2e4451b38..cf2336f78 100644 --- a/actix-web/src/rmap.rs +++ b/actix-web/src/rmap.rs @@ -15,16 +15,13 @@ const AVG_PATH_LEN: usize = 24; #[derive(Clone, Debug)] pub struct ResourceMap { - pattern: ResourceDef, - + pub(crate) pattern: ResourceDef, /// Named resources within the tree or, for external resources, it points to isolated nodes /// outside the tree. named: FoldHashMap>, - parent: RefCell>, - /// Must be `None` for "edge" nodes. - nodes: Option>>, + pub(crate) nodes: Option>>, } impl ResourceMap { @@ -38,42 +35,6 @@ impl ResourceMap { } } - #[cfg(feature = "resources-introspection")] - /// Returns a list of all paths in the resource map. - pub fn to_vec(&self) -> Vec { - 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) { - // 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 { diff --git a/actix-web/src/route.rs b/actix-web/src/route.rs index bcbfb9042..d502534a6 100644 --- a/actix-web/src/route.rs +++ b/actix-web/src/route.rs @@ -65,10 +65,10 @@ impl Route { pub(crate) fn take_guards(&mut self) -> Vec> { mem::take(Rc::get_mut(&mut self.guards).unwrap()) } - - #[cfg(feature = "resources-introspection")] - pub(crate) fn get_guards(&self) -> &Vec> { - &self.guards + /// Get the names of all guards applied to this route. + #[cfg(feature = "experimental-introspection")] + pub fn guard_names(&self) -> Vec { + self.guards.iter().map(|g| g.name()).collect() } } @@ -145,6 +145,23 @@ impl Route { self } + #[cfg(feature = "experimental-introspection")] + /// Get the first HTTP method guard applied to this route (if any). + /// WIP. + pub(crate) fn get_method(&self) -> Option { + self.guards.iter().find_map(|g| { + g.details().and_then(|details| { + details.into_iter().find_map(|d| { + if let crate::guard::GuardDetail::HttpMethods(mut m) = d { + m.pop().and_then(|s| s.parse().ok()) + } else { + None + } + }) + }) + }) + } + /// Add guard to the route. /// /// # Examples @@ -164,6 +181,10 @@ impl Route { self } + pub fn guards(&self) -> &Vec> { + &self.guards + } + /// Set handler function, use request extractors for parameters. /// /// # Examples diff --git a/actix-web/src/scope.rs b/actix-web/src/scope.rs index 0f2b87c86..2c1727779 100644 --- a/actix-web/src/scope.rs +++ b/actix-web/src/scope.rs @@ -395,9 +395,6 @@ where rmap.add(&mut rdef, None); } - #[cfg(feature = "resources-introspection")] - let mut rdef_methods: Vec<(String, Vec)> = Vec::new(); - // complete scope pipeline creation *self.factory_ref.borrow_mut() = Some(ScopeFactory { default, @@ -408,21 +405,38 @@ where .map(|(mut rdef, srv, guards, nested)| { rmap.add(&mut rdef, nested); - #[cfg(feature = "resources-introspection")] + #[cfg(feature = "experimental-introspection")] { - let http_methods: Vec = - guards.as_ref().map_or_else(Vec::new, |g| { - g.iter() - .flat_map(|g| { - crate::guard::HttpMethodsExtractor::extract_http_methods( - &**g, - ) - }) - .collect::>() - }); + use std::borrow::Borrow; - rdef_methods - .push((rdef.pattern().unwrap_or_default().to_string(), http_methods)); + // Get the pattern stored in ResourceMap + let pat = rdef.pattern().unwrap_or("").to_string(); + let guard_list: &[Box] = + guards.borrow().as_ref().map_or(&[], |v| &v[..]); + + // Extract HTTP methods from guards + let methods = guard_list + .iter() + .flat_map(|g| g.details().unwrap_or_default()) + .flat_map(|d| { + if let crate::guard::GuardDetail::HttpMethods(v) = d { + v.into_iter() + .filter_map(|s| s.parse().ok()) + .collect::>() + } else { + Vec::new() + } + }) + .collect::>(); + + // Extract guard names + let guard_names = guard_list + .iter() + .map(|g| g.name().to_string()) + .collect::>(); + + // Register route details for introspection + crate::introspection::register_pattern_detail(pat, methods, guard_names); } (rdef, srv, RefCell::new(guards)) @@ -452,14 +466,6 @@ 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), From 2f64cdb60af998be3ccf0a3596c0353ee7d4b8a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= Date: Mon, 12 May 2025 02:57:39 -0300 Subject: [PATCH 14/18] fix Cargo.lock --- Cargo.lock | 169 +---------------------------------------------------- 1 file changed, 2 insertions(+), 167 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ddb1d938..82f9024f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,7 +113,6 @@ dependencies = [ "rcgen", "regex", "rustls 0.23.27", - "rustls 0.23.27", "rustls-pemfile", "rustversion", "serde", @@ -253,10 +252,8 @@ dependencies = [ [[package]] name = "actix-server" version = "2.6.0" -version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" dependencies = [ "actix-rt", "actix-service", @@ -300,7 +297,6 @@ dependencies = [ "rustls 0.21.12", "rustls 0.22.4", "rustls 0.23.27", - "rustls 0.23.27", "serde", "serde_json", "serde_urlencoded", @@ -335,7 +331,6 @@ dependencies = [ "webpki-roots 0.22.6", "webpki-roots 0.25.4", "webpki-roots 0.26.11", - "webpki-roots 0.26.11", ] [[package]] @@ -393,7 +388,6 @@ dependencies = [ "regex", "regex-lite", "rustls 0.23.27", - "rustls 0.23.27", "rustls-pemfile", "serde", "serde_json", @@ -663,7 +657,6 @@ dependencies = [ "rustls 0.21.12", "rustls 0.22.4", "rustls 0.23.27", - "rustls 0.23.27", "rustls-pemfile", "serde", "serde_json", @@ -677,10 +670,8 @@ dependencies = [ [[package]] name = "aws-lc-rs" version = "1.13.1" -version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" -checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" dependencies = [ "aws-lc-sys", "zeroize", @@ -689,10 +680,8 @@ dependencies = [ [[package]] name = "aws-lc-sys" version = "0.29.0" -version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" -checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" dependencies = [ "bindgen", "cc", @@ -704,10 +693,8 @@ dependencies = [ [[package]] name = "backtrace" version = "0.3.75" -version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -787,9 +774,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.3" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -831,10 +818,8 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" version = "1.2.22" -version = "1.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" -checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" dependencies = [ "jobserver", "libc", @@ -1170,10 +1155,8 @@ dependencies = [ [[package]] name = "data-encoding" version = "2.9.0" -version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "deranged" @@ -1230,10 +1213,8 @@ dependencies = [ [[package]] name = "divan" version = "0.1.21" -version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a405457ec78b8fe08b0e32b4a3570ab5dff6dd16eb9e76a5ee0a9d9cbd898933" -checksum = "a405457ec78b8fe08b0e32b4a3570ab5dff6dd16eb9e76a5ee0a9d9cbd898933" dependencies = [ "cfg-if", "clap", @@ -1246,10 +1227,8 @@ dependencies = [ [[package]] name = "divan-macros" version = "0.1.21" -version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9556bc800956545d6420a640173e5ba7dfa82f38d3ea5a167eb555bc69ac3323" -checksum = "9556bc800956545d6420a640173e5ba7dfa82f38d3ea5a167eb555bc69ac3323" dependencies = [ "proc-macro2", "quote", @@ -1486,10 +1465,8 @@ dependencies = [ [[package]] name = "getrandom" version = "0.2.16" -version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -1499,10 +1476,8 @@ dependencies = [ [[package]] name = "getrandom" version = "0.3.3" -version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", @@ -1564,10 +1539,8 @@ dependencies = [ [[package]] name = "hashbrown" version = "0.15.3" -version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" [[package]] name = "heck" @@ -1584,10 +1557,8 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hermit-abi" version = "0.5.1" -version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" -checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" [[package]] name = "hkdf" @@ -1659,14 +1630,11 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "icu_collections" version = "2.0.0" -version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", "potential_utf", - "potential_utf", "yoke", "zerofrom", "zerovec", @@ -1675,11 +1643,8 @@ dependencies = [ [[package]] name = "icu_locale_core" version = "2.0.0" -name = "icu_locale_core" -version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -1691,10 +1656,8 @@ dependencies = [ [[package]] name = "icu_normalizer" version = "2.0.0" -version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -1708,58 +1671,45 @@ dependencies = [ [[package]] name = "icu_normalizer_data" version = "2.0.0" -version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" version = "2.0.0" -version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" -checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" dependencies = [ "displaydoc", "icu_collections", "icu_locale_core", - "icu_locale_core", "icu_properties_data", "icu_provider", "potential_utf", "zerotrie", - "potential_utf", - "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" version = "2.0.0" -version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" -checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" [[package]] name = "icu_provider" version = "2.0.0" -version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", "icu_locale_core", - "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerotrie", - "zerotrie", "zerovec", ] @@ -1793,10 +1743,8 @@ dependencies = [ [[package]] name = "idna_adapter" version = "1.2.1" -version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1861,7 +1809,6 @@ version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.5.1", "hermit-abi 0.5.1", "libc", "windows-sys 0.59.0", @@ -1900,10 +1847,8 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" version = "0.2.13" -version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" -checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" dependencies = [ "jiff-static", "log", @@ -1915,10 +1860,8 @@ dependencies = [ [[package]] name = "jiff-static" version = "0.2.13" -version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48" -checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48" dependencies = [ "proc-macro2", "quote", @@ -1931,7 +1874,6 @@ version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ - "getrandom 0.3.3", "getrandom 0.3.3", "libc", ] @@ -1967,10 +1909,8 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" version = "0.2.172" -version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libloading" @@ -2003,10 +1943,8 @@ checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" version = "0.8.0" -version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "local-channel" @@ -2213,10 +2151,8 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" version = "0.9.108" -version = "0.9.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" -checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" dependencies = [ "cc", "libc", @@ -2365,15 +2301,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "potential_utf" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" -dependencies = [ - "zerovec", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -2471,7 +2398,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.16", - "getrandom 0.2.16", ] [[package]] @@ -2481,7 +2407,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom 0.3.3", - "getrandom 0.3.3", ] [[package]] @@ -2520,10 +2445,8 @@ dependencies = [ [[package]] name = "redox_syscall" version = "0.5.12" -version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ "bitflags 2.9.0", ] @@ -2566,10 +2489,8 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "resolv-conf" version = "0.7.3" -version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7c8f7f733062b66dc1c63f9db168ac0b97a9210e247fa90fdc9ad08f51b302" -checksum = "fc7c8f7f733062b66dc1c63f9db168ac0b97a9210e247fa90fdc9ad08f51b302" [[package]] name = "ring" @@ -2595,7 +2516,6 @@ dependencies = [ "cc", "cfg-if", "getrandom 0.2.16", - "getrandom 0.2.16", "libc", "untrusted 0.9.0", "windows-sys 0.52.0", @@ -2629,10 +2549,8 @@ dependencies = [ [[package]] name = "rustix" version = "1.0.7" -version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ "bitflags 2.9.0", "errno", @@ -2682,17 +2600,14 @@ dependencies = [ [[package]] name = "rustls" version = "0.23.27" -version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "aws-lc-rs", "log", "once_cell", "rustls-pki-types", "rustls-webpki 0.103.3", - "rustls-webpki 0.103.3", "subtle", "zeroize", ] @@ -2722,16 +2637,11 @@ dependencies = [ [[package]] name = "rustls-pki-types" version = "1.12.0" -version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "zeroize", ] -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "zeroize", -] [[package]] name = "rustls-webpki" @@ -2757,10 +2667,8 @@ dependencies = [ [[package]] name = "rustls-webpki" version = "0.103.3" -version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "aws-lc-rs", "ring 0.17.14", @@ -2919,10 +2827,8 @@ dependencies = [ [[package]] name = "sha2" version = "0.10.9" -version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -2938,10 +2844,8 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.5" -version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -3020,10 +2924,8 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "2.0.101" -version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -3033,10 +2935,8 @@ dependencies = [ [[package]] name = "synstructure" version = "0.13.2" -version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -3052,17 +2952,13 @@ checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790" [[package]] name = "tempfile" version = "3.20.0" -version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", "getrandom 0.3.3", - "getrandom 0.3.3", "once_cell", "rustix 1.0.7", - "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -3081,7 +2977,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "rustix 1.0.7", "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -3140,10 +3035,8 @@ dependencies = [ [[package]] name = "tinystr" version = "0.8.1" -version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -3177,10 +3070,8 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.45.0" -version = "1.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" -checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" dependencies = [ "backtrace", "bytes", @@ -3254,7 +3145,6 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.27", "rustls 0.23.27", "tokio", ] @@ -3288,10 +3178,8 @@ dependencies = [ [[package]] name = "tokio-util" version = "0.7.15" -version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -3303,10 +3191,8 @@ dependencies = [ [[package]] name = "toml" version = "0.8.22" -version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" -checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" dependencies = [ "serde", "serde_spanned", @@ -3317,10 +3203,8 @@ dependencies = [ [[package]] name = "toml_datetime" version = "0.6.9" -version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" dependencies = [ "serde", ] @@ -3328,17 +3212,14 @@ dependencies = [ [[package]] name = "toml_edit" version = "0.22.26" -version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" -checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "toml_write", - "toml_write", "winnow", ] @@ -3348,12 +3229,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" -[[package]] -name = "toml_write" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" - [[package]] name = "tracing" version = "0.1.41" @@ -3435,10 +3310,8 @@ dependencies = [ [[package]] name = "trybuild" version = "1.0.105" -version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c9bf9513a2f4aeef5fdac8677d7d349c79fdbcc03b9c86da6e9d254f1e43be2" -checksum = "1c9bf9513a2f4aeef5fdac8677d7d349c79fdbcc03b9c86da6e9d254f1e43be2" dependencies = [ "glob", "serde", @@ -3672,23 +3545,12 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" version = "0.26.11" -version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ "webpki-roots 1.0.0", ] -[[package]] -name = "webpki-roots" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.0", -] - [[package]] name = "webpki-roots" version = "1.0.0" @@ -3962,10 +3824,8 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" version = "0.7.10" -version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" -checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] @@ -3992,10 +3852,8 @@ dependencies = [ [[package]] name = "writeable" version = "0.6.1" -version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "yasna" @@ -4009,10 +3867,8 @@ dependencies = [ [[package]] name = "yoke" version = "0.8.0" -version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -4023,10 +3879,8 @@ dependencies = [ [[package]] name = "yoke-derive" version = "0.8.0" -version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", @@ -4037,10 +3891,8 @@ dependencies = [ [[package]] name = "zerocopy" version = "0.8.25" -version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] @@ -4048,10 +3900,8 @@ dependencies = [ [[package]] name = "zerocopy-derive" version = "0.8.25" -version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", @@ -4096,24 +3946,11 @@ dependencies = [ "zerofrom", ] -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - [[package]] name = "zerovec" version = "0.11.2" -version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -4123,10 +3960,8 @@ dependencies = [ [[package]] name = "zerovec-derive" version = "0.11.1" -version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", From 0a9f6c19552325d995f013984c5788131b08ce94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= Date: Sun, 18 May 2025 21:19:34 -0300 Subject: [PATCH 15/18] feat(introspection): enhance introspection feature with detailed route registration and full path tracking --- Cargo.lock | 38 +++--- actix-web/examples/introspection.rs | 179 ++++++++++++++++++++-------- actix-web/src/app_service.rs | 29 ----- actix-web/src/config.rs | 64 ++++++++++ actix-web/src/introspection.rs | 165 +++++++++++++++++-------- actix-web/src/resource.rs | 62 ++++++---- actix-web/src/rmap.rs | 7 +- actix-web/src/route.rs | 25 +--- actix-web/src/scope.rs | 40 +------ 9 files changed, 377 insertions(+), 232 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 82f9024f3..64fb7d579 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" dependencies = [ "actix-rt", - "bitflags 2.9.0", + "bitflags 2.9.1", "bytes", "crossbeam-channel", "futures-core", @@ -31,7 +31,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "bytes", "futures-core", "futures-sink", @@ -53,7 +53,7 @@ dependencies = [ "actix-test", "actix-utils", "actix-web", - "bitflags 2.9.0", + "bitflags 2.9.1", "bytes", "derive_more", "env_logger", @@ -83,7 +83,7 @@ dependencies = [ "actix-web", "async-stream", "base64 0.22.1", - "bitflags 2.9.0", + "bitflags 2.9.1", "brotli", "bytes", "bytestring", @@ -723,7 +723,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cexpr", "clang-sys", "itertools 0.12.1", @@ -748,9 +748,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "block-buffer" @@ -817,9 +817,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.22" +version = "1.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" +checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" dependencies = [ "jobserver", "libc", @@ -1299,9 +1299,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", "windows-sys 0.59.0", @@ -2122,7 +2122,7 @@ version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if", "foreign-types", "libc", @@ -2448,7 +2448,7 @@ version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -2488,9 +2488,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "resolv-conf" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7c8f7f733062b66dc1c63f9db168ac0b97a9210e247fa90fdc9ad08f51b302" +checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" [[package]] name = "ring" @@ -2539,7 +2539,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2552,7 +2552,7 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.9.4", @@ -2734,7 +2734,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "core-foundation", "core-foundation-sys", "libc", @@ -3846,7 +3846,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] diff --git a/actix-web/examples/introspection.rs b/actix-web/examples/introspection.rs index 1de9cfec6..db633b276 100644 --- a/actix-web/examples/introspection.rs +++ b/actix-web/examples/introspection.rs @@ -1,10 +1,72 @@ -// NOTE: This is a work-in-progress example being used to test the new implementation -// of the experimental introspection feature. -// `cargo run --features experimental-introspection --example introspection` - +// Example showcasing the experimental introspection feature. +// Run with: `cargo run --features experimental-introspection --example introspection` use actix_web::{dev::Service, guard, web, App, HttpResponse, HttpServer, Responder}; use serde::Deserialize; -// Custom guard that checks if the Content-Type header is present. + +#[cfg(feature = "experimental-introspection")] +#[actix_web::get("/introspection")] +async fn introspection_handler() -> impl Responder { + use std::fmt::Write; + + use actix_web::introspection::{get_registry, initialize_registry}; + + initialize_registry(); + let registry = get_registry(); + let node = registry.lock().unwrap(); + + let mut buf = String::new(); + if node.children.is_empty() { + writeln!(buf, "No routes registered or introspection tree is empty.").unwrap(); + } else { + fn write_display( + node: &actix_web::introspection::IntrospectionNode, + parent_path: &str, + buf: &mut String, + ) { + let full_path = if parent_path.is_empty() { + node.pattern.clone() + } else { + format!( + "{}/{}", + parent_path.trim_end_matches('/'), + node.pattern.trim_start_matches('/') + ) + }; + if !node.methods.is_empty() || !node.guards.is_empty() { + let methods = if node.methods.is_empty() { + "".to_string() + } else { + format!("Methods: {:?}", node.methods) + }; + + let method_strings: Vec = + node.methods.iter().map(|m| m.to_string()).collect(); + + let filtered_guards: Vec<_> = node + .guards + .iter() + .filter(|guard| !method_strings.contains(&guard.to_string())) + .collect(); + + let guards = if filtered_guards.is_empty() { + "".to_string() + } else { + format!("Guards: {:?}", filtered_guards) + }; + + let _ = writeln!(buf, "{} {} {}", full_path, methods, guards); + } + for child in &node.children { + write_display(child, &full_path, buf); + } + } + write_display(&node, "/", &mut buf); + } + + HttpResponse::Ok().content_type("text/plain").body(buf) +} + +// Custom guard to check if the Content-Type header is present. struct ContentTypeGuard; impl guard::Guard for ContentTypeGuard { @@ -24,54 +86,62 @@ struct UserInfo { #[actix_web::main] async fn main() -> std::io::Result<()> { + // Initialize logging + env_logger::Builder::new() + .filter_level(log::LevelFilter::Debug) + .init(); + let server = HttpServer::new(|| { - let app = App::new() + let mut app = App::new() + // API endpoints under /api .service( web::scope("/api") + // Endpoints under /api/v1 .service( web::scope("/v1") - // GET /api/v1/item/{id}: returns the item id from the path. - .service(get_item) - // POST /api/v1/info: accepts JSON and returns user info. - .service(post_user_info) - // /api/v1/guarded: only accessible if Content-Type header is present. + .service(get_item) // GET /api/v1/item/{id} + .service(post_user_info) // POST /api/v1/info .route( "/guarded", - web::route().guard(ContentTypeGuard).to(guarded_handler), + web::route().guard(ContentTypeGuard).to(guarded_handler), // /api/v1/guarded ), ) - // API scope /api/v2: additional endpoint. - .service(web::scope("/v2").route("/hello", web::get().to(hello_v2))), + // Endpoints under /api/v2 + .service(web::scope("/v2").route("/hello", web::get().to(hello_v2))), // GET /api/v2/hello ) - // Scope /v1 outside /api: exposes only GET /v1/item/{id}. - .service(web::scope("/v1").service(get_item)) - // Scope /admin: admin endpoints with different HTTP methods. + // Endpoints under /v1 (outside /api) + .service(web::scope("/v1").service(get_item)) // GET /v1/item/{id} + // Admin endpoints under /admin .service( web::scope("/admin") - .route("/dashboard", web::get().to(admin_dashboard)) - // Single route handling multiple methods using separate handlers. + .route("/dashboard", web::get().to(admin_dashboard)) // GET /admin/dashboard .service( web::resource("/settings") - .route(web::get().to(get_settings)) - .route(web::post().to(update_settings)), + .route(web::get().to(get_settings)) // GET /admin/settings + .route(web::post().to(update_settings)), // POST /admin/settings ), ) - // Root resource: supports GET and POST on "/". + // Root endpoints .service( web::resource("/") - .route(web::get().to(root_index)) - .route(web::post().to(root_index)), + .route(web::get().to(root_index)) // GET / + .route(web::post().to(root_index)), // POST / ) - // Additional endpoints configured in a separate function. - .configure(extra_endpoints) - // Endpoint that rejects GET on /not_guard (allows other methods). + // Endpoints under /bar + .service(web::scope("/bar").configure(extra_endpoints)) // /bar/extra/ping, /bar/extra/multi, etc. + // Endpoints under /foo + .service(web::scope("/foo").configure(other_endpoints)) // /foo/extra/ping with POST and DELETE + // Additional endpoints under /extra + .configure(extra_endpoints) // /extra/ping, /extra/multi, etc. + .configure(other_endpoints) + // Endpoint that rejects GET on /not_guard (allows other methods) .route( "/not_guard", web::route() .guard(guard::Not(guard::Get())) .to(HttpResponse::MethodNotAllowed), ) - // Endpoint that requires GET, content-type: plain/text header, and/or POST on /all_guard. + // Endpoint that requires GET with header or POST on /all_guard .route( "/all_guard", web::route() @@ -83,29 +153,27 @@ async fn main() -> std::io::Result<()> { .to(HttpResponse::MethodNotAllowed), ); - /*#[cfg(feature = "experimental-introspection")] + // Register the introspection handler if the feature is enabled. + #[cfg(feature = "experimental-introspection")] { - actix_web::introspection::introspect(); - }*/ - // TODO: Enable introspection without the feature flag. + app = app.service(introspection_handler); // GET /introspection + } app }) - .workers(5) + .workers(1) .bind("127.0.0.1:8080")?; server.run().await } // GET /api/v1/item/{id} and GET /v1/item/{id} -// Returns a message with the provided id. -#[actix_web::get("/item/{id:\\d+}")] +#[actix_web::get("/item/{id}")] async fn get_item(path: web::Path) -> impl Responder { let id = path.into_inner(); HttpResponse::Ok().body(format!("Requested item with id: {}", id)) } // POST /api/v1/info -// Expects JSON and responds with the received user info. #[actix_web::post("/info")] async fn post_user_info(info: web::Json) -> impl Responder { HttpResponse::Ok().json(format!( @@ -115,63 +183,54 @@ async fn post_user_info(info: web::Json) -> impl Responder { } // /api/v1/guarded -// Uses a custom guard that requires the Content-Type header. async fn guarded_handler() -> impl Responder { HttpResponse::Ok().body("Passed the Content-Type guard!") } // GET /api/v2/hello -// Simple greeting endpoint. async fn hello_v2() -> impl Responder { HttpResponse::Ok().body("Hello from API v2!") } // GET /admin/dashboard -// Returns a message for the admin dashboard. async fn admin_dashboard() -> impl Responder { HttpResponse::Ok().body("Welcome to the Admin Dashboard!") } // GET /admin/settings -// Returns the current admin settings. async fn get_settings() -> impl Responder { HttpResponse::Ok().body("Current settings: ...") } // POST /admin/settings -// Updates the admin settings. async fn update_settings() -> impl Responder { HttpResponse::Ok().body("Settings have been updated!") } // GET and POST on / -// Generic root endpoint. async fn root_index() -> impl Responder { HttpResponse::Ok().body("Welcome to the Root Endpoint!") } -// Additional endpoints configured in a separate function. +// Additional endpoints for /extra fn extra_endpoints(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/extra") - // GET /extra/ping: simple ping endpoint. .route( "/ping", - web::get().to(|| async { HttpResponse::Ok().body("pong") }), + web::get().to(|| async { HttpResponse::Ok().body("pong") }), // GET /extra/ping ) - // /extra/multi: resource that supports GET and POST. .service( web::resource("/multi") .route( web::get().to(|| async { HttpResponse::Ok().body("GET response from /extra/multi") }), - ) + ) // GET /extra/multi .route(web::post().to(|| async { HttpResponse::Ok().body("POST response from /extra/multi") - })), + })), // POST /extra/multi ) - // /extra/{entities_id}/secure: nested scope with GET and POST, prints the received id. .service( web::scope("{entities_id:\\d+}") .service( @@ -181,15 +240,14 @@ fn extra_endpoints(cfg: &mut web::ServiceConfig) { web::get().to(|| async { HttpResponse::Ok().body("GET response from /extra/secure") }), - ) + ) // GET /extra/{entities_id}/secure/ .route( - "", + "/post", web::post().to(|| async { HttpResponse::Ok().body("POST response from /extra/secure") }), - ), + ), // POST /extra/{entities_id}/secure/post ) - // Middleware that prints the id received in the route. .wrap_fn(|req, srv| { println!( "Request to /extra/secure with id: {}", @@ -204,3 +262,18 @@ fn extra_endpoints(cfg: &mut web::ServiceConfig) { ), ); } + +// Additional endpoints for /foo +fn other_endpoints(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/extra") + .route( + "/ping", + web::post().to(|| async { HttpResponse::Ok().body("post from /extra/ping") }), // POST /foo/extra/ping + ) + .route( + "/ping", + web::delete().to(|| async { HttpResponse::Ok().body("delete from /extra/ping") }), // DELETE /foo/extra/ping + ), + ); +} diff --git a/actix-web/src/app_service.rs b/actix-web/src/app_service.rs index dd76c3d2d..8dc3af7a0 100644 --- a/actix-web/src/app_service.rs +++ b/actix-web/src/app_service.rs @@ -89,35 +89,6 @@ where .into_iter() .map(|(mut rdef, srv, guards, nested)| { rmap.add(&mut rdef, nested); - #[cfg(feature = "experimental-introspection")] - { - use std::borrow::Borrow; - let pat = rdef.pattern().unwrap_or("").to_string(); - let mut methods = Vec::new(); - let mut guard_names = Vec::new(); - if let Some(gs) = guards.borrow().as_ref() { - for g in gs.iter() { - let name = g.name().to_string(); - if !guard_names.contains(&name) { - guard_names.push(name.clone()); - } - if let Some(details) = g.details() { - for d in details { - if let crate::guard::GuardDetail::HttpMethods(v) = d { - for s in v { - if let Ok(m) = s.parse() { - if !methods.contains(&m) { - methods.push(m); - } - } - } - } - } - } - } - } - crate::introspection::register_pattern_detail(pat, methods, guard_names); - } (rdef, srv, RefCell::new(guards)) }) .collect::>() diff --git a/actix-web/src/config.rs b/actix-web/src/config.rs index 0e856f574..1d486b807 100644 --- a/actix-web/src/config.rs +++ b/actix-web/src/config.rs @@ -30,6 +30,8 @@ pub struct AppService { Option, Option>, )>, + #[cfg(feature = "experimental-introspection")] + pub current_prefix: String, } impl AppService { @@ -40,6 +42,8 @@ impl AppService { default, root: true, services: Vec::new(), + #[cfg(feature = "experimental-introspection")] + current_prefix: "".to_string(), } } @@ -71,6 +75,8 @@ impl AppService { default: Rc::clone(&self.default), services: Vec::new(), root: false, + #[cfg(feature = "experimental-introspection")] + current_prefix: self.current_prefix.clone(), } } @@ -101,9 +107,67 @@ impl AppService { InitError = (), > + 'static, { + #[cfg(feature = "experimental-introspection")] + { + use std::borrow::Borrow; + + use crate::introspection; + + // Build the full path for introspection + let pat = rdef.pattern().unwrap_or("").to_string(); + + let full_path = if self.current_prefix.is_empty() { + pat.clone() + } else { + format!( + "{}/{}", + self.current_prefix.trim_end_matches('/'), + pat.trim_start_matches('/') + ) + }; + + // Extract methods and guards for introspection + let guard_list: &[Box] = guards.borrow().as_ref().map_or(&[], |v| &v[..]); + let methods = guard_list + .iter() + .flat_map(|g| g.details().unwrap_or_default()) + .flat_map(|d| { + if let crate::guard::GuardDetail::HttpMethods(v) = d { + v.into_iter() + .filter_map(|s| s.parse().ok()) + .collect::>() + } else { + Vec::new() + } + }) + .collect::>(); + let guard_names = guard_list + .iter() + .map(|g| g.name().to_string()) + .collect::>(); + + // Determine if the registered service is a resource + let is_resource = rdef.pattern().is_some(); + introspection::register_pattern_detail(full_path, methods, guard_names, is_resource); + } + self.services .push((rdef, boxed::factory(factory.into_factory()), guards, nested)); } + + /// Update the current path prefix. + #[cfg(feature = "experimental-introspection")] + pub(crate) fn update_prefix(&mut self, prefix: &str) { + self.current_prefix = if self.current_prefix.is_empty() { + prefix.to_string() + } else { + format!( + "{}/{}", + self.current_prefix.trim_end_matches('/'), + prefix.trim_start_matches('/') + ) + }; + } } /// Application connection config. diff --git a/actix-web/src/introspection.rs b/actix-web/src/introspection.rs index 71e61eccd..53efc4d00 100644 --- a/actix-web/src/introspection.rs +++ b/actix-web/src/introspection.rs @@ -15,7 +15,13 @@ static DESIGNATED_THREAD: OnceLock = OnceLock::new(); static IS_INITIALIZED: AtomicBool = AtomicBool::new(false); pub fn initialize_registry() { - REGISTRY.get_or_init(|| Mutex::new(IntrospectionNode::new(ResourceType::App, "".into()))); + REGISTRY.get_or_init(|| { + Mutex::new(IntrospectionNode::new( + ResourceType::App, + "".into(), + "".into(), + )) + }); } pub fn get_registry() -> &'static Mutex { @@ -36,6 +42,7 @@ pub fn get_detail_registry() -> &'static Mutex> { pub struct RouteDetail { methods: Vec, guards: Vec, + is_resource: bool, // Indicates if this detail is for a final resource endpoint } #[derive(Debug, Clone, Copy)] @@ -48,35 +55,48 @@ pub enum ResourceType { #[derive(Debug, Clone)] pub struct IntrospectionNode { pub kind: ResourceType, - pub pattern: String, + pub pattern: String, // Local pattern + pub full_path: String, // Full path pub methods: Vec, pub guards: Vec, pub children: Vec, } impl IntrospectionNode { - pub fn new(kind: ResourceType, pattern: String) -> Self { + pub fn new(kind: ResourceType, pattern: String, full_path: String) -> Self { IntrospectionNode { kind, pattern, + full_path, methods: Vec::new(), guards: Vec::new(), children: Vec::new(), } } - pub fn display(&self, indent: usize, parent_path: &str) { - let full_path = if parent_path.is_empty() { - self.pattern.clone() - } else { - format!( - "{}/{}", - parent_path.trim_end_matches('/'), - self.pattern.trim_start_matches('/') - ) - }; + pub fn display(&self, indent: usize) -> String { + let mut result = String::new(); - if !self.methods.is_empty() || !self.guards.is_empty() { + // Helper function to determine if a node should be highlighted + let should_highlight = + |methods: &Vec, guards: &Vec| !methods.is_empty() || !guards.is_empty(); + + // Add the full path for all nodes + if !self.full_path.is_empty() { + if should_highlight(&self.methods, &self.guards) { + // Highlight full_path with yellow if it has methods or guards + result.push_str(&format!( + "{}\x1b[1;33m{}\x1b[0m", + " ".repeat(indent), + self.full_path + )); + } else { + result.push_str(&format!("{}{}", " ".repeat(indent), self.full_path)); + } + } + + // Only add methods and guards for resource nodes + if let ResourceType::Resource = self.kind { let methods = if self.methods.is_empty() { "".to_string() } else { @@ -88,39 +108,18 @@ impl IntrospectionNode { format!(" Guards: {:?}", self.guards) }; - println!("{}{}{}{}", " ".repeat(indent), full_path, methods, guards); + // Highlight final endpoints with ANSI codes for bold and green color + result.push_str(&format!("\x1b[1;32m{}{}\x1b[0m\n", methods, guards)); + } else { + // For non-resource nodes, just add a newline + result.push('\n'); } for child in &self.children { - child.display(indent, &full_path); + result.push_str(&child.display(indent + 2)); // Increase indent for children } - } -} -fn build_tree(node: &mut IntrospectionNode, rmap: &ResourceMap) { - initialize_detail_registry(); - let detail_registry = get_detail_registry(); - if let Some(ref children) = rmap.nodes { - for child_rc in children { - let child = child_rc; - let pat = child.pattern.pattern().unwrap_or("").to_string(); - let kind = if child.nodes.is_some() { - ResourceType::Scope - } else { - ResourceType::Resource - }; - let mut new_node = IntrospectionNode::new(kind, pat.clone()); - - if let ResourceType::Resource = new_node.kind { - if let Some(d) = detail_registry.lock().unwrap().get(&pat) { - new_node.methods = d.methods.clone(); - new_node.guards = d.guards.clone(); - } - } - - build_tree(&mut new_node, child); - node.children.push(new_node); - } + result } } @@ -134,19 +133,70 @@ fn is_designated_thread() -> bool { *DESIGNATED_THREAD.get().unwrap() == current_id } -pub fn register_rmap(rmap: &ResourceMap) { +pub fn register_rmap(_rmap: &ResourceMap) { if !is_designated_thread() { return; } initialize_registry(); - let mut root = IntrospectionNode::new(ResourceType::App, "".into()); - build_tree(&mut root, rmap); + initialize_detail_registry(); + + let detail_registry = get_detail_registry().lock().unwrap(); + let mut root = IntrospectionNode::new(ResourceType::App, "".into(), "".into()); + + // Build the introspection tree directly from the detail registry + for (full_path, _detail) in detail_registry.iter() { + let parts: Vec<&str> = full_path.split('/').collect(); + let mut current_node = &mut root; + + for (i, part) in parts.iter().enumerate() { + // Find the index of the existing child + let existing_child_index = current_node + .children + .iter() + .position(|n| n.pattern == *part); + + let child_index = if let Some(child_index) = existing_child_index { + child_index + } else { + // If it doesn't exist, create a new node and get its index + let child_full_path = parts[..=i].join("/"); + // Determine the kind based on whether this path exists as a resource in the detail registry + let kind = if detail_registry + .get(&child_full_path) + .is_some_and(|d| d.is_resource) + { + ResourceType::Resource + } else { + ResourceType::Scope + }; + let new_node = IntrospectionNode::new(kind, part.to_string(), child_full_path); + current_node.children.push(new_node); + current_node.children.len() - 1 + }; + + // Get a mutable reference to the child node + current_node = &mut current_node.children[child_index]; + + // If this node is marked as a resource, update its methods and guards + if let ResourceType::Resource = current_node.kind { + if let Some(detail) = detail_registry.get(¤t_node.full_path) { + update_unique(&mut current_node.methods, &detail.methods); + update_unique(&mut current_node.guards, &detail.guards); + } + } + } + } + *get_registry().lock().unwrap() = root; - // WIP. Display the introspection tree - let reg = get_registry().lock().unwrap(); - reg.display(0, ""); + // Display the introspection tree + let registry = get_registry().lock().unwrap(); + let tree_representation = registry.display(0); + log::debug!( + "Introspection Tree:\n{}", + tree_representation.trim_matches('\n').to_string() + ); } fn update_unique(existing: &mut Vec, new_items: &[T]) { @@ -157,16 +207,29 @@ fn update_unique(existing: &mut Vec, new_items: &[T]) { } } -pub fn register_pattern_detail(pattern: String, methods: Vec, guards: Vec) { +pub fn register_pattern_detail( + full_path: String, + methods: Vec, + guards: Vec, + is_resource: bool, +) { if !is_designated_thread() { return; } initialize_detail_registry(); let mut reg = get_detail_registry().lock().unwrap(); - reg.entry(pattern) + reg.entry(full_path) .and_modify(|d| { update_unique(&mut d.methods, &methods); update_unique(&mut d.guards, &guards); + // If the existing entry was not a resource but the new one is, update the kind + if !d.is_resource && is_resource { + d.is_resource = true; + } }) - .or_insert(RouteDetail { methods, guards }); + .or_insert(RouteDetail { + methods, + guards, + is_resource, + }); } diff --git a/actix-web/src/resource.rs b/actix-web/src/resource.rs index e6f3c0933..df5f05fb0 100644 --- a/actix-web/src/resource.rs +++ b/actix-web/src/resource.rs @@ -430,31 +430,53 @@ where } else { ResourceDef::new(self.rdef.clone()) }; + #[cfg(feature = "experimental-introspection")] + { + use crate::{http::Method, introspection}; + + let guards_routes = routes.iter().map(|r| r.guards()).collect::>(); + + let pat = rdef.pattern().unwrap_or("").to_string(); + let full_path = if config.current_prefix.is_empty() { + pat.clone() + } else { + format!( + "{}/{}", + config.current_prefix.trim_end_matches('/'), + pat.trim_start_matches('/') + ) + }; + + for route_guards in guards_routes { + // Log the guards and methods for introspection + let guard_names = route_guards.iter().map(|g| g.name()).collect::>(); + let methods = route_guards + .iter() + .flat_map(|g| g.details().unwrap_or_default()) + .flat_map(|d| { + if let crate::guard::GuardDetail::HttpMethods(v) = d { + v.into_iter() + .filter_map(|s| s.parse::().ok()) + .collect::>() + } else { + Vec::new() + } + }) + .collect::>(); + + introspection::register_pattern_detail( + full_path.clone(), + methods, + guard_names, + true, + ); + } + } if let Some(ref name) = self.name { rdef.set_name(name); } - #[cfg(feature = "experimental-introspection")] - { - let pat = rdef.pattern().unwrap_or("").to_string(); - let mut methods = Vec::new(); - let mut guard_names = Vec::new(); - for route in &routes { - if let Some(m) = route.get_method() { - if !methods.contains(&m) { - methods.push(m); - } - } - for name in route.guard_names() { - if !guard_names.contains(&name) { - guard_names.push(name.clone()); - } - } - } - crate::introspection::register_pattern_detail(pat, methods, guard_names); - } - *self.factory_ref.borrow_mut() = Some(ResourceFactory { routes, default: self.default, diff --git a/actix-web/src/rmap.rs b/actix-web/src/rmap.rs index cf2336f78..b445687ac 100644 --- a/actix-web/src/rmap.rs +++ b/actix-web/src/rmap.rs @@ -15,13 +15,16 @@ const AVG_PATH_LEN: usize = 24; #[derive(Clone, Debug)] pub struct ResourceMap { - pub(crate) pattern: ResourceDef, + pattern: ResourceDef, + /// Named resources within the tree or, for external resources, it points to isolated nodes /// outside the tree. named: FoldHashMap>, + parent: RefCell>, + /// Must be `None` for "edge" nodes. - pub(crate) nodes: Option>>, + nodes: Option>>, } impl ResourceMap { diff --git a/actix-web/src/route.rs b/actix-web/src/route.rs index d502534a6..65d7dcef0 100644 --- a/actix-web/src/route.rs +++ b/actix-web/src/route.rs @@ -65,11 +65,6 @@ impl Route { pub(crate) fn take_guards(&mut self) -> Vec> { mem::take(Rc::get_mut(&mut self.guards).unwrap()) } - /// Get the names of all guards applied to this route. - #[cfg(feature = "experimental-introspection")] - pub fn guard_names(&self) -> Vec { - self.guards.iter().map(|g| g.name()).collect() - } } impl ServiceFactory for Route { @@ -145,23 +140,6 @@ impl Route { self } - #[cfg(feature = "experimental-introspection")] - /// Get the first HTTP method guard applied to this route (if any). - /// WIP. - pub(crate) fn get_method(&self) -> Option { - self.guards.iter().find_map(|g| { - g.details().and_then(|details| { - details.into_iter().find_map(|d| { - if let crate::guard::GuardDetail::HttpMethods(mut m) = d { - m.pop().and_then(|s| s.parse().ok()) - } else { - None - } - }) - }) - }) - } - /// Add guard to the route. /// /// # Examples @@ -181,7 +159,8 @@ impl Route { self } - pub fn guards(&self) -> &Vec> { + #[cfg(feature = "experimental-introspection")] + pub(crate) fn guards(&self) -> &Vec> { &self.guards } diff --git a/actix-web/src/scope.rs b/actix-web/src/scope.rs index 2c1727779..979aad308 100644 --- a/actix-web/src/scope.rs +++ b/actix-web/src/scope.rs @@ -384,6 +384,11 @@ where // register nested services let mut cfg = config.clone_config(); + + // Update the prefix for the nested scope + #[cfg(feature = "experimental-introspection")] + cfg.update_prefix(&self.rdef); + self.services .into_iter() .for_each(|mut srv| srv.register(&mut cfg)); @@ -404,41 +409,6 @@ where .into_iter() .map(|(mut rdef, srv, guards, nested)| { rmap.add(&mut rdef, nested); - - #[cfg(feature = "experimental-introspection")] - { - use std::borrow::Borrow; - - // Get the pattern stored in ResourceMap - let pat = rdef.pattern().unwrap_or("").to_string(); - let guard_list: &[Box] = - guards.borrow().as_ref().map_or(&[], |v| &v[..]); - - // Extract HTTP methods from guards - let methods = guard_list - .iter() - .flat_map(|g| g.details().unwrap_or_default()) - .flat_map(|d| { - if let crate::guard::GuardDetail::HttpMethods(v) = d { - v.into_iter() - .filter_map(|s| s.parse().ok()) - .collect::>() - } else { - Vec::new() - } - }) - .collect::>(); - - // Extract guard names - let guard_names = guard_list - .iter() - .map(|g| g.name().to_string()) - .collect::>(); - - // Register route details for introspection - crate::introspection::register_pattern_detail(pat, methods, guard_names); - } - (rdef, srv, RefCell::new(guards)) }) .collect::>() From c8a7271d21c8f8e28d9f5bfe9d7a805d37de3ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= Date: Mon, 19 May 2025 02:41:48 -0300 Subject: [PATCH 16/18] optimize debug log and apply clippy/fmt suggestions --- actix-web/examples/introspection.rs | 519 ++++++++++++++-------------- actix-web/src/introspection.rs | 2 +- 2 files changed, 263 insertions(+), 258 deletions(-) diff --git a/actix-web/examples/introspection.rs b/actix-web/examples/introspection.rs index db633b276..75a34ccf5 100644 --- a/actix-web/examples/introspection.rs +++ b/actix-web/examples/introspection.rs @@ -1,279 +1,284 @@ // Example showcasing the experimental introspection feature. // Run with: `cargo run --features experimental-introspection --example introspection` -use actix_web::{dev::Service, guard, web, App, HttpResponse, HttpServer, Responder}; -use serde::Deserialize; - -#[cfg(feature = "experimental-introspection")] -#[actix_web::get("/introspection")] -async fn introspection_handler() -> impl Responder { - use std::fmt::Write; - - use actix_web::introspection::{get_registry, initialize_registry}; - - initialize_registry(); - let registry = get_registry(); - let node = registry.lock().unwrap(); - - let mut buf = String::new(); - if node.children.is_empty() { - writeln!(buf, "No routes registered or introspection tree is empty.").unwrap(); - } else { - fn write_display( - node: &actix_web::introspection::IntrospectionNode, - parent_path: &str, - buf: &mut String, - ) { - let full_path = if parent_path.is_empty() { - node.pattern.clone() - } else { - format!( - "{}/{}", - parent_path.trim_end_matches('/'), - node.pattern.trim_start_matches('/') - ) - }; - if !node.methods.is_empty() || !node.guards.is_empty() { - let methods = if node.methods.is_empty() { - "".to_string() - } else { - format!("Methods: {:?}", node.methods) - }; - - let method_strings: Vec = - node.methods.iter().map(|m| m.to_string()).collect(); - - let filtered_guards: Vec<_> = node - .guards - .iter() - .filter(|guard| !method_strings.contains(&guard.to_string())) - .collect(); - - let guards = if filtered_guards.is_empty() { - "".to_string() - } else { - format!("Guards: {:?}", filtered_guards) - }; - - let _ = writeln!(buf, "{} {} {}", full_path, methods, guards); - } - for child in &node.children { - write_display(child, &full_path, buf); - } - } - write_display(&node, "/", &mut buf); - } - - HttpResponse::Ok().content_type("text/plain").body(buf) -} - -// Custom guard to check if the Content-Type header is present. -struct ContentTypeGuard; - -impl guard::Guard for ContentTypeGuard { - fn check(&self, req: &guard::GuardContext<'_>) -> bool { - req.head() - .headers() - .contains_key(actix_web::http::header::CONTENT_TYPE) - } -} - -// Data structure for endpoints that receive JSON. -#[derive(Deserialize)] -struct UserInfo { - username: String, - age: u8, -} #[actix_web::main] async fn main() -> std::io::Result<()> { - // Initialize logging - env_logger::Builder::new() - .filter_level(log::LevelFilter::Debug) - .init(); + #[cfg(feature = "experimental-introspection")] + { + use actix_web::{dev::Service, guard, web, App, HttpResponse, HttpServer, Responder}; + use serde::Deserialize; - let server = HttpServer::new(|| { - let mut app = App::new() - // API endpoints under /api - .service( - web::scope("/api") - // Endpoints under /api/v1 - .service( - web::scope("/v1") - .service(get_item) // GET /api/v1/item/{id} - .service(post_user_info) // POST /api/v1/info - .route( - "/guarded", - web::route().guard(ContentTypeGuard).to(guarded_handler), // /api/v1/guarded - ), - ) - // Endpoints under /api/v2 - .service(web::scope("/v2").route("/hello", web::get().to(hello_v2))), // GET /api/v2/hello - ) - // Endpoints under /v1 (outside /api) - .service(web::scope("/v1").service(get_item)) // GET /v1/item/{id} - // Admin endpoints under /admin - .service( - web::scope("/admin") - .route("/dashboard", web::get().to(admin_dashboard)) // GET /admin/dashboard - .service( - web::resource("/settings") - .route(web::get().to(get_settings)) // GET /admin/settings - .route(web::post().to(update_settings)), // POST /admin/settings - ), - ) - // Root endpoints - .service( - web::resource("/") - .route(web::get().to(root_index)) // GET / - .route(web::post().to(root_index)), // POST / - ) - // Endpoints under /bar - .service(web::scope("/bar").configure(extra_endpoints)) // /bar/extra/ping, /bar/extra/multi, etc. - // Endpoints under /foo - .service(web::scope("/foo").configure(other_endpoints)) // /foo/extra/ping with POST and DELETE - // Additional endpoints under /extra - .configure(extra_endpoints) // /extra/ping, /extra/multi, etc. - .configure(other_endpoints) - // Endpoint that rejects GET on /not_guard (allows other methods) - .route( - "/not_guard", - web::route() - .guard(guard::Not(guard::Get())) - .to(HttpResponse::MethodNotAllowed), - ) - // Endpoint that requires GET with header or POST on /all_guard - .route( - "/all_guard", - web::route() - .guard( - guard::All(guard::Get()) - .and(guard::Header("content-type", "plain/text")) - .and(guard::Any(guard::Post())), - ) - .to(HttpResponse::MethodNotAllowed), - ); + // Initialize logging + env_logger::Builder::new() + .filter_level(log::LevelFilter::Debug) + .init(); - // Register the introspection handler if the feature is enabled. - #[cfg(feature = "experimental-introspection")] - { - app = app.service(introspection_handler); // GET /introspection + // Custom guard to check if the Content-Type header is present. + struct ContentTypeGuard; + + impl guard::Guard for ContentTypeGuard { + fn check(&self, req: &guard::GuardContext<'_>) -> bool { + req.head() + .headers() + .contains_key(actix_web::http::header::CONTENT_TYPE) + } } - app - }) - .workers(1) - .bind("127.0.0.1:8080")?; - server.run().await -} + // Data structure for endpoints that receive JSON. + #[derive(Deserialize)] + struct UserInfo { + username: String, + age: u8, + } -// GET /api/v1/item/{id} and GET /v1/item/{id} -#[actix_web::get("/item/{id}")] -async fn get_item(path: web::Path) -> impl Responder { - let id = path.into_inner(); - HttpResponse::Ok().body(format!("Requested item with id: {}", id)) -} + // GET /introspection + #[actix_web::get("/introspection")] + async fn introspection_handler() -> impl Responder { + use std::fmt::Write; -// POST /api/v1/info -#[actix_web::post("/info")] -async fn post_user_info(info: web::Json) -> impl Responder { - HttpResponse::Ok().json(format!( - "User {} with age {} received", - info.username, info.age - )) -} + use actix_web::introspection::{get_registry, initialize_registry}; -// /api/v1/guarded -async fn guarded_handler() -> impl Responder { - HttpResponse::Ok().body("Passed the Content-Type guard!") -} + initialize_registry(); + let registry = get_registry(); + let node = registry.lock().unwrap(); -// GET /api/v2/hello -async fn hello_v2() -> impl Responder { - HttpResponse::Ok().body("Hello from API v2!") -} + let mut buf = String::new(); + if node.children.is_empty() { + writeln!(buf, "No routes registered or introspection tree is empty.").unwrap(); + } else { + fn write_display( + node: &actix_web::introspection::IntrospectionNode, + parent_path: &str, + buf: &mut String, + ) { + let full_path = if parent_path.is_empty() { + node.pattern.clone() + } else { + format!( + "{}/{}", + parent_path.trim_end_matches('/'), + node.pattern.trim_start_matches('/') + ) + }; + if !node.methods.is_empty() || !node.guards.is_empty() { + let methods = if node.methods.is_empty() { + "".to_string() + } else { + format!("Methods: {:?}", node.methods) + }; -// GET /admin/dashboard -async fn admin_dashboard() -> impl Responder { - HttpResponse::Ok().body("Welcome to the Admin Dashboard!") -} + let method_strings: Vec = + node.methods.iter().map(|m| m.to_string()).collect(); -// GET /admin/settings -async fn get_settings() -> impl Responder { - HttpResponse::Ok().body("Current settings: ...") -} + let filtered_guards: Vec<_> = node + .guards + .iter() + .filter(|guard| !method_strings.contains(&guard.to_string())) + .collect(); -// POST /admin/settings -async fn update_settings() -> impl Responder { - HttpResponse::Ok().body("Settings have been updated!") -} + let guards = if filtered_guards.is_empty() { + "".to_string() + } else { + format!("Guards: {:?}", filtered_guards) + }; -// GET and POST on / -async fn root_index() -> impl Responder { - HttpResponse::Ok().body("Welcome to the Root Endpoint!") -} + let _ = writeln!(buf, "{} {} {}", full_path, methods, guards); + } + for child in &node.children { + write_display(child, &full_path, buf); + } + } + write_display(&node, "/", &mut buf); + } -// Additional endpoints for /extra -fn extra_endpoints(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/extra") - .route( - "/ping", - web::get().to(|| async { HttpResponse::Ok().body("pong") }), // GET /extra/ping - ) - .service( - web::resource("/multi") + HttpResponse::Ok().content_type("text/plain").body(buf) + } + // GET /api/v1/item/{id} and GET /v1/item/{id} + #[actix_web::get("/item/{id}")] + async fn get_item(path: web::Path) -> impl Responder { + let id = path.into_inner(); + HttpResponse::Ok().body(format!("Requested item with id: {}", id)) + } + + // POST /api/v1/info + #[actix_web::post("/info")] + async fn post_user_info(info: web::Json) -> impl Responder { + HttpResponse::Ok().json(format!( + "User {} with age {} received", + info.username, info.age + )) + } + + // /api/v1/guarded + async fn guarded_handler() -> impl Responder { + HttpResponse::Ok().body("Passed the Content-Type guard!") + } + + // GET /api/v2/hello + async fn hello_v2() -> impl Responder { + HttpResponse::Ok().body("Hello from API v2!") + } + + // GET /admin/dashboard + async fn admin_dashboard() -> impl Responder { + HttpResponse::Ok().body("Welcome to the Admin Dashboard!") + } + + // GET /admin/settings + async fn get_settings() -> impl Responder { + HttpResponse::Ok().body("Current settings: ...") + } + + // POST /admin/settings + async fn update_settings() -> impl Responder { + HttpResponse::Ok().body("Settings have been updated!") + } + + // GET and POST on / + async fn root_index() -> impl Responder { + HttpResponse::Ok().body("Welcome to the Root Endpoint!") + } + + // Additional endpoints for /extra + fn extra_endpoints(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/extra") .route( - web::get().to(|| async { - HttpResponse::Ok().body("GET response from /extra/multi") - }), - ) // GET /extra/multi - .route(web::post().to(|| async { - HttpResponse::Ok().body("POST response from /extra/multi") - })), // POST /extra/multi - ) - .service( - web::scope("{entities_id:\\d+}") - .service( - web::scope("/secure") - .route( - "", - web::get().to(|| async { - HttpResponse::Ok().body("GET response from /extra/secure") - }), - ) // GET /extra/{entities_id}/secure/ - .route( - "/post", - web::post().to(|| async { - HttpResponse::Ok().body("POST response from /extra/secure") - }), - ), // POST /extra/{entities_id}/secure/post + "/ping", + web::get().to(|| async { HttpResponse::Ok().body("pong") }), // GET /extra/ping ) - .wrap_fn(|req, srv| { - println!( - "Request to /extra/secure with id: {}", - req.match_info().get("entities_id").unwrap() - ); - let fut = srv.call(req); - async move { - let res = fut.await?; - Ok(res) - } - }), - ), - ); -} + .service( + web::resource("/multi") + .route(web::get().to(|| async { + HttpResponse::Ok().body("GET response from /extra/multi") + })) // GET /extra/multi + .route(web::post().to(|| async { + HttpResponse::Ok().body("POST response from /extra/multi") + })), // POST /extra/multi + ) + .service( + web::scope("{entities_id:\\d+}") + .service( + web::scope("/secure") + .route( + "", + web::get().to(|| async { + HttpResponse::Ok() + .body("GET response from /extra/secure") + }), + ) // GET /extra/{entities_id}/secure/ + .route( + "/post", + web::post().to(|| async { + HttpResponse::Ok() + .body("POST response from /extra/secure") + }), + ), // POST /extra/{entities_id}/secure/post + ) + .wrap_fn(|req, srv| { + println!( + "Request to /extra/secure with id: {}", + req.match_info().get("entities_id").unwrap() + ); + let fut = srv.call(req); + async move { + let res = fut.await?; + Ok(res) + } + }), + ), + ); + } -// Additional endpoints for /foo -fn other_endpoints(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/extra") - .route( - "/ping", - web::post().to(|| async { HttpResponse::Ok().body("post from /extra/ping") }), // POST /foo/extra/ping - ) - .route( - "/ping", - web::delete().to(|| async { HttpResponse::Ok().body("delete from /extra/ping") }), // DELETE /foo/extra/ping - ), - ); + // Additional endpoints for /foo + fn other_endpoints(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/extra") + .route( + "/ping", + web::post() + .to(|| async { HttpResponse::Ok().body("post from /extra/ping") }), // POST /foo/extra/ping + ) + .route( + "/ping", + web::delete() + .to(|| async { HttpResponse::Ok().body("delete from /extra/ping") }), // DELETE /foo/extra/ping + ), + ); + } + + // Create the HTTP server with all the routes and handlers + let server = HttpServer::new(|| { + App::new() + // API endpoints under /api + .service( + web::scope("/api") + // Endpoints under /api/v1 + .service( + web::scope("/v1") + .service(get_item) // GET /api/v1/item/{id} + .service(post_user_info) // POST /api/v1/info + .route( + "/guarded", + web::route().guard(ContentTypeGuard).to(guarded_handler), // /api/v1/guarded + ), + ) + // Endpoints under /api/v2 + .service(web::scope("/v2").route("/hello", web::get().to(hello_v2))), // GET /api/v2/hello + ) + // Endpoints under /v1 (outside /api) + .service(web::scope("/v1").service(get_item)) // GET /v1/item/{id} + // Admin endpoints under /admin + .service( + web::scope("/admin") + .route("/dashboard", web::get().to(admin_dashboard)) // GET /admin/dashboard + .service( + web::resource("/settings") + .route(web::get().to(get_settings)) // GET /admin/settings + .route(web::post().to(update_settings)), // POST /admin/settings + ), + ) + // Root endpoints + .service( + web::resource("/") + .route(web::get().to(root_index)) // GET / + .route(web::post().to(root_index)), // POST / + ) + // Endpoints under /bar + .service(web::scope("/bar").configure(extra_endpoints)) // /bar/extra/ping, /bar/extra/multi, etc. + // Endpoints under /foo + .service(web::scope("/foo").configure(other_endpoints)) // /foo/extra/ping with POST and DELETE + // Additional endpoints under /extra + .configure(extra_endpoints) // /extra/ping, /extra/multi, etc. + .configure(other_endpoints) + // Endpoint that rejects GET on /not_guard (allows other methods) + .route( + "/not_guard", + web::route() + .guard(guard::Not(guard::Get())) + .to(HttpResponse::MethodNotAllowed), + ) + // Endpoint that requires GET with header or POST on /all_guard + .route( + "/all_guard", + web::route() + .guard( + guard::All(guard::Get()) + .and(guard::Header("content-type", "plain/text")) + .and(guard::Any(guard::Post())), + ) + .to(HttpResponse::MethodNotAllowed), + ) + .service(introspection_handler) + }) + .workers(1) + .bind("127.0.0.1:8080")?; + + server.run().await + } + #[cfg(not(feature = "experimental-introspection"))] + { + eprintln!("This example requires the 'experimental-introspection' feature to be enabled."); + std::process::exit(1); + } } diff --git a/actix-web/src/introspection.rs b/actix-web/src/introspection.rs index 53efc4d00..de752e392 100644 --- a/actix-web/src/introspection.rs +++ b/actix-web/src/introspection.rs @@ -195,7 +195,7 @@ pub fn register_rmap(_rmap: &ResourceMap) { let tree_representation = registry.display(0); log::debug!( "Introspection Tree:\n{}", - tree_representation.trim_matches('\n').to_string() + tree_representation.trim_matches('\n') ); } From 23fed2298e49b34c919ccc9ede8e16ac358293d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20C=C3=A9spedes=20Tab=C3=A1rez?= Date: Mon, 19 May 2025 21:55:49 -0300 Subject: [PATCH 17/18] feat(introspection): enhance introspection handlers for JSON and plain text responses --- actix-web/examples/introspection.rs | 93 ++++++---------- actix-web/src/app_service.rs | 2 +- actix-web/src/introspection.rs | 166 ++++++++++++++++------------ 3 files changed, 130 insertions(+), 131 deletions(-) diff --git a/actix-web/examples/introspection.rs b/actix-web/examples/introspection.rs index 75a34ccf5..484160b37 100644 --- a/actix-web/examples/introspection.rs +++ b/actix-web/examples/introspection.rs @@ -31,68 +31,24 @@ async fn main() -> std::io::Result<()> { age: u8, } - // GET /introspection - #[actix_web::get("/introspection")] - async fn introspection_handler() -> impl Responder { - use std::fmt::Write; + // GET /introspection for JSON response + async fn introspection_handler_json() -> impl Responder { + use actix_web::introspection::introspection_report_as_json; - use actix_web::introspection::{get_registry, initialize_registry}; - - initialize_registry(); - let registry = get_registry(); - let node = registry.lock().unwrap(); - - let mut buf = String::new(); - if node.children.is_empty() { - writeln!(buf, "No routes registered or introspection tree is empty.").unwrap(); - } else { - fn write_display( - node: &actix_web::introspection::IntrospectionNode, - parent_path: &str, - buf: &mut String, - ) { - let full_path = if parent_path.is_empty() { - node.pattern.clone() - } else { - format!( - "{}/{}", - parent_path.trim_end_matches('/'), - node.pattern.trim_start_matches('/') - ) - }; - if !node.methods.is_empty() || !node.guards.is_empty() { - let methods = if node.methods.is_empty() { - "".to_string() - } else { - format!("Methods: {:?}", node.methods) - }; - - let method_strings: Vec = - node.methods.iter().map(|m| m.to_string()).collect(); - - let filtered_guards: Vec<_> = node - .guards - .iter() - .filter(|guard| !method_strings.contains(&guard.to_string())) - .collect(); - - let guards = if filtered_guards.is_empty() { - "".to_string() - } else { - format!("Guards: {:?}", filtered_guards) - }; - - let _ = writeln!(buf, "{} {} {}", full_path, methods, guards); - } - for child in &node.children { - write_display(child, &full_path, buf); - } - } - write_display(&node, "/", &mut buf); - } - - HttpResponse::Ok().content_type("text/plain").body(buf) + let report = introspection_report_as_json(); + HttpResponse::Ok() + .content_type("application/json") + .body(report) } + + // GET /introspection for plain text response + async fn introspection_handler_text() -> impl Responder { + use actix_web::introspection::introspection_report_as_text; + + let report = introspection_report_as_text(); + HttpResponse::Ok().content_type("text/plain").body(report) + } + // GET /api/v1/item/{id} and GET /v1/item/{id} #[actix_web::get("/item/{id}")] async fn get_item(path: web::Path) -> impl Responder { @@ -210,6 +166,22 @@ async fn main() -> std::io::Result<()> { // Create the HTTP server with all the routes and handlers let server = HttpServer::new(|| { App::new() + // Get introspection report + // curl --location '127.0.0.1:8080/introspection' --header 'Accept: application/json' + // curl --location '127.0.0.1:8080/introspection' --header 'Accept: text/plain' + .service( + web::resource("/introspection") + .route( + web::get() + .guard(guard::Header("accept", "application/json")) + .to(introspection_handler_json), + ) + .route( + web::get() + .guard(guard::Header("accept", "text/plain")) + .to(introspection_handler_text), + ), + ) // API endpoints under /api .service( web::scope("/api") @@ -269,7 +241,6 @@ async fn main() -> std::io::Result<()> { ) .to(HttpResponse::MethodNotAllowed), ) - .service(introspection_handler) }) .workers(1) .bind("127.0.0.1:8080")?; diff --git a/actix-web/src/app_service.rs b/actix-web/src/app_service.rs index 8dc3af7a0..e323a9592 100644 --- a/actix-web/src/app_service.rs +++ b/actix-web/src/app_service.rs @@ -132,7 +132,7 @@ where #[cfg(feature = "experimental-introspection")] { - crate::introspection::register_rmap(&rmap); + crate::introspection::finalize_registry(); } Ok(AppInitService { diff --git a/actix-web/src/introspection.rs b/actix-web/src/introspection.rs index de752e392..cedd3653f 100644 --- a/actix-web/src/introspection.rs +++ b/actix-web/src/introspection.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + fmt::Write as FmtWrite, sync::{ atomic::{AtomicBool, Ordering}, Mutex, OnceLock, @@ -7,14 +8,16 @@ use std::{ thread, }; -use crate::{http::Method, rmap::ResourceMap}; +use serde::Serialize; + +use crate::http::Method; static REGISTRY: OnceLock> = OnceLock::new(); static DETAIL_REGISTRY: OnceLock>> = OnceLock::new(); static DESIGNATED_THREAD: OnceLock = OnceLock::new(); static IS_INITIALIZED: AtomicBool = AtomicBool::new(false); -pub fn initialize_registry() { +fn initialize_registry() { REGISTRY.get_or_init(|| { Mutex::new(IntrospectionNode::new( ResourceType::App, @@ -24,20 +27,30 @@ pub fn initialize_registry() { }); } -pub fn get_registry() -> &'static Mutex { +fn get_registry() -> &'static Mutex { REGISTRY.get().expect("Registry not initialized") } -pub fn initialize_detail_registry() { +fn initialize_detail_registry() { DETAIL_REGISTRY.get_or_init(|| Mutex::new(HashMap::new())); } -pub fn get_detail_registry() -> &'static Mutex> { +fn get_detail_registry() -> &'static Mutex> { DETAIL_REGISTRY .get() .expect("Detail registry not initialized") } +fn is_designated_thread() -> bool { + let current_id = thread::current().id(); + DESIGNATED_THREAD.get_or_init(|| { + IS_INITIALIZED.store(true, Ordering::SeqCst); + current_id // Assign the first thread that calls this function + }); + + *DESIGNATED_THREAD.get().unwrap() == current_id +} + #[derive(Clone)] pub struct RouteDetail { methods: Vec, @@ -55,12 +68,18 @@ pub enum ResourceType { #[derive(Debug, Clone)] pub struct IntrospectionNode { pub kind: ResourceType, - pub pattern: String, // Local pattern - pub full_path: String, // Full path + pub pattern: String, + pub full_path: String, pub methods: Vec, pub guards: Vec, pub children: Vec, } +#[derive(Debug, Clone, Serialize)] +pub struct IntrospectionReportItem { + pub full_path: String, + pub methods: Vec, + pub guards: Vec, +} impl IntrospectionNode { pub fn new(kind: ResourceType, pattern: String, full_path: String) -> Self { @@ -73,67 +92,58 @@ impl IntrospectionNode { children: Vec::new(), } } +} - pub fn display(&self, indent: usize) -> String { - let mut result = String::new(); - - // Helper function to determine if a node should be highlighted - let should_highlight = - |methods: &Vec, guards: &Vec| !methods.is_empty() || !guards.is_empty(); - - // Add the full path for all nodes - if !self.full_path.is_empty() { - if should_highlight(&self.methods, &self.guards) { - // Highlight full_path with yellow if it has methods or guards - result.push_str(&format!( - "{}\x1b[1;33m{}\x1b[0m", - " ".repeat(indent), - self.full_path - )); +impl From<&IntrospectionNode> for Vec { + fn from(node: &IntrospectionNode) -> Self { + fn collect_report_items( + node: &IntrospectionNode, + parent_path: &str, + report_items: &mut Vec, + ) { + let full_path = if parent_path.is_empty() { + node.pattern.clone() } else { - result.push_str(&format!("{}{}", " ".repeat(indent), self.full_path)); + format!( + "{}/{}", + parent_path.trim_end_matches('/'), + node.pattern.trim_start_matches('/') + ) + }; + + if !node.methods.is_empty() || !node.guards.is_empty() { + // Filter guards that are already represented in methods + let filtered_guards: Vec = node + .guards + .iter() + .filter(|guard| { + !node + .methods + .iter() + .any(|method| method.to_string() == **guard) + }) + .cloned() + .collect(); + + report_items.push(IntrospectionReportItem { + full_path: full_path.clone(), + methods: node.methods.iter().map(|m| m.to_string()).collect(), + guards: filtered_guards, + }); + } + + for child in &node.children { + collect_report_items(child, &full_path, report_items); } } - // Only add methods and guards for resource nodes - if let ResourceType::Resource = self.kind { - let methods = if self.methods.is_empty() { - "".to_string() - } else { - format!(" Methods: {:?}", self.methods) - }; - let guards = if self.guards.is_empty() { - "".to_string() - } else { - format!(" Guards: {:?}", self.guards) - }; - - // Highlight final endpoints with ANSI codes for bold and green color - result.push_str(&format!("\x1b[1;32m{}{}\x1b[0m\n", methods, guards)); - } else { - // For non-resource nodes, just add a newline - result.push('\n'); - } - - for child in &self.children { - result.push_str(&child.display(indent + 2)); // Increase indent for children - } - - result + let mut report_items = Vec::new(); + collect_report_items(node, "/", &mut report_items); + report_items } } -fn is_designated_thread() -> bool { - let current_id = thread::current().id(); - DESIGNATED_THREAD.get_or_init(|| { - IS_INITIALIZED.store(true, Ordering::SeqCst); - current_id // Assign the first thread that calls this function - }); - - *DESIGNATED_THREAD.get().unwrap() == current_id -} - -pub fn register_rmap(_rmap: &ResourceMap) { +pub(crate) fn finalize_registry() { if !is_designated_thread() { return; } @@ -189,14 +199,6 @@ pub fn register_rmap(_rmap: &ResourceMap) { } *get_registry().lock().unwrap() = root; - - // Display the introspection tree - let registry = get_registry().lock().unwrap(); - let tree_representation = registry.display(0); - log::debug!( - "Introspection Tree:\n{}", - tree_representation.trim_matches('\n') - ); } fn update_unique(existing: &mut Vec, new_items: &[T]) { @@ -207,7 +209,7 @@ fn update_unique(existing: &mut Vec, new_items: &[T]) { } } -pub fn register_pattern_detail( +pub(crate) fn register_pattern_detail( full_path: String, methods: Vec, guards: Vec, @@ -233,3 +235,29 @@ pub fn register_pattern_detail( is_resource, }); } + +pub fn introspection_report_as_text() -> String { + let registry = get_registry(); + let node = registry.lock().unwrap(); + let report_items: Vec = (&*node).into(); + + let mut buf = String::new(); + for item in report_items { + writeln!( + buf, + "{} => Methods: {:?} | Guards: {:?}", + item.full_path, item.methods, item.guards + ) + .unwrap(); + } + + buf +} + +pub fn introspection_report_as_json() -> String { + let registry = get_registry(); + let node = registry.lock().unwrap(); + let report_items: Vec = (&*node).into(); + + serde_json::to_string_pretty(&report_items).unwrap() +} From 7ff7768dc47bcf2835dfa308225c0f265f1d3a13 Mon Sep 17 00:00:00 2001 From: dertin <1295883+dertin@users.noreply.github.com> Date: Wed, 11 Jun 2025 17:37:26 -0300 Subject: [PATCH 18/18] feat(introspection): implement experimental introspection feature with multiple App instances --- actix-web/examples/introspection.rs | 18 +- .../examples/introspection_multi_servers.rs | 52 ++++ actix-web/src/app.rs | 12 + actix-web/src/app_service.rs | 14 +- actix-web/src/config.rs | 36 ++- actix-web/src/introspection.rs | 268 +++++++----------- actix-web/src/resource.rs | 4 +- 7 files changed, 228 insertions(+), 176 deletions(-) create mode 100644 actix-web/examples/introspection_multi_servers.rs diff --git a/actix-web/examples/introspection.rs b/actix-web/examples/introspection.rs index 484160b37..2bca5a3e9 100644 --- a/actix-web/examples/introspection.rs +++ b/actix-web/examples/introspection.rs @@ -32,20 +32,20 @@ async fn main() -> std::io::Result<()> { } // GET /introspection for JSON response - async fn introspection_handler_json() -> impl Responder { - use actix_web::introspection::introspection_report_as_json; - - let report = introspection_report_as_json(); + async fn introspection_handler_json( + tree: web::Data, + ) -> impl Responder { + let report = tree.report_as_json(); HttpResponse::Ok() .content_type("application/json") .body(report) } // GET /introspection for plain text response - async fn introspection_handler_text() -> impl Responder { - use actix_web::introspection::introspection_report_as_text; - - let report = introspection_report_as_text(); + async fn introspection_handler_text( + tree: web::Data, + ) -> impl Responder { + let report = tree.report_as_text(); HttpResponse::Ok().content_type("text/plain").body(report) } @@ -242,7 +242,7 @@ async fn main() -> std::io::Result<()> { .to(HttpResponse::MethodNotAllowed), ) }) - .workers(1) + .workers(5) .bind("127.0.0.1:8080")?; server.run().await diff --git a/actix-web/examples/introspection_multi_servers.rs b/actix-web/examples/introspection_multi_servers.rs new file mode 100644 index 000000000..7ed2224e3 --- /dev/null +++ b/actix-web/examples/introspection_multi_servers.rs @@ -0,0 +1,52 @@ +// Example showcasing the experimental introspection feature with multiple App instances. +// Run with: `cargo run --features experimental-introspection --example introspection_multi_servers` + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + #[cfg(feature = "experimental-introspection")] + { + use actix_web::{web, App, HttpResponse, HttpServer, Responder}; + use futures_util::future; + + async fn introspection_handler( + tree: web::Data, + ) -> impl Responder { + HttpResponse::Ok() + .content_type("text/plain") + .body(tree.report_as_text()) + } + + async fn index() -> impl Responder { + HttpResponse::Ok().body("Hello from app") + } + + let srv1 = HttpServer::new(|| { + App::new() + .service(web::resource("/a").route(web::get().to(index))) + .service( + web::resource("/introspection").route(web::get().to(introspection_handler)), + ) + }) + .workers(8) + .bind("127.0.0.1:8081")? + .run(); + + let srv2 = HttpServer::new(|| { + App::new() + .service(web::resource("/b").route(web::get().to(index))) + .service( + web::resource("/introspection").route(web::get().to(introspection_handler)), + ) + }) + .workers(3) + .bind("127.0.0.1:8082")? + .run(); + + future::try_join(srv1, srv2).await?; + } + #[cfg(not(feature = "experimental-introspection"))] + { + eprintln!("This example requires the 'experimental-introspection' feature to be enabled."); + } + Ok(()) +} diff --git a/actix-web/src/app.rs b/actix-web/src/app.rs index f12d39979..1099731f3 100644 --- a/actix-web/src/app.rs +++ b/actix-web/src/app.rs @@ -30,6 +30,8 @@ pub struct App { data_factories: Vec, external: Vec, extensions: Extensions, + #[cfg(feature = "experimental-introspection")] + introspector: Rc>, } impl App { @@ -46,6 +48,10 @@ impl App { factory_ref, external: Vec::new(), extensions: Extensions::new(), + #[cfg(feature = "experimental-introspection")] + introspector: Rc::new(RefCell::new( + crate::introspection::IntrospectionCollector::new(), + )), } } } @@ -366,6 +372,8 @@ where factory_ref: self.factory_ref, external: self.external, extensions: self.extensions, + #[cfg(feature = "experimental-introspection")] + introspector: self.introspector, } } @@ -429,6 +437,8 @@ where factory_ref: self.factory_ref, external: self.external, extensions: self.extensions, + #[cfg(feature = "experimental-introspection")] + introspector: self.introspector, } } } @@ -453,6 +463,8 @@ where default: self.default, factory_ref: self.factory_ref, extensions: RefCell::new(Some(self.extensions)), + #[cfg(feature = "experimental-introspection")] + introspector: Rc::clone(&self.introspector), } } } diff --git a/actix-web/src/app_service.rs b/actix-web/src/app_service.rs index e323a9592..86f6a08b5 100644 --- a/actix-web/src/app_service.rs +++ b/actix-web/src/app_service.rs @@ -41,6 +41,8 @@ where pub(crate) default: Option>, pub(crate) factory_ref: Rc>>, pub(crate) external: RefCell>, + #[cfg(feature = "experimental-introspection")] + pub(crate) introspector: Rc>, } impl ServiceFactory for AppInit @@ -72,6 +74,10 @@ where // create App config to pass to child services let mut config = AppService::new(config, Rc::clone(&default)); + #[cfg(feature = "experimental-introspection")] + { + config.introspector = Rc::clone(&self.introspector); + } // register services mem::take(&mut *self.services.borrow_mut()) @@ -80,6 +86,9 @@ where let mut rmap = ResourceMap::new(ResourceDef::prefix("")); + #[cfg(feature = "experimental-introspection")] + let (config, services, _) = config.into_services(); + #[cfg(not(feature = "experimental-introspection"))] let (config, services) = config.into_services(); // complete pipeline creation. @@ -110,6 +119,8 @@ where // construct app service and middleware service factory future. let endpoint_fut = self.endpoint.new_service(()); + #[cfg(feature = "experimental-introspection")] + let introspector = Rc::clone(&self.introspector); // take extensions or create new one as app data container. let mut app_data = self.extensions.borrow_mut().take().unwrap_or_default(); @@ -132,7 +143,8 @@ where #[cfg(feature = "experimental-introspection")] { - crate::introspection::finalize_registry(); + let tree = introspector.borrow_mut().finalize(); + app_data.insert(crate::web::Data::new(tree)); } Ok(AppInitService { diff --git a/actix-web/src/config.rs b/actix-web/src/config.rs index 1d486b807..8c5a697ce 100644 --- a/actix-web/src/config.rs +++ b/actix-web/src/config.rs @@ -32,6 +32,9 @@ pub struct AppService { )>, #[cfg(feature = "experimental-introspection")] pub current_prefix: String, + #[cfg(feature = "experimental-introspection")] + pub(crate) introspector: + std::rc::Rc>, } impl AppService { @@ -44,6 +47,10 @@ impl AppService { services: Vec::new(), #[cfg(feature = "experimental-introspection")] current_prefix: "".to_string(), + #[cfg(feature = "experimental-introspection")] + introspector: std::rc::Rc::new(std::cell::RefCell::new( + crate::introspection::IntrospectionCollector::new(), + )), } } @@ -53,6 +60,24 @@ impl AppService { } #[allow(clippy::type_complexity)] + #[cfg(feature = "experimental-introspection")] + pub(crate) fn into_services( + self, + ) -> ( + AppConfig, + Vec<( + ResourceDef, + BoxedHttpServiceFactory, + Option, + Option>, + )>, + std::rc::Rc>, + ) { + (self.config, self.services, self.introspector) + } + + #[allow(clippy::type_complexity)] + #[cfg(not(feature = "experimental-introspection"))] pub(crate) fn into_services( self, ) -> ( @@ -77,6 +102,8 @@ impl AppService { root: false, #[cfg(feature = "experimental-introspection")] current_prefix: self.current_prefix.clone(), + #[cfg(feature = "experimental-introspection")] + introspector: std::rc::Rc::clone(&self.introspector), } } @@ -111,8 +138,6 @@ impl AppService { { use std::borrow::Borrow; - use crate::introspection; - // Build the full path for introspection let pat = rdef.pattern().unwrap_or("").to_string(); @@ -148,7 +173,12 @@ impl AppService { // Determine if the registered service is a resource let is_resource = rdef.pattern().is_some(); - introspection::register_pattern_detail(full_path, methods, guard_names, is_resource); + self.introspector.borrow_mut().register_pattern_detail( + full_path, + methods, + guard_names, + is_resource, + ); } self.services diff --git a/actix-web/src/introspection.rs b/actix-web/src/introspection.rs index cedd3653f..c52bea9df 100644 --- a/actix-web/src/introspection.rs +++ b/actix-web/src/introspection.rs @@ -1,61 +1,14 @@ -use std::{ - collections::HashMap, - fmt::Write as FmtWrite, - sync::{ - atomic::{AtomicBool, Ordering}, - Mutex, OnceLock, - }, - thread, -}; +use std::{collections::HashMap, fmt::Write as FmtWrite}; use serde::Serialize; use crate::http::Method; -static REGISTRY: OnceLock> = OnceLock::new(); -static DETAIL_REGISTRY: OnceLock>> = OnceLock::new(); -static DESIGNATED_THREAD: OnceLock = OnceLock::new(); -static IS_INITIALIZED: AtomicBool = AtomicBool::new(false); - -fn initialize_registry() { - REGISTRY.get_or_init(|| { - Mutex::new(IntrospectionNode::new( - ResourceType::App, - "".into(), - "".into(), - )) - }); -} - -fn get_registry() -> &'static Mutex { - REGISTRY.get().expect("Registry not initialized") -} - -fn initialize_detail_registry() { - DETAIL_REGISTRY.get_or_init(|| Mutex::new(HashMap::new())); -} - -fn get_detail_registry() -> &'static Mutex> { - DETAIL_REGISTRY - .get() - .expect("Detail registry not initialized") -} - -fn is_designated_thread() -> bool { - let current_id = thread::current().id(); - DESIGNATED_THREAD.get_or_init(|| { - IS_INITIALIZED.store(true, Ordering::SeqCst); - current_id // Assign the first thread that calls this function - }); - - *DESIGNATED_THREAD.get().unwrap() == current_id -} - #[derive(Clone)] pub struct RouteDetail { methods: Vec, guards: Vec, - is_resource: bool, // Indicates if this detail is for a final resource endpoint + is_resource: bool, } #[derive(Debug, Clone, Copy)] @@ -74,6 +27,7 @@ pub struct IntrospectionNode { pub guards: Vec, pub children: Vec, } + #[derive(Debug, Clone, Serialize)] pub struct IntrospectionReportItem { pub full_path: String, @@ -112,16 +66,10 @@ impl From<&IntrospectionNode> for Vec { }; if !node.methods.is_empty() || !node.guards.is_empty() { - // Filter guards that are already represented in methods let filtered_guards: Vec = node .guards .iter() - .filter(|guard| { - !node - .methods - .iter() - .any(|method| method.to_string() == **guard) - }) + .filter(|guard| !node.methods.iter().any(|m| m.to_string() == **guard)) .cloned() .collect(); @@ -143,62 +91,113 @@ impl From<&IntrospectionNode> for Vec { } } -pub(crate) fn finalize_registry() { - if !is_designated_thread() { - return; - } +#[derive(Clone, Default)] +pub struct IntrospectionCollector { + details: HashMap, +} - initialize_registry(); - initialize_detail_registry(); - - let detail_registry = get_detail_registry().lock().unwrap(); - let mut root = IntrospectionNode::new(ResourceType::App, "".into(), "".into()); - - // Build the introspection tree directly from the detail registry - for (full_path, _detail) in detail_registry.iter() { - let parts: Vec<&str> = full_path.split('/').collect(); - let mut current_node = &mut root; - - for (i, part) in parts.iter().enumerate() { - // Find the index of the existing child - let existing_child_index = current_node - .children - .iter() - .position(|n| n.pattern == *part); - - let child_index = if let Some(child_index) = existing_child_index { - child_index - } else { - // If it doesn't exist, create a new node and get its index - let child_full_path = parts[..=i].join("/"); - // Determine the kind based on whether this path exists as a resource in the detail registry - let kind = if detail_registry - .get(&child_full_path) - .is_some_and(|d| d.is_resource) - { - ResourceType::Resource - } else { - ResourceType::Scope - }; - let new_node = IntrospectionNode::new(kind, part.to_string(), child_full_path); - current_node.children.push(new_node); - current_node.children.len() - 1 - }; - - // Get a mutable reference to the child node - current_node = &mut current_node.children[child_index]; - - // If this node is marked as a resource, update its methods and guards - if let ResourceType::Resource = current_node.kind { - if let Some(detail) = detail_registry.get(¤t_node.full_path) { - update_unique(&mut current_node.methods, &detail.methods); - update_unique(&mut current_node.guards, &detail.guards); - } - } +impl IntrospectionCollector { + pub fn new() -> Self { + Self { + details: HashMap::new(), } } - *get_registry().lock().unwrap() = root; + pub fn register_pattern_detail( + &mut self, + full_path: String, + methods: Vec, + guards: Vec, + is_resource: bool, + ) { + self.details + .entry(full_path) + .and_modify(|d| { + update_unique(&mut d.methods, &methods); + update_unique(&mut d.guards, &guards); + if !d.is_resource && is_resource { + d.is_resource = true; + } + }) + .or_insert(RouteDetail { + methods, + guards, + is_resource, + }); + } + + pub fn finalize(&mut self) -> IntrospectionTree { + let detail_registry = std::mem::take(&mut self.details); + let mut root = IntrospectionNode::new(ResourceType::App, "".into(), "".into()); + + for (full_path, _) in detail_registry.iter() { + let parts: Vec<&str> = full_path.split('/').collect(); + let mut current_node = &mut root; + + for (i, part) in parts.iter().enumerate() { + let existing_child_index = current_node + .children + .iter() + .position(|n| n.pattern == *part); + + let child_index = if let Some(idx) = existing_child_index { + idx + } else { + let child_full_path = parts[..=i].join("/"); + let kind = if detail_registry + .get(&child_full_path) + .is_some_and(|d| d.is_resource) + { + ResourceType::Resource + } else { + ResourceType::Scope + }; + let new_node = IntrospectionNode::new(kind, part.to_string(), child_full_path); + current_node.children.push(new_node); + current_node.children.len() - 1 + }; + + current_node = &mut current_node.children[child_index]; + + if let ResourceType::Resource = current_node.kind { + if let Some(detail) = detail_registry.get(¤t_node.full_path) { + update_unique(&mut current_node.methods, &detail.methods); + update_unique(&mut current_node.guards, &detail.guards); + } + } + } + } + + IntrospectionTree { root } + } +} + +#[derive(Clone)] +pub struct IntrospectionTree { + pub root: IntrospectionNode, +} + +impl IntrospectionTree { + pub fn report_as_text(&self) -> String { + let report_items: Vec = (&self.root).into(); + + let mut buf = String::new(); + for item in report_items { + writeln!( + buf, + "{} => Methods: {:?} | Guards: {:?}", + item.full_path, item.methods, item.guards + ) + .unwrap(); + } + + buf + } + + pub fn report_as_json(&self) -> String { + let report_items: Vec = (&self.root).into(); + serde_json::to_string_pretty(&report_items).unwrap() + } } fn update_unique(existing: &mut Vec, new_items: &[T]) { @@ -208,56 +207,3 @@ fn update_unique(existing: &mut Vec, new_items: &[T]) { } } } - -pub(crate) fn register_pattern_detail( - full_path: String, - methods: Vec, - guards: Vec, - is_resource: bool, -) { - if !is_designated_thread() { - return; - } - initialize_detail_registry(); - let mut reg = get_detail_registry().lock().unwrap(); - reg.entry(full_path) - .and_modify(|d| { - update_unique(&mut d.methods, &methods); - update_unique(&mut d.guards, &guards); - // If the existing entry was not a resource but the new one is, update the kind - if !d.is_resource && is_resource { - d.is_resource = true; - } - }) - .or_insert(RouteDetail { - methods, - guards, - is_resource, - }); -} - -pub fn introspection_report_as_text() -> String { - let registry = get_registry(); - let node = registry.lock().unwrap(); - let report_items: Vec = (&*node).into(); - - let mut buf = String::new(); - for item in report_items { - writeln!( - buf, - "{} => Methods: {:?} | Guards: {:?}", - item.full_path, item.methods, item.guards - ) - .unwrap(); - } - - buf -} - -pub fn introspection_report_as_json() -> String { - let registry = get_registry(); - let node = registry.lock().unwrap(); - let report_items: Vec = (&*node).into(); - - serde_json::to_string_pretty(&report_items).unwrap() -} diff --git a/actix-web/src/resource.rs b/actix-web/src/resource.rs index df5f05fb0..7b20727c6 100644 --- a/actix-web/src/resource.rs +++ b/actix-web/src/resource.rs @@ -432,7 +432,7 @@ where }; #[cfg(feature = "experimental-introspection")] { - use crate::{http::Method, introspection}; + use crate::http::Method; let guards_routes = routes.iter().map(|r| r.guards()).collect::>(); @@ -464,7 +464,7 @@ where }) .collect::>(); - introspection::register_pattern_detail( + config.introspector.borrow_mut().register_pattern_detail( full_path.clone(), methods, guard_names,