Skip to main content

lmn_core/response_template/
stats.rs

1use 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}