#![warn(missing_docs)]
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum QExpr {
Term(Term),
Phrase(Phrase),
Near(Near),
And(Vec<QExpr>),
Or(Vec<QExpr>),
Not(Box<QExpr>),
Field(FieldName, Box<QExpr>),
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Term(pub String);
impl Term {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn is_blank(&self) -> bool {
self.0.trim().is_empty()
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Phrase {
pub terms: Vec<Term>,
}
impl Phrase {
pub fn new(terms: Vec<Term>) -> Self {
Self { terms }
}
pub fn is_blank(&self) -> bool {
self.terms.is_empty() || self.terms.iter().all(|t| t.is_blank())
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Near {
pub terms: Vec<Term>,
pub window: u32,
pub ordered: bool,
}
impl Near {
pub fn new(terms: Vec<Term>, window: u32, ordered: bool) -> Self {
Self {
terms,
window,
ordered,
}
}
pub fn is_blank(&self) -> bool {
self.terms.len() < 2 || self.terms.iter().all(|t| t.is_blank()) || self.window == 0
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FieldName(pub String);
impl FieldName {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn is_blank(&self) -> bool {
self.0.trim().is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidateError {
BlankTerm,
BlankPhrase,
BlankNear,
EmptyJunction,
BlankFieldName,
}
pub fn validate(expr: &QExpr) -> Result<(), ValidateError> {
match expr {
QExpr::Term(t) => {
if t.is_blank() {
Err(ValidateError::BlankTerm)
} else {
Ok(())
}
}
QExpr::Phrase(p) => {
if p.is_blank() {
Err(ValidateError::BlankPhrase)
} else {
Ok(())
}
}
QExpr::Near(n) => {
if n.is_blank() {
Err(ValidateError::BlankNear)
} else {
Ok(())
}
}
QExpr::And(xs) | QExpr::Or(xs) => {
if xs.is_empty() {
return Err(ValidateError::EmptyJunction);
}
for x in xs {
validate(x)?;
}
Ok(())
}
QExpr::Not(x) => validate(x),
QExpr::Field(name, inner) => {
if name.is_blank() {
return Err(ValidateError::BlankFieldName);
}
validate(inner)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_rejects_blank_term() {
assert_eq!(
validate(&QExpr::Term(Term::new(" "))).unwrap_err(),
ValidateError::BlankTerm
);
}
#[test]
fn validate_rejects_blank_phrase() {
assert_eq!(
validate(&QExpr::Phrase(Phrase::new(vec![]))).unwrap_err(),
ValidateError::BlankPhrase
);
assert_eq!(
validate(&QExpr::Phrase(Phrase::new(vec![Term::new(" ")]))).unwrap_err(),
ValidateError::BlankPhrase
);
}
#[test]
fn validate_rejects_blank_near() {
assert_eq!(
validate(&QExpr::Near(Near::new(
vec![Term::new("a"), Term::new("b")],
0,
false
)))
.unwrap_err(),
ValidateError::BlankNear
);
assert_eq!(
validate(&QExpr::Near(Near::new(vec![Term::new("a")], 5, false))).unwrap_err(),
ValidateError::BlankNear
);
}
#[test]
fn validate_rejects_empty_junctions() {
assert_eq!(
validate(&QExpr::And(vec![])).unwrap_err(),
ValidateError::EmptyJunction
);
assert_eq!(
validate(&QExpr::Or(vec![])).unwrap_err(),
ValidateError::EmptyJunction
);
}
#[test]
fn validate_rejects_blank_field_name() {
let q = QExpr::Field(FieldName::new(" "), Box::new(QExpr::Term(Term::new("x"))));
assert_eq!(validate(&q).unwrap_err(), ValidateError::BlankFieldName);
}
#[test]
fn validate_accepts_non_blank_tree() {
let q = QExpr::And(vec![
QExpr::Term(Term::new("alpha")),
QExpr::Phrase(Phrase::new(vec![Term::new("new"), Term::new("york")])),
QExpr::Phrase(Phrase::new(vec![Term::new(" "), Term::new("x")])),
QExpr::Near(Near::new(
vec![Term::new("deep"), Term::new("learning")],
5,
false,
)),
]);
validate(&q).unwrap();
}
}