Skip to content

Commit

Permalink
Request filter & order utils
Browse files Browse the repository at this point in the history
  • Loading branch information
tinrab committed Nov 12, 2023
1 parent 7e87489 commit c785a1b
Show file tree
Hide file tree
Showing 16 changed files with 1,572 additions and 3 deletions.
21 changes: 21 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"rust-analyzer.cargo.target": "x86_64-unknown-linux-gnu",
"rust-analyzer.runnables.extraEnv": {
"RUSTFLAGS": "-Awarnings"
},
"rust-analyzer.cargo.extraEnv": {
"RUSTFLAGS": "-Awarnings"
},
"rust-analyzer.server.extraEnv": {
"RUSTFLAGS": "-Awarnings"
},
"rust-analyzer.check.extraEnv": {
"RUSTFLAGS": "-Awarnings"
},
"rust-analyzer.showUnlinkedFileNotification": false,
"rust-analyzer.cargo.features": "all",
"rust-analyzer.check.features": "all"
}
8 changes: 7 additions & 1 deletion bomboni/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@ path = "src/lib.rs"
[features]
default = ["macros", "request"]
macros = ["dep:bomboni_derive", "dep:regex"]
request = []
request = ["dep:pest", "dep:pest_derive"]
testing = []

[dependencies]
itertools = "0.11.0"
chrono = "0.4.31"
thiserror = "1.0.50"
bomboni_derive = { version = "*", path = "../bomboni_derive", optional = true }
regex = { version = "1.10.2", optional = true }
pest = { version = "2.7.5", optional = true }
pest_derive = { version = "2.7.5", optional = true }

[dev-dependencies]
bomboni_derive = { path = "../bomboni_derive" }
4 changes: 4 additions & 0 deletions bomboni/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ pub mod macros;

#[cfg(feature = "request")]
pub mod request;

#[cfg(feature = "testing")]
#[allow(clippy::all, missing_docs)]
pub mod testing;
110 changes: 110 additions & 0 deletions bomboni/src/macros/collections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,31 @@ macro_rules! btree_map {
}};
}

/// A macro that creates a new `BTreeMap` instance with the given key-value pairs.
/// The same as `btree_map!`, but converts keys and values to the target type.
///
/// # Examples
///
/// Create a map of key-value pairs.
///
/// ```
/// # use std::collections::BTreeMap;
/// use bomboni::btree_map_into;
///
/// let map: BTreeMap<i32, String> = btree_map_into! {
/// 1 => "first",
/// 2 => "second",
/// };
/// assert_eq!(map.get(&1), Some(&"first".to_string()));
/// assert_eq!(map.get(&2), Some(&"second".to_string()));
/// ```
#[macro_export(local_inner_macros)]
macro_rules! btree_map_into {
($($key:expr => $value:expr),* $(,)?) => {
btree_map!($($key.into() => $value.into()),*)
};
}

/// A macro that creates a new `HashMap` instance with the given key-value pairs.
///
/// # Examples
Expand Down Expand Up @@ -87,6 +112,30 @@ macro_rules! hash_map {
_map
}};
}
/// A macro that creates a new `HashMap` instance with the given key-value pairs.
/// The same as `hash_map!`, but converts keys and values to the target type.
///
/// # Examples
///
/// Create a map of key-value pairs.
///
/// ```
/// # use std::collections::HashMap;
/// use bomboni::hash_map_into;
///
/// let map: HashMap<i32, String> = hash_map_into! {
/// 1 => "first",
/// 2 => "second",
/// };
/// assert_eq!(map.get(&1), Some(&"first".to_string()));
/// assert_eq!(map.get(&2), Some(&"second".to_string()));
/// ```
#[macro_export(local_inner_macros)]
macro_rules! hash_map_into {
($($key:expr => $value:expr),* $(,)?) => {
hash_map!($($key.into() => $value.into()),*)
};
}

/// A macro that creates a new `BTreeSet` and inserts the given values into it.
///
Expand Down Expand Up @@ -114,6 +163,27 @@ macro_rules! btree_set {
}};
}

