use super::MatchedStep; use super::PartialStep; use super::ScenarioStep; use super::StepKind; use crate::{Result, SubplotError}; use serde::Deserialize; use std::fs::File; use std::io::Read; use std::path::Path; use regex::{escape, Regex}; /// A binding of a scenario step to its implementation. /// /// Contains the pattern used to match against scenario steps, /// combined with the step kind. The pattern is a regular expression /// as understood by the regex crate. #[derive(Debug, Clone)] pub struct Binding { kind: StepKind, pattern: String, regex: Regex, function: String, cleanup: Option, } impl Binding { /// Create a new Binding, from a step kind and a pattern. pub fn new( kind: StepKind, pattern: &str, function: &str, cleanup: Option<&str>, ) -> Result { Ok(Binding { kind, pattern: pattern.to_owned(), regex: Regex::new(&format!("^{}$", pattern))?, function: function.to_string(), cleanup: cleanup.map(String::from), }) } /// Return the kind of step the binding is for. pub fn kind(&self) -> StepKind { self.kind } /// Return text of pattern. pub fn pattern(&self) -> &str { &self.pattern } /// Return name of function that implements step. pub fn function(&self) -> &str { &self.function } /// Return name of function that implements cleanup. pub fn cleanup(&self) -> Option<&str> { match self.cleanup { None => None, Some(ref s) => Some(&s), } } /// Return the compiled regular expression for the pattern of the /// binding. /// /// The regular expression matches the whole text of a scenario step. pub fn regex(&self) -> &Regex { &self.regex } /// Try to match defined binding against a parsed scenario step. pub fn match_with_step(&self, step: &ScenarioStep) -> Option { if self.kind() != step.kind() { return None; } let step_text = step.text(); let caps = self.regex.captures(step_text)?; // If there is only one capture, it's the whole string. let mut m = MatchedStep::new(self.kind(), &self.function, self.cleanup()); if caps.len() == 1 { m.append_part(PartialStep::uncaptured(step_text)); return Some(m); } // Otherwise, return captures as PartialStep::Text, and the // surrounding text as PartialStep::UnmatchedText. let mut prev_end = 0; for cap in caps.iter().skip(1) { if let Some(cap) = cap { if cap.start() > prev_end { let part = PartialStep::uncaptured(&step_text[prev_end..cap.start()]); m.append_part(part); } // Find name for capture. let mut capname: Option<&str> = None; for name in self.regex.capture_names() { if let Some(name) = name { if let Some(mm) = caps.name(name) { if mm.start() == cap.start() && mm.end() == cap.end() { capname = Some(name); } } } } let part = match capname { None => PartialStep::uncaptured(&step_text[prev_end..cap.start()]), Some(name) => PartialStep::text(name, cap.as_str()), }; m.append_part(part); prev_end = cap.end(); } } // There might be unmatched text at the end. if prev_end < step_text.len() { let part = PartialStep::uncaptured(&step_text[prev_end..]); m.append_part(part); } Some(m) } } impl PartialEq for Binding { fn eq(&self, other: &Self) -> bool { self.kind == other.kind && self.pattern == other.pattern } } impl Eq for Binding {} #[cfg(test)] mod test_binding { use super::Binding; use crate::PartialStep; use crate::ScenarioStep; use crate::StepKind; #[test] fn creates_new_without_cleanup() { let b = Binding::new(StepKind::Given, "I am Tomjon", "set_name", None).unwrap(); assert_eq!(b.kind(), StepKind::Given); assert!(b.regex().is_match("I am Tomjon")); assert!(!b.regex().is_match("I am Tomjon of Lancre")); assert!(!b.regex().is_match("Hello, I am Tomjon")); assert_eq!(b.function(), "set_name"); assert_eq!(b.cleanup(), None); } #[test] fn creates_new_with_cleanup() { let b = Binding::new( StepKind::Given, "I am Tomjon", "set_name", Some("unset_name"), ) .unwrap(); assert_eq!(b.kind(), StepKind::Given); assert!(b.regex().is_match("I am Tomjon")); assert!(!b.regex().is_match("I am Tomjon of Lancre")); assert!(!b.regex().is_match("Hello, I am Tomjon")); assert_eq!(b.function(), "set_name"); assert_eq!(b.cleanup(), Some("unset_name")); } #[test] fn equal() { let a = Binding::new(StepKind::Given, "I am Tomjon", "set_name", Some("unset")).unwrap(); let b = Binding::new(StepKind::Given, "I am Tomjon", "set_name", Some("unset")).unwrap(); assert_eq!(a, b); } #[test] fn not_equal() { let a = Binding::new(StepKind::Given, "I am Tomjon", "set_name", None).unwrap(); let b = Binding::new(StepKind::Given, "I am Tomjon of Lancre", "set_name", None).unwrap(); assert_ne!(a, b); } #[test] fn does_not_match_with_wrong_kind() { let step = ScenarioStep::new(StepKind::Given, "given", "yo"); let b = Binding::new(StepKind::When, "yo", "do_yo", None).unwrap(); assert!(b.match_with_step(&step).is_none()); } #[test] fn does_not_match_with_wrong_text() { let step = ScenarioStep::new(StepKind::Given, "given", "foo"); let b = Binding::new(StepKind::Given, "bar", "yo", None).unwrap(); assert!(b.match_with_step(&step).is_none()); } #[test] fn match_with_fixed_pattern() { let step = ScenarioStep::new(StepKind::Given, "given", "foo"); let b = Binding::new(StepKind::Given, "foo", "do_foo", None).unwrap(); let m = b.match_with_step(&step).unwrap(); assert_eq!(m.kind(), StepKind::Given); let mut parts = m.parts(); let p = parts.next().unwrap(); assert_eq!(p, &PartialStep::uncaptured("foo")); assert_eq!(parts.next(), None); } #[test] fn match_with_regex() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon, I am"); let b = Binding::new( StepKind::Given, r"I am (?P\S+), I am", "set_name", None, ) .unwrap(); let m = b.match_with_step(&step).unwrap(); assert_eq!(m.kind(), StepKind::Given); let mut parts = m.parts(); assert_eq!(parts.next().unwrap(), &PartialStep::uncaptured("I am ")); assert_eq!(parts.next().unwrap(), &PartialStep::text("who", "Tomjon")); assert_eq!(parts.next().unwrap(), &PartialStep::uncaptured(", I am")); assert_eq!(parts.next(), None); } } /// Set of all known bindings. #[derive(Debug)] pub struct Bindings { bindings: Vec, } impl Default for Bindings { fn default() -> Self { Bindings { bindings: vec![] } } } #[derive(Debug, Deserialize)] struct ParsedBinding { given: Option, when: Option, then: Option, function: String, cleanup: Option, regex: Option, } impl Bindings { /// Create a new, empty set of bindings. pub fn new() -> Bindings { Bindings::default() } /// Return number of bindings in set. pub fn len(&self) -> usize { self.bindings.len() } /// Are there no bindings? pub fn is_empty(&self) -> bool { self.bindings.is_empty() } /// Add a binding to the set. pub fn add(&mut self, binding: Binding) { self.bindings.push(binding); } /// Add bindings from a YAML string pub fn add_from_yaml(&mut self, yaml: &str) -> Result<()> { let bindings: Vec = serde_yaml::from_str(yaml)?; for b in bindings { self.add(from_hashmap(&b)?); } Ok(()) } /// Return slice of all bindings. pub fn bindings(&self) -> &[Binding] { &self.bindings } /// Find the binding matching a given scenario step, if there is /// exactly one. pub fn find(&self, step: &ScenarioStep) -> Result { let mut matches = self .bindings() .iter() .filter_map(|b| b.match_with_step(step)); match matches.next() { None => Err(SubplotError::BindingUnknown(step.to_string())), Some(matched) => match matches.next() { None => Ok(matched), Some(_) => Err(SubplotError::BindingNotUnique(step.to_string())), }, } } /// Add bindings from a file. pub fn add_from_file

