saluki_components/config_registry/
classifier.rs1use std::collections::HashMap;
2
3use serde_json::Value;
4
5use super::{PipelineAffinity, SchemaEntry, SupportLevel, ALL_ANNOTATIONS};
6
7pub struct Classification {
9 pub support_level: SupportLevel,
11 pub is_default: bool,
13 pub pipeline_affinity: PipelineAffinity,
15}
16
17pub struct ConfigClassifier {
22 lookup: HashMap<&'static str, (&'static SchemaEntry, SupportLevel, PipelineAffinity)>,
23}
24
25impl ConfigClassifier {
26 pub fn new() -> Self {
28 let mut lookup = HashMap::new();
29
30 for &annotation in ALL_ANNOTATIONS.iter() {
31 let entry = (
32 annotation.schema,
33 annotation.support_level,
34 annotation.pipeline_affinity,
35 );
36 lookup.insert(annotation.yaml_path(), entry);
37 for alias in annotation.additional_yaml_paths {
38 lookup.insert(alias, entry);
39 }
40 }
41
42 Self { lookup }
43 }
44
45 pub fn classify(&self, key: &str, value: &Value) -> Option<Classification> {
49 let &(schema, support_level, pipeline_affinity) = self.lookup.get(key)?;
50 Some(Classification {
51 support_level,
52 is_default: is_default_value(schema, value),
53 pipeline_affinity,
54 })
55 }
56}
57
58fn is_default_value(schema: &SchemaEntry, value: &Value) -> bool {
61 match schema.default {
62 Some(default_str) => match serde_json::from_str::<Value>(default_str) {
63 Ok(default_value) => *value == default_value,
64 Err(_) => false,
65 },
66 None => match value {
67 Value::Null => true,
68 Value::String(s) => s.is_empty(),
70 _ => false,
71 },
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use crate::config_registry::datadog::unsupported;
79 use crate::config_registry::SUPPORTED_ANNOTATIONS;
80
81 fn classifier() -> ConfigClassifier {
82 ConfigClassifier::new()
83 }
84
85 #[test]
86 fn full_non_default() {
87 let c = classifier();
88 let result = c.classify("dogstatsd_port", &Value::Number(9999.into())).unwrap();
89 assert_eq!(result.support_level, SupportLevel::Full);
90 assert!(!result.is_default);
91 }
92
93 #[test]
94 fn full_default() {
95 let c = classifier();
96 let result = c.classify("dogstatsd_port", &Value::Number(8125.into())).unwrap();
97 assert_eq!(result.support_level, SupportLevel::Full);
98 assert!(result.is_default);
99 }
100
101 #[test]
102 fn partial_non_default() {
103 let c = classifier();
104 let partial = SUPPORTED_ANNOTATIONS
105 .iter()
106 .find(|a| a.support_level == SupportLevel::Partial)
107 .expect("need at least one Partial annotation for this test");
108 let result = c
109 .classify(partial.yaml_path(), &Value::String("non_default_value".into()))
110 .unwrap();
111 assert_eq!(result.support_level, SupportLevel::Partial);
112 }
113
114 #[test]
115 fn incompatible_non_default() {
119 let c = classifier();
120 let key = unsupported::TLS_HANDSHAKE_TIMEOUT.yaml_path();
121 let result = c.classify(key, &Value::Number(999.into())).unwrap();
122 assert!(matches!(result.support_level, SupportLevel::Incompatible(_)));
123 assert!(!result.is_default);
124 }
125
126 #[test]
127 fn incompatible_default() {
128 let c = classifier();
129 let ann = &unsupported::TLS_HANDSHAKE_TIMEOUT;
130 let result = c.classify(ann.yaml_path(), &Value::String("".into())).unwrap();
131 assert!(matches!(result.support_level, SupportLevel::Incompatible(_)));
132 assert!(result.is_default);
133 }
134
135 #[test]
136 fn not_in_registry_returns_none() {
137 let c = classifier();
138 assert!(c.classify("totally_made_up_key", &Value::Bool(true)).is_none());
139 assert!(c.classify("GUI_host", &Value::String("localhost".into())).is_none());
140 }
141
142 #[test]
143 fn alias_resolves_to_annotation() {
144 let c = classifier();
145 let result = c
146 .classify("dogstatsd_expiry_seconds", &Value::Number(999.into()))
147 .unwrap();
148 assert_eq!(result.support_level, SupportLevel::Full);
149 assert!(!result.is_default);
150 }
151
152 #[test]
153 fn alias_default_matches_canonical() {
154 let c = classifier();
155 let canonical = c.classify("counter_expiry_seconds", &Value::Null).unwrap();
156 let alias = c.classify("dogstatsd_expiry_seconds", &Value::Null).unwrap();
157 assert!(canonical.is_default);
158 assert!(alias.is_default);
159 }
160
161 #[test]
162 fn log_payloads_is_supported() {
163 let c = classifier();
164 let result = c.classify("log_payloads", &Value::Bool(true)).unwrap();
165 assert_eq!(result.support_level, SupportLevel::Full);
166 assert!(!result.is_default);
167 }
168
169 #[test]
170 fn none_default_null_is_default() {
171 let c = classifier();
172 let ann = ALL_ANNOTATIONS
173 .iter()
174 .find(|a| a.schema.default.is_none())
175 .expect("need an annotation with default: None");
176 let result = c.classify(ann.yaml_path(), &Value::Null).unwrap();
177 assert!(result.is_default);
178 }
179
180 #[test]
181 fn none_default_empty_string_is_default() {
182 let c = classifier();
183 let ann = ALL_ANNOTATIONS
184 .iter()
185 .find(|a| a.schema.default.is_none())
186 .expect("need an annotation with default: None");
187 let result = c.classify(ann.yaml_path(), &Value::String("".into())).unwrap();
188 assert!(result.is_default);
189 }
190
191 #[test]
192 fn none_default_non_empty_is_not_default() {
193 let c = classifier();
194 let ann = ALL_ANNOTATIONS
195 .iter()
196 .find(|a| a.schema.default.is_none())
197 .expect("need an annotation with default: None");
198 let result = c.classify(ann.yaml_path(), &Value::String("something".into())).unwrap();
199 assert!(!result.is_default);
200 }
201}