Skip to main content

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
225pub async fn execute_flow(
226    Path(id): Path<String>,
227    State(state): State<ScenarioStudioState>,
228    Json(request): Json<ExecuteFlowRequest>,
229) -> Result<Json<FlowExecutionResult>, StatusCode> {
230    let flows = state.flows.read().await;
231    let flow = flows.get(&id).cloned().ok_or_else(|| {
232        error!("Flow not found: {}", id);
233        StatusCode::NOT_FOUND
234    })?;
235
236    drop(flows); // Release lock before async operation
237
238    let initial_variables = request.variables;
239    let mut executor = FlowExecutor::with_variables(initial_variables);
240    let result = executor.execute(&flow).await.map_err(|e| {
241        error!("Failed to execute flow {}: {}", id, e);
242        StatusCode::INTERNAL_SERVER_ERROR
243    })?;
244
245    info!("Executed flow: {}", id);
246    Ok(Json(result))
247}
248
249/// Create a flow variant
250///
251/// POST /api/v1/scenario-studio/flows/:id/variants
252pub async fn create_flow_variant(
253    State(state): State<ScenarioStudioState>,
254    Path(id): Path<String>,
255    Json(request): Json<CreateFlowVariantRequest>,
256) -> Result<Json<FlowVariant>, StatusCode> {
257    // Verify that the base flow exists
258    let flows = state.flows.read().await;
259    if !flows.contains_key(&id) {
260        error!("Base flow not found: {}", id);
261        return Err(StatusCode::NOT_FOUND);
262    }
263    drop(flows);
264
265    let mut variant = FlowVariant::new(request.name, id.clone());
266    variant.description = request.description;
267
268    let variant_id = variant.id.clone();
269    let mut variants = state.variants.write().await;
270    variants.insert(variant_id.clone(), variant.clone());
271
272    info!("Created flow variant: {} for flow: {}", variant_id, id);
273    Ok(Json(variant))
274}
275
276/// List all variants for a flow
277///
278/// GET /api/v1/scenario-studio/flows/:id/variants
279pub async fn list_flow_variants(
280    State(state): State<ScenarioStudioState>,
281    Path(id): Path<String>,
282) -> Result<Json<Vec<FlowVariant>>, StatusCode> {
283    let variants = state.variants.read().await;
284    let flow_variants: Vec<FlowVariant> =
285        variants.values().filter(|v| v.flow_id == id).cloned().collect();
286    Ok(Json(flow_variants))
287}
288
289/// Create scenario studio router
290pub fn scenario_studio_router(state: ScenarioStudioState) -> axum::Router {
291    use axum::routing::{get, post};
292    use axum::Router;
293
294    Router::new()
295        .route("/api/v1/scenario-studio/flows", post(create_flow).get(list_flows))
296        .route(
297            "/api/v1/scenario-studio/flows/{id}",
298            get(get_flow).put(update_flow).delete(delete_flow),
299        )
300        .route("/api/v1/scenario-studio/flows/{id}/execute", post(execute_flow))
301        .route(
302            "/api/v1/scenario-studio/flows/{id}/variants",
303            post(create_flow_variant).get(list_flow_variants),
304        )
305        .with_state(state)
306}