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)]
120#[serde(tag = "data_source", rename_all = "snake_case", deny_unknown_fields)]
121pub enum ReportProvenance {
122 Live,
124 Cache {
126 cache_path: String,
128 },
129}
130
131impl ReportProvenance {
132 pub(crate) const fn live() -> Self {
134 Self::Live
135 }
136
137 #[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 #[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 #[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 #[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#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
180#[serde(rename_all = "snake_case")]
181pub enum ReportDataSource {
182 Live,
184 Cache,
186}
187
188impl ReportDataSource {
189 #[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#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
212#[serde(rename_all = "kebab-case")]
213pub enum ReportResultScope {
214 BoundedLive,
216 CompleteCache,
218}
219
220impl ReportResultScope {
221 #[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 {
239 use super::{ReportDataSource, ReportResultScope};
240
241 #[cfg(any(feature = "cloud-engine-host", feature = "dashboard-host"))]
242 #[test]
243 fn source_provenance_comparison_preserves_field_order() {
244 let expected = ("endpoint", "time", "collector");
245 assert_eq!(super::source_provenance_mismatch(expected, expected), None);
246 for (actual, field) in [
247 (("other", "other", "other"), "source_endpoint"),
248 (("endpoint", "other", "other"), "fetched_at"),
249 (("endpoint", "time", "other"), "fetched_by"),
250 ] {
251 let reason = super::source_provenance_mismatch(expected, actual).unwrap();
252 assert!(reason.starts_with(&format!("{field} is ")));
253 }
254 }
255
256 #[test]
257 fn report_origins_round_trip_and_require_a_cache_path() {
258 for provenance in [
259 super::ReportProvenance::Live,
260 super::ReportProvenance::Cache {
261 cache_path: "/cache/snapshot.json".to_string(),
262 },
263 ] {
264 let value = serde_json::to_value(&provenance).unwrap();
265 assert_eq!(
266 serde_json::from_value::<super::ReportProvenance>(value).unwrap(),
267 provenance
268 );
269 }
270 assert!(
271 serde_json::from_value::<super::ReportProvenance>(serde_json::json!({
272 "data_source": "cache"
273 }))
274 .is_err()
275 );
276 }
277
278 #[test]
279 fn report_provenance_labels_round_trip() {
280 for (source, label) in [
281 (ReportDataSource::Live, "live"),
282 (ReportDataSource::Cache, "cache"),
283 ] {
284 assert_eq!(source.as_str(), label);
285 assert_eq!(source.to_string(), label);
286 assert_eq!(
287 serde_json::to_string(&source).expect("serialize report data source"),
288 format!("\"{label}\"")
289 );
290 assert_eq!(
291 serde_json::from_str::<ReportDataSource>(&format!("\"{label}\""))
292 .expect("deserialize report data source"),
293 source
294 );
295 }
296 for (scope, label) in [
297 (ReportResultScope::BoundedLive, "bounded-live"),
298 (ReportResultScope::CompleteCache, "complete-cache"),
299 ] {
300 assert_eq!(scope.as_str(), label);
301 assert_eq!(scope.to_string(), label);
302 assert_eq!(
303 serde_json::to_string(&scope).expect("serialize report result scope"),
304 format!("\"{label}\"")
305 );
306 assert_eq!(
307 serde_json::from_str::<ReportResultScope>(&format!("\"{label}\""))
308 .expect("deserialize report result scope"),
309 scope
310 );
311 }
312 assert!(serde_json::from_str::<ReportDataSource>("\"api\"").is_err());
313 assert!(serde_json::from_str::<ReportResultScope>("\"partial\"").is_err());
314 }
315}