Skip to main content

amalgam/
factory.rs

1//! The factory execution context and its product.
2//!
3//! When `get_or_set` must produce a value, it hands the factory a
4//! [`FactoryContext`]. Through it the factory can:
5//!
6//! * read the previously-cached **stale value** and its `ETag`/`LastModified`
7//!   (conditional refresh);
8//! * **adapt** the entry options for the value it is about to produce
9//!   (adaptive caching) via [`FactoryContext::options_mut`];
10//! * signal one of three outcomes: a new value ([`FactoryContext::value`] /
11//!   [`FactoryContext::modified`]), "nothing changed, reuse the stale value"
12//!   ([`FactoryContext::not_modified`]), or failure
13//!   ([`FactoryContext::fail`]).
14//!
15//! The factory returns `Result<FactoryProduct<V>, FactoryError>`; an `Err`
16//! drives the fail-safe path.
17
18use crate::error::FactoryError;
19use crate::options::EntryOptions;
20use crate::tags::Tag;
21use crate::time::Timestamp;
22
23/// The value (and metadata) a factory produced, ready to be cached.
24///
25/// Construct it through the [`FactoryContext`] methods rather than directly, so
26/// the (possibly adapted) options and tags are carried along correctly.
27#[derive(Debug)]
28pub struct FactoryProduct<V> {
29    pub(crate) value: V,
30    pub(crate) options: EntryOptions,
31    pub(crate) etag: Option<String>,
32    pub(crate) last_modified: Option<Timestamp>,
33    pub(crate) tags: Box<[Tag]>,
34    /// `true` when this product is the *reused stale value* (a `NotModified`
35    /// conditional-refresh result) rather than a freshly-produced value.
36    pub(crate) reused_stale: bool,
37}
38
39impl<V> FactoryProduct<V> {
40    /// Borrows the produced value.
41    #[must_use]
42    pub fn value(&self) -> &V {
43        &self.value
44    }
45}
46
47/// Context passed to a factory, carrying stale-value information and the mutable
48/// options for the value being produced.
49#[derive(Debug)]
50pub struct FactoryContext<V> {
51    key: std::sync::Arc<str>,
52    options: EntryOptions,
53    call_tags: Box<[Tag]>,
54    adaptive_tags: Option<Box<[Tag]>>,
55    stale_value: Option<V>,
56    stale_etag: Option<String>,
57    stale_last_modified: Option<Timestamp>,
58    stale_tags: Box<[Tag]>,
59}
60
61impl<V> FactoryContext<V> {
62    pub(crate) fn new(
63        key: std::sync::Arc<str>,
64        options: EntryOptions,
65        call_tags: Box<[Tag]>,
66        stale: Option<StaleInfo<V>>,
67    ) -> Self {
68        let (stale_value, stale_etag, stale_last_modified, stale_tags) = match stale {
69            Some(s) => (Some(s.value), s.etag, s.last_modified, s.tags),
70            None => (None, None, None, Box::from([])),
71        };
72        Self {
73            key,
74            options,
75            call_tags,
76            adaptive_tags: None,
77            stale_value,
78            stale_etag,
79            stale_last_modified,
80            stale_tags,
81        }
82    }
83
84    /// The (prefixed) cache key being produced.
85    #[must_use]
86    pub fn key(&self) -> &str {
87        &self.key
88    }
89
90    /// The options for the value being produced. Mutate these to **adapt** the
91    /// caching of this specific value (e.g. shorten the duration for an empty
92    /// result). This is *adaptive caching*.
93    #[must_use]
94    pub fn options_mut(&mut self) -> &mut EntryOptions {
95        &mut self.options
96    }
97
98    /// The options for the value being produced.
99    #[must_use]
100    pub fn options(&self) -> &EntryOptions {
101        &self.options
102    }
103
104    /// Adapts the options for the value being produced using the chainable
105    /// builder methods — the ergonomic way to do *adaptive caching*:
106    ///
107    /// ```ignore
108    /// ctx.adapt(|o| o.with_duration(Duration::from_secs(5)));
109    /// ```
110    pub fn adapt<F>(&mut self, f: F)
111    where
112        F: FnOnce(EntryOptions) -> EntryOptions,
113    {
114        let current = self.options.clone();
115        self.options = f(current);
116    }
117
118    /// `true` if a previously-cached (now stale) value is available.
119    #[must_use]
120    pub fn has_stale_value(&self) -> bool {
121        self.stale_value.is_some()
122    }
123
124    /// The previously-cached (now stale) value, if any.
125    #[must_use]
126    pub fn stale_value(&self) -> Option<&V> {
127        self.stale_value.as_ref()
128    }
129
130    /// The `ETag` of the stale value, for issuing a conditional request.
131    #[must_use]
132    pub fn stale_etag(&self) -> Option<&str> {
133        self.stale_etag.as_deref()
134    }
135
136    /// The `LastModified` of the stale value, for issuing a conditional request.
137    #[must_use]
138    pub fn stale_last_modified(&self) -> Option<Timestamp> {
139        self.stale_last_modified
140    }
141
142    /// Sets the tags for the value being produced (adaptive tagging). Overrides
143    /// the tags passed to the `get_or_set` call.
144    pub fn set_tags<I, S>(&mut self, tags: I)
145    where
146        I: IntoIterator<Item = S>,
147        S: AsRef<str>,
148    {
149        self.adaptive_tags = Some(crate::tags::collect_tags(tags));
150    }
151
152    fn effective_tags(&self) -> Box<[Tag]> {
153        self.adaptive_tags
154            .clone()
155            .unwrap_or_else(|| self.call_tags.clone())
156    }
157
158    /// Produces a new value, cached with the current (possibly adapted) options.
159    #[must_use]
160    pub fn value(self, value: V) -> FactoryProduct<V> {
161        let tags = self.effective_tags();
162        FactoryProduct {
163            value,
164            options: self.options,
165            etag: None,
166            last_modified: None,
167            tags,
168            reused_stale: false,
169        }
170    }
171
172    /// Begins producing a *modified* value, allowing an `ETag`/`LastModified`
173    /// (and tags) to be attached for future conditional refreshes.
174    pub fn modified(self, value: V) -> ModifiedBuilder<V> {
175        ModifiedBuilder {
176            ctx: self,
177            value,
178            etag: None,
179            last_modified: None,
180            tags: None,
181        }
182    }
183
184    /// Signals a factory failure, returning a [`FactoryError`] to return as
185    /// `Err`. Triggers the fail-safe path.
186    #[must_use]
187    pub fn fail(&self, message: impl Into<String>) -> FactoryError {
188        FactoryError::new(message)
189    }
190}
191
192impl<V: Clone> FactoryContext<V> {
193    /// Signals that the resource has **not changed** (e.g. an HTTP `304`): the
194    /// stale value is reused as the fresh result and its expiration is bumped
195    /// using the current options. Fails if there is no stale value to reuse.
196    pub fn not_modified(self) -> Result<FactoryProduct<V>, FactoryError> {
197        match &self.stale_value {
198            Some(value) => Ok(FactoryProduct {
199                value: value.clone(),
200                options: self.options.clone(),
201                etag: self.stale_etag.clone(),
202                last_modified: self.stale_last_modified,
203                tags: self.stale_tags.clone(),
204                reused_stale: true,
205            }),
206            None => Err(FactoryError::new(
207                "not_modified() was called but no stale value is available to reuse",
208            )),
209        }
210    }
211}
212
213/// Carries the stale value and its conditional-refresh metadata into a
214/// [`FactoryContext`].
215pub(crate) struct StaleInfo<V> {
216    pub(crate) value: V,
217    pub(crate) etag: Option<String>,
218    pub(crate) last_modified: Option<Timestamp>,
219    pub(crate) tags: Box<[Tag]>,
220}
221
222/// Builder for a modified factory result with conditional-refresh metadata.
223#[derive(Debug)]
224#[must_use = "call `.done()` to produce the FactoryProduct"]
225pub struct ModifiedBuilder<V> {
226    ctx: FactoryContext<V>,
227    value: V,
228    etag: Option<String>,
229    last_modified: Option<Timestamp>,
230    tags: Option<Box<[Tag]>>,
231}
232
233impl<V> ModifiedBuilder<V> {
234    /// Attaches an `ETag` to the produced value.
235    pub fn etag(mut self, etag: impl Into<String>) -> Self {
236        self.etag = Some(etag.into());
237        self
238    }
239
240    /// Attaches a `LastModified` timestamp to the produced value.
241    pub fn last_modified(mut self, last_modified: Timestamp) -> Self {
242        self.last_modified = Some(last_modified);
243        self
244    }
245
246    /// Sets the tags for the produced value (overriding call/adaptive tags).
247    pub fn tags<I, S>(mut self, tags: I) -> Self
248    where
249        I: IntoIterator<Item = S>,
250        S: AsRef<str>,
251    {
252        self.tags = Some(crate::tags::collect_tags(tags));
253        self
254    }
255
256    /// Finishes building the product.
257    #[must_use]
258    pub fn done(self) -> FactoryProduct<V> {
259        let tags = self.tags.unwrap_or_else(|| self.ctx.effective_tags());
260        FactoryProduct {
261            value: self.value,
262            options: self.ctx.options,
263            etag: self.etag,
264            last_modified: self.last_modified,
265            tags,
266            reused_stale: false,
267        }
268    }
269}