wist_contracts/
discovery_policy.rs1use serde::{Deserialize, Serialize};
23
24pub const DISCOVERY_ASPECTS: [&str; 7] = [
29 "host",
30 "network",
31 "process",
32 "endpoint",
33 "container",
34 "k8s",
35 "package",
36];
37
38pub const DISCOVERY_PLATFORMS: [&str; 2] = ["macos", "linux"];
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
43#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Probe")]
44#[serde(deny_unknown_fields)]
45pub struct DiscoveryAspectPolicy {
46 pub aspect: String,
48 pub default_interval_seconds: i64,
50 pub min_interval_seconds: i64,
52 pub max_interval_seconds: i64,
54 pub baseline: bool,
56 pub enabled_by_default: bool,
58 #[serde(default)]
60 pub platforms: Vec<String>,
61 #[serde(default)]
63 pub yields: String,
64}
65
66impl DiscoveryAspectPolicy {
67 pub fn supports(&self, platform: &str) -> bool {
69 self.platforms.iter().any(|item| item == platform)
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
78#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Probe")]
79#[serde(deny_unknown_fields)]
80pub struct DiscoveryAspectPolicySet {
81 pub policy_version: i64,
82 pub published_at: String,
83 #[serde(default)]
84 pub policies: Vec<DiscoveryAspectPolicy>,
85}
86
87impl DiscoveryAspectPolicySet {
88 pub fn new(
89 policy_version: i64,
90 published_at: String,
91 policies: Vec<DiscoveryAspectPolicy>,
92 ) -> Self {
93 Self {
94 policy_version,
95 published_at,
96 policies,
97 }
98 }
99
100 pub fn for_aspect(&self, aspect: &str) -> Option<&DiscoveryAspectPolicy> {
102 self.policies.iter().find(|policy| policy.aspect == aspect)
103 }
104
105 pub fn interval_seconds_for(&self, aspect: &str) -> Option<i64> {
107 self.for_aspect(aspect)
108 .map(|policy| policy.default_interval_seconds)
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 fn policy(aspect: &str, interval: i64) -> DiscoveryAspectPolicy {
117 DiscoveryAspectPolicy {
118 aspect: aspect.to_string(),
119 default_interval_seconds: interval,
120 min_interval_seconds: 60,
121 max_interval_seconds: 3600,
122 baseline: false,
123 enabled_by_default: true,
124 platforms: vec!["macos".to_string()],
125 yields: String::new(),
126 }
127 }
128
129 #[test]
130 fn looks_up_a_policy_by_aspect_name() {
131 let set = DiscoveryAspectPolicySet::new(
132 3,
133 "2026-09-22T00:00:00Z".to_string(),
134 vec![policy("host", 900), policy("process", 300)],
135 );
136
137 assert_eq!(set.interval_seconds_for("process"), Some(300));
138 assert_eq!(set.for_aspect("host").map(|p| p.baseline), Some(false));
139 assert_eq!(set.interval_seconds_for("package"), None);
141 }
142
143 #[test]
144 fn platform_membership_is_exact() {
145 let mut policy = policy("endpoint", 300);
146 policy.platforms = vec!["linux".to_string()];
147
148 assert!(policy.supports("linux"));
149 assert!(!policy.supports("macos"));
150 assert!(!policy.supports("linux2"));
152 }
153
154 #[test]
155 fn round_trips_with_serde() {
156 let set = DiscoveryAspectPolicySet::new(
157 1,
158 "2026-09-22T00:00:00Z".to_string(),
159 vec![policy("host", 900)],
160 );
161 let json = serde_json::to_string(&set).expect("serialize");
162 let decoded: DiscoveryAspectPolicySet = serde_json::from_str(&json).expect("deserialize");
163 assert_eq!(decoded, set);
164 }
165
166 #[test]
167 fn rejects_unknown_fields() {
168 let json = r#"{"policy_version":1,"published_at":"x","policies":[],"extra":true}"#;
171 assert!(serde_json::from_str::<DiscoveryAspectPolicySet>(json).is_err());
172 }
173
174 #[test]
175 fn an_empty_table_has_no_policy_for_any_aspect() {
176 let set = DiscoveryAspectPolicySet::new(0, "2026-09-27T00:00:00Z".to_string(), Vec::new());
178 for aspect in DISCOVERY_ASPECTS {
179 assert_eq!(set.interval_seconds_for(aspect), None, "{aspect}");
180 assert!(set.for_aspect(aspect).is_none(), "{aspect}");
181 }
182 }
183
184 #[test]
185 fn omitted_optional_fields_decode_to_stable_defaults() {
186 let set: DiscoveryAspectPolicySet =
188 serde_json::from_str(r#"{"policy_version":1,"published_at":"t"}"#).expect("decode");
189 assert!(set.policies.is_empty());
190
191 let policy: DiscoveryAspectPolicy =
192 serde_json::from_str(
193 r#"{"aspect":"host","default_interval_seconds":900,"min_interval_seconds":60,"max_interval_seconds":3600,"baseline":true,"enabled_by_default":true}"#,
194 )
195 .expect("decode");
196 assert!(policy.platforms.is_empty());
197 assert_eq!(policy.yields, "");
198 }
199}