/// A macro that creates a new `BTreeSet` and inserts the given values into it.
/// The same as `btree_set!`, but converts values to the target type.
///
/// # Examples
///
/// ```
/// # use std::collections::BTreeSet;
/// use bomboni::btree_set_into;
///
/// let set: BTreeSet<i32> = btree_set_into![1, 2, 3];
/// assert!(set.contains(&1));
/// assert!(set.contains(&2));
/// assert_eq!(set.len(), 3);
/// ```
#[macro_export(local_inner_macros)]
macro_rules! btree_set_into {
($($value:expr),* $(,)?) => {
btree_set!($($value.into()),*)
};
}

/// A macro that creates a new `HashSet` and inserts the given values into it.
///
/// # Examples
Expand All @@ -140,6 +210,27 @@ macro_rules! hash_set {
}};
}

/// A macro that creates a new `HashSet` and inserts the given values into it.
/// The same as `hash_set!`, but converts values to the target type.
///
/// # Examples
///
/// ```
/// # use std::collections::HashSet;
/// use bomboni::hash_set_into;
///
/// let set: HashSet<i32> = hash_set_into![1, 2, 3];
/// assert!(set.contains(&1));
/// assert!(set.contains(&2));
/// assert_eq!(set.len(), 3);
/// ```
#[macro_export(local_inner_macros)]
macro_rules! hash_set_into {
($($value:expr),* $(,)?) => {
hash_set!($($value.into()),*)
};
}

/// A macro that creates a new `VecDeque` instance with the given values.
///
/// # Examples
Expand All @@ -163,3 +254,22 @@ macro_rules! vec_deque {
])
}};
}

/// A macro that creates a new `VecDeque` instance with the given values.
/// The same as `vec_deque!`, but converts values to the target type.
///
/// # Examples
///
/// ```
/// # use std::collections::VecDeque;
/// use bomboni::vec_deque_into;
///
/// let deque: VecDeque<i32> = vec_deque_into![1, 2, 3];
/// assert_eq!(deque, VecDeque::from(vec![1, 2, 3]));
/// ```
#[macro_export(local_inner_macros)]
macro_rules! vec_deque_into {
($($value:expr),* $(,)?) => {
vec_deque!($($value.into()),*)
};
}
40 changes: 40 additions & 0 deletions bomboni/src/request/filter/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use pest::error::InputLocation;
use thiserror::Error;

use crate::request::schema::ValueType;

use super::parser::Rule;

#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum FilterError {
#[error("failed to parse filter from `{start}` to `{end}`")]
Parse { start: usize, end: usize },
#[error("invalid filter value `{0}`")]
InvalidNumber(String),
#[error("expected filter type `{expected}`, but got `{actual}`")]
InvalidType {
expected: ValueType,
actual: ValueType,
},
#[error("incomparable filter type `{0}`")]
IncomparableType(ValueType),
#[error("expected a filter value")]
ExpectedValue,
}

pub type FilterResult<T> = Result<T, FilterError>;

impl From<pest::error::Error<Rule>> for FilterError {
fn from(err: pest::error::Error<Rule>) -> Self {
match err.location {
InputLocation::Pos(pos) => FilterError::Parse {
start: pos,
end: pos,
},
InputLocation::Span(span) => FilterError::Parse {
start: span.0,
end: span.1,
},
}
}
}
166 changes: 166 additions & 0 deletions bomboni/src/request/filter/grammar.pest
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
WHITESPACE = _{ " " | "\t" | "\r" | "\n" }

// Filter, possibly empty.
filter = {
SOI
~ expression?
~ EOI
}

// Expressions may either be a conjunction (AND) of sequences or a simple
// sequence.
//
// Note, the AND is case-sensitive.
//
// Example: `a b AND c AND d`
//
// The expression `(a b) AND c AND d` is equivalent to the example.
expression = {
sequence ~ ("AND" ~ sequence)*
}

