Skip to content

Commit 75c8bce

Browse files
committed
feat(core): add EvalContext type-map for predicate and extractor evaluation
1 parent 7e77f7f commit 75c8bce

53 files changed

Lines changed: 845 additions & 279 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

hitbox-configuration/tests/test_request.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use hitbox_configuration::{
77
},
88
types::MaybeUndefined,
99
};
10+
use hitbox_core::EvalContext;
1011
use hitbox_http::predicates::NeutralRequestPredicate;
1112
use hitbox_http::{BufferedBody, CacheableHttpRequest};
1213
use http::Request as HttpRequest;
@@ -57,7 +58,7 @@ async fn test_expression_into_predicates() {
5758
.body(BufferedBody::Passthrough(Empty::<Bytes>::new()))
5859
.unwrap(),
5960
);
60-
let cacheable = predicate_or.check(request).await;
61+
let cacheable = predicate_or.check(request, &mut EvalContext::new()).await;
6162
assert!(matches!(cacheable, PredicateResult::NonCacheable(_)));
6263
}
6364

@@ -132,7 +133,7 @@ async fn test_or_with_matching_first_predicate() {
132133
.body(BufferedBody::Passthrough(Empty::<Bytes>::new()))
133134
.unwrap(),
134135
);
135-
let cacheable = predicate_or.check(request).await;
136+
let cacheable = predicate_or.check(request, &mut EvalContext::new()).await;
136137
assert!(matches!(cacheable, PredicateResult::Cacheable(_)));
137138
}
138139

@@ -154,7 +155,7 @@ async fn test_or_with_matching_middle_predicate() {
154155
.body(BufferedBody::Passthrough(Empty::<Bytes>::new()))
155156
.unwrap(),
156157
);
157-
let cacheable = predicate_or.check(request).await;
158+
let cacheable = predicate_or.check(request, &mut EvalContext::new()).await;
158159
assert!(matches!(cacheable, PredicateResult::Cacheable(_)));
159160
}
160161

@@ -176,7 +177,7 @@ async fn test_or_with_matching_last_predicate() {
176177
.body(BufferedBody::Passthrough(Empty::<Bytes>::new()))
177178
.unwrap(),
178179
);
179-
let cacheable = predicate_or.check(request).await;
180+
let cacheable = predicate_or.check(request, &mut EvalContext::new()).await;
180181
assert!(matches!(cacheable, PredicateResult::Cacheable(_)));
181182
}
182183

@@ -198,7 +199,7 @@ async fn test_or_with_no_matching_predicates() {
198199
.body(BufferedBody::Passthrough(Empty::<Bytes>::new()))
199200
.unwrap(),
200201
);
201-
let cacheable = predicate_or.check(request).await;
202+
let cacheable = predicate_or.check(request, &mut EvalContext::new()).await;
202203
assert!(matches!(cacheable, PredicateResult::NonCacheable(_)));
203204
}
204205

@@ -216,7 +217,7 @@ async fn test_or_with_single_predicate_matching() {
216217
.body(BufferedBody::Passthrough(Empty::<Bytes>::new()))
217218
.unwrap(),
218219
);
219-
let cacheable = predicate_or.check(request).await;
220+
let cacheable = predicate_or.check(request, &mut EvalContext::new()).await;
220221
assert!(matches!(cacheable, PredicateResult::Cacheable(_)));
221222
}
222223

@@ -234,7 +235,7 @@ async fn test_or_with_single_predicate_not_matching() {
234235
.body(BufferedBody::Passthrough(Empty::<Bytes>::new()))
235236
.unwrap(),
236237
);
237-
let cacheable = predicate_or.check(request).await;
238+
let cacheable = predicate_or.check(request, &mut EvalContext::new()).await;
238239
assert!(matches!(cacheable, PredicateResult::NonCacheable(_)));
239240
}
240241

@@ -256,7 +257,7 @@ async fn test_or_with_mixed_predicate_types() {
256257
.body(BufferedBody::Passthrough(Empty::<Bytes>::new()))
257258
.unwrap(),
258259
);
259-
let cacheable = predicate_or.check(request).await;
260+
let cacheable = predicate_or.check(request, &mut EvalContext::new()).await;
260261
assert!(matches!(cacheable, PredicateResult::Cacheable(_)));
261262
}
262263

