lmn_core/response_template/
stats.rs1use std::collections::HashMap;
2
3use crate::response_template::extractor::{ExtractedValue, ExtractionResult};
4
5pub struct FloatFieldStats {
6 pub values: Vec<f64>,
7}
8
9pub struct ResponseStats {
10 pub string_distributions: HashMap<String, HashMap<String, usize>>,
11 pub float_fields: HashMap<String, FloatFieldStats>,
12 pub mismatch_counts: HashMap<String, usize>,
13 pub total_responses: usize,
14}
15
16impl Default for ResponseStats {
17 fn default() -> Self {
18 Self::new()
19 }
20}
21
22impl ResponseStats {
23 pub fn new() -> Self {
24 Self {
25 string_distributions: HashMap::new(),
26 float_fields: HashMap::new(),
27 mismatch_counts: HashMap::new(),
28 total_responses: 0,
29 }
30 }
31
32 pub fn record(&mut self, result: ExtractionResult) {
33 self.total_responses += 1;
34
35 for (path, value) in result.values {
36 match value {
37 ExtractedValue::String(s) => {
38 *self
39 .string_distributions
40 .entry(path)
41 .or_default()
42 .entry(s)
43 .or_insert(0) += 1;
44 }
45 ExtractedValue::Float(f) => {
46 self.float_fields
47 .entry(path)
48 .or_insert_with(|| FloatFieldStats { values: Vec::new() })
49 .values
50 .push(f);
51 }
52 }
53 }
54
55 for path in result.mismatches {
56 *self.mismatch_counts.entry(path).or_insert(0) += 1;
57 }
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use crate::response_template::extractor::{ExtractedValue, ExtractionResult};
65
66 fn empty_result() -> ExtractionResult {
67 ExtractionResult {
68 values: vec![],
69 mismatches: vec![],
70 }
71 }
72
73 fn mixed_result() -> ExtractionResult {
74 ExtractionResult {
75 values: vec![
76 (
77 "status".to_string(),
78 ExtractedValue::String("ok".to_string()),
79 ),
80 ("score".to_string(), ExtractedValue::Float(9.5)),
81 ],
82 mismatches: vec!["missing".to_string()],
83 }
84 }
85
86 #[test]
87 fn empty_result_still_increments_total() {
88 let mut stats = ResponseStats::new();
89 stats.record(empty_result());
90 assert_eq!(stats.total_responses, 1);
91 assert!(stats.string_distributions.is_empty());
92 assert!(stats.float_fields.is_empty());
93 assert!(stats.mismatch_counts.is_empty());
94 }
95
96 #[test]
97 fn mixed_result_records_all_field_types() {
98 let mut stats = ResponseStats::new();
99 stats.record(mixed_result());
100 assert!(stats.string_distributions.contains_key("status"));
101 assert!(stats.float_fields.contains_key("score"));
102 assert_eq!(stats.mismatch_counts["missing"], 1);
103 }
104
105 #[test]
106 fn float_values_accumulate_across_records() {
107 let mut stats = ResponseStats::new();
108 stats.record(mixed_result());
109 stats.record(mixed_result());
110 assert_eq!(stats.float_fields["score"].values, vec![9.5, 9.5]);
111 }
112}