featherbit/plugins/native/
condition.rs1use async_trait::async_trait;
15use std::collections::HashMap;
16
17use crate::context::Context;
18use crate::plugins::{Plugin, PluginOutput, PluginResult};
19
20#[derive(Debug)]
23pub struct ConditionPlugin {
24 conditions: crate::vars::Expr,
25}
26
27impl ConditionPlugin {
28 pub fn from_config(config: &HashMap<String, serde_json::Value>) -> Result<Self, String> {
42 let raw = config
43 .get("conditions")
44 .ok_or("condition: 'conditions' is required")?;
45 if raw.as_array().is_some_and(|rules| rules.is_empty()) {
46 return Err("condition: 'conditions' must not be empty".to_string());
47 }
48 let conditions = crate::vars::Expr::parse(raw)
49 .map_err(|e| format!("condition: invalid 'conditions': {}", e))?;
50 Ok(Self { conditions })
51 }
52}
53
54#[async_trait]
55impl Plugin for ConditionPlugin {
56 fn plugin_type(&self) -> &str {
57 "condition"
58 }
59
60 async fn execute(&self, ctx: Context) -> PluginResult {
61 let port = if self.conditions.eval(&ctx) {
62 "true"
63 } else {
64 "false"
65 };
66 Ok(PluginOutput::on_port(ctx, port))
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use crate::context::{Context, GatewayRequest, GatewayResponse, Protocol};
73 use crate::plugins::native::condition::ConditionPlugin;
74 use crate::plugins::Plugin;
75 use bytes::Bytes;
76 use std::collections::HashMap;
77
78 fn test_context(body: &str) -> Context {
79 let mut headers = HashMap::new();
80 headers.insert("x-tier".to_string(), vec!["premium".to_string()]);
81 Context {
82 request: GatewayRequest {
83 method: "POST".to_string(),
84 path: "/api".to_string(),
85 host: "localhost".to_string(),
86 scheme: "http".to_string(),
87 headers,
88 query_params: HashMap::new(),
89 body: Bytes::from(body.to_string()),
90 remote_addr: "127.0.0.1:12345".to_string(),
91 protocol: Protocol::Http1,
92 },
93 response: GatewayResponse {
94 status_code: 0,
95 headers: HashMap::new(),
96 body: Bytes::new(),
97 stream: None,
98 },
99 message: HashMap::new(),
100 errors: Vec::new(),
101 }
102 }
103
104 fn plugin(conditions: serde_json::Value) -> ConditionPlugin {
105 let mut config = HashMap::new();
106 config.insert("conditions".to_string(), conditions);
107 ConditionPlugin::from_config(&config).unwrap()
108 }
109
110 #[tokio::test]
111 async fn test_condition_true_branch() {
112 let p = plugin(serde_json::json!([["http_x_tier", "==", "premium"]]));
113 let out = p.execute(test_context("")).await.unwrap();
114 assert_eq!(out.port, Some("true"));
115 }
116
117 #[tokio::test]
118 async fn test_condition_false_branch() {
119 let p = plugin(serde_json::json!([["http_x_tier", "==", "basic"]]));
120 let out = p.execute(test_context("")).await.unwrap();
121 assert_eq!(out.port, Some("false"));
122 }
123
124 #[tokio::test]
125 async fn test_condition_jsonpath_branches() {
126 let p = plugin(serde_json::json!([["$.user.tier", "==", "premium"]]));
127 let out = p
128 .execute(test_context(r#"{"user":{"tier":"premium"}}"#))
129 .await
130 .unwrap();
131 assert_eq!(out.port, Some("true"));
132
133 let out = p
134 .execute(test_context(r#"{"user":{"tier":"basic"}}"#))
135 .await
136 .unwrap();
137 assert_eq!(out.port, Some("false"));
138 }
139
140 #[tokio::test]
141 async fn test_condition_absent_var_comparison_is_false() {
142 let p = plugin(serde_json::json!([["arg_interactive", "==", "true"]]));
145 let out = p.execute(test_context("")).await.unwrap();
146 assert_eq!(out.port, Some("false"));
147 assert!(out.context.errors.is_empty());
148 }
149
150 #[tokio::test]
151 async fn test_condition_absent_var_ne_is_true() {
152 let p = plugin(serde_json::json!([["arg_interactive", "!=", "true"]]));
154 let out = p.execute(test_context("")).await.unwrap();
155 assert_eq!(out.port, Some("true"));
156 }
157
158 #[tokio::test]
159 async fn test_condition_non_json_body_jsonpath_is_false() {
160 let p = plugin(serde_json::json!([["$.user.tier", "==", "premium"]]));
162 let out = p.execute(test_context("not json")).await.unwrap();
163 assert_eq!(out.port, Some("false"));
164 assert!(out.context.errors.is_empty());
165 }
166
167 #[tokio::test]
168 async fn test_condition_existence_test_on_absent_var_is_checked() {
169 let p = plugin(serde_json::json!([["http_missing", "absent"]]));
171 let out = p.execute(test_context("")).await.unwrap();
172 assert_eq!(out.port, Some("true"));
173
174 let p = plugin(serde_json::json!([["http_missing", "present"]]));
175 let out = p.execute(test_context("")).await.unwrap();
176 assert_eq!(out.port, Some("false"));
177 }
178
179 #[tokio::test]
180 async fn test_condition_does_not_mutate_context() {
181 let p = plugin(serde_json::json!([["$.user.tier", "==", "premium"]]));
182 let body = r#"{"user": {"tier": "premium"}}"#;
183 let out = p.execute(test_context(body)).await.unwrap();
184 assert_eq!(out.context.request.body, Bytes::from(body));
186 assert_eq!(out.context.response.status_code, 0);
187 assert!(out.context.response.body.is_empty());
188 }
189
190 #[test]
191 fn test_condition_config_rejections() {
192 assert!(ConditionPlugin::from_config(&HashMap::new()).is_err());
194
195 let mut config = HashMap::new();
197 config.insert(
198 "conditions".to_string(),
199 serde_json::json!([["$.a", "bogus_op", 1]]),
200 );
201 let err = ConditionPlugin::from_config(&config).unwrap_err();
202 assert!(err.starts_with("condition: invalid 'conditions'"), "{err}");
203
204 let mut config = HashMap::new();
206 config.insert("conditions".to_string(), serde_json::json!([]));
207 assert!(ConditionPlugin::from_config(&config).is_err());
208 }
209
210 #[test]
211 fn test_condition_registered() {
212 assert!(crate::plugins::KNOWN_PLUGIN_TYPES.contains(&"condition"));
213
214 let mut config = HashMap::new();
215 config.insert(
216 "conditions".to_string(),
217 serde_json::json!([["http_x", "present"]]),
218 );
219 let p = crate::plugins::create_plugin(
220 "condition",
221 &config,
222 &crate::plugins::resources::PluginResources::empty(),
223 )
224 .unwrap();
225 assert_eq!(p.plugin_type(), "condition");
226
227 let spec = crate::plugins::port_spec("condition").unwrap();
228 let names: Vec<&str> = spec.outputs.iter().map(|p| p.name).collect();
229 assert_eq!(names, vec!["true", "false", "error"]);
230 }
231}