Skip to main content

zerodds_grpc_bridge/
timeout.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! gRPC `grpc-timeout` Header — Spec §"Timeout".
5
6use alloc::string::String;
7use core::fmt;
8
9/// Spec §"TimeoutUnit".
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum TimeoutUnit {
12    /// `H` — Hour.
13    Hour,
14    /// `M` — Minute.
15    Minute,
16    /// `S` — Second.
17    Second,
18    /// `m` — Millisecond.
19    Millisecond,
20    /// `u` — Microsecond.
21    Microsecond,
22    /// `n` — Nanosecond.
23    Nanosecond,
24}
25
26impl TimeoutUnit {
27    /// Wire-Char.
28    #[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    /// Konvertiert Wire-Char.
41    #[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/// Timeout-Parser-Fehler.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum TimeoutError {
58    /// Empty Header.
59    Empty,
60    /// Spec — TimeoutValue MUST positive integer with at most 8
61    /// digits.
62    ValueTooLong,
63    /// Non-digit Character vor Unit.
64    InvalidValue,
65    /// Spec — Unit MUST be one of H/M/S/m/u/n.
66    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
83/// Spec §"Timeout" — encodes timeout value+unit als
84/// `grpc-timeout`-Header-Wert.
85///
86/// Liefert formatted String wie `"100m"` (100 Millisekunden) oder
87/// `"30S"` (30 Sekunden).
88///
89/// # Errors
90/// `ValueTooLong` wenn `value > 99_999_999` (9+ digits).
91pub 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
100/// Spec §"Timeout" — decodes `grpc-timeout`-Header-Wert.
101///
102/// # Errors
103/// Siehe [`TimeoutError`].
104pub 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        // Spec §"TimeoutUnit".
143        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        // Spec §"Timeout" Beispiel.
154        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        // Spec §"TimeoutValue" — at most 8 digits.
168        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        // Spec — at most 8 digits.
211        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}