praxis_filter/
extensions.rs1use std::{any::Any, collections::HashMap};
23
24#[derive(Default)]
36pub struct RequestExtensions(HashMap<std::any::TypeId, Box<dyn Any + Send + Sync>>);
37
38impl RequestExtensions {
39 pub fn new() -> Self {
41 Self::default()
42 }
43
44 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 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 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 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 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#[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}