mcpls_core/config/
bounded_secs.rs1use std::num::NonZeroU64;
7use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10
11use super::bounded_number::impl_bounded_number;
12use super::limits::MAX_TIMEOUT_SECONDS;
13use crate::bridge::{
14 DEFAULT_INDEXING_READY_TIMEOUT_SECS, INDEXING_STALENESS_BOUND, PROGRESS_SETTLE,
15};
16
17#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
19#[error("must be between {min} and {max} seconds, got {value}")]
20pub struct InvalidSecs {
21 pub value: u64,
23 pub min: u64,
25 pub max: u64,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
45#[serde(try_from = "u64", into = "u64")]
46pub struct BoundedSecs<const MIN: u64, const MAX: u64>(NonZeroU64);
47
48impl<const MIN: u64, const MAX: u64> BoundedSecs<MIN, MAX> {
49 pub const fn new(secs: u64) -> Result<Self, InvalidSecs> {
55 const { assert!(MIN >= 1 && MIN <= MAX, "MIN must satisfy 1 <= MIN <= MAX") };
56 if secs >= MIN
57 && secs <= MAX
58 && let Some(secs) = NonZeroU64::new(secs)
59 {
60 return Ok(Self(secs));
61 }
62 Err(InvalidSecs {
63 value: secs,
64 min: MIN,
65 max: MAX,
66 })
67 }
68
69 #[must_use]
71 pub const fn get(self) -> u64 {
72 self.0.get()
73 }
74
75 #[must_use]
77 pub const fn as_duration(self) -> Duration {
78 Duration::from_secs(self.0.get())
79 }
80}
81
82impl_bounded_number!(
83 @convert [const MIN: u64, const MAX: u64] BoundedSecs<MIN, MAX>, u64, InvalidSecs,
84 |secs: BoundedSecs<MIN, MAX>| secs.get()
85);
86
87pub type TimeoutSecs = BoundedSecs<1, MAX_TIMEOUT_SECONDS>;
89
90impl_bounded_number!(@default TimeoutSecs, 30, "Thirty seconds.");
91
92pub type IndexingReadyTimeoutSecs =
99 BoundedSecs<{ PROGRESS_SETTLE.as_secs() + 1 }, { INDEXING_STALENESS_BOUND.as_secs() - 1 }>;
100
101impl_bounded_number!(
102 @default IndexingReadyTimeoutSecs,
103 DEFAULT_INDEXING_READY_TIMEOUT_SECS,
104 "The built-in default, `DEFAULT_INDEXING_READY_TIMEOUT_SECS`."
105);
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn test_timeout_bounds() {
113 assert!(TimeoutSecs::new(0).is_err());
114 assert_eq!(
115 TimeoutSecs::new(901),
116 Err(InvalidSecs {
117 value: 901,
118 min: 1,
119 max: MAX_TIMEOUT_SECONDS
120 })
121 );
122 assert_eq!(TimeoutSecs::new(1).unwrap().get(), 1);
123 assert_eq!(
124 TimeoutSecs::new(MAX_TIMEOUT_SECONDS).unwrap().get(),
125 MAX_TIMEOUT_SECONDS
126 );
127 assert!(TimeoutSecs::new(MAX_TIMEOUT_SECONDS + 1).is_err());
128 }
129
130 #[test]
131 fn test_indexing_ready_timeout_bounds_are_exclusive() {
132 let settle = PROGRESS_SETTLE.as_secs();
133 let stale = INDEXING_STALENESS_BOUND.as_secs();
134 assert!(IndexingReadyTimeoutSecs::new(settle).is_err());
135 assert!(IndexingReadyTimeoutSecs::new(settle + 1).is_ok());
136 assert!(IndexingReadyTimeoutSecs::new(stale - 1).is_ok());
137 assert!(IndexingReadyTimeoutSecs::new(stale).is_err());
138 }
139
140 #[test]
141 fn test_defaults_match_documented_values() {
142 assert_eq!(TimeoutSecs::default().get(), 30);
143 assert_eq!(
144 IndexingReadyTimeoutSecs::default().get(),
145 DEFAULT_INDEXING_READY_TIMEOUT_SECS
146 );
147 }
148
149 #[test]
150 fn test_try_from_error_reports_range() {
151 let err = TimeoutSecs::try_from(0).unwrap_err();
152 assert_eq!(
153 err.to_string(),
154 format!("must be between 1 and {MAX_TIMEOUT_SECONDS} seconds, got 0")
155 );
156 }
157
158 #[test]
159 fn test_serde_round_trip_and_rejection() {
160 #[derive(Debug, Serialize, Deserialize, PartialEq)]
161 struct Holder {
162 secs: TimeoutSecs,
163 }
164
165 let holder: Holder = toml::from_str("secs = 12").unwrap();
166 assert_eq!(holder.secs.get(), 12);
167 assert_eq!(toml::to_string(&holder).unwrap().trim(), "secs = 12");
168 assert!(toml::from_str::<Holder>("secs = -1").is_err());
169 }
170}