1use super::file_repository::FileRepository;
11use super::git_repository::GitRepository;
12use super::repository::{BacklogItemRepository, SprintRepository};
13#[cfg(feature = "sqlite")]
14use super::sqlite_repository::SqliteRepository;
15use crate::backlog::{BacklogItem, ItemId};
16use crate::config::StorageBackend;
17use crate::error::{Error, Result};
18use crate::sprint::{Sprint, SprintId};
19use std::path::PathBuf;
20
21#[derive(Debug, Clone)]
23pub enum Backend {
24 File(FileRepository),
26 Git(GitRepository),
28 #[cfg(feature = "sqlite")]
30 Sqlite(SqliteRepository),
31}
32
33impl Backend {
34 pub(crate) async fn save_item_batch(&self, items: &[BacklogItem]) -> Result<()> {
37 match self {
38 Backend::File(repository) => repository.save_batch(items).await,
39 Backend::Git(repository) => {
40 for item in items {
41 BacklogItemRepository::save(repository, item).await?;
42 }
43 Ok(())
44 }
45 #[cfg(feature = "sqlite")]
46 Backend::Sqlite(repository) => {
47 for item in items {
48 BacklogItemRepository::save(repository, item).await?;
49 }
50 Ok(())
51 }
52 }
53 }
54
55 pub async fn open(root: impl Into<PathBuf>, backend: StorageBackend) -> Result<Self> {
60 let root = root.into();
61 match backend {
62 StorageBackend::File => Ok(Backend::File(FileRepository::new(root))),
63 StorageBackend::Git => Ok(Backend::Git(GitRepository::new(root))),
64 #[cfg(feature = "sqlite")]
65 StorageBackend::Sqlite => Ok(Backend::Sqlite(SqliteRepository::new(root))),
66 }
67 }
68
69 pub(crate) async fn open_for_write(
71 root: impl Into<PathBuf>,
72 backend: StorageBackend,
73 ) -> Result<Self> {
74 let root = root.into();
75 match backend {
76 StorageBackend::File => Ok(Backend::File(FileRepository::new(root))),
77 StorageBackend::Git => Ok(Backend::Git(GitRepository::new(root).prepare().await?)),
78 #[cfg(feature = "sqlite")]
79 StorageBackend::Sqlite => Ok(Backend::Sqlite(SqliteRepository::new(root))),
80 }
81 }
82
83 pub(crate) async fn commit(&self, message: &str) -> Result<()> {
86 match self {
87 Backend::File(_) => Ok(()),
88 Backend::Git(repository) => repository.commit(message).await,
89 #[cfg(feature = "sqlite")]
90 Backend::Sqlite(_) => Ok(()),
91 }
92 }
93
94 pub(crate) async fn undo(&self) -> Result<String> {
100 match self {
101 Backend::File(_) => Err(Error::UndoUnsupported {
102 backend: "file".to_string(),
103 }),
104 Backend::Git(repository) => repository.undo_last().await,
105 #[cfg(feature = "sqlite")]
106 Backend::Sqlite(_) => Err(Error::UndoUnsupported {
107 backend: "sqlite".to_string(),
108 }),
109 }
110 }
111}
112
113impl BacklogItemRepository for Backend {
114 async fn save(&self, item: &BacklogItem) -> Result<()> {
115 match self {
116 Backend::File(r) => BacklogItemRepository::save(r, item).await,
117 Backend::Git(r) => BacklogItemRepository::save(r, item).await,
118 #[cfg(feature = "sqlite")]
119 Backend::Sqlite(r) => BacklogItemRepository::save(r, item).await,
120 }
121 }
122
123 async fn load(&self, id: &ItemId) -> Result<BacklogItem> {
124 match self {
125 Backend::File(r) => BacklogItemRepository::load(r, id).await,
126 Backend::Git(r) => BacklogItemRepository::load(r, id).await,
127 #[cfg(feature = "sqlite")]
128 Backend::Sqlite(r) => BacklogItemRepository::load(r, id).await,
129 }
130 }
131
132 async fn list(&self) -> Result<Vec<BacklogItem>> {
133 match self {
134 Backend::File(r) => BacklogItemRepository::list(r).await,
135 Backend::Git(r) => BacklogItemRepository::list(r).await,
136 #[cfg(feature = "sqlite")]
137 Backend::Sqlite(r) => BacklogItemRepository::list(r).await,
138 }
139 }
140
141 async fn list_archived(&self) -> Result<Vec<BacklogItem>> {
142 match self {
143 Backend::File(r) => BacklogItemRepository::list_archived(r).await,
144 Backend::Git(r) => BacklogItemRepository::list_archived(r).await,
145 #[cfg(feature = "sqlite")]
146 Backend::Sqlite(r) => BacklogItemRepository::list_archived(r).await,
147 }
148 }
149
150 async fn load_archived(&self, id: &ItemId) -> Result<BacklogItem> {
151 match self {
152 Backend::File(r) => BacklogItemRepository::load_archived(r, id).await,
153 Backend::Git(r) => BacklogItemRepository::load_archived(r, id).await,
154 #[cfg(feature = "sqlite")]
155 Backend::Sqlite(r) => BacklogItemRepository::load_archived(r, id).await,
156 }
157 }
158
159 async fn delete(&self, id: &ItemId) -> Result<()> {
160 match self {
161 Backend::File(r) => BacklogItemRepository::delete(r, id).await,
162 Backend::Git(r) => BacklogItemRepository::delete(r, id).await,
163 #[cfg(feature = "sqlite")]
164 Backend::Sqlite(r) => BacklogItemRepository::delete(r, id).await,
165 }
166 }
167
168 async fn archive(&self, id: &ItemId) -> Result<PathBuf> {
169 match self {
170 Backend::File(r) => r.archive(id).await,
171 Backend::Git(r) => r.archive(id).await,
172 #[cfg(feature = "sqlite")]
173 Backend::Sqlite(r) => r.archive(id).await,
174 }
175 }
176
177 async fn restore(&self, id: &ItemId) -> Result<()> {
178 match self {
179 Backend::File(r) => BacklogItemRepository::restore(r, id).await,
180 Backend::Git(r) => BacklogItemRepository::restore(r, id).await,
181 #[cfg(feature = "sqlite")]
182 Backend::Sqlite(r) => BacklogItemRepository::restore(r, id).await,
183 }
184 }
185
186 async fn next_id(&self, prefix: &str) -> Result<ItemId> {
187 match self {
188 Backend::File(r) => r.next_id(prefix).await,
189 Backend::Git(r) => r.next_id(prefix).await,
190 #[cfg(feature = "sqlite")]
191 Backend::Sqlite(r) => r.next_id(prefix).await,
192 }
193 }
194}
195
196impl SprintRepository for Backend {
197 async fn save(&self, sprint: &Sprint) -> Result<()> {
198 match self {
199 Backend::File(r) => SprintRepository::save(r, sprint).await,
200 Backend::Git(r) => SprintRepository::save(r, sprint).await,
201 #[cfg(feature = "sqlite")]
202 Backend::Sqlite(r) => SprintRepository::save(r, sprint).await,
203 }
204 }
205
206 async fn load(&self, id: &SprintId) -> Result<Sprint> {
207 match self {
208 Backend::File(r) => SprintRepository::load(r, id).await,
209 Backend::Git(r) => SprintRepository::load(r, id).await,
210 #[cfg(feature = "sqlite")]
211 Backend::Sqlite(r) => SprintRepository::load(r, id).await,
212 }
213 }
214
215 async fn list(&self) -> Result<Vec<Sprint>> {
216 match self {
217 Backend::File(r) => SprintRepository::list(r).await,
218 Backend::Git(r) => SprintRepository::list(r).await,
219 #[cfg(feature = "sqlite")]
220 Backend::Sqlite(r) => SprintRepository::list(r).await,
221 }
222 }
223
224 async fn delete(&self, id: &SprintId) -> Result<()> {
225 match self {
226 Backend::File(r) => SprintRepository::delete(r, id).await,
227 Backend::Git(r) => SprintRepository::delete(r, id).await,
228 #[cfg(feature = "sqlite")]
229 Backend::Sqlite(r) => SprintRepository::delete(r, id).await,
230 }
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::backlog::Status;
238 use crate::rank::Rank;
239 use chrono::{TimeZone, Utc};
240 use tempfile::TempDir;
241
242 #[tokio::test]
244 async fn file_backend_dispatches_save_and_load() {
245 let dir = TempDir::new().expect("temp dir");
246 let backend = Backend::open(dir.path().join(".pinto"), StorageBackend::File)
247 .await
248 .expect("open file backend");
249
250 let item = BacklogItem::new(
251 ItemId::new("T", 1),
252 "Dispatch",
253 Status::new("todo"),
254 Rank::after(None),
255 Utc.timestamp_opt(1_000, 0).single().unwrap(),
256 )
257 .expect("valid item");
258
259 BacklogItemRepository::save(&backend, &item)
260 .await
261 .expect("save via backend");
262 let loaded = BacklogItemRepository::load(&backend, &item.id)
263 .await
264 .expect("load via backend");
265 assert_eq!(loaded, item);
266 }
267
268 #[cfg(feature = "sqlite")]
270 #[tokio::test]
271 async fn sqlite_backend_dispatches_save_and_load() {
272 let dir = TempDir::new().expect("temp dir");
273 let backend = Backend::open(dir.path().join(".pinto"), StorageBackend::Sqlite)
274 .await
275 .expect("open sqlite backend");
276
277 let item = BacklogItem::new(
278 ItemId::new("T", 1),
279 "Dispatch",
280 Status::new("todo"),
281 Rank::after(None),
282 Utc.timestamp_opt(1_000, 0).single().unwrap(),
283 )
284 .expect("valid item");
285
286 BacklogItemRepository::save(&backend, &item)
287 .await
288 .expect("save via backend");
289 let loaded = BacklogItemRepository::load(&backend, &item.id)
290 .await
291 .expect("load via backend");
292 assert_eq!(loaded, item);
293 }
294
295 #[tokio::test]
296 async fn save_item_batch_dispatches_to_git_and_sqlite_backends() {
297 let item = BacklogItem::new(
298 ItemId::new("T", 1),
299 "Batch",
300 Status::new("todo"),
301 Rank::after(None),
302 Utc.timestamp_opt(1_000, 0).single().unwrap(),
303 )
304 .expect("valid item");
305
306 let git_dir = TempDir::new().expect("git temp dir");
307 let git = Backend::Git(GitRepository::new(git_dir.path().join(".pinto")));
308 git.save_item_batch(std::slice::from_ref(&item))
309 .await
310 .expect("git batch save");
311 assert_eq!(
312 BacklogItemRepository::list(&git).await.unwrap(),
313 vec![item.clone()]
314 );
315
316 #[cfg(feature = "sqlite")]
317 {
318 let sqlite_dir = TempDir::new().expect("sqlite temp dir");
319 let sqlite = Backend::Sqlite(SqliteRepository::new(sqlite_dir.path().join(".pinto")));
320 sqlite
321 .save_item_batch(std::slice::from_ref(&item))
322 .await
323 .expect("sqlite batch save");
324 assert_eq!(
325 BacklogItemRepository::list(&sqlite).await.unwrap(),
326 vec![item]
327 );
328 }
329 }
330
331 #[tokio::test]
332 async fn historyless_backends_report_undo_unsupported() {
333 let file_dir = TempDir::new().expect("file temp dir");
334 assert!(matches!(
335 Backend::File(FileRepository::new(file_dir.path().join(".pinto")))
336 .undo()
337 .await,
338 Err(Error::UndoUnsupported { backend }) if backend == "file"
339 ));
340
341 #[cfg(feature = "sqlite")]
342 {
343 let sqlite_dir = TempDir::new().expect("sqlite temp dir");
344 assert!(matches!(
345 Backend::Sqlite(SqliteRepository::new(sqlite_dir.path().join(".pinto")))
346 .undo()
347 .await,
348 Err(Error::UndoUnsupported { backend }) if backend == "sqlite"
349 ));
350 }
351 }
352}