#![allow(dead_code)]
use aho_corasick::{AhoCorasick, AhoCorasickBuilder, MatchKind};
use matchit::Router;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use crate::predicate::{CompiledRequestPredicate, PathMatcher, RequestPredicate};
#[derive(Debug, Clone)]
pub struct IndexedRule {
pub id: usize,
pub name: String,
pub predicate: CompiledRequestPredicate,
pub priority: u32,
}
#[derive(Debug, Clone, PartialEq)]
enum PathCategory {
Any,
Exact(String),
Prefix(String),
Contains(String),
EndsWith(String),
Regex,
}
#[derive(Debug)]
pub struct RuleIndex {
rules: Vec<IndexedRule>,
exact_paths: HashMap<String, Vec<usize>>,
prefix_router: Router<Vec<usize>>,
prefix_routes_count: usize,
contains_automaton: Option<AhoCorasick>,
contains_pattern_rules: Vec<Vec<usize>>,
ends_with_patterns: Vec<(String, Vec<usize>)>,
regex_rules: Vec<usize>,
any_path_rules: Vec<usize>,
method_index: HashMap<String, HashSet<usize>>,
}
impl RuleIndex {
pub fn new() -> Self {
RuleIndex {
rules: Vec::new(),
exact_paths: HashMap::new(),
prefix_router: Router::new(),
prefix_routes_count: 0,
contains_automaton: None,
contains_pattern_rules: Vec::new(),
ends_with_patterns: Vec::new(),
regex_rules: Vec::new(),
any_path_rules: Vec::new(),
method_index: HashMap::new(),
}
}
pub fn build(
predicates: Vec<(String, RequestPredicate, u32)>,
) -> Result<Self, IndexBuildError> {
let mut index = Self::new();
let mut contains_patterns: Vec<String> = Vec::new();
let mut contains_pattern_to_rules: HashMap<String, Vec<usize>> = HashMap::new();
let mut prefix_routes: HashMap<String, Vec<usize>> = HashMap::new();
for (idx, (name, predicate, priority)) in predicates.into_iter().enumerate() {
let compiled = CompiledRequestPredicate::compile(&predicate)
.map_err(|e| IndexBuildError::PredicateCompile(name.clone(), e.to_string()))?;
let category = Self::categorize_path(&predicate.path);
match &category {
PathCategory::Any => {
index.any_path_rules.push(idx);
}
PathCategory::Exact(path) => {
index.exact_paths.entry(path.clone()).or_default().push(idx);
}
PathCategory::Prefix(prefix) => {
prefix_routes.entry(prefix.clone()).or_default().push(idx);
}
PathCategory::Contains(pattern) => {
if !contains_pattern_to_rules.contains_key(pattern) {
contains_patterns.push(pattern.clone());
}
contains_pattern_to_rules
.entry(pattern.clone())
.or_default()
.push(idx);
}
PathCategory::EndsWith(suffix) => {
if let Some(entry) = index
.ends_with_patterns
.iter_mut()
.find(|(s, _)| s == suffix)
{
entry.1.push(idx);
} else {
index.ends_with_patterns.push((suffix.clone(), vec![idx]));
}
}
PathCategory::Regex => {
index.regex_rules.push(idx);
}
}
if let Some(crate::predicate::StringMatcher::Equals(method)) = &predicate.method {
index
.method_index
.entry(method.to_uppercase())
.or_default()
.insert(idx);
}
index.rules.push(IndexedRule {
id: idx,
name,
predicate: compiled,
priority,
});
}
if !contains_patterns.is_empty() {
let ac = AhoCorasickBuilder::new()
.match_kind(MatchKind::LeftmostFirst)
.build(&contains_patterns)
.map_err(|e| IndexBuildError::AhoCorasick(e.to_string()))?;
index.contains_automaton = Some(ac);
for pattern in &contains_patterns {
index.contains_pattern_rules.push(
contains_pattern_to_rules
.remove(pattern)
.unwrap_or_default(),
);
}
}
for (prefix, rule_ids) in prefix_routes {
let route = if prefix.ends_with('/') {
format!("{prefix}{{*rest}}")
} else {
format!("{prefix}/{{*rest}}")
};
index
.prefix_router
.insert(&prefix, rule_ids.clone())
.map_err(|e| IndexBuildError::RadixTrie(e.to_string()))?;
index.prefix_routes_count += 1;
if index.prefix_router.insert(&route, rule_ids).is_err() {
}
}
Ok(index)
}
fn categorize_path(path: &Option<PathMatcher>) -> PathCategory {
match path {
None => PathCategory::Any,
Some(PathMatcher::Any) => PathCategory::Any,
Some(PathMatcher::Exact { exact }) => PathCategory::Exact(exact.clone()),
Some(PathMatcher::Prefix { prefix }) => PathCategory::Prefix(prefix.clone()),
Some(PathMatcher::Contains { contains }) => PathCategory::Contains(contains.clone()),
Some(PathMatcher::EndsWith { ends_with }) => PathCategory::EndsWith(ends_with.clone()),
Some(PathMatcher::Regex { .. }) => PathCategory::Regex,
Some(PathMatcher::Full { matcher, .. }) => {
use crate::predicate::StringMatcher;
match matcher {
StringMatcher::Equals(v) => PathCategory::Exact(v.clone()),
StringMatcher::StartsWith(v) => PathCategory::Prefix(v.clone()),
StringMatcher::Contains(v) => PathCategory::Contains(v.clone()),
StringMatcher::EndsWith(v) => PathCategory::EndsWith(v.clone()),
StringMatcher::Matches(_) => PathCategory::Regex,
StringMatcher::Exists(_) => PathCategory::Any,
}
}
}
}
pub fn find_candidates(&self, path: &str, method: Option<&str>) -> Vec<usize> {
let mut candidates = HashSet::new();
for &id in &self.any_path_rules {
candidates.insert(id);
}
if let Some(ids) = self.exact_paths.get(path) {
for &id in ids {
candidates.insert(id);
}
}
if let Ok(matched) = self.prefix_router.at(path) {
for &id in matched.value {
candidates.insert(id);
}
}
if let Some(ref ac) = self.contains_automaton {
for mat in ac.find_iter(path) {
let pattern_idx = mat.pattern().as_usize();
if let Some(ids) = self.contains_pattern_rules.get(pattern_idx) {
for &id in ids {
candidates.insert(id);
}
}
}
}
for (suffix, ids) in &self.ends_with_patterns {
if path.ends_with(suffix) {
for &id in ids {
candidates.insert(id);
}
}
}
for &id in &self.regex_rules {
candidates.insert(id);
}
let mut result: Vec<usize> = if let Some(m) = method {
let method_upper = m.to_uppercase();
if let Some(method_rules) = self.method_index.get(&method_upper) {
candidates
.into_iter()
.filter(|id| method_rules.contains(id) || !self.has_method_constraint(*id))
.collect()
} else {
candidates
.into_iter()
.filter(|id| !self.has_method_constraint(*id))
.collect()
}
} else {
candidates.into_iter().collect()
};
result.sort_by_key(|&id| self.rules.get(id).map(|r| r.priority).unwrap_or(u32::MAX));
result
}
fn has_method_constraint(&self, rule_id: usize) -> bool {
self.method_index
.values()
.any(|rules| rules.contains(&rule_id))
}
pub fn get_rule(&self, id: usize) -> Option<&IndexedRule> {
self.rules.get(id)
}
pub fn rules(&self) -> &[IndexedRule] {
&self.rules
}
pub fn len(&self) -> usize {
self.rules.len()
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
pub fn stats(&self) -> IndexStats {
IndexStats {
total_rules: self.rules.len(),
exact_paths: self.exact_paths.len(),
prefix_routes: self.prefix_routes_count,
contains_patterns: self.contains_pattern_rules.len(),
ends_with_patterns: self.ends_with_patterns.len(),
regex_rules: self.regex_rules.len(),
any_path_rules: self.any_path_rules.len(),
}
}
}
impl Default for RuleIndex {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct IndexStats {
pub total_rules: usize,
pub exact_paths: usize,
pub prefix_routes: usize,
pub contains_patterns: usize,
pub ends_with_patterns: usize,
pub regex_rules: usize,
pub any_path_rules: usize,
}
#[derive(Debug, Clone)]
pub enum IndexBuildError {
PredicateCompile(String, String),
AhoCorasick(String),
RadixTrie(String),
}
impl std::fmt::Display for IndexBuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IndexBuildError::PredicateCompile(name, err) => {
write!(f, "Failed to compile predicate '{name}': {err}")
}
IndexBuildError::AhoCorasick(err) => {
write!(f, "Failed to build Aho-Corasick automaton: {err}")
}
IndexBuildError::RadixTrie(err) => {
write!(f, "Failed to build radix trie: {err}")
}
}
}
}
impl std::error::Error for IndexBuildError {}
#[derive(Debug, Clone)]
pub struct SharedRuleIndex {
inner: Arc<RuleIndex>,
}
impl SharedRuleIndex {
pub fn new(index: RuleIndex) -> Self {
SharedRuleIndex {
inner: Arc::new(index),
}
}
pub fn find_candidates(&self, path: &str, method: Option<&str>) -> Vec<usize> {
self.inner.find_candidates(path, method)
}
pub fn get_rule(&self, id: usize) -> Option<&IndexedRule> {
self.inner.get_rule(id)
}
pub fn stats(&self) -> IndexStats {
self.inner.stats()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::predicate::{PredicateOptions, StringMatcher};
fn make_predicate(path: Option<PathMatcher>, method: Option<&str>) -> RequestPredicate {
RequestPredicate {
method: method.map(|m| StringMatcher::Equals(m.to_string())),
path,
headers: vec![],
query: vec![],
body: None,
options: PredicateOptions::default(),
}
}
#[test]
fn test_exact_path_indexing() {
let predicates = vec![
(
"rule1".to_string(),
make_predicate(
Some(PathMatcher::Exact {
exact: "/api/users".to_string(),
}),
Some("GET"),
),
0,
),
(
"rule2".to_string(),
make_predicate(
Some(PathMatcher::Exact {
exact: "/api/items".to_string(),
}),
Some("GET"),
),
0,
),
];
let index = RuleIndex::build(predicates).unwrap();
let candidates = index.find_candidates("/api/users", Some("GET"));
assert!(candidates.contains(&0));
assert!(!candidates.contains(&1));
let candidates = index.find_candidates("/api/other", Some("GET"));
assert!(!candidates.contains(&0));
assert!(!candidates.contains(&1));
}
#[test]
fn test_prefix_path_indexing() {
let predicates = vec![
(
"api_rule".to_string(),
make_predicate(
Some(PathMatcher::Prefix {
prefix: "/api".to_string(),
}),
None,
),
0,
),
(
"admin_rule".to_string(),
make_predicate(
Some(PathMatcher::Prefix {
prefix: "/admin".to_string(),
}),
None,
),
0,
),
];
let index = RuleIndex::build(predicates).unwrap();
let candidates = index.find_candidates("/api/users", None);
assert!(candidates.contains(&0));
assert!(!candidates.contains(&1));
let candidates = index.find_candidates("/admin/dashboard", None);
assert!(!candidates.contains(&0));
assert!(candidates.contains(&1));
}
#[test]
fn test_contains_path_indexing() {
let predicates = vec![
(
"users_rule".to_string(),
make_predicate(
Some(PathMatcher::Contains {
contains: "users".to_string(),
}),
None,
),
0,
),
(
"items_rule".to_string(),
make_predicate(
Some(PathMatcher::Contains {
contains: "items".to_string(),
}),
None,
),
0,
),
];
let index = RuleIndex::build(predicates).unwrap();
let candidates = index.find_candidates("/api/users/123", None);
assert!(candidates.contains(&0));
assert!(!candidates.contains(&1));
let candidates = index.find_candidates("/v2/items/list", None);
assert!(!candidates.contains(&0));
assert!(candidates.contains(&1));
}
#[test]
fn test_ends_with_indexing() {
let predicates = vec![
(
"json_rule".to_string(),
make_predicate(
Some(PathMatcher::EndsWith {
ends_with: ".json".to_string(),
}),
None,
),
0,
),
(
"xml_rule".to_string(),
make_predicate(
Some(PathMatcher::EndsWith {
ends_with: ".xml".to_string(),
}),
None,
),
0,
),
];
let index = RuleIndex::build(predicates).unwrap();
let candidates = index.find_candidates("/data/export.json", None);
assert!(candidates.contains(&0));
assert!(!candidates.contains(&1));
let candidates = index.find_candidates("/data/export.xml", None);
assert!(!candidates.contains(&0));
assert!(candidates.contains(&1));
}
#[test]
fn test_any_path_always_included() {
let predicates = vec![
(
"catch_all".to_string(),
make_predicate(None, None),
10, ),
(
"specific".to_string(),
make_predicate(
Some(PathMatcher::Exact {
exact: "/api".to_string(),
}),
None,
),
0, ),
];
let index = RuleIndex::build(predicates).unwrap();
let candidates = index.find_candidates("/random/path", None);
assert!(candidates.contains(&0));
let candidates = index.find_candidates("/api", None);
assert!(candidates.contains(&0));
assert!(candidates.contains(&1));
assert_eq!(candidates[0], 1); }
#[test]
fn test_method_filtering() {
let predicates = vec![
(
"get_users".to_string(),
make_predicate(
Some(PathMatcher::Exact {
exact: "/users".to_string(),
}),
Some("GET"),
),
0,
),
(
"post_users".to_string(),
make_predicate(
Some(PathMatcher::Exact {
exact: "/users".to_string(),
}),
Some("POST"),
),
0,
),
(
"any_method".to_string(),
make_predicate(
Some(PathMatcher::Exact {
exact: "/users".to_string(),
}),
None,
),
0,
),
];
let index = RuleIndex::build(predicates).unwrap();
let candidates = index.find_candidates("/users", Some("GET"));
assert!(candidates.contains(&0)); assert!(!candidates.contains(&1)); assert!(candidates.contains(&2));
let candidates = index.find_candidates("/users", Some("POST"));
assert!(!candidates.contains(&0)); assert!(candidates.contains(&1)); assert!(candidates.contains(&2)); }
#[test]
fn test_index_stats() {
let predicates = vec![
("r1".to_string(), make_predicate(None, None), 0),
(
"r2".to_string(),
make_predicate(
Some(PathMatcher::Exact {
exact: "/a".to_string(),
}),
None,
),
0,
),
(
"r3".to_string(),
make_predicate(
Some(PathMatcher::Prefix {
prefix: "/b".to_string(),
}),
None,
),
0,
),
(
"r4".to_string(),
make_predicate(
Some(PathMatcher::Contains {
contains: "c".to_string(),
}),
None,
),
0,
),
(
"r5".to_string(),
make_predicate(
Some(PathMatcher::EndsWith {
ends_with: ".d".to_string(),
}),
None,
),
0,
),
(
"r6".to_string(),
make_predicate(
Some(PathMatcher::Regex {
regex: ".*".to_string(),
}),
None,
),
0,
),
];
let index = RuleIndex::build(predicates).unwrap();
let stats = index.stats();
assert_eq!(stats.total_rules, 6);
assert_eq!(stats.any_path_rules, 1);
assert_eq!(stats.exact_paths, 1);
assert_eq!(stats.prefix_routes, 1);
assert_eq!(stats.contains_patterns, 1);
assert_eq!(stats.ends_with_patterns, 1);
assert_eq!(stats.regex_rules, 1);
}
#[test]
fn test_priority_ordering() {
let predicates = vec![
("low_priority".to_string(), make_predicate(None, None), 100),
("high_priority".to_string(), make_predicate(None, None), 1),
(
"medium_priority".to_string(),
make_predicate(None, None),
50,
),
];
let index = RuleIndex::build(predicates).unwrap();
let candidates = index.find_candidates("/any", None);
assert_eq!(candidates, vec![1, 2, 0]); }
}