mockforge_core/scenarios/
executor.rs

1//! Scenario executor that converts scenarios to chains and executes them
2
3use crate::chain_execution::{ChainExecutionEngine, ChainExecutionResult, ChainExecutionStatus};
4use crate::request_chaining::{
5    ChainConfig, ChainDefinition, ChainLink, ChainRequest, RequestBody, RequestChainRegistry,
6};
7use crate::scenarios::registry::ScenarioRegistry;
8use crate::scenarios::types::{ScenarioDefinition, ScenarioResult, ScenarioStep, StepResult};
9use crate::{Error, Result};
10use reqwest::Client;
11use serde_json::Value;
12use std::collections::HashMap;
13use std::sync::Arc;
14use std::time::Instant;
15
16/// Executor for running scenario definitions
17#[derive(Debug, Clone)]
18pub struct ScenarioExecutor {
19    /// Scenario registry
20    registry: Arc<ScenarioRegistry>,
21    /// HTTP client for making requests
22    http_client: Client,
23    /// Base URL for API requests
24    base_url: String,
25}
26
27impl ScenarioExecutor {
28    /// Create a new scenario executor
29    pub fn new(registry: Arc<ScenarioRegistry>, base_url: impl Into<String>) -> Result<Self> {
30        let http_client = Client::builder()
31            .timeout(std::time::Duration::from_secs(30))
32            .build()
33            .map_err(|e| Error::generic(format!("Failed to create HTTP client: {}", e)))?;
34
35        Ok(Self {
36            registry,
37            http_client,
38            base_url: base_url.into(),
39        })
40    }
41
42    /// Execute a scenario by ID
43    pub async fn execute_scenario(
44        &self,
45        scenario_id: &str,
46        parameters: Option<HashMap<String, Value>>,
47    ) -> Result<ScenarioResult> {
48        let scenario = self
49            .registry
50            .get(scenario_id)
51            .await
52            .ok_or_else(|| Error::generic(format!("Scenario not found: {}", scenario_id)))?;
53
54        self.execute_scenario_definition(&scenario, parameters).await
55    }
56
57    /// Execute a scenario definition directly
58    pub async fn execute_scenario_definition(
59        &self,
60        scenario: &ScenarioDefinition,
61        parameters: Option<HashMap<String, Value>>,
62    ) -> Result<ScenarioResult> {
63        let start_time = Instant::now();
64        let mut step_results = Vec::new();
65        let mut state = scenario.variables.clone();
66
67        // Merge parameters into state
68        if let Some(params) = parameters {
69            for (key, value) in params {
70                state.insert(key, value);
71            }
72        }
73
74        // Execute steps in order (respecting dependencies)
75        let mut executed_steps = std::collections::HashSet::new();
76        let mut remaining_steps: Vec<&ScenarioStep> = scenario.steps.iter().collect();
77
78        while !remaining_steps.is_empty() {
79            let mut progress_made = false;
80
81            for step in remaining_steps.iter() {
82                // Check if dependencies are satisfied
83                let deps_satisfied =
84                    step.depends_on.iter().all(|dep_id| executed_steps.contains(dep_id));
85
86                if !deps_satisfied {
87                    continue;
88                }
89
90                // Execute step
91                let step_result = self.execute_step(step, &state).await;
92                let success = step_result.success;
93
94                // Update state with extracted variables
95                for (var_name, var_value) in &step_result.extracted_variables {
96                    state.insert(var_name.clone(), var_value.clone());
97                }
98
99                step_results.push(step_result);
100                executed_steps.insert(step.id.clone());
101                progress_made = true;
102
103                // If step failed and we shouldn't continue, break
104                if !success && !step.continue_on_failure {
105                    let duration_ms = start_time.elapsed().as_millis() as u64;
106                    return Ok(ScenarioResult {
107                        scenario_id: scenario.id.clone(),
108                        success: false,
109                        step_results,
110                        duration_ms,
111                        error: Some(format!("Step '{}' failed", step.id)),
112                        final_state: state,
113                    });
114                }
115            }
116
117            // Remove executed steps
118            remaining_steps.retain(|step| !executed_steps.contains(&step.id));
119
120            if !progress_made && !remaining_steps.is_empty() {
121                // Circular dependency or unsatisfiable dependencies
122                let duration_ms = start_time.elapsed().as_millis() as u64;
123                return Ok(ScenarioResult {
124                    scenario_id: scenario.id.clone(),
125                    success: false,
126                    step_results,
127                    duration_ms,
128                    error: Some("Circular or unsatisfiable dependencies detected".to_string()),
129                    final_state: state,
130                });
131            }
132        }
133
134        let duration_ms = start_time.elapsed().as_millis() as u64;
135        let all_successful = step_results.iter().all(|r| r.success);
136
137        Ok(ScenarioResult {
138            scenario_id: scenario.id.clone(),
139            success: all_successful,
140            step_results,
141            duration_ms,
142            error: if all_successful {
143                None
144            } else {
145                Some("One or more steps failed".to_string())
146            },
147            final_state: state,
148        })
149    }
150
151    /// Execute a single step
152    async fn execute_step(
153        &self,
154        step: &ScenarioStep,
155        state: &HashMap<String, Value>,
156    ) -> StepResult {
157        let step_start = Instant::now();
158
159        // Apply delay if specified
160        if let Some(delay_ms) = step.delay_ms {
161            tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
162        }
163
164        // Build URL with path parameters
165        let mut url = format!("{}{}", self.base_url, step.path);
166        for (param, value) in &step.path_params {
167            // Simple template substitution (in production, use proper templating)
168            let value_str = if let Some(state_value) = state.get(value) {
169                state_value.as_str().unwrap_or(value).to_string()
170            } else {
171                value.clone()
172            };
173            url = url.replace(&format!("{{{}}}", param), &value_str);
174        }
175
176        // Build query string
177        let mut query_parts = Vec::new();
178        for (key, value) in &step.query_params {
179            let value_str = if let Some(state_value) = state.get(value) {
180                state_value.as_str().unwrap_or(value).to_string()
181            } else {
182                value.clone()
183            };
184            query_parts.push(format!("{}={}", key, urlencoding::encode(&value_str)));
185        }
186        if !query_parts.is_empty() {
187            url = format!("{}?{}", url, query_parts.join("&"));
188        }
189
190        // Prepare request body (apply template substitution)
191        let body = step.body.as_ref().map(|b| {
192            // Simple JSON template substitution
193            // In production, use proper templating engine
194            let body_str = serde_json::to_string(b).unwrap_or_default();
195            let mut body_value = serde_json::from_str::<Value>(&body_str).unwrap_or(b.clone());
196            Self::substitute_templates(&mut body_value, state);
197            body_value
198        });
199
200        // Build request
201        let mut request = match step.method.as_str() {
202            "GET" => self.http_client.get(&url),
203            "POST" => self.http_client.post(&url),
204            "PUT" => self.http_client.put(&url),
205            "PATCH" => self.http_client.patch(&url),
206            "DELETE" => self.http_client.delete(&url),
207            _ => {
208                return StepResult {
209                    step_id: step.id.clone(),
210                    success: false,
211                    status_code: None,
212                    response_body: None,
213                    extracted_variables: HashMap::new(),
214                    error: Some(format!("Unsupported HTTP method: {}", step.method)),
215                    duration_ms: step_start.elapsed().as_millis() as u64,
216                };
217            }
218        };
219
220        // Add headers
221        for (key, value) in &step.headers {
222            request = request.header(key, value);
223        }
224
225        // Add body
226        if let Some(body_value) = body {
227            request = request.json(&body_value);
228        }
229
230        // Execute request
231        match request.send().await {
232            Ok(response) => {
233                let status = response.status().as_u16();
234                let response_body: Option<Value> = response.json().await.ok();
235
236                // Check expected status
237                let success = step
238                    .expected_status
239                    .map(|expected| status == expected)
240                    .unwrap_or(status >= 200 && status < 300);
241
242                // Extract variables from response
243                let mut extracted = HashMap::new();
244                if let Some(ref body) = response_body {
245                    for (var_name, json_path) in &step.extract {
246                        if let Some(value) = Self::extract_json_path(body, json_path) {
247                            extracted.insert(var_name.clone(), value);
248                        }
249                    }
250                }
251
252                StepResult {
253                    step_id: step.id.clone(),
254                    success,
255                    status_code: Some(status),
256                    response_body,
257                    extracted_variables: extracted,
258                    error: if success {
259                        None
260                    } else {
261                        Some(format!(
262                            "Expected status {}, got {}",
263                            step.expected_status.unwrap_or(200),
264                            status
265                        ))
266                    },
267                    duration_ms: step_start.elapsed().as_millis() as u64,
268                }
269            }
270            Err(e) => StepResult {
271                step_id: step.id.clone(),
272                success: false,
273                status_code: None,
274                response_body: None,
275                extracted_variables: HashMap::new(),
276                error: Some(format!("Request failed: {}", e)),
277                duration_ms: step_start.elapsed().as_millis() as u64,
278            },
279        }
280    }
281
282    /// Substitute template variables in a JSON value
283    fn substitute_templates(value: &mut Value, state: &HashMap<String, Value>) {
284        match value {
285            Value::String(s) => {
286                // Simple template substitution: {{variable_name}}
287                if s.starts_with("{{") && s.ends_with("}}") {
288                    let var_name = s.trim_start_matches("{{").trim_end_matches("}}").trim();
289                    if let Some(var_value) = state.get(var_name) {
290                        *value = var_value.clone();
291                    }
292                }
293            }
294            Value::Object(map) => {
295                for v in map.values_mut() {
296                    Self::substitute_templates(v, state);
297                }
298            }
299            Value::Array(arr) => {
300                for v in arr.iter_mut() {
301                    Self::substitute_templates(v, state);
302                }
303            }
304            _ => {}
305        }
306    }
307
308    /// Extract a value from JSON using a simple path (e.g., "body.user.id")
309    fn extract_json_path(value: &Value, path: &str) -> Option<Value> {
310        let parts: Vec<&str> = path.split('.').collect();
311        let mut current = value;
312
313        for part in parts {
314            match current {
315                Value::Object(map) => {
316                    current = map.get(part)?;
317                }
318                Value::Array(arr) => {
319                    let index: usize = part.parse().ok()?;
320                    current = arr.get(index)?;
321                }
322                _ => return None,
323            }
324        }
325
326        Some(current.clone())
327    }
328}