mockforge_http/
chain_handlers.rs

1//! Chain management HTTP handlers for MockForge
2//!
3//! This module provides REST endpoints for managing and executing request chains
4//! through the HTTP API.
5
6use axum::extract::{Path, State};
7use axum::response::{IntoResponse, Response};
8use axum::{http::StatusCode, Json};
9use serde::{Deserialize, Serialize};
10use std::sync::Arc;
11
12use mockforge_core::chain_execution::ChainExecutionEngine;
13use mockforge_core::request_chaining::RequestChainRegistry;
14
15/// Shared state for chain management
16#[derive(Clone)]
17pub struct ChainState {
18    /// Request chain registry for storing and retrieving chains
19    registry: Arc<RequestChainRegistry>,
20    /// Chain execution engine for running request chains
21    engine: Arc<ChainExecutionEngine>,
22}
23
24/// Create the chain state with registry and engine
25///
26/// # Arguments
27/// * `registry` - Request chain registry for chain storage
28/// * `engine` - Chain execution engine for running chains
29pub fn create_chain_state(
30    registry: Arc<RequestChainRegistry>,
31    engine: Arc<ChainExecutionEngine>,
32) -> ChainState {
33    ChainState { registry, engine }
34}
35
36/// Request body for executing a request chain
37#[derive(Debug, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct ChainExecutionRequest {
40    /// Optional variables to pass to the chain execution
41    pub variables: Option<serde_json::Value>,
42}
43
44/// Response from chain execution
45#[derive(Debug, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct ChainExecutionResponse {
48    /// ID of the executed chain
49    pub chain_id: String,
50    /// Execution status ("successful", "partial_success", "failed")
51    pub status: String,
52    /// Total execution duration in milliseconds
53    pub total_duration_ms: u64,
54    /// Results of individual requests in the chain
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub request_results: Option<serde_json::Value>,
57    /// Error message if execution failed
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub error_message: Option<String>,
60}
61
62/// Response listing all available chains
63#[derive(Debug, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct ChainListResponse {
66    /// List of chain summaries
67    pub chains: Vec<ChainSummary>,
68    /// Total number of chains
69    pub total: usize,
70}
71
72/// Summary information for a request chain
73#[derive(Debug, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct ChainSummary {
76    /// Unique chain identifier
77    pub id: String,
78    /// Human-readable chain name
79    pub name: String,
80    /// Optional chain description
81    pub description: Option<String>,
82    /// Tags associated with this chain
83    pub tags: Vec<String>,
84    /// Whether this chain is enabled
85    pub enabled: bool,
86    /// Number of links (requests) in this chain
87    pub link_count: usize,
88}
89
90/// Request body for creating a new chain
91#[derive(Debug, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct ChainCreateRequest {
94    /// YAML definition of the chain
95    pub definition: String,
96}
97
98/// Response from chain creation
99#[derive(Debug, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct ChainCreateResponse {
102    /// ID of the created chain
103    pub id: String,
104    /// Success message
105    pub message: String,
106}
107
108/// Response from chain validation
109#[derive(Debug, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct ChainValidationResponse {
112    /// Whether the chain is valid
113    pub valid: bool,
114    /// Validation error messages if any
115    pub errors: Vec<String>,
116    /// Validation warnings if any
117    pub warnings: Vec<String>,
118}
119
120/// Response containing chain execution history
121#[derive(Debug, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub struct ChainExecutionHistoryResponse {
124    /// ID of the chain
125    pub chain_id: String,
126    /// List of execution records
127    pub executions: Vec<ChainExecutionRecord>,
128    /// Total number of executions
129    pub total: usize,
130}
131
132/// Record of a single chain execution
133#[derive(Debug, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct ChainExecutionRecord {
136    /// ISO 8601 timestamp when execution started
137    pub executed_at: String,
138    /// Execution status ("successful", "partial_success", "failed")
139    pub status: String,
140    /// Total execution duration in milliseconds
141    pub total_duration_ms: u64,
142    /// Number of requests in the chain
143    pub request_count: usize,
144    /// Error message if execution failed
145    pub error_message: Option<String>,
146}
147
148/// GET /chains - List all available request chains
149pub async fn list_chains(State(state): State<ChainState>) -> impl IntoResponse {
150    let chain_ids = state.registry.list_chains().await;
151    let mut chains = Vec::new();
152
153    for id in chain_ids {
154        if let Some(chain) = state.registry.get_chain(&id).await {
155            chains.push(ChainSummary {
156                id: chain.id.clone(),
157                name: chain.name.clone(),
158                description: chain.description.clone(),
159                tags: chain.tags.clone(),
160                enabled: chain.config.enabled,
161                link_count: chain.links.len(),
162            });
163        }
164    }
165
166    let total = chains.len();
167    Json(ChainListResponse { chains, total })
168}
169
170/// GET /chains/:id - Get details for a specific chain
171pub async fn get_chain(Path(chain_id): Path<String>, State(state): State<ChainState>) -> Response {
172    match state.registry.get_chain(&chain_id).await {
173        Some(chain) => Json(chain).into_response(),
174        None => (StatusCode::NOT_FOUND, format!("Chain '{}' not found", chain_id)).into_response(),
175    }
176}
177
178/// POST /chains - Create a new request chain from YAML definition
179pub async fn create_chain(
180    State(state): State<ChainState>,
181    Json(request): Json<ChainCreateRequest>,
182) -> Response {
183    match state.registry.register_from_yaml(&request.definition).await {
184        Ok(id) => Json(ChainCreateResponse {
185            id: id.clone(),
186            message: format!("Chain '{}' created successfully", id),
187        })
188        .into_response(),
189        Err(e) => {
190            (StatusCode::BAD_REQUEST, format!("Failed to create chain: {}", e)).into_response()
191        }
192    }
193}
194
195/// PUT /chains/:id - Update an existing chain with new definition
196pub async fn update_chain(
197    Path(chain_id): Path<String>,
198    State(state): State<ChainState>,
199    Json(request): Json<ChainCreateRequest>,
200) -> Response {
201    // Remove the old chain first
202    if state.registry.remove_chain(&chain_id).await.is_err() {
203        return (StatusCode::NOT_FOUND, format!("Chain '{}' not found", chain_id)).into_response();
204    }
205
206    // Create the new chain
207    match state.registry.register_from_yaml(&request.definition).await {
208        Ok(new_id) => {
209            if new_id != chain_id {
210                return (StatusCode::BAD_REQUEST, "Chain ID mismatch in update".to_string())
211                    .into_response();
212            }
213            Json(serde_json::json!({
214                "id": new_id,
215                "message": "Chain updated successfully"
216            }))
217            .into_response()
218        }
219        Err(e) => {
220            (StatusCode::BAD_REQUEST, format!("Failed to update chain: {}", e)).into_response()
221        }
222    }
223}
224
225/// DELETE /chains/:id - Delete a request chain
226pub async fn delete_chain(
227    Path(chain_id): Path<String>,
228    State(state): State<ChainState>,
229) -> Response {
230    match state.registry.remove_chain(&chain_id).await {
231        Ok(_) => Json(serde_json::json!({
232            "id": chain_id,
233            "message": "Chain deleted successfully"
234        }))
235        .into_response(),
236        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to delete chain: {}", e))
237            .into_response(),
238    }
239}
240
241/// POST /chains/:id/execute - Execute a request chain with optional variables
242pub async fn execute_chain(
243    Path(chain_id): Path<String>,
244    State(state): State<ChainState>,
245    Json(request): Json<ChainExecutionRequest>,
246) -> Response {
247    match state.engine.execute_chain(&chain_id, request.variables).await {
248        Ok(result) => Json(ChainExecutionResponse {
249            chain_id: result.chain_id,
250            status: match result.status {
251                mockforge_core::chain_execution::ChainExecutionStatus::Successful => {
252                    "successful".to_string()
253                }
254                mockforge_core::chain_execution::ChainExecutionStatus::PartialSuccess => {
255                    "partial_success".to_string()
256                }
257                mockforge_core::chain_execution::ChainExecutionStatus::Failed => {
258                    "failed".to_string()
259                }
260            },
261            total_duration_ms: result.total_duration_ms,
262            request_results: Some(serde_json::to_value(result.request_results).unwrap_or_default()),
263            error_message: result.error_message,
264        })
265        .into_response(),
266        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to execute chain: {}", e))
267            .into_response(),
268    }
269}
270
271/// POST /chains/:id/validate - Validate chain definition for correctness
272pub async fn validate_chain(
273    Path(chain_id): Path<String>,
274    State(state): State<ChainState>,
275) -> Response {
276    match state.registry.get_chain(&chain_id).await {
277        Some(chain) => {
278            match state.registry.validate_chain(&chain).await {
279                Ok(()) => Json(ChainValidationResponse {
280                    valid: true,
281                    errors: vec![],
282                    warnings: vec![], // Could add warnings for potential issues
283                })
284                .into_response(),
285                Err(e) => Json(ChainValidationResponse {
286                    valid: false,
287                    errors: vec![e.to_string()],
288                    warnings: vec![],
289                })
290                .into_response(),
291            }
292        }
293        None => (StatusCode::NOT_FOUND, format!("Chain '{}' not found", chain_id)).into_response(),
294    }
295}
296
297/// GET /chains/:id/history - Get execution history for a chain
298pub async fn get_chain_history(
299    Path(chain_id): Path<String>,
300    State(state): State<ChainState>,
301) -> Response {
302    // Check if chain exists
303    if state.registry.get_chain(&chain_id).await.is_none() {
304        return (StatusCode::NOT_FOUND, format!("Chain '{}' not found", chain_id)).into_response();
305    }
306
307    let history = state.engine.get_chain_history(&chain_id).await;
308
309    let executions: Vec<ChainExecutionRecord> = history
310        .into_iter()
311        .map(|record| ChainExecutionRecord {
312            executed_at: record.executed_at,
313            status: match record.result.status {
314                mockforge_core::chain_execution::ChainExecutionStatus::Successful => {
315                    "successful".to_string()
316                }
317                mockforge_core::chain_execution::ChainExecutionStatus::PartialSuccess => {
318                    "partial_success".to_string()
319                }
320                mockforge_core::chain_execution::ChainExecutionStatus::Failed => {
321                    "failed".to_string()
322                }
323            },
324            total_duration_ms: record.result.total_duration_ms,
325            request_count: record.result.request_results.len(),
326            error_message: record.result.error_message,
327        })
328        .collect();
329
330    let total = executions.len();
331
332    Json(ChainExecutionHistoryResponse {
333        chain_id,
334        executions,
335        total,
336    })
337    .into_response()
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use mockforge_core::chain_execution::ChainExecutionEngine;
344    use mockforge_core::request_chaining::{ChainConfig, RequestChainRegistry};
345    use std::sync::Arc;
346
347    #[tokio::test]
348    async fn test_chain_state_creation() {
349        let registry = Arc::new(RequestChainRegistry::new(ChainConfig::default()));
350        let engine = Arc::new(ChainExecutionEngine::new(registry.clone(), ChainConfig::default()));
351        let _state = create_chain_state(registry, engine);
352
353        // Just verify creation works
354    }
355}