1use std::sync::Mutex;
4use std::str::FromStr;
5use std::cell::OnceCell;
6
7use thiserror::Error;
8use serde::{Serialize, Deserialize};
9use diesel::prelude::*;
10#[expect(unused_imports, reason = "Used in docs.")]
11use diesel::query_builder::SqlQuery;
12
13use crate::util::*;
14
15diesel::table! {
16 cache (id) {
18 id -> Integer,
20 category -> Text,
22 key -> Text,
24 value -> Nullable<Text>,
26 }
27}
28
29pub const DB_INIT_COMMAND: &str = r#"CREATE TABLE cache (
31 id INTEGER NOT NULL PRIMARY KEY,
32 category TEXT NOT NULL,
33 "key" TEXT NOT NULL,
34 value TEXT,
35 UNIQUE(category, "key") ON CONFLICT REPLACE
36)"#;
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Queryable, Selectable)]
40#[diesel(table_name = cache)]
41pub struct CacheEntry {
42 pub id: i32,
44 pub category: String,
46 pub key: String,
48 pub value: Option<String>
50}
51
52#[derive(Debug, PartialEq, Eq, Serialize, Insertable)]
54#[diesel(table_name = cache)]
55pub struct NewCacheEntry<'a> {
56 pub category: &'a str,
58 pub key: &'a str,
60 pub value: Option<&'a str>
62}
63
64#[derive(Debug, Default)]
79pub struct Cache(pub Mutex<InnerCache>);
80
81impl Cache {
82 #[allow(dead_code, reason = "Public API.")]
84 pub fn new(path: CachePath) -> Self {
85 path.into()
86 }
87}
88
89impl From<InnerCache> for Cache {
90 fn from(value: InnerCache) -> Self {
91 Self(Mutex::new(value))
92 }
93}
94
95impl From<CachePath> for Cache {
96 fn from(value: CachePath) -> Self {
97 Cache::from(InnerCache::from(value))
98 }
99}
100
101#[derive(Default)]
117pub struct InnerCache {
118 path: CachePath,
120 connection: OnceCell<SqliteConnection>
122}
123
124impl InnerCache {
125 pub fn new(path: CachePath) -> Self {
127 path.into()
128 }
129}
130
131impl PartialEq for InnerCache {
132 fn eq(&self, other: &Self) -> bool {
133 self.path == other.path
134 }
135}
136impl Eq for InnerCache {}
137
138impl From<CachePath> for InnerCache {
139 fn from(value: CachePath) -> Self {
140 Self {
141 path: value,
142 connection: Default::default()
143 }
144 }
145}
146
147#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Suitability)]
149#[serde(remote = "Self")]
150pub enum CachePath {
151 #[default]
155 Memory,
156 Path(String)
158}
159
160crate::util::string_or_struct_magic!(CachePath);
161
162impl CachePath {
163 pub fn as_str(&self) -> &str {
175 match self {
176 Self::Memory => ":memory:",
177 Self::Path(x) => x
178 }
179 }
180}
181
182impl AsRef<str> for CachePath {
183 fn as_ref(&self) -> &str {
184 self.as_str()
185 }
186}
187
188impl FromStr for CachePath {
189 type Err = std::convert::Infallible;
190
191 fn from_str(s: &str) -> Result<Self, Self::Err> {
192 Ok(s.into())
193 }
194}
195
196impl From<&str> for CachePath {
197 fn from(value: &str) -> Self {
198 value.to_string().into()
199 }
200}
201
202impl From<String> for CachePath {
203 fn from(value: String) -> Self {
204 match &*value {
205 ":memory:" => Self::Memory,
206 _ => Self::Path(value)
207 }
208 }
209}
210
211impl ::core::fmt::Debug for InnerCache {
212 fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
213 f.debug_struct("InnerCache")
214 .field("path", &self.path)
215 .field("connection", if self.connection.get().is_some() {&"OnceCell(..)"} else {&"OnceCell(<uninit>)"})
216 .finish()
217 }
218}
219
220#[derive(Debug, Error)]
222pub enum ReadFromCacheError {
223 #[error("{0}")]
225 MutexPoisonError(String),
226 #[error(transparent)]
228 DieselError(#[from] diesel::result::Error),
229 #[error(transparent)]
231 ConnectCacheError(#[from] ConnectCacheError)
232}
233
234#[derive(Debug, Error)]
236pub enum WriteToCacheError {
237 #[error("{0}")]
239 MutexPoisonError(String),
240 #[error(transparent)]
242 DieselError(#[from] diesel::result::Error),
243 #[error(transparent)]
245 ConnectCacheError(#[from] ConnectCacheError)
246}
247
248impl Cache {
249 pub fn read(&self, category: &str, key: &str) -> Result<Option<Option<String>>, ReadFromCacheError> {
253 self.0.lock().map_err(|e| ReadFromCacheError::MutexPoisonError(e.to_string()))?.read(category, key)
254 }
255
256 pub fn write(&self, category: &str, key: &str, value: Option<&str>) -> Result<(), WriteToCacheError> {
262 self.0.lock().map_err(|e| WriteToCacheError::MutexPoisonError(e.to_string()))?.write(category, key, value)
263 }
264}
265
266#[derive(Debug, Error)]
268pub enum ConnectCacheError {
269 #[error(transparent)]
271 ConnectionError(#[from] diesel::ConnectionError),
272 #[error(transparent)]
274 IoError(#[from] std::io::Error),
275 #[error(transparent)]
277 DieselError(#[from] diesel::result::Error)
278}
279
280impl InnerCache {
281 pub fn path(&self) -> &CachePath {
283 &self.path
284 }
285
286 pub fn connection(&mut self) -> Option<&mut SqliteConnection> {
288 self.connection.get_mut()
289 }
290
291 #[allow(clippy::missing_panics_doc, reason = "Doesn't panic, but should be replaced with OnceCell::get_or_try_init once that's stable.")]
301 pub fn connect(&mut self) -> Result<&mut SqliteConnection, ConnectCacheError> {
302 debug!(self, InnerCache::connect, self);
303 if self.connection.get().is_none() {
304 let mut needs_init = self.path == CachePath::Memory;
305 if let CachePath::Path(path) = &self.path {
306 if !std::fs::exists(path)? {
307 needs_init = true;
308 std::fs::File::create_new(path)?;
309 }
310 }
311 let mut connection = SqliteConnection::establish(self.path.as_str())?;
312 if needs_init {
313 diesel::sql_query(DB_INIT_COMMAND).execute(&mut connection)?;
314 }
315 self.connection.set(connection).map_err(|_| ()).expect("The connection to have just been confirmed unset.");
316 }
317 Ok(self.connection.get_mut().expect("The connection to have just been set."))
318 }
319
320 pub fn disconnect(&mut self) {
322 let _ = self.connection.take();
323 }
324
325 pub fn read(&mut self, category: &str, key: &str) -> Result<Option<Option<String>>, ReadFromCacheError> {
331 debug!(self, InnerCache::read, self, category, key);
332 Ok(cache::dsl::cache
333 .filter(cache::dsl::category.eq(category))
334 .filter(cache::dsl::key.eq(key))
335 .limit(1)
336 .select(CacheEntry::as_select())
337 .load(self.connect()?)?
338 .first()
339 .map(|cache_entry| cache_entry.value.to_owned()))
340 }
341
342 pub fn write(&mut self, category: &str, key: &str, value: Option<&str>) -> Result<(), WriteToCacheError> {
348 debug!(self, InnerCache::write, self, category, key, value);
349 diesel::insert_into(cache::table)
350 .values(NewCacheEntry {category, key, value})
351 .execute(self.connect()?)?;
352 Ok(())
353 }
354}
355
356impl From<InnerCache> for (CachePath, OnceCell<SqliteConnection>) {
357 fn from(value: InnerCache) -> Self {
358 (value.path, value.connection)
359 }
360}