mockforge_intelligence/voice/
workspace_scenario_generator.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct GeneratedWorkspaceScenario {
20 pub workspace_id: String,
22 pub name: String,
24 pub description: String,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub openapi_spec: Option<String>, pub chaos_config: Option<String>,
31 pub fixtures: HashMap<String, Vec<Value>>,
33 pub config_summary: WorkspaceConfigSummary,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct WorkspaceConfigSummary {
40 pub endpoint_count: usize,
42 pub model_count: usize,
44 pub chaos_characteristic_count: usize,
46 pub initial_data_counts: HashMap<String, usize>,
48}
49
50pub struct WorkspaceScenarioGenerator;
52
53impl WorkspaceScenarioGenerator {
54 pub fn new() -> Self {
56 Self
57 }
58
59 pub async fn generate_scenario(
61 &self,
62 parsed: &ParsedWorkspaceScenario,
63 ) -> Result<GeneratedWorkspaceScenario> {
64 let workspace_id = Uuid::new_v4().to_string();
66
67 let openapi_spec = if !parsed.api_requirements.endpoints.is_empty() {
69 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 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 let spec_generator = VoiceSpecGenerator::new();
97 let spec_result = spec_generator.generate_spec(&parsed_command).await;
98 spec_result.ok().and_then(|spec| {
100 if let Some(ref raw) = spec.raw_document {
102 serde_json::to_string(raw).ok()
103 } else {
104 serde_json::to_string(&spec.spec).ok()
106 }
107 })
108 } else {
109 None
110 };
111
112 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 let fixtures = self.generate_fixtures(parsed)?;
121
122 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 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 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 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 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 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 if let Some(rate) = char.config.get("error_rate").and_then(|v| v.as_f64()) {
207 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 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 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 fn generate_fixtures(
274 &self,
275 parsed: &ParsedWorkspaceScenario,
276 ) -> Result<HashMap<String, Vec<Value>>> {
277 let mut fixtures = HashMap::new();
278
279 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 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 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 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}