pub(crate) trait RegexEngine: Sized + Send + Sync {
type Error: RegexError;
fn is_match(&self, text: &str) -> Result<bool, Self::Error>;
}
#[derive(Debug)]
pub(crate) enum RegexFailureReason {
FancyRegex(fancy_regex::Error),
Panicked,
}
pub(crate) trait RegexError {
fn into_failure_reason(self) -> RegexFailureReason;
}
#[derive(Debug)]
pub(crate) enum FancyRegexError {
Engine(fancy_regex::Error),
Panicked,
}
impl RegexError for FancyRegexError {
fn into_failure_reason(self) -> RegexFailureReason {
match self {
Self::Engine(e) => RegexFailureReason::FancyRegex(e),
Self::Panicked => RegexFailureReason::Panicked,
}
}
}
impl RegexEngine for fancy_regex::Regex {
type Error = FancyRegexError;
fn is_match(&self, text: &str) -> Result<bool, Self::Error> {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
fancy_regex::Regex::is_match(self, text)
})) {
Ok(Ok(matched)) => Ok(matched),
Ok(Err(e)) => Err(FancyRegexError::Engine(e)),
Err(_) => Err(FancyRegexError::Panicked),
}
}
}
#[derive(Debug)]
pub(crate) struct RegexBackendPanic;
impl RegexError for RegexBackendPanic {
fn into_failure_reason(self) -> RegexFailureReason {
RegexFailureReason::Panicked
}
}
impl RegexEngine for regex::Regex {
type Error = RegexBackendPanic;
fn is_match(&self, text: &str) -> Result<bool, Self::Error> {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
regex::Regex::is_match(self, text)
}))
.map_err(|_| RegexBackendPanic)
}
}
#[derive(Debug)]
pub(crate) enum LiteralMatchError {}
impl RegexError for LiteralMatchError {
fn into_failure_reason(self) -> RegexFailureReason {
match self {}
}
}
pub(crate) enum LiteralMatcher {
Prefix {
literal: String,
},
Exact {
exact: String,
},
Alternation {
alternatives: Vec<String>,
},
NoWhitespace,
}
impl RegexEngine for LiteralMatcher {
type Error = LiteralMatchError;
#[inline]
fn is_match(&self, text: &str) -> Result<bool, Self::Error> {
match self {
Self::Prefix { literal } => Ok(text.starts_with(literal.as_str())),
Self::Exact { exact } => Ok(text == exact.as_str()),
Self::Alternation { alternatives } => {
Ok(alternatives.iter().any(|a| a.as_str() == text))
}
Self::NoWhitespace => Ok(!text.chars().any(is_ecma_whitespace)),
}
}
}
#[derive(Debug, PartialEq)]
pub(crate) enum PatternOptimization {
Prefix(String),
Exact(String),
Alternation(Vec<String>),
NoWhitespace,
}
pub(crate) use jsonschema_regex::is_ecma_whitespace;
pub(crate) fn analyze_pattern(pattern: &str) -> Option<PatternOptimization> {
Some(match jsonschema_regex::analyze_pattern(pattern)? {
jsonschema_regex::PatternAnalysis::Prefix(prefix) => {
PatternOptimization::Prefix(prefix.into_owned())
}
jsonschema_regex::PatternAnalysis::Exact(exact) => {
PatternOptimization::Exact(exact.into_owned())
}
jsonschema_regex::PatternAnalysis::Alternation(alternatives) => {
PatternOptimization::Alternation(alternatives)
}
jsonschema_regex::PatternAnalysis::NoWhitespace => PatternOptimization::NoWhitespace,
})
}
#[cfg(test)]
mod tests {
use super::{analyze_pattern, PatternOptimization};
use test_case::test_case;
#[test_case(r"^\S*$", Some(PatternOptimization::NoWhitespace) ; "no whitespace sentinel")]
#[test_case(
r"^(get|put|post)$",
Some(PatternOptimization::Alternation(vec!["get".into(), "post".into(), "put".into()])) ;
"sorted alternation"
)]
#[test_case(r"^(a|b|c^)$", None ; "invalid char in alternative")]
#[test_case(r"^(x-foo|x-bar)$", Some(PatternOptimization::Alternation(vec!["x-bar".into(), "x-foo".into()])) ; "alternation with dash")]
#[test_case(r"^(single)$", Some(PatternOptimization::Alternation(vec!["single".into()])) ; "single alternative")]
#[allow(clippy::needless_pass_by_value)]
fn test_analyze_pattern_new(pattern: &str, expected: Option<PatternOptimization>) {
assert_eq!(analyze_pattern(pattern), expected);
}
}