Crate entity_derive

Crate entity_derive 

Source
Expand description

entity-derive

One macro to rule them all

Generate DTOs, repositories, mappers, and SQL from a single entity definition

Crates.io Documentation

CI Status Coverage

License: MIT REUSE Compliant Wiki


§Table of Contents


§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 more

That’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 Option for 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

FeatureDescription
postgresPostgreSQL support via sqlx (stable)
clickhouseClickHouse support (planned)
mongodbMongoDB support (planned)
apiOpenAPI schema generation via utoipa
validateValidation 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(...)]

AttributeRequiredDefaultDescription
tableYesDatabase table name
schemaNo"public"Database schema
sqlNo"full"SQL generation level
dialectNo"postgres"Database dialect
uuidNo"v7"UUID version for ID generation
§Database Dialects
DialectAliasClientStatus
postgrespg, postgresqlsqlx::PgPoolStable
clickhousechclickhouse::ClientPlanned
mongodbmongomongodb::ClientPlanned
§UUID Versions
VersionMethodProperties
v7Uuid::now_v7()Time-ordered, sortable (recommended for databases)
v4Uuid::new_v4()Random, widely compatible
§SQL Levels
LevelRepository TraitPgPool ImplUse Case
fullYesYesSimple entities with standard CRUD
traitYesNoCustom queries (joins, CTEs, full-text search)
noneNoNoDTOs only, no database layer

§Field-Level Attributes

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

AspectWithout entity-deriveWith entity-derive
Lines of code200+ per entity~15 per entity
Type safetyManual sync requiredAutomatic
Sensitive data leaksPossiblePrevented by #[field(skip)]
Partial updatesManual wrappingAutomatic
SQL bindingsError-proneAlways in sync
RefactoringUpdate 8+ placesUpdate 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.

Coverage Sunburst

§Grid

Each block represents a file. Size = number of statements, color = coverage level (green = high, red = low).

Coverage Grid

§Icicle

Hierarchical view: top = entire project, descending through folders to individual files. Size and color represent statements and coverage.

Coverage Icicle

# Quick Navigation

§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 TypeDescription
CreateUserRequestDTO for POST requests
UpdateUserRequestDTO for PATCH requests (all fields Option<T>)
UserResponseDTO for API responses
UserRowDatabase row mapping (for sqlx::FromRow)
InsertableUserStruct for INSERT statements
UserRepositoryAsync trait with CRUD methods
impl UserRepository for PgPoolPostgreSQL implementation
From<...> implsType conversions between all structs

§SQL Generation Modes

ModeGenerates TraitGenerates ImplUse 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: UpdateRequest fields are properly wrapped in Option
  • Consistent mapping: From impls 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

Featureentity-deriveDieselSeaORM
DTO generation
Repository patternPartial
Type-safe SQL
Async supportPartial
Boilerplate reduction~90%~50%~60%

Derive Macros§

Entity
Derive macro for generating complete domain boilerplate from a single entity definition.