mockforge_contracts/contract_drift/
field_tracking.rs1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct FieldCountRecord {
13 pub workspace_id: Option<String>,
15 pub endpoint: String,
17 pub method: String,
19 pub field_count: u32,
21 pub recorded_at: DateTime<Utc>,
23}
24
25#[derive(Debug, Clone)]
27pub struct FieldCountTracker {
28 records: HashMap<String, Vec<FieldCountRecord>>,
30}
31
32impl FieldCountTracker {
33 pub fn new() -> Self {
35 Self {
36 records: HashMap::new(),
37 }
38 }
39
40 pub fn record_count(
42 &mut self,
43 workspace_id: Option<&str>,
44 endpoint: &str,
45 method: &str,
46 field_count: u32,
47 ) {
48 let key = Self::make_key(workspace_id, endpoint, method);
49 let record = FieldCountRecord {
50 workspace_id: workspace_id.map(|s| s.to_string()),
51 endpoint: endpoint.to_string(),
52 method: method.to_string(),
53 field_count,
54 recorded_at: Utc::now(),
55 };
56
57 self.records.entry(key).or_default().push(record);
58 }
59
60 pub fn get_baseline_count(
65 &self,
66 workspace_id: Option<&str>,
67 endpoint: &str,
68 method: &str,
69 before: Option<DateTime<Utc>>,
70 ) -> Option<u32> {
71 let key = Self::make_key(workspace_id, endpoint, method);
72 let records = self.records.get(&key)?;
73
74 let filtered: Vec<&FieldCountRecord> = if let Some(before_time) = before {
76 records.iter().filter(|r| r.recorded_at <= before_time).collect()
77 } else {
78 records.iter().collect()
79 };
80
81 filtered.iter().max_by_key(|r| r.recorded_at).map(|r| r.field_count)
83 }
84
85 pub fn get_average_count(
89 &self,
90 workspace_id: Option<&str>,
91 endpoint: &str,
92 method: &str,
93 window_days: u32,
94 ) -> Option<f64> {
95 let key = Self::make_key(workspace_id, endpoint, method);
96 let records = self.records.get(&key)?;
97
98 let cutoff = Utc::now() - chrono::Duration::days(window_days as i64);
99 let window_records: Vec<&FieldCountRecord> =
100 records.iter().filter(|r| r.recorded_at >= cutoff).collect();
101
102 if window_records.is_empty() {
103 return None;
104 }
105
106 let sum: u32 = window_records.iter().map(|r| r.field_count).sum();
107 Some(sum as f64 / window_records.len() as f64)
108 }
109
110 pub fn calculate_churn_percent(
115 &self,
116 workspace_id: Option<&str>,
117 endpoint: &str,
118 method: &str,
119 current_count: u32,
120 window_days: Option<u32>,
121 ) -> Option<f64> {
122 let baseline = if let Some(days) = window_days {
123 self.get_average_count(workspace_id, endpoint, method, days)?
125 } else {
126 self.get_baseline_count(workspace_id, endpoint, method, None)? as f64
128 };
129
130 if baseline == 0.0 {
131 return None;
132 }
133
134 let change = current_count as f64 - baseline;
135 Some((change / baseline) * 100.0)
136 }
137
138 pub fn cleanup_old_records(&mut self, retention_days: u32) {
140 let cutoff = Utc::now() - chrono::Duration::days(retention_days as i64);
141
142 for records in self.records.values_mut() {
143 records.retain(|r| r.recorded_at >= cutoff);
144 }
145
146 self.records.retain(|_, records| !records.is_empty());
148 }
149
150 fn make_key(workspace_id: Option<&str>, endpoint: &str, method: &str) -> String {
152 if let Some(ws_id) = workspace_id {
153 format!("{}:{} {}", ws_id, method, endpoint)
154 } else {
155 format!("{} {}", method, endpoint)
156 }
157 }
158}
159
160impl Default for FieldCountTracker {
161 fn default() -> Self {
162 Self::new()
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_record_and_retrieve_count() {
172 let mut tracker = FieldCountTracker::new();
173 tracker.record_count(None, "/api/users", "GET", 10);
174
175 let count = tracker.get_baseline_count(None, "/api/users", "GET", None);
176 assert_eq!(count, Some(10));
177 }
178
179 #[test]
180 fn test_calculate_churn_percent() {
181 let mut tracker = FieldCountTracker::new();
182 tracker.record_count(None, "/api/users", "GET", 10);
183
184 let churn = tracker.calculate_churn_percent(None, "/api/users", "GET", 12, None);
186 assert!(churn.is_some());
187 let churn_value = churn.unwrap();
188 assert!((churn_value - 20.0).abs() < 0.1); }
190
191 #[test]
192 fn test_average_count_over_window() {
193 let mut tracker = FieldCountTracker::new();
194
195 tracker.record_count(None, "/api/users", "GET", 10);
197 tracker.record_count(None, "/api/users", "GET", 12);
198 tracker.record_count(None, "/api/users", "GET", 14);
199
200 let avg = tracker.get_average_count(None, "/api/users", "GET", 30);
201 assert!(avg.is_some());
202 let avg_value = avg.unwrap();
203 assert!((avg_value - 12.0).abs() < 0.1); }
205}