big cleanup + simplified dependency inversion + action_log

rust-rewrite
Yves G 2021-02-28 22:18:51 +01:00
parent 5e120a05bb
commit 14b62814dc
19 changed files with 466 additions and 339 deletions

View File

@ -9,8 +9,8 @@ edition = "2018"
[dependencies] [dependencies]
chrono = "0.4" chrono = "0.4"
indexmap = { version = "1.3", features = ["serde-1"] } indexmap = { version = "1.3", features = ["serde-1"] }
inventory = "0.1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde_yaml = "0.8" serde_yaml = "0.8"
systemd = "0.8" systemd = "0.8"
regex = "1"

195
src/domain/action/log.rs Normal file
View File

@ -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<RefCell<dyn LogPort>>,
log_format: LogFormat,
template: String,
var_locations: Vec<Range<usize>>,
}
impl Log {
pub fn from_args(mut args: ModuleArgs, logger: Rc<RefCell<dyn LogPort>>) -> 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<Result<Record, ()>>,
vars: Vec<(&str, &str)>,
) -> (Log, Rc<RefCell<FakeLog>>, 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.");
}
};
}
}

View File

@ -1,2 +1,4 @@
mod log;
pub use self::log::*;
mod noop; mod noop;
pub use self::noop::*; pub use self::noop::*;

View File

