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
113#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
120pub struct ReportProvenance {
121 pub data_source: ReportDataSource,
123 pub cache_path: Option<String>,
125 pub cache_complete: Option<bool>,
127}
128
129impl ReportProvenance {
130 pub(crate) const fn live() -> Self {
132 Self {
133 data_source: ReportDataSource::Live,
134 cache_path: None,
135 cache_complete: None,
136 }
137 }
138
139 #[cfg(any(feature = "nns-host", feature = "sns-host"))]
141 pub(crate) fn cache(cache_path: &Path) -> Self {
142 Self {
143 data_source: ReportDataSource::Cache,
144 cache_path: Some(cache_path.display().to_string()),
145 cache_complete: Some(true),
146 }
147 }
148}
149
150#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
157#[serde(rename_all = "snake_case")]
158pub enum ReportDataSource {
159 Live,
161 Cache,
163}
164
165impl ReportDataSource {
166 #[must_use]
168 pub const fn as_str(self) -> &'static str {
169 match self {
170 Self::Live => "live",
171 Self::Cache => "cache",
172 }
173 }
174}
175
176impl fmt::Display for ReportDataSource {
177 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
178 formatter.write_str(self.as_str())
179 }
180}
181
182#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
189#[serde(rename_all = "kebab-case")]
190pub enum ReportResultScope {
191 BoundedLive,
193 CompleteCache,
195}
196
197impl ReportResultScope {
198 #[must_use]
200 pub const fn as_str(self) -> &'static str {
201 match self {
202 Self::BoundedLive => "bounded-live",
203 Self::CompleteCache => "complete-cache",
204 }
205 }
206}
207
208impl fmt::Display for ReportResultScope {
209 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
210 formatter.write_str(self.as_str())
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::{ReportDataSource, ReportResultScope};
217
218 #[cfg(any(feature = "cloud-engine-host", feature = "dashboard-host"))]
219 #[test]
220 fn source_provenance_comparison_preserves_field_order() {
221 let expected = ("endpoint", "time", "collector");
222 assert_eq!(super::source_provenance_mismatch(expected, expected), None);
223 for (actual, field) in [
224 (("other", "other", "other"), "source_endpoint"),
225 (("endpoint", "other", "other"), "fetched_at"),
226 (("endpoint", "time", "other"), "fetched_by"),
227 ] {
228 let reason = super::source_provenance_mismatch(expected, actual).unwrap();
229 assert!(reason.starts_with(&format!("{field} is ")));
230 }
231 }
232
233 #[test]
234 fn report_provenance_labels_round_trip() {
235 for (source, label) in [
236 (ReportDataSource::Live, "live"),
237 (ReportDataSource::Cache, "cache"),
238 ] {
239 assert_eq!(source.as_str(), label);
240 assert_eq!(source.to_string(), label);
241 assert_eq!(
242 serde_json::to_string(&source).expect("serialize report data source"),
243 format!("\"{label}\"")
244 );
245 assert_eq!(
246 serde_json::from_str::<ReportDataSource>(&format!("\"{label}\""))
247 .expect("deserialize report data source"),
248 source
249 );
250 }
251 for (scope, label) in [
252 (ReportResultScope::BoundedLive, "bounded-live"),
253 (ReportResultScope::CompleteCache, "complete-cache"),
254 ] {
255 assert_eq!(scope.as_str(), label);
256 assert_eq!(scope.to_string(), label);
257 assert_eq!(
258 serde_json::to_string(&scope).expect("serialize report result scope"),
259 format!("\"{label}\"")
260 );
261 assert_eq!(
262 serde_json::from_str::<ReportResultScope>(&format!("\"{label}\""))
263 .expect("deserialize report result scope"),
264 scope
265 );
266 }
267 assert!(serde_json::from_str::<ReportDataSource>("\"api\"").is_err());
268 assert!(serde_json::from_str::<ReportResultScope>("\"partial\"").is_err());
269 }
270}