diff --git a/Cargo.toml b/Cargo.toml index 0bc2b38..bc7aeb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,8 @@ edition = "2018" [dependencies] chrono = "0.4" indexmap = { version = "1.3", features = ["serde-1"] } -inventory = "0.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.8" systemd = "0.8" +regex = "1" diff --git a/src/domain/action/log.rs b/src/domain/action/log.rs new file mode 100644 index 0000000..98eaf58 --- /dev/null +++ b/src/domain/action/log.rs @@ -0,0 +1,195 @@ +use crate::domain::{Action, LogMessage, LogPort, ModuleArgs, Record, Value}; +use regex::Regex; +use std::cell::RefCell; +use std::ops::{Index, Range}; +use std::rc::Rc; + +type LogFormat = fn(&str) -> LogMessage; + +const EMERG_LOG_FORMAT: LogFormat = |s| LogMessage::EMERG(&s); +const ALERT_LOG_FORMAT: LogFormat = |s| LogMessage::ALERT(&s); +const CRIT_LOG_FORMAT: LogFormat = |s| LogMessage::CRIT(&s); +const ERR_LOG_FORMAT: LogFormat = |s| LogMessage::ERR(&s); +const WARNING_LOG_FORMAT: LogFormat = |s| LogMessage::WARNING(&s); +const NOTICE_LOG_FORMAT: LogFormat = |s| LogMessage::NOTICE(&s); +const INFO_LOG_FORMAT: LogFormat = |s| LogMessage::INFO(&s); +const DEBUG_LOG_FORMAT: LogFormat = |s| LogMessage::DEBUG(&s); + +pub struct Log { + logger: Rc>, + log_format: LogFormat, + template: String, + var_locations: Vec>, +} + +impl Log { + pub fn from_args(mut args: ModuleArgs, logger: Rc>) -> Log { + let log_format = match args.remove("level") { + Some(Value::Str(l)) => match l.as_ref() { + "EMERG" => EMERG_LOG_FORMAT, + "ALERT" => ALERT_LOG_FORMAT, + "CRIT" => CRIT_LOG_FORMAT, + "ERR" => ERR_LOG_FORMAT, + "WARNING" => WARNING_LOG_FORMAT, + "NOTICE" => NOTICE_LOG_FORMAT, + "INFO" => INFO_LOG_FORMAT, + "DEBUG" => DEBUG_LOG_FORMAT, + _ => { + eprintln!("Unknown error level: {}; Using INFO", l); + INFO_LOG_FORMAT + } + }, + _ => INFO_LOG_FORMAT, + }; + let template = match args.remove("message") { + Some(Value::Str(s)) => s, + _ => panic!("The Log action needs a message template in “message”"), + }; + let var_locations = Regex::new(r"\{\w+\}") + .unwrap() + .find_iter(&template) + .map(|m| m.range()) + .collect(); + Log { + logger, + log_format, + template, + var_locations, + } + } + + fn message_with_variables_from_record(&self, record: &mut Record) -> String { + let tpl = &self.template; + if self.var_locations.len() == 0 { + return tpl.clone(); + } + let mut message = String::with_capacity(tpl.len() * 2); + let mut last_index = 0; + for Range { start, end } in self.var_locations.iter() { + message.push_str(tpl.index(last_index..*start)); + if let Some(Value::Str(s)) = record.get(tpl.index((start + 1)..(end - 1))) { + message.push_str(&s); + } else { + message.push_str(tpl.index(*start..*end)); + } + last_index = *end; + } + message.push_str(tpl.index(last_index..tpl.len())); + message + } +} + +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)) + } +} + +#[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 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()); + } + + #[test] + fn default_log_level_is_info() { + let (mut log, logger, mut record) = create_log_logger_record("x", None, Vec::new(), Vec::new()); + log.act(&mut record).unwrap(); + assert_log_match!(logger, Some((ref l, _)), l == "INFO"); + } + + #[test] + fn test_levels_are_recognized() { + let mut levels = vec![ + "EMERG", "ALERT", "CRIT", "ERR", "WARNING", "NOTICE", "INFO", "DEBUG", + ]; + levels.iter_mut().for_each(|level| { + let (mut log, logger, mut record) = + create_log_logger_record("x", Some(level), Vec::new(), Vec::new()); + log.act(&mut record).unwrap(); + assert_log_match!(logger, Some((ref l, _)), l == level); + }); + } + + #[test] + fn template_without_placeholder_is_rendered_as_is() { + let (mut log, logger, mut record) = create_log_logger_record("x", None, Vec::new(), Vec::new()); + log.act(&mut record).unwrap(); + assert_log_match!(logger, Some((_, ref m)), m == "x"); + } + + #[test] + fn placeholder_without_variable_is_rendered_as_is() { + let (mut log, logger, mut record) = + create_log_logger_record("x{y}z", None, Vec::new(), Vec::new()); + log.act(&mut record).unwrap(); + assert_log_match!(logger, Some((_, ref m)), m == "x{y}z"); + } + + #[test] + fn placeholder_with_variable_is_replaced() { + let (mut log, logger, mut record) = + create_log_logger_record("x{y}z", None, Vec::new(), vec![("y", "y")]); + log.act(&mut record).unwrap(); + assert_log_match!(logger, Some((_, ref m)), m == "xyz"); + } + + #[test] + fn all_variables_are_replaced() { + let (mut log, logger, mut record) = create_log_logger_record( + "{x}{a}{yy}-{zzz}", + None, + Vec::new(), + vec![("x", "x"), ("yy", "y"), ("zzz", "z")], + ); + log.act(&mut record).unwrap(); + assert_log_match!(logger, Some((_, ref m)), m == "x{a}y-z"); + } + + fn create_log_logger_record( + template: &str, + level: Option<&str>, + logs: Vec>, + vars: Vec<(&str, &str)>, + ) -> (Log, Rc>, 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 mut record: Record = HashMap::new(); + for (name, value) in vars { + record.insert(name.to_string(), Value::Str(value.to_string())); + } + (log, logger, record) + } + + #[macro_export] + macro_rules! assert_log_match { + ( $l:ident , $x:pat , $y:expr ) => { + let logger_contents = (*$l).borrow_mut(); + if let $x = logger_contents.last_write { + assert!($y); + } else { + panic!("There should be a log entry in this test."); + } + }; + } +} diff --git a/src/domain/action/mod.rs b/src/domain/action/mod.rs index aac0a12..c437f6c 100644 --- a/src/domain/action/mod.rs +++ b/src/domain/action/mod.rs @@ -1,2 +1,4 @@ +mod log; +pub use self::log::*; mod noop; pub use self::noop::*; diff --git a/src/domain/action/noop.rs b/src/domain/action/noop.rs index 9c17ffa..b5e53cc 100644 --- a/src/domain/action/noop.rs +++ b/src/domain/action/noop.rs @@ -1,6 +1,5 @@ use crate::domain::{Action, ModuleArgs, Record}; -#[derive(Debug)] pub struct Noop {} impl Noop { @@ -10,7 +9,7 @@ impl Noop { } impl Action for Noop { - fn act(&self, _record: &mut Record) -> Result<(), ()> { + fn act(&mut self, _record: &mut Record) -> Result<(), ()> { Ok(()) } } @@ -31,7 +30,7 @@ mod tests { fn noop_does_nothing() { // Given let (args, mut record) = generate_empty_args_record(); - let action = Noop::from_args(args); + let mut action = Noop::from_args(args); // Then assert_eq!((), action.act(&mut record).unwrap()); diff --git a/src/domain/config.rs b/src/domain/config.rs index 562b4e6..43964a5 100644 --- a/src/domain/config.rs +++ b/src/domain/config.rs @@ -3,7 +3,7 @@ use indexmap::IndexMap; use std::collections::HashMap; pub trait ConfigPort { - fn get(&self) -> &Config; + fn get(&mut self) -> &mut Config; } pub struct Config { diff --git a/src/domain/filter/equals.rs b/src/domain/filter/equals.rs index 69a0169..0146482 100644 --- a/src/domain/filter/equals.rs +++ b/src/domain/filter/equals.rs @@ -1,6 +1,5 @@ use crate::domain::{Filter, ModuleArgs, Record, Value}; -#[derive(Debug)] pub struct Equals { field: String, value: Value, @@ -21,7 +20,7 @@ impl Equals { } impl Filter for Equals { - fn filter(&self, record: &mut Record) -> bool { + fn filter(&mut self, record: &mut Record) -> bool { match (record.get(&self.field), &self.value) { (Some(ref v1), ref v2) => v1 == v2, (None, _) => false, @@ -36,6 +35,124 @@ mod tests { use chrono::Utc; use std::collections::HashMap; + #[test] + fn filter_equals_returns_true_for_identical_bools() { + // Given + let (args, mut record) = + generate_args_record_equal(String::from("a_boolean"), Value::Bool(false)); + let mut filter = Equals::from_args(args); + + // Then + assert!(filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_true_for_identical_strings() { + // Given + let (args, mut record) = + generate_args_record_equal(String::from("a_string"), Value::Str(String::from("Hello!"))); + let mut filter = Equals::from_args(args); + + // Then + assert!(filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_true_for_identical_ints() { + // Given + let (args, mut record) = generate_args_record_equal(String::from("an_integer"), Value::Int(2)); + let mut filter = Equals::from_args(args); + + // Then + assert!(filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_true_for_identical_dates() { + // Given + let (args, mut record) = + generate_args_record_equal(String::from("a_date"), Value::Date(Utc::now())); + let mut filter = Equals::from_args(args); + + // Then + assert!(filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_false_for_different_bools() { + // Given + let (args, mut record) = generate_args_record_custom( + String::from("a_boolean"), + Value::Bool(true), + String::from("a_boolean"), + Value::Bool(false), + ); + let mut filter = Equals::from_args(args); + + // Then + assert!(!filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_false_for_different_strings() { + // Given + let (args, mut record) = generate_args_record_custom( + String::from("a_string"), + Value::Str(String::from("Hello!")), + String::from("a_string"), + Value::Str(String::from("World!")), + ); + let mut filter = Equals::from_args(args); + + // Then + assert!(!filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_false_for_different_ints() { + // Given + let (args, mut record) = generate_args_record_custom( + String::from("an_integer"), + Value::Int(2), + String::from("an_integer"), + Value::Int(3), + ); + let mut filter = Equals::from_args(args); + + // Then + assert!(!filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_false_for_different_dates() { + // Given + let (args, mut record) = generate_args_record_custom( + String::from("a_date"), + Value::Date(Utc::now()), + String::from("a_date"), + Value::Date(Utc::now()), + ); + let mut filter = Equals::from_args(args); + + // Then + assert!(!filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_false_for_non_matching_keys() { + // Given + let (args, mut record) = generate_args_record_custom( + String::from("first_one"), + Value::Int(1), + String::from("second_one"), + Value::Int(1), + ); + let mut filter = Equals::from_args(args); + + // Then + assert!(!filter.filter(&mut record)); + } + fn generate_args_record_equal(name: String, value: Value) -> (ModuleArgs, Record) { let mut args = HashMap::with_capacity(2); args.insert(String::from("field"), Value::Str(name.clone())); @@ -58,122 +175,4 @@ mod tests { record.insert(test_name, test_value); (args, record) } - - #[test] - fn filter_equals_returns_true_for_identical_bools() { - // Given - let (args, mut record) = - generate_args_record_equal(String::from("a_boolean"), Value::Bool(false)); - let filter = Equals::from_args(args); - - // Then - assert!(filter.filter(&mut record)); - } - - #[test] - fn filter_equals_returns_true_for_identical_strings() { - // Given - let (args, mut record) = - generate_args_record_equal(String::from("a_string"), Value::Str(String::from("Hello!"))); - let filter = Equals::from_args(args); - - // Then - assert!(filter.filter(&mut record)); - } - - #[test] - fn filter_equals_returns_true_for_identical_ints() { - // Given - let (args, mut record) = generate_args_record_equal(String::from("an_integer"), Value::Int(2)); - let filter = Equals::from_args(args); - - // Then - assert!(filter.filter(&mut record)); - } - - #[test] - fn filter_equals_returns_true_for_identical_dates() { - // Given - let (args, mut record) = - generate_args_record_equal(String::from("a_date"), Value::Date(Utc::now())); - let filter = Equals::from_args(args); - - // Then - assert!(filter.filter(&mut record)); - } - - #[test] - fn filter_equals_returns_false_for_different_bools() { - // Given - let (args, mut record) = generate_args_record_custom( - String::from("a_boolean"), - Value::Bool(true), - String::from("a_boolean"), - Value::Bool(false), - ); - let filter = Equals::from_args(args); - - // Then - assert!(!filter.filter(&mut record)); - } - - #[test] - fn filter_equals_returns_false_for_different_strings() { - // Given - let (args, mut record) = generate_args_record_custom( - String::from("a_string"), - Value::Str(String::from("Hello!")), - String::from("a_string"), - Value::Str(String::from("World!")), - ); - let filter = Equals::from_args(args); - - // Then - assert!(!filter.filter(&mut record)); - } - - #[test] - fn filter_equals_returns_false_for_different_ints() { - // Given - let (args, mut record) = generate_args_record_custom( - String::from("an_integer"), - Value::Int(2), - String::from("an_integer"), - Value::Int(3), - ); - let filter = Equals::from_args(args); - - // Then - assert!(!filter.filter(&mut record)); - } - - #[test] - fn filter_equals_returns_false_for_different_dates() { - // Given - let (args, mut record) = generate_args_record_custom( - String::from("a_date"), - Value::Date(Utc::now()), - String::from("a_date"), - Value::Date(Utc::now()), - ); - let filter = Equals::from_args(args); - - // Then - assert!(!filter.filter(&mut record)); - } - - #[test] - fn filter_equals_returns_false_for_non_matching_keys() { - // Given - let (args, mut record) = generate_args_record_custom( - String::from("first_one"), - Value::Int(1), - String::from("second_one"), - Value::Int(1), - ); - let filter = Equals::from_args(args); - - // Then - assert!(!filter.filter(&mut record)); - } } diff --git a/src/domain/log.rs b/src/domain/log.rs index 18f8744..bd9a1e4 100644 --- a/src/domain/log.rs +++ b/src/domain/log.rs @@ -1,6 +1,5 @@ use crate::domain::Record; -#[derive(Debug)] pub enum LogMessage<'t> { EMERG(&'t str), ALERT(&'t str), @@ -12,8 +11,7 @@ pub enum LogMessage<'t> { DEBUG(&'t str), } -pub trait LogPort: Sized { - fn open() -> Result; +pub trait LogPort { fn read_next(&mut self) -> Result; fn write(&mut self, message: LogMessage) -> Result<(), ()>; } diff --git a/src/domain/mod.rs b/src/domain/mod.rs index ce85054..492da58 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -1,12 +1,6 @@ -use chrono::DateTime; -use std::collections::HashMap; - pub mod action; pub mod filter; -#[cfg(test)] -mod test_util; - mod config; pub use self::config::*; mod log; @@ -16,7 +10,10 @@ pub use self::module::*; mod workflow; pub use self::workflow::*; -#[derive(Clone, Debug, Eq, PartialEq)] +use chrono::DateTime; +use std::collections::HashMap; + +#[derive(Clone, Eq, PartialEq)] pub enum Value { Bool(bool), Str(String), @@ -27,3 +24,6 @@ pub enum Value { } pub type Record = HashMap; + +#[cfg(test)] +mod test_util; diff --git a/src/domain/module.rs b/src/domain/module.rs index 198a10c..c2c0ad4 100644 --- a/src/domain/module.rs +++ b/src/domain/module.rs @@ -1,52 +1,48 @@ use crate::domain::{Record, Value}; use std::collections::HashMap; -use std::fmt::Debug; -pub struct AvailableAction { - pub name: String, - pub cons: fn(ModuleArgs) -> Box, +pub type ActionConstructor = Box Box>; +pub type FilterConstructor = Box Box>; + +pub struct Modules { + available_actions: HashMap, + available_filters: HashMap, } -impl AvailableAction { - pub fn new(name: String, cons: fn(ModuleArgs) -> Box) -> Self { - AvailableAction { name, cons } +impl Modules { + pub fn new() -> Modules { + Modules { + available_actions: HashMap::new(), + available_filters: HashMap::new(), + } + } + + pub fn register_action(&mut self, name: String, cons: ActionConstructor) { + self.available_actions.insert(name, cons); + } + + pub fn register_filter(&mut self, name: String, cons: FilterConstructor) { + self.available_filters.insert(name, cons); } } -pub struct AvailableFilter { - pub name: String, - pub cons: fn(ModuleArgs) -> Box, -} - -impl AvailableFilter { - pub fn new(name: String, cons: fn(ModuleArgs) -> Box) -> Self { - AvailableFilter { name, cons } - } -} - -pub trait ModulesPort { - fn available_actions(&self) -> HashMap<&String, &AvailableAction>; - fn available_filters(&self) -> HashMap<&String, &AvailableFilter>; -} - -#[derive(Debug)] pub enum Module { Action(Box), Filter(Box), } impl Module { - pub fn new(name: String, args: ModuleArgs, available: &dyn ModulesPort) -> Result { - if let Some(a) = available.available_actions().get(&name).map(|m| m.cons) { + pub fn new(name: String, args: ModuleArgs, available: &Modules) -> Result { + if let Some(a) = available.available_actions.get(&name) { Ok(Module::Action(a(args))) - } else if let Some(f) = available.available_filters().get(&name).map(|m| m.cons) { + } else if let Some(f) = available.available_filters.get(&name) { Ok(Module::Filter(f(args))) } else { Err(()) } } - pub fn run(&self, record: &mut Record) -> Result { + pub fn run(&mut self, record: &mut Record) -> Result { match self { Module::Action(a) => match a.act(record) { Ok(()) => Ok(true), @@ -57,34 +53,31 @@ impl Module { } } -pub trait Action: Debug { - fn act(&self, record: &mut Record) -> Result<(), ()>; +pub trait Action { + fn act(&mut self, record: &mut Record) -> Result<(), ()>; } -pub trait Filter: Debug { - fn filter(&self, record: &mut Record) -> bool; +pub trait Filter { + fn filter(&mut self, record: &mut Record) -> bool; } pub type ModuleArgs = HashMap; #[cfg(test)] mod tests { - use super::{AvailableAction, AvailableFilter, Module, Record, Value}; + use super::{Module, Modules, Record, Value}; use crate::domain::test_util::*; use std::collections::HashMap; #[test] fn available_action_can_be_generated_and_run() { // Given - let aa = [AvailableAction { - name: ACT_NAME.to_string(), - cons: |_| Box::new(FakeAction {}), - }]; + let mut mods = Modules::new(); + mods.register_action(ACT_NAME.to_string(), Box::new(|_| Box::new(FakeAction {}))); let mut record: Record = HashMap::new(); - let mods = FakeModulesAdapter::new(&aa, &[]); // When - let module = Module::new(ACT_NAME.to_string(), HashMap::new(), &mods).unwrap(); + let mut module = Module::new(ACT_NAME.to_string(), HashMap::new(), &mods).unwrap(); // Then assert!(module.run(&mut record) == Ok(true)); @@ -95,15 +88,12 @@ mod tests { #[test] fn available_filter_can_be_generated_and_run() { // Given - let af = [AvailableFilter { - name: FLT_NAME.to_string(), - cons: |_| Box::new(FakeFilter {}), - }]; + let mut mods = Modules::new(); + mods.register_filter(FLT_NAME.to_string(), Box::new(|_| Box::new(FakeFilter {}))); let mut record: Record = HashMap::new(); - let mods = FakeModulesAdapter::new(&[], &af); // When - let module = Module::new(FLT_NAME.to_string(), HashMap::new(), &mods).unwrap(); + let mut module = Module::new(FLT_NAME.to_string(), HashMap::new(), &mods).unwrap(); // Then assert!(module.run(&mut record) == Ok(false)); diff --git a/src/domain/test_util.rs b/src/domain/test_util.rs index e2acec9..f2c71b2 100644 --- a/src/domain/test_util.rs +++ b/src/domain/test_util.rs @@ -1,14 +1,12 @@ -use crate::domain::{Action, AvailableAction, AvailableFilter, Filter, ModulesPort, Record, Value}; -use std::collections::HashMap; +use crate::domain::{Action, Filter, LogMessage, LogPort, Record, Value}; pub const ACT_NAME: &str = "fake_action"; pub const FLT_NAME: &str = "fake_filter"; -#[derive(Debug)] pub struct FakeAction {} impl Action for FakeAction { - fn act(&self, record: &mut Record) -> Result<(), ()> { + fn act(&mut self, record: &mut Record) -> Result<(), ()> { let v = record.get(ACT_NAME).unwrap_or(&Value::Int(0)); match v { Value::Int(i) => record.insert(String::from(ACT_NAME), Value::Int(i + 1)), @@ -18,11 +16,10 @@ impl Action for FakeAction { } } -#[derive(Debug)] pub struct FakeFilter {} impl Filter for FakeFilter { - fn filter(&self, record: &mut Record) -> bool { + fn filter(&mut self, record: &mut Record) -> bool { let v = record.get(FLT_NAME).unwrap_or(&Value::Int(0)); match v { Value::Int(i) => record.insert(String::from(FLT_NAME), Value::Int(i + 1)), @@ -32,30 +29,40 @@ impl Filter for FakeFilter { } } -pub struct FakeModulesAdapter<'a> { - a: HashMap<&'a String, &'a AvailableAction>, - f: HashMap<&'a String, &'a AvailableFilter>, +pub struct FakeLog { + pub wanted_next: Vec>, + pub last_write: Option<(String, String)>, } -impl FakeModulesAdapter<'_> { - pub fn new<'a>(act: &'a [AvailableAction], flt: &'a [AvailableFilter]) -> FakeModulesAdapter<'a> { - let a = act - .iter() - .map(|m| (&m.name, m)) - .collect::>(); - let f = flt - .iter() - .map(|m| (&m.name, m)) - .collect::>(); - FakeModulesAdapter { a, f } +impl FakeLog { + pub fn new(wanted_next: Vec>) -> FakeLog { + FakeLog { + wanted_next, + last_write: None, + } } } -impl ModulesPort for FakeModulesAdapter<'_> { - fn available_actions(&self) -> HashMap<&String, &AvailableAction> { - self.a.clone() +impl LogPort for FakeLog { + fn read_next(&mut self) -> Result { + if self.wanted_next.is_empty() { + Err(()) + } else { + self.wanted_next.remove(0) + } } - fn available_filters(&self) -> HashMap<&String, &AvailableFilter> { - self.f.clone() + + fn write(&mut self, message: LogMessage) -> Result<(), ()> { + self.last_write = match message { + LogMessage::EMERG(m) => Some(("EMERG".to_string(), m.to_string())), + LogMessage::ALERT(m) => Some(("ALERT".to_string(), m.to_string())), + LogMessage::CRIT(m) => Some(("CRIT".to_string(), m.to_string())), + LogMessage::ERR(m) => Some(("ERR".to_string(), m.to_string())), + LogMessage::WARNING(m) => Some(("WARNING".to_string(), m.to_string())), + LogMessage::NOTICE(m) => Some(("NOTICE".to_string(), m.to_string())), + LogMessage::INFO(m) => Some(("INFO".to_string(), m.to_string())), + LogMessage::DEBUG(m) => Some(("DEBUG".to_string(), m.to_string())), + }; + Ok(()) } } diff --git a/src/domain/workflow.rs b/src/domain/workflow.rs index 639c09d..0aa9603 100644 --- a/src/domain/workflow.rs +++ b/src/domain/workflow.rs @@ -1,8 +1,7 @@ -use crate::domain::{Config, Module, ModulesPort, Record, Step}; +use crate::domain::{Config, Module, Modules, Record, Step}; use std::collections::HashMap; use std::ops::Add; -#[derive(Debug)] struct Node { name: String, module: Module, @@ -11,7 +10,7 @@ struct Node { } impl Node { - fn run(&self, record: &mut Record) -> isize { + fn run(&mut self, record: &mut Record) -> isize { if let Ok(b) = self.module.run(record) { if b { self.then_dest @@ -29,7 +28,7 @@ pub struct Workflow { } impl Workflow { - pub fn run(&self, record: &mut Record) { + pub fn run(&mut self, record: &mut Record) { let mut i = 0 as isize; while { i = self.nodes[i as usize].run(record); @@ -37,7 +36,7 @@ impl Workflow { } {} } - pub fn build(conf: &mut Config, available: &dyn ModulesPort) -> Workflow { + pub fn build(conf: &mut Config, available: &Modules) -> Workflow { let mut seen: Vec = Vec::new(); let mut nodes: Vec = Vec::new(); let mut dangling: DanglingInfo = HashMap::new(); @@ -83,7 +82,7 @@ fn build_chain( nodes: &mut Vec, seen: &mut Vec, dangling: &mut DanglingInfo, - available: &dyn ModulesPort, + available: &Modules, ) { let mut index = 0; for step in chain { @@ -165,7 +164,7 @@ fn node_wants_chain( #[cfg(test)] mod tests { use crate::domain::test_util::*; - use crate::domain::{AvailableAction, AvailableFilter, Chain, Config, Record, Step, Value, Workflow}; + use crate::domain::{Chain, Config, Modules, Record, Step, Value, Workflow}; use indexmap::IndexMap; use std::collections::HashMap; @@ -177,7 +176,7 @@ mod tests { actions: IndexMap::new(), options: HashMap::new(), }; - let mods = FakeModulesAdapter::new(&[], &[]); + let mods = Modules::new(); // When let _wf = Workflow::build(&mut conf, &mods); @@ -200,15 +199,12 @@ mod tests { actions, options: HashMap::new(), }; - let aa = [AvailableAction { - name: ACT_NAME.to_string(), - cons: |_| Box::new(FakeAction {}), - }]; - let mods = FakeModulesAdapter::new(&aa, &[]); + let mut mods = Modules::new(); + mods.register_action(ACT_NAME.to_string(), Box::new(|_| Box::new(FakeAction {}))); let mut record: Record = HashMap::new(); // When - let wf = Workflow::build(&mut conf, &mods); + let mut wf = Workflow::build(&mut conf, &mods); wf.run(&mut record); // Then @@ -246,19 +242,13 @@ mod tests { actions, options: HashMap::new(), }; - let aa = [AvailableAction { - name: ACT_NAME.to_string(), - cons: |_| Box::new(FakeAction {}), - }]; - let af = [AvailableFilter { - name: FLT_NAME.to_string(), - cons: |_| Box::new(FakeFilter {}), - }]; - let mods = FakeModulesAdapter::new(&aa, &af); + let mut mods = Modules::new(); + mods.register_action(ACT_NAME.to_string(), Box::new(|_| Box::new(FakeAction {}))); + mods.register_filter(FLT_NAME.to_string(), Box::new(|_| Box::new(FakeFilter {}))); let mut record: Record = HashMap::new(); // When - let wf = Workflow::build(&mut conf, &mods); + let mut wf = Workflow::build(&mut conf, &mods); wf.run(&mut record); // Then @@ -299,19 +289,13 @@ mod tests { actions, options: HashMap::new(), }; - let aa = [AvailableAction { - name: ACT_NAME.to_string(), - cons: |_| Box::new(FakeAction {}), - }]; - let af = [AvailableFilter { - name: FLT_NAME.to_string(), - cons: |_| Box::new(FakeFilter {}), - }]; - let mods = FakeModulesAdapter::new(&aa, &af); + let mut mods = Modules::new(); + mods.register_action(ACT_NAME.to_string(), Box::new(|_| Box::new(FakeAction {}))); + mods.register_filter(FLT_NAME.to_string(), Box::new(|_| Box::new(FakeFilter {}))); let mut record: Record = HashMap::new(); // When - let wf = Workflow::build(&mut conf, &mods); + let mut wf = Workflow::build(&mut conf, &mods); wf.run(&mut record); // Then diff --git a/src/infra/action.rs b/src/infra/action.rs deleted file mode 100644 index de3c2fe..0000000 --- a/src/infra/action.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::domain::action::Noop; -use crate::domain::AvailableAction; - -const ACTION_NOOP: &str = "action_noop"; - -inventory::submit! { - AvailableAction::new(ACTION_NOOP.to_string(), move |a| Box::new(Noop::from_args(a))) -} - -#[cfg(test)] -mod tests { - use crate::domain::{ModuleArgs, ModulesPort}; - use crate::infra::module::InventoryModulesAdapter; - use std::collections::HashMap; - - #[test] - fn action_noop_is_available() { - // Given - let args: ModuleArgs = HashMap::new(); - - // When - let aa = (InventoryModulesAdapter {}).available_actions(); - - // Then - assert!(aa.contains_key(&super::ACTION_NOOP.to_string())); - let _can_instantiate = (aa[&super::ACTION_NOOP.to_string()].cons)(args); - } -} diff --git a/src/infra/config/file.rs b/src/infra/config/file.rs index dffd1f3..4d285d9 100644 --- a/src/infra/config/file.rs +++ b/src/infra/config/file.rs @@ -3,7 +3,7 @@ use std::env; use std::ffi::OsString; use std::fs::File; use std::io::BufReader; -use std::path::Path; +use std::path::{Path, PathBuf}; const ENV_VARIABLE: &'static str = "PYRUSE_CONF"; const ETC_PATH: &'static str = "/etc/pyruse"; @@ -48,7 +48,16 @@ fn find_candidates() -> Vec { } } None => { + let cwd = env::current_dir().expect("Error accessing the current working directory"); + let add_file: fn(&PathBuf, &str) -> OsString = |c, f| { + let mut c2 = c.clone(); + c2.push(f); // not a fluent API… + c2.into_os_string() + }; vec![ + ConfFile::Json(add_file(&cwd, "pyruse.yml")), + ConfFile::Yaml(add_file(&cwd, "pyruse.yaml")), + ConfFile::Yaml(add_file(&cwd, "pyruse.yml")), ConfFile::Json(OsString::from(format!("{}/{}", ETC_PATH, "pyruse.json"))), ConfFile::Yaml(OsString::from(format!("{}/{}", ETC_PATH, "pyruse.yaml"))), ConfFile::Yaml(OsString::from(format!("{}/{}", ETC_PATH, "pyruse.yml"))), diff --git a/src/infra/config/mod.rs b/src/infra/config/mod.rs index 9a0747f..240d416 100644 --- a/src/infra/config/mod.rs +++ b/src/infra/config/mod.rs @@ -9,8 +9,9 @@ use std::fmt; use std::io::Read; mod file; +pub use self::file::*; -struct SerdeConfigAdapter { +pub struct SerdeConfigAdapter { config: Config, } @@ -20,7 +21,7 @@ impl SerdeConfigAdapter { serde_json::from_reader(data).expect("Failed to parse configuration"); SerdeConfigAdapter::from_serde(serde_config) } - fn from_yaml(data: impl Read) -> SerdeConfigAdapter { + pub fn from_yaml(data: impl Read) -> SerdeConfigAdapter { let serde_config: SerdeConfig = serde_yaml::from_reader(data).expect("Failed to parse configuration"); SerdeConfigAdapter::from_serde(serde_config) @@ -57,13 +58,13 @@ impl SerdeConfigAdapter { } impl ConfigPort for SerdeConfigAdapter { - fn get(&self) -> &Config { - &self.config + fn get(&mut self) -> &mut Config { + &mut self.config } } -#[derive(Debug, Deserialize)] -pub struct SerdeConfig { +#[derive(Deserialize)] +struct SerdeConfig { actions: IndexMap, #[serde(flatten)] @@ -72,16 +73,16 @@ pub struct SerdeConfig { type SerdeChain = Vec; -#[derive(Debug, Deserialize, Eq, PartialEq)] -pub enum StepType { +#[derive(Deserialize, Eq, PartialEq)] +enum StepType { #[serde(rename(deserialize = "action"))] Action(String), #[serde(rename(deserialize = "filter"))] Filter(String), } -#[derive(Debug, Deserialize)] -pub struct SerdeStep { +#[derive(Deserialize)] +struct SerdeStep { #[serde(flatten)] module: StepType, args: ModuleArgs, diff --git a/src/infra/filter.rs b/src/infra/filter.rs deleted file mode 100644 index 4da2be9..0000000 --- a/src/infra/filter.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::domain::filter::Equals; -use crate::domain::AvailableFilter; - -const FILTER_EQUALS: &str = "filter_equals"; - -inventory::submit! { - AvailableFilter::new(FILTER_EQUALS.to_string(), move |a| Box::new(Equals::from_args(a))) -} - -#[cfg(test)] -mod tests { - use crate::domain::{ModuleArgs, ModulesPort, Value}; - use crate::infra::module::InventoryModulesAdapter; - use std::collections::HashMap; - - #[test] - fn filter_equals_is_available() { - // Given - let mut args: ModuleArgs = HashMap::new(); - args.insert("field".to_string(), Value::Str("a_field".to_string())); - args.insert("value".to_string(), Value::Int(1)); - - // When - let af = (InventoryModulesAdapter {}).available_filters(); - - // Then - assert!(af.contains_key(&super::FILTER_EQUALS.to_string())); - let _can_instantiate = (af[&super::FILTER_EQUALS.to_string()].cons)(args); - } -} diff --git a/src/infra/log.rs b/src/infra/log.rs index aa51840..1a4bb21 100644 --- a/src/infra/log.rs +++ b/src/infra/log.rs @@ -23,8 +23,8 @@ pub struct SystemdLogAdapter { mappers: HashMap, } -impl LogPort for SystemdLogAdapter { - fn open() -> Result { +impl SystemdLogAdapter { + pub fn open() -> Result { let mappers = create_mappers(); if let Ok(mut journal) = OpenOptions::default() .system(true) @@ -38,7 +38,9 @@ impl LogPort for SystemdLogAdapter { } Err(()) } +} +impl LogPort for SystemdLogAdapter { fn read_next(&mut self) -> Result { loop { match self.journal.await_next_entry(None) { diff --git a/src/infra/mod.rs b/src/infra/mod.rs index 7b01b94..94d6b08 100644 --- a/src/infra/mod.rs +++ b/src/infra/mod.rs @@ -1,5 +1,2 @@ -pub mod action; pub mod config; -pub mod filter; pub mod log; -pub mod module; diff --git a/src/infra/module.rs b/src/infra/module.rs deleted file mode 100644 index aa73000..0000000 --- a/src/infra/module.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::domain::{AvailableAction, AvailableFilter, ModulesPort}; -use std::collections::HashMap; - -inventory::collect!(AvailableAction); -inventory::collect!(AvailableFilter); - -pub struct InventoryModulesAdapter {} - -impl ModulesPort for InventoryModulesAdapter { - fn available_actions(&self) -> HashMap<&String, &AvailableAction> { - let mut h: HashMap<&String, &AvailableAction> = HashMap::new(); - for action in inventory::iter:: { - h.insert(&action.name, &action); - } - h - } - - fn available_filters(&self) -> HashMap<&String, &AvailableFilter> { - let mut h: HashMap<&String, &AvailableFilter> = HashMap::new(); - for filter in inventory::iter:: { - h.insert(&filter.name, &filter); - } - h - } -} diff --git a/src/main.rs b/src/main.rs index ffd51aa..26e059b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,34 @@ mod domain; -mod service; mod infra; +mod service; + +use crate::domain::action::Log; +use crate::domain::action::Noop; +use crate::domain::filter::Equals; +use crate::domain::{ConfigPort, Modules, Workflow}; +use crate::infra::config::ConfFile; +use crate::infra::log::SystemdLogAdapter; +use std::cell::RefCell; +use std::rc::Rc; fn main() { + let mut conf = ConfFile::from_filesystem().to_config(); + let log = Rc::new(RefCell::new( + SystemdLogAdapter::open().expect("Error initializing systemd"), + )); + let mut modules = Modules::new(); + modules.register_action( + "action_noop".to_string(), + Box::new(move |a| Box::new(Noop::from_args(a))), + ); + modules.register_action( + "action_log".to_string(), + Box::new(move |a| Box::new(Log::from_args(a, log.clone()))), + ); + modules.register_filter( + "filter_equals".to_string(), + Box::new(move |a| Box::new(Equals::from_args(a))), + ); + let _workflow = Workflow::build(conf.get(), &modules); println!("Hello, world!"); }