1use std::fmt;
12use std::fmt::{Debug, Display, Formatter};
13use std::str::FromStr;
14use std::time::Duration;
15
16pub use go_duration::{parse_duration, ParseDurationError};
17use go_duration::{parse_duration_or_nanos, MAX_DURATION_NANOS};
18use serde::de::{self, Deserializer, Visitor};
19use serde::{Deserialize, Serialize, Serializer};
20
21#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
50pub struct DurationString(Duration);
51
52impl Debug for DurationString {
53 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
54 Debug::fmt(&self.0, f)
55 }
56}
57
58impl DurationString {
59 pub const fn new(d: Duration) -> Self {
61 Self(d)
62 }
63
64 pub const fn as_duration(&self) -> Duration {
66 self.0
67 }
68}
69
70impl From<Duration> for DurationString {
71 fn from(d: Duration) -> Self {
72 Self(d)
73 }
74}
75
76impl From<DurationString> for Duration {
77 fn from(d: DurationString) -> Self {
78 d.0
79 }
80}
81
82impl std::ops::Deref for DurationString {
83 type Target = Duration;
84
85 fn deref(&self) -> &Duration {
86 &self.0
87 }
88}
89
90impl FromStr for DurationString {
91 type Err = ParseDurationError;
92
93 fn from_str(s: &str) -> Result<Self, Self::Err> {
94 parse_duration_or_nanos(s).map(Self)
95 }
96}
97
98impl Display for DurationString {
99 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
100 write!(f, "{}s{}ns", self.0.as_secs(), self.0.subsec_nanos())
101 }
102}
103
104impl Serialize for DurationString {
105 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
106 serializer.collect_str(self)
107 }
108}
109
110impl<'de> Deserialize<'de> for DurationString {
111 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
112 deserializer.deserialize_any(DurationStringVisitor)
113 }
114}
115
116struct DurationStringVisitor;
117
118impl<'de> Visitor<'de> for DurationStringVisitor {
119 type Value = DurationString;
120
121 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 f.write_str("a duration string (e.g. \"30s\", \"1h30m\") or a non-negative number of nanoseconds")
123 }
124
125 fn visit_i64<E: de::Error>(self, n: i64) -> Result<DurationString, E> {
126 if n < 0 {
127 return Err(E::custom(ParseDurationError::Negative));
128 }
129 Ok(DurationString(Duration::from_nanos(n as u64)))
130 }
131
132 fn visit_i128<E: de::Error>(self, n: i128) -> Result<DurationString, E> {
133 if n < 0 {
134 return Err(E::custom(ParseDurationError::Negative));
135 }
136 if n > MAX_DURATION_NANOS as i128 {
137 return Err(E::custom(ParseDurationError::Overflow));
138 }
139 Ok(DurationString(Duration::from_nanos(n as u64)))
140 }
141
142 fn visit_u64<E: de::Error>(self, n: u64) -> Result<DurationString, E> {
143 if n > MAX_DURATION_NANOS {
144 return Err(E::custom(ParseDurationError::Overflow));
145 }
146 Ok(DurationString(Duration::from_nanos(n)))
147 }
148
149 fn visit_u128<E: de::Error>(self, n: u128) -> Result<DurationString, E> {
150 if n > MAX_DURATION_NANOS as u128 {
151 return Err(E::custom(ParseDurationError::Overflow));
152 }
153 Ok(DurationString(Duration::from_nanos(n as u64)))
154 }
155
156 fn visit_f64<E: de::Error>(self, f: f64) -> Result<DurationString, E> {
157 if !f.is_finite() {
158 return Err(E::custom("duration nanoseconds must be finite"));
159 }
160 if f < 0.0 {
161 return Err(E::custom(ParseDurationError::Negative));
162 }
163 if f > MAX_DURATION_NANOS as f64 {
164 return Err(E::custom(ParseDurationError::Overflow));
165 }
166 Ok(DurationString(Duration::from_nanos(f as u64)))
167 }
168
169 fn visit_str<E: de::Error>(self, s: &str) -> Result<DurationString, E> {
170 parse_duration_or_nanos(s).map(DurationString).map_err(E::custom)
171 }
172
173 fn visit_string<E: de::Error>(self, s: String) -> Result<DurationString, E> {
174 self.visit_str(&s)
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use anyhow::Context as _;
181 use serde_json::json;
182
183 use super::*;
184
185 const NS: Duration = Duration::from_nanos(1);
186 const _US: Duration = Duration::from_micros(1);
187 const MS: Duration = Duration::from_millis(1);
188 const S: Duration = Duration::from_secs(1);
189 const M: Duration = Duration::from_secs(60);
190 const H: Duration = Duration::from_secs(3600);
191
192 #[test]
193 fn deserialize_integer_succeeds() {
194 let json = r#"{ "value": 15 }"#;
195 let deserialized: SerdeTest = serde_json::from_str(json).unwrap();
196 assert_eq!(deserialized.value.as_duration(), 15 * NS);
197 }
198
199 #[test]
201 fn deserialize_float_succeeds() {
202 let json = r#"{ "value": 1.5 }"#;
203 let deserialized: SerdeTest = serde_json::from_str(json).unwrap();
204 assert_eq!(deserialized.value.as_duration(), 1 * NS);
205 }
206
207 #[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
208 struct SerdeTest {
209 value: DurationString,
210 }
211
212 impl From<Duration> for SerdeTest {
213 fn from(value: Duration) -> Self {
214 Self { value: value.into() }
215 }
216 }
217
218 fn test_json(input_value: &str) -> String {
219 json!({"value": input_value}).to_string()
220 }
221
222 fn test_yaml(input_value: &str) -> String {
223 format!("value: {input_value}")
224 }
225
226 fn run_success_case(input: &str, expected: Duration, serialized: &str) -> anyhow::Result<()> {
227 let expected_struct: SerdeTest = expected.into();
228 let json = test_json(input);
229 let yaml = test_yaml(input);
230 let msg = format!("failure for duration test case '{input}'");
231 let parsed_duration = DurationString::from_str(input).context(msg.clone())?;
232 anyhow::ensure!(
233 expected == parsed_duration.as_duration(),
234 "{msg}, expected: {expected:?}, got {:?}",
235 parsed_duration.as_duration()
236 );
237 let deserialized_from_json: SerdeTest = serde_json::from_str(&json).context(msg.clone())?;
238 anyhow::ensure!(
239 expected_struct == deserialized_from_json,
240 "{msg}, expected: {expected_struct:?}, got {deserialized_from_json:?}"
241 );
242 let roundtrip_json = serde_json::from_str(&serde_json::to_string(&expected_struct)?)?;
243 anyhow::ensure!(
244 expected_struct == roundtrip_json,
245 "{msg}, expected json roundrip to produce {expected_struct:?}, but got {roundtrip_json:?}"
246 );
247 let deserialized_from_yaml: SerdeTest = serde_yaml::from_str(&yaml).context(msg.clone())?;
248 anyhow::ensure!(
249 expected_struct == deserialized_from_yaml,
250 "{msg}, expected: {expected_struct:?}, got {deserialized_from_yaml:?}"
251 );
252 let roundtrip_yaml = serde_yaml::from_str(&serde_yaml::to_string(&expected_struct)?)?;
253 anyhow::ensure!(
254 expected_struct == roundtrip_yaml,
255 "{msg}, expected json roundrip to produce {expected_struct:?}, but got {roundtrip_yaml:?}"
256 );
257 let actual_serialized = parsed_duration.to_string();
258 anyhow::ensure!(
259 serialized == actual_serialized,
260 "Expected the input '{input}' to be serialized as '{serialized}' but got '{actual_serialized}'"
261 );
262 Ok(())
263 }
264
265 #[test]
266 fn parse_duration_supports_go_style_units() {
267 assert_eq!(parse_duration("10s").unwrap(), Duration::from_secs(10));
268 assert_eq!(parse_duration("1m0s").unwrap(), Duration::from_secs(60));
269 assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
270 assert_eq!(
271 parse_duration("1h2m3.5s").unwrap(),
272 Duration::from_secs(3723) + Duration::from_millis(500)
273 );
274 assert_eq!(parse_duration("250us").unwrap(), Duration::from_micros(250));
275 assert_eq!(parse_duration("250µs").unwrap(), Duration::from_micros(250));
276 assert_eq!(parse_duration("250μs").unwrap(), Duration::from_micros(250));
277 }
278
279 #[test]
280 fn parse_duration_rejects_config_only_values() {
281 assert!(parse_duration("").is_err());
282 assert!(parse_duration("abc").is_err());
283 assert!(parse_duration("10").is_err());
284 assert!(parse_duration(" 10s").is_err());
285 assert!(parse_duration("1xs").is_err());
286 }
287
288 #[test]
289 fn duration_string_success_cases() {
290 let cases: &[(&str, Duration, &str)] = &[
291 ("0", Duration::ZERO, "0s0ns"),
292 ("-0", Duration::ZERO, "0s0ns"),
293 ("+0", Duration::ZERO, "0s0ns"),
294 ("+5h", Duration::from_hours(5), "18000s0ns"),
295 (".5s", Duration::from_millis(500), "0s500000000ns"),
296 ("5.s", Duration::from_secs(5), "5s0ns"),
297 ("0.000000001s", Duration::from_nanos(1), "0s1ns"),
298 ("1.5h", Duration::from_mins(90), "5400s0ns"),
299 (
300 "2h45m30.5s",
301 (2 * H) + (45 * M) + (30 * S) + (500 * MS),
302 "9930s500000000ns",
303 ),
304 ("12µs", Duration::from_micros(12), "0s12000ns"),
305 ("12μs", Duration::from_micros(12), "0s12000ns"),
306 ("0s", Duration::ZERO, "0s0ns"),
307 ("1h1m1s1ms1us1ns", H + M + S + MS + (1000 * NS) + NS, "3661s1001001ns"),
308 ("24h", Duration::from_hours(24), "86400s0ns"),
309 (
310 "9223372036854775807ns",
311 Duration::from_nanos(9223372036854775807),
312 "9223372036s854775807ns",
313 ),
314 (
315 "9223372036854775.807us",
316 Duration::from_secs(9223372036) + (854775807 * NS),
317 "9223372036s854775807ns",
318 ),
319 (
320 "2562047h47m16.854775807s",
321 Duration::from_secs(9223372036) + (854775807 * NS),
322 "9223372036s854775807ns",
323 ),
324 ("0.1ns", Duration::ZERO, "0s0ns"),
325 ("05s", Duration::from_secs(5), "5s0ns"),
326 ("1ns1s", S + NS, "1s1ns"),
327 ("100h100m100s", (100 * H) + (100 * M) + (100 * S), "366100s0ns"),
328 ("5m32s", (5 * M) + (32 * S), "332s0ns"),
329 ("1m0s", M, "60s0ns"),
330 ("5m0s", 5 * M, "300s0ns"),
331 ("6m0s", 6 * M, "360s0ns"),
332 ("10m0s", 10 * M, "600s0ns"),
333 ("15m0s", 15 * M, "900s0ns"),
334 ("30m0s", 30 * M, "1800s0ns"),
335 ("40m0s", 40 * M, "2400s0ns"),
336 ("50m0s", 50 * M, "3000s0ns"),
337 ("87600h0m0s", 87600 * H, "315360000s0ns"),
338 ("5", 5 * NS, "0s5ns"),
339 (" 5s", 5 * S, "5s0ns"),
340 ("5s", 5 * S, "5s0ns"),
341 ];
342
343 for (input, expected, serialized) in cases {
344 run_success_case(input, *expected, serialized).unwrap();
345 }
346 }
347
348 fn run_failure_case(input: &str, expected_msg: &str) -> anyhow::Result<()> {
349 let result = DurationString::from_str(input);
350 match result {
351 Ok(value) => {
352 anyhow::bail!("Expected an error when parsing '{input}', but instead received the value '{value:?}'")
353 }
354 Err(e) => {
355 anyhow::ensure!(
356 e.to_string().contains(expected_msg),
357 "Expected the error message when parsing '{input}' to contain {expected_msg:?}, but the message is {e}"
358 );
359 }
360 }
361
362 Ok(())
363 }
364
365 #[test]
366 fn duration_string_failure_cases() {
367 let cases: &[(&str, &str)] = &[
368 ("5m32sFOO", "unknown unit 'sFOO'"),
369 ("", "empty duration"),
370 (" ", "empty duration"),
371 ("+", "empty duration"),
372 ("-", "empty duration"),
373 (".", "expected digits"),
374 ("s", "expected digits"),
375 (".s", "expected digits"),
376 ("--5s", "expected digits"),
377 ("5.5.5s", "missing unit"),
378 ("1e3s", "unknown unit 'e'"),
379 ("5ns5", "missing unit"),
380 ("9223372036854775808ns", "exceeds"),
381 ("-1s", "negative"),
382 ("-0.5h", "negative"),
383 ("1d", "unknown unit 'd'"),
384 ("1w", "unknown unit 'w'"),
385 ("1S", "unknown unit 'S'"),
386 ("12 µs", "missing unit"),
387 ("5 s", "missing unit"),
388 ("5. s", "missing unit"),
389 ];
390
391 for (input, expected_msg) in cases {
392 run_failure_case(input, expected_msg).unwrap();
393 }
394 }
395}