bool_tag_expr/lib.rs
1//!
2//! # About
3//!
4//! This crate allows boolean expressions of tags to be written,
5//! parsed, and evaluated (e.g. converted to SQL for selecting out of a
6//! database). The tags can be restricted to the more typical single-value
7//! kind, but support is also supplied for key-value tags - this is achieved by
8//! making the tag name optional.
9//!
10//! For example, to get all British and American scientists who are not
11//! chemists (see below for a proper example in Rust) one could use:
12//!
13//! `((nationality=american & scientist) | (=british & scientist)) & !chemist & person`
14//!
15//! The input requirements aren't rigid and so to get the same result we could
16//! also write, say:
17//!
18//! `(nationality=american | =british) & scientist & !chemist & person`
19//!
20//! Where:
21//!
22//! - `nationality=american` means entities with the a tag whose name is
23//! `nationality` and whose value is `american`
24//! - `=british` is the same as `british` and means entities with a tag value of
25//! `british`
26//! - `!chemist` means entities that do not have the tag value `chemist`
27//! - `person` means those entities that have a tag value of `person`
28//!
29//! ## Syntax
30//!
31//! - `&` for `AND` (`&&` is a lexical error)
32//! - `|` for `OR` (`||` is a lexical error)
33//! - `(` and `)` for logical grouping (`)(` is a syntax error)
34//! - `!` for `NOT`
35//! - `tag-name=tag-value`, `=tag-value`, and `tag-value` are all valid tags
36//!
37//! # Parsing
38//!
39//! A valid boolean expression must contain at least 1 tag.
40//!
41//! This modules provides functionality for both:
42//!
43//! 1. String (or custom type with necessary trait) → Bool expression tree
44//! 2. String (or custom type with necessary trait) → Lexical token stream → Bool expression tree
45//!
46//! See below for examples and clarification.
47//!
48//! # Examples
49//!
50//! ## Basic
51//!
52//! The simplest way to use this crate is to parse some boolean tag expression
53//! string and to then select items out of a database. Here is how to do that:
54//!
55//! ```rust
56//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
57//! use bool_tag_expr::BoolTagExpr;
58//! use bool_tag_expr::DbTableInfo;
59//!
60//! // The boolean tag expression
61//! let expr_str = "(nationality=american | =british) & scientist & !chemist & person";
62//!
63//! // Parse it, returning early if there is an error
64//! let expr_tree = BoolTagExpr::from(expr_str)?;
65//!
66//! // Setup the database info
67//! let table_name = "entity_tags";
68//! let id_column = "entity_id";
69//! let tag_name_column = "name";
70//! let tag_value_column = "value";
71//! let table_info = DbTableInfo::from(table_name, id_column, tag_name_column, tag_value_column)?;
72//!
73//! // Generate the SQL using the expression tree and the database info
74//! let bool_expr_sql_str = expr_tree.to_sql(&table_info);
75//!
76//! // Create the full SQL statement
77//! let sql = format!(
78//! r#"
79//! SELECT DISTINCT {id_column}
80//! FROM ({bool_expr_sql_str})
81//! LIMIT ?
82//! "#
83//! );
84//!
85//! # Ok(())
86//! # }
87//! ```
88//!
89//! ## Advanced
90//!
91//! Should you want to get a [`BoolTagExpr`] from some type that isn't readily
92//! converted into a string, you can implement [`BoolTagExprLexicalParse`] for
93//! it. You will then automatically get an implementation of
94//! [`BoolTagExprSyntaxParse`]. You can then use `BoolTagExpr::from(my_var)?;`
95//! as above. Alternatively, you can lexically parse and then syntactically
96//! parse any type implementing these 2 traits (already implemented for
97//! strings). This might be helpful if, for example, you want to limit the
98//! number of tags in a boolean expression:
99//!
100//! ```
101//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
102//! use bool_tag_expr::BoolTagExprSyntaxParse;
103//! use bool_tag_expr::BoolTagExprLexicalParse;
104//! use bool_tag_expr::Token;
105//!
106//! // Get the boolean expression (e.g. from user input)
107//! let expr = "(german | british) & poet";
108//!
109//! // Parse (lexical only) the boolean expression
110//! let tokens = expr.lexical_parse().unwrap_or_else(|error| {
111//! eprintln!("Lexical parse error: {error}");
112//! std::process::exit(1);
113//! });
114//!
115//! // Count the number of tags
116//! let tag_count = tokens
117//! .tokens()
118//! .iter()
119//! .filter(|token| if let Token::Tag(_) = token { true } else { false })
120//! .count();
121//!
122//! // Panic if there are too many tags
123//! if tag_count > 5 {
124//! panic!()
125//! }
126//!
127//! // Parse (syntax) the tokens
128//! let expr_tree = expr.syntax_parse().unwrap_or_else(|error| {
129//! eprintln!("Syntax parse error: {error}");
130//! std::process::exit(1);
131//! });
132//!
133//! # Ok(())
134//! # }
135//! ```
136//!
137//! # Crate Features
138//!
139//! There is only 1 optional feature: `sqlx`. It is not selected by default.
140//! Unless you are planning on compiling parts of the crate to WASM, it is
141//! recommended that you use the `sqlx` feature. If you are planning on pulling
142//! data out of a database, you must use this feature.
143//!
144//! # Warnings
145//!
146//! For the `NOT` functionality to work correctly for all entires, every entry
147//! must have at least 1 tag.
148//!
149
150#![warn(clippy::pedantic, clippy::nursery, clippy::all, clippy::cargo)]
151#![allow(clippy::module_name_repetitions)]
152
153mod lexical_parse;
154mod syntax_parse;
155mod tags;
156mod tutorial;
157
158pub use lexical_parse::*;
159pub use syntax_parse::*;
160pub use tags::*;
161
162use thiserror::Error;
163
164/// All possible lexical and syntactic parsing errors
165#[derive(Debug, PartialEq, Error, Clone, Hash, Eq)]
166pub enum ParseError {
167 /// A lexical parsing error occurred
168 #[error("Lexical pasing error: {0}")]
169 Lexical(#[from] LexicalParseError),
170
171 /// A syntax parsing error occurred
172 #[error("Syntax parsing error: {0}")]
173 Syntax(#[from] SyntaxParseError),
174}