use crate::scenario_orchestrator::{OrchestratedScenario, ScenarioStep};
use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use reqwest::Client;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
fn deserialize_body_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let value: Option<JsonValue> = Option::deserialize(deserializer)?;
match value {
None => Ok(None),
Some(JsonValue::String(s)) => Ok(Some(s)),
Some(json_obj) => {
serde_json::to_string(&json_obj).map_err(serde::de::Error::custom).map(Some)
}
}
}
#[derive(Error, Debug)]
pub enum OrchestrationError {
#[error("Assertion failed: {0}")]
AssertionFailed(String),
#[error("Hook execution failed: {0}")]
HookFailed(String),
#[error("Variable not found: {0}")]
VariableNotFound(String),
#[error("Condition evaluation failed: {0}")]
ConditionFailed(String),
#[error("Serialization error: {0}")]
SerializationError(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Condition {
Equals { variable: String, value: JsonValue },
NotEquals { variable: String, value: JsonValue },
GreaterThan { variable: String, value: f64 },
LessThan { variable: String, value: f64 },
GreaterThanOrEqual { variable: String, value: f64 },
LessThanOrEqual { variable: String, value: f64 },
Exists { variable: String },
And { conditions: Vec<Condition> },
Or { conditions: Vec<Condition> },
Not { condition: Box<Condition> },
PreviousStepSucceeded,
PreviousStepFailed,
MetricThreshold {
metric_name: String,
operator: ComparisonOperator,
threshold: f64,
},
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ComparisonOperator {
Equals,
NotEquals,
GreaterThan,
LessThan,
GreaterThanOrEqual,
LessThanOrEqual,
}
impl Condition {
pub fn evaluate(&self, context: &ExecutionContext) -> Result<bool, OrchestrationError> {
match self {
Condition::Equals { variable, value } => {
let var_value = context.get_variable(variable)?;
Ok(var_value == value)
}
Condition::NotEquals { variable, value } => {
let var_value = context.get_variable(variable)?;
Ok(var_value != value)
}
Condition::GreaterThan { variable, value } => {
let var_value = context.get_variable(variable)?;
if let Some(num) = var_value.as_f64() {
Ok(num > *value)
} else {
Err(OrchestrationError::ConditionFailed(format!(
"Variable {} is not a number",
variable
)))
}
}
Condition::LessThan { variable, value } => {
let var_value = context.get_variable(variable)?;
if let Some(num) = var_value.as_f64() {
Ok(num < *value)
} else {
Err(OrchestrationError::ConditionFailed(format!(
"Variable {} is not a number",
variable
)))
}
}
Condition::GreaterThanOrEqual { variable, value } => {
let var_value = context.get_variable(variable)?;
if let Some(num) = var_value.as_f64() {
Ok(num >= *value)
} else {
Err(OrchestrationError::ConditionFailed(format!(
"Variable {} is not a number",
variable
)))
}
}
Condition::LessThanOrEqual { variable, value } => {
let var_value = context.get_variable(variable)?;
if let Some(num) = var_value.as_f64() {
Ok(num <= *value)
} else {
Err(OrchestrationError::ConditionFailed(format!(
"Variable {} is not a number",
variable
)))
}
}
Condition::Exists { variable } => Ok(context.variables.contains_key(variable)),
Condition::And { conditions } => {
for cond in conditions {
if !cond.evaluate(context)? {
return Ok(false);
}
}
Ok(true)
}
Condition::Or { conditions } => {
for cond in conditions {
if cond.evaluate(context)? {
return Ok(true);
}
}
Ok(false)
}
Condition::Not { condition } => Ok(!condition.evaluate(context)?),
Condition::PreviousStepSucceeded => Ok(context.last_step_success),
Condition::PreviousStepFailed => Ok(!context.last_step_success),
Condition::MetricThreshold {
metric_name,
operator,
threshold,
} => {
if let Some(value) = context.metrics.get(metric_name) {
Ok(match operator {
ComparisonOperator::Equals => (value - threshold).abs() < f64::EPSILON,
ComparisonOperator::NotEquals => (value - threshold).abs() >= f64::EPSILON,
ComparisonOperator::GreaterThan => value > threshold,
ComparisonOperator::LessThan => value < threshold,
ComparisonOperator::GreaterThanOrEqual => value >= threshold,
ComparisonOperator::LessThanOrEqual => value <= threshold,
})
} else {
Err(OrchestrationError::ConditionFailed(format!(
"Metric {} not found",
metric_name
)))
}
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConditionalStep {
pub name: String,
pub condition: Condition,
pub then_steps: Vec<AdvancedScenarioStep>,
pub else_steps: Vec<AdvancedScenarioStep>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HookType {
PreStep,
PostStep,
PreOrchestration,
PostOrchestration,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum HookAction {
SetVariable { name: String, value: JsonValue },
Log { message: String, level: LogLevel },
HttpRequest {
url: String,
method: String,
#[serde(default, deserialize_with = "deserialize_body_string")]
body: Option<String>,
},
Command { command: String, args: Vec<String> },
RecordMetric { name: String, value: f64 },
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hook {
pub name: String,
pub hook_type: HookType,
pub actions: Vec<HookAction>,
pub condition: Option<Condition>,
}
impl Hook {
pub async fn execute(&self, context: &mut ExecutionContext) -> Result<(), OrchestrationError> {
if let Some(condition) = &self.condition {
if !condition.evaluate(context)? {
return Ok(());
}
}
for action in &self.actions {
self.execute_action(action, context).await?;
}
Ok(())
}
async fn execute_action(
&self,
action: &HookAction,
context: &mut ExecutionContext,
) -> Result<(), OrchestrationError> {
match action {
HookAction::SetVariable { name, value } => {
context.set_variable(name.clone(), value.clone());
Ok(())
}
HookAction::Log { message, level } => {
use tracing::{debug, error, info, trace, warn};
match level {
LogLevel::Trace => trace!("[Hook: {}] {}", self.name, message),
LogLevel::Debug => debug!("[Hook: {}] {}", self.name, message),
LogLevel::Info => info!("[Hook: {}] {}", self.name, message),
LogLevel::Warn => warn!("[Hook: {}] {}", self.name, message),
LogLevel::Error => error!("[Hook: {}] {}", self.name, message),
}
Ok(())
}
HookAction::HttpRequest { url, method, body } => {
let client =
Client::builder().timeout(Duration::from_secs(30)).build().map_err(|e| {
OrchestrationError::HookFailed(format!(
"Failed to create HTTP client: {}",
e
))
})?;
let http_method = method.parse::<reqwest::Method>().map_err(|e| {
OrchestrationError::HookFailed(format!(
"Invalid HTTP method '{}': {}",
method, e
))
})?;
let mut request_builder = client.request(http_method.clone(), url);
if let Some(body_value) = body {
request_builder = request_builder
.header("Content-Type", "application/json")
.body(body_value.clone());
}
match request_builder.send().await {
Ok(response) => {
let status = response.status();
let response_body = response.text().await.unwrap_or_default();
tracing::info!(
"[Hook: {}] HTTP {} {} → {} (body: {})",
self.name,
http_method,
url,
status,
if response_body.len() > 100 {
format!("{}...", &response_body[..100])
} else {
response_body
}
);
Ok(())
}
Err(e) => {
tracing::warn!(
"[Hook: {}] HTTP {} {} failed: {}",
self.name,
http_method,
url,
e
);
Ok(())
}
}
}
HookAction::Command { command, args } => {
tracing::info!("[Hook: {}] Execute: {} {:?}", self.name, command, args);
Ok(())
}
HookAction::RecordMetric { name, value } => {
context.record_metric(name.clone(), *value);
Ok(())
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Assertion {
VariableEquals {
variable: String,
expected: JsonValue,
},
MetricInRange { metric: String, min: f64, max: f64 },
StepSucceeded { step_name: String },
StepFailed { step_name: String },
Condition { condition: Condition },
}
impl Assertion {
pub fn validate(&self, context: &ExecutionContext) -> Result<bool, OrchestrationError> {
match self {
Assertion::VariableEquals { variable, expected } => {
let value = context.get_variable(variable)?;
Ok(value == expected)
}
Assertion::MetricInRange { metric, min, max } => {
if let Some(value) = context.metrics.get(metric) {
Ok(*value >= *min && *value <= *max)
} else {
Ok(false)
}
}
Assertion::StepSucceeded { step_name } => {
if let Some(result) = context.step_results.get(step_name) {
Ok(result.success)
} else {
Ok(false)
}
}
Assertion::StepFailed { step_name } => {
if let Some(result) = context.step_results.get(step_name) {
Ok(!result.success)
} else {
Ok(false)
}
}
Assertion::Condition { condition } => condition.evaluate(context),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdvancedScenarioStep {
#[serde(flatten)]
pub base: ScenarioStep,
pub condition: Option<Condition>,
pub pre_hooks: Vec<Hook>,
pub post_hooks: Vec<Hook>,
pub assertions: Vec<Assertion>,
pub variables: HashMap<String, JsonValue>,
pub timeout_seconds: Option<u64>,
pub retry: Option<RetryConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryConfig {
pub max_attempts: usize,
pub delay_seconds: u64,
pub exponential_backoff: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepResult {
pub step_name: String,
pub success: bool,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub duration_seconds: f64,
pub error: Option<String>,
pub assertion_results: Vec<AssertionResult>,
pub metrics: HashMap<String, f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssertionResult {
pub description: String,
pub passed: bool,
pub error: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ExecutionContext {
pub variables: HashMap<String, JsonValue>,
pub metrics: HashMap<String, f64>,
pub step_results: HashMap<String, StepResult>,
pub last_step_success: bool,
pub iteration: usize,
}
impl ExecutionContext {
pub fn new() -> Self {
Self {
variables: HashMap::new(),
metrics: HashMap::new(),
step_results: HashMap::new(),
last_step_success: true,
iteration: 0,
}
}
pub fn set_variable(&mut self, name: String, value: JsonValue) {
self.variables.insert(name, value);
}
pub fn get_variable(&self, name: &str) -> Result<&JsonValue, OrchestrationError> {
self.variables
.get(name)
.ok_or_else(|| OrchestrationError::VariableNotFound(name.to_string()))
}
pub fn record_metric(&mut self, name: String, value: f64) {
self.metrics.insert(name, value);
}
pub fn record_step_result(&mut self, result: StepResult) {
self.last_step_success = result.success;
self.step_results.insert(result.step_name.clone(), result);
}
}
impl Default for ExecutionContext {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdvancedOrchestratedScenario {
#[serde(flatten)]
pub base: OrchestratedScenario,
pub advanced_steps: Vec<AdvancedScenarioStep>,
pub conditional_steps: Vec<ConditionalStep>,
pub hooks: Vec<Hook>,
pub assertions: Vec<Assertion>,
pub variables: HashMap<String, JsonValue>,
pub enable_reporting: bool,
pub report_path: Option<String>,
}
impl AdvancedOrchestratedScenario {
pub fn from_base(base: OrchestratedScenario) -> Self {
Self {
base,
advanced_steps: Vec::new(),
conditional_steps: Vec::new(),
hooks: Vec::new(),
assertions: Vec::new(),
variables: HashMap::new(),
enable_reporting: false,
report_path: None,
}
}
pub fn with_variable(mut self, name: String, value: JsonValue) -> Self {
self.variables.insert(name, value);
self
}
pub fn with_hook(mut self, hook: Hook) -> Self {
self.hooks.push(hook);
self
}
pub fn with_assertion(mut self, assertion: Assertion) -> Self {
self.assertions.push(assertion);
self
}
pub fn with_reporting(mut self, path: Option<String>) -> Self {
self.enable_reporting = true;
self.report_path = path;
self
}
pub fn to_json(&self) -> Result<String, OrchestrationError> {
serde_json::to_string_pretty(self)
.map_err(|e| OrchestrationError::SerializationError(e.to_string()))
}
pub fn to_yaml(&self) -> Result<String, OrchestrationError> {
serde_yaml::to_string(self)
.map_err(|e| OrchestrationError::SerializationError(e.to_string()))
}
pub fn from_json(json: &str) -> Result<Self, OrchestrationError> {
serde_json::from_str(json)
.map_err(|e| OrchestrationError::SerializationError(e.to_string()))
}
pub fn from_yaml(yaml: &str) -> Result<Self, OrchestrationError> {
serde_yaml::from_str(yaml)
.map_err(|e| OrchestrationError::SerializationError(e.to_string()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionReport {
pub orchestration_name: String,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub total_duration_seconds: f64,
pub success: bool,
pub step_results: Vec<StepResult>,
pub assertion_results: Vec<AssertionResult>,
pub final_variables: HashMap<String, JsonValue>,
pub final_metrics: HashMap<String, f64>,
pub errors: Vec<String>,
}
impl ExecutionReport {
pub fn new(orchestration_name: String, start_time: DateTime<Utc>) -> Self {
Self {
orchestration_name,
start_time,
end_time: Utc::now(),
total_duration_seconds: 0.0,
success: true,
step_results: Vec::new(),
assertion_results: Vec::new(),
final_variables: HashMap::new(),
final_metrics: HashMap::new(),
errors: Vec::new(),
}
}
pub fn finalize(mut self, context: &ExecutionContext) -> Self {
self.end_time = Utc::now();
self.total_duration_seconds =
(self.end_time - self.start_time).num_milliseconds() as f64 / 1000.0;
self.final_variables = context.variables.clone();
self.final_metrics = context.metrics.clone();
self.step_results = context.step_results.values().cloned().collect();
self.success = self.step_results.iter().all(|r| r.success) && self.errors.is_empty();
self
}
pub fn to_json(&self) -> Result<String, OrchestrationError> {
serde_json::to_string_pretty(self)
.map_err(|e| OrchestrationError::SerializationError(e.to_string()))
}
pub fn to_html(&self) -> String {
format!(
r#"<!DOCTYPE html>
<html>
<head>
<title>Chaos Orchestration Report: {}</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
.header {{ background: #f5f5f5; padding: 20px; border-radius: 5px; }}
.success {{ color: green; }}
.failure {{ color: red; }}
table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
th {{ background: #f5f5f5; }}
</style>
</head>
<body>
<div class="header">
<h1>Chaos Orchestration Report</h1>
<h2>{}</h2>
<p><strong>Status:</strong> <span class="{}">{}</span></p>
<p><strong>Duration:</strong> {:.2} seconds</p>
<p><strong>Start Time:</strong> {}</p>
<p><strong>End Time:</strong> {}</p>
</div>
<h2>Step Results</h2>
<table>
<tr>
<th>Step</th>
<th>Status</th>
<th>Duration (s)</th>
<th>Assertions</th>
</tr>
{}
</table>
<h2>Metrics</h2>
<table>
<tr>
<th>Metric</th>
<th>Value</th>
</tr>
{}
</table>
</body>
</html>"#,
self.orchestration_name,
self.orchestration_name,
if self.success { "success" } else { "failure" },
if self.success { "SUCCESS" } else { "FAILURE" },
self.total_duration_seconds,
self.start_time,
self.end_time,
self.step_results
.iter()
.map(|r| format!(
"<tr><td>{}</td><td class=\"{}\">{}</td><td>{:.2}</td><td>{}/{}</td></tr>",
r.step_name,
if r.success { "success" } else { "failure" },
if r.success { "SUCCESS" } else { "FAILURE" },
r.duration_seconds,
r.assertion_results.iter().filter(|a| a.passed).count(),
r.assertion_results.len()
))
.collect::<Vec<_>>()
.join("\n "),
self.final_metrics
.iter()
.map(|(k, v)| format!("<tr><td>{}</td><td>{:.2}</td></tr>", k, v))
.collect::<Vec<_>>()
.join("\n ")
)
}
}
#[derive(Debug, Clone)]
pub struct OrchestrationLibrary {
orchestrations: Arc<RwLock<HashMap<String, AdvancedOrchestratedScenario>>>,
}
impl OrchestrationLibrary {
pub fn new() -> Self {
Self {
orchestrations: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn store(&self, name: String, orchestration: AdvancedOrchestratedScenario) {
let mut orch = self.orchestrations.write();
orch.insert(name, orchestration);
}
pub fn retrieve(&self, name: &str) -> Option<AdvancedOrchestratedScenario> {
let orch = self.orchestrations.read();
orch.get(name).cloned()
}
pub fn list(&self) -> Vec<String> {
let orch = self.orchestrations.read();
orch.keys().cloned().collect()
}
pub fn delete(&self, name: &str) -> bool {
let mut orch = self.orchestrations.write();
orch.remove(name).is_some()
}
pub fn import_from_directory(&self, _path: &str) -> Result<usize, OrchestrationError> {
Ok(0)
}
pub fn export_to_directory(&self, _path: &str) -> Result<usize, OrchestrationError> {
let orch = self.orchestrations.read();
Ok(orch.len())
}
}
impl Default for OrchestrationLibrary {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_condition_equals() {
let mut context = ExecutionContext::new();
context.set_variable("test".to_string(), JsonValue::String("value".to_string()));
let condition = Condition::Equals {
variable: "test".to_string(),
value: JsonValue::String("value".to_string()),
};
assert!(condition.evaluate(&context).unwrap());
}
#[test]
fn test_condition_and() {
let mut context = ExecutionContext::new();
context.set_variable("a".to_string(), JsonValue::Number(5.into()));
context.set_variable("b".to_string(), JsonValue::Number(10.into()));
let condition = Condition::And {
conditions: vec![
Condition::GreaterThan {
variable: "a".to_string(),
value: 3.0,
},
Condition::LessThan {
variable: "b".to_string(),
value: 15.0,
},
],
};
assert!(condition.evaluate(&context).unwrap());
}
#[test]
fn test_execution_context() {
let mut context = ExecutionContext::new();
context.set_variable("test".to_string(), JsonValue::String("value".to_string()));
context.record_metric("latency".to_string(), 100.0);
assert_eq!(context.get_variable("test").unwrap(), &JsonValue::String("value".to_string()));
assert_eq!(*context.metrics.get("latency").unwrap(), 100.0);
}
#[test]
fn test_orchestration_library() {
let library = OrchestrationLibrary::new();
let orch = AdvancedOrchestratedScenario::from_base(OrchestratedScenario::new("test"));
library.store("test".to_string(), orch.clone());
let retrieved = library.retrieve("test");
assert!(retrieved.is_some());
let list = library.list();
assert_eq!(list.len(), 1);
let deleted = library.delete("test");
assert!(deleted);
let list = library.list();
assert_eq!(list.len(), 0);
}
#[test]
fn test_execution_report() {
let report = ExecutionReport::new("test".to_string(), Utc::now());
let context = ExecutionContext::new();
let final_report = report.finalize(&context);
assert_eq!(final_report.orchestration_name, "test");
assert!(final_report.total_duration_seconds >= 0.0);
}
}