mirror of https://github.com/fafhrd91/actix-web
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.
This commit is contained in:
parent
013a8ec6b6
commit
d501102610
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Unreleased
|
## 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
|
## 4.10.2
|
||||||
|
|
||||||
|
|
|
@ -129,7 +129,7 @@ compat = [
|
||||||
compat-routing-macros-force-pub = ["actix-web-codegen?/compat-routing-macros-force-pub"]
|
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.
|
# Enabling the retrieval of metadata for initialized resources, including path and HTTP method.
|
||||||
resources-introspection = []
|
experimental-introspection = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
|
|
|
@ -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<u32>) -> 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<UserInfo>) -> 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)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -82,9 +82,6 @@ where
|
||||||
|
|
||||||
let (config, services) = config.into_services();
|
let (config, services) = config.into_services();
|
||||||
|
|
||||||
#[cfg(feature = "resources-introspection")]
|
|
||||||
let mut rdef_methods: Vec<(String, Vec<String>)> = Vec::new();
|
|
||||||
|
|
||||||
// complete pipeline creation.
|
// complete pipeline creation.
|
||||||
*self.factory_ref.borrow_mut() = Some(AppRoutingFactory {
|
*self.factory_ref.borrow_mut() = Some(AppRoutingFactory {
|
||||||
default,
|
default,
|
||||||
|
@ -92,24 +89,35 @@ where
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(mut rdef, srv, guards, nested)| {
|
.map(|(mut rdef, srv, guards, nested)| {
|
||||||
rmap.add(&mut rdef, nested);
|
rmap.add(&mut rdef, nested);
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
#[cfg(feature = "resources-introspection")]
|
|
||||||
{
|
{
|
||||||
let http_methods: Vec<String> =
|
use std::borrow::Borrow;
|
||||||
guards.as_ref().map_or_else(Vec::new, |g| {
|
let pat = rdef.pattern().unwrap_or("").to_string();
|
||||||
g.iter()
|
let mut methods = Vec::new();
|
||||||
.flat_map(|g| {
|
let mut guard_names = Vec::new();
|
||||||
crate::guard::HttpMethodsExtractor::extract_http_methods(
|
if let Some(gs) = guards.borrow().as_ref() {
|
||||||
&**g,
|
for g in gs.iter() {
|
||||||
)
|
let name = g.name().to_string();
|
||||||
})
|
if !guard_names.contains(&name) {
|
||||||
.collect::<Vec<_>>()
|
guard_names.push(name.clone());
|
||||||
});
|
}
|
||||||
|
if let Some(details) = g.details() {
|
||||||
rdef_methods
|
for d in details {
|
||||||
.push((rdef.pattern().unwrap_or_default().to_string(), http_methods));
|
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))
|
(rdef, srv, RefCell::new(guards))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
@ -126,11 +134,6 @@ where
|
||||||
let rmap = Rc::new(rmap);
|
let rmap = Rc::new(rmap);
|
||||||
ResourceMap::finish(&rmap);
|
ResourceMap::finish(&rmap);
|
||||||
|
|
||||||
#[cfg(feature = "resources-introspection")]
|
|
||||||
{
|
|
||||||
crate::introspection::process_introspection(Rc::clone(&rmap), rdef_methods);
|
|
||||||
}
|
|
||||||
|
|
||||||
// construct all async data factory futures
|
// construct all async data factory futures
|
||||||
let factory_futs = join_all(self.async_data_factories.iter().map(|f| f()));
|
let factory_futs = join_all(self.async_data_factories.iter().map(|f| f()));
|
||||||
|
|
||||||
|
@ -156,6 +159,11 @@ where
|
||||||
factory.create(&mut app_data);
|
factory.create(&mut app_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
{
|
||||||
|
crate::introspection::register_rmap(&rmap);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(AppInitService {
|
Ok(AppInitService {
|
||||||
service,
|
service,
|
||||||
app_data: Rc::new(app_data),
|
app_data: Rc::new(app_data),
|
||||||
|
|
|
@ -397,35 +397,6 @@ impl Guard for MethodGuard {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "resources-introspection")]
|
|
||||||
pub trait HttpMethodsExtractor {
|
|
||||||
fn extract_http_methods(&self) -> Vec<String>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "resources-introspection")]
|
|
||||||
impl HttpMethodsExtractor for dyn Guard {
|
|
||||||
fn extract_http_methods(&self) -> Vec<String> {
|
|
||||||
let methods: Vec<String> = 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 {
|
macro_rules! method_guard {
|
||||||
($method_fn:ident, $method_const:ident) => {
|
($method_fn:ident, $method_const:ident) => {
|
||||||
#[doc = concat!("Creates a guard that matches the `", stringify!($method_const), "` request method.")]
|
#[doc = concat!("Creates a guard that matches the `", stringify!($method_const), "` request method.")]
|
||||||
|
|
|
@ -1,494 +1,172 @@
|
||||||
use std::{
|
use std::{
|
||||||
rc::Rc,
|
collections::HashMap,
|
||||||
sync::{OnceLock, RwLock},
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Mutex, OnceLock,
|
||||||
|
},
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::rmap::ResourceMap;
|
use crate::{http::Method, rmap::ResourceMap};
|
||||||
|
|
||||||
/// Represents an HTTP resource registered for introspection.
|
static REGISTRY: OnceLock<Mutex<IntrospectionNode>> = OnceLock::new();
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
static DETAIL_REGISTRY: OnceLock<Mutex<HashMap<String, RouteDetail>>> = OnceLock::new();
|
||||||
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<Vec<ResourceIntrospection>> = 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<thread::ThreadId> = OnceLock::new();
|
static DESIGNATED_THREAD: OnceLock<thread::ThreadId> = OnceLock::new();
|
||||||
|
static IS_INITIALIZED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
/// Inserts a resource into the global registry, avoiding duplicates.
|
pub fn initialize_registry() {
|
||||||
pub fn register_resource(resource: ResourceIntrospection) {
|
REGISTRY.get_or_init(|| Mutex::new(IntrospectionNode::new(ResourceType::App, "".into())));
|
||||||
let mut global = RESOURCE_REGISTRY.write().unwrap();
|
|
||||||
if !global.iter().any(|r| r == &resource) {
|
|
||||||
global.push(resource);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Completes (updates) partial routes in the global registry whose path contains `marker`,
|
pub fn get_registry() -> &'static Mutex<IntrospectionNode> {
|
||||||
/// by applying the specified `prefix`.
|
REGISTRY.get().expect("Registry not initialized")
|
||||||
pub fn complete_partial_routes_with_marker(marker: &str, prefix: &str) {
|
}
|
||||||
let mut global = RESOURCE_REGISTRY.write().unwrap();
|
|
||||||
|
|
||||||
let mut updated = Vec::new();
|
pub fn initialize_detail_registry() {
|
||||||
let mut remaining = Vec::new();
|
DETAIL_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()));
|
||||||
|
}
|
||||||
|
|
||||||
// Move all items out of the current registry.
|
pub fn get_detail_registry() -> &'static Mutex<HashMap<String, RouteDetail>> {
|
||||||
for resource in global.drain(..) {
|
DETAIL_REGISTRY
|
||||||
if resource.path.contains(marker) {
|
.get()
|
||||||
// Build the full path by applying the prefix if needed.
|
.expect("Detail registry not initialized")
|
||||||
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 = ResourceIntrospection {
|
#[derive(Clone)]
|
||||||
method: resource.method,
|
pub struct RouteDetail {
|
||||||
path: full_path,
|
methods: Vec<Method>,
|
||||||
};
|
guards: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
// Add to `updated` if it's not already in there.
|
#[derive(Debug, Clone, Copy)]
|
||||||
if !updated.iter().any(|r| r == &completed) {
|
pub enum ResourceType {
|
||||||
updated.push(completed);
|
App,
|
||||||
}
|
Scope,
|
||||||
|
Resource,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct IntrospectionNode {
|
||||||
|
pub kind: ResourceType,
|
||||||
|
pub pattern: String,
|
||||||
|
pub methods: Vec<Method>,
|
||||||
|
pub guards: Vec<String>,
|
||||||
|
pub children: Vec<IntrospectionNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
// Keep this resource as-is.
|
format!(
|
||||||
remaining.push(resource);
|
"{}/{}",
|
||||||
}
|
parent_path.trim_end_matches('/'),
|
||||||
}
|
self.pattern.trim_start_matches('/')
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
// Merge updated items back with the remaining ones.
|
if !self.methods.is_empty() || !self.guards.is_empty() {
|
||||||
remaining.extend(updated);
|
let methods = if self.methods.is_empty() {
|
||||||
*global = remaining;
|
"".to_string()
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a **copy** of the global registry (safe to call from any thread).
|
|
||||||
pub fn get_registered_resources() -> Vec<ResourceIntrospection> {
|
|
||||||
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<ResourceMap>, rdef_methods: Vec<(String, Vec<String>)>) {
|
|
||||||
// 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<String> = 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()
|
|
||||||
} else {
|
} 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.
|
println!("{}{}{}{}", " ".repeat(indent), full_path, methods, guards);
|
||||||
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()
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(full_route) = chosen {
|
for child in &self.children {
|
||||||
// Register the endpoint in the global resource registry.
|
child.display(indent, &full_path);
|
||||||
register_resource(ResourceIntrospection {
|
}
|
||||||
method: http_methods.join(","),
|
}
|
||||||
path: full_route.clone(),
|
}
|
||||||
});
|
|
||||||
|
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)]
|
fn is_designated_thread() -> bool {
|
||||||
mod tests {
|
let current_id = thread::current().id();
|
||||||
use std::{num::NonZeroUsize, rc::Rc};
|
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;
|
*DESIGNATED_THREAD.get().unwrap() == current_id
|
||||||
use tokio::sync::oneshot;
|
}
|
||||||
|
|
||||||
use super::*;
|
pub fn register_rmap(rmap: &ResourceMap) {
|
||||||
use crate::rmap::ResourceMap;
|
if !is_designated_thread() {
|
||||||
|
return;
|
||||||
/// 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<ResourceMap> {
|
|
||||||
// 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.
|
initialize_registry();
|
||||||
// It processes introspection data for multiple blocks, each with a different set of routes and methods.
|
let mut root = IntrospectionNode::new(ResourceType::App, "".into());
|
||||||
fn run_full_introspection_flow() {
|
build_tree(&mut root, rmap);
|
||||||
// Block 1:
|
*get_registry().lock().unwrap() = root;
|
||||||
// rmap_vec: ["/item/{id}"]
|
|
||||||
// rdef_methods: []
|
|
||||||
process_introspection(create_resource_map(vec!["/item/{id}"]), vec![]);
|
|
||||||
|
|
||||||
// Block 2:
|
// WIP. Display the introspection tree
|
||||||
// rmap_vec: ["/info"]
|
let reg = get_registry().lock().unwrap();
|
||||||
// rdef_methods: []
|
reg.display(0, "");
|
||||||
process_introspection(create_resource_map(vec!["/info"]), vec![]);
|
}
|
||||||
|
|
||||||
// Block 3:
|
fn update_unique<T: Clone + PartialEq>(existing: &mut Vec<T>, new_items: &[T]) {
|
||||||
// rmap_vec: ["/guarded"]
|
for item in new_items {
|
||||||
// rdef_methods: []
|
if !existing.contains(item) {
|
||||||
process_introspection(create_resource_map(vec!["/guarded"]), vec![]);
|
existing.push(item.clone());
|
||||||
|
|
||||||
// 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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn register_pattern_detail(pattern: String, methods: Vec<Method>, guards: Vec<String>) {
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
|
@ -108,7 +108,7 @@ mod thin_data;
|
||||||
pub(crate) mod types;
|
pub(crate) mod types;
|
||||||
pub mod web;
|
pub mod web;
|
||||||
|
|
||||||
#[cfg(feature = "resources-introspection")]
|
#[cfg(feature = "experimental-introspection")]
|
||||||
pub mod introspection;
|
pub mod introspection;
|
||||||
|
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
|
|
|
@ -417,6 +417,8 @@ where
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
fn register(mut self, config: &mut AppService) {
|
fn register(mut self, config: &mut AppService) {
|
||||||
|
let routes = std::mem::take(&mut self.routes);
|
||||||
|
|
||||||
let guards = if self.guards.is_empty() {
|
let guards = if self.guards.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
@ -433,27 +435,28 @@ where
|
||||||
rdef.set_name(name);
|
rdef.set_name(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "resources-introspection")]
|
#[cfg(feature = "experimental-introspection")]
|
||||||
let mut rdef_methods: Vec<(String, Vec<String>)> = Vec::new();
|
|
||||||
#[cfg(feature = "resources-introspection")]
|
|
||||||
let mut rmap = crate::rmap::ResourceMap::new(ResourceDef::prefix(""));
|
|
||||||
|
|
||||||
#[cfg(feature = "resources-introspection")]
|
|
||||||
{
|
{
|
||||||
rmap.add(&mut rdef, None);
|
let pat = rdef.pattern().unwrap_or("").to_string();
|
||||||
|
let mut methods = Vec::new();
|
||||||
self.routes.iter().for_each(|r| {
|
let mut guard_names = Vec::new();
|
||||||
r.get_guards().iter().for_each(|g| {
|
for route in &routes {
|
||||||
let http_methods: Vec<String> =
|
if let Some(m) = route.get_method() {
|
||||||
crate::guard::HttpMethodsExtractor::extract_http_methods(&**g);
|
if !methods.contains(&m) {
|
||||||
rdef_methods
|
methods.push(m);
|
||||||
.push((rdef.pattern().unwrap_or_default().to_string(), http_methods));
|
}
|
||||||
});
|
}
|
||||||
});
|
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 {
|
*self.factory_ref.borrow_mut() = Some(ResourceFactory {
|
||||||
routes: self.routes,
|
routes,
|
||||||
default: self.default,
|
default: self.default,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -470,14 +473,6 @@ where
|
||||||
async { Ok(fut.await?.map_into_boxed_body()) }
|
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)
|
config.register_service(rdef, guards, endpoint, None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,16 +15,13 @@ const AVG_PATH_LEN: usize = 24;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ResourceMap {
|
pub struct ResourceMap {
|
||||||
pattern: ResourceDef,
|
pub(crate) pattern: ResourceDef,
|
||||||
|
|
||||||
/// Named resources within the tree or, for external resources, it points to isolated nodes
|
/// Named resources within the tree or, for external resources, it points to isolated nodes
|
||||||
/// outside the tree.
|
/// outside the tree.
|
||||||
named: FoldHashMap<String, Rc<ResourceMap>>,
|
named: FoldHashMap<String, Rc<ResourceMap>>,
|
||||||
|
|
||||||
parent: RefCell<Weak<ResourceMap>>,
|
parent: RefCell<Weak<ResourceMap>>,
|
||||||
|
|
||||||
/// Must be `None` for "edge" nodes.
|
/// Must be `None` for "edge" nodes.
|
||||||
nodes: Option<Vec<Rc<ResourceMap>>>,
|
pub(crate) nodes: Option<Vec<Rc<ResourceMap>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceMap {
|
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<String> {
|
|
||||||
let mut paths = Vec::new();
|
|
||||||
self.collect_full_paths(String::new(), &mut paths);
|
|
||||||
paths
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "resources-introspection")]
|
|
||||||
/// Recursive function that accumulates the full path and adds it only if the node is an endpoint (leaf).
|
|
||||||
fn collect_full_paths(&self, current_path: String, paths: &mut Vec<String>) {
|
|
||||||
// Get the current segment of the pattern
|
|
||||||
if let Some(segment) = self.pattern.pattern() {
|
|
||||||
let mut new_path = current_path;
|
|
||||||
// Add the '/' separator if necessary
|
|
||||||
if !segment.is_empty() {
|
|
||||||
if !new_path.ends_with('/') && !new_path.is_empty() && !segment.starts_with('/') {
|
|
||||||
new_path.push('/');
|
|
||||||
}
|
|
||||||
new_path.push_str(segment);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this node is an endpoint (has no children), add the full path
|
|
||||||
if self.nodes.is_none() {
|
|
||||||
paths.push(new_path.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it has children, iterate over each one
|
|
||||||
if let Some(children) = &self.nodes {
|
|
||||||
for child in children {
|
|
||||||
child.collect_full_paths(new_path.clone(), paths);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format resource map as tree structure (unfinished).
|
/// Format resource map as tree structure (unfinished).
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn tree(&self) -> String {
|
pub(crate) fn tree(&self) -> String {
|
||||||
|
|
|
@ -65,10 +65,10 @@ impl Route {
|
||||||
pub(crate) fn take_guards(&mut self) -> Vec<Box<dyn Guard>> {
|
pub(crate) fn take_guards(&mut self) -> Vec<Box<dyn Guard>> {
|
||||||
mem::take(Rc::get_mut(&mut self.guards).unwrap())
|
mem::take(Rc::get_mut(&mut self.guards).unwrap())
|
||||||
}
|
}
|
||||||
|
/// Get the names of all guards applied to this route.
|
||||||
#[cfg(feature = "resources-introspection")]
|
#[cfg(feature = "experimental-introspection")]
|
||||||
pub(crate) fn get_guards(&self) -> &Vec<Box<dyn Guard>> {
|
pub fn guard_names(&self) -> Vec<String> {
|
||||||
&self.guards
|
self.guards.iter().map(|g| g.name()).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,6 +145,23 @@ impl Route {
|
||||||
self
|
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<Method> {
|
||||||
|
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.
|
/// Add guard to the route.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
|
@ -164,6 +181,10 @@ impl Route {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn guards(&self) -> &Vec<Box<dyn Guard>> {
|
||||||
|
&self.guards
|
||||||
|
}
|
||||||
|
|
||||||
/// Set handler function, use request extractors for parameters.
|
/// Set handler function, use request extractors for parameters.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
|
|
|
@ -395,9 +395,6 @@ where
|
||||||
rmap.add(&mut rdef, None);
|
rmap.add(&mut rdef, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "resources-introspection")]
|
|
||||||
let mut rdef_methods: Vec<(String, Vec<String>)> = Vec::new();
|
|
||||||
|
|
||||||
// complete scope pipeline creation
|
// complete scope pipeline creation
|
||||||
*self.factory_ref.borrow_mut() = Some(ScopeFactory {
|
*self.factory_ref.borrow_mut() = Some(ScopeFactory {
|
||||||
default,
|
default,
|
||||||
|
@ -408,21 +405,38 @@ where
|
||||||
.map(|(mut rdef, srv, guards, nested)| {
|
.map(|(mut rdef, srv, guards, nested)| {
|
||||||
rmap.add(&mut rdef, nested);
|
rmap.add(&mut rdef, nested);
|
||||||
|
|
||||||
#[cfg(feature = "resources-introspection")]
|
#[cfg(feature = "experimental-introspection")]
|
||||||
{
|
{
|
||||||
let http_methods: Vec<String> =
|
use std::borrow::Borrow;
|
||||||
guards.as_ref().map_or_else(Vec::new, |g| {
|
|
||||||
g.iter()
|
|
||||||
.flat_map(|g| {
|
|
||||||
crate::guard::HttpMethodsExtractor::extract_http_methods(
|
|
||||||
&**g,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
});
|
|
||||||
|
|
||||||
rdef_methods
|
// Get the pattern stored in ResourceMap
|
||||||
.push((rdef.pattern().unwrap_or_default().to_string(), http_methods));
|
let pat = rdef.pattern().unwrap_or("").to_string();
|
||||||
|
let guard_list: &[Box<dyn Guard>] =
|
||||||
|
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::<Vec<_>>()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Extract guard names
|
||||||
|
let guard_names = guard_list
|
||||||
|
.iter()
|
||||||
|
.map(|g| g.name().to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Register route details for introspection
|
||||||
|
crate::introspection::register_pattern_detail(pat, methods, guard_names);
|
||||||
}
|
}
|
||||||
|
|
||||||
(rdef, srv, RefCell::new(guards))
|
(rdef, srv, RefCell::new(guards))
|
||||||
|
@ -452,14 +466,6 @@ where
|
||||||
async { Ok(fut.await?.map_into_boxed_body()) }
|
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
|
// register final service
|
||||||
config.register_service(
|
config.register_service(
|
||||||
ResourceDef::root_prefix(&self.rdef),
|
ResourceDef::root_prefix(&self.rdef),
|
||||||
|
|
Loading…
Reference in New Issue