Overview
The ordering system allows API clients to specify sort orders for query results. It supports multiple fields with explicit directions and validates ordering terms against schemas to ensure only orderable fields are used.
Ordering Syntax
Basic Ordering
Order by a single field:
use bomboni_request :: ordering :: Ordering ;
// Ascending order (default)
let ordering = Ordering :: parse ( "name" ) . unwrap ();
// Descending order
let ordering = Ordering :: parse ( "age desc" ) . unwrap ();
// Explicit ascending
let ordering = Ordering :: parse ( "created_at asc" ) . unwrap ();
Multi-Field Ordering
Order by multiple fields with comma separation:
// Order by name (ascending), then age (descending)
let ordering = Ordering :: parse ( "name asc, age desc" ) . unwrap ();
// Mixed with defaults
let ordering = Ordering :: parse ( "priority desc, created_at" ) . unwrap ();
Nested Fields
Use dot notation for nested object fields:
let ordering = Ordering :: parse ( "user.displayName desc, task.userId asc" ) . unwrap ();
Ordering Directions
use bomboni_request :: ordering :: { OrderingDirection , OrderingTerm };
let term = OrderingTerm {
name : "age" . into (),
direction : OrderingDirection :: Ascending ,
};
// Results ordered from smallest to largest
Parsing and Validation
Parse from String
use bomboni_request :: ordering :: Ordering ;
let ordering = Ordering :: parse ( "displayName desc, age asc" ) . unwrap ();
assert_eq! ( ordering . to_string (), "displayName desc, age asc" );
Schema Validation
Only fields marked as ordered in the schema can be used for ordering:
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 ), // Not orderable
},
};
// Valid - both fields are orderable
let ordering = Ordering :: parse ( "id asc, age desc" ) . unwrap ();
ordering . validate ( & schema ) . unwrap ();
// Invalid - name is not marked as orderable
let invalid_ordering = Ordering :: parse ( "name asc" ) . unwrap ();
assert! ( invalid_ordering . validate ( & schema ) . is_err ());
Ordering Evaluation
Compare Items
Use ordering to compare two items:
use bomboni_request :: ordering :: Ordering ;
use bomboni_request :: testing :: schema :: UserItem ;
use std :: cmp :: Ordering as CmpOrdering ;
let ordering = Ordering :: parse ( "displayName desc, age asc" ) . unwrap ();
let a = UserItem {
id : "1" . into (),
display_name : "Alice" . into (),
age : 30 ,
};
let b = UserItem {
id : "2" . into (),
display_name : "Bob" . into (),
age : 25 ,
};
let comparison = ordering . evaluate ( & a , & b ) . unwrap ();
assert_eq! ( comparison , CmpOrdering :: Greater ); // Alice > Bob (desc order)
Sort Collections
use bomboni_request :: ordering :: Ordering ;
use bomboni_request :: testing :: schema :: UserItem ;
let mut users = vec! [
UserItem { id : "3" . into (), display_name : "Charlie" . into (), age : 25 },
UserItem { id : "1" . into (), display_name : "Alice" . into (), age : 30 },
UserItem { id : "2" . into (), display_name : "Bob" . into (), age : 25 },
];
let ordering = Ordering :: parse ( "age desc, displayName asc" ) . unwrap ();
users . sort_by ( | a , b | ordering . evaluate ( a , b ) . unwrap ());
// Results:
// 1. Alice (age: 30)
// 2. Bob (age: 25, alphabetically first)
// 3. Charlie (age: 25, alphabetically second)
Ordering Terms
Access individual ordering terms:
use bomboni_request :: ordering :: Ordering ;
let ordering = Ordering :: parse ( "priority desc, created_at asc" ) . unwrap ();
for term in ordering . iter () {
println! ( "Field: {}, Direction: {}" , term . name, term . direction);
}
// Output:
// Field: priority, Direction: desc
// Field: created_at, Direction: asc
Integration with Queries
List Queries
Ordering is automatically integrated with list queries:
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 ;
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 ),
},
PlainPageTokenBuilder {},
);
let query = builder . build (
Some ( 50 ),
None ,
None ,
Some ( "displayName desc, age asc" ) // Ordering parameter
) . unwrap ();
assert_eq! ( query . ordering . to_string (), "id asc, displayName desc, age asc" );
// Note: primary_ordering_term (id asc) is prepended automatically
Primary Ordering Terms
The primary_ordering_term is automatically prepended to ordering specifications if not already present. This ensures consistent pagination behavior.
let config = ListQueryConfig {
primary_ordering_term : Some ( OrderingTerm {
name : "id" . into (),
direction : OrderingDirection :: Ascending ,
}),
// ... other config
};
// Client requests: "age desc"
// Actual ordering used: "id asc, age desc"
SQL Generation
Convert ordering to SQL ORDER BY clauses:
use bomboni_request :: sql :: { SqlDialect , ordering :: SqlOrderingBuilder };
use bomboni_request :: ordering :: Ordering ;
use bomboni_request :: testing :: schema :: UserItem ;
let ordering = Ordering :: parse ( "displayName desc, age asc" ) . unwrap ();
let schema = UserItem :: get_schema ();
let sql = SqlOrderingBuilder :: new ( SqlDialect :: Postgres , & schema )
. build ( & ordering )
. unwrap ();
assert_eq! ( sql , r#""displayName" DESC, "age" ASC"# );
Error Handling
Parse Errors
use bomboni_request :: ordering :: Ordering ;
// Duplicate fields are not allowed
let result = Ordering :: parse ( "name asc, name desc" );
assert! ( result . is_err ()); // DuplicateField error
// Invalid direction
let result = Ordering :: parse ( "name invalid" );
assert! ( result . is_err ()); // InvalidDirection error
// Too many parts
let result = Ordering :: parse ( "name asc extra" );
assert! ( result . is_err ()); // InvalidTermFormat error
Validation Errors
// Unknown field
let ordering = Ordering :: parse ( "unknown_field asc" ) . unwrap ();
let result = ordering . validate ( & schema );
assert! ( result . is_err ()); // UnknownMember error
// Unorderable field
let ordering = Ordering :: parse ( "description asc" ) . unwrap ();
let result = ordering . validate ( & schema );
assert! ( result . is_err ()); // UnorderedField error
Length Limits
Configure maximum ordering specification length:
use bomboni_request :: query :: list :: ListQueryConfig ;
let config = ListQueryConfig {
max_ordering_length : Some ( 100 ), // Maximum 100 characters
// ... other config
};
// Ordering strings exceeding this limit will be rejected
Best Practices
Mark Appropriate Fields as Orderable
Only mark indexed fields as orderable in your schema to ensure query performance
Always Include Primary Ordering
Use primary_ordering_term to ensure deterministic pagination with stable sort orders
Validate Before Use
Always validate ordering specifications against schemas before evaluation or SQL generation
Set Length Limits
Configure max_ordering_length to prevent excessively complex ordering specifications
Without a primary ordering term that guarantees unique sort order, pagination behavior may be unpredictable when multiple items have the same values for all ordering fields.
Complete Example
use bomboni_request :: ordering :: { Ordering , OrderingDirection };
use bomboni_request :: testing :: schema :: UserItem ;
// Parse ordering specification
let ordering = Ordering :: parse ( "displayName desc, age asc" ) . unwrap ();
assert_eq! ( ordering . to_string (), "displayName desc, age asc" );
// Compare items
let alice = UserItem {
id : "1" . into (),
display_name : "Alice" . into (),
age : 30 ,
};
let bob = UserItem {
id : "2" . into (),
display_name : "Bob" . into (),
age : 25 ,
};
let comparison = ordering . evaluate ( & alice , & bob ) . unwrap ();
assert_eq! ( comparison , std :: cmp :: Ordering :: Greater ); // Alice > Bob by displayName desc
// Sort a collection
let mut users = vec! [ bob . clone (), alice . clone ()];
users . sort_by ( | a , b | ordering . evaluate ( a , b ) . unwrap ());
assert_eq! ( users [ 0 ] . display_name, "Bob" ); // Bob comes first (desc order)
Pagination Use ordering with list queries
SQL Generation Convert ordering to SQL ORDER BY clauses