phab_lib/storage/
storage_fs.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::Path;
4use std::path::PathBuf;
5
6use anyhow::Error;
7use slugify::slugify;
8use thiserror::Error;
9
10use crate::dto::Task;
11use crate::dto::Watchlist;
12use crate::storage::storage::PhabStorage;
13use crate::types::ResultAnyError;
14
15type Table = HashMap<String, Watchlist>;
16type FileDB = HashMap<String, Table>;
17
18pub struct PhabStorageFilesystem {
19  pub filepath: PathBuf,
20  db_content: FileDB,
21}
22
23impl PhabStorageFilesystem {
24  pub fn new(filepath: impl AsRef<Path>) -> ResultAnyError<PhabStorageFilesystem> {
25    let mut storage = PhabStorageFilesystem {
26      db_content: HashMap::new(),
27      filepath: PathBuf::from(filepath.as_ref()),
28    };
29
30    storage.reload()?;
31
32    return Ok(storage);
33  }
34}
35
36impl PhabStorageFilesystem {
37  fn watchlist_table(&mut self) -> &mut Table {
38    return self
39      .db_content
40      .entry("watchlists".to_owned())
41      .or_insert(HashMap::new());
42  }
43
44  fn reload(&mut self) -> ResultAnyError<()> {
45    if !self.filepath.exists() {
46      let _ = self.watchlist_table();
47
48      self.persist()?;
49    }
50
51    let str = fs::read_to_string(&self.filepath)?;
52    self.db_content = serde_json::from_str(&str)?;
53
54    return Ok(());
55  }
56
57  fn persist(&self) -> ResultAnyError<()> {
58    // Create db file if not exist
59    if !self.filepath.exists() {
60      let mut cloned_filepath = self.filepath.clone();
61      cloned_filepath.pop();
62
63      fs::create_dir_all(&cloned_filepath)?;
64    }
65
66    // Write all db content to db file
67    let content = serde_json::to_string(&self.db_content)?;
68    fs::write(&self.filepath, content)?;
69
70    return Ok(());
71  }
72}
73
74#[derive(Debug, Error)]
75enum PhabStorageFilesystemError {
76  #[error("PhabStorageFilesystemError err: {message:?}")]
77  QueryError { message: String },
78}
79
80impl PhabStorageFilesystemError {
81  fn query_error(message: &str) -> Error {
82    return PhabStorageFilesystemError::QueryError {
83      message: message.to_owned(),
84    }
85    .into();
86  }
87}
88
89impl PhabStorage for PhabStorageFilesystem {
90  fn add_to_watchlist(&mut self, watchlist_id: &str, task: &Task) -> ResultAnyError<()> {
91    self
92      .watchlist_table()
93      .get_mut(watchlist_id)
94      .unwrap()
95      .tasks
96      .push(task.clone());
97
98    self.persist()?;
99
100    return Ok(());
101  }
102
103  fn create_watchlist(&mut self, watchlist: &Watchlist) -> ResultAnyError<Watchlist> {
104    let watchlists = self.db_content.get_mut("watchlists").unwrap();
105    let watchlist_id = slugify!(&watchlist.name);
106    let mut watchlist = watchlist.clone();
107
108    watchlist.id = Some(watchlist_id.clone());
109    watchlists.insert(watchlist_id, watchlist.to_owned());
110
111    self.persist()?;
112
113    return Ok(watchlist);
114  }
115
116  fn get_watchlists(&mut self) -> ResultAnyError<Vec<Watchlist>> {
117    let watchlists: Vec<Watchlist> = self.watchlist_table().values().cloned().collect();
118
119    return Ok(watchlists);
120  }
121
122  fn get_watchlist_by_id(&mut self, watchlist_id: &str) -> ResultAnyError<Option<Watchlist>> {
123    let watchlist = self.watchlist_table().get(watchlist_id).map(Clone::clone);
124
125    return Ok(watchlist);
126  }
127}
128
129#[cfg(test)]
130mod test {
131  use super::*;
132  use std::fs;
133
134  struct DirCleaner {
135    dir: PathBuf,
136  }
137
138  impl Drop for DirCleaner {
139    fn drop(&mut self) {
140      if self.dir.exists() {
141        fs::remove_dir_all(&self.dir).unwrap();
142      }
143    }
144  }
145
146  mod reload {
147    use super::*;
148    use fake::Fake;
149    use fake::Faker;
150
151    fn create_new(db_dir_path: PathBuf) -> ResultAnyError<PhabStorageFilesystem> {
152      let mut storage = PhabStorageFilesystem {
153        db_content: HashMap::new(),
154        filepath: PathBuf::from(format!(
155          "{}/{}",
156          db_dir_path.into_os_string().into_string().unwrap(),
157          "yo.json"
158        )),
159      };
160
161      storage.reload()?;
162
163      return Ok(storage);
164    }
165
166    fn test_db_dir(fn_name: &str) -> PathBuf {
167      return PathBuf::from(format!("/tmp/__phab_for_testing/db_{}", fn_name));
168    }
169
170    #[test]
171    fn it_should_create_dir_and_load_data_for_first_time() -> ResultAnyError<()> {
172      let db_dir_path = test_db_dir(function_name!());
173      let _dir_cleaner = DirCleaner {
174        dir: db_dir_path.clone(),
175      };
176      let mut storage = test::reload::create_new(db_dir_path)?;
177
178      let watchlists = storage.get_watchlists()?;
179
180      assert_eq!(watchlists.len(), 0);
181
182      return Ok(());
183    }
184
185    #[test]
186    fn it_should_insert_data() -> ResultAnyError<()> {
187      let db_dir_path = test_db_dir(function_name!());
188      let _dir_cleaner = DirCleaner {
189        dir: db_dir_path.clone(),
190      };
191      let mut storage = test::reload::create_new(db_dir_path)?;
192      let watchlist = Watchlist {
193        id: None,
194        name: String::from("hey ho test watchlist"),
195        tasks: vec![],
196      };
197
198      storage.create_watchlist(&watchlist)?;
199      let watchlists = storage.get_watchlists()?;
200
201      assert_eq!(watchlists.len(), 1);
202      assert_eq!(
203        watchlists.get(0).unwrap().id.as_ref().unwrap(),
204        "hey-ho-test-watchlist"
205      );
206
207      // Now we test reloading data, it should be the same.
208      storage.reload()?;
209      assert_eq!(watchlists.len(), 1);
210      assert_eq!(
211        watchlists.get(0).unwrap().id.as_ref().unwrap(),
212        "hey-ho-test-watchlist"
213      );
214
215      return Ok(());
216    }
217
218    #[test]
219    fn it_should_add_to_watchlist() -> ResultAnyError<()> {
220      let db_dir_path = test_db_dir(function_name!());
221      let _dir_cleaner = DirCleaner {
222        dir: db_dir_path.clone(),
223      };
224      let mut storage = test::reload::create_new(db_dir_path)?;
225      let watchlist = Watchlist {
226        id: None,
227        name: String::from("hey ho test watchlist"),
228        tasks: vec![],
229      };
230
231      let mut task_1: Task = Faker.fake();
232      task_1.id = "foo".to_owned();
233
234      let mut task_2: Task = Faker.fake();
235      task_2.id = "Bar".to_owned();
236
237      let watchlist = storage.create_watchlist(&watchlist)?;
238      let watchlist_id = watchlist.id.unwrap();
239      storage.add_to_watchlist(&watchlist_id, &task_1)?;
240      storage.add_to_watchlist(&watchlist_id, &task_2)?;
241
242      let watchlist = storage.get_watchlist_by_id(&watchlist_id)?;
243
244      assert!(watchlist.is_some());
245
246      let tasks: Vec<Task> = watchlist.unwrap().tasks;
247
248      assert_eq!(tasks.len(), 2);
249      assert_eq!(tasks.get(0).unwrap().id, "foo");
250      assert_eq!(tasks.get(1).unwrap().id, "Bar");
251
252      return Ok(());
253    }
254  }
255}