#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub mod builder;
#[cfg(feature = "rusqlite")]
mod rusqlite;
mod collect_as_chain;
mod direct_match;
mod list;
pub use self::collect_as_chain::CollectAsChain;
pub use self::list::CriteriumChainList;
use crate::LogicPath;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum CriteriumChain<T> {
And(CriteriumChainList<T>),
Or(CriteriumChainList<T>),
#[cfg_attr(feature = "serde", serde(alias = "not_chain", rename = "not"))]
NotChain(Box<Self>),
#[cfg_attr(feature = "serde", serde(rename = "always"))]
MatchAlways,
#[cfg_attr(feature = "serde", serde(rename = "never"))]
MatchNever,
#[cfg_attr(feature = "serde", serde(rename = "not_match"))]
Not(T),
#[cfg_attr(feature = "serde", serde(untagged))]
Match(T),
#[cfg_attr(feature = "serde", serde(untagged))]
WithLikelihood {
#[cfg_attr(feature = "serde", serde(rename = "match"))]
matcher: Box<Self>,
likelihood: f32,
},
}
impl<T> CriteriumChain<T> {
pub fn invert(self) -> Self {
match self {
CriteriumChain::NotChain(chain) => *chain,
CriteriumChain::Match(c) => CriteriumChain::Not(c),
CriteriumChain::Not(c) => CriteriumChain::Match(c),
CriteriumChain::MatchAlways => CriteriumChain::MatchNever,
CriteriumChain::MatchNever => CriteriumChain::MatchAlways,
chain => CriteriumChain::NotChain(Box::new(chain)),
}
}
pub fn invert_if(self, invert: bool) -> Self {
if invert {
self.invert()
} else {
self
}
}
pub fn with_likelihood(self, likelihood: f32) -> Self {
match self {
Self::WithLikelihood { matcher, .. } => Self::WithLikelihood {
matcher,
likelihood,
},
Self::NotChain(chain) => Self::NotChain(chain.with_likelihood(1.0 - likelihood).into()),
chain => Self::WithLikelihood {
matcher: chain.into(),
likelihood,
},
}
}
pub fn likely(self) -> Self {
self.with_likelihood(0.9375)
}
pub fn unlikely(self) -> Self {
self.with_likelihood(0.0625)
}
pub fn get_known_likelihood(&self) -> Option<f32> {
match self {
Self::WithLikelihood { likelihood, .. } => {
if likelihood.is_nan() {
None
} else {
Some(likelihood.clamp(0.0, 1.0))
}
}
Self::NotChain(chain) => chain.get_known_likelihood().map(|l| 1.0 - l),
Self::MatchAlways => Some(1.0),
Self::MatchNever => Some(0.0),
_ => None,
}
}
pub fn constant(value: bool) -> Self {
value.into()
}
pub fn and(self, criterium: T) -> Self {
self.and_chain(Self::Match(criterium))
}
pub fn and_chain(self, other_chain: Self) -> Self {
match self {
Self::And(mut list) => {
match other_chain {
Self::And(mut other) => {
list.criteria.append(&mut other.criteria);
}
_ => {
list.criteria.push(other_chain);
}
}
Self::And(list)
}
_ => Self::And(CriteriumChainList {
criteria: vec![self, other_chain],
fallback: false,
}),
}
}
pub fn or(self, criterium: T) -> Self {
self.or_chain(Self::Match(criterium))
}
pub fn or_chain(self, other_chain: Self) -> Self {
match self {
Self::Or(mut list) => {
match other_chain {
Self::Or(mut other) => {
list.criteria.append(&mut other.criteria);
}
_ => {
list.criteria.push(other_chain);
}
}
Self::Or(list)
}
_ => Self::Or(CriteriumChainList {
criteria: vec![self, other_chain],
fallback: false,
}),
}
}
pub fn translate<F, O>(self, func: &F) -> CriteriumChain<O>
where
F: Fn(T) -> O,
{
match self {
Self::And(list) => CriteriumChain::And(list.map(|c| c.translate(func))),
Self::Or(list) => CriteriumChain::Or(list.map(|c| c.translate(func))),
Self::NotChain(chain) => CriteriumChain::NotChain(Box::new(chain.translate(func))),
Self::WithLikelihood {
matcher,
likelihood,
} => CriteriumChain::WithLikelihood {
matcher: Box::new(matcher.translate(func)),
likelihood,
},
Self::Not(c) => CriteriumChain::Not(func(c)),
Self::Match(c) => CriteriumChain::Match(func(c)),
Self::MatchAlways => CriteriumChain::MatchAlways,
Self::MatchNever => CriteriumChain::MatchNever,
}
}
pub fn translate_with_structure<F, O>(
self,
func: &F,
path: Option<LogicPath>,
) -> CriteriumChain<O>
where
F: Fn(T, LogicPath) -> O,
{
let path = path.unwrap_or_else(LogicPath::new_root).traverse(&self);
match self {
Self::And(list) => {
CriteriumChain::And(list.map(|c| c.translate_with_structure(func, Some(path))))
}
Self::Or(list) => {
CriteriumChain::Or(list.map(|c| c.translate_with_structure(func, Some(path))))
}
Self::NotChain(chain) => {
CriteriumChain::NotChain(Box::new(chain.translate_with_structure(func, Some(path))))
}
Self::WithLikelihood {
matcher,
likelihood,
} => CriteriumChain::WithLikelihood {
matcher: Box::new(matcher.translate_with_structure(func, Some(path))),
likelihood,
},
Self::Not(c) => CriteriumChain::Not(func(c, path)),
Self::Match(c) => CriteriumChain::Match(func(c, path)),
Self::MatchAlways => CriteriumChain::MatchAlways,
Self::MatchNever => CriteriumChain::MatchNever,
}
}
pub fn translate_to_chain<F, O>(self, func: &F, path: Option<LogicPath>) -> CriteriumChain<O>
where
F: Fn(T, LogicPath) -> CriteriumChain<O>,
{
let path = path.unwrap_or_else(LogicPath::new_root).traverse(&self);
match self {
Self::And(list) => {
CriteriumChain::And(list.map(|c| c.translate_to_chain(func, Some(path))))
}
Self::Or(list) => {
CriteriumChain::Or(list.map(|c| c.translate_to_chain(func, Some(path))))
}
Self::NotChain(chain) => {
CriteriumChain::NotChain(Box::new(chain.translate_to_chain(func, Some(path))))
}
Self::WithLikelihood {
matcher,
likelihood,
} => CriteriumChain::WithLikelihood {
matcher: Box::new(matcher.translate_to_chain(func, Some(path))),
likelihood,
},
Self::Not(c) => func(c, path).invert(),
Self::Match(c) => func(c, path),
Self::MatchAlways => CriteriumChain::MatchAlways,
Self::MatchNever => CriteriumChain::MatchNever,
}
}
pub fn for_each_leaf<F>(&self, func: &mut F, path: Option<LogicPath>)
where
F: FnMut(&T, LogicPath),
{
let path = path.unwrap_or_else(LogicPath::new_root).traverse(self);
match self {
Self::And(list) => list.for_each(|c| c.for_each_leaf(func, Some(path))),
Self::Or(list) => list.for_each(|c| c.for_each_leaf(func, Some(path))),
Self::NotChain(chain) => chain.for_each_leaf(func, Some(path)),
Self::WithLikelihood { matcher, .. } => matcher.for_each_leaf(func, Some(path)),
Self::Not(c) | Self::Match(c) => func(c, path),
Self::MatchAlways | Self::MatchNever => { }
}
}
pub fn evaluate_constant(&self) -> Option<bool> {
match self {
Self::And(list) => {
if list.is_empty() {
return Some(list.fallback);
}
for chain in &list.criteria {
match chain.evaluate_constant() {
None => return None,
Some(true) => { }
Some(false) => return Some(false),
}
}
Some(true)
}
Self::Or(list) => {
if list.is_empty() {
return Some(list.fallback);
}
for chain in &list.criteria {
match chain.evaluate_constant() {
None => return None,
Some(true) => return Some(true),
Some(false) => { }
}
}
return Some(false);
}
Self::NotChain(chain) => chain.evaluate_constant().map(|b| !b),
Self::WithLikelihood { matcher, .. } => matcher.evaluate_constant(),
Self::MatchAlways => Some(true),
Self::MatchNever => Some(false),
Self::Not(_) | Self::Match(_) => None,
}
}
pub fn is_constant(&self) -> bool {
self.evaluate_constant().is_some()
}
pub fn is_empty(&self) -> bool {
match self {
Self::And(list) | Self::Or(list) => list.criteria.is_empty(),
Self::Not(_) | Self::Match(_) => false,
Self::NotChain(chain) => chain.is_empty(),
Self::WithLikelihood { matcher, .. } => matcher.is_empty(),
Self::MatchAlways | Self::MatchNever => false,
}
}
}
impl<T> From<Option<CriteriumChain<T>>> for CriteriumChain<T> {
fn from(chain_opt: Option<CriteriumChain<T>>) -> Self {
match chain_opt {
Some(cc) => cc,
None => CriteriumChain::MatchAlways,
}
}
}
impl<T> From<bool> for CriteriumChain<T> {
fn from(value: bool) -> Self {
match value {
true => CriteriumChain::MatchAlways,
false => CriteriumChain::MatchNever,
}
}
}
pub trait ChainInto<T, F>
where
T: From<F>,
{
fn chain_into(self) -> CriteriumChain<T>;
}
impl<T, F> ChainInto<T, F> for CriteriumChain<F>
where
T: From<F>,
{
fn chain_into(self) -> CriteriumChain<T> {
self.translate(&Into::into)
}
}
#[cfg(all(test, feature = "serde"))]
mod test_serde {
use super::*;
use crate::direct_match::DirectMatch;
use crate::string::StringCriterium;
use crate::CriteriumChainBuilder;
use serde_json;
#[test]
fn deserialize() {
let data = r#"{
"and": [
{ "not": { "equals": "foobar" } },
{ "not": { "equals": "foo" } },
{ "has_prefix": "foo" },
{ "likelihood": 0.3, "match": { "not": { "equals": "bar" } } },
"always",
{ "or": ["always"] }
]
}"#;
let res: CriteriumChain<StringCriterium> = serde_json::from_str(data).unwrap();
assert_eq!(
format!("{:?}",res),
"And(CriteriumChainList { criteria: [NotChain(Match(Equals(\"foobar\"))), NotChain(Match(Equals(\"foo\"))), Match(HasPrefix(\"foo\")), WithLikelihood { matcher: NotChain(Match(Equals(\"bar\"))), likelihood: 0.3 }, MatchAlways, Or(CriteriumChainList { criteria: [MatchAlways], fallback: false })], fallback: false })".to_string()
);
assert_eq!(res.evaluate_constant(), None);
assert!(res.criterium_match("foo_bar"));
assert!(res.criterium_match("foothing"));
assert!(res.criterium_match("foobaz"));
assert!(!res.criterium_match("foobar"));
assert!(!res.criterium_match("foo"));
assert!(!res.criterium_match("fo"));
}
#[test]
fn serialize() {
let mut builder = CriteriumChainBuilder::and(false);
builder.add_criterium_inverted(StringCriterium::Equals("foo".to_string()));
builder.add_criterium(StringCriterium::HasPrefix("foo".to_string()));
builder.add_chain(CriteriumChain::MatchAlways);
builder.add_chain(CriteriumChain::Or(CriteriumChainList::empty(true)));
assert_eq!(
serde_json::to_string(&builder.to_chain()).unwrap(),
r#"{"and":[{"not_match":{"equals":"foo"}},{"has_prefix":"foo"},"always",{"or":["always"]}]}"#.to_string()
);
}
#[test]
fn translate_preserve() {
let data = r#"{
"and": [
{ "not": { "equals": "foobar" } },
{ "not": { "equals": "foo" } },
{ "has_prefix": "foo" },
{ "likelihood": 0.3, "match": { "not": { "equals": "bar" } } },
"always",
{ "or": ["always"] }
]
}"#;
let res: CriteriumChain<StringCriterium> = serde_json::from_str(data).unwrap();
let debugged = "And(CriteriumChainList { criteria: [NotChain(Match(Equals(\"foobar\"))), NotChain(Match(Equals(\"foo\"))), Match(HasPrefix(\"foo\")), WithLikelihood { matcher: NotChain(Match(Equals(\"bar\"))), likelihood: 0.3 }, MatchAlways, Or(CriteriumChainList { criteria: [MatchAlways], fallback: false })], fallback: false })".to_string();
assert_eq!(format!("{:?}", res), debugged);
assert_eq!(format!("{:?}", res.clone().translate(&|t| t)), debugged);
assert_eq!(
format!(
"{:?}",
res.clone().translate_with_structure(&|t, _| t, None)
),
debugged
);
assert_eq!(
format!(
"{:?}",
res.translate_to_chain(&|t, _| CriteriumChain::Match(t), None)
),
debugged
);
}
}