hopr_ct_full_network/
config.rs1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3use validator::{Validate, ValidationError, ValidationErrors};
4
5#[derive(Debug, Clone, Copy, PartialEq, smart_default::SmartDefault)]
7#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(deny_unknown_fields))]
8pub struct ProberConfig {
9 #[cfg_attr(
11 feature = "serde",
12 serde(default = "default_probing_interval", with = "humantime_serde")
13 )]
14 #[default(default_probing_interval())]
15 pub interval: std::time::Duration,
16
17 #[cfg_attr(feature = "serde", serde(default = "default_staleness_weight"))]
23 #[default(default_staleness_weight())]
24 pub staleness_weight: f64,
25
26 #[cfg_attr(feature = "serde", serde(default = "default_quality_weight"))]
32 #[default(default_quality_weight())]
33 pub quality_weight: f64,
34
35 #[cfg_attr(feature = "serde", serde(default = "default_base_priority"))]
42 #[default(default_base_priority())]
43 pub base_priority: f64,
44
45 #[cfg_attr(feature = "serde", serde(default = "default_shuffle_ttl", with = "humantime_serde"))]
50 #[default(default_shuffle_ttl())]
51 pub shuffle_ttl: std::time::Duration,
52
53 #[cfg_attr(feature = "serde", serde(default = "just_true"))]
60 #[default(just_true())]
61 pub probe_connected_only: bool,
62}
63
64impl Validate for ProberConfig {
65 fn validate(&self) -> Result<(), ValidationErrors> {
66 let mut errors = ValidationErrors::new();
67
68 if !(0.0..=1.0).contains(&self.staleness_weight) {
69 errors.add(
70 "staleness_weight",
71 ValidationError::new("staleness_weight must be between 0.0 and 1.0"),
72 );
73 }
74 if !(0.0..=1.0).contains(&self.quality_weight) {
75 errors.add(
76 "quality_weight",
77 ValidationError::new("quality_weight must be between 0.0 and 1.0"),
78 );
79 }
80 if !(0.0..=1.0).contains(&self.base_priority) {
81 errors.add(
82 "base_priority",
83 ValidationError::new("base_priority must be between 0.0 and 1.0"),
84 );
85 }
86
87 if self.staleness_weight + self.quality_weight + self.base_priority <= 0.0 {
88 errors.add(
89 "weights",
90 ValidationError::new("at least one priority weight must be positive"),
91 );
92 }
93
94 if errors.is_empty() { Ok(()) } else { Err(errors) }
95 }
96}
97
98#[inline]
99const fn default_staleness_weight() -> f64 {
100 0.4
101}
102
103#[inline]
104const fn default_quality_weight() -> f64 {
105 0.3
106}
107
108#[inline]
109const fn default_base_priority() -> f64 {
110 0.3
111}
112
113#[inline]
114const fn default_shuffle_ttl() -> std::time::Duration {
115 std::time::Duration::from_secs(default_probing_interval().as_secs() * 2)
116}
117
118#[inline]
119const fn default_probing_interval() -> std::time::Duration {
120 std::time::Duration::from_secs(30)
121}
122
123#[inline]
124const fn just_true() -> bool {
125 true
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn default_config_is_valid() {
134 let cfg = ProberConfig::default();
135 assert!(cfg.validate().is_ok());
136 assert!(cfg.probe_connected_only, "probe_connected_only should default to true");
137 }
138
139 #[test]
140 fn all_zero_weights_are_invalid() {
141 let cfg = ProberConfig {
142 staleness_weight: 0.0,
143 quality_weight: 0.0,
144 base_priority: 0.0,
145 ..Default::default()
146 };
147 let err = cfg.validate().unwrap_err();
148 assert!(err.field_errors().contains_key("weights"));
149 }
150
151 #[test]
152 fn zero_staleness_weight_alone_is_valid() {
153 let cfg = ProberConfig {
154 staleness_weight: 0.0,
155 ..Default::default()
156 };
157 assert!(cfg.validate().is_ok());
158 }
159
160 #[test]
161 fn zero_quality_weight_alone_is_valid() {
162 let cfg = ProberConfig {
163 quality_weight: 0.0,
164 ..Default::default()
165 };
166 assert!(cfg.validate().is_ok());
167 }
168
169 #[test]
170 fn zero_base_priority_alone_is_valid() {
171 let cfg = ProberConfig {
172 base_priority: 0.0,
173 ..Default::default()
174 };
175 assert!(cfg.validate().is_ok());
176 }
177
178 #[test]
179 fn staleness_weight_above_one_is_invalid() {
180 let cfg = ProberConfig {
181 staleness_weight: 1.1,
182 ..Default::default()
183 };
184 let err = cfg.validate().unwrap_err();
185 assert!(err.field_errors().contains_key("staleness_weight"));
186 }
187
188 #[test]
189 fn quality_weight_above_one_is_invalid() {
190 let cfg = ProberConfig {
191 quality_weight: 1.1,
192 ..Default::default()
193 };
194 let err = cfg.validate().unwrap_err();
195 assert!(err.field_errors().contains_key("quality_weight"));
196 }
197
198 #[test]
199 fn base_priority_above_one_is_invalid() {
200 let cfg = ProberConfig {
201 base_priority: 1.1,
202 ..Default::default()
203 };
204 let err = cfg.validate().unwrap_err();
205 assert!(err.field_errors().contains_key("base_priority"));
206 }
207
208 #[test]
209 fn negative_staleness_weight_is_invalid() {
210 let cfg = ProberConfig {
211 staleness_weight: -0.1,
212 quality_weight: 0.5,
213 base_priority: 0.5,
214 ..Default::default()
215 };
216 let err = cfg.validate().unwrap_err();
217 assert!(err.field_errors().contains_key("staleness_weight"));
218 }
219
220 #[test]
221 fn negative_quality_weight_is_invalid() {
222 let cfg = ProberConfig {
223 staleness_weight: 0.5,
224 quality_weight: -0.1,
225 base_priority: 0.5,
226 ..Default::default()
227 };
228 let err = cfg.validate().unwrap_err();
229 assert!(err.field_errors().contains_key("quality_weight"));
230 }
231
232 #[test]
233 fn negative_base_priority_is_invalid() {
234 let cfg = ProberConfig {
235 staleness_weight: 0.5,
236 quality_weight: 0.5,
237 base_priority: -0.1,
238 ..Default::default()
239 };
240 let err = cfg.validate().unwrap_err();
241 assert!(err.field_errors().contains_key("base_priority"));
242 }
243}