1use serde::{Deserialize, Serialize};
4use std::fmt;
5#[cfg(any(feature = "nns-host", feature = "sns-host"))]
6use std::path::Path;
7
8#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct CollectionSourceRequest {
17 pub network: String,
19 pub endpoint: String,
21 pub fetched_at: String,
23 pub fetched_by: String,
25}
26
27impl CollectionSourceRequest {
28 #[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 #[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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
68pub enum SortDirection {
69 Asc,
71 #[default]
73 Desc,
74}
75
76impl SortDirection {
77 #[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#[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
105pub type SourceFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
112
113pub(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 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 #[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#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
153#[serde(rename_all = "snake_case")]
154pub enum ReportDataSource {
155 Live,
157 Cache,
159}
160
161impl ReportDataSource {
162 #[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#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
185#[serde(rename_all = "kebab-case")]
186pub enum ReportResultScope {
187 BoundedLive,
189 CompleteCache,
191}
192
193impl ReportResultScope {
194 #[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}