entity_derive/lib.rs
1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4#![doc = include_str!("../README.md")]
5#![doc(
6 html_logo_url = "https://raw.githubusercontent.com/RAprogramm/entity-derive/main/assets/logo.svg",
7 html_favicon_url = "https://raw.githubusercontent.com/RAprogramm/entity-derive/main/assets/favicon.ico"
8)]
9#![cfg_attr(docsrs, feature(doc_cfg))]
10#![warn(
11 missing_docs,
12 rustdoc::missing_crate_level_docs,
13 rustdoc::broken_intra_doc_links,
14 rust_2018_idioms
15)]
16#![deny(unsafe_code)]
17
18//! # Quick Navigation
19//!
20//! - **Getting Started**: See the [crate documentation](crate) above
21//! - **Derive Macro**: [`Entity`](macro@Entity) — the main derive macro
22//! - **Examples**: Check the [examples directory](https://github.com/RAprogramm/entity-derive/tree/main/examples)
23//! - **Wiki**: [Comprehensive guides](https://github.com/RAprogramm/entity-derive/wiki)
24//!
25//! # Attribute Quick Reference
26//!
27//! ## Entity-Level `#[entity(...)]`
28//!
29//! ```rust,ignore
30//! #[derive(Entity)]
31//! #[entity(
32//! table = "users", // Required: database table name
33//! schema = "public", // Optional: database schema (default: "public")
34//! sql = "full", // Optional: "full" | "trait" | "none" (default: "full")
35//! dialect = "postgres", // Optional: "postgres" | "clickhouse" | "mongodb" (default: "postgres")
36//! uuid = "v7" // Optional: "v7" | "v4" (default: "v7")
37//! )]
38//! pub struct User { /* ... */ }
39//! ```
40//!
41//! ## Field-Level Attributes
42//!
43//! ```rust,ignore
44//! pub struct User {
45//! #[id] // Primary key, UUID v7, always in response
46//! pub id: Uuid,
47//!
48//! #[field(create, update, response)] // In all DTOs
49//! pub name: String,
50//!
51//! #[field(create, response)] // Create + Response only
52//! pub email: String,
53//!
54//! #[field(skip)] // Excluded from all DTOs
55//! pub password_hash: String,
56//!
57//! #[field(response)]
58//! #[auto] // Auto-generated (excluded from create/update)
59//! pub created_at: DateTime<Utc>,
60//! }
61//! ```
62//!
63//! # Generated Code Overview
64//!
65//! For a `User` entity, the macro generates:
66//!
67//! | Generated Type | Description |
68//! |----------------|-------------|
69//! | `CreateUserRequest` | DTO for `POST` requests |
70//! | `UpdateUserRequest` | DTO for `PATCH` requests (all fields `Option<T>`) |
71//! | `UserResponse` | DTO for API responses |
72//! | `UserRow` | Database row mapping (for `sqlx::FromRow`) |
73//! | `InsertableUser` | Struct for `INSERT` statements |
74//! | `UserRepository` | Async trait with CRUD methods |
75//! | `impl UserRepository for PgPool` | PostgreSQL implementation |
76//! | `From<...>` impls | Type conversions between all structs |
77//!
78//! # SQL Generation Modes
79//!
80//! | Mode | Generates Trait | Generates Impl | Use Case |
81//! |------|-----------------|----------------|----------|
82//! | `sql = "full"` | ✅ | ✅ | Standard CRUD, simple queries |
83//! | `sql = "trait"` | ✅ | ❌ | Custom SQL (joins, CTEs, search) |
84//! | `sql = "none"` | ❌ | ❌ | DTOs only, no database layer |
85//!
86//! # Repository Methods
87//!
88//! The generated `{Name}Repository` trait includes:
89//!
90//! ```rust,ignore
91//! #[async_trait]
92//! pub trait UserRepository: Send + Sync {
93//! type Error: std::error::Error + Send + Sync;
94//!
95//! /// Create a new entity
96//! async fn create(&self, dto: CreateUserRequest) -> Result<User, Self::Error>;
97//!
98//! /// Find entity by primary key
99//! async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, Self::Error>;
100//!
101//! /// Update entity with partial data
102//! async fn update(&self, id: Uuid, dto: UpdateUserRequest) -> Result<User, Self::Error>;
103//!
104//! /// Delete entity by primary key
105//! async fn delete(&self, id: Uuid) -> Result<bool, Self::Error>;
106//!
107//! /// List entities with pagination
108//! async fn list(&self, limit: i64, offset: i64) -> Result<Vec<User>, Self::Error>;
109//! }
110//! ```
111//!
112//! # Error Handling
113//!
114//! The generated implementation uses `sqlx::Error` as the error type.
115//! You can wrap it in your application's error type:
116//!
117//! ```rust,ignore
118//! use entity_derive::Entity;
119//!
120//! #[derive(Entity)]
121//! #[entity(table = "users", sql = "trait")] // Generate trait only
122//! pub struct User { /* ... */ }
123//!
124//! // Implement with your own error type
125//! #[async_trait]
126//! impl UserRepository for MyDatabase {
127//! type Error = MyAppError; // Your custom error
128//!
129//! async fn create(&self, dto: CreateUserRequest) -> Result<User, Self::Error> {
130//! // Your implementation
131//! }
132//! }
133//! ```
134//!
135//! # Compile-Time Guarantees
136//!
137//! This crate provides several compile-time guarantees:
138//!
139//! - **No sensitive data leaks**: Fields marked `#[field(skip)]` are excluded
140//! from all DTOs
141//! - **Type-safe updates**: `UpdateRequest` fields are properly wrapped in
142//! `Option`
143//! - **Consistent mapping**: `From` impls are always in sync with field
144//! definitions
145//! - **SQL injection prevention**: All queries use parameterized bindings
146//!
147//! # Performance
148//!
149//! - **Zero runtime overhead**: All code generation happens at compile time
150//! - **No reflection**: Generated code is plain Rust structs and impls
151//! - **Minimal dependencies**: Only proc-macro essentials (syn, quote, darling)
152//!
153//! # Comparison with Alternatives
154//!
155//! | Feature | entity-derive | Diesel | SeaORM |
156//! |---------|---------------|--------|--------|
157//! | DTO generation | ✅ | ❌ | ❌ |
158//! | Repository pattern | ✅ | ❌ | Partial |
159//! | Type-safe SQL | ✅ | ✅ | ✅ |
160//! | Async support | ✅ | Partial | ✅ |
161//! | Boilerplate reduction | ~90% | ~50% | ~60% |
162
163mod entity;
164mod utils;
165
166use proc_macro::TokenStream;
167
168/// Derive macro for generating complete domain boilerplate from a single entity
169/// definition.
170///
171/// # Overview
172///
173/// The `Entity` derive macro generates all the boilerplate code needed for a
174/// typical CRUD application: DTOs, repository traits, SQL implementations, and
175/// type mappers.
176///
177/// # Generated Types
178///
179/// For an entity named `User`, the macro generates:
180///
181/// - **`CreateUserRequest`** — DTO for creation (fields marked with
182/// `#[field(create)]`)
183/// - **`UpdateUserRequest`** — DTO for updates (fields marked with
184/// `#[field(update)]`, wrapped in `Option`)
185/// - **`UserResponse`** — DTO for responses (fields marked with
186/// `#[field(response)]`)
187/// - **`UserRow`** — Database row struct (implements `sqlx::FromRow`)
188/// - **`InsertableUser`** — Struct for INSERT operations
189/// - **`UserRepository`** — Async trait with CRUD methods
190/// - **`impl UserRepository for PgPool`** — PostgreSQL implementation (when
191/// `sql = "full"`)
192///
193/// # Entity Attributes
194///
195/// Configure the entity using `#[entity(...)]`:
196///
197/// | Attribute | Required | Default | Description |
198/// |-----------|----------|---------|-------------|
199/// | `table` | **Yes** | — | Database table name |
200/// | `schema` | No | `"public"` | Database schema name |
201/// | `sql` | No | `"full"` | SQL generation: `"full"`, `"trait"`, or `"none"` |
202/// | `dialect` | No | `"postgres"` | Database dialect: `"postgres"`, `"clickhouse"`, `"mongodb"` |
203/// | `uuid` | No | `"v7"` | UUID version for ID: `"v7"` (time-ordered) or `"v4"` (random) |
204///
205/// # Field Attributes
206///
207/// | Attribute | Description |
208/// |-----------|-------------|
209/// | `#[id]` | Primary key. Auto-generates UUID (v7 by default, configurable with `uuid` attribute). Always included in `Response`. |
210/// | `#[auto]` | Auto-generated field (e.g., `created_at`). Excluded from `Create`/`Update`. |
211/// | `#[field(create)]` | Include in `CreateRequest`. |
212/// | `#[field(update)]` | Include in `UpdateRequest`. Wrapped in `Option<T>` if not already. |
213/// | `#[field(response)]` | Include in `Response`. |
214/// | `#[field(skip)]` | Exclude from ALL DTOs. Use for sensitive data. |
215///
216/// Multiple attributes can be combined: `#[field(create, update, response)]`
217///
218/// # Examples
219///
220/// ## Basic Usage
221///
222/// ```rust,ignore
223/// use entity_derive::Entity;
224/// use uuid::Uuid;
225/// use chrono::{DateTime, Utc};
226///
227/// #[derive(Entity)]
228/// #[entity(table = "users", schema = "core")]
229/// pub struct User {
230/// #[id]
231/// pub id: Uuid,
232///
233/// #[field(create, update, response)]
234/// pub name: String,
235///
236/// #[field(create, update, response)]
237/// pub email: String,
238///
239/// #[field(skip)]
240/// pub password_hash: String,
241///
242/// #[field(response)]
243/// #[auto]
244/// pub created_at: DateTime<Utc>,
245/// }
246/// ```
247///
248/// ## Custom SQL Implementation
249///
250/// For complex queries with joins, use `sql = "trait"`:
251///
252/// ```rust,ignore
253/// #[derive(Entity)]
254/// #[entity(table = "posts", sql = "trait")]
255/// pub struct Post {
256/// #[id]
257/// pub id: Uuid,
258/// #[field(create, update, response)]
259/// pub title: String,
260/// #[field(create, response)]
261/// pub author_id: Uuid,
262/// }
263///
264/// // Implement the repository yourself
265/// #[async_trait]
266/// impl PostRepository for PgPool {
267/// type Error = sqlx::Error;
268///
269/// async fn find_by_id(&self, id: Uuid) -> Result<Option<Post>, Self::Error> {
270/// sqlx::query_as!(Post,
271/// r#"SELECT p.*, u.name as author_name
272/// FROM posts p
273/// JOIN users u ON p.author_id = u.id
274/// WHERE p.id = $1"#,
275/// id
276/// )
277/// .fetch_optional(self)
278/// .await
279/// }
280/// // ... other methods
281/// }
282/// ```
283///
284/// ## DTOs Only (No Database Layer)
285///
286/// ```rust,ignore
287/// #[derive(Entity)]
288/// #[entity(table = "events", sql = "none")]
289/// pub struct Event {
290/// #[id]
291/// pub id: Uuid,
292/// #[field(create, response)]
293/// pub name: String,
294/// }
295/// // Only generates CreateEventRequest, EventResponse, etc.
296/// // No repository trait or SQL implementation
297/// ```
298///
299/// # Security
300///
301/// Use `#[field(skip)]` to prevent sensitive data from leaking:
302///
303/// ```rust,ignore
304/// pub struct User {
305/// #[field(skip)]
306/// pub password_hash: String, // Never in any DTO
307///
308/// #[field(skip)]
309/// pub api_secret: String, // Never in any DTO
310///
311/// #[field(skip)]
312/// pub internal_notes: String, // Admin-only, not in public API
313/// }
314/// ```
315///
316/// # Generated SQL
317///
318/// The macro generates parameterized SQL queries that are safe from injection:
319///
320/// ```sql
321/// -- CREATE
322/// INSERT INTO schema.table (id, field1, field2, ...)
323/// VALUES ($1, $2, $3, ...)
324///
325/// -- READ
326/// SELECT * FROM schema.table WHERE id = $1
327///
328/// -- UPDATE (dynamic based on provided fields)
329/// UPDATE schema.table SET field1 = $1, field2 = $2 WHERE id = $3
330///
331/// -- DELETE
332/// DELETE FROM schema.table WHERE id = $1 RETURNING id
333///
334/// -- LIST
335/// SELECT * FROM schema.table ORDER BY created_at DESC LIMIT $1 OFFSET $2
336/// ```
337#[proc_macro_derive(Entity, attributes(entity, field, id, auto, validate))]
338pub fn derive_entity(input: TokenStream) -> TokenStream {
339 entity::derive(input)
340}