mockforge_intelligence/handlers/drift_budget/
budgets.rs1use super::DriftBudgetSource;
4use super::DriftBudgetStateProvider;
5use axum::{
6 extract::{Path, Query, State},
7 http::StatusCode,
8 response::Json,
9};
10use mockforge_contracts::contract_drift::types::DriftBudget;
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Deserialize, Serialize)]
15pub struct CreateDriftBudgetRequest {
16 pub endpoint: String,
18 pub method: String,
20 pub max_breaking_changes: Option<u32>,
22 pub max_non_breaking_changes: Option<u32>,
24 pub severity_threshold: Option<String>,
26 pub enabled: Option<bool>,
28 pub workspace_id: Option<String>,
30}
31
32#[derive(Debug, Serialize)]
34pub struct DriftBudgetResponse {
35 pub id: String,
37 pub endpoint: String,
39 pub method: String,
41 pub budget: DriftBudget,
43 pub workspace_id: Option<String>,
45}
46
47#[derive(Debug, Deserialize)]
51pub struct GetBudgetQuery {
52 pub endpoint: String,
54 pub method: String,
56 pub workspace_id: Option<String>,
58 pub service_name: Option<String>,
60 pub tags: Option<String>,
62}
63
64pub async fn create_budget<S: DriftBudgetStateProvider>(
68 State(_state): State<S>,
69 Json(request): Json<CreateDriftBudgetRequest>,
70) -> Result<Json<DriftBudgetResponse>, StatusCode> {
71 let budget = DriftBudget {
72 max_breaking_changes: request.max_breaking_changes.unwrap_or(0),
73 max_non_breaking_changes: request.max_non_breaking_changes.unwrap_or(10),
74 max_field_churn_percent: None,
75 time_window_days: None,
76 severity_threshold: request
77 .severity_threshold
78 .as_deref()
79 .and_then(|s| match s.to_lowercase().as_str() {
80 "critical" => Some(crate::ai_contract_diff::MismatchSeverity::Critical),
81 "high" => Some(crate::ai_contract_diff::MismatchSeverity::High),
82 "medium" => Some(crate::ai_contract_diff::MismatchSeverity::Medium),
83 "low" => Some(crate::ai_contract_diff::MismatchSeverity::Low),
84 _ => None,
85 })
86 .unwrap_or(crate::ai_contract_diff::MismatchSeverity::High),
87 enabled: request.enabled.unwrap_or(true),
88 };
89
90 let budget_id = format!("{}:{}:{}", request.method, request.endpoint, uuid::Uuid::new_v4());
92
93 let key = format!("{} {}", request.method, request.endpoint);
95
96 let _ = key;
100
101 Ok(Json(DriftBudgetResponse {
102 id: budget_id,
103 endpoint: request.endpoint,
104 method: request.method,
105 budget,
106 workspace_id: request.workspace_id,
107 }))
108}
109
110pub async fn list_budgets<S: DriftBudgetStateProvider>(
114 State(state): State<S>,
115) -> Result<Json<serde_json::Value>, StatusCode> {
116 let config = state.engine().config();
117 let budgets: Vec<serde_json::Value> = config
118 .per_endpoint_budgets
119 .iter()
120 .map(|(key, budget)| {
121 let parts: Vec<&str> = key.splitn(2, ' ').collect();
123 let (method, endpoint) = if parts.len() == 2 {
124 (parts[0].to_string(), parts[1].to_string())
125 } else {
126 ("GET".to_string(), key.clone())
127 };
128 serde_json::json!({
129 "id": key,
130 "method": method,
131 "endpoint": endpoint,
132 "budget": {
133 "max_breaking_changes": budget.max_breaking_changes,
134 "max_non_breaking_changes": budget.max_non_breaking_changes,
135 "enabled": budget.enabled,
136 }
137 })
138 })
139 .collect();
140
141 Ok(Json(serde_json::json!({
142 "budgets": budgets,
143 "total": budgets.len(),
144 })))
145}
146
147pub async fn get_budget<S: DriftBudgetStateProvider>(
151 State(state): State<S>,
152 Path(id): Path<String>,
153) -> Result<Json<serde_json::Value>, StatusCode> {
154 let config = state.engine().config();
155 if let Some(budget) = config.per_endpoint_budgets.get(&id) {
156 let parts: Vec<&str> = id.splitn(2, ' ').collect();
157 let (method, endpoint) = if parts.len() == 2 {
158 (parts[0].to_string(), parts[1].to_string())
159 } else {
160 ("GET".to_string(), id.clone())
161 };
162 Ok(Json(serde_json::json!({
163 "id": id,
164 "method": method,
165 "endpoint": endpoint,
166 "budget": {
167 "max_breaking_changes": budget.max_breaking_changes,
168 "max_non_breaking_changes": budget.max_non_breaking_changes,
169 "enabled": budget.enabled,
170 }
171 })))
172 } else {
173 Err(StatusCode::NOT_FOUND)
174 }
175}
176
177pub async fn get_budget_for_endpoint<S: DriftBudgetStateProvider>(
181 State(state): State<S>,
182 Query(params): Query<GetBudgetQuery>,
183) -> Result<Json<serde_json::Value>, StatusCode> {
184 let tags = params
185 .tags
186 .as_ref()
187 .map(|t| t.split(',').map(|s| s.trim().to_string()).collect::<Vec<_>>());
188
189 let budget = state.engine().get_budget_for_endpoint(
190 ¶ms.endpoint,
191 ¶ms.method,
192 params.workspace_id.as_deref(),
193 params.service_name.as_deref(),
194 tags.as_deref(),
195 );
196
197 Ok(Json(serde_json::json!({
198 "endpoint": params.endpoint,
199 "method": params.method,
200 "workspace_id": params.workspace_id,
201 "service_name": params.service_name,
202 "budget": budget,
203 })))
204}