Skip to main content

ic_query/
report.rs

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