Skip to main content

hitbox_core/predicate/
mod.rs

1//! Caching decision predicates.
2//!
3//! This module provides the [`Predicate`] trait and [`PredicateResult`] enum
4//! for determining whether requests and responses should be cached.
5//!
6//! ## Overview
7//!
8//! Predicates are the core mechanism for controlling cache behavior. They
9//! evaluate a subject (request or response) and return whether it should
10//! be cached or passed through without caching.
11//!
12//! ## Composability
13//!
14//! Predicates are designed to be composed using logical combinators:
15//!
16//! - [`Not`] - Inverts a predicate result
17//! - [`Or`] - Either predicate returning `Cacheable` is sufficient
18//!
19//! Chaining predicates sequentially provides AND semantics by default.
20
21pub mod combinators;
22pub mod neutral;
23
24use std::sync::Arc;
25
26use async_trait::async_trait;
27
28pub use combinators::{And, Not, Or, PredicateExt};
29pub use neutral::Neutral;
30
31/// Result of a predicate evaluation.
32///
33/// Indicates whether the subject should be cached or not, while preserving
34/// ownership of the subject for further processing.
35#[derive(Debug)]
36pub enum PredicateResult<S> {
37    /// Subject should be cached.
38    Cacheable(S),
39    /// Subject should not be cached; pass through directly.
40    NonCacheable(S),
41}
42
43impl<S> PredicateResult<S> {
44    /// Chains predicate checks.
45    ///
46    /// If `Cacheable`, applies the function which may return `Cacheable` or
47    /// `NonCacheable`. If already `NonCacheable`, short-circuits and stays
48    /// `NonCacheable` without calling the function.
49    ///
50    /// This enables predicate chaining where `NonCacheable` is "sticky":
51    ///
52    /// ```ignore
53    /// predicate1.check(request).await
54    ///     .and_then(|req| predicate2.check(req)).await
55    ///     .and_then(|req| predicate3.check(req)).await
56    /// ```
57    pub async fn and_then<F, Fut>(self, f: F) -> PredicateResult<S>
58    where
59        F: FnOnce(S) -> Fut,
60        Fut: std::future::Future<Output = PredicateResult<S>>,
61    {
62        match self {
63            PredicateResult::Cacheable(value) => f(value).await,
64            PredicateResult::NonCacheable(value) => PredicateResult::NonCacheable(value),
65        }
66    }
67}
68
69/// Trait for evaluating whether a subject should be cached.
70///
71/// Predicates are the core abstraction for cache decision logic. They are
72/// **protocol-agnostic** - the same trait works for HTTP requests, gRPC
73/// messages, or any other protocol type.
74///
75/// # Type Parameters
76///
77/// The `Subject` associated type defines what this predicate evaluates.
78/// For request predicates, this is typically a request type. For response
79/// predicates, this is typically a response type.
80///
81/// # Ownership
82///
83/// The `check` method takes ownership of the subject and returns it wrapped
84/// in a [`PredicateResult`]. This allows the subject to flow through a chain
85/// of predicates without cloning.
86///
87#[async_trait]
88pub trait Predicate {
89    /// The type being evaluated by this predicate.
90    type Subject;
91
92    /// Evaluate whether the subject should be cached.
93    ///
94    /// Returns [`PredicateResult::Cacheable`] if the subject should be cached,
95    /// or [`PredicateResult::NonCacheable`] if it should bypass the cache.
96    async fn check(&self, subject: Self::Subject) -> PredicateResult<Self::Subject>;
97}
98
99#[async_trait]
100impl<T> Predicate for Box<T>
101where
102    T: Predicate + ?Sized + Sync,
103    T::Subject: Send,
104{
105    type Subject = T::Subject;
106
107    async fn check(&self, subject: T::Subject) -> PredicateResult<T::Subject> {
108        self.as_ref().check(subject).await
109    }
110}
111
112#[async_trait]
113impl<T> Predicate for &T
114where
115    T: Predicate + ?Sized + Sync,
116    T::Subject: Send,
117{
118    type Subject = T::Subject;
119
120    async fn check(&self, subject: T::Subject) -> PredicateResult<T::Subject> {
121        (*self).check(subject).await
122    }
123}
124
125#[async_trait]
126impl<T> Predicate for Arc<T>
127where
128    T: Predicate + Send + Sync + ?Sized,
129    T::Subject: Send,
130{
131    type Subject = T::Subject;
132
133    async fn check(&self, subject: T::Subject) -> PredicateResult<T::Subject> {
134        self.as_ref().check(subject).await
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[tokio::test]
143    async fn test_predicate_ext_with_box_dyn() {
144        let p1: Box<dyn Predicate<Subject = i32> + Send + Sync> = Box::new(Neutral::<i32>::new());
145        let p2: Box<dyn Predicate<Subject = i32> + Send + Sync> = Box::new(Neutral::<i32>::new());
146
147        // PredicateExt works on Box<dyn Predicate> because Box<T> is Sized
148        let combined = p1.or(p2);
149
150        let result = combined.check(42).await;
151        assert!(matches!(result, PredicateResult::Cacheable(42)));
152    }
153
154    #[tokio::test]
155    async fn test_predicate_ext_chaining_with_box_dyn() {
156        let p1: Box<dyn Predicate<Subject = i32> + Send + Sync> = Box::new(Neutral::<i32>::new());
157        let p2: Box<dyn Predicate<Subject = i32> + Send + Sync> = Box::new(Neutral::<i32>::new());
158        let p3: Box<dyn Predicate<Subject = i32> + Send + Sync> = Box::new(Neutral::<i32>::new());
159
160        // Chain: p1.and(p2).or(p3).not()
161        let combined = p1.and(p2).or(p3).not();
162
163        let result = combined.check(42).await;
164        // Neutral returns Cacheable, so: Cacheable AND Cacheable = Cacheable, OR Cacheable = Cacheable, NOT = NonCacheable
165        assert!(matches!(result, PredicateResult::NonCacheable(42)));
166    }
167
168    #[tokio::test]
169    async fn test_predicate_ext_boxed() {
170        // Use .boxed() for type erasure
171        let p1 = Neutral::<i32>::new().boxed();
172        let p2 = Neutral::<i32>::new().boxed();
173
174        // Can chain after boxing
175        let combined = p1.or(p2);
176
177        let result = combined.check(42).await;
178        assert!(matches!(result, PredicateResult::Cacheable(42)));
179    }
180
181    #[tokio::test]
182    async fn test_predicate_ext_boxed_in_vec() {
183        // Store heterogeneous predicates in a Vec
184        let predicates: Vec<Box<dyn Predicate<Subject = i32> + Send + Sync>> = vec![
185            Neutral::<i32>::new().boxed(),
186            Neutral::<i32>::new().not().boxed(),
187        ];
188
189        let result1 = predicates[0].check(1).await;
190        let result2 = predicates[1].check(2).await;
191
192        assert!(matches!(result1, PredicateResult::Cacheable(1)));
193        assert!(matches!(result2, PredicateResult::NonCacheable(2)));
194    }
195}