Skip to main content

hitbox_core/
response.rs

1//! Cacheable response types and traits.
2//!
3//! This module provides types for working with cacheable responses:
4//!
5//! - [`CacheableResponse`] - Trait for types that can be cached
6//! - [`CacheState`] - Freshness state of cached data
7//! - [`ResponseCachePolicy`] - Type alias for response cache decisions
8//!
9//! ## CacheableResponse Trait
10//!
11//! The [`CacheableResponse`] trait defines how response types are converted
12//! to and from their cached representation. This allows responses to be
13//! stored efficiently in cache backends.
14//!
15//! ## Cache States
16//!
17//! Cached data can be in three states:
18//!
19//! - [`CacheState::Actual`] - Data is fresh and valid
20//! - [`CacheState::Stale`] - Data is usable but should be refreshed
21//! - [`CacheState::Expired`] - Data is no longer valid
22//!
23//! ## Result Handling
24//!
25//! This module provides a blanket implementation of `CacheableResponse` for
26//! `Result<T, E>` where `T: CacheableResponse`. This allows error responses
27//! to pass through uncached while successful responses are cached.
28
29use std::fmt::Debug;
30use std::future::Future;
31
32const POLL_AFTER_READY_ERROR: &str = "ResultIntoCachedFuture can't be polled after finishing";
33use std::marker::PhantomData;
34use std::pin::Pin;
35use std::task::{Context, Poll};
36
37use chrono::Utc;
38use pin_project::pin_project;
39
40use crate::{
41    CachePolicy, EntityPolicyConfig,
42    predicate::{Predicate, PredicateResult},
43    value::CacheValue,
44};
45
46/// Cache policy for responses.
47///
48/// Type alias that specializes [`CachePolicy`] for response caching:
49/// - `Cacheable` variant contains a [`CacheValue`] with the cached representation
50/// - `NonCacheable` variant contains the original response
51pub type ResponseCachePolicy<C> = CachePolicy<CacheValue<<C as CacheableResponse>::Cached>, C>;
52
53/// Freshness state of cached data.
54///
55/// Represents the time-based state of a cached value relative to its
56/// staleness and expiration timestamps.
57#[derive(Debug, PartialEq, Eq)]
58pub enum CacheState<Cached> {
59    /// Data is stale but not expired (usable, should refresh in background).
60    Stale(Cached),
61    /// Data is fresh and valid.
62    Actual(Cached),
63    /// Data has expired (must refresh before use).
64    Expired(Cached),
65}
66
67/// Trait for response types that can be cached.
68///
69/// This trait defines how responses are converted to and from their cached
70/// representation. Implementations must provide methods for:
71///
72/// - Determining if a response should be cached (`cache_policy`)
73/// - Converting to the cached format (`into_cached`)
74/// - Reconstructing from cached data (`from_cached`)
75///
76/// # Associated Types
77///
78/// - `Cached` - The serializable representation stored in cache
79/// - `Subject` - The type that predicates evaluate (for wrapper types like `Result`)
80/// - `IntoCachedFuture` - Future returned by `into_cached`
81/// - `FromCachedFuture` - Future returned by `from_cached`
82///
83/// # Blanket Implementation
84///
85/// A blanket implementation is provided for `Result<T, E>` where `T: CacheableResponse`.
86/// This allows:
87/// - `Ok(response)` to be cached if the inner response is cacheable
88/// - `Err(error)` to always pass through without caching
89///
90/// # Example Implementation
91///
92/// ```
93/// use hitbox_core::{CacheableResponse, CachePolicy, EntityPolicyConfig};
94/// use hitbox_core::predicate::{Predicate, PredicateResult};
95/// use hitbox_core::response::ResponseCachePolicy;
96/// use hitbox_core::value::CacheValue;
97/// use chrono::Utc;
98///
99/// #[derive(Clone)]
100/// struct MyResponse {
101///     body: String,
102///     status: u16,
103/// }
104///
105/// impl CacheableResponse for MyResponse {
106///     type Cached = String;
107///     type Subject = Self;
108///     type IntoCachedFuture = std::future::Ready<CachePolicy<String, Self>>;
109///     type FromCachedFuture = std::future::Ready<Self>;
110///
111///     async fn cache_policy<P>(
112///         self,
113///         predicates: P,
114///         config: &EntityPolicyConfig,
115///     ) -> ResponseCachePolicy<Self>
116///     where
117///         P: Predicate<Subject = Self::Subject> + Send + Sync,
118///     {
119///         match predicates.check(self).await {
120///             PredicateResult::Cacheable(data) => {
121///                 let cached = data.body.clone();
122///                 CachePolicy::Cacheable(CacheValue::new(
123///                     cached,
124///                     config.ttl.map(|d| Utc::now() + d),
125///                     config.stale_ttl.map(|d| Utc::now() + d),
126///                 ))
127///             }
128///             PredicateResult::NonCacheable(data) => CachePolicy::NonCacheable(data),
129///         }
130///     }
131///
132///     fn into_cached(self) -> Self::IntoCachedFuture {
133///         std::future::ready(CachePolicy::Cacheable(self.body))
134///     }
135///
136///     fn from_cached(cached: String) -> Self::FromCachedFuture {
137///         std::future::ready(MyResponse { body: cached, status: 200 })
138///     }
139/// }
140/// ```
141pub trait CacheableResponse
142where
143    Self: Sized + Send + 'static,
144    Self::Cached: Clone,
145{
146    /// The serializable type stored in cache.
147    type Cached;
148
149    /// The type that response predicates evaluate.
150    ///
151    /// For simple responses, this is `Self`. For wrapper types like `Result<T, E>`,
152    /// this is the inner type `T`.
153    type Subject: CacheableResponse;
154
155    /// Future type for `into_cached` method.
156    type IntoCachedFuture: Future<Output = CachePolicy<Self::Cached, Self>> + Send;
157
158    /// Future type for `from_cached` method.
159    type FromCachedFuture: Future<Output = Self> + Send;
160
161    /// Determine if this response should be cached.
162    ///
163    /// Applies predicates to determine cacheability, then converts cacheable
164    /// responses to their cached representation with TTL metadata.
165    ///
166    /// # Arguments
167    ///
168    /// * `predicates` - Predicates to evaluate whether the response is cacheable
169    /// * `config` - TTL configuration for the cached entry
170    fn cache_policy<P>(
171        self,
172        predicates: P,
173        config: &EntityPolicyConfig,
174    ) -> impl Future<Output = ResponseCachePolicy<Self>> + Send
175    where
176        P: Predicate<Subject = Self::Subject> + Send + Sync;
177
178    /// Convert this response to its cached representation.
179    ///
180    /// Returns `Cacheable` with the serializable data, or `NonCacheable`
181    /// if the response should not be cached.
182    fn into_cached(self) -> Self::IntoCachedFuture;
183
184    /// Reconstruct a response from cached data.
185    ///
186    /// Creates a new response instance from previously cached data.
187    fn from_cached(cached: Self::Cached) -> Self::FromCachedFuture;
188}
189
190// =============================================================================
191// Scalar type implementations
192// =============================================================================
193
194macro_rules! impl_cacheable_response_for_scalar {
195    ($($ty:ty),* $(,)?) => {
196        $(
197            impl CacheableResponse for $ty {
198                type Cached = Self;
199                type Subject = Self;
200                type IntoCachedFuture = std::future::Ready<CachePolicy<Self, Self>>;
201                type FromCachedFuture = std::future::Ready<Self>;
202
203                async fn cache_policy<P>(
204                    self,
205                    predicates: P,
206                    config: &EntityPolicyConfig,
207                ) -> ResponseCachePolicy<Self>
208                where
209                    P: Predicate<Subject = Self::Subject> + Send + Sync,
210                {
211                    match predicates.check(self).await {
212                        PredicateResult::Cacheable(data) => {
213                            let cached = data.clone();
214                            CachePolicy::Cacheable(CacheValue::new(
215                                cached,
216                                config.ttl.map(|d| Utc::now() + d),
217                                config.stale_ttl.map(|d| Utc::now() + d),
218                            ))
219                        }
220                        PredicateResult::NonCacheable(data) => CachePolicy::NonCacheable(data),
221                    }
222                }
223
224                fn into_cached(self) -> Self::IntoCachedFuture {
225                    std::future::ready(CachePolicy::Cacheable(self))
226                }
227
228                fn from_cached(cached: Self) -> Self::FromCachedFuture {
229                    std::future::ready(cached)
230                }
231            }
232        )*
233    };
234}
235
236impl_cacheable_response_for_scalar! {
237    // Unsigned integers
238    u8, u16, u32, u64, u128, usize,
239    // Signed integers
240    i8, i16, i32, i64, i128, isize,
241    // Other primitives
242    bool, char,
243    // Common types
244    String,
245}
246
247// =============================================================================
248// Vec<T> implementation
249// =============================================================================
250
251impl<T> CacheableResponse for Vec<T>
252where
253    T: CacheableResponse<Cached = T, Subject = T> + Clone + Send + 'static,
254{
255    type Cached = Self;
256    type Subject = Self;
257    type IntoCachedFuture = std::future::Ready<CachePolicy<Self, Self>>;
258    type FromCachedFuture = std::future::Ready<Self>;
259
260    async fn cache_policy<P>(
261        self,
262        predicates: P,
263        config: &EntityPolicyConfig,
264    ) -> ResponseCachePolicy<Self>
265    where
266        P: Predicate<Subject = Self::Subject> + Send + Sync,
267    {
268        match predicates.check(self).await {
269            PredicateResult::Cacheable(data) => {
270                let cached = data.clone();
271                CachePolicy::Cacheable(CacheValue::new(
272                    cached,
273                    config.ttl.map(|d| Utc::now() + d),
274                    config.stale_ttl.map(|d| Utc::now() + d),
275                ))
276            }
277            PredicateResult::NonCacheable(data) => CachePolicy::NonCacheable(data),
278        }
279    }
280
281    fn into_cached(self) -> Self::IntoCachedFuture {
282        std::future::ready(CachePolicy::Cacheable(self))
283    }
284
285    fn from_cached(cached: Self) -> Self::FromCachedFuture {
286        std::future::ready(cached)
287    }
288}
289
290// =============================================================================
291// Result<T, E> wrapper futures
292// =============================================================================
293
294#[doc(hidden)]
295#[pin_project(project = ResultIntoCachedProj)]
296pub enum ResultIntoCachedFuture<T, E>
297where
298    T: CacheableResponse,
299{
300    /// Ok variant - wraps the inner type's future
301    Ok(#[pin] T::IntoCachedFuture),
302    /// Err variant - contains the error to return immediately
303    Err(Option<E>),
304}
305
306impl<T, E> Future for ResultIntoCachedFuture<T, E>
307where
308    T: CacheableResponse,
309{
310    type Output = CachePolicy<T::Cached, Result<T, E>>;
311
312    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
313        match self.project() {
314            ResultIntoCachedProj::Ok(fut) => fut.poll(cx).map(|policy| match policy {
315                CachePolicy::Cacheable(res) => CachePolicy::Cacheable(res),
316                CachePolicy::NonCacheable(res) => CachePolicy::NonCacheable(Ok(res)),
317            }),
318            ResultIntoCachedProj::Err(e) => Poll::Ready(CachePolicy::NonCacheable(Err(e
319                .take()
320                .expect(POLL_AFTER_READY_ERROR)))),
321        }
322    }
323}
324
325#[doc(hidden)]
326#[pin_project]
327pub struct ResultFromCachedFuture<T, E>
328where
329    T: CacheableResponse,
330{
331    #[pin]
332    inner: T::FromCachedFuture,
333    _marker: PhantomData<E>,
334}
335
336impl<T, E> Future for ResultFromCachedFuture<T, E>
337where
338    T: CacheableResponse,
339{
340    type Output = Result<T, E>;
341
342    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
343        self.project().inner.poll(cx).map(Ok)
344    }
345}
346
347// =============================================================================
348// Result<T, E> implementation
349// =============================================================================
350
351impl<T, E> CacheableResponse for Result<T, E>
352where
353    T: CacheableResponse + 'static,
354    E: Send + 'static,
355    T::Cached: Send,
356{
357    type Cached = <T as CacheableResponse>::Cached;
358    type Subject = T;
359    type IntoCachedFuture = ResultIntoCachedFuture<T, E>;
360    type FromCachedFuture = ResultFromCachedFuture<T, E>;
361
362    async fn cache_policy<P>(
363        self,
364        predicates: P,
365        config: &EntityPolicyConfig,
366    ) -> ResponseCachePolicy<Self>
367    where
368        P: Predicate<Subject = Self::Subject> + Send + Sync,
369    {
370        match self {
371            Ok(response) => match predicates.check(response).await {
372                PredicateResult::Cacheable(cacheable) => match cacheable.into_cached().await {
373                    CachePolicy::Cacheable(res) => CachePolicy::Cacheable(CacheValue::new(
374                        res,
375                        config.ttl.map(|duration| Utc::now() + duration),
376                        config.stale_ttl.map(|duration| Utc::now() + duration),
377                    )),
378                    CachePolicy::NonCacheable(res) => CachePolicy::NonCacheable(Ok(res)),
379                },
380                PredicateResult::NonCacheable(res) => CachePolicy::NonCacheable(Ok(res)),
381            },
382            Err(error) => ResponseCachePolicy::NonCacheable(Err(error)),
383        }
384    }
385
386    fn into_cached(self) -> Self::IntoCachedFuture {
387        match self {
388            Ok(response) => ResultIntoCachedFuture::Ok(response.into_cached()),
389            Err(error) => ResultIntoCachedFuture::Err(Some(error)),
390        }
391    }
392
393    fn from_cached(cached: Self::Cached) -> Self::FromCachedFuture {
394        ResultFromCachedFuture {
395            inner: T::from_cached(cached),
396            _marker: PhantomData,
397        }
398    }
399}