Skip to main content

dialcache/
use_case.rs

1//! Registered, typed use cases.
2
3use std::future::Future;
4use std::marker::PhantomData;
5use std::sync::Arc;
6
7use futures::future::BoxFuture;
8use serde::de::DeserializeOwned;
9use serde::Serialize;
10
11use crate::codec::{Codec, JsonCodec};
12use crate::engine::DialCache;
13use crate::error::{BoxError, ConfigError, Error};
14use crate::identity::{normalize_args, ArgValue, Identity, IntoKeyId};
15use crate::operation::{
16    downcast_value, erase_load, Comparator, ErasedOperation, JsonDefault, MismatchDiffHook,
17    MismatchEntry, Operation, OperationMetadata, Preview, RecoveryPredicate, SourceBudget,
18};
19use crate::policy::Policy;
20use crate::scope::Scope;
21
22/// The key of one call: the entity id and any secondary dimensions.
23#[derive(Debug, Clone, PartialEq, Default)]
24pub struct KeySpec {
25    /// The entity identifier, spelled as text.
26    pub id: String,
27    /// Secondary dimensions; absent values are dropped and names sorted when
28    /// the key is built.
29    pub args: Vec<(String, ArgValue)>,
30}
31
32impl KeySpec {
33    /// A key with `id` and no secondary dimensions. See [`IntoKeyId`] for
34    /// the shared numeric spelling and supported input types.
35    pub fn new(id: impl IntoKeyId) -> Self {
36        KeySpec {
37            id: id.into_key_id(),
38            args: Vec::new(),
39        }
40    }
41
42    /// Add a secondary key dimension. Absent values are omitted; names are
43    /// sorted by UTF-16 code units when the key is built. Primitive integers
44    /// preserve their exact decimal value; floats use JavaScript number
45    /// spelling, with `f32` promoted to `f64`. Shared references to supported
46    /// inputs are also accepted.
47    pub fn arg(mut self, name: impl Into<String>, value: impl Into<ArgValue>) -> Self {
48        self.args.push((name.into(), value.into()));
49        self
50    }
51}
52
53impl From<String> for KeySpec {
54    fn from(id: String) -> Self {
55        KeySpec::new(id)
56    }
57}
58
59impl From<&str> for KeySpec {
60    fn from(id: &str) -> Self {
61        KeySpec::new(id)
62    }
63}
64
65macro_rules! key_from_number {
66    ($($t:ty),*) => { $(impl From<$t> for KeySpec { fn from(id: $t) -> Self { KeySpec::new(id) } })* };
67}
68key_from_number!(i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f64, f32);
69
70impl From<&str> for ArgValue {
71    fn from(value: &str) -> Self {
72        ArgValue::Str(value.to_string())
73    }
74}
75
76impl From<String> for ArgValue {
77    fn from(value: String) -> Self {
78        ArgValue::Str(value)
79    }
80}
81
82impl<T: Clone + Into<ArgValue>> From<&T> for ArgValue {
83    fn from(value: &T) -> Self {
84        value.clone().into()
85    }
86}
87
88impl From<bool> for ArgValue {
89    fn from(value: bool) -> Self {
90        ArgValue::Bool(value)
91    }
92}
93
94macro_rules! integer_arg_value {
95    ($($t:ty),*) => { $(impl From<$t> for ArgValue {
96        fn from(value: $t) -> Self { ArgValue::Int(i64::from(value)) }
97    })* };
98}
99integer_arg_value!(i8, i16, i32, i64, u8, u16, u32);
100
101macro_rules! wide_integer_arg_value {
102    ($($t:ty),*) => { $(impl From<$t> for ArgValue {
103        fn from(value: $t) -> Self {
104            match i64::try_from(value) {
105                Ok(value) => ArgValue::Int(value),
106                Err(_) => ArgValue::BigInt(value.to_string()),
107            }
108        }
109    })* };
110}
111wide_integer_arg_value!(i128, isize, u64, u128, usize);
112
113impl From<f64> for ArgValue {
114    fn from(value: f64) -> Self {
115        ArgValue::Number(value)
116    }
117}
118
119impl From<f32> for ArgValue {
120    fn from(value: f32) -> Self {
121        ArgValue::Number(f64::from(value))
122    }
123}
124
125impl<T: Into<ArgValue>> From<Option<T>> for ArgValue {
126    fn from(value: Option<T>) -> Self {
127        match value {
128            Some(value) => value.into(),
129            None => ArgValue::Absent,
130        }
131    }
132}
133
134type KeyFn<Args> = Arc<dyn Fn(&Args) -> KeySpec + Send + Sync>;
135type SourceFn<Args, T> =
136    Arc<dyn Fn(Scope, Args) -> BoxFuture<'static, Result<T, BoxError>> + Send + Sync>;
137
138/// Builds a registered use case. Finish with [`register`](Self::register)
139/// for `serde` JSON values or [`register_custom`](Self::register_custom)
140/// with an explicit codec and comparator.
141pub struct UseCaseBuilder<Args, T> {
142    cache: DialCache,
143    key_type: String,
144    use_case: String,
145    tracked: bool,
146    policy: Policy,
147    budget: SourceBudget,
148    codec: Option<Arc<dyn Codec<T>>>,
149    comparator: Option<Comparator<T>>,
150    should_recover: Option<RecoveryPredicate>,
151    preview: Option<Preview<T>>,
152    mismatch_diff: Option<MismatchDiffHook<T>>,
153    key: Option<KeyFn<Args>>,
154    source: Option<SourceFn<Args, T>>,
155}
156
157impl<Args, T> std::fmt::Debug for UseCaseBuilder<Args, T> {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        f.debug_struct("UseCaseBuilder")
160            .field("key_type", &self.key_type)
161            .field("use_case", &self.use_case)
162            .finish_non_exhaustive()
163    }
164}
165
166impl<Args, T> UseCaseBuilder<Args, T>
167where
168    Args: Clone + Send + Sync + 'static,
169    T: Send + Sync + 'static,
170{
171    pub(crate) fn new(cache: DialCache, key_type: String, use_case: String) -> Self {
172        UseCaseBuilder {
173            cache,
174            key_type,
175            use_case,
176            tracked: false,
177            policy: Policy::default(),
178            budget: SourceBudget::Default,
179            codec: None,
180            comparator: None,
181            should_recover: None,
182            preview: None,
183            mismatch_diff: None,
184            key: None,
185            source: None,
186        }
187    }
188
189    /// Static policy captured for every call of this use case.
190    pub fn policy(mut self, policy: Policy) -> Self {
191        self.policy = policy;
192        self
193    }
194
195    /// Share the entity's invalidation watermark with every tracked variant.
196    pub fn tracked(mut self, tracked: bool) -> Self {
197        self.tracked = tracked;
198        self
199    }
200
201    /// The source deadline. Defaults to 60 s.
202    pub fn budget(mut self, budget: SourceBudget) -> Self {
203        self.budget = budget;
204        self
205    }
206
207    /// Replace the JSON codec; required by [`register_custom`](Self::register_custom).
208    pub fn codec(mut self, codec: Arc<dyn Codec<T>>) -> Self {
209        self.codec = Some(codec);
210        self
211    }
212
213    /// Application equality for shadow validation. Defaults to `PartialEq`.
214    pub fn comparator(
215        mut self,
216        comparator: impl Fn(&T, &T) -> bool + Send + Sync + 'static,
217    ) -> Self {
218        self.comparator = Some(Arc::new(comparator));
219        self
220    }
221
222    /// Override the instance classifier for stale recovery.
223    pub fn should_recover(
224        mut self,
225        predicate: impl Fn(&Error) -> bool + Send + Sync + 'static,
226    ) -> Self {
227        self.should_recover = Some(Arc::new(predicate));
228        self
229    }
230
231    /// The text mismatch warnings log for each value in place of its JSON;
232    /// required for value fields under
233    /// [`register_custom`](Self::register_custom). Difference fields then
234    /// come only from [`mismatch_diff`](Self::mismatch_diff). See [`Preview`].
235    pub fn preview(
236        mut self,
237        preview: impl Fn(&T) -> Option<String> + Send + Sync + 'static,
238    ) -> Self {
239        self.preview = Some(Arc::new(preview));
240        self
241    }
242
243    /// Replace the built-in difference computation of mismatch warnings;
244    /// required for difference fields under
245    /// [`register_custom`](Self::register_custom). See [`MismatchDiffHook`].
246    pub fn mismatch_diff(
247        mut self,
248        hook: impl Fn(&T, &T) -> Vec<MismatchEntry> + Send + Sync + 'static,
249    ) -> Self {
250        self.mismatch_diff = Some(Arc::new(hook));
251        self
252    }
253
254    /// Select every input dimension that affects the value. Runs only for
255    /// enabled calls.
256    pub fn key(mut self, key: impl Fn(&Args) -> KeySpec + Send + Sync + 'static) -> Self {
257        self.key = Some(Arc::new(key));
258        self
259    }
260
261    /// The source of truth. It receives the call's scope so nested cached
262    /// calls can participate; served-hit shadow validation invokes it again
263    /// under disabled caching.
264    pub fn source<F, Fut>(mut self, source: F) -> Self
265    where
266        F: Fn(Scope, Args) -> Fut + Send + Sync + 'static,
267        Fut: Future<Output = Result<T, BoxError>> + Send + 'static,
268    {
269        self.source = Some(Arc::new(move |scope, args| Box::pin(source(scope, args))));
270        self
271    }
272
273    fn finish(
274        self,
275        codec: Arc<dyn Codec<T>>,
276        comparator: Comparator<T>,
277        json: Option<JsonDefault<T>>,
278    ) -> Result<UseCase<Args, T>, ConfigError> {
279        let key = self
280            .key
281            .ok_or_else(|| ConfigError::invalid("DialCache use case needs a key selector"))?;
282        let source = self
283            .source
284            .ok_or_else(|| ConfigError::invalid("DialCache use case needs a source"))?;
285        self.policy
286            .validate()
287            .map_err(|e| ConfigError::invalid(e.to_string()))?;
288        if let SourceBudget::Millis(ms) = self.budget {
289            if !(1..=crate::limits::MAX_DEADLINE_MS).contains(&ms) {
290                return Err(ConfigError::invalid(
291                    "DialCache source budget is outside its domain",
292                ));
293            }
294        }
295        self.cache.register_use_case(&self.use_case)?;
296        let (identity, metadata) = Operation {
297            identity: Identity::new(self.key_type, "", self.use_case).tracked(self.tracked),
298            policy: self.policy,
299            budget: self.budget,
300            codec,
301            comparator,
302            should_recover: self.should_recover,
303            // Without a preview of its own, a JSON use case keeps the default.
304            preview: self
305                .preview
306                .or_else(|| json.as_ref().map(JsonDefault::preview)),
307            mismatch_diff: self.mismatch_diff,
308            json,
309        }
310        .erase();
311        Ok(UseCase {
312            inner: Arc::new(UseCaseInner {
313                cache: self.cache,
314                identity,
315                metadata,
316                key,
317                source,
318            }),
319            _args: PhantomData,
320        })
321    }
322
323    /// Register with an explicit codec and comparator (set with
324    /// [`codec`](Self::codec) and [`comparator`](Self::comparator)).
325    pub fn register_custom(self) -> Result<UseCase<Args, T>, ConfigError> {
326        let codec = self
327            .codec
328            .clone()
329            .ok_or_else(|| ConfigError::invalid("DialCache use case needs a codec"))?;
330        let comparator = self
331            .comparator
332            .clone()
333            .ok_or_else(|| ConfigError::invalid("DialCache use case needs a comparator"))?;
334        self.finish(codec, comparator, None)
335    }
336}
337
338impl<Args, T> UseCaseBuilder<Args, T>
339where
340    Args: Clone + Send + Sync + 'static,
341    T: Serialize + DeserializeOwned + PartialEq + Send + Sync + 'static,
342{
343    /// Register with the JSON codec and `PartialEq` comparison unless
344    /// overridden. Mismatch warnings use the values' JSON, as for
345    /// [`Operation::new`].
346    pub fn register(self) -> Result<UseCase<Args, T>, ConfigError> {
347        let codec = self.codec.clone().unwrap_or_else(|| Arc::new(JsonCodec));
348        let comparator = self
349            .comparator
350            .clone()
351            .unwrap_or_else(|| Arc::new(|a: &T, b: &T| a == b));
352        self.finish(codec, comparator, Some(JsonDefault::new()))
353    }
354}
355
356struct UseCaseInner<Args, T> {
357    cache: DialCache,
358    identity: Identity,
359    metadata: Arc<OperationMetadata>,
360    key: KeyFn<Args>,
361    source: SourceFn<Args, T>,
362}
363
364/// A registered use case: a typed cached function.
365///
366/// Values are shared by reference and must be treated as immutable.
367pub struct UseCase<Args, T> {
368    inner: Arc<UseCaseInner<Args, T>>,
369    _args: PhantomData<fn(Args)>,
370}
371
372impl<Args, T> Clone for UseCase<Args, T> {
373    fn clone(&self) -> Self {
374        UseCase {
375            inner: self.inner.clone(),
376            _args: PhantomData,
377        }
378    }
379}
380
381impl<Args, T> std::fmt::Debug for UseCase<Args, T> {
382    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
383        f.debug_struct("UseCase")
384            .field("key_type", &self.inner.identity.key_type)
385            .field("use_case", &self.inner.identity.use_case)
386            .finish()
387    }
388}
389
390impl<Args, T> UseCase<Args, T>
391where
392    Args: Clone + Send + Sync + 'static,
393    T: Send + Sync + 'static,
394{
395    /// The operation name given at registration.
396    pub fn use_case(&self) -> &str {
397        &self.inner.identity.use_case
398    }
399
400    /// The entity type given at registration.
401    pub fn key_type(&self) -> &str {
402        &self.inner.identity.key_type
403    }
404
405    /// Get the value for `args` through the configured layers.
406    pub async fn get(&self, scope: &Scope, args: Args) -> Result<Arc<T>, Error> {
407        let inner = self.inner.clone();
408        let identity = inner.identity.clone();
409        let provider_identity = identity.clone();
410        let provider_args = args.clone();
411        let provider_inner = inner.clone();
412        let identity_provider = Arc::new(move || -> Result<Identity, BoxError> {
413            let spec = (provider_inner.key)(&provider_args);
414            let args = normalize_args(spec.args.clone())?;
415            Ok(Identity {
416                id: spec.id,
417                args,
418                ..provider_identity.clone()
419            })
420        });
421        let source_inner = inner.clone();
422        let load = erase_load(move |scope: Scope| (source_inner.source)(scope, args.clone()));
423        let erased = ErasedOperation {
424            identity,
425            identity_provider: Some(identity_provider),
426            metadata: inner.metadata.clone(),
427            load,
428        };
429        let value = inner.cache.execute(scope, erased).await?;
430        downcast_value::<T>(value)
431    }
432
433    /// Invoke the source directly, outside any scope. Equivalent to
434    /// [`get`](Self::get) with [`Scope::outside`].
435    pub async fn get_uncached(&self, args: Args) -> Result<Arc<T>, Error> {
436        self.get(&Scope::outside(), args).await
437    }
438}
439
440impl DialCache {
441    /// Start registering a use case. `key_type` names the entity type and
442    /// `use_case` the operation; the pair identifies cached values.
443    pub fn use_case<Args, T>(
444        &self,
445        key_type: impl Into<String>,
446        use_case: impl Into<String>,
447    ) -> UseCaseBuilder<Args, T>
448    where
449        Args: Clone + Send + Sync + 'static,
450        T: Send + Sync + 'static,
451    {
452        UseCaseBuilder::new(self.clone(), key_type.into(), use_case.into())
453    }
454}