Skip to main content

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// Several parsing helpers (column DDL, composite indexes, projection
11// metadata) are only consumed by the `migrations` and `projections`
12// generators. When users opt out of those features, the helpers become
13// unused — silence the dead-code lint in those configurations so minimal
14// builds stay warning-clean. Default builds (every feature on) keep the
15// warning active.
16#![cfg_attr(
17    any(not(feature = "migrations"), not(feature = "projections")),
18    allow(dead_code, unused_imports)
19)]
20#![warn(
21    missing_docs,
22    rustdoc::missing_crate_level_docs,
23    rustdoc::broken_intra_doc_links,
24    rust_2018_idioms
25)]
26#![deny(unsafe_code)]
27#![allow(clippy::option_if_let_else)]
28#![allow(clippy::match_same_arms)]
29#![allow(clippy::trivially_copy_pass_by_ref)]
30#![allow(clippy::struct_excessive_bools)]
31#![allow(clippy::too_many_lines)]
32#![allow(clippy::format_push_string)]
33#![allow(clippy::unused_self)]
34#![allow(clippy::needless_continue)]
35#![allow(clippy::needless_pass_by_value)]
36#![allow(clippy::uninlined_format_args)]
37#![allow(clippy::needless_raw_string_hashes)]
38#![allow(clippy::doc_markdown)]
39#![allow(clippy::missing_const_for_fn)]
40#![allow(clippy::used_underscore_binding)]
41#![allow(clippy::useless_format)]
42#![allow(clippy::approx_constant)]
43#![allow(clippy::needless_collect)]
44
45//! # Quick Navigation
46//!
47//! - **Getting Started**: See the [crate documentation](crate) above
48//! - **Derive Macro**: [`Entity`](macro@Entity) — the main derive macro
49//! - **Examples**: Check the [examples directory](https://github.com/RAprogramm/entity-derive/tree/main/examples)
50//! - **Wiki**: [Comprehensive guides](https://github.com/RAprogramm/entity-derive/wiki)
51//!
52//! # Attribute Quick Reference
53//!
54//! ## Entity-Level `#[entity(...)]`
55//!
56//! ```rust,ignore
57//! #[derive(Entity)]
58//! #[entity(
59//!     table = "users",      // Required: database table name
60//!     schema = "public",    // Optional: database schema (omit to exclude from SQL)
61//!     sql = "full",         // Optional: "full" | "trait" | "none" (default: "full")
62//!     dialect = "postgres", // Optional: "postgres" | "clickhouse" | "mongodb" (default: "postgres")
63//!     uuid = "v7"           // Optional: "v7" | "v4" (default: "v7")
64//! )]
65//! pub struct User { /* ... */ }
66//! ```
67//!
68//! ## Field-Level Attributes
69//!
70//! ```rust,ignore
71//! pub struct User {
72//!     #[id]                           // Primary key, UUID v7, always in response
73//!     pub id: Uuid,
74//!
75//!     #[field(create, update, response)]  // In all DTOs
76//!     pub name: String,
77//!
78//!     #[field(create, response)]      // Create + Response only
79//!     pub email: String,
80//!
81//!     #[field(skip)]                  // Excluded from all DTOs
82//!     pub password_hash: String,
83//!
84//!     #[field(response)]
85//!     #[auto]                         // Auto-generated (excluded from create/update)
86//!     pub created_at: DateTime<Utc>,
87//!
88//!     #[belongs_to(Organization)]     // Foreign key relation
89//!     pub org_id: Uuid,
90//!
91//!     #[filter]                        // Exact match filter in Query struct
92//!     pub status: String,
93//!
94//!     #[filter(like)]                  // ILIKE pattern filter
95//!     pub name: String,
96//!
97//!     #[filter(range)]                 // Range filter (generates from/to fields)
98//!     pub created_at: DateTime<Utc>,
99//! }
100//!
101//! // Projections - partial views of the entity
102//! #[projection(Public: id, name)]           // UserPublic struct
103//! #[projection(Admin: id, name, email)]     // UserAdmin struct
104//! ```
105//!
106//! # Generated Code Overview
107//!
108//! For a `User` entity, the macro generates:
109//!
110//! | Generated Type | Description |
111//! |----------------|-------------|
112//! | `CreateUserRequest` | DTO for `POST` requests |
113//! | `UpdateUserRequest` | DTO for `PATCH` requests (all fields `Option<T>`) |
114//! | `UserResponse` | DTO for API responses |
115//! | `UserRow` | Database row mapping (for `sqlx::FromRow`) |
116//! | `InsertableUser` | Struct for `INSERT` statements |
117//! | `UserQuery` | Query struct for type-safe filtering (if `#[filter]` used) |
118//! | `UserRepository` | Async trait with CRUD methods |
119//! | `impl UserRepository for PgPool` | `PostgreSQL` implementation |
120//! | `User{Projection}` | Projection structs (e.g., `UserPublic`, `UserAdmin`) |
121//! | `From<...>` impls | Type conversions between all structs |
122//!
123//! # SQL Generation Modes
124//!
125//! | Mode | Generates Trait | Generates Impl | Use Case |
126//! |------|-----------------|----------------|----------|
127//! | `sql = "full"` | ✅ | ✅ | Standard CRUD, simple queries |
128//! | `sql = "trait"` | ✅ | ❌ | Custom SQL (joins, CTEs, search) |
129//! | `sql = "none"` | ❌ | ❌ | DTOs only, no database layer |
130//!
131//! # Repository Methods
132//!
133//! The generated `{Name}Repository` trait includes:
134//!
135//! ```rust,ignore
136//! #[async_trait]
137//! pub trait UserRepository: Send + Sync {
138//!     type Error: std::error::Error + Send + Sync;
139//!
140//!     /// Create a new entity
141//!     async fn create(&self, dto: CreateUserRequest) -> Result<User, Self::Error>;
142//!
143//!     /// Find entity by primary key
144//!     async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, Self::Error>;
145//!
146//!     /// Update entity with partial data
147//!     async fn update(&self, id: Uuid, dto: UpdateUserRequest) -> Result<User, Self::Error>;
148//!
149//!     /// Delete entity by primary key
150//!     async fn delete(&self, id: Uuid) -> Result<bool, Self::Error>;
151//!
152//!     /// List entities with pagination
153//!     async fn list(&self, limit: i64, offset: i64) -> Result<Vec<User>, Self::Error>;
154//!
155//!     /// Query entities with type-safe filters (if #[filter] used)
156//!     async fn query(&self, query: UserQuery) -> Result<Vec<User>, Self::Error>;
157//!
158//!     // For each projection, generates optimized SELECT method
159//!     async fn find_by_id_public(&self, id: Uuid) -> Result<Option<UserPublic>, Self::Error>;
160//!     async fn find_by_id_admin(&self, id: Uuid) -> Result<Option<UserAdmin>, Self::Error>;
161//! }
162//! ```
163//!
164//! # Projections
165//!
166//! Define partial views of entities for optimized SELECT queries:
167//!
168//! ```rust,ignore
169//! #[derive(Entity)]
170//! #[entity(table = "users")]
171//! #[projection(Public: id, name, avatar)]    // Public profile
172//! #[projection(Admin: id, name, email, role)] // Admin view
173//! pub struct User {
174//!     #[id]
175//!     pub id: Uuid,
176//!     #[field(create, update, response)]
177//!     pub name: String,
178//!     #[field(create, response)]
179//!     pub email: String,
180//!     #[field(update, response)]
181//!     pub avatar: Option<String>,
182//!     #[field(response)]
183//!     pub role: String,
184//! }
185//!
186//! // Generated: UserPublic, UserAdmin structs
187//! // Generated: find_by_id_public, find_by_id_admin methods
188//!
189//! // SQL: SELECT id, name, avatar FROM users WHERE id = $1
190//! let public = repo.find_by_id_public(user_id).await?;
191//! ```
192//!
193//! # Error Handling
194//!
195//! The generated implementation uses `sqlx::Error` as the error type.
196//! You can wrap it in your application's error type:
197//!
198//! ```rust,ignore
199//! use entity_derive::Entity;
200//!
201//! #[derive(Entity)]
202//! #[entity(table = "users", sql = "trait")]  // Generate trait only
203//! pub struct User { /* ... */ }
204//!
205//! // Implement with your own error type
206//! #[async_trait]
207//! impl UserRepository for MyDatabase {
208//!     type Error = MyAppError;  // Your custom error
209//!
210//!     async fn create(&self, dto: CreateUserRequest) -> Result<User, Self::Error> {
211//!         // Your implementation
212//!     }
213//! }
214//! ```
215//!
216//! # Compile-Time Guarantees
217//!
218//! This crate provides several compile-time guarantees:
219//!
220//! - **No sensitive data leaks**: Fields marked `#[field(skip)]` are excluded
221//!   from all DTOs
222//! - **Type-safe updates**: `UpdateRequest` fields are properly wrapped in
223//!   `Option`
224//! - **Consistent mapping**: `From` impls are always in sync with field
225//!   definitions
226//! - **SQL injection prevention**: All queries use parameterized bindings
227//!
228//! # Performance
229//!
230//! - **Zero runtime overhead**: All code generation happens at compile time
231//! - **No reflection**: Generated code is plain Rust structs and impls
232//! - **Minimal dependencies**: Only proc-macro essentials (syn, quote, darling)
233//!
234//! # Comparison with Alternatives
235//!
236//! | Feature | entity-derive | Diesel | SeaORM |
237//! |---------|---------------|--------|--------|
238//! | DTO generation | ✅ | ❌ | ❌ |
239//! | Repository pattern | ✅ | ❌ | Partial |
240//! | Type-safe SQL | ✅ | ✅ | ✅ |
241//! | Async support | ✅ | Partial | ✅ |
242//! | Boilerplate reduction | ~90% | ~50% | ~60% |
243
244mod entity;
245mod error;
246mod utils;
247mod value_object;
248
249use proc_macro::TokenStream;
250
251/// Derive macro for generating complete domain boilerplate from a single entity
252/// definition.
253///
254/// # Overview
255///
256/// The `Entity` derive macro generates all the boilerplate code needed for a
257/// typical CRUD application: DTOs, repository traits, SQL implementations, and
258/// type mappers.
259///
260/// # Generated Types
261///
262/// For an entity named `User`, the macro generates:
263///
264/// - **`CreateUserRequest`** — DTO for creation (fields marked with
265///   `#[field(create)]`)
266/// - **`UpdateUserRequest`** — DTO for updates (fields marked with
267///   `#[field(update)]`, wrapped in `Option`)
268/// - **`UserResponse`** — DTO for responses (fields marked with
269///   `#[field(response)]`)
270/// - **`UserRow`** — Database row struct (implements `sqlx::FromRow`)
271/// - **`InsertableUser`** — Struct for INSERT operations
272/// - **`UserRepository`** — Async trait with CRUD methods
273/// - **`impl UserRepository for PgPool`** — `PostgreSQL` implementation (when
274///   `sql = "full"`)
275///
276/// # Entity Attributes
277///
278/// Configure the entity using `#[entity(...)]`:
279///
280/// | Attribute | Required | Default | Description |
281/// |-----------|----------|---------|-------------|
282/// | `table` | **Yes** | — | Database table name |
283/// | `schema` | No | — | Database schema name (omitted = no prefix in SQL) |
284/// | `sql` | No | `"full"` | SQL generation: `"full"`, `"trait"`, or `"none"` |
285/// | `dialect` | No | `"postgres"` | Database dialect: `"postgres"`, `"clickhouse"`, `"mongodb"` |
286/// | `uuid` | No | `"v7"` | UUID version for ID: `"v7"` (time-ordered) or `"v4"` (random) |
287/// | `migrations` | No | `false` | Generate `MIGRATION_UP` and `MIGRATION_DOWN` constants |
288///
289/// # Field Attributes
290///
291/// | Attribute | Description |
292/// |-----------|-------------|
293/// | `#[id]` | Primary key. Auto-generates UUID (v7 by default, configurable with `uuid` attribute). Always included in `Response`. |
294/// | `#[auto]` | Auto-generated field (e.g., `created_at`). Excluded from `Create`/`Update`. |
295/// | `#[field(create)]` | Include in `CreateRequest`. |
296/// | `#[field(update)]` | Include in `UpdateRequest`. Wrapped in `Option<T>` if not already. |
297/// | `#[field(response)]` | Include in `Response`. |
298/// | `#[field(skip)]` | Exclude from ALL DTOs. Use for sensitive data. |
299/// | `#[belongs_to(Entity)]` | Foreign key relation. Generates `find_{entity}` method in repository. |
300/// | `#[belongs_to(Entity, on_delete = "...")]` | Foreign key with ON DELETE action (`cascade`, `set null`, `restrict`). |
301/// | `#[has_many(Entity)]` | One-to-many relation (entity-level). Generates `find_{entities}` method. |
302/// | `#[projection(Name: f1, f2)]` | Entity-level. Defines a projection struct with specified fields. |
303/// | `#[filter]` | Exact match filter. Generates field in Query struct with `=` comparison. |
304/// | `#[filter(like)]` | ILIKE pattern filter. Generates field for text pattern matching. |
305/// | `#[filter(range)]` | Range filter. Generates `field_from` and `field_to` fields. |
306/// | `#[column(unique)]` | Add UNIQUE constraint in migrations. |
307/// | `#[column(index)]` | Add btree index in migrations. |
308/// | `#[column(index = "gin")]` | Add index with specific type (btree, hash, gin, gist, brin). |
309/// | `#[column(default = "...")]` | Set DEFAULT value in migrations. |
310/// | `#[column(check = "...")]` | Add CHECK constraint in migrations. |
311/// | `#[column(varchar = N)]` | Use VARCHAR(N) instead of TEXT in migrations. |
312///
313/// Multiple attributes can be combined: `#[field(create, update, response)]`
314///
315/// # Examples
316///
317/// ## Basic Usage
318///
319/// ```rust,ignore
320/// use entity_derive::Entity;
321/// use uuid::Uuid;
322/// use chrono::{DateTime, Utc};
323///
324/// #[derive(Entity)]
325/// #[entity(table = "users", schema = "core")]
326/// pub struct User {
327///     #[id]
328///     pub id: Uuid,
329///
330///     #[field(create, update, response)]
331///     pub name: String,
332///
333///     #[field(create, update, response)]
334///     pub email: String,
335///
336///     #[field(skip)]
337///     pub password_hash: String,
338///
339///     #[field(response)]
340///     #[auto]
341///     pub created_at: DateTime<Utc>,
342/// }
343/// ```
344///
345/// ## Custom SQL Implementation
346///
347/// For complex queries with joins, use `sql = "trait"`:
348///
349/// ```rust,ignore
350/// #[derive(Entity)]
351/// #[entity(table = "posts", sql = "trait")]
352/// pub struct Post {
353///     #[id]
354///     pub id: Uuid,
355///     #[field(create, update, response)]
356///     pub title: String,
357///     #[field(create, response)]
358///     pub author_id: Uuid,
359/// }
360///
361/// // Implement the repository yourself
362/// #[async_trait]
363/// impl PostRepository for PgPool {
364///     type Error = sqlx::Error;
365///
366///     async fn find_by_id(&self, id: Uuid) -> Result<Option<Post>, Self::Error> {
367///         sqlx::query_as!(Post,
368///             r#"SELECT p.*, u.name as author_name
369///                FROM posts p
370///                JOIN users u ON p.author_id = u.id
371///                WHERE p.id = $1"#,
372///             id
373///         )
374///         .fetch_optional(self)
375///         .await
376///     }
377///     // ... other methods
378/// }
379/// ```
380///
381/// ## DTOs Only (No Database Layer)
382///
383/// ```rust,ignore
384/// #[derive(Entity)]
385/// #[entity(table = "events", sql = "none")]
386/// pub struct Event {
387///     #[id]
388///     pub id: Uuid,
389///     #[field(create, response)]
390///     pub name: String,
391/// }
392/// // Only generates CreateEventRequest, EventResponse, etc.
393/// // No repository trait or SQL implementation
394/// ```
395///
396/// ## Migration Generation
397///
398/// Generate compile-time SQL migrations with `migrations`:
399///
400/// ```rust,ignore
401/// #[derive(Entity)]
402/// #[entity(table = "products", migrations)]
403/// pub struct Product {
404///     #[id]
405///     pub id: Uuid,
406///
407///     #[field(create, update, response)]
408///     #[column(unique, index)]
409///     pub sku: String,
410///
411///     #[field(create, update, response)]
412///     #[column(varchar = 200)]
413///     pub name: String,
414///
415///     #[field(create, update, response)]
416///     #[column(check = "price >= 0")]
417///     pub price: f64,
418///
419///     #[belongs_to(Category, on_delete = "cascade")]
420///     pub category_id: Uuid,
421/// }
422///
423/// // Generated constants:
424/// // Product::MIGRATION_UP - CREATE TABLE, indexes, constraints
425/// // Product::MIGRATION_DOWN - DROP TABLE CASCADE
426///
427/// // Apply migration:
428/// sqlx::query(Product::MIGRATION_UP).execute(&pool).await?;
429/// ```
430///
431/// # Security
432///
433/// Use `#[field(skip)]` to prevent sensitive data from leaking:
434///
435/// ```rust,ignore
436/// pub struct User {
437///     #[field(skip)]
438///     pub password_hash: String,  // Never in any DTO
439///
440///     #[field(skip)]
441///     pub api_secret: String,     // Never in any DTO
442///
443///     #[field(skip)]
444///     pub internal_notes: String, // Admin-only, not in public API
445/// }
446/// ```
447///
448/// # Generated SQL
449///
450/// The macro generates parameterized SQL queries that are safe from injection:
451///
452/// ```sql
453/// -- CREATE
454/// INSERT INTO users (id, field1, field2, ...)
455/// VALUES ($1, $2, $3, ...)
456///
457/// -- READ
458/// SELECT * FROM users WHERE id = $1
459///
460/// -- UPDATE (dynamic based on provided fields)
461/// UPDATE users SET field1 = $1, field2 = $2 WHERE id = $3
462///
463/// -- DELETE
464/// DELETE FROM users WHERE id = $1 RETURNING id
465///
466/// -- LIST
467/// SELECT * FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2
468/// ```
469#[proc_macro_derive(
470    Entity,
471    attributes(
472        entity, field, id, auto, validate, belongs_to, has_many, projection, filter, command,
473        example, column, map
474    )
475)]
476pub fn derive_entity(input: TokenStream) -> TokenStream {
477    entity::derive(input)
478}
479
480/// Derive macro for generating `OpenAPI` error response documentation.
481///
482/// # Overview
483///
484/// The `EntityError` derive macro generates `OpenAPI` response documentation
485/// from error enum variants, using `#[status(code)]` attributes and doc
486/// comments.
487///
488/// # Example
489///
490/// ```rust,ignore
491/// use entity_derive::EntityError;
492/// use thiserror::Error;
493/// use utoipa::ToSchema;
494///
495/// #[derive(Debug, Error, ToSchema, EntityError)]
496/// pub enum UserError {
497///     /// User with this email already exists
498///     #[error("Email already exists")]
499///     #[status(409)]
500///     EmailExists,
501///
502///     /// User not found by ID
503///     #[error("User not found")]
504///     #[status(404)]
505///     NotFound,
506///
507///     /// Invalid credentials provided
508///     #[error("Invalid credentials")]
509///     #[status(401)]
510///     InvalidCredentials,
511/// }
512/// ```
513///
514/// # Generated Code
515///
516/// For `UserError`, generates:
517/// - `UserErrorResponses` struct with helper methods
518/// - `status_codes()` - returns all error status codes
519/// - `descriptions()` - returns all error descriptions
520/// - `utoipa_responses()` - returns tuples for `OpenAPI` responses
521///
522/// # Attributes
523///
524/// | Attribute | Required | Description |
525/// |-----------|----------|-------------|
526/// | `#[status(code)]` | **Yes** | HTTP status code (e.g., 404, 409, 500) |
527/// | `/// Doc comment` | No | Used as response description |
528#[proc_macro_derive(EntityError, attributes(status))]
529pub fn derive_entity_error(input: TokenStream) -> TokenStream {
530    error::derive(input)
531}
532
533/// Derive macro for generating `PostgreSQL` enum boilerplate.
534///
535/// # Overview
536///
537/// The `ValueObject` derive macro generates all boilerplate code needed for
538/// a `PostgreSQL` enum type: `Display`, `FromStr`, `AsRef<str>`, and
539/// `TryFrom<&str>` implementations, plus `sqlx` and `serde` attributes.
540///
541/// # Generated Code
542///
543/// For an enum named `OrderStatus` with `pg_type = "order_status"`,
544/// the macro generates:
545///
546/// - **`Debug, Clone, PartialEq, Eq, PartialOrd`** derives
547/// - **`sqlx(type_name = "order_status", rename_all = "lowercase")`** attribute
548/// - **`serde(rename_all = "lowercase")`** attribute
549/// - **`impl Display`** — lowercase variant names
550/// - **`impl FromStr`** — case-insensitive parsing
551/// - **`impl AsRef<str>`** — lowercase string representation
552/// - **`impl TryFrom<&str>`** — delegates to `FromStr`
553///
554/// # Attributes
555///
556/// | Attribute | Required | Description |
557/// |-----------|----------|-------------|
558/// | `pg_type` | **Yes** | `PostgreSQL` enum type name (e.g., `"order_status"`) |
559///
560/// # Example
561///
562/// ```rust,ignore
563/// use entity_derive::ValueObject;
564///
565/// #[derive(ValueObject)]
566/// #[value_object(pg_type = "order_status")]
567/// pub enum OrderStatus {
568///     Pending,
569///     Confirmed,
570///     Cancelled,
571/// }
572///
573/// // Generated:
574/// // impl Display for OrderStatus
575/// // impl FromStr for OrderStatus (case-insensitive)
576/// // impl AsRef<str> for OrderStatus
577/// // impl TryFrom<&str> for OrderStatus
578/// ```
579#[proc_macro_derive(ValueObject, attributes(value_object))]
580pub fn derive_value_object(input: TokenStream) -> TokenStream {
581    value_object::derive(input)
582}