term_guard/analyzers/
context.rs

1//! Context for storing analyzer computation results.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use super::errors::AnalyzerError;
7use super::types::MetricValue;
8
9/// Context that stores the results of analyzer computations.
10///
11/// The AnalyzerContext provides a centralized storage for metrics computed
12/// by different analyzers, allowing for efficient access and serialization
13/// of results.
14///
15/// # Example
16///
17/// ```rust,ignore
18/// use term_guard::analyzers::{AnalyzerContext, MetricValue};
19///
20/// let mut context = AnalyzerContext::new();
21///
22/// // Store metrics from different analyzers
23/// context.store_metric("size", MetricValue::Long(1000));
24/// context.store_metric("completeness.user_id", MetricValue::Double(0.98));
25///
26/// // Retrieve metrics
27/// if let Some(size) = context.get_metric("size") {
28///     println!("Dataset size: {}", size);
29/// }
30///
31/// // Get all metrics for a specific analyzer
32/// let completeness_metrics = context.get_analyzer_metrics("completeness");
33/// ```
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct AnalyzerContext {
36    /// Stored metrics indexed by analyzer name and metric key.
37    metrics: HashMap<String, MetricValue>,
38
39    /// Metadata about the analysis run.
40    metadata: AnalysisMetadata,
41
42    /// Errors that occurred during analysis.
43    errors: Vec<AnalysisError>,
44}
45
46impl AnalyzerContext {
47    /// Creates a new empty analyzer context.
48    pub fn new() -> Self {
49        Self {
50            metrics: HashMap::new(),
51            metadata: AnalysisMetadata::new(),
52            errors: Vec::new(),
53        }
54    }
55
56    /// Creates a new context with the given dataset name.
57    pub fn with_dataset(dataset_name: impl Into<String>) -> Self {
58        Self {
59            metrics: HashMap::new(),
60            metadata: AnalysisMetadata::with_dataset(dataset_name),
61            errors: Vec::new(),
62        }
63    }
64
65    /// Stores a metric value with the given key.
66    ///
67    /// # Arguments
68    ///
69    /// * `key` - The metric key, typically in format "analyzer_name.metric_name"
70    /// * `value` - The metric value to store
71    pub fn store_metric(&mut self, key: impl Into<String>, value: MetricValue) {
72        self.metrics.insert(key.into(), value);
73    }
74
75    /// Stores a metric with a composite key built from analyzer and metric names.
76    ///
77    /// # Arguments
78    ///
79    /// * `analyzer_name` - The name of the analyzer
80    /// * `metric_name` - The name of the specific metric
81    /// * `value` - The metric value to store
82    pub fn store_analyzer_metric(
83        &mut self,
84        analyzer_name: &str,
85        metric_name: &str,
86        value: MetricValue,
87    ) {
88        let key = format!("{analyzer_name}.{metric_name}");
89        self.store_metric(key, value);
90    }
91
92    /// Retrieves a metric value by key.
93    pub fn get_metric(&self, key: &str) -> Option<&MetricValue> {
94        self.metrics.get(key)
95    }
96
97    /// Retrieves all metrics for a specific analyzer.
98    ///
99    /// Returns metrics whose keys start with the analyzer name followed by a dot.
100    pub fn get_analyzer_metrics(&self, analyzer_name: &str) -> HashMap<String, &MetricValue> {
101        let prefix = format!("{analyzer_name}.");
102        self.metrics
103            .iter()
104            .filter(|(k, _)| k.starts_with(&prefix))
105            .map(|(k, v)| {
106                let metric_name = k.strip_prefix(&prefix).unwrap_or(k);
107                (metric_name.to_string(), v)
108            })
109            .collect()
110    }
111
112    /// Returns all stored metrics.
113    pub fn all_metrics(&self) -> &HashMap<String, MetricValue> {
114        &self.metrics
115    }
116
117    /// Records an error that occurred during analysis.
118    pub fn record_error(&mut self, analyzer_name: impl Into<String>, error: AnalyzerError) {
119        self.errors.push(AnalysisError {
120            analyzer_name: analyzer_name.into(),
121            error: error.to_string(),
122        });
123    }
124
125    /// Returns all recorded errors.
126    pub fn errors(&self) -> &[AnalysisError] {
127        &self.errors
128    }
129
130    /// Checks if any errors occurred during analysis.
131    pub fn has_errors(&self) -> bool {
132        !self.errors.is_empty()
133    }
134
135    /// Returns the analysis metadata.
136    pub fn metadata(&self) -> &AnalysisMetadata {
137        &self.metadata
138    }
139
140    /// Returns a mutable reference to the analysis metadata.
141    pub fn metadata_mut(&mut self) -> &mut AnalysisMetadata {
142        &mut self.metadata
143    }
144
145    /// Merges another context into this one.
146    ///
147    /// Metrics from the other context will overwrite existing metrics with the same key.
148    pub fn merge(&mut self, other: AnalyzerContext) {
149        self.metrics.extend(other.metrics);
150        self.errors.extend(other.errors);
151        self.metadata.merge(other.metadata);
152    }
153
154    /// Creates a summary of the analysis results.
155    pub fn summary(&self) -> AnalysisSummary {
156        AnalysisSummary {
157            total_metrics: self.metrics.len(),
158            total_errors: self.errors.len(),
159            analyzer_count: self.count_analyzers(),
160            dataset_name: self.metadata.dataset_name.clone(),
161        }
162    }
163
164    /// Counts the number of unique analyzers that contributed metrics.
165    fn count_analyzers(&self) -> usize {
166        let mut analyzers = std::collections::HashSet::new();
167        for key in self.metrics.keys() {
168            if let Some(analyzer_name) = key.split('.').next() {
169                analyzers.insert(analyzer_name);
170            }
171        }
172        analyzers.len()
173    }
174}
175
176impl Default for AnalyzerContext {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182/// Metadata about an analysis run.
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct AnalysisMetadata {
185    /// Name of the dataset being analyzed.
186    pub dataset_name: Option<String>,
187
188    /// Timestamp when the analysis started.
189    pub start_time: Option<chrono::DateTime<chrono::Utc>>,
190
191    /// Timestamp when the analysis completed.
192    pub end_time: Option<chrono::DateTime<chrono::Utc>>,
193
194    /// Additional custom metadata.
195    pub custom: HashMap<String, String>,
196}
197
198impl AnalysisMetadata {
199    /// Creates new empty metadata.
200    pub fn new() -> Self {
201        Self {
202            dataset_name: None,
203            start_time: None,
204            end_time: None,
205            custom: HashMap::new(),
206        }
207    }
208
209    /// Creates metadata with a dataset name.
210    pub fn with_dataset(name: impl Into<String>) -> Self {
211        Self {
212            dataset_name: Some(name.into()),
213            start_time: None,
214            end_time: None,
215            custom: HashMap::new(),
216        }
217    }
218
219    /// Records the start time of the analysis.
220    pub fn record_start(&mut self) {
221        self.start_time = Some(chrono::Utc::now());
222    }
223
224    /// Records the end time of the analysis.
225    pub fn record_end(&mut self) {
226        self.end_time = Some(chrono::Utc::now());
227    }
228
229    /// Returns the duration of the analysis if both start and end times are recorded.
230    pub fn duration(&self) -> Option<chrono::Duration> {
231        match (self.start_time, self.end_time) {
232            (Some(start), Some(end)) => Some(end - start),
233            _ => None,
234        }
235    }
236
237    /// Adds custom metadata.
238    pub fn add_custom(&mut self, key: impl Into<String>, value: impl Into<String>) {
239        self.custom.insert(key.into(), value.into());
240    }
241
242    /// Merges another metadata instance into this one.
243    fn merge(&mut self, other: AnalysisMetadata) {
244        if self.dataset_name.is_none() {
245            self.dataset_name = other.dataset_name;
246        }
247        if self.start_time.is_none() {
248            self.start_time = other.start_time;
249        }
250        if self.end_time.is_none() {
251            self.end_time = other.end_time;
252        }
253        self.custom.extend(other.custom);
254    }
255}
256
257impl Default for AnalysisMetadata {
258    fn default() -> Self {
259        Self::new()
260    }
261}
262
263/// Error information from analysis.
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct AnalysisError {
266    /// Name of the analyzer that produced the error.
267    pub analyzer_name: String,
268
269    /// Error message.
270    pub error: String,
271}
272
273/// Summary of analysis results.
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct AnalysisSummary {
276    /// Total number of metrics computed.
277    pub total_metrics: usize,
278
279    /// Total number of errors encountered.
280    pub total_errors: usize,
281
282    /// Number of unique analyzers that ran.
283    pub analyzer_count: usize,
284
285    /// Name of the analyzed dataset.
286    pub dataset_name: Option<String>,
287}