use crate::routers::namespace::Namespace;
use hyper::Method;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouteInfo {
pub path: String,
pub methods: Vec<String>,
pub name: Option<String>,
pub namespace: Option<String>,
pub route_name: Option<String>,
pub params: Vec<String>,
pub metadata: HashMap<String, String>,
}
impl RouteInfo {
pub fn new(
path: impl Into<String>,
methods: Vec<Method>,
name: Option<impl Into<String>>,
) -> Self {
let path = path.into();
let name = name.map(|n| n.into());
let methods: Vec<String> = methods.iter().map(|m| m.as_str().to_string()).collect();
let params = super::namespace::extract_param_names(&path);
let (namespace, route_name) = if let Some(ref n) = name {
let parts: Vec<&str> = n.split(':').collect();
if parts.len() >= 2 {
let namespace_end = parts.len() - 1;
let ns = parts[..namespace_end].join(":");
let rn = parts[namespace_end].to_string();
(Some(ns), Some(rn))
} else {
(None, Some(n.clone()))
}
} else {
(None, None)
};
Self {
path,
methods,
name,
namespace,
route_name,
params,
metadata: HashMap::new(),
}
}
pub fn add_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.metadata.insert(key.into(), value.into());
}
pub fn supports_method(&self, method: &Method) -> bool {
self.methods.contains(&method.as_str().to_string())
}
pub fn namespace_object(&self) -> Option<Namespace> {
self.namespace.as_ref().map(Namespace::new)
}
}
pub struct RouteInspector {
routes: Vec<RouteInfo>,
path_index: HashMap<String, usize>,
name_index: HashMap<String, usize>,
}
impl RouteInspector {
pub fn new() -> Self {
Self {
routes: Vec::new(),
path_index: HashMap::new(),
name_index: HashMap::new(),
}
}
pub fn add_route(
&mut self,
path: impl Into<String>,
methods: Vec<Method>,
name: Option<impl Into<String>>,
metadata: Option<HashMap<String, String>>,
) {
let path = path.into();
let mut route = RouteInfo::new(&path, methods, name);
if let Some(meta) = metadata {
route.metadata = meta;
}
let index = self.routes.len();
self.path_index.insert(path.clone(), index);
if let Some(ref name) = route.name {
self.name_index.insert(name.clone(), index);
}
self.routes.push(route);
}
pub fn all_routes(&self) -> &[RouteInfo] {
&self.routes
}
pub fn find_by_path(&self, path: &str) -> Option<&RouteInfo> {
self.path_index.get(path).map(|&idx| &self.routes[idx])
}
pub fn find_by_name(&self, name: &str) -> Option<&RouteInfo> {
self.name_index.get(name).map(|&idx| &self.routes[idx])
}
pub fn find_by_path_prefix(&self, prefix: &str) -> Vec<&RouteInfo> {
self.routes
.iter()
.filter(|route| route.path.starts_with(prefix))
.collect()
}
pub fn find_by_namespace(&self, namespace: &str) -> Vec<&RouteInfo> {
self.routes
.iter()
.filter(|route| {
route
.namespace
.as_ref()
.map(|ns| {
ns == namespace || ns.starts_with(&format!("{}:", namespace))
})
.unwrap_or(false)
})
.collect()
}
pub fn find_by_method(&self, method: &Method) -> Vec<&RouteInfo> {
self.routes
.iter()
.filter(|route| route.supports_method(method))
.collect()
}
pub fn all_namespaces(&self) -> Vec<String> {
let mut namespaces = HashSet::new();
for route in &self.routes {
if let Some(ref ns_str) = route.namespace {
let parts: Vec<&str> = ns_str.split(':').collect();
let mut current_path = String::new();
for part in parts {
if !current_path.is_empty() {
current_path.push(':');
}
current_path.push_str(part);
namespaces.insert(current_path.clone());
}
}
}
let mut result: Vec<String> = namespaces.into_iter().collect();
result.sort();
result
}
pub fn all_methods(&self) -> Vec<Method> {
let mut methods: HashSet<String> = HashSet::new();
for route in &self.routes {
for method in &route.methods {
methods.insert(method.clone());
}
}
let mut result: Vec<Method> = methods.into_iter().filter_map(|m| m.parse().ok()).collect();
result.sort_by(|a, b| a.as_str().cmp(b.as_str()));
result
}
pub fn statistics(&self) -> RouteStatistics {
let total_routes = self.routes.len();
let total_namespaces = self.all_namespaces().len();
let total_methods = self.all_methods().len();
let routes_with_params = self
.routes
.iter()
.filter(|route| !route.params.is_empty())
.count();
let routes_with_names = self
.routes
.iter()
.filter(|route| route.name.is_some())
.count();
RouteStatistics {
total_routes,
total_namespaces,
total_methods,
routes_with_params,
routes_with_names,
}
}
pub fn route_count(&self) -> usize {
self.routes.len()
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(&self.routes)
}
pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
serde_yaml::to_string(&self.routes)
}
}
impl Default for RouteInspector {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouteStatistics {
pub total_routes: usize,
pub total_namespaces: usize,
pub total_methods: usize,
pub routes_with_params: usize,
pub routes_with_names: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_route_info_creation() {
let info = RouteInfo::new("/users/{id}/", vec![Method::GET], Some("users:detail"));
assert_eq!(info.path, "/users/{id}/");
assert_eq!(info.params, vec!["id"]);
assert_eq!(info.name, Some("users:detail".to_string()));
assert_eq!(info.namespace, Some("users".to_string()));
assert_eq!(info.route_name, Some("detail".to_string()));
}
#[test]
fn test_route_info_supports_method() {
let info = RouteInfo::new("/users/", vec![Method::GET, Method::POST], None::<String>);
assert!(info.supports_method(&Method::GET));
assert!(info.supports_method(&Method::POST));
assert!(!info.supports_method(&Method::DELETE));
}
#[test]
fn test_route_info_metadata() {
let mut info = RouteInfo::new("/users/", vec![Method::GET], None::<String>);
info.add_metadata("description", "List users");
assert_eq!(
info.metadata.get("description"),
Some(&"List users".to_string())
);
}
#[test]
fn test_route_inspector_add_and_count() {
let mut inspector = RouteInspector::new();
inspector.add_route(
"/users/",
vec![Method::GET],
None::<String>,
None::<std::collections::HashMap<String, String>>,
);
inspector.add_route(
"/posts/",
vec![Method::GET],
None::<String>,
None::<std::collections::HashMap<String, String>>,
);
assert_eq!(inspector.route_count(), 2);
}
#[test]
fn test_route_inspector_find_by_path() {
let mut inspector = RouteInspector::new();
inspector.add_route("/users/", vec![Method::GET], Some("users:list"), None);
let route = inspector.find_by_path("/users/").unwrap();
assert_eq!(route.name, Some("users:list".to_string()));
}
#[test]
fn test_route_inspector_find_by_name() {
let mut inspector = RouteInspector::new();
inspector.add_route("/users/", vec![Method::GET], Some("users:list"), None);
let route = inspector.find_by_name("users:list").unwrap();
assert_eq!(route.path, "/users/");
}
#[test]
fn test_route_inspector_find_by_prefix() {
let mut inspector = RouteInspector::new();
inspector.add_route(
"/api/v1/users/",
vec![Method::GET],
None::<String>,
None::<std::collections::HashMap<String, String>>,
);
inspector.add_route(
"/api/v1/posts/",
vec![Method::GET],
None::<String>,
None::<std::collections::HashMap<String, String>>,
);
inspector.add_route(
"/api/v2/users/",
vec![Method::GET],
None::<String>,
None::<std::collections::HashMap<String, String>>,
);
let routes = inspector.find_by_path_prefix("/api/v1");
assert_eq!(routes.len(), 2);
}
#[test]
fn test_route_inspector_find_by_namespace() {
let mut inspector = RouteInspector::new();
inspector.add_route(
"/users/",
vec![Method::GET],
Some("api:v1:users:list"),
None,
);
inspector.add_route(
"/posts/",
vec![Method::GET],
Some("api:v1:posts:list"),
None,
);
inspector.add_route(
"/users/",
vec![Method::GET],
Some("api:v2:users:list"),
None,
);
let routes = inspector.find_by_namespace("api:v1");
assert_eq!(routes.len(), 2);
}
#[test]
fn test_route_inspector_all_namespaces() {
let mut inspector = RouteInspector::new();
inspector.add_route(
"/users/",
vec![Method::GET],
Some("api:v1:users:list"),
None,
);
inspector.add_route(
"/posts/",
vec![Method::GET],
Some("api:v2:posts:list"),
None,
);
let namespaces = inspector.all_namespaces();
assert_eq!(namespaces.len(), 5);
assert!(namespaces.contains(&"api".to_string()));
assert!(namespaces.contains(&"api:v1".to_string()));
assert!(namespaces.contains(&"api:v1:users".to_string()));
assert!(namespaces.contains(&"api:v2".to_string()));
assert!(namespaces.contains(&"api:v2:posts".to_string()));
}
#[test]
fn test_route_inspector_statistics() {
let mut inspector = RouteInspector::new();
inspector.add_route("/users/", vec![Method::GET], Some("api:users:list"), None);
inspector.add_route(
"/users/{id}/",
vec![Method::GET],
Some("api:users:detail"),
None,
);
let stats = inspector.statistics();
assert_eq!(stats.total_routes, 2);
assert_eq!(stats.routes_with_params, 1);
assert_eq!(stats.routes_with_names, 2);
}
#[test]
fn test_route_inspector_to_json() {
let mut inspector = RouteInspector::new();
inspector.add_route("/users/", vec![Method::GET], Some("users:list"), None);
let json = inspector.to_json().unwrap();
assert!(json.contains("users:list"));
}
}