1#![allow(rustdoc::broken_intra_doc_links)]
2
3use serde::{Deserialize, Serialize};
4
5pub const DEFAULT_TUNING_MRFT_DELAY_SECS: u32 = 0;
7
8pub const DEFAULT_TUNING_POLL_INTERVAL_MS: u64 = 800;
10
11pub const DEFAULT_TUNING_TIMEOUT_SECS: u64 = 3_600;
13
14pub const DEFAULT_TUNING_OP_TIMEOUT_SECS: u64 = 30;
16
17pub const DEFAULT_TUNING_RESTORE_TIMEOUT_SECS: u64 = 30;
19
20pub const MAX_TUNING_MRFT_DELAY_SECS: u32 = 3_600;
22
23pub const MIN_OPC_RESTORE_TIMEOUT_SECS: u64 = 4;
25
26#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
32#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
33pub struct TuningConfig {
34 #[cfg_attr(feature = "schemars", schemars(range(max = 3_600)))]
35 pub mrft_delay_secs: Option<u32>,
36 #[cfg_attr(feature = "schemars", schemars(range(min = 1)))]
37 pub poll_interval_ms: Option<u64>,
38 #[cfg_attr(feature = "schemars", schemars(range(min = 1)))]
39 pub timeout_secs: Option<u64>,
40 #[cfg_attr(feature = "schemars", schemars(range(min = 1)))]
41 pub op_timeout_secs: Option<u64>,
42 #[cfg_attr(feature = "schemars", schemars(range(min = 1)))]
43 pub restore_timeout_secs: Option<u64>,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct EffectiveTuningConfig {
49 pub mrft_delay_secs: u32,
50 pub poll_interval_ms: u64,
51 pub timeout_secs: u64,
52 pub op_timeout_secs: u64,
53 pub restore_timeout_secs: u64,
54}
55
56impl Default for EffectiveTuningConfig {
57 fn default() -> Self {
58 Self {
59 mrft_delay_secs: DEFAULT_TUNING_MRFT_DELAY_SECS,
60 poll_interval_ms: DEFAULT_TUNING_POLL_INTERVAL_MS,
61 timeout_secs: DEFAULT_TUNING_TIMEOUT_SECS,
62 op_timeout_secs: DEFAULT_TUNING_OP_TIMEOUT_SECS,
63 restore_timeout_secs: DEFAULT_TUNING_RESTORE_TIMEOUT_SECS,
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum TuningConfigSource {
71 Toml,
72 BuiltInDefault,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct TuningConfigSources {
78 pub mrft_delay_secs: TuningConfigSource,
79 pub poll_interval_ms: TuningConfigSource,
80 pub timeout_secs: TuningConfigSource,
81 pub op_timeout_secs: TuningConfigSource,
82 pub restore_timeout_secs: TuningConfigSource,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum TuningConfigError {
88 MrftDelayOutOfRange { value: u32 },
89 PollIntervalTooSmall { value: u64 },
90 TimeoutTooSmall { value: u64 },
91 OpTimeoutTooSmall { value: u64 },
92 RestoreTimeoutTooSmall { value: u64, minimum: u64 },
93}
94
95impl std::fmt::Display for TuningConfigError {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 match self {
98 Self::MrftDelayOutOfRange { value } => write!(
99 f,
100 "tuning.mrft_delay_secs must be between 0 and {MAX_TUNING_MRFT_DELAY_SECS}, got {value}"
101 ),
102 Self::PollIntervalTooSmall { value } => {
103 write!(f, "tuning.poll_interval_ms must be at least 1, got {value}")
104 }
105 Self::TimeoutTooSmall { value } => {
106 write!(f, "tuning.timeout_secs must be at least 1, got {value}")
107 }
108 Self::OpTimeoutTooSmall { value } => {
109 write!(f, "tuning.op_timeout_secs must be at least 1, got {value}")
110 }
111 Self::RestoreTimeoutTooSmall { value, minimum } => write!(
112 f,
113 "tuning.restore_timeout_secs must be at least {minimum}, got {value}"
114 ),
115 }
116 }
117}
118
119impl std::error::Error for TuningConfigError {}
120
121pub fn resolve_tuning_config(config: &TuningConfig) -> EffectiveTuningConfig {
123 EffectiveTuningConfig {
124 mrft_delay_secs: config
125 .mrft_delay_secs
126 .unwrap_or(DEFAULT_TUNING_MRFT_DELAY_SECS),
127 poll_interval_ms: config
128 .poll_interval_ms
129 .unwrap_or(DEFAULT_TUNING_POLL_INTERVAL_MS),
130 timeout_secs: config.timeout_secs.unwrap_or(DEFAULT_TUNING_TIMEOUT_SECS),
131 op_timeout_secs: config
132 .op_timeout_secs
133 .unwrap_or(DEFAULT_TUNING_OP_TIMEOUT_SECS),
134 restore_timeout_secs: config
135 .restore_timeout_secs
136 .unwrap_or(DEFAULT_TUNING_RESTORE_TIMEOUT_SECS),
137 }
138}
139
140pub fn tuning_config_sources(config: &TuningConfig) -> TuningConfigSources {
142 fn source<T>(value: Option<T>) -> TuningConfigSource {
143 if value.is_some() {
144 TuningConfigSource::Toml
145 } else {
146 TuningConfigSource::BuiltInDefault
147 }
148 }
149
150 TuningConfigSources {
151 mrft_delay_secs: source(config.mrft_delay_secs),
152 poll_interval_ms: source(config.poll_interval_ms),
153 timeout_secs: source(config.timeout_secs),
154 op_timeout_secs: source(config.op_timeout_secs),
155 restore_timeout_secs: source(config.restore_timeout_secs),
156 }
157}
158
159pub fn validate_tuning_config(
164 config: &EffectiveTuningConfig,
165 require_opc_restore_minimum: bool,
166) -> Result<(), TuningConfigError> {
167 if config.mrft_delay_secs > MAX_TUNING_MRFT_DELAY_SECS {
168 return Err(TuningConfigError::MrftDelayOutOfRange {
169 value: config.mrft_delay_secs,
170 });
171 }
172 if config.poll_interval_ms == 0 {
173 return Err(TuningConfigError::PollIntervalTooSmall {
174 value: config.poll_interval_ms,
175 });
176 }
177 if config.timeout_secs == 0 {
178 return Err(TuningConfigError::TimeoutTooSmall {
179 value: config.timeout_secs,
180 });
181 }
182 if config.op_timeout_secs == 0 {
183 return Err(TuningConfigError::OpTimeoutTooSmall {
184 value: config.op_timeout_secs,
185 });
186 }
187 let restore_minimum = if require_opc_restore_minimum {
188 MIN_OPC_RESTORE_TIMEOUT_SECS
189 } else {
190 1
191 };
192 if config.restore_timeout_secs < restore_minimum {
193 return Err(TuningConfigError::RestoreTimeoutTooSmall {
194 value: config.restore_timeout_secs,
195 minimum: restore_minimum,
196 });
197 }
198 Ok(())
199}
200
201pub fn resolve_and_validate_tuning_config(
203 config: &TuningConfig,
204 require_opc_restore_minimum: bool,
205) -> Result<EffectiveTuningConfig, TuningConfigError> {
206 let effective = resolve_tuning_config(config);
207 validate_tuning_config(&effective, require_opc_restore_minimum)?;
208 Ok(effective)
209}
210
211#[cfg(test)]
212mod tests {
213 use super::super::model::parse_config_contents;
214 use super::*;
215 #[test]
216 fn missing_tuning_table_preserves_none_and_resolves_built_in_defaults() {
217 let config = parse_config_contents("bridge_host = \"gateway:7600\"\n").unwrap();
218
219 assert_eq!(config.tuning, TuningConfig::default());
220 assert_eq!(
221 resolve_and_validate_tuning_config(&config.tuning, true).unwrap(),
222 EffectiveTuningConfig::default()
223 );
224 }
225
226 #[test]
227 fn tuning_table_round_trips_all_optional_values() {
228 let raw = r"
229[tuning]
230mrft_delay_secs = 12
231poll_interval_ms = 900
232timeout_secs = 4000
233op_timeout_secs = 31
234restore_timeout_secs = 32
235";
236 let config = parse_config_contents(raw).unwrap();
237
238 assert_eq!(
239 config.tuning,
240 TuningConfig {
241 mrft_delay_secs: Some(12),
242 poll_interval_ms: Some(900),
243 timeout_secs: Some(4_000),
244 op_timeout_secs: Some(31),
245 restore_timeout_secs: Some(32),
246 }
247 );
248 let encoded = toml::to_string(&config).unwrap();
249 assert_eq!(parse_config_contents(&encoded).unwrap(), config);
250 }
251
252 #[test]
253 fn tuning_resolution_preserves_partial_raw_values_and_tracks_sources() {
254 let raw = TuningConfig {
255 poll_interval_ms: Some(250),
256 restore_timeout_secs: Some(8),
257 ..Default::default()
258 };
259
260 assert_eq!(
261 resolve_tuning_config(&raw),
262 EffectiveTuningConfig {
263 poll_interval_ms: 250,
264 restore_timeout_secs: 8,
265 ..Default::default()
266 }
267 );
268 assert_eq!(
269 tuning_config_sources(&raw),
270 TuningConfigSources {
271 mrft_delay_secs: TuningConfigSource::BuiltInDefault,
272 poll_interval_ms: TuningConfigSource::Toml,
273 timeout_secs: TuningConfigSource::BuiltInDefault,
274 op_timeout_secs: TuningConfigSource::BuiltInDefault,
275 restore_timeout_secs: TuningConfigSource::Toml,
276 }
277 );
278 }
279
280 #[test]
281 fn tuning_validation_rejects_each_invalid_general_value() {
282 let cases = [
283 (
284 EffectiveTuningConfig {
285 mrft_delay_secs: MAX_TUNING_MRFT_DELAY_SECS + 1,
286 ..Default::default()
287 },
288 TuningConfigError::MrftDelayOutOfRange {
289 value: MAX_TUNING_MRFT_DELAY_SECS + 1,
290 },
291 ),
292 (
293 EffectiveTuningConfig {
294 poll_interval_ms: 0,
295 ..Default::default()
296 },
297 TuningConfigError::PollIntervalTooSmall { value: 0 },
298 ),
299 (
300 EffectiveTuningConfig {
301 timeout_secs: 0,
302 ..Default::default()
303 },
304 TuningConfigError::TimeoutTooSmall { value: 0 },
305 ),
306 (
307 EffectiveTuningConfig {
308 op_timeout_secs: 0,
309 ..Default::default()
310 },
311 TuningConfigError::OpTimeoutTooSmall { value: 0 },
312 ),
313 (
314 EffectiveTuningConfig {
315 restore_timeout_secs: 0,
316 ..Default::default()
317 },
318 TuningConfigError::RestoreTimeoutTooSmall {
319 value: 0,
320 minimum: 1,
321 },
322 ),
323 ];
324
325 for (config, expected) in cases {
326 assert_eq!(validate_tuning_config(&config, false), Err(expected));
327 assert!(!expected.to_string().is_empty());
328 }
329 }
330
331 #[test]
332 fn opc_restore_timeout_requires_four_seconds_but_general_validation_allows_one() {
333 let config = EffectiveTuningConfig {
334 restore_timeout_secs: MIN_OPC_RESTORE_TIMEOUT_SECS - 1,
335 ..Default::default()
336 };
337
338 assert_eq!(validate_tuning_config(&config, false), Ok(()));
339 assert_eq!(
340 validate_tuning_config(&config, true),
341 Err(TuningConfigError::RestoreTimeoutTooSmall {
342 value: MIN_OPC_RESTORE_TIMEOUT_SECS - 1,
343 minimum: MIN_OPC_RESTORE_TIMEOUT_SECS,
344 })
345 );
346 assert!(
347 validate_tuning_config(
348 &EffectiveTuningConfig {
349 restore_timeout_secs: MIN_OPC_RESTORE_TIMEOUT_SECS,
350 ..Default::default()
351 },
352 true,
353 )
354 .is_ok()
355 );
356 }
357}