From 85ef562c49e2f5a2a5d19098282ce375f4d649f6 Mon Sep 17 00:00:00 2001
From: librelois <elois@ifee.fr>
Date: Sat, 20 Apr 2019 00:45:50 +0200
Subject: [PATCH] [feat] add crate rules-engine
---
Cargo.toml | 1 +
lib/tools/rules-engine/Cargo.toml | 17 +
lib/tools/rules-engine/src/lib.rs | 479 +++++++++++++++++++++++++++++
lib/tools/rules-engine/src/rule.rs | 138 +++++++++
4 files changed, 635 insertions(+)
create mode 100644 lib/tools/rules-engine/Cargo.toml
create mode 100644 lib/tools/rules-engine/src/lib.rs
create mode 100644 lib/tools/rules-engine/src/rule.rs
diff --git a/Cargo.toml b/Cargo.toml
index efc3bc36..650d3911 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,6 +18,7 @@ members = [
"lib/tools/documents",
"lib/tools/json-pest-parser",
"lib/tools/network-documents",
+ "lib/tools/rules-engine",
"lib/tests-tools/crypto-tests-tools",
"lib/tests-tools/documents-tests-tools",
"lib/tests-tools/rust-tests-tools",
diff --git a/lib/tools/rules-engine/Cargo.toml b/lib/tools/rules-engine/Cargo.toml
new file mode 100644
index 00000000..a206cdd4
--- /dev/null
+++ b/lib/tools/rules-engine/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "rules-engine"
+version = "0.1.0"
+authors = ["librelois <elois@ifee.fr>"]
+description = "Rules engine"
+license = "AGPL-3.0"
+edition = "2018"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
+failure = "0.1.5"
+rayon = "1.0.3"
+
+[dev-dependencies]
+maplit = "1.0.1"
diff --git a/lib/tools/rules-engine/src/lib.rs b/lib/tools/rules-engine/src/lib.rs
new file mode 100644
index 00000000..80071192
--- /dev/null
+++ b/lib/tools/rules-engine/src/lib.rs
@@ -0,0 +1,479 @@
+// Copyright (C) 2019 Éloïs SANCHEZ
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+//! Rules engine
+
+#![deny(
+ missing_copy_implementations,
+ trivial_casts,
+ trivial_numeric_casts,
+ unsafe_code,
+ unstable_features,
+ unused_import_braces
+)]
+
+pub mod rule;
+
+use failure::Fail;
+use rayon::prelude::*;
+use rule::{Rule, RuleError, RuleNumber};
+use std::collections::BTreeMap;
+use std::fmt::Debug;
+
+#[derive(Copy, Clone, Debug, Ord, PartialEq, PartialOrd, Eq, Hash)]
+pub struct ProtocolVersion(pub usize);
+
+impl std::fmt::Display for ProtocolVersion {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub struct ProtocolRules(pub Vec<RulesGroup>);
+
+impl From<Vec<usize>> for ProtocolRules {
+ fn from(rules_numbers: Vec<usize>) -> Self {
+ ProtocolRules(vec![RulesGroup::Ser(
+ rules_numbers.into_iter().map(RuleNumber).collect(),
+ )])
+ }
+}
+
+impl From<Vec<RulesGroup>> for ProtocolRules {
+ fn from(rules_groups: Vec<RulesGroup>) -> Self {
+ ProtocolRules(rules_groups)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+/// Protocol
+pub struct Protocol(BTreeMap<ProtocolVersion, ProtocolRules>);
+
+impl Protocol {
+ /// Create new protocol
+ /// protocol_versions: Dictionary of rules to be applied for each version of the protocol (rules will be applied in the order provided)
+ pub fn new(protocol_versions: BTreeMap<ProtocolVersion, ProtocolRules>) -> Self {
+ Protocol(protocol_versions)
+ }
+
+ /// Get specific protocol version
+ pub fn get(&self, protocol_version: ProtocolVersion) -> Option<&ProtocolRules> {
+ self.0.get(&protocol_version)
+ }
+}
+
+/// Rules groups
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum RulesGroup {
+ /// In serial
+ Ser(Vec<RuleNumber>),
+ /// In parallel
+ Par(Vec<RulesGroup>),
+}
+
+impl RulesGroup {
+ #[inline]
+ /// Create singleton rules group
+ pub fn s1(rule_number: usize) -> Self {
+ RulesGroup::Ser(vec![RuleNumber(rule_number)])
+ }
+ #[inline]
+ /// Create parallel set of rules
+ pub fn pr(rules_numbers: Vec<usize>) -> Self {
+ RulesGroup::Par(rules_numbers.into_iter().map(RulesGroup::s1).collect())
+ }
+}
+
+/// Rules engine
+pub struct RulesEngine<D: Debug + Sync, E: Eq + Fail + PartialEq> {
+ /// All rules
+ all_rules: BTreeMap<RuleNumber, Rule<D, E>>,
+}
+
+impl<D: Debug + Sync, E: Eq + Fail + PartialEq> RulesEngine<D, E> {
+ /// Create new rules engine
+ pub fn new(all_rules: BTreeMap<RuleNumber, Rule<D, E>>) -> Self {
+ RulesEngine { all_rules }
+ }
+
+ fn apply_rules_group_ref(
+ &self,
+ protocol_version: ProtocolVersion,
+ rules_group: RulesGroup,
+ rule_datas: &D,
+ ) -> Result<(), EngineError<E>> {
+ match rules_group {
+ RulesGroup::Ser(rules_numbers) => rules_numbers
+ .into_iter()
+ .map(|rule_number| self.apply_rule_ref(protocol_version, rule_number, rule_datas))
+ .collect(),
+ RulesGroup::Par(rules_group) => rules_group
+ .into_par_iter()
+ .map(|rg| self.apply_rules_group_ref(protocol_version, rg, rule_datas))
+ .collect(),
+ }
+ }
+
+ fn apply_rule_ref(
+ &self,
+ protocol_version: ProtocolVersion,
+ rule_number: RuleNumber,
+ rule_datas: &D,
+ ) -> Result<(), EngineError<E>> {
+ if let Some(rule) = self.all_rules.get(&rule_number) {
+ rule.execute(protocol_version, rule_number, rule_datas)
+ } else {
+ Err(EngineError::RuleNotExist {
+ rule_number,
+ protocol_version,
+ })
+ }
+ }
+
+ fn apply_rule_mut(
+ &self,
+ protocol_version: ProtocolVersion,
+ rule_number: RuleNumber,
+ rule_datas: &mut D,
+ ) -> Result<(), EngineError<E>> {
+ if let Some(rule) = self.all_rules.get(&rule_number) {
+ rule.execute_mut(protocol_version, rule_number, rule_datas)
+ } else {
+ Err(EngineError::RuleNotExist {
+ rule_number,
+ protocol_version,
+ })
+ }
+ }
+
+ /// Apply a specific version of the protocol
+ pub fn apply_protocol(
+ &self,
+ protocol: Protocol,
+ protocol_version: ProtocolVersion,
+ rule_datas: &mut D,
+ ) -> Result<(), EngineError<E>> {
+ if let Some(protocol_rules) = protocol.get(protocol_version) {
+ for rules_group in &protocol_rules.0 {
+ let result: Result<(), EngineError<E>> = match rules_group {
+ RulesGroup::Ser(rules_numbers) => rules_numbers
+ .iter()
+ .map(|rule_number| {
+ self.apply_rule_mut(protocol_version, *rule_number, rule_datas)
+ })
+ .collect(),
+ RulesGroup::Par(rules_group) => rules_group
+ .par_iter()
+ .map(|rg| {
+ self.apply_rules_group_ref(protocol_version, rg.clone(), rule_datas)
+ })
+ .collect(),
+ };
+ if let Err(err) = result {
+ return Err(err);
+ }
+ }
+
+ Ok(())
+ } else {
+ Err(EngineError::ProtocolVersionNotExist { protocol_version })
+ }
+ }
+}
+
+/// Protocol error
+#[derive(Debug, Eq, Fail, PartialEq)]
+pub enum EngineError<E: Eq + Fail + PartialEq> {
+ #[fail(display = "{}", _0)]
+ /// Rule Error
+ RuleError(RuleError<E>),
+ #[fail(display = "protocol V{} not exist", protocol_version)]
+ /// The protocol version does not exist
+ ProtocolVersionNotExist {
+ /// Protocole version
+ protocol_version: ProtocolVersion,
+ },
+ #[fail(
+ display = "Rule n°{} not exist (require by protocol V{})",
+ rule_number, protocol_version
+ )]
+ /// A rule required by the protocol version does not exist
+ RuleNotExist {
+ /// Rule number
+ rule_number: RuleNumber,
+ /// Protocole version
+ protocol_version: ProtocolVersion,
+ },
+ #[fail(
+ display = "Rule n°{} is mutable and called in parallel in the V{} protocol, this is prohibited.
+ A rule can be mutable or called in parallel but not both at the same time.",
+ rule_number, protocol_version
+ )]
+ /// Calling a mutable rule in a part executed in parallel
+ MutRuleInPar {
+ /// Rule number
+ rule_number: RuleNumber,
+ /// Protocole version
+ protocol_version: ProtocolVersion,
+ },
+ #[fail(
+ display = "Rule n°{} does not exist in a version less than or equal to the protocol version (V{})",
+ rule_number, protocol_version
+ )]
+ /// Calling a rule too recent
+ RuleTooRecent {
+ /// Rule number
+ rule_number: RuleNumber,
+ /// Protocole version
+ protocol_version: ProtocolVersion,
+ },
+}
+
+#[cfg(test)]
+mod tests {
+
+ use super::rule::*;
+ use super::*;
+ use maplit::btreemap;
+
+ #[derive(Debug)]
+ struct Datas {
+ i: usize,
+ }
+
+ #[derive(Debug, Eq, Fail, PartialEq)]
+ #[fail(display = "")]
+ struct Error {}
+
+ fn r2_v1(datas: &mut Datas) -> Result<(), Error> {
+ if datas.i == 0 {
+ datas.i += 1;
+ Ok(())
+ } else {
+ Err(Error {})
+ }
+ }
+
+ fn r3_v2(datas: &Datas) -> Result<(), Error> {
+ if datas.i == 1 {
+ Ok(())
+ } else {
+ Err(Error {})
+ }
+ }
+
+ fn get_test_engine() -> RulesEngine<Datas, Error> {
+ let all_rules: BTreeMap<RuleNumber, Rule<Datas, Error>> = btreemap![
+ RuleNumber(2) => Rule::new(RuleNumber(2), btreemap![
+ ProtocolVersion(1) => RuleFn::RefMut(r2_v1),
+ ]).expect("Fail to create rule n°2"),
+ RuleNumber(3) => Rule::new(RuleNumber(3), btreemap![
+ ProtocolVersion(2) => RuleFn::Ref(r3_v2),
+ ]).expect("Fail to create rule n°2"),
+ ];
+
+ RulesEngine::new(all_rules)
+ }
+
+ #[test]
+ fn rule_without_impl() {
+ if let Err(err) = Rule::<Datas, Error>::new(RuleNumber(1), btreemap![]) {
+ assert_eq!(
+ RuleWithoutImpl {
+ rule_number: RuleNumber(1),
+ },
+ err,
+ )
+ } else {
+ panic!("Rule creation must be fail")
+ }
+
+ println!("{}", ProtocolVersion(1));
+ println!("{}", RuleNumber(1));
+ }
+
+ #[test]
+ fn protocol_empty() -> Result<(), EngineError<Error>> {
+ let engine = get_test_engine();
+
+ let mut datas = Datas { i: 0 };
+
+ let protocol_empty: Protocol = Protocol::new(btreemap![
+ ProtocolVersion(1) => Vec::<usize>::with_capacity(0).into()
+ ]);
+
+ engine.apply_protocol(protocol_empty, ProtocolVersion(1), &mut datas)
+ }
+
+ #[test]
+ fn protocol_version_not_exist() {
+ let engine = get_test_engine();
+
+ let mut datas = Datas { i: 0 };
+
+ let protocol_empty: Protocol = Protocol::new(btreemap![
+ ProtocolVersion(1) => Vec::<usize>::with_capacity(0).into()
+ ]);
+
+ assert_eq!(
+ Err(EngineError::ProtocolVersionNotExist {
+ protocol_version: ProtocolVersion(2),
+ }),
+ engine.apply_protocol(protocol_empty, ProtocolVersion(2), &mut datas)
+ )
+ }
+
+ #[test]
+ fn rule_not_exist() {
+ let engine = get_test_engine();
+
+ let mut datas = Datas { i: 0 };
+
+ let protocol: Protocol = Protocol::new(btreemap![
+ ProtocolVersion(1) => vec![1usize].into()
+ ]);
+
+ assert_eq!(
+ Err(EngineError::RuleNotExist {
+ rule_number: RuleNumber(1),
+ protocol_version: ProtocolVersion(1)
+ }),
+ engine.apply_protocol(protocol, ProtocolVersion(1), &mut datas)
+ );
+
+ let mut datas = Datas { i: 0 };
+
+ let protocol_par: Protocol = Protocol::new(btreemap![
+ ProtocolVersion(1) => vec![RulesGroup::pr(vec![1usize])].into()
+ ]);
+
+ assert_eq!(
+ Err(EngineError::RuleNotExist {
+ rule_number: RuleNumber(1),
+ protocol_version: ProtocolVersion(1)
+ }),
+ engine.apply_protocol(protocol_par, ProtocolVersion(1), &mut datas)
+ );
+ }
+
+ #[test]
+ fn rule_fail() {
+ let engine = get_test_engine();
+
+ let mut datas = Datas { i: 1 };
+
+ let protocol: Protocol = Protocol::new(btreemap![
+ ProtocolVersion(1) => vec![2usize].into()
+ ]);
+
+ assert_eq!(
+ Err(EngineError::RuleError(RuleError {
+ rule_number: RuleNumber(2),
+ cause: Error {},
+ })),
+ engine.apply_protocol(protocol, ProtocolVersion(1), &mut datas)
+ )
+ }
+
+ #[test]
+ fn par_rule_fail() {
+ let engine = get_test_engine();
+
+ let mut datas = Datas { i: 0 };
+
+ let protocol: Protocol = Protocol::new(btreemap![
+ ProtocolVersion(2) => vec![RulesGroup::pr(vec![3usize])].into()
+ ]);
+
+ assert_eq!(
+ Err(EngineError::RuleError(RuleError {
+ rule_number: RuleNumber(3),
+ cause: Error {},
+ })),
+ engine.apply_protocol(protocol, ProtocolVersion(2), &mut datas)
+ )
+ }
+
+ #[test]
+ fn rule_too_recent() {
+ let engine = get_test_engine();
+
+ let mut datas = Datas { i: 0 };
+
+ let protocol: Protocol = Protocol::new(btreemap![
+ ProtocolVersion(1) => vec![2usize, 3].into()
+ ]);
+
+ assert_eq!(
+ Err(EngineError::RuleTooRecent {
+ protocol_version: ProtocolVersion(1),
+ rule_number: RuleNumber(3),
+ }),
+ engine.apply_protocol(protocol, ProtocolVersion(1), &mut datas)
+ )
+ }
+
+ #[test]
+ fn par_rule_too_recent() {
+ let engine = get_test_engine();
+
+ let mut datas = Datas { i: 0 };
+
+ let protocol: Protocol = Protocol::new(btreemap![
+ ProtocolVersion(1) => vec![RulesGroup::pr(vec![3])].into()
+ ]);
+
+ assert_eq!(
+ Err(EngineError::RuleTooRecent {
+ protocol_version: ProtocolVersion(1),
+ rule_number: RuleNumber(3),
+ }),
+ engine.apply_protocol(protocol, ProtocolVersion(1), &mut datas)
+ )
+ }
+
+ #[test]
+ fn mut_rule_in_par_protocol() {
+ let engine = get_test_engine();
+
+ let mut datas = Datas { i: 1 };
+
+ let protocol: Protocol = Protocol::new(btreemap![
+ ProtocolVersion(2) => vec![RulesGroup::pr(vec![2usize, 3])].into()
+ ]);
+
+ assert_eq!(
+ Err(EngineError::MutRuleInPar {
+ protocol_version: ProtocolVersion(2),
+ rule_number: RuleNumber(2),
+ }),
+ engine.apply_protocol(protocol, ProtocolVersion(2), &mut datas)
+ )
+ }
+
+ #[test]
+ fn protocol_success() -> Result<(), EngineError<Error>> {
+ let engine = get_test_engine();
+
+ let mut datas = Datas { i: 0 };
+
+ let protocol: Protocol = Protocol::new(btreemap![
+ ProtocolVersion(2) => vec![2usize, 3].into()
+ ]);
+
+ engine.apply_protocol(protocol, ProtocolVersion(2), &mut datas)
+ }
+}
diff --git a/lib/tools/rules-engine/src/rule.rs b/lib/tools/rules-engine/src/rule.rs
new file mode 100644
index 00000000..62dfd220
--- /dev/null
+++ b/lib/tools/rules-engine/src/rule.rs
@@ -0,0 +1,138 @@
+// Copyright (C) 2019 Éloïs SANCHEZ
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+//! Rules engine : rules
+
+use crate::{EngineError, ProtocolVersion};
+use failure::Fail;
+use std::collections::BTreeMap;
+use std::fmt::Debug;
+
+#[derive(Copy, Clone, Debug, Ord, PartialEq, PartialOrd, Eq, Hash)]
+/// Rule number
+pub struct RuleNumber(pub usize);
+
+impl std::fmt::Display for RuleNumber {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+/// Rule error
+#[derive(Debug, Eq, Fail, PartialEq)]
+#[fail(display = "An error occurred with rule n°{} : {}", rule_number, cause)]
+pub struct RuleError<E: Eq + Fail + PartialEq> {
+ /// Rule number
+ pub rule_number: RuleNumber,
+ /// Cause of the error
+ pub cause: E,
+}
+
+/// Rule immutable execution function
+pub type RuleFnRef<D, E> = fn(&D) -> Result<(), E>;
+
+/// Rule mutable execution function
+pub type RuleFnRefMut<D, E> = fn(&mut D) -> Result<(), E>;
+
+/// Rule execution function
+pub enum RuleFn<D, E> {
+ Ref(RuleFnRef<D, E>),
+ RefMut(RuleFnRefMut<D, E>),
+}
+
+#[derive(Debug, Copy, Clone, Eq, Fail, PartialEq)]
+#[fail(
+ display = "Fatal error: rules-engine: try to create rule n°{} without implementation !",
+ rule_number
+)]
+pub struct RuleWithoutImpl {
+ pub rule_number: RuleNumber,
+}
+
+/// Rule
+pub struct Rule<D: Debug, E: Eq + Fail + PartialEq> {
+ /// Dictionary of the different versions of the rule execution function
+ rule_versions: BTreeMap<ProtocolVersion, RuleFn<D, E>>,
+}
+
+impl<D: Debug, E: Eq + Fail + PartialEq> Rule<D, E> {
+ /// Create new rule
+ pub fn new(
+ rule_number: RuleNumber,
+ rule_versions: BTreeMap<ProtocolVersion, RuleFn<D, E>>,
+ ) -> Result<Self, RuleWithoutImpl> {
+ if rule_versions.is_empty() {
+ Err(RuleWithoutImpl { rule_number })
+ } else {
+ Ok(Rule { rule_versions })
+ }
+ }
+ /// Executes the correct version of the rule
+ pub fn execute(
+ &self,
+ protocol_version: ProtocolVersion,
+ rule_number: RuleNumber,
+ rule_datas: &D,
+ ) -> Result<(), EngineError<E>> {
+ let rule_opt: Option<(&ProtocolVersion, &RuleFn<D, E>)> =
+ self.rule_versions.range(..=protocol_version).last();
+ if let Some((_, rule_fn)) = rule_opt {
+ match rule_fn {
+ RuleFn::Ref(rule_fn_ref) => rule_fn_ref(rule_datas).map_err(|err| {
+ EngineError::RuleError(RuleError {
+ rule_number,
+ cause: err,
+ })
+ }),
+ RuleFn::RefMut(_) => Err(EngineError::MutRuleInPar {
+ rule_number,
+ protocol_version,
+ }),
+ }
+ } else {
+ Err(EngineError::RuleTooRecent {
+ rule_number,
+ protocol_version,
+ })
+ }
+ }
+ /// Executes the correct version of the rule
+ pub fn execute_mut(
+ &self,
+ protocol_version: ProtocolVersion,
+ rule_number: RuleNumber,
+ rule_datas: &mut D,
+ ) -> Result<(), EngineError<E>> {
+ let rule_opt: Option<(&ProtocolVersion, &RuleFn<D, E>)> =
+ self.rule_versions.range(..=protocol_version).last();
+ if let Some((_, rule_fn)) = rule_opt {
+ match rule_fn {
+ RuleFn::Ref(rule_fn_ref) => rule_fn_ref(rule_datas),
+ RuleFn::RefMut(rule_fn_ref_mut) => rule_fn_ref_mut(rule_datas),
+ }
+ .map_err(|err| {
+ EngineError::RuleError(RuleError {
+ rule_number,
+ cause: err,
+ })
+ })
+ } else {
+ Err(EngineError::RuleTooRecent {
+ rule_number,
+ protocol_version,
+ })
+ }
+ }
+}
--
GitLab