Skip to main content

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:
OperatorDescriptionExample
=Equalage = 30
!=Not equalstatus != "deleted"
<Less thanprice < 100
<=Less than or equalage <= 65
>Greater thancount > 0
>=Greater than or equalscore >= 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:
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.
1

Define Schemas

Create comprehensive schemas for all filterable resources with appropriate value types
2

Set Length Limits

Configure max_filter_length in query builders to prevent excessive filter complexity
3

Implement SchemaMapped

Ensure all fields referenced in filters are properly mapped in SchemaMapped implementations
4

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

Build docs developers (and LLMs) love