redis_cell_rs/lib.rs
1//!This crate provides Rust bindings for the [Redis Cell](https://github.com/brandur/redis-cell) module.
2//!
3//!You can define a rate-limiting [`Policy`] in quite a few ways:
4//!```
5//!use redis_cell_rs::Policy;
6//!use std::time::Duration;
7//!
8//!const POLICY1: Policy = Policy::from_tokens_per_second(1);
9//!
10//!const POLICY2: Policy = Policy::from_tokens_per_minute(100);
11//!
12//!const POLICY3: Policy = Policy::from_tokens_per_hour(1_000);
13//!
14//!const POLICY4: Policy = Policy::from_tokens_per_day(5_000);
15//!
16//!const POLICY5: Policy = Policy::from_tokens_per_period(100, Duration::from_secs(100))
17//! .max_burst(100)
18//! .apply_tokens(2)
19//! .name("general_policy");
20//!
21//!const POLICY6: Policy = Policy::new(
22//! /* burst */ 10,
23//! /* tokens */ 100,
24//! /* period */ Duration::from_secs(100),
25//! /* apply */ 1
26//!);
27//!```
28//!
29//!A policy can now be used to crate rate-limiting request (command), which - in
30//!its turn - can be turned into a Redis command and sent over to the server using
31//!a Redis client. The response can then be converted into [`Verdict`].
32//!
33//!```no_run
34//!# use redis_cell_rs::Policy;
35//!# const POLICY1: Policy = Policy::from_tokens_per_second(1);
36//!
37//!use redis::{Cmd as RedisCmd, Client};
38//!use redis_cell_rs::{Cmd, Verdict, AllowedDetails, BlockedDetails};
39//!
40//!let cmd: RedisCmd = Cmd::new("user123", &POLICY1).into();
41//!
42//!let client = Client::open("redis://127.0.0.1/").unwrap();
43//!let mut con = client.get_connection().unwrap();
44//!
45//!let verdict: Verdict = cmd.query(&mut con).unwrap();
46//!match verdict {
47//! Verdict::Allowed(details) => {
48//! let AllowedDetails {total, remaining, reset_after, .. } = details;
49//! println!("total={}, remaining={}, reset_after={}", total, remaining, reset_after);
50//! },
51//! Verdict::Blocked(details) => {
52//! let BlockedDetails {total, remaining, reset_after, retry_after, .. } = details;
53//! println!(
54//! "total={}, remaining={}, reset_after={}, retry_after={}",
55//! total,
56//! remaining,
57//! reset_after,
58//! retry_after,
59//! );
60//! }
61//!}
62//!```
63//!
64
65// #![deny(missing_docs)]
66
67mod command;
68mod verdict;
69
70pub use command::{Cmd, Policy};
71pub use verdict::{AllowedDetails, BlockedDetails, Verdict};
72
73#[cfg(test)]
74mod tests {
75 use crate::{Cmd, Policy, Verdict};
76 use redis::Cmd as RedisCmd;
77 use std::time::Duration;
78 use testcontainers::core::IntoContainerPort as _;
79 use testcontainers::runners::AsyncRunner;
80 use testcontainers::{GenericImage, core::WaitFor};
81
82 async fn it_works_with(image: &str) {
83 let container = GenericImage::new(image, "latest")
84 .with_exposed_port(6379.tcp())
85 .with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
86 .start()
87 .await
88 .unwrap();
89 let port = container.get_host_port_ipv4(6379).await.unwrap();
90 let client = redis::Client::open(("localhost", port)).unwrap();
91 let config = redis::aio::ConnectionManagerConfig::new().set_number_of_retries(1);
92 let mut client = redis::aio::ConnectionManager::new_with_config(client, config)
93 .await
94 .unwrap();
95 let policy = Policy::new(1, 10, Duration::from_secs(60), 1);
96 let cmd: RedisCmd = Cmd::new("user123", &policy).into();
97 let verdict: Verdict = cmd.query_async(&mut client).await.unwrap();
98 dbg!(verdict);
99 }
100
101 #[tokio::test]
102 async fn it_works_with_redis() {
103 it_works_with("redis-cell").await
104 }
105
106 #[tokio::test]
107 async fn it_works_with_valkey() {
108 it_works_with("valkey-cell").await
109 }
110}