clean architecture + log, workflow
parent
d441ed3b14
commit
5e120a05bb
|
@ -8,8 +8,9 @@ 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.4"
|
||||
systemd = "0.8"
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
mod noop;
|
||||
pub use self::noop::*;
|
||||
|
||||
/*
|
||||
pub trait Action {
|
||||
fn act(&self, record: &mut Record) -> Result<(), ()>;
|
||||
}
|
||||
|
||||
impl<T: Action> Module for T {
|
||||
fn run(&self, record: &mut Record) -> Result<bool, ()> {
|
||||
match self.act(record) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(()) => Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -1,14 +0,0 @@
|
|||
use chrono::DateTime;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Value {
|
||||
Bool(bool),
|
||||
Str(String),
|
||||
Int(isize),
|
||||
Date(DateTime<chrono::Utc>),
|
||||
Map(HashMap<String, Value>),
|
||||
List(Vec<Value>)
|
||||
}
|
||||
|
||||
pub type Record<'a> = HashMap<&'a str, Value>;
|
|
@ -1,205 +0,0 @@
|
|||
use serde::de::{self,Deserializer,MapAccess,SeqAccess,Visitor};
|
||||
use serde::Deserialize;
|
||||
use serde_json;
|
||||
use serde_yaml;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::io::Read;
|
||||
use crate::common::Value;
|
||||
use crate::modules::ModuleArgs;
|
||||
|
||||
mod file;
|
||||
|
||||
thread_local!(static CONFIG: RefCell<Option<Config>> = RefCell::new(None));
|
||||
|
||||
#[derive(Debug,Deserialize)]
|
||||
pub struct Config {
|
||||
actions: HashMap<String, Chain>,
|
||||
|
||||
#[serde(flatten)]
|
||||
options: HashMap<String, Value>
|
||||
}
|
||||
|
||||
type Chain = Vec<Step>;
|
||||
|
||||
#[derive(Debug,Deserialize)]
|
||||
pub enum StepType {
|
||||
#[serde(rename(deserialize = "action"))]
|
||||
Action(String),
|
||||
#[serde(rename(deserialize = "filter"))]
|
||||
Filter(String)
|
||||
}
|
||||
|
||||
#[derive(Debug,Deserialize)]
|
||||
pub struct Step {
|
||||
#[serde(flatten)]
|
||||
module: StepType,
|
||||
args: ModuleArgs,
|
||||
#[serde(rename(deserialize = "then"))]
|
||||
then_dest: Option<String>,
|
||||
#[serde(rename(deserialize = "else"))]
|
||||
else_dest: Option<String>
|
||||
}
|
||||
|
||||
/* *** serde for Value *** */
|
||||
|
||||
struct ValueVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ValueVisitor {
|
||||
type Value = Value;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a boolean, string, or integer")
|
||||
}
|
||||
|
||||
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Bool(v))
|
||||
}
|
||||
|
||||
fn visit_i8<E>(self, v: i8) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_i16<E>(self, v: i16) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_i128<E>(self, v: i128) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_u128<E>(self, v: u128) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_char<E>(self, v: char) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Str(v.to_string()))
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Str(String::from(v)))
|
||||
}
|
||||
|
||||
fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Str(String::from(v)))
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Str(v))
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Str(std::str::from_utf8(v).expect("Strings in the configuration must be UTF-8").to_string()))
|
||||
}
|
||||
|
||||
fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Str(std::str::from_utf8(v).expect("Strings in the configuration must be UTF-8").to_string()))
|
||||
}
|
||||
|
||||
fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E> where E: de::Error {
|
||||
Ok(Value::Str(String::from_utf8(v).expect("Strings in the configuration must be UTF-8")))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> where A: SeqAccess<'de> {
|
||||
let mut result = Vec::with_capacity(seq.size_hint().unwrap_or(0));
|
||||
while let Some(v) = seq.next_element()? {
|
||||
result.push(v);
|
||||
}
|
||||
Ok(Value::List(result))
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> where A: MapAccess<'de> {
|
||||
let mut result = HashMap::with_capacity(map.size_hint().unwrap_or(0));
|
||||
while let Some((k, v)) = map.next_entry()? {
|
||||
result.insert(k, v);
|
||||
}
|
||||
Ok(Value::Map(result))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Value {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Value, D::Error> where D: Deserializer<'de> {
|
||||
deserializer.deserialize_any(ValueVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_json(data: impl Read) {
|
||||
CONFIG.with(|config| {
|
||||
config.replace(Some(serde_json::from_reader(data).expect("Failed to parse configuration")));
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_yaml(data: impl Read) {
|
||||
CONFIG.with(|config| {
|
||||
config.replace(Some(serde_yaml::from_reader(data).expect("Failed to parse configuration")));
|
||||
});
|
||||
}
|
||||
|
||||
//fn handle_serde(data: se)
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_json;
|
||||
|
||||
#[test]
|
||||
fn parse_json_works() {
|
||||
let json = r#"
|
||||
{
|
||||
"actions": {
|
||||
"Detect request errors with Nextcloud": [
|
||||
{
|
||||
"filter": "filter_equals",
|
||||
"args": { "field": "SYSLOG_IDENTIFIER", "value": "uwsgi" }
|
||||
},
|
||||
{
|
||||
"filter": "filter_pcre",
|
||||
"args": { "field": "MESSAGE", "re": "^\\[[^]]+\\] ([^ ]+) .*\\] ([A-Z]+ /[^?]*)(?:\\?.*)? => .*\\(HTTP/1.1 5..\\)", "save": [ "thatIP", "HTTPrequest" ] },
|
||||
"else": "… Report insufficient buffer-size for Nextcloud QUERY_STRING"
|
||||
},
|
||||
{
|
||||
"action": "action_dailyReport",
|
||||
"args": { "level": "INFO", "message": "IP {thatIP} failed to {HTTPrequest} on Nextcloud", "details": "FIRSTLAST" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"debug": false
|
||||
}
|
||||
"#.as_bytes();
|
||||
parse_json(json);
|
||||
super::CONFIG.with(|config| {
|
||||
println!("{:#?}", config.borrow());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
mod noop;
|
||||
pub use self::noop::*;
|
|
@ -1,15 +1,10 @@
|
|||
use crate::modules::{Action,AvailableAction,ModuleArgs};
|
||||
use crate::common::Record;
|
||||
use crate::domain::{Action, ModuleArgs, Record};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Noop {}
|
||||
|
||||
inventory::submit! {
|
||||
AvailableAction::new("action_noop", move |a| Box::new(Noop::from_args(a)))
|
||||
}
|
||||
|
||||
impl Noop {
|
||||
pub fn from_args(mut _args: ModuleArgs) -> Noop {
|
||||
pub fn from_args(_args: ModuleArgs) -> Noop {
|
||||
Noop {}
|
||||
}
|
||||
}
|
||||
|
@ -22,12 +17,11 @@ impl Action for Noop {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::domain::action::Noop;
|
||||
use crate::domain::{Action, ModuleArgs, Record};
|
||||
use std::collections::HashMap;
|
||||
use crate::common::Record;
|
||||
use crate::actions::Noop;
|
||||
use crate::modules::{Action,ModuleArgs};
|
||||
|
||||
fn generate_empty_args_record() -> (ModuleArgs, Record<'static>) {
|
||||
fn generate_empty_args_record() -> (ModuleArgs, Record) {
|
||||
let args = HashMap::with_capacity(0);
|
||||
let record = HashMap::with_capacity(0);
|
||||
(args, record)
|
||||
|
@ -35,8 +29,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn noop_does_nothing() {
|
||||
// Given
|
||||
let (args, mut record) = generate_empty_args_record();
|
||||
let action = Noop::from_args(args);
|
||||
|
||||
// Then
|
||||
assert_eq!((), action.act(&mut record).unwrap());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
use crate::domain::{ModuleArgs, Value};
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub trait ConfigPort {
|
||||
fn get(&self) -> &Config;
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
pub actions: IndexMap<String, Chain>,
|
||||
pub options: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
pub type Chain = Vec<Step>;
|
||||
|
||||
pub struct Step {
|
||||
pub module: String,
|
||||
pub args: ModuleArgs,
|
||||
pub then_dest: Option<String>,
|
||||
pub else_dest: Option<String>,
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
use crate::domain::{Filter, ModuleArgs, Record, Value};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Equals {
|
||||
field: String,
|
||||
value: Value,
|
||||
}
|
||||
|
||||
impl Equals {
|
||||
pub fn from_args(mut args: ModuleArgs) -> Equals {
|
||||
Equals {
|
||||
field: match args.remove("field") {
|
||||
Some(Value::Str(s)) => s,
|
||||
_ => panic!("The Equals filter needs a field to filter in “field”"),
|
||||
},
|
||||
value: args
|
||||
.remove("value")
|
||||
.expect("The Equals filter needs a reference value in “value”"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Filter for Equals {
|
||||
fn filter(&self, record: &mut Record) -> bool {
|
||||
match (record.get(&self.field), &self.value) {
|
||||
(Some(ref v1), ref v2) => v1 == v2,
|
||||
(None, _) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::domain::filter::Equals;
|
||||
use crate::domain::{Filter, ModuleArgs, Record, Value};
|
||||
use chrono::Utc;
|
||||
use std::collections::HashMap;
|
||||
|
||||
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()));
|
||||
args.insert(String::from("value"), value.clone());
|
||||
let mut record = HashMap::with_capacity(1);
|
||||
record.insert(name, value);
|
||||
(args, record)
|
||||
}
|
||||
|
||||
fn generate_args_record_custom(
|
||||
ref_name: String,
|
||||
ref_value: Value,
|
||||
test_name: String,
|
||||
test_value: Value,
|
||||
) -> (ModuleArgs, Record) {
|
||||
let mut args = HashMap::with_capacity(2);
|
||||
args.insert(String::from("field"), Value::Str(ref_name));
|
||||
args.insert(String::from("value"), ref_value);
|
||||
let mut record = HashMap::with_capacity(1);
|
||||
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));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
mod equals;
|
||||
pub use self::equals::*;
|
|
@ -0,0 +1,19 @@
|
|||
use crate::domain::Record;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LogMessage<'t> {
|
||||
EMERG(&'t str),
|
||||
ALERT(&'t str),
|
||||
CRIT(&'t str),
|
||||
ERR(&'t str),
|
||||
WARNING(&'t str),
|
||||
NOTICE(&'t str),
|
||||
INFO(&'t str),
|
||||
DEBUG(&'t str),
|
||||
}
|
||||
|
||||
pub trait LogPort: Sized {
|
||||
fn open() -> Result<Self, ()>;
|
||||
fn read_next(&mut self) -> Result<Record, ()>;
|
||||
fn write(&mut self, message: LogMessage) -> Result<(), ()>;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
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;
|
||||
pub use self::log::*;
|
||||
mod module;
|
||||
pub use self::module::*;
|
||||
mod workflow;
|
||||
pub use self::workflow::*;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Value {
|
||||
Bool(bool),
|
||||
Str(String),
|
||||
Int(isize),
|
||||
Date(DateTime<chrono::Utc>),
|
||||
Map(HashMap<String, Value>),
|
||||
List(Vec<Value>),
|
||||
}
|
||||
|
||||
pub type Record = HashMap<String, Value>;
|
|
@ -0,0 +1,113 @@
|
|||
use crate::domain::{Record, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub struct AvailableAction {
|
||||
pub name: String,
|
||||
pub cons: fn(ModuleArgs) -> Box<dyn Action>,
|
||||
}
|
||||
|
||||
impl AvailableAction {
|
||||
pub fn new(name: String, cons: fn(ModuleArgs) -> Box<dyn Action>) -> Self {
|
||||
AvailableAction { 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 {
|
||||
Action(Box<dyn Action>),
|
||||
Filter(Box<dyn Filter>),
|
||||
}
|
||||
|
||||
impl Module {
|
||||
pub fn new(name: String, args: ModuleArgs, available: &dyn ModulesPort) -> Result<Module, ()> {
|
||||
if let Some(a) = available.available_actions().get(&name).map(|m| m.cons) {
|
||||
Ok(Module::Action(a(args)))
|
||||
} else if let Some(f) = available.available_filters().get(&name).map(|m| m.cons) {
|
||||
Ok(Module::Filter(f(args)))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&self, record: &mut Record) -> Result<bool, ()> {
|
||||
match self {
|
||||
Module::Action(a) => match a.act(record) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(()) => Err(()),
|
||||
},
|
||||
Module::Filter(f) => Ok(f.filter(record)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Action: Debug {
|
||||
fn act(&self, record: &mut Record) -> Result<(), ()>;
|
||||
}
|
||||
|
||||
pub trait Filter: Debug {
|
||||
fn filter(&self, record: &mut Record) -> bool;
|
||||
}
|
||||
|
||||
pub type ModuleArgs = HashMap<String, Value>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AvailableAction, AvailableFilter, Module, 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 record: Record = HashMap::new();
|
||||
let mods = FakeModulesAdapter::new(&aa, &[]);
|
||||
|
||||
// When
|
||||
let module = Module::new(ACT_NAME.to_string(), HashMap::new(), &mods).unwrap();
|
||||
|
||||
// Then
|
||||
assert!(module.run(&mut record) == Ok(true));
|
||||
assert!(record.contains_key(ACT_NAME));
|
||||
assert!(record[ACT_NAME] == Value::Int(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn available_filter_can_be_generated_and_run() {
|
||||
// Given
|
||||
let af = [AvailableFilter {
|
||||
name: FLT_NAME.to_string(),
|
||||
cons: |_| 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();
|
||||
|
||||
// Then
|
||||
assert!(module.run(&mut record) == Ok(false));
|
||||
assert!(record.contains_key(FLT_NAME));
|
||||
assert!(record[FLT_NAME] == Value::Int(1));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
use crate::domain::{Action, AvailableAction, AvailableFilter, Filter, ModulesPort, Record, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
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<(), ()> {
|
||||
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)),
|
||||
_ => panic!("The record did not contain the expected value."),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FakeFilter {}
|
||||
|
||||
impl Filter for FakeFilter {
|
||||
fn filter(&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)),
|
||||
_ => panic!("The record did not contain the expected value."),
|
||||
};
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FakeModulesAdapter<'a> {
|
||||
a: HashMap<&'a String, &'a AvailableAction>,
|
||||
f: HashMap<&'a String, &'a AvailableFilter>,
|
||||
}
|
||||
|
||||
impl FakeModulesAdapter<'_> {
|
||||
pub fn new<'a>(act: &'a [AvailableAction], flt: &'a [AvailableFilter]) -> FakeModulesAdapter<'a> {
|
||||
let a = act
|
||||
.iter()
|
||||
.map(|m| (&m.name, m))
|
||||
.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<'_> {
|
||||
fn available_actions(&self) -> HashMap<&String, &AvailableAction> {
|
||||
self.a.clone()
|
||||
}
|
||||
fn available_filters(&self) -> HashMap<&String, &AvailableFilter> {
|
||||
self.f.clone()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,328 @@
|
|||
use crate::domain::{Config, Module, ModulesPort, Record, Step};
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Add;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Node {
|
||||
name: String,
|
||||
module: Module,
|
||||
then_dest: isize, // <0 for a leaf-Node
|
||||
else_dest: isize, // <0 for a leaf-Node
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn run(&self, record: &mut Record) -> isize {
|
||||
if let Ok(b) = self.module.run(record) {
|
||||
if b {
|
||||
self.then_dest
|
||||
} else {
|
||||
self.else_dest
|
||||
}
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Workflow {
|
||||
nodes: Vec<Node>,
|
||||
}
|
||||
|
||||
impl Workflow {
|
||||
pub fn run(&self, record: &mut Record) {
|
||||
let mut i = 0 as isize;
|
||||
while {
|
||||
i = self.nodes[i as usize].run(record);
|
||||
i >= 0
|
||||
} {}
|
||||
}
|
||||
|
||||
pub fn build(conf: &mut Config, available: &dyn ModulesPort) -> Workflow {
|
||||
let mut seen: Vec<String> = Vec::new();
|
||||
let mut nodes: Vec<Node> = Vec::new();
|
||||
let mut dangling: DanglingInfo = HashMap::new();
|
||||
for (name, chain) in conf.actions.drain(..) {
|
||||
build_chain(name, chain, &mut nodes, &mut seen, &mut dangling, available);
|
||||
}
|
||||
if nodes.is_empty() {
|
||||
panic!("A configuration must have at least one module.");
|
||||
}
|
||||
if !dangling.is_empty() {
|
||||
let mut error = "Incomplete configuration:".to_string();
|
||||
let mut unknown = false;
|
||||
for (o, v) in dangling {
|
||||
if let Some(c) = o {
|
||||
unknown = true;
|
||||
error = format!(
|
||||
"{}\n\tReference to unknown chain “{}” found at:",
|
||||
&error, &c
|
||||
);
|
||||
for (i, is_then) in v {
|
||||
error = format!(
|
||||
"{}\n\t {}:{}",
|
||||
&error,
|
||||
nodes[i].name,
|
||||
if is_then { "then" } else { "else" }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if unknown {
|
||||
panic!(error);
|
||||
}
|
||||
}
|
||||
Workflow { nodes }
|
||||
}
|
||||
}
|
||||
|
||||
type DanglingInfo = HashMap<Option<String>, Vec<(usize, bool)>>;
|
||||
|
||||
fn build_chain(
|
||||
chain_name: String,
|
||||
chain: Vec<Step>,
|
||||
nodes: &mut Vec<Node>,
|
||||
seen: &mut Vec<String>,
|
||||
dangling: &mut DanglingInfo,
|
||||
available: &dyn ModulesPort,
|
||||
) {
|
||||
let mut index = 0;
|
||||
for step in chain {
|
||||
let next_node_u = nodes.len();
|
||||
let next_node_i = next_node_u as isize;
|
||||
let mut then_was_used = false;
|
||||
if index == 0 {
|
||||
if let Some(v) = dangling.remove(&Some(&chain_name).cloned()) {
|
||||
for (i, is_then) in v {
|
||||
match is_then {
|
||||
true => nodes.get_mut(i).unwrap().then_dest = next_node_i,
|
||||
false => nodes.get_mut(i).unwrap().else_dest = next_node_i,
|
||||
};
|
||||
}
|
||||
} else if let Some(v) = dangling.remove(&None) {
|
||||
for (i, is_then) in v {
|
||||
match is_then {
|
||||
true => nodes.get_mut(i).unwrap().then_dest = next_node_i,
|
||||
false => nodes.get_mut(i).unwrap().else_dest = next_node_i,
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nodes.get_mut(next_node_u - 1).unwrap().then_dest = next_node_i;
|
||||
}
|
||||
let name = chain_name
|
||||
.clone()
|
||||
.add(&format!("[{}]:{}", index, &step.module));
|
||||
let module = Module::new(step.module, step.args, available)
|
||||
.expect(&format!("Module {} could not be created.", &name));
|
||||
let then_dest = step
|
||||
.then_dest
|
||||
.map(|c| {
|
||||
if seen.contains(&c) {
|
||||
panic!(format!("Configuration loop at {}:then", name));
|
||||
}
|
||||
then_was_used = true;
|
||||
node_wants_chain(next_node_u, true, dangling, Some(c));
|
||||
-1
|
||||
})
|
||||
.unwrap_or(-1);
|
||||
let else_dest = step
|
||||
.else_dest
|
||||
.map(|c| {
|
||||
if seen.contains(&c) {
|
||||
panic!(format!("Configuration loop at {}:else", name));
|
||||
}
|
||||
node_wants_chain(next_node_u, false, dangling, Some(c));
|
||||
-1
|
||||
})
|
||||
.unwrap_or({
|
||||
node_wants_chain(next_node_u, false, dangling, None);
|
||||
-1
|
||||
});
|
||||
nodes.push(Node {
|
||||
name,
|
||||
module,
|
||||
then_dest,
|
||||
else_dest,
|
||||
});
|
||||
index += 1;
|
||||
if then_was_used {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn node_wants_chain(
|
||||
next_node_index: usize,
|
||||
is_then: bool,
|
||||
dangling: &mut DanglingInfo,
|
||||
wanted_chain: Option<String>,
|
||||
) {
|
||||
let mut d = dangling.remove(&wanted_chain).unwrap_or(Vec::new());
|
||||
d.push((next_node_index, is_then));
|
||||
dangling.insert(wanted_chain, d);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::domain::test_util::*;
|
||||
use crate::domain::{AvailableAction, AvailableFilter, Chain, Config, Record, Step, Value, Workflow};
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "A configuration must have at least one module.")]
|
||||
fn empty_config_results_in_panic() {
|
||||
// Given
|
||||
let mut conf = Config {
|
||||
actions: IndexMap::new(),
|
||||
options: HashMap::new(),
|
||||
};
|
||||
let mods = FakeModulesAdapter::new(&[], &[]);
|
||||
|
||||
// When
|
||||
let _wf = Workflow::build(&mut conf, &mods);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_with_one_module_is_ok() {
|
||||
// Given
|
||||
let mut actions: IndexMap<String, Chain> = IndexMap::new();
|
||||
actions.insert(
|
||||
"chain1".to_string(),
|
||||
vec![Step {
|
||||
module: ACT_NAME.to_string(),
|
||||
args: HashMap::new(),
|
||||
then_dest: None,
|
||||
else_dest: None,
|
||||
}],
|
||||
);
|
||||
let mut conf = Config {
|
||||
actions,
|
||||
options: HashMap::new(),
|
||||
};
|
||||
let aa = [AvailableAction {
|
||||
name: ACT_NAME.to_string(),
|
||||
cons: |_| Box::new(FakeAction {}),
|
||||
}];
|
||||
let mods = FakeModulesAdapter::new(&aa, &[]);
|
||||
let mut record: Record = HashMap::new();
|
||||
|
||||
// When
|
||||
let wf = Workflow::build(&mut conf, &mods);
|
||||
wf.run(&mut record);
|
||||
|
||||
// Then
|
||||
assert!(wf.nodes.len() == 1);
|
||||
assert!(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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_chain_to_chain_link_works() {
|
||||
// Given
|
||||
let mut actions: IndexMap<String, Chain> = IndexMap::new();
|
||||
actions.insert(
|
||||
"chain1".to_string(),
|
||||
vec![Step {
|
||||
module: FLT_NAME.to_string(),
|
||||
args: HashMap::new(),
|
||||
then_dest: None,
|
||||
else_dest: Some("chain2".to_string()),
|
||||
}],
|
||||
);
|
||||
actions.insert(
|
||||
"chain2".to_string(),
|
||||
vec![Step {
|
||||
module: ACT_NAME.to_string(),
|
||||
args: HashMap::new(),
|
||||
then_dest: None,
|
||||
else_dest: None,
|
||||
}],
|
||||
);
|
||||
let mut conf = Config {
|
||||
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 record: Record = HashMap::new();
|
||||
|
||||
// When
|
||||
let wf = Workflow::build(&mut conf, &mods);
|
||||
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!(wf.nodes[0].then_dest < 0);
|
||||
assert!(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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_chain_to_chain_link_works() {
|
||||
// Given
|
||||
let mut actions: IndexMap<String, Chain> = IndexMap::new();
|
||||
actions.insert(
|
||||
"chain1".to_string(),
|
||||
vec![Step {
|
||||
module: FLT_NAME.to_string(),
|
||||
args: HashMap::new(),
|
||||
then_dest: None,
|
||||
else_dest: None,
|
||||
}],
|
||||
);
|
||||
actions.insert(
|
||||
"chain2".to_string(),
|
||||
vec![Step {
|
||||
module: ACT_NAME.to_string(),
|
||||
args: HashMap::new(),
|
||||
then_dest: None,
|
||||
else_dest: None,
|
||||
}],
|
||||
);
|
||||
let mut conf = Config {
|
||||
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 record: Record = HashMap::new();
|
||||
|
||||
// When
|
||||
let wf = Workflow::build(&mut conf, &mods);
|
||||
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!(wf.nodes[0].then_dest < 0);
|
||||
assert!(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));
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
use crate::modules::{AvailableFilter,Filter,ModuleArgs};
|
||||
use crate::common::Record;
|
||||
use crate::common::Value;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Equals {
|
||||
field: String,
|
||||
value: Value
|
||||
}
|
||||
|
||||
inventory::submit! {
|
||||
AvailableFilter::new("filter_equals", move |a| Box::new(Equals::from_args(a)))
|
||||
}
|
||||
|
||||
impl Equals {
|
||||
pub fn from_args(mut args: ModuleArgs) -> Equals {
|
||||
Equals {
|
||||
field: match args.remove("field") {
|
||||
Some(Value::Str(s)) => s,
|
||||
_ => panic!("The Equals filter needs a field to filter in “field”")
|
||||
},
|
||||
value: args.remove("value").expect("The Equals filter needs a reference value in “value”")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Filter for Equals {
|
||||
fn filter(&self, record: &mut Record) -> bool {
|
||||
match (record.get(&self.field.as_ref()), &self.value) {
|
||||
(Some(ref v1), ref v2) => v1 == v2,
|
||||
(None, _) => false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::Utc;
|
||||
use std::collections::HashMap;
|
||||
use crate::common::{Record,Value};
|
||||
use crate::filters::Equals;
|
||||
use crate::modules::{Filter,ModuleArgs};
|
||||
|
||||
fn generate_args_record_equal<'a>(name: &'a str, value: Value) -> (ModuleArgs, Record<'a>) {
|
||||
let mut args = HashMap::with_capacity(2);
|
||||
args.insert(String::from("field"), Value::Str(String::from(name)));
|
||||
args.insert(String::from("value"), value.clone());
|
||||
let mut record = HashMap::with_capacity(1);
|
||||
record.insert(name, value);
|
||||
(args, record)
|
||||
}
|
||||
|
||||
fn generate_args_record_custom<'a>(ref_name: &str, ref_value: Value, test_name: &'a str, test_value: Value) -> (ModuleArgs, Record<'a>) {
|
||||
let mut args = HashMap::with_capacity(2);
|
||||
args.insert(String::from("field"), Value::Str(String::from(ref_name)));
|
||||
args.insert(String::from("value"), ref_value);
|
||||
let mut record = HashMap::with_capacity(1);
|
||||
record.insert(test_name, test_value);
|
||||
(args, record)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_equals_should_return_true() {
|
||||
let (args, mut record) = generate_args_record_equal("a_boolean", Value::Bool(false));
|
||||
let filter = Equals::from_args(args);
|
||||
assert!(filter.filter(&mut record));
|
||||
|
||||
let (args, mut record) = generate_args_record_equal("a_string", Value::Str(String::from("Hello!")));
|
||||
let filter = Equals::from_args(args);
|
||||
assert!(filter.filter(&mut record));
|
||||
|
||||
let (args, mut record) = generate_args_record_equal("an_integer", Value::Int(2));
|
||||
let filter = Equals::from_args(args);
|
||||
assert!(filter.filter(&mut record));
|
||||
|
||||
let (args, mut record) = generate_args_record_equal("a_date", Value::Date(Utc::now()));
|
||||
let filter = Equals::from_args(args);
|
||||
assert!(filter.filter(&mut record));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_equals_should_return_false() {
|
||||
let (args, mut record) = generate_args_record_custom("a_boolean", Value::Bool(true), "a_boolean", Value::Bool(false));
|
||||
let filter = Equals::from_args(args);
|
||||
assert!(! filter.filter(&mut record));
|
||||
|
||||
let (args, mut record) = generate_args_record_custom("a_string", Value::Str(String::from("Hello!")), "a_string", Value::Str(String::from("World!")));
|
||||
let filter = Equals::from_args(args);
|
||||
assert!(! filter.filter(&mut record));
|
||||
|
||||
let (args, mut record) = generate_args_record_custom("an_integer", Value::Int(2), "an_integer", Value::Int(3));
|
||||
let filter = Equals::from_args(args);
|
||||
assert!(! filter.filter(&mut record));
|
||||
|
||||
let (args, mut record) = generate_args_record_custom("a_date", Value::Date(Utc::now()), "a_date", Value::Date(Utc::now()));
|
||||
let filter = Equals::from_args(args);
|
||||
assert!(! filter.filter(&mut record));
|
||||
|
||||
let (args, mut record) = generate_args_record_custom("first_one", Value::Int(1), "second_one", Value::Int(1));
|
||||
let filter = Equals::from_args(args);
|
||||
assert!(! filter.filter(&mut record));
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
mod equals;
|
||||
pub use self::equals::*;
|
||||
|
||||
/*
|
||||
pub trait Filter {
|
||||
fn filter(&self, record: &mut Record) -> bool;
|
||||
}
|
||||
|
||||
impl<T: Filter> Module for T {
|
||||
fn run(&self, record: &mut Record) -> Result<bool, ()> {
|
||||
Ok(self.filter(record))
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -0,0 +1,28 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -1,21 +1,33 @@
|
|||
use super::SerdeConfigAdapter;
|
||||
use std::env;
|
||||
use std::ffi::{OsString};
|
||||
use std::io::{BufReader,Error,Result};
|
||||
use std::path::Path;
|
||||
use std::ffi::OsString;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
|
||||
const ENV_VARIABLE: &'static str = "PYRUSE_CONF";
|
||||
const ETC_PATH: &'static str = "/etc/pyruse";
|
||||
|
||||
enum ConfFile {
|
||||
pub enum ConfFile {
|
||||
Json(OsString),
|
||||
Yaml(OsString)
|
||||
Yaml(OsString),
|
||||
}
|
||||
|
||||
pub fn from_file() {
|
||||
match find_file(find_candidates()) {
|
||||
ConfFile::Json(path) => super::parse_json(BufReader::new(File::open(path).expect("Read error"))),
|
||||
ConfFile::Yaml(path) => super::parse_yaml(BufReader::new(File::open(path).expect("Read error")))
|
||||
};
|
||||
impl ConfFile {
|
||||
pub fn from_filesystem() -> ConfFile {
|
||||
find_file(find_candidates())
|
||||
}
|
||||
|
||||
pub fn to_config(self) -> SerdeConfigAdapter {
|
||||
match self {
|
||||
ConfFile::Json(path) => {
|
||||
SerdeConfigAdapter::from_json(BufReader::new(File::open(path).expect("Read error")))
|
||||
}
|
||||
ConfFile::Yaml(path) => {
|
||||
SerdeConfigAdapter::from_yaml(BufReader::new(File::open(path).expect("Read error")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_candidates() -> Vec<ConfFile> {
|
||||
|
@ -29,14 +41,17 @@ fn find_candidates() -> Vec<ConfFile> {
|
|||
match s.as_ref() {
|
||||
"json" => vec![ConfFile::Json(path)],
|
||||
"yaml" | "yml" => vec![ConfFile::Yaml(path)],
|
||||
_ => panic!("Cannot determine file format from file name: {}", path.to_string_lossy())
|
||||
_ => panic!(
|
||||
"Cannot determine file format from file name: {}",
|
||||
path.to_string_lossy()
|
||||
),
|
||||
}
|
||||
},
|
||||
}
|
||||
None => {
|
||||
vec![
|
||||
ConfFile::Json(OsString::from("pyruse.json")),
|
||||
ConfFile::Yaml(OsString::from("pyruse.yaml")),
|
||||
ConfFile::Yaml(OsString::from("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"))),
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,336 @@
|
|||
use crate::domain::{Chain, Config, ConfigPort, ModuleArgs, Step, Value};
|
||||
use indexmap::IndexMap;
|
||||
use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor};
|
||||
use serde::Deserialize;
|
||||
use serde_json;
|
||||
use serde_yaml;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::io::Read;
|
||||
|
||||
mod file;
|
||||
|
||||
struct SerdeConfigAdapter {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl SerdeConfigAdapter {
|
||||
pub fn from_json(data: impl Read) -> SerdeConfigAdapter {
|
||||
let serde_config: SerdeConfig =
|
||||
serde_json::from_reader(data).expect("Failed to parse configuration");
|
||||
SerdeConfigAdapter::from_serde(serde_config)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
fn from_serde(mut data: SerdeConfig) -> SerdeConfigAdapter {
|
||||
SerdeConfigAdapter {
|
||||
config: Config {
|
||||
actions: data
|
||||
.actions
|
||||
.drain(..)
|
||||
.map(|(name, mut serde_chain)| {
|
||||
(
|
||||
name,
|
||||
serde_chain
|
||||
.drain(..)
|
||||
.map(|step| Step {
|
||||
module: match step.module {
|
||||
StepType::Action(s) => s,
|
||||
StepType::Filter(s) => s,
|
||||
},
|
||||
args: step.args,
|
||||
then_dest: step.then_dest,
|
||||
else_dest: step.else_dest,
|
||||
})
|
||||
.collect::<Chain>(),
|
||||
)
|
||||
})
|
||||
.collect::<IndexMap<String, Chain>>(),
|
||||
options: data.options,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigPort for SerdeConfigAdapter {
|
||||
fn get(&self) -> &Config {
|
||||
&self.config
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SerdeConfig {
|
||||
actions: IndexMap<String, SerdeChain>,
|
||||
|
||||
#[serde(flatten)]
|
||||
options: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
type SerdeChain = Vec<SerdeStep>;
|
||||
|
||||
#[derive(Debug, Deserialize, Eq, PartialEq)]
|
||||
pub enum StepType {
|
||||
#[serde(rename(deserialize = "action"))]
|
||||
Action(String),
|
||||
#[serde(rename(deserialize = "filter"))]
|
||||
Filter(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SerdeStep {
|
||||
#[serde(flatten)]
|
||||
module: StepType,
|
||||
args: ModuleArgs,
|
||||
#[serde(rename(deserialize = "then"))]
|
||||
then_dest: Option<String>,
|
||||
#[serde(rename(deserialize = "else"))]
|
||||
else_dest: Option<String>,
|
||||
}
|
||||
|
||||
/* *** serde for Value *** */
|
||||
|
||||
struct ValueVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ValueVisitor {
|
||||
type Value = Value;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a boolean, string, or integer")
|
||||
}
|
||||
|
||||
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Bool(v))
|
||||
}
|
||||
|
||||
fn visit_i8<E>(self, v: i8) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_i16<E>(self, v: i16) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_i128<E>(self, v: i128) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_u128<E>(self, v: u128) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Int(v as isize))
|
||||
}
|
||||
|
||||
fn visit_char<E>(self, v: char) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Str(v.to_string()))
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Str(String::from(v)))
|
||||
}
|
||||
|
||||
fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Str(String::from(v)))
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Str(v))
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Str(
|
||||
std::str::from_utf8(v)
|
||||
.expect("Strings in the configuration must be UTF-8")
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Str(
|
||||
std::str::from_utf8(v)
|
||||
.expect("Strings in the configuration must be UTF-8")
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Str(
|
||||
String::from_utf8(v).expect("Strings in the configuration must be UTF-8"),
|
||||
))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
let mut result = Vec::with_capacity(seq.size_hint().unwrap_or(0));
|
||||
while let Some(v) = seq.next_element()? {
|
||||
result.push(v);
|
||||
}
|
||||
Ok(Value::List(result))
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: MapAccess<'de>,
|
||||
{
|
||||
let mut result = HashMap::with_capacity(map.size_hint().unwrap_or(0));
|
||||
while let Some((k, v)) = map.next_entry()? {
|
||||
result.insert(k, v);
|
||||
}
|
||||
Ok(Value::Map(result))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Value {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(ValueVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
/* *** end of serde *** */
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SerdeConfigAdapter;
|
||||
use crate::domain::Value;
|
||||
|
||||
#[test]
|
||||
fn parse_json_works() {
|
||||
// Given
|
||||
let json = r#"
|
||||
{
|
||||
"actions": {
|
||||
"Detect request errors with Nextcloud": [
|
||||
{
|
||||
"filter": "filter_equals",
|
||||
"args": { "field": "SYSLOG_IDENTIFIER", "value": "uwsgi" }
|
||||
},
|
||||
{
|
||||
"filter": "filter_pcre",
|
||||
"args": { "field": "MESSAGE", "re": "^\\[[^]]+\\] ([^ ]+) .*\\] ([A-Z]+ /[^?]*)(?:\\?.*)? => .*\\(HTTP/1.1 5..\\)", "save": [ "thatIP", "HTTPrequest" ] },
|
||||
"else": "… Report insufficient buffer-size for Nextcloud QUERY_STRING"
|
||||
},
|
||||
{
|
||||
"action": "action_dailyReport",
|
||||
"args": { "level": "INFO", "message": "IP {thatIP} failed to {HTTPrequest} on Nextcloud", "details": "FIRSTLAST" }
|
||||
}
|
||||
],
|
||||
"… Report insufficient buffer-size for Nextcloud QUERY_STRING": [
|
||||
]
|
||||
},
|
||||
"debug": false
|
||||
}
|
||||
"#.as_bytes();
|
||||
|
||||
// When
|
||||
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!(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)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
use crate::domain::{LogMessage, LogPort, Record, Value};
|
||||
use chrono::DateTime;
|
||||
use std::collections::HashMap;
|
||||
use std::iter::FromIterator;
|
||||
use systemd::journal::{print, Journal, OpenOptions};
|
||||
|
||||
type JournalFieldMapper = fn(String) -> Value;
|
||||
|
||||
const STR_MAPPER: JournalFieldMapper = |s| Value::Str(s);
|
||||
const INT_MAPPER: JournalFieldMapper = |s| {
|
||||
s.parse::<isize>()
|
||||
.map(|i| Value::Int(i))
|
||||
.unwrap_or(Value::Str(s))
|
||||
};
|
||||
const DATE_MAPPER: JournalFieldMapper = |s| {
|
||||
s.parse::<DateTime<chrono::Utc>>()
|
||||
.map(|d| Value::Date(d))
|
||||
.unwrap_or(Value::Str(s))
|
||||
};
|
||||
|
||||
pub struct SystemdLogAdapter {
|
||||
journal: Journal,
|
||||
mappers: HashMap<String, JournalFieldMapper>,
|
||||
}
|
||||
|
||||
impl LogPort for SystemdLogAdapter {
|
||||
fn open() -> Result<Self, ()> {
|
||||
let mappers = create_mappers();
|
||||
if let Ok(mut journal) = OpenOptions::default()
|
||||
.system(true)
|
||||
.current_user(false)
|
||||
.local_only(false)
|
||||
.open()
|
||||
{
|
||||
if let Ok(_) = journal.seek_tail() {
|
||||
return Ok(SystemdLogAdapter { journal, mappers });
|
||||
}
|
||||
}
|
||||
Err(())
|
||||
}
|
||||
|
||||
fn read_next(&mut self) -> Result<Record, ()> {
|
||||
loop {
|
||||
match self.journal.await_next_entry(None) {
|
||||
Err(_) => return Err(()),
|
||||
Ok(Some(mut entry)) => {
|
||||
let mut record: Record = HashMap::with_capacity(entry.len());
|
||||
let all_keys = entry.keys().map(|s| s.clone()).collect::<Vec<String>>();
|
||||
for k in all_keys {
|
||||
let (k, v) = entry.remove_entry(&k).unwrap();
|
||||
let mapper = self.mappers.get(&k);
|
||||
if mapper == None {
|
||||
continue;
|
||||
}
|
||||
record.insert(k, (mapper.unwrap())(v));
|
||||
}
|
||||
return Ok(record);
|
||||
}
|
||||
Ok(None) => continue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn write(&mut self, message: LogMessage) -> Result<(), ()> {
|
||||
let unix_status = match message {
|
||||
LogMessage::EMERG(m) => print(0, m),
|
||||
LogMessage::ALERT(m) => print(1, m),
|
||||
LogMessage::CRIT(m) => print(2, m),
|
||||
LogMessage::ERR(m) => print(3, m),
|
||||
LogMessage::WARNING(m) => print(4, m),
|
||||
LogMessage::NOTICE(m) => print(5, m),
|
||||
LogMessage::INFO(m) => print(6, m),
|
||||
LogMessage::DEBUG(m) => print(7, m),
|
||||
};
|
||||
match unix_status {
|
||||
0 => Ok(()),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_mappers<'t>() -> HashMap<String, JournalFieldMapper> {
|
||||
let map: HashMap<String, JournalFieldMapper> = HashMap::from_iter(
|
||||
[
|
||||
("MESSAGE", STR_MAPPER),
|
||||
("MESSAGE_ID", STR_MAPPER),
|
||||
("PRIORITY", INT_MAPPER),
|
||||
("CODE_FILE", STR_MAPPER),
|
||||
("CODE_LINE", INT_MAPPER),
|
||||
("CODE_FUNC", STR_MAPPER),
|
||||
("ERRNO", INT_MAPPER),
|
||||
("INVOCATION_ID", STR_MAPPER),
|
||||
("USER_INVOCATION_ID", STR_MAPPER),
|
||||
("SYSLOG_FACILITY", INT_MAPPER),
|
||||
("SYSLOG_IDENTIFIER", STR_MAPPER),
|
||||
("SYSLOG_PID", INT_MAPPER),
|
||||
("SYSLOG_TIMESTAMP", DATE_MAPPER),
|
||||
("SYSLOG_RAW", STR_MAPPER),
|
||||
("DOCUMENTATION", STR_MAPPER),
|
||||
("TID", INT_MAPPER),
|
||||
("_PID", INT_MAPPER),
|
||||
("_UID", INT_MAPPER),
|
||||
("_GID", INT_MAPPER),
|
||||
("_COMM", STR_MAPPER),
|
||||
("_EXE", STR_MAPPER),
|
||||
("_CMDLINE", STR_MAPPER),
|
||||
("_CAP_EFFECTIVE", STR_MAPPER),
|
||||
("_AUDIT_SESSION", STR_MAPPER),
|
||||
("_AUDIT_LOGINUID", INT_MAPPER),
|
||||
("_SYSTEMD_CGROUP", STR_MAPPER),
|
||||
("_SYSTEMD_SLICE", STR_MAPPER),
|
||||
("_SYSTEMD_UNIT", STR_MAPPER),
|
||||
("_SYSTEMD_USER_UNIT", STR_MAPPER),
|
||||
("_SYSTEMD_USER_SLICE", STR_MAPPER),
|
||||
("_SYSTEMD_SESSION", STR_MAPPER),
|
||||
("_SYSTEMD_OWNER_UID", INT_MAPPER),
|
||||
("_SELINUX_CONTEXT", STR_MAPPER),
|
||||
("_SOURCE_REALTIME_TIMESTAMP", DATE_MAPPER),
|
||||
("_BOOT_ID", STR_MAPPER),
|
||||
("_MACHINE_ID", STR_MAPPER),
|
||||
("_SYSTEMD_INVOCATION_ID", STR_MAPPER),
|
||||
("_HOSTNAME", STR_MAPPER),
|
||||
("_TRANSPORT", STR_MAPPER),
|
||||
("_STREAM_ID", STR_MAPPER),
|
||||
("_LINE_BREAK", STR_MAPPER),
|
||||
("_NAMESPACE", STR_MAPPER),
|
||||
("_KERNEL_DEVICE", STR_MAPPER),
|
||||
("_KERNEL_SUBSYSTEM", STR_MAPPER),
|
||||
("_UDEV_SYSNAME", STR_MAPPER),
|
||||
("_UDEV_DEVNODE", STR_MAPPER),
|
||||
("_UDEV_DEVLINK", STR_MAPPER),
|
||||
("COREDUMP_UNIT", STR_MAPPER),
|
||||
("COREDUMP_USER_UNIT", STR_MAPPER),
|
||||
("OBJECT_PID", INT_MAPPER),
|
||||
("OBJECT_UID", INT_MAPPER),
|
||||
("OBJECT_GID", INT_MAPPER),
|
||||
("OBJECT_COMM", STR_MAPPER),
|
||||
("OBJECT_EXE", STR_MAPPER),
|
||||
("OBJECT_CMDLINE", STR_MAPPER),
|
||||
("OBJECT_AUDIT_SESSION", STR_MAPPER),
|
||||
("OBJECT_AUDIT_LOGINUID", INT_MAPPER),
|
||||
("OBJECT_SYSTEMD_CGROUP", STR_MAPPER),
|
||||
("OBJECT_SYSTEMD_SESSION", STR_MAPPER),
|
||||
("OBJECT_SYSTEMD_OWNER_UID", INT_MAPPER),
|
||||
("OBJECT_SYSTEMD_UNIT", STR_MAPPER),
|
||||
("OBJECT_SYSTEMD_USER_UNIT", STR_MAPPER),
|
||||
("__CURSOR", STR_MAPPER),
|
||||
("__REALTIME_TIMESTAMP", DATE_MAPPER),
|
||||
("__MONOTONIC_TIMESTAMP", DATE_MAPPER),
|
||||
]
|
||||
.iter()
|
||||
.map(|(s, m)| (s.to_string(), m.to_owned())),
|
||||
);
|
||||
map
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
pub mod action;
|
||||
pub mod config;
|
||||
pub mod filter;
|
||||
pub mod log;
|
||||
pub mod module;
|
|
@ -0,0 +1,25 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
mod actions;
|
||||
mod common;
|
||||
mod config;
|
||||
mod filters;
|
||||
mod modules;
|
||||
mod domain;
|
||||
mod service;
|
||||
mod infra;
|
||||
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
use crate::common::{Record,Value};
|
||||
|
||||
pub struct AvailableAction {
|
||||
name: &'static str,
|
||||
cons: fn(ModuleArgs) -> Box<dyn Action>
|
||||
}
|
||||
|
||||
impl AvailableAction {
|
||||
pub fn new(name: &'static str, cons: fn(ModuleArgs) -> Box<dyn Action>) -> Self {
|
||||
AvailableAction { name, cons }
|
||||
}
|
||||
}
|
||||
|
||||
inventory::collect!(AvailableAction);
|
||||
|
||||
pub struct AvailableFilter {
|
||||
name: &'static str,
|
||||
cons: fn(ModuleArgs) -> Box<dyn Filter>
|
||||
}
|
||||
|
||||
impl AvailableFilter {
|
||||
pub fn new(name: &'static str, cons: fn(ModuleArgs) -> Box<dyn Filter>) -> Self {
|
||||
AvailableFilter { name, cons }
|
||||
}
|
||||
}
|
||||
|
||||
inventory::collect!(AvailableFilter);
|
||||
|
||||
pub enum Module {
|
||||
Action(Box<dyn Action>),
|
||||
Filter(Box<dyn Filter>)
|
||||
}
|
||||
|
||||
impl Module {
|
||||
pub fn get_module(name: &str, args: ModuleArgs) -> Result<Module, ()> {
|
||||
for action in inventory::iter::<AvailableAction> {
|
||||
if action.name == name {
|
||||
return Ok(Module::Action((action.cons)(args)))
|
||||
}
|
||||
}
|
||||
for filter in inventory::iter::<AvailableFilter> {
|
||||
if filter.name == name {
|
||||
return Ok(Module::Filter((filter.cons)(args)))
|
||||
}
|
||||
}
|
||||
Err(())
|
||||
}
|
||||
|
||||
pub fn run(&self, record: &mut Record) -> Result<bool, ()> {
|
||||
match self {
|
||||
Module::Action(a) => match a.act(record) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(()) => Err(())
|
||||
},
|
||||
Module::Filter(f) => Ok(f.filter(record))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Action {
|
||||
fn act(&self, record: &mut Record) -> Result<(), ()>;
|
||||
}
|
||||
|
||||
pub trait Filter {
|
||||
fn filter(&self, record: &mut Record) -> bool;
|
||||
}
|
||||
|
||||
pub type ModuleArgs = HashMap<String, Value>;
|
Loading…
Reference in New Issue