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}