#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrioritizedRule {
pub id: String,
pub salience: i32,
pub group: String,
pub mutex_group: Option<String>,
}
impl PrioritizedRule {
pub fn new(
id: impl Into<String>,
salience: i32,
group: impl Into<String>,
mutex_group: Option<String>,
) -> Self {
Self {
id: id.into(),
salience,
group: group.into(),
mutex_group,
}
}
}
#[derive(Debug, Default, Clone)]
pub struct ConflictSet {
pub rules: Vec<PrioritizedRule>,
cursor: usize,
}
impl ConflictSet {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, rule: PrioritizedRule) {
self.rules.push(rule);
}
pub fn remove(&mut self, id: &str) -> bool {
let before = self.rules.len();
self.rules.retain(|r| r.id != id);
if self.cursor >= self.rules.len() && !self.rules.is_empty() {
self.cursor = 0;
}
self.rules.len() < before
}
pub fn len(&self) -> usize {
self.rules.len()
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SelectionStrategy {
HighestSalience,
MostRecent,
LeastRecent,
FirstMatch,
RoundRobin,
}
#[derive(Debug)]
pub struct RulePrioritizer {
strategy: SelectionStrategy,
rr_index: usize,
}
impl RulePrioritizer {
pub fn new(strategy: SelectionStrategy) -> Self {
Self {
strategy,
rr_index: 0,
}
}
pub fn select<'a>(&mut self, conflict_set: &'a ConflictSet) -> Option<&'a PrioritizedRule> {
if conflict_set.is_empty() {
return None;
}
match self.strategy {
SelectionStrategy::HighestSalience => conflict_set
.rules
.iter()
.max_by_key(|r| r.salience),
SelectionStrategy::MostRecent => conflict_set.rules.last(),
SelectionStrategy::LeastRecent | SelectionStrategy::FirstMatch => {
conflict_set.rules.first()
}
SelectionStrategy::RoundRobin => {
let idx = self.rr_index % conflict_set.rules.len();
self.rr_index = self.rr_index.wrapping_add(1);
conflict_set.rules.get(idx)
}
}
}
pub fn order<'a>(&self, conflict_set: &'a ConflictSet) -> Vec<&'a PrioritizedRule> {
let mut refs: Vec<&PrioritizedRule> = conflict_set.rules.iter().collect();
match self.strategy {
SelectionStrategy::HighestSalience => {
refs.sort_by(|a, b| b.salience.cmp(&a.salience));
}
SelectionStrategy::MostRecent => {
refs.reverse();
}
SelectionStrategy::LeastRecent
| SelectionStrategy::FirstMatch
| SelectionStrategy::RoundRobin => {
}
}
refs
}
pub fn filter_by_group<'a>(rules: &'a [PrioritizedRule], group: &str) -> Vec<&'a PrioritizedRule> {
rules.iter().filter(|r| r.group == group).collect()
}
pub fn resolve_mutex<'a>(rules: &'a [PrioritizedRule]) -> Vec<&'a PrioritizedRule> {
use std::collections::HashMap;
let mut seen_mutex: HashMap<&str, &PrioritizedRule> = HashMap::new();
let mut result: Vec<&PrioritizedRule> = Vec::new();
for rule in rules {
match &rule.mutex_group {
None => result.push(rule),
Some(mg) => {
let entry = seen_mutex.entry(mg.as_str()).or_insert(rule);
if rule.salience > entry.salience {
*entry = rule;
}
}
}
}
for winner in seen_mutex.values() {
result.push(winner);
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_rule(id: &str, salience: i32, group: &str) -> PrioritizedRule {
PrioritizedRule::new(id, salience, group, None)
}
fn make_mutex_rule(id: &str, salience: i32, group: &str, mg: &str) -> PrioritizedRule {
PrioritizedRule::new(id, salience, group, Some(mg.to_string()))
}
#[test]
fn test_conflict_set_empty() {
let cs = ConflictSet::new();
assert!(cs.is_empty());
assert_eq!(cs.len(), 0);
}
#[test]
fn test_conflict_set_add() {
let mut cs = ConflictSet::new();
cs.add(make_rule("r1", 10, "g1"));
assert_eq!(cs.len(), 1);
assert!(!cs.is_empty());
}
#[test]
fn test_conflict_set_add_multiple() {
let mut cs = ConflictSet::new();
cs.add(make_rule("r1", 10, "g1"));
cs.add(make_rule("r2", 20, "g1"));
cs.add(make_rule("r3", 5, "g2"));
assert_eq!(cs.len(), 3);
}
#[test]
fn test_conflict_set_remove_existing() {
let mut cs = ConflictSet::new();
cs.add(make_rule("r1", 10, "g1"));
cs.add(make_rule("r2", 20, "g1"));
let removed = cs.remove("r1");
assert!(removed);
assert_eq!(cs.len(), 1);
}
#[test]
fn test_conflict_set_remove_nonexistent() {
let mut cs = ConflictSet::new();
cs.add(make_rule("r1", 10, "g1"));
let removed = cs.remove("no_such");
assert!(!removed);
assert_eq!(cs.len(), 1);
}
#[test]
fn test_conflict_set_remove_all() {
let mut cs = ConflictSet::new();
cs.add(make_rule("r1", 10, "g1"));
cs.remove("r1");
assert!(cs.is_empty());
}
#[test]
fn test_highest_salience_basic() {
let mut cs = ConflictSet::new();
cs.add(make_rule("r1", 5, "g1"));
cs.add(make_rule("r2", 15, "g1"));
cs.add(make_rule("r3", 10, "g1"));
let mut p = RulePrioritizer::new(SelectionStrategy::HighestSalience);
let selected = p.select(&cs).expect("should select");
assert_eq!(selected.id, "r2");
}
#[test]
fn test_highest_salience_single() {
let mut cs = ConflictSet::new();
cs.add(make_rule("r1", 99, "g1"));
let mut p = RulePrioritizer::new(SelectionStrategy::HighestSalience);
let selected = p.select(&cs).expect("should select");
assert_eq!(selected.id, "r1");
}
#[test]
fn test_highest_salience_empty() {
let cs = ConflictSet::new();
let mut p = RulePrioritizer::new(SelectionStrategy::HighestSalience);
assert!(p.select(&cs).is_none());
}
#[test]
fn test_highest_salience_order() {
let mut cs = ConflictSet::new();
cs.add(make_rule("r1", 5, "g1"));
cs.add(make_rule("r2", 30, "g1"));
cs.add(make_rule("r3", 20, "g1"));
let p = RulePrioritizer::new(SelectionStrategy::HighestSalience);
let ordered = p.order(&cs);
assert_eq!(ordered[0].id, "r2");
assert_eq!(ordered[1].id, "r3");
assert_eq!(ordered[2].id, "r1");
}
#[test]
fn test_most_recent_basic() {
let mut cs = ConflictSet::new();
cs.add(make_rule("r1", 10, "g1"));
cs.add(make_rule("r2", 10, "g1"));
let mut p = RulePrioritizer::new(SelectionStrategy::MostRecent);
let selected = p.select(&cs).expect("should select");
assert_eq!(selected.id, "r2");
}
#[test]
fn test_most_recent_order() {
let mut cs = ConflictSet::new();
cs.add(make_rule("r1", 10, "g1"));
cs.add(make_rule("r2", 10, "g1"));
cs.add(make_rule("r3", 10, "g1"));
let p = RulePrioritizer::new(SelectionStrategy::MostRecent);
let ordered = p.order(&cs);
assert_eq!(ordered[0].id, "r3");
assert_eq!(ordered[1].id, "r2");
assert_eq!(ordered[2].id, "r1");
}
#[test]
fn test_least_recent_basic() {
let mut cs = ConflictSet::new();
cs.add(make_rule("r1", 10, "g1"));
cs.add(make_rule("r2", 10, "g1"));
let mut p = RulePrioritizer::new(SelectionStrategy::LeastRecent);
let selected = p.select(&cs).expect("should select");
assert_eq!(selected.id, "r1");
}
#[test]
fn test_least_recent_order() {
let mut cs = ConflictSet::new();
cs.add(make_rule("r3", 10, "g1"));
cs.add(make_rule("r1", 10, "g1"));
let p = RulePrioritizer::new(SelectionStrategy::LeastRecent);
let ordered = p.order(&cs);
assert_eq!(ordered[0].id, "r3");
assert_eq!(ordered[1].id, "r1");
}
#[test]
fn test_first_match_basic() {
let mut cs = ConflictSet::new();
cs.add(make_rule("r1", 1, "g1"));
cs.add(make_rule("r2", 100, "g1"));
let mut p = RulePrioritizer::new(SelectionStrategy::FirstMatch);
let selected = p.select(&cs).expect("should select");
assert_eq!(selected.id, "r1");
}
#[test]
fn test_round_robin_cycles() {
let mut cs = ConflictSet::new();
cs.add(make_rule("r1", 0, "g1"));
cs.add(make_rule("r2", 0, "g1"));
cs.add(make_rule("r3", 0, "g1"));
let mut p = RulePrioritizer::new(SelectionStrategy::RoundRobin);
let s0 = p.select(&cs).expect("s0").id.clone();
let s1 = p.select(&cs).expect("s1").id.clone();
let s2 = p.select(&cs).expect("s2").id.clone();
let s3 = p.select(&cs).expect("s3").id.clone();
assert_eq!(s0, "r1");
assert_eq!(s1, "r2");
assert_eq!(s2, "r3");
assert_eq!(s3, "r1");
}
#[test]
fn test_filter_by_group_basic() {
let rules = vec![
make_rule("r1", 10, "A"),
make_rule("r2", 20, "B"),
make_rule("r3", 30, "A"),
];
let filtered = RulePrioritizer::filter_by_group(&rules, "A");
assert_eq!(filtered.len(), 2);
assert!(filtered.iter().all(|r| r.group == "A"));
}
#[test]
fn test_filter_by_group_empty() {
let rules = vec![make_rule("r1", 10, "A")];
let filtered = RulePrioritizer::filter_by_group(&rules, "B");
assert!(filtered.is_empty());
}
#[test]
fn test_filter_by_group_all_match() {
let rules = vec![
make_rule("r1", 10, "X"),
make_rule("r2", 20, "X"),
];
let filtered = RulePrioritizer::filter_by_group(&rules, "X");
assert_eq!(filtered.len(), 2);
}
#[test]
fn test_resolve_mutex_keeps_highest_salience() {
let rules = vec![
make_mutex_rule("r1", 10, "g1", "mx"),
make_mutex_rule("r2", 20, "g1", "mx"),
];
let resolved = RulePrioritizer::resolve_mutex(&rules);
let mx_rules: Vec<_> = resolved.iter().filter(|r| r.mutex_group.as_deref() == Some("mx")).collect();
assert_eq!(mx_rules.len(), 1);
assert_eq!(mx_rules[0].id, "r2");
}
#[test]
fn test_resolve_mutex_no_mutex_rules_kept() {
let rules = vec![
make_rule("r1", 10, "g1"),
make_rule("r2", 20, "g1"),
];
let resolved = RulePrioritizer::resolve_mutex(&rules);
assert_eq!(resolved.len(), 2);
}
#[test]
fn test_resolve_mutex_mixed() {
let rules = vec![
make_rule("r1", 10, "g1"),
make_mutex_rule("r2", 5, "g1", "mxA"),
make_mutex_rule("r3", 15, "g1", "mxA"),
make_mutex_rule("r4", 8, "g1", "mxB"),
];
let resolved = RulePrioritizer::resolve_mutex(&rules);
assert!(resolved.iter().any(|r| r.id == "r1"));
assert!(resolved.iter().any(|r| r.id == "r3"));
assert!(resolved.iter().any(|r| r.id == "r4"));
assert!(!resolved.iter().any(|r| r.id == "r2"));
}
#[test]
fn test_resolve_mutex_different_groups() {
let rules = vec![
make_mutex_rule("r1", 10, "g1", "mxA"),
make_mutex_rule("r2", 20, "g1", "mxB"),
];
let resolved = RulePrioritizer::resolve_mutex(&rules);
assert_eq!(resolved.len(), 2);
}
#[test]
fn test_prioritized_rule_new() {
let r = PrioritizedRule::new("x", 42, "g", Some("mg".to_string()));
assert_eq!(r.id, "x");
assert_eq!(r.salience, 42);
assert_eq!(r.group, "g");
assert_eq!(r.mutex_group, Some("mg".to_string()));
}
#[test]
fn test_prioritized_rule_equality() {
let r1 = make_rule("a", 1, "g");
let r2 = make_rule("a", 1, "g");
assert_eq!(r1, r2);
}
#[test]
fn test_select_empty_returns_none() {
let cs = ConflictSet::new();
let mut p = RulePrioritizer::new(SelectionStrategy::MostRecent);
assert!(p.select(&cs).is_none());
}
#[test]
fn test_order_single_element() {
let mut cs = ConflictSet::new();
cs.add(make_rule("only", 5, "g"));
let p = RulePrioritizer::new(SelectionStrategy::HighestSalience);
let ordered = p.order(&cs);
assert_eq!(ordered.len(), 1);
assert_eq!(ordered[0].id, "only");
}
#[test]
fn test_negative_salience() {
let mut cs = ConflictSet::new();
cs.add(make_rule("low", -100, "g"));
cs.add(make_rule("high", 100, "g"));
let mut p = RulePrioritizer::new(SelectionStrategy::HighestSalience);
let sel = p.select(&cs).expect("should select");
assert_eq!(sel.id, "high");
}
#[test]
fn test_remove_and_reselect() {
let mut cs = ConflictSet::new();
cs.add(make_rule("r1", 5, "g"));
cs.add(make_rule("r2", 99, "g"));
cs.remove("r2");
let mut p = RulePrioritizer::new(SelectionStrategy::HighestSalience);
let sel = p.select(&cs).expect("should select");
assert_eq!(sel.id, "r1");
}
}