Skip to main content

praxis_filter/
extensions.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Praxis Contributors
3
4//! Type-safe, request-scoped extension container.
5//!
6//! [`RequestExtensions`] is a type-map keyed by [`TypeId`]. Filters
7//! store and retrieve arbitrary typed values that persist across all
8//! Pingora lifecycle phases (request, request body, response,
9//! response body, logging).
10//!
11//! The framework has no knowledge of what filters store in it. The
12//! cost when unused is an empty [`HashMap`] (zero allocations, no
13//! overhead on existing filter chains).
14//!
15//! Only one value per concrete type can be stored. Filters must use
16//! private newtypes for their state, not bare types like
17//! [`serde_json::Value`] or `Vec<String>`, to avoid overwriting
18//! each other's data.
19//!
20//! [`TypeId`]: std::any::TypeId
21
22use std::{any::Any, collections::HashMap};
23
24// -----------------------------------------------------------------------------
25// RequestExtensions
26// -----------------------------------------------------------------------------
27
28/// Type-safe, request-scoped extension container.
29///
30/// Keyed by [`TypeId`], so only one value per concrete type is
31/// stored. Use private newtypes to avoid collisions between
32/// independent filters.
33///
34/// [`TypeId`]: std::any::TypeId
35#[derive(Default)]
36pub struct RequestExtensions(HashMap<std::any::TypeId, Box<dyn Any + Send + Sync>>);
37
38impl RequestExtensions {
39    /// Create an empty extension container.
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Insert a typed value, replacing any previous value of the same type.
45    pub fn insert<T: Send + Sync + 'static>(&mut self, val: T) {
46        self.0.insert(std::any::TypeId::of::<T>(), Box::new(val));
47    }
48
49    /// Get a shared reference to a stored value by type.
50    pub fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
51        self.0
52            .get(&std::any::TypeId::of::<T>())
53            .and_then(|boxed| boxed.downcast_ref())
54    }
55
56    /// Get an exclusive reference to a stored value by type.
57    pub fn get_mut<T: Send + Sync + 'static>(&mut self) -> Option<&mut T> {
58        self.0
59            .get_mut(&std::any::TypeId::of::<T>())
60            .and_then(|boxed| boxed.downcast_mut())
61    }
62
63    /// Get an exclusive reference to a stored value, inserting a
64    /// default computed by `f` if absent.
65    ///
66    /// # Panics
67    ///
68    /// Cannot panic in practice: the value was just inserted with
69    /// the correct type. The `expect` guards against impossible
70    /// `TypeId` collisions in the standard library.
71    pub fn get_or_insert_with<T: Send + Sync + 'static>(&mut self, f: impl FnOnce() -> T) -> &mut T {
72        #[expect(clippy::expect_used, reason = "downcast cannot fail after typed insert")]
73        self.0
74            .entry(std::any::TypeId::of::<T>())
75            .or_insert_with(|| Box::new(f()))
76            .downcast_mut()
77            .expect("type mismatch after insert")
78    }
79
80    /// Remove a stored value by type, returning it if present.
81    pub fn remove<T: Send + Sync + 'static>(&mut self) -> Option<T> {
82        self.0
83            .remove(&std::any::TypeId::of::<T>())
84            .and_then(|boxed| boxed.downcast().ok())
85            .map(|boxed| *boxed)
86    }
87}
88
89// -----------------------------------------------------------------------------
90// Tests
91// -----------------------------------------------------------------------------
92
93#[cfg(test)]
94#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
95#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, reason = "tests")]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn default_is_empty() {
101        let ext = RequestExtensions::default();
102        assert!(ext.get::<String>().is_none(), "default should contain no values");
103    }
104
105    #[test]
106    fn insert_and_get() {
107        let mut ext = RequestExtensions::new();
108        ext.insert(42_u32);
109        assert_eq!(ext.get::<u32>(), Some(&42), "should retrieve inserted value");
110    }
111
112    #[test]
113    fn insert_and_get_mut() {
114        let mut ext = RequestExtensions::new();
115        ext.insert("hello".to_owned());
116        if let Some(val) = ext.get_mut::<String>() {
117            val.push_str(" world");
118        }
119        assert_eq!(
120            ext.get::<String>().map(String::as_str),
121            Some("hello world"),
122            "get_mut should allow mutation"
123        );
124    }
125
126    #[test]
127    fn multiple_types_coexist() {
128        let mut ext = RequestExtensions::new();
129        ext.insert(1_u32);
130        ext.insert("text".to_owned());
131        ext.insert(1.5_f64);
132        assert_eq!(ext.get::<u32>(), Some(&1), "u32 should be present");
133        assert_eq!(
134            ext.get::<String>().map(String::as_str),
135            Some("text"),
136            "String should be present"
137        );
138        assert_eq!(ext.get::<f64>(), Some(&1.5), "f64 should be present");
139    }
140
141    #[test]
142    fn insert_same_type_overwrites() {
143        let mut ext = RequestExtensions::new();
144        ext.insert(1_u32);
145        ext.insert(2_u32);
146        assert_eq!(ext.get::<u32>(), Some(&2), "second insert should overwrite first");
147    }
148
149    #[test]
150    fn remove_returns_owned_value() {
151        let mut ext = RequestExtensions::new();
152        ext.insert(99_u32);
153        let removed = ext.remove::<u32>();
154        assert_eq!(removed, Some(99), "remove should return the stored value");
155        assert!(ext.get::<u32>().is_none(), "value should be gone after remove");
156    }
157
158    #[test]
159    fn remove_absent_returns_none() {
160        let mut ext = RequestExtensions::new();
161        assert!(ext.remove::<u32>().is_none(), "removing absent type should return None");
162    }
163
164    #[test]
165    fn get_or_insert_with_creates_when_absent() {
166        let mut ext = RequestExtensions::new();
167        let val = ext.get_or_insert_with(|| 42_u32);
168        assert_eq!(*val, 42, "should create value when absent");
169    }
170
171    #[test]
172    fn get_or_insert_with_returns_existing() {
173        let mut ext = RequestExtensions::new();
174        ext.insert(10_u32);
175        let val = ext.get_or_insert_with(|| 42_u32);
176        assert_eq!(*val, 10, "should return existing value without calling factory");
177    }
178
179    #[test]
180    fn get_wrong_type_returns_none() {
181        let mut ext = RequestExtensions::new();
182        ext.insert(42_u32);
183        assert!(ext.get::<String>().is_none(), "wrong type should return None");
184    }
185
186    #[test]
187    fn newtypes_are_independent() {
188        struct FilterAState(u32);
189        struct FilterBState(u32);
190
191        let mut ext = RequestExtensions::new();
192        ext.insert(FilterAState(1));
193        ext.insert(FilterBState(2));
194
195        assert_eq!(
196            ext.get::<FilterAState>().map(|s| s.0),
197            Some(1),
198            "FilterAState should be 1"
199        );
200        assert_eq!(
201            ext.get::<FilterBState>().map(|s| s.0),
202            Some(2),
203            "FilterBState should be 2"
204        );
205    }
206}