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}