mockforge_core/proxy/
conditional.rs

1//! Conditional proxy evaluation using expressions (JSONPath, JavaScript-like, etc.)
2
3use crate::conditions::{evaluate_condition, ConditionContext};
4use crate::proxy::config::ProxyRule;
5use crate::{Error, Result};
6use axum::http::{HeaderMap, Method, Uri};
7use serde_json::Value;
8use std::collections::HashMap;
9use tracing::debug;
10
11/// Evaluate whether a proxy rule's condition matches the request
12pub fn evaluate_proxy_condition(
13    rule: &ProxyRule,
14    method: &Method,
15    uri: &Uri,
16    headers: &HeaderMap,
17    body: Option<&[u8]>,
18) -> Result<bool> {
19    // If no condition is specified, the rule matches (path pattern already matched)
20    let Some(ref condition) = rule.condition else {
21        return Ok(true);
22    };
23
24    // Build condition context from request
25    let mut context = ConditionContext::new()
26        .with_method(method.as_str().to_string())
27        .with_path(uri.path().to_string());
28
29    // Extract query parameters
30    let query_params: HashMap<String, String> = uri
31        .query()
32        .map(|q| {
33            url::form_urlencoded::parse(q.as_bytes())
34                .map(|(k, v)| (k.to_string(), v.to_string()))
35                .collect()
36        })
37        .unwrap_or_default();
38    context = context.with_query_params(query_params);
39
40    // Extract headers
41    let headers_map: HashMap<String, String> = headers
42        .iter()
43        .filter_map(|(k, v)| {
44            v.to_str().ok().map(|v_str| (k.as_str().to_lowercase(), v_str.to_string()))
45        })
46        .collect();
47    context = context.with_headers(headers_map);
48
49    // Parse request body if present
50    if let Some(body_bytes) = body {
51        if let Ok(body_str) = std::str::from_utf8(body_bytes) {
52            // Try to parse as JSON
53            if let Ok(json_value) = serde_json::from_str::<Value>(body_str) {
54                context = context.with_request_body(json_value);
55            }
56        }
57    }
58
59    // Evaluate the condition
60    match evaluate_condition(condition, &context) {
61        Ok(result) => {
62            debug!(
63                "Proxy condition '{}' evaluated to {} for {} {}",
64                condition,
65                result,
66                method,
67                uri.path()
68            );
69            Ok(result)
70        }
71        Err(e) => {
72            // Log error but don't fail - treat as false (don't proxy)
73            tracing::warn!(
74                "Failed to evaluate proxy condition '{}': {}. Treating as false.",
75                condition,
76                e
77            );
78            Ok(false)
79        }
80    }
81}
82
83/// Find matching proxy rule with condition evaluation
84pub fn find_matching_rule<'a>(
85    rules: &'a [ProxyRule],
86    method: &Method,
87    uri: &Uri,
88    headers: &HeaderMap,
89    body: Option<&[u8]>,
90    path_matches: impl Fn(&str, &str) -> bool,
91) -> Option<&'a ProxyRule> {
92    for rule in rules {
93        if !rule.enabled {
94            continue;
95        }
96
97        // Check if path matches
98        if !path_matches(&rule.path_pattern, uri.path()) {
99            continue;
100        }
101
102        // Evaluate condition if present
103        match evaluate_proxy_condition(rule, method, uri, headers, body) {
104            Ok(true) => return Some(rule),
105            Ok(false) => continue, // Condition didn't match, try next rule
106            Err(e) => {
107                tracing::warn!("Error evaluating condition for rule {}: {}", rule.path_pattern, e);
108                continue;
109            }
110        }
111    }
112
113    None
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::proxy::config::ProxyRule;
120    use axum::http::HeaderValue;
121    use serde_json::json;
122
123    fn create_test_rule(path: &str, condition: Option<&str>) -> ProxyRule {
124        ProxyRule {
125            path_pattern: path.to_string(),
126            target_url: "http://example.com".to_string(),
127            enabled: true,
128            pattern: path.to_string(),
129            upstream_url: "http://example.com".to_string(),
130            migration_mode: crate::proxy::config::MigrationMode::Auto,
131            migration_group: None,
132            condition: condition.map(|s| s.to_string()),
133        }
134    }
135
136    #[test]
137    fn test_no_condition() {
138        let rule = create_test_rule("/api/users", None);
139        let method = Method::GET;
140        let uri = Uri::from_static("/api/users");
141        let headers = HeaderMap::new();
142
143        let result = evaluate_proxy_condition(&rule, &method, &uri, &headers, None).unwrap();
144        assert!(result); // No condition means always true
145    }
146
147    #[test]
148    fn test_header_condition() {
149        let rule = create_test_rule("/api/users", Some("header[authorization] != ''"));
150        let method = Method::GET;
151        let uri = Uri::from_static("/api/users");
152        let mut headers = HeaderMap::new();
153        headers.insert("authorization", HeaderValue::from_static("Bearer token123"));
154
155        let result = evaluate_proxy_condition(&rule, &method, &uri, &headers, None).unwrap();
156        assert!(result);
157    }
158
159    #[test]
160    fn test_jsonpath_condition() {
161        let rule = create_test_rule("/api/users", Some("$.user.role"));
162        let method = Method::POST;
163        let uri = Uri::from_static("/api/users");
164        let headers = HeaderMap::new();
165        let body = json!({
166            "user": {
167                "role": "admin"
168            }
169        });
170        let body_bytes = serde_json::to_string(&body).unwrap().into_bytes();
171
172        let result =
173            evaluate_proxy_condition(&rule, &method, &uri, &headers, Some(&body_bytes)).unwrap();
174        assert!(result);
175    }
176
177    #[test]
178    fn test_query_param_condition() {
179        let rule = create_test_rule("/api/users", Some("query[env] == 'production'"));
180        let method = Method::GET;
181        let uri = Uri::from_static("/api/users?env=production");
182        let headers = HeaderMap::new();
183
184        let result = evaluate_proxy_condition(&rule, &method, &uri, &headers, None).unwrap();
185        assert!(result);
186    }
187
188    #[test]
189    fn test_complex_condition() {
190        let rule = create_test_rule(
191            "/api/users",
192            Some("AND(header[authorization] != '', query[env] == 'production')"),
193        );
194        let method = Method::GET;
195        let uri = Uri::from_static("/api/users?env=production");
196        let mut headers = HeaderMap::new();
197        headers.insert("authorization", HeaderValue::from_static("Bearer token"));
198
199        let result = evaluate_proxy_condition(&rule, &method, &uri, &headers, None).unwrap();
200        assert!(result);
201    }
202}