summaryrefslogtreecommitdiff
path: root/src/bindings.rs
diff options
context:
space:
mode:
authorDaniel Silverstone <dsilvers@digital-scurf.org>2020-12-18 19:16:34 +0000
committerDaniel Silverstone <dsilvers@digital-scurf.org>2020-12-18 19:16:34 +0000
commit040c049e7ffed04a9b302a6c28b180dd0692d504 (patch)
treea730ed6fb937af239a73c4e26f2ece51d924e332 /src/bindings.rs
parent1965b8b9d16bfb0328b4fb96c952b97199165830 (diff)
downloadsubplot-040c049e7ffed04a9b302a6c28b180dd0692d504.tar.gz
bindings: Change typemap to use an enum
Since we're validating and restricting types/kinds to the given set of valid values, we may as well actually enforce that by means of an enumeration. Signed-off-by: Daniel Silverstone <dsilvers@digital-scurf.org>
Diffstat (limited to 'src/bindings.rs')
-rw-r--r--src/bindings.rs147
1 files changed, 108 insertions, 39 deletions
diff --git a/src/bindings.rs b/src/bindings.rs
index e0476c3..5f5bece 100644
--- a/src/bindings.rs
+++ b/src/bindings.rs
@@ -4,7 +4,7 @@ use super::ScenarioStep;
use super::StepKind;
use crate::{Result, SubplotError};
-use serde::Deserialize;
+use serde::{Deserialize, Serialize};
use serde_aux::prelude::*;
use std::collections::HashMap;
@@ -12,10 +12,84 @@ use std::convert::identity;
use std::fs::File;
use std::io::Read;
use std::path::Path;
+use std::str::FromStr;
use lazy_static::lazy_static;
use regex::{escape, Regex, RegexBuilder};
+#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+/// The type of a scenario step capture
+///
+/// Each scenario step is given a particular type, these types are defined by
+/// the binding for the step, either by means of simple patterns, or by
+/// the types map in the binding.
+pub enum CaptureType {
+ /// A word is a sequence of non-whitespace characters and is the default
+ /// capture type.
+ Word,
+
+ /// Text is simply any sequence of characters. Typically used at the end
+ /// of a capture string.
+ Text,
+
+ /// Integers are optionally negative numbers with no decimal part.
+ Int,
+
+ /// Uints are integers with no sign marker permitted.
+ Uint,
+
+ /// Numbers are optionally negative sequences of digits with an optional
+ /// decimal point and subsequent decimal digits.
+ Number,
+
+ /// Files are words which are special in that they have to match a filename
+ /// for one of the embedded files in the document, otherwise codegen will
+ /// refuse to run.
+ File,
+}
+
+impl FromStr for CaptureType {
+ type Err = SubplotError;
+
+ fn from_str(value: &str) -> Result<Self> {
+ match value.to_ascii_lowercase().as_str() {
+ "word" => Ok(Self::Word),
+ "text" => Ok(Self::Text),
+ "int" => Ok(Self::Int),
+ "uint" => Ok(Self::Uint),
+ "number" => Ok(Self::Number),
+ "file" => Ok(Self::File),
+ _ => Err(SubplotError::UnknownTypeInBinding(value.to_string())),
+ }
+ }
+}
+
+impl CaptureType {
+ /// Retrieve the string representation of this capture type
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Word => "word",
+ Self::Text => "text",
+ Self::Int => "int",
+ Self::Uint => "uint",
+ Self::Number => "number",
+ Self::File => "file",
+ }
+ }
+
+ /// Retrieve the regular expression representation of this capture type
+ pub fn regex_str(self) -> &'static str {
+ match self {
+ Self::Word => r"\S+",
+ Self::Text => r".*",
+ Self::Int => r"-?\d+",
+ Self::Uint => r"\d+",
+ Self::Number => r"-?\d+(\.\d+)?",
+ Self::File => r"\S+",
+ }
+ }
+}
/// A binding of a scenario step to its implementation.
///
/// Contains the pattern used to match against scenario steps,
@@ -28,7 +102,7 @@ pub struct Binding {
regex: Regex,
function: String,
cleanup: Option<String>,
- types: HashMap<String, String>,
+ types: HashMap<String, CaptureType>,
}
impl Binding {
@@ -39,7 +113,7 @@ impl Binding {
function: &str,
cleanup: Option<&str>,
case_sensitive: bool,
- mut types: HashMap<String, String>,
+ mut types: HashMap<String, CaptureType>,
) -> Result<Binding> {
let regex = RegexBuilder::new(&format!("^{}$", pattern))
.case_insensitive(!case_sensitive)
@@ -48,10 +122,7 @@ impl Binding {
// If the type is missing from the map, we default to `text` which is
// the .* pattern
for capture in regex.capture_names().filter_map(identity) {
- let tyname = &*types.entry(capture.into()).or_insert_with(|| "text".into());
- if !KIND_PATTERNS.contains_key(tyname.as_str()) {
- return Err(SubplotError::UnknownTypeInBinding(tyname.clone()));
- }
+ types.entry(capture.into()).or_insert(CaptureType::Text);
}
Ok(Binding {
@@ -141,8 +212,8 @@ 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 tyname = self.types.get(name).unwrap();
- let rx = &KIND_PATTERNS.get(tyname.as_str()).unwrap().1;
+ let ty = self.types.get(name).unwrap();
+ let rx = &KIND_PATTERNS.get(ty).unwrap();
if !rx.is_match(cap) {
// This capture doesn't match the kind so it's not
// valid for this binding.
@@ -371,7 +442,7 @@ struct ParsedBinding {
#[serde(default)]
case_sensitive: bool,
#[serde(default)]
- types: HashMap<String, String>,
+ types: HashMap<String, CaptureType>,
}
#[derive(Debug, Deserialize)]
@@ -724,21 +795,21 @@ mod test_bindings {
}
lazy_static! {
- static ref KIND_PATTERNS: HashMap<&'static str, (&'static str, Regex)> = {
+ static ref KIND_PATTERNS: HashMap<CaptureType, Regex> = {
let mut map = HashMap::new();
- for (tyname, pattern) in (&[
- ("file", r"\S+"),
- ("word", r"\S+"),
- ("text", r".*"),
- ("int", r"-?\d+"),
- ("uint", r"\d+"),
- ("number", r"-?\d+(\.\d+)?"),
+ for ty in (&[
+ CaptureType::Word,
+ CaptureType::Text,
+ CaptureType::Int,
+ CaptureType::Uint,
+ CaptureType::Number,
+ CaptureType::File,
]).iter().copied() {
// This Unwrap is okay because we shouldn't have any bugs in the
// regular expressions here, and if we did, it'd be bad for everyone
// and caught in the test suite anyway.
- let rx = Regex::new(&format!("^{}$", pattern)).unwrap();
- map.insert(tyname, (pattern, rx));
+ let rx = Regex::new(&format!("^{}$", ty.regex_str())).unwrap();
+ map.insert(ty, rx);
}
map
};
@@ -747,7 +818,7 @@ lazy_static! {
fn regex_from_simple_pattern(
pattern: &str,
explicit_plain: bool,
- types: &mut HashMap<String, String>,
+ types: &mut HashMap<String, CaptureType>,
) -> Result<String> {
let pat = Regex::new(r"\{[^\s\{\}]+\}").unwrap();
let mut r = String::new();
@@ -768,7 +839,9 @@ fn regex_from_simple_pattern(
let (name, kind) = if let Some(i) = name.find(':') {
let (name, suffix) = name.split_at(i);
assert!(suffix.starts_with(':'));
- (name, Some(&suffix[1..]))
+ let kind = &suffix[1..];
+ let kind = CaptureType::from_str(kind)?;
+ (name, Some(kind))
} else {
(name, None)
};
@@ -776,32 +849,28 @@ fn regex_from_simple_pattern(
let (name, kind) = match (name, kind, types.contains_key(name)) {
(name, Some(kind), false) => {
// There is a kind, but it's not in the map
- types.insert(name.to_string(), kind.to_string());
+ types.insert(name.to_string(), kind);
(name, kind)
}
(name, None, true) => {
// There is no kind, but it is present in the map
- (name, types[name].as_str())
+ (name, types[name])
}
(name, Some(kind), true) => {
// There is a kind and it's in the map, they must match
- if kind != types.get(name).unwrap().as_str() {
+ if kind != *types.get(name).unwrap() {
return Err(SubplotError::SimplePatternKindMismatch(name.to_string()));
}
(name, kind)
}
(name, None, false) => {
// There is no kind, and it's not in the map, so default to word
- types.insert(name.to_string(), "word".to_string());
- (name, "word")
+ types.insert(name.to_string(), CaptureType::Word);
+ (name, CaptureType::Word)
}
};
- if let Some((regex, _)) = KIND_PATTERNS.get(kind) {
- r.push_str(&format!(r"(?P<{}>{})", name, regex));
- } else {
- return Err(SubplotError::UnknownSimplePatternKind(kind.to_string()));
- }
+ r.push_str(&format!(r"(?P<{}>{})", name, kind.regex_str()));
end = m.end();
}
let after = &pattern[end..];
@@ -819,7 +888,7 @@ fn regex_from_simple_pattern(
#[cfg(test)]
mod test_regex_from_simple_pattern {
- use super::regex_from_simple_pattern;
+ use super::{regex_from_simple_pattern, CaptureType};
use crate::SubplotError;
use regex::Regex;
use std::collections::HashMap;
@@ -968,29 +1037,29 @@ mod test_regex_from_simple_pattern {
fn typemap_updated_on_pattern_parse_default() {
let mut types = HashMap::new();
assert!(regex_from_simple_pattern("{foo}", false, &mut types).is_ok());
- assert_eq!(types.get("foo").map(String::as_str), Some("word"));
+ assert!(matches!(types.get("foo"), Some(CaptureType::Word)));
}
#[test]
fn typemap_checked_on_pattern_parse_and_default_agrees() {
let mut types = HashMap::new();
- types.insert("foo".into(), "word".into());
+ types.insert("foo".into(), "word".parse().unwrap());
assert!(regex_from_simple_pattern("{foo}", false, &mut types).is_ok());
assert_eq!(types.len(), 1);
- assert_eq!(types.get("foo").map(String::as_str), Some("word"));
+ assert!(matches!(types.get("foo"), Some(CaptureType::Word)));
}
#[test]
fn typemap_updated_on_pattern_parse_explicit() {
let mut types = HashMap::new();
assert!(regex_from_simple_pattern("{foo:number}", false, &mut types).is_ok());
- assert_eq!(types.get("foo").map(String::as_str), Some("number"));
+ assert!(matches!(types.get("foo"), Some(CaptureType::Number)));
}
#[test]
fn typemap_used_when_kind_not_present() {
let mut types = HashMap::new();
- types.insert("foo".into(), "number".into());
+ types.insert("foo".into(), "number".parse().unwrap());
assert_eq!(
regex_from_simple_pattern("{foo}", false, &mut types).unwrap(),
r"(?P<foo>-?\d+(\.\d+)?)"
@@ -1000,7 +1069,7 @@ mod test_regex_from_simple_pattern {
#[test]
fn typemap_and_pattern_kind_must_match() {
let mut types = HashMap::new();
- types.insert("foo".into(), "number".into());
+ types.insert("foo".into(), "number".parse().unwrap());
assert!(matches!(
regex_from_simple_pattern("{foo:word}", false, &mut types),
Err(SubplotError::SimplePatternKindMismatch(_))