Skip to main content

acari_lib/
cached_client.rs

1use crate::error::AcariError;
2use crate::model::{Account, Customer, Minutes, Project, Service, TimeEntry, Tracker, User};
3use crate::model::{ProjectId, ServiceId, TimeEntryId};
4use crate::query::{DateSpan, Day};
5use crate::Client;
6use serde::de::DeserializeOwned;
7use serde::ser::Serialize;
8use std::io;
9use std::path::PathBuf;
10use std::time::Duration;
11use std::{
12  fs::{self, File},
13  path::Path,
14};
15
16pub fn clear_cache() -> Result<(), AcariError> {
17  let cache_dir = cache_dir();
18
19  fs::remove_dir_all(cache_dir)?;
20
21  Ok(())
22}
23
24#[derive(Debug)]
25pub struct CachedClient<C> {
26  client: C,
27  cache_dir: PathBuf,
28  cache_ttl: Duration,
29}
30
31impl<C> CachedClient<C>
32where
33  C: Client,
34{
35  pub fn new(client: C, cache_ttl: Duration) -> Result<CachedClient<C>, AcariError> {
36    let cache_dir = cache_dir().join(client.get_domain());
37
38    fs::create_dir_all(&cache_dir)?;
39
40    Ok(CachedClient { client, cache_dir, cache_ttl })
41  }
42
43  fn cache_data<T, F>(&self, cache_name: &str, fetch_data: F) -> Result<T, AcariError>
44  where
45    T: DeserializeOwned + Serialize,
46    F: FnOnce() -> Result<T, AcariError>,
47  {
48    let cache_file = self.cache_dir.join(cache_name);
49    let cache_valid = file_age(&cache_file)?.map(|age| age < self.cache_ttl).unwrap_or(false);
50
51    if cache_valid {
52      Ok(serde_json::from_reader(File::open(cache_file)?)?)
53    } else {
54      match fetch_data() {
55        Ok(data) => {
56          serde_json::to_writer(File::create(cache_file)?, &data)?;
57          Ok(data)
58        }
59        err => err,
60      }
61    }
62  }
63}
64
65impl<C> Client for CachedClient<C>
66where
67  C: Client,
68{
69  fn get_domain(&self) -> String {
70    self.client.get_domain()
71  }
72
73  fn get_account(&self) -> Result<Account, AcariError> {
74    self.cache_data("account.json", || self.client.get_account())
75  }
76
77  fn get_myself(&self) -> Result<User, AcariError> {
78    self.cache_data("user.json", || self.client.get_myself())
79  }
80
81  fn get_customers(&self) -> Result<Vec<Customer>, AcariError> {
82    self.cache_data("customers.json", || self.client.get_customers())
83  }
84
85  fn get_projects(&self) -> Result<Vec<Project>, AcariError> {
86    self.cache_data("projects.json", || self.client.get_projects())
87  }
88
89  fn get_services(&self, project_id: &ProjectId) -> Result<Vec<Service>, AcariError> {
90    self.cache_data(&format!("services-{}.json", project_id), || self.client.get_services(project_id))
91  }
92
93  fn get_time_entries(&self, date_span: DateSpan) -> Result<Vec<TimeEntry>, AcariError> {
94    self.client.get_time_entries(date_span) // This should not be cached
95  }
96
97  fn create_time_entry(
98    &self,
99    day: Day,
100    project_id: &ProjectId,
101    service_id: &ServiceId,
102    minutes: Minutes,
103    note: Option<String>,
104  ) -> Result<TimeEntry, AcariError> {
105    self.client.create_time_entry(day, project_id, service_id, minutes, note)
106  }
107
108  fn update_time_entry(&self, entry_id: &TimeEntryId, minutes: Minutes, note: Option<String>) -> Result<(), AcariError> {
109    self.client.update_time_entry(entry_id, minutes, note)
110  }
111
112  fn delete_time_entry(&self, entry_id: &TimeEntryId) -> Result<(), AcariError> {
113    self.client.delete_time_entry(entry_id)
114  }
115
116  fn get_tracker(&self) -> Result<Tracker, AcariError> {
117    self.client.get_tracker() // This should not be cached
118  }
119
120  fn create_tracker(&self, entry_id: &TimeEntryId) -> Result<Tracker, AcariError> {
121    self.client.create_tracker(entry_id)
122  }
123
124  fn delete_tracker(&self, entry_id: &TimeEntryId) -> Result<Tracker, AcariError> {
125    self.client.delete_tracker(entry_id)
126  }
127}
128
129fn file_age(path: &Path) -> Result<Option<Duration>, AcariError> {
130  match fs::metadata(path) {
131    Ok(metadata) => Ok(Some(metadata.modified()?.elapsed()?)),
132    Err(ref err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
133    Err(err) => Err(err.into()),
134  }
135}
136
137fn cache_dir() -> PathBuf {
138  let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
139  dirs::cache_dir()
140    .map(|cache| cache.join("acari"))
141    .unwrap_or_else(|| home_dir.join(".acari_cache"))
142}