OxiMod
Overview
OxiMod is a schema-based modeling layer for MongoDB, designed for Rust developers who want a more expressive way to define models without giving up direct access to the MongoDB driver.
Inspired by ODM-style workflows, OxiMod provides:
- derive-based schema configuration
- builder-style model construction
- field-level validations, defaults, and indexes
- convenient model helpers
- global and explicit-client workflows
- optional lifecycle hooks
At the same time, it preserves MongoDB’s native power by exposing:
mongodb::Collection<Self>mongodb::Collection<Document>
OxiMod is best understood as:
MongoDB with stronger model ergonomics, not a replacement for the driver.
This ensures:
- zero feature lock-in
- full MongoDB flexibility
- long-term maintainability
Example
use ;
use ;
use ;
async
For more examples, feel free to check out the examples/ directory.
Model API
The following methods are automatically generated when deriving the Model macro and provide a typed interface for interacting with your MongoDB collection.
Core
| Method/Associated Function | Signature | Description |
|---|---|---|
save() |
async fn save(&self) -> Result<ObjectId, OxiModError> |
Inserts the current model instance into the database. Runs validation and non-mutable hooks, and returns the inserted document’s _id. |
save_mut() |
async fn save_mut(&mut self) -> Result<ObjectId, OxiModError> |
Inserts the model while allowing mutable hooks to modify it before persistence. Returns the inserted document’s _id. |
clear() |
async fn clear() -> Result<DeleteResult, OxiModError> |
Deletes all documents in the model’s collection. Returns a DeleteResult indicating how many documents were removed. |
validate() |
fn validate(&self) -> Result<(), OxiModError> |
Validates the model instance against all defined validation rules. Returns Ok(()) if valid, or a Validation error containing all field violations. |
get_collection() |
fn get_collection() -> Result<Collection<Self>, OxiModError> |
Returns the typed mongodb::Collection<Self> for performing advanced or custom queries. |
get_document_collection() |
fn get_document_collection() -> Result<Collection<Document>, OxiModError> |
Returns the raw mongodb::Collection<Document> for working directly with BSON when full flexibility is needed. |
Identity Helpers
| Associated Function | Signature | Description |
|---|---|---|
find_by_id() |
async fn find_by_id(id: ObjectId) -> Result<Option<Self>, OxiModError> |
Fetches a document by its _id. Returns Some(model) if found, otherwise None. |
update_by_id() |
async fn update_by_id(id: ObjectId, update: Document) -> Result<UpdateResult, OxiModError> |
Updates a document by _id using a MongoDB update document (e.g., $set). Returns an UpdateResult describing the operation. |
delete_by_id() |
async fn delete_by_id(id: ObjectId) -> Result<DeleteResult, OxiModError> |
Deletes a document by _id. Returns a DeleteResult indicating whether a document was removed. |
Utilities
| Associated Function | Signature | Description |
|---|---|---|
exists() |
async fn exists(filter: Document) -> Result<bool, OxiModError> |
Checks if at least one document matches the given filter. Returns true if a match exists. |
count() |
async fn count(filter: Document) -> Result<u64, OxiModError> |
Counts the number of documents matching the given filter and returns the total. |
Builder API
OxiMod generates builder-style setter methods for models, making it easier to construct documents in a fluent and readable way.
By default, the generated _id setter is named id(). If you want a different name, you can customize it with the #[document_id_setter_ident("id_setter")] attribute.
use ObjectId;
use Model;
use ;
Once derived, you can build a model instance like this:
let user = new
.id_settter
.name
.age
.active;
The builder API provides a fluent and ergonomic way to construct model instances with type-safe setters generated at compile time.
Features
-
Flexible input types (
Into<T>)
Setter methods accept any type that can be converted into the field type, such as&strfor aStringfield, which helps reduce unnecessary boilerplate. -
Automatic type conversion
Values are converted into their target field types internally, allowing concise and expressive model construction. -
Default value support
Fields annotated with#[default(...)]are automatically populated if they are not explicitly set by the builder. -
Optional and required fields
The builder supports bothOption<T>and required fields, making it suitable for a wide range of model definitions. -
Customizable
_idsetter
By default, the generated setter for_idisid(). You can rename it with#[document_id_setter_ident("id_setter")]when a custom method name better fits your API.
Client Usage
OxiMod supports two client access patterns: a global client for simple application-wide usage, and an explicit client for cases where you want more control over how connections are managed.
The global client is ideal when your application uses a single MongoDB connection shared across model operations. Explicit clients are better when you want to avoid global state or pass a client directly into your models and queries.
Global
Use the global client when your application has a single primary MongoDB connection and you want OxiMod methods like save(), find_by_id(), and count() to work without manually passing a client every time.
Initialize it once, typically during application startup:
init_global.await?;
user.save.await?;
After the global client has been initialized, model methods that do not take a client explicitly will automatically use it internally.
Explicit
Use the explicit-client API when you want to pass a specific MongoDB client into each operation instead of relying on global state.
user.save_from.await?;
This pattern is useful when you need tighter control over connection lifetimes or when different parts of your application may use different database clients.
Used for:
- tests
- multi-tenant applications
- dependency injection
- scoped or non-global connection management
OxiClient API
| Method/Associated Function | Signature | Description |
|---|---|---|
new() |
async fn new(url: String) -> Result<OxiClient, OxiModError> |
Creates a new OxiClient instance by connecting to MongoDB using the provided connection string. The resulting client stores a local mongodb::Client internally. |
init_client() |
async fn init_client(&mut self, mongo_uri: String) -> Result<(), OxiModError> |
Initializes or replaces the local MongoDB client stored inside an existing OxiClient instance. |
client() |
fn client(&self) -> Option<&Client> |
Returns a reference to the local inner mongodb::Client if one has been initialized. |
client_mut() |
fn client_mut(&mut self) -> Option<&Client> |
Returns access to the local inner mongodb::Client if initialized, for advanced driver-level usage. |
init_global() |
async fn init_global(mongo_uri: String) -> Result<(), OxiModError> |
Initializes the global MongoDB client used by OxiMod APIs that do not receive an explicit client. This should typically be called once at application startup. |
global() |
fn global() -> Result<Arc<Client>, OxiModError> |
Returns the globally initialized MongoDB client. Fails if init_global() has not been called yet. |
Choosing between global and explicit clients
| Pattern | Best for |
|---|---|
| Global client | Applications with a single shared MongoDB connection and a simpler setup. |
| Explicit client | Tests, multi-tenant systems, dependency injection, and cases where you want to avoid relying on global state. |
Collections
OxiMod provides two ways to access MongoDB collections: typed collections for type-safe operations and raw collections for full flexibility.
Typed
let collection = get_collection?;
Returns mongodb::Collection<Self>, enabling type-safe queries and automatic (de)serialization. This is the recommended approach for most use cases.
API
| Method/Associated Function | Signature | Description |
|---|---|---|
get_collection() |
fn get_collection() -> Result<Collection<Self>, OxiModError> |
Gets the typed collection using the global client. |
get_collection_from() |
fn get_collection_from(client: &Client) -> Result<Collection<Self>, OxiModError> |
Gets the typed collection using an explicit client. |
Raw
let collection = get_document_collection?;
Returns mongodb::Collection<Document>, allowing direct interaction with BSON documents.
API
| Associated Function | Signature | Description |
|---|---|---|
get_document_collection() |
fn get_document_collection() -> Result<Collection<Document>, OxiModError> |
Gets the raw collection using the global client. |
get_document_collection_from() |
fn get_document_collection_from(client: &Client) -> Result<Collection<Document>, OxiModError> |
Gets the raw collection using an explicit client. |
When to use each
| Type | Best for |
|---|---|
| Typed | Everyday usage with type safety |
| Raw | Advanced queries and dynamic data |
Attributes
OxiMod uses attributes to configure how your models behave at compile time.
Struct-level attributes define how your model maps to MongoDB and how certain features (like indexes and hooks) behave.
Struct-level
| Attribute | Description |
|---|---|
#[db("name")] |
Required. Specifies the MongoDB database name where this model will be stored. This is required for resolving the correct database at runtime. |
#[collection("name")] |
Required. Defines the MongoDB collection name associated with the model. All operations (save, query, delete) will target this collection. |
#[document_id_setter_ident("id_setter")] |
Renames the generated builder setter for the _id field. By default, the setter is id(), but this allows you to customize it (e.g., id_setter()). |
#[index_max_retries(N)] |
Sets how many times OxiMod will retry index creation during initialization. Useful for handling transient database issues during startup. |
#[index_max_init_seconds(N)] |
Specifies the maximum time (in seconds) allowed for index initialization before timing out. Helps prevent long startup delays. |
#[hooks] |
Enables lifecycle hooks (e.g., pre/post save). Without this attribute, hook-related logic is not generated, avoiding unnecessary overhead. |
Example
Field-level
Indexing
Use #[index(...)] on a field to declare a MongoDB index directly in your model.
These options let you define common index behavior close to the field itself, without dropping down to the full driver API.
Core
| Attribute | Description |
|---|---|
unique |
Creates a unique index, preventing duplicate values for the indexed field. |
sparse |
Excludes documents where the field is missing from the index. |
hidden |
Creates the index but hides it from the query planner unless explicitly hinted. |
name = "..." |
Assigns a custom name to the index instead of using MongoDB’s generated name. |
order = 1/-1 |
Sets ascending (1) or descending (-1) order for a standard scalar index. |
expire_after_secs = N |
Creates a TTL index so documents expire automatically after N seconds. |
background |
Builds the index in the background to reduce disruption to database operations. |
Advanced Types
| Attribute | Description |
|---|---|
text |
Creates a text index for MongoDB text search. |
hashed |
Creates a hashed index, useful for hashed lookups and some sharding strategies. |
wildcard |
Creates a wildcard-style index for dynamic or document-like fields. |
geo_2dsphere |
Creates a 2dsphere geospatial index for spherical location queries. |
geo_2d |
Creates a 2d geospatial index for planar coordinate queries. |
Advanced Options
| Attribute | Description |
|---|---|
version = N |
Sets the version of a standard index structure when supported by MongoDB. |
text_index_version = N |
Sets the version of a text index. Only meaningful with text. |
geo_2dsphere_index_version = N |
Sets the version of a 2dsphere index. Only meaningful with geo_2dsphere. |
weight = N |
Assigns a weight to a text-indexed field to influence text search scoring. |
default_language = "..." |
Sets the default language used by a text index. |
language_override = "..." |
Specifies the document field that overrides the default text index language. |
case_insensitive |
Applies a case-insensitive collation preset for string-based lookups. |
bits = N |
Sets precision for a geo_2d index. |
min = N |
Sets the lower bound for a geo_2d index. |
max = N |
Sets the upper bound for a geo_2d index. |
Example
email: String,
title: String,
location: GeoJsonPoint,
Notes
- These options are meant to cover common field-level index needs in a compact way.
- Specialized index types such as
text,hashed,wildcard,geo_2dsphere, andgeo_2dare generally used one at a time per field. - Some options only make sense with specific index types. For example,
weightapplies totext, whilebits,min, andmaxapply togeo_2d.
Validation
Use #[validate(...)] to attach field-level validation rules directly to your model fields.
These validators are checked before persistence, helping you catch invalid data close to the model itself. OxiMod also performs compile-time checks to prevent validators from being applied to incompatible field types. For example, string validators are restricted to string-like fields, numeric validators to numeric fields, and required to Option<T> fields.
Length
These validators apply to types with a length, such as String, Vec<T>, arrays, and map/set types.
| Validator | Description |
|---|---|
min_length = N |
Requires the value length to be at least N. |
max_length = N |
Requires the value length to be at most N. |
non_empty |
Requires the value to contain at least one element or character. Equivalent to min_length = 1. |
String
These validators apply to string-like fields.
| Validator | Description |
|---|---|
starts_with = "..." |
Requires the string to start with the given prefix. |
ends_with = "..." |
Requires the string to end with the given suffix. |
includes = "..." |
Requires the string to contain the given substring. |
alphanumeric |
Restricts the value to ASCII letters and digits only. |
email |
Requires the value to match a basic email format. |
pattern = "..." |
Validates the value against a custom regular expression. |
Numeric
These validators apply to numeric fields. OxiMod supports inclusive range bounds through min and max, and also supports exclusive bounds with min_exclusive and max_exclusive.
| Validator | Description |
|---|---|
min = N / max = N |
Sets inclusive lower and upper bounds for the value. |
min_exclusive / max_exclusive |
Makes min strictly greater than and/or max strictly less than. |
positive |
Requires the value to be greater than 0. |
negative |
Requires the value to be less than 0. |
non_negative |
Requires the value to be greater than or equal to 0. |
non_positive |
Requires the value to be less than or equal to 0. |
Integer
These validators apply only to integer fields.
| Validator | Description |
|---|---|
multiple_of = N |
Requires the value to be evenly divisible by N. |
Optional
These validators apply to Option<T> fields.
| Validator | Description |
|---|---|
required |
Rejects None and requires the field to contain Some(...). |
Custom
You can also provide your own validator function:
Custom validators run after the built-in validations. The function receives a reference to the validated field type and must return Result<(), String>. For optional fields, OxiMod validates the inner type, so a custom validator on Option<String> receives &String, not &Option<String>. This keeps custom validation ergonomic and flexible.
Because your validator receives a reference to the field's effective validated type, you can keep the function focused on the actual value being checked. This works naturally alongside OxiMod's builder conversions and typed model fields.
Example
Defaults
Use #[default(...)] to assign a default value to a field when it is not explicitly set through the builder.
This allows you to define fallback values directly in your model, keeping initialization logic simple and centralized.
When a field is not provided during construction, OxiMod automatically applies the specified default before validation and persistence.
Examples
name: String,
score: i32,
active: bool,
Behavior
-
Applied during model construction
Defaults are applied if the field is not set via the builder API. -
Works with builder setters (
Into<T>)
Since setters acceptInto<T>, defaults integrate seamlessly with flexible inputs (e.g.,&str→String). -
Supports optional and required fields
- For required fields, the default acts as a fallback value.
- For
Option<T>, defaults can still be applied if no value is provided.
-
Type-safe at compile time
The default value must match the field type, ensuring correctness at compile time.
Example
let user = new;
// name = "Guest", active = false
Notes
- Defaults reduce boilerplate by eliminating the need to manually initialize common values.
- They are applied before validation, so validation rules still apply to defaulted values.
Hooks
Hooks provide lifecycle extension points for model operations such as saving, querying, updating, and deleting documents.
They allow you to inject custom logic directly into your model workflows without modifying the core database logic.
Hooks are optional and must be enabled at the struct level using #[hooks].
Save Hooks
| Hook | Signature | Description |
|---|---|---|
pre_save |
async fn pre_save(&self) -> Result<(), OxiModError> |
Runs before save(). Use for validation or checks without mutation. |
post_save |
async fn post_save(&self) -> Result<(), OxiModError> |
Runs after save(). Useful for logging or side effects. |
pre_save_mut |
async fn pre_save_mut(&mut self) -> Result<(), OxiModError> |
Runs before save_mut(). Allows modifying the model before persistence. |
post_save_mut |
async fn post_save_mut(&mut self) -> Result<(), OxiModError> |
Runs after save_mut(). Can modify in-memory state (not auto-persisted). |
Query Hooks
| Hook | Signature | Description |
|---|---|---|
pre_find |
async fn pre_find(id: ObjectId) -> Result<(), OxiModError> |
Runs before find_by_id(). Useful for access control or logging. |
post_find |
async fn post_find(result: &Option<Self>) -> Result<(), OxiModError> |
Runs after find_by_id(). Allows inspection of the fetched result. |
Mutation Hooks
| Hook | Signature | Description |
|---|---|---|
pre_update |
async fn pre_update(id: ObjectId, update: &Document) -> Result<(), OxiModError> |
Runs before update_by_id(). Can abort based on custom logic. |
post_update |
async fn post_update(id: ObjectId, update: &Document) -> Result<(), OxiModError> |
Runs after update_by_id(). Useful for logging or events. |
pre_delete |
async fn pre_delete(id: ObjectId) -> Result<(), OxiModError> |
Runs before delete_by_id(). Can prevent deletion. |
post_delete |
async fn post_delete(id: ObjectId) -> Result<(), OxiModError> |
Runs after delete_by_id(). Useful for cleanup or logging. |
Behavior
-
Opt-in feature
Hooks are only generated when#[hooks]is present. -
Model-level only
Hooks run only for OxiMod model APIs (not raw collections). -
Default no-op
All hooks default toOk(()); implement only what you need. -
Error handling
- Pre-hook errors abort operations
- Post-hook errors indicate post-processing failure
Example
use ;
License
MIT