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}