use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
};
use mockforge_core::scenario_studio::{
FlowDefinition, FlowExecutionResult, FlowExecutor, FlowType, FlowVariant,
};
use serde::Deserialize;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{error, info};
#[derive(Clone)]
pub struct ScenarioStudioState {
flows: Arc<RwLock<HashMap<String, FlowDefinition>>>,
variants: Arc<RwLock<HashMap<String, FlowVariant>>>,
}
impl ScenarioStudioState {
pub fn new() -> Self {
Self {
flows: Arc::new(RwLock::new(HashMap::new())),
variants: Arc::new(RwLock::new(HashMap::new())),
}
}
}
impl Default for ScenarioStudioState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Deserialize)]
pub struct CreateFlowRequest {
pub name: String,
pub description: Option<String>,
pub flow_type: FlowType,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct UpdateFlowRequest {
pub name: Option<String>,
pub description: Option<String>,
pub flow_type: Option<FlowType>,
pub steps: Option<Vec<mockforge_core::scenario_studio::FlowStep>>,
pub connections: Option<Vec<mockforge_core::scenario_studio::FlowConnection>>,
pub variables: Option<HashMap<String, Value>>,
pub tags: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
pub struct ExecuteFlowRequest {
#[serde(default)]
pub variables: HashMap<String, Value>,
}
#[derive(Debug, Deserialize)]
pub struct CreateFlowVariantRequest {
pub name: String,
pub description: Option<String>,
pub flow_id: String,
}
#[derive(Debug, Deserialize)]
pub struct WorkspaceQuery {
#[serde(default = "default_workspace")]
pub workspace: String,
}
fn default_workspace() -> String {
"default".to_string()
}
pub async fn create_flow(
State(state): State<ScenarioStudioState>,
Json(request): Json<CreateFlowRequest>,
) -> Result<Json<FlowDefinition>, StatusCode> {
let mut flow = FlowDefinition::new(request.name, request.flow_type);
flow.description = request.description;
flow.tags = request.tags;
let flow_id = flow.id.clone();
let mut flows = state.flows.write().await;
flows.insert(flow_id.clone(), flow.clone());
info!("Created flow: {}", flow_id);
Ok(Json(flow))
}
pub async fn list_flows(
State(state): State<ScenarioStudioState>,
) -> Result<Json<Vec<FlowDefinition>>, StatusCode> {
let flows = state.flows.read().await;
let flows_list: Vec<FlowDefinition> = flows.values().cloned().collect();
Ok(Json(flows_list))
}
pub async fn get_flow(
State(state): State<ScenarioStudioState>,
Path(id): Path<String>,
) -> Result<Json<FlowDefinition>, StatusCode> {
let flows = state.flows.read().await;
let flow = flows.get(&id).cloned().ok_or_else(|| {
error!("Flow not found: {}", id);
StatusCode::NOT_FOUND
})?;
Ok(Json(flow))
}
pub async fn update_flow(
State(state): State<ScenarioStudioState>,
Path(id): Path<String>,
Json(request): Json<UpdateFlowRequest>,
) -> Result<Json<FlowDefinition>, StatusCode> {
let mut flows = state.flows.write().await;
let flow = flows.get_mut(&id).ok_or_else(|| {
error!("Flow not found: {}", id);
StatusCode::NOT_FOUND
})?;
if let Some(name) = request.name {
flow.name = name;
}
if let Some(description) = request.description {
flow.description = Some(description);
}
if let Some(flow_type) = request.flow_type {
flow.flow_type = flow_type;
}
if let Some(steps) = request.steps {
flow.steps = steps;
}
if let Some(connections) = request.connections {
flow.connections = connections;
}
if let Some(variables) = request.variables {
flow.variables = variables;
}
if let Some(tags) = request.tags {
flow.tags = tags;
}
flow.updated_at = chrono::Utc::now();
let flow_clone = flow.clone();
info!("Updated flow: {}", id);
Ok(Json(flow_clone))
}
pub async fn delete_flow(
State(state): State<ScenarioStudioState>,
Path(id): Path<String>,
) -> Result<Json<Value>, StatusCode> {
let mut flows = state.flows.write().await;
if flows.remove(&id).is_none() {
error!("Flow not found: {}", id);
return Err(StatusCode::NOT_FOUND);
}
let mut variants = state.variants.write().await;
variants.retain(|_, v| v.flow_id != id);
info!("Deleted flow: {}", id);
Ok(Json(serde_json::json!({
"success": true,
"message": format!("Flow {} deleted", id),
})))
}
#[axum::debug_handler]
pub async fn execute_flow(
Path(id): Path<String>,
State(state): State<ScenarioStudioState>,
Json(request): Json<ExecuteFlowRequest>,
) -> Result<Json<FlowExecutionResult>, StatusCode> {
let flows = state.flows.read().await;
let flow = flows.get(&id).cloned().ok_or_else(|| {
error!("Flow not found: {}", id);
StatusCode::NOT_FOUND
})?;
drop(flows);
let initial_variables = request.variables;
let mut executor = FlowExecutor::with_variables(initial_variables);
let result = executor.execute(&flow).await.map_err(|e| {
error!("Failed to execute flow {}: {}", id, e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
info!("Executed flow: {}", id);
Ok(Json(result))
}
pub async fn create_flow_variant(
State(state): State<ScenarioStudioState>,
Path(id): Path<String>,
Json(request): Json<CreateFlowVariantRequest>,
) -> Result<Json<FlowVariant>, StatusCode> {
let flows = state.flows.read().await;
if !flows.contains_key(&id) {
error!("Base flow not found: {}", id);
return Err(StatusCode::NOT_FOUND);
}
drop(flows);
let mut variant = FlowVariant::new(request.name, id.clone());
variant.description = request.description;
let variant_id = variant.id.clone();
let mut variants = state.variants.write().await;
variants.insert(variant_id.clone(), variant.clone());
info!("Created flow variant: {} for flow: {}", variant_id, id);
Ok(Json(variant))
}
pub async fn list_flow_variants(
State(state): State<ScenarioStudioState>,
Path(id): Path<String>,
) -> Result<Json<Vec<FlowVariant>>, StatusCode> {
let variants = state.variants.read().await;
let flow_variants: Vec<FlowVariant> =
variants.values().filter(|v| v.flow_id == id).cloned().collect();
Ok(Json(flow_variants))
}
pub fn scenario_studio_router(state: ScenarioStudioState) -> axum::Router {
use axum::routing::{get, post};
use axum::Router;
Router::new()
.route("/api/v1/scenario-studio/flows", post(create_flow).get(list_flows))
.route(
"/api/v1/scenario-studio/flows/{id}",
get(get_flow).put(update_flow).delete(delete_flow),
)
.route("/api/v1/scenario-studio/flows/{id}/execute", post(execute_flow))
.route(
"/api/v1/scenario-studio/flows/{id}/variants",
post(create_flow_variant).get(list_flow_variants),
)
.with_state(state)
}