1use crate::config::{ChaosConfig, FaultInjectionConfig, LatencyConfig};
8use crate::scenarios::ChaosScenario;
9use serde::{Deserialize, Serialize};
10use serde_json::{json, Value};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct FailureDesignRule {
18 pub name: String,
20 pub target: FailureTarget,
22 pub failure_type: FailureType,
24 #[serde(default)]
26 pub conditions: Vec<FailureCondition>,
27 pub probability: f64,
29 #[serde(default)]
31 pub description: Option<String>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct FailureTarget {
37 #[serde(default)]
39 pub endpoints: Vec<String>,
40 #[serde(default)]
42 pub user_agents: Option<Vec<String>>,
43 #[serde(default)]
45 pub ip_ranges: Option<Vec<String>>,
46 #[serde(default)]
48 pub headers: Option<HashMap<String, String>>,
49 #[serde(default)]
51 pub methods: Option<Vec<String>>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(tag = "type", rename_all = "snake_case")]
57pub enum FailureType {
58 WebhookFailure {
60 webhook_pattern: String,
62 },
63 StatusCode {
65 code: u16,
67 },
68 Latency {
70 delay_ms: u64,
72 },
73 Timeout {
75 timeout_ms: u64,
77 },
78 ConnectionError,
80 PartialResponse {
82 truncate_percentage: f64,
84 },
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct FailureCondition {
90 pub condition_type: ConditionType,
92 pub field: String,
94 pub operator: ConditionOperator,
96 pub value: Value,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103pub enum ConditionType {
104 Header,
106 Query,
108 Body,
110 Path,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116#[serde(rename_all = "snake_case")]
117pub enum ConditionOperator {
118 Equals,
120 NotEquals,
122 Contains,
124 Matches,
126 GreaterThan,
128 LessThan,
130}
131
132pub struct FailureDesigner;
136
137impl FailureDesigner {
138 pub fn new() -> Self {
140 Self
141 }
142
143 pub fn validate_rule(&self, rule: &FailureDesignRule) -> Result<(), String> {
145 if rule.probability < 0.0 || rule.probability > 1.0 {
147 return Err("Probability must be between 0.0 and 1.0".to_string());
148 }
149
150 if rule.target.endpoints.is_empty() {
152 return Err("At least one endpoint must be specified".to_string());
153 }
154
155 match &rule.failure_type {
157 FailureType::WebhookFailure { webhook_pattern } => {
158 if webhook_pattern.is_empty() {
159 return Err("Webhook pattern cannot be empty".to_string());
160 }
161 }
162 FailureType::StatusCode { code } => {
163 if *code < 100 || *code > 599 {
164 return Err("Status code must be between 100 and 599".to_string());
165 }
166 }
167 FailureType::Latency { delay_ms } => {
168 if *delay_ms == 0 {
169 return Err("Delay must be greater than 0".to_string());
170 }
171 }
172 FailureType::Timeout { timeout_ms } => {
173 if *timeout_ms == 0 {
174 return Err("Timeout must be greater than 0".to_string());
175 }
176 }
177 FailureType::PartialResponse {
178 truncate_percentage,
179 } => {
180 if *truncate_percentage < 0.0 || *truncate_percentage > 1.0 {
181 return Err("Truncate percentage must be between 0.0 and 1.0".to_string());
182 }
183 }
184 FailureType::ConnectionError => {
185 }
187 }
188
189 Ok(())
190 }
191
192 pub fn rule_to_scenario(&self, rule: &FailureDesignRule) -> Result<ChaosScenario, String> {
194 self.validate_rule(rule)?;
196
197 let chaos_config = match &rule.failure_type {
199 FailureType::StatusCode { code } => {
200 let fault_config = FaultInjectionConfig {
201 enabled: true,
202 http_errors: vec![*code],
203 http_error_probability: rule.probability,
204 connection_errors: false,
205 connection_error_probability: 0.0,
206 timeout_errors: false,
207 timeout_ms: 5000,
208 timeout_probability: 0.0,
209 partial_responses: false,
210 partial_response_probability: 0.0,
211 payload_corruption: false,
212 payload_corruption_probability: 0.0,
213 corruption_type: crate::config::CorruptionType::None,
214 error_pattern: None,
215 mockai_enabled: false,
216 };
217
218 ChaosConfig {
219 enabled: true,
220 latency: None,
221 fault_injection: Some(fault_config),
222 rate_limit: None,
223 traffic_shaping: None,
224 circuit_breaker: None,
225 bulkhead: None,
226 }
227 }
228 FailureType::Latency { delay_ms } => {
229 let latency_config = LatencyConfig {
230 enabled: true,
231 fixed_delay_ms: Some(*delay_ms),
232 random_delay_range_ms: None,
233 jitter_percent: 0.0,
234 probability: rule.probability,
235 };
236
237 ChaosConfig {
238 enabled: true,
239 latency: Some(latency_config),
240 fault_injection: None,
241 rate_limit: None,
242 traffic_shaping: None,
243 circuit_breaker: None,
244 bulkhead: None,
245 }
246 }
247 FailureType::Timeout { timeout_ms } => {
248 let fault_config = FaultInjectionConfig {
249 enabled: true,
250 http_errors: vec![],
251 http_error_probability: 0.0,
252 connection_errors: false,
253 connection_error_probability: 0.0,
254 timeout_errors: true,
255 timeout_ms: *timeout_ms,
256 timeout_probability: rule.probability,
257 partial_responses: false,
258 partial_response_probability: 0.0,
259 payload_corruption: false,
260 payload_corruption_probability: 0.0,
261 corruption_type: crate::config::CorruptionType::None,
262 error_pattern: None,
263 mockai_enabled: false,
264 };
265
266 ChaosConfig {
267 enabled: true,
268 latency: None,
269 fault_injection: Some(fault_config),
270 rate_limit: None,
271 traffic_shaping: None,
272 circuit_breaker: None,
273 bulkhead: None,
274 }
275 }
276 FailureType::ConnectionError => {
277 let fault_config = FaultInjectionConfig {
278 enabled: true,
279 http_errors: vec![],
280 http_error_probability: 0.0,
281 connection_errors: true,
282 connection_error_probability: rule.probability,
283 timeout_errors: false,
284 timeout_ms: 5000,
285 timeout_probability: 0.0,
286 partial_responses: false,
287 partial_response_probability: 0.0,
288 payload_corruption: false,
289 payload_corruption_probability: 0.0,
290 corruption_type: crate::config::CorruptionType::None,
291 error_pattern: None,
292 mockai_enabled: false,
293 };
294
295 ChaosConfig {
296 enabled: true,
297 latency: None,
298 fault_injection: Some(fault_config),
299 rate_limit: None,
300 traffic_shaping: None,
301 circuit_breaker: None,
302 bulkhead: None,
303 }
304 }
305 FailureType::PartialResponse {
306 truncate_percentage,
307 } => {
308 let fault_config = FaultInjectionConfig {
309 enabled: true,
310 http_errors: vec![],
311 http_error_probability: 0.0,
312 connection_errors: false,
313 connection_error_probability: 0.0,
314 timeout_errors: false,
315 timeout_ms: 5000,
316 timeout_probability: 0.0,
317 partial_responses: true,
318 partial_response_probability: rule.probability,
319 payload_corruption: false,
320 payload_corruption_probability: 0.0,
321 corruption_type: crate::config::CorruptionType::None,
322 error_pattern: None,
323 mockai_enabled: false,
324 };
325
326 ChaosConfig {
327 enabled: true,
328 latency: None,
329 fault_injection: Some(fault_config),
330 rate_limit: None,
331 traffic_shaping: None,
332 circuit_breaker: None,
333 bulkhead: None,
334 }
335 }
336 FailureType::WebhookFailure { .. } => {
337 ChaosConfig::default()
340 }
341 };
342
343 let scenario = ChaosScenario::new(rule.name.clone(), chaos_config)
345 .with_description(rule.description.clone().unwrap_or_default());
346
347 Ok(scenario)
348 }
349
350 pub fn generate_webhook_hook(&self, rule: &FailureDesignRule) -> Result<Value, String> {
355 if let FailureType::WebhookFailure { webhook_pattern } = &rule.failure_type {
356 Ok(json!({
358 "type": "webhook_failure",
359 "name": rule.name,
360 "webhook_pattern": webhook_pattern,
361 "probability": rule.probability,
362 "target": rule.target,
363 "conditions": rule.conditions,
364 }))
365 } else {
366 Err("Rule is not a webhook failure type".to_string())
367 }
368 }
369
370 pub fn generate_route_chaos_config(&self, rule: &FailureDesignRule) -> Result<Value, String> {
375 self.validate_rule(rule)?;
377
378 let mut route_configs = Vec::new();
380
381 for endpoint in &rule.target.endpoints {
382 let mut route_config = json!({
383 "path": endpoint,
384 "probability": rule.probability,
385 });
386
387 if let Some(methods) = &rule.target.methods {
389 route_config["methods"] = json!(methods);
390 }
391
392 match &rule.failure_type {
394 FailureType::StatusCode { code } => {
395 route_config["fault_injection"] = json!({
396 "enabled": true,
397 "status_code": code,
398 });
399 }
400 FailureType::Latency { delay_ms } => {
401 route_config["latency"] = json!({
402 "enabled": true,
403 "delay_ms": delay_ms,
404 });
405 }
406 FailureType::Timeout { timeout_ms } => {
407 route_config["fault_injection"] = json!({
408 "enabled": true,
409 "timeout": true,
410 "timeout_ms": timeout_ms,
411 });
412 }
413 FailureType::ConnectionError => {
414 route_config["fault_injection"] = json!({
415 "enabled": true,
416 "connection_error": true,
417 });
418 }
419 FailureType::PartialResponse {
420 truncate_percentage,
421 } => {
422 route_config["fault_injection"] = json!({
423 "enabled": true,
424 "partial_response": true,
425 "truncate_percentage": truncate_percentage,
426 });
427 }
428 FailureType::WebhookFailure { .. } => {
429 continue;
431 }
432 }
433
434 if !rule.conditions.is_empty() {
436 route_config["conditions"] = json!(rule.conditions);
437 }
438
439 if let Some(user_agents) = &rule.target.user_agents {
441 route_config["user_agent_patterns"] = json!(user_agents);
442 }
443
444 if let Some(ip_ranges) = &rule.target.ip_ranges {
446 route_config["ip_ranges"] = json!(ip_ranges);
447 }
448
449 if let Some(headers) = &rule.target.headers {
451 route_config["header_filters"] = json!(headers);
452 }
453
454 route_configs.push(route_config);
455 }
456
457 Ok(json!({
458 "routes": route_configs,
459 }))
460 }
461}
462
463impl Default for FailureDesigner {
464 fn default() -> Self {
465 Self::new()
466 }
467}