Getting Started
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 accessMongoComparable
enables comparison operations for filteringSerialize
/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:
- Field Witnesses: Compile-time types that represent struct fields (
user_fields::Name
) - Type Safety: Field names and types are validated at compile time
- Builder Pattern: Fluent API for constructing complex operations
- 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:
- Finding Documents - Learn advanced query building and projections
- Updating Documents - Master update operations
- Derive Macros - Understand the macro system in detail