use std::collections::HashMap;
use crate::storage::models::{CHANNEL_TYPE_SYNC, Channel, ChannelProtocol};
struct RouteEntry {
channel_name: String,
methods: Vec<String>,
segments: Vec<RouteSegment>,
priority: i64,
}
#[derive(Debug, Clone)]
enum RouteSegment {
Static(String),
Param(String),
}
#[derive(Debug, Clone)]
pub struct RouteMatch {
pub channel_name: String,
pub params: HashMap<String, String>,
}
fn parse_route_pattern(pattern: &str) -> Vec<RouteSegment> {
pattern
.split('/')
.filter(|s| !s.is_empty())
.map(|seg| {
if seg.starts_with('{') && seg.ends_with('}') {
RouteSegment::Param(seg[1..seg.len() - 1].to_string())
} else {
RouteSegment::Static(seg.to_string())
}
})
.collect()
}
fn match_segments(
segments: &[RouteSegment],
path_parts: &[&str],
) -> Option<HashMap<String, String>> {
if segments.len() != path_parts.len() {
return None;
}
let param_count = segments
.iter()
.filter(|s| matches!(s, RouteSegment::Param(_)))
.count();
let mut params = HashMap::with_capacity(param_count);
for (seg, part) in segments.iter().zip(path_parts.iter()) {
match seg {
RouteSegment::Static(expected) => {
if !expected.eq_ignore_ascii_case(part) {
return None;
}
}
RouteSegment::Param(name) => {
params.insert(name.clone(), (*part).to_string());
}
}
}
Some(params)
}
pub struct RouteTable {
entries: Vec<RouteEntry>,
}
impl RouteTable {
pub(super) fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub(super) fn build(channels: &[Channel]) -> Self {
let mut entries: Vec<RouteEntry> = channels
.iter()
.filter(|ch| {
ch.channel_type == CHANNEL_TYPE_SYNC
&& (ch.protocol == ChannelProtocol::Rest.as_str()
|| ch.protocol == ChannelProtocol::Http.as_str())
&& ch.route_pattern.is_some()
})
.filter_map(|ch| {
let pattern = ch.route_pattern.as_deref()?;
let segments = parse_route_pattern(pattern);
let methods: Vec<String> = ch
.methods
.as_deref()
.and_then(|m| serde_json::from_str::<Vec<String>>(m).ok())
.unwrap_or_default()
.into_iter()
.map(|m| m.to_uppercase())
.collect();
Some(RouteEntry {
channel_name: ch.name.clone(),
methods,
segments,
priority: ch.priority,
})
})
.collect();
entries.sort_by(|a, b| {
b.priority
.cmp(&a.priority)
.then_with(|| b.segments.len().cmp(&a.segments.len()))
});
Self { entries }
}
pub fn match_route(&self, method: &str, path: &str) -> Option<RouteMatch> {
let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
for entry in &self.entries {
if !entry.methods.is_empty()
&& !entry.methods.iter().any(|m| m.eq_ignore_ascii_case(method))
{
continue;
}
if let Some(params) = match_segments(&entry.segments, &path_parts) {
return Some(RouteMatch {
channel_name: entry.channel_name.clone(),
params,
});
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_route_pattern_simple() {
let segments = parse_route_pattern("/orders");
assert_eq!(segments.len(), 1);
assert!(matches!(&segments[0], RouteSegment::Static(s) if s == "orders"));
}
#[test]
fn test_parse_route_pattern_with_params() {
let segments = parse_route_pattern("/orders/{id}/items/{item_id}");
assert_eq!(segments.len(), 4);
assert!(matches!(&segments[0], RouteSegment::Static(s) if s == "orders"));
assert!(matches!(&segments[1], RouteSegment::Param(s) if s == "id"));
assert!(matches!(&segments[2], RouteSegment::Static(s) if s == "items"));
assert!(matches!(&segments[3], RouteSegment::Param(s) if s == "item_id"));
}
#[test]
fn test_match_segments_exact() {
let segments = parse_route_pattern("/orders/{id}");
let params = match_segments(&segments, &["orders", "123"]);
assert!(params.is_some());
assert_eq!(params.expect("test").get("id").expect("test"), "123");
}
#[test]
fn test_match_segments_no_match() {
let segments = parse_route_pattern("/orders/{id}");
assert!(match_segments(&segments, &["users", "123"]).is_none());
assert!(match_segments(&segments, &["orders"]).is_none());
assert!(match_segments(&segments, &["orders", "123", "items"]).is_none());
}
#[test]
fn test_route_table_match() {
let table = RouteTable {
entries: vec![RouteEntry {
channel_name: "orders.get".to_string(),
methods: vec!["GET".to_string()],
segments: parse_route_pattern("/orders/{id}"),
priority: 0,
}],
};
let result = table.match_route("GET", "orders/42");
assert!(result.is_some());
let rm = result.expect("test");
assert_eq!(rm.channel_name, "orders.get");
assert_eq!(rm.params.get("id").expect("test"), "42");
}
#[test]
fn test_route_table_method_mismatch() {
let table = RouteTable {
entries: vec![RouteEntry {
channel_name: "orders.get".to_string(),
methods: vec!["GET".to_string()],
segments: parse_route_pattern("/orders/{id}"),
priority: 0,
}],
};
assert!(table.match_route("POST", "orders/42").is_none());
}
#[test]
fn test_route_table_priority_ordering() {
let table = RouteTable {
entries: vec![
RouteEntry {
channel_name: "low".to_string(),
methods: vec![],
segments: parse_route_pattern("/items/{id}"),
priority: 0,
},
RouteEntry {
channel_name: "high".to_string(),
methods: vec![],
segments: parse_route_pattern("/items/{id}"),
priority: 10,
},
],
};
assert_eq!(
table
.match_route("GET", "items/1")
.expect("test")
.channel_name,
"low"
);
}
}