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, 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
119#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
120#[serde(tag = "data_source", rename_all = "snake_case", deny_unknown_fields)]
121pub enum ReportProvenance {
122    /// Rows collected directly from their source.
123    Live,
124    /// Rows read from one admitted complete snapshot.
125    Cache {
126        /// Complete snapshot path.
127        cache_path: String,
128    },
129}
130
131impl ReportProvenance {
132    /// Build provenance for live reports.
133    pub(crate) const fn live() -> Self {
134        Self::Live
135    }
136
137    /// Build provenance for complete-cache reports.
138    #[cfg(any(feature = "nns-host", feature = "sns-host"))]
139    pub(crate) fn cache(cache_path: &Path) -> Self {
140        Self::Cache {
141            cache_path: cache_path.display().to_string(),
142        }
143    }
144
145    /// Return the origin of these rows.
146    #[must_use]
147    pub const fn data_source(&self) -> ReportDataSource {
148        match self {
149            Self::Live => ReportDataSource::Live,
150            Self::Cache { .. } => ReportDataSource::Cache,
151        }
152    }
153
154    /// Return the complete snapshot path for cached rows.
155    #[must_use]
156    pub fn cache_path(&self) -> Option<&str> {
157        match self {
158            Self::Live => None,
159            Self::Cache { cache_path } => Some(cache_path),
160        }
161    }
162
163    /// Return API-exhaustion evidence for cached rows.
164    #[must_use]
165    pub const fn cache_complete(&self) -> Option<bool> {
166        match self {
167            Self::Live => None,
168            Self::Cache { .. } => Some(true),
169        }
170    }
171}
172
173///
174/// ReportDataSource
175///
176/// Origin of the data exposed by a report.
177///
178
179#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
180#[serde(rename_all = "snake_case")]
181pub enum ReportDataSource {
182    /// Rows were collected from live IC query calls.
183    Live,
184    /// Rows were read from a complete local snapshot.
185    Cache,
186}
187
188impl ReportDataSource {
189    /// Return the stable JSON and text label.
190    #[must_use]
191    pub const fn as_str(self) -> &'static str {
192        match self {
193            Self::Live => "live",
194            Self::Cache => "cache",
195        }
196    }
197}
198
199impl fmt::Display for ReportDataSource {
200    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
201        formatter.write_str(self.as_str())
202    }
203}
204
205///
206/// ReportResultScope
207///
208/// Completeness boundary represented by a report view.
209///
210
211#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
212#[serde(rename_all = "kebab-case")]
213pub enum ReportResultScope {
214    /// A bounded page or detail view collected live.
215    BoundedLive,
216    /// A view derived from an API-exhausted complete cache.
217    CompleteCache,
218}
219
220impl ReportResultScope {
221    /// Return the stable JSON and text label.
222    #[must_use]
223    pub const fn as_str(self) -> &'static str {
224        match self {
225            Self::BoundedLive => "bounded-live",
226            Self::CompleteCache => "complete-cache",
227        }
228    }
229}
230
231impl fmt::Display for ReportResultScope {
232    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
233        formatter.write_str(self.as_str())
234    }
235}
236
237#[cfg(test)]
238mod tests;