Skip to main content

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