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
34#[cfg(feature = "outbox")]
35pub mod outbox;
36pub mod policy;
37pub mod prelude;
38#[cfg(feature = "streams")]
39pub mod stream;
40pub mod transaction;
41
42/// Re-export `async_trait` for generated code.
43pub use async_trait::async_trait;
44
45/// Compare two strings in const context.
46///
47/// Used by generated code to verify at compile time that
48/// `#[column(pg_enum = "...")]` matches the `ValueObject`'s
49/// `#[value_object(pg_type = "...")]` declaration.
50#[must_use]
51pub const fn const_str_eq(a: &str, b: &str) -> bool {
52 let a = a.as_bytes();
53 let b = b.as_bytes();
54 if a.len() != b.len() {
55 return false;
56 }
57 let mut i = 0;
58 while i < a.len() {
59 if a[i] != b[i] {
60 return false;
61 }
62 i += 1;
63 }
64 true
65}
66
67/// Base repository trait.
68///
69/// All generated `{Entity}Repository` traits include these associated types
70/// and methods. This trait is not directly extended but serves as documentation
71/// for the common interface.
72///
73/// # Associated Types
74///
75/// - `Error` — Error type for repository operations
76/// - `Pool` — Underlying database pool type
77///
78/// # Example
79///
80/// Generated traits follow this pattern:
81///
82/// ```rust,ignore
83/// #[async_trait]
84/// pub trait UserRepository: Send + Sync {
85/// type Error: std::error::Error + Send + Sync;
86/// type Pool;
87///
88/// fn pool(&self) -> &Self::Pool;
89/// async fn create(&self, dto: CreateUserRequest) -> Result<User, Self::Error>;
90/// async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, Self::Error>;
91/// // ...
92/// }
93/// ```
94pub trait Repository: Send + Sync {
95 /// Error type for repository operations.
96 ///
97 /// Must implement `std::error::Error + Send + Sync` for async
98 /// compatibility.
99 type Error: std::error::Error + Send + Sync;
100
101 /// Underlying database pool type.
102 ///
103 /// Enables access to the pool for transactions and custom queries.
104 type Pool;
105
106 /// Get reference to the underlying database pool.
107 ///
108 /// # Example
109 ///
110 /// ```rust,ignore
111 /// let pool = repo.pool();
112 /// let mut tx = pool.begin().await?;
113 /// // Custom operations...
114 /// tx.commit().await?;
115 /// ```
116 fn pool(&self) -> &Self::Pool;
117}
118
119/// Pagination parameters for list operations.
120///
121/// Used by `list` and `query` methods to control result pagination.
122///
123/// # Example
124///
125/// ```rust
126/// use entity_core::Pagination;
127///
128/// let page = Pagination::new(10, 0); // First 10 items
129/// let next = Pagination::new(10, 10); // Next 10 items
130/// ```
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct Pagination {
133 /// Maximum number of results to return.
134 pub limit: i64,
135
136 /// Number of results to skip.
137 pub offset: i64
138}
139
140impl Pagination {
141 /// Create new pagination parameters.
142 ///
143 /// # Arguments
144 ///
145 /// * `limit` — Maximum results to return
146 /// * `offset` — Number of results to skip
147 #[must_use]
148 pub const fn new(limit: i64, offset: i64) -> Self {
149 Self {
150 limit,
151 offset
152 }
153 }
154
155 /// Create pagination for a specific page.
156 ///
157 /// # Arguments
158 ///
159 /// * `page` — Page number (0-indexed)
160 /// * `per_page` — Items per page
161 ///
162 /// # Example
163 ///
164 /// ```rust
165 /// use entity_core::Pagination;
166 ///
167 /// let page_0 = Pagination::page(0, 25); // offset=0, limit=25
168 /// let page_2 = Pagination::page(2, 25); // offset=50, limit=25
169 /// ```
170 #[must_use]
171 pub const fn page(page: i64, per_page: i64) -> Self {
172 Self {
173 limit: per_page,
174 offset: page * per_page
175 }
176 }
177}
178
179impl Default for Pagination {
180 fn default() -> Self {
181 Self {
182 limit: 100,
183 offset: 0
184 }
185 }
186}
187
188/// Sort direction for ordered queries.
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
190pub enum SortDirection {
191 /// Ascending order (A-Z, 0-9, oldest first).
192 #[default]
193 Asc,
194
195 /// Descending order (Z-A, 9-0, newest first).
196 Desc
197}
198
199impl SortDirection {
200 /// Convert to SQL keyword.
201 #[must_use]
202 pub const fn as_sql(&self) -> &'static str {
203 match self {
204 Self::Asc => "ASC",
205 Self::Desc => "DESC"
206 }
207 }
208}
209
210/// Kind of lifecycle event.
211///
212/// Used by generated event enums to categorize events.
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
214pub enum EventKind {
215 /// Entity was created.
216 Created,
217
218 /// Entity was updated.
219 Updated,
220
221 /// Entity was soft-deleted.
222 SoftDeleted,
223
224 /// Entity was hard-deleted (permanently removed).
225 HardDeleted,
226
227 /// Entity was restored from soft-delete.
228 Restored
229}
230
231impl EventKind {
232 /// Check if this is a delete event (soft or hard).
233 #[must_use]
234 pub const fn is_delete(&self) -> bool {
235 matches!(self, Self::SoftDeleted | Self::HardDeleted)
236 }
237
238 /// Check if this is a mutation event (create, update, delete).
239 #[must_use]
240 pub const fn is_mutation(&self) -> bool {
241 !matches!(self, Self::Restored)
242 }
243}
244
245/// Base trait for entity lifecycle events.
246///
247/// Generated event enums implement this trait, enabling generic
248/// event handling and dispatching.
249///
250/// # Example
251///
252/// ```rust,ignore
253/// fn handle_event<E: EntityEvent>(event: &E) {
254/// println!("Event {:?} for entity {:?}", event.kind(), event.entity_id());
255/// }
256/// ```
257pub trait EntityEvent: Send + Sync + std::fmt::Debug {
258 /// Type of entity ID.
259 type Id;
260
261 /// Get the kind of event.
262 fn kind(&self) -> EventKind;
263
264 /// Get the entity ID associated with this event.
265 fn entity_id(&self) -> &Self::Id;
266}
267
268/// Kind of business command.
269///
270/// Used by generated command enums to categorize commands for auditing
271/// and routing purposes.
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
273pub enum CommandKind {
274 /// Creates a new entity (e.g., Register, Create).
275 Create,
276
277 /// Modifies an existing entity (e.g., `UpdateEmail`, `ChangeStatus`).
278 Update,
279
280 /// Removes an entity (e.g., Delete, Deactivate).
281 Delete,
282
283 /// Custom business operation that doesn't fit CRUD.
284 Custom
285}
286
287impl CommandKind {
288 /// Check if this command creates an entity.
289 #[must_use]
290 pub const fn is_create(&self) -> bool {
291 matches!(self, Self::Create)
292 }
293
294 /// Check if this command modifies state.
295 #[must_use]
296 pub const fn is_mutation(&self) -> bool {
297 !matches!(self, Self::Custom)
298 }
299}
300
301/// Base trait for entity commands.
302///
303/// Generated command enums implement this trait, enabling generic
304/// command handling, auditing, and dispatching.
305///
306/// # Example
307///
308/// ```rust,ignore
309/// fn audit_command<C: EntityCommand>(cmd: &C) {
310/// log::info!("Executing command: {} ({:?})", cmd.name(), cmd.kind());
311/// }
312/// ```
313pub trait EntityCommand: Send + Sync + std::fmt::Debug {
314 /// Get the kind of command for categorization.
315 fn kind(&self) -> CommandKind;
316
317 /// Get the command name as a string for logging/auditing.
318 fn name(&self) -> &'static str;
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn pagination_new() {
327 let p = Pagination::new(50, 100);
328 assert_eq!(p.limit, 50);
329 assert_eq!(p.offset, 100);
330 }
331
332 #[test]
333 fn pagination_page() {
334 let p = Pagination::page(2, 25);
335 assert_eq!(p.limit, 25);
336 assert_eq!(p.offset, 50);
337 }
338
339 #[test]
340 fn pagination_default() {
341 let p = Pagination::default();
342 assert_eq!(p.limit, 100);
343 assert_eq!(p.offset, 0);
344 }
345
346 #[test]
347 fn sort_direction_sql() {
348 assert_eq!(SortDirection::Asc.as_sql(), "ASC");
349 assert_eq!(SortDirection::Desc.as_sql(), "DESC");
350 }
351
352 #[test]
353 fn sort_direction_default() {
354 assert_eq!(SortDirection::default(), SortDirection::Asc);
355 }
356
357 #[test]
358 fn event_kind_is_delete() {
359 assert!(!EventKind::Created.is_delete());
360 assert!(!EventKind::Updated.is_delete());
361 assert!(EventKind::SoftDeleted.is_delete());
362 assert!(EventKind::HardDeleted.is_delete());
363 assert!(!EventKind::Restored.is_delete());
364 }
365
366 #[test]
367 fn event_kind_is_mutation() {
368 assert!(EventKind::Created.is_mutation());
369 assert!(EventKind::Updated.is_mutation());
370 assert!(EventKind::SoftDeleted.is_mutation());
371 assert!(EventKind::HardDeleted.is_mutation());
372 assert!(!EventKind::Restored.is_mutation());
373 }
374
375 #[test]
376 fn command_kind_is_create() {
377 assert!(CommandKind::Create.is_create());
378 assert!(!CommandKind::Update.is_create());
379 assert!(!CommandKind::Delete.is_create());
380 assert!(!CommandKind::Custom.is_create());
381 }
382
383 #[test]
384 fn command_kind_is_mutation() {
385 assert!(CommandKind::Create.is_mutation());
386 assert!(CommandKind::Update.is_mutation());
387 assert!(CommandKind::Delete.is_mutation());
388 assert!(!CommandKind::Custom.is_mutation());
389 }
390
391 #[test]
392 fn const_str_eq_equal_strings() {
393 assert!(const_str_eq("order_status", "order_status"));
394 assert!(const_str_eq("", ""));
395 }
396
397 #[test]
398 fn const_str_eq_different_lengths() {
399 assert!(!const_str_eq("order", "order_status"));
400 assert!(!const_str_eq("order_status", ""));
401 }
402
403 #[test]
404 fn const_str_eq_same_length_different_content() {
405 assert!(!const_str_eq("order_status", "order_states"));
406 assert!(!const_str_eq("abc", "abd"));
407 }
408
409 #[test]
410 fn const_str_eq_in_const_context() {
411 const OK: bool = const_str_eq("user_role", "user_role");
412 const MISMATCH: bool = const_str_eq("user_role", "user_rank");
413 assert_eq!((OK, MISMATCH), (true, false));
414 }
415}
416
417/// Serde helpers for generated DTOs.
418#[cfg(feature = "serde")]
419pub mod serde_helpers {
420 /// Double-`Option` (de)serialization for PATCH semantics.
421 ///
422 /// | JSON | Rust |
423 /// |------|------|
424 /// | field absent | `None` (leave unchanged) |
425 /// | `"field": null` | `Some(None)` (set column to NULL) |
426 /// | `"field": v` | `Some(Some(v))` (set column to v) |
427 pub mod double_option {
428 use serde::{Deserialize, Deserializer, Serialize, Serializer};
429
430 /// Deserialize a present-but-maybe-null field into `Some(inner)`.
431 ///
432 /// # Errors
433 ///
434 /// Propagates inner deserialization errors.
435 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
436 where
437 T: Deserialize<'de>,
438 D: Deserializer<'de>
439 {
440 Option::<T>::deserialize(deserializer).map(Some)
441 }
442
443 /// Serialize the inner `Option`, treating outer `None` as null.
444 ///
445 /// # Errors
446 ///
447 /// Propagates inner serialization errors.
448 pub fn serialize<T, S>(value: &Option<Option<T>>, serializer: S) -> Result<S::Ok, S::Error>
449 where
450 T: Serialize,
451 S: Serializer
452 {
453 match value {
454 Some(inner) => inner.serialize(serializer),
455 None => serializer.serialize_none()
456 }
457 }
458 }
459
460 #[cfg(all(test, feature = "serde_json"))]
461 mod tests {
462 #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Default)]
463 struct Patch {
464 #[serde(
465 default,
466 skip_serializing_if = "Option::is_none",
467 with = "super::double_option"
468 )]
469 nick: Option<Option<String>>
470 }
471
472 #[test]
473 fn absent_field_is_outer_none() {
474 let patch: Patch = serde_json::from_str("{}").unwrap();
475 assert_eq!(patch.nick, None);
476 }
477
478 #[test]
479 fn null_field_is_some_none() {
480 let patch: Patch = serde_json::from_str(r#"{"nick": null}"#).unwrap();
481 assert_eq!(patch.nick, Some(None));
482 }
483
484 #[test]
485 fn value_field_is_some_some() {
486 let patch: Patch = serde_json::from_str(r#"{"nick": "neo"}"#).unwrap();
487 assert_eq!(patch.nick, Some(Some("neo".to_string())));
488 }
489
490 #[test]
491 fn outer_none_skipped_on_serialize() {
492 let json = serde_json::to_string(&Patch {
493 nick: None
494 })
495 .unwrap();
496 assert_eq!(json, "{}");
497 }
498
499 #[test]
500 fn some_none_serializes_null() {
501 let json = serde_json::to_string(&Patch {
502 nick: Some(None)
503 })
504 .unwrap();
505 assert_eq!(json, r#"{"nick":null}"#);
506 }
507 }
508}