dcs2 0.1.0

An extensible distributed control system framework made in rust with no-std support.
Documentation
use core::fmt::Display;
use bumpalo::Bump;
use serde::{Deserialize, Serialize};

use crate::heapless::LinearMap;
use crate::properties::RULES_AMOUNT;
use crate::rules::measurements::SystemState;
use crate::rules::strategy::average::{MeanBelowStrategy, MeanOverStrategy};
use crate::rules::strategy::max::MaxOverStrategy;
use crate::rules::strategy::min::MinBelowStrategy;
use crate::rules::strategy::{Rule, RuleStrategy, RuleType};

/// The type of alert, it defines if the system should increase or decrease any variable.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum SystemStatus {
    DECREASE,
    INCREASE,
    OK,
}

impl Display for SystemStatus {
    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            SystemStatus::DECREASE => formatter.write_str("DECREASE"),
            SystemStatus::INCREASE => formatter.write_str("INCREASE"),
            SystemStatus::OK => formatter.write_str("OK"),
        }
    }
}

/// Main engine abstraction, it contains a pool of rules identified by tags, the current active one and
/// a control value that will be used to check if a it was triggered.
pub struct RulesEngine {
    rules: RulesPool,
    current: RuleType,
    control: i32,
}

impl RulesEngine {
    pub fn new(tag: RuleType, threshold: i32) -> Self {
        RulesEngine {
            rules: RulesPool::dcs(),
            current: tag,
            control: threshold,
        }
    }

    /// Returns true if, given the `state` the current rule is triggered. If the current rule
    /// is not included in the rule pool then false is returned.
    pub fn evaluate(&mut self, state: &SystemState) -> SystemStatus {
        self.rules
            .get(self.current)
            .map(|rule| rule.evaluate(self.control, &state.last_measurements()))
            .unwrap_or(SystemStatus::OK)
    }

    /// Updates the current active strategy
    pub fn update_strategy(&mut self, change: Rule) {
        self.current = change.name;
        self.control = change.threshold;
    }

    pub fn current(&self) -> Rule {
        Rule { name: self.current, threshold: self.control }
    }
}

#[derive(Default)]
pub struct RulesPool {
    strategies: LinearMap<RuleType, *const dyn RuleStrategy, RULES_AMOUNT>,
    bump: Bump,
}

impl RulesPool {
    pub fn dcs() -> Self {
        let mut pool = RulesPool::empty();
        pool.add(RuleType::MaxOver, MaxOverStrategy);
        pool.add(RuleType::MinBelow, MinBelowStrategy);
        pool.add(RuleType::MeanOver, MeanOverStrategy);
        pool.add(RuleType::MeanBelow, MeanBelowStrategy);
        pool
    }

    pub fn empty() -> Self {
        Self {
            strategies: LinearMap::new(),
            bump: Bump::new(),
        }
    }

    pub fn is_empty(&self) -> bool {
        self.strategies.is_empty()
    }

    pub fn add<T: RuleStrategy + 'static>(&mut self, tag: RuleType, rule: T) {
        let bumped: *const dyn RuleStrategy = self.bump.alloc(rule);
        let _ = self.strategies.insert(tag, bumped);
    }

    pub fn get(&self, tag: RuleType) -> Option<&dyn RuleStrategy> {
        self.strategies.get(&tag).and_then(|ptr| {
            unsafe { ptr.as_ref() }
        })
    }
}

#[cfg(test)]
mod test_utils {
    use crate::heapless::Vec;
    use crate::nodes::SystemNodeId;
    use crate::properties::MeasurementsVec;
    use crate::rules::manager::SystemStatus;
    use crate::rules::measurements::{Measurement, ClusterType, SystemState};
    use crate::rules::strategy::RuleStrategy;

    #[derive(Debug)]
    pub struct FakeStrategy {
        status: SystemStatus,
    }

    impl FakeStrategy {
        pub fn new(status: SystemStatus) -> Self {
            Self { status }
        }
    }

