1use 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#[derive(Clone)]
17pub struct ChainState {
18 registry: Arc<RequestChainRegistry>,
20 engine: Arc<ChainExecutionEngine>,
22}
23
24pub fn create_chain_state(
30 registry: Arc<RequestChainRegistry>,
31 engine: Arc<ChainExecutionEngine>,
32) -> ChainState {
33 ChainState { registry, engine }
34}
35
36#[derive(Debug, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct ChainExecutionRequest {
40 pub variables: Option<serde_json::Value>,
42}
43
44#[derive(Debug, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct ChainExecutionResponse {
48 pub chain_id: String,
50 pub status: String,
52 pub total_duration_ms: u64,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub request_results: Option<serde_json::Value>,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub error_message: Option<String>,
60}
61
62#[derive(Debug, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct ChainListResponse {
66 pub chains: Vec<ChainSummary>,
68 pub total: usize,
70}
71
72#[derive(Debug, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct ChainSummary {
76 pub id: String,
78 pub name: String,
80 pub description: Option<String>,
82 pub tags: Vec<String>,
84 pub enabled: bool,
86 pub link_count: usize,
88}
89
90#[derive(Debug, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct ChainCreateRequest {
94 pub definition: String,
96}
97
98#[derive(Debug, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct ChainCreateResponse {
102 pub id: String,
104 pub message: String,
106}
107
108#[derive(Debug, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct ChainValidationResponse {
112 pub valid: bool,
114 pub errors: Vec<String>,
116 pub warnings: Vec<String>,
118}
119
120#[derive(Debug, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub struct ChainExecutionHistoryResponse {
124 pub chain_id: String,
126 pub executions: Vec<ChainExecutionRecord>,
128 pub total: usize,
130}
131
132#[derive(Debug, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct ChainExecutionRecord {
136 pub executed_at: String,
138 pub status: String,
140 pub total_duration_ms: u64,
142 pub request_count: usize,
144 pub error_message: Option<String>,
146}
147
148pub 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
170pub 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
178pub 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
195pub async fn update_chain(
197 Path(chain_id): Path<String>,
198 State(state): State<ChainState>,
199 Json(request): Json<ChainCreateRequest>,
200) -> Response {
201 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 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
225pub 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
241pub 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
271pub 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![], })
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
297pub async fn get_chain_history(
299 Path(chain_id): Path<String>,
300 State(state): State<ChainState>,
301) -> Response {
302 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 }
355}