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