synd_term/application/cache/
mod.rs1use std::{
2 borrow::Borrow,
3 io,
4 path::{Path, PathBuf},
5};
6
7use serde::{de::DeserializeOwned, Serialize};
8use synd_stdx::fs::{fsimpl, FileSystem};
9use thiserror::Error;
10
11use crate::{
12 auth::{Credential, Unverified},
13 config,
14 ui::components::gh_notifications::GhNotificationFilterOptions,
15};
16
17#[derive(Debug, Error)]
18pub enum PersistCacheError {
19 #[error("io error: {path} {io} ")]
20 Io { path: PathBuf, io: io::Error },
21 #[error("serialize error: {0}")]
22 Serialize(#[from] serde_json::Error),
23}
24
25#[derive(Debug, Error)]
26pub enum LoadCacheError {
27 #[error("cache entry not found")]
28 NotFound,
29 #[error("io error: {path} {io}")]
30 Io { path: PathBuf, io: io::Error },
31 #[error("deserialize error: {0}")]
32 Deserialize(#[from] serde_json::Error),
33}
34
35pub struct Cache<FS = fsimpl::FileSystem> {
36 dir: PathBuf,
37 fs: FS,
38}
39
40impl Cache<fsimpl::FileSystem> {
41 pub fn new(dir: impl Into<PathBuf>) -> Self {
42 Self::with(dir, fsimpl::FileSystem::new())
43 }
44}
45
46impl<FS> Cache<FS>
47where
48 FS: FileSystem,
49{
50 pub fn with(dir: impl Into<PathBuf>, fs: FS) -> Self {
51 Self {
52 dir: dir.into(),
53 fs,
54 }
55 }
56
57 pub fn persist_credential(
60 &self,
61 cred: impl Borrow<Credential>,
62 ) -> Result<(), PersistCacheError> {
63 self.persist(&self.credential_file(), cred.borrow())
64 }
65
66 pub(crate) fn persist_gh_notification_filter_options(
67 &self,
68 options: impl Borrow<GhNotificationFilterOptions>,
69 ) -> Result<(), PersistCacheError> {
70 self.persist(&self.gh_notification_filter_option_file(), options.borrow())
71 }
72
73 fn persist<T>(&self, path: &Path, entry: &T) -> Result<(), PersistCacheError>
74 where
75 T: ?Sized + Serialize,
76 {
77 if let Some(parent) = path.parent() {
78 self.fs
79 .create_dir_all(parent)
80 .map_err(|err| PersistCacheError::Io {
81 path: parent.to_path_buf(),
82 io: err,
83 })?;
84 }
85
86 self.fs
87 .create_file(path)
88 .map_err(|err| PersistCacheError::Io {
89 path: path.to_path_buf(),
90 io: err,
91 })
92 .and_then(|mut file| {
93 serde_json::to_writer(&mut file, entry).map_err(PersistCacheError::Serialize)
94 })
95 }
96
97 pub fn load_credential(&self) -> Result<Unverified<Credential>, LoadCacheError> {
100 self.load::<Credential>(&self.credential_file())
101 .map(Unverified::from)
102 }
103
104 pub(crate) fn load_gh_notification_filter_options(
105 &self,
106 ) -> Result<GhNotificationFilterOptions, LoadCacheError> {
107 self.load(&self.gh_notification_filter_option_file())
108 }
109
110 fn load<T>(&self, path: &Path) -> Result<T, LoadCacheError>
111 where
112 T: DeserializeOwned,
113 {
114 self.fs
115 .open_file(path)
116 .map_err(|err| LoadCacheError::Io {
117 io: err,
118 path: path.to_path_buf(),
119 })
120 .and_then(|mut file| {
121 serde_json::from_reader::<_, T>(&mut file).map_err(LoadCacheError::Deserialize)
122 })
123 }
124
125 fn credential_file(&self) -> PathBuf {
126 self.dir.join(config::cache::CREDENTIAL_FILE)
127 }
128
129 fn gh_notification_filter_option_file(&self) -> PathBuf {
130 self.dir
131 .join(config::cache::GH_NOTIFICATION_FILTER_OPTION_FILE)
132 }
133
134 pub(crate) fn clean(&self) -> io::Result<()> {
136 match self.fs.remove_file(self.credential_file()) {
139 Ok(()) => Ok(()),
140 Err(err) => match err.kind() {
141 io::ErrorKind::NotFound => Ok(()),
142 _ => Err(err),
143 },
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150
151 use crate::auth::Credential;
152
153 use super::*;
154
155 #[test]
156 fn persist_then_load_credential() {
157 let tmp = temp_dir();
158 let cache = Cache::new(tmp);
159 let cred = Credential::Github {
160 access_token: "rust is fun".into(),
161 };
162 assert!(cache.persist_credential(&cred).is_ok());
163
164 let loaded = cache.load_credential().unwrap();
165 assert_eq!(loaded, Unverified::from(cred),);
166 }
167
168 #[test]
169 fn filesystem_error() {
170 let cache = Cache::new("/dev/null");
171 assert!(cache
172 .persist_credential(Credential::Github {
173 access_token: "dummy".into(),
174 })
175 .is_err());
176 }
177
178 fn temp_dir() -> PathBuf {
179 tempfile::TempDir::new().unwrap().into_path()
180 }
181}