Skip to content

Commit 7b79e6b

Browse files
committed
refactor(core): replace Box<dyn Any> extensions with generic type parameter
1 parent 0a19b0e commit 7b79e6b

5 files changed

Lines changed: 104 additions & 100 deletions

File tree

hitbox-backend/src/composition/context.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,6 @@ impl Context for CompositionContext {
108108
self.inner.set_stored(stored);
109109
}
110110

111-
fn extensions(&self) -> Option<&(dyn Any + Send + Sync)> {
112-
self.inner.extensions()
113-
}
114-
115-
fn set_extensions(&mut self, ext: Option<Box<dyn Any + Send + Sync>>) {
116-
self.inner.set_extensions(ext);
117-
}
118-
119111
fn as_any(&self) -> &dyn Any {
120112
self
121113
}

hitbox-core/src/context.rs

Lines changed: 58 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -192,18 +192,6 @@ pub trait Context: Send + Sync {
192192
// Default implementation does nothing
193193
}
194194

195-
// Protocol extensions
196-
197-
/// Returns a reference to the protocol-specific extension data, if any.
198-
fn extensions(&self) -> Option<&(dyn Any + Send + Sync)> {
199-
None
200-
}
201-
202-
/// Sets protocol-specific extension data.
203-
fn set_extensions(&mut self, _ext: Option<Box<dyn Any + Send + Sync>>) {
204-
// Default implementation does nothing
205-
}
206-
207195
// Type identity and conversion
208196

