Skip to main content

ic_query/
report.rs

1//! Shared collection provenance and report classifications.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5#[cfg(any(feature = "nns-host", feature = "sns-host"))]
6use std::path::Path;
7
8///
9/// CollectionSourceRequest
10///
11/// Explicit network, endpoint and collection provenance shared by canister report sources.
12/// Source families retain their own network admission and transport policy.
13///
14
15#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct CollectionSourceRequest {
17    /// Requested IC network identity.
18    pub network: String,
19    /// Explicit replica endpoint used for collection.
20    pub endpoint: String,
21    /// UTC collection timestamp recorded in the report.
22    pub fetched_at: String,
23    /// Collector identity recorded in the report.
24    pub fetched_by: String,
25}
26
27impl CollectionSourceRequest {
28    /// Construct source settings with explicit network and provenance.
29    #[must_use]
30    pub fn new(
31        network: impl Into<String>,
32        endpoint: impl Into<String>,
33        fetched_at: impl Into<String>,
34        fetched_by: impl Into<String>,
35    ) -> Self {
36        Self {
37            network: network.into(),
38            endpoint: endpoint.into(),
39            fetched_at: fetched_at.into(),
40            fetched_by: fetched_by.into(),
41        }
42    }
43
44    /// Construct source settings from a caller-supplied Unix collection timestamp.
45    #[must_use]
46    pub fn from_unix_secs(
47        network: impl Into<String>,
48        endpoint: impl Into<String>,
49        fetched_at_unix_secs: u64,
50        fetched_by: impl Into<String>,
51    ) -> Self {
52        Self::new(
53            network,
54            endpoint,
55            ic_host_text::timestamp::format_utc_timestamp_secs(fetched_at_unix_secs),
56            fetched_by,
57        )
58    }
59}
60
61///
62/// SortDirection
63///
64/// Direction shared by local report sorts; each sort key owns its default.
65///
66
67#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
68pub enum SortDirection {
69    /// Order values from lowest to highest.
70    Asc,
71    /// Order values from highest to lowest.
72    #[default]
73    Desc,
74}
75
76impl SortDirection {
77    /// Return the stable report label.
78    #[must_use]
79    pub const fn as_str(self) -> &'static str {
80        match self {
81            Self::Asc => "asc",
82            Self::Desc => "desc",
83        }
84    }
85}
86
87/// Compare the endpoint and collection provenance echoed by a report source.
88#[cfg(any(feature = "cloud-engine-host", feature = "dashboard-host"))]
89pub(crate) fn source_provenance_mismatch(
90    expected: (&str, &str, &str),
91    actual: (&str, &str, &str),
92) -> Option<String> {
93    [
94        ("source_endpoint", expected.0, actual.0),
95        ("fetched_at", expected.1, actual.1),
96        ("fetched_by", expected.2, actual.2),
97    ]
98    .into_iter()
99    .find_map(|(field, expected, actual)| {
100        (actual != expected)
101            .then(|| format!("{field} is {actual:?}, expected requested value {expected:?}"))
102    })
103}
104
105///
106/// SourceFuture
107///
108/// Caller-runtime future returned by an object-safe report source capability.
109///
110
111pub type SourceFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
112
113///
114/// ReportProvenance
115///
116/// Source and complete-cache metadata shared by report families.
117///
118
119pub(crate) struct ReportProvenance {
120    pub(crate) data_source: ReportDataSource,
121    pub(crate) cache_path: Option<String>,
122    pub(crate) cache_complete: Option<bool>,
123}
124
125impl ReportProvenance {
126    /// Build provenance for live reports.
127    pub(crate) const fn live() -> Self {
128        Self {
129            data_source: ReportDataSource::Live,
130            cache_path: None,
131            cache_complete: None,
132        }
133    }
134
135    /// Build provenance for complete-cache reports.
136    #[cfg(any(feature = "nns-host", feature = "sns-host"))]
137    pub(crate) fn cache(cache_path: &Path) -> Self {
138        Self {
139            data_source: ReportDataSource::Cache,
140            cache_path: Some(cache_path.display().to_string()),
141            cache_complete: Some(true),
142        }
143    }
144}
145
146///
147/// ReportDataSource
148///
149/// Origin of the data exposed by a report.
150///
151
152#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
153#[serde(rename_all = "snake_case")]
154pub enum ReportDataSource {
155    /// Rows were collected from live IC query calls.
156    Live,
157    /// Rows were read from a complete local snapshot.
158    Cache,
159}
160
161impl ReportDataSource {
162    /// Return the stable JSON and text label.
163    #[must_use]
164    pub const fn as_str(self) -> &'static str {
165        match self {
166            Self::Live => "live",
167            Self::Cache => "cache",
168        }
169    }
170}
171
172impl fmt::Display for ReportDataSource {
173    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
174        formatter.write_str(self.as_str())
175    }
176}
177
178///
179/// ReportResultScope
180///
181/// Completeness boundary represented by a report view.
182///
183
184#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
185#[serde(rename_all = "kebab-case")]
186pub enum ReportResultScope {
187    /// A bounded page or detail view collected live.
188    BoundedLive,
189    /// A view derived from an API-exhausted complete cache.
190    CompleteCache,
191}
192
193impl ReportResultScope {
194    /// Return the stable JSON and text label.
195    #[must_use]
196    pub const fn as_str(self) -> &'static str {
197        match self {
198            Self::BoundedLive => "bounded-live",
199            Self::CompleteCache => "complete-cache",
200        }
201    }
202}
203
204impl fmt::Display for ReportResultScope {
205    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
206        formatter.write_str(self.as_str())
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::{ReportDataSource, ReportResultScope};
213
214    #[cfg(any(feature = "cloud-engine-host", feature = "dashboard-host"))]
215    #[test]
216    fn source_provenance_comparison_preserves_field_order() {
217        let expected = ("endpoint", "time", "collector");
218        assert_eq!(super::source_provenance_mismatch(expected, expected), None);
219        for (actual, field) in [
220            (("other", "other", "other"), "source_endpoint"),
221            (("endpoint", "other", "other"), "fetched_at"),
222            (("endpoint", "time", "other"), "fetched_by"),
223        ] {
224            let reason = super::source_provenance_mismatch(expected, actual).unwrap();
225            assert!(reason.starts_with(&format!("{field} is ")));
226        }
227    }
228
229    #[test]
230    fn report_provenance_labels_round_trip() {
231        for (source, label) in [
232            (ReportDataSource::Live, "live"),
233            (ReportDataSource::Cache, "cache"),
234        ] {
235            assert_eq!(source.as_str(), label);
236            assert_eq!(source.to_string(), label);
237            assert_eq!(
238                serde_json::to_string(&source).expect("serialize report data source"),
239                format!("\"{label}\"")
240            );
241            assert_eq!(
242                serde_json::from_str::<ReportDataSource>(&format!("\"{label}\""))
243                    .expect("deserialize report data source"),
244                source
245            );
246        }
247        for (scope, label) in [
248            (ReportResultScope::BoundedLive, "bounded-live"),
249            (ReportResultScope::CompleteCache, "complete-cache"),
250        ] {
251            assert_eq!(scope.as_str(), label);
252            assert_eq!(scope.to_string(), label);
253            assert_eq!(
254                serde_json::to_string(&scope).expect("serialize report result scope"),
255                format!("\"{label}\"")
256            );
257            assert_eq!(
258                serde_json::from_str::<ReportResultScope>(&format!("\"{label}\""))
259                    .expect("deserialize report result scope"),
260                scope
261            );
262        }
263        assert!(serde_json::from_str::<ReportDataSource>("\"api\"").is_err());
264        assert!(serde_json::from_str::<ReportResultScope>("\"partial\"").is_err());
265    }
266}