datadog_agent_config/classifier/
classifier.rs1use std::collections::HashMap;
2
3use serde_json::Value;
4
5use super::{ClassifierEntry, PipelineAffinity, SupportLevel, CLASSIFIER_ENTRIES};
6
7pub struct Classification {
9 pub support_level: SupportLevel,
11 pub is_default: bool,
13 pub pipeline_affinity: PipelineAffinity,
15}
16
17pub struct ConfigClassifier {
23 lookup: HashMap<&'static str, &'static ClassifierEntry>,
24}
25
26impl ConfigClassifier {
27 pub fn new() -> Self {
29 let mut lookup = HashMap::new();
30
31 for entry in CLASSIFIER_ENTRIES {
32 lookup.insert(entry.yaml_path, entry);
33 for alias in entry.aliases {
34 lookup.insert(alias, entry);
35 }
36 }
37
38 Self { lookup }
39 }
40
41 pub fn classify(&self, key: &str, value: &Value) -> Option<Classification> {
45 let entry = self.lookup.get(key)?;
46 Some(Classification {
47 support_level: entry.support_level,
48 is_default: is_default_value(entry.default, value),
49 pipeline_affinity: entry.pipeline_affinity,
50 })
51 }
52}
53
54fn is_default_value(default: Option<&str>, value: &Value) -> bool {
55 match default {
56 Some(default_str) => match serde_json::from_str::<Value>(default_str) {
57 Ok(default_value) => *value == default_value,
58 Err(_) => false,
59 },
60 None => match value {
61 Value::Null => true,
62 Value::String(s) => s.is_empty(),
63 _ => false,
64 },
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use crate::classifier::Severity;
72
73 fn classifier() -> ConfigClassifier {
74 ConfigClassifier::new()
75 }
76
77 #[test]
78 fn full_support_keys_not_in_classifier() {
79 let c = classifier();
82 assert!(c.classify("dogstatsd_port", &Value::Number(9999.into())).is_none());
83 assert!(c.classify("log_payloads", &Value::Bool(true)).is_none());
84 }
85
86 #[test]
87 fn partial_non_default() {
88 let c = classifier();
89 let result = c
90 .classify("min_tls_version", &Value::String("non_default_value".into()))
91 .unwrap();
92 assert_eq!(result.support_level, SupportLevel::Partial);
93 }
94
95 #[test]
96 fn incompatible_non_default() {
97 let c = classifier();
98 let result = c.classify("tls_handshake_timeout", &Value::Number(999.into())).unwrap();
99 assert!(matches!(result.support_level, SupportLevel::Incompatible(_)));
100 assert!(!result.is_default);
101 }
102
103 #[test]
104 fn incompatible_default() {
105 let c = classifier();
106 let result = c.classify("config_id", &Value::String("".into())).unwrap();
108 assert!(matches!(result.support_level, SupportLevel::Incompatible(_)));
109 assert!(result.is_default);
110 }
111
112 #[test]
113 fn not_in_registry_returns_none() {
114 let c = classifier();
115 assert!(c.classify("totally_made_up_key", &Value::Bool(true)).is_none());
116 assert!(c.classify("GUI_host", &Value::String("localhost".into())).is_none());
117 }
118
119 #[test]
120 fn duration_default_null_is_not_default() {
121 let c = classifier();
122 let result = c.classify("tls_handshake_timeout", &Value::Null).unwrap();
124 assert!(!result.is_default);
125 }
126
127 #[test]
128 fn duration_default_empty_string_is_not_default() {
129 let c = classifier();
130 let result = c.classify("tls_handshake_timeout", &Value::String("".into())).unwrap();
132 assert!(!result.is_default);
133 }
134
135 #[test]
136 fn none_default_non_empty_is_not_default() {
137 let c = classifier();
138 let result = c
139 .classify("tls_handshake_timeout", &Value::String("something".into()))
140 .unwrap();
141 assert!(!result.is_default);
142 }
143
144 #[test]
145 fn incompatible_severity_levels() {
146 let c = classifier();
147 let result = c.classify("tls_handshake_timeout", &Value::Number(30.into())).unwrap();
148 assert!(matches!(
149 result.support_level,
150 SupportLevel::Incompatible(Severity::Medium)
151 ));
152 }
153}