rsolr 0.3.2

A Solr client for Rust.
Documentation
//! You can build Solr queries with this module.
//! For easy integration queries should be rendered to str at the end, if you want to use in the client.
//! Currently it's rather cumbersome, on it.
//!
//! ```rust
//!
//! use rsolr::query::{Query, Range, Term, Stringable, Date};
//! fn query() -> Query {
//!     Query::from_term(
//!         Term::from_str("simple").boost(2.3).tilde(3).required()
//!         )
//!         .term(Term::from_str("next to the first term").in_field("with_field_specification"))
//!         .and()
//!         .subquery(
//!             Query::from_term(
//!                 Term::from_str(&Range::inclusive("1", "1000").as_str()).in_field("popularity")
//!                 )
//!                 .or()
//!                 .term(Term::from_str(&Range::inclusive(&Date::new("NOW").minus(&Date::month(2)).as_str(), "NOW").as_str()).in_field("created"))
//!         )
//! }
//! ```
//!
//!

pub trait Stringable {
    fn as_str(&self) -> String;
    fn is_query(&self) -> bool;
}

pub struct And {}
impl Stringable for And {
    fn as_str(&self) -> String {
        "AND".to_owned()
    }

    fn is_query(&self) -> bool {
        false
    }
}

pub struct Or {}
impl Stringable for Or {
    fn as_str(&self) -> String {
        "OR".to_owned()
    }

    fn is_query(&self) -> bool {
        false
    }
}

pub struct Query {
    parts: Vec<Box<dyn Stringable>>
}

impl Query {

    pub fn from_str(str: &str) -> Self {
        let mut parts: Vec<Box<dyn Stringable>> = Vec::new();
        parts.push(Box::new(Term::from_str(str)));
        Query { parts }
    }

    pub fn from_term(term: Term) -> Self {
        let mut parts: Vec<Box<dyn Stringable>> = Vec::new();
        parts.push(Box::new(term));
        Query { parts }
    }

    pub fn term(mut self, term: Term) -> Self {
        self.parts.push(Box::new(term));
        self
    }

    pub fn and(mut self) -> Self {
        self.parts.push(Box::new(And {}));
        self
    }

    pub fn or(mut self) -> Self {
        self.parts.push(Box::new(Or {}));
        self
    }

    pub fn subquery(mut self, query: Query) -> Self {
        self.parts.push(Box::new(query));
        self
    }

}

impl Stringable for Query {
    fn as_str(&self) -> String {
        let mut query = "".to_owned();
        for (_, item) in self.parts.iter().enumerate() {
            query = match query.len() {
                0 => item.as_str(),
                _ => {
                    match item.is_query() {
                        true => format!("{} ({})", query, item.as_str()),
                        false => format!("{} {}", query, item.as_str())
                    }
                }
            }
        }
        query
    }

    fn is_query(&self) -> bool {
        true
    }
}

pub struct Term {
    term: String,
    field: Option<String>
}

impl Term {

    pub fn from_str(term_str: &str) -> Self {
        if term_str.find(" ") != None {
            return Term{ term: format!("\"{}\"", term_str.to_owned()), field: None };
        }
        Term{ term: term_str.to_owned(), field: None }
    }

    pub fn in_field(mut self, field: &str) -> Self {
        self.field = Some(field.to_owned());
        self
    }

    pub fn boost(mut self, value: f32) -> Self {
        self.term = format!("{}^{}", self.term, value.to_string());
        self
    }

    pub fn tilde(mut self, value: u32) -> Self {
        self.term = format!("{}~{}", self.term, value.to_string());
        self
    }

    pub fn required(mut self) -> Self {
        self.term = format!("+{}", self.term);
        self
    }

    pub fn prohibit(mut self) -> Self {
        self.term = format!("-{}", self.term);
        self
    }
}

impl Stringable for Term {
    fn as_str(&self) -> String {
        match &self.field {
            Some(field) => format!("{}: {}", field, self.term),
            None => self.term.clone()
        }
    }

    fn is_query(&self) -> bool {
        false
    }
}

pub struct Date {
    date: String
}

impl Date  {

    pub fn year(count: u32) -> String {
        count.to_string() + "YEARS"
    }

    pub fn month(count: u32) -> String {
        count.to_string() + "MONTHS"
    }

    pub fn day(count: u32) -> String {
        count.to_string() + "DAYS"
    }

    pub fn hour(count: u32) -> String {
        count.to_string() + "HOURS"
    }

    pub fn minute(count: u32) -> String {
        count.to_string() + "MINUTES"
    }

    pub fn second(count: u32) -> String {
        count.to_string() + "SECONDS"
    }

    pub fn new(date_string: &str) -> Self {
        Date { date: date_string.to_owned() }
    }

    pub fn plus(mut self, duration: &str) -> Self {
        self.date = format!("{}+{}", self.date, duration);
        self
    }

