entity_derive_impl/
lib.rs

1// SPDX-FileCopyrightText: 2025-2026 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//!     #[belongs_to(Organization)]     // Foreign key relation
62//!     pub org_id: Uuid,
63//!
64//!     #[filter]                        // Exact match filter in Query struct
65//!     pub status: String,
66//!
67//!     #[filter(like)]                  // ILIKE pattern filter
68//!     pub name: String,
69//!
70//!     #[filter(range)]                 // Range filter (generates from/to fields)
71//!     pub created_at: DateTime<Utc>,
72//! }
73//!
74//! // Projections - partial views of the entity
75//! #[projection(Public: id, name)]           // UserPublic struct
76//! #[projection(Admin: id, name, email)]     // UserAdmin struct
77//! ```
78//!
79//! # Generated Code Overview
80//!
81//! For a `User` entity, the macro generates:
82//!
83//! | Generated Type | Description |
84//! |----------------|-------------|
85//! | `CreateUserRequest` | DTO for `POST` requests |
86//! | `UpdateUserRequest` | DTO for `PATCH` requests (all fields `Option<T>`) |
87//! | `UserResponse` | DTO for API responses |
88//! | `UserRow` | Database row mapping (for `sqlx::FromRow`) |
89//! | `InsertableUser` | Struct for `INSERT` statements |
90//! | `UserQuery` | Query struct for type-safe filtering (if `#[filter]` used) |
91//! | `UserRepository` | Async trait with CRUD methods |
92//! | `impl UserRepository for PgPool` | PostgreSQL implementation |
93//! | `User{Projection}` | Projection structs (e.g., `UserPublic`, `UserAdmin`) |
94//! | `From<...>` impls | Type conversions between all structs |
95//!
96//! # SQL Generation Modes
97//!
98//! | Mode | Generates Trait | Generates Impl | Use Case |
99//! |------|-----------------|----------------|----------|
100//! | `sql = "full"` | ✅ | ✅ | Standard CRUD, simple queries |
101//! | `sql = "trait"` | ✅ | ❌ | Custom SQL (joins, CTEs, search) |
102//! | `sql = "none"` | ❌ | ❌ | DTOs only, no database layer |
103//!
104//! # Repository Methods
105//!
106//! The generated `{Name}Repository` trait includes:
107//!
108//! ```rust,ignore
109//! #[async_trait]
110//! pub trait UserRepository: Send + Sync {
111//!     type Error: std::error::Error + Send + Sync;
112//!
113//!     /// Create a new entity
114//!     async fn create(&self, dto: CreateUserRequest) -> Result<User, Self::Error>;
115//!
116//!     /// Find entity by primary key
117//!     async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, Self::Error>;
118//!
119//!     /// Update entity with partial data
120//!     async fn update(&self, id: Uuid, dto: UpdateUserRequest) -> Result<User, Self::Error>;
121//!
122//!     /// Delete entity by primary key
123//!     async fn delete(&self, id: Uuid) -> Result<bool, Self::Error>;
124//!
125//!     /// List entities with pagination
126//!     async fn list(&self, limit: i64, offset: i64) -> Result<Vec<User>, Self::Error>;
127//!
128//!     /// Query entities with type-safe filters (if #[filter] used)
129//!     async fn query(&self, query: UserQuery) -> Result<Vec<User>, Self::Error>;
130//!
131//!     // For each projection, generates optimized SELECT method
132//!     async fn find_by_id_public(&self, id: Uuid) -> Result<Option<UserPublic>, Self::Error>;
133//!     async fn find_by_id_admin(&self, id: Uuid) -> Result<Option<UserAdmin>, Self::Error>;
134//! }
135//! ```
136//!
137//! # Projections
138//!
139//! Define partial views of entities for optimized SELECT queries:
140//!
141//! ```rust,ignore
142//! #[derive(Entity)]
143//! #[entity(table = "users")]
144//! #[projection(Public: id, name, avatar)]    // Public profile
145//! #[projection(Admin: id, name, email, role)] // Admin view
146//! pub struct User {
147//!     #[id]
148//!     pub id: Uuid,
149//!     #[field(create, update, response)]
150//!     pub name: String,
151//!     #[field(create, response)]
152//!     pub email: String,
153//!     #[field(update, response)]
154//!     pub avatar: Option<String>,
155//!     #[field(response)]
156//!     pub role: String,
157//! }
158//!
159//! // Generated: UserPublic, UserAdmin structs
160//! // Generated: find_by_id_public, find_by_id_admin methods
161//!
162//! // SQL: SELECT id, name, avatar FROM public.users WHERE id = $1
163//! let public = repo.find_by_id_public(user_id).await?;
164//! ```
165//!
166//! # Error Handling
167//!
168//! The generated implementation uses `sqlx::Error` as the error type.
169//! You can wrap it in your application's error type:
170//!
171//! ```rust,ignore
172//! use entity_derive::Entity;
173//!
174//! #[derive(Entity)]
175//! #[entity(table = "users", sql = "trait")]  // Generate trait only
176//! pub struct User { /* ... */ }
177//!
178//! // Implement with your own error type
179//! #[async_trait]
180//! impl UserRepository for MyDatabase {
181//!     type Error = MyAppError;  // Your custom error
182//!
183//!     async fn create(&self, dto: CreateUserRequest) -> Result<User, Self::Error> {
184//!         // Your implementation
185//!     }
186//! }
187//! ```
188//!
189//! # Compile-Time Guarantees
190//!
191//! This crate provides several compile-time guarantees:
192//!
193//! - **No sensitive data leaks**: Fields marked `#[field(skip)]` are excluded
194//!   from all DTOs
195//! - **Type-safe updates**: `UpdateRequest` fields are properly wrapped in
196//!   `Option`
197//! - **Consistent mapping**: `From` impls are always in sync with field
198//!   definitions
199//! - **SQL injection prevention**: All queries use parameterized bindings
200//!
201//! # Performance
202//!
203//! - **Zero runtime overhead**: All code generation happens at compile time
204//! - **No reflection**: Generated code is plain Rust structs and impls
205//! - **Minimal dependencies**: Only proc-macro essentials (syn, quote, darling)
206//!
207//! # Comparison with Alternatives
208//!
209//! | Feature | entity-derive | Diesel | SeaORM |
210//! |---------|---------------|--------|--------|
211//! | DTO generation | ✅ | ❌ | ❌ |
212//! | Repository pattern | ✅ | ❌ | Partial |
213//! | Type-safe SQL | ✅ | ✅ | ✅ |
214//! | Async support | ✅ | Partial | ✅ |
215//! | Boilerplate reduction | ~90% | ~50% | ~60% |
216
217mod entity;
218mod error;
219mod utils;
220
221use proc_macro::TokenStream;
222
223/// Derive macro for generating complete domain boilerplate from a single entity
224/// definition.
225///
226/// # Overview
227///
228/// The `Entity` derive macro generates all the boilerplate code needed for a
229/// typical CRUD application: DTOs, repository traits, SQL implementations, and
230/// type mappers.
231///
232/// # Generated Types
233///
234/// For an entity named `User`, the macro generates:
235///
236/// - **`CreateUserRequest`** — DTO for creation (fields marked with
237///   `#[field(create)]`)
238/// - **`UpdateUserRequest`** — DTO for updates (fields marked with
239///   `#[field(update)]`, wrapped in `Option`)
240/// - **`UserResponse`** — DTO for responses (fields marked with
241///   `#[field(response)]`)
242/// - **`UserRow`** — Database row struct (implements `sqlx::FromRow`)
243/// - **`InsertableUser`** — Struct for INSERT operations
244/// - **`UserRepository`** — Async trait with CRUD methods
245/// - **`impl UserRepository for PgPool`** — PostgreSQL implementation (when
246///   `sql = "full"`)
247///
248/// # Entity Attributes
249///
250/// Configure the entity using `#[entity(...)]`:
251///
252/// | Attribute | Required | Default | Description |
253/// |-----------|----------|---------|-------------|
254/// | `table` | **Yes** | — | Database table name |
255/// | `schema` | No | `"public"` | Database schema name |
256/// | `sql` | No | `"full"` | SQL generation: `"full"`, `"trait"`, or `"none"` |
257/// | `dialect` | No | `"postgres"` | Database dialect: `"postgres"`, `"clickhouse"`, `"mongodb"` |
258/// | `uuid` | No | `"v7"` | UUID version for ID: `"v7"` (time-ordered) or `"v4"` (random) |
259/// | `migrations` | No | `false` | Generate `MIGRATION_UP` and `MIGRATION_DOWN` constants |
260///
261/// # Field Attributes
262///
263/// | Attribute | Description |
264/// |-----------|-------------|
265/// | `#[id]` | Primary key. Auto-generates UUID (v7 by default, configurable with `uuid` attribute). Always included in `Response`. |
266/// | `#[auto]` | Auto-generated field (e.g., `created_at`). Excluded from `Create`/`Update`. |
267/// | `#[field(create)]` | Include in `CreateRequest`. |
268/// | `#[field(update)]` | Include in `UpdateRequest`. Wrapped in `Option<T>` if not already. |
269/// | `#[field(response)]` | Include in `Response`. |
270/// | `#[field(skip)]` | Exclude from ALL DTOs. Use for sensitive data. |
271/// | `#[belongs_to(Entity)]` | Foreign key relation. Generates `find_{entity}` method in repository. |
272/// | `#[belongs_to(Entity, on_delete = "...")]` | Foreign key with ON DELETE action (`cascade`, `set null`, `restrict`). |
273/// | `#[has_many(Entity)]` | One-to-many relation (entity-level). Generates `find_{entities}` method. |
274/// | `#[projection(Name: f1, f2)]` | Entity-level. Defines a projection struct with specified fields. |
275/// | `#[filter]` | Exact match filter. Generates field in Query struct with `=` comparison. |
276/// | `#[filter(like)]` | ILIKE pattern filter. Generates field for text pattern matching. |
277/// | `#[filter(range)]` | Range filter. Generates `field_from` and `field_to` fields. |
278/// | `#[column(unique)]` | Add UNIQUE constraint in migrations. |
279/// | `#[column(index)]` | Add btree index in migrations. |
280/// | `#[column(index = "gin")]` | Add index with specific type (btree, hash, gin, gist, brin). |
281/// | `#[column(default = "...")]` | Set DEFAULT value in migrations. |
282/// | `#[column(check = "...")]` | Add CHECK constraint in migrations. |
283/// | `#[column(varchar = N)]` | Use VARCHAR(N) instead of TEXT in migrations. |
284///
285/// Multiple attributes can be combined: `#[field(create, update, response)]`
286///
287/// # Examples
288///
289/// ## Basic Usage
290///
291/// ```rust,ignore
292/// use entity_derive::Entity;
293/// use uuid::Uuid;
294/// use chrono::{DateTime, Utc};
295///
296/// #[derive(Entity)]
297/// #[entity(table = "users", schema = "core")]
298/// pub struct User {
299///     #[id]
300///     pub id: Uuid,
301///
302///     #[field(create, update, response)]
303///     pub name: String,
304///
305///     #[field(create, update, response)]
306///     pub email: String,
307///
308///     #[field(skip)]
309///     pub password_hash: String,
310///
311///     #[field(response)]
312///     #[auto]
313///     pub created_at: DateTime<Utc>,
314/// }
315/// ```
316///
317/// ## Custom SQL Implementation
318///
319/// For complex queries with joins, use `sql = "trait"`:
320///
321/// ```rust,ignore
322/// #[derive(Entity)]
323/// #[entity(table = "posts", sql = "trait")]
324/// pub struct Post {
325///     #[id]
326///     pub id: Uuid,
327///     #[field(create, update, response)]
328///     pub title: String,
329///     #[field(create, response)]
330///     pub author_id: Uuid,
331/// }
332///
333/// // Implement the repository yourself
334/// #[async_trait]
335/// impl PostRepository for PgPool {
336///     type Error = sqlx::Error;
337///
338///     async fn find_by_id(&self, id: Uuid) -> Result<Option<Post>, Self::Error> {
339///         sqlx::query_as!(Post,
340///             r#"SELECT p.*, u.name as author_name
341///                FROM posts p
342///                JOIN users u ON p.author_id = u.id
343///                WHERE p.id = $1"#,
344///             id
345///         )
346///         .fetch_optional(self)
347///         .await
348///     }
349///     // ... other methods
350/// }
351/// ```
352///
353/// ## DTOs Only (No Database Layer)
354///
355/// ```rust,ignore
356/// #[derive(Entity)]
357/// #[entity(table = "events", sql = "none")]
358/// pub struct Event {
359///     #[id]
360///     pub id: Uuid,
361///     #[field(create, response)]
362///     pub name: String,
363/// }
364/// // Only generates CreateEventRequest, EventResponse, etc.
365/// // No repository trait or SQL implementation
366/// ```
367///
368/// ## Migration Generation
369///
370/// Generate compile-time SQL migrations with `migrations`:
371///
372/// ```rust,ignore
373/// #[derive(Entity)]
374/// #[entity(table = "products", migrations)]
375/// pub struct Product {
376///     #[id]
377///     pub id: Uuid,
378///
379///     #[field(create, update, response)]
380///     #[column(unique, index)]
381///     pub sku: String,
382///
383///     #[field(create, update, response)]
384///     #[column(varchar = 200)]
385///     pub name: String,
386///
387///     #[field(create, update, response)]
388///     #[column(check = "price >= 0")]
389///     pub price: f64,
390///
391///     #[belongs_to(Category, on_delete = "cascade")]
392///     pub category_id: Uuid,
393/// }
394///
395/// // Generated constants:
396/// // Product::MIGRATION_UP - CREATE TABLE, indexes, constraints
397/// // Product::MIGRATION_DOWN - DROP TABLE CASCADE
398///
399/// // Apply migration:
400/// sqlx::query(Product::MIGRATION_UP).execute(&pool).await?;
401/// ```
402///
403/// # Security
404///
405/// Use `#[field(skip)]` to prevent sensitive data from leaking:
406///
407/// ```rust,ignore
408/// pub struct User {
409///     #[field(skip)]
410///     pub password_hash: String,  // Never in any DTO
411///
412///     #[field(skip)]
413///     pub api_secret: String,     // Never in any DTO
414///
415///     #[field(skip)]
416///     pub internal_notes: String, // Admin-only, not in public API
417/// }
418/// ```
419///
420/// # Generated SQL
421///
422/// The macro generates parameterized SQL queries that are safe from injection:
423///
424/// ```sql
425/// -- CREATE
426/// INSERT INTO schema.table (id, field1, field2, ...)
427/// VALUES ($1, $2, $3, ...)
428///
429/// -- READ
430/// SELECT * FROM schema.table WHERE id = $1
431///
432/// -- UPDATE (dynamic based on provided fields)
433/// UPDATE schema.table SET field1 = $1, field2 = $2 WHERE id = $3
434///
435/// -- DELETE
436/// DELETE FROM schema.table WHERE id = $1 RETURNING id
437///
438/// -- LIST
439/// SELECT * FROM schema.table ORDER BY created_at DESC LIMIT $1 OFFSET $2
440/// ```
441#[proc_macro_derive(
442    Entity,
443    attributes(
444        entity, field, id, auto, validate, belongs_to, has_many, projection, filter, command,
445        example, column
446    )
447)]
448pub fn derive_entity(input: TokenStream) -> TokenStream {
449    entity::derive(input)
450}
451
452/// Derive macro for generating OpenAPI error response documentation.
453///
454/// # Overview
455///
456/// The `EntityError` derive macro generates OpenAPI response documentation
457/// from error enum variants, using `#[status(code)]` attributes and doc
458/// comments.
459///
460/// # Example
461///
462/// ```rust,ignore
463/// use entity_derive::EntityError;
464/// use thiserror::Error;
465/// use utoipa::ToSchema;
466///
467/// #[derive(Debug, Error, ToSchema, EntityError)]
468/// pub enum UserError {
469///     /// User with this email already exists
470///     #[error("Email already exists")]
471///     #[status(409)]
472///     EmailExists,
473///
474///     /// User not found by ID
475///     #[error("User not found")]
476///     #[status(404)]
477///     NotFound,
478///
479///     /// Invalid credentials provided
480///     #[error("Invalid credentials")]
481///     #[status(401)]
482///     InvalidCredentials,
483/// }
484/// ```
485///
486/// # Generated Code
487///
488/// For `UserError`, generates:
489/// - `UserErrorResponses` struct with helper methods
490/// - `status_codes()` - returns all error status codes
491/// - `descriptions()` - returns all error descriptions
492/// - `utoipa_responses()` - returns tuples for OpenAPI responses
493///
494/// # Attributes
495///
496/// | Attribute | Required | Description |
497/// |-----------|----------|-------------|
498/// | `#[status(code)]` | **Yes** | HTTP status code (e.g., 404, 409, 500) |
499/// | `/// Doc comment` | No | Used as response description |
500#[proc_macro_derive(EntityError, attributes(status))]
501pub fn derive_entity_error(input: TokenStream) -> TokenStream {
502    error::derive(input)
503}