Refactor route registration to use RouteInfo struct & cargo clippy

This commit is contained in:
Guillermo Céspedes Tabárez 2025-12-31 16:03:43 -03:00
parent 68f01edb72
commit b75fcd8fac
4 changed files with 113 additions and 106 deletions

View File

@ -187,13 +187,16 @@ impl AppService {
let parent_scope_id = self.scope_id_stack.last().copied(); let parent_scope_id = self.scope_id_stack.last().copied();
for full_path in full_paths { for full_path in full_paths {
self.introspector.borrow_mut().register_service( let info = crate::introspection::RouteInfo::new(
full_path, full_path,
methods.clone(), methods.clone(),
guard_names.clone(), guard_names.clone(),
guard_details.clone(), guard_details.clone(),
patterns.clone(), patterns.clone(),
resource_name.clone(), resource_name.clone(),
);
self.introspector.borrow_mut().register_service(
info,
is_resource, is_resource,
is_prefix, is_prefix,
scope_id, scope_id,

View File

@ -41,6 +41,37 @@ pub struct RouteDetail {
is_resource: bool, is_resource: bool,
} }
/// Input data for registering routes with the introspector.
#[derive(Clone)]
pub struct RouteInfo {
full_path: String,
methods: Vec<Method>,
guards: Vec<String>,
guard_details: Vec<GuardReport>,
patterns: Vec<String>,
resource_name: Option<String>,
}
impl RouteInfo {
pub fn new(
full_path: String,
methods: Vec<Method>,
guards: Vec<String>,
guard_details: Vec<GuardReport>,
patterns: Vec<String>,
resource_name: Option<String>,
) -> Self {
Self {
full_path,
methods,
guards,
guard_details,
patterns,
resource_name,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct GuardReport { pub struct GuardReport {
pub name: String, pub name: String,
@ -273,28 +304,15 @@ impl IntrospectionCollector {
pub fn register_service( pub fn register_service(
&mut self, &mut self,
full_path: String, info: RouteInfo,
methods: Vec<Method>,
guards: Vec<String>,
guard_details: Vec<GuardReport>,
patterns: Vec<String>,
resource_name: Option<String>,
is_resource: bool, is_resource: bool,
is_prefix: bool, is_prefix: bool,
scope_id: Option<usize>, scope_id: Option<usize>,
parent_scope_id: Option<usize>, parent_scope_id: Option<usize>,
) { ) {
let full_path = normalize_path(&full_path); let full_path = normalize_path(&info.full_path);
self.register_pattern_detail( self.register_pattern_detail(&full_path, &info, is_resource);
full_path.clone(),
methods.clone(),
guards.clone(),
guard_details.clone(),
patterns.clone(),
resource_name.clone(),
is_resource,
);
self.registrations.push(Registration { self.registrations.push(Registration {
order: self.next_registration_order, order: self.next_registration_order,
@ -303,33 +321,16 @@ impl IntrospectionCollector {
parent_scope_id, parent_scope_id,
full_path, full_path,
is_prefix, is_prefix,
methods, methods: info.methods,
guards, guards: info.guards,
}); });
self.next_registration_order += 1; self.next_registration_order += 1;
} }
pub fn register_route( pub fn register_route(&mut self, info: RouteInfo, scope_id: Option<usize>) {
&mut self, let full_path = normalize_path(&info.full_path);
full_path: String,
methods: Vec<Method>,
guards: Vec<String>,
guard_details: Vec<GuardReport>,
patterns: Vec<String>,
resource_name: Option<String>,
scope_id: Option<usize>,
) {
let full_path = normalize_path(&full_path);
self.register_pattern_detail( self.register_pattern_detail(&full_path, &info, true);
full_path.clone(),
methods.clone(),
guards.clone(),
guard_details.clone(),
patterns.clone(),
resource_name.clone(),
true,
);
self.registrations.push(Registration { self.registrations.push(Registration {
order: self.next_registration_order, order: self.next_registration_order,
@ -338,8 +339,8 @@ impl IntrospectionCollector {
parent_scope_id: None, parent_scope_id: None,
full_path, full_path,
is_prefix: false, is_prefix: false,
methods, methods: info.methods,
guards, guards: info.guards,
}); });
self.next_registration_order += 1; self.next_registration_order += 1;
} }
@ -366,36 +367,32 @@ impl IntrospectionCollector {
/// Registers details for a route pattern. /// Registers details for a route pattern.
pub fn register_pattern_detail( pub fn register_pattern_detail(
&mut self, &mut self,
full_path: String, full_path: &str,
methods: Vec<Method>, info: &RouteInfo,
guards: Vec<String>,
guard_details: Vec<GuardReport>,
patterns: Vec<String>,
resource_name: Option<String>,
is_resource: bool, is_resource: bool,
) { ) {
let full_path = normalize_path(&full_path); let full_path = normalize_path(full_path);
self.details self.details
.entry(full_path) .entry(full_path)
.and_modify(|d| { .and_modify(|d| {
update_unique(&mut d.methods, &methods); update_unique(&mut d.methods, &info.methods);
update_unique(&mut d.guards, &guards); update_unique(&mut d.guards, &info.guards);
merge_guard_reports(&mut d.guard_details, &guard_details); merge_guard_reports(&mut d.guard_details, &info.guard_details);
update_unique(&mut d.patterns, &patterns); update_unique(&mut d.patterns, &info.patterns);
if d.resource_name.is_none() { if d.resource_name.is_none() {
d.resource_name = resource_name.clone(); d.resource_name = info.resource_name.clone();
} }
if !d.is_resource && is_resource { if !d.is_resource && is_resource {
d.is_resource = true; d.is_resource = true;
} }
}) })
.or_insert(RouteDetail { .or_insert(RouteDetail {
methods, methods: info.methods.clone(),
guards, guards: info.guards.clone(),
guard_details, guard_details: info.guard_details.clone(),
patterns, patterns: info.patterns.clone(),
resource_name, resource_name: info.resource_name.clone(),
is_resource, is_resource,
}); });
} }
@ -413,13 +410,8 @@ impl IntrospectionCollector {
let mut assembled = String::new(); let mut assembled = String::new();
for part in parts.iter() { for part in parts.iter() {
if assembled.is_empty() { assembled.push('/');
assembled.push('/'); assembled.push_str(part);
assembled.push_str(part);
} else {
assembled.push('/');
assembled.push_str(part);
}
let child_full_path = assembled.clone(); let child_full_path = assembled.clone();
let existing_child_index = current_node let existing_child_index = current_node
@ -939,18 +931,36 @@ avoid exposing introspection endpoints in production"
mod tests { mod tests {
use super::*; use super::*;
fn route_info(
full_path: &str,
methods: Vec<Method>,
guards: Vec<String>,
guard_details: Vec<GuardReport>,
patterns: Vec<String>,
resource_name: Option<String>,
) -> RouteInfo {
RouteInfo::new(
full_path.to_string(),
methods,
guards,
guard_details,
patterns,
resource_name,
)
}
#[test] #[test]
fn report_includes_resources_without_methods() { fn report_includes_resources_without_methods() {
let mut collector = IntrospectionCollector::new(); let mut collector = IntrospectionCollector::new();
collector.register_route( let info = route_info(
"/no-guards".to_string(), "/no-guards",
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
None, None,
None,
); );
collector.register_route(info, None);
let tree = collector.finalize(); let tree = collector.finalize();
let items: Vec<IntrospectionReportItem> = (&tree.root).into(); let items: Vec<IntrospectionReportItem> = (&tree.root).into();
@ -979,15 +989,15 @@ mod tests {
}], }],
}]; }];
collector.register_route( let info = route_info(
"/meta".to_string(), "/meta",
vec![Method::GET], vec![Method::GET],
vec!["Header(accept, text/plain)".to_string()], vec!["Header(accept, text/plain)".to_string()],
guard_details, guard_details,
vec!["/meta".to_string()], vec!["/meta".to_string()],
Some("meta-resource".to_string()), Some("meta-resource".to_string()),
None,
); );
collector.register_route(info, None);
let tree = collector.finalize(); let tree = collector.finalize();
let items: Vec<IntrospectionReportItem> = (&tree.root).into(); let items: Vec<IntrospectionReportItem> = (&tree.root).into();
@ -1024,15 +1034,15 @@ mod tests {
#[test] #[test]
fn conflicting_method_guards_mark_unreachable() { fn conflicting_method_guards_mark_unreachable() {
let mut collector = IntrospectionCollector::new(); let mut collector = IntrospectionCollector::new();
collector.register_route( let info = route_info(
"/all-guard".to_string(), "/all-guard",
vec![Method::GET, Method::POST], vec![Method::GET, Method::POST],
vec!["AllGuard(GET, POST)".to_string()], vec!["AllGuard(GET, POST)".to_string()],
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
None, None,
None,
); );
collector.register_route(info, None);
let tree = collector.finalize(); let tree = collector.finalize();
let items: Vec<IntrospectionReportItem> = (&tree.root).into(); let items: Vec<IntrospectionReportItem> = (&tree.root).into();
@ -1052,50 +1062,44 @@ mod tests {
let mut collector = IntrospectionCollector::new(); let mut collector = IntrospectionCollector::new();
let scope_a = collector.next_scope_id(); let scope_a = collector.next_scope_id();
collector.register_service( let info = route_info(
"/extra".to_string(), "/extra",
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
None, None,
true,
true,
Some(scope_a),
None,
); );
collector.register_route( collector.register_service(info, true, true, Some(scope_a), None);
"/extra/ping".to_string(), let info = route_info(
"/extra/ping",
vec![Method::GET], vec![Method::GET],
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
None, None,
Some(scope_a),
); );
collector.register_route(info, Some(scope_a));
let scope_b = collector.next_scope_id(); let scope_b = collector.next_scope_id();
collector.register_service( let info = route_info(
"/extra".to_string(), "/extra",
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
None, None,
true,
true,
Some(scope_b),
None,
); );
collector.register_route( collector.register_service(info, true, true, Some(scope_b), None);
"/extra/ping".to_string(), let info = route_info(
"/extra/ping",
vec![Method::POST], vec![Method::POST],
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
None, None,
Some(scope_b),
); );
collector.register_route(info, Some(scope_b));
let tree = collector.finalize(); let tree = collector.finalize();
let items: Vec<IntrospectionReportItem> = (&tree.root).into(); let items: Vec<IntrospectionReportItem> = (&tree.root).into();
@ -1121,24 +1125,24 @@ mod tests {
fn shadowed_routes_include_context() { fn shadowed_routes_include_context() {
let mut collector = IntrospectionCollector::new(); let mut collector = IntrospectionCollector::new();
collector.register_route( let info = route_info(
"/shadow".to_string(), "/shadow",
vec![Method::GET], vec![Method::GET],
vec!["GET".to_string()], vec!["GET".to_string()],
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
None, None,
None,
); );
collector.register_route( collector.register_route(info, None);
"/shadow".to_string(), let info = route_info(
"/shadow",
vec![Method::GET], vec![Method::GET],
vec!["GET".to_string()], vec!["GET".to_string()],
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
None, None,
None,
); );
collector.register_route(info, None);
let tree = collector.finalize(); let tree = collector.finalize();
let items: Vec<IntrospectionReportItem> = (&tree.root).into(); let items: Vec<IntrospectionReportItem> = (&tree.root).into();

View File

@ -473,15 +473,18 @@ where
); );
for full_path in &full_paths { for full_path in &full_paths {
config.introspector.borrow_mut().register_route( let info = crate::introspection::RouteInfo::new(
full_path.clone(), full_path.clone(),
methods.clone(), methods.clone(),
guard_names.clone(), guard_names.clone(),
guard_details.clone(), guard_details.clone(),
patterns.clone(), patterns.clone(),
resource_name.clone(), resource_name.clone(),
scope_id,
); );
config
.introspector
.borrow_mut()
.register_route(info, scope_id);
} }
} }
} }

View File

@ -40,12 +40,12 @@ async fn introspection_report_includes_details_and_metadata() {
.service( .service(
web::resource(["/alpha", "/beta"]) web::resource(["/alpha", "/beta"])
.name("multi") .name("multi")
.route(web::get().to(|| async { HttpResponse::Ok() })), .route(web::get().to(HttpResponse::Ok)),
) )
.service( .service(
web::resource("/guarded") web::resource("/guarded")
.guard(guard::Header("accept", "text/plain")) .guard(guard::Header("accept", "text/plain"))
.route(web::get().to(|| async { HttpResponse::Ok() })), .route(web::get().to(HttpResponse::Ok)),
) )
.service( .service(
web::scope("/scoped") web::scope("/scoped")
@ -53,10 +53,7 @@ async fn introspection_report_includes_details_and_metadata() {
.configure(|cfg| { .configure(|cfg| {
cfg.external_resource("scope-external", "https://scope.example/{id}"); cfg.external_resource("scope-external", "https://scope.example/{id}");
}) })
.service( .service(web::resource("/item").route(web::get().to(HttpResponse::Ok))),
web::resource("/item")
.route(web::get().to(|| async { HttpResponse::Ok() })),
),
) )
.service(web::resource("/introspection").route(web::get().to(introspection_handler))) .service(web::resource("/introspection").route(web::get().to(introspection_handler)))
.service( .service(