    pub fn minus(mut self, duration: &str) -> Self {
        self.date = format!("{}-{}", self.date, duration);
        self
    }
}

impl Stringable for Date {
    fn as_str(&self) -> String {
        self.date.clone()
    }

    fn is_query(&self) -> bool {
        false
    }
}

pub struct Range<'a> {
    from: &'a str,
    to: &'a str,
    mode: Type
}

#[derive(PartialEq)]
enum Type {
    Inclusive,
    Exclusive
}

impl<'a> Range<'a> {

    pub fn inclusive(from: &'a str, to: &'a str) -> Self {
        Range { from, to, mode: Type::Inclusive }
    }

    pub fn exclusive(from: &'a str, to: &'a str) -> Self {
        Range { from, to, mode: Type::Exclusive }
    }
}

impl<'a> Stringable for Range<'a> {
    fn as_str(&self) -> String {
        if self.mode == Type::Inclusive {
            return format!("[{} TO {}]",self.from, self.to);
        }

        format!("{{{} TO {}}}",self.from, self.to)
    }

    fn is_query(&self) -> bool {
        false
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn query_create_from_str() {
        assert_eq!(Query::from_str("*:*").as_str(), "*:*");
    }

    #[test]
    fn query_create_from_a_single_term() {
        let term = Term::from_str("*:*");
        assert_eq!(Query::from_term(term).as_str(), "*:*");
    }

    #[test]
    fn query_concat_two_terms() {
        let term = Term::from_str("*:*");
        let term2 = Term::from_str("another term");
        assert_eq!(Query::from_term(term).term(term2).as_str(), "*:* \"another term\"");
    }

    #[test]
    fn query_concat_two_terms_with_and() {
        let term = Term::from_str("*:*");
        let term2 = Term::from_str("another term");
        assert_eq!(Query::from_term(term).and().term(term2).as_str(), "*:* AND \"another term\"");
    }

    #[test]
    fn query_concat_two_terms_with_or() {
        let term = Term::from_str("*:*");
        let term2 = Term::from_str("another term");
        assert_eq!(Query::from_term(term).or().term(term2).as_str(), "*:* OR \"another term\"");
    }

    #[test]
    fn query_concat_a_term_with_a_subquery() {
        let term = Term::from_str("another term");
        let query =
            Query::from_term(Term::from_str("one_thing"))
            .and()
            .term(Term::from_str("another_thing"));

        assert_eq!(Query::from_term(term).or().subquery(query).as_str(), "\"another term\" OR (one_thing AND another_thing)");
    }

    #[test]
    fn term_as_str_returns_term_as_str_in_quotes() {
        let term = "term term";
        assert_eq!(Term::from_str(term).as_str(), format!("\"{}\"", term));
    }

    #[test]
    fn term_as_str_returns_term_without_quotes() {
        let term = "term";
        assert_eq!(Term::from_str(term).as_str(), term);
    }

    #[test]
    fn term_in_field_decorate_it_with_field() {
        let term_str = "term term";
        let term = Term::from_str(term_str);
        assert_eq!(term.in_field("field").as_str(), "field: \"term term\"");
    }

    #[test]
    fn term_boost_term_chained_with_field() {
        let term = Term::from_str("term term");
        let term_str = term.in_field("field").boost(3.2).as_str();
        assert_eq!(term_str, "field: \"term term\"^3.2");
    }

    #[test]
    fn term_tilde_term_chained_with_boost() {
        let term = Term::from_str("term term");
        let term_str = term.boost(3.2).tilde(20).as_str();
        assert_eq!(term_str, "\"term term\"^3.2~20");
    }

    #[test]
    fn term_require_term() {
        let term = Term::from_str("term");
        let term_str = term.required().as_str();
        assert_eq!(term_str, "+term");
    }

    #[test]
    fn term_prohibit_term() {
        let term = Term::from_str("term");
        let term_str = term.prohibit().as_str();
        assert_eq!(term_str, "-term");
    }

    #[test]
    fn date_as_str_returns_date() {
        let date_string = "NOW";
        assert_eq!(Date::new(date_string).as_str(), date_string);
    }

    #[test]
    fn date_plus_concat_text() {
        let date_string = "NOW";
        let expected = "NOW+2MONTHS";
        let date = Date::new(date_string);
        assert_eq!(date.plus(Date::month(2).as_str()).as_str(), expected);
    }

    #[test]
    fn date_minus_concat_text() {
        let date_string = "NOW";
        let expected = "NOW-2YEARS";
        let date = Date::new(date_string);
        assert_eq!(date.minus(Date::year(2).as_str()).as_str(), expected);
    }

    #[test]
    fn range_create_inclusive_range() {
        let range = Range::inclusive("a", "b");
        assert_eq!(range.as_str(), "[a TO b]");
    }

    #[test]
    fn range_create_exclusive_range() {
        let range = Range::exclusive("a", "b");
        assert_eq!(range.as_str(), "{a TO b}");
    }
}