Skip to main content

solti_model/domain/
timeout.rs

1//! # Attempt timeout
2//!
3//! [`Timeout`] is a positive millisecond value used by [`TaskSpec`](crate::TaskSpec).
4
5use std::{fmt, num::NonZeroU64};
6
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9use crate::{ModelError, ModelResult};
10
11/// Timeout value in milliseconds.
12///
13/// ```
14/// use solti_model::Timeout;
15///
16/// let timeout = Timeout::new(5_000).unwrap();
17/// assert_eq!(timeout.as_millis(), 5_000);
18///
19/// let timeout = Timeout::new(10_000).unwrap();
20/// assert_eq!(format!("{timeout}"), "10000ms");
21/// ```
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
23#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
24#[cfg_attr(feature = "schema", schemars(transparent))]
25pub struct Timeout(NonZeroU64);
26
27impl Timeout {
28    /// Creates a timeout value.
29    ///
30    /// # Errors
31    ///
32    /// Returns [`ModelError::Invalid`] when `ms` is zero.
33    ///
34    /// ## Example
35    ///
36    /// ```
37    /// use solti_model::Timeout;
38    ///
39    /// let timeout = Timeout::new(5_000).unwrap();
40    /// assert_eq!(timeout.as_millis(), 5_000);
41    /// ```
42    pub fn new(ms: u64) -> ModelResult<Self> {
43        NonZeroU64::new(ms)
44            .map(Self)
45            .ok_or_else(|| ModelError::Invalid("timeout must be greater than zero".into()))
46    }
47
48    /// Returns the timeout in milliseconds.
49    ///
50    /// ## Example
51    ///
52    /// ```
53    /// use solti_model::Timeout;
54    ///
55    /// let timeout = Timeout::new(10_000).unwrap();
56    /// assert_eq!(timeout.as_millis(), 10_000);
57    /// ```
58    pub const fn as_millis(&self) -> u64 {
59        self.0.get()
60    }
61}
62
63impl TryFrom<u64> for Timeout {
64    type Error = ModelError;
65
66    #[inline]
67    fn try_from(ms: u64) -> Result<Self, Self::Error> {
68        Self::new(ms)
69    }
70}
71
72impl From<Timeout> for u64 {
73    #[inline]
74    fn from(t: Timeout) -> Self {
75        t.as_millis()
76    }
77}
78
79impl Serialize for Timeout {
80    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
81    where
82        S: Serializer,
83    {
84        serializer.serialize_u64(self.as_millis())
85    }
86}
87
88impl<'de> Deserialize<'de> for Timeout {
89    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
90    where
91        D: Deserializer<'de>,
92    {
93        Self::new(u64::deserialize(deserializer)?).map_err(serde::de::Error::custom)
94    }
95}
96
97impl fmt::Display for Timeout {
98    #[inline]
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        write!(f, "{}ms", self.as_millis())
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::Timeout;
107
108    #[test]
109    fn exposes_milliseconds_conversions_display_and_ordering() {
110        let timeout = Timeout::try_from(1_500).unwrap();
111        assert_eq!(timeout.as_millis(), 1_500);
112        assert_eq!(format!("{timeout}"), "1500ms");
113        assert_eq!(u64::from(timeout), 1_500);
114
115        let a = Timeout::new(100).unwrap();
116        let b = Timeout::new(200).unwrap();
117        assert!(a < b);
118    }
119
120    #[test]
121    fn serde_is_transparent_and_validated() {
122        let timeout = Timeout::new(5_000).unwrap();
123        let json = serde_json::to_string(&timeout).unwrap();
124        assert_eq!(json, "5000");
125        assert_eq!(serde_json::from_str::<Timeout>(&json).unwrap(), timeout);
126        assert!(Timeout::new(0).is_err());
127        assert!(serde_json::from_str::<Timeout>("0").is_err());
128    }
129}