#![no_std]
extern crate alloc;
#[cfg(feature = "serde")]
use alloc::borrow::ToOwned;
use alloc::string::String;
#[cfg(feature = "serde")]
use core::fmt;
use core::{
hash::{Hash, Hasher},
ops::Deref,
};
use regex::Regex;
#[cfg(feature = "serde")]
use regex::RegexBuilder;
#[cfg(feature = "serde")]
use serde::{
de::{Error, Unexpected, Visitor},
Deserialize, Deserializer,
};
#[derive(Clone, Debug)]
pub enum Matchable {
Str(String),
Regex(Regex),
}
impl Matchable {
#[inline]
pub fn is_match(&self, text: impl AsRef<str>) -> bool {
let text = text.as_ref();
match self {
Self::Str(str) => str == text,
Self::Regex(regex) => regex.is_match(text),
}
}
#[inline]
pub fn as_str(&self) -> &str {
match self {
Self::Str(str) => str,
Self::Regex(regex) => regex.as_str(),
}
}
}
impl Default for Matchable {
fn default() -> Self {
Self::Str(Default::default())
}
}
impl Hash for Matchable {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Self::Str(str) => str.hash(state),
Self::Regex(regex) => regex.as_str().hash(state),
};
}
}
impl PartialEq for Matchable {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Matchable::Str(a), Matchable::Str(b)) => a == b,
(Matchable::Regex(a), Matchable::Regex(b)) => a.as_str() == b.as_str(),
_ => false,
}
}
}
impl Eq for Matchable {}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Matchable {
fn deserialize<D>(deserializer: D) -> Result<Matchable, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_string(MatchableVisitor)
}
}
#[cfg(feature = "serde")]
struct MatchableVisitor;
#[cfg(feature = "serde")]
impl<'de> Visitor<'de> for MatchableVisitor {
type Value = Matchable;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(
formatter,
"a normal string or a string that represents a regex"
)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
if let Some((regex, flags)) = extract_regex(v) {
build_regex(regex, flags)
.map(Matchable::Regex)
.map_err(|_| E::invalid_value(Unexpected::Str(regex), &"a valid regex"))
} else {
Ok(Matchable::Str(v.to_owned()))
}
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: Error,
{
if let Some((regex, flags)) = extract_regex(&v) {
build_regex(regex, flags)
.map(Matchable::Regex)
.map_err(|_| E::invalid_value(Unexpected::Str(regex), &"a valid regex"))
} else {
Ok(Matchable::Str(v))
}
}
}
#[derive(Clone, Debug)]
pub struct RegexOnly(Regex);
impl Deref for RegexOnly {
type Target = Regex;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for RegexOnly {
fn deserialize<D>(deserializer: D) -> Result<RegexOnly, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_string(RegexOnlyVisitor)
}
}
#[cfg(feature = "serde")]
struct RegexOnlyVisitor;
#[cfg(feature = "serde")]
impl<'de> Visitor<'de> for RegexOnlyVisitor {
type Value = RegexOnly;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a string that represents a regex")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
Regex::new(v)
.map(RegexOnly)
.map_err(|_| E::invalid_value(Unexpected::Str(v), &"a valid regex"))
}
}
#[cfg(feature = "serde")]
fn extract_regex(s: &str) -> Option<(&str, &str)> {
s.strip_prefix('/').and_then(|s| s.rsplit_once('/'))
}
#[cfg(feature = "serde")]
fn build_regex(regex: &str, flags: &str) -> Result<Regex, regex::Error> {
let mut builder = RegexBuilder::new(regex);
builder.case_insensitive(flags.contains('i'));
builder.multi_line(flags.contains('m'));
builder.dot_matches_new_line(flags.contains('s'));
builder.build()
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn test_matchable_is_match() {
let matchable = Matchable::Str(String::from("abc"));
assert!(matchable.is_match("abc"));
assert!(!matchable.is_match("abd"));
let matchable = Matchable::Regex(Regex::new("\\d+").unwrap());
assert!(matchable.is_match("123"));
assert!(!matchable.is_match("abc"));
}
#[test]
fn test_str() {
let matchable = serde_json::from_str(r#""abc""#).unwrap();
assert!(matches!(matchable, Matchable::Str(s) if s == "abc"));
}
#[test]
fn test_regex() {
let matchable = serde_json::from_str(r#""/\\d+/""#).unwrap();
assert!(matches!(&matchable, Matchable::Regex(regex) if regex.is_match("123")));
let matchable = serde_json::from_str(r#""/[ab]/i""#).unwrap();
assert!(matches!(&matchable, Matchable::Regex(regex) if regex.is_match("a")));
assert!(matches!(&matchable, Matchable::Regex(regex) if regex.is_match("B")));
let matchable = serde_json::from_str(r#""/./s""#).unwrap();
assert!(matches!(&matchable, Matchable::Regex(regex) if regex.is_match("a")));
assert!(matches!(&matchable, Matchable::Regex(regex) if regex.is_match("\n")));
let error = serde_json::from_str::<Matchable>(r#""/(/""#).unwrap_err();
assert!(error.to_string().contains("expected a valid regex"));
}
#[test]
fn test_regex_only() {
let regex = serde_json::from_str::<RegexOnly>(r#""\\d+""#).unwrap();
assert!(regex.is_match("123"));
assert!(!regex.is_match("abc"));
let regex = serde_json::from_str::<RegexOnly>(r#""/[ab]/i""#).unwrap();
assert!(regex.is_match("/a/i"));
assert!(regex.is_match("/b/i"));
assert!(!regex.is_match("A"));
assert!(!regex.is_match("B"));
let error = serde_json::from_str::<RegexOnly>(r#""(""#).unwrap_err();
assert!(error.to_string().contains("expected a valid regex"));
}
}