This guide will walk you through setting up Tnuctipun and creating your first type-safe MongoDB operations.

Installation

Add Tnuctipun to your Cargo.toml:

[dependencies]
tnuctipun = "0.1.1"

Tnuctipun is a MongoDB query builder library that generates BSON documents. It doesn’t include MongoDB connectivity - you’ll need to add the MongoDB driver separately if you want to connect to a database.

Required Dependencies

Tnuctipun works with the official MongoDB Rust driver. Here are the dependencies you’ll need:

[dependencies]
# Core dependencies
tnuctipun = "0.1.1"
serde = { version = "1.0", features = ["derive"] }
bson = "2.0"

# For MongoDB connectivity (you must add these separately)
mongodb = "2.0"  # For connecting to MongoDB
tokio = { version = "1.0", features = ["full"] }  # For async operations

Basic Setup

1. Define Your Data Structure

Start by defining a struct that represents your MongoDB document:

use serde::{Deserialize, Serialize};
use tnuctipun::{FieldWitnesses, MongoComparable};

#[derive(Debug, Serialize, Deserialize, FieldWitnesses, MongoComparable)]
struct User {
    pub name: String,
    pub age: i32,
    pub email: String,
    pub is_active: bool,
}

Important Notes:

  • FieldWitnesses generates compile-time field witnesses for type-safe access
  • MongoComparable enables comparison operations for filtering
  • Serialize/Deserialize are required for MongoDB document conversion
  • Field visibility (pub vs private) affects which fields are available (see Derive Macros for details)

2. Generated Field Witnesses

The FieldWitnesses derive macro automatically generates a module with field witnesses:

// Automatically generated by the derive macro
mod user_fields {
    pub struct Name;
    pub struct Age;
    pub struct Email;
    pub struct IsActive;
}

These witnesses provide compile-time field validation and are used in all query operations.

Your First Query

Let’s build a simple filter to find users:

use serde::{Deserialize, Serialize};
use tnuctipun::{FieldWitnesses, MongoComparable, filters::empty};

#[derive(Debug, Serialize, Deserialize, FieldWitnesses, MongoComparable)]
struct User {
    pub name: String,
    pub age: i32,
    pub email: String,
    pub is_active: bool,
}

fn main() {
    // Build filter with method chaining
    let filter_doc = empty::<User>()
        .eq::<user_fields::Name, _>("John".to_string())     // Add name condition
        .gt::<user_fields::Age, _>(18)                      // Add age condition
        .eq::<user_fields::IsActive, _>(true)               // Add active condition
        .and();                                             // Combine with AND
    
    println!("Generated filter: {}", filter_doc);
    // Output: { "$and": [{ "name": "John" }, { "age": { "$gt": 18 } }, { "is_active": true }] }
}

Your First Projection

Create a projection to select specific fields:

use serde::{Deserialize, Serialize};
use tnuctipun::{FieldWitnesses, MongoComparable, projection};

#[derive(Debug, Serialize, Deserialize, FieldWitnesses, MongoComparable)]
struct User {
    pub name: String,
    pub age: i32,
    pub email: String,
    pub is_active: bool,
}

fn main() {
    let projection_doc = projection::empty::<User>()
        .includes::<user_fields::Name>()
        .includes::<user_fields::Age>()
        .excludes::<user_fields::Email>()  // Hide sensitive data
        .build();
    
    println!("Generated projection: {}", projection_doc);
    // Output: { "name": 1, "age": 1, "email": 0 }
}

Your First Update

Build an update document to modify existing records:

use serde::{Deserialize, Serialize};
use tnuctipun::{FieldWitnesses, MongoComparable, updates};

#[derive(Debug, Serialize, Deserialize, FieldWitnesses, MongoComparable)]
struct User {
    pub name: String,
    pub age: i32,
    pub email: String,
    pub is_active: bool,
}

fn main() {
    let update_doc = updates::empty::<User>()
        .set::<user_fields::Name, _>("Jane".to_string())
        .inc::<user_fields::Age, _>(1)
        .set::<user_fields::IsActive, _>(true)
        .build();
    
    println!("Generated update: {}", update_doc);
    // Output: { "$set": { "name": "Jane", "is_active": true }, "$inc": { "age": 1 } }
}

