mirror of https://github.com/fafhrd91/actix-web
feat(introspection): enhance introspection handlers for JSON and plain text responses
This commit is contained in:
parent
c8a7271d21
commit
23fed2298e
|
@ -31,68 +31,24 @@ async fn main() -> std::io::Result<()> {
|
||||||
age: u8,
|
age: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /introspection
|
// GET /introspection for JSON response
|
||||||
#[actix_web::get("/introspection")]
|
async fn introspection_handler_json() -> impl Responder {
|
||||||
async fn introspection_handler() -> impl Responder {
|
use actix_web::introspection::introspection_report_as_json;
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
use actix_web::introspection::{get_registry, initialize_registry};
|
let report = introspection_report_as_json();
|
||||||
|
HttpResponse::Ok()
|
||||||
initialize_registry();
|
.content_type("application/json")
|
||||||
let registry = get_registry();
|
.body(report)
|
||||||
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<String> =
|
|
||||||
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)
|
// 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}
|
// GET /api/v1/item/{id} and GET /v1/item/{id}
|
||||||
#[actix_web::get("/item/{id}")]
|
#[actix_web::get("/item/{id}")]
|
||||||
async fn get_item(path: web::Path<u32>) -> impl Responder {
|
async fn get_item(path: web::Path<u32>) -> impl Responder {
|
||||||
|
@ -210,6 +166,22 @@ async fn main() -> std::io::Result<()> {
|
||||||
// Create the HTTP server with all the routes and handlers
|
// Create the HTTP server with all the routes and handlers
|
||||||
let server = HttpServer::new(|| {
|
let server = HttpServer::new(|| {
|
||||||
App::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
|
// API endpoints under /api
|
||||||
.service(
|
.service(
|
||||||
web::scope("/api")
|
web::scope("/api")
|
||||||
|
@ -269,7 +241,6 @@ async fn main() -> std::io::Result<()> {
|
||||||
)
|
)
|
||||||
.to(HttpResponse::MethodNotAllowed),
|
.to(HttpResponse::MethodNotAllowed),
|
||||||
)
|
)
|
||||||
.service(introspection_handler)
|
|
||||||
})
|
})
|
||||||
.workers(1)
|
.workers(1)
|
||||||
.bind("127.0.0.1:8080")?;
|
.bind("127.0.0.1:8080")?;
|
||||||
|
|
|
@ -132,7 +132,7 @@ where
|
||||||
|
|
||||||
#[cfg(feature = "experimental-introspection")]
|
#[cfg(feature = "experimental-introspection")]
|
||||||
{
|
{
|
||||||
crate::introspection::register_rmap(&rmap);
|
crate::introspection::finalize_registry();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(AppInitService {
|
Ok(AppInitService {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
fmt::Write as FmtWrite,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
Mutex, OnceLock,
|
Mutex, OnceLock,
|
||||||
|
@ -7,14 +8,16 @@ use std::{
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{http::Method, rmap::ResourceMap};
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::http::Method;
|
||||||
|
|
||||||
static REGISTRY: OnceLock<Mutex<IntrospectionNode>> = OnceLock::new();
|
static REGISTRY: OnceLock<Mutex<IntrospectionNode>> = OnceLock::new();
|
||||||
static DETAIL_REGISTRY: OnceLock<Mutex<HashMap<String, RouteDetail>>> = OnceLock::new();
|
static DETAIL_REGISTRY: OnceLock<Mutex<HashMap<String, RouteDetail>>> = OnceLock::new();
|
||||||
static DESIGNATED_THREAD: OnceLock<thread::ThreadId> = OnceLock::new();
|
static DESIGNATED_THREAD: OnceLock<thread::ThreadId> = OnceLock::new();
|
||||||
static IS_INITIALIZED: AtomicBool = AtomicBool::new(false);
|
static IS_INITIALIZED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
pub fn initialize_registry() {
|
fn initialize_registry() {
|
||||||
REGISTRY.get_or_init(|| {
|
REGISTRY.get_or_init(|| {
|
||||||
Mutex::new(IntrospectionNode::new(
|
Mutex::new(IntrospectionNode::new(
|
||||||
ResourceType::App,
|
ResourceType::App,
|
||||||
|
@ -24,20 +27,30 @@ pub fn initialize_registry() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_registry() -> &'static Mutex<IntrospectionNode> {
|
fn get_registry() -> &'static Mutex<IntrospectionNode> {
|
||||||
REGISTRY.get().expect("Registry not initialized")
|
REGISTRY.get().expect("Registry not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn initialize_detail_registry() {
|
fn initialize_detail_registry() {
|
||||||
DETAIL_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()));
|
DETAIL_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_detail_registry() -> &'static Mutex<HashMap<String, RouteDetail>> {
|
fn get_detail_registry() -> &'static Mutex<HashMap<String, RouteDetail>> {
|
||||||
DETAIL_REGISTRY
|
DETAIL_REGISTRY
|
||||||
.get()
|
.get()
|
||||||
.expect("Detail registry not initialized")
|
.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)]
|
#[derive(Clone)]
|
||||||
pub struct RouteDetail {
|
pub struct RouteDetail {
|
||||||
methods: Vec<Method>,
|
methods: Vec<Method>,
|
||||||
|
@ -55,12 +68,18 @@ pub enum ResourceType {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct IntrospectionNode {
|
pub struct IntrospectionNode {
|
||||||
pub kind: ResourceType,
|
pub kind: ResourceType,
|
||||||
pub pattern: String, // Local pattern
|
pub pattern: String,
|
||||||
pub full_path: String, // Full path
|
pub full_path: String,
|
||||||
pub methods: Vec<Method>,
|
pub methods: Vec<Method>,
|
||||||
pub guards: Vec<String>,
|
pub guards: Vec<String>,
|
||||||
pub children: Vec<IntrospectionNode>,
|
pub children: Vec<IntrospectionNode>,
|
||||||
}
|
}
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct IntrospectionReportItem {
|
||||||
|
pub full_path: String,
|
||||||
|
pub methods: Vec<String>,
|
||||||
|
pub guards: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl IntrospectionNode {
|
impl IntrospectionNode {
|
||||||
pub fn new(kind: ResourceType, pattern: String, full_path: String) -> Self {
|
pub fn new(kind: ResourceType, pattern: String, full_path: String) -> Self {
|
||||||
|
@ -73,67 +92,58 @@ impl IntrospectionNode {
|
||||||
children: Vec::new(),
|
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<Method>, guards: &Vec<String>| !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
|
impl From<&IntrospectionNode> for Vec<IntrospectionReportItem> {
|
||||||
if let ResourceType::Resource = self.kind {
|
fn from(node: &IntrospectionNode) -> Self {
|
||||||
let methods = if self.methods.is_empty() {
|
fn collect_report_items(
|
||||||
"".to_string()
|
node: &IntrospectionNode,
|
||||||
|
parent_path: &str,
|
||||||
|
report_items: &mut Vec<IntrospectionReportItem>,
|
||||||
|
) {
|
||||||
|
let full_path = if parent_path.is_empty() {
|
||||||
|
node.pattern.clone()
|
||||||
} else {
|
} else {
|
||||||
format!(" Methods: {:?}", self.methods)
|
format!(
|
||||||
};
|
"{}/{}",
|
||||||
let guards = if self.guards.is_empty() {
|
parent_path.trim_end_matches('/'),
|
||||||
"".to_string()
|
node.pattern.trim_start_matches('/')
|
||||||
} else {
|
)
|
||||||
format!(" Guards: {:?}", self.guards)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Highlight final endpoints with ANSI codes for bold and green color
|
if !node.methods.is_empty() || !node.guards.is_empty() {
|
||||||
result.push_str(&format!("\x1b[1;32m{}{}\x1b[0m\n", methods, guards));
|
// Filter guards that are already represented in methods
|
||||||
} else {
|
let filtered_guards: Vec<String> = node
|
||||||
// For non-resource nodes, just add a newline
|
.guards
|
||||||
result.push('\n');
|
.iter()
|
||||||
}
|
.filter(|guard| {
|
||||||
|
!node
|
||||||
|
.methods
|
||||||
|
.iter()
|
||||||
|
.any(|method| method.to_string() == **guard)
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
for child in &self.children {
|
report_items.push(IntrospectionReportItem {
|
||||||
result.push_str(&child.display(indent + 2)); // Increase indent for children
|
full_path: full_path.clone(),
|
||||||
}
|
methods: node.methods.iter().map(|m| m.to_string()).collect(),
|
||||||
|
guards: filtered_guards,
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
for child in &node.children {
|
||||||
|
collect_report_items(child, &full_path, report_items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut report_items = Vec::new();
|
||||||
|
collect_report_items(node, "/", &mut report_items);
|
||||||
|
report_items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn finalize_registry() {
|
||||||
if !is_designated_thread() {
|
if !is_designated_thread() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -189,14 +199,6 @@ pub fn register_rmap(_rmap: &ResourceMap) {
|
||||||
}
|
}
|
||||||
|
|
||||||
*get_registry().lock().unwrap() = root;
|
*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<T: Clone + PartialEq>(existing: &mut Vec<T>, new_items: &[T]) {
|
fn update_unique<T: Clone + PartialEq>(existing: &mut Vec<T>, new_items: &[T]) {
|
||||||
|
@ -207,7 +209,7 @@ fn update_unique<T: Clone + PartialEq>(existing: &mut Vec<T>, new_items: &[T]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_pattern_detail(
|
pub(crate) fn register_pattern_detail(
|
||||||
full_path: String,
|
full_path: String,
|
||||||
methods: Vec<Method>,
|
methods: Vec<Method>,
|
||||||
guards: Vec<String>,
|
guards: Vec<String>,
|
||||||
|
@ -233,3 +235,29 @@ pub fn register_pattern_detail(
|
||||||
is_resource,
|
is_resource,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn introspection_report_as_text() -> String {
|
||||||
|
let registry = get_registry();
|
||||||
|
let node = registry.lock().unwrap();
|
||||||
|
let report_items: Vec<IntrospectionReportItem> = (&*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<IntrospectionReportItem> = (&*node).into();
|
||||||
|
|
||||||
|
serde_json::to_string_pretty(&report_items).unwrap()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue