zerodds_grpc_bridge/
timeout.rs1use alloc::string::String;
7use core::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum TimeoutUnit {
12 Hour,
14 Minute,
16 Second,
18 Millisecond,
20 Microsecond,
22 Nanosecond,
24}
25
26impl TimeoutUnit {
27 #[must_use]
29 pub const fn to_char(self) -> char {
30 match self {
31 Self::Hour => 'H',
32 Self::Minute => 'M',
33 Self::Second => 'S',
34 Self::Millisecond => 'm',
35 Self::Microsecond => 'u',
36 Self::Nanosecond => 'n',
37 }
38 }
39
40 #[must_use]
42 pub const fn from_char(c: char) -> Option<Self> {
43 match c {
44 'H' => Some(Self::Hour),
45 'M' => Some(Self::Minute),
46 'S' => Some(Self::Second),
47 'm' => Some(Self::Millisecond),
48 'u' => Some(Self::Microsecond),
49 'n' => Some(Self::Nanosecond),
50 _ => None,
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum TimeoutError {
58 Empty,
60 ValueTooLong,
63 InvalidValue,
65 InvalidUnit(char),
67}
68
69impl fmt::Display for TimeoutError {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 match self {
72 Self::Empty => f.write_str("empty timeout header"),
73 Self::ValueTooLong => f.write_str("timeout value > 8 digits"),
74 Self::InvalidValue => f.write_str("non-digit in timeout value"),
75 Self::InvalidUnit(c) => write!(f, "invalid timeout unit `{c}`"),
76 }
77 }
78}
79
80#[cfg(feature = "std")]
81impl std::error::Error for TimeoutError {}
82
83pub fn encode_timeout(value: u32, unit: TimeoutUnit) -> Result<String, TimeoutError> {
92 if value > 99_999_999 {
93 return Err(TimeoutError::ValueTooLong);
94 }
95 let mut s = alloc::format!("{value}");
96 s.push(unit.to_char());
97 Ok(s)
98}
99
100pub fn decode_timeout(header: &str) -> Result<(u32, TimeoutUnit), TimeoutError> {
105 if header.is_empty() {
106 return Err(TimeoutError::Empty);
107 }
108 let last_char = header.chars().next_back().ok_or(TimeoutError::Empty)?;
109 let unit = TimeoutUnit::from_char(last_char).ok_or(TimeoutError::InvalidUnit(last_char))?;
110 let value_str = &header[..header.len() - last_char.len_utf8()];
111 if value_str.is_empty() || value_str.len() > 8 {
112 return Err(TimeoutError::ValueTooLong);
113 }
114 if !value_str.bytes().all(|b| b.is_ascii_digit()) {
115 return Err(TimeoutError::InvalidValue);
116 }
117 let value: u32 = value_str.parse().map_err(|_| TimeoutError::InvalidValue)?;
118 Ok((value, unit))
119}
120
121#[cfg(test)]
122#[allow(clippy::expect_used)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn timeout_unit_round_trip_for_all() {
128 for u in [
129 TimeoutUnit::Hour,
130 TimeoutUnit::Minute,
131 TimeoutUnit::Second,
132 TimeoutUnit::Millisecond,
133 TimeoutUnit::Microsecond,
134 TimeoutUnit::Nanosecond,
135 ] {
136 assert_eq!(TimeoutUnit::from_char(u.to_char()), Some(u));
137 }
138 }
139
140 #[test]
141 fn well_known_unit_chars_match_spec() {
142 assert_eq!(TimeoutUnit::Hour.to_char(), 'H');
144 assert_eq!(TimeoutUnit::Minute.to_char(), 'M');
145 assert_eq!(TimeoutUnit::Second.to_char(), 'S');
146 assert_eq!(TimeoutUnit::Millisecond.to_char(), 'm');
147 assert_eq!(TimeoutUnit::Microsecond.to_char(), 'u');
148 assert_eq!(TimeoutUnit::Nanosecond.to_char(), 'n');
149 }
150
151 #[test]
152 fn encodes_30_seconds() {
153 assert_eq!(encode_timeout(30, TimeoutUnit::Second).expect("ok"), "30S");
155 }
156
157 #[test]
158 fn encodes_500_milliseconds() {
159 assert_eq!(
160 encode_timeout(500, TimeoutUnit::Millisecond).expect("ok"),
161 "500m"
162 );
163 }
164
165 #[test]
166 fn rejects_value_above_8_digits_on_encode() {
167 assert_eq!(
169 encode_timeout(100_000_000, TimeoutUnit::Second),
170 Err(TimeoutError::ValueTooLong)
171 );
172 }
173
174 #[test]
175 fn round_trip_decode_encode() {
176 for v in [1u32, 30, 500, 99_999_999] {
177 for u in [
178 TimeoutUnit::Hour,
179 TimeoutUnit::Minute,
180 TimeoutUnit::Second,
181 TimeoutUnit::Millisecond,
182 TimeoutUnit::Microsecond,
183 TimeoutUnit::Nanosecond,
184 ] {
185 let s = encode_timeout(v, u).expect("encode");
186 let (dv, du) = decode_timeout(&s).expect("decode");
187 assert_eq!(dv, v);
188 assert_eq!(du, u);
189 }
190 }
191 }
192
193 #[test]
194 fn decode_rejects_empty() {
195 assert_eq!(decode_timeout(""), Err(TimeoutError::Empty));
196 }
197
198 #[test]
199 fn decode_rejects_unknown_unit() {
200 assert_eq!(decode_timeout("100x"), Err(TimeoutError::InvalidUnit('x')));
201 }
202
203 #[test]
204 fn decode_rejects_non_digit_value() {
205 assert_eq!(decode_timeout("abcS"), Err(TimeoutError::InvalidValue));
206 }
207
208 #[test]
209 fn decode_rejects_value_above_8_digits() {
210 assert_eq!(
212 decode_timeout("123456789S"),
213 Err(TimeoutError::ValueTooLong)
214 );
215 }
216
217 #[test]
218 fn decode_rejects_only_unit_no_value() {
219 assert_eq!(decode_timeout("S"), Err(TimeoutError::ValueTooLong));
220 }
221}