use crate::routers::reverse::{ReverseError, ReverseResult};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Namespace {
full_path: String,
components: Vec<String>,
}
impl Namespace {
pub fn new(path: impl Into<String>) -> Self {
let full_path = path.into();
let components = full_path
.split(':')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
Self {
full_path,
components,
}
}
pub fn full_path(&self) -> &str {
&self.full_path
}
pub fn components(&self) -> &[String] {
&self.components
}
pub fn root(&self) -> Option<&str> {
self.components.first().map(|s| s.as_str())
}
pub fn parent(&self) -> Option<Namespace> {
if self.components.len() <= 1 {
return None;
}
let parent_components = &self.components[..self.components.len() - 1];
Some(Namespace::new(parent_components.join(":")))
}
pub fn leaf(&self) -> Option<&str> {
self.components.last().map(|s| s.as_str())
}
pub fn is_parent_of(&self, other: &Namespace) -> bool {
if self.components.len() >= other.components.len() {
return false;
}
self.components
.iter()
.zip(other.components.iter())
.all(|(a, b)| a == b)
}
pub fn is_child_of(&self, other: &Namespace) -> bool {
other.is_parent_of(self)
}
pub fn append(&self, component: impl Into<String>) -> Namespace {
let component = component.into();
let new_path = if self.full_path.is_empty() {
component
} else {
format!("{}:{}", self.full_path, component)
};
Namespace::new(new_path)
}
pub fn depth(&self) -> usize {
self.components.len()
}
}
impl std::fmt::Display for Namespace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.full_path)
}
}
impl From<&str> for Namespace {
fn from(s: &str) -> Self {
Namespace::new(s)
}
}
impl From<String> for Namespace {
fn from(s: String) -> Self {
Namespace::new(s)
}
}
#[derive(Debug, Clone)]
pub struct NamespacedRoute {
pub full_name: String,
pub namespace: Namespace,
pub route_name: String,
pub pattern: String,
pub param_names: Vec<String>,
}
impl NamespacedRoute {
pub fn new(full_name: impl Into<String>, pattern: impl Into<String>) -> Self {
let full_name = full_name.into();
let pattern = pattern.into();
let parts: Vec<&str> = full_name.split(':').collect();
let (route_name, namespace) = if parts.len() >= 2 {
let namespace_end = parts.len() - 1;
let ns = parts[..namespace_end].join(":");
let rn = parts[namespace_end].to_string();
(rn, Namespace::new(ns))
} else {
(full_name.clone(), Namespace::new(""))
};
let param_names = extract_param_names(&pattern);
Self {
full_name,
namespace,
route_name,
pattern,
param_names,
}
}
pub fn resolve(&self, params: &[(&str, &str)]) -> ReverseResult<String> {
let param_map: HashMap<&str, &str> = params.iter().copied().collect();
let mut result = self.pattern.clone();
for param_name in &self.param_names {
let value = param_map.get(param_name.as_str()).ok_or_else(|| {
ReverseError::Validation(format!("missing parameter: {}", param_name))
})?;
let placeholder = format!("{{{}}}", param_name);
result = result.replace(&placeholder, value);
}
Ok(result)
}
}
pub fn extract_param_names(pattern: &str) -> Vec<String> {
let mut params = Vec::new();
let mut chars = pattern.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '{' {
let mut param_name = String::new();
while let Some(&next_ch) = chars.peek() {
if next_ch == '}' {
chars.next(); params.push(param_name);
break;
}
param_name.push(chars.next().unwrap());
}
}
}
params
}
#[derive(Debug, Clone)]
pub struct NamespaceResolver {
routes: HashMap<String, NamespacedRoute>,
namespace_index: HashMap<String, Vec<String>>,
}
impl NamespaceResolver {
pub fn new() -> Self {
Self {
routes: HashMap::new(),
namespace_index: HashMap::new(),
}
}
pub fn register(&mut self, full_name: impl Into<String>, pattern: impl Into<String>) {
let full_name = full_name.into();
let route = NamespacedRoute::new(&full_name, pattern);
let ns_path = route.namespace.full_path().to_string();
self.namespace_index
.entry(ns_path)
.or_default()
.push(full_name.clone());
self.routes.insert(full_name, route);
}
pub fn resolve(&self, name: &str, params: &[(&str, &str)]) -> ReverseResult<String> {
let route = self
.routes
.get(name)
.ok_or_else(|| ReverseError::NotFound(name.to_string()))?;
route.resolve(params)
}
pub fn list_routes_in_namespace(&self, namespace: &str) -> Vec<&NamespacedRoute> {
let ns = Namespace::new(namespace);
self.routes
.values()
.filter(|route| route.namespace == ns)
.collect()
}
pub fn list_child_namespaces(&self, namespace: &str) -> Vec<String> {
let parent_ns = Namespace::new(namespace);
let parent_depth = parent_ns.depth();
let mut children = std::collections::HashSet::new();
for route in self.routes.values() {
if route.namespace.is_child_of(&parent_ns) {
if let Some(child_component) = route.namespace.components().get(parent_depth) {
children.insert(child_component.clone());
}
}
}
let mut result: Vec<String> = children.into_iter().collect();
result.sort();
result
}
pub fn list_all_namespaces(&self) -> Vec<String> {
let mut namespaces: Vec<String> = self.namespace_index.keys().cloned().collect();
namespaces.sort();
namespaces
}
pub fn all_routes(&self) -> Vec<&NamespacedRoute> {
self.routes.values().collect()
}
pub fn has_route(&self, name: &str) -> bool {
self.routes.contains_key(name)
}
pub fn route_count(&self) -> usize {
self.routes.len()
}
pub fn namespace_count(&self) -> usize {
self.namespace_index.len()
}
}
impl Default for NamespaceResolver {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_namespace_creation() {
let ns = Namespace::new("api:v1:users");
assert_eq!(ns.full_path(), "api:v1:users");
assert_eq!(ns.components(), &["api", "v1", "users"]);
}
#[test]
fn test_namespace_root() {
let ns = Namespace::new("api:v1:users");
assert_eq!(ns.root(), Some("api"));
let empty = Namespace::new("");
assert_eq!(empty.root(), None);
}
#[test]
fn test_namespace_parent() {
let ns = Namespace::new("api:v1:users");
let parent = ns.parent().unwrap();
assert_eq!(parent.full_path(), "api:v1");
let root = Namespace::new("api");
assert!(root.parent().is_none());
}
#[test]
fn test_namespace_leaf() {
let ns = Namespace::new("api:v1:users");
assert_eq!(ns.leaf(), Some("users"));
}
#[test]
fn test_namespace_is_parent_of() {
let parent = Namespace::new("api:v1");
let child = Namespace::new("api:v1:users");
let other = Namespace::new("api:v2");
assert!(parent.is_parent_of(&child));
assert!(!parent.is_parent_of(&other));
assert!(!child.is_parent_of(&parent));
}
#[test]
fn test_namespace_append() {
let parent = Namespace::new("api:v1");
let child = parent.append("users");
assert_eq!(child.full_path(), "api:v1:users");
}
#[test]
fn test_namespace_depth() {
assert_eq!(Namespace::new("api").depth(), 1);
assert_eq!(Namespace::new("api:v1").depth(), 2);
assert_eq!(Namespace::new("api:v1:users").depth(), 3);
}
#[test]
fn test_extract_param_names() {
let params = extract_param_names("/users/{id}/posts/{post_id}/");
assert_eq!(params, vec!["id", "post_id"]);
let no_params = extract_param_names("/users/");
assert!(no_params.is_empty());
}
#[test]
fn test_namespaced_route_creation() {
let route = NamespacedRoute::new("api:v1:users:detail", "/api/v1/users/{id}/");
assert_eq!(route.namespace.full_path(), "api:v1:users");
assert_eq!(route.route_name, "detail");
assert_eq!(route.pattern, "/api/v1/users/{id}/");
assert_eq!(route.param_names, vec!["id"]);
}
#[test]
fn test_namespaced_route_resolve() {
let route = NamespacedRoute::new("api:v1:users:detail", "/api/v1/users/{id}/");
let url = route.resolve(&[("id", "123")]).unwrap();
assert_eq!(url, "/api/v1/users/123/");
}
#[test]
fn test_namespace_resolver_register_and_resolve() {
let mut resolver = NamespaceResolver::new();
resolver.register("api:v1:users:detail", "/api/v1/users/{id}/");
let url = resolver
.resolve("api:v1:users:detail", &[("id", "123")])
.unwrap();
assert_eq!(url, "/api/v1/users/123/");
}
#[test]
fn test_namespace_resolver_list_routes() {
let mut resolver = NamespaceResolver::new();
resolver.register("api:v1:users:list", "/api/v1/users/");
resolver.register("api:v1:users:detail", "/api/v1/users/{id}/");
resolver.register("api:v2:users:list", "/api/v2/users/");
let routes = resolver.list_routes_in_namespace("api:v1:users");
assert_eq!(routes.len(), 2);
}
#[test]
fn test_namespace_resolver_list_child_namespaces() {
let mut resolver = NamespaceResolver::new();
resolver.register("api:v1:users:list", "/api/v1/users/");
resolver.register("api:v1:posts:list", "/api/v1/posts/");
resolver.register("api:v2:users:list", "/api/v2/users/");
let children = resolver.list_child_namespaces("api:v1");
assert_eq!(children.len(), 2);
assert!(children.contains(&"users".to_string()));
assert!(children.contains(&"posts".to_string()));
}
#[test]
fn test_namespace_resolver_has_route() {
let mut resolver = NamespaceResolver::new();
resolver.register("api:v1:users:list", "/api/v1/users/");
assert!(resolver.has_route("api:v1:users:list"));
assert!(!resolver.has_route("api:v2:users:list"));
}
}