    impl RuleStrategy for FakeStrategy {
        fn evaluate(&self, _: i32, _: &MeasurementsVec) -> SystemStatus {
            self.status.clone()
        }
    }

    pub fn some_measurements() -> MeasurementsVec {
        Vec::new()
    }

    pub fn some_state() -> SystemState {
        SystemState::new(
            SystemNodeId::default(),
            Measurement::new(ClusterType::TEMPERATURE, 20),
        )
    }
}

#[cfg(test)]
mod engine_test {
    use crate::rules::manager::test_utils::{some_state, FakeStrategy};
    use crate::rules::manager::*;

    impl RulesEngine {
        pub fn test(rules: RulesPool, current: RuleType) -> Self {
            RulesEngine {
                rules,
                current,
                control: 0,
            }
        }
    }

    #[test]
    fn returns_result_from_strategy() {
        let mut rules = RulesPool::empty();
        rules.add(
            RuleType::MeanOver,
            FakeStrategy::new(SystemStatus::DECREASE),
        );

        let mut engine = RulesEngine::test(rules, RuleType::MeanOver);
        matches!(engine.evaluate(&some_state()), SystemStatus::DECREASE);
    }

    #[test]
    fn can_update_rule_strategy() {
        let mut rules = RulesPool::empty();
        rules.add(
            RuleType::MeanOver,
            FakeStrategy::new(SystemStatus::DECREASE),
        );
        rules.add(
            RuleType::MeanBelow,
            FakeStrategy::new(SystemStatus::INCREASE),
        );

        let mut engine = RulesEngine::test(rules, RuleType::MeanOver);
        engine.update_strategy(Rule {
            name: RuleType::MeanBelow,
            threshold: 0,
        });

        matches!(engine.evaluate(&some_state()), SystemStatus::INCREASE);
    }
}

#[cfg(test)]
mod pool_tests {
    use crate::rules::manager::test_utils::{some_measurements, FakeStrategy};

    use super::*;

    #[test]
    fn can_create_empty_pool() {
        let pool = RulesPool::empty();
        assert!(pool.is_empty());
    }

    #[test]
    fn after_adding_rule_then_is_not_empty() {
        let mut pool = RulesPool::empty();
        pool.add(RuleType::MeanOver, rule_returning_increase());
        assert!(!pool.is_empty());
    }

    #[test]
    fn after_adding_rule_then_can_get_the_rule() {
        let mut pool = RulesPool::empty();
        pool.add(RuleType::MeanOver, rule_returning_increase());
        let rule = pool.get(RuleType::MeanOver).unwrap();
        matches!(
            rule.evaluate(0, &some_measurements()),
            SystemStatus::INCREASE
        );
    }

    #[test]
    fn getting_nonexistent_rule_returns_none() {
        let pool = RulesPool::empty();
        let rule = pool.get(RuleType::MeanOver);
        assert!(rule.is_none());
    }

    #[test]
    fn given_pool_with_multiple_rules_then_can_get_both() {
        let mut pool = RulesPool::empty();
        pool.add(RuleType::MeanBelow, rule_returning_increase());
        pool.add(RuleType::MeanOver, rule_returning_decrease());

        let rule1 = pool.get(RuleType::MeanBelow).unwrap();
        let rule2 = pool.get(RuleType::MeanOver).unwrap();

        matches!(
            rule1.evaluate(0, &some_measurements()),
            SystemStatus::INCREASE
        );
        matches!(
            rule2.evaluate(0, &some_measurements()),
            SystemStatus::DECREASE
        );
    }

    fn rule_returning_increase() -> FakeStrategy {
        FakeStrategy::new(SystemStatus::INCREASE)
    }

    fn rule_returning_decrease() -> FakeStrategy {
        FakeStrategy::new(SystemStatus::INCREASE)
    }
}

#[macro_export]
macro_rules! pool {
    ( $( $tag:ident, $x:ident ),* ) => {
        {
            let mut pool = RulesPool::new();
            $(
                pool.add($tag, $x)
            )*
            pool
        }
    };
}