1use 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#[derive(Clone)]
22pub struct ABTestingState {
23 pub variant_manager: Arc<VariantManager>,
25}
26
27impl ABTestingState {
28 pub fn new() -> Self {
30 Self {
31 variant_manager: Arc::new(VariantManager::new()),
32 }
33 }
34}
35
36#[derive(Debug, Deserialize)]
38pub struct CreateABTestRequest {
39 pub test: ABTestConfig,
41}
42
43#[derive(Debug, Deserialize)]
45pub struct UpdateAllocationRequest {
46 pub allocations: Vec<mockforge_core::ab_testing::VariantAllocation>,
48}
49
50#[derive(Debug, Deserialize)]
52pub struct EndpointQuery {
53 pub method: String,
55 pub path: String,
57}
58
59pub 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 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
86pub 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(¶ms.method, ¶ms.path)
96 .await
97 .map(Json)
98 .ok_or(StatusCode::NOT_FOUND)
99}
100
101pub 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
111pub 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(¶ms.method, ¶ms.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
133pub 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(¶ms.method, ¶ms.path)
143 .await
144 .ok_or(StatusCode::NOT_FOUND)?;
145
146 let variant_analytics =
147 state.variant_manager.get_endpoint_analytics(¶ms.method, ¶ms.path).await;
148
149 let report = ABTestReport::new(test_config, variant_analytics);
150 Ok(Json(report))
151}
152
153pub 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
172pub 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
200pub 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(¶ms.method, ¶ms.path)
211 .await
212 .ok_or(StatusCode::NOT_FOUND)?;
213
214 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 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
234pub 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
273pub 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}