1use std::{collections::HashMap, fmt, sync::Arc};
17
18use serde::{Deserialize, Serialize};
19
20use super::run::ValueStore;
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct NamedConditionId(pub String);
28
29impl fmt::Display for NamedConditionId {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 f.write_str(&self.0)
32 }
33}
34
35impl From<&str> for NamedConditionId {
36 fn from(s: &str) -> Self {
37 Self(s.to_string())
38 }
39}
40
41impl From<String> for NamedConditionId {
42 fn from(s: String) -> Self {
43 Self(s)
44 }
45}
46
47pub type ConditionFn<S> = Arc<dyn Fn(&S, &ValueStore) -> bool + Send + Sync>;
49
50pub type ConditionMap<S> = HashMap<String, ConditionFn<S>>;
53
54pub struct ConditionEval<'a, S> {
59 pub state: &'a S,
61 pub values: &'a ValueStore,
63 pub conditions: &'a ConditionMap<S>,
65 pub source_skipped: bool,
68 pub source_errored: bool,
71}
72
73pub enum Condition<S> {
83 Always,
85 Never,
87 OnSkip,
89 OnError,
93 Named(NamedConditionId),
95 And(Vec<Condition<S>>),
97 Or(Vec<Condition<S>>),
99 Not(Box<Condition<S>>),
101 Custom {
105 label: String,
107 f: ConditionFn<S>,
109 },
110}
111
112impl<S> Clone for Condition<S> {
115 fn clone(&self) -> Self {
116 match self {
117 Self::Always => Self::Always,
118 Self::Never => Self::Never,
119 Self::OnSkip => Self::OnSkip,
120 Self::OnError => Self::OnError,
121 Self::Named(id) => Self::Named(id.clone()),
122 Self::And(conds) => Self::And(conds.clone()),
123 Self::Or(conds) => Self::Or(conds.clone()),
124 Self::Not(cond) => Self::Not(cond.clone()),
125 Self::Custom { label, f } => Self::Custom {
126 label: label.clone(),
127 f: Arc::clone(f),
128 },
129 }
130 }
131}
132
133impl<S> Condition<S> {
134 #[must_use]
140 pub fn eval(&self, ctx: &ConditionEval<'_, S>) -> bool {
141 match self {
142 Self::Always => true,
143 Self::Never => false,
144 Self::OnSkip => ctx.source_skipped,
145 Self::OnError => ctx.source_errored,
146 Self::Named(id) => ctx
147 .conditions
148 .get(&id.0)
149 .is_some_and(|f| f(ctx.state, ctx.values)),
150 Self::And(conds) => conds.iter().all(|c| c.eval(ctx)),
151 Self::Or(conds) => conds.iter().any(|c| c.eval(ctx)),
152 Self::Not(cond) => !cond.eval(ctx),
153 Self::Custom { f, .. } => f(ctx.state, ctx.values),
154 }
155 }
156
157 #[must_use]
159 pub fn label(&self) -> String {
160 match self {
161 Self::Always => "always".to_string(),
162 Self::Never => "never".to_string(),
163 Self::OnSkip => "on_skip".to_string(),
164 Self::OnError => "on_error".to_string(),
165 Self::Named(id) => format!("named:{}", id.0),
166 Self::And(conds) => format!(
167 "and({})",
168 conds.iter().map(Self::label).collect::<Vec<_>>().join(",")
169 ),
170 Self::Or(conds) => format!(
171 "or({})",
172 conds.iter().map(Self::label).collect::<Vec<_>>().join(",")
173 ),
174 Self::Not(cond) => format!("not({})", cond.label()),
175 Self::Custom { label, .. } => format!("custom:{label}"),
176 }
177 }
178
179 pub fn collect_named(&self, out: &mut Vec<String>) {
183 match self {
184 Self::Named(id) => out.push(id.0.clone()),
185 Self::And(conds) | Self::Or(conds) => {
186 for c in conds {
187 c.collect_named(out);
188 }
189 }
190 Self::Not(cond) => cond.collect_named(out),
191 Self::Always | Self::Never | Self::OnSkip | Self::OnError | Self::Custom { .. } => {}
192 }
193 }
194
195 pub fn collect_custom_labels(&self, out: &mut Vec<String>) {
200 match self {
201 Self::Custom { label, .. } => out.push(label.clone()),
202 Self::And(conds) | Self::Or(conds) => {
203 for c in conds {
204 c.collect_custom_labels(out);
205 }
206 }
207 Self::Not(cond) => cond.collect_custom_labels(out),
208 Self::Always | Self::Never | Self::OnSkip | Self::OnError | Self::Named(_) => {}
209 }
210 }
211}
212
213impl<S> fmt::Debug for Condition<S> {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 f.write_str(&self.label())
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 fn eval_with<S>(cond: &Condition<S>, state: &S, conditions: &ConditionMap<S>) -> bool {
224 let values = ValueStore::default();
225 let ctx = ConditionEval {
226 state,
227 values: &values,
228 conditions,
229 source_skipped: false,
230 source_errored: false,
231 };
232 cond.eval(&ctx)
233 }
234
235 #[test]
236 fn test_always_and_never() {
237 let conditions: ConditionMap<()> = HashMap::new();
238 assert!(eval_with(&Condition::Always, &(), &conditions));
239 assert!(!eval_with(&Condition::Never, &(), &conditions));
240 }
241
242 #[test]
243 fn test_on_skip_and_on_error_flags() {
244 let values = ValueStore::default();
245 let conditions: ConditionMap<()> = HashMap::new();
246 let ctx = ConditionEval {
247 state: &(),
248 values: &values,
249 conditions: &conditions,
250 source_skipped: true,
251 source_errored: true,
252 };
253 assert!(Condition::<()>::OnSkip.eval(&ctx));
254 assert!(Condition::<()>::OnError.eval(&ctx));
255 assert!(!eval_with(&Condition::OnSkip, &(), &conditions));
256 assert!(!eval_with(&Condition::OnError, &(), &conditions));
257 }
258
259 #[test]
260 fn test_logical_combinators() {
261 let conditions: ConditionMap<()> = HashMap::new();
262 let cond: Condition<()> = Condition::And(vec![
263 Condition::Always,
264 Condition::Not(Box::new(Condition::Never)),
265 ]);
266 assert!(eval_with(&cond, &(), &conditions));
267
268 let cond: Condition<()> = Condition::Or(vec![Condition::Never, Condition::Always]);
269 assert!(eval_with(&cond, &(), &conditions));
270
271 let cond: Condition<()> = Condition::And(vec![Condition::Always, Condition::Never]);
272 assert!(!eval_with(&cond, &(), &conditions));
273 }
274
275 #[test]
276 fn test_named_condition_resolution_and_missing_name() {
277 let mut conditions: ConditionMap<u64> = HashMap::new();
278 conditions.insert("is_big".to_string(), Arc::new(|s: &u64, _| *s > 10));
279 assert!(eval_with(
280 &Condition::Named("is_big".into()),
281 &42_u64,
282 &conditions
283 ));
284 assert!(!eval_with(
286 &Condition::Named("nope".into()),
287 &42_u64,
288 &conditions
289 ));
290 }
291
292 #[test]
293 fn test_custom_condition_and_label_collection() {
294 let cond: Condition<u64> = Condition::And(vec![
295 Condition::Custom {
296 label: "hand_rolled".to_string(),
297 f: Arc::new(|s: &u64, _| *s == 7),
298 },
299 Condition::Named("registered".into()),
300 ]);
301 let mut custom = Vec::new();
302 let mut named = Vec::new();
303 cond.collect_custom_labels(&mut custom);
304 cond.collect_named(&mut named);
305 assert_eq!(custom, vec!["hand_rolled".to_string()]);
306 assert_eq!(named, vec!["registered".to_string()]);
307
308 let conditions: ConditionMap<u64> = HashMap::new();
310 assert!(!eval_with(&cond, &7_u64, &conditions));
311 }
312
313 #[test]
314 fn test_labels_for_events() {
315 assert_eq!(Condition::<()>::Always.label(), "always");
316 assert_eq!(Condition::<()>::Named("x".into()).label(), "named:x");
317 assert_eq!(
318 Condition::<()>::Not(Box::new(Condition::OnError)).label(),
319 "not(on_error)"
320 );
321 }
322}