// Sequence is composed of one or more whitespace (WHITESPACE) separated factors.
//
// A sequence expresses a logical relationship between 'factors' where
// the ranking of a filter result may be scored according to the number
// factors that match and other such criteria as the proximity of factors
// to each other within a document.
//
// When filters are used with exact match semantics rather than fuzzy
// match semantics, a sequence is equivalent to AND.
//
// Example: `New York Giants OR Yankees`
//
// The expression `New York (Giants OR Yankees)` is equivalent to the
// example.
sequence = _{
factor ~ factor*
}

// Factors may either be a disjunction (OR) of terms or a simple term.
//
// Note, the OR is case-sensitive.
//
// Example: `a < 10 OR a >= 100`
factor = {
term ~ ("OR" ~ term)*
}

// Terms may either be unary or simple expressions.
//
// Unary expressions negate the simple expression, either mathematically `-`
// or logically `NOT`. The negation styles may be used interchangeably.
//
// Note, the `NOT` is case-sensitive and must be followed by at least one
// whitespace (WHITESPACE).
//
// Examples:
// * logical not : `NOT (a OR b)`
// * alternative not : `-file:".java"`
// * negation : `-30`
term = @{
("NOT" ~ WHITESPACE+)? ~ simple
}

// Simple expressions may either be a restriction or a nested (composite)
// expression.
simple = _{
restriction | composite
}

// Restrictions express a relationship between a comparable value and a
// single argument. When the restriction only specifies a comparable
// without an operator, this is a global restriction.
//
// Note, restrictions are not whitespace sensitive.
//
// Examples:
// * equality : `package=com.google`
// * inequality : `msg != 'hello'`
// * greater than : `1 > 0`
// * greater or equal : `2.5 >= 2.4`
// * less than : `yesterday < request.time`
// * less or equal : `experiment.rollout <= cohort(request.user)`
// * has : `map:key`
// * global : `prod`
//
// In addition to the global, equality, and ordering operators, filters
// also support the has (`:`) operator. The has operator is unique in
// that it can test for presence or value based on the proto3 type of
// the `comparable` value. The has operator is useful for validating the
// structure and contents of complex values.
restriction = !{
comparable ~ (comparator ~ arg)?
}

// Comparable may either be a member, function or a value.
comparable = {
function | value | name
}

// Function calls may use simple or qualified names with zero or more
// arguments.
//
// All functions declared within the list filter, apart from the special
// `arguments` function must be provided by the host service.
//
// Examples:
// * `regex(m.key, '^.*prod.*$')`
// * `math.mem('30mb')`
//
// Antipattern: simple and qualified function names may include keywords:
// NOT, AND, OR. It is not recommended that any of these names be used
// within functions exposed by a service that supports list filters.
function = {
name ~ "(" ~ argList? ~ ")"
}

// Comparators supported by list filters.
comparator = {
"<=" | "<" | ">=" | ">" | "!=" | "=" | ":"
}

// Composite is a parenthesized expression, commonly used to group
// terms or clarify operator precedence.
//
// Example: `(msg.endsWith('world') AND retries < 10)`
composite = !{
"(" ~ expression ~ ")"
}

name = @{
identifier ~ ("." ~ identifier)*
}

identifier = ${ !keyword ~ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* }

keyword = { "AND" | "OR" | "NOT" }

argList = _{
arg ~ ("," ~ arg)*
}

arg = _{
comparable | composite
}

value = _{ string | boolean | number | any }

string = ${ "\"" ~ inner ~ "\"" }
inner = @{ char* }
char = {
!("\"" | "\\") ~ ANY
| "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t")
| "\\" ~ ("u" ~ ASCII_HEX_DIGIT{4})
}

boolean = { "true" | "false" }

number = @{
"-"?
~ ("0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*)
~ ("." ~ ASCII_DIGIT*)?
~ (^"e" ~ ("+" | "-")? ~ ASCII_DIGIT+)?
}

any = { "*" }
Loading

0 comments on commit c785a1b

Please sign in to comment.