1#![allow(missing_docs)]
46
47use std::sync::Arc;
48
49pub struct Chain<I: ?Sized, O> {
51 rules: Vec<Arc<dyn Fn(&I) -> Option<O> + Send + Sync>>,
52 fallback: Arc<dyn Fn(&I) -> O + Send + Sync>,
53}
54
55impl<I: ?Sized, O> Chain<I, O> {
56 pub fn new<F>(fallback: F) -> Self
60 where
61 F: Fn(&I) -> O + Send + Sync + 'static,
62 {
63 Self {
64 rules: Vec::new(),
65 fallback: Arc::new(fallback),
66 }
67 }
68
69 #[must_use]
71 pub fn with_rule<R>(mut self, rule: R) -> Self
72 where
73 R: Fn(&I) -> Option<O> + Send + Sync + 'static,
74 {
75 self.rules.push(Arc::new(rule));
76 self
77 }
78
79 pub fn rule_count(&self) -> usize {
81 self.rules.len()
82 }
83
84 pub fn evaluate(&self, input: &I) -> O {
88 for rule in &self.rules {
89 if let Some(o) = rule(input) {
90 return o;
91 }
92 }
93 (self.fallback)(input)
94 }
95}
96
97impl<I: ?Sized, O> std::fmt::Debug for Chain<I, O> {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 f.debug_struct("Chain")
100 .field("rules", &self.rules.len())
101 .finish()
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::testing::assert_deterministic_over;
109
110 #[derive(Debug, PartialEq, Eq, Clone)]
111 enum Color {
112 Red,
113 Green,
114 Blue,
115 Unknown,
116 }
117
118 #[test]
119 fn empty_chain_returns_fallback() {
120 let c = Chain::<str, Color>::new(|_| Color::Unknown);
121 assert_eq!(c.evaluate("anything"), Color::Unknown);
122 assert_eq!(c.rule_count(), 0);
123 }
124
125 #[test]
126 fn first_matching_rule_wins() {
127 let c = Chain::<str, Color>::new(|_| Color::Unknown)
128 .with_rule(|s| s.contains("red").then_some(Color::Red))
129 .with_rule(|s| s.contains("green").then_some(Color::Green))
130 .with_rule(|s| s.contains("blue").then_some(Color::Blue));
131
132 assert_eq!(c.evaluate("red and green"), Color::Red);
133 assert_eq!(c.evaluate("blue sky"), Color::Blue);
134 assert_eq!(c.evaluate("nothing"), Color::Unknown);
135 assert_eq!(c.rule_count(), 3);
136 }
137
138 #[test]
139 fn fallback_fires_on_no_match() {
140 let c = Chain::<str, Color>::new(|_| Color::Unknown)
141 .with_rule(|s| s.contains("red").then_some(Color::Red));
142 assert_eq!(c.evaluate("none of that"), Color::Unknown);
143 }
144
145 #[test]
146 fn determinism_law() {
147 let c = Chain::<str, Color>::new(|_| Color::Unknown)
148 .with_rule(|s| s.contains("r").then_some(Color::Red))
149 .with_rule(|s| s.contains("g").then_some(Color::Green));
150
151 assert_deterministic_over(&["red", "green", "ranger", "neither", ""], |&input| {
152 c.evaluate(input)
153 });
154 }
155
156 #[test]
157 fn debug_impl_shows_rule_count() {
158 let c = Chain::<str, Color>::new(|_| Color::Unknown)
159 .with_rule(|s| s.contains("r").then_some(Color::Red));
160 let debug_str = format!("{c:?}");
161 assert!(debug_str.contains("rules"));
162 assert!(debug_str.contains('1'));
163 }
164
165 #[test]
166 fn composes_with_classifier_trait() {
167 use crate::Classifier;
172 let c = Chain::<str, Color>::new(|_| Color::Unknown)
173 .with_rule(|s: &str| s.contains("red").then_some(Color::Red));
174 assert_eq!(c.classify("red"), Color::Red);
175 assert_eq!(c.classify("none"), Color::Unknown);
176 }
177}