entity-derive 0.1.2

Derive macro for generating DTOs, repositories, and SQL from a single entity definition
docs.rs failed to build entity-derive-0.1.2
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: entity-derive-0.5.0


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 = "0.1"

# Required peer dependencies
uuid = { version = "1", features = ["v7"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
async-trait = "0.1"

# For database support
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }

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

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), 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)]

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.

Documentation

MSRV

Minimum Supported Rust Version: 1.92 (Edition 2024)

License

Licensed under the MIT License.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.