mockforge_http/handlers/
ab_testing.rs

1//! A/B Testing API handlers
2//!
3//! This module provides HTTP handlers for managing A/B tests and mock variants.
4
5use axum::{
6    extract::{Path, Query, State},
7    http::StatusCode,
8    response::Json,
9};
10use mockforge_core::ab_testing::analytics::ABTestReport;
11use mockforge_core::ab_testing::{
12    ABTestConfig, VariantAnalytics, VariantComparison, VariantManager,
13};
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use std::collections::HashMap;
17use std::sync::Arc;
18use tracing::{error, info};
19
20/// State for A/B testing handlers
21#[derive(Clone)]
22pub struct ABTestingState {
23    /// Variant manager
24    pub variant_manager: Arc<VariantManager>,
25}
26
27impl ABTestingState {
28    /// Create new A/B testing state
29    pub fn new() -> Self {
30        Self {
31            variant_manager: Arc::new(VariantManager::new()),
32        }
33    }
34}
35
36/// Request to create or update an A/B test
37#[derive(Debug, Deserialize)]
38pub struct CreateABTestRequest {
39    /// A/B test configuration
40    pub test: ABTestConfig,
41}
42
43/// Request to update variant allocation
44#[derive(Debug, Deserialize)]
45pub struct UpdateAllocationRequest {
46    /// New allocations
47    pub allocations: Vec<mockforge_core::ab_testing::VariantAllocation>,
48}
49
50/// Query parameters for endpoint operations
51#[derive(Debug, Deserialize)]
52pub struct EndpointQuery {
53    /// HTTP method
54    pub method: String,
55    /// Endpoint path
56    pub path: String,
57}
58
59/// Create or update an A/B test
60///
61/// POST /api/v1/ab-tests
62pub async fn create_ab_test(
63    State(state): State<ABTestingState>,
64    Json(req): Json<CreateABTestRequest>,
65) -> Result<Json<ABTestConfig>, StatusCode> {
66    info!("Creating A/B test: {}", req.test.test_name);
67
68    // Validate allocations
69    if let Err(e) = req.test.validate_allocations() {
70        error!("Invalid A/B test configuration: {}", e);
71        return Err(StatusCode::BAD_REQUEST);
72    }
73
74    match state.variant_manager.register_test(req.test.clone()).await {
75        Ok(_) => {
76            info!("A/B test created successfully: {}", req.test.test_name);
77            Ok(Json(req.test))
78        }
79        Err(e) => {
80            error!("Failed to create A/B test: {}", e);
81            Err(StatusCode::INTERNAL_SERVER_ERROR)
82        }
83    }
84}
85
86/// Get A/B test configuration for an endpoint
87///
88/// GET /api/v1/ab-tests?method={method}&path={path}
89pub async fn get_ab_test(
90    State(state): State<ABTestingState>,
91    Query(params): Query<EndpointQuery>,
92) -> Result<Json<ABTestConfig>, StatusCode> {
93    state
94        .variant_manager
95        .get_test(&params.method, &params.path)
96        .await
97        .map(Json)
98        .ok_or(StatusCode::NOT_FOUND)
99}
100
101/// List all A/B tests
102///
103/// GET /api/v1/ab-tests
104pub async fn list_ab_tests(
105    State(state): State<ABTestingState>,
106) -> Result<Json<Vec<ABTestConfig>>, StatusCode> {
107    let tests = state.variant_manager.list_tests().await;
108    Ok(Json(tests))
109}
110
111/// Delete an A/B test
112///
113/// DELETE /api/v1/ab-tests?method={method}&path={path}
114pub async fn delete_ab_test(
115    State(state): State<ABTestingState>,
116    Query(params): Query<EndpointQuery>,
117) -> Result<Json<Value>, StatusCode> {
118    match state.variant_manager.remove_test(&params.method, &params.path).await {
119        Ok(_) => {
120            info!("A/B test deleted: {} {}", params.method, params.path);
121            Ok(Json(serde_json::json!({
122                "success": true,
123                "message": format!("A/B test deleted for {} {}", params.method, params.path)
124            })))
125        }
126        Err(e) => {
127            error!("Failed to delete A/B test: {}", e);
128            Err(StatusCode::INTERNAL_SERVER_ERROR)
129        }
130    }
131}
132
133/// Get analytics for an A/B test
134///
135/// GET /api/v1/ab-tests/analytics?method={method}&path={path}
136pub async fn get_ab_test_analytics(
137    State(state): State<ABTestingState>,
138    Query(params): Query<EndpointQuery>,
139) -> Result<Json<ABTestReport>, StatusCode> {
140    let test_config = state
141        .variant_manager
142        .get_test(&params.method, &params.path)
143        .await
144        .ok_or(StatusCode::NOT_FOUND)?;
145
146    let variant_analytics =
147        state.variant_manager.get_endpoint_analytics(&params.method, &params.path).await;
148
149    let report = ABTestReport::new(test_config, variant_analytics);
150    Ok(Json(report))
151}
152
153/// Get analytics for a specific variant
154///
155/// GET /api/v1/ab-tests/variants/analytics?method={method}&path={path}&variant_id={variant_id}
156pub async fn get_variant_analytics(
157    State(state): State<ABTestingState>,
158    Query(params): Query<HashMap<String, String>>,
159) -> Result<Json<VariantAnalytics>, StatusCode> {
160    let method = params.get("method").ok_or(StatusCode::BAD_REQUEST)?;
161    let path = params.get("path").ok_or(StatusCode::BAD_REQUEST)?;
162    let variant_id = params.get("variant_id").ok_or(StatusCode::BAD_REQUEST)?;
163
164    state
165        .variant_manager
166        .get_variant_analytics(method, path, variant_id)
167        .await
168        .map(Json)
169        .ok_or(StatusCode::NOT_FOUND)
170}
171
172/// Compare two variants
173///
174/// GET /api/v1/ab-tests/variants/compare?method={method}&path={path}&variant_a={id}&variant_b={id}
175pub async fn compare_variants(
176    State(state): State<ABTestingState>,
177    Query(params): Query<HashMap<String, String>>,
178) -> Result<Json<VariantComparison>, StatusCode> {
179    let method = params.get("method").ok_or(StatusCode::BAD_REQUEST)?;
180    let path = params.get("path").ok_or(StatusCode::BAD_REQUEST)?;
181    let variant_a_id = params.get("variant_a").ok_or(StatusCode::BAD_REQUEST)?;
182    let variant_b_id = params.get("variant_b").ok_or(StatusCode::BAD_REQUEST)?;
183
184    let analytics_a = state
185        .variant_manager
186        .get_variant_analytics(method, path, variant_a_id)
187        .await
188        .ok_or(StatusCode::NOT_FOUND)?;
189
190    let analytics_b = state
191        .variant_manager
192        .get_variant_analytics(method, path, variant_b_id)
193        .await
194        .ok_or(StatusCode::NOT_FOUND)?;
195
196    let comparison = VariantComparison::new(&analytics_a, &analytics_b);
197    Ok(Json(comparison))
198}
199
200/// Update variant allocations for an A/B test
201///
202/// PUT /api/v1/ab-tests/allocations?method={method}&path={path}
203pub async fn update_allocations(
204    State(state): State<ABTestingState>,
205    Query(params): Query<EndpointQuery>,
206    Json(req): Json<UpdateAllocationRequest>,
207) -> Result<Json<ABTestConfig>, StatusCode> {
208    let mut test_config = state
209        .variant_manager
210        .get_test(&params.method, &params.path)
211        .await
212        .ok_or(StatusCode::NOT_FOUND)?;
213
214    // Validate new allocations
215    test_config.allocations = req.allocations;
216    if let Err(e) = test_config.validate_allocations() {
217        error!("Invalid allocations: {}", e);
218        return Err(StatusCode::BAD_REQUEST);
219    }
220
221    // Update the test
222    match state.variant_manager.register_test(test_config.clone()).await {
223        Ok(_) => {
224            info!("Updated allocations for {} {}", params.method, params.path);
225            Ok(Json(test_config))
226        }
227        Err(e) => {
228            error!("Failed to update allocations: {}", e);
229            Err(StatusCode::INTERNAL_SERVER_ERROR)
230        }
231    }
232}
233
234/// Enable or disable an A/B test
235///
236/// PATCH /api/v1/ab-tests/enable?method={method}&path={path}&enabled={true|false}
237pub async fn toggle_ab_test(
238    State(state): State<ABTestingState>,
239    Query(params): Query<HashMap<String, String>>,
240) -> Result<Json<ABTestConfig>, StatusCode> {
241    let method = params.get("method").ok_or(StatusCode::BAD_REQUEST)?;
242    let path = params.get("path").ok_or(StatusCode::BAD_REQUEST)?;
243    let enabled = params
244        .get("enabled")
245        .and_then(|v| v.parse::<bool>().ok())
246        .ok_or(StatusCode::BAD_REQUEST)?;
247
248    let mut test_config = state
249        .variant_manager
250        .get_test(method, path)
251        .await
252        .ok_or(StatusCode::NOT_FOUND)?;
253
254    test_config.enabled = enabled;
255
256    match state.variant_manager.register_test(test_config.clone()).await {
257        Ok(_) => {
258            info!(
259                "{} A/B test for {} {}",
260                if enabled { "Enabled" } else { "Disabled" },
261                method,
262                path
263            );
264            Ok(Json(test_config))
265        }
266        Err(e) => {
267            error!("Failed to toggle A/B test: {}", e);
268            Err(StatusCode::INTERNAL_SERVER_ERROR)
269        }
270    }
271}
272
273/// Create A/B testing router
274pub fn ab_testing_router(state: ABTestingState) -> axum::Router {
275    use axum::routing::{delete, get, patch, post, put};
276    use axum::Router;
277
278    Router::new()
279        .route("/api/v1/ab-tests", post(create_ab_test).get(list_ab_tests))
280        .route("/api/v1/ab-tests/analytics", get(get_ab_test_analytics))
281        .route("/api/v1/ab-tests/variants/analytics", get(get_variant_analytics))
282        .route("/api/v1/ab-tests/variants/compare", get(compare_variants))
283        .route("/api/v1/ab-tests/allocations", put(update_allocations))
284        .route("/api/v1/ab-tests/enable", patch(toggle_ab_test))
285        .route("/api/v1/ab-tests/delete", delete(delete_ab_test))
286        .with_state(state)
287}