Skip to main content

mockforge_intelligence/voice/
workspace_scenario_generator.rs

1//! Workspace scenario generator
2//!
3//! Generates complete workspace configurations from parsed scenario descriptions,
4//! including OpenAPI specs, chaos configs, initial data, and workspace structure.
5
6use chrono::Utc;
7use mockforge_foundation::Result;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use serde_yaml;
11use std::collections::HashMap;
12use uuid::Uuid;
13
14use super::command_parser::ParsedWorkspaceScenario;
15use super::spec_generator::VoiceSpecGenerator;
16
17/// Generated workspace scenario
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct GeneratedWorkspaceScenario {
20    /// Workspace ID
21    pub workspace_id: String,
22    /// Workspace name
23    pub name: String,
24    /// Workspace description
25    pub description: String,
26    /// Generated OpenAPI specification (serialized as JSON string since OpenApiSpec doesn't implement Serialize)
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub openapi_spec: Option<String>, // Serialized as JSON string
29    /// Chaos configuration (YAML)
30    pub chaos_config: Option<String>,
31    /// Initial fixture data
32    pub fixtures: HashMap<String, Vec<Value>>,
33    /// Workspace configuration summary
34    pub config_summary: WorkspaceConfigSummary,
35}
36
37/// Workspace configuration summary
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct WorkspaceConfigSummary {
40    /// Number of endpoints
41    pub endpoint_count: usize,
42    /// Number of models
43    pub model_count: usize,
44    /// Number of chaos characteristics
45    pub chaos_characteristic_count: usize,
46    /// Initial data counts
47    pub initial_data_counts: HashMap<String, usize>,
48}
49
50/// Generator for workspace scenarios
51pub struct WorkspaceScenarioGenerator;
52
53impl WorkspaceScenarioGenerator {
54    /// Create a new workspace scenario generator
55    pub fn new() -> Self {
56        Self
57    }
58
59    /// Generate a complete workspace scenario from parsed description
60    pub async fn generate_scenario(
61        &self,
62        parsed: &ParsedWorkspaceScenario,
63    ) -> Result<GeneratedWorkspaceScenario> {
64        // Generate workspace ID
65        let workspace_id = Uuid::new_v4().to_string();
66
67        // Generate OpenAPI spec from API requirements
68        let openapi_spec = if !parsed.api_requirements.endpoints.is_empty() {
69            // Convert API requirements to ParsedCommand format for spec generation
70            let mut parsed_command = super::command_parser::ParsedCommand {
71                api_type: parsed.domain.clone(),
72                title: parsed.title.clone(),
73                description: parsed.description.clone(),
74                endpoints: parsed.api_requirements.endpoints.clone(),
75                models: parsed.api_requirements.models.clone(),
76                relationships: vec![],
77                sample_counts: HashMap::new(),
78                flows: vec![],
79            };
80
81            // Add sample counts from initial data
82            if let Some(user_count) = parsed.initial_data.users {
83                parsed_command.sample_counts.insert("User".to_string(), user_count);
84            }
85            if let Some(dispute_count) = parsed.initial_data.disputes {
86                parsed_command.sample_counts.insert("Dispute".to_string(), dispute_count);
87            }
88            if let Some(order_count) = parsed.initial_data.orders {
89                parsed_command.sample_counts.insert("Order".to_string(), order_count);
90            }
91            for (entity, count) in &parsed.initial_data.custom {
92                parsed_command.sample_counts.insert(entity.clone(), *count);
93            }
94
95            // Generate spec
96            let spec_generator = VoiceSpecGenerator::new();
97            let spec_result = spec_generator.generate_spec(&parsed_command).await;
98            // Convert OpenApiSpec to JSON string for serialization
99            spec_result.ok().and_then(|spec| {
100                // Use raw_document if available, otherwise serialize the spec
101                if let Some(ref raw) = spec.raw_document {
102                    serde_json::to_string(raw).ok()
103                } else {
104                    // Fallback: try to serialize the spec struct
105                    serde_json::to_string(&spec.spec).ok()
106                }
107            })
108        } else {
109            None
110        };
111
112        // Generate chaos configuration
113        let chaos_config = if !parsed.chaos_characteristics.is_empty() {
114            Some(self.generate_chaos_config(&parsed.chaos_characteristics)?)
115        } else {
116            None
117        };
118
119        // Generate initial fixture data
120        let fixtures = self.generate_fixtures(parsed)?;
121
122        // Build config summary
123        let mut initial_data_counts = HashMap::new();
124        if let Some(count) = parsed.initial_data.users {
125            initial_data_counts.insert("users".to_string(), count);
126        }
127        if let Some(count) = parsed.initial_data.disputes {
128            initial_data_counts.insert("disputes".to_string(), count);
129        }
130        if let Some(count) = parsed.initial_data.orders {
131            initial_data_counts.insert("orders".to_string(), count);
132        }
133        for (entity, count) in &parsed.initial_data.custom {
134            initial_data_counts.insert(entity.clone(), *count);
135        }
136
137        let config_summary = WorkspaceConfigSummary {
138            endpoint_count: parsed.api_requirements.endpoints.len(),
139            model_count: parsed.api_requirements.models.len(),
140            chaos_characteristic_count: parsed.chaos_characteristics.len(),
141            initial_data_counts,
142        };
143
144        Ok(GeneratedWorkspaceScenario {
145            workspace_id,
146            name: parsed.title.clone(),
147            description: parsed.description.clone(),
148            openapi_spec,
149            chaos_config,
150            fixtures,
151            config_summary,
152        })
153    }
154
155    /// Generate chaos configuration YAML from characteristics
156    fn generate_chaos_config(
157        &self,
158        characteristics: &[super::command_parser::ChaosCharacteristic],
159    ) -> Result<String> {
160        let mut config = serde_yaml::Mapping::new();
161
162        // Build chaos configuration
163        let mut chaos = serde_yaml::Mapping::new();
164        chaos.insert(
165            serde_yaml::Value::String("enabled".to_string()),
166            serde_yaml::Value::Bool(true),
167        );
168
169        // Process each characteristic
170        for char in characteristics {
171            match char.r#type.as_str() {
172                "latency" | "slow" => {
173                    let mut latency = serde_yaml::Mapping::new();
174                    latency.insert(
175                        serde_yaml::Value::String("enabled".to_string()),
176                        serde_yaml::Value::Bool(true),
177                    );
178
179                    // Extract delay from config
180                    if let Some(delay) = char.config.get("delay_ms").and_then(|v| v.as_u64()) {
181                        latency.insert(
182                            serde_yaml::Value::String("fixed_delay_ms".to_string()),
183                            serde_yaml::Value::Number(delay.into()),
184                        );
185                    } else {
186                        // Default slow latency
187                        latency.insert(
188                            serde_yaml::Value::String("fixed_delay_ms".to_string()),
189                            serde_yaml::Value::Number(1000.into()),
190                        );
191                    }
192
193                    chaos.insert(
194                        serde_yaml::Value::String("latency".to_string()),
195                        serde_yaml::Value::Mapping(latency),
196                    );
197                }
198                "failure" | "flaky" | "error" => {
199                    let mut fault = serde_yaml::Mapping::new();
200                    fault.insert(
201                        serde_yaml::Value::String("enabled".to_string()),
202                        serde_yaml::Value::Bool(true),
203                    );
204
205                    // Extract error rate and codes
206                    if let Some(rate) = char.config.get("error_rate").and_then(|v| v.as_f64()) {
207                        // serde_yaml::Number doesn't have from_f64, so we convert to string and parse
208                        let num_str = rate.to_string();
209                        fault.insert(
210                            serde_yaml::Value::String("http_error_probability".to_string()),
211                            serde_yaml::Value::Number(
212                                num_str
213                                    .parse::<serde_yaml::Number>()
214                                    .unwrap_or_else(|_| serde_yaml::Number::from(0.1)),
215                            ),
216                        );
217                    } else {
218                        fault.insert(
219                            serde_yaml::Value::String("http_error_probability".to_string()),
220                            serde_yaml::Value::Number(0.1.into()),
221                        );
222                    }
223
224                    if let Some(codes) = char.config.get("error_codes").and_then(|v| v.as_array()) {
225                        let codes: Vec<serde_yaml::Value> = codes
226                            .iter()
227                            .filter_map(|v| v.as_u64().map(|n| serde_yaml::Value::Number(n.into())))
228                            .collect();
229                        fault.insert(
230                            serde_yaml::Value::String("http_errors".to_string()),
231                            serde_yaml::Value::Sequence(codes),
232                        );
233                    } else {
234                        fault.insert(
235                            serde_yaml::Value::String("http_errors".to_string()),
236                            serde_yaml::Value::Sequence(vec![
237                                serde_yaml::Value::Number(500.into()),
238                                serde_yaml::Value::Number(502.into()),
239                                serde_yaml::Value::Number(503.into()),
240                            ]),
241                        );
242                    }
243
244                    chaos.insert(
245                        serde_yaml::Value::String("fault_injection".to_string()),
246                        serde_yaml::Value::Mapping(fault),
247                    );
248                }
249                _ => {
250                    // Generic characteristic - add to config as-is
251                    if let Ok(value) = serde_yaml::to_value(&char.config) {
252                        chaos.insert(serde_yaml::Value::String(char.r#type.clone()), value);
253                    }
254                }
255            }
256        }
257
258        config.insert(
259            serde_yaml::Value::String("chaos".to_string()),
260            serde_yaml::Value::Mapping(chaos),
261        );
262
263        // Convert to YAML string
264        serde_yaml::to_string(&config).map_err(|e| {
265            mockforge_foundation::Error::config(format!(
266                "Failed to serialize chaos config to YAML: {}",
267                e
268            ))
269        })
270    }
271
272    /// Generate initial fixture data
273    fn generate_fixtures(
274        &self,
275        parsed: &ParsedWorkspaceScenario,
276    ) -> Result<HashMap<String, Vec<Value>>> {
277        let mut fixtures = HashMap::new();
278
279        // Generate user fixtures
280        if let Some(user_count) = parsed.initial_data.users {
281            let mut users = Vec::new();
282            for i in 0..user_count {
283                users.push(serde_json::json!({
284                    "id": i + 1,
285                    "name": format!("User {}", i + 1),
286                    "email": format!("user{}@example.com", i + 1),
287                    "created_at": Utc::now().to_rfc3339(),
288                }));
289            }
290            fixtures.insert("users".to_string(), users);
291        }
292
293        // Generate dispute fixtures
294        if let Some(dispute_count) = parsed.initial_data.disputes {
295            let mut disputes = Vec::new();
296            for i in 0..dispute_count {
297                disputes.push(serde_json::json!({
298                    "id": i + 1,
299                    "user_id": (i % parsed.initial_data.users.unwrap_or(1)) + 1,
300                    "status": "open",
301                    "description": format!("Dispute {}", i + 1),
302                    "created_at": Utc::now().to_rfc3339(),
303                }));
304            }
305            fixtures.insert("disputes".to_string(), disputes);
306        }
307
308        // Generate order fixtures
309        if let Some(order_count) = parsed.initial_data.orders {
310            let mut orders = Vec::new();
311            for i in 0..order_count {
312                orders.push(serde_json::json!({
313                    "id": i + 1,
314                    "user_id": (i % parsed.initial_data.users.unwrap_or(1)) + 1,
315                    "status": "pending",
316                    "total": 100.0 + (i as f64 * 10.0),
317                    "created_at": Utc::now().to_rfc3339(),
318                }));
319            }
320            fixtures.insert("orders".to_string(), orders);
321        }
322
323        // Generate custom entity fixtures
324        for (entity_name, count) in &parsed.initial_data.custom {
325            let mut entities = Vec::new();
326            for i in 0..*count {
327                entities.push(serde_json::json!({
328                    "id": i + 1,
329                    "name": format!("{} {}", entity_name, i + 1),
330                    "created_at": Utc::now().to_rfc3339(),
331                }));
332            }
333            fixtures.insert(entity_name.clone(), entities);
334        }
335
336        Ok(fixtures)
337    }
338}
339
340impl Default for WorkspaceScenarioGenerator {
341    fn default() -> Self {
342        Self::new()
343    }
344}