redis_cell_rs/
verdict.rs

1use redis::{ErrorKind, FromRedisValue, RedisError, RedisResult, Value as RedisValue};
2
3#[derive(Debug, Clone)]
4#[non_exhaustive]
5pub struct AllowedDetails {
6    pub total: usize,
7    pub remaining: usize,
8    pub reset_after: u64,
9}
10
11#[derive(Debug, Clone)]
12#[non_exhaustive]
13pub struct BlockedDetails {
14    pub total: usize,
15    pub remaining: usize,
16    pub reset_after: u64,
17    pub retry_after: u64,
18}
19
20#[derive(Debug, Clone)]
21pub enum Verdict {
22    Allowed(AllowedDetails),
23    Blocked(BlockedDetails),
24}
25
26impl Verdict {
27    pub fn try_from_redis_value(value: &RedisValue) -> RedisResult<Self> {
28        let value = value.as_sequence().ok_or_else(|| {
29            let detail = format!(
30                "failed to decode Redis Cell response: exapected sequence, but got {:?}",
31                value
32            );
33            (
34                ErrorKind::ResponseError,
35                "invalid Redis Cell response",
36                detail,
37            )
38        })?;
39
40        if value.len() != 5 {
41            let detail = format!(
42                "failed to decode Redis Cell response: exapected sequence of 5 elements, but got {:?}",
43                value
44            );
45            let error = (
46                ErrorKind::ResponseError,
47                "invalid Redis Cell response",
48                detail,
49            )
50                .into();
51            return Err(error);
52        }
53
54        let (throttled, total, remaining, retry_after, reset_after) =
55            (&value[0], &value[1], &value[2], &value[3], &value[4]);
56
57        let verdict = if parse_throttled(throttled).map_to_redis_err()? {
58            Verdict::Blocked(BlockedDetails {
59                total: try_to_usize("total", total).map_to_redis_err()?,
60                remaining: try_to_usize("remaining", remaining).map_to_redis_err()?,
61                retry_after: try_to_u64("retry_after", retry_after).map_to_redis_err()?,
62                reset_after: try_to_u64("reset_after", reset_after).map_to_redis_err()?,
63            })
64        } else {
65            Verdict::Allowed(AllowedDetails {
66                total: try_to_usize("total", total).map_to_redis_err()?,
67                remaining: try_to_usize("remaining", remaining).map_to_redis_err()?,
68                reset_after: try_to_u64("reset_after", reset_after).map_to_redis_err()?,
69            })
70        };
71        Ok(verdict)
72    }
73}
74
75impl FromRedisValue for Verdict {
76    fn from_redis_value(v: &RedisValue) -> redis::RedisResult<Self> {
77        Verdict::try_from_redis_value(v)
78    }
79}
80
81fn parse_throttled(value: &RedisValue) -> Result<bool, String> {
82    let value = try_to_int("throttled", value)?;
83    match value {
84        0 => Ok(false),
85        1 => Ok(true),
86        other => Err(format!(
87            "failed to parse value for throttled (blocked), expected 0 or 1, but got {}",
88            other
89        )),
90    }
91}
92
93fn try_to_usize(field: &str, value: &RedisValue) -> Result<usize, String> {
94    let value = try_to_int(field, value)?;
95    usize::try_from(value).map_err(|_| {
96        format!(
97            "failed to parse {} as usize: tried to convert {}",
98            field, value
99        )
100    })
101}
102
103fn try_to_u64(field: &str, value: &RedisValue) -> Result<u64, String> {
104    let value = try_to_int(field, value)?;
105    u64::try_from(value).map_err(|_| {
106        format!(
107            "failed to parse {} as u64: tried to convert {}",
108            field, value
109        )
110    })
111}
112
113fn try_to_int(field: &str, value: &RedisValue) -> Result<i64, String> {
114    match value {
115        RedisValue::Int(value) => Ok(*value),
116        _ => Err(format!(
117            "failed to parse {}: expected integer, but got {:?}",
118            field, value
119        )),
120    }
121}
122
123trait MapToRedisError<T> {
124    fn map_to_redis_err(self) -> Result<T, RedisError>;
125}
126
127impl<T> MapToRedisError<T> for Result<T, String> {
128    fn map_to_redis_err(self) -> Result<T, RedisError> {
129        self.map_err(|detail| {
130            (
131                ErrorKind::ResponseError,
132                "invalid Redis Cell response",
133                detail,
134            )
135                .into()
136        })
137    }
138}