diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..2cfb404 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,24 @@ +name: gh.build +on: [push] +jobs: + build: + runs-on: ubuntu-latest + services: + redis: + image: redis + ports: + - 6379:6379 + options: --entrypoint redis-server + steps: + - uses: actions/checkout@v1 + - name: install-beta + run: rustup toolchain install beta + - name: use-beta + run: rustup default beta + - name: build + run: cargo build + - name: test + run: cargo test --features kramer-io + env: + REDIS_HOST: localhost + REDIS_PORT: ${{ job.services.redis.ports[6379] }} diff --git a/.gitignore b/.gitignore index 6936990..53eaa21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /target **/*.rs.bk -Cargo.lock diff --git a/.todo.md b/.todo.md new file mode 100644 index 0000000..e6a6098 --- /dev/null +++ b/.todo.md @@ -0,0 +1,278 @@ +# Milestone: 0.1.0 + +| Command | Introduced | Use | +| :--- | :--- | :-- | +| `append` | [`9c23bf7`] | `Command::Strings(StringCommand::Append("seinfeld", "kramer")))` | +| `blpop` | [`d27df86`] | `ListCommand::Pop(Side::Left, "seinfeld", Some((None, 10)))` | +| `brpop` | [`d27df86`] | `ListCommand::Pop(Side::Right, "seinfeld", Some((None, 10)))` | +| `decr` | [`d27df86`] | `Command::Decr("seinfeld", 1)` | +| `decrby` | [`d51737e`] | `Command::Decr("seinfeld", 2)` | +| `del` | [`1a15a9e`] | `Command::Del(Arity::One("seinfeld"))` | +| `echo` | [`a851137`] | `Command::Echo("seinfeld")` | +| `exists` | [`1a15a9e`] | `Command::Exists("seinfeld")` | +| `get` | [`9c23bf7`] | `StringCommand::Get(Arity::One("seinfeld"))` | +| `hdel` | [`cd15162`] | `HashCommand::Del("seinfeld", "name", None))` | +| `hexists` | [`220d748`] | `HashCommand::Exists("seinfeld", "name"))` | +| `hget` | [`220d748`] | `HashCommand::Get("seinfeld", Some(Arity::One("name"))))` | +| `hgetall` | [`220d748`] | `HashCOmmand::Get("Seinfeld", None))` | +| `hincrby` | [`9ccd5fe`] | `HashCommand::Incr("counters", "episodes", 10)` | +| `hkeys` | [`220d748`] | `HashCommand::Keys("seinfeld")` | +| `hlen` | [`220d748`] | `HashCommand::Len("seinfeld")` | +| `hmget` | [`220d748`] | `HashCommand::Get("seinfeld", Some(Arity::Many(vec!["name", "friend"])))` | +| `hset` | [`9e08436`] | `HashCommand::Set("seinfeld", Arity::One(("name", "kramer")), Insertion::Always)` | +| `hsetnx` | [`9e08436`] | `HashCommand::Set("seinfeld", Arity::One(("name", "kramer")), Insertion::IfNotExists)` | +| `hstrlen` | [`9ccd5fe`] | `HashCommand::StrLen("seinfeld", "name")` | +| `hvals` | [`9ccd5fe`] | `HashCommand::Vals("seinfeld")` | +| `incr` | [`ea58902`] | `StringCommand::Incr("episodes", 1)` | +| `incrby` | [`ea58902`] | `StringCommand::Incr("episodes", 10)` | +| `keys` | [`1a15a9e`] | `Command::Keys("*")` | +| `lindex` | [`ea58902`] | `Command::List(ListCommand::Index("episodes", 1))` | +| `linsert` | [`ea58902`] | `Command::List(ListCommand::Insert("episodes", Side::Left, "10", "100"))` | +| `llen` | [`1a15a9e`] | `ListCommand::Len(key))` | +| `lpop` | [`1a15a9e`] | `ListCommand::Pop(Side::Left, key, None))` | +| `lpush` | [`1a15a9e`] | `ListCommand::Push((Side::Left, Insertion::Always), key, Arity::One("kramer")))` | +| `lpushx` | [`7b4f430`] | `ListCommand::Push((Side::Left, Insertion::IfExists), key, Arity::One("kramer")))` | +| `lrange` | [`1a15a9e`] | `ListCommand::Range("seinfeld", 0, -1)` | +| `lrem` | [`ea58902`] | `Command::List(ListCommand::Rem("episodes", "10", 100))` | +| `lset` | [`ea58902`] | `Command::List(ListCommand::Set("episodes", 1, "pilot"))` | +| `ltrim` | [`ea58902`] | `Command::List(ListCommand::Trim("episodes", 0, 10))` | +| `mget` | [`1a15a9e`] | `StringCommand::Get(Arity::Many(vec!["seinfeld", "peaky"]))` | +| `mset` | [`8e6cab7`] | `StringCommand::Set(Arity::Many(vec![("name", "jerry")]), None, Insertion::Always)` | +| `mset` | [`8e6cab7`] | `StringCommand::Set(Arity::Many(vec![("name", "jerry")]), None, Insertion::IfNotExists)` | +| `rpop` | [`1a15a9e`] | `ListCommand::Pop(Side::Right, key, None)` | +| `rpush` | [`1a15a9e`] | `ListCommand::Push((Side::Right, Insertion::Always), key, Arity::One("kramer")))` | +| `rpushx` | [`1a15a9e`] | `ListCommand::Push((Side::Right, Insertion::IfExists), key, Arity::One("kramer")))` | +| `set` | [`1a15a9e`] | `StringCommand::Set(Arity::One((key, "kramer")), None, Insertion::Always)` | + +# Milestone: 0.2.0 + +- [ ] auth +- [ ] bitcount +- [ ] bitfield +- [ ] bitop +- [ ] bitpos +- [ ] bgrewriteaof +- [ ] bgsave +- [ ] brpoplpush +- [ ] bzpopmin +- [ ] bzpopmax +- [ ] discard +- [ ] dump +- [ ] eval +- [ ] evalsha +- [ ] exec +- [ ] expire +- [ ] expireat +- [ ] flushall +- [ ] flushdb +- [ ] geoadd +- [ ] geohash +- [ ] geopos +- [ ] geodist +- [ ] georadius +- [ ] georadiusbymember +- [ ] getbit +- [ ] getrange +- [ ] getset +- [ ] hincrbyfloat +- [ ] incrbyfloat +- [ ] info +- [ ] lastsave +- [ ] migrate +- [ ] move +- [ ] multi +- [ ] object +- [ ] persist +- [ ] pexpire +- [ ] pexpireat +- [ ] pfadd +- [ ] pfcount +- [ ] pfmerge +- [ ] ping +- [ ] psetex +- [ ] psubscribe +- [ ] pubsub +- [ ] pttl +- [ ] publish +- [ ] punsubscribe +- [ ] quit +- [ ] randomkey +- [ ] readonly +- [ ] readwrite +- [ ] rename +- [ ] renamenx +- [ ] restore +- [ ] role +- [ ] rpoplpush +- [ ] sadd +- [ ] save +- [ ] scard +- [ ] sdiff +- [ ] sdiffstore +- [ ] select +- [ ] setbit +- [ ] setrange +- [ ] shutdown +- [ ] sinter +- [ ] sinterstore +- [ ] sismember +- [ ] slaveof +- [ ] replicaof +- [ ] slowlog +- [ ] smembers +- [ ] smove +- [ ] sort +- [ ] spop +- [ ] srandmember +- [ ] srem +- [ ] strlen +- [ ] subscribe +- [ ] sunion +- [ ] sunionstore +- [ ] swapdb +- [ ] sync +- [ ] psync +- [ ] time +- [ ] touch +- [ ] ttl +- [ ] type +- [ ] unsubscribe +- [ ] unlink +- [ ] unwatch +- [ ] wait +- [ ] watch +- [ ] zadd +- [ ] zcard +- [ ] zcount +- [ ] zincrby +- [ ] zinterstore +- [ ] zlexcount +- [ ] zpopmax +- [ ] zpopmin +- [ ] zrange +- [ ] zrangebylex +- [ ] zrevrangebylex +- [ ] zrangebyscore +- [ ] zrank +- [ ] zrem +- [ ] zremrangebylex +- [ ] zremrangebyrank +- [ ] zremrangebyscore +- [ ] zrevrange +- [ ] zrevrangebyscore +- [ ] zrevrank +- [ ] zscore +- [ ] zunionstore +- [ ] scan +- [ ] sscan +- [ ] hscan +- [ ] zscan +- [ ] xinfo +- [ ] xadd +- [ ] xtrim +- [ ] xdel +- [ ] xrange +- [ ] xrevrange +- [ ] xlen +- [ ] xread +- [ ] xgroup +- [ ] xreadgroup +- [ ] xack +- [ ] xclaim +- [ ] xpending + +# Milestone: 0.X.0 + +The following commands are not part of the roadmap for this library. + +- [ ] command +- [ ] command count +- [ ] command getkeys +- [ ] command info +- [ ] config get +- [ ] config rewrite +- [ ] config set +- [ ] config resetstat +- [ ] dbsize +- [ ] debug object +- [ ] debug segfault +- [ ] client id +- [ ] client kill +- [ ] client list +- [ ] client getname +- [ ] client pause +- [ ] client reply +- [ ] client setname +- [ ] client unblock +- [ ] cluster addslots +- [ ] cluster bumpepoch +- [ ] cluster count-failure-reports +- [ ] cluster countkeysinslot +- [ ] cluster delslots +- [ ] cluster failover +- [ ] cluster flushslots +- [ ] cluster forget +- [ ] cluster getkeysinslot +- [ ] cluster info +- [ ] cluster keyslot +- [ ] cluster meet +- [ ] cluster myid +- [ ] cluster nodes +- [ ] cluster replicate +- [ ] cluster reset +- [ ] cluster saveconfig +- [ ] cluster set-config-epoch +- [ ] cluster setslot +- [ ] cluster slaves +- [ ] cluster replicas +- [ ] cluster slots +- [ ] script debug +- [ ] script exists +- [ ] script flush +- [ ] script kill +- [ ] script load +- [ ] latency doctor +- [ ] latency graph +- [ ] latency history +- [ ] latency latest +- [ ] latency reset +- [ ] latency help +- [ ] lolwut +- [ ] memory doctor +- [ ] memory help +- [ ] memory malloc-stats +- [ ] memory purge +- [ ] memory stats +- [ ] memory usage +- [ ] module list +- [ ] module load +- [ ] module unload +- [ ] monitor + +# Exceptions + +- [ ] `setex` - unlikely to implement; can be accomplished with `set` +- [ ] `setnx` - unlikely to implement; can be accomplished with `set` +- [ ] `hmset` - `hset` can do many + +> This list was generated by running the following script in the chrome developer tools on the [command][command-li] +> list page of the official redis website: +> +> var items = Array.from(document.querySelectorAll('#commands li[data-name]')); +> copy(items.map(li => li.getAttribute('data-name')).join('\n')) + +[command-li]: https://redis.io/commands + + +[`1a15a9e`]: https://github.com/sizethree/kramer/commit/1a15a9eb89f0c5a23bb4ad9e52f0082997e017a0 +[`220d748`]: https://github.com/sizethree/kramer/commit/220d748 +[`7b4f430`]: https://github.com/sizethree/kramer/commit/7b4f430 +[`8e6cab7`]: https://github.com/sizethree/kramer/commit/8e6cab7076527f00efc59d39b3c5258e6cd3f404 +[`9c23bf7`]: https://github.com/sizethree/kramer/commit/9c23bf7a4282a4803b96a41824d47a60b3c176b5 +[`9ccd5fe`]: https://github.com/sizethree/kramer/commit/9ccd5fe6f17aa6a1839fafb75859ff1b3ad3a49d +[`9e08436`]: https://github.com/sizethree/kramer/commit/9e08436 +[`a851137`]: https://github.com/sizethree/kramer/commit/a8511379c77b52f78e85e3f2d5a4dd94ef1cd84e +[`cd15162`]: https://github.com/sizethree/kramer/commit/cd1516296c31707bb9bc5878b3b323b1629ba692 +[`d27df86`]: https://github.com/sizethree/kramer/commit/d27df866b9c62ce47b18980354b3fc145e0fb3a2 +[`d51737e`]: https://github.com/sizethree/kramer/commit/d51737e2911d883bf26d1659fb3b6be6057594fb +[`ea58902`]: https://github.com/sizethree/kramer/commit/ea58902003a13a951d60d37b7b7705082af6ee36 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0847be0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,4 @@ +## Contributing + +This project is entirely open to help from the software community. +All pull requests and issues are welcome and be considered honestly. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..41c7ad5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,402 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "nodrop 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "async-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-core-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "async-std" +version = "0.99.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "async-macros 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "async-task 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-channel 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-deque 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-io-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-timer 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "kv-log-macro 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)", + "mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project-lite 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "async-task" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "crossbeam-channel" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-epoch 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arrayvec 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "memoffset 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crossbeam-utils" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures-core-preview" +version = "0.3.0-alpha.19" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures-io-preview" +version = "0.3.0-alpha.19" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures-timer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-core-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "futures-util-preview" +version = "0.3.0-alpha.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-core-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hermit-abi" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "kramer" +version = "0.1.0" +dependencies = [ + "async-std 0.99.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memchr" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memoffset" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mio" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mio-uds" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "miow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "net2" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "num_cpus" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "hermit-abi 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "pin-utils" +version = "0.1.0-alpha.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "scopeguard" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum arrayvec 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +"checksum async-macros 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e421d59b24c1feea2496e409b3e0a8de23e5fc130a2ddc0b012e551f3b272bba" +"checksum async-std 0.99.11 (registry+https://github.com/rust-lang/crates.io-index)" = "a30de63082eec5f5a04bcb32efff6b4564579b8982487ebc61641d74c9b00e8d" +"checksum async-task 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de6bd58f7b9cc49032559422595c81cbfcf04db2f2133592f70af19e258a1ced" +"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +"checksum crossbeam-channel 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c8ec7fcd21571dc78f96cc96243cab8d8f035247c3efd16c687be154c3fa9efa" +"checksum crossbeam-deque 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b18cd2e169ad86297e6bc0ad9aa679aee9daa4f19e8163860faf7c164e4f5a71" +"checksum crossbeam-epoch 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fedcd6772e37f3da2a9af9bf12ebe046c0dfe657992377b4df982a2b54cd37a9" +"checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" +"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +"checksum futures-core-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)" = "b35b6263fb1ef523c3056565fa67b1d16f0a8604ff12b11b08c25f28a734c60a" +"checksum futures-io-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)" = "f4914ae450db1921a56c91bde97a27846287d062087d4a652efc09bb3a01ebda" +"checksum futures-timer 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2879f3aa8fd2f60d17ede13349e11d0c132d0daa1b44e061f133f8928ddfaeea" +"checksum futures-util-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)" = "5ce968633c17e5f97936bd2797b6e38fb56cf16a7422319f7ec2e30d3c470e8d" +"checksum hermit-abi 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "307c3c9f937f38e3534b1d6447ecf090cafcc9744e4a6360e8b037b2cf5af120" +"checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +"checksum kv-log-macro 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c54d9f465d530a752e6ebdc217e081a7a614b48cb200f6f0aee21ba6bc9aabb" +"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +"checksum libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)" = "1a31a0627fdf1f6a39ec0dd577e101440b7db22672c0901fe00a9a6fbb5c24e8" +"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" +"checksum memoffset 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce6075db033bbbb7ee5a0bbd3a3186bbae616f57fb001c485c7ff77955f8177f" +"checksum mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)" = "83f51996a3ed004ef184e16818edc51fadffe8e7ca68be67f9dee67d84d0ff23" +"checksum mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125" +"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +"checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" +"checksum nodrop 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +"checksum num_cpus 1.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "155394f924cdddf08149da25bfb932d226b4a593ca7468b08191ff6335941af5" +"checksum pin-project-lite 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4f1f61fedcfa3b402e157c23e235b1ab6c30d52c219492c63504059e2bdd3408" +"checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" +"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +"checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" +"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" diff --git a/Cargo.toml b/Cargo.toml index 59c9a73..f4e8e7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,14 @@ name = "kramer" version = "0.1.0" authors = ["Danny Hadley "] edition = "2018" +license = "MIT" +homepage = "https://github.com/sizethree/kramer" +repository = "https://github.com/sizethree/kramer.git" +description = "Redis communication" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies.async-std] +version = "^0.99" +optional = true -[dependencies] +[features] +kramer-io = ["async-std"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..26e3661 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Danny Hadley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index e69de29..194c715 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,21 @@ +## kramer + +[![ci.img]][ci.url] [![docs.img]][docs.url] + +An implementation of the [redis protocol specification][redis] with an execution helper using the +[`TcpStream`][tcp-stream] provided by [async-std]. + + +For a list of supported commands see [todo.md](/.todo.md). + +## Contributing + +See [CONTRIBUTING](/CONTRIBUTING.md). + +[ci.img]: https://github.com/sizethree/kramer/workflows/gh.build/badge.svg?flat +[ci.url]: https://github.com/sizethree/kramer/actions?workflow=gh.build +[redis]: https://redis.io/topics/protocol +[async-std]: https://github.com/async-rs/async-std +[tcp-stream]: https://docs.rs/async-std/0.99.11/async_std/net/struct.TcpStream.html +[docs.img]: https://docs.rs/kramer/badge.svg +[docs.url]: https://docs.rs/kramer diff --git a/rustfmt.toml b/rustfmt.toml index e8fd890..9e986a4 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,2 +1,3 @@ tab_spaces = 2 edition = "2018" +max_width = 120 diff --git a/src/hashes.rs b/src/hashes.rs new file mode 100644 index 0000000..014bf41 --- /dev/null +++ b/src/hashes.rs @@ -0,0 +1,133 @@ +use crate::modifiers::{format_bulk_string, Arity, Insertion}; + +#[derive(Debug)] +pub enum HashCommand +where + S: std::fmt::Display, +{ + Del(S, Arity), + Set(S, Arity<(S, S)>, Insertion), + Get(S, Option>), + StrLen(S, S), + Len(S), + Incr(S, S, i64), + Keys(S), + Vals(S), + Exists(S, S), +} + +impl std::fmt::Display for HashCommand { + fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + HashCommand::StrLen(key, field) => { + let tail = format!("{}{}", format_bulk_string(key), format_bulk_string(field)); + write!(formatter, "*3\r\n$7\r\nHSTRLEN\r\n{}", tail) + } + HashCommand::Incr(key, field, amt) => { + let tail = format!( + "{}{}{}", + format_bulk_string(key), + format_bulk_string(field), + format_bulk_string(amt) + ); + write!(formatter, "*4\r\n$7\r\nHINCRBY\r\n{}", tail) + } + HashCommand::Vals(key) => write!(formatter, "*2\r\n$5\r\nHVALS\r\n{}", format_bulk_string(key)), + HashCommand::Keys(key) => write!(formatter, "*2\r\n$5\r\nHKEYS\r\n{}", format_bulk_string(key)), + HashCommand::Len(key) => write!(formatter, "*2\r\n$4\r\nHLEN\r\n{}", format_bulk_string(key)), + HashCommand::Get(key, None) => write!(formatter, "*2\r\n$7\r\nHGETALL\r\n{}", format_bulk_string(key)), + HashCommand::Get(key, Some(Arity::One(field))) => write!( + formatter, + "*3\r\n$4\r\nHGET\r\n{}{}", + format_bulk_string(key), + format_bulk_string(field) + ), + HashCommand::Get(key, Some(Arity::Many(fields))) => { + let len = fields.len(); + + // Awkward; Get("foo", Some(Arity::Many(vec![]))) == Get("foo", None) + if len == 0 { + let formatted = format!("{}", key); + return write!(formatter, "{}", HashCommand::Get(formatted, None)); + } + + let tail = fields.iter().map(format_bulk_string).collect::(); + + write!( + formatter, + "*{}\r\n$5\r\nHMGET\r\n{}{}", + 2 + len, + format_bulk_string(key), + tail + ) + } + HashCommand::Exists(key, field) => write!( + formatter, + "*3\r\n$7\r\nHEXISTS\r\n{}{}", + format_bulk_string(key), + format_bulk_string(field) + ), + HashCommand::Set(key, Arity::One((field, value)), Insertion::IfNotExists) => write!( + formatter, + "*4\r\n$6\r\nHSETNX\r\n{}{}{}", + format_bulk_string(key), + format_bulk_string(field), + format_bulk_string(value) + ), + HashCommand::Set(key, Arity::Many(mappings), Insertion::IfNotExists) => { + let count = mappings.len(); + let tail = mappings + .iter() + .map(|(k, v)| format!("{}{}", format_bulk_string(k), format_bulk_string(v))) + .collect::(); + + write!( + formatter, + "*{}\r\n$6\r\nHSETNX\r\n{}{}", + 2 + (count * 2), + format_bulk_string(key), + tail + ) + } + HashCommand::Set(key, Arity::One((field, value)), _) => write!( + formatter, + "*4\r\n$4\r\nHSET\r\n{}{}{}", + format_bulk_string(key), + format_bulk_string(field), + format_bulk_string(value) + ), + HashCommand::Set(key, Arity::Many(mappings), _) => { + let count = mappings.len(); + let tail = mappings + .iter() + .map(|(k, v)| format!("{}{}", format_bulk_string(k), format_bulk_string(v))) + .collect::(); + + write!( + formatter, + "*{}\r\n$4\r\nHSET\r\n{}{}", + 2 + (count * 2), + format_bulk_string(key), + tail + ) + } + HashCommand::Del(key, Arity::One(field)) => write!( + formatter, + "*3\r\n$4\r\nHDEL\r\n{}{}", + format_bulk_string(key), + format_bulk_string(field) + ), + HashCommand::Del(key, Arity::Many(fields)) => { + let count = fields.len(); + let bits = fields.iter().map(format_bulk_string).collect::(); + write!( + formatter, + "*{}\r\n$4\r\nHDEL\r\n{}{}", + count + 2, + format_bulk_string(key), + bits + ) + } + } + } +} diff --git a/src/io.rs b/src/io.rs new file mode 100644 index 0000000..17c498b --- /dev/null +++ b/src/io.rs @@ -0,0 +1,153 @@ +#[cfg(feature = "kramer-io")] +extern crate async_std; +#[cfg(feature = "kramer-io")] +use async_std::net::TcpStream; +#[cfg(feature = "kramer-io")] +use async_std::prelude::*; + +use std::io::{Error, ErrorKind}; + +#[derive(Debug)] +pub enum ResponseLine { + Array(usize), + SimpleString(String), + Error(String), + Integer(i64), + BulkString(usize), + Null, +} + +#[derive(Debug, PartialEq)] +pub enum ResponseValue { + Empty, + String(String), + Integer(i64), +} + +#[derive(Debug, PartialEq)] +pub enum Response { + Array(Vec), + Item(ResponseValue), + Error, +} + +#[cfg(feature = "kramer-io")] +fn read_line_size(line: String) -> Result, Error> { + match line.split_at(1).1 { + "-1" => Ok(None), + value => value + .parse::() + .map_err(|e| { + Error::new( + ErrorKind::Other, + format!("invalid array length value '{}': {}", line.as_str(), e), + ) + }) + .map(|v| Some(v)), + } +} + +#[cfg(feature = "kramer-io")] +fn readline(result: Option>) -> Result { + let line = result.ok_or_else(|| Error::new(ErrorKind::Other, "no line to work with"))??; + + match line.bytes().next() { + Some(b'*') => match read_line_size(line)? { + None => Ok(ResponseLine::Null), + Some(size) => Ok(ResponseLine::Array(size)), + }, + Some(b'$') => match read_line_size(line)? { + Some(size) => Ok(ResponseLine::BulkString(size)), + None => Ok(ResponseLine::Null), + }, + Some(b'-') => Ok(ResponseLine::Error(line)), + Some(b'+') => Ok(ResponseLine::SimpleString(String::from(line.split_at(1).1))), + Some(b':') => { + let (_, rest) = line.split_at(1); + rest + .parse::() + .map_err(|e| Error::new(ErrorKind::Other, format!("{:?}", e))) + .and_then(|v| Ok(ResponseLine::Integer(v))) + } + Some(unknown) => Err(Error::new( + ErrorKind::Other, + format!("invalid message byte leader: {}", unknown), + )), + None => Err(Error::new( + ErrorKind::Other, + "empty line in response, unable to determine type", + )), + } +} + +#[cfg(feature = "kramer-io")] +pub async fn read(connection: C) -> Result +where + C: async_std::io::Read + std::marker::Unpin, +{ + let mut lines = async_std::io::BufReader::new(connection).lines(); + + match readline(lines.next().await) { + Ok(ResponseLine::Array(size)) => { + let mut store = Vec::with_capacity(size); + + if size == 0 { + return Ok(Response::Array(vec![])); + } + + while let Ok(kind) = readline(lines.next().await) { + match kind { + ResponseLine::BulkString(size) => match lines.next().await { + Some(Ok(bulky)) if bulky.len() == size => { + store.push(ResponseValue::String(bulky)); + } + _ => break, + }, + _ => break, + } + + if store.len() >= size { + return Ok(Response::Array(store)); + } + } + + Ok(Response::Array(store)) + } + Ok(ResponseLine::BulkString(size)) => { + if size < 1 { + return Ok(Response::Item(ResponseValue::Empty)); + } + + let out = lines + .next() + .await + .ok_or_else(|| Error::new(ErrorKind::Other, "no line to work with"))??; + + Ok(Response::Item(ResponseValue::String(out))) + } + Ok(ResponseLine::Null) => Ok(Response::Item(ResponseValue::Empty)), + Ok(ResponseLine::SimpleString(simple)) => Ok(Response::Item(ResponseValue::String(simple))), + Ok(ResponseLine::Integer(value)) => Ok(Response::Item(ResponseValue::Integer(value))), + Ok(ResponseLine::Error(e)) => Err(Error::new(ErrorKind::Other, e)), + Err(e) => Err(e), + } +} + +#[cfg(feature = "kramer-io")] +pub async fn execute(mut connection: C, message: S) -> Result +where + S: std::fmt::Display, + C: async_std::io::Write + async_std::io::Read + std::marker::Unpin, +{ + write!(connection, "{}", message).await?; + read(connection).await +} + +#[cfg(feature = "kramer-io")] +pub async fn send(addr: &str, message: S) -> Result +where + S: std::fmt::Display, +{ + let mut stream = TcpStream::connect(addr).await?; + execute(&mut stream, message).await +} diff --git a/src/lib.rs b/src/lib.rs old mode 100644 new mode 100755 index 74be4bb..38d5efd --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,644 @@ +//! An implementation of the [redis protocol specification][redis] with an execution helper using +//! the [`TcpStream`][tcp-stream] provided by [async-std]. +//! +//! ## Example +//! +//! ``` +//! use kramer::{Command, StringCommand, Arity, Insertion}; +//! use std::env::{var}; +//! use std::io::prelude::*; +//! +//! fn get_redis_url() -> String { +//! let host = var("REDIS_HOST").unwrap_or(String::from("0.0.0.0")); +//! let port = var("REDIS_PORT").unwrap_or(String::from("6379")); +//! format!("{}:{}", host, port) +//! } +//! +//! fn main() -> Result<(), Box> { +//! let url = get_redis_url(); +//! let cmd = Command::Keys("*"); +//! let mut stream = std::net::TcpStream::connect(url)?; +//! write!(stream, "{}", cmd)?; +//! write!(stream, "{}", StringCommand::Set(Arity::One(("name", "kramer")), None, Insertion::Always))?; +//! Ok(()) +//! } +//! ``` +//! +//! [redis]: https://redis.io/topics/protocol +//! [async-std]: https://github.com/async-rs/async-std +//! [tcp-stream]: https://docs.rs/async-std/0.99.11/async_std/net/struct.TcpStream.html +#[cfg(feature = "kramer-io")] +mod io; +#[cfg(feature = "kramer-io")] +pub use io::{execute, send, Response, ResponseValue}; + +mod modifiers; +use modifiers::format_bulk_string; +pub use modifiers::{Arity, Insertion, Side}; + +mod lists; +pub use lists::ListCommand; + +mod strings; +pub use strings::StringCommand; + +mod hashes; +pub use hashes::HashCommand; + +#[derive(Debug)] +pub enum Command +where + S: std::fmt::Display, +{ + Keys(S), + Del(Arity), + Exists(Arity), + List(ListCommand), + Strings(StringCommand), + Hashes(HashCommand), + Echo(S), +} + +impl std::fmt::Display for Command { + fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Command::Echo(value) => write!(formatter, "*2\r\n$4\r\nECHO\r\n{}", format_bulk_string(value)), + Command::Keys(value) => write!(formatter, "*2\r\n$4\r\nKEYS\r\n{}", format_bulk_string(value)), + Command::Exists(Arity::Many(values)) => { + let len = values.len(); + let right = values.iter().map(format_bulk_string).collect::(); + write!(formatter, "*{}\r\n$6\r\nEXISTS\r\n{}", len + 1, right) + } + Command::Exists(Arity::One(value)) => write!(formatter, "*2\r\n$6\r\nEXISTS\r\n{}", format_bulk_string(value)), + Command::Del(Arity::One(value)) => write!(formatter, "*2\r\n$3\r\nDEL\r\n{}", format_bulk_string(value)), + Command::Del(Arity::Many(values)) => { + let len = values.len(); + let right = values.iter().map(format_bulk_string).collect::(); + write!(formatter, "*{}\r\n$3\r\nDEL\r\n{}", len + 1, right) + } + Command::List(list_command) => write!(formatter, "{}", list_command), + Command::Strings(string_command) => write!(formatter, "{}", string_command), + Command::Hashes(hash_command) => write!(formatter, "{}", hash_command), + } + } +} + #[cfg(test)] -mod tests { +mod fmt_tests { + use super::{Arity, Command, HashCommand, Insertion, ListCommand, Side, StringCommand}; + use std::io::Write; + + #[test] + fn test_keys_fmt() { + assert_eq!( + format!("{}", Command::Keys(String::from("*"))), + "*2\r\n$4\r\nKEYS\r\n$1\r\n*\r\n" + ); + } + + #[test] + fn test_llen_fmt() { + assert_eq!( + format!("{}", Command::List(ListCommand::Len("kramer"))), + "*2\r\n$4\r\nLLEN\r\n$6\r\nkramer\r\n" + ); + } + + #[test] + fn test_lpush_fmt() { + assert_eq!( + format!( + "{}", + Command::List(ListCommand::Push( + (Side::Left, Insertion::Always), + "seinfeld", + Arity::Many(vec!["kramer"]), + )) + ), + "*3\r\n$5\r\nLPUSH\r\n$8\r\nseinfeld\r\n$6\r\nkramer\r\n" + ); + } + + #[test] + fn test_rpush_fmt() { + assert_eq!( + format!( + "{}", + Command::List(ListCommand::Push( + (Side::Right, Insertion::Always), + "seinfeld", + Arity::Many(vec!["kramer"]), + )) + ), + "*3\r\n$5\r\nRPUSH\r\n$8\r\nseinfeld\r\n$6\r\nkramer\r\n" + ); + } + + #[test] + fn test_rpushx_fmt() { + assert_eq!( + format!( + "{}", + Command::List(ListCommand::Push( + (Side::Right, Insertion::IfExists), + "seinfeld", + Arity::Many(vec!["kramer"]), + )) + ), + "*3\r\n$6\r\nRPUSHX\r\n$8\r\nseinfeld\r\n$6\r\nkramer\r\n" + ); + } + + #[test] + fn test_lpushx_fmt() { + assert_eq!( + format!( + "{}", + Command::List(ListCommand::Push( + (Side::Left, Insertion::IfExists), + "seinfeld", + Arity::Many(vec!["kramer"]), + )) + ), + "*3\r\n$6\r\nLPUSHX\r\n$8\r\nseinfeld\r\n$6\r\nkramer\r\n" + ); + } + + #[test] + fn test_rpush_fmt_multi() { + assert_eq!( + format!( + "{}", + Command::List(ListCommand::Push( + (Side::Right, Insertion::Always), + "seinfeld", + Arity::Many(vec!["kramer", "jerry"]), + )) + ), + "*4\r\n$5\r\nRPUSH\r\n$8\r\nseinfeld\r\n$6\r\nkramer\r\n$5\r\njerry\r\n" + ); + } + + #[test] + fn test_rpop_fmt() { + assert_eq!( + format!("{}", Command::List(ListCommand::Pop(Side::Right, "seinfeld", None))), + "*2\r\n$4\r\nRPOP\r\n$8\r\nseinfeld\r\n" + ); + } + + #[test] + fn test_lpop_fmt() { + assert_eq!( + format!("{}", Command::List(ListCommand::Pop(Side::Left, "seinfeld", None))), + "*2\r\n$4\r\nLPOP\r\n$8\r\nseinfeld\r\n" + ); + } + + #[test] + fn test_lrange_fmt() { + assert_eq!( + format!("{}", Command::List(ListCommand::Range("seinfeld", 0, -1))), + "*4\r\n$6\r\nLRANGE\r\n$8\r\nseinfeld\r\n$1\r\n0\r\n$2\r\n-1\r\n" + ); + } + + #[test] + fn test_brpop_timeout_fmt() { + assert_eq!( + format!( + "{}", + Command::List(ListCommand::Pop(Side::Right, "seinfeld", Some((None, 10)))) + ), + "*3\r\n$5\r\nBRPOP\r\n$8\r\nseinfeld\r\n$2\r\n10\r\n" + ); + } + + #[test] + fn test_brpop_timeout_multi_fmt() { + assert_eq!( + format!( + "{}", + Command::List(ListCommand::Pop( + Side::Right, + "seinfeld", + Some((Some(Arity::One("derry-girls")), 10)) + )) + ), + "*4\r\n$5\r\nBRPOP\r\n$8\r\nseinfeld\r\n$11\r\nderry-girls\r\n$2\r\n10\r\n" + ); + } + + #[test] + fn test_brpop_timeout_multi_many_fmt() { + assert_eq!( + format!( + "{}", + Command::List(ListCommand::Pop( + Side::Right, + "seinfeld", + Some((Some(Arity::Many(vec!["derry-girls", "creek"])), 10)) + )) + ), + "*5\r\n$5\r\nBRPOP\r\n$8\r\nseinfeld\r\n$11\r\nderry-girls\r\n$5\r\ncreek\r\n$2\r\n10\r\n" + ); + } + + #[test] + fn test_blpop_timeout_fmt() { + assert_eq!( + format!( + "{}", + Command::List(ListCommand::Pop(Side::Left, "seinfeld", Some((None, 10)))) + ), + "*3\r\n$5\r\nBLPOP\r\n$8\r\nseinfeld\r\n$2\r\n10\r\n" + ); + } + + #[test] + fn test_blpop_timeout_multi_fmt() { + assert_eq!( + format!( + "{}", + Command::List(ListCommand::Pop( + Side::Left, + "seinfeld", + Some((Some(Arity::One("derry-girls")), 10)) + )) + ), + "*4\r\n$5\r\nBLPOP\r\n$8\r\nseinfeld\r\n$11\r\nderry-girls\r\n$2\r\n10\r\n" + ); + } + + #[test] + fn test_blpop_timeout_multi_many_fmt() { + assert_eq!( + format!( + "{}", + Command::List(ListCommand::Pop( + Side::Left, + "seinfeld", + Some((Some(Arity::Many(vec!["derry-girls", "creek"])), 10)) + )) + ), + "*5\r\n$5\r\nBLPOP\r\n$8\r\nseinfeld\r\n$11\r\nderry-girls\r\n$5\r\ncreek\r\n$2\r\n10\r\n" + ); + } + + #[test] + fn test_del_fmt() { + assert_eq!( + format!("{}", Command::Del(Arity::Many(vec!["kramer"]))), + "*2\r\n$3\r\nDEL\r\n$6\r\nkramer\r\n" + ); + } + + #[test] + fn test_del_fmt_multi() { + assert_eq!( + format!("{}", Command::Del(Arity::Many(vec!["kramer", "jerry"]))), + "*3\r\n$3\r\nDEL\r\n$6\r\nkramer\r\n$5\r\njerry\r\n" + ); + } + + #[test] + fn test_set_fmt() { + assert_eq!( + format!( + "{}", + Command::Strings(StringCommand::Set( + Arity::One(("seinfeld", "kramer")), + None, + Insertion::Always + )) + ), + "*3\r\n$3\r\nSET\r\n$8\r\nseinfeld\r\n$6\r\nkramer\r\n" + ); + } + + #[test] + fn test_set_fmt_duration() { + assert_eq!( + format!( + "{}", + Command::Strings(StringCommand::Set( + Arity::One(("seinfeld", "kramer")), + Some(std::time::Duration::new(1, 0)), + Insertion::Always + )) + ), + "*5\r\n$3\r\nSET\r\n$8\r\nseinfeld\r\n$6\r\nkramer\r\n$2\r\nPX\r\n$4\r\n1000\r\n" + ); + } + + #[test] + fn test_set_fmt_if_not_exists() { + assert_eq!( + format!( + "{}", + Command::Strings(StringCommand::Set( + Arity::One(("seinfeld", "kramer")), + None, + Insertion::IfNotExists + )) + ), + "*4\r\n$3\r\nSET\r\n$8\r\nseinfeld\r\n$6\r\nkramer\r\n$2\r\nNX\r\n" + ); + } + + #[test] + fn test_set_fmt_if_exists() { + assert_eq!( + format!( + "{}", + Command::Strings(StringCommand::Set( + Arity::One(("seinfeld", "kramer")), + None, + Insertion::IfExists + )) + ), + "*4\r\n$3\r\nSET\r\n$8\r\nseinfeld\r\n$6\r\nkramer\r\n$2\r\nXX\r\n" + ); + } + + #[test] + fn test_lrem_fmt() { + assert_eq!( + format!("{}", Command::List(ListCommand::Rem("seinfeld", "kramer", 1))), + "*4\r\n$4\r\nLREM\r\n$8\r\nseinfeld\r\n$1\r\n1\r\n$6\r\nkramer\r\n" + ); + } + + #[test] + fn test_get_fmt() { + assert_eq!( + format!("{}", Command::Strings(StringCommand::Get(Arity::One("seinfeld")))), + "*2\r\n$3\r\nGET\r\n$8\r\nseinfeld\r\n" + ); + } + + #[test] + fn test_decr_fmt() { + assert_eq!( + format!("{}", Command::Strings(StringCommand::Decr("seinfeld", 1))), + "*2\r\n$4\r\nDECR\r\n$8\r\nseinfeld\r\n" + ); + } + + #[test] + fn test_append_fmt() { + assert_eq!( + format!("{}", Command::Strings(StringCommand::Append("seinfeld", "kramer"))), + "*3\r\n$6\r\nAPPEND\r\n$8\r\nseinfeld\r\n$6\r\nkramer\r\n" + ); + } + + #[test] + fn test_macro_write() { + let cmd = Command::Strings(StringCommand::Decr("one", 1)); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*2\r\n$4\r\nDECR\r\n$3\r\none\r\n") + ); + } + + #[test] + fn test_hdel_single() { + let cmd = Command::Hashes(HashCommand::Del("seinfeld", Arity::One("kramer"))); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*3\r\n$4\r\nHDEL\r\n$8\r\nseinfeld\r\n$6\r\nkramer\r\n") + ); + } + + #[test] + fn test_hdel_many() { + let cmd = Command::Hashes(HashCommand::Del("seinfeld", Arity::Many(vec!["kramer", "jerry"]))); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*4\r\n$4\r\nHDEL\r\n$8\r\nseinfeld\r\n$6\r\nkramer\r\n$5\r\njerry\r\n") + ); + } + + #[test] + fn test_hset_single() { + let cmd = Command::Hashes(HashCommand::Set( + "seinfeld", + Arity::One(("name", "kramer")), + Insertion::Always, + )); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*4\r\n$4\r\nHSET\r\n$8\r\nseinfeld\r\n$4\r\nname\r\n$6\r\nkramer\r\n") + ); + } + + #[test] + fn test_hexists() { + let cmd = Command::Hashes(HashCommand::Exists("seinfeld", "kramer")); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*3\r\n$7\r\nHEXISTS\r\n$8\r\nseinfeld\r\n$6\r\nkramer\r\n") + ); + } + + #[test] + fn test_echo() { + let cmd = Command::Echo("hello"); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*2\r\n$4\r\nECHO\r\n$5\r\nhello\r\n") + ); + } + + #[test] + fn test_hset_many() { + let cmd = Command::Hashes(HashCommand::Set( + "seinfeld", + Arity::Many(vec![("name", "kramer"), ("friend", "jerry")]), + Insertion::Always, + )); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from( + "*6\r\n$4\r\nHSET\r\n$8\r\nseinfeld\r\n$4\r\nname\r\n$6\r\nkramer\r\n$6\r\nfriend\r\n$5\r\njerry\r\n" + ) + ); + } + + #[test] + fn test_hgetall() { + let cmd = Command::Hashes(HashCommand::Get("seinfeld", None)); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*2\r\n$7\r\nHGETALL\r\n$8\r\nseinfeld\r\n") + ); + } + + #[test] + fn test_mset() { + let cmd = Command::Strings(StringCommand::Set( + Arity::Many(vec![("name", "kramer"), ("friend", "jerry")]), + None, + Insertion::Always, + )); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*5\r\n$4\r\nMSET\r\n$4\r\nname\r\n$6\r\nkramer\r\n$6\r\nfriend\r\n$5\r\njerry\r\n") + ); + } + + #[test] + fn test_msetnx() { + let cmd = Command::Strings(StringCommand::Set( + Arity::Many(vec![("name", "kramer"), ("friend", "jerry")]), + None, + Insertion::IfNotExists, + )); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*5\r\n$6\r\nMSETNX\r\n$4\r\nname\r\n$6\r\nkramer\r\n$6\r\nfriend\r\n$5\r\njerry\r\n") + ); + } + + #[test] + fn test_hincrby() { + let cmd = Command::Hashes(HashCommand::Incr("kramer", "episodes", 10)); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*4\r\n$7\r\nHINCRBY\r\n$6\r\nkramer\r\n$8\r\nepisodes\r\n$2\r\n10\r\n") + ); + } + + #[test] + fn test_hlen() { + let cmd = Command::Hashes(HashCommand::Len("seinfeld")); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*2\r\n$4\r\nHLEN\r\n$8\r\nseinfeld\r\n") + ); + } + + #[test] + fn test_hvals() { + let cmd = Command::Hashes(HashCommand::Vals("seinfeld")); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*2\r\n$5\r\nHVALS\r\n$8\r\nseinfeld\r\n") + ); + } + + #[test] + fn test_hstrlen() { + let cmd = Command::Hashes(HashCommand::StrLen("seinfeld", "name")); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*3\r\n$7\r\nHSTRLEN\r\n$8\r\nseinfeld\r\n$4\r\nname\r\n") + ); + } + + #[test] + fn test_hget() { + let cmd = Command::Hashes(HashCommand::Get("seinfeld", Some(Arity::One("name")))); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*3\r\n$4\r\nHGET\r\n$8\r\nseinfeld\r\n$4\r\nname\r\n") + ); + } + + #[test] + fn test_ltrim() { + let cmd = Command::List(ListCommand::Trim("episodes", 0, 10)); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*4\r\n$5\r\nLTRIM\r\n$8\r\nepisodes\r\n$1\r\n0\r\n$2\r\n10\r\n") + ); + } + + #[test] + fn test_linsert_before() { + let cmd = Command::List(ListCommand::Insert("episodes", Side::Left, "10", "9")); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*5\r\n$7\r\nLINSERT\r\n$8\r\nepisodes\r\n$6\r\nBEFORE\r\n$2\r\n10\r\n$1\r\n9\r\n") + ); + } + + #[test] + fn test_linsert_after() { + let cmd = Command::List(ListCommand::Insert("episodes", Side::Right, "10", "11")); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*5\r\n$7\r\nLINSERT\r\n$8\r\nepisodes\r\n$5\r\nAFTER\r\n$2\r\n10\r\n$2\r\n11\r\n") + ); + } + + #[test] + fn test_lrem() { + let cmd = Command::List(ListCommand::Rem("episodes", "10", 100)); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*4\r\n$4\r\nLREM\r\n$8\r\nepisodes\r\n$3\r\n100\r\n$2\r\n10\r\n") + ); + } + + #[test] + fn test_lindex() { + let cmd = Command::List(ListCommand::Index("episodes", 1)); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*3\r\n$6\r\nLINDEX\r\n$8\r\nepisodes\r\n$1\r\n1\r\n") + ); + } + #[test] - fn it_works() { - assert_eq!(2 + 2, 4); + fn test_lset() { + let cmd = Command::List(ListCommand::Set("episodes", 1, "pilot")); + let mut buffer = Vec::new(); + write!(buffer, "{}", cmd).expect("was able to write"); + assert_eq!( + String::from_utf8(buffer).unwrap(), + String::from("*4\r\n$4\r\nLSET\r\n$8\r\nepisodes\r\n$1\r\n1\r\n$5\r\npilot\r\n") + ); } } diff --git a/src/lists.rs b/src/lists.rs new file mode 100644 index 0000000..203a599 --- /dev/null +++ b/src/lists.rs @@ -0,0 +1,132 @@ +use crate::modifiers::{format_bulk_string, Arity, Insertion, Side}; + +#[derive(Debug)] +pub enum ListCommand +where + S: std::fmt::Display, +{ + Len(S), + Push((Side, Insertion), S, Arity), + Pop(Side, S, Option<(Option>, u64)>), + Rem(S, S, u64), + Index(S, i64), + Set(S, u64, S), + Insert(S, Side, S, S), + Trim(S, i64, i64), + Range(S, i64, i64), +} + +impl std::fmt::Display for ListCommand { + fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + ListCommand::Trim(key, start, stop) => { + let tail = format!( + "{}{}{}", + format_bulk_string(key), + format_bulk_string(start), + format_bulk_string(stop) + ); + write!(formatter, "*4\r\n$5\r\nLTRIM\r\n{}", tail) + } + ListCommand::Set(key, index, element) => { + let tail = format!( + "{}{}{}", + format_bulk_string(key), + format_bulk_string(index), + format_bulk_string(element) + ); + write!(formatter, "*4\r\n$4\r\nLSET\r\n{}", tail) + } + ListCommand::Insert(key, side, pivot, element) => { + let side = match side { + Side::Left => format_bulk_string("BEFORE"), + Side::Right => format_bulk_string("AFTER"), + }; + let tail = format!("{}{}", format_bulk_string(pivot), format_bulk_string(element)); + + write!( + formatter, + "*5\r\n$7\r\nLINSERT\r\n{}{}{}", + format_bulk_string(key), + side, + tail, + ) + } + ListCommand::Index(key, amt) => { + let tail = format!("{}{}", format_bulk_string(key), format_bulk_string(amt)); + write!(formatter, "*3\r\n$6\r\nLINDEX\r\n{}", tail) + } + ListCommand::Rem(key, value, count) => { + let end = format!( + "{}{}{}", + format_bulk_string(key), + format_bulk_string(count), + format_bulk_string(value), + ); + + write!(formatter, "*4\r\n$4\r\nLREM\r\n{}", end) + } + ListCommand::Range(key, from, to) => { + let end = format!("{}{}", format_bulk_string(from), format_bulk_string(to)); + write!(formatter, "*4\r\n$6\r\nLRANGE\r\n{}{}", format_bulk_string(key), end) + } + ListCommand::Len(key) => write!(formatter, "*2\r\n$4\r\nLLEN\r\n{}", format_bulk_string(key)), + ListCommand::Pop(side, key, block) => { + let (cmd, ext, kc) = match (side, block) { + (Side::Left, None) => ("LPOP", format!(""), 0), + (Side::Right, None) => ("RPOP", format!(""), 0), + (Side::Left, Some((None, timeout))) => ("BLPOP", format_bulk_string(timeout), 1), + (Side::Right, Some((None, timeout))) => ("BRPOP", format_bulk_string(timeout), 1), + (Side::Left, Some((Some(values), timeout))) => { + let (vc, ext) = match values { + Arity::One(value) => (1, format_bulk_string(value)), + Arity::Many(values) => (values.len(), values.iter().map(format_bulk_string).collect::()), + }; + ("BLPOP", format!("{}{}", ext, format_bulk_string(timeout)), vc + 1) + } + (Side::Right, Some((Some(values), timeout))) => { + let (vc, ext) = match values { + Arity::One(value) => (1, format_bulk_string(value)), + Arity::Many(values) => (values.len(), values.iter().map(format_bulk_string).collect::()), + }; + ("BRPOP", format!("{}{}", ext, format_bulk_string(timeout)), vc + 1) + } + }; + write!( + formatter, + "*{}\r\n${}\r\n{}\r\n{}{}", + 2 + kc, + cmd.len(), + cmd, + format_bulk_string(key), + ext + ) + } + ListCommand::Push(operation, k, Arity::One(v)) => { + let cmd = match operation { + (Side::Left, Insertion::IfExists) => "LPUSHX", + (Side::Right, Insertion::IfExists) => "RPUSHX", + (Side::Left, _) => "LPUSH", + (Side::Right, _) => "RPUSH", + }; + let parts = format!("{}{}", format_bulk_string(k), format_bulk_string(v),); + write!(formatter, "*3\r\n${}\r\n{}\r\n{}", cmd.len(), cmd, parts) + } + ListCommand::Push(operation, k, Arity::Many(v)) => { + let size = v.len(); + let cmd = match operation { + (Side::Left, Insertion::IfExists) => "LPUSHX", + (Side::Right, Insertion::IfExists) => "RPUSHX", + (Side::Left, _) => "LPUSH", + (Side::Right, _) => "RPUSH", + }; + let parts = format!( + "{}{}", + format_bulk_string(k), + v.iter().map(format_bulk_string).collect::() + ); + write!(formatter, "*{}\r\n${}\r\n{}\r\n{}", 2 + size, cmd.len(), cmd, parts) + } + } + } +} diff --git a/src/modifiers.rs b/src/modifiers.rs new file mode 100644 index 0000000..1ae5155 --- /dev/null +++ b/src/modifiers.rs @@ -0,0 +1,23 @@ +#[derive(Debug, Clone, PartialEq)] +pub enum Side { + Left, + Right, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Insertion { + Always, + IfExists, + IfNotExists, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Arity { + Many(Vec), + One(S), +} + +pub fn format_bulk_string(input: S) -> String { + let as_str = format!("{}", input); + format!("${}\r\n{}\r\n", as_str.len(), as_str) +} diff --git a/src/strings.rs b/src/strings.rs new file mode 100644 index 0000000..b7ef90d --- /dev/null +++ b/src/strings.rs @@ -0,0 +1,75 @@ +use crate::modifiers::{format_bulk_string, Arity, Insertion}; + +#[derive(Debug)] +pub enum StringCommand +where + S: std::fmt::Display, +{ + Set(Arity<(S, S)>, Option, Insertion), + Get(Arity), + Decr(S, usize), + Incr(S, i64), + Append(S, S), +} + +impl std::fmt::Display for StringCommand { + fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + StringCommand::Incr(key, 1) => write!(formatter, "*2\r\n$4\r\nINCR\r\n{}", format_bulk_string(key)), + StringCommand::Incr(key, amt) => write!( + formatter, + "*3\r\n$6\r\nINCRBY\r\n{}{}", + format_bulk_string(key), + format_bulk_string(amt) + ), + StringCommand::Decr(key, 1) => write!(formatter, "*2\r\n$4\r\nDECR\r\n{}", format_bulk_string(key)), + StringCommand::Decr(key, amt) => write!( + formatter, + "*3\r\n$6\r\nDECRBY\r\n{}{}", + format_bulk_string(key), + format_bulk_string(amt) + ), + StringCommand::Get(Arity::One(key)) => write!(formatter, "*2\r\n$3\r\nGET\r\n{}", format_bulk_string(key)), + StringCommand::Get(Arity::Many(keys)) => { + let count = keys.len(); + let tail = keys.iter().map(format_bulk_string).collect::(); + write!(formatter, "*{}\r\n$4\r\nMGET\r\n{}", count + 1, tail) + } + StringCommand::Append(key, value) => write!( + formatter, + "*3\r\n$6\r\nAPPEND\r\n{}{}", + format_bulk_string(key), + format_bulk_string(value) + ), + StringCommand::Set(Arity::One((key, value)), timeout, insertion) => { + let (k, v) = (format_bulk_string(key), format_bulk_string(value)); + let (cx, px) = match timeout { + None => (0, format!("")), + Some(t) => ( + 2, + format!("{}{}", format_bulk_string("PX"), format_bulk_string(t.as_millis())), + ), + }; + let (ci, i) = match insertion { + Insertion::IfExists => (1, format_bulk_string("XX")), + Insertion::IfNotExists => (1, format_bulk_string("NX")), + Insertion::Always => (0, format!("")), + }; + write!(formatter, "*{}\r\n$3\r\nSET\r\n{}{}{}{}", 3 + ci + cx, k, v, px, i) + } + // Timeouts are not supported with a many set. + StringCommand::Set(Arity::Many(assignments), _, insertion) => { + let count = (assignments.len() * 2) + 1; + let cmd = match insertion { + Insertion::IfNotExists => "MSETNX", + _ => "MSET", + }; + let tail = assignments + .iter() + .map(|(k, v)| format!("{}{}", format_bulk_string(k), format_bulk_string(v))) + .collect::(); + write!(formatter, "*{}\r\n{}{}", count, format_bulk_string(cmd), tail) + } + } + } +} diff --git a/tests/execute_test.rs b/tests/execute_test.rs new file mode 100644 index 0000000..90f8e65 --- /dev/null +++ b/tests/execute_test.rs @@ -0,0 +1,1093 @@ +#![cfg(feature = "kramer-io")] + +extern crate kramer; + +use kramer::{send, Arity, Command, HashCommand, Insertion, ListCommand, Response, ResponseValue, Side, StringCommand}; +use std::env::var; + +#[cfg(test)] +fn set_field(key: S, field: S, value: S) -> Command { + Command::Hashes(HashCommand::Set(key, Arity::One((field, value)), Insertion::Always)) +} + +#[cfg(test)] +fn arity_single_pair(key: S, value: S) -> Arity<(S, S)> { + Arity::One((key, value)) +} + +#[cfg(test)] +fn get_redis_url() -> String { + let host = var("REDIS_HOST").unwrap_or(String::from("0.0.0.0")); + let port = var("REDIS_PORT").unwrap_or(String::from("6379")); + format!("{}:{}", host, port) +} + +#[test] +fn test_echo() { + let url = get_redis_url(); + let result = async_std::task::block_on(send(url.as_str(), Command::Echo("hello"))); + assert_eq!( + result.unwrap(), + Response::Item(ResponseValue::String("hello".to_string())) + ); +} + +#[test] +fn test_send_keys() { + let url = get_redis_url(); + let result = async_std::task::block_on(send(url.as_str(), Command::Keys("*"))); + assert!(result.is_ok()); +} + +#[test] +fn test_set_vanilla() { + let url = get_redis_url(); + let key = "test_set_vanilla"; + let result = async_std::task::block_on(async { + let set_result = send( + url.as_str(), + Command::Strings(StringCommand::Set(Arity::One((key, "kramer")), None, Insertion::Always)), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + set_result + }); + assert_eq!( + result.unwrap(), + Response::Item(ResponseValue::String(String::from("OK"))) + ) +} + +#[test] +fn test_set_if_not_exists_w_not_exists() { + let key = "test_set_if_not_exists_w_not_exists"; + let url = get_redis_url(); + let result = async_std::task::block_on(async { + let set_result = send( + url.as_str(), + Command::Strings(StringCommand::Set( + Arity::One((key, "kramer")), + None, + Insertion::IfNotExists, + )), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + set_result + }); + assert_eq!( + result.unwrap(), + Response::Item(ResponseValue::String(String::from("OK"))) + ); +} + +#[test] +fn test_set_if_not_exists_w_exists() { + let key = "test_set_if_not_exists_w_exists"; + let url = get_redis_url(); + + let result = async_std::task::block_on(async { + send( + url.as_str(), + Command::Strings(StringCommand::Set(Arity::One((key, "kramer")), None, Insertion::Always)), + ) + .await?; + let set_result = send( + url.as_str(), + Command::Strings(StringCommand::Set( + arity_single_pair(key, "jerry"), + None, + Insertion::IfNotExists, + )), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + set_result + }); + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Empty)); +} + +#[test] +fn test_set_if_exists_w_not_exists() { + let key = "test_set_if_exists_w_not_exists"; + let url = get_redis_url(); + + let result = async_std::task::block_on(async { + let set_result = send( + url.as_str(), + Command::Strings(StringCommand::Set( + arity_single_pair(key, "kramer"), + None, + Insertion::IfExists, + )), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + set_result + }); + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Empty)); +} + +#[test] +fn test_set_if_exists_w_exists() { + let key = "test_set_if_exists_w_exists"; + let url = get_redis_url(); + let result = async_std::task::block_on(async { + send( + url.as_str(), + Command::Strings(StringCommand::Set( + arity_single_pair(key, "kramer"), + None, + Insertion::Always, + )), + ) + .await?; + let set_result = send( + url.as_str(), + Command::Strings(StringCommand::Set( + arity_single_pair(key, "jerry"), + None, + Insertion::IfExists, + )), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + set_result + }); + assert_eq!( + result.unwrap(), + Response::Item(ResponseValue::String(String::from("OK"))) + ); +} + +#[test] +fn test_set_with_duration() { + let (key, url) = ("test_set_duration", get_redis_url()); + + let result = async_std::task::block_on(async { + let set_result = send( + url.as_str(), + Command::Strings(StringCommand::Set( + arity_single_pair(key, "kramer"), + Some(std::time::Duration::new(10, 0)), + Insertion::Always, + )), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + set_result + }); + assert_eq!( + result.unwrap(), + Response::Item(ResponseValue::String(String::from("OK"))) + ) +} + +#[test] +fn test_lpush_single() { + let (key, url) = ("test_lpush_single", get_redis_url()); + + let result = async_std::task::block_on(async { + let out = send( + url.as_str(), + Command::List(ListCommand::Push( + (Side::Left, Insertion::Always), + key, + Arity::One("kramer"), + )), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + out + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(1))); +} + +#[test] +fn test_llen_single() { + let (key, url) = ("test_llen_single", get_redis_url()); + + let result = async_std::task::block_on(async { + let ins = Command::List(ListCommand::Push( + (Side::Left, Insertion::Always), + key, + Arity::One("kramer"), + )); + send(url.as_str(), ins).await?; + let result = send(url.as_str(), Command::List(ListCommand::Len(key))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(1))); +} + +#[test] +fn test_lpush_multi() { + let (key, url) = ("test_lpush_multi", get_redis_url()); + + let result = async_std::task::block_on(async { + let out = send( + url.as_str(), + Command::List(ListCommand::Push( + (Side::Left, Insertion::Always), + key, + Arity::Many(vec!["kramer", "jerry"]), + )), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + out + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(2))); +} + +#[test] +fn test_lpushx_single_w_no_exists() { + let (key, url) = ("test_lpushx_single_w_no_exists", get_redis_url()); + + let result = async_std::task::block_on(async { + let out = send( + url.as_str(), + Command::List(ListCommand::Push( + (Side::Left, Insertion::IfExists), + key, + Arity::One("kramer"), + )), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + out + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(0))); +} + +#[test] +fn test_lpushx_single_w_exists() { + let (key, url) = ("test_lpushx_single_w_exists", get_redis_url()); + + let result = async_std::task::block_on(async { + send( + url.as_str(), + Command::List(ListCommand::Push( + (Side::Left, Insertion::Always), + key, + Arity::One("kramer"), + )), + ) + .await?; + let out = send( + url.as_str(), + Command::List(ListCommand::Push( + (Side::Left, Insertion::IfExists), + key, + Arity::One("kramer"), + )), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + out + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(2))); +} + +#[test] +fn test_rpush_single() { + let (key, url) = ("test_rpush_single", get_redis_url()); + + let result = async_std::task::block_on(async { + let set_result = send( + url.as_str(), + Command::List(ListCommand::Push( + (Side::Right, Insertion::Always), + key, + Arity::One("kramer"), + )), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + set_result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(1))); +} + +#[test] +fn test_lpop_single() { + let (key, url) = ("test_lpop_single", get_redis_url()); + + let result = async_std::task::block_on(async { + let push = Command::List(ListCommand::Push( + (Side::Right, Insertion::Always), + key, + Arity::Many(vec!["kramer", "jerry"]), + )); + send(url.as_str(), push).await?; + let result = send(url.as_str(), Command::List(ListCommand::Pop(Side::Left, key, None))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!( + result.unwrap(), + Response::Item(ResponseValue::String(String::from("kramer"))) + ); +} + +#[test] +fn test_rpop_single() { + let (key, url) = ("test_rpop_single", get_redis_url()); + + let result = async_std::task::block_on(async { + let push = Command::List(ListCommand::Push( + (Side::Right, Insertion::Always), + key, + Arity::Many(vec!["kramer", "jerry"]), + )); + send(url.as_str(), push).await?; + let result = send(url.as_str(), Command::List(ListCommand::Pop(Side::Right, key, None))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!( + result.unwrap(), + Response::Item(ResponseValue::String(String::from("jerry"))) + ); +} + +#[test] +fn test_rpush_multiple() { + let (key, url) = ("test_rpush_many", get_redis_url()); + + let result = async_std::task::block_on(async { + let set_result = send( + url.as_str(), + Command::List(ListCommand::Push( + (Side::Right, Insertion::Always), + key, + Arity::Many(vec!["kramer", "jerry"]), + )), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + set_result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(2))); +} + +#[test] +fn test_append() { + let (key, url) = ("test_append", get_redis_url()); + + let result = async_std::task::block_on(async { + let set_result = send(url.as_str(), Command::Strings(StringCommand::Append(key, "jerry"))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + set_result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(5))); +} + +#[test] +fn test_get() { + let (key, url) = ("test_get", get_redis_url()); + + let result = async_std::task::block_on(async { + send(url.as_str(), Command::Strings(StringCommand::Append(key, "jerry"))).await?; + let result = send(url.as_str(), Command::Strings(StringCommand::Get(Arity::One(key)))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!( + result.unwrap(), + Response::Item(ResponseValue::String(String::from("jerry"))) + ); +} + +#[test] +fn test_get_multi_append() { + let (key, url) = ("test_get_multi_append", get_redis_url()); + + let result = async_std::task::block_on(async { + send(url.as_str(), Command::Strings(StringCommand::Append(key, "jerry"))).await?; + send(url.as_str(), Command::Strings(StringCommand::Append(key, "kramer"))).await?; + let result = send(url.as_str(), Command::Strings(StringCommand::Get(Arity::One(key)))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!( + result.unwrap(), + Response::Item(ResponseValue::String(String::from("jerrykramer"))) + ); +} + +#[test] +fn test_multi_get() { + let (one, two, url) = ("test_multi_get_1", "test_multi_get_2", get_redis_url()); + + let result = async_std::task::block_on(async { + send(url.as_str(), Command::Strings(StringCommand::Append(one, "jerry"))).await?; + send(url.as_str(), Command::Strings(StringCommand::Append(two, "kramer"))).await?; + let result = send( + url.as_str(), + Command::Strings(StringCommand::Get(Arity::Many(vec![one, two]))), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(one))).await?; + send(url.as_str(), Command::Del(Arity::One(two))).await?; + result + }); + + assert_eq!( + result.unwrap(), + Response::Array(vec![ + ResponseValue::String(String::from("jerry")), + ResponseValue::String(String::from("kramer")), + ]), + ); +} + +#[test] +fn test_decr_single() { + let (key, url) = ("test_decr_single", get_redis_url()); + + let result = async_std::task::block_on(async { + let push = Command::Strings(StringCommand::Set(arity_single_pair(key, "3"), None, Insertion::Always)); + send(url.as_str(), push).await?; + let result = send(url.as_str(), Command::Strings(StringCommand::Decr(key, 1))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(2))); +} + +#[test] +fn test_decrby_single() { + let (key, url) = ("test_decrby_single", get_redis_url()); + + let result = async_std::task::block_on(async { + let push = Command::Strings(StringCommand::Set(arity_single_pair(key, "3"), None, Insertion::Always)); + send(url.as_str(), push).await?; + let result = send(url.as_str(), Command::Strings(StringCommand::Decr(key, 2))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(1))); +} + +#[test] +fn test_hset_single() { + let (key, url) = ("test_hset_single", get_redis_url()); + + let result = async_std::task::block_on(async { + let do_set = Command::Hashes(HashCommand::Set(key, Arity::One(("name", "kramer")), Insertion::Always)); + let result = send(url.as_str(), do_set).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(1))); +} + +#[test] +fn test_hset_multi() { + let (key, url) = ("test_hset_multi", get_redis_url()); + + let result = async_std::task::block_on(async { + let do_set = Command::Hashes(HashCommand::Set( + key, + Arity::Many(vec![("name", "kramer"), ("friend", "jerry")]), + Insertion::Always, + )); + let result = send(url.as_str(), do_set).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(2))); +} + +#[test] +fn test_hdel_single() { + let (key, url) = ("test_hdel_single", get_redis_url()); + + let result = async_std::task::block_on(async { + send( + url.as_str(), + Command::Hashes(HashCommand::Set(key, Arity::One(("name", "kramer")), Insertion::Always)), + ) + .await?; + let del = Command::Hashes(HashCommand::Del(key, Arity::One("name"))); + let result = send(url.as_str(), del).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(1))); +} + +#[test] +fn test_hdel_multi() { + let (key, url) = ("test_hdel_multi", get_redis_url()); + + let result = async_std::task::block_on(async { + send( + url.as_str(), + Command::Hashes(HashCommand::Set( + key, + Arity::Many(vec![("name", "kramer"), ("friend", "jerry")]), + Insertion::Always, + )), + ) + .await?; + let del = Command::Hashes(HashCommand::Del(key, Arity::Many(vec!["name", "friend", "foo"]))); + let result = send(url.as_str(), del).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(2))); +} + +#[test] +fn test_hsetnx_single_w_no_exists() { + let (key, url) = ("test_hsetnx_single_w_no_exists", get_redis_url()); + + let result = async_std::task::block_on(async { + let do_set = Command::Hashes(HashCommand::Set( + key, + Arity::One(("name", "kramer")), + Insertion::IfNotExists, + )); + let result = send(url.as_str(), do_set).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(1))); +} + +#[test] +fn test_hsetnx_single_w_exists() { + let (key, url) = ("test_hsetnx_single_w_exists", get_redis_url()); + + let result = async_std::task::block_on(async { + let pre_set = Command::Hashes(HashCommand::Set(key, Arity::One(("name", "kramer")), Insertion::Always)); + send(url.as_str(), pre_set).await?; + let do_set = Command::Hashes(HashCommand::Set( + key, + Arity::One(("name", "kramer")), + Insertion::IfNotExists, + )); + let result = send(url.as_str(), do_set).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(0))); +} + +#[test] +fn test_hexists_single() { + let (key, url) = ("test_hexists_single", get_redis_url()); + + let result = async_std::task::block_on(async { + send( + url.as_str(), + Command::Hashes(HashCommand::Set(key, Arity::One(("name", "kramer")), Insertion::Always)), + ) + .await?; + let exists = Command::Hashes(HashCommand::Exists(key, "name")); + let result = send(url.as_str(), exists).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(1))); +} + +#[test] +fn test_hexists_not_found() { + let (key, url) = ("test_hexists_not_found", get_redis_url()); + + let result = async_std::task::block_on(async { + let exists = Command::Hashes(HashCommand::Exists(key, "name")); + let result = send(url.as_str(), exists).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(0))); +} + +#[test] +fn test_hgetall_values() { + let (key, url) = ("test_hgetall_values", get_redis_url()); + + let result = async_std::task::block_on(async { + send(url.as_str(), set_field(key, "name", "kramer")).await?; + send(url.as_str(), set_field(key, "friend", "jerry")).await?; + let getall = Command::Hashes(HashCommand::Get(key, None)); + let result = send(url.as_str(), getall).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!( + result.unwrap(), + Response::Array(vec![ + ResponseValue::String(String::from("name")), + ResponseValue::String(String::from("kramer")), + ResponseValue::String(String::from("friend")), + ResponseValue::String(String::from("jerry")), + ]) + ); +} + +#[test] +fn test_hgetall_empty() { + let (key, url) = ("test_hgetall_empty", get_redis_url()); + + let result = async_std::task::block_on(async { + let getall = Command::Hashes(HashCommand::Get(key, None)); + let result = send(url.as_str(), getall).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!(result.unwrap(), Response::Array(vec![])); +} + +#[test] +fn test_hget_values() { + let (key, url) = ("test_hget_values", get_redis_url()); + + let result = async_std::task::block_on(async { + send(url.as_str(), set_field(key, "name", "kramer")).await?; + let getall = Command::Hashes(HashCommand::Get(key, Some(Arity::One("name")))); + let result = send(url.as_str(), getall).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!( + result.unwrap(), + Response::Item(ResponseValue::String(String::from("kramer"))), + ); +} + +#[test] +fn test_hmget_values() { + let (key, url) = ("test_hmget_values", get_redis_url()); + + let result = async_std::task::block_on(async { + send(url.as_str(), set_field(key, "name", "kramer")).await?; + send(url.as_str(), set_field(key, "friend", "jerry")).await?; + let getall = Command::Hashes(HashCommand::Get(key, Some(Arity::Many(vec!["name", "friend"])))); + let result = send(url.as_str(), getall).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!( + result.unwrap(), + Response::Array(vec![ + ResponseValue::String(String::from("kramer")), + ResponseValue::String(String::from("jerry")) + ]), + ); +} + +#[test] +fn test_hlen_values() { + let (key, url) = ("test_hlen_values", get_redis_url()); + + let result = async_std::task::block_on(async { + send(url.as_str(), set_field(key, "name", "kramer")).await?; + let getall = Command::Hashes(HashCommand::Len(key)); + let result = send(url.as_str(), getall).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(1))); +} + +#[test] +fn test_hlen_no_exists() { + let (key, url) = ("test_hlen_no_exists", get_redis_url()); + + let result = async_std::task::block_on(async { + let hlen = Command::Hashes(HashCommand::Len(key)); + send(url.as_str(), hlen).await + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(0))); +} + +#[test] +fn test_hkeys_values() { + let (key, url) = ("test_hkeys_values", get_redis_url()); + + let result = async_std::task::block_on(async { + send(url.as_str(), set_field(key, "name", "kramer")).await?; + let getall = Command::Hashes(HashCommand::Keys(key)); + let result = send(url.as_str(), getall).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!( + result.unwrap(), + Response::Array(vec![ResponseValue::String(String::from("name"))]) + ); +} + +#[test] +fn test_hkeys_no_exists() { + let (key, url) = ("test_hkeys_no_exists", get_redis_url()); + + let result = async_std::task::block_on(async { + let hlen = Command::Hashes(HashCommand::Keys(key)); + send(url.as_str(), hlen).await + }); + + assert_eq!(result.unwrap(), Response::Array(vec![])); +} + +#[test] +fn test_mset_many() { + let (one, two, url) = ("test_mset_many_1", "test_mset_many_2", get_redis_url()); + + let result = async_std::task::block_on(async { + let do_set = Command::Strings(StringCommand::Set( + Arity::Many(vec![(one, "hello"), (two, "goodbye")]), + None, + Insertion::Always, + )); + send(url.as_str(), do_set).await?; + let result = send( + url.as_str(), + Command::Strings(StringCommand::Get(Arity::Many(vec![one, two]))), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(one))).await?; + send(url.as_str(), Command::Del(Arity::One(two))).await?; + result + }); + + assert_eq!( + result.unwrap(), + Response::Array(vec![ + ResponseValue::String(String::from("hello")), + ResponseValue::String(String::from("goodbye")) + ]), + ); +} + +#[test] +fn test_msetnx_many() { + let (one, two, url) = ("test_msetnx_many_1", "test_msetnx_many_2", get_redis_url()); + + let result = async_std::task::block_on(async { + let do_set = Command::Strings(StringCommand::Set( + Arity::Many(vec![(one, "hello"), (two, "goodbye")]), + None, + Insertion::IfNotExists, + )); + send(url.as_str(), do_set).await?; + let result = send( + url.as_str(), + Command::Strings(StringCommand::Get(Arity::Many(vec![one, two]))), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(one))).await?; + send(url.as_str(), Command::Del(Arity::One(two))).await?; + result + }); + + assert_eq!( + result.unwrap(), + Response::Array(vec![ + ResponseValue::String(String::from("hello")), + ResponseValue::String(String::from("goodbye")) + ]), + ); +} + +#[test] +fn test_msetnx_already_exists() { + let (one, two, url) = ( + "test_msetnx_alredy_exits_1", + "test_msetnx_already_exists_2", + get_redis_url(), + ); + + let result = async_std::task::block_on(async { + send( + url.as_str(), + Command::Strings(StringCommand::Set(Arity::One((one, "foo")), None, Insertion::Always)), + ) + .await?; + let do_set = Command::Strings(StringCommand::Set( + Arity::Many(vec![(one, "hello"), (two, "goodbye")]), + None, + Insertion::IfNotExists, + )); + let result = send(url.as_str(), do_set).await; + send(url.as_str(), Command::Del(Arity::One(one))).await?; + send(url.as_str(), Command::Del(Arity::One(two))).await?; + result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(0)),); +} + +#[test] +fn test_hvals_values() { + let (key, url) = ("test_hvals_values", get_redis_url()); + + let result = async_std::task::block_on(async { + send(url.as_str(), set_field(key, "name", "kramer")).await?; + let getall = Command::Hashes(HashCommand::Vals(key)); + let result = send(url.as_str(), getall).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!( + result.unwrap(), + Response::Array(vec![ResponseValue::String(String::from("kramer"))]) + ); +} + +#[test] +fn test_hstrlen_values() { + let (key, url) = ("test_hstrlen_values", get_redis_url()); + + let result = async_std::task::block_on(async { + send(url.as_str(), set_field(key, "name", "kramer")).await?; + let getall = Command::Hashes(HashCommand::StrLen(key, "name")); + let result = send(url.as_str(), getall).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(6))); +} + +#[test] +fn test_hincrby() { + let (key, url) = ("test_hincrby", get_redis_url()); + + let result = async_std::task::block_on(async { + send(url.as_str(), set_field(key, "episodes", "10")).await?; + let inc = Command::Hashes(HashCommand::Incr(key, "episodes", 10)); + send(url.as_str(), inc).await?; + let result = send( + url.as_str(), + Command::Hashes(HashCommand::Get(key, Some(Arity::One("episodes")))), + ) + .await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + result + }); + + assert_eq!( + result.unwrap(), + Response::Item(ResponseValue::String(String::from("20"))) + ); +} + +#[test] +fn test_lrange() { + let (key, url) = ("test_lrange", get_redis_url()); + + let result = async_std::task::block_on(async { + let ins = Command::List(ListCommand::Push( + (Side::Left, Insertion::Always), + key, + Arity::One("kramer"), + )); + send(url.as_str(), ins).await?; + let out = send(url.as_str(), Command::List(ListCommand::Range(key, 0, 10))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + out + }); + + assert_eq!( + result.unwrap(), + Response::Array(vec![ResponseValue::String(String::from("kramer"))]) + ); +} + +#[test] +fn test_lindex_present() { + let (key, url) = ("test_lindex_present", get_redis_url()); + + let result = async_std::task::block_on(async { + let ins = Command::List(ListCommand::Push( + (Side::Left, Insertion::Always), + key, + Arity::One("kramer"), + )); + send(url.as_str(), ins).await?; + let out = send(url.as_str(), Command::List(ListCommand::Index(key, 0))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + out + }); + + assert_eq!( + result.unwrap(), + Response::Item(ResponseValue::String(String::from("kramer"))) + ); +} + +#[test] +fn test_lindex_missing() { + let (key, url) = ("test_lindex_missing", get_redis_url()); + + let result = async_std::task::block_on(async { + let out = send(url.as_str(), Command::List(ListCommand::Index(key, 0))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + out + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Empty)); +} + +#[test] +fn test_lrem_present() { + let (key, url) = ("test_lrem_present", get_redis_url()); + + let result = async_std::task::block_on(async { + let ins = Command::List(ListCommand::Push( + (Side::Left, Insertion::Always), + key, + Arity::One("kramer"), + )); + send(url.as_str(), ins).await?; + let out = send(url.as_str(), Command::List(ListCommand::Rem(key, "kramer", 1))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + out + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(1))); +} + +#[test] +fn test_lrem_missing() { + let (key, url) = ("test_lrem_missing", get_redis_url()); + + let result = async_std::task::block_on(async { + let out = send(url.as_str(), Command::List(ListCommand::Rem(key, "kramer", 1))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + out + }); + + assert_eq!(result.unwrap(), Response::Item(ResponseValue::Integer(0))); +} + +#[test] +fn test_ltrim_present() { + let (key, url) = ("test_ltrim_present", get_redis_url()); + + let result = async_std::task::block_on(async { + let ins = Command::List(ListCommand::Push( + (Side::Right, Insertion::Always), + key, + Arity::Many(vec!["kramer", "jerry", "elaine", "george"]), + )); + send(url.as_str(), ins).await?; + send(url.as_str(), Command::List(ListCommand::Trim(key, 0, 2))).await?; + let out = send(url.as_str(), Command::List(ListCommand::Range(key, 0, 10))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + out + }); + + assert_eq!( + result.unwrap(), + Response::Array(vec![ + ResponseValue::String(String::from("kramer")), + ResponseValue::String(String::from("jerry")), + ResponseValue::String(String::from("elaine")), + ]) + ); +} + +#[test] +fn test_linsert_left_present() { + let (key, url) = ("test_linsert_left_present", get_redis_url()); + + let result = async_std::task::block_on(async { + let ins = Command::List(ListCommand::Push( + (Side::Right, Insertion::Always), + key, + Arity::Many(vec!["kramer", "jerry", "elaine", "george"]), + )); + send(url.as_str(), ins).await?; + send( + url.as_str(), + Command::List(ListCommand::Insert(key, Side::Left, "george", "newman")), + ) + .await?; + let out = send(url.as_str(), Command::List(ListCommand::Range(key, 0, 10))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + out + }); + + assert_eq!( + result.unwrap(), + Response::Array(vec![ + ResponseValue::String(String::from("kramer")), + ResponseValue::String(String::from("jerry")), + ResponseValue::String(String::from("elaine")), + ResponseValue::String(String::from("newman")), + ResponseValue::String(String::from("george")), + ]) + ); +} + +#[cfg(feature = "kramer-io")] +#[test] +fn test_linsert_right_present() { + let (key, url) = ("test_linsert_right_present", get_redis_url()); + + let result = async_std::task::block_on(async { + let ins = Command::List(ListCommand::Push( + (Side::Right, Insertion::Always), + key, + Arity::Many(vec!["kramer", "jerry", "elaine", "george"]), + )); + send(url.as_str(), ins).await?; + send( + url.as_str(), + Command::List(ListCommand::Insert(key, Side::Right, "george", "newman")), + ) + .await?; + let out = send(url.as_str(), Command::List(ListCommand::Range(key, 0, 10))).await; + send(url.as_str(), Command::Del(Arity::One(key))).await?; + out + }); + + assert_eq!( + result.unwrap(), + Response::Array(vec![ + ResponseValue::String(String::from("kramer")), + ResponseValue::String(String::from("jerry")), + ResponseValue::String(String::from("elaine")), + ResponseValue::String(String::from("george")), + ResponseValue::String(String::from("newman")), + ]) + ); +}