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}