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 Statistics {
13 min: f64,
14 p25: Option<f64>,
15 mean: f64,
16 p50: Option<f64>,
17 p75: Option<f64>,
18 max: f64,
19 },
20 FrequencyResponse {
22 magnitude: Vec<f64>,
23 phase: Option<Vec<f64>>,
24 },
25 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
289pub 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 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 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
330pub 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}