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
6pub type RepoFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
7
8pub trait EntityMeta: Send + Sync + 'static {
9    type Id: Send + Sync + Clone + 'static;
10
11    fn table_name() -> &'static str;
12    fn id_column() -> &'static str {
13        "id"
14    }
15    fn id_value(&self) -> &Self::Id;
16}
17
18pub trait Repo<T>: Send + Sync
19where
20    T: EntityMeta,
21{
22    fn find_all(&self) -> RepoFuture<'_, Result<Vec<T>, OrmError>>;
23    fn find_by_id(&self, id: T::Id) -> RepoFuture<'_, Result<Option<T>, OrmError>>;
24    fn create(&self, entity: T) -> RepoFuture<'_, Result<T, OrmError>>;
25    fn update_by_id(&self, id: T::Id, entity: T) -> RepoFuture<'_, Result<T, OrmError>>;
26    fn delete_by_id(&self, id: T::Id) -> RepoFuture<'_, Result<(), OrmError>>;
27}
28
29#[derive(Debug, Error)]
30pub enum OrmError {
31    #[error("Database error: {0}")]
32    Db(#[from] DbError),
33    #[error("Repository configuration is incomplete: missing `{operation}` implementation")]
34    MissingOperation { operation: &'static str },
35}
36
37type FindAllHandler<T> =
38    Arc<dyn Fn(&Db) -> RepoFuture<'static, Result<Vec<T>, OrmError>> + Send + Sync>;
39type FindByIdHandler<T> = Arc<
40    dyn Fn(&Db, <T as EntityMeta>::Id) -> RepoFuture<'static, Result<Option<T>, OrmError>>
41        + Send
42        + Sync,
43>;
44type CreateHandler<T> =
45    Arc<dyn Fn(&Db, T) -> RepoFuture<'static, Result<T, OrmError>> + Send + Sync>;
46type UpdateByIdHandler<T> = Arc<
47    dyn Fn(&Db, <T as EntityMeta>::Id, T) -> RepoFuture<'static, Result<T, OrmError>> + Send + Sync,
48>;
49type DeleteByIdHandler<T> = Arc<
50    dyn Fn(&Db, <T as EntityMeta>::Id) -> RepoFuture<'static, Result<(), OrmError>> + Send + Sync,
51>;
52
53pub struct SqlRepo<T>
54where
55    T: EntityMeta,
56{
57    db: Db,
58    find_all_handler: FindAllHandler<T>,
59    find_by_id_handler: FindByIdHandler<T>,
60    create_handler: CreateHandler<T>,
61    update_by_id_handler: UpdateByIdHandler<T>,
62    delete_by_id_handler: DeleteByIdHandler<T>,
63}
64
65impl<T> Clone for SqlRepo<T>
66where
67    T: EntityMeta,
68{
69    fn clone(&self) -> Self {
70        Self {
71            db: self.db.clone(),
72            find_all_handler: Arc::clone(&self.find_all_handler),
73            find_by_id_handler: Arc::clone(&self.find_by_id_handler),
74            create_handler: Arc::clone(&self.create_handler),
75            update_by_id_handler: Arc::clone(&self.update_by_id_handler),
76            delete_by_id_handler: Arc::clone(&self.delete_by_id_handler),
77        }
78    }
79}
80
81impl<T> Repo<T> for SqlRepo<T>
82where
83    T: EntityMeta,
84{
85    fn find_all(&self) -> RepoFuture<'_, Result<Vec<T>, OrmError>> {
86        (self.find_all_handler)(&self.db)
87    }
88
89    fn find_by_id(&self, id: T::Id) -> RepoFuture<'_, Result<Option<T>, OrmError>> {
90        (self.find_by_id_handler)(&self.db, id)
91    }
92
93    fn create(&self, entity: T) -> RepoFuture<'_, Result<T, OrmError>> {
94        (self.create_handler)(&self.db, entity)
95    }
96
97    fn update_by_id(&self, id: T::Id, entity: T) -> RepoFuture<'_, Result<T, OrmError>> {
98        (self.update_by_id_handler)(&self.db, id, entity)
99    }
100
101    fn delete_by_id(&self, id: T::Id) -> RepoFuture<'_, Result<(), OrmError>> {
102        (self.delete_by_id_handler)(&self.db, id)
103    }
104}
105
106#[derive(Clone)]
107pub struct SqlRepoBuilder<T>
108where
109    T: EntityMeta,
110{
111    db: Db,
112    find_all_handler: Option<FindAllHandler<T>>,
113    find_by_id_handler: Option<FindByIdHandler<T>>,
114    create_handler: Option<CreateHandler<T>>,
115    update_by_id_handler: Option<UpdateByIdHandler<T>>,
116    delete_by_id_handler: Option<DeleteByIdHandler<T>>,
117}
118
119impl<T> SqlRepoBuilder<T>
120where
121    T: EntityMeta,
122{
123    pub fn new(db: Db) -> Self {
124        Self {
125            db,
126            find_all_handler: None,
127            find_by_id_handler: None,
128            create_handler: None,
129            update_by_id_handler: None,
130            delete_by_id_handler: None,
131        }
132    }
133
134    pub fn with_find_all<F>(mut self, handler: F) -> Self
135    where
136        F: Fn(&Db) -> RepoFuture<'static, Result<Vec<T>, OrmError>> + Send + Sync + 'static,
137    {
138        self.find_all_handler = Some(Arc::new(handler));
139        self
140    }
141
142    pub fn with_find_by_id<F>(mut self, handler: F) -> Self
143    where
144        F: Fn(&Db, T::Id) -> RepoFuture<'static, Result<Option<T>, OrmError>>
145            + Send
146            + Sync
147            + 'static,
148    {
149        self.find_by_id_handler = Some(Arc::new(handler));
150        self
151    }
152
153    pub fn with_create<F>(mut self, handler: F) -> Self
154    where
155        F: Fn(&Db, T) -> RepoFuture<'static, Result<T, OrmError>> + Send + Sync + 'static,
156    {
157        self.create_handler = Some(Arc::new(handler));
158        self
159    }
160
161    pub fn with_update_by_id<F>(mut self, handler: F) -> Self
162    where
163        F: Fn(&Db, T::Id, T) -> RepoFuture<'static, Result<T, OrmError>> + Send + Sync + 'static,
164    {
165        self.update_by_id_handler = Some(Arc::new(handler));
166        self
167    }
168
169    pub fn with_delete_by_id<F>(mut self, handler: F) -> Self
170    where
171        F: Fn(&Db, T::Id) -> RepoFuture<'static, Result<(), OrmError>> + Send + Sync + 'static,
172    {
173        self.delete_by_id_handler = Some(Arc::new(handler));
174        self
175    }
176
177    pub fn build(self) -> Result<SqlRepo<T>, OrmError> {
178        Ok(SqlRepo {
179            db: self.db,
180            find_all_handler: self.find_all_handler.ok_or(OrmError::MissingOperation {
181                operation: "find_all",
182            })?,
183            find_by_id_handler: self.find_by_id_handler.ok_or(OrmError::MissingOperation {
184                operation: "find_by_id",
185            })?,
186            create_handler: self.create_handler.ok_or(OrmError::MissingOperation {
187                operation: "create",
188            })?,
189            update_by_id_handler: self
190                .update_by_id_handler
191                .ok_or(OrmError::MissingOperation {
192                    operation: "update_by_id",
193                })?,
194            delete_by_id_handler: self
195                .delete_by_id_handler
196                .ok_or(OrmError::MissingOperation {
197                    operation: "delete_by_id",
198                })?,
199        })
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use nestforge_db::DbConfig;
207
208    #[derive(Clone, Debug, PartialEq, Eq)]
209    struct User {
210        id: i64,
211        name: String,
212    }
213
214    impl EntityMeta for User {
215        type Id = i64;
216
217        fn table_name() -> &'static str {
218            "users"
219        }
220
221        fn id_value(&self) -> &Self::Id {
222            &self.id
223        }
224    }
225
226    #[tokio::test]
227    async fn builder_requires_all_handlers() {
228        let db = Db::connect_lazy(DbConfig::postgres_local("postgres")).expect("lazy db");
229        let err = match SqlRepoBuilder::<User>::new(db).build() {
230            Ok(_) => panic!("incomplete builder should fail"),
231            Err(err) => err,
232        };
233
234        assert!(matches!(err, OrmError::MissingOperation { .. }));
235    }
236
237    #[tokio::test]
238    async fn repo_delegates_to_configured_handlers() {
239        let db = Db::connect_lazy(DbConfig::postgres_local("postgres")).expect("lazy db");
240        let repo = SqlRepoBuilder::<User>::new(db)
241            .with_find_all(|_| {
242                Box::pin(async {
243                    Ok(vec![User {
244                        id: 1,
245                        name: "Vernon".to_string(),
246                    }])
247                })
248            })
249            .with_find_by_id(|_, id| {
250                Box::pin(async move {
251                    Ok(Some(User {
252                        id,
253                        name: "Vernon".to_string(),
254                    }))
255                })
256            })
257            .with_create(|_, entity| Box::pin(async move { Ok(entity) }))
258            .with_update_by_id(|_, id, mut entity| {
259                Box::pin(async move {
260                    entity.id = id;
261                    Ok(entity)
262                })
263            })
264            .with_delete_by_id(|_, _| Box::pin(async { Ok(()) }))
265            .build()
266            .expect("builder should succeed");
267
268        let all = repo.find_all().await.expect("find_all");
269        assert_eq!(all.len(), 1);
270
271        let one = repo.find_by_id(1).await.expect("find_by_id");
272        assert!(one.is_some());
273    }
274}