Skip to main content

mockforge_intelligence/behavioral_economics/
actions.rs

1//! Behavior action definitions and execution
2//!
3//! Defines actions that can be executed when behavior conditions are met.
4//! Actions modify mock behavior, responses, or trigger chaos rules.
5
6use mockforge_foundation::Result;
7use serde::{Deserialize, Serialize};
8
9/// Behavior action
10///
11/// Actions are executed when behavior conditions evaluate to true.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
14#[serde(tag = "type", rename_all = "snake_case")]
15pub enum BehaviorAction {
16    /// No operation (for testing or disabled rules)
17    NoOp,
18
19    /// Modify conversion rate
20    ModifyConversionRate {
21        /// Multiplier (e.g., 0.8 = 80% of original rate)
22        multiplier: f64,
23    },
24
25    /// Decline transaction
26    DeclineTransaction {
27        /// Decline reason
28        reason: String,
29    },
30
31    /// Increase churn probability
32    IncreaseChurnProbability {
33        /// Factor to multiply churn probability by
34        factor: f64,
35    },
36
37    /// Change response status code
38    ChangeResponseStatus {
39        /// HTTP status code
40        status: u16,
41    },
42
43    /// Modify latency
44    ModifyLatency {
45        /// Adjustment in milliseconds (can be negative)
46        adjustment_ms: i64,
47    },
48
49    /// Trigger chaos rule
50    TriggerChaosRule {
51        /// Name of chaos rule to trigger
52        rule_name: String,
53    },
54
55    /// Modify response body
56    ModifyResponseBody {
57        /// JSON path to modify
58        path: String,
59        /// New value (as JSON string)
60        value: String,
61    },
62}
63
64/// The concrete effect produced by executing an action
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
66#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
67#[serde(tag = "type", rename_all = "snake_case")]
68pub enum ActionEffect {
69    /// No effect
70    None,
71    /// Multiply a rate (conversion, churn, etc.)
72    RateMultiplier {
73        /// Rate being modified
74        target: String,
75        /// Multiplier value
76        multiplier: f64,
77    },
78    /// Decline / reject a request
79    Rejection {
80        /// Reason for rejection
81        reason: String,
82    },
83    /// Override the HTTP status code
84    StatusOverride {
85        /// New status code
86        status: u16,
87    },
88    /// Adjust latency by a delta
89    LatencyAdjustment {
90        /// Millisecond adjustment (can be negative)
91        delta_ms: i64,
92    },
93    /// Trigger an external chaos rule
94    Chaostrigger {
95        /// Name of the chaos rule
96        rule_name: String,
97    },
98    /// Patch the response body at a JSON path
99    BodyPatch {
100        /// JSON path to modify
101        path: String,
102        /// New value (serialised JSON)
103        value: String,
104    },
105}
106
107/// Result of executing an action, containing both a human-readable
108/// description and the structured effect.
109#[derive(Debug, Clone)]
110pub struct ActionResult {
111    /// Human-readable description of what happened
112    pub description: String,
113    /// Structured effect that downstream code can act on
114    pub effect: ActionEffect,
115}
116
117/// Action executor
118///
119/// Executes behavior actions and returns structured results describing
120/// the effect that should be applied to the request/response pipeline.
121pub struct ActionExecutor;
122
123impl ActionExecutor {
124    /// Create a new action executor
125    pub fn new() -> Self {
126        Self
127    }
128
129    /// Execute an action and return a structured result
130    pub fn execute_action(&self, action: &BehaviorAction) -> Result<ActionResult> {
131        match action {
132            BehaviorAction::NoOp => Ok(ActionResult {
133                description: "No operation".to_string(),
134                effect: ActionEffect::None,
135            }),
136
137            BehaviorAction::ModifyConversionRate { multiplier } => Ok(ActionResult {
138                description: format!("Modified conversion rate by factor {}", multiplier),
139                effect: ActionEffect::RateMultiplier {
140                    target: "conversion".to_string(),
141                    multiplier: *multiplier,
142                },
143            }),
144
145            BehaviorAction::DeclineTransaction { reason } => Ok(ActionResult {
146                description: format!("Declined transaction: {}", reason),
147                effect: ActionEffect::Rejection {
148                    reason: reason.clone(),
149                },
150            }),
151
152            BehaviorAction::IncreaseChurnProbability { factor } => Ok(ActionResult {
153                description: format!("Increased churn probability by factor {}", factor),
154                effect: ActionEffect::RateMultiplier {
155                    target: "churn".to_string(),
156                    multiplier: *factor,
157                },
158            }),
159
160            BehaviorAction::ChangeResponseStatus { status } => Ok(ActionResult {
161                description: format!("Changed response status to {}", status),
162                effect: ActionEffect::StatusOverride { status: *status },
163            }),
164
165            BehaviorAction::ModifyLatency { adjustment_ms } => Ok(ActionResult {
166                description: format!("Modified latency by {}ms", adjustment_ms),
167                effect: ActionEffect::LatencyAdjustment {
168                    delta_ms: *adjustment_ms,
169                },
170            }),
171
172            BehaviorAction::TriggerChaosRule { rule_name } => Ok(ActionResult {
173                description: format!("Triggered chaos rule: {}", rule_name),
174                effect: ActionEffect::Chaostrigger {
175                    rule_name: rule_name.clone(),
176                },
177            }),
178
179            BehaviorAction::ModifyResponseBody { path, value } => Ok(ActionResult {
180                description: format!("Modified response body at {} to {}", path, value),
181                effect: ActionEffect::BodyPatch {
182                    path: path.clone(),
183                    value: value.clone(),
184                },
185            }),
186        }
187    }
188
189    /// Execute an action and return a description string
190    ///
191    /// This is a convenience wrapper around [`execute_action`](Self::execute_action)
192    /// for callers that only need a log-friendly description.
193    pub fn execute(&self, action: &BehaviorAction) -> Result<String> {
194        self.execute_action(action).map(|r| r.description)
195    }
196}
197
198impl Default for ActionExecutor {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_noop_action() {
210        let executor = ActionExecutor::new();
211        let result = executor.execute(&BehaviorAction::NoOp).unwrap();
212        assert_eq!(result, "No operation");
213    }
214
215    #[test]
216    fn test_modify_conversion_rate() {
217        let executor = ActionExecutor::new();
218        let result = executor
219            .execute(&BehaviorAction::ModifyConversionRate { multiplier: 0.8 })
220            .unwrap();
221        assert!(result.contains("0.8"));
222    }
223
224    #[test]
225    fn test_decline_transaction() {
226        let executor = ActionExecutor::new();
227        let result = executor
228            .execute(&BehaviorAction::DeclineTransaction {
229                reason: "fraud_detected".to_string(),
230            })
231            .unwrap();
232        assert!(result.contains("fraud_detected"));
233    }
234
235    #[test]
236    fn test_execute_action_status_override() {
237        let executor = ActionExecutor::new();
238        let result = executor
239            .execute_action(&BehaviorAction::ChangeResponseStatus { status: 503 })
240            .unwrap();
241        assert_eq!(result.effect, ActionEffect::StatusOverride { status: 503 });
242    }
243
244    #[test]
245    fn test_execute_action_latency_adjustment() {
246        let executor = ActionExecutor::new();
247        let result = executor
248            .execute_action(&BehaviorAction::ModifyLatency { adjustment_ms: -50 })
249            .unwrap();
250        assert_eq!(result.effect, ActionEffect::LatencyAdjustment { delta_ms: -50 });
251    }
252
253    #[test]
254    fn test_execute_action_body_patch() {
255        let executor = ActionExecutor::new();
256        let result = executor
257            .execute_action(&BehaviorAction::ModifyResponseBody {
258                path: "$.price".to_string(),
259                value: "99.99".to_string(),
260            })
261            .unwrap();
262        assert_eq!(
263            result.effect,
264            ActionEffect::BodyPatch {
265                path: "$.price".to_string(),
266                value: "99.99".to_string(),
267            }
268        );
269    }
270
271    #[test]
272    fn test_execute_action_churn_multiplier() {
273        let executor = ActionExecutor::new();
274        let result = executor
275            .execute_action(&BehaviorAction::IncreaseChurnProbability { factor: 2.0 })
276            .unwrap();
277        assert_eq!(
278            result.effect,
279            ActionEffect::RateMultiplier {
280                target: "churn".to_string(),
281                multiplier: 2.0,
282            }
283        );
284    }
285
286    #[test]
287    fn test_execute_action_noop() {
288        let executor = ActionExecutor::new();
289        let result = executor.execute_action(&BehaviorAction::NoOp).unwrap();
290        assert_eq!(result.effect, ActionEffect::None);
291    }
292}