diff options
Diffstat (limited to 'src/bindings.rs')
-rw-r--r-- | src/bindings.rs | 279 |
1 files changed, 224 insertions, 55 deletions
diff --git a/src/bindings.rs b/src/bindings.rs index 98379c9..825b895 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -3,6 +3,7 @@ use super::MatchedSteps; use super::PartialStep; use super::ScenarioStep; use super::StepKind; +use crate::Warning; use crate::{resource, SubplotError}; use serde::{Deserialize, Serialize}; @@ -166,6 +167,8 @@ pub struct Binding { regex: Regex, impls: HashMap<String, Arc<BindingImpl>>, types: HashMap<String, CaptureType>, + doc: Option<String>, + filename: Arc<Path>, } impl Binding { @@ -174,18 +177,14 @@ impl Binding { kind: StepKind, pattern: &str, case_sensitive: bool, - mut types: HashMap<String, CaptureType>, + types: HashMap<String, CaptureType>, + doc: Option<String>, + filename: Arc<Path>, ) -> Result<Binding, SubplotError> { - let regex = RegexBuilder::new(&format!("^{}$", pattern)) + let regex = RegexBuilder::new(&format!("^{pattern}$")) .case_insensitive(!case_sensitive) .build() .map_err(|err| SubplotError::Regex(pattern.to_string(), err))?; - // For every named capture, ensure we have a known type for it. - // If the type is missing from the map, we default to `text` which is - // the .* pattern - for capture in regex.capture_names().flatten() { - types.entry(capture.into()).or_insert(CaptureType::Text); - } Ok(Binding { kind, @@ -193,6 +192,8 @@ impl Binding { regex, impls: HashMap::new(), types, + doc, + filename, }) } @@ -214,6 +215,11 @@ impl Binding { &self.pattern } + /// Return documentation string for binding, if any. + pub fn doc(&self) -> Option<&str> { + self.doc.as_deref() + } + /// Retrieve a particular implementation by name pub fn step_impl(&self, template: &str) -> Option<Arc<BindingImpl>> { self.impls.get(template).cloned() @@ -242,7 +248,7 @@ impl Binding { let caps = self.regex.captures(step_text)?; // If there is only one capture, it's the whole string. - let mut m = MatchedStep::new(self, template); + let mut m = MatchedStep::new(self, template, step.origin().clone()); if caps.len() == 1 { m.append_part(PartialStep::uncaptured(step_text)); return Some(m); @@ -275,14 +281,14 @@ impl Binding { let cap = cap.as_str(); // These unwraps are safe because we ensured the map is complete // in the constructor, and that all the types are known. - let ty = self.types.get(name).unwrap(); - let rx = &KIND_PATTERNS.get(ty).unwrap(); + let kind = self.types.get(name).copied().unwrap_or(CaptureType::Text); + let rx = KIND_PATTERNS.get(&kind).unwrap(); if !rx.is_match(cap) { // This capture doesn't match the kind so it's not // valid for this binding. return None; } - PartialStep::text(name, cap) + PartialStep::text(name, cap, kind) } }; @@ -298,6 +304,39 @@ impl Binding { Some(m) } + + fn filename(&self) -> &Path { + &self.filename + } + + fn check(&self, warnings: &mut crate::Warnings) -> Result<(), SubplotError> { + fn nth(i: usize) -> &'static str { + match i % 10 { + 1 => "st", + 2 => "nd", + 3 => "rd", + _ => "th", + } + } + for (nr, capture) in self.regex.capture_names().enumerate().skip(1) { + if let Some(name) = capture { + if !self.types.contains_key(name) { + warnings.push(Warning::MissingCaptureType( + self.filename().to_owned(), + format!("{}: {}", self.kind(), self.pattern()), + name.to_string(), + )); + } + } else { + warnings.push(Warning::MissingCaptureName( + self.filename().to_owned(), + format!("{}: {}", self.kind(), self.pattern()), + format!("{nr}{}", nth(nr)), + )); + } + } + Ok(()) + } } impl PartialEq for Binding { @@ -310,15 +349,31 @@ impl Eq for Binding {} #[cfg(test)] mod test_binding { - use super::Binding; + use super::{Binding, CaptureType}; + use crate::html::Location; use crate::PartialStep; use crate::ScenarioStep; use crate::StepKind; use std::collections::HashMap; + use std::path::Path; + use std::path::PathBuf; + use std::sync::Arc; + + fn path() -> Arc<Path> { + PathBuf::new().into() + } #[test] fn creates_new() { - let b = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap(); + let b = Binding::new( + StepKind::Given, + "I am Tomjon", + false, + HashMap::new(), + None, + path(), + ) + .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")); @@ -327,19 +382,45 @@ mod test_binding { #[test] fn equal() { - let a = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap(); - let b = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap(); + let a = Binding::new( + StepKind::Given, + "I am Tomjon", + false, + HashMap::new(), + None, + path(), + ) + .unwrap(); + let b = Binding::new( + StepKind::Given, + "I am Tomjon", + false, + HashMap::new(), + None, + path(), + ) + .unwrap(); assert_eq!(a, b); } #[test] fn not_equal() { - let a = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap(); + let a = Binding::new( + StepKind::Given, + "I am Tomjon", + false, + HashMap::new(), + None, + path(), + ) + .unwrap(); let b = Binding::new( StepKind::Given, "I am Tomjon of Lancre", false, HashMap::new(), + None, + path(), ) .unwrap(); assert_ne!(a, b); @@ -347,22 +428,22 @@ mod test_binding { #[test] fn does_not_match_with_wrong_kind() { - let step = ScenarioStep::new(StepKind::Given, "given", "yo"); - let b = Binding::new(StepKind::When, "yo", false, HashMap::new()).unwrap(); + let step = ScenarioStep::new(StepKind::Given, "given", "yo", Location::Unknown); + let b = Binding::new(StepKind::When, "yo", false, HashMap::new(), None, path()).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", false, HashMap::new()).unwrap(); + let step = ScenarioStep::new(StepKind::Given, "given", "foo", Location::Unknown); + let b = Binding::new(StepKind::Given, "bar", false, HashMap::new(), None, path()).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", false, HashMap::new()).unwrap(); + let step = ScenarioStep::new(StepKind::Given, "given", "foo", Location::Unknown); + let b = Binding::new(StepKind::Given, "foo", false, HashMap::new(), None, path()).unwrap(); let m = b.match_with_step("", &step).unwrap(); assert_eq!(m.kind(), StepKind::Given); let mut parts = m.parts(); @@ -373,29 +454,55 @@ mod test_binding { #[test] fn match_with_regex() { - let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon, I am"); + let step = ScenarioStep::new( + StepKind::Given, + "given", + "I am Tomjon, I am", + Location::Unknown, + ); let b = Binding::new( StepKind::Given, r"I am (?P<who>\S+), I am", false, HashMap::new(), + None, + path(), ) .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::text("who", "Tomjon", CaptureType::Text) + ); assert_eq!(parts.next().unwrap(), &PartialStep::uncaptured(", I am")); assert_eq!(parts.next(), None); } #[test] fn case_sensitive_mismatch() { - let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); - let b = Binding::new(StepKind::Given, r"i am tomjon", false, HashMap::new()).unwrap(); + let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown); + let b = Binding::new( + StepKind::Given, + r"i am tomjon", + false, + HashMap::new(), + None, + path(), + ) + .unwrap(); assert!(b.match_with_step("", &step).is_some()); - let b = Binding::new(StepKind::Given, r"i am tomjon", true, HashMap::new()).unwrap(); + let b = Binding::new( + StepKind::Given, + r"i am tomjon", + true, + HashMap::new(), + None, + path(), + ) + .unwrap(); assert!(b.match_with_step("", &step).is_none()); } } @@ -407,13 +514,14 @@ pub struct Bindings { } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct ParsedImpl { function: String, cleanup: Option<String>, } #[derive(Debug, Deserialize)] -#[serde(transparent)] +#[serde(transparent, deny_unknown_fields)] struct ParsedImplWrapper { #[serde(deserialize_with = "deserialize_struct_case_insensitive")] pimpl: ParsedImpl, @@ -428,6 +536,7 @@ impl Deref for ParsedImplWrapper { } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct ParsedBinding { given: Option<String>, when: Option<String>, @@ -439,10 +548,11 @@ struct ParsedBinding { case_sensitive: bool, #[serde(default)] types: HashMap<String, CaptureType>, + doc: Option<String>, } #[derive(Debug, Deserialize)] -#[serde(transparent)] +#[serde(transparent, deny_unknown_fields)] struct ParsedBindingWrapper { #[serde(deserialize_with = "deserialize_struct_case_insensitive")] binding: ParsedBinding, @@ -470,11 +580,11 @@ impl Bindings { } /// Add bindings from a YAML string - pub fn add_from_yaml(&mut self, yaml: &str) -> Result<(), SubplotError> { + pub fn add_from_yaml(&mut self, yaml: &str, filename: Arc<Path>) -> Result<(), SubplotError> { let bindings: Vec<ParsedBindingWrapper> = serde_yaml::from_str(yaml).map_err(SubplotError::Metadata)?; for wrapper in bindings { - self.add(from_hashmap(&wrapper.binding)?); + self.add(from_hashmap(&wrapper.binding, Arc::clone(&filename))?); } Ok(()) } @@ -516,12 +626,12 @@ impl Bindings { where P: AsRef<Path> + Debug, { - let yaml = resource::read_as_string(filename.as_ref(), template) - .map_err(|e| SubplotError::BindingsFileNotFound(filename.as_ref().into(), e))?; + let filename = filename.as_ref(); + let yaml = resource::read_as_string(filename, template) + .map_err(|e| SubplotError::BindingsFileNotFound(filename.into(), e))?; trace!("Loaded file content"); - self.add_from_yaml(&yaml).map_err(|e| { - SubplotError::BindingFileParseError(filename.as_ref().to_owned(), Box::new(e)) - })?; + self.add_from_yaml(&yaml, filename.to_owned().into()) + .map_err(|e| SubplotError::BindingFileParseError(filename.to_owned(), Box::new(e)))?; Ok(()) } @@ -533,20 +643,28 @@ impl Bindings { .filter(|b| b.kind() == kind && b.pattern() == pattern); m.count() == 1 } + + /// Check these bindings for any warnings which users might need to know about + pub fn check(&self, warnings: &mut crate::Warnings) -> Result<(), SubplotError> { + for binding in self.bindings() { + binding.check(warnings)?; + } + Ok(()) + } } -fn from_hashmap(parsed: &ParsedBinding) -> Result<Binding, SubplotError> { +fn from_hashmap(parsed: &ParsedBinding, filename: Arc<Path>) -> Result<Binding, SubplotError> { 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); + let msg = format!("{parsed:?}"); return Err(SubplotError::BindingWithoutKnownKeyword(msg)); } if given + when + then > 1 { - let msg = format!("{:?}", parsed); + let msg = format!("{parsed:?}"); return Err(SubplotError::BindingHasManyKeywords(msg)); } @@ -557,7 +675,7 @@ fn from_hashmap(parsed: &ParsedBinding) -> Result<Binding, SubplotError> { } else if parsed.then.is_some() { (StepKind::Then, parsed.then.as_ref().unwrap()) } else { - let msg = format!("{:?}", parsed); + let msg = format!("{parsed:?}"); return Err(SubplotError::BindingWithoutKnownKeyword(msg)); }; @@ -572,7 +690,14 @@ fn from_hashmap(parsed: &ParsedBinding) -> Result<Binding, SubplotError> { trace!("Successfully acquired binding"); - let mut ret = Binding::new(kind, &pattern, parsed.case_sensitive, types)?; + let mut ret = Binding::new( + kind, + &pattern, + parsed.case_sensitive, + types, + parsed.doc.clone(), + filename, + )?; trace!("Binding parsed OK"); for (template, pimpl) in &parsed.impls { ret.add_impl(template, &pimpl.function, pimpl.cleanup.as_deref()); @@ -583,6 +708,8 @@ fn from_hashmap(parsed: &ParsedBinding) -> Result<Binding, SubplotError> { #[cfg(test)] mod test_bindings { + use crate::bindings::CaptureType; + use crate::html::Location; use crate::Binding; use crate::Bindings; use crate::PartialStep; @@ -591,6 +718,13 @@ mod test_bindings { use crate::SubplotError; use std::collections::HashMap; + use std::path::Path; + use std::path::PathBuf; + use std::sync::Arc; + + fn path() -> Arc<Path> { + PathBuf::new().into() + } #[test] fn has_no_bindings_initially() { @@ -605,6 +739,8 @@ mod test_bindings { r"I am (?P<name>\S+)", false, HashMap::new(), + None, + path(), ) .unwrap(); let mut bindings = Bindings::new(); @@ -640,8 +776,8 @@ mod test_bindings { total: word "; let mut bindings = Bindings::new(); - bindings.add_from_yaml(yaml).unwrap(); - println!("test: {:?}", bindings); + bindings.add_from_yaml(yaml, path()).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")); @@ -660,7 +796,7 @@ mod test_bindings { python: FUNCTION: set_name "; - match Bindings::new().add_from_yaml(yaml) { + match Bindings::new().add_from_yaml(yaml, path()) { Ok(_) => unreachable!(), Err(SubplotError::BindingHasManyKeywords(_)) => (), Err(e) => panic!("Incorrect error: {}", e), @@ -677,7 +813,7 @@ mod test_bindings { types: age: number "; - match Bindings::new().add_from_yaml(yaml) { + match Bindings::new().add_from_yaml(yaml, path()) { Ok(_) => unreachable!(), Err(SubplotError::SimplePatternKindMismatch(_)) => (), Err(e) => panic!("Incorrect error: {}", e), @@ -686,8 +822,16 @@ mod test_bindings { #[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", false, HashMap::new()).unwrap(); + let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown); + let binding = Binding::new( + StepKind::When, + r"I am Tomjon", + false, + HashMap::new(), + None, + path(), + ) + .unwrap(); let mut bindings = Bindings::new(); bindings.add(binding); assert!(matches!( @@ -698,12 +842,14 @@ mod test_bindings { #[test] fn does_not_find_match_for_unmatching_pattern() { - let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); + let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown); let binding = Binding::new( StepKind::Given, r"I am Tomjon of Lancre", false, HashMap::new(), + None, + path(), ) .unwrap(); let mut bindings = Bindings::new(); @@ -716,9 +862,19 @@ mod test_bindings { #[test] fn two_matching_bindings() { - let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); + let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown); let mut bindings = Bindings::default(); - bindings.add(Binding::new(StepKind::Given, r"I am Tomjon", false, HashMap::new()).unwrap()); + bindings.add( + Binding::new( + StepKind::Given, + r"I am Tomjon", + false, + HashMap::new(), + None, + path(), + ) + .unwrap(), + ); bindings.add( Binding::new( StepKind::Given, @@ -726,6 +882,8 @@ mod test_bindings { .unwrap(), false, HashMap::new(), + None, + path(), ) .unwrap(), ); @@ -737,8 +895,16 @@ mod test_bindings { #[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", false, HashMap::new()).unwrap(); + let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown); + let binding = Binding::new( + StepKind::Given, + r"I am Tomjon", + false, + HashMap::new(), + None, + path(), + ) + .unwrap(); let mut bindings = Bindings::new(); bindings.add(binding); let m = bindings.find("", &step).unwrap(); @@ -754,12 +920,14 @@ mod test_bindings { #[test] fn finds_match_for_regexp_pattern() { - let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); + let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown); let binding = Binding::new( StepKind::Given, r"I am (?P<name>\S+)", false, HashMap::new(), + None, + path(), ) .unwrap(); let mut bindings = Bindings::new(); @@ -774,9 +942,10 @@ mod test_bindings { } let p = parts.next().unwrap(); match p { - PartialStep::CapturedText { name, text } => { + PartialStep::CapturedText { name, text, kind } => { assert_eq!(name, "name"); assert_eq!(text, "Tomjon"); + assert_eq!(kind, &CaptureType::Text); } _ => panic!("unexpected part: {:?}", p), } |