hitbox-core/src/eval_context.rs

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
//! Evaluation context for predicates and extractors.
2+
//!
3+
//! [`EvalContext`] is a type-map that allows predicates and extractors to share
4+
//! computed values during a single evaluation phase. This avoids redundant
5+
//! expensive operations (e.g., deserializing a protobuf body into a
6+
//! `DynamicMessage`) when multiple predicates or extractors need the same data.
7+
//!
8+
//! ## Usage
9+
//!
10+
//! ```rust
11+
//! use hitbox_core::EvalContext;
12+
//!
13+
//! struct ParsedProto(String);
14+
//!
15+
//! let mut ctx = EvalContext::new();
16+
//! ctx.insert(ParsedProto("hello".into()));
17+
//!
18+
//! assert!(ctx.contains::<ParsedProto>());
19+
//! assert_eq!(ctx.get::<ParsedProto>().unwrap().0, "hello");
20+
//! ```
21+
//!
22+
//! ## Lifecycle
23+
//!
24+
//! An `EvalContext` is created inside each `cache_policy` implementation:
25+
//! one for the request phase (shared by request predicates and extractors)
26+
//! and another for the response phase (used by response predicates).
27+
28+
use std::any::{Any, TypeId};
29+
use std::collections::HashMap;
30+
31+
/// A type-map for sharing computed values across predicates and extractors.
32+
///
33+
/// Each value is keyed by its concrete type (`TypeId`), so only one value
34+
/// of each type can be stored. Use newtype wrappers to store multiple
35+
/// values of the same underlying type.
36+
///
37+
/// # Thread Safety
38+
///
39+
/// `EvalContext` is `Send + Sync` because all stored values must be
40+
/// `Send + Sync + 'static`. It is passed as `&mut EvalContext` through
41+
/// the evaluation chain, which is always sequential.
42+
pub struct EvalContext {
43+
map: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
44+
}
45+
46+
impl EvalContext {
47+
/// Creates an empty evaluation context.
48+
pub fn new() -> Self {
49+
Self {
50+
map: HashMap::new(),
51+
}
52+
}
53+
54+
/// Inserts a value into the context.
55+
///
56+
/// If a value of this type already exists, it is replaced and the old
57+
/// value is returned.
58+
pub fn insert<T: Send + Sync + 'static>(&mut self, val: T) -> Option<T> {
59+
self.map
60+
.insert(TypeId::of::<T>(), Box::new(val))
61+
.and_then(|boxed| boxed.downcast().ok().map(|b| *b))
62+
}
63+
64+
/// Returns a reference to a value of the given type, if present.
65+
pub fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
66+
self.map
67+
.get(&TypeId::of::<T>())
68+
.and_then(|boxed| boxed.downcast_ref())
69+
}
70+
71+
/// Returns a mutable reference to a value of the given type, if present.
72+
pub fn get_mut<T: Send + Sync + 'static>(&mut self) -> Option<&mut T> {
73+
self.map
74+
.get_mut(&TypeId::of::<T>())
75+
.and_then(|boxed| boxed.downcast_mut())
76+
}
77+
78+
/// Returns a mutable reference to a value of the given type, inserting
79+
/// a default computed by `f` if not present.
80+
///
81+
/// This is the primary method for expensive lazy initialization:
82+
///
83+
/// ```ignore
84+
/// let msg = ctx.get_or_insert_with(|| {
85+
/// ParsedProto(DynamicMessage::decode(descriptor, body_bytes).unwrap())
86+
/// });
87+
/// ```
88+
pub fn get_or_insert_with<T: Send + Sync + 'static>(
89+
&mut self,
90+
f: impl FnOnce() -> T,
91+
) -> &mut T {
92+
self.map
93+
.entry(TypeId::of::<T>())
94+
.or_insert_with(|| Box::new(f()))
95+
.downcast_mut()
96+
.expect("type mismatch in EvalContext (this is a bug)")
97+
}
98+
99+
/// Returns `true` if the context contains a value of the given type.
100+
pub fn contains<T: Send + Sync + 'static>(&self) -> bool {
101+
self.map.contains_key(&TypeId::of::<T>())
102+
}
103+
104+
/// Removes and returns a value of the given type, if present.
105+
pub fn remove<T: Send + Sync + 'static>(&mut self) -> Option<T> {
106+
self.map
107+
.remove(&TypeId::of::<T>())
108+
.and_then(|boxed| boxed.downcast().ok().map(|b| *b))
109+
}
110+
}
111+
112+
impl Default for EvalContext {
113+
fn default() -> Self {
114+
Self::new()
115+
}
116+
}
117+
118+
impl std::fmt::Debug for EvalContext {
119+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120+
f.debug_struct("EvalContext")
121+
.field("entries", &self.map.len())
122+
.finish()
123+
}
124+
}
125+
126+
#[cfg(test)]
127+
mod tests {
128+
use super::*;
129+
130+
struct StringValue(String);
131+
struct Counter(u32);
132+
133+
#[test]
134+
fn test_insert_and_get() {
135+
let mut ctx = EvalContext::new();
136+
ctx.insert(StringValue("hello".into()));
137+
138+
assert!(ctx.contains::<StringValue>());
139+
assert_eq!(ctx.get::<StringValue>().unwrap().0, "hello");
140+
}
141+
142+
#[test]
143+
fn test_insert_replaces_and_returns_old() {
144+
let mut ctx = EvalContext::new();
145+
assert!(ctx.insert(Counter(1)).is_none());
146+
let old = ctx.insert(Counter(2));
147+
assert_eq!(old.unwrap().0, 1);
148+
assert_eq!(ctx.get::<Counter>().unwrap().0, 2);
149+
}
150+
151+
#[test]
152+
fn test_get_mut() {
153+
let mut ctx = EvalContext::new();
154+
ctx.insert(Counter(0));
155+
ctx.get_mut::<Counter>().unwrap().0 += 1;
156+
assert_eq!(ctx.get::<Counter>().unwrap().0, 1);
157+
}
158+
159+
#[test]
160+
fn test_get_or_insert_with() {
161+
let mut ctx = EvalContext::new();
162+
163+
// First call inserts
164+
let val = ctx.get_or_insert_with(|| Counter(42));
165+
assert_eq!(val.0, 42);
166+
167+
// Second call returns existing
168+
let val = ctx.get_or_insert_with(|| Counter(99));
169+
assert_eq!(val.0, 42);
170+
}
171+
172+
#[test]
173+
fn test_remove() {
174+
let mut ctx = EvalContext::new();
175+
ctx.insert(Counter(10));
176+
let removed = ctx.remove::<Counter>();
177+
assert_eq!(removed.unwrap().0, 10);
178+
assert!(!ctx.contains::<Counter>());
179+
}
180+
181+
#[test]
182+
fn test_missing_type_returns_none() {
183+
let ctx = EvalContext::new();
184+
assert!(ctx.get::<Counter>().is_none());
185+
}
186+
187+
#[test]
188+
fn test_multiple_types() {
189+
let mut ctx = EvalContext::new();
190+
ctx.insert(StringValue("a".into()));
191+
ctx.insert(Counter(1));
192+
193+
assert_eq!(ctx.get::<StringValue>().unwrap().0, "a");
194+
assert_eq!(ctx.get::<Counter>().unwrap().0, 1);
195+
}
196+
197+
#[test]
198+
fn test_default() {
199+
let ctx = EvalContext::default();
200+
assert!(!ctx.contains::<Counter>());
201+
}
202+
}

