1use serde::{Deserialize, Serialize};
4use std::fmt;
5#[cfg(any(feature = "nns-host", feature = "sns-host"))]
6use std::path::Path;
7
8#[cfg(any(feature = "cloud-engine-host", feature = "dashboard-host"))]
10pub(crate) fn source_provenance_mismatch(
11 expected: (&str, &str, &str),
12 actual: (&str, &str, &str),
13) -> Option<String> {
14 [
15 ("source_endpoint", expected.0, actual.0),
16 ("fetched_at", expected.1, actual.1),
17 ("fetched_by", expected.2, actual.2),
18 ]
19 .into_iter()
20 .find_map(|(field, expected, actual)| {
21 (actual != expected)
22 .then(|| format!("{field} is {actual:?}, expected requested value {expected:?}"))
23 })
24}
25
26pub type SourceFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
33
34pub(crate) struct ReportProvenance {
41 pub(crate) data_source: ReportDataSource,
42 pub(crate) cache_path: Option<String>,
43 pub(crate) cache_complete: Option<bool>,
44}
45
46impl ReportProvenance {
47 pub(crate) const fn live() -> Self {
49 Self {
50 data_source: ReportDataSource::Live,
51 cache_path: None,
52 cache_complete: None,
53 }
54 }
55
56 #[cfg(any(feature = "nns-host", feature = "sns-host"))]
58 pub(crate) fn cache(cache_path: &Path) -> Self {
59 Self {
60 data_source: ReportDataSource::Cache,
61 cache_path: Some(cache_path.display().to_string()),
62 cache_complete: Some(true),
63 }
64 }
65}
66
67#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
74#[serde(rename_all = "snake_case")]
75pub enum ReportDataSource {
76 Live,
78 Cache,
80}
81
82impl ReportDataSource {
83 #[must_use]
85 pub const fn as_str(self) -> &'static str {
86 match self {
87 Self::Live => "live",
88 Self::Cache => "cache",
89 }
90 }
91}
92
93impl fmt::Display for ReportDataSource {
94 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95 formatter.write_str(self.as_str())
96 }
97}
98
99#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
106#[serde(rename_all = "kebab-case")]
107pub enum ReportResultScope {
108 BoundedLive,
110 CompleteCache,
112}
113
114impl ReportResultScope {
115 #[must_use]
117 pub const fn as_str(self) -> &'static str {
118 match self {
119 Self::BoundedLive => "bounded-live",
120 Self::CompleteCache => "complete-cache",
121 }
122 }
123}
124
125impl fmt::Display for ReportResultScope {
126 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127 formatter.write_str(self.as_str())
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::{ReportDataSource, ReportResultScope};
134
135 #[cfg(any(feature = "cloud-engine-host", feature = "dashboard-host"))]
136 #[test]
137 fn source_provenance_comparison_preserves_field_order() {
138 let expected = ("endpoint", "time", "collector");
139 assert_eq!(super::source_provenance_mismatch(expected, expected), None);
140 for (actual, field) in [
141 (("other", "other", "other"), "source_endpoint"),
142 (("endpoint", "other", "other"), "fetched_at"),
143 (("endpoint", "time", "other"), "fetched_by"),
144 ] {
145 let reason = super::source_provenance_mismatch(expected, actual).unwrap();
146 assert!(reason.starts_with(&format!("{field} is ")));
147 }
148 }
149
150 #[test]
151 fn report_provenance_labels_round_trip() {
152 for (source, label) in [
153 (ReportDataSource::Live, "live"),
154 (ReportDataSource::Cache, "cache"),
155 ] {
156 assert_eq!(source.as_str(), label);
157 assert_eq!(source.to_string(), label);
158 assert_eq!(
159 serde_json::to_string(&source).expect("serialize report data source"),
160 format!("\"{label}\"")
161 );
162 assert_eq!(
163 serde_json::from_str::<ReportDataSource>(&format!("\"{label}\""))
164 .expect("deserialize report data source"),
165 source
166 );
167 }
168 for (scope, label) in [
169 (ReportResultScope::BoundedLive, "bounded-live"),
170 (ReportResultScope::CompleteCache, "complete-cache"),
171 ] {
172 assert_eq!(scope.as_str(), label);
173 assert_eq!(scope.to_string(), label);
174 assert_eq!(
175 serde_json::to_string(&scope).expect("serialize report result scope"),
176 format!("\"{label}\"")
177 );
178 assert_eq!(
179 serde_json::from_str::<ReportResultScope>(&format!("\"{label}\""))
180 .expect("deserialize report result scope"),
181 scope
182 );
183 }
184 assert!(serde_json::from_str::<ReportDataSource>("\"api\"").is_err());
185 assert!(serde_json::from_str::<ReportResultScope>("\"partial\"").is_err());
186 }
187}