powerpack_cache/query.rs
1use std::convert::Infallible;
2use std::fmt::Write as _;
3use std::io;
4use std::time::Duration;
5
6use flagset::{FlagSet, flags};
7use serde_json as json;
8use thiserror::Error;
9
10/// Raised when accessing data in the cache.
11#[derive(Debug, Error)]
12#[non_exhaustive]
13pub enum QueryError {
14 /// Raised when there is a cache miss.
15 #[error("cache miss")]
16 Miss,
17
18 /// Raised when an I/O error occurs.
19 ///
20 /// This can occur when reading the cache file.
21 #[error("io error")]
22 Io(#[from] io::Error),
23
24 /// Raised when JSON deserialization occurs.
25 ///
26 /// Data is stored in the cache as JSON, so this error is raised when
27 /// deserializing the data fails.
28 ///
29 /// Since the caller provides the type that is stored in the cache, this
30 /// will typically occur if the type changes.
31 #[error("deserialization error")]
32 BadData(#[from] json::Error),
33}
34
35flags! {
36 /// The policy for querying the cache.
37 ///
38 /// Holds various toggles for when to update the cache and when to return
39 /// stale data. This type follows a bitflag pattern, so multiple flags can
40 /// be combined using the `|` operator.
41 ///
42 /// The default policy is [`QueryPolicy::default_set()`], which is
43 /// equivalent to the following example.
44 ///
45 /// # Examples
46 ///
47 /// ```
48 /// # use powerpack_cache::{Query, QueryPolicy};
49 /// let q = Query::new("unique_key").policy(
50 /// QueryPolicy::UpdateBadData
51 /// | QueryPolicy::UpdateChecksumMismatch
52 /// | QueryPolicy::UpdateExpired
53 /// | QueryPolicy::ReturnExpired
54 /// );
55 /// # let q: Query<'_, ()> = q;
56 /// ```
57 pub enum QueryPolicy: u16 {
58 /// Always update the cache when a [`Query::update_fn`] is provided.
59 ///
60 /// This option overrides the other `Update...`flags and will always
61 /// update the cache. If not set then the cache will only be updated if
62 /// the data is bad or stale. Outlined in the below flags.
63 ///
64 /// Generally this should not be set because:
65 /// - It defeats the purpose of a TTL
66 /// - Alfred can spawn many instances of a process in a short period of
67 /// time, e.g. one for each character typed, this could result in many
68 /// unnecessary updates (depending on what type of data you're
69 /// storing).
70 UpdateAlways,
71
72 /// Update the cache if the data is bad (fails to deserialize).
73 ///
74 /// Generally this should be set because if the data is bad then you
75 /// want to correct it in the cache.
76 UpdateBadData,
77
78 /// Update the cache if the checksum is different.
79 ///
80 /// Generally this should be set because the checksum is used to
81 /// determine if the data is still applicable.
82 UpdateChecksumMismatch,
83
84 /// Update the cache if the data is expired.
85 UpdateExpired,
86
87 /// Always return data if it is available.
88 ///
89 /// This option is forward compatible with any other flags that may be
90 /// added in the future for returning data. Right now it is equivalent
91 /// to `ReturnBadDataErr | ReturnChecksumMismatch | ReturnExpired`.
92 ///
93 /// Generally this should not be set.
94 ReturnAlways,
95
96 /// Return the error if the data is bad, [`QueryError::BadData`] which
97 /// contains the deserialization error in the source.
98 ///
99 /// If not set then [`QueryError::Miss`] will be returned.
100 ///
101 /// Whether this should be set depends on whether your code is planning
102 /// on handling the error.
103 ReturnBadDataErr,
104
105 /// Return data even if the checksum is different.
106 ///
107 /// If not set then [`QueryError::Miss`] will be returned.
108 ///
109 /// Generally this should not be set because the checksum is used to
110 /// determine if the data is still applicable.
111 ReturnChecksumMismatch,
112
113 /// Return data if it is expired.
114 ///
115 /// If not set then [`QueryError::Miss`] will be returned.
116 ///
117 /// Generally this should be set because for Alfred workflows it is
118 /// desirable to return *something* even if the data is expired.
119 ReturnExpired,
120 }
121}
122
123impl QueryPolicy {
124 /// Returns the default policy.
125 ///
126 /// The default enables the following flags only.
127 /// - [`QueryPolicy::UpdateBadData`]
128 /// - [`QueryPolicy::UpdateChecksumMismatch`]
129 /// - [`QueryPolicy::UpdateExpired`]
130 /// - [`QueryPolicy::ReturnExpired`]
131 pub fn default_set() -> FlagSet<Self> {
132 QueryPolicy::UpdateBadData
133 | QueryPolicy::UpdateChecksumMismatch
134 | QueryPolicy::UpdateExpired
135 | QueryPolicy::ReturnExpired
136 }
137}
138
139/// Query the cache for data.
140///
141/// A query must be constructed, using the builder pattern and then passed to
142/// [`Cache::query`](crate::Cache::query) to retrieve the data.
143///
144/// The following fields are required when constructing a query:
145///
146/// - `key`: passed to [`Query::new`], this is a unique identifier for the
147/// data in the cache, and is used to determine the name of the cache file.
148///
149/// - type `T`: the type of the data stored in the cache, it must implement
150/// [`serde::Serialize`] and [`serde::Deserialize`].
151///
152/// The following fields are optional:
153///
154/// - `update_fn`: used to update the cache, see [`Query::update_fn`].
155/// - `checksum`: used to determine staleness of the cache, see
156/// [`Query::checksum`].
157/// - `policy`: used to determine when to update the cache and when to return
158/// stale data, see [`Query::policy`].
159/// - `ttl`: the Time To Live (TTL) for the data in the cache, see
160/// [`Query::ttl`].
161/// - `initial_poll`: the duration to wait for the cache to be populated on the
162/// first call, see [`Query::initial_poll`].
163///
164pub struct Query<'a, T, E = Infallible> {
165 pub(crate) key: &'a str,
166 pub(crate) update_fn: Option<Box<dyn FnOnce() -> Result<T, E> + 'a>>,
167 pub(crate) policy: Option<FlagSet<QueryPolicy>>,
168 pub(crate) checksum: Option<String>,
169 pub(crate) ttl: Option<Duration>,
170 pub(crate) initial_poll: Option<Duration>,
171}
172
173impl<'a> Query<'a, (), Infallible> {
174 /// Returns a new cache query.
175 ///
176 /// The key is used to determine the name of the cache file.
177 #[inline]
178 pub fn new(key: &'a str) -> Self {
179 Query {
180 key,
181 update_fn: None,
182 policy: None,
183 checksum: None,
184 ttl: None,
185 initial_poll: None,
186 }
187 }
188
189 /// Set the function to update the cache.
190 ///
191 /// This function is called if the cache needs to be updated.
192 ///
193 /// # 💡 Note
194 ///
195 /// The cache is updated in a separate process to avoid blocking the main
196 /// thread, this means that any errors from the update function will not be
197 /// propagated. Stale data will be returned in the meantime.
198 #[inline]
199 pub fn update_fn<F, T, E>(self, update_fn: F) -> Query<'a, T, E>
200 where
201 F: FnOnce() -> Result<T, E> + 'a,
202 {
203 Query {
204 key: self.key,
205 checksum: self.checksum,
206 policy: self.policy,
207 ttl: self.ttl,
208 initial_poll: self.initial_poll,
209 update_fn: Some(Box::new(update_fn)),
210 }
211 }
212}
213
214impl<T, E> Query<'_, T, E> {
215 /// Set the checksum for the cache.
216 ///
217 /// This is used to determine staleness and is used in two places:
218 ///
219 /// - Whether to the cache needs to be updated (in addition to the TTL).
220 /// If the checksum is different to the one stored in the cache then the
221 /// cache might be updated, depending on the [`QueryPolicy`].
222 ///
223 /// - Whether to return stale data. If the checksum is different to the one
224 /// stored in the cache then depending on the [`QueryPolicy`] stale data
225 /// may be returned.
226 ///
227 #[inline]
228 pub fn checksum<C>(mut self, checksum: C) -> Self
229 where
230 C: AsRef<[u8]>,
231 {
232 self.checksum = Some(to_hex(checksum.as_ref()));
233 self
234 }
235
236 /// Set the policy for the cache query.
237 ///
238 /// This is used to determine when updates should occur and stale data is
239 /// allowed to be returned.
240 ///
241 /// Defaults to the cache's policy.
242 #[inline]
243 pub fn policy(mut self, policy: impl Into<FlagSet<QueryPolicy>>) -> Self {
244 self.policy = Some(policy.into());
245 self
246 }
247
248 /// Set the Time To Live (TTL) for the data in the cache.
249 ///
250 /// If the data in the cache is older than this then the cache will be
251 /// automatically refreshed. Stale data will be returned in the meantime.
252 ///
253 /// Defaults to the cache's TTL.
254 #[inline]
255 pub fn ttl(mut self, ttl: Duration) -> Self {
256 self.ttl = Some(ttl);
257 self
258 }
259
260 /// Set the initial poll duration.
261 ///
262 /// This is the duration to wait for the cache to be populated on the first
263 /// call. If the cache is not populated within this duration, a miss error
264 /// will be raised.
265 ///
266 /// Defaults to the cache's initial poll duration.
267 #[inline]
268 pub fn initial_poll(mut self, initial_poll: Duration) -> Self {
269 self.initial_poll = Some(initial_poll);
270 self
271 }
272}
273
274fn to_hex(b: &[u8]) -> String {
275 let mut s = String::with_capacity(b.len() * 2);
276 for byte in b {
277 write!(&mut s, "{:02x}", byte).unwrap();
278 }
279 s
280}