use serde::{Deserialize, Serialize};
pub const DISCOVERY_ASPECTS: [&str; 7] = [
"host",
"network",
"process",
"endpoint",
"container",
"k8s",
"package",
];
pub const DISCOVERY_PLATFORMS: [&str; 2] = ["macos", "linux"];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Probe")]
#[serde(deny_unknown_fields)]
pub struct DiscoveryAspectPolicy {
pub aspect: String,
pub default_interval_seconds: i64,
pub min_interval_seconds: i64,
pub max_interval_seconds: i64,
pub baseline: bool,
pub enabled_by_default: bool,
#[serde(default)]
pub platforms: Vec<String>,
#[serde(default)]
pub yields: String,
}
impl DiscoveryAspectPolicy {
pub fn supports(&self, platform: &str) -> bool {
self.platforms.iter().any(|item| item == platform)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Probe")]
#[serde(deny_unknown_fields)]
pub struct DiscoveryAspectPolicySet {
pub policy_version: i64,
pub published_at: String,
#[serde(default)]
pub policies: Vec<DiscoveryAspectPolicy>,
}
impl DiscoveryAspectPolicySet {
pub fn new(
policy_version: i64,
published_at: String,
policies: Vec<DiscoveryAspectPolicy>,
) -> Self {
Self {
policy_version,
published_at,
policies,
}
}
pub fn for_aspect(&self, aspect: &str) -> Option<&DiscoveryAspectPolicy> {
self.policies.iter().find(|policy| policy.aspect == aspect)
}
pub fn interval_seconds_for(&self, aspect: &str) -> Option<i64> {
self.for_aspect(aspect)
.map(|policy| policy.default_interval_seconds)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn policy(aspect: &str, interval: i64) -> DiscoveryAspectPolicy {
DiscoveryAspectPolicy {
aspect: aspect.to_string(),
default_interval_seconds: interval,
min_interval_seconds: 60,
max_interval_seconds: 3600,
baseline: false,
enabled_by_default: true,
platforms: vec!["macos".to_string()],
yields: String::new(),
}
}
#[test]
fn looks_up_a_policy_by_aspect_name() {
let set = DiscoveryAspectPolicySet::new(
3,
"2026-09-22T00:00:00Z".to_string(),
vec![policy("host", 900), policy("process", 300)],
);
assert_eq!(set.interval_seconds_for("process"), Some(300));
assert_eq!(set.for_aspect("host").map(|p| p.baseline), Some(false));
assert_eq!(set.interval_seconds_for("package"), None);
}
#[test]
fn platform_membership_is_exact() {
let mut policy = policy("endpoint", 300);
policy.platforms = vec!["linux".to_string()];
assert!(policy.supports("linux"));
assert!(!policy.supports("macos"));
assert!(!policy.supports("linux2"));
}
#[test]
fn round_trips_with_serde() {
let set = DiscoveryAspectPolicySet::new(
1,
"2026-09-22T00:00:00Z".to_string(),
vec![policy("host", 900)],
);
let json = serde_json::to_string(&set).expect("serialize");
let decoded: DiscoveryAspectPolicySet = serde_json::from_str(&json).expect("deserialize");
assert_eq!(decoded, set);
}
#[test]
fn rejects_unknown_fields() {
let json = r#"{"policy_version":1,"published_at":"x","policies":[],"extra":true}"#;
assert!(serde_json::from_str::<DiscoveryAspectPolicySet>(json).is_err());
}
#[test]
fn an_empty_table_has_no_policy_for_any_aspect() {
let set = DiscoveryAspectPolicySet::new(0, "2026-09-27T00:00:00Z".to_string(), Vec::new());
for aspect in DISCOVERY_ASPECTS {
assert_eq!(set.interval_seconds_for(aspect), None, "{aspect}");
assert!(set.for_aspect(aspect).is_none(), "{aspect}");
}
}
#[test]
fn omitted_optional_fields_decode_to_stable_defaults() {
let set: DiscoveryAspectPolicySet =
serde_json::from_str(r#"{"policy_version":1,"published_at":"t"}"#).expect("decode");
assert!(set.policies.is_empty());
let policy: DiscoveryAspectPolicy =
serde_json::from_str(
r#"{"aspect":"host","default_interval_seconds":900,"min_interval_seconds":60,"max_interval_seconds":3600,"baseline":true,"enabled_by_default":true}"#,
)
.expect("decode");
assert!(policy.platforms.is_empty());
assert_eq!(policy.yields, "");
}
}