mockforge_core/scenarios/
executor.rs1use 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#[derive(Debug, Clone)]
14pub struct ScenarioExecutor {
15 registry: Arc<ScenarioRegistry>,
17 http_client: Client,
19 base_url: String,
21}
22
23impl ScenarioExecutor {
24 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 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 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 if let Some(params) = parameters {
65 for (key, value) in params {
66 state.insert(key, value);
67 }
68 }
69
70 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 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 let step_result = self.execute_step(step, &state).await;
88 let success = step_result.success;
89
90 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 !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 remaining_steps.retain(|step| !executed_steps.contains(&step.id));
115
116 if !progress_made && !remaining_steps.is_empty() {
117 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 async fn execute_step(
149 &self,
150 step: &ScenarioStep,
151 state: &HashMap<String, Value>,
152 ) -> StepResult {
153 let step_start = Instant::now();
154
155 if let Some(delay_ms) = step.delay_ms {
157 tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
158 }
159
160 let mut url = format!("{}{}", self.base_url, step.path);
162 for (param, value) in &step.path_params {
163 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 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 let body = step.body.as_ref().map(|b| {
188 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 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 for (key, value) in &step.headers {
218 request = request.header(key, value);
219 }
220
221 if let Some(body_value) = body {
223 request = request.json(&body_value);
224 }
225
226 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 let success = step
234 .expected_status
235 .map(|expected| status == expected)
236 .unwrap_or((200..300).contains(&status));
237
238 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 fn substitute_templates(value: &mut Value, state: &HashMap<String, Value>) {
280 match value {
281 Value::String(s) => {
282 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 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}