put all the items behind feature gate

This commit is contained in:
Yuki Okushi 2026-02-11 09:07:13 +09:00
parent 9a0190a1cc
commit 1facfec04b
5 changed files with 87 additions and 88 deletions

View File

@ -165,10 +165,7 @@ impl AppService {
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let guard_names = guard_list let guard_names = guard_list.iter().map(|g| g.name()).collect::<Vec<_>>();
.iter()
.map(|g| g.name().to_string())
.collect::<Vec<_>>();
let guard_details = crate::introspection::guard_reports_from_iter(guard_list.iter()); let guard_details = crate::introspection::guard_reports_from_iter(guard_list.iter());
let is_resource = nested.is_none(); let is_resource = nested.is_none();

View File

@ -1,4 +1,4 @@
use super::{Guard, GuardContext, GuardDetail}; use super::{Guard, GuardContext};
use crate::http::header::Accept; use crate::http::header::Accept;
/// A guard that verifies that an `Accept` header is present and it contains a compatible MIME type. /// A guard that verifies that an `Accept` header is present and it contains a compatible MIME type.
@ -64,35 +64,25 @@ impl Guard for Acceptable {
false false
} }
#[cfg(feature = "experimental-introspection")]
fn name(&self) -> String { fn name(&self) -> String {
#[cfg(feature = "experimental-introspection")] if self.match_star_star {
{ format!("Acceptable({}, match_star_star=true)", self.mime)
if self.match_star_star { } else {
format!("Acceptable({}, match_star_star=true)", self.mime) format!("Acceptable({})", self.mime)
} else {
format!("Acceptable({})", self.mime)
}
}
#[cfg(not(feature = "experimental-introspection"))]
{
std::any::type_name::<Self>().to_string()
} }
} }
fn details(&self) -> Option<Vec<GuardDetail>> { #[cfg(feature = "experimental-introspection")]
#[cfg(feature = "experimental-introspection")] fn details(&self) -> Option<Vec<super::GuardDetail>> {
{ let mut details = Vec::new();
let mut details = Vec::new(); details.push(super::GuardDetail::Generic(format!("mime={}", self.mime)));
details.push(GuardDetail::Generic(format!("mime={}", self.mime))); if self.match_star_star {
if self.match_star_star { details.push(super::GuardDetail::Generic(
details.push(GuardDetail::Generic("match_star_star=true".to_string())); "match_star_star=true".to_string(),
} ));
Some(details)
}
#[cfg(not(feature = "experimental-introspection"))]
{
None
} }
Some(details)
} }
} }
@ -135,12 +125,12 @@ mod tests {
let details = guard.details().expect("missing guard details"); let details = guard.details().expect("missing guard details");
assert!(details.iter().any(|detail| match detail { assert!(details.iter().any(|detail| match detail {
GuardDetail::Generic(value) => value == "match_star_star=true", crate::guard::GuardDetail::Generic(value) => value == "match_star_star=true",
_ => false, _ => false,
})); }));
let expected = format!("mime={}", mime::APPLICATION_JSON); let expected = format!("mime={}", mime::APPLICATION_JSON);
assert!(details.iter().any(|detail| match detail { assert!(details.iter().any(|detail| match detail {
GuardDetail::Generic(value) => value == &expected, crate::guard::GuardDetail::Generic(value) => value == &expected,
_ => false, _ => false,
})); }));
assert_eq!( assert_eq!(

View File

@ -1,6 +1,6 @@
use actix_http::{header, uri::Uri, RequestHead, Version}; use actix_http::{header, uri::Uri, RequestHead, Version};
use super::{Guard, GuardContext, GuardDetail}; use super::{Guard, GuardContext};
/// Creates a guard that matches requests targeting a specific host. /// Creates a guard that matches requests targeting a specific host.
/// ///
@ -118,39 +118,27 @@ impl Guard for HostGuard {
true true
} }
#[cfg(feature = "experimental-introspection")]
fn name(&self) -> String { fn name(&self) -> String {
#[cfg(feature = "experimental-introspection")] if let Some(ref scheme) = self.scheme {
{ format!("Host({}, scheme={})", self.host, scheme)
if let Some(ref scheme) = self.scheme { } else {
format!("Host({}, scheme={})", self.host, scheme) format!("Host({})", self.host)
} else {
format!("Host({})", self.host)
}
}
#[cfg(not(feature = "experimental-introspection"))]
{
std::any::type_name::<Self>().to_string()
} }
} }
fn details(&self) -> Option<Vec<GuardDetail>> { #[cfg(feature = "experimental-introspection")]
#[cfg(feature = "experimental-introspection")] fn details(&self) -> Option<Vec<super::GuardDetail>> {
{ let mut details = vec![super::GuardDetail::Headers(vec![(
let mut details = vec![GuardDetail::Headers(vec![( "host".to_string(),
"host".to_string(), self.host.clone(),
self.host.clone(), )])];
)])];
if let Some(ref scheme) = self.scheme { if let Some(ref scheme) = self.scheme {
details.push(GuardDetail::Generic(format!("scheme={scheme}"))); details.push(super::GuardDetail::Generic(format!("scheme={scheme}")));
} }
Some(details) Some(details)
}
#[cfg(not(feature = "experimental-introspection"))]
{
None
}
} }
} }
@ -282,13 +270,13 @@ mod tests {
let details = host.details().expect("missing guard details"); let details = host.details().expect("missing guard details");
assert!(details.iter().any(|detail| match detail { assert!(details.iter().any(|detail| match detail {
GuardDetail::Headers(headers) => headers crate::guard::GuardDetail::Headers(headers) => headers
.iter() .iter()
.any(|(name, value)| name == "host" && value == "example.com"), .any(|(name, value)| name == "host" && value == "example.com"),
_ => false, _ => false,
})); }));
assert!(details.iter().any(|detail| match detail { assert!(details.iter().any(|detail| match detail {
GuardDetail::Generic(value) => value == "scheme=https", crate::guard::GuardDetail::Generic(value) => value == "scheme=https",
_ => false, _ => false,
})); }));
assert_eq!(host.name(), "Host(example.com, scheme=https)"); assert_eq!(host.name(), "Host(example.com, scheme=https)");

View File

@ -67,6 +67,8 @@ pub use self::{
}; };
/// Enum to encapsulate various introspection details of a guard. /// Enum to encapsulate various introspection details of a guard.
#[cfg(feature = "experimental-introspection")]
#[non_exhaustive]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum GuardDetail { pub enum GuardDetail {
/// Detail associated with explicit HTTP method guards. /// Detail associated with explicit HTTP method guards.
@ -137,6 +139,7 @@ pub trait Guard {
fn check(&self, ctx: &GuardContext<'_>) -> bool; fn check(&self, ctx: &GuardContext<'_>) -> bool;
/// Returns a nominal representation of the guard. /// Returns a nominal representation of the guard.
#[cfg(feature = "experimental-introspection")]
fn name(&self) -> String { fn name(&self) -> String {
std::any::type_name::<Self>().to_string() std::any::type_name::<Self>().to_string()
} }
@ -144,6 +147,7 @@ pub trait Guard {
/// Returns detailed introspection information, when available. /// Returns detailed introspection information, when available.
/// ///
/// This is best-effort and may omit complex guard logic. /// This is best-effort and may omit complex guard logic.
#[cfg(feature = "experimental-introspection")]
fn details(&self) -> Option<Vec<GuardDetail>> { fn details(&self) -> Option<Vec<GuardDetail>> {
None None
} }
@ -153,9 +157,13 @@ impl Guard for Rc<dyn Guard> {
fn check(&self, ctx: &GuardContext<'_>) -> bool { fn check(&self, ctx: &GuardContext<'_>) -> bool {
(**self).check(ctx) (**self).check(ctx)
} }
#[cfg(feature = "experimental-introspection")]
fn name(&self) -> String { fn name(&self) -> String {
(**self).name() (**self).name()
} }
#[cfg(feature = "experimental-introspection")]
fn details(&self) -> Option<Vec<GuardDetail>> { fn details(&self) -> Option<Vec<GuardDetail>> {
(**self).details() (**self).details()
} }
@ -248,6 +256,8 @@ impl Guard for AnyGuard {
false false
} }
#[cfg(feature = "experimental-introspection")]
fn name(&self) -> String { fn name(&self) -> String {
format!( format!(
"AnyGuard({})", "AnyGuard({})",
@ -258,6 +268,8 @@ impl Guard for AnyGuard {
.join(", ") .join(", ")
) )
} }
#[cfg(feature = "experimental-introspection")]
fn details(&self) -> Option<Vec<GuardDetail>> { fn details(&self) -> Option<Vec<GuardDetail>> {
Some( Some(
self.guards self.guards
@ -318,6 +330,8 @@ impl Guard for AllGuard {
true true
} }
#[cfg(feature = "experimental-introspection")]
fn name(&self) -> String { fn name(&self) -> String {
format!( format!(
"AllGuard({})", "AllGuard({})",
@ -328,6 +342,8 @@ impl Guard for AllGuard {
.join(", ") .join(", ")
) )
} }
#[cfg(feature = "experimental-introspection")]
fn details(&self) -> Option<Vec<GuardDetail>> { fn details(&self) -> Option<Vec<GuardDetail>> {
Some( Some(
self.guards self.guards
@ -356,18 +372,15 @@ impl<G: Guard> Guard for Not<G> {
fn check(&self, ctx: &GuardContext<'_>) -> bool { fn check(&self, ctx: &GuardContext<'_>) -> bool {
!self.0.check(ctx) !self.0.check(ctx)
} }
#[cfg(feature = "experimental-introspection")]
fn name(&self) -> String { fn name(&self) -> String {
format!("Not({})", self.0.name()) format!("Not({})", self.0.name())
} }
#[cfg(feature = "experimental-introspection")]
fn details(&self) -> Option<Vec<GuardDetail>> { fn details(&self) -> Option<Vec<GuardDetail>> {
#[cfg(feature = "experimental-introspection")] Some(vec![GuardDetail::Generic(self.name())])
{
Some(vec![GuardDetail::Generic(self.name())])
}
#[cfg(not(feature = "experimental-introspection"))]
{
None
}
} }
} }
@ -398,9 +411,13 @@ impl Guard for MethodGuard {
ctx.head().method == self.0 ctx.head().method == self.0
} }
#[cfg(feature = "experimental-introspection")]
fn name(&self) -> String { fn name(&self) -> String {
self.0.to_string() self.0.to_string()
} }
#[cfg(feature = "experimental-introspection")]
fn details(&self) -> Option<Vec<GuardDetail>> { fn details(&self) -> Option<Vec<GuardDetail>> {
Some(vec![GuardDetail::HttpMethods(vec![self.0.to_string()])]) Some(vec![GuardDetail::HttpMethods(vec![self.0.to_string()])])
} }
@ -466,9 +483,13 @@ impl Guard for HeaderGuard {
false false
} }
#[cfg(feature = "experimental-introspection")]
fn name(&self) -> String { fn name(&self) -> String {
format!("Header({}, {})", self.0, self.1.to_str().unwrap_or("")) format!("Header({}, {})", self.0, self.1.to_str().unwrap_or(""))
} }
#[cfg(feature = "experimental-introspection")]
fn details(&self) -> Option<Vec<GuardDetail>> { fn details(&self) -> Option<Vec<GuardDetail>> {
Some(vec![GuardDetail::Headers(vec![( Some(vec![GuardDetail::Headers(vec![(
self.0.to_string(), self.0.to_string(),

View File

@ -12,7 +12,9 @@
//! //!
//! Notes: //! Notes:
//! - Method lists are best-effort and derived only from explicit method guards; an empty list means //! - Method lists are best-effort and derived only from explicit method guards; an empty list means
//! the route matches any method. //! no explicit method guards were observed for the node.
//! - Guard and method lists are aggregated per `full_path` and do not preserve per-route
//! correlations when multiple routes/services share the same path.
//! - Reachability hints are best-effort and should be treated as diagnostics, not a hard guarantee. //! - Reachability hints are best-effort and should be treated as diagnostics, not a hard guarantee.
//! //!
//! This feature is intended for local/non-production use. Avoid exposing introspection endpoints //! This feature is intended for local/non-production use. Avoid exposing introspection endpoints
@ -32,7 +34,7 @@ use crate::{
}; };
#[derive(Clone)] #[derive(Clone)]
pub struct RouteDetail { struct RouteDetail {
methods: Vec<Method>, methods: Vec<Method>,
guards: Vec<String>, guards: Vec<String>,
guard_details: Vec<GuardReport>, guard_details: Vec<GuardReport>,
@ -43,7 +45,7 @@ pub struct RouteDetail {
/// Input data for registering routes with the introspector. /// Input data for registering routes with the introspector.
#[derive(Clone)] #[derive(Clone)]
pub struct RouteInfo { pub(crate) struct RouteInfo {
full_path: String, full_path: String,
methods: Vec<Method>, methods: Vec<Method>,
guards: Vec<String>, guards: Vec<String>,
@ -53,7 +55,7 @@ pub struct RouteInfo {
} }
impl RouteInfo { impl RouteInfo {
pub fn new( pub(crate) fn new(
full_path: String, full_path: String,
methods: Vec<Method>, methods: Vec<Method>,
guards: Vec<String>, guards: Vec<String>,
@ -182,9 +184,12 @@ pub struct IntrospectionReportItem {
pub full_path: String, pub full_path: String,
/// Methods derived from explicit method guards. /// Methods derived from explicit method guards.
/// ///
/// An empty list indicates the route matches any method. /// An empty list indicates no explicit method guards were observed for the node.
pub methods: Vec<String>, pub methods: Vec<String>,
/// Guard names attached to the route. /// Guard names attached to the route.
///
/// This is aggregated per `full_path` and does not necessarily represent a single matching
/// condition when multiple routes/services share the same path.
pub guards: Vec<String>, pub guards: Vec<String>,
/// Structured guard details, when available. /// Structured guard details, when available.
/// ///
@ -201,7 +206,10 @@ pub struct IntrospectionReportItem {
pub patterns: Vec<String>, pub patterns: Vec<String>,
/// The type of node represented by the report item. /// The type of node represented by the report item.
pub resource_type: String, pub resource_type: String,
/// Depth within the scope tree (root = 0). /// Depth within this report tree (root = 0).
///
/// This currently corresponds to the number of path segments (for example, `/foo` has depth 1
/// and `/foo/bar` has depth 2).
pub scope_depth: usize, pub scope_depth: usize,
/// True if the route might be unreachable at runtime. /// True if the route might be unreachable at runtime.
#[serde(skip_serializing_if = "is_false")] #[serde(skip_serializing_if = "is_false")]
@ -276,7 +284,7 @@ impl From<&IntrospectionNode> for Vec<IntrospectionReportItem> {
/// Collects route details during app configuration. /// Collects route details during app configuration.
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct IntrospectionCollector { pub(crate) struct IntrospectionCollector {
details: BTreeMap<String, RouteDetail>, details: BTreeMap<String, RouteDetail>,
registrations: Vec<Registration>, registrations: Vec<Registration>,
externals: Vec<ExternalResourceReportItem>, externals: Vec<ExternalResourceReportItem>,
@ -286,7 +294,7 @@ pub struct IntrospectionCollector {
impl IntrospectionCollector { impl IntrospectionCollector {
/// Creates a new, empty collector. /// Creates a new, empty collector.
pub fn new() -> Self { pub(crate) fn new() -> Self {
Self { Self {
details: BTreeMap::new(), details: BTreeMap::new(),
registrations: Vec::new(), registrations: Vec::new(),
@ -296,13 +304,13 @@ impl IntrospectionCollector {
} }
} }
pub fn next_scope_id(&mut self) -> usize { pub(crate) fn next_scope_id(&mut self) -> usize {
let scope_id = self.next_scope_id; let scope_id = self.next_scope_id;
self.next_scope_id += 1; self.next_scope_id += 1;
scope_id scope_id
} }
pub fn register_service( pub(crate) fn register_service(
&mut self, &mut self,
info: RouteInfo, info: RouteInfo,
is_resource: bool, is_resource: bool,
@ -327,7 +335,7 @@ impl IntrospectionCollector {
self.next_registration_order += 1; self.next_registration_order += 1;
} }
pub fn register_route(&mut self, info: RouteInfo, scope_id: Option<usize>) { pub(crate) fn register_route(&mut self, info: RouteInfo, scope_id: Option<usize>) {
let full_path = normalize_path(&info.full_path); let full_path = normalize_path(&info.full_path);
self.register_pattern_detail(&full_path, &info, true); self.register_pattern_detail(&full_path, &info, true);
@ -345,7 +353,7 @@ impl IntrospectionCollector {
self.next_registration_order += 1; self.next_registration_order += 1;
} }
pub fn register_external(&mut self, rdef: &ResourceDef, origin_scope: &str) { pub(crate) fn register_external(&mut self, rdef: &ResourceDef, origin_scope: &str) {
let report = external_report_from_rdef(rdef, origin_scope); let report = external_report_from_rdef(rdef, origin_scope);
if let Some(name) = report.name.as_deref() { if let Some(name) = report.name.as_deref() {
@ -365,12 +373,7 @@ impl IntrospectionCollector {
} }
/// Registers details for a route pattern. /// Registers details for a route pattern.
pub fn register_pattern_detail( fn register_pattern_detail(&mut self, full_path: &str, info: &RouteInfo, is_resource: bool) {
&mut self,
full_path: &str,
info: &RouteInfo,
is_resource: bool,
) {
let full_path = normalize_path(full_path); let full_path = normalize_path(full_path);
self.details self.details
@ -398,7 +401,7 @@ impl IntrospectionCollector {
} }
/// Produces the finalized introspection tree. /// Produces the finalized introspection tree.
pub fn finalize(&mut self) -> IntrospectionTree { pub(crate) fn finalize(&mut self) -> IntrospectionTree {
let detail_registry = std::mem::take(&mut self.details); let detail_registry = std::mem::take(&mut self.details);
let registrations = std::mem::take(&mut self.registrations); let registrations = std::mem::take(&mut self.registrations);
let externals = std::mem::take(&mut self.externals); let externals = std::mem::take(&mut self.externals);