notmongo 0.1.7

In-process, bad database with an API similar to MongoDB
Documentation
use crate::helpers;
use bson;
use bson::Bson;
use std::fmt;

enum SubQuery {
    MatchAll,
    Eq(String, Bson),
    Gt(String, Bson),
    Gte(String, Bson),
    Lt(String, Bson),
    Lte(String, Bson),
    Ne(String, Bson),
    And(Vec<SubQuery>),
    Not(Box<SubQuery>),
    Or(Vec<SubQuery>),
    Exists(String, bool),
}

impl SubQuery {
    fn parse_root(query: &bson::Bson) -> Result<SubQuery, String> {
        let mut subqueries = Vec::new();

        // Parse all subqueries
        match query {
            Bson::Document(doc) => {
                for (operator, argument) in doc {
                    subqueries.push(SubQuery::parse_outer(operator, argument)?);
                }
            }
            _ => {
                return Err(format!(
                    "Queries need to be Objects, but received {}",
                    query
                ));
            }
        }

        // Wrap them into a result
        match subqueries.len() {
            0 => Ok(SubQuery::MatchAll),
            1 => Ok(subqueries.pop().unwrap()),
            _ => Ok(SubQuery::And(subqueries)),
        }
    }

    fn parse_outer(operator: &str, argument: &bson::Bson) -> Result<SubQuery, String> {
        // Operations
        match operator {
            "$and" => match argument {
                Bson::Array(raw_subqueries) => {
                    let mut subqueries = Vec::new();

                    for val in raw_subqueries {
                        subqueries.push(SubQuery::parse_root(val)?);
                    }

                    Ok(SubQuery::And(subqueries))
                }
                _ => Err(format!(
                    "The $and operator needs an array as argument, got {:?}",
                    argument
                )),
            },
            "$not" => Ok(SubQuery::Not(Box::new(SubQuery::parse_root(argument)?))),
            "$or" => match argument {
                Bson::Array(raw_subqueries) => {
                    let mut subqueries = Vec::new();

                    for val in raw_subqueries {
                        subqueries.push(SubQuery::parse_root(val)?);
                    }

                    Ok(SubQuery::Or(subqueries))
                }
                _ => Err(format!(
                    "The $and filter needs an array as argument, got {:?}",
                    argument
                )),
            },
            // Operator or field name
            value => {
                if value.starts_with("$") {
                    Err(format!(
                        "This operator cannot be placed here, or is unsupported: {}",
                        value
                    ))
                } else {
                    SubQuery::parse_inner(value, argument)
                }
            }
        }
    }

    fn parse_inner(field: &str, argument: &bson::Bson) -> Result<SubQuery, String> {
        // Is the argument an operator?
        let (operator, argument) = match argument {
            // Document -> Operator
            Bson::Document(doc) => {
                if doc.len() != 1 {
                    return Err(format!(
                        "Operators need to be documents with one key, but got {:?}",
                        doc
                    ));
                }

                doc.iter().next().unwrap()
            }

            // Anything else -> Implicit $eq operator
            value => return Ok(SubQuery::Eq(field.to_string(), value.clone())),
        };

        // Parse the operator
        match operator.as_str() {
            "$lt" => Ok(SubQuery::Lt(field.to_string(), argument.clone())),
            "$lte" => Ok(SubQuery::Lte(field.to_string(), argument.clone())),
            "$eq" => Ok(SubQuery::Eq(field.to_string(), argument.clone())),
            "$ne" => Ok(SubQuery::Ne(field.to_string(), argument.clone())),
            "$gt" => Ok(SubQuery::Gt(field.to_string(), argument.clone())),
            "$gte" => Ok(SubQuery::Gte(field.to_string(), argument.clone())),
            "$exists" => match argument {
                Bson::Boolean(value) => Ok(SubQuery::Exists(field.to_string(), *value)),
                _ => Err(format!(
                    "Expected boolean value for $exists filter, got {:?}",
                    argument
                )),
            },
            op_name => Err(format!(
                "This operator cannot be placed here, or is unsupported: {}",
                op_name
            )),
        }
    }