hitbox-core/src/extractor.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use std::sync::Arc;
3939

4040
use async_trait::async_trait;
4141

42+
use crate::EvalContext;
4243
use crate::KeyParts;
4344

4445
/// Trait for extracting cache key components from a subject.
@@ -73,7 +74,7 @@ pub trait Extractor {
7374
/// Extract cache key components from the subject.
7475
///
7576
/// Returns a [`KeyParts`] containing the subject and accumulated key parts.
76-
async fn get(&self, subject: Self::Subject) -> KeyParts<Self::Subject>;
77+
async fn get(&self, subject: Self::Subject, ctx: &mut EvalContext) -> KeyParts<Self::Subject>;
7778
}
7879

7980
#[async_trait]
@@ -84,8 +85,8 @@ where
8485
{
8586
type Subject = T::Subject;
8687

87-
async fn get(&self, subject: T::Subject) -> KeyParts<T::Subject> {
88-
self.get(subject).await
88+
async fn get(&self, subject: T::Subject, ctx: &mut EvalContext) -> KeyParts<T::Subject> {
89+
(**self).get(subject, ctx).await
8990
}
9091
}
9192

@@ -97,8 +98,8 @@ where
9798
{
9899
type Subject = T::Subject;
99100

100-
async fn get(&self, subject: T::Subject) -> KeyParts<T::Subject> {
101-
self.as_ref().get(subject).await
101+
async fn get(&self, subject: T::Subject, ctx: &mut EvalContext) -> KeyParts<T::Subject> {
102+
self.as_ref().get(subject, ctx).await
102103
}
103104
}
104105

@@ -110,7 +111,7 @@ where
110111
{
111112
type Subject = T::Subject;
112113

113-
async fn get(&self, subject: T::Subject) -> KeyParts<T::Subject> {
114-
self.as_ref().get(subject).await
114+
async fn get(&self, subject: T::Subject, ctx: &mut EvalContext) -> KeyParts<T::Subject> {
115+
self.as_ref().get(subject, ctx).await
115116
}
116117
}

hitbox-core/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
pub mod cacheable;
55
pub mod config;
66
pub mod context;
7+
pub mod eval_context;
78
pub mod extractor;
89
pub mod key;
910
pub mod label;
@@ -21,6 +22,7 @@ pub use context::{
2122
BoxContext, CacheContext, CacheStatus, CacheStatusExt, Context, ReadMode, ResponseSource,
2223
finalize_context,
2324
};
25+
pub use eval_context::EvalContext;
2426
pub use extractor::Extractor;
2527
pub use key::{CacheKey, KeyPart, KeyParts};
2628
pub use label::BackendLabel;

0 commit comments

Comments
 (0)