mockforge_core/scenarios/
executor.rs1use 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#[derive(Debug, Clone)]
18pub struct ScenarioExecutor {
19 registry: Arc<ScenarioRegistry>,
21 http_client: Client,
23 base_url: String,
25}
26
27impl ScenarioExecutor {
28 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 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 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 if let Some(params) = parameters {
69 for (key, value) in params {
70 state.insert(key, value);
71 }
72 }
73
74 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 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 let step_result = self.execute_step(step, &state).await;
92 let success = step_result.success;
93
94 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 !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 remaining_steps.retain(|step| !executed_steps.contains(&step.id));
119
120 if !progress_made && !remaining_steps.is_empty() {
121 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 async fn execute_step(
153 &self,
154 step: &ScenarioStep,
155 state: &HashMap<String, Value>,
156 ) -> StepResult {
157 let step_start = Instant::now();
158
159 if let Some(delay_ms) = step.delay_ms {
161 tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
162 }
163
164 let mut url = format!("{}{}", self.base_url, step.path);
166 for (param, value) in &step.path_params {
167 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 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 let body = step.body.as_ref().map(|b| {
192 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 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 for (key, value) in &step.headers {
222 request = request.header(key, value);
223 }
224
225 if let Some(body_value) = body {
227 request = request.json(&body_value);
228 }
229
230 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 let success = step
238 .expected_status
239 .map(|expected| status == expected)
240 .unwrap_or(status >= 200 && status < 300);
241
242 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 fn substitute_templates(value: &mut Value, state: &HashMap<String, Value>) {
284 match value {
285 Value::String(s) => {
286 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 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}