@ -1,6 +1,5 @@
use crate::domain::{Action, ModuleArgs, Record}; use crate::domain::{Action, ModuleArgs, Record};
#[derive(Debug)]
pub struct Noop {} pub struct Noop {}
impl Noop { impl Noop {
@ -10,7 +9,7 @@ impl Noop {
} }
impl Action for Noop { impl Action for Noop {
fn act(&self, _record: &mut Record) -> Result<(), ()> { fn act(&mut self, _record: &mut Record) -> Result<(), ()> {
Ok(()) Ok(())
} }
} }
@ -31,7 +30,7 @@ mod tests {
fn noop_does_nothing() { fn noop_does_nothing() {
// Given // Given
let (args, mut record) = generate_empty_args_record(); let (args, mut record) = generate_empty_args_record();
let action = Noop::from_args(args); let mut action = Noop::from_args(args);
// Then // Then
assert_eq!((), action.act(&mut record).unwrap()); assert_eq!((), action.act(&mut record).unwrap());

View File

@ -3,7 +3,7 @@ use indexmap::IndexMap;
use std::collections::HashMap; use std::collections::HashMap;
pub trait ConfigPort { pub trait ConfigPort {
fn get(&self) -> &Config; fn get(&mut self) -> &mut Config;
} }
pub struct Config { pub struct Config {

View File

@ -1,6 +1,5 @@
use crate::domain::{Filter, ModuleArgs, Record, Value}; use crate::domain::{Filter, ModuleArgs, Record, Value};
#[derive(Debug)]
pub struct Equals { pub struct Equals {
field: String, field: String,
value: Value, value: Value,
@ -21,7 +20,7 @@ impl Equals {
} }
impl Filter for 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) { match (record.get(&self.field), &self.value) {
(Some(ref v1), ref v2) => v1 == v2, (Some(ref v1), ref v2) => v1 == v2,
(None, _) => false, (None, _) => false,
@ -36,6 +35,124 @@ mod tests {
use chrono::Utc; use chrono::Utc;
use std::collections::HashMap; 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) { fn generate_args_record_equal(name: String, value: Value) -> (ModuleArgs, Record) {
let mut args = HashMap::with_capacity(2); let mut args = HashMap::with_capacity(2);
args.insert(String::from("field"), Value::Str(name.clone())); args.insert(String::from("field"), Value::Str(name.clone()));
@ -58,122 +175,4 @@ mod tests {
record.insert(test_name, test_value); record.insert(test_name, test_value);
(args, record) (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));
}
} }

View File

@ -1,6 +1,5 @@
use crate::domain::Record; use crate::domain::Record;
#[derive(Debug)]
pub enum LogMessage<'t> { pub enum LogMessage<'t> {
EMERG(&'t str), EMERG(&'t str),
ALERT(&'t str), ALERT(&'t str),
@ -12,8 +11,7 @@ pub enum LogMessage<'t> {
DEBUG(&'t str), DEBUG(&'t str),
} }
pub trait LogPort: Sized { pub trait LogPort {
fn open() -> Result<Self, ()>;
fn read_next(&mut self) -> Result<Record, ()>; fn read_next(&mut self) -> Result<Record, ()>;
fn write(&mut self, message: LogMessage) -> Result<(), ()>; fn write(&mut self, message: LogMessage) -> Result<(), ()>;
} }

View File

@ -1,12 +1,6 @@
use chrono::DateTime;
use std::collections::HashMap;
pub mod action; pub mod action;
pub mod filter; pub mod filter;
#[cfg(test)]
mod test_util;
mod config; mod config;
pub use self::config::*; pub use self::config::*;
mod log; mod log;
@ -16,7 +10,10 @@ pub use self::module::*;
mod workflow; mod workflow;
pub use self::workflow::*; pub use self::workflow::*;
#[derive(Clone, Debug, Eq, PartialEq)] use chrono::DateTime;
use std::collections::HashMap;
#[derive(Clone, Eq, PartialEq)]
pub enum Value { pub enum Value {
Bool(bool), Bool(bool),
Str(String), Str(String),
@ -27,3 +24,6 @@ pub enum Value {
} }
pub type Record = HashMap<String, Value>; pub type Record = HashMap<String, Value>;
#[cfg(test)]
mod test_util;

View File

@ -1,52 +1,48 @@
use crate::domain::{Record, Value}; use crate::domain::{Record, Value};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Debug;
pub struct AvailableAction { pub type ActionConstructor = Box<dyn Fn(ModuleArgs) -> Box<dyn Action>>;
pub name: String, pub type FilterConstructor = Box<dyn Fn(ModuleArgs) -> Box<dyn Filter>>;
pub cons: fn(ModuleArgs) -> Box<dyn Action>,
pub struct Modules {
available_actions: HashMap<String, ActionConstructor>,
available_filters: HashMap<String, FilterConstructor>,
} }
impl AvailableAction { impl Modules {
pub fn new(name: String, cons: fn(ModuleArgs) -> Box<dyn Action>) -> Self { pub fn new() -> Modules {
AvailableAction { name, cons } 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<dyn Filter>,
}
impl AvailableFilter {
pub fn new(name: String, cons: fn(ModuleArgs) -> Box<dyn Filter>) -> 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 { pub enum Module {
Action(Box<dyn Action>), Action(Box<dyn Action>),
Filter(Box<dyn Filter>), Filter(Box<dyn Filter>),
} }
impl Module { impl Module {
pub fn new(name: String, args: ModuleArgs, available: &dyn ModulesPort) -> Result<Module, ()> { pub fn new(name: String, args: ModuleArgs, available: &Modules) -> Result<Module, ()> {
if let Some(a) = available.available_actions().get(&name).map(|m| m.cons) { if let Some(a) = available.available_actions.get(&name) {
Ok(Module::Action(a(args))) 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))) Ok(Module::Filter(f(args)))
} else { } else {
Err(()) Err(())
} }
} }
pub fn run(&self, record: &mut Record) -> Result<bool, ()> { pub fn run(&mut self, record: &mut Record) -> Result<bool, ()> {
match self { match self {
Module::Action(a) => match a.act(record) { Module::Action(a) => match a.act(record) {
Ok(()) => Ok(true), Ok(()) => Ok(true),
@ -57,34 +53,31 @@ impl Module {
} }
} }
pub trait Action: Debug { pub trait Action {
fn act(&self, record: &mut Record) -> Result<(), ()>; fn act(&mut self, record: &mut Record) -> Result<(), ()>;
} }
pub trait Filter: Debug { pub trait Filter {
fn filter(&self, record: &mut Record) -> bool; fn filter(&mut self, record: &mut Record) -> bool;
} }
pub type ModuleArgs = HashMap<String, Value>; pub type ModuleArgs = HashMap<String, Value>;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{AvailableAction, AvailableFilter, Module, Record, Value}; use super::{Module, Modules, Record, Value};
use crate::domain::test_util::*; use crate::domain::test_util::*;
use std::collections::HashMap; use std::collections::HashMap;
#[test] #[test]
fn available_action_can_be_generated_and_run() { fn available_action_can_be_generated_and_run() {
// Given // Given
let aa = [AvailableAction { let mut mods = Modules::new();
name: ACT_NAME.to_string(), mods.register_action(ACT_NAME.to_string(), Box::new(|_| Box::new(FakeAction {})));
cons: |_| Box::new(FakeAction {}),
}];
let mut record: Record = HashMap::new(); let mut record: Record = HashMap::new();
let mods = FakeModulesAdapter::new(&aa, &[]);
// When // 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 // Then
assert!(module.run(&mut record) == Ok(true)); assert!(module.run(&mut record) == Ok(true));
@ -95,15 +88,12 @@ mod tests {
#[test] #[test]
fn available_filter_can_be_generated_and_run() { fn available_filter_can_be_generated_and_run() {
// Given // Given
let af = [AvailableFilter { let mut mods = Modules::new();
name: FLT_NAME.to_string(), mods.register_filter(FLT_NAME.to_string(), Box::new(|_| Box::new(FakeFilter {})));
cons: |_| Box::new(FakeFilter {}),
}];
let mut record: Record = HashMap::new(); let mut record: Record = HashMap::new();
let mods = FakeModulesAdapter::new(&[], &af);
// When // 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 // Then
assert!(module.run(&mut record) == Ok(false)); assert!(module.run(&mut record) == Ok(false));

View File

@ -1,14 +1,12 @@
use crate::domain::{Action, AvailableAction, AvailableFilter, Filter, ModulesPort, Record, Value}; use crate::domain::{Action, Filter, LogMessage, LogPort, Record, Value};
use std::collections::HashMap;
pub const ACT_NAME: &str = "fake_action"; pub const ACT_NAME: &str = "fake_action";
pub const FLT_NAME: &str = "fake_filter"; pub const FLT_NAME: &str = "fake_filter";
#[derive(Debug)]
pub struct FakeAction {} pub struct FakeAction {}
impl Action for 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)); let v = record.get(ACT_NAME).unwrap_or(&Value::Int(0));
match v { match v {
Value::Int(i) => record.insert(String::from(ACT_NAME), Value::Int(i + 1)), 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 {} pub struct FakeFilter {}
impl Filter for 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)); let v = record.get(FLT_NAME).unwrap_or(&Value::Int(0));
match v { match v {
Value::Int(i) => record.insert(String::from(FLT_NAME), Value::Int(i + 1)), 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> { pub struct FakeLog {
a: HashMap<&'a String, &'a AvailableAction>, pub wanted_next: Vec<Result<Record, ()>>,
f: HashMap<&'a String, &'a AvailableFilter>, pub last_write: Option<(String, String)>,
} }
impl FakeModulesAdapter<'_> { impl FakeLog {
pub fn new<'a>(act: &'a [AvailableAction], flt: &'a [AvailableFilter]) -> FakeModulesAdapter<'a> { pub fn new(wanted_next: Vec<Result<Record, ()>>) -> FakeLog {
let a = act FakeLog {
.iter() wanted_next,
.map(|m| (&m.name, m)) last_write: None,
.collect::<HashMap<&'a String, &'a AvailableAction>>(); }
let f = flt
.iter()
.map(|m| (&m.name, m))
.collect::<HashMap<&'a String, &'a AvailableFilter>>();
FakeModulesAdapter { a, f }
} }
} }
impl ModulesPort for FakeModulesAdapter<'_> { impl LogPort for FakeLog {
fn available_actions(&self) -> HashMap<&String, &AvailableAction> { fn read_next(&mut self) -> Result<Record, ()> {
self.a.clone() 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(())
} }
} }

View File

@ -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::collections::HashMap;
use std::ops::Add; use std::ops::Add;
#[derive(Debug)]
struct Node { struct Node {
name: String, name: String,
module: Module, module: Module,
@ -11,7 +10,7 @@ struct Node {
} }
impl 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 let Ok(b) = self.module.run(record) {
if b { if b {
self.then_dest self.then_dest
@ -29,7 +28,7 @@ pub struct Workflow {
} }
impl Workflow { impl Workflow {
pub fn run(&self, record: &mut Record) { pub fn run(&mut self, record: &mut Record) {
let mut i = 0 as isize; let mut i = 0 as isize;
while { while {
i = self.nodes[i as usize].run(record); 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<String> = Vec::new(); let mut seen: Vec<String> = Vec::new();
let mut nodes: Vec<Node> = Vec::new(); let mut nodes: Vec<Node> = Vec::new();
let mut dangling: DanglingInfo = HashMap::new(); let mut dangling: DanglingInfo = HashMap::new();
@ -83,7 +82,7 @@ fn build_chain(
nodes: &mut Vec<Node>, nodes: &mut Vec<Node>,
seen: &mut Vec<String>, seen: &mut Vec<String>,
dangling: &mut DanglingInfo, dangling: &mut DanglingInfo,
available: &dyn ModulesPort, available: &Modules,
) { ) {
let mut index = 0; let mut index = 0;
for step in chain { for step in chain {
@ -165,7 +164,7 @@ fn node_wants_chain(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::domain::test_util::*; 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 indexmap::IndexMap;
use std::collections::HashMap; use std::collections::HashMap;
@ -177,7 +176,7 @@ mod tests {
actions: IndexMap::new(), actions: IndexMap::new(),
options: HashMap::new(), options: HashMap::new(),
}; };
let mods = FakeModulesAdapter::new(&[], &[]); let mods = Modules::new();
// When // When
let _wf = Workflow::build(&mut conf, &mods); let _wf = Workflow::build(&mut conf, &mods);
@ -200,15 +199,12 @@ mod tests {
actions, actions,
options: HashMap::new(), options: HashMap::new(),
}; };
let aa = [AvailableAction { let mut mods = Modules::new();
name: ACT_NAME.to_string(), mods.register_action(ACT_NAME.to_string(), Box::new(|_| Box::new(FakeAction {})));
cons: |_| Box::new(FakeAction {}),
}];
let mods = FakeModulesAdapter::new(&aa, &[]);
let mut record: Record = HashMap::new(); let mut record: Record = HashMap::new();
// When // When
let wf = Workflow::build(&mut conf, &mods); let mut wf = Workflow::build(&mut conf, &mods);
wf.run(&mut record); wf.run(&mut record);
// Then // Then
@ -246,19 +242,13 @@ mod tests {
actions, actions,
options: HashMap::new(), options: HashMap::new(),
}; };
let aa = [AvailableAction { let mut mods = Modules::new();
name: ACT_NAME.to_string(), mods.register_action(ACT_NAME.to_string(), Box::new(|_| Box::new(FakeAction {})));
cons: |_| Box::new(FakeAction {}), mods.register_filter(FLT_NAME.to_string(), Box::new(|_| Box::new(FakeFilter {})));
}];
let af = [AvailableFilter {
name: FLT_NAME.to_string(),
cons: |_| Box::new(FakeFilter {}),
}];
let mods = FakeModulesAdapter::new(&aa, &af);
let mut record: Record = HashMap::new(); let mut record: Record = HashMap::new();
// When // When
let wf = Workflow::build(&mut conf, &mods); let mut wf = Workflow::build(&mut conf, &mods);
wf.run(&mut record); wf.run(&mut record);
// Then // Then
@ -299,19 +289,13 @@ mod tests {
actions, actions,
options: HashMap::new(), options: HashMap::new(),
}; };
let aa = [AvailableAction { let mut mods = Modules::new();
name: ACT_NAME.to_string(), mods.register_action(ACT_NAME.to_string(), Box::new(|_| Box::new(FakeAction {})));
cons: |_| Box::new(FakeAction {}), mods.register_filter(FLT_NAME.to_string(), Box::new(|_| Box::new(FakeFilter {})));
}];
let af = [AvailableFilter {
name: FLT_NAME.to_string(),
cons: |_| Box::new(FakeFilter {}),
}];
let mods = FakeModulesAdapter::new(&aa, &af);
let mut record: Record = HashMap::new(); let mut record: Record = HashMap::new();
// When // When
let wf = Workflow::build(&mut conf, &mods); let mut wf = Workflow::build(&mut conf, &mods);
wf.run(&mut record); wf.run(&mut record);
// Then // Then

View File

@ -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);
}
}

View File

@ -3,7 +3,7 @@ use std::env;
use std::ffi::OsString; use std::ffi::OsString;
use std::fs::File; use std::fs::File;
use std::io::BufReader; use std::io::BufReader;
use std::path::Path; use std::path::{Path, PathBuf};
const ENV_VARIABLE: &'static str = "PYRUSE_CONF"; const ENV_VARIABLE: &'static str = "PYRUSE_CONF";
const ETC_PATH: &'static str = "/etc/pyruse"; const ETC_PATH: &'static str = "/etc/pyruse";
@ -48,7 +48,16 @@ fn find_candidates() -> Vec<ConfFile> {
} }
} }
None => { 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![ 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::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.yaml"))),
ConfFile::Yaml(OsString::from(format!("{}/{}", ETC_PATH, "pyruse.yml"))), ConfFile::Yaml(OsString::from(format!("{}/{}", ETC_PATH, "pyruse.yml"))),

View File

@ -9,8 +9,9 @@ use std::fmt;
use std::io::Read; use std::io::Read;
mod file; mod file;
pub use self::file::*;
struct SerdeConfigAdapter { pub struct SerdeConfigAdapter {
config: Config, config: Config,
} }
@ -20,7 +21,7 @@ impl SerdeConfigAdapter {
serde_json::from_reader(data).expect("Failed to parse configuration"); serde_json::from_reader(data).expect("Failed to parse configuration");
SerdeConfigAdapter::from_serde(serde_config) SerdeConfigAdapter::from_serde(serde_config)
} }
fn from_yaml(data: impl Read) -> SerdeConfigAdapter { pub fn from_yaml(data: impl Read) -> SerdeConfigAdapter {
let serde_config: SerdeConfig = let serde_config: SerdeConfig =
serde_yaml::from_reader(data).expect("Failed to parse configuration"); serde_yaml::from_reader(data).expect("Failed to parse configuration");
SerdeConfigAdapter::from_serde(serde_config) SerdeConfigAdapter::from_serde(serde_config)
@ -57,13 +58,13 @@ impl SerdeConfigAdapter {
} }
impl ConfigPort for SerdeConfigAdapter { impl ConfigPort for SerdeConfigAdapter {
fn get(&self) -> &Config { fn get(&mut self) -> &mut Config {
&self.config &mut self.config
} }
} }
#[derive(Debug, Deserialize)] #[derive(Deserialize)]
pub struct SerdeConfig { struct SerdeConfig {
actions: IndexMap<String, SerdeChain>, actions: IndexMap<String, SerdeChain>,
#[serde(flatten)] #[serde(flatten)]
@ -72,16 +73,16 @@ pub struct SerdeConfig {
type SerdeChain = Vec<SerdeStep>; type SerdeChain = Vec<SerdeStep>;
#[derive(Debug, Deserialize, Eq, PartialEq)] #[derive(Deserialize, Eq, PartialEq)]
pub enum StepType { enum StepType {
#[serde(rename(deserialize = "action"))] #[serde(rename(deserialize = "action"))]
Action(String), Action(String),
#[serde(rename(deserialize = "filter"))] #[serde(rename(deserialize = "filter"))]
Filter(String), Filter(String),
} }
#[derive(Debug, Deserialize)] #[derive(Deserialize)]
pub struct SerdeStep { struct SerdeStep {
#[serde(flatten)] #[serde(flatten)]
module: StepType, module: StepType,
args: ModuleArgs, args: ModuleArgs,

View File

@ -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);
}
}

View File

@ -23,8 +23,8 @@ pub struct SystemdLogAdapter {
mappers: HashMap<String, JournalFieldMapper>, mappers: HashMap<String, JournalFieldMapper>,
} }
impl LogPort for SystemdLogAdapter { impl SystemdLogAdapter {
fn open() -> Result<Self, ()> { pub fn open() -> Result<Self, ()> {
let mappers = create_mappers(); let mappers = create_mappers();
if let Ok(mut journal) = OpenOptions::default() if let Ok(mut journal) = OpenOptions::default()
.system(true) .system(true)
@ -38,7 +38,9 @@ impl LogPort for SystemdLogAdapter {
} }
Err(()) Err(())
} }
}
impl LogPort for SystemdLogAdapter {
fn read_next(&mut self) -> Result<Record, ()> { fn read_next(&mut self) -> Result<Record, ()> {
loop { loop {
match self.journal.await_next_entry(None) { match self.journal.await_next_entry(None) {

View File

@ -1,5 +1,2 @@
pub mod action;
pub mod config; pub mod config;
pub mod filter;
pub mod log; pub mod log;
pub mod module;

View File

@ -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::<AvailableAction> {
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::<AvailableFilter> {
h.insert(&filter.name, &filter);
}
h
}
}

View File

@ -1,7 +1,34 @@
mod domain; mod domain;
mod service;
mod infra; 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() { 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!"); println!("Hello, world!");
} }