209197
/// Returns a reference to self as `Any` for downcasting.
@@ -277,9 +265,14 @@ pub fn finalize_context(ctx: BoxContext) -> CacheContext {
277265
/// Context information about a cache operation.
278266
///
279267
/// This is the single source of truth for all cache operation metadata.
280-
/// Protocol-specific data (e.g., HTTP status codes) is stored in [`extensions`](Self::extensions).
281-
#[derive(Debug, Default)]
282-
pub struct CacheContext {
268+
/// The `Ext` type parameter carries protocol-specific data (e.g., HTTP status codes).
269+
///
270+
/// - Inside the FSM: `CacheContext` (= `CacheContext<()>`) — protocol-agnostic
271+
/// - At the integration boundary: `CacheContext<Option<HttpCacheData>>` — protocol-specific
272+
///
273+
/// Use [`with_extensions`](Self::with_extensions) to transform between extension types.
274+
#[derive(Debug, Clone, Default)]
275+
pub struct CacheContext<Ext = ()> {
283276
/// What the cache did with this request (hit, stale, collapsed, or forwarded).
284277
pub status: CacheStatus,
285278
/// Read mode for this operation.
@@ -293,28 +286,10 @@ pub struct CacheContext {
293286
pub stored: bool,
294287
/// Protocol-specific extension data.
295288
///
296-
/// Each protocol crate defines its own struct and stores it here.
297-
/// Costs 8 bytes (null pointer) when unused, one small heap allocation when used.
298-
///
299-
/// Examples:
300-
/// - HTTP: `HttpCacheData { upstream_status: u16 }`
301-
/// - gRPC: `GrpcCacheData { grpc_status: i32 }`
302-
pub extensions: Option<Box<dyn Any + Send + Sync>>,
303-
}
304-
305-
impl Clone for CacheContext {
306-
fn clone(&self) -> Self {
307-
Self {
308-
status: self.status,
309-
read_mode: self.read_mode,
310-
source: self.source.clone(),
311-
timing: self.timing,
312-
stored: self.stored,
313-
// Extensions are protocol-specific and not cloned.
314-
// They are only needed at the header-generation point.
315-
extensions: None,
316-
}
317-
}
289+
/// Defaults to `()` inside the FSM. Protocol crates use their own type:
290+
/// - HTTP: `Option<HttpCacheData>`
291+
/// - gRPC: `Option<GrpcCacheData>`
292+
pub extensions: Ext,
318293
}
319294

320295
impl CacheContext {
@@ -327,6 +302,31 @@ impl CacheContext {
327302
}
328303
}
329304

305+
impl<Ext> CacheContext<Ext> {
306+
/// Transform this context's extension type.
307+
///
308+
/// Copies all protocol-agnostic fields and replaces extensions with the new value.
309+
/// Used at the integration boundary to add protocol-specific data.
310+
///
311+
/// # Example
312+
///
313+
/// ```ignore
314+
/// let ctx: CacheContext = finalize_context(box_ctx); // CacheContext<()>
315+
/// let http_ctx = ctx.with_extensions(Some(HttpCacheData { upstream_status: 200 }));
316+
/// // http_ctx: CacheContext<Option<HttpCacheData>>
317+
/// ```
318+
pub fn with_extensions<NewExt>(self, extensions: NewExt) -> CacheContext<NewExt> {
319+
CacheContext {
320+
status: self.status,
321+
read_mode: self.read_mode,
322+
source: self.source,
323+
timing: self.timing,
324+
stored: self.stored,
325+
extensions,
326+
}
327+
}
328+
}
329+
330330
impl Context for CacheContext {
331331
fn status(&self) -> CacheStatus {
332332
self.status
@@ -368,14 +368,6 @@ impl Context for CacheContext {
368368
self.stored = stored;
369369
}
370370

371-
fn extensions(&self) -> Option<&(dyn Any + Send + Sync)> {
372-
self.extensions.as_deref()
373-
}
374-
375-
fn set_extensions(&mut self, ext: Option<Box<dyn Any + Send + Sync>>) {
376-
self.extensions = ext;
377-
}
378-
379371
fn as_any(&self) -> &dyn Any {
380372
self
381373
}
@@ -393,25 +385,26 @@ impl Context for CacheContext {
393385
///
394386
/// This trait provides a protocol-agnostic way to attach cache status
395387
/// metadata to responses. Each protocol (HTTP, gRPC, etc.) implements
396-
/// this trait with its own configuration type.
397-
///
398-
/// The full [`CacheContext`] is passed, giving implementations access to
399-
/// status, timing, stored flag, and protocol-specific extensions.
388+
/// this trait with its own configuration and extension types.
400389
///
401390
/// # Example
402391
///
403392
/// ```ignore
404393
/// use hitbox_core::{CacheContext, CacheStatusExt};
405394
///
406395
/// // For HTTP responses (implemented in hitbox-http)
407-
/// response.cache_status(&cache_context, &config);
396+
/// let http_ctx = cache_context.with_extensions(Some(http_data));
397+
/// response.cache_status(&http_ctx, &config);
408398
/// ```
409399
pub trait CacheStatusExt {
410400
/// Configuration type for applying cache status (e.g., header name for HTTP).
411401
type Config;
412402

403+
/// Protocol-specific extension type carried by [`CacheContext`].
404+
type Extensions;
405+
413406
/// Applies cache status information to the response.
414-
fn cache_status(&mut self, context: &CacheContext, config: &Self::Config);
407+
fn cache_status(&mut self, context: &CacheContext<Self::Extensions>, config: &Self::Config);
415408
}
416409

417410
#[cfg(test)]
@@ -433,7 +426,7 @@ mod tests {
433426
println!("BoxContext size: {} bytes", box_ctx_size);
434427
println!("S4 inline space: {} bytes", s4_space);
435428

436-
// CacheContext now exceeds S4 due to timing + stored + extensions fields.
429+
// CacheContext now exceeds S4 due to timing + stored fields.
437430
// SmallBox automatically falls back to heap allocation, which is fine
438431
// since context is created once per request.
439432
println!(
@@ -461,4 +454,17 @@ mod tests {
461454
assert!(!CacheStatus::Forward(ForwardReason::Expired).is_served_from_cache());
462455
assert!(!CacheStatus::Forward(ForwardReason::Bypass).is_served_from_cache());
463456
}
457+
458+
#[test]
459+
fn test_with_extensions() {
460+
let ctx: CacheContext = CacheContext {
461+
status: CacheStatus::Hit,
462+
stored: true,
463+
..Default::default()
464+
};
465+
let ext_ctx = ctx.with_extensions(Some(42u16));
466+
assert_eq!(ext_ctx.status, CacheStatus::Hit);
467+
assert!(ext_ctx.stored);
468+
assert_eq!(ext_ctx.extensions, Some(42u16));
469+
}
464470
}

hitbox-http/src/cache_status.rs

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ impl HttpCacheStatusConfig {
8383
/// Formats the RFC 9211 `Cache-Status` header value.
8484
///
8585
/// See: <https://www.rfc-editor.org/rfc/rfc9211>
86-
fn format_cache_status(ctx: &CacheContext, cache_name: &str) -> String {
86+
/// Type alias for the HTTP-specific cache context.
87+
pub type HttpCacheContext = CacheContext<Option<HttpCacheData>>;
88+
89+
fn format_cache_status(ctx: &HttpCacheContext, cache_name: &str) -> String {
8790
let mut buf = String::with_capacity(64);
8891
buf.push_str(cache_name);
8992

@@ -114,9 +117,7 @@ fn format_cache_status(ctx: &CacheContext, cache_name: &str) -> String {
114117
let _ = write!(buf, "; fwd={fwd_value}");
115118

116119
// Add fwd-status from protocol extensions
117-
if let Some(ext) = &ctx.extensions
118-
&& let Some(http_data) = ext.downcast_ref::<HttpCacheData>()
119-
{
120+
if let Some(http_data) = &ctx.extensions {
120121
let _ = write!(buf, "; fwd-status={}", http_data.upstream_status);
121122
}
122123
}
@@ -134,7 +135,7 @@ fn format_cache_status(ctx: &CacheContext, cache_name: &str) -> String {
134135
///
135136
/// Returns `None` if the response wasn't served from cache.
136137
/// Per RFC 9111 §5.1, caches MUST generate an Age header in responses served from cache.
137-
fn compute_age(ctx: &CacheContext) -> Option<u64> {
138+
fn compute_age(ctx: &HttpCacheContext) -> Option<u64> {
138139
if !ctx.status.is_served_from_cache() {
139140
return None;
140141
}
@@ -150,8 +151,9 @@ where
150151
ResBody: HttpBody,
151152
{
152153
type Config = HttpCacheStatusConfig;
154+
type Extensions = Option<HttpCacheData>;
153155

154-
fn cache_status(&mut self, context: &CacheContext, config: &Self::Config) {
156+
fn cache_status(&mut self, context: &HttpCacheContext, config: &Self::Config) {
155157
// RFC 9211: Cache-Status structured header
156158
let cache_status_value = format_cache_status(context, &config.cache_name);
157159
if let Ok(value) = HeaderValue::from_str(&cache_status_value) {
@@ -191,7 +193,7 @@ mod tests {
191193
let created = Utc::now() - Duration::seconds(900);
192194
let expire = Utc::now() + Duration::seconds(2700);
193195

194-
let ctx = CacheContext {
196+
let ctx: HttpCacheContext = CacheContext {
195197
status: CacheStatus::Hit,
196198
timing: Some(CacheTiming {
197199
created_at: created,
@@ -210,7 +212,7 @@ mod tests {
210212

211213
#[test]
212214
fn test_format_cache_miss() {
213-
let ctx = CacheContext {
215+
let ctx: HttpCacheContext = CacheContext {
214216
status: CacheStatus::Forward(ForwardReason::Miss),
215217
stored: true,
216218
..Default::default()
@@ -222,12 +224,12 @@ mod tests {
222224

223225
#[test]
224226
fn test_format_cache_miss_with_fwd_status() {
225-
let ctx = CacheContext {
227+
let ctx: HttpCacheContext = CacheContext {
226228
status: CacheStatus::Forward(ForwardReason::Miss),
227229
stored: true,
228-
extensions: Some(Box::new(HttpCacheData {
230+
extensions: Some(HttpCacheData {
229231
upstream_status: 200,
230-
})),
232+
}),
231233
..Default::default()
232234
};
233235

@@ -237,7 +239,7 @@ mod tests {
237239

238240
#[test]
239241
fn test_format_cache_bypass() {
240-
let ctx = CacheContext {
242+
let ctx: HttpCacheContext = CacheContext {
241243
status: CacheStatus::Forward(ForwardReason::Bypass),
242244
..Default::default()
243245
};
@@ -251,7 +253,7 @@ mod tests {
251253
let created = Utc::now() - Duration::seconds(3720);
252254
let expire = Utc::now() - Duration::seconds(120);
253255

254-
let ctx = CacheContext {
256+
let ctx: HttpCacheContext = CacheContext {
255257
status: CacheStatus::Stale,
256258
timing: Some(CacheTiming {
257259
created_at: created,
@@ -269,7 +271,7 @@ mod tests {
269271

270272
#[test]
271273
fn test_format_collapsed() {
272-
let ctx = CacheContext {
274+
let ctx: HttpCacheContext = CacheContext {
273275
status: CacheStatus::Collapsed,
274276
timing: Some(CacheTiming {
275277
created_at: Utc::now(),
@@ -284,12 +286,12 @@ mod tests {
284286

285287
#[test]
286288
fn test_format_expired_forward() {
287-
let ctx = CacheContext {
289+
let ctx: HttpCacheContext = CacheContext {
288290
status: CacheStatus::Forward(ForwardReason::Expired),
289291
stored: true,
290-
extensions: Some(Box::new(HttpCacheData {
292+
extensions: Some(HttpCacheData {
291293
upstream_status: 200,
292-
})),
294+
}),
293295
..Default::default()
294296
};
295297

@@ -299,7 +301,7 @@ mod tests {
299301

300302
#[test]
301303
fn test_format_custom_cache_name() {
302-
let ctx = CacheContext {
304+
let ctx: HttpCacheContext = CacheContext {
303305
status: CacheStatus::Hit,
304306
..Default::default()
305307
};
@@ -310,7 +312,7 @@ mod tests {
310312

311313
#[test]
312314
fn test_compute_age_cache_hit() {
313-
let ctx = CacheContext {
315+
let ctx: HttpCacheContext = CacheContext {
314316
status: CacheStatus::Hit,
315317
timing: Some(CacheTiming {
316318
created_at: Utc::now() - Duration::seconds(900),
@@ -325,7 +327,7 @@ mod tests {
325327

326328
#[test]
327329
fn test_compute_age_cache_miss_returns_none() {
328-
let ctx = CacheContext {
330+
let ctx: HttpCacheContext = CacheContext {
329331
status: CacheStatus::Forward(ForwardReason::Miss),
330332
..Default::default()
331333
};
@@ -335,7 +337,7 @@ mod tests {
335337

336338
#[test]
337339
fn test_compute_age_no_timing_returns_none() {
338-
let ctx = CacheContext {
340+
let ctx: HttpCacheContext = CacheContext {
339341
status: CacheStatus::Hit,
340342
timing: None,
341343
..Default::default()
@@ -346,12 +348,12 @@ mod tests {
346348

347349
#[test]
348350
fn test_format_upstream_error_not_stored() {
349-
let ctx = CacheContext {
351+
let ctx: HttpCacheContext = CacheContext {
350352
status: CacheStatus::Forward(ForwardReason::Miss),
351353
stored: false,
352-
extensions: Some(Box::new(HttpCacheData {
354+
extensions: Some(HttpCacheData {
353355
upstream_status: 500,
354-
})),
356+
}),
355357
..Default::default()
356358
};
357359

hitbox-reqwest/src/middleware.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,21 +140,23 @@ where
140140
));
141141

142142
// Execute cache future
143-
let (response, mut cache_context) = cache_future.await;
143+
let (response, cache_context) = cache_future.await;
144144

145145
// Convert CacheableHttpResponse back to reqwest::Response
146146
let mut cacheable_response = response?;
147147

148148
// Set HTTP-specific extension data (upstream status code)
149-
if matches!(cache_context.status, CacheStatus::Forward(_)) {
150-
let status_code = cacheable_response.parts.status.as_u16();
151-
cache_context.extensions = Some(Box::new(HttpCacheData {
152-
upstream_status: status_code,
153-
}));
154-
}
149+
let http_ext = if matches!(cache_context.status, CacheStatus::Forward(_)) {
150+
Some(HttpCacheData {
151+
upstream_status: cacheable_response.parts.status.as_u16(),
152+
})
153+
} else {
154+
None
155+
};
156+
let http_ctx = cache_context.with_extensions(http_ext);
155157

156158
// Add cache status headers (RFC 9211 Cache-Status, Age, legacy x-cache-status)
157-
cacheable_response.cache_status(&cache_context, &self.cache_status_config);
159+
cacheable_response.cache_status(&http_ctx, &self.cache_status_config);
158160

159161
let http_response = cacheable_response.into_response();
160162
let (parts, buffered_body) = http_response.into_parts();

0 commit comments

Comments
 (0)