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/// ```ignore
93/// use hitbox_core::{CacheableResponse, CachePolicy, EntityPolicyConfig};
94/// use hitbox_core::predicate::Predicate;
95///
96/// struct MyResponse {
97///     body: String,
98///     status: u16,
99/// }
100///
101/// impl CacheableResponse for MyResponse {
102///     type Cached = String;
103///     type Subject = Self;
104///     type IntoCachedFuture = std::future::Ready<CachePolicy<String, Self>>;
105///     type FromCachedFuture = std::future::Ready<Self>;
106///
107///     async fn cache_policy<P>(
108///         self,
109///         predicates: P,
110///         config: &EntityPolicyConfig,
111///     ) -> ResponseCachePolicy<Self>
112///     where
113///         P: Predicate<Subject = Self::Subject> + Send + Sync
114///     {
115///         // Implementation details...
116///     }
117///
118///     fn into_cached(self) -> Self::IntoCachedFuture {
119///         std::future::ready(CachePolicy::Cacheable(self.body))
120///     }
121///
122///     fn from_cached(cached: String) -> Self::FromCachedFuture {
123///         std::future::ready(MyResponse { body: cached, status: 200 })
124///     }
125/// }
126/// ```
127pub trait CacheableResponse
128where
129    Self: Sized + Send + 'static,
130    Self::Cached: Clone,
131{
132    /// The serializable type stored in cache.
133    type Cached;
134
135    /// The type that response predicates evaluate.
136    ///
137    /// For simple responses, this is `Self`. For wrapper types like `Result<T, E>`,
138    /// this is the inner type `T`.
139    type Subject: CacheableResponse;
140
141    /// Future type for `into_cached` method.
142    type IntoCachedFuture: Future<Output = CachePolicy<Self::Cached, Self>> + Send;
143
144    /// Future type for `from_cached` method.
145    type FromCachedFuture: Future<Output = Self> + Send;
146
147    /// Determine if this response should be cached.
148    ///
149    /// Applies predicates to determine cacheability, then converts cacheable
150    /// responses to their cached representation with TTL metadata.
151    ///
152    /// # Arguments
153    ///
154    /// * `predicates` - Predicates to evaluate whether the response is cacheable
155    /// * `config` - TTL configuration for the cached entry
156    fn cache_policy<P>(
157        self,
158        predicates: P,
159        config: &EntityPolicyConfig,
160    ) -> impl Future<Output = ResponseCachePolicy<Self>> + Send
161    where
162        P: Predicate<Subject = Self::Subject> + Send + Sync;
163
164    /// Convert this response to its cached representation.
165    ///
166    /// Returns `Cacheable` with the serializable data, or `NonCacheable`
167    /// if the response should not be cached.
168    fn into_cached(self) -> Self::IntoCachedFuture;
169
170    /// Reconstruct a response from cached data.
171    ///
172    /// Creates a new response instance from previously cached data.
173    fn from_cached(cached: Self::Cached) -> Self::FromCachedFuture;
174}
175
176// =============================================================================
177// Result<T, E> wrapper futures
178// =============================================================================
179
180#[doc(hidden)]
181#[pin_project(project = ResultIntoCachedProj)]
182pub enum ResultIntoCachedFuture<T, E>
183where
184    T: CacheableResponse,
185{
186    /// Ok variant - wraps the inner type's future
187    Ok(#[pin] T::IntoCachedFuture),
188    /// Err variant - contains the error to return immediately
189    Err(Option<E>),
190}
191
192impl<T, E> Future for ResultIntoCachedFuture<T, E>
193where
194    T: CacheableResponse,
195{
196    type Output = CachePolicy<T::Cached, Result<T, E>>;
197
198    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
199        match self.project() {
200            ResultIntoCachedProj::Ok(fut) => fut.poll(cx).map(|policy| match policy {
201                CachePolicy::Cacheable(res) => CachePolicy::Cacheable(res),
202                CachePolicy::NonCacheable(res) => CachePolicy::NonCacheable(Ok(res)),
203            }),
204            ResultIntoCachedProj::Err(e) => Poll::Ready(CachePolicy::NonCacheable(Err(e
205                .take()
206                .expect(POLL_AFTER_READY_ERROR)))),
207        }
208    }
209}
210
211#[doc(hidden)]
212#[pin_project]
213pub struct ResultFromCachedFuture<T, E>
214where
215    T: CacheableResponse,
216{
217    #[pin]
218    inner: T::FromCachedFuture,
219    _marker: PhantomData<E>,
220}
221
222impl<T, E> Future for ResultFromCachedFuture<T, E>
223where
224    T: CacheableResponse,
225{
226    type Output = Result<T, E>;
227
228    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
229        self.project().inner.poll(cx).map(Ok)
230    }
231}
232
233// =============================================================================
234// Result<T, E> implementation
235// =============================================================================
236
237impl<T, E> CacheableResponse for Result<T, E>
238where
239    T: CacheableResponse + 'static,
240    E: Send + 'static,
241    T::Cached: Send,
242{
243    type Cached = <T as CacheableResponse>::Cached;
244    type Subject = T;
245    type IntoCachedFuture = ResultIntoCachedFuture<T, E>;
246    type FromCachedFuture = ResultFromCachedFuture<T, E>;
247
248    async fn cache_policy<P>(
249        self,
250        predicates: P,
251        config: &EntityPolicyConfig,
252    ) -> ResponseCachePolicy<Self>
253    where
254        P: Predicate<Subject = Self::Subject> + Send + Sync,
255    {
256        match self {
257            Ok(response) => match predicates.check(response).await {
258                PredicateResult::Cacheable(cacheable) => match cacheable.into_cached().await {
259                    CachePolicy::Cacheable(res) => CachePolicy::Cacheable(CacheValue::new(
260                        res,
261                        config.ttl.map(|duration| Utc::now() + duration),
262                        config.stale_ttl.map(|duration| Utc::now() + duration),
263                    )),
264                    CachePolicy::NonCacheable(res) => CachePolicy::NonCacheable(Ok(res)),
265                },
266                PredicateResult::NonCacheable(res) => CachePolicy::NonCacheable(Ok(res)),
267            },
268            Err(error) => ResponseCachePolicy::NonCacheable(Err(error)),
269        }
270    }
271
272    fn into_cached(self) -> Self::IntoCachedFuture {
273        match self {
274            Ok(response) => ResultIntoCachedFuture::Ok(response.into_cached()),
275            Err(error) => ResultIntoCachedFuture::Err(Some(error)),
276        }
277    }
278
279    fn from_cached(cached: Self::Cached) -> Self::FromCachedFuture {
280        ResultFromCachedFuture {
281            inner: T::from_cached(cached),
282            _marker: PhantomData,
283        }
284    }
285}