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}