Overview
The pagination system implements Google AIP-132 standard for List methods, providing secure, efficient pagination with support for filtering and ordering. It includes specialized builders for both list and search queries.
Page Token Types
Page tokens encode the position for the next page of results. Multiple encoding strategies are available:
Plain No encoding - useful for development and debugging
Base64 Base64 encoding - simple obfuscation
AES256 AES-256 symmetric encryption - secure with shared key
RSA RSA asymmetric encryption - secure with key pairs
Use encrypted page tokens (AES256 or RSA) in production to prevent token tampering and information disclosure.
List Queries
Basic List Query
use bomboni_request :: query :: list :: { ListQueryBuilder , ListQueryConfig };
use bomboni_request :: query :: page_token :: plain :: PlainPageTokenBuilder ;
use bomboni_request :: ordering :: { OrderingTerm , OrderingDirection };
use bomboni_request :: testing :: schema :: UserItem ;
use std :: collections :: BTreeMap ;
// Create list query builder
let list_builder = ListQueryBuilder :: new (
UserItem :: get_schema (),
BTreeMap :: new (),
ListQueryConfig {
max_page_size : Some ( 100 ),
default_page_size : 20 ,
primary_ordering_term : Some ( OrderingTerm {
name : "id" . into (),
direction : OrderingDirection :: Ascending ,
}),
max_filter_length : Some ( 1000 ),
max_ordering_length : Some ( 100 ),
},
PlainPageTokenBuilder {},
);
// Build list query
let list_query = list_builder . build (
Some ( 50 ), // page_size
None , // page_token
Some ( r#"displayName = "John""# ), // filter
Some ( "age desc" ) // ordering
) . unwrap ();
assert_eq! ( list_query . page_size, 50 );
assert_eq! ( list_query . filter . to_string (), r#"displayName = "John""# );
assert_eq! ( list_query . ordering . to_string (), "id asc, age desc" );
List Query Configuration
Maximum allowed page size. Requests exceeding this are automatically clamped.
Default page size when client doesn’t specify one.
Primary ordering term prepended to all queries. Should be a unique field like id to ensure deterministic pagination.
Maximum allowed filter string length in characters.
Maximum allowed ordering string length in characters.
Building Next Page Tokens
use bomboni_request :: testing :: schema :: UserItem ;
// Assume we fetched page_size + 1 items
let users = vec! [
// ... first page of results
];
if users . len () > list_query . page_size as usize {
// There's a next page
let last_item = & users [ list_query . page_size as usize - 1 ];
let next_page_token = list_builder
. build_next_page_token ( & list_query , last_item )
. unwrap ();
// Return next_page_token to client
}
Search Queries
Basic Search Query
Search queries extend list queries with a text search query:
use bomboni_request :: query :: search :: { SearchQueryBuilder , SearchQueryConfig };
use bomboni_request :: query :: page_token :: plain :: PlainPageTokenBuilder ;
use bomboni_request :: ordering :: { OrderingTerm , OrderingDirection };
use bomboni_request :: testing :: schema :: UserItem ;
use std :: collections :: BTreeMap ;
// Create search query builder
let search_builder = SearchQueryBuilder :: new (
UserItem :: get_schema (),
BTreeMap :: new (),
SearchQueryConfig {
max_query_length : Some ( 100 ),
max_page_size : Some ( 20 ),
default_page_size : 10 ,
primary_ordering_term : Some ( OrderingTerm {
name : "id" . into (),
direction : OrderingDirection :: Descending ,
}),
max_filter_length : Some ( 1000 ),
max_ordering_length : Some ( 100 ),
},
PlainPageTokenBuilder {},
);
// Build search query
let search_query = search_builder . build (
"john doe" , // search query text
Some ( 25 ), // page_size
None , // page_token
Some ( r#"age >= 18 AND displayName = "John""# ), // filter
Some ( "age desc, displayName asc" ) // ordering
) . unwrap ();
assert_eq! ( search_query . query, "john doe" );
assert_eq! ( search_query . page_size, 20 ); // Clamped to max_page_size
assert_eq! ( search_query . filter . to_string (), r#"age >= 18 AND displayName = "John""# );
assert_eq! ( search_query . ordering . to_string (), "id desc, age desc, displayName asc" );
Search Query Configuration
Extends ListQueryConfig with:
Maximum allowed search query string length.
Encrypted Page Tokens
AES256 (Symmetric Encryption)
use bomboni_request :: query :: list :: Aes256ListQueryBuilder ;
use bomboni_request :: query :: page_token :: aes256 :: Aes256PageTokenBuilder ;
// Generate a 32-byte key
let key = b"your-32-byte-secret-key-here!!!!" ;
let builder = Aes256ListQueryBuilder :: new (
schema ,
schema_functions ,
config ,
Aes256PageTokenBuilder :: new ( * key ),
);
// Tokens are now encrypted with AES-256
let query = builder . build ( Some ( 20 ), None , None , None ) . unwrap ();
RSA (Asymmetric Encryption)
use bomboni_request :: query :: list :: RsaListQueryBuilder ;
use bomboni_request :: query :: page_token :: rsa :: RsaPageTokenBuilder ;
use rsa :: { RsaPrivateKey , RsaPublicKey };
// Generate RSA key pair
let mut rng = rand :: thread_rng ();
let private_key = RsaPrivateKey :: new ( & mut rng , 2048 ) . unwrap ();
let public_key = RsaPublicKey :: from ( & private_key );
let builder = RsaListQueryBuilder :: new (
schema ,
schema_functions ,
config ,
RsaPageTokenBuilder :: new ( private_key , public_key ),
);
// Tokens are now RSA encrypted
let query = builder . build ( Some ( 20 ), None , None , None ) . unwrap ();
Base64 Encoding
use bomboni_request :: query :: list :: Base64ListQueryBuilder ;
use bomboni_request :: query :: page_token :: base64 :: Base64PageTokenBuilder ;
let builder = Base64ListQueryBuilder :: new (
schema ,
schema_functions ,
config ,
Base64PageTokenBuilder {},
);
// Tokens are base64 encoded (not secure, but obfuscated)
let query = builder . build ( Some ( 20 ), None , None , None ) . unwrap ();
Parse Derive Integration
Automate query parsing with the Parse derive macro:
use bomboni_request_derive :: Parse ;
use bomboni_request :: parse :: RequestParse ;
use bomboni_request :: query :: list :: ListQuery ;
#[derive( Debug , Clone , PartialEq , Default )]
struct ListUsersRequest {
page_size : Option < u32 >,
page_token : Option < String >,
filter : Option < String >,
order_by : Option < String >,
}
#[derive( Debug , Clone , PartialEq , Parse )]
#[parse(source = ListUsersRequest , write)]
struct ParsedListUsersRequest {
#[parse(list_query)]
query : ListQuery ,
}
// Parse request automatically
let request = ListUsersRequest {
page_size : Some ( 50 ),
page_token : None ,
filter : Some ( r#"displayName = "John""# . into ()),
order_by : Some ( "age desc" . into ()),
};
let parsed = ParsedListUsersRequest :: parse_list_query ( request , & list_builder ) . unwrap ();
assert_eq! ( parsed . query . page_size, 50 );
Search Query with Parse Derive
#[derive( Debug , Clone , PartialEq , Default )]
struct SearchUsersRequest {
query : String ,
page_size : Option < u32 >,
page_token : Option < String >,
filter : Option < String >,
order_by : Option < String >,
}
#[derive( Debug , Clone , PartialEq , Parse )]
#[parse(source = SearchUsersRequest , write)]
struct ParsedSearchUsersRequest {
#[parse(search_query)]
query : SearchQuery ,
}
let request = SearchUsersRequest {
query : "john doe" . to_string (),
page_size : Some ( 25 ),
page_token : None ,
filter : Some ( r#"age >= 18"# . into ()),
order_by : Some ( "age desc" . into ()),
};
let parsed = ParsedSearchUsersRequest :: parse_search_query ( request , & search_builder ) . unwrap ();
assert_eq! ( parsed . query . query, "john doe" );
Complete API Example
use bomboni_request :: query :: list :: { ListQueryBuilder , ListQueryConfig };
use bomboni_request :: query :: page_token :: aes256 :: Aes256PageTokenBuilder ;
use bomboni_request :: ordering :: { OrderingTerm , OrderingDirection };
use std :: collections :: BTreeMap ;
// Initialize builder (typically done once at startup)
let builder = ListQueryBuilder :: new (
UserItem :: get_schema (),
BTreeMap :: new (),
ListQueryConfig {
max_page_size : Some ( 100 ),
default_page_size : 20 ,
primary_ordering_term : Some ( OrderingTerm {
name : "id" . into (),
direction : OrderingDirection :: Ascending ,
}),
max_filter_length : Some ( 1000 ),
max_ordering_length : Some ( 100 ),
},
Aes256PageTokenBuilder :: new ( * b"your-32-byte-secret-key-here!!!!" ),
);
// In your API handler
fn list_users (
page_size : Option < i32 >,
page_token : Option < String >,
filter : Option < String >,
order_by : Option < String >,
) -> Result < ListUsersResponse , Error > {
// Parse and validate request
let query = builder . build (
page_size ,
page_token . as_deref (),
filter . as_deref (),
order_by . as_deref (),
) ? ;
// Fetch page_size + 1 items to determine if there's a next page
let limit = query . page_size + 1 ;
let users = fetch_users_from_db ( & query . filter, & query . ordering, limit ) ? ;
// Build response
let has_next = users . len () > query . page_size as usize ;
let mut response_users = users ;
if has_next {
response_users . truncate ( query . page_size as usize );
}
let next_page_token = if has_next {
Some ( builder . build_next_page_token ( & query , users . last () . unwrap ()) ? )
} else {
None
};
Ok ( ListUsersResponse {
users : response_users ,
next_page_token ,
})
}
Best Practices
Use Encrypted Tokens in Production
Always use AES256 or RSA page tokens in production environments to prevent tampering
Fetch N+1 Items
Fetch page_size + 1 items to efficiently determine if there’s a next page without a separate count query
Set Primary Ordering
Always configure a primary_ordering_term using a unique field (like id) to ensure stable pagination
Configure Limits
Set appropriate max_page_size, max_filter_length, and max_ordering_length to prevent abuse
Validate Tokens
Page token builders automatically validate that tokens match the current filter and ordering
Page tokens are tied to specific filter and ordering parameters. If a client changes these parameters, previous page tokens become invalid.
Error Handling
use bomboni_request :: query :: error :: QueryError ;
match builder . build ( Some ( - 1 ), None , None , None ) {
Err ( QueryError :: InvalidPageSize ) => {
// Handle invalid page size
}
Err ( QueryError :: FilterTooLong ) => {
// Handle filter exceeding max length
}
Err ( QueryError :: PageTokenInvalid ) => {
// Handle invalid/tampered page token
}
Ok ( query ) => {
// Process query
}
Err ( e ) => {
// Handle other errors
}
}
Filtering Learn about filter expressions
Ordering Understand ordering specifications
Parse Derive Auto-generate query parsers