Skip to main content

insta_fun/
meta.rs

1use std::fmt;
2
3pub use insta_fun_meta_macros::insta_fun_meta;
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum MetaValue {
7    Scalar(f64),
8    Range { min: f64, max: f64 },
9    Line(Vec<f64>),
10    Histogram(Vec<f64>),
11    /// Statistical summary with min/mean/max and optional percentiles (p25, p50, p75)
12    Statistics {
13        min: f64,
14        p25: Option<f64>,
15        mean: f64,
16        p50: Option<f64>,
17        p75: Option<f64>,
18        max: f64,
19    },
20    /// Frequency response: magnitude in dB at each frequency bin, optional phase in degrees
21    FrequencyResponse {
22        magnitude: Vec<f64>,
23        phase: Option<Vec<f64>>,
24    },
25    /// Tabular metadata: field name -> value pairs
26    Table(Vec<(String, String)>),
27}
28
29#[derive(Debug, Clone, PartialEq)]
30pub struct MetaField {
31    pub name: String,
32    pub value: MetaValue,
33}
34
35impl MetaField {
36    pub fn new(name: impl Into<String>, value: impl Into<MetaValue>) -> Self {
37        Self {
38            name: name.into(),
39            value: value.into(),
40        }
41    }
42}
43
44#[derive(Debug, Clone, PartialEq)]
45pub struct SnapshotMetadata {
46    pub fields: Vec<MetaField>,
47}
48
49impl SnapshotMetadata {
50    pub fn new(fields: Vec<MetaField>) -> Self {
51        Self { fields }
52    }
53
54    pub fn validate(&self) -> Result<(), MetadataValidationError> {
55        if self.fields.is_empty() {
56            return Err(MetadataValidationError::EmptyMetadata);
57        }
58
59        for field in &self.fields {
60            if field.name.trim().is_empty() {
61                return Err(MetadataValidationError::EmptyFieldName);
62            }
63
64            match &field.value {
65                MetaValue::Scalar(v) => {
66                    if !v.is_finite() {
67                        return Err(MetadataValidationError::NonFiniteValue {
68                            field: field.name.clone(),
69                        });
70                    }
71                }
72                MetaValue::Range { min, max } => {
73                    if !min.is_finite() || !max.is_finite() {
74                        return Err(MetadataValidationError::NonFiniteValue {
75                            field: field.name.clone(),
76                        });
77                    }
78                }
79                MetaValue::Line(values) | MetaValue::Histogram(values) => {
80                    if values.is_empty() {
81                        return Err(MetadataValidationError::EmptySeries {
82                            field: field.name.clone(),
83                        });
84                    }
85                    if values.iter().any(|v| !v.is_finite()) {
86                        return Err(MetadataValidationError::NonFiniteValue {
87                            field: field.name.clone(),
88                        });
89                    }
90                }
91                MetaValue::Statistics {
92                    min,
93                    mean,
94                    max,
95                    p25,
96                    p50,
97                    p75,
98                } => {
99                    if !min.is_finite() || !mean.is_finite() || !max.is_finite() {
100                        return Err(MetadataValidationError::NonFiniteValue {
101                            field: field.name.clone(),
102                        });
103                    }
104                    if let Some(p) = p25
105                        && (!p.is_finite() || *p < *min || *p > *max) {
106                            return Err(MetadataValidationError::InvalidStatistics {
107                                field: field.name.clone(),
108                            });
109                        }
110                    if let Some(p) = p50
111                        && (!p.is_finite() || *p < *min || *p > *max) {
112                            return Err(MetadataValidationError::InvalidStatistics {
113                                field: field.name.clone(),
114                            });
115                        }
116                    if let Some(p) = p75
117                        && (!p.is_finite() || *p < *min || *p > *max) {
118                            return Err(MetadataValidationError::InvalidStatistics {
119                                field: field.name.clone(),
120                            });
121                        }
122                }
123                MetaValue::FrequencyResponse { magnitude: _, phase } => {
124                    if let Some(phases) = phase
125                        && phases.iter().any(|v| !v.is_finite()) {
126                            return Err(MetadataValidationError::NonFiniteValue {
127                                field: field.name.clone(),
128                            });
129                        }
130                }
131                MetaValue::Table(pairs) => {
132                    if pairs.is_empty() {
133                        return Err(MetadataValidationError::EmptySeries {
134                            field: field.name.clone(),
135                        });
136                    }
137                    let mut seen_keys = std::collections::HashSet::new();
138                    for (key, _) in pairs {
139                        if key.trim().is_empty() {
140                            return Err(MetadataValidationError::EmptyFieldName);
141                        }
142                        if !seen_keys.insert(key.clone()) {
143                            return Err(MetadataValidationError::DuplicateTableKey {
144                                field: field.name.clone(),
145                                key: key.clone(),
146                            });
147                        }
148                    }
149                }
150            }
151        }
152
153        Ok(())
154    }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub enum MetadataValidationError {
159    EmptyMetadata,
160    EmptyFieldName,
161    EmptySeries { field: String },
162    NonFiniteValue { field: String },
163    InvalidStatistics { field: String },
164    DuplicateTableKey { field: String, key: String },
165}
166
167impl fmt::Display for MetadataValidationError {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        match self {
170            MetadataValidationError::EmptyMetadata => {
171                write!(f, "metadata dashboard requires at least one field")
172            }
173            MetadataValidationError::EmptyFieldName => {
174                write!(f, "metadata field name cannot be empty")
175            }
176            MetadataValidationError::EmptySeries { field } => {
177                write!(f, "metadata field '{field}' has an empty series")
178            }
179            MetadataValidationError::NonFiniteValue { field } => {
180                write!(f, "metadata field '{field}' contains non-finite values")
181            }
182            MetadataValidationError::InvalidStatistics { field } => {
183                write!(f, "metadata field '{field}' has invalid percentiles (must be between min and max)")
184            }
185            MetadataValidationError::DuplicateTableKey { field, key } => {
186                write!(f, "metadata field '{field}' has duplicate table key '{key}'")
187            }
188        }
189    }
190}
191
192impl std::error::Error for MetadataValidationError {}
193
194pub trait ToMetaNumber {
195    fn to_meta_number(self) -> f64;
196}
197
198macro_rules! impl_to_meta_number {
199    ($($ty:ty),* $(,)?) => {
200        $(
201            impl ToMetaNumber for $ty {
202                fn to_meta_number(self) -> f64 {
203                    self as f64
204                }
205            }
206        )*
207    };
208}
209
210impl_to_meta_number!(
211    f32, f64, i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
212);
213
214pub fn scalar<T>(value: T) -> MetaValue
215where
216    T: ToMetaNumber,
217{
218    MetaValue::Scalar(value.to_meta_number())
219}
220
221pub fn range<T, U>(min: T, max: U) -> MetaValue
222where
223    T: ToMetaNumber,
224    U: ToMetaNumber,
225{
226    MetaValue::Range {
227        min: min.to_meta_number(),
228        max: max.to_meta_number(),
229    }
230}
231
232pub fn line<I, T>(values: I) -> MetaValue
233where
234    I: IntoIterator<Item = T>,
235    T: ToMetaNumber,
236{
237    MetaValue::Line(values.into_iter().map(ToMetaNumber::to_meta_number).collect())
238}
239
240pub fn histogram<I, T>(values: I) -> MetaValue
241where
242    I: IntoIterator<Item = T>,
243    T: ToMetaNumber,
244{
245    MetaValue::Histogram(values.into_iter().map(ToMetaNumber::to_meta_number).collect())
246}
247
248pub fn statistics<T, U>(
249    min: T,
250    mean: U,
251    max: T,
252) -> MetaValue
253where
254    T: ToMetaNumber,
255    U: ToMetaNumber,
256{
257    MetaValue::Statistics {
258        min: min.to_meta_number(),
259        mean: mean.to_meta_number(),
260        max: max.to_meta_number(),
261        p25: None,
262        p50: None,
263        p75: None,
264    }
265}
266
267pub fn statistics_with_percentiles<T, U>(
268    min: T,
269    p25: Option<T>,
270    p50: Option<T>,
271    mean: U,
272    p75: Option<T>,
273    max: T,
274) -> MetaValue
275where
276    T: ToMetaNumber,
277    U: ToMetaNumber,
278{
279    MetaValue::Statistics {
280        min: min.to_meta_number(),
281        p25: p25.map(|v| v.to_meta_number()),
282        p50: p50.map(|v| v.to_meta_number()),
283        mean: mean.to_meta_number(),
284        p75: p75.map(|v| v.to_meta_number()),
285        max: max.to_meta_number(),
286    }
287}
288
289/// Compute statistics from raw data, auto-calculating min, mean, max and percentiles (p25, p50, p75).
290pub fn statistics_from_data<I, T>(data: I) -> MetaValue
291where
292    I: IntoIterator<Item = T>,
293    T: ToMetaNumber,
294{
295    let values: Vec<f64> = data.into_iter().map(ToMetaNumber::to_meta_number).collect();
296    if values.is_empty() {
297        // Return default for empty data
298        return MetaValue::Statistics {
299            min: 0.0,
300            p25: None,
301            mean: 0.0,
302            p50: None,
303            p75: None,
304            max: 0.0,
305        };
306    }
307
308    let min = values.iter().copied().fold(f64::INFINITY, f64::min);
309    let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
310    let mean = values.iter().sum::<f64>() / values.len() as f64;
311
312    // Compute percentiles
313    let mut sorted = values.clone();
314    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
315
316    let p25 = Some(sorted[sorted.len() / 4]);
317    let p50 = Some(sorted[sorted.len() / 2]);
318    let p75 = Some(sorted[(sorted.len() * 3) / 4]);
319
320    MetaValue::Statistics {
321        min,
322        p25,
323        mean,
324        p50,
325        p75,
326        max,
327    }
328}
329
330/// Compute statistics from raw data without percentiles (min, mean, max only).
331pub fn statistics_from_data_simple<I, T>(data: I) -> MetaValue
332where
333    I: IntoIterator<Item = T>,
334    T: ToMetaNumber,
335{
336    let values: Vec<f64> = data.into_iter().map(ToMetaNumber::to_meta_number).collect();
337    if values.is_empty() {
338        return MetaValue::Statistics {
339            min: 0.0,
340            p25: None,
341            mean: 0.0,
342            p50: None,
343            p75: None,
344            max: 0.0,
345        };
346    }
347
348    let min = values.iter().copied().fold(f64::INFINITY, f64::min);
349    let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
350    let mean = values.iter().sum::<f64>() / values.len() as f64;
351
352    MetaValue::Statistics {
353        min,
354        p25: None,
355        mean,
356        p50: None,
357        p75: None,
358        max,
359    }
360}
361
362pub fn frequency_response<I, T>(magnitude: I) -> MetaValue
363where
364    I: IntoIterator<Item = T>,
365    T: ToMetaNumber,
366{
367    MetaValue::FrequencyResponse {
368        magnitude: magnitude.into_iter().map(ToMetaNumber::to_meta_number).collect(),
369        phase: None,
370    }
371}
372
373pub fn frequency_response_with_phase<I, T, J, U>(
374    magnitude: I,
375    phase: J,
376) -> MetaValue
377where
378    I: IntoIterator<Item = T>,
379    T: ToMetaNumber,
380    J: IntoIterator<Item = U>,
381    U: ToMetaNumber,
382{
383    MetaValue::FrequencyResponse {
384        magnitude: magnitude.into_iter().map(ToMetaNumber::to_meta_number).collect(),
385        phase: Some(phase.into_iter().map(ToMetaNumber::to_meta_number).collect()),
386    }
387}
388
389pub fn table<I, K, V>(pairs: I) -> MetaValue
390where
391    I: IntoIterator<Item = (K, V)>,
392    K: Into<String>,
393    V: fmt::Display,
394{
395    MetaValue::Table(
396        pairs
397            .into_iter()
398            .map(|(k, v)| (k.into(), v.to_string()))
399            .collect(),
400    )
401}