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