    fn matches_document(&self, doc: &bson::Document) -> bool {
        // Helper function to avoid repeating code in comparisons
        let comparison_or_return_if_not_exists =
            |field: &str, value: &Bson, ordering: std::cmp::Ordering, negate: bool| -> bool {
                // Get the field
                let field_value = match doc.get(field) {
                    Some(doc_value) => doc_value,
                    None => return false,
                };

                // Special case for arrays: Implicitly perform an `$in` operation
                match field_value {
                    Bson::Array(array) => {
                        match ordering {
                            // $eq with array -> implicit $in
                            std::cmp::Ordering::Equal => {
                                for array_value in array {
                                    if helpers::compare_bson(array_value, value)
                                        == std::cmp::Ordering::Equal
                                    {
                                        return !negate;
                                    }
                                }
                            }
                            _ => {}
                        }
                    }
                    _ => {}
                }

                // Compare them as scalars
                let order_is = helpers::compare_bson(field_value, value);
                (order_is == ordering) ^ negate
            };

        match self {
            SubQuery::MatchAll => true,
            SubQuery::And(subqueries) => {
                for subquery in subqueries {
                    if !subquery.matches_document(doc) {
                        return false;
                    }
                }
                true
            }
            SubQuery::Not(subquery) => !subquery.matches_document(doc),
            SubQuery::Or(subqueries) => {
                for subquery in subqueries {
                    if subquery.matches_document(doc) {
                        return true;
                    }
                }
                false
            }
            SubQuery::Lt(field, value) => {
                comparison_or_return_if_not_exists(field, value, std::cmp::Ordering::Less, false)
            }
            SubQuery::Lte(field, value) => {
                comparison_or_return_if_not_exists(field, value, std::cmp::Ordering::Greater, true)
            }
            SubQuery::Eq(field, value) => {
                comparison_or_return_if_not_exists(field, value, std::cmp::Ordering::Equal, false)
            }
            SubQuery::Ne(field, value) => {
                comparison_or_return_if_not_exists(field, value, std::cmp::Ordering::Equal, true)
            }
            SubQuery::Gt(field, value) => {
                comparison_or_return_if_not_exists(field, value, std::cmp::Ordering::Greater, false)
            }
            SubQuery::Gte(field, value) => {
                comparison_or_return_if_not_exists(field, value, std::cmp::Ordering::Less, true)
            }
            SubQuery::Exists(field, should_exist) => {
                let does_exist = matches!(doc.get(field), Some(_));
                *should_exist == does_exist
            }
        }
    }

    fn to_bson(&self) -> bson::Document {
        match self {
            SubQuery::MatchAll => bson::Document::new(),
            SubQuery::And(subqueries) => bson::doc! {
                "$and": subqueries.iter().map(|subquery| subquery.to_bson()).collect::<Vec<_>>()
            },
            SubQuery::Not(subquery) => bson::doc! {
                "$not": subquery.to_bson()
            },
            SubQuery::Or(subqueries) => bson::doc! {
                "$or": subqueries.iter().map(|subquery| subquery.to_bson()).collect::<Vec<_>>()
            },
            SubQuery::Exists(field, should_exist) => bson::doc! {
                "$exists": {field: *should_exist}
            },
            SubQuery::Eq(field, value) => bson::doc! {
                "$eq": {field: value}
            },
            SubQuery::Gt(field, value) => bson::doc! {
                "$gt": {field: value}
            },
            SubQuery::Gte(field, value) => bson::doc! {
                "$gte": {field: value}
            },
            SubQuery::Lt(field, value) => bson::doc! {
                "$lt": {field: value}
            },
            SubQuery::Lte(field, value) => bson::doc! {
                "$lte": {field: value}
            },
            SubQuery::Ne(field, value) => bson::doc! {
                "$ne": {field: value}
            },
        }
    }
}

pub struct FindQuery {
    root_query: SubQuery,
}

impl FindQuery {
    pub fn new_all() -> FindQuery {
        FindQuery {
            root_query: SubQuery::MatchAll,
        }
    }

    pub fn parse_bson<B: Into<Bson>>(query: B) -> Result<FindQuery, String> {
        let query: Bson = query.into();
        Ok(FindQuery {
            root_query: SubQuery::parse_root(&query)?,
        })
    }

    pub fn to_bson(&self) -> bson::Document {
        self.root_query.to_bson()
    }

    pub(crate) fn matches(&self, value: &bson::Document) -> bool {
        self.root_query.matches_document(value)
    }
}

impl fmt::Display for FindQuery {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
        let as_bson = self.to_bson();
        write!(
            f,
            "{}",
            helpers::dump_to_json_string(as_bson.into(), true, true).unwrap()
        )
    }
}