diff --git a/src/domain/action/log.rs b/src/domain/action/log.rs index 98eaf58..648e2b2 100644 --- a/src/domain/action/log.rs +++ b/src/domain/action/log.rs @@ -1,8 +1,7 @@ -use crate::domain::{Action, LogMessage, LogPort, ModuleArgs, Record, Value}; +use crate::domain::{Action, LogMessage, LogPort, ModuleArgs, Record, Singleton, Value}; +use crate::singleton_borrow; use regex::Regex; -use std::cell::RefCell; use std::ops::{Index, Range}; -use std::rc::Rc; type LogFormat = fn(&str) -> LogMessage; @@ -16,14 +15,14 @@ const INFO_LOG_FORMAT: LogFormat = |s| LogMessage::INFO(&s); const DEBUG_LOG_FORMAT: LogFormat = |s| LogMessage::DEBUG(&s); pub struct Log { - logger: Rc>, + logger: Singleton, log_format: LogFormat, template: String, var_locations: Vec>, } impl Log { - pub fn from_args(mut args: ModuleArgs, logger: Rc>) -> Log { + pub fn from_args(mut args: ModuleArgs, logger: Singleton) -> Log { let log_format = match args.remove("level") { Some(Value::Str(l)) => match l.as_ref() { "EMERG" => EMERG_LOG_FORMAT, @@ -82,28 +81,25 @@ impl Log { impl Action for Log { fn act(&mut self, record: &mut Record) -> Result<(), ()> { let message = self.message_with_variables_from_record(record); - (self.logger.borrow_mut()).write((self.log_format)(&message)) + singleton_borrow!(self.logger).write((self.log_format)(&message)) } } #[cfg(test)] -#[macro_use] mod tests { use super::Log; - use crate::assert_log_match; - use crate::domain::test_util::*; - use crate::domain::{Action, ModuleArgs, Record, Value}; + use crate::domain::test_util::FakeLog; + use crate::domain::{Action, ModuleArgs, Record, Singleton, Value}; + use crate::{assert_log_match, singleton_new, singleton_share}; use core::panic; - use std::cell::RefCell; use std::collections::HashMap; - use std::rc::Rc; #[test] #[should_panic(expected = "The Log action needs a message template in “message”")] fn arg_message_is_mandatory() { let args: ModuleArgs = HashMap::new(); - let logger = Rc::new(RefCell::new(FakeLog::new(Vec::new()))); - Log::from_args(args, logger.clone()); + let logger = singleton_new!(FakeLog::new(Vec::new())); + Log::from_args(args, singleton_share!(logger)); } #[test] @@ -166,14 +162,14 @@ mod tests { level: Option<&str>, logs: Vec>, vars: Vec<(&str, &str)>, - ) -> (Log, Rc>, Record) { + ) -> (Log, Singleton, Record) { let mut args: ModuleArgs = HashMap::new(); args.insert("message".to_string(), Value::Str(template.to_string())); if let Some(l) = level { args.insert("level".to_string(), Value::Str(l.to_string())); } - let logger = Rc::new(RefCell::new(FakeLog::new(logs))); - let log = Log::from_args(args, logger.clone()); + let logger = singleton_new!(FakeLog::new(logs)); + let log = Log::from_args(args, singleton_share!(logger)); let mut record: Record = HashMap::new(); for (name, value) in vars { record.insert(name.to_string(), Value::Str(value.to_string())); diff --git a/src/domain/action/mod.rs b/src/domain/action/mod.rs index c437f6c..2f780ef 100644 --- a/src/domain/action/mod.rs +++ b/src/domain/action/mod.rs @@ -1,4 +1,63 @@ +mod counter_raise; +pub use self::counter_raise::*; +mod counter_reset; +pub use self::counter_reset::*; mod log; pub use self::log::*; mod noop; pub use self::noop::*; + +use crate::domain::{Counters, CountersPort, ModuleArgs, Singleton, Value}; +use chrono::Duration; + +pub struct CounterAction { + counters: Singleton>, + counter_name: String, + counter_key: String, + save_into: Option, + duration: Option, +} + +impl CounterAction { + pub fn from_args( + mut args: ModuleArgs, + counters: Singleton>, + action_name: &str, + duration_name: &str, + ) -> CounterAction { + let counter_name = remove_acceptable_key(&mut args, "counter").expect(&format!( + "The {} action needs a counter name in “counter”", + action_name + )); + let counter_key = remove_acceptable_key(&mut args, "for").expect(&format!( + "The {} action needs a counter key in “for”", + action_name + )); + let save_into = remove_acceptable_key(&mut args, "save"); + let duration = match args.remove(duration_name) { + None => None, + Some(Value::Int(i)) => Some(Duration::seconds(i as i64)), + _ => panic!(format!( + "The {} only accepts a number of seconds in “{}”", + action_name, duration_name + )), + }; + CounterAction { + counters, + counter_name, + counter_key, + save_into, + duration, + } + } +} + +fn remove_acceptable_key(args: &mut ModuleArgs, key: &str) -> Option { + match args.remove(key) { + None => None, + Some(Value::Str(s)) => Some(s), + Some(Value::Int(i)) => Some(format!("{}", i)), + Some(Value::Date(d)) => Some(format!("{}", d.timestamp())), + _ => None, + } +} diff --git a/src/domain/mod.rs b/src/domain/mod.rs index 492da58..b532e13 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -9,21 +9,59 @@ mod module; pub use self::module::*; mod workflow; pub use self::workflow::*; +mod counter; +pub use self::counter::*; -use chrono::DateTime; +use chrono::{DateTime, Utc}; use std::collections::HashMap; +use std::hash::{Hash, Hasher}; -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum Value { Bool(bool), Str(String), Int(isize), - Date(DateTime), + Date(DateTime), Map(HashMap), List(Vec), } +impl Hash for Value { + fn hash(&self, state: &mut H) { + match self { + Value::Bool(b) => b.hash(state), + Value::Str(s) => s.hash(state), + Value::Int(i) => i.hash(state), + Value::Date(d) => d.hash(state), + Value::Map(h) => h.keys().collect::>().sort().hash(state), + Value::List(v) => v.hash(state), + }; + } +} + pub type Record = HashMap; +pub type Singleton = std::rc::Rc>; + +#[macro_export] +macro_rules! singleton_new { + ( $s:expr ) => { + std::rc::Rc::new(std::cell::RefCell::new($s)) + }; +} + +#[macro_export] +macro_rules! singleton_share { + ( $s:expr ) => { + $s.clone() + }; +} + +#[macro_export] +macro_rules! singleton_borrow { + ( $s:expr ) => { + (*$s).borrow_mut() + }; +} #[cfg(test)] mod test_util; diff --git a/src/domain/module.rs b/src/domain/module.rs index c2c0ad4..2218fa9 100644 --- a/src/domain/module.rs +++ b/src/domain/module.rs @@ -66,7 +66,7 @@ pub type ModuleArgs = HashMap; #[cfg(test)] mod tests { use super::{Module, Modules, Record, Value}; - use crate::domain::test_util::*; + use crate::domain::test_util::{FakeAction, FakeFilter, ACT_NAME, FLT_NAME}; use std::collections::HashMap; #[test] @@ -80,9 +80,9 @@ mod tests { let mut module = Module::new(ACT_NAME.to_string(), HashMap::new(), &mods).unwrap(); // Then - assert!(module.run(&mut record) == Ok(true)); + assert_eq!(module.run(&mut record), Ok(true)); assert!(record.contains_key(ACT_NAME)); - assert!(record[ACT_NAME] == Value::Int(1)); + assert_eq!(record[ACT_NAME], Value::Int(1)); } #[test] @@ -96,8 +96,8 @@ mod tests { let mut module = Module::new(FLT_NAME.to_string(), HashMap::new(), &mods).unwrap(); // Then - assert!(module.run(&mut record) == Ok(false)); + assert_eq!(module.run(&mut record), Ok(false)); assert!(record.contains_key(FLT_NAME)); - assert!(record[FLT_NAME] == Value::Int(1)); + assert_eq!(record[FLT_NAME], Value::Int(1)); } } diff --git a/src/domain/test_util.rs b/src/domain/test_util.rs index f2c71b2..b8566c8 100644 --- a/src/domain/test_util.rs +++ b/src/domain/test_util.rs @@ -1,4 +1,5 @@ -use crate::domain::{Action, Filter, LogMessage, LogPort, Record, Value}; +use crate::domain::{Action, CounterData, CounterRef, CountersPort, Filter, LogMessage, LogPort, Record, Singleton, Value}; +use std::collections::HashMap; pub const ACT_NAME: &str = "fake_action"; pub const FLT_NAME: &str = "fake_filter"; @@ -66,3 +67,28 @@ impl LogPort for FakeLog { Ok(()) } } + +pub struct FakeCountersAdapter { + pub counters: Singleton>, +} +impl CountersPort for FakeCountersAdapter { + fn modify( + &mut self, + entry: CounterRef, + data: CounterData, + mut f: impl FnMut(&mut CounterData, CounterData) -> usize, + ) -> usize { + let k = (entry.0.to_string(), entry.1.clone()); + if !singleton_borrow!(self.counters).contains_key(&k) { + singleton_borrow!(self.counters).insert(k.clone(), (0, None)); + } + f(singleton_borrow!(self.counters).get_mut(&k).unwrap(), data) + } + fn remove(&mut self, entry: CounterRef) -> Option { + let k = (entry.0.to_string(), entry.1.clone()); + singleton_borrow!(self.counters).remove(&k) + } + fn remove_if(&mut self, predicate: impl Fn(&CounterData) -> bool) { + singleton_borrow!(self.counters).retain(|_, v| !predicate(v)); + } +} diff --git a/src/domain/workflow.rs b/src/domain/workflow.rs index 0aa9603..cd5ba21 100644 --- a/src/domain/workflow.rs +++ b/src/domain/workflow.rs @@ -163,7 +163,7 @@ fn node_wants_chain( #[cfg(test)] mod tests { - use crate::domain::test_util::*; + use crate::domain::test_util::{FakeAction, FakeFilter, ACT_NAME, FLT_NAME}; use crate::domain::{Chain, Config, Modules, Record, Step, Value, Workflow}; use indexmap::IndexMap; use std::collections::HashMap; @@ -208,12 +208,12 @@ mod tests { wf.run(&mut record); // Then - assert!(wf.nodes.len() == 1); - assert!(wf.nodes[0].name == "chain1[0]:fake_action"); + assert_eq!(wf.nodes.len(), 1); + assert_eq!(wf.nodes[0].name, "chain1[0]:fake_action"); assert!(wf.nodes[0].then_dest < 0); assert!(wf.nodes[0].else_dest < 0); - assert!(record[ACT_NAME] == Value::Int(1)); - assert!(record.get(FLT_NAME) == None); + assert_eq!(record[ACT_NAME], Value::Int(1)); + assert_eq!(record.get(FLT_NAME), None); } #[test] @@ -252,15 +252,15 @@ mod tests { wf.run(&mut record); // Then - assert!(wf.nodes.len() == 2); - assert!(wf.nodes[0].name == "chain1[0]:fake_filter"); - assert!(wf.nodes[1].name == "chain2[0]:fake_action"); + assert_eq!(wf.nodes.len(), 2); + assert_eq!(wf.nodes[0].name, "chain1[0]:fake_filter"); + assert_eq!(wf.nodes[1].name, "chain2[0]:fake_action"); assert!(wf.nodes[0].then_dest < 0); - assert!(wf.nodes[0].else_dest == 1); + assert_eq!(wf.nodes[0].else_dest, 1); assert!(wf.nodes[1].then_dest < 0); assert!(wf.nodes[1].else_dest < 0); - assert!(record[ACT_NAME] == Value::Int(1)); - assert!(record[FLT_NAME] == Value::Int(1)); + assert_eq!(record[ACT_NAME], Value::Int(1)); + assert_eq!(record[FLT_NAME], Value::Int(1)); } #[test] @@ -299,14 +299,14 @@ mod tests { wf.run(&mut record); // Then - assert!(wf.nodes.len() == 2); - assert!(wf.nodes[0].name == "chain1[0]:fake_filter"); - assert!(wf.nodes[1].name == "chain2[0]:fake_action"); + assert_eq!(wf.nodes.len(), 2); + assert_eq!(wf.nodes[0].name, "chain1[0]:fake_filter"); + assert_eq!(wf.nodes[1].name, "chain2[0]:fake_action"); assert!(wf.nodes[0].then_dest < 0); - assert!(wf.nodes[0].else_dest == 1); + assert_eq!(wf.nodes[0].else_dest, 1); assert!(wf.nodes[1].then_dest < 0); assert!(wf.nodes[1].else_dest < 0); - assert!(record[ACT_NAME] == Value::Int(1)); - assert!(record[FLT_NAME] == Value::Int(1)); + assert_eq!(record[ACT_NAME], Value::Int(1)); + assert_eq!(record[FLT_NAME], Value::Int(1)); } } diff --git a/src/infra/config/mod.rs b/src/infra/config/mod.rs index 240d416..8a7ce69 100644 --- a/src/infra/config/mod.rs +++ b/src/infra/config/mod.rs @@ -324,14 +324,20 @@ mod tests { let conf = SerdeConfigAdapter::from_json(json).config; // Then - assert!(conf.actions.len() == 2); - assert!(conf.actions.get_index(0).unwrap().0 == "Detect request errors with Nextcloud"); - assert!( - conf.actions.get_index(1).unwrap().0 - == "… Report insufficient buffer-size for Nextcloud QUERY_STRING" + assert_eq!(conf.actions.len(), 2); + assert_eq!( + conf.actions.get_index(0).unwrap().0, + "Detect request errors with Nextcloud" ); - assert!(conf.actions.get_index(0).unwrap().1.len() == 3); - assert!(conf.actions.get_index(0).unwrap().1[1].module == String::from("filter_pcre")); - assert!(conf.options.get("debug") == Some(&Value::Bool(false))); + assert_eq!( + conf.actions.get_index(1).unwrap().0, + "… Report insufficient buffer-size for Nextcloud QUERY_STRING" + ); + assert_eq!(conf.actions.get_index(0).unwrap().1.len(), 3); + assert_eq!( + conf.actions.get_index(0).unwrap().1[1].module, + String::from("filter_pcre") + ); + assert_eq!(conf.options.get("debug"), Some(&Value::Bool(false))); } } diff --git a/src/infra/log.rs b/src/infra/log.rs index 1a4bb21..c06e9cf 100644 --- a/src/infra/log.rs +++ b/src/infra/log.rs @@ -1,5 +1,5 @@ use crate::domain::{LogMessage, LogPort, Record, Value}; -use chrono::DateTime; +use chrono::{DateTime, Utc}; use std::collections::HashMap; use std::iter::FromIterator; use systemd::journal::{print, Journal, OpenOptions}; @@ -13,7 +13,7 @@ const INT_MAPPER: JournalFieldMapper = |s| { .unwrap_or(Value::Str(s)) }; const DATE_MAPPER: JournalFieldMapper = |s| { - s.parse::>() + s.parse::>() .map(|d| Value::Date(d)) .unwrap_or(Value::Str(s)) }; diff --git a/src/infra/mod.rs b/src/infra/mod.rs index 94d6b08..d647db7 100644 --- a/src/infra/mod.rs +++ b/src/infra/mod.rs @@ -1,2 +1,3 @@ pub mod config; +pub mod counter; pub mod log; diff --git a/src/main.rs b/src/main.rs index 26e059b..a0d99ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,29 +1,50 @@ +#[macro_use] mod domain; mod infra; mod service; -use crate::domain::action::Log; -use crate::domain::action::Noop; +use crate::domain::action::{CounterRaise, CounterReset, Log, Noop}; use crate::domain::filter::Equals; -use crate::domain::{ConfigPort, Modules, Workflow}; +use crate::domain::{ConfigPort, Counters, Modules, Workflow}; use crate::infra::config::ConfFile; +use crate::infra::counter::InMemoryCounterAdapter; use crate::infra::log::SystemdLogAdapter; -use std::cell::RefCell; -use std::rc::Rc; + +type CountersImpl = InMemoryCounterAdapter; +type LogImpl = SystemdLogAdapter; fn main() { let mut conf = ConfFile::from_filesystem().to_config(); - let log = Rc::new(RefCell::new( - SystemdLogAdapter::open().expect("Error initializing systemd"), - )); + let log = singleton_new!(LogImpl::open().expect("Error initializing systemd")); let mut modules = Modules::new(); + let counters = singleton_new!(Counters::::new(CountersImpl::new())); + let gets_moved_into_closure = singleton_share!(counters); modules.register_action( - "action_noop".to_string(), - Box::new(move |a| Box::new(Noop::from_args(a))), + "action_counterRaise".to_string(), + Box::new(move |a| { + Box::new(CounterRaise::::from_args( + a, + singleton_share!(gets_moved_into_closure), // clone for each call of the constructor + )) + }), + ); + let gets_moved_into_closure = singleton_share!(counters); + modules.register_action( + "action_counterReset".to_string(), + Box::new(move |a| { + Box::new(CounterReset::::from_args( + a, + singleton_share!(gets_moved_into_closure), // clone for each call of the constructor + )) + }), ); modules.register_action( "action_log".to_string(), - Box::new(move |a| Box::new(Log::from_args(a, log.clone()))), + Box::new(move |a| Box::new(Log::from_args(a, singleton_share!(log)))), + ); + modules.register_action( + "action_noop".to_string(), + Box::new(move |a| Box::new(Noop::from_args(a))), ); modules.register_filter( "filter_equals".to_string(),