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}