1use 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#[derive(Clone)]
22pub struct ABTestingState {
23 pub variant_manager: Arc<VariantManager>,
25}
26
27impl Default for ABTestingState {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl ABTestingState {
34 pub fn new() -> Self {
36 Self {
37 variant_manager: Arc::new(VariantManager::new()),
38 }
39 }
40}
41
42#[derive(Debug, Deserialize)]
44pub struct CreateABTestRequest {
45 pub test: ABTestConfig,
47}
48
49#[derive(Debug, Deserialize)]
51pub struct UpdateAllocationRequest {
52 pub allocations: Vec<mockforge_core::ab_testing::VariantAllocation>,
54}
55
56#[derive(Debug, Deserialize)]
58pub struct EndpointQuery {
59 pub method: String,
61 pub path: String,
63}
64
65pub 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 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
92pub 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(¶ms.method, ¶ms.path)
102 .await
103 .map(Json)
104 .ok_or(StatusCode::NOT_FOUND)
105}
106
107pub 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
117pub 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(¶ms.method, ¶ms.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
139pub 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(¶ms.method, ¶ms.path)
149 .await
150 .ok_or(StatusCode::NOT_FOUND)?;
151
152 let variant_analytics =
153 state.variant_manager.get_endpoint_analytics(¶ms.method, ¶ms.path).await;
154
155 let report = ABTestReport::new(test_config, variant_analytics);
156 Ok(Json(report))
157}
158
159pub 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
178pub 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
206pub 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(¶ms.method, ¶ms.path)
217 .await
218 .ok_or(StatusCode::NOT_FOUND)?;
219
220 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 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
240pub 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
279pub 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}