Skip to main content

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