bool-tag-expr 0.1.0-beta.2

Parse boolean expressions of tags for filtering and selecting
Documentation
//!
//! # About
//!
//! This crate allows boolean expressions of tags to be written,
//! parsed, and evaluated (e.g. converted to SQL for selecting out of a
//! database).  The tags can be restricted to the more typical single-value
//! kind, but support is also supplied for key-value tags - this is achieved by
//! making the tag name optional.
//!
//! For example, to get all British and American scientists who are not
//! chemists (see below for a proper example in Rust) one could use:
//!
//! `((nationality=american & scientist) | (=british & scientist)) & !chemist & person`
//!
//! The input requirements aren't rigid and so to get the same result we could
//! also write, say:
//!
//! `(nationality=american | =british) & scientist & !chemist & person`
//!
//! Where:
//!
//! - `nationality=american` means entities with the a tag whose name is
//!   `nationality` and whose value is `american`
//! - `=british` is the same as `british` and means entities with a tag value of
//!   `british`
//! - `!chemist` means entities that do not have the tag value `chemist`
//! - `person` means those entities that have a tag value of `person`
//!
//! ## Syntax
//!
//! - `&` for `AND` (`&&` is a lexical error)
//! - `|` for `OR` (`||` is a lexical error)
//! - `(` and `)` for logical grouping (`)(` is a syntax error)
//! - `!` for `NOT`
//! - `tag-name=tag-value`, `=tag-value`, and `tag-value` are all valid tags
//!
//! # Parsing
//!
//! A valid boolean expression must contain at least 1 tag.
//!
//! This modules provides functionality for both:
//!
//! 1. String (or custom type with necessary trait) → Bool expression tree
//! 2. String (or custom type with necessary trait) → Lexical token stream → Bool expression tree
//!
//! See below for examples and clarification.
//!
//! # Examples
//!
//! ## Basic
//!
//! The simplest way to use this crate is to parse some boolean tag expression
//! string and to then select items out of a database.  Here is how to do that:
//!
//! ```rust
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use bool_tag_expr::BoolTagExpr;
//! use bool_tag_expr::DbTableInfo;
//!
//! // The boolean tag expression
//! let expr_str = "(nationality=american | =british) & scientist & !chemist & person";
//!
//! // Parse it, returning early if there is an error
//! let expr_tree = BoolTagExpr::from(expr_str)?;
//!
//! // Setup the database info
//! let table_name = "entity_tags";
//! let id_column = "entity_id";
//! let tag_name_column = "name";
//! let tag_value_column = "value";
//! let table_info = DbTableInfo::from(table_name, id_column, tag_name_column, tag_value_column)?;
//!
//! // Generate the SQL using the expression tree and the database info
//! let bool_expr_sql_str = expr_tree.to_sql(&table_info);
//!
//! // Create the full SQL statement
//! let sql = format!(
//!     r#"
//!         SELECT DISTINCT {id_column}
//!         FROM ({bool_expr_sql_str})
//!         LIMIT ?
//!     "#
//! );
//!
//! # Ok(())
//! # }
//! ```
//!
//! ## Advanced
//!
//! Should you want to get a [`BoolTagExpr`] from some type that isn't readily
//! converted into a string, you can implement [`BoolTagExprLexicalParse`] for
//! it.  You will then automatically get an implementation of
//! [`BoolTagExprSyntaxParse`].  You can then use `BoolTagExpr::from(my_var)?;`
//! as above.  Alternatively, you can lexically parse and then syntactically
//! parse any type implementing these 2 traits (already implemented for
//! strings).  This might be helpful if, for example, you want to limit the
//! number of tags in a boolean expression:
//!
//! ```
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use bool_tag_expr::BoolTagExprSyntaxParse;
//! use bool_tag_expr::BoolTagExprLexicalParse;
//! use bool_tag_expr::Token;
//!
//! // Get the boolean expression (e.g. from user input)
//! let expr = "(german | british) & poet";
//!
//! // Parse (lexical only) the boolean expression
//! let tokens = expr.lexical_parse().unwrap_or_else(|error| {
//!     eprintln!("Lexical parse error: {error}");
//!     std::process::exit(1);
//! });
//!
//! // Count the number of tags
//! let tag_count = tokens
//!     .tokens()
//!     .iter()
//!     .filter(|token| if let Token::Tag(_) = token { true } else { false })
//!     .count();
//!
//! // Panic if there are too many tags
//! if tag_count > 5 {
//!     panic!()
//! }
//!
//! // Parse (syntax) the tokens
//! let expr_tree = expr.syntax_parse().unwrap_or_else(|error| {
//!     eprintln!("Syntax parse error: {error}");
//!     std::process::exit(1);
//! });
//!
//! # Ok(())
//! # }
//! ```
//!
//! # Crate Features
//!
//! There is only 1 optional feature: `sqlx`.  It is not selected by default.
//! Unless you are planning on compiling parts of the crate to WASM, it is
//! recommended that you use the `sqlx` feature.  If you are planning on pulling
//! data out of a database, you must use this feature.
//!
//! # Warnings
//!
//! For the `NOT` functionality to work correctly for all entires, every entry
//! must have at least 1 tag.
//!

#![warn(clippy::pedantic, clippy::nursery, clippy::all, clippy::cargo)]
#![allow(clippy::module_name_repetitions)]

mod lexical_parse;
mod syntax_parse;
mod tags;
mod tutorial;

pub use lexical_parse::*;
pub use syntax_parse::*;
pub use tags::*;

use thiserror::Error;

/// All possible lexical and syntactic parsing errors
#[derive(Debug, PartialEq, Error, Clone, Hash, Eq)]
pub enum ParseError {
    /// A lexical parsing error occurred
    #[error("Lexical pasing error: {0}")]
    Lexical(#[from] LexicalParseError),

    /// A syntax parsing error occurred
    #[error("Syntax parsing error: {0}")]
    Syntax(#[from] SyntaxParseError),
}