use crate::domain::{Config, Module, Modules, Record, Step}; use std::collections::HashMap; use std::ops::Add; 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(&mut 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, } impl Workflow { pub fn run(&mut 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: &Modules) -> Workflow { let mut seen: Vec = Vec::new(); let mut nodes: Vec = 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, Vec<(usize, bool)>>; fn build_chain( chain_name: String, chain: Vec, nodes: &mut Vec, seen: &mut Vec, dangling: &mut DanglingInfo, available: &Modules, ) { 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, ) { 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::{Chain, Config, Modules, 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 = Modules::new(); // When let _wf = Workflow::build(&mut conf, &mods); } #[test] fn config_with_one_module_is_ok() { // Given let mut actions: IndexMap = 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 mut mods = Modules::new(); mods.register_action(ACT_NAME.to_string(), Box::new(|_| Box::new(FakeAction {}))); let mut record: Record = HashMap::new(); // When let mut 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 = 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 mut mods = Modules::new(); mods.register_action(ACT_NAME.to_string(), Box::new(|_| Box::new(FakeAction {}))); mods.register_filter(FLT_NAME.to_string(), Box::new(|_| Box::new(FakeFilter {}))); let mut record: Record = HashMap::new(); // When let mut 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 = 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 mut mods = Modules::new(); mods.register_action(ACT_NAME.to_string(), Box::new(|_| Box::new(FakeAction {}))); mods.register_filter(FLT_NAME.to_string(), Box::new(|_| Box::new(FakeFilter {}))); let mut record: Record = HashMap::new(); // When let mut 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)); } }