# ruvio-client
Blocking RESP2 client for [Ruvio](https://salihcantekin.github.io/ruvio/). Connecting does not send `CLIENT SETINFO` or `HELLO`. One `Client` is one TCP connection. Methods take `&mut self` and are named after the command they send. They cover every command Ruvio supports. `execute` and `execute_many` send any argument list, as `redis-cli` would.
```rust
use std::time::Duration;
use ruvio_client::Client;
let mut db = Client::connect("127.0.0.1", 6379)?;
db.set("session:ada", "hello")?;
let value = db.get_string("session:ada")?;
db.incr("visits")?;
db.expire("session:ada", Duration::from_secs(60))?;
db.hset("user:1", &[("name", "Ada"), ("plan", "pro")])?;
db.zadd("leaderboard", &[("player1", 100.0), ("player2", 80.0)])?;
let top = db.zrevrange_with_scores("leaderboard", 0, 9)?;
```
## Connecting
One client is bound to one logical database. Database 0 sends nothing. Any other index sends `SELECT` before the connect call returns.
```rust
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use ruvio_client::{Client, ClientOptions};
let mut cache = Client::connect_database("127.0.0.1", 6379, 2)?;
let mut sessions = Client::connect_ip(IpAddr::V4(Ipv4Addr::LOCALHOST), 6379, 3)?;
let mut main = Client::connect_addr(SocketAddr::from(([10, 0, 0, 5], 6379)), 0)?;
let mut secured = Client::connect_with(ClientOptions {
host: "10.0.0.5".into(),
username: Some("app".into()),
password: Some("secret".into()),
database: 4,
..ClientOptions::default()
})?;
```
`select` switches the same connection, and `into_database` does the same and returns the client. `flushdb`, `flushall`, `swapdb`, and `move_key` send `FLUSHDB`, `FLUSHALL`, `SWAPDB`, and `MOVE`.
A password is sent as `AUTH` only when `ClientOptions::password` is set. A server error reply leaves the connection usable. A broken read or write does not.
## Commands
| Strings | `get`, `get_string`, `set`, `set_with`, `set_expires_at`, `set_keep_ttl`, `set_and_get`, `mget`, `mset`, `getset`, `append`, `strlen`, `incr`, `incr_by`, `decr`, `decr_by` |
| Keys | `del`, `unlink`, `exists`, `key_type`, `rename`, `scan`, `dbsize`, `expire`, `pexpire`, `expire_at`, `ttl`, `pttl`, `persist` |
| Lists | `lpush`, `rpush`, `lpop`, `lpop_count`, `rpop`, `rpop_count`, `llen`, `lindex`, `lrange`, `ltrim`, `blpop`, `brpop` |
| Sets | `sadd`, `srem`, `sismember`, `scard`, `smembers`, `sinter`, `sunion`, `sdiff`, `sinterstore`, `sunionstore`, `sdiffstore`, `smove`, `spop`, `spop_count`, `srandmember`, `srandmember_count` |
| Hashes | `hset`, `hget`, `hdel`, `hlen`, `hgetall`, `hmget`, `hexists`, `hkeys`, `hvals`, `hincr_by`, `hsetnx`, `hstrlen`, `hscan` |
| Sorted sets | `zadd`, `zadd_with`, `zadd_incr`, `zincr_by`, `zrange`, `zrevrange`, `zrange_with_scores`, `zrevrange_with_scores`, `zrange_by_score_with_scores`, `zrem`, `zcard`, `zscore`, `zrank`, `zrevrank` |
| Bloom filters | `bf_reserve`, `bf_add`, `bf_exists` |
| Streams | `xadd`, `xadd_maxlen`, `xlen`, `xrange`, `xrevrange`, `xdel`, `xtrim_maxlen`, `xread`, `xgroup_create`, `xreadgroup`, `xgroup_destroy`, `xgroup_setid`, `xgroup_delconsumer`, `xack`, `xpending_summary`, `xpending`, `xclaim`, `xclaim_ids` |
| Transactions | `multi`, `exec`, `discard`, `watch`, `unwatch` |
| Scripts and functions | `eval`, `evalsha`, `script_load`, `script_exists`, `script_flush`, `script_kill`, `fcall`, `function_load`, `function_list`, `function_delete` |
| Pub/Sub | `publish`, `spublish`, `subscribe`, `unsubscribe`, `psubscribe`, `punsubscribe`, `ssubscribe`, `sunsubscribe`, `next_message`, `read_message` |
| Server | `ping`, `ping_message`, `info`, `config_get`, `save`, `bgsave`, `select`, `flushdb`, `flushall`, `swapdb`, `move_key`, `hello`, `client_getname`, `client_setname`, `client_tracking`, `cluster_slots`, `cluster_nodes` |
| ACL | `acl_whoami`, `acl_users`, `acl_list`, `acl_getuser`, `acl_cat`, `acl_setuser` |
`SortedSetAddOptions`, `StreamReadOptions`, `StreamPendingFilter`, and `StreamClaimOptions` carry the optional `ZADD`, `XREAD`/`XREADGROUP`, `XPENDING`, and `XCLAIM` arguments. `eval`, `evalsha`, and `fcall` take keys and arguments separately and send `keys.len()` as the key count.
Inside `MULTI` the server answers `QUEUED` instead of the typed reply, so queue commands with `execute` and read the results from `exec`:
```rust
db.watch(&["balance"])?;
db.multi()?;
db.execute(&["INCRBY", "balance", "10"])?;
db.execute(&["GET", "balance"])?;
let replies = db.exec()?; // None when a watched key changed
```
After `subscribe`, `psubscribe`, or `ssubscribe`, `next_message` blocks until the next `message`, `pmessage`, or `smessage`. `set_read_timeout` bounds that wait.
## Tests
`cargo test` runs the unit tests. `RUVIO_TEST_ADDR=127.0.0.1:16390 cargo test` also runs `tests/live.rs` against a server on that address. Those tests use databases 0 to 7 and flush them.
## 0.2.0
- Typed methods for every command Ruvio supports.
- `ClientOptions::database`, `connect_database`, `connect_ip`, `connect_addr`, `select`, `into_database`, and `database`.
- `set_expires_at` (`PXAT`), `set_keep_ttl` (`KEEPTTL`), `set_and_get` (`GET`), and `ping_message`.
- `expire` sends `PEXPIRE` when the duration is not whole seconds. It used to round up to the next second.
- `RespValue::as_integer`, `as_array`, and `is_null`.
- The minimum Rust version is 1.87.