use std::collections::BTreeMap;
pub const IMPLICIT_METHODS: [&str; 2] = ["HEAD", "OPTIONS"];
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RouteMeta {
pub summary: Option<String>,
}
impl RouteMeta {
pub fn summary<S: Into<String>>(summary: S) -> Self {
Self {
summary: Some(summary.into()),
}
}
}
pub type PathParams = BTreeMap<String, String>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RouteEntry {
pub pattern: String,
pub methods: Vec<(String, RouteMeta)>,
}
impl RouteEntry {
pub fn has_method(&self, method: &str) -> bool {
self.methods.iter().any(|(name, _)| name == method)
}
}
fn segments(path: &str) -> Vec<&str> {
let trimmed = if path.len() > 1 {
path.strip_suffix('/').unwrap_or(path)
} else {
path
};
trimmed.split('/').collect()
}
pub fn match_path(pattern: &str, pathname: &str) -> Option<PathParams> {
let pattern_segments = segments(pattern);
let path_segments = segments(pathname);
let mut params = PathParams::new();
for (index, expected) in pattern_segments.iter().enumerate() {
if let Some(name) = expected.strip_prefix('*') {
let rest = path_segments.get(index..).unwrap_or(&[]).join("/");
if !name.is_empty() {
params.insert(name.to_string(), rest);
}
return Some(params);
}
let actual = path_segments.get(index)?;
match expected.strip_prefix(':') {
Some(name) => {
if actual.is_empty() {
return None;
}
params.insert(name.to_string(), (*actual).to_string());
}
None => {
if expected != actual {
return None;
}
}
}
}
if path_segments.len() > pattern_segments.len() {
return None;
}
Some(params)
}
#[derive(Debug, Clone, Default)]
pub struct RouteTable {
routes: Vec<RouteEntry>,
}
impl RouteTable {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, method: &str, pattern: &str, meta: RouteMeta) {
let method = method.to_uppercase();
let entry = match self
.routes
.iter_mut()
.position(|entry| entry.pattern == pattern)
{
Some(index) => &mut self.routes[index],
None => {
self.routes.push(RouteEntry {
pattern: pattern.to_string(),
methods: Vec::new(),
});
self.routes.last_mut().expect("just pushed")
}
};
match entry.methods.iter_mut().find(|(name, _)| *name == method) {
Some(existing) => existing.1 = meta,
None => entry.methods.push((method, meta)),
}
}
pub fn remove(&mut self, method: &str, pattern: &str) {
let method = method.to_uppercase();
let Some(index) = self
.routes
.iter()
.position(|entry| entry.pattern == pattern)
else {
return;
};
self.routes[index]
.methods
.retain(|(name, _)| *name != method);
if self.routes[index].methods.is_empty() {
self.routes.remove(index);
}
}
pub fn find(&self, pathname: &str) -> Option<&RouteEntry> {
self.match_path(pathname).map(|(entry, _)| entry)
}
pub fn match_path(&self, pathname: &str) -> Option<(&RouteEntry, PathParams)> {
self.routes
.iter()
.find_map(|entry| match_path(&entry.pattern, pathname).map(|params| (entry, params)))
}
pub fn allowed_methods(&self, pathname: &str) -> Option<Vec<String>> {
let entry = self.find(pathname)?;
Some(self.allowed_methods_of(entry))
}
pub fn allowed_methods_of(&self, entry: &RouteEntry) -> Vec<String> {
let mut methods: Vec<String> = entry.methods.iter().map(|(name, _)| name.clone()).collect();
if methods.iter().any(|method| method == "GET") && !methods.iter().any(|m| m == "HEAD") {
methods.push("HEAD".to_string());
}
if !methods.iter().any(|method| method == "OPTIONS") {
methods.push("OPTIONS".to_string());
}
methods.sort();
methods.dedup();
methods
}
pub fn entries(&self) -> &[RouteEntry] {
&self.routes
}
pub fn describe(&self) -> Vec<RouteDescription> {
let mut descriptions: Vec<RouteDescription> = self
.routes
.iter()
.map(|entry| RouteDescription {
path: entry.pattern.clone(),
methods: self.allowed_methods_of(entry),
summary: entry
.methods
.iter()
.find_map(|(_, meta)| meta.summary.clone()),
})
.collect();
descriptions.sort_by(|left, right| left.path.cmp(&right.path));
descriptions
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RouteDescription {
pub path: String,
pub methods: Vec<String>,
pub summary: Option<String>,
}