Skip to main content

synap_sdk/
sorted_set.rs

1//! Sorted Set data structure operations
2//!
3//! Sorted Sets are collections of unique members, each associated with a score.
4//! Members are ordered by score, enabling range queries, ranking, and leaderboard functionality.
5//!
6//! Use cases:
7//! - Gaming leaderboards
8//! - Priority queues
9//! - Rate limiting with timestamps
10//! - Time-series data
11//! - Auto-complete with relevance scores
12
13use crate::client::SynapClient;
14use crate::error::Result;
15use serde::{Deserialize, Serialize};
16use serde_json::json;
17
18/// Sorted Set data structure interface (Redis-compatible)
19///
20/// Provides scored, ordered collections with ranking and range query capabilities.
21#[derive(Clone)]
22pub struct SortedSetManager {
23    client: SynapClient,
24}
25
26/// A member with its score
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ScoredMember {
29    pub member: String,
30    pub score: f64,
31}
32
33impl SortedSetManager {
34    /// Create a new Sorted Set manager interface
35    pub(crate) fn new(client: SynapClient) -> Self {
36        Self { client }
37    }
38
39    /// Add member with score to sorted set (ZADD)
40    ///
41    /// # Example
42    /// ```no_run
43    /// # use synap_sdk::SynapClient;
44    /// # async fn example(client: &SynapClient) -> synap_sdk::Result<()> {
45    /// client.sorted_set().add("leaderboard", "player1", 100.0).await?;
46    /// # Ok(())
47    /// # }
48    /// ```
49    pub async fn add<K, M>(&self, key: K, member: M, score: f64) -> Result<bool>
50    where
51        K: AsRef<str>,
52        M: AsRef<str>,
53    {
54        let payload = json!({
55            "key": key.as_ref(),
56            "member": member.as_ref(),
57            "score": score,
58        });
59
60        let response = self.client.send_command("sortedset.zadd", payload).await?;
61        Ok(response.get("added").and_then(|v| v.as_u64()).unwrap_or(0) > 0)
62    }
63
64    /// Add multiple members with scores to sorted set (ZADD with array)
65    ///
66    /// # Example
67    /// ```no_run
68    /// # use synap_sdk::{SynapClient, sorted_set::ScoredMember};
69    /// # async fn example(client: &SynapClient) -> synap_sdk::Result<()> {
70    /// let members = vec![
71    ///     ScoredMember { member: "player1".to_string(), score: 100.0 },
72    ///     ScoredMember { member: "player2".to_string(), score: 200.0 },
73    /// ];
74    /// client.sorted_set().add_multiple("leaderboard", members).await?;
75    /// # Ok(())
76    /// # }
77    /// ```
78    pub async fn add_multiple<K>(&self, key: K, members: Vec<ScoredMember>) -> Result<usize>
79    where
80        K: AsRef<str>,
81    {
82        let payload = json!({
83            "key": key.as_ref(),
84            "members": members.iter().map(|m| json!({
85                "member": m.member,
86                "score": m.score,
87            })).collect::<Vec<_>>(),
88        });
89
90        let response = self.client.send_command("sortedset.zadd", payload).await?;
91        Ok(response.get("added").and_then(|v| v.as_u64()).unwrap_or(0) as usize)
92    }
93
94    /// Remove members from sorted set (ZREM)
95    pub async fn rem<K>(&self, key: K, members: Vec<String>) -> Result<usize>
96    where
97        K: AsRef<str>,
98    {
99        let payload = json!({
100            "key": key.as_ref(),
101            "members": members,
102        });
103
104        let response = self.client.send_command("sortedset.zrem", payload).await?;
105        Ok(response
106            .get("removed")
107            .and_then(|v| v.as_u64())
108            .unwrap_or(0) as usize)
109    }
110
111    /// Get score of a member (ZSCORE)
112    pub async fn score<K, M>(&self, key: K, member: M) -> Result<Option<f64>>
113    where
114        K: AsRef<str>,
115        M: AsRef<str>,
116    {
117        let payload = json!({
118            "key": key.as_ref(),
119            "member": member.as_ref(),
120        });
121
122        let response = self
123            .client
124            .send_command("sortedset.zscore", payload)
125            .await?;
126        Ok(response.get("score").and_then(|v| v.as_f64()))
127    }
128
129    /// Get cardinality (number of members) (ZCARD)
130    pub async fn card<K>(&self, key: K) -> Result<usize>
131    where
132        K: AsRef<str>,
133    {
134        let payload = json!({"key": key.as_ref()});
135        let response = self.client.send_command("sortedset.zcard", payload).await?;
136        Ok(response.get("count").and_then(|v| v.as_u64()).unwrap_or(0) as usize)
137    }
138
139    /// Increment score of member (ZINCRBY)
140    pub async fn incr_by<K, M>(&self, key: K, member: M, increment: f64) -> Result<f64>
141    where
142        K: AsRef<str>,
143        M: AsRef<str>,
144    {
145        let payload = json!({
146            "key": key.as_ref(),
147            "member": member.as_ref(),
148            "increment": increment,
149        });
150
151        let response = self
152            .client
153            .send_command("sortedset.zincrby", payload)
154            .await?;
155        Ok(response
156            .get("score")
157            .and_then(|v| v.as_f64())
158            .unwrap_or(0.0))
159    }
160
161    /// Get range by rank (0-based index) (ZRANGE)
162    ///
163    /// # Example
164    /// ```no_run
165    /// # use synap_sdk::SynapClient;
166    /// # async fn example(client: &SynapClient) -> synap_sdk::Result<()> {
167    /// // Get top 10 from leaderboard
168    /// let top10 = client.sorted_set().range("leaderboard", 0, 9, true).await?;
169    /// for member in top10 {
170    ///     tracing::info!("{}: {}", member.member, member.score);
171    /// }
172    /// # Ok(())
173    /// # }
174    /// ```
175    pub async fn range<K>(
176        &self,
177        key: K,
178        start: i64,
179        stop: i64,
180        with_scores: bool,
181    ) -> Result<Vec<ScoredMember>>
182    where
183        K: AsRef<str>,
184    {
185        let payload = json!({
186            "key": key.as_ref(),
187            "start": start,
188            "stop": stop,
189            "withscores": with_scores,
190        });
191
192        let response = self
193            .client
194            .send_command("sortedset.zrange", payload)
195            .await?;
196
197        if let Some(members_val) = response.get("members") {
198            Ok(serde_json::from_value(members_val.clone()).unwrap_or_default())
199        } else {
200            Ok(Vec::new())
201        }
202    }
203
204    /// Get reverse range by rank (highest to lowest) (ZREVRANGE)
205    pub async fn rev_range<K>(
206        &self,
207        key: K,
208        start: i64,
209        stop: i64,
210        with_scores: bool,
211    ) -> Result<Vec<ScoredMember>>
212    where
213        K: AsRef<str>,
214    {
215        let payload = json!({
216            "key": key.as_ref(),
217            "start": start,
218            "stop": stop,
219            "withscores": with_scores,
220        });
221
222        let response = self
223            .client
224            .send_command("sortedset.zrevrange", payload)
225            .await?;
226
227        if let Some(members_val) = response.get("members") {
228            Ok(serde_json::from_value(members_val.clone()).unwrap_or_default())
229        } else {
230            Ok(Vec::new())
231        }
232    }
233
234    /// Get rank of member (0-based, lowest score = rank 0) (ZRANK)
235    pub async fn rank<K, M>(&self, key: K, member: M) -> Result<Option<usize>>
236    where
237        K: AsRef<str>,
238        M: AsRef<str>,
239    {
240        let payload = json!({
241            "key": key.as_ref(),
242            "member": member.as_ref(),
243        });
244
245        let response = self.client.send_command("sortedset.zrank", payload).await?;
246        Ok(response
247            .get("rank")
248            .and_then(|v| v.as_u64())
249            .map(|v| v as usize))
250    }
251
252    /// Get reverse rank of member (0-based, highest score = rank 0) (ZREVRANK)
253    pub async fn rev_rank<K, M>(&self, key: K, member: M) -> Result<Option<usize>>
254    where
255        K: AsRef<str>,
256        M: AsRef<str>,
257    {
258        let payload = json!({
259            "key": key.as_ref(),
260            "member": member.as_ref(),
261        });
262
263        let response = self
264            .client
265            .send_command("sortedset.zrevrank", payload)
266            .await?;
267        Ok(response
268            .get("rank")
269            .and_then(|v| v.as_u64())
270            .map(|v| v as usize))
271    }
272
273    /// Count members with scores in range (ZCOUNT)
274    pub async fn count<K>(&self, key: K, min: f64, max: f64) -> Result<usize>
275    where
276        K: AsRef<str>,
277    {
278        let payload = json!({
279            "key": key.as_ref(),
280            "min": min,
281            "max": max,
282        });
283
284        let response = self
285            .client
286            .send_command("sortedset.zcount", payload)
287            .await?;
288        Ok(response.get("count").and_then(|v| v.as_u64()).unwrap_or(0) as usize)
289    }
290
291    /// Get range by score (ZRANGEBYSCORE)
292    pub async fn range_by_score<K>(
293        &self,
294        key: K,
295        min: f64,
296        max: f64,
297        with_scores: bool,
298    ) -> Result<Vec<ScoredMember>>
299    where
300        K: AsRef<str>,
301    {
302        let payload = json!({
303            "key": key.as_ref(),
304            "min": min,
305            "max": max,
306            "withscores": with_scores,
307        });
308
309        let response = self
310            .client
311            .send_command("sortedset.zrangebyscore", payload)
312            .await?;
313
314        if let Some(members_val) = response.get("members") {
315            Ok(serde_json::from_value(members_val.clone()).unwrap_or_default())
316        } else {
317            Ok(Vec::new())
318        }
319    }
320
321    /// Pop minimum scored members (ZPOPMIN)
322    pub async fn pop_min<K>(&self, key: K, count: usize) -> Result<Vec<ScoredMember>>
323    where
324        K: AsRef<str>,
325    {
326        let payload = json!({
327            "key": key.as_ref(),
328            "count": count,
329        });
330
331        let response = self
332            .client
333            .send_command("sortedset.zpopmin", payload)
334            .await?;
335
336        if let Some(members_val) = response.get("members") {
337            Ok(serde_json::from_value(members_val.clone()).unwrap_or_default())
338        } else {
339            Ok(Vec::new())
340        }
341    }
342
343    /// Pop maximum scored members (ZPOPMAX)
344    pub async fn pop_max<K>(&self, key: K, count: usize) -> Result<Vec<ScoredMember>>
345    where
346        K: AsRef<str>,
347    {
348        let payload = json!({
349            "key": key.as_ref(),
350            "count": count,
351        });
352
353        let response = self
354            .client
355            .send_command("sortedset.zpopmax", payload)
356            .await?;
357
358        if let Some(members_val) = response.get("members") {
359            Ok(serde_json::from_value(members_val.clone()).unwrap_or_default())
360        } else {
361            Ok(Vec::new())
362        }
363    }
364
365    /// Remove members by rank range (ZREMRANGEBYRANK)
366    pub async fn rem_range_by_rank<K>(&self, key: K, start: i64, stop: i64) -> Result<usize>
367    where
368        K: AsRef<str>,
369    {
370        let payload = json!({
371            "key": key.as_ref(),
372            "start": start,
373            "stop": stop,
374        });
375
376        let response = self
377            .client
378            .send_command("sortedset.zremrangebyrank", payload)
379            .await?;
380        Ok(response
381            .get("removed")
382            .and_then(|v| v.as_u64())
383            .unwrap_or(0) as usize)
384    }
385
386    /// Remove members by score range (ZREMRANGEBYSCORE)
387    pub async fn rem_range_by_score<K>(&self, key: K, min: f64, max: f64) -> Result<usize>
388    where
389        K: AsRef<str>,
390    {
391        let payload = json!({
392            "key": key.as_ref(),
393            "min": min,
394            "max": max,
395        });
396
397        let response = self
398            .client
399            .send_command("sortedset.zremrangebyscore", payload)
400            .await?;
401        Ok(response
402            .get("removed")
403            .and_then(|v| v.as_u64())
404            .unwrap_or(0) as usize)
405    }
406
407    /// Compute intersection and store in destination (ZINTERSTORE)
408    ///
409    /// # Example
410    /// ```no_run
411    /// # use synap_sdk::SynapClient;
412    /// # async fn example(client: &SynapClient) -> synap_sdk::Result<()> {
413    /// // Intersect two leaderboards
414    /// let count = client.sorted_set().inter_store(
415    ///     "combined",
416    ///     vec!["board1", "board2"],
417    ///     None,
418    ///     "sum",
419    /// ).await?;
420    /// # Ok(())
421    /// # }
422    /// ```
423    pub async fn inter_store<D>(
424        &self,
425        destination: D,
426        keys: Vec<&str>,
427        weights: Option<Vec<f64>>,
428        aggregate: &str,
429    ) -> Result<usize>
430    where
431        D: AsRef<str>,
432    {
433        let payload = json!({
434            "destination": destination.as_ref(),
435            "keys": keys,
436            "weights": weights,
437            "aggregate": aggregate,
438        });
439
440        let response = self
441            .client
442            .send_command("sortedset.zinterstore", payload)
443            .await?;
444        Ok(response.get("count").and_then(|v| v.as_u64()).unwrap_or(0) as usize)
445    }
446
447    /// Compute union and store in destination (ZUNIONSTORE)
448    pub async fn union_store<D>(
449        &self,
450        destination: D,
451        keys: Vec<&str>,
452        weights: Option<Vec<f64>>,
453        aggregate: &str,
454    ) -> Result<usize>
455    where
456        D: AsRef<str>,
457    {
458        let payload = json!({
459            "destination": destination.as_ref(),
460            "keys": keys,
461            "weights": weights,
462            "aggregate": aggregate,
463        });
464
465        let response = self
466            .client
467            .send_command("sortedset.zunionstore", payload)
468            .await?;
469        Ok(response.get("count").and_then(|v| v.as_u64()).unwrap_or(0) as usize)
470    }
471
472    /// Compute difference and store in destination (ZDIFFSTORE)
473    pub async fn diff_store<D>(&self, destination: D, keys: Vec<&str>) -> Result<usize>
474    where
475        D: AsRef<str>,
476    {
477        let payload = json!({
478            "destination": destination.as_ref(),
479            "keys": keys,
480        });
481
482        let response = self
483            .client
484            .send_command("sortedset.zdiffstore", payload)
485            .await?;
486        Ok(response.get("count").and_then(|v| v.as_u64()).unwrap_or(0) as usize)
487    }
488
489    /// Get statistics
490    pub async fn stats(&self) -> Result<SortedSetStats> {
491        let payload = json!({});
492        let response = self.client.send_command("sortedset.stats", payload).await?;
493        Ok(serde_json::from_value(response).unwrap_or_default())
494    }
495}
496
497/// Statistics for sorted sets
498#[derive(Debug, Clone, Default, Serialize, Deserialize)]
499pub struct SortedSetStats {
500    pub total_keys: usize,
501    pub total_members: usize,
502    pub avg_members_per_key: f64,
503    pub memory_bytes: usize,
504}