1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Deserialize, Serialize)]
9pub struct CorrelationsTotals {
10 pub date: String,
11 pub release: u64,
12 pub beta: u64,
13 pub nightly: u64,
14 pub esr: u64,
15}
16
17impl CorrelationsTotals {
18 pub fn total_for_channel(&self, channel: &str) -> Option<u64> {
19 match channel {
20 "release" => Some(self.release),
21 "beta" => Some(self.beta),
22 "nightly" => Some(self.nightly),
23 "esr" => Some(self.esr),
24 _ => None,
25 }
26 }
27}
28
29#[derive(Debug, Deserialize, Serialize)]
30pub struct CorrelationsResponse {
31 pub total: f64,
32 pub results: Vec<CorrelationResult>,
33}
34
35#[derive(Debug, Deserialize, Serialize)]
36pub struct CorrelationResult {
37 pub item: HashMap<String, serde_json::Value>,
38 pub count_reference: f64,
39 pub count_group: f64,
40 pub prior: Option<CorrelationPrior>,
41}
42
43#[derive(Debug, Deserialize, Serialize)]
44pub struct CorrelationPrior {
45 pub item: HashMap<String, serde_json::Value>,
46 pub count_reference: f64,
47 pub count_group: f64,
48 pub total_reference: f64,
49 pub total_group: f64,
50}
51
52#[derive(Debug)]
53pub struct CorrelationsSummary {
54 pub signature: String,
55 pub channel: String,
56 pub date: String,
57 pub sig_count: f64,
58 pub ref_count: u64,
59 pub items: Vec<CorrelationItem>,
60}
61
62#[derive(Debug)]
63pub struct CorrelationItem {
64 pub label: String,
65 pub sig_pct: f64,
66 pub ref_pct: f64,
67 pub prior: Option<CorrelationItemPrior>,
68}
69
70#[derive(Debug)]
71pub struct CorrelationItemPrior {
72 pub label: String,
73 pub sig_pct: f64,
74 pub ref_pct: f64,
75}
76
77pub fn format_item_map(item: &HashMap<String, serde_json::Value>) -> String {
78 let mut keys: Vec<&String> = item.keys().collect();
79 keys.sort();
80 let parts: Vec<String> = keys
81 .iter()
82 .map(|k| {
83 let v = &item[*k];
84 let val_str = match v {
85 serde_json::Value::Null => "null".to_string(),
86 serde_json::Value::Bool(b) => b.to_string(),
87 serde_json::Value::String(s) => s.clone(),
88 serde_json::Value::Number(n) => n.to_string(),
89 other => other.to_string(),
90 };
91 format!("{} = {}", k, val_str)
92 })
93 .collect();
94 parts.join(" \u{2227} ")
95}
96
97impl CorrelationsResponse {
98 pub fn to_summary(
99 &self,
100 signature: &str,
101 channel: &str,
102 totals: &CorrelationsTotals,
103 ) -> CorrelationsSummary {
104 let ref_count = totals.total_for_channel(channel).unwrap_or(0);
105 let items = self
106 .results
107 .iter()
108 .map(|r| {
109 let sig_pct = if self.total > 0.0 {
110 r.count_group / self.total * 100.0
111 } else {
112 0.0
113 };
114 let ref_pct = if ref_count > 0 {
115 r.count_reference / ref_count as f64 * 100.0
116 } else {
117 0.0
118 };
119 let prior = r.prior.as_ref().map(|p| {
120 let prior_sig_pct = if p.total_group > 0.0 {
121 p.count_group / p.total_group * 100.0
122 } else {
123 0.0
124 };
125 let prior_ref_pct = if p.total_reference > 0.0 {
126 p.count_reference / p.total_reference * 100.0
127 } else {
128 0.0
129 };
130 CorrelationItemPrior {
131 label: format_item_map(&p.item),
132 sig_pct: prior_sig_pct,
133 ref_pct: prior_ref_pct,
134 }
135 });
136 CorrelationItem {
137 label: format_item_map(&r.item),
138 sig_pct,
139 ref_pct,
140 prior,
141 }
142 })
143 .collect();
144
145 CorrelationsSummary {
146 signature: signature.to_string(),
147 channel: channel.to_string(),
148 date: totals.date.clone(),
149 sig_count: self.total,
150 ref_count,
151 items,
152 }
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use serde_json::json;
160
161 #[test]
162 fn test_deserialize_totals() {
163 let data = r#"{"date":"2026-02-13","release":79268,"beta":4996,"nightly":4876,"esr":792}"#;
164 let totals: CorrelationsTotals = serde_json::from_str(data).unwrap();
165 assert_eq!(totals.date, "2026-02-13");
166 assert_eq!(totals.release, 79268);
167 assert_eq!(totals.beta, 4996);
168 assert_eq!(totals.nightly, 4876);
169 assert_eq!(totals.esr, 792);
170 }
171
172 #[test]
173 fn test_total_for_channel_valid() {
174 let totals = CorrelationsTotals {
175 date: "2026-02-13".to_string(),
176 release: 79268,
177 beta: 4996,
178 nightly: 4876,
179 esr: 792,
180 };
181 assert_eq!(totals.total_for_channel("release"), Some(79268));
182 assert_eq!(totals.total_for_channel("beta"), Some(4996));
183 assert_eq!(totals.total_for_channel("nightly"), Some(4876));
184 assert_eq!(totals.total_for_channel("esr"), Some(792));
185 }
186
187 #[test]
188 fn test_total_for_channel_invalid() {
189 let totals = CorrelationsTotals {
190 date: "2026-02-13".to_string(),
191 release: 79268,
192 beta: 4996,
193 nightly: 4876,
194 esr: 792,
195 };
196 assert_eq!(totals.total_for_channel("aurora"), None);
197 assert_eq!(totals.total_for_channel("unknown"), None);
198 }
199
200 #[test]
201 fn test_deserialize_correlations_response() {
202 let data = r#"{
203 "total": 220.0,
204 "results": [
205 {
206 "item": {"Module \"cscapi.dll\"": true},
207 "count_reference": 19432.0,
208 "count_group": 220.0,
209 "prior": null
210 },
211 {
212 "item": {"startup_crash": null},
213 "count_reference": 920.0,
214 "count_group": 65.0,
215 "prior": {
216 "item": {"process_type": "parent"},
217 "count_reference": 3630.0,
218 "count_group": 112.0,
219 "total_reference": 79268.0,
220 "total_group": 220.0
221 }
222 }
223 ]
224 }"#;
225 let resp: CorrelationsResponse = serde_json::from_str(data).unwrap();
226 assert_eq!(resp.total, 220.0);
227 assert_eq!(resp.results.len(), 2);
228 assert_eq!(resp.results[0].count_group, 220.0);
229 assert!(resp.results[0].prior.is_none());
230 assert!(resp.results[1].prior.is_some());
231 }
232
233 #[test]
234 fn test_to_summary_percentages() {
235 let totals = CorrelationsTotals {
236 date: "2026-02-13".to_string(),
237 release: 79268,
238 beta: 4996,
239 nightly: 4876,
240 esr: 792,
241 };
242 let mut item = HashMap::new();
243 item.insert("Module \"cscapi.dll\"".to_string(), json!(true));
244 let resp = CorrelationsResponse {
245 total: 220.0,
246 results: vec![CorrelationResult {
247 item,
248 count_reference: 19432.0,
249 count_group: 220.0,
250 prior: None,
251 }],
252 };
253 let summary = resp.to_summary("TestSig", "release", &totals);
254 assert_eq!(summary.sig_count, 220.0);
255 assert_eq!(summary.ref_count, 79268);
256 assert!((summary.items[0].sig_pct - 100.0).abs() < 0.01);
257 assert!((summary.items[0].ref_pct - 24.51).abs() < 0.01);
258 }
259
260 #[test]
261 fn test_to_summary_with_prior() {
262 let totals = CorrelationsTotals {
263 date: "2026-02-13".to_string(),
264 release: 79268,
265 beta: 4996,
266 nightly: 4876,
267 esr: 792,
268 };
269 let mut item = HashMap::new();
270 item.insert("startup_crash".to_string(), serde_json::Value::Null);
271 let mut prior_item = HashMap::new();
272 prior_item.insert("process_type".to_string(), json!("parent"));
273 let resp = CorrelationsResponse {
274 total: 220.0,
275 results: vec![CorrelationResult {
276 item,
277 count_reference: 920.0,
278 count_group: 65.0,
279 prior: Some(CorrelationPrior {
280 item: prior_item,
281 count_reference: 3630.0,
282 count_group: 112.0,
283 total_reference: 79268.0,
284 total_group: 220.0,
285 }),
286 }],
287 };
288 let summary = resp.to_summary("TestSig", "release", &totals);
289 let item = &summary.items[0];
290 assert!((item.sig_pct - 29.545).abs() < 0.01);
291 let prior = item.prior.as_ref().unwrap();
292 assert!((prior.sig_pct - 50.909).abs() < 0.01);
293 assert!((prior.ref_pct - 4.578).abs() < 0.01);
294 }
295
296 #[test]
297 fn test_format_item_map_single_key_true() {
298 let mut item = HashMap::new();
299 item.insert("Module \"cscapi.dll\"".to_string(), json!(true));
300 assert_eq!(format_item_map(&item), "Module \"cscapi.dll\" = true");
301 }
302
303 #[test]
304 fn test_format_item_map_single_key_null() {
305 let mut item = HashMap::new();
306 item.insert("startup_crash".to_string(), serde_json::Value::Null);
307 assert_eq!(format_item_map(&item), "startup_crash = null");
308 }
309
310 #[test]
311 fn test_format_item_map_single_key_string() {
312 let mut item = HashMap::new();
313 item.insert("process_type".to_string(), json!("parent"));
314 assert_eq!(format_item_map(&item), "process_type = parent");
315 }
316
317 #[test]
318 fn test_format_item_map_multi_key_sorted() {
319 let mut item = HashMap::new();
320 item.insert("z_field".to_string(), json!(true));
321 item.insert("a_field".to_string(), json!("value"));
322 let result = format_item_map(&item);
323 assert_eq!(result, "a_field = value \u{2227} z_field = true");
324 }
325}