use crate::db::Store;
use crate::error::{Error, Result};
use crate::types::{Target, ValueType};
pub const META_LOCAL_PREFIX: &str = "meta:local:";
pub const MAIN_DEST: &str = "main";
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FilterAction {
Exclude,
Route(Vec<String>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FilterRule {
pub action: FilterAction,
pub(crate) pattern: Vec<PatternSegment>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PatternSegment {
Literal(String),
Star,
GlobStar,
}
pub fn parse_filter_rules(db: &Store) -> Result<Vec<FilterRule>> {
let mut rules = Vec::new();
if let Some(entry) = db.get(&Target::project(), "meta:local:filter")? {
if entry.value_type == ValueType::Set {
let members: Vec<String> = serde_json::from_str(&entry.value)?;
for member in members {
rules.push(parse_rule(&member)?);
}
}
}
if let Some(entry) = db.get(&Target::project(), "meta:filter")? {
if entry.value_type == ValueType::Set {
let members: Vec<String> = serde_json::from_str(&entry.value)?;
for member in members {
rules.push(parse_rule(&member)?);
}
}
}
Ok(rules)
}
fn parse_rule(s: &str) -> Result<FilterRule> {
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() < 2 {
return Err(Error::InvalidFilterRule(format!(
"invalid filter rule (need at least action and pattern): '{s}'"
)));
}
let action = match parts[0] {
"exclude" => FilterAction::Exclude,
"route" => {
if parts.len() < 3 {
return Err(Error::InvalidFilterRule(format!(
"route rule requires a destination: '{s}'"
)));
}
let destinations: Vec<String> = parts[2]
.split(',')
.map(|d| d.trim().to_string())
.filter(|d| !d.is_empty())
.collect();
FilterAction::Route(destinations)
}
other => {
return Err(Error::InvalidFilterRule(format!(
"unknown filter action '{other}' in rule '{s}'"
)))
}
};
let pattern = parse_pattern(parts[1]);
Ok(FilterRule { action, pattern })
}
fn parse_pattern(s: &str) -> Vec<PatternSegment> {
s.split(':')
.map(|seg| match seg {
"**" => PatternSegment::GlobStar,
"*" => PatternSegment::Star,
_ => PatternSegment::Literal(seg.to_string()),
})
.collect()
}
fn pattern_matches(pattern: &[PatternSegment], key_segments: &[&str]) -> bool {
match (pattern.first(), key_segments.first()) {
(None, None) => true,
(None, Some(_)) => false,
(Some(PatternSegment::GlobStar), _) => {
if pattern.len() == 1 {
return true;
}
for skip in 0..=key_segments.len() {
if pattern_matches(&pattern[1..], &key_segments[skip..]) {
return true;
}
}
false
}
(Some(_), None) => false,
(Some(PatternSegment::Star), Some(_)) => pattern_matches(&pattern[1..], &key_segments[1..]),
(Some(PatternSegment::Literal(lit)), Some(seg)) => {
lit == seg && pattern_matches(&pattern[1..], &key_segments[1..])
}
}
}
pub fn classify_key(key: &str, rules: &[FilterRule]) -> Option<Vec<String>> {
if key.starts_with(META_LOCAL_PREFIX) {
return None;
}
let segments: Vec<&str> = key.split(':').collect();
let mut matched_routes: Vec<String> = Vec::new();
let mut excluded = false;
for rule in rules {
if pattern_matches(&rule.pattern, &segments) {
match &rule.action {
FilterAction::Exclude => {
excluded = true;
}
FilterAction::Route(dests) => {
for d in dests {
if !matched_routes.contains(d) {
matched_routes.push(d.clone());
}
}
}
}
}
}
if excluded {
return None;
}
if matched_routes.is_empty() {
Some(vec![MAIN_DEST.to_string()])
} else {
Some(matched_routes)
}
}