diff options
author | Daniel Silverstone <dsilvers@digital-scurf.org> | 2020-12-18 19:16:34 +0000 |
---|---|---|
committer | Daniel Silverstone <dsilvers@digital-scurf.org> | 2020-12-18 19:16:34 +0000 |
commit | 040c049e7ffed04a9b302a6c28b180dd0692d504 (patch) | |
tree | a730ed6fb937af239a73c4e26f2ece51d924e332 /src/bindings.rs | |
parent | 1965b8b9d16bfb0328b4fb96c952b97199165830 (diff) | |
download | subplot-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.rs | 147 |
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(_)) |