1use std::sync::Arc;
7
8use serde::de::DeserializeOwned;
9
10use crate::{
11 any_filter::AnyFilter,
12 filter::{FilterError, HttpFilter},
13 tcp_filter::TcpFilter,
14};
15
16pub fn parse_filter_config<T: DeserializeOwned>(name: &str, config: &serde_yaml::Value) -> Result<T, FilterError> {
43 let cleaned = strip_structural_keys(config);
44 serde_yaml::from_value(cleaned).map_err(|e| -> FilterError { format!("{name}: {e}").into() })
45}
46
47fn strip_structural_keys(config: &serde_yaml::Value) -> serde_yaml::Value {
56 const STRUCTURAL: &[&str] = &[
57 "branch_chains",
58 "conditions",
59 "failure_mode",
60 "filter",
61 "name",
62 "response_conditions",
63 ];
64
65 let Some(mapping) = config.as_mapping() else {
66 return config.clone();
67 };
68
69 let filtered = mapping
70 .iter()
71 .filter(|(k, _)| !k.as_str().is_some_and(|key| STRUCTURAL.contains(&key)))
72 .map(|(k, v)| (k.clone(), v.clone()))
73 .collect();
74
75 serde_yaml::Value::Mapping(filtered)
76}
77
78pub type HttpFilterFactory = Arc<dyn Fn(&serde_yaml::Value) -> Result<Box<dyn HttpFilter>, FilterError> + Send + Sync>;
84
85pub type TcpFilterFactory = Arc<dyn Fn(&serde_yaml::Value) -> Result<Box<dyn TcpFilter>, FilterError> + Send + Sync>;
87
88pub enum FilterFactory {
94 Http(HttpFilterFactory),
96
97 Tcp(TcpFilterFactory),
99}
100
101impl FilterFactory {
102 pub(crate) fn create(&self, config: &serde_yaml::Value) -> Result<AnyFilter, FilterError> {
104 match self {
105 Self::Http(f) => Ok(AnyFilter::Http(f(config)?)),
106 Self::Tcp(f) => Ok(AnyFilter::Tcp(f(config)?)),
107 }
108 }
109}
110
111#[expect(clippy::type_complexity, reason = "complex function pointer")]
127pub fn http_builtin(f: fn(&serde_yaml::Value) -> Result<Box<dyn HttpFilter>, FilterError>) -> FilterFactory {
128 FilterFactory::Http(Arc::new(f))
129}
130
131#[expect(clippy::type_complexity, reason = "complex function pointer")]
143pub fn tcp_builtin(f: fn(&serde_yaml::Value) -> Result<Box<dyn TcpFilter>, FilterError>) -> FilterFactory {
144 FilterFactory::Tcp(Arc::new(f))
145}
146
147#[cfg(test)]
152#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
153#[allow(
154 clippy::unwrap_used,
155 clippy::expect_used,
156 clippy::indexing_slicing,
157 clippy::panic,
158 clippy::unnecessary_wraps,
159 reason = "tests"
160)]
161mod tests {
162 use async_trait::async_trait;
163
164 use super::*;
165 use crate::{actions::FilterAction, context::HttpFilterContext};
166
167 #[test]
168 fn http_builtin_creates_http_variant() {
169 fn make(_: &serde_yaml::Value) -> Result<Box<dyn HttpFilter>, FilterError> {
170 Ok(Box::new(MinimalFilter))
171 }
172
173 let factory = http_builtin(make);
174 let filter = factory.create(&serde_yaml::Value::Null).unwrap();
175
176 assert_eq!(filter.name(), "minimal");
177 assert!(matches!(filter, AnyFilter::Http(_)));
178 }
179
180 #[test]
181 fn tcp_builtin_creates_tcp_variant() {
182 fn make(_: &serde_yaml::Value) -> Result<Box<dyn TcpFilter>, FilterError> {
183 Ok(Box::new(MinimalTcpFilter))
184 }
185
186 let factory = tcp_builtin(make);
187 let filter = factory.create(&serde_yaml::Value::Null).unwrap();
188
189 assert_eq!(filter.name(), "minimal_tcp");
190 assert!(matches!(filter, AnyFilter::Tcp(_)));
191 }
192
193 #[test]
194 fn strip_structural_keys_removes_known_keys() {
195 let mut mapping = serde_yaml::Mapping::new();
196 mapping.insert("filter".into(), "router".into());
197 mapping.insert("conditions".into(), serde_yaml::Value::Sequence(vec![]));
198 mapping.insert("name".into(), "my_filter".into());
199 mapping.insert("my_config_field".into(), "value".into());
200
201 let cleaned = strip_structural_keys(&serde_yaml::Value::Mapping(mapping));
202
203 let result = cleaned.as_mapping().expect("should be mapping");
204 assert!(
205 result.get("filter").is_none(),
206 "structural key 'filter' should be stripped"
207 );
208 assert!(
209 result.get("conditions").is_none(),
210 "structural key 'conditions' should be stripped"
211 );
212 assert!(result.get("name").is_none(), "structural key 'name' should be stripped");
213 assert_eq!(
214 result.get("my_config_field").and_then(|v| v.as_str()),
215 Some("value"),
216 "non-structural key should be preserved"
217 );
218 }
219
220 #[test]
221 fn strip_structural_keys_non_mapping_passes_through() {
222 let input = serde_yaml::Value::String("hello".to_owned());
223 let output = strip_structural_keys(&input);
224 assert_eq!(
225 output.as_str(),
226 Some("hello"),
227 "non-mapping value should pass through unchanged"
228 );
229 }
230
231 #[test]
232 fn strip_structural_keys_only_structural_produces_empty_mapping() {
233 let mut mapping = serde_yaml::Mapping::new();
234 mapping.insert("filter".into(), "router".into());
235 mapping.insert("conditions".into(), serde_yaml::Value::Null);
236 mapping.insert("name".into(), "x".into());
237 mapping.insert("failure_mode".into(), "open".into());
238 mapping.insert("response_conditions".into(), serde_yaml::Value::Null);
239 mapping.insert("branch_chains".into(), serde_yaml::Value::Null);
240
241 let cleaned = strip_structural_keys(&serde_yaml::Value::Mapping(mapping));
242
243 let result = cleaned.as_mapping().expect("should be mapping");
244 assert!(
245 result.is_empty(),
246 "mapping with only structural keys should be empty after stripping"
247 );
248 }
249
250 struct MinimalFilter;
256
257 #[async_trait]
258 impl HttpFilter for MinimalFilter {
259 fn name(&self) -> &'static str {
260 "minimal"
261 }
262
263 async fn on_request(&self, _ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
264 Ok(FilterAction::Continue)
265 }
266 }
267
268 struct MinimalTcpFilter;
270
271 #[async_trait]
272 impl TcpFilter for MinimalTcpFilter {
273 fn name(&self) -> &'static str {
274 "minimal_tcp"
275 }
276 }
277}