Skip to main content

whereexpr/
lib.rs

1//! # whereexpr
2//!
3//! A library for building and evaluating **type-safe, compiled boolean filter
4//! expressions** over arbitrary Rust structs.
5//!
6//! Expressions are written as human-readable strings — e.g.
7//! `"age > 30 && name is-one-of [Alice, Bob]"` — parsed once at build time,
8//! and then evaluated at zero-allocation cost against any number of objects.
9//!
10//! ---
11//!
12//! ## Quick start
13//!
14//! ### 1. Implement [`Attributes`] for your type
15//!
16//! Declare one [`AttributeIndex`] constant per field and implement the three
17//! trait methods:
18//!
19//! ```rust
20//! use whereexpr::{Attributes, AttributeIndex, Value, ValueKind};
21//!
22//! struct Person {
23//!     name: String,
24//!     age:  u32,
25//! }
26//!
27//! impl Person {
28//!     const NAME: AttributeIndex = AttributeIndex::new(0);
29//!     const AGE:  AttributeIndex = AttributeIndex::new(1);
30//! }
31//!
32//! impl Attributes for Person {
33//!     const TYPE_ID: u64 = 0x517652f2; // unique ID for Person type (a hash or other unique identifier)
34//!     const TYPE_NAME: &'static str = "Person";
35//!     fn get(&self, idx: AttributeIndex) -> Option<Value<'_>> {
36//!         match idx {
37//!             Self::NAME => Some(Value::String(&self.name)),
38//!             Self::AGE  => Some(Value::U32(self.age)),
39//!             _          => None,
40//!         }
41//!     }
42//!     fn kind(idx: AttributeIndex) -> Option<ValueKind> {
43//!         match idx {
44//!             Self::NAME => Some(ValueKind::String),
45//!             Self::AGE  => Some(ValueKind::U32),
46//!             _          => None,
47//!         }
48//!     }
49//!     fn index(name: &str) -> Option<AttributeIndex> {
50//!         match name {
51//!             "name" => Some(Self::NAME),
52//!             "age"  => Some(Self::AGE),
53//!             _      => None,
54//!         }
55//!     }
56//! }
57//! ```
58//!
59//! ### 2. Build an expression
60//!
61//! Use [`ExpressionBuilder`] to register named conditions, then compile them
62//! into a reusable [`Expression`] with a boolean expression string:
63//!
64//! ```rust
65//! # use whereexpr::*;
66//! # struct Person { name: String, age: u32 }
67//! # impl Person {
68//! #     const NAME: AttributeIndex = AttributeIndex::new(0);
69//! #     const AGE: AttributeIndex = AttributeIndex::new(1);
70//! # }
71//! # impl Attributes for Person {
72//! #     const TYPE_ID: u64 = 0x517652f2; // unique ID for Person type (a hash or other unique identifier)
73//! #     const TYPE_NAME: &'static str = "Person";
74//! #     fn get(&self, idx: AttributeIndex) -> Option<Value<'_>> {
75//! #         match idx { Self::NAME => Some(Value::String(&self.name)), Self::AGE => Some(Value::U32(self.age)), _ => None }
76//! #     }
77//! #     fn kind(idx: AttributeIndex) -> Option<ValueKind> {
78//! #         match idx { Self::NAME => Some(ValueKind::String), Self::AGE => Some(ValueKind::U32), _ => None }
79//! #     }
80//! #     fn index(name: &str) -> Option<AttributeIndex> {
81//! #         match name { "name" => Some(Self::NAME), "age" => Some(Self::AGE), _ => None }
82//! #     }
83//! # }
84//! let expr = ExpressionBuilder::<Person>::new()
85//!     .add("has_name", Condition::from_str("name is-one-of [Alice, Bob] {ignore-case}"))
86//!     .add("is_adult", Condition::from_str("age >= 18"))
87//!     .build("has_name && is_adult")
88//!     .unwrap();
89//! ```
90//!
91//! ### 3. Evaluate
92//!
93//! Call [`Expression::matches`] to test any object:
94//!
95//! ```rust
96//! # use whereexpr::*;
97//! # struct Person { name: String, age: u32 }
98//! # impl Person {
99//! #     const NAME: AttributeIndex = AttributeIndex::new(0);
100//! #     const AGE: AttributeIndex = AttributeIndex::new(1);
101//! # }
102//! # impl Attributes for Person {
103//! #     const TYPE_ID: u64 = 0x517652f2; // unique ID for Person type (a hash or other unique identifier)
104//! #     const TYPE_NAME: &'static str = "Person";
105//! #     fn get(&self, idx: AttributeIndex) -> Option<Value<'_>> {
106//! #         match idx { Self::NAME => Some(Value::String(&self.name)), Self::AGE => Some(Value::U32(self.age)), _ => None }
107//! #     }
108//! #     fn kind(idx: AttributeIndex) -> Option<ValueKind> {
109//! #         match idx { Self::NAME => Some(ValueKind::String), Self::AGE => Some(ValueKind::U32), _ => None }
110//! #     }
111//! #     fn index(name: &str) -> Option<AttributeIndex> {
112//! #         match name { "name" => Some(Self::NAME), "age" => Some(Self::AGE), _ => None }
113//! #     }
114//! # }
115//! # let expr = ExpressionBuilder::<Person>::new()
116//! #     .add("has_name", Condition::from_str("name is-one-of [Alice, Bob] {ignore-case}"))
117//! #     .add("is_adult", Condition::from_str("age >= 18"))
118//! #     .build("has_name && is_adult")
119//! #     .unwrap();
120//! let people = vec![
121//!     Person { name: "Alice".into(), age: 30 },
122//!     Person { name: "Charlie".into(), age: 25 },
123//! ];
124//!
125//! let matches: Vec<_> = people.iter().filter(|p| expr.matches(*p)).collect();
126//! // → only Alice
127//! ```
128//!
129//! ---
130//!
131//! ## Core concepts
132//!
133//! | Type | Role |
134//! |---|---|
135//! | [`Attributes`] | Trait your struct implements to expose its fields |
136//! | [`AttributeIndex`] | Opaque index that identifies a field |
137//! | [`Value`] | The runtime value of a field (tagged union) |
138//! | [`ValueKind`] | The type tag of a field (no data) |
139//! | [`Condition`] | Maps one attribute to a [`Predicate`] |
140//! | [`Predicate`] | A compiled single-field test (operation + reference value) |
141//! | [`Operation`] | The comparison operator (`is`, `>`, `contains`, `glob`, …) |
142//! | [`ExpressionBuilder`] | Fluent builder: registers conditions and compiles the boolean expression |
143//! | [`Expression`] | The compiled, reusable filter — call `.matches(&obj)` to evaluate |
144//! | [`Error`] | All errors that can occur during building or predicate construction |
145//!
146//! ---
147//!
148//! ## Condition string syntax
149//!
150//! A condition string has the form:
151//!
152//! ```text
153//! <attribute>  <operation>  <value>  [<modifiers>]
154//! ```
155//!
156//! Single value:
157//!
158//! ```text
159//! age > 30
160//! name is Alice
161//! status is-not deleted
162//! filename ends-with .log
163//! path glob /var/log/**/*.log
164//! ```
165//!
166//! List value (one or more comma-separated entries in `[ ]`):
167//!
168//! ```text
169//! status    is-one-of       [active, pending, paused]
170//! extension ends-with-one-of [.jpg, .jpeg, .png]
171//! message   contains-one-of  [error, fatal, critical]
172//! ```
173//!
174//! Range (exactly two values in `[ ]`):
175//!
176//! ```text
177//! age       in-range     [18, 65]
178//! score     not-in-range [0, 10]
179//! port      in-range     [1024, 65535]
180//! ```
181//!
182//! Modifiers (appended in `{ }`):
183//!
184//! ```text
185//! name is-one-of [alice, bob] {ignore-case}
186//! path starts-with /Home       {ignore-case}
187//! ```
188//!
189//! ---
190//!
191//! ## Boolean expression syntax
192//!
193//! The string passed to [`ExpressionBuilder::build`] combines named conditions
194//! using standard boolean operators:
195//!
196//! | Operator | Aliases | Example |
197//! |---|---|---|
198//! | AND | `&&`, `AND` | `cond_a && cond_b` |
199//! | OR  | `\|\|`, `OR` | `cond_a \|\| cond_b` |
200//! | NOT | `!`, `NOT`  | `!cond_a` |
201//! | Grouping | `( )` | `(cond_a \|\| cond_b) && cond_c` |
202//!
203//! `&&` and `||` **cannot be mixed** at the same nesting level without
204//! parentheses — this is intentional to avoid precedence ambiguity:
205//!
206//! ```text
207//! // ✗ error: mixed operators
208//! cond_a && cond_b || cond_c
209//!
210//! // ✓ ok: grouped explicitly
211//! (cond_a && cond_b) || cond_c
212//! cond_a && (cond_b || cond_c)
213//! ```
214//!
215//! ---
216//!
217//! ## Available operations
218//!
219//! See [`Operation`] for the full list with per-variant aliases and examples.
220//!
221//! | Family | Operations |
222//! |---|---|
223//! | Equality | `is`, `is-not` |
224//! | Membership | `is-one-of`, `is-not-one-of` |
225//! | String prefix | `starts-with`, `not-starts-with`, `starts-with-one-of`, `not-starts-with-one-of` |
226//! | String suffix | `ends-with`, `not-ends-with`, `ends-with-one-of`, `not-ends-with-one-of` |
227//! | Substring | `contains`, `not-contains`, `contains-one-of`, `not-contains-one-of` |
228//! | Glob pattern | `glob`, `not-glob` |
229//! | Numeric | `>`, `>=`, `<`, `<=`, `in-range`, `not-in-range` |
230//!
231//! ---
232//!
233//! ## Supported value types
234//!
235//! See [`Value`] and [`ValueKind`] for details. In brief:
236//! `String`, `Path`, `u8`–`u64`, `i8`–`i64`, `f32`, `f64`,
237//! `Hash128`, `Hash160`, `Hash256`, `IpAddr`, `DateTime` (Unix timestamp),
238//! `Bool`.
239//!
240//! ---
241//!
242//! ## Feature flags
243//!
244//! | Flag | Effect |
245//! |---|---|
246//! | `error_description` | Implements [`std::fmt::Display`] for [`Error`], providing annotated error messages with `^` underlines pointing at the problematic token. |
247//! | `enable_type_check` | Enables type checking at runtime. This is useful for debugging and for ensuring that the correct type is used when evaluating an expression. |
248
249mod cond_parser;
250mod condition;
251mod condition_list;
252mod error;
253mod expr_parser;
254mod expression;
255mod operation;
256mod predicate;
257mod predicates;
258mod types;
259mod value;
260
261#[cfg(test)]
262mod tests;
263
264pub(crate) use condition::CompiledCondition;
265pub use condition::Condition;
266pub(crate) use condition::ConditionAttribute;
267pub(crate) use condition::ConditionPredicate;
268pub(crate) use condition_list::ConditionList;
269pub use error::Error;
270pub use expression::Expression;
271pub use expression::ExpressionBuilder;
272pub use operation::Operation;
273pub use predicate::Predicate;
274pub use value::AttributeIndex;
275pub use value::Attributes;
276pub use value::Value;
277pub use value::ValueKind;