Compare commits

..

No commits in common. "d1706dcdd647a40a7ad4107b4738b93f5d57c4de" and "c8a7271d21c8f8e28d9f5bfe9d7a805d37de3ee9" have entirely different histories.

7 changed files with 138 additions and 137 deletions

View File

@ -49,7 +49,7 @@ jobs:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.51.2
uses: taiki-e/install-action@v2.50.10
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -83,7 +83,7 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
- name: Install just, cargo-hack
uses: taiki-e/install-action@v2.51.2
uses: taiki-e/install-action@v2.50.10
with:
tool: just,cargo-hack

View File

@ -64,7 +64,7 @@ jobs:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.51.2
uses: taiki-e/install-action@v2.50.10
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -113,7 +113,7 @@ jobs:
toolchain: nightly
- name: Install just
uses: taiki-e/install-action@v2.51.2
uses: taiki-e/install-action@v2.50.10
with:
tool: just

View File

@ -24,7 +24,7 @@ jobs:
components: llvm-tools
- name: Install just, cargo-llvm-cov, cargo-nextest
uses: taiki-e/install-action@v2.51.2
uses: taiki-e/install-action@v2.50.10
with:
tool: just,cargo-llvm-cov,cargo-nextest
@ -32,7 +32,7 @@ jobs:
run: just test-coverage-codecov
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.4.3
uses: codecov/codecov-action@v5.4.2
with:
files: codecov.json
fail_ci_if_error: true

View File

@ -77,7 +77,7 @@ jobs:
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
- name: Install just
uses: taiki-e/install-action@v2.51.2
uses: taiki-e/install-action@v2.50.10
with:
tool: just

View File

@ -31,24 +31,68 @@ async fn main() -> std::io::Result<()> {
age: u8,
}
// GET /introspection for JSON response
async fn introspection_handler_json() -> impl Responder {
use actix_web::introspection::introspection_report_as_json;
// GET /introspection
#[actix_web::get("/introspection")]
async fn introspection_handler() -> impl Responder {
use std::fmt::Write;
let report = introspection_report_as_json();
HttpResponse::Ok()
.content_type("application/json")
.body(report)
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<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}
#[actix_web::get("/item/{id}")]
async fn get_item(path: web::Path<u32>) -> impl Responder {
@ -166,22 +210,6 @@ 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")
@ -241,6 +269,7 @@ async fn main() -> std::io::Result<()> {
)
.to(HttpResponse::MethodNotAllowed),
)
.service(introspection_handler)
})
.workers(1)
.bind("127.0.0.1:8080")?;

View File

@ -132,7 +132,7 @@ where
#[cfg(feature = "experimental-introspection")]
{
crate::introspection::finalize_registry();
crate::introspection::register_rmap(&rmap);
}
Ok(AppInitService {

View File

@ -1,6 +1,5 @@
use std::{
collections::HashMap,
fmt::Write as FmtWrite,
sync::{
atomic::{AtomicBool, Ordering},
Mutex, OnceLock,
@ -8,16 +7,14 @@ use std::{
thread,
};
use serde::Serialize;
use crate::http::Method;
use crate::{http::Method, rmap::ResourceMap};
static REGISTRY: OnceLock<Mutex<IntrospectionNode>> = OnceLock::new();
static DETAIL_REGISTRY: OnceLock<Mutex<HashMap<String, RouteDetail>>> = OnceLock::new();
static DESIGNATED_THREAD: OnceLock<thread::ThreadId> = OnceLock::new();
static IS_INITIALIZED: AtomicBool = AtomicBool::new(false);
fn initialize_registry() {
pub fn initialize_registry() {
REGISTRY.get_or_init(|| {
Mutex::new(IntrospectionNode::new(
ResourceType::App,
@ -27,30 +24,20 @@ fn initialize_registry() {
});
}
fn get_registry() -> &'static Mutex<IntrospectionNode> {
pub fn get_registry() -> &'static Mutex<IntrospectionNode> {
REGISTRY.get().expect("Registry not initialized")
}
fn initialize_detail_registry() {
pub fn initialize_detail_registry() {
DETAIL_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()));
}
fn get_detail_registry() -> &'static Mutex<HashMap<String, RouteDetail>> {
pub fn get_detail_registry() -> &'static Mutex<HashMap<String, RouteDetail>> {
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<Method>,
@ -68,18 +55,12 @@ pub enum ResourceType {
#[derive(Debug, Clone)]
pub struct IntrospectionNode {
pub kind: ResourceType,
pub pattern: String,
pub full_path: String,
pub pattern: String, // Local pattern
pub full_path: String, // Full path
pub methods: Vec<Method>,
pub guards: Vec<String>,
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 {
pub fn new(kind: ResourceType, pattern: String, full_path: String) -> Self {
@ -92,58 +73,67 @@ impl IntrospectionNode {
children: Vec::new(),
}
}
}
impl From<&IntrospectionNode> for Vec<IntrospectionReportItem> {
fn from(node: &IntrospectionNode) -> Self {
fn collect_report_items(
node: &IntrospectionNode,
parent_path: &str,
report_items: &mut Vec<IntrospectionReportItem>,
) {
let full_path = if parent_path.is_empty() {
node.pattern.clone()
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 {
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<String> = 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);
result.push_str(&format!("{}{}", " ".repeat(indent), self.full_path));
}
}
let mut report_items = Vec::new();
collect_report_items(node, "/", &mut report_items);
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
}
}
pub(crate) fn finalize_registry() {
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) {
if !is_designated_thread() {
return;
}
@ -199,6 +189,14 @@ pub(crate) fn finalize_registry() {
}
*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]) {
@ -209,7 +207,7 @@ fn update_unique<T: Clone + PartialEq>(existing: &mut Vec<T>, new_items: &[T]) {
}
}
pub(crate) fn register_pattern_detail(
pub fn register_pattern_detail(
full_path: String,
methods: Vec<Method>,
guards: Vec<String>,
@ -235,29 +233,3 @@ pub(crate) 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<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()
}