use std::marker::PhantomData;
use std::sync::Arc;
pub trait Classifier<I: ?Sized, O>: Send + Sync {
fn classify(&self, input: &I) -> O;
}
pub struct FnClassifier<I: ?Sized, O, F>
where
F: Fn(&I) -> O + Send + Sync,
{
f: F,
_phantom: PhantomData<fn(&I) -> O>,
}
impl<I: ?Sized, O, F> FnClassifier<I, O, F>
where
F: Fn(&I) -> O + Send + Sync,
{
pub fn new(f: F) -> Self {
Self {
f,
_phantom: PhantomData,
}
}
}
impl<I: ?Sized, O, F> Classifier<I, O> for FnClassifier<I, O, F>
where
F: Fn(&I) -> O + Send + Sync,
{
fn classify(&self, input: &I) -> O {
(self.f)(input)
}
}
pub type ChainedClassifier<I, O> = crate::chain::Chain<I, O>;
impl<I: ?Sized, O> Classifier<I, O> for crate::chain::Chain<I, O> {
fn classify(&self, input: &I) -> O {
self.evaluate(input)
}
}
#[derive(Debug, Default, Copy, Clone)]
pub struct FailureClassifier;
impl Classifier<str, crate::failure::FailureKind> for FailureClassifier {
fn classify(&self, input: &str) -> crate::failure::FailureKind {
crate::failure::classify(input)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::failure::FailureKind;
#[derive(Debug, Clone, PartialEq, Eq)]
enum Color {
Red,
Green,
Blue,
Unknown,
}
#[test]
fn fn_classifier_wraps_free_function() {
let c = FnClassifier::new(|s: &str| {
if s == "red" { Color::Red } else { Color::Unknown }
});
assert_eq!(c.classify("red"), Color::Red);
assert_eq!(c.classify("orange"), Color::Unknown);
}
#[test]
fn chained_classifier_first_match_wins() {
let c = ChainedClassifier::<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.classify("the red car"), Color::Red);
assert_eq!(c.classify("green tree"), Color::Green);
assert_eq!(c.classify("blue sky"), Color::Blue);
}
#[test]
fn chained_classifier_fallback_fires_on_no_match() {
let c = ChainedClassifier::<str, Color>::new(|_| Color::Unknown)
.with_rule(|s| s.contains("red").then_some(Color::Red));
assert_eq!(c.classify("totally unrelated"), Color::Unknown);
assert_eq!(c.classify(""), Color::Unknown);
}
#[test]
fn chained_classifier_rule_order_matters() {
let c = ChainedClassifier::<str, Color>::new(|_| Color::Unknown)
.with_rule(|s| s.contains("multi").then_some(Color::Red))
.with_rule(|s| s.contains("multi").then_some(Color::Green));
assert_eq!(c.classify("multi"), Color::Red, "first rule wins");
}
#[test]
fn chained_classifier_empty_chain_returns_fallback() {
let c = ChainedClassifier::<str, Color>::new(|_| Color::Blue);
assert_eq!(c.rule_count(), 0);
assert_eq!(c.classify("anything"), Color::Blue);
}
#[test]
fn classifier_dyn_polymorphism_works() {
let c1: Box<dyn Classifier<str, Color>> = Box::new(
ChainedClassifier::new(|_| Color::Unknown)
.with_rule(|s: &str| s.contains("r").then_some(Color::Red)),
);
let c2: Box<dyn Classifier<str, Color>> =
Box::new(FnClassifier::new(|_| Color::Green));
assert_eq!(c1.classify("red"), Color::Red);
assert_eq!(c2.classify("anything"), Color::Green);
}
#[test]
fn classifier_law_determinism() {
let c = ChainedClassifier::<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));
crate::testing::assert_deterministic_over(
&["red", "green", "ranger", "neither", ""],
|&input| c.classify(input),
);
}
#[test]
fn failure_classifier_wraps_free_function() {
let c = FailureClassifier;
assert_eq!(
c.classify("does not exist"),
FailureKind::Declarative,
"declarative pattern routes correctly"
);
assert_eq!(
c.classify("network timeout"),
FailureKind::Transient,
"non-declarative stderr defaults to Transient"
);
}
#[test]
fn failure_classifier_via_dyn() {
let c: &dyn Classifier<str, FailureKind> = &FailureClassifier;
assert_eq!(c.classify("infinite recursion encountered"), FailureKind::Declarative);
assert_eq!(c.classify("connection refused"), FailureKind::Transient);
}
}