1use std::io;
7
8use kevy_resp::Reply;
9
10#[non_exhaustive]
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum PubsubEvent {
18 Subscribe {
20 channel: Vec<u8>,
22 count: i64,
24 },
25 Psubscribe {
27 pattern: Vec<u8>,
29 count: i64,
31 },
32 Unsubscribe {
34 channel: Option<Vec<u8>>,
36 count: i64,
38 },
39 Punsubscribe {
41 pattern: Option<Vec<u8>>,
43 count: i64,
45 },
46 Message {
48 channel: Vec<u8>,
50 payload: Vec<u8>,
52 },
53 Pmessage {
55 pattern: Vec<u8>,
57 channel: Vec<u8>,
59 payload: Vec<u8>,
61 },
62}
63
64pub fn classify_pubsub(reply: Reply) -> io::Result<PubsubEvent> {
68 let items = match reply {
69 Reply::Array(v) | Reply::Push(v) => v,
70 Reply::Error(e) => return Err(io::Error::other(String::from_utf8_lossy(&e).into_owned())),
71 other => {
72 return Err(invalid(format!("pubsub: expected array/push, got {}", shape(&other))));
73 }
74 };
75
76 let mut it = items.into_iter();
77 let kind = take_bulk(it.next().ok_or_else(|| invalid("pubsub: empty frame"))?, "kind")?;
78
79 match kind.as_slice() {
80 b"subscribe" => {
81 let channel = take_bulk(
82 it.next().ok_or_else(|| invalid("subscribe: missing channel"))?,
83 "channel",
84 )?;
85 let count =
86 take_int(it.next().ok_or_else(|| invalid("subscribe: missing count"))?, "count")?;
87 Ok(PubsubEvent::Subscribe { channel, count })
88 }
89 b"psubscribe" => {
90 let pattern = take_bulk(
91 it.next().ok_or_else(|| invalid("psubscribe: missing pattern"))?,
92 "pattern",
93 )?;
94 let count =
95 take_int(it.next().ok_or_else(|| invalid("psubscribe: missing count"))?, "count")?;
96 Ok(PubsubEvent::Psubscribe { pattern, count })
97 }
98 b"unsubscribe" => {
99 let channel = take_bulk_or_nil(
100 it.next().ok_or_else(|| invalid("unsubscribe: missing channel"))?,
101 "channel",
102 )?;
103 let count =
104 take_int(it.next().ok_or_else(|| invalid("unsubscribe: missing count"))?, "count")?;
105 Ok(PubsubEvent::Unsubscribe { channel, count })
106 }
107 b"punsubscribe" => {
108 let pattern = take_bulk_or_nil(
109 it.next().ok_or_else(|| invalid("punsubscribe: missing pattern"))?,
110 "pattern",
111 )?;
112 let count = take_int(
113 it.next().ok_or_else(|| invalid("punsubscribe: missing count"))?,
114 "count",
115 )?;
116 Ok(PubsubEvent::Punsubscribe { pattern, count })
117 }
118 b"message" => {
119 let channel = take_bulk(
120 it.next().ok_or_else(|| invalid("message: missing channel"))?,
121 "channel",
122 )?;
123 let payload = take_bulk(
124 it.next().ok_or_else(|| invalid("message: missing payload"))?,
125 "payload",
126 )?;
127 Ok(PubsubEvent::Message { channel, payload })
128 }
129 b"pmessage" => {
130 let pattern = take_bulk(
131 it.next().ok_or_else(|| invalid("pmessage: missing pattern"))?,
132 "pattern",
133 )?;
134 let channel = take_bulk(
135 it.next().ok_or_else(|| invalid("pmessage: missing channel"))?,
136 "channel",
137 )?;
138 let payload = take_bulk(
139 it.next().ok_or_else(|| invalid("pmessage: missing payload"))?,
140 "payload",
141 )?;
142 Ok(PubsubEvent::Pmessage { pattern, channel, payload })
143 }
144 other => Err(invalid(format!("unknown pubsub kind: {}", String::from_utf8_lossy(other)))),
145 }
146}
147
148fn take_bulk(r: Reply, field: &str) -> io::Result<Vec<u8>> {
149 match r {
150 Reply::Bulk(v) | Reply::Simple(v) => Ok(v),
151 other => {
152 Err(invalid(format!("pubsub field {field}: expected bulk, got {}", shape(&other))))
153 }
154 }
155}
156
157fn take_bulk_or_nil(r: Reply, field: &str) -> io::Result<Option<Vec<u8>>> {
158 match r {
159 Reply::Bulk(v) | Reply::Simple(v) => Ok(Some(v)),
160 Reply::Nil | Reply::Null => Ok(None),
161 other => {
162 Err(invalid(format!("pubsub field {field}: expected bulk/nil, got {}", shape(&other))))
163 }
164 }
165}
166
167fn take_int(r: Reply, field: &str) -> io::Result<i64> {
168 match r {
169 Reply::Int(n) => Ok(n),
170 other => Err(invalid(format!("pubsub field {field}: expected int, got {}", shape(&other)))),
171 }
172}
173
174fn shape(r: &Reply) -> &'static str {
175 match r {
176 Reply::Simple(_) => "simple",
177 Reply::Error(_) => "error",
178 Reply::Int(_) => "int",
179 Reply::Bulk(_) => "bulk",
180 Reply::Nil | Reply::Null => "nil",
181 Reply::Array(_) => "array",
182 Reply::Map(_) => "map",
183 Reply::Set(_) => "set",
184 Reply::Double(_) => "double",
185 Reply::Boolean(_) => "boolean",
186 Reply::Verbatim { .. } => "verbatim",
187 Reply::BigNumber(_) => "bignumber",
188 Reply::Push(_) => "push",
189 Reply::BlobError(_) => "bloberror",
190 }
191}
192
193fn invalid(msg: impl Into<String>) -> io::Error {
194 io::Error::new(io::ErrorKind::InvalidData, msg.into())
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn classify_subscribe_ack() {
203 let r = Reply::Array(vec![
204 Reply::Bulk(b"subscribe".to_vec()),
205 Reply::Bulk(b"chan".to_vec()),
206 Reply::Int(1),
207 ]);
208 assert_eq!(
209 classify_pubsub(r).unwrap(),
210 PubsubEvent::Subscribe { channel: b"chan".to_vec(), count: 1 }
211 );
212 }
213
214 #[test]
215 fn classify_message_event() {
216 let r = Reply::Array(vec![
217 Reply::Bulk(b"message".to_vec()),
218 Reply::Bulk(b"news".to_vec()),
219 Reply::Bulk(b"hello".to_vec()),
220 ]);
221 assert_eq!(
222 classify_pubsub(r).unwrap(),
223 PubsubEvent::Message { channel: b"news".to_vec(), payload: b"hello".to_vec() }
224 );
225 }
226
227 #[test]
228 fn classify_pmessage_event() {
229 let r = Reply::Array(vec![
230 Reply::Bulk(b"pmessage".to_vec()),
231 Reply::Bulk(b"news.*".to_vec()),
232 Reply::Bulk(b"news.tech".to_vec()),
233 Reply::Bulk(b"hi".to_vec()),
234 ]);
235 assert_eq!(
236 classify_pubsub(r).unwrap(),
237 PubsubEvent::Pmessage {
238 pattern: b"news.*".to_vec(),
239 channel: b"news.tech".to_vec(),
240 payload: b"hi".to_vec(),
241 }
242 );
243 }
244
245 #[test]
246 fn classify_unsubscribe_with_nil_channel() {
247 let r = Reply::Array(vec![Reply::Bulk(b"unsubscribe".to_vec()), Reply::Nil, Reply::Int(0)]);
248 assert_eq!(
249 classify_pubsub(r).unwrap(),
250 PubsubEvent::Unsubscribe { channel: None, count: 0 }
251 );
252 }
253
254 #[test]
255 fn classify_accepts_push_frame() {
256 let r = Reply::Push(vec![
258 Reply::Bulk(b"message".to_vec()),
259 Reply::Bulk(b"c".to_vec()),
260 Reply::Bulk(b"p".to_vec()),
261 ]);
262 assert_eq!(
263 classify_pubsub(r).unwrap(),
264 PubsubEvent::Message { channel: b"c".to_vec(), payload: b"p".to_vec() }
265 );
266 }
267
268 #[test]
269 fn classify_accepts_simple_string_fields() {
270 let r = Reply::Array(vec![
273 Reply::Simple(b"subscribe".to_vec()),
274 Reply::Simple(b"chan".to_vec()),
275 Reply::Int(2),
276 ]);
277 assert_eq!(
278 classify_pubsub(r).unwrap(),
279 PubsubEvent::Subscribe { channel: b"chan".to_vec(), count: 2 }
280 );
281 }
282
283 #[test]
284 fn classify_rejects_unknown_kind() {
285 let r = Reply::Array(vec![
286 Reply::Bulk(b"bogus".to_vec()),
287 Reply::Bulk(b"x".to_vec()),
288 Reply::Int(0),
289 ]);
290 assert!(classify_pubsub(r).is_err());
291 }
292
293 #[test]
294 fn classify_rejects_wrong_arity() {
295 let r = Reply::Array(vec![Reply::Bulk(b"subscribe".to_vec()), Reply::Bulk(b"x".to_vec())]);
296 assert!(classify_pubsub(r).is_err());
297 }
298}