mockforge_core/scenarios/
executor.rs

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