Skip to main content

oxihuman_core/
route_table.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Simple string-pattern route table with parameter extraction.
6
7/// A route entry mapping a pattern to a handler name.
8#[derive(Debug, Clone)]
9pub struct RouteEntry {
10    pub pattern: String,
11    pub handler: String,
12    pub priority: i32,
13}
14
15/// Result of a route match.
16#[derive(Debug, Clone)]
17pub struct RouteMatch {
18    pub handler: String,
19    pub params: Vec<(String, String)>,
20}
21
22/// Route table mapping URL-like paths to handlers.
23pub struct RouteTable {
24    routes: Vec<RouteEntry>,
25    dispatch_count: u64,
26}
27
28fn match_pattern(pattern: &str, path: &str) -> Option<Vec<(String, String)>> {
29    let pat_parts: Vec<&str> = pattern.split('/').collect();
30    let path_parts: Vec<&str> = path.split('/').collect();
31    if pat_parts.len() != path_parts.len() {
32        return None;
33    }
34    let mut params = Vec::new();
35    for (pp, sp) in pat_parts.iter().zip(path_parts.iter()) {
36        if let Some(name) = pp.strip_prefix(':') {
37            params.push((name.to_string(), (*sp).to_string()));
38        } else if pp != sp {
39            return None;
40        }
41    }
42    Some(params)
43}
44
45#[allow(dead_code)]
46impl RouteTable {
47    pub fn new() -> Self {
48        RouteTable {
49            routes: Vec::new(),
50            dispatch_count: 0,
51        }
52    }
53
54    pub fn add_route(&mut self, pattern: &str, handler: &str, priority: i32) {
55        let pos = self.routes.partition_point(|r| r.priority > priority);
56        self.routes.insert(
57            pos,
58            RouteEntry {
59                pattern: pattern.to_string(),
60                handler: handler.to_string(),
61                priority,
62            },
63        );
64    }
65
66    pub fn dispatch(&mut self, path: &str) -> Option<RouteMatch> {
67        self.dispatch_count += 1;
68        for route in &self.routes {
69            if let Some(params) = match_pattern(&route.pattern, path) {
70                return Some(RouteMatch {
71                    handler: route.handler.clone(),
72                    params,
73                });
74            }
75        }
76        None
77    }
78
79    pub fn remove_handler(&mut self, handler: &str) -> usize {
80        let before = self.routes.len();
81        self.routes.retain(|r| r.handler != handler);
82        before - self.routes.len()
83    }
84
85    pub fn route_count(&self) -> usize {
86        self.routes.len()
87    }
88
89    pub fn dispatch_count(&self) -> u64 {
90        self.dispatch_count
91    }
92
93    pub fn has_pattern(&self, pattern: &str) -> bool {
94        self.routes.iter().any(|r| r.pattern == pattern)
95    }
96
97    pub fn handlers(&self) -> Vec<&str> {
98        self.routes.iter().map(|r| r.handler.as_str()).collect()
99    }
100
101    pub fn clear(&mut self) {
102        self.routes.clear();
103    }
104
105    pub fn is_empty(&self) -> bool {
106        self.routes.is_empty()
107    }
108}
109
110impl Default for RouteTable {
111    fn default() -> Self {
112        Self::new()
113    }
114}
115
116pub fn new_route_table() -> RouteTable {
117    RouteTable::new()
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn exact_match() {
126        let mut t = new_route_table();
127        t.add_route("/health", "health_handler", 0);
128        let m = t.dispatch("/health").expect("should succeed");
129        assert_eq!(m.handler, "health_handler");
130        assert!(m.params.is_empty());
131    }
132
133    #[test]
134    fn param_extraction() {
135        let mut t = new_route_table();
136        t.add_route("/user/:id", "user_handler", 0);
137        let m = t.dispatch("/user/42").expect("should succeed");
138        assert_eq!(m.handler, "user_handler");
139        assert_eq!(m.params[0], ("id".to_string(), "42".to_string()));
140    }
141
142    #[test]
143    fn no_match() {
144        let mut t = new_route_table();
145        t.add_route("/a", "h", 0);
146        assert!(t.dispatch("/b").is_none());
147    }
148
149    #[test]
150    fn priority_ordering() {
151        let mut t = new_route_table();
152        t.add_route("/item/:id", "generic", 0);
153        t.add_route("/item/special", "specific", 10);
154        let m = t.dispatch("/item/special").expect("should succeed");
155        assert_eq!(m.handler, "specific");
156    }
157
158    #[test]
159    fn remove_handler() {
160        let mut t = new_route_table();
161        t.add_route("/a", "h", 0);
162        t.add_route("/b", "h", 0);
163        assert_eq!(t.remove_handler("h"), 2);
164        assert!(t.is_empty());
165    }
166
167    #[test]
168    fn dispatch_count_tracked() {
169        let mut t = new_route_table();
170        t.add_route("/x", "h", 0);
171        t.dispatch("/x");
172        t.dispatch("/y");
173        assert_eq!(t.dispatch_count(), 2);
174    }
175
176    #[test]
177    fn has_pattern() {
178        let mut t = new_route_table();
179        t.add_route("/foo", "h", 0);
180        assert!(t.has_pattern("/foo"));
181        assert!(!t.has_pattern("/bar"));
182    }
183
184    #[test]
185    fn multiple_params() {
186        let mut t = new_route_table();
187        t.add_route("/a/:x/b/:y", "h", 0);
188        let m = t.dispatch("/a/1/b/2").expect("should succeed");
189        assert_eq!(m.params.len(), 2);
190    }
191
192    #[test]
193    fn clear_table() {
194        let mut t = new_route_table();
195        t.add_route("/x", "h", 0);
196        t.clear();
197        assert_eq!(t.route_count(), 0);
198    }
199}