Skip to main content

nestforge_orm/
lib.rs

1use std::{future::Future, pin::Pin, sync::Arc};
2
3use nestforge_db::{Db, DbError};
4use thiserror::Error;
5
6/** Type alias for async repository operations */
7pub type RepoFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
8
9/**
10 * EntityMeta Trait
11 *
12 * Metadata trait for database entities.
13 * Provides information about the table and ID column.
14 *
15 * # Type Parameters
16 * - `Self`: The entity type implementing the trait
17 * - `Id`: The type of the entity's primary key
18 *
19 * # Implementation
20 * Typically implemented via the `#[entity]` macro.
21 */
22pub trait EntityMeta: Send + Sync + 'static {
23    /** The type of the primary key */
24    type Id: Send + Sync + Clone + 'static;
25
26    /**
27     * Returns the database table name for this entity.
28     */
29    fn table_name() -> &'static str;
30    
31    /**
32     * Returns the column name for the primary key.
33     * Defaults to "id".
34     */
35    fn id_column() -> &'static str {
36        "id"
37    }
38    
39    /**
40     * Returns a reference to the entity's ID value.
41     */
42    fn id_value(&self) -> &Self::Id;
43}
44
45/**
46 * Repo Trait
47 *
48 * A trait for implementing SQL repositories in NestForge.
49 * Provides standard CRUD operations for database entities.
50 *
51 * # Type Parameters
52 * - `T`: The entity type (must implement EntityMeta)
53 *
54 * # Methods
55 * - `find_all`: Retrieves all entities
56 * - `find_by_id`: Retrieves a single entity by ID
57 * - `create`: Creates a new entity
58 * - `update_by_id`: Updates an existing entity
59 * - `delete_by_id`: Removes an entity
60 */
61pub trait Repo<T>: Send + Sync
62where
63    T: EntityMeta,
64{
65    /**
66     * Retrieves all entities of type T.
67     */
68    fn find_all(&self) -> RepoFuture<'_, Result<Vec<T>, OrmError>>;
69    
70    /**
71     * Retrieves a single entity by its ID.
72     */
73    fn find_by_id(&self, id: T::Id) -> RepoFuture<'_, Result<Option<T>, OrmError>>;
74    
75    /**
76     * Creates a new entity in the database.
77     */
78    fn create(&self, entity: T) -> RepoFuture<'_, Result<T, OrmError>>;
79    
80    /**
81     * Updates an existing entity by ID.
82     */
83    fn update_by_id(&self, id: T::Id, entity: T) -> RepoFuture<'_, Result<T, OrmError>>;
84    
85    /**
86     * Deletes an entity by ID.
87     */
88    fn delete_by_id(&self, id: T::Id) -> RepoFuture<'_, Result<(), OrmError>>;
89}
90
91/**
92 * OrmError
93 *
94 * Error types that can occur during ORM operations.
95 */
96#[derive(Debug, Error)]
97pub enum OrmError {
98    #[error("Database error: {0}")]
99    Db(#[from] DbError),
100    #[error("Repository configuration is incomplete: missing `{operation}` implementation")]
101    MissingOperation { operation: &'static str },
102}
103
104type FindAllHandler<T> =
105    Arc<dyn Fn(&Db) -> RepoFuture<'static, Result<Vec<T>, OrmError>> + Send + Sync>;
106type FindByIdHandler<T> = Arc<
107    dyn Fn(&Db, <T as EntityMeta>::Id) -> RepoFuture<'static, Result<Option<T>, OrmError>>
108        + Send
109        + Sync,
110>;
111type CreateHandler<T> =
112    Arc<dyn Fn(&Db, T) -> RepoFuture<'static, Result<T, OrmError>> + Send + Sync>;
113type UpdateByIdHandler<T> = Arc<
114    dyn Fn(&Db, <T as EntityMeta>::Id, T) -> RepoFuture<'static, Result<T, OrmError>> + Send + Sync,
115>;
116type DeleteByIdHandler<T> = Arc<
117    dyn Fn(&Db, <T as EntityMeta>::Id) -> RepoFuture<'static, Result<(), OrmError>> + Send + Sync,
118>;
119
120/**
121 * SqlRepo
122 *
123 * A generic SQL repository implementation.
124 * Provides CRUD operations backed by a SQL database.
125 *
126 * # Type Parameters
127 * - `T`: The entity type (must implement EntityMeta)
128 */
129pub struct SqlRepo<T>
130where
131    T: EntityMeta,
132{
133    db: Db,
134    find_all_handler: FindAllHandler<T>,
135    find_by_id_handler: FindByIdHandler<T>,
136    create_handler: CreateHandler<T>,
137    update_by_id_handler: UpdateByIdHandler<T>,
138    delete_by_id_handler: DeleteByIdHandler<T>,
139}
140
141impl<T> Clone for SqlRepo<T>
142where
143    T: EntityMeta,
144{
145    fn clone(&self) -> Self {
146        Self {
147            db: self.db.clone(),
148            find_all_handler: Arc::clone(&self.find_all_handler),
149            find_by_id_handler: Arc::clone(&self.find_by_id_handler),
150            create_handler: Arc::clone(&self.create_handler),
151            update_by_id_handler: Arc::clone(&self.update_by_id_handler),
152            delete_by_id_handler: Arc::clone(&self.delete_by_id_handler),
153        }
154    }
155}
156
157impl<T> Repo<T> for SqlRepo<T>
158where
159    T: EntityMeta,
160{
161    fn find_all(&self) -> RepoFuture<'_, Result<Vec<T>, OrmError>> {
162        (self.find_all_handler)(&self.db)
163    }
164
165    fn find_by_id(&self, id: T::Id) -> RepoFuture<'_, Result<Option<T>, OrmError>> {
166        (self.find_by_id_handler)(&self.db, id)
167    }
168
169    fn create(&self, entity: T) -> RepoFuture<'_, Result<T, OrmError>> {
170        (self.create_handler)(&self.db, entity)
171    }
172
173    fn update_by_id(&self, id: T::Id, entity: T) -> RepoFuture<'_, Result<T, OrmError>> {
174        (self.update_by_id_handler)(&self.db, id, entity)
175    }
176
177    fn delete_by_id(&self, id: T::Id) -> RepoFuture<'_, Result<(), OrmError>> {
178        (self.delete_by_id_handler)(&self.db, id)
179    }
180}
181
182#[derive(Clone)]
183pub struct SqlRepoBuilder<T>
184where
185    T: EntityMeta,
186{
187    db: Db,
188    find_all_handler: Option<FindAllHandler<T>>,
189    find_by_id_handler: Option<FindByIdHandler<T>>,
190    create_handler: Option<CreateHandler<T>>,
191    update_by_id_handler: Option<UpdateByIdHandler<T>>,
192    delete_by_id_handler: Option<DeleteByIdHandler<T>>,
193}
194
195impl<T> SqlRepoBuilder<T>
196where
197    T: EntityMeta,
198{
199    pub fn new(db: Db) -> Self {
200        Self {
201            db,
202            find_all_handler: None,
203            find_by_id_handler: None,
204            create_handler: None,
205            update_by_id_handler: None,
206            delete_by_id_handler: None,
207        }
208    }
209
210    pub fn with_find_all<F>(mut self, handler: F) -> Self
211    where
212        F: Fn(&Db) -> RepoFuture<'static, Result<Vec<T>, OrmError>> + Send + Sync + 'static,
213    {
214        self.find_all_handler = Some(Arc::new(handler));
215        self
216    }
217
218    pub fn with_find_by_id<F>(mut self, handler: F) -> Self
219    where
220        F: Fn(&Db, T::Id) -> RepoFuture<'static, Result<Option<T>, OrmError>>
221            + Send
222            + Sync
223            + 'static,
224    {
225        self.find_by_id_handler = Some(Arc::new(handler));
226        self
227    }
228
229    pub fn with_create<F>(mut self, handler: F) -> Self
230    where
231        F: Fn(&Db, T) -> RepoFuture<'static, Result<T, OrmError>> + Send + Sync + 'static,
232    {
233        self.create_handler = Some(Arc::new(handler));
234        self
235    }
236
237    pub fn with_update_by_id<F>(mut self, handler: F) -> Self
238    where
239        F: Fn(&Db, T::Id, T) -> RepoFuture<'static, Result<T, OrmError>> + Send + Sync + 'static,
240    {
241        self.update_by_id_handler = Some(Arc::new(handler));
242        self
243    }
244
245    pub fn with_delete_by_id<F>(mut self, handler: F) -> Self
246    where
247        F: Fn(&Db, T::Id) -> RepoFuture<'static, Result<(), OrmError>> + Send + Sync + 'static,
248    {
249        self.delete_by_id_handler = Some(Arc::new(handler));
250        self
251    }
252
253    pub fn build(self) -> Result<SqlRepo<T>, OrmError> {
254        Ok(SqlRepo {
255            db: self.db,
256            find_all_handler: self.find_all_handler.ok_or(OrmError::MissingOperation {
257                operation: "find_all",
258            })?,
259            find_by_id_handler: self.find_by_id_handler.ok_or(OrmError::MissingOperation {
260                operation: "find_by_id",
261            })?,
262            create_handler: self.create_handler.ok_or(OrmError::MissingOperation {
263                operation: "create",
264            })?,
265            update_by_id_handler: self
266                .update_by_id_handler
267                .ok_or(OrmError::MissingOperation {
268                    operation: "update_by_id",
269                })?,
270            delete_by_id_handler: self
271                .delete_by_id_handler
272                .ok_or(OrmError::MissingOperation {
273                    operation: "delete_by_id",
274                })?,
275        })
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use nestforge_db::DbConfig;
283
284    #[derive(Clone, Debug, PartialEq, Eq)]
285    struct User {
286        id: i64,
287        name: String,
288    }
289
290    impl EntityMeta for User {
291        type Id = i64;
292
293        fn table_name() -> &'static str {
294            "users"
295        }
296
297        fn id_value(&self) -> &Self::Id {
298            &self.id
299        }
300    }
301
302    #[tokio::test]
303    async fn builder_requires_all_handlers() {
304        let db = Db::connect_lazy(DbConfig::postgres_local("postgres")).expect("lazy db");
305        let err = match SqlRepoBuilder::<User>::new(db).build() {
306            Ok(_) => panic!("incomplete builder should fail"),
307            Err(err) => err,
308        };
309
310        assert!(matches!(err, OrmError::MissingOperation { .. }));
311    }
312
313    #[tokio::test]
314    async fn repo_delegates_to_configured_handlers() {
315        let db = Db::connect_lazy(DbConfig::postgres_local("postgres")).expect("lazy db");
316        let repo = SqlRepoBuilder::<User>::new(db)
317            .with_find_all(|_| {
318                Box::pin(async {
319                    Ok(vec![User {
320                        id: 1,
321                        name: "Vernon".to_string(),
322                    }])
323                })
324            })
325            .with_find_by_id(|_, id| {
326                Box::pin(async move {
327                    Ok(Some(User {
328                        id,
329                        name: "Vernon".to_string(),
330                    }))
331                })
332            })
333            .with_create(|_, entity| Box::pin(async move { Ok(entity) }))
334            .with_update_by_id(|_, id, mut entity| {
335                Box::pin(async move {
336                    entity.id = id;
337                    Ok(entity)
338                })
339            })
340            .with_delete_by_id(|_, _| Box::pin(async { Ok(()) }))
341            .build()
342            .expect("builder should succeed");
343
344        let all = repo.find_all().await.expect("find_all");
345        assert_eq!(all.len(), 1);
346
347        let one = repo.find_by_id(1).await.expect("find_by_id");
348        assert!(one.is_some());
349    }
350}