summaryrefslogtreecommitdiff
path: root/src/bindings.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bindings.rs')
-rw-r--r--src/bindings.rs279
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),
}