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; pub use self::file::*; pub 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) } pub fn from_yaml(data: impl Read) -> SerdeConfigAdapter { let serde_config: SerdeConfig = serde_yaml::from_reader(data).expect("Failed to parse configuration"); SerdeConfigAdapter::from_serde(serde_config) } 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::(), ) }) .collect::>(), options: data.options, }, } } } impl ConfigPort for SerdeConfigAdapter { fn get(&mut self) -> &mut Config { &mut self.config } } #[derive(Deserialize)] struct SerdeConfig { actions: IndexMap, #[serde(flatten)] options: HashMap, } type SerdeChain = Vec; #[derive(Deserialize, Eq, PartialEq)] enum StepType { #[serde(rename(deserialize = "action"))] Action(String), #[serde(rename(deserialize = "filter"))] Filter(String), } #[derive(Deserialize)] struct SerdeStep { #[serde(flatten)] module: StepType, args: ModuleArgs, #[serde(rename(deserialize = "then"))] then_dest: Option, #[serde(rename(deserialize = "else"))] else_dest: Option, } /* *** 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(self, v: bool) -> Result where E: de::Error, { Ok(Value::Bool(v)) } fn visit_i8(self, v: i8) -> Result where E: de::Error, { Ok(Value::Int(v as isize)) } fn visit_i16(self, v: i16) -> Result where E: de::Error, { Ok(Value::Int(v as isize)) } fn visit_i32(self, v: i32) -> Result where E: de::Error, { Ok(Value::Int(v as isize)) } fn visit_i64(self, v: i64) -> Result where E: de::Error, { Ok(Value::Int(v as isize)) } fn visit_i128(self, v: i128) -> Result where E: de::Error, { Ok(Value::Int(v as isize)) } fn visit_u8(self, v: u8) -> Result where E: de::Error, { Ok(Value::Int(v as isize)) } fn visit_u16(self, v: u16) -> Result where E: de::Error, { Ok(Value::Int(v as isize)) } fn visit_u32(self, v: u32) -> Result where E: de::Error, { Ok(Value::Int(v as isize)) } fn visit_u64(self, v: u64) -> Result where E: de::Error, { Ok(Value::Int(v as isize)) } fn visit_u128(self, v: u128) -> Result where E: de::Error, { Ok(Value::Int(v as isize)) } fn visit_f32(self, v: f32) -> Result where E: de::Error, { Ok(Value::Int(v as isize)) } fn visit_f64(self, v: f64) -> Result where E: de::Error, { Ok(Value::Int(v as isize)) } fn visit_char(self, v: char) -> Result where E: de::Error, { Ok(Value::Str(v.to_string())) } fn visit_str(self, v: &str) -> Result where E: de::Error, { Ok(Value::Str(String::from(v))) } fn visit_borrowed_str(self, v: &'de str) -> Result where E: de::Error, { Ok(Value::Str(String::from(v))) } fn visit_string(self, v: String) -> Result where E: de::Error, { Ok(Value::Str(v)) } fn visit_bytes(self, v: &[u8]) -> Result 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(self, v: &'de [u8]) -> Result 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(self, v: Vec) -> Result where E: de::Error, { Ok(Value::Str( String::from_utf8(v).expect("Strings in the configuration must be UTF-8"), )) } fn visit_seq(self, mut seq: A) -> Result 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(self, mut map: A) -> Result 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(deserializer: D) -> Result 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_eq!(conf.actions.len(), 2); assert_eq!( conf.actions.get_index(0).unwrap().0, "Detect request errors with Nextcloud" ); assert_eq!( conf.actions.get_index(1).unwrap().0, "… Report insufficient buffer-size for Nextcloud QUERY_STRING" ); assert_eq!(conf.actions.get_index(0).unwrap().1.len(), 3); assert_eq!( conf.actions.get_index(0).unwrap().1[1].module, String::from("filter_pcre") ); assert_eq!(conf.options.get("debug"), Some(&Value::Bool(false))); } }