entity_core/lib.rs
1// SPDX-FileCopyrightText: 2025-2026 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4//! Core traits and types for entity-derive.
5//!
6//! This crate provides the foundational traits and types used by entity-derive
7//! generated code. It can also be used standalone for manual implementations.
8//!
9//! # Overview
10//!
11//! - [`Repository`] — Base trait for all generated repository traits
12//! - [`Pagination`] — Common pagination parameters
13//! - [`prelude`] — Convenient re-exports
14//!
15//! # Usage
16//!
17//! Most users should use `entity-derive` directly, which re-exports this crate.
18//! For manual implementations:
19//!
20//! ```rust,ignore
21//! use entity_core::prelude::*;
22//!
23//! #[async_trait]
24//! impl UserRepository for MyPool {
25//! type Error = MyError;
26//! type Pool = PgPool;
27//! // ...
28//! }
29//! ```
30
31#![warn(missing_docs)]
32#![warn(clippy::all)]
33
34pub mod policy;
35pub mod prelude;
36#[cfg(feature = "streams")]
37pub mod stream;
38pub mod transaction;
39
40/// Re-export `async_trait` for generated code.
41pub use async_trait::async_trait;
42
43/// Base repository trait.
44///
45/// All generated `{Entity}Repository` traits include these associated types
46/// and methods. This trait is not directly extended but serves as documentation
47/// for the common interface.
48///
49/// # Associated Types
50///
51/// - `Error` — Error type for repository operations
52/// - `Pool` — Underlying database pool type
53///
54/// # Example
55///
56/// Generated traits follow this pattern:
57///
58/// ```rust,ignore
59/// #[async_trait]
60/// pub trait UserRepository: Send + Sync {
61/// type Error: std::error::Error + Send + Sync;
62/// type Pool;
63///
64/// fn pool(&self) -> &Self::Pool;
65/// async fn create(&self, dto: CreateUserRequest) -> Result<User, Self::Error>;
66/// async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, Self::Error>;
67/// // ...
68/// }
69/// ```
70pub trait Repository: Send + Sync {
71 /// Error type for repository operations.
72 ///
73 /// Must implement `std::error::Error + Send + Sync` for async
74 /// compatibility.
75 type Error: std::error::Error + Send + Sync;
76
77 /// Underlying database pool type.
78 ///
79 /// Enables access to the pool for transactions and custom queries.
80 type Pool;
81
82 /// Get reference to the underlying database pool.
83 ///
84 /// # Example
85 ///
86 /// ```rust,ignore
87 /// let pool = repo.pool();
88 /// let mut tx = pool.begin().await?;
89 /// // Custom operations...
90 /// tx.commit().await?;
91 /// ```
92 fn pool(&self) -> &Self::Pool;
93}
94
95/// Pagination parameters for list operations.
96///
97/// Used by `list` and `query` methods to control result pagination.
98///
99/// # Example
100///
101/// ```rust
102/// use entity_core::Pagination;
103///
104/// let page = Pagination::new(10, 0); // First 10 items
105/// let next = Pagination::new(10, 10); // Next 10 items
106/// ```
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct Pagination {
109 /// Maximum number of results to return.
110 pub limit: i64,
111
112 /// Number of results to skip.
113 pub offset: i64
114}
115
116impl Pagination {
117 /// Create new pagination parameters.
118 ///
119 /// # Arguments
120 ///
121 /// * `limit` — Maximum results to return
122 /// * `offset` — Number of results to skip
123 #[must_use]
124 pub const fn new(limit: i64, offset: i64) -> Self {
125 Self {
126 limit,
127 offset
128 }
129 }
130
131 /// Create pagination for a specific page.
132 ///
133 /// # Arguments
134 ///
135 /// * `page` — Page number (0-indexed)
136 /// * `per_page` — Items per page
137 ///
138 /// # Example
139 ///
140 /// ```rust
141 /// use entity_core::Pagination;
142 ///
143 /// let page_0 = Pagination::page(0, 25); // offset=0, limit=25
144 /// let page_2 = Pagination::page(2, 25); // offset=50, limit=25
145 /// ```
146 #[must_use]
147 pub const fn page(page: i64, per_page: i64) -> Self {
148 Self {
149 limit: per_page,
150 offset: page * per_page
151 }
152 }
153}
154
155impl Default for Pagination {
156 fn default() -> Self {
157 Self {
158 limit: 100,
159 offset: 0
160 }
161 }
162}
163
164/// Sort direction for ordered queries.
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
166pub enum SortDirection {
167 /// Ascending order (A-Z, 0-9, oldest first).
168 #[default]
169 Asc,
170
171 /// Descending order (Z-A, 9-0, newest first).
172 Desc
173}
174
175impl SortDirection {
176 /// Convert to SQL keyword.
177 #[must_use]
178 pub const fn as_sql(&self) -> &'static str {
179 match self {
180 Self::Asc => "ASC",
181 Self::Desc => "DESC"
182 }
183 }
184}
185
186/// Kind of lifecycle event.
187///
188/// Used by generated event enums to categorize events.
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
190pub enum EventKind {
191 /// Entity was created.
192 Created,
193
194 /// Entity was updated.
195 Updated,
196
197 /// Entity was soft-deleted.
198 SoftDeleted,
199
200 /// Entity was hard-deleted (permanently removed).
201 HardDeleted,
202
203 /// Entity was restored from soft-delete.
204 Restored
205}
206
207impl EventKind {
208 /// Check if this is a delete event (soft or hard).
209 #[must_use]
210 pub const fn is_delete(&self) -> bool {
211 matches!(self, Self::SoftDeleted | Self::HardDeleted)
212 }
213
214 /// Check if this is a mutation event (create, update, delete).
215 #[must_use]
216 pub const fn is_mutation(&self) -> bool {
217 !matches!(self, Self::Restored)
218 }
219}
220
221/// Base trait for entity lifecycle events.
222///
223/// Generated event enums implement this trait, enabling generic
224/// event handling and dispatching.
225///
226/// # Example
227///
228/// ```rust,ignore
229/// fn handle_event<E: EntityEvent>(event: &E) {
230/// println!("Event {:?} for entity {:?}", event.kind(), event.entity_id());
231/// }
232/// ```
233pub trait EntityEvent: Send + Sync + std::fmt::Debug {
234 /// Type of entity ID.
235 type Id;
236
237 /// Get the kind of event.
238 fn kind(&self) -> EventKind;
239
240 /// Get the entity ID associated with this event.
241 fn entity_id(&self) -> &Self::Id;
242}
243
244/// Kind of business command.
245///
246/// Used by generated command enums to categorize commands for auditing
247/// and routing purposes.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249pub enum CommandKind {
250 /// Creates a new entity (e.g., Register, Create).
251 Create,
252
253 /// Modifies an existing entity (e.g., `UpdateEmail`, `ChangeStatus`).
254 Update,
255
256 /// Removes an entity (e.g., Delete, Deactivate).
257 Delete,
258
259 /// Custom business operation that doesn't fit CRUD.
260 Custom
261}
262
263impl CommandKind {
264 /// Check if this command creates an entity.
265 #[must_use]
266 pub const fn is_create(&self) -> bool {
267 matches!(self, Self::Create)
268 }
269
270 /// Check if this command modifies state.
271 #[must_use]
272 pub const fn is_mutation(&self) -> bool {
273 !matches!(self, Self::Custom)
274 }
275}
276
277/// Base trait for entity commands.
278///
279/// Generated command enums implement this trait, enabling generic
280/// command handling, auditing, and dispatching.
281///
282/// # Example
283///
284/// ```rust,ignore
285/// fn audit_command<C: EntityCommand>(cmd: &C) {
286/// log::info!("Executing command: {} ({:?})", cmd.name(), cmd.kind());
287/// }
288/// ```
289pub trait EntityCommand: Send + Sync + std::fmt::Debug {
290 /// Get the kind of command for categorization.
291 fn kind(&self) -> CommandKind;
292
293 /// Get the command name as a string for logging/auditing.
294 fn name(&self) -> &'static str;
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn pagination_new() {
303 let p = Pagination::new(50, 100);
304 assert_eq!(p.limit, 50);
305 assert_eq!(p.offset, 100);
306 }
307
308 #[test]
309 fn pagination_page() {
310 let p = Pagination::page(2, 25);
311 assert_eq!(p.limit, 25);
312 assert_eq!(p.offset, 50);
313 }
314
315 #[test]
316 fn pagination_default() {
317 let p = Pagination::default();
318 assert_eq!(p.limit, 100);
319 assert_eq!(p.offset, 0);
320 }
321
322 #[test]
323 fn sort_direction_sql() {
324 assert_eq!(SortDirection::Asc.as_sql(), "ASC");
325 assert_eq!(SortDirection::Desc.as_sql(), "DESC");
326 }
327
328 #[test]
329 fn sort_direction_default() {
330 assert_eq!(SortDirection::default(), SortDirection::Asc);
331 }
332
333 #[test]
334 fn event_kind_is_delete() {
335 assert!(!EventKind::Created.is_delete());
336 assert!(!EventKind::Updated.is_delete());
337 assert!(EventKind::SoftDeleted.is_delete());
338 assert!(EventKind::HardDeleted.is_delete());
339 assert!(!EventKind::Restored.is_delete());
340 }
341
342 #[test]
343 fn event_kind_is_mutation() {
344 assert!(EventKind::Created.is_mutation());
345 assert!(EventKind::Updated.is_mutation());
346 assert!(EventKind::SoftDeleted.is_mutation());
347 assert!(EventKind::HardDeleted.is_mutation());
348 assert!(!EventKind::Restored.is_mutation());
349 }
350
351 #[test]
352 fn command_kind_is_create() {
353 assert!(CommandKind::Create.is_create());
354 assert!(!CommandKind::Update.is_create());
355 assert!(!CommandKind::Delete.is_create());
356 assert!(!CommandKind::Custom.is_create());
357 }
358
359 #[test]
360 fn command_kind_is_mutation() {
361 assert!(CommandKind::Create.is_mutation());
362 assert!(CommandKind::Update.is_mutation());
363 assert!(CommandKind::Delete.is_mutation());
364 assert!(!CommandKind::Custom.is_mutation());
365 }
366}