Overview
The filtering system implements Google AIP-160, which defines a standard filtering language based on the Common Expression Language (CEL). This provides a consistent, powerful way for API clients to filter resources.
All filter expressions are validated against schemas to ensure type safety and prevent runtime errors.
Filter Syntax
Basic Comparisons
Filter expressions support standard comparison operators:
Operator Description Example =Equal age = 30!=Not equal status != "deleted"<Less than price < 100<=Less than or equal age <= 65>Greater than count > 0>=Greater than or equal score >= 80:Has (contains) tags:("urgent")
Logical Operators
Combine multiple conditions with logical operators:
use bomboni_request :: filter :: Filter ;
// AND operator (all conditions must be true)
let filter = Filter :: parse ( "age >= 18 AND status = \" active \" " ) . unwrap ();
// OR operator (any condition can be true)
let filter = Filter :: parse ( "role = \" admin \" OR role = \" moderator \" " ) . unwrap ();
// NOT operator (negation)
let filter = Filter :: parse ( "NOT deleted = true" ) . unwrap ();
Nested Expressions
Use parentheses for complex logic:
let filter = Filter :: parse ( r#"
(age >= 18 AND age < 65)
AND (status = "active" OR status = "pending")
AND NOT deleted = true
"# ) . unwrap ();
Field References
Simple Fields
let filter = Filter :: parse ( "name = \" John \" " ) . unwrap ();
let filter = Filter :: parse ( "age >= 30" ) . unwrap ();
Nested Fields
Access nested object fields with dot notation:
let filter = Filter :: parse ( "user.displayName = \" Alice \" " ) . unwrap ();
let filter = Filter :: parse ( "task.userId = \" 123 \" " ) . unwrap ();
let filter = Filter :: parse ( "address.city = \" San Francisco \" " ) . unwrap ();
Complex Filter Examples
Multi-Field Filtering
use bomboni_request :: filter :: Filter ;
use bomboni_request :: testing :: schema :: { RequestItem , UserItem , TaskItem };
let filter = Filter :: parse ( r#"
user.age >= 18
AND user.id:"4"
AND NOT (task.deleted = false)
AND task.content = user.displayName
AND task.tags:("a" "b")
"# ) . unwrap ();
// Evaluate against data
let result = filter . evaluate ( & RequestItem {
user : UserItem {
id : "42" . into (),
display_name : "test" . into (),
age : 30 ,
},
task : TaskItem {
id : "1" . into (),
user_id : "42" . into (),
content : "test" . into (),
deleted : true ,
tags : vec! [ "a" . into (), "b" . into (), "c" . into ()],
},
}) . unwrap ();
assert_eq! ( result , bomboni_request :: value :: Value :: Boolean ( true ));
String Matching
The : operator performs substring matching on strings:
// Check if string contains substring
let filter = Filter :: parse ( r#"name:"Alice""# ) . unwrap ();
// Check if array contains value
let filter = Filter :: parse ( r#"tags:"urgent""# ) . unwrap ();
Array Operations
Filter on repeated fields (arrays):
let filter = Filter :: parse ( r#"
tags:("a" "b") // Array contains both "a" AND "b"
"# ) . unwrap ();
let filter = Filter :: parse ( r#"
tags:("a" OR "b") // Array contains "a" OR "b"
"# ) . unwrap ();
Schema Validation
Defining a Schema
use bomboni_request :: schema :: { Schema , FieldMemberSchema , ValueType };
use bomboni_macros :: btree_map_into;
let schema = Schema {
members : btree_map_into! {
"id" => FieldMemberSchema :: new_ordered ( ValueType :: String ),
"age" => FieldMemberSchema :: new_ordered ( ValueType :: Integer ),
"name" => FieldMemberSchema :: new ( ValueType :: String ),
"tags" => FieldMemberSchema :: new_repeated ( ValueType :: String ),
"active" => FieldMemberSchema :: new ( ValueType :: Boolean ),
"price" => FieldMemberSchema :: new_ordered ( ValueType :: Float ),
},
};
Validating Filters
use bomboni_request :: filter :: Filter ;
// Valid filter
let filter = Filter :: parse ( "age >= 18 AND name = \" John \" " ) . unwrap ();
filter . validate ( & schema , None ) . unwrap (); // OK
// Invalid filter - unknown field
let invalid_filter = Filter :: parse ( "unknown_field = \" test \" " ) . unwrap ();
assert! ( invalid_filter . validate ( & schema , None ) . is_err ());
// Invalid filter - type mismatch
let invalid_filter = Filter :: parse ( "age = \" not a number \" " ) . unwrap ();
assert! ( invalid_filter . validate ( & schema , None ) . is_err ());
SchemaMapped Trait
Implement SchemaMapped to enable filter evaluation on your types:
use bomboni_request :: schema :: SchemaMapped ;
use bomboni_request :: value :: Value ;
struct User {
id : String ,
display_name : String ,
age : i32 ,
}
impl SchemaMapped for User {
fn get_field ( & self , name : & str ) -> Value {
match name {
"id" => self . id . clone () . into (),
"displayName" => self . display_name . clone () . into (),
"age" => self . age . into (),
_ => unimplemented! ( "Unknown field: {}" , name ),
}
}
}
Nested Objects
For nested structures, split the field path:
struct RequestItem {
user : UserItem ,
task : TaskItem ,
}
impl SchemaMapped for RequestItem {
fn get_field ( & self , name : & str ) -> Value {
let parts : Vec < _ > = name . split ( '.' ) . collect ();
match * parts . first () . unwrap () {
"user" => self . user . get_field ( parts [ 1 ]),
"task" => self . task . get_field ( parts [ 1 ]),
_ => unimplemented! ( "SchemaMapped: RequestItem::{}" , name ),
}
}
}
Filter Evaluation
In-Memory Evaluation
Evaluate filters against Rust data structures:
use bomboni_request :: filter :: Filter ;
use bomboni_request :: testing :: schema :: UserItem ;
let filter = Filter :: parse ( "age >= 18 AND displayName = \" Alice \" " ) . unwrap ();
let user = UserItem {
id : "1" . into (),
display_name : "Alice" . into (),
age : 25 ,
};
let result = filter . evaluate ( & user ) . unwrap ();
assert_eq! ( result , bomboni_request :: value :: Value :: Boolean ( true ));
Filter Collection
let users = vec! [
UserItem { id : "1" . into (), display_name : "Alice" . into (), age : 25 },
UserItem { id : "2" . into (), display_name : "Bob" . into (), age : 17 },
UserItem { id : "3" . into (), display_name : "Charlie" . into (), age : 30 },
];
let filter = Filter :: parse ( "age >= 18" ) . unwrap ();
let adults : Vec < _ > = users . into_iter ()
. filter ( | user | {
if let Some ( Value :: Boolean ( true )) = filter . evaluate ( user ) {
true
} else {
false
}
})
. collect ();
assert_eq! ( adults . len (), 2 ); // Alice and Charlie
Value Types
Supported value types in filters:
Integer
Float
String
Boolean
Timestamp
let filter = Filter :: parse ( "age = 30" ) . unwrap ();
let filter = Filter :: parse ( "count >= 100" ) . unwrap ();
Error Handling
use bomboni_request :: filter :: Filter ;
match Filter :: parse ( "invalid filter syntax ====" ) {
Ok ( filter ) => println! ( "Parsed: {}" , filter ),
Err ( e ) => eprintln! ( "Parse error: {}" , e ),
}
let filter = Filter :: parse ( "unknown_field = 42" ) . unwrap ();
match filter . validate ( & schema , None ) {
Ok ( _ ) => println! ( "Valid filter" ),
Err ( e ) => eprintln! ( "Validation error: {}" , e ),
}
Best Practices
Always validate filters against schemas before evaluation or SQL generation to catch errors early.
Define Schemas
Create comprehensive schemas for all filterable resources with appropriate value types
Set Length Limits
Configure max_filter_length in query builders to prevent excessive filter complexity
Implement SchemaMapped
Ensure all fields referenced in filters are properly mapped in SchemaMapped implementations
Handle Errors
Provide clear error messages to API clients when filters fail validation
SQL Generation Convert filters to SQL WHERE clauses
Pagination Use filters with list queries