Expand description
entity-derive
One macro to rule them all
Generate DTOs, repositories, mappers, and SQL from a single entity definition
§Table of Contents
- The Problem
- The Solution
- Features
- Installation
- Quick Start
- Attribute Reference
- Generated Code
- Architecture
- Comparison
- Code Coverage
§The Problem
Building a typical CRUD application requires writing the same boilerplate over and over:
// 1. Your domain entity
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub password_hash: String,
pub created_at: DateTime<Utc>,
}
// 2. DTO for creating (without id, without auto-generated fields)
pub struct CreateUserRequest {
pub name: String,
pub email: String,
}
// 3. DTO for updating (all fields optional for partial updates)
pub struct UpdateUserRequest {
pub name: Option<String>,
pub email: Option<String>,
}
// 4. DTO for API response (without sensitive fields)
pub struct UserResponse {
pub id: Uuid,
pub name: String,
pub email: String,
pub created_at: DateTime<Utc>,
}
// 5. Database row struct
pub struct UserRow { /* ... */ }
// 6. Insertable struct
pub struct InsertableUser { /* ... */ }
// 7. Repository trait
pub trait UserRepository { /* ... */ }
// 8. SQL implementation
impl UserRepository for PgPool { /* ... */ }
// 9. Six From implementations for mapping between types
impl From<UserRow> for User { /* ... */ }
impl From<User> for UserResponse { /* ... */ }
// ... and moreThat’s 200+ lines of boilerplate for a single entity.
§The Solution
use entity_derive::Entity;
#[derive(Entity)]
#[entity(table = "users", schema = "core")]
pub struct User {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub name: String,
#[field(create, update, response)]
pub email: String,
#[field(skip)]
pub password_hash: String,
#[field(response)]
#[auto]
pub created_at: DateTime<Utc>,
}Done. The macro generates everything else.
§Features
- Zero Runtime Cost — All code generation happens at compile time
- Type Safe — Change a field type once, everything updates automatically
- Flexible Attributes — Fine-grained control over what goes where
- SQL Generation — Complete CRUD operations for PostgreSQL (via sqlx)
- Partial Updates — Non-optional fields automatically wrapped in
Optionfor updates - Security by Default —
#[field(skip)]ensures sensitive data never leaks to responses
§Installation
Add to your Cargo.toml:
[dependencies]
entity-derive = { version = "0.2", features = ["postgres"] }
# Required peer dependencies
uuid = { version = "1", features = ["v4", "v7"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
async-trait = "0.1"
# For PostgreSQL support
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }§Available Features
| Feature | Description |
|---|---|
postgres | PostgreSQL support via sqlx (stable) |
clickhouse | ClickHouse support (planned) |
mongodb | MongoDB support (planned) |
api | OpenAPI schema generation via utoipa |
validate | Validation derives via validator |
§Quick Start
use entity_derive::Entity;
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Entity)]
#[entity(table = "posts", schema = "blog")]
pub struct Post {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub title: String,
#[field(create, update, response)]
pub content: String,
#[field(create, response)]
pub author_id: Uuid,
#[field(response)]
#[auto]
pub created_at: DateTime<Utc>,
#[field(response)]
#[auto]
pub updated_at: DateTime<Utc>,
}
// Now you have:
// - CreatePostRequest { title, content, author_id }
// - UpdatePostRequest { title?, content? }
// - PostResponse { id, title, content, author_id, created_at, updated_at }
// - PostRow, InsertablePost
// - PostRepository trait
// - impl PostRepository for sqlx::PgPool§Attribute Reference
§Entity-Level: #[entity(...)]
| Attribute | Required | Default | Description |
|---|---|---|---|
table | Yes | — | Database table name |
schema | No | "public" | Database schema |
sql | No | "full" | SQL generation level |
dialect | No | "postgres" | Database dialect |
uuid | No | "v7" | UUID version for ID generation |
§Database Dialects
| Dialect | Alias | Client | Status |
|---|---|---|---|
postgres | pg, postgresql | sqlx::PgPool | Stable |
clickhouse | ch | clickhouse::Client | Planned |
mongodb | mongo | mongodb::Client | Planned |
§UUID Versions
| Version | Method | Properties |
|---|---|---|
v7 | Uuid::now_v7() | Time-ordered, sortable (recommended for databases) |
v4 | Uuid::new_v4() | Random, widely compatible |
§SQL Levels
| Level | Repository Trait | PgPool Impl | Use Case |
|---|---|---|---|
full | Yes | Yes | Simple entities with standard CRUD |
trait | Yes | No | Custom queries (joins, CTEs, full-text search) |
none | No | No | DTOs only, no database layer |
§Field-Level Attributes
| Attribute | Effect |
|---|---|
#[id] | Primary key, auto-generated UUID (v7 by default, configurable with uuid attribute), always in response |
#[auto] | Auto-generated field (timestamps), excluded from create/update |
#[field(create)] | Include in CreateRequest |
#[field(update)] | Include in UpdateRequest (wrapped in Option if not already) |
#[field(response)] | Include in Response |
#[field(skip)] | Exclude from all DTOs (for sensitive data) |
Combine multiple: #[field(create, update, response)]
§Example with All Options
#[derive(Entity)]
#[entity(
table = "sessions",
schema = "auth",
sql = "full",
dialect = "postgres",
uuid = "v4" // Use random UUID instead of time-ordered
)]
pub struct Session {
#[id]
pub id: Uuid,
// ...
}§Generated Code
For a User entity, the macro generates:
§DTOs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateUserRequest {
pub name: String,
pub email: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UpdateUserRequest {
pub name: Option<String>,
pub email: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserResponse {
pub id: Uuid,
pub name: String,
pub email: String,
pub created_at: DateTime<Utc>,
}§Repository Trait
#[async_trait]
pub trait UserRepository: Send + Sync {
type Error: std::error::Error + Send + Sync;
async fn create(&self, dto: CreateUserRequest) -> Result<User, Self::Error>;
async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, Self::Error>;
async fn update(&self, id: Uuid, dto: UpdateUserRequest) -> Result<User, Self::Error>;
async fn delete(&self, id: Uuid) -> Result<bool, Self::Error>;
async fn list(&self, limit: i64, offset: i64) -> Result<Vec<User>, Self::Error>;
}§SQL Implementation
#[async_trait]
impl UserRepository for sqlx::PgPool {
type Error = sqlx::Error;
async fn create(&self, dto: CreateUserRequest) -> Result<User, Self::Error> {
let entity = User::from(dto);
let insertable = InsertableUser::from(&entity);
sqlx::query(
"INSERT INTO core.users (id, name, email, password_hash, created_at) \
VALUES ($1, $2, $3, $4, $5)"
)
.bind(insertable.id)
.bind(&insertable.name)
.bind(&insertable.email)
.bind(&insertable.password_hash)
.bind(insertable.created_at)
.execute(self)
.await?;
Ok(entity)
}
// ... find_by_id, update, delete, list
}§Mappers
impl From<UserRow> for User { /* ... */ }
impl From<CreateUserRequest> for User { /* ... */ }
impl From<User> for UserResponse { /* ... */ }
impl From<&User> for InsertableUser { /* ... */ }
// ... and more§Architecture
┌─────────────────────────────────────────────────────────────┐
│ Your Code │
│ #[derive(Entity)] │
│ pub struct User { ... } │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ entity-derive │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Parser │ │ Generators │ │ Output │ │
│ │ │ │ │ │ │ │
│ │ EntityDef │─>│ dto.rs │─>│ CreateRequest │ │
│ │ FieldDef │ │ row.rs │ │ UpdateRequest │ │
│ │ SqlLevel │ │ repository │ │ Response │ │
│ │ │ │ sql.rs │ │ Row, Insertable │ │
│ │ │ │ mappers.rs │ │ Repository trait │ │
│ │ │ │ │ │ PgPool impl │ │
│ │ │ │ │ │ From impls │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘§Comparison
| Aspect | Without entity-derive | With entity-derive |
|---|---|---|
| Lines of code | 200+ per entity | ~15 per entity |
| Type safety | Manual sync required | Automatic |
| Sensitive data leaks | Possible | Prevented by #[field(skip)] |
| Partial updates | Manual wrapping | Automatic |
| SQL bindings | Error-prone | Always in sync |
| Refactoring | Update 8+ places | Update 1 place |
§Code Coverage
We maintain high test coverage to ensure reliability. Below are visual representations of our codebase coverage:
§Sunburst
The inner circle represents the entire project. Moving outward: folders, then individual files. Size = number of statements, color = coverage percentage.
§Grid
Each block represents a file. Size = number of statements, color = coverage level (green = high, red = low).
§Icicle
Hierarchical view: top = entire project, descending through folders to individual files. Size and color represent statements and coverage.
# Quick Navigation- Getting Started: See the crate documentation above
- Derive Macro:
Entity— the main derive macro - Examples: Check the examples directory
- Wiki: Comprehensive guides
§Attribute Quick Reference
§Entity-Level #[entity(...)]
#[derive(Entity)]
#[entity(
table = "users", // Required: database table name
schema = "public", // Optional: database schema (default: "public")
sql = "full", // Optional: "full" | "trait" | "none" (default: "full")
dialect = "postgres", // Optional: "postgres" | "clickhouse" | "mongodb" (default: "postgres")
uuid = "v7" // Optional: "v7" | "v4" (default: "v7")
)]
pub struct User { /* ... */ }§Field-Level Attributes
pub struct User {
#[id] // Primary key, UUID v7, always in response
pub id: Uuid,
#[field(create, update, response)] // In all DTOs
pub name: String,
#[field(create, response)] // Create + Response only
pub email: String,
#[field(skip)] // Excluded from all DTOs
pub password_hash: String,
#[field(response)]
#[auto] // Auto-generated (excluded from create/update)
pub created_at: DateTime<Utc>,
}§Generated Code Overview
For a User entity, the macro generates:
| Generated Type | Description |
|---|---|
CreateUserRequest | DTO for POST requests |
UpdateUserRequest | DTO for PATCH requests (all fields Option<T>) |
UserResponse | DTO for API responses |
UserRow | Database row mapping (for sqlx::FromRow) |
InsertableUser | Struct for INSERT statements |
UserRepository | Async trait with CRUD methods |
impl UserRepository for PgPool | PostgreSQL implementation |
From<...> impls | Type conversions between all structs |
§SQL Generation Modes
| Mode | Generates Trait | Generates Impl | Use Case |
|---|---|---|---|
sql = "full" | ✅ | ✅ | Standard CRUD, simple queries |
sql = "trait" | ✅ | ❌ | Custom SQL (joins, CTEs, search) |
sql = "none" | ❌ | ❌ | DTOs only, no database layer |
§Repository Methods
The generated {Name}Repository trait includes:
#[async_trait]
pub trait UserRepository: Send + Sync {
type Error: std::error::Error + Send + Sync;
/// Create a new entity
async fn create(&self, dto: CreateUserRequest) -> Result<User, Self::Error>;
/// Find entity by primary key
async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, Self::Error>;
/// Update entity with partial data
async fn update(&self, id: Uuid, dto: UpdateUserRequest) -> Result<User, Self::Error>;
/// Delete entity by primary key
async fn delete(&self, id: Uuid) -> Result<bool, Self::Error>;
/// List entities with pagination
async fn list(&self, limit: i64, offset: i64) -> Result<Vec<User>, Self::Error>;
}§Error Handling
The generated implementation uses sqlx::Error as the error type.
You can wrap it in your application’s error type:
use entity_derive::Entity;
#[derive(Entity)]
#[entity(table = "users", sql = "trait")] // Generate trait only
pub struct User { /* ... */ }
// Implement with your own error type
#[async_trait]
impl UserRepository for MyDatabase {
type Error = MyAppError; // Your custom error
async fn create(&self, dto: CreateUserRequest) -> Result<User, Self::Error> {
// Your implementation
}
}§Compile-Time Guarantees
This crate provides several compile-time guarantees:
- No sensitive data leaks: Fields marked
#[field(skip)]are excluded from all DTOs - Type-safe updates:
UpdateRequestfields are properly wrapped inOption - Consistent mapping:
Fromimpls are always in sync with field definitions - SQL injection prevention: All queries use parameterized bindings
§Performance
- Zero runtime overhead: All code generation happens at compile time
- No reflection: Generated code is plain Rust structs and impls
- Minimal dependencies: Only proc-macro essentials (syn, quote, darling)
§Comparison with Alternatives
| Feature | entity-derive | Diesel | SeaORM |
|---|---|---|---|
| DTO generation | ✅ | ❌ | ❌ |
| Repository pattern | ✅ | ❌ | Partial |
| Type-safe SQL | ✅ | ✅ | ✅ |
| Async support | ✅ | Partial | ✅ |
| Boilerplate reduction | ~90% | ~50% | ~60% |
Derive Macros§
- Entity
- Derive macro for generating complete domain boilerplate from a single entity definition.