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