9 releases
Uses new Rust 2024
| new 0.2.12 | Feb 18, 2026 |
|---|---|
| 0.2.11 | Feb 18, 2026 |
| 0.1.0 | Jan 25, 2026 |
#142 in Testing
290 downloads per month
Used in 16 crates
(via cf-modkit)
185KB
3.5K
SLoC
modkit-sdk
Security Context Scoping and Typed OData Query Builder for Clients
Overview
This crate provides two main features:
- Security Context Scoping: A lightweight, zero-allocation wrapper that binds a
SecurityContextto any client type - Typed OData Query Builder: A generic, reusable query builder that produces type-safe OData queries with automatic filter hashing
Security Context Scoping
Example
use modkit_sdk::WithSecurityContext;
use modkit_security::SecurityContext;
let client = MyClient::new();
let ctx = SecurityContext::root();
// Bind the security context to the client
let secured = client.security_ctx(&ctx);
// Access the client and context
let client_ref = secured.client();
let ctx_ref = secured.ctx();
Typed OData Query Builder
The typed OData query builder provides a type-safe way to construct OData queries without manually building ODataQuery instances. It ensures compile-time type checking for field references and operations.
Features
- Type-safe field references: Field types are checked at compile time
- Schema trait: Define field enums and their string mappings
- Typed filter constructors: Comparison and string operations with type safety
- Fluent API: Chain methods to build complex queries
- Automatic filter hashing: Stable, deterministic hashing for cursor pagination
- No proc macros: Pure Rust implementation without code generation
Quick Start
1. Define Your Schema
use modkit_sdk::odata::{Schema, FieldRef};
// Define field enum
#[derive(Copy, Clone, Eq, PartialEq)]
enum UserField {
Id,
Name,
Email,
Age,
}
// Define schema struct
struct UserSchema;
// Implement Schema trait
impl Schema for UserSchema {
type Field = UserField;
fn field_name(field: Self::Field) -> &'static str {
match field {
UserField::Id => "id",
UserField::Name => "name",
UserField::Email => "email",
UserField::Age => "age",
}
}
}
2. Create Typed Field References
// Define typed field constants
const ID: FieldRef<UserSchema, uuid::Uuid> = FieldRef::new(UserField::Id);
const NAME: FieldRef<UserSchema, String> = FieldRef::new(UserField::Name);
const EMAIL: FieldRef<UserSchema, String> = FieldRef::new(UserField::Email);
const AGE: FieldRef<UserSchema, i32> = FieldRef::new(UserField::Age);
3. Build Queries
use modkit_sdk::odata::{QueryBuilder, FilterExpr};
use modkit_odata::SortDir;
// Simple equality filter
let user_id = uuid::Uuid::new_v4();
let query = QueryBuilder::<UserSchema>::new()
.filter(ID.eq(user_id))
.build();
// Complex filter with AND/OR
let query = QueryBuilder::<UserSchema>::new()
.filter(
AGE.ge(18)
.and(AGE.le(65))
.and(NAME.contains("smith"))
)
.order_by(NAME, SortDir::Asc)
.page_size(50)
.build();
// Full query with all features
let query = QueryBuilder::<UserSchema>::new()
.filter(ID.eq(user_id).and(AGE.gt(18)))
.order_by(NAME, SortDir::Asc)
.order_by(AGE, SortDir::Desc)
.select([NAME, EMAIL])
.page_size(25)
.build();
Supported Operations
Comparison Operators (All Field Types)
eq(value)- Equality:field eq valuene(value)- Not equal:field ne valuegt(value)- Greater than:field gt valuege(value)- Greater or equal:field ge valuelt(value)- Less than:field lt valuele(value)- Less or equal:field le value
String Operations (String Fields Only)
contains(value)- Contains:contains(field, 'value')startswith(value)- Starts with:startswith(field, 'value')endswith(value)- Ends with:endswith(field, 'value')
Logical Combinators
and(expr)- Logical AND:expr1 and expr2or(expr)- Logical OR:expr1 or expr2not()- Logical NOT:not expr
Query Builder Methods
filter(expr)- Set the filter expressionorder_by(field, dir)- Add an order-by clause (can be called multiple times)select(fields)- Set field projection (pass&[&field1, &field2, ...])page_size(limit)- Set the page size limitbuild()- Build the finalODataQuerywith computed filter hash
Type Safety
The query builder enforces type safety at compile time:
// ✅ Correct: String field with string operations
let query = QueryBuilder::<UserSchema>::new()
.filter(NAME.contains("john"))
.build();
// ❌ Compile error: contains() only available for String fields
let query = QueryBuilder::<UserSchema>::new()
.filter(AGE.contains("test")) // Won't compile!
.build();
// ✅ Correct: Comparison operations work on all types
let query = QueryBuilder::<UserSchema>::new()
.filter(AGE.gt(18))
.build();
Filter Hash Stability
The query builder automatically computes a stable, deterministic hash for filter expressions using the same algorithm as modkit_odata::pagination::short_filter_hash. This ensures cursor pagination consistency:
let user_id = uuid::Uuid::new_v4();
let query1 = QueryBuilder::<UserSchema>::new()
.filter(ID.eq(user_id))
.build();
let query2 = QueryBuilder::<UserSchema>::new()
.filter(ID.eq(user_id))
.build();
// Same filter produces same hash
assert_eq!(query1.filter_hash, query2.filter_hash);
Supported Value Types
The following Rust types can be used in filter expressions:
booluuid::UuidStringand&stri32,i64,u32,u64
Additional types can be supported by implementing the IntoODataValue trait.
Examples
See examples/typed_odata_query.rs for examples demonstrating:
- Simple equality filters
- String operations (contains, startswith, endswith)
- Complex filters with AND/OR/NOT
- Ordering and field selection
- Page size limits
- Full queries with all features
- Filter hash stability
Run the example:
cargo run --package modkit-sdk --example typed_odata_query
Design Constraints
- No proc macros: Pure Rust implementation without code generation
- Small footprint: Minimal API surface with no large facades
- No DB concepts: Does not expose database or SeaORM concepts
- AST-based: Produces
modkit_odata::ast::Exprfor maximum flexibility - Stable hashing: Deterministic filter hashing for cursor pagination
Testing
The query builder includes comprehensive unit tests verifying:
- Field name mapping works correctly
- Building queries sets order/limit/filter properly
- Filter hash is stable for identical filters
- All comparison and string operations work
- Logical combinators (AND/OR/NOT) function correctly
- Field selection handles heterogeneous field types
Run tests:
cargo test --package modkit-sdk --lib odata
License
See workspace license.
Dependencies
~5–6.5MB
~132K SLoC