Skip to main content

ic_query/
report.rs

1//! Shared report provenance classifications.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Compare the endpoint and collection provenance echoed by a report source.
7#[cfg(any(feature = "cloud-engine-host", feature = "dashboard-host"))]
8pub(crate) fn source_provenance_mismatch(
9    expected: (&str, &str, &str),
10    actual: (&str, &str, &str),
11) -> Option<String> {
12    [
13        ("source_endpoint", expected.0, actual.0),
14        ("fetched_at", expected.1, actual.1),
15        ("fetched_by", expected.2, actual.2),
16    ]
17    .into_iter()
18    .find_map(|(field, expected, actual)| {
19        (actual != expected)
20            .then(|| format!("{field} is {actual:?}, expected requested value {expected:?}"))
21    })
22}
23
24///
25/// SourceFuture
26///
27/// Caller-runtime future returned by an object-safe report source capability.
28///
29
30pub type SourceFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
31
32///
33/// ReportDataSource
34///
35/// Origin of the data exposed by a report.
36///
37
38#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
39#[serde(rename_all = "snake_case")]
40pub enum ReportDataSource {
41    /// Rows were collected from live IC query calls.
42    Live,
43    /// Rows were read from a complete local snapshot.
44    Cache,
45}
46
47impl ReportDataSource {
48    /// Return the stable JSON and text label.
49    #[must_use]
50    pub const fn as_str(self) -> &'static str {
51        match self {
52            Self::Live => "live",
53            Self::Cache => "cache",
54        }
55    }
56}
57
58impl fmt::Display for ReportDataSource {
59    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60        formatter.write_str(self.as_str())
61    }
62}
63
64///
65/// ReportResultScope
66///
67/// Completeness boundary represented by a report view.
68///
69
70#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
71#[serde(rename_all = "kebab-case")]
72pub enum ReportResultScope {
73    /// A bounded page or detail view collected live.
74    BoundedLive,
75    /// A view derived from an API-exhausted complete cache.
76    CompleteCache,
77}
78
79impl ReportResultScope {
80    /// Return the stable JSON and text label.
81    #[must_use]
82    pub const fn as_str(self) -> &'static str {
83        match self {
84            Self::BoundedLive => "bounded-live",
85            Self::CompleteCache => "complete-cache",
86        }
87    }
88}
89
90impl fmt::Display for ReportResultScope {
91    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
92        formatter.write_str(self.as_str())
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::{ReportDataSource, ReportResultScope};
99
100    #[cfg(any(feature = "cloud-engine-host", feature = "dashboard-host"))]
101    #[test]
102    fn source_provenance_comparison_preserves_field_order() {
103        let expected = ("endpoint", "time", "collector");
104        assert_eq!(super::source_provenance_mismatch(expected, expected), None);
105        for (actual, field) in [
106            (("other", "other", "other"), "source_endpoint"),
107            (("endpoint", "other", "other"), "fetched_at"),
108            (("endpoint", "time", "other"), "fetched_by"),
109        ] {
110            let reason = super::source_provenance_mismatch(expected, actual).unwrap();
111            assert!(reason.starts_with(&format!("{field} is ")));
112        }
113    }
114
115    #[test]
116    fn report_provenance_labels_round_trip() {
117        for (source, label) in [
118            (ReportDataSource::Live, "live"),
119            (ReportDataSource::Cache, "cache"),
120        ] {
121            assert_eq!(source.as_str(), label);
122            assert_eq!(source.to_string(), label);
123            assert_eq!(
124                serde_json::to_string(&source).expect("serialize report data source"),
125                format!("\"{label}\"")
126            );
127            assert_eq!(
128                serde_json::from_str::<ReportDataSource>(&format!("\"{label}\""))
129                    .expect("deserialize report data source"),
130                source
131            );
132        }
133        for (scope, label) in [
134            (ReportResultScope::BoundedLive, "bounded-live"),
135            (ReportResultScope::CompleteCache, "complete-cache"),
136        ] {
137            assert_eq!(scope.as_str(), label);
138            assert_eq!(scope.to_string(), label);
139            assert_eq!(
140                serde_json::to_string(&scope).expect("serialize report result scope"),
141                format!("\"{label}\"")
142            );
143            assert_eq!(
144                serde_json::from_str::<ReportResultScope>(&format!("\"{label}\""))
145                    .expect("deserialize report result scope"),
146                scope
147            );
148        }
149        assert!(serde_json::from_str::<ReportDataSource>("\"api\"").is_err());
150        assert!(serde_json::from_str::<ReportResultScope>("\"partial\"").is_err());
151    }
152}