Skip to main content

bamboo_domain/session/composition/
condition.rs

1//! Condition predicates for workflow control flow.
2//!
3//! Pure data types — the `evaluate(&ToolResult)` method lives in
4//! bamboo-application-agent's composition module since it depends on
5//! the application-layer `ToolResult` type.
6
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9
10/// Condition for control flow in tool expressions.
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12#[serde(rename_all = "snake_case", tag = "type")]
13pub enum Condition {
14    /// Check if the result was successful
15    Success,
16    /// Check if JSON path contains a specific value
17    Contains { path: String, value: String },
18    /// Check if value at JSON path matches a regex pattern
19    Matches { path: String, pattern: String },
20    /// All conditions must be true
21    And { conditions: Vec<Condition> },
22    /// At least one condition must be true
23    Or { conditions: Vec<Condition> },
24}
25
26/// Evaluate condition against a (success, result_json) pair.
27///
28/// This is the domain-layer evaluation that doesn't depend on
29/// application-layer types.
30pub fn evaluate_condition(condition: &Condition, success: bool, result_json: &str) -> bool {
31    match condition {
32        Condition::Success => success,
33        Condition::Contains { path, value } => evaluate_contains(result_json, path, value),
34        Condition::Matches { path, pattern } => evaluate_matches(result_json, path, pattern),
35        Condition::And { conditions } => conditions
36            .iter()
37            .all(|c| evaluate_condition(c, success, result_json)),
38        Condition::Or { conditions } => conditions
39            .iter()
40            .any(|c| evaluate_condition(c, success, result_json)),
41    }
42}
43
44fn extract_at_path(json_str: &str, path: &str) -> Option<String> {
45    let value: serde_json::Value = serde_json::from_str(json_str).ok()?;
46    let parts: Vec<&str> = path.split('.').collect();
47    let mut current = &value;
48    for part in parts {
49        if let Ok(index) = part.parse::<usize>() {
50            current = current.get(index)?;
51        } else {
52            current = current.get(part)?;
53        }
54    }
55    Some(current.to_string().trim_matches('"').to_string())
56}
57
58fn evaluate_contains(json_str: &str, path: &str, expected: &str) -> bool {
59    extract_at_path(json_str, path)
60        .map(|v| v.contains(expected))
61        .unwrap_or(false)
62}
63
64fn evaluate_matches(json_str: &str, path: &str, pattern: &str) -> bool {
65    extract_at_path(json_str, path)
66        .and_then(|v| Regex::new(pattern).ok().map(|re| re.is_match(&v)))
67        .unwrap_or(false)
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn test_success_condition() {
76        assert!(evaluate_condition(&Condition::Success, true, "{}"));
77        assert!(!evaluate_condition(&Condition::Success, false, "{}"));
78    }
79
80    #[test]
81    fn test_contains_condition() {
82        let json = r#"{"status": "completed", "data": {"name": "test"}}"#;
83        let cond = Condition::Contains {
84            path: "status".into(),
85            value: "complete".into(),
86        };
87        assert!(evaluate_condition(&cond, true, json));
88
89        let cond = Condition::Contains {
90            path: "data.name".into(),
91            value: "test".into(),
92        };
93        assert!(evaluate_condition(&cond, true, json));
94
95        let cond = Condition::Contains {
96            path: "status".into(),
97            value: "failed".into(),
98        };
99        assert!(!evaluate_condition(&cond, true, json));
100    }
101
102    #[test]
103    fn test_matches_condition() {
104        let json = r#"{"email": "user@example.com"}"#;
105        let cond = Condition::Matches {
106            path: "email".into(),
107            pattern: r"^\S+@\S+\.\S+$".into(),
108        };
109        assert!(evaluate_condition(&cond, true, json));
110    }
111
112    #[test]
113    fn test_json_serialization() {
114        let cond = Condition::And {
115            conditions: vec![
116                Condition::Success,
117                Condition::Contains {
118                    path: "status".into(),
119                    value: "ok".into(),
120                },
121            ],
122        };
123        let json = serde_json::to_string(&cond).unwrap();
124        assert!(json.contains("\"type\":\"and\"") || json.contains("\"type\": \"and\""));
125        let deserialized: Condition = serde_json::from_str(&json).unwrap();
126        assert_eq!(cond, deserialized);
127    }
128}