mockforge_http/handlers/
scenario_studio.rs

1//! Scenario Studio API handlers
2//!
3//! This module provides HTTP handlers for managing business flows in the Scenario Studio.
4
5use axum::{
6    extract::{Path, State},
7    http::StatusCode,
8    response::Json,
9};
10use mockforge_core::scenario_studio::{
11    FlowDefinition, FlowExecutionResult, FlowExecutor, FlowType, FlowVariant,
12};
13use serde::Deserialize;
14use serde_json::Value;
15use std::collections::HashMap;
16use std::sync::Arc;
17use tokio::sync::RwLock;
18use tracing::{error, info};
19
20/// State for scenario studio handlers
21#[derive(Clone)]
22pub struct ScenarioStudioState {
23    /// In-memory store for flows (can be replaced with database later)
24    flows: Arc<RwLock<HashMap<String, FlowDefinition>>>,
25    /// In-memory store for flow variants
26    variants: Arc<RwLock<HashMap<String, FlowVariant>>>,
27}
28
29impl ScenarioStudioState {
30    /// Create a new scenario studio state
31    pub fn new() -> Self {
32        Self {
33            flows: Arc::new(RwLock::new(HashMap::new())),
34            variants: Arc::new(RwLock::new(HashMap::new())),
35        }
36    }
37}
38
39impl Default for ScenarioStudioState {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45/// Request to create a flow
46#[derive(Debug, Deserialize)]
47pub struct CreateFlowRequest {
48    /// Flow name
49    pub name: String,
50    /// Optional description
51    pub description: Option<String>,
52    /// Flow type
53    pub flow_type: FlowType,
54    /// Optional tags
55    #[serde(default)]
56    pub tags: Vec<String>,
57}
58
59/// Request to update a flow
60#[derive(Debug, Deserialize)]
61pub struct UpdateFlowRequest {
62    /// Flow name
63    pub name: Option<String>,
64    /// Optional description
65    pub description: Option<String>,
66    /// Flow type
67    pub flow_type: Option<FlowType>,
68    /// Steps in the flow
69    pub steps: Option<Vec<mockforge_core::scenario_studio::FlowStep>>,
70    /// Connections between steps
71    pub connections: Option<Vec<mockforge_core::scenario_studio::FlowConnection>>,
72    /// Variables
73    pub variables: Option<HashMap<String, Value>>,
74    /// Tags
75    pub tags: Option<Vec<String>>,
76}
77
78/// Request to execute a flow
79#[derive(Debug, Deserialize)]
80pub struct ExecuteFlowRequest {
81    /// Optional initial variables
82    #[serde(default)]
83    pub variables: HashMap<String, Value>,
84}
85
86/// Request to create a flow variant
87#[derive(Debug, Deserialize)]
88pub struct CreateFlowVariantRequest {
89    /// Variant name
90    pub name: String,
91    /// Optional description
92    pub description: Option<String>,
93    /// ID of the base flow
94    pub flow_id: String,
95}
96
97/// Query parameters for workspace operations
98#[derive(Debug, Deserialize)]
99pub struct WorkspaceQuery {
100    /// Workspace ID (defaults to "default" if not provided)
101    #[serde(default = "default_workspace")]
102    pub workspace: String,
103}
104
105fn default_workspace() -> String {
106    "default".to_string()
107}
108
109/// Create a new flow
110///
111/// POST /api/v1/scenario-studio/flows
112pub async fn create_flow(
113    State(state): State<ScenarioStudioState>,
114    Json(request): Json<CreateFlowRequest>,
115) -> Result<Json<FlowDefinition>, StatusCode> {
116    let mut flow = FlowDefinition::new(request.name, request.flow_type);
117    flow.description = request.description;
118    flow.tags = request.tags;
119
120    let flow_id = flow.id.clone();
121    let mut flows = state.flows.write().await;
122    flows.insert(flow_id.clone(), flow.clone());
123
124    info!("Created flow: {}", flow_id);
125    Ok(Json(flow))
126}
127
128/// List all flows
129///
130/// GET /api/v1/scenario-studio/flows
131pub async fn list_flows(
132    State(state): State<ScenarioStudioState>,
133) -> Result<Json<Vec<FlowDefinition>>, StatusCode> {
134    let flows = state.flows.read().await;
135    let flows_list: Vec<FlowDefinition> = flows.values().cloned().collect();
136    Ok(Json(flows_list))
137}
138
139/// Get a specific flow
140///
141/// GET /api/v1/scenario-studio/flows/:id
142pub async fn get_flow(
143    State(state): State<ScenarioStudioState>,
144    Path(id): Path<String>,
145) -> Result<Json<FlowDefinition>, StatusCode> {
146    let flows = state.flows.read().await;
147    let flow = flows.get(&id).cloned().ok_or_else(|| {
148        error!("Flow not found: {}", id);
149        StatusCode::NOT_FOUND
150    })?;
151
152    Ok(Json(flow))
153}
154
155/// Update a flow
156///
157/// PUT /api/v1/scenario-studio/flows/:id
158pub async fn update_flow(
159    State(state): State<ScenarioStudioState>,
160    Path(id): Path<String>,
161    Json(request): Json<UpdateFlowRequest>,
162) -> Result<Json<FlowDefinition>, StatusCode> {
163    let mut flows = state.flows.write().await;
164    let flow = flows.get_mut(&id).ok_or_else(|| {
165        error!("Flow not found: {}", id);
166        StatusCode::NOT_FOUND
167    })?;
168
169    if let Some(name) = request.name {
170        flow.name = name;
171    }
172    if let Some(description) = request.description {
173        flow.description = Some(description);
174    }
175    if let Some(flow_type) = request.flow_type {
176        flow.flow_type = flow_type;
177    }
178    if let Some(steps) = request.steps {
179        flow.steps = steps;
180    }
181    if let Some(connections) = request.connections {
182        flow.connections = connections;
183    }
184    if let Some(variables) = request.variables {
185        flow.variables = variables;
186    }
187    if let Some(tags) = request.tags {
188        flow.tags = tags;
189    }
190
191    flow.updated_at = chrono::Utc::now();
192
193    let flow_clone = flow.clone();
194    info!("Updated flow: {}", id);
195    Ok(Json(flow_clone))
196}
197
198/// Delete a flow
199///
200/// DELETE /api/v1/scenario-studio/flows/:id
201pub async fn delete_flow(
202    State(state): State<ScenarioStudioState>,
203    Path(id): Path<String>,
204) -> Result<Json<Value>, StatusCode> {
205    let mut flows = state.flows.write().await;
206    if flows.remove(&id).is_none() {
207        error!("Flow not found: {}", id);
208        return Err(StatusCode::NOT_FOUND);
209    }
210
211    // Also remove any variants associated with this flow
212    let mut variants = state.variants.write().await;
213    variants.retain(|_, v| v.flow_id != id);
214
215    info!("Deleted flow: {}", id);
216    Ok(Json(serde_json::json!({
217        "success": true,
218        "message": format!("Flow {} deleted", id),
219    })))
220}
221
222/// Execute a flow
223///
224/// POST /api/v1/scenario-studio/flows/:id/execute
225#[axum::debug_handler]
226pub async fn execute_flow(
227    Path(id): Path<String>,
228    State(state): State<ScenarioStudioState>,
229    Json(request): Json<ExecuteFlowRequest>,
230) -> Result<Json<FlowExecutionResult>, StatusCode> {
231    let flows = state.flows.read().await;
232    let flow = flows.get(&id).cloned().ok_or_else(|| {
233        error!("Flow not found: {}", id);
234        StatusCode::NOT_FOUND
235    })?;
236
237    drop(flows); // Release lock before async operation
238
239    let initial_variables = request.variables;
240    let mut executor = FlowExecutor::with_variables(initial_variables);
241    let result = executor.execute(&flow).await.map_err(|e| {
242        error!("Failed to execute flow {}: {}", id, e);
243        StatusCode::INTERNAL_SERVER_ERROR
244    })?;
245
246    info!("Executed flow: {}", id);
247    Ok(Json(result))
248}
249
250/// Create a flow variant
251///
252/// POST /api/v1/scenario-studio/flows/:id/variants
253pub async fn create_flow_variant(
254    State(state): State<ScenarioStudioState>,
255    Path(id): Path<String>,
256    Json(request): Json<CreateFlowVariantRequest>,
257) -> Result<Json<FlowVariant>, StatusCode> {
258    // Verify that the base flow exists
259    let flows = state.flows.read().await;
260    if !flows.contains_key(&id) {
261        error!("Base flow not found: {}", id);
262        return Err(StatusCode::NOT_FOUND);
263    }
264    drop(flows);
265
266    let mut variant = FlowVariant::new(request.name, id.clone());
267    variant.description = request.description;
268
269    let variant_id = variant.id.clone();
270    let mut variants = state.variants.write().await;
271    variants.insert(variant_id.clone(), variant.clone());
272
273    info!("Created flow variant: {} for flow: {}", variant_id, id);
274    Ok(Json(variant))
275}
276
277/// List all variants for a flow
278///
279/// GET /api/v1/scenario-studio/flows/:id/variants
280pub async fn list_flow_variants(
281    State(state): State<ScenarioStudioState>,
282    Path(id): Path<String>,
283) -> Result<Json<Vec<FlowVariant>>, StatusCode> {
284    let variants = state.variants.read().await;
285    let flow_variants: Vec<FlowVariant> =
286        variants.values().filter(|v| v.flow_id == id).cloned().collect();
287    Ok(Json(flow_variants))
288}
289
290/// Create scenario studio router
291pub fn scenario_studio_router(state: ScenarioStudioState) -> axum::Router {
292    use axum::routing::{get, post};
293    use axum::Router;
294
295    Router::new()
296        .route("/api/v1/scenario-studio/flows", post(create_flow).get(list_flows))
297        .route(
298            "/api/v1/scenario-studio/flows/{id}",
299            get(get_flow).put(update_flow).delete(delete_flow),
300        )
301        .route("/api/v1/scenario-studio/flows/{id}/execute", post(execute_flow))
302        .route(
303            "/api/v1/scenario-studio/flows/{id}/variants",
304            post(create_flow_variant).get(list_flow_variants),
305        )
306        .with_state(state)
307}