(&mut self, filename: P) -> Result<()> where P: AsRef, { let mut f = File::open(filename.as_ref()) .map_err(|e| SubplotError::BindingsFileNotFound(filename.as_ref().into(), e))?; let mut yaml = String::new(); f.read_to_string(&mut yaml)?; self.add_from_yaml(&yaml)?; Ok(()) } /// Is there a binding for a given raw step? pub fn has(&self, kind: StepKind, pattern: &str) -> bool { let m = self .bindings .iter() .filter(|b| b.kind() == kind && b.pattern() == pattern); m.count() == 1 } } fn from_hashmap(parsed: &ParsedBinding) -> Result { let given: i32 = parsed.given.is_some().into(); let when: i32 = parsed.when.is_some().into(); let then: i32 = parsed.then.is_some().into(); if given + when + then == 0 { let msg = format!("{:?}", parsed); return Err(SubplotError::BindingWithoutKnownKeyword(msg)); } if given + when + then > 1 { let msg = format!("{:?}", parsed); return Err(SubplotError::BindingHasManyKeywords(msg)); } let (kind, pattern) = if parsed.given.is_some() { (StepKind::Given, parsed.given.as_ref().unwrap()) } else if parsed.when.is_some() { (StepKind::When, parsed.when.as_ref().unwrap()) } else if parsed.then.is_some() { (StepKind::Then, parsed.then.as_ref().unwrap()) } else { let msg = format!("{:?}", parsed); return Err(SubplotError::BindingWithoutKnownKeyword(msg)); }; let pattern = if parsed.regex.unwrap_or(false) { pattern.to_string() } else { // if we get here parsed.regex is either None or Some(false) regex_from_simple_pattern(pattern, parsed.regex.is_some())? }; Ok(Binding::new( kind, &pattern, &parsed.function, match parsed.cleanup { None => None, Some(ref s) => Some(s), }, )?) } #[cfg(test)] mod test_bindings { use crate::Binding; use crate::Bindings; use crate::PartialStep; use crate::ScenarioStep; use crate::StepKind; use crate::SubplotError; #[test] fn has_no_bindings_initially() { let bindings = Bindings::new(); assert_eq!(bindings.bindings().len(), 0); } #[test] fn adds_binding() { let binding = Binding::new(StepKind::Given, r"I am (?P\S+)", "set_name", None).unwrap(); let mut bindings = Bindings::new(); bindings.add(binding.clone()); assert_eq!(bindings.bindings(), &[binding]); } #[test] fn adds_from_yaml() { let yaml = " - given: I am Tomjon function: set_name - when: I declare myself king function: declare_king - then: there is applause function: check_for_applause "; let mut bindings = Bindings::new(); bindings.add_from_yaml(&yaml).unwrap(); println!("test: {:?}", bindings); assert!(bindings.has(StepKind::Given, "I am Tomjon")); assert!(bindings.has(StepKind::When, "I declare myself king")); assert!(bindings.has(StepKind::Then, "there is applause")); assert_eq!(bindings.len(), 3); } #[test] fn add_from_yaml_notices_multiple_keywords() { let yaml = " - given: I am Tomjon when: I am indeed Tomjon function: set_name "; match Bindings::new().add_from_yaml(&yaml) { Ok(_) => unreachable!(), Err(SubplotError::BindingHasManyKeywords(_)) => (), Err(e) => panic!("Incorrect error: {}", e), } } #[test] fn does_not_find_match_for_unmatching_kind() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); let binding = Binding::new(StepKind::When, r"I am Tomjon", "set_foo", None).unwrap(); let mut bindings = Bindings::new(); bindings.add(binding); assert!(match bindings.find(&step) { Err(SubplotError::BindingUnknown(_)) => true, _ => false, }); } #[test] fn does_not_find_match_for_unmatching_pattern() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); let binding = Binding::new(StepKind::Given, r"I am Tomjon of Lancre", "set_foo", None).unwrap(); let mut bindings = Bindings::new(); bindings.add(binding); assert!(match bindings.find(&step) { Err(SubplotError::BindingUnknown(_)) => true, _ => false, }); } #[test] fn two_matching_bindings() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); let mut bindings = Bindings::default(); bindings.add(Binding::new(StepKind::Given, r"I am Tomjon", "set_foo", None).unwrap()); bindings.add( Binding::new( StepKind::Given, &super::regex_from_simple_pattern(r"I am {name}", false).unwrap(), "set_foo", None, ) .unwrap(), ); assert!(match bindings.find(&step) { Err(SubplotError::BindingNotUnique(_)) => true, _ => false, }); } #[test] fn finds_match_for_fixed_string_pattern() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); let binding = Binding::new(StepKind::Given, r"I am Tomjon", "set_name", None).unwrap(); let mut bindings = Bindings::new(); bindings.add(binding); let m = bindings.find(&step).unwrap(); assert_eq!(m.kind(), StepKind::Given); let mut parts = m.parts(); let p = parts.next().unwrap(); match p { PartialStep::UncapturedText(t) => assert_eq!(t.text(), "I am Tomjon"), _ => panic!("unexpected part: {:?}", p), } assert_eq!(parts.next(), None); } #[test] fn finds_match_for_regexp_pattern() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); let binding = Binding::new(StepKind::Given, r"I am (?P\S+)", "set_name", None).unwrap(); let mut bindings = Bindings::new(); bindings.add(binding); let m = bindings.find(&step).unwrap(); assert_eq!(m.kind(), StepKind::Given); let mut parts = m.parts(); let p = parts.next().unwrap(); match p { PartialStep::UncapturedText(t) => assert_eq!(t.text(), "I am "), _ => panic!("unexpected part: {:?}", p), } let p = parts.next().unwrap(); match p { PartialStep::CapturedText { name, text } => { assert_eq!(name, "name"); assert_eq!(text, "Tomjon"); } _ => panic!("unexpected part: {:?}", p), } assert_eq!(parts.next(), None); } } fn regex_from_simple_pattern(pattern: &str, explicit_plain: bool) -> Result { let pat = Regex::new(r"\{[^\s\{\}]+\}").unwrap(); let mut r = String::new(); let mut end = 0; for m in pat.find_iter(pattern) { let before = &pattern[end..m.start()]; if before.find('{').is_some() || before.find('}').is_some() { return Err(SubplotError::StrayBraceInSimplePattern(pattern.to_string())); } if !explicit_plain && before.chars().any(|c| r"$^*.()+\?|[]".contains(c)) { return Err(SubplotError::SimplePatternHasMetaCharacters( pattern.to_owned(), )); } r.push_str(&escape(before)); let name = &pattern[m.start() + 1..m.end() - 1]; r.push_str(&format!(r"(?P<{}>\S+)", name)); end = m.end(); } let after = &pattern[end..]; if after.find('{').is_some() || after.find('}').is_some() { return Err(SubplotError::StrayBraceInSimplePattern(pattern.to_string())); } if !explicit_plain && after.chars().any(|c| r"$^*.()+\?|[]".contains(c)) { return Err(SubplotError::SimplePatternHasMetaCharacters( pattern.to_owned(), )); } r.push_str(&escape(after)); Ok(r) } #[cfg(test)] mod test_regex_from_simple_pattern { use super::regex_from_simple_pattern; use crate::SubplotError; #[test] fn returns_empty_string_as_is() { let ret = regex_from_simple_pattern("", false).unwrap(); assert_eq!(ret, ""); } #[test] fn returns_boring_pattern_as_is() { let ret = regex_from_simple_pattern("boring", false).unwrap(); assert_eq!(ret, "boring"); } #[test] fn returns_pattern_with_regexp_chars_escaped() { let ret = regex_from_simple_pattern(r".[]*\\", true).unwrap(); assert_eq!(ret, r"\.\[\]\*\\\\"); } #[test] fn returns_simple_pattern_expressed_as_regexp() { let ret = regex_from_simple_pattern("I am {name}", false).unwrap(); assert_eq!(ret, r"I am (?P\S+)"); } #[test] fn returns_error_for_stray_opening_brace() { match regex_from_simple_pattern("{", false) { Err(SubplotError::StrayBraceInSimplePattern(_)) => (), Err(e) => panic!("unexpected error: {}", e), _ => unreachable!(), } } #[test] fn returns_error_for_stray_closing_brace() { match regex_from_simple_pattern("}", false) { Err(SubplotError::StrayBraceInSimplePattern(_)) => (), Err(e) => panic!("unexpected error: {}", e), _ => unreachable!(), } } #[test] fn returns_error_for_stray_opening_brace_before_capture() { match regex_from_simple_pattern("{{foo}", false) { Err(SubplotError::StrayBraceInSimplePattern(_)) => (), Err(e) => panic!("unexpected error: {}", e), _ => unreachable!(), } } #[test] fn returns_error_for_stray_closing_brace_before_capture() { match regex_from_simple_pattern("}{foo}", false) { Err(SubplotError::StrayBraceInSimplePattern(_)) => (), Err(e) => panic!("unexpected error: {}", e), _ => unreachable!(), } } }