oximod 0.2.5

MongoDB ODM for Rust inspired by Mongoose
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
# OxiMod

<p align="center">
  <strong>Schema-aware MongoDB modeling for Rust</strong>
</p>

<p align="center">
  <img src="https://img.shields.io/crates/v/oximod">
  <img src="https://img.shields.io/crates/d/oximod">
  <img src="https://img.shields.io/badge/license-MIT-blue">
</p>

---

## 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

```rust
use mongodb::bson::{doc, oid::ObjectId};
use oximod::{Hooks, Model, OxiClient, OxiModError};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Model)]
#[db("my_app_db")]
#[collection("users")]
#[document_id_setter_ident("id_setter")]
#[hooks]
struct User {
    #[serde(skip_serializing_if = "Option::is_none")]
    _id: Option<ObjectId>,

    #[index(unique, name = "email_idx", case_insensitive)]
    #[validate(email)]
    email: String,

    #[validate(min_length = 3, max_length = 32)]
    name: String,

    #[validate(non_negative, min = 18)]
    age: i32,

    #[default(false)]
    active: bool,
}

#[async_trait::async_trait]
impl Hooks for User {
    async fn pre_save_mut(&mut self) -> Result<(), OxiModError> {
        self.email = self.email.trim().to_lowercase();
        self.name = self.name.trim().to_string();
        Ok(())
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    dotenv::dotenv().ok();
    let uri = std::env::var("MONGODB_URI")?;
    OxiClient::init_global(uri).await?;

    User::clear().await?;

    // Builder API with Into<T> support and defaults
    let mut user = User::new()
        .email("  ALICE@EXAMPLE.COM  ")
        .name("Alice")
        .age(30);

    // save_mut() runs mutable hooks before persistence
    let id = user.save_mut().await?;
    println!("Inserted user: {}", id);

    let found = User::find_by_id(id).await?;
    println!("Found user: {found:#?}");

    User::update_by_id(id, doc! { "$set": { "active": true } }).await?;

    let active_exists = User::exists(doc! { "active": true }).await?;
    let total = User::count(doc! {}).await?;
    println!("Any active users? {}", active_exists);
    println!("Total users: {}", total);

    // Access the typed MongoDB collection directly when needed
    let collection = User::get_collection()?;
    let active_users = collection.count_documents(doc! { "active": true }).await?;
    println!("Active users counted via driver: {}", active_users);

    Ok(())
}
```

For more examples, feel free to check out the [`examples/`](https://github.com/arshia-eskandari/oximod/tree/main/oximod/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.

```rust
use mongodb::bson::oid::ObjectId;
use oximod::Model;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Model)]
#[db("my_app_db")]
#[collection("users")]
#[document_id_setter_ident("id_setter")]
struct User {
    #[serde(skip_serializing_if = "Option::is_none")]
    _id: Option<ObjectId>,
    name: String,
    age: i32,
    active: bool,
}
```

Once derived, you can build a model instance like this:

```rust
let user = User::new()
    .id_settter(ObjectId::new())
    .name("Alice")
    .age(30)
    .active(true);
```

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 `&str` for a `String` field, 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 both `Option<T>` and required fields, making it suitable for a wide range of model definitions.

- **Customizable `_id` setter**  
  By default, the generated setter for `_id` is `id()`. 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:

```rust
OxiClient::init_global(uri).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.

```rust
user.save_from(&client).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

```rust
let collection = User::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

```rust
let collection = User::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

```rust
#[derive(Model)]
#[db("my_app_db")]
#[collection("users")]
#[document_id_setter_ident("id_setter")]
#[index_max_retries(3)]
#[index_max_init_seconds(10)]
#[hooks]
struct User {
    _id: Option<ObjectId>,
    name: String,
}
```

### 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.

```rust
#[index(...)]
```

##### 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

```rust
#[index(unique, sparse, name = "email_idx")]
email: String,

#[index(text, weight = 10, default_language = "english")]
title: String,

#[index(geo_2dsphere)]
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`, and `geo_2d` are generally used one at a time per field.
- Some options only make sense with specific index types. For example, `weight` applies to `text`, while `bits`, `min`, and `max` apply to `geo_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.

```rust
#[validate(...)]
```

##### 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:

```rust
#[validate(custom(fn_name))]
```

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.

```rust
fn validate_name(value: &String) -> Result<(), String> {
    if value == "admin" {
        return Err("reserved name".into());
    }
    Ok(())
}
```

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

```rust
#[derive(Model)]
struct User {
    #[validate(required, min_length = 3, max_length = 30)]
    username: Option<String>,

    #[validate(email)]
    email: String,

    #[validate(non_negative, max = 100)]
    score: i64,

    #[validate(custom(validate_name))]
    name: String,
}
```

#### 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.

```rust
#[default(...)]
```

When a field is not provided during construction, OxiMod automatically applies the specified default before validation and persistence.

##### Examples

```rust
#[default("Guest")]
name: String,

#[default(42)]
score: i32,

#[default(false)]
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 accept `Into<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

```rust
#[derive(Model)]
struct User {
    #[default("Guest")]
    name: String,

    #[default(false)]
    active: bool,
}
```

```rust
let user = 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

```rust
#[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 to `Ok(())`; implement only what you need.

- **Error handling**  
  - Pre-hook errors abort operations  
  - Post-hook errors indicate post-processing failure


##### Example

```rust
use oximod::{Hooks, Model};

#[derive(Model)]
#[db("app")]
#[collection("logs")]
#[hooks]
struct Log {
    message: String,
}

#[async_trait::async_trait]
impl Hooks for Log {
    async fn pre_save(&self) -> Result<(), oximod::OxiModError> {
        println!("Saving log");
        Ok(())
    }

    async fn pre_save_mut(&mut self) -> Result<(), oximod::OxiModError> {
        self.message = self.message.trim().to_string();
        Ok(())
    }
}
```

---

## License

MIT