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 (accompanied by [`Key`]) can now be used to crate rate-limiting
30//!request ([`Cmd`]), which - in its turn - can be turned into a Redis command
31//!and sent over to the server using a Redis client. The response can then be
32//!converted into [`Verdict`].
33//!
34//!```no_run
35//!# use redis_cell_rs::Policy;
36//!# const POLICY1: Policy = Policy::from_tokens_per_second(1);
37//!
38//!use redis::{Cmd as RedisCmd, Client};
39//!use redis_cell_rs::{Cmd, Key, Verdict, AllowedDetails, BlockedDetails};
40//!
41//!let key = Key::pair("user123", "/api/infer");
42//!let cmd: RedisCmd = Cmd::new(&key, &POLICY1).into();
43//!
44//!let client = Client::open("redis://127.0.0.1/").unwrap();
45//!let mut con = client.get_connection().unwrap();
46//!
47//!let verdict: Verdict = cmd.query(&mut con).unwrap();
48//!match verdict {
49//! Verdict::Allowed(details) => {
50//! let AllowedDetails {total, remaining, reset_after, .. } = details;
51//! println!("total={}, remaining={}, reset_after={}", total, remaining, reset_after);
52//! },
53//! Verdict::Blocked(details) => {
54//! let BlockedDetails {total, remaining, reset_after, retry_after, .. } = details;
55//! println!(
56//! "total={}, remaining={}, reset_after={}, retry_after={}",
57//! total,
58//! remaining,
59//! reset_after,
60//! retry_after,
61//! );
62//! }
63//!}
64//!```
65//!
66
67// #![deny(missing_docs)]
68
69mod command;
70mod key;
71mod policy;
72mod verdict;
73
74pub use command::Cmd;
75pub use key::Key;
76pub use policy::Policy;
77pub use verdict::{AllowedDetails, BlockedDetails, Verdict};
78
79#[cfg(test)]
80mod tests {
81 use crate::{Cmd, Policy, Verdict};
82 use redis::Cmd as RedisCmd;
83 use std::time::Duration;
84 use testcontainers::core::IntoContainerPort as _;
85 use testcontainers::runners::AsyncRunner;
86 use testcontainers::{GenericImage, core::WaitFor};
87
88 async fn it_works_with(image: &str) {
89 let container = GenericImage::new(image, "latest")
90 .with_exposed_port(6379.tcp())
91 .with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
92 .start()
93 .await
94 .unwrap();
95 let port = container.get_host_port_ipv4(6379).await.unwrap();
96 let client = redis::Client::open(("localhost", port)).unwrap();
97 let config = redis::aio::ConnectionManagerConfig::new().set_number_of_retries(1);
98 let mut client = redis::aio::ConnectionManager::new_with_config(client, config)
99 .await
100 .unwrap();
101 let policy = Policy::new(1, 10, Duration::from_secs(60), 1);
102 let key = "user123".into();
103 let cmd: RedisCmd = Cmd::new(&key, &policy).into();
104 let verdict: Verdict = cmd.query_async(&mut client).await.unwrap();
105 dbg!(verdict);
106 }
107
108 #[tokio::test]
109 async fn it_works_with_redis() {
110 it_works_with("redis-cell").await
111 }
112
113 #[tokio::test]
114 async fn it_works_with_valkey() {
115 it_works_with("valkey-cell").await
116 }
117}