mirror of https://github.com/fafhrd91/actix-web
tweak
This commit is contained in:
parent
1facfec04b
commit
a1a26e9b53
|
|
@ -126,7 +126,7 @@ compat = ["compat-routing-macros-force-pub"]
|
||||||
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.
|
||||||
experimental-introspection = []
|
experimental-introspection = ["serde/derive"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ impl RouteInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
pub struct GuardReport {
|
pub struct GuardReport {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
@ -81,6 +82,7 @@ pub struct GuardReport {
|
||||||
pub details: Vec<GuardDetailReport>,
|
pub details: Vec<GuardDetailReport>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
pub enum GuardDetailReport {
|
pub enum GuardDetailReport {
|
||||||
|
|
@ -89,6 +91,7 @@ pub enum GuardDetailReport {
|
||||||
Generic { value: String },
|
Generic { value: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
pub struct HeaderReport {
|
pub struct HeaderReport {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
@ -99,6 +102,7 @@ pub struct HeaderReport {
|
||||||
///
|
///
|
||||||
/// `origin_scope` is the scope path where the external resource was registered. It is informational
|
/// `origin_scope` is the scope path where the external resource was registered. It is informational
|
||||||
/// only and does not affect URL generation or routing; external resources are always global.
|
/// only and does not affect URL generation or routing; external resources are always global.
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
pub struct ExternalResourceReportItem {
|
pub struct ExternalResourceReportItem {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
|
@ -132,6 +136,7 @@ struct ShadowingContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Node type within an introspection tree.
|
/// Node type within an introspection tree.
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum ResourceType {
|
pub enum ResourceType {
|
||||||
/// The application root.
|
/// The application root.
|
||||||
|
|
@ -151,6 +156,7 @@ fn resource_type_label(kind: ResourceType) -> &'static str {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A node in the introspection tree.
|
/// A node in the introspection tree.
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct IntrospectionNode {
|
pub struct IntrospectionNode {
|
||||||
/// The node's classification.
|
/// The node's classification.
|
||||||
|
|
@ -178,6 +184,7 @@ pub struct IntrospectionNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A flattened report item for a route.
|
/// A flattened report item for a route.
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct IntrospectionReportItem {
|
pub struct IntrospectionReportItem {
|
||||||
/// Full path for the route.
|
/// Full path for the route.
|
||||||
|
|
@ -460,6 +467,7 @@ impl IntrospectionCollector {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The finalized introspection tree.
|
/// The finalized introspection tree.
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct IntrospectionTree {
|
pub struct IntrospectionTree {
|
||||||
/// Root node of the tree.
|
/// Root node of the tree.
|
||||||
|
|
@ -793,19 +801,122 @@ fn guards_only_methods(guards: &[String], methods: &[Method]) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_conflicting_methods(methods: &[Method], guards: &[String]) -> bool {
|
fn has_conflicting_methods(methods: &[Method], guards: &[String]) -> bool {
|
||||||
let method_names = method_set(methods);
|
// This check is best-effort: it tries to determine if the conjunction of method guards can
|
||||||
if method_names.len() <= 1 {
|
// match any single HTTP method. It relies on guard names since introspection details flatten
|
||||||
|
// guard structure.
|
||||||
|
if method_set(methods).len() <= 1 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let has_any = guards.iter().any(|name| name.starts_with("AnyGuard("));
|
fn split_top_level_args(mut args: &str) -> Vec<&str> {
|
||||||
let has_all = guards.iter().any(|name| name.starts_with("AllGuard("));
|
args = args.trim();
|
||||||
|
if args.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
if has_all {
|
let mut parts = Vec::new();
|
||||||
return true;
|
let mut depth = 0usize;
|
||||||
|
let mut start = 0usize;
|
||||||
|
|
||||||
|
for (idx, ch) in args.char_indices() {
|
||||||
|
match ch {
|
||||||
|
'(' => depth += 1,
|
||||||
|
')' => depth = depth.saturating_sub(1),
|
||||||
|
',' if depth == 0 => {
|
||||||
|
parts.push(args[start..idx].trim());
|
||||||
|
start = idx + 1;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.push(args[start..].trim());
|
||||||
|
parts.into_iter().filter(|s| !s.is_empty()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
!has_any
|
fn parse_method(name: &str) -> Option<BTreeSet<String>> {
|
||||||
|
name.trim().parse::<Method>().ok().map(|method| {
|
||||||
|
let mut set = BTreeSet::new();
|
||||||
|
set.insert(method.to_string());
|
||||||
|
set
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn union_methods(
|
||||||
|
left: Option<BTreeSet<String>>,
|
||||||
|
right: Option<BTreeSet<String>>,
|
||||||
|
) -> Option<BTreeSet<String>> {
|
||||||
|
match (left, right) {
|
||||||
|
// If any branch doesn't constrain methods, the disjunction doesn't either.
|
||||||
|
(None, _) | (_, None) => None,
|
||||||
|
(Some(mut a), Some(b)) => {
|
||||||
|
a.extend(b);
|
||||||
|
Some(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn intersect_methods(
|
||||||
|
left: Option<BTreeSet<String>>,
|
||||||
|
right: Option<BTreeSet<String>>,
|
||||||
|
) -> Option<BTreeSet<String>> {
|
||||||
|
match (left, right) {
|
||||||
|
(None, x) | (x, None) => x,
|
||||||
|
(Some(a), Some(b)) => Some(a.intersection(&b).cloned().collect()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guard_possible_methods(name: &str) -> Option<BTreeSet<String>> {
|
||||||
|
let name = name.trim();
|
||||||
|
if name.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(set) = parse_method(name) {
|
||||||
|
return Some(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(inner) = name
|
||||||
|
.strip_prefix("AnyGuard(")
|
||||||
|
.and_then(|s| s.strip_suffix(')'))
|
||||||
|
{
|
||||||
|
let mut acc = Some(BTreeSet::new());
|
||||||
|
for arg in split_top_level_args(inner) {
|
||||||
|
acc = union_methods(acc, guard_possible_methods(arg));
|
||||||
|
if acc.is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(inner) = name
|
||||||
|
.strip_prefix("AllGuard(")
|
||||||
|
.and_then(|s| s.strip_suffix(')'))
|
||||||
|
{
|
||||||
|
let mut acc = None;
|
||||||
|
for arg in split_top_level_args(inner) {
|
||||||
|
acc = intersect_methods(acc, guard_possible_methods(arg));
|
||||||
|
if matches!(acc, Some(ref set) if set.is_empty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `Not(...)` (and unknown/custom guard names) are treated as not restricting methods.
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut possible = None;
|
||||||
|
for guard in guards {
|
||||||
|
possible = intersect_methods(possible, guard_possible_methods(guard));
|
||||||
|
if matches!(possible, Some(ref set) if set.is_empty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn method_set(methods: &[Method]) -> BTreeSet<String> {
|
fn method_set(methods: &[Method]) -> BTreeSet<String> {
|
||||||
|
|
@ -890,9 +1001,14 @@ fn format_reachability(item: &IntrospectionReportItem) -> String {
|
||||||
if item.reachability_notes.is_empty() {
|
if item.reachability_notes.is_empty() {
|
||||||
" | PotentiallyUnreachable".to_string()
|
" | PotentiallyUnreachable".to_string()
|
||||||
} else {
|
} else {
|
||||||
|
let notes = item
|
||||||
|
.reachability_notes
|
||||||
|
.iter()
|
||||||
|
.map(|note| sanitize_text(note))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
format!(
|
format!(
|
||||||
" | PotentiallyUnreachable | Notes: {:?}",
|
" | PotentiallyUnreachable | Notes: {:?}",
|
||||||
item.reachability_notes
|
notes
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1060,6 +1176,61 @@ mod tests {
|
||||||
.contains(&"conflicting_method_guards".to_string()));
|
.contains(&"conflicting_method_guards".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allguard_anyguard_does_not_mark_conflict_when_methods_are_feasible() {
|
||||||
|
let mut collector = IntrospectionCollector::new();
|
||||||
|
let info = route_info(
|
||||||
|
"/feasible",
|
||||||
|
vec![Method::GET, Method::POST],
|
||||||
|
vec![
|
||||||
|
"AllGuard(AnyGuard(GET, POST), Header(x, y))".to_string(),
|
||||||
|
"Header(x, y)".to_string(),
|
||||||
|
],
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
collector.register_route(info, None);
|
||||||
|
let tree = collector.finalize();
|
||||||
|
let items: Vec<IntrospectionReportItem> = (&tree.root).into();
|
||||||
|
|
||||||
|
let item = items
|
||||||
|
.iter()
|
||||||
|
.find(|item| item.full_path == "/feasible")
|
||||||
|
.expect("missing route");
|
||||||
|
|
||||||
|
assert!(!item.potentially_unreachable);
|
||||||
|
assert!(!item
|
||||||
|
.reachability_notes
|
||||||
|
.contains(&"conflicting_method_guards".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allguard_anyguard_marks_conflict_when_methods_are_impossible() {
|
||||||
|
let mut collector = IntrospectionCollector::new();
|
||||||
|
let info = route_info(
|
||||||
|
"/impossible",
|
||||||
|
vec![Method::GET, Method::POST],
|
||||||
|
vec!["AllGuard(GET, AnyGuard(POST))".to_string()],
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
collector.register_route(info, None);
|
||||||
|
let tree = collector.finalize();
|
||||||
|
let items: Vec<IntrospectionReportItem> = (&tree.root).into();
|
||||||
|
|
||||||
|
let item = items
|
||||||
|
.iter()
|
||||||
|
.find(|item| item.full_path == "/impossible")
|
||||||
|
.expect("missing route");
|
||||||
|
|
||||||
|
assert!(item.potentially_unreachable);
|
||||||
|
assert!(item
|
||||||
|
.reachability_notes
|
||||||
|
.contains(&"conflicting_method_guards".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shadowed_scopes_mark_routes() {
|
fn shadowed_scopes_mark_routes() {
|
||||||
let mut collector = IntrospectionCollector::new();
|
let mut collector = IntrospectionCollector::new();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue