use regex::{Regex, RegexSet};
use serde::Deserialize;
use std::convert::TryFrom;
use crate::{
error::{Error, Result},
meal::MealComplete,
tag::Tag,
};
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Rule {
#[serde(default)]
pub name: RegexRule,
#[serde(default)]
pub tag: TagRule,
#[serde(default)]
pub category: RegexRule,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct TagRule {
#[serde(default)]
pub add: Vec<Tag>,
#[serde(default)]
pub sub: Vec<Tag>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(try_from = "RawRegexRule")]
pub struct RegexRule {
pub add: Option<RegexSet>,
pub sub: Option<RegexSet>,
}
#[derive(Debug, Clone, Default, Deserialize)]
struct RawRegexRule {
#[serde(default)]
pub add: Vec<String>,
#[serde(default)]
pub sub: Vec<String>,
}
impl Rule {
pub fn is_match(&self, meal: &MealComplete) -> bool {
let all_adds_empty =
self.tag.is_empty_add() && self.category.is_empty_add() && self.name.is_empty_add();
let any_add = self.tag.is_match_add(meal)
|| self.category.is_match_add(meal)
|| self.name.is_match_add(meal);
let any_sub = self.tag.is_match_sub(meal)
|| self.category.is_match_sub(meal)
|| self.name.is_match_sub(meal);
(all_adds_empty || any_add) && !any_sub
}
pub fn is_non_empty_match(&self, meal: &MealComplete) -> bool {
!self.is_empty() && self.is_match(meal)
}
pub fn joined(self, other: Self) -> Self {
Self {
name: self.name.joined(other.name),
tag: self.tag.joined(other.tag),
category: self.category.joined(other.category),
}
}
pub fn is_empty(&self) -> bool {
self.name.is_empty() && self.tag.is_empty() && self.category.is_empty()
}
}
impl TagRule {
pub fn is_empty(&self) -> bool {
self.add.is_empty() && self.sub.is_empty()
}
fn is_match_add(&self, meal: &MealComplete) -> bool {
self.add.iter().any(|tag| meal.meta.tags.contains(tag))
}
fn is_match_sub(&self, meal: &MealComplete) -> bool {
self.sub.iter().any(|tag| meal.meta.tags.contains(tag))
}
fn is_empty_add(&self) -> bool {
self.add.is_empty()
}
fn joined(mut self, other: Self) -> Self {
self.add.extend(other.add);
self.sub.extend(other.sub);
self
}
}
impl RegexRule {
pub fn from_arg_parts(add: &[Regex], sub: &[Regex]) -> Self {
let add: Vec<_> = add.iter().map(|re| re.as_str().to_owned()).collect();
let sub: Vec<_> = sub.iter().map(|re| re.as_str().to_owned()).collect();
let add = if add.is_empty() {
None
} else {
Some(RegexSet::new(&add).unwrap())
};
let sub = if sub.is_empty() {
None
} else {
Some(RegexSet::new(&sub).unwrap())
};
Self { add, sub }
}
pub fn is_empty(&self) -> bool {
self.add.is_none() && self.sub.is_none()
}
fn is_match_add(&self, meal: &MealComplete) -> bool {
match self.add {
Some(ref rset) => rset.is_match(&meal.meta.category),
None => false,
}
}
fn is_match_sub(&self, meal: &MealComplete) -> bool {
match self.sub {
Some(ref rset) => rset.is_match(&meal.meta.category),
None => false,
}
}
fn is_empty_add(&self) -> bool {
self.add.is_none()
}
fn joined(self, other: RegexRule) -> RegexRule {
let option_and = |this: Option<RegexSet>, other: Option<RegexSet>| {
match (this, other) {
(Some(this), Some(other)) => {
let mut patterns = this.patterns().to_vec();
patterns.extend(other.patterns().to_vec());
Some(RegexSet::new(patterns).unwrap())
}
(Some(this), None) => Some(this),
(None, Some(other)) => Some(other),
(None, None) => None,
}
};
Self {
add: option_and(self.add, other.add),
sub: option_and(self.sub, other.sub),
}
}
}
impl TryFrom<RawRegexRule> for RegexRule {
type Error = Error;
fn try_from(raw: RawRegexRule) -> Result<Self> {
let add = slice_to_option(
&raw.add,
RegexSet::new(&raw.add).map_err(Error::ParsingFilterRegex)?,
);
let sub = slice_to_option(
&raw.sub,
RegexSet::new(&raw.sub).map_err(Error::ParsingFilterRegex)?,
);
Ok(Self { add, sub })
}
}
fn slice_to_option<T, V>(vec: &[T], val: V) -> Option<V> {
if vec.is_empty() {
None
} else {
Some(val)
}
}