1use serde::{Deserialize, Serialize};
4use std::fmt;
5
6#[cfg(any(feature = "cloud-engine-host", feature = "dashboard-host"))]
8pub(crate) fn source_provenance_mismatch(
9 expected: (&str, &str, &str),
10 actual: (&str, &str, &str),
11) -> Option<String> {
12 [
13 ("source_endpoint", expected.0, actual.0),
14 ("fetched_at", expected.1, actual.1),
15 ("fetched_by", expected.2, actual.2),
16 ]
17 .into_iter()
18 .find_map(|(field, expected, actual)| {
19 (actual != expected)
20 .then(|| format!("{field} is {actual:?}, expected requested value {expected:?}"))
21 })
22}
23
24pub type SourceFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
31
32#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
39#[serde(rename_all = "snake_case")]
40pub enum ReportDataSource {
41 Live,
43 Cache,
45}
46
47impl ReportDataSource {
48 #[must_use]
50 pub const fn as_str(self) -> &'static str {
51 match self {
52 Self::Live => "live",
53 Self::Cache => "cache",
54 }
55 }
56}
57
58impl fmt::Display for ReportDataSource {
59 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60 formatter.write_str(self.as_str())
61 }
62}
63
64#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
71#[serde(rename_all = "kebab-case")]
72pub enum ReportResultScope {
73 BoundedLive,
75 CompleteCache,
77}
78
79impl ReportResultScope {
80 #[must_use]
82 pub const fn as_str(self) -> &'static str {
83 match self {
84 Self::BoundedLive => "bounded-live",
85 Self::CompleteCache => "complete-cache",
86 }
87 }
88}
89
90impl fmt::Display for ReportResultScope {
91 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
92 formatter.write_str(self.as_str())
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::{ReportDataSource, ReportResultScope};
99
100 #[cfg(any(feature = "cloud-engine-host", feature = "dashboard-host"))]
101 #[test]
102 fn source_provenance_comparison_preserves_field_order() {
103 let expected = ("endpoint", "time", "collector");
104 assert_eq!(super::source_provenance_mismatch(expected, expected), None);
105 for (actual, field) in [
106 (("other", "other", "other"), "source_endpoint"),
107 (("endpoint", "other", "other"), "fetched_at"),
108 (("endpoint", "time", "other"), "fetched_by"),
109 ] {
110 let reason = super::source_provenance_mismatch(expected, actual).unwrap();
111 assert!(reason.starts_with(&format!("{field} is ")));
112 }
113 }
114
115 #[test]
116 fn report_provenance_labels_round_trip() {
117 for (source, label) in [
118 (ReportDataSource::Live, "live"),
119 (ReportDataSource::Cache, "cache"),
120 ] {
121 assert_eq!(source.as_str(), label);
122 assert_eq!(source.to_string(), label);
123 assert_eq!(
124 serde_json::to_string(&source).expect("serialize report data source"),
125 format!("\"{label}\"")
126 );
127 assert_eq!(
128 serde_json::from_str::<ReportDataSource>(&format!("\"{label}\""))
129 .expect("deserialize report data source"),
130 source
131 );
132 }
133 for (scope, label) in [
134 (ReportResultScope::BoundedLive, "bounded-live"),
135 (ReportResultScope::CompleteCache, "complete-cache"),
136 ] {
137 assert_eq!(scope.as_str(), label);
138 assert_eq!(scope.to_string(), label);
139 assert_eq!(
140 serde_json::to_string(&scope).expect("serialize report result scope"),
141 format!("\"{label}\"")
142 );
143 assert_eq!(
144 serde_json::from_str::<ReportResultScope>(&format!("\"{label}\""))
145 .expect("deserialize report result scope"),
146 scope
147 );
148 }
149 assert!(serde_json::from_str::<ReportDataSource>("\"api\"").is_err());
150 assert!(serde_json::from_str::<ReportResultScope>("\"partial\"").is_err());
151 }
152}