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>>;
8
9pub trait EntityMeta: Send + Sync + 'static {
23 type Id: Send + Sync + Clone + 'static;
25
26 fn table_name() -> &'static str;
30
31 fn id_column() -> &'static str {
36 "id"
37 }
38
39 fn id_value(&self) -> &Self::Id;
43}
44
45pub trait Repo<T>: Send + Sync
62where
63 T: EntityMeta,
64{
65 fn find_all(&self) -> RepoFuture<'_, Result<Vec<T>, OrmError>>;
69
70 fn find_by_id(&self, id: T::Id) -> RepoFuture<'_, Result<Option<T>, OrmError>>;
74
75 fn create(&self, entity: T) -> RepoFuture<'_, Result<T, OrmError>>;
79
80 fn update_by_id(&self, id: T::Id, entity: T) -> RepoFuture<'_, Result<T, OrmError>>;
84
85 fn delete_by_id(&self, id: T::Id) -> RepoFuture<'_, Result<(), OrmError>>;
89}
90
91#[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
120pub 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}