use std::{collections::HashMap, fmt, sync::Arc};
use serde::{Deserialize, Serialize};
use super::run::ValueStore;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NamedConditionId(pub String);
impl fmt::Display for NamedConditionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<&str> for NamedConditionId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl From<String> for NamedConditionId {
fn from(s: String) -> Self {
Self(s)
}
}
pub type ConditionFn<S> = Arc<dyn Fn(&S, &ValueStore) -> bool + Send + Sync>;
pub type ConditionMap<S> = HashMap<String, ConditionFn<S>>;
pub struct ConditionEval<'a, S> {
pub state: &'a S,
pub values: &'a ValueStore,
pub conditions: &'a ConditionMap<S>,
pub source_skipped: bool,
pub source_errored: bool,
}
pub enum Condition<S> {
Always,
Never,
OnSkip,
OnError,
Named(NamedConditionId),
And(Vec<Condition<S>>),
Or(Vec<Condition<S>>),
Not(Box<Condition<S>>),
Custom {
label: String,
f: ConditionFn<S>,
},
}
impl<S> Clone for Condition<S> {
fn clone(&self) -> Self {
match self {
Self::Always => Self::Always,
Self::Never => Self::Never,
Self::OnSkip => Self::OnSkip,
Self::OnError => Self::OnError,
Self::Named(id) => Self::Named(id.clone()),
Self::And(conds) => Self::And(conds.clone()),
Self::Or(conds) => Self::Or(conds.clone()),
Self::Not(cond) => Self::Not(cond.clone()),
Self::Custom { label, f } => Self::Custom {
label: label.clone(),
f: Arc::clone(f),
},
}
}
}
impl<S> Condition<S> {
#[must_use]
pub fn eval(&self, ctx: &ConditionEval<'_, S>) -> bool {
match self {
Self::Always => true,
Self::Never => false,
Self::OnSkip => ctx.source_skipped,
Self::OnError => ctx.source_errored,
Self::Named(id) => ctx
.conditions
.get(&id.0)
.is_some_and(|f| f(ctx.state, ctx.values)),
Self::And(conds) => conds.iter().all(|c| c.eval(ctx)),
Self::Or(conds) => conds.iter().any(|c| c.eval(ctx)),
Self::Not(cond) => !cond.eval(ctx),
Self::Custom { f, .. } => f(ctx.state, ctx.values),
}
}
#[must_use]
pub fn label(&self) -> String {
match self {
Self::Always => "always".to_string(),
Self::Never => "never".to_string(),
Self::OnSkip => "on_skip".to_string(),
Self::OnError => "on_error".to_string(),
Self::Named(id) => format!("named:{}", id.0),
Self::And(conds) => format!(
"and({})",
conds.iter().map(Self::label).collect::<Vec<_>>().join(",")
),
Self::Or(conds) => format!(
"or({})",
conds.iter().map(Self::label).collect::<Vec<_>>().join(",")
),
Self::Not(cond) => format!("not({})", cond.label()),
Self::Custom { label, .. } => format!("custom:{label}"),
}
}
pub fn collect_named(&self, out: &mut Vec<String>) {
match self {
Self::Named(id) => out.push(id.0.clone()),
Self::And(conds) | Self::Or(conds) => {
for c in conds {
c.collect_named(out);
}
}
Self::Not(cond) => cond.collect_named(out),
Self::Always | Self::Never | Self::OnSkip | Self::OnError | Self::Custom { .. } => {}
}
}
pub fn collect_custom_labels(&self, out: &mut Vec<String>) {
match self {
Self::Custom { label, .. } => out.push(label.clone()),
Self::And(conds) | Self::Or(conds) => {
for c in conds {
c.collect_custom_labels(out);
}
}
Self::Not(cond) => cond.collect_custom_labels(out),
Self::Always | Self::Never | Self::OnSkip | Self::OnError | Self::Named(_) => {}
}
}
}
impl<S> fmt::Debug for Condition<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.label())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn eval_with<S>(cond: &Condition<S>, state: &S, conditions: &ConditionMap<S>) -> bool {
let values = ValueStore::default();
let ctx = ConditionEval {
state,
values: &values,
conditions,
source_skipped: false,
source_errored: false,
};
cond.eval(&ctx)
}
#[test]
fn test_always_and_never() {
let conditions: ConditionMap<()> = HashMap::new();
assert!(eval_with(&Condition::Always, &(), &conditions));
assert!(!eval_with(&Condition::Never, &(), &conditions));
}
#[test]
fn test_on_skip_and_on_error_flags() {
let values = ValueStore::default();
let conditions: ConditionMap<()> = HashMap::new();
let ctx = ConditionEval {
state: &(),
values: &values,
conditions: &conditions,
source_skipped: true,
source_errored: true,
};
assert!(Condition::<()>::OnSkip.eval(&ctx));
assert!(Condition::<()>::OnError.eval(&ctx));
assert!(!eval_with(&Condition::OnSkip, &(), &conditions));
assert!(!eval_with(&Condition::OnError, &(), &conditions));
}
#[test]
fn test_logical_combinators() {
let conditions: ConditionMap<()> = HashMap::new();
let cond: Condition<()> = Condition::And(vec![
Condition::Always,
Condition::Not(Box::new(Condition::Never)),
]);
assert!(eval_with(&cond, &(), &conditions));
let cond: Condition<()> = Condition::Or(vec![Condition::Never, Condition::Always]);
assert!(eval_with(&cond, &(), &conditions));
let cond: Condition<()> = Condition::And(vec![Condition::Always, Condition::Never]);
assert!(!eval_with(&cond, &(), &conditions));
}
#[test]
fn test_named_condition_resolution_and_missing_name() {
let mut conditions: ConditionMap<u64> = HashMap::new();
conditions.insert("is_big".to_string(), Arc::new(|s: &u64, _| *s > 10));
assert!(eval_with(
&Condition::Named("is_big".into()),
&42_u64,
&conditions
));
assert!(!eval_with(
&Condition::Named("nope".into()),
&42_u64,
&conditions
));
}
#[test]
fn test_custom_condition_and_label_collection() {
let cond: Condition<u64> = Condition::And(vec![
Condition::Custom {
label: "hand_rolled".to_string(),
f: Arc::new(|s: &u64, _| *s == 7),
},
Condition::Named("registered".into()),
]);
let mut custom = Vec::new();
let mut named = Vec::new();
cond.collect_custom_labels(&mut custom);
cond.collect_named(&mut named);
assert_eq!(custom, vec!["hand_rolled".to_string()]);
assert_eq!(named, vec!["registered".to_string()]);
let conditions: ConditionMap<u64> = HashMap::new();
assert!(!eval_with(&cond, &7_u64, &conditions));
}
#[test]
fn test_labels_for_events() {
assert_eq!(Condition::<()>::Always.label(), "always");
assert_eq!(Condition::<()>::Named("x".into()).label(), "named:x");
assert_eq!(
Condition::<()>::Not(Box::new(Condition::OnError)).label(),
"not(on_error)"
);
}
}