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] 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() +}