Skip to main content

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/// Compare two strings in const context.
44///
45/// Used by generated code to verify at compile time that
46/// `#[column(pg_enum = "...")]` matches the `ValueObject`'s
47/// `#[value_object(pg_type = "...")]` declaration.
48#[must_use]
49pub const fn const_str_eq(a: &str, b: &str) -> bool {
50    let a = a.as_bytes();
51    let b = b.as_bytes();
52    if a.len() != b.len() {
53        return false;
54    }
55    let mut i = 0;
56    while i < a.len() {
57        if a[i] != b[i] {
58            return false;
59        }
60        i += 1;
61    }
62    true
63}
64
65/// Base repository trait.
66///
67/// All generated `{Entity}Repository` traits include these associated types
68/// and methods. This trait is not directly extended but serves as documentation
69/// for the common interface.
70///
71/// # Associated Types
72///
73/// - `Error` — Error type for repository operations
74/// - `Pool` — Underlying database pool type
75///
76/// # Example
77///
78/// Generated traits follow this pattern:
79///
80/// ```rust,ignore
81/// #[async_trait]
82/// pub trait UserRepository: Send + Sync {
83///     type Error: std::error::Error + Send + Sync;
84///     type Pool;
85///
86///     fn pool(&self) -> &Self::Pool;
87///     async fn create(&self, dto: CreateUserRequest) -> Result<User, Self::Error>;
88///     async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, Self::Error>;
89///     // ...
90/// }
91/// ```
92pub trait Repository: Send + Sync {
93    /// Error type for repository operations.
94    ///
95    /// Must implement `std::error::Error + Send + Sync` for async
96    /// compatibility.
97    type Error: std::error::Error + Send + Sync;
98
99    /// Underlying database pool type.
100    ///
101    /// Enables access to the pool for transactions and custom queries.
102    type Pool;
103
104    /// Get reference to the underlying database pool.
105    ///
106    /// # Example
107    ///
108    /// ```rust,ignore
109    /// let pool = repo.pool();
110    /// let mut tx = pool.begin().await?;
111    /// // Custom operations...
112    /// tx.commit().await?;
113    /// ```
114    fn pool(&self) -> &Self::Pool;
115}
116
117/// Pagination parameters for list operations.
118///
119/// Used by `list` and `query` methods to control result pagination.
120///
121/// # Example
122///
123/// ```rust
124/// use entity_core::Pagination;
125///
126/// let page = Pagination::new(10, 0); // First 10 items
127/// let next = Pagination::new(10, 10); // Next 10 items
128/// ```
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub struct Pagination {
131    /// Maximum number of results to return.
132    pub limit: i64,
133
134    /// Number of results to skip.
135    pub offset: i64
136}
137
138impl Pagination {
139    /// Create new pagination parameters.
140    ///
141    /// # Arguments
142    ///
143    /// * `limit` — Maximum results to return
144    /// * `offset` — Number of results to skip
145    #[must_use]
146    pub const fn new(limit: i64, offset: i64) -> Self {
147        Self {
148            limit,
149            offset
150        }
151    }
152
153    /// Create pagination for a specific page.
154    ///
155    /// # Arguments
156    ///
157    /// * `page` — Page number (0-indexed)
158    /// * `per_page` — Items per page
159    ///
160    /// # Example
161    ///
162    /// ```rust
163    /// use entity_core::Pagination;
164    ///
165    /// let page_0 = Pagination::page(0, 25); // offset=0, limit=25
166    /// let page_2 = Pagination::page(2, 25); // offset=50, limit=25
167    /// ```
168    #[must_use]
169    pub const fn page(page: i64, per_page: i64) -> Self {
170        Self {
171            limit:  per_page,
172            offset: page * per_page
173        }
174    }
175}
176
177impl Default for Pagination {
178    fn default() -> Self {
179        Self {
180            limit:  100,
181            offset: 0
182        }
183    }
184}
185
186/// Sort direction for ordered queries.
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
188pub enum SortDirection {
189    /// Ascending order (A-Z, 0-9, oldest first).
190    #[default]
191    Asc,
192
193    /// Descending order (Z-A, 9-0, newest first).
194    Desc
195}
196
197impl SortDirection {
198    /// Convert to SQL keyword.
199    #[must_use]
200    pub const fn as_sql(&self) -> &'static str {
201        match self {
202            Self::Asc => "ASC",
203            Self::Desc => "DESC"
204        }
205    }
206}
207
208/// Kind of lifecycle event.
209///
210/// Used by generated event enums to categorize events.
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
212pub enum EventKind {
213    /// Entity was created.
214    Created,
215
216    /// Entity was updated.
217    Updated,
218
219    /// Entity was soft-deleted.
220    SoftDeleted,
221
222    /// Entity was hard-deleted (permanently removed).
223    HardDeleted,
224
225    /// Entity was restored from soft-delete.
226    Restored
227}
228
229impl EventKind {
230    /// Check if this is a delete event (soft or hard).
231    #[must_use]
232    pub const fn is_delete(&self) -> bool {
233        matches!(self, Self::SoftDeleted | Self::HardDeleted)
234    }
235
236    /// Check if this is a mutation event (create, update, delete).
237    #[must_use]
238    pub const fn is_mutation(&self) -> bool {
239        !matches!(self, Self::Restored)
240    }
241}
242
243/// Base trait for entity lifecycle events.
244///
245/// Generated event enums implement this trait, enabling generic
246/// event handling and dispatching.
247///
248/// # Example
249///
250/// ```rust,ignore
251/// fn handle_event<E: EntityEvent>(event: &E) {
252///     println!("Event {:?} for entity {:?}", event.kind(), event.entity_id());
253/// }
254/// ```
255pub trait EntityEvent: Send + Sync + std::fmt::Debug {
256    /// Type of entity ID.
257    type Id;
258
259    /// Get the kind of event.
260    fn kind(&self) -> EventKind;
261
262    /// Get the entity ID associated with this event.
263    fn entity_id(&self) -> &Self::Id;
264}
265
266/// Kind of business command.
267///
268/// Used by generated command enums to categorize commands for auditing
269/// and routing purposes.
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
271pub enum CommandKind {
272    /// Creates a new entity (e.g., Register, Create).
273    Create,
274
275    /// Modifies an existing entity (e.g., `UpdateEmail`, `ChangeStatus`).
276    Update,
277
278    /// Removes an entity (e.g., Delete, Deactivate).
279    Delete,
280
281    /// Custom business operation that doesn't fit CRUD.
282    Custom
283}
284
285impl CommandKind {
286    /// Check if this command creates an entity.
287    #[must_use]
288    pub const fn is_create(&self) -> bool {
289        matches!(self, Self::Create)
290    }
291
292    /// Check if this command modifies state.
293    #[must_use]
294    pub const fn is_mutation(&self) -> bool {
295        !matches!(self, Self::Custom)
296    }
297}
298
299/// Base trait for entity commands.
300///
301/// Generated command enums implement this trait, enabling generic
302/// command handling, auditing, and dispatching.
303///
304/// # Example
305///
306/// ```rust,ignore
307/// fn audit_command<C: EntityCommand>(cmd: &C) {
308///     log::info!("Executing command: {} ({:?})", cmd.name(), cmd.kind());
309/// }
310/// ```
311pub trait EntityCommand: Send + Sync + std::fmt::Debug {
312    /// Get the kind of command for categorization.
313    fn kind(&self) -> CommandKind;
314
315    /// Get the command name as a string for logging/auditing.
316    fn name(&self) -> &'static str;
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn pagination_new() {
325        let p = Pagination::new(50, 100);
326        assert_eq!(p.limit, 50);
327        assert_eq!(p.offset, 100);
328    }
329
330    #[test]
331    fn pagination_page() {
332        let p = Pagination::page(2, 25);
333        assert_eq!(p.limit, 25);
334        assert_eq!(p.offset, 50);
335    }
336
337    #[test]
338    fn pagination_default() {
339        let p = Pagination::default();
340        assert_eq!(p.limit, 100);
341        assert_eq!(p.offset, 0);
342    }
343
344    #[test]
345    fn sort_direction_sql() {
346        assert_eq!(SortDirection::Asc.as_sql(), "ASC");
347        assert_eq!(SortDirection::Desc.as_sql(), "DESC");
348    }
349
350    #[test]
351    fn sort_direction_default() {
352        assert_eq!(SortDirection::default(), SortDirection::Asc);
353    }
354
355    #[test]
356    fn event_kind_is_delete() {
357        assert!(!EventKind::Created.is_delete());
358        assert!(!EventKind::Updated.is_delete());
359        assert!(EventKind::SoftDeleted.is_delete());
360        assert!(EventKind::HardDeleted.is_delete());
361        assert!(!EventKind::Restored.is_delete());
362    }
363
364    #[test]
365    fn event_kind_is_mutation() {
366        assert!(EventKind::Created.is_mutation());
367        assert!(EventKind::Updated.is_mutation());
368        assert!(EventKind::SoftDeleted.is_mutation());
369        assert!(EventKind::HardDeleted.is_mutation());
370        assert!(!EventKind::Restored.is_mutation());
371    }
372
373    #[test]
374    fn command_kind_is_create() {
375        assert!(CommandKind::Create.is_create());
376        assert!(!CommandKind::Update.is_create());
377        assert!(!CommandKind::Delete.is_create());
378        assert!(!CommandKind::Custom.is_create());
379    }
380
381    #[test]
382    fn command_kind_is_mutation() {
383        assert!(CommandKind::Create.is_mutation());
384        assert!(CommandKind::Update.is_mutation());
385        assert!(CommandKind::Delete.is_mutation());
386        assert!(!CommandKind::Custom.is_mutation());
387    }
388
389    #[test]
390    fn const_str_eq_equal_strings() {
391        assert!(const_str_eq("order_status", "order_status"));
392        assert!(const_str_eq("", ""));
393    }
394
395    #[test]
396    fn const_str_eq_different_lengths() {
397        assert!(!const_str_eq("order", "order_status"));
398        assert!(!const_str_eq("order_status", ""));
399    }
400
401    #[test]
402    fn const_str_eq_same_length_different_content() {
403        assert!(!const_str_eq("order_status", "order_states"));
404        assert!(!const_str_eq("abc", "abd"));
405    }
406
407    #[test]
408    fn const_str_eq_in_const_context() {
409        const OK: bool = const_str_eq("user_role", "user_role");
410        const MISMATCH: bool = const_str_eq("user_role", "user_rank");
411        assert_eq!((OK, MISMATCH), (true, false));
412    }
413}