Complete Example with MongoDB

Here’s a complete example showing how to use Tnuctipun with the MongoDB driver:

use mongodb::{Client, options::ClientOptions, Collection};
use tnuctipun::{FieldWitnesses, MongoComparable, filters::empty, projection, updates};
use serde::{Deserialize, Serialize};
use bson::doc;

#[derive(Debug, Serialize, Deserialize, FieldWitnesses, MongoComparable)]
struct User {
    #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
    pub id: Option<bson::oid::ObjectId>,
    pub name: String,
    pub age: i32,
    pub email: String,
    pub is_active: bool,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Connect to MongoDB
    let client_options = ClientOptions::parse("mongodb://localhost:27017").await?;
    let client = Client::with_options(client_options)?;
    let database = client.database("myapp");
    let collection: Collection<User> = database.collection("users");
    
    // 1. Build a type-safe filter
    let filter = empty::<User>()
        .gt::<user_fields::Age, _>(18)
        .eq::<user_fields::IsActive, _>(true)
        .and();
    
    // 2. Build a type-safe projection
    let projection_doc = projection::empty::<User>()
        .includes::<user_fields::Name>()
        .includes::<user_fields::Age>()
        .excludes::<user_fields::Email>()  // Hide sensitive data
        .build();
    
    // 3. Use with MongoDB find operation
    let find_options = mongodb::options::FindOptions::builder()
        .projection(projection_doc)
        .build();
    
    let _cursor = collection.find(filter, find_options).await?;
    
    // Process results (requires futures_util dependency)
    // while let Some(user) = cursor.try_next().await? {
    //     println!("Found user: {:?}", user);
    // }
    
    // 4. Build and execute a type-safe update
    let update_filter = doc! { "name": "John" };
    let update_doc = updates::empty::<User>()
        .inc::<user_fields::Age, _>(1)
        .set::<user_fields::IsActive, _>(true)
        .build();
    
    let update_result = collection.update_many(update_filter, update_doc, None).await?;
    println!("Updated {} documents", update_result.modified_count);
    
    Ok(())
}

Key Concepts Summary

Before moving to more advanced topics, make sure you understand these core concepts:

  1. Field Witnesses: Compile-time types that represent struct fields (user_fields::Name)
  2. Type Safety: Field names and types are validated at compile time
  3. Builder Pattern: Fluent API for constructing complex operations
  4. MongoDB Integration: Generated documents work directly with the MongoDB driver

Common Patterns

Conditional Query Building

use serde::{Deserialize, Serialize};
use tnuctipun::{FieldWitnesses, MongoComparable, filters::empty};

#[derive(Debug, Serialize, Deserialize, FieldWitnesses, MongoComparable)]
struct User {
    pub name: String,
    pub age: i32,
    pub email: String,
    pub is_active: bool,
}

fn main() {
    let mut filter_builder = empty::<User>();

    // Add conditions based on runtime parameters
    let user_name: Option<String> = Some("John".to_string());
    let minimum_age: Option<i32> = Some(18);

    if let Some(name) = user_name {
        filter_builder.eq::<user_fields::Name, _>(name);
    }

    if let Some(min_age) = minimum_age {
        filter_builder.gte::<user_fields::Age, _>(min_age);
    }

    let filter_doc = filter_builder.and();
}

Error Handling

use serde::{Deserialize, Serialize};
use tnuctipun::{FieldWitnesses, MongoComparable, filters::empty};

#[derive(Debug, Serialize, Deserialize, FieldWitnesses, MongoComparable)]
struct User {
    pub name: String,
    pub age: i32,
    pub email: String,
    pub is_active: bool,
}

fn main() {
    let mut filter_builder = empty::<User>();

    // Compile-time errors for invalid fields
    // filter_builder.eq::<user_fields::InvalidField, _>("value");  // ❌ Compile error

    // Compile-time errors for wrong types
    // filter_builder.eq::<user_fields::Age, _>("not a number");    // ❌ Compile error
}

Next Steps

Now that you have the basics working: