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 utils;
219
220use proc_macro::TokenStream;
221
222/// Derive macro for generating complete domain boilerplate from a single entity
223/// definition.
224///
225/// # Overview
226///
227/// The `Entity` derive macro generates all the boilerplate code needed for a
228/// typical CRUD application: DTOs, repository traits, SQL implementations, and
229/// type mappers.
230///
231/// # Generated Types
232///
233/// For an entity named `User`, the macro generates:
234///
235/// - **`CreateUserRequest`** — DTO for creation (fields marked with
236/// `#[field(create)]`)
237/// - **`UpdateUserRequest`** — DTO for updates (fields marked with
238/// `#[field(update)]`, wrapped in `Option`)
239/// - **`UserResponse`** — DTO for responses (fields marked with
240/// `#[field(response)]`)
241/// - **`UserRow`** — Database row struct (implements `sqlx::FromRow`)
242/// - **`InsertableUser`** — Struct for INSERT operations
243/// - **`UserRepository`** — Async trait with CRUD methods
244/// - **`impl UserRepository for PgPool`** — PostgreSQL implementation (when
245/// `sql = "full"`)
246///
247/// # Entity Attributes
248///
249/// Configure the entity using `#[entity(...)]`:
250///
251/// | Attribute | Required | Default | Description |
252/// |-----------|----------|---------|-------------|
253/// | `table` | **Yes** | — | Database table name |
254/// | `schema` | No | `"public"` | Database schema name |
255/// | `sql` | No | `"full"` | SQL generation: `"full"`, `"trait"`, or `"none"` |
256/// | `dialect` | No | `"postgres"` | Database dialect: `"postgres"`, `"clickhouse"`, `"mongodb"` |
257/// | `uuid` | No | `"v7"` | UUID version for ID: `"v7"` (time-ordered) or `"v4"` (random) |
258///
259/// # Field Attributes
260///
261/// | Attribute | Description |
262/// |-----------|-------------|
263/// | `#[id]` | Primary key. Auto-generates UUID (v7 by default, configurable with `uuid` attribute). Always included in `Response`. |
264/// | `#[auto]` | Auto-generated field (e.g., `created_at`). Excluded from `Create`/`Update`. |
265/// | `#[field(create)]` | Include in `CreateRequest`. |
266/// | `#[field(update)]` | Include in `UpdateRequest`. Wrapped in `Option<T>` if not already. |
267/// | `#[field(response)]` | Include in `Response`. |
268/// | `#[field(skip)]` | Exclude from ALL DTOs. Use for sensitive data. |
269/// | `#[belongs_to(Entity)]` | Foreign key relation. Generates `find_{entity}` method in repository. |
270/// | `#[has_many(Entity)]` | One-to-many relation (entity-level). Generates `find_{entities}` method. |
271/// | `#[projection(Name: f1, f2)]` | Entity-level. Defines a projection struct with specified fields. |
272/// | `#[filter]` | Exact match filter. Generates field in Query struct with `=` comparison. |
273/// | `#[filter(like)]` | ILIKE pattern filter. Generates field for text pattern matching. |
274/// | `#[filter(range)]` | Range filter. Generates `field_from` and `field_to` fields. |
275///
276/// Multiple attributes can be combined: `#[field(create, update, response)]`
277///
278/// # Examples
279///
280/// ## Basic Usage
281///
282/// ```rust,ignore
283/// use entity_derive::Entity;
284/// use uuid::Uuid;
285/// use chrono::{DateTime, Utc};
286///
287/// #[derive(Entity)]
288/// #[entity(table = "users", schema = "core")]
289/// pub struct User {
290/// #[id]
291/// pub id: Uuid,
292///
293/// #[field(create, update, response)]
294/// pub name: String,
295///
296/// #[field(create, update, response)]
297/// pub email: String,
298///
299/// #[field(skip)]
300/// pub password_hash: String,
301///
302/// #[field(response)]
303/// #[auto]
304/// pub created_at: DateTime<Utc>,
305/// }
306/// ```
307///
308/// ## Custom SQL Implementation
309///
310/// For complex queries with joins, use `sql = "trait"`:
311///
312/// ```rust,ignore
313/// #[derive(Entity)]
314/// #[entity(table = "posts", sql = "trait")]
315/// pub struct Post {
316/// #[id]
317/// pub id: Uuid,
318/// #[field(create, update, response)]
319/// pub title: String,
320/// #[field(create, response)]
321/// pub author_id: Uuid,
322/// }
323///
324/// // Implement the repository yourself
325/// #[async_trait]
326/// impl PostRepository for PgPool {
327/// type Error = sqlx::Error;
328///
329/// async fn find_by_id(&self, id: Uuid) -> Result<Option<Post>, Self::Error> {
330/// sqlx::query_as!(Post,
331/// r#"SELECT p.*, u.name as author_name
332/// FROM posts p
333/// JOIN users u ON p.author_id = u.id
334/// WHERE p.id = $1"#,
335/// id
336/// )
337/// .fetch_optional(self)
338/// .await
339/// }
340/// // ... other methods
341/// }
342/// ```
343///
344/// ## DTOs Only (No Database Layer)
345///
346/// ```rust,ignore
347/// #[derive(Entity)]
348/// #[entity(table = "events", sql = "none")]
349/// pub struct Event {
350/// #[id]
351/// pub id: Uuid,
352/// #[field(create, response)]
353/// pub name: String,
354/// }
355/// // Only generates CreateEventRequest, EventResponse, etc.
356/// // No repository trait or SQL implementation
357/// ```
358///
359/// # Security
360///
361/// Use `#[field(skip)]` to prevent sensitive data from leaking:
362///
363/// ```rust,ignore
364/// pub struct User {
365/// #[field(skip)]
366/// pub password_hash: String, // Never in any DTO
367///
368/// #[field(skip)]
369/// pub api_secret: String, // Never in any DTO
370///
371/// #[field(skip)]
372/// pub internal_notes: String, // Admin-only, not in public API
373/// }
374/// ```
375///
376/// # Generated SQL
377///
378/// The macro generates parameterized SQL queries that are safe from injection:
379///
380/// ```sql
381/// -- CREATE
382/// INSERT INTO schema.table (id, field1, field2, ...)
383/// VALUES ($1, $2, $3, ...)
384///
385/// -- READ
386/// SELECT * FROM schema.table WHERE id = $1
387///
388/// -- UPDATE (dynamic based on provided fields)
389/// UPDATE schema.table SET field1 = $1, field2 = $2 WHERE id = $3
390///
391/// -- DELETE
392/// DELETE FROM schema.table WHERE id = $1 RETURNING id
393///
394/// -- LIST
395/// SELECT * FROM schema.table ORDER BY created_at DESC LIMIT $1 OFFSET $2
396/// ```
397#[proc_macro_derive(
398 Entity,
399 attributes(
400 entity, field, id, auto, validate, belongs_to, has_many, projection, filter, command
401 )
402)]
403pub fn derive_entity(input: TokenStream) -> TokenStream {
404 entity::derive(input)
405}