#![allow(missing_docs)]
use std::sync::Arc;
pub struct Chain<I: ?Sized, O> {
rules: Vec<Arc<dyn Fn(&I) -> Option<O> + Send + Sync>>,
fallback: Arc<dyn Fn(&I) -> O + Send + Sync>,
}
impl<I: ?Sized, O> Chain<I, O> {
pub fn new<F>(fallback: F) -> Self
where
F: Fn(&I) -> O + Send + Sync + 'static,
{
Self {
rules: Vec::new(),
fallback: Arc::new(fallback),
}
}
#[must_use]
pub fn with_rule<R>(mut self, rule: R) -> Self
where
R: Fn(&I) -> Option<O> + Send + Sync + 'static,
{
self.rules.push(Arc::new(rule));
self
}
pub fn rule_count(&self) -> usize {
self.rules.len()
}
pub fn evaluate(&self, input: &I) -> O {
for rule in &self.rules {
if let Some(o) = rule(input) {
return o;
}
}
(self.fallback)(input)
}
}
impl<I: ?Sized, O> std::fmt::Debug for Chain<I, O> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Chain")
.field("rules", &self.rules.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::assert_deterministic_over;
#[derive(Debug, PartialEq, Eq, Clone)]
enum Color {
Red,
Green,
Blue,
Unknown,
}
#[test]
fn empty_chain_returns_fallback() {
let c = Chain::<str, Color>::new(|_| Color::Unknown);
assert_eq!(c.evaluate("anything"), Color::Unknown);
assert_eq!(c.rule_count(), 0);
}
#[test]
fn first_matching_rule_wins() {
let c = Chain::<str, Color>::new(|_| Color::Unknown)
.with_rule(|s| s.contains("red").then_some(Color::Red))
.with_rule(|s| s.contains("green").then_some(Color::Green))
.with_rule(|s| s.contains("blue").then_some(Color::Blue));
assert_eq!(c.evaluate("red and green"), Color::Red);
assert_eq!(c.evaluate("blue sky"), Color::Blue);
assert_eq!(c.evaluate("nothing"), Color::Unknown);
assert_eq!(c.rule_count(), 3);
}
#[test]
fn fallback_fires_on_no_match() {
let c = Chain::<str, Color>::new(|_| Color::Unknown)
.with_rule(|s| s.contains("red").then_some(Color::Red));
assert_eq!(c.evaluate("none of that"), Color::Unknown);
}
#[test]
fn determinism_law() {
let c = Chain::<str, Color>::new(|_| Color::Unknown)
.with_rule(|s| s.contains("r").then_some(Color::Red))
.with_rule(|s| s.contains("g").then_some(Color::Green));
assert_deterministic_over(&["red", "green", "ranger", "neither", ""], |&input| {
c.evaluate(input)
});
}
#[test]
fn debug_impl_shows_rule_count() {
let c = Chain::<str, Color>::new(|_| Color::Unknown)
.with_rule(|s| s.contains("r").then_some(Color::Red));
let debug_str = format!("{c:?}");
assert!(debug_str.contains("rules"));
assert!(debug_str.contains('1'));
}
#[test]
fn composes_with_classifier_trait() {
use crate::Classifier;
let c = Chain::<str, Color>::new(|_| Color::Unknown)
.with_rule(|s: &str| s.contains("red").then_some(Color::Red));
assert_eq!(c.classify("red"), Color::Red);
assert_eq!(c.classify("none"), Color::Unknown);
}
}