From 54bf0c00696e7aa492b469bc18f43d04ecfbac4a Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Fri, 9 Oct 2026 16:24:16 -0400 Subject: [PATCH 01/37] Split the client into connection, transport and commands Replace the `store` interface, a connection resource carrying every command, with interfaces that separate the mechanics of talking to a server from the transport carrying it and from the commands sent over it. Connections always speak RESP3. WIT, componentized:valkey: - `connection`: request/reply over a pair of value streams, for any transport. `new` returns the requests stream and a future resolving to the connection once `HELLO 3` succeeds, which pins RESP3 and confirms the server can be reached. `call` pairs replies with commands in order with several in flight, `send` is fire and forget, `pushes` broadcasts push messages to every reader, closing a reader that falls behind rather than holding up replies, `set-keep-alive` pings an idle connection - `tcp`: connections over wasi:sockets, a `transport` resource and `connect` - `connector`: connections whose transport and settings are chosen by the provider - command interfaces for each of Valkey's ACL categories, e.g. `hash-commands`, functions borrowing a connection so each category can be implemented independently. The commands not yet implemented are listed in each interface - `store` is removed Components: - `resp`: exports the RESP codec, the types every component shares - `connection`, `tcp` and `tcp-connector`, which connects to the server configured with wasi:config, with keys `hostname`, `port`, `username`, `password`, `client-name` and `keep-alive-interval-ms` - `commands`, every command interface in one component, and a component for each category, e.g. `hash-commands`, compiled from the shared sources in crates/commands - `as-keyvalue` gets connections from a `connector`, rather than connecting over TCP itself - `cli` and `client` are composed from the new components - `ops` is removed RESP: - the codec moves to crates/resp-codec, a regular crate the components share through wit-bindgen's `with` - messages that span several socket reads are framed before they are decoded, bytes past a message are kept for the next - nested values are copied as received, rather than decoded and encoded again Tests: - crates/test-harness instantiates components with wasmtime, and plays the server over a stub transport, or over TCP - the tests against a Valkey server are ignored unless run with `make test-valkey`, which CI runs after `make test` - CI and the dev container use Valkey 9, which as-keyvalue's ttls need for HEXPIRE Signed-off-by: Scott Andrews Co-Authored-By: Claude --- .github/workflows/bump-version.yaml | 2 +- .github/workflows/ci.yaml | 4 +- Cargo.lock | 2409 +++++++++++++++-- Cargo.toml | 21 +- Makefile | 14 +- README.md | 34 + components/admin-commands/Cargo.toml | 23 + components/admin-commands/README.md | 5 + components/as-keyvalue/Cargo.toml | 7 +- components/as-keyvalue/README.md | 13 + components/as-keyvalue/src/lib.rs | 85 +- components/as-keyvalue/tests/keyvalue.rs | 146 + components/cli/cli.wac | 27 +- components/client/README.md | 2 +- components/client/client.wac | 34 +- components/commands/Cargo.toml | 28 + components/commands/README.md | 5 + components/commands/tests/commands.rs | 756 ++++++ components/connection-commands/Cargo.toml | 23 + components/connection-commands/README.md | 5 + components/connection/Cargo.toml | 19 + components/connection/README.md | 5 + components/connection/src/lib.rs | 413 +++ components/connection/tests/connection.rs | 291 ++ components/dep-cli/Cargo.toml | 2 +- components/dep-cli/README.md | 2 +- components/dep-cli/src/lib.rs | 221 +- components/hash-commands/Cargo.toml | 23 + components/hash-commands/README.md | 5 + components/keyspace-commands/Cargo.toml | 23 + components/keyspace-commands/README.md | 5 + components/ops/README.md | 3 - components/ops/src/lib.rs | 1380 ---------- components/ops/src/resp/mod.rs | 358 --- components/pubsub-commands/Cargo.toml | 23 + components/pubsub-commands/README.md | 5 + components/resp/Cargo.toml | 17 + components/resp/README.md | 5 + components/resp/src/lib.rs | 71 + components/resp/tests/resp.rs | 80 + components/sample-http-incrementor/Cargo.toml | 2 +- components/sample-http-incrementor/README.md | 4 +- components/sample-http-incrementor/src/lib.rs | 2 - components/string-commands/Cargo.toml | 23 + components/string-commands/README.md | 5 + components/{ops => tcp-connector}/Cargo.toml | 5 +- components/tcp-connector/README.md | 14 + components/tcp-connector/src/lib.rs | 76 + components/tcp/Cargo.toml | 19 + components/tcp/README.md | 5 + components/tcp/src/lib.rs | 291 ++ components/tcp/tests/tcp.rs | 260 ++ components/test-commands/README.md | 3 + components/test-commands/test-commands.wac | 21 + components/test-tcp/README.md | 3 + components/test-tcp/test-tcp.wac | 15 + components/wit/worlds.wit | 78 +- crates/commands/src/admin.rs | 39 + crates/commands/src/connection.rs | 48 + crates/commands/src/hash.rs | 332 +++ crates/commands/src/keyspace.rs | 73 + crates/commands/src/lib.rs | 209 ++ crates/commands/src/pubsub.rs | 17 + crates/commands/src/reply.rs | 132 + crates/commands/src/string.rs | 37 + crates/resp-codec/Cargo.toml | 9 + .../resp-codec}/LICENSE-APACHE.txt | 0 .../resp-codec}/LICENSE-MIT.txt | 0 .../resp => crates/resp-codec}/LICENSE.txt | 0 .../src/resp => crates/resp-codec}/README.md | 0 crates/resp-codec/src/codec.rs | 424 +++ crates/resp-codec/src/lib.rs | 35 + crates/test-harness/Cargo.toml | 12 + crates/test-harness/src/lib.rs | 660 +++++ crates/test-harness/src/server.rs | 82 + wit/admin-commands.wit | 86 + wit/bitmap-commands.wit | 13 + wit/connection-commands.wit | 58 + wit/connection.wit | 86 + wit/connector.wit | 11 + wit/geo-commands.wit | 16 + wit/hash-commands.wit | 70 + wit/hyperloglog-commands.wit | 11 + wit/keyspace-commands.wit | 65 + wit/list-commands.wit | 30 + wit/pubsub-commands.wit | 22 + wit/scripting-commands.wit | 26 + wit/set-commands.wit | 25 + wit/sortedset-commands.wit | 43 + wit/store.wit | 94 - wit/stream-commands.wit | 27 + wit/string-commands.wit | 31 + wit/tcp.wit | 32 + wit/transaction-commands.wit | 11 + wit/worlds.wit | 21 +- 95 files changed, 8120 insertions(+), 2187 deletions(-) create mode 100644 components/admin-commands/Cargo.toml create mode 100644 components/admin-commands/README.md create mode 100644 components/as-keyvalue/tests/keyvalue.rs create mode 100644 components/commands/Cargo.toml create mode 100644 components/commands/README.md create mode 100644 components/commands/tests/commands.rs create mode 100644 components/connection-commands/Cargo.toml create mode 100644 components/connection-commands/README.md create mode 100644 components/connection/Cargo.toml create mode 100644 components/connection/README.md create mode 100644 components/connection/src/lib.rs create mode 100644 components/connection/tests/connection.rs create mode 100644 components/hash-commands/Cargo.toml create mode 100644 components/hash-commands/README.md create mode 100644 components/keyspace-commands/Cargo.toml create mode 100644 components/keyspace-commands/README.md delete mode 100644 components/ops/README.md delete mode 100644 components/ops/src/lib.rs delete mode 100644 components/ops/src/resp/mod.rs create mode 100644 components/pubsub-commands/Cargo.toml create mode 100644 components/pubsub-commands/README.md create mode 100644 components/resp/Cargo.toml create mode 100644 components/resp/README.md create mode 100644 components/resp/src/lib.rs create mode 100644 components/resp/tests/resp.rs create mode 100644 components/string-commands/Cargo.toml create mode 100644 components/string-commands/README.md rename components/{ops => tcp-connector}/Cargo.toml (64%) create mode 100644 components/tcp-connector/README.md create mode 100644 components/tcp-connector/src/lib.rs create mode 100644 components/tcp/Cargo.toml create mode 100644 components/tcp/README.md create mode 100644 components/tcp/src/lib.rs create mode 100644 components/tcp/tests/tcp.rs create mode 100644 components/test-commands/README.md create mode 100644 components/test-commands/test-commands.wac create mode 100644 components/test-tcp/README.md create mode 100644 components/test-tcp/test-tcp.wac create mode 100644 crates/commands/src/admin.rs create mode 100644 crates/commands/src/connection.rs create mode 100644 crates/commands/src/hash.rs create mode 100644 crates/commands/src/keyspace.rs create mode 100644 crates/commands/src/lib.rs create mode 100644 crates/commands/src/pubsub.rs create mode 100644 crates/commands/src/reply.rs create mode 100644 crates/commands/src/string.rs create mode 100644 crates/resp-codec/Cargo.toml rename {components/ops/src/resp => crates/resp-codec}/LICENSE-APACHE.txt (100%) rename {components/ops/src/resp => crates/resp-codec}/LICENSE-MIT.txt (100%) rename {components/ops/src/resp => crates/resp-codec}/LICENSE.txt (100%) rename {components/ops/src/resp => crates/resp-codec}/README.md (100%) create mode 100644 crates/resp-codec/src/codec.rs create mode 100644 crates/resp-codec/src/lib.rs create mode 100644 crates/test-harness/Cargo.toml create mode 100644 crates/test-harness/src/lib.rs create mode 100644 crates/test-harness/src/server.rs create mode 100644 wit/admin-commands.wit create mode 100644 wit/bitmap-commands.wit create mode 100644 wit/connection-commands.wit create mode 100644 wit/connection.wit create mode 100644 wit/connector.wit create mode 100644 wit/geo-commands.wit create mode 100644 wit/hash-commands.wit create mode 100644 wit/hyperloglog-commands.wit create mode 100644 wit/keyspace-commands.wit create mode 100644 wit/list-commands.wit create mode 100644 wit/pubsub-commands.wit create mode 100644 wit/scripting-commands.wit create mode 100644 wit/set-commands.wit create mode 100644 wit/sortedset-commands.wit delete mode 100644 wit/store.wit create mode 100644 wit/stream-commands.wit create mode 100644 wit/string-commands.wit create mode 100644 wit/tcp.wit create mode 100644 wit/transaction-commands.wit diff --git a/.github/workflows/bump-version.yaml b/.github/workflows/bump-version.yaml index b139c64..8c153d6 100644 --- a/.github/workflows/bump-version.yaml +++ b/.github/workflows/bump-version.yaml @@ -28,7 +28,7 @@ jobs: make tools make -s tools-path >> "${GITHUB_PATH}" - name: Start Valkey service - run: docker run --rm --name valkey -d -p 6379:6379 valkey/valkey:8 + run: docker run --rm --name valkey -d -p 6379:6379 valkey/valkey:9 - name: Bump version # also fetches the wit dependencies for the new version, and builds and tests the components run: scripts/bump-version.sh "${VERSION}" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 21bf394..b6e068a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -39,9 +39,11 @@ jobs: path: components.tar retention-days: 7 - name: Start Valkey service - run: docker run --rm --name valkey -d -p 6379:6379 valkey/valkey:8 + run: docker run --rm --name valkey -d -p 6379:6379 valkey/valkey:9 - name: Test run: make test + - name: Test against Valkey + run: make test-valkey - name: Capture WIT working-directory: ./target/components run: | diff --git a/Cargo.lock b/Cargo.lock index 1a626fc..2222435 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,35 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + +[[package]] +name = "admin-commands" +version = "0.1.0" +dependencies = [ + "resp-codec", + "wit-bindgen", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "anstream" version = "1.0.0" @@ -38,7 +67,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -49,28 +78,125 @@ checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", "once_cell", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" [[package]] name = "as-keyvalue" version = "0.1.0" dependencies = [ + "test-harness", + "tokio", + "wasmtime", "wit-bindgen", ] +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cap-primitives" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes 3.0.1", + "ipnet", + "maybe-owned", + "rustix", + "rustix-linux-procfs", + "windows-sys 0.61.2", + "winx", +] + +[[package]] +name = "cc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core", +] + [[package]] name = "clap" version = "4.6.7" @@ -111,6 +237,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.21", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -118,346 +253,2112 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] -name = "dep-cli" +name = "commands" version = "0.1.0" dependencies = [ - "clap", + "resp-codec", + "test-harness", + "tokio", + "wasmtime", "wit-bindgen", ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "connection" +version = "0.1.0" +dependencies = [ + "futures-channel", + "futures-core", + "resp-codec", + "test-harness", + "tokio", + "wasmtime", + "wit-bindgen", +] + +[[package]] +name = "connection-commands" +version = "0.1.0" +dependencies = [ + "resp-codec", + "wit-bindgen", +] + +[[package]] +name = "core_detect" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" [[package]] -name = "foldhash" -version = "0.2.0" +name = "cpp_demangle" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" +dependencies = [ + "cfg-if", +] [[package]] -name = "futures" -version = "0.3.34" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "libc", ] [[package]] -name = "futures-channel" -version = "0.3.34" +name = "cpufeatures" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ - "futures-core", - "futures-sink", + "libc", ] [[package]] -name = "futures-core" -version = "0.3.34" +name = "cranelift-assembler-x64" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +checksum = "658e8623bea025d142fb11c299b8c6572ffe9fd663b52887c4dc124735cc130b" +dependencies = [ + "cranelift-assembler-x64-meta", +] [[package]] -name = "futures-io" -version = "0.3.34" +name = "cranelift-assembler-x64-meta" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" +checksum = "a4b12b8c55c31c3d8598c2de9a657c3b978f905f6886c43e02216e0b15fe45a3" +dependencies = [ + "cranelift-srcgen", +] [[package]] -name = "futures-sink" -version = "0.3.34" +name = "cranelift-bforest" +version = "0.136.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" +checksum = "f4710c34489bc844e45c1c0005d3e44d3d2b29cee77187507ba8d0bb400c621f" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] [[package]] -name = "futures-task" -version = "0.3.34" +name = "cranelift-bitset" +version = "0.136.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" +checksum = "76b63a4b0cfea80610150fffda11b1a9c3dc5a835403d9f381bf7d1a2874b4d7" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] [[package]] -name = "futures-util" -version = "0.3.34" +name = "cranelift-codegen" +version = "0.136.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +checksum = "e1af881f3b3392433e6f4f9b172b8d99270c538db4effea62b28bc48f02f0833" dependencies = [ - "futures-core", - "futures-sink", - "futures-task", - "pin-project-lite", - "slab", + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.17.0", + "libm", + "log", + "postcard", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", ] [[package]] -name = "hashbrown" -version = "0.17.0" +name = "cranelift-codegen-meta" +version = "0.136.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "24ce99c30dc7a692294613241119eea0de3c3cb4cf10ac2570ed72adf9a8c2eb" dependencies = [ - "foldhash", + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", ] [[package]] -name = "heck" -version = "0.5.0" +name = "cranelift-codegen-shared" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "df315a8b1cfd6e620971e3d4249bd2c4528167717e8f48a6317a87c55891bcb8" [[package]] -name = "id-arena" -version = "2.3.0" +name = "cranelift-control" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +checksum = "2c3fff7610fb17f228f16598e1a044db79a12806ece7496ff2c69cf1a27a79a5" +dependencies = [ + "arbitrary", +] [[package]] -name = "indexmap" -version = "2.14.0" +name = "cranelift-entity" +version = "0.136.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "e73c65538d460b1062cb9a97428ee6bb4b616f5cb17be3b3ac0a3e4f1f789166" dependencies = [ - "equivalent", - "hashbrown", + "cranelift-bitset", "serde", - "serde_core", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.136.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ed70ab5849184973c42b57366f57611dc81c3e355d5b612c68a2900bea92242" +dependencies = [ + "cranelift-codegen", + "hashbrown 0.17.0", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.136.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "379f10a451c9b1bf4f59e4ce38061c53ca2454783041a3c1fafa6deef3c37108" + +[[package]] +name = "cranelift-native" +version = "0.136.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f496865b1a14bc85bcc9fb51a9539ca93b1d1f44fb92bd6a091fba02d6ab4a7" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4944ef05513e1c1a85a945cf8f29d95ddea8bcbb19e0bfd4e71063450967420" + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "dep-cli" +version = "0.1.0" +dependencies = [ + "clap", + "wit-bindgen", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes 2.0.4", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags", + "debugid", + "rustc-hash", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", +] + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "hash-commands" +version = "0.1.0" +dependencies = [ + "resp-codec", + "wit-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "io-extras" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" +dependencies = [ + "io-lifetimes 3.0.1", + "windows-sys 0.59.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "io-lifetimes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keyspace-commands" +version = "0.1.0" +dependencies = [ + "resp-codec", + "wit-bindgen", +] + +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "macro-string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memfd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" +dependencies = [ + "rustix", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" +dependencies = [ + "crc32fast", + "hashbrown 0.17.0", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pubsub-commands" +version = "0.1.0" +dependencies = [ + "resp-codec", + "wit-bindgen", +] + +[[package]] +name = "pulley-interpreter" +version = "49.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0091857bc410bd7117eb6ac042ced7714c69f994a8b9c2ae30add2f73e3a25ac" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "49.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8798ebc6bf0ef227e204fa3fad05b04970599a85f456fd28365450fd679b2600" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.0", + "log", + "rustc-hash", + "serde", + "smallvec", +] + +[[package]] +name = "resp" +version = "0.1.0" +dependencies = [ + "resp-codec", + "test-harness", + "tokio", + "wasmtime", + "wit-bindgen", +] + +[[package]] +name = "resp-codec" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "sample-http-incrementor" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string-commands" +version = "0.1.0" +dependencies = [ + "resp-codec", + "wit-bindgen", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tcp" +version = "0.1.0" +dependencies = [ + "futures-channel", + "futures-core", + "resp-codec", + "test-harness", + "tokio", + "wasmtime", + "wit-bindgen", +] + +[[package]] +name = "tcp-connector" +version = "0.1.0" +dependencies = [ + "resp-codec", + "wit-bindgen", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "test-harness" +version = "0.1.0" +dependencies = [ + "resp-codec", + "tokio", + "wasmtime", + "wasmtime-wasi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.3", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-compose" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd08e5283f98238f2f16f5f978188a33af9901b81186bb549faad306a7b5388" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "log", + "petgraph", + "smallvec", + "wasm-encoder 0.258.0", + "wasmparser 0.258.0", + "wat", +] + +[[package]] +name = "wasm-encoder" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e974fe6821a8cf64575d51ea2194e2c8f77e7b66e9afe7419ce8a97f9ee0d251" +dependencies = [ + "leb128fmt", + "wasmparser 0.258.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2608e8bb6d67fd68f5a8d0eb1363d6e7bcbc1f8ded5a0bd3a1e382462b876b22" +dependencies = [ + "leb128fmt", + "wasmparser 0.261.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a11585adb92fe9b55ad1d760e8d8fb5d87e0d2e303cb8eed57f078d54293a2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.258.0", + "wasmparser 0.258.0", +] + +[[package]] +name = "wasmparser" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a61719f93a87b16d325921e251800c4833f8fab50fa21c7de73aed50086313" +dependencies = [ + "bitflags", + "hashbrown 0.17.0", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f20f20e44f7e8aeb6744823ea9d869ede51e51be4fdaedede2852282e54d2d8" +dependencies = [ + "bitflags", + "indexmap", + "semver", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.1" +name = "wasmprinter" +version = "0.258.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "3a65e30fb2cd3cc5cb7761a6064787b2abf017e6566d8d0dedf2bb7f55ad5383" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.258.0", +] [[package]] -name = "itoa" -version = "1.0.14" +name = "wasmtime" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "942ddd2fc5800ec01d4b11c28ad62ab00517dcd416aff728f7d73fddf75872c1" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "bytes", + "cc", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.258.0", + "wasmparser 0.258.0", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wat", + "windows-sys 0.61.2", + "wit-parser", +] [[package]] -name = "leb128fmt" -version = "0.1.0" +name = "wasmtime-environ" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "98f2ca21b23a51c1b1944297e9671a628c368e92aa6250e4c38f48e2dc251f0c" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.17.0", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasm-encoder 0.258.0", + "wasmparser 0.258.0", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] [[package]] -name = "log" -version = "0.4.26" +name = "wasmtime-internal-cache" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "76c83428ffb93757881522619aef1f6571a235c25ea5e9e6b67aa6a2324da72a" +dependencies = [ + "base64", + "directories-next", + "log", + "postcard", + "rustix", + "serde", + "serde_derive", + "sha2", + "toml", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] [[package]] -name = "macro-string" -version = "0.2.0" +name = "wasmtime-internal-component-macro" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" +checksum = "92c42f95d879875994319bd6689b17570e68aebd1a17e0dc54430928ed0699b5" dependencies = [ + "anyhow", "proc-macro2", "quote", "syn 2.0.117", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", ] [[package]] -name = "memchr" -version = "2.7.4" +name = "wasmtime-internal-component-util" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "294c7f12f062ceb148cd26c77170ff739552db76d1f203f6127f249684988a70" [[package]] -name = "once_cell" -version = "1.20.3" +name = "wasmtime-internal-core" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" - -[[package]] -name = "ops" -version = "0.1.0" +checksum = "64b67114739063bb72bb8ed2b4994b8b22809d354432db68553ad529850f9536" dependencies = [ - "wit-bindgen", + "anyhow", + "hashbrown 0.17.0", + "libm", + "serde", ] [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "wasmtime-internal-cranelift" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "b7d19868a30cc20df6f6648c9a4b3f700fcd1ddd0a63af9a98cb75f2b58dc874" +dependencies = [ + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.21", + "wasmparser 0.258.0", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] [[package]] -name = "prettyplease" -version = "0.2.29" +name = "wasmtime-internal-fiber" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +checksum = "4f1999aee268c3f625666b12ba0ded55a6ac2c800b60b426af7d164d63411908" dependencies = [ - "proc-macro2", - "syn 2.0.117", + "cc", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", ] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "wasmtime-internal-jit-debug" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "33ca2499301c6f11cc9eb42c2beafc8efae396ae4cb91077e3ac080dc5d765e4" dependencies = [ - "unicode-ident", + "cc", + "object", + "rustix", + "wasmtime-internal-versioned-export-macros", ] [[package]] -name = "quote" -version = "1.0.45" +name = "wasmtime-internal-jit-icache-coherence" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "7162d829c56265e183d23b17d67afc5c6876433daaed54c137388a8078a1bb6b" dependencies = [ - "proc-macro2", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", ] [[package]] -name = "ryu" -version = "1.0.19" +name = "wasmtime-internal-unwinder" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "4e167cf144f3cee40838f7663e3a589f93bba65151efa084a2d58b2bd47469f1" +dependencies = [ + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] [[package]] -name = "sample-http-incrementor" -version = "0.1.0" +name = "wasmtime-internal-versioned-export-macros" +version = "49.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d09e038ad69c741dae29da4606ccfc0df66f156e62cb7f0664c8d7d1272a36d" dependencies = [ - "wit-bindgen", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "semver" -version = "1.0.25" +name = "wasmtime-internal-wit-bindgen" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "c63d97a742fc9b415dad7ed2ac33ddb8fe6c5413fcd0c7ee58907f2ab6f9a430" +dependencies = [ + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-component", + "wit-parser", +] [[package]] -name = "serde" -version = "1.0.228" +name = "wasmtime-wasi" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "ce11727cb331abeaace3681a4b072e4fe04818d7b99786d183b318671f9e4e0e" dependencies = [ - "serde_core", + "async-trait", + "bitflags", + "bytes", + "cap-primitives", + "futures", + "rand", + "rustix", + "rustix-linux-procfs", + "thiserror 2.0.21", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "wiggle", + "windows-sys 0.61.2", + "winx", ] [[package]] -name = "serde_core" -version = "1.0.228" +name = "wasmtime-wasi-io" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "35f5ed8d710d8daf71cb4742c6b90c1fd121acb62b6848556eb4e0661cbd28bd" dependencies = [ - "serde_derive", + "async-trait", + "bytes", + "futures", + "tracing", + "wasmtime", ] [[package]] -name = "serde_derive" -version = "1.0.228" +name = "wast" +version = "35.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "leb128", ] [[package]] -name = "serde_json" -version = "1.0.139" +name = "wast" +version = "261.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +checksum = "776443145731a4062e5b0d392892a2005909b6ab72d9fdc3cad53dd1a714e44a" dependencies = [ - "itoa", + "bumpalo", + "leb128fmt", "memchr", - "ryu", - "serde", + "unicode-width", + "wasm-encoder 0.261.0", ] [[package]] -name = "slab" -version = "0.4.12" +name = "wat" +version = "1.261.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "a7b4d1a49ea73a8f3326e74e3a05db667001b16bd1035ed3356fc1a0ed05ca7f" +dependencies = [ + "wast 261.0.0", +] [[package]] -name = "strsim" -version = "0.11.1" +name = "wiggle" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "3dfb96da9ecf96621fee71a01916ca99038cc8f212cd19a8923fb32c36060477" +dependencies = [ + "bitflags", + "thiserror 2.0.21", + "tracing", + "wasmtime", + "wasmtime-environ", + "wiggle-macro", +] [[package]] -name = "syn" -version = "2.0.117" +name = "wiggle-generate" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "ba7cbdab1c5fa79f98a96c3b0b02b81f83585a4cdc3ce1d2744e6cfa2aef333b" dependencies = [ + "heck", "proc-macro2", "quote", - "unicode-ident", + "syn 2.0.117", + "wasmtime-environ", + "witx", ] [[package]] -name = "syn" -version = "3.0.3" +name = "wiggle-macro" +version = "49.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "daa97f5b7ad6ace3254b6bec1f82ef53f7c49c4cf5f117fad4171acc15fd6ff0" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn 2.0.117", + "wiggle-generate", ] [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] [[package]] -name = "utf8parse" -version = "0.2.2" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "wasm-encoder" -version = "0.258.0" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e974fe6821a8cf64575d51ea2194e2c8f77e7b66e9afe7419ce8a97f9ee0d251" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "leb128fmt", - "wasmparser", + "windows-sys 0.59.0", ] [[package]] -name = "wasm-metadata" -version = "0.258.0" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a11585adb92fe9b55ad1d760e8d8fb5d87e0d2e303cb8eed57f078d54293a2" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "wasmparser" -version = "0.258.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a61719f93a87b16d325921e251800c4833f8fab50fa21c7de73aed50086313" -dependencies = [ - "bitflags", - "hashbrown", - "indexmap", - "semver", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" @@ -468,6 +2369,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -532,6 +2442,22 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.61.1" @@ -599,9 +2525,9 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", + "wasm-encoder 0.258.0", "wasm-metadata", - "wasmparser", + "wasmparser 0.258.0", "wit-parser", ] @@ -612,7 +2538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff4daaa3cd97ae49ecd0a99dc009d453f93e0f083dd3be38c0f24a83a93e37ac" dependencies = [ "anyhow", - "hashbrown", + "hashbrown 0.17.0", "id-arena", "indexmap", "log", @@ -621,5 +2547,128 @@ dependencies = [ "serde_derive", "serde_json", "unicode-ident", - "wasmparser", + "wasmparser 0.258.0", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.1+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeec9eaf2dffbbd09201e23bd0ffcbaa33bb8e9266a10734fd7ed90a85eca078" +dependencies = [ + "cc", + "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index 56cd6f2..87f0942 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,35 @@ [workspace] resolver = "2" members = [ + "components/admin-commands", "components/as-keyvalue", + "components/commands", + "components/connection", + "components/connection-commands", "components/dep-cli", - "components/ops", + "components/hash-commands", + "components/keyspace-commands", + "components/pubsub-commands", + "components/resp", "components/sample-http-incrementor", + "components/string-commands", + "components/tcp", + "components/tcp-connector", + "crates/resp-codec", + "crates/test-harness", ] [workspace.package] version = "0.3.0-dev" [workspace.dependencies] +futures-channel = "0.3" +futures-core = "0.3" +resp-codec = { path = "./crates/resp-codec" } +test-harness = { path = "./crates/test-harness" } +tokio = { version = "1", features = ["io-util", "macros", "net", "rt-multi-thread", "sync", "time"] } +wasmtime = { version = "49", features = ["component-model-async"] } +wasmtime-wasi = { version = "49", features = ["p3"] } wit-bindgen = "0.61.0" [profile.release] diff --git a/Makefile b/Makefile index 17b8b96..e022e16 100644 --- a/Makefile +++ b/Makefile @@ -26,12 +26,20 @@ clean: clean-components clean-wit clean-all: clean-components clean-tools clean-wit cargo clean +# runs the cli with the given arguments, e.g. `$(call run_cli,hello)` +run_cli = $(WASMTIME) run $(WASMTIME_RUN_FLAGS) ${COMPONENTS_DIR}/cli/cli.debug.wasm $(1) .PHONY: run ## Run the cli against a Valkey server, e.g. `make run cmd="keys '*'"` run: ${COMPONENTS_DIR}/cli/cli.debug.wasm | $(call tool,wasmtime-cli) - @$(WASMTIME) run $(WASMTIME_RUN_FLAGS) ${COMPONENTS_DIR}/cli/cli.debug.wasm $(cmd) + @$(call run_cli,$(cmd)) + +# the tests that need a Valkey server on localhost:6379, ignored tests in `with_valkey` modules, +# other ignored tests stay ignored +.PHONY: test-valkey +test-valkey: components + cargo test --workspace -- --ignored with_valkey:: + @$(call run_cli,hello) -# requires a running Valkey server .PHONY: test test: components - @$(MAKE) run cmd="hello" + cargo test --workspace diff --git a/README.md b/README.md index fb42323..625b495 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ A WASM component client for Valkey (and Redis). +- [Components](#components) - [Build](#build) +- [Test](#test) - [Run](#run) - [Samples](#samples) - [Community](#community) @@ -13,6 +15,23 @@ A WASM component client for Valkey (and Redis). - [License](#license) +## Components + +The `componentized:valkey` WIT package, in [`wit`](./wit), separates the connection to a server from the transport carrying it, and from the commands sent over it. Connections always speak RESP3. + +| Component | | +| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`resp`](./components/resp/) | encodes and decodes RESP values, the types every other component shares | +| [`connection`](./components/connection/) | the mechanics of a connection: pairs commands with replies, keeps push messages apart, establishes it with `HELLO 3`, optionally keeps it alive | +| [`tcp`](./components/tcp/) | connections over wasi:sockets TCP | +| [`tcp-connector`](./components/tcp-connector/) | connections over TCP to the server configured with wasi:config, for components that need a connection without a say in how it is made | +| [`commands`](./components/commands/) | the commands, an interface for each of Valkey's ACL categories, e.g. `hash-commands`, each also available as a component of its own, e.g. [`hash-commands`](./components/hash-commands/) | +| [`as-keyvalue`](./components/as-keyvalue/) | wasmcloud:keyvalue, backed by Valkey hashes | +| [`client`](./components/client/) | everything above, composed into one component | +| [`cli`](./components/cli/) | a command line client | + +A composition picks the transport, e.g. `tcp` and `tcp-connector`, other transports can provide connections to the same commands. The [`*.wac`](./components/client/client.wac) files show how the components are composed. + ## Build A [dev container](https://containers.dev) is available that contains the necessary tools and configuration out of the box. @@ -29,6 +48,18 @@ The build creates each component in [`components`](./components) into `target/co The cli tools the build uses, [`wasm-tools`](https://github.com/bytecodealliance/wasm-tools), [`wac`](https://github.com/bytecodealliance/wac), [`wasmtime`](https://github.com/bytecodealliance/wasmtime) and [`wkg`](https://github.com/bytecodealliance/wasm-pkg-tools), are pinned in [`tools/Cargo.toml`](./tools/Cargo.toml) and installed into `target/tools/`, e.g. `target/tools/aarch64-apple-darwin`, as needed, or ahead of time with `make tools`. Dependabot bumps the pinned versions. +## Test + +```sh +make test +``` + +Tests the components without a Valkey server, playing the server themselves, then runs the cli against a Valkey server on `localhost:6379`. The tests against a Valkey server, ignored by `cargo test`, run with: + +```sh +make test-valkey +``` + ## Run Prereqs: @@ -42,6 +73,9 @@ wasmtime run -Sinherit-network -Sallow-ip-name-lookup target/components/cli/cli. - use `--host` to specify a host other than `127.0.0.1` - use `--port` to specify a port other than `6379` +- use `--password`, and `--username` for a user other than `default`, to authenticate +- use `--client-name` to name the connection +- use `--help` for the commands, and their options - `-Sallow-ip-name-lookup` is only required if a hostname is used for the connection instead of an IP address. To aid incremental development a make target is available to rebuild and run the CLI: diff --git a/components/admin-commands/Cargo.toml b/components/admin-commands/Cargo.toml new file mode 100644 index 0000000..c267832 --- /dev/null +++ b/components/admin-commands/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "admin-commands" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] +# shared with the other command components +path = "../../crates/commands/src/lib.rs" + +[features] +default = ["admin"] +admin = [] +connection = [] +hash = [] +keyspace = [] +pubsub = [] +string = [] + +[dependencies] +resp-codec = { workspace = true } +wit-bindgen = { workspace = true } diff --git a/components/admin-commands/README.md b/components/admin-commands/README.md new file mode 100644 index 0000000..8e7532a --- /dev/null +++ b/components/admin-commands/README.md @@ -0,0 +1,5 @@ +# admin-commands + +Administrative commands, Valkey's @admin ACL category, and the rest of the ACL family. + +Exports `componentized:valkey/admin-commands` over an imported [`connection`](../connection/). [`commands`](../commands/) exports every command interface in one component. diff --git a/components/as-keyvalue/Cargo.toml b/components/as-keyvalue/Cargo.toml index 65e84e2..c326a37 100644 --- a/components/as-keyvalue/Cargo.toml +++ b/components/as-keyvalue/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "as-keyvalue" version = "0.1.0" -edition = "2021" +edition = "2024" license = "Apache-2.0" [lib] @@ -9,3 +9,8 @@ crate-type = ["cdylib"] [dependencies] wit-bindgen = { workspace = true } + +[dev-dependencies] +test-harness = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } diff --git a/components/as-keyvalue/README.md b/components/as-keyvalue/README.md index 7c5671f..030a1fc 100644 --- a/components/as-keyvalue/README.md +++ b/components/as-keyvalue/README.md @@ -4,3 +4,16 @@ Adapter to back wasmcloud:keyvalue with the componentized:valkey client. > [!NOTE] > `wasi:keyvalue` is currently unsupportable with an async based client. It will be restored when either it updates it's own API to be async, or the component model is fully able to make async calls from sync functions. + +Each bucket is a Valkey hash, its keys are the hash's fields. The hash is named for the bucket's identifier, after an optional prefix: + +| Key | Default | | +| --- | --- | --- | +| `key-prefix` | | prefix of the hash for each bucket, e.g. `app:` stores the bucket `users` in the hash `app:users` | + +Opening a bucket establishes a new connection with the imported `connector`, which decides the transport and the server, e.g. [`tcp-connector`](../tcp-connector/) with its own configuration. A connector that fails to connect makes the store unavailable. [`client`](../client/) composes it with `tcp-connector`. + +- values are stored as strings, a value must be UTF-8 +- a ttl expires the key with `HEXPIRE`, which needs Valkey 9 or later and has whole second precision, a ttl is rounded up to whole seconds +- keys are listed with `HSCAN`, a prefix matches literally +- compare and swap, `wasmcloud:keyvalue/cas`, is not supported diff --git a/components/as-keyvalue/src/lib.rs b/components/as-keyvalue/src/lib.rs index 31966c0..ae1e4ce 100644 --- a/components/as-keyvalue/src/lib.rs +++ b/components/as-keyvalue/src/lib.rs @@ -1,6 +1,6 @@ -#![no_main] - -use componentized::valkey::store::{self as valkey, Connection, Duration, HelloOpts, HscanOpts}; +use componentized::valkey::connection::{Connection, Error as ValkeyError}; +use componentized::valkey::connector; +use componentized::valkey::hash_commands::{self, Duration, HscanOpts}; use exports::wasmcloud::keyvalue::atomics::Guest as AtomicsGuest; use exports::wasmcloud::keyvalue::batch::Guest as BatchGuest; use exports::wasmcloud::keyvalue::store::Guest as StoreGuest; @@ -9,13 +9,6 @@ use exports::wasmcloud::keyvalue::types::{ }; use wasi::config::store::{self as config}; -const HOSTNAME_KEY: &str = "hostname"; -const HOSTNAME_DEFAULT: &str = "127.0.0.1"; -const PORT_KEY: &str = "port"; -const PORT_DEFAULT: &str = "6379"; -const USERNAME_KEY: &str = "username"; -const USERNAME_DEFAULT: &str = "default"; -const PASSWORD_KEY: &str = "password"; const KEY_PREFIX_KEY: &str = "key-prefix"; const KEY_PREFIX_DEFAULT: &str = ""; @@ -28,25 +21,8 @@ impl TypesGuest for AsKeyvalue { impl StoreGuest for AsKeyvalue { async fn open(identifier: String) -> Result { - let hostname: String = config::get(HOSTNAME_KEY)?.unwrap_or(HOSTNAME_DEFAULT.to_string()); - let port = config::get(PORT_KEY)?.unwrap_or(PORT_DEFAULT.to_string()); - let port: u16 = port - .parse() - .map_err(|_| Error::Other(String::from("port must be an integer")))?; - - let opts = HelloOpts { - proto_ver: Some("3".to_string()), - auth: match config::get(PASSWORD_KEY)? { - Some(password) => { - let username: String = - config::get(USERNAME_KEY)?.unwrap_or(USERNAME_DEFAULT.to_string()); - Some((username, password)) - } - None => None, - }, - client_name: None, - }; - let connection = valkey::connect(hostname, port, Some(opts)) + // the connector decides the transport and the server, e.g. from its own configuration + let connection = connector::connect() .await .map_err(|_| Error::StoreUnavailable)?; @@ -67,7 +43,7 @@ struct KeyvalueToValkeyBucket { impl GuestBucket for KeyvalueToValkeyBucket { async fn get(&self, key: String) -> Result>, Error> { - match self.connection.hget(self.hash_key.clone(), key).await? { + match hash_commands::hget(&self.connection, self.hash_key.clone(), key).await? { Some(value) => Ok(Some(value.into_bytes())), None => Ok(None), } @@ -86,34 +62,36 @@ impl GuestBucket for KeyvalueToValkeyBucket { }); if options.if_not_exists { - match self - .connection - .hsetnx(self.hash_key.clone(), key.clone(), value) + match hash_commands::hsetnx(&self.connection, self.hash_key.clone(), key.clone(), value) .await? { true => (), false => Err(Error::PreconditionFailed)?, } } else { - self.connection - .hset(self.hash_key.clone(), key.clone(), value) - .await? + hash_commands::hset(&self.connection, self.hash_key.clone(), key.clone(), value).await? } if let Some(ttl_ms) = options.ttl_ms { - let ttl: Duration = ttl_ms / 1000; - self.connection - .hexpire(self.hash_key.clone(), ttl, None, vec![key]) - .await?; + // HEXPIRE has whole second precision, rounded up so a short ttl does not expire at once + let ttl: Duration = ttl_ms.div_ceil(1000) * 1_000_000_000; + hash_commands::hexpire( + &self.connection, + self.hash_key.clone(), + ttl, + None, + vec![key], + ) + .await?; } Ok(()) } async fn delete(&self, key: String) -> Result<(), Error> { - Ok(self.connection.hdel(self.hash_key.clone(), key).await?) + Ok(hash_commands::hdel(&self.connection, self.hash_key.clone(), key).await?) } async fn exists(&self, key: String) -> Result { - Ok(self.connection.hexists(self.hash_key.clone(), key).await?) + Ok(hash_commands::hexists(&self.connection, self.hash_key.clone(), key).await?) } async fn list_keys( @@ -126,10 +104,9 @@ impl GuestBucket for KeyvalueToValkeyBucket { count: None, no_values: Some(true), }; - let (cursor, fields) = self - .connection - .hscan(self.hash_key.clone(), cursor, Some(opts)) - .await?; + let (cursor, fields) = + hash_commands::hscan(&self.connection, self.hash_key.clone(), cursor, Some(opts)) + .await?; Ok(KeyResponse { keys: fields.into_iter().map(|(key, _)| key).collect(), @@ -142,10 +119,7 @@ impl AtomicsGuest for AsKeyvalue { async fn increment(bucket: BucketBorrow<'_>, key: String, delta: i64) -> Result { let bucket: &KeyvalueToValkeyBucket = bucket.get(); - Ok(bucket - .connection - .hincrby(bucket.hash_key.clone(), key, delta) - .await?) + Ok(hash_commands::hincrby(&bucket.connection, bucket.hash_key.clone(), key, delta).await?) } } @@ -214,10 +188,13 @@ impl From for Error { } } -impl From for Error { - fn from(e: valkey::Error) -> Self { - // TODO distinguish between IO, auth and other types of errors - Self::Other(format!("Valkey: {e}")) +impl From for Error { + fn from(e: ValkeyError) -> Self { + match e { + // the transport closed, retrying with a new bucket may succeed + ValkeyError::Closed(_) => Self::StoreUnavailable, + e => Self::Other(format!("Valkey: {e}")), + } } } diff --git a/components/as-keyvalue/tests/keyvalue.rs b/components/as-keyvalue/tests/keyvalue.rs new file mode 100644 index 0000000..4722d86 --- /dev/null +++ b/components/as-keyvalue/tests/keyvalue.rs @@ -0,0 +1,146 @@ +//! Tests for `as-keyvalue`, run against `client`, which composes it with `tcp-connector` and the +//! rest of the client components. + +use std::time::Duration; + +use test_harness::Harness; +use test_harness::keyvalue::types::{Error, KeyResponse, SetOptions}; + +/// Each bucket is a hash, named for the test, with a prefix to keep them apart from other data. +const KEY_PREFIX: &str = "as-keyvalue-test:"; + +#[tokio::test(flavor = "multi_thread")] +async fn unreachable_servers_are_unavailable() -> wasmtime::Result<()> { + // a port nothing listens on + let port = test_harness::Server::bind().await?.port().to_string(); + let mut harness = Harness::with_config("client", &[("port", &port)]).await?; + harness + .run(async |accessor, exports| { + let bucket = exports + .keyvalue_store() + .call_open(accessor, "bucket".to_string()) + .await?; + assert_eq!(bucket.err(), Some(Error::StoreUnavailable)); + Ok(()) + }) + .await +} + +/// The tests against a Valkey server on localhost:6379, ignored unless run with +/// `make test-valkey`, which only runs the ignored tests in `with_valkey` modules. +mod with_valkey { + use super::*; + + /// Against a Valkey server on localhost:6379, run with `cargo test -p as-keyvalue -- --ignored with_valkey::`. + #[tokio::test(flavor = "multi_thread")] + #[ignore = "needs a Valkey server on localhost:6379"] + async fn buckets_against_valkey() -> wasmtime::Result<()> { + let mut harness = Harness::with_config("client", &[("key-prefix", KEY_PREFIX)]).await?; + harness + .run(async |accessor, exports| { + let s = |value: &str| value.to_string(); + let b = |value: &str| value.as_bytes().to_vec(); + let bucket = exports + .keyvalue_store() + .call_open(accessor, s("buckets")) + .await? + .map_err(|error| wasmtime::format_err!("{error:?}"))?; + let buckets = exports.keyvalue_types().bucket(); + + buckets + .call_set(accessor, bucket, s("a"), b("1"), None) + .await??; + assert_eq!( + buckets.call_get(accessor, bucket, s("a")).await??, + Some(b("1")) + ); + assert!(buckets.call_exists(accessor, bucket, s("a")).await??); + assert_eq!( + buckets.call_get(accessor, bucket, s("missing")).await??, + None + ); + + // if-not-exists only writes a new key + let if_not_exists = Some(SetOptions { + ttl_ms: None, + if_not_exists: true, + }); + assert_eq!( + buckets + .call_set(accessor, bucket, s("a"), b("2"), if_not_exists.clone()) + .await?, + Err(Error::PreconditionFailed) + ); + buckets + .call_set(accessor, bucket, s("b"), b("2"), if_not_exists) + .await??; + + // keys are listed by prefix, glob characters in it match literally + buckets + .call_set(accessor, bucket, s("a*"), b("3"), None) + .await??; + let KeyResponse { mut keys, cursor } = buckets + .call_list_keys(accessor, bucket, Some(s("a")), None) + .await??; + keys.sort(); + assert_eq!((keys, cursor), (vec![s("a"), s("a*")], None)); + let KeyResponse { keys, .. } = buckets + .call_list_keys(accessor, bucket, Some(s("a*")), None) + .await??; + assert_eq!(keys, vec![s("a*")]); + + assert_eq!( + exports + .keyvalue_atomics() + .call_increment(accessor, bucket, s("count"), 5) + .await??, + 5 + ); + + let batch = exports.keyvalue_batch(); + batch + .call_set_many(accessor, bucket, vec![(s("x"), b("1")), (s("y"), b("2"))]) + .await??; + assert_eq!( + batch + .call_get_many(accessor, bucket, vec![s("x"), s("z")]) + .await??, + vec![Some((s("x"), b("1"))), None] + ); + + // a ttl under a second is rounded up to one, the precision of HEXPIRE + buckets + .call_set( + accessor, + bucket, + s("expiring"), + b("1"), + Some(SetOptions { + ttl_ms: Some(200), + if_not_exists: false, + }), + ) + .await??; + assert!( + buckets + .call_exists(accessor, bucket, s("expiring")) + .await?? + ); + tokio::time::sleep(Duration::from_millis(1500)).await; + assert!( + !buckets + .call_exists(accessor, bucket, s("expiring")) + .await?? + ); + + let keys = vec![s("a"), s("b"), s("a*"), s("count"), s("x"), s("y")]; + batch.call_delete_many(accessor, bucket, keys).await??; + let KeyResponse { keys, .. } = buckets + .call_list_keys(accessor, bucket, None, None) + .await??; + assert!(keys.is_empty(), "left over {keys:?}"); + Ok(()) + }) + .await + } +} diff --git a/components/cli/cli.wac b/components/cli/cli.wac index c39d6a9..caf4d6d 100644 --- a/components/cli/cli.wac +++ b/components/cli/cli.wac @@ -1,9 +1,30 @@ package componentized:valkey; -let ops = new local:ops { ... }; +let resp = new local:resp { ... }; +let connection = new local:connection { + "componentized:valkey/resp@0.3.0-dev": resp.resp, + ... +}; +let tcp = new local:tcp { + "componentized:valkey/resp@0.3.0-dev": resp.resp, + "componentized:valkey/connection@0.3.0-dev": connection.connection, + ... +}; +let commands = new local:commands { + "componentized:valkey/resp@0.3.0-dev": resp.resp, + "componentized:valkey/connection@0.3.0-dev": connection.connection, + ... +}; let cli = new local:dep-cli { - "componentized:valkey/store@0.3.0-dev": ops.store, - "componentized:valkey/resp@0.3.0-dev": ops.resp, + "componentized:valkey/resp@0.3.0-dev": resp.resp, + "componentized:valkey/connection@0.3.0-dev": connection.connection, + "componentized:valkey/tcp@0.3.0-dev": tcp.tcp, + "componentized:valkey/admin-commands@0.3.0-dev": commands["componentized:valkey/admin-commands@0.3.0-dev"], + "componentized:valkey/connection-commands@0.3.0-dev": commands["componentized:valkey/connection-commands@0.3.0-dev"], + "componentized:valkey/hash-commands@0.3.0-dev": commands["componentized:valkey/hash-commands@0.3.0-dev"], + "componentized:valkey/keyspace-commands@0.3.0-dev": commands["componentized:valkey/keyspace-commands@0.3.0-dev"], + "componentized:valkey/pubsub-commands@0.3.0-dev": commands["componentized:valkey/pubsub-commands@0.3.0-dev"], + "componentized:valkey/string-commands@0.3.0-dev": commands["componentized:valkey/string-commands@0.3.0-dev"], ... }; diff --git a/components/client/README.md b/components/client/README.md index bf0b3f6..89cf87e 100644 --- a/components/client/README.md +++ b/components/client/README.md @@ -5,4 +5,4 @@ Client for interacting with a Valkey server, as componentized:valkey and wasmclo > [!NOTE] > `wasi:keyvalue` is currently unsupportable with an async based client. It will be restored when either it updates it's own API to be async, or the component model is fully able to make async calls from sync functions. -Composes [`ops`](../ops/) with [`as-keyvalue`](../as-keyvalue/), exporting both. +Composes [`resp`](../resp/), [`connection`](../connection/), [`tcp`](../tcp/), [`tcp-connector`](../tcp-connector/), [`commands`](../commands/) and [`as-keyvalue`](../as-keyvalue/), exporting each of their interfaces. diff --git a/components/client/client.wac b/components/client/client.wac index 0a2d838..c94b8c1 100644 --- a/components/client/client.wac +++ b/components/client/client.wac @@ -1,11 +1,37 @@ package componentized:valkey; -let ops = new local:ops { ... }; +let resp = new local:resp { ... }; +let connection = new local:connection { + "componentized:valkey/resp@0.3.0-dev": resp.resp, + ... +}; +let tcp = new local:tcp { + "componentized:valkey/resp@0.3.0-dev": resp.resp, + "componentized:valkey/connection@0.3.0-dev": connection.connection, + ... +}; +let commands = new local:commands { + "componentized:valkey/resp@0.3.0-dev": resp.resp, + "componentized:valkey/connection@0.3.0-dev": connection.connection, + ... +}; +let connector = new local:tcp-connector { + "componentized:valkey/resp@0.3.0-dev": resp.resp, + "componentized:valkey/connection@0.3.0-dev": connection.connection, + "componentized:valkey/tcp@0.3.0-dev": tcp.tcp, + ... +}; let keyvalue = new local:as-keyvalue { - "componentized:valkey/store@0.3.0-dev": ops.store, - "componentized:valkey/resp@0.3.0-dev": ops.resp, + "componentized:valkey/resp@0.3.0-dev": resp.resp, + "componentized:valkey/connection@0.3.0-dev": connection.connection, + "componentized:valkey/connector@0.3.0-dev": connector.connector, + "componentized:valkey/hash-commands@0.3.0-dev": commands["componentized:valkey/hash-commands@0.3.0-dev"], ... }; -export ops...; +export resp.resp; +export connection.connection; +export tcp.tcp; +export connector.connector; +export commands...; export keyvalue...; diff --git a/components/commands/Cargo.toml b/components/commands/Cargo.toml new file mode 100644 index 0000000..60cb9d7 --- /dev/null +++ b/components/commands/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "commands" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] +# shared with the other command components +path = "../../crates/commands/src/lib.rs" + +[features] +default = ["admin", "connection", "hash", "keyspace", "pubsub", "string"] +admin = [] +connection = [] +hash = [] +keyspace = [] +pubsub = [] +string = [] + +[dependencies] +resp-codec = { workspace = true } +wit-bindgen = { workspace = true } + +[dev-dependencies] +test-harness = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } diff --git a/components/commands/README.md b/components/commands/README.md new file mode 100644 index 0000000..696e491 --- /dev/null +++ b/components/commands/README.md @@ -0,0 +1,5 @@ +# commands + +Every implemented command interface, in one component. + +Exports the `-commands` interfaces of each of Valkey's ACL categories that has commands, over an imported [`connection`](../connection/). Each is also available on its own, e.g. [`hash-commands`](../hash-commands/). diff --git a/components/commands/tests/commands.rs b/components/commands/tests/commands.rs new file mode 100644 index 0000000..0d4f487 --- /dev/null +++ b/components/commands/tests/commands.rs @@ -0,0 +1,756 @@ +//! Tests for the commands, run against `test-commands`, `commands` composed with `connection`, +//! `resp` and `tcp`. The test plays the server over a stub transport, checking the command sent +//! and the conversion of its reply. + +use std::future::Future; + +use test_harness::{ + Harness, Transport, Value, bulk, command, connection_commands, establish, hash_commands, + keyspace_commands, map, +}; + +/// Answer the next command with `reply` while `call` waits for it, returning the call's result +/// and the command sent. +async fn round_trip( + transport: &mut Transport, + reply: Value, + call: impl Future>, +) -> (T, Vec) { + let serve = async { + let request = transport.request().await.expect("a command"); + transport.reply(reply); + request + }; + let (result, request) = tokio::join!(call, serve); + (result.expect("call"), request) +} + +fn ok() -> Value { + Value::String("OK".to_string()) +} + +fn array(items: Vec) -> Value { + Value::Array(items.into_iter().map(test_harness::encode).collect()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn string_commands() -> wasmtime::Result<()> { + let mut harness = Harness::new("test-commands").await?; + harness + .run(async |accessor, exports| { + let (mut transport, conn) = establish(accessor, exports).await?; + let strings = exports.string_commands(); + + let (result, sent) = round_trip( + &mut transport, + bulk("value"), + strings.call_get(accessor, conn, "key".to_string()), + ) + .await; + assert_eq!(sent, command(&["GET", "key"])); + assert_eq!(result, Ok(Some("value".to_string()))); + + let (result, _) = round_trip( + &mut transport, + Value::Null, + strings.call_get(accessor, conn, "missing".to_string()), + ) + .await; + assert_eq!(result, Ok(None)); + + let (result, sent) = round_trip( + &mut transport, + ok(), + strings.call_set(accessor, conn, "key".to_string(), "value".to_string()), + ) + .await; + assert_eq!(sent, command(&["SET", "key", "value"])); + assert_eq!(result, Ok(())); + + let (result, sent) = round_trip( + &mut transport, + Value::Integer(1), + strings.call_incr(accessor, conn, "count".to_string()), + ) + .await; + assert_eq!(sent, command(&["INCR", "count"])); + assert_eq!(result, Ok(1)); + + let (result, sent) = round_trip( + &mut transport, + Value::Integer(-4), + strings.call_incrby(accessor, conn, "count".to_string(), -5), + ) + .await; + assert_eq!(sent, command(&["INCRBY", "count", "-5"])); + assert_eq!(result, Ok(-4)); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn keyspace_commands() -> wasmtime::Result<()> { + use keyspace_commands::{ExpireMode, ExpireResult}; + let mut harness = Harness::new("test-commands").await?; + harness + .run(async |accessor, exports| { + let (mut transport, conn) = establish(accessor, exports).await?; + let keyspace = exports.keyspace_commands(); + + let (result, sent) = round_trip( + &mut transport, + Value::Integer(1), + keyspace.call_del(accessor, conn, "key".to_string()), + ) + .await; + assert_eq!(sent, command(&["DEL", "key"])); + assert_eq!(result, Ok(())); + + let (result, sent) = round_trip( + &mut transport, + Value::Integer(0), + keyspace.call_exists(accessor, conn, "key".to_string()), + ) + .await; + assert_eq!(sent, command(&["EXISTS", "key"])); + assert_eq!(result, Ok(false)); + + // durations are nanoseconds, EXPIRE takes whole seconds + let (result, sent) = round_trip( + &mut transport, + Value::Integer(1), + keyspace.call_expire( + accessor, + conn, + "key".to_string(), + 90_500_000_000, + Some(ExpireMode::Nx), + ), + ) + .await; + assert_eq!(sent, command(&["EXPIRE", "key", "90", "NX"])); + assert_eq!(result, Ok(ExpireResult::Applied)); + + let (result, sent) = round_trip( + &mut transport, + Value::Integer(0), + keyspace.call_expireat( + accessor, + conn, + "key".to_string(), + keyspace_commands::Instant { + seconds: 1_900_000_000, + nanoseconds: 0, + }, + None, + ), + ) + .await; + assert_eq!(sent, command(&["EXPIREAT", "key", "1900000000"])); + assert_eq!(result, Ok(ExpireResult::ConditionNotMet)); + + let (result, sent) = round_trip( + &mut transport, + array(vec![bulk("a"), bulk("b")]), + keyspace.call_keys(accessor, conn, "*".to_string()), + ) + .await; + assert_eq!(sent, command(&["KEYS", "*"])); + assert_eq!(result, Ok(vec!["a".to_string(), "b".to_string()])); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn hash_commands() -> wasmtime::Result<()> { + use hash_commands::{ExpireResult, HrandfieldOpts, HscanOpts}; + let mut harness = Harness::new("test-commands").await?; + harness + .run(async |accessor, exports| { + let (mut transport, conn) = establish(accessor, exports).await?; + let hashes = exports.hash_commands(); + let key = || "hash".to_string(); + + let (result, sent) = round_trip( + &mut transport, + Value::Integer(1), + hashes.call_hset(accessor, conn, key(), "f".to_string(), "v".to_string()), + ) + .await; + assert_eq!(sent, command(&["HSET", "hash", "f", "v"])); + assert_eq!(result, Ok(())); + + let (result, sent) = round_trip( + &mut transport, + bulk("v"), + hashes.call_hget(accessor, conn, key(), "f".to_string()), + ) + .await; + assert_eq!(sent, command(&["HGET", "hash", "f"])); + assert_eq!(result, Ok(Some("v".to_string()))); + + // RESP3 replies to HGETALL with a map + let (result, sent) = round_trip( + &mut transport, + map(vec![(bulk("f"), bulk("v")), (bulk("g"), bulk("w"))]), + hashes.call_hgetall(accessor, conn, key()), + ) + .await; + assert_eq!(sent, command(&["HGETALL", "hash"])); + assert_eq!( + result, + Ok(vec![ + ("f".to_string(), "v".to_string()), + ("g".to_string(), "w".to_string()) + ]) + ); + + let (result, _) = round_trip( + &mut transport, + Value::Integer(1), + hashes.call_hsetnx(accessor, conn, key(), "f".to_string(), "v".to_string()), + ) + .await; + assert_eq!(result, Ok(true)); + + let (result, _) = round_trip( + &mut transport, + Value::Integer(1), + hashes.call_hexists(accessor, conn, key(), "f".to_string()), + ) + .await; + assert_eq!(result, Ok(true)); + + let (result, sent) = round_trip( + &mut transport, + Value::Integer(2), + hashes.call_hlen(accessor, conn, key()), + ) + .await; + assert_eq!(sent, command(&["HLEN", "hash"])); + assert_eq!(result, Ok(2)); + + let (result, sent) = round_trip( + &mut transport, + array(vec![bulk("v"), Value::Null]), + hashes.call_hmget( + accessor, + conn, + key(), + vec!["f".to_string(), "x".to_string()], + ), + ) + .await; + assert_eq!(sent, command(&["HMGET", "hash", "f", "x"])); + assert_eq!(result, Ok(vec![Some("v".to_string()), None])); + + let (result, sent) = round_trip( + &mut transport, + ok(), + hashes.call_hmset( + accessor, + conn, + key(), + vec![ + ("f".to_string(), "v".to_string()), + ("g".to_string(), "w".to_string()), + ], + ), + ) + .await; + assert_eq!(sent, command(&["HMSET", "hash", "f", "v", "g", "w"])); + assert_eq!(result, Ok(())); + + let (result, sent) = round_trip( + &mut transport, + Value::Integer(6), + hashes.call_hincrby(accessor, conn, key(), "n".to_string(), 5), + ) + .await; + assert_eq!(sent, command(&["HINCRBY", "hash", "n", "5"])); + assert_eq!(result, Ok(6)); + + let (result, sent) = round_trip( + &mut transport, + bulk("2.5"), + hashes.call_hincrbyfloat(accessor, conn, key(), "n".to_string(), 1.5), + ) + .await; + assert_eq!(sent, command(&["HINCRBYFLOAT", "hash", "n", "1.5"])); + assert_eq!(result, Ok("2.5".to_string())); + + let (result, _) = round_trip( + &mut transport, + array(vec![bulk("f"), bulk("g")]), + hashes.call_hkeys(accessor, conn, key()), + ) + .await; + assert_eq!(result, Ok(vec!["f".to_string(), "g".to_string()])); + + let (result, _) = round_trip( + &mut transport, + array(vec![bulk("v"), bulk("w")]), + hashes.call_hvals(accessor, conn, key()), + ) + .await; + assert_eq!(result, Ok(vec!["v".to_string(), "w".to_string()])); + + let (result, sent) = round_trip( + &mut transport, + Value::Integer(1), + hashes.call_hstrlen(accessor, conn, key(), "f".to_string()), + ) + .await; + assert_eq!(sent, command(&["HSTRLEN", "hash", "f"])); + assert_eq!(result, Ok(1)); + + let (result, sent) = round_trip( + &mut transport, + array(vec![Value::Integer(1), Value::Integer(-2)]), + hashes.call_hexpire( + accessor, + conn, + key(), + 60_000_000_000, + None, + vec!["f".to_string(), "x".to_string()], + ), + ) + .await; + assert_eq!( + sent, + command(&["HEXPIRE", "hash", "60", "FIELDS", "2", "f", "x"]) + ); + assert_eq!( + result, + Ok(vec![ExpireResult::Applied, ExpireResult::DoesNotExist]) + ); + + // without a count, one field or null + let (result, sent) = round_trip( + &mut transport, + bulk("f"), + hashes.call_hrandfield(accessor, conn, key(), None), + ) + .await; + assert_eq!(sent, command(&["HRANDFIELD", "hash"])); + assert_eq!(result, Ok(Some(vec![("f".to_string(), None)]))); + + // RESP3 replies with values as [field, value] pairs + let (result, sent) = round_trip( + &mut transport, + array(vec![ + array(vec![bulk("f"), bulk("v")]), + array(vec![bulk("g"), bulk("w")]), + ]), + hashes.call_hrandfield( + accessor, + conn, + key(), + Some(HrandfieldOpts { + count: Some(2), + with_values: Some(true), + }), + ), + ) + .await; + assert_eq!(sent, command(&["HRANDFIELD", "hash", "2", "WITHVALUES"])); + assert_eq!( + result, + Ok(Some(vec![ + ("f".to_string(), Some("v".to_string())), + ("g".to_string(), Some("w".to_string())) + ])) + ); + + let (result, sent) = round_trip( + &mut transport, + array(vec![bulk("17"), array(vec![bulk("f"), bulk("v")])]), + hashes.call_hscan( + accessor, + conn, + key(), + None, + Some(HscanOpts { + match_: Some("f*".to_string()), + count: Some(10), + no_values: None, + }), + ), + ) + .await; + assert_eq!( + sent, + command(&["HSCAN", "hash", "0", "MATCH", "f*", "COUNT", "10"]) + ); + assert_eq!( + result, + Ok(( + Some("17".to_string()), + vec![("f".to_string(), Some("v".to_string()))] + )) + ); + + // the last page has a cursor of 0 + let (result, sent) = round_trip( + &mut transport, + array(vec![bulk("0"), array(vec![bulk("g")])]), + hashes.call_hscan( + accessor, + conn, + key(), + Some("17".to_string()), + Some(HscanOpts { + match_: None, + count: None, + no_values: Some(true), + }), + ), + ) + .await; + assert_eq!(sent, command(&["HSCAN", "hash", "17", "NOVALUES"])); + assert_eq!(result, Ok((None, vec![("g".to_string(), None)]))); + + let (result, _) = round_trip( + &mut transport, + Value::Integer(1), + hashes.call_hdel(accessor, conn, key(), "f".to_string()), + ) + .await; + assert_eq!(result, Ok(())); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn hrandfield_with_values_needs_a_count() -> wasmtime::Result<()> { + let mut harness = Harness::new("test-commands").await?; + harness + .run(async |accessor, exports| { + let (_transport, conn) = establish(accessor, exports).await?; + let result = exports + .hash_commands() + .call_hrandfield( + accessor, + conn, + "hash".to_string(), + Some(hash_commands::HrandfieldOpts { + count: None, + with_values: Some(true), + }), + ) + .await?; + assert!(matches!(result, Err(test_harness::Error::Client(_)))); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn connection_commands() -> wasmtime::Result<()> { + let mut harness = Harness::new("test-commands").await?; + harness + .run(async |accessor, exports| { + let (mut transport, conn) = establish(accessor, exports).await?; + let connections = exports.connection_commands(); + + let (result, sent) = round_trip( + &mut transport, + Value::String("PONG".to_string()), + connections.call_ping(accessor, conn), + ) + .await; + assert_eq!(sent, command(&["PING"])); + assert_eq!(result, Ok(())); + + let (result, sent) = round_trip( + &mut transport, + ok(), + connections.call_auth(accessor, conn, "user".to_string(), "secret".to_string()), + ) + .await; + assert_eq!(sent, command(&["AUTH", "user", "secret"])); + assert_eq!(result, Ok(())); + + let (result, sent) = round_trip( + &mut transport, + map(vec![(bulk("server"), bulk("valkey"))]), + connections.call_hello( + accessor, + conn, + Some(connection_commands::HelloOpts { + auth: None, + client_name: Some("tests".to_string()), + }), + ), + ) + .await; + assert_eq!(sent, command(&["HELLO", "3", "SETNAME", "tests"])); + assert_eq!(result, Ok(vec![("server".to_string(), bulk("valkey"))])); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn admin_and_pubsub_commands() -> wasmtime::Result<()> { + let mut harness = Harness::new("test-commands").await?; + harness + .run(async |accessor, exports| { + let (mut transport, conn) = establish(accessor, exports).await?; + let admin = exports.admin_commands(); + + let (result, sent) = round_trip( + &mut transport, + ok(), + admin.call_acl_setuser( + accessor, + conn, + "user".to_string(), + vec!["on".to_string(), ">secret".to_string()], + ), + ) + .await; + assert_eq!(sent, command(&["ACL", "SETUSER", "user", "on", ">secret"])); + assert_eq!(result, Ok(())); + + let (result, sent) = round_trip( + &mut transport, + bulk("dd721260bfe1b3d9601e7fbab36de6d04e2e67b0ef1c53de59d45950db0dd3cc"), + admin.call_acl_genpass(accessor, conn), + ) + .await; + assert_eq!(sent, command(&["ACL", "GENPASS"])); + assert_eq!(result.map(|pass| pass.len()), Ok(64)); + + let (result, sent) = round_trip( + &mut transport, + Value::Integer(1), + admin.call_acl_deluser(accessor, conn, "user".to_string()), + ) + .await; + assert_eq!(sent, command(&["ACL", "DELUSER", "user"])); + assert_eq!(result, Ok(())); + + let (result, sent) = round_trip( + &mut transport, + Value::Integer(2), + exports.pubsub_commands().call_publish( + accessor, + conn, + "news".to_string(), + "hello".to_string(), + ), + ) + .await; + assert_eq!(sent, command(&["PUBLISH", "news", "hello"])); + assert_eq!(result, Ok(2)); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn unexpected_replies_fail_the_command() -> wasmtime::Result<()> { + let mut harness = Harness::new("test-commands").await?; + harness + .run(async |accessor, exports| { + let (mut transport, conn) = establish(accessor, exports).await?; + let (result, _) = round_trip( + &mut transport, + Value::Integer(7), + exports + .string_commands() + .call_get(accessor, conn, "key".to_string()), + ) + .await; + assert!(matches!(result, Err(test_harness::Error::Client(_)))); + Ok(()) + }) + .await +} + +/// The tests against a Valkey server on localhost:6379, ignored unless run with +/// `make test-valkey`, which only runs the ignored tests in `with_valkey` modules. +mod with_valkey { + use super::*; + + /// Every command against a Valkey server on localhost:6379, run with + /// `cargo test -p commands -- --ignored with_valkey::`. + #[tokio::test(flavor = "multi_thread")] + #[ignore = "needs a Valkey server on localhost:6379"] + async fn against_valkey() -> wasmtime::Result<()> { + use hash_commands::{HrandfieldOpts, HscanOpts}; + let mut harness = Harness::new("test-commands").await?; + harness + .run(async |accessor, exports| { + let conn = exports + .tcp() + .call_connect(accessor, "localhost".to_string(), 6379, None) + .await? + .map_err(|error| wasmtime::format_err!("{error:?}"))?; + let strings = exports.string_commands(); + let keyspace = exports.keyspace_commands(); + let hashes = exports.hash_commands(); + let s = |value: &str| value.to_string(); + + exports + .connection_commands() + .call_ping(accessor, conn) + .await??; + let hello = exports + .connection_commands() + .call_hello(accessor, conn, None) + .await??; + assert!(hello.contains(&(s("proto"), Value::Integer(3)))); + + strings + .call_set(accessor, conn, s("commands-test:string"), s("1")) + .await??; + assert_eq!( + strings + .call_get(accessor, conn, s("commands-test:string")) + .await??, + Some(s("1")) + ); + assert_eq!( + strings + .call_incr(accessor, conn, s("commands-test:string")) + .await??, + 2 + ); + assert_eq!( + strings + .call_incrby(accessor, conn, s("commands-test:string"), 3) + .await??, + 5 + ); + assert!( + keyspace + .call_exists(accessor, conn, s("commands-test:string")) + .await?? + ); + assert_eq!( + keyspace + .call_expire( + accessor, + conn, + s("commands-test:string"), + 60_000_000_000, + None + ) + .await??, + keyspace_commands::ExpireResult::Applied + ); + assert_eq!( + keyspace + .call_keys(accessor, conn, s("commands-test:str*")) + .await??, + vec![s("commands-test:string")] + ); + keyspace + .call_del(accessor, conn, s("commands-test:string")) + .await??; + assert_eq!( + strings + .call_get(accessor, conn, s("commands-test:string")) + .await??, + None + ); + + let key = || s("commands-test:hash"); + hashes + .call_hmset( + accessor, + conn, + key(), + vec![(s("f"), s("v")), (s("g"), s("w"))], + ) + .await??; + let mut all = hashes.call_hgetall(accessor, conn, key()).await??; + all.sort(); + assert_eq!(all, vec![(s("f"), s("v")), (s("g"), s("w"))]); + assert_eq!(hashes.call_hlen(accessor, conn, key()).await??, 2); + assert_eq!( + hashes + .call_hmget(accessor, conn, key(), vec![s("f"), s("x")]) + .await??, + vec![Some(s("v")), None] + ); + let fields = hashes + .call_hrandfield( + accessor, + conn, + key(), + Some(HrandfieldOpts { + count: Some(2), + with_values: Some(true), + }), + ) + .await?? + .expect("fields"); + assert_eq!(fields.len(), 2); + assert!(fields.iter().all(|(_, value)| value.is_some())); + let (cursor, mut scanned) = hashes + .call_hscan( + accessor, + conn, + key(), + None, + Some(HscanOpts { + match_: None, + count: None, + no_values: None, + }), + ) + .await??; + scanned.sort(); + assert_eq!(cursor, None); + assert_eq!( + scanned, + vec![(s("f"), Some(s("v"))), (s("g"), Some(s("w")))] + ); + assert_eq!( + hashes + .call_hincrbyfloat(accessor, conn, key(), s("n"), 1.5) + .await??, + s("1.5") + ); + assert_eq!( + hashes + .call_hexpire( + accessor, + conn, + key(), + 60_000_000_000, + None, + vec![s("f"), s("x")] + ) + .await??, + vec![ + hash_commands::ExpireResult::Applied, + hash_commands::ExpireResult::DoesNotExist + ] + ); + keyspace.call_del(accessor, conn, key()).await??; + + assert_eq!( + exports + .pubsub_commands() + .call_publish(accessor, conn, s("commands-test"), s("hello")) + .await??, + 0 + ); + let pass = exports + .admin_commands() + .call_acl_genpass(accessor, conn) + .await??; + assert_eq!(pass.len(), 64); + Ok(()) + }) + .await + } +} diff --git a/components/connection-commands/Cargo.toml b/components/connection-commands/Cargo.toml new file mode 100644 index 0000000..a8543d9 --- /dev/null +++ b/components/connection-commands/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "connection-commands" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] +# shared with the other command components +path = "../../crates/commands/src/lib.rs" + +[features] +default = ["connection"] +admin = [] +connection = [] +hash = [] +keyspace = [] +pubsub = [] +string = [] + +[dependencies] +resp-codec = { workspace = true } +wit-bindgen = { workspace = true } diff --git a/components/connection-commands/README.md b/components/connection-commands/README.md new file mode 100644 index 0000000..4a8855f --- /dev/null +++ b/components/connection-commands/README.md @@ -0,0 +1,5 @@ +# connection-commands + +Commands about the connection, Valkey's @connection ACL category. + +Exports `componentized:valkey/connection-commands` over an imported [`connection`](../connection/). [`commands`](../commands/) exports every command interface in one component. diff --git a/components/connection/Cargo.toml b/components/connection/Cargo.toml new file mode 100644 index 0000000..7a8d537 --- /dev/null +++ b/components/connection/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "connection" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +futures-channel = { workspace = true } +futures-core = { workspace = true } +resp-codec = { workspace = true } +wit-bindgen = { workspace = true, features = ["async-spawn", "inter-task-wakeup"] } + +[dev-dependencies] +test-harness = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } diff --git a/components/connection/README.md b/components/connection/README.md new file mode 100644 index 0000000..3dd104d --- /dev/null +++ b/components/connection/README.md @@ -0,0 +1,5 @@ +# connection + +The mechanics of a connection to a Valkey server, independent of the transport carrying it. + +Pairs each command with its reply, keeps push messages apart for any number of readers, establishes the connection with `HELLO 3` and optionally keeps it alive with `PING`. A transport, e.g. [`tcp`](../tcp/), carries the RESP values to and from the server. diff --git a/components/connection/src/lib.rs b/components/connection/src/lib.rs new file mode 100644 index 0000000..6a95f30 --- /dev/null +++ b/components/connection/src/lib.rs @@ -0,0 +1,413 @@ +//! The mechanics of a connection to a Valkey server, independent of the transport carrying it. +//! +//! A connection runs background tasks for as long as it is open, in the task of the call to +//! `connection.new` that created it: +//! +//! - the writer writes queued commands to the requests stream, in order +//! - the reader reads the replies stream, handing each reply to the oldest command waiting for one, +//! and each push message to every reader of push messages +//! - the supervisor starts the work methods ask for that outlives them, e.g. keep-alive, since sync +//! methods have no task of their own to spawn it into +//! +//! Dropping the connection drops its queue of commands, the writer then closes the requests stream +//! and the transport closes, which ends the reader. + +// the test harness provides a main +#![cfg_attr(not(test), no_main)] + +use std::cell::RefCell; +use std::collections::VecDeque; +use std::future::poll_fn; +use std::pin::Pin; +use std::rc::{Rc, Weak}; + +use exports::componentized::valkey::connection::{ + ConnectOpts, Connection, Error, Guest, GuestConnection, +}; +use futures_channel::{mpsc, oneshot}; +use futures_core::Stream; +use resp_codec::{NestedValue, Value}; +use wasi::clocks::monotonic_clock::wait_for; +use wasi::clocks::types::Duration; +use wit_bindgen::{FutureReader, FutureWriter, StreamReader, StreamWriter, spawn_local}; + +/// How many push messages a reader may fall behind before its stream is closed. +const PUSH_BUFFER: usize = 256; + +struct Component; + +impl Guest for Component { + type Connection = ConnectionImpl; +} + +/// Where the reply to a command goes. +enum Reply { + /// To the caller waiting for it. + Call(oneshot::Sender>), + /// Nowhere, the command was sent without waiting for its reply. + Discard, +} + +/// A command for the writer, with a signal for once it is written. +struct Request { + command: Value, + written: Option>, +} + +/// Work for the supervisor to start. +enum Task { + /// Forward push messages from the channel to a reader's stream. + Pushes(mpsc::Receiver, StreamWriter), + /// Keep the connection alive, until the keep-alive is set again. + KeepAlive { generation: u64, interval: Duration }, +} + +/// State shared by a connection and its background tasks. +#[derive(Default)] +struct State { + /// Where each reply goes, in the order the commands were sent. + pending: VecDeque, + /// Why the connection closed, once it has. + closed: Option, + /// Counts the commands written and the values read, it stays the same while the connection is + /// idle. + activity: u64, + /// The readers of push messages. + pushes: Vec>, + /// The server's properties from the reply to HELLO. + server_properties: Vec<(String, Value)>, + /// Bumped each time the keep-alive is set, a keep-alive task stops once it is outdated. + keep_alive_generation: u64, +} + +/// A connection's queues to its background tasks. +/// +/// Only the connection holds it, the background tasks hold the state alone or a weak reference, +/// so dropping the connection drops the queues and ends the tasks. +struct Inner { + state: Rc>, + requests: mpsc::UnboundedSender, + tasks: mpsc::UnboundedSender, +} + +pub struct ConnectionImpl(Rc); + +impl GuestConnection for ConnectionImpl { + async fn new( + replies: StreamReader, + closed: FutureReader>, + opts: Option, + ) -> (StreamReader, FutureReader>) { + let (output, requests) = wit_stream::new(); + // the default is only written if the connection is dropped before it is established + let (connection_writer, connection) = wit_future::new(|| Err(Error::Closed(None))); + + let state = Rc::new(RefCell::new(State::default())); + let (requests_tx, requests_rx) = mpsc::unbounded(); + let (tasks_tx, tasks_rx) = mpsc::unbounded(); + let inner = Rc::new(Inner { + state: state.clone(), + requests: requests_tx, + tasks: tasks_tx, + }); + + spawn_local(write_requests(state.clone(), requests_rx, output)); + spawn_local(read_replies(state, replies, closed)); + spawn_local(supervise(tasks_rx, Rc::downgrade(&inner))); + spawn_local(establish(inner, opts, connection_writer)); + + (requests, connection) + } + + fn server_properties(&self) -> Vec<(String, Value)> { + self.0.state.borrow().server_properties.clone() + } + + async fn call(&self, command: Vec) -> Result { + call(&self.0, command).await + } + + async fn send(&self, command: Vec) -> Result<(), Error> { + let (written_tx, written_rx) = oneshot::channel(); + enqueue(&self.0, command, Reply::Discard, Some(written_tx))?; + // the signal is dropped unsent when the transport closed first + written_rx.await.map_err(|_| closed_error(&self.0.state)) + } + + fn pushes(&self) -> StreamReader { + let (writer, reader) = wit_stream::new(); + let (tx, rx) = mpsc::channel(PUSH_BUFFER); + let mut state = self.0.state.borrow_mut(); + // a closed connection drops the writer, closing the stream at once + if state.closed.is_none() && self.0.tasks.unbounded_send(Task::Pushes(rx, writer)).is_ok() + { + state.pushes.push(tx); + } + reader + } + + fn set_keep_alive(&self, interval: Option) { + let generation = { + let mut state = self.0.state.borrow_mut(); + state.keep_alive_generation += 1; + state.keep_alive_generation + }; + // a zero interval would ping whenever the connection is idle, treated as disabled + if let Some(interval) = interval.filter(|interval| *interval > 0) { + let _ = self + .0 + .tasks + .unbounded_send(Task::KeepAlive { generation, interval }); + } + } +} + +/// Queue a command, and where its reply goes, as one step so replies pair with commands in order. +fn enqueue( + inner: &Inner, + command: Vec, + reply: Reply, + written: Option>, +) -> Result<(), Error> { + let mut state = inner.state.borrow_mut(); + if let Some(error) = &state.closed { + return Err(error.clone()); + } + let command = Value::Array(command.into_iter().map(resp_codec::encode).collect()); + inner + .requests + .unbounded_send(Request { command, written }) + .map_err(|_| Error::Closed(None))?; + state.pending.push_back(reply); + Ok(()) +} + +async fn call(inner: &Inner, command: Vec) -> Result { + let (reply_tx, reply_rx) = oneshot::channel(); + enqueue(inner, command, Reply::Call(reply_tx), None)?; + // the reply is dropped unsent when the connection closes, the reader fails it first + reply_rx + .await + .unwrap_or_else(|_| Err(closed_error(&inner.state))) +} + +/// The error a command fails with once the connection closed. +fn closed_error(state: &RefCell) -> Error { + state.borrow().closed.clone().unwrap_or(Error::Closed(None)) +} + +/// Close the connection, failing every command waiting for a reply and closing the readers of push +/// messages. +fn close(state: &RefCell, error: Error) { + let mut state = state.borrow_mut(); + let error = state.closed.get_or_insert(error).clone(); + for reply in state.pending.drain(..) { + if let Reply::Call(reply) = reply { + let _ = reply.send(Err(error.clone())); + } + } + state.pushes.clear(); +} + +/// The next item of a channel. +async fn next(stream: &mut S) -> Option { + poll_fn(|cx| Pin::new(&mut *stream).poll_next(cx)).await +} + +fn bulk(value: &str) -> Value { + Value::BulkString(value.to_string()) +} + +/// Write each queued command to the requests stream, closing it once the connection is dropped. +async fn write_requests( + state: Rc>, + mut requests: mpsc::UnboundedReceiver, + mut output: StreamWriter, +) { + while let Some(Request { command, written }) = next(&mut requests).await { + state.borrow_mut().activity += 1; + if output.write_one(command).await.is_some() { + // the transport stopped reading, the reader closes the connection once it ends + break; + } + if let Some(written) = written { + let _ = written.send(()); + } + } +} + +/// Hand each reply to the oldest command waiting for one, and each push message to the readers of +/// push messages, until the transport closes. +async fn read_replies( + state: Rc>, + mut replies: StreamReader, + closed: FutureReader>, +) { + while let Some(value) = replies.next().await { + let mut state = state.borrow_mut(); + state.activity += 1; + match value { + Value::Push(_) => { + // a reader that is full or gone is dropped, rather than holding up replies + state + .pushes + .retain_mut(|pushes| pushes.try_send(value.clone()).is_ok()); + } + value => match state.pending.pop_front() { + Some(Reply::Call(reply)) => { + let _ = reply.send(reply_result(value)); + } + // a discarded reply, or one the server sent unprompted + Some(Reply::Discard) | None => {} + }, + } + } + let reason = match closed.await { + Ok(()) => None, + Err(error) => describe(error), + }; + close(&state, Error::Closed(reason)); +} + +/// Start the work methods ask for that outlives them. +async fn supervise(mut tasks: mpsc::UnboundedReceiver, inner: Weak) { + while let Some(task) = next(&mut tasks).await { + match task { + Task::Pushes(pushes, writer) => spawn_local(forward_pushes(pushes, writer)), + Task::KeepAlive { + generation, + interval, + } => spawn_local(keep_alive(inner.clone(), generation, interval)), + } + } +} + +/// Forward push messages to a reader's stream, until the reader or the connection is gone. +async fn forward_pushes(mut pushes: mpsc::Receiver, mut writer: StreamWriter) { + while let Some(value) = next(&mut pushes).await { + if writer.write_one(value).await.is_some() { + break; + } + } +} + +/// Send PING whenever the connection was idle for `interval`, closing it if PING fails. +async fn keep_alive(inner: Weak, generation: u64, interval: Duration) { + loop { + let Some(seen) = keep_alive_activity(&inner, generation) else { + return; + }; + wait_for(interval).await; + let Some(inner) = inner.upgrade() else { + return; + }; + let idle = { + let state = inner.state.borrow(); + if state.keep_alive_generation != generation || state.closed.is_some() { + return; + } + state.activity == seen && state.pending.is_empty() + }; + if idle { + if let Err(error) = call(&inner, vec![bulk("PING")]).await { + close(&inner.state, error); + inner.requests.close_channel(); + return; + } + } + } +} + +/// The connection's activity, `None` once the keep-alive should stop. +fn keep_alive_activity(inner: &Weak, generation: u64) -> Option { + let inner = inner.upgrade()?; + let state = inner.state.borrow(); + (state.keep_alive_generation == generation && state.closed.is_none()).then_some(state.activity) +} + +/// Send HELLO, then hand over the connection, or the error that stopped it. +async fn establish( + inner: Rc, + opts: Option, + connection: FutureWriter>, +) { + let (auth, client_name, keep_alive_interval) = match opts { + Some(opts) => (opts.auth, opts.client_name, opts.keep_alive_interval), + None => (None, None, None), + }; + + let mut hello = vec![bulk("HELLO"), bulk("3")]; + if let Some((username, password)) = auth { + hello.extend([bulk("AUTH"), bulk(&username), bulk(&password)]); + } + if let Some(client_name) = client_name { + hello.extend([bulk("SETNAME"), bulk(&client_name)]); + } + + let result = match call(&inner, hello).await.and_then(server_properties) { + Ok(properties) => { + inner.state.borrow_mut().server_properties = properties; + let established = ConnectionImpl(inner); + established.set_keep_alive(keep_alive_interval); + Ok(Connection::new(established)) + } + // dropping `inner` closes the connection + Err(error) => Err(error), + }; + // the connection is dropped, and closed, when nobody waits for it anymore + let _ = connection.write(result).await; +} + +/// The server's properties from the reply to HELLO, a map of property names to values. +fn server_properties(reply: Value) -> Result, Error> { + let Value::Map(entries) = reply else { + return Err(Error::Client(format!("unexpected reply to HELLO: {reply:?}"))); + }; + entries + .into_iter() + .map(|(name, value)| { + let name = match decode(name)? { + Value::String(name) | Value::BulkString(name) => name, + name => { + return Err(Error::Client(format!( + "unexpected property name in reply to HELLO: {name:?}" + ))); + } + }; + Ok((name, decode(value)?)) + }) + .collect() +} + +fn decode(value: NestedValue) -> Result { + resp_codec::decode(value).map_err(Error::Resp) +} + +/// An error value from the server fails the command. +fn reply_result(value: Value) -> Result { + match value { + Value::Error(message) | Value::BulkError(message) => Err(Error::Valkey(message)), + value => Ok(value), + } +} + +/// A transport's error as the reason a connection closed. +fn describe(error: Error) -> Option { + match error { + Error::Closed(reason) => reason, + Error::Client(message) | Error::Resp(message) | Error::Valkey(message) => Some(message), + } +} + +wit_bindgen::generate!({ + // `path` is relative to the manifest, `components/connection/` + path: "../wit", + world: "connection", + // the resp types shared by every component + with: { + "componentized:valkey/resp@0.3.0-dev": resp_codec::bindings::componentized::valkey::resp, + }, + generate_all, +}); + +export!(Component); diff --git a/components/connection/tests/connection.rs b/components/connection/tests/connection.rs new file mode 100644 index 0000000..b58e5e0 --- /dev/null +++ b/components/connection/tests/connection.rs @@ -0,0 +1,291 @@ +//! Tests for the `connection` component, over a stub transport playing the server. + +use std::time::Duration; + +use test_harness::{ + ConnectOpts, Error, Harness, Value, bulk, command, connect, establish, hello_reply, push, + receive, resolve, +}; + +/// Long enough for the connection to act on what it was given, short enough to keep tests quick. +const SETTLE: Duration = Duration::from_millis(100); + +#[tokio::test(flavor = "multi_thread")] +async fn establishes_with_hello() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (_transport, connection) = establish(accessor, exports).await?; + let properties = guest.call_server_properties(accessor, connection).await?; + assert_eq!( + properties, + vec![ + ("server".to_string(), bulk("valkey")), + ("version".to_string(), bulk("9.1.2")), + ("proto".to_string(), Value::Integer(3)), + ] + ); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn hello_carries_auth_and_client_name() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let opts = ConnectOpts { + auth: Some(("user".to_string(), "secret".to_string())), + client_name: Some("tests".to_string()), + keep_alive_interval: None, + }; + let (mut transport, connection) = connect(accessor, exports, Some(opts)).await?; + assert_eq!( + transport.request().await, + Some(command(&[ + "HELLO", "3", "AUTH", "user", "secret", "SETNAME", "tests" + ])) + ); + transport.reply(hello_reply()); + assert!(resolve(accessor, connection).await?.is_ok()); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn hello_error_fails_to_establish() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let (mut transport, connection) = connect(accessor, exports, None).await?; + transport.request().await; + transport.reply(Value::Error("WRONGPASS invalid password".to_string())); + let Err(error) = resolve(accessor, connection).await? else { + panic!("established despite the error reply"); + }; + assert_eq!( + error, + Error::Valkey("WRONGPASS invalid password".to_string()) + ); + // the connection is dropped, which closes the requests stream + assert_eq!(transport.request().await, None); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn closing_before_hello_fails_to_establish() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let (mut transport, connection) = connect(accessor, exports, None).await?; + transport.request().await; + transport.close(Err(Error::Closed(Some("connection refused".to_string())))); + assert_eq!( + resolve(accessor, connection).await?.err(), + Some(Error::Closed(Some("connection refused".to_string()))) + ); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn calls_are_paired_with_replies_in_order() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + let first = guest.call_call(accessor, connection, command(&["GET", "a"])); + let second = guest.call_call(accessor, connection, command(&["GET", "b"])); + let server = async { + // both are in flight before either is answered + assert_eq!(transport.request().await, Some(command(&["GET", "a"]))); + assert_eq!(transport.request().await, Some(command(&["GET", "b"]))); + transport.reply(bulk("first")); + transport.reply(bulk("second")); + }; + let (first, second, ()) = tokio::join!(first, second, server); + assert_eq!(first?, Ok(bulk("first"))); + assert_eq!(second?, Ok(bulk("second"))); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn error_replies_fail_the_call() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + let call = guest.call_call(accessor, connection, command(&["INCR", "text"])); + let server = async { + transport.request().await; + transport.reply(Value::Error("ERR value is not an integer".to_string())); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!( + result?, + Err(Error::Valkey("ERR value is not an integer".to_string())) + ); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn send_discards_its_reply() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + guest + .call_send(accessor, connection, command(&["SET", "a", "1"])) + .await??; + let call = guest.call_call(accessor, connection, command(&["GET", "a"])); + let server = async { + assert_eq!(transport.request().await, Some(command(&["SET", "a", "1"]))); + assert_eq!(transport.request().await, Some(command(&["GET", "a"]))); + // the reply to SET is discarded, even an error + transport.reply(Value::Error("ERR discarded".to_string())); + transport.reply(bulk("1")); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(bulk("1"))); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn push_messages_go_to_every_reader() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + let first = guest.call_pushes(accessor, connection).await?; + let second = guest.call_pushes(accessor, connection).await?; + let mut first = receive(accessor, first)?; + let mut second = receive(accessor, second)?; + + let message = push(vec![bulk("message"), bulk("news"), bulk("hello")]); + let call = guest.call_call(accessor, connection, command(&["PING"])); + let server = async { + transport.request().await; + // a push message arriving ahead of a reply is not mistaken for it + transport.reply(message.clone()); + transport.reply(Value::String("PONG".to_string())); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(Value::String("PONG".to_string()))); + assert_eq!(first.recv().await, Some(message.clone())); + assert_eq!(second.recv().await, Some(message)); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn slow_push_readers_are_closed() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + // a reader that never reads + let _slow = guest.call_pushes(accessor, connection).await?; + for n in 0..1000 { + transport.reply(push(vec![ + bulk("message"), + bulk("news"), + bulk(&n.to_string()), + ])); + } + // replies are not held up behind the reader + let call = guest.call_call(accessor, connection, command(&["PING"])); + let server = async { + transport.request().await; + transport.reply(Value::String("PONG".to_string())); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(Value::String("PONG".to_string()))); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn closing_fails_waiting_and_later_calls() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + let reset = Error::Closed(Some("connection reset".to_string())); + let call = guest.call_call(accessor, connection, command(&["GET", "a"])); + let server = async { + transport.request().await; + transport.close(Err(reset.clone())); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Err(reset.clone())); + let later = guest + .call_call(accessor, connection, command(&["GET", "a"])) + .await?; + assert_eq!(later, Err(reset)); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn keep_alive_pings_an_idle_connection() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + let interval = Duration::from_millis(20).as_nanos() as u64; + guest + .call_set_keep_alive(accessor, connection, Some(interval)) + .await?; + assert_eq!(transport.request().await, Some(command(&["PING"]))); + transport.reply(Value::String("PONG".to_string())); + assert_eq!(transport.request().await, Some(command(&["PING"]))); + transport.reply(Value::String("PONG".to_string())); + + // disabled again, no more pings + guest + .call_set_keep_alive(accessor, connection, None) + .await?; + let quiet = tokio::time::timeout(SETTLE, transport.request()).await; + assert!(quiet.is_err(), "pinged after keep-alive was disabled"); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn dropping_the_connection_closes_the_requests() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + let (mut transport, connection) = harness + .run(async |accessor, exports| establish(accessor, exports).await) + .await?; + harness.drop_resource(connection).await?; + // the guest closes the requests stream once its tasks run again + harness + .run(async |_accessor, _exports| { + assert_eq!(transport.request().await, None); + Ok(()) + }) + .await +} diff --git a/components/dep-cli/Cargo.toml b/components/dep-cli/Cargo.toml index c2b0dca..c8c1601 100644 --- a/components/dep-cli/Cargo.toml +++ b/components/dep-cli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dep-cli" version = "0.1.0" -edition = "2021" +edition = "2024" license = "Apache-2.0" [lib] diff --git a/components/dep-cli/README.md b/components/dep-cli/README.md index f31f01b..c4f8d8e 100644 --- a/components/dep-cli/README.md +++ b/components/dep-cli/README.md @@ -1,3 +1,3 @@ # dep-cli -The Valkey CLI before the ops client is composed in, see [`cli`](../cli/). +The Valkey CLI before the client components are composed in, see [`cli`](../cli/). diff --git a/components/dep-cli/src/lib.rs b/components/dep-cli/src/lib.rs index 84cd6b4..3f21154 100644 --- a/components/dep-cli/src/lib.rs +++ b/components/dep-cli/src/lib.rs @@ -1,7 +1,12 @@ use clap::{ArgAction, Args, Parser, Subcommand}; use componentized::valkey::{ + admin_commands, + connection::{ConnectOpts, Error}, + connection_commands, hash_commands, + hash_commands::{HrandfieldOpts, HscanOpts}, + keyspace_commands, pubsub_commands, resp::{self, Value}, - store::{connect, Error, HelloOpts, HrandfieldOpts, HscanOpts}, + string_commands, tcp, }; use exports::wasi::cli::run::Guest; use std::fmt; @@ -47,7 +52,26 @@ async fn write_to( impl Guest for Cli { async fn run() -> Result<(), ()> { - match exec().await { + // clap prints help and errors itself and exits the process, neither works on this target, + // they are printed here instead + let cli = match Cli::try_parse_from(get_arguments()) { + Ok(cli) => cli, + Err(error) => { + let message = error.render().to_string(); + if error.use_stderr() { + eprintln!("{}", message.trim_end()); + } else { + println!("{}", message.trim_end()); + } + // help and version succeed, invalid arguments fail + return if error.exit_code() == 0 { + Ok(()) + } else { + Err(()) + }; + } + }; + match exec(cli).await { Err(e) => { eprintln!("Error: {e}"); Err(()) @@ -68,15 +92,6 @@ struct Cli { #[arg(long, default_value = "6379")] port: u16, - /// RESP Protocol version - #[arg( - long, - // TODO change default to 3 - default_value = "2", - value_parser = clap::builder::PossibleValuesParser::new(["2", "3"]), - )] - proto_ver: String, - /// Authentication username #[arg(short, long, default_value = "default")] username: String, @@ -371,18 +386,13 @@ enum ACLCommands { }, } -async fn exec() -> Result<(), Error> { - let cli = Cli::parse_from(get_arguments().iter()); - - let opts = HelloOpts { - proto_ver: Some(cli.proto_ver.to_string()), - auth: match cli.password { - Some(password) => Some((cli.username, password)), - None => None, - }, +async fn exec(cli: Cli) -> Result<(), Error> { + let opts = ConnectOpts { + auth: cli.password.map(|password| (cli.username, password)), client_name: cli.client_name, + keep_alive_interval: None, }; - let connection = connect(cli.host, cli.port, Some(opts)).await?; + let connection = tcp::connect(cli.host, cli.port, Some(opts)).await?; match &cli.command { Commands::SEND { cmd } => { @@ -390,43 +400,44 @@ async fn exec() -> Result<(), Error> { .iter() .map(|c| Value::BulkString(c.to_string())) .collect(); - let response = connection.send(cmd).await?; + let response = connection.call(cmd).await?; println!("{response}"); } Commands::ACL(aclargs) => match &aclargs.command { ACLCommands::DELUSER { username } => { - connection.acl_deluser(username.to_string()).await?; + admin_commands::acl_deluser(&connection, username.to_string()).await?; println!("Deleted user {username}"); } ACLCommands::GENPASS => { - let pass = connection.acl_genpass().await?; + let pass = admin_commands::acl_genpass(&connection).await?; println!("{pass}"); } ACLCommands::SETUSER { username, rules } => { - connection - .acl_setuser(username.to_string(), rules.to_vec()) + admin_commands::acl_setuser(&connection, username.to_string(), rules.to_vec()) .await?; println!("Set user {username}"); } }, Commands::DEL { key } => { - connection.del(key.to_string()).await?; + keyspace_commands::del(&connection, key.to_string()).await?; println!("Deleted {key}"); } - Commands::EXISTS { key } => match connection.exists(key.to_string()).await? { - true => println!("true"), - false => println!("false"), - }, - Commands::GET { key } => match connection.get(key.to_string()).await? { + Commands::EXISTS { key } => { + match keyspace_commands::exists(&connection, key.to_string()).await? { + true => println!("true"), + false => println!("false"), + } + } + Commands::GET { key } => match string_commands::get(&connection, key.to_string()).await? { Some(value) => println!("{}", value), None => println!(""), }, Commands::HDEL { key, field } => { - connection.hdel(key.to_string(), field.to_string()).await?; + hash_commands::hdel(&connection, key.to_string(), field.to_string()).await?; println!("Deleted {field}"); } Commands::HELLO => { - for (key, value) in connection.hello(None).await? { + for (key, value) in connection_commands::hello(&connection, None).await? { match value { Value::Null => println!("{key}: "), Value::String(value) => println!("{key}: {value}"), @@ -454,21 +465,20 @@ async fn exec() -> Result<(), Error> { } } } - Commands::HEXISTS { key, field } => match connection - .hexists(key.to_string(), field.to_string()) - .await? - { - true => println!("true"), - false => println!("false"), - }, + Commands::HEXISTS { key, field } => { + match hash_commands::hexists(&connection, key.to_string(), field.to_string()).await? { + true => println!("true"), + false => println!("false"), + } + } Commands::HGET { key, field } => { - match connection.hget(key.to_string(), field.to_string()).await? { + match hash_commands::hget(&connection, key.to_string(), field.to_string()).await? { Some(value) => println!("{}", value), None => println!(""), } } Commands::HGETALL { key } => { - let fields = connection.hgetall(key.to_string()).await?; + let fields = hash_commands::hgetall(&connection, key.to_string()).await?; if fields.len() == 0 { println!(""); } @@ -481,9 +491,9 @@ async fn exec() -> Result<(), Error> { field, increment, } => { - let value = connection - .hincrby(key.to_string(), field.to_string(), *increment) - .await?; + let value = + hash_commands::hincrby(&connection, key.to_string(), field.to_string(), *increment) + .await?; println!("{}", value); } Commands::HINCRBYFLOAT { @@ -491,24 +501,28 @@ async fn exec() -> Result<(), Error> { field, increment, } => { - let value = connection - .hincrbyfloat(key.to_string(), field.to_string(), *increment) - .await?; + let value = hash_commands::hincrbyfloat( + &connection, + key.to_string(), + field.to_string(), + *increment, + ) + .await?; println!("{}", value); } Commands::HKEYS { key } => { - for key in connection.hkeys(key.to_string()).await? { + for key in hash_commands::hkeys(&connection, key.to_string()).await? { println!("- {key}"); } } Commands::HLEN { key } => { - let len = connection.hlen(key.to_string()).await?; + let len = hash_commands::hlen(&connection, key.to_string()).await?; println!("{len}"); } Commands::HMGET { key, fields } => { - let fields = connection - .hmget(key.to_string().to_string(), fields.to_vec()) - .await?; + let fields = + hash_commands::hmget(&connection, key.to_string().to_string(), fields.to_vec()) + .await?; for field in fields { match field { None => println!("- "), @@ -526,7 +540,7 @@ async fn exec() -> Result<(), Error> { .chunks(2) .map(|c| (c[0].clone(), c[1].clone())) .collect(); - connection.hmset(key.to_string(), fields.clone()).await?; + hash_commands::hmset(&connection, key.to_string(), fields.clone()).await?; println!("Set {} field(s)", fields.len()); } Commands::HRANDFIELD { @@ -538,7 +552,8 @@ async fn exec() -> Result<(), Error> { count: *count, with_values: *with_values, }; - let fields = connection.hrandfield(key.to_string(), Some(opts)).await?; + let fields = + hash_commands::hrandfield(&connection, key.to_string(), Some(opts)).await?; match fields { None => println!(""), Some(fields) => { @@ -563,9 +578,13 @@ async fn exec() -> Result<(), Error> { count: *count, no_values: *no_values, }; - let (cursor, fields) = connection - .hscan(key.to_string(), cursor.clone(), Some(opts.clone())) - .await?; + let (cursor, fields) = hash_commands::hscan( + &connection, + key.to_string(), + cursor.clone(), + Some(opts.clone()), + ) + .await?; if let Some(cursor) = cursor { println!("(cursor) {cursor}"); } @@ -580,28 +599,35 @@ async fn exec() -> Result<(), Error> { } } Commands::HSET { key, field, value } => { - connection - .hset(key.to_string(), field.to_string(), value.to_string()) - .await?; + hash_commands::hset( + &connection, + key.to_string(), + field.to_string(), + value.to_string(), + ) + .await?; println!("Set {field}"); } Commands::HSETNX { key, field, value } => { - match connection - .hsetnx(key.to_string(), field.to_string(), value.to_string()) - .await? + match hash_commands::hsetnx( + &connection, + key.to_string(), + field.to_string(), + value.to_string(), + ) + .await? { false => println!("Field already set"), true => println!("Set {field}"), }; } Commands::HSTRLEN { key, field } => { - let len = connection - .hstrlen(key.to_string(), field.to_string()) - .await?; + let len = + hash_commands::hstrlen(&connection, key.to_string(), field.to_string()).await?; println!("{len}"); } Commands::HVALS { key } => { - let values = connection.hvals(key.to_string()).await?; + let values = hash_commands::hvals(&connection, key.to_string()).await?; if values.len() == 0 { println!(""); } @@ -610,28 +636,27 @@ async fn exec() -> Result<(), Error> { } } Commands::INCR { key } => { - let value = connection.incr(key.to_string()).await?; + let value = string_commands::incr(&connection, key.to_string()).await?; println!("{}", value); } Commands::INCRBY { key, increment } => { - let value = connection - .incrby(key.to_string(), increment.clone()) - .await?; + let value = + string_commands::incrby(&connection, key.to_string(), increment.clone()).await?; println!("{}", value); } Commands::KEYS { pattern } => { - for key in connection.keys(pattern.to_string()).await? { + for key in keyspace_commands::keys(&connection, pattern.to_string()).await? { println!("{key}"); } } Commands::PUBLISH { channel, message } => { - let value = connection - .publish(channel.to_string(), message.to_string()) - .await?; + let value = + pubsub_commands::publish(&connection, channel.to_string(), message.to_string()) + .await?; println!("{}", value); } Commands::SET { key, value } => { - connection.set(key.to_string(), value.to_string()).await?; + string_commands::set(&connection, key.to_string(), value.to_string()).await?; println!("Set {key}"); } } @@ -683,10 +708,8 @@ impl fmt::Display for Value { Value::Double(val) => write!(f, "(double) {val}"), Value::BigNumber(val) => write!(f, "(number) {val}"), Value::BulkError(val) => write!(f, "(error) {val}"), - Value::VerbatimString((_encoding, _val)) => { - // TODO - write!(f, "{self:?}") - } + // text to show as is, e.g. from INFO, the encoding is `txt` or `mkd` + Value::VerbatimString((_encoding, val)) => write!(f, "{val}"), Value::Map(val) => { if val.len() == 0 { write!(f, "(empty map)")?; @@ -695,16 +718,30 @@ impl fmt::Display for Value { for (i, (key, val)) in val.iter().enumerate() { let key = resp::decode(key).map_err(|_| fmt::Error)?.to_string(); let val = resp::decode(val).map_err(|_| fmt::Error)?.to_string(); - // TODO handle multi-line keys - let prefix = format!("{:i_max_width$}# {key} => ", i + 1); - let mut padding = String::new(); - for _ in 0..prefix.len() { - padding.push(' ') - } if i != 0 { write!(f, "\n")?; } - for (j, line) in val.lines().enumerate() { + // a key over several lines is indented under its number, the value follows its + // last line + let number = format!("{:i_max_width$}# ", i + 1); + let key_lines = lines(&key); + let (last, first) = key_lines.split_last().expect("at least one line"); + for (j, line) in first.iter().enumerate() { + let indent = if j == 0 { + number.clone() + } else { + " ".repeat(number.len()) + }; + write!(f, "{indent}{line}\n")?; + } + let indent = if first.is_empty() { + number.clone() + } else { + " ".repeat(number.len()) + }; + let prefix = format!("{indent}{last} => "); + let padding = " ".repeat(prefix.len()); + for (j, line) in lines(&val).into_iter().enumerate() { match j { 0 => write!(f, "{prefix}{line}")?, _ => write!(f, "\n{padding}{line}")?, @@ -765,6 +802,12 @@ impl fmt::Display for Value { } } +/// The lines of a value, an empty value is one empty line. +fn lines(value: &str) -> Vec<&str> { + let lines: Vec<&str> = value.lines().collect(); + if lines.is_empty() { vec![""] } else { lines } +} + wit_bindgen::generate!({ world: "cli", path: "../wit", diff --git a/components/hash-commands/Cargo.toml b/components/hash-commands/Cargo.toml new file mode 100644 index 0000000..d13c141 --- /dev/null +++ b/components/hash-commands/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "hash-commands" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] +# shared with the other command components +path = "../../crates/commands/src/lib.rs" + +[features] +default = ["hash"] +admin = [] +connection = [] +hash = [] +keyspace = [] +pubsub = [] +string = [] + +[dependencies] +resp-codec = { workspace = true } +wit-bindgen = { workspace = true } diff --git a/components/hash-commands/README.md b/components/hash-commands/README.md new file mode 100644 index 0000000..5ef15cc --- /dev/null +++ b/components/hash-commands/README.md @@ -0,0 +1,5 @@ +# hash-commands + +Commands for hash values, Valkey's @hash ACL category. + +Exports `componentized:valkey/hash-commands` over an imported [`connection`](../connection/). [`commands`](../commands/) exports every command interface in one component. diff --git a/components/keyspace-commands/Cargo.toml b/components/keyspace-commands/Cargo.toml new file mode 100644 index 0000000..2906ff7 --- /dev/null +++ b/components/keyspace-commands/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "keyspace-commands" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] +# shared with the other command components +path = "../../crates/commands/src/lib.rs" + +[features] +default = ["keyspace"] +admin = [] +connection = [] +hash = [] +keyspace = [] +pubsub = [] +string = [] + +[dependencies] +resp-codec = { workspace = true } +wit-bindgen = { workspace = true } diff --git a/components/keyspace-commands/README.md b/components/keyspace-commands/README.md new file mode 100644 index 0000000..73cf5a0 --- /dev/null +++ b/components/keyspace-commands/README.md @@ -0,0 +1,5 @@ +# keyspace-commands + +Commands for keys of any type, Valkey's @keyspace ACL category. + +Exports `componentized:valkey/keyspace-commands` over an imported [`connection`](../connection/). [`commands`](../commands/) exports every command interface in one component. diff --git a/components/ops/README.md b/components/ops/README.md deleted file mode 100644 index 5277783..0000000 --- a/components/ops/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# ops - -Client library for interacting with a Valkey server, and RESP messages. diff --git a/components/ops/src/lib.rs b/components/ops/src/lib.rs deleted file mode 100644 index 43612f0..0000000 --- a/components/ops/src/lib.rs +++ /dev/null @@ -1,1380 +0,0 @@ -#![no_main] - -use exports::componentized::valkey::resp::{ - Error as RespError, Guest as RespGuest, NestedValue, Value, -}; -use exports::componentized::valkey::store::{ - Connection, Duration, Error, ExpireMode, ExpireResult, Guest as StoreGuest, GuestConnection, - HelloOpts, HrandfieldOpts, HscanOpts, Instant, -}; -use resp::{decode, encode}; -use std::cell::RefCell; -use std::net::IpAddr; -use std::vec; -use wasi::sockets::ip_name_lookup::{resolve_addresses, ErrorCode as LookupErrorCode}; -use wasi::sockets::types::{ - ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, - TcpSocket, -}; -use wit_bindgen::{FutureReader, StreamReader, StreamResult, StreamWriter}; - -pub mod resp; - -#[derive(Debug, Clone)] -struct ValkeyOps; - -impl ValkeyOps { - async fn open(address: IpSocketAddress) -> Result { - let socket = match address { - IpSocketAddress::Ipv4(_) => TcpSocket::create(IpAddressFamily::Ipv4)?, - IpSocketAddress::Ipv6(_) => TcpSocket::create(IpAddressFamily::Ipv6)?, - }; - socket.connect(address).await?; - - // send and receive may each be called once per socket, the streams are kept for the life - // of the connection - let (output, send_stream) = wit_stream::new(); - let sent = socket.send(send_stream); - let (input, received) = socket.receive(); - - Ok(ValkeyConnection { - streams: RefCell::new(Some((output, input))), - _sent: sent, - _received: received, - _socket: socket, - }) - } - - async fn resolve_ip_socket_addresses( - host: &str, - port: u16, - ) -> Result, Error> { - let ip_addresses = match host.parse() { - Ok(IpAddr::V4(addr)) => { - // host is an ipv4 address - let octets = addr.octets(); - vec![IpAddress::Ipv4(( - octets[0], octets[1], octets[2], octets[3], - ))] - } - Ok(IpAddr::V6(addr)) => { - // host is an ipv6 address - let segments = addr.segments(); - vec![IpAddress::Ipv6(( - segments[0], - segments[1], - segments[2], - segments[3], - segments[4], - segments[5], - segments[6], - segments[7], - ))] - } - Err(_) => { - // resolve as a hostname - resolve_addresses(host.to_string()).await? - } - }; - - // convert resolved ip address into socket address - let socket_addresses = ip_addresses - .into_iter() - .map(|ip_address| match ip_address { - IpAddress::Ipv4(address) => { - IpSocketAddress::Ipv4(Ipv4SocketAddress { address, port }) - } - IpAddress::Ipv6(address) => { - let flow_info = 0; - let scope_id = 0; - IpSocketAddress::Ipv6(Ipv6SocketAddress { - port, - flow_info, - address, - scope_id, - }) - } - }) - .collect(); - Ok(socket_addresses) - } -} - -impl StoreGuest for ValkeyOps { - type Connection = ValkeyConnection; - - async fn connect( - host: String, - port: u16, - opts: Option, - ) -> Result { - for addr in Self::resolve_ip_socket_addresses(&host, port).await? { - if let Ok(conn) = Self::open(addr).await { - // check the connection is alive - if conn.hello(opts.clone()).await.is_ok() { - // it's alive - return Ok(Connection::new(conn)); - } - } - } - Err(Error::Client(format!("unable to connect to {host}:{port}"))) - } -} - -struct ValkeyConnection { - /// The streams writing to and reading from the socket, taken while a command is in flight, - /// a connection handles one command at a time. - streams: RefCell, StreamReader)>>, - _sent: FutureReader>, - _received: FutureReader>, - _socket: TcpSocket, -} - -impl ValkeyConnection { - async fn exchange( - output: &mut StreamWriter, - input: &mut StreamReader, - request: Vec, - ) -> Result, Error> { - if !output.write_all(request).await.is_empty() { - Err(Error::Client("Stream Closed".to_string()))? - } - - // TODO handle responses spanning multiple windows - match input.read(Vec::with_capacity(1024)).await { - (StreamResult::Complete(_), response) => Ok(response), - (StreamResult::Dropped, _) => Err(Error::Client("Stream Closed".to_string())), - (StreamResult::Cancelled, _) => Err(Error::Client("Stream Cancelled".to_string())), - } - } -} - -impl GuestConnection for ValkeyConnection { - async fn send(&self, command: Vec) -> Result { - let request = encode(Value::Array( - command.into_iter().map(|c| c.into()).collect(), - )); - - let (mut output, mut input) = self - .streams - .borrow_mut() - .take() - .ok_or_else(|| Error::Client("Connection busy with another command".to_string()))?; - let response = Self::exchange(&mut output, &mut input, request).await; - *self.streams.borrow_mut() = Some((output, input)); - - decode(response?).map(|r| r.into()) - } - - async fn acl_deluser(&self, username: String) -> Result<(), Error> { - // https://valkey.io/commands/acl-deluser/ - // ACL DELUSER username [ username ... ] - - let response = self - .send(vec![ - Value::BulkString("ACL".to_string()), - Value::BulkString("DELUSER".to_string()), - Value::BulkString(username), - ]) - .await?; - match response { - Value::Integer(_) => Ok(()), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn acl_genpass(&self) -> Result { - // https://valkey.io/commands/acl-genpass/ - // ACL GENPASS [ bits ] - - let response = self - .send(vec![ - Value::BulkString("ACL".to_string()), - Value::BulkString("GENPASS".to_string()), - ]) - .await?; - match response { - Value::BulkString(pass) => Ok(pass), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn acl_setuser(&self, username: String, rules: Vec) -> Result<(), Error> { - // https://valkey.io/commands/acl-setuser/ - // ACL SETUSER username [ rule ] [ [ rule ] ... ] - - let mut command = vec![ - Value::BulkString("ACL".to_string()), - Value::BulkString("SETUSER".to_string()), - Value::BulkString(username), - ]; - for rule in rules { - command.push(Value::BulkString(rule)); - } - let response = self.send(command).await?; - match response { - Value::String(msg) => match msg.as_str() { - "OK" => Ok(()), - msg => Err(Error::Client(format!("Not OK: {msg}")))?, - }, - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn auth(&self, username: String, password: String) -> Result<(), Error> { - // https://valkey.io/commands/auth/ - // AUTH [ username ] password - - let response = self - .send(vec![ - Value::BulkString("AUTH".to_string()), - Value::BulkString(username), - Value::BulkString(password), - ]) - .await?; - match response { - Value::String(msg) => match msg.as_str() { - "OK" => Ok(()), - msg => Err(Error::Client(format!("Not OK: {msg}")))?, - }, - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn del(&self, key: String) -> Result<(), Error> { - // https://valkey.io/commands/del/ - // DEL key [ key ... ] - - // TODO handle multiple keys - let response = self - .send(vec![ - Value::BulkString("DEL".to_string()), - Value::BulkString(key), - ]) - .await?; - match response { - Value::Integer(_) => Ok(()), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn exists(&self, key: String) -> Result { - // https://valkey.io/commands/exists/ - // EXISTS key [ key ... ] - - // TODO handle multiple keys - let response = self - .send(vec![ - Value::BulkString("EXISTS".to_string()), - Value::BulkString(key), - ]) - .await?; - match response { - Value::Integer(0) => Ok(false), - Value::Integer(1) => Ok(true), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - #[allow(async_fn_in_trait)] - async fn expire( - &self, - key: String, - after: Duration, - mode: Option, - ) -> Result { - // https://valkey.io/commands/exire/ - // EXPIRE key seconds [ NX | XX | GT | LT ] - - let mut cmd = vec![ - Value::BulkString("EXPIRE".to_string()), - Value::BulkString(key), - Value::Integer((after / 1000) as i64), - ]; - if let Some(mode) = mode { - cmd.push(mode.into()); - } - - let response = self.send(cmd).await?; - match response { - Value::Integer(code) => match code { - 0 => Ok(ExpireResult::ConditionNotMet), - 1 => Ok(ExpireResult::Applied), - _ => Err(Error::Client(format!("Unknown reply: {code}")))?, - }, - Value::Null => Err(Error::Client("Operation aborted".to_string()))?, - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - #[allow(async_fn_in_trait)] - async fn expireat( - &self, - key: String, - after: Instant, - mode: Option, - ) -> Result { - // https://valkey.io/commands/exireat/ - // EXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] - - let mut cmd = vec![ - Value::BulkString("EXPIRE".to_string()), - Value::BulkString(key), - Value::Integer(after.seconds), - ]; - if let Some(mode) = mode { - cmd.push(mode.into()); - } - - let response = self.send(cmd).await?; - match response { - Value::Integer(code) => match code { - 0 => Ok(ExpireResult::ConditionNotMet), - 1 => Ok(ExpireResult::Applied), - _ => Err(Error::Client(format!("Unknown reply: {code}")))?, - }, - Value::Null => Err(Error::Client("Operation aborted".to_string()))?, - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn get(&self, key: String) -> Result, Error> { - // https://valkey.io/commands/get/ - // GET key - - let response = self - .send(vec![ - Value::BulkString("GET".to_string()), - Value::BulkString(key), - ]) - .await?; - match response { - Value::BulkString(value) => Ok(Some(value)), - Value::Null => Ok(None), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hdel(&self, key: String, field: String) -> Result<(), Error> { - // https://valkey.io/commands/hdel/ - // HDEL key field [ field ... ] - - let response = self - .send(vec![ - Value::BulkString("HDEL".to_string()), - Value::BulkString(key), - Value::BulkString(field), - ]) - .await?; - match response { - Value::Integer(_) => Ok(()), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hello(&self, opts: Option) -> Result, Error> { - // https://valkey.io/commands/hello/ - // HELLO [ protover [ AUTH username password ] [ SETNAME clientname ] ] - - let mut cmd = vec![Value::BulkString("HELLO".to_string())]; - if let Some(opts) = opts { - let has_proto = opts.proto_ver.is_none(); - if let Some(proto_ver) = opts.proto_ver { - cmd.push(Value::BulkString(proto_ver)); - } - if let Some((username, password)) = opts.auth { - if has_proto { - Err(Error::Client( - "proto-ver must be specified to use auth".to_string(), - ))? - } - cmd.push(Value::BulkString("AUTH".to_string())); - cmd.push(Value::BulkString(username)); - cmd.push(Value::BulkString(password)); - } - if let Some(client_name) = opts.client_name { - if has_proto { - Err(Error::Client( - "proto-ver must be specified to use client-name".to_string(), - ))? - } - cmd.push(Value::BulkString("SETNAME".to_string())); - cmd.push(Value::BulkString(client_name)); - } - } - let response = self.send(cmd).await?; - match response { - // convert RESP2 array - Value::Array(items) => { - let mut hello: Vec<(String, Value)> = vec![]; - for item in items.chunks(2) { - let key = item[0].clone().into(); - let value = item[1].clone().into(); - let key = match key { - Value::BulkString(key) => key, - key => Err(Error::Client(format!("Unexpected key type: {:?}", key)))?, - }; - hello.push((key, value)) - } - Ok(hello) - } - // RESP3 - Value::Map(items) => { - let mut hello: Vec<(String, Value)> = vec![]; - for (key, value) in items { - let key = match key.into() { - Value::BulkString(key) => key, - key => Err(Error::Client(format!("Unexpected key type: {:?}", key)))?, - }; - hello.push((key, value.into())) - } - Ok(hello) - } - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hexists(&self, key: String, field: String) -> Result { - // https://valkey.io/commands/hexists/ - // HEXISTS key field - - let response = self - .send(vec![ - Value::BulkString("HEXISTS".to_string()), - Value::BulkString(key), - Value::BulkString(field), - ]) - .await?; - match response { - Value::Integer(0) => Ok(false), - Value::Integer(1) => Ok(true), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - #[allow(async_fn_in_trait)] - async fn hexpire( - &self, - key: String, - after: Duration, - mode: Option, - fields: Vec, - ) -> Result, Error> { - // https://valkey.io/commands/hexire/ - // HEXPIRE key seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] - - let mut cmd = vec![ - Value::BulkString("HEXPIRE".to_string()), - Value::BulkString(key), - Value::Integer((after / 1000) as i64), - ]; - if let Some(mode) = mode { - cmd.push(mode.into()); - } - cmd.push(Value::BulkString("FIELDS".to_string())); - cmd.push(Value::Integer(fields.len() as i64)); - for field in fields { - cmd.push(Value::BulkString(field)); - } - - let response = self.send(cmd).await?; - match response { - Value::Array(codes) => { - let mut replies = vec![]; - for code in codes { - let code = code.into(); - match code { - Value::Integer(code) => { - replies.push(match code { - -2 => ExpireResult::DoesNotExist, - 0 => ExpireResult::ConditionNotMet, - 1 => ExpireResult::Applied, - 2 => ExpireResult::ZeroSeconds, - _ => Err(Error::Client(format!("Unknown reply: {code}")))?, - }); - } - value => Err(Error::Client(format!( - "Unexpected array item type: {:?}", - value - )))?, - } - } - Ok(replies) - } - Value::Null => Err(Error::Client("Operation aborted".to_string()))?, - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - #[allow(async_fn_in_trait)] - async fn hexpireat( - &self, - key: String, - after: Instant, - mode: Option, - fields: Vec, - ) -> Result, Error> { - // https://valkey.io/commands/hexireat/ - // HEXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] - - let mut cmd = vec![ - Value::BulkString("HEXPIREAT".to_string()), - Value::BulkString(key), - Value::Integer(after.seconds), - ]; - if let Some(mode) = mode { - cmd.push(mode.into()); - } - cmd.push(Value::BulkString("FIELDS".to_string())); - cmd.push(Value::Integer(fields.len() as i64)); - for field in fields { - cmd.push(Value::BulkString(field)); - } - - let response = self.send(cmd).await?; - match response { - Value::Array(codes) => { - let mut replies = vec![]; - for code in codes { - let code = code.into(); - match code { - Value::Integer(code) => { - replies.push(match code { - -2 => ExpireResult::DoesNotExist, - 0 => ExpireResult::ConditionNotMet, - 1 => ExpireResult::Applied, - 2 => ExpireResult::ZeroSeconds, - _ => Err(Error::Client(format!("Unknown reply: {code}")))?, - }); - } - value => Err(Error::Client(format!( - "Unexpected array item type: {:?}", - value - )))?, - } - } - Ok(replies) - } - Value::Null => Err(Error::Client("Operation aborted".to_string()))?, - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hget(&self, key: String, field: String) -> Result, Error> { - // https://valkey.io/commands/hget/ - // HGET key field - - let response = self - .send(vec![ - Value::BulkString("HGET".to_string()), - Value::BulkString(key), - Value::BulkString(field), - ]) - .await?; - match response { - Value::BulkString(value) => Ok(Some(value)), - Value::Null => Ok(None), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hgetall(&self, key: String) -> Result, Error> { - // https://valkey.io/commands/hgetall/ - // HGETALL key - - let response = self - .send(vec![ - Value::BulkString("HGETALL".to_string()), - Value::BulkString(key), - ]) - .await?; - match response { - // RESP2 - Value::Array(items) => { - let mut fields = vec![]; - for item in items.chunks(2) { - let key = match item[0].clone().into() { - Value::BulkString(key) => key, - key => Err(Error::Client(format!("Unexpected key type: {:?}", key)))?, - }; - let value = match item[1].clone().into() { - Value::BulkString(value) => value, - value => Err(Error::Client(format!("Unexpected value type: {:?}", value)))?, - }; - fields.push((key, value)); - } - Ok(fields) - } - // RESP3 - Value::Map(items) => { - let mut fields = vec![]; - for (key, value) in items { - let key = match key.into() { - Value::BulkString(key) => key, - key => Err(Error::Client(format!("Unexpected key type: {:?}", key)))?, - }; - let value = match value.into() { - Value::BulkString(value) => value, - value => Err(Error::Client(format!("Unexpected value type: {:?}", value)))?, - }; - fields.push((key, value)); - } - Ok(fields) - } - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hincrby(&self, key: String, field: String, increment: i64) -> Result { - // https://valkey.io/commands/hincrby/ - // HINCRBY key field increment - - let response = self - .send(vec![ - Value::BulkString("HINCRBY".to_string()), - Value::BulkString(key), - Value::BulkString(field), - Value::BulkString(increment.to_string()), - ]) - .await?; - match response { - Value::Integer(value) => Ok(value), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hincrbyfloat( - &self, - key: String, - field: String, - increment: f64, - ) -> Result { - // https://valkey.io/commands/hincrbyfloat/ - // HINCRBYFLOAT key field increment - - let response = self - .send(vec![ - Value::BulkString("HINCRBYFLOAT".to_string()), - Value::BulkString(key), - Value::BulkString(field), - Value::BulkString(increment.to_string()), - ]) - .await?; - match response { - Value::BulkString(value) => Ok(value), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hkeys(&self, key: String) -> Result, Error> { - // https://valkey.io/commands/hkeys/ - // HKEYS key - - let response = self - .send(vec![ - Value::BulkString("HKEYS".to_string()), - Value::BulkString(key), - ]) - .await?; - match response { - Value::Array(values) => { - let mut keys = vec![]; - for value in values { - let value = value.into(); - match value { - Value::BulkString(key) => keys.push(key), - value => Err(Error::Client(format!( - "Unexpected array item type: {:?}", - value - )))?, - } - } - Ok(keys) - } - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hlen(&self, key: String) -> Result { - // https://valkey.io/commands/hlen/ - // HLEN key - - let response = self - .send(vec![ - Value::BulkString("HLEN".to_string()), - Value::BulkString(key), - ]) - .await?; - match response { - Value::Integer(value) => Ok(value as u64), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hmget(&self, key: String, fields: Vec) -> Result>, Error> { - // https://valkey.io/commands/hmget/ - // HMGET key field [ field ... ] - - let mut cmd = vec![ - Value::BulkString("HMGET".to_string()), - Value::BulkString(key), - ]; - for field in fields { - cmd.push(Value::BulkString(field)); - } - let response = self.send(cmd).await?; - match response { - Value::Array(items) => { - let mut values = vec![]; - for item in items { - match item.into() { - Value::BulkString(val) => values.push(Some(val)), - Value::Null => values.push(None), - item => Err(Error::Client(format!( - "Unexpected array item type: {:?}", - item - )))?, - } - } - Ok(values) - } - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hmset(&self, key: String, fields: Vec<(String, String)>) -> Result<(), Error> { - // https://valkey.io/commands/hmset/ - // HMSET key field value [ field value ... ] - - let mut cmd = vec![ - Value::BulkString("HMSET".to_string()), - Value::BulkString(key), - ]; - for (field, value) in fields { - cmd.push(Value::BulkString(field)); - cmd.push(Value::BulkString(value)); - } - let response = self.send(cmd).await?; - match response { - Value::String(msg) => match msg.as_str() { - "OK" => Ok(()), - msg => Err(Error::Client(format!("Not OK: {msg}")))?, - }, - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hrandfield( - &self, - key: String, - opts: Option, - ) -> Result)>>, Error> { - // https://valkey.io/commands/hrandfield/ - // HRANDFIELD key [ count [ WITHVALUES ] ] - - let mut cmd = vec![ - Value::BulkString("HRANDFIELD".to_string()), - Value::BulkString(key), - ]; - if let Some(opts) = opts { - if let Some(count) = opts.count { - cmd.push(Value::BulkString(count.to_string())); - } - if let Some(with_values) = opts.with_values { - if with_values { - if opts.count.is_none() { - Err(Error::Client( - "count must be specified to use with-values".to_string(), - ))? - } - cmd.push(Value::BulkString("WITHVALUES".to_string())); - } - } - } - let response = self.send(cmd).await?; - match response { - Value::BulkString(value) => Ok(Some(vec![(value, None)])), - Value::Array(items) => match items.len() { - 0 => Ok(None), - _ => { - let mut foo = vec![]; - match opts { - Some(HrandfieldOpts { - count: _, - with_values: Some(true), - }) => { - for item in items.chunks(2) { - let key = match item[0].clone().into() { - Value::BulkString(key) => key, - key => Err(Error::Client(format!( - "Unexpected key type: {:?}", - key - )))?, - }; - let value = match item[1].clone().into() { - Value::BulkString(value) => value, - value => Err(Error::Client(format!( - "Unexpected value type: {:?}", - value - )))?, - }; - foo.push((key, Some(value))); - } - } - _ => { - for key in items { - let key = match key.into() { - Value::BulkString(key) => key, - key => Err(Error::Client(format!( - "Unexpected key type: {:?}", - key - )))?, - }; - foo.push((key, None)); - } - } - } - Ok(Some(foo)) - } - }, - Value::Null => Ok(None), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hscan( - &self, - key: String, - cursor: Option, - opts: Option, - ) -> Result<(Option, Vec<(String, Option)>), Error> { - // https://valkey.io/commands/hscan/ - // HSCAN key cursor [ MATCH pattern ] [ COUNT count ] [ NOVALUES ] - - let mut cmd = vec![ - Value::BulkString("HSCAN".to_string()), - Value::BulkString(key), - Value::BulkString(cursor.unwrap_or("0".to_string())), - ]; - if let Some(opts) = opts.clone() { - if let Some(match_) = opts.match_ { - cmd.push(Value::BulkString("MATCH".to_string())); - cmd.push(Value::BulkString(match_)); - } - if let Some(count) = opts.count { - cmd.push(Value::BulkString("COUNT".to_string())); - cmd.push(Value::BulkString(count.to_string())); - } - if let Some(no_values) = opts.no_values { - if no_values { - cmd.push(Value::BulkString("NOVALUES".to_string())); - } - } - } - let response = self.send(cmd).await?; - match response { - Value::Array(items) => { - let cursor = match items[0].clone().into() { - Value::BulkString(cursor) => cursor, - cursor => Err(Error::Client(format!( - "Unexpected cursor type: {:?}", - cursor - )))?, - }; - let elements = match items[1].clone().into() { - Value::Array(elements) => elements, - elements => Err(Error::Client(format!( - "Unexpected elements type: {:?}", - elements - )))?, - }; - let mut fields = vec![]; - match opts { - Some(HscanOpts { - match_: _, - count: _, - no_values: Some(true), - }) => { - for field in elements { - match field.clone().into() { - Value::BulkString(field) => fields.push((field, None)), - field => Err(Error::Client(format!( - "Unexpected field type: {:?}", - field - )))?, - } - } - } - _ => { - for item in elements.chunks(2) { - let key = match item[0].clone().into() { - Value::BulkString(key) => key, - key => { - Err(Error::Client(format!("Unexpected key type: {:?}", key)))? - } - }; - let value = match item[1].clone().into() { - Value::BulkString(value) => value, - value => Err(Error::Client(format!( - "Unexpected value type: {:?}", - value - )))?, - }; - fields.push((key, Some(value))); - } - } - }; - - let cursor = match cursor.as_str() { - "0" => None, - _ => Some(cursor), - }; - - Ok((cursor, fields)) - } - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hset(&self, key: String, field: String, value: String) -> Result<(), Error> { - // https://valkey.io/commands/hset/ - // HSET key field value [ field value ... ] - - let response = self - .send(vec![ - Value::BulkString("HSET".to_string()), - Value::BulkString(key), - Value::BulkString(field), - Value::BulkString(value), - ]) - .await?; - match response { - Value::Integer(_) => Ok(()), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hsetnx(&self, key: String, field: String, value: String) -> Result { - // https://valkey.io/commands/hsetnx/ - // HSETNX key field value - - let response = self - .send(vec![ - Value::BulkString("HSETNX".to_string()), - Value::BulkString(key), - Value::BulkString(field), - Value::BulkString(value), - ]) - .await?; - match response { - Value::Integer(0) => Ok(false), - Value::Integer(1) => Ok(true), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hstrlen(&self, key: String, field: String) -> Result { - // https://valkey.io/commands/hstrlen/ - // HSTRLEN key field - - let response = self - .send(vec![ - Value::BulkString("HSTRLEN".to_string()), - Value::BulkString(key), - Value::BulkString(field), - ]) - .await?; - match response { - Value::Integer(len) => Ok(len as u64), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn hvals(&self, key: String) -> Result, Error> { - // https://valkey.io/commands/hvals/ - // HVALS key - - let response = self - .send(vec![ - Value::BulkString("HVALS".to_string()), - Value::BulkString(key), - ]) - .await?; - match response { - Value::Array(items) => { - let mut fields = vec![]; - for item in items { - match item.into() { - Value::BulkString(field) => fields.push(field), - field => Err(Error::Client(format!("Unexpected field type: {:?}", field)))?, - } - } - Ok(fields) - } - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn incr(&self, key: String) -> Result { - // https://valkey.io/commands/incr/ - // INCR key - - let response = self - .send(vec![ - Value::BulkString("INCR".to_string()), - Value::BulkString(key), - ]) - .await?; - match response { - Value::Integer(value) => Ok(value), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn incrby(&self, key: String, increment: i64) -> Result { - // https://valkey.io/commands/incrby/ - // INCRBY key increment - - let response = self - .send(vec![ - Value::BulkString("INCRBY".to_string()), - Value::BulkString(key), - Value::BulkString(increment.to_string()), - ]) - .await?; - match response { - Value::Integer(value) => Ok(value), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn keys(&self, pattern: String) -> Result, Error> { - // https://valkey.io/commands/keys/ - // KEYS pattern - - let response = self - .send(vec![ - Value::BulkString("KEYS".to_string()), - Value::BulkString(pattern), - ]) - .await?; - match response { - Value::Array(values) => { - let mut keys = vec![]; - for value in values { - let value = value.into(); - match value { - Value::BulkString(key) => keys.push(key), - value => Err(Error::Client(format!( - "Unexpected array item type: {:?}", - value - )))?, - } - } - Ok(keys) - } - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn ping(&self) -> Result<(), Error> { - // https://valkey.io/commands/ping/ - // PING [ message ] - - // TODO support command options - let response = self - .send(vec![Value::BulkString("PING".to_string())]) - .await?; - match response { - Value::String(msg) => match msg.as_str() { - "PONG" => Ok(()), - msg => Err(Error::Client(format!("Not PONG: {msg}")))?, - }, - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn quit(&self) -> Result<(), Error> { - // https://valkey.io/commands/quit/ - // QUIT - - let response = self - .send(vec![Value::BulkString("QUIT".to_string())]) - .await?; - match response { - Value::String(msg) => match msg.as_str() { - "OK" => Ok(()), - msg => Err(Error::Client(format!("Not OK: {msg}")))?, - }, - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn publish(&self, channel: String, message: String) -> Result { - // https://valkey.io/commands/publish/ - // PUBLISH channel message - - let response = self - .send(vec![ - Value::BulkString("PUBLISH".to_string()), - Value::BulkString(channel), - Value::BulkString(message), - ]) - .await?; - match response { - Value::Integer(value) => Ok(value), - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } - - async fn set(&self, key: String, value: String) -> Result<(), Error> { - // https://valkey.io/commands/set/ - // SET key value - // [ NX | XX | IFEQ comparison-value ] - // [ GET ] - // [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] - - // TODO support command options - let response = self - .send(vec![ - Value::BulkString("SET".to_string()), - Value::BulkString(key), - Value::BulkString(value), - ]) - .await?; - match response { - Value::String(msg) => match msg.as_str() { - "OK" => Ok(()), - msg => Err(Error::Client(format!("Not OK: {msg}")))?, - }, - Value::Null => Err(Error::Client("Operation aborted".to_string()))?, - Value::Error(err) => Err(Error::Valkey(err))?, - response => Err(Error::Client(format!( - "Unexpected response type: {:?}", - response - )))?, - } - } -} - -impl From for Error { - fn from(e: ErrorCode) -> Self { - match e { - ErrorCode::AccessDenied => Self::Client("Network AccessDenied".to_string()), - ErrorCode::NotSupported => Self::Client("Network NotSupported".to_string()), - ErrorCode::InvalidArgument => Self::Client("Network InvalidArgument".to_string()), - ErrorCode::OutOfMemory => Self::Client("Network OutOfMemory".to_string()), - ErrorCode::Timeout => Self::Client("Network Timeout".to_string()), - ErrorCode::InvalidState => Self::Client("Network InvalidState".to_string()), - ErrorCode::AddressNotBindable => Self::Client("Network AddressNotBindable".to_string()), - ErrorCode::AddressInUse => Self::Client("Network AddressInUse".to_string()), - ErrorCode::RemoteUnreachable => Self::Client("Network RemoteUnreachable".to_string()), - ErrorCode::ConnectionRefused => Self::Client("Network ConnectionRefused".to_string()), - ErrorCode::ConnectionBroken => Self::Client("Network ConnectionBroken".to_string()), - ErrorCode::ConnectionReset => Self::Client("Network ConnectionReset".to_string()), - ErrorCode::ConnectionAborted => Self::Client("Network ConnectionAborted".to_string()), - ErrorCode::DatagramTooLarge => Self::Client("Network DatagramTooLarge".to_string()), - ErrorCode::Other(None) => Self::Client("Network Other".to_string()), - ErrorCode::Other(Some(message)) => Self::Client(format!("Network Other: {message}")), - } - } -} - -impl From for Error { - fn from(e: LookupErrorCode) -> Self { - match e { - LookupErrorCode::AccessDenied => Self::Client("Network AccessDenied".to_string()), - LookupErrorCode::InvalidArgument => Self::Client("Network InvalidArgument".to_string()), - LookupErrorCode::NameUnresolvable => { - Self::Client("Network NameUnresolvable".to_string()) - } - LookupErrorCode::TemporaryResolverFailure => { - Self::Client("Network TemporaryResolverFailure".to_string()) - } - LookupErrorCode::PermanentResolverFailure => { - Self::Client("Network PermanentResolverFailure".to_string()) - } - LookupErrorCode::Other(None) => Self::Client("Network Other".to_string()), - LookupErrorCode::Other(Some(message)) => { - Self::Client(format!("Network Other: {message}")) - } - } - } -} - -impl RespGuest for ValkeyOps { - fn decode(data: Vec) -> Result { - decode(data).map_err(|e| e.to_string()) - } - - fn encode(value: Value) -> Result, RespError> { - Ok(encode(value.into())) - } -} - -impl From for NestedValue { - fn from(value: Value) -> Self { - ValkeyOps::encode(value).expect("values must encode cleanly") - } -} - -impl From for Value { - fn from(value: NestedValue) -> Self { - ValkeyOps::decode(value).expect("nested values must decode cleanly") - } -} - -impl From for Value { - fn from(mode: ExpireMode) -> Self { - match mode { - ExpireMode::Nx => Value::BulkString("NX".to_string()), - ExpireMode::Xx => Value::BulkString("XX".to_string()), - ExpireMode::Gt => Value::BulkString("GT".to_string()), - ExpireMode::Lt => Value::BulkString("LT".to_string()), - } - } -} - -wit_bindgen::generate!({ - world: "ops", - path: "../wit", - generate_all -}); - -export!(ValkeyOps); diff --git a/components/ops/src/resp/mod.rs b/components/ops/src/resp/mod.rs deleted file mode 100644 index ba7ba4f..0000000 --- a/components/ops/src/resp/mod.rs +++ /dev/null @@ -1,358 +0,0 @@ -//! RESP serialize - -use crate::exports::componentized::valkey::resp::{NestedValue, Value}; -use crate::exports::componentized::valkey::store::Error; -use core::f64; -use std::io::{BufRead, BufReader, Read}; -use std::string::String; -use std::vec::Vec; - -/// up to 512 MB in length -const RESP_MAX_SIZE: i64 = 512 * 1024 * 1024; -const CRLF_BYTES: &'static [u8] = b"\r\n"; - -/// Encodes RESP value to RESP binary buffer. -/// # Examples -/// ``` -/// # use self::resp::{Value, encode}; -/// let val = Value::String("OK".to_string()); -/// assert_eq!(encode(&val), vec![43, 79, 75, 13, 10]); -/// ``` -pub fn encode(value: Value) -> Vec { - let mut res: Vec = Vec::new(); - buf_encode(value, &mut res); - res -} - -#[inline] -fn buf_encode(value: Value, buf: &mut Vec) { - match value { - Value::Null => { - buf.push(b'_'); - buf.extend_from_slice(CRLF_BYTES); - } - Value::String(val) => { - buf.push(b'+'); - buf.extend_from_slice(val.as_bytes()); - buf.extend_from_slice(CRLF_BYTES); - } - Value::Error(val) => { - buf.push(b'-'); - buf.extend_from_slice(val.as_bytes()); - buf.extend_from_slice(CRLF_BYTES); - } - Value::Integer(val) => { - buf.push(b':'); - buf.extend_from_slice(val.to_string().as_bytes()); - buf.extend_from_slice(CRLF_BYTES); - } - Value::BulkString(val) => { - buf.push(b'$'); - buf.extend_from_slice(val.len().to_string().as_bytes()); - buf.extend_from_slice(CRLF_BYTES); - buf.extend_from_slice(val.as_bytes()); - buf.extend_from_slice(CRLF_BYTES); - } - Value::Array(val) => { - buf.push(b'*'); - buf.extend_from_slice(val.len().to_string().as_bytes()); - buf.extend_from_slice(CRLF_BYTES); - for item in val { - buf_encode(item.into(), buf); - } - } - Value::Boolean(ref val) => { - buf.push(b'#'); - match val { - true => buf.push(b't'), - false => buf.push(b'f'), - } - buf.extend_from_slice(CRLF_BYTES); - } - Value::Double(ref val) => { - buf.push(b','); - buf.extend_from_slice(val.to_string().to_lowercase().as_bytes()); - buf.extend_from_slice(CRLF_BYTES); - } - Value::BigNumber(ref val) => { - buf.push(b'('); - buf.extend_from_slice(val.as_bytes()); - buf.extend_from_slice(CRLF_BYTES); - } - Value::BulkError(ref val) => { - buf.push(b'!'); - buf.extend_from_slice(val.len().to_string().as_bytes()); - buf.extend_from_slice(CRLF_BYTES); - buf.extend_from_slice(val.as_bytes()); - buf.extend_from_slice(CRLF_BYTES); - } - Value::VerbatimString((ref encoding, ref value)) => { - let val = format!("{encoding}:{value}"); - buf.push(b'='); - buf.extend_from_slice(val.len().to_string().as_bytes()); - buf.extend_from_slice(CRLF_BYTES); - buf.extend_from_slice(val.as_bytes()); - buf.extend_from_slice(CRLF_BYTES); - } - Value::Map(val) => { - buf.push(b'%'); - buf.extend_from_slice(val.len().to_string().as_bytes()); - buf.extend_from_slice(CRLF_BYTES); - for (key, value) in val { - buf_encode(key.into(), buf); - buf_encode(value.into(), buf); - } - } - Value::Set(val) => { - buf.push(b'~'); - buf.extend_from_slice(val.len().to_string().as_bytes()); - buf.extend_from_slice(CRLF_BYTES); - for item in val { - buf_encode(item.into(), buf); - } - } - Value::Push(val) => { - buf.push(b'>'); - buf.extend_from_slice(val.len().to_string().as_bytes()); - buf.extend_from_slice(CRLF_BYTES); - for item in val { - buf_encode(item.into(), buf); - } - } - } -} - -pub fn decode(value: Vec) -> Result { - Decoder::new(BufReader::new(value.as_slice())).decode() -} - -/// A streaming RESP Decoder. -#[derive(Debug)] -pub struct Decoder { - reader: BufReader, -} - -impl Decoder { - /// Creates a Decoder instance with given BufReader for decoding the RESP buffers. - /// # Examples - /// ``` - /// # use std::io::BufReader; - /// # use self::resp::{Decoder, Value}; - /// - /// let value = Value::BulkString("Hello".to_string()); - /// let buf = value.encode(); - /// let mut decoder = Decoder::new(BufReader::new(buf.as_slice())); - /// assert_eq!(decoder.decode().unwrap(), Value::BulkString("Hello".to_string())); - /// ``` - pub fn new(reader: BufReader) -> Self { - Decoder { reader: reader } - } - - /// It will read buffers from the inner BufReader, decode it to a Value. - pub fn decode(&mut self) -> Result { - let mut res: Vec = Vec::new(); - self.reader - .read_until(b'\n', &mut res) - .map_err(|e| Error::Resp(e.to_string()))?; - - let len = res.len(); - if len == 0 { - Err(Error::Resp("unexpected EOF".to_string()))? - } - if len < 3 { - Err(Error::Resp(format!("too short: {}", len)))? - } - if !is_crlf(res[len - 2], res[len - 1]) { - Err(Error::Resp(format!("invalid CRLF: {:?}", res)))? - } - - let bytes = res[1..len - 2].as_ref(); - match res[0] { - // Value::String - b'+' => parse_string(bytes).map(Value::String), - // Value::Error - b'-' => parse_string(bytes).map(Value::Error), - // Value::Integer - b':' => parse_integer(bytes).map(Value::Integer), - // Value::BulkString - b'$' => { - let int = parse_integer(bytes)?; - if int == -1 { - // Null bulk string, special case for RESP2 - return Ok(Value::Null); - } - if int < -1 || int >= RESP_MAX_SIZE { - Err(Error::Resp(format!("invalid bulk string length: {}", int)))? - } - - let mut buf: Vec = Vec::new(); - let int = int as usize; - buf.resize(int + 2, 0); - self.reader - .read_exact(buf.as_mut_slice()) - .map_err(|e| Error::Resp(e.to_string()))?; - if !is_crlf(buf[int], buf[int + 1]) { - Err(Error::Resp(format!("invalid CRLF: {:?}", buf)))? - } - buf.truncate(int); - parse_string(buf.as_slice()).map(Value::BulkString) - } - // Value::Array - b'*' => { - let int = parse_integer(bytes)?; - if int == -1 { - // Null array, special case for RESP2 - return Ok(Value::Null); - } - if int < -1 || int >= RESP_MAX_SIZE { - Err(Error::Resp(format!("invalid array length: {}", int)))? - } - - let mut array: Vec = Vec::with_capacity(int as usize); - for _ in 0..int { - // TODO avoid full decoding just to re-encode - let val = self.decode()?; - array.push(val.into()); - } - Ok(Value::Array(array)) - } - // Value::Null - b'_' => Ok(Value::Null), - // Value::Boolean - b'#' => match bytes[0] { - b'f' => Ok(Value::Boolean(false)), - b't' => Ok(Value::Boolean(true)), - _ => Err(Error::Resp(format!("invalid RESP boolean: {:?}", bytes))), - }, - // Value::Double - b',' => parse_double(bytes).map(Value::Double), - // Value::BigNumber - b'(' => parse_string(bytes).map(Value::BigNumber), - // Value::BulkError - b'!' => { - let int = parse_integer(bytes)?; - if int < 0 || int >= RESP_MAX_SIZE { - Err(Error::Resp(format!("invalid bulk error length: {}", int)))? - } - - let mut buf: Vec = Vec::new(); - let int = int as usize; - buf.resize(int + 2, 0); - self.reader - .read_exact(buf.as_mut_slice()) - .map_err(|e| Error::Resp(e.to_string()))?; - if !is_crlf(buf[int], buf[int + 1]) { - Err(Error::Resp(format!("invalid CRLF: {:?}", buf)))? - } - buf.truncate(int); - parse_string(buf.as_slice()).map(Value::BulkError) - } - // Value::VerbatimString - b'=' => { - let int = parse_integer(bytes)?; - if int < 0 || int >= RESP_MAX_SIZE { - Err(Error::Resp(format!( - "invalid verbatim string length: {}", - int - )))? - } - - let mut buf: Vec = Vec::new(); - let int = int as usize; - buf.resize(int + 2, 0); - self.reader - .read_exact(buf.as_mut_slice()) - .map_err(|e| Error::Resp(e.to_string()))?; - if !is_crlf(buf[int], buf[int + 1]) { - Err(Error::Resp(format!("invalid CRLF: {:?}", buf)))? - } - buf.truncate(int); - match parse_string(buf.as_slice()) { - Err(err) => Err(err)?, - Ok(str) => { - let parts: Vec<&str> = str.splitn(2, ':').collect(); - if parts.len() != 2 { - Err(Error::Resp( - "invalid verbatim string, missing encoding".to_string(), - ))? - } - Ok(Value::VerbatimString(( - parts[0].to_string(), - parts[1].to_string(), - ))) - } - } - } - // Value::Map - b'%' => { - let int = parse_integer(bytes)?; - if int < 0 || int >= RESP_MAX_SIZE { - Err(Error::Resp(format!("invalid array length: {}", int)))? - } - - let mut map: Vec<(NestedValue, NestedValue)> = Vec::with_capacity(int as usize); - for _ in 0..int { - // TODO avoid full decoding just to re-encode - let key = self.decode()?; - let val = self.decode()?; - map.push((key.into(), val.into())); - } - Ok(Value::Map(map)) - } - // Value::Set - b'~' => { - let int = parse_integer(bytes)?; - if int < 0 || int >= RESP_MAX_SIZE { - Err(Error::Resp(format!("invalid array length: {}", int)))? - } - - let mut set: Vec = Vec::with_capacity(int as usize); - for _ in 0..int { - // TODO avoid full decoding just to re-encode - let val = self.decode()?; - set.push(val.into()); - } - Ok(Value::Set(set)) - } - // Value::Push - b'>' => { - let int = parse_integer(bytes)?; - if int < 0 || int >= RESP_MAX_SIZE { - Err(Error::Resp(format!("invalid array length: {}", int)))? - } - - let mut push: Vec = Vec::with_capacity(int as usize); - for _ in 0..int { - // TODO avoid full decoding just to re-encode - let val = self.decode()?; - push.push(val.into()); - } - Ok(Value::Push(push)) - } - - prefix => Err(Error::Resp(format!("invalid RESP type: {:?}", prefix))), - } - } -} - -#[inline] -fn is_crlf(a: u8, b: u8) -> bool { - a == b'\r' && b == b'\n' -} - -#[inline] -fn parse_string(bytes: &[u8]) -> Result { - String::from_utf8(bytes.to_vec()).map_err(|err| Error::Resp(err.to_string())) -} - -#[inline] -fn parse_integer(bytes: &[u8]) -> Result { - let str_integer = parse_string(bytes)?; - (str_integer.parse::()).map_err(|err| Error::Resp(err.to_string())) -} - -#[inline] -fn parse_double(bytes: &[u8]) -> Result { - let str_double = parse_string(bytes)?; - (str_double.parse::()).map_err(|err| Error::Resp(err.to_string())) -} diff --git a/components/pubsub-commands/Cargo.toml b/components/pubsub-commands/Cargo.toml new file mode 100644 index 0000000..62a6376 --- /dev/null +++ b/components/pubsub-commands/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "pubsub-commands" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] +# shared with the other command components +path = "../../crates/commands/src/lib.rs" + +[features] +default = ["pubsub"] +admin = [] +connection = [] +hash = [] +keyspace = [] +pubsub = [] +string = [] + +[dependencies] +resp-codec = { workspace = true } +wit-bindgen = { workspace = true } diff --git a/components/pubsub-commands/README.md b/components/pubsub-commands/README.md new file mode 100644 index 0000000..4682fa0 --- /dev/null +++ b/components/pubsub-commands/README.md @@ -0,0 +1,5 @@ +# pubsub-commands + +Publish/subscribe commands, Valkey's @pubsub ACL category. + +Exports `componentized:valkey/pubsub-commands` over an imported [`connection`](../connection/). [`commands`](../commands/) exports every command interface in one component. diff --git a/components/resp/Cargo.toml b/components/resp/Cargo.toml new file mode 100644 index 0000000..58fdd9a --- /dev/null +++ b/components/resp/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "resp" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +resp-codec = { workspace = true } +wit-bindgen = { workspace = true } + +[dev-dependencies] +test-harness = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } diff --git a/components/resp/README.md b/components/resp/README.md new file mode 100644 index 0000000..f038704 --- /dev/null +++ b/components/resp/README.md @@ -0,0 +1,5 @@ +# resp + +Encodes and decodes RESP values, the `componentized:valkey/resp` interface. + +The other components share the RESP types with this one, a composition that uses them includes it. diff --git a/components/resp/src/lib.rs b/components/resp/src/lib.rs new file mode 100644 index 0000000..28c9e4d --- /dev/null +++ b/components/resp/src/lib.rs @@ -0,0 +1,71 @@ +//! Exports `componentized:valkey/resp` with the shared codec. +//! +//! An exported interface has types of its own, they are converted to and from the shared codec's +//! types. A value is not recursive, nested values are encoded RESP, so each conversion is one +//! match. + +// the test harness provides a main +#![cfg_attr(not(test), no_main)] + +use exports::componentized::valkey::resp::{Error, Guest, Value}; + +struct Component; + +impl Guest for Component { + fn decode(data: Vec) -> Result { + resp_codec::decode(data).map(from_codec) + } + + fn encode(value: Value) -> Result, Error> { + Ok(resp_codec::encode(to_codec(value))) + } +} + +fn to_codec(value: Value) -> resp_codec::Value { + use resp_codec::Value as V; + match value { + Value::String(value) => V::String(value), + Value::Error(value) => V::Error(value), + Value::Integer(value) => V::Integer(value), + Value::BulkString(value) => V::BulkString(value), + Value::Array(value) => V::Array(value), + Value::Null => V::Null, + Value::Boolean(value) => V::Boolean(value), + Value::Double(value) => V::Double(value), + Value::BigNumber(value) => V::BigNumber(value), + Value::BulkError(value) => V::BulkError(value), + Value::VerbatimString(value) => V::VerbatimString(value), + Value::Map(value) => V::Map(value), + Value::Set(value) => V::Set(value), + Value::Push(value) => V::Push(value), + } +} + +fn from_codec(value: resp_codec::Value) -> Value { + use resp_codec::Value as V; + match value { + V::String(value) => Value::String(value), + V::Error(value) => Value::Error(value), + V::Integer(value) => Value::Integer(value), + V::BulkString(value) => Value::BulkString(value), + V::Array(value) => Value::Array(value), + V::Null => Value::Null, + V::Boolean(value) => Value::Boolean(value), + V::Double(value) => Value::Double(value), + V::BigNumber(value) => Value::BigNumber(value), + V::BulkError(value) => Value::BulkError(value), + V::VerbatimString(value) => Value::VerbatimString(value), + V::Map(value) => Value::Map(value), + V::Set(value) => Value::Set(value), + V::Push(value) => Value::Push(value), + } +} + +wit_bindgen::generate!({ + // `path` is relative to the manifest, `components/resp/` + path: "../wit", + world: "resp", + generate_all, +}); + +export!(Component); diff --git a/components/resp/tests/resp.rs b/components/resp/tests/resp.rs new file mode 100644 index 0000000..e8ba771 --- /dev/null +++ b/components/resp/tests/resp.rs @@ -0,0 +1,80 @@ +//! Tests for the `resp` component, which converts between its exported types and the codec's. + +use test_harness::Harness; +use test_harness::resp_bindings::exports::componentized::valkey::resp::Value; + +/// One value of every type, nested values are encoded RESP. +fn values() -> Vec { + vec![ + Value::String("OK".to_string()), + Value::Error("ERR oops".to_string()), + Value::Integer(-42), + Value::BulkString("hello\r\nworld".to_string()), + Value::Array(vec![b"+a\r\n".to_vec(), b":1\r\n".to_vec()]), + Value::Null, + Value::Boolean(true), + Value::Double(1.5), + Value::BigNumber("3492890328409238509324850943850943825024385".to_string()), + Value::BulkError("SYNTAX invalid".to_string()), + Value::VerbatimString(("txt".to_string(), "Some string".to_string())), + Value::Map(vec![(b"+key\r\n".to_vec(), b"+value\r\n".to_vec())]), + Value::Set(vec![b"+a\r\n".to_vec(), b"+b\r\n".to_vec()]), + Value::Push(vec![b"+message\r\n".to_vec()]), + ] +} + +#[tokio::test(flavor = "multi_thread")] +async fn values_round_trip() -> wasmtime::Result<()> { + let mut harness = Harness::new("resp").await?; + harness + .run(async |accessor, exports| { + for value in values() { + let encoded = exports + .resp() + .call_encode(accessor, value.clone()) + .await? + .expect("encode"); + let decoded = exports + .resp() + .call_decode(accessor, encoded) + .await? + .expect("decode"); + assert_eq!(decoded, value); + } + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn encodes_resp() -> wasmtime::Result<()> { + let mut harness = Harness::new("resp").await?; + harness + .run(async |accessor, exports| { + let encoded = exports + .resp() + .call_encode( + accessor, + Value::Array(vec![b"$3\r\nGET\r\n".to_vec(), b"$1\r\na\r\n".to_vec()]), + ) + .await?; + assert_eq!(encoded, Ok(b"*2\r\n$3\r\nGET\r\n$1\r\na\r\n".to_vec())); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn invalid_resp_fails_to_decode() -> wasmtime::Result<()> { + let mut harness = Harness::new("resp").await?; + harness + .run(async |accessor, exports| { + let decoded = exports + .resp() + .call_decode(accessor, b"?nope\r\n".to_vec()) + .await?; + assert!(decoded.is_err(), "decoded {decoded:?}"); + Ok(()) + }) + .await +} diff --git a/components/sample-http-incrementor/Cargo.toml b/components/sample-http-incrementor/Cargo.toml index 4bda6c8..d93f7e9 100644 --- a/components/sample-http-incrementor/Cargo.toml +++ b/components/sample-http-incrementor/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sample-http-incrementor" version = "0.1.0" -edition = "2021" +edition = "2024" license = "Apache-2.0" [lib] diff --git a/components/sample-http-incrementor/README.md b/components/sample-http-incrementor/README.md index edcb89b..6aa63d3 100644 --- a/components/sample-http-incrementor/README.md +++ b/components/sample-http-incrementor/README.md @@ -12,7 +12,7 @@ wac plug sample-http-incrementor.wasm --plug client.wasm -o valkey-http-incremen Start a Valkey server: ```sh -docker run --rm -p 6379:6379 valkey/valkey:8 +docker run --rm -p 6379:6379 valkey/valkey:9 ``` Start the incrementor: @@ -21,6 +21,8 @@ Start the incrementor: wasmtime serve -Sconfig -Sinherit-network -Sallow-ip-name-lookup -Scli valkey-http-incrementor.wasm ``` +The client connects to `127.0.0.1:6379` unless configured otherwise, e.g. `-Sconfig-var=hostname=valkey.example.com -Sconfig-var=port=6380`, see [`tcp-connector`](../tcp-connector/) for the settings, and [`as-keyvalue`](../as-keyvalue/) for `key-prefix`. + Increment all the things: ```sh diff --git a/components/sample-http-incrementor/src/lib.rs b/components/sample-http-incrementor/src/lib.rs index e2efb03..7d74522 100644 --- a/components/sample-http-incrementor/src/lib.rs +++ b/components/sample-http-incrementor/src/lib.rs @@ -1,5 +1,3 @@ -#![no_main] - use exports::wasi::http::handler::Guest; use wasi::http::types::{ErrorCode, Fields, Request, Response}; use wasmcloud::keyvalue::{atomics, store, types::Error}; diff --git a/components/string-commands/Cargo.toml b/components/string-commands/Cargo.toml new file mode 100644 index 0000000..7ad1a39 --- /dev/null +++ b/components/string-commands/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "string-commands" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] +# shared with the other command components +path = "../../crates/commands/src/lib.rs" + +[features] +default = ["string"] +admin = [] +connection = [] +hash = [] +keyspace = [] +pubsub = [] +string = [] + +[dependencies] +resp-codec = { workspace = true } +wit-bindgen = { workspace = true } diff --git a/components/string-commands/README.md b/components/string-commands/README.md new file mode 100644 index 0000000..8ddce74 --- /dev/null +++ b/components/string-commands/README.md @@ -0,0 +1,5 @@ +# string-commands + +Commands for string values, Valkey's @string ACL category. + +Exports `componentized:valkey/string-commands` over an imported [`connection`](../connection/). [`commands`](../commands/) exports every command interface in one component. diff --git a/components/ops/Cargo.toml b/components/tcp-connector/Cargo.toml similarity index 64% rename from components/ops/Cargo.toml rename to components/tcp-connector/Cargo.toml index 0925829..af3b52a 100644 --- a/components/ops/Cargo.toml +++ b/components/tcp-connector/Cargo.toml @@ -1,11 +1,12 @@ [package] -name = "ops" +name = "tcp-connector" version = "0.1.0" -edition = "2021" +edition = "2024" license = "Apache-2.0" [lib] crate-type = ["cdylib"] [dependencies] +resp-codec = { workspace = true } wit-bindgen = { workspace = true } diff --git a/components/tcp-connector/README.md b/components/tcp-connector/README.md new file mode 100644 index 0000000..d7e6122 --- /dev/null +++ b/components/tcp-connector/README.md @@ -0,0 +1,14 @@ +# tcp-connector + +A connector for connections over TCP, to the server configured with wasi:config. + +| Key | Default | | +| --- | --- | --- | +| `hostname` | `127.0.0.1` | hostname or IP address of the server | +| `port` | `6379` | port of the server | +| `username` | `default` | user to authenticate as, with `password` | +| `password` | | authenticates when set | +| `client-name` | | name of the connection, see `CLIENT SETNAME` | +| `keep-alive-interval-ms` | | sends `PING` after this many milliseconds idle, disabled when unset or `0` | + +Compose with [`tcp`](../tcp/), e.g. to provide connections to [`as-keyvalue`](../as-keyvalue/). diff --git a/components/tcp-connector/src/lib.rs b/components/tcp-connector/src/lib.rs new file mode 100644 index 0000000..56b073c --- /dev/null +++ b/components/tcp-connector/src/lib.rs @@ -0,0 +1,76 @@ +//! A connector for connections over TCP, to the server configured with wasi:config. + +// the test harness provides a main +#![cfg_attr(not(test), no_main)] + +use componentized::valkey::connection::{ConnectOpts, Connection, Error}; +use componentized::valkey::tcp; +use exports::componentized::valkey::connector::Guest; +use wasi::config::store as config; + +const HOSTNAME_KEY: &str = "hostname"; +const HOSTNAME_DEFAULT: &str = "127.0.0.1"; +const PORT_KEY: &str = "port"; +const PORT_DEFAULT: &str = "6379"; +const USERNAME_KEY: &str = "username"; +const USERNAME_DEFAULT: &str = "default"; +const PASSWORD_KEY: &str = "password"; +const CLIENT_NAME_KEY: &str = "client-name"; +const KEEP_ALIVE_INTERVAL_MS_KEY: &str = "keep-alive-interval-ms"; + +struct Component; + +impl Guest for Component { + async fn connect() -> Result { + let hostname = get(HOSTNAME_KEY)?.unwrap_or(HOSTNAME_DEFAULT.to_string()); + let port = get(PORT_KEY)?.unwrap_or(PORT_DEFAULT.to_string()); + let port: u16 = port + .parse() + .map_err(|_| Error::Client(format!("{PORT_KEY} must be a port number: {port}")))?; + let auth = match get(PASSWORD_KEY)? { + Some(password) => { + let username = get(USERNAME_KEY)?.unwrap_or(USERNAME_DEFAULT.to_string()); + Some((username, password)) + } + None => None, + }; + let keep_alive_interval = match get(KEEP_ALIVE_INTERVAL_MS_KEY)? { + Some(interval) => { + let interval: u64 = interval.parse().map_err(|_| { + Error::Client(format!( + "{KEEP_ALIVE_INTERVAL_MS_KEY} must be a number of milliseconds: {interval}" + )) + })?; + // a duration is in nanoseconds + Some(interval.saturating_mul(1_000_000)) + } + None => None, + }; + let opts = ConnectOpts { + auth, + client_name: get(CLIENT_NAME_KEY)?, + keep_alive_interval, + }; + tcp::connect(hostname, port, Some(opts)).await + } +} + +fn get(key: &str) -> Result, Error> { + config::get(key).map_err(|error| match error { + config::Error::Upstream(message) => Error::Client(format!("config upstream: {message}")), + config::Error::Io(message) => Error::Client(format!("config io: {message}")), + }) +} + +wit_bindgen::generate!({ + // `path` is relative to the manifest, `components/tcp-connector/` + path: "../wit", + world: "tcp-connector", + // the resp types shared by every component + with: { + "componentized:valkey/resp@0.3.0-dev": resp_codec::bindings::componentized::valkey::resp, + }, + generate_all, +}); + +export!(Component); diff --git a/components/tcp/Cargo.toml b/components/tcp/Cargo.toml new file mode 100644 index 0000000..6848807 --- /dev/null +++ b/components/tcp/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "tcp" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +futures-channel = { workspace = true } +futures-core = { workspace = true } +resp-codec = { workspace = true } +wit-bindgen = { workspace = true, features = ["async-spawn", "inter-task-wakeup"] } + +[dev-dependencies] +test-harness = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } diff --git a/components/tcp/README.md b/components/tcp/README.md new file mode 100644 index 0000000..96ca6cd --- /dev/null +++ b/components/tcp/README.md @@ -0,0 +1,5 @@ +# tcp + +Connections to a Valkey server over wasi:sockets TCP. + +Frames the bytes the socket carries into RESP values in each direction, and establishes a [`connection`](../connection/) over them. Compose with `connection` and [`resp`](../resp/). diff --git a/components/tcp/src/lib.rs b/components/tcp/src/lib.rs new file mode 100644 index 0000000..870e50f --- /dev/null +++ b/components/tcp/src/lib.rs @@ -0,0 +1,291 @@ +//! Connections to a Valkey server over wasi:sockets TCP. +//! +//! Opening a transport connects the socket and starts two background tasks, in the task of the +//! call that opened it: +//! +//! - the receive pump frames the bytes the socket receives into RESP values +//! - the send pump encodes the RESP values handed to `send` into bytes for the socket +//! +//! `send` and `receive` are sync methods, which have no task of their own to start background +//! work in, so they only hand over the streams the pumps already work with, and leave resolving +//! the futures of repeated calls to a third task. The pumps keep the socket open until both +//! directions are done, so a connection keeps its transport alive. + +// the test harness provides a main +#![cfg_attr(not(test), no_main)] + +use std::cell::RefCell; +use std::future::poll_fn; +use std::net::IpAddr; +use std::pin::Pin; +use std::rc::Rc; + +use componentized::valkey::connection::{ConnectOpts, Connection, Error}; +use exports::componentized::valkey::tcp::{Guest, GuestTransport, Transport}; +use futures_channel::{mpsc, oneshot}; +use futures_core::Stream; +use resp_codec::Value; +use wasi::sockets::ip_name_lookup::{ErrorCode as LookupErrorCode, resolve_addresses}; +use wasi::sockets::types::{ + ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, + TcpSocket, +}; +use wit_bindgen::{ + FutureReader, FutureWriter, StreamReader, StreamResult, StreamWriter, spawn_local, +}; + +/// How many bytes to read from the socket at a time. +const READ_SIZE: usize = 4096; + +struct Component; + +impl Guest for Component { + type Transport = TransportImpl; + + async fn connect( + host: String, + port: u16, + opts: Option, + ) -> Result { + let transport = open(&host, port).await?; + let (replies, closed) = transport.receive(); + let (requests, connection) = Connection::new(replies, closed, opts).await; + // the connection resolves with the send pump's errors, the result of sending is not needed + let _sent = transport.send(requests); + connection.await + } +} + +/// The streams of an open socket, handed over once each. +pub struct TransportImpl { + requests: RefCell< + Option<( + oneshot::Sender>, + FutureReader>, + )>, + >, + replies: RefCell, FutureReader>)>>, + /// Futures for the resolver to fail, a sync method can't write them itself. + failures: mpsc::UnboundedSender<(FutureWriter>, &'static str)>, +} + +impl TransportImpl { + /// A future the resolver fails with `message`. + fn failed(&self, message: &'static str) -> FutureReader> { + let (writer, reader) = wit_future::new(|| Err(Error::Closed(None))); + // a transport whose resolver stopped drops the writer, resolving it to the default + let _ = self.failures.unbounded_send((writer, message)); + reader + } +} + +impl GuestTransport for TransportImpl { + async fn open(host: String, port: u16) -> Result { + Ok(Transport::new(open(&host, port).await?)) + } + + fn send(&self, requests: StreamReader) -> FutureReader> { + match self.requests.borrow_mut().take() { + Some((pump, sent)) => { + // the pump only stops waiting for the requests once the socket is closed + let _ = pump.send(requests); + sent + } + None => self.failed("send may be called at most once"), + } + } + + fn receive(&self) -> (StreamReader, FutureReader>) { + self.replies.borrow_mut().take().unwrap_or_else(|| { + let (_, replies) = wit_stream::new(); + (replies, self.failed("receive may be called at most once")) + }) + } +} + +/// Fail each future handed over, until the transport is dropped. +async fn resolve_failures( + mut failures: mpsc::UnboundedReceiver<(FutureWriter>, &'static str)>, +) { + while let Some((writer, message)) = poll_fn(|cx| Pin::new(&mut failures).poll_next(cx)).await { + let _ = writer.write(Err(Error::Client(message.to_string()))).await; + } +} + +/// Connect a socket to `host` and start its pumps. +async fn open(host: &str, port: u16) -> Result { + let socket = Rc::new(connect_socket(host, port).await?); + + let (bytes_out, bytes_out_reader) = wit_stream::new(); + let socket_sent = socket.send(bytes_out_reader); + let (bytes_in, socket_received) = socket.receive(); + + let (replies_writer, replies) = wit_stream::new(); + // the defaults are only written if a pump stops without a result, the socket closed under it + let (closed_writer, closed) = wit_future::new(|| Err(Error::Closed(None))); + let (sent_writer, sent) = wit_future::new(|| Err(Error::Closed(None))); + let (requests_tx, requests_rx) = oneshot::channel(); + let (failures_tx, failures_rx) = mpsc::unbounded(); + + spawn_local(receive_pump( + socket.clone(), + bytes_in, + socket_received, + replies_writer, + closed_writer, + )); + spawn_local(send_pump( + socket, + requests_rx, + bytes_out, + socket_sent, + sent_writer, + )); + spawn_local(resolve_failures(failures_rx)); + + Ok(TransportImpl { + requests: RefCell::new(Some((requests_tx, sent))), + replies: RefCell::new(Some((replies, closed))), + failures: failures_tx, + }) +} + +/// Frame the bytes the socket receives into values, until the socket closes or the reader of the +/// values is gone. +async fn receive_pump( + _socket: Rc, + mut bytes: StreamReader, + received: FutureReader>, + mut replies: StreamWriter, + closed: FutureWriter>, +) { + // bytes read from the socket that are not part of a whole value yet + let mut buffered: Vec = Vec::new(); + let result = 'pump: loop { + // hand over each whole value buffered, a value may span several reads + loop { + match resp_codec::frame_len(&buffered) { + Ok(Some(len)) => { + let rest = buffered.split_off(len); + let frame = std::mem::replace(&mut buffered, rest); + match resp_codec::decode(frame) { + Ok(value) => { + if replies.write_one(value).await.is_some() { + // nobody reads the values anymore + break 'pump Ok(()); + } + } + Err(error) => break 'pump Err(Error::Resp(error)), + } + } + Ok(None) => break, + Err(error) => break 'pump Err(Error::Resp(error)), + } + } + match bytes.read(Vec::with_capacity(READ_SIZE)).await { + (StreamResult::Complete(_), chunk) => buffered.extend_from_slice(&chunk), + (StreamResult::Dropped | StreamResult::Cancelled, _) => { + // the socket closed, its future says whether it closed gracefully + break received.await.map_err(socket_error); + } + } + }; + // closing the values ends the connection's reader, which then waits for the result + drop(replies); + let _ = closed.write(result).await; +} + +/// Encode the values handed to `send` into bytes for the socket, until the values end, which +/// half-closes the socket. +async fn send_pump( + _socket: Rc, + requests: oneshot::Receiver>, + mut bytes: StreamWriter, + socket_sent: FutureReader>, + sent: FutureWriter>, +) { + // a transport dropped without sending half-closes the socket + let Ok(mut requests) = requests.await else { + return; + }; + while let Some(value) = requests.next().await { + if !bytes.write_all(resp_codec::encode(value)).await.is_empty() { + // the socket stopped sending + break; + } + } + drop(bytes); + let result = socket_sent.await.map_err(socket_error); + let _ = sent.write(result).await; +} + +/// Connect a socket to the first address of `host` that accepts it. +async fn connect_socket(host: &str, port: u16) -> Result { + let mut last_error = None; + for address in resolve(host, port).await? { + let family = match address { + IpSocketAddress::Ipv4(_) => IpAddressFamily::Ipv4, + IpSocketAddress::Ipv6(_) => IpAddressFamily::Ipv6, + }; + let socket = TcpSocket::create(family).map_err(socket_error)?; + match socket.connect(address).await { + Ok(()) => return Ok(socket), + Err(error) => last_error = Some(error), + } + } + let reason = match last_error { + Some(error) => format!("unable to connect to {host}:{port}: {error:?}"), + None => format!("unable to connect to {host}:{port}: no addresses"), + }; + Err(Error::Closed(Some(reason))) +} + +/// The socket addresses of `host`, an IP address or a hostname to resolve. +async fn resolve(host: &str, port: u16) -> Result, Error> { + let addresses = match host.parse() { + Ok(IpAddr::V4(address)) => { + let [a, b, c, d] = address.octets(); + vec![IpAddress::Ipv4((a, b, c, d))] + } + Ok(IpAddr::V6(address)) => { + let [a, b, c, d, e, f, g, h] = address.segments(); + vec![IpAddress::Ipv6((a, b, c, d, e, f, g, h))] + } + Err(_) => resolve_addresses(host.to_string()) + .await + .map_err(|error| lookup_error(host, error))?, + }; + Ok(addresses + .into_iter() + .map(|address| match address { + IpAddress::Ipv4(address) => IpSocketAddress::Ipv4(Ipv4SocketAddress { port, address }), + IpAddress::Ipv6(address) => IpSocketAddress::Ipv6(Ipv6SocketAddress { + port, + flow_info: 0, + address, + scope_id: 0, + }), + }) + .collect()) +} + +fn socket_error(error: ErrorCode) -> Error { + Error::Closed(Some(format!("{error:?}"))) +} + +fn lookup_error(host: &str, error: LookupErrorCode) -> Error { + Error::Closed(Some(format!("unable to resolve {host}: {error:?}"))) +} + +wit_bindgen::generate!({ + // `path` is relative to the manifest, `components/tcp/` + path: "../wit", + world: "tcp", + // the resp types shared by every component + with: { + "componentized:valkey/resp@0.3.0-dev": resp_codec::bindings::componentized::valkey::resp, + }, + generate_all, +}); + +export!(Component); diff --git a/components/tcp/tests/tcp.rs b/components/tcp/tests/tcp.rs new file mode 100644 index 0000000..bfd9356 --- /dev/null +++ b/components/tcp/tests/tcp.rs @@ -0,0 +1,260 @@ +//! Tests for the `tcp` component, composed with `connection` and `resp` as `test-tcp`, against a +//! server played by the test over TCP. + +use test_harness::{ + Client, Ctx, Error, Exports, Harness, Server, Value, bulk, command, hello_reply, resolve, +}; +use wasmtime::component::{Accessor, ResourceAny}; + +/// Connect to the server and answer HELLO, returning the server's side and the connection. +async fn establish( + accessor: &Accessor, + exports: &Exports, + server: &Server, + host: &str, +) -> wasmtime::Result<(Client, ResourceAny)> { + let connect = exports + .tcp() + .call_connect(accessor, host.to_string(), server.port(), None); + let serve = async { + let mut client = server.accept().await.expect("accept"); + assert_eq!(client.request().await, Some(command(&["HELLO", "3"]))); + client.reply(hello_reply()).await; + client + }; + let (connection, client) = tokio::join!(connect, serve); + let connection = connection?.map_err(|error| wasmtime::format_err!("{error:?}"))?; + Ok((client, connection)) +} + +#[tokio::test(flavor = "multi_thread")] +async fn connects_and_calls() -> wasmtime::Result<()> { + let server = Server::bind().await?; + let mut harness = Harness::new("test-tcp").await?; + harness + .run(async |accessor, exports| { + let (mut client, connection) = + establish(accessor, exports, &server, "127.0.0.1").await?; + let guest = exports.connection(); + let call = guest.call_call(accessor, connection, command(&["PING"])); + let serve = async { + assert_eq!(client.request().await, Some(command(&["PING"]))); + client.reply(Value::String("PONG".to_string())).await; + }; + let (result, ()) = tokio::join!(call, serve); + assert_eq!(result?, Ok(Value::String("PONG".to_string()))); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn resolves_hostnames() -> wasmtime::Result<()> { + let server = Server::bind().await?; + let mut harness = Harness::new("test-tcp").await?; + harness + .run(async |accessor, exports| { + // localhost may resolve to ::1 first, which nothing listens on, the next address is tried + establish(accessor, exports, &server, "localhost").await?; + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn replies_may_span_many_reads() -> wasmtime::Result<()> { + let server = Server::bind().await?; + let mut harness = Harness::new("test-tcp").await?; + harness + .run(async |accessor, exports| { + let (mut client, connection) = + establish(accessor, exports, &server, "127.0.0.1").await?; + let value: String = (0..200_000) + .map(|n| char::from(b'a' + (n % 26) as u8)) + .collect(); + let guest = exports.connection(); + let call = guest.call_call(accessor, connection, command(&["GET", "big"])); + let serve = async { + client.request().await; + let reply = test_harness::encode(bulk(&value)); + for chunk in reply.chunks(1000) { + client.write(chunk).await; + tokio::task::yield_now().await; + } + }; + let (result, ()) = tokio::join!(call, serve); + assert_eq!(result?, Ok(bulk(&value))); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn replies_may_share_a_read() -> wasmtime::Result<()> { + let server = Server::bind().await?; + let mut harness = Harness::new("test-tcp").await?; + harness + .run(async |accessor, exports| { + let (mut client, connection) = + establish(accessor, exports, &server, "127.0.0.1").await?; + let guest = exports.connection(); + let first = guest.call_call(accessor, connection, command(&["GET", "a"])); + let second = guest.call_call(accessor, connection, command(&["GET", "b"])); + let serve = async { + client.request().await; + client.request().await; + let mut replies = test_harness::encode(bulk("first")); + replies.extend(test_harness::encode(bulk("second"))); + client.write(&replies).await; + }; + let (first, second, ()) = tokio::join!(first, second, serve); + assert_eq!(first?, Ok(bulk("first"))); + assert_eq!(second?, Ok(bulk("second"))); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn server_closing_fails_calls() -> wasmtime::Result<()> { + let server = Server::bind().await?; + let mut harness = Harness::new("test-tcp").await?; + harness + .run(async |accessor, exports| { + let (client, connection) = establish(accessor, exports, &server, "127.0.0.1").await?; + client.close().await; + let result = exports + .connection() + .call_call(accessor, connection, command(&["PING"])) + .await?; + assert!( + matches!(result, Err(Error::Closed(_))), + "expected a closed error, got {result:?}" + ); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn refused_connections_fail_to_connect() -> wasmtime::Result<()> { + // a port nothing listens on + let port = Server::bind().await?.port(); + let mut harness = Harness::new("test-tcp").await?; + harness + .run(async |accessor, exports| { + let result = exports + .tcp() + .call_connect(accessor, "127.0.0.1".to_string(), port, None) + .await?; + let Err(Error::Closed(Some(reason))) = result else { + panic!("expected a closed error, got {result:?}"); + }; + assert!( + reason.starts_with("unable to connect to 127.0.0.1"), + "{reason}" + ); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn transports_wire_to_connections() -> wasmtime::Result<()> { + let server = Server::bind().await?; + let mut harness = Harness::new("test-tcp").await?; + harness + .run(async |accessor, exports| { + let transport = exports.tcp().transport(); + let open = transport.call_open(accessor, "127.0.0.1".to_string(), server.port()); + let (transport_handle, client) = + tokio::join!(open, async { server.accept().await.expect("accept") }); + let transport_handle = + transport_handle?.map_err(|error| wasmtime::format_err!("{error:?}"))?; + let mut client = client; + + let (replies, closed) = transport.call_receive(accessor, transport_handle).await?; + let (requests, connection) = exports + .connection() + .call_new(accessor, replies, closed, None) + .await?; + let sent = transport + .call_send(accessor, transport_handle, requests) + .await?; + drop(sent); + + let serve = async { + assert_eq!(client.request().await, Some(command(&["HELLO", "3"]))); + client.reply(hello_reply()).await; + }; + let (connection, ()) = tokio::join!(resolve(accessor, connection), serve); + assert!(connection?.is_ok()); + + // each stream is handed over once + let (_, again) = transport.call_receive(accessor, transport_handle).await?; + assert!(matches!( + resolve(accessor, again).await?, + Err(Error::Client(_)) + )); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn dropping_the_connection_closes_the_socket() -> wasmtime::Result<()> { + let server = Server::bind().await?; + let mut harness = Harness::new("test-tcp").await?; + let (mut client, connection) = harness + .run(async |accessor, exports| establish(accessor, exports, &server, "127.0.0.1").await) + .await?; + harness.drop_resource(connection).await?; + harness + .run(async |_accessor, _exports| { + assert_eq!(client.request().await, None); + Ok(()) + }) + .await +} + +/// The tests against a Valkey server on localhost:6379, ignored unless run with +/// `make test-valkey`, which only runs the ignored tests in `with_valkey` modules. +mod with_valkey { + use super::*; + + /// Against a Valkey server on localhost:6379, run with `cargo test -p tcp -- --ignored with_valkey::`. + #[tokio::test(flavor = "multi_thread")] + #[ignore = "needs a Valkey server on localhost:6379"] + async fn against_valkey() -> wasmtime::Result<()> { + let mut harness = Harness::new("test-tcp").await?; + harness + .run(async |accessor, exports| { + let connection = exports + .tcp() + .call_connect(accessor, "localhost".to_string(), 6379, None) + .await? + .map_err(|error| wasmtime::format_err!("{error:?}"))?; + let guest = exports.connection(); + let properties = guest.call_server_properties(accessor, connection).await?; + assert!(properties.contains(&("proto".to_string(), Value::Integer(3)))); + + let value: String = (0..100_000) + .map(|n| char::from(b'a' + (n % 26) as u8)) + .collect(); + let set = guest + .call_call(accessor, connection, command(&["SET", "tcp-test", &value])) + .await?; + assert_eq!(set, Ok(Value::String("OK".to_string()))); + let get = guest + .call_call(accessor, connection, command(&["GET", "tcp-test"])) + .await?; + assert_eq!(get, Ok(bulk(&value))); + let del = guest + .call_call(accessor, connection, command(&["DEL", "tcp-test"])) + .await?; + assert_eq!(del, Ok(Value::Integer(1))); + Ok(()) + }) + .await + } +} diff --git a/components/test-commands/README.md b/components/test-commands/README.md new file mode 100644 index 0000000..e7e0173 --- /dev/null +++ b/components/test-commands/README.md @@ -0,0 +1,3 @@ +# test-commands + +`commands` composed with `connection`, `resp` and `tcp`, to test the commands over a stub transport or against a server. diff --git a/components/test-commands/test-commands.wac b/components/test-commands/test-commands.wac new file mode 100644 index 0000000..5849eda --- /dev/null +++ b/components/test-commands/test-commands.wac @@ -0,0 +1,21 @@ +package componentized:valkey; + +let resp = new local:resp { ... }; +let connection = new local:connection { + "componentized:valkey/resp@0.3.0-dev": resp.resp, + ... +}; +let tcp = new local:tcp { + "componentized:valkey/resp@0.3.0-dev": resp.resp, + "componentized:valkey/connection@0.3.0-dev": connection.connection, + ... +}; +let commands = new local:commands { + "componentized:valkey/resp@0.3.0-dev": resp.resp, + "componentized:valkey/connection@0.3.0-dev": connection.connection, + ... +}; + +export connection.connection; +export tcp.tcp; +export commands...; diff --git a/components/test-tcp/README.md b/components/test-tcp/README.md new file mode 100644 index 0000000..fed2bc4 --- /dev/null +++ b/components/test-tcp/README.md @@ -0,0 +1,3 @@ +# test-tcp + +`tcp` composed with `connection` and `resp`, to test `tcp` against a server. diff --git a/components/test-tcp/test-tcp.wac b/components/test-tcp/test-tcp.wac new file mode 100644 index 0000000..bd2fa70 --- /dev/null +++ b/components/test-tcp/test-tcp.wac @@ -0,0 +1,15 @@ +package componentized:valkey; + +let resp = new local:resp { ... }; +let connection = new local:connection { + "componentized:valkey/resp@0.3.0-dev": resp.resp, + ... +}; +let tcp = new local:tcp { + "componentized:valkey/resp@0.3.0-dev": resp.resp, + "componentized:valkey/connection@0.3.0-dev": connection.connection, + ... +}; + +export connection.connection; +export tcp.tcp; diff --git a/components/wit/worlds.wit b/components/wit/worlds.wit index 895a998..31538b2 100644 --- a/components/wit/worlds.wit +++ b/components/wit/worlds.wit @@ -2,18 +2,86 @@ package componentized:valkey-components; world cli { include wasi:cli/imports@0.3.0; - import componentized:valkey/store@0.3.0-dev; + import componentized:valkey/tcp@0.3.0-dev; + import componentized:valkey/admin-commands@0.3.0-dev; + import componentized:valkey/connection-commands@0.3.0-dev; + import componentized:valkey/hash-commands@0.3.0-dev; + import componentized:valkey/keyspace-commands@0.3.0-dev; + import componentized:valkey/pubsub-commands@0.3.0-dev; + import componentized:valkey/string-commands@0.3.0-dev; export wasi:cli/run@0.3.0; } -world ops { - include wasi:sockets/imports@0.3.0; +/// The RESP codec, for the components that import the resp types. +world resp { export componentized:valkey/resp@0.3.0-dev; - export componentized:valkey/store@0.3.0-dev; +} + +/// The mechanics of a connection, independent of the transport. +world connection { + import wasi:clocks/monotonic-clock@0.3.0; + export componentized:valkey/connection@0.3.0-dev; +} + +/// Connections over wasi:sockets TCP. +world tcp { + import wasi:sockets/types@0.3.0; + import wasi:sockets/ip-name-lookup@0.3.0; + import componentized:valkey/connection@0.3.0-dev; + export componentized:valkey/tcp@0.3.0-dev; +} + +/// Commands, one world per ACL category. +world admin-commands { + import componentized:valkey/connection@0.3.0-dev; + export componentized:valkey/admin-commands@0.3.0-dev; +} + +world connection-commands { + import componentized:valkey/connection@0.3.0-dev; + export componentized:valkey/connection-commands@0.3.0-dev; +} + +world hash-commands { + import componentized:valkey/connection@0.3.0-dev; + export componentized:valkey/hash-commands@0.3.0-dev; +} + +world keyspace-commands { + import componentized:valkey/connection@0.3.0-dev; + export componentized:valkey/keyspace-commands@0.3.0-dev; +} + +world pubsub-commands { + import componentized:valkey/connection@0.3.0-dev; + export componentized:valkey/pubsub-commands@0.3.0-dev; +} + +world string-commands { + import componentized:valkey/connection@0.3.0-dev; + export componentized:valkey/string-commands@0.3.0-dev; +} + +/// Every implemented command interface, in one component. +world commands { + include admin-commands; + include connection-commands; + include hash-commands; + include keyspace-commands; + include pubsub-commands; + include string-commands; +} + +/// A connector for connections over TCP, to the server configured with wasi:config. +world tcp-connector { + import wasi:config/store@0.2.0-rc.1; + import componentized:valkey/tcp@0.3.0-dev; + export componentized:valkey/connector@0.3.0-dev; } world as-keyvalue { - import componentized:valkey/store@0.3.0-dev; + import componentized:valkey/connector@0.3.0-dev; + import componentized:valkey/hash-commands@0.3.0-dev; import wasi:config/store@0.2.0-rc.1; export wasmcloud:keyvalue/types@0.2.0; export wasmcloud:keyvalue/store@0.2.0; diff --git a/crates/commands/src/admin.rs b/crates/commands/src/admin.rs new file mode 100644 index 0000000..17550f2 --- /dev/null +++ b/crates/commands/src/admin.rs @@ -0,0 +1,39 @@ +//! `admin-commands`, Valkey's @admin ACL category, and the rest of the ACL family. + +use crate::componentized::valkey::connection::{Connection, Error}; +use crate::exports::componentized::valkey::admin_commands::Guest; +use crate::reply::{call, integer, ok, string}; + +impl Guest for crate::Commands { + async fn acl_deluser(connection: &Connection, username: String) -> Result<(), Error> { + // https://valkey.io/commands/acl-deluser/ + // ACL DELUSER username [ username ... ] + integer( + call( + connection, + ["ACL".to_string(), "DELUSER".to_string(), username], + ) + .await?, + )?; + Ok(()) + } + + async fn acl_genpass(connection: &Connection) -> Result { + // https://valkey.io/commands/acl-genpass/ + // ACL GENPASS [ bits ] + string(call(connection, ["ACL", "GENPASS"]).await?) + } + + async fn acl_setuser( + connection: &Connection, + username: String, + rules: Vec, + ) -> Result<(), Error> { + // https://valkey.io/commands/acl-setuser/ + // ACL SETUSER username [ rule ] [ [ rule ] ... ] + let command = ["ACL".to_string(), "SETUSER".to_string(), username] + .into_iter() + .chain(rules); + ok(call(connection, command).await?) + } +} diff --git a/crates/commands/src/connection.rs b/crates/commands/src/connection.rs new file mode 100644 index 0000000..db8ca36 --- /dev/null +++ b/crates/commands/src/connection.rs @@ -0,0 +1,48 @@ +//! `connection-commands`, Valkey's @connection ACL category. + +use crate::componentized::valkey::connection::{Connection, Error}; +use crate::exports::componentized::valkey::connection_commands::{Guest, HelloOpts}; +use crate::reply::{call, entries, ok, string, unexpected}; +use resp_codec::Value; + +impl Guest for crate::Commands { + async fn auth( + connection: &Connection, + username: String, + password: String, + ) -> Result<(), Error> { + // https://valkey.io/commands/auth/ + // AUTH [ username ] password + ok(call(connection, ["AUTH".to_string(), username, password]).await?) + } + + async fn hello( + connection: &Connection, + opts: Option, + ) -> Result, Error> { + // https://valkey.io/commands/hello/ + // HELLO [ protover [ AUTH username password ] [ SETNAME clientname ] ] + let mut command = vec!["HELLO".to_string(), "3".to_string()]; + if let Some(opts) = opts { + if let Some((username, password)) = opts.auth { + command.extend(["AUTH".to_string(), username, password]); + } + if let Some(client_name) = opts.client_name { + command.extend(["SETNAME".to_string(), client_name]); + } + } + entries(call(connection, command).await?)? + .into_iter() + .map(|(name, value)| Ok((string(name)?, value))) + .collect() + } + + async fn ping(connection: &Connection) -> Result<(), Error> { + // https://valkey.io/commands/ping/ + // PING [ message ] + match call(connection, ["PING"]).await? { + Value::String(reply) if reply == "PONG" => Ok(()), + reply => Err(unexpected(&reply)), + } + } +} diff --git a/crates/commands/src/hash.rs b/crates/commands/src/hash.rs new file mode 100644 index 0000000..c12c6bd --- /dev/null +++ b/crates/commands/src/hash.rs @@ -0,0 +1,332 @@ +//! `hash-commands`, Valkey's @hash ACL category. + +use crate::componentized::valkey::connection::{Connection, Error}; +use crate::exports::componentized::valkey::hash_commands::{ + Duration, ExpireMode, ExpireResult, Guest, HrandfieldOpts, HscanOpts, Instant, +}; +use crate::reply::{ + boolean, call, integer, items, ok, optional_string, optional_strings, string, string_entries, + strings, unexpected, unsigned, whole_seconds, +}; +use resp_codec::Value; + +impl Guest for crate::Commands { + async fn hdel(connection: &Connection, key: String, field: String) -> Result<(), Error> { + // https://valkey.io/commands/hdel/ + // HDEL key field [ field ... ] + integer(call(connection, ["HDEL".to_string(), key, field]).await?)?; + Ok(()) + } + + async fn hexists(connection: &Connection, key: String, field: String) -> Result { + // https://valkey.io/commands/hexists/ + // HEXISTS key field + boolean(call(connection, ["HEXISTS".to_string(), key, field]).await?) + } + + async fn hexpire( + connection: &Connection, + key: String, + seconds: Duration, + mode: Option, + fields: Vec, + ) -> Result, Error> { + // https://valkey.io/commands/hexpire/ + // HEXPIRE key seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] + let mut command = vec!["HEXPIRE".to_string(), key, whole_seconds(seconds)]; + command.extend(mode.map(expire_mode)); + command.extend(expire_fields(fields)); + expire_results(call(connection, command).await?) + } + + async fn hexpireat( + connection: &Connection, + key: String, + seconds: Instant, + mode: Option, + fields: Vec, + ) -> Result, Error> { + // https://valkey.io/commands/hexpireat/ + // HEXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] + let mut command = vec!["HEXPIREAT".to_string(), key, seconds.seconds.to_string()]; + command.extend(mode.map(expire_mode)); + command.extend(expire_fields(fields)); + expire_results(call(connection, command).await?) + } + + async fn hget( + connection: &Connection, + key: String, + field: String, + ) -> Result, Error> { + // https://valkey.io/commands/hget/ + // HGET key field + optional_string(call(connection, ["HGET".to_string(), key, field]).await?) + } + + async fn hgetall(connection: &Connection, key: String) -> Result, Error> { + // https://valkey.io/commands/hgetall/ + // HGETALL key + string_entries(call(connection, ["HGETALL".to_string(), key]).await?) + } + + async fn hincrby( + connection: &Connection, + key: String, + field: String, + increment: i64, + ) -> Result { + // https://valkey.io/commands/hincrby/ + // HINCRBY key field increment + integer( + call( + connection, + ["HINCRBY".to_string(), key, field, increment.to_string()], + ) + .await?, + ) + } + + async fn hincrbyfloat( + connection: &Connection, + key: String, + field: String, + increment: f64, + ) -> Result { + // https://valkey.io/commands/hincrbyfloat/ + // HINCRBYFLOAT key field increment + let command = [ + "HINCRBYFLOAT".to_string(), + key, + field, + increment.to_string(), + ]; + match call(connection, command).await? { + Value::Double(value) => Ok(value.to_string()), + reply => string(reply), + } + } + + async fn hkeys(connection: &Connection, key: String) -> Result, Error> { + // https://valkey.io/commands/hkeys/ + // HKEYS key + strings(call(connection, ["HKEYS".to_string(), key]).await?) + } + + async fn hlen(connection: &Connection, key: String) -> Result { + // https://valkey.io/commands/hlen/ + // HLEN key + unsigned(call(connection, ["HLEN".to_string(), key]).await?) + } + + async fn hmget( + connection: &Connection, + key: String, + fields: Vec, + ) -> Result>, Error> { + // https://valkey.io/commands/hmget/ + // HMGET key field [ field ... ] + let command = ["HMGET".to_string(), key].into_iter().chain(fields); + optional_strings(call(connection, command).await?) + } + + async fn hmset( + connection: &Connection, + key: String, + fields: Vec<(String, String)>, + ) -> Result<(), Error> { + // https://valkey.io/commands/hmset/ + // HMSET key field value [ field value ... ] + let command = ["HMSET".to_string(), key] + .into_iter() + .chain(fields.into_iter().flat_map(|(field, value)| [field, value])); + ok(call(connection, command).await?) + } + + async fn hrandfield( + connection: &Connection, + key: String, + opts: Option, + ) -> Result)>>, Error> { + // https://valkey.io/commands/hrandfield/ + // HRANDFIELD key [ count [ WITHVALUES ] ] + let (count, with_values) = match opts { + Some(opts) => (opts.count, opts.with_values.unwrap_or(false)), + None => (None, false), + }; + let mut command = vec!["HRANDFIELD".to_string(), key]; + match (count, with_values) { + (None, true) => { + return Err(Error::Client( + "count must be specified to use with-values".to_string(), + )); + } + (None, false) => {} + (Some(count), with_values) => { + command.push(count.to_string()); + if with_values { + command.push("WITHVALUES".to_string()); + } + } + } + + let reply = call(connection, command).await?; + if count.is_none() { + // a single field, or null when the hash does not exist + return Ok(optional_string(reply)?.map(|field| vec![(field, None)])); + } + let items = items(reply)?; + if items.is_empty() { + return Ok(None); + } + let fields = if with_values { + field_value_pairs(items)? + } else { + items + .into_iter() + .map(|field| Ok((string(field)?, None))) + .collect::>()? + }; + Ok(Some(fields)) + } + + async fn hscan( + connection: &Connection, + key: String, + cursor: Option, + opts: Option, + ) -> Result<(Option, Vec<(String, Option)>), Error> { + // https://valkey.io/commands/hscan/ + // HSCAN key cursor [ MATCH pattern ] [ COUNT count ] [ NOVALUES ] + let mut command = vec![ + "HSCAN".to_string(), + key, + cursor.unwrap_or_else(|| "0".to_string()), + ]; + let mut no_values = false; + if let Some(opts) = opts { + if let Some(pattern) = opts.match_ { + command.extend(["MATCH".to_string(), pattern]); + } + if let Some(count) = opts.count { + command.extend(["COUNT".to_string(), count.to_string()]); + } + if opts.no_values == Some(true) { + command.push("NOVALUES".to_string()); + no_values = true; + } + } + + let reply = call(connection, command).await?; + let mut parts = items(reply.clone())?.into_iter(); + let (Some(cursor), Some(elements), None) = (parts.next(), parts.next(), parts.next()) + else { + return Err(unexpected(&reply)); + }; + let cursor = string(cursor)?; + let elements = items(elements)?; + let fields = if no_values { + elements + .into_iter() + .map(|field| Ok((string(field)?, None))) + .collect::>()? + } else { + field_value_pairs(elements)? + }; + // a cursor of 0 ends the scan + let cursor = (cursor != "0").then_some(cursor); + Ok((cursor, fields)) + } + + async fn hset( + connection: &Connection, + key: String, + field: String, + value: String, + ) -> Result<(), Error> { + // https://valkey.io/commands/hset/ + // HSET key field value [ field value ... ] + integer(call(connection, ["HSET".to_string(), key, field, value]).await?)?; + Ok(()) + } + + async fn hsetnx( + connection: &Connection, + key: String, + field: String, + value: String, + ) -> Result { + // https://valkey.io/commands/hsetnx/ + // HSETNX key field value + boolean(call(connection, ["HSETNX".to_string(), key, field, value]).await?) + } + + async fn hstrlen(connection: &Connection, key: String, field: String) -> Result { + // https://valkey.io/commands/hstrlen/ + // HSTRLEN key field + unsigned(call(connection, ["HSTRLEN".to_string(), key, field]).await?) + } + + async fn hvals(connection: &Connection, key: String) -> Result, Error> { + // https://valkey.io/commands/hvals/ + // HVALS key + strings(call(connection, ["HVALS".to_string(), key]).await?) + } +} + +/// Fields and their values, as `[field, value]` pairs (RESP3) or a flat array (RESP2). +fn field_value_pairs(items: Vec) -> Result)>, Error> { + if matches!(items.first(), Some(Value::Array(_))) { + return items + .into_iter() + .map(|pair| { + let mut pair = crate::reply::items(pair)?.into_iter(); + match (pair.next(), pair.next(), pair.next()) { + (Some(field), Some(value), None) => Ok((string(field)?, Some(string(value)?))), + _ => Err(Error::Client("unexpected field and value pair".to_string())), + } + }) + .collect(); + } + if items.len() % 2 != 0 { + return Err(Error::Client(format!( + "unexpected odd number of fields and values: {}", + items.len() + ))); + } + let mut items = items.into_iter(); + let mut fields = vec![]; + while let (Some(field), Some(value)) = (items.next(), items.next()) { + fields.push((string(field)?, Some(string(value)?))); + } + Ok(fields) +} + +fn expire_mode(mode: ExpireMode) -> String { + match mode { + ExpireMode::Nx => "NX", + ExpireMode::Xx => "XX", + ExpireMode::Gt => "GT", + ExpireMode::Lt => "LT", + } + .to_string() +} + +fn expire_fields(fields: Vec) -> impl Iterator { + ["FIELDS".to_string(), fields.len().to_string()] + .into_iter() + .chain(fields) +} + +fn expire_results(reply: Value) -> Result, Error> { + items(reply)? + .into_iter() + .map(|code| match integer(code)? { + -2 => Ok(ExpireResult::DoesNotExist), + 0 => Ok(ExpireResult::ConditionNotMet), + 1 => Ok(ExpireResult::Applied), + 2 => Ok(ExpireResult::ZeroSeconds), + code => Err(Error::Client(format!("unexpected reply: {code}"))), + }) + .collect() +} diff --git a/crates/commands/src/keyspace.rs b/crates/commands/src/keyspace.rs new file mode 100644 index 0000000..1598d2e --- /dev/null +++ b/crates/commands/src/keyspace.rs @@ -0,0 +1,73 @@ +//! `keyspace-commands`, Valkey's @keyspace ACL category. + +use crate::componentized::valkey::connection::{Connection, Error}; +use crate::exports::componentized::valkey::keyspace_commands::{ + Duration, ExpireMode, ExpireResult, Guest, Instant, +}; +use crate::reply::{boolean, call, integer, strings, whole_seconds}; + +impl Guest for crate::Commands { + async fn del(connection: &Connection, key: String) -> Result<(), Error> { + // https://valkey.io/commands/del/ + // DEL key [ key ... ] + integer(call(connection, ["DEL".to_string(), key]).await?)?; + Ok(()) + } + + async fn exists(connection: &Connection, key: String) -> Result { + // https://valkey.io/commands/exists/ + // EXISTS key [ key ... ] + boolean(call(connection, ["EXISTS".to_string(), key]).await?) + } + + async fn expire( + connection: &Connection, + key: String, + seconds: Duration, + mode: Option, + ) -> Result { + // https://valkey.io/commands/expire/ + // EXPIRE key seconds [ NX | XX | GT | LT ] + let mut command = vec!["EXPIRE".to_string(), key, whole_seconds(seconds)]; + command.extend(mode.map(expire_mode)); + expire_result(integer(call(connection, command).await?)?) + } + + async fn expireat( + connection: &Connection, + key: String, + seconds: Instant, + mode: Option, + ) -> Result { + // https://valkey.io/commands/expireat/ + // EXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] + let mut command = vec!["EXPIREAT".to_string(), key, seconds.seconds.to_string()]; + command.extend(mode.map(expire_mode)); + expire_result(integer(call(connection, command).await?)?) + } + + async fn keys(connection: &Connection, pattern: String) -> Result, Error> { + // https://valkey.io/commands/keys/ + // KEYS pattern + strings(call(connection, ["KEYS".to_string(), pattern]).await?) + } +} + +fn expire_mode(mode: ExpireMode) -> String { + match mode { + ExpireMode::Nx => "NX", + ExpireMode::Xx => "XX", + ExpireMode::Gt => "GT", + ExpireMode::Lt => "LT", + } + .to_string() +} + +fn expire_result(code: i64) -> Result { + match code { + // EXPIRE replies 0 both for a key that does not exist and for a condition not met + 0 => Ok(ExpireResult::ConditionNotMet), + 1 => Ok(ExpireResult::Applied), + code => Err(Error::Client(format!("unexpected reply: {code}"))), + } +} diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs new file mode 100644 index 0000000..ebac0ac --- /dev/null +++ b/crates/commands/src/lib.rs @@ -0,0 +1,209 @@ +//! Valkey's commands, grouped by ACL category. +//! +//! These sources are shared by the command components. Each component compiles them with the +//! features for the interfaces it exports, and exports them on the connection it imports: +//! +//! - `admin`: exports `admin-commands` +//! - `connection`: exports `connection-commands` +//! - `hash`: exports `hash-commands` +//! - `keyspace`: exports `keyspace-commands` +//! - `pubsub`: exports `pubsub-commands` +//! - `string`: exports `string-commands` +//! +//! A component compiles one feature, e.g. `hash-commands`, or every feature, `commands`. + +// the test harness provides a main +#![cfg_attr(not(test), no_main)] + +// each component uses the conversions its commands need +#[allow(dead_code)] +mod reply; + +#[cfg(feature = "admin")] +mod admin; +#[cfg(feature = "connection")] +mod connection; +#[cfg(feature = "hash")] +mod hash; +#[cfg(feature = "keyspace")] +mod keyspace; +#[cfg(feature = "pubsub")] +mod pubsub; +#[cfg(feature = "string")] +mod string; + +struct Commands; + +/// Generate the bindings for the world of the component compiling these sources. +macro_rules! generate { + ($world:literal) => { + wit_bindgen::generate!({ + // `path` is relative to the manifest of the component compiling these sources, + // `components//` + path: "../wit", + world: $world, + // the resp types shared by every component + with: { + "componentized:valkey/resp@0.3.0-dev": resp_codec::bindings::componentized::valkey::resp, + }, + generate_all, + }); + }; +} + +#[cfg(all( + feature = "admin", + feature = "connection", + feature = "hash", + feature = "keyspace", + feature = "pubsub", + feature = "string" +))] +generate!("commands"); + +#[cfg(all( + feature = "admin", + not(any( + feature = "connection", + feature = "hash", + feature = "keyspace", + feature = "pubsub", + feature = "string" + )) +))] +generate!("admin-commands"); + +#[cfg(all( + feature = "connection", + not(any( + feature = "admin", + feature = "hash", + feature = "keyspace", + feature = "pubsub", + feature = "string" + )) +))] +generate!("connection-commands"); + +#[cfg(all( + feature = "hash", + not(any( + feature = "admin", + feature = "connection", + feature = "keyspace", + feature = "pubsub", + feature = "string" + )) +))] +generate!("hash-commands"); + +#[cfg(all( + feature = "keyspace", + not(any( + feature = "admin", + feature = "connection", + feature = "hash", + feature = "pubsub", + feature = "string" + )) +))] +generate!("keyspace-commands"); + +#[cfg(all( + feature = "pubsub", + not(any( + feature = "admin", + feature = "connection", + feature = "hash", + feature = "keyspace", + feature = "string" + )) +))] +generate!("pubsub-commands"); + +#[cfg(all( + feature = "string", + not(any( + feature = "admin", + feature = "connection", + feature = "hash", + feature = "keyspace", + feature = "pubsub" + )) +))] +generate!("string-commands"); + +// a feature count other than one or all six has no world +#[cfg(not(any( + all( + feature = "admin", + feature = "connection", + feature = "hash", + feature = "keyspace", + feature = "pubsub", + feature = "string" + ), + all( + feature = "admin", + not(any( + feature = "connection", + feature = "hash", + feature = "keyspace", + feature = "pubsub", + feature = "string" + )) + ), + all( + feature = "connection", + not(any( + feature = "admin", + feature = "hash", + feature = "keyspace", + feature = "pubsub", + feature = "string" + )) + ), + all( + feature = "hash", + not(any( + feature = "admin", + feature = "connection", + feature = "keyspace", + feature = "pubsub", + feature = "string" + )) + ), + all( + feature = "keyspace", + not(any( + feature = "admin", + feature = "connection", + feature = "hash", + feature = "pubsub", + feature = "string" + )) + ), + all( + feature = "pubsub", + not(any( + feature = "admin", + feature = "connection", + feature = "hash", + feature = "keyspace", + feature = "string" + )) + ), + all( + feature = "string", + not(any( + feature = "admin", + feature = "connection", + feature = "hash", + feature = "keyspace", + feature = "pubsub" + )) + ), +)))] +compile_error!("enable one command feature, or all of them"); + +export!(Commands); diff --git a/crates/commands/src/pubsub.rs b/crates/commands/src/pubsub.rs new file mode 100644 index 0000000..54d20b1 --- /dev/null +++ b/crates/commands/src/pubsub.rs @@ -0,0 +1,17 @@ +//! `pubsub-commands`, Valkey's @pubsub ACL category. + +use crate::componentized::valkey::connection::{Connection, Error}; +use crate::exports::componentized::valkey::pubsub_commands::Guest; +use crate::reply::{call, integer}; + +impl Guest for crate::Commands { + async fn publish( + connection: &Connection, + channel: String, + message: String, + ) -> Result { + // https://valkey.io/commands/publish/ + // PUBLISH channel message + integer(call(connection, ["PUBLISH".to_string(), channel, message]).await?) + } +} diff --git a/crates/commands/src/reply.rs b/crates/commands/src/reply.rs new file mode 100644 index 0000000..6145aef --- /dev/null +++ b/crates/commands/src/reply.rs @@ -0,0 +1,132 @@ +//! Sending commands, and converting their replies. +//! +//! Connections speak RESP3. Where RESP3 replies with a richer type than RESP2, e.g. a map rather +//! than a flat array of keys and values, both are accepted. + +use resp_codec::{NestedValue, Value}; + +use crate::componentized::valkey::connection::{Connection, Error}; + +/// Send a command, its words as bulk strings, and wait for the reply. +/// +/// An error reply fails the command, see `connection.call`. +pub async fn call(connection: &Connection, words: I) -> Result +where + I: IntoIterator, + W: Into, +{ + let command = words + .into_iter() + .map(|word| Value::BulkString(word.into())) + .collect(); + connection.call(command).await +} + +pub fn unexpected(reply: &Value) -> Error { + Error::Client(format!("unexpected reply: {reply:?}")) +} + +pub fn decode(value: NestedValue) -> Result { + resp_codec::decode(value).map_err(Error::Resp) +} + +/// A simple string `OK`. +pub fn ok(reply: Value) -> Result<(), Error> { + match &reply { + Value::String(value) if value == "OK" => Ok(()), + _ => Err(unexpected(&reply)), + } +} + +pub fn integer(reply: Value) -> Result { + match reply { + Value::Integer(value) => Ok(value), + reply => Err(unexpected(&reply)), + } +} + +pub fn unsigned(reply: Value) -> Result { + let value = integer(reply)?; + u64::try_from(value).map_err(|_| Error::Client(format!("unexpected negative reply: {value}"))) +} + +/// An integer `0` or `1`. +pub fn boolean(reply: Value) -> Result { + match reply { + Value::Integer(0) => Ok(false), + Value::Integer(1) => Ok(true), + Value::Boolean(value) => Ok(value), + reply => Err(unexpected(&reply)), + } +} + +/// A bulk or simple string. +pub fn string(reply: Value) -> Result { + match reply { + Value::BulkString(value) | Value::String(value) => Ok(value), + Value::VerbatimString((_, value)) => Ok(value), + reply => Err(unexpected(&reply)), + } +} + +/// A string, or null. +pub fn optional_string(reply: Value) -> Result, Error> { + match reply { + Value::Null => Ok(None), + reply => string(reply).map(Some), + } +} + +/// The nested values of an array, or a set. +pub fn items(reply: Value) -> Result, Error> { + match reply { + Value::Array(items) | Value::Set(items) => items.into_iter().map(decode).collect(), + reply => Err(unexpected(&reply)), + } +} + +pub fn strings(reply: Value) -> Result, Error> { + items(reply)?.into_iter().map(string).collect() +} + +pub fn optional_strings(reply: Value) -> Result>, Error> { + items(reply)?.into_iter().map(optional_string).collect() +} + +/// The entries of a map, or of a flat array of keys and values. +pub fn entries(reply: Value) -> Result, Error> { + match reply { + Value::Map(entries) => entries + .into_iter() + .map(|(key, value)| Ok((decode(key)?, decode(value)?))) + .collect(), + Value::Array(items) => { + if items.len() % 2 != 0 { + return Err(Error::Client(format!( + "unexpected odd number of keys and values: {}", + items.len() + ))); + } + let mut items = items.into_iter(); + let mut entries = vec![]; + while let (Some(key), Some(value)) = (items.next(), items.next()) { + entries.push((decode(key)?, decode(value)?)); + } + Ok(entries) + } + reply => Err(unexpected(&reply)), + } +} + +/// The entries of a map of strings to strings. +pub fn string_entries(reply: Value) -> Result, Error> { + entries(reply)? + .into_iter() + .map(|(key, value)| Ok((string(key)?, string(value)?))) + .collect() +} + +/// A duration, in nanoseconds, as whole seconds, the precision of EXPIRE and HEXPIRE. +pub fn whole_seconds(nanoseconds: u64) -> String { + (nanoseconds / 1_000_000_000).to_string() +} diff --git a/crates/commands/src/string.rs b/crates/commands/src/string.rs new file mode 100644 index 0000000..5c891fd --- /dev/null +++ b/crates/commands/src/string.rs @@ -0,0 +1,37 @@ +//! `string-commands`, Valkey's @string ACL category. + +use crate::componentized::valkey::connection::{Connection, Error}; +use crate::exports::componentized::valkey::string_commands::Guest; +use crate::reply::{call, integer, ok, optional_string}; + +impl Guest for crate::Commands { + async fn get(connection: &Connection, key: String) -> Result, Error> { + // https://valkey.io/commands/get/ + // GET key + optional_string(call(connection, ["GET".to_string(), key]).await?) + } + + async fn incr(connection: &Connection, key: String) -> Result { + // https://valkey.io/commands/incr/ + // INCR key + integer(call(connection, ["INCR".to_string(), key]).await?) + } + + async fn incrby(connection: &Connection, key: String, increment: i64) -> Result { + // https://valkey.io/commands/incrby/ + // INCRBY key increment + integer( + call( + connection, + ["INCRBY".to_string(), key, increment.to_string()], + ) + .await?, + ) + } + + async fn set(connection: &Connection, key: String, value: String) -> Result<(), Error> { + // https://valkey.io/commands/set/ + // SET key value + ok(call(connection, ["SET".to_string(), key, value]).await?) + } +} diff --git a/crates/resp-codec/Cargo.toml b/crates/resp-codec/Cargo.toml new file mode 100644 index 0000000..3d120e6 --- /dev/null +++ b/crates/resp-codec/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "resp-codec" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" +publish = false + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/ops/src/resp/LICENSE-APACHE.txt b/crates/resp-codec/LICENSE-APACHE.txt similarity index 100% rename from components/ops/src/resp/LICENSE-APACHE.txt rename to crates/resp-codec/LICENSE-APACHE.txt diff --git a/components/ops/src/resp/LICENSE-MIT.txt b/crates/resp-codec/LICENSE-MIT.txt similarity index 100% rename from components/ops/src/resp/LICENSE-MIT.txt rename to crates/resp-codec/LICENSE-MIT.txt diff --git a/components/ops/src/resp/LICENSE.txt b/crates/resp-codec/LICENSE.txt similarity index 100% rename from components/ops/src/resp/LICENSE.txt rename to crates/resp-codec/LICENSE.txt diff --git a/components/ops/src/resp/README.md b/crates/resp-codec/README.md similarity index 100% rename from components/ops/src/resp/README.md rename to crates/resp-codec/README.md diff --git a/crates/resp-codec/src/codec.rs b/crates/resp-codec/src/codec.rs new file mode 100644 index 0000000..d1ae9f7 --- /dev/null +++ b/crates/resp-codec/src/codec.rs @@ -0,0 +1,424 @@ +//! RESP serialization, from . + +use crate::{Error, NestedValue, Value}; + +/// up to 512 MB in length +const RESP_MAX_SIZE: i64 = 512 * 1024 * 1024; +const CRLF_BYTES: &'static [u8] = b"\r\n"; + +/// Encodes RESP value to RESP binary buffer. +/// # Examples +/// ``` +/// # use resp_codec::{Value, encode}; +/// let val = Value::String("OK".to_string()); +/// assert_eq!(encode(val), b"+OK\r\n"); +/// ``` +pub fn encode(value: Value) -> Vec { + let mut res: Vec = Vec::new(); + buf_encode(value, &mut res); + res +} + +#[inline] +fn buf_encode(value: Value, buf: &mut Vec) { + match value { + Value::Null => { + buf.push(b'_'); + buf.extend_from_slice(CRLF_BYTES); + } + Value::String(val) => { + buf.push(b'+'); + buf.extend_from_slice(val.as_bytes()); + buf.extend_from_slice(CRLF_BYTES); + } + Value::Error(val) => { + buf.push(b'-'); + buf.extend_from_slice(val.as_bytes()); + buf.extend_from_slice(CRLF_BYTES); + } + Value::Integer(val) => { + buf.push(b':'); + buf.extend_from_slice(val.to_string().as_bytes()); + buf.extend_from_slice(CRLF_BYTES); + } + Value::BulkString(val) => { + buf.push(b'$'); + buf.extend_from_slice(val.len().to_string().as_bytes()); + buf.extend_from_slice(CRLF_BYTES); + buf.extend_from_slice(val.as_bytes()); + buf.extend_from_slice(CRLF_BYTES); + } + Value::Array(val) => { + buf.push(b'*'); + buf.extend_from_slice(val.len().to_string().as_bytes()); + buf.extend_from_slice(CRLF_BYTES); + for item in val { + buf.extend_from_slice(&item); + } + } + Value::Boolean(ref val) => { + buf.push(b'#'); + match val { + true => buf.push(b't'), + false => buf.push(b'f'), + } + buf.extend_from_slice(CRLF_BYTES); + } + Value::Double(ref val) => { + buf.push(b','); + buf.extend_from_slice(val.to_string().to_lowercase().as_bytes()); + buf.extend_from_slice(CRLF_BYTES); + } + Value::BigNumber(ref val) => { + buf.push(b'('); + buf.extend_from_slice(val.as_bytes()); + buf.extend_from_slice(CRLF_BYTES); + } + Value::BulkError(ref val) => { + buf.push(b'!'); + buf.extend_from_slice(val.len().to_string().as_bytes()); + buf.extend_from_slice(CRLF_BYTES); + buf.extend_from_slice(val.as_bytes()); + buf.extend_from_slice(CRLF_BYTES); + } + Value::VerbatimString((ref encoding, ref value)) => { + let val = format!("{encoding}:{value}"); + buf.push(b'='); + buf.extend_from_slice(val.len().to_string().as_bytes()); + buf.extend_from_slice(CRLF_BYTES); + buf.extend_from_slice(val.as_bytes()); + buf.extend_from_slice(CRLF_BYTES); + } + Value::Map(val) => { + buf.push(b'%'); + buf.extend_from_slice(val.len().to_string().as_bytes()); + buf.extend_from_slice(CRLF_BYTES); + for (key, value) in val { + buf.extend_from_slice(&key); + buf.extend_from_slice(&value); + } + } + Value::Set(val) => { + buf.push(b'~'); + buf.extend_from_slice(val.len().to_string().as_bytes()); + buf.extend_from_slice(CRLF_BYTES); + for item in val { + buf.extend_from_slice(&item); + } + } + Value::Push(val) => { + buf.push(b'>'); + buf.extend_from_slice(val.len().to_string().as_bytes()); + buf.extend_from_slice(CRLF_BYTES); + for item in val { + buf.extend_from_slice(&item); + } + } + } +} + +/// Decodes the RESP value `value` holds, which must be exactly one whole value. +/// +/// The elements of an aggregate, e.g. an array, are kept as the RESP they were received as, they +/// are only checked to be whole values. +/// +/// # Examples +/// ``` +/// # use resp_codec::{Value, decode, encode}; +/// let value = Value::BulkString("Hello".to_string()); +/// assert_eq!(decode(encode(value.clone())), Ok(value)); +/// ``` +pub fn decode(value: Vec) -> Result { + match frame_end(&value, 0)? { + Some(end) if end == value.len() => decode_frame(&value), + Some(end) => Err(format!( + "unexpected bytes after the RESP value: {}", + value.len() - end + )), + None => Err("incomplete RESP value".to_string()), + } +} + +/// The length of the first complete RESP value in `buf`, or `None` when the value is incomplete +/// and more bytes are needed. Only the framing is checked, the value is not decoded. +pub fn frame_len(buf: &[u8]) -> Result, Error> { + frame_end(buf, 0) +} + +/// The end of the RESP value starting at `start`, or `None` when it is incomplete. +fn frame_end(buf: &[u8], start: usize) -> Result, Error> { + let Some(prefix) = buf.get(start) else { + return Ok(None); + }; + // the header line, after the type prefix up to the CRLF + let Some(crlf) = buf[start..].windows(2).position(|w| w == CRLF_BYTES) else { + return Ok(None); + }; + let header = &buf[start + 1..start + crlf]; + let end = start + crlf + 2; + + let length = |min: i64| -> Result { + let int = parse_integer(header)?; + if int < min || int >= RESP_MAX_SIZE { + Err(format!("invalid length: {}", int))? + } + Ok(int) + }; + let children = |end: usize, count: i64| -> Result, Error> { + let mut end = end; + for _ in 0..count { + match frame_end(buf, end)? { + Some(child_end) => end = child_end, + None => return Ok(None), + } + } + Ok(Some(end)) + }; + + match prefix { + // simple values end with their header line + b'+' | b'-' | b':' | b'_' | b'#' | b',' | b'(' => Ok(Some(end)), + // blob values are followed by their length in bytes and a CRLF, a bulk string may be + // null with a length of -1 in RESP2 + b'$' | b'!' | b'=' => { + let int = length(if *prefix == b'$' { -1 } else { 0 })?; + if int == -1 { + return Ok(Some(end)); + } + let end = end + int as usize + 2; + match buf.get(end - 2..end) { + Some(CRLF_BYTES) => Ok(Some(end)), + Some(terminator) => Err(format!("invalid CRLF after blob: {terminator:?}")), + None => Ok(None), + } + } + // aggregates are followed by their elements, an array may be null with a length of -1 in + // RESP2 + b'*' | b'~' | b'>' => { + let int = length(if *prefix == b'*' { -1 } else { 0 })?; + if int == -1 { + return Ok(Some(end)); + } + children(end, int) + } + // maps are followed by a key and a value for each entry + b'%' => children(end, length(0)? * 2), + prefix => Err(format!("invalid RESP type: {:?}", prefix)), + } +} + +/// Decodes the whole RESP value at the start of `buf`, its framing already checked by `frame_end`. +fn decode_frame(buf: &[u8]) -> Result { + let crlf = buf + .windows(2) + .position(|w| w == CRLF_BYTES) + .ok_or("missing CRLF")?; + let header = &buf[1..crlf]; + let body = crlf + 2; + + // the bytes of a blob value, after its length line + let blob = || -> Result<&[u8], Error> { + let len = parse_integer(header)? as usize; + Ok(&buf[body..body + len]) + }; + // the elements of an aggregate value, each kept as its RESP + let elements = |count: i64| -> Result, Error> { + let mut start = body; + let mut elements = Vec::with_capacity(count as usize); + for _ in 0..count { + let end = frame_end(buf, start)?.ok_or("incomplete RESP value")?; + elements.push(buf[start..end].to_vec()); + start = end; + } + Ok(elements) + }; + + match buf[0] { + b'+' => parse_string(header).map(Value::String), + b'-' => parse_string(header).map(Value::Error), + b':' => parse_integer(header).map(Value::Integer), + b'_' => Ok(Value::Null), + b'#' => match header { + b"t" => Ok(Value::Boolean(true)), + b"f" => Ok(Value::Boolean(false)), + _ => Err(format!("invalid RESP boolean: {header:?}")), + }, + b',' => parse_double(header).map(Value::Double), + b'(' => parse_string(header).map(Value::BigNumber), + b'$' => match parse_integer(header)? { + // a null bulk string, RESP2's null + -1 => Ok(Value::Null), + _ => parse_string(blob()?).map(Value::BulkString), + }, + b'!' => parse_string(blob()?).map(Value::BulkError), + b'=' => { + let value = parse_string(blob()?)?; + let Some((encoding, value)) = value.split_once(':') else { + return Err("invalid verbatim string, missing encoding".to_string()); + }; + Ok(Value::VerbatimString(( + encoding.to_string(), + value.to_string(), + ))) + } + b'*' => match parse_integer(header)? { + // a null array, RESP2's null + -1 => Ok(Value::Null), + count => elements(count).map(Value::Array), + }, + b'~' => elements(parse_integer(header)?).map(Value::Set), + b'>' => elements(parse_integer(header)?).map(Value::Push), + b'%' => { + let mut entries = elements(parse_integer(header)? * 2)?.into_iter(); + let mut map = vec![]; + while let (Some(key), Some(value)) = (entries.next(), entries.next()) { + map.push((key, value)); + } + Ok(Value::Map(map)) + } + prefix => Err(format!("invalid RESP type: {prefix:?}")), + } +} + +#[inline] +fn parse_string(bytes: &[u8]) -> Result { + String::from_utf8(bytes.to_vec()).map_err(|err| err.to_string()) +} + +#[inline] +fn parse_integer(bytes: &[u8]) -> Result { + let str_integer = parse_string(bytes)?; + (str_integer.parse::()).map_err(|err| err.to_string()) +} + +#[inline] +fn parse_double(bytes: &[u8]) -> Result { + let str_double = parse_string(bytes)?; + (str_double.parse::()).map_err(|err| err.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn frame_len_complete_values() { + let cases: &[&[u8]] = &[ + b"+OK\r\n", + b"-ERR oops\r\n", + b":42\r\n", + b"_\r\n", + b"#t\r\n", + b",1.5\r\n", + b"(123\r\n", + b"$5\r\nhello\r\n", + b"$0\r\n\r\n", + b"$-1\r\n", + b"!3\r\nerr\r\n", + b"=7\r\ntxt:abc\r\n", + b"*0\r\n", + b"*-1\r\n", + b"*2\r\n$1\r\na\r\n:1\r\n", + b"%1\r\n+key\r\n*1\r\n$2\r\n\r\n\r\n", + b"~2\r\n+a\r\n+b\r\n", + b">1\r\n+msg\r\n", + ]; + for value in cases { + assert_eq!(frame_len(value).unwrap(), Some(value.len()), "{value:?}"); + } + } + + #[test] + fn frame_len_incomplete_values() { + let value: &[u8] = b"*3\r\n$5\r\nhello\r\n%1\r\n+k\r\n$3\r\nval\r\n:7\r\n"; + for len in 0..value.len() { + assert_eq!(frame_len(&value[..len]).unwrap(), None, "{len}"); + } + assert_eq!(frame_len(value).unwrap(), Some(value.len())); + } + + #[test] + fn frame_len_ignores_following_bytes() { + assert_eq!(frame_len(b"+OK\r\n:1\r\n").unwrap(), Some(5)); + // a bulk string's content may contain CRLF + assert_eq!(frame_len(b"$4\r\n\r\n\r\n\r\n+next\r\n").unwrap(), Some(10)); + } + + #[test] + fn frame_len_invalid_values() { + assert!(frame_len(b"?nope\r\n").is_err()); + assert!(frame_len(b"$-2\r\n").is_err()); + assert!(frame_len(b"%-1\r\n").is_err()); + assert!(frame_len(b"*x\r\n").is_err()); + } + + #[test] + fn decode_round_trips() { + let values = vec![ + Value::String("OK".to_string()), + Value::Error("ERR oops".to_string()), + Value::Integer(-42), + Value::BulkString("a\r\nb".to_string()), + Value::Null, + Value::Boolean(false), + Value::Double(1.5), + Value::BigNumber("12345678901234567890".to_string()), + Value::BulkError("SYNTAX invalid".to_string()), + Value::VerbatimString(("txt".to_string(), "a:b".to_string())), + Value::Array(vec![encode(Value::Integer(1)), encode(Value::Null)]), + Value::Set(vec![encode(Value::String("a".to_string()))]), + Value::Push(vec![encode(Value::String("message".to_string()))]), + Value::Map(vec![( + encode(Value::String("key".to_string())), + encode(Value::Array(vec![encode(Value::Integer(1))])), + )]), + ]; + for value in values { + assert_eq!(decode(encode(value.clone())), Ok(value)); + } + } + + #[test] + fn decode_keeps_elements_as_received() { + // a nested array, kept as its RESP rather than decoded and encoded again + let decoded = decode(b"*2\r\n*1\r\n:1\r\n$1\r\na\r\n".to_vec()); + assert_eq!( + decoded, + Ok(Value::Array(vec![ + b"*1\r\n:1\r\n".to_vec(), + b"$1\r\na\r\n".to_vec() + ])) + ); + } + + #[test] + fn decode_resp2_nulls() { + assert_eq!(decode(b"$-1\r\n".to_vec()), Ok(Value::Null)); + assert_eq!(decode(b"*-1\r\n".to_vec()), Ok(Value::Null)); + } + + #[test] + fn decode_needs_exactly_one_value() { + assert!(decode(b"+OK\r\n:1\r\n".to_vec()).is_err()); + assert!(decode(b"*2\r\n:1\r\n".to_vec()).is_err()); + assert!(decode(vec![]).is_err()); + } + + #[test] + fn decode_invalid_values() { + assert!(decode(b"$3\r\nabcde\r\n".to_vec()).is_err()); + assert!(decode(b"#x\r\n".to_vec()).is_err()); + assert!(decode(b"=3\r\ntxt\r\n".to_vec()).is_err()); + assert!(decode(b":one\r\n".to_vec()).is_err()); + } + + #[test] + fn frame_len_matches_encode() { + let value = Value::Array(vec![ + encode(Value::BulkString("a\r\nb".to_string())), + encode(Value::Integer(-3)), + ]); + let encoded = encode(value); + assert_eq!(frame_len(&encoded).unwrap(), Some(encoded.len())); + } +} diff --git a/crates/resp-codec/src/lib.rs b/crates/resp-codec/src/lib.rs new file mode 100644 index 0000000..13080de --- /dev/null +++ b/crates/resp-codec/src/lib.rs @@ -0,0 +1,35 @@ +//! The RESP codec, and the `componentized:valkey/resp` types it works with. +//! +//! A component that imports `resp` uses these types for it, so every component shares them, e.g. +//! +//! ```text +//! wit_bindgen::generate!({ +//! with: { "componentized:valkey/resp@0.3.0-dev": resp_codec::bindings::componentized::valkey::resp }, +//! ... +//! }); +//! ``` +//! +//! The `resp` component exports the interface, converting between its own generated types and +//! these. + +pub mod bindings { + wit_bindgen::generate!({ + // `path` is relative to the manifest, `crates/resp-codec/` + path: "../../wit", + inline: " + package componentized:resp-codec; + + world types { + import componentized:valkey/resp@0.3.0-dev; + } + ", + world: "componentized:resp-codec/types", + additional_derives: [PartialEq], + generate_all, + }); +} + +mod codec; + +pub use bindings::componentized::valkey::resp::{Error, NestedValue, Value}; +pub use codec::{decode, encode, frame_len}; diff --git a/crates/test-harness/Cargo.toml b/crates/test-harness/Cargo.toml new file mode 100644 index 0000000..b25c739 --- /dev/null +++ b/crates/test-harness/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test-harness" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" +publish = false + +[dependencies] +resp-codec = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } +wasmtime-wasi = { workspace = true } diff --git a/crates/test-harness/src/lib.rs b/crates/test-harness/src/lib.rs new file mode 100644 index 0000000..fe4184e --- /dev/null +++ b/crates/test-harness/src/lib.rs @@ -0,0 +1,660 @@ +//! Test harness for the valkey components. +//! +//! A [`Harness`] instantiates a component (`target/components/*/*.wasm`) with wasmtime. No Valkey +//! server is needed, the test plays the server: +//! +//! - over a stub [`Transport`], reading the commands a connection writes, and deciding when each +//! reply, push message or close arrives +//! - over TCP with a [`Server`], for components that open sockets, e.g. `tcp` + +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::process::Command; +use std::sync::Mutex; +use std::task::{Context, Poll}; + +use tokio::sync::{mpsc, oneshot}; +use wasmtime::component::{ + Accessor, Component, Destination, FutureConsumer, FutureReader, HasSelf, Lift, Linker, + ResourceAny, ResourceTable, Source, StreamConsumer, StreamProducer, StreamReader, StreamResult, +}; +use wasmtime::error::Context as _; +use wasmtime::{Config, Engine, Result, Store, StoreContextMut, bail, format_err}; +use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; + +pub use bindings::componentized::valkey::resp::{NestedValue, Value}; +pub use bindings::exports::componentized::valkey::connection::{ConnectOpts, Error}; + +mod server; + +pub use server::{Client, Server}; + +pub mod bindings { + wasmtime::component::bindgen!({ + path: "../../components/wit", + inline: " + package componentized:test-harness; + + world harness { + import componentized:valkey/resp@0.3.0-dev; + import wasi:config/store@0.2.0-rc.1; + export componentized:valkey/connection@0.3.0-dev; + export componentized:valkey/tcp@0.3.0-dev; + export componentized:valkey/admin-commands@0.3.0-dev; + export componentized:valkey/connection-commands@0.3.0-dev; + export componentized:valkey/hash-commands@0.3.0-dev; + export componentized:valkey/keyspace-commands@0.3.0-dev; + export componentized:valkey/pubsub-commands@0.3.0-dev; + export componentized:valkey/string-commands@0.3.0-dev; + export wasmcloud:keyvalue/types@0.2.0; + export wasmcloud:keyvalue/store@0.2.0; + export wasmcloud:keyvalue/atomics@0.2.0; + export wasmcloud:keyvalue/batch@0.2.0; + } + ", + world: "componentized:test-harness/harness", + exports: { default: async | store }, + additional_derives: [PartialEq], + with: { + "wasi:clocks": wasmtime_wasi::p3::bindings::clocks, + }, + }); +} + +/// Bindings for the `resp` component, which exports the interface the other components import. +pub mod resp_bindings { + wasmtime::component::bindgen!({ + path: "../../components/wit", + inline: " + package componentized:test-harness-resp; + + world resp { + export componentized:valkey/resp@0.3.0-dev; + } + ", + world: "componentized:test-harness-resp/resp", + exports: { default: async | store }, + additional_derives: [PartialEq], + }); +} + +pub type Resp = resp_bindings::exports::componentized::valkey::resp::Guest; + +/// Encode a value as RESP, e.g. to nest it in an aggregate. +pub fn encode(value: Value) -> NestedValue { + resp_codec::encode(to_codec(value)) +} + +/// Decode a nested value. +pub fn decode(value: NestedValue) -> Result { + resp_codec::decode(value).map(from_codec) +} + +// a value is not recursive, nested values are encoded RESP, so each conversion is one match +fn to_codec(value: Value) -> resp_codec::Value { + use resp_codec::Value as V; + match value { + Value::String(value) => V::String(value), + Value::Error(value) => V::Error(value), + Value::Integer(value) => V::Integer(value), + Value::BulkString(value) => V::BulkString(value), + Value::Array(value) => V::Array(value), + Value::Null => V::Null, + Value::Boolean(value) => V::Boolean(value), + Value::Double(value) => V::Double(value), + Value::BigNumber(value) => V::BigNumber(value), + Value::BulkError(value) => V::BulkError(value), + Value::VerbatimString(value) => V::VerbatimString(value), + Value::Map(value) => V::Map(value), + Value::Set(value) => V::Set(value), + Value::Push(value) => V::Push(value), + } +} + +fn from_codec(value: resp_codec::Value) -> Value { + use resp_codec::Value as V; + match value { + V::String(value) => Value::String(value), + V::Error(value) => Value::Error(value), + V::Integer(value) => Value::Integer(value), + V::BulkString(value) => Value::BulkString(value), + V::Array(value) => Value::Array(value), + V::Null => Value::Null, + V::Boolean(value) => Value::Boolean(value), + V::Double(value) => Value::Double(value), + V::BigNumber(value) => Value::BigNumber(value), + V::BulkError(value) => Value::BulkError(value), + V::VerbatimString(value) => Value::VerbatimString(value), + V::Map(value) => Value::Map(value), + V::Set(value) => Value::Set(value), + V::Push(value) => Value::Push(value), + } +} + +pub type Connection<'a> = bindings::exports::componentized::valkey::connection::GuestConnection<'a>; +pub type Tcp = bindings::exports::componentized::valkey::tcp::Guest; +pub use bindings::exports::wasmcloud::keyvalue; +pub use bindings::exports::componentized::valkey::{ + admin_commands, + connection_commands, + hash_commands, + keyspace_commands, + pubsub_commands, + string_commands, +}; + +/// A bulk string, the type commands are sent as. +pub fn bulk(value: &str) -> Value { + Value::BulkString(value.to_string()) +} + +/// A command, e.g. `command(&["GET", "key"])`. +pub fn command(words: &[&str]) -> Vec { + words.iter().map(|word| bulk(word)).collect() +} + +/// A map reply, e.g. to HELLO. +pub fn map(entries: Vec<(Value, Value)>) -> Value { + Value::Map( + entries + .into_iter() + .map(|(key, value)| (encode(key), encode(value))) + .collect(), + ) +} + +/// A push message, e.g. a pub/sub message. +pub fn push(items: Vec) -> Value { + Value::Push(items.into_iter().map(encode).collect()) +} + +/// The server's properties in the canned reply to HELLO. +pub fn hello_reply() -> Value { + map(vec![ + (bulk("server"), bulk("valkey")), + (bulk("version"), bulk("9.1.2")), + (bulk("proto"), Value::Integer(3)), + ]) +} + +/// The stub transport and server of a connection. +pub struct Transport { + requests: mpsc::UnboundedReceiver, + replies: Option>, + closed: Option>>, +} + +impl Transport { + /// The next command the connection wrote, as its words, `None` once it closed the requests + /// stream. + pub async fn request(&mut self) -> Option> { + let request = self.requests.recv().await?; + let Value::Array(words) = request else { + panic!("a command is an array, got {request:?}"); + }; + Some( + words + .into_iter() + .map(|word| decode(word).expect("encoded word")) + .collect(), + ) + } + + /// Send a value to the connection, a reply or a push message. + pub fn reply(&self, value: Value) { + let replies = self.replies.as_ref().expect("transport is open"); + replies.send(value).expect("connection reads replies"); + } + + /// Close the transport, gracefully with `Ok(())`, or with the error. + pub fn close(&mut self, result: Result<(), Error>) { + self.replies.take(); + if let Some(closed) = self.closed.take() { + let _ = closed.send(result); + } + } +} + +/// Start a connection over a stub transport, returning the transport and the connection the +/// guest resolves once it is established. +pub async fn connect( + accessor: &Accessor, + exports: &Exports, + opts: Option, +) -> Result<(Transport, FutureReader>)> { + let (replies_tx, replies_rx) = mpsc::unbounded_channel(); + let (closed_tx, closed_rx) = oneshot::channel(); + let replies = accessor.with(|store| StreamReader::new(store, ChannelProducer(replies_rx)))?; + let closed = accessor.with(|store| { + FutureReader::new(store, async move { + // a transport dropped without closing it closed gracefully + Ok::<_, wasmtime::Error>(closed_rx.await.unwrap_or(Ok(()))) + }) + })?; + let (requests, connection) = exports + .connection() + .call_new(accessor, replies, closed, opts) + .await?; + let (requests_tx, requests_rx) = mpsc::unbounded_channel(); + accessor.with(|store| requests.pipe(store, ChannelConsumer(requests_tx)))?; + Ok(( + Transport { + requests: requests_rx, + replies: Some(replies_tx), + closed: Some(closed_tx), + }, + connection, + )) +} + +/// Start a connection and answer its HELLO with [`hello_reply`], returning the transport and the +/// established connection. +pub async fn establish( + accessor: &Accessor, + exports: &Exports, +) -> Result<(Transport, ResourceAny)> { + let (mut transport, connection) = connect(accessor, exports, None).await?; + let hello = transport.request().await.expect("HELLO"); + assert_eq!(hello, command(&["HELLO", "3"])); + transport.reply(hello_reply()); + let connection = resolve(accessor, connection) + .await? + .map_err(|error| format_err!("failed to establish the connection: {error:?}"))?; + Ok((transport, connection)) +} + +/// Wait for the value of a guest future. +pub async fn resolve( + accessor: &Accessor, + future: FutureReader, +) -> Result { + let (tx, rx) = oneshot::channel(); + accessor.with(|store| future.pipe(store, OneshotConsumer(Some(tx))))?; + rx.await + .map_err(|_| format_err!("future closed without a value")) +} + +/// Read every item of a guest stream into a channel, until it closes. +pub fn receive( + accessor: &Accessor, + stream: StreamReader, +) -> Result> { + let (tx, rx) = mpsc::unbounded_channel(); + accessor.with(|store| stream.pipe(store, ChannelConsumer(tx)))?; + Ok(rx) +} + +/// Produces the items sent to the channel, until every sender is dropped. +struct ChannelProducer(mpsc::UnboundedReceiver); + +impl StreamProducer for ChannelProducer { + type Item = T; + type Buffer = Option; + + fn poll_produce<'a>( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + _store: StoreContextMut<'a, D>, + mut destination: Destination<'a, T, Option>, + finish: bool, + ) -> Poll> { + match self.get_mut().0.poll_recv(cx) { + Poll::Ready(Some(item)) => { + destination.set_buffer(Some(item)); + Poll::Ready(Ok(StreamResult::Completed)) + } + Poll::Ready(None) => Poll::Ready(Ok(StreamResult::Dropped)), + Poll::Pending if finish => Poll::Ready(Ok(StreamResult::Cancelled)), + Poll::Pending => Poll::Pending, + } + } +} + +struct OneshotConsumer(Option>); + +impl FutureConsumer for OneshotConsumer { + type Item = T; + + fn poll_consume( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + store: StoreContextMut, + mut source: Source<'_, T>, + _finish: bool, + ) -> Poll> { + let mut item = None; + source.read(store, &mut item)?; + if let (Some(item), Some(tx)) = (item, self.get_mut().0.take()) { + // the receiver is only dropped when the test stopped waiting + let _ = tx.send(item); + } + Poll::Ready(Ok(())) + } +} + +struct ChannelConsumer(mpsc::UnboundedSender); + +impl StreamConsumer for ChannelConsumer { + type Item = T; + + fn poll_consume( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + store: StoreContextMut, + mut source: Source<'_, T>, + _finish: bool, + ) -> Poll> { + let mut item = None; + source.read(store, &mut item)?; + if let Some(item) = item { + if self.0.send(item).is_err() { + return Poll::Ready(Ok(StreamResult::Dropped)); + } + } + Poll::Ready(Ok(StreamResult::Completed)) + } +} + +/// Root of the workspace, where the Makefile lives. +fn workspace_dir() -> PathBuf { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + dir.canonicalize().unwrap_or(dir) +} + +/// Path containing the built component. +pub fn component_path(name: &str) -> PathBuf { + workspace_dir().join(format!("target/components/{name}/{name}.wasm")) +} + +/// Rebuild the named component in `target/components/` with make, unless it was already built +/// by this process. +fn ensure_built(name: &str) -> Result<()> { + static BUILT: Mutex>> = Mutex::new(None); + + // hold the lock while building so concurrent tests don't run make over each other + let mut built = BUILT.lock().unwrap_or_else(|err| err.into_inner()); + let built = built.get_or_insert_with(HashSet::new); + if built.contains(name) { + return Ok(()); + } + + // the Makefile's targets are relative to the workspace, make matches them textually + let target = format!("target/components/{name}/{name}.wasm"); + let output = Command::new("make") + .arg("-C") + .arg(workspace_dir()) + .arg(&target) + .output() + .context("failed to run make")?; + if !output.status.success() { + bail!( + "failed to build {target}:\n{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + built.insert(name.to_string()); + Ok(()) +} + +pub struct Ctx { + wasi: WasiCtx, + table: ResourceTable, + /// The values of `wasi:config/store`. + config: Vec<(String, String)>, +} + +impl bindings::wasi::config::store::Host for Ctx { + fn get( + &mut self, + key: String, + ) -> Result, bindings::wasi::config::store::Error> { + Ok(self + .config + .iter() + .rev() + .find(|(k, _)| *k == key) + .map(|(_, v)| v.clone())) + } + + fn get_all(&mut self) -> Result, bindings::wasi::config::store::Error> { + Ok(self.config.clone()) + } +} + +impl WasiView for Ctx { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi, + table: &mut self.table, + } + } +} + +// the components only import the types of `resp`, the functions are here for completeness +impl bindings::componentized::valkey::resp::Host for Ctx { + fn decode(&mut self, data: Vec) -> Result { + decode(data) + } + + fn encode(&mut self, value: Value) -> Result, String> { + Ok(encode(value)) + } +} + +/// The interfaces a component exports. +pub struct Exports { + connection: Option, + tcp: Option, + keyvalue_store: Option, + keyvalue_types: Option, + keyvalue_atomics: Option, + keyvalue_batch: Option, + admin_commands: Option, + connection_commands: Option, + hash_commands: Option, + keyspace_commands: Option, + pubsub_commands: Option, + string_commands: Option, + resp: Option, +} + +impl Exports { + pub fn keyvalue_store(&self) -> &keyvalue::store::Guest { + self.keyvalue_store + .as_ref() + .expect("component exports wasmcloud:keyvalue/store") + } + + pub fn keyvalue_types(&self) -> &keyvalue::types::Guest { + self.keyvalue_types + .as_ref() + .expect("component exports wasmcloud:keyvalue/types") + } + + pub fn keyvalue_atomics(&self) -> &keyvalue::atomics::Guest { + self.keyvalue_atomics + .as_ref() + .expect("component exports wasmcloud:keyvalue/atomics") + } + + pub fn keyvalue_batch(&self) -> &keyvalue::batch::Guest { + self.keyvalue_batch + .as_ref() + .expect("component exports wasmcloud:keyvalue/batch") + } + + pub fn admin_commands(&self) -> &admin_commands::Guest { + self.admin_commands + .as_ref() + .expect("component exports componentized:valkey/admin-commands") + } + + pub fn connection_commands(&self) -> &connection_commands::Guest { + self.connection_commands + .as_ref() + .expect("component exports componentized:valkey/connection-commands") + } + + pub fn hash_commands(&self) -> &hash_commands::Guest { + self.hash_commands + .as_ref() + .expect("component exports componentized:valkey/hash-commands") + } + + pub fn keyspace_commands(&self) -> &keyspace_commands::Guest { + self.keyspace_commands + .as_ref() + .expect("component exports componentized:valkey/keyspace-commands") + } + + pub fn pubsub_commands(&self) -> &pubsub_commands::Guest { + self.pubsub_commands + .as_ref() + .expect("component exports componentized:valkey/pubsub-commands") + } + + pub fn string_commands(&self) -> &string_commands::Guest { + self.string_commands + .as_ref() + .expect("component exports componentized:valkey/string-commands") + } + + pub fn tcp(&self) -> &Tcp { + self.tcp + .as_ref() + .expect("component exports componentized:valkey/tcp") + } + pub fn resp(&self) -> &Resp { + self.resp + .as_ref() + .expect("component exports componentized:valkey/resp") + } + + pub fn connection(&self) -> Connection<'_> { + self.connection + .as_ref() + .expect("component exports componentized:valkey/connection") + .connection() + } +} + +/// An instantiated component, see [`Harness::run`]. +pub struct Harness { + store: Store, + exports: Exports, +} + +impl Harness { + /// Build and instantiate the named component, e.g. `connection`. + pub async fn new(name: &str) -> Result { + Self::with_config(name, &[]).await + } + + /// Build and instantiate the named component, with the values of `wasi:config/store`. + pub async fn with_config(name: &str, config_values: &[(&str, &str)]) -> Result { + ensure_built(name)?; + + let mut config = Config::new(); + config.wasm_component_model_async(true); + let engine = Engine::new(&config)?; + let component = Component::from_file(&engine, component_path(name)) + .with_context(|| format!("failed to load {name}"))?; + + let mut linker = Linker::new(&engine); + wasmtime_wasi::p3::add_to_linker(&mut linker)?; + bindings::componentized::valkey::resp::add_to_linker::<_, HasSelf>( + &mut linker, + |ctx| ctx, + )?; + bindings::wasi::config::store::add_to_linker::<_, HasSelf>(&mut linker, |ctx| ctx)?; + + let mut store = Store::new( + &engine, + Ctx { + wasi: WasiCtxBuilder::new() + .inherit_stdio() + // for components that open sockets, the tests' servers listen on localhost + .inherit_network() + .allow_tcp(true) + .allow_ip_name_lookup(true) + .build(), + table: ResourceTable::new(), + config: config_values + .iter() + .map(|(key, value)| (key.to_string(), value.to_string())) + .collect(), + }, + ); + let instance_pre = linker.instantiate_pre(&component)?; + let instance = instance_pre + .instantiate_async(&mut store) + .await + .with_context(|| format!("failed to instantiate {name}"))?; + let exports = Exports { + connection: bindings::exports::componentized::valkey::connection::GuestIndices::new( + &instance_pre, + ) + .ok() + .and_then(|indices| indices.load(&mut store, &instance).ok()), + tcp: bindings::exports::componentized::valkey::tcp::GuestIndices::new(&instance_pre) + .ok() + .and_then(|indices| indices.load(&mut store, &instance).ok()), + keyvalue_store: keyvalue::store::GuestIndices::new(&instance_pre) + .ok() + .and_then(|indices| indices.load(&mut store, &instance).ok()), + keyvalue_types: keyvalue::types::GuestIndices::new(&instance_pre) + .ok() + .and_then(|indices| indices.load(&mut store, &instance).ok()), + keyvalue_atomics: keyvalue::atomics::GuestIndices::new(&instance_pre) + .ok() + .and_then(|indices| indices.load(&mut store, &instance).ok()), + keyvalue_batch: keyvalue::batch::GuestIndices::new(&instance_pre) + .ok() + .and_then(|indices| indices.load(&mut store, &instance).ok()), + admin_commands: admin_commands::GuestIndices::new(&instance_pre) + .ok() + .and_then(|indices| indices.load(&mut store, &instance).ok()), + connection_commands: connection_commands::GuestIndices::new(&instance_pre) + .ok() + .and_then(|indices| indices.load(&mut store, &instance).ok()), + hash_commands: hash_commands::GuestIndices::new(&instance_pre) + .ok() + .and_then(|indices| indices.load(&mut store, &instance).ok()), + keyspace_commands: keyspace_commands::GuestIndices::new(&instance_pre) + .ok() + .and_then(|indices| indices.load(&mut store, &instance).ok()), + pubsub_commands: pubsub_commands::GuestIndices::new(&instance_pre) + .ok() + .and_then(|indices| indices.load(&mut store, &instance).ok()), + string_commands: string_commands::GuestIndices::new(&instance_pre) + .ok() + .and_then(|indices| indices.load(&mut store, &instance).ok()), + resp: resp_bindings::exports::componentized::valkey::resp::GuestIndices::new( + &instance_pre, + ) + .ok() + .and_then(|indices| indices.load(&mut store, &instance).ok()), + }; + Ok(Self { store, exports }) + } + + /// Drop a resource the component exports, running its destructor. + /// + /// Between runs, the store is only free to drop a resource while no run is in progress. + pub async fn drop_resource(&mut self, resource: ResourceAny) -> Result<()> { + resource.resource_drop_async(&mut self.store).await + } + + /// Run a test body against the component's exports. + pub async fn run( + &mut self, + f: impl AsyncFnOnce(&Accessor, &Exports) -> Result + Send, + ) -> Result { + let exports = &self.exports; + self.store + .run_concurrent(async move |accessor| f(accessor, exports).await) + .await? + } +} diff --git a/crates/test-harness/src/server.rs b/crates/test-harness/src/server.rs new file mode 100644 index 0000000..6d2da0e --- /dev/null +++ b/crates/test-harness/src/server.rs @@ -0,0 +1,82 @@ +//! A server over TCP, played by the test. + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; + +use crate::{Value, decode, encode}; + +/// Listens on localhost for the component's connections. +pub struct Server { + listener: TcpListener, +} + +impl Server { + pub async fn bind() -> std::io::Result { + Ok(Self { + listener: TcpListener::bind("127.0.0.1:0").await?, + }) + } + + pub fn port(&self) -> u16 { + self.listener.local_addr().expect("bound").port() + } + + /// The next connection to the server. + pub async fn accept(&self) -> std::io::Result { + let (stream, _) = self.listener.accept().await?; + Ok(Client { + stream, + buffered: vec![], + }) + } +} + +/// A connection to the [`Server`], from the component. +pub struct Client { + stream: TcpStream, + /// Bytes read that are not part of a whole command yet. + buffered: Vec, +} + +impl Client { + /// The next command the client sent, as its words, `None` once it closed its side. + pub async fn request(&mut self) -> Option> { + loop { + if let Some(len) = resp_codec::frame_len(&self.buffered).expect("valid RESP") { + let rest = self.buffered.split_off(len); + let frame = std::mem::replace(&mut self.buffered, rest); + let Value::Array(words) = decode(frame).expect("valid RESP") else { + panic!("a command is an array"); + }; + return Some( + words + .into_iter() + .map(|word| decode(word).expect("encoded word")) + .collect(), + ); + } + let mut chunk = [0; 4096]; + let read = self.stream.read(&mut chunk).await.expect("read"); + if read == 0 { + return None; + } + self.buffered.extend_from_slice(&chunk[..read]); + } + } + + /// Send a value to the client. + pub async fn reply(&mut self, value: Value) { + self.write(&encode(value)).await; + } + + /// Send raw bytes to the client, e.g. part of a value. + pub async fn write(&mut self, bytes: &[u8]) { + self.stream.write_all(bytes).await.expect("write"); + self.stream.flush().await.expect("flush"); + } + + /// Close the connection. + pub async fn close(mut self) { + let _ = self.stream.shutdown().await; + } +} diff --git a/wit/admin-commands.wit b/wit/admin-commands.wit new file mode 100644 index 0000000..6e6a57c --- /dev/null +++ b/wit/admin-commands.wit @@ -0,0 +1,86 @@ +/// Administrative commands, Valkey's @admin ACL category. +/// +/// Also holds the rest of the ACL family, e.g. ACL GENPASS, which is in no data type or +/// functional category. +interface admin-commands { + use connection.{connection, error}; + + acl-deluser: async func(connection: borrow, username: string) -> result<_, error>; + acl-genpass: async func(connection: borrow) -> result; + acl-setuser: async func(connection: borrow, username: string, rules: list) -> result<_, error>; + + // not yet implemented, from the @admin category: + // ACL CAT + // ACL DRYRUN + // ACL GETUSER + // ACL LIST + // ACL LOAD + // ACL LOG + // ACL SAVE + // ACL USERS + // ACL WHOAMI + // BGREWRITEAOF + // BGSAVE + // CLIENT KILL + // CLIENT LIST + // CLIENT NO-EVICT + // CLIENT PAUSE + // CLIENT UNBLOCK + // CLIENT UNPAUSE + // CLUSTER ADDSLOTS + // CLUSTER ADDSLOTSRANGE + // CLUSTER BUMPEPOCH + // CLUSTER CANCELSLOTMIGRATIONS + // CLUSTER COUNT-FAILURE-REPORTS + // CLUSTER DELSLOTS + // CLUSTER DELSLOTSRANGE + // CLUSTER FAILOVER + // CLUSTER FLUSHSLOT + // CLUSTER FLUSHSLOTS + // CLUSTER FORGET + // CLUSTER GETSLOTMIGRATIONS + // CLUSTER MEET + // CLUSTER MIGRATESLOTS + // CLUSTER REPLICAS + // CLUSTER REPLICATE + // CLUSTER RESET + // CLUSTER SAVECONFIG + // CLUSTER SET-CONFIG-EPOCH + // CLUSTER SETSLOT + // CLUSTER SLAVES + // CLUSTER SYNCSLOTS + // COMMANDLOG GET + // COMMANDLOG LEN + // COMMANDLOG RESET + // CONFIG GET + // CONFIG RESETSTAT + // CONFIG REWRITE + // CONFIG SET + // DEBUG + // FAILOVER + // LASTSAVE + // LATENCY DOCTOR + // LATENCY GRAPH + // LATENCY HISTOGRAM + // LATENCY HISTORY + // LATENCY LATEST + // LATENCY RESET + // MODULE LIST + // MODULE LOAD + // MODULE LOADEX + // MODULE UNLOAD + // MONITOR + // PFDEBUG + // PFSELFTEST + // PSYNC + // REPLCONF + // REPLICAOF + // ROLE + // SAVE + // SHUTDOWN + // SLAVEOF + // SLOWLOG GET + // SLOWLOG LEN + // SLOWLOG RESET + // SYNC +} diff --git a/wit/bitmap-commands.wit b/wit/bitmap-commands.wit new file mode 100644 index 0000000..5ddcc62 --- /dev/null +++ b/wit/bitmap-commands.wit @@ -0,0 +1,13 @@ +/// Commands for bitmaps, Valkey's @bitmap ACL category. +interface bitmap-commands { + use connection.{connection, error}; + + // not yet implemented, from the @bitmap category: + // BITCOUNT + // BITFIELD + // BITFIELD_RO + // BITOP + // BITPOS + // GETBIT + // SETBIT +} diff --git a/wit/connection-commands.wit b/wit/connection-commands.wit new file mode 100644 index 0000000..0c48b1b --- /dev/null +++ b/wit/connection-commands.wit @@ -0,0 +1,58 @@ +/// Commands about the connection, Valkey's @connection ACL category. +/// +/// Not to be confused with the `connection` interface, the mechanics of talking to a server. +interface connection-commands { + use resp.{value}; + use connection.{connection, error}; + + /// Options for `hello`, the protocol is always 3. + record hello-opts { + auth: option>, + client-name: option, + } + + auth: async func(connection: borrow, username: string, password: string) -> result<_, error>; + /// Sends `HELLO 3`, with the options' AUTH and SETNAME, and returns the server's properties. + /// + /// A connection already sends HELLO when it is established, see `connection.new`, sending it + /// again is useful e.g. to authenticate as another user. + hello: async func(connection: borrow, opts: option) -> result>, error>; + ping: async func(connection: borrow) -> result<_, error>; + + // not yet implemented, from the @connection category: + // ASKING + // CLIENT CACHING + // CLIENT CAPA + // CLIENT GETNAME + // CLIENT GETREDIR + // CLIENT ID + // CLIENT IMPORT-SOURCE + // CLIENT INFO + // CLIENT KILL + // CLIENT LIST + // CLIENT NO-EVICT + // CLIENT NO-TOUCH + // CLIENT PAUSE + // CLIENT REPLY + // CLIENT SETINFO + // CLIENT SETNAME + // CLIENT TRACKING + // CLIENT TRACKINGINFO + // CLIENT UNBLOCK + // CLIENT UNPAUSE + // COMMAND + // COMMAND COUNT + // COMMAND DOCS + // COMMAND GETKEYS + // COMMAND GETKEYSANDFLAGS + // COMMAND INFO + // COMMAND LIST + // ECHO + // QUIT + // READONLY + // READWRITE + // RESET + // SELECT + // WAIT + // WAITAOF +} diff --git a/wit/connection.wit b/wit/connection.wit new file mode 100644 index 0000000..1128d9c --- /dev/null +++ b/wit/connection.wit @@ -0,0 +1,86 @@ +/// RESP request/reply over a pair of value streams, independent of the transport carrying them. +/// +/// A transport and a connection are wired together with their streams: take the replies the +/// transport receives, establish the connection with them, hand the requests the connection +/// returns to the transport to send, then await the connection. See `tcp.connect` for a transport +/// that does this for its caller. +/// +/// The connection is only the mechanics of talking to a server. Commands are functions in the +/// command interfaces, e.g. `string-commands`, that borrow the connection, so command groups can be +/// implemented independently of each other. The groups follow Valkey's ACL categories. +interface connection { + use wasi:clocks/types@0.3.0.{duration}; + use resp.{error as resp-error, value}; + + /// The errors of a connection and of every command interface. + variant error { + /// The transport closed while a reply was outstanding, or before the command was sent. Holds + /// the transport's error when it did not close gracefully. + closed(option), + /// Client errors are semantic issues with input/output values, e.g. a reply that does not + /// have the shape the command expects. + client(string), + /// RESP errors are encoding issues converting values to/from bytes. + resp(resp-error), + /// Valkey errors are Error or BulkError values returned directly from the server. + valkey(string), + } + + /// Options for establishing a connection. + record connect-opts { + /// The username and password to authenticate with. + auth: option>, + /// The name of the connection, see `CLIENT SETNAME`. + client-name: option, + /// Enables keep-alive pings at this interval, see `connection.set-keep-alive`. + keep-alive-interval: option, + } + + resource connection { + /// Establishes a connection over a transport. + /// + /// `replies` are the values the transport receives from the server, and `closed` resolves + /// when the transport closes, to `ok` for a graceful close or to the error otherwise, see + /// `tcp.transport.receive`. Returns the requests for the transport to write to the server, + /// in order, and the connection once it is established. + /// + /// The connection sends `HELLO 3`, with the `auth` and `client-name` of `opts`, as its first + /// command. This pins the connection to RESP3 and confirms the server can be reached. The + /// future resolves to the connection once HELLO succeeds, or to the error that stopped it. + /// The rest of `opts` is applied before the connection is returned. + /// + /// Async so the connection keeps working between commands after it returns, e.g. to read + /// push messages and to send keep-alive pings. + new: static async func(replies: stream, closed: future>, opts: option) -> tuple, future>>; + + /// The server's properties from the reply to HELLO, e.g. `server`, `version` and `mode`. + server-properties: func() -> list>; + + /// Sends a command and waits for its reply. + /// + /// Several commands may be in flight at once, replies are paired with commands in the order + /// the commands were sent. + call: async func(command: list) -> result; + + /// Sends a command without waiting for its reply, resolving once the command is written. + /// + /// The server still replies, the reply is discarded when it arrives and an error it reports + /// is not returned. An implementation may instead ask the server not to reply, e.g. with + /// `CLIENT REPLY SKIP`. + send: async func(command: list) -> result<_, error>; + + /// Push messages, e.g. from SUBSCRIBE or client tracking, kept apart from replies. + /// + /// Each call returns a new stream that receives every push message arriving after the call, + /// push messages arriving while no stream is open are dropped. A stream that falls too far + /// behind is closed rather than holding up replies, the push messages it missed are lost. + pushes: func() -> stream; + + /// Sends PING when no command has been sent and no reply is outstanding for `interval`, + /// `none` disables it. A PING that fails closes the connection. Disabled by default. + /// + /// Unlike TCP keep-alive, a PING counts as activity for the server's idle client timeout, + /// and works over any transport. + set-keep-alive: func(interval: option); + } +} diff --git a/wit/connector.wit b/wit/connector.wit new file mode 100644 index 0000000..abfe5f3 --- /dev/null +++ b/wit/connector.wit @@ -0,0 +1,11 @@ +/// Establishes connections, with a transport and settings chosen by whoever provides it, e.g. from +/// configuration. +/// +/// A component that needs a connection, but not a say in how it is made, imports a connector, the +/// composition decides which one, e.g. `tcp-connector`. +interface connector { + use connection.{connection, error}; + + /// Establishes a new connection. + connect: async func() -> result; +} diff --git a/wit/geo-commands.wit b/wit/geo-commands.wit new file mode 100644 index 0000000..db26228 --- /dev/null +++ b/wit/geo-commands.wit @@ -0,0 +1,16 @@ +/// Commands for geospatial indexes, Valkey's @geo ACL category. +interface geo-commands { + use connection.{connection, error}; + + // not yet implemented, from the @geo category: + // GEOADD + // GEODIST + // GEOHASH + // GEOPOS + // GEORADIUS + // GEORADIUS_RO + // GEORADIUSBYMEMBER + // GEORADIUSBYMEMBER_RO + // GEOSEARCH + // GEOSEARCHSTORE +} diff --git a/wit/hash-commands.wit b/wit/hash-commands.wit new file mode 100644 index 0000000..9885bcc --- /dev/null +++ b/wit/hash-commands.wit @@ -0,0 +1,70 @@ +/// Commands for hash values, Valkey's @hash ACL category. +interface hash-commands { + use wasi:clocks/system-clock@0.3.0.{duration, instant}; + use connection.{connection, error}; + + // copied from keyspace-commands, so each command interface stands alone + enum expire-mode { + /// Set expiration only when the field has no expiration. + nx, + /// Set expiration only when the field has an existing expiration. + xx, + /// Set expiration only when the new expiration is greater than current one. + gt, + /// Set expiration only when the new expiration is less than current one. + lt, + } + + enum expire-result { + /// -2: key does not exist, or field does not exist in the HASH. + does-not-exist, + /// 0: the specified NX | XX | GT | LT condition has not been met. + condition-not-met, + /// 1: the expiration time was applied. + applied, + /// 2: when called with 0 seconds. + zero-seconds, + } + + record hrandfield-opts { + count: option, + with-values: option, + } + + record hscan-opts { + match: option, + count: option, + no-values: option, + } + + hdel: async func(connection: borrow, key: string, field: string) -> result<_, error>; + hexists: async func(connection: borrow, key: string, field: string) -> result; + hexpire: async func(connection: borrow, key: string, seconds: duration, mode: option, fields: list) -> result, error>; + hexpireat: async func(connection: borrow, key: string, seconds: instant, mode: option, fields: list) -> result, error>; + hget: async func(connection: borrow, key: string, field: string) -> result, error>; + hgetall: async func(connection: borrow, key: string) -> result>, error>; + hincrby: async func(connection: borrow, key: string, field: string, increment: s64) -> result; + hincrbyfloat: async func(connection: borrow, key: string, field: string, increment: f64) -> result; + hkeys: async func(connection: borrow, key: string) -> result, error>; + hlen: async func(connection: borrow, key: string) -> result; + hmget: async func(connection: borrow, key: string, fields: list) -> result>, error>; + hmset: async func(connection: borrow, key: string, fields: list>) -> result<_, error>; + hrandfield: async func(connection: borrow, key: string, opts: option) -> result>>>, error>; + hscan: async func(connection: borrow, key: string, cursor: option, opts: option) -> result, list>>>, error>; + hset: async func(connection: borrow, key: string, field: string, value: string) -> result<_, error>; + hsetnx: async func(connection: borrow, key: string, field: string, value: string) -> result; + hstrlen: async func(connection: borrow, key: string, field: string) -> result; + hvals: async func(connection: borrow, key: string) -> result, error>; + + // not yet implemented, from the @hash category: + // HEXPIRETIME + // HGETDEL + // HGETEX + // HPERSIST + // HPEXPIRE + // HPEXPIREAT + // HPEXPIRETIME + // HPTTL + // HSETEX + // HTTL +} diff --git a/wit/hyperloglog-commands.wit b/wit/hyperloglog-commands.wit new file mode 100644 index 0000000..7dc4765 --- /dev/null +++ b/wit/hyperloglog-commands.wit @@ -0,0 +1,11 @@ +/// Commands for HyperLogLogs, Valkey's @hyperloglog ACL category. +interface hyperloglog-commands { + use connection.{connection, error}; + + // not yet implemented, from the @hyperloglog category: + // PFADD + // PFCOUNT + // PFDEBUG + // PFMERGE + // PFSELFTEST +} diff --git a/wit/keyspace-commands.wit b/wit/keyspace-commands.wit new file mode 100644 index 0000000..101c145 --- /dev/null +++ b/wit/keyspace-commands.wit @@ -0,0 +1,65 @@ +/// Commands for keys of any type, Valkey's @keyspace ACL category. +interface keyspace-commands { + use wasi:clocks/system-clock@0.3.0.{duration, instant}; + use connection.{connection, error}; + + enum expire-mode { + /// Set expiration only when the field has no expiration. + nx, + /// Set expiration only when the field has an existing expiration. + xx, + /// Set expiration only when the new expiration is greater than current one. + gt, + /// Set expiration only when the new expiration is less than current one. + lt, + } + + enum expire-result { + /// -2: key does not exist, or field does not exist in the HASH. + does-not-exist, + /// 0: the specified NX | XX | GT | LT condition has not been met. + condition-not-met, + /// 1: the expiration time was applied. + applied, + /// 2: when called with 0 seconds. + zero-seconds, + } + + del: async func(connection: borrow, key: string) -> result<_, error>; + exists: async func(connection: borrow, key: string) -> result; + expire: async func(connection: borrow, key: string, seconds: duration, mode: option) -> result; + expireat: async func(connection: borrow, key: string, seconds: instant, mode: option) -> result; + keys: async func(connection: borrow, pattern: string) -> result, error>; + + // not yet implemented, from the @keyspace category: + // CLUSTERSCAN + // CLUSTER FLUSHSLOT + // COPY + // DBSIZE + // DUMP + // EXPIRETIME + // FLUSHALL + // FLUSHDB + // MIGRATE + // MOVE + // OBJECT ENCODING + // OBJECT FREQ + // OBJECT IDLETIME + // OBJECT REFCOUNT + // PERSIST + // PEXPIRE + // PEXPIREAT + // PEXPIRETIME + // PTTL + // RANDOMKEY + // RENAME + // RENAMENX + // RESTORE + // RESTORE-ASKING + // SCAN + // SWAPDB + // TOUCH + // TTL + // TYPE + // UNLINK +} diff --git a/wit/list-commands.wit b/wit/list-commands.wit new file mode 100644 index 0000000..5d75644 --- /dev/null +++ b/wit/list-commands.wit @@ -0,0 +1,30 @@ +/// Commands for list values, Valkey's @list ACL category. +interface list-commands { + use connection.{connection, error}; + + // not yet implemented, from the @list category: + // BLMOVE + // BLMPOP + // BLPOP + // BRPOP + // BRPOPLPUSH + // LINDEX + // LINSERT + // LLEN + // LMOVE + // LMPOP + // LPOP + // LPOS + // LPUSH + // LPUSHX + // LRANGE + // LREM + // LSET + // LTRIM + // RPOP + // RPOPLPUSH + // RPUSH + // RPUSHX + // SORT + // SORT_RO +} diff --git a/wit/pubsub-commands.wit b/wit/pubsub-commands.wit new file mode 100644 index 0000000..a28bc7a --- /dev/null +++ b/wit/pubsub-commands.wit @@ -0,0 +1,22 @@ +/// Publish/subscribe commands, Valkey's @pubsub ACL category. +/// +/// Messages for subscriptions arrive as push messages, see `connection.pushes`. +interface pubsub-commands { + use connection.{connection, error}; + + publish: async func(connection: borrow, channel: string, message: string) -> result; + + // not yet implemented, from the @pubsub category: + // PSUBSCRIBE + // PUBSUB CHANNELS + // PUBSUB NUMPAT + // PUBSUB NUMSUB + // PUBSUB SHARDCHANNELS + // PUBSUB SHARDNUMSUB + // PUNSUBSCRIBE + // SPUBLISH + // SSUBSCRIBE + // SUBSCRIBE + // SUNSUBSCRIBE + // UNSUBSCRIBE +} diff --git a/wit/scripting-commands.wit b/wit/scripting-commands.wit new file mode 100644 index 0000000..3c950fb --- /dev/null +++ b/wit/scripting-commands.wit @@ -0,0 +1,26 @@ +/// Commands for scripts and functions, Valkey's @scripting ACL category. +interface scripting-commands { + use connection.{connection, error}; + + // not yet implemented, from the @scripting category: + // EVAL + // EVAL_RO + // EVALSHA + // EVALSHA_RO + // FCALL + // FCALL_RO + // FUNCTION DELETE + // FUNCTION DUMP + // FUNCTION FLUSH + // FUNCTION KILL + // FUNCTION LIST + // FUNCTION LOAD + // FUNCTION RESTORE + // FUNCTION STATS + // SCRIPT DEBUG + // SCRIPT EXISTS + // SCRIPT FLUSH + // SCRIPT KILL + // SCRIPT LOAD + // SCRIPT SHOW +} diff --git a/wit/set-commands.wit b/wit/set-commands.wit new file mode 100644 index 0000000..d6f6d06 --- /dev/null +++ b/wit/set-commands.wit @@ -0,0 +1,25 @@ +/// Commands for set values, Valkey's @set ACL category. +interface set-commands { + use connection.{connection, error}; + + // not yet implemented, from the @set category: + // SADD + // SCARD + // SDIFF + // SDIFFSTORE + // SINTER + // SINTERCARD + // SINTERSTORE + // SISMEMBER + // SMEMBERS + // SMISMEMBER + // SMOVE + // SORT + // SORT_RO + // SPOP + // SRANDMEMBER + // SREM + // SSCAN + // SUNION + // SUNIONSTORE +} diff --git a/wit/sortedset-commands.wit b/wit/sortedset-commands.wit new file mode 100644 index 0000000..fdb3ef6 --- /dev/null +++ b/wit/sortedset-commands.wit @@ -0,0 +1,43 @@ +/// Commands for sorted set values, Valkey's @sortedset ACL category. +interface sortedset-commands { + use connection.{connection, error}; + + // not yet implemented, from the @sortedset category: + // BZMPOP + // BZPOPMAX + // BZPOPMIN + // SORT + // SORT_RO + // ZADD + // ZCARD + // ZCOUNT + // ZDIFF + // ZDIFFSTORE + // ZINCRBY + // ZINTER + // ZINTERCARD + // ZINTERSTORE + // ZLEXCOUNT + // ZMPOP + // ZMSCORE + // ZPOPMAX + // ZPOPMIN + // ZRANDMEMBER + // ZRANGE + // ZRANGEBYLEX + // ZRANGEBYSCORE + // ZRANGESTORE + // ZRANK + // ZREM + // ZREMRANGEBYLEX + // ZREMRANGEBYRANK + // ZREMRANGEBYSCORE + // ZREVRANGE + // ZREVRANGEBYLEX + // ZREVRANGEBYSCORE + // ZREVRANK + // ZSCAN + // ZSCORE + // ZUNION + // ZUNIONSTORE +} diff --git a/wit/store.wit b/wit/store.wit deleted file mode 100644 index b19cf73..0000000 --- a/wit/store.wit +++ /dev/null @@ -1,94 +0,0 @@ -interface store { - use wasi:clocks/system-clock@0.3.0.{ duration, instant }; - use resp.{error as resp-error, value}; - - variant error { - /// Client errors can represent semantic issues with input/output values, or connection errors communicating with the server - client(string), - /// RESP errors are encoding issues converting values to/from bytes - resp(resp-error), - /// Valkey errors are Error or BulkError values returned directly from the server - valkey(string), - } - - enum expire-mode { - /// Set expiration only when the field has no expiration. - nx, - /// Set expiration only when the field has an existing expiration. - xx, - /// Set expiration only when the new expiration is greater than current one. - gt, - /// Set expiration only when the new expiration is less than current one. - lt, - } - - enum expire-result { - /// -2: key does not exist, or field does not exist in the HASH. - does-not-exist, - /// 0: the specified NX | XX | GT | LT condition has not been met. - condition-not-met, - /// 1: the expiration time was applied. - applied, - /// 2: when called with 0 seconds. - zero-seconds, - } - - record hello-opts { - proto-ver: option, - auth: option>, - client-name: option, - } - - record hrandfield-opts { - count: option, - with-values: option, - } - - record hscan-opts { - match: option, - count: option, - no-values: option, - } - - resource connection { - send: async func(command: list) -> result; - - acl-deluser: async func(username: string) -> result<_, error>; - acl-genpass: async func() -> result; - acl-setuser: async func(username: string, rules: list) -> result<_, error>; - auth: async func(username: string, password: string) -> result<_, error>; - del: async func(key: string) -> result<_, error>; - exists: async func(key: string) -> result; - expire: async func(key: string, seconds: duration, mode: option) -> result; - expireat: async func(key: string, seconds: instant, mode: option) -> result; - get: async func(key: string) -> result, error>; - hdel: async func(key: string, field: string) -> result<_, error>; - hello: async func(opts: option) -> result>, error>; - hexists: async func(key: string, field: string) -> result; - hexpire: async func(key: string, seconds: duration, mode: option, fields: list) -> result, error>; - hexpireat: async func(key: string, seconds: instant, mode: option, fields: list) -> result, error>; - hget: async func(key: string, field: string) -> result, error>; - hgetall: async func(key: string) -> result>, error>; - hincrby: async func(key: string, field: string, increment: s64) -> result; - hincrbyfloat: async func(key: string, field: string, increment: f64) -> result; - hkeys: async func(key: string) -> result, error>; - hlen: async func(key: string) -> result; - hmget: async func(key: string, fields: list) -> result>, error>; - hmset: async func(key: string, fields: list>) -> result<_, error>; - hrandfield: async func(key: string, opts: option) -> result>>>, error>; - hscan: async func(key: string, cursor: option, opts: option) -> result, list>>>, error>; - hset: async func(key: string, field: string, value: string) -> result<_, error>; - hsetnx: async func(key: string, field: string, value: string) -> result; - hstrlen: async func(key: string, field: string) -> result; - hvals: async func(key: string) -> result, error>; - incr: async func(key: string) -> result; - incrby: async func(key: string, increment: s64) -> result; - keys: async func(pattern: string) -> result, error>; - ping: async func() -> result<_, error>; - publish: async func(channel: string, message: string) -> result; - quit: async func() -> result<_, error>; - set: async func(key: string, value: string) -> result<_, error>; - } - - connect: async func(host: string, port: u16, opts: option) -> result; -} diff --git a/wit/stream-commands.wit b/wit/stream-commands.wit new file mode 100644 index 0000000..c7f992e --- /dev/null +++ b/wit/stream-commands.wit @@ -0,0 +1,27 @@ +/// Commands for stream values, Valkey's @stream ACL category. +interface stream-commands { + use connection.{connection, error}; + + // not yet implemented, from the @stream category: + // XACK + // XADD + // XAUTOCLAIM + // XCLAIM + // XDEL + // XGROUP CREATE + // XGROUP CREATECONSUMER + // XGROUP DELCONSUMER + // XGROUP DESTROY + // XGROUP SETID + // XINFO CONSUMERS + // XINFO GROUPS + // XINFO STREAM + // XLEN + // XPENDING + // XRANGE + // XREAD + // XREADGROUP + // XREVRANGE + // XSETID + // XTRIM +} diff --git a/wit/string-commands.wit b/wit/string-commands.wit new file mode 100644 index 0000000..d39b963 --- /dev/null +++ b/wit/string-commands.wit @@ -0,0 +1,31 @@ +/// Commands for string values, Valkey's @string ACL category. +interface string-commands { + use connection.{connection, error}; + + get: async func(connection: borrow, key: string) -> result, error>; + incr: async func(connection: borrow, key: string) -> result; + incrby: async func(connection: borrow, key: string, increment: s64) -> result; + set: async func(connection: borrow, key: string, value: string) -> result<_, error>; + + // not yet implemented, from the @string category: + // APPEND + // DECR + // DECRBY + // DELIFEQ + // GETDEL + // GETEX + // GETRANGE + // GETSET + // INCRBYFLOAT + // LCS + // MGET + // MSET + // MSETEX + // MSETNX + // PSETEX + // SETEX + // SETNX + // SETRANGE + // STRLEN + // SUBSTR +} diff --git a/wit/tcp.wit b/wit/tcp.wit new file mode 100644 index 0000000..c9a8405 --- /dev/null +++ b/wit/tcp.wit @@ -0,0 +1,32 @@ +/// A transport for connections over wasi:sockets TCP. +interface tcp { + use resp.{value}; + use connection.{connection, connect-opts, error}; + + /// A TCP connection to a Valkey server, framing the bytes it carries into RESP values in each + /// direction. + resource transport { + /// Opens a transport to `host`, a hostname or an IP address. Each address the host resolves + /// to is tried in turn. + open: static async func(host: string, port: u16) -> result; + + /// Writes each value from `requests` to the server, in order. + /// + /// May be called at most once, like `tcp-socket.send`. Closing `requests` half-closes the + /// socket. The future resolves once every value is written, or with the error that stopped + /// writing. + send: func(requests: stream) -> future>; + + /// The values the server sends, in order. + /// + /// May be called at most once, like `tcp-socket.receive`. The stream closes when the server + /// closes the socket, the future resolves to `ok` for a graceful close or to the error + /// otherwise. + receive: func() -> tuple, future>>; + } + + /// Opens a transport and establishes a connection over it with `opts`, see `connection.new`. + /// + /// The connection keeps the transport alive. + connect: async func(host: string, port: u16, opts: option) -> result; +} diff --git a/wit/transaction-commands.wit b/wit/transaction-commands.wit new file mode 100644 index 0000000..13f6eb4 --- /dev/null +++ b/wit/transaction-commands.wit @@ -0,0 +1,11 @@ +/// Commands for transactions, Valkey's @transaction ACL category. +interface transaction-commands { + use connection.{connection, error}; + + // not yet implemented, from the @transaction category: + // DISCARD + // EXEC + // MULTI + // UNWATCH + // WATCH +} diff --git a/wit/worlds.wit b/wit/worlds.wit index d229a58..33a0496 100644 --- a/wit/worlds.wit +++ b/wit/worlds.wit @@ -2,5 +2,24 @@ package componentized:valkey@0.3.0-dev; world imports { import resp; - import store; + import connection; + import connector; + import tcp; + + // commands, grouped by Valkey's ACL categories + import admin-commands; + import bitmap-commands; + import connection-commands; + import geo-commands; + import hash-commands; + import hyperloglog-commands; + import keyspace-commands; + import list-commands; + import pubsub-commands; + import scripting-commands; + import set-commands; + import sortedset-commands; + import stream-commands; + import string-commands; + import transaction-commands; } From 9edc07ebf83229111bbc491c1f8e466098a2fc86 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Sat, 10 Oct 2026 12:17:24 -0400 Subject: [PATCH 02/37] flesh out pubsub commands Signed-off-by: Scott Andrews --- crates/commands/src/pubsub.rs | 169 +++++++++++++++++++++++++++++++++- crates/commands/src/reply.rs | 24 +++++ wit/pubsub-commands.wit | 88 +++++++++++++++--- 3 files changed, 264 insertions(+), 17 deletions(-) diff --git a/crates/commands/src/pubsub.rs b/crates/commands/src/pubsub.rs index 54d20b1..3f077c5 100644 --- a/crates/commands/src/pubsub.rs +++ b/crates/commands/src/pubsub.rs @@ -2,16 +2,177 @@ use crate::componentized::valkey::connection::{Connection, Error}; use crate::exports::componentized::valkey::pubsub_commands::Guest; -use crate::reply::{call, integer}; +use crate::reply::{call, counts, integer, send, strings}; +use wit_bindgen::rt::Map; impl Guest for crate::Commands { + #[doc = " Subscribes the client to the given patterns."] + #[doc = ""] + #[doc = " PSUBSCRIBE pattern [ pattern ... ]"] + #[doc = " https://valkey.io/commands/psubscribe/"] + #[allow(async_fn_in_trait)] + async fn psubscribe(connection: &Connection, patterns: Vec) -> Result<(), Error> { + let mut command = vec!["PSUBSCRIBE".to_string()]; + command.extend(patterns); + send(connection, command).await + } + + #[doc = " Posts a message to the given channel."] + #[doc = ""] + #[doc = " PUBLISH channel message"] + #[doc = " https://valkey.io/commands/publish/"] + #[allow(async_fn_in_trait)] async fn publish( connection: &Connection, channel: String, message: String, ) -> Result { - // https://valkey.io/commands/publish/ - // PUBLISH channel message - integer(call(connection, ["PUBLISH".to_string(), channel, message]).await?) + let command = ["PUBLISH".to_string(), channel, message]; + let response = call(connection, command).await?; + integer(response) + } + + #[doc = " Lists the currently active channels."] + #[doc = ""] + #[doc = " PUBSUB CHANNELS [ pattern ]"] + #[doc = " https://valkey.io/commands/pubsub-channels/"] + #[allow(async_fn_in_trait)] + async fn pubsub_channels( + connection: &Connection, + pattern: Option, + ) -> Result, Error> { + let mut command = vec!["PUBSUB".to_string(), "CHANNELS".to_string()]; + command.extend(pattern); + let response = call(connection, command).await?; + strings(response) + } + + #[doc = " Returns the number of unique patterns that are subscribed to by clients."] + #[doc = ""] + #[doc = " PUBSUB NUMPAT"] + #[doc = " https://valkey.io/commands/pubsub-numpat/"] + #[allow(async_fn_in_trait)] + async fn pubsub_numpat(connection: &Connection) -> Result { + let command = ["PUBSUB".to_string(), "NUMPAT".to_string()]; + let response = call(connection, command).await?; + integer(response) + } + + #[doc = " Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels."] + #[doc = ""] + #[doc = " PUBSUB NUMSUB [ channel ] [ [ channel ] ... ]"] + #[doc = " https://valkey.io/commands/pubsub-numsub/"] + #[allow(async_fn_in_trait)] + async fn pubsub_numsub( + connection: &Connection, + channels: Vec, + ) -> Result, Error> { + let mut command = vec!["PUBSUB".to_string(), "NUMSUB".to_string()]; + command.extend(channels); + let response = call(connection, command).await?; + counts(response) + } + + #[doc = " Lists the currently active shard channels."] + #[doc = ""] + #[doc = " PUBSUB SHARDCHANNELS [ pattern ]"] + #[doc = " https://valkey.io/commands/pubsub-shardchannels/"] + #[allow(async_fn_in_trait)] + async fn pubsub_shardchannels( + connection: &Connection, + pattern: Option, + ) -> Result, Error> { + let mut command = vec!["PUBSUB".to_string(), "SHARDCHANNELS".to_string()]; + command.extend(pattern); + let response = call(connection, command).await?; + strings(response) + } + + #[doc = " Returns the number of subscribers for the specified shard channels."] + #[doc = ""] + #[doc = " PUBSUB SHARDNUMSUB [ shardchannel [ shardchannel ... ] ]"] + #[doc = " https://valkey.io/commands/pubsub-shardnumsub/"] + #[allow(async_fn_in_trait)] + async fn pubsub_shardnumsub( + connection: &Connection, + shardchannels: Vec, + ) -> Result, Error> { + let mut command = vec!["PUBSUB".to_string(), "SHARDNUMSUB".to_string()]; + command.extend(shardchannels); + let response = call(connection, command).await?; + counts(response) + } + + #[doc = " Unsubscribes the client from the given patterns, or from all of them if none is given."] + #[doc = ""] + #[doc = " PUNSUBSCRIBE [ pattern [ pattern ... ] ]"] + #[doc = " https://valkey.io/commands/punsubscribe/"] + #[allow(async_fn_in_trait)] + async fn punsubscribe(connection: &Connection, patterns: Vec) -> Result<(), Error> { + let mut command = vec!["PUNSUBSCRIBE".to_string()]; + command.extend(patterns); + send(connection, command).await + } + + #[doc = " Posts a message to the given shard channel."] + #[doc = ""] + #[doc = " SPUBLISH shardchannel message"] + #[doc = " https://valkey.io/commands/spublish/"] + #[allow(async_fn_in_trait)] + async fn spublish( + connection: &Connection, + shardchannel: String, + message: String, + ) -> Result { + let command = vec!["SSUBSCRIBE".to_string(), shardchannel, message]; + let response = call(connection, command).await?; + integer(response) + } + + #[doc = " Subscribes the client to the specified shard channels."] + #[doc = ""] + #[doc = " SSUBSCRIBE shardchannel [ shardchannel ... ]"] + #[doc = " https://valkey.io/commands/ssubscribe/"] + #[allow(async_fn_in_trait)] + async fn ssubscribe(connection: &Connection, shardchannels: Vec) -> Result<(), Error> { + let mut command = vec!["SSUBSCRIBE".to_string()]; + command.extend(shardchannels); + send(connection, command).await + } + + #[doc = " Subscribes the client to the specified channels."] + #[doc = ""] + #[doc = " SUBSCRIBE channel [ channel ... ]"] + #[doc = " https://valkey.io/commands/subscribe/"] + #[allow(async_fn_in_trait)] + async fn subscribe(connection: &Connection, channels: Vec) -> Result<(), Error> { + let mut command = vec!["SUBSCRIBE".to_string()]; + command.extend(channels); + send(connection, command).await + } + + #[doc = " Unsubscribes the client from the given shard channels, or from all of them if none is given."] + #[doc = ""] + #[doc = " SUNSUBSCRIBE [ shardchannel ] [ [ shardchannel ] ... ]"] + #[doc = " https://valkey.io/commands/sunsubscribe/"] + #[allow(async_fn_in_trait)] + async fn sunsubscribe( + connection: &Connection, + shardchannels: Vec, + ) -> Result<(), Error> { + let mut command = vec!["SUNSUBSCRIBE".to_string()]; + command.extend(shardchannels); + send(connection, command).await + } + + #[doc = " Unsubscribes the client from the given channels, or from all of them if none is given."] + #[doc = ""] + #[doc = " UNSUBSCRIBE [ channel ] [ [ channel ] ... ]"] + #[doc = " https://valkey.io/commands/unsubscribe/"] + #[allow(async_fn_in_trait)] + async fn unsubscribe(connection: &Connection, channels: Vec) -> Result<(), Error> { + let mut command = vec!["UNSUBSCRIBE".to_string()]; + command.extend(channels); + send(connection, command).await } } diff --git a/crates/commands/src/reply.rs b/crates/commands/src/reply.rs index 6145aef..e3d5980 100644 --- a/crates/commands/src/reply.rs +++ b/crates/commands/src/reply.rs @@ -4,6 +4,7 @@ //! than a flat array of keys and values, both are accepted. use resp_codec::{NestedValue, Value}; +use wit_bindgen::rt::Map; use crate::componentized::valkey::connection::{Connection, Error}; @@ -22,6 +23,21 @@ where connection.call(command).await } +/// Send a command, its words as bulk strings, without waiting for a reply. +/// +/// An error reply fails the command, see `connection.send`. +pub async fn send(connection: &Connection, words: I) -> Result<(), Error> +where + I: IntoIterator, + W: Into, +{ + let command = words + .into_iter() + .map(|word| Value::BulkString(word.into())) + .collect(); + connection.send(command).await +} + pub fn unexpected(reply: &Value) -> Error { Error::Client(format!("unexpected reply: {reply:?}")) } @@ -126,6 +142,14 @@ pub fn string_entries(reply: Value) -> Result, Error> { .collect() } +pub fn counts(reply: Value) -> Result, Error> { + let mut map = Map::new(); + for (key, value) in entries(reply)? { + map.insert(string(key)?, integer(value)?); + } + Ok(map) +} + /// A duration, in nanoseconds, as whole seconds, the precision of EXPIRE and HEXPIRE. pub fn whole_seconds(nanoseconds: u64) -> String { (nanoseconds / 1_000_000_000).to_string() diff --git a/wit/pubsub-commands.wit b/wit/pubsub-commands.wit index a28bc7a..8d2685b 100644 --- a/wit/pubsub-commands.wit +++ b/wit/pubsub-commands.wit @@ -4,19 +4,81 @@ interface pubsub-commands { use connection.{connection, error}; + /// Subscribes the client to the given patterns. + /// + /// PSUBSCRIBE pattern [ pattern ... ] + /// https://valkey.io/commands/psubscribe/ + psubscribe: async func(connection: borrow, patterns: list) -> result<_, error>; + + /// Posts a message to the given channel. + /// + /// PUBLISH channel message + /// https://valkey.io/commands/publish/ publish: async func(connection: borrow, channel: string, message: string) -> result; - // not yet implemented, from the @pubsub category: - // PSUBSCRIBE - // PUBSUB CHANNELS - // PUBSUB NUMPAT - // PUBSUB NUMSUB - // PUBSUB SHARDCHANNELS - // PUBSUB SHARDNUMSUB - // PUNSUBSCRIBE - // SPUBLISH - // SSUBSCRIBE - // SUBSCRIBE - // SUNSUBSCRIBE - // UNSUBSCRIBE + /// Lists the currently active channels. + /// + /// PUBSUB CHANNELS [ pattern ] + /// https://valkey.io/commands/pubsub-channels/ + pubsub-channels: async func(connection: borrow, pattern: option) -> result, error>; + + /// Returns the number of unique patterns that are subscribed to by clients. + /// + /// PUBSUB NUMPAT + /// https://valkey.io/commands/pubsub-numpat/ + pubsub-numpat: async func(connection: borrow) -> result; + + /// Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels. + /// + /// PUBSUB NUMSUB [ channel ] [ [ channel ] ... ] + /// https://valkey.io/commands/pubsub-numsub/ + pubsub-numsub: async func(connection: borrow, channels: list) -> result, error>; + + /// Lists the currently active shard channels. + /// + /// PUBSUB SHARDCHANNELS [ pattern ] + /// https://valkey.io/commands/pubsub-shardchannels/ + pubsub-shardchannels: async func(connection: borrow, pattern: option) -> result, error>; + + /// Returns the number of subscribers for the specified shard channels. + /// + /// PUBSUB SHARDNUMSUB [ shardchannel [ shardchannel ... ] ] + /// https://valkey.io/commands/pubsub-shardnumsub/ + pubsub-shardnumsub: async func(connection: borrow, shardchannels: list) -> result, error>; + + /// Unsubscribes the client from the given patterns, or from all of them if none is given. + /// + /// PUNSUBSCRIBE [ pattern [ pattern ... ] ] + /// https://valkey.io/commands/punsubscribe/ + punsubscribe: async func(connection: borrow, patterns: list) -> result<_, error>; + + /// Posts a message to the given shard channel. + /// + /// SPUBLISH shardchannel message + /// https://valkey.io/commands/spublish/ + spublish: async func(connection: borrow, shardchannel: string, message: string) -> result; + + /// Subscribes the client to the specified shard channels. + /// + /// SSUBSCRIBE shardchannel [ shardchannel ... ] + /// https://valkey.io/commands/ssubscribe/ + ssubscribe: async func(connection: borrow, shardchannels: list) -> result<_, error>; + + /// Subscribes the client to the specified channels. + /// + /// SUBSCRIBE channel [ channel ... ] + /// https://valkey.io/commands/subscribe/ + subscribe: async func(connection: borrow, channels: list) -> result<_, error>; + + /// Unsubscribes the client from the given shard channels, or from all of them if none is given. + /// + /// SUNSUBSCRIBE [ shardchannel ] [ [ shardchannel ] ... ] + /// https://valkey.io/commands/sunsubscribe/ + sunsubscribe: async func(connection: borrow, shardchannels: list) -> result<_, error>; + + /// Unsubscribes the client from the given channels, or from all of them if none is given. + /// + /// UNSUBSCRIBE [ channel ] [ [ channel ] ... ] + /// https://valkey.io/commands/unsubscribe/ + unsubscribe: async func(connection: borrow, channels: list) -> result<_, error>; } From 2e2e74573b1f8011ab7644ed940acd0abe528319 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Mon, 12 Oct 2026 05:27:44 -0400 Subject: [PATCH 03/37] wip Signed-off-by: Scott Andrews --- Cargo.lock | 166 +-- Cargo.toml | 6 - Makefile | 2 +- README.md | 10 +- components/admin-commands/Cargo.toml | 23 - components/admin-commands/README.md | 5 - components/as-keyvalue/src/lib.rs | 43 +- components/cli/README.md | 2 +- components/cli/cli.wac | 11 +- components/commands/Cargo.toml | 11 - components/commands/src/command.rs | 415 ++++++ components/commands/src/lib.rs | 1494 ++++++++++++++++++++ components/commands/src/value.rs | 410 ++++++ components/commands/tests/commands.rs | 41 +- components/connection-commands/Cargo.toml | 23 - components/connection-commands/README.md | 5 - components/connection/src/lib.rs | 91 +- components/connection/tests/connection.rs | 27 + components/dep-cli/Cargo.toml | 4 +- components/dep-cli/README.md | 6 + components/dep-cli/src/lib.rs | 1133 +++++---------- components/hash-commands/Cargo.toml | 23 - components/hash-commands/README.md | 5 - components/keyspace-commands/Cargo.toml | 23 - components/keyspace-commands/README.md | 5 - components/pubsub-commands/Cargo.toml | 23 - components/pubsub-commands/README.md | 5 - components/string-commands/Cargo.toml | 23 - components/string-commands/README.md | 5 - components/test-commands/README.md | 3 - components/test-commands/test-commands.wac | 21 - components/wit/worlds.wit | 67 +- crates/commands/src/admin.rs | 39 - crates/commands/src/connection.rs | 48 - crates/commands/src/hash.rs | 332 ----- crates/commands/src/keyspace.rs | 73 - crates/commands/src/lib.rs | 209 --- crates/commands/src/pubsub.rs | 178 --- crates/commands/src/reply.rs | 156 -- crates/commands/src/string.rs | 37 - crates/test-harness/src/lib.rs | 18 +- wit/admin-commands.wit | 86 -- wit/bitmap-commands.wit | 13 - wit/commands.wit | 1360 ++++++++++++++++++ wit/connection-commands.wit | 58 - wit/connection.wit | 11 +- wit/geo-commands.wit | 16 - wit/hash-commands.wit | 70 - wit/hyperloglog-commands.wit | 11 - wit/keyspace-commands.wit | 65 - wit/list-commands.wit | 30 - wit/pubsub-commands.wit | 84 -- wit/resp.wit | 7 +- wit/scripting-commands.wit | 26 - wit/set-commands.wit | 25 - wit/sortedset-commands.wit | 43 - wit/stream-commands.wit | 27 - wit/string-commands.wit | 31 - wit/transaction-commands.wit | 11 - wit/worlds.wit | 20 +- 60 files changed, 4247 insertions(+), 2968 deletions(-) delete mode 100644 components/admin-commands/Cargo.toml delete mode 100644 components/admin-commands/README.md create mode 100644 components/commands/src/command.rs create mode 100644 components/commands/src/lib.rs create mode 100644 components/commands/src/value.rs delete mode 100644 components/connection-commands/Cargo.toml delete mode 100644 components/connection-commands/README.md delete mode 100644 components/hash-commands/Cargo.toml delete mode 100644 components/hash-commands/README.md delete mode 100644 components/keyspace-commands/Cargo.toml delete mode 100644 components/keyspace-commands/README.md delete mode 100644 components/pubsub-commands/Cargo.toml delete mode 100644 components/pubsub-commands/README.md delete mode 100644 components/string-commands/Cargo.toml delete mode 100644 components/string-commands/README.md delete mode 100644 components/test-commands/README.md delete mode 100644 components/test-commands/test-commands.wac delete mode 100644 crates/commands/src/admin.rs delete mode 100644 crates/commands/src/connection.rs delete mode 100644 crates/commands/src/hash.rs delete mode 100644 crates/commands/src/keyspace.rs delete mode 100644 crates/commands/src/lib.rs delete mode 100644 crates/commands/src/pubsub.rs delete mode 100644 crates/commands/src/reply.rs delete mode 100644 crates/commands/src/string.rs delete mode 100644 wit/admin-commands.wit delete mode 100644 wit/bitmap-commands.wit create mode 100644 wit/commands.wit delete mode 100644 wit/connection-commands.wit delete mode 100644 wit/geo-commands.wit delete mode 100644 wit/hash-commands.wit delete mode 100644 wit/hyperloglog-commands.wit delete mode 100644 wit/keyspace-commands.wit delete mode 100644 wit/list-commands.wit delete mode 100644 wit/pubsub-commands.wit delete mode 100644 wit/scripting-commands.wit delete mode 100644 wit/set-commands.wit delete mode 100644 wit/sortedset-commands.wit delete mode 100644 wit/stream-commands.wit delete mode 100644 wit/string-commands.wit delete mode 100644 wit/transaction-commands.wit diff --git a/Cargo.lock b/Cargo.lock index 2222435..03b37e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,14 +11,6 @@ dependencies = [ "gimli", ] -[[package]] -name = "admin-commands" -version = "0.1.0" -dependencies = [ - "resp-codec", - "wit-bindgen", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -31,56 +23,6 @@ version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" -dependencies = [ - "anstyle", - "once_cell", - "windows-sys 0.59.0", -] - [[package]] name = "anyhow" version = "1.0.104" @@ -197,46 +139,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "clap" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "clap_lex" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" - [[package]] name = "cobs" version = "0.3.0" @@ -246,12 +148,6 @@ dependencies = [ "thiserror 2.0.21", ] -[[package]] -name = "colorchoice" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" - [[package]] name = "commands" version = "0.1.0" @@ -276,14 +172,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "connection-commands" -version = "0.1.0" -dependencies = [ - "resp-codec", - "wit-bindgen", -] - [[package]] name = "core_detect" version = "1.0.0" @@ -520,7 +408,9 @@ dependencies = [ name = "dep-cli" version = "0.1.0" dependencies = [ - "clap", + "futures-channel", + "futures-core", + "resp-codec", "wit-bindgen", ] @@ -787,14 +677,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "hash-commands" -version = "0.1.0" -dependencies = [ - "resp-codec", - "wit-bindgen", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -968,12 +850,6 @@ version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - [[package]] name = "itertools" version = "0.14.0" @@ -1030,14 +906,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "keyspace-commands" -version = "0.1.0" -dependencies = [ - "resp-codec", - "wit-bindgen", -] - [[package]] name = "leb128" version = "0.2.7" @@ -1230,14 +1098,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "pubsub-commands" -version = "0.1.0" -dependencies = [ - "resp-codec", - "wit-bindgen", -] - [[package]] name = "pulley-interpreter" version = "49.0.1" @@ -1532,20 +1392,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "string-commands" -version = "0.1.0" -dependencies = [ - "resp-codec", - "wit-bindgen", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "syn" version = "2.0.117" @@ -1820,12 +1666,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - [[package]] name = "uuid" version = "1.28.0" diff --git a/Cargo.toml b/Cargo.toml index 87f0942..18a612b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,12 @@ [workspace] resolver = "2" members = [ - "components/admin-commands", "components/as-keyvalue", "components/commands", "components/connection", - "components/connection-commands", "components/dep-cli", - "components/hash-commands", - "components/keyspace-commands", - "components/pubsub-commands", "components/resp", "components/sample-http-incrementor", - "components/string-commands", "components/tcp", "components/tcp-connector", "crates/resp-codec", diff --git a/Makefile b/Makefile index e022e16..be1527a 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ SHELL := /bin/bash export RUST_BACKTRACE ?= 1 export WASMTIME_BACKTRACE_DETAILS ?= 1 export WKG_CONFIG_FILE := $(abspath .config/wasm-pkg/config.toml) -WASMTIME_RUN_FLAGS ?= -Sinherit-network -Sallow-ip-name-lookup +WASMTIME_RUN_FLAGS ?= -Sinherit-network -Sallow-ip-name-lookup -Sconfig -Wcomponent-model-map # `make` without a goal runs all, otherwise the first target of an included file would run .DEFAULT_GOAL := all diff --git a/README.md b/README.md index 625b495..f083b2e 100644 --- a/README.md +++ b/README.md @@ -68,14 +68,12 @@ Prereqs: - access to a running [Valkey](https://valkey.io) server ```sh -wasmtime run -Sinherit-network -Sallow-ip-name-lookup target/components/cli/cli.wasm keys '*' +wasmtime run -Sinherit-network -Sallow-ip-name-lookup -Sconfig target/components/cli/cli.wasm keys '*' ``` -- use `--host` to specify a host other than `127.0.0.1` -- use `--port` to specify a port other than `6379` -- use `--password`, and `--username` for a user other than `default`, to authenticate -- use `--client-name` to name the connection -- use `--help` for the commands, and their options +The arguments are sent as a command and the reply is printed. A command without a reply, like `SUBSCRIBE`, prints the push messages that arrive until interrupted or the connection closes. + +- the server is configured with wasi:config variables, e.g. `-Sconfig-var=hostname=valkey.example.com -Sconfig-var=port=6380`, see [`tcp-connector`](./components/tcp-connector/) for the keys and their defaults - `-Sallow-ip-name-lookup` is only required if a hostname is used for the connection instead of an IP address. To aid incremental development a make target is available to rebuild and run the CLI: diff --git a/components/admin-commands/Cargo.toml b/components/admin-commands/Cargo.toml deleted file mode 100644 index c267832..0000000 --- a/components/admin-commands/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "admin-commands" -version = "0.1.0" -edition = "2024" -license = "Apache-2.0" - -[lib] -crate-type = ["cdylib"] -# shared with the other command components -path = "../../crates/commands/src/lib.rs" - -[features] -default = ["admin"] -admin = [] -connection = [] -hash = [] -keyspace = [] -pubsub = [] -string = [] - -[dependencies] -resp-codec = { workspace = true } -wit-bindgen = { workspace = true } diff --git a/components/admin-commands/README.md b/components/admin-commands/README.md deleted file mode 100644 index 8e7532a..0000000 --- a/components/admin-commands/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# admin-commands - -Administrative commands, Valkey's @admin ACL category, and the rest of the ACL family. - -Exports `componentized:valkey/admin-commands` over an imported [`connection`](../connection/). [`commands`](../commands/) exports every command interface in one component. diff --git a/components/as-keyvalue/src/lib.rs b/components/as-keyvalue/src/lib.rs index ae1e4ce..0a3ea0b 100644 --- a/components/as-keyvalue/src/lib.rs +++ b/components/as-keyvalue/src/lib.rs @@ -1,6 +1,6 @@ +use componentized::valkey::commands::{self, Duration}; use componentized::valkey::connection::{Connection, Error as ValkeyError}; use componentized::valkey::connector; -use componentized::valkey::hash_commands::{self, Duration, HscanOpts}; use exports::wasmcloud::keyvalue::atomics::Guest as AtomicsGuest; use exports::wasmcloud::keyvalue::batch::Guest as BatchGuest; use exports::wasmcloud::keyvalue::store::Guest as StoreGuest; @@ -43,7 +43,7 @@ struct KeyvalueToValkeyBucket { impl GuestBucket for KeyvalueToValkeyBucket { async fn get(&self, key: String) -> Result>, Error> { - match hash_commands::hget(&self.connection, self.hash_key.clone(), key).await? { + match commands::hget(&self.connection, self.hash_key.clone(), key).await? { Some(value) => Ok(Some(value.into_bytes())), None => Ok(None), } @@ -62,19 +62,24 @@ impl GuestBucket for KeyvalueToValkeyBucket { }); if options.if_not_exists { - match hash_commands::hsetnx(&self.connection, self.hash_key.clone(), key.clone(), value) + match commands::hsetnx(&self.connection, self.hash_key.clone(), key.clone(), value) .await? { true => (), false => Err(Error::PreconditionFailed)?, } } else { - hash_commands::hset(&self.connection, self.hash_key.clone(), key.clone(), value).await? + commands::hset( + &self.connection, + self.hash_key.clone(), + vec![(key.clone(), value)], + ) + .await?; } if let Some(ttl_ms) = options.ttl_ms { // HEXPIRE has whole second precision, rounded up so a short ttl does not expire at once let ttl: Duration = ttl_ms.div_ceil(1000) * 1_000_000_000; - hash_commands::hexpire( + commands::hexpire( &self.connection, self.hash_key.clone(), ttl, @@ -87,11 +92,12 @@ impl GuestBucket for KeyvalueToValkeyBucket { } async fn delete(&self, key: String) -> Result<(), Error> { - Ok(hash_commands::hdel(&self.connection, self.hash_key.clone(), key).await?) + commands::hdel(&self.connection, self.hash_key.clone(), vec![key]).await?; + Ok(()) } async fn exists(&self, key: String) -> Result { - Ok(hash_commands::hexists(&self.connection, self.hash_key.clone(), key).await?) + Ok(commands::hexists(&self.connection, self.hash_key.clone(), key).await?) } async fn list_keys( @@ -99,18 +105,21 @@ impl GuestBucket for KeyvalueToValkeyBucket { prefix: Option, cursor: Option, ) -> Result { - let opts = HscanOpts { - match_: prefix.map(|prefix| format!("{}*", escape_glob(&prefix))), - count: None, - no_values: Some(true), - }; - let (cursor, fields) = - hash_commands::hscan(&self.connection, self.hash_key.clone(), cursor, Some(opts)) - .await?; + let match_ = prefix.map(|prefix| format!("{}*", escape_glob(&prefix))); + let count = None; + + let (cursor, fields) = commands::hscan_novalues( + &self.connection, + self.hash_key.clone(), + cursor, + match_, + count, + ) + .await?; Ok(KeyResponse { keys: fields.into_iter().map(|(key, _)| key).collect(), - cursor, + cursor: Some(cursor.to_string()), }) } } @@ -119,7 +128,7 @@ impl AtomicsGuest for AsKeyvalue { async fn increment(bucket: BucketBorrow<'_>, key: String, delta: i64) -> Result { let bucket: &KeyvalueToValkeyBucket = bucket.get(); - Ok(hash_commands::hincrby(&bucket.connection, bucket.hash_key.clone(), key, delta).await?) + Ok(commands::hincrby(&bucket.connection, bucket.hash_key.clone(), key, delta).await?) } } diff --git a/components/cli/README.md b/components/cli/README.md index 05a41eb..3c4c108 100644 --- a/components/cli/README.md +++ b/components/cli/README.md @@ -1,3 +1,3 @@ # Valkey CLI -A wasi:cli/command exposing valkey commands. +A wasi:cli/command sending its arguments as a command to Valkey, composed of [`dep-cli`](../dep-cli/) and a [`tcp-connector`](../tcp-connector/) configured with wasi:config. diff --git a/components/cli/cli.wac b/components/cli/cli.wac index caf4d6d..9341892 100644 --- a/components/cli/cli.wac +++ b/components/cli/cli.wac @@ -10,21 +10,16 @@ let tcp = new local:tcp { "componentized:valkey/connection@0.3.0-dev": connection.connection, ... }; -let commands = new local:commands { +let connector = new local:tcp-connector { "componentized:valkey/resp@0.3.0-dev": resp.resp, "componentized:valkey/connection@0.3.0-dev": connection.connection, + "componentized:valkey/tcp@0.3.0-dev": tcp.tcp, ... }; let cli = new local:dep-cli { "componentized:valkey/resp@0.3.0-dev": resp.resp, "componentized:valkey/connection@0.3.0-dev": connection.connection, - "componentized:valkey/tcp@0.3.0-dev": tcp.tcp, - "componentized:valkey/admin-commands@0.3.0-dev": commands["componentized:valkey/admin-commands@0.3.0-dev"], - "componentized:valkey/connection-commands@0.3.0-dev": commands["componentized:valkey/connection-commands@0.3.0-dev"], - "componentized:valkey/hash-commands@0.3.0-dev": commands["componentized:valkey/hash-commands@0.3.0-dev"], - "componentized:valkey/keyspace-commands@0.3.0-dev": commands["componentized:valkey/keyspace-commands@0.3.0-dev"], - "componentized:valkey/pubsub-commands@0.3.0-dev": commands["componentized:valkey/pubsub-commands@0.3.0-dev"], - "componentized:valkey/string-commands@0.3.0-dev": commands["componentized:valkey/string-commands@0.3.0-dev"], + "componentized:valkey/connector@0.3.0-dev": connector.connector, ... }; diff --git a/components/commands/Cargo.toml b/components/commands/Cargo.toml index 60cb9d7..d0e6fcb 100644 --- a/components/commands/Cargo.toml +++ b/components/commands/Cargo.toml @@ -6,17 +6,6 @@ license = "Apache-2.0" [lib] crate-type = ["cdylib"] -# shared with the other command components -path = "../../crates/commands/src/lib.rs" - -[features] -default = ["admin", "connection", "hash", "keyspace", "pubsub", "string"] -admin = [] -connection = [] -hash = [] -keyspace = [] -pubsub = [] -string = [] [dependencies] resp-codec = { workspace = true } diff --git a/components/commands/src/command.rs b/components/commands/src/command.rs new file mode 100644 index 0000000..e394fb2 --- /dev/null +++ b/components/commands/src/command.rs @@ -0,0 +1,415 @@ +use crate::exports::componentized::valkey::commands::{ + ChannelRule, CommandRule, Connection, DbRule, Duration, Error, GetexTtl, Instant, KeyRule, + MsetexPrecondition, Rule, RuleSelector, SetPrecondition, SetTtl, StreamTrim, + StreamTrimEfficiency, TtlMode, UserRule, Value, XclaimOpts, +}; +use crate::value::{FromValue, OrZeroId, ZERO_ID}; + +/// Send a command, its words as bulk strings, and wait for the reply. +/// +/// An error reply fails the command, see `connection.call`. +async fn call(connection: &Connection, command: impl IntoCommand) -> Result { + connection.call(bulk_strings(command)).await +} + +/// Send a command, its words as bulk strings, without waiting for a reply. +/// +/// An error reply fails the command, see `connection.send`. +pub(crate) async fn send(connection: &Connection, command: impl IntoCommand) -> Result<(), Error> { + connection.send(bulk_strings(command)).await +} + +/// Send a command, see `call`, and convert its reply. +pub(crate) async fn query( + connection: &Connection, + command: impl IntoCommand, +) -> Result { + T::from_value(call(connection, command).await?) +} + +fn bulk_strings(command: impl IntoCommand) -> Vec { + command + .into_command() + .into_iter() + .map(Value::BulkString) + .collect() +} + +/// Converts a Rust type into the words of a command, e.g. `Option` into no word, or one. +/// +/// The impls compose, a tuple is the words of each of its items in order, so a command is written +/// as one value, e.g. `("SET", key, value, precondition, ttl)`. +pub trait IntoCommand { + fn into_command(self) -> Vec; +} + +impl IntoCommand for &str { + fn into_command(self) -> Vec { + vec![self.to_string()] + } +} + +impl IntoCommand for String { + fn into_command(self) -> Vec { + vec![self] + } +} + +impl IntoCommand for &String { + fn into_command(self) -> Vec { + vec![self.clone()] + } +} + +/// Numbers, without a unit, a `Duration` is a `u64` of nanoseconds, see `Seconds` and +/// `Milliseconds`. +macro_rules! into_command_numbers { + ($($number:ty),*) => { + $( + impl IntoCommand for $number { + fn into_command(self) -> Vec { + vec![self.to_string()] + } + } + )* + }; +} + +into_command_numbers!(i32, i64, u8, u32, u64, usize, f64); + +/// No word for none. +impl IntoCommand for Option { + fn into_command(self) -> Vec { + self.map(IntoCommand::into_command).unwrap_or_default() + } +} + +impl IntoCommand for Vec { + fn into_command(self) -> Vec { + self.into_iter() + .flat_map(IntoCommand::into_command) + .collect() + } +} + +impl IntoCommand for [T; N] { + fn into_command(self) -> Vec { + self.into_iter() + .flat_map(IntoCommand::into_command) + .collect() + } +} + +/// The words of each item, in order. +macro_rules! into_command_tuples { + ($(($($item:ident),+)),*) => { + $( + impl<$($item: IntoCommand),+> IntoCommand for ($($item,)+) { + #[allow(non_snake_case)] + fn into_command(self) -> Vec { + let ($($item,)+) = self; + let mut command = vec![]; + $(command.extend($item.into_command());)+ + command + } + } + )* + }; +} + +into_command_tuples!( + (A), + (A, B), + (A, B, C), + (A, B, C, D), + (A, B, C, D, E), + (A, B, C, D, E, F), + (A, B, C, D, E, F, G), + (A, B, C, D, E, F, G, H), + (A, B, C, D, E, F, G, H, I), + (A, B, C, D, E, F, G, H, I, J) +); + +/// A word that is present when true, e.g. `Flag("WITHMATCHLEN", with_match_len)`. +/// +/// A `bool` has no impl of its own, the word it stands for depends on the command. +pub struct Flag(pub &'static str, pub bool); + +impl IntoCommand for Flag { + fn into_command(self) -> Vec { + let Flag(word, present) = self; + if present { + vec![word.to_string()] + } else { + vec![] + } + } +} + +pub struct NamedOption(pub &'static str, pub Option); + +impl IntoCommand for NamedOption { + fn into_command(self) -> Vec { + let NamedOption(name, value) = self; + match value { + Some(_) => (name, value).into_command(), + None => vec![], + } + } +} + +/// `0-0` for none, e.g. to start XAUTOCLAIM from the beginning. +impl IntoCommand for OrZeroId { + fn into_command(self) -> Vec { + vec![self.0.unwrap_or_else(|| ZERO_ID.to_string())] + } +} + +/// A duration or instant in whole seconds, e.g. for EX and EXAT. +pub struct Seconds(pub T); + +/// A duration or instant in whole milliseconds, e.g. for PX and PXAT. +pub struct Milliseconds(pub T); + +impl IntoCommand for Seconds { + fn into_command(self) -> Vec { + vec![duration_seconds(self.0)] + } +} + +impl IntoCommand for Milliseconds { + fn into_command(self) -> Vec { + vec![duration_milliseconds(self.0)] + } +} + +impl IntoCommand for Seconds { + fn into_command(self) -> Vec { + vec![instant_seconds(self.0)] + } +} + +impl IntoCommand for Milliseconds { + fn into_command(self) -> Vec { + vec![instant_milliseconds(self.0)] + } +} + +impl IntoCommand for Rule { + fn into_command(self) -> Vec { + match self { + Rule::Keys(rule) => rule.into_command(), + Rule::Channel(rule) => rule.into_command(), + Rule::Command(rule) => rule.into_command(), + Rule::Db(rule) => rule.into_command(), + Rule::User(rule) => rule.into_command(), + } + } +} + +impl IntoCommand for RuleSelector { + fn into_command(self) -> Vec { + match self { + RuleSelector::Keys(rule) => rule.into_command(), + RuleSelector::Channel(rule) => rule.into_command(), + RuleSelector::Command(rule) => rule.into_command(), + RuleSelector::Db(rule) => rule.into_command(), + } + } +} + +impl IntoCommand for KeyRule { + fn into_command(self) -> Vec { + vec![match self { + KeyRule::Add(pattern) => format!("~{pattern}"), + KeyRule::AddRead(pattern) => format!("%R~{pattern}"), + KeyRule::AddWrite(pattern) => format!("%W~{pattern}"), + KeyRule::AddReadWrite(pattern) => format!("%RW~{pattern}"), + KeyRule::All => "allkeys".to_string(), + KeyRule::Reset => "resetkeys".to_string(), + }] + } +} + +impl IntoCommand for ChannelRule { + fn into_command(self) -> Vec { + vec![match self { + ChannelRule::Add(pattern) => format!("&{pattern}"), + ChannelRule::All => "allchannels".to_string(), + ChannelRule::Reset => "resetchannels".to_string(), + }] + } +} + +impl IntoCommand for CommandRule { + fn into_command(self) -> Vec { + vec![match self { + CommandRule::Add(command) => format!("+{command}"), + CommandRule::AddCategory(category) => format!("+@{category}"), + CommandRule::AddFirstArg((command, arg)) => format!("+{command}|{arg}"), + CommandRule::All => "allcommands".to_string(), + CommandRule::Remove(command) => format!("-{command}"), + CommandRule::RemoveCategory(category) => format!("-@{category}"), + CommandRule::Reset => "nocommands".to_string(), + }] + } +} + +impl IntoCommand for DbRule { + fn into_command(self) -> Vec { + vec![match self { + DbRule::Db(dbs) => format!( + "db={}", + dbs.iter() + .map(|db| db.to_string()) + .collect::>() + .join(",") + ), + DbRule::All => "alldbs".to_string(), + DbRule::Reset => "resetdbs".to_string(), + }] + } +} + +impl IntoCommand for UserRule { + fn into_command(self) -> Vec { + match self { + UserRule::On => "on".into_command(), + UserRule::Off => "off".into_command(), + UserRule::Nopass => "nopass".into_command(), + UserRule::Password(password) => format!(">{password}").into_command(), + UserRule::HashedPassword(hash) => format!("#{hash}").into_command(), + UserRule::RemovePassword(password) => format!("<{password}").into_command(), + UserRule::RemoveHashedPassword(hash) => format!("!{hash}").into_command(), + UserRule::Selector(rules) => ("(", rules, ")").into_command(), + UserRule::Clearselectors => "clearselectors".into_command(), + UserRule::Reset => "reset".into_command(), + } + } +} + +impl IntoCommand for TtlMode { + fn into_command(self) -> Vec { + vec![ + match self { + TtlMode::Nx => "NX", + TtlMode::Xx => "XX", + TtlMode::Gt => "GT", + TtlMode::Lt => "LT", + } + .to_string(), + ] + } +} + +impl IntoCommand for GetexTtl { + fn into_command(self) -> Vec { + match self { + GetexTtl::Ex(duration) => ("EX", Seconds(duration)).into_command(), + GetexTtl::Px(duration) => ("PX", Milliseconds(duration)).into_command(), + GetexTtl::Exat(instant) => ("EXAT", Seconds(instant)).into_command(), + GetexTtl::Pxat(instant) => ("PXAT", Milliseconds(instant)).into_command(), + GetexTtl::Persist => "PERSIST".into_command(), + } + } +} + +/// Also `MsetexTtl`, the same type. +impl IntoCommand for SetTtl { + fn into_command(self) -> Vec { + match self { + SetTtl::Ex(duration) => ("EX", Seconds(duration)).into_command(), + SetTtl::Px(duration) => ("PX", Milliseconds(duration)).into_command(), + SetTtl::Exat(instant) => ("EXAT", Seconds(instant)).into_command(), + SetTtl::Pxat(instant) => ("PXAT", Milliseconds(instant)).into_command(), + SetTtl::Keepttl => "KEEPTTL".into_command(), + } + } +} + +impl IntoCommand for SetPrecondition { + fn into_command(self) -> Vec { + match self { + SetPrecondition::Nx => "NX".into_command(), + SetPrecondition::Xx => "XX".into_command(), + SetPrecondition::Ifeq(value) => ("IFEQ", value).into_command(), + SetPrecondition::Ifne(value) => ("IFNE", value).into_command(), + } + } +} + +impl IntoCommand for MsetexPrecondition { + fn into_command(self) -> Vec { + match self { + MsetexPrecondition::Nx => "NX".into_command(), + MsetexPrecondition::Xx => "XX".into_command(), + } + } +} + +impl IntoCommand for StreamTrim { + fn into_command(self) -> Vec { + //〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] + match self { + StreamTrim::Maxlen(trim) => ( + "MAXLEN", + trim.efficiency, + trim.threshold, + trim.limit.map(|l| ("LIMIT", l).into_command()), + ) + .into_command(), + StreamTrim::Minid(trim) => ( + "MINID", + trim.efficiency, + trim.threshold, + trim.limit.map(|l| ("LIMIT", l).into_command()), + ) + .into_command(), + } + } +} + +impl IntoCommand for StreamTrimEfficiency { + fn into_command(self) -> Vec { + // = | ~ + (match self { + StreamTrimEfficiency::Exact => "=", + StreamTrimEfficiency::MearlyExact => "~", + }) + .into_command() + } +} + +/// A duration, in nanoseconds, as whole seconds, the precision of EX. +pub fn duration_seconds(nanoseconds: u64) -> String { + nanoseconds.div_ceil(1_000_000_000).to_string() +} + +/// A duration, in nanoseconds, as whole milliseconds, the precision of PX. +pub fn duration_milliseconds(nanoseconds: u64) -> String { + nanoseconds.div_ceil(1_000_000).to_string() +} + +/// An instant as seconds since the unix epoch, the precision of EXAT. +pub fn instant_seconds(instant: Instant) -> String { + (instant.seconds + instant.nanoseconds.div_ceil(1_000_000_000) as i64).to_string() +} + +/// An instant as milliseconds since the unix epoch, the precision of PXAT. +pub fn instant_milliseconds(instant: Instant) -> String { + ((instant.seconds * 1_000) + instant.nanoseconds.div_ceil(1_000_000) as i64).to_string() +} + +impl IntoCommand for XclaimOpts { + fn into_command(self) -> Vec { + // [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] [ LASTID lastid ] + ( + NamedOption("IDLE", self.idle.map(Milliseconds)), + NamedOption("TIME", self.time.map(Milliseconds)), + NamedOption("RETRYCOUNT", self.retrycount), + Flag("FORCE", self.force), + NamedOption("LASTID", self.lastid), + ) + .into_command() + } +} diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs new file mode 100644 index 0000000..b3dd4bb --- /dev/null +++ b/components/commands/src/lib.rs @@ -0,0 +1,1494 @@ +use crate::command::{Flag, Milliseconds, NamedOption, Seconds, query, send}; +use crate::exports::componentized::valkey::commands::{ + Connection, Cursor, Double, Duration, Error, GetexTtl, Guest, Instant, Integer, LcsIdxReply, + MsetexPrecondition, MsetexTtl, Rule, SetPrecondition, SetTtl, StreamId, StreamTrim, TtlMode, + TtlResult, Value, XclaimOpts, XinfoConsumer, XinfoGroup, XinfoStreamReply, + XinfoStreamReplyFull, +}; +use crate::value::OrZeroId; + +use wit_bindgen::rt::Map; + +mod command; +mod value; + +struct Commands; + +impl Guest for Commands { + #[doc = " admin commands"] + #[doc = " Delete all the specified ACL users and terminate all the connections that are authenticated with such users."] + #[doc = ""] + #[doc = " ACL DELUSER username [ username ... ]"] + #[doc = " https://valkey.io/commands/acl-deluser/"] + #[allow(async_fn_in_trait)] + async fn acl_deluser( + connection: &Connection, + usernames: Vec, + ) -> Result { + query(connection, ("ACL", "DELUSER", usernames)).await + } + + #[doc = " Generates a random password."] + #[doc = ""] + #[doc = " ACL GENPASS [ bits ]"] + #[doc = " https://valkey.io/commands/acl-genpass/"] + #[allow(async_fn_in_trait)] + async fn acl_genpass(connection: &Connection, bits: Option) -> Result { + query(connection, ("ACL", "GENPASS", bits)).await + } + + #[doc = " Create an ACL user with the specified rules or modify the rules of an existing user."] + #[doc = ""] + #[doc = " ACL SETUSER username [ rule ] [ [ rule ] ... ]"] + #[doc = " https://valkey.io/commands/acl-setuser/"] + #[allow(async_fn_in_trait)] + async fn acl_setuser( + connection: &Connection, + username: String, + rules: Vec, + ) -> Result<(), Error> { + query(connection, ("ACL", "SETUSER", username, rules)).await + } + + #[doc = " Authenticates the current connection."] + #[doc = ""] + #[doc = " AUTH [ username ] password"] + #[doc = " https://valkey.io/commands/auth/"] + #[allow(async_fn_in_trait)] + async fn auth( + connection: &Connection, + username: Option, + password: String, + ) -> Result<(), Error> { + query(connection, ("AUTH", username, password)).await + } + + #[doc = " Switch to a different protocol, optionally authenticating and setting the connection\'s name, or provide a contextual client report."] + #[doc = ""] + #[doc = " RESP2 is not fully supported by this client."] + #[doc = ""] + #[doc = " HELLO 3 [ AUTH username password ] [ SETNAME clientname ]"] + #[doc = " https://valkey.io/commands/hello/"] + #[allow(async_fn_in_trait)] + async fn hello( + connection: &Connection, + auth: Option<(String, String)>, + client_name: Option, + ) -> Result, Error> { + query( + connection, + ( + "HELLO", + "3", + auth.map(|auth| ("AUTH", auth)), + client_name.map(|name| ("SETNAME", name)), + ), + ) + .await + } + + #[doc = " Returns PONG if no argument is provided, otherwise return a copy of the argument as a bulk."] + #[doc = ""] + #[doc = " PING [ message ]"] + #[doc = " https://valkey.io/commands/ping/"] + #[allow(async_fn_in_trait)] + async fn ping(connection: &Connection, message: Option) -> Result<(), Error> { + let reply: String = query(connection, ("PING", message.clone())).await?; + if reply == message.unwrap_or("PONG".to_string()) { + Ok(()) + } else { + Err(Error::Client(format!("unexpected reply: {reply:?}"))) + } + } + + #[doc = " Removes the specified fields from the hash stored at key."] + #[doc = ""] + #[doc = " HDEL key field [ field ... ]"] + #[doc = " https://valkey.io/commands/hdel/"] + #[allow(async_fn_in_trait)] + async fn hdel( + connection: &Connection, + key: String, + fields: Vec, + ) -> Result { + query(connection, ("HDEL", key, fields)).await + } + + #[doc = " Returns if field is an existing field in the hash stored at key."] + #[doc = ""] + #[doc = " HEXISTS key field"] + #[doc = " https://valkey.io/commands/hexists/"] + #[allow(async_fn_in_trait)] + async fn hexists(connection: &Connection, key: String, field: String) -> Result { + query(connection, ("HEXISTS", key, field)).await + } + + #[doc = " Manipulate the expiration time for existing hash fields."] + #[doc = ""] + #[doc = " HEXPIRE key seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ]"] + #[doc = " https://valkey.io/commands/hexpire/"] + #[allow(async_fn_in_trait)] + async fn hexpire( + connection: &Connection, + key: String, + seconds: Duration, + mode: Option, + fields: Vec, + ) -> Result, Error> { + query( + connection, + ( + "HEXPIRE", + key, + Seconds(seconds), + mode, + "FIELDS", + fields.len(), + fields, + ), + ) + .await + } + + #[doc = " Manipulate the expiration time for existing hash fields."] + #[doc = ""] + #[doc = " HEXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ]"] + #[doc = " https://valkey.io/commands/hexpireat/"] + #[allow(async_fn_in_trait)] + async fn hexpireat( + connection: &Connection, + key: String, + seconds: Instant, + mode: Option, + fields: Vec, + ) -> Result, Error> { + query( + connection, + ( + "HEXPIREAT", + key, + Seconds(seconds), + mode, + "FIELDS", + fields.len(), + fields, + ), + ) + .await + } + + #[doc = " Returns the value associated with field in the hash stored at key."] + #[doc = ""] + #[doc = " HGET key field"] + #[doc = " https://valkey.io/commands/hget/"] + #[allow(async_fn_in_trait)] + async fn hget( + connection: &Connection, + key: String, + field: String, + ) -> Result, Error> { + query(connection, ("HGET", key, field)).await + } + + #[doc = " Returns all fields and values of the hash stored at key."] + #[doc = ""] + #[doc = " HGETALL key"] + #[doc = " https://valkey.io/commands/hgetall/"] + #[allow(async_fn_in_trait)] + async fn hgetall(connection: &Connection, key: String) -> Result, Error> { + query(connection, ("HGETALL", key)).await + } + + #[doc = " Increments the number stored at field in the hash stored at key by increment."] + #[doc = ""] + #[doc = " HINCRBY key field increment"] + #[doc = " https://valkey.io/commands/hincrby/"] + #[allow(async_fn_in_trait)] + async fn hincrby( + connection: &Connection, + key: String, + field: String, + increment: Integer, + ) -> Result { + query(connection, ("HINCRBY", key, field, increment)).await + } + + #[doc = " Increment the specified field of a hash stored at key, and representing a floating point number, by the specified increment."] + #[doc = ""] + #[doc = " HINCRBYFLOAT key field increment"] + #[doc = " https://valkey.io/commands/hincrbyfloat/"] + #[allow(async_fn_in_trait)] + async fn hincrbyfloat( + connection: &Connection, + key: String, + field: String, + increment: Double, + ) -> Result { + query(connection, ("HINCRBYFLOAT", key, field, increment)).await + } + + #[doc = " Returns all field names in the hash stored at key."] + #[doc = ""] + #[doc = " HKEYS key"] + #[doc = " https://valkey.io/commands/hkeys/"] + #[allow(async_fn_in_trait)] + async fn hkeys(connection: &Connection, key: String) -> Result, Error> { + query(connection, ("HKEYS", key)).await + } + + #[doc = " Returns the number of fields contained in the hash stored at key."] + #[doc = ""] + #[doc = " HLEN key"] + #[doc = " https://valkey.io/commands/hlen/"] + #[allow(async_fn_in_trait)] + async fn hlen(connection: &Connection, key: String) -> Result { + query(connection, ("HLEN", key)).await + } + + #[doc = " Returns the values associated with the specified fields in the hash stored at key."] + #[doc = ""] + #[doc = " HMGET key field [ field ... ]"] + #[doc = " https://valkey.io/commands/hmget/"] + #[allow(async_fn_in_trait)] + async fn hmget( + connection: &Connection, + key: String, + fields: Vec, + ) -> Result>, Error> { + query(connection, ("HMGET", key, fields)).await + } + + #[doc = " Sets the specified fields to their respective values in the hash stored at key."] + #[doc = ""] + #[doc = " HMSET key field value [ field value ... ]"] + #[doc = " https://valkey.io/commands/hmset/"] + #[allow(async_fn_in_trait)] + async fn hmset( + connection: &Connection, + key: String, + field_and_values: Vec<(String, String)>, + ) -> Result<(), Error> { + query(connection, ("HMSET", key, field_and_values)).await + } + + #[doc = " A random field from the hash value stored at key."] + #[doc = ""] + #[doc = " Also see `hrandfield-count` and `hrandfield-count-withvalues`."] + #[doc = ""] + #[doc = " HRANDFIELD key"] + #[doc = " https://valkey.io/commands/hrandfield/"] + #[allow(async_fn_in_trait)] + async fn hrandfield(connection: &Connection, key: String) -> Result, Error> { + query(connection, ("HRANDFIELD", key)).await + } + + #[doc = " A random set of fields from the hash value stored at key up to the count."] + #[doc = ""] + #[doc = " Also see `hrandfield` and `hrandfield-count-withvalues`."] + #[doc = ""] + #[doc = " HRANDFIELD key count"] + #[doc = " https://valkey.io/commands/hrandfield/"] + #[allow(async_fn_in_trait)] + async fn hrandfield_count( + connection: &Connection, + key: String, + count: Integer, + ) -> Result, Error> { + query(connection, ("HRANDFIELD", key, count)).await + } + + #[doc = " A random map of fields and values from the hash value stored at key up to the count."] + #[doc = ""] + #[doc = " Also see `hrandfield` and `hrandfield-count`."] + #[doc = ""] + #[doc = " HRANDFIELD key count WITHVALUES"] + #[doc = " https://valkey.io/commands/hrandfield/"] + #[allow(async_fn_in_trait)] + async fn hrandfield_count_withvalues( + connection: &Connection, + key: String, + count: Integer, + ) -> Result, Error> { + query(connection, ("HRANDFIELD", key, count, "WITHVALUES")).await + } + + #[doc = " Incrementally iterate over a collection of elements."] + #[doc = ""] + #[doc = " See also `hscan-novalues`."] + #[doc = ""] + #[doc = " HSCAN key cursor [ MATCH pattern ] [ COUNT count ]"] + #[doc = " https://valkey.io/commands/hscan/"] + #[allow(async_fn_in_trait)] + async fn hscan( + connection: &Connection, + key: String, + cursor: Option, + match_: Option, + count: Option, + ) -> Result<(Cursor, Vec), Error> { + query( + connection, + ( + "HSCAN", + key, + cursor.unwrap_or(Cursor::from("0")), + match_.map(|m| ("MATCH", m)), + count.map(|c| ("COUNT", c)), + ), + ) + .await + } + + #[doc = " Incrementally iterate over a collection of keys."] + #[doc = ""] + #[doc = " See also `hscan`."] + #[doc = ""] + #[doc = " HSCAN key cursor [ MATCH pattern ] [ COUNT count ] NOVALUES"] + #[doc = " https://valkey.io/commands/hscan/"] + #[allow(async_fn_in_trait)] + async fn hscan_novalues( + connection: &Connection, + key: String, + cursor: Option, + match_: Option, + count: Option, + ) -> Result<(Cursor, Map), Error> { + query( + connection, + ( + "HSCAN", + key, + cursor.unwrap_or(Cursor::from("0")), + match_.map(|m| ("MATCH", m)), + count.map(|c| ("COUNT", c)), + "NOVALUES", + ), + ) + .await + } + + #[doc = " Sets the specified fields to their respective values in the hash stored at key."] + #[doc = ""] + #[doc = " HSET key field value [ field value ... ]"] + #[doc = " https://valkey.io/commands/hset/"] + #[allow(async_fn_in_trait)] + async fn hset( + connection: &Connection, + key: String, + field_and_values: Vec<(String, String)>, + ) -> Result { + query(connection, ("HSET", key, field_and_values)).await + } + + #[doc = " Sets field in the hash stored at key to value, only if field does not yet exist."] + #[doc = ""] + #[doc = " HSETNX key field value"] + #[doc = " https://valkey.io/commands/hsetnx/"] + #[allow(async_fn_in_trait)] + async fn hsetnx( + connection: &Connection, + key: String, + field: String, + value: String, + ) -> Result { + query(connection, ("HSETNX", key, field, value)).await + } + + #[doc = " Returns the string length of the value associated with field in the hash stored at key."] + #[doc = ""] + #[doc = " HSTRLEN key field"] + #[doc = " https://valkey.io/commands/hstrlen/"] + #[allow(async_fn_in_trait)] + async fn hstrlen( + connection: &Connection, + key: String, + field: String, + ) -> Result { + query(connection, ("HSTRLEN", key, field)).await + } + + #[doc = " All values in the hash stored at key."] + #[doc = ""] + #[doc = " HVALS key"] + #[doc = " https://valkey.io/commands/hvals/"] + #[allow(async_fn_in_trait)] + async fn hvals(connection: &Connection, key: String) -> Result, Error> { + query(connection, ("HVALS", key)).await + } + + #[doc = " Removes the specified keys."] + #[doc = ""] + #[doc = " DEL key [ key ... ]"] + #[doc = " https://valkey.io/commands/del/"] + #[allow(async_fn_in_trait)] + async fn del(connection: &Connection, keys: Vec) -> Result { + query(connection, ("DEL", keys)).await + } + + #[doc = " If key exists."] + #[doc = ""] + #[doc = " EXISTS key [ key ... ]"] + #[doc = " https://valkey.io/commands/exists/"] + #[allow(async_fn_in_trait)] + async fn exists(connection: &Connection, keys: Vec) -> Result { + query(connection, ("EXISTS", keys)).await + } + + #[doc = " Set a timeout on key."] + #[doc = ""] + #[doc = " EXPIRE key seconds [ NX | XX | GT | LT ]"] + #[doc = " https://valkey.io/commands/expire/"] + #[allow(async_fn_in_trait)] + async fn expire( + connection: &Connection, + key: String, + seconds: Duration, + mode: Option, + ) -> Result { + query(connection, ("EXPIRE", key, seconds, mode)).await + } + + #[doc = " Set a timeout on key."] + #[doc = ""] + #[doc = " EXPIREAT key unix-time-seconds [ NX | XX | GT | LT ]"] + #[doc = " https://valkey.io/commands/expireat/"] + #[allow(async_fn_in_trait)] + async fn expireat( + connection: &Connection, + key: String, + seconds: Instant, + mode: Option, + ) -> Result { + query(connection, ("EXPIREAT", key, Seconds(seconds), mode)).await + } + + #[doc = " Returns all keys matching pattern."] + #[doc = ""] + #[doc = " KEYS pattern"] + #[doc = " https://valkey.io/commands/keys/"] + #[allow(async_fn_in_trait)] + async fn keys(connection: &Connection, pattern: String) -> Result, Error> { + query(connection, ("KEYS", pattern)).await + } + + #[doc = " Subscribes the client to the given patterns."] + #[doc = ""] + #[doc = " PSUBSCRIBE pattern [ pattern ... ]"] + #[doc = " https://valkey.io/commands/psubscribe/"] + #[allow(async_fn_in_trait)] + async fn psubscribe(connection: &Connection, patterns: Vec) -> Result<(), Error> { + send(connection, ("PSUBSCRIBE", patterns)).await + } + + #[doc = " Posts a message to the given channel."] + #[doc = ""] + #[doc = " PUBLISH channel message"] + #[doc = " https://valkey.io/commands/publish/"] + #[allow(async_fn_in_trait)] + async fn publish( + connection: &Connection, + channel: String, + message: String, + ) -> Result { + query(connection, ("PUBLISH", channel, message)).await + } + + #[doc = " Lists the currently active channels."] + #[doc = ""] + #[doc = " PUBSUB CHANNELS [ pattern ]"] + #[doc = " https://valkey.io/commands/pubsub-channels/"] + #[allow(async_fn_in_trait)] + async fn pubsub_channels( + connection: &Connection, + pattern: Option, + ) -> Result, Error> { + query(connection, ("PUBSUB", "CHANNELS", pattern)).await + } + + #[doc = " Returns the number of unique patterns that are subscribed to by clients."] + #[doc = ""] + #[doc = " PUBSUB NUMPAT"] + #[doc = " https://valkey.io/commands/pubsub-numpat/"] + #[allow(async_fn_in_trait)] + async fn pubsub_numpat(connection: &Connection) -> Result { + query(connection, ("PUBSUB", "NUMPAT")).await + } + + #[doc = " Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels."] + #[doc = ""] + #[doc = " PUBSUB NUMSUB [ channel ] [ [ channel ] ... ]"] + #[doc = " https://valkey.io/commands/pubsub-numsub/"] + #[allow(async_fn_in_trait)] + async fn pubsub_numsub( + connection: &Connection, + channels: Vec, + ) -> Result, Error> { + query(connection, ("PUBSUB", "NUMSUB", channels)).await + } + + #[doc = " Lists the currently active shard channels."] + #[doc = ""] + #[doc = " PUBSUB SHARDCHANNELS [ pattern ]"] + #[doc = " https://valkey.io/commands/pubsub-shardchannels/"] + #[allow(async_fn_in_trait)] + async fn pubsub_shardchannels( + connection: &Connection, + pattern: Option, + ) -> Result, Error> { + query(connection, ("PUBSUB", "SHARDCHANNELS", pattern)).await + } + + #[doc = " Returns the number of subscribers for the specified shard channels."] + #[doc = ""] + #[doc = " PUBSUB SHARDNUMSUB [ shardchannel [ shardchannel ... ] ]"] + #[doc = " https://valkey.io/commands/pubsub-shardnumsub/"] + #[allow(async_fn_in_trait)] + async fn pubsub_shardnumsub( + connection: &Connection, + shardchannels: Vec, + ) -> Result, Error> { + query(connection, ("PUBSUB", "SHARDNUMSUB", shardchannels)).await + } + + #[doc = " Unsubscribes the client from the given patterns, or from all of them if none is given."] + #[doc = ""] + #[doc = " PUNSUBSCRIBE [ pattern [ pattern ... ] ]"] + #[doc = " https://valkey.io/commands/punsubscribe/"] + #[allow(async_fn_in_trait)] + async fn punsubscribe(connection: &Connection, patterns: Vec) -> Result<(), Error> { + send(connection, ("PUNSUBSCRIBE", patterns)).await + } + + #[doc = " Posts a message to the given shard channel."] + #[doc = ""] + #[doc = " SPUBLISH shardchannel message"] + #[doc = " https://valkey.io/commands/spublish/"] + #[allow(async_fn_in_trait)] + async fn spublish( + connection: &Connection, + shardchannel: String, + message: String, + ) -> Result { + query(connection, ("SPUBLISH", shardchannel, message)).await + } + + #[doc = " Subscribes the client to the specified shard channels."] + #[doc = ""] + #[doc = " SSUBSCRIBE shardchannel [ shardchannel ... ]"] + #[doc = " https://valkey.io/commands/ssubscribe/"] + #[allow(async_fn_in_trait)] + async fn ssubscribe(connection: &Connection, shardchannels: Vec) -> Result<(), Error> { + send(connection, ("SSUBSCRIBE", shardchannels)).await + } + + #[doc = " Subscribes the client to the specified channels."] + #[doc = ""] + #[doc = " SUBSCRIBE channel [ channel ... ]"] + #[doc = " https://valkey.io/commands/subscribe/"] + #[allow(async_fn_in_trait)] + async fn subscribe(connection: &Connection, channels: Vec) -> Result<(), Error> { + send(connection, ("SUBSCRIBE", channels)).await + } + + #[doc = " Unsubscribes the client from the given shard channels, or from all of them if none is given."] + #[doc = ""] + #[doc = " SUNSUBSCRIBE [ shardchannel ] [ [ shardchannel ] ... ]"] + #[doc = " https://valkey.io/commands/sunsubscribe/"] + #[allow(async_fn_in_trait)] + async fn sunsubscribe( + connection: &Connection, + shardchannels: Vec, + ) -> Result<(), Error> { + send(connection, ("SUNSUBSCRIBE", shardchannels)).await + } + + #[doc = " Unsubscribes the client from the given channels, or from all of them if none is given."] + #[doc = ""] + #[doc = " UNSUBSCRIBE [ channel ] [ [ channel ] ... ]"] + #[doc = " https://valkey.io/commands/unsubscribe/"] + #[allow(async_fn_in_trait)] + async fn unsubscribe(connection: &Connection, channels: Vec) -> Result<(), Error> { + send(connection, ("UNSUBSCRIBE", channels)).await + } + + #[doc = " Appends the value at the end of the string."] + #[doc = ""] + #[doc = " APPEND key value"] + #[doc = " https://valkey.io/commands/append/"] + #[allow(async_fn_in_trait)] + async fn append(connection: &Connection, key: String, value: String) -> Result { + query(connection, ("APPEND", key, value)).await + } + + #[doc = " Decrements the number stored at key by one."] + #[doc = ""] + #[doc = " DECR key"] + #[doc = " https://valkey.io/commands/decr/"] + #[allow(async_fn_in_trait)] + async fn decr(connection: &Connection, key: String) -> Result { + query(connection, ("DECR", key)).await + } + + #[doc = " Decrements the number stored at key by decrement."] + #[doc = ""] + #[doc = " DECRBY key decrement"] + #[doc = " https://valkey.io/commands/decrby/"] + #[allow(async_fn_in_trait)] + async fn decrby( + connection: &Connection, + key: String, + decrement: Integer, + ) -> Result { + query(connection, ("DECRBY", key, decrement)).await + } + + #[doc = " Deletes the key if its value matches the given string."] + #[doc = ""] + #[doc = " DELIFEQ key value"] + #[doc = " https://valkey.io/commands/delifeq/"] + #[allow(async_fn_in_trait)] + async fn delifeq(connection: &Connection, key: String, value: String) -> Result { + query(connection, ("DELIFEQ", key, value)).await + } + + #[doc = " Get the value of key."] + #[doc = ""] + #[doc = " GET key"] + #[doc = " https://valkey.io/commands/get/"] + #[allow(async_fn_in_trait)] + async fn get(connection: &Connection, key: String) -> Result, Error> { + query(connection, ("GET", key)).await + } + + #[doc = " Get the value of key and delete the key."] + #[doc = ""] + #[doc = " GETDEL key"] + #[doc = " https://valkey.io/commands/getdel/"] + #[allow(async_fn_in_trait)] + async fn getdel(connection: &Connection, key: String) -> Result, Error> { + query(connection, ("GETDEL", key)).await + } + + #[doc = " Get the value of key and optionally set its expiration."] + #[doc = ""] + #[doc = " GETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ]"] + #[doc = " https://valkey.io/commands/getex/"] + #[allow(async_fn_in_trait)] + async fn getex( + connection: &Connection, + key: String, + ttl: Option, + ) -> Result, Error> { + query(connection, ("GETEX", key, ttl)).await + } + + #[doc = " Returns the substring of the string value stored at key, determined by the offsets start and end."] + #[doc = ""] + #[doc = " GETRANGE key start end"] + #[doc = " https://valkey.io/commands/getrange/"] + #[allow(async_fn_in_trait)] + async fn getrange( + connection: &Connection, + key: String, + start: Integer, + end: Integer, + ) -> Result { + query(connection, ("GETRANGE", key, start, end)).await + } + + #[doc = " Atomically sets key to value and returns the old value stored at key."] + #[doc = ""] + #[doc = " GETSET key value"] + #[doc = " https://valkey.io/commands/getset/"] + #[allow(async_fn_in_trait)] + async fn getset( + connection: &Connection, + key: String, + value: String, + ) -> Result, Error> { + query(connection, ("GETSET", key, value)).await + } + + #[doc = " Increments the number stored at key by one."] + #[doc = ""] + #[doc = " INCR key"] + #[doc = " https://valkey.io/commands/incr/"] + #[allow(async_fn_in_trait)] + async fn incr(connection: &Connection, key: String) -> Result { + query(connection, ("INCR", key)).await + } + + #[doc = " Increments the number stored at key by increment."] + #[doc = ""] + #[doc = " INCRBY key increment"] + #[doc = " https://valkey.io/commands/incrby/"] + #[allow(async_fn_in_trait)] + async fn incrby( + connection: &Connection, + key: String, + increment: Integer, + ) -> Result { + query(connection, ("INCRBY", key, increment)).await + } + + #[doc = " Increment the string representing a floating point number stored at key by the specified increment."] + #[doc = ""] + #[doc = " INCRBYFLOAT key increment"] + #[doc = " https://valkey.io/commands/incrbyfloat/"] + #[allow(async_fn_in_trait)] + async fn incrbyfloat( + connection: &Connection, + key: String, + increment: Double, + ) -> Result { + query(connection, ("INCRBYFLOAT", key, increment)).await + } + + #[doc = " The LCS command implements the longest common subsequence algorithm."] + #[doc = ""] + #[doc = " See also `lcs-len` and `lcs-idx`"] + #[doc = ""] + #[doc = " LCS key1 key2"] + #[doc = " https://valkey.io/commands/lcs/"] + #[allow(async_fn_in_trait)] + async fn lcs(connection: &Connection, key1: String, key2: String) -> Result { + query(connection, ("LCS", key1, key2)).await + } + + #[doc = " The LCS command implements the longest common subsequence algorithm."] + #[doc = ""] + #[doc = " See also `lcs` and `lcs-idx`"] + #[doc = ""] + #[doc = " LCS key1 key2 LEN"] + #[doc = " https://valkey.io/commands/lcs/"] + #[allow(async_fn_in_trait)] + async fn lcs_len( + connection: &Connection, + key1: String, + key2: String, + ) -> Result { + query(connection, ("LCS", key1, key2, "LEN")).await + } + + #[doc = " The LCS command implements the longest common subsequence algorithm."] + #[doc = ""] + #[doc = " See also `lcs` and `lcs-len`"] + #[doc = ""] + #[doc = " LCS key1 key2 IDX [ MINMATCHLEN min-match-len ] [ WITHMATCHLEN ]"] + #[doc = " https://valkey.io/commands/lcs/"] + #[allow(async_fn_in_trait)] + async fn lcs_idx( + connection: &Connection, + key1: String, + key2: String, + min_match_len: Option, + with_match_len: bool, + ) -> Result { + query( + connection, + ( + "LCS", + key1, + key2, + "IDX", + min_match_len.map(|len| ("MINMATCHLEN", len)), + Flag("WITHMATCHLEN", with_match_len), + ), + ) + .await + } + + #[doc = " Returns the values of all specified keys."] + #[doc = ""] + #[doc = " MGET key [ key ... ]"] + #[doc = " https://valkey.io/commands/mget/"] + #[allow(async_fn_in_trait)] + async fn mget( + connection: &Connection, + keys: Vec, + ) -> Result>, Error> { + query(connection, ("MGET", keys)).await + } + + #[doc = " Sets the given keys to their respective values."] + #[doc = ""] + #[doc = " MSET key value [ key value ... ]"] + #[doc = " https://valkey.io/commands/mset/"] + #[allow(async_fn_in_trait)] + async fn mset( + connection: &Connection, + keys_and_values: Vec<(String, String)>, + ) -> Result<(), Error> { + query(connection, ("MSET", keys_and_values)).await + } + + #[doc = " Sets multiple key-value pairs while also assigning a shared expiration time to all the keys atomically."] + #[doc = ""] + #[doc = " MSETEX numkeys key value [ key value ... ] [ NX | XX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ]"] + #[doc = " https://valkey.io/commands/msetex/"] + #[allow(async_fn_in_trait)] + async fn msetex( + connection: &Connection, + keys_and_values: Vec<(String, String)>, + precondition: Option, + ttl: Option, + ) -> Result { + query( + connection, + ( + "MSETEX", + keys_and_values.len(), + keys_and_values, + precondition, + ttl, + ), + ) + .await + } + + #[doc = " Sets the given keys to their respective values."] + #[doc = ""] + #[doc = " MSETNX key value [ key value ... ]"] + #[doc = " https://valkey.io/commands/msetnx/"] + #[allow(async_fn_in_trait)] + async fn msetnx( + connection: &Connection, + keys_and_values: Vec<(String, String)>, + ) -> Result { + query(connection, ("MSETNX", keys_and_values)).await + } + + #[doc = " Set key to hold the string value with expiration in milliseconds."] + #[doc = ""] + #[doc = " PSETEX key milliseconds value"] + #[doc = " https://valkey.io/commands/psetex/"] + #[allow(async_fn_in_trait)] + async fn psetex( + connection: &Connection, + key: String, + milliseconds: Duration, + value: String, + ) -> Result<(), Error> { + query( + connection, + ("PSETEX", key, Milliseconds(milliseconds), value), + ) + .await + } + + #[doc = " Set key to hold the string value."] + #[doc = ""] + #[doc = " SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ]"] + #[doc = " https://valkey.io/commands/set/"] + #[allow(async_fn_in_trait)] + async fn set( + connection: &Connection, + key: String, + value: String, + precondition: Option, + ttl: Option, + ) -> Result<(), Error> { + query(connection, ("SET", key, value, precondition, ttl)).await + } + + #[doc = " Set key to hold the string value. Returning the previous value."] + #[doc = ""] + #[doc = " SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] GET [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ]"] + #[doc = " https://valkey.io/commands/set/"] + #[allow(async_fn_in_trait)] + async fn set_get( + connection: &Connection, + key: String, + value: String, + precondition: Option, + ttl: Option, + ) -> Result, Error> { + query(connection, ("SET", key, value, precondition, "GET", ttl)).await + } + + #[doc = " Set key to hold the string value with expiration in seconds."] + #[doc = ""] + #[doc = " SETEX key seconds value"] + #[doc = " https://valkey.io/commands/setex/"] + #[allow(async_fn_in_trait)] + async fn setex( + connection: &Connection, + key: String, + seconds: Duration, + value: String, + ) -> Result<(), Error> { + query(connection, ("SETEX", key, seconds, value)).await + } + + #[doc = " Set key to hold string value if key does not exist."] + #[doc = ""] + #[doc = " SETNX key value"] + #[doc = " https://valkey.io/commands/setnx/"] + #[allow(async_fn_in_trait)] + async fn setnx(connection: &Connection, key: String, value: String) -> Result { + query(connection, ("SETNX", key, value)).await + } + + #[doc = " Overwrites part of the string stored at key, starting at the specified offset, for the entire length of value."] + #[doc = ""] + #[doc = " SETRANGE key offset value"] + #[doc = " https://valkey.io/commands/setrange/"] + #[allow(async_fn_in_trait)] + async fn setrange( + connection: &Connection, + key: String, + offset: u32, + value: String, + ) -> Result { + query(connection, ("SETRANGE", key, offset, value)).await + } + + #[doc = " Returns the length of the string value stored at key."] + #[doc = ""] + #[doc = " STRLEN key"] + #[doc = " hthttps://valkey.io/commands/strlen/"] + #[allow(async_fn_in_trait)] + async fn strlen(connection: &Connection, key: String) -> Result { + query(connection, ("STRLEN", key)).await + } + + #[doc = " Returns the substring of the string value stored at key, determined by the offsets start and end."] + #[doc = ""] + #[doc = " SUBSTR key start end"] + #[doc = " https://valkey.io/commands/substr/"] + #[allow(async_fn_in_trait)] + async fn substr( + connection: &Connection, + key: String, + start: i32, + end: i32, + ) -> Result { + query(connection, ("SUBSTR", key, start, end)).await + } + + #[doc = " Removes one or multiple messages from the Pending Entries List (PEL) of a stream consumer group."] + #[doc = ""] + #[doc = " XACK key group ID [ ID ... ]"] + #[doc = " https://valkey.io/commands/xack/"] + #[allow(async_fn_in_trait)] + async fn xack( + connection: &Connection, + key: String, + group: String, + ids: Vec, + ) -> Result { + query(connection, ("XACK", key, group, ids)).await + } + + #[doc = " Appends the specified stream entry to the stream at the specified key."] + #[doc = ""] + #[doc = " XADD key [ NOMKSTREAM ] [ 〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] ] 〈 * | id 〉 field value [ field value ... ]"] + #[doc = " https://valkey.io/commands/xadd/"] + #[allow(async_fn_in_trait)] + async fn xadd( + connection: &Connection, + key: String, + nomkstream: bool, + trim: Option, + id: StreamId, + field_and_values: Vec<(String, String)>, + ) -> Result, Error> { + query( + connection, + ( + "XADD", + key, + Flag("NOMKSTREAM", nomkstream), + trim, + id, + field_and_values, + ), + ) + .await + } + + #[doc = " Transfers ownership of pending stream entries that match the specified criteria."] + #[doc = ""] + #[doc = " See also `xautoclaim-justid`"] + #[doc = ""] + #[doc = " XAUTOCLAIM key group consumer min-idle-time start [ COUNT count ]"] + #[doc = " https://valkey.io/commands/xautoclaim/"] + #[allow(async_fn_in_trait)] + async fn xautoclaim( + connection: &Connection, + key: String, + group: String, + consumer: String, + min_idle_time: Duration, + start: Option, + count: Option, + ) -> Result< + ( + Option, + Vec<(StreamId, Map)>, + Vec, + ), + Error, + > { + let (next, claimed, deleted): (OrZeroId, _, _) = query( + connection, + ( + "XAUTOCLAIM", + key, + group, + consumer, + Milliseconds(min_idle_time), + OrZeroId(start), + NamedOption("COUNT", count), + ), + ) + .await?; + Ok((next.0, claimed, deleted)) + } + + #[doc = " Transfers ownership of pending stream entries that match the specified criteria."] + #[doc = ""] + #[doc = " See also `xautoclaim`"] + #[doc = ""] + #[doc = " XAUTOCLAIM key group consumer min-idle-time start [ COUNT count ] JUSTID"] + #[doc = " https://valkey.io/commands/xautoclaim/"] + #[allow(async_fn_in_trait)] + async fn xautoclaim_justid( + connection: &Connection, + key: String, + group: String, + consumer: String, + min_idle_time: Duration, + start: Option, + count: Option, + ) -> Result<(Option, Vec, Vec), Error> { + let (next, claimed, deleted): (OrZeroId, _, _) = query( + connection, + ( + "XAUTOCLAIM", + key, + group, + consumer, + Milliseconds(min_idle_time), + OrZeroId(start), + NamedOption("COUNT", count), + "JUSTID", + ), + ) + .await?; + Ok((next.0, claimed, deleted)) + } + + #[doc = " Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument."] + #[doc = ""] + #[doc = " Also see `xclaim-justid`"] + #[doc = ""] + #[doc = " XCLAIM key group consumer min-idle-time ID [ ID ... ] [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] [ LASTID lastid ]"] + #[doc = " https://valkey.io/commands/xclaim/"] + #[allow(async_fn_in_trait)] + async fn xclaim( + connection: &Connection, + key: String, + group: String, + consumer: String, + min_idle_time: Duration, + ids: Vec, + opts: Option, + ) -> Result)>, Error> { + query( + connection, + ("XCLAIM", key, group, consumer, min_idle_time, ids, opts), + ) + .await + } + + #[doc = " Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument."] + #[doc = ""] + #[doc = " Also see `xclaim`"] + #[doc = ""] + #[doc = " XCLAIM key group consumer min-idle-time ID [ ID ... ] [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] JUSTID [ LASTID lastid ]"] + #[doc = " https://valkey.io/commands/xclaim/"] + #[allow(async_fn_in_trait)] + async fn xclaim_justid( + connection: &Connection, + key: String, + group: String, + consumer: String, + min_idle_time: Duration, + ids: Vec, + opts: Option, + ) -> Result, Error> { + query( + connection, + ("XCLAIM", key, group, consumer, min_idle_time, ids, opts), + ) + .await + } + + #[doc = " Removes the specified entries from a stream, and returns the number of entries deleted."] + #[doc = ""] + #[doc = " XDEL key ID [ ID ... ]"] + #[doc = " https://valkey.io/commands/xdel/"] + #[allow(async_fn_in_trait)] + async fn xdel( + connection: &Connection, + key: String, + ids: Vec, + ) -> Result { + query(connection, ("XDEL", key, ids)).await + } + + #[doc = " Create a new consumer group uniquely identified by for the stream stored at ."] + #[doc = ""] + #[doc = " XGROUP CREATE key group 〈 id | $ 〉 [ MKSTREAM ] [ ENTRIESREAD entriesread ]"] + #[doc = " https://valkey.io/commands/xgroup-create/"] + #[allow(async_fn_in_trait)] + async fn xgroup_create( + connection: &Connection, + key: String, + group: String, + id: StreamId, + mkstream: bool, + entriesread: Option, + ) -> Result<(), Error> { + query( + connection, + ( + "XGROUP", + "CREATE", + key, + group, + id, + Flag("MKSTREAM", mkstream), + NamedOption("ENTRIESREAD", entriesread), + ), + ) + .await + } + + #[doc = " Create a consumer named in the consumer group of the stream that\'s stored at ."] + #[doc = ""] + #[doc = " XGROUP CREATECONSUMER key group consumer"] + #[doc = " https://valkey.io/commands/xgroup-createconsumer/"] + #[allow(async_fn_in_trait)] + async fn xgroup_createconsumer( + connection: &Connection, + key: String, + group: String, + consumer: String, + ) -> Result { + query( + connection, + ("XGROUP", "CREATECONSUMER", key, group, consumer), + ) + .await + } + + #[doc = " The XGROUP DELCONSUMER command deletes a consumer from the consumer group."] + #[doc = ""] + #[doc = " XGROUP DELCONSUMER key group consumer"] + #[doc = " https://valkey.io/commands/xgroup-delconsumer/"] + #[allow(async_fn_in_trait)] + async fn xgroup_delconsumer( + connection: &Connection, + key: String, + group: String, + consumer: String, + ) -> Result { + query(connection, ("XGROUP", "DELCONSUMER", key, group, consumer)).await + } + + #[doc = " Completely destroys the consumer group."] + #[doc = ""] + #[doc = " XGROUP DESTROY key group"] + #[doc = " https://valkey.io/commands/xgroup-destroy/"] + #[allow(async_fn_in_trait)] + async fn xgroup_destroy( + connection: &Connection, + key: String, + group: String, + ) -> Result { + query(connection, ("XGROUP", "DESTROY", key, group)).await + } + + #[doc = " Set the last delivered ID for a consumer group."] + #[doc = ""] + #[doc = " XGROUP SETID key group 〈 id | $ 〉 [ ENTRIESREAD entriesread ]"] + #[doc = " https://valkey.io/commands/xgroup-setid/"] + #[allow(async_fn_in_trait)] + async fn xgroup_setid( + connection: &Connection, + key: String, + group: String, + id: StreamId, + entriesread: Option, + ) -> Result<(), Error> { + query( + connection, + ( + "XGROUP", + "SETID", + key, + group, + id, + NamedOption("ENTRIESREAD", entriesread), + ), + ) + .await + } + + #[doc = " Consumers that belong to the consumer group of the stream stored at ."] + #[doc = ""] + #[doc = " XINFO CONSUMERS key group"] + #[doc = " https://valkey.io/commands/xinfo-consumers/"] + #[allow(async_fn_in_trait)] + async fn xinfo_consumers( + connection: &Connection, + key: String, + group: String, + ) -> Result, Error> { + query(connection, ("XINFO", "CONSUMERS", key, group)).await + } + + #[doc = " Consumer groups of the stream stored at ."] + #[doc = ""] + #[doc = " XINFO GROUPS key"] + #[doc = " https://valkey.io/commands/xinfo-groups/"] + #[allow(async_fn_in_trait)] + async fn xinfo_groups(connection: &Connection, key: String) -> Result, Error> { + query(connection, ("XINFO", "GROUPS", key)).await + } + + #[doc = " Information about the stream stored at ."] + #[doc = ""] + #[doc = " See also `xinfo-stream-full`"] + #[doc = ""] + #[doc = " XINFO STREAM key"] + #[doc = " https://valkey.io/commands/xinfo-stream/"] + #[allow(async_fn_in_trait)] + async fn xinfo_stream(connection: &Connection, key: String) -> Result { + query(connection, ("XINFO", "STREAM", key)).await + } + + #[doc = " Information about the stream stored at ."] + #[doc = ""] + #[doc = " See also `xinfo-stream`"] + #[doc = ""] + #[doc = " XINFO STREAM key FULL [ COUNT count ]"] + #[doc = " https://valkey.io/commands/xinfo-stream/"] + #[allow(async_fn_in_trait)] + async fn xinfo_stream_full( + connection: &Connection, + key: String, + count: Option, + ) -> Result { + query( + connection, + ("XINFO", "STREAM", key, "FULL", NamedOption("COUNT", count)), + ) + .await + } + + #[doc = " Number of entries inside a stream."] + #[doc = ""] + #[doc = " XLEN key"] + #[doc = " https://valkey.io/commands/xlen/"] + #[allow(async_fn_in_trait)] + async fn xlen(connection: &Connection, key: String) -> Result { + query(connection, ("XLEN", key)).await + } + + #[doc = " List of pending messages."] + #[doc = ""] + #[doc = " See also `xpending-extended`."] + #[doc = ""] + #[doc = " XPENDING key group"] + #[doc = " https://valkey.io/commands/xpending/"] + #[allow(async_fn_in_trait)] + async fn xpending( + connection: &Connection, + key: String, + group: String, + ) -> Result<(Integer, StreamId, StreamId, Map), Error> { + query(connection, ("XPENDING", key, group)).await + } + + #[doc = " List of pending messages."] + #[doc = ""] + #[doc = " See also `xpending`."] + #[doc = ""] + #[doc = " XPENDING key group [ IDLE min-idle-time ] start end count [ consumer ]"] + #[doc = " https://valkey.io/commands/xpending/"] + #[allow(async_fn_in_trait)] + async fn xpending_extended( + connection: &Connection, + key: String, + group: String, + min_idle_time: Option, + start: StreamId, + end: StreamId, + count: Integer, + consumer: Option, + ) -> Result, Error> { + query( + connection, + ( + "XPENDING", + key, + group, + NamedOption("IDLE", min_idle_time.map(Milliseconds)), + start, + end, + count, + consumer, + ), + ) + .await + } + + #[doc = " Stream entries matching a given range of IDs."] + #[doc = ""] + #[doc = " XRANGE key start end [ COUNT count ]"] + #[doc = " https://valkey.io/commands/xrange/"] + #[allow(async_fn_in_trait)] + async fn xrange( + connection: &Connection, + key: String, + start: StreamId, + end: StreamId, + count: Option, + ) -> Result)>, Error> { + query( + connection, + ("XRANGE", key, start, end, NamedOption("COUNT", count)), + ) + .await + } + + #[doc = " Read data from one or multiple streams, only returning entries with an ID greater than the last received ID reported by the caller."] + #[doc = ""] + #[doc = " XREAD [ COUNT count ] [ BLOCK milliseconds ] STREAMS key [ key ... ] ID [ ID ... ]"] + #[doc = " https://valkey.io/commands/xread/"] + #[allow(async_fn_in_trait)] + async fn xread( + connection: &Connection, + count: Option, + block: Option, + keys: Vec, + ids: Vec, + ) -> Result)>>>, Error> { + query( + connection, + ( + "XREAD", + NamedOption("COUNT", count), + NamedOption("BLOCK", block), + "STREAMS", + keys, + ids, + ), + ) + .await + } + + #[doc = " Special version of the XREAD command with support for consumer groups."] + #[doc = ""] + #[doc = " XREADGROUP GROUP group consumer [ COUNT count ] [ BLOCK milliseconds ] [ NOACK ] STREAMS key [ key ... ] ID [ ID ... ]"] + #[doc = " https://valkey.io/commands/xreadgroup/"] + #[allow(async_fn_in_trait)] + async fn xreadgroup( + connection: &Connection, + group: String, + consumer: String, + count: Option, + block: Option, + noack: bool, + keys: Vec, + ids: Vec, + ) -> Result)>>>, Error> { + query( + connection, + ( + "XREADGROUP", + "GROUP", + group, + consumer, + NamedOption("COUNT", count), + NamedOption("BLOCK", block), + "STREAMS", + keys, + ids, + ), + ) + .await + } + + #[doc = " XRANGE, but returning the entries in reverse order."] + #[doc = ""] + #[doc = " XREVRANGE key end start [ COUNT count ]"] + #[doc = " https://valkey.io/commands/xrevrange/"] + #[allow(async_fn_in_trait)] + async fn xrevrange( + connection: &Connection, + key: String, + end: StreamId, + start: StreamId, + count: Option, + ) -> Result)>, Error> { + query( + connection, + ("XREVRANGE", key, end, start, NamedOption("COUNT", count)), + ) + .await + } + + #[doc = " Internal command. It is used by a Valkey primary to replicate the last delivered ID of streams."] + #[doc = ""] + #[doc = " XSETID key last-id [ ENTRIESADDED entries-added ] [ MAXDELETEDID max-deleted-id ]"] + #[doc = " https://valkey.io/commands/xsetid/"] + #[allow(async_fn_in_trait)] + async fn xsetid( + connection: &Connection, + key: String, + last_id: StreamId, + entries_added: Option, + max_deleted_id: Option, + ) -> Result<(), Error> { + query( + connection, + ( + "XSETID", + key, + last_id, + NamedOption("ENTRIESADDED", entries_added), + NamedOption("MAXDELETEDID", max_deleted_id), + ), + ) + .await + } + + #[doc = " Trims the stream by evicting older entries."] + #[doc = ""] + #[doc = " XTRIM key 〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ]"] + #[doc = " https://valkey.io/commands/xtrim/"] + #[allow(async_fn_in_trait)] + async fn xtrim( + connection: &Connection, + key: String, + trim: StreamTrim, + ) -> Result { + query(connection, ("XTRIM", key, trim)).await + } +} + +wit_bindgen::generate!({ + path: "../wit", + world: "commands", + // the resp types shared by every component + with: { + "componentized:valkey/resp@0.3.0-dev": resp_codec::bindings::componentized::valkey::resp, + }, + generate_all, +}); + +export!(Commands); diff --git a/components/commands/src/value.rs b/components/commands/src/value.rs new file mode 100644 index 0000000..ba25837 --- /dev/null +++ b/components/commands/src/value.rs @@ -0,0 +1,410 @@ +use crate::exports::componentized::valkey::commands::{ + Double, Duration, Error, Instant, Integer, LcsIdxMatch, LcsIdxReply, StreamId, TtlResult, + Value, XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, + XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, +}; + +use resp_codec::NestedValue; +use wit_bindgen::rt::Map; + +/// Converts a reply into a Rust type, e.g. `Option` for a bulk string or null. +pub trait FromValue: Sized { + fn from_value(value: Value) -> Result; +} + +/// The reply itself, e.g. for a field of mixed types. +impl FromValue for Value { + fn from_value(value: Value) -> Result { + Ok(value) + } +} + +/// A simple string `OK`. +impl FromValue for () { + fn from_value(value: Value) -> Result { + match &value { + Value::String(reply) if reply == "OK" => Ok(()), + _ => Err(unexpected(&value)), + } + } +} + +impl FromValue for Integer { + fn from_value(value: Value) -> Result { + match value { + Value::Integer(value) => Ok(value), + value => Err(unexpected(&value)), + } + } +} + +impl FromValue for Double { + fn from_value(value: Value) -> Result { + match value { + Value::Double(value) => Ok(value), + value => Err(unexpected(&value)), + } + } +} + +/// A non-negative integer. +impl FromValue for u64 { + fn from_value(value: Value) -> Result { + let value = Integer::from_value(value)?; + u64::try_from(value) + .map_err(|_| Error::Client(format!("unexpected negative reply: {value}"))) + } +} + +/// An integer `0` or `1`, or a boolean. +impl FromValue for bool { + fn from_value(value: Value) -> Result { + match value { + Value::Integer(0) => Ok(false), + Value::Integer(1) => Ok(true), + Value::Boolean(value) => Ok(value), + value => Err(unexpected(&value)), + } + } +} + +/// A bulk, simple or verbatim string. +impl FromValue for String { + fn from_value(value: Value) -> Result { + match value { + Value::BulkString(value) | Value::String(value) => Ok(value), + Value::VerbatimString((_, value)) => Ok(value), + value => Err(unexpected(&value)), + } + } +} + +/// Null, or a value. +impl FromValue for Option { + fn from_value(value: Value) -> Result { + match value { + Value::Null => Ok(None), + value => T::from_value(value).map(Some), + } + } +} + +/// An array, or a set. +impl FromValue for Vec { + fn from_value(value: Value) -> Result { + items(value)?.into_iter().map(T::from_value).collect() + } +} + +/// An array of exactly as many items as the tuple. +macro_rules! from_value_tuples { + ($(($len:literal; $($item:ident),+)),*) => { + $( + impl<$($item: FromValue),+> FromValue for ($($item,)+) { + #[allow(non_snake_case)] + fn from_value(value: Value) -> Result { + match <[Value; $len]>::try_from(items(value)?) { + Ok([$($item),+]) => Ok(($($item::from_value($item)?,)+)), + Err(items) => Err(Error::Client(format!( + "unexpected number of items, expected {}: {}", + $len, + items.len() + ))), + } + } + } + )* + }; +} + +from_value_tuples!((2; A, B), (3; A, B, C), (4; A, B, C, D)); + +/// A map, or a flat array of keys and values. +impl FromValue for Map { + fn from_value(value: Value) -> Result { + Ok(entries(value)?.into_iter().collect()) + } +} + +/// The fields of a map, by name, e.g. to build a record. +/// +/// Fields that are not taken are ignored, so newer servers may add fields. +pub struct Fields(Vec<(String, Value)>); + +impl FromValue for Fields { + fn from_value(value: Value) -> Result { + entries(value).map(Fields) + } +} + +impl Fields { + /// Take the named field, if present. + pub fn optional(&mut self, name: &str) -> Result, Error> { + match self.0.iter().position(|(key, _)| key == name) { + Some(index) => T::from_value(self.0.swap_remove(index).1).map(Some), + None => Ok(None), + } + } + + /// Take the named field, failing if it is missing. + pub fn required(&mut self, name: &str) -> Result { + self.optional(name)? + .ok_or_else(|| Error::Client(format!("missing field: {name}"))) + } +} + +/// A duration or instant in whole seconds, e.g. for EX and EXAT. +pub struct Seconds(pub T); + +/// A duration or instant in whole milliseconds, e.g. for PX and PXAT. +pub struct Milliseconds(pub T); + +impl FromValue for Seconds { + fn from_value(value: Value) -> Result { + let seconds = u64::from_value(value)?; + Ok(Seconds(seconds.saturating_mul(1_000_000_000))) + } +} + +impl FromValue for Milliseconds { + fn from_value(value: Value) -> Result { + let milliseconds = u64::from_value(value)?; + Ok(Milliseconds(milliseconds.saturating_mul(1_000_000))) + } +} + +/// Seconds since the unix epoch. +impl FromValue for Seconds { + fn from_value(value: Value) -> Result { + let seconds = Integer::from_value(value)?; + Ok(Seconds(Instant { + seconds, + nanoseconds: 0, + })) + } +} + +/// Milliseconds since the unix epoch. +impl FromValue for Milliseconds { + fn from_value(value: Value) -> Result { + let milliseconds = Integer::from_value(value)?; + // euclidean, so the nanoseconds are positive for an instant before the epoch + Ok(Milliseconds(Instant { + seconds: milliseconds.div_euclid(1_000), + nanoseconds: milliseconds.rem_euclid(1_000) as u32 * 1_000_000, + })) + } +} + +/// A value, or `-1` for none, e.g. the `active-time` of a consumer that has never been active. +pub struct OrNever(pub Option); + +impl FromValue for OrNever { + fn from_value(value: Value) -> Result { + match value { + Value::Integer(-1) => Ok(OrNever(None)), + value => T::from_value(value).map(|value| OrNever(Some(value))), + } + } +} + +/// A stream ID, or `0-0` for none, e.g. the start of the next XAUTOCLAIM, `0-0` once the scan is +/// complete. +pub struct OrZeroId(pub Option); + +pub const ZERO_ID: &str = "0-0"; + +impl FromValue for OrZeroId { + fn from_value(value: Value) -> Result { + let id = StreamId::from_value(value)?; + Ok(OrZeroId((id != ZERO_ID).then_some(id))) + } +} + +impl FromValue for TtlResult { + fn from_value(value: Value) -> Result { + match Integer::from_value(value)? { + -2 => Ok(Self::DoesNotExist), + 0 => Ok(Self::ConditionNotMet), + 1 => Ok(Self::Applied), + 2 => Ok(Self::ZeroSeconds), + code => Err(Error::Client(format!("unexpected reply: {code}"))), + } + } +} + +/// A map of the matches and the length of the longest common subsequence. +impl FromValue for LcsIdxReply { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + matches: fields.required("matches")?, + len: fields.required("len")?, + }) + } +} + +/// The inclusive ranges of a match in each key and, with `WITHMATCHLEN`, its length. +impl FromValue for LcsIdxMatch { + fn from_value(value: Value) -> Result { + let mut items = items(value)?.into_iter(); + let (Some(key1_range), Some(key2_range), len, None) = + (items.next(), items.next(), items.next(), items.next()) + else { + return Err(Error::Client( + "unexpected LCS match, expected 2 or 3 items".to_string(), + )); + }; + Ok(Self { + key1_range: FromValue::from_value(key1_range)?, + key2_range: FromValue::from_value(key2_range)?, + len: len.map(FromValue::from_value).transpose()?, + }) + } +} + +impl FromValue for XinfoConsumer { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + let idle: Milliseconds = fields.required("idle")?; + let inactive: OrNever> = fields.required("inactive")?; + Ok(Self { + name: fields.required("name")?, + pending: fields.required("pending")?, + idle: idle.0, + inactive: inactive.0.map(|inactive| inactive.0), + }) + } +} + +impl FromValue for XinfoGroup { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + name: fields.required("name")?, + consumers: fields.required("consumers")?, + last_delivered_id: fields.required("last-delivered-id")?, + entries_read: fields.required("entries-read")?, + lag: fields.required("lag")?, + }) + } +} + +impl FromValue for XinfoStreamReply { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + length: fields.required("length")?, + radix_tree_keys: fields.required("radix-tree-keys")?, + radix_tree_nodes: fields.required("radix-tree-nodes")?, + groups: fields.required("groups")?, + last_generated_id: fields.required("last-generated-id")?, + max_deleted_entry_id: fields.required("max-deleted-entry-id")?, + entries_added: fields.required("entries-added")?, + first_entry: fields.required("first-entry")?, + last_entry: fields.required("last-entry")?, + }) + } +} + +impl FromValue for XinfoStreamReplyFull { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + length: fields.required("length")?, + radix_tree_keys: fields.required("radix-tree-keys")?, + radix_tree_nodes: fields.required("radix-tree-nodes")?, + last_generated_id: fields.required("last-generated-id")?, + max_deleted_entry_id: fields.required("max-deleted-entry-id")?, + entries_added: fields.required("entries-added")?, + entries: fields.required("entries")?, + groups: fields.required("groups")?, + }) + } +} + +impl FromValue for XinfoStreamReplyFullGroup { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + // the entry, its consumer, when it was last delivered and how often + let pending: Vec<(StreamId, String, Milliseconds, Integer)> = + fields.required("pending")?; + Ok(Self { + name: fields.required("name")?, + last_delivered_id: fields.required("last-delivered-id")?, + entries_read: fields.required("entries-read")?, + lag: fields.required("lag")?, + pel_count: fields.required("pel-count")?, + pending: pending + .into_iter() + .map(|(id, consumer, delivered, deliveries)| { + (id, consumer, delivered.0, deliveries) + }) + .collect(), + consumers: fields.required("consumers")?, + }) + } +} + +impl FromValue for XinfoStreamReplyFullConsumer { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + let seen_time: Milliseconds = fields.required("seen-time")?; + let active_time: OrNever> = fields.required("active-time")?; + // the entry, when it was last delivered and how often + let pending: Vec<(StreamId, Milliseconds, Integer)> = + fields.required("pending")?; + Ok(Self { + name: fields.required("name")?, + seen_time: seen_time.0, + active_time: active_time.0.map(|active_time| active_time.0), + pel_count: fields.required("pel-count")?, + pending: pending + .into_iter() + .map(|(id, delivered, deliveries)| (id, delivered.0, deliveries)) + .collect(), + }) + } +} + +fn unexpected(reply: &Value) -> Error { + Error::Client(format!("unexpected reply: {reply:?}")) +} + +fn decode(value: NestedValue) -> Result { + resp_codec::decode(value).map_err(Error::Resp) +} + +/// The nested values of an array, or a set. +fn items(reply: Value) -> Result, Error> { + match reply { + Value::Array(items) | Value::Set(items) => items.into_iter().map(decode).collect(), + reply => Err(unexpected(&reply)), + } +} + +/// The entries of a map, or of a flat array of keys and values. +fn entries(reply: Value) -> Result, Error> { + let entry = |key, value| Ok((K::from_value(decode(key)?)?, V::from_value(decode(value)?)?)); + match reply { + Value::Map(entries) => entries + .into_iter() + .map(|(key, value)| entry(key, value)) + .collect(), + Value::Array(items) => { + if items.len() % 2 != 0 { + return Err(Error::Client(format!( + "unexpected odd number of keys and values: {}", + items.len() + ))); + } + let mut items = items.into_iter(); + let mut entries = vec![]; + while let (Some(key), Some(value)) = (items.next(), items.next()) { + entries.push(entry(key, value)?); + } + Ok(entries) + } + reply => Err(unexpected(&reply)), + } +} diff --git a/components/commands/tests/commands.rs b/components/commands/tests/commands.rs index 0d4f487..018f2aa 100644 --- a/components/commands/tests/commands.rs +++ b/components/commands/tests/commands.rs @@ -2,7 +2,7 @@ //! `resp` and `tcp`. The test plays the server over a stub transport, checking the command sent //! and the conversion of its reply. -use std::future::Future; +use std::{collections::HashMap, future::Future}; use test_harness::{ Harness, Transport, Value, bulk, command, connection_commands, establish, hash_commands, @@ -61,7 +61,14 @@ async fn string_commands() -> wasmtime::Result<()> { let (result, sent) = round_trip( &mut transport, ok(), - strings.call_set(accessor, conn, "key".to_string(), "value".to_string()), + strings.call_set( + accessor, + conn, + "key".to_string(), + "value".to_string(), + None, + None, + ), ) .await; assert_eq!(sent, command(&["SET", "key", "value"])); @@ -105,7 +112,7 @@ async fn keyspace_commands() -> wasmtime::Result<()> { ) .await; assert_eq!(sent, command(&["DEL", "key"])); - assert_eq!(result, Ok(())); + assert_eq!(result, Ok(1)); let (result, sent) = round_trip( &mut transport, @@ -129,7 +136,7 @@ async fn keyspace_commands() -> wasmtime::Result<()> { ), ) .await; - assert_eq!(sent, command(&["EXPIRE", "key", "90", "NX"])); + assert_eq!(sent, command(&["EXPIRE", "key", "91", "NX"])); assert_eq!(result, Ok(ExpireResult::Applied)); let (result, sent) = round_trip( @@ -180,7 +187,7 @@ async fn hash_commands() -> wasmtime::Result<()> { ) .await; assert_eq!(sent, command(&["HSET", "hash", "f", "v"])); - assert_eq!(result, Ok(())); + assert_eq!(result, Ok(1)); let (result, sent) = round_trip( &mut transport, @@ -201,10 +208,10 @@ async fn hash_commands() -> wasmtime::Result<()> { assert_eq!(sent, command(&["HGETALL", "hash"])); assert_eq!( result, - Ok(vec![ + Ok(HashMap::from([ ("f".to_string(), "v".to_string()), ("g".to_string(), "w".to_string()) - ]) + ])) ); let (result, _) = round_trip( @@ -419,7 +426,7 @@ async fn hash_commands() -> wasmtime::Result<()> { hashes.call_hdel(accessor, conn, key(), "f".to_string()), ) .await; - assert_eq!(result, Ok(())); + assert_eq!(result, Ok(1)); Ok(()) }) .await @@ -533,7 +540,7 @@ async fn admin_and_pubsub_commands() -> wasmtime::Result<()> { ) .await; assert_eq!(sent, command(&["ACL", "DELUSER", "user"])); - assert_eq!(result, Ok(())); + assert_eq!(result, Ok(1)); let (result, sent) = round_trip( &mut transport, @@ -576,6 +583,8 @@ async fn unexpected_replies_fail_the_command() -> wasmtime::Result<()> { /// The tests against a Valkey server on localhost:6379, ignored unless run with /// `make test-valkey`, which only runs the ignored tests in `with_valkey` modules. mod with_valkey { + use std::collections::HashMap; + use super::*; /// Every command against a Valkey server on localhost:6379, run with @@ -608,7 +617,14 @@ mod with_valkey { assert!(hello.contains(&(s("proto"), Value::Integer(3)))); strings - .call_set(accessor, conn, s("commands-test:string"), s("1")) + .call_set( + accessor, + conn, + s("commands-test:string"), + s("1"), + None, + None, + ) .await??; assert_eq!( strings @@ -670,9 +686,8 @@ mod with_valkey { vec![(s("f"), s("v")), (s("g"), s("w"))], ) .await??; - let mut all = hashes.call_hgetall(accessor, conn, key()).await??; - all.sort(); - assert_eq!(all, vec![(s("f"), s("v")), (s("g"), s("w"))]); + let all = hashes.call_hgetall(accessor, conn, key()).await??; + assert_eq!(all, HashMap::from([(s("f"), s("v")), (s("g"), s("w"))])); assert_eq!(hashes.call_hlen(accessor, conn, key()).await??, 2); assert_eq!( hashes diff --git a/components/connection-commands/Cargo.toml b/components/connection-commands/Cargo.toml deleted file mode 100644 index a8543d9..0000000 --- a/components/connection-commands/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "connection-commands" -version = "0.1.0" -edition = "2024" -license = "Apache-2.0" - -[lib] -crate-type = ["cdylib"] -# shared with the other command components -path = "../../crates/commands/src/lib.rs" - -[features] -default = ["connection"] -admin = [] -connection = [] -hash = [] -keyspace = [] -pubsub = [] -string = [] - -[dependencies] -resp-codec = { workspace = true } -wit-bindgen = { workspace = true } diff --git a/components/connection-commands/README.md b/components/connection-commands/README.md deleted file mode 100644 index 4a8855f..0000000 --- a/components/connection-commands/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# connection-commands - -Commands about the connection, Valkey's @connection ACL category. - -Exports `componentized:valkey/connection-commands` over an imported [`connection`](../connection/). [`commands`](../commands/) exports every command interface in one component. diff --git a/components/connection/src/lib.rs b/components/connection/src/lib.rs index 6a95f30..dded97b 100644 --- a/components/connection/src/lib.rs +++ b/components/connection/src/lib.rs @@ -5,7 +5,7 @@ //! //! - the writer writes queued commands to the requests stream, in order //! - the reader reads the replies stream, handing each reply to the oldest command waiting for one, -//! and each push message to every reader of push messages +//! and each push message, or value the server sent unprompted, to every reader of push messages //! - the supervisor starts the work methods ask for that outlives them, e.g. keep-alive, since sync //! methods have no task of their own to spawn it into //! @@ -128,10 +128,7 @@ impl GuestConnection for ConnectionImpl { } async fn send(&self, command: Vec) -> Result<(), Error> { - let (written_tx, written_rx) = oneshot::channel(); - enqueue(&self.0, command, Reply::Discard, Some(written_tx))?; - // the signal is dropped unsent when the transport closed first - written_rx.await.map_err(|_| closed_error(&self.0.state)) + send(&self.0, command).await } fn pushes(&self) -> StreamReader { @@ -139,7 +136,12 @@ impl GuestConnection for ConnectionImpl { let (tx, rx) = mpsc::channel(PUSH_BUFFER); let mut state = self.0.state.borrow_mut(); // a closed connection drops the writer, closing the stream at once - if state.closed.is_none() && self.0.tasks.unbounded_send(Task::Pushes(rx, writer)).is_ok() + if state.closed.is_none() + && self + .0 + .tasks + .unbounded_send(Task::Pushes(rx, writer)) + .is_ok() { state.pushes.push(tx); } @@ -154,19 +156,20 @@ impl GuestConnection for ConnectionImpl { }; // a zero interval would ping whenever the connection is idle, treated as disabled if let Some(interval) = interval.filter(|interval| *interval > 0) { - let _ = self - .0 - .tasks - .unbounded_send(Task::KeepAlive { generation, interval }); + let _ = self.0.tasks.unbounded_send(Task::KeepAlive { + generation, + interval, + }); } } } /// Queue a command, and where its reply goes, as one step so replies pair with commands in order. +/// A command without a reply takes no place in the order. fn enqueue( inner: &Inner, command: Vec, - reply: Reply, + reply: Option, written: Option>, ) -> Result<(), Error> { let mut state = inner.state.borrow_mut(); @@ -178,19 +181,52 @@ fn enqueue( .requests .unbounded_send(Request { command, written }) .map_err(|_| Error::Closed(None))?; - state.pending.push_back(reply); + state.pending.extend(reply); Ok(()) } async fn call(inner: &Inner, command: Vec) -> Result { + if replies_with_pushes(&command) { + return send(inner, command).await.map(|()| Value::Null); + } let (reply_tx, reply_rx) = oneshot::channel(); - enqueue(inner, command, Reply::Call(reply_tx), None)?; + enqueue(inner, command, Some(Reply::Call(reply_tx)), None)?; // the reply is dropped unsent when the connection closes, the reader fails it first reply_rx .await .unwrap_or_else(|_| Err(closed_error(&inner.state))) } +async fn send(inner: &Inner, command: Vec) -> Result<(), Error> { + let reply = (!replies_with_pushes(&command)).then_some(Reply::Discard); + let (written_tx, written_rx) = oneshot::channel(); + enqueue(inner, command, reply, Some(written_tx))?; + // the signal is dropped unsent when the transport closed first + written_rx.await.map_err(|_| closed_error(&inner.state)) +} + +/// Whether the command's replies are push messages, e.g. SUBSCRIBE, rather than a reply. +/// +/// In RESP3 the server confirms each channel subscribed to, or unsubscribed from, with a push +/// message. An error, e.g. for a command missing its channels, is a reply the server sends +/// unprompted. +fn replies_with_pushes(command: &[Value]) -> bool { + const COMMANDS: [&str; 6] = [ + "SUBSCRIBE", + "PSUBSCRIBE", + "SSUBSCRIBE", + "UNSUBSCRIBE", + "PUNSUBSCRIBE", + "SUNSUBSCRIBE", + ]; + match command.first() { + Some(Value::BulkString(name) | Value::String(name)) => COMMANDS + .iter() + .any(|command| name.eq_ignore_ascii_case(command)), + _ => false, + } +} + /// The error a command fails with once the connection closed. fn closed_error(state: &RefCell) -> Error { state.borrow().closed.clone().unwrap_or(Error::Closed(None)) @@ -236,8 +272,8 @@ async fn write_requests( } } -/// Hand each reply to the oldest command waiting for one, and each push message to the readers of -/// push messages, until the transport closes. +/// Hand each reply to the oldest command waiting for one, and each push message, or value the server +/// sent unprompted, to the readers of push messages, until the transport closes. async fn read_replies( state: Rc>, mut replies: StreamReader, @@ -246,21 +282,22 @@ async fn read_replies( while let Some(value) = replies.next().await { let mut state = state.borrow_mut(); state.activity += 1; - match value { - Value::Push(_) => { - // a reader that is full or gone is dropped, rather than holding up replies - state - .pushes - .retain_mut(|pushes| pushes.try_send(value.clone()).is_ok()); - } + let value = match value { + Value::Push(_) => value, value => match state.pending.pop_front() { Some(Reply::Call(reply)) => { let _ = reply.send(reply_result(value)); + continue; } - // a discarded reply, or one the server sent unprompted - Some(Reply::Discard) | None => {} + Some(Reply::Discard) => continue, + // sent unprompted, e.g. the feed of MONITOR + None => value, }, - } + }; + // a reader that is full or gone is dropped, rather than holding up replies + state + .pushes + .retain_mut(|pushes| pushes.try_send(value.clone()).is_ok()); } let reason = match closed.await { Ok(()) => None, @@ -361,7 +398,9 @@ async fn establish( /// The server's properties from the reply to HELLO, a map of property names to values. fn server_properties(reply: Value) -> Result, Error> { let Value::Map(entries) = reply else { - return Err(Error::Client(format!("unexpected reply to HELLO: {reply:?}"))); + return Err(Error::Client(format!( + "unexpected reply to HELLO: {reply:?}" + ))); }; entries .into_iter() diff --git a/components/connection/tests/connection.rs b/components/connection/tests/connection.rs index b58e5e0..706872f 100644 --- a/components/connection/tests/connection.rs +++ b/components/connection/tests/connection.rs @@ -194,6 +194,33 @@ async fn push_messages_go_to_every_reader() -> wasmtime::Result<()> { .await } +#[tokio::test(flavor = "multi_thread")] +async fn unprompted_values_go_to_push_readers() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + let pushes = guest.call_pushes(accessor, connection).await?; + let mut pushes = receive(accessor, pushes)?; + + let call = guest.call_call(accessor, connection, command(&["MONITOR"])); + let server = async { + transport.request().await; + transport.reply(Value::String("OK".to_string())); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(Value::String("OK".to_string()))); + + // the feed of MONITOR, simple strings with no command waiting for them + let feed = Value::String(r#"1760190000.123456 [0 127.0.0.1:52144] "ping""#.to_string()); + transport.reply(feed.clone()); + assert_eq!(pushes.recv().await, Some(feed)); + Ok(()) + }) + .await +} + #[tokio::test(flavor = "multi_thread")] async fn slow_push_readers_are_closed() -> wasmtime::Result<()> { let mut harness = Harness::new("connection").await?; diff --git a/components/dep-cli/Cargo.toml b/components/dep-cli/Cargo.toml index c8c1601..2e7cc1e 100644 --- a/components/dep-cli/Cargo.toml +++ b/components/dep-cli/Cargo.toml @@ -8,5 +8,7 @@ license = "Apache-2.0" crate-type = ["cdylib"] [dependencies] -clap = { version = "4.6.1", features = ["derive"] } +futures-channel = { workspace = true } +futures-core = { workspace = true } +resp-codec = { workspace = true } wit-bindgen = { workspace = true } diff --git a/components/dep-cli/README.md b/components/dep-cli/README.md index c4f8d8e..90b5cdc 100644 --- a/components/dep-cli/README.md +++ b/components/dep-cli/README.md @@ -1,3 +1,9 @@ # dep-cli The Valkey CLI before the client components are composed in, see [`cli`](../cli/). + +Sends its arguments as a command over a connection from a `connector` and prints the reply. A command without a reply, like `SUBSCRIBE`, prints the push messages that arrive until interrupted or the connection closes. + +`UNSUBSCRIBE`, `PUNSUBSCRIBE` and `SUNSUBSCRIBE` print their confirmations and exit. `MONITOR` prints its feed until interrupted or the connection closes. + +Without arguments, each line of stdin is sent as a command over one connection, kept open until stdin closes, printing replies and push messages as they arrive. Words are split on whitespace, quotes keep a word with whitespace together, e.g. `set greeting "hello world"`. diff --git a/components/dep-cli/src/lib.rs b/components/dep-cli/src/lib.rs index 3f21154..52a876c 100644 --- a/components/dep-cli/src/lib.rs +++ b/components/dep-cli/src/lib.rs @@ -1,817 +1,420 @@ -use clap::{ArgAction, Args, Parser, Subcommand}; -use componentized::valkey::{ - admin_commands, - connection::{ConnectOpts, Error}, - connection_commands, hash_commands, - hash_commands::{HrandfieldOpts, HscanOpts}, - keyspace_commands, pubsub_commands, - resp::{self, Value}, - string_commands, tcp, -}; +//! A cli sending commands to Valkey, printing the replies. +//! +//! With arguments, they are sent as one command. A command without a reply, e.g. SUBSCRIBE, prints +//! the push messages that arrive until the cli is interrupted or the connection closes. +//! +//! Without arguments, each line of stdin is sent as a command over one connection, kept open until +//! stdin closes. Push messages are printed as they arrive. + +use std::future::{Future, poll_fn}; +use std::pin::{Pin, pin}; +use std::task::Poll; + +use componentized::valkey::connection::{Connection, Error}; +use componentized::valkey::connector; use exports::wasi::cli::run::Guest; -use std::fmt; -use wasi::cli::{environment::get_arguments, stderr, stdout, types::ErrorCode}; -use wit_bindgen::{FutureReader, StreamReader}; - -/// Prints a line to stdout, like `std::println!`, which has no stdout on wasm32-unknown-unknown. -/// The write is awaited, so it is only usable in an async function. -macro_rules! println { - () => { - $crate::write_to(stdout::write_via_stream, String::from("\n")).await - }; - ($($arg:tt)*) => { - $crate::write_to(stdout::write_via_stream, format!("{}\n", format_args!($($arg)*))).await - }; -} +use futures_channel::mpsc; +use futures_core::Stream; +use resp_codec::{NestedValue, Value}; +use wasi::cli::{environment, stderr, stdin, stdout, types::ErrorCode}; +use wit_bindgen::{FutureReader, StreamReader, StreamResult, StreamWriter}; -/// Prints a line to stderr, like `std::eprintln!`, which has no stderr on wasm32-unknown-unknown. -/// The write is awaited, so it is only usable in an async function. -macro_rules! eprintln { - () => { - $crate::write_to(stderr::write_via_stream, String::from("\n")).await - }; - ($($arg:tt)*) => { - $crate::write_to(stderr::write_via_stream, format!("{}\n", format_args!($($arg)*))).await - }; -} +const USAGE: &str = "\ +Usage: cli [COMMAND [ARG]...] -/// Writes the value to the target, panics when it can't be written, like `std::println!`. -async fn write_to( - write_via_stream: fn(StreamReader) -> FutureReader>, - value: String, -) { - let (mut writer, reader) = wit_stream::new(); - let result = write_via_stream(reader); - writer.write_all(value.into_bytes()).await; - // the host finishes writing once the writable end is dropped - drop(writer); - if let Err(e) = result.await { - panic!("failed printing: {e:?}"); - } -} +Sends the command to Valkey and prints the reply. A command without a reply, e.g. SUBSCRIBE, +prints the push messages that arrive until interrupted. + +Without a command, each line of stdin is sent as a command, until stdin closes. Words are split on +whitespace, quote a word to include whitespace, e.g. \"hello world\", in double quotes escapes like +\\n and \\x41 are expanded. + +The server is configured with wasi:config, see the tcp-connector component."; + +/// Commands without a reply, the server confirms each channel with a push message. +const SUBSCRIBE: [&str; 3] = ["SUBSCRIBE", "PSUBSCRIBE", "SSUBSCRIBE"]; +const UNSUBSCRIBE: [&str; 3] = ["UNSUBSCRIBE", "PUNSUBSCRIBE", "SUNSUBSCRIBE"]; + +struct Cli; impl Guest for Cli { async fn run() -> Result<(), ()> { - // clap prints help and errors itself and exits the process, neither works on this target, - // they are printed here instead - let cli = match Cli::try_parse_from(get_arguments()) { - Ok(cli) => cli, - Err(error) => { - let message = error.render().to_string(); - if error.use_stderr() { - eprintln!("{}", message.trim_end()); - } else { - println!("{}", message.trim_end()); - } - // help and version succeed, invalid arguments fail - return if error.exit_code() == 0 { - Ok(()) - } else { - Err(()) - }; - } + let command: Vec = environment::get_arguments().into_iter().skip(1).collect(); + if let [flag] = command.as_slice() + && (flag == "-h" || flag == "--help") + { + print(stdout::write_via_stream, USAGE).await; + return Ok(()); + } + + let mut stdout = Output::new(stdout::write_via_stream); + let result = if command.is_empty() { + session(&mut stdout).await + } else { + exec(command, &mut stdout).await }; - match exec(cli).await { - Err(e) => { - eprintln!("Error: {e}"); - Err(()) - } - _ => Ok(()), + stdout.close().await; + if let Err(error) = result { + print(stderr::write_via_stream, &render_error(error)).await; + return Err(()); } + Ok(()) } } -#[derive(Parser)] -#[command(name = "cli", version, about, long_about = None)] -struct Cli { - /// Host or ip address hosting the Valkey service - #[arg(long, default_value = "127.0.0.1")] - host: String, - - /// Port hosting the Valkey service - #[arg(long, default_value = "6379")] - port: u16, - - /// Authentication username - #[arg(short, long, default_value = "default")] - username: String, - - /// Authentication password - #[arg(short, long)] - password: Option, +/// Sends the command, printing its reply, or the push messages a command without one causes. +async fn exec(command: Vec, out: &mut Output) -> Result<(), Error> { + let connection = connector::connect().await?; + // before the command is sent, so none of the push messages it causes are missed + let mut pushes = connection.pushes(); + let name = command[0].to_ascii_uppercase(); + let channels = command.len() - 1; + let command = command.into_iter().map(Value::BulkString).collect(); + + if SUBSCRIBE.contains(&name.as_str()) { + connection.send(command).await?; + print_pushes(&mut pushes, out, None).await + } else if UNSUBSCRIBE.contains(&name.as_str()) { + // a push message for each channel, or one when no channel is given, as a new connection is + // not subscribed to any + connection.send(command).await?; + print_pushes(&mut pushes, out, Some(channels.max(1))).await + } else if name == "MONITOR" { + // the feed follows the reply, values sent unprompted that the connection treats as push + // messages + let reply = connection.call(command).await?; + out.line(&render(reply)).await; + print_pushes(&mut pushes, out, None).await + } else { + let reply = connection.call(command).await?; + out.line(&render(reply)).await; + Ok(()) + } +} - /// Client name - #[arg(long)] - client_name: Option, +/// Prints push messages, up to `limit` of them, failing once they stop. +async fn print_pushes( + pushes: &mut StreamReader, + out: &mut Output, + limit: Option, +) -> Result<(), Error> { + for _ in 0..limit.unwrap_or(usize::MAX) { + match pushes.next().await { + // sent unprompted, e.g. for a SUBSCRIBE missing its channels + Some(Value::Error(message) | Value::BulkError(message)) => { + return Err(Error::Valkey(message)); + } + Some(push) => out.line(&render(push)).await, + None => { + return Err(Error::Client( + "push messages stopped, the connection closed or they were not read fast enough" + .to_string(), + )); + } + } + } + Ok(()) +} - #[command(subcommand)] - command: Commands, +/// Sends each line of stdin as a command, printing the replies, and push messages as they arrive, +/// until stdin closes. +async fn session(out: &mut Output) -> Result<(), Error> { + let connection = connector::connect().await?; + let pushes = connection.pushes(); + let (stdin, _stdin_result) = stdin::read_via_stream(); + + // replies and push messages arrive independently, both are printed by one writer + let (lines_tx, lines_rx) = mpsc::unbounded(); + let mut commands = Some(Box::pin(send_lines(&connection, stdin, lines_tx.clone()))); + let mut forwarding = Some(Box::pin(forward_pushes(pushes, lines_tx))); + let mut writing = pin!(write_lines(lines_rx, out)); + let mut result = Ok(()); + poll_fn(|cx| { + let sent = match &mut commands { + Some(commands) => commands.as_mut().poll(cx), + None => Poll::Pending, + }; + if let Poll::Ready(sent) = sent { + result = sent; + // dropping the senders lets the writer finish + commands = None; + forwarding = None; + } + if let Some(pushes) = &mut forwarding + && pushes.as_mut().poll(cx).is_ready() + { + forwarding = None; + } + writing.as_mut().poll(cx) + }) + .await; + result } -#[derive(Subcommand)] -enum Commands { - /// Send a raw command - SEND { - #[arg()] - cmd: Vec, - }, +async fn send_lines( + connection: &Connection, + stdin: StreamReader, + lines: mpsc::UnboundedSender, +) -> Result<(), Error> { + let mut input = Lines::new(stdin); + while let Some(line) = input.next().await { + let words = match split(&line) { + Ok(words) => words, + Err(message) => { + let _ = lines.unbounded_send(format!("(error) {message}")); + continue; + } + }; + let Some(name) = words.first().map(|name| name.to_ascii_uppercase()) else { + continue; + }; + let command = words.into_iter().map(Value::BulkString).collect(); + if SUBSCRIBE.contains(&name.as_str()) || UNSUBSCRIBE.contains(&name.as_str()) { + // no reply, the push messages confirming it are printed as they arrive + connection.send(command).await?; + continue; + } + let text = match connection.call(command).await { + Ok(reply) => render(reply), + // the command failed, not the session + Err(Error::Valkey(message)) => format!("(error) {message}"), + Err(error) => return Err(error), + }; + let _ = lines.unbounded_send(text); + } + Ok(()) +} - /// Access Control List commands - ACL(ACLArgs), - /// Removes the specified keys - DEL { - /// Key to delete - #[arg()] - key: String, - }, - /// Returns if key exists - EXISTS { - /// Key to check - #[arg()] - key: String, - }, - /// Get the value of key - GET { - /// Key to get - #[arg()] - key: String, - }, - /// Removes the specified fields from the hash stored at key - HDEL { - /// Key for hash - #[arg()] - key: String, - - /// Field to delete - #[arg()] - field: String, - }, - /// Current server and connection properties - HELLO, - /// Returns if the field exists in the hash stored at key - HEXISTS { - /// Key for hash - #[arg()] - key: String, - - /// Field to check - #[arg()] - field: String, - }, - /// Returns the value associated with field in the hash stored at key - HGET { - /// Key for hash - #[arg()] - key: String, - - /// Field to get - #[arg()] - field: String, - }, - /// Returns all fields and values of the hash stored at key - HGETALL { - /// Key for hash - #[arg()] - key: String, - }, - /// Increments the number stored at field in the hash stored at key by increment - HINCRBY { - /// Key for hash - #[arg()] - key: String, - - /// Field to increment - #[arg()] - field: String, - - /// Amount to increment by - #[arg(allow_hyphen_values = true)] - increment: i64, - }, - /// Increments the number stored at field in the hash stored at key by increment - HINCRBYFLOAT { - /// Key for hash - #[arg()] - key: String, - - /// Field to increment - #[arg()] - field: String, - - /// Amount to increment by - #[arg(allow_hyphen_values = true)] - increment: f64, - }, - /// Returns all field names in the hash stored at key - HKEYS { - /// Key for hash - #[arg()] - key: String, - }, - /// Returns the number of fields contained in the hash stored at key - HLEN { - /// Key for hash - #[arg()] - key: String, - }, - /// Returns the values associated with the specified fields in the hash stored at key - HMGET { - /// Key for hash - #[arg()] - key: String, - - /// Fields to get - #[arg()] - fields: Vec, - }, - /// Sets the specified fields to their respective values in the hash stored at key - HMSET { - /// Key for hash - #[arg()] - key: String, - - /// Field-value pairs to set - #[arg(value_name = "FIELD VALUE", required = true)] - fields: Vec, - }, - /// Random field from the hash value stored at key - HRANDFIELD { - /// Key for hash - #[arg()] - key: String, - - /// Number of fields to return - #[arg(short, long, default_value = "1")] - count: Option, - - /// Return the corresponding values along with the keys in the hash table - #[arg(long, action = ArgAction::SetTrue)] - with_values: Option, - }, - /// Incrementally iterate over a collection of fields in a hash stored at key - HSCAN { - /// Key for hash - #[arg()] - key: String, - - /// Cursor to resume a previous scan - #[arg()] - cursor: Option, - - /// Only iterate elements matching a given glob-style pattern - #[arg(short, long)] - match_: Option, - - /// Amount of work that should be done at every call in order to retrieve elements from the collection - #[arg(short, long)] - count: Option, - - /// Return only the keys in the hash table without their corresponding values - #[arg(long, action = ArgAction::SetTrue)] - no_values: Option, - }, - /// Sets the specified field to a value in the hash stored at key - HSET { - /// Key for hash - #[arg()] - key: String, - - /// Field to set - #[arg()] - field: String, - - /// Value to set - #[arg()] - value: String, - }, - /// Sets field in the hash stored at key to value, only if field does not yet exist - HSETNX { - /// Key for hash - #[arg()] - key: String, - - /// Field to set - #[arg()] - field: String, - - /// Value to set - #[arg()] - value: String, - }, - /// Returns the string length of the value associated with field in the hash stored at key - HSTRLEN { - /// Key for hash - #[arg()] - key: String, - - /// Field to check - #[arg()] - field: String, - }, - /// Returns all values in the hash stored at key - HVALS { - /// Key for hash - #[arg()] - key: String, - }, - /// Increments the number stored at key by one - INCR { - /// Key to increment - #[arg()] - key: String, - }, - /// Increments the number stored at key by increment - INCRBY { - /// Key to increment - #[arg()] - key: String, - - /// Amount to increment by - #[arg(allow_hyphen_values = true)] - increment: i64, - }, - /// Returns all keys matching pattern - KEYS { - /// Pattern for matching keys - #[arg()] - pattern: String, - }, - /// Posts a message to the given channel - PUBLISH { - /// Channel where message should be sent - #[arg()] - channel: String, - - /// Message to send - #[arg()] - message: String, - }, - /// Set key to hold the string value - SET { - /// Key to set - #[arg()] - key: String, - - /// Value to set - #[arg()] - value: String, - }, +async fn forward_pushes(mut pushes: StreamReader, lines: mpsc::UnboundedSender) { + while let Some(push) = pushes.next().await { + let _ = lines.unbounded_send(render(push)); + } } -#[derive(Args)] -struct ACLArgs { - #[command(subcommand)] - command: ACLCommands, +/// Prints lines until every sender is gone. +async fn write_lines(mut lines: mpsc::UnboundedReceiver, out: &mut Output) { + while let Some(line) = poll_fn(|cx| Pin::new(&mut lines).poll_next(cx)).await { + out.line(&line).await; + } } -#[derive(Subcommand)] -enum ACLCommands { - /// Deletes the specified ACL user - DELUSER { - /// User to delete - #[arg()] - username: String, - }, - /// Generates a long and strong password - GENPASS, - /// Create an ACL user with the specified rules or modify the rules of an existing user - SETUSER { - /// User to set - #[arg()] - username: String, - - /// ACL rules to set - #[arg()] - rules: Vec, - }, +/// The lines of a stream of bytes. +struct Lines { + stream: StreamReader, + buffer: Vec, + closed: bool, } -async fn exec(cli: Cli) -> Result<(), Error> { - let opts = ConnectOpts { - auth: cli.password.map(|password| (cli.username, password)), - client_name: cli.client_name, - keep_alive_interval: None, - }; - let connection = tcp::connect(cli.host, cli.port, Some(opts)).await?; - - match &cli.command { - Commands::SEND { cmd } => { - let cmd: Vec = cmd - .iter() - .map(|c| Value::BulkString(c.to_string())) - .collect(); - let response = connection.call(cmd).await?; - println!("{response}"); - } - Commands::ACL(aclargs) => match &aclargs.command { - ACLCommands::DELUSER { username } => { - admin_commands::acl_deluser(&connection, username.to_string()).await?; - println!("Deleted user {username}"); - } - ACLCommands::GENPASS => { - let pass = admin_commands::acl_genpass(&connection).await?; - println!("{pass}"); - } - ACLCommands::SETUSER { username, rules } => { - admin_commands::acl_setuser(&connection, username.to_string(), rules.to_vec()) - .await?; - println!("Set user {username}"); - } - }, - Commands::DEL { key } => { - keyspace_commands::del(&connection, key.to_string()).await?; - println!("Deleted {key}"); - } - Commands::EXISTS { key } => { - match keyspace_commands::exists(&connection, key.to_string()).await? { - true => println!("true"), - false => println!("false"), - } - } - Commands::GET { key } => match string_commands::get(&connection, key.to_string()).await? { - Some(value) => println!("{}", value), - None => println!(""), - }, - Commands::HDEL { key, field } => { - hash_commands::hdel(&connection, key.to_string(), field.to_string()).await?; - println!("Deleted {field}"); - } - Commands::HELLO => { - for (key, value) in connection_commands::hello(&connection, None).await? { - match value { - Value::Null => println!("{key}: "), - Value::String(value) => println!("{key}: {value}"), - Value::Integer(value) => println!("{key}: {value}"), - Value::BulkString(value) => println!("{key}: {value}"), - Value::Array(items) => { - if items.is_empty() { - println!("{key}: "); - continue; - } - println!("{key}:"); - for item in items { - match resp::decode(&item).map_err(|e| Error::Resp(e))? { - Value::Null => println!("- "), - Value::String(item) => println!("- {item}"), - Value::Integer(item) => println!("- {item}"), - Value::BulkString(item) => println!("- {item}"), - item => { - println!("- {}", indent(&item.to_string(), " ")) - } - } - } - } - value => println!("{key}:\n {}", indent(&value.to_string(), " ")), - } - } - } - Commands::HEXISTS { key, field } => { - match hash_commands::hexists(&connection, key.to_string(), field.to_string()).await? { - true => println!("true"), - false => println!("false"), - } +impl Lines { + fn new(stream: StreamReader) -> Self { + Lines { + stream, + buffer: Vec::new(), + closed: false, } - Commands::HGET { key, field } => { - match hash_commands::hget(&connection, key.to_string(), field.to_string()).await? { - Some(value) => println!("{}", value), - None => println!(""), - } - } - Commands::HGETALL { key } => { - let fields = hash_commands::hgetall(&connection, key.to_string()).await?; - if fields.len() == 0 { - println!(""); - } - for (key, value) in fields { - println!("- {key}: {value}"); - } - } - Commands::HINCRBY { - key, - field, - increment, - } => { - let value = - hash_commands::hincrby(&connection, key.to_string(), field.to_string(), *increment) - .await?; - println!("{}", value); - } - Commands::HINCRBYFLOAT { - key, - field, - increment, - } => { - let value = hash_commands::hincrbyfloat( - &connection, - key.to_string(), - field.to_string(), - *increment, - ) - .await?; - println!("{}", value); - } - Commands::HKEYS { key } => { - for key in hash_commands::hkeys(&connection, key.to_string()).await? { - println!("- {key}"); + } + + /// The next line, without its line ending, or none once the stream closed. + async fn next(&mut self) -> Option { + loop { + if let Some(end) = self.buffer.iter().position(|byte| *byte == b'\n') { + let line: Vec = self.buffer.drain(..=end).collect(); + let line = String::from_utf8_lossy(&line); + return Some(line.trim_end_matches(['\n', '\r']).to_string()); } - } - Commands::HLEN { key } => { - let len = hash_commands::hlen(&connection, key.to_string()).await?; - println!("{len}"); - } - Commands::HMGET { key, fields } => { - let fields = - hash_commands::hmget(&connection, key.to_string().to_string(), fields.to_vec()) - .await?; - for field in fields { - match field { - None => println!("- "), - Some(value) => println!("- {value}"), - } + if self.closed { + // the last line, without a line ending + let line = std::mem::take(&mut self.buffer); + return (!line.is_empty()).then(|| String::from_utf8_lossy(&line).into_owned()); } - } - Commands::HMSET { key, fields } => { - if fields.len() % 2 != 0 { - Err(Error::Client( - "An equal number of FIELDs and VALUEs are required".to_string(), - ))?; + let mut buffer = std::mem::take(&mut self.buffer); + buffer.reserve(4096); + let (result, buffer) = self.stream.read(buffer).await; + self.buffer = buffer; + if !matches!(result, StreamResult::Complete(_)) { + self.closed = true; } - let fields: Vec<(String, String)> = fields - .chunks(2) - .map(|c| (c[0].clone(), c[1].clone())) - .collect(); - hash_commands::hmset(&connection, key.to_string(), fields.clone()).await?; - println!("Set {} field(s)", fields.len()); } - Commands::HRANDFIELD { - key, - count, - with_values, - } => { - let opts = HrandfieldOpts { - count: *count, - with_values: *with_values, - }; - let fields = - hash_commands::hrandfield(&connection, key.to_string(), Some(opts)).await?; - match fields { - None => println!(""), - Some(fields) => { - for (field, value) in fields { - match value { - None => println!("- {field}"), - Some(value) => println!("- {field}: {value}"), - } + } +} + +/// Splits a line into words, like valkey-cli. +/// +/// Words are separated by whitespace. A double quoted word may contain whitespace and escapes, e.g. +/// `\"`, `\n` and `\x41`, a single quoted word is taken as is, except for `\'`. +fn split(line: &str) -> Result, String> { + let mut words = vec![]; + let mut chars = line.chars().peekable(); + loop { + while chars.next_if(|c| c.is_whitespace()).is_some() {} + let Some(&first) = chars.peek() else { + return Ok(words); + }; + let mut word = String::new(); + if first == '"' || first == '\'' { + chars.next(); + loop { + match (chars.next(), first) { + (None, _) => return Err("unbalanced quotes".to_string()), + (Some(c), quote) if c == quote => break, + (Some('\\'), '"') => word.push(unescape(&mut chars)?), + (Some('\\'), '\'') if chars.peek() == Some(&'\'') => { + word.push(chars.next().unwrap_or_default()) } + (Some(c), _) => word.push(c), } } - } - Commands::HSCAN { - key, - cursor, - match_, - count, - no_values, - } => { - let opts = &HscanOpts { - match_: match_.clone(), - count: *count, - no_values: *no_values, - }; - let (cursor, fields) = hash_commands::hscan( - &connection, - key.to_string(), - cursor.clone(), - Some(opts.clone()), - ) - .await?; - if let Some(cursor) = cursor { - println!("(cursor) {cursor}"); - } - if fields.len() == 0 { - println!(""); - } - for (field, value) in fields { - match value { - None => println!("- {field}"), - Some(value) => println!("- {field}: {value}"), - } - } - } - Commands::HSET { key, field, value } => { - hash_commands::hset( - &connection, - key.to_string(), - field.to_string(), - value.to_string(), - ) - .await?; - println!("Set {field}"); - } - Commands::HSETNX { key, field, value } => { - match hash_commands::hsetnx( - &connection, - key.to_string(), - field.to_string(), - value.to_string(), - ) - .await? - { - false => println!("Field already set"), - true => println!("Set {field}"), - }; - } - Commands::HSTRLEN { key, field } => { - let len = - hash_commands::hstrlen(&connection, key.to_string(), field.to_string()).await?; - println!("{len}"); - } - Commands::HVALS { key } => { - let values = hash_commands::hvals(&connection, key.to_string()).await?; - if values.len() == 0 { - println!(""); + if chars.peek().is_some_and(|c| !c.is_whitespace()) { + return Err("closing quote must be followed by a space".to_string()); } - for value in values { - println!("- {value}"); + } else { + while let Some(c) = chars.next_if(|c| !c.is_whitespace()) { + word.push(c); } } - Commands::INCR { key } => { - let value = string_commands::incr(&connection, key.to_string()).await?; - println!("{}", value); - } - Commands::INCRBY { key, increment } => { - let value = - string_commands::incrby(&connection, key.to_string(), increment.clone()).await?; - println!("{}", value); - } - Commands::KEYS { pattern } => { - for key in keyspace_commands::keys(&connection, pattern.to_string()).await? { - println!("{key}"); + words.push(word); + } +} + +/// The character of an escape in a double quoted word, after its `\`. +fn unescape(chars: &mut impl Iterator) -> Result { + Ok(match chars.next() { + Some('n') => '\n', + Some('r') => '\r', + Some('t') => '\t', + Some('b') => '\u{8}', + Some('a') => '\u{7}', + Some('x') => { + let hex: String = chars.take(2).collect(); + match u8::from_str_radix(&hex, 16) { + // a string holds UTF-8, a byte on its own is only a character when it is ASCII + Ok(byte) if byte.is_ascii() => byte as char, + _ => return Err(format!("unsupported escape: \\x{hex}")), } } - Commands::PUBLISH { channel, message } => { - let value = - pubsub_commands::publish(&connection, channel.to_string(), message.to_string()) - .await?; - println!("{}", value); - } - Commands::SET { key, value } => { - string_commands::set(&connection, key.to_string(), value.to_string()).await?; - println!("Set {key}"); - } + Some(c) => c, + None => return Err("unbalanced quotes".to_string()), + }) +} + +/// A stream to stdout or stderr. +struct Output { + writer: StreamWriter, + written: FutureReader>, +} + +impl Output { + fn new(write_via_stream: fn(StreamReader) -> FutureReader>) -> Self { + let (writer, reader) = wit_stream::new(); + let written = write_via_stream(reader); + Output { writer, written } } - Ok(()) + async fn line(&mut self, text: &str) { + self.writer + .write_all(format!("{text}\n").into_bytes()) + .await; + } + + async fn close(self) { + // the host finishes writing once the writable end is dropped + drop(self.writer); + // nowhere left to report a failure to print + let _ = self.written.await; + } } -/// Indents each line after the first, for multi-line values printed after a prefix. -fn indent(value: &str, padding: &str) -> String { - value.replace('\n', &format!("\n{padding}")) +/// Prints a line to a new stream to stdout or stderr. +async fn print( + write_via_stream: fn(StreamReader) -> FutureReader>, + text: &str, +) { + let mut output = Output::new(write_via_stream); + output.line(text).await; + output.close().await; } -impl fmt::Display for Value { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Value::String(val) => write!(f, "{}", val), - Value::Error(val) => write!(f, "(error) {val}"), - Value::Integer(val) => write!(f, "(integer) {}", val), - Value::BulkString(val) => write!(f, "\"{}\"", val), - Value::Array(val) => { - if val.len() == 0 { - write!(f, "(empty array)")?; - } - let i_max_width = val.len().to_string().len(); - for (i, val) in val.iter().enumerate() { - let val = resp::decode(val).map_err(|_| fmt::Error)?.to_string(); - let prefix = format!("{:i_max_width$}) ", i + 1); - let mut padding = String::new(); - for _ in 0..prefix.len() { - padding.push(' ') - } - if i != 0 { - write!(f, "\n")?; - } - for (j, line) in val.lines().enumerate() { - match j { - 0 => write!(f, "{prefix}{line}")?, - _ => write!(f, "\n{padding}{line}")?, - } - } - } - Ok(()) - } - Value::Null => write!(f, "(nil)"), - Value::Boolean(val) => match val { - false => write!(f, "(false)"), - true => write!(f, "(true)"), - }, - Value::Double(val) => write!(f, "(double) {val}"), - Value::BigNumber(val) => write!(f, "(number) {val}"), - Value::BulkError(val) => write!(f, "(error) {val}"), - // text to show as is, e.g. from INFO, the encoding is `txt` or `mkd` - Value::VerbatimString((_encoding, val)) => write!(f, "{val}"), - Value::Map(val) => { - if val.len() == 0 { - write!(f, "(empty map)")?; - } - let i_max_width = val.len().to_string().len(); - for (i, (key, val)) in val.iter().enumerate() { - let key = resp::decode(key).map_err(|_| fmt::Error)?.to_string(); - let val = resp::decode(val).map_err(|_| fmt::Error)?.to_string(); - if i != 0 { - write!(f, "\n")?; - } - // a key over several lines is indented under its number, the value follows its - // last line - let number = format!("{:i_max_width$}# ", i + 1); - let key_lines = lines(&key); - let (last, first) = key_lines.split_last().expect("at least one line"); - for (j, line) in first.iter().enumerate() { - let indent = if j == 0 { - number.clone() - } else { - " ".repeat(number.len()) - }; - write!(f, "{indent}{line}\n")?; - } - let indent = if first.is_empty() { - number.clone() - } else { - " ".repeat(number.len()) - }; - let prefix = format!("{indent}{last} => "); - let padding = " ".repeat(prefix.len()); - for (j, line) in lines(&val).into_iter().enumerate() { - match j { - 0 => write!(f, "{prefix}{line}")?, - _ => write!(f, "\n{padding}{line}")?, - } - } - } - Ok(()) - } - Value::Set(val) => { - if val.len() == 0 { - write!(f, "(empty set)")?; - } - let i_max_width = val.len().to_string().len(); - for (i, val) in val.iter().enumerate() { - let val = resp::decode(val).map_err(|_| fmt::Error)?.to_string(); - let prefix = format!("{:i_max_width$}~ ", i + 1); - let mut padding = String::new(); - for _ in 0..prefix.len() { - padding.push(' ') - } - if i != 0 { - write!(f, "\n")?; - } - for (j, line) in val.lines().enumerate() { - match j { - 0 => write!(f, "{prefix}{line}")?, - _ => write!(f, "\n{padding}{line}")?, - } - } - } - Ok(()) - } - Value::Push(val) => { - if val.len() == 0 { - write!(f, "(empty push)")?; - } - let i_max_width = val.len().to_string().len(); - for (i, val) in val.iter().enumerate() { - let val = resp::decode(val).map_err(|_| fmt::Error)?.to_string(); - let prefix = format!("{:i_max_width$}) ", i + 1); - let mut padding = String::new(); - for _ in 0..prefix.len() { - padding.push(' ') - } - if i != 0 { - write!(f, "\n")?; - } - for (j, line) in val.lines().enumerate() { - match j { - 0 => write!(f, "{prefix}{line}")?, - _ => write!(f, "\n{padding}{line}")?, - } - } - } - Ok(()) - } +fn render_error(error: Error) -> String { + match error { + Error::Valkey(message) => format!("(error) {message}"), + Error::Closed(None) => "connection closed".to_string(), + Error::Closed(Some(reason)) => format!("connection closed: {reason}"), + Error::Client(message) => message, + Error::Resp(message) => format!("invalid RESP: {message}"), + } +} + +/// Renders a value for people to read, in the style of valkey-cli. +fn render(value: Value) -> String { + match value { + Value::String(text) => text, + Value::Error(message) | Value::BulkError(message) => format!("(error) {message}"), + Value::Integer(value) => format!("(integer) {value}"), + // quoted, with special characters escaped + Value::BulkString(text) => format!("{text:?}"), + Value::Null => "(nil)".to_string(), + Value::Boolean(value) => format!("({value})"), + Value::Double(value) => format!("(double) {value}"), + Value::BigNumber(value) => format!("(big number) {value}"), + // text to show as is, e.g. from INFO, the format is `txt` or `mkd` + Value::VerbatimString((_format, text)) => text, + Value::Array(items) | Value::Push(items) => { + numbered(items.into_iter().map(render_nested).collect(), ')', "array") } + Value::Set(items) => numbered(items.into_iter().map(render_nested).collect(), '~', "set"), + Value::Map(entries) => numbered( + entries + .into_iter() + .map(|(key, value)| { + indent( + &format!("{} => ", render_nested(key)), + &render_nested(value), + ) + }) + .collect(), + '#', + "map", + ), } } -/// The lines of a value, an empty value is one empty line. -fn lines(value: &str) -> Vec<&str> { - let lines: Vec<&str> = value.lines().collect(); - if lines.is_empty() { vec![""] } else { lines } +fn render_nested(value: NestedValue) -> String { + match resp_codec::decode(value) { + Ok(value) => render(value), + Err(error) => format!("(invalid) {error}"), + } +} + +/// Numbers each item, e.g. `1) `, the numbers right aligned. +fn numbered(items: Vec, marker: char, kind: &str) -> String { + if items.is_empty() { + return format!("(empty {kind})"); + } + let width = items.len().to_string().len(); + items + .iter() + .enumerate() + .map(|(i, item)| indent(&format!("{:>width$}{marker} ", i + 1), item)) + .collect::>() + .join("\n") +} + +/// Prefixes the text, lining its other lines up under the first. +fn indent(prefix: &str, text: &str) -> String { + let last_line = prefix.rsplit('\n').next().unwrap_or_default(); + let padding = format!("\n{}", " ".repeat(last_line.chars().count())); + format!("{prefix}{}", text.replace('\n', &padding)) } wit_bindgen::generate!({ - world: "cli", + // `path` is relative to the manifest, `components/dep-cli/` path: "../wit", - generate_all + world: "cli", + // the resp types shared by every component + with: { + "componentized:valkey/resp@0.3.0-dev": resp_codec::bindings::componentized::valkey::resp, + }, + generate_all, }); export!(Cli); diff --git a/components/hash-commands/Cargo.toml b/components/hash-commands/Cargo.toml deleted file mode 100644 index d13c141..0000000 --- a/components/hash-commands/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "hash-commands" -version = "0.1.0" -edition = "2024" -license = "Apache-2.0" - -[lib] -crate-type = ["cdylib"] -# shared with the other command components -path = "../../crates/commands/src/lib.rs" - -[features] -default = ["hash"] -admin = [] -connection = [] -hash = [] -keyspace = [] -pubsub = [] -string = [] - -[dependencies] -resp-codec = { workspace = true } -wit-bindgen = { workspace = true } diff --git a/components/hash-commands/README.md b/components/hash-commands/README.md deleted file mode 100644 index 5ef15cc..0000000 --- a/components/hash-commands/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# hash-commands - -Commands for hash values, Valkey's @hash ACL category. - -Exports `componentized:valkey/hash-commands` over an imported [`connection`](../connection/). [`commands`](../commands/) exports every command interface in one component. diff --git a/components/keyspace-commands/Cargo.toml b/components/keyspace-commands/Cargo.toml deleted file mode 100644 index 2906ff7..0000000 --- a/components/keyspace-commands/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "keyspace-commands" -version = "0.1.0" -edition = "2024" -license = "Apache-2.0" - -[lib] -crate-type = ["cdylib"] -# shared with the other command components -path = "../../crates/commands/src/lib.rs" - -[features] -default = ["keyspace"] -admin = [] -connection = [] -hash = [] -keyspace = [] -pubsub = [] -string = [] - -[dependencies] -resp-codec = { workspace = true } -wit-bindgen = { workspace = true } diff --git a/components/keyspace-commands/README.md b/components/keyspace-commands/README.md deleted file mode 100644 index 73cf5a0..0000000 --- a/components/keyspace-commands/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# keyspace-commands - -Commands for keys of any type, Valkey's @keyspace ACL category. - -Exports `componentized:valkey/keyspace-commands` over an imported [`connection`](../connection/). [`commands`](../commands/) exports every command interface in one component. diff --git a/components/pubsub-commands/Cargo.toml b/components/pubsub-commands/Cargo.toml deleted file mode 100644 index 62a6376..0000000 --- a/components/pubsub-commands/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "pubsub-commands" -version = "0.1.0" -edition = "2024" -license = "Apache-2.0" - -[lib] -crate-type = ["cdylib"] -# shared with the other command components -path = "../../crates/commands/src/lib.rs" - -[features] -default = ["pubsub"] -admin = [] -connection = [] -hash = [] -keyspace = [] -pubsub = [] -string = [] - -[dependencies] -resp-codec = { workspace = true } -wit-bindgen = { workspace = true } diff --git a/components/pubsub-commands/README.md b/components/pubsub-commands/README.md deleted file mode 100644 index 4682fa0..0000000 --- a/components/pubsub-commands/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# pubsub-commands - -Publish/subscribe commands, Valkey's @pubsub ACL category. - -Exports `componentized:valkey/pubsub-commands` over an imported [`connection`](../connection/). [`commands`](../commands/) exports every command interface in one component. diff --git a/components/string-commands/Cargo.toml b/components/string-commands/Cargo.toml deleted file mode 100644 index 7ad1a39..0000000 --- a/components/string-commands/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "string-commands" -version = "0.1.0" -edition = "2024" -license = "Apache-2.0" - -[lib] -crate-type = ["cdylib"] -# shared with the other command components -path = "../../crates/commands/src/lib.rs" - -[features] -default = ["string"] -admin = [] -connection = [] -hash = [] -keyspace = [] -pubsub = [] -string = [] - -[dependencies] -resp-codec = { workspace = true } -wit-bindgen = { workspace = true } diff --git a/components/string-commands/README.md b/components/string-commands/README.md deleted file mode 100644 index 8ddce74..0000000 --- a/components/string-commands/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# string-commands - -Commands for string values, Valkey's @string ACL category. - -Exports `componentized:valkey/string-commands` over an imported [`connection`](../connection/). [`commands`](../commands/) exports every command interface in one component. diff --git a/components/test-commands/README.md b/components/test-commands/README.md deleted file mode 100644 index e7e0173..0000000 --- a/components/test-commands/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# test-commands - -`commands` composed with `connection`, `resp` and `tcp`, to test the commands over a stub transport or against a server. diff --git a/components/test-commands/test-commands.wac b/components/test-commands/test-commands.wac deleted file mode 100644 index 5849eda..0000000 --- a/components/test-commands/test-commands.wac +++ /dev/null @@ -1,21 +0,0 @@ -package componentized:valkey; - -let resp = new local:resp { ... }; -let connection = new local:connection { - "componentized:valkey/resp@0.3.0-dev": resp.resp, - ... -}; -let tcp = new local:tcp { - "componentized:valkey/resp@0.3.0-dev": resp.resp, - "componentized:valkey/connection@0.3.0-dev": connection.connection, - ... -}; -let commands = new local:commands { - "componentized:valkey/resp@0.3.0-dev": resp.resp, - "componentized:valkey/connection@0.3.0-dev": connection.connection, - ... -}; - -export connection.connection; -export tcp.tcp; -export commands...; diff --git a/components/wit/worlds.wit b/components/wit/worlds.wit index 31538b2..e11f898 100644 --- a/components/wit/worlds.wit +++ b/components/wit/worlds.wit @@ -1,17 +1,5 @@ package componentized:valkey-components; -world cli { - include wasi:cli/imports@0.3.0; - import componentized:valkey/tcp@0.3.0-dev; - import componentized:valkey/admin-commands@0.3.0-dev; - import componentized:valkey/connection-commands@0.3.0-dev; - import componentized:valkey/hash-commands@0.3.0-dev; - import componentized:valkey/keyspace-commands@0.3.0-dev; - import componentized:valkey/pubsub-commands@0.3.0-dev; - import componentized:valkey/string-commands@0.3.0-dev; - export wasi:cli/run@0.3.0; -} - /// The RESP codec, for the components that import the resp types. world resp { export componentized:valkey/resp@0.3.0-dev; @@ -23,6 +11,11 @@ world connection { export componentized:valkey/connection@0.3.0-dev; } +world commands { + import componentized:valkey/connection@0.3.0-dev; + export componentized:valkey/commands@0.3.0-dev; +} + /// Connections over wasi:sockets TCP. world tcp { import wasi:sockets/types@0.3.0; @@ -31,47 +24,6 @@ world tcp { export componentized:valkey/tcp@0.3.0-dev; } -/// Commands, one world per ACL category. -world admin-commands { - import componentized:valkey/connection@0.3.0-dev; - export componentized:valkey/admin-commands@0.3.0-dev; -} - -world connection-commands { - import componentized:valkey/connection@0.3.0-dev; - export componentized:valkey/connection-commands@0.3.0-dev; -} - -world hash-commands { - import componentized:valkey/connection@0.3.0-dev; - export componentized:valkey/hash-commands@0.3.0-dev; -} - -world keyspace-commands { - import componentized:valkey/connection@0.3.0-dev; - export componentized:valkey/keyspace-commands@0.3.0-dev; -} - -world pubsub-commands { - import componentized:valkey/connection@0.3.0-dev; - export componentized:valkey/pubsub-commands@0.3.0-dev; -} - -world string-commands { - import componentized:valkey/connection@0.3.0-dev; - export componentized:valkey/string-commands@0.3.0-dev; -} - -/// Every implemented command interface, in one component. -world commands { - include admin-commands; - include connection-commands; - include hash-commands; - include keyspace-commands; - include pubsub-commands; - include string-commands; -} - /// A connector for connections over TCP, to the server configured with wasi:config. world tcp-connector { import wasi:config/store@0.2.0-rc.1; @@ -81,7 +33,7 @@ world tcp-connector { world as-keyvalue { import componentized:valkey/connector@0.3.0-dev; - import componentized:valkey/hash-commands@0.3.0-dev; + import componentized:valkey/commands@0.3.0-dev; import wasi:config/store@0.2.0-rc.1; export wasmcloud:keyvalue/types@0.2.0; export wasmcloud:keyvalue/store@0.2.0; @@ -94,3 +46,10 @@ world sample-http-incrementor { import wasmcloud:keyvalue/atomics@0.2.0; export wasi:http/handler@0.3.0; } + +/// A cli sending its arguments as a command over a connection from the connector. +world cli { + include wasi:cli/imports@0.3.0; + import componentized:valkey/connector@0.3.0-dev; + export wasi:cli/run@0.3.0; +} diff --git a/crates/commands/src/admin.rs b/crates/commands/src/admin.rs deleted file mode 100644 index 17550f2..0000000 --- a/crates/commands/src/admin.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! `admin-commands`, Valkey's @admin ACL category, and the rest of the ACL family. - -use crate::componentized::valkey::connection::{Connection, Error}; -use crate::exports::componentized::valkey::admin_commands::Guest; -use crate::reply::{call, integer, ok, string}; - -impl Guest for crate::Commands { - async fn acl_deluser(connection: &Connection, username: String) -> Result<(), Error> { - // https://valkey.io/commands/acl-deluser/ - // ACL DELUSER username [ username ... ] - integer( - call( - connection, - ["ACL".to_string(), "DELUSER".to_string(), username], - ) - .await?, - )?; - Ok(()) - } - - async fn acl_genpass(connection: &Connection) -> Result { - // https://valkey.io/commands/acl-genpass/ - // ACL GENPASS [ bits ] - string(call(connection, ["ACL", "GENPASS"]).await?) - } - - async fn acl_setuser( - connection: &Connection, - username: String, - rules: Vec, - ) -> Result<(), Error> { - // https://valkey.io/commands/acl-setuser/ - // ACL SETUSER username [ rule ] [ [ rule ] ... ] - let command = ["ACL".to_string(), "SETUSER".to_string(), username] - .into_iter() - .chain(rules); - ok(call(connection, command).await?) - } -} diff --git a/crates/commands/src/connection.rs b/crates/commands/src/connection.rs deleted file mode 100644 index db8ca36..0000000 --- a/crates/commands/src/connection.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! `connection-commands`, Valkey's @connection ACL category. - -use crate::componentized::valkey::connection::{Connection, Error}; -use crate::exports::componentized::valkey::connection_commands::{Guest, HelloOpts}; -use crate::reply::{call, entries, ok, string, unexpected}; -use resp_codec::Value; - -impl Guest for crate::Commands { - async fn auth( - connection: &Connection, - username: String, - password: String, - ) -> Result<(), Error> { - // https://valkey.io/commands/auth/ - // AUTH [ username ] password - ok(call(connection, ["AUTH".to_string(), username, password]).await?) - } - - async fn hello( - connection: &Connection, - opts: Option, - ) -> Result, Error> { - // https://valkey.io/commands/hello/ - // HELLO [ protover [ AUTH username password ] [ SETNAME clientname ] ] - let mut command = vec!["HELLO".to_string(), "3".to_string()]; - if let Some(opts) = opts { - if let Some((username, password)) = opts.auth { - command.extend(["AUTH".to_string(), username, password]); - } - if let Some(client_name) = opts.client_name { - command.extend(["SETNAME".to_string(), client_name]); - } - } - entries(call(connection, command).await?)? - .into_iter() - .map(|(name, value)| Ok((string(name)?, value))) - .collect() - } - - async fn ping(connection: &Connection) -> Result<(), Error> { - // https://valkey.io/commands/ping/ - // PING [ message ] - match call(connection, ["PING"]).await? { - Value::String(reply) if reply == "PONG" => Ok(()), - reply => Err(unexpected(&reply)), - } - } -} diff --git a/crates/commands/src/hash.rs b/crates/commands/src/hash.rs deleted file mode 100644 index c12c6bd..0000000 --- a/crates/commands/src/hash.rs +++ /dev/null @@ -1,332 +0,0 @@ -//! `hash-commands`, Valkey's @hash ACL category. - -use crate::componentized::valkey::connection::{Connection, Error}; -use crate::exports::componentized::valkey::hash_commands::{ - Duration, ExpireMode, ExpireResult, Guest, HrandfieldOpts, HscanOpts, Instant, -}; -use crate::reply::{ - boolean, call, integer, items, ok, optional_string, optional_strings, string, string_entries, - strings, unexpected, unsigned, whole_seconds, -}; -use resp_codec::Value; - -impl Guest for crate::Commands { - async fn hdel(connection: &Connection, key: String, field: String) -> Result<(), Error> { - // https://valkey.io/commands/hdel/ - // HDEL key field [ field ... ] - integer(call(connection, ["HDEL".to_string(), key, field]).await?)?; - Ok(()) - } - - async fn hexists(connection: &Connection, key: String, field: String) -> Result { - // https://valkey.io/commands/hexists/ - // HEXISTS key field - boolean(call(connection, ["HEXISTS".to_string(), key, field]).await?) - } - - async fn hexpire( - connection: &Connection, - key: String, - seconds: Duration, - mode: Option, - fields: Vec, - ) -> Result, Error> { - // https://valkey.io/commands/hexpire/ - // HEXPIRE key seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] - let mut command = vec!["HEXPIRE".to_string(), key, whole_seconds(seconds)]; - command.extend(mode.map(expire_mode)); - command.extend(expire_fields(fields)); - expire_results(call(connection, command).await?) - } - - async fn hexpireat( - connection: &Connection, - key: String, - seconds: Instant, - mode: Option, - fields: Vec, - ) -> Result, Error> { - // https://valkey.io/commands/hexpireat/ - // HEXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] - let mut command = vec!["HEXPIREAT".to_string(), key, seconds.seconds.to_string()]; - command.extend(mode.map(expire_mode)); - command.extend(expire_fields(fields)); - expire_results(call(connection, command).await?) - } - - async fn hget( - connection: &Connection, - key: String, - field: String, - ) -> Result, Error> { - // https://valkey.io/commands/hget/ - // HGET key field - optional_string(call(connection, ["HGET".to_string(), key, field]).await?) - } - - async fn hgetall(connection: &Connection, key: String) -> Result, Error> { - // https://valkey.io/commands/hgetall/ - // HGETALL key - string_entries(call(connection, ["HGETALL".to_string(), key]).await?) - } - - async fn hincrby( - connection: &Connection, - key: String, - field: String, - increment: i64, - ) -> Result { - // https://valkey.io/commands/hincrby/ - // HINCRBY key field increment - integer( - call( - connection, - ["HINCRBY".to_string(), key, field, increment.to_string()], - ) - .await?, - ) - } - - async fn hincrbyfloat( - connection: &Connection, - key: String, - field: String, - increment: f64, - ) -> Result { - // https://valkey.io/commands/hincrbyfloat/ - // HINCRBYFLOAT key field increment - let command = [ - "HINCRBYFLOAT".to_string(), - key, - field, - increment.to_string(), - ]; - match call(connection, command).await? { - Value::Double(value) => Ok(value.to_string()), - reply => string(reply), - } - } - - async fn hkeys(connection: &Connection, key: String) -> Result, Error> { - // https://valkey.io/commands/hkeys/ - // HKEYS key - strings(call(connection, ["HKEYS".to_string(), key]).await?) - } - - async fn hlen(connection: &Connection, key: String) -> Result { - // https://valkey.io/commands/hlen/ - // HLEN key - unsigned(call(connection, ["HLEN".to_string(), key]).await?) - } - - async fn hmget( - connection: &Connection, - key: String, - fields: Vec, - ) -> Result>, Error> { - // https://valkey.io/commands/hmget/ - // HMGET key field [ field ... ] - let command = ["HMGET".to_string(), key].into_iter().chain(fields); - optional_strings(call(connection, command).await?) - } - - async fn hmset( - connection: &Connection, - key: String, - fields: Vec<(String, String)>, - ) -> Result<(), Error> { - // https://valkey.io/commands/hmset/ - // HMSET key field value [ field value ... ] - let command = ["HMSET".to_string(), key] - .into_iter() - .chain(fields.into_iter().flat_map(|(field, value)| [field, value])); - ok(call(connection, command).await?) - } - - async fn hrandfield( - connection: &Connection, - key: String, - opts: Option, - ) -> Result)>>, Error> { - // https://valkey.io/commands/hrandfield/ - // HRANDFIELD key [ count [ WITHVALUES ] ] - let (count, with_values) = match opts { - Some(opts) => (opts.count, opts.with_values.unwrap_or(false)), - None => (None, false), - }; - let mut command = vec!["HRANDFIELD".to_string(), key]; - match (count, with_values) { - (None, true) => { - return Err(Error::Client( - "count must be specified to use with-values".to_string(), - )); - } - (None, false) => {} - (Some(count), with_values) => { - command.push(count.to_string()); - if with_values { - command.push("WITHVALUES".to_string()); - } - } - } - - let reply = call(connection, command).await?; - if count.is_none() { - // a single field, or null when the hash does not exist - return Ok(optional_string(reply)?.map(|field| vec![(field, None)])); - } - let items = items(reply)?; - if items.is_empty() { - return Ok(None); - } - let fields = if with_values { - field_value_pairs(items)? - } else { - items - .into_iter() - .map(|field| Ok((string(field)?, None))) - .collect::>()? - }; - Ok(Some(fields)) - } - - async fn hscan( - connection: &Connection, - key: String, - cursor: Option, - opts: Option, - ) -> Result<(Option, Vec<(String, Option)>), Error> { - // https://valkey.io/commands/hscan/ - // HSCAN key cursor [ MATCH pattern ] [ COUNT count ] [ NOVALUES ] - let mut command = vec![ - "HSCAN".to_string(), - key, - cursor.unwrap_or_else(|| "0".to_string()), - ]; - let mut no_values = false; - if let Some(opts) = opts { - if let Some(pattern) = opts.match_ { - command.extend(["MATCH".to_string(), pattern]); - } - if let Some(count) = opts.count { - command.extend(["COUNT".to_string(), count.to_string()]); - } - if opts.no_values == Some(true) { - command.push("NOVALUES".to_string()); - no_values = true; - } - } - - let reply = call(connection, command).await?; - let mut parts = items(reply.clone())?.into_iter(); - let (Some(cursor), Some(elements), None) = (parts.next(), parts.next(), parts.next()) - else { - return Err(unexpected(&reply)); - }; - let cursor = string(cursor)?; - let elements = items(elements)?; - let fields = if no_values { - elements - .into_iter() - .map(|field| Ok((string(field)?, None))) - .collect::>()? - } else { - field_value_pairs(elements)? - }; - // a cursor of 0 ends the scan - let cursor = (cursor != "0").then_some(cursor); - Ok((cursor, fields)) - } - - async fn hset( - connection: &Connection, - key: String, - field: String, - value: String, - ) -> Result<(), Error> { - // https://valkey.io/commands/hset/ - // HSET key field value [ field value ... ] - integer(call(connection, ["HSET".to_string(), key, field, value]).await?)?; - Ok(()) - } - - async fn hsetnx( - connection: &Connection, - key: String, - field: String, - value: String, - ) -> Result { - // https://valkey.io/commands/hsetnx/ - // HSETNX key field value - boolean(call(connection, ["HSETNX".to_string(), key, field, value]).await?) - } - - async fn hstrlen(connection: &Connection, key: String, field: String) -> Result { - // https://valkey.io/commands/hstrlen/ - // HSTRLEN key field - unsigned(call(connection, ["HSTRLEN".to_string(), key, field]).await?) - } - - async fn hvals(connection: &Connection, key: String) -> Result, Error> { - // https://valkey.io/commands/hvals/ - // HVALS key - strings(call(connection, ["HVALS".to_string(), key]).await?) - } -} - -/// Fields and their values, as `[field, value]` pairs (RESP3) or a flat array (RESP2). -fn field_value_pairs(items: Vec) -> Result)>, Error> { - if matches!(items.first(), Some(Value::Array(_))) { - return items - .into_iter() - .map(|pair| { - let mut pair = crate::reply::items(pair)?.into_iter(); - match (pair.next(), pair.next(), pair.next()) { - (Some(field), Some(value), None) => Ok((string(field)?, Some(string(value)?))), - _ => Err(Error::Client("unexpected field and value pair".to_string())), - } - }) - .collect(); - } - if items.len() % 2 != 0 { - return Err(Error::Client(format!( - "unexpected odd number of fields and values: {}", - items.len() - ))); - } - let mut items = items.into_iter(); - let mut fields = vec![]; - while let (Some(field), Some(value)) = (items.next(), items.next()) { - fields.push((string(field)?, Some(string(value)?))); - } - Ok(fields) -} - -fn expire_mode(mode: ExpireMode) -> String { - match mode { - ExpireMode::Nx => "NX", - ExpireMode::Xx => "XX", - ExpireMode::Gt => "GT", - ExpireMode::Lt => "LT", - } - .to_string() -} - -fn expire_fields(fields: Vec) -> impl Iterator { - ["FIELDS".to_string(), fields.len().to_string()] - .into_iter() - .chain(fields) -} - -fn expire_results(reply: Value) -> Result, Error> { - items(reply)? - .into_iter() - .map(|code| match integer(code)? { - -2 => Ok(ExpireResult::DoesNotExist), - 0 => Ok(ExpireResult::ConditionNotMet), - 1 => Ok(ExpireResult::Applied), - 2 => Ok(ExpireResult::ZeroSeconds), - code => Err(Error::Client(format!("unexpected reply: {code}"))), - }) - .collect() -} diff --git a/crates/commands/src/keyspace.rs b/crates/commands/src/keyspace.rs deleted file mode 100644 index 1598d2e..0000000 --- a/crates/commands/src/keyspace.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! `keyspace-commands`, Valkey's @keyspace ACL category. - -use crate::componentized::valkey::connection::{Connection, Error}; -use crate::exports::componentized::valkey::keyspace_commands::{ - Duration, ExpireMode, ExpireResult, Guest, Instant, -}; -use crate::reply::{boolean, call, integer, strings, whole_seconds}; - -impl Guest for crate::Commands { - async fn del(connection: &Connection, key: String) -> Result<(), Error> { - // https://valkey.io/commands/del/ - // DEL key [ key ... ] - integer(call(connection, ["DEL".to_string(), key]).await?)?; - Ok(()) - } - - async fn exists(connection: &Connection, key: String) -> Result { - // https://valkey.io/commands/exists/ - // EXISTS key [ key ... ] - boolean(call(connection, ["EXISTS".to_string(), key]).await?) - } - - async fn expire( - connection: &Connection, - key: String, - seconds: Duration, - mode: Option, - ) -> Result { - // https://valkey.io/commands/expire/ - // EXPIRE key seconds [ NX | XX | GT | LT ] - let mut command = vec!["EXPIRE".to_string(), key, whole_seconds(seconds)]; - command.extend(mode.map(expire_mode)); - expire_result(integer(call(connection, command).await?)?) - } - - async fn expireat( - connection: &Connection, - key: String, - seconds: Instant, - mode: Option, - ) -> Result { - // https://valkey.io/commands/expireat/ - // EXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] - let mut command = vec!["EXPIREAT".to_string(), key, seconds.seconds.to_string()]; - command.extend(mode.map(expire_mode)); - expire_result(integer(call(connection, command).await?)?) - } - - async fn keys(connection: &Connection, pattern: String) -> Result, Error> { - // https://valkey.io/commands/keys/ - // KEYS pattern - strings(call(connection, ["KEYS".to_string(), pattern]).await?) - } -} - -fn expire_mode(mode: ExpireMode) -> String { - match mode { - ExpireMode::Nx => "NX", - ExpireMode::Xx => "XX", - ExpireMode::Gt => "GT", - ExpireMode::Lt => "LT", - } - .to_string() -} - -fn expire_result(code: i64) -> Result { - match code { - // EXPIRE replies 0 both for a key that does not exist and for a condition not met - 0 => Ok(ExpireResult::ConditionNotMet), - 1 => Ok(ExpireResult::Applied), - code => Err(Error::Client(format!("unexpected reply: {code}"))), - } -} diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs deleted file mode 100644 index ebac0ac..0000000 --- a/crates/commands/src/lib.rs +++ /dev/null @@ -1,209 +0,0 @@ -//! Valkey's commands, grouped by ACL category. -//! -//! These sources are shared by the command components. Each component compiles them with the -//! features for the interfaces it exports, and exports them on the connection it imports: -//! -//! - `admin`: exports `admin-commands` -//! - `connection`: exports `connection-commands` -//! - `hash`: exports `hash-commands` -//! - `keyspace`: exports `keyspace-commands` -//! - `pubsub`: exports `pubsub-commands` -//! - `string`: exports `string-commands` -//! -//! A component compiles one feature, e.g. `hash-commands`, or every feature, `commands`. - -// the test harness provides a main -#![cfg_attr(not(test), no_main)] - -// each component uses the conversions its commands need -#[allow(dead_code)] -mod reply; - -#[cfg(feature = "admin")] -mod admin; -#[cfg(feature = "connection")] -mod connection; -#[cfg(feature = "hash")] -mod hash; -#[cfg(feature = "keyspace")] -mod keyspace; -#[cfg(feature = "pubsub")] -mod pubsub; -#[cfg(feature = "string")] -mod string; - -struct Commands; - -/// Generate the bindings for the world of the component compiling these sources. -macro_rules! generate { - ($world:literal) => { - wit_bindgen::generate!({ - // `path` is relative to the manifest of the component compiling these sources, - // `components//` - path: "../wit", - world: $world, - // the resp types shared by every component - with: { - "componentized:valkey/resp@0.3.0-dev": resp_codec::bindings::componentized::valkey::resp, - }, - generate_all, - }); - }; -} - -#[cfg(all( - feature = "admin", - feature = "connection", - feature = "hash", - feature = "keyspace", - feature = "pubsub", - feature = "string" -))] -generate!("commands"); - -#[cfg(all( - feature = "admin", - not(any( - feature = "connection", - feature = "hash", - feature = "keyspace", - feature = "pubsub", - feature = "string" - )) -))] -generate!("admin-commands"); - -#[cfg(all( - feature = "connection", - not(any( - feature = "admin", - feature = "hash", - feature = "keyspace", - feature = "pubsub", - feature = "string" - )) -))] -generate!("connection-commands"); - -#[cfg(all( - feature = "hash", - not(any( - feature = "admin", - feature = "connection", - feature = "keyspace", - feature = "pubsub", - feature = "string" - )) -))] -generate!("hash-commands"); - -#[cfg(all( - feature = "keyspace", - not(any( - feature = "admin", - feature = "connection", - feature = "hash", - feature = "pubsub", - feature = "string" - )) -))] -generate!("keyspace-commands"); - -#[cfg(all( - feature = "pubsub", - not(any( - feature = "admin", - feature = "connection", - feature = "hash", - feature = "keyspace", - feature = "string" - )) -))] -generate!("pubsub-commands"); - -#[cfg(all( - feature = "string", - not(any( - feature = "admin", - feature = "connection", - feature = "hash", - feature = "keyspace", - feature = "pubsub" - )) -))] -generate!("string-commands"); - -// a feature count other than one or all six has no world -#[cfg(not(any( - all( - feature = "admin", - feature = "connection", - feature = "hash", - feature = "keyspace", - feature = "pubsub", - feature = "string" - ), - all( - feature = "admin", - not(any( - feature = "connection", - feature = "hash", - feature = "keyspace", - feature = "pubsub", - feature = "string" - )) - ), - all( - feature = "connection", - not(any( - feature = "admin", - feature = "hash", - feature = "keyspace", - feature = "pubsub", - feature = "string" - )) - ), - all( - feature = "hash", - not(any( - feature = "admin", - feature = "connection", - feature = "keyspace", - feature = "pubsub", - feature = "string" - )) - ), - all( - feature = "keyspace", - not(any( - feature = "admin", - feature = "connection", - feature = "hash", - feature = "pubsub", - feature = "string" - )) - ), - all( - feature = "pubsub", - not(any( - feature = "admin", - feature = "connection", - feature = "hash", - feature = "keyspace", - feature = "string" - )) - ), - all( - feature = "string", - not(any( - feature = "admin", - feature = "connection", - feature = "hash", - feature = "keyspace", - feature = "pubsub" - )) - ), -)))] -compile_error!("enable one command feature, or all of them"); - -export!(Commands); diff --git a/crates/commands/src/pubsub.rs b/crates/commands/src/pubsub.rs deleted file mode 100644 index 3f077c5..0000000 --- a/crates/commands/src/pubsub.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! `pubsub-commands`, Valkey's @pubsub ACL category. - -use crate::componentized::valkey::connection::{Connection, Error}; -use crate::exports::componentized::valkey::pubsub_commands::Guest; -use crate::reply::{call, counts, integer, send, strings}; -use wit_bindgen::rt::Map; - -impl Guest for crate::Commands { - #[doc = " Subscribes the client to the given patterns."] - #[doc = ""] - #[doc = " PSUBSCRIBE pattern [ pattern ... ]"] - #[doc = " https://valkey.io/commands/psubscribe/"] - #[allow(async_fn_in_trait)] - async fn psubscribe(connection: &Connection, patterns: Vec) -> Result<(), Error> { - let mut command = vec!["PSUBSCRIBE".to_string()]; - command.extend(patterns); - send(connection, command).await - } - - #[doc = " Posts a message to the given channel."] - #[doc = ""] - #[doc = " PUBLISH channel message"] - #[doc = " https://valkey.io/commands/publish/"] - #[allow(async_fn_in_trait)] - async fn publish( - connection: &Connection, - channel: String, - message: String, - ) -> Result { - let command = ["PUBLISH".to_string(), channel, message]; - let response = call(connection, command).await?; - integer(response) - } - - #[doc = " Lists the currently active channels."] - #[doc = ""] - #[doc = " PUBSUB CHANNELS [ pattern ]"] - #[doc = " https://valkey.io/commands/pubsub-channels/"] - #[allow(async_fn_in_trait)] - async fn pubsub_channels( - connection: &Connection, - pattern: Option, - ) -> Result, Error> { - let mut command = vec!["PUBSUB".to_string(), "CHANNELS".to_string()]; - command.extend(pattern); - let response = call(connection, command).await?; - strings(response) - } - - #[doc = " Returns the number of unique patterns that are subscribed to by clients."] - #[doc = ""] - #[doc = " PUBSUB NUMPAT"] - #[doc = " https://valkey.io/commands/pubsub-numpat/"] - #[allow(async_fn_in_trait)] - async fn pubsub_numpat(connection: &Connection) -> Result { - let command = ["PUBSUB".to_string(), "NUMPAT".to_string()]; - let response = call(connection, command).await?; - integer(response) - } - - #[doc = " Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels."] - #[doc = ""] - #[doc = " PUBSUB NUMSUB [ channel ] [ [ channel ] ... ]"] - #[doc = " https://valkey.io/commands/pubsub-numsub/"] - #[allow(async_fn_in_trait)] - async fn pubsub_numsub( - connection: &Connection, - channels: Vec, - ) -> Result, Error> { - let mut command = vec!["PUBSUB".to_string(), "NUMSUB".to_string()]; - command.extend(channels); - let response = call(connection, command).await?; - counts(response) - } - - #[doc = " Lists the currently active shard channels."] - #[doc = ""] - #[doc = " PUBSUB SHARDCHANNELS [ pattern ]"] - #[doc = " https://valkey.io/commands/pubsub-shardchannels/"] - #[allow(async_fn_in_trait)] - async fn pubsub_shardchannels( - connection: &Connection, - pattern: Option, - ) -> Result, Error> { - let mut command = vec!["PUBSUB".to_string(), "SHARDCHANNELS".to_string()]; - command.extend(pattern); - let response = call(connection, command).await?; - strings(response) - } - - #[doc = " Returns the number of subscribers for the specified shard channels."] - #[doc = ""] - #[doc = " PUBSUB SHARDNUMSUB [ shardchannel [ shardchannel ... ] ]"] - #[doc = " https://valkey.io/commands/pubsub-shardnumsub/"] - #[allow(async_fn_in_trait)] - async fn pubsub_shardnumsub( - connection: &Connection, - shardchannels: Vec, - ) -> Result, Error> { - let mut command = vec!["PUBSUB".to_string(), "SHARDNUMSUB".to_string()]; - command.extend(shardchannels); - let response = call(connection, command).await?; - counts(response) - } - - #[doc = " Unsubscribes the client from the given patterns, or from all of them if none is given."] - #[doc = ""] - #[doc = " PUNSUBSCRIBE [ pattern [ pattern ... ] ]"] - #[doc = " https://valkey.io/commands/punsubscribe/"] - #[allow(async_fn_in_trait)] - async fn punsubscribe(connection: &Connection, patterns: Vec) -> Result<(), Error> { - let mut command = vec!["PUNSUBSCRIBE".to_string()]; - command.extend(patterns); - send(connection, command).await - } - - #[doc = " Posts a message to the given shard channel."] - #[doc = ""] - #[doc = " SPUBLISH shardchannel message"] - #[doc = " https://valkey.io/commands/spublish/"] - #[allow(async_fn_in_trait)] - async fn spublish( - connection: &Connection, - shardchannel: String, - message: String, - ) -> Result { - let command = vec!["SSUBSCRIBE".to_string(), shardchannel, message]; - let response = call(connection, command).await?; - integer(response) - } - - #[doc = " Subscribes the client to the specified shard channels."] - #[doc = ""] - #[doc = " SSUBSCRIBE shardchannel [ shardchannel ... ]"] - #[doc = " https://valkey.io/commands/ssubscribe/"] - #[allow(async_fn_in_trait)] - async fn ssubscribe(connection: &Connection, shardchannels: Vec) -> Result<(), Error> { - let mut command = vec!["SSUBSCRIBE".to_string()]; - command.extend(shardchannels); - send(connection, command).await - } - - #[doc = " Subscribes the client to the specified channels."] - #[doc = ""] - #[doc = " SUBSCRIBE channel [ channel ... ]"] - #[doc = " https://valkey.io/commands/subscribe/"] - #[allow(async_fn_in_trait)] - async fn subscribe(connection: &Connection, channels: Vec) -> Result<(), Error> { - let mut command = vec!["SUBSCRIBE".to_string()]; - command.extend(channels); - send(connection, command).await - } - - #[doc = " Unsubscribes the client from the given shard channels, or from all of them if none is given."] - #[doc = ""] - #[doc = " SUNSUBSCRIBE [ shardchannel ] [ [ shardchannel ] ... ]"] - #[doc = " https://valkey.io/commands/sunsubscribe/"] - #[allow(async_fn_in_trait)] - async fn sunsubscribe( - connection: &Connection, - shardchannels: Vec, - ) -> Result<(), Error> { - let mut command = vec!["SUNSUBSCRIBE".to_string()]; - command.extend(shardchannels); - send(connection, command).await - } - - #[doc = " Unsubscribes the client from the given channels, or from all of them if none is given."] - #[doc = ""] - #[doc = " UNSUBSCRIBE [ channel ] [ [ channel ] ... ]"] - #[doc = " https://valkey.io/commands/unsubscribe/"] - #[allow(async_fn_in_trait)] - async fn unsubscribe(connection: &Connection, channels: Vec) -> Result<(), Error> { - let mut command = vec!["UNSUBSCRIBE".to_string()]; - command.extend(channels); - send(connection, command).await - } -} diff --git a/crates/commands/src/reply.rs b/crates/commands/src/reply.rs deleted file mode 100644 index e3d5980..0000000 --- a/crates/commands/src/reply.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! Sending commands, and converting their replies. -//! -//! Connections speak RESP3. Where RESP3 replies with a richer type than RESP2, e.g. a map rather -//! than a flat array of keys and values, both are accepted. - -use resp_codec::{NestedValue, Value}; -use wit_bindgen::rt::Map; - -use crate::componentized::valkey::connection::{Connection, Error}; - -/// Send a command, its words as bulk strings, and wait for the reply. -/// -/// An error reply fails the command, see `connection.call`. -pub async fn call(connection: &Connection, words: I) -> Result -where - I: IntoIterator, - W: Into, -{ - let command = words - .into_iter() - .map(|word| Value::BulkString(word.into())) - .collect(); - connection.call(command).await -} - -/// Send a command, its words as bulk strings, without waiting for a reply. -/// -/// An error reply fails the command, see `connection.send`. -pub async fn send(connection: &Connection, words: I) -> Result<(), Error> -where - I: IntoIterator, - W: Into, -{ - let command = words - .into_iter() - .map(|word| Value::BulkString(word.into())) - .collect(); - connection.send(command).await -} - -pub fn unexpected(reply: &Value) -> Error { - Error::Client(format!("unexpected reply: {reply:?}")) -} - -pub fn decode(value: NestedValue) -> Result { - resp_codec::decode(value).map_err(Error::Resp) -} - -/// A simple string `OK`. -pub fn ok(reply: Value) -> Result<(), Error> { - match &reply { - Value::String(value) if value == "OK" => Ok(()), - _ => Err(unexpected(&reply)), - } -} - -pub fn integer(reply: Value) -> Result { - match reply { - Value::Integer(value) => Ok(value), - reply => Err(unexpected(&reply)), - } -} - -pub fn unsigned(reply: Value) -> Result { - let value = integer(reply)?; - u64::try_from(value).map_err(|_| Error::Client(format!("unexpected negative reply: {value}"))) -} - -/// An integer `0` or `1`. -pub fn boolean(reply: Value) -> Result { - match reply { - Value::Integer(0) => Ok(false), - Value::Integer(1) => Ok(true), - Value::Boolean(value) => Ok(value), - reply => Err(unexpected(&reply)), - } -} - -/// A bulk or simple string. -pub fn string(reply: Value) -> Result { - match reply { - Value::BulkString(value) | Value::String(value) => Ok(value), - Value::VerbatimString((_, value)) => Ok(value), - reply => Err(unexpected(&reply)), - } -} - -/// A string, or null. -pub fn optional_string(reply: Value) -> Result, Error> { - match reply { - Value::Null => Ok(None), - reply => string(reply).map(Some), - } -} - -/// The nested values of an array, or a set. -pub fn items(reply: Value) -> Result, Error> { - match reply { - Value::Array(items) | Value::Set(items) => items.into_iter().map(decode).collect(), - reply => Err(unexpected(&reply)), - } -} - -pub fn strings(reply: Value) -> Result, Error> { - items(reply)?.into_iter().map(string).collect() -} - -pub fn optional_strings(reply: Value) -> Result>, Error> { - items(reply)?.into_iter().map(optional_string).collect() -} - -/// The entries of a map, or of a flat array of keys and values. -pub fn entries(reply: Value) -> Result, Error> { - match reply { - Value::Map(entries) => entries - .into_iter() - .map(|(key, value)| Ok((decode(key)?, decode(value)?))) - .collect(), - Value::Array(items) => { - if items.len() % 2 != 0 { - return Err(Error::Client(format!( - "unexpected odd number of keys and values: {}", - items.len() - ))); - } - let mut items = items.into_iter(); - let mut entries = vec![]; - while let (Some(key), Some(value)) = (items.next(), items.next()) { - entries.push((decode(key)?, decode(value)?)); - } - Ok(entries) - } - reply => Err(unexpected(&reply)), - } -} - -/// The entries of a map of strings to strings. -pub fn string_entries(reply: Value) -> Result, Error> { - entries(reply)? - .into_iter() - .map(|(key, value)| Ok((string(key)?, string(value)?))) - .collect() -} - -pub fn counts(reply: Value) -> Result, Error> { - let mut map = Map::new(); - for (key, value) in entries(reply)? { - map.insert(string(key)?, integer(value)?); - } - Ok(map) -} - -/// A duration, in nanoseconds, as whole seconds, the precision of EXPIRE and HEXPIRE. -pub fn whole_seconds(nanoseconds: u64) -> String { - (nanoseconds / 1_000_000_000).to_string() -} diff --git a/crates/commands/src/string.rs b/crates/commands/src/string.rs deleted file mode 100644 index 5c891fd..0000000 --- a/crates/commands/src/string.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! `string-commands`, Valkey's @string ACL category. - -use crate::componentized::valkey::connection::{Connection, Error}; -use crate::exports::componentized::valkey::string_commands::Guest; -use crate::reply::{call, integer, ok, optional_string}; - -impl Guest for crate::Commands { - async fn get(connection: &Connection, key: String) -> Result, Error> { - // https://valkey.io/commands/get/ - // GET key - optional_string(call(connection, ["GET".to_string(), key]).await?) - } - - async fn incr(connection: &Connection, key: String) -> Result { - // https://valkey.io/commands/incr/ - // INCR key - integer(call(connection, ["INCR".to_string(), key]).await?) - } - - async fn incrby(connection: &Connection, key: String, increment: i64) -> Result { - // https://valkey.io/commands/incrby/ - // INCRBY key increment - integer( - call( - connection, - ["INCRBY".to_string(), key, increment.to_string()], - ) - .await?, - ) - } - - async fn set(connection: &Connection, key: String, value: String) -> Result<(), Error> { - // https://valkey.io/commands/set/ - // SET key value - ok(call(connection, ["SET".to_string(), key, value]).await?) - } -} diff --git a/crates/test-harness/src/lib.rs b/crates/test-harness/src/lib.rs index fe4184e..fab16d6 100644 --- a/crates/test-harness/src/lib.rs +++ b/crates/test-harness/src/lib.rs @@ -56,9 +56,6 @@ pub mod bindings { world: "componentized:test-harness/harness", exports: { default: async | store }, additional_derives: [PartialEq], - with: { - "wasi:clocks": wasmtime_wasi::p3::bindings::clocks, - }, }); } @@ -134,15 +131,11 @@ fn from_codec(value: resp_codec::Value) -> Value { pub type Connection<'a> = bindings::exports::componentized::valkey::connection::GuestConnection<'a>; pub type Tcp = bindings::exports::componentized::valkey::tcp::Guest; -pub use bindings::exports::wasmcloud::keyvalue; pub use bindings::exports::componentized::valkey::{ - admin_commands, - connection_commands, - hash_commands, - keyspace_commands, - pubsub_commands, + admin_commands, connection_commands, hash_commands, keyspace_commands, pubsub_commands, string_commands, }; +pub use bindings::exports::wasmcloud::keyvalue; /// A bulk string, the type commands are sent as. pub fn bulk(value: &str) -> Value { @@ -406,10 +399,7 @@ pub struct Ctx { } impl bindings::wasi::config::store::Host for Ctx { - fn get( - &mut self, - key: String, - ) -> Result, bindings::wasi::config::store::Error> { + fn get(&mut self, key: String) -> Result, bindings::wasi::config::store::Error> { Ok(self .config .iter() @@ -557,7 +547,7 @@ impl Harness { ensure_built(name)?; let mut config = Config::new(); - config.wasm_component_model_async(true); + config.wasm_component_model_map(true); let engine = Engine::new(&config)?; let component = Component::from_file(&engine, component_path(name)) .with_context(|| format!("failed to load {name}"))?; diff --git a/wit/admin-commands.wit b/wit/admin-commands.wit deleted file mode 100644 index 6e6a57c..0000000 --- a/wit/admin-commands.wit +++ /dev/null @@ -1,86 +0,0 @@ -/// Administrative commands, Valkey's @admin ACL category. -/// -/// Also holds the rest of the ACL family, e.g. ACL GENPASS, which is in no data type or -/// functional category. -interface admin-commands { - use connection.{connection, error}; - - acl-deluser: async func(connection: borrow, username: string) -> result<_, error>; - acl-genpass: async func(connection: borrow) -> result; - acl-setuser: async func(connection: borrow, username: string, rules: list) -> result<_, error>; - - // not yet implemented, from the @admin category: - // ACL CAT - // ACL DRYRUN - // ACL GETUSER - // ACL LIST - // ACL LOAD - // ACL LOG - // ACL SAVE - // ACL USERS - // ACL WHOAMI - // BGREWRITEAOF - // BGSAVE - // CLIENT KILL - // CLIENT LIST - // CLIENT NO-EVICT - // CLIENT PAUSE - // CLIENT UNBLOCK - // CLIENT UNPAUSE - // CLUSTER ADDSLOTS - // CLUSTER ADDSLOTSRANGE - // CLUSTER BUMPEPOCH - // CLUSTER CANCELSLOTMIGRATIONS - // CLUSTER COUNT-FAILURE-REPORTS - // CLUSTER DELSLOTS - // CLUSTER DELSLOTSRANGE - // CLUSTER FAILOVER - // CLUSTER FLUSHSLOT - // CLUSTER FLUSHSLOTS - // CLUSTER FORGET - // CLUSTER GETSLOTMIGRATIONS - // CLUSTER MEET - // CLUSTER MIGRATESLOTS - // CLUSTER REPLICAS - // CLUSTER REPLICATE - // CLUSTER RESET - // CLUSTER SAVECONFIG - // CLUSTER SET-CONFIG-EPOCH - // CLUSTER SETSLOT - // CLUSTER SLAVES - // CLUSTER SYNCSLOTS - // COMMANDLOG GET - // COMMANDLOG LEN - // COMMANDLOG RESET - // CONFIG GET - // CONFIG RESETSTAT - // CONFIG REWRITE - // CONFIG SET - // DEBUG - // FAILOVER - // LASTSAVE - // LATENCY DOCTOR - // LATENCY GRAPH - // LATENCY HISTOGRAM - // LATENCY HISTORY - // LATENCY LATEST - // LATENCY RESET - // MODULE LIST - // MODULE LOAD - // MODULE LOADEX - // MODULE UNLOAD - // MONITOR - // PFDEBUG - // PFSELFTEST - // PSYNC - // REPLCONF - // REPLICAOF - // ROLE - // SAVE - // SHUTDOWN - // SLAVEOF - // SLOWLOG GET - // SLOWLOG LEN - // SLOWLOG RESET - // SYNC -} diff --git a/wit/bitmap-commands.wit b/wit/bitmap-commands.wit deleted file mode 100644 index 5ddcc62..0000000 --- a/wit/bitmap-commands.wit +++ /dev/null @@ -1,13 +0,0 @@ -/// Commands for bitmaps, Valkey's @bitmap ACL category. -interface bitmap-commands { - use connection.{connection, error}; - - // not yet implemented, from the @bitmap category: - // BITCOUNT - // BITFIELD - // BITFIELD_RO - // BITOP - // BITPOS - // GETBIT - // SETBIT -} diff --git a/wit/commands.wit b/wit/commands.wit new file mode 100644 index 0000000..57a9fa4 --- /dev/null +++ b/wit/commands.wit @@ -0,0 +1,1360 @@ +interface commands { + use connection.{connection, double, duration, instant, integer, value, error}; + + /// XXXXXX + /// + /// XXXXXX + /// https://valkey.io/commands/XXXXXX/ + + + /// The condition for setting an expiration, e.g. with EXPIRE or HEXPIRE. + enum ttl-mode { + /// Set expiration only when the field has no expiration. + nx, + /// Set expiration only when the field has an existing expiration. + xx, + /// Set expiration only when the new expiration is greater than current one. + gt, + /// Set expiration only when the new expiration is less than current one. + lt, + } + + /// The outcome of setting the expiration of a hash field, e.g. with HEXPIRE. + enum ttl-result { + /// -2: key does not exist, or field does not exist in the HASH. + does-not-exist, + /// 0: the specified NX | XX | GT | LT condition has not been met. + condition-not-met, + /// 1: the expiration time was applied. + applied, + /// 2: when called with 0 seconds. + zero-seconds, + } + + // admin commands + + /// Delete all the specified ACL users and terminate all the connections that are authenticated with such users. + /// + /// ACL DELUSER username [ username ... ] + /// https://valkey.io/commands/acl-deluser/ + acl-deluser: async func(connection: borrow, usernames: list) -> result; + + /// Generates a random password. + /// + /// ACL GENPASS [ bits ] + /// https://valkey.io/commands/acl-genpass/ + acl-genpass: async func(connection: borrow, bits: option) -> result; + + /// A rule of an ACL user, see `acl-setuser`. + /// + /// https://valkey.io/topics/acl/ + variant rule { + /// The keys the user may access. + keys(key-rule), + /// The Pub/Sub channels the user may access. + channel(channel-rule), + /// The commands the user may call. + command(command-rule), + /// The databases the user may access. + db(db-rule), + /// The user itself, e.g. whether it is enabled and its passwords. + user(user-rule), + } + + /// A rule of a selector, a set of permissions matched independently of the user's root permissions, + /// the user may run a command when either permits it. + variant rule-selector { + /// The keys the selector permits access to. + keys(key-rule), + /// The Pub/Sub channels the selector permits access to. + channel(channel-rule), + /// The commands the selector permits. + command(command-rule), + /// The databases the selector permits access to. + db(db-rule), + } + + /// A rule for the keys a user may access. + variant key-rule { + /// `~`: allows reading and writing the keys matching the glob-style pattern. + add(string), + /// `%R~`: allows reading the keys matching the glob-style pattern. + add-read(string), + /// `%W~`: allows writing the keys matching the glob-style pattern. + add-write(string), + /// `%RW~`: allows reading and writing the keys matching the glob-style pattern, the + /// same as `add`. + add-read-write(string), + /// `allkeys`: allows access to all keys, an alias for `~*`. + all, + /// `resetkeys`: removes all the allowed key patterns. + reset, + } + + /// A rule for the Pub/Sub channels a user may access. + variant channel-rule { + /// `&`: allows access to the Pub/Sub channels matching the glob-style pattern. + add(string), + /// `allchannels`: allows access to all Pub/Sub channels, an alias for `&*`. + all, + /// `resetchannels`: removes all the allowed channel patterns, disconnecting the user's Pub/Sub + /// clients that can no longer access their channels. + reset, + } + + /// A rule for the commands a user may call. + variant command-rule { + /// `+`: allows the command, or a subcommand, e.g. `config|get`. + add(string), + /// `+@`: allows all the commands in the category, e.g. `read`, see ACL CAT for the + /// categories. + add-category(string), + /// `+|`: allows the command only with the given first argument, e.g. `select` + /// with `0`. + add-first-arg(tuple), + /// `allcommands`: allows all commands, including those of modules loaded later, an alias for `+@all`. + all, + /// `-`: disallows the command, or a subcommand. + remove(string), + /// `-@`: disallows all the commands in the category. + remove-category(string), + /// `nocommands`: disallows all commands, an alias for `-@all`. + reset, + } + + /// A rule for the databases a user may access. + variant db-rule { + /// `db=[,...]`: allows access to the databases with the given IDs. + db(list), + /// `alldbs`: allows access to all databases. + all, + /// `resetdbs`: removes all the allowed databases. + reset, + } + + /// A rule for the user itself. + variant user-rule { + /// `on`: enables the user, it is possible to authenticate as this user. + on, + /// `off`: disables the user, it is no longer possible to authenticate as this user, connections + /// already authenticated keep working. + off, + /// `nopass`: removes all the user's passwords, any password then authenticates as the user. + nopass, + /// `>`: adds the password to the user's passwords. + password(string), + /// `#`: adds a password, by its SHA-256 hash as 64 lowercase hex characters, to the user's + /// passwords. + hashed-password(string), + /// `<`: removes the password from the user's passwords. + remove-password(string), + /// `!`: removes a password, by its SHA-256 hash, from the user's passwords. + remove-hashed-password(string), + /// `( ...)`: adds a selector, with the given rules. + selector(list), + /// `clearselectors`: removes all the user's selectors. + clearselectors, + /// `reset`: resets the user to its state when created, disabled, without passwords, keys, + /// channels, commands or selectors. + reset, + } + + /// Create an ACL user with the specified rules or modify the rules of an existing user. + /// + /// ACL SETUSER username [ rule ] [ [ rule ] ... ] + /// https://valkey.io/commands/acl-setuser/ + acl-setuser: async func(connection: borrow, username: string, rules: list) -> result<_, error>; + + + // not yet implemented, from the @admin category: + // ACL CAT + // ACL DRYRUN + // ACL GETUSER + // ACL LIST + // ACL LOAD + // ACL LOG + // ACL SAVE + // ACL USERS + // ACL WHOAMI + // BGREWRITEAOF + // BGSAVE + // CLIENT KILL + // CLIENT LIST + // CLIENT NO-EVICT + // CLIENT PAUSE + // CLIENT UNBLOCK + // CLIENT UNPAUSE + // CLUSTER ADDSLOTS + // CLUSTER ADDSLOTSRANGE + // CLUSTER BUMPEPOCH + // CLUSTER CANCELSLOTMIGRATIONS + // CLUSTER COUNT-FAILURE-REPORTS + // CLUSTER DELSLOTS + // CLUSTER DELSLOTSRANGE + // CLUSTER FAILOVER + // CLUSTER FLUSHSLOT + // CLUSTER FLUSHSLOTS + // CLUSTER FORGET + // CLUSTER GETSLOTMIGRATIONS + // CLUSTER MEET + // CLUSTER MIGRATESLOTS + // CLUSTER REPLICAS + // CLUSTER REPLICATE + // CLUSTER RESET + // CLUSTER SAVECONFIG + // CLUSTER SET-CONFIG-EPOCH + // CLUSTER SETSLOT + // CLUSTER SLAVES + // CLUSTER SYNCSLOTS + // COMMANDLOG GET + // COMMANDLOG LEN + // COMMANDLOG RESET + // CONFIG GET + // CONFIG RESETSTAT + // CONFIG REWRITE + // CONFIG SET + // DEBUG + // FAILOVER + // LASTSAVE + // LATENCY DOCTOR + // LATENCY GRAPH + // LATENCY HISTOGRAM + // LATENCY HISTORY + // LATENCY LATEST + // LATENCY RESET + // MODULE LIST + // MODULE LOAD + // MODULE LOADEX + // MODULE UNLOAD + // MONITOR + // PFDEBUG + // PFSELFTEST + // PSYNC + // REPLCONF + // REPLICAOF + // ROLE + // SAVE + // SHUTDOWN + // SLAVEOF + // SLOWLOG GET + // SLOWLOG LEN + // SLOWLOG RESET + // SYNC + + + // bitmap commands + + // not yet implemented, from the @bitmap category: + // BITCOUNT + // BITFIELD + // BITFIELD_RO + // BITOP + // BITPOS + // GETBIT + // SETBIT + + // connection commands + + /// Authenticates the current connection. + /// + /// AUTH [ username ] password + /// https://valkey.io/commands/auth/ + auth: async func(connection: borrow, username: option, password: string) -> result<_, error>; + + /// Switch to a different protocol, optionally authenticating and setting the connection's name, or provide a contextual client report. + /// + /// RESP2 is not fully supported by this client. + /// + /// HELLO 3 [ AUTH username password ] [ SETNAME clientname ] + /// https://valkey.io/commands/hello/ + hello: async func(connection: borrow, auth: option>, client-name: option) -> result, error>; + + /// Returns PONG if no argument is provided, otherwise return a copy of the argument as a bulk. + /// + /// PING [ message ] + /// https://valkey.io/commands/ping/ + ping: async func(connection: borrow, message: option) -> result<_, error>; + + // not yet implemented, from the @connection category: + // ASKING + // CLIENT CACHING + // CLIENT CAPA + // CLIENT GETNAME + // CLIENT GETREDIR + // CLIENT ID + // CLIENT IMPORT-SOURCE + // CLIENT INFO + // CLIENT KILL + // CLIENT LIST + // CLIENT NO-EVICT + // CLIENT NO-TOUCH + // CLIENT PAUSE + // CLIENT REPLY + // CLIENT SETINFO + // CLIENT SETNAME + // CLIENT TRACKING + // CLIENT TRACKINGINFO + // CLIENT UNBLOCK + // CLIENT UNPAUSE + // COMMAND + // COMMAND COUNT + // COMMAND DOCS + // COMMAND GETKEYS + // COMMAND GETKEYSANDFLAGS + // COMMAND INFO + // COMMAND LIST + // ECHO + // QUIT + // READONLY + // READWRITE + // RESET + // SELECT + // WAIT + // WAITAOF + + + // geo commands + + // not yet implemented, from the @geo category: + // GEOADD + // GEODIST + // GEOHASH + // GEOPOS + // GEORADIUS + // GEORADIUS_RO + // GEORADIUSBYMEMBER + // GEORADIUSBYMEMBER_RO + // GEOSEARCH + // GEOSEARCHSTORE + + + // hash commands + + /// Removes the specified fields from the hash stored at key. + /// + /// HDEL key field [ field ... ] + /// https://valkey.io/commands/hdel/ + hdel: async func(connection: borrow, key: string, fields: list) -> result; + + /// Returns if field is an existing field in the hash stored at key. + /// + /// HEXISTS key field + /// https://valkey.io/commands/hexists/ + hexists: async func(connection: borrow, key: string, field: string) -> result; + + /// Manipulate the expiration time for existing hash fields. + /// + /// HEXPIRE key seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] + /// https://valkey.io/commands/hexpire/ + hexpire: async func(connection: borrow, key: string, seconds: duration, mode: option, fields: list) -> result, error>; + + /// Manipulate the expiration time for existing hash fields. + /// + /// HEXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] + /// https://valkey.io/commands/hexpireat/ + hexpireat: async func(connection: borrow, key: string, seconds: instant, mode: option, fields: list) -> result, error>; + + /// Returns the value associated with field in the hash stored at key. + /// + /// HGET key field + /// https://valkey.io/commands/hget/ + hget: async func(connection: borrow, key: string, field: string) -> result, error>; + + /// Returns all fields and values of the hash stored at key. + /// + /// HGETALL key + /// https://valkey.io/commands/hgetall/ + hgetall: async func(connection: borrow, key: string) -> result, error>; + + /// Increments the number stored at field in the hash stored at key by increment. + /// + /// HINCRBY key field increment + /// https://valkey.io/commands/hincrby/ + hincrby: async func(connection: borrow, key: string, field: string, increment: integer) -> result; + + /// Increment the specified field of a hash stored at key, and representing a floating point number, by the specified increment. + /// + /// HINCRBYFLOAT key field increment + /// https://valkey.io/commands/hincrbyfloat/ + hincrbyfloat: async func(connection: borrow, key: string, field: string, increment: double) -> result; + + /// Returns all field names in the hash stored at key. + /// + /// HKEYS key + /// https://valkey.io/commands/hkeys/ + hkeys: async func(connection: borrow, key: string) -> result, error>; + + /// Returns the number of fields contained in the hash stored at key. + /// + /// HLEN key + /// https://valkey.io/commands/hlen/ + hlen: async func(connection: borrow, key: string) -> result; + + /// Returns the values associated with the specified fields in the hash stored at key. + /// + /// HMGET key field [ field ... ] + /// https://valkey.io/commands/hmget/ + hmget: async func(connection: borrow, key: string, fields: list) -> result>, error>; + + /// Sets the specified fields to their respective values in the hash stored at key. + /// + /// HMSET key field value [ field value ... ] + /// https://valkey.io/commands/hmset/ + hmset: async func(connection: borrow, key: string, field-and-values: list>) -> result<_, error>; + + /// A random field from the hash value stored at key. + /// + /// Also see `hrandfield-count` and `hrandfield-count-withvalues`. + /// + /// HRANDFIELD key + /// https://valkey.io/commands/hrandfield/ + hrandfield: async func(connection: borrow, key: string) -> result, error>; + + /// A random set of fields from the hash value stored at key up to the count. + /// + /// Also see `hrandfield` and `hrandfield-count-withvalues`. + /// + /// HRANDFIELD key count + /// https://valkey.io/commands/hrandfield/ + hrandfield-count: async func(connection: borrow, key: string, count: integer) -> result, error>; + + /// A random map of fields and values from the hash value stored at key up to the count. + /// + /// Also see `hrandfield` and `hrandfield-count`. + /// + /// HRANDFIELD key count WITHVALUES + /// https://valkey.io/commands/hrandfield/ + hrandfield-count-withvalues: async func(connection: borrow, key: string, count: integer) -> result, error>; + + /// The position of an incremental iteration, e.g. with HSCAN. + /// + /// None starts a new iteration, the iteration is complete once the cursor returned is `0`. + type cursor = string; + + /// Incrementally iterate over a collection of elements. + /// + /// See also `hscan-novalues`. + /// + /// HSCAN key cursor [ MATCH pattern ] [ COUNT count ] + /// https://valkey.io/commands/hscan/ + hscan: async func(connection: borrow, key: string, cursor: option, match: option, count: option) -> result>, error>; + + /// Incrementally iterate over a collection of keys. + /// + /// See also `hscan`. + /// + /// HSCAN key cursor [ MATCH pattern ] [ COUNT count ] NOVALUES + /// https://valkey.io/commands/hscan/ + hscan-novalues: async func(connection: borrow, key: string, cursor: option, match: option, count: option) -> result>, error>; + + /// Sets the specified fields to their respective values in the hash stored at key. + /// + /// HSET key field value [ field value ... ] + /// https://valkey.io/commands/hset/ + hset: async func(connection: borrow, key: string, field-and-values: list>) -> result; + + /// Sets field in the hash stored at key to value, only if field does not yet exist. + /// + /// HSETNX key field value + /// https://valkey.io/commands/hsetnx/ + hsetnx: async func(connection: borrow, key: string, field: string, value: string) -> result; + + /// Returns the string length of the value associated with field in the hash stored at key. + /// + /// HSTRLEN key field + /// https://valkey.io/commands/hstrlen/ + hstrlen: async func(connection: borrow, key: string, field: string) -> result; + + /// All values in the hash stored at key. + /// + /// HVALS key + /// https://valkey.io/commands/hvals/ + hvals: async func(connection: borrow, key: string) -> result, error>; + + // not yet implemented, from the @hash category: + // HEXPIRETIME + // HGETDEL + // HGETEX + // HPERSIST + // HPEXPIRE + // HPEXPIREAT + // HPEXPIRETIME + // HPTTL + // HSETEX + // HTTL + + + // hyperlog commands + + // not yet implemented, from the @hyperloglog category: + // PFADD + // PFCOUNT + // PFDEBUG + // PFMERGE + // PFSELFTEST + + + // keyspace commands + + /// Removes the specified keys. + /// + /// DEL key [ key ... ] + /// https://valkey.io/commands/del/ + del: async func(connection: borrow, keys: list) -> result; + + /// If key exists. + /// + /// EXISTS key [ key ... ] + /// https://valkey.io/commands/exists/ + exists: async func(connection: borrow, keys: list) -> result; + + /// Set a timeout on key. + /// + /// EXPIRE key seconds [ NX | XX | GT | LT ] + /// https://valkey.io/commands/expire/ + expire: async func(connection: borrow, key: string, seconds: duration, mode: option) -> result; + + /// Set a timeout on key. + /// + /// EXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] + /// https://valkey.io/commands/expireat/ + expireat: async func(connection: borrow, key: string, seconds: instant, mode: option) -> result; + + /// Returns all keys matching pattern. + /// + /// KEYS pattern + /// https://valkey.io/commands/keys/ + keys: async func(connection: borrow, pattern: string) -> result, error>; + + // not yet implemented, from the @keyspace category: + // CLUSTERSCAN + // CLUSTER FLUSHSLOT + // COPY + // DBSIZE + // DUMP + // EXPIRETIME + // FLUSHALL + // FLUSHDB + // MIGRATE + // MOVE + // OBJECT ENCODING + // OBJECT FREQ + // OBJECT IDLETIME + // OBJECT REFCOUNT + // PERSIST + // PEXPIRE + // PEXPIREAT + // PEXPIRETIME + // PTTL + // RANDOMKEY + // RENAME + // RENAMENX + // RESTORE + // RESTORE-ASKING + // SCAN + // SWAPDB + // TOUCH + // TTL + // TYPE + // UNLINK + + + // list commands + + // not yet implemented, from the @list category: + // BLMOVE + // BLMPOP + // BLPOP + // BRPOP + // BRPOPLPUSH + // LINDEX + // LINSERT + // LLEN + // LMOVE + // LMPOP + // LPOP + // LPOS + // LPUSH + // LPUSHX + // LRANGE + // LREM + // LSET + // LTRIM + // RPOP + // RPOPLPUSH + // RPUSH + // RPUSHX + // SORT + // SORT_RO + + + // pubsub commands + + /// Subscribes the client to the given patterns. + /// + /// PSUBSCRIBE pattern [ pattern ... ] + /// https://valkey.io/commands/psubscribe/ + psubscribe: async func(connection: borrow, patterns: list) -> result<_, error>; + + /// Posts a message to the given channel. + /// + /// PUBLISH channel message + /// https://valkey.io/commands/publish/ + publish: async func(connection: borrow, channel: string, message: string) -> result; + + /// Lists the currently active channels. + /// + /// PUBSUB CHANNELS [ pattern ] + /// https://valkey.io/commands/pubsub-channels/ + pubsub-channels: async func(connection: borrow, pattern: option) -> result, error>; + + /// Returns the number of unique patterns that are subscribed to by clients. + /// + /// PUBSUB NUMPAT + /// https://valkey.io/commands/pubsub-numpat/ + pubsub-numpat: async func(connection: borrow) -> result; + + /// Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels. + /// + /// PUBSUB NUMSUB [ channel ] [ [ channel ] ... ] + /// https://valkey.io/commands/pubsub-numsub/ + pubsub-numsub: async func(connection: borrow, channels: list) -> result, error>; + + /// Lists the currently active shard channels. + /// + /// PUBSUB SHARDCHANNELS [ pattern ] + /// https://valkey.io/commands/pubsub-shardchannels/ + pubsub-shardchannels: async func(connection: borrow, pattern: option) -> result, error>; + + /// Returns the number of subscribers for the specified shard channels. + /// + /// PUBSUB SHARDNUMSUB [ shardchannel [ shardchannel ... ] ] + /// https://valkey.io/commands/pubsub-shardnumsub/ + pubsub-shardnumsub: async func(connection: borrow, shardchannels: list) -> result, error>; + + /// Unsubscribes the client from the given patterns, or from all of them if none is given. + /// + /// PUNSUBSCRIBE [ pattern [ pattern ... ] ] + /// https://valkey.io/commands/punsubscribe/ + punsubscribe: async func(connection: borrow, patterns: list) -> result<_, error>; + + /// Posts a message to the given shard channel. + /// + /// SPUBLISH shardchannel message + /// https://valkey.io/commands/spublish/ + spublish: async func(connection: borrow, shardchannel: string, message: string) -> result; + + /// Subscribes the client to the specified shard channels. + /// + /// SSUBSCRIBE shardchannel [ shardchannel ... ] + /// https://valkey.io/commands/ssubscribe/ + ssubscribe: async func(connection: borrow, shardchannels: list) -> result<_, error>; + + /// Subscribes the client to the specified channels. + /// + /// SUBSCRIBE channel [ channel ... ] + /// https://valkey.io/commands/subscribe/ + subscribe: async func(connection: borrow, channels: list) -> result<_, error>; + + /// Unsubscribes the client from the given shard channels, or from all of them if none is given. + /// + /// SUNSUBSCRIBE [ shardchannel ] [ [ shardchannel ] ... ] + /// https://valkey.io/commands/sunsubscribe/ + sunsubscribe: async func(connection: borrow, shardchannels: list) -> result<_, error>; + + /// Unsubscribes the client from the given channels, or from all of them if none is given. + /// + /// UNSUBSCRIBE [ channel ] [ [ channel ] ... ] + /// https://valkey.io/commands/unsubscribe/ + unsubscribe: async func(connection: borrow, channels: list) -> result<_, error>; + + + // scripting commands + + // not yet implemented, from the @scripting category: + // EVAL + // EVAL_RO + // EVALSHA + // EVALSHA_RO + // FCALL + // FCALL_RO + // FUNCTION DELETE + // FUNCTION DUMP + // FUNCTION FLUSH + // FUNCTION KILL + // FUNCTION LIST + // FUNCTION LOAD + // FUNCTION RESTORE + // FUNCTION STATS + // SCRIPT DEBUG + // SCRIPT EXISTS + // SCRIPT FLUSH + // SCRIPT KILL + // SCRIPT LOAD + // SCRIPT SHOW + + + // set commands + + // not yet implemented, from the @set category: + // SADD + // SCARD + // SDIFF + // SDIFFSTORE + // SINTER + // SINTERCARD + // SINTERSTORE + // SISMEMBER + // SMEMBERS + // SMISMEMBER + // SMOVE + // SORT + // SORT_RO + // SPOP + // SRANDMEMBER + // SREM + // SSCAN + // SUNION + // SUNIONSTORE + + + // sortedset commands + + + // not yet implemented, from the @sortedset category: + // BZMPOP + // BZPOPMAX + // BZPOPMIN + // SORT + // SORT_RO + // ZADD + // ZCARD + // ZCOUNT + // ZDIFF + // ZDIFFSTORE + // ZINCRBY + // ZINTER + // ZINTERCARD + // ZINTERSTORE + // ZLEXCOUNT + // ZMPOP + // ZMSCORE + // ZPOPMAX + // ZPOPMIN + // ZRANDMEMBER + // ZRANGE + // ZRANGEBYLEX + // ZRANGEBYSCORE + // ZRANGESTORE + // ZRANK + // ZREM + // ZREMRANGEBYLEX + // ZREMRANGEBYRANK + // ZREMRANGEBYSCORE + // ZREVRANGE + // ZREVRANGEBYLEX + // ZREVRANGEBYSCORE + // ZREVRANK + // ZSCAN + // ZSCORE + // ZUNION + // ZUNIONSTORE + + + // stream commands + + /// The ID of a stream entry, `-`, e.g. `1526919030474-55`. + /// + /// Commands that add an entry also accept `*`, for the server to generate the ID. + type stream-id = string; + + /// How a stream is trimmed, e.g. with XADD or XTRIM. + variant stream-trim { + /// Evicts entries as long as the stream's length exceeds the specified threshold, where threshold is a positive integer. + maxlen(stream-trim-maxlen), + /// Evicts entries with IDs lower than threshold, where threshold is a stream ID. + minid(stream-trim-minid), + } + + /// Trims the stream to a maximum length, `MAXLEN [ = | ~ ] threshold [ LIMIT count ]`. + record stream-trim-maxlen { + /// Exact, `=`, when none. + efficiency: option, + /// The maximum number of entries kept. + threshold: integer, + /// The maximum number of entries evicted, only with nearly exact trimming, `0` for no limit. + limit: option, + } + + /// Trims the entries with lower IDs, `MINID [ = | ~ ] threshold [ LIMIT count ]`. + record stream-trim-minid { + /// Exact, `=`, when none. + efficiency: option, + /// The lowest ID kept. + threshold: stream-id, + /// The maximum number of entries evicted, only with nearly exact trimming, `0` for no limit. + limit: option, + } + + /// How exactly a stream is trimmed. Nearly exact trimming is more efficient, the stream may keep a + /// few entries more than the threshold. + enum stream-trim-efficiency { + /// `=`: exact trimming + exact, + /// `~`: nearly exact trimming + mearly-exact, + } + + /// Removes one or multiple messages from the Pending Entries List (PEL) of a stream consumer group. + /// + /// XACK key group ID [ ID ... ] + /// https://valkey.io/commands/xack/ + xack: async func(connection: borrow, key: string, group: string, ids: list) -> result; + + /// Appends the specified stream entry to the stream at the specified key. + /// + /// XADD key [ NOMKSTREAM ] [ 〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] ] 〈 * | id 〉 field value [ field value ... ] + /// https://valkey.io/commands/xadd/ + xadd: async func(connection: borrow, key: string, nomkstream: bool, trim: option, id: stream-id, field-and-values: list>) -> result, error>; + + /// Transfers ownership of pending stream entries that match the specified criteria. + /// + /// See also `xautoclaim-justid` + /// + /// A start of none scans from the beginning, `0-0`. The next start is returned, none once the scan + /// is complete, along with the IDs of claimed entries that no longer exist, which are removed + /// from the pending entries list. + /// + /// XAUTOCLAIM key group consumer min-idle-time start [ COUNT count ] + /// https://valkey.io/commands/xautoclaim/ + xautoclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration, start: option, count: option) -> result, list>>, list>, error>; + + /// Transfers ownership of pending stream entries that match the specified criteria. + /// + /// See also `xautoclaim` + /// + /// A start of none scans from the beginning, `0-0`. The next start is returned, none once the scan + /// is complete, along with the IDs of claimed entries that no longer exist, which are removed + /// from the pending entries list. + /// + /// XAUTOCLAIM key group consumer min-idle-time start [ COUNT count ] JUSTID + /// https://valkey.io/commands/xautoclaim/ + xautoclaim-justid: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration, start: option, count: option) -> result, list, list>, error>; + + + /// Options of XCLAIM. + record xclaim-opts { + /// `IDLE ms`: sets the idle time of the entries, the time since they were last delivered, instead + /// of resetting it. + idle: option, + /// `TIME unix-time-milliseconds`: sets the idle time of the entries to the time since this instant. + time: option, + /// `RETRYCOUNT count`: sets the delivery count of the entries, instead of incrementing it. + retrycount: option, + /// `FORCE`: claims the entries even when they are not pending for any consumer, as long as they + /// exist in the stream. + force: bool, + /// `LASTID lastid`: sets the ID of the last entry delivered to the group. + lastid: option, + } + + /// Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument. + /// + /// Also see `xclaim-justid` + /// + /// XCLAIM key group consumer min-idle-time ID [ ID ... ] [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] [ LASTID lastid ] + /// https://valkey.io/commands/xclaim/ + xclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration, ids: list, opts: option) -> result>>, error>; + + /// Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument. + /// + /// Also see `xclaim` + /// + /// XCLAIM key group consumer min-idle-time ID [ ID ... ] [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] JUSTID [ LASTID lastid ] + /// https://valkey.io/commands/xclaim/ + xclaim-justid: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration, ids: list, opts: option) -> result, error>; + + /// Removes the specified entries from a stream, and returns the number of entries deleted. + /// + /// XDEL key ID [ ID ... ] + /// https://valkey.io/commands/xdel/ + xdel: async func(connection: borrow, key: string, ids: list) -> result; + + /// Create a new consumer group uniquely identified by for the stream stored at . + /// + /// XGROUP CREATE key group 〈 id | $ 〉 [ MKSTREAM ] [ ENTRIESREAD entriesread ] + /// https://valkey.io/commands/xgroup-create/ + xgroup-create: async func(connection: borrow, key: string, group: string, id: stream-id, mkstream: bool, entriesread: option) -> result<_, error>; + + /// Create a consumer named in the consumer group of the stream that's stored at . + /// + /// XGROUP CREATECONSUMER key group consumer + /// https://valkey.io/commands/xgroup-createconsumer/ + xgroup-createconsumer: async func(connection: borrow, key: string, group: string, consumer: string) -> result; + + /// The XGROUP DELCONSUMER command deletes a consumer from the consumer group. + /// + /// XGROUP DELCONSUMER key group consumer + /// https://valkey.io/commands/xgroup-delconsumer/ + xgroup-delconsumer: async func(connection: borrow, key: string, group: string, consumer: string) -> result; + + /// Completely destroys the consumer group. + /// + /// XGROUP DESTROY key group + /// https://valkey.io/commands/xgroup-destroy/ + xgroup-destroy: async func(connection: borrow, key: string, group: string) -> result; + + /// Set the last delivered ID for a consumer group. + /// + /// XGROUP SETID key group 〈 id | $ 〉 [ ENTRIESREAD entriesread ] + /// https://valkey.io/commands/xgroup-setid/ + xgroup-setid: async func(connection: borrow, key: string, group: string, id: stream-id, entriesread: option) -> result<_, error>; + + /// A consumer of a group, see `xinfo-consumers`. + record xinfo-consumer { + /// The consumer's name. + name: string, + /// The number of entries pending for the consumer. + pending: integer, + /// The time since the consumer last attempted an interaction, e.g. XREADGROUP, XCLAIM or + /// XAUTOCLAIM. + idle: duration, + /// The time since the consumer last interacted successfully, e.g. read or claimed entries. + /// None when the consumer has never been active. + inactive: option, + } + + /// Consumers that belong to the consumer group of the stream stored at . + /// + /// XINFO CONSUMERS key group + /// https://valkey.io/commands/xinfo-consumers/ + xinfo-consumers: async func(connection: borrow, key: string, group: string) -> result, error>; + + /// A consumer group of a stream, see `xinfo-groups`. + record xinfo-group { + /// The group's name. + name: string, + /// The number of consumers in the group. + consumers: integer, + /// The ID of the last entry delivered to the group's consumers. + last-delivered-id: stream-id, + /// The logical read counter of the last entry delivered to the group's consumers, none when unknown. + entries-read: option, + /// The number of entries in the stream not yet delivered to the group's consumers, none when it + /// cannot be determined. + lag: option, + } + + /// Consumer groups of the stream stored at . + /// + /// XINFO GROUPS key + /// https://valkey.io/commands/xinfo-groups/ + xinfo-groups: async func(connection: borrow, key: string) -> result, error>; + + /// Information about a stream, see `xinfo-stream`. + record xinfo-stream-reply { + /// The number of entries in the stream. + length: integer, + /// The number of keys in the radix tree storing the stream. + radix-tree-keys: integer, + /// The number of nodes in the radix tree storing the stream. + radix-tree-nodes: integer, + /// The number of consumer groups of the stream. + groups: integer, + /// The ID of the last entry added to the stream. + last-generated-id: stream-id, + /// The highest ID deleted from the stream. + max-deleted-entry-id: stream-id, + /// The number of entries added to the stream over its lifetime. + entries-added: integer, + /// The ID and fields of the first entry. + /// None when the stream is empty. + first-entry: option>>, + /// The ID and fields of the last entry. + /// None when the stream is empty. + last-entry: option>>, + } + + /// Information about the stream stored at . + /// + /// See also `xinfo-stream-full` + /// + /// XINFO STREAM key + /// https://valkey.io/commands/xinfo-stream/ + xinfo-stream: async func(connection: borrow, key: string) -> result; + + /// Information about a stream, its entries and consumer groups, see `xinfo-stream-full`. + record xinfo-stream-reply-full { + /// The number of entries in the stream. + length: integer, + /// The number of keys in the radix tree storing the stream. + radix-tree-keys: integer, + /// The number of nodes in the radix tree storing the stream. + radix-tree-nodes: integer, + /// The ID of the last entry added to the stream. + last-generated-id: stream-id, + /// The highest ID deleted from the stream. + max-deleted-entry-id: stream-id, + /// The number of entries added to the stream over its lifetime. + entries-added: integer, + /// The ID and fields of the entries, in ascending ID order, up to the count given. + entries: list>>, + /// The consumer groups of the stream. + groups: list, + } + + /// A consumer group of a stream, with its pending entries and consumers. + record xinfo-stream-reply-full-group { + /// The group's name. + name: string, + /// The ID of the last entry delivered to the group's consumers. + last-delivered-id: stream-id, + /// The logical read counter of the last entry delivered to the group's consumers, none when unknown. + entries-read: option, + /// The number of entries in the stream not yet delivered to the group's consumers, none when it + /// cannot be determined. + lag: option, + /// The number of entries in the group's pending entries list (PEL), delivered but not acknowledged. + pel-count: integer, + /// The group's pending entries, the ID of each, the consumer it was delivered to, when it was + /// last delivered and the number of times it was delivered. + pending: list>, + /// The group's consumers. + consumers: list, + } + + /// A consumer of a group, with its pending entries. + record xinfo-stream-reply-full-consumer { + /// The consumer's name. + name: string, + /// When the consumer last attempted an interaction, e.g. XREADGROUP, XCLAIM or XAUTOCLAIM. + seen-time: instant, + /// When the consumer last interacted successfully, e.g. read or claimed entries. + /// None when the consumer has never been active. + active-time: option, + /// The number of entries pending for the consumer, delivered but not acknowledged. + pel-count: integer, + /// The consumer's pending entries, the ID of each, when it was last delivered and the number of + /// times it was delivered. + pending: list>, + } + + /// Information about the stream stored at . + /// + /// See also `xinfo-stream` + /// + /// XINFO STREAM key FULL [ COUNT count ] + /// https://valkey.io/commands/xinfo-stream/ + xinfo-stream-full: async func(connection: borrow, key: string, count: option) -> result; + + /// Number of entries inside a stream. + /// + /// XLEN key + /// https://valkey.io/commands/xlen/ + xlen: async func(connection: borrow, key: string) -> result; + + /// List of pending messages. + /// + /// See also `xpending-extended`. + /// + /// XPENDING key group + /// https://valkey.io/commands/xpending/ + xpending: async func(connection: borrow, key: string, group: string) -> result>, error>; + + /// List of pending messages. + /// + /// See also `xpending`. + /// + /// XPENDING key group [ IDLE min-idle-time ] start end count [ consumer ] + /// https://valkey.io/commands/xpending/ + xpending-extended: async func(connection: borrow, key: string, group: string, min-idle-time: option, start: stream-id, end: stream-id, count: integer, consumer: option) -> result>, error>; + + /// Stream entries matching a given range of IDs. + /// + /// XRANGE key start end [ COUNT count ] + /// https://valkey.io/commands/xrange/ + xrange: async func(connection: borrow, key: string, start: stream-id, end: stream-id, count: option) -> result>>, error>; + + /// Read data from one or multiple streams, only returning entries with an ID greater than the last received ID reported by the caller. + /// + /// XREAD [ COUNT count ] [ BLOCK milliseconds ] STREAMS key [ key ... ] ID [ ID ... ] + /// https://valkey.io/commands/xread/ + xread: async func(connection: borrow, count: option, block: option, keys: list, ids: list) -> result>>>>, error>; + + /// Special version of the XREAD command with support for consumer groups. + /// + /// XREADGROUP GROUP group consumer [ COUNT count ] [ BLOCK milliseconds ] [ NOACK ] STREAMS key [ key ... ] ID [ ID ... ] + /// https://valkey.io/commands/xreadgroup/ + xreadgroup: async func(connection: borrow, group: string, consumer: string, count: option, block: option, noack: bool, keys: list, ids: list) -> result>>>>, error>; + + /// XRANGE, but returning the entries in reverse order. + /// + /// XREVRANGE key end start [ COUNT count ] + /// https://valkey.io/commands/xrevrange/ + xrevrange: async func(connection: borrow, key: string, end: stream-id, start: stream-id, count: option) -> result>>, error>; + + /// Internal command. It is used by a Valkey primary to replicate the last delivered ID of streams. + /// + /// XSETID key last-id [ ENTRIESADDED entries-added ] [ MAXDELETEDID max-deleted-id ] + /// https://valkey.io/commands/xsetid/ + xsetid: async func(connection: borrow, key: string, last-id: stream-id, entries-added: option, max-deleted-id: option) -> result<_, error>; + + /// Trims the stream by evicting older entries. + /// + /// XTRIM key 〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] + /// https://valkey.io/commands/xtrim/ + xtrim: async func(connection: borrow, key: string, trim: stream-trim) -> result; + + + // string commands + + + /// Appends the value at the end of the string. + /// + /// APPEND key value + /// https://valkey.io/commands/append/ + append: async func(connection: borrow, key: string, value: string) -> result; + + /// Decrements the number stored at key by one. + /// + /// DECR key + /// https://valkey.io/commands/decr/ + decr: async func(connection: borrow, key: string) -> result; + + /// Decrements the number stored at key by decrement. + /// + /// DECRBY key decrement + /// https://valkey.io/commands/decrby/ + decrby: async func(connection: borrow, key: string, decrement: integer) -> result; + + /// Deletes the key if its value matches the given string. + /// + /// DELIFEQ key value + /// https://valkey.io/commands/delifeq/ + delifeq: async func(connection: borrow, key: string, value: string) -> result; + + + /// Get the value of key. + /// + /// GET key + /// https://valkey.io/commands/get/ + get: async func(connection: borrow, key: string) -> result, error>; + + /// Get the value of key and delete the key. + /// + /// GETDEL key + /// https://valkey.io/commands/getdel/ + getdel: async func(connection: borrow, key: string) -> result, error>; + + /// The expiration to set on a key, see `getex`. + variant getex-ttl { + /// Set the specified expire time, in seconds. + ex(duration), + /// Set the specified expire time, in milliseconds. + px(duration), + /// Set the specified Unix time at which the key will expire, in seconds. + exat(instant), + /// Set the specified Unix time at which the key will expire, in milliseconds. + pxat(instant), + /// Remove the time to live associated with the key. + persist, + } + + /// Get the value of key and optionally set its expiration. + /// + /// GETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ] + /// https://valkey.io/commands/getex/ + getex: async func(connection: borrow, key: string, ttl: option) -> result, error>; + + /// Returns the substring of the string value stored at key, determined by the offsets start and end. + /// + /// GETRANGE key start end + /// https://valkey.io/commands/getrange/ + getrange: async func(connection: borrow, key: string, start: integer, end: integer) -> result; + + /// Atomically sets key to value and returns the old value stored at key. + /// + /// GETSET key value + /// https://valkey.io/commands/getset/ + getset: async func(connection: borrow, key: string, value: string) -> result, error>; + + /// Increments the number stored at key by one. + /// + /// INCR key + /// https://valkey.io/commands/incr/ + incr: async func(connection: borrow, key: string) -> result; + + /// Increments the number stored at key by increment. + /// + /// INCRBY key increment + /// https://valkey.io/commands/incrby/ + incrby: async func(connection: borrow, key: string, increment: integer) -> result; + + /// Increment the string representing a floating point number stored at key by the specified increment. + /// + /// INCRBYFLOAT key increment + /// https://valkey.io/commands/incrbyfloat/ + incrbyfloat: async func(connection: borrow, key: string, increment: double) -> result; + + /// The LCS command implements the longest common subsequence algorithm. + /// + /// See also `lcs-len` and `lcs-idx` + /// + /// LCS key1 key2 + /// https://valkey.io/commands/lcs/ + lcs: async func(connection: borrow, key1: string, key2: string) -> result; + + /// The LCS command implements the longest common subsequence algorithm. + /// + /// See also `lcs` and `lcs-idx` + /// + /// LCS key1 key2 LEN + /// https://valkey.io/commands/lcs/ + lcs-len: async func(connection: borrow, key1: string, key2: string) -> result; + + /// The longest common subsequence of two strings, as the ranges matching in each, see `lcs-idx`. + record lcs-idx-reply { + /// The ranges that match, from the end of the strings to their start. + matches: list, + /// The length of the longest common subsequence. + len: integer, + } + + /// A part of the longest common subsequence. + record lcs-idx-match { + /// The inclusive start and end positions of the match in the first key's string. + key1-range: tuple, + /// The inclusive start and end positions of the match in the second key's string. + key2-range: tuple, + /// The length of the match, with `with-match-len`. + len: option, + } + + /// The LCS command implements the longest common subsequence algorithm. + /// + /// See also `lcs` and `lcs-len` + /// + /// LCS key1 key2 IDX [ MINMATCHLEN min-match-len ] [ WITHMATCHLEN ] + /// https://valkey.io/commands/lcs/ + lcs-idx: async func(connection: borrow, key1: string, key2: string, min-match-len: option, with-match-len: bool ) -> result; + + /// Returns the values of all specified keys. + /// + /// MGET key [ key ... ] + /// https://valkey.io/commands/mget/ + mget: async func(connection: borrow, keys: list) -> result>, error>; + + /// Sets the given keys to their respective values. + /// + /// MSET key value [ key value ... ] + /// https://valkey.io/commands/mset/ + mset: async func(connection: borrow, keys-and-values: list>) -> result<_, error>; + + /// The condition for setting the keys, see `msetex`. + variant msetex-precondition { + /// Only set the keys if all of them do not already exist. + nx, + /// Only set the keys if all of them already exist. + xx, + } + + /// The expiration to set on the keys, see `msetex`. + type msetex-ttl = set-ttl; + + /// Sets multiple key-value pairs while also assigning a shared expiration time to all the keys atomically. + /// + /// MSETEX numkeys key value [ key value ... ] [ NX | XX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] + /// https://valkey.io/commands/msetex/ + msetex: async func(connection: borrow, keys-and-values: list>, precondition: option, ttl: option) -> result; + + /// Sets the given keys to their respective values. + /// + /// MSETNX key value [ key value ... ] + /// https://valkey.io/commands/msetnx/ + msetnx: async func(connection: borrow, keys-and-values: list>) -> result; + + /// Set key to hold the string value with expiration in milliseconds. + /// + /// PSETEX key milliseconds value + /// https://valkey.io/commands/psetex/ + psetex: async func(connection: borrow, key: string, milliseconds: duration, value: string) -> result<_, error>; + + /// The condition for setting a key, see `set`. + variant set-precondition { + /// Only set the key if it does not already exist. + nx, + /// Only set the key if it already exists. + xx, + /// Set the key if the comparison value matches the existing value. + ifeq(string), + /// Set the key if the comparison value does not match the existing value. + ifne(string), + } + + /// The expiration to set on a key, see `set`. + variant set-ttl { + /// Set the specified expire time, in seconds. + ex(duration), + /// Set the specified expire time, in milliseconds. + px(duration), + /// Set the specified Unix time at which the key will expire, in seconds. + exat(instant), + /// Set the specified Unix time at which the key will expire, in milliseconds. + pxat(instant), + /// Retain the time to live associated with the keys. + keepttl, + } + + /// Set key to hold the string value. + /// + /// SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] + /// https://valkey.io/commands/set/ + set: async func(connection: borrow, key: string, value: string, precondition: option, ttl: option) -> result<_, error>; + + /// Set key to hold the string value. Returning the previous value. + /// + /// SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] GET [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] + /// https://valkey.io/commands/set/ + set-get: async func(connection: borrow, key: string, value: string, precondition: option, ttl: option) -> result, error>; + + /// Set key to hold the string value with expiration in seconds. + /// + /// SETEX key seconds value + /// https://valkey.io/commands/setex/ + setex: async func(connection: borrow, key: string, seconds: duration, value: string) -> result<_, error>; + + /// Set key to hold string value if key does not exist. + /// + /// SETNX key value + /// https://valkey.io/commands/setnx/ + setnx: async func(connection: borrow, key: string, value: string) -> result; + + /// Overwrites part of the string stored at key, starting at the specified offset, for the entire length of value. + /// + /// SETRANGE key offset value + /// https://valkey.io/commands/setrange/ + setrange: async func(connection: borrow, key: string, offset: u32, value: string) -> result; + + /// Returns the length of the string value stored at key. + /// + /// STRLEN key + /// hthttps://valkey.io/commands/strlen/ + strlen: async func(connection: borrow, key: string) -> result; + + /// Returns the substring of the string value stored at key, determined by the offsets start and end. + /// + /// SUBSTR key start end + /// https://valkey.io/commands/substr/ + substr: async func(connection: borrow, key: string, start: s32, end: s32) -> result; + + + // transaction commands + + // not yet implemented, from the @transaction category: + // DISCARD + // EXEC + // MULTI + // UNWATCH + // WATCH + +} diff --git a/wit/connection-commands.wit b/wit/connection-commands.wit deleted file mode 100644 index 0c48b1b..0000000 --- a/wit/connection-commands.wit +++ /dev/null @@ -1,58 +0,0 @@ -/// Commands about the connection, Valkey's @connection ACL category. -/// -/// Not to be confused with the `connection` interface, the mechanics of talking to a server. -interface connection-commands { - use resp.{value}; - use connection.{connection, error}; - - /// Options for `hello`, the protocol is always 3. - record hello-opts { - auth: option>, - client-name: option, - } - - auth: async func(connection: borrow, username: string, password: string) -> result<_, error>; - /// Sends `HELLO 3`, with the options' AUTH and SETNAME, and returns the server's properties. - /// - /// A connection already sends HELLO when it is established, see `connection.new`, sending it - /// again is useful e.g. to authenticate as another user. - hello: async func(connection: borrow, opts: option) -> result>, error>; - ping: async func(connection: borrow) -> result<_, error>; - - // not yet implemented, from the @connection category: - // ASKING - // CLIENT CACHING - // CLIENT CAPA - // CLIENT GETNAME - // CLIENT GETREDIR - // CLIENT ID - // CLIENT IMPORT-SOURCE - // CLIENT INFO - // CLIENT KILL - // CLIENT LIST - // CLIENT NO-EVICT - // CLIENT NO-TOUCH - // CLIENT PAUSE - // CLIENT REPLY - // CLIENT SETINFO - // CLIENT SETNAME - // CLIENT TRACKING - // CLIENT TRACKINGINFO - // CLIENT UNBLOCK - // CLIENT UNPAUSE - // COMMAND - // COMMAND COUNT - // COMMAND DOCS - // COMMAND GETKEYS - // COMMAND GETKEYSANDFLAGS - // COMMAND INFO - // COMMAND LIST - // ECHO - // QUIT - // READONLY - // READWRITE - // RESET - // SELECT - // WAIT - // WAITAOF -} diff --git a/wit/connection.wit b/wit/connection.wit index 1128d9c..9a07790 100644 --- a/wit/connection.wit +++ b/wit/connection.wit @@ -9,8 +9,8 @@ /// command interfaces, e.g. `string-commands`, that borrow the connection, so command groups can be /// implemented independently of each other. The groups follow Valkey's ACL categories. interface connection { - use wasi:clocks/types@0.3.0.{duration}; - use resp.{error as resp-error, value}; + use wasi:clocks/system-clock@0.3.0.{duration, instant}; + use resp.{error as resp-error, value, integer, double}; /// The errors of a connection and of every command interface. variant error { @@ -60,6 +60,10 @@ interface connection { /// /// Several commands may be in flight at once, replies are paired with commands in the order /// the commands were sent. + /// + /// SUBSCRIBE, PSUBSCRIBE, SSUBSCRIBE and their UNSUBSCRIBE counterparts have no reply, the + /// server confirms them with push messages, see `pushes`. Their call resolves to null once + /// the command is written. call: async func(command: list) -> result; /// Sends a command without waiting for its reply, resolving once the command is written. @@ -71,6 +75,9 @@ interface connection { /// Push messages, e.g. from SUBSCRIBE or client tracking, kept apart from replies. /// + /// Values the server sends unprompted, when no command is waiting for a reply, are + /// included, e.g. the feed of MONITOR, which are not RESP push values. + /// /// Each call returns a new stream that receives every push message arriving after the call, /// push messages arriving while no stream is open are dropped. A stream that falls too far /// behind is closed rather than holding up replies, the push messages it missed are lost. diff --git a/wit/geo-commands.wit b/wit/geo-commands.wit deleted file mode 100644 index db26228..0000000 --- a/wit/geo-commands.wit +++ /dev/null @@ -1,16 +0,0 @@ -/// Commands for geospatial indexes, Valkey's @geo ACL category. -interface geo-commands { - use connection.{connection, error}; - - // not yet implemented, from the @geo category: - // GEOADD - // GEODIST - // GEOHASH - // GEOPOS - // GEORADIUS - // GEORADIUS_RO - // GEORADIUSBYMEMBER - // GEORADIUSBYMEMBER_RO - // GEOSEARCH - // GEOSEARCHSTORE -} diff --git a/wit/hash-commands.wit b/wit/hash-commands.wit deleted file mode 100644 index 9885bcc..0000000 --- a/wit/hash-commands.wit +++ /dev/null @@ -1,70 +0,0 @@ -/// Commands for hash values, Valkey's @hash ACL category. -interface hash-commands { - use wasi:clocks/system-clock@0.3.0.{duration, instant}; - use connection.{connection, error}; - - // copied from keyspace-commands, so each command interface stands alone - enum expire-mode { - /// Set expiration only when the field has no expiration. - nx, - /// Set expiration only when the field has an existing expiration. - xx, - /// Set expiration only when the new expiration is greater than current one. - gt, - /// Set expiration only when the new expiration is less than current one. - lt, - } - - enum expire-result { - /// -2: key does not exist, or field does not exist in the HASH. - does-not-exist, - /// 0: the specified NX | XX | GT | LT condition has not been met. - condition-not-met, - /// 1: the expiration time was applied. - applied, - /// 2: when called with 0 seconds. - zero-seconds, - } - - record hrandfield-opts { - count: option, - with-values: option, - } - - record hscan-opts { - match: option, - count: option, - no-values: option, - } - - hdel: async func(connection: borrow, key: string, field: string) -> result<_, error>; - hexists: async func(connection: borrow, key: string, field: string) -> result; - hexpire: async func(connection: borrow, key: string, seconds: duration, mode: option, fields: list) -> result, error>; - hexpireat: async func(connection: borrow, key: string, seconds: instant, mode: option, fields: list) -> result, error>; - hget: async func(connection: borrow, key: string, field: string) -> result, error>; - hgetall: async func(connection: borrow, key: string) -> result>, error>; - hincrby: async func(connection: borrow, key: string, field: string, increment: s64) -> result; - hincrbyfloat: async func(connection: borrow, key: string, field: string, increment: f64) -> result; - hkeys: async func(connection: borrow, key: string) -> result, error>; - hlen: async func(connection: borrow, key: string) -> result; - hmget: async func(connection: borrow, key: string, fields: list) -> result>, error>; - hmset: async func(connection: borrow, key: string, fields: list>) -> result<_, error>; - hrandfield: async func(connection: borrow, key: string, opts: option) -> result>>>, error>; - hscan: async func(connection: borrow, key: string, cursor: option, opts: option) -> result, list>>>, error>; - hset: async func(connection: borrow, key: string, field: string, value: string) -> result<_, error>; - hsetnx: async func(connection: borrow, key: string, field: string, value: string) -> result; - hstrlen: async func(connection: borrow, key: string, field: string) -> result; - hvals: async func(connection: borrow, key: string) -> result, error>; - - // not yet implemented, from the @hash category: - // HEXPIRETIME - // HGETDEL - // HGETEX - // HPERSIST - // HPEXPIRE - // HPEXPIREAT - // HPEXPIRETIME - // HPTTL - // HSETEX - // HTTL -} diff --git a/wit/hyperloglog-commands.wit b/wit/hyperloglog-commands.wit deleted file mode 100644 index 7dc4765..0000000 --- a/wit/hyperloglog-commands.wit +++ /dev/null @@ -1,11 +0,0 @@ -/// Commands for HyperLogLogs, Valkey's @hyperloglog ACL category. -interface hyperloglog-commands { - use connection.{connection, error}; - - // not yet implemented, from the @hyperloglog category: - // PFADD - // PFCOUNT - // PFDEBUG - // PFMERGE - // PFSELFTEST -} diff --git a/wit/keyspace-commands.wit b/wit/keyspace-commands.wit deleted file mode 100644 index 101c145..0000000 --- a/wit/keyspace-commands.wit +++ /dev/null @@ -1,65 +0,0 @@ -/// Commands for keys of any type, Valkey's @keyspace ACL category. -interface keyspace-commands { - use wasi:clocks/system-clock@0.3.0.{duration, instant}; - use connection.{connection, error}; - - enum expire-mode { - /// Set expiration only when the field has no expiration. - nx, - /// Set expiration only when the field has an existing expiration. - xx, - /// Set expiration only when the new expiration is greater than current one. - gt, - /// Set expiration only when the new expiration is less than current one. - lt, - } - - enum expire-result { - /// -2: key does not exist, or field does not exist in the HASH. - does-not-exist, - /// 0: the specified NX | XX | GT | LT condition has not been met. - condition-not-met, - /// 1: the expiration time was applied. - applied, - /// 2: when called with 0 seconds. - zero-seconds, - } - - del: async func(connection: borrow, key: string) -> result<_, error>; - exists: async func(connection: borrow, key: string) -> result; - expire: async func(connection: borrow, key: string, seconds: duration, mode: option) -> result; - expireat: async func(connection: borrow, key: string, seconds: instant, mode: option) -> result; - keys: async func(connection: borrow, pattern: string) -> result, error>; - - // not yet implemented, from the @keyspace category: - // CLUSTERSCAN - // CLUSTER FLUSHSLOT - // COPY - // DBSIZE - // DUMP - // EXPIRETIME - // FLUSHALL - // FLUSHDB - // MIGRATE - // MOVE - // OBJECT ENCODING - // OBJECT FREQ - // OBJECT IDLETIME - // OBJECT REFCOUNT - // PERSIST - // PEXPIRE - // PEXPIREAT - // PEXPIRETIME - // PTTL - // RANDOMKEY - // RENAME - // RENAMENX - // RESTORE - // RESTORE-ASKING - // SCAN - // SWAPDB - // TOUCH - // TTL - // TYPE - // UNLINK -} diff --git a/wit/list-commands.wit b/wit/list-commands.wit deleted file mode 100644 index 5d75644..0000000 --- a/wit/list-commands.wit +++ /dev/null @@ -1,30 +0,0 @@ -/// Commands for list values, Valkey's @list ACL category. -interface list-commands { - use connection.{connection, error}; - - // not yet implemented, from the @list category: - // BLMOVE - // BLMPOP - // BLPOP - // BRPOP - // BRPOPLPUSH - // LINDEX - // LINSERT - // LLEN - // LMOVE - // LMPOP - // LPOP - // LPOS - // LPUSH - // LPUSHX - // LRANGE - // LREM - // LSET - // LTRIM - // RPOP - // RPOPLPUSH - // RPUSH - // RPUSHX - // SORT - // SORT_RO -} diff --git a/wit/pubsub-commands.wit b/wit/pubsub-commands.wit deleted file mode 100644 index 8d2685b..0000000 --- a/wit/pubsub-commands.wit +++ /dev/null @@ -1,84 +0,0 @@ -/// Publish/subscribe commands, Valkey's @pubsub ACL category. -/// -/// Messages for subscriptions arrive as push messages, see `connection.pushes`. -interface pubsub-commands { - use connection.{connection, error}; - - /// Subscribes the client to the given patterns. - /// - /// PSUBSCRIBE pattern [ pattern ... ] - /// https://valkey.io/commands/psubscribe/ - psubscribe: async func(connection: borrow, patterns: list) -> result<_, error>; - - /// Posts a message to the given channel. - /// - /// PUBLISH channel message - /// https://valkey.io/commands/publish/ - publish: async func(connection: borrow, channel: string, message: string) -> result; - - /// Lists the currently active channels. - /// - /// PUBSUB CHANNELS [ pattern ] - /// https://valkey.io/commands/pubsub-channels/ - pubsub-channels: async func(connection: borrow, pattern: option) -> result, error>; - - /// Returns the number of unique patterns that are subscribed to by clients. - /// - /// PUBSUB NUMPAT - /// https://valkey.io/commands/pubsub-numpat/ - pubsub-numpat: async func(connection: borrow) -> result; - - /// Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels. - /// - /// PUBSUB NUMSUB [ channel ] [ [ channel ] ... ] - /// https://valkey.io/commands/pubsub-numsub/ - pubsub-numsub: async func(connection: borrow, channels: list) -> result, error>; - - /// Lists the currently active shard channels. - /// - /// PUBSUB SHARDCHANNELS [ pattern ] - /// https://valkey.io/commands/pubsub-shardchannels/ - pubsub-shardchannels: async func(connection: borrow, pattern: option) -> result, error>; - - /// Returns the number of subscribers for the specified shard channels. - /// - /// PUBSUB SHARDNUMSUB [ shardchannel [ shardchannel ... ] ] - /// https://valkey.io/commands/pubsub-shardnumsub/ - pubsub-shardnumsub: async func(connection: borrow, shardchannels: list) -> result, error>; - - /// Unsubscribes the client from the given patterns, or from all of them if none is given. - /// - /// PUNSUBSCRIBE [ pattern [ pattern ... ] ] - /// https://valkey.io/commands/punsubscribe/ - punsubscribe: async func(connection: borrow, patterns: list) -> result<_, error>; - - /// Posts a message to the given shard channel. - /// - /// SPUBLISH shardchannel message - /// https://valkey.io/commands/spublish/ - spublish: async func(connection: borrow, shardchannel: string, message: string) -> result; - - /// Subscribes the client to the specified shard channels. - /// - /// SSUBSCRIBE shardchannel [ shardchannel ... ] - /// https://valkey.io/commands/ssubscribe/ - ssubscribe: async func(connection: borrow, shardchannels: list) -> result<_, error>; - - /// Subscribes the client to the specified channels. - /// - /// SUBSCRIBE channel [ channel ... ] - /// https://valkey.io/commands/subscribe/ - subscribe: async func(connection: borrow, channels: list) -> result<_, error>; - - /// Unsubscribes the client from the given shard channels, or from all of them if none is given. - /// - /// SUNSUBSCRIBE [ shardchannel ] [ [ shardchannel ] ... ] - /// https://valkey.io/commands/sunsubscribe/ - sunsubscribe: async func(connection: borrow, shardchannels: list) -> result<_, error>; - - /// Unsubscribes the client from the given channels, or from all of them if none is given. - /// - /// UNSUBSCRIBE [ channel ] [ [ channel ] ... ] - /// https://valkey.io/commands/unsubscribe/ - unsubscribe: async func(connection: borrow, channels: list) -> result<_, error>; -} diff --git a/wit/resp.wit b/wit/resp.wit index fcd0d8e..ebde634 100644 --- a/wit/resp.wit +++ b/wit/resp.wit @@ -1,15 +1,18 @@ interface resp { type error = string; + type integer = s64; + type double = f64; + variant value { %string(string), error(string), - integer(s64), + integer(integer), bulk-string(string), array(list), null, boolean(bool), - double(f64), + double(double), big-number(string), bulk-error(string), verbatim-string(tuple), diff --git a/wit/scripting-commands.wit b/wit/scripting-commands.wit deleted file mode 100644 index 3c950fb..0000000 --- a/wit/scripting-commands.wit +++ /dev/null @@ -1,26 +0,0 @@ -/// Commands for scripts and functions, Valkey's @scripting ACL category. -interface scripting-commands { - use connection.{connection, error}; - - // not yet implemented, from the @scripting category: - // EVAL - // EVAL_RO - // EVALSHA - // EVALSHA_RO - // FCALL - // FCALL_RO - // FUNCTION DELETE - // FUNCTION DUMP - // FUNCTION FLUSH - // FUNCTION KILL - // FUNCTION LIST - // FUNCTION LOAD - // FUNCTION RESTORE - // FUNCTION STATS - // SCRIPT DEBUG - // SCRIPT EXISTS - // SCRIPT FLUSH - // SCRIPT KILL - // SCRIPT LOAD - // SCRIPT SHOW -} diff --git a/wit/set-commands.wit b/wit/set-commands.wit deleted file mode 100644 index d6f6d06..0000000 --- a/wit/set-commands.wit +++ /dev/null @@ -1,25 +0,0 @@ -/// Commands for set values, Valkey's @set ACL category. -interface set-commands { - use connection.{connection, error}; - - // not yet implemented, from the @set category: - // SADD - // SCARD - // SDIFF - // SDIFFSTORE - // SINTER - // SINTERCARD - // SINTERSTORE - // SISMEMBER - // SMEMBERS - // SMISMEMBER - // SMOVE - // SORT - // SORT_RO - // SPOP - // SRANDMEMBER - // SREM - // SSCAN - // SUNION - // SUNIONSTORE -} diff --git a/wit/sortedset-commands.wit b/wit/sortedset-commands.wit deleted file mode 100644 index fdb3ef6..0000000 --- a/wit/sortedset-commands.wit +++ /dev/null @@ -1,43 +0,0 @@ -/// Commands for sorted set values, Valkey's @sortedset ACL category. -interface sortedset-commands { - use connection.{connection, error}; - - // not yet implemented, from the @sortedset category: - // BZMPOP - // BZPOPMAX - // BZPOPMIN - // SORT - // SORT_RO - // ZADD - // ZCARD - // ZCOUNT - // ZDIFF - // ZDIFFSTORE - // ZINCRBY - // ZINTER - // ZINTERCARD - // ZINTERSTORE - // ZLEXCOUNT - // ZMPOP - // ZMSCORE - // ZPOPMAX - // ZPOPMIN - // ZRANDMEMBER - // ZRANGE - // ZRANGEBYLEX - // ZRANGEBYSCORE - // ZRANGESTORE - // ZRANK - // ZREM - // ZREMRANGEBYLEX - // ZREMRANGEBYRANK - // ZREMRANGEBYSCORE - // ZREVRANGE - // ZREVRANGEBYLEX - // ZREVRANGEBYSCORE - // ZREVRANK - // ZSCAN - // ZSCORE - // ZUNION - // ZUNIONSTORE -} diff --git a/wit/stream-commands.wit b/wit/stream-commands.wit deleted file mode 100644 index c7f992e..0000000 --- a/wit/stream-commands.wit +++ /dev/null @@ -1,27 +0,0 @@ -/// Commands for stream values, Valkey's @stream ACL category. -interface stream-commands { - use connection.{connection, error}; - - // not yet implemented, from the @stream category: - // XACK - // XADD - // XAUTOCLAIM - // XCLAIM - // XDEL - // XGROUP CREATE - // XGROUP CREATECONSUMER - // XGROUP DELCONSUMER - // XGROUP DESTROY - // XGROUP SETID - // XINFO CONSUMERS - // XINFO GROUPS - // XINFO STREAM - // XLEN - // XPENDING - // XRANGE - // XREAD - // XREADGROUP - // XREVRANGE - // XSETID - // XTRIM -} diff --git a/wit/string-commands.wit b/wit/string-commands.wit deleted file mode 100644 index d39b963..0000000 --- a/wit/string-commands.wit +++ /dev/null @@ -1,31 +0,0 @@ -/// Commands for string values, Valkey's @string ACL category. -interface string-commands { - use connection.{connection, error}; - - get: async func(connection: borrow, key: string) -> result, error>; - incr: async func(connection: borrow, key: string) -> result; - incrby: async func(connection: borrow, key: string, increment: s64) -> result; - set: async func(connection: borrow, key: string, value: string) -> result<_, error>; - - // not yet implemented, from the @string category: - // APPEND - // DECR - // DECRBY - // DELIFEQ - // GETDEL - // GETEX - // GETRANGE - // GETSET - // INCRBYFLOAT - // LCS - // MGET - // MSET - // MSETEX - // MSETNX - // PSETEX - // SETEX - // SETNX - // SETRANGE - // STRLEN - // SUBSTR -} diff --git a/wit/transaction-commands.wit b/wit/transaction-commands.wit deleted file mode 100644 index 13f6eb4..0000000 --- a/wit/transaction-commands.wit +++ /dev/null @@ -1,11 +0,0 @@ -/// Commands for transactions, Valkey's @transaction ACL category. -interface transaction-commands { - use connection.{connection, error}; - - // not yet implemented, from the @transaction category: - // DISCARD - // EXEC - // MULTI - // UNWATCH - // WATCH -} diff --git a/wit/worlds.wit b/wit/worlds.wit index 33a0496..b6f4dbb 100644 --- a/wit/worlds.wit +++ b/wit/worlds.wit @@ -1,25 +1,9 @@ package componentized:valkey@0.3.0-dev; world imports { - import resp; + import commands; import connection; import connector; + import resp; import tcp; - - // commands, grouped by Valkey's ACL categories - import admin-commands; - import bitmap-commands; - import connection-commands; - import geo-commands; - import hash-commands; - import hyperloglog-commands; - import keyspace-commands; - import list-commands; - import pubsub-commands; - import scripting-commands; - import set-commands; - import sortedset-commands; - import stream-commands; - import string-commands; - import transaction-commands; } From abaf9fe0153e0d746df2146093be194b26646cee Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Mon, 12 Oct 2026 12:29:59 -0400 Subject: [PATCH 04/37] polishing Signed-off-by: Scott Andrews --- components/as-keyvalue/src/lib.rs | 4 +- components/commands/src/lib.rs | 78 ++++++++++++++------------ components/commands/src/value.rs | 57 +++++++++++++++++-- wit/commands.wit | 91 ++++++++++++++++++++----------- 4 files changed, 157 insertions(+), 73 deletions(-) diff --git a/components/as-keyvalue/src/lib.rs b/components/as-keyvalue/src/lib.rs index 0a3ea0b..d45ae34 100644 --- a/components/as-keyvalue/src/lib.rs +++ b/components/as-keyvalue/src/lib.rs @@ -108,7 +108,7 @@ impl GuestBucket for KeyvalueToValkeyBucket { let match_ = prefix.map(|prefix| format!("{}*", escape_glob(&prefix))); let count = None; - let (cursor, fields) = commands::hscan_novalues( + let (cursor, keys) = commands::hscan_novalues( &self.connection, self.hash_key.clone(), cursor, @@ -118,7 +118,7 @@ impl GuestBucket for KeyvalueToValkeyBucket { .await?; Ok(KeyResponse { - keys: fields.into_iter().map(|(key, _)| key).collect(), + keys, cursor: Some(cursor.to_string()), }) } diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index b3dd4bb..f3eebee 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -3,7 +3,7 @@ use crate::exports::componentized::valkey::commands::{ Connection, Cursor, Double, Duration, Error, GetexTtl, Guest, Instant, Integer, LcsIdxReply, MsetexPrecondition, MsetexTtl, Rule, SetPrecondition, SetTtl, StreamId, StreamTrim, TtlMode, TtlResult, Value, XclaimOpts, XinfoConsumer, XinfoGroup, XinfoStreamReply, - XinfoStreamReplyFull, + XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, }; use crate::value::OrZeroId; @@ -271,44 +271,44 @@ impl Guest for Commands { query(connection, ("HMSET", key, field_and_values)).await } - #[doc = " A random field from the hash value stored at key."] + #[doc = " Random fields from the hash value stored at key, up to the count."] #[doc = ""] - #[doc = " Also see `hrandfield-count` and `hrandfield-count-withvalues`."] + #[doc = " Without a count, one field, or none when the key does not exist. A negative count may return"] + #[doc = " the same field more than once, its absolute value is the number of fields returned."] #[doc = ""] - #[doc = " HRANDFIELD key"] - #[doc = " https://valkey.io/commands/hrandfield/"] - #[allow(async_fn_in_trait)] - async fn hrandfield(connection: &Connection, key: String) -> Result, Error> { - query(connection, ("HRANDFIELD", key)).await - } - - #[doc = " A random set of fields from the hash value stored at key up to the count."] - #[doc = ""] - #[doc = " Also see `hrandfield` and `hrandfield-count-withvalues`."] + #[doc = " Also see `hrandfield-withvalues`."] #[doc = ""] - #[doc = " HRANDFIELD key count"] + #[doc = " HRANDFIELD key [ count ]"] #[doc = " https://valkey.io/commands/hrandfield/"] #[allow(async_fn_in_trait)] - async fn hrandfield_count( + async fn hrandfield( connection: &Connection, key: String, - count: Integer, + count: Option, ) -> Result, Error> { - query(connection, ("HRANDFIELD", key, count)).await + if count.is_some() { + return query(connection, ("HRANDFIELD", key, count)).await; + } + // without a count the reply is the field itself, or null + let field: Option = query(connection, ("HRANDFIELD", key)).await?; + Ok(field.into_iter().collect()) } - #[doc = " A random map of fields and values from the hash value stored at key up to the count."] + #[doc = " Random fields and their values from the hash value stored at key, up to the count."] #[doc = ""] - #[doc = " Also see `hrandfield` and `hrandfield-count`."] + #[doc = " A negative count may return the same field more than once, its absolute value is the number of"] + #[doc = " fields returned."] + #[doc = ""] + #[doc = " Also see `hrandfield`."] #[doc = ""] #[doc = " HRANDFIELD key count WITHVALUES"] #[doc = " https://valkey.io/commands/hrandfield/"] #[allow(async_fn_in_trait)] - async fn hrandfield_count_withvalues( + async fn hrandfield_withvalues( connection: &Connection, key: String, count: Integer, - ) -> Result, Error> { + ) -> Result, Error> { query(connection, ("HRANDFIELD", key, count, "WITHVALUES")).await } @@ -325,7 +325,7 @@ impl Guest for Commands { cursor: Option, match_: Option, count: Option, - ) -> Result<(Cursor, Vec), Error> { + ) -> Result<(Cursor, Map), Error> { query( connection, ( @@ -352,7 +352,7 @@ impl Guest for Commands { cursor: Option, match_: Option, count: Option, - ) -> Result<(Cursor, Map), Error> { + ) -> Result<(Cursor, Vec), Error> { query( connection, ( @@ -878,6 +878,8 @@ impl Guest for Commands { #[doc = " Set key to hold the string value."] #[doc = ""] + #[doc = " Returns false when the precondition is not met, the key is unchanged."] + #[doc = ""] #[doc = " SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ]"] #[doc = " https://valkey.io/commands/set/"] #[allow(async_fn_in_trait)] @@ -887,8 +889,10 @@ impl Guest for Commands { value: String, precondition: Option, ttl: Option, - ) -> Result<(), Error> { - query(connection, ("SET", key, value, precondition, ttl)).await + ) -> Result { + // OK once set, null when the precondition is not met + let reply: Option<()> = query(connection, ("SET", key, value, precondition, ttl)).await?; + Ok(reply.is_some()) } #[doc = " Set key to hold the string value. Returning the previous value."] @@ -960,8 +964,8 @@ impl Guest for Commands { async fn substr( connection: &Connection, key: String, - start: i32, - end: i32, + start: Integer, + end: Integer, ) -> Result { query(connection, ("SUBSTR", key, start, end)).await } @@ -1149,7 +1153,7 @@ impl Guest for Commands { group: String, id: StreamId, mkstream: bool, - entriesread: Option, + entriesread: Option, ) -> Result<(), Error> { query( connection, @@ -1221,7 +1225,7 @@ impl Guest for Commands { key: String, group: String, id: StreamId, - entriesread: Option, + entriesread: Option, ) -> Result<(), Error> { query( connection, @@ -1309,7 +1313,7 @@ impl Guest for Commands { connection: &Connection, key: String, group: String, - ) -> Result<(Integer, StreamId, StreamId, Map), Error> { + ) -> Result { query(connection, ("XPENDING", key, group)).await } @@ -1329,7 +1333,7 @@ impl Guest for Commands { end: StreamId, count: Integer, consumer: Option, - ) -> Result, Error> { + ) -> Result, Error> { query( connection, ( @@ -1382,7 +1386,7 @@ impl Guest for Commands { ( "XREAD", NamedOption("COUNT", count), - NamedOption("BLOCK", block), + NamedOption("BLOCK", block.map(Milliseconds)), "STREAMS", keys, ids, @@ -1393,6 +1397,9 @@ impl Guest for Commands { #[doc = " Special version of the XREAD command with support for consumer groups."] #[doc = ""] + #[doc = " The fields of an entry are none when it was deleted after it was delivered, e.g. when reading"] + #[doc = " the pending entries with the ID `0`."] + #[doc = ""] #[doc = " XREADGROUP GROUP group consumer [ COUNT count ] [ BLOCK milliseconds ] [ NOACK ] STREAMS key [ key ... ] ID [ ID ... ]"] #[doc = " https://valkey.io/commands/xreadgroup/"] #[allow(async_fn_in_trait)] @@ -1404,8 +1411,8 @@ impl Guest for Commands { block: Option, noack: bool, keys: Vec, - ids: Vec, - ) -> Result)>>>, Error> { + ids: Vec, + ) -> Result>)>>>, Error> { query( connection, ( @@ -1414,7 +1421,8 @@ impl Guest for Commands { group, consumer, NamedOption("COUNT", count), - NamedOption("BLOCK", block), + NamedOption("BLOCK", block.map(Milliseconds)), + Flag("NOACK", noack), "STREAMS", keys, ids, diff --git a/components/commands/src/value.rs b/components/commands/src/value.rs index ba25837..daafd20 100644 --- a/components/commands/src/value.rs +++ b/components/commands/src/value.rs @@ -1,7 +1,8 @@ use crate::exports::componentized::valkey::commands::{ Double, Duration, Error, Instant, Integer, LcsIdxMatch, LcsIdxReply, StreamId, TtlResult, Value, XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, - XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, + XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, XpendingExtendedEntry, + XpendingSummary, }; use resp_codec::NestedValue; @@ -38,10 +39,14 @@ impl FromValue for Integer { } } +/// A double, or a bulk string holding one, e.g. the reply of INCRBYFLOAT, even with RESP3. impl FromValue for Double { fn from_value(value: Value) -> Result { match value { Value::Double(value) => Ok(value), + Value::BulkString(text) => text + .parse() + .map_err(|_| Error::Client(format!("unexpected reply, not a double: {text:?}"))), value => Err(unexpected(&value)), } } @@ -221,6 +226,47 @@ impl FromValue for OrZeroId { } } +/// `[count, lowest-id, highest-id, [[consumer, count]...]]`, the IDs and consumers are null when +/// nothing is pending, and each consumer's count is a bulk string. +impl FromValue for XpendingSummary { + fn from_value(value: Value) -> Result { + let (count, lowest_id, highest_id, consumers): (_, _, _, Option>) = + FromValue::from_value(value)?; + let consumers = consumers + .unwrap_or_default() + .into_iter() + .map(|(consumer, count)| { + let count = count.parse().map_err(|_| { + Error::Client(format!( + "unexpected pending count, not an integer: {count:?}" + )) + })?; + Ok((consumer, count)) + }) + .collect::>()?; + Ok(Self { + count, + lowest_id, + highest_id, + consumers, + }) + } +} + +/// `[id, consumer, milliseconds-since-delivery, delivery-count]`. +impl FromValue for XpendingExtendedEntry { + fn from_value(value: Value) -> Result { + let (id, name, since_delivery, delivery_count): (_, _, Milliseconds, _) = + FromValue::from_value(value)?; + Ok(Self { + id, + name, + since_delivery: since_delivery.0, + delivery_count, + }) + } +} + impl FromValue for TtlResult { fn from_value(value: Value) -> Result { match Integer::from_value(value)? { @@ -283,6 +329,7 @@ impl FromValue for XinfoGroup { Ok(Self { name: fields.required("name")?, consumers: fields.required("consumers")?, + pending: fields.required("pending")?, last_delivered_id: fields.required("last-delivered-id")?, entries_read: fields.required("entries-read")?, lag: fields.required("lag")?, @@ -298,8 +345,8 @@ impl FromValue for XinfoStreamReply { radix_tree_keys: fields.required("radix-tree-keys")?, radix_tree_nodes: fields.required("radix-tree-nodes")?, groups: fields.required("groups")?, - last_generated_id: fields.required("last-generated-id")?, - max_deleted_entry_id: fields.required("max-deleted-entry-id")?, + last_generated_id: fields.required::("last-generated-id")?.0, + max_deleted_entry_id: fields.required::("max-deleted-entry-id")?.0, entries_added: fields.required("entries-added")?, first_entry: fields.required("first-entry")?, last_entry: fields.required("last-entry")?, @@ -314,8 +361,8 @@ impl FromValue for XinfoStreamReplyFull { length: fields.required("length")?, radix_tree_keys: fields.required("radix-tree-keys")?, radix_tree_nodes: fields.required("radix-tree-nodes")?, - last_generated_id: fields.required("last-generated-id")?, - max_deleted_entry_id: fields.required("max-deleted-entry-id")?, + last_generated_id: fields.required::("last-generated-id")?.0, + max_deleted_entry_id: fields.required::("max-deleted-entry-id")?.0, entries_added: fields.required("entries-added")?, entries: fields.required("entries")?, groups: fields.required("groups")?, diff --git a/wit/commands.wit b/wit/commands.wit index 57a9fa4..7f9dd1b 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -402,29 +402,27 @@ interface commands { /// https://valkey.io/commands/hmset/ hmset: async func(connection: borrow, key: string, field-and-values: list>) -> result<_, error>; - /// A random field from the hash value stored at key. + /// Random fields from the hash value stored at key, up to the count. /// - /// Also see `hrandfield-count` and `hrandfield-count-withvalues`. + /// Without a count, one field, or none when the key does not exist. A negative count may return + /// the same field more than once, its absolute value is the number of fields returned. /// - /// HRANDFIELD key - /// https://valkey.io/commands/hrandfield/ - hrandfield: async func(connection: borrow, key: string) -> result, error>; - - /// A random set of fields from the hash value stored at key up to the count. + /// Also see `hrandfield-withvalues`. /// - /// Also see `hrandfield` and `hrandfield-count-withvalues`. - /// - /// HRANDFIELD key count + /// HRANDFIELD key [ count ] /// https://valkey.io/commands/hrandfield/ - hrandfield-count: async func(connection: borrow, key: string, count: integer) -> result, error>; + hrandfield: async func(connection: borrow, key: string, count: option) -> result, error>; - /// A random map of fields and values from the hash value stored at key up to the count. + /// Random fields and their values from the hash value stored at key, up to the count. + /// + /// A negative count may return the same field more than once, its absolute value is the number of + /// fields returned. /// - /// Also see `hrandfield` and `hrandfield-count`. + /// Also see `hrandfield`. /// /// HRANDFIELD key count WITHVALUES /// https://valkey.io/commands/hrandfield/ - hrandfield-count-withvalues: async func(connection: borrow, key: string, count: integer) -> result, error>; + hrandfield-withvalues: async func(connection: borrow, key: string, count: integer) -> result>, error>; /// The position of an incremental iteration, e.g. with HSCAN. /// @@ -437,7 +435,7 @@ interface commands { /// /// HSCAN key cursor [ MATCH pattern ] [ COUNT count ] /// https://valkey.io/commands/hscan/ - hscan: async func(connection: borrow, key: string, cursor: option, match: option, count: option) -> result>, error>; + hscan: async func(connection: borrow, key: string, cursor: option, match: option, count: option) -> result>, error>; /// Incrementally iterate over a collection of keys. /// @@ -445,7 +443,7 @@ interface commands { /// /// HSCAN key cursor [ MATCH pattern ] [ COUNT count ] NOVALUES /// https://valkey.io/commands/hscan/ - hscan-novalues: async func(connection: borrow, key: string, cursor: option, match: option, count: option) -> result>, error>; + hscan-novalues: async func(connection: borrow, key: string, cursor: option, match: option, count: option) -> result>, error>; /// Sets the specified fields to their respective values in the hash stored at key. /// @@ -884,7 +882,7 @@ interface commands { /// /// XGROUP CREATE key group 〈 id | $ 〉 [ MKSTREAM ] [ ENTRIESREAD entriesread ] /// https://valkey.io/commands/xgroup-create/ - xgroup-create: async func(connection: borrow, key: string, group: string, id: stream-id, mkstream: bool, entriesread: option) -> result<_, error>; + xgroup-create: async func(connection: borrow, key: string, group: string, id: stream-id, mkstream: bool, entriesread: option) -> result<_, error>; /// Create a consumer named in the consumer group of the stream that's stored at . /// @@ -908,7 +906,7 @@ interface commands { /// /// XGROUP SETID key group 〈 id | $ 〉 [ ENTRIESREAD entriesread ] /// https://valkey.io/commands/xgroup-setid/ - xgroup-setid: async func(connection: borrow, key: string, group: string, id: stream-id, entriesread: option) -> result<_, error>; + xgroup-setid: async func(connection: borrow, key: string, group: string, id: stream-id, entriesread: option) -> result<_, error>; /// A consumer of a group, see `xinfo-consumers`. record xinfo-consumer { @@ -936,6 +934,8 @@ interface commands { name: string, /// The number of consumers in the group. consumers: integer, + /// The number of entries in the group's pending entries list (PEL), delivered but not acknowledged. + pending: integer, /// The ID of the last entry delivered to the group's consumers. last-delivered-id: stream-id, /// The logical read counter of the last entry delivered to the group's consumers, none when unknown. @@ -961,10 +961,10 @@ interface commands { radix-tree-nodes: integer, /// The number of consumer groups of the stream. groups: integer, - /// The ID of the last entry added to the stream. - last-generated-id: stream-id, - /// The highest ID deleted from the stream. - max-deleted-entry-id: stream-id, + /// The ID of the last entry added to the stream, none when no entry was ever added. + last-generated-id: option, + /// The highest ID deleted from the stream, none when no entry was ever deleted. + max-deleted-entry-id: option, /// The number of entries added to the stream over its lifetime. entries-added: integer, /// The ID and fields of the first entry. @@ -991,10 +991,10 @@ interface commands { radix-tree-keys: integer, /// The number of nodes in the radix tree storing the stream. radix-tree-nodes: integer, - /// The ID of the last entry added to the stream. - last-generated-id: stream-id, - /// The highest ID deleted from the stream. - max-deleted-entry-id: stream-id, + /// The ID of the last entry added to the stream, none when no entry was ever added. + last-generated-id: option, + /// The highest ID deleted from the stream, none when no entry was ever deleted. + max-deleted-entry-id: option, /// The number of entries added to the stream over its lifetime. entries-added: integer, /// The ID and fields of the entries, in ascending ID order, up to the count given. @@ -1053,13 +1053,37 @@ interface commands { /// https://valkey.io/commands/xlen/ xlen: async func(connection: borrow, key: string) -> result; + /// The pending entries of a consumer group, see `xpending`. + record xpending-summary { + /// The number of pending entries. + count: integer, + /// The lowest ID of the pending entries, none when there are none. + lowest-id: option, + /// The highest ID of the pending entries, none when there are none. + highest-id: option, + /// Each consumer with pending entries, and how many it has. + consumers: list>, + } + /// List of pending messages. /// /// See also `xpending-extended`. /// /// XPENDING key group /// https://valkey.io/commands/xpending/ - xpending: async func(connection: borrow, key: string, group: string) -> result>, error>; + xpending: async func(connection: borrow, key: string, group: string) -> result; + + /// The pending entries of a consumer group, see `xpending-extended`. + record xpending-extended-entry { + /// The ID of the message. + id: stream-id, + /// The name of the consumer that fetched the message and has still to acknowledge it. We call it the current owner of the message. + name: string, + /// The number of milliseconds that elapsed since the last time this message was delivered to this consumer. + since-delivery: duration, + /// The number of times this message was delivered. + delivery-count: integer, + } /// List of pending messages. /// @@ -1067,7 +1091,7 @@ interface commands { /// /// XPENDING key group [ IDLE min-idle-time ] start end count [ consumer ] /// https://valkey.io/commands/xpending/ - xpending-extended: async func(connection: borrow, key: string, group: string, min-idle-time: option, start: stream-id, end: stream-id, count: integer, consumer: option) -> result>, error>; + xpending-extended: async func(connection: borrow, key: string, group: string, min-idle-time: option, start: stream-id, end: stream-id, count: integer, consumer: option) -> result, error>; /// Stream entries matching a given range of IDs. /// @@ -1083,9 +1107,12 @@ interface commands { /// Special version of the XREAD command with support for consumer groups. /// + /// The fields of an entry are none when it was deleted after it was delivered, e.g. when reading + /// the pending entries with the ID `0`. + /// /// XREADGROUP GROUP group consumer [ COUNT count ] [ BLOCK milliseconds ] [ NOACK ] STREAMS key [ key ... ] ID [ ID ... ] /// https://valkey.io/commands/xreadgroup/ - xreadgroup: async func(connection: borrow, group: string, consumer: string, count: option, block: option, noack: bool, keys: list, ids: list) -> result>>>>, error>; + xreadgroup: async func(connection: borrow, group: string, consumer: string, count: option, block: option, noack: bool, keys: list, ids: list) -> result>>>>>, error>; /// XRANGE, but returning the entries in reverse order. /// @@ -1307,9 +1334,11 @@ interface commands { /// Set key to hold the string value. /// + /// Returns false when the precondition is not met, the key is unchanged. + /// /// SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] /// https://valkey.io/commands/set/ - set: async func(connection: borrow, key: string, value: string, precondition: option, ttl: option) -> result<_, error>; + set: async func(connection: borrow, key: string, value: string, precondition: option, ttl: option) -> result; /// Set key to hold the string value. Returning the previous value. /// @@ -1345,7 +1374,7 @@ interface commands { /// /// SUBSTR key start end /// https://valkey.io/commands/substr/ - substr: async func(connection: borrow, key: string, start: s32, end: s32) -> result; + substr: async func(connection: borrow, key: string, start: integer, end: integer) -> result; // transaction commands From c053a034458896ad9040f95ea17a635486339ef6 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Mon, 12 Oct 2026 14:24:01 -0400 Subject: [PATCH 05/37] transtion commands Signed-off-by: Scott Andrews --- components/commands/src/command.rs | 24 ++-- components/commands/src/lib.rs | 14 +-- components/connection/README.md | 2 +- components/connection/src/lib.rs | 134 ++++++++++++++++++---- components/connection/tests/connection.rs | 30 ++--- components/dep-cli/src/lib.rs | 14 +-- components/tcp/tests/tcp.rs | 22 ++-- crates/test-harness/src/lib.rs | 7 +- wit/commands.wit | 10 -- wit/connection.wit | 54 +++++++-- 10 files changed, 213 insertions(+), 98 deletions(-) diff --git a/components/commands/src/command.rs b/components/commands/src/command.rs index e394fb2..cf6e005 100644 --- a/components/commands/src/command.rs +++ b/components/commands/src/command.rs @@ -7,32 +7,24 @@ use crate::value::{FromValue, OrZeroId, ZERO_ID}; /// Send a command, its words as bulk strings, and wait for the reply. /// -/// An error reply fails the command, see `connection.call`. -async fn call(connection: &Connection, command: impl IntoCommand) -> Result { - connection.call(bulk_strings(command)).await +/// An error reply fails the command, see `connection.exchange`. +async fn exchange(connection: &Connection, command: impl IntoCommand) -> Result { + connection.exchange(command.into_command()).await } /// Send a command, its words as bulk strings, without waiting for a reply. /// -/// An error reply fails the command, see `connection.send`. -pub(crate) async fn send(connection: &Connection, command: impl IntoCommand) -> Result<(), Error> { - connection.send(bulk_strings(command)).await +/// An error reply fails the command, see `connection.emit`. +pub(crate) async fn emit(connection: &Connection, command: impl IntoCommand) -> Result<(), Error> { + connection.emit(command.into_command()).await } -/// Send a command, see `call`, and convert its reply. +/// Send a command, see `exchange`, and convert its reply. pub(crate) async fn query( connection: &Connection, command: impl IntoCommand, ) -> Result { - T::from_value(call(connection, command).await?) -} - -fn bulk_strings(command: impl IntoCommand) -> Vec { - command - .into_command() - .into_iter() - .map(Value::BulkString) - .collect() + T::from_value(exchange(connection, command).await?) } /// Converts a Rust type into the words of a command, e.g. `Option` into no word, or one. diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index f3eebee..0471820 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -1,4 +1,4 @@ -use crate::command::{Flag, Milliseconds, NamedOption, Seconds, query, send}; +use crate::command::{Flag, Milliseconds, NamedOption, Seconds, emit, query}; use crate::exports::componentized::valkey::commands::{ Connection, Cursor, Double, Duration, Error, GetexTtl, Guest, Instant, Integer, LcsIdxReply, MsetexPrecondition, MsetexTtl, Rule, SetPrecondition, SetTtl, StreamId, StreamTrim, TtlMode, @@ -477,7 +477,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/psubscribe/"] #[allow(async_fn_in_trait)] async fn psubscribe(connection: &Connection, patterns: Vec) -> Result<(), Error> { - send(connection, ("PSUBSCRIBE", patterns)).await + emit(connection, ("PSUBSCRIBE", patterns)).await } #[doc = " Posts a message to the given channel."] @@ -556,7 +556,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/punsubscribe/"] #[allow(async_fn_in_trait)] async fn punsubscribe(connection: &Connection, patterns: Vec) -> Result<(), Error> { - send(connection, ("PUNSUBSCRIBE", patterns)).await + emit(connection, ("PUNSUBSCRIBE", patterns)).await } #[doc = " Posts a message to the given shard channel."] @@ -578,7 +578,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/ssubscribe/"] #[allow(async_fn_in_trait)] async fn ssubscribe(connection: &Connection, shardchannels: Vec) -> Result<(), Error> { - send(connection, ("SSUBSCRIBE", shardchannels)).await + emit(connection, ("SSUBSCRIBE", shardchannels)).await } #[doc = " Subscribes the client to the specified channels."] @@ -587,7 +587,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/subscribe/"] #[allow(async_fn_in_trait)] async fn subscribe(connection: &Connection, channels: Vec) -> Result<(), Error> { - send(connection, ("SUBSCRIBE", channels)).await + emit(connection, ("SUBSCRIBE", channels)).await } #[doc = " Unsubscribes the client from the given shard channels, or from all of them if none is given."] @@ -599,7 +599,7 @@ impl Guest for Commands { connection: &Connection, shardchannels: Vec, ) -> Result<(), Error> { - send(connection, ("SUNSUBSCRIBE", shardchannels)).await + emit(connection, ("SUNSUBSCRIBE", shardchannels)).await } #[doc = " Unsubscribes the client from the given channels, or from all of them if none is given."] @@ -608,7 +608,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/unsubscribe/"] #[allow(async_fn_in_trait)] async fn unsubscribe(connection: &Connection, channels: Vec) -> Result<(), Error> { - send(connection, ("UNSUBSCRIBE", channels)).await + emit(connection, ("UNSUBSCRIBE", channels)).await } #[doc = " Appends the value at the end of the string."] diff --git a/components/connection/README.md b/components/connection/README.md index 3dd104d..781d1a3 100644 --- a/components/connection/README.md +++ b/components/connection/README.md @@ -2,4 +2,4 @@ The mechanics of a connection to a Valkey server, independent of the transport carrying it. -Pairs each command with its reply, keeps push messages apart for any number of readers, establishes the connection with `HELLO 3` and optionally keeps it alive with `PING`. A transport, e.g. [`tcp`](../tcp/), carries the RESP values to and from the server. +Pairs each command with its reply, keeps push messages apart for any number of readers, runs transactions with `WATCH`, `MULTI` and `EXEC`, establishes the connection with `HELLO 3` and optionally keeps it alive with `PING`. A transport, e.g. [`tcp`](../tcp/), carries the RESP values to and from the server. diff --git a/components/connection/src/lib.rs b/components/connection/src/lib.rs index dded97b..772554e 100644 --- a/components/connection/src/lib.rs +++ b/components/connection/src/lib.rs @@ -123,12 +123,29 @@ impl GuestConnection for ConnectionImpl { self.0.state.borrow().server_properties.clone() } - async fn call(&self, command: Vec) -> Result { - call(&self.0, command).await + async fn exchange(&self, command: Vec) -> Result { + exchange(&self.0, command).await } - async fn send(&self, command: Vec) -> Result<(), Error> { - send(&self.0, command).await + async fn exchange_all(&self, commands: Vec>) -> Vec> { + exchange_all(&self.0, commands).await + } + + async fn emit(&self, command: Vec) -> Result<(), Error> { + emit(&self.0, command).await + } + + async fn watch(&self, keys: Vec) -> Result<(), Error> { + let command = std::iter::once("WATCH".to_string()).chain(keys).collect(); + ok(exchange(&self.0, command).await?) + } + + async fn unwatch(&self) -> Result<(), Error> { + ok(exchange(&self.0, words(&["UNWATCH"])).await?) + } + + async fn transaction(&self, commands: Vec>) -> Result>, Error> { + transaction(&self.0, commands).await } fn pushes(&self) -> StreamReader { @@ -168,7 +185,7 @@ impl GuestConnection for ConnectionImpl { /// A command without a reply takes no place in the order. fn enqueue( inner: &Inner, - command: Vec, + command: Vec, reply: Option, written: Option>, ) -> Result<(), Error> { @@ -176,7 +193,13 @@ fn enqueue( if let Some(error) = &state.closed { return Err(error.clone()); } - let command = Value::Array(command.into_iter().map(resp_codec::encode).collect()); + // the words of a command are bulk strings + let command = Value::Array( + command + .into_iter() + .map(|word| resp_codec::encode(Value::BulkString(word))) + .collect(), + ); inner .requests .unbounded_send(Request { command, written }) @@ -185,9 +208,9 @@ fn enqueue( Ok(()) } -async fn call(inner: &Inner, command: Vec) -> Result { +async fn exchange(inner: &Inner, command: Vec) -> Result { if replies_with_pushes(&command) { - return send(inner, command).await.map(|()| Value::Null); + return emit(inner, command).await.map(|()| Value::Null); } let (reply_tx, reply_rx) = oneshot::channel(); enqueue(inner, command, Some(Reply::Call(reply_tx)), None)?; @@ -197,7 +220,63 @@ async fn call(inner: &Inner, command: Vec) -> Result { .unwrap_or_else(|_| Err(closed_error(&inner.state))) } -async fn send(inner: &Inner, command: Vec) -> Result<(), Error> { +/// Queue every command before waiting for any reply, so no other command is sent between them. +async fn exchange_all(inner: &Inner, commands: Vec>) -> Vec> { + // no await until every command is queued + let queued: Vec>, Error>> = commands + .into_iter() + .map(|command| { + if replies_with_pushes(&command) { + // no reply, as for `exchange` + return enqueue(inner, command, None, None).map(|()| None); + } + let (reply_tx, reply_rx) = oneshot::channel(); + enqueue(inner, command, Some(Reply::Call(reply_tx)), None).map(|()| Some(reply_rx)) + }) + .collect(); + let mut replies = Vec::with_capacity(queued.len()); + for reply in queued { + replies.push(match reply { + Err(error) => Err(error), + Ok(None) => Ok(Value::Null), + // the reply is dropped unsent when the connection closes, the reader fails it first + Ok(Some(reply)) => reply + .await + .unwrap_or_else(|_| Err(closed_error(&inner.state))), + }); + } + replies +} + +/// MULTI, the commands, then EXEC, with no other command between them. +async fn transaction( + inner: &Inner, + commands: Vec>, +) -> Result>, Error> { + let batch = std::iter::once(words(&["MULTI"])) + .chain(commands) + .chain(std::iter::once(words(&["EXEC"]))) + .collect(); + let mut replies = exchange_all(inner, batch).await; + let exec = replies.pop().expect("a reply for each command"); + // MULTI's OK and each command's QUEUED, the first command rejected while queuing is a clearer + // error than EXEC's EXECABORT + for reply in replies { + reply?; + } + match exec? { + // a watched key changed + Value::Null => Ok(None), + Value::Array(replies) => replies + .into_iter() + .map(|reply| resp_codec::decode(reply).map_err(Error::Resp)) + .collect::>() + .map(Some), + reply => Err(unexpected(&reply)), + } +} + +async fn emit(inner: &Inner, command: Vec) -> Result<(), Error> { let reply = (!replies_with_pushes(&command)).then_some(Reply::Discard); let (written_tx, written_rx) = oneshot::channel(); enqueue(inner, command, reply, Some(written_tx))?; @@ -210,7 +289,7 @@ async fn send(inner: &Inner, command: Vec) -> Result<(), Error> { /// In RESP3 the server confirms each channel subscribed to, or unsubscribed from, with a push /// message. An error, e.g. for a command missing its channels, is a reply the server sends /// unprompted. -fn replies_with_pushes(command: &[Value]) -> bool { +fn replies_with_pushes(command: &[String]) -> bool { const COMMANDS: [&str; 6] = [ "SUBSCRIBE", "PSUBSCRIBE", @@ -219,12 +298,11 @@ fn replies_with_pushes(command: &[Value]) -> bool { "PUNSUBSCRIBE", "SUNSUBSCRIBE", ]; - match command.first() { - Some(Value::BulkString(name) | Value::String(name)) => COMMANDS + command.first().is_some_and(|name| { + COMMANDS .iter() - .any(|command| name.eq_ignore_ascii_case(command)), - _ => false, - } + .any(|command| name.eq_ignore_ascii_case(command)) + }) } /// The error a command fails with once the connection closed. @@ -250,8 +328,8 @@ async fn next(stream: &mut S) -> Option { poll_fn(|cx| Pin::new(&mut *stream).poll_next(cx)).await } -fn bulk(value: &str) -> Value { - Value::BulkString(value.to_string()) +fn words(words: &[&str]) -> Vec { + words.iter().map(|word| word.to_string()).collect() } /// Write each queued command to the requests stream, closing it once the connection is dropped. @@ -346,7 +424,7 @@ async fn keep_alive(inner: Weak, generation: u64, interval: Duration) { state.activity == seen && state.pending.is_empty() }; if idle { - if let Err(error) = call(&inner, vec![bulk("PING")]).await { + if let Err(error) = exchange(&inner, words(&["PING"])).await { close(&inner.state, error); inner.requests.close_channel(); return; @@ -373,15 +451,15 @@ async fn establish( None => (None, None, None), }; - let mut hello = vec![bulk("HELLO"), bulk("3")]; + let mut hello = words(&["HELLO", "3"]); if let Some((username, password)) = auth { - hello.extend([bulk("AUTH"), bulk(&username), bulk(&password)]); + hello.extend(["AUTH".to_string(), username, password]); } if let Some(client_name) = client_name { - hello.extend([bulk("SETNAME"), bulk(&client_name)]); + hello.extend(["SETNAME".to_string(), client_name]); } - let result = match call(&inner, hello).await.and_then(server_properties) { + let result = match exchange(&inner, hello).await.and_then(server_properties) { Ok(properties) => { inner.state.borrow_mut().server_properties = properties; let established = ConnectionImpl(inner); @@ -423,6 +501,18 @@ fn decode(value: NestedValue) -> Result { } /// An error value from the server fails the command. +/// A simple string `OK`. +fn ok(reply: Value) -> Result<(), Error> { + match reply { + Value::String(text) if text == "OK" => Ok(()), + reply => Err(unexpected(&reply)), + } +} + +fn unexpected(reply: &Value) -> Error { + Error::Client(format!("unexpected reply: {reply:?}")) +} + fn reply_result(value: Value) -> Result { match value { Value::Error(message) | Value::BulkError(message) => Err(Error::Valkey(message)), diff --git a/components/connection/tests/connection.rs b/components/connection/tests/connection.rs index 706872f..a9394ee 100644 --- a/components/connection/tests/connection.rs +++ b/components/connection/tests/connection.rs @@ -4,7 +4,7 @@ use std::time::Duration; use test_harness::{ ConnectOpts, Error, Harness, Value, bulk, command, connect, establish, hello_reply, push, - receive, resolve, + receive, resolve, words, }; /// Long enough for the connection to act on what it was given, short enough to keep tests quick. @@ -95,14 +95,14 @@ async fn closing_before_hello_fails_to_establish() -> wasmtime::Result<()> { } #[tokio::test(flavor = "multi_thread")] -async fn calls_are_paired_with_replies_in_order() -> wasmtime::Result<()> { +async fn exchanges_are_paired_with_replies_in_order() -> wasmtime::Result<()> { let mut harness = Harness::new("connection").await?; harness .run(async |accessor, exports| { let guest = exports.connection(); let (mut transport, connection) = establish(accessor, exports).await?; - let first = guest.call_call(accessor, connection, command(&["GET", "a"])); - let second = guest.call_call(accessor, connection, command(&["GET", "b"])); + let first = guest.call_exchange(accessor, connection, words(&["GET", "a"])); + let second = guest.call_exchange(accessor, connection, words(&["GET", "b"])); let server = async { // both are in flight before either is answered assert_eq!(transport.request().await, Some(command(&["GET", "a"]))); @@ -119,13 +119,13 @@ async fn calls_are_paired_with_replies_in_order() -> wasmtime::Result<()> { } #[tokio::test(flavor = "multi_thread")] -async fn error_replies_fail_the_call() -> wasmtime::Result<()> { +async fn error_replies_fail_the_exchange() -> wasmtime::Result<()> { let mut harness = Harness::new("connection").await?; harness .run(async |accessor, exports| { let guest = exports.connection(); let (mut transport, connection) = establish(accessor, exports).await?; - let call = guest.call_call(accessor, connection, command(&["INCR", "text"])); + let call = guest.call_exchange(accessor, connection, words(&["INCR", "text"])); let server = async { transport.request().await; transport.reply(Value::Error("ERR value is not an integer".to_string())); @@ -141,16 +141,16 @@ async fn error_replies_fail_the_call() -> wasmtime::Result<()> { } #[tokio::test(flavor = "multi_thread")] -async fn send_discards_its_reply() -> wasmtime::Result<()> { +async fn emit_discards_its_reply() -> wasmtime::Result<()> { let mut harness = Harness::new("connection").await?; harness .run(async |accessor, exports| { let guest = exports.connection(); let (mut transport, connection) = establish(accessor, exports).await?; guest - .call_send(accessor, connection, command(&["SET", "a", "1"])) + .call_emit(accessor, connection, words(&["SET", "a", "1"])) .await??; - let call = guest.call_call(accessor, connection, command(&["GET", "a"])); + let call = guest.call_exchange(accessor, connection, words(&["GET", "a"])); let server = async { assert_eq!(transport.request().await, Some(command(&["SET", "a", "1"]))); assert_eq!(transport.request().await, Some(command(&["GET", "a"]))); @@ -178,7 +178,7 @@ async fn push_messages_go_to_every_reader() -> wasmtime::Result<()> { let mut second = receive(accessor, second)?; let message = push(vec![bulk("message"), bulk("news"), bulk("hello")]); - let call = guest.call_call(accessor, connection, command(&["PING"])); + let call = guest.call_exchange(accessor, connection, words(&["PING"])); let server = async { transport.request().await; // a push message arriving ahead of a reply is not mistaken for it @@ -204,7 +204,7 @@ async fn unprompted_values_go_to_push_readers() -> wasmtime::Result<()> { let pushes = guest.call_pushes(accessor, connection).await?; let mut pushes = receive(accessor, pushes)?; - let call = guest.call_call(accessor, connection, command(&["MONITOR"])); + let call = guest.call_exchange(accessor, connection, words(&["MONITOR"])); let server = async { transport.request().await; transport.reply(Value::String("OK".to_string())); @@ -238,7 +238,7 @@ async fn slow_push_readers_are_closed() -> wasmtime::Result<()> { ])); } // replies are not held up behind the reader - let call = guest.call_call(accessor, connection, command(&["PING"])); + let call = guest.call_exchange(accessor, connection, words(&["PING"])); let server = async { transport.request().await; transport.reply(Value::String("PONG".to_string())); @@ -251,14 +251,14 @@ async fn slow_push_readers_are_closed() -> wasmtime::Result<()> { } #[tokio::test(flavor = "multi_thread")] -async fn closing_fails_waiting_and_later_calls() -> wasmtime::Result<()> { +async fn closing_fails_waiting_and_later_exchanges() -> wasmtime::Result<()> { let mut harness = Harness::new("connection").await?; harness .run(async |accessor, exports| { let guest = exports.connection(); let (mut transport, connection) = establish(accessor, exports).await?; let reset = Error::Closed(Some("connection reset".to_string())); - let call = guest.call_call(accessor, connection, command(&["GET", "a"])); + let call = guest.call_exchange(accessor, connection, words(&["GET", "a"])); let server = async { transport.request().await; transport.close(Err(reset.clone())); @@ -266,7 +266,7 @@ async fn closing_fails_waiting_and_later_calls() -> wasmtime::Result<()> { let (result, ()) = tokio::join!(call, server); assert_eq!(result?, Err(reset.clone())); let later = guest - .call_call(accessor, connection, command(&["GET", "a"])) + .call_exchange(accessor, connection, words(&["GET", "a"])) .await?; assert_eq!(later, Err(reset)); Ok(()) diff --git a/components/dep-cli/src/lib.rs b/components/dep-cli/src/lib.rs index 52a876c..6545225 100644 --- a/components/dep-cli/src/lib.rs +++ b/components/dep-cli/src/lib.rs @@ -69,24 +69,23 @@ async fn exec(command: Vec, out: &mut Output) -> Result<(), Error> { let mut pushes = connection.pushes(); let name = command[0].to_ascii_uppercase(); let channels = command.len() - 1; - let command = command.into_iter().map(Value::BulkString).collect(); if SUBSCRIBE.contains(&name.as_str()) { - connection.send(command).await?; + connection.emit(command).await?; print_pushes(&mut pushes, out, None).await } else if UNSUBSCRIBE.contains(&name.as_str()) { // a push message for each channel, or one when no channel is given, as a new connection is // not subscribed to any - connection.send(command).await?; + connection.emit(command).await?; print_pushes(&mut pushes, out, Some(channels.max(1))).await } else if name == "MONITOR" { // the feed follows the reply, values sent unprompted that the connection treats as push // messages - let reply = connection.call(command).await?; + let reply = connection.exchange(command).await?; out.line(&render(reply)).await; print_pushes(&mut pushes, out, None).await } else { - let reply = connection.call(command).await?; + let reply = connection.exchange(command).await?; out.line(&render(reply)).await; Ok(()) } @@ -168,13 +167,12 @@ async fn send_lines( let Some(name) = words.first().map(|name| name.to_ascii_uppercase()) else { continue; }; - let command = words.into_iter().map(Value::BulkString).collect(); if SUBSCRIBE.contains(&name.as_str()) || UNSUBSCRIBE.contains(&name.as_str()) { // no reply, the push messages confirming it are printed as they arrive - connection.send(command).await?; + connection.emit(words).await?; continue; } - let text = match connection.call(command).await { + let text = match connection.exchange(words).await { Ok(reply) => render(reply), // the command failed, not the session Err(Error::Valkey(message)) => format!("(error) {message}"), diff --git a/components/tcp/tests/tcp.rs b/components/tcp/tests/tcp.rs index bfd9356..a30c3e0 100644 --- a/components/tcp/tests/tcp.rs +++ b/components/tcp/tests/tcp.rs @@ -2,7 +2,7 @@ //! server played by the test over TCP. use test_harness::{ - Client, Ctx, Error, Exports, Harness, Server, Value, bulk, command, hello_reply, resolve, + Client, Ctx, Error, Exports, Harness, Server, Value, bulk, command, hello_reply, resolve, words, }; use wasmtime::component::{Accessor, ResourceAny}; @@ -28,7 +28,7 @@ async fn establish( } #[tokio::test(flavor = "multi_thread")] -async fn connects_and_calls() -> wasmtime::Result<()> { +async fn connects_and_exchanges() -> wasmtime::Result<()> { let server = Server::bind().await?; let mut harness = Harness::new("test-tcp").await?; harness @@ -36,7 +36,7 @@ async fn connects_and_calls() -> wasmtime::Result<()> { let (mut client, connection) = establish(accessor, exports, &server, "127.0.0.1").await?; let guest = exports.connection(); - let call = guest.call_call(accessor, connection, command(&["PING"])); + let call = guest.call_exchange(accessor, connection, words(&["PING"])); let serve = async { assert_eq!(client.request().await, Some(command(&["PING"]))); client.reply(Value::String("PONG".to_string())).await; @@ -73,7 +73,7 @@ async fn replies_may_span_many_reads() -> wasmtime::Result<()> { .map(|n| char::from(b'a' + (n % 26) as u8)) .collect(); let guest = exports.connection(); - let call = guest.call_call(accessor, connection, command(&["GET", "big"])); + let call = guest.call_exchange(accessor, connection, words(&["GET", "big"])); let serve = async { client.request().await; let reply = test_harness::encode(bulk(&value)); @@ -98,8 +98,8 @@ async fn replies_may_share_a_read() -> wasmtime::Result<()> { let (mut client, connection) = establish(accessor, exports, &server, "127.0.0.1").await?; let guest = exports.connection(); - let first = guest.call_call(accessor, connection, command(&["GET", "a"])); - let second = guest.call_call(accessor, connection, command(&["GET", "b"])); + let first = guest.call_exchange(accessor, connection, words(&["GET", "a"])); + let second = guest.call_exchange(accessor, connection, words(&["GET", "b"])); let serve = async { client.request().await; client.request().await; @@ -116,7 +116,7 @@ async fn replies_may_share_a_read() -> wasmtime::Result<()> { } #[tokio::test(flavor = "multi_thread")] -async fn server_closing_fails_calls() -> wasmtime::Result<()> { +async fn server_closing_fails_exchanges() -> wasmtime::Result<()> { let server = Server::bind().await?; let mut harness = Harness::new("test-tcp").await?; harness @@ -125,7 +125,7 @@ async fn server_closing_fails_calls() -> wasmtime::Result<()> { client.close().await; let result = exports .connection() - .call_call(accessor, connection, command(&["PING"])) + .call_exchange(accessor, connection, words(&["PING"])) .await?; assert!( matches!(result, Err(Error::Closed(_))), @@ -242,15 +242,15 @@ mod with_valkey { .map(|n| char::from(b'a' + (n % 26) as u8)) .collect(); let set = guest - .call_call(accessor, connection, command(&["SET", "tcp-test", &value])) + .call_exchange(accessor, connection, words(&["SET", "tcp-test", &value])) .await?; assert_eq!(set, Ok(Value::String("OK".to_string()))); let get = guest - .call_call(accessor, connection, command(&["GET", "tcp-test"])) + .call_exchange(accessor, connection, words(&["GET", "tcp-test"])) .await?; assert_eq!(get, Ok(bulk(&value))); let del = guest - .call_call(accessor, connection, command(&["DEL", "tcp-test"])) + .call_exchange(accessor, connection, words(&["DEL", "tcp-test"])) .await?; assert_eq!(del, Ok(Value::Integer(1))); Ok(()) diff --git a/crates/test-harness/src/lib.rs b/crates/test-harness/src/lib.rs index fab16d6..1520f47 100644 --- a/crates/test-harness/src/lib.rs +++ b/crates/test-harness/src/lib.rs @@ -142,7 +142,12 @@ pub fn bulk(value: &str) -> Value { Value::BulkString(value.to_string()) } -/// A command, e.g. `command(&["GET", "key"])`. +/// The words of a command to send, e.g. `words(&["GET", "key"])`. +pub fn words(words: &[&str]) -> Vec { + words.iter().map(|word| word.to_string()).collect() +} + +/// A command as the server receives it, e.g. `command(&["GET", "key"])`. pub fn command(words: &[&str]) -> Vec { words.iter().map(|word| bulk(word)).collect() } diff --git a/wit/commands.wit b/wit/commands.wit index 7f9dd1b..c25e917 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -1376,14 +1376,4 @@ interface commands { /// https://valkey.io/commands/substr/ substr: async func(connection: borrow, key: string, start: integer, end: integer) -> result; - - // transaction commands - - // not yet implemented, from the @transaction category: - // DISCARD - // EXEC - // MULTI - // UNWATCH - // WATCH - } diff --git a/wit/connection.wit b/wit/connection.wit index 9a07790..23a7121 100644 --- a/wit/connection.wit +++ b/wit/connection.wit @@ -5,9 +5,9 @@ /// returns to the transport to send, then await the connection. See `tcp.connect` for a transport /// that does this for its caller. /// -/// The connection is only the mechanics of talking to a server. Commands are functions in the -/// command interfaces, e.g. `string-commands`, that borrow the connection, so command groups can be -/// implemented independently of each other. The groups follow Valkey's ACL categories. +/// The connection is the mechanics of talking to a server: sending commands and pairing replies +/// with them, push messages, and transactions. Higher level, typed operations are functions in the +/// `commands` interface that borrow the connection. interface connection { use wasi:clocks/system-clock@0.3.0.{duration, instant}; use resp.{error as resp-error, value, integer, double}; @@ -62,16 +62,56 @@ interface connection { /// the commands were sent. /// /// SUBSCRIBE, PSUBSCRIBE, SSUBSCRIBE and their UNSUBSCRIBE counterparts have no reply, the - /// server confirms them with push messages, see `pushes`. Their call resolves to null once - /// the command is written. - call: async func(command: list) -> result; + /// server confirms them with push messages, see `pushes`. Their exchange resolves to null + /// once the command is written. + exchange: async func(command: list) -> result; + + /// Sends the commands one after another, with no other command sent between them, and waits + /// for every reply. + /// + /// Each command has its own result, in order, as if each was sent with `exchange`. Commands + /// sharing a connection may otherwise be interleaved with other callers' commands, which + /// matters for a sequence that must not be interrupted, e.g. MULTI, its commands and EXEC. + exchange-all: async func(commands: list>) -> list>; /// Sends a command without waiting for its reply, resolving once the command is written. /// /// The server still replies, the reply is discarded when it arrives and an error it reports /// is not returned. An implementation may instead ask the server not to reply, e.g. with /// `CLIENT REPLY SKIP`. - send: async func(command: list) -> result<_, error>; + emit: async func(command: list) -> result<_, error>; + + /// Marks the keys to watch, EXEC aborts the next transaction when any of them changed. + /// + /// Watched keys belong to the connection, and EXEC, DISCARD or UNWATCH from any caller sharing + /// it clears them. Use a connection of your own for WATCH, the reads it guards, and the + /// transaction. + /// + /// WATCH key [ key ... ] + /// https://valkey.io/commands/watch/ + watch: async func(keys: list) -> result<_, error>; + + /// Forgets all the watched keys. + /// + /// UNWATCH + /// https://valkey.io/commands/unwatch/ + unwatch: async func() -> result<_, error>; + + /// Runs the commands as a transaction, MULTI, each command, then EXEC, with no other command + /// sent between them, see `exchange-all`. + /// + /// Returns none when a watched key changed and the transaction was aborted, otherwise the + /// reply of each command in order. A command that fails while running is an error value in + /// its place, the others still ran. A command rejected while queuing, e.g. an unknown + /// command, fails the transaction with that error, none of the commands ran. + /// + /// MULTI and EXEC are only sent as part of a transaction, the commands between them reply + /// QUEUED rather than their reply. + /// + /// MULTI, EXEC + /// https://valkey.io/commands/multi/ + /// https://valkey.io/commands/exec/ + transaction: async func(commands: list>) -> result>, error>; /// Push messages, e.g. from SUBSCRIBE or client tracking, kept apart from replies. /// From d14dacdd1b3a6326ca5afb84aaba35a6129c1ce3 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Mon, 12 Oct 2026 20:22:27 -0400 Subject: [PATCH 06/37] set and sortedset commands Signed-off-by: Scott Andrews --- components/commands/src/command.rs | 192 ++++- components/commands/src/lib.rs | 1220 +++++++++++++++++++++++++++- components/commands/src/value.rs | 37 + components/connection/src/lib.rs | 14 + wit/commands.wit | 639 +++++++++++++-- wit/connection.wit | 6 + 6 files changed, 2029 insertions(+), 79 deletions(-) diff --git a/components/commands/src/command.rs b/components/commands/src/command.rs index cf6e005..e344d19 100644 --- a/components/commands/src/command.rs +++ b/components/commands/src/command.rs @@ -1,7 +1,8 @@ use crate::exports::componentized::valkey::commands::{ - ChannelRule, CommandRule, Connection, DbRule, Duration, Error, GetexTtl, Instant, KeyRule, - MsetexPrecondition, Rule, RuleSelector, SetPrecondition, SetTtl, StreamTrim, - StreamTrimEfficiency, TtlMode, UserRule, Value, XclaimOpts, + Aggregate, ChannelRule, CommandRule, Connection, DbRule, Duration, Error, GetexTtl, Instant, + KeyRule, LexBound, Limit, MsetexPrecondition, PopEnd, Rule, RuleSelector, ScoreBound, + SetPrecondition, SetTtl, SortOpts, SortOrder, StreamTrim, StreamTrimEfficiency, TtlMode, + UserRule, Value, XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, }; use crate::value::{FromValue, OrZeroId, ZERO_ID}; @@ -405,3 +406,188 @@ impl IntoCommand for XclaimOpts { .into_command() } } + +/// A duration in seconds with a fraction, e.g. the timeout of BZPOPMIN. +pub struct DecimalSeconds(pub Duration); + +impl IntoCommand for DecimalSeconds { + fn into_command(self) -> Vec { + vec![(self.0 as f64 / 1_000_000_000.0).to_string()] + } +} + +/// A list the command requires at least one item of, e.g. the keys of DEL. +pub struct NonEmpty(Vec); + +impl NonEmpty { + /// Fails, before anything is sent, when there are no items. + pub fn new(name: &str, items: Vec) -> Result { + if items.is_empty() { + return Err(Error::Client(format!("{name} requires at least one item"))); + } + Ok(NonEmpty(items)) + } + + /// A list the command allows to be left out, none rather than empty. + pub fn optional(name: &str, items: Option>) -> Result, Error> { + items.map(|items| NonEmpty::new(name, items)).transpose() + } + + pub fn len(&self) -> usize { + self.0.len() + } +} + +impl IntoCommand for NonEmpty { + fn into_command(self) -> Vec { + self.0.into_command() + } +} + +/// The keys preceded by how many there are, e.g. for ZINTER. +pub struct NumKeys(pub NonEmpty); + +impl IntoCommand for NumKeys { + fn into_command(self) -> Vec { + (self.0.len(), self.0).into_command() + } +} + +impl IntoCommand for Limit { + fn into_command(self) -> Vec { + ("LIMIT", self.offset, self.count).into_command() + } +} + +/// The start and stop of a range of scores, as given, the start is the maximum with REV. +pub struct ScoreRange { + pub start: ScoreBound, + pub stop: ScoreBound, + pub rev: bool, +} + +impl IntoCommand for ScoreRange { + fn into_command(self) -> Vec { + let bound = |bound, unbounded: &str| match bound { + ScoreBound::Inclusive(score) => score.to_string(), + ScoreBound::Exclusive(score) => format!("({score}"), + ScoreBound::Unbounded => unbounded.to_string(), + }; + let (low, high) = ("-inf", "+inf"); + let (start, stop) = if self.rev { (high, low) } else { (low, high) }; + vec![bound(self.start, start), bound(self.stop, stop)] + } +} + +/// The start and stop of a range of members, as given, the start is the maximum with REV. +pub struct LexRange { + pub start: LexBound, + pub stop: LexBound, + pub rev: bool, +} + +impl IntoCommand for LexRange { + fn into_command(self) -> Vec { + let bound = |bound, unbounded: &str| match bound { + LexBound::Inclusive(member) => format!("[{member}"), + LexBound::Exclusive(member) => format!("({member}"), + LexBound::Unbounded => unbounded.to_string(), + }; + let (low, high) = ("-", "+"); + let (start, stop) = if self.rev { (high, low) } else { (low, high) }; + vec![bound(self.start, start), bound(self.stop, stop)] + } +} + +/// ZRANGE and ZRANGESTORE, `start stop [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ]`. +pub struct Zrange { + pub range: ZrangeBy, + pub rev: bool, + pub limit: Option, +} + +impl IntoCommand for Zrange { + fn into_command(self) -> Vec { + let Zrange { range, rev, limit } = self; + let (range, by) = match range { + ZrangeBy::Rank((start, stop)) => ((start, stop).into_command(), None), + ZrangeBy::Score((start, stop)) => ( + ScoreRange { start, stop, rev }.into_command(), + Some("BYSCORE"), + ), + ZrangeBy::Lex((start, stop)) => { + (LexRange { start, stop, rev }.into_command(), Some("BYLEX")) + } + }; + (range, by, Flag("REV", rev), limit).into_command() + } +} + +impl IntoCommand for Aggregate { + fn into_command(self) -> Vec { + let aggregate = match self { + Aggregate::Sum => "SUM", + Aggregate::Min => "MIN", + Aggregate::Max => "MAX", + }; + ("AGGREGATE", aggregate).into_command() + } +} + +impl IntoCommand for PopEnd { + fn into_command(self) -> Vec { + match self { + PopEnd::Min => "MIN", + PopEnd::Max => "MAX", + } + .into_command() + } +} + +impl IntoCommand for ZaddCondition { + fn into_command(self) -> Vec { + match self { + ZaddCondition::Nx => "NX", + ZaddCondition::Xx => "XX", + } + .into_command() + } +} + +impl IntoCommand for ZaddComparison { + fn into_command(self) -> Vec { + match self { + ZaddComparison::Gt => "GT", + ZaddComparison::Lt => "LT", + } + .into_command() + } +} + +impl IntoCommand for SortOrder { + fn into_command(self) -> Vec { + match self { + SortOrder::Asc => "ASC", + SortOrder::Desc => "DESC", + } + .into_command() + } +} + +/// `[ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ]` +impl IntoCommand for SortOpts { + fn into_command(self) -> Vec { + ( + NamedOption("BY", self.by), + self.limit, + self.get + .into_iter() + .flatten() + .map(|pattern| ("GET", pattern)) + .collect::>(), + self.order, + Flag("ALPHA", self.alpha), + ) + .into_command() + } +} diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index 0471820..e46a13d 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -1,11 +1,16 @@ -use crate::command::{Flag, Milliseconds, NamedOption, Seconds, emit, query}; +use crate::command::{ + DecimalSeconds, Flag, LexRange, Milliseconds, NamedOption, NonEmpty, NumKeys, ScoreRange, + Seconds, Zrange, emit, query, +}; use crate::exports::componentized::valkey::commands::{ - Connection, Cursor, Double, Duration, Error, GetexTtl, Guest, Instant, Integer, LcsIdxReply, - MsetexPrecondition, MsetexTtl, Rule, SetPrecondition, SetTtl, StreamId, StreamTrim, TtlMode, - TtlResult, Value, XclaimOpts, XinfoConsumer, XinfoGroup, XinfoStreamReply, - XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, + Aggregate, Connection, Cursor, DbRule, Double, Duration, Error, GetexTtl, Guest, Instant, + Integer, LcsIdxReply, LexBound, Limit, MsetexPrecondition, MsetexTtl, PopEnd, Rule, + RuleSelector, ScoreBound, SetPrecondition, SetTtl, SortOpts, StreamId, StreamTrim, TtlMode, + TtlResult, UserRule, Value, XclaimOpts, XinfoConsumer, XinfoGroup, XinfoStreamReply, + XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, ZaddComparison, ZaddCondition, + ZrangeBy, ZrangeWithscoresBy, }; -use crate::value::OrZeroId; +use crate::value::{FlatPairs, OrNever, OrZeroId, ScoredMembers}; use wit_bindgen::rt::Map; @@ -14,6 +19,39 @@ mod value; struct Commands; +/// The number of values SORT returns for each element, one for each GET pattern, or the element. +fn sort_row_len(opts: &Option) -> usize { + opts.as_ref() + .and_then(|opts| opts.get.as_ref()) + .map_or(1, Vec::len) +} + +/// The GET patterns of SORT, when given, at least one. +fn check_sort_opts(opts: &Option) -> Result<(), Error> { + match opts.as_ref().and_then(|opts| opts.get.as_ref()) { + Some(get) if get.is_empty() => { + Err(Error::Client("get requires at least one item".to_string())) + } + _ => Ok(()), + } +} + +/// The databases of a `db=` rule, at least one, including those of selectors. +fn check_rules(rules: &[Rule]) -> Result<(), Error> { + let empty = |rule: &DbRule| matches!(rule, DbRule::Db(dbs) if dbs.is_empty()); + let invalid = rules.iter().any(|rule| match rule { + Rule::Db(rule) => empty(rule), + Rule::User(UserRule::Selector(selectors)) => selectors + .iter() + .any(|selector| matches!(selector, RuleSelector::Db(rule) if empty(rule))), + _ => false, + }); + if invalid { + return Err(Error::Client("db requires at least one item".to_string())); + } + Ok(()) +} + impl Guest for Commands { #[doc = " admin commands"] #[doc = " Delete all the specified ACL users and terminate all the connections that are authenticated with such users."] @@ -25,6 +63,7 @@ impl Guest for Commands { connection: &Connection, usernames: Vec, ) -> Result { + let usernames = NonEmpty::new("usernames", usernames)?; query(connection, ("ACL", "DELUSER", usernames)).await } @@ -45,8 +84,12 @@ impl Guest for Commands { async fn acl_setuser( connection: &Connection, username: String, - rules: Vec, + rules: Option>, ) -> Result<(), Error> { + if let Some(rules) = &rules { + check_rules(rules)?; + } + let rules = NonEmpty::optional("rules", rules)?; query(connection, ("ACL", "SETUSER", username, rules)).await } @@ -111,6 +154,7 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result { + let fields = NonEmpty::new("fields", fields)?; query(connection, ("HDEL", key, fields)).await } @@ -135,6 +179,7 @@ impl Guest for Commands { mode: Option, fields: Vec, ) -> Result, Error> { + let fields = NonEmpty::new("fields", fields)?; query( connection, ( @@ -162,6 +207,7 @@ impl Guest for Commands { mode: Option, fields: Vec, ) -> Result, Error> { + let fields = NonEmpty::new("fields", fields)?; query( connection, ( @@ -255,6 +301,7 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result>, Error> { + let fields = NonEmpty::new("fields", fields)?; query(connection, ("HMGET", key, fields)).await } @@ -268,6 +315,7 @@ impl Guest for Commands { key: String, field_and_values: Vec<(String, String)>, ) -> Result<(), Error> { + let field_and_values = NonEmpty::new("field-and-values", field_and_values)?; query(connection, ("HMSET", key, field_and_values)).await } @@ -377,6 +425,7 @@ impl Guest for Commands { key: String, field_and_values: Vec<(String, String)>, ) -> Result { + let field_and_values = NonEmpty::new("field-and-values", field_and_values)?; query(connection, ("HSET", key, field_and_values)).await } @@ -422,6 +471,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/del/"] #[allow(async_fn_in_trait)] async fn del(connection: &Connection, keys: Vec) -> Result { + let keys = NonEmpty::new("keys", keys)?; query(connection, ("DEL", keys)).await } @@ -431,6 +481,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/exists/"] #[allow(async_fn_in_trait)] async fn exists(connection: &Connection, keys: Vec) -> Result { + let keys = NonEmpty::new("keys", keys)?; query(connection, ("EXISTS", keys)).await } @@ -477,6 +528,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/psubscribe/"] #[allow(async_fn_in_trait)] async fn psubscribe(connection: &Connection, patterns: Vec) -> Result<(), Error> { + let patterns = NonEmpty::new("patterns", patterns)?; emit(connection, ("PSUBSCRIBE", patterns)).await } @@ -521,8 +573,9 @@ impl Guest for Commands { #[allow(async_fn_in_trait)] async fn pubsub_numsub( connection: &Connection, - channels: Vec, + channels: Option>, ) -> Result, Error> { + let channels = NonEmpty::optional("channels", channels)?; query(connection, ("PUBSUB", "NUMSUB", channels)).await } @@ -545,8 +598,9 @@ impl Guest for Commands { #[allow(async_fn_in_trait)] async fn pubsub_shardnumsub( connection: &Connection, - shardchannels: Vec, + shardchannels: Option>, ) -> Result, Error> { + let shardchannels = NonEmpty::optional("shardchannels", shardchannels)?; query(connection, ("PUBSUB", "SHARDNUMSUB", shardchannels)).await } @@ -555,7 +609,11 @@ impl Guest for Commands { #[doc = " PUNSUBSCRIBE [ pattern [ pattern ... ] ]"] #[doc = " https://valkey.io/commands/punsubscribe/"] #[allow(async_fn_in_trait)] - async fn punsubscribe(connection: &Connection, patterns: Vec) -> Result<(), Error> { + async fn punsubscribe( + connection: &Connection, + patterns: Option>, + ) -> Result<(), Error> { + let patterns = NonEmpty::optional("patterns", patterns)?; emit(connection, ("PUNSUBSCRIBE", patterns)).await } @@ -578,6 +636,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/ssubscribe/"] #[allow(async_fn_in_trait)] async fn ssubscribe(connection: &Connection, shardchannels: Vec) -> Result<(), Error> { + let shardchannels = NonEmpty::new("shardchannels", shardchannels)?; emit(connection, ("SSUBSCRIBE", shardchannels)).await } @@ -587,6 +646,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/subscribe/"] #[allow(async_fn_in_trait)] async fn subscribe(connection: &Connection, channels: Vec) -> Result<(), Error> { + let channels = NonEmpty::new("channels", channels)?; emit(connection, ("SUBSCRIBE", channels)).await } @@ -597,8 +657,9 @@ impl Guest for Commands { #[allow(async_fn_in_trait)] async fn sunsubscribe( connection: &Connection, - shardchannels: Vec, + shardchannels: Option>, ) -> Result<(), Error> { + let shardchannels = NonEmpty::optional("shardchannels", shardchannels)?; emit(connection, ("SUNSUBSCRIBE", shardchannels)).await } @@ -607,7 +668,11 @@ impl Guest for Commands { #[doc = " UNSUBSCRIBE [ channel ] [ [ channel ] ... ]"] #[doc = " https://valkey.io/commands/unsubscribe/"] #[allow(async_fn_in_trait)] - async fn unsubscribe(connection: &Connection, channels: Vec) -> Result<(), Error> { + async fn unsubscribe( + connection: &Connection, + channels: Option>, + ) -> Result<(), Error> { + let channels = NonEmpty::optional("channels", channels)?; emit(connection, ("UNSUBSCRIBE", channels)).await } @@ -807,6 +872,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, ) -> Result>, Error> { + let keys = NonEmpty::new("keys", keys)?; query(connection, ("MGET", keys)).await } @@ -819,6 +885,7 @@ impl Guest for Commands { connection: &Connection, keys_and_values: Vec<(String, String)>, ) -> Result<(), Error> { + let keys_and_values = NonEmpty::new("keys-and-values", keys_and_values)?; query(connection, ("MSET", keys_and_values)).await } @@ -833,6 +900,7 @@ impl Guest for Commands { precondition: Option, ttl: Option, ) -> Result { + let keys_and_values = NonEmpty::new("keys-and-values", keys_and_values)?; query( connection, ( @@ -855,6 +923,7 @@ impl Guest for Commands { connection: &Connection, keys_and_values: Vec<(String, String)>, ) -> Result { + let keys_and_values = NonEmpty::new("keys-and-values", keys_and_values)?; query(connection, ("MSETNX", keys_and_values)).await } @@ -981,6 +1050,7 @@ impl Guest for Commands { group: String, ids: Vec, ) -> Result { + let ids = NonEmpty::new("ids", ids)?; query(connection, ("XACK", key, group, ids)).await } @@ -997,6 +1067,7 @@ impl Guest for Commands { id: StreamId, field_and_values: Vec<(String, String)>, ) -> Result, Error> { + let field_and_values = NonEmpty::new("field-and-values", field_and_values)?; query( connection, ( @@ -1099,6 +1170,7 @@ impl Guest for Commands { ids: Vec, opts: Option, ) -> Result)>, Error> { + let ids = NonEmpty::new("ids", ids)?; query( connection, ("XCLAIM", key, group, consumer, min_idle_time, ids, opts), @@ -1122,6 +1194,7 @@ impl Guest for Commands { ids: Vec, opts: Option, ) -> Result, Error> { + let ids = NonEmpty::new("ids", ids)?; query( connection, ("XCLAIM", key, group, consumer, min_idle_time, ids, opts), @@ -1139,6 +1212,7 @@ impl Guest for Commands { key: String, ids: Vec, ) -> Result { + let ids = NonEmpty::new("ids", ids)?; query(connection, ("XDEL", key, ids)).await } @@ -1381,6 +1455,8 @@ impl Guest for Commands { keys: Vec, ids: Vec, ) -> Result)>>>, Error> { + let keys = NonEmpty::new("keys", keys)?; + let ids = NonEmpty::new("ids", ids)?; query( connection, ( @@ -1413,6 +1489,8 @@ impl Guest for Commands { keys: Vec, ids: Vec, ) -> Result>)>>>, Error> { + let keys = NonEmpty::new("keys", keys)?; + let ids = NonEmpty::new("ids", ids)?; query( connection, ( @@ -1487,6 +1565,1124 @@ impl Guest for Commands { ) -> Result { query(connection, ("XTRIM", key, trim)).await } + + #[doc = " Adds the members to the set stored at key, returning how many were not already members."] + #[doc = ""] + #[doc = " SADD key member [ member ... ]"] + #[doc = " https://valkey.io/commands/sadd/"] + #[allow(async_fn_in_trait)] + async fn sadd( + connection: &Connection, + key: String, + members: Vec, + ) -> Result { + let members = NonEmpty::new("members", members)?; + query(connection, ("SADD", key, members)).await + } + + #[doc = " The number of members in the set stored at key."] + #[doc = ""] + #[doc = " SCARD key"] + #[doc = " https://valkey.io/commands/scard/"] + #[allow(async_fn_in_trait)] + async fn scard(connection: &Connection, key: String) -> Result { + query(connection, ("SCARD", key)).await + } + + #[doc = " The members of the first set that are not in any of the others."] + #[doc = ""] + #[doc = " SDIFF key [ key ... ]"] + #[doc = " https://valkey.io/commands/sdiff/"] + #[allow(async_fn_in_trait)] + async fn sdiff(connection: &Connection, keys: Vec) -> Result, Error> { + let keys = NonEmpty::new("keys", keys)?; + query(connection, ("SDIFF", keys)).await + } + + #[doc = " Stores the members of the first set that are not in any of the others, returning how many."] + #[doc = ""] + #[doc = " SDIFFSTORE destination key [ key ... ]"] + #[doc = " https://valkey.io/commands/sdiffstore/"] + #[allow(async_fn_in_trait)] + async fn sdiffstore( + connection: &Connection, + destination: String, + keys: Vec, + ) -> Result { + let keys = NonEmpty::new("keys", keys)?; + query(connection, ("SDIFFSTORE", destination, keys)).await + } + + #[doc = " The members in all of the sets."] + #[doc = ""] + #[doc = " SINTER key [ key ... ]"] + #[doc = " https://valkey.io/commands/sinter/"] + #[allow(async_fn_in_trait)] + async fn sinter(connection: &Connection, keys: Vec) -> Result, Error> { + let keys = NonEmpty::new("keys", keys)?; + query(connection, ("SINTER", keys)).await + } + + #[doc = " The number of members in all of the sets, counting stops at the limit."] + #[doc = ""] + #[doc = " SINTERCARD numkeys key [ key ... ] [ LIMIT limit ]"] + #[doc = " https://valkey.io/commands/sintercard/"] + #[allow(async_fn_in_trait)] + async fn sintercard( + connection: &Connection, + keys: Vec, + limit: Option, + ) -> Result { + let keys = NonEmpty::new("keys", keys)?; + query( + connection, + ("SINTERCARD", NumKeys(keys), NamedOption("LIMIT", limit)), + ) + .await + } + + #[doc = " Stores the members in all of the sets, returning how many."] + #[doc = ""] + #[doc = " SINTERSTORE destination key [ key ... ]"] + #[doc = " https://valkey.io/commands/sinterstore/"] + #[allow(async_fn_in_trait)] + async fn sinterstore( + connection: &Connection, + destination: String, + keys: Vec, + ) -> Result { + let keys = NonEmpty::new("keys", keys)?; + query(connection, ("SINTERSTORE", destination, keys)).await + } + + #[doc = " Whether the member is in the set stored at key, false when the key does not exist."] + #[doc = ""] + #[doc = " See also `sismember-xx`."] + #[doc = ""] + #[doc = " SISMEMBER key member"] + #[doc = " https://valkey.io/commands/sismember/"] + #[allow(async_fn_in_trait)] + async fn sismember( + connection: &Connection, + key: String, + member: String, + ) -> Result { + query(connection, ("SISMEMBER", key, member)).await + } + + #[doc = " Whether the member is in the set stored at key, none when the key does not exist."] + #[doc = ""] + #[doc = " See also `sismember`."] + #[doc = ""] + #[doc = " SISMEMBER key member XX"] + #[doc = " https://valkey.io/commands/sismember/"] + #[allow(async_fn_in_trait)] + async fn sismember_xx( + connection: &Connection, + key: String, + member: String, + ) -> Result, Error> { + // -1 when the key does not exist + let member: OrNever = query(connection, ("SISMEMBER", key, member, "XX")).await?; + Ok(member.0) + } + + #[doc = " The members of the set stored at key."] + #[doc = ""] + #[doc = " SMEMBERS key"] + #[doc = " https://valkey.io/commands/smembers/"] + #[allow(async_fn_in_trait)] + async fn smembers(connection: &Connection, key: String) -> Result, Error> { + query(connection, ("SMEMBERS", key)).await + } + + #[doc = " Whether each of the members is in the set stored at key."] + #[doc = ""] + #[doc = " SMISMEMBER key member [ member ... ]"] + #[doc = " https://valkey.io/commands/smismember/"] + #[allow(async_fn_in_trait)] + async fn smismember( + connection: &Connection, + key: String, + members: Vec, + ) -> Result, Error> { + let members = NonEmpty::new("members", members)?; + query(connection, ("SMISMEMBER", key, members)).await + } + + #[doc = " Moves the member from one set to another, false when it was not in the source set."] + #[doc = ""] + #[doc = " SMOVE source destination member"] + #[doc = " https://valkey.io/commands/smove/"] + #[allow(async_fn_in_trait)] + async fn smove( + connection: &Connection, + source: String, + destination: String, + member: String, + ) -> Result { + query(connection, ("SMOVE", source, destination, member)).await + } + + #[doc = " Removes and returns random members of the set stored at key, up to the count."] + #[doc = ""] + #[doc = " Without a count, one member, or none when the key does not exist."] + #[doc = ""] + #[doc = " SPOP key [ count ]"] + #[doc = " https://valkey.io/commands/spop/"] + #[allow(async_fn_in_trait)] + async fn spop( + connection: &Connection, + key: String, + count: Option, + ) -> Result, Error> { + if count.is_some() { + return query(connection, ("SPOP", key, count)).await; + } + // without a count the reply is the member itself, or null + let member: Option = query(connection, ("SPOP", key)).await?; + Ok(member.into_iter().collect()) + } + + #[doc = " Random members of the set stored at key, up to the count."] + #[doc = ""] + #[doc = " Without a count, one member, or none when the key does not exist. A negative count may return"] + #[doc = " the same member more than once, its absolute value is the number of members returned."] + #[doc = ""] + #[doc = " SRANDMEMBER key [ count ]"] + #[doc = " https://valkey.io/commands/srandmember/"] + #[allow(async_fn_in_trait)] + async fn srandmember( + connection: &Connection, + key: String, + count: Option, + ) -> Result, Error> { + if count.is_some() { + return query(connection, ("SRANDMEMBER", key, count)).await; + } + // without a count the reply is the member itself, or null + let member: Option = query(connection, ("SRANDMEMBER", key)).await?; + Ok(member.into_iter().collect()) + } + + #[doc = " Removes the members from the set stored at key, returning how many were members."] + #[doc = ""] + #[doc = " SREM key member [ member ... ]"] + #[doc = " https://valkey.io/commands/srem/"] + #[allow(async_fn_in_trait)] + async fn srem( + connection: &Connection, + key: String, + members: Vec, + ) -> Result { + let members = NonEmpty::new("members", members)?; + query(connection, ("SREM", key, members)).await + } + + #[doc = " Incrementally iterate over the members of the set stored at key."] + #[doc = ""] + #[doc = " SSCAN key cursor [ MATCH pattern ] [ COUNT count ]"] + #[doc = " https://valkey.io/commands/sscan/"] + #[allow(async_fn_in_trait)] + async fn sscan( + connection: &Connection, + key: String, + cursor: Option, + match_: Option, + count: Option, + ) -> Result<(Cursor, Vec), Error> { + query( + connection, + ( + "SSCAN", + key, + cursor.unwrap_or(Cursor::from("0")), + NamedOption("MATCH", match_), + NamedOption("COUNT", count), + ), + ) + .await + } + + #[doc = " The members in any of the sets."] + #[doc = ""] + #[doc = " SUNION key [ key ... ]"] + #[doc = " https://valkey.io/commands/sunion/"] + #[allow(async_fn_in_trait)] + async fn sunion(connection: &Connection, keys: Vec) -> Result, Error> { + let keys = NonEmpty::new("keys", keys)?; + query(connection, ("SUNION", keys)).await + } + + #[doc = " Stores the members in any of the sets, returning how many."] + #[doc = ""] + #[doc = " SUNIONSTORE destination key [ key ... ]"] + #[doc = " https://valkey.io/commands/sunionstore/"] + #[allow(async_fn_in_trait)] + async fn sunionstore( + connection: &Connection, + destination: String, + keys: Vec, + ) -> Result { + let keys = NonEmpty::new("keys", keys)?; + query(connection, ("SUNIONSTORE", destination, keys)).await + } + + #[doc = " Sorts the elements of the list, set or sorted set stored at key."] + #[doc = ""] + #[doc = " Each row is the element, or with `get`, the value of each pattern, none when its key does not"] + #[doc = " exist."] + #[doc = ""] + #[doc = " SORT key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ]"] + #[doc = " https://valkey.io/commands/sort/"] + #[allow(async_fn_in_trait)] + async fn sort( + connection: &Connection, + key: String, + opts: Option, + ) -> Result>>, Error> { + check_sort_opts(&opts)?; + let row = sort_row_len(&opts); + let values: Vec> = query(connection, ("SORT", key, opts)).await?; + Ok(values.chunks(row).map(<[_]>::to_vec).collect()) + } + + #[doc = " Sorts the elements of the list, set or sorted set stored at key, storing them as a list,"] + #[doc = " returning how many."] + #[doc = ""] + #[doc = " SORT key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] STORE destination"] + #[doc = " https://valkey.io/commands/sort/"] + #[allow(async_fn_in_trait)] + async fn sort_store( + connection: &Connection, + key: String, + opts: Option, + destination: String, + ) -> Result { + check_sort_opts(&opts)?; + query(connection, ("SORT", key, opts, "STORE", destination)).await + } + + #[doc = " The read-only variant of `sort`, e.g. for replicas."] + #[doc = ""] + #[doc = " SORT_RO key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ]"] + #[doc = " https://valkey.io/commands/sort_ro/"] + #[allow(async_fn_in_trait)] + async fn sort_ro( + connection: &Connection, + key: String, + opts: Option, + ) -> Result>>, Error> { + check_sort_opts(&opts)?; + let row = sort_row_len(&opts); + let values: Vec> = query(connection, ("SORT_RO", key, opts)).await?; + Ok(values.chunks(row).map(<[_]>::to_vec).collect()) + } + + #[doc = " Removes the members with the lowest or highest scores from the first non-empty sorted set,"] + #[doc = " waiting up to the timeout for one, returning its key and the members and their scores."] + #[doc = ""] + #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] + #[doc = ""] + #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] + #[doc = " it. Use a connection of your own."] + #[doc = ""] + #[doc = " BZMPOP timeout numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ]"] + #[doc = " https://valkey.io/commands/bzmpop/"] + #[allow(async_fn_in_trait)] + async fn bzmpop( + connection: &Connection, + timeout: Duration, + keys: Vec, + end: PopEnd, + count: Option, + ) -> Result)>, Error> { + let keys = NonEmpty::new("keys", keys)?; + let command = ( + "BZMPOP", + DecimalSeconds(timeout), + NumKeys(keys), + end, + NamedOption("COUNT", count), + ); + query(connection, command).await + } + + #[doc = " Removes the member with the highest score from the first non-empty sorted set, waiting up to"] + #[doc = " the timeout for one, returning its key, the member and its score."] + #[doc = ""] + #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] + #[doc = ""] + #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] + #[doc = " it. Use a connection of your own."] + #[doc = ""] + #[doc = " BZPOPMAX key [ key ... ] timeout"] + #[doc = " https://valkey.io/commands/bzpopmax/"] + #[allow(async_fn_in_trait)] + async fn bzpopmax( + connection: &Connection, + keys: Vec, + timeout: Duration, + ) -> Result, Error> { + let keys = NonEmpty::new("keys", keys)?; + query(connection, ("BZPOPMAX", keys, DecimalSeconds(timeout))).await + } + + #[doc = " Removes the member with the lowest score from the first non-empty sorted set, waiting up to"] + #[doc = " the timeout for one, returning its key, the member and its score."] + #[doc = ""] + #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] + #[doc = ""] + #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] + #[doc = " it. Use a connection of your own."] + #[doc = ""] + #[doc = " BZPOPMIN key [ key ... ] timeout"] + #[doc = " https://valkey.io/commands/bzpopmin/"] + #[allow(async_fn_in_trait)] + async fn bzpopmin( + connection: &Connection, + keys: Vec, + timeout: Duration, + ) -> Result, Error> { + let keys = NonEmpty::new("keys", keys)?; + query(connection, ("BZPOPMIN", keys, DecimalSeconds(timeout))).await + } + + #[doc = " Adds the members with their scores to the sorted set stored at key, or updates their scores,"] + #[doc = " returning how many were added, or with `changed`, added or updated."] + #[doc = ""] + #[doc = " See also `zadd-incr`."] + #[doc = ""] + #[doc = " ZADD key [ NX | XX ] [ GT | LT ] [ CH ] score member [ score member ... ]"] + #[doc = " https://valkey.io/commands/zadd/"] + #[allow(async_fn_in_trait)] + async fn zadd( + connection: &Connection, + key: String, + condition: Option, + comparison: Option, + changed: bool, + score_and_members: Vec<(Double, String)>, + ) -> Result, Error> { + let score_and_members = NonEmpty::new("score-and-members", score_and_members)?; + let command = ( + "ZADD", + key, + condition, + comparison, + Flag("CH", changed), + score_and_members, + ); + query(connection, command).await + } + + #[doc = " Increments the score of the member in the sorted set stored at key, returning the new score,"] + #[doc = " none when the condition or comparison prevented it."] + #[doc = ""] + #[doc = " See also `zadd` and `zincrby`."] + #[doc = ""] + #[doc = " ZADD key [ NX | XX ] [ GT | LT ] INCR score member"] + #[doc = " https://valkey.io/commands/zadd/"] + #[allow(async_fn_in_trait)] + async fn zadd_incr( + connection: &Connection, + key: String, + condition: Option, + comparison: Option, + increment: Double, + member: String, + ) -> Result, Error> { + let command = ( + "ZADD", key, condition, comparison, "INCR", increment, member, + ); + query(connection, command).await + } + + #[doc = " The number of members in the sorted set stored at key."] + #[doc = ""] + #[doc = " ZCARD key"] + #[doc = " https://valkey.io/commands/zcard/"] + #[allow(async_fn_in_trait)] + async fn zcard(connection: &Connection, key: String) -> Result { + query(connection, ("ZCARD", key)).await + } + + #[doc = " The number of members of the sorted set stored at key with scores in the range."] + #[doc = ""] + #[doc = " ZCOUNT key min max"] + #[doc = " https://valkey.io/commands/zcount/"] + #[allow(async_fn_in_trait)] + async fn zcount( + connection: &Connection, + key: String, + min: ScoreBound, + max: ScoreBound, + ) -> Result { + query( + connection, + ( + "ZCOUNT", + key, + ScoreRange { + start: min, + stop: max, + rev: false, + }, + ), + ) + .await + } + + #[doc = " The members of the first sorted set that are not in any of the others."] + #[doc = ""] + #[doc = " See also `zdiff-withscores`."] + #[doc = ""] + #[doc = " ZDIFF numkeys key [ key ... ]"] + #[doc = " https://valkey.io/commands/zdiff/"] + #[allow(async_fn_in_trait)] + async fn zdiff(connection: &Connection, keys: Vec) -> Result, Error> { + let keys = NonEmpty::new("keys", keys)?; + query(connection, ("ZDIFF", NumKeys(keys))).await + } + + #[doc = " The members, and their scores, of the first sorted set that are not in any of the others."] + #[doc = ""] + #[doc = " See also `zdiff`."] + #[doc = ""] + #[doc = " ZDIFF numkeys key [ key ... ] WITHSCORES"] + #[doc = " https://valkey.io/commands/zdiff/"] + #[allow(async_fn_in_trait)] + async fn zdiff_withscores( + connection: &Connection, + keys: Vec, + ) -> Result, Error> { + let keys = NonEmpty::new("keys", keys)?; + query(connection, ("ZDIFF", NumKeys(keys), "WITHSCORES")).await + } + + #[doc = " Stores the members of the first sorted set that are not in any of the others, returning how"] + #[doc = " many."] + #[doc = ""] + #[doc = " ZDIFFSTORE destination numkeys key [ key ... ]"] + #[doc = " https://valkey.io/commands/zdiffstore/"] + #[allow(async_fn_in_trait)] + async fn zdiffstore( + connection: &Connection, + destination: String, + keys: Vec, + ) -> Result { + let keys = NonEmpty::new("keys", keys)?; + query(connection, ("ZDIFFSTORE", destination, NumKeys(keys))).await + } + + #[doc = " Increments the score of the member in the sorted set stored at key, returning the new score."] + #[doc = ""] + #[doc = " ZINCRBY key increment member"] + #[doc = " https://valkey.io/commands/zincrby/"] + #[allow(async_fn_in_trait)] + async fn zincrby( + connection: &Connection, + key: String, + increment: Double, + member: String, + ) -> Result { + query(connection, ("ZINCRBY", key, increment, member)).await + } + + #[doc = " The members in all of the sorted sets."] + #[doc = ""] + #[doc = " The weights, when given, multiply the scores of each sorted set, one for each key."] + #[doc = ""] + #[doc = " See also `zinter-withscores`."] + #[doc = ""] + #[doc = " ZINTER numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ]"] + #[doc = " https://valkey.io/commands/zinter/"] + #[allow(async_fn_in_trait)] + async fn zinter( + connection: &Connection, + keys: Vec, + weights: Option>, + aggregate: Option, + ) -> Result, Error> { + let keys = NonEmpty::new("keys", keys)?; + let weights = NonEmpty::optional("weights", weights)?; + query( + connection, + ( + "ZINTER", + NumKeys(keys), + NamedOption("WEIGHTS", weights), + aggregate, + ), + ) + .await + } + + #[doc = " The members, and their combined scores, in all of the sorted sets."] + #[doc = ""] + #[doc = " See also `zinter`."] + #[doc = ""] + #[doc = " ZINTER numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] WITHSCORES"] + #[doc = " https://valkey.io/commands/zinter/"] + #[allow(async_fn_in_trait)] + async fn zinter_withscores( + connection: &Connection, + keys: Vec, + weights: Option>, + aggregate: Option, + ) -> Result, Error> { + let keys = NonEmpty::new("keys", keys)?; + let weights = NonEmpty::optional("weights", weights)?; + let command = ( + "ZINTER", + NumKeys(keys), + NamedOption("WEIGHTS", weights), + aggregate, + "WITHSCORES", + ); + query(connection, command).await + } + + #[doc = " The number of members in all of the sorted sets, counting stops at the limit."] + #[doc = ""] + #[doc = " ZINTERCARD numkeys key [ key ... ] [ LIMIT limit ]"] + #[doc = " https://valkey.io/commands/zintercard/"] + #[allow(async_fn_in_trait)] + async fn zintercard( + connection: &Connection, + keys: Vec, + limit: Option, + ) -> Result { + let keys = NonEmpty::new("keys", keys)?; + query( + connection, + ("ZINTERCARD", NumKeys(keys), NamedOption("LIMIT", limit)), + ) + .await + } + + #[doc = " Stores the members in all of the sorted sets, with their combined scores, returning how many."] + #[doc = ""] + #[doc = " ZINTERSTORE destination numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ]"] + #[doc = " https://valkey.io/commands/zinterstore/"] + #[allow(async_fn_in_trait)] + async fn zinterstore( + connection: &Connection, + destination: String, + keys: Vec, + weights: Option>, + aggregate: Option, + ) -> Result { + let keys = NonEmpty::new("keys", keys)?; + let weights = NonEmpty::optional("weights", weights)?; + let command = ( + "ZINTERSTORE", + destination, + NumKeys(keys), + NamedOption("WEIGHTS", weights), + aggregate, + ); + query(connection, command).await + } + + #[doc = " The number of members of the sorted set stored at key in the range, for members of equal"] + #[doc = " score."] + #[doc = ""] + #[doc = " ZLEXCOUNT key min max"] + #[doc = " https://valkey.io/commands/zlexcount/"] + #[allow(async_fn_in_trait)] + async fn zlexcount( + connection: &Connection, + key: String, + min: LexBound, + max: LexBound, + ) -> Result { + query( + connection, + ( + "ZLEXCOUNT", + key, + LexRange { + start: min, + stop: max, + rev: false, + }, + ), + ) + .await + } + + #[doc = " Removes the members with the lowest or highest scores from the first non-empty sorted set,"] + #[doc = " returning its key and the members and their scores, none when every sorted set is empty."] + #[doc = ""] + #[doc = " ZMPOP numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ]"] + #[doc = " https://valkey.io/commands/zmpop/"] + #[allow(async_fn_in_trait)] + async fn zmpop( + connection: &Connection, + keys: Vec, + end: PopEnd, + count: Option, + ) -> Result)>, Error> { + let keys = NonEmpty::new("keys", keys)?; + query( + connection, + ("ZMPOP", NumKeys(keys), end, NamedOption("COUNT", count)), + ) + .await + } + + #[doc = " The score of each of the members of the sorted set stored at key, none for a member that is"] + #[doc = " not in it."] + #[doc = ""] + #[doc = " ZMSCORE key member [ member ... ]"] + #[doc = " https://valkey.io/commands/zmscore/"] + #[allow(async_fn_in_trait)] + async fn zmscore( + connection: &Connection, + key: String, + members: Vec, + ) -> Result>, Error> { + let members = NonEmpty::new("members", members)?; + query(connection, ("ZMSCORE", key, members)).await + } + + #[doc = " Removes and returns the members with the highest scores, and their scores, up to the count,"] + #[doc = " one without a count."] + #[doc = ""] + #[doc = " ZPOPMAX key [ count ]"] + #[doc = " https://valkey.io/commands/zpopmax/"] + #[allow(async_fn_in_trait)] + async fn zpopmax( + connection: &Connection, + key: String, + count: Option, + ) -> Result, Error> { + let members: ScoredMembers = query(connection, ("ZPOPMAX", key, count)).await?; + Ok(members.0) + } + + #[doc = " Removes and returns the members with the lowest scores, and their scores, up to the count,"] + #[doc = " one without a count."] + #[doc = ""] + #[doc = " ZPOPMIN key [ count ]"] + #[doc = " https://valkey.io/commands/zpopmin/"] + #[allow(async_fn_in_trait)] + async fn zpopmin( + connection: &Connection, + key: String, + count: Option, + ) -> Result, Error> { + let members: ScoredMembers = query(connection, ("ZPOPMIN", key, count)).await?; + Ok(members.0) + } + + #[doc = " Random members of the sorted set stored at key, up to the count."] + #[doc = ""] + #[doc = " Without a count, one member, or none when the key does not exist. A negative count may return"] + #[doc = " the same member more than once, its absolute value is the number of members returned."] + #[doc = ""] + #[doc = " See also `zrandmember-withscores`."] + #[doc = ""] + #[doc = " ZRANDMEMBER key [ count ]"] + #[doc = " https://valkey.io/commands/zrandmember/"] + #[allow(async_fn_in_trait)] + async fn zrandmember( + connection: &Connection, + key: String, + count: Option, + ) -> Result, Error> { + if count.is_some() { + return query(connection, ("ZRANDMEMBER", key, count)).await; + } + // without a count the reply is the member itself, or null + let member: Option = query(connection, ("ZRANDMEMBER", key)).await?; + Ok(member.into_iter().collect()) + } + + #[doc = " Random members of the sorted set stored at key, and their scores, up to the count."] + #[doc = ""] + #[doc = " A negative count may return the same member more than once, its absolute value is the number"] + #[doc = " of members returned."] + #[doc = ""] + #[doc = " See also `zrandmember`."] + #[doc = ""] + #[doc = " ZRANDMEMBER key count WITHSCORES"] + #[doc = " https://valkey.io/commands/zrandmember/"] + #[allow(async_fn_in_trait)] + async fn zrandmember_withscores( + connection: &Connection, + key: String, + count: Integer, + ) -> Result, Error> { + query(connection, ("ZRANDMEMBER", key, count, "WITHSCORES")).await + } + + #[doc = " The members of the sorted set stored at key in the range, from the lowest score, or the"] + #[doc = " highest with `rev`."] + #[doc = ""] + #[doc = " The limit is a range of the members in the range, only with BYSCORE or BYLEX. With `xx`,"] + #[doc = " returns none when the key does not exist."] + #[doc = ""] + #[doc = " Also covers the deprecated ZRANGEBYLEX, ZRANGEBYSCORE, ZREVRANGE, ZREVRANGEBYLEX and"] + #[doc = " ZREVRANGEBYSCORE. See also `zrange-withscores`."] + #[doc = ""] + #[doc = " ZRANGE key start stop [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ] [ XX ]"] + #[doc = " https://valkey.io/commands/zrange/"] + #[allow(async_fn_in_trait)] + async fn zrange( + connection: &Connection, + key: String, + range: ZrangeBy, + rev: bool, + limit: Option, + xx: bool, + ) -> Result>, Error> { + let range = Zrange { range, rev, limit }; + query(connection, ("ZRANGE", key, range, Flag("XX", xx))).await + } + + #[doc = " The members of the sorted set stored at key in the range, and their scores, from the lowest"] + #[doc = " score, or the highest with `rev`."] + #[doc = ""] + #[doc = " The limit is a range of the members in the range, only with BYSCORE. With `xx`, returns none"] + #[doc = " when the key does not exist."] + #[doc = ""] + #[doc = " See also `zrange`."] + #[doc = ""] + #[doc = " ZRANGE key start stop [ BYSCORE ] [ REV ] [ LIMIT offset count ] WITHSCORES [ XX ]"] + #[doc = " https://valkey.io/commands/zrange/"] + #[allow(async_fn_in_trait)] + async fn zrange_withscores( + connection: &Connection, + key: String, + range: ZrangeWithscoresBy, + rev: bool, + limit: Option, + xx: bool, + ) -> Result>, Error> { + let range = match range { + ZrangeWithscoresBy::Rank(rank) => ZrangeBy::Rank(rank), + ZrangeWithscoresBy::Score(score) => ZrangeBy::Score(score), + }; + let range = Zrange { range, rev, limit }; + let command = ("ZRANGE", key, range, "WITHSCORES", Flag("XX", xx)); + query(connection, command).await + } + + #[doc = " Stores the members of the sorted set stored at source in the range, returning how many."] + #[doc = ""] + #[doc = " The limit is a range of the members in the range, only with BYSCORE or BYLEX."] + #[doc = ""] + #[doc = " ZRANGESTORE dst src min max [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ]"] + #[doc = " https://valkey.io/commands/zrangestore/"] + #[allow(async_fn_in_trait)] + async fn zrangestore( + connection: &Connection, + destination: String, + source: String, + range: ZrangeBy, + rev: bool, + limit: Option, + ) -> Result { + let range = Zrange { range, rev, limit }; + query(connection, ("ZRANGESTORE", destination, source, range)).await + } + + #[doc = " The position of the member in the sorted set stored at key, from the lowest score, none when"] + #[doc = " it is not a member."] + #[doc = ""] + #[doc = " See also `zrank-withscore` and `zrevrank`."] + #[doc = ""] + #[doc = " ZRANK key member"] + #[doc = " https://valkey.io/commands/zrank/"] + #[allow(async_fn_in_trait)] + async fn zrank( + connection: &Connection, + key: String, + member: String, + ) -> Result, Error> { + query(connection, ("ZRANK", key, member)).await + } + + #[doc = " The position, and the score, of the member in the sorted set stored at key, from the lowest"] + #[doc = " score, none when it is not a member."] + #[doc = ""] + #[doc = " See also `zrank`."] + #[doc = ""] + #[doc = " ZRANK key member WITHSCORE"] + #[doc = " https://valkey.io/commands/zrank/"] + #[allow(async_fn_in_trait)] + async fn zrank_withscore( + connection: &Connection, + key: String, + member: String, + ) -> Result, Error> { + query(connection, ("ZRANK", key, member, "WITHSCORE")).await + } + + #[doc = " Removes the members from the sorted set stored at key, returning how many were members."] + #[doc = ""] + #[doc = " ZREM key member [ member ... ]"] + #[doc = " https://valkey.io/commands/zrem/"] + #[allow(async_fn_in_trait)] + async fn zrem( + connection: &Connection, + key: String, + members: Vec, + ) -> Result { + let members = NonEmpty::new("members", members)?; + query(connection, ("ZREM", key, members)).await + } + + #[doc = " Removes the members of the sorted set stored at key in the range, for members of equal score,"] + #[doc = " returning how many."] + #[doc = ""] + #[doc = " ZREMRANGEBYLEX key min max"] + #[doc = " https://valkey.io/commands/zremrangebylex/"] + #[allow(async_fn_in_trait)] + async fn zremrangebylex( + connection: &Connection, + key: String, + min: LexBound, + max: LexBound, + ) -> Result { + query( + connection, + ( + "ZREMRANGEBYLEX", + key, + LexRange { + start: min, + stop: max, + rev: false, + }, + ), + ) + .await + } + + #[doc = " Removes the members of the sorted set stored at key at the positions, both included,"] + #[doc = " negative from the end, returning how many."] + #[doc = ""] + #[doc = " ZREMRANGEBYRANK key start stop"] + #[doc = " https://valkey.io/commands/zremrangebyrank/"] + #[allow(async_fn_in_trait)] + async fn zremrangebyrank( + connection: &Connection, + key: String, + start: Integer, + stop: Integer, + ) -> Result { + query(connection, ("ZREMRANGEBYRANK", key, start, stop)).await + } + + #[doc = " Removes the members of the sorted set stored at key with scores in the range, returning how"] + #[doc = " many."] + #[doc = ""] + #[doc = " ZREMRANGEBYSCORE key min max"] + #[doc = " https://valkey.io/commands/zremrangebyscore/"] + #[allow(async_fn_in_trait)] + async fn zremrangebyscore( + connection: &Connection, + key: String, + min: ScoreBound, + max: ScoreBound, + ) -> Result { + query( + connection, + ( + "ZREMRANGEBYSCORE", + key, + ScoreRange { + start: min, + stop: max, + rev: false, + }, + ), + ) + .await + } + + #[doc = " The position of the member in the sorted set stored at key, from the highest score, none"] + #[doc = " when it is not a member."] + #[doc = ""] + #[doc = " See also `zrevrank-withscore` and `zrank`."] + #[doc = ""] + #[doc = " ZREVRANK key member"] + #[doc = " https://valkey.io/commands/zrevrank/"] + #[allow(async_fn_in_trait)] + async fn zrevrank( + connection: &Connection, + key: String, + member: String, + ) -> Result, Error> { + query(connection, ("ZREVRANK", key, member)).await + } + + #[doc = " The position, and the score, of the member in the sorted set stored at key, from the highest"] + #[doc = " score, none when it is not a member."] + #[doc = ""] + #[doc = " See also `zrevrank`."] + #[doc = ""] + #[doc = " ZREVRANK key member WITHSCORE"] + #[doc = " https://valkey.io/commands/zrevrank/"] + #[allow(async_fn_in_trait)] + async fn zrevrank_withscore( + connection: &Connection, + key: String, + member: String, + ) -> Result, Error> { + query(connection, ("ZREVRANK", key, member, "WITHSCORE")).await + } + + #[doc = " Incrementally iterate over the members of the sorted set stored at key, and their scores."] + #[doc = ""] + #[doc = " See also `zscan-noscores`."] + #[doc = ""] + #[doc = " ZSCAN key cursor [ MATCH pattern ] [ COUNT count ]"] + #[doc = " https://valkey.io/commands/zscan/"] + #[allow(async_fn_in_trait)] + async fn zscan( + connection: &Connection, + key: String, + cursor: Option, + match_: Option, + count: Option, + ) -> Result<(Cursor, Vec<(String, Double)>), Error> { + // the members and scores are a flat array, the scores bulk strings + let (cursor, members): (Cursor, FlatPairs) = query( + connection, + ( + "ZSCAN", + key, + cursor.unwrap_or(Cursor::from("0")), + NamedOption("MATCH", match_), + NamedOption("COUNT", count), + ), + ) + .await?; + Ok((cursor, members.0)) + } + + #[doc = " Incrementally iterate over the members of the sorted set stored at key."] + #[doc = ""] + #[doc = " See also `zscan`."] + #[doc = ""] + #[doc = " ZSCAN key cursor [ MATCH pattern ] [ COUNT count ] NOSCORES"] + #[doc = " https://valkey.io/commands/zscan/"] + #[allow(async_fn_in_trait)] + async fn zscan_noscores( + connection: &Connection, + key: String, + cursor: Option, + match_: Option, + count: Option, + ) -> Result<(Cursor, Vec), Error> { + query( + connection, + ( + "ZSCAN", + key, + cursor.unwrap_or(Cursor::from("0")), + NamedOption("MATCH", match_), + NamedOption("COUNT", count), + "NOSCORES", + ), + ) + .await + } + + #[doc = " The score of the member in the sorted set stored at key, none when it is not a member."] + #[doc = ""] + #[doc = " ZSCORE key member"] + #[doc = " https://valkey.io/commands/zscore/"] + #[allow(async_fn_in_trait)] + async fn zscore( + connection: &Connection, + key: String, + member: String, + ) -> Result, Error> { + query(connection, ("ZSCORE", key, member)).await + } + + #[doc = " The members in any of the sorted sets."] + #[doc = ""] + #[doc = " The weights, when given, multiply the scores of each sorted set, one for each key."] + #[doc = ""] + #[doc = " See also `zunion-withscores`."] + #[doc = ""] + #[doc = " ZUNION numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ]"] + #[doc = " https://valkey.io/commands/zunion/"] + #[allow(async_fn_in_trait)] + async fn zunion( + connection: &Connection, + keys: Vec, + weights: Option>, + aggregate: Option, + ) -> Result, Error> { + let keys = NonEmpty::new("keys", keys)?; + let weights = NonEmpty::optional("weights", weights)?; + query( + connection, + ( + "ZUNION", + NumKeys(keys), + NamedOption("WEIGHTS", weights), + aggregate, + ), + ) + .await + } + + #[doc = " The members, and their combined scores, in any of the sorted sets."] + #[doc = ""] + #[doc = " See also `zunion`."] + #[doc = ""] + #[doc = " ZUNION numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] WITHSCORES"] + #[doc = " https://valkey.io/commands/zunion/"] + #[allow(async_fn_in_trait)] + async fn zunion_withscores( + connection: &Connection, + keys: Vec, + weights: Option>, + aggregate: Option, + ) -> Result, Error> { + let keys = NonEmpty::new("keys", keys)?; + let weights = NonEmpty::optional("weights", weights)?; + let command = ( + "ZUNION", + NumKeys(keys), + NamedOption("WEIGHTS", weights), + aggregate, + "WITHSCORES", + ); + query(connection, command).await + } + + #[doc = " Stores the members in any of the sorted sets, with their combined scores, returning how many."] + #[doc = ""] + #[doc = " ZUNIONSTORE destination numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ]"] + #[doc = " https://valkey.io/commands/zunionstore/"] + #[allow(async_fn_in_trait)] + async fn zunionstore( + connection: &Connection, + destination: String, + keys: Vec, + weights: Option>, + aggregate: Option, + ) -> Result { + let keys = NonEmpty::new("keys", keys)?; + let weights = NonEmpty::optional("weights", weights)?; + let command = ( + "ZUNIONSTORE", + destination, + NumKeys(keys), + NamedOption("WEIGHTS", weights), + aggregate, + ); + query(connection, command).await + } } wit_bindgen::generate!({ diff --git a/components/commands/src/value.rs b/components/commands/src/value.rs index daafd20..719ccc2 100644 --- a/components/commands/src/value.rs +++ b/components/commands/src/value.rs @@ -267,6 +267,43 @@ impl FromValue for XpendingExtendedEntry { } } +/// Members and their scores, e.g. from ZPOPMIN, which replies with one pair, or none, without a +/// count, and a list of pairs with one. +pub struct ScoredMembers(pub Vec<(String, Double)>); + +impl FromValue for ScoredMembers { + fn from_value(value: Value) -> Result { + let items = items(value)?; + match items.first() { + None => Ok(ScoredMembers(vec![])), + Some(Value::Array(_)) => items + .into_iter() + .map(FromValue::from_value) + .collect::>() + .map(ScoredMembers), + Some(_) => match <[Value; 2]>::try_from(items) { + Ok([member, score]) => Ok(ScoredMembers(vec![( + String::from_value(member)?, + Double::from_value(score)?, + )])), + Err(items) => Err(Error::Client(format!( + "unexpected number of items, expected a member and its score: {}", + items.len() + ))), + }, + } + } +} + +/// Pairs in a flat array, `[key, value, key, value...]`, e.g. the members and scores of ZSCAN. +pub struct FlatPairs(pub Vec<(K, V)>); + +impl FromValue for FlatPairs { + fn from_value(value: Value) -> Result { + entries(value).map(FlatPairs) + } +} + impl FromValue for TtlResult { fn from_value(value: Value) -> Result { match Integer::from_value(value)? { diff --git a/components/connection/src/lib.rs b/components/connection/src/lib.rs index 772554e..00a7c68 100644 --- a/components/connection/src/lib.rs +++ b/components/connection/src/lib.rs @@ -136,6 +136,9 @@ impl GuestConnection for ConnectionImpl { } async fn watch(&self, keys: Vec) -> Result<(), Error> { + if keys.is_empty() { + return Err(Error::Client("keys requires at least one item".to_string())); + } let command = std::iter::once("WATCH".to_string()).chain(keys).collect(); ok(exchange(&self.0, command).await?) } @@ -189,6 +192,11 @@ fn enqueue( reply: Option, written: Option>, ) -> Result<(), Error> { + if command.is_empty() { + return Err(Error::Client( + "a command requires at least one word".to_string(), + )); + } let mut state = inner.state.borrow_mut(); if let Some(error) = &state.closed { return Err(error.clone()); @@ -253,6 +261,12 @@ async fn transaction( inner: &Inner, commands: Vec>, ) -> Result>, Error> { + // before MULTI is sent, so an invalid command fails the transaction rather than being left out + if commands.iter().any(Vec::is_empty) { + return Err(Error::Client( + "a command requires at least one word".to_string(), + )); + } let batch = std::iter::once(words(&["MULTI"])) .chain(commands) .chain(std::iter::once(words(&["EXEC"]))) diff --git a/wit/commands.wit b/wit/commands.wit index c25e917..704b5a2 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -1,3 +1,8 @@ +/// Typed functions for Valkey's commands, over the mechanics of a connection. +/// +/// A list argument must have at least one item, as the command requires, an empty list fails with a +/// client error before anything is sent. A list the command allows to be left out is an option, +/// none rather than empty. interface commands { use connection.{connection, double, duration, instant, integer, value, error}; @@ -163,7 +168,7 @@ interface commands { /// /// ACL SETUSER username [ rule ] [ [ rule ] ... ] /// https://valkey.io/commands/acl-setuser/ - acl-setuser: async func(connection: borrow, username: string, rules: list) -> result<_, error>; + acl-setuser: async func(connection: borrow, username: string, rules: option>) -> result<_, error>; // not yet implemented, from the @admin category: @@ -616,7 +621,7 @@ interface commands { /// /// PUBSUB NUMSUB [ channel ] [ [ channel ] ... ] /// https://valkey.io/commands/pubsub-numsub/ - pubsub-numsub: async func(connection: borrow, channels: list) -> result, error>; + pubsub-numsub: async func(connection: borrow, channels: option>) -> result, error>; /// Lists the currently active shard channels. /// @@ -628,13 +633,13 @@ interface commands { /// /// PUBSUB SHARDNUMSUB [ shardchannel [ shardchannel ... ] ] /// https://valkey.io/commands/pubsub-shardnumsub/ - pubsub-shardnumsub: async func(connection: borrow, shardchannels: list) -> result, error>; + pubsub-shardnumsub: async func(connection: borrow, shardchannels: option>) -> result, error>; /// Unsubscribes the client from the given patterns, or from all of them if none is given. /// /// PUNSUBSCRIBE [ pattern [ pattern ... ] ] /// https://valkey.io/commands/punsubscribe/ - punsubscribe: async func(connection: borrow, patterns: list) -> result<_, error>; + punsubscribe: async func(connection: borrow, patterns: option>) -> result<_, error>; /// Posts a message to the given shard channel. /// @@ -658,13 +663,13 @@ interface commands { /// /// SUNSUBSCRIBE [ shardchannel ] [ [ shardchannel ] ... ] /// https://valkey.io/commands/sunsubscribe/ - sunsubscribe: async func(connection: borrow, shardchannels: list) -> result<_, error>; + sunsubscribe: async func(connection: borrow, shardchannels: option>) -> result<_, error>; /// Unsubscribes the client from the given channels, or from all of them if none is given. /// /// UNSUBSCRIBE [ channel ] [ [ channel ] ... ] /// https://valkey.io/commands/unsubscribe/ - unsubscribe: async func(connection: borrow, channels: list) -> result<_, error>; + unsubscribe: async func(connection: borrow, channels: option>) -> result<_, error>; // scripting commands @@ -694,69 +699,575 @@ interface commands { // set commands - // not yet implemented, from the @set category: - // SADD - // SCARD - // SDIFF - // SDIFFSTORE - // SINTER - // SINTERCARD - // SINTERSTORE - // SISMEMBER - // SMEMBERS - // SMISMEMBER - // SMOVE - // SORT - // SORT_RO - // SPOP - // SRANDMEMBER - // SREM - // SSCAN - // SUNION - // SUNIONSTORE + /// Adds the members to the set stored at key, returning how many were not already members. + /// + /// SADD key member [ member ... ] + /// https://valkey.io/commands/sadd/ + sadd: async func(connection: borrow, key: string, members: list) -> result; + + /// The number of members in the set stored at key. + /// + /// SCARD key + /// https://valkey.io/commands/scard/ + scard: async func(connection: borrow, key: string) -> result; + + /// The members of the first set that are not in any of the others. + /// + /// SDIFF key [ key ... ] + /// https://valkey.io/commands/sdiff/ + sdiff: async func(connection: borrow, keys: list) -> result, error>; + + /// Stores the members of the first set that are not in any of the others, returning how many. + /// + /// SDIFFSTORE destination key [ key ... ] + /// https://valkey.io/commands/sdiffstore/ + sdiffstore: async func(connection: borrow, destination: string, keys: list) -> result; + + /// The members in all of the sets. + /// + /// SINTER key [ key ... ] + /// https://valkey.io/commands/sinter/ + sinter: async func(connection: borrow, keys: list) -> result, error>; + + /// The number of members in all of the sets, counting stops at the limit. + /// + /// SINTERCARD numkeys key [ key ... ] [ LIMIT limit ] + /// https://valkey.io/commands/sintercard/ + sintercard: async func(connection: borrow, keys: list, limit: option) -> result; + + /// Stores the members in all of the sets, returning how many. + /// + /// SINTERSTORE destination key [ key ... ] + /// https://valkey.io/commands/sinterstore/ + sinterstore: async func(connection: borrow, destination: string, keys: list) -> result; + + /// Whether the member is in the set stored at key, false when the key does not exist. + /// + /// See also `sismember-xx`. + /// + /// SISMEMBER key member + /// https://valkey.io/commands/sismember/ + sismember: async func(connection: borrow, key: string, member: string) -> result; + + /// Whether the member is in the set stored at key, none when the key does not exist. + /// + /// See also `sismember`. + /// + /// SISMEMBER key member XX + /// https://valkey.io/commands/sismember/ + sismember-xx: async func(connection: borrow, key: string, member: string) -> result, error>; + + /// The members of the set stored at key. + /// + /// SMEMBERS key + /// https://valkey.io/commands/smembers/ + smembers: async func(connection: borrow, key: string) -> result, error>; + + /// Whether each of the members is in the set stored at key. + /// + /// SMISMEMBER key member [ member ... ] + /// https://valkey.io/commands/smismember/ + smismember: async func(connection: borrow, key: string, members: list) -> result, error>; + + /// Moves the member from one set to another, false when it was not in the source set. + /// + /// SMOVE source destination member + /// https://valkey.io/commands/smove/ + smove: async func(connection: borrow, source: string, destination: string, member: string) -> result; + + /// Removes and returns random members of the set stored at key, up to the count. + /// + /// Without a count, one member, or none when the key does not exist. + /// + /// SPOP key [ count ] + /// https://valkey.io/commands/spop/ + spop: async func(connection: borrow, key: string, count: option) -> result, error>; + + /// Random members of the set stored at key, up to the count. + /// + /// Without a count, one member, or none when the key does not exist. A negative count may return + /// the same member more than once, its absolute value is the number of members returned. + /// + /// SRANDMEMBER key [ count ] + /// https://valkey.io/commands/srandmember/ + srandmember: async func(connection: borrow, key: string, count: option) -> result, error>; + + /// Removes the members from the set stored at key, returning how many were members. + /// + /// SREM key member [ member ... ] + /// https://valkey.io/commands/srem/ + srem: async func(connection: borrow, key: string, members: list) -> result; + + /// Incrementally iterate over the members of the set stored at key. + /// + /// SSCAN key cursor [ MATCH pattern ] [ COUNT count ] + /// https://valkey.io/commands/sscan/ + sscan: async func(connection: borrow, key: string, cursor: option, match: option, count: option) -> result>, error>; + + /// The members in any of the sets. + /// + /// SUNION key [ key ... ] + /// https://valkey.io/commands/sunion/ + sunion: async func(connection: borrow, keys: list) -> result, error>; + + /// Stores the members in any of the sets, returning how many. + /// + /// SUNIONSTORE destination key [ key ... ] + /// https://valkey.io/commands/sunionstore/ + sunionstore: async func(connection: borrow, destination: string, keys: list) -> result; + + /// A range of a sorted result, `LIMIT offset count`. + record limit { + /// The number of elements skipped. + offset: integer, + /// The maximum number of elements returned, negative for all of them. + count: integer, + } + + /// How SORT orders and projects the elements of a list, set or sorted set. + record sort-opts { + /// `BY pattern`: sorts by the values of the keys the pattern names, `*` replaced with each + /// element, or `nosort` to skip sorting. + by: option, + /// `LIMIT offset count`: a range of the sorted elements. + limit: option, + /// `GET pattern`: returns the values of the keys each pattern names instead of the elements, + /// `#` for the element itself. + get: option>, + /// order to sort elements. + order: option, + /// `ALPHA`: sorts lexicographically, rather than numerically. + alpha: bool, + } + + enum sort-order { + /// ascending, smallest to largest. + asc, + /// descending, largest to smallest. + desc, + } + + /// Sorts the elements of the list, set or sorted set stored at key. + /// + /// Each row is the element, or with `get`, the value of each pattern, none when its key does not + /// exist. + /// + /// SORT key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] + /// https://valkey.io/commands/sort/ + sort: async func(connection: borrow, key: string, opts: option) -> result>>, error>; + + /// Sorts the elements of the list, set or sorted set stored at key, storing them as a list, + /// returning how many. + /// + /// SORT key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] STORE destination + /// https://valkey.io/commands/sort/ + sort-store: async func(connection: borrow, key: string, opts: option, destination: string) -> result; + + /// The read-only variant of `sort`, e.g. for replicas. + /// + /// SORT_RO key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] + /// https://valkey.io/commands/sort_ro/ + sort-ro: async func(connection: borrow, key: string, opts: option) -> result>>, error>; // sortedset commands + /// A bound of a range of scores. + variant score-bound { + /// The score, included in the range. + inclusive(double), + /// `(score`: the score, excluded from the range. + exclusive(double), + /// `-inf` as the min, `+inf` as the max, the start is the max with `REV`. + unbounded, + } - // not yet implemented, from the @sortedset category: - // BZMPOP - // BZPOPMAX - // BZPOPMIN - // SORT - // SORT_RO - // ZADD - // ZCARD - // ZCOUNT - // ZDIFF - // ZDIFFSTORE - // ZINCRBY - // ZINTER - // ZINTERCARD - // ZINTERSTORE - // ZLEXCOUNT - // ZMPOP - // ZMSCORE - // ZPOPMAX - // ZPOPMIN - // ZRANDMEMBER - // ZRANGE - // ZRANGEBYLEX - // ZRANGEBYSCORE - // ZRANGESTORE - // ZRANK - // ZREM - // ZREMRANGEBYLEX - // ZREMRANGEBYRANK - // ZREMRANGEBYSCORE - // ZREVRANGE - // ZREVRANGEBYLEX - // ZREVRANGEBYSCORE - // ZREVRANK - // ZSCAN - // ZSCORE - // ZUNION - // ZUNIONSTORE + /// A bound of a range of members, for members of equal score, ordered lexicographically. + variant lex-bound { + /// `[member`: the member, included in the range. + inclusive(string), + /// `(member`: the member, excluded from the range. + exclusive(string), + /// `-` as the min, `+` as the max, the start is the max with `REV`. + unbounded, + } + + /// Which end of a sorted set to pop from. + enum pop-end { + /// The members with the lowest scores. + min, + /// The members with the highest scores. + max, + } + + /// How scores combine for a member in several sorted sets. + enum aggregate { + /// The sum of the weighted scores, the default. + sum, + /// The lowest weighted score. + min, + /// The highest weighted score. + max, + } + + /// The range of ZRANGE and ZRANGESTORE, `start` and `stop`, given as they are sent: with `REV`, + /// the higher bound first. + variant zrange-by { + /// Positions, both included, negative from the end. + rank(tuple), + /// `BYSCORE`: the members with scores in the range. + score(tuple), + /// `BYLEX`: the members in the range, for members of equal score. + lex(tuple), + } + + /// The range of ZRANGE WITHSCORES, which does not support BYLEX, see `zrange-by`. + variant zrange-withscores-by { + /// Positions, both included, negative from the end. + rank(tuple), + /// `BYSCORE`: the members with scores in the range. + score(tuple), + } + + /// Only update or only add members, ZADD's `NX` and `XX`. + enum zadd-condition { + /// Only add new members, never update existing ones. + nx, + /// Only update existing members, never add new ones. + xx, + } + + /// Only update a member's score when the new score is greater or less, ZADD's `GT` and `LT`. + enum zadd-comparison { + /// Only update when the new score is greater than the current one. + gt, + /// Only update when the new score is less than the current one. + lt, + } + + /// Removes the members with the lowest or highest scores from the first non-empty sorted set, + /// waiting up to the timeout for one, returning its key and the members and their scores. + /// + /// Returns none when the timeout passed. A timeout of zero waits indefinitely. + /// + /// The connection waits too, replies to commands sent after it on the connection wait behind + /// it. Use a connection of your own. + /// + /// BZMPOP timeout numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ] + /// https://valkey.io/commands/bzmpop/ + bzmpop: async func(connection: borrow, timeout: duration, keys: list, end: pop-end, count: option) -> result>>>, error>; + + /// Removes the member with the highest score from the first non-empty sorted set, waiting up to + /// the timeout for one, returning its key, the member and its score. + /// + /// Returns none when the timeout passed. A timeout of zero waits indefinitely. + /// + /// The connection waits too, replies to commands sent after it on the connection wait behind + /// it. Use a connection of your own. + /// + /// BZPOPMAX key [ key ... ] timeout + /// https://valkey.io/commands/bzpopmax/ + bzpopmax: async func(connection: borrow, keys: list, timeout: duration) -> result>, error>; + + /// Removes the member with the lowest score from the first non-empty sorted set, waiting up to + /// the timeout for one, returning its key, the member and its score. + /// + /// Returns none when the timeout passed. A timeout of zero waits indefinitely. + /// + /// The connection waits too, replies to commands sent after it on the connection wait behind + /// it. Use a connection of your own. + /// + /// BZPOPMIN key [ key ... ] timeout + /// https://valkey.io/commands/bzpopmin/ + bzpopmin: async func(connection: borrow, keys: list, timeout: duration) -> result>, error>; + + /// Adds the members with their scores to the sorted set stored at key, or updates their scores, + /// returning how many were added, or with `changed`, added or updated. + /// + /// See also `zadd-incr`. + /// + /// ZADD key [ NX | XX ] [ GT | LT ] [ CH ] score member [ score member ... ] + /// https://valkey.io/commands/zadd/ + zadd: async func(connection: borrow, key: string, condition: option, comparison: option, changed: bool, score-and-members: list>) -> result, error>; + + /// Increments the score of the member in the sorted set stored at key, returning the new score, + /// none when the condition or comparison prevented it. + /// + /// See also `zadd` and `zincrby`. + /// + /// ZADD key [ NX | XX ] [ GT | LT ] INCR score member + /// https://valkey.io/commands/zadd/ + zadd-incr: async func(connection: borrow, key: string, condition: option, comparison: option, increment: double, member: string) -> result, error>; + + /// The number of members in the sorted set stored at key. + /// + /// ZCARD key + /// https://valkey.io/commands/zcard/ + zcard: async func(connection: borrow, key: string) -> result; + + /// The number of members of the sorted set stored at key with scores in the range. + /// + /// ZCOUNT key min max + /// https://valkey.io/commands/zcount/ + zcount: async func(connection: borrow, key: string, min: score-bound, max: score-bound) -> result; + + /// The members of the first sorted set that are not in any of the others. + /// + /// See also `zdiff-withscores`. + /// + /// ZDIFF numkeys key [ key ... ] + /// https://valkey.io/commands/zdiff/ + zdiff: async func(connection: borrow, keys: list) -> result, error>; + + /// The members, and their scores, of the first sorted set that are not in any of the others. + /// + /// See also `zdiff`. + /// + /// ZDIFF numkeys key [ key ... ] WITHSCORES + /// https://valkey.io/commands/zdiff/ + zdiff-withscores: async func(connection: borrow, keys: list) -> result>, error>; + + /// Stores the members of the first sorted set that are not in any of the others, returning how + /// many. + /// + /// ZDIFFSTORE destination numkeys key [ key ... ] + /// https://valkey.io/commands/zdiffstore/ + zdiffstore: async func(connection: borrow, destination: string, keys: list) -> result; + + /// Increments the score of the member in the sorted set stored at key, returning the new score. + /// + /// ZINCRBY key increment member + /// https://valkey.io/commands/zincrby/ + zincrby: async func(connection: borrow, key: string, increment: double, member: string) -> result; + + /// The members in all of the sorted sets. + /// + /// The weights, when given, multiply the scores of each sorted set, one for each key. + /// + /// See also `zinter-withscores`. + /// + /// ZINTER numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] + /// https://valkey.io/commands/zinter/ + zinter: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; + + /// The members, and their combined scores, in all of the sorted sets. + /// + /// See also `zinter`. + /// + /// ZINTER numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] WITHSCORES + /// https://valkey.io/commands/zinter/ + zinter-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; + + /// The number of members in all of the sorted sets, counting stops at the limit. + /// + /// ZINTERCARD numkeys key [ key ... ] [ LIMIT limit ] + /// https://valkey.io/commands/zintercard/ + zintercard: async func(connection: borrow, keys: list, limit: option) -> result; + + /// Stores the members in all of the sorted sets, with their combined scores, returning how many. + /// + /// ZINTERSTORE destination numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] + /// https://valkey.io/commands/zinterstore/ + zinterstore: async func(connection: borrow, destination: string, keys: list, weights: option>, aggregate: option) -> result; + + /// The number of members of the sorted set stored at key in the range, for members of equal + /// score. + /// + /// ZLEXCOUNT key min max + /// https://valkey.io/commands/zlexcount/ + zlexcount: async func(connection: borrow, key: string, min: lex-bound, max: lex-bound) -> result; + + /// Removes the members with the lowest or highest scores from the first non-empty sorted set, + /// returning its key and the members and their scores, none when every sorted set is empty. + /// + /// ZMPOP numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ] + /// https://valkey.io/commands/zmpop/ + zmpop: async func(connection: borrow, keys: list, end: pop-end, count: option) -> result>>>, error>; + + /// The score of each of the members of the sorted set stored at key, none for a member that is + /// not in it. + /// + /// ZMSCORE key member [ member ... ] + /// https://valkey.io/commands/zmscore/ + zmscore: async func(connection: borrow, key: string, members: list) -> result>, error>; + + /// Removes and returns the members with the highest scores, and their scores, up to the count, + /// one without a count. + /// + /// ZPOPMAX key [ count ] + /// https://valkey.io/commands/zpopmax/ + zpopmax: async func(connection: borrow, key: string, count: option) -> result>, error>; + + /// Removes and returns the members with the lowest scores, and their scores, up to the count, + /// one without a count. + /// + /// ZPOPMIN key [ count ] + /// https://valkey.io/commands/zpopmin/ + zpopmin: async func(connection: borrow, key: string, count: option) -> result>, error>; + + /// Random members of the sorted set stored at key, up to the count. + /// + /// Without a count, one member, or none when the key does not exist. A negative count may return + /// the same member more than once, its absolute value is the number of members returned. + /// + /// See also `zrandmember-withscores`. + /// + /// ZRANDMEMBER key [ count ] + /// https://valkey.io/commands/zrandmember/ + zrandmember: async func(connection: borrow, key: string, count: option) -> result, error>; + + /// Random members of the sorted set stored at key, and their scores, up to the count. + /// + /// A negative count may return the same member more than once, its absolute value is the number + /// of members returned. + /// + /// See also `zrandmember`. + /// + /// ZRANDMEMBER key count WITHSCORES + /// https://valkey.io/commands/zrandmember/ + zrandmember-withscores: async func(connection: borrow, key: string, count: integer) -> result>, error>; + + /// The members of the sorted set stored at key in the range, from the lowest score, or the + /// highest with `rev`. + /// + /// The limit is a range of the members in the range, only with BYSCORE or BYLEX. With `xx`, + /// returns none when the key does not exist. + /// + /// Also covers the deprecated ZRANGEBYLEX, ZRANGEBYSCORE, ZREVRANGE, ZREVRANGEBYLEX and + /// ZREVRANGEBYSCORE. See also `zrange-withscores`. + /// + /// ZRANGE key start stop [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ] [ XX ] + /// https://valkey.io/commands/zrange/ + zrange: async func(connection: borrow, key: string, range: zrange-by, rev: bool, limit: option, xx: bool) -> result>, error>; + + /// The members of the sorted set stored at key in the range, and their scores, from the lowest + /// score, or the highest with `rev`. + /// + /// The limit is a range of the members in the range, only with BYSCORE. With `xx`, returns none + /// when the key does not exist. + /// + /// See also `zrange`. + /// + /// ZRANGE key start stop [ BYSCORE ] [ REV ] [ LIMIT offset count ] WITHSCORES [ XX ] + /// https://valkey.io/commands/zrange/ + zrange-withscores: async func(connection: borrow, key: string, range: zrange-withscores-by, rev: bool, limit: option, xx: bool) -> result>>, error>; + + /// Stores the members of the sorted set stored at source in the range, returning how many. + /// + /// The limit is a range of the members in the range, only with BYSCORE or BYLEX. + /// + /// ZRANGESTORE dst src min max [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ] + /// https://valkey.io/commands/zrangestore/ + zrangestore: async func(connection: borrow, destination: string, source: string, range: zrange-by, rev: bool, limit: option) -> result; + + /// The position of the member in the sorted set stored at key, from the lowest score, none when + /// it is not a member. + /// + /// See also `zrank-withscore` and `zrevrank`. + /// + /// ZRANK key member + /// https://valkey.io/commands/zrank/ + zrank: async func(connection: borrow, key: string, member: string) -> result, error>; + + /// The position, and the score, of the member in the sorted set stored at key, from the lowest + /// score, none when it is not a member. + /// + /// See also `zrank`. + /// + /// ZRANK key member WITHSCORE + /// https://valkey.io/commands/zrank/ + zrank-withscore: async func(connection: borrow, key: string, member: string) -> result>, error>; + + /// Removes the members from the sorted set stored at key, returning how many were members. + /// + /// ZREM key member [ member ... ] + /// https://valkey.io/commands/zrem/ + zrem: async func(connection: borrow, key: string, members: list) -> result; + + /// Removes the members of the sorted set stored at key in the range, for members of equal score, + /// returning how many. + /// + /// ZREMRANGEBYLEX key min max + /// https://valkey.io/commands/zremrangebylex/ + zremrangebylex: async func(connection: borrow, key: string, min: lex-bound, max: lex-bound) -> result; + + /// Removes the members of the sorted set stored at key at the positions, both included, + /// negative from the end, returning how many. + /// + /// ZREMRANGEBYRANK key start stop + /// https://valkey.io/commands/zremrangebyrank/ + zremrangebyrank: async func(connection: borrow, key: string, start: integer, stop: integer) -> result; + + /// Removes the members of the sorted set stored at key with scores in the range, returning how + /// many. + /// + /// ZREMRANGEBYSCORE key min max + /// https://valkey.io/commands/zremrangebyscore/ + zremrangebyscore: async func(connection: borrow, key: string, min: score-bound, max: score-bound) -> result; + + /// The position of the member in the sorted set stored at key, from the highest score, none + /// when it is not a member. + /// + /// See also `zrevrank-withscore`. + /// + /// ZREVRANK key member + /// https://valkey.io/commands/zrevrank/ + zrevrank: async func(connection: borrow, key: string, member: string) -> result, error>; + + /// The position, and the score, of the member in the sorted set stored at key, from the highest + /// score, none when it is not a member. + /// + /// See also `zrevrank`. + /// + /// ZREVRANK key member WITHSCORE + /// https://valkey.io/commands/zrevrank/ + zrevrank-withscore: async func(connection: borrow, key: string, member: string) -> result>, error>; + + /// Incrementally iterate over the members of the sorted set stored at key, and their scores. + /// + /// See also `zscan-noscores`. + /// + /// ZSCAN key cursor [ MATCH pattern ] [ COUNT count ] + /// https://valkey.io/commands/zscan/ + zscan: async func(connection: borrow, key: string, cursor: option, match: option, count: option) -> result>>, error>; + + /// Incrementally iterate over the members of the sorted set stored at key. + /// + /// See also `zscan`. + /// + /// ZSCAN key cursor [ MATCH pattern ] [ COUNT count ] NOSCORES + /// https://valkey.io/commands/zscan/ + zscan-noscores: async func(connection: borrow, key: string, cursor: option, match: option, count: option) -> result>, error>; + + /// The score of the member in the sorted set stored at key, none when it is not a member. + /// + /// ZSCORE key member + /// https://valkey.io/commands/zscore/ + zscore: async func(connection: borrow, key: string, member: string) -> result, error>; + + /// The members in any of the sorted sets. + /// + /// The weights, when given, multiply the scores of each sorted set, one for each key. + /// + /// See also `zunion-withscores`. + /// + /// ZUNION numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] + /// https://valkey.io/commands/zunion/ + zunion: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; + + /// The members, and their combined scores, in any of the sorted sets. + /// + /// See also `zunion`. + /// + /// ZUNION numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] WITHSCORES + /// https://valkey.io/commands/zunion/ + zunion-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; + + /// Stores the members in any of the sorted sets, with their combined scores, returning how many. + /// + /// ZUNIONSTORE destination numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] + /// https://valkey.io/commands/zunionstore/ + zunionstore: async func(connection: borrow, destination: string, keys: list, weights: option>, aggregate: option) -> result; // stream commands diff --git a/wit/connection.wit b/wit/connection.wit index 23a7121..f6a3a0d 100644 --- a/wit/connection.wit +++ b/wit/connection.wit @@ -64,6 +64,8 @@ interface connection { /// SUBSCRIBE, PSUBSCRIBE, SSUBSCRIBE and their UNSUBSCRIBE counterparts have no reply, the /// server confirms them with push messages, see `pushes`. Their exchange resolves to null /// once the command is written. + /// + /// A command has at least one word, an empty command fails with a client error. exchange: async func(command: list) -> result; /// Sends the commands one after another, with no other command sent between them, and waits @@ -79,6 +81,8 @@ interface connection { /// The server still replies, the reply is discarded when it arrives and an error it reports /// is not returned. An implementation may instead ask the server not to reply, e.g. with /// `CLIENT REPLY SKIP`. + /// + /// A command has at least one word, an empty command fails with a client error. emit: async func(command: list) -> result<_, error>; /// Marks the keys to watch, EXEC aborts the next transaction when any of them changed. @@ -89,6 +93,8 @@ interface connection { /// /// WATCH key [ key ... ] /// https://valkey.io/commands/watch/ + /// + /// At least one key, no keys fails with a client error. watch: async func(keys: list) -> result<_, error>; /// Forgets all the watched keys. From 2e9a6807e38d2d771b86e40a316e5c1d569a63fc Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Mon, 12 Oct 2026 20:44:52 -0400 Subject: [PATCH 07/37] remaining hash commands Signed-off-by: Scott Andrews --- components/commands/src/command.rs | 29 +++- components/commands/src/lib.rs | 225 ++++++++++++++++++++++++++++- components/commands/src/value.rs | 48 +++++- wit/commands.wit | 128 ++++++++++++++-- 4 files changed, 408 insertions(+), 22 deletions(-) diff --git a/components/commands/src/command.rs b/components/commands/src/command.rs index e344d19..ddea1d7 100644 --- a/components/commands/src/command.rs +++ b/components/commands/src/command.rs @@ -1,8 +1,9 @@ use crate::exports::componentized::valkey::commands::{ - Aggregate, ChannelRule, CommandRule, Connection, DbRule, Duration, Error, GetexTtl, Instant, - KeyRule, LexBound, Limit, MsetexPrecondition, PopEnd, Rule, RuleSelector, ScoreBound, - SetPrecondition, SetTtl, SortOpts, SortOrder, StreamTrim, StreamTrimEfficiency, TtlMode, - UserRule, Value, XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, + Aggregate, ChannelRule, CommandRule, Connection, DbRule, Duration, Error, GetexTtl, + HsetexCondition, HsetexFieldCondition, Instant, KeyRule, LexBound, Limit, MsetexPrecondition, + PopEnd, Rule, RuleSelector, ScoreBound, SetPrecondition, SetTtl, SortOpts, SortOrder, + StreamTrim, StreamTrimEfficiency, TtlMode, UserRule, Value, XclaimOpts, ZaddComparison, + ZaddCondition, ZrangeBy, }; use crate::value::{FromValue, OrZeroId, ZERO_ID}; @@ -591,3 +592,23 @@ impl IntoCommand for SortOpts { .into_command() } } + +impl IntoCommand for HsetexCondition { + fn into_command(self) -> Vec { + match self { + HsetexCondition::Nx => "NX", + HsetexCondition::Xx => "XX", + } + .into_command() + } +} + +impl IntoCommand for HsetexFieldCondition { + fn into_command(self) -> Vec { + match self { + HsetexFieldCondition::Fnx => "FNX", + HsetexFieldCondition::Fxx => "FXX", + } + .into_command() + } +} diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index e46a13d..bdb1cdc 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -3,14 +3,15 @@ use crate::command::{ Seconds, Zrange, emit, query, }; use crate::exports::componentized::valkey::commands::{ - Aggregate, Connection, Cursor, DbRule, Double, Duration, Error, GetexTtl, Guest, Instant, + Aggregate, Connection, Cursor, DbRule, Double, Duration, Error, FieldExpiration, + FieldPersistResult, FieldTtl, GetexTtl, Guest, HsetexCondition, HsetexFieldCondition, Instant, Integer, LcsIdxReply, LexBound, Limit, MsetexPrecondition, MsetexTtl, PopEnd, Rule, RuleSelector, ScoreBound, SetPrecondition, SetTtl, SortOpts, StreamId, StreamTrim, TtlMode, TtlResult, UserRule, Value, XclaimOpts, XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, ZaddComparison, ZaddCondition, ZrangeBy, ZrangeWithscoresBy, }; -use crate::value::{FlatPairs, OrNever, OrZeroId, ScoredMembers}; +use crate::value::{FlatPairs, OrNever, OrZeroId, ScoredMembers, field_expiration, field_ttl}; use wit_bindgen::rt::Map; @@ -167,6 +168,226 @@ impl Guest for Commands { query(connection, ("HEXISTS", key, field)).await } + #[doc = " When each of the hash fields expires, to the second."] + #[doc = ""] + #[doc = " See also `hpexpiretime`."] + #[doc = ""] + #[doc = " HEXPIRETIME key FIELDS numfields field [ field ... ]"] + #[doc = " https://valkey.io/commands/hexpiretime/"] + #[allow(async_fn_in_trait)] + async fn hexpiretime( + connection: &Connection, + key: String, + fields: Vec, + ) -> Result, Error> { + let fields = NonEmpty::new("fields", fields)?; + let codes: Vec = query( + connection, + ("HEXPIRETIME", key, "FIELDS", fields.len(), fields), + ) + .await?; + codes + .into_iter() + .map(|code| field_expiration(code, false)) + .collect() + } + + #[doc = " The values of the hash fields, none for a field that does not exist, deleting the fields."] + #[doc = ""] + #[doc = " HGETDEL key FIELDS numfields field [ field ... ]"] + #[doc = " https://valkey.io/commands/hgetdel/"] + #[allow(async_fn_in_trait)] + async fn hgetdel( + connection: &Connection, + key: String, + fields: Vec, + ) -> Result>, Error> { + let fields = NonEmpty::new("fields", fields)?; + query(connection, ("HGETDEL", key, "FIELDS", fields.len(), fields)).await + } + + #[doc = " The values of the hash fields, none for a field that does not exist, optionally setting or"] + #[doc = " removing their expiration."] + #[doc = ""] + #[doc = " HGETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ] FIELDS numfields field [ field ... ]"] + #[doc = " https://valkey.io/commands/hgetex/"] + #[allow(async_fn_in_trait)] + async fn hgetex( + connection: &Connection, + key: String, + ttl: Option, + fields: Vec, + ) -> Result>, Error> { + let fields = NonEmpty::new("fields", fields)?; + query( + connection, + ("HGETEX", key, ttl, "FIELDS", fields.len(), fields), + ) + .await + } + + #[doc = " Removes the expiration of the hash fields."] + #[doc = ""] + #[doc = " HPERSIST key FIELDS numfields field [ field ... ]"] + #[doc = " https://valkey.io/commands/hpersist/"] + #[allow(async_fn_in_trait)] + async fn hpersist( + connection: &Connection, + key: String, + fields: Vec, + ) -> Result, Error> { + let fields = NonEmpty::new("fields", fields)?; + query( + connection, + ("HPERSIST", key, "FIELDS", fields.len(), fields), + ) + .await + } + + #[doc = " Manipulate the expiration time for existing hash fields, in milliseconds."] + #[doc = ""] + #[doc = " HPEXPIRE key milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ]"] + #[doc = " https://valkey.io/commands/hpexpire/"] + #[allow(async_fn_in_trait)] + async fn hpexpire( + connection: &Connection, + key: String, + milliseconds: Duration, + mode: Option, + fields: Vec, + ) -> Result, Error> { + let fields = NonEmpty::new("fields", fields)?; + let command = ( + "HPEXPIRE", + key, + Milliseconds(milliseconds), + mode, + "FIELDS", + fields.len(), + fields, + ); + query(connection, command).await + } + + #[doc = " Manipulate the expiration time for existing hash fields, as a Unix time in milliseconds."] + #[doc = ""] + #[doc = " HPEXPIREAT key unix-time-milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ]"] + #[doc = " https://valkey.io/commands/hpexpireat/"] + #[allow(async_fn_in_trait)] + async fn hpexpireat( + connection: &Connection, + key: String, + milliseconds: Instant, + mode: Option, + fields: Vec, + ) -> Result, Error> { + let fields = NonEmpty::new("fields", fields)?; + let command = ( + "HPEXPIREAT", + key, + Milliseconds(milliseconds), + mode, + "FIELDS", + fields.len(), + fields, + ); + query(connection, command).await + } + + #[doc = " When each of the hash fields expires, to the millisecond."] + #[doc = ""] + #[doc = " See also `hexpiretime`."] + #[doc = ""] + #[doc = " HPEXPIRETIME key FIELDS numfields field [ field ... ]"] + #[doc = " https://valkey.io/commands/hpexpiretime/"] + #[allow(async_fn_in_trait)] + async fn hpexpiretime( + connection: &Connection, + key: String, + fields: Vec, + ) -> Result, Error> { + let fields = NonEmpty::new("fields", fields)?; + let codes: Vec = query( + connection, + ("HPEXPIRETIME", key, "FIELDS", fields.len(), fields), + ) + .await?; + codes + .into_iter() + .map(|code| field_expiration(code, true)) + .collect() + } + + #[doc = " The remaining time to live of each of the hash fields, to the millisecond."] + #[doc = ""] + #[doc = " See also `httl`."] + #[doc = ""] + #[doc = " HPTTL key FIELDS numfields field [ field ... ]"] + #[doc = " https://valkey.io/commands/hpttl/"] + #[allow(async_fn_in_trait)] + async fn hpttl( + connection: &Connection, + key: String, + fields: Vec, + ) -> Result, Error> { + let fields = NonEmpty::new("fields", fields)?; + let codes: Vec = + query(connection, ("HPTTL", key, "FIELDS", fields.len(), fields)).await?; + codes + .into_iter() + .map(|code| field_ttl(code, 1_000_000)) + .collect() + } + + #[doc = " Sets the fields to their values in the hash stored at key, optionally setting their"] + #[doc = " expiration, returning whether they were set, false when a condition prevented it."] + #[doc = ""] + #[doc = " HSETEX key [ NX | XX ] [ FNX | FXX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] FIELDS numfields field value [ field value ... ]"] + #[doc = " https://valkey.io/commands/hsetex/"] + #[allow(async_fn_in_trait)] + async fn hsetex( + connection: &Connection, + key: String, + condition: Option, + field_condition: Option, + ttl: Option, + field_and_values: Vec<(String, String)>, + ) -> Result { + let field_and_values = NonEmpty::new("field-and-values", field_and_values)?; + let command = ( + "HSETEX", + key, + condition, + field_condition, + ttl, + "FIELDS", + field_and_values.len(), + field_and_values, + ); + query(connection, command).await + } + + #[doc = " The remaining time to live of each of the hash fields, to the second."] + #[doc = ""] + #[doc = " See also `hpttl`."] + #[doc = ""] + #[doc = " HTTL key FIELDS numfields field [ field ... ]"] + #[doc = " https://valkey.io/commands/httl/"] + #[allow(async_fn_in_trait)] + async fn httl( + connection: &Connection, + key: String, + fields: Vec, + ) -> Result, Error> { + let fields = NonEmpty::new("fields", fields)?; + let codes: Vec = + query(connection, ("HTTL", key, "FIELDS", fields.len(), fields)).await?; + codes + .into_iter() + .map(|code| field_ttl(code, 1_000_000_000)) + .collect() + } + #[doc = " Manipulate the expiration time for existing hash fields."] #[doc = ""] #[doc = " HEXPIRE key seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ]"] diff --git a/components/commands/src/value.rs b/components/commands/src/value.rs index 719ccc2..8a882ea 100644 --- a/components/commands/src/value.rs +++ b/components/commands/src/value.rs @@ -1,8 +1,8 @@ use crate::exports::componentized::valkey::commands::{ - Double, Duration, Error, Instant, Integer, LcsIdxMatch, LcsIdxReply, StreamId, TtlResult, - Value, XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, - XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, XpendingExtendedEntry, - XpendingSummary, + Double, Duration, Error, FieldExpiration, FieldPersistResult, FieldTtl, Instant, Integer, + LcsIdxMatch, LcsIdxReply, StreamId, TtlResult, Value, XinfoConsumer, XinfoGroup, + XinfoStreamReply, XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, + XinfoStreamReplyFullGroup, XpendingExtendedEntry, XpendingSummary, }; use resp_codec::NestedValue; @@ -304,6 +304,46 @@ impl FromValue for FlatPairs { } } +/// `-2` when the field does not exist, `-1` when it has no expiration, otherwise the time to live in +/// the unit, as nanoseconds, e.g. seconds for HTTL. +pub fn field_ttl(code: Integer, unit: u64) -> Result { + match code { + -2 => Ok(FieldTtl::DoesNotExist), + -1 => Ok(FieldTtl::NoExpiration), + time if time >= 0 => Ok(FieldTtl::ExpiresIn((time as u64).saturating_mul(unit))), + code => Err(Error::Client(format!("unexpected reply: {code}"))), + } +} + +/// `-2` when the field does not exist, `-1` when it has no expiration, otherwise when it expires +/// as a Unix time, in seconds or milliseconds. +pub fn field_expiration(code: Integer, milliseconds: bool) -> Result { + match code { + -2 => Ok(FieldExpiration::DoesNotExist), + -1 => Ok(FieldExpiration::NoExpiration), + time if time >= 0 && milliseconds => Ok(FieldExpiration::ExpiresAt(Instant { + seconds: time / 1_000, + nanoseconds: (time % 1_000) as u32 * 1_000_000, + })), + time if time >= 0 => Ok(FieldExpiration::ExpiresAt(Instant { + seconds: time, + nanoseconds: 0, + })), + code => Err(Error::Client(format!("unexpected reply: {code}"))), + } +} + +impl FromValue for FieldPersistResult { + fn from_value(value: Value) -> Result { + match Integer::from_value(value)? { + -2 => Ok(Self::DoesNotExist), + -1 => Ok(Self::NoExpiration), + 1 => Ok(Self::Removed), + code => Err(Error::Client(format!("unexpected reply: {code}"))), + } + } +} + impl FromValue for TtlResult { fn from_value(value: Value) -> Result { match Integer::from_value(value)? { diff --git a/wit/commands.wit b/wit/commands.wit index 704b5a2..1bfd800 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -359,6 +359,34 @@ interface commands { /// https://valkey.io/commands/hexpireat/ hexpireat: async func(connection: borrow, key: string, seconds: instant, mode: option, fields: list) -> result, error>; + /// The remaining time to live of a hash field. + variant field-ttl { + /// -2: the field does not exist, or the hash does not exist. + does-not-exist, + /// -1: the field exists, but has no expiration. + no-expiration, + /// The time until the field expires. + expires-in(duration), + } + + /// When a hash field expires. + variant field-expiration { + /// -2: the field does not exist, or the hash does not exist. + does-not-exist, + /// -1: the field exists, but has no expiration. + no-expiration, + /// The time at which the field expires. + expires-at(instant), + } + + /// When each of the hash fields expires, to the second. + /// + /// See also `hpexpiretime`. + /// + /// HEXPIRETIME key FIELDS numfields field [ field ... ] + /// https://valkey.io/commands/hexpiretime/ + hexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; + /// Returns the value associated with field in the hash stored at key. /// /// HGET key field @@ -371,6 +399,19 @@ interface commands { /// https://valkey.io/commands/hgetall/ hgetall: async func(connection: borrow, key: string) -> result, error>; + /// The values of the hash fields, none for a field that does not exist, deleting the fields. + /// + /// HGETDEL key FIELDS numfields field [ field ... ] + /// https://valkey.io/commands/hgetdel/ + hgetdel: async func(connection: borrow, key: string, fields: list) -> result>, error>; + + /// The values of the hash fields, none for a field that does not exist, optionally setting or + /// removing their expiration. + /// + /// HGETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ] FIELDS numfields field [ field ... ] + /// https://valkey.io/commands/hgetex/ + hgetex: async func(connection: borrow, key: string, ttl: option, fields: list) -> result>, error>; + /// Increments the number stored at field in the hash stored at key by increment. /// /// HINCRBY key field increment @@ -407,6 +448,50 @@ interface commands { /// https://valkey.io/commands/hmset/ hmset: async func(connection: borrow, key: string, field-and-values: list>) -> result<_, error>; + /// The outcome of removing the expiration of a hash field. + enum field-persist-result { + /// -2: the field does not exist, or the hash does not exist. + does-not-exist, + /// -1: the field exists, but has no expiration. + no-expiration, + /// 1: the expiration was removed. + removed, + } + + /// Removes the expiration of the hash fields. + /// + /// HPERSIST key FIELDS numfields field [ field ... ] + /// https://valkey.io/commands/hpersist/ + hpersist: async func(connection: borrow, key: string, fields: list) -> result, error>; + + /// Manipulate the expiration time for existing hash fields, in milliseconds. + /// + /// HPEXPIRE key milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] + /// https://valkey.io/commands/hpexpire/ + hpexpire: async func(connection: borrow, key: string, milliseconds: duration, mode: option, fields: list) -> result, error>; + + /// Manipulate the expiration time for existing hash fields, as a Unix time in milliseconds. + /// + /// HPEXPIREAT key unix-time-milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] + /// https://valkey.io/commands/hpexpireat/ + hpexpireat: async func(connection: borrow, key: string, milliseconds: instant, mode: option, fields: list) -> result, error>; + + /// When each of the hash fields expires, to the millisecond. + /// + /// See also `hexpiretime`. + /// + /// HPEXPIRETIME key FIELDS numfields field [ field ... ] + /// https://valkey.io/commands/hpexpiretime/ + hpexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; + + /// The remaining time to live of each of the hash fields, to the millisecond. + /// + /// See also `httl`. + /// + /// HPTTL key FIELDS numfields field [ field ... ] + /// https://valkey.io/commands/hpttl/ + hpttl: async func(connection: borrow, key: string, fields: list) -> result, error>; + /// Random fields from the hash value stored at key, up to the count. /// /// Without a count, one field, or none when the key does not exist. A negative count may return @@ -456,6 +541,29 @@ interface commands { /// https://valkey.io/commands/hset/ hset: async func(connection: borrow, key: string, field-and-values: list>) -> result; + /// Only set the fields when the hash does, or does not, exist, HSETEX's `NX` and `XX`. + enum hsetex-condition { + /// Only set the fields when the hash does not exist. + nx, + /// Only set the fields when the hash exists. + xx, + } + + /// Only set the fields when none, or all, of them exist, HSETEX's `FNX` and `FXX`. + enum hsetex-field-condition { + /// Only set the fields when none of them exist. + fnx, + /// Only set the fields when all of them exist. + fxx, + } + + /// Sets the fields to their values in the hash stored at key, optionally setting their + /// expiration, returning whether they were set, false when a condition prevented it. + /// + /// HSETEX key [ NX | XX ] [ FNX | FXX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] FIELDS numfields field value [ field value ... ] + /// https://valkey.io/commands/hsetex/ + hsetex: async func(connection: borrow, key: string, condition: option, field-condition: option, ttl: option, field-and-values: list>) -> result; + /// Sets field in the hash stored at key to value, only if field does not yet exist. /// /// HSETNX key field value @@ -468,24 +576,20 @@ interface commands { /// https://valkey.io/commands/hstrlen/ hstrlen: async func(connection: borrow, key: string, field: string) -> result; + /// The remaining time to live of each of the hash fields, to the second. + /// + /// See also `hpttl`. + /// + /// HTTL key FIELDS numfields field [ field ... ] + /// https://valkey.io/commands/httl/ + httl: async func(connection: borrow, key: string, fields: list) -> result, error>; + /// All values in the hash stored at key. /// /// HVALS key /// https://valkey.io/commands/hvals/ hvals: async func(connection: borrow, key: string) -> result, error>; - // not yet implemented, from the @hash category: - // HEXPIRETIME - // HGETDEL - // HGETEX - // HPERSIST - // HPEXPIRE - // HPEXPIREAT - // HPEXPIRETIME - // HPTTL - // HSETEX - // HTTL - // hyperlog commands From 7a2c0315e031354164b3a308df1ac01286bee6d6 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Mon, 12 Oct 2026 21:18:08 -0400 Subject: [PATCH 08/37] Use dedicated time types Signed-off-by: Scott Andrews --- components/as-keyvalue/src/lib.rs | 7 +- components/as-keyvalue/tests/keyvalue.rs | 4 +- components/client/client.wac | 2 +- components/commands/src/command.rs | 90 ++++++--------- components/commands/src/lib.rs | 118 ++++++++------------ components/commands/src/value.rs | 135 +++++++++-------------- wit/commands.wit | 129 ++++++++++++++-------- 7 files changed, 226 insertions(+), 259 deletions(-) diff --git a/components/as-keyvalue/src/lib.rs b/components/as-keyvalue/src/lib.rs index d45ae34..8e610b8 100644 --- a/components/as-keyvalue/src/lib.rs +++ b/components/as-keyvalue/src/lib.rs @@ -1,4 +1,4 @@ -use componentized::valkey::commands::{self, Duration}; +use componentized::valkey::commands::{self, DurationMilliseconds}; use componentized::valkey::connection::{Connection, Error as ValkeyError}; use componentized::valkey::connector; use exports::wasmcloud::keyvalue::atomics::Guest as AtomicsGuest; @@ -77,9 +77,8 @@ impl GuestBucket for KeyvalueToValkeyBucket { .await?; } if let Some(ttl_ms) = options.ttl_ms { - // HEXPIRE has whole second precision, rounded up so a short ttl does not expire at once - let ttl: Duration = ttl_ms.div_ceil(1000) * 1_000_000_000; - commands::hexpire( + let ttl = DurationMilliseconds { px: ttl_ms }; + commands::hpexpire( &self.connection, self.hash_key.clone(), ttl, diff --git a/components/as-keyvalue/tests/keyvalue.rs b/components/as-keyvalue/tests/keyvalue.rs index 4722d86..ebb3c2a 100644 --- a/components/as-keyvalue/tests/keyvalue.rs +++ b/components/as-keyvalue/tests/keyvalue.rs @@ -108,7 +108,7 @@ mod with_valkey { vec![Some((s("x"), b("1"))), None] ); - // a ttl under a second is rounded up to one, the precision of HEXPIRE + // a ttl to the millisecond, with HPEXPIRE buckets .call_set( accessor, @@ -126,7 +126,7 @@ mod with_valkey { .call_exists(accessor, bucket, s("expiring")) .await?? ); - tokio::time::sleep(Duration::from_millis(1500)).await; + tokio::time::sleep(Duration::from_millis(500)).await; assert!( !buckets .call_exists(accessor, bucket, s("expiring")) diff --git a/components/client/client.wac b/components/client/client.wac index c94b8c1..d368c93 100644 --- a/components/client/client.wac +++ b/components/client/client.wac @@ -25,7 +25,7 @@ let keyvalue = new local:as-keyvalue { "componentized:valkey/resp@0.3.0-dev": resp.resp, "componentized:valkey/connection@0.3.0-dev": connection.connection, "componentized:valkey/connector@0.3.0-dev": connector.connector, - "componentized:valkey/hash-commands@0.3.0-dev": commands["componentized:valkey/hash-commands@0.3.0-dev"], + "componentized:valkey/commands@0.3.0-dev": commands.commands, ... }; diff --git a/components/commands/src/command.rs b/components/commands/src/command.rs index ddea1d7..cde7596 100644 --- a/components/commands/src/command.rs +++ b/components/commands/src/command.rs @@ -1,9 +1,9 @@ use crate::exports::componentized::valkey::commands::{ - Aggregate, ChannelRule, CommandRule, Connection, DbRule, Duration, Error, GetexTtl, - HsetexCondition, HsetexFieldCondition, Instant, KeyRule, LexBound, Limit, MsetexPrecondition, - PopEnd, Rule, RuleSelector, ScoreBound, SetPrecondition, SetTtl, SortOpts, SortOrder, - StreamTrim, StreamTrimEfficiency, TtlMode, UserRule, Value, XclaimOpts, ZaddComparison, - ZaddCondition, ZrangeBy, + Aggregate, ChannelRule, CommandRule, Connection, DbRule, DurationMilliseconds, DurationSeconds, + Error, GetexTtl, HsetexCondition, HsetexFieldCondition, KeyRule, LexBound, Limit, + MsetexPrecondition, PopEnd, Rule, RuleSelector, ScoreBound, SetPrecondition, SetTtl, SortOpts, + SortOrder, StreamTrim, StreamTrimEfficiency, TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, + UserRule, Value, XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, }; use crate::value::{FromValue, OrZeroId, ZERO_ID}; @@ -55,8 +55,7 @@ impl IntoCommand for &String { } } -/// Numbers, without a unit, a `Duration` is a `u64` of nanoseconds, see `Seconds` and -/// `Milliseconds`. +/// Numbers, without a unit, see `DurationSeconds` and the other records for times. macro_rules! into_command_numbers { ($($number:ty),*) => { $( @@ -159,33 +158,31 @@ impl IntoCommand for OrZeroId { } } -/// A duration or instant in whole seconds, e.g. for EX and EXAT. -pub struct Seconds(pub T); - -/// A duration or instant in whole milliseconds, e.g. for PX and PXAT. -pub struct Milliseconds(pub T); - -impl IntoCommand for Seconds { +/// Whole seconds, e.g. for EX. +impl IntoCommand for DurationSeconds { fn into_command(self) -> Vec { - vec![duration_seconds(self.0)] + self.ex.into_command() } } -impl IntoCommand for Milliseconds { +/// Whole milliseconds, e.g. for PX. +impl IntoCommand for DurationMilliseconds { fn into_command(self) -> Vec { - vec![duration_milliseconds(self.0)] + self.px.into_command() } } -impl IntoCommand for Seconds { +/// Whole seconds since the Unix epoch, e.g. for EXAT. +impl IntoCommand for UnixTimeSeconds { fn into_command(self) -> Vec { - vec![instant_seconds(self.0)] + self.exat.into_command() } } -impl IntoCommand for Milliseconds { +/// Whole milliseconds since the Unix epoch, e.g. for PXAT. +impl IntoCommand for UnixTimeMilliseconds { fn into_command(self) -> Vec { - vec![instant_milliseconds(self.0)] + self.pxat.into_command() } } @@ -299,10 +296,10 @@ impl IntoCommand for TtlMode { impl IntoCommand for GetexTtl { fn into_command(self) -> Vec { match self { - GetexTtl::Ex(duration) => ("EX", Seconds(duration)).into_command(), - GetexTtl::Px(duration) => ("PX", Milliseconds(duration)).into_command(), - GetexTtl::Exat(instant) => ("EXAT", Seconds(instant)).into_command(), - GetexTtl::Pxat(instant) => ("PXAT", Milliseconds(instant)).into_command(), + GetexTtl::Ex(duration) => ("EX", duration).into_command(), + GetexTtl::Px(duration) => ("PX", duration).into_command(), + GetexTtl::Exat(unix_time) => ("EXAT", unix_time).into_command(), + GetexTtl::Pxat(unix_time) => ("PXAT", unix_time).into_command(), GetexTtl::Persist => "PERSIST".into_command(), } } @@ -312,10 +309,10 @@ impl IntoCommand for GetexTtl { impl IntoCommand for SetTtl { fn into_command(self) -> Vec { match self { - SetTtl::Ex(duration) => ("EX", Seconds(duration)).into_command(), - SetTtl::Px(duration) => ("PX", Milliseconds(duration)).into_command(), - SetTtl::Exat(instant) => ("EXAT", Seconds(instant)).into_command(), - SetTtl::Pxat(instant) => ("PXAT", Milliseconds(instant)).into_command(), + SetTtl::Ex(duration) => ("EX", duration).into_command(), + SetTtl::Px(duration) => ("PX", duration).into_command(), + SetTtl::Exat(unix_time) => ("EXAT", unix_time).into_command(), + SetTtl::Pxat(unix_time) => ("PXAT", unix_time).into_command(), SetTtl::Keepttl => "KEEPTTL".into_command(), } } @@ -374,32 +371,12 @@ impl IntoCommand for StreamTrimEfficiency { } } -/// A duration, in nanoseconds, as whole seconds, the precision of EX. -pub fn duration_seconds(nanoseconds: u64) -> String { - nanoseconds.div_ceil(1_000_000_000).to_string() -} - -/// A duration, in nanoseconds, as whole milliseconds, the precision of PX. -pub fn duration_milliseconds(nanoseconds: u64) -> String { - nanoseconds.div_ceil(1_000_000).to_string() -} - -/// An instant as seconds since the unix epoch, the precision of EXAT. -pub fn instant_seconds(instant: Instant) -> String { - (instant.seconds + instant.nanoseconds.div_ceil(1_000_000_000) as i64).to_string() -} - -/// An instant as milliseconds since the unix epoch, the precision of PXAT. -pub fn instant_milliseconds(instant: Instant) -> String { - ((instant.seconds * 1_000) + instant.nanoseconds.div_ceil(1_000_000) as i64).to_string() -} - impl IntoCommand for XclaimOpts { fn into_command(self) -> Vec { // [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] [ LASTID lastid ] ( - NamedOption("IDLE", self.idle.map(Milliseconds)), - NamedOption("TIME", self.time.map(Milliseconds)), + NamedOption("IDLE", self.idle), + NamedOption("TIME", self.time), NamedOption("RETRYCOUNT", self.retrycount), Flag("FORCE", self.force), NamedOption("LASTID", self.lastid), @@ -408,12 +385,17 @@ impl IntoCommand for XclaimOpts { } } -/// A duration in seconds with a fraction, e.g. the timeout of BZPOPMIN. -pub struct DecimalSeconds(pub Duration); +/// Milliseconds as seconds with a fraction, e.g. the timeout of BZPOPMIN, `1500` as `1.500`. +pub struct DecimalSeconds(pub DurationMilliseconds); impl IntoCommand for DecimalSeconds { fn into_command(self) -> Vec { - vec![(self.0 as f64 / 1_000_000_000.0).to_string()] + let milliseconds = self.0.px; + vec![format!( + "{}.{:03}", + milliseconds / 1_000, + milliseconds % 1_000 + )] } } diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index bdb1cdc..4963f78 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -1,17 +1,17 @@ use crate::command::{ - DecimalSeconds, Flag, LexRange, Milliseconds, NamedOption, NonEmpty, NumKeys, ScoreRange, - Seconds, Zrange, emit, query, + DecimalSeconds, Flag, LexRange, NamedOption, NonEmpty, NumKeys, ScoreRange, Zrange, emit, query, }; use crate::exports::componentized::valkey::commands::{ - Aggregate, Connection, Cursor, DbRule, Double, Duration, Error, FieldExpiration, - FieldPersistResult, FieldTtl, GetexTtl, Guest, HsetexCondition, HsetexFieldCondition, Instant, - Integer, LcsIdxReply, LexBound, Limit, MsetexPrecondition, MsetexTtl, PopEnd, Rule, - RuleSelector, ScoreBound, SetPrecondition, SetTtl, SortOpts, StreamId, StreamTrim, TtlMode, - TtlResult, UserRule, Value, XclaimOpts, XinfoConsumer, XinfoGroup, XinfoStreamReply, - XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, ZaddComparison, ZaddCondition, - ZrangeBy, ZrangeWithscoresBy, + Aggregate, Connection, Cursor, DbRule, Double, DurationMilliseconds, DurationSeconds, Error, + FieldExpiretime, FieldPersistResult, FieldPexpiretime, FieldPttl, FieldTtl, GetexTtl, Guest, + HsetexCondition, HsetexFieldCondition, Integer, LcsIdxReply, LexBound, Limit, + MsetexPrecondition, MsetexTtl, PopEnd, Rule, RuleSelector, ScoreBound, SetPrecondition, SetTtl, + SortOpts, StreamId, StreamTrim, TtlMode, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, + UserRule, Value, XclaimOpts, XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, + XpendingExtendedEntry, XpendingSummary, ZaddComparison, ZaddCondition, ZrangeBy, + ZrangeWithscoresBy, }; -use crate::value::{FlatPairs, OrNever, OrZeroId, ScoredMembers, field_expiration, field_ttl}; +use crate::value::{FlatPairs, OrNever, OrZeroId, ScoredMembers}; use wit_bindgen::rt::Map; @@ -179,17 +179,13 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result, Error> { + ) -> Result, Error> { let fields = NonEmpty::new("fields", fields)?; - let codes: Vec = query( + query( connection, ("HEXPIRETIME", key, "FIELDS", fields.len(), fields), ) - .await?; - codes - .into_iter() - .map(|code| field_expiration(code, false)) - .collect() + .await } #[doc = " The values of the hash fields, none for a field that does not exist, deleting the fields."] @@ -252,7 +248,7 @@ impl Guest for Commands { async fn hpexpire( connection: &Connection, key: String, - milliseconds: Duration, + milliseconds: DurationMilliseconds, mode: Option, fields: Vec, ) -> Result, Error> { @@ -260,7 +256,7 @@ impl Guest for Commands { let command = ( "HPEXPIRE", key, - Milliseconds(milliseconds), + milliseconds, mode, "FIELDS", fields.len(), @@ -277,7 +273,7 @@ impl Guest for Commands { async fn hpexpireat( connection: &Connection, key: String, - milliseconds: Instant, + milliseconds: UnixTimeMilliseconds, mode: Option, fields: Vec, ) -> Result, Error> { @@ -285,7 +281,7 @@ impl Guest for Commands { let command = ( "HPEXPIREAT", key, - Milliseconds(milliseconds), + milliseconds, mode, "FIELDS", fields.len(), @@ -305,17 +301,13 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result, Error> { + ) -> Result, Error> { let fields = NonEmpty::new("fields", fields)?; - let codes: Vec = query( + query( connection, ("HPEXPIRETIME", key, "FIELDS", fields.len(), fields), ) - .await?; - codes - .into_iter() - .map(|code| field_expiration(code, true)) - .collect() + .await } #[doc = " The remaining time to live of each of the hash fields, to the millisecond."] @@ -329,14 +321,9 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result, Error> { + ) -> Result, Error> { let fields = NonEmpty::new("fields", fields)?; - let codes: Vec = - query(connection, ("HPTTL", key, "FIELDS", fields.len(), fields)).await?; - codes - .into_iter() - .map(|code| field_ttl(code, 1_000_000)) - .collect() + query(connection, ("HPTTL", key, "FIELDS", fields.len(), fields)).await } #[doc = " Sets the fields to their values in the hash stored at key, optionally setting their"] @@ -380,12 +367,7 @@ impl Guest for Commands { fields: Vec, ) -> Result, Error> { let fields = NonEmpty::new("fields", fields)?; - let codes: Vec = - query(connection, ("HTTL", key, "FIELDS", fields.len(), fields)).await?; - codes - .into_iter() - .map(|code| field_ttl(code, 1_000_000_000)) - .collect() + query(connection, ("HTTL", key, "FIELDS", fields.len(), fields)).await } #[doc = " Manipulate the expiration time for existing hash fields."] @@ -396,7 +378,7 @@ impl Guest for Commands { async fn hexpire( connection: &Connection, key: String, - seconds: Duration, + seconds: DurationSeconds, mode: Option, fields: Vec, ) -> Result, Error> { @@ -406,7 +388,7 @@ impl Guest for Commands { ( "HEXPIRE", key, - Seconds(seconds), + seconds, mode, "FIELDS", fields.len(), @@ -424,7 +406,7 @@ impl Guest for Commands { async fn hexpireat( connection: &Connection, key: String, - seconds: Instant, + seconds: UnixTimeSeconds, mode: Option, fields: Vec, ) -> Result, Error> { @@ -434,7 +416,7 @@ impl Guest for Commands { ( "HEXPIREAT", key, - Seconds(seconds), + seconds, mode, "FIELDS", fields.len(), @@ -714,7 +696,7 @@ impl Guest for Commands { async fn expire( connection: &Connection, key: String, - seconds: Duration, + seconds: DurationSeconds, mode: Option, ) -> Result { query(connection, ("EXPIRE", key, seconds, mode)).await @@ -728,10 +710,10 @@ impl Guest for Commands { async fn expireat( connection: &Connection, key: String, - seconds: Instant, + seconds: UnixTimeSeconds, mode: Option, ) -> Result { - query(connection, ("EXPIREAT", key, Seconds(seconds), mode)).await + query(connection, ("EXPIREAT", key, seconds, mode)).await } #[doc = " Returns all keys matching pattern."] @@ -1156,14 +1138,10 @@ impl Guest for Commands { async fn psetex( connection: &Connection, key: String, - milliseconds: Duration, + milliseconds: DurationMilliseconds, value: String, ) -> Result<(), Error> { - query( - connection, - ("PSETEX", key, Milliseconds(milliseconds), value), - ) - .await + query(connection, ("PSETEX", key, milliseconds, value)).await } #[doc = " Set key to hold the string value."] @@ -1208,7 +1186,7 @@ impl Guest for Commands { async fn setex( connection: &Connection, key: String, - seconds: Duration, + seconds: DurationSeconds, value: String, ) -> Result<(), Error> { query(connection, ("SETEX", key, seconds, value)).await @@ -1315,7 +1293,7 @@ impl Guest for Commands { key: String, group: String, consumer: String, - min_idle_time: Duration, + min_idle_time: DurationMilliseconds, start: Option, count: Option, ) -> Result< @@ -1333,7 +1311,7 @@ impl Guest for Commands { key, group, consumer, - Milliseconds(min_idle_time), + min_idle_time, OrZeroId(start), NamedOption("COUNT", count), ), @@ -1354,7 +1332,7 @@ impl Guest for Commands { key: String, group: String, consumer: String, - min_idle_time: Duration, + min_idle_time: DurationMilliseconds, start: Option, count: Option, ) -> Result<(Option, Vec, Vec), Error> { @@ -1365,7 +1343,7 @@ impl Guest for Commands { key, group, consumer, - Milliseconds(min_idle_time), + min_idle_time, OrZeroId(start), NamedOption("COUNT", count), "JUSTID", @@ -1387,7 +1365,7 @@ impl Guest for Commands { key: String, group: String, consumer: String, - min_idle_time: Duration, + min_idle_time: DurationMilliseconds, ids: Vec, opts: Option, ) -> Result)>, Error> { @@ -1411,7 +1389,7 @@ impl Guest for Commands { key: String, group: String, consumer: String, - min_idle_time: Duration, + min_idle_time: DurationMilliseconds, ids: Vec, opts: Option, ) -> Result, Error> { @@ -1623,7 +1601,7 @@ impl Guest for Commands { connection: &Connection, key: String, group: String, - min_idle_time: Option, + min_idle_time: Option, start: StreamId, end: StreamId, count: Integer, @@ -1635,7 +1613,7 @@ impl Guest for Commands { "XPENDING", key, group, - NamedOption("IDLE", min_idle_time.map(Milliseconds)), + NamedOption("IDLE", min_idle_time), start, end, count, @@ -1672,7 +1650,7 @@ impl Guest for Commands { async fn xread( connection: &Connection, count: Option, - block: Option, + block: Option, keys: Vec, ids: Vec, ) -> Result)>>>, Error> { @@ -1683,7 +1661,7 @@ impl Guest for Commands { ( "XREAD", NamedOption("COUNT", count), - NamedOption("BLOCK", block.map(Milliseconds)), + NamedOption("BLOCK", block), "STREAMS", keys, ids, @@ -1705,7 +1683,7 @@ impl Guest for Commands { group: String, consumer: String, count: Option, - block: Option, + block: Option, noack: bool, keys: Vec, ids: Vec, @@ -1720,7 +1698,7 @@ impl Guest for Commands { group, consumer, NamedOption("COUNT", count), - NamedOption("BLOCK", block.map(Milliseconds)), + NamedOption("BLOCK", block), Flag("NOACK", noack), "STREAMS", keys, @@ -2113,7 +2091,7 @@ impl Guest for Commands { #[allow(async_fn_in_trait)] async fn bzmpop( connection: &Connection, - timeout: Duration, + timeout: DurationMilliseconds, keys: Vec, end: PopEnd, count: Option, @@ -2143,7 +2121,7 @@ impl Guest for Commands { async fn bzpopmax( connection: &Connection, keys: Vec, - timeout: Duration, + timeout: DurationMilliseconds, ) -> Result, Error> { let keys = NonEmpty::new("keys", keys)?; query(connection, ("BZPOPMAX", keys, DecimalSeconds(timeout))).await @@ -2163,7 +2141,7 @@ impl Guest for Commands { async fn bzpopmin( connection: &Connection, keys: Vec, - timeout: Duration, + timeout: DurationMilliseconds, ) -> Result, Error> { let keys = NonEmpty::new("keys", keys)?; query(connection, ("BZPOPMIN", keys, DecimalSeconds(timeout))).await diff --git a/components/commands/src/value.rs b/components/commands/src/value.rs index 8a882ea..abceeb4 100644 --- a/components/commands/src/value.rs +++ b/components/commands/src/value.rs @@ -1,8 +1,9 @@ use crate::exports::componentized::valkey::commands::{ - Double, Duration, Error, FieldExpiration, FieldPersistResult, FieldTtl, Instant, Integer, - LcsIdxMatch, LcsIdxReply, StreamId, TtlResult, Value, XinfoConsumer, XinfoGroup, - XinfoStreamReply, XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, - XinfoStreamReplyFullGroup, XpendingExtendedEntry, XpendingSummary, + Double, DurationMilliseconds, DurationSeconds, Error, FieldExpiretime, FieldPersistResult, + FieldPexpiretime, FieldPttl, FieldTtl, Integer, LcsIdxMatch, LcsIdxReply, StreamId, TtlResult, + UnixTimeMilliseconds, UnixTimeSeconds, Value, XinfoConsumer, XinfoGroup, XinfoStreamReply, + XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, + XpendingExtendedEntry, XpendingSummary, }; use resp_codec::NestedValue; @@ -158,46 +159,31 @@ impl Fields { } } -/// A duration or instant in whole seconds, e.g. for EX and EXAT. -pub struct Seconds(pub T); - -/// A duration or instant in whole milliseconds, e.g. for PX and PXAT. -pub struct Milliseconds(pub T); - -impl FromValue for Seconds { +/// Whole seconds, never negative. +impl FromValue for DurationSeconds { fn from_value(value: Value) -> Result { - let seconds = u64::from_value(value)?; - Ok(Seconds(seconds.saturating_mul(1_000_000_000))) + u64::from_value(value).map(|ex| DurationSeconds { ex }) } } -impl FromValue for Milliseconds { +/// Whole milliseconds, never negative. +impl FromValue for DurationMilliseconds { fn from_value(value: Value) -> Result { - let milliseconds = u64::from_value(value)?; - Ok(Milliseconds(milliseconds.saturating_mul(1_000_000))) + u64::from_value(value).map(|px| DurationMilliseconds { px }) } } -/// Seconds since the unix epoch. -impl FromValue for Seconds { +/// Whole seconds since the Unix epoch. +impl FromValue for UnixTimeSeconds { fn from_value(value: Value) -> Result { - let seconds = Integer::from_value(value)?; - Ok(Seconds(Instant { - seconds, - nanoseconds: 0, - })) + Integer::from_value(value).map(|exat| UnixTimeSeconds { exat }) } } -/// Milliseconds since the unix epoch. -impl FromValue for Milliseconds { +/// Whole milliseconds since the Unix epoch. +impl FromValue for UnixTimeMilliseconds { fn from_value(value: Value) -> Result { - let milliseconds = Integer::from_value(value)?; - // euclidean, so the nanoseconds are positive for an instant before the epoch - Ok(Milliseconds(Instant { - seconds: milliseconds.div_euclid(1_000), - nanoseconds: milliseconds.rem_euclid(1_000) as u32 * 1_000_000, - })) + Integer::from_value(value).map(|pxat| UnixTimeMilliseconds { pxat }) } } @@ -256,12 +242,11 @@ impl FromValue for XpendingSummary { /// `[id, consumer, milliseconds-since-delivery, delivery-count]`. impl FromValue for XpendingExtendedEntry { fn from_value(value: Value) -> Result { - let (id, name, since_delivery, delivery_count): (_, _, Milliseconds, _) = - FromValue::from_value(value)?; + let (id, name, since_delivery, delivery_count) = FromValue::from_value(value)?; Ok(Self { id, name, - since_delivery: since_delivery.0, + since_delivery, delivery_count, }) } @@ -304,34 +289,30 @@ impl FromValue for FlatPairs { } } -/// `-2` when the field does not exist, `-1` when it has no expiration, otherwise the time to live in -/// the unit, as nanoseconds, e.g. seconds for HTTL. -pub fn field_ttl(code: Integer, unit: u64) -> Result { - match code { - -2 => Ok(FieldTtl::DoesNotExist), - -1 => Ok(FieldTtl::NoExpiration), - time if time >= 0 => Ok(FieldTtl::ExpiresIn((time as u64).saturating_mul(unit))), - code => Err(Error::Client(format!("unexpected reply: {code}"))), - } +/// The codes of HTTL and the like, `-2` when the field does not exist, `-1` when it has no +/// expiration, otherwise the time. +macro_rules! from_value_field_times { + ($($variant:ident($time:ident) => $case:ident),*) => { + $( + impl FromValue for $variant { + fn from_value(value: Value) -> Result { + match value { + Value::Integer(-2) => Ok($variant::DoesNotExist), + Value::Integer(-1) => Ok($variant::NoExpiration), + value => $time::from_value(value).map($variant::$case), + } + } + } + )* + }; } -/// `-2` when the field does not exist, `-1` when it has no expiration, otherwise when it expires -/// as a Unix time, in seconds or milliseconds. -pub fn field_expiration(code: Integer, milliseconds: bool) -> Result { - match code { - -2 => Ok(FieldExpiration::DoesNotExist), - -1 => Ok(FieldExpiration::NoExpiration), - time if time >= 0 && milliseconds => Ok(FieldExpiration::ExpiresAt(Instant { - seconds: time / 1_000, - nanoseconds: (time % 1_000) as u32 * 1_000_000, - })), - time if time >= 0 => Ok(FieldExpiration::ExpiresAt(Instant { - seconds: time, - nanoseconds: 0, - })), - code => Err(Error::Client(format!("unexpected reply: {code}"))), - } -} +from_value_field_times!( + FieldTtl(DurationSeconds) => ExpiresIn, + FieldPttl(DurationMilliseconds) => ExpiresIn, + FieldExpiretime(UnixTimeSeconds) => ExpiresAt, + FieldPexpiretime(UnixTimeMilliseconds) => ExpiresAt +); impl FromValue for FieldPersistResult { fn from_value(value: Value) -> Result { @@ -389,13 +370,13 @@ impl FromValue for LcsIdxMatch { impl FromValue for XinfoConsumer { fn from_value(value: Value) -> Result { let mut fields = Fields::from_value(value)?; - let idle: Milliseconds = fields.required("idle")?; - let inactive: OrNever> = fields.required("inactive")?; Ok(Self { name: fields.required("name")?, pending: fields.required("pending")?, - idle: idle.0, - inactive: inactive.0.map(|inactive| inactive.0), + idle: fields.required("idle")?, + inactive: fields + .required::>("inactive")? + .0, }) } } @@ -450,21 +431,13 @@ impl FromValue for XinfoStreamReplyFull { impl FromValue for XinfoStreamReplyFullGroup { fn from_value(value: Value) -> Result { let mut fields = Fields::from_value(value)?; - // the entry, its consumer, when it was last delivered and how often - let pending: Vec<(StreamId, String, Milliseconds, Integer)> = - fields.required("pending")?; Ok(Self { name: fields.required("name")?, last_delivered_id: fields.required("last-delivered-id")?, entries_read: fields.required("entries-read")?, lag: fields.required("lag")?, pel_count: fields.required("pel-count")?, - pending: pending - .into_iter() - .map(|(id, consumer, delivered, deliveries)| { - (id, consumer, delivered.0, deliveries) - }) - .collect(), + pending: fields.required("pending")?, consumers: fields.required("consumers")?, }) } @@ -473,20 +446,14 @@ impl FromValue for XinfoStreamReplyFullGroup { impl FromValue for XinfoStreamReplyFullConsumer { fn from_value(value: Value) -> Result { let mut fields = Fields::from_value(value)?; - let seen_time: Milliseconds = fields.required("seen-time")?; - let active_time: OrNever> = fields.required("active-time")?; - // the entry, when it was last delivered and how often - let pending: Vec<(StreamId, Milliseconds, Integer)> = - fields.required("pending")?; Ok(Self { name: fields.required("name")?, - seen_time: seen_time.0, - active_time: active_time.0.map(|active_time| active_time.0), + seen_time: fields.required("seen-time")?, + active_time: fields + .required::>("active-time")? + .0, pel_count: fields.required("pel-count")?, - pending: pending - .into_iter() - .map(|(id, delivered, deliveries)| (id, delivered.0, deliveries)) - .collect(), + pending: fields.required("pending")?, }) } } diff --git a/wit/commands.wit b/wit/commands.wit index 1bfd800..fa31fcb 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -4,7 +4,28 @@ /// client error before anything is sent. A list the command allows to be left out is an option, /// none rather than empty. interface commands { - use connection.{connection, double, duration, instant, integer, value, error}; + use connection.{connection, double, integer, value, error}; + + /// A duration in whole seconds, e.g. `EX seconds`. + record duration-seconds { + ex: u64, + } + + /// A duration in whole milliseconds, e.g. `PX milliseconds`. + record duration-milliseconds { + px: u64, + } + + /// A point in time as whole seconds since the Unix epoch, e.g. `EXAT unix-time-seconds`. + record unix-time-seconds { + exat: s64, + } + + /// A point in time as whole milliseconds since the Unix epoch, e.g. `PXAT + /// unix-time-milliseconds`. + record unix-time-milliseconds { + pxat: s64, + } /// XXXXXX /// @@ -351,32 +372,52 @@ interface commands { /// /// HEXPIRE key seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpire/ - hexpire: async func(connection: borrow, key: string, seconds: duration, mode: option, fields: list) -> result, error>; + hexpire: async func(connection: borrow, key: string, seconds: duration-seconds, mode: option, fields: list) -> result, error>; /// Manipulate the expiration time for existing hash fields. /// /// HEXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpireat/ - hexpireat: async func(connection: borrow, key: string, seconds: instant, mode: option, fields: list) -> result, error>; + hexpireat: async func(connection: borrow, key: string, seconds: unix-time-seconds, mode: option, fields: list) -> result, error>; - /// The remaining time to live of a hash field. + /// The remaining time to live of a hash field, to the second, see `httl`. variant field-ttl { /// -2: the field does not exist, or the hash does not exist. does-not-exist, /// -1: the field exists, but has no expiration. no-expiration, /// The time until the field expires. - expires-in(duration), + expires-in(duration-seconds), + } + + /// The remaining time to live of a hash field, to the millisecond, see `hpttl`. + variant field-pttl { + /// -2: the field does not exist, or the hash does not exist. + does-not-exist, + /// -1: the field exists, but has no expiration. + no-expiration, + /// The time until the field expires. + expires-in(duration-milliseconds), + } + + /// When a hash field expires, to the second, see `hexpiretime`. + variant field-expiretime { + /// -2: the field does not exist, or the hash does not exist. + does-not-exist, + /// -1: the field exists, but has no expiration. + no-expiration, + /// The time at which the field expires. + expires-at(unix-time-seconds), } - /// When a hash field expires. - variant field-expiration { + /// When a hash field expires, to the millisecond, see `hpexpiretime`. + variant field-pexpiretime { /// -2: the field does not exist, or the hash does not exist. does-not-exist, /// -1: the field exists, but has no expiration. no-expiration, /// The time at which the field expires. - expires-at(instant), + expires-at(unix-time-milliseconds), } /// When each of the hash fields expires, to the second. @@ -385,7 +426,7 @@ interface commands { /// /// HEXPIRETIME key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpiretime/ - hexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; + hexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; /// Returns the value associated with field in the hash stored at key. /// @@ -468,13 +509,13 @@ interface commands { /// /// HPEXPIRE key milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpire/ - hpexpire: async func(connection: borrow, key: string, milliseconds: duration, mode: option, fields: list) -> result, error>; + hpexpire: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, mode: option, fields: list) -> result, error>; /// Manipulate the expiration time for existing hash fields, as a Unix time in milliseconds. /// /// HPEXPIREAT key unix-time-milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpireat/ - hpexpireat: async func(connection: borrow, key: string, milliseconds: instant, mode: option, fields: list) -> result, error>; + hpexpireat: async func(connection: borrow, key: string, milliseconds: unix-time-milliseconds, mode: option, fields: list) -> result, error>; /// When each of the hash fields expires, to the millisecond. /// @@ -482,7 +523,7 @@ interface commands { /// /// HPEXPIRETIME key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpiretime/ - hpexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; + hpexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; /// The remaining time to live of each of the hash fields, to the millisecond. /// @@ -490,7 +531,7 @@ interface commands { /// /// HPTTL key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpttl/ - hpttl: async func(connection: borrow, key: string, fields: list) -> result, error>; + hpttl: async func(connection: borrow, key: string, fields: list) -> result, error>; /// Random fields from the hash value stored at key, up to the count. /// @@ -619,13 +660,13 @@ interface commands { /// /// EXPIRE key seconds [ NX | XX | GT | LT ] /// https://valkey.io/commands/expire/ - expire: async func(connection: borrow, key: string, seconds: duration, mode: option) -> result; + expire: async func(connection: borrow, key: string, seconds: duration-seconds, mode: option) -> result; /// Set a timeout on key. /// /// EXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] /// https://valkey.io/commands/expireat/ - expireat: async func(connection: borrow, key: string, seconds: instant, mode: option) -> result; + expireat: async func(connection: borrow, key: string, seconds: unix-time-seconds, mode: option) -> result; /// Returns all keys matching pattern. /// @@ -1059,7 +1100,7 @@ interface commands { /// /// BZMPOP timeout numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ] /// https://valkey.io/commands/bzmpop/ - bzmpop: async func(connection: borrow, timeout: duration, keys: list, end: pop-end, count: option) -> result>>>, error>; + bzmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: pop-end, count: option) -> result>>>, error>; /// Removes the member with the highest score from the first non-empty sorted set, waiting up to /// the timeout for one, returning its key, the member and its score. @@ -1071,7 +1112,7 @@ interface commands { /// /// BZPOPMAX key [ key ... ] timeout /// https://valkey.io/commands/bzpopmax/ - bzpopmax: async func(connection: borrow, keys: list, timeout: duration) -> result>, error>; + bzpopmax: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; /// Removes the member with the lowest score from the first non-empty sorted set, waiting up to /// the timeout for one, returning its key, the member and its score. @@ -1083,7 +1124,7 @@ interface commands { /// /// BZPOPMIN key [ key ... ] timeout /// https://valkey.io/commands/bzpopmin/ - bzpopmin: async func(connection: borrow, keys: list, timeout: duration) -> result>, error>; + bzpopmin: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; /// Adds the members with their scores to the sorted set stored at key, or updates their scores, /// returning how many were added, or with `changed`, added or updated. @@ -1440,7 +1481,7 @@ interface commands { /// /// XAUTOCLAIM key group consumer min-idle-time start [ COUNT count ] /// https://valkey.io/commands/xautoclaim/ - xautoclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration, start: option, count: option) -> result, list>>, list>, error>; + xautoclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, start: option, count: option) -> result, list>>, list>, error>; /// Transfers ownership of pending stream entries that match the specified criteria. /// @@ -1452,16 +1493,16 @@ interface commands { /// /// XAUTOCLAIM key group consumer min-idle-time start [ COUNT count ] JUSTID /// https://valkey.io/commands/xautoclaim/ - xautoclaim-justid: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration, start: option, count: option) -> result, list, list>, error>; + xautoclaim-justid: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, start: option, count: option) -> result, list, list>, error>; /// Options of XCLAIM. record xclaim-opts { /// `IDLE ms`: sets the idle time of the entries, the time since they were last delivered, instead /// of resetting it. - idle: option, + idle: option, /// `TIME unix-time-milliseconds`: sets the idle time of the entries to the time since this instant. - time: option, + time: option, /// `RETRYCOUNT count`: sets the delivery count of the entries, instead of incrementing it. retrycount: option, /// `FORCE`: claims the entries even when they are not pending for any consumer, as long as they @@ -1477,7 +1518,7 @@ interface commands { /// /// XCLAIM key group consumer min-idle-time ID [ ID ... ] [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] [ LASTID lastid ] /// https://valkey.io/commands/xclaim/ - xclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration, ids: list, opts: option) -> result>>, error>; + xclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, ids: list, opts: option) -> result>>, error>; /// Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument. /// @@ -1485,7 +1526,7 @@ interface commands { /// /// XCLAIM key group consumer min-idle-time ID [ ID ... ] [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] JUSTID [ LASTID lastid ] /// https://valkey.io/commands/xclaim/ - xclaim-justid: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration, ids: list, opts: option) -> result, error>; + xclaim-justid: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, ids: list, opts: option) -> result, error>; /// Removes the specified entries from a stream, and returns the number of entries deleted. /// @@ -1531,10 +1572,10 @@ interface commands { pending: integer, /// The time since the consumer last attempted an interaction, e.g. XREADGROUP, XCLAIM or /// XAUTOCLAIM. - idle: duration, + idle: duration-milliseconds, /// The time since the consumer last interacted successfully, e.g. read or claimed entries. /// None when the consumer has never been active. - inactive: option, + inactive: option, } /// Consumers that belong to the consumer group of the stream stored at . @@ -1633,7 +1674,7 @@ interface commands { pel-count: integer, /// The group's pending entries, the ID of each, the consumer it was delivered to, when it was /// last delivered and the number of times it was delivered. - pending: list>, + pending: list>, /// The group's consumers. consumers: list, } @@ -1643,15 +1684,15 @@ interface commands { /// The consumer's name. name: string, /// When the consumer last attempted an interaction, e.g. XREADGROUP, XCLAIM or XAUTOCLAIM. - seen-time: instant, + seen-time: unix-time-milliseconds, /// When the consumer last interacted successfully, e.g. read or claimed entries. /// None when the consumer has never been active. - active-time: option, + active-time: option, /// The number of entries pending for the consumer, delivered but not acknowledged. pel-count: integer, /// The consumer's pending entries, the ID of each, when it was last delivered and the number of /// times it was delivered. - pending: list>, + pending: list>, } /// Information about the stream stored at . @@ -1695,7 +1736,7 @@ interface commands { /// The name of the consumer that fetched the message and has still to acknowledge it. We call it the current owner of the message. name: string, /// The number of milliseconds that elapsed since the last time this message was delivered to this consumer. - since-delivery: duration, + since-delivery: duration-milliseconds, /// The number of times this message was delivered. delivery-count: integer, } @@ -1706,7 +1747,7 @@ interface commands { /// /// XPENDING key group [ IDLE min-idle-time ] start end count [ consumer ] /// https://valkey.io/commands/xpending/ - xpending-extended: async func(connection: borrow, key: string, group: string, min-idle-time: option, start: stream-id, end: stream-id, count: integer, consumer: option) -> result, error>; + xpending-extended: async func(connection: borrow, key: string, group: string, min-idle-time: option, start: stream-id, end: stream-id, count: integer, consumer: option) -> result, error>; /// Stream entries matching a given range of IDs. /// @@ -1718,7 +1759,7 @@ interface commands { /// /// XREAD [ COUNT count ] [ BLOCK milliseconds ] STREAMS key [ key ... ] ID [ ID ... ] /// https://valkey.io/commands/xread/ - xread: async func(connection: borrow, count: option, block: option, keys: list, ids: list) -> result>>>>, error>; + xread: async func(connection: borrow, count: option, block: option, keys: list, ids: list) -> result>>>>, error>; /// Special version of the XREAD command with support for consumer groups. /// @@ -1727,7 +1768,7 @@ interface commands { /// /// XREADGROUP GROUP group consumer [ COUNT count ] [ BLOCK milliseconds ] [ NOACK ] STREAMS key [ key ... ] ID [ ID ... ] /// https://valkey.io/commands/xreadgroup/ - xreadgroup: async func(connection: borrow, group: string, consumer: string, count: option, block: option, noack: bool, keys: list, ids: list) -> result>>>>>, error>; + xreadgroup: async func(connection: borrow, group: string, consumer: string, count: option, block: option, noack: bool, keys: list, ids: list) -> result>>>>>, error>; /// XRANGE, but returning the entries in reverse order. /// @@ -1791,13 +1832,13 @@ interface commands { /// The expiration to set on a key, see `getex`. variant getex-ttl { /// Set the specified expire time, in seconds. - ex(duration), + ex(duration-seconds), /// Set the specified expire time, in milliseconds. - px(duration), + px(duration-milliseconds), /// Set the specified Unix time at which the key will expire, in seconds. - exat(instant), + exat(unix-time-seconds), /// Set the specified Unix time at which the key will expire, in milliseconds. - pxat(instant), + pxat(unix-time-milliseconds), /// Remove the time to live associated with the key. persist, } @@ -1919,7 +1960,7 @@ interface commands { /// /// PSETEX key milliseconds value /// https://valkey.io/commands/psetex/ - psetex: async func(connection: borrow, key: string, milliseconds: duration, value: string) -> result<_, error>; + psetex: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, value: string) -> result<_, error>; /// The condition for setting a key, see `set`. variant set-precondition { @@ -1936,13 +1977,13 @@ interface commands { /// The expiration to set on a key, see `set`. variant set-ttl { /// Set the specified expire time, in seconds. - ex(duration), + ex(duration-seconds), /// Set the specified expire time, in milliseconds. - px(duration), + px(duration-milliseconds), /// Set the specified Unix time at which the key will expire, in seconds. - exat(instant), + exat(unix-time-seconds), /// Set the specified Unix time at which the key will expire, in milliseconds. - pxat(instant), + pxat(unix-time-milliseconds), /// Retain the time to live associated with the keys. keepttl, } @@ -1965,7 +2006,7 @@ interface commands { /// /// SETEX key seconds value /// https://valkey.io/commands/setex/ - setex: async func(connection: borrow, key: string, seconds: duration, value: string) -> result<_, error>; + setex: async func(connection: borrow, key: string, seconds: duration-seconds, value: string) -> result<_, error>; /// Set key to hold string value if key does not exist. /// From 12b4957a57816ea61abd3ecc6d27be01454d20fe Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Mon, 12 Oct 2026 22:21:29 -0400 Subject: [PATCH 09/37] polishing Signed-off-by: Scott Andrews --- components/as-keyvalue/src/lib.rs | 14 +- components/commands/src/command.rs | 264 ++++++++------- components/commands/src/lib.rs | 497 ++++++++++++++++------------- components/commands/src/value.rs | 22 +- wit/commands.wit | 27 +- 5 files changed, 466 insertions(+), 358 deletions(-) diff --git a/components/as-keyvalue/src/lib.rs b/components/as-keyvalue/src/lib.rs index 8e610b8..384c0cd 100644 --- a/components/as-keyvalue/src/lib.rs +++ b/components/as-keyvalue/src/lib.rs @@ -1,4 +1,4 @@ -use componentized::valkey::commands::{self, DurationMilliseconds}; +use componentized::valkey::commands::{self, Cursor, DurationMilliseconds}; use componentized::valkey::connection::{Connection, Error as ValkeyError}; use componentized::valkey::connector; use exports::wasmcloud::keyvalue::atomics::Guest as AtomicsGuest; @@ -107,6 +107,8 @@ impl GuestBucket for KeyvalueToValkeyBucket { let match_ = prefix.map(|prefix| format!("{}*", escape_glob(&prefix))); let count = None; + // the keyvalue cursor is none to start, and none once the keys are listed, like a scan's + let cursor = cursor.map_or(Cursor::Start, Cursor::Resume); let (cursor, keys) = commands::hscan_novalues( &self.connection, self.hash_key.clone(), @@ -116,10 +118,12 @@ impl GuestBucket for KeyvalueToValkeyBucket { ) .await?; - Ok(KeyResponse { - keys, - cursor: Some(cursor.to_string()), - }) + // a returned cursor is always one to resume from + let cursor = cursor.map(|cursor| match cursor { + Cursor::Resume(cursor) => cursor, + Cursor::Start => unreachable!("a scan never returns start"), + }); + Ok(KeyResponse { keys, cursor }) } } diff --git a/components/commands/src/command.rs b/components/commands/src/command.rs index cde7596..adc3d9b 100644 --- a/components/commands/src/command.rs +++ b/components/commands/src/command.rs @@ -1,9 +1,9 @@ use crate::exports::componentized::valkey::commands::{ - Aggregate, ChannelRule, CommandRule, Connection, DbRule, DurationMilliseconds, DurationSeconds, - Error, GetexTtl, HsetexCondition, HsetexFieldCondition, KeyRule, LexBound, Limit, - MsetexPrecondition, PopEnd, Rule, RuleSelector, ScoreBound, SetPrecondition, SetTtl, SortOpts, - SortOrder, StreamTrim, StreamTrimEfficiency, TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, - UserRule, Value, XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, + Aggregate, ChannelRule, CommandRule, Connection, Cursor, DbRule, DurationMilliseconds, + DurationSeconds, Error, GetexTtl, HsetexCondition, HsetexFieldCondition, KeyRule, LexBound, + Limit, MsetexPrecondition, PopEnd, Rule, RuleSelector, ScoreBound, SetPrecondition, SetTtl, + SortOpts, SortOrder, StreamTrim, StreamTrimEfficiency, TtlMode, UnixTimeMilliseconds, + UnixTimeSeconds, UserRule, Value, XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, }; use crate::value::{FromValue, OrZeroId, ZERO_ID}; @@ -11,14 +11,14 @@ use crate::value::{FromValue, OrZeroId, ZERO_ID}; /// /// An error reply fails the command, see `connection.exchange`. async fn exchange(connection: &Connection, command: impl IntoCommand) -> Result { - connection.exchange(command.into_command()).await + connection.exchange(command.into_command()?).await } /// Send a command, its words as bulk strings, without waiting for a reply. /// /// An error reply fails the command, see `connection.emit`. pub(crate) async fn emit(connection: &Connection, command: impl IntoCommand) -> Result<(), Error> { - connection.emit(command.into_command()).await + connection.emit(command.into_command()?).await } /// Send a command, see `exchange`, and convert its reply. @@ -33,25 +33,28 @@ pub(crate) async fn query( /// /// The impls compose, a tuple is the words of each of its items in order, so a command is written /// as one value, e.g. `("SET", key, value, precondition, ttl)`. +/// +/// A value the command does not accept, e.g. an empty list where it requires an item, fails with a +/// client error, and the command is not sent. pub trait IntoCommand { - fn into_command(self) -> Vec; + fn into_command(self) -> Result, Error>; } impl IntoCommand for &str { - fn into_command(self) -> Vec { - vec![self.to_string()] + fn into_command(self) -> Result, Error> { + Ok(vec![self.to_string()]) } } impl IntoCommand for String { - fn into_command(self) -> Vec { - vec![self] + fn into_command(self) -> Result, Error> { + Ok(vec![self]) } } impl IntoCommand for &String { - fn into_command(self) -> Vec { - vec![self.clone()] + fn into_command(self) -> Result, Error> { + Ok(vec![self.clone()]) } } @@ -60,8 +63,8 @@ macro_rules! into_command_numbers { ($($number:ty),*) => { $( impl IntoCommand for $number { - fn into_command(self) -> Vec { - vec![self.to_string()] + fn into_command(self) -> Result, Error> { + Ok(vec![self.to_string()]) } } )* @@ -72,24 +75,28 @@ into_command_numbers!(i32, i64, u8, u32, u64, usize, f64); /// No word for none. impl IntoCommand for Option { - fn into_command(self) -> Vec { - self.map(IntoCommand::into_command).unwrap_or_default() + fn into_command(self) -> Result, Error> { + self.map_or(Ok(vec![]), IntoCommand::into_command) } } impl IntoCommand for Vec { - fn into_command(self) -> Vec { - self.into_iter() - .flat_map(IntoCommand::into_command) - .collect() + fn into_command(self) -> Result, Error> { + let mut command = vec![]; + for item in self { + command.extend(item.into_command()?); + } + Ok(command) } } impl IntoCommand for [T; N] { - fn into_command(self) -> Vec { - self.into_iter() - .flat_map(IntoCommand::into_command) - .collect() + fn into_command(self) -> Result, Error> { + let mut command = vec![]; + for item in self { + command.extend(item.into_command()?); + } + Ok(command) } } @@ -99,11 +106,11 @@ macro_rules! into_command_tuples { $( impl<$($item: IntoCommand),+> IntoCommand for ($($item,)+) { #[allow(non_snake_case)] - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { let ($($item,)+) = self; let mut command = vec![]; - $(command.extend($item.into_command());)+ - command + $(command.extend($item.into_command()?);)+ + Ok(command) } } )* @@ -129,65 +136,65 @@ into_command_tuples!( pub struct Flag(pub &'static str, pub bool); impl IntoCommand for Flag { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { let Flag(word, present) = self; - if present { + Ok(if present { vec![word.to_string()] } else { vec![] - } + }) } } pub struct NamedOption(pub &'static str, pub Option); impl IntoCommand for NamedOption { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { let NamedOption(name, value) = self; match value { Some(_) => (name, value).into_command(), - None => vec![], + None => Ok(vec![]), } } } /// `0-0` for none, e.g. to start XAUTOCLAIM from the beginning. impl IntoCommand for OrZeroId { - fn into_command(self) -> Vec { - vec![self.0.unwrap_or_else(|| ZERO_ID.to_string())] + fn into_command(self) -> Result, Error> { + Ok(vec![self.0.unwrap_or_else(|| ZERO_ID.to_string())]) } } /// Whole seconds, e.g. for EX. impl IntoCommand for DurationSeconds { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { self.ex.into_command() } } /// Whole milliseconds, e.g. for PX. impl IntoCommand for DurationMilliseconds { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { self.px.into_command() } } /// Whole seconds since the Unix epoch, e.g. for EXAT. impl IntoCommand for UnixTimeSeconds { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { self.exat.into_command() } } /// Whole milliseconds since the Unix epoch, e.g. for PXAT. impl IntoCommand for UnixTimeMilliseconds { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { self.pxat.into_command() } } impl IntoCommand for Rule { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { match self { Rule::Keys(rule) => rule.into_command(), Rule::Channel(rule) => rule.into_command(), @@ -199,7 +206,7 @@ impl IntoCommand for Rule { } impl IntoCommand for RuleSelector { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { match self { RuleSelector::Keys(rule) => rule.into_command(), RuleSelector::Channel(rule) => rule.into_command(), @@ -210,31 +217,31 @@ impl IntoCommand for RuleSelector { } impl IntoCommand for KeyRule { - fn into_command(self) -> Vec { - vec![match self { + fn into_command(self) -> Result, Error> { + Ok(vec![match self { KeyRule::Add(pattern) => format!("~{pattern}"), KeyRule::AddRead(pattern) => format!("%R~{pattern}"), KeyRule::AddWrite(pattern) => format!("%W~{pattern}"), KeyRule::AddReadWrite(pattern) => format!("%RW~{pattern}"), KeyRule::All => "allkeys".to_string(), KeyRule::Reset => "resetkeys".to_string(), - }] + }]) } } impl IntoCommand for ChannelRule { - fn into_command(self) -> Vec { - vec![match self { + fn into_command(self) -> Result, Error> { + Ok(vec![match self { ChannelRule::Add(pattern) => format!("&{pattern}"), ChannelRule::All => "allchannels".to_string(), ChannelRule::Reset => "resetchannels".to_string(), - }] + }]) } } impl IntoCommand for CommandRule { - fn into_command(self) -> Vec { - vec![match self { + fn into_command(self) -> Result, Error> { + Ok(vec![match self { CommandRule::Add(command) => format!("+{command}"), CommandRule::AddCategory(category) => format!("+@{category}"), CommandRule::AddFirstArg((command, arg)) => format!("+{command}|{arg}"), @@ -242,13 +249,17 @@ impl IntoCommand for CommandRule { CommandRule::Remove(command) => format!("-{command}"), CommandRule::RemoveCategory(category) => format!("-@{category}"), CommandRule::Reset => "nocommands".to_string(), - }] + }]) } } +/// `db=` requires at least one database. impl IntoCommand for DbRule { - fn into_command(self) -> Vec { - vec![match self { + fn into_command(self) -> Result, Error> { + Ok(vec![match self { + DbRule::Db(dbs) if dbs.is_empty() => { + return Err(Error::Client("db requires at least one item".to_string())); + } DbRule::Db(dbs) => format!( "db={}", dbs.iter() @@ -258,12 +269,12 @@ impl IntoCommand for DbRule { ), DbRule::All => "alldbs".to_string(), DbRule::Reset => "resetdbs".to_string(), - }] + }]) } } impl IntoCommand for UserRule { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { match self { UserRule::On => "on".into_command(), UserRule::Off => "off".into_command(), @@ -280,21 +291,19 @@ impl IntoCommand for UserRule { } impl IntoCommand for TtlMode { - fn into_command(self) -> Vec { - vec![ - match self { - TtlMode::Nx => "NX", - TtlMode::Xx => "XX", - TtlMode::Gt => "GT", - TtlMode::Lt => "LT", - } - .to_string(), - ] + fn into_command(self) -> Result, Error> { + match self { + TtlMode::Nx => "NX", + TtlMode::Xx => "XX", + TtlMode::Gt => "GT", + TtlMode::Lt => "LT", + } + .into_command() } } impl IntoCommand for GetexTtl { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { match self { GetexTtl::Ex(duration) => ("EX", duration).into_command(), GetexTtl::Px(duration) => ("PX", duration).into_command(), @@ -307,7 +316,7 @@ impl IntoCommand for GetexTtl { /// Also `MsetexTtl`, the same type. impl IntoCommand for SetTtl { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { match self { SetTtl::Ex(duration) => ("EX", duration).into_command(), SetTtl::Px(duration) => ("PX", duration).into_command(), @@ -319,7 +328,7 @@ impl IntoCommand for SetTtl { } impl IntoCommand for SetPrecondition { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { match self { SetPrecondition::Nx => "NX".into_command(), SetPrecondition::Xx => "XX".into_command(), @@ -330,7 +339,7 @@ impl IntoCommand for SetPrecondition { } impl IntoCommand for MsetexPrecondition { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { match self { MsetexPrecondition::Nx => "NX".into_command(), MsetexPrecondition::Xx => "XX".into_command(), @@ -339,21 +348,21 @@ impl IntoCommand for MsetexPrecondition { } impl IntoCommand for StreamTrim { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { //〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] match self { StreamTrim::Maxlen(trim) => ( "MAXLEN", trim.efficiency, trim.threshold, - trim.limit.map(|l| ("LIMIT", l).into_command()), + NamedOption("LIMIT", trim.limit), ) .into_command(), StreamTrim::Minid(trim) => ( "MINID", trim.efficiency, trim.threshold, - trim.limit.map(|l| ("LIMIT", l).into_command()), + NamedOption("LIMIT", trim.limit), ) .into_command(), } @@ -361,7 +370,7 @@ impl IntoCommand for StreamTrim { } impl IntoCommand for StreamTrimEfficiency { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { // = | ~ (match self { StreamTrimEfficiency::Exact => "=", @@ -372,7 +381,7 @@ impl IntoCommand for StreamTrimEfficiency { } impl IntoCommand for XclaimOpts { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { // [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] [ LASTID lastid ] ( NamedOption("IDLE", self.idle), @@ -389,55 +398,66 @@ impl IntoCommand for XclaimOpts { pub struct DecimalSeconds(pub DurationMilliseconds); impl IntoCommand for DecimalSeconds { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { let milliseconds = self.0.px; - vec![format!( + Ok(vec![format!( "{}.{:03}", milliseconds / 1_000, milliseconds % 1_000 - )] + )]) } } -/// A list the command requires at least one item of, e.g. the keys of DEL. -pub struct NonEmpty(Vec); +/// A list the command requires at least one item of, e.g. the keys of DEL, named for the error when +/// it is empty. +pub struct NonEmpty(&'static str, Vec); impl NonEmpty { - /// Fails, before anything is sent, when there are no items. - pub fn new(name: &str, items: Vec) -> Result { - if items.is_empty() { - return Err(Error::Client(format!("{name} requires at least one item"))); - } - Ok(NonEmpty(items)) + pub fn new(name: &'static str, items: Vec) -> Self { + NonEmpty(name, items) } /// A list the command allows to be left out, none rather than empty. - pub fn optional(name: &str, items: Option>) -> Result, Error> { - items.map(|items| NonEmpty::new(name, items)).transpose() + pub fn optional(name: &'static str, items: Option>) -> Option { + items.map(|items| NonEmpty(name, items)) } pub fn len(&self) -> usize { - self.0.len() + self.1.len() } } impl IntoCommand for NonEmpty { - fn into_command(self) -> Vec { - self.0.into_command() + fn into_command(self) -> Result, Error> { + let NonEmpty(name, items) = self; + if items.is_empty() { + return Err(Error::Client(format!("{name} requires at least one item"))); + } + items.into_command() } } -/// The keys preceded by how many there are, e.g. for ZINTER. -pub struct NumKeys(pub NonEmpty); +/// The items preceded by how many there are, e.g. the keys of ZINTER, or the keys and values of +/// MSETEX, a pair counts as one. +pub struct NumKeys(pub NonEmpty); -impl IntoCommand for NumKeys { - fn into_command(self) -> Vec { +impl IntoCommand for NumKeys { + fn into_command(self) -> Result, Error> { (self.0.len(), self.0).into_command() } } +/// `FIELDS numfields field [ field ... ]`, e.g. for HEXPIRE, a field and its value counts as one. +pub struct Fields(pub NonEmpty); + +impl IntoCommand for Fields { + fn into_command(self) -> Result, Error> { + ("FIELDS", NumKeys(self.0)).into_command() + } +} + impl IntoCommand for Limit { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { ("LIMIT", self.offset, self.count).into_command() } } @@ -450,7 +470,7 @@ pub struct ScoreRange { } impl IntoCommand for ScoreRange { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { let bound = |bound, unbounded: &str| match bound { ScoreBound::Inclusive(score) => score.to_string(), ScoreBound::Exclusive(score) => format!("({score}"), @@ -458,7 +478,7 @@ impl IntoCommand for ScoreRange { }; let (low, high) = ("-inf", "+inf"); let (start, stop) = if self.rev { (high, low) } else { (low, high) }; - vec![bound(self.start, start), bound(self.stop, stop)] + Ok(vec![bound(self.start, start), bound(self.stop, stop)]) } } @@ -470,7 +490,7 @@ pub struct LexRange { } impl IntoCommand for LexRange { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { let bound = |bound, unbounded: &str| match bound { LexBound::Inclusive(member) => format!("[{member}"), LexBound::Exclusive(member) => format!("({member}"), @@ -478,7 +498,7 @@ impl IntoCommand for LexRange { }; let (low, high) = ("-", "+"); let (start, stop) = if self.rev { (high, low) } else { (low, high) }; - vec![bound(self.start, start), bound(self.stop, stop)] + Ok(vec![bound(self.start, start), bound(self.stop, stop)]) } } @@ -490,16 +510,16 @@ pub struct Zrange { } impl IntoCommand for Zrange { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { let Zrange { range, rev, limit } = self; let (range, by) = match range { - ZrangeBy::Rank((start, stop)) => ((start, stop).into_command(), None), + ZrangeBy::Rank((start, stop)) => ((start, stop).into_command()?, None), ZrangeBy::Score((start, stop)) => ( - ScoreRange { start, stop, rev }.into_command(), + ScoreRange { start, stop, rev }.into_command()?, Some("BYSCORE"), ), ZrangeBy::Lex((start, stop)) => { - (LexRange { start, stop, rev }.into_command(), Some("BYLEX")) + (LexRange { start, stop, rev }.into_command()?, Some("BYLEX")) } }; (range, by, Flag("REV", rev), limit).into_command() @@ -507,7 +527,7 @@ impl IntoCommand for Zrange { } impl IntoCommand for Aggregate { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { let aggregate = match self { Aggregate::Sum => "SUM", Aggregate::Min => "MIN", @@ -518,7 +538,7 @@ impl IntoCommand for Aggregate { } impl IntoCommand for PopEnd { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { match self { PopEnd::Min => "MIN", PopEnd::Max => "MAX", @@ -528,7 +548,7 @@ impl IntoCommand for PopEnd { } impl IntoCommand for ZaddCondition { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { match self { ZaddCondition::Nx => "NX", ZaddCondition::Xx => "XX", @@ -538,7 +558,7 @@ impl IntoCommand for ZaddCondition { } impl IntoCommand for ZaddComparison { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { match self { ZaddComparison::Gt => "GT", ZaddComparison::Lt => "LT", @@ -548,7 +568,7 @@ impl IntoCommand for ZaddComparison { } impl IntoCommand for SortOrder { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { match self { SortOrder::Asc => "ASC", SortOrder::Desc => "DESC", @@ -559,15 +579,14 @@ impl IntoCommand for SortOrder { /// `[ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ]` impl IntoCommand for SortOpts { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { ( NamedOption("BY", self.by), self.limit, - self.get - .into_iter() - .flatten() - .map(|pattern| ("GET", pattern)) - .collect::>(), + NonEmpty::optional("get", self.get).map(|NonEmpty(name, patterns)| { + let patterns = patterns.into_iter().map(|pattern| ("GET", pattern)); + NonEmpty(name, patterns.collect()) + }), self.order, Flag("ALPHA", self.alpha), ) @@ -576,7 +595,7 @@ impl IntoCommand for SortOpts { } impl IntoCommand for HsetexCondition { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { match self { HsetexCondition::Nx => "NX", HsetexCondition::Xx => "XX", @@ -586,7 +605,7 @@ impl IntoCommand for HsetexCondition { } impl IntoCommand for HsetexFieldCondition { - fn into_command(self) -> Vec { + fn into_command(self) -> Result, Error> { match self { HsetexFieldCondition::Fnx => "FNX", HsetexFieldCondition::Fxx => "FXX", @@ -594,3 +613,16 @@ impl IntoCommand for HsetexFieldCondition { .into_command() } } + +/// `0` to start, failing to resume from `0`, which would restart the scan. +impl IntoCommand for Cursor { + fn into_command(self) -> Result, Error> { + match self { + Cursor::Start => "0".into_command(), + Cursor::Resume(cursor) if cursor == "0" => Err(Error::Client( + "resume requires the cursor a scan returned, use start to begin a scan".to_string(), + )), + Cursor::Resume(cursor) => cursor.into_command(), + } + } +} diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index 4963f78..9f916b6 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -1,17 +1,18 @@ use crate::command::{ - DecimalSeconds, Flag, LexRange, NamedOption, NonEmpty, NumKeys, ScoreRange, Zrange, emit, query, + DecimalSeconds, Fields, Flag, LexRange, NamedOption, NonEmpty, NumKeys, ScoreRange, Zrange, + emit, query, }; use crate::exports::componentized::valkey::commands::{ - Aggregate, Connection, Cursor, DbRule, Double, DurationMilliseconds, DurationSeconds, Error, + Aggregate, Connection, Cursor, Double, DurationMilliseconds, DurationSeconds, Error, FieldExpiretime, FieldPersistResult, FieldPexpiretime, FieldPttl, FieldTtl, GetexTtl, Guest, HsetexCondition, HsetexFieldCondition, Integer, LcsIdxReply, LexBound, Limit, - MsetexPrecondition, MsetexTtl, PopEnd, Rule, RuleSelector, ScoreBound, SetPrecondition, SetTtl, - SortOpts, StreamId, StreamTrim, TtlMode, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, - UserRule, Value, XclaimOpts, XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, + MsetexPrecondition, MsetexTtl, PopEnd, Rule, ScoreBound, SetPrecondition, SetTtl, SortOpts, + StreamId, StreamTrim, TtlMode, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, + XclaimOpts, XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, ZaddComparison, ZaddCondition, ZrangeBy, ZrangeWithscoresBy, }; -use crate::value::{FlatPairs, OrNever, OrZeroId, ScoredMembers}; +use crate::value::{FlatPairs, NextCursor, OrNever, OrZeroId, ScoredMembers}; use wit_bindgen::rt::Map; @@ -27,32 +28,6 @@ fn sort_row_len(opts: &Option) -> usize { .map_or(1, Vec::len) } -/// The GET patterns of SORT, when given, at least one. -fn check_sort_opts(opts: &Option) -> Result<(), Error> { - match opts.as_ref().and_then(|opts| opts.get.as_ref()) { - Some(get) if get.is_empty() => { - Err(Error::Client("get requires at least one item".to_string())) - } - _ => Ok(()), - } -} - -/// The databases of a `db=` rule, at least one, including those of selectors. -fn check_rules(rules: &[Rule]) -> Result<(), Error> { - let empty = |rule: &DbRule| matches!(rule, DbRule::Db(dbs) if dbs.is_empty()); - let invalid = rules.iter().any(|rule| match rule { - Rule::Db(rule) => empty(rule), - Rule::User(UserRule::Selector(selectors)) => selectors - .iter() - .any(|selector| matches!(selector, RuleSelector::Db(rule) if empty(rule))), - _ => false, - }); - if invalid { - return Err(Error::Client("db requires at least one item".to_string())); - } - Ok(()) -} - impl Guest for Commands { #[doc = " admin commands"] #[doc = " Delete all the specified ACL users and terminate all the connections that are authenticated with such users."] @@ -64,8 +39,11 @@ impl Guest for Commands { connection: &Connection, usernames: Vec, ) -> Result { - let usernames = NonEmpty::new("usernames", usernames)?; - query(connection, ("ACL", "DELUSER", usernames)).await + query( + connection, + ("ACL", "DELUSER", NonEmpty::new("usernames", usernames)), + ) + .await } #[doc = " Generates a random password."] @@ -87,11 +65,16 @@ impl Guest for Commands { username: String, rules: Option>, ) -> Result<(), Error> { - if let Some(rules) = &rules { - check_rules(rules)?; - } - let rules = NonEmpty::optional("rules", rules)?; - query(connection, ("ACL", "SETUSER", username, rules)).await + query( + connection, + ( + "ACL", + "SETUSER", + username, + NonEmpty::optional("rules", rules), + ), + ) + .await } #[doc = " Authenticates the current connection."] @@ -155,8 +138,7 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result { - let fields = NonEmpty::new("fields", fields)?; - query(connection, ("HDEL", key, fields)).await + query(connection, ("HDEL", key, NonEmpty::new("fields", fields))).await } #[doc = " Returns if field is an existing field in the hash stored at key."] @@ -180,10 +162,9 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result, Error> { - let fields = NonEmpty::new("fields", fields)?; query( connection, - ("HEXPIRETIME", key, "FIELDS", fields.len(), fields), + ("HEXPIRETIME", key, Fields(NonEmpty::new("fields", fields))), ) .await } @@ -198,8 +179,11 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result>, Error> { - let fields = NonEmpty::new("fields", fields)?; - query(connection, ("HGETDEL", key, "FIELDS", fields.len(), fields)).await + query( + connection, + ("HGETDEL", key, Fields(NonEmpty::new("fields", fields))), + ) + .await } #[doc = " The values of the hash fields, none for a field that does not exist, optionally setting or"] @@ -214,10 +198,9 @@ impl Guest for Commands { ttl: Option, fields: Vec, ) -> Result>, Error> { - let fields = NonEmpty::new("fields", fields)?; query( connection, - ("HGETEX", key, ttl, "FIELDS", fields.len(), fields), + ("HGETEX", key, ttl, Fields(NonEmpty::new("fields", fields))), ) .await } @@ -232,10 +215,9 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result, Error> { - let fields = NonEmpty::new("fields", fields)?; query( connection, - ("HPERSIST", key, "FIELDS", fields.len(), fields), + ("HPERSIST", key, Fields(NonEmpty::new("fields", fields))), ) .await } @@ -252,15 +234,12 @@ impl Guest for Commands { mode: Option, fields: Vec, ) -> Result, Error> { - let fields = NonEmpty::new("fields", fields)?; let command = ( "HPEXPIRE", key, milliseconds, mode, - "FIELDS", - fields.len(), - fields, + Fields(NonEmpty::new("fields", fields)), ); query(connection, command).await } @@ -277,15 +256,12 @@ impl Guest for Commands { mode: Option, fields: Vec, ) -> Result, Error> { - let fields = NonEmpty::new("fields", fields)?; let command = ( "HPEXPIREAT", key, milliseconds, mode, - "FIELDS", - fields.len(), - fields, + Fields(NonEmpty::new("fields", fields)), ); query(connection, command).await } @@ -302,10 +278,9 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result, Error> { - let fields = NonEmpty::new("fields", fields)?; query( connection, - ("HPEXPIRETIME", key, "FIELDS", fields.len(), fields), + ("HPEXPIRETIME", key, Fields(NonEmpty::new("fields", fields))), ) .await } @@ -322,8 +297,11 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result, Error> { - let fields = NonEmpty::new("fields", fields)?; - query(connection, ("HPTTL", key, "FIELDS", fields.len(), fields)).await + query( + connection, + ("HPTTL", key, Fields(NonEmpty::new("fields", fields))), + ) + .await } #[doc = " Sets the fields to their values in the hash stored at key, optionally setting their"] @@ -340,16 +318,13 @@ impl Guest for Commands { ttl: Option, field_and_values: Vec<(String, String)>, ) -> Result { - let field_and_values = NonEmpty::new("field-and-values", field_and_values)?; let command = ( "HSETEX", key, condition, field_condition, ttl, - "FIELDS", - field_and_values.len(), - field_and_values, + Fields(NonEmpty::new("field-and-values", field_and_values)), ); query(connection, command).await } @@ -366,8 +341,11 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result, Error> { - let fields = NonEmpty::new("fields", fields)?; - query(connection, ("HTTL", key, "FIELDS", fields.len(), fields)).await + query( + connection, + ("HTTL", key, Fields(NonEmpty::new("fields", fields))), + ) + .await } #[doc = " Manipulate the expiration time for existing hash fields."] @@ -382,7 +360,6 @@ impl Guest for Commands { mode: Option, fields: Vec, ) -> Result, Error> { - let fields = NonEmpty::new("fields", fields)?; query( connection, ( @@ -390,9 +367,7 @@ impl Guest for Commands { key, seconds, mode, - "FIELDS", - fields.len(), - fields, + Fields(NonEmpty::new("fields", fields)), ), ) .await @@ -410,7 +385,6 @@ impl Guest for Commands { mode: Option, fields: Vec, ) -> Result, Error> { - let fields = NonEmpty::new("fields", fields)?; query( connection, ( @@ -418,9 +392,7 @@ impl Guest for Commands { key, seconds, mode, - "FIELDS", - fields.len(), - fields, + Fields(NonEmpty::new("fields", fields)), ), ) .await @@ -504,8 +476,7 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result>, Error> { - let fields = NonEmpty::new("fields", fields)?; - query(connection, ("HMGET", key, fields)).await + query(connection, ("HMGET", key, NonEmpty::new("fields", fields))).await } #[doc = " Sets the specified fields to their respective values in the hash stored at key."] @@ -518,8 +489,15 @@ impl Guest for Commands { key: String, field_and_values: Vec<(String, String)>, ) -> Result<(), Error> { - let field_and_values = NonEmpty::new("field-and-values", field_and_values)?; - query(connection, ("HMSET", key, field_and_values)).await + query( + connection, + ( + "HMSET", + key, + NonEmpty::new("field-and-values", field_and_values), + ), + ) + .await } #[doc = " Random fields from the hash value stored at key, up to the count."] @@ -573,21 +551,22 @@ impl Guest for Commands { async fn hscan( connection: &Connection, key: String, - cursor: Option, + cursor: Cursor, match_: Option, count: Option, - ) -> Result<(Cursor, Map), Error> { - query( + ) -> Result<(Option, Map), Error> { + let (cursor, items): (NextCursor, Map) = query( connection, ( "HSCAN", key, - cursor.unwrap_or(Cursor::from("0")), + cursor, match_.map(|m| ("MATCH", m)), count.map(|c| ("COUNT", c)), ), ) - .await + .await?; + Ok((cursor.0, items)) } #[doc = " Incrementally iterate over a collection of keys."] @@ -600,22 +579,23 @@ impl Guest for Commands { async fn hscan_novalues( connection: &Connection, key: String, - cursor: Option, + cursor: Cursor, match_: Option, count: Option, - ) -> Result<(Cursor, Vec), Error> { - query( + ) -> Result<(Option, Vec), Error> { + let (cursor, items): (NextCursor, Vec) = query( connection, ( "HSCAN", key, - cursor.unwrap_or(Cursor::from("0")), + cursor, match_.map(|m| ("MATCH", m)), count.map(|c| ("COUNT", c)), "NOVALUES", ), ) - .await + .await?; + Ok((cursor.0, items)) } #[doc = " Sets the specified fields to their respective values in the hash stored at key."] @@ -628,8 +608,15 @@ impl Guest for Commands { key: String, field_and_values: Vec<(String, String)>, ) -> Result { - let field_and_values = NonEmpty::new("field-and-values", field_and_values)?; - query(connection, ("HSET", key, field_and_values)).await + query( + connection, + ( + "HSET", + key, + NonEmpty::new("field-and-values", field_and_values), + ), + ) + .await } #[doc = " Sets field in the hash stored at key to value, only if field does not yet exist."] @@ -674,8 +661,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/del/"] #[allow(async_fn_in_trait)] async fn del(connection: &Connection, keys: Vec) -> Result { - let keys = NonEmpty::new("keys", keys)?; - query(connection, ("DEL", keys)).await + query(connection, ("DEL", NonEmpty::new("keys", keys))).await } #[doc = " If key exists."] @@ -684,8 +670,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/exists/"] #[allow(async_fn_in_trait)] async fn exists(connection: &Connection, keys: Vec) -> Result { - let keys = NonEmpty::new("keys", keys)?; - query(connection, ("EXISTS", keys)).await + query(connection, ("EXISTS", NonEmpty::new("keys", keys))).await } #[doc = " Set a timeout on key."] @@ -731,8 +716,11 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/psubscribe/"] #[allow(async_fn_in_trait)] async fn psubscribe(connection: &Connection, patterns: Vec) -> Result<(), Error> { - let patterns = NonEmpty::new("patterns", patterns)?; - emit(connection, ("PSUBSCRIBE", patterns)).await + emit( + connection, + ("PSUBSCRIBE", NonEmpty::new("patterns", patterns)), + ) + .await } #[doc = " Posts a message to the given channel."] @@ -778,8 +766,11 @@ impl Guest for Commands { connection: &Connection, channels: Option>, ) -> Result, Error> { - let channels = NonEmpty::optional("channels", channels)?; - query(connection, ("PUBSUB", "NUMSUB", channels)).await + query( + connection, + ("PUBSUB", "NUMSUB", NonEmpty::optional("channels", channels)), + ) + .await } #[doc = " Lists the currently active shard channels."] @@ -803,8 +794,15 @@ impl Guest for Commands { connection: &Connection, shardchannels: Option>, ) -> Result, Error> { - let shardchannels = NonEmpty::optional("shardchannels", shardchannels)?; - query(connection, ("PUBSUB", "SHARDNUMSUB", shardchannels)).await + query( + connection, + ( + "PUBSUB", + "SHARDNUMSUB", + NonEmpty::optional("shardchannels", shardchannels), + ), + ) + .await } #[doc = " Unsubscribes the client from the given patterns, or from all of them if none is given."] @@ -816,8 +814,11 @@ impl Guest for Commands { connection: &Connection, patterns: Option>, ) -> Result<(), Error> { - let patterns = NonEmpty::optional("patterns", patterns)?; - emit(connection, ("PUNSUBSCRIBE", patterns)).await + emit( + connection, + ("PUNSUBSCRIBE", NonEmpty::optional("patterns", patterns)), + ) + .await } #[doc = " Posts a message to the given shard channel."] @@ -839,8 +840,11 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/ssubscribe/"] #[allow(async_fn_in_trait)] async fn ssubscribe(connection: &Connection, shardchannels: Vec) -> Result<(), Error> { - let shardchannels = NonEmpty::new("shardchannels", shardchannels)?; - emit(connection, ("SSUBSCRIBE", shardchannels)).await + emit( + connection, + ("SSUBSCRIBE", NonEmpty::new("shardchannels", shardchannels)), + ) + .await } #[doc = " Subscribes the client to the specified channels."] @@ -849,8 +853,11 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/subscribe/"] #[allow(async_fn_in_trait)] async fn subscribe(connection: &Connection, channels: Vec) -> Result<(), Error> { - let channels = NonEmpty::new("channels", channels)?; - emit(connection, ("SUBSCRIBE", channels)).await + emit( + connection, + ("SUBSCRIBE", NonEmpty::new("channels", channels)), + ) + .await } #[doc = " Unsubscribes the client from the given shard channels, or from all of them if none is given."] @@ -862,8 +869,14 @@ impl Guest for Commands { connection: &Connection, shardchannels: Option>, ) -> Result<(), Error> { - let shardchannels = NonEmpty::optional("shardchannels", shardchannels)?; - emit(connection, ("SUNSUBSCRIBE", shardchannels)).await + emit( + connection, + ( + "SUNSUBSCRIBE", + NonEmpty::optional("shardchannels", shardchannels), + ), + ) + .await } #[doc = " Unsubscribes the client from the given channels, or from all of them if none is given."] @@ -875,8 +888,11 @@ impl Guest for Commands { connection: &Connection, channels: Option>, ) -> Result<(), Error> { - let channels = NonEmpty::optional("channels", channels)?; - emit(connection, ("UNSUBSCRIBE", channels)).await + emit( + connection, + ("UNSUBSCRIBE", NonEmpty::optional("channels", channels)), + ) + .await } #[doc = " Appends the value at the end of the string."] @@ -1075,8 +1091,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, ) -> Result>, Error> { - let keys = NonEmpty::new("keys", keys)?; - query(connection, ("MGET", keys)).await + query(connection, ("MGET", NonEmpty::new("keys", keys))).await } #[doc = " Sets the given keys to their respective values."] @@ -1088,8 +1103,11 @@ impl Guest for Commands { connection: &Connection, keys_and_values: Vec<(String, String)>, ) -> Result<(), Error> { - let keys_and_values = NonEmpty::new("keys-and-values", keys_and_values)?; - query(connection, ("MSET", keys_and_values)).await + query( + connection, + ("MSET", NonEmpty::new("keys-and-values", keys_and_values)), + ) + .await } #[doc = " Sets multiple key-value pairs while also assigning a shared expiration time to all the keys atomically."] @@ -1103,13 +1121,11 @@ impl Guest for Commands { precondition: Option, ttl: Option, ) -> Result { - let keys_and_values = NonEmpty::new("keys-and-values", keys_and_values)?; query( connection, ( "MSETEX", - keys_and_values.len(), - keys_and_values, + NumKeys(NonEmpty::new("keys-and-values", keys_and_values)), precondition, ttl, ), @@ -1126,8 +1142,11 @@ impl Guest for Commands { connection: &Connection, keys_and_values: Vec<(String, String)>, ) -> Result { - let keys_and_values = NonEmpty::new("keys-and-values", keys_and_values)?; - query(connection, ("MSETNX", keys_and_values)).await + query( + connection, + ("MSETNX", NonEmpty::new("keys-and-values", keys_and_values)), + ) + .await } #[doc = " Set key to hold the string value with expiration in milliseconds."] @@ -1249,8 +1268,7 @@ impl Guest for Commands { group: String, ids: Vec, ) -> Result { - let ids = NonEmpty::new("ids", ids)?; - query(connection, ("XACK", key, group, ids)).await + query(connection, ("XACK", key, group, NonEmpty::new("ids", ids))).await } #[doc = " Appends the specified stream entry to the stream at the specified key."] @@ -1266,7 +1284,6 @@ impl Guest for Commands { id: StreamId, field_and_values: Vec<(String, String)>, ) -> Result, Error> { - let field_and_values = NonEmpty::new("field-and-values", field_and_values)?; query( connection, ( @@ -1275,7 +1292,7 @@ impl Guest for Commands { Flag("NOMKSTREAM", nomkstream), trim, id, - field_and_values, + NonEmpty::new("field-and-values", field_and_values), ), ) .await @@ -1369,10 +1386,17 @@ impl Guest for Commands { ids: Vec, opts: Option, ) -> Result)>, Error> { - let ids = NonEmpty::new("ids", ids)?; query( connection, - ("XCLAIM", key, group, consumer, min_idle_time, ids, opts), + ( + "XCLAIM", + key, + group, + consumer, + min_idle_time, + NonEmpty::new("ids", ids), + opts, + ), ) .await } @@ -1393,10 +1417,17 @@ impl Guest for Commands { ids: Vec, opts: Option, ) -> Result, Error> { - let ids = NonEmpty::new("ids", ids)?; query( connection, - ("XCLAIM", key, group, consumer, min_idle_time, ids, opts), + ( + "XCLAIM", + key, + group, + consumer, + min_idle_time, + NonEmpty::new("ids", ids), + opts, + ), ) .await } @@ -1411,8 +1442,7 @@ impl Guest for Commands { key: String, ids: Vec, ) -> Result { - let ids = NonEmpty::new("ids", ids)?; - query(connection, ("XDEL", key, ids)).await + query(connection, ("XDEL", key, NonEmpty::new("ids", ids))).await } #[doc = " Create a new consumer group uniquely identified by for the stream stored at ."] @@ -1654,8 +1684,6 @@ impl Guest for Commands { keys: Vec, ids: Vec, ) -> Result)>>>, Error> { - let keys = NonEmpty::new("keys", keys)?; - let ids = NonEmpty::new("ids", ids)?; query( connection, ( @@ -1663,8 +1691,8 @@ impl Guest for Commands { NamedOption("COUNT", count), NamedOption("BLOCK", block), "STREAMS", - keys, - ids, + NonEmpty::new("keys", keys), + NonEmpty::new("ids", ids), ), ) .await @@ -1688,8 +1716,6 @@ impl Guest for Commands { keys: Vec, ids: Vec, ) -> Result>)>>>, Error> { - let keys = NonEmpty::new("keys", keys)?; - let ids = NonEmpty::new("ids", ids)?; query( connection, ( @@ -1701,8 +1727,8 @@ impl Guest for Commands { NamedOption("BLOCK", block), Flag("NOACK", noack), "STREAMS", - keys, - ids, + NonEmpty::new("keys", keys), + NonEmpty::new("ids", ids), ), ) .await @@ -1775,8 +1801,7 @@ impl Guest for Commands { key: String, members: Vec, ) -> Result { - let members = NonEmpty::new("members", members)?; - query(connection, ("SADD", key, members)).await + query(connection, ("SADD", key, NonEmpty::new("members", members))).await } #[doc = " The number of members in the set stored at key."] @@ -1794,8 +1819,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/sdiff/"] #[allow(async_fn_in_trait)] async fn sdiff(connection: &Connection, keys: Vec) -> Result, Error> { - let keys = NonEmpty::new("keys", keys)?; - query(connection, ("SDIFF", keys)).await + query(connection, ("SDIFF", NonEmpty::new("keys", keys))).await } #[doc = " Stores the members of the first set that are not in any of the others, returning how many."] @@ -1808,8 +1832,11 @@ impl Guest for Commands { destination: String, keys: Vec, ) -> Result { - let keys = NonEmpty::new("keys", keys)?; - query(connection, ("SDIFFSTORE", destination, keys)).await + query( + connection, + ("SDIFFSTORE", destination, NonEmpty::new("keys", keys)), + ) + .await } #[doc = " The members in all of the sets."] @@ -1818,8 +1845,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/sinter/"] #[allow(async_fn_in_trait)] async fn sinter(connection: &Connection, keys: Vec) -> Result, Error> { - let keys = NonEmpty::new("keys", keys)?; - query(connection, ("SINTER", keys)).await + query(connection, ("SINTER", NonEmpty::new("keys", keys))).await } #[doc = " The number of members in all of the sets, counting stops at the limit."] @@ -1832,10 +1858,13 @@ impl Guest for Commands { keys: Vec, limit: Option, ) -> Result { - let keys = NonEmpty::new("keys", keys)?; query( connection, - ("SINTERCARD", NumKeys(keys), NamedOption("LIMIT", limit)), + ( + "SINTERCARD", + NumKeys(NonEmpty::new("keys", keys)), + NamedOption("LIMIT", limit), + ), ) .await } @@ -1850,8 +1879,11 @@ impl Guest for Commands { destination: String, keys: Vec, ) -> Result { - let keys = NonEmpty::new("keys", keys)?; - query(connection, ("SINTERSTORE", destination, keys)).await + query( + connection, + ("SINTERSTORE", destination, NonEmpty::new("keys", keys)), + ) + .await } #[doc = " Whether the member is in the set stored at key, false when the key does not exist."] @@ -1905,8 +1937,11 @@ impl Guest for Commands { key: String, members: Vec, ) -> Result, Error> { - let members = NonEmpty::new("members", members)?; - query(connection, ("SMISMEMBER", key, members)).await + query( + connection, + ("SMISMEMBER", key, NonEmpty::new("members", members)), + ) + .await } #[doc = " Moves the member from one set to another, false when it was not in the source set."] @@ -1974,8 +2009,7 @@ impl Guest for Commands { key: String, members: Vec, ) -> Result { - let members = NonEmpty::new("members", members)?; - query(connection, ("SREM", key, members)).await + query(connection, ("SREM", key, NonEmpty::new("members", members))).await } #[doc = " Incrementally iterate over the members of the set stored at key."] @@ -1986,21 +2020,22 @@ impl Guest for Commands { async fn sscan( connection: &Connection, key: String, - cursor: Option, + cursor: Cursor, match_: Option, count: Option, - ) -> Result<(Cursor, Vec), Error> { - query( + ) -> Result<(Option, Vec), Error> { + let (cursor, items): (NextCursor, Vec) = query( connection, ( "SSCAN", key, - cursor.unwrap_or(Cursor::from("0")), + cursor, NamedOption("MATCH", match_), NamedOption("COUNT", count), ), ) - .await + .await?; + Ok((cursor.0, items)) } #[doc = " The members in any of the sets."] @@ -2009,8 +2044,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/sunion/"] #[allow(async_fn_in_trait)] async fn sunion(connection: &Connection, keys: Vec) -> Result, Error> { - let keys = NonEmpty::new("keys", keys)?; - query(connection, ("SUNION", keys)).await + query(connection, ("SUNION", NonEmpty::new("keys", keys))).await } #[doc = " Stores the members in any of the sets, returning how many."] @@ -2023,8 +2057,11 @@ impl Guest for Commands { destination: String, keys: Vec, ) -> Result { - let keys = NonEmpty::new("keys", keys)?; - query(connection, ("SUNIONSTORE", destination, keys)).await + query( + connection, + ("SUNIONSTORE", destination, NonEmpty::new("keys", keys)), + ) + .await } #[doc = " Sorts the elements of the list, set or sorted set stored at key."] @@ -2040,7 +2077,6 @@ impl Guest for Commands { key: String, opts: Option, ) -> Result>>, Error> { - check_sort_opts(&opts)?; let row = sort_row_len(&opts); let values: Vec> = query(connection, ("SORT", key, opts)).await?; Ok(values.chunks(row).map(<[_]>::to_vec).collect()) @@ -2058,7 +2094,6 @@ impl Guest for Commands { opts: Option, destination: String, ) -> Result { - check_sort_opts(&opts)?; query(connection, ("SORT", key, opts, "STORE", destination)).await } @@ -2072,7 +2107,6 @@ impl Guest for Commands { key: String, opts: Option, ) -> Result>>, Error> { - check_sort_opts(&opts)?; let row = sort_row_len(&opts); let values: Vec> = query(connection, ("SORT_RO", key, opts)).await?; Ok(values.chunks(row).map(<[_]>::to_vec).collect()) @@ -2096,11 +2130,10 @@ impl Guest for Commands { end: PopEnd, count: Option, ) -> Result)>, Error> { - let keys = NonEmpty::new("keys", keys)?; let command = ( "BZMPOP", DecimalSeconds(timeout), - NumKeys(keys), + NumKeys(NonEmpty::new("keys", keys)), end, NamedOption("COUNT", count), ); @@ -2123,8 +2156,15 @@ impl Guest for Commands { keys: Vec, timeout: DurationMilliseconds, ) -> Result, Error> { - let keys = NonEmpty::new("keys", keys)?; - query(connection, ("BZPOPMAX", keys, DecimalSeconds(timeout))).await + query( + connection, + ( + "BZPOPMAX", + NonEmpty::new("keys", keys), + DecimalSeconds(timeout), + ), + ) + .await } #[doc = " Removes the member with the lowest score from the first non-empty sorted set, waiting up to"] @@ -2143,8 +2183,15 @@ impl Guest for Commands { keys: Vec, timeout: DurationMilliseconds, ) -> Result, Error> { - let keys = NonEmpty::new("keys", keys)?; - query(connection, ("BZPOPMIN", keys, DecimalSeconds(timeout))).await + query( + connection, + ( + "BZPOPMIN", + NonEmpty::new("keys", keys), + DecimalSeconds(timeout), + ), + ) + .await } #[doc = " Adds the members with their scores to the sorted set stored at key, or updates their scores,"] @@ -2163,14 +2210,13 @@ impl Guest for Commands { changed: bool, score_and_members: Vec<(Double, String)>, ) -> Result, Error> { - let score_and_members = NonEmpty::new("score-and-members", score_and_members)?; let command = ( "ZADD", key, condition, comparison, Flag("CH", changed), - score_and_members, + NonEmpty::new("score-and-members", score_and_members), ); query(connection, command).await } @@ -2240,8 +2286,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/zdiff/"] #[allow(async_fn_in_trait)] async fn zdiff(connection: &Connection, keys: Vec) -> Result, Error> { - let keys = NonEmpty::new("keys", keys)?; - query(connection, ("ZDIFF", NumKeys(keys))).await + query(connection, ("ZDIFF", NumKeys(NonEmpty::new("keys", keys)))).await } #[doc = " The members, and their scores, of the first sorted set that are not in any of the others."] @@ -2255,8 +2300,11 @@ impl Guest for Commands { connection: &Connection, keys: Vec, ) -> Result, Error> { - let keys = NonEmpty::new("keys", keys)?; - query(connection, ("ZDIFF", NumKeys(keys), "WITHSCORES")).await + query( + connection, + ("ZDIFF", NumKeys(NonEmpty::new("keys", keys)), "WITHSCORES"), + ) + .await } #[doc = " Stores the members of the first sorted set that are not in any of the others, returning how"] @@ -2270,8 +2318,15 @@ impl Guest for Commands { destination: String, keys: Vec, ) -> Result { - let keys = NonEmpty::new("keys", keys)?; - query(connection, ("ZDIFFSTORE", destination, NumKeys(keys))).await + query( + connection, + ( + "ZDIFFSTORE", + destination, + NumKeys(NonEmpty::new("keys", keys)), + ), + ) + .await } #[doc = " Increments the score of the member in the sorted set stored at key, returning the new score."] @@ -2303,14 +2358,12 @@ impl Guest for Commands { weights: Option>, aggregate: Option, ) -> Result, Error> { - let keys = NonEmpty::new("keys", keys)?; - let weights = NonEmpty::optional("weights", weights)?; query( connection, ( "ZINTER", - NumKeys(keys), - NamedOption("WEIGHTS", weights), + NumKeys(NonEmpty::new("keys", keys)), + NamedOption("WEIGHTS", NonEmpty::optional("weights", weights)), aggregate, ), ) @@ -2330,12 +2383,10 @@ impl Guest for Commands { weights: Option>, aggregate: Option, ) -> Result, Error> { - let keys = NonEmpty::new("keys", keys)?; - let weights = NonEmpty::optional("weights", weights)?; let command = ( "ZINTER", - NumKeys(keys), - NamedOption("WEIGHTS", weights), + NumKeys(NonEmpty::new("keys", keys)), + NamedOption("WEIGHTS", NonEmpty::optional("weights", weights)), aggregate, "WITHSCORES", ); @@ -2352,10 +2403,13 @@ impl Guest for Commands { keys: Vec, limit: Option, ) -> Result { - let keys = NonEmpty::new("keys", keys)?; query( connection, - ("ZINTERCARD", NumKeys(keys), NamedOption("LIMIT", limit)), + ( + "ZINTERCARD", + NumKeys(NonEmpty::new("keys", keys)), + NamedOption("LIMIT", limit), + ), ) .await } @@ -2372,13 +2426,11 @@ impl Guest for Commands { weights: Option>, aggregate: Option, ) -> Result { - let keys = NonEmpty::new("keys", keys)?; - let weights = NonEmpty::optional("weights", weights)?; let command = ( "ZINTERSTORE", destination, - NumKeys(keys), - NamedOption("WEIGHTS", weights), + NumKeys(NonEmpty::new("keys", keys)), + NamedOption("WEIGHTS", NonEmpty::optional("weights", weights)), aggregate, ); query(connection, command).await @@ -2423,10 +2475,14 @@ impl Guest for Commands { end: PopEnd, count: Option, ) -> Result)>, Error> { - let keys = NonEmpty::new("keys", keys)?; query( connection, - ("ZMPOP", NumKeys(keys), end, NamedOption("COUNT", count)), + ( + "ZMPOP", + NumKeys(NonEmpty::new("keys", keys)), + end, + NamedOption("COUNT", count), + ), ) .await } @@ -2442,8 +2498,11 @@ impl Guest for Commands { key: String, members: Vec, ) -> Result>, Error> { - let members = NonEmpty::new("members", members)?; - query(connection, ("ZMSCORE", key, members)).await + query( + connection, + ("ZMSCORE", key, NonEmpty::new("members", members)), + ) + .await } #[doc = " Removes and returns the members with the highest scores, and their scores, up to the count,"] @@ -2630,8 +2689,7 @@ impl Guest for Commands { key: String, members: Vec, ) -> Result { - let members = NonEmpty::new("members", members)?; - query(connection, ("ZREM", key, members)).await + query(connection, ("ZREM", key, NonEmpty::new("members", members))).await } #[doc = " Removes the members of the sorted set stored at key in the range, for members of equal score,"] @@ -2745,23 +2803,23 @@ impl Guest for Commands { async fn zscan( connection: &Connection, key: String, - cursor: Option, + cursor: Cursor, match_: Option, count: Option, - ) -> Result<(Cursor, Vec<(String, Double)>), Error> { + ) -> Result<(Option, Vec<(String, Double)>), Error> { // the members and scores are a flat array, the scores bulk strings - let (cursor, members): (Cursor, FlatPairs) = query( + let (cursor, members): (NextCursor, FlatPairs) = query( connection, ( "ZSCAN", key, - cursor.unwrap_or(Cursor::from("0")), + cursor, NamedOption("MATCH", match_), NamedOption("COUNT", count), ), ) .await?; - Ok((cursor, members.0)) + Ok((cursor.0, members.0)) } #[doc = " Incrementally iterate over the members of the sorted set stored at key."] @@ -2774,22 +2832,23 @@ impl Guest for Commands { async fn zscan_noscores( connection: &Connection, key: String, - cursor: Option, + cursor: Cursor, match_: Option, count: Option, - ) -> Result<(Cursor, Vec), Error> { - query( + ) -> Result<(Option, Vec), Error> { + let (cursor, items): (NextCursor, Vec) = query( connection, ( "ZSCAN", key, - cursor.unwrap_or(Cursor::from("0")), + cursor, NamedOption("MATCH", match_), NamedOption("COUNT", count), "NOSCORES", ), ) - .await + .await?; + Ok((cursor.0, items)) } #[doc = " The score of the member in the sorted set stored at key, none when it is not a member."] @@ -2820,14 +2879,12 @@ impl Guest for Commands { weights: Option>, aggregate: Option, ) -> Result, Error> { - let keys = NonEmpty::new("keys", keys)?; - let weights = NonEmpty::optional("weights", weights)?; query( connection, ( "ZUNION", - NumKeys(keys), - NamedOption("WEIGHTS", weights), + NumKeys(NonEmpty::new("keys", keys)), + NamedOption("WEIGHTS", NonEmpty::optional("weights", weights)), aggregate, ), ) @@ -2847,12 +2904,10 @@ impl Guest for Commands { weights: Option>, aggregate: Option, ) -> Result, Error> { - let keys = NonEmpty::new("keys", keys)?; - let weights = NonEmpty::optional("weights", weights)?; let command = ( "ZUNION", - NumKeys(keys), - NamedOption("WEIGHTS", weights), + NumKeys(NonEmpty::new("keys", keys)), + NamedOption("WEIGHTS", NonEmpty::optional("weights", weights)), aggregate, "WITHSCORES", ); @@ -2871,13 +2926,11 @@ impl Guest for Commands { weights: Option>, aggregate: Option, ) -> Result { - let keys = NonEmpty::new("keys", keys)?; - let weights = NonEmpty::optional("weights", weights)?; let command = ( "ZUNIONSTORE", destination, - NumKeys(keys), - NamedOption("WEIGHTS", weights), + NumKeys(NonEmpty::new("keys", keys)), + NamedOption("WEIGHTS", NonEmpty::optional("weights", weights)), aggregate, ); query(connection, command).await diff --git a/components/commands/src/value.rs b/components/commands/src/value.rs index abceeb4..a16e877 100644 --- a/components/commands/src/value.rs +++ b/components/commands/src/value.rs @@ -1,9 +1,9 @@ use crate::exports::componentized::valkey::commands::{ - Double, DurationMilliseconds, DurationSeconds, Error, FieldExpiretime, FieldPersistResult, - FieldPexpiretime, FieldPttl, FieldTtl, Integer, LcsIdxMatch, LcsIdxReply, StreamId, TtlResult, - UnixTimeMilliseconds, UnixTimeSeconds, Value, XinfoConsumer, XinfoGroup, XinfoStreamReply, - XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, - XpendingExtendedEntry, XpendingSummary, + Cursor, Double, DurationMilliseconds, DurationSeconds, Error, FieldExpiretime, + FieldPersistResult, FieldPexpiretime, FieldPttl, FieldTtl, Integer, LcsIdxMatch, LcsIdxReply, + StreamId, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, XinfoConsumer, XinfoGroup, + XinfoStreamReply, XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, + XinfoStreamReplyFullGroup, XpendingExtendedEntry, XpendingSummary, }; use resp_codec::NestedValue; @@ -325,6 +325,18 @@ impl FromValue for FieldPersistResult { } } +/// The cursor a scan returns to resume from, none once the scan is complete, `0`. +pub struct NextCursor(pub Option); + +impl FromValue for NextCursor { + fn from_value(value: Value) -> Result { + let cursor = String::from_value(value)?; + Ok(NextCursor( + (cursor != "0").then_some(Cursor::Resume(cursor)), + )) + } +} + impl FromValue for TtlResult { fn from_value(value: Value) -> Result { match Integer::from_value(value)? { diff --git a/wit/commands.wit b/wit/commands.wit index fa31fcb..b01d344 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -27,6 +27,18 @@ interface commands { pxat: s64, } + /// Where an incremental iteration continues from, e.g. with HSCAN. + /// + /// A scan returns the cursor to resume from, always `resume`, none once the scan is complete. + /// Pass it to the next call until then. + variant cursor { + /// Starts a new scan, the cursor `0`. + start, + /// Continues a scan from the opaque cursor the previous call returned, `0` fails with a client + /// error, use `start`. + resume(string), + } + /// XXXXXX /// /// XXXXXX @@ -555,18 +567,13 @@ interface commands { /// https://valkey.io/commands/hrandfield/ hrandfield-withvalues: async func(connection: borrow, key: string, count: integer) -> result>, error>; - /// The position of an incremental iteration, e.g. with HSCAN. - /// - /// None starts a new iteration, the iteration is complete once the cursor returned is `0`. - type cursor = string; - /// Incrementally iterate over a collection of elements. /// /// See also `hscan-novalues`. /// /// HSCAN key cursor [ MATCH pattern ] [ COUNT count ] /// https://valkey.io/commands/hscan/ - hscan: async func(connection: borrow, key: string, cursor: option, match: option, count: option) -> result>, error>; + hscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, map>, error>; /// Incrementally iterate over a collection of keys. /// @@ -574,7 +581,7 @@ interface commands { /// /// HSCAN key cursor [ MATCH pattern ] [ COUNT count ] NOVALUES /// https://valkey.io/commands/hscan/ - hscan-novalues: async func(connection: borrow, key: string, cursor: option, match: option, count: option) -> result>, error>; + hscan-novalues: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; /// Sets the specified fields to their respective values in the hash stored at key. /// @@ -947,7 +954,7 @@ interface commands { /// /// SSCAN key cursor [ MATCH pattern ] [ COUNT count ] /// https://valkey.io/commands/sscan/ - sscan: async func(connection: borrow, key: string, cursor: option, match: option, count: option) -> result>, error>; + sscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; /// The members in any of the sets. /// @@ -1374,7 +1381,7 @@ interface commands { /// /// ZSCAN key cursor [ MATCH pattern ] [ COUNT count ] /// https://valkey.io/commands/zscan/ - zscan: async func(connection: borrow, key: string, cursor: option, match: option, count: option) -> result>>, error>; + zscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>>, error>; /// Incrementally iterate over the members of the sorted set stored at key. /// @@ -1382,7 +1389,7 @@ interface commands { /// /// ZSCAN key cursor [ MATCH pattern ] [ COUNT count ] NOSCORES /// https://valkey.io/commands/zscan/ - zscan-noscores: async func(connection: borrow, key: string, cursor: option, match: option, count: option) -> result>, error>; + zscan-noscores: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; /// The score of the member in the sorted set stored at key, none when it is not a member. /// From d4a1140b2e480e52c4adf0cabae4480e640bf768 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Mon, 12 Oct 2026 22:26:33 -0400 Subject: [PATCH 10/37] hyperlog commands Signed-off-by: Scott Andrews --- components/commands/src/lib.rs | 54 ++++++++++++++++++++++++++++++++++ wit/commands.wit | 35 +++++++++++++++++----- 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index 9f916b6..7207df6 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -655,6 +655,60 @@ impl Guest for Commands { query(connection, ("HVALS", key)).await } + #[doc = " Adds the elements to the HyperLogLog stored at key, creating it when it does not exist,"] + #[doc = " returning whether its estimate may have changed."] + #[doc = ""] + #[doc = " Without elements, creates an empty HyperLogLog, true only when the key did not exist."] + #[doc = ""] + #[doc = " PFADD key [ element [ element ... ] ]"] + #[doc = " https://valkey.io/commands/pfadd/"] + #[allow(async_fn_in_trait)] + async fn pfadd( + connection: &Connection, + key: String, + elements: Option>, + ) -> Result { + query( + connection, + ("PFADD", key, NonEmpty::optional("elements", elements)), + ) + .await + } + + #[doc = " The approximate number of unique elements added to the HyperLogLogs, of their union for"] + #[doc = " more than one, 0 for a key that does not exist."] + #[doc = ""] + #[doc = " PFCOUNT key [ key ... ]"] + #[doc = " https://valkey.io/commands/pfcount/"] + #[allow(async_fn_in_trait)] + async fn pfcount(connection: &Connection, keys: Vec) -> Result { + query(connection, ("PFCOUNT", NonEmpty::new("keys", keys))).await + } + + #[doc = " Merges the HyperLogLogs stored at the source keys into the one stored at destination,"] + #[doc = " creating it when it does not exist."] + #[doc = ""] + #[doc = " Without source keys, creates an empty HyperLogLog when destination does not exist."] + #[doc = ""] + #[doc = " PFMERGE destkey [ sourcekey [ sourcekey ... ] ]"] + #[doc = " https://valkey.io/commands/pfmerge/"] + #[allow(async_fn_in_trait)] + async fn pfmerge( + connection: &Connection, + destination: String, + sources: Option>, + ) -> Result<(), Error> { + query( + connection, + ( + "PFMERGE", + destination, + NonEmpty::optional("sources", sources), + ), + ) + .await + } + #[doc = " Removes the specified keys."] #[doc = ""] #[doc = " DEL key [ key ... ]"] diff --git a/wit/commands.wit b/wit/commands.wit index b01d344..6177509 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -639,14 +639,35 @@ interface commands { hvals: async func(connection: borrow, key: string) -> result, error>; - // hyperlog commands + // hyperloglog commands - // not yet implemented, from the @hyperloglog category: - // PFADD - // PFCOUNT - // PFDEBUG - // PFMERGE - // PFSELFTEST + /// Adds the elements to the HyperLogLog stored at key, creating it when it does not exist, + /// returning whether its estimate may have changed. + /// + /// Without elements, creates an empty HyperLogLog, true only when the key did not exist. + /// + /// PFADD key [ element [ element ... ] ] + /// https://valkey.io/commands/pfadd/ + pfadd: async func(connection: borrow, key: string, elements: option>) -> result; + + /// The approximate number of unique elements added to the HyperLogLogs, of their union for + /// more than one, 0 for a key that does not exist. + /// + /// PFCOUNT key [ key ... ] + /// https://valkey.io/commands/pfcount/ + pfcount: async func(connection: borrow, keys: list) -> result; + + /// Merges the HyperLogLogs stored at the source keys into the one stored at destination, + /// creating it when it does not exist. + /// + /// Without source keys, creates an empty HyperLogLog when destination does not exist. + /// + /// PFMERGE destkey [ sourcekey [ sourcekey ... ] ] + /// https://valkey.io/commands/pfmerge/ + pfmerge: async func(connection: borrow, destination: string, sources: option>) -> result<_, error>; + + // PFDEBUG and PFSELFTEST are internal commands, for debugging and testing Valkey itself, and + // are not implemented // keyspace commands From f308cbc77c3206bb66e46f194539bb303885d66c Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 13 Oct 2026 08:48:08 -0400 Subject: [PATCH 11/37] list commands Signed-off-by: Scott Andrews --- components/commands/src/command.rs | 27 +- components/commands/src/lib.rs | 471 ++++++++++++++++++++++++++++- components/commands/src/value.rs | 20 +- wit/commands.wit | 240 +++++++++++++-- 4 files changed, 719 insertions(+), 39 deletions(-) diff --git a/components/commands/src/command.rs b/components/commands/src/command.rs index adc3d9b..262d4df 100644 --- a/components/commands/src/command.rs +++ b/components/commands/src/command.rs @@ -1,9 +1,10 @@ use crate::exports::componentized::valkey::commands::{ Aggregate, ChannelRule, CommandRule, Connection, Cursor, DbRule, DurationMilliseconds, DurationSeconds, Error, GetexTtl, HsetexCondition, HsetexFieldCondition, KeyRule, LexBound, - Limit, MsetexPrecondition, PopEnd, Rule, RuleSelector, ScoreBound, SetPrecondition, SetTtl, - SortOpts, SortOrder, StreamTrim, StreamTrimEfficiency, TtlMode, UnixTimeMilliseconds, - UnixTimeSeconds, UserRule, Value, XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, + Limit, LinsertPosition, ListSide, MsetexPrecondition, PopEnd, Rule, RuleSelector, ScoreBound, + SetPrecondition, SetTtl, SortOpts, SortOrder, StreamTrim, StreamTrimEfficiency, TtlMode, + UnixTimeMilliseconds, UnixTimeSeconds, UserRule, Value, XclaimOpts, ZaddComparison, + ZaddCondition, ZrangeBy, }; use crate::value::{FromValue, OrZeroId, ZERO_ID}; @@ -626,3 +627,23 @@ impl IntoCommand for Cursor { } } } + +impl IntoCommand for ListSide { + fn into_command(self) -> Result, Error> { + match self { + ListSide::Left => "LEFT", + ListSide::Right => "RIGHT", + } + .into_command() + } +} + +impl IntoCommand for LinsertPosition { + fn into_command(self) -> Result, Error> { + match self { + LinsertPosition::Before => "BEFORE", + LinsertPosition::After => "AFTER", + } + .into_command() + } +} diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index 7207df6..d7bac56 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -5,12 +5,12 @@ use crate::command::{ use crate::exports::componentized::valkey::commands::{ Aggregate, Connection, Cursor, Double, DurationMilliseconds, DurationSeconds, Error, FieldExpiretime, FieldPersistResult, FieldPexpiretime, FieldPttl, FieldTtl, GetexTtl, Guest, - HsetexCondition, HsetexFieldCondition, Integer, LcsIdxReply, LexBound, Limit, - MsetexPrecondition, MsetexTtl, PopEnd, Rule, ScoreBound, SetPrecondition, SetTtl, SortOpts, - StreamId, StreamTrim, TtlMode, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, - XclaimOpts, XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, - XpendingExtendedEntry, XpendingSummary, ZaddComparison, ZaddCondition, ZrangeBy, - ZrangeWithscoresBy, + HsetexCondition, HsetexFieldCondition, Integer, LcsIdxReply, LexBound, Limit, LinsertPosition, + LinsertResult, ListSide, MsetexPrecondition, MsetexTtl, PopEnd, Rule, ScoreBound, + SetPrecondition, SetTtl, SortOpts, StreamId, StreamTrim, TtlMode, TtlResult, + UnixTimeMilliseconds, UnixTimeSeconds, Value, XclaimOpts, XinfoConsumer, XinfoGroup, + XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, ZaddComparison, + ZaddCondition, ZrangeBy, ZrangeWithscoresBy, }; use crate::value::{FlatPairs, NextCursor, OrNever, OrZeroId, ScoredMembers}; @@ -709,6 +709,465 @@ impl Guest for Commands { .await } + #[doc = " Moves an element from one end of the source list to an end of the destination list, waiting"] + #[doc = " up to the timeout for the source to have one, returning it."] + #[doc = ""] + #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] + #[doc = ""] + #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] + #[doc = " it. Use a connection of your own."] + #[doc = ""] + #[doc = " BLMOVE source destination 〈 LEFT | RIGHT 〉 〈 LEFT | RIGHT 〉 timeout"] + #[doc = " https://valkey.io/commands/blmove/"] + #[allow(async_fn_in_trait)] + async fn blmove( + connection: &Connection, + source: String, + destination: String, + wherefrom: ListSide, + whereto: ListSide, + timeout: DurationMilliseconds, + ) -> Result, Error> { + query( + connection, + ( + "BLMOVE", + source, + destination, + wherefrom, + whereto, + DecimalSeconds(timeout), + ), + ) + .await + } + + #[doc = " Removes elements from an end of the first non-empty list, waiting up to the timeout for one,"] + #[doc = " returning its key and the elements, one without a count."] + #[doc = ""] + #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] + #[doc = ""] + #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] + #[doc = " it. Use a connection of your own."] + #[doc = ""] + #[doc = " BLMPOP timeout numkeys key [ key ... ] 〈 LEFT | RIGHT 〉 [ COUNT count ]"] + #[doc = " https://valkey.io/commands/blmpop/"] + #[allow(async_fn_in_trait)] + async fn blmpop( + connection: &Connection, + timeout: DurationMilliseconds, + keys: Vec, + end: ListSide, + count: Option, + ) -> Result)>, Error> { + query( + connection, + ( + "BLMPOP", + DecimalSeconds(timeout), + NumKeys(NonEmpty::new("keys", keys)), + end, + NamedOption("COUNT", count), + ), + ) + .await + } + + #[doc = " Removes the first element of the first non-empty list, waiting up to the timeout for one,"] + #[doc = " returning its key and the element."] + #[doc = ""] + #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] + #[doc = ""] + #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] + #[doc = " it. Use a connection of your own."] + #[doc = ""] + #[doc = " BLPOP key [ key ... ] timeout"] + #[doc = " https://valkey.io/commands/blpop/"] + #[allow(async_fn_in_trait)] + async fn blpop( + connection: &Connection, + keys: Vec, + timeout: DurationMilliseconds, + ) -> Result, Error> { + query( + connection, + ( + "BLPOP", + NonEmpty::new("keys", keys), + DecimalSeconds(timeout), + ), + ) + .await + } + + #[doc = " Removes the last element of the first non-empty list, waiting up to the timeout for one,"] + #[doc = " returning its key and the element."] + #[doc = ""] + #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] + #[doc = ""] + #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] + #[doc = " it. Use a connection of your own."] + #[doc = ""] + #[doc = " BRPOP key [ key ... ] timeout"] + #[doc = " https://valkey.io/commands/brpop/"] + #[allow(async_fn_in_trait)] + async fn brpop( + connection: &Connection, + keys: Vec, + timeout: DurationMilliseconds, + ) -> Result, Error> { + query( + connection, + ( + "BRPOP", + NonEmpty::new("keys", keys), + DecimalSeconds(timeout), + ), + ) + .await + } + + #[doc = " Moves the last element of the source list to the head of the destination list, waiting up"] + #[doc = " to the timeout for the source to have one, returning it."] + #[doc = ""] + #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] + #[doc = ""] + #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] + #[doc = " it. Use a connection of your own."] + #[doc = ""] + #[doc = " See also `blmove`."] + #[doc = ""] + #[doc = " BRPOPLPUSH source destination timeout"] + #[doc = " https://valkey.io/commands/brpoplpush/"] + #[allow(async_fn_in_trait)] + async fn brpoplpush( + connection: &Connection, + source: String, + destination: String, + timeout: DurationMilliseconds, + ) -> Result, Error> { + query( + connection, + ("BRPOPLPUSH", source, destination, DecimalSeconds(timeout)), + ) + .await + } + + #[doc = " The element at the index of the list stored at key, negative from the end, none when the"] + #[doc = " index is out of range."] + #[doc = ""] + #[doc = " LINDEX key index"] + #[doc = " https://valkey.io/commands/lindex/"] + #[allow(async_fn_in_trait)] + async fn lindex( + connection: &Connection, + key: String, + index: Integer, + ) -> Result, Error> { + query(connection, ("LINDEX", key, index)).await + } + + #[doc = " Inserts the element before or after the first occurrence of the pivot in the list stored at"] + #[doc = " key."] + #[doc = ""] + #[doc = " LINSERT key 〈 BEFORE | AFTER 〉 pivot element"] + #[doc = " https://valkey.io/commands/linsert/"] + #[allow(async_fn_in_trait)] + async fn linsert( + connection: &Connection, + key: String, + position: LinsertPosition, + pivot: String, + element: String, + ) -> Result { + query(connection, ("LINSERT", key, position, pivot, element)).await + } + + #[doc = " The length of the list stored at key, 0 when the key does not exist."] + #[doc = ""] + #[doc = " LLEN key"] + #[doc = " https://valkey.io/commands/llen/"] + #[allow(async_fn_in_trait)] + async fn llen(connection: &Connection, key: String) -> Result { + query(connection, ("LLEN", key)).await + } + + #[doc = " Moves an element from one end of the source list to an end of the destination list,"] + #[doc = " returning it, none when the source does not exist."] + #[doc = ""] + #[doc = " LMOVE source destination 〈 LEFT | RIGHT 〉 〈 LEFT | RIGHT 〉"] + #[doc = " https://valkey.io/commands/lmove/"] + #[allow(async_fn_in_trait)] + async fn lmove( + connection: &Connection, + source: String, + destination: String, + wherefrom: ListSide, + whereto: ListSide, + ) -> Result, Error> { + query( + connection, + ("LMOVE", source, destination, wherefrom, whereto), + ) + .await + } + + #[doc = " Removes elements from an end of the first non-empty list, returning its key and the"] + #[doc = " elements, one without a count, none when every list is empty."] + #[doc = ""] + #[doc = " LMPOP numkeys key [ key ... ] 〈 LEFT | RIGHT 〉 [ COUNT count ]"] + #[doc = " https://valkey.io/commands/lmpop/"] + #[allow(async_fn_in_trait)] + async fn lmpop( + connection: &Connection, + keys: Vec, + end: ListSide, + count: Option, + ) -> Result)>, Error> { + let command = ( + "LMPOP", + NumKeys(NonEmpty::new("keys", keys)), + end, + NamedOption("COUNT", count), + ); + query(connection, command).await + } + + #[doc = " Removes and returns the first elements of the list stored at key, up to the count, one"] + #[doc = " without a count, none when the key does not exist."] + #[doc = ""] + #[doc = " LPOP key [ count ]"] + #[doc = " https://valkey.io/commands/lpop/"] + #[allow(async_fn_in_trait)] + async fn lpop( + connection: &Connection, + key: String, + count: Option, + ) -> Result, Error> { + match count { + Some(count) => { + // null when the key does not exist + let elements: Option> = query(connection, ("LPOP", key, count)).await?; + Ok(elements.unwrap_or_default()) + } + None => { + // without a count the reply is the element itself, or null + let element: Option = query(connection, ("LPOP", key)).await?; + Ok(element.into_iter().collect()) + } + } + } + + #[doc = " The positions of the element in the list stored at key, up to the count, 0 for every match,"] + #[doc = " the first match without a count."] + #[doc = ""] + #[doc = " The rank skips matches, the second match for 2, counting from the end when negative. The"] + #[doc = " max length limits how many elements are compared."] + #[doc = ""] + #[doc = " LPOS key element [ RANK rank ] [ COUNT num-matches ] [ MAXLEN len ]"] + #[doc = " https://valkey.io/commands/lpos/"] + #[allow(async_fn_in_trait)] + async fn lpos( + connection: &Connection, + key: String, + element: String, + rank: Option, + count: Option, + maxlen: Option, + ) -> Result, Error> { + let command = ( + "LPOS", + key, + element, + NamedOption("RANK", rank), + NamedOption("COUNT", count), + NamedOption("MAXLEN", maxlen), + ); + + match count { + Some(_) => query(connection, command).await, + None => { + // without a count the reply is the position itself, or null + let position: Option = query(connection, command).await?; + Ok(position.into_iter().collect()) + } + } + } + + #[doc = " Inserts the elements at the head of the list stored at key, creating it when it does not"] + #[doc = " exist, returning its length after."] + #[doc = ""] + #[doc = " LPUSH key element [ element ... ]"] + #[doc = " https://valkey.io/commands/lpush/"] + #[allow(async_fn_in_trait)] + async fn lpush( + connection: &Connection, + key: String, + elements: Vec, + ) -> Result { + query( + connection, + ("LPUSH", key, NonEmpty::new("elements", elements)), + ) + .await + } + + #[doc = " Inserts the elements at the head of the list stored at key, only when it exists, returning"] + #[doc = " its length after, 0 when it does not exist."] + #[doc = ""] + #[doc = " LPUSHX key element [ element ... ]"] + #[doc = " https://valkey.io/commands/lpushx/"] + #[allow(async_fn_in_trait)] + async fn lpushx( + connection: &Connection, + key: String, + elements: Vec, + ) -> Result { + query( + connection, + ("LPUSHX", key, NonEmpty::new("elements", elements)), + ) + .await + } + + #[doc = " The elements of the list stored at key from the start to the stop, both included, negative"] + #[doc = " from the end."] + #[doc = ""] + #[doc = " LRANGE key start stop"] + #[doc = " https://valkey.io/commands/lrange/"] + #[allow(async_fn_in_trait)] + async fn lrange( + connection: &Connection, + key: String, + start: Integer, + stop: Integer, + ) -> Result, Error> { + query(connection, ("LRANGE", key, start, stop)).await + } + + #[doc = " Removes occurrences of the element from the list stored at key, returning how many."] + #[doc = ""] + #[doc = " A positive count removes up to that many from the head, a negative count from the tail, and"] + #[doc = " 0 removes them all."] + #[doc = ""] + #[doc = " LREM key count element"] + #[doc = " https://valkey.io/commands/lrem/"] + #[allow(async_fn_in_trait)] + async fn lrem( + connection: &Connection, + key: String, + count: Integer, + element: String, + ) -> Result { + query(connection, ("LREM", key, count, element)).await + } + + #[doc = " Sets the element at the index of the list stored at key, negative from the end."] + #[doc = ""] + #[doc = " LSET key index element"] + #[doc = " https://valkey.io/commands/lset/"] + #[allow(async_fn_in_trait)] + async fn lset( + connection: &Connection, + key: String, + index: Integer, + element: String, + ) -> Result<(), Error> { + query(connection, ("LSET", key, index, element)).await + } + + #[doc = " Trims the list stored at key to the elements from the start to the stop, both included,"] + #[doc = " negative from the end."] + #[doc = ""] + #[doc = " LTRIM key start stop"] + #[doc = " https://valkey.io/commands/ltrim/"] + #[allow(async_fn_in_trait)] + async fn ltrim( + connection: &Connection, + key: String, + start: Integer, + stop: Integer, + ) -> Result<(), Error> { + query(connection, ("LTRIM", key, start, stop)).await + } + + #[doc = " Removes and returns the last elements of the list stored at key, up to the count, one"] + #[doc = " without a count, none when the key does not exist."] + #[doc = ""] + #[doc = " RPOP key [ count ]"] + #[doc = " https://valkey.io/commands/rpop/"] + #[allow(async_fn_in_trait)] + async fn rpop( + connection: &Connection, + key: String, + count: Option, + ) -> Result, Error> { + match count { + Some(count) => { + // null when the key does not exist + let elements: Option> = query(connection, ("RPOP", key, count)).await?; + Ok(elements.unwrap_or_default()) + } + None => { + // without a count the reply is the element itself, or null + let element: Option = query(connection, ("RPOP", key)).await?; + Ok(element.into_iter().collect()) + } + } + } + + #[doc = " Moves the last element of the source list to the head of the destination list, returning"] + #[doc = " it, none when the source does not exist."] + #[doc = ""] + #[doc = " See also `lmove`."] + #[doc = ""] + #[doc = " RPOPLPUSH source destination"] + #[doc = " https://valkey.io/commands/rpoplpush/"] + #[allow(async_fn_in_trait)] + async fn rpoplpush( + connection: &Connection, + source: String, + destination: String, + ) -> Result, Error> { + query(connection, ("RPOPLPUSH", source, destination)).await + } + + #[doc = " Inserts the elements at the tail of the list stored at key, creating it when it does not"] + #[doc = " exist, returning its length after."] + #[doc = ""] + #[doc = " RPUSH key element [ element ... ]"] + #[doc = " https://valkey.io/commands/rpush/"] + #[allow(async_fn_in_trait)] + async fn rpush( + connection: &Connection, + key: String, + elements: Vec, + ) -> Result { + query( + connection, + ("RPUSH", key, NonEmpty::new("elements", elements)), + ) + .await + } + + #[doc = " Inserts the elements at the tail of the list stored at key, only when it exists, returning"] + #[doc = " its length after, 0 when it does not exist."] + #[doc = ""] + #[doc = " RPUSHX key element [ element ... ]"] + #[doc = " https://valkey.io/commands/rpushx/"] + #[allow(async_fn_in_trait)] + async fn rpushx( + connection: &Connection, + key: String, + elements: Vec, + ) -> Result { + query( + connection, + ("RPUSHX", key, NonEmpty::new("elements", elements)), + ) + .await + } + #[doc = " Removes the specified keys."] #[doc = ""] #[doc = " DEL key [ key ... ]"] diff --git a/components/commands/src/value.rs b/components/commands/src/value.rs index a16e877..d993656 100644 --- a/components/commands/src/value.rs +++ b/components/commands/src/value.rs @@ -1,9 +1,10 @@ use crate::exports::componentized::valkey::commands::{ Cursor, Double, DurationMilliseconds, DurationSeconds, Error, FieldExpiretime, FieldPersistResult, FieldPexpiretime, FieldPttl, FieldTtl, Integer, LcsIdxMatch, LcsIdxReply, - StreamId, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, XinfoConsumer, XinfoGroup, - XinfoStreamReply, XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, - XinfoStreamReplyFullGroup, XpendingExtendedEntry, XpendingSummary, + LinsertResult, StreamId, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, + XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, + XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, XpendingExtendedEntry, + XpendingSummary, }; use resp_codec::NestedValue; @@ -337,6 +338,19 @@ impl FromValue for NextCursor { } } +/// The length of the list after the insert, `0` when the key does not exist, `-1` when the pivot +/// was not found. +impl FromValue for LinsertResult { + fn from_value(value: Value) -> Result { + match Integer::from_value(value)? { + -1 => Ok(Self::PivotNotFound), + 0 => Ok(Self::NoSuchKey), + length if length > 0 => Ok(Self::Inserted(length)), + code => Err(Error::Client(format!("unexpected reply: {code}"))), + } + } +} + impl FromValue for TtlResult { fn from_value(value: Value) -> Result { match Integer::from_value(value)? { diff --git a/wit/commands.wit b/wit/commands.wit index 6177509..0671169 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -737,31 +737,217 @@ interface commands { // list commands - // not yet implemented, from the @list category: - // BLMOVE - // BLMPOP - // BLPOP - // BRPOP - // BRPOPLPUSH - // LINDEX - // LINSERT - // LLEN - // LMOVE - // LMPOP - // LPOP - // LPOS - // LPUSH - // LPUSHX - // LRANGE - // LREM - // LSET - // LTRIM - // RPOP - // RPOPLPUSH - // RPUSH - // RPUSHX - // SORT - // SORT_RO + /// An end of a list. + enum list-side { + /// `LEFT`: the head, the first element. + left, + /// `RIGHT`: the tail, the last element. + right, + } + + /// Where LINSERT inserts an element, relative to the pivot. + enum linsert-position { + /// `BEFORE`: in front of the pivot. + before, + /// `AFTER`: behind the pivot. + after, + } + + /// The outcome of LINSERT. + variant linsert-result { + /// The element was inserted, the length of the list after it. + inserted(integer), + /// 0: the key does not exist, nothing was inserted. + no-such-key, + /// -1: the pivot is not in the list, nothing was inserted. + pivot-not-found, + } + + /// Moves an element from one end of the source list to an end of the destination list, waiting + /// up to the timeout for the source to have one, returning it. + /// + /// Returns none when the timeout passed. A timeout of zero waits indefinitely. + /// + /// The connection waits too, replies to commands sent after it on the connection wait behind + /// it. Use a connection of your own. + /// + /// BLMOVE source destination 〈 LEFT | RIGHT 〉 〈 LEFT | RIGHT 〉 timeout + /// https://valkey.io/commands/blmove/ + blmove: async func(connection: borrow, source: string, destination: string, wherefrom: list-side, whereto: list-side, timeout: duration-milliseconds) -> result, error>; + + /// Removes elements from an end of the first non-empty list, waiting up to the timeout for one, + /// returning its key and the elements, one without a count. + /// + /// Returns none when the timeout passed. A timeout of zero waits indefinitely. + /// + /// The connection waits too, replies to commands sent after it on the connection wait behind + /// it. Use a connection of your own. + /// + /// BLMPOP timeout numkeys key [ key ... ] 〈 LEFT | RIGHT 〉 [ COUNT count ] + /// https://valkey.io/commands/blmpop/ + blmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: list-side, count: option) -> result>>, error>; + + /// Removes the first element of the first non-empty list, waiting up to the timeout for one, + /// returning its key and the element. + /// + /// Returns none when the timeout passed. A timeout of zero waits indefinitely. + /// + /// The connection waits too, replies to commands sent after it on the connection wait behind + /// it. Use a connection of your own. + /// + /// BLPOP key [ key ... ] timeout + /// https://valkey.io/commands/blpop/ + blpop: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; + + /// Removes the last element of the first non-empty list, waiting up to the timeout for one, + /// returning its key and the element. + /// + /// Returns none when the timeout passed. A timeout of zero waits indefinitely. + /// + /// The connection waits too, replies to commands sent after it on the connection wait behind + /// it. Use a connection of your own. + /// + /// BRPOP key [ key ... ] timeout + /// https://valkey.io/commands/brpop/ + brpop: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; + + /// Moves the last element of the source list to the head of the destination list, waiting up + /// to the timeout for the source to have one, returning it. + /// + /// Returns none when the timeout passed. A timeout of zero waits indefinitely. + /// + /// The connection waits too, replies to commands sent after it on the connection wait behind + /// it. Use a connection of your own. + /// + /// See also `blmove`. + /// + /// BRPOPLPUSH source destination timeout + /// https://valkey.io/commands/brpoplpush/ + brpoplpush: async func(connection: borrow, source: string, destination: string, timeout: duration-milliseconds) -> result, error>; + + /// The element at the index of the list stored at key, negative from the end, none when the + /// index is out of range. + /// + /// LINDEX key index + /// https://valkey.io/commands/lindex/ + lindex: async func(connection: borrow, key: string, index: integer) -> result, error>; + + /// Inserts the element before or after the first occurrence of the pivot in the list stored at + /// key. + /// + /// LINSERT key 〈 BEFORE | AFTER 〉 pivot element + /// https://valkey.io/commands/linsert/ + linsert: async func(connection: borrow, key: string, position: linsert-position, pivot: string, element: string) -> result; + + /// The length of the list stored at key, 0 when the key does not exist. + /// + /// LLEN key + /// https://valkey.io/commands/llen/ + llen: async func(connection: borrow, key: string) -> result; + + /// Moves an element from one end of the source list to an end of the destination list, + /// returning it, none when the source does not exist. + /// + /// LMOVE source destination 〈 LEFT | RIGHT 〉 〈 LEFT | RIGHT 〉 + /// https://valkey.io/commands/lmove/ + lmove: async func(connection: borrow, source: string, destination: string, wherefrom: list-side, whereto: list-side) -> result, error>; + + /// Removes elements from an end of the first non-empty list, returning its key and the + /// elements, one without a count, none when every list is empty. + /// + /// LMPOP numkeys key [ key ... ] 〈 LEFT | RIGHT 〉 [ COUNT count ] + /// https://valkey.io/commands/lmpop/ + lmpop: async func(connection: borrow, keys: list, end: list-side, count: option) -> result>>, error>; + + /// Removes and returns the first elements of the list stored at key, up to the count, one + /// without a count, none when the key does not exist. + /// + /// LPOP key [ count ] + /// https://valkey.io/commands/lpop/ + lpop: async func(connection: borrow, key: string, count: option) -> result, error>; + + /// The positions of the element in the list stored at key, up to the count, 0 for every match, + /// the first match without a count. + /// + /// The rank skips matches, the second match for 2, counting from the end when negative. The + /// max length limits how many elements are compared. + /// + /// LPOS key element [ RANK rank ] [ COUNT num-matches ] [ MAXLEN len ] + /// https://valkey.io/commands/lpos/ + lpos: async func(connection: borrow, key: string, element: string, rank: option, count: option, maxlen: option) -> result, error>; + + /// Inserts the elements at the head of the list stored at key, creating it when it does not + /// exist, returning its length after. + /// + /// LPUSH key element [ element ... ] + /// https://valkey.io/commands/lpush/ + lpush: async func(connection: borrow, key: string, elements: list) -> result; + + /// Inserts the elements at the head of the list stored at key, only when it exists, returning + /// its length after, 0 when it does not exist. + /// + /// LPUSHX key element [ element ... ] + /// https://valkey.io/commands/lpushx/ + lpushx: async func(connection: borrow, key: string, elements: list) -> result; + + /// The elements of the list stored at key from the start to the stop, both included, negative + /// from the end. + /// + /// LRANGE key start stop + /// https://valkey.io/commands/lrange/ + lrange: async func(connection: borrow, key: string, start: integer, stop: integer) -> result, error>; + + /// Removes occurrences of the element from the list stored at key, returning how many. + /// + /// A positive count removes up to that many from the head, a negative count from the tail, and + /// 0 removes them all. + /// + /// LREM key count element + /// https://valkey.io/commands/lrem/ + lrem: async func(connection: borrow, key: string, count: integer, element: string) -> result; + + /// Sets the element at the index of the list stored at key, negative from the end. + /// + /// LSET key index element + /// https://valkey.io/commands/lset/ + lset: async func(connection: borrow, key: string, index: integer, element: string) -> result<_, error>; + + /// Trims the list stored at key to the elements from the start to the stop, both included, + /// negative from the end. + /// + /// LTRIM key start stop + /// https://valkey.io/commands/ltrim/ + ltrim: async func(connection: borrow, key: string, start: integer, stop: integer) -> result<_, error>; + + /// Removes and returns the last elements of the list stored at key, up to the count, one + /// without a count, none when the key does not exist. + /// + /// RPOP key [ count ] + /// https://valkey.io/commands/rpop/ + rpop: async func(connection: borrow, key: string, count: option) -> result, error>; + + /// Moves the last element of the source list to the head of the destination list, returning + /// it, none when the source does not exist. + /// + /// See also `lmove`. + /// + /// RPOPLPUSH source destination + /// https://valkey.io/commands/rpoplpush/ + rpoplpush: async func(connection: borrow, source: string, destination: string) -> result, error>; + + /// Inserts the elements at the tail of the list stored at key, creating it when it does not + /// exist, returning its length after. + /// + /// RPUSH key element [ element ... ] + /// https://valkey.io/commands/rpush/ + rpush: async func(connection: borrow, key: string, elements: list) -> result; + + /// Inserts the elements at the tail of the list stored at key, only when it exists, returning + /// its length after, 0 when it does not exist. + /// + /// RPUSHX key element [ element ... ] + /// https://valkey.io/commands/rpushx/ + rpushx: async func(connection: borrow, key: string, elements: list) -> result; // pubsub commands @@ -1306,8 +1492,8 @@ interface commands { /// The limit is a range of the members in the range, only with BYSCORE or BYLEX. With `xx`, /// returns none when the key does not exist. /// - /// Also covers the deprecated ZRANGEBYLEX, ZRANGEBYSCORE, ZREVRANGE, ZREVRANGEBYLEX and - /// ZREVRANGEBYSCORE. See also `zrange-withscores`. + /// Also covers ZRANGEBYLEX, ZRANGEBYSCORE, ZREVRANGE, ZREVRANGEBYLEX and ZREVRANGEBYSCORE. See + /// also `zrange-withscores`. /// /// ZRANGE key start stop [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ] [ XX ] /// https://valkey.io/commands/zrange/ From 130b586a2b51dc43f80a0d69fcc2d9c219ffc16b Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 13 Oct 2026 09:51:11 -0400 Subject: [PATCH 12/37] bitmap commands Signed-off-by: Scott Andrews --- components/commands/src/command.rs | 98 ++++++++++++++++++-- components/commands/src/lib.rs | 144 +++++++++++++++++++++++++++-- wit/commands.wit | 138 +++++++++++++++++++++++++-- 3 files changed, 356 insertions(+), 24 deletions(-) diff --git a/components/commands/src/command.rs b/components/commands/src/command.rs index 262d4df..41a7251 100644 --- a/components/commands/src/command.rs +++ b/components/commands/src/command.rs @@ -1,10 +1,11 @@ use crate::exports::componentized::valkey::commands::{ - Aggregate, ChannelRule, CommandRule, Connection, Cursor, DbRule, DurationMilliseconds, - DurationSeconds, Error, GetexTtl, HsetexCondition, HsetexFieldCondition, KeyRule, LexBound, - Limit, LinsertPosition, ListSide, MsetexPrecondition, PopEnd, Rule, RuleSelector, ScoreBound, - SetPrecondition, SetTtl, SortOpts, SortOrder, StreamTrim, StreamTrimEfficiency, TtlMode, - UnixTimeMilliseconds, UnixTimeSeconds, UserRule, Value, XclaimOpts, ZaddComparison, - ZaddCondition, ZrangeBy, + Aggregate, BitRange, BitfieldEncoding, BitfieldGet, BitfieldIncrby, BitfieldOffset, + BitfieldOperation, BitfieldOverflow, BitfieldSet, ChannelRule, CommandRule, Connection, Cursor, + DbRule, DurationMilliseconds, DurationSeconds, Error, GetexTtl, HsetexCondition, + HsetexFieldCondition, KeyRule, LexBound, Limit, LinsertPosition, ListSide, MsetexPrecondition, + PopEnd, Rule, RuleSelector, ScoreBound, SetPrecondition, SetTtl, SortOpts, SortOrder, + StreamTrim, StreamTrimEfficiency, TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, UserRule, + Value, XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, }; use crate::value::{FromValue, OrZeroId, ZERO_ID}; @@ -647,3 +648,88 @@ impl IntoCommand for LinsertPosition { .into_command() } } + +/// `start [ end [ BIT ] ]`, BYTE is the default, and left out for servers before it existed. +impl IntoCommand for BitRange { + fn into_command(self) -> Result, Error> { + match self { + BitRange::From(start) => start.into_command(), + BitRange::Bytes((start, end)) => (start, end).into_command(), + BitRange::Bits((start, end)) => (start, end, "BIT").into_command(), + } + } +} + +/// `i1` to `i64`, `u1` to `u63`, other widths fail. +impl IntoCommand for BitfieldEncoding { + fn into_command(self) -> Result, Error> { + match self { + BitfieldEncoding::Signed(width @ 1..=64) => format!("i{width}").into_command(), + BitfieldEncoding::Unsigned(width @ 1..=63) => format!("u{width}").into_command(), + BitfieldEncoding::Signed(width) => Err(Error::Client(format!( + "a signed bitfield is 1 to 64 bits wide, not {width}" + ))), + BitfieldEncoding::Unsigned(width) => Err(Error::Client(format!( + "an unsigned bitfield is 1 to 63 bits wide, not {width}" + ))), + } + } +} + +impl IntoCommand for BitfieldOffset { + fn into_command(self) -> Result, Error> { + match self { + BitfieldOffset::Bit(offset) => offset.into_command(), + BitfieldOffset::Field(field) => format!("#{field}").into_command(), + } + } +} + +impl IntoCommand for BitfieldOverflow { + fn into_command(self) -> Result, Error> { + let overflow = match self { + BitfieldOverflow::Wrap => "WRAP", + BitfieldOverflow::Sat => "SAT", + BitfieldOverflow::Fail => "FAIL", + }; + ("OVERFLOW", overflow).into_command() + } +} + +impl IntoCommand for BitfieldGet { + fn into_command(self) -> Result, Error> { + ("GET", self.encoding, self.offset).into_command() + } +} + +impl IntoCommand for BitfieldSet { + fn into_command(self) -> Result, Error> { + ("SET", self.encoding, self.offset, self.value).into_command() + } +} + +impl IntoCommand for BitfieldIncrby { + fn into_command(self) -> Result, Error> { + ("INCRBY", self.encoding, self.offset, self.increment).into_command() + } +} + +/// A write is preceded by its overflow mode, when given. +impl IntoCommand for BitfieldOperation { + fn into_command(self) -> Result, Error> { + match self { + BitfieldOperation::Get(get) => get.into_command(), + BitfieldOperation::Set((overflow, set)) => (overflow, set).into_command(), + BitfieldOperation::Incrby((overflow, incrby)) => (overflow, incrby).into_command(), + } + } +} + +/// A bit, `1` or `0`, e.g. for SETBIT. +pub struct Bit(pub bool); + +impl IntoCommand for Bit { + fn into_command(self) -> Result, Error> { + if self.0 { "1" } else { "0" }.into_command() + } +} diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index d7bac56..5bfed6f 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -1,16 +1,17 @@ use crate::command::{ - DecimalSeconds, Fields, Flag, LexRange, NamedOption, NonEmpty, NumKeys, ScoreRange, Zrange, - emit, query, + Bit, DecimalSeconds, Fields, Flag, LexRange, NamedOption, NonEmpty, NumKeys, ScoreRange, + Zrange, emit, query, }; use crate::exports::componentized::valkey::commands::{ - Aggregate, Connection, Cursor, Double, DurationMilliseconds, DurationSeconds, Error, - FieldExpiretime, FieldPersistResult, FieldPexpiretime, FieldPttl, FieldTtl, GetexTtl, Guest, - HsetexCondition, HsetexFieldCondition, Integer, LcsIdxReply, LexBound, Limit, LinsertPosition, - LinsertResult, ListSide, MsetexPrecondition, MsetexTtl, PopEnd, Rule, ScoreBound, - SetPrecondition, SetTtl, SortOpts, StreamId, StreamTrim, TtlMode, TtlResult, - UnixTimeMilliseconds, UnixTimeSeconds, Value, XclaimOpts, XinfoConsumer, XinfoGroup, - XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, ZaddComparison, - ZaddCondition, ZrangeBy, ZrangeWithscoresBy, + Aggregate, BitRange, BitfieldGet, BitfieldOperation, BitopOperation, Connection, Cursor, + Double, DurationMilliseconds, DurationSeconds, Error, FieldExpiretime, FieldPersistResult, + FieldPexpiretime, FieldPttl, FieldTtl, GetexTtl, Guest, HsetexCondition, HsetexFieldCondition, + Integer, LcsIdxReply, LexBound, Limit, LinsertPosition, LinsertResult, ListSide, + MsetexPrecondition, MsetexTtl, PopEnd, Rule, ScoreBound, SetPrecondition, SetTtl, SortOpts, + StreamId, StreamTrim, TtlMode, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, + XclaimOpts, XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, + XpendingExtendedEntry, XpendingSummary, ZaddComparison, ZaddCondition, ZrangeBy, + ZrangeWithscoresBy, }; use crate::value::{FlatPairs, NextCursor, OrNever, OrZeroId, ScoredMembers}; @@ -77,6 +78,129 @@ impl Guest for Commands { .await } + #[doc = " The number of bits set to 1 in the string stored at key, in the range, 0 when the key does"] + #[doc = " not exist."] + #[doc = ""] + #[doc = " BITCOUNT key [ start [ end [ BYTE | BIT ] ] ]"] + #[doc = " https://valkey.io/commands/bitcount/"] + #[allow(async_fn_in_trait)] + async fn bitcount( + connection: &Connection, + key: String, + range: Option, + ) -> Result { + query(connection, ("BITCOUNT", key, range)).await + } + + #[doc = " Reads and writes integer fields of the string stored at key, returning the result of each"] + #[doc = " operation in order, none for a write that failed with `OVERFLOW FAIL`."] + #[doc = ""] + #[doc = " See also `bitfield-ro`."] + #[doc = ""] + #[doc = " BITFIELD key [ GET encoding offset | [ OVERFLOW WRAP | SAT | FAIL ] 〈 SET encoding offset value | INCRBY encoding offset increment 〉 [ GET encoding offset | [ OVERFLOW WRAP | SAT | FAIL ] 〈 SET encoding offset value | INCRBY encoding offset increment 〉 ... ] ]"] + #[doc = " https://valkey.io/commands/bitfield/"] + #[allow(async_fn_in_trait)] + async fn bitfield( + connection: &Connection, + key: String, + operations: Option>, + ) -> Result>, Error> { + query( + connection, + ( + "BITFIELD", + key, + NonEmpty::optional("operations", operations), + ), + ) + .await + } + + #[doc = " Reads integer fields of the string stored at key, the read-only variant of `bitfield`."] + #[doc = ""] + #[doc = " BITFIELD_RO key [ GET encoding offset [ GET encoding offset ... ] ]"] + #[doc = " https://valkey.io/commands/bitfield_ro/"] + #[allow(async_fn_in_trait)] + async fn bitfield_ro( + connection: &Connection, + key: String, + gets: Option>, + ) -> Result, Error> { + query( + connection, + ("BITFIELD_RO", key, NonEmpty::optional("gets", gets)), + ) + .await + } + + #[doc = " Stores the bitwise operation of the strings stored at the source keys in the destination,"] + #[doc = " returning its length, the length of the longest source."] + #[doc = ""] + #[doc = " BITOP 〈 AND | OR | XOR | NOT 〉 destkey key [ key ... ]"] + #[doc = " https://valkey.io/commands/bitop/"] + #[allow(async_fn_in_trait)] + async fn bitop( + connection: &Connection, + destination: String, + operation: BitopOperation, + ) -> Result { + // the operation and its keys are either side of the destination + let (operation, keys) = match operation { + BitopOperation::And(keys) => ("AND", keys), + BitopOperation::Or(keys) => ("OR", keys), + BitopOperation::Xor(keys) => ("XOR", keys), + BitopOperation::Not(key) => ("NOT", vec![key]), + }; + query( + connection, + ("BITOP", operation, destination, NonEmpty::new("keys", keys)), + ) + .await + } + + #[doc = " The position of the first bit set to the bit in the string stored at key, in the range,"] + #[doc = " none when there is none."] + #[doc = ""] + #[doc = " BITPOS key bit [ start [ end [ BYTE | BIT ] ] ]"] + #[doc = " https://valkey.io/commands/bitpos/"] + #[allow(async_fn_in_trait)] + async fn bitpos( + connection: &Connection, + key: String, + bit: bool, + range: Option, + ) -> Result, Error> { + // -1 when there is no such bit + let position: OrNever = + query(connection, ("BITPOS", key, Bit(bit), range)).await?; + Ok(position.0) + } + + #[doc = " The bit at the offset of the string stored at key, false beyond its end, or when the key"] + #[doc = " does not exist."] + #[doc = ""] + #[doc = " GETBIT key offset"] + #[doc = " https://valkey.io/commands/getbit/"] + #[allow(async_fn_in_trait)] + async fn getbit(connection: &Connection, key: String, offset: u64) -> Result { + query(connection, ("GETBIT", key, offset)).await + } + + #[doc = " Sets the bit at the offset of the string stored at key, growing it as needed, returning the"] + #[doc = " previous bit."] + #[doc = ""] + #[doc = " SETBIT key offset value"] + #[doc = " https://valkey.io/commands/setbit/"] + #[allow(async_fn_in_trait)] + async fn setbit( + connection: &Connection, + key: String, + offset: u64, + value: bool, + ) -> Result { + query(connection, ("SETBIT", key, offset, Bit(value))).await + } + #[doc = " Authenticates the current connection."] #[doc = ""] #[doc = " AUTH [ username ] password"] diff --git a/wit/commands.wit b/wit/commands.wit index 0671169..09917e6 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -282,14 +282,136 @@ interface commands { // bitmap commands - // not yet implemented, from the @bitmap category: - // BITCOUNT - // BITFIELD - // BITFIELD_RO - // BITOP - // BITPOS - // GETBIT - // SETBIT + /// A range of a string, negative from the end, for BITCOUNT and BITPOS. + variant bit-range { + /// `start`: the bytes from the start to the end of the string. + %from(integer), + /// `start end`: the bytes from the start to the end, both included. + bytes(tuple), + /// `start end BIT`: the bits from the start to the end, both included. + bits(tuple), + } + + /// The number of bits set to 1 in the string stored at key, in the range, 0 when the key does + /// not exist. + /// + /// BITCOUNT key [ start [ end [ BYTE | BIT ] ] ] + /// https://valkey.io/commands/bitcount/ + bitcount: async func(connection: borrow, key: string, range: option) -> result; + + /// The type of an integer field of a string, for BITFIELD. + variant bitfield-encoding { + /// `i1` to `i64`: a signed integer of the width in bits. + signed(u8), + /// `u1` to `u63`: an unsigned integer of the width in bits. + unsigned(u8), + } + + /// Where an integer field of a string starts, for BITFIELD. + variant bitfield-offset { + /// The offset in bits. + bit(u64), + /// `#field`: the field of the encoding's width, the offset in bits is the field times the + /// width. + field(u64), + } + + /// What a write does when the field overflows, `OVERFLOW`. + enum bitfield-overflow { + /// `WRAP`: wraps around, the default. + wrap, + /// `SAT`: saturates at the minimum or maximum. + sat, + /// `FAIL`: does not write, the result is none. + fail, + } + + /// `GET encoding offset`: the value of the field. + record bitfield-get { + encoding: bitfield-encoding, + offset: bitfield-offset, + } + + /// `SET encoding offset value`: sets the field, the result is its previous value. + record bitfield-set { + encoding: bitfield-encoding, + offset: bitfield-offset, + value: s64, + } + + /// `INCRBY encoding offset increment`: increments the field, the result is its new value. + record bitfield-incrby { + encoding: bitfield-encoding, + offset: bitfield-offset, + increment: s64, + } + + /// An operation of BITFIELD. + /// + /// A write is preceded by an optional overflow mode, `OVERFLOW WRAP | SAT | FAIL`, for it and + /// the following writes. None keeps the current mode, `WRAP` at the start of the command. + variant bitfield-operation { + /// `GET encoding offset` + get(bitfield-get), + /// `[ OVERFLOW WRAP | SAT | FAIL ] SET encoding offset value` + set(tuple, bitfield-set>), + /// `[ OVERFLOW WRAP | SAT | FAIL ] INCRBY encoding offset increment` + incrby(tuple, bitfield-incrby>), + } + + /// Reads and writes integer fields of the string stored at key, returning the result of each + /// operation in order, none for a write that failed with `OVERFLOW FAIL`. + /// + /// BITFIELD key [ GET encoding offset | [ OVERFLOW WRAP | SAT | FAIL ] 〈 SET encoding offset value | INCRBY encoding offset increment 〉 [ GET encoding offset | [ OVERFLOW WRAP | SAT | FAIL ] 〈 SET encoding offset value | INCRBY encoding offset increment 〉 ... ] ] + /// https://valkey.io/commands/bitfield/ + bitfield: async func(connection: borrow, key: string, operations: option>) -> result>, error>; + + /// Reads integer fields of the string stored at key, the read-only variant of `bitfield`. + /// + /// BITFIELD_RO key [ GET encoding offset [ GET encoding offset ... ] ] + /// https://valkey.io/commands/bitfield_ro/ + bitfield-ro: async func(connection: borrow, key: string, gets: option>) -> result, error>; + + /// A bitwise operation of BITOP, and its source keys. + variant bitop-operation { + /// `AND` + and(list), + /// `OR` + or(list), + /// `XOR` + xor(list), + /// `NOT`: of a single key. + not(string), + } + + /// Stores the bitwise operation of the strings stored at the source keys in the destination, + /// returning its length, the length of the longest source. + /// + /// BITOP 〈 AND | OR | XOR | NOT 〉 destkey key [ key ... ] + /// https://valkey.io/commands/bitop/ + bitop: async func(connection: borrow, destination: string, operation: bitop-operation) -> result; + + /// The position of the first bit set to the bit in the string stored at key, in the range, + /// none when there is none. + /// + /// BITPOS key bit [ start [ end [ BYTE | BIT ] ] ] + /// https://valkey.io/commands/bitpos/ + bitpos: async func(connection: borrow, key: string, bit: bool, range: option) -> result, error>; + + /// The bit at the offset of the string stored at key, false beyond its end, or when the key + /// does not exist. + /// + /// GETBIT key offset + /// https://valkey.io/commands/getbit/ + getbit: async func(connection: borrow, key: string, offset: u64) -> result; + + /// Sets the bit at the offset of the string stored at key, growing it as needed, returning the + /// previous bit. + /// + /// SETBIT key offset value + /// https://valkey.io/commands/setbit/ + setbit: async func(connection: borrow, key: string, offset: u64, value: bool) -> result; + // connection commands From 164e6d05642135f20736aeb92bf1e0df262a5f1f Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 13 Oct 2026 10:38:42 -0400 Subject: [PATCH 13/37] geo commands Signed-off-by: Scott Andrews --- components/commands/src/command.rs | 78 ++++++++++++- components/commands/src/lib.rs | 170 +++++++++++++++++++++++++++-- components/commands/src/value.rs | 47 +++++++- wit/commands.wit | 141 ++++++++++++++++++++++-- 4 files changed, 410 insertions(+), 26 deletions(-) diff --git a/components/commands/src/command.rs b/components/commands/src/command.rs index 41a7251..68005fb 100644 --- a/components/commands/src/command.rs +++ b/components/commands/src/command.rs @@ -1,11 +1,12 @@ use crate::exports::componentized::valkey::commands::{ Aggregate, BitRange, BitfieldEncoding, BitfieldGet, BitfieldIncrby, BitfieldOffset, BitfieldOperation, BitfieldOverflow, BitfieldSet, ChannelRule, CommandRule, Connection, Cursor, - DbRule, DurationMilliseconds, DurationSeconds, Error, GetexTtl, HsetexCondition, - HsetexFieldCondition, KeyRule, LexBound, Limit, LinsertPosition, ListSide, MsetexPrecondition, - PopEnd, Rule, RuleSelector, ScoreBound, SetPrecondition, SetTtl, SortOpts, SortOrder, - StreamTrim, StreamTrimEfficiency, TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, UserRule, - Value, XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, + DbRule, DurationMilliseconds, DurationSeconds, Error, GeoCoordinates, GeoCount, GeoFrom, + GeoShape, GeoUnit, GeoaddCondition, GetexTtl, HsetexCondition, HsetexFieldCondition, KeyRule, + LexBound, Limit, LinsertPosition, ListSide, MsetexPrecondition, PopEnd, Rule, RuleSelector, + ScoreBound, SetPrecondition, SetTtl, SortOpts, SortOrder, StreamTrim, StreamTrimEfficiency, + TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, UserRule, Value, XclaimOpts, ZaddComparison, + ZaddCondition, ZrangeBy, }; use crate::value::{FromValue, OrZeroId, ZERO_ID}; @@ -733,3 +734,70 @@ impl IntoCommand for Bit { if self.0 { "1" } else { "0" }.into_command() } } + +/// `longitude latitude` +impl IntoCommand for GeoCoordinates { + fn into_command(self) -> Result, Error> { + (self.longitude, self.latitude).into_command() + } +} + +impl IntoCommand for GeoUnit { + fn into_command(self) -> Result, Error> { + match self { + GeoUnit::M => "m", + GeoUnit::Km => "km", + GeoUnit::Ft => "ft", + GeoUnit::Mi => "mi", + } + .into_command() + } +} + +impl IntoCommand for GeoaddCondition { + fn into_command(self) -> Result, Error> { + match self { + GeoaddCondition::Nx => "NX", + GeoaddCondition::Xx => "XX", + } + .into_command() + } +} + +impl IntoCommand for GeoFrom { + fn into_command(self) -> Result, Error> { + match self { + GeoFrom::Member(member) => ("FROMMEMBER", member).into_command(), + GeoFrom::Lonlat(coordinates) => ("FROMLONLAT", coordinates).into_command(), + } + } +} + +/// The center, when the shape has one, then the shape. +impl IntoCommand for GeoShape { + fn into_command(self) -> Result, Error> { + match self { + GeoShape::Radius(circle) => { + (circle.from, "BYRADIUS", circle.radius, circle.unit).into_command() + } + GeoShape::Box(rectangle) => ( + rectangle.from, + "BYBOX", + rectangle.width, + rectangle.height, + rectangle.unit, + ) + .into_command(), + GeoShape::Polygon(vertices) => { + ("BYPOLYGON", NumKeys(NonEmpty::new("vertices", vertices))).into_command() + } + } + } +} + +/// `COUNT count [ ANY ]` +impl IntoCommand for GeoCount { + fn into_command(self) -> Result, Error> { + ("COUNT", self.count, Flag("ANY", self.any)).into_command() + } +} diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index 5bfed6f..606cd2b 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -5,13 +5,13 @@ use crate::command::{ use crate::exports::componentized::valkey::commands::{ Aggregate, BitRange, BitfieldGet, BitfieldOperation, BitopOperation, Connection, Cursor, Double, DurationMilliseconds, DurationSeconds, Error, FieldExpiretime, FieldPersistResult, - FieldPexpiretime, FieldPttl, FieldTtl, GetexTtl, Guest, HsetexCondition, HsetexFieldCondition, - Integer, LcsIdxReply, LexBound, Limit, LinsertPosition, LinsertResult, ListSide, - MsetexPrecondition, MsetexTtl, PopEnd, Rule, ScoreBound, SetPrecondition, SetTtl, SortOpts, - StreamId, StreamTrim, TtlMode, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, - XclaimOpts, XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, - XpendingExtendedEntry, XpendingSummary, ZaddComparison, ZaddCondition, ZrangeBy, - ZrangeWithscoresBy, + FieldPexpiretime, FieldPttl, FieldTtl, GeoCoordinates, GeoCount, GeoSearchResult, GeoShape, + GeoUnit, GeoaddCondition, GetexTtl, Guest, HsetexCondition, HsetexFieldCondition, Integer, + LcsIdxReply, LexBound, Limit, LinsertPosition, LinsertResult, ListSide, MsetexPrecondition, + MsetexTtl, PopEnd, Rule, ScoreBound, SetPrecondition, SetTtl, SortOpts, SortOrder, StreamId, + StreamTrim, TtlMode, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, XclaimOpts, + XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, + XpendingSummary, ZaddComparison, ZaddCondition, ZrangeBy, ZrangeWithscoresBy, }; use crate::value::{FlatPairs, NextCursor, OrNever, OrZeroId, ScoredMembers}; @@ -252,6 +252,162 @@ impl Guest for Commands { } } + #[doc = " Adds the members at their positions to the geospatial index stored at key, or updates their"] + #[doc = " positions, returning how many were added, or with `changed`, added or updated."] + #[doc = ""] + #[doc = " GEOADD key [ NX | XX ] [ CH ] longitude latitude member [ longitude latitude member ... ]"] + #[doc = " https://valkey.io/commands/geoadd/"] + #[allow(async_fn_in_trait)] + async fn geoadd( + connection: &Connection, + key: String, + condition: Option, + changed: bool, + coordinates_and_members: Vec<(GeoCoordinates, String)>, + ) -> Result { + query( + connection, + ( + "GEOADD", + key, + condition, + Flag("CH", changed), + NonEmpty::new("coordinates-and-members", coordinates_and_members), + ), + ) + .await + } + + #[doc = " The distance between the members, in meters or the unit, none when either is not a member."] + #[doc = ""] + #[doc = " GEODIST key member1 member2 [ m | km | ft | mi ]"] + #[doc = " https://valkey.io/commands/geodist/"] + #[allow(async_fn_in_trait)] + async fn geodist( + connection: &Connection, + key: String, + member1: String, + member2: String, + unit: Option, + ) -> Result, Error> { + query(connection, ("GEODIST", key, member1, member2, unit)).await + } + + #[doc = " The geohash of each of the members, none for a member that is not in the index."] + #[doc = ""] + #[doc = " GEOHASH key [ member [ member ... ] ]"] + #[doc = " https://valkey.io/commands/geohash/"] + #[allow(async_fn_in_trait)] + async fn geohash( + connection: &Connection, + key: String, + members: Option>, + ) -> Result>, Error> { + query( + connection, + ("GEOHASH", key, NonEmpty::optional("members", members)), + ) + .await + } + + #[doc = " The position of each of the members, none for a member that is not in the index."] + #[doc = ""] + #[doc = " GEOPOS key [ member [ member ... ] ]"] + #[doc = " https://valkey.io/commands/geopos/"] + #[allow(async_fn_in_trait)] + async fn geopos( + connection: &Connection, + key: String, + members: Option>, + ) -> Result>, Error> { + query( + connection, + ("GEOPOS", key, NonEmpty::optional("members", members)), + ) + .await + } + + #[doc = " The members of the geospatial index stored at key in the shape."] + #[doc = ""] + #[doc = " See also `geosearch-with`."] + #[doc = ""] + #[doc = " GEOSEARCH key [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ]"] + #[doc = " https://valkey.io/commands/geosearch/"] + #[allow(async_fn_in_trait)] + async fn geosearch( + connection: &Connection, + key: String, + shape: GeoShape, + order: Option, + count: Option, + ) -> Result, Error> { + query(connection, ("GEOSEARCH", key, shape, order, count)).await + } + + #[doc = " The members of the geospatial index stored at key in the shape, with the details asked for."] + #[doc = ""] + #[doc = " See also `geosearch`."] + #[doc = ""] + #[doc = " GEOSEARCH key [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] [ WITHCOORD ] [ WITHDIST ] [ WITHHASH ]"] + #[doc = " https://valkey.io/commands/geosearch/"] + #[allow(async_fn_in_trait)] + async fn geosearch_with( + connection: &Connection, + key: String, + shape: GeoShape, + order: Option, + count: Option, + withcoord: bool, + withdist: bool, + withhash: bool, + ) -> Result, Error> { + query( + connection, + ( + "GEOSEARCH", + key, + shape, + order, + count, + Flag("WITHCOORD", withcoord), + Flag("WITHDIST", withdist), + Flag("WITHHASH", withhash), + ), + ) + .await + } + + #[doc = " Stores the members of the geospatial index stored at source in the shape, returning how"] + #[doc = " many, with their positions, or with `storedist`, their distance from the center as the"] + #[doc = " score."] + #[doc = ""] + #[doc = " GEOSEARCHSTORE destination source [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] [ STOREDIST ]"] + #[doc = " https://valkey.io/commands/geosearchstore/"] + #[allow(async_fn_in_trait)] + async fn geosearchstore( + connection: &Connection, + destination: String, + source: String, + shape: GeoShape, + order: Option, + count: Option, + storedist: bool, + ) -> Result { + query( + connection, + ( + "GEOSEARCHSTORE", + destination, + source, + shape, + order, + count, + Flag("STOREDIST", storedist), + ), + ) + .await + } + #[doc = " Removes the specified fields from the hash stored at key."] #[doc = ""] #[doc = " HDEL key field [ field ... ]"] diff --git a/components/commands/src/value.rs b/components/commands/src/value.rs index d993656..b3ee2b5 100644 --- a/components/commands/src/value.rs +++ b/components/commands/src/value.rs @@ -1,8 +1,8 @@ use crate::exports::componentized::valkey::commands::{ Cursor, Double, DurationMilliseconds, DurationSeconds, Error, FieldExpiretime, - FieldPersistResult, FieldPexpiretime, FieldPttl, FieldTtl, Integer, LcsIdxMatch, LcsIdxReply, - LinsertResult, StreamId, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, - XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, + FieldPersistResult, FieldPexpiretime, FieldPttl, FieldTtl, GeoCoordinates, GeoSearchResult, + Integer, LcsIdxMatch, LcsIdxReply, LinsertResult, StreamId, TtlResult, UnixTimeMilliseconds, + UnixTimeSeconds, Value, XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, XpendingExtendedEntry, XpendingSummary, }; @@ -351,6 +351,47 @@ impl FromValue for LinsertResult { } } +/// `[longitude, latitude]` +impl FromValue for GeoCoordinates { + fn from_value(value: Value) -> Result { + let (longitude, latitude) = FromValue::from_value(value)?; + Ok(GeoCoordinates { + longitude, + latitude, + }) + } +} + +/// The member, or with any of WITHDIST, WITHHASH and WITHCOORD, an array of the member then the +/// details, in that order, told apart by their types. +impl FromValue for GeoSearchResult { + fn from_value(value: Value) -> Result { + let mut result = GeoSearchResult { + member: String::new(), + distance: None, + hash: None, + coordinates: None, + }; + let items = match value { + Value::Array(_) => items(value)?, + // without details, the member alone + value => vec![value], + }; + let mut items = items.into_iter(); + result.member = String::from_value(items.next().unwrap_or(Value::Null))?; + for item in items { + match item { + // a bulk string, as RESP3 has no other way to send a fixed precision double + Value::BulkString(_) => result.distance = Some(Double::from_value(item)?), + Value::Integer(hash) => result.hash = Some(hash), + Value::Array(_) => result.coordinates = Some(GeoCoordinates::from_value(item)?), + item => return Err(unexpected(&item)), + } + } + Ok(result) + } +} + impl FromValue for TtlResult { fn from_value(value: Value) -> Result { match Integer::from_value(value)? { diff --git a/wit/commands.wit b/wit/commands.wit index 09917e6..0f4db08 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -475,17 +475,136 @@ interface commands { // geo commands - // not yet implemented, from the @geo category: - // GEOADD - // GEODIST - // GEOHASH - // GEOPOS - // GEORADIUS - // GEORADIUS_RO - // GEORADIUSBYMEMBER - // GEORADIUSBYMEMBER_RO - // GEOSEARCH - // GEOSEARCHSTORE + /// A position on the Earth. + /// + /// The exact limits, as specified by EPSG:900913 / EPSG:3785 / OSGEO:41001 + record geo-coordinates { + /// The longitude, from -180 to 180 degrees. + longitude: double, + /// The latitude, from -85.05112878 to 85.05112878 degrees. + latitude: double, + } + + /// A unit of distance. + enum geo-unit { + /// `m`: meters. + m, + /// `km`: kilometers. + km, + /// `ft`: feet. + ft, + /// `mi`: miles. + mi, + } + + /// Only add or only update members, GEOADD's `NX` and `XX`. + enum geoadd-condition { + /// Only add new members, never update existing ones. + nx, + /// Only update existing members, never add new ones. + xx, + } + + /// Adds the members at their positions to the geospatial index stored at key, or updates their + /// positions, returning how many were added, or with `changed`, added or updated. + /// + /// GEOADD key [ NX | XX ] [ CH ] longitude latitude member [ longitude latitude member ... ] + /// https://valkey.io/commands/geoadd/ + geoadd: async func(connection: borrow, key: string, condition: option, changed: bool, coordinates-and-members: list>) -> result; + + /// The distance between the members, in meters or the unit, none when either is not a member. + /// + /// GEODIST key member1 member2 [ m | km | ft | mi ] + /// https://valkey.io/commands/geodist/ + geodist: async func(connection: borrow, key: string, member1: string, member2: string, unit: option) -> result, error>; + + /// The geohash of each of the members, none for a member that is not in the index. + /// + /// GEOHASH key [ member [ member ... ] ] + /// https://valkey.io/commands/geohash/ + geohash: async func(connection: borrow, key: string, members: option>) -> result>, error>; + + /// The position of each of the members, none for a member that is not in the index. + /// + /// GEOPOS key [ member [ member ... ] ] + /// https://valkey.io/commands/geopos/ + geopos: async func(connection: borrow, key: string, members: option>) -> result>, error>; + + /// The center of a search, `FROMMEMBER` or `FROMLONLAT`. + variant geo-from { + /// `FROMMEMBER member`: the position of a member of the index. + member(string), + /// `FROMLONLAT longitude latitude` + lonlat(geo-coordinates), + } + + /// `BYRADIUS`: a circle around the center. + record geo-radius { + %from: geo-from, + radius: double, + unit: geo-unit, + } + + /// `BYBOX`: a rectangle around the center. + record geo-box { + %from: geo-from, + width: double, + height: double, + unit: geo-unit, + } + + /// The area of a search. + variant geo-shape { + /// `[ FROMMEMBER member | FROMLONLAT longitude latitude ] BYRADIUS radius unit` + radius(geo-radius), + /// `[ FROMMEMBER member | FROMLONLAT longitude latitude ] BYBOX width height unit` + box(geo-box), + /// `BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ]`: the polygon with + /// the vertices, in order, which has no center. + polygon(list), + } + + /// At most a count of the members, `COUNT count [ ANY ]`. + record geo-count { + count: integer, + /// `ANY`: returns as soon as enough members are found, rather than the nearest ones. + any: bool, + } + + /// A member a search found, with the details asked for. + record geo-search-result { + member: string, + /// `WITHDIST`: the distance from the center, in the unit of the shape. + distance: option, + /// `WITHHASH`: the geohash, as an integer. + hash: option, + /// `WITHCOORD`: the position. + coordinates: option, + } + + /// The members of the geospatial index stored at key in the shape. + /// + /// See also `geosearch-with`. + /// + /// GEOSEARCH key [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] + /// https://valkey.io/commands/geosearch/ + geosearch: async func(connection: borrow, key: string, shape: geo-shape, order: option, count: option) -> result, error>; + + /// The members of the geospatial index stored at key in the shape, with the details asked for. + /// + /// See also `geosearch`. + /// + /// GEOSEARCH key [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] [ WITHCOORD ] [ WITHDIST ] [ WITHHASH ] + /// https://valkey.io/commands/geosearch/ + geosearch-with: async func(connection: borrow, key: string, shape: geo-shape, order: option, count: option, withcoord: bool, withdist: bool, withhash: bool) -> result, error>; + + /// Stores the members of the geospatial index stored at source in the shape, returning how + /// many, with their positions, or with `storedist`, their distance from the center as the + /// score. + /// + /// GEOSEARCHSTORE destination source [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] [ STOREDIST ] + /// https://valkey.io/commands/geosearchstore/ + geosearchstore: async func(connection: borrow, destination: string, source: string, shape: geo-shape, order: option, count: option, storedist: bool) -> result; // hash commands From 523733a5dd9c7deb556aeef5c45ec374ca730b80 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 13 Oct 2026 12:50:36 -0400 Subject: [PATCH 14/37] scripting commands Signed-off-by: Scott Andrews --- components/commands/src/command.rs | 38 ++++- components/commands/src/lib.rs | 240 +++++++++++++++++++++++++++-- components/commands/src/value.rs | 64 +++++++- wit/commands.wit | 192 ++++++++++++++++++++--- 4 files changed, 491 insertions(+), 43 deletions(-) diff --git a/components/commands/src/command.rs b/components/commands/src/command.rs index 68005fb..d2492f5 100644 --- a/components/commands/src/command.rs +++ b/components/commands/src/command.rs @@ -1,12 +1,12 @@ use crate::exports::componentized::valkey::commands::{ Aggregate, BitRange, BitfieldEncoding, BitfieldGet, BitfieldIncrby, BitfieldOffset, BitfieldOperation, BitfieldOverflow, BitfieldSet, ChannelRule, CommandRule, Connection, Cursor, - DbRule, DurationMilliseconds, DurationSeconds, Error, GeoCoordinates, GeoCount, GeoFrom, - GeoShape, GeoUnit, GeoaddCondition, GetexTtl, HsetexCondition, HsetexFieldCondition, KeyRule, - LexBound, Limit, LinsertPosition, ListSide, MsetexPrecondition, PopEnd, Rule, RuleSelector, - ScoreBound, SetPrecondition, SetTtl, SortOpts, SortOrder, StreamTrim, StreamTrimEfficiency, - TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, UserRule, Value, XclaimOpts, ZaddComparison, - ZaddCondition, ZrangeBy, + DbRule, DurationMilliseconds, DurationSeconds, Error, FlushMode, GeoCoordinates, GeoCount, + GeoFrom, GeoShape, GeoUnit, GeoaddCondition, GetexTtl, HsetexCondition, HsetexFieldCondition, + KeyRule, LexBound, Limit, LinsertPosition, ListSide, MsetexPrecondition, PopEnd, Rule, + RuleSelector, ScoreBound, SetPrecondition, SetTtl, SortOpts, SortOrder, StreamTrim, + StreamTrimEfficiency, TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, UserRule, Value, + XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, }; use crate::value::{FromValue, OrZeroId, ZERO_ID}; @@ -801,3 +801,29 @@ impl IntoCommand for GeoCount { ("COUNT", self.count, Flag("ANY", self.any)).into_command() } } + +impl IntoCommand for FlushMode { + fn into_command(self) -> Result, Error> { + match self { + FlushMode::Async => "ASYNC", + FlushMode::Sync => "SYNC", + } + .into_command() + } +} + +/// `numkeys [ key ... ] [ arg ... ]`, e.g. for EVAL, `0` without keys. +pub struct KeysAndArgs(pub Option>, pub Option>); + +impl IntoCommand for KeysAndArgs { + fn into_command(self) -> Result, Error> { + let KeysAndArgs(keys, args) = self; + let numkeys = keys.as_ref().map_or(0, Vec::len); + ( + numkeys, + NonEmpty::optional("keys", keys), + NonEmpty::optional("args", args), + ) + .into_command() + } +} diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index 606cd2b..1024d57 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -1,17 +1,18 @@ use crate::command::{ - Bit, DecimalSeconds, Fields, Flag, LexRange, NamedOption, NonEmpty, NumKeys, ScoreRange, - Zrange, emit, query, + Bit, DecimalSeconds, Fields, Flag, KeysAndArgs, LexRange, NamedOption, NonEmpty, NumKeys, + ScoreRange, Zrange, emit, query, }; use crate::exports::componentized::valkey::commands::{ Aggregate, BitRange, BitfieldGet, BitfieldOperation, BitopOperation, Connection, Cursor, Double, DurationMilliseconds, DurationSeconds, Error, FieldExpiretime, FieldPersistResult, - FieldPexpiretime, FieldPttl, FieldTtl, GeoCoordinates, GeoCount, GeoSearchResult, GeoShape, - GeoUnit, GeoaddCondition, GetexTtl, Guest, HsetexCondition, HsetexFieldCondition, Integer, - LcsIdxReply, LexBound, Limit, LinsertPosition, LinsertResult, ListSide, MsetexPrecondition, - MsetexTtl, PopEnd, Rule, ScoreBound, SetPrecondition, SetTtl, SortOpts, SortOrder, StreamId, - StreamTrim, TtlMode, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, XclaimOpts, - XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, - XpendingSummary, ZaddComparison, ZaddCondition, ZrangeBy, ZrangeWithscoresBy, + FieldPexpiretime, FieldPttl, FieldTtl, FlushMode, FunctionStatsReply, GeoCoordinates, GeoCount, + GeoSearchResult, GeoShape, GeoUnit, GeoaddCondition, GetexTtl, Guest, HsetexCondition, + HsetexFieldCondition, Integer, LcsIdxReply, LexBound, Library, Limit, LinsertPosition, + LinsertResult, ListSide, MsetexPrecondition, MsetexTtl, PopEnd, Rule, ScoreBound, + SetPrecondition, SetTtl, SortOpts, SortOrder, StreamId, StreamTrim, TtlMode, TtlResult, + UnixTimeMilliseconds, UnixTimeSeconds, Value, XclaimOpts, XinfoConsumer, XinfoGroup, + XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, ZaddComparison, + ZaddCondition, ZrangeBy, ZrangeWithscoresBy, }; use crate::value::{FlatPairs, NextCursor, OrNever, OrZeroId, ScoredMembers}; @@ -1598,6 +1599,227 @@ impl Guest for Commands { .await } + #[doc = " Runs the Lua script, returning its result, the keys it accesses and its arguments are"] + #[doc = " `KEYS` and `ARGV` in the script."] + #[doc = ""] + #[doc = " An error the script returns fails the call."] + #[doc = ""] + #[doc = " EVAL script numkeys [ key [ key ... ] ] [ arg [ arg ... ] ]"] + #[doc = " https://valkey.io/commands/eval/"] + #[allow(async_fn_in_trait)] + async fn eval( + connection: &Connection, + script: String, + keys: Option>, + args: Option>, + ) -> Result { + query(connection, ("EVAL", script, KeysAndArgs(keys, args))).await + } + + #[doc = " Runs the Lua script, which may not write, returning its result, see `eval`."] + #[doc = ""] + #[doc = " EVAL_RO script numkeys [ key [ key ... ] ] [ arg [ arg ... ] ]"] + #[doc = " https://valkey.io/commands/eval_ro/"] + #[allow(async_fn_in_trait)] + async fn eval_ro( + connection: &Connection, + script: String, + keys: Option>, + args: Option>, + ) -> Result { + query(connection, ("EVAL_RO", script, KeysAndArgs(keys, args))).await + } + + #[doc = " Runs the Lua script cached with the SHA1 digest, see `script-load`, returning its result, see"] + #[doc = " `eval`."] + #[doc = ""] + #[doc = " EVALSHA sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ]"] + #[doc = " https://valkey.io/commands/evalsha/"] + #[allow(async_fn_in_trait)] + async fn evalsha( + connection: &Connection, + sha1: String, + keys: Option>, + args: Option>, + ) -> Result { + query(connection, ("EVALSHA", sha1, KeysAndArgs(keys, args))).await + } + + #[doc = " Runs the Lua script cached with the SHA1 digest, which may not write, returning its result,"] + #[doc = " see `eval`."] + #[doc = ""] + #[doc = " EVALSHA_RO sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ]"] + #[doc = " https://valkey.io/commands/evalsha_ro/"] + #[allow(async_fn_in_trait)] + async fn evalsha_ro( + connection: &Connection, + sha1: String, + keys: Option>, + args: Option>, + ) -> Result { + query(connection, ("EVALSHA_RO", sha1, KeysAndArgs(keys, args))).await + } + + #[doc = " Calls the function, see `function-load`, returning its result, the keys it accesses and its"] + #[doc = " arguments are passed to it."] + #[doc = ""] + #[doc = " An error the function returns fails the call."] + #[doc = ""] + #[doc = " FCALL function numkeys [ key [ key ... ] ] [ arg [ arg ... ] ]"] + #[doc = " https://valkey.io/commands/fcall/"] + #[allow(async_fn_in_trait)] + async fn fcall( + connection: &Connection, + function: String, + keys: Option>, + args: Option>, + ) -> Result { + query(connection, ("FCALL", function, KeysAndArgs(keys, args))).await + } + + #[doc = " Calls the function, which must be flagged `no-writes`, returning its result, see `fcall`."] + #[doc = ""] + #[doc = " FCALL_RO function numkeys [ key [ key ... ] ] [ arg [ arg ... ] ]"] + #[doc = " https://valkey.io/commands/fcall_ro/"] + #[allow(async_fn_in_trait)] + async fn fcall_ro( + connection: &Connection, + function: String, + keys: Option>, + args: Option>, + ) -> Result { + query(connection, ("FCALL_RO", function, KeysAndArgs(keys, args))).await + } + + #[doc = " Deletes the library and its functions."] + #[doc = ""] + #[doc = " FUNCTION DELETE library-name"] + #[doc = " https://valkey.io/commands/function-delete/"] + #[allow(async_fn_in_trait)] + async fn function_delete(connection: &Connection, library_name: String) -> Result<(), Error> { + query(connection, ("FUNCTION", "DELETE", library_name)).await + } + + #[doc = " Deletes all the libraries, in the mode, the server's `lazyfree-lazy-user-flush` without one."] + #[doc = ""] + #[doc = " FUNCTION FLUSH [ ASYNC | SYNC ]"] + #[doc = " https://valkey.io/commands/function-flush/"] + #[allow(async_fn_in_trait)] + async fn function_flush(connection: &Connection, mode: Option) -> Result<(), Error> { + query(connection, ("FUNCTION", "FLUSH", mode)).await + } + + #[doc = " Kills the function that is running, as long as it has not written."] + #[doc = ""] + #[doc = " FUNCTION KILL"] + #[doc = " https://valkey.io/commands/function-kill/"] + #[allow(async_fn_in_trait)] + async fn function_kill(connection: &Connection) -> Result<(), Error> { + query(connection, ("FUNCTION", "KILL")).await + } + + #[doc = " The libraries, those with names matching the glob-style pattern with one."] + #[doc = ""] + #[doc = " FUNCTION LIST [ LIBRARYNAME library-name-pattern ] [ WITHCODE ]"] + #[doc = " https://valkey.io/commands/function-list/"] + #[allow(async_fn_in_trait)] + async fn function_list( + connection: &Connection, + library_name_pattern: Option, + withcode: bool, + ) -> Result, Error> { + query( + connection, + ( + "FUNCTION", + "LIST", + NamedOption("LIBRARYNAME", library_name_pattern), + Flag("WITHCODE", withcode), + ), + ) + .await + } + + #[doc = " Loads the library from its source code, replacing a library of the same name with"] + #[doc = " `replace`, returning its name."] + #[doc = ""] + #[doc = " FUNCTION LOAD [ REPLACE ] function-code"] + #[doc = " https://valkey.io/commands/function-load/"] + #[allow(async_fn_in_trait)] + async fn function_load( + connection: &Connection, + replace: bool, + function_code: String, + ) -> Result { + query( + connection, + ("FUNCTION", "LOAD", Flag("REPLACE", replace), function_code), + ) + .await + } + + #[doc = " The function that is running, and the libraries and functions of each engine."] + #[doc = ""] + #[doc = " FUNCTION STATS"] + #[doc = " https://valkey.io/commands/function-stats/"] + #[allow(async_fn_in_trait)] + async fn function_stats(connection: &Connection) -> Result { + query(connection, ("FUNCTION", "STATS")).await + } + + #[doc = " Whether each of the scripts is cached, by SHA1 digest."] + #[doc = ""] + #[doc = " SCRIPT EXISTS sha1 [ sha1 ... ]"] + #[doc = " https://valkey.io/commands/script-exists/"] + #[allow(async_fn_in_trait)] + async fn script_exists( + connection: &Connection, + sha1s: Vec, + ) -> Result, Error> { + query( + connection, + ("SCRIPT", "EXISTS", NonEmpty::new("sha1s", sha1s)), + ) + .await + } + + #[doc = " Removes all the scripts from the cache, in the mode, the server's `lazyfree-lazy-user-flush`"] + #[doc = " without one."] + #[doc = ""] + #[doc = " SCRIPT FLUSH [ ASYNC | SYNC ]"] + #[doc = " https://valkey.io/commands/script-flush/"] + #[allow(async_fn_in_trait)] + async fn script_flush(connection: &Connection, mode: Option) -> Result<(), Error> { + query(connection, ("SCRIPT", "FLUSH", mode)).await + } + + #[doc = " Kills the script that is running, as long as it has not written."] + #[doc = ""] + #[doc = " SCRIPT KILL"] + #[doc = " https://valkey.io/commands/script-kill/"] + #[allow(async_fn_in_trait)] + async fn script_kill(connection: &Connection) -> Result<(), Error> { + query(connection, ("SCRIPT", "KILL")).await + } + + #[doc = " Caches the script without running it, returning its SHA1 digest, see `evalsha`."] + #[doc = ""] + #[doc = " SCRIPT LOAD script"] + #[doc = " https://valkey.io/commands/script-load/"] + #[allow(async_fn_in_trait)] + async fn script_load(connection: &Connection, script: String) -> Result { + query(connection, ("SCRIPT", "LOAD", script)).await + } + + #[doc = " The source of the cached script with the SHA1 digest."] + #[doc = ""] + #[doc = " SCRIPT SHOW sha1"] + #[doc = " https://valkey.io/commands/script-show/"] + #[allow(async_fn_in_trait)] + async fn script_show(connection: &Connection, sha1: String) -> Result { + query(connection, ("SCRIPT", "SHOW", sha1)).await + } + #[doc = " Unsubscribes the client from the given patterns, or from all of them if none is given."] #[doc = ""] #[doc = " PUNSUBSCRIBE [ pattern [ pattern ... ] ]"] diff --git a/components/commands/src/value.rs b/components/commands/src/value.rs index b3ee2b5..769fbd5 100644 --- a/components/commands/src/value.rs +++ b/components/commands/src/value.rs @@ -1,8 +1,9 @@ use crate::exports::componentized::valkey::commands::{ - Cursor, Double, DurationMilliseconds, DurationSeconds, Error, FieldExpiretime, - FieldPersistResult, FieldPexpiretime, FieldPttl, FieldTtl, GeoCoordinates, GeoSearchResult, - Integer, LcsIdxMatch, LcsIdxReply, LinsertResult, StreamId, TtlResult, UnixTimeMilliseconds, - UnixTimeSeconds, Value, XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, + Cursor, Double, DurationMilliseconds, DurationSeconds, EngineStats, Error, FieldExpiretime, + FieldPersistResult, FieldPexpiretime, FieldPttl, FieldTtl, FunctionStatsReply, GeoCoordinates, + GeoSearchResult, Integer, LcsIdxMatch, LcsIdxReply, Library, LibraryFunction, LinsertResult, + RunningFunction, StreamId, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, + XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, XpendingExtendedEntry, XpendingSummary, }; @@ -392,6 +393,61 @@ impl FromValue for GeoSearchResult { } } +impl FromValue for LibraryFunction { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + name: fields.required("name")?, + description: fields.required("description")?, + flags: fields.required("flags")?, + }) + } +} + +impl FromValue for Library { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + library_name: fields.required("library_name")?, + engine: fields.required("engine")?, + functions: fields.required("functions")?, + // only with WITHCODE + library_code: fields.optional("library_code")?, + }) + } +} + +impl FromValue for RunningFunction { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + name: fields.required("name")?, + command: fields.required("command")?, + duration: fields.required("duration_ms")?, + }) + } +} + +impl FromValue for EngineStats { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + libraries_count: fields.required("libraries_count")?, + functions_count: fields.required("functions_count")?, + }) + } +} + +impl FromValue for FunctionStatsReply { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + running_script: fields.required("running_script")?, + engines: fields.required("engines")?, + }) + } +} + impl FromValue for TtlResult { fn from_value(value: Value) -> Result { match Integer::from_value(value)? { diff --git a/wit/commands.wit b/wit/commands.wit index 0f4db08..4e4ecd7 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -948,7 +948,7 @@ interface commands { // CLUSTER FLUSHSLOT // COPY // DBSIZE - // DUMP + // DUMP, needs binary support, the payload is not a string // EXPIRETIME // FLUSHALL // FLUSHDB @@ -966,8 +966,8 @@ interface commands { // RANDOMKEY // RENAME // RENAMENX - // RESTORE - // RESTORE-ASKING + // RESTORE, needs binary support, the payload is not a string + // RESTORE-ASKING, needs binary support, the payload is not a string // SCAN // SWAPDB // TOUCH @@ -1274,27 +1274,171 @@ interface commands { // scripting commands - // not yet implemented, from the @scripting category: - // EVAL - // EVAL_RO - // EVALSHA - // EVALSHA_RO - // FCALL - // FCALL_RO - // FUNCTION DELETE - // FUNCTION DUMP - // FUNCTION FLUSH - // FUNCTION KILL - // FUNCTION LIST - // FUNCTION LOAD - // FUNCTION RESTORE - // FUNCTION STATS - // SCRIPT DEBUG - // SCRIPT EXISTS - // SCRIPT FLUSH - // SCRIPT KILL - // SCRIPT LOAD - // SCRIPT SHOW + /// Runs the Lua script, returning its result, the keys it accesses and its arguments are + /// `KEYS` and `ARGV` in the script. + /// + /// An error the script returns fails the call. + /// + /// EVAL script numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] + /// https://valkey.io/commands/eval/ + eval: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; + + /// Runs the Lua script, which may not write, returning its result, see `eval`. + /// + /// EVAL_RO script numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] + /// https://valkey.io/commands/eval_ro/ + eval-ro: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; + + /// Runs the Lua script cached with the SHA1 digest, see `script-load`, returning its result, see + /// `eval`. + /// + /// EVALSHA sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] + /// https://valkey.io/commands/evalsha/ + evalsha: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; + + /// Runs the Lua script cached with the SHA1 digest, which may not write, returning its result, + /// see `eval`. + /// + /// EVALSHA_RO sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] + /// https://valkey.io/commands/evalsha_ro/ + evalsha-ro: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; + + /// Calls the function, see `function-load`, returning its result, the keys it accesses and its + /// arguments are passed to it. + /// + /// An error the function returns fails the call. + /// + /// FCALL function numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] + /// https://valkey.io/commands/fcall/ + fcall: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; + + /// Calls the function, which must be flagged `no-writes`, returning its result, see `fcall`. + /// + /// FCALL_RO function numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] + /// https://valkey.io/commands/fcall_ro/ + fcall-ro: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; + + /// Deletes the library and its functions. + /// + /// FUNCTION DELETE library-name + /// https://valkey.io/commands/function-delete/ + function-delete: async func(connection: borrow, library-name: string) -> result<_, error>; + + /// Whether a flush happens in the background, `ASYNC`, or before replying, `SYNC`. + enum flush-mode { + /// `ASYNC` + %async, + /// `SYNC` + sync, + } + + /// Deletes all the libraries, in the mode, the server's `lazyfree-lazy-user-flush` without one. + /// + /// FUNCTION FLUSH [ ASYNC | SYNC ] + /// https://valkey.io/commands/function-flush/ + function-flush: async func(connection: borrow, mode: option) -> result<_, error>; + + /// Kills the function that is running, as long as it has not written. + /// + /// FUNCTION KILL + /// https://valkey.io/commands/function-kill/ + function-kill: async func(connection: borrow) -> result<_, error>; + + /// A function of a library. + record library-function { + name: string, + description: option, + /// The function's flags, e.g. `no-writes`. + %flags: list, + } + + /// A library of functions. + record library { + library-name: string, + /// The engine that runs the library, e.g. `LUA`. + engine: string, + functions: list, + /// The library's source code, with `withcode`. + library-code: option, + } + + /// The libraries, those with names matching the glob-style pattern with one. + /// + /// FUNCTION LIST [ LIBRARYNAME library-name-pattern ] [ WITHCODE ] + /// https://valkey.io/commands/function-list/ + function-list: async func(connection: borrow, library-name-pattern: option, withcode: bool) -> result, error>; + + /// Loads the library from its source code, replacing a library of the same name with + /// `replace`, returning its name. + /// + /// FUNCTION LOAD [ REPLACE ] function-code + /// https://valkey.io/commands/function-load/ + function-load: async func(connection: borrow, replace: bool, function-code: string) -> result; + + /// The function that is running. + record running-function { + name: string, + /// The command and its arguments that called the function. + command: list, + /// How long the function has been running. + duration: duration-milliseconds, + } + + /// The libraries and functions of an engine. + record engine-stats { + libraries-count: integer, + functions-count: integer, + } + + /// The function that is running, and the libraries and functions of each engine. + record function-stats-reply { + /// None when no function is running. + running-script: option, + /// The statistics of each engine, by name. + engines: map, + } + + /// The function that is running, and the libraries and functions of each engine. + /// + /// FUNCTION STATS + /// https://valkey.io/commands/function-stats/ + function-stats: async func(connection: borrow) -> result; + + /// Whether each of the scripts is cached, by SHA1 digest. + /// + /// SCRIPT EXISTS sha1 [ sha1 ... ] + /// https://valkey.io/commands/script-exists/ + script-exists: async func(connection: borrow, sha1s: list) -> result, error>; + + /// Removes all the scripts from the cache, in the mode, the server's `lazyfree-lazy-user-flush` + /// without one. + /// + /// SCRIPT FLUSH [ ASYNC | SYNC ] + /// https://valkey.io/commands/script-flush/ + script-flush: async func(connection: borrow, mode: option) -> result<_, error>; + + /// Kills the script that is running, as long as it has not written. + /// + /// SCRIPT KILL + /// https://valkey.io/commands/script-kill/ + script-kill: async func(connection: borrow) -> result<_, error>; + + /// Caches the script without running it, returning its SHA1 digest, see `evalsha`. + /// + /// SCRIPT LOAD script + /// https://valkey.io/commands/script-load/ + script-load: async func(connection: borrow, script: string) -> result; + + /// The source of the cached script with the SHA1 digest. + /// + /// SCRIPT SHOW sha1 + /// https://valkey.io/commands/script-show/ + script-show: async func(connection: borrow, sha1: string) -> result; + + // not implemented, from the @scripting category: + // FUNCTION DUMP and FUNCTION RESTORE, the payload is binary, not a string + // SCRIPT DEBUG, the debugger is interactive, with a protocol of its own + // set commands From bc27d8bf3d856b11feebe9c796878698741274a6 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 13 Oct 2026 13:11:43 -0400 Subject: [PATCH 15/37] keyspace commands Signed-off-by: Scott Andrews --- components/commands/src/command.rs | 37 ++- components/commands/src/lib.rs | 394 ++++++++++++++++++++++++++++- components/commands/src/value.rs | 49 +++- wit/commands.wit | 327 ++++++++++++++++++------ 4 files changed, 710 insertions(+), 97 deletions(-) diff --git a/components/commands/src/command.rs b/components/commands/src/command.rs index d2492f5..40905f7 100644 --- a/components/commands/src/command.rs +++ b/components/commands/src/command.rs @@ -3,10 +3,10 @@ use crate::exports::componentized::valkey::commands::{ BitfieldOperation, BitfieldOverflow, BitfieldSet, ChannelRule, CommandRule, Connection, Cursor, DbRule, DurationMilliseconds, DurationSeconds, Error, FlushMode, GeoCoordinates, GeoCount, GeoFrom, GeoShape, GeoUnit, GeoaddCondition, GetexTtl, HsetexCondition, HsetexFieldCondition, - KeyRule, LexBound, Limit, LinsertPosition, ListSide, MsetexPrecondition, PopEnd, Rule, - RuleSelector, ScoreBound, SetPrecondition, SetTtl, SortOpts, SortOrder, StreamTrim, - StreamTrimEfficiency, TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, UserRule, Value, - XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, + KeyRule, KeyType, LexBound, Limit, LinsertPosition, ListSide, MigrateAuth, MsetexPrecondition, + PopEnd, Rule, RuleSelector, ScoreBound, SetPrecondition, SetTtl, SortOpts, SortOrder, + StreamTrim, StreamTrimEfficiency, TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, UserRule, + Value, XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, }; use crate::value::{FromValue, OrZeroId, ZERO_ID}; @@ -74,7 +74,7 @@ macro_rules! into_command_numbers { }; } -into_command_numbers!(i32, i64, u8, u32, u64, usize, f64); +into_command_numbers!(i32, i64, u8, u16, u32, u64, usize, f64); /// No word for none. impl IntoCommand for Option { @@ -827,3 +827,30 @@ impl IntoCommand for KeysAndArgs { .into_command() } } + +/// The name of the type, e.g. for SCAN's TYPE. +impl IntoCommand for KeyType { + fn into_command(self) -> Result, Error> { + match self { + KeyType::String => "string".to_string(), + KeyType::List => "list".to_string(), + KeyType::Set => "set".to_string(), + KeyType::Zset => "zset".to_string(), + KeyType::Hash => "hash".to_string(), + KeyType::Stream => "stream".to_string(), + KeyType::Other(name) => name, + } + .into_command() + } +} + +impl IntoCommand for MigrateAuth { + fn into_command(self) -> Result, Error> { + match self { + MigrateAuth::Password(password) => ("AUTH", password).into_command(), + MigrateAuth::UsernamePassword((username, password)) => { + ("AUTH2", username, password).into_command() + } + } + } +} diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index 1024d57..30348e5 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -4,17 +4,19 @@ use crate::command::{ }; use crate::exports::componentized::valkey::commands::{ Aggregate, BitRange, BitfieldGet, BitfieldOperation, BitopOperation, Connection, Cursor, - Double, DurationMilliseconds, DurationSeconds, Error, FieldExpiretime, FieldPersistResult, - FieldPexpiretime, FieldPttl, FieldTtl, FlushMode, FunctionStatsReply, GeoCoordinates, GeoCount, - GeoSearchResult, GeoShape, GeoUnit, GeoaddCondition, GetexTtl, Guest, HsetexCondition, - HsetexFieldCondition, Integer, LcsIdxReply, LexBound, Library, Limit, LinsertPosition, - LinsertResult, ListSide, MsetexPrecondition, MsetexTtl, PopEnd, Rule, ScoreBound, - SetPrecondition, SetTtl, SortOpts, SortOrder, StreamId, StreamTrim, TtlMode, TtlResult, - UnixTimeMilliseconds, UnixTimeSeconds, Value, XclaimOpts, XinfoConsumer, XinfoGroup, + Double, DurationMilliseconds, DurationSeconds, Error, ExpiretimeReply, FieldPersistResult, + FlushMode, FunctionStatsReply, GeoCoordinates, GeoCount, GeoSearchResult, GeoShape, GeoUnit, + GeoaddCondition, GetexTtl, Guest, HsetexCondition, HsetexFieldCondition, Integer, KeyType, + LcsIdxReply, LexBound, Library, Limit, LinsertPosition, LinsertResult, ListSide, MigrateAuth, + MsetexPrecondition, MsetexTtl, PexpiretimeReply, PopEnd, PttlReply, Rule, ScoreBound, + SetPrecondition, SetTtl, SortOpts, SortOrder, StreamId, StreamTrim, TtlMode, TtlReply, + TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, XclaimOpts, XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, ZaddComparison, ZaddCondition, ZrangeBy, ZrangeWithscoresBy, }; -use crate::value::{FlatPairs, NextCursor, OrNever, OrZeroId, ScoredMembers}; +use crate::value::{ + FlatPairs, Migrated, NextCursor, OptionalKeyType, OrNever, OrZeroId, ScoredMembers, +}; use wit_bindgen::rt::Map; @@ -442,7 +444,7 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result, Error> { + ) -> Result, Error> { query( connection, ("HEXPIRETIME", key, Fields(NonEmpty::new("fields", fields))), @@ -558,7 +560,7 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result, Error> { + ) -> Result, Error> { query( connection, ("HPEXPIRETIME", key, Fields(NonEmpty::new("fields", fields))), @@ -577,7 +579,7 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result, Error> { + ) -> Result, Error> { query( connection, ("HPTTL", key, Fields(NonEmpty::new("fields", fields))), @@ -621,7 +623,7 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result, Error> { + ) -> Result, Error> { query( connection, ("HTTL", key, Fields(NonEmpty::new("fields", fields))), @@ -1504,6 +1506,374 @@ impl Guest for Commands { query(connection, ("KEYS", pattern)).await } + #[doc = " Removes all the keys of the slot, in the mode, the server's `lazyfree-lazy-user-flush`"] + #[doc = " without one."] + #[doc = ""] + #[doc = " CLUSTER FLUSHSLOT slot [ ASYNC | SYNC ]"] + #[doc = " https://valkey.io/commands/cluster-flushslot/"] + #[allow(async_fn_in_trait)] + async fn cluster_flushslot( + connection: &Connection, + slot: u16, + mode: Option, + ) -> Result<(), Error> { + query(connection, ("CLUSTER", "FLUSHSLOT", slot, mode)).await + } + + #[doc = " Incrementally iterate over the keys of a cluster, slot by slot, or within the slot."] + #[doc = ""] + #[doc = " The cursor carries the slot, for clients to route each call to the node that owns it."] + #[doc = ""] + #[doc = " CLUSTERSCAN cursor [ MATCH match-pattern ] [ COUNT count ] [ TYPE type ] [ SLOT slot ]"] + #[doc = " https://valkey.io/commands/clusterscan/"] + #[allow(async_fn_in_trait)] + async fn clusterscan( + connection: &Connection, + cursor: Cursor, + match_: Option, + count: Option, + type_: Option, + slot: Option, + ) -> Result<(Option, Vec), Error> { + let command = ( + "CLUSTERSCAN", + cursor, + NamedOption("MATCH", match_), + NamedOption("COUNT", count), + NamedOption("TYPE", type_), + NamedOption("SLOT", slot), + ); + let (cursor, keys): (NextCursor, Vec) = query(connection, command).await?; + Ok((cursor.0, keys)) + } + + #[doc = " Copies the value of the source key to the destination key, in the database, or the current"] + #[doc = " one, returning whether it was copied, false when the destination exists without `replace`."] + #[doc = ""] + #[doc = " COPY source destination [ DB destination-db ] [ REPLACE ]"] + #[doc = " https://valkey.io/commands/copy/"] + #[allow(async_fn_in_trait)] + async fn copy( + connection: &Connection, + source: String, + destination: String, + destination_db: Option, + replace: bool, + ) -> Result { + query( + connection, + ( + "COPY", + source, + destination, + NamedOption("DB", destination_db), + Flag("REPLACE", replace), + ), + ) + .await + } + + #[doc = " The number of keys in the current database."] + #[doc = ""] + #[doc = " DBSIZE"] + #[doc = " https://valkey.io/commands/dbsize/"] + #[allow(async_fn_in_trait)] + async fn dbsize(connection: &Connection) -> Result { + query(connection, "DBSIZE").await + } + + #[doc = " When the key expires, to the second."] + #[doc = ""] + #[doc = " See also `pexpiretime`."] + #[doc = ""] + #[doc = " EXPIRETIME key"] + #[doc = " https://valkey.io/commands/expiretime/"] + #[allow(async_fn_in_trait)] + async fn expiretime(connection: &Connection, key: String) -> Result { + query(connection, ("EXPIRETIME", key)).await + } + + #[doc = " Removes all the keys of all the databases, in the mode, the server's"] + #[doc = " `lazyfree-lazy-user-flush` without one."] + #[doc = ""] + #[doc = " FLUSHALL [ ASYNC | SYNC ]"] + #[doc = " https://valkey.io/commands/flushall/"] + #[allow(async_fn_in_trait)] + async fn flushall(connection: &Connection, mode: Option) -> Result<(), Error> { + query(connection, ("FLUSHALL", mode)).await + } + + #[doc = " Removes all the keys of the current database, in the mode, the server's"] + #[doc = " `lazyfree-lazy-user-flush` without one."] + #[doc = ""] + #[doc = " FLUSHDB [ ASYNC | SYNC ]"] + #[doc = " https://valkey.io/commands/flushdb/"] + #[allow(async_fn_in_trait)] + async fn flushdb(connection: &Connection, mode: Option) -> Result<(), Error> { + query(connection, ("FLUSHDB", mode)).await + } + + #[doc = " Moves the keys to the database of another instance, returning whether any were moved, false"] + #[doc = " when none of the keys exist."] + #[doc = ""] + #[doc = " With `copy` the keys are kept in this instance, with `replace` existing keys in the other"] + #[doc = " instance are replaced."] + #[doc = ""] + #[doc = " MIGRATE host port \"\" destination-db timeout [ COPY ] [ REPLACE ] [ AUTH password | AUTH2 username password ] KEYS key [ key ... ]"] + #[doc = " https://valkey.io/commands/migrate/"] + #[allow(async_fn_in_trait)] + async fn migrate( + connection: &Connection, + host: String, + port: u16, + destination_db: Integer, + timeout: DurationMilliseconds, + copy: bool, + replace: bool, + auth: Option, + keys: Vec, + ) -> Result { + // "" in place of the key, the keys follow KEYS + let command = ( + "MIGRATE", + host, + port, + "", + destination_db, + timeout, + Flag("COPY", copy), + Flag("REPLACE", replace), + auth, + ("KEYS", NonEmpty::new("keys", keys)), + ); + let migrated: Migrated = query(connection, command).await?; + Ok(migrated.0) + } + + #[doc = " Moves the key to the database, returning whether it was moved, false when it does not"] + #[doc = " exist, or exists in the database without `replace`."] + #[doc = ""] + #[doc = " MOVE key db [ REPLACE ]"] + #[doc = " https://valkey.io/commands/move/"] + #[allow(async_fn_in_trait)] + async fn move_( + connection: &Connection, + key: String, + db: Integer, + replace: bool, + ) -> Result { + query(connection, ("MOVE", key, db, Flag("REPLACE", replace))).await + } + + #[doc = " The internal encoding of the value stored at key, e.g. `listpack`, none when the key does"] + #[doc = " not exist."] + #[doc = ""] + #[doc = " OBJECT ENCODING key"] + #[doc = " https://valkey.io/commands/object-encoding/"] + #[allow(async_fn_in_trait)] + async fn object_encoding( + connection: &Connection, + key: String, + ) -> Result, Error> { + query(connection, ("OBJECT", "ENCODING", key)).await + } + + #[doc = " The logarithmic access frequency counter of the key, none when it does not exist."] + #[doc = ""] + #[doc = " Only with an LFU `maxmemory-policy`, otherwise it fails."] + #[doc = ""] + #[doc = " OBJECT FREQ key"] + #[doc = " https://valkey.io/commands/object-freq/"] + #[allow(async_fn_in_trait)] + async fn object_freq(connection: &Connection, key: String) -> Result, Error> { + query(connection, ("OBJECT", "FREQ", key)).await + } + + #[doc = " The time since the key was last accessed, none when it does not exist."] + #[doc = ""] + #[doc = " OBJECT IDLETIME key"] + #[doc = " https://valkey.io/commands/object-idletime/"] + #[allow(async_fn_in_trait)] + async fn object_idletime( + connection: &Connection, + key: String, + ) -> Result, Error> { + query(connection, ("OBJECT", "IDLETIME", key)).await + } + + #[doc = " The number of references to the value stored at key, none when the key does not exist."] + #[doc = ""] + #[doc = " OBJECT REFCOUNT key"] + #[doc = " https://valkey.io/commands/object-refcount/"] + #[allow(async_fn_in_trait)] + async fn object_refcount( + connection: &Connection, + key: String, + ) -> Result, Error> { + query(connection, ("OBJECT", "REFCOUNT", key)).await + } + + #[doc = " Removes the expiration of the key, returning whether it had one."] + #[doc = ""] + #[doc = " PERSIST key"] + #[doc = " https://valkey.io/commands/persist/"] + #[allow(async_fn_in_trait)] + async fn persist(connection: &Connection, key: String) -> Result { + query(connection, ("PERSIST", key)).await + } + + #[doc = " Set a timeout on key, in milliseconds."] + #[doc = ""] + #[doc = " PEXPIRE key milliseconds [ NX | XX | GT | LT ]"] + #[doc = " https://valkey.io/commands/pexpire/"] + #[allow(async_fn_in_trait)] + async fn pexpire( + connection: &Connection, + key: String, + milliseconds: DurationMilliseconds, + mode: Option, + ) -> Result { + query(connection, ("PEXPIRE", key, milliseconds, mode)).await + } + + #[doc = " Set a timeout on key, as a Unix time in milliseconds."] + #[doc = ""] + #[doc = " PEXPIREAT key unix-time-milliseconds [ NX | XX | GT | LT ]"] + #[doc = " https://valkey.io/commands/pexpireat/"] + #[allow(async_fn_in_trait)] + async fn pexpireat( + connection: &Connection, + key: String, + milliseconds: UnixTimeMilliseconds, + mode: Option, + ) -> Result { + query(connection, ("PEXPIREAT", key, milliseconds, mode)).await + } + + #[doc = " When the key expires, to the millisecond."] + #[doc = ""] + #[doc = " See also `expiretime`."] + #[doc = ""] + #[doc = " PEXPIRETIME key"] + #[doc = " https://valkey.io/commands/pexpiretime/"] + #[allow(async_fn_in_trait)] + async fn pexpiretime(connection: &Connection, key: String) -> Result { + query(connection, ("PEXPIRETIME", key)).await + } + + #[doc = " The remaining time to live of the key, to the millisecond."] + #[doc = ""] + #[doc = " See also `ttl`."] + #[doc = ""] + #[doc = " PTTL key"] + #[doc = " https://valkey.io/commands/pttl/"] + #[allow(async_fn_in_trait)] + async fn pttl(connection: &Connection, key: String) -> Result { + query(connection, ("PTTL", key)).await + } + + #[doc = " A random key of the current database, none when it is empty."] + #[doc = ""] + #[doc = " RANDOMKEY"] + #[doc = " https://valkey.io/commands/randomkey/"] + #[allow(async_fn_in_trait)] + async fn randomkey(connection: &Connection) -> Result, Error> { + query(connection, "RANDOMKEY").await + } + + #[doc = " Renames the key, replacing the new key when it exists."] + #[doc = ""] + #[doc = " RENAME key newkey"] + #[doc = " https://valkey.io/commands/rename/"] + #[allow(async_fn_in_trait)] + async fn rename(connection: &Connection, key: String, newkey: String) -> Result<(), Error> { + query(connection, ("RENAME", key, newkey)).await + } + + #[doc = " Renames the key, only when the new key does not exist, returning whether it was renamed."] + #[doc = ""] + #[doc = " RENAMENX key newkey"] + #[doc = " https://valkey.io/commands/renamenx/"] + #[allow(async_fn_in_trait)] + async fn renamenx(connection: &Connection, key: String, newkey: String) -> Result { + query(connection, ("RENAMENX", key, newkey)).await + } + + #[doc = " Incrementally iterate over the keys of the current database."] + #[doc = ""] + #[doc = " SCAN cursor [ MATCH pattern ] [ COUNT count ] [ TYPE type ]"] + #[doc = " https://valkey.io/commands/scan/"] + #[allow(async_fn_in_trait)] + async fn scan( + connection: &Connection, + cursor: Cursor, + match_: Option, + count: Option, + type_: Option, + ) -> Result<(Option, Vec), Error> { + let command = ( + "SCAN", + cursor, + NamedOption("MATCH", match_), + NamedOption("COUNT", count), + NamedOption("TYPE", type_), + ); + let (cursor, keys): (NextCursor, Vec) = query(connection, command).await?; + Ok((cursor.0, keys)) + } + + #[doc = " Swaps the two databases, for connections using either."] + #[doc = ""] + #[doc = " SWAPDB index1 index2"] + #[doc = " https://valkey.io/commands/swapdb/"] + #[allow(async_fn_in_trait)] + async fn swapdb( + connection: &Connection, + index1: Integer, + index2: Integer, + ) -> Result<(), Error> { + query(connection, ("SWAPDB", index1, index2)).await + } + + #[doc = " Updates the last access time of the keys, returning how many exist."] + #[doc = ""] + #[doc = " TOUCH key [ key ... ]"] + #[doc = " https://valkey.io/commands/touch/"] + #[allow(async_fn_in_trait)] + async fn touch(connection: &Connection, keys: Vec) -> Result { + query(connection, ("TOUCH", NonEmpty::new("keys", keys))).await + } + + #[doc = " The remaining time to live of the key, to the second."] + #[doc = ""] + #[doc = " See also `pttl`."] + #[doc = ""] + #[doc = " TTL key"] + #[doc = " https://valkey.io/commands/ttl/"] + #[allow(async_fn_in_trait)] + async fn ttl(connection: &Connection, key: String) -> Result { + query(connection, ("TTL", key)).await + } + + #[doc = " The type of the value stored at key, none when the key does not exist."] + #[doc = ""] + #[doc = " TYPE key"] + #[doc = " https://valkey.io/commands/type/"] + #[allow(async_fn_in_trait)] + async fn type_(connection: &Connection, key: String) -> Result, Error> { + let key_type: OptionalKeyType = query(connection, ("TYPE", key)).await?; + Ok(key_type.0) + } + + #[doc = " Removes the keys, freeing their memory in the background, returning how many existed."] + #[doc = ""] + #[doc = " UNLINK key [ key ... ]"] + #[doc = " https://valkey.io/commands/unlink/"] + #[allow(async_fn_in_trait)] + async fn unlink(connection: &Connection, keys: Vec) -> Result { + query(connection, ("UNLINK", NonEmpty::new("keys", keys))).await + } + #[doc = " Subscribes the client to the given patterns."] #[doc = ""] #[doc = " PSUBSCRIBE pattern [ pattern ... ]"] diff --git a/components/commands/src/value.rs b/components/commands/src/value.rs index 769fbd5..e2c7947 100644 --- a/components/commands/src/value.rs +++ b/components/commands/src/value.rs @@ -1,8 +1,8 @@ use crate::exports::componentized::valkey::commands::{ - Cursor, Double, DurationMilliseconds, DurationSeconds, EngineStats, Error, FieldExpiretime, - FieldPersistResult, FieldPexpiretime, FieldPttl, FieldTtl, FunctionStatsReply, GeoCoordinates, - GeoSearchResult, Integer, LcsIdxMatch, LcsIdxReply, Library, LibraryFunction, LinsertResult, - RunningFunction, StreamId, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, + Cursor, Double, DurationMilliseconds, DurationSeconds, EngineStats, Error, ExpiretimeReply, + FieldPersistResult, FunctionStatsReply, GeoCoordinates, GeoSearchResult, Integer, KeyType, + LcsIdxMatch, LcsIdxReply, Library, LibraryFunction, LinsertResult, PexpiretimeReply, PttlReply, + RunningFunction, StreamId, TtlReply, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, XpendingExtendedEntry, XpendingSummary, @@ -310,10 +310,10 @@ macro_rules! from_value_field_times { } from_value_field_times!( - FieldTtl(DurationSeconds) => ExpiresIn, - FieldPttl(DurationMilliseconds) => ExpiresIn, - FieldExpiretime(UnixTimeSeconds) => ExpiresAt, - FieldPexpiretime(UnixTimeMilliseconds) => ExpiresAt + TtlReply(DurationSeconds) => ExpiresIn, + PttlReply(DurationMilliseconds) => ExpiresIn, + ExpiretimeReply(UnixTimeSeconds) => ExpiresAt, + PexpiretimeReply(UnixTimeMilliseconds) => ExpiresAt ); impl FromValue for FieldPersistResult { @@ -448,6 +448,39 @@ impl FromValue for FunctionStatsReply { } } +/// The type of a key, by name, none for `none`, a key that does not exist. +pub struct OptionalKeyType(pub Option); + +impl FromValue for OptionalKeyType { + fn from_value(value: Value) -> Result { + // `none` rather than null, though the reply schema has null + let name: Option = FromValue::from_value(value)?; + Ok(OptionalKeyType(match name.as_deref() { + None | Some("none") => None, + Some("string") => Some(KeyType::String), + Some("list") => Some(KeyType::List), + Some("set") => Some(KeyType::Set), + Some("zset") => Some(KeyType::Zset), + Some("hash") => Some(KeyType::Hash), + Some("stream") => Some(KeyType::Stream), + Some(_) => name.map(KeyType::Other), + })) + } +} + +/// `OK` when keys were moved, `NOKEY` when none of them exist. +pub struct Migrated(pub bool); + +impl FromValue for Migrated { + fn from_value(value: Value) -> Result { + match &value { + Value::String(reply) if reply == "OK" => Ok(Migrated(true)), + Value::String(reply) if reply == "NOKEY" => Ok(Migrated(false)), + _ => Err(unexpected(&value)), + } + } +} + impl FromValue for TtlResult { fn from_value(value: Value) -> Result { match Integer::from_value(value)? { diff --git a/wit/commands.wit b/wit/commands.wit index 4e4ecd7..d51fa64 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -69,6 +69,46 @@ interface commands { zero-seconds, } + /// The remaining time to live of a key or hash field, to the second, see `ttl` and `httl`. + variant ttl-reply { + /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. + does-not-exist, + /// -1: the key or field exists, but has no expiration. + no-expiration, + /// The time until it expires. + expires-in(duration-seconds), + } + + /// The remaining time to live of a key or hash field, to the millisecond, see `pttl` and `hpttl`. + variant pttl-reply { + /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. + does-not-exist, + /// -1: the key or field exists, but has no expiration. + no-expiration, + /// The time until it expires. + expires-in(duration-milliseconds), + } + + /// When a key or hash field expires, to the second, see `expiretime` and `hexpiretime`. + variant expiretime-reply { + /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. + does-not-exist, + /// -1: the key or field exists, but has no expiration. + no-expiration, + /// The time at which it expires. + expires-at(unix-time-seconds), + } + + /// When a key or hash field expires, to the millisecond, see `pexpiretime` and `hpexpiretime`. + variant pexpiretime-reply { + /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. + does-not-exist, + /// -1: the key or field exists, but has no expiration. + no-expiration, + /// The time at which it expires. + expires-at(unix-time-milliseconds), + } + // admin commands /// Delete all the specified ACL users and terminate all the connections that are authenticated with such users. @@ -633,53 +673,13 @@ interface commands { /// https://valkey.io/commands/hexpireat/ hexpireat: async func(connection: borrow, key: string, seconds: unix-time-seconds, mode: option, fields: list) -> result, error>; - /// The remaining time to live of a hash field, to the second, see `httl`. - variant field-ttl { - /// -2: the field does not exist, or the hash does not exist. - does-not-exist, - /// -1: the field exists, but has no expiration. - no-expiration, - /// The time until the field expires. - expires-in(duration-seconds), - } - - /// The remaining time to live of a hash field, to the millisecond, see `hpttl`. - variant field-pttl { - /// -2: the field does not exist, or the hash does not exist. - does-not-exist, - /// -1: the field exists, but has no expiration. - no-expiration, - /// The time until the field expires. - expires-in(duration-milliseconds), - } - - /// When a hash field expires, to the second, see `hexpiretime`. - variant field-expiretime { - /// -2: the field does not exist, or the hash does not exist. - does-not-exist, - /// -1: the field exists, but has no expiration. - no-expiration, - /// The time at which the field expires. - expires-at(unix-time-seconds), - } - - /// When a hash field expires, to the millisecond, see `hpexpiretime`. - variant field-pexpiretime { - /// -2: the field does not exist, or the hash does not exist. - does-not-exist, - /// -1: the field exists, but has no expiration. - no-expiration, - /// The time at which the field expires. - expires-at(unix-time-milliseconds), - } - /// When each of the hash fields expires, to the second. /// /// See also `hpexpiretime`. /// /// HEXPIRETIME key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpiretime/ - hexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; + hexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; /// Returns the value associated with field in the hash stored at key. /// @@ -776,7 +776,7 @@ interface commands { /// /// HPEXPIRETIME key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpiretime/ - hpexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; + hpexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; /// The remaining time to live of each of the hash fields, to the millisecond. /// @@ -784,7 +784,7 @@ interface commands { /// /// HPTTL key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpttl/ - hpttl: async func(connection: borrow, key: string, fields: list) -> result, error>; + hpttl: async func(connection: borrow, key: string, fields: list) -> result, error>; /// Random fields from the hash value stored at key, up to the count. /// @@ -871,7 +871,7 @@ interface commands { /// /// HTTL key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/httl/ - httl: async func(connection: borrow, key: string, fields: list) -> result, error>; + httl: async func(connection: borrow, key: string, fields: list) -> result, error>; /// All values in the hash stored at key. /// @@ -913,6 +913,60 @@ interface commands { // keyspace commands + /// The type of the value stored at a key. + variant key-type { + /// `string` + %string, + /// `list` + %list, + /// `set` + set, + /// `zset`: a sorted set. + zset, + /// `hash` + hash, + /// `stream` + %stream, + /// The type of a module, by name. + other(string), + } + + /// The credentials to authenticate to the other instance with, for MIGRATE. + variant migrate-auth { + /// `AUTH password` + password(string), + /// `AUTH2 username password` + username-password(tuple), + } + + /// Removes all the keys of the slot, in the mode, the server's `lazyfree-lazy-user-flush` + /// without one. + /// + /// CLUSTER FLUSHSLOT slot [ ASYNC | SYNC ] + /// https://valkey.io/commands/cluster-flushslot/ + cluster-flushslot: async func(connection: borrow, slot: u16, mode: option) -> result<_, error>; + + /// Incrementally iterate over the keys of a cluster, slot by slot, or within the slot. + /// + /// The cursor carries the slot, for clients to route each call to the node that owns it. + /// + /// CLUSTERSCAN cursor [ MATCH match-pattern ] [ COUNT count ] [ TYPE type ] [ SLOT slot ] + /// https://valkey.io/commands/clusterscan/ + clusterscan: async func(connection: borrow, cursor: cursor, match: option, count: option, %type: option, slot: option) -> result, list>, error>; + + /// Copies the value of the source key to the destination key, in the database, or the current + /// one, returning whether it was copied, false when the destination exists without `replace`. + /// + /// COPY source destination [ DB destination-db ] [ REPLACE ] + /// https://valkey.io/commands/copy/ + copy: async func(connection: borrow, source: string, destination: string, destination-db: option, replace: bool) -> result; + + /// The number of keys in the current database. + /// + /// DBSIZE + /// https://valkey.io/commands/dbsize/ + dbsize: async func(connection: borrow) -> result; + /// Removes the specified keys. /// /// DEL key [ key ... ] @@ -937,43 +991,172 @@ interface commands { /// https://valkey.io/commands/expireat/ expireat: async func(connection: borrow, key: string, seconds: unix-time-seconds, mode: option) -> result; + /// When the key expires, to the second. + /// + /// See also `pexpiretime`. + /// + /// EXPIRETIME key + /// https://valkey.io/commands/expiretime/ + expiretime: async func(connection: borrow, key: string) -> result; + + /// Removes all the keys of all the databases, in the mode, the server's + /// `lazyfree-lazy-user-flush` without one. + /// + /// FLUSHALL [ ASYNC | SYNC ] + /// https://valkey.io/commands/flushall/ + flushall: async func(connection: borrow, mode: option) -> result<_, error>; + + /// Removes all the keys of the current database, in the mode, the server's + /// `lazyfree-lazy-user-flush` without one. + /// + /// FLUSHDB [ ASYNC | SYNC ] + /// https://valkey.io/commands/flushdb/ + flushdb: async func(connection: borrow, mode: option) -> result<_, error>; + /// Returns all keys matching pattern. /// /// KEYS pattern /// https://valkey.io/commands/keys/ keys: async func(connection: borrow, pattern: string) -> result, error>; - // not yet implemented, from the @keyspace category: - // CLUSTERSCAN - // CLUSTER FLUSHSLOT - // COPY - // DBSIZE + /// Moves the keys to the database of another instance, returning whether any were moved, false + /// when none of the keys exist. + /// + /// With `copy` the keys are kept in this instance, with `replace` existing keys in the other + /// instance are replaced. + /// + /// MIGRATE host port "" destination-db timeout [ COPY ] [ REPLACE ] [ AUTH password | AUTH2 username password ] KEYS key [ key ... ] + /// https://valkey.io/commands/migrate/ + migrate: async func(connection: borrow, host: string, port: u16, destination-db: integer, timeout: duration-milliseconds, copy: bool, replace: bool, auth: option, keys: list) -> result; + + /// Moves the key to the database, returning whether it was moved, false when it does not + /// exist, or exists in the database without `replace`. + /// + /// MOVE key db [ REPLACE ] + /// https://valkey.io/commands/move/ + move: async func(connection: borrow, key: string, db: integer, replace: bool) -> result; + + /// The internal encoding of the value stored at key, e.g. `listpack`, none when the key does + /// not exist. + /// + /// OBJECT ENCODING key + /// https://valkey.io/commands/object-encoding/ + object-encoding: async func(connection: borrow, key: string) -> result, error>; + + /// The logarithmic access frequency counter of the key, none when it does not exist. + /// + /// Only with an LFU `maxmemory-policy`, otherwise it fails. + /// + /// OBJECT FREQ key + /// https://valkey.io/commands/object-freq/ + object-freq: async func(connection: borrow, key: string) -> result, error>; + + /// The time since the key was last accessed, none when it does not exist. + /// + /// OBJECT IDLETIME key + /// https://valkey.io/commands/object-idletime/ + object-idletime: async func(connection: borrow, key: string) -> result, error>; + + /// The number of references to the value stored at key, none when the key does not exist. + /// + /// OBJECT REFCOUNT key + /// https://valkey.io/commands/object-refcount/ + object-refcount: async func(connection: borrow, key: string) -> result, error>; + + /// Removes the expiration of the key, returning whether it had one. + /// + /// PERSIST key + /// https://valkey.io/commands/persist/ + persist: async func(connection: borrow, key: string) -> result; + + /// Set a timeout on key, in milliseconds. + /// + /// PEXPIRE key milliseconds [ NX | XX | GT | LT ] + /// https://valkey.io/commands/pexpire/ + pexpire: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, mode: option) -> result; + + /// Set a timeout on key, as a Unix time in milliseconds. + /// + /// PEXPIREAT key unix-time-milliseconds [ NX | XX | GT | LT ] + /// https://valkey.io/commands/pexpireat/ + pexpireat: async func(connection: borrow, key: string, milliseconds: unix-time-milliseconds, mode: option) -> result; + + /// When the key expires, to the millisecond. + /// + /// See also `expiretime`. + /// + /// PEXPIRETIME key + /// https://valkey.io/commands/pexpiretime/ + pexpiretime: async func(connection: borrow, key: string) -> result; + + /// The remaining time to live of the key, to the millisecond. + /// + /// See also `ttl`. + /// + /// PTTL key + /// https://valkey.io/commands/pttl/ + pttl: async func(connection: borrow, key: string) -> result; + + /// A random key of the current database, none when it is empty. + /// + /// RANDOMKEY + /// https://valkey.io/commands/randomkey/ + randomkey: async func(connection: borrow) -> result, error>; + + /// Renames the key, replacing the new key when it exists. + /// + /// RENAME key newkey + /// https://valkey.io/commands/rename/ + rename: async func(connection: borrow, key: string, newkey: string) -> result<_, error>; + + /// Renames the key, only when the new key does not exist, returning whether it was renamed. + /// + /// RENAMENX key newkey + /// https://valkey.io/commands/renamenx/ + renamenx: async func(connection: borrow, key: string, newkey: string) -> result; + + /// Incrementally iterate over the keys of the current database. + /// + /// SCAN cursor [ MATCH pattern ] [ COUNT count ] [ TYPE type ] + /// https://valkey.io/commands/scan/ + scan: async func(connection: borrow, cursor: cursor, match: option, count: option, %type: option) -> result, list>, error>; + + /// Swaps the two databases, for connections using either. + /// + /// SWAPDB index1 index2 + /// https://valkey.io/commands/swapdb/ + swapdb: async func(connection: borrow, index1: integer, index2: integer) -> result<_, error>; + + /// Updates the last access time of the keys, returning how many exist. + /// + /// TOUCH key [ key ... ] + /// https://valkey.io/commands/touch/ + touch: async func(connection: borrow, keys: list) -> result; + + /// The remaining time to live of the key, to the second. + /// + /// See also `pttl`. + /// + /// TTL key + /// https://valkey.io/commands/ttl/ + ttl: async func(connection: borrow, key: string) -> result; + + /// The type of the value stored at key, none when the key does not exist. + /// + /// TYPE key + /// https://valkey.io/commands/type/ + %type: async func(connection: borrow, key: string) -> result, error>; + + /// Removes the keys, freeing their memory in the background, returning how many existed. + /// + /// UNLINK key [ key ... ] + /// https://valkey.io/commands/unlink/ + unlink: async func(connection: borrow, keys: list) -> result; + + // not implemented, from the @keyspace category: // DUMP, needs binary support, the payload is not a string - // EXPIRETIME - // FLUSHALL - // FLUSHDB - // MIGRATE - // MOVE - // OBJECT ENCODING - // OBJECT FREQ - // OBJECT IDLETIME - // OBJECT REFCOUNT - // PERSIST - // PEXPIRE - // PEXPIREAT - // PEXPIRETIME - // PTTL - // RANDOMKEY - // RENAME - // RENAMENX // RESTORE, needs binary support, the payload is not a string // RESTORE-ASKING, needs binary support, the payload is not a string - // SCAN - // SWAPDB - // TOUCH - // TTL - // TYPE - // UNLINK // list commands From 2e9081b83268b62d76b054f72b5cd06ba6d7b106 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 13 Oct 2026 14:35:20 -0400 Subject: [PATCH 16/37] admin commands Signed-off-by: Scott Andrews --- components/commands/src/command.rs | 176 +++++- components/commands/src/lib.rs | 843 ++++++++++++++++++++++++++++- components/commands/src/value.rs | 465 +++++++++++++++- wit/commands.wit | 807 ++++++++++++++++++++++++--- 4 files changed, 2191 insertions(+), 100 deletions(-) diff --git a/components/commands/src/command.rs b/components/commands/src/command.rs index 40905f7..295e191 100644 --- a/components/commands/src/command.rs +++ b/components/commands/src/command.rs @@ -1,12 +1,15 @@ use crate::exports::componentized::valkey::commands::{ - Aggregate, BitRange, BitfieldEncoding, BitfieldGet, BitfieldIncrby, BitfieldOffset, - BitfieldOperation, BitfieldOverflow, BitfieldSet, ChannelRule, CommandRule, Connection, Cursor, - DbRule, DurationMilliseconds, DurationSeconds, Error, FlushMode, GeoCoordinates, GeoCount, - GeoFrom, GeoShape, GeoUnit, GeoaddCondition, GetexTtl, HsetexCondition, HsetexFieldCondition, - KeyRule, KeyType, LexBound, Limit, LinsertPosition, ListSide, MigrateAuth, MsetexPrecondition, - PopEnd, Rule, RuleSelector, ScoreBound, SetPrecondition, SetTtl, SortOpts, SortOrder, - StreamTrim, StreamTrimEfficiency, TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, UserRule, - Value, XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, + Aggregate, BgsaveOperation, BitRange, BitfieldEncoding, BitfieldGet, BitfieldIncrby, + BitfieldOffset, BitfieldOperation, BitfieldOverflow, BitfieldSet, ChannelRule, ClientFilter, + ClientPauseMode, ClientType, ClientUnblockType, ClusterFailoverMode, ClusterResetMode, + ClusterSetslotState, CommandRule, CommandlogType, Connection, Cursor, DbRule, + DurationMilliseconds, DurationSeconds, Error, FailoverTarget, FlushMode, GeoCoordinates, + GeoCount, GeoFrom, GeoShape, GeoUnit, GeoaddCondition, GetexTtl, HsetexCondition, + HsetexFieldCondition, KeyRule, KeyType, LexBound, Limit, LinsertPosition, ListSide, + MigrateAuth, MsetexPrecondition, PopEnd, Rule, RuleSelector, ScoreBound, SetPrecondition, + SetTtl, ShutdownSave, SlotMigrationGroup, SortOpts, SortOrder, StreamTrim, + StreamTrimEfficiency, TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, UserRule, Value, + XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, }; use crate::value::{FromValue, OrZeroId, ZERO_ID}; @@ -204,6 +207,7 @@ impl IntoCommand for Rule { Rule::Command(rule) => rule.into_command(), Rule::Db(rule) => rule.into_command(), Rule::User(rule) => rule.into_command(), + Rule::Other(rule) => rule.into_command(), } } } @@ -215,6 +219,7 @@ impl IntoCommand for RuleSelector { RuleSelector::Channel(rule) => rule.into_command(), RuleSelector::Command(rule) => rule.into_command(), RuleSelector::Db(rule) => rule.into_command(), + RuleSelector::Other(rule) => rule.into_command(), } } } @@ -282,6 +287,8 @@ impl IntoCommand for UserRule { UserRule::On => "on".into_command(), UserRule::Off => "off".into_command(), UserRule::Nopass => "nopass".into_command(), + UserRule::SanitizePayload => "sanitize-payload".into_command(), + UserRule::SkipSanitizePayload => "skip-sanitize-payload".into_command(), UserRule::Password(password) => format!(">{password}").into_command(), UserRule::HashedPassword(hash) => format!("#{hash}").into_command(), UserRule::RemovePassword(password) => format!("<{password}").into_command(), @@ -854,3 +861,156 @@ impl IntoCommand for MigrateAuth { } } } + +impl IntoCommand for BgsaveOperation { + fn into_command(self) -> Result, Error> { + match self { + BgsaveOperation::Schedule => "SCHEDULE", + BgsaveOperation::Cancel => "CANCEL", + } + .into_command() + } +} + +/// `master` rather than `primary`, which servers before Valkey 8.0 do not know. +impl IntoCommand for ClientType { + fn into_command(self) -> Result, Error> { + match self { + ClientType::Normal => "normal", + ClientType::Primary => "master", + ClientType::Replica => "replica", + ClientType::Pubsub => "pubsub", + } + .into_command() + } +} + +impl IntoCommand for ClientPauseMode { + fn into_command(self) -> Result, Error> { + match self { + ClientPauseMode::Write => "WRITE", + ClientPauseMode::All => "ALL", + } + .into_command() + } +} + +impl IntoCommand for ClientUnblockType { + fn into_command(self) -> Result, Error> { + match self { + ClientUnblockType::Timeout => "TIMEOUT", + ClientUnblockType::Error => "ERROR", + } + .into_command() + } +} + +impl IntoCommand for ClusterFailoverMode { + fn into_command(self) -> Result, Error> { + match self { + ClusterFailoverMode::Force => "FORCE", + ClusterFailoverMode::Takeover => "TAKEOVER", + } + .into_command() + } +} + +impl IntoCommand for ClusterResetMode { + fn into_command(self) -> Result, Error> { + match self { + ClusterResetMode::Hard => "HARD", + ClusterResetMode::Soft => "SOFT", + } + .into_command() + } +} + +impl IntoCommand for CommandlogType { + fn into_command(self) -> Result, Error> { + match self { + CommandlogType::Slow => "slow", + CommandlogType::LargeRequest => "large-request", + CommandlogType::LargeReply => "large-reply", + } + .into_command() + } +} + +impl IntoCommand for ShutdownSave { + fn into_command(self) -> Result, Error> { + match self { + ShutdownSave::Nosave => "NOSAVE", + ShutdownSave::Save => "SAVE", + } + .into_command() + } +} + +impl IntoCommand for ClientFilter { + fn into_command(self) -> Result, Error> { + match self { + ClientFilter::Id(ids) => ("ID", NonEmpty::new("id", ids)).into_command(), + ClientFilter::Type(client_type) => ("TYPE", client_type).into_command(), + ClientFilter::User(user) => ("USER", user).into_command(), + ClientFilter::Addr(addr) => ("ADDR", addr).into_command(), + ClientFilter::Laddr(laddr) => ("LADDR", laddr).into_command(), + ClientFilter::Skipme(skip) => { + ("SKIPME", if skip { "YES" } else { "NO" }).into_command() + } + ClientFilter::Maxage(age) => ("MAXAGE", age).into_command(), + ClientFilter::Name(name) => ("NAME", name).into_command(), + ClientFilter::Idle(idle) => ("IDLE", idle).into_command(), + ClientFilter::Flags(flags) => ("FLAGS", flags).into_command(), + ClientFilter::LibName(name) => ("LIB-NAME", name).into_command(), + ClientFilter::LibVer(version) => ("LIB-VER", version).into_command(), + ClientFilter::Db(db) => ("DB", db).into_command(), + ClientFilter::Capa(capability) => ("CAPA", capability).into_command(), + ClientFilter::Ip(ip) => ("IP", ip).into_command(), + ClientFilter::NotId(ids) => ("NOT-ID", NonEmpty::new("not-id", ids)).into_command(), + ClientFilter::NotType(client_type) => ("NOT-TYPE", client_type).into_command(), + ClientFilter::NotUser(user) => ("NOT-USER", user).into_command(), + ClientFilter::NotAddr(addr) => ("NOT-ADDR", addr).into_command(), + ClientFilter::NotLaddr(laddr) => ("NOT-LADDR", laddr).into_command(), + ClientFilter::NotName(name) => ("NOT-NAME", name).into_command(), + ClientFilter::NotFlags(flags) => ("NOT-FLAGS", flags).into_command(), + ClientFilter::NotLibName(name) => ("NOT-LIB-NAME", name).into_command(), + ClientFilter::NotLibVer(version) => ("NOT-LIB-VER", version).into_command(), + ClientFilter::NotDb(db) => ("NOT-DB", db).into_command(), + ClientFilter::NotCapa(capability) => ("NOT-CAPA", capability).into_command(), + ClientFilter::NotIp(ip) => ("NOT-IP", ip).into_command(), + } + } +} + +impl IntoCommand for ClusterSetslotState { + fn into_command(self) -> Result, Error> { + match self { + ClusterSetslotState::Importing(node_id) => ("IMPORTING", node_id).into_command(), + ClusterSetslotState::Migrating(node_id) => ("MIGRATING", node_id).into_command(), + ClusterSetslotState::Node(node_id) => ("NODE", node_id).into_command(), + ClusterSetslotState::Stable => "STABLE".into_command(), + } + } +} + +/// `TO host port [ FORCE ]` +impl IntoCommand for FailoverTarget { + fn into_command(self) -> Result, Error> { + ("TO", self.host, self.port, Flag("FORCE", self.force)).into_command() + } +} + +/// `SLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] NODE node-id [ AUTH username password ]` +impl IntoCommand for SlotMigrationGroup { + fn into_command(self) -> Result, Error> { + ( + "SLOTSRANGE", + NonEmpty::new("ranges", self.ranges), + "NODE", + self.node_id, + self.auth + .map(|(username, password)| ("AUTH", username, password)), + ) + .into_command() + } +} diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index 30348e5..d3b2a94 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -3,19 +3,24 @@ use crate::command::{ ScoreRange, Zrange, emit, query, }; use crate::exports::componentized::valkey::commands::{ - Aggregate, BitRange, BitfieldGet, BitfieldOperation, BitopOperation, Connection, Cursor, - Double, DurationMilliseconds, DurationSeconds, Error, ExpiretimeReply, FieldPersistResult, - FlushMode, FunctionStatsReply, GeoCoordinates, GeoCount, GeoSearchResult, GeoShape, GeoUnit, - GeoaddCondition, GetexTtl, Guest, HsetexCondition, HsetexFieldCondition, Integer, KeyType, - LcsIdxReply, LexBound, Library, Limit, LinsertPosition, LinsertResult, ListSide, MigrateAuth, - MsetexPrecondition, MsetexTtl, PexpiretimeReply, PopEnd, PttlReply, Rule, ScoreBound, - SetPrecondition, SetTtl, SortOpts, SortOrder, StreamId, StreamTrim, TtlMode, TtlReply, - TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, XclaimOpts, XinfoConsumer, XinfoGroup, - XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, ZaddComparison, - ZaddCondition, ZrangeBy, ZrangeWithscoresBy, + AclDryrunResult, AclListEntry, AclLogEntry, Aggregate, BgsaveOperation, BitRange, BitfieldGet, + BitfieldOperation, BitopOperation, ClientFilter, ClientPauseMode, ClientUnblockType, + ClusterBumpepochResult, ClusterFailoverMode, ClusterResetMode, ClusterSetslotState, + CommandlogType, Connection, Cursor, Double, DurationMilliseconds, DurationSeconds, Error, + ExpiretimeReply, FailoverTarget, FieldPersistResult, FlushMode, FunctionStatsReply, + GeoCoordinates, GeoCount, GeoSearchResult, GeoShape, GeoUnit, GeoaddCondition, GetexTtl, Guest, + HsetexCondition, HsetexFieldCondition, Integer, KeyType, LatencyHistogramEntry, + LatencyLatestEntry, LcsIdxReply, LexBound, Library, Limit, LinsertPosition, LinsertResult, + ListSide, LogEntry, MigrateAuth, ModuleInfo, MsetexPrecondition, MsetexTtl, PexpiretimeReply, + PopEnd, PttlReply, RoleReply, Rule, ScoreBound, SetPrecondition, SetTtl, ShutdownSave, + SlotMigration, SlotMigrationGroup, SortOpts, SortOrder, StreamId, StreamTrim, TtlMode, + TtlReply, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, XclaimOpts, XinfoConsumer, + XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, + ZaddComparison, ZaddCondition, ZrangeBy, ZrangeWithscoresBy, }; use crate::value::{ - FlatPairs, Migrated, NextCursor, OptionalKeyType, OrNever, OrZeroId, ScoredMembers, + AclList, AclUserRules, ClientList, FlatPairs, Migrated, NextCursor, OptionalKeyType, OrNever, + OrZeroId, ScoredMembers, }; use wit_bindgen::rt::Map; @@ -81,6 +86,822 @@ impl Guest for Commands { .await } + #[doc = " The ACL categories, or with a category, the commands in it."] + #[doc = ""] + #[doc = " ACL CAT [ category ]"] + #[doc = " https://valkey.io/commands/acl-cat/"] + #[allow(async_fn_in_trait)] + async fn acl_cat( + connection: &Connection, + category: Option, + ) -> Result, Error> { + query(connection, ("ACL", "CAT", category)).await + } + + #[doc = " Whether the user may run the command, without running it."] + #[doc = ""] + #[doc = " ACL DRYRUN username command [ arg [ arg ... ] ]"] + #[doc = " https://valkey.io/commands/acl-dryrun/"] + #[allow(async_fn_in_trait)] + async fn acl_dryrun( + connection: &Connection, + username: String, + command: Vec, + ) -> Result { + query( + connection, + ("ACL", "DRYRUN", username, NonEmpty::new("command", command)), + ) + .await + } + + #[doc = " The rules of the user, none when the user does not exist."] + #[doc = ""] + #[doc = " ACL GETUSER username"] + #[doc = " https://valkey.io/commands/acl-getuser/"] + #[allow(async_fn_in_trait)] + async fn acl_getuser( + connection: &Connection, + username: String, + ) -> Result>, Error> { + let rules: Option = query(connection, ("ACL", "GETUSER", username)).await?; + Ok(rules.map(|rules| rules.0)) + } + + #[doc = " The rules of every user, in the format of an ACL file."] + #[doc = ""] + #[doc = " ACL LIST"] + #[doc = " https://valkey.io/commands/acl-list/"] + #[allow(async_fn_in_trait)] + async fn acl_list(connection: &Connection) -> Result, Error> { + let users: AclList = query(connection, ("ACL", "LIST")).await?; + Ok(users.0) + } + + #[doc = " Replaces the users with those of the configured ACL file."] + #[doc = ""] + #[doc = " ACL LOAD"] + #[doc = " https://valkey.io/commands/acl-load/"] + #[allow(async_fn_in_trait)] + async fn acl_load(connection: &Connection) -> Result<(), Error> { + query(connection, ("ACL", "LOAD")).await + } + + #[doc = " The recent security events, commands or authentications the rules denied, up to the count,"] + #[doc = " 10 without one."] + #[doc = ""] + #[doc = " See also `acl-log-reset`."] + #[doc = ""] + #[doc = " ACL LOG [ count ]"] + #[doc = " https://valkey.io/commands/acl-log/"] + #[allow(async_fn_in_trait)] + async fn acl_log( + connection: &Connection, + count: Option, + ) -> Result, Error> { + query(connection, ("ACL", "LOG", count)).await + } + + #[doc = " Clears the security events."] + #[doc = ""] + #[doc = " See also `acl-log`."] + #[doc = ""] + #[doc = " ACL LOG RESET"] + #[doc = " https://valkey.io/commands/acl-log/"] + #[allow(async_fn_in_trait)] + async fn acl_log_reset(connection: &Connection) -> Result<(), Error> { + query(connection, ("ACL", "LOG", "RESET")).await + } + + #[doc = " Saves the users to the configured ACL file."] + #[doc = ""] + #[doc = " ACL SAVE"] + #[doc = " https://valkey.io/commands/acl-save/"] + #[allow(async_fn_in_trait)] + async fn acl_save(connection: &Connection) -> Result<(), Error> { + query(connection, ("ACL", "SAVE")).await + } + + #[doc = " The names of the users."] + #[doc = ""] + #[doc = " ACL USERS"] + #[doc = " https://valkey.io/commands/acl-users/"] + #[allow(async_fn_in_trait)] + async fn acl_users(connection: &Connection) -> Result, Error> { + query(connection, ("ACL", "USERS")).await + } + + #[doc = " The name of the user the connection is authenticated as."] + #[doc = ""] + #[doc = " ACL WHOAMI"] + #[doc = " https://valkey.io/commands/acl-whoami/"] + #[allow(async_fn_in_trait)] + async fn acl_whoami(connection: &Connection) -> Result { + query(connection, ("ACL", "WHOAMI")).await + } + + #[doc = " Starts rewriting the append-only file in the background, returning the server's message."] + #[doc = ""] + #[doc = " BGREWRITEAOF"] + #[doc = " https://valkey.io/commands/bgrewriteaof/"] + #[allow(async_fn_in_trait)] + async fn bgrewriteaof(connection: &Connection) -> Result { + query(connection, "BGREWRITEAOF").await + } + + #[doc = " Starts saving the databases to disk in the background, or schedules or cancels it, returning"] + #[doc = " the server's message."] + #[doc = ""] + #[doc = " BGSAVE [ SCHEDULE | CANCEL ]"] + #[doc = " https://valkey.io/commands/bgsave/"] + #[allow(async_fn_in_trait)] + async fn bgsave( + connection: &Connection, + operation: Option, + ) -> Result { + query(connection, ("BGSAVE", operation)).await + } + + #[doc = " Closes the connections that match every one of the filters, returning how many."] + #[doc = ""] + #[doc = " CLIENT KILL filter [ filter ... ]"] + #[doc = " https://valkey.io/commands/client-kill/"] + #[allow(async_fn_in_trait)] + async fn client_kill( + connection: &Connection, + filters: Vec, + ) -> Result { + query( + connection, + ("CLIENT", "KILL", NonEmpty::new("filters", filters)), + ) + .await + } + + #[doc = " The connections that match every one of the filters, all of them without filters, each a"] + #[doc = " map of its fields, e.g. `id`, `addr` and `name`."] + #[doc = ""] + #[doc = " CLIENT LIST [ filter [ filter ... ] ]"] + #[doc = " https://valkey.io/commands/client-list/"] + #[allow(async_fn_in_trait)] + async fn client_list( + connection: &Connection, + filters: Option>, + ) -> Result>, Error> { + let command = ("CLIENT", "LIST", NonEmpty::optional("filters", filters)); + let clients: ClientList = query(connection, command).await?; + Ok(clients.0) + } + + #[doc = " Whether the connection may be evicted to free memory, `ON` excludes it."] + #[doc = ""] + #[doc = " CLIENT NO-EVICT 〈 ON | OFF 〉"] + #[doc = " https://valkey.io/commands/client-no-evict/"] + #[allow(async_fn_in_trait)] + async fn client_no_evict(connection: &Connection, enabled: bool) -> Result<(), Error> { + query( + connection, + ("CLIENT", "NO-EVICT", if enabled { "ON" } else { "OFF" }), + ) + .await + } + + #[doc = " Suspends running the commands of other connections, writes only with `WRITE`, for the time."] + #[doc = ""] + #[doc = " CLIENT PAUSE timeout [ WRITE | ALL ]"] + #[doc = " https://valkey.io/commands/client-pause/"] + #[allow(async_fn_in_trait)] + async fn client_pause( + connection: &Connection, + timeout: DurationMilliseconds, + mode: Option, + ) -> Result<(), Error> { + query(connection, ("CLIENT", "PAUSE", timeout, mode)).await + } + + #[doc = " Unblocks the connection with the ID from a blocking command, as if it timed out, or with an"] + #[doc = " error, returning whether it was blocked."] + #[doc = ""] + #[doc = " CLIENT UNBLOCK client-id [ TIMEOUT | ERROR ]"] + #[doc = " https://valkey.io/commands/client-unblock/"] + #[allow(async_fn_in_trait)] + async fn client_unblock( + connection: &Connection, + client_id: Integer, + unblock_type: Option, + ) -> Result { + query(connection, ("CLIENT", "UNBLOCK", client_id, unblock_type)).await + } + + #[doc = " Resumes running the commands of connections paused with `client-pause`."] + #[doc = ""] + #[doc = " CLIENT UNPAUSE"] + #[doc = " https://valkey.io/commands/client-unpause/"] + #[allow(async_fn_in_trait)] + async fn client_unpause(connection: &Connection) -> Result<(), Error> { + query(connection, ("CLIENT", "UNPAUSE")).await + } + + #[doc = " Assigns the slots to the node."] + #[doc = ""] + #[doc = " CLUSTER ADDSLOTS slot [ slot ... ]"] + #[doc = " https://valkey.io/commands/cluster-addslots/"] + #[allow(async_fn_in_trait)] + async fn cluster_addslots(connection: &Connection, slots: Vec) -> Result<(), Error> { + query( + connection, + ("CLUSTER", "ADDSLOTS", NonEmpty::new("slots", slots)), + ) + .await + } + + #[doc = " Assigns the ranges of slots, start and end both included, to the node."] + #[doc = ""] + #[doc = " CLUSTER ADDSLOTSRANGE start-slot end-slot [ start-slot end-slot ... ]"] + #[doc = " https://valkey.io/commands/cluster-addslotsrange/"] + #[allow(async_fn_in_trait)] + async fn cluster_addslotsrange( + connection: &Connection, + ranges: Vec<(u16, u16)>, + ) -> Result<(), Error> { + query( + connection, + ("CLUSTER", "ADDSLOTSRANGE", NonEmpty::new("ranges", ranges)), + ) + .await + } + + #[doc = " Advances the node's config epoch, unless it is already the greatest in the cluster."] + #[doc = ""] + #[doc = " CLUSTER BUMPEPOCH"] + #[doc = " https://valkey.io/commands/cluster-bumpepoch/"] + #[allow(async_fn_in_trait)] + async fn cluster_bumpepoch(connection: &Connection) -> Result { + query(connection, ("CLUSTER", "BUMPEPOCH")).await + } + + #[doc = " Cancels the slot migrations in progress."] + #[doc = ""] + #[doc = " CLUSTER CANCELSLOTMIGRATIONS"] + #[doc = " https://valkey.io/commands/cluster-cancelslotmigrations/"] + #[allow(async_fn_in_trait)] + async fn cluster_cancelslotmigrations(connection: &Connection) -> Result<(), Error> { + query(connection, ("CLUSTER", "CANCELSLOTMIGRATIONS")).await + } + + #[doc = " The number of active failure reports for the node."] + #[doc = ""] + #[doc = " CLUSTER COUNT-FAILURE-REPORTS node-id"] + #[doc = " https://valkey.io/commands/cluster-count-failure-reports/"] + #[allow(async_fn_in_trait)] + async fn cluster_count_failure_reports( + connection: &Connection, + node_id: String, + ) -> Result { + query(connection, ("CLUSTER", "COUNT-FAILURE-REPORTS", node_id)).await + } + + #[doc = " Unassigns the slots from the node."] + #[doc = ""] + #[doc = " CLUSTER DELSLOTS slot [ slot ... ]"] + #[doc = " https://valkey.io/commands/cluster-delslots/"] + #[allow(async_fn_in_trait)] + async fn cluster_delslots(connection: &Connection, slots: Vec) -> Result<(), Error> { + query( + connection, + ("CLUSTER", "DELSLOTS", NonEmpty::new("slots", slots)), + ) + .await + } + + #[doc = " Unassigns the ranges of slots, start and end both included, from the node."] + #[doc = ""] + #[doc = " CLUSTER DELSLOTSRANGE start-slot end-slot [ start-slot end-slot ... ]"] + #[doc = " https://valkey.io/commands/cluster-delslotsrange/"] + #[allow(async_fn_in_trait)] + async fn cluster_delslotsrange( + connection: &Connection, + ranges: Vec<(u16, u16)>, + ) -> Result<(), Error> { + query( + connection, + ("CLUSTER", "DELSLOTSRANGE", NonEmpty::new("ranges", ranges)), + ) + .await + } + + #[doc = " Fails over the primary of this replica, coordinated with it unless `FORCE` or `TAKEOVER`."] + #[doc = ""] + #[doc = " CLUSTER FAILOVER [ FORCE | TAKEOVER ]"] + #[doc = " https://valkey.io/commands/cluster-failover/"] + #[allow(async_fn_in_trait)] + async fn cluster_failover( + connection: &Connection, + mode: Option, + ) -> Result<(), Error> { + query(connection, ("CLUSTER", "FAILOVER", mode)).await + } + + #[doc = " Unassigns all the slots of the node."] + #[doc = ""] + #[doc = " CLUSTER FLUSHSLOTS"] + #[doc = " https://valkey.io/commands/cluster-flushslots/"] + #[allow(async_fn_in_trait)] + async fn cluster_flushslots(connection: &Connection) -> Result<(), Error> { + query(connection, ("CLUSTER", "FLUSHSLOTS")).await + } + + #[doc = " Removes the node from the nodes this node knows of."] + #[doc = ""] + #[doc = " CLUSTER FORGET node-id"] + #[doc = " https://valkey.io/commands/cluster-forget/"] + #[allow(async_fn_in_trait)] + async fn cluster_forget(connection: &Connection, node_id: String) -> Result<(), Error> { + query(connection, ("CLUSTER", "FORGET", node_id)).await + } + + #[doc = " The slot migrations in progress, and those recently finished."] + #[doc = ""] + #[doc = " CLUSTER GETSLOTMIGRATIONS"] + #[doc = " https://valkey.io/commands/cluster-getslotmigrations/"] + #[allow(async_fn_in_trait)] + async fn cluster_getslotmigrations( + connection: &Connection, + ) -> Result, Error> { + query(connection, ("CLUSTER", "GETSLOTMIGRATIONS")).await + } + + #[doc = " Connects the node to another, adding it to the cluster."] + #[doc = ""] + #[doc = " CLUSTER MEET ip port [ cluster-bus-port ]"] + #[doc = " https://valkey.io/commands/cluster-meet/"] + #[allow(async_fn_in_trait)] + async fn cluster_meet( + connection: &Connection, + ip: String, + port: u16, + cluster_bus_port: Option, + ) -> Result<(), Error> { + query(connection, ("CLUSTER", "MEET", ip, port, cluster_bus_port)).await + } + + #[doc = " Migrates the ranges of slots from this node to other nodes."] + #[doc = ""] + #[doc = " CLUSTER MIGRATESLOTS SLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] NODE node-id [ AUTH username password ] [ SLOTSRANGE ... ]"] + #[doc = " https://valkey.io/commands/cluster-migrateslots/"] + #[allow(async_fn_in_trait)] + async fn cluster_migrateslots( + connection: &Connection, + migrations: Vec, + ) -> Result<(), Error> { + query( + connection, + ( + "CLUSTER", + "MIGRATESLOTS", + NonEmpty::new("migrations", migrations), + ), + ) + .await + } + + #[doc = " The replicas of the primary node, each a line in the format of CLUSTER NODES."] + #[doc = ""] + #[doc = " CLUSTER REPLICAS node-id"] + #[doc = " https://valkey.io/commands/cluster-replicas/"] + #[allow(async_fn_in_trait)] + async fn cluster_replicas( + connection: &Connection, + node_id: String, + ) -> Result, Error> { + query(connection, ("CLUSTER", "REPLICAS", node_id)).await + } + + #[doc = " Makes the node a replica of the primary node, or with none, detaches it from its primary,"] + #[doc = " `NO ONE`."] + #[doc = ""] + #[doc = " CLUSTER REPLICATE 〈 node-id | NO ONE 〉"] + #[doc = " https://valkey.io/commands/cluster-replicate/"] + #[allow(async_fn_in_trait)] + async fn cluster_replicate( + connection: &Connection, + node_id: Option, + ) -> Result<(), Error> { + // NO ONE detaches the replica from its primary + let node = match node_id { + Some(node_id) => vec![node_id], + None => vec!["NO".to_string(), "ONE".to_string()], + }; + query(connection, ("CLUSTER", "REPLICATE", node)).await + } + + #[doc = " Resets the node, forgetting the other nodes and its slots, `SOFT` without a mode."] + #[doc = ""] + #[doc = " CLUSTER RESET [ HARD | SOFT ]"] + #[doc = " https://valkey.io/commands/cluster-reset/"] + #[allow(async_fn_in_trait)] + async fn cluster_reset( + connection: &Connection, + mode: Option, + ) -> Result<(), Error> { + query(connection, ("CLUSTER", "RESET", mode)).await + } + + #[doc = " Saves the node's cluster configuration to disk."] + #[doc = ""] + #[doc = " CLUSTER SAVECONFIG"] + #[doc = " https://valkey.io/commands/cluster-saveconfig/"] + #[allow(async_fn_in_trait)] + async fn cluster_saveconfig(connection: &Connection) -> Result<(), Error> { + query(connection, ("CLUSTER", "SAVECONFIG")).await + } + + #[doc = " Sets the config epoch of a new node."] + #[doc = ""] + #[doc = " CLUSTER SET-CONFIG-EPOCH config-epoch"] + #[doc = " https://valkey.io/commands/cluster-set-config-epoch/"] + #[allow(async_fn_in_trait)] + async fn cluster_set_config_epoch( + connection: &Connection, + config_epoch: Integer, + ) -> Result<(), Error> { + query(connection, ("CLUSTER", "SET-CONFIG-EPOCH", config_epoch)).await + } + + #[doc = " Changes the state of the slot on the node."] + #[doc = ""] + #[doc = " CLUSTER SETSLOT slot 〈 IMPORTING node-id | MIGRATING node-id | NODE node-id | STABLE 〉 [ TIMEOUT timeout ]"] + #[doc = " https://valkey.io/commands/cluster-setslot/"] + #[allow(async_fn_in_trait)] + async fn cluster_setslot( + connection: &Connection, + slot: u16, + state: ClusterSetslotState, + timeout: Option, + ) -> Result<(), Error> { + query( + connection, + ( + "CLUSTER", + "SETSLOT", + slot, + state, + NamedOption("TIMEOUT", timeout), + ), + ) + .await + } + + #[doc = " The entries of the command log of the type, up to the count, all of them for -1."] + #[doc = ""] + #[doc = " COMMANDLOG GET count 〈 slow | large-request | large-reply 〉"] + #[doc = " https://valkey.io/commands/commandlog-get/"] + #[allow(async_fn_in_trait)] + async fn commandlog_get( + connection: &Connection, + count: Integer, + type_: CommandlogType, + ) -> Result, Error> { + query(connection, ("COMMANDLOG", "GET", count, type_)).await + } + + #[doc = " The number of entries in the command log of the type."] + #[doc = ""] + #[doc = " COMMANDLOG LEN 〈 slow | large-request | large-reply 〉"] + #[doc = " https://valkey.io/commands/commandlog-len/"] + #[allow(async_fn_in_trait)] + async fn commandlog_len( + connection: &Connection, + type_: CommandlogType, + ) -> Result { + query(connection, ("COMMANDLOG", "LEN", type_)).await + } + + #[doc = " Clears the command log of the type."] + #[doc = ""] + #[doc = " COMMANDLOG RESET 〈 slow | large-request | large-reply 〉"] + #[doc = " https://valkey.io/commands/commandlog-reset/"] + #[allow(async_fn_in_trait)] + async fn commandlog_reset(connection: &Connection, type_: CommandlogType) -> Result<(), Error> { + query(connection, ("COMMANDLOG", "RESET", type_)).await + } + + #[doc = " The values of the configuration parameters matching the glob-style patterns, by name."] + #[doc = ""] + #[doc = " CONFIG GET parameter [ parameter ... ]"] + #[doc = " https://valkey.io/commands/config-get/"] + #[allow(async_fn_in_trait)] + async fn config_get( + connection: &Connection, + parameters: Vec, + ) -> Result, Error> { + query( + connection, + ("CONFIG", "GET", NonEmpty::new("parameters", parameters)), + ) + .await + } + + #[doc = " Resets the server's statistics, e.g. those of INFO."] + #[doc = ""] + #[doc = " CONFIG RESETSTAT"] + #[doc = " https://valkey.io/commands/config-resetstat/"] + #[allow(async_fn_in_trait)] + async fn config_resetstat(connection: &Connection) -> Result<(), Error> { + query(connection, ("CONFIG", "RESETSTAT")).await + } + + #[doc = " Rewrites the configuration file with the current configuration."] + #[doc = ""] + #[doc = " CONFIG REWRITE"] + #[doc = " https://valkey.io/commands/config-rewrite/"] + #[allow(async_fn_in_trait)] + async fn config_rewrite(connection: &Connection) -> Result<(), Error> { + query(connection, ("CONFIG", "REWRITE")).await + } + + #[doc = " Sets the configuration parameters to the values, all or none of them."] + #[doc = ""] + #[doc = " CONFIG SET parameter value [ parameter value ... ]"] + #[doc = " https://valkey.io/commands/config-set/"] + #[allow(async_fn_in_trait)] + async fn config_set( + connection: &Connection, + parameters_and_values: Vec<(String, String)>, + ) -> Result<(), Error> { + let parameters_and_values = NonEmpty::new("parameters-and-values", parameters_and_values); + query(connection, ("CONFIG", "SET", parameters_and_values)).await + } + + #[doc = " Fails over from this primary to a replica, the one at the target, or aborts a failover in"] + #[doc = " progress."] + #[doc = ""] + #[doc = " FAILOVER [ TO host port [ FORCE ] ] [ ABORT ] [ TIMEOUT milliseconds ]"] + #[doc = " https://valkey.io/commands/failover/"] + #[allow(async_fn_in_trait)] + async fn failover( + connection: &Connection, + target: Option, + abort: bool, + timeout: Option, + ) -> Result<(), Error> { + let command = ( + "FAILOVER", + target, + Flag("ABORT", abort), + NamedOption("TIMEOUT", timeout), + ); + query(connection, command).await + } + + #[doc = " When the databases were last saved to disk."] + #[doc = ""] + #[doc = " LASTSAVE"] + #[doc = " https://valkey.io/commands/lastsave/"] + #[allow(async_fn_in_trait)] + async fn lastsave(connection: &Connection) -> Result { + query(connection, "LASTSAVE").await + } + + #[doc = " A report of the latency events, for people to read."] + #[doc = ""] + #[doc = " LATENCY DOCTOR"] + #[doc = " https://valkey.io/commands/latency-doctor/"] + #[allow(async_fn_in_trait)] + async fn latency_doctor(connection: &Connection) -> Result { + query(connection, ("LATENCY", "DOCTOR")).await + } + + #[doc = " A graph of the latency of the event, as text."] + #[doc = ""] + #[doc = " LATENCY GRAPH event"] + #[doc = " https://valkey.io/commands/latency-graph/"] + #[allow(async_fn_in_trait)] + async fn latency_graph(connection: &Connection, event: String) -> Result { + query(connection, ("LATENCY", "GRAPH", event)).await + } + + #[doc = " The latency histogram of the commands, all of them without commands, by command name."] + #[doc = ""] + #[doc = " LATENCY HISTOGRAM [ command [ command ... ] ]"] + #[doc = " https://valkey.io/commands/latency-histogram/"] + #[allow(async_fn_in_trait)] + async fn latency_histogram( + connection: &Connection, + commands: Option>, + ) -> Result, Error> { + query( + connection, + ( + "LATENCY", + "HISTOGRAM", + NonEmpty::optional("commands", commands), + ), + ) + .await + } + + #[doc = " The samples of the latency of the event, when each was taken and the latency."] + #[doc = ""] + #[doc = " LATENCY HISTORY event"] + #[doc = " https://valkey.io/commands/latency-history/"] + #[allow(async_fn_in_trait)] + async fn latency_history( + connection: &Connection, + event: String, + ) -> Result, Error> { + query(connection, ("LATENCY", "HISTORY", event)).await + } + + #[doc = " The latest sample of the latency of each event."] + #[doc = ""] + #[doc = " LATENCY LATEST"] + #[doc = " https://valkey.io/commands/latency-latest/"] + #[allow(async_fn_in_trait)] + async fn latency_latest(connection: &Connection) -> Result, Error> { + query(connection, ("LATENCY", "LATEST")).await + } + + #[doc = " Clears the samples of the events, all of them without events, returning how many events."] + #[doc = ""] + #[doc = " LATENCY RESET [ event [ event ... ] ]"] + #[doc = " https://valkey.io/commands/latency-reset/"] + #[allow(async_fn_in_trait)] + async fn latency_reset( + connection: &Connection, + events: Option>, + ) -> Result { + query( + connection, + ("LATENCY", "RESET", NonEmpty::optional("events", events)), + ) + .await + } + + #[doc = " The loaded modules."] + #[doc = ""] + #[doc = " MODULE LIST"] + #[doc = " https://valkey.io/commands/module-list/"] + #[allow(async_fn_in_trait)] + async fn module_list(connection: &Connection) -> Result, Error> { + query(connection, ("MODULE", "LIST")).await + } + + #[doc = " Loads the module at the path, with the arguments."] + #[doc = ""] + #[doc = " MODULE LOAD path [ arg [ arg ... ] ]"] + #[doc = " https://valkey.io/commands/module-load/"] + #[allow(async_fn_in_trait)] + async fn module_load( + connection: &Connection, + path: String, + args: Option>, + ) -> Result<(), Error> { + query( + connection, + ("MODULE", "LOAD", path, NonEmpty::optional("args", args)), + ) + .await + } + + #[doc = " Loads the module at the path, with the configuration parameters and arguments."] + #[doc = ""] + #[doc = " MODULE LOADEX path [ CONFIG name value [ CONFIG name value ... ] ] [ ARGS args [ args ... ] ]"] + #[doc = " https://valkey.io/commands/module-loadex/"] + #[allow(async_fn_in_trait)] + async fn module_loadex( + connection: &Connection, + path: String, + configs: Option>, + args: Option>, + ) -> Result<(), Error> { + // CONFIG before each name and value, ARGS once before the arguments + let configs = configs.map(|configs| { + let configs = configs + .into_iter() + .map(|(name, value)| ("CONFIG", name, value)); + configs.collect::>() + }); + let configs = NonEmpty::optional("configs", configs); + let args = NonEmpty::optional("args", args).map(|args| ("ARGS", args)); + query(connection, ("MODULE", "LOADEX", path, configs, args)).await + } + + #[doc = " Unloads the module, by name."] + #[doc = ""] + #[doc = " MODULE UNLOAD name"] + #[doc = " https://valkey.io/commands/module-unload/"] + #[allow(async_fn_in_trait)] + async fn module_unload(connection: &Connection, name: String) -> Result<(), Error> { + query(connection, ("MODULE", "UNLOAD", name)).await + } + + #[doc = " Makes the server a replica of the primary at the host and port, or with none, a primary,"] + #[doc = " `NO ONE`, returning the server's message."] + #[doc = ""] + #[doc = " REPLICAOF 〈 host port | NO ONE 〉"] + #[doc = " https://valkey.io/commands/replicaof/"] + #[allow(async_fn_in_trait)] + async fn replicaof( + connection: &Connection, + primary: Option<(String, u16)>, + ) -> Result { + // NO ONE makes the server a primary + let primary = match primary { + Some((host, port)) => vec![host, port.to_string()], + None => vec!["NO".to_string(), "ONE".to_string()], + }; + query(connection, ("REPLICAOF", primary)).await + } + + #[doc = " The server's role in replication, and its state."] + #[doc = ""] + #[doc = " ROLE"] + #[doc = " https://valkey.io/commands/role/"] + #[allow(async_fn_in_trait)] + async fn role(connection: &Connection) -> Result { + query(connection, "ROLE").await + } + + #[doc = " Saves the databases to disk, blocking the server until done."] + #[doc = ""] + #[doc = " SAVE"] + #[doc = " https://valkey.io/commands/save/"] + #[allow(async_fn_in_trait)] + async fn save(connection: &Connection) -> Result<(), Error> { + query(connection, "SAVE").await + } + + #[doc = " Stops the server, saving to disk as configured, or as the save mode says."] + #[doc = ""] + #[doc = " The server closes the connection once it stops, so a shutdown that succeeds fails the call"] + #[doc = " with a closed error."] + #[doc = ""] + #[doc = " See also `shutdown-abort`."] + #[doc = ""] + #[doc = " SHUTDOWN [ NOSAVE | SAVE ] [ NOW ] [ FORCE ] [ SAFE ] [ FAILOVER ]"] + #[doc = " https://valkey.io/commands/shutdown/"] + #[allow(async_fn_in_trait)] + async fn shutdown( + connection: &Connection, + save: Option, + now: bool, + force: bool, + safe: bool, + failover: bool, + ) -> Result<(), Error> { + let command = ( + "SHUTDOWN", + save, + Flag("NOW", now), + Flag("FORCE", force), + Flag("SAFE", safe), + Flag("FAILOVER", failover), + ); + query(connection, command).await + } + + #[doc = " Aborts a shutdown in progress."] + #[doc = ""] + #[doc = " See also `shutdown`."] + #[doc = ""] + #[doc = " SHUTDOWN ABORT"] + #[doc = " https://valkey.io/commands/shutdown/"] + #[allow(async_fn_in_trait)] + async fn shutdown_abort(connection: &Connection) -> Result<(), Error> { + query(connection, ("SHUTDOWN", "ABORT")).await + } + + #[doc = " The entries of the slow log, up to the count, 10 without one, all of them for -1."] + #[doc = ""] + #[doc = " SLOWLOG GET [ count ]"] + #[doc = " https://valkey.io/commands/slowlog-get/"] + #[allow(async_fn_in_trait)] + async fn slowlog_get( + connection: &Connection, + count: Option, + ) -> Result, Error> { + query(connection, ("SLOWLOG", "GET", count)).await + } + + #[doc = " The number of entries in the slow log."] + #[doc = ""] + #[doc = " SLOWLOG LEN"] + #[doc = " https://valkey.io/commands/slowlog-len/"] + #[allow(async_fn_in_trait)] + async fn slowlog_len(connection: &Connection) -> Result { + query(connection, ("SLOWLOG", "LEN")).await + } + + #[doc = " Clears the slow log."] + #[doc = ""] + #[doc = " SLOWLOG RESET"] + #[doc = " https://valkey.io/commands/slowlog-reset/"] + #[allow(async_fn_in_trait)] + async fn slowlog_reset(connection: &Connection) -> Result<(), Error> { + query(connection, ("SLOWLOG", "RESET")).await + } + #[doc = " The number of bits set to 1 in the string stored at key, in the range, 0 when the key does"] #[doc = " not exist."] #[doc = ""] diff --git a/components/commands/src/value.rs b/components/commands/src/value.rs index e2c7947..bcd7ac8 100644 --- a/components/commands/src/value.rs +++ b/components/commands/src/value.rs @@ -1,11 +1,14 @@ use crate::exports::componentized::valkey::commands::{ - Cursor, Double, DurationMilliseconds, DurationSeconds, EngineStats, Error, ExpiretimeReply, - FieldPersistResult, FunctionStatsReply, GeoCoordinates, GeoSearchResult, Integer, KeyType, - LcsIdxMatch, LcsIdxReply, Library, LibraryFunction, LinsertResult, PexpiretimeReply, PttlReply, - RunningFunction, StreamId, TtlReply, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, - XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, - XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, XpendingExtendedEntry, - XpendingSummary, + AclDryrunResult, AclListEntry, AclLogEntry, ChannelRule, ClusterBumpepochResult, CommandRule, + Cursor, DbRule, Double, DurationMilliseconds, DurationSeconds, EngineStats, Error, + ExpiretimeReply, FieldPersistResult, FunctionStatsReply, GeoCoordinates, GeoSearchResult, + Integer, KeyRule, KeyType, LatencyHistogramEntry, LatencyLatestEntry, LcsIdxMatch, LcsIdxReply, + Library, LibraryFunction, LinsertResult, LogEntry, ModuleInfo, PexpiretimeReply, PttlReply, + RolePrimary, RoleReplica, RoleReplicaInfo, RoleReply, Rule, RuleSelector, RunningFunction, + SlotMigration, SlotMigrationOperation, StreamId, TtlReply, TtlResult, UnixTimeMilliseconds, + UnixTimeSeconds, UserRule, Value, XinfoConsumer, XinfoGroup, XinfoStreamReply, + XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, + XpendingExtendedEntry, XpendingSummary, }; use resp_codec::NestedValue; @@ -481,6 +484,454 @@ impl FromValue for Migrated { } } +/// `OK` when the user may run the command, otherwise why not. +impl FromValue for AclDryrunResult { + fn from_value(value: Value) -> Result { + match value { + Value::String(reply) if reply == "OK" => Ok(AclDryrunResult::Allowed), + value => String::from_value(value).map(AclDryrunResult::Denied), + } + } +} + +/// The rules of ACL GETUSER: the flags, the hashed passwords, the root selector's rules, then the +/// selectors. +pub struct AclUserRules(pub Vec); + +impl FromValue for AclUserRules { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + let flags: Vec = fields.required("flags")?; + let passwords: Vec = fields.required("passwords")?; + let mut rules: Vec = flags.iter().map(|flag| rule(flag)).collect(); + rules.extend( + passwords + .into_iter() + .map(|hash| Rule::User(UserRule::HashedPassword(hash))), + ); + rules.extend(selector_rules(&mut fields)?.into_iter().map(root_rule)); + let selectors: Vec = fields.required("selectors")?; + for mut selector in selectors { + let selector = selector_rules(&mut selector)?; + rules.push(Rule::User(UserRule::Selector(selector))); + } + Ok(AclUserRules(rules)) + } +} + +/// The rules of a selector, from its `keys`, `channels`, `databases` and `commands` fields, each +/// rules separated by spaces. +/// +/// The channels are preceded by `resetchannels`, as ACL LIST writes them, so the server's +/// `acl-pubsub-default` does not add to them. +fn selector_rules(fields: &mut Fields) -> Result, Error> { + let keys: String = fields.required("keys")?; + let channels: String = fields.required("channels")?; + // since Valkey 9.1 + let databases: Option = fields.optional("databases")?; + let commands: String = fields.required("commands")?; + let mut rules: Vec = keys.split_whitespace().map(key_rule).collect(); + rules.push(RuleSelector::Channel(ChannelRule::Reset)); + rules.extend(channels.split_whitespace().map(channel_rule)); + rules.extend( + databases + .iter() + .flat_map(|databases| databases.split_whitespace().map(db_rule)), + ); + rules.extend(commands.split_whitespace().map(command_rule)); + Ok(rules) +} + +fn key_rule(rule: &str) -> RuleSelector { + let key_rule = match rule { + "allkeys" => KeyRule::All, + "resetkeys" => KeyRule::Reset, + _ => { + if let Some(pattern) = rule.strip_prefix("%RW~") { + KeyRule::AddReadWrite(pattern.to_string()) + } else if let Some(pattern) = rule.strip_prefix("%R~") { + KeyRule::AddRead(pattern.to_string()) + } else if let Some(pattern) = rule.strip_prefix("%W~") { + KeyRule::AddWrite(pattern.to_string()) + } else if let Some(pattern) = rule.strip_prefix('~') { + KeyRule::Add(pattern.to_string()) + } else { + return RuleSelector::Other(rule.to_string()); + } + } + }; + RuleSelector::Keys(key_rule) +} + +fn channel_rule(rule: &str) -> RuleSelector { + let channel_rule = match rule { + "allchannels" => ChannelRule::All, + "resetchannels" => ChannelRule::Reset, + _ => match rule.strip_prefix('&') { + Some(pattern) => ChannelRule::Add(pattern.to_string()), + None => return RuleSelector::Other(rule.to_string()), + }, + }; + RuleSelector::Channel(channel_rule) +} + +fn db_rule(rule: &str) -> RuleSelector { + let db_rule = match rule { + "alldbs" => DbRule::All, + "resetdbs" => DbRule::Reset, + _ => { + let dbs = rule + .strip_prefix("db=") + .map(|dbs| dbs.split(',').map(str::parse).collect::>()); + match dbs { + Some(Ok(dbs)) => DbRule::Db(dbs), + _ => return RuleSelector::Other(rule.to_string()), + } + } + }; + RuleSelector::Db(db_rule) +} + +/// `+config|get` is a subcommand, or a first argument, either way it is sent back as is. +fn command_rule(rule: &str) -> RuleSelector { + let command_rule = match rule { + "allcommands" | "+@all" => CommandRule::All, + "nocommands" | "-@all" => CommandRule::Reset, + _ => { + if let Some(category) = rule.strip_prefix("+@") { + CommandRule::AddCategory(category.to_string()) + } else if let Some(category) = rule.strip_prefix("-@") { + CommandRule::RemoveCategory(category.to_string()) + } else if let Some(command) = rule.strip_prefix('+') { + CommandRule::Add(command.to_string()) + } else if let Some(command) = rule.strip_prefix('-') { + CommandRule::Remove(command.to_string()) + } else { + return RuleSelector::Other(rule.to_string()); + } + } + }; + RuleSelector::Command(command_rule) +} + +/// The lines of ACL LIST, each `user ...`, with selectors as `( ...)`. +pub struct AclList(pub Vec); + +impl FromValue for AclList { + fn from_value(value: Value) -> Result { + let lines: Vec = FromValue::from_value(value)?; + lines + .iter() + .map(|line| acl_list_entry(line)) + .collect::>() + .map(AclList) + } +} + +fn acl_list_entry(line: &str) -> Result { + let invalid = || Error::Client(format!("unexpected ACL LIST line: {line:?}")); + let mut words = line + .strip_prefix("user ") + .ok_or_else(invalid)? + .split_whitespace(); + let username = words.next().ok_or_else(invalid)?.to_string(); + let mut rules = vec![]; + while let Some(word) = words.next() { + if !word.starts_with('(') { + rules.push(rule(word)); + continue; + } + // a selector, its rules up to the word that closes it + let mut selector = vec![word]; + while !selector.last().is_some_and(|word| word.ends_with(')')) { + selector.push(words.next().ok_or_else(invalid)?); + } + let selector = selector.join(" "); + let selector = selector + .strip_prefix('(') + .and_then(|selector| selector.strip_suffix(')')) + .ok_or_else(invalid)?; + let selector = selector.split_whitespace().map(selector_rule).collect(); + rules.push(Rule::User(UserRule::Selector(selector))); + } + Ok(AclListEntry { username, rules }) +} + +/// A rule of the user or its root selector, e.g. `on`, `#` or `~app:*`. +fn rule(word: &str) -> Rule { + let user_rule = match word { + "on" => UserRule::On, + "off" => UserRule::Off, + "nopass" => UserRule::Nopass, + "sanitize-payload" => UserRule::SanitizePayload, + "skip-sanitize-payload" => UserRule::SkipSanitizePayload, + "clearselectors" => UserRule::Clearselectors, + "reset" => UserRule::Reset, + _ => { + if let Some(hash) = word.strip_prefix('#') { + UserRule::HashedPassword(hash.to_string()) + } else if let Some(hash) = word.strip_prefix('!') { + UserRule::RemoveHashedPassword(hash.to_string()) + } else if let Some(password) = word.strip_prefix('>') { + UserRule::Password(password.to_string()) + } else if let Some(password) = word.strip_prefix('<') { + UserRule::RemovePassword(password.to_string()) + } else { + return root_rule(selector_rule(word)); + } + } + }; + Rule::User(user_rule) +} + +/// A rule of a selector, e.g. `~app:*`, `&news:*`, `db=0` or `+get`. +fn selector_rule(word: &str) -> RuleSelector { + match word { + "allkeys" | "resetkeys" => key_rule(word), + "allchannels" | "resetchannels" => channel_rule(word), + "alldbs" | "resetdbs" => db_rule(word), + "allcommands" | "nocommands" => command_rule(word), + _ if word.starts_with('~') || word.starts_with('%') => key_rule(word), + _ if word.starts_with('&') => channel_rule(word), + _ if word.starts_with("db=") => db_rule(word), + _ if word.starts_with('+') || word.starts_with('-') => command_rule(word), + _ => RuleSelector::Other(word.to_string()), + } +} + +/// A rule of the root selector. +fn root_rule(rule: RuleSelector) -> Rule { + match rule { + RuleSelector::Keys(rule) => Rule::Keys(rule), + RuleSelector::Channel(rule) => Rule::Channel(rule), + RuleSelector::Command(rule) => Rule::Command(rule), + RuleSelector::Db(rule) => Rule::Db(rule), + RuleSelector::Other(rule) => Rule::Other(rule), + } +} + +impl FromValue for AclLogEntry { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + count: fields.required("count")?, + reason: fields.required("reason")?, + context: fields.required("context")?, + object: fields.required("object")?, + username: fields.required("username")?, + age_seconds: fields.required("age-seconds")?, + client_info: fields.required("client-info")?, + // since Valkey 7.2 + entry_id: fields.optional("entry-id")?, + timestamp_created: fields.optional("timestamp-created")?, + timestamp_last_updated: fields.optional("timestamp-last-updated")?, + }) + } +} + +/// `BUMPED epoch` or `STILL epoch`. +impl FromValue for ClusterBumpepochResult { + fn from_value(value: Value) -> Result { + let reply = String::from_value(value)?; + let invalid = || Error::Client(format!("unexpected reply: {reply:?}")); + let (outcome, epoch) = reply.split_once(' ').ok_or_else(invalid)?; + let epoch = epoch.parse().map_err(|_| invalid())?; + match outcome { + "BUMPED" => Ok(ClusterBumpepochResult::Bumped(epoch)), + "STILL" => Ok(ClusterBumpepochResult::Still(epoch)), + _ => Err(invalid()), + } + } +} + +impl FromValue for SlotMigrationOperation { + fn from_value(value: Value) -> Result { + match String::from_value(value)?.as_str() { + "IMPORT" => Ok(SlotMigrationOperation::Import), + "EXPORT" => Ok(SlotMigrationOperation::Export), + operation => Err(Error::Client(format!( + "unexpected operation: {operation:?}" + ))), + } + } +} + +/// Ranges of slots, `start-end start-end ...`. +pub struct SlotRanges(pub Vec<(u16, u16)>); + +impl FromValue for SlotRanges { + fn from_value(value: Value) -> Result { + let ranges = String::from_value(value)?; + let invalid = || Error::Client(format!("unexpected slot ranges: {ranges:?}")); + ranges + .split_whitespace() + .map(|range| { + let (start, end) = range.split_once('-').ok_or_else(invalid)?; + Ok(( + start.parse().map_err(|_| invalid())?, + end.parse().map_err(|_| invalid())?, + )) + }) + .collect::>() + .map(SlotRanges) + } +} + +impl FromValue for SlotMigration { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + let slot_ranges: SlotRanges = fields.required("slot_ranges")?; + Ok(Self { + link_name: fields.required("link_name")?, + operation: fields.required("operation")?, + slot_ranges: slot_ranges.0, + target_node: fields.required("target_node")?, + source_node: fields.required("source_node")?, + create_time: fields.required("create_time")?, + last_update_time: fields.required("last_update_time")?, + last_ack_time: fields.required("last_ack_time")?, + state: fields.required("state")?, + message: fields.required("message")?, + cow_size: fields.required("cow_size")?, + }) + } +} + +/// `[id, timestamp, value, [argument ...], client-address, client-name]`, of SLOWLOG GET and +/// COMMANDLOG GET. +impl FromValue for LogEntry { + fn from_value(value: Value) -> Result { + match <[Value; 6]>::try_from(items(value)?) { + Ok([id, timestamp, value, arguments, client_address, client_name]) => Ok(Self { + id: FromValue::from_value(id)?, + timestamp: FromValue::from_value(timestamp)?, + value: FromValue::from_value(value)?, + arguments: FromValue::from_value(arguments)?, + client_address: FromValue::from_value(client_address)?, + client_name: FromValue::from_value(client_name)?, + }), + Err(items) => Err(Error::Client(format!( + "unexpected number of items, expected 6: {}", + items.len() + ))), + } + } +} + +impl FromValue for LatencyHistogramEntry { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + // a map of each bucket to its cumulative count + let histogram: FlatPairs = fields.required("histogram_usec")?; + Ok(Self { + calls: fields.required("calls")?, + histogram_usec: histogram.0, + }) + } +} + +/// `[event, timestamp, latest, max, sum, count]` +impl FromValue for LatencyLatestEntry { + fn from_value(value: Value) -> Result { + match <[Value; 6]>::try_from(items(value)?) { + Ok([event, timestamp, latest, max, sum, count]) => Ok(Self { + event: FromValue::from_value(event)?, + timestamp: FromValue::from_value(timestamp)?, + latest: FromValue::from_value(latest)?, + max: FromValue::from_value(max)?, + sum: FromValue::from_value(sum)?, + count: FromValue::from_value(count)?, + }), + Err(items) => Err(Error::Client(format!( + "unexpected number of items, expected 6: {}", + items.len() + ))), + } + } +} + +impl FromValue for ModuleInfo { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + name: fields.required("name")?, + ver: fields.required("ver")?, + path: fields.required("path")?, + args: fields.required("args")?, + }) + } +} + +/// A number sent as a string, e.g. the port of a replica in ROLE. +fn parse_string(value: Value) -> Result { + let text = String::from_value(value)?; + text.parse() + .map_err(|_| Error::Client(format!("unexpected number: {text:?}"))) +} + +/// `[ip, port, offset]`, the port and offset are strings. +impl FromValue for RoleReplicaInfo { + fn from_value(value: Value) -> Result { + let (ip, port, offset) = FromValue::from_value(value)?; + Ok(Self { + ip, + port: parse_string(port)?, + offset: parse_string(offset)?, + }) + } +} + +/// `["master", offset, [replica ...]]`, `["slave", host, port, state, offset]` or +/// `["sentinel", [primary ...]]`. +impl FromValue for RoleReply { + fn from_value(value: Value) -> Result { + let mut items = items(value)?.into_iter(); + let role = String::from_value(items.next().unwrap_or(Value::Null))?; + let mut next = || { + items + .next() + .ok_or_else(|| Error::Client(format!("too few items for the role {role:?}"))) + }; + match role.as_str() { + "master" => Ok(RoleReply::Primary(RolePrimary { + offset: FromValue::from_value(next()?)?, + replicas: FromValue::from_value(next()?)?, + })), + "slave" => Ok(RoleReply::Replica(RoleReplica { + primary_host: FromValue::from_value(next()?)?, + primary_port: u64::from_value(next()?)? + .try_into() + .map_err(|_| Error::Client("unexpected port".to_string()))?, + state: FromValue::from_value(next()?)?, + offset: FromValue::from_value(next()?)?, + })), + "sentinel" => Ok(RoleReply::Sentinel(FromValue::from_value(next()?)?)), + role => Err(Error::Client(format!("unexpected role: {role:?}"))), + } + } +} + +/// The lines of CLIENT LIST, each of the fields of a connection, `name=value` separated by spaces. +#[derive(Debug)] +pub struct ClientList(pub Vec>); + +impl FromValue for ClientList { + fn from_value(value: Value) -> Result { + let text = String::from_value(value)?; + let clients = text + .lines() + .filter(|line| !line.is_empty()) + .map(|line| { + line.split(' ') + .filter_map(|field| field.split_once('=')) + .map(|(name, value)| (name.to_string(), value.to_string())) + .collect() + }) + .collect(); + Ok(ClientList(clients)) + } +} + impl FromValue for TtlResult { fn from_value(value: Value) -> Result { match Integer::from_value(value)? { diff --git a/wit/commands.wit b/wit/commands.wit index d51fa64..19cbf2e 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -111,18 +111,396 @@ interface commands { // admin commands + /// Whether a user may run a command, see `acl-dryrun`. + variant acl-dryrun-result { + /// The user may run the command. + allowed, + /// The user may not run the command, and why. + denied(string), + } + + /// A security event, a command or authentication the rules denied, see `acl-log`. + record acl-log-entry { + /// The number of events like this one, within 60 seconds of each other. + count: integer, + /// What was denied, e.g. `command`, `key`, `channel` or `auth`. + reason: string, + /// Where it was denied, e.g. `toplevel`, `multi` or `lua`. + context: string, + /// The command, key or channel that was denied. + object: string, + username: string, + /// The seconds since the latest of these events. + age-seconds: double, + /// The connection, in the format of CLIENT LIST. + client-info: string, + /// The ID of the entry, since Valkey 7.2. + entry-id: option, + /// When the first of these events happened, since Valkey 7.2. + timestamp-created: option, + /// When the latest of these events happened, since Valkey 7.2. + timestamp-last-updated: option, + } + + /// What BGSAVE does. + enum bgsave-operation { + /// `SCHEDULE`: saves once the rewrite of the append-only file in progress is done. + schedule, + /// `CANCEL`: cancels the save in progress, or scheduled. + cancel, + } + + /// The type of a connection, for the `client-filter`. + enum client-type { + /// `normal` + normal, + /// `master`: the connection to the primary of a replica. + primary, + /// `replica` + replica, + /// `pubsub` + pubsub, + } + + /// A filter of connections, for CLIENT KILL and CLIENT LIST. + variant client-filter { + /// `ID client-id [ client-id ... ]` + id(list), + /// `TYPE` + %type(client-type), + /// `USER username` + user(string), + /// `ADDR ip:port`: the address of the client. + addr(string), + /// `LADDR ip:port`: the address of the server the client connected to. + laddr(string), + /// `SKIPME YES | NO`: whether to skip this connection, yes by default for CLIENT KILL. + skipme(bool), + /// `MAXAGE`: connections older than the time. + maxage(duration-seconds), + /// `NAME` + name(string), + /// `IDLE`: connections idle for at least the time. + idle(duration-seconds), + /// `FLAGS`: connections with all the flags, e.g. `N`. + %flags(string), + /// `LIB-NAME` + lib-name(string), + /// `LIB-VER` + lib-ver(string), + /// `DB` + db(integer), + /// `CAPA`: connections with the capability, e.g. `redirect`. + capa(string), + /// `IP` + ip(string), + /// `NOT-ID client-id [ client-id ... ]` + not-id(list), + /// `NOT-TYPE` + not-type(client-type), + /// `NOT-USER` + not-user(string), + /// `NOT-ADDR` + not-addr(string), + /// `NOT-LADDR` + not-laddr(string), + /// `NOT-NAME` + not-name(string), + /// `NOT-FLAGS` + not-flags(string), + /// `NOT-LIB-NAME` + not-lib-name(string), + /// `NOT-LIB-VER` + not-lib-ver(string), + /// `NOT-DB` + not-db(integer), + /// `NOT-CAPA` + not-capa(string), + /// `NOT-IP` + not-ip(string), + } + + /// Which commands CLIENT PAUSE suspends. + enum client-pause-mode { + /// `WRITE`: commands that may write. + write, + /// `ALL`: every command, the default. + all, + } + + /// How CLIENT UNBLOCK unblocks a connection. + enum client-unblock-type { + /// `TIMEOUT`: as if the command timed out, the default. + timeout, + /// `ERROR`: with an `UNBLOCKED` error. + error, + } + + /// The outcome of CLUSTER BUMPEPOCH. + variant cluster-bumpepoch-result { + /// `BUMPED`: the config epoch was advanced, to this one. + bumped(integer), + /// `STILL`: the config epoch is already the greatest in the cluster, this one. + still(integer), + } + + /// How CLUSTER FAILOVER fails over. + enum cluster-failover-mode { + /// `FORCE`: without the agreement of the primary, which may be down. + force, + /// `TAKEOVER`: without the agreement of the primary or the rest of the cluster. + takeover, + } + + /// Which way a slot migration moves the slots. + enum slot-migration-operation { + /// `IMPORT`: to this node. + %import, + /// `EXPORT`: from this node. + %export, + } + + /// A slot migration, see `cluster-getslotmigrations`. + record slot-migration { + /// The ID of the migration. + link-name: string, + operation: slot-migration-operation, + /// The ranges of slots, start and end both included. + slot-ranges: list>, + /// The ID of the node the slots move to. + target-node: string, + /// The ID of the node the slots move from. + source-node: string, + create-time: unix-time-seconds, + last-update-time: unix-time-seconds, + last-ack-time: unix-time-seconds, + /// The state of the migration, for people to read. + state: string, + /// More details of the state, for people to read. + message: string, + /// The bytes copied on write while forked for the migration. + cow-size: integer, + } + + /// Ranges of slots to migrate to a node, see `cluster-migrateslots`. + record slot-migration-group { + /// The ranges of slots, start and end both included. + ranges: list>, + /// The ID of the node to migrate the slots to. + node-id: string, + /// `AUTH username password`: the credentials for the node, since Valkey 9.2. + auth: option>, + } + + /// How CLUSTER RESET resets a node. + enum cluster-reset-mode { + /// `HARD`: also generates a new node ID and resets the epochs. + hard, + /// `SOFT`: the default. + soft, + } + + /// The state to set a slot to, see `cluster-setslot`. + variant cluster-setslot-state { + /// `IMPORTING node-id`: importing the slot from the node. + importing(string), + /// `MIGRATING node-id`: migrating the slot to the node. + migrating(string), + /// `NODE node-id`: assigned to the node. + node(string), + /// `STABLE`: neither importing nor migrating. + stable, + } + + /// A command log, see `commandlog-get`. + enum commandlog-type { + /// `slow`: commands that took longer than `commandlog-execution-slower-than`. + slow, + /// `large-request`: commands larger than `commandlog-request-larger-than`. + large-request, + /// `large-reply`: replies larger than `commandlog-reply-larger-than`. + large-reply, + } + + /// An entry of the slow log or a command log. + record log-entry { + /// The ID of the entry, unique until the server restarts. + id: integer, + /// When the command ran. + timestamp: unix-time-seconds, + /// The microseconds the command took, or for the large logs, the bytes of the request or + /// reply. + value: integer, + /// The command and its arguments, long ones shortened. + arguments: list, + /// The client's address, `ip:port`. + client-address: string, + /// The client's name, see CLIENT SETNAME, empty when it has none. + client-name: string, + } + + /// Where FAILOVER fails over to. + record failover-target { + host: string, + port: u16, + /// `FORCE`: fails over at the timeout, even before the replica has caught up. + force: bool, + } + + /// The latency histogram of a command, see `latency-histogram`. + record latency-histogram-entry { + /// The number of times the command ran. + calls: integer, + /// The cumulative counts of the calls that took up to each number of microseconds, a power + /// of 2. + histogram-usec: list>, + } + + /// The latest sample of the latency of an event, see `latency-latest`. + record latency-latest-entry { + event: string, + /// When the latest sample was taken. + timestamp: unix-time-seconds, + latest: duration-milliseconds, + /// The highest latency since the server started, or the event was reset. + max: duration-milliseconds, + /// The sum of the latencies of the samples. + sum: duration-milliseconds, + /// The number of samples. + count: integer, + } + + /// A loaded module, see `module-list`. + record module-info { + name: string, + /// The module's version. + ver: integer, + /// The path the module was loaded from. + path: string, + /// The arguments the module was loaded with. + args: list, + } + + /// A replica of a primary, see `role`. + record role-replica-info { + ip: string, + port: u16, + /// The replication offset the replica last acknowledged. + offset: integer, + } + + /// The replication state of a primary, see `role`. + record role-primary { + /// The primary's replication offset. + offset: integer, + replicas: list, + } + + /// The replication state of a replica, see `role`. + record role-replica { + primary-host: string, + primary-port: u16, + /// The state of replication, e.g. `connected`, `connecting` or `sync`. + state: string, + /// The replication offset of the data received from the primary, -1 when not connected. + offset: integer, + } + + /// The server's role in replication, see `role`. + variant role-reply { + /// `master` + primary(role-primary), + /// `slave` + replica(role-replica), + /// `sentinel`: the names of the primaries it monitors. + sentinel(list), + } + + /// Whether SHUTDOWN saves to disk. + enum shutdown-save { + /// `NOSAVE`: does not save, even with save points configured. + nosave, + /// `SAVE`: saves, even without save points configured. + save, + } + + /// The ACL categories, or with a category, the commands in it. + /// + /// ACL CAT [ category ] + /// https://valkey.io/commands/acl-cat/ + acl-cat: async func(connection: borrow, category: option) -> result, error>; + /// Delete all the specified ACL users and terminate all the connections that are authenticated with such users. /// /// ACL DELUSER username [ username ... ] /// https://valkey.io/commands/acl-deluser/ acl-deluser: async func(connection: borrow, usernames: list) -> result; + /// Whether the user may run the command, without running it. + /// + /// ACL DRYRUN username command [ arg [ arg ... ] ] + /// https://valkey.io/commands/acl-dryrun/ + acl-dryrun: async func(connection: borrow, username: string, command: list) -> result; + /// Generates a random password. /// /// ACL GENPASS [ bits ] /// https://valkey.io/commands/acl-genpass/ acl-genpass: async func(connection: borrow, bits: option) -> result; + /// The rules of the user, none when the user does not exist. + /// + /// The flags, the hashed passwords, the root selector's rules, then the selectors, as the server + /// normalized them, e.g. `allkeys` is `~*`. Applied with `acl-setuser`, after `reset`, they + /// recreate the user. The roles of Valkey 9.2 are not included. + /// + /// ACL GETUSER username + /// https://valkey.io/commands/acl-getuser/ + acl-getuser: async func(connection: borrow, username: string) -> result>, error>; + + /// A user and its rules, see `acl-list`. + record acl-list-entry { + username: string, + /// The user's rules, as the server normalized them, e.g. `allkeys` is `~*`. Applied with + /// `acl-setuser`, after `reset`, they recreate the user. + rules: list, + } + + /// The rules of every user, as written to an ACL file. + /// + /// ACL LIST + /// https://valkey.io/commands/acl-list/ + acl-list: async func(connection: borrow) -> result, error>; + + /// Replaces the users with those of the configured ACL file. + /// + /// ACL LOAD + /// https://valkey.io/commands/acl-load/ + acl-load: async func(connection: borrow) -> result<_, error>; + + /// The recent security events, commands or authentications the rules denied, up to the count, + /// 10 without one. + /// + /// See also `acl-log-reset`. + /// + /// ACL LOG [ count ] + /// https://valkey.io/commands/acl-log/ + acl-log: async func(connection: borrow, count: option) -> result, error>; + + /// Clears the security events. + /// + /// See also `acl-log`. + /// + /// ACL LOG RESET + /// https://valkey.io/commands/acl-log/ + acl-log-reset: async func(connection: borrow) -> result<_, error>; + + /// Saves the users to the configured ACL file. + /// + /// ACL SAVE + /// https://valkey.io/commands/acl-save/ + acl-save: async func(connection: borrow) -> result<_, error>; + /// A rule of an ACL user, see `acl-setuser`. /// /// https://valkey.io/topics/acl/ @@ -137,6 +515,8 @@ interface commands { db(db-rule), /// The user itself, e.g. whether it is enabled and its passwords. user(user-rule), + /// A rule this interface does not know, e.g. from a newer server, sent as is. + other(string), } /// A rule of a selector, a set of permissions matched independently of the user's root permissions, @@ -150,6 +530,8 @@ interface commands { command(command-rule), /// The databases the selector permits access to. db(db-rule), + /// A rule this interface does not know, e.g. from a newer server, sent as is. + other(string), } /// A rule for the keys a user may access. @@ -219,6 +601,10 @@ interface commands { off, /// `nopass`: removes all the user's passwords, any password then authenticates as the user. nopass, + /// `sanitize-payload`: validates the payloads of the user's RESTORE commands, the default. + sanitize-payload, + /// `skip-sanitize-payload`: skips validating the payloads of the user's RESTORE commands. + skip-sanitize-payload, /// `>`: adds the password to the user's passwords. password(string), /// `#`: adds a password, by its SHA-256 hash as 64 lowercase hex characters, to the user's @@ -243,81 +629,354 @@ interface commands { /// https://valkey.io/commands/acl-setuser/ acl-setuser: async func(connection: borrow, username: string, rules: option>) -> result<_, error>; + /// The names of the users. + /// + /// ACL USERS + /// https://valkey.io/commands/acl-users/ + acl-users: async func(connection: borrow) -> result, error>; - // not yet implemented, from the @admin category: - // ACL CAT - // ACL DRYRUN - // ACL GETUSER - // ACL LIST - // ACL LOAD - // ACL LOG - // ACL SAVE - // ACL USERS - // ACL WHOAMI - // BGREWRITEAOF - // BGSAVE - // CLIENT KILL - // CLIENT LIST - // CLIENT NO-EVICT - // CLIENT PAUSE - // CLIENT UNBLOCK - // CLIENT UNPAUSE - // CLUSTER ADDSLOTS - // CLUSTER ADDSLOTSRANGE - // CLUSTER BUMPEPOCH - // CLUSTER CANCELSLOTMIGRATIONS - // CLUSTER COUNT-FAILURE-REPORTS - // CLUSTER DELSLOTS - // CLUSTER DELSLOTSRANGE - // CLUSTER FAILOVER - // CLUSTER FLUSHSLOT - // CLUSTER FLUSHSLOTS - // CLUSTER FORGET - // CLUSTER GETSLOTMIGRATIONS - // CLUSTER MEET - // CLUSTER MIGRATESLOTS - // CLUSTER REPLICAS - // CLUSTER REPLICATE - // CLUSTER RESET - // CLUSTER SAVECONFIG - // CLUSTER SET-CONFIG-EPOCH - // CLUSTER SETSLOT - // CLUSTER SLAVES - // CLUSTER SYNCSLOTS - // COMMANDLOG GET - // COMMANDLOG LEN - // COMMANDLOG RESET - // CONFIG GET - // CONFIG RESETSTAT - // CONFIG REWRITE - // CONFIG SET - // DEBUG - // FAILOVER - // LASTSAVE - // LATENCY DOCTOR - // LATENCY GRAPH - // LATENCY HISTOGRAM - // LATENCY HISTORY - // LATENCY LATEST - // LATENCY RESET - // MODULE LIST - // MODULE LOAD - // MODULE LOADEX - // MODULE UNLOAD - // MONITOR - // PFDEBUG - // PFSELFTEST - // PSYNC - // REPLCONF - // REPLICAOF - // ROLE - // SAVE - // SHUTDOWN - // SLAVEOF - // SLOWLOG GET - // SLOWLOG LEN - // SLOWLOG RESET - // SYNC + /// The name of the user the connection is authenticated as. + /// + /// ACL WHOAMI + /// https://valkey.io/commands/acl-whoami/ + acl-whoami: async func(connection: borrow) -> result; + + /// Starts rewriting the append-only file in the background, returning the server's message. + /// + /// BGREWRITEAOF + /// https://valkey.io/commands/bgrewriteaof/ + bgrewriteaof: async func(connection: borrow) -> result; + + /// Starts saving the databases to disk in the background, or schedules or cancels it, returning + /// the server's message. + /// + /// BGSAVE [ SCHEDULE | CANCEL ] + /// https://valkey.io/commands/bgsave/ + bgsave: async func(connection: borrow, operation: option) -> result; + + /// Closes the connections that match every one of the filters, returning how many. + /// + /// CLIENT KILL filter [ filter ... ] + /// https://valkey.io/commands/client-kill/ + client-kill: async func(connection: borrow, filters: list) -> result; + + /// The connections that match every one of the filters, all of them without filters, each a + /// map of its fields, e.g. `id`, `addr` and `name`. + /// + /// CLIENT LIST [ filter [ filter ... ] ] + /// https://valkey.io/commands/client-list/ + client-list: async func(connection: borrow, filters: option>) -> result>, error>; + + /// Whether the connection may be evicted to free memory, `ON` excludes it. + /// + /// CLIENT NO-EVICT 〈 ON | OFF 〉 + /// https://valkey.io/commands/client-no-evict/ + client-no-evict: async func(connection: borrow, enabled: bool) -> result<_, error>; + + /// Suspends running the commands of other connections, writes only with `WRITE`, for the time. + /// + /// CLIENT PAUSE timeout [ WRITE | ALL ] + /// https://valkey.io/commands/client-pause/ + client-pause: async func(connection: borrow, timeout: duration-milliseconds, mode: option) -> result<_, error>; + + /// Unblocks the connection with the ID from a blocking command, as if it timed out, or with an + /// error, returning whether it was blocked. + /// + /// CLIENT UNBLOCK client-id [ TIMEOUT | ERROR ] + /// https://valkey.io/commands/client-unblock/ + client-unblock: async func(connection: borrow, client-id: integer, unblock-type: option) -> result; + + /// Resumes running the commands of connections paused with `client-pause`. + /// + /// CLIENT UNPAUSE + /// https://valkey.io/commands/client-unpause/ + client-unpause: async func(connection: borrow) -> result<_, error>; + + /// Assigns the slots to the node. + /// + /// CLUSTER ADDSLOTS slot [ slot ... ] + /// https://valkey.io/commands/cluster-addslots/ + cluster-addslots: async func(connection: borrow, slots: list) -> result<_, error>; + + /// Assigns the ranges of slots, start and end both included, to the node. + /// + /// CLUSTER ADDSLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] + /// https://valkey.io/commands/cluster-addslotsrange/ + cluster-addslotsrange: async func(connection: borrow, ranges: list>) -> result<_, error>; + + /// Advances the node's config epoch, unless it is already the greatest in the cluster. + /// + /// CLUSTER BUMPEPOCH + /// https://valkey.io/commands/cluster-bumpepoch/ + cluster-bumpepoch: async func(connection: borrow) -> result; + + /// Cancels the slot migrations in progress. + /// + /// CLUSTER CANCELSLOTMIGRATIONS + /// https://valkey.io/commands/cluster-cancelslotmigrations/ + cluster-cancelslotmigrations: async func(connection: borrow) -> result<_, error>; + + /// The number of active failure reports for the node. + /// + /// CLUSTER COUNT-FAILURE-REPORTS node-id + /// https://valkey.io/commands/cluster-count-failure-reports/ + cluster-count-failure-reports: async func(connection: borrow, node-id: string) -> result; + + /// Unassigns the slots from the node. + /// + /// CLUSTER DELSLOTS slot [ slot ... ] + /// https://valkey.io/commands/cluster-delslots/ + cluster-delslots: async func(connection: borrow, slots: list) -> result<_, error>; + + /// Unassigns the ranges of slots, start and end both included, from the node. + /// + /// CLUSTER DELSLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] + /// https://valkey.io/commands/cluster-delslotsrange/ + cluster-delslotsrange: async func(connection: borrow, ranges: list>) -> result<_, error>; + + /// Fails over the primary of this replica, coordinated with it unless `FORCE` or `TAKEOVER`. + /// + /// CLUSTER FAILOVER [ FORCE | TAKEOVER ] + /// https://valkey.io/commands/cluster-failover/ + cluster-failover: async func(connection: borrow, mode: option) -> result<_, error>; + + /// Unassigns all the slots of the node. + /// + /// CLUSTER FLUSHSLOTS + /// https://valkey.io/commands/cluster-flushslots/ + cluster-flushslots: async func(connection: borrow) -> result<_, error>; + + /// Removes the node from the nodes this node knows of. + /// + /// CLUSTER FORGET node-id + /// https://valkey.io/commands/cluster-forget/ + cluster-forget: async func(connection: borrow, node-id: string) -> result<_, error>; + + /// The slot migrations in progress, and those recently finished. + /// + /// CLUSTER GETSLOTMIGRATIONS + /// https://valkey.io/commands/cluster-getslotmigrations/ + cluster-getslotmigrations: async func(connection: borrow) -> result, error>; + + /// Connects the node to another, adding it to the cluster. + /// + /// CLUSTER MEET ip port [ cluster-bus-port ] + /// https://valkey.io/commands/cluster-meet/ + cluster-meet: async func(connection: borrow, ip: string, port: u16, cluster-bus-port: option) -> result<_, error>; + + /// Migrates the ranges of slots from this node to other nodes. + /// + /// CLUSTER MIGRATESLOTS SLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] NODE node-id [ AUTH username password ] [ SLOTSRANGE ... ] + /// https://valkey.io/commands/cluster-migrateslots/ + cluster-migrateslots: async func(connection: borrow, migrations: list) -> result<_, error>; + + /// The replicas of the primary node, each a line in the format of CLUSTER NODES. + /// + /// CLUSTER REPLICAS node-id + /// https://valkey.io/commands/cluster-replicas/ + cluster-replicas: async func(connection: borrow, node-id: string) -> result, error>; + + /// Makes the node a replica of the primary node, or with none, detaches it from its primary, + /// `NO ONE`. + /// + /// CLUSTER REPLICATE 〈 node-id | NO ONE 〉 + /// https://valkey.io/commands/cluster-replicate/ + cluster-replicate: async func(connection: borrow, node-id: option) -> result<_, error>; + + /// Resets the node, forgetting the other nodes and its slots, `SOFT` without a mode. + /// + /// CLUSTER RESET [ HARD | SOFT ] + /// https://valkey.io/commands/cluster-reset/ + cluster-reset: async func(connection: borrow, mode: option) -> result<_, error>; + + /// Saves the node's cluster configuration to disk. + /// + /// CLUSTER SAVECONFIG + /// https://valkey.io/commands/cluster-saveconfig/ + cluster-saveconfig: async func(connection: borrow) -> result<_, error>; + + /// Sets the config epoch of a new node. + /// + /// CLUSTER SET-CONFIG-EPOCH config-epoch + /// https://valkey.io/commands/cluster-set-config-epoch/ + cluster-set-config-epoch: async func(connection: borrow, config-epoch: integer) -> result<_, error>; + + /// Changes the state of the slot on the node. + /// + /// CLUSTER SETSLOT slot 〈 IMPORTING node-id | MIGRATING node-id | NODE node-id | STABLE 〉 [ TIMEOUT timeout ] + /// https://valkey.io/commands/cluster-setslot/ + cluster-setslot: async func(connection: borrow, slot: u16, state: cluster-setslot-state, timeout: option) -> result<_, error>; + + /// The entries of the command log of the type, up to the count, all of them for -1. + /// + /// COMMANDLOG GET count 〈 slow | large-request | large-reply 〉 + /// https://valkey.io/commands/commandlog-get/ + commandlog-get: async func(connection: borrow, count: integer, %type: commandlog-type) -> result, error>; + + /// The number of entries in the command log of the type. + /// + /// COMMANDLOG LEN 〈 slow | large-request | large-reply 〉 + /// https://valkey.io/commands/commandlog-len/ + commandlog-len: async func(connection: borrow, %type: commandlog-type) -> result; + + /// Clears the command log of the type. + /// + /// COMMANDLOG RESET 〈 slow | large-request | large-reply 〉 + /// https://valkey.io/commands/commandlog-reset/ + commandlog-reset: async func(connection: borrow, %type: commandlog-type) -> result<_, error>; + + /// The values of the configuration parameters matching the glob-style patterns, by name. + /// + /// CONFIG GET parameter [ parameter ... ] + /// https://valkey.io/commands/config-get/ + config-get: async func(connection: borrow, parameters: list) -> result, error>; + + /// Resets the server's statistics, e.g. those of INFO. + /// + /// CONFIG RESETSTAT + /// https://valkey.io/commands/config-resetstat/ + config-resetstat: async func(connection: borrow) -> result<_, error>; + + /// Rewrites the configuration file with the current configuration. + /// + /// CONFIG REWRITE + /// https://valkey.io/commands/config-rewrite/ + config-rewrite: async func(connection: borrow) -> result<_, error>; + + /// Sets the configuration parameters to the values, all or none of them. + /// + /// CONFIG SET parameter value [ parameter value ... ] + /// https://valkey.io/commands/config-set/ + config-set: async func(connection: borrow, parameters-and-values: list>) -> result<_, error>; + + /// Fails over from this primary to a replica, the one at the target, or aborts a failover in + /// progress. + /// + /// FAILOVER [ TO host port [ FORCE ] ] [ ABORT ] [ TIMEOUT milliseconds ] + /// https://valkey.io/commands/failover/ + failover: async func(connection: borrow, target: option, abort: bool, timeout: option) -> result<_, error>; + + /// When the databases were last saved to disk. + /// + /// LASTSAVE + /// https://valkey.io/commands/lastsave/ + lastsave: async func(connection: borrow) -> result; + + /// A report of the latency events, for people to read. + /// + /// LATENCY DOCTOR + /// https://valkey.io/commands/latency-doctor/ + latency-doctor: async func(connection: borrow) -> result; + + /// A graph of the latency of the event, as text. + /// + /// LATENCY GRAPH event + /// https://valkey.io/commands/latency-graph/ + latency-graph: async func(connection: borrow, event: string) -> result; + + /// The latency histogram of the commands, all of them without commands, by command name. + /// + /// LATENCY HISTOGRAM [ command [ command ... ] ] + /// https://valkey.io/commands/latency-histogram/ + latency-histogram: async func(connection: borrow, commands: option>) -> result, error>; + + /// The samples of the latency of the event, when each was taken and the latency. + /// + /// LATENCY HISTORY event + /// https://valkey.io/commands/latency-history/ + latency-history: async func(connection: borrow, event: string) -> result>, error>; + + /// The latest sample of the latency of each event. + /// + /// LATENCY LATEST + /// https://valkey.io/commands/latency-latest/ + latency-latest: async func(connection: borrow) -> result, error>; + + /// Clears the samples of the events, all of them without events, returning how many events. + /// + /// LATENCY RESET [ event [ event ... ] ] + /// https://valkey.io/commands/latency-reset/ + latency-reset: async func(connection: borrow, events: option>) -> result; + + /// The loaded modules. + /// + /// MODULE LIST + /// https://valkey.io/commands/module-list/ + module-list: async func(connection: borrow) -> result, error>; + + /// Loads the module at the path, with the arguments. + /// + /// MODULE LOAD path [ arg [ arg ... ] ] + /// https://valkey.io/commands/module-load/ + module-load: async func(connection: borrow, path: string, args: option>) -> result<_, error>; + + /// Loads the module at the path, with the configuration parameters and arguments. + /// + /// MODULE LOADEX path [ CONFIG name value [ CONFIG name value ... ] ] [ ARGS args [ args ... ] ] + /// https://valkey.io/commands/module-loadex/ + module-loadex: async func(connection: borrow, path: string, configs: option>>, args: option>) -> result<_, error>; + + /// Unloads the module, by name. + /// + /// MODULE UNLOAD name + /// https://valkey.io/commands/module-unload/ + module-unload: async func(connection: borrow, name: string) -> result<_, error>; + + /// Makes the server a replica of the primary at the host and port, or with none, a primary, + /// `NO ONE`, returning the server's message. + /// + /// REPLICAOF 〈 host port | NO ONE 〉 + /// https://valkey.io/commands/replicaof/ + replicaof: async func(connection: borrow, primary: option>) -> result; + + /// The server's role in replication, and its state. + /// + /// ROLE + /// https://valkey.io/commands/role/ + role: async func(connection: borrow) -> result; + + /// Saves the databases to disk, blocking the server until done. + /// + /// SAVE + /// https://valkey.io/commands/save/ + save: async func(connection: borrow) -> result<_, error>; + + /// Stops the server, saving to disk as configured, or as the save mode says. + /// + /// The server closes the connection once it stops, so a shutdown that succeeds fails the call + /// with a closed error. + /// + /// See also `shutdown-abort`. + /// + /// SHUTDOWN [ NOSAVE | SAVE ] [ NOW ] [ FORCE ] [ SAFE ] [ FAILOVER ] + /// https://valkey.io/commands/shutdown/ + shutdown: async func(connection: borrow, save: option, now: bool, force: bool, safe: bool, failover: bool) -> result<_, error>; + + /// Aborts a shutdown in progress. + /// + /// See also `shutdown`. + /// + /// SHUTDOWN ABORT + /// https://valkey.io/commands/shutdown/ + shutdown-abort: async func(connection: borrow) -> result<_, error>; + + /// The entries of the slow log, up to the count, 10 without one, all of them for -1. + /// + /// SLOWLOG GET [ count ] + /// https://valkey.io/commands/slowlog-get/ + slowlog-get: async func(connection: borrow, count: option) -> result, error>; + + /// The number of entries in the slow log. + /// + /// SLOWLOG LEN + /// https://valkey.io/commands/slowlog-len/ + slowlog-len: async func(connection: borrow) -> result; + + /// Clears the slow log. + /// + /// SLOWLOG RESET + /// https://valkey.io/commands/slowlog-reset/ + slowlog-reset: async func(connection: borrow) -> result<_, error>; // bitmap commands From 228a849a72ad1031ab3c3796866c5e50677aaada Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 13 Oct 2026 14:51:05 -0400 Subject: [PATCH 17/37] connection commands Signed-off-by: Scott Andrews --- components/commands/src/command.rs | 64 ++++- components/commands/src/lib.rs | 367 +++++++++++++++++++++++-- components/commands/src/value.rs | 226 ++++++++++++++- wit/commands.wit | 428 ++++++++++++++++++++++++++--- 4 files changed, 1020 insertions(+), 65 deletions(-) diff --git a/components/commands/src/command.rs b/components/commands/src/command.rs index 295e191..f4fd874 100644 --- a/components/commands/src/command.rs +++ b/components/commands/src/command.rs @@ -1,8 +1,9 @@ use crate::exports::componentized::valkey::commands::{ Aggregate, BgsaveOperation, BitRange, BitfieldEncoding, BitfieldGet, BitfieldIncrby, BitfieldOffset, BitfieldOperation, BitfieldOverflow, BitfieldSet, ChannelRule, ClientFilter, - ClientPauseMode, ClientType, ClientUnblockType, ClusterFailoverMode, ClusterResetMode, - ClusterSetslotState, CommandRule, CommandlogType, Connection, Cursor, DbRule, + ClientPauseMode, ClientSetinfoAttr, ClientTrackingMode, ClientTrackingOptions, ClientType, + ClientUnblockType, ClusterFailoverMode, ClusterResetMode, ClusterSetslotState, + CommandListFilter, CommandRule, CommandlogType, Connection, Cursor, DbRule, DurationMilliseconds, DurationSeconds, Error, FailoverTarget, FlushMode, GeoCoordinates, GeoCount, GeoFrom, GeoShape, GeoUnit, GeoaddCondition, GetexTtl, HsetexCondition, HsetexFieldCondition, KeyRule, KeyType, LexBound, Limit, LinsertPosition, ListSide, @@ -1014,3 +1015,62 @@ impl IntoCommand for SlotMigrationGroup { .into_command() } } + +impl IntoCommand for ClientSetinfoAttr { + fn into_command(self) -> Result, Error> { + match self { + ClientSetinfoAttr::LibName(name) => ("LIB-NAME", name).into_command(), + ClientSetinfoAttr::LibVer(version) => ("LIB-VER", version).into_command(), + } + } +} + +impl IntoCommand for ClientTrackingMode { + fn into_command(self) -> Result, Error> { + match self { + ClientTrackingMode::Optin => "OPTIN", + ClientTrackingMode::Optout => "OPTOUT", + } + .into_command() + } +} + +/// `[ REDIRECT client-id ] [ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN | OPTOUT ] [ NOLOOP ]` +impl IntoCommand for ClientTrackingOptions { + fn into_command(self) -> Result, Error> { + // PREFIX before each prefix + let prefixes = self.prefixes.map(|prefixes| { + let prefixes = prefixes.into_iter().map(|prefix| ("PREFIX", prefix)); + prefixes.collect::>() + }); + ( + NamedOption("REDIRECT", self.redirect), + NonEmpty::optional("prefixes", prefixes), + Flag("BCAST", self.bcast), + self.mode, + Flag("NOLOOP", self.noloop), + ) + .into_command() + } +} + +/// `FILTERBY 〈 MODULE module-name | ACLCAT category | PATTERN pattern 〉` +impl IntoCommand for CommandListFilter { + fn into_command(self) -> Result, Error> { + let (filter, value) = match self { + CommandListFilter::Module(name) => ("MODULE", name), + CommandListFilter::Aclcat(category) => ("ACLCAT", category), + CommandListFilter::Pattern(pattern) => ("PATTERN", pattern), + }; + ("FILTERBY", filter, value).into_command() + } +} + +/// `ON` or `OFF`, e.g. for CLIENT NO-TOUCH. +pub struct OnOff(pub bool); + +impl IntoCommand for OnOff { + fn into_command(self) -> Result, Error> { + if self.0 { "ON" } else { "OFF" }.into_command() + } +} diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index d3b2a94..f5ed515 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -1,26 +1,28 @@ use crate::command::{ Bit, DecimalSeconds, Fields, Flag, KeysAndArgs, LexRange, NamedOption, NonEmpty, NumKeys, - ScoreRange, Zrange, emit, query, + OnOff, ScoreRange, Zrange, emit, query, }; use crate::exports::componentized::valkey::commands::{ AclDryrunResult, AclListEntry, AclLogEntry, Aggregate, BgsaveOperation, BitRange, BitfieldGet, - BitfieldOperation, BitopOperation, ClientFilter, ClientPauseMode, ClientUnblockType, - ClusterBumpepochResult, ClusterFailoverMode, ClusterResetMode, ClusterSetslotState, - CommandlogType, Connection, Cursor, Double, DurationMilliseconds, DurationSeconds, Error, - ExpiretimeReply, FailoverTarget, FieldPersistResult, FlushMode, FunctionStatsReply, - GeoCoordinates, GeoCount, GeoSearchResult, GeoShape, GeoUnit, GeoaddCondition, GetexTtl, Guest, - HsetexCondition, HsetexFieldCondition, Integer, KeyType, LatencyHistogramEntry, - LatencyLatestEntry, LcsIdxReply, LexBound, Library, Limit, LinsertPosition, LinsertResult, - ListSide, LogEntry, MigrateAuth, ModuleInfo, MsetexPrecondition, MsetexTtl, PexpiretimeReply, - PopEnd, PttlReply, RoleReply, Rule, ScoreBound, SetPrecondition, SetTtl, ShutdownSave, - SlotMigration, SlotMigrationGroup, SortOpts, SortOrder, StreamId, StreamTrim, TtlMode, - TtlReply, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, XclaimOpts, XinfoConsumer, - XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, - ZaddComparison, ZaddCondition, ZrangeBy, ZrangeWithscoresBy, + BitfieldOperation, BitopOperation, ClientFilter, ClientPauseMode, ClientSetinfoAttr, + ClientTrackingOptions, ClientTrackinginfoReply, ClientUnblockType, ClusterBumpepochResult, + ClusterFailoverMode, ClusterResetMode, ClusterSetslotState, CommandDocsReply, CommandInfoReply, + CommandListFilter, CommandlogType, Connection, Cursor, Double, DurationMilliseconds, + DurationSeconds, Error, ExpiretimeReply, FailoverTarget, FieldPersistResult, FlushMode, + FunctionStatsReply, GeoCoordinates, GeoCount, GeoSearchResult, GeoShape, GeoUnit, + GeoaddCondition, GetexTtl, Guest, HsetexCondition, HsetexFieldCondition, Integer, KeyType, + LatencyHistogramEntry, LatencyLatestEntry, LcsIdxReply, LexBound, Library, Limit, + LinsertPosition, LinsertResult, ListSide, LogEntry, MigrateAuth, ModuleInfo, + MsetexPrecondition, MsetexTtl, PexpiretimeReply, PopEnd, PttlReply, RoleReply, Rule, + ScoreBound, SetPrecondition, SetTtl, ShutdownSave, SlotMigration, SlotMigrationGroup, SortOpts, + SortOrder, StreamId, StreamTrim, TrackingRedirect, TtlMode, TtlReply, TtlResult, + UnixTimeMilliseconds, UnixTimeSeconds, Value, XclaimOpts, XinfoConsumer, XinfoGroup, + XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, ZaddComparison, + ZaddCondition, ZrangeBy, ZrangeWithscoresBy, }; use crate::value::{ - AclList, AclUserRules, ClientList, FlatPairs, Migrated, NextCursor, OptionalKeyType, OrNever, - OrZeroId, ScoredMembers, + AclList, AclUserRules, ClientList, CommandDocsList, CommandInfoList, FlatPairs, Migrated, + NextCursor, OptionalKeyType, OrNever, OrZeroId, ScoredMembers, }; use wit_bindgen::rt::Map; @@ -1076,6 +1078,339 @@ impl Guest for Commands { } } + #[doc = " Signals that the next command follows an ASK redirect, for a slot being imported."] + #[doc = ""] + #[doc = " It applies to the next command on the connection, which may be another caller's when the"] + #[doc = " connection is shared. Use a connection of your own."] + #[doc = ""] + #[doc = " ASKING"] + #[doc = " https://valkey.io/commands/asking/"] + #[allow(async_fn_in_trait)] + async fn asking(connection: &Connection) -> Result<(), Error> { + query(connection, "ASKING").await + } + + #[doc = " Whether the keys the next command reads are tracked, with tracking in `optin` or `optout`"] + #[doc = " mode."] + #[doc = ""] + #[doc = " It applies to the next command on the connection, which may be another caller's when the"] + #[doc = " connection is shared. Use a connection of your own."] + #[doc = ""] + #[doc = " CLIENT CACHING 〈 YES | NO 〉"] + #[doc = " https://valkey.io/commands/client-caching/"] + #[allow(async_fn_in_trait)] + async fn client_caching(connection: &Connection, enabled: bool) -> Result<(), Error> { + query( + connection, + ("CLIENT", "CACHING", if enabled { "YES" } else { "NO" }), + ) + .await + } + + #[doc = " Declares the capabilities the client supports, e.g. `redirect`."] + #[doc = ""] + #[doc = " CLIENT CAPA capability [ capability ... ]"] + #[doc = " https://valkey.io/commands/client-capa/"] + #[allow(async_fn_in_trait)] + async fn client_capa(connection: &Connection, capabilities: Vec) -> Result<(), Error> { + query( + connection, + ( + "CLIENT", + "CAPA", + NonEmpty::new("capabilities", capabilities), + ), + ) + .await + } + + #[doc = " The name of the connection, none when it has none."] + #[doc = ""] + #[doc = " CLIENT GETNAME"] + #[doc = " https://valkey.io/commands/client-getname/"] + #[allow(async_fn_in_trait)] + async fn client_getname(connection: &Connection) -> Result, Error> { + query(connection, ("CLIENT", "GETNAME")).await + } + + #[doc = " Where the connection's tracking notifications are redirected."] + #[doc = ""] + #[doc = " CLIENT GETREDIR"] + #[doc = " https://valkey.io/commands/client-getredir/"] + #[allow(async_fn_in_trait)] + async fn client_getredir(connection: &Connection) -> Result { + query(connection, ("CLIENT", "GETREDIR")).await + } + + #[doc = " The ID of the connection, unique until the server restarts."] + #[doc = ""] + #[doc = " CLIENT ID"] + #[doc = " https://valkey.io/commands/client-id/"] + #[allow(async_fn_in_trait)] + async fn client_id(connection: &Connection) -> Result { + query(connection, ("CLIENT", "ID")).await + } + + #[doc = " Whether the connection is an import source, which may write while the server is in import"] + #[doc = " mode."] + #[doc = ""] + #[doc = " CLIENT IMPORT-SOURCE 〈 ON | OFF 〉"] + #[doc = " https://valkey.io/commands/client-import-source/"] + #[allow(async_fn_in_trait)] + async fn client_import_source(connection: &Connection, enabled: bool) -> Result<(), Error> { + query(connection, ("CLIENT", "IMPORT-SOURCE", OnOff(enabled))).await + } + + #[doc = " The fields of the connection, as CLIENT LIST has them, e.g. `id`, `addr` and `name`."] + #[doc = ""] + #[doc = " CLIENT INFO"] + #[doc = " https://valkey.io/commands/client-info/"] + #[allow(async_fn_in_trait)] + async fn client_info(connection: &Connection) -> Result, Error> { + // a line of CLIENT LIST + let clients: ClientList = query(connection, ("CLIENT", "INFO")).await?; + clients + .0 + .into_iter() + .next() + .ok_or_else(|| Error::Client("unexpected empty reply".to_string())) + } + + #[doc = " Whether the connection's commands leave the access time of the keys they read unchanged."] + #[doc = ""] + #[doc = " It applies to the connection, so to every caller sharing it."] + #[doc = ""] + #[doc = " CLIENT NO-TOUCH 〈 ON | OFF 〉"] + #[doc = " https://valkey.io/commands/client-no-touch/"] + #[allow(async_fn_in_trait)] + async fn client_no_touch(connection: &Connection, enabled: bool) -> Result<(), Error> { + query(connection, ("CLIENT", "NO-TOUCH", OnOff(enabled))).await + } + + #[doc = " Sets the name or version of the library of the connection, see CLIENT LIST."] + #[doc = ""] + #[doc = " CLIENT SETINFO 〈 LIB-NAME libname | LIB-VER libver 〉"] + #[doc = " https://valkey.io/commands/client-setinfo/"] + #[allow(async_fn_in_trait)] + async fn client_setinfo(connection: &Connection, attr: ClientSetinfoAttr) -> Result<(), Error> { + query(connection, ("CLIENT", "SETINFO", attr)).await + } + + #[doc = " Sets the name of the connection."] + #[doc = ""] + #[doc = " CLIENT SETNAME connection-name"] + #[doc = " https://valkey.io/commands/client-setname/"] + #[allow(async_fn_in_trait)] + async fn client_setname(connection: &Connection, connection_name: String) -> Result<(), Error> { + query(connection, ("CLIENT", "SETNAME", connection_name)).await + } + + #[doc = " Turns tracking on with the options, or off with none, for server assisted client side"] + #[doc = " caching. Invalidations arrive as push messages, see `connection.pushes`."] + #[doc = ""] + #[doc = " It applies to the connection, so to every caller sharing it."] + #[doc = ""] + #[doc = " CLIENT TRACKING 〈 ON | OFF 〉 [ REDIRECT client-id ] [ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN ] [ OPTOUT ] [ NOLOOP ]"] + #[doc = " https://valkey.io/commands/client-tracking/"] + #[allow(async_fn_in_trait)] + async fn client_tracking( + connection: &Connection, + options: Option, + ) -> Result<(), Error> { + // ON with options, OFF without + let status = OnOff(options.is_some()); + query(connection, ("CLIENT", "TRACKING", status, options)).await + } + + #[doc = " The connection's tracking state."] + #[doc = ""] + #[doc = " CLIENT TRACKINGINFO"] + #[doc = " https://valkey.io/commands/client-trackinginfo/"] + #[allow(async_fn_in_trait)] + async fn client_trackinginfo( + connection: &Connection, + ) -> Result { + query(connection, ("CLIENT", "TRACKINGINFO")).await + } + + #[doc = " The number of commands of the server."] + #[doc = ""] + #[doc = " COMMAND COUNT"] + #[doc = " https://valkey.io/commands/command-count/"] + #[allow(async_fn_in_trait)] + async fn command_count(connection: &Connection) -> Result { + query(connection, ("COMMAND", "COUNT")).await + } + + #[doc = " Every command of the server, a container followed by its subcommands."] + #[doc = ""] + #[doc = " COMMAND"] + #[doc = " https://valkey.io/commands/command/"] + #[allow(async_fn_in_trait)] + async fn command(connection: &Connection) -> Result, Error> { + let commands: CommandInfoList = query(connection, "COMMAND").await?; + Ok(commands.0) + } + + #[doc = " The documentation of the commands, all of them without names, a container followed by its"] + #[doc = " subcommands, commands that do not exist left out."] + #[doc = ""] + #[doc = " COMMAND DOCS [ command-name [ command-name ... ] ]"] + #[doc = " https://valkey.io/commands/command-docs/"] + #[allow(async_fn_in_trait)] + async fn command_docs( + connection: &Connection, + command_names: Option>, + ) -> Result, Error> { + let command_names = NonEmpty::optional("command-names", command_names); + let commands: CommandDocsList = + query(connection, ("COMMAND", "DOCS", command_names)).await?; + Ok(commands.0) + } + + #[doc = " The commands, all of them without names, a container followed by its subcommands, commands"] + #[doc = " that do not exist left out."] + #[doc = ""] + #[doc = " COMMAND INFO [ command-name [ command-name ... ] ]"] + #[doc = " https://valkey.io/commands/command-info/"] + #[allow(async_fn_in_trait)] + async fn command_info( + connection: &Connection, + command_names: Option>, + ) -> Result, Error> { + let command_names = NonEmpty::optional("command-names", command_names); + let commands: CommandInfoList = + query(connection, ("COMMAND", "INFO", command_names)).await?; + Ok(commands.0) + } + + #[doc = " The keys of the command, its name and arguments."] + #[doc = ""] + #[doc = " COMMAND GETKEYS command [ arg [ arg ... ] ]"] + #[doc = " https://valkey.io/commands/command-getkeys/"] + #[allow(async_fn_in_trait)] + async fn command_getkeys( + connection: &Connection, + command: Vec, + ) -> Result, Error> { + query( + connection, + ("COMMAND", "GETKEYS", NonEmpty::new("command", command)), + ) + .await + } + + #[doc = " The keys of the command, its name and arguments, and how the command uses each, e.g. `RW`"] + #[doc = " and `access`."] + #[doc = ""] + #[doc = " COMMAND GETKEYSANDFLAGS command [ arg [ arg ... ] ]"] + #[doc = " https://valkey.io/commands/command-getkeysandflags/"] + #[allow(async_fn_in_trait)] + async fn command_getkeysandflags( + connection: &Connection, + command: Vec, + ) -> Result)>, Error> { + query( + connection, + ( + "COMMAND", + "GETKEYSANDFLAGS", + NonEmpty::new("command", command), + ), + ) + .await + } + + #[doc = " The names of the commands of the server, those that match the filter with one."] + #[doc = ""] + #[doc = " COMMAND LIST [ FILTERBY 〈 MODULE module-name | ACLCAT category | PATTERN pattern 〉 ]"] + #[doc = " https://valkey.io/commands/command-list/"] + #[allow(async_fn_in_trait)] + async fn command_list( + connection: &Connection, + filter: Option, + ) -> Result, Error> { + query(connection, ("COMMAND", "LIST", filter)).await + } + + #[doc = " Returns the message."] + #[doc = ""] + #[doc = " ECHO message"] + #[doc = " https://valkey.io/commands/echo/"] + #[allow(async_fn_in_trait)] + async fn echo(connection: &Connection, message: String) -> Result { + query(connection, ("ECHO", message)).await + } + + #[doc = " Allows the connection to read from a replica of a cluster, for the slots of its primary."] + #[doc = ""] + #[doc = " It applies to the connection, so to every caller sharing it."] + #[doc = ""] + #[doc = " READONLY"] + #[doc = " https://valkey.io/commands/readonly/"] + #[allow(async_fn_in_trait)] + async fn readonly(connection: &Connection) -> Result<(), Error> { + query(connection, "READONLY").await + } + + #[doc = " Disallows the connection to read from a replica of a cluster, the default."] + #[doc = ""] + #[doc = " It applies to the connection, so to every caller sharing it."] + #[doc = ""] + #[doc = " READWRITE"] + #[doc = " https://valkey.io/commands/readwrite/"] + #[allow(async_fn_in_trait)] + async fn readwrite(connection: &Connection) -> Result<(), Error> { + query(connection, "READWRITE").await + } + + #[doc = " Changes the database of the connection."] + #[doc = ""] + #[doc = " It applies to the connection, so to every caller sharing it."] + #[doc = ""] + #[doc = " SELECT index"] + #[doc = " https://valkey.io/commands/select/"] + #[allow(async_fn_in_trait)] + async fn select(connection: &Connection, index: Integer) -> Result<(), Error> { + query(connection, ("SELECT", index)).await + } + + #[doc = " Waits until the connection's writes reached the number of replicas, or the timeout passed,"] + #[doc = " returning how many replicas they reached. A timeout of zero waits indefinitely."] + #[doc = ""] + #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] + #[doc = " it. Use a connection of your own."] + #[doc = ""] + #[doc = " WAIT numreplicas timeout"] + #[doc = " https://valkey.io/commands/wait/"] + #[allow(async_fn_in_trait)] + async fn wait( + connection: &Connection, + numreplicas: Integer, + timeout: DurationMilliseconds, + ) -> Result { + query(connection, ("WAIT", numreplicas, timeout)).await + } + + #[doc = " Waits until the connection's writes are in the append-only file of the primary, and of the"] + #[doc = " number of replicas, or the timeout passed, returning how many of each they reached. A timeout"] + #[doc = " of zero waits indefinitely."] + #[doc = ""] + #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] + #[doc = " it. Use a connection of your own."] + #[doc = ""] + #[doc = " WAITAOF numlocal numreplicas timeout"] + #[doc = " https://valkey.io/commands/waitaof/"] + #[allow(async_fn_in_trait)] + async fn waitaof( + connection: &Connection, + numlocal: Integer, + numreplicas: Integer, + timeout: DurationMilliseconds, + ) -> Result<(Integer, Integer), Error> { + query(connection, ("WAITAOF", numlocal, numreplicas, timeout)).await + } + #[doc = " Adds the members at their positions to the geospatial index stored at key, or updates their"] #[doc = " positions, returning how many were added, or with `changed`, added or updated."] #[doc = ""] diff --git a/components/commands/src/value.rs b/components/commands/src/value.rs index bcd7ac8..80eaaa1 100644 --- a/components/commands/src/value.rs +++ b/components/commands/src/value.rs @@ -1,14 +1,16 @@ use crate::exports::componentized::valkey::commands::{ - AclDryrunResult, AclListEntry, AclLogEntry, ChannelRule, ClusterBumpepochResult, CommandRule, - Cursor, DbRule, Double, DurationMilliseconds, DurationSeconds, EngineStats, Error, - ExpiretimeReply, FieldPersistResult, FunctionStatsReply, GeoCoordinates, GeoSearchResult, - Integer, KeyRule, KeyType, LatencyHistogramEntry, LatencyLatestEntry, LcsIdxMatch, LcsIdxReply, - Library, LibraryFunction, LinsertResult, LogEntry, ModuleInfo, PexpiretimeReply, PttlReply, - RolePrimary, RoleReplica, RoleReplicaInfo, RoleReply, Rule, RuleSelector, RunningFunction, - SlotMigration, SlotMigrationOperation, StreamId, TtlReply, TtlResult, UnixTimeMilliseconds, - UnixTimeSeconds, UserRule, Value, XinfoConsumer, XinfoGroup, XinfoStreamReply, - XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, - XpendingExtendedEntry, XpendingSummary, + AclDryrunResult, AclListEntry, AclLogEntry, ChannelRule, ClientTrackinginfoReply, + ClusterBumpepochResult, CommandArgument, CommandArgumentType, CommandDocsReply, + CommandInfoReply, CommandRule, Cursor, DbRule, Double, DurationMilliseconds, DurationSeconds, + EngineStats, Error, ExpiretimeReply, FieldPersistResult, FunctionStatsReply, GeoCoordinates, + GeoSearchResult, Integer, KeyRule, KeySpec, KeySpecBeginSearch, KeySpecFindKeys, KeySpecKeynum, + KeySpecKeyword, KeySpecRange, KeyType, LatencyHistogramEntry, LatencyLatestEntry, LcsIdxMatch, + LcsIdxReply, Library, LibraryFunction, LinsertResult, LogEntry, ModuleInfo, PexpiretimeReply, + PttlReply, RolePrimary, RoleReplica, RoleReplicaInfo, RoleReply, Rule, RuleSelector, + RunningFunction, SlotMigration, SlotMigrationOperation, StreamId, TrackingRedirect, TtlReply, + TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, UserRule, Value, XinfoConsumer, XinfoGroup, + XinfoStreamReply, XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, + XinfoStreamReplyFullGroup, XpendingExtendedEntry, XpendingSummary, }; use resp_codec::NestedValue; @@ -932,6 +934,208 @@ impl FromValue for ClientList { } } +/// `-1` when tracking is off, `0` when not redirected, otherwise the ID of the connection. +impl FromValue for TrackingRedirect { + fn from_value(value: Value) -> Result { + match Integer::from_value(value)? { + -1 => Ok(TrackingRedirect::NotTracking), + 0 => Ok(TrackingRedirect::NotRedirected), + id if id > 0 => Ok(TrackingRedirect::Redirected(id)), + code => Err(Error::Client(format!("unexpected reply: {code}"))), + } + } +} + +impl FromValue for ClientTrackinginfoReply { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + flags: fields.required("flags")?, + redirect: fields.required("redirect")?, + prefixes: fields.required("prefixes")?, + }) + } +} + +/// The entries of COMMAND and COMMAND INFO, each followed by its subcommands, nulls, for commands +/// that do not exist, left out. +pub struct CommandInfoList(pub Vec); + +impl FromValue for CommandInfoList { + fn from_value(value: Value) -> Result { + let mut commands = vec![]; + for command in items(value)? { + command_info(command, &mut commands)?; + } + Ok(CommandInfoList(commands)) + } +} + +/// `[name, arity, flags, first-key, last-key, step, acl-categories, tips, key-specs, subcommands]`, +/// the ACL categories since 6.0, the rest since 7.0. +fn command_info(value: Value, commands: &mut Vec) -> Result<(), Error> { + if value == Value::Null { + return Ok(()); + } + let mut items = items(value)?.into_iter(); + let mut next = || { + items + .next() + .ok_or_else(|| Error::Client("too few items for a command".to_string())) + }; + let info = CommandInfoReply { + name: FromValue::from_value(next()?)?, + arity: FromValue::from_value(next()?)?, + flags: FromValue::from_value(next()?)?, + first_key: FromValue::from_value(next()?)?, + last_key: FromValue::from_value(next()?)?, + step: FromValue::from_value(next()?)?, + acl_categories: next().map_or(Ok(vec![]), FromValue::from_value)?, + tips: next().map_or(Ok(vec![]), FromValue::from_value)?, + key_specs: next().map_or(Ok(vec![]), FromValue::from_value)?, + }; + let subcommands: Vec = next().map_or(Ok(vec![]), FromValue::from_value)?; + commands.push(info); + for subcommand in subcommands { + command_info(subcommand, commands)?; + } + Ok(()) +} + +impl FromValue for KeySpec { + fn from_value(value: Value) -> Result { + let mut fields = Fields::from_value(value)?; + let mut begin_search: Fields = fields.required("begin_search")?; + let mut find_keys: Fields = fields.required("find_keys")?; + Ok(Self { + notes: fields.optional("notes")?, + flags: fields.required("flags")?, + begin_search: begin_search_spec(&mut begin_search)?, + find_keys: find_keys_spec(&mut find_keys)?, + }) + } +} + +/// `{type, spec}`, a type this interface does not know is `unknown`. +fn begin_search_spec(fields: &mut Fields) -> Result { + let kind: String = fields.required("type")?; + let mut spec: Fields = fields.required("spec")?; + Ok(match kind.as_str() { + "index" => KeySpecBeginSearch::Index(spec.required("index")?), + "keyword" => KeySpecBeginSearch::Keyword(KeySpecKeyword { + keyword: spec.required("keyword")?, + startfrom: spec.required("startfrom")?, + }), + _ => KeySpecBeginSearch::Unknown, + }) +} + +/// `{type, spec}`, a type this interface does not know is `unknown`. +fn find_keys_spec(fields: &mut Fields) -> Result { + let kind: String = fields.required("type")?; + let mut spec: Fields = fields.required("spec")?; + Ok(match kind.as_str() { + "range" => KeySpecFindKeys::Range(KeySpecRange { + lastkey: spec.required("lastkey")?, + keystep: spec.required("keystep")?, + limit: spec.required("limit")?, + }), + "keynum" => KeySpecFindKeys::Keynum(KeySpecKeynum { + keynumidx: spec.required("keynumidx")?, + firstkey: spec.required("firstkey")?, + keystep: spec.required("keystep")?, + }), + _ => KeySpecFindKeys::Unknown, + }) +} + +/// The documentation of COMMAND DOCS, a map by name, each followed by its subcommands. +pub struct CommandDocsList(pub Vec); + +impl FromValue for CommandDocsList { + fn from_value(value: Value) -> Result { + let mut commands = vec![]; + for (name, doc) in entries::(value)? { + command_doc(name, doc, &mut commands)?; + } + Ok(CommandDocsList(commands)) + } +} + +fn command_doc( + name: String, + mut fields: Fields, + commands: &mut Vec, +) -> Result<(), Error> { + let mut arguments = vec![]; + let nested: Option> = fields.optional("arguments")?; + command_arguments(nested.unwrap_or_default(), None, &mut arguments)?; + commands.push(CommandDocsReply { + name, + summary: fields.required("summary")?, + since: fields.required("since")?, + group: fields.required("group")?, + complexity: fields.optional("complexity")?, + module: fields.optional("module")?, + doc_flags: fields.optional("doc_flags")?.unwrap_or_default(), + deprecated_since: fields.optional("deprecated_since")?, + replaced_by: fields.optional("replaced_by")?, + history: fields.optional("history")?.unwrap_or_default(), + arguments, + }); + let subcommands: Option> = fields + .optional::>("subcommands")? + .map(|subcommands| subcommands.0); + for (name, doc) in subcommands.unwrap_or_default() { + command_doc(name, doc, commands)?; + } + Ok(()) +} + +/// The arguments, depth first, each followed by those it contains, which point to it as their +/// parent. +fn command_arguments( + nested: Vec, + parent: Option, + arguments: &mut Vec, +) -> Result<(), Error> { + for mut fields in nested { + let index = u32::try_from(arguments.len()) + .map_err(|_| Error::Client("too many arguments".to_string()))?; + let kind: String = fields.required("type")?; + arguments.push(CommandArgument { + parent, + name: fields.required("name")?, + type_: argument_type(kind), + display_text: fields.optional("display_text")?, + key_spec_index: fields.optional("key_spec_index")?, + token: fields.optional("token")?, + summary: fields.optional("summary")?, + since: fields.optional("since")?, + deprecated_since: fields.optional("deprecated_since")?, + flags: fields.optional("flags")?.unwrap_or_default(), + }); + let contained: Option> = fields.optional("arguments")?; + command_arguments(contained.unwrap_or_default(), Some(index), arguments)?; + } + Ok(()) +} + +fn argument_type(kind: String) -> CommandArgumentType { + match kind.as_str() { + "string" => CommandArgumentType::String, + "integer" => CommandArgumentType::Integer, + "double" => CommandArgumentType::Double, + "key" => CommandArgumentType::Key, + "pattern" => CommandArgumentType::Pattern, + "unix-time" => CommandArgumentType::UnixTime, + "pure-token" => CommandArgumentType::PureToken, + "oneof" => CommandArgumentType::Oneof, + "block" => CommandArgumentType::Block, + _ => CommandArgumentType::Other(kind), + } +} + impl FromValue for TtlResult { fn from_value(value: Value) -> Result { match Integer::from_value(value)? { @@ -1082,7 +1286,7 @@ fn items(reply: Value) -> Result, Error> { } /// The entries of a map, or of a flat array of keys and values. -fn entries(reply: Value) -> Result, Error> { +pub fn entries(reply: Value) -> Result, Error> { let entry = |key, value| Ok((K::from_value(decode(key)?)?, V::from_value(decode(value)?)?)); match reply { Value::Map(entries) => entries diff --git a/wit/commands.wit b/wit/commands.wit index 19cbf2e..3e1da90 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -1114,12 +1114,360 @@ interface commands { // connection commands + /// How a key spec finds the first key, see `key-spec`. + variant key-spec-begin-search { + /// `index`: the argument at the index. + index(integer), + /// `keyword`: the argument after the keyword. + keyword(key-spec-keyword), + /// `unknown`: the keys cannot be found from the arguments alone. + unknown, + } + + /// The keyword before the first key, see `key-spec-begin-search`. + record key-spec-keyword { + keyword: string, + /// The index of the argument to start searching from, from the end when negative. + startfrom: integer, + } + + /// How a key spec finds the keys after the first one, see `key-spec`. + variant key-spec-find-keys { + /// `range`: a range of arguments. + range(key-spec-range), + /// `keynum`: the number of keys is an argument. + keynum(key-spec-keynum), + /// `unknown`: the keys cannot be found from the arguments alone. + unknown, + } + + /// A range of keys, see `key-spec-find-keys`. + record key-spec-range { + /// The index of the last key, relative to the first, from the end when negative. + lastkey: integer, + /// The number of arguments from one key to the next. + keystep: integer, + /// With a `lastkey` of -1, divides the remaining arguments, e.g. 2 for half of them. + limit: integer, + } + + /// A number of keys, see `key-spec-find-keys`. + record key-spec-keynum { + /// The index of the argument with the number of keys, relative to the first. + keynumidx: integer, + /// The index of the first key, relative to the first. + firstkey: integer, + /// The number of arguments from one key to the next. + keystep: integer, + } + + /// Where a command's keys are and how it uses them. + record key-spec { + notes: option, + /// How the command uses the keys, e.g. `RW`, `access` and `update`. + %flags: list, + begin-search: key-spec-begin-search, + find-keys: key-spec-find-keys, + } + + /// A command, see `command` and `command-info`. + record command-info-reply { + /// The command's name, e.g. `get`, or for a subcommand, `config|get`. + name: string, + /// The number of arguments, the name included, the minimum when negative. + arity: integer, + /// The command's flags, e.g. `readonly` and `fast`. + %flags: list, + /// The index of the first key, 0 for none. + first-key: integer, + /// The index of the last key, from the end when negative. + last-key: integer, + /// The number of arguments from one key to the next. + step: integer, + /// The ACL categories of the command, e.g. `@read`. + acl-categories: list, + /// Hints for clients, e.g. `nondeterministic_output`. + tips: list, + key-specs: list, + } + + /// The type of an argument of a command, see `command-argument`. + variant command-argument-type { + /// `string` + %string, + /// `integer` + integer, + /// `double` + double, + /// `key`: a key, see `key-spec-index`. + key, + /// `pattern`: a glob-style pattern. + pattern, + /// `unix-time` + unix-time, + /// `pure-token`: the token alone, without a value. + pure-token, + /// `oneof`: one of the arguments it contains. + oneof, + /// `block`: the arguments it contains, together. + block, + /// A type this interface does not know, e.g. from a newer server. + other(string), + } + + /// An argument of a command, see `command-docs`. + record command-argument { + /// The index of the `oneof` or `block` argument containing this one, in the command's + /// arguments, none at the top level. + parent: option, + name: string, + %type: command-argument-type, + /// The name to show for the argument. + display-text: option, + /// The index of the argument's key spec, for a key. + key-spec-index: option, + /// The token before the argument's value, e.g. `EX`, or for a `pure-token`, the token alone. + token: option, + summary: option, + /// The version that added the argument. + since: option, + deprecated-since: option, + /// The argument's flags, e.g. `optional`, `multiple` and `multiple_token`. + %flags: list, + } + + /// The documentation of a command, see `command-docs`. + record command-docs-reply { + /// The command's name, e.g. `get`, or for a subcommand, `config|get`. + name: string, + summary: string, + /// The version that added the command. + since: string, + /// The command's group, e.g. `string`. + group: string, + /// The command's time complexity, none for a few commands, e.g. MONITOR. + complexity: option, + /// The module of the command, for a module's command. + module: option, + /// The command's documentation flags, e.g. `deprecated` or `syscmd`. + doc-flags: list, + deprecated-since: option, + /// The command to use instead, for a deprecated command. + replaced-by: option, + /// The changes to the command, the version of each and what changed. + history: list>, + /// The arguments, depth first, those an argument contains after it. + arguments: list, + } + + /// Where a connection's tracking notifications are redirected, see `client-getredir` and + /// `client-trackinginfo`. + variant tracking-redirect { + /// -1: tracking is not on. + not-tracking, + /// 0: tracking is on, the notifications are not redirected. + not-redirected, + /// The ID of the connection the notifications are redirected to. + redirected(integer), + } + + /// Information about the library of a connection, see `client-setinfo`. + variant client-setinfo-attr { + /// `LIB-NAME`: the name of the library, e.g. `valkey-wasm`. + lib-name(string), + /// `LIB-VER`: the version of the library. + lib-ver(string), + } + + /// Which keys tracking tracks without CLIENT CACHING. + enum client-tracking-mode { + /// `OPTIN`: none, only those of commands after CLIENT CACHING YES. + optin, + /// `OPTOUT`: all, except those of commands after CLIENT CACHING NO. + optout, + } + + /// The options of CLIENT TRACKING ON. + record client-tracking-options { + /// `REDIRECT client-id`: sends the notifications to the connection with the ID instead. + redirect: option, + /// `PREFIX prefix`: with `bcast`, only notifies about keys with the prefixes. + prefixes: option>, + /// `BCAST`: notifies about every change of the keys with the prefixes, read or not. + bcast: bool, + mode: option, + /// `NOLOOP`: does not notify about the keys the connection changed itself. + noloop: bool, + } + + /// A connection's tracking state, see `client-trackinginfo`. + record client-trackinginfo-reply { + /// The tracking flags, e.g. `on`, `bcast`, `optin` or `noloop`. + %flags: list, + /// Where the notifications are redirected. + redirect: tracking-redirect, + /// The prefixes of BCAST. + prefixes: list, + } + + /// The filter of COMMAND LIST. + variant command-list-filter { + /// `MODULE module-name`: the commands of the module. + module(string), + /// `ACLCAT category`: the commands in the ACL category. + aclcat(string), + /// `PATTERN pattern`: the commands matching the glob-style pattern. + pattern(string), + } + + /// Signals that the next command follows an ASK redirect, for a slot being imported. + /// + /// It applies to the next command on the connection, which may be another caller's when the + /// connection is shared. Use a connection of your own. + /// + /// ASKING + /// https://valkey.io/commands/asking/ + asking: async func(connection: borrow) -> result<_, error>; + /// Authenticates the current connection. /// /// AUTH [ username ] password /// https://valkey.io/commands/auth/ auth: async func(connection: borrow, username: option, password: string) -> result<_, error>; + /// Whether the keys the next command reads are tracked, with tracking in `optin` or `optout` + /// mode. + /// + /// It applies to the next command on the connection, which may be another caller's when the + /// connection is shared. Use a connection of your own. + /// + /// CLIENT CACHING 〈 YES | NO 〉 + /// https://valkey.io/commands/client-caching/ + client-caching: async func(connection: borrow, enabled: bool) -> result<_, error>; + + /// Declares the capabilities the client supports, e.g. `redirect`. + /// + /// CLIENT CAPA capability [ capability ... ] + /// https://valkey.io/commands/client-capa/ + client-capa: async func(connection: borrow, capabilities: list) -> result<_, error>; + + /// The name of the connection, none when it has none. + /// + /// CLIENT GETNAME + /// https://valkey.io/commands/client-getname/ + client-getname: async func(connection: borrow) -> result, error>; + + /// Where the connection's tracking notifications are redirected. + /// + /// CLIENT GETREDIR + /// https://valkey.io/commands/client-getredir/ + client-getredir: async func(connection: borrow) -> result; + + /// The ID of the connection, unique until the server restarts. + /// + /// CLIENT ID + /// https://valkey.io/commands/client-id/ + client-id: async func(connection: borrow) -> result; + + /// Whether the connection is an import source, which may write while the server is in import + /// mode. + /// + /// CLIENT IMPORT-SOURCE 〈 ON | OFF 〉 + /// https://valkey.io/commands/client-import-source/ + client-import-source: async func(connection: borrow, enabled: bool) -> result<_, error>; + + /// The fields of the connection, as CLIENT LIST has them, e.g. `id`, `addr` and `name`. + /// + /// CLIENT INFO + /// https://valkey.io/commands/client-info/ + client-info: async func(connection: borrow) -> result, error>; + + /// Whether the connection's commands leave the access time of the keys they read unchanged. + /// + /// It applies to the connection, so to every caller sharing it. + /// + /// CLIENT NO-TOUCH 〈 ON | OFF 〉 + /// https://valkey.io/commands/client-no-touch/ + client-no-touch: async func(connection: borrow, enabled: bool) -> result<_, error>; + + /// Sets the name or version of the library of the connection, see CLIENT LIST. + /// + /// CLIENT SETINFO 〈 LIB-NAME libname | LIB-VER libver 〉 + /// https://valkey.io/commands/client-setinfo/ + client-setinfo: async func(connection: borrow, attr: client-setinfo-attr) -> result<_, error>; + + /// Sets the name of the connection. + /// + /// CLIENT SETNAME connection-name + /// https://valkey.io/commands/client-setname/ + client-setname: async func(connection: borrow, connection-name: string) -> result<_, error>; + + /// Turns tracking on with the options, or off with none, for server assisted client side + /// caching. Invalidations arrive as push messages, see `connection.pushes`. + /// + /// It applies to the connection, so to every caller sharing it. + /// + /// CLIENT TRACKING 〈 ON | OFF 〉 [ REDIRECT client-id ] [ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN ] [ OPTOUT ] [ NOLOOP ] + /// https://valkey.io/commands/client-tracking/ + client-tracking: async func(connection: borrow, options: option) -> result<_, error>; + + /// The connection's tracking state. + /// + /// CLIENT TRACKINGINFO + /// https://valkey.io/commands/client-trackinginfo/ + client-trackinginfo: async func(connection: borrow) -> result; + + /// Every command of the server, a container followed by its subcommands. + /// + /// COMMAND + /// https://valkey.io/commands/command/ + command: async func(connection: borrow) -> result, error>; + + /// The number of commands of the server. + /// + /// COMMAND COUNT + /// https://valkey.io/commands/command-count/ + command-count: async func(connection: borrow) -> result; + + /// The documentation of the commands, all of them without names, a container followed by its + /// subcommands, commands that do not exist left out. + /// + /// COMMAND DOCS [ command-name [ command-name ... ] ] + /// https://valkey.io/commands/command-docs/ + command-docs: async func(connection: borrow, command-names: option>) -> result, error>; + + /// The keys of the command, its name and arguments. + /// + /// COMMAND GETKEYS command [ arg [ arg ... ] ] + /// https://valkey.io/commands/command-getkeys/ + command-getkeys: async func(connection: borrow, command: list) -> result, error>; + + /// The keys of the command, its name and arguments, and how the command uses each, e.g. `RW` + /// and `access`. + /// + /// COMMAND GETKEYSANDFLAGS command [ arg [ arg ... ] ] + /// https://valkey.io/commands/command-getkeysandflags/ + command-getkeysandflags: async func(connection: borrow, command: list) -> result>>, error>; + + /// The commands, all of them without names, a container followed by its subcommands, commands + /// that do not exist left out. + /// + /// COMMAND INFO [ command-name [ command-name ... ] ] + /// https://valkey.io/commands/command-info/ + command-info: async func(connection: borrow, command-names: option>) -> result, error>; + + /// The names of the commands of the server, those that match the filter with one. + /// + /// COMMAND LIST [ FILTERBY 〈 MODULE module-name | ACLCAT category | PATTERN pattern 〉 ] + /// https://valkey.io/commands/command-list/ + command-list: async func(connection: borrow, filter: option) -> result, error>; + + /// Returns the message. + /// + /// ECHO message + /// https://valkey.io/commands/echo/ + echo: async func(connection: borrow, message: string) -> result; + /// Switch to a different protocol, optionally authenticating and setting the connection's name, or provide a contextual client report. /// /// RESP2 is not fully supported by this client. @@ -1134,42 +1482,50 @@ interface commands { /// https://valkey.io/commands/ping/ ping: async func(connection: borrow, message: option) -> result<_, error>; - // not yet implemented, from the @connection category: - // ASKING - // CLIENT CACHING - // CLIENT CAPA - // CLIENT GETNAME - // CLIENT GETREDIR - // CLIENT ID - // CLIENT IMPORT-SOURCE - // CLIENT INFO - // CLIENT KILL - // CLIENT LIST - // CLIENT NO-EVICT - // CLIENT NO-TOUCH - // CLIENT PAUSE - // CLIENT REPLY - // CLIENT SETINFO - // CLIENT SETNAME - // CLIENT TRACKING - // CLIENT TRACKINGINFO - // CLIENT UNBLOCK - // CLIENT UNPAUSE - // COMMAND - // COMMAND COUNT - // COMMAND DOCS - // COMMAND GETKEYS - // COMMAND GETKEYSANDFLAGS - // COMMAND INFO - // COMMAND LIST - // ECHO - // QUIT - // READONLY - // READWRITE - // RESET - // SELECT - // WAIT - // WAITAOF + /// Allows the connection to read from a replica of a cluster, for the slots of its primary. + /// + /// It applies to the connection, so to every caller sharing it. + /// + /// READONLY + /// https://valkey.io/commands/readonly/ + readonly: async func(connection: borrow) -> result<_, error>; + + /// Disallows the connection to read from a replica of a cluster, the default. + /// + /// It applies to the connection, so to every caller sharing it. + /// + /// READWRITE + /// https://valkey.io/commands/readwrite/ + readwrite: async func(connection: borrow) -> result<_, error>; + + /// Changes the database of the connection. + /// + /// It applies to the connection, so to every caller sharing it. + /// + /// SELECT index + /// https://valkey.io/commands/select/ + select: async func(connection: borrow, index: integer) -> result<_, error>; + + /// Waits until the connection's writes reached the number of replicas, or the timeout passed, + /// returning how many replicas they reached. A timeout of zero waits indefinitely. + /// + /// The connection waits too, replies to commands sent after it on the connection wait behind + /// it. Use a connection of your own. + /// + /// WAIT numreplicas timeout + /// https://valkey.io/commands/wait/ + wait: async func(connection: borrow, numreplicas: integer, timeout: duration-milliseconds) -> result; + + /// Waits until the connection's writes are in the append-only file of the primary, and of the + /// number of replicas, or the timeout passed, returning how many of each they reached. A timeout + /// of zero waits indefinitely. + /// + /// The connection waits too, replies to commands sent after it on the connection wait behind + /// it. Use a connection of your own. + /// + /// WAITAOF numlocal numreplicas timeout + /// https://valkey.io/commands/waitaof/ + waitaof: async func(connection: borrow, numlocal: integer, numreplicas: integer, timeout: duration-milliseconds) -> result, error>; // geo commands From cab8241cbd2c36b1fd2e71a7a310dfb6a8ed7b70 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 13 Oct 2026 15:00:27 -0400 Subject: [PATCH 18/37] use flags Signed-off-by: Scott Andrews --- components/commands/src/lib.rs | 70 ++++++++++++++-------------------- wit/commands.wit | 39 ++++++++++++++++--- 2 files changed, 62 insertions(+), 47 deletions(-) diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index f5ed515..925bb0d 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -10,15 +10,15 @@ use crate::exports::componentized::valkey::commands::{ CommandListFilter, CommandlogType, Connection, Cursor, Double, DurationMilliseconds, DurationSeconds, Error, ExpiretimeReply, FailoverTarget, FieldPersistResult, FlushMode, FunctionStatsReply, GeoCoordinates, GeoCount, GeoSearchResult, GeoShape, GeoUnit, - GeoaddCondition, GetexTtl, Guest, HsetexCondition, HsetexFieldCondition, Integer, KeyType, - LatencyHistogramEntry, LatencyLatestEntry, LcsIdxReply, LexBound, Library, Limit, - LinsertPosition, LinsertResult, ListSide, LogEntry, MigrateAuth, ModuleInfo, - MsetexPrecondition, MsetexTtl, PexpiretimeReply, PopEnd, PttlReply, RoleReply, Rule, - ScoreBound, SetPrecondition, SetTtl, ShutdownSave, SlotMigration, SlotMigrationGroup, SortOpts, - SortOrder, StreamId, StreamTrim, TrackingRedirect, TtlMode, TtlReply, TtlResult, - UnixTimeMilliseconds, UnixTimeSeconds, Value, XclaimOpts, XinfoConsumer, XinfoGroup, - XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, ZaddComparison, - ZaddCondition, ZrangeBy, ZrangeWithscoresBy, + GeoaddCondition, GeosearchWithFlags, GetexTtl, Guest, HsetexCondition, HsetexFieldCondition, + Integer, KeyType, LatencyHistogramEntry, LatencyLatestEntry, LcsIdxReply, LexBound, Library, + Limit, LinsertPosition, LinsertResult, ListSide, LogEntry, MigrateAuth, MigrateFlags, + ModuleInfo, MsetexPrecondition, MsetexTtl, PexpiretimeReply, PopEnd, PttlReply, RoleReply, + Rule, ScoreBound, SetPrecondition, SetTtl, ShutdownFlags, ShutdownSave, SlotMigration, + SlotMigrationGroup, SortOpts, SortOrder, StreamId, StreamTrim, TrackingRedirect, TtlMode, + TtlReply, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, XclaimOpts, XinfoConsumer, + XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, + ZaddComparison, ZaddCondition, ZrangeBy, ZrangeWithscoresBy, }; use crate::value::{ AclList, AclUserRules, ClientList, CommandDocsList, CommandInfoList, FlatPairs, Migrated, @@ -847,18 +847,15 @@ impl Guest for Commands { async fn shutdown( connection: &Connection, save: Option, - now: bool, - force: bool, - safe: bool, - failover: bool, + modifiers: ShutdownFlags, ) -> Result<(), Error> { let command = ( "SHUTDOWN", save, - Flag("NOW", now), - Flag("FORCE", force), - Flag("SAFE", safe), - Flag("FAILOVER", failover), + Flag("NOW", modifiers.contains(ShutdownFlags::NOW)), + Flag("FORCE", modifiers.contains(ShutdownFlags::FORCE)), + Flag("SAFE", modifiers.contains(ShutdownFlags::SAFE)), + Flag("FAILOVER", modifiers.contains(ShutdownFlags::FAILOVER)), ); query(connection, command).await } @@ -1516,24 +1513,19 @@ impl Guest for Commands { shape: GeoShape, order: Option, count: Option, - withcoord: bool, - withdist: bool, - withhash: bool, + details: GeosearchWithFlags, ) -> Result, Error> { - query( - connection, - ( - "GEOSEARCH", - key, - shape, - order, - count, - Flag("WITHCOORD", withcoord), - Flag("WITHDIST", withdist), - Flag("WITHHASH", withhash), - ), - ) - .await + let command = ( + "GEOSEARCH", + key, + shape, + order, + count, + Flag("WITHCOORD", details.contains(GeosearchWithFlags::WITHCOORD)), + Flag("WITHDIST", details.contains(GeosearchWithFlags::WITHDIST)), + Flag("WITHHASH", details.contains(GeosearchWithFlags::WITHHASH)), + ); + query(connection, command).await } #[doc = " Stores the members of the geospatial index stored at source in the shape, returning how"] @@ -2772,9 +2764,6 @@ impl Guest for Commands { #[doc = " Moves the keys to the database of another instance, returning whether any were moved, false"] #[doc = " when none of the keys exist."] #[doc = ""] - #[doc = " With `copy` the keys are kept in this instance, with `replace` existing keys in the other"] - #[doc = " instance are replaced."] - #[doc = ""] #[doc = " MIGRATE host port \"\" destination-db timeout [ COPY ] [ REPLACE ] [ AUTH password | AUTH2 username password ] KEYS key [ key ... ]"] #[doc = " https://valkey.io/commands/migrate/"] #[allow(async_fn_in_trait)] @@ -2784,8 +2773,7 @@ impl Guest for Commands { port: u16, destination_db: Integer, timeout: DurationMilliseconds, - copy: bool, - replace: bool, + options: MigrateFlags, auth: Option, keys: Vec, ) -> Result { @@ -2797,8 +2785,8 @@ impl Guest for Commands { "", destination_db, timeout, - Flag("COPY", copy), - Flag("REPLACE", replace), + Flag("COPY", options.contains(MigrateFlags::COPY)), + Flag("REPLACE", options.contains(MigrateFlags::REPLACE)), auth, ("KEYS", NonEmpty::new("keys", keys)), ); diff --git a/wit/commands.wit b/wit/commands.wit index 3e1da90..6c79d75 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -416,6 +416,18 @@ interface commands { sentinel(list), } + /// The modifiers of SHUTDOWN. + flags shutdown-flags { + /// `NOW`: does not wait for lagging replicas. + now, + /// `FORCE`: ignores errors that would prevent the shutdown, e.g. failing to save. + force, + /// `SAFE`: refuses to shut down while it would lose the cluster's quorum of primaries. + safe, + /// `FAILOVER`: fails over to a replica first. + failover, + } + /// Whether SHUTDOWN saves to disk. enum shutdown-save { /// `NOSAVE`: does not save, even with save points configured. @@ -950,7 +962,7 @@ interface commands { /// /// SHUTDOWN [ NOSAVE | SAVE ] [ NOW ] [ FORCE ] [ SAFE ] [ FAILOVER ] /// https://valkey.io/commands/shutdown/ - shutdown: async func(connection: borrow, save: option, now: bool, force: bool, safe: bool, failover: bool) -> result<_, error>; + shutdown: async func(connection: borrow, save: option, modifiers: shutdown-flags) -> result<_, error>; /// Aborts a shutdown in progress. /// @@ -1626,6 +1638,16 @@ interface commands { any: bool, } + /// The details GEOSEARCH returns of each member it found. + flags geosearch-with-flags { + /// `WITHCOORD`: the position. + withcoord, + /// `WITHDIST`: the distance from the center. + withdist, + /// `WITHHASH`: the geohash, as an integer. + withhash, + } + /// A member a search found, with the details asked for. record geo-search-result { member: string, @@ -1651,7 +1673,7 @@ interface commands { /// /// GEOSEARCH key [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] [ WITHCOORD ] [ WITHDIST ] [ WITHHASH ] /// https://valkey.io/commands/geosearch/ - geosearch-with: async func(connection: borrow, key: string, shape: geo-shape, order: option, count: option, withcoord: bool, withdist: bool, withhash: bool) -> result, error>; + geosearch-with: async func(connection: borrow, key: string, shape: geo-shape, order: option, count: option, details: geosearch-with-flags) -> result, error>; /// Stores the members of the geospatial index stored at source in the shape, returning how /// many, with their positions, or with `storedist`, their distance from the center as the @@ -1946,6 +1968,14 @@ interface commands { other(string), } + /// The options of MIGRATE. + flags migrate-flags { + /// `COPY`: keeps the keys in this instance. + copy, + /// `REPLACE`: replaces keys that exist in the other instance. + replace, + } + /// The credentials to authenticate to the other instance with, for MIGRATE. variant migrate-auth { /// `AUTH password` @@ -2037,12 +2067,9 @@ interface commands { /// Moves the keys to the database of another instance, returning whether any were moved, false /// when none of the keys exist. /// - /// With `copy` the keys are kept in this instance, with `replace` existing keys in the other - /// instance are replaced. - /// /// MIGRATE host port "" destination-db timeout [ COPY ] [ REPLACE ] [ AUTH password | AUTH2 username password ] KEYS key [ key ... ] /// https://valkey.io/commands/migrate/ - migrate: async func(connection: borrow, host: string, port: u16, destination-db: integer, timeout: duration-milliseconds, copy: bool, replace: bool, auth: option, keys: list) -> result; + migrate: async func(connection: borrow, host: string, port: u16, destination-db: integer, timeout: duration-milliseconds, options: migrate-flags, auth: option, keys: list) -> result; /// Moves the key to the database, returning whether it was moved, false when it does not /// exist, or exists in the database without `replace`. From 716e792f71292a66bad965850ef14cc3a151e940 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 13 Oct 2026 15:56:55 -0400 Subject: [PATCH 19/37] start types refactor Signed-off-by: Scott Andrews --- components/cli/cli.wac | 5 - components/client/client.wac | 5 - components/commands/src/lib.rs | 2 +- components/connection/src/lib.rs | 19 +- components/dep-cli/src/lib.rs | 2 +- components/tcp-connector/src/lib.rs | 9 +- components/tcp/src/lib.rs | 2 +- components/test-tcp/test-tcp.wac | 3 - crates/resp-codec/src/lib.rs | 11 +- wit/commands.wit | 1474 +------------------------- wit/connection.wit | 29 +- wit/connector.wit | 3 +- wit/resp.wit | 28 +- wit/tcp.wit | 4 +- wit/types.wit | 1519 +++++++++++++++++++++++++++ wit/worlds.wit | 1 + wkg.lock | 10 +- 17 files changed, 1586 insertions(+), 1540 deletions(-) create mode 100644 wit/types.wit diff --git a/components/cli/cli.wac b/components/cli/cli.wac index 9341892..6ce69c1 100644 --- a/components/cli/cli.wac +++ b/components/cli/cli.wac @@ -1,23 +1,18 @@ package componentized:valkey; -let resp = new local:resp { ... }; let connection = new local:connection { - "componentized:valkey/resp@0.3.0-dev": resp.resp, ... }; let tcp = new local:tcp { - "componentized:valkey/resp@0.3.0-dev": resp.resp, "componentized:valkey/connection@0.3.0-dev": connection.connection, ... }; let connector = new local:tcp-connector { - "componentized:valkey/resp@0.3.0-dev": resp.resp, "componentized:valkey/connection@0.3.0-dev": connection.connection, "componentized:valkey/tcp@0.3.0-dev": tcp.tcp, ... }; let cli = new local:dep-cli { - "componentized:valkey/resp@0.3.0-dev": resp.resp, "componentized:valkey/connection@0.3.0-dev": connection.connection, "componentized:valkey/connector@0.3.0-dev": connector.connector, ... diff --git a/components/client/client.wac b/components/client/client.wac index d368c93..56ed4d5 100644 --- a/components/client/client.wac +++ b/components/client/client.wac @@ -2,27 +2,22 @@ package componentized:valkey; let resp = new local:resp { ... }; let connection = new local:connection { - "componentized:valkey/resp@0.3.0-dev": resp.resp, ... }; let tcp = new local:tcp { - "componentized:valkey/resp@0.3.0-dev": resp.resp, "componentized:valkey/connection@0.3.0-dev": connection.connection, ... }; let commands = new local:commands { - "componentized:valkey/resp@0.3.0-dev": resp.resp, "componentized:valkey/connection@0.3.0-dev": connection.connection, ... }; let connector = new local:tcp-connector { - "componentized:valkey/resp@0.3.0-dev": resp.resp, "componentized:valkey/connection@0.3.0-dev": connection.connection, "componentized:valkey/tcp@0.3.0-dev": tcp.tcp, ... }; let keyvalue = new local:as-keyvalue { - "componentized:valkey/resp@0.3.0-dev": resp.resp, "componentized:valkey/connection@0.3.0-dev": connection.connection, "componentized:valkey/connector@0.3.0-dev": connector.connector, "componentized:valkey/commands@0.3.0-dev": commands.commands, diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index 925bb0d..1be3692 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -5471,7 +5471,7 @@ wit_bindgen::generate!({ world: "commands", // the resp types shared by every component with: { - "componentized:valkey/resp@0.3.0-dev": resp_codec::bindings::componentized::valkey::resp, + "componentized:valkey/types@0.3.0-dev": resp_codec::bindings::componentized::valkey::types, }, generate_all, }); diff --git a/components/connection/src/lib.rs b/components/connection/src/lib.rs index 00a7c68..f5a1f89 100644 --- a/components/connection/src/lib.rs +++ b/components/connection/src/lib.rs @@ -22,13 +22,12 @@ use std::pin::Pin; use std::rc::{Rc, Weak}; use exports::componentized::valkey::connection::{ - ConnectOpts, Connection, Error, Guest, GuestConnection, + ConnectOpts, Connection, DurationMilliseconds, Error, Guest, GuestConnection, }; use futures_channel::{mpsc, oneshot}; use futures_core::Stream; use resp_codec::{NestedValue, Value}; use wasi::clocks::monotonic_clock::wait_for; -use wasi::clocks::types::Duration; use wit_bindgen::{FutureReader, FutureWriter, StreamReader, StreamWriter, spawn_local}; /// How many push messages a reader may fall behind before its stream is closed. @@ -59,7 +58,10 @@ enum Task { /// Forward push messages from the channel to a reader's stream. Pushes(mpsc::Receiver, StreamWriter), /// Keep the connection alive, until the keep-alive is set again. - KeepAlive { generation: u64, interval: Duration }, + KeepAlive { + generation: u64, + interval: DurationMilliseconds, + }, } /// State shared by a connection and its background tasks. @@ -168,14 +170,14 @@ impl GuestConnection for ConnectionImpl { reader } - fn set_keep_alive(&self, interval: Option) { + fn set_keep_alive(&self, interval: Option) { let generation = { let mut state = self.0.state.borrow_mut(); state.keep_alive_generation += 1; state.keep_alive_generation }; // a zero interval would ping whenever the connection is idle, treated as disabled - if let Some(interval) = interval.filter(|interval| *interval > 0) { + if let Some(interval) = interval.filter(|interval| interval.px > 0) { let _ = self.0.tasks.unbounded_send(Task::KeepAlive { generation, interval, @@ -421,12 +423,13 @@ async fn forward_pushes(mut pushes: mpsc::Receiver, mut writer: StreamWri } /// Send PING whenever the connection was idle for `interval`, closing it if PING fails. -async fn keep_alive(inner: Weak, generation: u64, interval: Duration) { +async fn keep_alive(inner: Weak, generation: u64, interval: DurationMilliseconds) { loop { let Some(seen) = keep_alive_activity(&inner, generation) else { return; }; - wait_for(interval).await; + // a wasi:clocks duration is in nanoseconds + wait_for(interval.px.saturating_mul(1_000_000)).await; let Some(inner) = inner.upgrade() else { return; }; @@ -548,7 +551,7 @@ wit_bindgen::generate!({ world: "connection", // the resp types shared by every component with: { - "componentized:valkey/resp@0.3.0-dev": resp_codec::bindings::componentized::valkey::resp, + "componentized:valkey/types@0.3.0-dev": resp_codec::bindings::componentized::valkey::types, }, generate_all, }); diff --git a/components/dep-cli/src/lib.rs b/components/dep-cli/src/lib.rs index 6545225..32fa5a9 100644 --- a/components/dep-cli/src/lib.rs +++ b/components/dep-cli/src/lib.rs @@ -410,7 +410,7 @@ wit_bindgen::generate!({ world: "cli", // the resp types shared by every component with: { - "componentized:valkey/resp@0.3.0-dev": resp_codec::bindings::componentized::valkey::resp, + "componentized:valkey/types@0.3.0-dev": resp_codec::bindings::componentized::valkey::types, }, generate_all, }); diff --git a/components/tcp-connector/src/lib.rs b/components/tcp-connector/src/lib.rs index 56b073c..fb25d27 100644 --- a/components/tcp-connector/src/lib.rs +++ b/components/tcp-connector/src/lib.rs @@ -3,7 +3,7 @@ // the test harness provides a main #![cfg_attr(not(test), no_main)] -use componentized::valkey::connection::{ConnectOpts, Connection, Error}; +use componentized::valkey::connection::{ConnectOpts, Connection, DurationMilliseconds, Error}; use componentized::valkey::tcp; use exports::componentized::valkey::connector::Guest; use wasi::config::store as config; @@ -36,13 +36,12 @@ impl Guest for Component { }; let keep_alive_interval = match get(KEEP_ALIVE_INTERVAL_MS_KEY)? { Some(interval) => { - let interval: u64 = interval.parse().map_err(|_| { + let px = interval.parse().map_err(|_| { Error::Client(format!( "{KEEP_ALIVE_INTERVAL_MS_KEY} must be a number of milliseconds: {interval}" )) })?; - // a duration is in nanoseconds - Some(interval.saturating_mul(1_000_000)) + Some(DurationMilliseconds { px }) } None => None, }; @@ -68,7 +67,7 @@ wit_bindgen::generate!({ world: "tcp-connector", // the resp types shared by every component with: { - "componentized:valkey/resp@0.3.0-dev": resp_codec::bindings::componentized::valkey::resp, + "componentized:valkey/types@0.3.0-dev": resp_codec::bindings::componentized::valkey::types, }, generate_all, }); diff --git a/components/tcp/src/lib.rs b/components/tcp/src/lib.rs index 870e50f..855771c 100644 --- a/components/tcp/src/lib.rs +++ b/components/tcp/src/lib.rs @@ -283,7 +283,7 @@ wit_bindgen::generate!({ world: "tcp", // the resp types shared by every component with: { - "componentized:valkey/resp@0.3.0-dev": resp_codec::bindings::componentized::valkey::resp, + "componentized:valkey/types@0.3.0-dev": resp_codec::bindings::componentized::valkey::types, }, generate_all, }); diff --git a/components/test-tcp/test-tcp.wac b/components/test-tcp/test-tcp.wac index bd2fa70..c2b4cbc 100644 --- a/components/test-tcp/test-tcp.wac +++ b/components/test-tcp/test-tcp.wac @@ -1,12 +1,9 @@ package componentized:valkey; -let resp = new local:resp { ... }; let connection = new local:connection { - "componentized:valkey/resp@0.3.0-dev": resp.resp, ... }; let tcp = new local:tcp { - "componentized:valkey/resp@0.3.0-dev": resp.resp, "componentized:valkey/connection@0.3.0-dev": connection.connection, ... }; diff --git a/crates/resp-codec/src/lib.rs b/crates/resp-codec/src/lib.rs index 13080de..b2928b2 100644 --- a/crates/resp-codec/src/lib.rs +++ b/crates/resp-codec/src/lib.rs @@ -1,10 +1,11 @@ -//! The RESP codec, and the `componentized:valkey/resp` types it works with. +//! The RESP codec, and the `componentized:valkey/types` it works with. //! -//! A component that imports `resp` uses these types for it, so every component shares them, e.g. +//! A component that imports an interface of the package uses these types for it, so every +//! component shares them, e.g. //! //! ```text //! wit_bindgen::generate!({ -//! with: { "componentized:valkey/resp@0.3.0-dev": resp_codec::bindings::componentized::valkey::resp }, +//! with: { "componentized:valkey/types@0.3.0-dev": resp_codec::bindings::componentized::valkey::types }, //! ... //! }); //! ``` @@ -20,7 +21,7 @@ pub mod bindings { package componentized:resp-codec; world types { - import componentized:valkey/resp@0.3.0-dev; + import componentized:valkey/types@0.3.0-dev; } ", world: "componentized:resp-codec/types", @@ -31,5 +32,5 @@ pub mod bindings { mod codec; -pub use bindings::componentized::valkey::resp::{Error, NestedValue, Value}; +pub use bindings::componentized::valkey::types::{NestedValue, RespError as Error, Value}; pub use codec::{decode, encode, frame_len}; diff --git a/wit/commands.wit b/wit/commands.wit index 6c79d75..6cf067b 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -1,440 +1,44 @@ /// Typed functions for Valkey's commands, over the mechanics of a connection. /// +/// The types are those of the `types` interface, used here under names that may change, e.g. +/// renamed to avoid clashing with a function for a new command. Refer to a type through `types` +/// for a name that does not change. +/// /// A list argument must have at least one item, as the command requires, an empty list fails with a /// client error before anything is sent. A list the command allows to be left out is an option, /// none rather than empty. interface commands { - use connection.{connection, double, integer, value, error}; - - /// A duration in whole seconds, e.g. `EX seconds`. - record duration-seconds { - ex: u64, - } - - /// A duration in whole milliseconds, e.g. `PX milliseconds`. - record duration-milliseconds { - px: u64, - } - - /// A point in time as whole seconds since the Unix epoch, e.g. `EXAT unix-time-seconds`. - record unix-time-seconds { - exat: s64, - } - - /// A point in time as whole milliseconds since the Unix epoch, e.g. `PXAT - /// unix-time-milliseconds`. - record unix-time-milliseconds { - pxat: s64, - } - - /// Where an incremental iteration continues from, e.g. with HSCAN. - /// - /// A scan returns the cursor to resume from, always `resume`, none once the scan is complete. - /// Pass it to the next call until then. - variant cursor { - /// Starts a new scan, the cursor `0`. - start, - /// Continues a scan from the opaque cursor the previous call returned, `0` fails with a client - /// error, use `start`. - resume(string), - } - - /// XXXXXX - /// - /// XXXXXX - /// https://valkey.io/commands/XXXXXX/ - - - /// The condition for setting an expiration, e.g. with EXPIRE or HEXPIRE. - enum ttl-mode { - /// Set expiration only when the field has no expiration. - nx, - /// Set expiration only when the field has an existing expiration. - xx, - /// Set expiration only when the new expiration is greater than current one. - gt, - /// Set expiration only when the new expiration is less than current one. - lt, - } - - /// The outcome of setting the expiration of a hash field, e.g. with HEXPIRE. - enum ttl-result { - /// -2: key does not exist, or field does not exist in the HASH. - does-not-exist, - /// 0: the specified NX | XX | GT | LT condition has not been met. - condition-not-met, - /// 1: the expiration time was applied. - applied, - /// 2: when called with 0 seconds. - zero-seconds, - } - - /// The remaining time to live of a key or hash field, to the second, see `ttl` and `httl`. - variant ttl-reply { - /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. - does-not-exist, - /// -1: the key or field exists, but has no expiration. - no-expiration, - /// The time until it expires. - expires-in(duration-seconds), - } - - /// The remaining time to live of a key or hash field, to the millisecond, see `pttl` and `hpttl`. - variant pttl-reply { - /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. - does-not-exist, - /// -1: the key or field exists, but has no expiration. - no-expiration, - /// The time until it expires. - expires-in(duration-milliseconds), - } - - /// When a key or hash field expires, to the second, see `expiretime` and `hexpiretime`. - variant expiretime-reply { - /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. - does-not-exist, - /// -1: the key or field exists, but has no expiration. - no-expiration, - /// The time at which it expires. - expires-at(unix-time-seconds), - } - - /// When a key or hash field expires, to the millisecond, see `pexpiretime` and `hpexpiretime`. - variant pexpiretime-reply { - /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. - does-not-exist, - /// -1: the key or field exists, but has no expiration. - no-expiration, - /// The time at which it expires. - expires-at(unix-time-milliseconds), - } + use connection.{connection}; + use types.{ + double, error, integer, value, duration-seconds, duration-milliseconds, unix-time-seconds, + unix-time-milliseconds, cursor, ttl-mode, ttl-result, ttl-reply, pttl-reply, + expiretime-reply, pexpiretime-reply, acl-dryrun-result, acl-log-entry, bgsave-operation, + client-type, client-filter, client-pause-mode, client-unblock-type, + cluster-bumpepoch-result, cluster-failover-mode, slot-migration-operation, slot-migration, + slot-migration-group, cluster-reset-mode, cluster-setslot-state, commandlog-type, log-entry, + failover-target, latency-histogram-entry, latency-latest-entry, module-info, + role-replica-info, role-primary, role-replica, role-reply, shutdown-flags, shutdown-save, + acl-list-entry, rule, rule-selector, key-rule, channel-rule, command-rule, db-rule, + user-rule, bit-range, bitfield-encoding, bitfield-offset, bitfield-overflow, bitfield-get, + bitfield-set, bitfield-incrby, bitfield-operation, bitop-operation, key-spec-begin-search, + key-spec-keyword, key-spec-find-keys, key-spec-range, key-spec-keynum, key-spec, + command-info-reply, command-argument-type, command-argument, command-docs-reply, + tracking-redirect, client-setinfo-attr, client-tracking-mode, client-tracking-options, + client-trackinginfo-reply, command-list-filter, geo-coordinates, geo-unit, geoadd-condition, + geo-from, geo-radius, geo-box, geo-shape, geo-count, geosearch-with-flags, + geo-search-result, field-persist-result, hsetex-condition, hsetex-field-condition, key-type, + migrate-flags, migrate-auth, list-side, linsert-position, linsert-result, flush-mode, + library-function, library, running-function, engine-stats, function-stats-reply, limit, + sort-opts, sort-order, score-bound, lex-bound, pop-end, aggregate, zrange-by, + zrange-withscores-by, zadd-condition, zadd-comparison, stream-id, stream-trim, + stream-trim-maxlen, stream-trim-minid, stream-trim-efficiency, xclaim-opts, xinfo-consumer, + xinfo-group, xinfo-stream-reply, xinfo-stream-reply-full, xinfo-stream-reply-full-group, + xinfo-stream-reply-full-consumer, xpending-summary, xpending-extended-entry, getex-ttl, + lcs-idx-reply, lcs-idx-match, msetex-precondition, msetex-ttl, set-precondition, set-ttl + }; - // admin commands - /// Whether a user may run a command, see `acl-dryrun`. - variant acl-dryrun-result { - /// The user may run the command. - allowed, - /// The user may not run the command, and why. - denied(string), - } - - /// A security event, a command or authentication the rules denied, see `acl-log`. - record acl-log-entry { - /// The number of events like this one, within 60 seconds of each other. - count: integer, - /// What was denied, e.g. `command`, `key`, `channel` or `auth`. - reason: string, - /// Where it was denied, e.g. `toplevel`, `multi` or `lua`. - context: string, - /// The command, key or channel that was denied. - object: string, - username: string, - /// The seconds since the latest of these events. - age-seconds: double, - /// The connection, in the format of CLIENT LIST. - client-info: string, - /// The ID of the entry, since Valkey 7.2. - entry-id: option, - /// When the first of these events happened, since Valkey 7.2. - timestamp-created: option, - /// When the latest of these events happened, since Valkey 7.2. - timestamp-last-updated: option, - } - - /// What BGSAVE does. - enum bgsave-operation { - /// `SCHEDULE`: saves once the rewrite of the append-only file in progress is done. - schedule, - /// `CANCEL`: cancels the save in progress, or scheduled. - cancel, - } - - /// The type of a connection, for the `client-filter`. - enum client-type { - /// `normal` - normal, - /// `master`: the connection to the primary of a replica. - primary, - /// `replica` - replica, - /// `pubsub` - pubsub, - } - - /// A filter of connections, for CLIENT KILL and CLIENT LIST. - variant client-filter { - /// `ID client-id [ client-id ... ]` - id(list), - /// `TYPE` - %type(client-type), - /// `USER username` - user(string), - /// `ADDR ip:port`: the address of the client. - addr(string), - /// `LADDR ip:port`: the address of the server the client connected to. - laddr(string), - /// `SKIPME YES | NO`: whether to skip this connection, yes by default for CLIENT KILL. - skipme(bool), - /// `MAXAGE`: connections older than the time. - maxage(duration-seconds), - /// `NAME` - name(string), - /// `IDLE`: connections idle for at least the time. - idle(duration-seconds), - /// `FLAGS`: connections with all the flags, e.g. `N`. - %flags(string), - /// `LIB-NAME` - lib-name(string), - /// `LIB-VER` - lib-ver(string), - /// `DB` - db(integer), - /// `CAPA`: connections with the capability, e.g. `redirect`. - capa(string), - /// `IP` - ip(string), - /// `NOT-ID client-id [ client-id ... ]` - not-id(list), - /// `NOT-TYPE` - not-type(client-type), - /// `NOT-USER` - not-user(string), - /// `NOT-ADDR` - not-addr(string), - /// `NOT-LADDR` - not-laddr(string), - /// `NOT-NAME` - not-name(string), - /// `NOT-FLAGS` - not-flags(string), - /// `NOT-LIB-NAME` - not-lib-name(string), - /// `NOT-LIB-VER` - not-lib-ver(string), - /// `NOT-DB` - not-db(integer), - /// `NOT-CAPA` - not-capa(string), - /// `NOT-IP` - not-ip(string), - } - - /// Which commands CLIENT PAUSE suspends. - enum client-pause-mode { - /// `WRITE`: commands that may write. - write, - /// `ALL`: every command, the default. - all, - } - - /// How CLIENT UNBLOCK unblocks a connection. - enum client-unblock-type { - /// `TIMEOUT`: as if the command timed out, the default. - timeout, - /// `ERROR`: with an `UNBLOCKED` error. - error, - } - - /// The outcome of CLUSTER BUMPEPOCH. - variant cluster-bumpepoch-result { - /// `BUMPED`: the config epoch was advanced, to this one. - bumped(integer), - /// `STILL`: the config epoch is already the greatest in the cluster, this one. - still(integer), - } - - /// How CLUSTER FAILOVER fails over. - enum cluster-failover-mode { - /// `FORCE`: without the agreement of the primary, which may be down. - force, - /// `TAKEOVER`: without the agreement of the primary or the rest of the cluster. - takeover, - } - - /// Which way a slot migration moves the slots. - enum slot-migration-operation { - /// `IMPORT`: to this node. - %import, - /// `EXPORT`: from this node. - %export, - } - - /// A slot migration, see `cluster-getslotmigrations`. - record slot-migration { - /// The ID of the migration. - link-name: string, - operation: slot-migration-operation, - /// The ranges of slots, start and end both included. - slot-ranges: list>, - /// The ID of the node the slots move to. - target-node: string, - /// The ID of the node the slots move from. - source-node: string, - create-time: unix-time-seconds, - last-update-time: unix-time-seconds, - last-ack-time: unix-time-seconds, - /// The state of the migration, for people to read. - state: string, - /// More details of the state, for people to read. - message: string, - /// The bytes copied on write while forked for the migration. - cow-size: integer, - } - - /// Ranges of slots to migrate to a node, see `cluster-migrateslots`. - record slot-migration-group { - /// The ranges of slots, start and end both included. - ranges: list>, - /// The ID of the node to migrate the slots to. - node-id: string, - /// `AUTH username password`: the credentials for the node, since Valkey 9.2. - auth: option>, - } - - /// How CLUSTER RESET resets a node. - enum cluster-reset-mode { - /// `HARD`: also generates a new node ID and resets the epochs. - hard, - /// `SOFT`: the default. - soft, - } - - /// The state to set a slot to, see `cluster-setslot`. - variant cluster-setslot-state { - /// `IMPORTING node-id`: importing the slot from the node. - importing(string), - /// `MIGRATING node-id`: migrating the slot to the node. - migrating(string), - /// `NODE node-id`: assigned to the node. - node(string), - /// `STABLE`: neither importing nor migrating. - stable, - } - - /// A command log, see `commandlog-get`. - enum commandlog-type { - /// `slow`: commands that took longer than `commandlog-execution-slower-than`. - slow, - /// `large-request`: commands larger than `commandlog-request-larger-than`. - large-request, - /// `large-reply`: replies larger than `commandlog-reply-larger-than`. - large-reply, - } - - /// An entry of the slow log or a command log. - record log-entry { - /// The ID of the entry, unique until the server restarts. - id: integer, - /// When the command ran. - timestamp: unix-time-seconds, - /// The microseconds the command took, or for the large logs, the bytes of the request or - /// reply. - value: integer, - /// The command and its arguments, long ones shortened. - arguments: list, - /// The client's address, `ip:port`. - client-address: string, - /// The client's name, see CLIENT SETNAME, empty when it has none. - client-name: string, - } - - /// Where FAILOVER fails over to. - record failover-target { - host: string, - port: u16, - /// `FORCE`: fails over at the timeout, even before the replica has caught up. - force: bool, - } - - /// The latency histogram of a command, see `latency-histogram`. - record latency-histogram-entry { - /// The number of times the command ran. - calls: integer, - /// The cumulative counts of the calls that took up to each number of microseconds, a power - /// of 2. - histogram-usec: list>, - } - - /// The latest sample of the latency of an event, see `latency-latest`. - record latency-latest-entry { - event: string, - /// When the latest sample was taken. - timestamp: unix-time-seconds, - latest: duration-milliseconds, - /// The highest latency since the server started, or the event was reset. - max: duration-milliseconds, - /// The sum of the latencies of the samples. - sum: duration-milliseconds, - /// The number of samples. - count: integer, - } - - /// A loaded module, see `module-list`. - record module-info { - name: string, - /// The module's version. - ver: integer, - /// The path the module was loaded from. - path: string, - /// The arguments the module was loaded with. - args: list, - } - - /// A replica of a primary, see `role`. - record role-replica-info { - ip: string, - port: u16, - /// The replication offset the replica last acknowledged. - offset: integer, - } - - /// The replication state of a primary, see `role`. - record role-primary { - /// The primary's replication offset. - offset: integer, - replicas: list, - } - - /// The replication state of a replica, see `role`. - record role-replica { - primary-host: string, - primary-port: u16, - /// The state of replication, e.g. `connected`, `connecting` or `sync`. - state: string, - /// The replication offset of the data received from the primary, -1 when not connected. - offset: integer, - } - - /// The server's role in replication, see `role`. - variant role-reply { - /// `master` - primary(role-primary), - /// `slave` - replica(role-replica), - /// `sentinel`: the names of the primaries it monitors. - sentinel(list), - } - - /// The modifiers of SHUTDOWN. - flags shutdown-flags { - /// `NOW`: does not wait for lagging replicas. - now, - /// `FORCE`: ignores errors that would prevent the shutdown, e.g. failing to save. - force, - /// `SAFE`: refuses to shut down while it would lose the cluster's quorum of primaries. - safe, - /// `FAILOVER`: fails over to a replica first. - failover, - } - - /// Whether SHUTDOWN saves to disk. - enum shutdown-save { - /// `NOSAVE`: does not save, even with save points configured. - nosave, - /// `SAVE`: saves, even without save points configured. - save, - } + // admin commands /// The ACL categories, or with a category, the commands in it. /// @@ -470,14 +74,6 @@ interface commands { /// https://valkey.io/commands/acl-getuser/ acl-getuser: async func(connection: borrow, username: string) -> result>, error>; - /// A user and its rules, see `acl-list`. - record acl-list-entry { - username: string, - /// The user's rules, as the server normalized them, e.g. `allkeys` is `~*`. Applied with - /// `acl-setuser`, after `reset`, they recreate the user. - rules: list, - } - /// The rules of every user, as written to an ACL file. /// /// ACL LIST @@ -513,128 +109,6 @@ interface commands { /// https://valkey.io/commands/acl-save/ acl-save: async func(connection: borrow) -> result<_, error>; - /// A rule of an ACL user, see `acl-setuser`. - /// - /// https://valkey.io/topics/acl/ - variant rule { - /// The keys the user may access. - keys(key-rule), - /// The Pub/Sub channels the user may access. - channel(channel-rule), - /// The commands the user may call. - command(command-rule), - /// The databases the user may access. - db(db-rule), - /// The user itself, e.g. whether it is enabled and its passwords. - user(user-rule), - /// A rule this interface does not know, e.g. from a newer server, sent as is. - other(string), - } - - /// A rule of a selector, a set of permissions matched independently of the user's root permissions, - /// the user may run a command when either permits it. - variant rule-selector { - /// The keys the selector permits access to. - keys(key-rule), - /// The Pub/Sub channels the selector permits access to. - channel(channel-rule), - /// The commands the selector permits. - command(command-rule), - /// The databases the selector permits access to. - db(db-rule), - /// A rule this interface does not know, e.g. from a newer server, sent as is. - other(string), - } - - /// A rule for the keys a user may access. - variant key-rule { - /// `~`: allows reading and writing the keys matching the glob-style pattern. - add(string), - /// `%R~`: allows reading the keys matching the glob-style pattern. - add-read(string), - /// `%W~`: allows writing the keys matching the glob-style pattern. - add-write(string), - /// `%RW~`: allows reading and writing the keys matching the glob-style pattern, the - /// same as `add`. - add-read-write(string), - /// `allkeys`: allows access to all keys, an alias for `~*`. - all, - /// `resetkeys`: removes all the allowed key patterns. - reset, - } - - /// A rule for the Pub/Sub channels a user may access. - variant channel-rule { - /// `&`: allows access to the Pub/Sub channels matching the glob-style pattern. - add(string), - /// `allchannels`: allows access to all Pub/Sub channels, an alias for `&*`. - all, - /// `resetchannels`: removes all the allowed channel patterns, disconnecting the user's Pub/Sub - /// clients that can no longer access their channels. - reset, - } - - /// A rule for the commands a user may call. - variant command-rule { - /// `+`: allows the command, or a subcommand, e.g. `config|get`. - add(string), - /// `+@`: allows all the commands in the category, e.g. `read`, see ACL CAT for the - /// categories. - add-category(string), - /// `+|`: allows the command only with the given first argument, e.g. `select` - /// with `0`. - add-first-arg(tuple), - /// `allcommands`: allows all commands, including those of modules loaded later, an alias for `+@all`. - all, - /// `-`: disallows the command, or a subcommand. - remove(string), - /// `-@`: disallows all the commands in the category. - remove-category(string), - /// `nocommands`: disallows all commands, an alias for `-@all`. - reset, - } - - /// A rule for the databases a user may access. - variant db-rule { - /// `db=[,...]`: allows access to the databases with the given IDs. - db(list), - /// `alldbs`: allows access to all databases. - all, - /// `resetdbs`: removes all the allowed databases. - reset, - } - - /// A rule for the user itself. - variant user-rule { - /// `on`: enables the user, it is possible to authenticate as this user. - on, - /// `off`: disables the user, it is no longer possible to authenticate as this user, connections - /// already authenticated keep working. - off, - /// `nopass`: removes all the user's passwords, any password then authenticates as the user. - nopass, - /// `sanitize-payload`: validates the payloads of the user's RESTORE commands, the default. - sanitize-payload, - /// `skip-sanitize-payload`: skips validating the payloads of the user's RESTORE commands. - skip-sanitize-payload, - /// `>`: adds the password to the user's passwords. - password(string), - /// `#`: adds a password, by its SHA-256 hash as 64 lowercase hex characters, to the user's - /// passwords. - hashed-password(string), - /// `<`: removes the password from the user's passwords. - remove-password(string), - /// `!`: removes a password, by its SHA-256 hash, from the user's passwords. - remove-hashed-password(string), - /// `( ...)`: adds a selector, with the given rules. - selector(list), - /// `clearselectors`: removes all the user's selectors. - clearselectors, - /// `reset`: resets the user to its state when created, disabled, without passwords, keys, - /// channels, commands or selectors. - reset, - } - /// Create an ACL user with the specified rules or modify the rules of an existing user. /// /// ACL SETUSER username [ rule ] [ [ rule ] ... ] @@ -993,16 +467,6 @@ interface commands { // bitmap commands - /// A range of a string, negative from the end, for BITCOUNT and BITPOS. - variant bit-range { - /// `start`: the bytes from the start to the end of the string. - %from(integer), - /// `start end`: the bytes from the start to the end, both included. - bytes(tuple), - /// `start end BIT`: the bits from the start to the end, both included. - bits(tuple), - } - /// The number of bits set to 1 in the string stored at key, in the range, 0 when the key does /// not exist. /// @@ -1010,66 +474,6 @@ interface commands { /// https://valkey.io/commands/bitcount/ bitcount: async func(connection: borrow, key: string, range: option) -> result; - /// The type of an integer field of a string, for BITFIELD. - variant bitfield-encoding { - /// `i1` to `i64`: a signed integer of the width in bits. - signed(u8), - /// `u1` to `u63`: an unsigned integer of the width in bits. - unsigned(u8), - } - - /// Where an integer field of a string starts, for BITFIELD. - variant bitfield-offset { - /// The offset in bits. - bit(u64), - /// `#field`: the field of the encoding's width, the offset in bits is the field times the - /// width. - field(u64), - } - - /// What a write does when the field overflows, `OVERFLOW`. - enum bitfield-overflow { - /// `WRAP`: wraps around, the default. - wrap, - /// `SAT`: saturates at the minimum or maximum. - sat, - /// `FAIL`: does not write, the result is none. - fail, - } - - /// `GET encoding offset`: the value of the field. - record bitfield-get { - encoding: bitfield-encoding, - offset: bitfield-offset, - } - - /// `SET encoding offset value`: sets the field, the result is its previous value. - record bitfield-set { - encoding: bitfield-encoding, - offset: bitfield-offset, - value: s64, - } - - /// `INCRBY encoding offset increment`: increments the field, the result is its new value. - record bitfield-incrby { - encoding: bitfield-encoding, - offset: bitfield-offset, - increment: s64, - } - - /// An operation of BITFIELD. - /// - /// A write is preceded by an optional overflow mode, `OVERFLOW WRAP | SAT | FAIL`, for it and - /// the following writes. None keeps the current mode, `WRAP` at the start of the command. - variant bitfield-operation { - /// `GET encoding offset` - get(bitfield-get), - /// `[ OVERFLOW WRAP | SAT | FAIL ] SET encoding offset value` - set(tuple, bitfield-set>), - /// `[ OVERFLOW WRAP | SAT | FAIL ] INCRBY encoding offset increment` - incrby(tuple, bitfield-incrby>), - } - /// Reads and writes integer fields of the string stored at key, returning the result of each /// operation in order, none for a write that failed with `OVERFLOW FAIL`. /// @@ -1083,18 +487,6 @@ interface commands { /// https://valkey.io/commands/bitfield_ro/ bitfield-ro: async func(connection: borrow, key: string, gets: option>) -> result, error>; - /// A bitwise operation of BITOP, and its source keys. - variant bitop-operation { - /// `AND` - and(list), - /// `OR` - or(list), - /// `XOR` - xor(list), - /// `NOT`: of a single key. - not(string), - } - /// Stores the bitwise operation of the strings stored at the source keys in the destination, /// returning its length, the length of the longest source. /// @@ -1126,212 +518,6 @@ interface commands { // connection commands - /// How a key spec finds the first key, see `key-spec`. - variant key-spec-begin-search { - /// `index`: the argument at the index. - index(integer), - /// `keyword`: the argument after the keyword. - keyword(key-spec-keyword), - /// `unknown`: the keys cannot be found from the arguments alone. - unknown, - } - - /// The keyword before the first key, see `key-spec-begin-search`. - record key-spec-keyword { - keyword: string, - /// The index of the argument to start searching from, from the end when negative. - startfrom: integer, - } - - /// How a key spec finds the keys after the first one, see `key-spec`. - variant key-spec-find-keys { - /// `range`: a range of arguments. - range(key-spec-range), - /// `keynum`: the number of keys is an argument. - keynum(key-spec-keynum), - /// `unknown`: the keys cannot be found from the arguments alone. - unknown, - } - - /// A range of keys, see `key-spec-find-keys`. - record key-spec-range { - /// The index of the last key, relative to the first, from the end when negative. - lastkey: integer, - /// The number of arguments from one key to the next. - keystep: integer, - /// With a `lastkey` of -1, divides the remaining arguments, e.g. 2 for half of them. - limit: integer, - } - - /// A number of keys, see `key-spec-find-keys`. - record key-spec-keynum { - /// The index of the argument with the number of keys, relative to the first. - keynumidx: integer, - /// The index of the first key, relative to the first. - firstkey: integer, - /// The number of arguments from one key to the next. - keystep: integer, - } - - /// Where a command's keys are and how it uses them. - record key-spec { - notes: option, - /// How the command uses the keys, e.g. `RW`, `access` and `update`. - %flags: list, - begin-search: key-spec-begin-search, - find-keys: key-spec-find-keys, - } - - /// A command, see `command` and `command-info`. - record command-info-reply { - /// The command's name, e.g. `get`, or for a subcommand, `config|get`. - name: string, - /// The number of arguments, the name included, the minimum when negative. - arity: integer, - /// The command's flags, e.g. `readonly` and `fast`. - %flags: list, - /// The index of the first key, 0 for none. - first-key: integer, - /// The index of the last key, from the end when negative. - last-key: integer, - /// The number of arguments from one key to the next. - step: integer, - /// The ACL categories of the command, e.g. `@read`. - acl-categories: list, - /// Hints for clients, e.g. `nondeterministic_output`. - tips: list, - key-specs: list, - } - - /// The type of an argument of a command, see `command-argument`. - variant command-argument-type { - /// `string` - %string, - /// `integer` - integer, - /// `double` - double, - /// `key`: a key, see `key-spec-index`. - key, - /// `pattern`: a glob-style pattern. - pattern, - /// `unix-time` - unix-time, - /// `pure-token`: the token alone, without a value. - pure-token, - /// `oneof`: one of the arguments it contains. - oneof, - /// `block`: the arguments it contains, together. - block, - /// A type this interface does not know, e.g. from a newer server. - other(string), - } - - /// An argument of a command, see `command-docs`. - record command-argument { - /// The index of the `oneof` or `block` argument containing this one, in the command's - /// arguments, none at the top level. - parent: option, - name: string, - %type: command-argument-type, - /// The name to show for the argument. - display-text: option, - /// The index of the argument's key spec, for a key. - key-spec-index: option, - /// The token before the argument's value, e.g. `EX`, or for a `pure-token`, the token alone. - token: option, - summary: option, - /// The version that added the argument. - since: option, - deprecated-since: option, - /// The argument's flags, e.g. `optional`, `multiple` and `multiple_token`. - %flags: list, - } - - /// The documentation of a command, see `command-docs`. - record command-docs-reply { - /// The command's name, e.g. `get`, or for a subcommand, `config|get`. - name: string, - summary: string, - /// The version that added the command. - since: string, - /// The command's group, e.g. `string`. - group: string, - /// The command's time complexity, none for a few commands, e.g. MONITOR. - complexity: option, - /// The module of the command, for a module's command. - module: option, - /// The command's documentation flags, e.g. `deprecated` or `syscmd`. - doc-flags: list, - deprecated-since: option, - /// The command to use instead, for a deprecated command. - replaced-by: option, - /// The changes to the command, the version of each and what changed. - history: list>, - /// The arguments, depth first, those an argument contains after it. - arguments: list, - } - - /// Where a connection's tracking notifications are redirected, see `client-getredir` and - /// `client-trackinginfo`. - variant tracking-redirect { - /// -1: tracking is not on. - not-tracking, - /// 0: tracking is on, the notifications are not redirected. - not-redirected, - /// The ID of the connection the notifications are redirected to. - redirected(integer), - } - - /// Information about the library of a connection, see `client-setinfo`. - variant client-setinfo-attr { - /// `LIB-NAME`: the name of the library, e.g. `valkey-wasm`. - lib-name(string), - /// `LIB-VER`: the version of the library. - lib-ver(string), - } - - /// Which keys tracking tracks without CLIENT CACHING. - enum client-tracking-mode { - /// `OPTIN`: none, only those of commands after CLIENT CACHING YES. - optin, - /// `OPTOUT`: all, except those of commands after CLIENT CACHING NO. - optout, - } - - /// The options of CLIENT TRACKING ON. - record client-tracking-options { - /// `REDIRECT client-id`: sends the notifications to the connection with the ID instead. - redirect: option, - /// `PREFIX prefix`: with `bcast`, only notifies about keys with the prefixes. - prefixes: option>, - /// `BCAST`: notifies about every change of the keys with the prefixes, read or not. - bcast: bool, - mode: option, - /// `NOLOOP`: does not notify about the keys the connection changed itself. - noloop: bool, - } - - /// A connection's tracking state, see `client-trackinginfo`. - record client-trackinginfo-reply { - /// The tracking flags, e.g. `on`, `bcast`, `optin` or `noloop`. - %flags: list, - /// Where the notifications are redirected. - redirect: tracking-redirect, - /// The prefixes of BCAST. - prefixes: list, - } - - /// The filter of COMMAND LIST. - variant command-list-filter { - /// `MODULE module-name`: the commands of the module. - module(string), - /// `ACLCAT category`: the commands in the ACL category. - aclcat(string), - /// `PATTERN pattern`: the commands matching the glob-style pattern. - pattern(string), - } - /// Signals that the next command follows an ASK redirect, for a slot being imported. /// /// It applies to the next command on the connection, which may be another caller's when the @@ -1542,36 +728,6 @@ interface commands { // geo commands - /// A position on the Earth. - /// - /// The exact limits, as specified by EPSG:900913 / EPSG:3785 / OSGEO:41001 - record geo-coordinates { - /// The longitude, from -180 to 180 degrees. - longitude: double, - /// The latitude, from -85.05112878 to 85.05112878 degrees. - latitude: double, - } - - /// A unit of distance. - enum geo-unit { - /// `m`: meters. - m, - /// `km`: kilometers. - km, - /// `ft`: feet. - ft, - /// `mi`: miles. - mi, - } - - /// Only add or only update members, GEOADD's `NX` and `XX`. - enum geoadd-condition { - /// Only add new members, never update existing ones. - nx, - /// Only update existing members, never add new ones. - xx, - } - /// Adds the members at their positions to the geospatial index stored at key, or updates their /// positions, returning how many were added, or with `changed`, added or updated. /// @@ -1597,68 +753,6 @@ interface commands { /// https://valkey.io/commands/geopos/ geopos: async func(connection: borrow, key: string, members: option>) -> result>, error>; - /// The center of a search, `FROMMEMBER` or `FROMLONLAT`. - variant geo-from { - /// `FROMMEMBER member`: the position of a member of the index. - member(string), - /// `FROMLONLAT longitude latitude` - lonlat(geo-coordinates), - } - - /// `BYRADIUS`: a circle around the center. - record geo-radius { - %from: geo-from, - radius: double, - unit: geo-unit, - } - - /// `BYBOX`: a rectangle around the center. - record geo-box { - %from: geo-from, - width: double, - height: double, - unit: geo-unit, - } - - /// The area of a search. - variant geo-shape { - /// `[ FROMMEMBER member | FROMLONLAT longitude latitude ] BYRADIUS radius unit` - radius(geo-radius), - /// `[ FROMMEMBER member | FROMLONLAT longitude latitude ] BYBOX width height unit` - box(geo-box), - /// `BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ]`: the polygon with - /// the vertices, in order, which has no center. - polygon(list), - } - - /// At most a count of the members, `COUNT count [ ANY ]`. - record geo-count { - count: integer, - /// `ANY`: returns as soon as enough members are found, rather than the nearest ones. - any: bool, - } - - /// The details GEOSEARCH returns of each member it found. - flags geosearch-with-flags { - /// `WITHCOORD`: the position. - withcoord, - /// `WITHDIST`: the distance from the center. - withdist, - /// `WITHHASH`: the geohash, as an integer. - withhash, - } - - /// A member a search found, with the details asked for. - record geo-search-result { - member: string, - /// `WITHDIST`: the distance from the center, in the unit of the shape. - distance: option, - /// `WITHHASH`: the geohash, as an integer. - hash: option, - /// `WITHCOORD`: the position. - coordinates: option, - } - /// The members of the geospatial index stored at key in the shape. /// /// See also `geosearch-with`. @@ -1779,16 +873,6 @@ interface commands { /// https://valkey.io/commands/hmset/ hmset: async func(connection: borrow, key: string, field-and-values: list>) -> result<_, error>; - /// The outcome of removing the expiration of a hash field. - enum field-persist-result { - /// -2: the field does not exist, or the hash does not exist. - does-not-exist, - /// -1: the field exists, but has no expiration. - no-expiration, - /// 1: the expiration was removed. - removed, - } - /// Removes the expiration of the hash fields. /// /// HPERSIST key FIELDS numfields field [ field ... ] @@ -1867,22 +951,6 @@ interface commands { /// https://valkey.io/commands/hset/ hset: async func(connection: borrow, key: string, field-and-values: list>) -> result; - /// Only set the fields when the hash does, or does not, exist, HSETEX's `NX` and `XX`. - enum hsetex-condition { - /// Only set the fields when the hash does not exist. - nx, - /// Only set the fields when the hash exists. - xx, - } - - /// Only set the fields when none, or all, of them exist, HSETEX's `FNX` and `FXX`. - enum hsetex-field-condition { - /// Only set the fields when none of them exist. - fnx, - /// Only set the fields when all of them exist. - fxx, - } - /// Sets the fields to their values in the hash stored at key, optionally setting their /// expiration, returning whether they were set, false when a condition prevented it. /// @@ -1950,40 +1018,6 @@ interface commands { // keyspace commands - /// The type of the value stored at a key. - variant key-type { - /// `string` - %string, - /// `list` - %list, - /// `set` - set, - /// `zset`: a sorted set. - zset, - /// `hash` - hash, - /// `stream` - %stream, - /// The type of a module, by name. - other(string), - } - - /// The options of MIGRATE. - flags migrate-flags { - /// `COPY`: keeps the keys in this instance. - copy, - /// `REPLACE`: replaces keys that exist in the other instance. - replace, - } - - /// The credentials to authenticate to the other instance with, for MIGRATE. - variant migrate-auth { - /// `AUTH password` - password(string), - /// `AUTH2 username password` - username-password(tuple), - } - /// Removes all the keys of the slot, in the mode, the server's `lazyfree-lazy-user-flush` /// without one. /// @@ -2203,32 +1237,6 @@ interface commands { // list commands - /// An end of a list. - enum list-side { - /// `LEFT`: the head, the first element. - left, - /// `RIGHT`: the tail, the last element. - right, - } - - /// Where LINSERT inserts an element, relative to the pivot. - enum linsert-position { - /// `BEFORE`: in front of the pivot. - before, - /// `AFTER`: behind the pivot. - after, - } - - /// The outcome of LINSERT. - variant linsert-result { - /// The element was inserted, the length of the list after it. - inserted(integer), - /// 0: the key does not exist, nothing was inserted. - no-such-key, - /// -1: the pivot is not in the list, nothing was inserted. - pivot-not-found, - } - /// Moves an element from one end of the source list to an end of the destination list, waiting /// up to the timeout for the source to have one, returning it. /// @@ -2549,14 +1557,6 @@ interface commands { /// https://valkey.io/commands/function-delete/ function-delete: async func(connection: borrow, library-name: string) -> result<_, error>; - /// Whether a flush happens in the background, `ASYNC`, or before replying, `SYNC`. - enum flush-mode { - /// `ASYNC` - %async, - /// `SYNC` - sync, - } - /// Deletes all the libraries, in the mode, the server's `lazyfree-lazy-user-flush` without one. /// /// FUNCTION FLUSH [ ASYNC | SYNC ] @@ -2569,24 +1569,6 @@ interface commands { /// https://valkey.io/commands/function-kill/ function-kill: async func(connection: borrow) -> result<_, error>; - /// A function of a library. - record library-function { - name: string, - description: option, - /// The function's flags, e.g. `no-writes`. - %flags: list, - } - - /// A library of functions. - record library { - library-name: string, - /// The engine that runs the library, e.g. `LUA`. - engine: string, - functions: list, - /// The library's source code, with `withcode`. - library-code: option, - } - /// The libraries, those with names matching the glob-style pattern with one. /// /// FUNCTION LIST [ LIBRARYNAME library-name-pattern ] [ WITHCODE ] @@ -2600,29 +1582,6 @@ interface commands { /// https://valkey.io/commands/function-load/ function-load: async func(connection: borrow, replace: bool, function-code: string) -> result; - /// The function that is running. - record running-function { - name: string, - /// The command and its arguments that called the function. - command: list, - /// How long the function has been running. - duration: duration-milliseconds, - } - - /// The libraries and functions of an engine. - record engine-stats { - libraries-count: integer, - functions-count: integer, - } - - /// The function that is running, and the libraries and functions of each engine. - record function-stats-reply { - /// None when no function is running. - running-script: option, - /// The statistics of each engine, by name. - engines: map, - } - /// The function that is running, and the libraries and functions of each engine. /// /// FUNCTION STATS @@ -2665,7 +1624,6 @@ interface commands { // SCRIPT DEBUG, the debugger is interactive, with a protocol of its own - // set commands /// Adds the members to the set stored at key, returning how many were not already members. @@ -2785,37 +1743,6 @@ interface commands { /// https://valkey.io/commands/sunionstore/ sunionstore: async func(connection: borrow, destination: string, keys: list) -> result; - /// A range of a sorted result, `LIMIT offset count`. - record limit { - /// The number of elements skipped. - offset: integer, - /// The maximum number of elements returned, negative for all of them. - count: integer, - } - - /// How SORT orders and projects the elements of a list, set or sorted set. - record sort-opts { - /// `BY pattern`: sorts by the values of the keys the pattern names, `*` replaced with each - /// element, or `nosort` to skip sorting. - by: option, - /// `LIMIT offset count`: a range of the sorted elements. - limit: option, - /// `GET pattern`: returns the values of the keys each pattern names instead of the elements, - /// `#` for the element itself. - get: option>, - /// order to sort elements. - order: option, - /// `ALPHA`: sorts lexicographically, rather than numerically. - alpha: bool, - } - - enum sort-order { - /// ascending, smallest to largest. - asc, - /// descending, largest to smallest. - desc, - } - /// Sorts the elements of the list, set or sorted set stored at key. /// /// Each row is the element, or with `get`, the value of each pattern, none when its key does not @@ -2841,79 +1768,6 @@ interface commands { // sortedset commands - /// A bound of a range of scores. - variant score-bound { - /// The score, included in the range. - inclusive(double), - /// `(score`: the score, excluded from the range. - exclusive(double), - /// `-inf` as the min, `+inf` as the max, the start is the max with `REV`. - unbounded, - } - - /// A bound of a range of members, for members of equal score, ordered lexicographically. - variant lex-bound { - /// `[member`: the member, included in the range. - inclusive(string), - /// `(member`: the member, excluded from the range. - exclusive(string), - /// `-` as the min, `+` as the max, the start is the max with `REV`. - unbounded, - } - - /// Which end of a sorted set to pop from. - enum pop-end { - /// The members with the lowest scores. - min, - /// The members with the highest scores. - max, - } - - /// How scores combine for a member in several sorted sets. - enum aggregate { - /// The sum of the weighted scores, the default. - sum, - /// The lowest weighted score. - min, - /// The highest weighted score. - max, - } - - /// The range of ZRANGE and ZRANGESTORE, `start` and `stop`, given as they are sent: with `REV`, - /// the higher bound first. - variant zrange-by { - /// Positions, both included, negative from the end. - rank(tuple), - /// `BYSCORE`: the members with scores in the range. - score(tuple), - /// `BYLEX`: the members in the range, for members of equal score. - lex(tuple), - } - - /// The range of ZRANGE WITHSCORES, which does not support BYLEX, see `zrange-by`. - variant zrange-withscores-by { - /// Positions, both included, negative from the end. - rank(tuple), - /// `BYSCORE`: the members with scores in the range. - score(tuple), - } - - /// Only update or only add members, ZADD's `NX` and `XX`. - enum zadd-condition { - /// Only add new members, never update existing ones. - nx, - /// Only update existing members, never add new ones. - xx, - } - - /// Only update a member's score when the new score is greater or less, ZADD's `GT` and `LT`. - enum zadd-comparison { - /// Only update when the new score is greater than the current one. - gt, - /// Only update when the new score is less than the current one. - lt, - } - /// Removes the members with the lowest or highest scores from the first non-empty sorted set, /// waiting up to the timeout for one, returning its key and the members and their scores. /// @@ -3241,48 +2095,6 @@ interface commands { // stream commands - /// The ID of a stream entry, `-`, e.g. `1526919030474-55`. - /// - /// Commands that add an entry also accept `*`, for the server to generate the ID. - type stream-id = string; - - /// How a stream is trimmed, e.g. with XADD or XTRIM. - variant stream-trim { - /// Evicts entries as long as the stream's length exceeds the specified threshold, where threshold is a positive integer. - maxlen(stream-trim-maxlen), - /// Evicts entries with IDs lower than threshold, where threshold is a stream ID. - minid(stream-trim-minid), - } - - /// Trims the stream to a maximum length, `MAXLEN [ = | ~ ] threshold [ LIMIT count ]`. - record stream-trim-maxlen { - /// Exact, `=`, when none. - efficiency: option, - /// The maximum number of entries kept. - threshold: integer, - /// The maximum number of entries evicted, only with nearly exact trimming, `0` for no limit. - limit: option, - } - - /// Trims the entries with lower IDs, `MINID [ = | ~ ] threshold [ LIMIT count ]`. - record stream-trim-minid { - /// Exact, `=`, when none. - efficiency: option, - /// The lowest ID kept. - threshold: stream-id, - /// The maximum number of entries evicted, only with nearly exact trimming, `0` for no limit. - limit: option, - } - - /// How exactly a stream is trimmed. Nearly exact trimming is more efficient, the stream may keep a - /// few entries more than the threshold. - enum stream-trim-efficiency { - /// `=`: exact trimming - exact, - /// `~`: nearly exact trimming - mearly-exact, - } - /// Removes one or multiple messages from the Pending Entries List (PEL) of a stream consumer group. /// /// XACK key group ID [ ID ... ] @@ -3320,22 +2132,6 @@ interface commands { xautoclaim-justid: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, start: option, count: option) -> result, list, list>, error>; - /// Options of XCLAIM. - record xclaim-opts { - /// `IDLE ms`: sets the idle time of the entries, the time since they were last delivered, instead - /// of resetting it. - idle: option, - /// `TIME unix-time-milliseconds`: sets the idle time of the entries to the time since this instant. - time: option, - /// `RETRYCOUNT count`: sets the delivery count of the entries, instead of incrementing it. - retrycount: option, - /// `FORCE`: claims the entries even when they are not pending for any consumer, as long as they - /// exist in the stream. - force: bool, - /// `LASTID lastid`: sets the ID of the last entry delivered to the group. - lastid: option, - } - /// Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument. /// /// Also see `xclaim-justid` @@ -3388,73 +2184,18 @@ interface commands { /// https://valkey.io/commands/xgroup-setid/ xgroup-setid: async func(connection: borrow, key: string, group: string, id: stream-id, entriesread: option) -> result<_, error>; - /// A consumer of a group, see `xinfo-consumers`. - record xinfo-consumer { - /// The consumer's name. - name: string, - /// The number of entries pending for the consumer. - pending: integer, - /// The time since the consumer last attempted an interaction, e.g. XREADGROUP, XCLAIM or - /// XAUTOCLAIM. - idle: duration-milliseconds, - /// The time since the consumer last interacted successfully, e.g. read or claimed entries. - /// None when the consumer has never been active. - inactive: option, - } - /// Consumers that belong to the consumer group of the stream stored at . /// /// XINFO CONSUMERS key group /// https://valkey.io/commands/xinfo-consumers/ xinfo-consumers: async func(connection: borrow, key: string, group: string) -> result, error>; - /// A consumer group of a stream, see `xinfo-groups`. - record xinfo-group { - /// The group's name. - name: string, - /// The number of consumers in the group. - consumers: integer, - /// The number of entries in the group's pending entries list (PEL), delivered but not acknowledged. - pending: integer, - /// The ID of the last entry delivered to the group's consumers. - last-delivered-id: stream-id, - /// The logical read counter of the last entry delivered to the group's consumers, none when unknown. - entries-read: option, - /// The number of entries in the stream not yet delivered to the group's consumers, none when it - /// cannot be determined. - lag: option, - } - /// Consumer groups of the stream stored at . /// /// XINFO GROUPS key /// https://valkey.io/commands/xinfo-groups/ xinfo-groups: async func(connection: borrow, key: string) -> result, error>; - /// Information about a stream, see `xinfo-stream`. - record xinfo-stream-reply { - /// The number of entries in the stream. - length: integer, - /// The number of keys in the radix tree storing the stream. - radix-tree-keys: integer, - /// The number of nodes in the radix tree storing the stream. - radix-tree-nodes: integer, - /// The number of consumer groups of the stream. - groups: integer, - /// The ID of the last entry added to the stream, none when no entry was ever added. - last-generated-id: option, - /// The highest ID deleted from the stream, none when no entry was ever deleted. - max-deleted-entry-id: option, - /// The number of entries added to the stream over its lifetime. - entries-added: integer, - /// The ID and fields of the first entry. - /// None when the stream is empty. - first-entry: option>>, - /// The ID and fields of the last entry. - /// None when the stream is empty. - last-entry: option>>, - } - /// Information about the stream stored at . /// /// See also `xinfo-stream-full` @@ -3463,62 +2204,6 @@ interface commands { /// https://valkey.io/commands/xinfo-stream/ xinfo-stream: async func(connection: borrow, key: string) -> result; - /// Information about a stream, its entries and consumer groups, see `xinfo-stream-full`. - record xinfo-stream-reply-full { - /// The number of entries in the stream. - length: integer, - /// The number of keys in the radix tree storing the stream. - radix-tree-keys: integer, - /// The number of nodes in the radix tree storing the stream. - radix-tree-nodes: integer, - /// The ID of the last entry added to the stream, none when no entry was ever added. - last-generated-id: option, - /// The highest ID deleted from the stream, none when no entry was ever deleted. - max-deleted-entry-id: option, - /// The number of entries added to the stream over its lifetime. - entries-added: integer, - /// The ID and fields of the entries, in ascending ID order, up to the count given. - entries: list>>, - /// The consumer groups of the stream. - groups: list, - } - - /// A consumer group of a stream, with its pending entries and consumers. - record xinfo-stream-reply-full-group { - /// The group's name. - name: string, - /// The ID of the last entry delivered to the group's consumers. - last-delivered-id: stream-id, - /// The logical read counter of the last entry delivered to the group's consumers, none when unknown. - entries-read: option, - /// The number of entries in the stream not yet delivered to the group's consumers, none when it - /// cannot be determined. - lag: option, - /// The number of entries in the group's pending entries list (PEL), delivered but not acknowledged. - pel-count: integer, - /// The group's pending entries, the ID of each, the consumer it was delivered to, when it was - /// last delivered and the number of times it was delivered. - pending: list>, - /// The group's consumers. - consumers: list, - } - - /// A consumer of a group, with its pending entries. - record xinfo-stream-reply-full-consumer { - /// The consumer's name. - name: string, - /// When the consumer last attempted an interaction, e.g. XREADGROUP, XCLAIM or XAUTOCLAIM. - seen-time: unix-time-milliseconds, - /// When the consumer last interacted successfully, e.g. read or claimed entries. - /// None when the consumer has never been active. - active-time: option, - /// The number of entries pending for the consumer, delivered but not acknowledged. - pel-count: integer, - /// The consumer's pending entries, the ID of each, when it was last delivered and the number of - /// times it was delivered. - pending: list>, - } - /// Information about the stream stored at . /// /// See also `xinfo-stream` @@ -3533,18 +2218,6 @@ interface commands { /// https://valkey.io/commands/xlen/ xlen: async func(connection: borrow, key: string) -> result; - /// The pending entries of a consumer group, see `xpending`. - record xpending-summary { - /// The number of pending entries. - count: integer, - /// The lowest ID of the pending entries, none when there are none. - lowest-id: option, - /// The highest ID of the pending entries, none when there are none. - highest-id: option, - /// Each consumer with pending entries, and how many it has. - consumers: list>, - } - /// List of pending messages. /// /// See also `xpending-extended`. @@ -3553,18 +2226,6 @@ interface commands { /// https://valkey.io/commands/xpending/ xpending: async func(connection: borrow, key: string, group: string) -> result; - /// The pending entries of a consumer group, see `xpending-extended`. - record xpending-extended-entry { - /// The ID of the message. - id: stream-id, - /// The name of the consumer that fetched the message and has still to acknowledge it. We call it the current owner of the message. - name: string, - /// The number of milliseconds that elapsed since the last time this message was delivered to this consumer. - since-delivery: duration-milliseconds, - /// The number of times this message was delivered. - delivery-count: integer, - } - /// List of pending messages. /// /// See also `xpending`. @@ -3653,20 +2314,6 @@ interface commands { /// https://valkey.io/commands/getdel/ getdel: async func(connection: borrow, key: string) -> result, error>; - /// The expiration to set on a key, see `getex`. - variant getex-ttl { - /// Set the specified expire time, in seconds. - ex(duration-seconds), - /// Set the specified expire time, in milliseconds. - px(duration-milliseconds), - /// Set the specified Unix time at which the key will expire, in seconds. - exat(unix-time-seconds), - /// Set the specified Unix time at which the key will expire, in milliseconds. - pxat(unix-time-milliseconds), - /// Remove the time to live associated with the key. - persist, - } - /// Get the value of key and optionally set its expiration. /// /// GETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ] @@ -3719,24 +2366,6 @@ interface commands { /// https://valkey.io/commands/lcs/ lcs-len: async func(connection: borrow, key1: string, key2: string) -> result; - /// The longest common subsequence of two strings, as the ranges matching in each, see `lcs-idx`. - record lcs-idx-reply { - /// The ranges that match, from the end of the strings to their start. - matches: list, - /// The length of the longest common subsequence. - len: integer, - } - - /// A part of the longest common subsequence. - record lcs-idx-match { - /// The inclusive start and end positions of the match in the first key's string. - key1-range: tuple, - /// The inclusive start and end positions of the match in the second key's string. - key2-range: tuple, - /// The length of the match, with `with-match-len`. - len: option, - } - /// The LCS command implements the longest common subsequence algorithm. /// /// See also `lcs` and `lcs-len` @@ -3757,17 +2386,6 @@ interface commands { /// https://valkey.io/commands/mset/ mset: async func(connection: borrow, keys-and-values: list>) -> result<_, error>; - /// The condition for setting the keys, see `msetex`. - variant msetex-precondition { - /// Only set the keys if all of them do not already exist. - nx, - /// Only set the keys if all of them already exist. - xx, - } - - /// The expiration to set on the keys, see `msetex`. - type msetex-ttl = set-ttl; - /// Sets multiple key-value pairs while also assigning a shared expiration time to all the keys atomically. /// /// MSETEX numkeys key value [ key value ... ] [ NX | XX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] @@ -3786,32 +2404,6 @@ interface commands { /// https://valkey.io/commands/psetex/ psetex: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, value: string) -> result<_, error>; - /// The condition for setting a key, see `set`. - variant set-precondition { - /// Only set the key if it does not already exist. - nx, - /// Only set the key if it already exists. - xx, - /// Set the key if the comparison value matches the existing value. - ifeq(string), - /// Set the key if the comparison value does not match the existing value. - ifne(string), - } - - /// The expiration to set on a key, see `set`. - variant set-ttl { - /// Set the specified expire time, in seconds. - ex(duration-seconds), - /// Set the specified expire time, in milliseconds. - px(duration-milliseconds), - /// Set the specified Unix time at which the key will expire, in seconds. - exat(unix-time-seconds), - /// Set the specified Unix time at which the key will expire, in milliseconds. - pxat(unix-time-milliseconds), - /// Retain the time to live associated with the keys. - keepttl, - } - /// Set key to hold the string value. /// /// Returns false when the precondition is not met, the key is unchanged. diff --git a/wit/connection.wit b/wit/connection.wit index f6a3a0d..436bed2 100644 --- a/wit/connection.wit +++ b/wit/connection.wit @@ -9,32 +9,7 @@ /// with them, push messages, and transactions. Higher level, typed operations are functions in the /// `commands` interface that borrow the connection. interface connection { - use wasi:clocks/system-clock@0.3.0.{duration, instant}; - use resp.{error as resp-error, value, integer, double}; - - /// The errors of a connection and of every command interface. - variant error { - /// The transport closed while a reply was outstanding, or before the command was sent. Holds - /// the transport's error when it did not close gracefully. - closed(option), - /// Client errors are semantic issues with input/output values, e.g. a reply that does not - /// have the shape the command expects. - client(string), - /// RESP errors are encoding issues converting values to/from bytes. - resp(resp-error), - /// Valkey errors are Error or BulkError values returned directly from the server. - valkey(string), - } - - /// Options for establishing a connection. - record connect-opts { - /// The username and password to authenticate with. - auth: option>, - /// The name of the connection, see `CLIENT SETNAME`. - client-name: option, - /// Enables keep-alive pings at this interval, see `connection.set-keep-alive`. - keep-alive-interval: option, - } + use types.{connect-opts, duration-milliseconds, error, value}; resource connection { /// Establishes a connection over a transport. @@ -134,6 +109,6 @@ interface connection { /// /// Unlike TCP keep-alive, a PING counts as activity for the server's idle client timeout, /// and works over any transport. - set-keep-alive: func(interval: option); + set-keep-alive: func(interval: option); } } diff --git a/wit/connector.wit b/wit/connector.wit index abfe5f3..3414a4e 100644 --- a/wit/connector.wit +++ b/wit/connector.wit @@ -4,7 +4,8 @@ /// A component that needs a connection, but not a say in how it is made, imports a connector, the /// composition decides which one, e.g. `tcp-connector`. interface connector { - use connection.{connection, error}; + use types.{error}; + use connection.{connection}; /// Establishes a new connection. connect: async func() -> result; diff --git a/wit/resp.wit b/wit/resp.wit index ebde634..b77c199 100644 --- a/wit/resp.wit +++ b/wit/resp.wit @@ -1,30 +1,6 @@ +/// Encodes and decodes RESP values. interface resp { - type error = string; - - type integer = s64; - type double = f64; - - variant value { - %string(string), - error(string), - integer(integer), - bulk-string(string), - array(list), - null, - boolean(bool), - double(double), - big-number(string), - bulk-error(string), - verbatim-string(tuple), - %map(list>), - set(list), - push(list), - } - - /// Values nested within an aggregate type. Since WIT types are not allowed to depend on - /// themselves, nested values are stored as encoded RESP. - type nested-value = list; - + use types.{resp-error as error, nested-value, value}; decode: func(data: list) -> result; encode: func(value: value) -> result, error>; } diff --git a/wit/tcp.wit b/wit/tcp.wit index c9a8405..4ec8d41 100644 --- a/wit/tcp.wit +++ b/wit/tcp.wit @@ -1,7 +1,7 @@ /// A transport for connections over wasi:sockets TCP. interface tcp { - use resp.{value}; - use connection.{connection, connect-opts, error}; + use types.{connect-opts, error, value}; + use connection.{connection}; /// A TCP connection to a Valkey server, framing the bytes it carries into RESP values in each /// direction. diff --git a/wit/types.wit b/wit/types.wit new file mode 100644 index 0000000..52756db --- /dev/null +++ b/wit/types.wit @@ -0,0 +1,1519 @@ +/// The types of the package's interfaces, their stable public names. +/// +/// The other interfaces use these types under names of their own, which may change, e.g. renamed +/// to avoid clashing with a function for a new command. Refer to a type through this interface for +/// a name that does not change. +interface types { + // resp + + /// An encoding error, converting RESP values to or from bytes. + type resp-error = string; + + type integer = s64; + + type double = f64; + + variant value { + %string(string), + error(string), + integer(integer), + bulk-string(string), + array(list), + null, + boolean(bool), + double(double), + big-number(string), + bulk-error(string), + verbatim-string(tuple), + %map(list>), + set(list), + push(list), + } + + /// Values nested within an aggregate type. Since WIT types are not allowed to depend on + /// themselves, nested values are stored as encoded RESP. + type nested-value = list; + + // connection + + /// The errors of a connection and of every command interface. + variant error { + /// The transport closed while a reply was outstanding, or before the command was sent. Holds + /// the transport's error when it did not close gracefully. + closed(option), + /// Client errors are semantic issues with input/output values, e.g. a reply that does not + /// have the shape the command expects. + client(string), + /// RESP errors are encoding issues converting values to/from bytes. + resp(resp-error), + /// Valkey errors are Error or BulkError values returned directly from the server. + valkey(string), + } + + /// Options for establishing a connection. + record connect-opts { + /// The username and password to authenticate with. + auth: option>, + /// The name of the connection, see `CLIENT SETNAME`. + client-name: option, + /// Enables keep-alive pings at this interval, see `connection.set-keep-alive`. + keep-alive-interval: option, + } + + /// A duration in whole seconds, e.g. `EX seconds`. + record duration-seconds { + ex: u64, + } + + /// A duration in whole milliseconds, e.g. `PX milliseconds`. + record duration-milliseconds { + px: u64, + } + + /// A point in time as whole seconds since the Unix epoch, e.g. `EXAT unix-time-seconds`. + record unix-time-seconds { + exat: s64, + } + + /// A point in time as whole milliseconds since the Unix epoch, e.g. `PXAT + /// unix-time-milliseconds`. + record unix-time-milliseconds { + pxat: s64, + } + + /// Where an incremental iteration continues from, e.g. with HSCAN. + /// + /// A scan returns the cursor to resume from, always `resume`, none once the scan is complete. + /// Pass it to the next call until then. + variant cursor { + /// Starts a new scan, the cursor `0`. + start, + /// Continues a scan from the opaque cursor the previous call returned, `0` fails with a client + /// error, use `start`. + resume(string), + } + + /// The condition for setting an expiration, e.g. with EXPIRE or HEXPIRE. + enum ttl-mode { + /// Set expiration only when the field has no expiration. + nx, + /// Set expiration only when the field has an existing expiration. + xx, + /// Set expiration only when the new expiration is greater than current one. + gt, + /// Set expiration only when the new expiration is less than current one. + lt, + } + + /// The outcome of setting the expiration of a hash field, e.g. with HEXPIRE. + enum ttl-result { + /// -2: key does not exist, or field does not exist in the HASH. + does-not-exist, + /// 0: the specified NX | XX | GT | LT condition has not been met. + condition-not-met, + /// 1: the expiration time was applied. + applied, + /// 2: when called with 0 seconds. + zero-seconds, + } + + /// The remaining time to live of a key or hash field, to the second, see `ttl` and `httl`. + variant ttl-reply { + /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. + does-not-exist, + /// -1: the key or field exists, but has no expiration. + no-expiration, + /// The time until it expires. + expires-in(duration-seconds), + } + + /// The remaining time to live of a key or hash field, to the millisecond, see `pttl` and `hpttl`. + variant pttl-reply { + /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. + does-not-exist, + /// -1: the key or field exists, but has no expiration. + no-expiration, + /// The time until it expires. + expires-in(duration-milliseconds), + } + + /// When a key or hash field expires, to the second, see `expiretime` and `hexpiretime`. + variant expiretime-reply { + /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. + does-not-exist, + /// -1: the key or field exists, but has no expiration. + no-expiration, + /// The time at which it expires. + expires-at(unix-time-seconds), + } + + /// When a key or hash field expires, to the millisecond, see `pexpiretime` and `hpexpiretime`. + variant pexpiretime-reply { + /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. + does-not-exist, + /// -1: the key or field exists, but has no expiration. + no-expiration, + /// The time at which it expires. + expires-at(unix-time-milliseconds), + } + + // admin commands + + /// Whether a user may run a command, see `acl-dryrun`. + variant acl-dryrun-result { + /// The user may run the command. + allowed, + /// The user may not run the command, and why. + denied(string), + } + + /// A security event, a command or authentication the rules denied, see `acl-log`. + record acl-log-entry { + /// The number of events like this one, within 60 seconds of each other. + count: integer, + /// What was denied, e.g. `command`, `key`, `channel` or `auth`. + reason: string, + /// Where it was denied, e.g. `toplevel`, `multi` or `lua`. + context: string, + /// The command, key or channel that was denied. + object: string, + username: string, + /// The seconds since the latest of these events. + age-seconds: double, + /// The connection, in the format of CLIENT LIST. + client-info: string, + /// The ID of the entry, since Valkey 7.2. + entry-id: option, + /// When the first of these events happened, since Valkey 7.2. + timestamp-created: option, + /// When the latest of these events happened, since Valkey 7.2. + timestamp-last-updated: option, + } + + /// What BGSAVE does. + enum bgsave-operation { + /// `SCHEDULE`: saves once the rewrite of the append-only file in progress is done. + schedule, + /// `CANCEL`: cancels the save in progress, or scheduled. + cancel, + } + + /// The type of a connection, for the `client-filter`. + enum client-type { + /// `normal` + normal, + /// `master`: the connection to the primary of a replica. + primary, + /// `replica` + replica, + /// `pubsub` + pubsub, + } + + /// A filter of connections, for CLIENT KILL and CLIENT LIST. + variant client-filter { + /// `ID client-id [ client-id ... ]` + id(list), + /// `TYPE` + %type(client-type), + /// `USER username` + user(string), + /// `ADDR ip:port`: the address of the client. + addr(string), + /// `LADDR ip:port`: the address of the server the client connected to. + laddr(string), + /// `SKIPME YES | NO`: whether to skip this connection, yes by default for CLIENT KILL. + skipme(bool), + /// `MAXAGE`: connections older than the time. + maxage(duration-seconds), + /// `NAME` + name(string), + /// `IDLE`: connections idle for at least the time. + idle(duration-seconds), + /// `FLAGS`: connections with all the flags, e.g. `N`. + %flags(string), + /// `LIB-NAME` + lib-name(string), + /// `LIB-VER` + lib-ver(string), + /// `DB` + db(integer), + /// `CAPA`: connections with the capability, e.g. `redirect`. + capa(string), + /// `IP` + ip(string), + /// `NOT-ID client-id [ client-id ... ]` + not-id(list), + /// `NOT-TYPE` + not-type(client-type), + /// `NOT-USER` + not-user(string), + /// `NOT-ADDR` + not-addr(string), + /// `NOT-LADDR` + not-laddr(string), + /// `NOT-NAME` + not-name(string), + /// `NOT-FLAGS` + not-flags(string), + /// `NOT-LIB-NAME` + not-lib-name(string), + /// `NOT-LIB-VER` + not-lib-ver(string), + /// `NOT-DB` + not-db(integer), + /// `NOT-CAPA` + not-capa(string), + /// `NOT-IP` + not-ip(string), + } + + /// Which commands CLIENT PAUSE suspends. + enum client-pause-mode { + /// `WRITE`: commands that may write. + write, + /// `ALL`: every command, the default. + all, + } + + /// How CLIENT UNBLOCK unblocks a connection. + enum client-unblock-type { + /// `TIMEOUT`: as if the command timed out, the default. + timeout, + /// `ERROR`: with an `UNBLOCKED` error. + error, + } + + /// The outcome of CLUSTER BUMPEPOCH. + variant cluster-bumpepoch-result { + /// `BUMPED`: the config epoch was advanced, to this one. + bumped(integer), + /// `STILL`: the config epoch is already the greatest in the cluster, this one. + still(integer), + } + + /// How CLUSTER FAILOVER fails over. + enum cluster-failover-mode { + /// `FORCE`: without the agreement of the primary, which may be down. + force, + /// `TAKEOVER`: without the agreement of the primary or the rest of the cluster. + takeover, + } + + /// Which way a slot migration moves the slots. + enum slot-migration-operation { + /// `IMPORT`: to this node. + %import, + /// `EXPORT`: from this node. + %export, + } + + /// A slot migration, see `cluster-getslotmigrations`. + record slot-migration { + /// The ID of the migration. + link-name: string, + operation: slot-migration-operation, + /// The ranges of slots, start and end both included. + slot-ranges: list>, + /// The ID of the node the slots move to. + target-node: string, + /// The ID of the node the slots move from. + source-node: string, + create-time: unix-time-seconds, + last-update-time: unix-time-seconds, + last-ack-time: unix-time-seconds, + /// The state of the migration, for people to read. + state: string, + /// More details of the state, for people to read. + message: string, + /// The bytes copied on write while forked for the migration. + cow-size: integer, + } + + /// Ranges of slots to migrate to a node, see `cluster-migrateslots`. + record slot-migration-group { + /// The ranges of slots, start and end both included. + ranges: list>, + /// The ID of the node to migrate the slots to. + node-id: string, + /// `AUTH username password`: the credentials for the node, since Valkey 9.2. + auth: option>, + } + + /// How CLUSTER RESET resets a node. + enum cluster-reset-mode { + /// `HARD`: also generates a new node ID and resets the epochs. + hard, + /// `SOFT`: the default. + soft, + } + + /// The state to set a slot to, see `cluster-setslot`. + variant cluster-setslot-state { + /// `IMPORTING node-id`: importing the slot from the node. + importing(string), + /// `MIGRATING node-id`: migrating the slot to the node. + migrating(string), + /// `NODE node-id`: assigned to the node. + node(string), + /// `STABLE`: neither importing nor migrating. + stable, + } + + /// A command log, see `commandlog-get`. + enum commandlog-type { + /// `slow`: commands that took longer than `commandlog-execution-slower-than`. + slow, + /// `large-request`: commands larger than `commandlog-request-larger-than`. + large-request, + /// `large-reply`: replies larger than `commandlog-reply-larger-than`. + large-reply, + } + + /// An entry of the slow log or a command log. + record log-entry { + /// The ID of the entry, unique until the server restarts. + id: integer, + /// When the command ran. + timestamp: unix-time-seconds, + /// The microseconds the command took, or for the large logs, the bytes of the request or + /// reply. + value: integer, + /// The command and its arguments, long ones shortened. + arguments: list, + /// The client's address, `ip:port`. + client-address: string, + /// The client's name, see CLIENT SETNAME, empty when it has none. + client-name: string, + } + + /// Where FAILOVER fails over to. + record failover-target { + host: string, + port: u16, + /// `FORCE`: fails over at the timeout, even before the replica has caught up. + force: bool, + } + + /// The latency histogram of a command, see `latency-histogram`. + record latency-histogram-entry { + /// The number of times the command ran. + calls: integer, + /// The cumulative counts of the calls that took up to each number of microseconds, a power + /// of 2. + histogram-usec: list>, + } + + /// The latest sample of the latency of an event, see `latency-latest`. + record latency-latest-entry { + event: string, + /// When the latest sample was taken. + timestamp: unix-time-seconds, + latest: duration-milliseconds, + /// The highest latency since the server started, or the event was reset. + max: duration-milliseconds, + /// The sum of the latencies of the samples. + sum: duration-milliseconds, + /// The number of samples. + count: integer, + } + + /// A loaded module, see `module-list`. + record module-info { + name: string, + /// The module's version. + ver: integer, + /// The path the module was loaded from. + path: string, + /// The arguments the module was loaded with. + args: list, + } + + /// A replica of a primary, see `role`. + record role-replica-info { + ip: string, + port: u16, + /// The replication offset the replica last acknowledged. + offset: integer, + } + + /// The replication state of a primary, see `role`. + record role-primary { + /// The primary's replication offset. + offset: integer, + replicas: list, + } + + /// The replication state of a replica, see `role`. + record role-replica { + primary-host: string, + primary-port: u16, + /// The state of replication, e.g. `connected`, `connecting` or `sync`. + state: string, + /// The replication offset of the data received from the primary, -1 when not connected. + offset: integer, + } + + /// The server's role in replication, see `role`. + variant role-reply { + /// `master` + primary(role-primary), + /// `slave` + replica(role-replica), + /// `sentinel`: the names of the primaries it monitors. + sentinel(list), + } + + /// The modifiers of SHUTDOWN. + flags shutdown-flags { + /// `NOW`: does not wait for lagging replicas. + now, + /// `FORCE`: ignores errors that would prevent the shutdown, e.g. failing to save. + force, + /// `SAFE`: refuses to shut down while it would lose the cluster's quorum of primaries. + safe, + /// `FAILOVER`: fails over to a replica first. + failover, + } + + /// Whether SHUTDOWN saves to disk. + enum shutdown-save { + /// `NOSAVE`: does not save, even with save points configured. + nosave, + /// `SAVE`: saves, even without save points configured. + save, + } + + /// A user and its rules, see `acl-list`. + record acl-list-entry { + username: string, + /// The user's rules, as the server normalized them, e.g. `allkeys` is `~*`. Applied with + /// `acl-setuser`, after `reset`, they recreate the user. + rules: list, + } + + /// A rule of an ACL user, see `acl-setuser`. + /// + /// https://valkey.io/topics/acl/ + variant rule { + /// The keys the user may access. + keys(key-rule), + /// The Pub/Sub channels the user may access. + channel(channel-rule), + /// The commands the user may call. + command(command-rule), + /// The databases the user may access. + db(db-rule), + /// The user itself, e.g. whether it is enabled and its passwords. + user(user-rule), + /// A rule this interface does not know, e.g. from a newer server, sent as is. + other(string), + } + + /// A rule of a selector, a set of permissions matched independently of the user's root permissions, + /// the user may run a command when either permits it. + variant rule-selector { + /// The keys the selector permits access to. + keys(key-rule), + /// The Pub/Sub channels the selector permits access to. + channel(channel-rule), + /// The commands the selector permits. + command(command-rule), + /// The databases the selector permits access to. + db(db-rule), + /// A rule this interface does not know, e.g. from a newer server, sent as is. + other(string), + } + + /// A rule for the keys a user may access. + variant key-rule { + /// `~`: allows reading and writing the keys matching the glob-style pattern. + add(string), + /// `%R~`: allows reading the keys matching the glob-style pattern. + add-read(string), + /// `%W~`: allows writing the keys matching the glob-style pattern. + add-write(string), + /// `%RW~`: allows reading and writing the keys matching the glob-style pattern, the + /// same as `add`. + add-read-write(string), + /// `allkeys`: allows access to all keys, an alias for `~*`. + all, + /// `resetkeys`: removes all the allowed key patterns. + reset, + } + + /// A rule for the Pub/Sub channels a user may access. + variant channel-rule { + /// `&`: allows access to the Pub/Sub channels matching the glob-style pattern. + add(string), + /// `allchannels`: allows access to all Pub/Sub channels, an alias for `&*`. + all, + /// `resetchannels`: removes all the allowed channel patterns, disconnecting the user's Pub/Sub + /// clients that can no longer access their channels. + reset, + } + + /// A rule for the commands a user may call. + variant command-rule { + /// `+`: allows the command, or a subcommand, e.g. `config|get`. + add(string), + /// `+@`: allows all the commands in the category, e.g. `read`, see ACL CAT for the + /// categories. + add-category(string), + /// `+|`: allows the command only with the given first argument, e.g. `select` + /// with `0`. + add-first-arg(tuple), + /// `allcommands`: allows all commands, including those of modules loaded later, an alias for `+@all`. + all, + /// `-`: disallows the command, or a subcommand. + remove(string), + /// `-@`: disallows all the commands in the category. + remove-category(string), + /// `nocommands`: disallows all commands, an alias for `-@all`. + reset, + } + + /// A rule for the databases a user may access. + variant db-rule { + /// `db=[,...]`: allows access to the databases with the given IDs. + db(list), + /// `alldbs`: allows access to all databases. + all, + /// `resetdbs`: removes all the allowed databases. + reset, + } + + /// A rule for the user itself. + variant user-rule { + /// `on`: enables the user, it is possible to authenticate as this user. + on, + /// `off`: disables the user, it is no longer possible to authenticate as this user, connections + /// already authenticated keep working. + off, + /// `nopass`: removes all the user's passwords, any password then authenticates as the user. + nopass, + /// `sanitize-payload`: validates the payloads of the user's RESTORE commands, the default. + sanitize-payload, + /// `skip-sanitize-payload`: skips validating the payloads of the user's RESTORE commands. + skip-sanitize-payload, + /// `>`: adds the password to the user's passwords. + password(string), + /// `#`: adds a password, by its SHA-256 hash as 64 lowercase hex characters, to the user's + /// passwords. + hashed-password(string), + /// `<`: removes the password from the user's passwords. + remove-password(string), + /// `!`: removes a password, by its SHA-256 hash, from the user's passwords. + remove-hashed-password(string), + /// `( ...)`: adds a selector, with the given rules. + selector(list), + /// `clearselectors`: removes all the user's selectors. + clearselectors, + /// `reset`: resets the user to its state when created, disabled, without passwords, keys, + /// channels, commands or selectors. + reset, + } + + // bitmap commands + + /// A range of a string, negative from the end, for BITCOUNT and BITPOS. + variant bit-range { + /// `start`: the bytes from the start to the end of the string. + %from(integer), + /// `start end`: the bytes from the start to the end, both included. + bytes(tuple), + /// `start end BIT`: the bits from the start to the end, both included. + bits(tuple), + } + + /// The type of an integer field of a string, for BITFIELD. + variant bitfield-encoding { + /// `i1` to `i64`: a signed integer of the width in bits. + signed(u8), + /// `u1` to `u63`: an unsigned integer of the width in bits. + unsigned(u8), + } + + /// Where an integer field of a string starts, for BITFIELD. + variant bitfield-offset { + /// The offset in bits. + bit(u64), + /// `#field`: the field of the encoding's width, the offset in bits is the field times the + /// width. + field(u64), + } + + /// What a write does when the field overflows, `OVERFLOW`. + enum bitfield-overflow { + /// `WRAP`: wraps around, the default. + wrap, + /// `SAT`: saturates at the minimum or maximum. + sat, + /// `FAIL`: does not write, the result is none. + fail, + } + + /// `GET encoding offset`: the value of the field. + record bitfield-get { + encoding: bitfield-encoding, + offset: bitfield-offset, + } + + /// `SET encoding offset value`: sets the field, the result is its previous value. + record bitfield-set { + encoding: bitfield-encoding, + offset: bitfield-offset, + value: s64, + } + + /// `INCRBY encoding offset increment`: increments the field, the result is its new value. + record bitfield-incrby { + encoding: bitfield-encoding, + offset: bitfield-offset, + increment: s64, + } + + /// An operation of BITFIELD. + /// + /// A write is preceded by an optional overflow mode, `OVERFLOW WRAP | SAT | FAIL`, for it and + /// the following writes. None keeps the current mode, `WRAP` at the start of the command. + variant bitfield-operation { + /// `GET encoding offset` + get(bitfield-get), + /// `[ OVERFLOW WRAP | SAT | FAIL ] SET encoding offset value` + set(tuple, bitfield-set>), + /// `[ OVERFLOW WRAP | SAT | FAIL ] INCRBY encoding offset increment` + incrby(tuple, bitfield-incrby>), + } + + /// A bitwise operation of BITOP, and its source keys. + variant bitop-operation { + /// `AND` + and(list), + /// `OR` + or(list), + /// `XOR` + xor(list), + /// `NOT`: of a single key. + not(string), + } + + // connection commands + + /// How a key spec finds the first key, see `key-spec`. + variant key-spec-begin-search { + /// `index`: the argument at the index. + index(integer), + /// `keyword`: the argument after the keyword. + keyword(key-spec-keyword), + /// `unknown`: the keys cannot be found from the arguments alone. + unknown, + } + + /// The keyword before the first key, see `key-spec-begin-search`. + record key-spec-keyword { + keyword: string, + /// The index of the argument to start searching from, from the end when negative. + startfrom: integer, + } + + /// How a key spec finds the keys after the first one, see `key-spec`. + variant key-spec-find-keys { + /// `range`: a range of arguments. + range(key-spec-range), + /// `keynum`: the number of keys is an argument. + keynum(key-spec-keynum), + /// `unknown`: the keys cannot be found from the arguments alone. + unknown, + } + + /// A range of keys, see `key-spec-find-keys`. + record key-spec-range { + /// The index of the last key, relative to the first, from the end when negative. + lastkey: integer, + /// The number of arguments from one key to the next. + keystep: integer, + /// With a `lastkey` of -1, divides the remaining arguments, e.g. 2 for half of them. + limit: integer, + } + + /// A number of keys, see `key-spec-find-keys`. + record key-spec-keynum { + /// The index of the argument with the number of keys, relative to the first. + keynumidx: integer, + /// The index of the first key, relative to the first. + firstkey: integer, + /// The number of arguments from one key to the next. + keystep: integer, + } + + /// Where a command's keys are and how it uses them. + record key-spec { + notes: option, + /// How the command uses the keys, e.g. `RW`, `access` and `update`. + %flags: list, + begin-search: key-spec-begin-search, + find-keys: key-spec-find-keys, + } + + /// A command, see `command` and `command-info`. + record command-info-reply { + /// The command's name, e.g. `get`, or for a subcommand, `config|get`. + name: string, + /// The number of arguments, the name included, the minimum when negative. + arity: integer, + /// The command's flags, e.g. `readonly` and `fast`. + %flags: list, + /// The index of the first key, 0 for none. + first-key: integer, + /// The index of the last key, from the end when negative. + last-key: integer, + /// The number of arguments from one key to the next. + step: integer, + /// The ACL categories of the command, e.g. `@read`. + acl-categories: list, + /// Hints for clients, e.g. `nondeterministic_output`. + tips: list, + key-specs: list, + } + + /// The type of an argument of a command, see `command-argument`. + variant command-argument-type { + /// `string` + %string, + /// `integer` + integer, + /// `double` + double, + /// `key`: a key, see `key-spec-index`. + key, + /// `pattern`: a glob-style pattern. + pattern, + /// `unix-time` + unix-time, + /// `pure-token`: the token alone, without a value. + pure-token, + /// `oneof`: one of the arguments it contains. + oneof, + /// `block`: the arguments it contains, together. + block, + /// A type this interface does not know, e.g. from a newer server. + other(string), + } + + /// An argument of a command, see `command-docs`. + record command-argument { + /// The index of the `oneof` or `block` argument containing this one, in the command's + /// arguments, none at the top level. + parent: option, + name: string, + %type: command-argument-type, + /// The name to show for the argument. + display-text: option, + /// The index of the argument's key spec, for a key. + key-spec-index: option, + /// The token before the argument's value, e.g. `EX`, or for a `pure-token`, the token alone. + token: option, + summary: option, + /// The version that added the argument. + since: option, + deprecated-since: option, + /// The argument's flags, e.g. `optional`, `multiple` and `multiple_token`. + %flags: list, + } + + /// The documentation of a command, see `command-docs`. + record command-docs-reply { + /// The command's name, e.g. `get`, or for a subcommand, `config|get`. + name: string, + summary: string, + /// The version that added the command. + since: string, + /// The command's group, e.g. `string`. + group: string, + /// The command's time complexity, none for a few commands, e.g. MONITOR. + complexity: option, + /// The module of the command, for a module's command. + module: option, + /// The command's documentation flags, e.g. `deprecated` or `syscmd`. + doc-flags: list, + deprecated-since: option, + /// The command to use instead, for a deprecated command. + replaced-by: option, + /// The changes to the command, the version of each and what changed. + history: list>, + /// The arguments, depth first, those an argument contains after it. + arguments: list, + } + + /// Where a connection's tracking notifications are redirected, see `client-getredir` and + /// `client-trackinginfo`. + variant tracking-redirect { + /// -1: tracking is not on. + not-tracking, + /// 0: tracking is on, the notifications are not redirected. + not-redirected, + /// The ID of the connection the notifications are redirected to. + redirected(integer), + } + + /// Information about the library of a connection, see `client-setinfo`. + variant client-setinfo-attr { + /// `LIB-NAME`: the name of the library, e.g. `valkey-wasm`. + lib-name(string), + /// `LIB-VER`: the version of the library. + lib-ver(string), + } + + /// Which keys tracking tracks without CLIENT CACHING. + enum client-tracking-mode { + /// `OPTIN`: none, only those of commands after CLIENT CACHING YES. + optin, + /// `OPTOUT`: all, except those of commands after CLIENT CACHING NO. + optout, + } + + /// The options of CLIENT TRACKING ON. + record client-tracking-options { + /// `REDIRECT client-id`: sends the notifications to the connection with the ID instead. + redirect: option, + /// `PREFIX prefix`: with `bcast`, only notifies about keys with the prefixes. + prefixes: option>, + /// `BCAST`: notifies about every change of the keys with the prefixes, read or not. + bcast: bool, + mode: option, + /// `NOLOOP`: does not notify about the keys the connection changed itself. + noloop: bool, + } + + /// A connection's tracking state, see `client-trackinginfo`. + record client-trackinginfo-reply { + /// The tracking flags, e.g. `on`, `bcast`, `optin` or `noloop`. + %flags: list, + /// Where the notifications are redirected. + redirect: tracking-redirect, + /// The prefixes of BCAST. + prefixes: list, + } + + /// The filter of COMMAND LIST. + variant command-list-filter { + /// `MODULE module-name`: the commands of the module. + module(string), + /// `ACLCAT category`: the commands in the ACL category. + aclcat(string), + /// `PATTERN pattern`: the commands matching the glob-style pattern. + pattern(string), + } + + // geo commands + + /// A position on the Earth. + /// + /// The exact limits, as specified by EPSG:900913 / EPSG:3785 / OSGEO:41001 + record geo-coordinates { + /// The longitude, from -180 to 180 degrees. + longitude: double, + /// The latitude, from -85.05112878 to 85.05112878 degrees. + latitude: double, + } + + /// A unit of distance. + enum geo-unit { + /// `m`: meters. + m, + /// `km`: kilometers. + km, + /// `ft`: feet. + ft, + /// `mi`: miles. + mi, + } + + /// Only add or only update members, GEOADD's `NX` and `XX`. + enum geoadd-condition { + /// Only add new members, never update existing ones. + nx, + /// Only update existing members, never add new ones. + xx, + } + + /// The center of a search, `FROMMEMBER` or `FROMLONLAT`. + variant geo-from { + /// `FROMMEMBER member`: the position of a member of the index. + member(string), + /// `FROMLONLAT longitude latitude` + lonlat(geo-coordinates), + } + + /// `BYRADIUS`: a circle around the center. + record geo-radius { + %from: geo-from, + radius: double, + unit: geo-unit, + } + + /// `BYBOX`: a rectangle around the center. + record geo-box { + %from: geo-from, + width: double, + height: double, + unit: geo-unit, + } + + /// The area of a search. + variant geo-shape { + /// `[ FROMMEMBER member | FROMLONLAT longitude latitude ] BYRADIUS radius unit` + radius(geo-radius), + /// `[ FROMMEMBER member | FROMLONLAT longitude latitude ] BYBOX width height unit` + box(geo-box), + /// `BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ]`: the polygon with + /// the vertices, in order, which has no center. + polygon(list), + } + + /// At most a count of the members, `COUNT count [ ANY ]`. + record geo-count { + count: integer, + /// `ANY`: returns as soon as enough members are found, rather than the nearest ones. + any: bool, + } + + /// The details GEOSEARCH returns of each member it found. + flags geosearch-with-flags { + /// `WITHCOORD`: the position. + withcoord, + /// `WITHDIST`: the distance from the center. + withdist, + /// `WITHHASH`: the geohash, as an integer. + withhash, + } + + /// A member a search found, with the details asked for. + record geo-search-result { + member: string, + /// `WITHDIST`: the distance from the center, in the unit of the shape. + distance: option, + /// `WITHHASH`: the geohash, as an integer. + hash: option, + /// `WITHCOORD`: the position. + coordinates: option, + } + + // hash commands + + /// The outcome of removing the expiration of a hash field. + enum field-persist-result { + /// -2: the field does not exist, or the hash does not exist. + does-not-exist, + /// -1: the field exists, but has no expiration. + no-expiration, + /// 1: the expiration was removed. + removed, + } + + /// Only set the fields when the hash does, or does not, exist, HSETEX's `NX` and `XX`. + enum hsetex-condition { + /// Only set the fields when the hash does not exist. + nx, + /// Only set the fields when the hash exists. + xx, + } + + /// Only set the fields when none, or all, of them exist, HSETEX's `FNX` and `FXX`. + enum hsetex-field-condition { + /// Only set the fields when none of them exist. + fnx, + /// Only set the fields when all of them exist. + fxx, + } + + // keyspace commands + + /// The type of the value stored at a key. + variant key-type { + /// `string` + %string, + /// `list` + %list, + /// `set` + set, + /// `zset`: a sorted set. + zset, + /// `hash` + hash, + /// `stream` + %stream, + /// The type of a module, by name. + other(string), + } + + /// The options of MIGRATE. + flags migrate-flags { + /// `COPY`: keeps the keys in this instance. + copy, + /// `REPLACE`: replaces keys that exist in the other instance. + replace, + } + + /// The credentials to authenticate to the other instance with, for MIGRATE. + variant migrate-auth { + /// `AUTH password` + password(string), + /// `AUTH2 username password` + username-password(tuple), + } + + // list commands + + /// An end of a list. + enum list-side { + /// `LEFT`: the head, the first element. + left, + /// `RIGHT`: the tail, the last element. + right, + } + + /// Where LINSERT inserts an element, relative to the pivot. + enum linsert-position { + /// `BEFORE`: in front of the pivot. + before, + /// `AFTER`: behind the pivot. + after, + } + + /// The outcome of LINSERT. + variant linsert-result { + /// The element was inserted, the length of the list after it. + inserted(integer), + /// 0: the key does not exist, nothing was inserted. + no-such-key, + /// -1: the pivot is not in the list, nothing was inserted. + pivot-not-found, + } + + // scripting commands + + /// Whether a flush happens in the background, `ASYNC`, or before replying, `SYNC`. + enum flush-mode { + /// `ASYNC` + %async, + /// `SYNC` + sync, + } + + /// A function of a library. + record library-function { + name: string, + description: option, + /// The function's flags, e.g. `no-writes`. + %flags: list, + } + + /// A library of functions. + record library { + library-name: string, + /// The engine that runs the library, e.g. `LUA`. + engine: string, + functions: list, + /// The library's source code, with `withcode`. + library-code: option, + } + + /// The function that is running. + record running-function { + name: string, + /// The command and its arguments that called the function. + command: list, + /// How long the function has been running. + duration: duration-milliseconds, + } + + /// The libraries and functions of an engine. + record engine-stats { + libraries-count: integer, + functions-count: integer, + } + + /// The function that is running, and the libraries and functions of each engine. + record function-stats-reply { + /// None when no function is running. + running-script: option, + /// The statistics of each engine, by name. + engines: map, + } + + // set commands + + /// A range of a sorted result, `LIMIT offset count`. + record limit { + /// The number of elements skipped. + offset: integer, + /// The maximum number of elements returned, negative for all of them. + count: integer, + } + + /// How SORT orders and projects the elements of a list, set or sorted set. + record sort-opts { + /// `BY pattern`: sorts by the values of the keys the pattern names, `*` replaced with each + /// element, or `nosort` to skip sorting. + by: option, + /// `LIMIT offset count`: a range of the sorted elements. + limit: option, + /// `GET pattern`: returns the values of the keys each pattern names instead of the elements, + /// `#` for the element itself. + get: option>, + /// order to sort elements. + order: option, + /// `ALPHA`: sorts lexicographically, rather than numerically. + alpha: bool, + } + + enum sort-order { + /// ascending, smallest to largest. + asc, + /// descending, largest to smallest. + desc, + } + + // sortedset commands + + /// A bound of a range of scores. + variant score-bound { + /// The score, included in the range. + inclusive(double), + /// `(score`: the score, excluded from the range. + exclusive(double), + /// `-inf` as the min, `+inf` as the max, the start is the max with `REV`. + unbounded, + } + + /// A bound of a range of members, for members of equal score, ordered lexicographically. + variant lex-bound { + /// `[member`: the member, included in the range. + inclusive(string), + /// `(member`: the member, excluded from the range. + exclusive(string), + /// `-` as the min, `+` as the max, the start is the max with `REV`. + unbounded, + } + + /// Which end of a sorted set to pop from. + enum pop-end { + /// The members with the lowest scores. + min, + /// The members with the highest scores. + max, + } + + /// How scores combine for a member in several sorted sets. + enum aggregate { + /// The sum of the weighted scores, the default. + sum, + /// The lowest weighted score. + min, + /// The highest weighted score. + max, + } + + /// The range of ZRANGE and ZRANGESTORE, `start` and `stop`, given as they are sent: with `REV`, + /// the higher bound first. + variant zrange-by { + /// Positions, both included, negative from the end. + rank(tuple), + /// `BYSCORE`: the members with scores in the range. + score(tuple), + /// `BYLEX`: the members in the range, for members of equal score. + lex(tuple), + } + + /// The range of ZRANGE WITHSCORES, which does not support BYLEX, see `zrange-by`. + variant zrange-withscores-by { + /// Positions, both included, negative from the end. + rank(tuple), + /// `BYSCORE`: the members with scores in the range. + score(tuple), + } + + /// Only update or only add members, ZADD's `NX` and `XX`. + enum zadd-condition { + /// Only add new members, never update existing ones. + nx, + /// Only update existing members, never add new ones. + xx, + } + + /// Only update a member's score when the new score is greater or less, ZADD's `GT` and `LT`. + enum zadd-comparison { + /// Only update when the new score is greater than the current one. + gt, + /// Only update when the new score is less than the current one. + lt, + } + + // stream commands + + /// The ID of a stream entry, `-`, e.g. `1526919030474-55`. + /// + /// Commands that add an entry also accept `*`, for the server to generate the ID. + type stream-id = string; + + /// How a stream is trimmed, e.g. with XADD or XTRIM. + variant stream-trim { + /// Evicts entries as long as the stream's length exceeds the specified threshold, where threshold is a positive integer. + maxlen(stream-trim-maxlen), + /// Evicts entries with IDs lower than threshold, where threshold is a stream ID. + minid(stream-trim-minid), + } + + /// Trims the stream to a maximum length, `MAXLEN [ = | ~ ] threshold [ LIMIT count ]`. + record stream-trim-maxlen { + /// Exact, `=`, when none. + efficiency: option, + /// The maximum number of entries kept. + threshold: integer, + /// The maximum number of entries evicted, only with nearly exact trimming, `0` for no limit. + limit: option, + } + + /// Trims the entries with lower IDs, `MINID [ = | ~ ] threshold [ LIMIT count ]`. + record stream-trim-minid { + /// Exact, `=`, when none. + efficiency: option, + /// The lowest ID kept. + threshold: stream-id, + /// The maximum number of entries evicted, only with nearly exact trimming, `0` for no limit. + limit: option, + } + + /// How exactly a stream is trimmed. Nearly exact trimming is more efficient, the stream may keep a + /// few entries more than the threshold. + enum stream-trim-efficiency { + /// `=`: exact trimming + exact, + /// `~`: nearly exact trimming + mearly-exact, + } + + /// Options of XCLAIM. + record xclaim-opts { + /// `IDLE ms`: sets the idle time of the entries, the time since they were last delivered, instead + /// of resetting it. + idle: option, + /// `TIME unix-time-milliseconds`: sets the idle time of the entries to the time since this instant. + time: option, + /// `RETRYCOUNT count`: sets the delivery count of the entries, instead of incrementing it. + retrycount: option, + /// `FORCE`: claims the entries even when they are not pending for any consumer, as long as they + /// exist in the stream. + force: bool, + /// `LASTID lastid`: sets the ID of the last entry delivered to the group. + lastid: option, + } + + /// A consumer of a group, see `xinfo-consumers`. + record xinfo-consumer { + /// The consumer's name. + name: string, + /// The number of entries pending for the consumer. + pending: integer, + /// The time since the consumer last attempted an interaction, e.g. XREADGROUP, XCLAIM or + /// XAUTOCLAIM. + idle: duration-milliseconds, + /// The time since the consumer last interacted successfully, e.g. read or claimed entries. + /// None when the consumer has never been active. + inactive: option, + } + + /// A consumer group of a stream, see `xinfo-groups`. + record xinfo-group { + /// The group's name. + name: string, + /// The number of consumers in the group. + consumers: integer, + /// The number of entries in the group's pending entries list (PEL), delivered but not acknowledged. + pending: integer, + /// The ID of the last entry delivered to the group's consumers. + last-delivered-id: stream-id, + /// The logical read counter of the last entry delivered to the group's consumers, none when unknown. + entries-read: option, + /// The number of entries in the stream not yet delivered to the group's consumers, none when it + /// cannot be determined. + lag: option, + } + + /// Information about a stream, see `xinfo-stream`. + record xinfo-stream-reply { + /// The number of entries in the stream. + length: integer, + /// The number of keys in the radix tree storing the stream. + radix-tree-keys: integer, + /// The number of nodes in the radix tree storing the stream. + radix-tree-nodes: integer, + /// The number of consumer groups of the stream. + groups: integer, + /// The ID of the last entry added to the stream, none when no entry was ever added. + last-generated-id: option, + /// The highest ID deleted from the stream, none when no entry was ever deleted. + max-deleted-entry-id: option, + /// The number of entries added to the stream over its lifetime. + entries-added: integer, + /// The ID and fields of the first entry. + /// None when the stream is empty. + first-entry: option>>, + /// The ID and fields of the last entry. + /// None when the stream is empty. + last-entry: option>>, + } + + /// Information about a stream, its entries and consumer groups, see `xinfo-stream-full`. + record xinfo-stream-reply-full { + /// The number of entries in the stream. + length: integer, + /// The number of keys in the radix tree storing the stream. + radix-tree-keys: integer, + /// The number of nodes in the radix tree storing the stream. + radix-tree-nodes: integer, + /// The ID of the last entry added to the stream, none when no entry was ever added. + last-generated-id: option, + /// The highest ID deleted from the stream, none when no entry was ever deleted. + max-deleted-entry-id: option, + /// The number of entries added to the stream over its lifetime. + entries-added: integer, + /// The ID and fields of the entries, in ascending ID order, up to the count given. + entries: list>>, + /// The consumer groups of the stream. + groups: list, + } + + /// A consumer group of a stream, with its pending entries and consumers. + record xinfo-stream-reply-full-group { + /// The group's name. + name: string, + /// The ID of the last entry delivered to the group's consumers. + last-delivered-id: stream-id, + /// The logical read counter of the last entry delivered to the group's consumers, none when unknown. + entries-read: option, + /// The number of entries in the stream not yet delivered to the group's consumers, none when it + /// cannot be determined. + lag: option, + /// The number of entries in the group's pending entries list (PEL), delivered but not acknowledged. + pel-count: integer, + /// The group's pending entries, the ID of each, the consumer it was delivered to, when it was + /// last delivered and the number of times it was delivered. + pending: list>, + /// The group's consumers. + consumers: list, + } + + /// A consumer of a group, with its pending entries. + record xinfo-stream-reply-full-consumer { + /// The consumer's name. + name: string, + /// When the consumer last attempted an interaction, e.g. XREADGROUP, XCLAIM or XAUTOCLAIM. + seen-time: unix-time-milliseconds, + /// When the consumer last interacted successfully, e.g. read or claimed entries. + /// None when the consumer has never been active. + active-time: option, + /// The number of entries pending for the consumer, delivered but not acknowledged. + pel-count: integer, + /// The consumer's pending entries, the ID of each, when it was last delivered and the number of + /// times it was delivered. + pending: list>, + } + + /// The pending entries of a consumer group, see `xpending`. + record xpending-summary { + /// The number of pending entries. + count: integer, + /// The lowest ID of the pending entries, none when there are none. + lowest-id: option, + /// The highest ID of the pending entries, none when there are none. + highest-id: option, + /// Each consumer with pending entries, and how many it has. + consumers: list>, + } + + /// The pending entries of a consumer group, see `xpending-extended`. + record xpending-extended-entry { + /// The ID of the message. + id: stream-id, + /// The name of the consumer that fetched the message and has still to acknowledge it. We call it the current owner of the message. + name: string, + /// The number of milliseconds that elapsed since the last time this message was delivered to this consumer. + since-delivery: duration-milliseconds, + /// The number of times this message was delivered. + delivery-count: integer, + } + + // string commands + + /// The expiration to set on a key, see `getex`. + variant getex-ttl { + /// Set the specified expire time, in seconds. + ex(duration-seconds), + /// Set the specified expire time, in milliseconds. + px(duration-milliseconds), + /// Set the specified Unix time at which the key will expire, in seconds. + exat(unix-time-seconds), + /// Set the specified Unix time at which the key will expire, in milliseconds. + pxat(unix-time-milliseconds), + /// Remove the time to live associated with the key. + persist, + } + + /// The longest common subsequence of two strings, as the ranges matching in each, see `lcs-idx`. + record lcs-idx-reply { + /// The ranges that match, from the end of the strings to their start. + matches: list, + /// The length of the longest common subsequence. + len: integer, + } + + /// A part of the longest common subsequence. + record lcs-idx-match { + /// The inclusive start and end positions of the match in the first key's string. + key1-range: tuple, + /// The inclusive start and end positions of the match in the second key's string. + key2-range: tuple, + /// The length of the match, with `with-match-len`. + len: option, + } + + /// The condition for setting the keys, see `msetex`. + variant msetex-precondition { + /// Only set the keys if all of them do not already exist. + nx, + /// Only set the keys if all of them already exist. + xx, + } + + /// The expiration to set on the keys, see `msetex`. + type msetex-ttl = set-ttl; + + /// The condition for setting a key, see `set`. + variant set-precondition { + /// Only set the key if it does not already exist. + nx, + /// Only set the key if it already exists. + xx, + /// Set the key if the comparison value matches the existing value. + ifeq(string), + /// Set the key if the comparison value does not match the existing value. + ifne(string), + } + + /// The expiration to set on a key, see `set`. + variant set-ttl { + /// Set the specified expire time, in seconds. + ex(duration-seconds), + /// Set the specified expire time, in milliseconds. + px(duration-milliseconds), + /// Set the specified Unix time at which the key will expire, in seconds. + exat(unix-time-seconds), + /// Set the specified Unix time at which the key will expire, in milliseconds. + pxat(unix-time-milliseconds), + /// Retain the time to live associated with the keys. + keepttl, + } +} diff --git a/wit/worlds.wit b/wit/worlds.wit index b6f4dbb..7feda80 100644 --- a/wit/worlds.wit +++ b/wit/worlds.wit @@ -6,4 +6,5 @@ world imports { import connector; import resp; import tcp; + import types; } diff --git a/wkg.lock b/wkg.lock index 5b9a143..2ad4aa8 100644 --- a/wkg.lock +++ b/wkg.lock @@ -1,12 +1,4 @@ # This file is automatically generated. # It is not intended for manual editing. version = 1 - -[[packages]] -name = "wasi:clocks" -registry = "wasi.dev" - -[[packages.versions]] -requirement = "=0.3.0" -version = "0.3.0" -digest = "sha256:59e1f4079e64ada450e19ffc9d08854c904e356ed8cc1fda36ff4fe150264db2" +packages = [] From 7542a93d22623130eabaaa39fa1751d04ea58f85 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 13 Oct 2026 16:17:19 -0400 Subject: [PATCH 20/37] drop resp component and interface, fold traits into crate Signed-off-by: Scott Andrews --- Cargo.lock | 11 - Cargo.toml | 1 - README.md | 1 - components/client/client.wac | 2 - components/commands/src/lib.rs | 715 +++++++++--------- components/resp/Cargo.toml | 17 - components/resp/README.md | 5 - components/resp/src/lib.rs | 71 -- components/resp/tests/resp.rs | 80 -- components/wit/worlds.wit | 5 - .../resp-codec}/src/command.rs | 41 +- crates/resp-codec/src/lib.rs | 10 +- .../resp-codec}/src/value.rs | 6 +- wit/resp.wit | 6 - wit/worlds.wit | 1 - 15 files changed, 386 insertions(+), 586 deletions(-) delete mode 100644 components/resp/Cargo.toml delete mode 100644 components/resp/README.md delete mode 100644 components/resp/src/lib.rs delete mode 100644 components/resp/tests/resp.rs rename {components/commands => crates/resp-codec}/src/command.rs (95%) rename {components/commands => crates/resp-codec}/src/value.rs (99%) delete mode 100644 wit/resp.wit diff --git a/Cargo.lock b/Cargo.lock index 03b37e7..b8eeabd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1199,17 +1199,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "resp" -version = "0.1.0" -dependencies = [ - "resp-codec", - "test-harness", - "tokio", - "wasmtime", - "wit-bindgen", -] - [[package]] name = "resp-codec" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 18a612b..b1cddb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,6 @@ members = [ "components/commands", "components/connection", "components/dep-cli", - "components/resp", "components/sample-http-incrementor", "components/tcp", "components/tcp-connector", diff --git a/README.md b/README.md index f083b2e..4b08bfd 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,6 @@ The `componentized:valkey` WIT package, in [`wit`](./wit), separates the connect | Component | | | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [`resp`](./components/resp/) | encodes and decodes RESP values, the types every other component shares | | [`connection`](./components/connection/) | the mechanics of a connection: pairs commands with replies, keeps push messages apart, establishes it with `HELLO 3`, optionally keeps it alive | | [`tcp`](./components/tcp/) | connections over wasi:sockets TCP | | [`tcp-connector`](./components/tcp-connector/) | connections over TCP to the server configured with wasi:config, for components that need a connection without a say in how it is made | diff --git a/components/client/client.wac b/components/client/client.wac index 56ed4d5..8a7b6dd 100644 --- a/components/client/client.wac +++ b/components/client/client.wac @@ -1,6 +1,5 @@ package componentized:valkey; -let resp = new local:resp { ... }; let connection = new local:connection { ... }; @@ -24,7 +23,6 @@ let keyvalue = new local:as-keyvalue { ... }; -export resp.resp; export connection.connection; export tcp.tcp; export connector.connector; diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index 1be3692..4acb1a4 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -1,7 +1,3 @@ -use crate::command::{ - Bit, DecimalSeconds, Fields, Flag, KeysAndArgs, LexRange, NamedOption, NonEmpty, NumKeys, - OnOff, ScoreRange, Zrange, emit, query, -}; use crate::exports::componentized::valkey::commands::{ AclDryrunResult, AclListEntry, AclLogEntry, Aggregate, BgsaveOperation, BitRange, BitfieldGet, BitfieldOperation, BitopOperation, ClientFilter, ClientPauseMode, ClientSetinfoAttr, @@ -20,15 +16,36 @@ use crate::exports::componentized::valkey::commands::{ XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, ZaddComparison, ZaddCondition, ZrangeBy, ZrangeWithscoresBy, }; -use crate::value::{ +use resp_codec::command::{ + Bit, DecimalSeconds, Fields, Flag, KeysAndArgs, LexRange, NamedOption, NonEmpty, NumKeys, + OnOff, ScoreRange, Zrange, +}; +use resp_codec::value::{ AclList, AclUserRules, ClientList, CommandDocsList, CommandInfoList, FlatPairs, Migrated, NextCursor, OptionalKeyType, OrNever, OrZeroId, ScoredMembers, }; use wit_bindgen::rt::Map; -mod command; -mod value; +use resp_codec::{FromValue, IntoCommand}; + +/// Send a command, its words as bulk strings, and convert its reply. +/// +/// An error reply fails the command, see `connection.exchange`. +async fn exchange( + connection: &Connection, + command: impl IntoCommand, +) -> Result { + let reply = connection.exchange(command.into_command()?).await?; + T::from_value(reply) +} + +/// Send a command, its words as bulk strings, without waiting for a reply. +/// +/// An error reply fails the command, see `connection.emit`. +async fn emit(connection: &Connection, command: impl IntoCommand) -> Result<(), Error> { + connection.emit(command.into_command()?).await +} struct Commands; @@ -50,7 +67,7 @@ impl Guest for Commands { connection: &Connection, usernames: Vec, ) -> Result { - query( + exchange( connection, ("ACL", "DELUSER", NonEmpty::new("usernames", usernames)), ) @@ -63,7 +80,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/acl-genpass/"] #[allow(async_fn_in_trait)] async fn acl_genpass(connection: &Connection, bits: Option) -> Result { - query(connection, ("ACL", "GENPASS", bits)).await + exchange(connection, ("ACL", "GENPASS", bits)).await } #[doc = " Create an ACL user with the specified rules or modify the rules of an existing user."] @@ -76,7 +93,7 @@ impl Guest for Commands { username: String, rules: Option>, ) -> Result<(), Error> { - query( + exchange( connection, ( "ACL", @@ -97,7 +114,7 @@ impl Guest for Commands { connection: &Connection, category: Option, ) -> Result, Error> { - query(connection, ("ACL", "CAT", category)).await + exchange(connection, ("ACL", "CAT", category)).await } #[doc = " Whether the user may run the command, without running it."] @@ -110,7 +127,7 @@ impl Guest for Commands { username: String, command: Vec, ) -> Result { - query( + exchange( connection, ("ACL", "DRYRUN", username, NonEmpty::new("command", command)), ) @@ -126,7 +143,8 @@ impl Guest for Commands { connection: &Connection, username: String, ) -> Result>, Error> { - let rules: Option = query(connection, ("ACL", "GETUSER", username)).await?; + let rules: Option = + exchange(connection, ("ACL", "GETUSER", username)).await?; Ok(rules.map(|rules| rules.0)) } @@ -136,7 +154,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/acl-list/"] #[allow(async_fn_in_trait)] async fn acl_list(connection: &Connection) -> Result, Error> { - let users: AclList = query(connection, ("ACL", "LIST")).await?; + let users: AclList = exchange(connection, ("ACL", "LIST")).await?; Ok(users.0) } @@ -146,7 +164,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/acl-load/"] #[allow(async_fn_in_trait)] async fn acl_load(connection: &Connection) -> Result<(), Error> { - query(connection, ("ACL", "LOAD")).await + exchange(connection, ("ACL", "LOAD")).await } #[doc = " The recent security events, commands or authentications the rules denied, up to the count,"] @@ -161,7 +179,7 @@ impl Guest for Commands { connection: &Connection, count: Option, ) -> Result, Error> { - query(connection, ("ACL", "LOG", count)).await + exchange(connection, ("ACL", "LOG", count)).await } #[doc = " Clears the security events."] @@ -172,7 +190,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/acl-log/"] #[allow(async_fn_in_trait)] async fn acl_log_reset(connection: &Connection) -> Result<(), Error> { - query(connection, ("ACL", "LOG", "RESET")).await + exchange(connection, ("ACL", "LOG", "RESET")).await } #[doc = " Saves the users to the configured ACL file."] @@ -181,7 +199,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/acl-save/"] #[allow(async_fn_in_trait)] async fn acl_save(connection: &Connection) -> Result<(), Error> { - query(connection, ("ACL", "SAVE")).await + exchange(connection, ("ACL", "SAVE")).await } #[doc = " The names of the users."] @@ -190,7 +208,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/acl-users/"] #[allow(async_fn_in_trait)] async fn acl_users(connection: &Connection) -> Result, Error> { - query(connection, ("ACL", "USERS")).await + exchange(connection, ("ACL", "USERS")).await } #[doc = " The name of the user the connection is authenticated as."] @@ -199,7 +217,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/acl-whoami/"] #[allow(async_fn_in_trait)] async fn acl_whoami(connection: &Connection) -> Result { - query(connection, ("ACL", "WHOAMI")).await + exchange(connection, ("ACL", "WHOAMI")).await } #[doc = " Starts rewriting the append-only file in the background, returning the server's message."] @@ -208,7 +226,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/bgrewriteaof/"] #[allow(async_fn_in_trait)] async fn bgrewriteaof(connection: &Connection) -> Result { - query(connection, "BGREWRITEAOF").await + exchange(connection, "BGREWRITEAOF").await } #[doc = " Starts saving the databases to disk in the background, or schedules or cancels it, returning"] @@ -221,7 +239,7 @@ impl Guest for Commands { connection: &Connection, operation: Option, ) -> Result { - query(connection, ("BGSAVE", operation)).await + exchange(connection, ("BGSAVE", operation)).await } #[doc = " Closes the connections that match every one of the filters, returning how many."] @@ -233,7 +251,7 @@ impl Guest for Commands { connection: &Connection, filters: Vec, ) -> Result { - query( + exchange( connection, ("CLIENT", "KILL", NonEmpty::new("filters", filters)), ) @@ -251,7 +269,7 @@ impl Guest for Commands { filters: Option>, ) -> Result>, Error> { let command = ("CLIENT", "LIST", NonEmpty::optional("filters", filters)); - let clients: ClientList = query(connection, command).await?; + let clients: ClientList = exchange(connection, command).await?; Ok(clients.0) } @@ -261,7 +279,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/client-no-evict/"] #[allow(async_fn_in_trait)] async fn client_no_evict(connection: &Connection, enabled: bool) -> Result<(), Error> { - query( + exchange( connection, ("CLIENT", "NO-EVICT", if enabled { "ON" } else { "OFF" }), ) @@ -278,7 +296,7 @@ impl Guest for Commands { timeout: DurationMilliseconds, mode: Option, ) -> Result<(), Error> { - query(connection, ("CLIENT", "PAUSE", timeout, mode)).await + exchange(connection, ("CLIENT", "PAUSE", timeout, mode)).await } #[doc = " Unblocks the connection with the ID from a blocking command, as if it timed out, or with an"] @@ -292,7 +310,7 @@ impl Guest for Commands { client_id: Integer, unblock_type: Option, ) -> Result { - query(connection, ("CLIENT", "UNBLOCK", client_id, unblock_type)).await + exchange(connection, ("CLIENT", "UNBLOCK", client_id, unblock_type)).await } #[doc = " Resumes running the commands of connections paused with `client-pause`."] @@ -301,7 +319,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/client-unpause/"] #[allow(async_fn_in_trait)] async fn client_unpause(connection: &Connection) -> Result<(), Error> { - query(connection, ("CLIENT", "UNPAUSE")).await + exchange(connection, ("CLIENT", "UNPAUSE")).await } #[doc = " Assigns the slots to the node."] @@ -310,7 +328,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/cluster-addslots/"] #[allow(async_fn_in_trait)] async fn cluster_addslots(connection: &Connection, slots: Vec) -> Result<(), Error> { - query( + exchange( connection, ("CLUSTER", "ADDSLOTS", NonEmpty::new("slots", slots)), ) @@ -326,7 +344,7 @@ impl Guest for Commands { connection: &Connection, ranges: Vec<(u16, u16)>, ) -> Result<(), Error> { - query( + exchange( connection, ("CLUSTER", "ADDSLOTSRANGE", NonEmpty::new("ranges", ranges)), ) @@ -339,7 +357,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/cluster-bumpepoch/"] #[allow(async_fn_in_trait)] async fn cluster_bumpepoch(connection: &Connection) -> Result { - query(connection, ("CLUSTER", "BUMPEPOCH")).await + exchange(connection, ("CLUSTER", "BUMPEPOCH")).await } #[doc = " Cancels the slot migrations in progress."] @@ -348,7 +366,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/cluster-cancelslotmigrations/"] #[allow(async_fn_in_trait)] async fn cluster_cancelslotmigrations(connection: &Connection) -> Result<(), Error> { - query(connection, ("CLUSTER", "CANCELSLOTMIGRATIONS")).await + exchange(connection, ("CLUSTER", "CANCELSLOTMIGRATIONS")).await } #[doc = " The number of active failure reports for the node."] @@ -360,7 +378,7 @@ impl Guest for Commands { connection: &Connection, node_id: String, ) -> Result { - query(connection, ("CLUSTER", "COUNT-FAILURE-REPORTS", node_id)).await + exchange(connection, ("CLUSTER", "COUNT-FAILURE-REPORTS", node_id)).await } #[doc = " Unassigns the slots from the node."] @@ -369,7 +387,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/cluster-delslots/"] #[allow(async_fn_in_trait)] async fn cluster_delslots(connection: &Connection, slots: Vec) -> Result<(), Error> { - query( + exchange( connection, ("CLUSTER", "DELSLOTS", NonEmpty::new("slots", slots)), ) @@ -385,7 +403,7 @@ impl Guest for Commands { connection: &Connection, ranges: Vec<(u16, u16)>, ) -> Result<(), Error> { - query( + exchange( connection, ("CLUSTER", "DELSLOTSRANGE", NonEmpty::new("ranges", ranges)), ) @@ -401,7 +419,7 @@ impl Guest for Commands { connection: &Connection, mode: Option, ) -> Result<(), Error> { - query(connection, ("CLUSTER", "FAILOVER", mode)).await + exchange(connection, ("CLUSTER", "FAILOVER", mode)).await } #[doc = " Unassigns all the slots of the node."] @@ -410,7 +428,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/cluster-flushslots/"] #[allow(async_fn_in_trait)] async fn cluster_flushslots(connection: &Connection) -> Result<(), Error> { - query(connection, ("CLUSTER", "FLUSHSLOTS")).await + exchange(connection, ("CLUSTER", "FLUSHSLOTS")).await } #[doc = " Removes the node from the nodes this node knows of."] @@ -419,7 +437,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/cluster-forget/"] #[allow(async_fn_in_trait)] async fn cluster_forget(connection: &Connection, node_id: String) -> Result<(), Error> { - query(connection, ("CLUSTER", "FORGET", node_id)).await + exchange(connection, ("CLUSTER", "FORGET", node_id)).await } #[doc = " The slot migrations in progress, and those recently finished."] @@ -430,7 +448,7 @@ impl Guest for Commands { async fn cluster_getslotmigrations( connection: &Connection, ) -> Result, Error> { - query(connection, ("CLUSTER", "GETSLOTMIGRATIONS")).await + exchange(connection, ("CLUSTER", "GETSLOTMIGRATIONS")).await } #[doc = " Connects the node to another, adding it to the cluster."] @@ -444,7 +462,7 @@ impl Guest for Commands { port: u16, cluster_bus_port: Option, ) -> Result<(), Error> { - query(connection, ("CLUSTER", "MEET", ip, port, cluster_bus_port)).await + exchange(connection, ("CLUSTER", "MEET", ip, port, cluster_bus_port)).await } #[doc = " Migrates the ranges of slots from this node to other nodes."] @@ -456,7 +474,7 @@ impl Guest for Commands { connection: &Connection, migrations: Vec, ) -> Result<(), Error> { - query( + exchange( connection, ( "CLUSTER", @@ -476,7 +494,7 @@ impl Guest for Commands { connection: &Connection, node_id: String, ) -> Result, Error> { - query(connection, ("CLUSTER", "REPLICAS", node_id)).await + exchange(connection, ("CLUSTER", "REPLICAS", node_id)).await } #[doc = " Makes the node a replica of the primary node, or with none, detaches it from its primary,"] @@ -494,7 +512,7 @@ impl Guest for Commands { Some(node_id) => vec![node_id], None => vec!["NO".to_string(), "ONE".to_string()], }; - query(connection, ("CLUSTER", "REPLICATE", node)).await + exchange(connection, ("CLUSTER", "REPLICATE", node)).await } #[doc = " Resets the node, forgetting the other nodes and its slots, `SOFT` without a mode."] @@ -506,7 +524,7 @@ impl Guest for Commands { connection: &Connection, mode: Option, ) -> Result<(), Error> { - query(connection, ("CLUSTER", "RESET", mode)).await + exchange(connection, ("CLUSTER", "RESET", mode)).await } #[doc = " Saves the node's cluster configuration to disk."] @@ -515,7 +533,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/cluster-saveconfig/"] #[allow(async_fn_in_trait)] async fn cluster_saveconfig(connection: &Connection) -> Result<(), Error> { - query(connection, ("CLUSTER", "SAVECONFIG")).await + exchange(connection, ("CLUSTER", "SAVECONFIG")).await } #[doc = " Sets the config epoch of a new node."] @@ -527,7 +545,7 @@ impl Guest for Commands { connection: &Connection, config_epoch: Integer, ) -> Result<(), Error> { - query(connection, ("CLUSTER", "SET-CONFIG-EPOCH", config_epoch)).await + exchange(connection, ("CLUSTER", "SET-CONFIG-EPOCH", config_epoch)).await } #[doc = " Changes the state of the slot on the node."] @@ -541,7 +559,7 @@ impl Guest for Commands { state: ClusterSetslotState, timeout: Option, ) -> Result<(), Error> { - query( + exchange( connection, ( "CLUSTER", @@ -564,7 +582,7 @@ impl Guest for Commands { count: Integer, type_: CommandlogType, ) -> Result, Error> { - query(connection, ("COMMANDLOG", "GET", count, type_)).await + exchange(connection, ("COMMANDLOG", "GET", count, type_)).await } #[doc = " The number of entries in the command log of the type."] @@ -576,7 +594,7 @@ impl Guest for Commands { connection: &Connection, type_: CommandlogType, ) -> Result { - query(connection, ("COMMANDLOG", "LEN", type_)).await + exchange(connection, ("COMMANDLOG", "LEN", type_)).await } #[doc = " Clears the command log of the type."] @@ -585,7 +603,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/commandlog-reset/"] #[allow(async_fn_in_trait)] async fn commandlog_reset(connection: &Connection, type_: CommandlogType) -> Result<(), Error> { - query(connection, ("COMMANDLOG", "RESET", type_)).await + exchange(connection, ("COMMANDLOG", "RESET", type_)).await } #[doc = " The values of the configuration parameters matching the glob-style patterns, by name."] @@ -597,7 +615,7 @@ impl Guest for Commands { connection: &Connection, parameters: Vec, ) -> Result, Error> { - query( + exchange( connection, ("CONFIG", "GET", NonEmpty::new("parameters", parameters)), ) @@ -610,7 +628,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/config-resetstat/"] #[allow(async_fn_in_trait)] async fn config_resetstat(connection: &Connection) -> Result<(), Error> { - query(connection, ("CONFIG", "RESETSTAT")).await + exchange(connection, ("CONFIG", "RESETSTAT")).await } #[doc = " Rewrites the configuration file with the current configuration."] @@ -619,7 +637,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/config-rewrite/"] #[allow(async_fn_in_trait)] async fn config_rewrite(connection: &Connection) -> Result<(), Error> { - query(connection, ("CONFIG", "REWRITE")).await + exchange(connection, ("CONFIG", "REWRITE")).await } #[doc = " Sets the configuration parameters to the values, all or none of them."] @@ -632,7 +650,7 @@ impl Guest for Commands { parameters_and_values: Vec<(String, String)>, ) -> Result<(), Error> { let parameters_and_values = NonEmpty::new("parameters-and-values", parameters_and_values); - query(connection, ("CONFIG", "SET", parameters_and_values)).await + exchange(connection, ("CONFIG", "SET", parameters_and_values)).await } #[doc = " Fails over from this primary to a replica, the one at the target, or aborts a failover in"] @@ -653,7 +671,7 @@ impl Guest for Commands { Flag("ABORT", abort), NamedOption("TIMEOUT", timeout), ); - query(connection, command).await + exchange(connection, command).await } #[doc = " When the databases were last saved to disk."] @@ -662,7 +680,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/lastsave/"] #[allow(async_fn_in_trait)] async fn lastsave(connection: &Connection) -> Result { - query(connection, "LASTSAVE").await + exchange(connection, "LASTSAVE").await } #[doc = " A report of the latency events, for people to read."] @@ -671,7 +689,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/latency-doctor/"] #[allow(async_fn_in_trait)] async fn latency_doctor(connection: &Connection) -> Result { - query(connection, ("LATENCY", "DOCTOR")).await + exchange(connection, ("LATENCY", "DOCTOR")).await } #[doc = " A graph of the latency of the event, as text."] @@ -680,7 +698,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/latency-graph/"] #[allow(async_fn_in_trait)] async fn latency_graph(connection: &Connection, event: String) -> Result { - query(connection, ("LATENCY", "GRAPH", event)).await + exchange(connection, ("LATENCY", "GRAPH", event)).await } #[doc = " The latency histogram of the commands, all of them without commands, by command name."] @@ -692,7 +710,7 @@ impl Guest for Commands { connection: &Connection, commands: Option>, ) -> Result, Error> { - query( + exchange( connection, ( "LATENCY", @@ -712,7 +730,7 @@ impl Guest for Commands { connection: &Connection, event: String, ) -> Result, Error> { - query(connection, ("LATENCY", "HISTORY", event)).await + exchange(connection, ("LATENCY", "HISTORY", event)).await } #[doc = " The latest sample of the latency of each event."] @@ -721,7 +739,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/latency-latest/"] #[allow(async_fn_in_trait)] async fn latency_latest(connection: &Connection) -> Result, Error> { - query(connection, ("LATENCY", "LATEST")).await + exchange(connection, ("LATENCY", "LATEST")).await } #[doc = " Clears the samples of the events, all of them without events, returning how many events."] @@ -733,7 +751,7 @@ impl Guest for Commands { connection: &Connection, events: Option>, ) -> Result { - query( + exchange( connection, ("LATENCY", "RESET", NonEmpty::optional("events", events)), ) @@ -746,7 +764,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/module-list/"] #[allow(async_fn_in_trait)] async fn module_list(connection: &Connection) -> Result, Error> { - query(connection, ("MODULE", "LIST")).await + exchange(connection, ("MODULE", "LIST")).await } #[doc = " Loads the module at the path, with the arguments."] @@ -759,7 +777,7 @@ impl Guest for Commands { path: String, args: Option>, ) -> Result<(), Error> { - query( + exchange( connection, ("MODULE", "LOAD", path, NonEmpty::optional("args", args)), ) @@ -786,7 +804,7 @@ impl Guest for Commands { }); let configs = NonEmpty::optional("configs", configs); let args = NonEmpty::optional("args", args).map(|args| ("ARGS", args)); - query(connection, ("MODULE", "LOADEX", path, configs, args)).await + exchange(connection, ("MODULE", "LOADEX", path, configs, args)).await } #[doc = " Unloads the module, by name."] @@ -795,7 +813,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/module-unload/"] #[allow(async_fn_in_trait)] async fn module_unload(connection: &Connection, name: String) -> Result<(), Error> { - query(connection, ("MODULE", "UNLOAD", name)).await + exchange(connection, ("MODULE", "UNLOAD", name)).await } #[doc = " Makes the server a replica of the primary at the host and port, or with none, a primary,"] @@ -813,7 +831,7 @@ impl Guest for Commands { Some((host, port)) => vec![host, port.to_string()], None => vec!["NO".to_string(), "ONE".to_string()], }; - query(connection, ("REPLICAOF", primary)).await + exchange(connection, ("REPLICAOF", primary)).await } #[doc = " The server's role in replication, and its state."] @@ -822,7 +840,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/role/"] #[allow(async_fn_in_trait)] async fn role(connection: &Connection) -> Result { - query(connection, "ROLE").await + exchange(connection, "ROLE").await } #[doc = " Saves the databases to disk, blocking the server until done."] @@ -831,7 +849,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/save/"] #[allow(async_fn_in_trait)] async fn save(connection: &Connection) -> Result<(), Error> { - query(connection, "SAVE").await + exchange(connection, "SAVE").await } #[doc = " Stops the server, saving to disk as configured, or as the save mode says."] @@ -857,7 +875,7 @@ impl Guest for Commands { Flag("SAFE", modifiers.contains(ShutdownFlags::SAFE)), Flag("FAILOVER", modifiers.contains(ShutdownFlags::FAILOVER)), ); - query(connection, command).await + exchange(connection, command).await } #[doc = " Aborts a shutdown in progress."] @@ -868,7 +886,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/shutdown/"] #[allow(async_fn_in_trait)] async fn shutdown_abort(connection: &Connection) -> Result<(), Error> { - query(connection, ("SHUTDOWN", "ABORT")).await + exchange(connection, ("SHUTDOWN", "ABORT")).await } #[doc = " The entries of the slow log, up to the count, 10 without one, all of them for -1."] @@ -880,7 +898,7 @@ impl Guest for Commands { connection: &Connection, count: Option, ) -> Result, Error> { - query(connection, ("SLOWLOG", "GET", count)).await + exchange(connection, ("SLOWLOG", "GET", count)).await } #[doc = " The number of entries in the slow log."] @@ -889,7 +907,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/slowlog-len/"] #[allow(async_fn_in_trait)] async fn slowlog_len(connection: &Connection) -> Result { - query(connection, ("SLOWLOG", "LEN")).await + exchange(connection, ("SLOWLOG", "LEN")).await } #[doc = " Clears the slow log."] @@ -898,7 +916,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/slowlog-reset/"] #[allow(async_fn_in_trait)] async fn slowlog_reset(connection: &Connection) -> Result<(), Error> { - query(connection, ("SLOWLOG", "RESET")).await + exchange(connection, ("SLOWLOG", "RESET")).await } #[doc = " The number of bits set to 1 in the string stored at key, in the range, 0 when the key does"] @@ -912,7 +930,7 @@ impl Guest for Commands { key: String, range: Option, ) -> Result { - query(connection, ("BITCOUNT", key, range)).await + exchange(connection, ("BITCOUNT", key, range)).await } #[doc = " Reads and writes integer fields of the string stored at key, returning the result of each"] @@ -928,7 +946,7 @@ impl Guest for Commands { key: String, operations: Option>, ) -> Result>, Error> { - query( + exchange( connection, ( "BITFIELD", @@ -949,7 +967,7 @@ impl Guest for Commands { key: String, gets: Option>, ) -> Result, Error> { - query( + exchange( connection, ("BITFIELD_RO", key, NonEmpty::optional("gets", gets)), ) @@ -974,7 +992,7 @@ impl Guest for Commands { BitopOperation::Xor(keys) => ("XOR", keys), BitopOperation::Not(key) => ("NOT", vec![key]), }; - query( + exchange( connection, ("BITOP", operation, destination, NonEmpty::new("keys", keys)), ) @@ -995,7 +1013,7 @@ impl Guest for Commands { ) -> Result, Error> { // -1 when there is no such bit let position: OrNever = - query(connection, ("BITPOS", key, Bit(bit), range)).await?; + exchange(connection, ("BITPOS", key, Bit(bit), range)).await?; Ok(position.0) } @@ -1006,7 +1024,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/getbit/"] #[allow(async_fn_in_trait)] async fn getbit(connection: &Connection, key: String, offset: u64) -> Result { - query(connection, ("GETBIT", key, offset)).await + exchange(connection, ("GETBIT", key, offset)).await } #[doc = " Sets the bit at the offset of the string stored at key, growing it as needed, returning the"] @@ -1021,7 +1039,7 @@ impl Guest for Commands { offset: u64, value: bool, ) -> Result { - query(connection, ("SETBIT", key, offset, Bit(value))).await + exchange(connection, ("SETBIT", key, offset, Bit(value))).await } #[doc = " Authenticates the current connection."] @@ -1034,7 +1052,7 @@ impl Guest for Commands { username: Option, password: String, ) -> Result<(), Error> { - query(connection, ("AUTH", username, password)).await + exchange(connection, ("AUTH", username, password)).await } #[doc = " Switch to a different protocol, optionally authenticating and setting the connection\'s name, or provide a contextual client report."] @@ -1049,7 +1067,7 @@ impl Guest for Commands { auth: Option<(String, String)>, client_name: Option, ) -> Result, Error> { - query( + exchange( connection, ( "HELLO", @@ -1067,7 +1085,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/ping/"] #[allow(async_fn_in_trait)] async fn ping(connection: &Connection, message: Option) -> Result<(), Error> { - let reply: String = query(connection, ("PING", message.clone())).await?; + let reply: String = exchange(connection, ("PING", message.clone())).await?; if reply == message.unwrap_or("PONG".to_string()) { Ok(()) } else { @@ -1084,7 +1102,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/asking/"] #[allow(async_fn_in_trait)] async fn asking(connection: &Connection) -> Result<(), Error> { - query(connection, "ASKING").await + exchange(connection, "ASKING").await } #[doc = " Whether the keys the next command reads are tracked, with tracking in `optin` or `optout`"] @@ -1097,7 +1115,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/client-caching/"] #[allow(async_fn_in_trait)] async fn client_caching(connection: &Connection, enabled: bool) -> Result<(), Error> { - query( + exchange( connection, ("CLIENT", "CACHING", if enabled { "YES" } else { "NO" }), ) @@ -1110,7 +1128,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/client-capa/"] #[allow(async_fn_in_trait)] async fn client_capa(connection: &Connection, capabilities: Vec) -> Result<(), Error> { - query( + exchange( connection, ( "CLIENT", @@ -1127,7 +1145,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/client-getname/"] #[allow(async_fn_in_trait)] async fn client_getname(connection: &Connection) -> Result, Error> { - query(connection, ("CLIENT", "GETNAME")).await + exchange(connection, ("CLIENT", "GETNAME")).await } #[doc = " Where the connection's tracking notifications are redirected."] @@ -1136,7 +1154,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/client-getredir/"] #[allow(async_fn_in_trait)] async fn client_getredir(connection: &Connection) -> Result { - query(connection, ("CLIENT", "GETREDIR")).await + exchange(connection, ("CLIENT", "GETREDIR")).await } #[doc = " The ID of the connection, unique until the server restarts."] @@ -1145,7 +1163,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/client-id/"] #[allow(async_fn_in_trait)] async fn client_id(connection: &Connection) -> Result { - query(connection, ("CLIENT", "ID")).await + exchange(connection, ("CLIENT", "ID")).await } #[doc = " Whether the connection is an import source, which may write while the server is in import"] @@ -1155,7 +1173,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/client-import-source/"] #[allow(async_fn_in_trait)] async fn client_import_source(connection: &Connection, enabled: bool) -> Result<(), Error> { - query(connection, ("CLIENT", "IMPORT-SOURCE", OnOff(enabled))).await + exchange(connection, ("CLIENT", "IMPORT-SOURCE", OnOff(enabled))).await } #[doc = " The fields of the connection, as CLIENT LIST has them, e.g. `id`, `addr` and `name`."] @@ -1165,7 +1183,7 @@ impl Guest for Commands { #[allow(async_fn_in_trait)] async fn client_info(connection: &Connection) -> Result, Error> { // a line of CLIENT LIST - let clients: ClientList = query(connection, ("CLIENT", "INFO")).await?; + let clients: ClientList = exchange(connection, ("CLIENT", "INFO")).await?; clients .0 .into_iter() @@ -1181,7 +1199,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/client-no-touch/"] #[allow(async_fn_in_trait)] async fn client_no_touch(connection: &Connection, enabled: bool) -> Result<(), Error> { - query(connection, ("CLIENT", "NO-TOUCH", OnOff(enabled))).await + exchange(connection, ("CLIENT", "NO-TOUCH", OnOff(enabled))).await } #[doc = " Sets the name or version of the library of the connection, see CLIENT LIST."] @@ -1190,7 +1208,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/client-setinfo/"] #[allow(async_fn_in_trait)] async fn client_setinfo(connection: &Connection, attr: ClientSetinfoAttr) -> Result<(), Error> { - query(connection, ("CLIENT", "SETINFO", attr)).await + exchange(connection, ("CLIENT", "SETINFO", attr)).await } #[doc = " Sets the name of the connection."] @@ -1199,7 +1217,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/client-setname/"] #[allow(async_fn_in_trait)] async fn client_setname(connection: &Connection, connection_name: String) -> Result<(), Error> { - query(connection, ("CLIENT", "SETNAME", connection_name)).await + exchange(connection, ("CLIENT", "SETNAME", connection_name)).await } #[doc = " Turns tracking on with the options, or off with none, for server assisted client side"] @@ -1216,7 +1234,7 @@ impl Guest for Commands { ) -> Result<(), Error> { // ON with options, OFF without let status = OnOff(options.is_some()); - query(connection, ("CLIENT", "TRACKING", status, options)).await + exchange(connection, ("CLIENT", "TRACKING", status, options)).await } #[doc = " The connection's tracking state."] @@ -1227,7 +1245,7 @@ impl Guest for Commands { async fn client_trackinginfo( connection: &Connection, ) -> Result { - query(connection, ("CLIENT", "TRACKINGINFO")).await + exchange(connection, ("CLIENT", "TRACKINGINFO")).await } #[doc = " The number of commands of the server."] @@ -1236,7 +1254,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/command-count/"] #[allow(async_fn_in_trait)] async fn command_count(connection: &Connection) -> Result { - query(connection, ("COMMAND", "COUNT")).await + exchange(connection, ("COMMAND", "COUNT")).await } #[doc = " Every command of the server, a container followed by its subcommands."] @@ -1245,7 +1263,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/command/"] #[allow(async_fn_in_trait)] async fn command(connection: &Connection) -> Result, Error> { - let commands: CommandInfoList = query(connection, "COMMAND").await?; + let commands: CommandInfoList = exchange(connection, "COMMAND").await?; Ok(commands.0) } @@ -1261,7 +1279,7 @@ impl Guest for Commands { ) -> Result, Error> { let command_names = NonEmpty::optional("command-names", command_names); let commands: CommandDocsList = - query(connection, ("COMMAND", "DOCS", command_names)).await?; + exchange(connection, ("COMMAND", "DOCS", command_names)).await?; Ok(commands.0) } @@ -1277,7 +1295,7 @@ impl Guest for Commands { ) -> Result, Error> { let command_names = NonEmpty::optional("command-names", command_names); let commands: CommandInfoList = - query(connection, ("COMMAND", "INFO", command_names)).await?; + exchange(connection, ("COMMAND", "INFO", command_names)).await?; Ok(commands.0) } @@ -1290,7 +1308,7 @@ impl Guest for Commands { connection: &Connection, command: Vec, ) -> Result, Error> { - query( + exchange( connection, ("COMMAND", "GETKEYS", NonEmpty::new("command", command)), ) @@ -1307,7 +1325,7 @@ impl Guest for Commands { connection: &Connection, command: Vec, ) -> Result)>, Error> { - query( + exchange( connection, ( "COMMAND", @@ -1327,7 +1345,7 @@ impl Guest for Commands { connection: &Connection, filter: Option, ) -> Result, Error> { - query(connection, ("COMMAND", "LIST", filter)).await + exchange(connection, ("COMMAND", "LIST", filter)).await } #[doc = " Returns the message."] @@ -1336,7 +1354,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/echo/"] #[allow(async_fn_in_trait)] async fn echo(connection: &Connection, message: String) -> Result { - query(connection, ("ECHO", message)).await + exchange(connection, ("ECHO", message)).await } #[doc = " Allows the connection to read from a replica of a cluster, for the slots of its primary."] @@ -1347,7 +1365,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/readonly/"] #[allow(async_fn_in_trait)] async fn readonly(connection: &Connection) -> Result<(), Error> { - query(connection, "READONLY").await + exchange(connection, "READONLY").await } #[doc = " Disallows the connection to read from a replica of a cluster, the default."] @@ -1358,7 +1376,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/readwrite/"] #[allow(async_fn_in_trait)] async fn readwrite(connection: &Connection) -> Result<(), Error> { - query(connection, "READWRITE").await + exchange(connection, "READWRITE").await } #[doc = " Changes the database of the connection."] @@ -1369,7 +1387,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/select/"] #[allow(async_fn_in_trait)] async fn select(connection: &Connection, index: Integer) -> Result<(), Error> { - query(connection, ("SELECT", index)).await + exchange(connection, ("SELECT", index)).await } #[doc = " Waits until the connection's writes reached the number of replicas, or the timeout passed,"] @@ -1386,7 +1404,7 @@ impl Guest for Commands { numreplicas: Integer, timeout: DurationMilliseconds, ) -> Result { - query(connection, ("WAIT", numreplicas, timeout)).await + exchange(connection, ("WAIT", numreplicas, timeout)).await } #[doc = " Waits until the connection's writes are in the append-only file of the primary, and of the"] @@ -1405,7 +1423,7 @@ impl Guest for Commands { numreplicas: Integer, timeout: DurationMilliseconds, ) -> Result<(Integer, Integer), Error> { - query(connection, ("WAITAOF", numlocal, numreplicas, timeout)).await + exchange(connection, ("WAITAOF", numlocal, numreplicas, timeout)).await } #[doc = " Adds the members at their positions to the geospatial index stored at key, or updates their"] @@ -1421,7 +1439,7 @@ impl Guest for Commands { changed: bool, coordinates_and_members: Vec<(GeoCoordinates, String)>, ) -> Result { - query( + exchange( connection, ( "GEOADD", @@ -1446,7 +1464,7 @@ impl Guest for Commands { member2: String, unit: Option, ) -> Result, Error> { - query(connection, ("GEODIST", key, member1, member2, unit)).await + exchange(connection, ("GEODIST", key, member1, member2, unit)).await } #[doc = " The geohash of each of the members, none for a member that is not in the index."] @@ -1459,7 +1477,7 @@ impl Guest for Commands { key: String, members: Option>, ) -> Result>, Error> { - query( + exchange( connection, ("GEOHASH", key, NonEmpty::optional("members", members)), ) @@ -1476,7 +1494,7 @@ impl Guest for Commands { key: String, members: Option>, ) -> Result>, Error> { - query( + exchange( connection, ("GEOPOS", key, NonEmpty::optional("members", members)), ) @@ -1497,7 +1515,7 @@ impl Guest for Commands { order: Option, count: Option, ) -> Result, Error> { - query(connection, ("GEOSEARCH", key, shape, order, count)).await + exchange(connection, ("GEOSEARCH", key, shape, order, count)).await } #[doc = " The members of the geospatial index stored at key in the shape, with the details asked for."] @@ -1525,7 +1543,7 @@ impl Guest for Commands { Flag("WITHDIST", details.contains(GeosearchWithFlags::WITHDIST)), Flag("WITHHASH", details.contains(GeosearchWithFlags::WITHHASH)), ); - query(connection, command).await + exchange(connection, command).await } #[doc = " Stores the members of the geospatial index stored at source in the shape, returning how"] @@ -1544,7 +1562,7 @@ impl Guest for Commands { count: Option, storedist: bool, ) -> Result { - query( + exchange( connection, ( "GEOSEARCHSTORE", @@ -1569,7 +1587,7 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result { - query(connection, ("HDEL", key, NonEmpty::new("fields", fields))).await + exchange(connection, ("HDEL", key, NonEmpty::new("fields", fields))).await } #[doc = " Returns if field is an existing field in the hash stored at key."] @@ -1578,7 +1596,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/hexists/"] #[allow(async_fn_in_trait)] async fn hexists(connection: &Connection, key: String, field: String) -> Result { - query(connection, ("HEXISTS", key, field)).await + exchange(connection, ("HEXISTS", key, field)).await } #[doc = " When each of the hash fields expires, to the second."] @@ -1593,7 +1611,7 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result, Error> { - query( + exchange( connection, ("HEXPIRETIME", key, Fields(NonEmpty::new("fields", fields))), ) @@ -1610,7 +1628,7 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result>, Error> { - query( + exchange( connection, ("HGETDEL", key, Fields(NonEmpty::new("fields", fields))), ) @@ -1629,7 +1647,7 @@ impl Guest for Commands { ttl: Option, fields: Vec, ) -> Result>, Error> { - query( + exchange( connection, ("HGETEX", key, ttl, Fields(NonEmpty::new("fields", fields))), ) @@ -1646,7 +1664,7 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result, Error> { - query( + exchange( connection, ("HPERSIST", key, Fields(NonEmpty::new("fields", fields))), ) @@ -1672,7 +1690,7 @@ impl Guest for Commands { mode, Fields(NonEmpty::new("fields", fields)), ); - query(connection, command).await + exchange(connection, command).await } #[doc = " Manipulate the expiration time for existing hash fields, as a Unix time in milliseconds."] @@ -1694,7 +1712,7 @@ impl Guest for Commands { mode, Fields(NonEmpty::new("fields", fields)), ); - query(connection, command).await + exchange(connection, command).await } #[doc = " When each of the hash fields expires, to the millisecond."] @@ -1709,7 +1727,7 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result, Error> { - query( + exchange( connection, ("HPEXPIRETIME", key, Fields(NonEmpty::new("fields", fields))), ) @@ -1728,7 +1746,7 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result, Error> { - query( + exchange( connection, ("HPTTL", key, Fields(NonEmpty::new("fields", fields))), ) @@ -1757,7 +1775,7 @@ impl Guest for Commands { ttl, Fields(NonEmpty::new("field-and-values", field_and_values)), ); - query(connection, command).await + exchange(connection, command).await } #[doc = " The remaining time to live of each of the hash fields, to the second."] @@ -1772,7 +1790,7 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result, Error> { - query( + exchange( connection, ("HTTL", key, Fields(NonEmpty::new("fields", fields))), ) @@ -1791,7 +1809,7 @@ impl Guest for Commands { mode: Option, fields: Vec, ) -> Result, Error> { - query( + exchange( connection, ( "HEXPIRE", @@ -1816,7 +1834,7 @@ impl Guest for Commands { mode: Option, fields: Vec, ) -> Result, Error> { - query( + exchange( connection, ( "HEXPIREAT", @@ -1839,7 +1857,7 @@ impl Guest for Commands { key: String, field: String, ) -> Result, Error> { - query(connection, ("HGET", key, field)).await + exchange(connection, ("HGET", key, field)).await } #[doc = " Returns all fields and values of the hash stored at key."] @@ -1848,7 +1866,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/hgetall/"] #[allow(async_fn_in_trait)] async fn hgetall(connection: &Connection, key: String) -> Result, Error> { - query(connection, ("HGETALL", key)).await + exchange(connection, ("HGETALL", key)).await } #[doc = " Increments the number stored at field in the hash stored at key by increment."] @@ -1862,7 +1880,7 @@ impl Guest for Commands { field: String, increment: Integer, ) -> Result { - query(connection, ("HINCRBY", key, field, increment)).await + exchange(connection, ("HINCRBY", key, field, increment)).await } #[doc = " Increment the specified field of a hash stored at key, and representing a floating point number, by the specified increment."] @@ -1876,7 +1894,7 @@ impl Guest for Commands { field: String, increment: Double, ) -> Result { - query(connection, ("HINCRBYFLOAT", key, field, increment)).await + exchange(connection, ("HINCRBYFLOAT", key, field, increment)).await } #[doc = " Returns all field names in the hash stored at key."] @@ -1885,7 +1903,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/hkeys/"] #[allow(async_fn_in_trait)] async fn hkeys(connection: &Connection, key: String) -> Result, Error> { - query(connection, ("HKEYS", key)).await + exchange(connection, ("HKEYS", key)).await } #[doc = " Returns the number of fields contained in the hash stored at key."] @@ -1894,7 +1912,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/hlen/"] #[allow(async_fn_in_trait)] async fn hlen(connection: &Connection, key: String) -> Result { - query(connection, ("HLEN", key)).await + exchange(connection, ("HLEN", key)).await } #[doc = " Returns the values associated with the specified fields in the hash stored at key."] @@ -1907,7 +1925,7 @@ impl Guest for Commands { key: String, fields: Vec, ) -> Result>, Error> { - query(connection, ("HMGET", key, NonEmpty::new("fields", fields))).await + exchange(connection, ("HMGET", key, NonEmpty::new("fields", fields))).await } #[doc = " Sets the specified fields to their respective values in the hash stored at key."] @@ -1920,7 +1938,7 @@ impl Guest for Commands { key: String, field_and_values: Vec<(String, String)>, ) -> Result<(), Error> { - query( + exchange( connection, ( "HMSET", @@ -1947,10 +1965,10 @@ impl Guest for Commands { count: Option, ) -> Result, Error> { if count.is_some() { - return query(connection, ("HRANDFIELD", key, count)).await; + return exchange(connection, ("HRANDFIELD", key, count)).await; } // without a count the reply is the field itself, or null - let field: Option = query(connection, ("HRANDFIELD", key)).await?; + let field: Option = exchange(connection, ("HRANDFIELD", key)).await?; Ok(field.into_iter().collect()) } @@ -1969,7 +1987,7 @@ impl Guest for Commands { key: String, count: Integer, ) -> Result, Error> { - query(connection, ("HRANDFIELD", key, count, "WITHVALUES")).await + exchange(connection, ("HRANDFIELD", key, count, "WITHVALUES")).await } #[doc = " Incrementally iterate over a collection of elements."] @@ -1986,7 +2004,7 @@ impl Guest for Commands { match_: Option, count: Option, ) -> Result<(Option, Map), Error> { - let (cursor, items): (NextCursor, Map) = query( + let (cursor, items): (NextCursor, Map) = exchange( connection, ( "HSCAN", @@ -2014,7 +2032,7 @@ impl Guest for Commands { match_: Option, count: Option, ) -> Result<(Option, Vec), Error> { - let (cursor, items): (NextCursor, Vec) = query( + let (cursor, items): (NextCursor, Vec) = exchange( connection, ( "HSCAN", @@ -2039,7 +2057,7 @@ impl Guest for Commands { key: String, field_and_values: Vec<(String, String)>, ) -> Result { - query( + exchange( connection, ( "HSET", @@ -2061,7 +2079,7 @@ impl Guest for Commands { field: String, value: String, ) -> Result { - query(connection, ("HSETNX", key, field, value)).await + exchange(connection, ("HSETNX", key, field, value)).await } #[doc = " Returns the string length of the value associated with field in the hash stored at key."] @@ -2074,7 +2092,7 @@ impl Guest for Commands { key: String, field: String, ) -> Result { - query(connection, ("HSTRLEN", key, field)).await + exchange(connection, ("HSTRLEN", key, field)).await } #[doc = " All values in the hash stored at key."] @@ -2083,7 +2101,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/hvals/"] #[allow(async_fn_in_trait)] async fn hvals(connection: &Connection, key: String) -> Result, Error> { - query(connection, ("HVALS", key)).await + exchange(connection, ("HVALS", key)).await } #[doc = " Adds the elements to the HyperLogLog stored at key, creating it when it does not exist,"] @@ -2099,7 +2117,7 @@ impl Guest for Commands { key: String, elements: Option>, ) -> Result { - query( + exchange( connection, ("PFADD", key, NonEmpty::optional("elements", elements)), ) @@ -2113,7 +2131,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/pfcount/"] #[allow(async_fn_in_trait)] async fn pfcount(connection: &Connection, keys: Vec) -> Result { - query(connection, ("PFCOUNT", NonEmpty::new("keys", keys))).await + exchange(connection, ("PFCOUNT", NonEmpty::new("keys", keys))).await } #[doc = " Merges the HyperLogLogs stored at the source keys into the one stored at destination,"] @@ -2129,7 +2147,7 @@ impl Guest for Commands { destination: String, sources: Option>, ) -> Result<(), Error> { - query( + exchange( connection, ( "PFMERGE", @@ -2159,7 +2177,7 @@ impl Guest for Commands { whereto: ListSide, timeout: DurationMilliseconds, ) -> Result, Error> { - query( + exchange( connection, ( "BLMOVE", @@ -2191,7 +2209,7 @@ impl Guest for Commands { end: ListSide, count: Option, ) -> Result)>, Error> { - query( + exchange( connection, ( "BLMPOP", @@ -2220,7 +2238,7 @@ impl Guest for Commands { keys: Vec, timeout: DurationMilliseconds, ) -> Result, Error> { - query( + exchange( connection, ( "BLPOP", @@ -2247,7 +2265,7 @@ impl Guest for Commands { keys: Vec, timeout: DurationMilliseconds, ) -> Result, Error> { - query( + exchange( connection, ( "BRPOP", @@ -2277,7 +2295,7 @@ impl Guest for Commands { destination: String, timeout: DurationMilliseconds, ) -> Result, Error> { - query( + exchange( connection, ("BRPOPLPUSH", source, destination, DecimalSeconds(timeout)), ) @@ -2295,7 +2313,7 @@ impl Guest for Commands { key: String, index: Integer, ) -> Result, Error> { - query(connection, ("LINDEX", key, index)).await + exchange(connection, ("LINDEX", key, index)).await } #[doc = " Inserts the element before or after the first occurrence of the pivot in the list stored at"] @@ -2311,7 +2329,7 @@ impl Guest for Commands { pivot: String, element: String, ) -> Result { - query(connection, ("LINSERT", key, position, pivot, element)).await + exchange(connection, ("LINSERT", key, position, pivot, element)).await } #[doc = " The length of the list stored at key, 0 when the key does not exist."] @@ -2320,7 +2338,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/llen/"] #[allow(async_fn_in_trait)] async fn llen(connection: &Connection, key: String) -> Result { - query(connection, ("LLEN", key)).await + exchange(connection, ("LLEN", key)).await } #[doc = " Moves an element from one end of the source list to an end of the destination list,"] @@ -2336,7 +2354,7 @@ impl Guest for Commands { wherefrom: ListSide, whereto: ListSide, ) -> Result, Error> { - query( + exchange( connection, ("LMOVE", source, destination, wherefrom, whereto), ) @@ -2361,7 +2379,7 @@ impl Guest for Commands { end, NamedOption("COUNT", count), ); - query(connection, command).await + exchange(connection, command).await } #[doc = " Removes and returns the first elements of the list stored at key, up to the count, one"] @@ -2378,12 +2396,13 @@ impl Guest for Commands { match count { Some(count) => { // null when the key does not exist - let elements: Option> = query(connection, ("LPOP", key, count)).await?; + let elements: Option> = + exchange(connection, ("LPOP", key, count)).await?; Ok(elements.unwrap_or_default()) } None => { // without a count the reply is the element itself, or null - let element: Option = query(connection, ("LPOP", key)).await?; + let element: Option = exchange(connection, ("LPOP", key)).await?; Ok(element.into_iter().collect()) } } @@ -2416,10 +2435,10 @@ impl Guest for Commands { ); match count { - Some(_) => query(connection, command).await, + Some(_) => exchange(connection, command).await, None => { // without a count the reply is the position itself, or null - let position: Option = query(connection, command).await?; + let position: Option = exchange(connection, command).await?; Ok(position.into_iter().collect()) } } @@ -2436,7 +2455,7 @@ impl Guest for Commands { key: String, elements: Vec, ) -> Result { - query( + exchange( connection, ("LPUSH", key, NonEmpty::new("elements", elements)), ) @@ -2454,7 +2473,7 @@ impl Guest for Commands { key: String, elements: Vec, ) -> Result { - query( + exchange( connection, ("LPUSHX", key, NonEmpty::new("elements", elements)), ) @@ -2473,7 +2492,7 @@ impl Guest for Commands { start: Integer, stop: Integer, ) -> Result, Error> { - query(connection, ("LRANGE", key, start, stop)).await + exchange(connection, ("LRANGE", key, start, stop)).await } #[doc = " Removes occurrences of the element from the list stored at key, returning how many."] @@ -2490,7 +2509,7 @@ impl Guest for Commands { count: Integer, element: String, ) -> Result { - query(connection, ("LREM", key, count, element)).await + exchange(connection, ("LREM", key, count, element)).await } #[doc = " Sets the element at the index of the list stored at key, negative from the end."] @@ -2504,7 +2523,7 @@ impl Guest for Commands { index: Integer, element: String, ) -> Result<(), Error> { - query(connection, ("LSET", key, index, element)).await + exchange(connection, ("LSET", key, index, element)).await } #[doc = " Trims the list stored at key to the elements from the start to the stop, both included,"] @@ -2519,7 +2538,7 @@ impl Guest for Commands { start: Integer, stop: Integer, ) -> Result<(), Error> { - query(connection, ("LTRIM", key, start, stop)).await + exchange(connection, ("LTRIM", key, start, stop)).await } #[doc = " Removes and returns the last elements of the list stored at key, up to the count, one"] @@ -2536,12 +2555,13 @@ impl Guest for Commands { match count { Some(count) => { // null when the key does not exist - let elements: Option> = query(connection, ("RPOP", key, count)).await?; + let elements: Option> = + exchange(connection, ("RPOP", key, count)).await?; Ok(elements.unwrap_or_default()) } None => { // without a count the reply is the element itself, or null - let element: Option = query(connection, ("RPOP", key)).await?; + let element: Option = exchange(connection, ("RPOP", key)).await?; Ok(element.into_iter().collect()) } } @@ -2560,7 +2580,7 @@ impl Guest for Commands { source: String, destination: String, ) -> Result, Error> { - query(connection, ("RPOPLPUSH", source, destination)).await + exchange(connection, ("RPOPLPUSH", source, destination)).await } #[doc = " Inserts the elements at the tail of the list stored at key, creating it when it does not"] @@ -2574,7 +2594,7 @@ impl Guest for Commands { key: String, elements: Vec, ) -> Result { - query( + exchange( connection, ("RPUSH", key, NonEmpty::new("elements", elements)), ) @@ -2592,7 +2612,7 @@ impl Guest for Commands { key: String, elements: Vec, ) -> Result { - query( + exchange( connection, ("RPUSHX", key, NonEmpty::new("elements", elements)), ) @@ -2605,7 +2625,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/del/"] #[allow(async_fn_in_trait)] async fn del(connection: &Connection, keys: Vec) -> Result { - query(connection, ("DEL", NonEmpty::new("keys", keys))).await + exchange(connection, ("DEL", NonEmpty::new("keys", keys))).await } #[doc = " If key exists."] @@ -2614,7 +2634,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/exists/"] #[allow(async_fn_in_trait)] async fn exists(connection: &Connection, keys: Vec) -> Result { - query(connection, ("EXISTS", NonEmpty::new("keys", keys))).await + exchange(connection, ("EXISTS", NonEmpty::new("keys", keys))).await } #[doc = " Set a timeout on key."] @@ -2628,7 +2648,7 @@ impl Guest for Commands { seconds: DurationSeconds, mode: Option, ) -> Result { - query(connection, ("EXPIRE", key, seconds, mode)).await + exchange(connection, ("EXPIRE", key, seconds, mode)).await } #[doc = " Set a timeout on key."] @@ -2642,7 +2662,7 @@ impl Guest for Commands { seconds: UnixTimeSeconds, mode: Option, ) -> Result { - query(connection, ("EXPIREAT", key, seconds, mode)).await + exchange(connection, ("EXPIREAT", key, seconds, mode)).await } #[doc = " Returns all keys matching pattern."] @@ -2651,7 +2671,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/keys/"] #[allow(async_fn_in_trait)] async fn keys(connection: &Connection, pattern: String) -> Result, Error> { - query(connection, ("KEYS", pattern)).await + exchange(connection, ("KEYS", pattern)).await } #[doc = " Removes all the keys of the slot, in the mode, the server's `lazyfree-lazy-user-flush`"] @@ -2665,7 +2685,7 @@ impl Guest for Commands { slot: u16, mode: Option, ) -> Result<(), Error> { - query(connection, ("CLUSTER", "FLUSHSLOT", slot, mode)).await + exchange(connection, ("CLUSTER", "FLUSHSLOT", slot, mode)).await } #[doc = " Incrementally iterate over the keys of a cluster, slot by slot, or within the slot."] @@ -2691,7 +2711,7 @@ impl Guest for Commands { NamedOption("TYPE", type_), NamedOption("SLOT", slot), ); - let (cursor, keys): (NextCursor, Vec) = query(connection, command).await?; + let (cursor, keys): (NextCursor, Vec) = exchange(connection, command).await?; Ok((cursor.0, keys)) } @@ -2708,7 +2728,7 @@ impl Guest for Commands { destination_db: Option, replace: bool, ) -> Result { - query( + exchange( connection, ( "COPY", @@ -2727,7 +2747,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/dbsize/"] #[allow(async_fn_in_trait)] async fn dbsize(connection: &Connection) -> Result { - query(connection, "DBSIZE").await + exchange(connection, "DBSIZE").await } #[doc = " When the key expires, to the second."] @@ -2738,7 +2758,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/expiretime/"] #[allow(async_fn_in_trait)] async fn expiretime(connection: &Connection, key: String) -> Result { - query(connection, ("EXPIRETIME", key)).await + exchange(connection, ("EXPIRETIME", key)).await } #[doc = " Removes all the keys of all the databases, in the mode, the server's"] @@ -2748,7 +2768,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/flushall/"] #[allow(async_fn_in_trait)] async fn flushall(connection: &Connection, mode: Option) -> Result<(), Error> { - query(connection, ("FLUSHALL", mode)).await + exchange(connection, ("FLUSHALL", mode)).await } #[doc = " Removes all the keys of the current database, in the mode, the server's"] @@ -2758,7 +2778,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/flushdb/"] #[allow(async_fn_in_trait)] async fn flushdb(connection: &Connection, mode: Option) -> Result<(), Error> { - query(connection, ("FLUSHDB", mode)).await + exchange(connection, ("FLUSHDB", mode)).await } #[doc = " Moves the keys to the database of another instance, returning whether any were moved, false"] @@ -2790,7 +2810,7 @@ impl Guest for Commands { auth, ("KEYS", NonEmpty::new("keys", keys)), ); - let migrated: Migrated = query(connection, command).await?; + let migrated: Migrated = exchange(connection, command).await?; Ok(migrated.0) } @@ -2806,7 +2826,7 @@ impl Guest for Commands { db: Integer, replace: bool, ) -> Result { - query(connection, ("MOVE", key, db, Flag("REPLACE", replace))).await + exchange(connection, ("MOVE", key, db, Flag("REPLACE", replace))).await } #[doc = " The internal encoding of the value stored at key, e.g. `listpack`, none when the key does"] @@ -2819,7 +2839,7 @@ impl Guest for Commands { connection: &Connection, key: String, ) -> Result, Error> { - query(connection, ("OBJECT", "ENCODING", key)).await + exchange(connection, ("OBJECT", "ENCODING", key)).await } #[doc = " The logarithmic access frequency counter of the key, none when it does not exist."] @@ -2830,7 +2850,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/object-freq/"] #[allow(async_fn_in_trait)] async fn object_freq(connection: &Connection, key: String) -> Result, Error> { - query(connection, ("OBJECT", "FREQ", key)).await + exchange(connection, ("OBJECT", "FREQ", key)).await } #[doc = " The time since the key was last accessed, none when it does not exist."] @@ -2842,7 +2862,7 @@ impl Guest for Commands { connection: &Connection, key: String, ) -> Result, Error> { - query(connection, ("OBJECT", "IDLETIME", key)).await + exchange(connection, ("OBJECT", "IDLETIME", key)).await } #[doc = " The number of references to the value stored at key, none when the key does not exist."] @@ -2854,7 +2874,7 @@ impl Guest for Commands { connection: &Connection, key: String, ) -> Result, Error> { - query(connection, ("OBJECT", "REFCOUNT", key)).await + exchange(connection, ("OBJECT", "REFCOUNT", key)).await } #[doc = " Removes the expiration of the key, returning whether it had one."] @@ -2863,7 +2883,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/persist/"] #[allow(async_fn_in_trait)] async fn persist(connection: &Connection, key: String) -> Result { - query(connection, ("PERSIST", key)).await + exchange(connection, ("PERSIST", key)).await } #[doc = " Set a timeout on key, in milliseconds."] @@ -2877,7 +2897,7 @@ impl Guest for Commands { milliseconds: DurationMilliseconds, mode: Option, ) -> Result { - query(connection, ("PEXPIRE", key, milliseconds, mode)).await + exchange(connection, ("PEXPIRE", key, milliseconds, mode)).await } #[doc = " Set a timeout on key, as a Unix time in milliseconds."] @@ -2891,7 +2911,7 @@ impl Guest for Commands { milliseconds: UnixTimeMilliseconds, mode: Option, ) -> Result { - query(connection, ("PEXPIREAT", key, milliseconds, mode)).await + exchange(connection, ("PEXPIREAT", key, milliseconds, mode)).await } #[doc = " When the key expires, to the millisecond."] @@ -2902,7 +2922,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/pexpiretime/"] #[allow(async_fn_in_trait)] async fn pexpiretime(connection: &Connection, key: String) -> Result { - query(connection, ("PEXPIRETIME", key)).await + exchange(connection, ("PEXPIRETIME", key)).await } #[doc = " The remaining time to live of the key, to the millisecond."] @@ -2913,7 +2933,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/pttl/"] #[allow(async_fn_in_trait)] async fn pttl(connection: &Connection, key: String) -> Result { - query(connection, ("PTTL", key)).await + exchange(connection, ("PTTL", key)).await } #[doc = " A random key of the current database, none when it is empty."] @@ -2922,7 +2942,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/randomkey/"] #[allow(async_fn_in_trait)] async fn randomkey(connection: &Connection) -> Result, Error> { - query(connection, "RANDOMKEY").await + exchange(connection, "RANDOMKEY").await } #[doc = " Renames the key, replacing the new key when it exists."] @@ -2931,7 +2951,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/rename/"] #[allow(async_fn_in_trait)] async fn rename(connection: &Connection, key: String, newkey: String) -> Result<(), Error> { - query(connection, ("RENAME", key, newkey)).await + exchange(connection, ("RENAME", key, newkey)).await } #[doc = " Renames the key, only when the new key does not exist, returning whether it was renamed."] @@ -2940,7 +2960,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/renamenx/"] #[allow(async_fn_in_trait)] async fn renamenx(connection: &Connection, key: String, newkey: String) -> Result { - query(connection, ("RENAMENX", key, newkey)).await + exchange(connection, ("RENAMENX", key, newkey)).await } #[doc = " Incrementally iterate over the keys of the current database."] @@ -2962,7 +2982,7 @@ impl Guest for Commands { NamedOption("COUNT", count), NamedOption("TYPE", type_), ); - let (cursor, keys): (NextCursor, Vec) = query(connection, command).await?; + let (cursor, keys): (NextCursor, Vec) = exchange(connection, command).await?; Ok((cursor.0, keys)) } @@ -2976,7 +2996,7 @@ impl Guest for Commands { index1: Integer, index2: Integer, ) -> Result<(), Error> { - query(connection, ("SWAPDB", index1, index2)).await + exchange(connection, ("SWAPDB", index1, index2)).await } #[doc = " Updates the last access time of the keys, returning how many exist."] @@ -2985,7 +3005,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/touch/"] #[allow(async_fn_in_trait)] async fn touch(connection: &Connection, keys: Vec) -> Result { - query(connection, ("TOUCH", NonEmpty::new("keys", keys))).await + exchange(connection, ("TOUCH", NonEmpty::new("keys", keys))).await } #[doc = " The remaining time to live of the key, to the second."] @@ -2996,7 +3016,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/ttl/"] #[allow(async_fn_in_trait)] async fn ttl(connection: &Connection, key: String) -> Result { - query(connection, ("TTL", key)).await + exchange(connection, ("TTL", key)).await } #[doc = " The type of the value stored at key, none when the key does not exist."] @@ -3005,7 +3025,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/type/"] #[allow(async_fn_in_trait)] async fn type_(connection: &Connection, key: String) -> Result, Error> { - let key_type: OptionalKeyType = query(connection, ("TYPE", key)).await?; + let key_type: OptionalKeyType = exchange(connection, ("TYPE", key)).await?; Ok(key_type.0) } @@ -3015,7 +3035,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/unlink/"] #[allow(async_fn_in_trait)] async fn unlink(connection: &Connection, keys: Vec) -> Result { - query(connection, ("UNLINK", NonEmpty::new("keys", keys))).await + exchange(connection, ("UNLINK", NonEmpty::new("keys", keys))).await } #[doc = " Subscribes the client to the given patterns."] @@ -3041,7 +3061,7 @@ impl Guest for Commands { channel: String, message: String, ) -> Result { - query(connection, ("PUBLISH", channel, message)).await + exchange(connection, ("PUBLISH", channel, message)).await } #[doc = " Lists the currently active channels."] @@ -3053,7 +3073,7 @@ impl Guest for Commands { connection: &Connection, pattern: Option, ) -> Result, Error> { - query(connection, ("PUBSUB", "CHANNELS", pattern)).await + exchange(connection, ("PUBSUB", "CHANNELS", pattern)).await } #[doc = " Returns the number of unique patterns that are subscribed to by clients."] @@ -3062,7 +3082,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/pubsub-numpat/"] #[allow(async_fn_in_trait)] async fn pubsub_numpat(connection: &Connection) -> Result { - query(connection, ("PUBSUB", "NUMPAT")).await + exchange(connection, ("PUBSUB", "NUMPAT")).await } #[doc = " Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels."] @@ -3074,7 +3094,7 @@ impl Guest for Commands { connection: &Connection, channels: Option>, ) -> Result, Error> { - query( + exchange( connection, ("PUBSUB", "NUMSUB", NonEmpty::optional("channels", channels)), ) @@ -3090,7 +3110,7 @@ impl Guest for Commands { connection: &Connection, pattern: Option, ) -> Result, Error> { - query(connection, ("PUBSUB", "SHARDCHANNELS", pattern)).await + exchange(connection, ("PUBSUB", "SHARDCHANNELS", pattern)).await } #[doc = " Returns the number of subscribers for the specified shard channels."] @@ -3102,7 +3122,7 @@ impl Guest for Commands { connection: &Connection, shardchannels: Option>, ) -> Result, Error> { - query( + exchange( connection, ( "PUBSUB", @@ -3127,7 +3147,7 @@ impl Guest for Commands { keys: Option>, args: Option>, ) -> Result { - query(connection, ("EVAL", script, KeysAndArgs(keys, args))).await + exchange(connection, ("EVAL", script, KeysAndArgs(keys, args))).await } #[doc = " Runs the Lua script, which may not write, returning its result, see `eval`."] @@ -3141,7 +3161,7 @@ impl Guest for Commands { keys: Option>, args: Option>, ) -> Result { - query(connection, ("EVAL_RO", script, KeysAndArgs(keys, args))).await + exchange(connection, ("EVAL_RO", script, KeysAndArgs(keys, args))).await } #[doc = " Runs the Lua script cached with the SHA1 digest, see `script-load`, returning its result, see"] @@ -3156,7 +3176,7 @@ impl Guest for Commands { keys: Option>, args: Option>, ) -> Result { - query(connection, ("EVALSHA", sha1, KeysAndArgs(keys, args))).await + exchange(connection, ("EVALSHA", sha1, KeysAndArgs(keys, args))).await } #[doc = " Runs the Lua script cached with the SHA1 digest, which may not write, returning its result,"] @@ -3171,7 +3191,7 @@ impl Guest for Commands { keys: Option>, args: Option>, ) -> Result { - query(connection, ("EVALSHA_RO", sha1, KeysAndArgs(keys, args))).await + exchange(connection, ("EVALSHA_RO", sha1, KeysAndArgs(keys, args))).await } #[doc = " Calls the function, see `function-load`, returning its result, the keys it accesses and its"] @@ -3188,7 +3208,7 @@ impl Guest for Commands { keys: Option>, args: Option>, ) -> Result { - query(connection, ("FCALL", function, KeysAndArgs(keys, args))).await + exchange(connection, ("FCALL", function, KeysAndArgs(keys, args))).await } #[doc = " Calls the function, which must be flagged `no-writes`, returning its result, see `fcall`."] @@ -3202,7 +3222,7 @@ impl Guest for Commands { keys: Option>, args: Option>, ) -> Result { - query(connection, ("FCALL_RO", function, KeysAndArgs(keys, args))).await + exchange(connection, ("FCALL_RO", function, KeysAndArgs(keys, args))).await } #[doc = " Deletes the library and its functions."] @@ -3211,7 +3231,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/function-delete/"] #[allow(async_fn_in_trait)] async fn function_delete(connection: &Connection, library_name: String) -> Result<(), Error> { - query(connection, ("FUNCTION", "DELETE", library_name)).await + exchange(connection, ("FUNCTION", "DELETE", library_name)).await } #[doc = " Deletes all the libraries, in the mode, the server's `lazyfree-lazy-user-flush` without one."] @@ -3220,7 +3240,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/function-flush/"] #[allow(async_fn_in_trait)] async fn function_flush(connection: &Connection, mode: Option) -> Result<(), Error> { - query(connection, ("FUNCTION", "FLUSH", mode)).await + exchange(connection, ("FUNCTION", "FLUSH", mode)).await } #[doc = " Kills the function that is running, as long as it has not written."] @@ -3229,7 +3249,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/function-kill/"] #[allow(async_fn_in_trait)] async fn function_kill(connection: &Connection) -> Result<(), Error> { - query(connection, ("FUNCTION", "KILL")).await + exchange(connection, ("FUNCTION", "KILL")).await } #[doc = " The libraries, those with names matching the glob-style pattern with one."] @@ -3242,7 +3262,7 @@ impl Guest for Commands { library_name_pattern: Option, withcode: bool, ) -> Result, Error> { - query( + exchange( connection, ( "FUNCTION", @@ -3265,7 +3285,7 @@ impl Guest for Commands { replace: bool, function_code: String, ) -> Result { - query( + exchange( connection, ("FUNCTION", "LOAD", Flag("REPLACE", replace), function_code), ) @@ -3278,7 +3298,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/function-stats/"] #[allow(async_fn_in_trait)] async fn function_stats(connection: &Connection) -> Result { - query(connection, ("FUNCTION", "STATS")).await + exchange(connection, ("FUNCTION", "STATS")).await } #[doc = " Whether each of the scripts is cached, by SHA1 digest."] @@ -3290,7 +3310,7 @@ impl Guest for Commands { connection: &Connection, sha1s: Vec, ) -> Result, Error> { - query( + exchange( connection, ("SCRIPT", "EXISTS", NonEmpty::new("sha1s", sha1s)), ) @@ -3304,7 +3324,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/script-flush/"] #[allow(async_fn_in_trait)] async fn script_flush(connection: &Connection, mode: Option) -> Result<(), Error> { - query(connection, ("SCRIPT", "FLUSH", mode)).await + exchange(connection, ("SCRIPT", "FLUSH", mode)).await } #[doc = " Kills the script that is running, as long as it has not written."] @@ -3313,7 +3333,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/script-kill/"] #[allow(async_fn_in_trait)] async fn script_kill(connection: &Connection) -> Result<(), Error> { - query(connection, ("SCRIPT", "KILL")).await + exchange(connection, ("SCRIPT", "KILL")).await } #[doc = " Caches the script without running it, returning its SHA1 digest, see `evalsha`."] @@ -3322,7 +3342,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/script-load/"] #[allow(async_fn_in_trait)] async fn script_load(connection: &Connection, script: String) -> Result { - query(connection, ("SCRIPT", "LOAD", script)).await + exchange(connection, ("SCRIPT", "LOAD", script)).await } #[doc = " The source of the cached script with the SHA1 digest."] @@ -3331,7 +3351,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/script-show/"] #[allow(async_fn_in_trait)] async fn script_show(connection: &Connection, sha1: String) -> Result { - query(connection, ("SCRIPT", "SHOW", sha1)).await + exchange(connection, ("SCRIPT", "SHOW", sha1)).await } #[doc = " Unsubscribes the client from the given patterns, or from all of them if none is given."] @@ -3360,7 +3380,7 @@ impl Guest for Commands { shardchannel: String, message: String, ) -> Result { - query(connection, ("SPUBLISH", shardchannel, message)).await + exchange(connection, ("SPUBLISH", shardchannel, message)).await } #[doc = " Subscribes the client to the specified shard channels."] @@ -3430,7 +3450,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/append/"] #[allow(async_fn_in_trait)] async fn append(connection: &Connection, key: String, value: String) -> Result { - query(connection, ("APPEND", key, value)).await + exchange(connection, ("APPEND", key, value)).await } #[doc = " Decrements the number stored at key by one."] @@ -3439,7 +3459,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/decr/"] #[allow(async_fn_in_trait)] async fn decr(connection: &Connection, key: String) -> Result { - query(connection, ("DECR", key)).await + exchange(connection, ("DECR", key)).await } #[doc = " Decrements the number stored at key by decrement."] @@ -3452,7 +3472,7 @@ impl Guest for Commands { key: String, decrement: Integer, ) -> Result { - query(connection, ("DECRBY", key, decrement)).await + exchange(connection, ("DECRBY", key, decrement)).await } #[doc = " Deletes the key if its value matches the given string."] @@ -3461,7 +3481,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/delifeq/"] #[allow(async_fn_in_trait)] async fn delifeq(connection: &Connection, key: String, value: String) -> Result { - query(connection, ("DELIFEQ", key, value)).await + exchange(connection, ("DELIFEQ", key, value)).await } #[doc = " Get the value of key."] @@ -3470,7 +3490,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/get/"] #[allow(async_fn_in_trait)] async fn get(connection: &Connection, key: String) -> Result, Error> { - query(connection, ("GET", key)).await + exchange(connection, ("GET", key)).await } #[doc = " Get the value of key and delete the key."] @@ -3479,7 +3499,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/getdel/"] #[allow(async_fn_in_trait)] async fn getdel(connection: &Connection, key: String) -> Result, Error> { - query(connection, ("GETDEL", key)).await + exchange(connection, ("GETDEL", key)).await } #[doc = " Get the value of key and optionally set its expiration."] @@ -3492,7 +3512,7 @@ impl Guest for Commands { key: String, ttl: Option, ) -> Result, Error> { - query(connection, ("GETEX", key, ttl)).await + exchange(connection, ("GETEX", key, ttl)).await } #[doc = " Returns the substring of the string value stored at key, determined by the offsets start and end."] @@ -3506,7 +3526,7 @@ impl Guest for Commands { start: Integer, end: Integer, ) -> Result { - query(connection, ("GETRANGE", key, start, end)).await + exchange(connection, ("GETRANGE", key, start, end)).await } #[doc = " Atomically sets key to value and returns the old value stored at key."] @@ -3519,7 +3539,7 @@ impl Guest for Commands { key: String, value: String, ) -> Result, Error> { - query(connection, ("GETSET", key, value)).await + exchange(connection, ("GETSET", key, value)).await } #[doc = " Increments the number stored at key by one."] @@ -3528,7 +3548,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/incr/"] #[allow(async_fn_in_trait)] async fn incr(connection: &Connection, key: String) -> Result { - query(connection, ("INCR", key)).await + exchange(connection, ("INCR", key)).await } #[doc = " Increments the number stored at key by increment."] @@ -3541,7 +3561,7 @@ impl Guest for Commands { key: String, increment: Integer, ) -> Result { - query(connection, ("INCRBY", key, increment)).await + exchange(connection, ("INCRBY", key, increment)).await } #[doc = " Increment the string representing a floating point number stored at key by the specified increment."] @@ -3554,7 +3574,7 @@ impl Guest for Commands { key: String, increment: Double, ) -> Result { - query(connection, ("INCRBYFLOAT", key, increment)).await + exchange(connection, ("INCRBYFLOAT", key, increment)).await } #[doc = " The LCS command implements the longest common subsequence algorithm."] @@ -3565,7 +3585,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/lcs/"] #[allow(async_fn_in_trait)] async fn lcs(connection: &Connection, key1: String, key2: String) -> Result { - query(connection, ("LCS", key1, key2)).await + exchange(connection, ("LCS", key1, key2)).await } #[doc = " The LCS command implements the longest common subsequence algorithm."] @@ -3580,7 +3600,7 @@ impl Guest for Commands { key1: String, key2: String, ) -> Result { - query(connection, ("LCS", key1, key2, "LEN")).await + exchange(connection, ("LCS", key1, key2, "LEN")).await } #[doc = " The LCS command implements the longest common subsequence algorithm."] @@ -3597,7 +3617,7 @@ impl Guest for Commands { min_match_len: Option, with_match_len: bool, ) -> Result { - query( + exchange( connection, ( "LCS", @@ -3620,7 +3640,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, ) -> Result>, Error> { - query(connection, ("MGET", NonEmpty::new("keys", keys))).await + exchange(connection, ("MGET", NonEmpty::new("keys", keys))).await } #[doc = " Sets the given keys to their respective values."] @@ -3632,7 +3652,7 @@ impl Guest for Commands { connection: &Connection, keys_and_values: Vec<(String, String)>, ) -> Result<(), Error> { - query( + exchange( connection, ("MSET", NonEmpty::new("keys-and-values", keys_and_values)), ) @@ -3650,7 +3670,7 @@ impl Guest for Commands { precondition: Option, ttl: Option, ) -> Result { - query( + exchange( connection, ( "MSETEX", @@ -3671,7 +3691,7 @@ impl Guest for Commands { connection: &Connection, keys_and_values: Vec<(String, String)>, ) -> Result { - query( + exchange( connection, ("MSETNX", NonEmpty::new("keys-and-values", keys_and_values)), ) @@ -3689,7 +3709,7 @@ impl Guest for Commands { milliseconds: DurationMilliseconds, value: String, ) -> Result<(), Error> { - query(connection, ("PSETEX", key, milliseconds, value)).await + exchange(connection, ("PSETEX", key, milliseconds, value)).await } #[doc = " Set key to hold the string value."] @@ -3707,7 +3727,8 @@ impl Guest for Commands { ttl: Option, ) -> Result { // OK once set, null when the precondition is not met - let reply: Option<()> = query(connection, ("SET", key, value, precondition, ttl)).await?; + let reply: Option<()> = + exchange(connection, ("SET", key, value, precondition, ttl)).await?; Ok(reply.is_some()) } @@ -3723,7 +3744,7 @@ impl Guest for Commands { precondition: Option, ttl: Option, ) -> Result, Error> { - query(connection, ("SET", key, value, precondition, "GET", ttl)).await + exchange(connection, ("SET", key, value, precondition, "GET", ttl)).await } #[doc = " Set key to hold the string value with expiration in seconds."] @@ -3737,7 +3758,7 @@ impl Guest for Commands { seconds: DurationSeconds, value: String, ) -> Result<(), Error> { - query(connection, ("SETEX", key, seconds, value)).await + exchange(connection, ("SETEX", key, seconds, value)).await } #[doc = " Set key to hold string value if key does not exist."] @@ -3746,7 +3767,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/setnx/"] #[allow(async_fn_in_trait)] async fn setnx(connection: &Connection, key: String, value: String) -> Result { - query(connection, ("SETNX", key, value)).await + exchange(connection, ("SETNX", key, value)).await } #[doc = " Overwrites part of the string stored at key, starting at the specified offset, for the entire length of value."] @@ -3760,7 +3781,7 @@ impl Guest for Commands { offset: u32, value: String, ) -> Result { - query(connection, ("SETRANGE", key, offset, value)).await + exchange(connection, ("SETRANGE", key, offset, value)).await } #[doc = " Returns the length of the string value stored at key."] @@ -3769,7 +3790,7 @@ impl Guest for Commands { #[doc = " hthttps://valkey.io/commands/strlen/"] #[allow(async_fn_in_trait)] async fn strlen(connection: &Connection, key: String) -> Result { - query(connection, ("STRLEN", key)).await + exchange(connection, ("STRLEN", key)).await } #[doc = " Returns the substring of the string value stored at key, determined by the offsets start and end."] @@ -3783,7 +3804,7 @@ impl Guest for Commands { start: Integer, end: Integer, ) -> Result { - query(connection, ("SUBSTR", key, start, end)).await + exchange(connection, ("SUBSTR", key, start, end)).await } #[doc = " Removes one or multiple messages from the Pending Entries List (PEL) of a stream consumer group."] @@ -3797,7 +3818,7 @@ impl Guest for Commands { group: String, ids: Vec, ) -> Result { - query(connection, ("XACK", key, group, NonEmpty::new("ids", ids))).await + exchange(connection, ("XACK", key, group, NonEmpty::new("ids", ids))).await } #[doc = " Appends the specified stream entry to the stream at the specified key."] @@ -3813,7 +3834,7 @@ impl Guest for Commands { id: StreamId, field_and_values: Vec<(String, String)>, ) -> Result, Error> { - query( + exchange( connection, ( "XADD", @@ -3850,7 +3871,7 @@ impl Guest for Commands { ), Error, > { - let (next, claimed, deleted): (OrZeroId, _, _) = query( + let (next, claimed, deleted): (OrZeroId, _, _) = exchange( connection, ( "XAUTOCLAIM", @@ -3882,7 +3903,7 @@ impl Guest for Commands { start: Option, count: Option, ) -> Result<(Option, Vec, Vec), Error> { - let (next, claimed, deleted): (OrZeroId, _, _) = query( + let (next, claimed, deleted): (OrZeroId, _, _) = exchange( connection, ( "XAUTOCLAIM", @@ -3915,7 +3936,7 @@ impl Guest for Commands { ids: Vec, opts: Option, ) -> Result)>, Error> { - query( + exchange( connection, ( "XCLAIM", @@ -3946,7 +3967,7 @@ impl Guest for Commands { ids: Vec, opts: Option, ) -> Result, Error> { - query( + exchange( connection, ( "XCLAIM", @@ -3971,7 +3992,7 @@ impl Guest for Commands { key: String, ids: Vec, ) -> Result { - query(connection, ("XDEL", key, NonEmpty::new("ids", ids))).await + exchange(connection, ("XDEL", key, NonEmpty::new("ids", ids))).await } #[doc = " Create a new consumer group uniquely identified by for the stream stored at ."] @@ -3987,7 +4008,7 @@ impl Guest for Commands { mkstream: bool, entriesread: Option, ) -> Result<(), Error> { - query( + exchange( connection, ( "XGROUP", @@ -4013,7 +4034,7 @@ impl Guest for Commands { group: String, consumer: String, ) -> Result { - query( + exchange( connection, ("XGROUP", "CREATECONSUMER", key, group, consumer), ) @@ -4031,7 +4052,7 @@ impl Guest for Commands { group: String, consumer: String, ) -> Result { - query(connection, ("XGROUP", "DELCONSUMER", key, group, consumer)).await + exchange(connection, ("XGROUP", "DELCONSUMER", key, group, consumer)).await } #[doc = " Completely destroys the consumer group."] @@ -4044,7 +4065,7 @@ impl Guest for Commands { key: String, group: String, ) -> Result { - query(connection, ("XGROUP", "DESTROY", key, group)).await + exchange(connection, ("XGROUP", "DESTROY", key, group)).await } #[doc = " Set the last delivered ID for a consumer group."] @@ -4059,7 +4080,7 @@ impl Guest for Commands { id: StreamId, entriesread: Option, ) -> Result<(), Error> { - query( + exchange( connection, ( "XGROUP", @@ -4083,7 +4104,7 @@ impl Guest for Commands { key: String, group: String, ) -> Result, Error> { - query(connection, ("XINFO", "CONSUMERS", key, group)).await + exchange(connection, ("XINFO", "CONSUMERS", key, group)).await } #[doc = " Consumer groups of the stream stored at ."] @@ -4092,7 +4113,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/xinfo-groups/"] #[allow(async_fn_in_trait)] async fn xinfo_groups(connection: &Connection, key: String) -> Result, Error> { - query(connection, ("XINFO", "GROUPS", key)).await + exchange(connection, ("XINFO", "GROUPS", key)).await } #[doc = " Information about the stream stored at ."] @@ -4103,7 +4124,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/xinfo-stream/"] #[allow(async_fn_in_trait)] async fn xinfo_stream(connection: &Connection, key: String) -> Result { - query(connection, ("XINFO", "STREAM", key)).await + exchange(connection, ("XINFO", "STREAM", key)).await } #[doc = " Information about the stream stored at ."] @@ -4118,7 +4139,7 @@ impl Guest for Commands { key: String, count: Option, ) -> Result { - query( + exchange( connection, ("XINFO", "STREAM", key, "FULL", NamedOption("COUNT", count)), ) @@ -4131,7 +4152,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/xlen/"] #[allow(async_fn_in_trait)] async fn xlen(connection: &Connection, key: String) -> Result { - query(connection, ("XLEN", key)).await + exchange(connection, ("XLEN", key)).await } #[doc = " List of pending messages."] @@ -4146,7 +4167,7 @@ impl Guest for Commands { key: String, group: String, ) -> Result { - query(connection, ("XPENDING", key, group)).await + exchange(connection, ("XPENDING", key, group)).await } #[doc = " List of pending messages."] @@ -4166,7 +4187,7 @@ impl Guest for Commands { count: Integer, consumer: Option, ) -> Result, Error> { - query( + exchange( connection, ( "XPENDING", @@ -4194,7 +4215,7 @@ impl Guest for Commands { end: StreamId, count: Option, ) -> Result)>, Error> { - query( + exchange( connection, ("XRANGE", key, start, end, NamedOption("COUNT", count)), ) @@ -4213,7 +4234,7 @@ impl Guest for Commands { keys: Vec, ids: Vec, ) -> Result)>>>, Error> { - query( + exchange( connection, ( "XREAD", @@ -4245,7 +4266,7 @@ impl Guest for Commands { keys: Vec, ids: Vec, ) -> Result>)>>>, Error> { - query( + exchange( connection, ( "XREADGROUP", @@ -4275,7 +4296,7 @@ impl Guest for Commands { start: StreamId, count: Option, ) -> Result)>, Error> { - query( + exchange( connection, ("XREVRANGE", key, end, start, NamedOption("COUNT", count)), ) @@ -4294,7 +4315,7 @@ impl Guest for Commands { entries_added: Option, max_deleted_id: Option, ) -> Result<(), Error> { - query( + exchange( connection, ( "XSETID", @@ -4317,7 +4338,7 @@ impl Guest for Commands { key: String, trim: StreamTrim, ) -> Result { - query(connection, ("XTRIM", key, trim)).await + exchange(connection, ("XTRIM", key, trim)).await } #[doc = " Adds the members to the set stored at key, returning how many were not already members."] @@ -4330,7 +4351,7 @@ impl Guest for Commands { key: String, members: Vec, ) -> Result { - query(connection, ("SADD", key, NonEmpty::new("members", members))).await + exchange(connection, ("SADD", key, NonEmpty::new("members", members))).await } #[doc = " The number of members in the set stored at key."] @@ -4339,7 +4360,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/scard/"] #[allow(async_fn_in_trait)] async fn scard(connection: &Connection, key: String) -> Result { - query(connection, ("SCARD", key)).await + exchange(connection, ("SCARD", key)).await } #[doc = " The members of the first set that are not in any of the others."] @@ -4348,7 +4369,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/sdiff/"] #[allow(async_fn_in_trait)] async fn sdiff(connection: &Connection, keys: Vec) -> Result, Error> { - query(connection, ("SDIFF", NonEmpty::new("keys", keys))).await + exchange(connection, ("SDIFF", NonEmpty::new("keys", keys))).await } #[doc = " Stores the members of the first set that are not in any of the others, returning how many."] @@ -4361,7 +4382,7 @@ impl Guest for Commands { destination: String, keys: Vec, ) -> Result { - query( + exchange( connection, ("SDIFFSTORE", destination, NonEmpty::new("keys", keys)), ) @@ -4374,7 +4395,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/sinter/"] #[allow(async_fn_in_trait)] async fn sinter(connection: &Connection, keys: Vec) -> Result, Error> { - query(connection, ("SINTER", NonEmpty::new("keys", keys))).await + exchange(connection, ("SINTER", NonEmpty::new("keys", keys))).await } #[doc = " The number of members in all of the sets, counting stops at the limit."] @@ -4387,7 +4408,7 @@ impl Guest for Commands { keys: Vec, limit: Option, ) -> Result { - query( + exchange( connection, ( "SINTERCARD", @@ -4408,7 +4429,7 @@ impl Guest for Commands { destination: String, keys: Vec, ) -> Result { - query( + exchange( connection, ("SINTERSTORE", destination, NonEmpty::new("keys", keys)), ) @@ -4427,7 +4448,7 @@ impl Guest for Commands { key: String, member: String, ) -> Result { - query(connection, ("SISMEMBER", key, member)).await + exchange(connection, ("SISMEMBER", key, member)).await } #[doc = " Whether the member is in the set stored at key, none when the key does not exist."] @@ -4443,7 +4464,7 @@ impl Guest for Commands { member: String, ) -> Result, Error> { // -1 when the key does not exist - let member: OrNever = query(connection, ("SISMEMBER", key, member, "XX")).await?; + let member: OrNever = exchange(connection, ("SISMEMBER", key, member, "XX")).await?; Ok(member.0) } @@ -4453,7 +4474,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/smembers/"] #[allow(async_fn_in_trait)] async fn smembers(connection: &Connection, key: String) -> Result, Error> { - query(connection, ("SMEMBERS", key)).await + exchange(connection, ("SMEMBERS", key)).await } #[doc = " Whether each of the members is in the set stored at key."] @@ -4466,7 +4487,7 @@ impl Guest for Commands { key: String, members: Vec, ) -> Result, Error> { - query( + exchange( connection, ("SMISMEMBER", key, NonEmpty::new("members", members)), ) @@ -4484,7 +4505,7 @@ impl Guest for Commands { destination: String, member: String, ) -> Result { - query(connection, ("SMOVE", source, destination, member)).await + exchange(connection, ("SMOVE", source, destination, member)).await } #[doc = " Removes and returns random members of the set stored at key, up to the count."] @@ -4500,10 +4521,10 @@ impl Guest for Commands { count: Option, ) -> Result, Error> { if count.is_some() { - return query(connection, ("SPOP", key, count)).await; + return exchange(connection, ("SPOP", key, count)).await; } // without a count the reply is the member itself, or null - let member: Option = query(connection, ("SPOP", key)).await?; + let member: Option = exchange(connection, ("SPOP", key)).await?; Ok(member.into_iter().collect()) } @@ -4521,10 +4542,10 @@ impl Guest for Commands { count: Option, ) -> Result, Error> { if count.is_some() { - return query(connection, ("SRANDMEMBER", key, count)).await; + return exchange(connection, ("SRANDMEMBER", key, count)).await; } // without a count the reply is the member itself, or null - let member: Option = query(connection, ("SRANDMEMBER", key)).await?; + let member: Option = exchange(connection, ("SRANDMEMBER", key)).await?; Ok(member.into_iter().collect()) } @@ -4538,7 +4559,7 @@ impl Guest for Commands { key: String, members: Vec, ) -> Result { - query(connection, ("SREM", key, NonEmpty::new("members", members))).await + exchange(connection, ("SREM", key, NonEmpty::new("members", members))).await } #[doc = " Incrementally iterate over the members of the set stored at key."] @@ -4553,7 +4574,7 @@ impl Guest for Commands { match_: Option, count: Option, ) -> Result<(Option, Vec), Error> { - let (cursor, items): (NextCursor, Vec) = query( + let (cursor, items): (NextCursor, Vec) = exchange( connection, ( "SSCAN", @@ -4573,7 +4594,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/sunion/"] #[allow(async_fn_in_trait)] async fn sunion(connection: &Connection, keys: Vec) -> Result, Error> { - query(connection, ("SUNION", NonEmpty::new("keys", keys))).await + exchange(connection, ("SUNION", NonEmpty::new("keys", keys))).await } #[doc = " Stores the members in any of the sets, returning how many."] @@ -4586,7 +4607,7 @@ impl Guest for Commands { destination: String, keys: Vec, ) -> Result { - query( + exchange( connection, ("SUNIONSTORE", destination, NonEmpty::new("keys", keys)), ) @@ -4607,7 +4628,7 @@ impl Guest for Commands { opts: Option, ) -> Result>>, Error> { let row = sort_row_len(&opts); - let values: Vec> = query(connection, ("SORT", key, opts)).await?; + let values: Vec> = exchange(connection, ("SORT", key, opts)).await?; Ok(values.chunks(row).map(<[_]>::to_vec).collect()) } @@ -4623,7 +4644,7 @@ impl Guest for Commands { opts: Option, destination: String, ) -> Result { - query(connection, ("SORT", key, opts, "STORE", destination)).await + exchange(connection, ("SORT", key, opts, "STORE", destination)).await } #[doc = " The read-only variant of `sort`, e.g. for replicas."] @@ -4637,7 +4658,7 @@ impl Guest for Commands { opts: Option, ) -> Result>>, Error> { let row = sort_row_len(&opts); - let values: Vec> = query(connection, ("SORT_RO", key, opts)).await?; + let values: Vec> = exchange(connection, ("SORT_RO", key, opts)).await?; Ok(values.chunks(row).map(<[_]>::to_vec).collect()) } @@ -4666,7 +4687,7 @@ impl Guest for Commands { end, NamedOption("COUNT", count), ); - query(connection, command).await + exchange(connection, command).await } #[doc = " Removes the member with the highest score from the first non-empty sorted set, waiting up to"] @@ -4685,7 +4706,7 @@ impl Guest for Commands { keys: Vec, timeout: DurationMilliseconds, ) -> Result, Error> { - query( + exchange( connection, ( "BZPOPMAX", @@ -4712,7 +4733,7 @@ impl Guest for Commands { keys: Vec, timeout: DurationMilliseconds, ) -> Result, Error> { - query( + exchange( connection, ( "BZPOPMIN", @@ -4747,7 +4768,7 @@ impl Guest for Commands { Flag("CH", changed), NonEmpty::new("score-and-members", score_and_members), ); - query(connection, command).await + exchange(connection, command).await } #[doc = " Increments the score of the member in the sorted set stored at key, returning the new score,"] @@ -4769,7 +4790,7 @@ impl Guest for Commands { let command = ( "ZADD", key, condition, comparison, "INCR", increment, member, ); - query(connection, command).await + exchange(connection, command).await } #[doc = " The number of members in the sorted set stored at key."] @@ -4778,7 +4799,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/zcard/"] #[allow(async_fn_in_trait)] async fn zcard(connection: &Connection, key: String) -> Result { - query(connection, ("ZCARD", key)).await + exchange(connection, ("ZCARD", key)).await } #[doc = " The number of members of the sorted set stored at key with scores in the range."] @@ -4792,7 +4813,7 @@ impl Guest for Commands { min: ScoreBound, max: ScoreBound, ) -> Result { - query( + exchange( connection, ( "ZCOUNT", @@ -4815,7 +4836,7 @@ impl Guest for Commands { #[doc = " https://valkey.io/commands/zdiff/"] #[allow(async_fn_in_trait)] async fn zdiff(connection: &Connection, keys: Vec) -> Result, Error> { - query(connection, ("ZDIFF", NumKeys(NonEmpty::new("keys", keys)))).await + exchange(connection, ("ZDIFF", NumKeys(NonEmpty::new("keys", keys)))).await } #[doc = " The members, and their scores, of the first sorted set that are not in any of the others."] @@ -4829,7 +4850,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, ) -> Result, Error> { - query( + exchange( connection, ("ZDIFF", NumKeys(NonEmpty::new("keys", keys)), "WITHSCORES"), ) @@ -4847,7 +4868,7 @@ impl Guest for Commands { destination: String, keys: Vec, ) -> Result { - query( + exchange( connection, ( "ZDIFFSTORE", @@ -4869,7 +4890,7 @@ impl Guest for Commands { increment: Double, member: String, ) -> Result { - query(connection, ("ZINCRBY", key, increment, member)).await + exchange(connection, ("ZINCRBY", key, increment, member)).await } #[doc = " The members in all of the sorted sets."] @@ -4887,7 +4908,7 @@ impl Guest for Commands { weights: Option>, aggregate: Option, ) -> Result, Error> { - query( + exchange( connection, ( "ZINTER", @@ -4919,7 +4940,7 @@ impl Guest for Commands { aggregate, "WITHSCORES", ); - query(connection, command).await + exchange(connection, command).await } #[doc = " The number of members in all of the sorted sets, counting stops at the limit."] @@ -4932,7 +4953,7 @@ impl Guest for Commands { keys: Vec, limit: Option, ) -> Result { - query( + exchange( connection, ( "ZINTERCARD", @@ -4962,7 +4983,7 @@ impl Guest for Commands { NamedOption("WEIGHTS", NonEmpty::optional("weights", weights)), aggregate, ); - query(connection, command).await + exchange(connection, command).await } #[doc = " The number of members of the sorted set stored at key in the range, for members of equal"] @@ -4977,7 +4998,7 @@ impl Guest for Commands { min: LexBound, max: LexBound, ) -> Result { - query( + exchange( connection, ( "ZLEXCOUNT", @@ -5004,7 +5025,7 @@ impl Guest for Commands { end: PopEnd, count: Option, ) -> Result)>, Error> { - query( + exchange( connection, ( "ZMPOP", @@ -5027,7 +5048,7 @@ impl Guest for Commands { key: String, members: Vec, ) -> Result>, Error> { - query( + exchange( connection, ("ZMSCORE", key, NonEmpty::new("members", members)), ) @@ -5045,7 +5066,7 @@ impl Guest for Commands { key: String, count: Option, ) -> Result, Error> { - let members: ScoredMembers = query(connection, ("ZPOPMAX", key, count)).await?; + let members: ScoredMembers = exchange(connection, ("ZPOPMAX", key, count)).await?; Ok(members.0) } @@ -5060,7 +5081,7 @@ impl Guest for Commands { key: String, count: Option, ) -> Result, Error> { - let members: ScoredMembers = query(connection, ("ZPOPMIN", key, count)).await?; + let members: ScoredMembers = exchange(connection, ("ZPOPMIN", key, count)).await?; Ok(members.0) } @@ -5080,10 +5101,10 @@ impl Guest for Commands { count: Option, ) -> Result, Error> { if count.is_some() { - return query(connection, ("ZRANDMEMBER", key, count)).await; + return exchange(connection, ("ZRANDMEMBER", key, count)).await; } // without a count the reply is the member itself, or null - let member: Option = query(connection, ("ZRANDMEMBER", key)).await?; + let member: Option = exchange(connection, ("ZRANDMEMBER", key)).await?; Ok(member.into_iter().collect()) } @@ -5102,7 +5123,7 @@ impl Guest for Commands { key: String, count: Integer, ) -> Result, Error> { - query(connection, ("ZRANDMEMBER", key, count, "WITHSCORES")).await + exchange(connection, ("ZRANDMEMBER", key, count, "WITHSCORES")).await } #[doc = " The members of the sorted set stored at key in the range, from the lowest score, or the"] @@ -5126,7 +5147,7 @@ impl Guest for Commands { xx: bool, ) -> Result>, Error> { let range = Zrange { range, rev, limit }; - query(connection, ("ZRANGE", key, range, Flag("XX", xx))).await + exchange(connection, ("ZRANGE", key, range, Flag("XX", xx))).await } #[doc = " The members of the sorted set stored at key in the range, and their scores, from the lowest"] @@ -5154,7 +5175,7 @@ impl Guest for Commands { }; let range = Zrange { range, rev, limit }; let command = ("ZRANGE", key, range, "WITHSCORES", Flag("XX", xx)); - query(connection, command).await + exchange(connection, command).await } #[doc = " Stores the members of the sorted set stored at source in the range, returning how many."] @@ -5173,7 +5194,7 @@ impl Guest for Commands { limit: Option, ) -> Result { let range = Zrange { range, rev, limit }; - query(connection, ("ZRANGESTORE", destination, source, range)).await + exchange(connection, ("ZRANGESTORE", destination, source, range)).await } #[doc = " The position of the member in the sorted set stored at key, from the lowest score, none when"] @@ -5189,7 +5210,7 @@ impl Guest for Commands { key: String, member: String, ) -> Result, Error> { - query(connection, ("ZRANK", key, member)).await + exchange(connection, ("ZRANK", key, member)).await } #[doc = " The position, and the score, of the member in the sorted set stored at key, from the lowest"] @@ -5205,7 +5226,7 @@ impl Guest for Commands { key: String, member: String, ) -> Result, Error> { - query(connection, ("ZRANK", key, member, "WITHSCORE")).await + exchange(connection, ("ZRANK", key, member, "WITHSCORE")).await } #[doc = " Removes the members from the sorted set stored at key, returning how many were members."] @@ -5218,7 +5239,7 @@ impl Guest for Commands { key: String, members: Vec, ) -> Result { - query(connection, ("ZREM", key, NonEmpty::new("members", members))).await + exchange(connection, ("ZREM", key, NonEmpty::new("members", members))).await } #[doc = " Removes the members of the sorted set stored at key in the range, for members of equal score,"] @@ -5233,7 +5254,7 @@ impl Guest for Commands { min: LexBound, max: LexBound, ) -> Result { - query( + exchange( connection, ( "ZREMRANGEBYLEX", @@ -5260,7 +5281,7 @@ impl Guest for Commands { start: Integer, stop: Integer, ) -> Result { - query(connection, ("ZREMRANGEBYRANK", key, start, stop)).await + exchange(connection, ("ZREMRANGEBYRANK", key, start, stop)).await } #[doc = " Removes the members of the sorted set stored at key with scores in the range, returning how"] @@ -5275,7 +5296,7 @@ impl Guest for Commands { min: ScoreBound, max: ScoreBound, ) -> Result { - query( + exchange( connection, ( "ZREMRANGEBYSCORE", @@ -5303,7 +5324,7 @@ impl Guest for Commands { key: String, member: String, ) -> Result, Error> { - query(connection, ("ZREVRANK", key, member)).await + exchange(connection, ("ZREVRANK", key, member)).await } #[doc = " The position, and the score, of the member in the sorted set stored at key, from the highest"] @@ -5319,7 +5340,7 @@ impl Guest for Commands { key: String, member: String, ) -> Result, Error> { - query(connection, ("ZREVRANK", key, member, "WITHSCORE")).await + exchange(connection, ("ZREVRANK", key, member, "WITHSCORE")).await } #[doc = " Incrementally iterate over the members of the sorted set stored at key, and their scores."] @@ -5337,7 +5358,7 @@ impl Guest for Commands { count: Option, ) -> Result<(Option, Vec<(String, Double)>), Error> { // the members and scores are a flat array, the scores bulk strings - let (cursor, members): (NextCursor, FlatPairs) = query( + let (cursor, members): (NextCursor, FlatPairs) = exchange( connection, ( "ZSCAN", @@ -5365,7 +5386,7 @@ impl Guest for Commands { match_: Option, count: Option, ) -> Result<(Option, Vec), Error> { - let (cursor, items): (NextCursor, Vec) = query( + let (cursor, items): (NextCursor, Vec) = exchange( connection, ( "ZSCAN", @@ -5390,7 +5411,7 @@ impl Guest for Commands { key: String, member: String, ) -> Result, Error> { - query(connection, ("ZSCORE", key, member)).await + exchange(connection, ("ZSCORE", key, member)).await } #[doc = " The members in any of the sorted sets."] @@ -5408,7 +5429,7 @@ impl Guest for Commands { weights: Option>, aggregate: Option, ) -> Result, Error> { - query( + exchange( connection, ( "ZUNION", @@ -5440,7 +5461,7 @@ impl Guest for Commands { aggregate, "WITHSCORES", ); - query(connection, command).await + exchange(connection, command).await } #[doc = " Stores the members in any of the sorted sets, with their combined scores, returning how many."] @@ -5462,7 +5483,7 @@ impl Guest for Commands { NamedOption("WEIGHTS", NonEmpty::optional("weights", weights)), aggregate, ); - query(connection, command).await + exchange(connection, command).await } } diff --git a/components/resp/Cargo.toml b/components/resp/Cargo.toml deleted file mode 100644 index 58fdd9a..0000000 --- a/components/resp/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "resp" -version = "0.1.0" -edition = "2024" -license = "Apache-2.0" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -resp-codec = { workspace = true } -wit-bindgen = { workspace = true } - -[dev-dependencies] -test-harness = { workspace = true } -tokio = { workspace = true } -wasmtime = { workspace = true } diff --git a/components/resp/README.md b/components/resp/README.md deleted file mode 100644 index f038704..0000000 --- a/components/resp/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# resp - -Encodes and decodes RESP values, the `componentized:valkey/resp` interface. - -The other components share the RESP types with this one, a composition that uses them includes it. diff --git a/components/resp/src/lib.rs b/components/resp/src/lib.rs deleted file mode 100644 index 28c9e4d..0000000 --- a/components/resp/src/lib.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Exports `componentized:valkey/resp` with the shared codec. -//! -//! An exported interface has types of its own, they are converted to and from the shared codec's -//! types. A value is not recursive, nested values are encoded RESP, so each conversion is one -//! match. - -// the test harness provides a main -#![cfg_attr(not(test), no_main)] - -use exports::componentized::valkey::resp::{Error, Guest, Value}; - -struct Component; - -impl Guest for Component { - fn decode(data: Vec) -> Result { - resp_codec::decode(data).map(from_codec) - } - - fn encode(value: Value) -> Result, Error> { - Ok(resp_codec::encode(to_codec(value))) - } -} - -fn to_codec(value: Value) -> resp_codec::Value { - use resp_codec::Value as V; - match value { - Value::String(value) => V::String(value), - Value::Error(value) => V::Error(value), - Value::Integer(value) => V::Integer(value), - Value::BulkString(value) => V::BulkString(value), - Value::Array(value) => V::Array(value), - Value::Null => V::Null, - Value::Boolean(value) => V::Boolean(value), - Value::Double(value) => V::Double(value), - Value::BigNumber(value) => V::BigNumber(value), - Value::BulkError(value) => V::BulkError(value), - Value::VerbatimString(value) => V::VerbatimString(value), - Value::Map(value) => V::Map(value), - Value::Set(value) => V::Set(value), - Value::Push(value) => V::Push(value), - } -} - -fn from_codec(value: resp_codec::Value) -> Value { - use resp_codec::Value as V; - match value { - V::String(value) => Value::String(value), - V::Error(value) => Value::Error(value), - V::Integer(value) => Value::Integer(value), - V::BulkString(value) => Value::BulkString(value), - V::Array(value) => Value::Array(value), - V::Null => Value::Null, - V::Boolean(value) => Value::Boolean(value), - V::Double(value) => Value::Double(value), - V::BigNumber(value) => Value::BigNumber(value), - V::BulkError(value) => Value::BulkError(value), - V::VerbatimString(value) => Value::VerbatimString(value), - V::Map(value) => Value::Map(value), - V::Set(value) => Value::Set(value), - V::Push(value) => Value::Push(value), - } -} - -wit_bindgen::generate!({ - // `path` is relative to the manifest, `components/resp/` - path: "../wit", - world: "resp", - generate_all, -}); - -export!(Component); diff --git a/components/resp/tests/resp.rs b/components/resp/tests/resp.rs deleted file mode 100644 index e8ba771..0000000 --- a/components/resp/tests/resp.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Tests for the `resp` component, which converts between its exported types and the codec's. - -use test_harness::Harness; -use test_harness::resp_bindings::exports::componentized::valkey::resp::Value; - -/// One value of every type, nested values are encoded RESP. -fn values() -> Vec { - vec![ - Value::String("OK".to_string()), - Value::Error("ERR oops".to_string()), - Value::Integer(-42), - Value::BulkString("hello\r\nworld".to_string()), - Value::Array(vec![b"+a\r\n".to_vec(), b":1\r\n".to_vec()]), - Value::Null, - Value::Boolean(true), - Value::Double(1.5), - Value::BigNumber("3492890328409238509324850943850943825024385".to_string()), - Value::BulkError("SYNTAX invalid".to_string()), - Value::VerbatimString(("txt".to_string(), "Some string".to_string())), - Value::Map(vec![(b"+key\r\n".to_vec(), b"+value\r\n".to_vec())]), - Value::Set(vec![b"+a\r\n".to_vec(), b"+b\r\n".to_vec()]), - Value::Push(vec![b"+message\r\n".to_vec()]), - ] -} - -#[tokio::test(flavor = "multi_thread")] -async fn values_round_trip() -> wasmtime::Result<()> { - let mut harness = Harness::new("resp").await?; - harness - .run(async |accessor, exports| { - for value in values() { - let encoded = exports - .resp() - .call_encode(accessor, value.clone()) - .await? - .expect("encode"); - let decoded = exports - .resp() - .call_decode(accessor, encoded) - .await? - .expect("decode"); - assert_eq!(decoded, value); - } - Ok(()) - }) - .await -} - -#[tokio::test(flavor = "multi_thread")] -async fn encodes_resp() -> wasmtime::Result<()> { - let mut harness = Harness::new("resp").await?; - harness - .run(async |accessor, exports| { - let encoded = exports - .resp() - .call_encode( - accessor, - Value::Array(vec![b"$3\r\nGET\r\n".to_vec(), b"$1\r\na\r\n".to_vec()]), - ) - .await?; - assert_eq!(encoded, Ok(b"*2\r\n$3\r\nGET\r\n$1\r\na\r\n".to_vec())); - Ok(()) - }) - .await -} - -#[tokio::test(flavor = "multi_thread")] -async fn invalid_resp_fails_to_decode() -> wasmtime::Result<()> { - let mut harness = Harness::new("resp").await?; - harness - .run(async |accessor, exports| { - let decoded = exports - .resp() - .call_decode(accessor, b"?nope\r\n".to_vec()) - .await?; - assert!(decoded.is_err(), "decoded {decoded:?}"); - Ok(()) - }) - .await -} diff --git a/components/wit/worlds.wit b/components/wit/worlds.wit index e11f898..9e800c8 100644 --- a/components/wit/worlds.wit +++ b/components/wit/worlds.wit @@ -1,10 +1,5 @@ package componentized:valkey-components; -/// The RESP codec, for the components that import the resp types. -world resp { - export componentized:valkey/resp@0.3.0-dev; -} - /// The mechanics of a connection, independent of the transport. world connection { import wasi:clocks/monotonic-clock@0.3.0; diff --git a/components/commands/src/command.rs b/crates/resp-codec/src/command.rs similarity index 95% rename from components/commands/src/command.rs rename to crates/resp-codec/src/command.rs index f4fd874..0a42407 100644 --- a/components/commands/src/command.rs +++ b/crates/resp-codec/src/command.rs @@ -1,40 +1,17 @@ -use crate::exports::componentized::valkey::commands::{ +use crate::bindings::componentized::valkey::types::{ Aggregate, BgsaveOperation, BitRange, BitfieldEncoding, BitfieldGet, BitfieldIncrby, BitfieldOffset, BitfieldOperation, BitfieldOverflow, BitfieldSet, ChannelRule, ClientFilter, ClientPauseMode, ClientSetinfoAttr, ClientTrackingMode, ClientTrackingOptions, ClientType, ClientUnblockType, ClusterFailoverMode, ClusterResetMode, ClusterSetslotState, - CommandListFilter, CommandRule, CommandlogType, Connection, Cursor, DbRule, - DurationMilliseconds, DurationSeconds, Error, FailoverTarget, FlushMode, GeoCoordinates, - GeoCount, GeoFrom, GeoShape, GeoUnit, GeoaddCondition, GetexTtl, HsetexCondition, - HsetexFieldCondition, KeyRule, KeyType, LexBound, Limit, LinsertPosition, ListSide, - MigrateAuth, MsetexPrecondition, PopEnd, Rule, RuleSelector, ScoreBound, SetPrecondition, - SetTtl, ShutdownSave, SlotMigrationGroup, SortOpts, SortOrder, StreamTrim, - StreamTrimEfficiency, TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, UserRule, Value, - XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, + CommandListFilter, CommandRule, CommandlogType, Cursor, DbRule, DurationMilliseconds, + DurationSeconds, Error, FailoverTarget, FlushMode, GeoCoordinates, GeoCount, GeoFrom, GeoShape, + GeoUnit, GeoaddCondition, GetexTtl, HsetexCondition, HsetexFieldCondition, KeyRule, KeyType, + LexBound, Limit, LinsertPosition, ListSide, MigrateAuth, MsetexPrecondition, PopEnd, Rule, + RuleSelector, ScoreBound, SetPrecondition, SetTtl, ShutdownSave, SlotMigrationGroup, SortOpts, + SortOrder, StreamTrim, StreamTrimEfficiency, TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, + UserRule, XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, }; -use crate::value::{FromValue, OrZeroId, ZERO_ID}; - -/// Send a command, its words as bulk strings, and wait for the reply. -/// -/// An error reply fails the command, see `connection.exchange`. -async fn exchange(connection: &Connection, command: impl IntoCommand) -> Result { - connection.exchange(command.into_command()?).await -} - -/// Send a command, its words as bulk strings, without waiting for a reply. -/// -/// An error reply fails the command, see `connection.emit`. -pub(crate) async fn emit(connection: &Connection, command: impl IntoCommand) -> Result<(), Error> { - connection.emit(command.into_command()?).await -} - -/// Send a command, see `exchange`, and convert its reply. -pub(crate) async fn query( - connection: &Connection, - command: impl IntoCommand, -) -> Result { - T::from_value(exchange(connection, command).await?) -} +use crate::value::{OrZeroId, ZERO_ID}; /// Converts a Rust type into the words of a command, e.g. `Option` into no word, or one. /// diff --git a/crates/resp-codec/src/lib.rs b/crates/resp-codec/src/lib.rs index b2928b2..1e66ee7 100644 --- a/crates/resp-codec/src/lib.rs +++ b/crates/resp-codec/src/lib.rs @@ -1,4 +1,5 @@ -//! The RESP codec, and the `componentized:valkey/types` it works with. +//! The RESP codec, the `componentized:valkey/types` it works with, and the conversions between +//! those types and commands: `IntoCommand` for a command's words, `FromValue` for its reply. //! //! A component that imports an interface of the package uses these types for it, so every //! component shares them, e.g. @@ -9,9 +10,6 @@ //! ... //! }); //! ``` -//! -//! The `resp` component exports the interface, converting between its own generated types and -//! these. pub mod bindings { wit_bindgen::generate!({ @@ -31,6 +29,10 @@ pub mod bindings { } mod codec; +pub mod command; +pub mod value; pub use bindings::componentized::valkey::types::{NestedValue, RespError as Error, Value}; pub use codec::{decode, encode, frame_len}; +pub use command::IntoCommand; +pub use value::FromValue; diff --git a/components/commands/src/value.rs b/crates/resp-codec/src/value.rs similarity index 99% rename from components/commands/src/value.rs rename to crates/resp-codec/src/value.rs index 80eaaa1..734d4fc 100644 --- a/components/commands/src/value.rs +++ b/crates/resp-codec/src/value.rs @@ -1,4 +1,4 @@ -use crate::exports::componentized::valkey::commands::{ +use crate::bindings::componentized::valkey::types::{ AclDryrunResult, AclListEntry, AclLogEntry, ChannelRule, ClientTrackinginfoReply, ClusterBumpepochResult, CommandArgument, CommandArgumentType, CommandDocsReply, CommandInfoReply, CommandRule, Cursor, DbRule, Double, DurationMilliseconds, DurationSeconds, @@ -13,7 +13,7 @@ use crate::exports::componentized::valkey::commands::{ XinfoStreamReplyFullGroup, XpendingExtendedEntry, XpendingSummary, }; -use resp_codec::NestedValue; +use crate::NestedValue; use wit_bindgen::rt::Map; /// Converts a reply into a Rust type, e.g. `Option` for a bulk string or null. @@ -1274,7 +1274,7 @@ fn unexpected(reply: &Value) -> Error { } fn decode(value: NestedValue) -> Result { - resp_codec::decode(value).map_err(Error::Resp) + crate::codec::decode(value).map_err(Error::Resp) } /// The nested values of an array, or a set. diff --git a/wit/resp.wit b/wit/resp.wit deleted file mode 100644 index b77c199..0000000 --- a/wit/resp.wit +++ /dev/null @@ -1,6 +0,0 @@ -/// Encodes and decodes RESP values. -interface resp { - use types.{resp-error as error, nested-value, value}; - decode: func(data: list) -> result; - encode: func(value: value) -> result, error>; -} diff --git a/wit/worlds.wit b/wit/worlds.wit index 7feda80..69497a9 100644 --- a/wit/worlds.wit +++ b/wit/worlds.wit @@ -4,7 +4,6 @@ world imports { import commands; import connection; import connector; - import resp; import tcp; import types; } From 86d0905563aeef8ec6958b6402e2c6520ad8d879 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 13 Oct 2026 16:34:02 -0400 Subject: [PATCH 21/37] rename crate, clarifying license Signed-off-by: Scott Andrews --- Cargo.lock | 26 +-- Cargo.toml | 4 +- NOTICE | 35 ++- components/commands/Cargo.toml | 2 +- components/commands/src/lib.rs | 8 +- components/connection/Cargo.toml | 2 +- components/connection/src/lib.rs | 10 +- components/dep-cli/Cargo.toml | 2 +- components/dep-cli/src/lib.rs | 6 +- components/tcp-connector/Cargo.toml | 2 +- components/tcp-connector/src/lib.rs | 2 +- components/tcp/Cargo.toml | 2 +- components/tcp/src/lib.rs | 14 +- crates/componentized-valkey/Cargo.toml | 10 + crates/componentized-valkey/LICENSE | 201 ++++++++++++++++++ .../NOTICE} | 13 ++ crates/componentized-valkey/README.md | 11 + .../src/codec.rs | 8 +- .../src/command.rs | 2 +- crates/componentized-valkey/src/lib.rs | 44 ++++ .../src/value.rs | 16 +- crates/resp-codec/Cargo.toml | 9 - crates/resp-codec/LICENSE-APACHE.txt | 201 ------------------ crates/resp-codec/LICENSE.txt | 5 - crates/resp-codec/README.md | 3 - crates/resp-codec/src/lib.rs | 38 ---- crates/test-harness/Cargo.toml | 2 +- crates/test-harness/src/lib.rs | 12 +- crates/test-harness/src/server.rs | 4 +- 29 files changed, 378 insertions(+), 316 deletions(-) create mode 100644 crates/componentized-valkey/Cargo.toml create mode 100644 crates/componentized-valkey/LICENSE rename crates/{resp-codec/LICENSE-MIT.txt => componentized-valkey/NOTICE} (66%) create mode 100644 crates/componentized-valkey/README.md rename crates/{resp-codec => componentized-valkey}/src/codec.rs (98%) rename crates/{resp-codec => componentized-valkey}/src/command.rs (99%) create mode 100644 crates/componentized-valkey/src/lib.rs rename crates/{resp-codec => componentized-valkey}/src/value.rs (98%) delete mode 100644 crates/resp-codec/Cargo.toml delete mode 100644 crates/resp-codec/LICENSE-APACHE.txt delete mode 100644 crates/resp-codec/LICENSE.txt delete mode 100644 crates/resp-codec/README.md delete mode 100644 crates/resp-codec/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index b8eeabd..34fec74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,20 +152,27 @@ dependencies = [ name = "commands" version = "0.1.0" dependencies = [ - "resp-codec", + "componentized-valkey", "test-harness", "tokio", "wasmtime", "wit-bindgen", ] +[[package]] +name = "componentized-valkey" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "connection" version = "0.1.0" dependencies = [ + "componentized-valkey", "futures-channel", "futures-core", - "resp-codec", "test-harness", "tokio", "wasmtime", @@ -408,9 +415,9 @@ dependencies = [ name = "dep-cli" version = "0.1.0" dependencies = [ + "componentized-valkey", "futures-channel", "futures-core", - "resp-codec", "wit-bindgen", ] @@ -1199,13 +1206,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "resp-codec" -version = "0.1.0" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "rustc-demangle" version = "0.1.28" @@ -1424,9 +1424,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" name = "tcp" version = "0.1.0" dependencies = [ + "componentized-valkey", "futures-channel", "futures-core", - "resp-codec", "test-harness", "tokio", "wasmtime", @@ -1437,7 +1437,7 @@ dependencies = [ name = "tcp-connector" version = "0.1.0" dependencies = [ - "resp-codec", + "componentized-valkey", "wit-bindgen", ] @@ -1467,7 +1467,7 @@ dependencies = [ name = "test-harness" version = "0.1.0" dependencies = [ - "resp-codec", + "componentized-valkey", "tokio", "wasmtime", "wasmtime-wasi", diff --git a/Cargo.toml b/Cargo.toml index b1cddb0..f738cf4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ "components/sample-http-incrementor", "components/tcp", "components/tcp-connector", - "crates/resp-codec", + "crates/componentized-valkey", "crates/test-harness", ] @@ -18,7 +18,7 @@ version = "0.3.0-dev" [workspace.dependencies] futures-channel = "0.3" futures-core = "0.3" -resp-codec = { path = "./crates/resp-codec" } +componentized-valkey = { path = "./crates/componentized-valkey" } test-harness = { path = "./crates/test-harness" } tokio = { version = "1", features = ["io-util", "macros", "net", "rt-multi-thread", "sync", "time"] } wasmtime = { version = "49", features = ["component-model-async"] } diff --git a/NOTICE b/NOTICE index 5397c13..ddea1ff 100644 --- a/NOTICE +++ b/NOTICE @@ -2,5 +2,36 @@ Componentized valkey Copyright 2025 the original author or authors. This product may include a number of subcomponents with separate copyright notices and license terms. -Your use of these subcomponents is subject to the terms and conditions of the subcomponent's license, -as noted in the LICENSE file. +Your use of these subcomponents is subject to the terms and conditions of the subcomponent's license. + +------------------------------------------------------------------------------- + +This product includes a RESP codec adapted from resp v1.0.3 +(https://github.com/iorust/resp/tree/v1.0.3), in crates/componentized-valkey/src/codec.rs, +used under the MIT license: + +Copyright (c) 2016-2022 IORust Developers + +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/components/commands/Cargo.toml b/components/commands/Cargo.toml index d0e6fcb..644a197 100644 --- a/components/commands/Cargo.toml +++ b/components/commands/Cargo.toml @@ -8,7 +8,7 @@ license = "Apache-2.0" crate-type = ["cdylib"] [dependencies] -resp-codec = { workspace = true } +componentized-valkey = { workspace = true } wit-bindgen = { workspace = true } [dev-dependencies] diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index 4acb1a4..d42df54 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -16,18 +16,18 @@ use crate::exports::componentized::valkey::commands::{ XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, ZaddComparison, ZaddCondition, ZrangeBy, ZrangeWithscoresBy, }; -use resp_codec::command::{ +use componentized_valkey::command::{ Bit, DecimalSeconds, Fields, Flag, KeysAndArgs, LexRange, NamedOption, NonEmpty, NumKeys, OnOff, ScoreRange, Zrange, }; -use resp_codec::value::{ +use componentized_valkey::value::{ AclList, AclUserRules, ClientList, CommandDocsList, CommandInfoList, FlatPairs, Migrated, NextCursor, OptionalKeyType, OrNever, OrZeroId, ScoredMembers, }; use wit_bindgen::rt::Map; -use resp_codec::{FromValue, IntoCommand}; +use componentized_valkey::{FromValue, IntoCommand}; /// Send a command, its words as bulk strings, and convert its reply. /// @@ -5492,7 +5492,7 @@ wit_bindgen::generate!({ world: "commands", // the resp types shared by every component with: { - "componentized:valkey/types@0.3.0-dev": resp_codec::bindings::componentized::valkey::types, + "componentized:valkey/types@0.3.0-dev": componentized_valkey::types, }, generate_all, }); diff --git a/components/connection/Cargo.toml b/components/connection/Cargo.toml index 7a8d537..704394b 100644 --- a/components/connection/Cargo.toml +++ b/components/connection/Cargo.toml @@ -10,7 +10,7 @@ crate-type = ["cdylib"] [dependencies] futures-channel = { workspace = true } futures-core = { workspace = true } -resp-codec = { workspace = true } +componentized-valkey = { workspace = true } wit-bindgen = { workspace = true, features = ["async-spawn", "inter-task-wakeup"] } [dev-dependencies] diff --git a/components/connection/src/lib.rs b/components/connection/src/lib.rs index f5a1f89..e5b398c 100644 --- a/components/connection/src/lib.rs +++ b/components/connection/src/lib.rs @@ -21,12 +21,12 @@ use std::future::poll_fn; use std::pin::Pin; use std::rc::{Rc, Weak}; +use componentized_valkey::types::{NestedValue, Value}; use exports::componentized::valkey::connection::{ ConnectOpts, Connection, DurationMilliseconds, Error, Guest, GuestConnection, }; use futures_channel::{mpsc, oneshot}; use futures_core::Stream; -use resp_codec::{NestedValue, Value}; use wasi::clocks::monotonic_clock::wait_for; use wit_bindgen::{FutureReader, FutureWriter, StreamReader, StreamWriter, spawn_local}; @@ -207,7 +207,7 @@ fn enqueue( let command = Value::Array( command .into_iter() - .map(|word| resp_codec::encode(Value::BulkString(word))) + .map(|word| componentized_valkey::codec::encode(Value::BulkString(word))) .collect(), ); inner @@ -285,7 +285,7 @@ async fn transaction( Value::Null => Ok(None), Value::Array(replies) => replies .into_iter() - .map(|reply| resp_codec::decode(reply).map_err(Error::Resp)) + .map(|reply| componentized_valkey::codec::decode(reply).map_err(Error::Resp)) .collect::>() .map(Some), reply => Err(unexpected(&reply)), @@ -514,7 +514,7 @@ fn server_properties(reply: Value) -> Result, Error> { } fn decode(value: NestedValue) -> Result { - resp_codec::decode(value).map_err(Error::Resp) + componentized_valkey::codec::decode(value).map_err(Error::Resp) } /// An error value from the server fails the command. @@ -551,7 +551,7 @@ wit_bindgen::generate!({ world: "connection", // the resp types shared by every component with: { - "componentized:valkey/types@0.3.0-dev": resp_codec::bindings::componentized::valkey::types, + "componentized:valkey/types@0.3.0-dev": componentized_valkey::types, }, generate_all, }); diff --git a/components/dep-cli/Cargo.toml b/components/dep-cli/Cargo.toml index 2e7cc1e..f37eaeb 100644 --- a/components/dep-cli/Cargo.toml +++ b/components/dep-cli/Cargo.toml @@ -10,5 +10,5 @@ crate-type = ["cdylib"] [dependencies] futures-channel = { workspace = true } futures-core = { workspace = true } -resp-codec = { workspace = true } +componentized-valkey = { workspace = true } wit-bindgen = { workspace = true } diff --git a/components/dep-cli/src/lib.rs b/components/dep-cli/src/lib.rs index 32fa5a9..f038c70 100644 --- a/components/dep-cli/src/lib.rs +++ b/components/dep-cli/src/lib.rs @@ -12,10 +12,10 @@ use std::task::Poll; use componentized::valkey::connection::{Connection, Error}; use componentized::valkey::connector; +use componentized_valkey::types::{NestedValue, Value}; use exports::wasi::cli::run::Guest; use futures_channel::mpsc; use futures_core::Stream; -use resp_codec::{NestedValue, Value}; use wasi::cli::{environment, stderr, stdin, stdout, types::ErrorCode}; use wit_bindgen::{FutureReader, StreamReader, StreamResult, StreamWriter}; @@ -377,7 +377,7 @@ fn render(value: Value) -> String { } fn render_nested(value: NestedValue) -> String { - match resp_codec::decode(value) { + match componentized_valkey::codec::decode(value) { Ok(value) => render(value), Err(error) => format!("(invalid) {error}"), } @@ -410,7 +410,7 @@ wit_bindgen::generate!({ world: "cli", // the resp types shared by every component with: { - "componentized:valkey/types@0.3.0-dev": resp_codec::bindings::componentized::valkey::types, + "componentized:valkey/types@0.3.0-dev": componentized_valkey::types, }, generate_all, }); diff --git a/components/tcp-connector/Cargo.toml b/components/tcp-connector/Cargo.toml index af3b52a..3f1e4e0 100644 --- a/components/tcp-connector/Cargo.toml +++ b/components/tcp-connector/Cargo.toml @@ -8,5 +8,5 @@ license = "Apache-2.0" crate-type = ["cdylib"] [dependencies] -resp-codec = { workspace = true } +componentized-valkey = { workspace = true } wit-bindgen = { workspace = true } diff --git a/components/tcp-connector/src/lib.rs b/components/tcp-connector/src/lib.rs index fb25d27..b48c0fd 100644 --- a/components/tcp-connector/src/lib.rs +++ b/components/tcp-connector/src/lib.rs @@ -67,7 +67,7 @@ wit_bindgen::generate!({ world: "tcp-connector", // the resp types shared by every component with: { - "componentized:valkey/types@0.3.0-dev": resp_codec::bindings::componentized::valkey::types, + "componentized:valkey/types@0.3.0-dev": componentized_valkey::types, }, generate_all, }); diff --git a/components/tcp/Cargo.toml b/components/tcp/Cargo.toml index 6848807..5fc71bd 100644 --- a/components/tcp/Cargo.toml +++ b/components/tcp/Cargo.toml @@ -10,7 +10,7 @@ crate-type = ["cdylib"] [dependencies] futures-channel = { workspace = true } futures-core = { workspace = true } -resp-codec = { workspace = true } +componentized-valkey = { workspace = true } wit-bindgen = { workspace = true, features = ["async-spawn", "inter-task-wakeup"] } [dev-dependencies] diff --git a/components/tcp/src/lib.rs b/components/tcp/src/lib.rs index 855771c..1f1818b 100644 --- a/components/tcp/src/lib.rs +++ b/components/tcp/src/lib.rs @@ -21,10 +21,10 @@ use std::pin::Pin; use std::rc::Rc; use componentized::valkey::connection::{ConnectOpts, Connection, Error}; +use componentized_valkey::types::Value; use exports::componentized::valkey::tcp::{Guest, GuestTransport, Transport}; use futures_channel::{mpsc, oneshot}; use futures_core::Stream; -use resp_codec::Value; use wasi::sockets::ip_name_lookup::{ErrorCode as LookupErrorCode, resolve_addresses}; use wasi::sockets::types::{ ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, @@ -164,11 +164,11 @@ async fn receive_pump( let result = 'pump: loop { // hand over each whole value buffered, a value may span several reads loop { - match resp_codec::frame_len(&buffered) { + match componentized_valkey::codec::frame_len(&buffered) { Ok(Some(len)) => { let rest = buffered.split_off(len); let frame = std::mem::replace(&mut buffered, rest); - match resp_codec::decode(frame) { + match componentized_valkey::codec::decode(frame) { Ok(value) => { if replies.write_one(value).await.is_some() { // nobody reads the values anymore @@ -209,7 +209,11 @@ async fn send_pump( return; }; while let Some(value) = requests.next().await { - if !bytes.write_all(resp_codec::encode(value)).await.is_empty() { + if !bytes + .write_all(componentized_valkey::codec::encode(value)) + .await + .is_empty() + { // the socket stopped sending break; } @@ -283,7 +287,7 @@ wit_bindgen::generate!({ world: "tcp", // the resp types shared by every component with: { - "componentized:valkey/types@0.3.0-dev": resp_codec::bindings::componentized::valkey::types, + "componentized:valkey/types@0.3.0-dev": componentized_valkey::types, }, generate_all, }); diff --git a/crates/componentized-valkey/Cargo.toml b/crates/componentized-valkey/Cargo.toml new file mode 100644 index 0000000..7f63e8c --- /dev/null +++ b/crates/componentized-valkey/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "componentized-valkey" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" +description = "Rust types and conversions for the componentized:valkey WIT package: the shared types, a RESP codec, and the conversions between commands and replies." +publish = false + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/crates/componentized-valkey/LICENSE b/crates/componentized-valkey/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/crates/componentized-valkey/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/resp-codec/LICENSE-MIT.txt b/crates/componentized-valkey/NOTICE similarity index 66% rename from crates/resp-codec/LICENSE-MIT.txt rename to crates/componentized-valkey/NOTICE index d1a0869..9b623b9 100644 --- a/crates/resp-codec/LICENSE-MIT.txt +++ b/crates/componentized-valkey/NOTICE @@ -1,3 +1,16 @@ +Componentized valkey +Copyright 2025 the original author or authors. + +This product may include a number of subcomponents with separate copyright notices and license terms. +Your use of these subcomponents is subject to the terms and conditions of the subcomponent's license, +as noted in the LICENSE file. + +------------------------------------------------------------------------------- + +This product includes a RESP codec adapted from resp v1.0.3 +(https://github.com/iorust/resp/tree/v1.0.3), in src/codec.rs, +used under the MIT license: + Copyright (c) 2016-2022 IORust Developers Permission is hereby granted, free of charge, to any diff --git a/crates/componentized-valkey/README.md b/crates/componentized-valkey/README.md new file mode 100644 index 0000000..20e8b7a --- /dev/null +++ b/crates/componentized-valkey/README.md @@ -0,0 +1,11 @@ +# componentized-valkey + +Rust support for the `componentized:valkey` WIT package: the shared types, a RESP codec, and the +conversions between Rust values and commands and replies. + +## License + +Licensed under the Apache License, Version 2.0, see [LICENSE](./LICENSE). + +The RESP codec, `src/codec.rs`, is adapted from https://github.com/iorust/resp/tree/v1.0.3, used +under the MIT license, see [NOTICE](./NOTICE). diff --git a/crates/resp-codec/src/codec.rs b/crates/componentized-valkey/src/codec.rs similarity index 98% rename from crates/resp-codec/src/codec.rs rename to crates/componentized-valkey/src/codec.rs index d1ae9f7..67da305 100644 --- a/crates/resp-codec/src/codec.rs +++ b/crates/componentized-valkey/src/codec.rs @@ -1,6 +1,6 @@ //! RESP serialization, from . -use crate::{Error, NestedValue, Value}; +use crate::types::{NestedValue, RespError as Error, Value}; /// up to 512 MB in length const RESP_MAX_SIZE: i64 = 512 * 1024 * 1024; @@ -9,7 +9,8 @@ const CRLF_BYTES: &'static [u8] = b"\r\n"; /// Encodes RESP value to RESP binary buffer. /// # Examples /// ``` -/// # use resp_codec::{Value, encode}; +/// # use componentized_valkey::codec::encode; +/// # use componentized_valkey::types::Value; /// let val = Value::String("OK".to_string()); /// assert_eq!(encode(val), b"+OK\r\n"); /// ``` @@ -124,7 +125,8 @@ fn buf_encode(value: Value, buf: &mut Vec) { /// /// # Examples /// ``` -/// # use resp_codec::{Value, decode, encode}; +/// # use componentized_valkey::codec::{decode, encode}; +/// # use componentized_valkey::types::Value; /// let value = Value::BulkString("Hello".to_string()); /// assert_eq!(decode(encode(value.clone())), Ok(value)); /// ``` diff --git a/crates/resp-codec/src/command.rs b/crates/componentized-valkey/src/command.rs similarity index 99% rename from crates/resp-codec/src/command.rs rename to crates/componentized-valkey/src/command.rs index 0a42407..b126e24 100644 --- a/crates/resp-codec/src/command.rs +++ b/crates/componentized-valkey/src/command.rs @@ -1,4 +1,4 @@ -use crate::bindings::componentized::valkey::types::{ +use crate::types::{ Aggregate, BgsaveOperation, BitRange, BitfieldEncoding, BitfieldGet, BitfieldIncrby, BitfieldOffset, BitfieldOperation, BitfieldOverflow, BitfieldSet, ChannelRule, ClientFilter, ClientPauseMode, ClientSetinfoAttr, ClientTrackingMode, ClientTrackingOptions, ClientType, diff --git a/crates/componentized-valkey/src/lib.rs b/crates/componentized-valkey/src/lib.rs new file mode 100644 index 0000000..5b0744c --- /dev/null +++ b/crates/componentized-valkey/src/lib.rs @@ -0,0 +1,44 @@ +//! Rust support for the `componentized:valkey` WIT package, for components that use or implement +//! its interfaces. +//! +//! - [`types`]: the package's `types` interface, shared by every component's bindings. +//! - [`codec`]: RESP serialization of a [`types::Value`], to and from bytes. +//! - [`command`]: [`IntoCommand`], converting Rust values into the words of a command. +//! - [`value`]: [`FromValue`], converting a reply into Rust values. +//! +//! A component's bindings use these types for the package's types interface, so every component +//! shares them, e.g. +//! +//! ```text +//! wit_bindgen::generate!({ +//! with: { "componentized:valkey/types@0.3.0-dev": componentized_valkey::types }, +//! ... +//! }); +//! ``` + +mod bindings { + wit_bindgen::generate!({ + // `path` is relative to the manifest, `crates/componentized-valkey/` + path: "../../wit", + inline: " + package componentized:valkey-crate; + + world types { + import componentized:valkey/types@0.3.0-dev; + } + ", + world: "componentized:valkey-crate/types", + additional_derives: [PartialEq], + generate_all, + }); +} + +/// The `componentized:valkey/types` interface. +pub use bindings::componentized::valkey::types; + +pub mod codec; +pub mod command; +pub mod value; + +pub use command::IntoCommand; +pub use value::FromValue; diff --git a/crates/resp-codec/src/value.rs b/crates/componentized-valkey/src/value.rs similarity index 98% rename from crates/resp-codec/src/value.rs rename to crates/componentized-valkey/src/value.rs index 734d4fc..a855f11 100644 --- a/crates/resp-codec/src/value.rs +++ b/crates/componentized-valkey/src/value.rs @@ -1,19 +1,19 @@ -use crate::bindings::componentized::valkey::types::{ +use crate::types::{ AclDryrunResult, AclListEntry, AclLogEntry, ChannelRule, ClientTrackinginfoReply, ClusterBumpepochResult, CommandArgument, CommandArgumentType, CommandDocsReply, CommandInfoReply, CommandRule, Cursor, DbRule, Double, DurationMilliseconds, DurationSeconds, EngineStats, Error, ExpiretimeReply, FieldPersistResult, FunctionStatsReply, GeoCoordinates, GeoSearchResult, Integer, KeyRule, KeySpec, KeySpecBeginSearch, KeySpecFindKeys, KeySpecKeynum, KeySpecKeyword, KeySpecRange, KeyType, LatencyHistogramEntry, LatencyLatestEntry, LcsIdxMatch, - LcsIdxReply, Library, LibraryFunction, LinsertResult, LogEntry, ModuleInfo, PexpiretimeReply, - PttlReply, RolePrimary, RoleReplica, RoleReplicaInfo, RoleReply, Rule, RuleSelector, - RunningFunction, SlotMigration, SlotMigrationOperation, StreamId, TrackingRedirect, TtlReply, - TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, UserRule, Value, XinfoConsumer, XinfoGroup, - XinfoStreamReply, XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, - XinfoStreamReplyFullGroup, XpendingExtendedEntry, XpendingSummary, + LcsIdxReply, Library, LibraryFunction, LinsertResult, LogEntry, ModuleInfo, NestedValue, + PexpiretimeReply, PttlReply, RolePrimary, RoleReplica, RoleReplicaInfo, RoleReply, Rule, + RuleSelector, RunningFunction, SlotMigration, SlotMigrationOperation, StreamId, + TrackingRedirect, TtlReply, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, UserRule, Value, + XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, + XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, XpendingExtendedEntry, + XpendingSummary, }; -use crate::NestedValue; use wit_bindgen::rt::Map; /// Converts a reply into a Rust type, e.g. `Option` for a bulk string or null. diff --git a/crates/resp-codec/Cargo.toml b/crates/resp-codec/Cargo.toml deleted file mode 100644 index 3d120e6..0000000 --- a/crates/resp-codec/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "resp-codec" -version = "0.1.0" -edition = "2024" -license = "Apache-2.0" -publish = false - -[dependencies] -wit-bindgen = { workspace = true } diff --git a/crates/resp-codec/LICENSE-APACHE.txt b/crates/resp-codec/LICENSE-APACHE.txt deleted file mode 100644 index 16fe87b..0000000 --- a/crates/resp-codec/LICENSE-APACHE.txt +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/crates/resp-codec/LICENSE.txt b/crates/resp-codec/LICENSE.txt deleted file mode 100644 index 7bd5ff0..0000000 --- a/crates/resp-codec/LICENSE.txt +++ /dev/null @@ -1,5 +0,0 @@ -MIT/Apache-2.0 - -Copyright (c) 2016-2022 IORust Developers - -See LICENSE-APACHE and LICENSE-MIT. diff --git a/crates/resp-codec/README.md b/crates/resp-codec/README.md deleted file mode 100644 index 8e551b3..0000000 --- a/crates/resp-codec/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# resp - -Code from https://github.com/iorust/resp/tree/v1.0.3 diff --git a/crates/resp-codec/src/lib.rs b/crates/resp-codec/src/lib.rs deleted file mode 100644 index 1e66ee7..0000000 --- a/crates/resp-codec/src/lib.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! The RESP codec, the `componentized:valkey/types` it works with, and the conversions between -//! those types and commands: `IntoCommand` for a command's words, `FromValue` for its reply. -//! -//! A component that imports an interface of the package uses these types for it, so every -//! component shares them, e.g. -//! -//! ```text -//! wit_bindgen::generate!({ -//! with: { "componentized:valkey/types@0.3.0-dev": resp_codec::bindings::componentized::valkey::types }, -//! ... -//! }); -//! ``` - -pub mod bindings { - wit_bindgen::generate!({ - // `path` is relative to the manifest, `crates/resp-codec/` - path: "../../wit", - inline: " - package componentized:resp-codec; - - world types { - import componentized:valkey/types@0.3.0-dev; - } - ", - world: "componentized:resp-codec/types", - additional_derives: [PartialEq], - generate_all, - }); -} - -mod codec; -pub mod command; -pub mod value; - -pub use bindings::componentized::valkey::types::{NestedValue, RespError as Error, Value}; -pub use codec::{decode, encode, frame_len}; -pub use command::IntoCommand; -pub use value::FromValue; diff --git a/crates/test-harness/Cargo.toml b/crates/test-harness/Cargo.toml index b25c739..0b7fa63 100644 --- a/crates/test-harness/Cargo.toml +++ b/crates/test-harness/Cargo.toml @@ -6,7 +6,7 @@ license = "Apache-2.0" publish = false [dependencies] -resp-codec = { workspace = true } +componentized-valkey = { workspace = true } tokio = { workspace = true } wasmtime = { workspace = true } wasmtime-wasi = { workspace = true } diff --git a/crates/test-harness/src/lib.rs b/crates/test-harness/src/lib.rs index 1520f47..7246f4a 100644 --- a/crates/test-harness/src/lib.rs +++ b/crates/test-harness/src/lib.rs @@ -80,17 +80,17 @@ pub type Resp = resp_bindings::exports::componentized::valkey::resp::Guest; /// Encode a value as RESP, e.g. to nest it in an aggregate. pub fn encode(value: Value) -> NestedValue { - resp_codec::encode(to_codec(value)) + componentized_valkey::codec::encode(to_codec(value)) } /// Decode a nested value. pub fn decode(value: NestedValue) -> Result { - resp_codec::decode(value).map(from_codec) + componentized_valkey::codec::decode(value).map(from_codec) } // a value is not recursive, nested values are encoded RESP, so each conversion is one match -fn to_codec(value: Value) -> resp_codec::Value { - use resp_codec::Value as V; +fn to_codec(value: Value) -> componentized_valkey::types::Value { + use componentized_valkey::types::Value as V; match value { Value::String(value) => V::String(value), Value::Error(value) => V::Error(value), @@ -109,8 +109,8 @@ fn to_codec(value: Value) -> resp_codec::Value { } } -fn from_codec(value: resp_codec::Value) -> Value { - use resp_codec::Value as V; +fn from_codec(value: componentized_valkey::types::Value) -> Value { + use componentized_valkey::types::Value as V; match value { V::String(value) => Value::String(value), V::Error(value) => Value::Error(value), diff --git a/crates/test-harness/src/server.rs b/crates/test-harness/src/server.rs index 6d2da0e..3b09ac4 100644 --- a/crates/test-harness/src/server.rs +++ b/crates/test-harness/src/server.rs @@ -42,7 +42,9 @@ impl Client { /// The next command the client sent, as its words, `None` once it closed its side. pub async fn request(&mut self) -> Option> { loop { - if let Some(len) = resp_codec::frame_len(&self.buffered).expect("valid RESP") { + if let Some(len) = + componentized_valkey::codec::frame_len(&self.buffered).expect("valid RESP") + { let rest = self.buffered.split_off(len); let frame = std::mem::replace(&mut self.buffered, rest); let Value::Array(words) = decode(frame).expect("valid RESP") else { From 76f45d6516507978dbec4e20ddc65d3080e28fbc Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 13 Oct 2026 16:52:11 -0400 Subject: [PATCH 22/37] harmonize type names Signed-off-by: Scott Andrews --- components/commands/src/lib.rs | 111 +++--- components/connection/src/lib.rs | 79 ++-- components/dep-cli/src/lib.rs | 38 +- components/tcp/src/lib.rs | 14 +- crates/componentized-valkey/src/codec.rs | 126 +++--- crates/componentized-valkey/src/command.rs | 155 ++++---- crates/componentized-valkey/src/lib.rs | 2 +- crates/componentized-valkey/src/value.rs | 442 +++++++++++---------- crates/test-harness/src/lib.rs | 108 ++--- crates/test-harness/src/server.rs | 8 +- wit/commands.wit | 145 +++---- wit/connection.wit | 14 +- wit/tcp.wit | 6 +- wit/types.wit | 209 ++++++---- 14 files changed, 767 insertions(+), 690 deletions(-) diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index d42df54..b5f3cc4 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -1,20 +1,22 @@ use crate::exports::componentized::valkey::commands::{ - AclDryrunResult, AclListEntry, AclLogEntry, Aggregate, BgsaveOperation, BitRange, BitfieldGet, + AclDryrunResult, AclListEntry, AclLogEntry, AclRule, BgsaveOperation, BitRange, BitfieldGet, BitfieldOperation, BitopOperation, ClientFilter, ClientPauseMode, ClientSetinfoAttr, - ClientTrackingOptions, ClientTrackinginfoReply, ClientUnblockType, ClusterBumpepochResult, - ClusterFailoverMode, ClusterResetMode, ClusterSetslotState, CommandDocsReply, CommandInfoReply, - CommandListFilter, CommandlogType, Connection, Cursor, Double, DurationMilliseconds, - DurationSeconds, Error, ExpiretimeReply, FailoverTarget, FieldPersistResult, FlushMode, - FunctionStatsReply, GeoCoordinates, GeoCount, GeoSearchResult, GeoShape, GeoUnit, - GeoaddCondition, GeosearchWithFlags, GetexTtl, Guest, HsetexCondition, HsetexFieldCondition, - Integer, KeyType, LatencyHistogramEntry, LatencyLatestEntry, LcsIdxReply, LexBound, Library, - Limit, LinsertPosition, LinsertResult, ListSide, LogEntry, MigrateAuth, MigrateFlags, - ModuleInfo, MsetexPrecondition, MsetexTtl, PexpiretimeReply, PopEnd, PttlReply, RoleReply, - Rule, ScoreBound, SetPrecondition, SetTtl, ShutdownFlags, ShutdownSave, SlotMigration, - SlotMigrationGroup, SortOpts, SortOrder, StreamId, StreamTrim, TrackingRedirect, TtlMode, - TtlReply, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, Value, XclaimOpts, XinfoConsumer, - XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, - ZaddComparison, ZaddCondition, ZrangeBy, ZrangeWithscoresBy, + ClientTrackingOptions, ClientTrackingRedirect, ClientTrackinginfoReply, ClientUnblockType, + ClusterBumpepochResult, ClusterFailoverMode, ClusterGetslotmigrationsEntry, + ClusterMigrateslotsGroup, ClusterResetMode, ClusterSetslotState, CommandDocsReply, + CommandInfoReply, CommandListFilter, CommandlogEntry, CommandlogType, Connection, Cursor, + Double, DurationMilliseconds, DurationSeconds, Error, ExpiretimeReply, FailoverTarget, + FlushMode, FunctionListLibrary, FunctionStatsReply, GeoCoordinates, GeoUnit, GeoaddCondition, + GeosearchCount, GeosearchShape, GeosearchWithFlags, GeosearchWithResult, GetexTtl, Guest, + HexpireResult, HpersistResult, HsetexCondition, HsetexFieldCondition, Integer, KeyType, + LatencyHistogramEntry, LatencyLatestEntry, LcsIdxReply, LexBound, Limit, LinsertPosition, + LinsertResult, ListSide, MigrateAuth, MigrateFlags, ModuleListEntry, MsetexPrecondition, + MsetexTtl, PexpiretimeReply, PttlReply, RespValue, RoleReply, ScoreBound, SetPrecondition, + SetTtl, ShutdownFlags, ShutdownSave, SortOpts, SortOrder, StreamId, StreamTrim, TtlMode, + TtlReply, UnixTimeMilliseconds, UnixTimeSeconds, XclaimOpts, XinfoConsumersEntry, + XinfoGroupsEntry, XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, + XpendingSummary, ZaddComparison, ZaddCondition, ZmpopEnd, ZrangeBy, ZrangeWithscoresBy, + ZsetAggregate, }; use componentized_valkey::command::{ Bit, DecimalSeconds, Fields, Flag, KeysAndArgs, LexRange, NamedOption, NonEmpty, NumKeys, @@ -91,7 +93,7 @@ impl Guest for Commands { async fn acl_setuser( connection: &Connection, username: String, - rules: Option>, + rules: Option>, ) -> Result<(), Error> { exchange( connection, @@ -142,7 +144,7 @@ impl Guest for Commands { async fn acl_getuser( connection: &Connection, username: String, - ) -> Result>, Error> { + ) -> Result>, Error> { let rules: Option = exchange(connection, ("ACL", "GETUSER", username)).await?; Ok(rules.map(|rules| rules.0)) @@ -447,7 +449,7 @@ impl Guest for Commands { #[allow(async_fn_in_trait)] async fn cluster_getslotmigrations( connection: &Connection, - ) -> Result, Error> { + ) -> Result, Error> { exchange(connection, ("CLUSTER", "GETSLOTMIGRATIONS")).await } @@ -472,7 +474,7 @@ impl Guest for Commands { #[allow(async_fn_in_trait)] async fn cluster_migrateslots( connection: &Connection, - migrations: Vec, + migrations: Vec, ) -> Result<(), Error> { exchange( connection, @@ -581,7 +583,7 @@ impl Guest for Commands { connection: &Connection, count: Integer, type_: CommandlogType, - ) -> Result, Error> { + ) -> Result, Error> { exchange(connection, ("COMMANDLOG", "GET", count, type_)).await } @@ -763,7 +765,7 @@ impl Guest for Commands { #[doc = " MODULE LIST"] #[doc = " https://valkey.io/commands/module-list/"] #[allow(async_fn_in_trait)] - async fn module_list(connection: &Connection) -> Result, Error> { + async fn module_list(connection: &Connection) -> Result, Error> { exchange(connection, ("MODULE", "LIST")).await } @@ -897,7 +899,7 @@ impl Guest for Commands { async fn slowlog_get( connection: &Connection, count: Option, - ) -> Result, Error> { + ) -> Result, Error> { exchange(connection, ("SLOWLOG", "GET", count)).await } @@ -1066,7 +1068,7 @@ impl Guest for Commands { connection: &Connection, auth: Option<(String, String)>, client_name: Option, - ) -> Result, Error> { + ) -> Result, Error> { exchange( connection, ( @@ -1153,7 +1155,7 @@ impl Guest for Commands { #[doc = " CLIENT GETREDIR"] #[doc = " https://valkey.io/commands/client-getredir/"] #[allow(async_fn_in_trait)] - async fn client_getredir(connection: &Connection) -> Result { + async fn client_getredir(connection: &Connection) -> Result { exchange(connection, ("CLIENT", "GETREDIR")).await } @@ -1511,9 +1513,9 @@ impl Guest for Commands { async fn geosearch( connection: &Connection, key: String, - shape: GeoShape, + shape: GeosearchShape, order: Option, - count: Option, + count: Option, ) -> Result, Error> { exchange(connection, ("GEOSEARCH", key, shape, order, count)).await } @@ -1528,11 +1530,11 @@ impl Guest for Commands { async fn geosearch_with( connection: &Connection, key: String, - shape: GeoShape, + shape: GeosearchShape, order: Option, - count: Option, + count: Option, details: GeosearchWithFlags, - ) -> Result, Error> { + ) -> Result, Error> { let command = ( "GEOSEARCH", key, @@ -1557,9 +1559,9 @@ impl Guest for Commands { connection: &Connection, destination: String, source: String, - shape: GeoShape, + shape: GeosearchShape, order: Option, - count: Option, + count: Option, storedist: bool, ) -> Result { exchange( @@ -1663,7 +1665,7 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result, Error> { + ) -> Result, Error> { exchange( connection, ("HPERSIST", key, Fields(NonEmpty::new("fields", fields))), @@ -1682,7 +1684,7 @@ impl Guest for Commands { milliseconds: DurationMilliseconds, mode: Option, fields: Vec, - ) -> Result, Error> { + ) -> Result, Error> { let command = ( "HPEXPIRE", key, @@ -1704,7 +1706,7 @@ impl Guest for Commands { milliseconds: UnixTimeMilliseconds, mode: Option, fields: Vec, - ) -> Result, Error> { + ) -> Result, Error> { let command = ( "HPEXPIREAT", key, @@ -1808,7 +1810,7 @@ impl Guest for Commands { seconds: DurationSeconds, mode: Option, fields: Vec, - ) -> Result, Error> { + ) -> Result, Error> { exchange( connection, ( @@ -1833,7 +1835,7 @@ impl Guest for Commands { seconds: UnixTimeSeconds, mode: Option, fields: Vec, - ) -> Result, Error> { + ) -> Result, Error> { exchange( connection, ( @@ -3146,7 +3148,7 @@ impl Guest for Commands { script: String, keys: Option>, args: Option>, - ) -> Result { + ) -> Result { exchange(connection, ("EVAL", script, KeysAndArgs(keys, args))).await } @@ -3160,7 +3162,7 @@ impl Guest for Commands { script: String, keys: Option>, args: Option>, - ) -> Result { + ) -> Result { exchange(connection, ("EVAL_RO", script, KeysAndArgs(keys, args))).await } @@ -3175,7 +3177,7 @@ impl Guest for Commands { sha1: String, keys: Option>, args: Option>, - ) -> Result { + ) -> Result { exchange(connection, ("EVALSHA", sha1, KeysAndArgs(keys, args))).await } @@ -3190,7 +3192,7 @@ impl Guest for Commands { sha1: String, keys: Option>, args: Option>, - ) -> Result { + ) -> Result { exchange(connection, ("EVALSHA_RO", sha1, KeysAndArgs(keys, args))).await } @@ -3207,7 +3209,7 @@ impl Guest for Commands { function: String, keys: Option>, args: Option>, - ) -> Result { + ) -> Result { exchange(connection, ("FCALL", function, KeysAndArgs(keys, args))).await } @@ -3221,7 +3223,7 @@ impl Guest for Commands { function: String, keys: Option>, args: Option>, - ) -> Result { + ) -> Result { exchange(connection, ("FCALL_RO", function, KeysAndArgs(keys, args))).await } @@ -3261,7 +3263,7 @@ impl Guest for Commands { connection: &Connection, library_name_pattern: Option, withcode: bool, - ) -> Result, Error> { + ) -> Result, Error> { exchange( connection, ( @@ -4103,7 +4105,7 @@ impl Guest for Commands { connection: &Connection, key: String, group: String, - ) -> Result, Error> { + ) -> Result, Error> { exchange(connection, ("XINFO", "CONSUMERS", key, group)).await } @@ -4112,7 +4114,10 @@ impl Guest for Commands { #[doc = " XINFO GROUPS key"] #[doc = " https://valkey.io/commands/xinfo-groups/"] #[allow(async_fn_in_trait)] - async fn xinfo_groups(connection: &Connection, key: String) -> Result, Error> { + async fn xinfo_groups( + connection: &Connection, + key: String, + ) -> Result, Error> { exchange(connection, ("XINFO", "GROUPS", key)).await } @@ -4677,7 +4682,7 @@ impl Guest for Commands { connection: &Connection, timeout: DurationMilliseconds, keys: Vec, - end: PopEnd, + end: ZmpopEnd, count: Option, ) -> Result)>, Error> { let command = ( @@ -4906,7 +4911,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, weights: Option>, - aggregate: Option, + aggregate: Option, ) -> Result, Error> { exchange( connection, @@ -4931,7 +4936,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, weights: Option>, - aggregate: Option, + aggregate: Option, ) -> Result, Error> { let command = ( "ZINTER", @@ -4974,7 +4979,7 @@ impl Guest for Commands { destination: String, keys: Vec, weights: Option>, - aggregate: Option, + aggregate: Option, ) -> Result { let command = ( "ZINTERSTORE", @@ -5022,7 +5027,7 @@ impl Guest for Commands { async fn zmpop( connection: &Connection, keys: Vec, - end: PopEnd, + end: ZmpopEnd, count: Option, ) -> Result)>, Error> { exchange( @@ -5427,7 +5432,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, weights: Option>, - aggregate: Option, + aggregate: Option, ) -> Result, Error> { exchange( connection, @@ -5452,7 +5457,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, weights: Option>, - aggregate: Option, + aggregate: Option, ) -> Result, Error> { let command = ( "ZUNION", @@ -5474,7 +5479,7 @@ impl Guest for Commands { destination: String, keys: Vec, weights: Option>, - aggregate: Option, + aggregate: Option, ) -> Result { let command = ( "ZUNIONSTORE", diff --git a/components/connection/src/lib.rs b/components/connection/src/lib.rs index e5b398c..6efa1df 100644 --- a/components/connection/src/lib.rs +++ b/components/connection/src/lib.rs @@ -21,7 +21,7 @@ use std::future::poll_fn; use std::pin::Pin; use std::rc::{Rc, Weak}; -use componentized_valkey::types::{NestedValue, Value}; +use componentized_valkey::types::{RespNestedValue, RespValue}; use exports::componentized::valkey::connection::{ ConnectOpts, Connection, DurationMilliseconds, Error, Guest, GuestConnection, }; @@ -42,21 +42,21 @@ impl Guest for Component { /// Where the reply to a command goes. enum Reply { /// To the caller waiting for it. - Call(oneshot::Sender>), + Call(oneshot::Sender>), /// Nowhere, the command was sent without waiting for its reply. Discard, } /// A command for the writer, with a signal for once it is written. struct Request { - command: Value, + command: RespValue, written: Option>, } /// Work for the supervisor to start. enum Task { /// Forward push messages from the channel to a reader's stream. - Pushes(mpsc::Receiver, StreamWriter), + Pushes(mpsc::Receiver, StreamWriter), /// Keep the connection alive, until the keep-alive is set again. KeepAlive { generation: u64, @@ -75,9 +75,9 @@ struct State { /// idle. activity: u64, /// The readers of push messages. - pushes: Vec>, + pushes: Vec>, /// The server's properties from the reply to HELLO. - server_properties: Vec<(String, Value)>, + server_properties: Vec<(String, RespValue)>, /// Bumped each time the keep-alive is set, a keep-alive task stops once it is outdated. keep_alive_generation: u64, } @@ -96,10 +96,13 @@ pub struct ConnectionImpl(Rc); impl GuestConnection for ConnectionImpl { async fn new( - replies: StreamReader, + replies: StreamReader, closed: FutureReader>, opts: Option, - ) -> (StreamReader, FutureReader>) { + ) -> ( + StreamReader, + FutureReader>, + ) { let (output, requests) = wit_stream::new(); // the default is only written if the connection is dropped before it is established let (connection_writer, connection) = wit_future::new(|| Err(Error::Closed(None))); @@ -121,15 +124,15 @@ impl GuestConnection for ConnectionImpl { (requests, connection) } - fn server_properties(&self) -> Vec<(String, Value)> { + fn server_properties(&self) -> Vec<(String, RespValue)> { self.0.state.borrow().server_properties.clone() } - async fn exchange(&self, command: Vec) -> Result { + async fn exchange(&self, command: Vec) -> Result { exchange(&self.0, command).await } - async fn exchange_all(&self, commands: Vec>) -> Vec> { + async fn exchange_all(&self, commands: Vec>) -> Vec> { exchange_all(&self.0, commands).await } @@ -149,11 +152,14 @@ impl GuestConnection for ConnectionImpl { ok(exchange(&self.0, words(&["UNWATCH"])).await?) } - async fn transaction(&self, commands: Vec>) -> Result>, Error> { + async fn transaction( + &self, + commands: Vec>, + ) -> Result>, Error> { transaction(&self.0, commands).await } - fn pushes(&self) -> StreamReader { + fn pushes(&self) -> StreamReader { let (writer, reader) = wit_stream::new(); let (tx, rx) = mpsc::channel(PUSH_BUFFER); let mut state = self.0.state.borrow_mut(); @@ -204,10 +210,10 @@ fn enqueue( return Err(error.clone()); } // the words of a command are bulk strings - let command = Value::Array( + let command = RespValue::Array( command .into_iter() - .map(|word| componentized_valkey::codec::encode(Value::BulkString(word))) + .map(|word| componentized_valkey::codec::encode(RespValue::BulkString(word))) .collect(), ); inner @@ -218,9 +224,9 @@ fn enqueue( Ok(()) } -async fn exchange(inner: &Inner, command: Vec) -> Result { +async fn exchange(inner: &Inner, command: Vec) -> Result { if replies_with_pushes(&command) { - return emit(inner, command).await.map(|()| Value::Null); + return emit(inner, command).await.map(|()| RespValue::Null); } let (reply_tx, reply_rx) = oneshot::channel(); enqueue(inner, command, Some(Reply::Call(reply_tx)), None)?; @@ -231,7 +237,7 @@ async fn exchange(inner: &Inner, command: Vec) -> Result { } /// Queue every command before waiting for any reply, so no other command is sent between them. -async fn exchange_all(inner: &Inner, commands: Vec>) -> Vec> { +async fn exchange_all(inner: &Inner, commands: Vec>) -> Vec> { // no await until every command is queued let queued: Vec>, Error>> = commands .into_iter() @@ -248,7 +254,7 @@ async fn exchange_all(inner: &Inner, commands: Vec>) -> Vec Err(error), - Ok(None) => Ok(Value::Null), + Ok(None) => Ok(RespValue::Null), // the reply is dropped unsent when the connection closes, the reader fails it first Ok(Some(reply)) => reply .await @@ -262,7 +268,7 @@ async fn exchange_all(inner: &Inner, commands: Vec>) -> Vec>, -) -> Result>, Error> { +) -> Result>, Error> { // before MULTI is sent, so an invalid command fails the transaction rather than being left out if commands.iter().any(Vec::is_empty) { return Err(Error::Client( @@ -282,8 +288,8 @@ async fn transaction( } match exec? { // a watched key changed - Value::Null => Ok(None), - Value::Array(replies) => replies + RespValue::Null => Ok(None), + RespValue::Array(replies) => replies .into_iter() .map(|reply| componentized_valkey::codec::decode(reply).map_err(Error::Resp)) .collect::>() @@ -352,7 +358,7 @@ fn words(words: &[&str]) -> Vec { async fn write_requests( state: Rc>, mut requests: mpsc::UnboundedReceiver, - mut output: StreamWriter, + mut output: StreamWriter, ) { while let Some(Request { command, written }) = next(&mut requests).await { state.borrow_mut().activity += 1; @@ -370,14 +376,14 @@ async fn write_requests( /// sent unprompted, to the readers of push messages, until the transport closes. async fn read_replies( state: Rc>, - mut replies: StreamReader, + mut replies: StreamReader, closed: FutureReader>, ) { while let Some(value) = replies.next().await { let mut state = state.borrow_mut(); state.activity += 1; let value = match value { - Value::Push(_) => value, + RespValue::Push(_) => value, value => match state.pending.pop_front() { Some(Reply::Call(reply)) => { let _ = reply.send(reply_result(value)); @@ -414,7 +420,10 @@ async fn supervise(mut tasks: mpsc::UnboundedReceiver, inner: Weak) } /// Forward push messages to a reader's stream, until the reader or the connection is gone. -async fn forward_pushes(mut pushes: mpsc::Receiver, mut writer: StreamWriter) { +async fn forward_pushes( + mut pushes: mpsc::Receiver, + mut writer: StreamWriter, +) { while let Some(value) = next(&mut pushes).await { if writer.write_one(value).await.is_some() { break; @@ -491,8 +500,8 @@ async fn establish( } /// The server's properties from the reply to HELLO, a map of property names to values. -fn server_properties(reply: Value) -> Result, Error> { - let Value::Map(entries) = reply else { +fn server_properties(reply: RespValue) -> Result, Error> { + let RespValue::Map(entries) = reply else { return Err(Error::Client(format!( "unexpected reply to HELLO: {reply:?}" ))); @@ -501,7 +510,7 @@ fn server_properties(reply: Value) -> Result, Error> { .into_iter() .map(|(name, value)| { let name = match decode(name)? { - Value::String(name) | Value::BulkString(name) => name, + RespValue::String(name) | RespValue::BulkString(name) => name, name => { return Err(Error::Client(format!( "unexpected property name in reply to HELLO: {name:?}" @@ -513,26 +522,26 @@ fn server_properties(reply: Value) -> Result, Error> { .collect() } -fn decode(value: NestedValue) -> Result { +fn decode(value: RespNestedValue) -> Result { componentized_valkey::codec::decode(value).map_err(Error::Resp) } /// An error value from the server fails the command. /// A simple string `OK`. -fn ok(reply: Value) -> Result<(), Error> { +fn ok(reply: RespValue) -> Result<(), Error> { match reply { - Value::String(text) if text == "OK" => Ok(()), + RespValue::String(text) if text == "OK" => Ok(()), reply => Err(unexpected(&reply)), } } -fn unexpected(reply: &Value) -> Error { +fn unexpected(reply: &RespValue) -> Error { Error::Client(format!("unexpected reply: {reply:?}")) } -fn reply_result(value: Value) -> Result { +fn reply_result(value: RespValue) -> Result { match value { - Value::Error(message) | Value::BulkError(message) => Err(Error::Valkey(message)), + RespValue::Error(message) | RespValue::BulkError(message) => Err(Error::Valkey(message)), value => Ok(value), } } diff --git a/components/dep-cli/src/lib.rs b/components/dep-cli/src/lib.rs index f038c70..9986824 100644 --- a/components/dep-cli/src/lib.rs +++ b/components/dep-cli/src/lib.rs @@ -12,7 +12,7 @@ use std::task::Poll; use componentized::valkey::connection::{Connection, Error}; use componentized::valkey::connector; -use componentized_valkey::types::{NestedValue, Value}; +use componentized_valkey::types::{RespNestedValue, RespValue}; use exports::wasi::cli::run::Guest; use futures_channel::mpsc; use futures_core::Stream; @@ -93,14 +93,14 @@ async fn exec(command: Vec, out: &mut Output) -> Result<(), Error> { /// Prints push messages, up to `limit` of them, failing once they stop. async fn print_pushes( - pushes: &mut StreamReader, + pushes: &mut StreamReader, out: &mut Output, limit: Option, ) -> Result<(), Error> { for _ in 0..limit.unwrap_or(usize::MAX) { match pushes.next().await { // sent unprompted, e.g. for a SUBSCRIBE missing its channels - Some(Value::Error(message) | Value::BulkError(message)) => { + Some(RespValue::Error(message) | RespValue::BulkError(message)) => { return Err(Error::Valkey(message)); } Some(push) => out.line(&render(push)).await, @@ -183,7 +183,7 @@ async fn send_lines( Ok(()) } -async fn forward_pushes(mut pushes: StreamReader, lines: mpsc::UnboundedSender) { +async fn forward_pushes(mut pushes: StreamReader, lines: mpsc::UnboundedSender) { while let Some(push) = pushes.next().await { let _ = lines.unbounded_send(render(push)); } @@ -343,24 +343,26 @@ fn render_error(error: Error) -> String { } /// Renders a value for people to read, in the style of valkey-cli. -fn render(value: Value) -> String { +fn render(value: RespValue) -> String { match value { - Value::String(text) => text, - Value::Error(message) | Value::BulkError(message) => format!("(error) {message}"), - Value::Integer(value) => format!("(integer) {value}"), + RespValue::String(text) => text, + RespValue::Error(message) | RespValue::BulkError(message) => format!("(error) {message}"), + RespValue::Integer(value) => format!("(integer) {value}"), // quoted, with special characters escaped - Value::BulkString(text) => format!("{text:?}"), - Value::Null => "(nil)".to_string(), - Value::Boolean(value) => format!("({value})"), - Value::Double(value) => format!("(double) {value}"), - Value::BigNumber(value) => format!("(big number) {value}"), + RespValue::BulkString(text) => format!("{text:?}"), + RespValue::Null => "(nil)".to_string(), + RespValue::Boolean(value) => format!("({value})"), + RespValue::Double(value) => format!("(double) {value}"), + RespValue::BigNumber(value) => format!("(big number) {value}"), // text to show as is, e.g. from INFO, the format is `txt` or `mkd` - Value::VerbatimString((_format, text)) => text, - Value::Array(items) | Value::Push(items) => { + RespValue::VerbatimString((_format, text)) => text, + RespValue::Array(items) | RespValue::Push(items) => { numbered(items.into_iter().map(render_nested).collect(), ')', "array") } - Value::Set(items) => numbered(items.into_iter().map(render_nested).collect(), '~', "set"), - Value::Map(entries) => numbered( + RespValue::Set(items) => { + numbered(items.into_iter().map(render_nested).collect(), '~', "set") + } + RespValue::Map(entries) => numbered( entries .into_iter() .map(|(key, value)| { @@ -376,7 +378,7 @@ fn render(value: Value) -> String { } } -fn render_nested(value: NestedValue) -> String { +fn render_nested(value: RespNestedValue) -> String { match componentized_valkey::codec::decode(value) { Ok(value) => render(value), Err(error) => format!("(invalid) {error}"), diff --git a/components/tcp/src/lib.rs b/components/tcp/src/lib.rs index 1f1818b..5162cb0 100644 --- a/components/tcp/src/lib.rs +++ b/components/tcp/src/lib.rs @@ -21,7 +21,7 @@ use std::pin::Pin; use std::rc::Rc; use componentized::valkey::connection::{ConnectOpts, Connection, Error}; -use componentized_valkey::types::Value; +use componentized_valkey::types::RespValue; use exports::componentized::valkey::tcp::{Guest, GuestTransport, Transport}; use futures_channel::{mpsc, oneshot}; use futures_core::Stream; @@ -60,11 +60,11 @@ impl Guest for Component { pub struct TransportImpl { requests: RefCell< Option<( - oneshot::Sender>, + oneshot::Sender>, FutureReader>, )>, >, - replies: RefCell, FutureReader>)>>, + replies: RefCell, FutureReader>)>>, /// Futures for the resolver to fail, a sync method can't write them itself. failures: mpsc::UnboundedSender<(FutureWriter>, &'static str)>, } @@ -84,7 +84,7 @@ impl GuestTransport for TransportImpl { Ok(Transport::new(open(&host, port).await?)) } - fn send(&self, requests: StreamReader) -> FutureReader> { + fn send(&self, requests: StreamReader) -> FutureReader> { match self.requests.borrow_mut().take() { Some((pump, sent)) => { // the pump only stops waiting for the requests once the socket is closed @@ -95,7 +95,7 @@ impl GuestTransport for TransportImpl { } } - fn receive(&self) -> (StreamReader, FutureReader>) { + fn receive(&self) -> (StreamReader, FutureReader>) { self.replies.borrow_mut().take().unwrap_or_else(|| { let (_, replies) = wit_stream::new(); (replies, self.failed("receive may be called at most once")) @@ -156,7 +156,7 @@ async fn receive_pump( _socket: Rc, mut bytes: StreamReader, received: FutureReader>, - mut replies: StreamWriter, + mut replies: StreamWriter, closed: FutureWriter>, ) { // bytes read from the socket that are not part of a whole value yet @@ -199,7 +199,7 @@ async fn receive_pump( /// half-closes the socket. async fn send_pump( _socket: Rc, - requests: oneshot::Receiver>, + requests: oneshot::Receiver>, mut bytes: StreamWriter, socket_sent: FutureReader>, sent: FutureWriter>, diff --git a/crates/componentized-valkey/src/codec.rs b/crates/componentized-valkey/src/codec.rs index 67da305..156bb5f 100644 --- a/crates/componentized-valkey/src/codec.rs +++ b/crates/componentized-valkey/src/codec.rs @@ -1,6 +1,6 @@ //! RESP serialization, from . -use crate::types::{NestedValue, RespError as Error, Value}; +use crate::types::{RespError as Error, RespNestedValue, RespValue}; /// up to 512 MB in length const RESP_MAX_SIZE: i64 = 512 * 1024 * 1024; @@ -10,46 +10,46 @@ const CRLF_BYTES: &'static [u8] = b"\r\n"; /// # Examples /// ``` /// # use componentized_valkey::codec::encode; -/// # use componentized_valkey::types::Value; -/// let val = Value::String("OK".to_string()); +/// # use componentized_valkey::types::RespValue; +/// let val = RespValue::String("OK".to_string()); /// assert_eq!(encode(val), b"+OK\r\n"); /// ``` -pub fn encode(value: Value) -> Vec { +pub fn encode(value: RespValue) -> Vec { let mut res: Vec = Vec::new(); buf_encode(value, &mut res); res } #[inline] -fn buf_encode(value: Value, buf: &mut Vec) { +fn buf_encode(value: RespValue, buf: &mut Vec) { match value { - Value::Null => { + RespValue::Null => { buf.push(b'_'); buf.extend_from_slice(CRLF_BYTES); } - Value::String(val) => { + RespValue::String(val) => { buf.push(b'+'); buf.extend_from_slice(val.as_bytes()); buf.extend_from_slice(CRLF_BYTES); } - Value::Error(val) => { + RespValue::Error(val) => { buf.push(b'-'); buf.extend_from_slice(val.as_bytes()); buf.extend_from_slice(CRLF_BYTES); } - Value::Integer(val) => { + RespValue::Integer(val) => { buf.push(b':'); buf.extend_from_slice(val.to_string().as_bytes()); buf.extend_from_slice(CRLF_BYTES); } - Value::BulkString(val) => { + RespValue::BulkString(val) => { buf.push(b'$'); buf.extend_from_slice(val.len().to_string().as_bytes()); buf.extend_from_slice(CRLF_BYTES); buf.extend_from_slice(val.as_bytes()); buf.extend_from_slice(CRLF_BYTES); } - Value::Array(val) => { + RespValue::Array(val) => { buf.push(b'*'); buf.extend_from_slice(val.len().to_string().as_bytes()); buf.extend_from_slice(CRLF_BYTES); @@ -57,7 +57,7 @@ fn buf_encode(value: Value, buf: &mut Vec) { buf.extend_from_slice(&item); } } - Value::Boolean(ref val) => { + RespValue::Boolean(ref val) => { buf.push(b'#'); match val { true => buf.push(b't'), @@ -65,24 +65,24 @@ fn buf_encode(value: Value, buf: &mut Vec) { } buf.extend_from_slice(CRLF_BYTES); } - Value::Double(ref val) => { + RespValue::Double(ref val) => { buf.push(b','); buf.extend_from_slice(val.to_string().to_lowercase().as_bytes()); buf.extend_from_slice(CRLF_BYTES); } - Value::BigNumber(ref val) => { + RespValue::BigNumber(ref val) => { buf.push(b'('); buf.extend_from_slice(val.as_bytes()); buf.extend_from_slice(CRLF_BYTES); } - Value::BulkError(ref val) => { + RespValue::BulkError(ref val) => { buf.push(b'!'); buf.extend_from_slice(val.len().to_string().as_bytes()); buf.extend_from_slice(CRLF_BYTES); buf.extend_from_slice(val.as_bytes()); buf.extend_from_slice(CRLF_BYTES); } - Value::VerbatimString((ref encoding, ref value)) => { + RespValue::VerbatimString((ref encoding, ref value)) => { let val = format!("{encoding}:{value}"); buf.push(b'='); buf.extend_from_slice(val.len().to_string().as_bytes()); @@ -90,7 +90,7 @@ fn buf_encode(value: Value, buf: &mut Vec) { buf.extend_from_slice(val.as_bytes()); buf.extend_from_slice(CRLF_BYTES); } - Value::Map(val) => { + RespValue::Map(val) => { buf.push(b'%'); buf.extend_from_slice(val.len().to_string().as_bytes()); buf.extend_from_slice(CRLF_BYTES); @@ -99,7 +99,7 @@ fn buf_encode(value: Value, buf: &mut Vec) { buf.extend_from_slice(&value); } } - Value::Set(val) => { + RespValue::Set(val) => { buf.push(b'~'); buf.extend_from_slice(val.len().to_string().as_bytes()); buf.extend_from_slice(CRLF_BYTES); @@ -107,7 +107,7 @@ fn buf_encode(value: Value, buf: &mut Vec) { buf.extend_from_slice(&item); } } - Value::Push(val) => { + RespValue::Push(val) => { buf.push(b'>'); buf.extend_from_slice(val.len().to_string().as_bytes()); buf.extend_from_slice(CRLF_BYTES); @@ -126,11 +126,11 @@ fn buf_encode(value: Value, buf: &mut Vec) { /// # Examples /// ``` /// # use componentized_valkey::codec::{decode, encode}; -/// # use componentized_valkey::types::Value; -/// let value = Value::BulkString("Hello".to_string()); +/// # use componentized_valkey::types::RespValue; +/// let value = RespValue::BulkString("Hello".to_string()); /// assert_eq!(decode(encode(value.clone())), Ok(value)); /// ``` -pub fn decode(value: Vec) -> Result { +pub fn decode(value: Vec) -> Result { match frame_end(&value, 0)? { Some(end) if end == value.len() => decode_frame(&value), Some(end) => Err(format!( @@ -210,7 +210,7 @@ fn frame_end(buf: &[u8], start: usize) -> Result, Error> { } /// Decodes the whole RESP value at the start of `buf`, its framing already checked by `frame_end`. -fn decode_frame(buf: &[u8]) -> Result { +fn decode_frame(buf: &[u8]) -> Result { let crlf = buf .windows(2) .position(|w| w == CRLF_BYTES) @@ -224,7 +224,7 @@ fn decode_frame(buf: &[u8]) -> Result { Ok(&buf[body..body + len]) }; // the elements of an aggregate value, each kept as its RESP - let elements = |count: i64| -> Result, Error> { + let elements = |count: i64| -> Result, Error> { let mut start = body; let mut elements = Vec::with_capacity(count as usize); for _ in 0..count { @@ -236,47 +236,47 @@ fn decode_frame(buf: &[u8]) -> Result { }; match buf[0] { - b'+' => parse_string(header).map(Value::String), - b'-' => parse_string(header).map(Value::Error), - b':' => parse_integer(header).map(Value::Integer), - b'_' => Ok(Value::Null), + b'+' => parse_string(header).map(RespValue::String), + b'-' => parse_string(header).map(RespValue::Error), + b':' => parse_integer(header).map(RespValue::Integer), + b'_' => Ok(RespValue::Null), b'#' => match header { - b"t" => Ok(Value::Boolean(true)), - b"f" => Ok(Value::Boolean(false)), + b"t" => Ok(RespValue::Boolean(true)), + b"f" => Ok(RespValue::Boolean(false)), _ => Err(format!("invalid RESP boolean: {header:?}")), }, - b',' => parse_double(header).map(Value::Double), - b'(' => parse_string(header).map(Value::BigNumber), + b',' => parse_double(header).map(RespValue::Double), + b'(' => parse_string(header).map(RespValue::BigNumber), b'$' => match parse_integer(header)? { // a null bulk string, RESP2's null - -1 => Ok(Value::Null), - _ => parse_string(blob()?).map(Value::BulkString), + -1 => Ok(RespValue::Null), + _ => parse_string(blob()?).map(RespValue::BulkString), }, - b'!' => parse_string(blob()?).map(Value::BulkError), + b'!' => parse_string(blob()?).map(RespValue::BulkError), b'=' => { let value = parse_string(blob()?)?; let Some((encoding, value)) = value.split_once(':') else { return Err("invalid verbatim string, missing encoding".to_string()); }; - Ok(Value::VerbatimString(( + Ok(RespValue::VerbatimString(( encoding.to_string(), value.to_string(), ))) } b'*' => match parse_integer(header)? { // a null array, RESP2's null - -1 => Ok(Value::Null), - count => elements(count).map(Value::Array), + -1 => Ok(RespValue::Null), + count => elements(count).map(RespValue::Array), }, - b'~' => elements(parse_integer(header)?).map(Value::Set), - b'>' => elements(parse_integer(header)?).map(Value::Push), + b'~' => elements(parse_integer(header)?).map(RespValue::Set), + b'>' => elements(parse_integer(header)?).map(RespValue::Push), b'%' => { let mut entries = elements(parse_integer(header)? * 2)?.into_iter(); let mut map = vec![]; while let (Some(key), Some(value)) = (entries.next(), entries.next()) { map.push((key, value)); } - Ok(Value::Map(map)) + Ok(RespValue::Map(map)) } prefix => Err(format!("invalid RESP type: {prefix:?}")), } @@ -357,22 +357,22 @@ mod tests { #[test] fn decode_round_trips() { let values = vec![ - Value::String("OK".to_string()), - Value::Error("ERR oops".to_string()), - Value::Integer(-42), - Value::BulkString("a\r\nb".to_string()), - Value::Null, - Value::Boolean(false), - Value::Double(1.5), - Value::BigNumber("12345678901234567890".to_string()), - Value::BulkError("SYNTAX invalid".to_string()), - Value::VerbatimString(("txt".to_string(), "a:b".to_string())), - Value::Array(vec![encode(Value::Integer(1)), encode(Value::Null)]), - Value::Set(vec![encode(Value::String("a".to_string()))]), - Value::Push(vec![encode(Value::String("message".to_string()))]), - Value::Map(vec![( - encode(Value::String("key".to_string())), - encode(Value::Array(vec![encode(Value::Integer(1))])), + RespValue::String("OK".to_string()), + RespValue::Error("ERR oops".to_string()), + RespValue::Integer(-42), + RespValue::BulkString("a\r\nb".to_string()), + RespValue::Null, + RespValue::Boolean(false), + RespValue::Double(1.5), + RespValue::BigNumber("12345678901234567890".to_string()), + RespValue::BulkError("SYNTAX invalid".to_string()), + RespValue::VerbatimString(("txt".to_string(), "a:b".to_string())), + RespValue::Array(vec![encode(RespValue::Integer(1)), encode(RespValue::Null)]), + RespValue::Set(vec![encode(RespValue::String("a".to_string()))]), + RespValue::Push(vec![encode(RespValue::String("message".to_string()))]), + RespValue::Map(vec![( + encode(RespValue::String("key".to_string())), + encode(RespValue::Array(vec![encode(RespValue::Integer(1))])), )]), ]; for value in values { @@ -386,7 +386,7 @@ mod tests { let decoded = decode(b"*2\r\n*1\r\n:1\r\n$1\r\na\r\n".to_vec()); assert_eq!( decoded, - Ok(Value::Array(vec![ + Ok(RespValue::Array(vec![ b"*1\r\n:1\r\n".to_vec(), b"$1\r\na\r\n".to_vec() ])) @@ -395,8 +395,8 @@ mod tests { #[test] fn decode_resp2_nulls() { - assert_eq!(decode(b"$-1\r\n".to_vec()), Ok(Value::Null)); - assert_eq!(decode(b"*-1\r\n".to_vec()), Ok(Value::Null)); + assert_eq!(decode(b"$-1\r\n".to_vec()), Ok(RespValue::Null)); + assert_eq!(decode(b"*-1\r\n".to_vec()), Ok(RespValue::Null)); } #[test] @@ -416,9 +416,9 @@ mod tests { #[test] fn frame_len_matches_encode() { - let value = Value::Array(vec![ - encode(Value::BulkString("a\r\nb".to_string())), - encode(Value::Integer(-3)), + let value = RespValue::Array(vec![ + encode(RespValue::BulkString("a\r\nb".to_string())), + encode(RespValue::Integer(-3)), ]); let encoded = encode(value); assert_eq!(frame_len(&encoded).unwrap(), Some(encoded.len())); diff --git a/crates/componentized-valkey/src/command.rs b/crates/componentized-valkey/src/command.rs index b126e24..2023f22 100644 --- a/crates/componentized-valkey/src/command.rs +++ b/crates/componentized-valkey/src/command.rs @@ -1,15 +1,16 @@ use crate::types::{ - Aggregate, BgsaveOperation, BitRange, BitfieldEncoding, BitfieldGet, BitfieldIncrby, - BitfieldOffset, BitfieldOperation, BitfieldOverflow, BitfieldSet, ChannelRule, ClientFilter, - ClientPauseMode, ClientSetinfoAttr, ClientTrackingMode, ClientTrackingOptions, ClientType, - ClientUnblockType, ClusterFailoverMode, ClusterResetMode, ClusterSetslotState, - CommandListFilter, CommandRule, CommandlogType, Cursor, DbRule, DurationMilliseconds, - DurationSeconds, Error, FailoverTarget, FlushMode, GeoCoordinates, GeoCount, GeoFrom, GeoShape, - GeoUnit, GeoaddCondition, GetexTtl, HsetexCondition, HsetexFieldCondition, KeyRule, KeyType, - LexBound, Limit, LinsertPosition, ListSide, MigrateAuth, MsetexPrecondition, PopEnd, Rule, - RuleSelector, ScoreBound, SetPrecondition, SetTtl, ShutdownSave, SlotMigrationGroup, SortOpts, - SortOrder, StreamTrim, StreamTrimEfficiency, TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, - UserRule, XclaimOpts, ZaddComparison, ZaddCondition, ZrangeBy, + AclChannelRule, AclCommandRule, AclDbRule, AclKeyRule, AclRule, AclSelectorRule, AclUserRule, + BgsaveOperation, BitRange, BitfieldEncoding, BitfieldGet, BitfieldIncrby, BitfieldOffset, + BitfieldOperation, BitfieldOverflow, BitfieldSet, ClientFilter, ClientPauseMode, + ClientSetinfoAttr, ClientTrackingMode, ClientTrackingOptions, ClientType, ClientUnblockType, + ClusterFailoverMode, ClusterMigrateslotsGroup, ClusterResetMode, ClusterSetslotState, + CommandListFilter, CommandlogType, Cursor, DurationMilliseconds, DurationSeconds, Error, + FailoverTarget, FlushMode, GeoCoordinates, GeoUnit, GeoaddCondition, GeosearchCount, + GeosearchFrom, GeosearchShape, GetexTtl, HsetexCondition, HsetexFieldCondition, KeyType, + LexBound, Limit, LinsertPosition, ListSide, MigrateAuth, MsetexPrecondition, ScoreBound, + SetPrecondition, SetTtl, ShutdownSave, SortOpts, SortOrder, StreamTrim, StreamTrimEfficiency, + TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, XclaimOpts, ZaddComparison, ZaddCondition, + ZmpopEnd, ZrangeBy, ZsetAggregate, }; use crate::value::{OrZeroId, ZERO_ID}; @@ -177,103 +178,103 @@ impl IntoCommand for UnixTimeMilliseconds { } } -impl IntoCommand for Rule { +impl IntoCommand for AclRule { fn into_command(self) -> Result, Error> { match self { - Rule::Keys(rule) => rule.into_command(), - Rule::Channel(rule) => rule.into_command(), - Rule::Command(rule) => rule.into_command(), - Rule::Db(rule) => rule.into_command(), - Rule::User(rule) => rule.into_command(), - Rule::Other(rule) => rule.into_command(), + AclRule::Keys(rule) => rule.into_command(), + AclRule::Channel(rule) => rule.into_command(), + AclRule::Command(rule) => rule.into_command(), + AclRule::Db(rule) => rule.into_command(), + AclRule::User(rule) => rule.into_command(), + AclRule::Other(rule) => rule.into_command(), } } } -impl IntoCommand for RuleSelector { +impl IntoCommand for AclSelectorRule { fn into_command(self) -> Result, Error> { match self { - RuleSelector::Keys(rule) => rule.into_command(), - RuleSelector::Channel(rule) => rule.into_command(), - RuleSelector::Command(rule) => rule.into_command(), - RuleSelector::Db(rule) => rule.into_command(), - RuleSelector::Other(rule) => rule.into_command(), + AclSelectorRule::Keys(rule) => rule.into_command(), + AclSelectorRule::Channel(rule) => rule.into_command(), + AclSelectorRule::Command(rule) => rule.into_command(), + AclSelectorRule::Db(rule) => rule.into_command(), + AclSelectorRule::Other(rule) => rule.into_command(), } } } -impl IntoCommand for KeyRule { +impl IntoCommand for AclKeyRule { fn into_command(self) -> Result, Error> { Ok(vec![match self { - KeyRule::Add(pattern) => format!("~{pattern}"), - KeyRule::AddRead(pattern) => format!("%R~{pattern}"), - KeyRule::AddWrite(pattern) => format!("%W~{pattern}"), - KeyRule::AddReadWrite(pattern) => format!("%RW~{pattern}"), - KeyRule::All => "allkeys".to_string(), - KeyRule::Reset => "resetkeys".to_string(), + AclKeyRule::Add(pattern) => format!("~{pattern}"), + AclKeyRule::AddRead(pattern) => format!("%R~{pattern}"), + AclKeyRule::AddWrite(pattern) => format!("%W~{pattern}"), + AclKeyRule::AddReadWrite(pattern) => format!("%RW~{pattern}"), + AclKeyRule::All => "allkeys".to_string(), + AclKeyRule::Reset => "resetkeys".to_string(), }]) } } -impl IntoCommand for ChannelRule { +impl IntoCommand for AclChannelRule { fn into_command(self) -> Result, Error> { Ok(vec![match self { - ChannelRule::Add(pattern) => format!("&{pattern}"), - ChannelRule::All => "allchannels".to_string(), - ChannelRule::Reset => "resetchannels".to_string(), + AclChannelRule::Add(pattern) => format!("&{pattern}"), + AclChannelRule::All => "allchannels".to_string(), + AclChannelRule::Reset => "resetchannels".to_string(), }]) } } -impl IntoCommand for CommandRule { +impl IntoCommand for AclCommandRule { fn into_command(self) -> Result, Error> { Ok(vec![match self { - CommandRule::Add(command) => format!("+{command}"), - CommandRule::AddCategory(category) => format!("+@{category}"), - CommandRule::AddFirstArg((command, arg)) => format!("+{command}|{arg}"), - CommandRule::All => "allcommands".to_string(), - CommandRule::Remove(command) => format!("-{command}"), - CommandRule::RemoveCategory(category) => format!("-@{category}"), - CommandRule::Reset => "nocommands".to_string(), + AclCommandRule::Add(command) => format!("+{command}"), + AclCommandRule::AddCategory(category) => format!("+@{category}"), + AclCommandRule::AddFirstArg((command, arg)) => format!("+{command}|{arg}"), + AclCommandRule::All => "allcommands".to_string(), + AclCommandRule::Remove(command) => format!("-{command}"), + AclCommandRule::RemoveCategory(category) => format!("-@{category}"), + AclCommandRule::Reset => "nocommands".to_string(), }]) } } /// `db=` requires at least one database. -impl IntoCommand for DbRule { +impl IntoCommand for AclDbRule { fn into_command(self) -> Result, Error> { Ok(vec![match self { - DbRule::Db(dbs) if dbs.is_empty() => { + AclDbRule::Db(dbs) if dbs.is_empty() => { return Err(Error::Client("db requires at least one item".to_string())); } - DbRule::Db(dbs) => format!( + AclDbRule::Db(dbs) => format!( "db={}", dbs.iter() .map(|db| db.to_string()) .collect::>() .join(",") ), - DbRule::All => "alldbs".to_string(), - DbRule::Reset => "resetdbs".to_string(), + AclDbRule::All => "alldbs".to_string(), + AclDbRule::Reset => "resetdbs".to_string(), }]) } } -impl IntoCommand for UserRule { +impl IntoCommand for AclUserRule { fn into_command(self) -> Result, Error> { match self { - UserRule::On => "on".into_command(), - UserRule::Off => "off".into_command(), - UserRule::Nopass => "nopass".into_command(), - UserRule::SanitizePayload => "sanitize-payload".into_command(), - UserRule::SkipSanitizePayload => "skip-sanitize-payload".into_command(), - UserRule::Password(password) => format!(">{password}").into_command(), - UserRule::HashedPassword(hash) => format!("#{hash}").into_command(), - UserRule::RemovePassword(password) => format!("<{password}").into_command(), - UserRule::RemoveHashedPassword(hash) => format!("!{hash}").into_command(), - UserRule::Selector(rules) => ("(", rules, ")").into_command(), - UserRule::Clearselectors => "clearselectors".into_command(), - UserRule::Reset => "reset".into_command(), + AclUserRule::On => "on".into_command(), + AclUserRule::Off => "off".into_command(), + AclUserRule::Nopass => "nopass".into_command(), + AclUserRule::SanitizePayload => "sanitize-payload".into_command(), + AclUserRule::SkipSanitizePayload => "skip-sanitize-payload".into_command(), + AclUserRule::Password(password) => format!(">{password}").into_command(), + AclUserRule::HashedPassword(hash) => format!("#{hash}").into_command(), + AclUserRule::RemovePassword(password) => format!("<{password}").into_command(), + AclUserRule::RemoveHashedPassword(hash) => format!("!{hash}").into_command(), + AclUserRule::Selector(rules) => ("(", rules, ")").into_command(), + AclUserRule::Clearselectors => "clearselectors".into_command(), + AclUserRule::Reset => "reset".into_command(), } } } @@ -514,22 +515,22 @@ impl IntoCommand for Zrange { } } -impl IntoCommand for Aggregate { +impl IntoCommand for ZsetAggregate { fn into_command(self) -> Result, Error> { let aggregate = match self { - Aggregate::Sum => "SUM", - Aggregate::Min => "MIN", - Aggregate::Max => "MAX", + ZsetAggregate::Sum => "SUM", + ZsetAggregate::Min => "MIN", + ZsetAggregate::Max => "MAX", }; ("AGGREGATE", aggregate).into_command() } } -impl IntoCommand for PopEnd { +impl IntoCommand for ZmpopEnd { fn into_command(self) -> Result, Error> { match self { - PopEnd::Min => "MIN", - PopEnd::Max => "MAX", + ZmpopEnd::Min => "MIN", + ZmpopEnd::Max => "MAX", } .into_command() } @@ -749,23 +750,23 @@ impl IntoCommand for GeoaddCondition { } } -impl IntoCommand for GeoFrom { +impl IntoCommand for GeosearchFrom { fn into_command(self) -> Result, Error> { match self { - GeoFrom::Member(member) => ("FROMMEMBER", member).into_command(), - GeoFrom::Lonlat(coordinates) => ("FROMLONLAT", coordinates).into_command(), + GeosearchFrom::Member(member) => ("FROMMEMBER", member).into_command(), + GeosearchFrom::Lonlat(coordinates) => ("FROMLONLAT", coordinates).into_command(), } } } /// The center, when the shape has one, then the shape. -impl IntoCommand for GeoShape { +impl IntoCommand for GeosearchShape { fn into_command(self) -> Result, Error> { match self { - GeoShape::Radius(circle) => { + GeosearchShape::Radius(circle) => { (circle.from, "BYRADIUS", circle.radius, circle.unit).into_command() } - GeoShape::Box(rectangle) => ( + GeosearchShape::Box(rectangle) => ( rectangle.from, "BYBOX", rectangle.width, @@ -773,7 +774,7 @@ impl IntoCommand for GeoShape { rectangle.unit, ) .into_command(), - GeoShape::Polygon(vertices) => { + GeosearchShape::Polygon(vertices) => { ("BYPOLYGON", NumKeys(NonEmpty::new("vertices", vertices))).into_command() } } @@ -781,7 +782,7 @@ impl IntoCommand for GeoShape { } /// `COUNT count [ ANY ]` -impl IntoCommand for GeoCount { +impl IntoCommand for GeosearchCount { fn into_command(self) -> Result, Error> { ("COUNT", self.count, Flag("ANY", self.any)).into_command() } @@ -979,7 +980,7 @@ impl IntoCommand for FailoverTarget { } /// `SLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] NODE node-id [ AUTH username password ]` -impl IntoCommand for SlotMigrationGroup { +impl IntoCommand for ClusterMigrateslotsGroup { fn into_command(self) -> Result, Error> { ( "SLOTSRANGE", diff --git a/crates/componentized-valkey/src/lib.rs b/crates/componentized-valkey/src/lib.rs index 5b0744c..4453d02 100644 --- a/crates/componentized-valkey/src/lib.rs +++ b/crates/componentized-valkey/src/lib.rs @@ -2,7 +2,7 @@ //! its interfaces. //! //! - [`types`]: the package's `types` interface, shared by every component's bindings. -//! - [`codec`]: RESP serialization of a [`types::Value`], to and from bytes. +//! - [`codec`]: RESP serialization of a [`types::RespValue`], to and from bytes. //! - [`command`]: [`IntoCommand`], converting Rust values into the words of a command. //! - [`value`]: [`FromValue`], converting a reply into Rust values. //! diff --git a/crates/componentized-valkey/src/value.rs b/crates/componentized-valkey/src/value.rs index a855f11..eb622d5 100644 --- a/crates/componentized-valkey/src/value.rs +++ b/crates/componentized-valkey/src/value.rs @@ -1,47 +1,49 @@ use crate::types::{ - AclDryrunResult, AclListEntry, AclLogEntry, ChannelRule, ClientTrackinginfoReply, - ClusterBumpepochResult, CommandArgument, CommandArgumentType, CommandDocsReply, - CommandInfoReply, CommandRule, Cursor, DbRule, Double, DurationMilliseconds, DurationSeconds, - EngineStats, Error, ExpiretimeReply, FieldPersistResult, FunctionStatsReply, GeoCoordinates, - GeoSearchResult, Integer, KeyRule, KeySpec, KeySpecBeginSearch, KeySpecFindKeys, KeySpecKeynum, - KeySpecKeyword, KeySpecRange, KeyType, LatencyHistogramEntry, LatencyLatestEntry, LcsIdxMatch, - LcsIdxReply, Library, LibraryFunction, LinsertResult, LogEntry, ModuleInfo, NestedValue, - PexpiretimeReply, PttlReply, RolePrimary, RoleReplica, RoleReplicaInfo, RoleReply, Rule, - RuleSelector, RunningFunction, SlotMigration, SlotMigrationOperation, StreamId, - TrackingRedirect, TtlReply, TtlResult, UnixTimeMilliseconds, UnixTimeSeconds, UserRule, Value, - XinfoConsumer, XinfoGroup, XinfoStreamReply, XinfoStreamReplyFull, - XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, XpendingExtendedEntry, - XpendingSummary, + AclChannelRule, AclCommandRule, AclDbRule, AclDryrunResult, AclKeyRule, AclListEntry, + AclLogEntry, AclRule, AclSelectorRule, AclUserRule, ClientTrackingRedirect, + ClientTrackinginfoReply, ClusterBumpepochResult, ClusterGetslotmigrationsEntry, + ClusterGetslotmigrationsOperation, CommandDocsArgument, CommandDocsArgumentType, + CommandDocsReply, CommandInfoReply, CommandKeySpec, CommandKeySpecBeginSearch, + CommandKeySpecFindKeys, CommandKeySpecKeynum, CommandKeySpecKeyword, CommandKeySpecRange, + CommandlogEntry, Cursor, Double, DurationMilliseconds, DurationSeconds, Error, ExpiretimeReply, + FunctionListFunction, FunctionListLibrary, FunctionStatsEngine, FunctionStatsReply, + FunctionStatsRunning, GeoCoordinates, GeosearchWithResult, HexpireResult, HpersistResult, + Integer, KeyType, LatencyHistogramEntry, LatencyLatestEntry, LcsIdxMatch, LcsIdxReply, + LinsertResult, ModuleListEntry, PexpiretimeReply, PttlReply, RespNestedValue, RespValue, + RolePrimary, RolePrimaryReplica, RoleReplica, RoleReply, StreamId, TtlReply, + UnixTimeMilliseconds, UnixTimeSeconds, XinfoConsumersEntry, XinfoGroupsEntry, XinfoStreamReply, + XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, + XpendingExtendedEntry, XpendingSummary, }; use wit_bindgen::rt::Map; /// Converts a reply into a Rust type, e.g. `Option` for a bulk string or null. pub trait FromValue: Sized { - fn from_value(value: Value) -> Result; + fn from_value(value: RespValue) -> Result; } /// The reply itself, e.g. for a field of mixed types. -impl FromValue for Value { - fn from_value(value: Value) -> Result { +impl FromValue for RespValue { + fn from_value(value: RespValue) -> Result { Ok(value) } } /// A simple string `OK`. impl FromValue for () { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { match &value { - Value::String(reply) if reply == "OK" => Ok(()), + RespValue::String(reply) if reply == "OK" => Ok(()), _ => Err(unexpected(&value)), } } } impl FromValue for Integer { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { match value { - Value::Integer(value) => Ok(value), + RespValue::Integer(value) => Ok(value), value => Err(unexpected(&value)), } } @@ -49,10 +51,10 @@ impl FromValue for Integer { /// A double, or a bulk string holding one, e.g. the reply of INCRBYFLOAT, even with RESP3. impl FromValue for Double { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { match value { - Value::Double(value) => Ok(value), - Value::BulkString(text) => text + RespValue::Double(value) => Ok(value), + RespValue::BulkString(text) => text .parse() .map_err(|_| Error::Client(format!("unexpected reply, not a double: {text:?}"))), value => Err(unexpected(&value)), @@ -62,7 +64,7 @@ impl FromValue for Double { /// A non-negative integer. impl FromValue for u64 { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let value = Integer::from_value(value)?; u64::try_from(value) .map_err(|_| Error::Client(format!("unexpected negative reply: {value}"))) @@ -71,11 +73,11 @@ impl FromValue for u64 { /// An integer `0` or `1`, or a boolean. impl FromValue for bool { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { match value { - Value::Integer(0) => Ok(false), - Value::Integer(1) => Ok(true), - Value::Boolean(value) => Ok(value), + RespValue::Integer(0) => Ok(false), + RespValue::Integer(1) => Ok(true), + RespValue::Boolean(value) => Ok(value), value => Err(unexpected(&value)), } } @@ -83,10 +85,10 @@ impl FromValue for bool { /// A bulk, simple or verbatim string. impl FromValue for String { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { match value { - Value::BulkString(value) | Value::String(value) => Ok(value), - Value::VerbatimString((_, value)) => Ok(value), + RespValue::BulkString(value) | RespValue::String(value) => Ok(value), + RespValue::VerbatimString((_, value)) => Ok(value), value => Err(unexpected(&value)), } } @@ -94,9 +96,9 @@ impl FromValue for String { /// Null, or a value. impl FromValue for Option { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { match value { - Value::Null => Ok(None), + RespValue::Null => Ok(None), value => T::from_value(value).map(Some), } } @@ -104,7 +106,7 @@ impl FromValue for Option { /// An array, or a set. impl FromValue for Vec { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { items(value)?.into_iter().map(T::from_value).collect() } } @@ -115,8 +117,8 @@ macro_rules! from_value_tuples { $( impl<$($item: FromValue),+> FromValue for ($($item,)+) { #[allow(non_snake_case)] - fn from_value(value: Value) -> Result { - match <[Value; $len]>::try_from(items(value)?) { + fn from_value(value: RespValue) -> Result { + match <[RespValue; $len]>::try_from(items(value)?) { Ok([$($item),+]) => Ok(($($item::from_value($item)?,)+)), Err(items) => Err(Error::Client(format!( "unexpected number of items, expected {}: {}", @@ -134,7 +136,7 @@ from_value_tuples!((2; A, B), (3; A, B, C), (4; A, B, C, D)); /// A map, or a flat array of keys and values. impl FromValue for Map { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { Ok(entries(value)?.into_iter().collect()) } } @@ -142,10 +144,10 @@ impl FromValue for Map { /// The fields of a map, by name, e.g. to build a record. /// /// Fields that are not taken are ignored, so newer servers may add fields. -pub struct Fields(Vec<(String, Value)>); +pub struct Fields(Vec<(String, RespValue)>); impl FromValue for Fields { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { entries(value).map(Fields) } } @@ -168,28 +170,28 @@ impl Fields { /// Whole seconds, never negative. impl FromValue for DurationSeconds { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { u64::from_value(value).map(|ex| DurationSeconds { ex }) } } /// Whole milliseconds, never negative. impl FromValue for DurationMilliseconds { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { u64::from_value(value).map(|px| DurationMilliseconds { px }) } } /// Whole seconds since the Unix epoch. impl FromValue for UnixTimeSeconds { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { Integer::from_value(value).map(|exat| UnixTimeSeconds { exat }) } } /// Whole milliseconds since the Unix epoch. impl FromValue for UnixTimeMilliseconds { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { Integer::from_value(value).map(|pxat| UnixTimeMilliseconds { pxat }) } } @@ -198,9 +200,9 @@ impl FromValue for UnixTimeMilliseconds { pub struct OrNever(pub Option); impl FromValue for OrNever { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { match value { - Value::Integer(-1) => Ok(OrNever(None)), + RespValue::Integer(-1) => Ok(OrNever(None)), value => T::from_value(value).map(|value| OrNever(Some(value))), } } @@ -213,7 +215,7 @@ pub struct OrZeroId(pub Option); pub const ZERO_ID: &str = "0-0"; impl FromValue for OrZeroId { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let id = StreamId::from_value(value)?; Ok(OrZeroId((id != ZERO_ID).then_some(id))) } @@ -222,7 +224,7 @@ impl FromValue for OrZeroId { /// `[count, lowest-id, highest-id, [[consumer, count]...]]`, the IDs and consumers are null when /// nothing is pending, and each consumer's count is a bulk string. impl FromValue for XpendingSummary { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let (count, lowest_id, highest_id, consumers): (_, _, _, Option>) = FromValue::from_value(value)?; let consumers = consumers @@ -248,7 +250,7 @@ impl FromValue for XpendingSummary { /// `[id, consumer, milliseconds-since-delivery, delivery-count]`. impl FromValue for XpendingExtendedEntry { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let (id, name, since_delivery, delivery_count) = FromValue::from_value(value)?; Ok(Self { id, @@ -264,16 +266,16 @@ impl FromValue for XpendingExtendedEntry { pub struct ScoredMembers(pub Vec<(String, Double)>); impl FromValue for ScoredMembers { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let items = items(value)?; match items.first() { None => Ok(ScoredMembers(vec![])), - Some(Value::Array(_)) => items + Some(RespValue::Array(_)) => items .into_iter() .map(FromValue::from_value) .collect::>() .map(ScoredMembers), - Some(_) => match <[Value; 2]>::try_from(items) { + Some(_) => match <[RespValue; 2]>::try_from(items) { Ok([member, score]) => Ok(ScoredMembers(vec![( String::from_value(member)?, Double::from_value(score)?, @@ -291,7 +293,7 @@ impl FromValue for ScoredMembers { pub struct FlatPairs(pub Vec<(K, V)>); impl FromValue for FlatPairs { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { entries(value).map(FlatPairs) } } @@ -302,10 +304,10 @@ macro_rules! from_value_field_times { ($($variant:ident($time:ident) => $case:ident),*) => { $( impl FromValue for $variant { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { match value { - Value::Integer(-2) => Ok($variant::DoesNotExist), - Value::Integer(-1) => Ok($variant::NoExpiration), + RespValue::Integer(-2) => Ok($variant::DoesNotExist), + RespValue::Integer(-1) => Ok($variant::NoExpiration), value => $time::from_value(value).map($variant::$case), } } @@ -321,8 +323,8 @@ from_value_field_times!( PexpiretimeReply(UnixTimeMilliseconds) => ExpiresAt ); -impl FromValue for FieldPersistResult { - fn from_value(value: Value) -> Result { +impl FromValue for HpersistResult { + fn from_value(value: RespValue) -> Result { match Integer::from_value(value)? { -2 => Ok(Self::DoesNotExist), -1 => Ok(Self::NoExpiration), @@ -336,7 +338,7 @@ impl FromValue for FieldPersistResult { pub struct NextCursor(pub Option); impl FromValue for NextCursor { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let cursor = String::from_value(value)?; Ok(NextCursor( (cursor != "0").then_some(Cursor::Resume(cursor)), @@ -347,7 +349,7 @@ impl FromValue for NextCursor { /// The length of the list after the insert, `0` when the key does not exist, `-1` when the pivot /// was not found. impl FromValue for LinsertResult { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { match Integer::from_value(value)? { -1 => Ok(Self::PivotNotFound), 0 => Ok(Self::NoSuchKey), @@ -359,7 +361,7 @@ impl FromValue for LinsertResult { /// `[longitude, latitude]` impl FromValue for GeoCoordinates { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let (longitude, latitude) = FromValue::from_value(value)?; Ok(GeoCoordinates { longitude, @@ -370,27 +372,27 @@ impl FromValue for GeoCoordinates { /// The member, or with any of WITHDIST, WITHHASH and WITHCOORD, an array of the member then the /// details, in that order, told apart by their types. -impl FromValue for GeoSearchResult { - fn from_value(value: Value) -> Result { - let mut result = GeoSearchResult { +impl FromValue for GeosearchWithResult { + fn from_value(value: RespValue) -> Result { + let mut result = GeosearchWithResult { member: String::new(), distance: None, hash: None, coordinates: None, }; let items = match value { - Value::Array(_) => items(value)?, + RespValue::Array(_) => items(value)?, // without details, the member alone value => vec![value], }; let mut items = items.into_iter(); - result.member = String::from_value(items.next().unwrap_or(Value::Null))?; + result.member = String::from_value(items.next().unwrap_or(RespValue::Null))?; for item in items { match item { // a bulk string, as RESP3 has no other way to send a fixed precision double - Value::BulkString(_) => result.distance = Some(Double::from_value(item)?), - Value::Integer(hash) => result.hash = Some(hash), - Value::Array(_) => result.coordinates = Some(GeoCoordinates::from_value(item)?), + RespValue::BulkString(_) => result.distance = Some(Double::from_value(item)?), + RespValue::Integer(hash) => result.hash = Some(hash), + RespValue::Array(_) => result.coordinates = Some(GeoCoordinates::from_value(item)?), item => return Err(unexpected(&item)), } } @@ -398,8 +400,8 @@ impl FromValue for GeoSearchResult { } } -impl FromValue for LibraryFunction { - fn from_value(value: Value) -> Result { +impl FromValue for FunctionListFunction { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { name: fields.required("name")?, @@ -409,8 +411,8 @@ impl FromValue for LibraryFunction { } } -impl FromValue for Library { - fn from_value(value: Value) -> Result { +impl FromValue for FunctionListLibrary { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { library_name: fields.required("library_name")?, @@ -422,8 +424,8 @@ impl FromValue for Library { } } -impl FromValue for RunningFunction { - fn from_value(value: Value) -> Result { +impl FromValue for FunctionStatsRunning { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { name: fields.required("name")?, @@ -433,8 +435,8 @@ impl FromValue for RunningFunction { } } -impl FromValue for EngineStats { - fn from_value(value: Value) -> Result { +impl FromValue for FunctionStatsEngine { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { libraries_count: fields.required("libraries_count")?, @@ -444,7 +446,7 @@ impl FromValue for EngineStats { } impl FromValue for FunctionStatsReply { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { running_script: fields.required("running_script")?, @@ -457,7 +459,7 @@ impl FromValue for FunctionStatsReply { pub struct OptionalKeyType(pub Option); impl FromValue for OptionalKeyType { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { // `none` rather than null, though the reply schema has null let name: Option = FromValue::from_value(value)?; Ok(OptionalKeyType(match name.as_deref() { @@ -477,10 +479,10 @@ impl FromValue for OptionalKeyType { pub struct Migrated(pub bool); impl FromValue for Migrated { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { match &value { - Value::String(reply) if reply == "OK" => Ok(Migrated(true)), - Value::String(reply) if reply == "NOKEY" => Ok(Migrated(false)), + RespValue::String(reply) if reply == "OK" => Ok(Migrated(true)), + RespValue::String(reply) if reply == "NOKEY" => Ok(Migrated(false)), _ => Err(unexpected(&value)), } } @@ -488,9 +490,9 @@ impl FromValue for Migrated { /// `OK` when the user may run the command, otherwise why not. impl FromValue for AclDryrunResult { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { match value { - Value::String(reply) if reply == "OK" => Ok(AclDryrunResult::Allowed), + RespValue::String(reply) if reply == "OK" => Ok(AclDryrunResult::Allowed), value => String::from_value(value).map(AclDryrunResult::Denied), } } @@ -498,24 +500,24 @@ impl FromValue for AclDryrunResult { /// The rules of ACL GETUSER: the flags, the hashed passwords, the root selector's rules, then the /// selectors. -pub struct AclUserRules(pub Vec); +pub struct AclUserRules(pub Vec); impl FromValue for AclUserRules { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; let flags: Vec = fields.required("flags")?; let passwords: Vec = fields.required("passwords")?; - let mut rules: Vec = flags.iter().map(|flag| rule(flag)).collect(); + let mut rules: Vec = flags.iter().map(|flag| rule(flag)).collect(); rules.extend( passwords .into_iter() - .map(|hash| Rule::User(UserRule::HashedPassword(hash))), + .map(|hash| AclRule::User(AclUserRule::HashedPassword(hash))), ); rules.extend(selector_rules(&mut fields)?.into_iter().map(root_rule)); let selectors: Vec = fields.required("selectors")?; for mut selector in selectors { let selector = selector_rules(&mut selector)?; - rules.push(Rule::User(UserRule::Selector(selector))); + rules.push(AclRule::User(AclUserRule::Selector(selector))); } Ok(AclUserRules(rules)) } @@ -526,14 +528,14 @@ impl FromValue for AclUserRules { /// /// The channels are preceded by `resetchannels`, as ACL LIST writes them, so the server's /// `acl-pubsub-default` does not add to them. -fn selector_rules(fields: &mut Fields) -> Result, Error> { +fn selector_rules(fields: &mut Fields) -> Result, Error> { let keys: String = fields.required("keys")?; let channels: String = fields.required("channels")?; // since Valkey 9.1 let databases: Option = fields.optional("databases")?; let commands: String = fields.required("commands")?; - let mut rules: Vec = keys.split_whitespace().map(key_rule).collect(); - rules.push(RuleSelector::Channel(ChannelRule::Reset)); + let mut rules: Vec = keys.split_whitespace().map(key_rule).collect(); + rules.push(AclSelectorRule::Channel(AclChannelRule::Reset)); rules.extend(channels.split_whitespace().map(channel_rule)); rules.extend( databases @@ -544,83 +546,83 @@ fn selector_rules(fields: &mut Fields) -> Result, Error> { Ok(rules) } -fn key_rule(rule: &str) -> RuleSelector { +fn key_rule(rule: &str) -> AclSelectorRule { let key_rule = match rule { - "allkeys" => KeyRule::All, - "resetkeys" => KeyRule::Reset, + "allkeys" => AclKeyRule::All, + "resetkeys" => AclKeyRule::Reset, _ => { if let Some(pattern) = rule.strip_prefix("%RW~") { - KeyRule::AddReadWrite(pattern.to_string()) + AclKeyRule::AddReadWrite(pattern.to_string()) } else if let Some(pattern) = rule.strip_prefix("%R~") { - KeyRule::AddRead(pattern.to_string()) + AclKeyRule::AddRead(pattern.to_string()) } else if let Some(pattern) = rule.strip_prefix("%W~") { - KeyRule::AddWrite(pattern.to_string()) + AclKeyRule::AddWrite(pattern.to_string()) } else if let Some(pattern) = rule.strip_prefix('~') { - KeyRule::Add(pattern.to_string()) + AclKeyRule::Add(pattern.to_string()) } else { - return RuleSelector::Other(rule.to_string()); + return AclSelectorRule::Other(rule.to_string()); } } }; - RuleSelector::Keys(key_rule) + AclSelectorRule::Keys(key_rule) } -fn channel_rule(rule: &str) -> RuleSelector { +fn channel_rule(rule: &str) -> AclSelectorRule { let channel_rule = match rule { - "allchannels" => ChannelRule::All, - "resetchannels" => ChannelRule::Reset, + "allchannels" => AclChannelRule::All, + "resetchannels" => AclChannelRule::Reset, _ => match rule.strip_prefix('&') { - Some(pattern) => ChannelRule::Add(pattern.to_string()), - None => return RuleSelector::Other(rule.to_string()), + Some(pattern) => AclChannelRule::Add(pattern.to_string()), + None => return AclSelectorRule::Other(rule.to_string()), }, }; - RuleSelector::Channel(channel_rule) + AclSelectorRule::Channel(channel_rule) } -fn db_rule(rule: &str) -> RuleSelector { +fn db_rule(rule: &str) -> AclSelectorRule { let db_rule = match rule { - "alldbs" => DbRule::All, - "resetdbs" => DbRule::Reset, + "alldbs" => AclDbRule::All, + "resetdbs" => AclDbRule::Reset, _ => { let dbs = rule .strip_prefix("db=") .map(|dbs| dbs.split(',').map(str::parse).collect::>()); match dbs { - Some(Ok(dbs)) => DbRule::Db(dbs), - _ => return RuleSelector::Other(rule.to_string()), + Some(Ok(dbs)) => AclDbRule::Db(dbs), + _ => return AclSelectorRule::Other(rule.to_string()), } } }; - RuleSelector::Db(db_rule) + AclSelectorRule::Db(db_rule) } /// `+config|get` is a subcommand, or a first argument, either way it is sent back as is. -fn command_rule(rule: &str) -> RuleSelector { +fn command_rule(rule: &str) -> AclSelectorRule { let command_rule = match rule { - "allcommands" | "+@all" => CommandRule::All, - "nocommands" | "-@all" => CommandRule::Reset, + "allcommands" | "+@all" => AclCommandRule::All, + "nocommands" | "-@all" => AclCommandRule::Reset, _ => { if let Some(category) = rule.strip_prefix("+@") { - CommandRule::AddCategory(category.to_string()) + AclCommandRule::AddCategory(category.to_string()) } else if let Some(category) = rule.strip_prefix("-@") { - CommandRule::RemoveCategory(category.to_string()) + AclCommandRule::RemoveCategory(category.to_string()) } else if let Some(command) = rule.strip_prefix('+') { - CommandRule::Add(command.to_string()) + AclCommandRule::Add(command.to_string()) } else if let Some(command) = rule.strip_prefix('-') { - CommandRule::Remove(command.to_string()) + AclCommandRule::Remove(command.to_string()) } else { - return RuleSelector::Other(rule.to_string()); + return AclSelectorRule::Other(rule.to_string()); } } }; - RuleSelector::Command(command_rule) + AclSelectorRule::Command(command_rule) } /// The lines of ACL LIST, each `user ...`, with selectors as `( ...)`. pub struct AclList(pub Vec); impl FromValue for AclList { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let lines: Vec = FromValue::from_value(value)?; lines .iter() @@ -654,40 +656,40 @@ fn acl_list_entry(line: &str) -> Result { .and_then(|selector| selector.strip_suffix(')')) .ok_or_else(invalid)?; let selector = selector.split_whitespace().map(selector_rule).collect(); - rules.push(Rule::User(UserRule::Selector(selector))); + rules.push(AclRule::User(AclUserRule::Selector(selector))); } Ok(AclListEntry { username, rules }) } /// A rule of the user or its root selector, e.g. `on`, `#` or `~app:*`. -fn rule(word: &str) -> Rule { +fn rule(word: &str) -> AclRule { let user_rule = match word { - "on" => UserRule::On, - "off" => UserRule::Off, - "nopass" => UserRule::Nopass, - "sanitize-payload" => UserRule::SanitizePayload, - "skip-sanitize-payload" => UserRule::SkipSanitizePayload, - "clearselectors" => UserRule::Clearselectors, - "reset" => UserRule::Reset, + "on" => AclUserRule::On, + "off" => AclUserRule::Off, + "nopass" => AclUserRule::Nopass, + "sanitize-payload" => AclUserRule::SanitizePayload, + "skip-sanitize-payload" => AclUserRule::SkipSanitizePayload, + "clearselectors" => AclUserRule::Clearselectors, + "reset" => AclUserRule::Reset, _ => { if let Some(hash) = word.strip_prefix('#') { - UserRule::HashedPassword(hash.to_string()) + AclUserRule::HashedPassword(hash.to_string()) } else if let Some(hash) = word.strip_prefix('!') { - UserRule::RemoveHashedPassword(hash.to_string()) + AclUserRule::RemoveHashedPassword(hash.to_string()) } else if let Some(password) = word.strip_prefix('>') { - UserRule::Password(password.to_string()) + AclUserRule::Password(password.to_string()) } else if let Some(password) = word.strip_prefix('<') { - UserRule::RemovePassword(password.to_string()) + AclUserRule::RemovePassword(password.to_string()) } else { return root_rule(selector_rule(word)); } } }; - Rule::User(user_rule) + AclRule::User(user_rule) } /// A rule of a selector, e.g. `~app:*`, `&news:*`, `db=0` or `+get`. -fn selector_rule(word: &str) -> RuleSelector { +fn selector_rule(word: &str) -> AclSelectorRule { match word { "allkeys" | "resetkeys" => key_rule(word), "allchannels" | "resetchannels" => channel_rule(word), @@ -697,23 +699,23 @@ fn selector_rule(word: &str) -> RuleSelector { _ if word.starts_with('&') => channel_rule(word), _ if word.starts_with("db=") => db_rule(word), _ if word.starts_with('+') || word.starts_with('-') => command_rule(word), - _ => RuleSelector::Other(word.to_string()), + _ => AclSelectorRule::Other(word.to_string()), } } /// A rule of the root selector. -fn root_rule(rule: RuleSelector) -> Rule { +fn root_rule(rule: AclSelectorRule) -> AclRule { match rule { - RuleSelector::Keys(rule) => Rule::Keys(rule), - RuleSelector::Channel(rule) => Rule::Channel(rule), - RuleSelector::Command(rule) => Rule::Command(rule), - RuleSelector::Db(rule) => Rule::Db(rule), - RuleSelector::Other(rule) => Rule::Other(rule), + AclSelectorRule::Keys(rule) => AclRule::Keys(rule), + AclSelectorRule::Channel(rule) => AclRule::Channel(rule), + AclSelectorRule::Command(rule) => AclRule::Command(rule), + AclSelectorRule::Db(rule) => AclRule::Db(rule), + AclSelectorRule::Other(rule) => AclRule::Other(rule), } } impl FromValue for AclLogEntry { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { count: fields.required("count")?, @@ -733,7 +735,7 @@ impl FromValue for AclLogEntry { /// `BUMPED epoch` or `STILL epoch`. impl FromValue for ClusterBumpepochResult { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let reply = String::from_value(value)?; let invalid = || Error::Client(format!("unexpected reply: {reply:?}")); let (outcome, epoch) = reply.split_once(' ').ok_or_else(invalid)?; @@ -746,11 +748,11 @@ impl FromValue for ClusterBumpepochResult { } } -impl FromValue for SlotMigrationOperation { - fn from_value(value: Value) -> Result { +impl FromValue for ClusterGetslotmigrationsOperation { + fn from_value(value: RespValue) -> Result { match String::from_value(value)?.as_str() { - "IMPORT" => Ok(SlotMigrationOperation::Import), - "EXPORT" => Ok(SlotMigrationOperation::Export), + "IMPORT" => Ok(ClusterGetslotmigrationsOperation::Import), + "EXPORT" => Ok(ClusterGetslotmigrationsOperation::Export), operation => Err(Error::Client(format!( "unexpected operation: {operation:?}" ))), @@ -762,7 +764,7 @@ impl FromValue for SlotMigrationOperation { pub struct SlotRanges(pub Vec<(u16, u16)>); impl FromValue for SlotRanges { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let ranges = String::from_value(value)?; let invalid = || Error::Client(format!("unexpected slot ranges: {ranges:?}")); ranges @@ -779,8 +781,8 @@ impl FromValue for SlotRanges { } } -impl FromValue for SlotMigration { - fn from_value(value: Value) -> Result { +impl FromValue for ClusterGetslotmigrationsEntry { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; let slot_ranges: SlotRanges = fields.required("slot_ranges")?; Ok(Self { @@ -801,9 +803,9 @@ impl FromValue for SlotMigration { /// `[id, timestamp, value, [argument ...], client-address, client-name]`, of SLOWLOG GET and /// COMMANDLOG GET. -impl FromValue for LogEntry { - fn from_value(value: Value) -> Result { - match <[Value; 6]>::try_from(items(value)?) { +impl FromValue for CommandlogEntry { + fn from_value(value: RespValue) -> Result { + match <[RespValue; 6]>::try_from(items(value)?) { Ok([id, timestamp, value, arguments, client_address, client_name]) => Ok(Self { id: FromValue::from_value(id)?, timestamp: FromValue::from_value(timestamp)?, @@ -821,7 +823,7 @@ impl FromValue for LogEntry { } impl FromValue for LatencyHistogramEntry { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; // a map of each bucket to its cumulative count let histogram: FlatPairs = fields.required("histogram_usec")?; @@ -834,8 +836,8 @@ impl FromValue for LatencyHistogramEntry { /// `[event, timestamp, latest, max, sum, count]` impl FromValue for LatencyLatestEntry { - fn from_value(value: Value) -> Result { - match <[Value; 6]>::try_from(items(value)?) { + fn from_value(value: RespValue) -> Result { + match <[RespValue; 6]>::try_from(items(value)?) { Ok([event, timestamp, latest, max, sum, count]) => Ok(Self { event: FromValue::from_value(event)?, timestamp: FromValue::from_value(timestamp)?, @@ -852,8 +854,8 @@ impl FromValue for LatencyLatestEntry { } } -impl FromValue for ModuleInfo { - fn from_value(value: Value) -> Result { +impl FromValue for ModuleListEntry { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { name: fields.required("name")?, @@ -865,15 +867,15 @@ impl FromValue for ModuleInfo { } /// A number sent as a string, e.g. the port of a replica in ROLE. -fn parse_string(value: Value) -> Result { +fn parse_string(value: RespValue) -> Result { let text = String::from_value(value)?; text.parse() .map_err(|_| Error::Client(format!("unexpected number: {text:?}"))) } /// `[ip, port, offset]`, the port and offset are strings. -impl FromValue for RoleReplicaInfo { - fn from_value(value: Value) -> Result { +impl FromValue for RolePrimaryReplica { + fn from_value(value: RespValue) -> Result { let (ip, port, offset) = FromValue::from_value(value)?; Ok(Self { ip, @@ -886,9 +888,9 @@ impl FromValue for RoleReplicaInfo { /// `["master", offset, [replica ...]]`, `["slave", host, port, state, offset]` or /// `["sentinel", [primary ...]]`. impl FromValue for RoleReply { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let mut items = items(value)?.into_iter(); - let role = String::from_value(items.next().unwrap_or(Value::Null))?; + let role = String::from_value(items.next().unwrap_or(RespValue::Null))?; let mut next = || { items .next() @@ -918,7 +920,7 @@ impl FromValue for RoleReply { pub struct ClientList(pub Vec>); impl FromValue for ClientList { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let text = String::from_value(value)?; let clients = text .lines() @@ -935,19 +937,19 @@ impl FromValue for ClientList { } /// `-1` when tracking is off, `0` when not redirected, otherwise the ID of the connection. -impl FromValue for TrackingRedirect { - fn from_value(value: Value) -> Result { +impl FromValue for ClientTrackingRedirect { + fn from_value(value: RespValue) -> Result { match Integer::from_value(value)? { - -1 => Ok(TrackingRedirect::NotTracking), - 0 => Ok(TrackingRedirect::NotRedirected), - id if id > 0 => Ok(TrackingRedirect::Redirected(id)), + -1 => Ok(ClientTrackingRedirect::NotTracking), + 0 => Ok(ClientTrackingRedirect::NotRedirected), + id if id > 0 => Ok(ClientTrackingRedirect::Redirected(id)), code => Err(Error::Client(format!("unexpected reply: {code}"))), } } } impl FromValue for ClientTrackinginfoReply { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { flags: fields.required("flags")?, @@ -962,7 +964,7 @@ impl FromValue for ClientTrackinginfoReply { pub struct CommandInfoList(pub Vec); impl FromValue for CommandInfoList { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let mut commands = vec![]; for command in items(value)? { command_info(command, &mut commands)?; @@ -973,8 +975,8 @@ impl FromValue for CommandInfoList { /// `[name, arity, flags, first-key, last-key, step, acl-categories, tips, key-specs, subcommands]`, /// the ACL categories since 6.0, the rest since 7.0. -fn command_info(value: Value, commands: &mut Vec) -> Result<(), Error> { - if value == Value::Null { +fn command_info(value: RespValue, commands: &mut Vec) -> Result<(), Error> { + if value == RespValue::Null { return Ok(()); } let mut items = items(value)?.into_iter(); @@ -994,7 +996,7 @@ fn command_info(value: Value, commands: &mut Vec) -> Result<() tips: next().map_or(Ok(vec![]), FromValue::from_value)?, key_specs: next().map_or(Ok(vec![]), FromValue::from_value)?, }; - let subcommands: Vec = next().map_or(Ok(vec![]), FromValue::from_value)?; + let subcommands: Vec = next().map_or(Ok(vec![]), FromValue::from_value)?; commands.push(info); for subcommand in subcommands { command_info(subcommand, commands)?; @@ -1002,8 +1004,8 @@ fn command_info(value: Value, commands: &mut Vec) -> Result<() Ok(()) } -impl FromValue for KeySpec { - fn from_value(value: Value) -> Result { +impl FromValue for CommandKeySpec { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; let mut begin_search: Fields = fields.required("begin_search")?; let mut find_keys: Fields = fields.required("find_keys")?; @@ -1017,35 +1019,35 @@ impl FromValue for KeySpec { } /// `{type, spec}`, a type this interface does not know is `unknown`. -fn begin_search_spec(fields: &mut Fields) -> Result { +fn begin_search_spec(fields: &mut Fields) -> Result { let kind: String = fields.required("type")?; let mut spec: Fields = fields.required("spec")?; Ok(match kind.as_str() { - "index" => KeySpecBeginSearch::Index(spec.required("index")?), - "keyword" => KeySpecBeginSearch::Keyword(KeySpecKeyword { + "index" => CommandKeySpecBeginSearch::Index(spec.required("index")?), + "keyword" => CommandKeySpecBeginSearch::Keyword(CommandKeySpecKeyword { keyword: spec.required("keyword")?, startfrom: spec.required("startfrom")?, }), - _ => KeySpecBeginSearch::Unknown, + _ => CommandKeySpecBeginSearch::Unknown, }) } /// `{type, spec}`, a type this interface does not know is `unknown`. -fn find_keys_spec(fields: &mut Fields) -> Result { +fn find_keys_spec(fields: &mut Fields) -> Result { let kind: String = fields.required("type")?; let mut spec: Fields = fields.required("spec")?; Ok(match kind.as_str() { - "range" => KeySpecFindKeys::Range(KeySpecRange { + "range" => CommandKeySpecFindKeys::Range(CommandKeySpecRange { lastkey: spec.required("lastkey")?, keystep: spec.required("keystep")?, limit: spec.required("limit")?, }), - "keynum" => KeySpecFindKeys::Keynum(KeySpecKeynum { + "keynum" => CommandKeySpecFindKeys::Keynum(CommandKeySpecKeynum { keynumidx: spec.required("keynumidx")?, firstkey: spec.required("firstkey")?, keystep: spec.required("keystep")?, }), - _ => KeySpecFindKeys::Unknown, + _ => CommandKeySpecFindKeys::Unknown, }) } @@ -1053,7 +1055,7 @@ fn find_keys_spec(fields: &mut Fields) -> Result { pub struct CommandDocsList(pub Vec); impl FromValue for CommandDocsList { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let mut commands = vec![]; for (name, doc) in entries::(value)? { command_doc(name, doc, &mut commands)?; @@ -1097,13 +1099,13 @@ fn command_doc( fn command_arguments( nested: Vec, parent: Option, - arguments: &mut Vec, + arguments: &mut Vec, ) -> Result<(), Error> { for mut fields in nested { let index = u32::try_from(arguments.len()) .map_err(|_| Error::Client("too many arguments".to_string()))?; let kind: String = fields.required("type")?; - arguments.push(CommandArgument { + arguments.push(CommandDocsArgument { parent, name: fields.required("name")?, type_: argument_type(kind), @@ -1121,23 +1123,23 @@ fn command_arguments( Ok(()) } -fn argument_type(kind: String) -> CommandArgumentType { +fn argument_type(kind: String) -> CommandDocsArgumentType { match kind.as_str() { - "string" => CommandArgumentType::String, - "integer" => CommandArgumentType::Integer, - "double" => CommandArgumentType::Double, - "key" => CommandArgumentType::Key, - "pattern" => CommandArgumentType::Pattern, - "unix-time" => CommandArgumentType::UnixTime, - "pure-token" => CommandArgumentType::PureToken, - "oneof" => CommandArgumentType::Oneof, - "block" => CommandArgumentType::Block, - _ => CommandArgumentType::Other(kind), + "string" => CommandDocsArgumentType::String, + "integer" => CommandDocsArgumentType::Integer, + "double" => CommandDocsArgumentType::Double, + "key" => CommandDocsArgumentType::Key, + "pattern" => CommandDocsArgumentType::Pattern, + "unix-time" => CommandDocsArgumentType::UnixTime, + "pure-token" => CommandDocsArgumentType::PureToken, + "oneof" => CommandDocsArgumentType::Oneof, + "block" => CommandDocsArgumentType::Block, + _ => CommandDocsArgumentType::Other(kind), } } -impl FromValue for TtlResult { - fn from_value(value: Value) -> Result { +impl FromValue for HexpireResult { + fn from_value(value: RespValue) -> Result { match Integer::from_value(value)? { -2 => Ok(Self::DoesNotExist), 0 => Ok(Self::ConditionNotMet), @@ -1150,7 +1152,7 @@ impl FromValue for TtlResult { /// A map of the matches and the length of the longest common subsequence. impl FromValue for LcsIdxReply { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { matches: fields.required("matches")?, @@ -1161,7 +1163,7 @@ impl FromValue for LcsIdxReply { /// The inclusive ranges of a match in each key and, with `WITHMATCHLEN`, its length. impl FromValue for LcsIdxMatch { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let mut items = items(value)?.into_iter(); let (Some(key1_range), Some(key2_range), len, None) = (items.next(), items.next(), items.next(), items.next()) @@ -1178,8 +1180,8 @@ impl FromValue for LcsIdxMatch { } } -impl FromValue for XinfoConsumer { - fn from_value(value: Value) -> Result { +impl FromValue for XinfoConsumersEntry { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { name: fields.required("name")?, @@ -1192,8 +1194,8 @@ impl FromValue for XinfoConsumer { } } -impl FromValue for XinfoGroup { - fn from_value(value: Value) -> Result { +impl FromValue for XinfoGroupsEntry { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { name: fields.required("name")?, @@ -1207,7 +1209,7 @@ impl FromValue for XinfoGroup { } impl FromValue for XinfoStreamReply { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { length: fields.required("length")?, @@ -1224,7 +1226,7 @@ impl FromValue for XinfoStreamReply { } impl FromValue for XinfoStreamReplyFull { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { length: fields.required("length")?, @@ -1240,7 +1242,7 @@ impl FromValue for XinfoStreamReplyFull { } impl FromValue for XinfoStreamReplyFullGroup { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { name: fields.required("name")?, @@ -1255,7 +1257,7 @@ impl FromValue for XinfoStreamReplyFullGroup { } impl FromValue for XinfoStreamReplyFullConsumer { - fn from_value(value: Value) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { name: fields.required("name")?, @@ -1269,31 +1271,31 @@ impl FromValue for XinfoStreamReplyFullConsumer { } } -fn unexpected(reply: &Value) -> Error { +fn unexpected(reply: &RespValue) -> Error { Error::Client(format!("unexpected reply: {reply:?}")) } -fn decode(value: NestedValue) -> Result { +fn decode(value: RespNestedValue) -> Result { crate::codec::decode(value).map_err(Error::Resp) } /// The nested values of an array, or a set. -fn items(reply: Value) -> Result, Error> { +fn items(reply: RespValue) -> Result, Error> { match reply { - Value::Array(items) | Value::Set(items) => items.into_iter().map(decode).collect(), + RespValue::Array(items) | RespValue::Set(items) => items.into_iter().map(decode).collect(), reply => Err(unexpected(&reply)), } } /// The entries of a map, or of a flat array of keys and values. -pub fn entries(reply: Value) -> Result, Error> { +pub fn entries(reply: RespValue) -> Result, Error> { let entry = |key, value| Ok((K::from_value(decode(key)?)?, V::from_value(decode(value)?)?)); match reply { - Value::Map(entries) => entries + RespValue::Map(entries) => entries .into_iter() .map(|(key, value)| entry(key, value)) .collect(), - Value::Array(items) => { + RespValue::Array(items) => { if items.len() % 2 != 0 { return Err(Error::Client(format!( "unexpected odd number of keys and values: {}", diff --git a/crates/test-harness/src/lib.rs b/crates/test-harness/src/lib.rs index 7246f4a..2a08cd7 100644 --- a/crates/test-harness/src/lib.rs +++ b/crates/test-harness/src/lib.rs @@ -23,7 +23,7 @@ use wasmtime::error::Context as _; use wasmtime::{Config, Engine, Result, Store, StoreContextMut, bail, format_err}; use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; -pub use bindings::componentized::valkey::resp::{NestedValue, Value}; +pub use bindings::componentized::valkey::resp::{RespNestedValue, RespValue}; pub use bindings::exports::componentized::valkey::connection::{ConnectOpts, Error}; mod server; @@ -79,53 +79,53 @@ pub mod resp_bindings { pub type Resp = resp_bindings::exports::componentized::valkey::resp::Guest; /// Encode a value as RESP, e.g. to nest it in an aggregate. -pub fn encode(value: Value) -> NestedValue { +pub fn encode(value: RespValue) -> RespNestedValue { componentized_valkey::codec::encode(to_codec(value)) } /// Decode a nested value. -pub fn decode(value: NestedValue) -> Result { +pub fn decode(value: RespNestedValue) -> Result { componentized_valkey::codec::decode(value).map(from_codec) } // a value is not recursive, nested values are encoded RESP, so each conversion is one match -fn to_codec(value: Value) -> componentized_valkey::types::Value { - use componentized_valkey::types::Value as V; +fn to_codec(value: RespValue) -> componentized_valkey::types::RespValue { + use componentized_valkey::types::RespValue as V; match value { - Value::String(value) => V::String(value), - Value::Error(value) => V::Error(value), - Value::Integer(value) => V::Integer(value), - Value::BulkString(value) => V::BulkString(value), - Value::Array(value) => V::Array(value), - Value::Null => V::Null, - Value::Boolean(value) => V::Boolean(value), - Value::Double(value) => V::Double(value), - Value::BigNumber(value) => V::BigNumber(value), - Value::BulkError(value) => V::BulkError(value), - Value::VerbatimString(value) => V::VerbatimString(value), - Value::Map(value) => V::Map(value), - Value::Set(value) => V::Set(value), - Value::Push(value) => V::Push(value), - } -} - -fn from_codec(value: componentized_valkey::types::Value) -> Value { - use componentized_valkey::types::Value as V; + RespValue::String(value) => V::String(value), + RespValue::Error(value) => V::Error(value), + RespValue::Integer(value) => V::Integer(value), + RespValue::BulkString(value) => V::BulkString(value), + RespValue::Array(value) => V::Array(value), + RespValue::Null => V::Null, + RespValue::Boolean(value) => V::Boolean(value), + RespValue::Double(value) => V::Double(value), + RespValue::BigNumber(value) => V::BigNumber(value), + RespValue::BulkError(value) => V::BulkError(value), + RespValue::VerbatimString(value) => V::VerbatimString(value), + RespValue::Map(value) => V::Map(value), + RespValue::Set(value) => V::Set(value), + RespValue::Push(value) => V::Push(value), + } +} + +fn from_codec(value: componentized_valkey::types::RespValue) -> RespValue { + use componentized_valkey::types::RespValue as V; match value { - V::String(value) => Value::String(value), - V::Error(value) => Value::Error(value), - V::Integer(value) => Value::Integer(value), - V::BulkString(value) => Value::BulkString(value), - V::Array(value) => Value::Array(value), - V::Null => Value::Null, - V::Boolean(value) => Value::Boolean(value), - V::Double(value) => Value::Double(value), - V::BigNumber(value) => Value::BigNumber(value), - V::BulkError(value) => Value::BulkError(value), - V::VerbatimString(value) => Value::VerbatimString(value), - V::Map(value) => Value::Map(value), - V::Set(value) => Value::Set(value), - V::Push(value) => Value::Push(value), + V::String(value) => RespValue::String(value), + V::Error(value) => RespValue::Error(value), + V::Integer(value) => RespValue::Integer(value), + V::BulkString(value) => RespValue::BulkString(value), + V::Array(value) => RespValue::Array(value), + V::Null => RespValue::Null, + V::Boolean(value) => RespValue::Boolean(value), + V::Double(value) => RespValue::Double(value), + V::BigNumber(value) => RespValue::BigNumber(value), + V::BulkError(value) => RespValue::BulkError(value), + V::VerbatimString(value) => RespValue::VerbatimString(value), + V::Map(value) => RespValue::Map(value), + V::Set(value) => RespValue::Set(value), + V::Push(value) => RespValue::Push(value), } } @@ -138,8 +138,8 @@ pub use bindings::exports::componentized::valkey::{ pub use bindings::exports::wasmcloud::keyvalue; /// A bulk string, the type commands are sent as. -pub fn bulk(value: &str) -> Value { - Value::BulkString(value.to_string()) +pub fn bulk(value: &str) -> RespValue { + RespValue::BulkString(value.to_string()) } /// The words of a command to send, e.g. `words(&["GET", "key"])`. @@ -148,13 +148,13 @@ pub fn words(words: &[&str]) -> Vec { } /// A command as the server receives it, e.g. `command(&["GET", "key"])`. -pub fn command(words: &[&str]) -> Vec { +pub fn command(words: &[&str]) -> Vec { words.iter().map(|word| bulk(word)).collect() } /// A map reply, e.g. to HELLO. -pub fn map(entries: Vec<(Value, Value)>) -> Value { - Value::Map( +pub fn map(entries: Vec<(RespValue, RespValue)>) -> RespValue { + RespValue::Map( entries .into_iter() .map(|(key, value)| (encode(key), encode(value))) @@ -163,32 +163,32 @@ pub fn map(entries: Vec<(Value, Value)>) -> Value { } /// A push message, e.g. a pub/sub message. -pub fn push(items: Vec) -> Value { - Value::Push(items.into_iter().map(encode).collect()) +pub fn push(items: Vec) -> RespValue { + RespValue::Push(items.into_iter().map(encode).collect()) } /// The server's properties in the canned reply to HELLO. -pub fn hello_reply() -> Value { +pub fn hello_reply() -> RespValue { map(vec![ (bulk("server"), bulk("valkey")), (bulk("version"), bulk("9.1.2")), - (bulk("proto"), Value::Integer(3)), + (bulk("proto"), RespValue::Integer(3)), ]) } /// The stub transport and server of a connection. pub struct Transport { - requests: mpsc::UnboundedReceiver, - replies: Option>, + requests: mpsc::UnboundedReceiver, + replies: Option>, closed: Option>>, } impl Transport { /// The next command the connection wrote, as its words, `None` once it closed the requests /// stream. - pub async fn request(&mut self) -> Option> { + pub async fn request(&mut self) -> Option> { let request = self.requests.recv().await?; - let Value::Array(words) = request else { + let RespValue::Array(words) = request else { panic!("a command is an array, got {request:?}"); }; Some( @@ -200,7 +200,7 @@ impl Transport { } /// Send a value to the connection, a reply or a push message. - pub fn reply(&self, value: Value) { + pub fn reply(&self, value: RespValue) { let replies = self.replies.as_ref().expect("transport is open"); replies.send(value).expect("connection reads replies"); } @@ -429,11 +429,11 @@ impl WasiView for Ctx { // the components only import the types of `resp`, the functions are here for completeness impl bindings::componentized::valkey::resp::Host for Ctx { - fn decode(&mut self, data: Vec) -> Result { + fn decode(&mut self, data: Vec) -> Result { decode(data) } - fn encode(&mut self, value: Value) -> Result, String> { + fn encode(&mut self, value: RespValue) -> Result, String> { Ok(encode(value)) } } diff --git a/crates/test-harness/src/server.rs b/crates/test-harness/src/server.rs index 3b09ac4..bb96686 100644 --- a/crates/test-harness/src/server.rs +++ b/crates/test-harness/src/server.rs @@ -3,7 +3,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; -use crate::{Value, decode, encode}; +use crate::{RespValue, decode, encode}; /// Listens on localhost for the component's connections. pub struct Server { @@ -40,14 +40,14 @@ pub struct Client { impl Client { /// The next command the client sent, as its words, `None` once it closed its side. - pub async fn request(&mut self) -> Option> { + pub async fn request(&mut self) -> Option> { loop { if let Some(len) = componentized_valkey::codec::frame_len(&self.buffered).expect("valid RESP") { let rest = self.buffered.split_off(len); let frame = std::mem::replace(&mut self.buffered, rest); - let Value::Array(words) = decode(frame).expect("valid RESP") else { + let RespValue::Array(words) = decode(frame).expect("valid RESP") else { panic!("a command is an array"); }; return Some( @@ -67,7 +67,7 @@ impl Client { } /// Send a value to the client. - pub async fn reply(&mut self, value: Value) { + pub async fn reply(&mut self, value: RespValue) { self.write(&encode(value)).await; } diff --git a/wit/commands.wit b/wit/commands.wit index 6cf067b..652ae1f 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -10,31 +10,38 @@ interface commands { use connection.{connection}; use types.{ - double, error, integer, value, duration-seconds, duration-milliseconds, unix-time-seconds, - unix-time-milliseconds, cursor, ttl-mode, ttl-result, ttl-reply, pttl-reply, - expiretime-reply, pexpiretime-reply, acl-dryrun-result, acl-log-entry, bgsave-operation, - client-type, client-filter, client-pause-mode, client-unblock-type, - cluster-bumpepoch-result, cluster-failover-mode, slot-migration-operation, slot-migration, - slot-migration-group, cluster-reset-mode, cluster-setslot-state, commandlog-type, log-entry, - failover-target, latency-histogram-entry, latency-latest-entry, module-info, - role-replica-info, role-primary, role-replica, role-reply, shutdown-flags, shutdown-save, - acl-list-entry, rule, rule-selector, key-rule, channel-rule, command-rule, db-rule, - user-rule, bit-range, bitfield-encoding, bitfield-offset, bitfield-overflow, bitfield-get, - bitfield-set, bitfield-incrby, bitfield-operation, bitop-operation, key-spec-begin-search, - key-spec-keyword, key-spec-find-keys, key-spec-range, key-spec-keynum, key-spec, - command-info-reply, command-argument-type, command-argument, command-docs-reply, - tracking-redirect, client-setinfo-attr, client-tracking-mode, client-tracking-options, - client-trackinginfo-reply, command-list-filter, geo-coordinates, geo-unit, geoadd-condition, - geo-from, geo-radius, geo-box, geo-shape, geo-count, geosearch-with-flags, - geo-search-result, field-persist-result, hsetex-condition, hsetex-field-condition, key-type, - migrate-flags, migrate-auth, list-side, linsert-position, linsert-result, flush-mode, - library-function, library, running-function, engine-stats, function-stats-reply, limit, - sort-opts, sort-order, score-bound, lex-bound, pop-end, aggregate, zrange-by, - zrange-withscores-by, zadd-condition, zadd-comparison, stream-id, stream-trim, - stream-trim-maxlen, stream-trim-minid, stream-trim-efficiency, xclaim-opts, xinfo-consumer, - xinfo-group, xinfo-stream-reply, xinfo-stream-reply-full, xinfo-stream-reply-full-group, - xinfo-stream-reply-full-consumer, xpending-summary, xpending-extended-entry, getex-ttl, - lcs-idx-reply, lcs-idx-match, msetex-precondition, msetex-ttl, set-precondition, set-ttl + double, error, integer, resp-value, duration-seconds, duration-milliseconds, + unix-time-seconds, unix-time-milliseconds, cursor, ttl-mode, hexpire-result, + hpexpireat-result, hpexpire-result, hexpireat-result, ttl-reply, httl-reply, pttl-reply, + hpttl-reply, expiretime-reply, hexpiretime-reply, pexpiretime-reply, hpexpiretime-reply, + acl-dryrun-result, acl-log-entry, bgsave-operation, client-type, client-filter, + client-pause-mode, client-unblock-type, cluster-bumpepoch-result, cluster-failover-mode, + cluster-getslotmigrations-operation, cluster-getslotmigrations-entry, + cluster-migrateslots-group, cluster-reset-mode, cluster-setslot-state, commandlog-type, + commandlog-entry, slowlog-entry, failover-target, latency-histogram-entry, + latency-latest-entry, module-list-entry, role-primary-replica, role-primary, role-replica, + role-reply, shutdown-flags, shutdown-save, acl-list-entry, acl-rule, acl-selector-rule, + acl-key-rule, acl-channel-rule, acl-command-rule, acl-db-rule, acl-user-rule, bit-range, + bitfield-encoding, bitfield-offset, bitfield-overflow, bitfield-get, bitfield-ro-get, + bitfield-set, bitfield-incrby, bitfield-operation, bitop-operation, + command-key-spec-begin-search, command-key-spec-keyword, command-key-spec-find-keys, + command-key-spec-range, command-key-spec-keynum, command-key-spec, command-info-reply, + command-reply, command-docs-argument-type, command-docs-argument, command-docs-reply, + client-tracking-redirect, client-setinfo-attr, client-tracking-mode, + client-tracking-options, client-trackinginfo-reply, command-list-filter, geo-coordinates, + geo-unit, geoadd-condition, geosearch-from, geosearch-radius, geosearch-box, + geosearch-shape, geosearchstore-shape, geosearch-count, geosearchstore-count, + geosearch-with-flags, geosearch-with-result, hpersist-result, hsetex-condition, + hsetex-field-condition, key-type, migrate-flags, migrate-auth, list-side, linsert-position, + linsert-result, flush-mode, function-list-function, function-list-library, + function-stats-running, function-stats-engine, function-stats-reply, limit, sort-opts, + sort-ro-opts, sort-order, score-bound, lex-bound, zmpop-end, bzmpop-end, zset-aggregate, + zrange-by, zrangestore-by, zrange-withscores-by, zadd-condition, zadd-comparison, stream-id, + stream-trim, stream-trim-maxlen, stream-trim-minid, stream-trim-efficiency, xclaim-opts, + xinfo-consumers-entry, xinfo-groups-entry, xinfo-stream-reply, xinfo-stream-reply-full, + xinfo-stream-reply-full-group, xinfo-stream-reply-full-consumer, xpending-summary, + xpending-extended-entry, getex-ttl, hgetex-ttl, lcs-idx-reply, lcs-idx-match, + msetex-precondition, msetex-ttl, set-precondition, set-ttl, hsetex-ttl }; @@ -72,7 +79,7 @@ interface commands { /// /// ACL GETUSER username /// https://valkey.io/commands/acl-getuser/ - acl-getuser: async func(connection: borrow, username: string) -> result>, error>; + acl-getuser: async func(connection: borrow, username: string) -> result>, error>; /// The rules of every user, as written to an ACL file. /// @@ -113,7 +120,7 @@ interface commands { /// /// ACL SETUSER username [ rule ] [ [ rule ] ... ] /// https://valkey.io/commands/acl-setuser/ - acl-setuser: async func(connection: borrow, username: string, rules: option>) -> result<_, error>; + acl-setuser: async func(connection: borrow, username: string, rules: option>) -> result<_, error>; /// The names of the users. /// @@ -242,7 +249,7 @@ interface commands { /// /// CLUSTER GETSLOTMIGRATIONS /// https://valkey.io/commands/cluster-getslotmigrations/ - cluster-getslotmigrations: async func(connection: borrow) -> result, error>; + cluster-getslotmigrations: async func(connection: borrow) -> result, error>; /// Connects the node to another, adding it to the cluster. /// @@ -254,7 +261,7 @@ interface commands { /// /// CLUSTER MIGRATESLOTS SLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] NODE node-id [ AUTH username password ] [ SLOTSRANGE ... ] /// https://valkey.io/commands/cluster-migrateslots/ - cluster-migrateslots: async func(connection: borrow, migrations: list) -> result<_, error>; + cluster-migrateslots: async func(connection: borrow, migrations: list) -> result<_, error>; /// The replicas of the primary node, each a line in the format of CLUSTER NODES. /// @@ -297,7 +304,7 @@ interface commands { /// /// COMMANDLOG GET count 〈 slow | large-request | large-reply 〉 /// https://valkey.io/commands/commandlog-get/ - commandlog-get: async func(connection: borrow, count: integer, %type: commandlog-type) -> result, error>; + commandlog-get: async func(connection: borrow, count: integer, %type: commandlog-type) -> result, error>; /// The number of entries in the command log of the type. /// @@ -388,7 +395,7 @@ interface commands { /// /// MODULE LIST /// https://valkey.io/commands/module-list/ - module-list: async func(connection: borrow) -> result, error>; + module-list: async func(connection: borrow) -> result, error>; /// Loads the module at the path, with the arguments. /// @@ -450,7 +457,7 @@ interface commands { /// /// SLOWLOG GET [ count ] /// https://valkey.io/commands/slowlog-get/ - slowlog-get: async func(connection: borrow, count: option) -> result, error>; + slowlog-get: async func(connection: borrow, count: option) -> result, error>; /// The number of entries in the slow log. /// @@ -485,7 +492,7 @@ interface commands { /// /// BITFIELD_RO key [ GET encoding offset [ GET encoding offset ... ] ] /// https://valkey.io/commands/bitfield_ro/ - bitfield-ro: async func(connection: borrow, key: string, gets: option>) -> result, error>; + bitfield-ro: async func(connection: borrow, key: string, gets: option>) -> result, error>; /// Stores the bitwise operation of the strings stored at the source keys in the destination, /// returning its length, the length of the longest source. @@ -559,7 +566,7 @@ interface commands { /// /// CLIENT GETREDIR /// https://valkey.io/commands/client-getredir/ - client-getredir: async func(connection: borrow) -> result; + client-getredir: async func(connection: borrow) -> result; /// The ID of the connection, unique until the server restarts. /// @@ -619,7 +626,7 @@ interface commands { /// /// COMMAND /// https://valkey.io/commands/command/ - command: async func(connection: borrow) -> result, error>; + command: async func(connection: borrow) -> result, error>; /// The number of commands of the server. /// @@ -672,7 +679,7 @@ interface commands { /// /// HELLO 3 [ AUTH username password ] [ SETNAME clientname ] /// https://valkey.io/commands/hello/ - hello: async func(connection: borrow, auth: option>, client-name: option) -> result, error>; + hello: async func(connection: borrow, auth: option>, client-name: option) -> result, error>; /// Returns PONG if no argument is provided, otherwise return a copy of the argument as a bulk. /// @@ -759,7 +766,7 @@ interface commands { /// /// GEOSEARCH key [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] /// https://valkey.io/commands/geosearch/ - geosearch: async func(connection: borrow, key: string, shape: geo-shape, order: option, count: option) -> result, error>; + geosearch: async func(connection: borrow, key: string, shape: geosearch-shape, order: option, count: option) -> result, error>; /// The members of the geospatial index stored at key in the shape, with the details asked for. /// @@ -767,7 +774,7 @@ interface commands { /// /// GEOSEARCH key [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] [ WITHCOORD ] [ WITHDIST ] [ WITHHASH ] /// https://valkey.io/commands/geosearch/ - geosearch-with: async func(connection: borrow, key: string, shape: geo-shape, order: option, count: option, details: geosearch-with-flags) -> result, error>; + geosearch-with: async func(connection: borrow, key: string, shape: geosearch-shape, order: option, count: option, details: geosearch-with-flags) -> result, error>; /// Stores the members of the geospatial index stored at source in the shape, returning how /// many, with their positions, or with `storedist`, their distance from the center as the @@ -775,7 +782,7 @@ interface commands { /// /// GEOSEARCHSTORE destination source [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] [ STOREDIST ] /// https://valkey.io/commands/geosearchstore/ - geosearchstore: async func(connection: borrow, destination: string, source: string, shape: geo-shape, order: option, count: option, storedist: bool) -> result; + geosearchstore: async func(connection: borrow, destination: string, source: string, shape: geosearchstore-shape, order: option, count: option, storedist: bool) -> result; // hash commands @@ -796,13 +803,13 @@ interface commands { /// /// HEXPIRE key seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpire/ - hexpire: async func(connection: borrow, key: string, seconds: duration-seconds, mode: option, fields: list) -> result, error>; + hexpire: async func(connection: borrow, key: string, seconds: duration-seconds, mode: option, fields: list) -> result, error>; /// Manipulate the expiration time for existing hash fields. /// /// HEXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpireat/ - hexpireat: async func(connection: borrow, key: string, seconds: unix-time-seconds, mode: option, fields: list) -> result, error>; + hexpireat: async func(connection: borrow, key: string, seconds: unix-time-seconds, mode: option, fields: list) -> result, error>; /// When each of the hash fields expires, to the second. /// @@ -810,7 +817,7 @@ interface commands { /// /// HEXPIRETIME key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpiretime/ - hexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; + hexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; /// Returns the value associated with field in the hash stored at key. /// @@ -835,7 +842,7 @@ interface commands { /// /// HGETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hgetex/ - hgetex: async func(connection: borrow, key: string, ttl: option, fields: list) -> result>, error>; + hgetex: async func(connection: borrow, key: string, ttl: option, fields: list) -> result>, error>; /// Increments the number stored at field in the hash stored at key by increment. /// @@ -877,19 +884,19 @@ interface commands { /// /// HPERSIST key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpersist/ - hpersist: async func(connection: borrow, key: string, fields: list) -> result, error>; + hpersist: async func(connection: borrow, key: string, fields: list) -> result, error>; /// Manipulate the expiration time for existing hash fields, in milliseconds. /// /// HPEXPIRE key milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpire/ - hpexpire: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, mode: option, fields: list) -> result, error>; + hpexpire: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, mode: option, fields: list) -> result, error>; /// Manipulate the expiration time for existing hash fields, as a Unix time in milliseconds. /// /// HPEXPIREAT key unix-time-milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpireat/ - hpexpireat: async func(connection: borrow, key: string, milliseconds: unix-time-milliseconds, mode: option, fields: list) -> result, error>; + hpexpireat: async func(connection: borrow, key: string, milliseconds: unix-time-milliseconds, mode: option, fields: list) -> result, error>; /// When each of the hash fields expires, to the millisecond. /// @@ -897,7 +904,7 @@ interface commands { /// /// HPEXPIRETIME key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpiretime/ - hpexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; + hpexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; /// The remaining time to live of each of the hash fields, to the millisecond. /// @@ -905,7 +912,7 @@ interface commands { /// /// HPTTL key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpttl/ - hpttl: async func(connection: borrow, key: string, fields: list) -> result, error>; + hpttl: async func(connection: borrow, key: string, fields: list) -> result, error>; /// Random fields from the hash value stored at key, up to the count. /// @@ -956,7 +963,7 @@ interface commands { /// /// HSETEX key [ NX | XX ] [ FNX | FXX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] FIELDS numfields field value [ field value ... ] /// https://valkey.io/commands/hsetex/ - hsetex: async func(connection: borrow, key: string, condition: option, field-condition: option, ttl: option, field-and-values: list>) -> result; + hsetex: async func(connection: borrow, key: string, condition: option, field-condition: option, ttl: option, field-and-values: list>) -> result; /// Sets field in the hash stored at key to value, only if field does not yet exist. /// @@ -976,7 +983,7 @@ interface commands { /// /// HTTL key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/httl/ - httl: async func(connection: borrow, key: string, fields: list) -> result, error>; + httl: async func(connection: borrow, key: string, fields: list) -> result, error>; /// All values in the hash stored at key. /// @@ -1514,27 +1521,27 @@ interface commands { /// /// EVAL script numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/eval/ - eval: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; + eval: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; /// Runs the Lua script, which may not write, returning its result, see `eval`. /// /// EVAL_RO script numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/eval_ro/ - eval-ro: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; + eval-ro: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; /// Runs the Lua script cached with the SHA1 digest, see `script-load`, returning its result, see /// `eval`. /// /// EVALSHA sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/evalsha/ - evalsha: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; + evalsha: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; /// Runs the Lua script cached with the SHA1 digest, which may not write, returning its result, /// see `eval`. /// /// EVALSHA_RO sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/evalsha_ro/ - evalsha-ro: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; + evalsha-ro: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; /// Calls the function, see `function-load`, returning its result, the keys it accesses and its /// arguments are passed to it. @@ -1543,13 +1550,13 @@ interface commands { /// /// FCALL function numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/fcall/ - fcall: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; + fcall: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; /// Calls the function, which must be flagged `no-writes`, returning its result, see `fcall`. /// /// FCALL_RO function numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/fcall_ro/ - fcall-ro: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; + fcall-ro: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; /// Deletes the library and its functions. /// @@ -1573,7 +1580,7 @@ interface commands { /// /// FUNCTION LIST [ LIBRARYNAME library-name-pattern ] [ WITHCODE ] /// https://valkey.io/commands/function-list/ - function-list: async func(connection: borrow, library-name-pattern: option, withcode: bool) -> result, error>; + function-list: async func(connection: borrow, library-name-pattern: option, withcode: bool) -> result, error>; /// Loads the library from its source code, replacing a library of the same name with /// `replace`, returning its name. @@ -1763,7 +1770,7 @@ interface commands { /// /// SORT_RO key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] /// https://valkey.io/commands/sort_ro/ - sort-ro: async func(connection: borrow, key: string, opts: option) -> result>>, error>; + sort-ro: async func(connection: borrow, key: string, opts: option) -> result>>, error>; // sortedset commands @@ -1778,7 +1785,7 @@ interface commands { /// /// BZMPOP timeout numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ] /// https://valkey.io/commands/bzmpop/ - bzmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: pop-end, count: option) -> result>>>, error>; + bzmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: bzmpop-end, count: option) -> result>>>, error>; /// Removes the member with the highest score from the first non-empty sorted set, waiting up to /// the timeout for one, returning its key, the member and its score. @@ -1871,7 +1878,7 @@ interface commands { /// /// ZINTER numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zinter/ - zinter: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; + zinter: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; /// The members, and their combined scores, in all of the sorted sets. /// @@ -1879,7 +1886,7 @@ interface commands { /// /// ZINTER numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] WITHSCORES /// https://valkey.io/commands/zinter/ - zinter-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; + zinter-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; /// The number of members in all of the sorted sets, counting stops at the limit. /// @@ -1891,7 +1898,7 @@ interface commands { /// /// ZINTERSTORE destination numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zinterstore/ - zinterstore: async func(connection: borrow, destination: string, keys: list, weights: option>, aggregate: option) -> result; + zinterstore: async func(connection: borrow, destination: string, keys: list, weights: option>, aggregate: option) -> result; /// The number of members of the sorted set stored at key in the range, for members of equal /// score. @@ -1905,7 +1912,7 @@ interface commands { /// /// ZMPOP numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ] /// https://valkey.io/commands/zmpop/ - zmpop: async func(connection: borrow, keys: list, end: pop-end, count: option) -> result>>>, error>; + zmpop: async func(connection: borrow, keys: list, end: zmpop-end, count: option) -> result>>>, error>; /// The score of each of the members of the sorted set stored at key, none for a member that is /// not in it. @@ -1981,7 +1988,7 @@ interface commands { /// /// ZRANGESTORE dst src min max [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ] /// https://valkey.io/commands/zrangestore/ - zrangestore: async func(connection: borrow, destination: string, source: string, range: zrange-by, rev: bool, limit: option) -> result; + zrangestore: async func(connection: borrow, destination: string, source: string, range: zrangestore-by, rev: bool, limit: option) -> result; /// The position of the member in the sorted set stored at key, from the lowest score, none when /// it is not a member. @@ -2076,7 +2083,7 @@ interface commands { /// /// ZUNION numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zunion/ - zunion: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; + zunion: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; /// The members, and their combined scores, in any of the sorted sets. /// @@ -2084,13 +2091,13 @@ interface commands { /// /// ZUNION numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] WITHSCORES /// https://valkey.io/commands/zunion/ - zunion-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; + zunion-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; /// Stores the members in any of the sorted sets, with their combined scores, returning how many. /// /// ZUNIONSTORE destination numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zunionstore/ - zunionstore: async func(connection: borrow, destination: string, keys: list, weights: option>, aggregate: option) -> result; + zunionstore: async func(connection: borrow, destination: string, keys: list, weights: option>, aggregate: option) -> result; // stream commands @@ -2188,13 +2195,13 @@ interface commands { /// /// XINFO CONSUMERS key group /// https://valkey.io/commands/xinfo-consumers/ - xinfo-consumers: async func(connection: borrow, key: string, group: string) -> result, error>; + xinfo-consumers: async func(connection: borrow, key: string, group: string) -> result, error>; /// Consumer groups of the stream stored at . /// /// XINFO GROUPS key /// https://valkey.io/commands/xinfo-groups/ - xinfo-groups: async func(connection: borrow, key: string) -> result, error>; + xinfo-groups: async func(connection: borrow, key: string) -> result, error>; /// Information about the stream stored at . /// diff --git a/wit/connection.wit b/wit/connection.wit index 436bed2..9ece459 100644 --- a/wit/connection.wit +++ b/wit/connection.wit @@ -9,7 +9,7 @@ /// with them, push messages, and transactions. Higher level, typed operations are functions in the /// `commands` interface that borrow the connection. interface connection { - use types.{connect-opts, duration-milliseconds, error, value}; + use types.{connect-opts, duration-milliseconds, error, resp-value}; resource connection { /// Establishes a connection over a transport. @@ -26,10 +26,10 @@ interface connection { /// /// Async so the connection keeps working between commands after it returns, e.g. to read /// push messages and to send keep-alive pings. - new: static async func(replies: stream, closed: future>, opts: option) -> tuple, future>>; + new: static async func(replies: stream, closed: future>, opts: option) -> tuple, future>>; /// The server's properties from the reply to HELLO, e.g. `server`, `version` and `mode`. - server-properties: func() -> list>; + server-properties: func() -> list>; /// Sends a command and waits for its reply. /// @@ -41,7 +41,7 @@ interface connection { /// once the command is written. /// /// A command has at least one word, an empty command fails with a client error. - exchange: async func(command: list) -> result; + exchange: async func(command: list) -> result; /// Sends the commands one after another, with no other command sent between them, and waits /// for every reply. @@ -49,7 +49,7 @@ interface connection { /// Each command has its own result, in order, as if each was sent with `exchange`. Commands /// sharing a connection may otherwise be interleaved with other callers' commands, which /// matters for a sequence that must not be interrupted, e.g. MULTI, its commands and EXEC. - exchange-all: async func(commands: list>) -> list>; + exchange-all: async func(commands: list>) -> list>; /// Sends a command without waiting for its reply, resolving once the command is written. /// @@ -92,7 +92,7 @@ interface connection { /// MULTI, EXEC /// https://valkey.io/commands/multi/ /// https://valkey.io/commands/exec/ - transaction: async func(commands: list>) -> result>, error>; + transaction: async func(commands: list>) -> result>, error>; /// Push messages, e.g. from SUBSCRIBE or client tracking, kept apart from replies. /// @@ -102,7 +102,7 @@ interface connection { /// Each call returns a new stream that receives every push message arriving after the call, /// push messages arriving while no stream is open are dropped. A stream that falls too far /// behind is closed rather than holding up replies, the push messages it missed are lost. - pushes: func() -> stream; + pushes: func() -> stream; /// Sends PING when no command has been sent and no reply is outstanding for `interval`, /// `none` disables it. A PING that fails closes the connection. Disabled by default. diff --git a/wit/tcp.wit b/wit/tcp.wit index 4ec8d41..ad67a14 100644 --- a/wit/tcp.wit +++ b/wit/tcp.wit @@ -1,6 +1,6 @@ /// A transport for connections over wasi:sockets TCP. interface tcp { - use types.{connect-opts, error, value}; + use types.{connect-opts, error, resp-value}; use connection.{connection}; /// A TCP connection to a Valkey server, framing the bytes it carries into RESP values in each @@ -15,14 +15,14 @@ interface tcp { /// May be called at most once, like `tcp-socket.send`. Closing `requests` half-closes the /// socket. The future resolves once every value is written, or with the error that stopped /// writing. - send: func(requests: stream) -> future>; + send: func(requests: stream) -> future>; /// The values the server sends, in order. /// /// May be called at most once, like `tcp-socket.receive`. The stream closes when the server /// closes the socket, the future resolves to `ok` for a graceful close or to the error /// otherwise. - receive: func() -> tuple, future>>; + receive: func() -> tuple, future>>; } /// Opens a transport and establishes a connection over it with `opts`, see `connection.new`. diff --git a/wit/types.wit b/wit/types.wit index 52756db..a6213df 100644 --- a/wit/types.wit +++ b/wit/types.wit @@ -13,26 +13,26 @@ interface types { type double = f64; - variant value { + variant resp-value { %string(string), error(string), integer(integer), bulk-string(string), - array(list), + array(list), null, boolean(bool), double(double), big-number(string), bulk-error(string), verbatim-string(tuple), - %map(list>), - set(list), - push(list), + %map(list>), + set(list), + push(list), } /// Values nested within an aggregate type. Since WIT types are not allowed to depend on /// themselves, nested values are stored as encoded RESP. - type nested-value = list; + type resp-nested-value = list; // connection @@ -106,7 +106,7 @@ interface types { } /// The outcome of setting the expiration of a hash field, e.g. with HEXPIRE. - enum ttl-result { + enum hexpire-result { /// -2: key does not exist, or field does not exist in the HASH. does-not-exist, /// 0: the specified NX | XX | GT | LT condition has not been met. @@ -117,6 +117,15 @@ interface types { zero-seconds, } + /// The result for a field, see `hexpireat`. + type hexpireat-result = hexpire-result; + + /// The result for a field, see `hpexpire`. + type hpexpire-result = hexpire-result; + + /// The result for a field, see `hpexpireat`. + type hpexpireat-result = hexpire-result; + /// The remaining time to live of a key or hash field, to the second, see `ttl` and `httl`. variant ttl-reply { /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. @@ -127,6 +136,9 @@ interface types { expires-in(duration-seconds), } + /// The remaining time to live of a field, see `httl`. + type httl-reply = ttl-reply; + /// The remaining time to live of a key or hash field, to the millisecond, see `pttl` and `hpttl`. variant pttl-reply { /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. @@ -137,6 +149,9 @@ interface types { expires-in(duration-milliseconds), } + /// The remaining time to live of a field, see `hpttl`. + type hpttl-reply = pttl-reply; + /// When a key or hash field expires, to the second, see `expiretime` and `hexpiretime`. variant expiretime-reply { /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. @@ -147,6 +162,9 @@ interface types { expires-at(unix-time-seconds), } + /// The expiration time of a field, see `hexpiretime`. + type hexpiretime-reply = expiretime-reply; + /// When a key or hash field expires, to the millisecond, see `pexpiretime` and `hpexpiretime`. variant pexpiretime-reply { /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. @@ -157,6 +175,9 @@ interface types { expires-at(unix-time-milliseconds), } + /// The expiration time of a field, see `hpexpiretime`. + type hpexpiretime-reply = pexpiretime-reply; + // admin commands /// Whether a user may run a command, see `acl-dryrun`. @@ -301,7 +322,7 @@ interface types { } /// Which way a slot migration moves the slots. - enum slot-migration-operation { + enum cluster-getslotmigrations-operation { /// `IMPORT`: to this node. %import, /// `EXPORT`: from this node. @@ -309,10 +330,10 @@ interface types { } /// A slot migration, see `cluster-getslotmigrations`. - record slot-migration { + record cluster-getslotmigrations-entry { /// The ID of the migration. link-name: string, - operation: slot-migration-operation, + operation: cluster-getslotmigrations-operation, /// The ranges of slots, start and end both included. slot-ranges: list>, /// The ID of the node the slots move to. @@ -331,7 +352,7 @@ interface types { } /// Ranges of slots to migrate to a node, see `cluster-migrateslots`. - record slot-migration-group { + record cluster-migrateslots-group { /// The ranges of slots, start and end both included. ranges: list>, /// The ID of the node to migrate the slots to. @@ -371,7 +392,7 @@ interface types { } /// An entry of the slow log or a command log. - record log-entry { + record commandlog-entry { /// The ID of the entry, unique until the server restarts. id: integer, /// When the command ran. @@ -387,6 +408,9 @@ interface types { client-name: string, } + /// An entry of the slow log, see `slowlog-get`. + type slowlog-entry = commandlog-entry; + /// Where FAILOVER fails over to. record failover-target { host: string, @@ -419,7 +443,7 @@ interface types { } /// A loaded module, see `module-list`. - record module-info { + record module-list-entry { name: string, /// The module's version. ver: integer, @@ -430,7 +454,7 @@ interface types { } /// A replica of a primary, see `role`. - record role-replica-info { + record role-primary-replica { ip: string, port: u16, /// The replication offset the replica last acknowledged. @@ -441,7 +465,7 @@ interface types { record role-primary { /// The primary's replication offset. offset: integer, - replicas: list, + replicas: list, } /// The replication state of a replica, see `role`. @@ -489,44 +513,44 @@ interface types { username: string, /// The user's rules, as the server normalized them, e.g. `allkeys` is `~*`. Applied with /// `acl-setuser`, after `reset`, they recreate the user. - rules: list, + rules: list, } /// A rule of an ACL user, see `acl-setuser`. /// /// https://valkey.io/topics/acl/ - variant rule { + variant acl-rule { /// The keys the user may access. - keys(key-rule), + keys(acl-key-rule), /// The Pub/Sub channels the user may access. - channel(channel-rule), + channel(acl-channel-rule), /// The commands the user may call. - command(command-rule), + command(acl-command-rule), /// The databases the user may access. - db(db-rule), + db(acl-db-rule), /// The user itself, e.g. whether it is enabled and its passwords. - user(user-rule), + user(acl-user-rule), /// A rule this interface does not know, e.g. from a newer server, sent as is. other(string), } /// A rule of a selector, a set of permissions matched independently of the user's root permissions, /// the user may run a command when either permits it. - variant rule-selector { + variant acl-selector-rule { /// The keys the selector permits access to. - keys(key-rule), + keys(acl-key-rule), /// The Pub/Sub channels the selector permits access to. - channel(channel-rule), + channel(acl-channel-rule), /// The commands the selector permits. - command(command-rule), + command(acl-command-rule), /// The databases the selector permits access to. - db(db-rule), + db(acl-db-rule), /// A rule this interface does not know, e.g. from a newer server, sent as is. other(string), } /// A rule for the keys a user may access. - variant key-rule { + variant acl-key-rule { /// `~`: allows reading and writing the keys matching the glob-style pattern. add(string), /// `%R~`: allows reading the keys matching the glob-style pattern. @@ -543,7 +567,7 @@ interface types { } /// A rule for the Pub/Sub channels a user may access. - variant channel-rule { + variant acl-channel-rule { /// `&`: allows access to the Pub/Sub channels matching the glob-style pattern. add(string), /// `allchannels`: allows access to all Pub/Sub channels, an alias for `&*`. @@ -554,7 +578,7 @@ interface types { } /// A rule for the commands a user may call. - variant command-rule { + variant acl-command-rule { /// `+`: allows the command, or a subcommand, e.g. `config|get`. add(string), /// `+@`: allows all the commands in the category, e.g. `read`, see ACL CAT for the @@ -574,7 +598,7 @@ interface types { } /// A rule for the databases a user may access. - variant db-rule { + variant acl-db-rule { /// `db=[,...]`: allows access to the databases with the given IDs. db(list), /// `alldbs`: allows access to all databases. @@ -584,7 +608,7 @@ interface types { } /// A rule for the user itself. - variant user-rule { + variant acl-user-rule { /// `on`: enables the user, it is possible to authenticate as this user. on, /// `off`: disables the user, it is no longer possible to authenticate as this user, connections @@ -606,7 +630,7 @@ interface types { /// `!`: removes a password, by its SHA-256 hash, from the user's passwords. remove-hashed-password(string), /// `( ...)`: adds a selector, with the given rules. - selector(list), + selector(list), /// `clearselectors`: removes all the user's selectors. clearselectors, /// `reset`: resets the user to its state when created, disabled, without passwords, keys, @@ -659,6 +683,9 @@ interface types { offset: bitfield-offset, } + /// A GET operation, see `bitfield-ro`. + type bitfield-ro-get = bitfield-get; + /// `SET encoding offset value`: sets the field, the result is its previous value. record bitfield-set { encoding: bitfield-encoding, @@ -700,35 +727,35 @@ interface types { // connection commands - /// How a key spec finds the first key, see `key-spec`. - variant key-spec-begin-search { + /// How a key spec finds the first key, see `command-key-spec`. + variant command-key-spec-begin-search { /// `index`: the argument at the index. index(integer), /// `keyword`: the argument after the keyword. - keyword(key-spec-keyword), + keyword(command-key-spec-keyword), /// `unknown`: the keys cannot be found from the arguments alone. unknown, } - /// The keyword before the first key, see `key-spec-begin-search`. - record key-spec-keyword { + /// The keyword before the first key, see `command-key-spec-begin-search`. + record command-key-spec-keyword { keyword: string, /// The index of the argument to start searching from, from the end when negative. startfrom: integer, } - /// How a key spec finds the keys after the first one, see `key-spec`. - variant key-spec-find-keys { + /// How a key spec finds the keys after the first one, see `command-key-spec`. + variant command-key-spec-find-keys { /// `range`: a range of arguments. - range(key-spec-range), + range(command-key-spec-range), /// `keynum`: the number of keys is an argument. - keynum(key-spec-keynum), + keynum(command-key-spec-keynum), /// `unknown`: the keys cannot be found from the arguments alone. unknown, } - /// A range of keys, see `key-spec-find-keys`. - record key-spec-range { + /// A range of keys, see `command-key-spec-find-keys`. + record command-key-spec-range { /// The index of the last key, relative to the first, from the end when negative. lastkey: integer, /// The number of arguments from one key to the next. @@ -737,8 +764,8 @@ interface types { limit: integer, } - /// A number of keys, see `key-spec-find-keys`. - record key-spec-keynum { + /// A number of keys, see `command-key-spec-find-keys`. + record command-key-spec-keynum { /// The index of the argument with the number of keys, relative to the first. keynumidx: integer, /// The index of the first key, relative to the first. @@ -748,12 +775,12 @@ interface types { } /// Where a command's keys are and how it uses them. - record key-spec { + record command-key-spec { notes: option, /// How the command uses the keys, e.g. `RW`, `access` and `update`. %flags: list, - begin-search: key-spec-begin-search, - find-keys: key-spec-find-keys, + begin-search: command-key-spec-begin-search, + find-keys: command-key-spec-find-keys, } /// A command, see `command` and `command-info`. @@ -774,11 +801,14 @@ interface types { acl-categories: list, /// Hints for clients, e.g. `nondeterministic_output`. tips: list, - key-specs: list, + key-specs: list, } - /// The type of an argument of a command, see `command-argument`. - variant command-argument-type { + /// The details of a command, see `command`. + type command-reply = command-info-reply; + + /// The type of an argument of a command, see `command-docs-argument`. + variant command-docs-argument-type { /// `string` %string, /// `integer` @@ -802,12 +832,12 @@ interface types { } /// An argument of a command, see `command-docs`. - record command-argument { + record command-docs-argument { /// The index of the `oneof` or `block` argument containing this one, in the command's /// arguments, none at the top level. parent: option, name: string, - %type: command-argument-type, + %type: command-docs-argument-type, /// The name to show for the argument. display-text: option, /// The index of the argument's key spec, for a key. @@ -843,12 +873,12 @@ interface types { /// The changes to the command, the version of each and what changed. history: list>, /// The arguments, depth first, those an argument contains after it. - arguments: list, + arguments: list, } /// Where a connection's tracking notifications are redirected, see `client-getredir` and /// `client-trackinginfo`. - variant tracking-redirect { + variant client-tracking-redirect { /// -1: tracking is not on. not-tracking, /// 0: tracking is on, the notifications are not redirected. @@ -891,7 +921,7 @@ interface types { /// The tracking flags, e.g. `on`, `bcast`, `optin` or `noloop`. %flags: list, /// Where the notifications are redirected. - redirect: tracking-redirect, + redirect: client-tracking-redirect, /// The prefixes of BCAST. prefixes: list, } @@ -939,7 +969,7 @@ interface types { } /// The center of a search, `FROMMEMBER` or `FROMLONLAT`. - variant geo-from { + variant geosearch-from { /// `FROMMEMBER member`: the position of a member of the index. member(string), /// `FROMLONLAT longitude latitude` @@ -947,38 +977,44 @@ interface types { } /// `BYRADIUS`: a circle around the center. - record geo-radius { - %from: geo-from, + record geosearch-radius { + %from: geosearch-from, radius: double, unit: geo-unit, } /// `BYBOX`: a rectangle around the center. - record geo-box { - %from: geo-from, + record geosearch-box { + %from: geosearch-from, width: double, height: double, unit: geo-unit, } /// The area of a search. - variant geo-shape { + variant geosearch-shape { /// `[ FROMMEMBER member | FROMLONLAT longitude latitude ] BYRADIUS radius unit` - radius(geo-radius), + radius(geosearch-radius), /// `[ FROMMEMBER member | FROMLONLAT longitude latitude ] BYBOX width height unit` - box(geo-box), + box(geosearch-box), /// `BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ]`: the polygon with /// the vertices, in order, which has no center. polygon(list), } + /// The area to search, see `geosearchstore`. + type geosearchstore-shape = geosearch-shape; + /// At most a count of the members, `COUNT count [ ANY ]`. - record geo-count { + record geosearch-count { count: integer, /// `ANY`: returns as soon as enough members are found, rather than the nearest ones. any: bool, } + /// The number of members to store, see `geosearchstore`. + type geosearchstore-count = geosearch-count; + /// The details GEOSEARCH returns of each member it found. flags geosearch-with-flags { /// `WITHCOORD`: the position. @@ -990,7 +1026,7 @@ interface types { } /// A member a search found, with the details asked for. - record geo-search-result { + record geosearch-with-result { member: string, /// `WITHDIST`: the distance from the center, in the unit of the shape. distance: option, @@ -1003,7 +1039,7 @@ interface types { // hash commands /// The outcome of removing the expiration of a hash field. - enum field-persist-result { + enum hpersist-result { /// -2: the field does not exist, or the hash does not exist. does-not-exist, /// -1: the field exists, but has no expiration. @@ -1103,7 +1139,7 @@ interface types { } /// A function of a library. - record library-function { + record function-list-function { name: string, description: option, /// The function's flags, e.g. `no-writes`. @@ -1111,17 +1147,17 @@ interface types { } /// A library of functions. - record library { + record function-list-library { library-name: string, /// The engine that runs the library, e.g. `LUA`. engine: string, - functions: list, + functions: list, /// The library's source code, with `withcode`. library-code: option, } /// The function that is running. - record running-function { + record function-stats-running { name: string, /// The command and its arguments that called the function. command: list, @@ -1130,7 +1166,7 @@ interface types { } /// The libraries and functions of an engine. - record engine-stats { + record function-stats-engine { libraries-count: integer, functions-count: integer, } @@ -1138,9 +1174,9 @@ interface types { /// The function that is running, and the libraries and functions of each engine. record function-stats-reply { /// None when no function is running. - running-script: option, + running-script: option, /// The statistics of each engine, by name. - engines: map, + engines: map, } // set commands @@ -1169,6 +1205,9 @@ interface types { alpha: bool, } + /// The options of the sort, see `sort-ro`. + type sort-ro-opts = sort-opts; + enum sort-order { /// ascending, smallest to largest. asc, @@ -1199,15 +1238,18 @@ interface types { } /// Which end of a sorted set to pop from. - enum pop-end { + enum zmpop-end { /// The members with the lowest scores. min, /// The members with the highest scores. max, } + /// The end to pop from, see `bzmpop`. + type bzmpop-end = zmpop-end; + /// How scores combine for a member in several sorted sets. - enum aggregate { + enum zset-aggregate { /// The sum of the weighted scores, the default. sum, /// The lowest weighted score. @@ -1227,6 +1269,9 @@ interface types { lex(tuple), } + /// The range of members to store, see `zrangestore`. + type zrangestore-by = zrange-by; + /// The range of ZRANGE WITHSCORES, which does not support BYLEX, see `zrange-by`. variant zrange-withscores-by { /// Positions, both included, negative from the end. @@ -1312,7 +1357,7 @@ interface types { } /// A consumer of a group, see `xinfo-consumers`. - record xinfo-consumer { + record xinfo-consumers-entry { /// The consumer's name. name: string, /// The number of entries pending for the consumer. @@ -1326,7 +1371,7 @@ interface types { } /// A consumer group of a stream, see `xinfo-groups`. - record xinfo-group { + record xinfo-groups-entry { /// The group's name. name: string, /// The number of consumers in the group. @@ -1462,6 +1507,9 @@ interface types { persist, } + /// The expiration to set on the fields, see `hgetex`. + type hgetex-ttl = getex-ttl; + /// The longest common subsequence of two strings, as the ranges matching in each, see `lcs-idx`. record lcs-idx-reply { /// The ranges that match, from the end of the strings to their start. @@ -1516,4 +1564,7 @@ interface types { /// Retain the time to live associated with the keys. keepttl, } + + /// The expiration to set on the fields, see `hsetex`. + type hsetex-ttl = set-ttl; } From 2042249554347d3cc6ee56011e76c1e191eac2df Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 13 Oct 2026 18:38:58 -0400 Subject: [PATCH 23/37] codec -> resp Signed-off-by: Scott Andrews --- NOTICE | 2 +- components/connection/src/lib.rs | 6 +++--- components/dep-cli/src/lib.rs | 2 +- components/tcp/src/lib.rs | 6 +++--- crates/componentized-valkey/NOTICE | 2 +- crates/componentized-valkey/README.md | 2 +- crates/componentized-valkey/src/lib.rs | 4 ++-- crates/componentized-valkey/src/{codec.rs => resp.rs} | 4 ++-- crates/componentized-valkey/src/value.rs | 2 +- crates/test-harness/src/lib.rs | 4 ++-- crates/test-harness/src/server.rs | 2 +- 11 files changed, 18 insertions(+), 18 deletions(-) rename crates/componentized-valkey/src/{codec.rs => resp.rs} (99%) diff --git a/NOTICE b/NOTICE index ddea1ff..09279a4 100644 --- a/NOTICE +++ b/NOTICE @@ -7,7 +7,7 @@ Your use of these subcomponents is subject to the terms and conditions of the su ------------------------------------------------------------------------------- This product includes a RESP codec adapted from resp v1.0.3 -(https://github.com/iorust/resp/tree/v1.0.3), in crates/componentized-valkey/src/codec.rs, +(https://github.com/iorust/resp/tree/v1.0.3), in crates/componentized-valkey/src/resp.rs, used under the MIT license: Copyright (c) 2016-2022 IORust Developers diff --git a/components/connection/src/lib.rs b/components/connection/src/lib.rs index 6efa1df..a95417a 100644 --- a/components/connection/src/lib.rs +++ b/components/connection/src/lib.rs @@ -213,7 +213,7 @@ fn enqueue( let command = RespValue::Array( command .into_iter() - .map(|word| componentized_valkey::codec::encode(RespValue::BulkString(word))) + .map(|word| componentized_valkey::resp::encode(RespValue::BulkString(word))) .collect(), ); inner @@ -291,7 +291,7 @@ async fn transaction( RespValue::Null => Ok(None), RespValue::Array(replies) => replies .into_iter() - .map(|reply| componentized_valkey::codec::decode(reply).map_err(Error::Resp)) + .map(|reply| componentized_valkey::resp::decode(reply).map_err(Error::Resp)) .collect::>() .map(Some), reply => Err(unexpected(&reply)), @@ -523,7 +523,7 @@ fn server_properties(reply: RespValue) -> Result, Error } fn decode(value: RespNestedValue) -> Result { - componentized_valkey::codec::decode(value).map_err(Error::Resp) + componentized_valkey::resp::decode(value).map_err(Error::Resp) } /// An error value from the server fails the command. diff --git a/components/dep-cli/src/lib.rs b/components/dep-cli/src/lib.rs index 9986824..2e5726d 100644 --- a/components/dep-cli/src/lib.rs +++ b/components/dep-cli/src/lib.rs @@ -379,7 +379,7 @@ fn render(value: RespValue) -> String { } fn render_nested(value: RespNestedValue) -> String { - match componentized_valkey::codec::decode(value) { + match componentized_valkey::resp::decode(value) { Ok(value) => render(value), Err(error) => format!("(invalid) {error}"), } diff --git a/components/tcp/src/lib.rs b/components/tcp/src/lib.rs index 5162cb0..6ff9491 100644 --- a/components/tcp/src/lib.rs +++ b/components/tcp/src/lib.rs @@ -164,11 +164,11 @@ async fn receive_pump( let result = 'pump: loop { // hand over each whole value buffered, a value may span several reads loop { - match componentized_valkey::codec::frame_len(&buffered) { + match componentized_valkey::resp::frame_len(&buffered) { Ok(Some(len)) => { let rest = buffered.split_off(len); let frame = std::mem::replace(&mut buffered, rest); - match componentized_valkey::codec::decode(frame) { + match componentized_valkey::resp::decode(frame) { Ok(value) => { if replies.write_one(value).await.is_some() { // nobody reads the values anymore @@ -210,7 +210,7 @@ async fn send_pump( }; while let Some(value) = requests.next().await { if !bytes - .write_all(componentized_valkey::codec::encode(value)) + .write_all(componentized_valkey::resp::encode(value)) .await .is_empty() { diff --git a/crates/componentized-valkey/NOTICE b/crates/componentized-valkey/NOTICE index 9b623b9..481f2c4 100644 --- a/crates/componentized-valkey/NOTICE +++ b/crates/componentized-valkey/NOTICE @@ -8,7 +8,7 @@ as noted in the LICENSE file. ------------------------------------------------------------------------------- This product includes a RESP codec adapted from resp v1.0.3 -(https://github.com/iorust/resp/tree/v1.0.3), in src/codec.rs, +(https://github.com/iorust/resp/tree/v1.0.3), in src/resp.rs, used under the MIT license: Copyright (c) 2016-2022 IORust Developers diff --git a/crates/componentized-valkey/README.md b/crates/componentized-valkey/README.md index 20e8b7a..d83dfaa 100644 --- a/crates/componentized-valkey/README.md +++ b/crates/componentized-valkey/README.md @@ -7,5 +7,5 @@ conversions between Rust values and commands and replies. Licensed under the Apache License, Version 2.0, see [LICENSE](./LICENSE). -The RESP codec, `src/codec.rs`, is adapted from https://github.com/iorust/resp/tree/v1.0.3, used +The RESP codec, `src/resp.rs`, is adapted from https://github.com/iorust/resp/tree/v1.0.3, used under the MIT license, see [NOTICE](./NOTICE). diff --git a/crates/componentized-valkey/src/lib.rs b/crates/componentized-valkey/src/lib.rs index 4453d02..8a400b6 100644 --- a/crates/componentized-valkey/src/lib.rs +++ b/crates/componentized-valkey/src/lib.rs @@ -2,7 +2,7 @@ //! its interfaces. //! //! - [`types`]: the package's `types` interface, shared by every component's bindings. -//! - [`codec`]: RESP serialization of a [`types::RespValue`], to and from bytes. +//! - [`resp`]: RESP serialization of a [`types::RespValue`], to and from bytes. //! - [`command`]: [`IntoCommand`], converting Rust values into the words of a command. //! - [`value`]: [`FromValue`], converting a reply into Rust values. //! @@ -36,8 +36,8 @@ mod bindings { /// The `componentized:valkey/types` interface. pub use bindings::componentized::valkey::types; -pub mod codec; pub mod command; +pub mod resp; pub mod value; pub use command::IntoCommand; diff --git a/crates/componentized-valkey/src/codec.rs b/crates/componentized-valkey/src/resp.rs similarity index 99% rename from crates/componentized-valkey/src/codec.rs rename to crates/componentized-valkey/src/resp.rs index 156bb5f..8089441 100644 --- a/crates/componentized-valkey/src/codec.rs +++ b/crates/componentized-valkey/src/resp.rs @@ -9,7 +9,7 @@ const CRLF_BYTES: &'static [u8] = b"\r\n"; /// Encodes RESP value to RESP binary buffer. /// # Examples /// ``` -/// # use componentized_valkey::codec::encode; +/// # use componentized_valkey::resp::encode; /// # use componentized_valkey::types::RespValue; /// let val = RespValue::String("OK".to_string()); /// assert_eq!(encode(val), b"+OK\r\n"); @@ -125,7 +125,7 @@ fn buf_encode(value: RespValue, buf: &mut Vec) { /// /// # Examples /// ``` -/// # use componentized_valkey::codec::{decode, encode}; +/// # use componentized_valkey::resp::{decode, encode}; /// # use componentized_valkey::types::RespValue; /// let value = RespValue::BulkString("Hello".to_string()); /// assert_eq!(decode(encode(value.clone())), Ok(value)); diff --git a/crates/componentized-valkey/src/value.rs b/crates/componentized-valkey/src/value.rs index eb622d5..345472e 100644 --- a/crates/componentized-valkey/src/value.rs +++ b/crates/componentized-valkey/src/value.rs @@ -1276,7 +1276,7 @@ fn unexpected(reply: &RespValue) -> Error { } fn decode(value: RespNestedValue) -> Result { - crate::codec::decode(value).map_err(Error::Resp) + crate::resp::decode(value).map_err(Error::Resp) } /// The nested values of an array, or a set. diff --git a/crates/test-harness/src/lib.rs b/crates/test-harness/src/lib.rs index 2a08cd7..15d937a 100644 --- a/crates/test-harness/src/lib.rs +++ b/crates/test-harness/src/lib.rs @@ -80,12 +80,12 @@ pub type Resp = resp_bindings::exports::componentized::valkey::resp::Guest; /// Encode a value as RESP, e.g. to nest it in an aggregate. pub fn encode(value: RespValue) -> RespNestedValue { - componentized_valkey::codec::encode(to_codec(value)) + componentized_valkey::resp::encode(to_codec(value)) } /// Decode a nested value. pub fn decode(value: RespNestedValue) -> Result { - componentized_valkey::codec::decode(value).map(from_codec) + componentized_valkey::resp::decode(value).map(from_codec) } // a value is not recursive, nested values are encoded RESP, so each conversion is one match diff --git a/crates/test-harness/src/server.rs b/crates/test-harness/src/server.rs index bb96686..fd1e8e9 100644 --- a/crates/test-harness/src/server.rs +++ b/crates/test-harness/src/server.rs @@ -43,7 +43,7 @@ impl Client { pub async fn request(&mut self) -> Option> { loop { if let Some(len) = - componentized_valkey::codec::frame_len(&self.buffered).expect("valid RESP") + componentized_valkey::resp::frame_len(&self.buffered).expect("valid RESP") { let rest = self.buffered.split_off(len); let frame = std::mem::replace(&mut self.buffered, rest); From edf90924ba2a4c17a41b8cacd33ec1c913e54687 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 13 Oct 2026 19:12:47 -0400 Subject: [PATCH 24/37] reorganize trait impls by type Signed-off-by: Scott Andrews --- components/commands/src/lib.rs | 19 +- crates/componentized-valkey/src/command.rs | 990 ++--------- .../src/commands/admin.rs | 1553 +++++++++++++++++ .../src/commands/bitmap.rs | 197 +++ .../src/commands/connection.rs | 737 ++++++++ .../componentized-valkey/src/commands/geo.rs | 269 +++ .../componentized-valkey/src/commands/hash.rs | 124 ++ .../src/commands/keyspace.rs | 196 +++ .../componentized-valkey/src/commands/list.rs | 61 + .../componentized-valkey/src/commands/mod.rs | 16 + .../src/commands/scripting.rs | 225 +++ .../src/commands/shared.rs | 309 ++++ .../src/commands/sortedset.rs | 256 +++ .../src/commands/stream.rs | 509 ++++++ .../src/commands/string.rs | 214 +++ crates/componentized-valkey/src/lib.rs | 5 + crates/componentized-valkey/src/test_util.rs | 79 + crates/componentized-valkey/src/value.rs | 1262 ++------------ wit/types.wit | 114 +- 19 files changed, 5097 insertions(+), 2038 deletions(-) create mode 100644 crates/componentized-valkey/src/commands/admin.rs create mode 100644 crates/componentized-valkey/src/commands/bitmap.rs create mode 100644 crates/componentized-valkey/src/commands/connection.rs create mode 100644 crates/componentized-valkey/src/commands/geo.rs create mode 100644 crates/componentized-valkey/src/commands/hash.rs create mode 100644 crates/componentized-valkey/src/commands/keyspace.rs create mode 100644 crates/componentized-valkey/src/commands/list.rs create mode 100644 crates/componentized-valkey/src/commands/mod.rs create mode 100644 crates/componentized-valkey/src/commands/scripting.rs create mode 100644 crates/componentized-valkey/src/commands/shared.rs create mode 100644 crates/componentized-valkey/src/commands/sortedset.rs create mode 100644 crates/componentized-valkey/src/commands/stream.rs create mode 100644 crates/componentized-valkey/src/commands/string.rs create mode 100644 crates/componentized-valkey/src/test_util.rs diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index b5f3cc4..ee1f458 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -18,14 +18,17 @@ use crate::exports::componentized::valkey::commands::{ XpendingSummary, ZaddComparison, ZaddCondition, ZmpopEnd, ZrangeBy, ZrangeWithscoresBy, ZsetAggregate, }; -use componentized_valkey::command::{ - Bit, DecimalSeconds, Fields, Flag, KeysAndArgs, LexRange, NamedOption, NonEmpty, NumKeys, - OnOff, ScoreRange, Zrange, -}; -use componentized_valkey::value::{ - AclList, AclUserRules, ClientList, CommandDocsList, CommandInfoList, FlatPairs, Migrated, - NextCursor, OptionalKeyType, OrNever, OrZeroId, ScoredMembers, -}; +use componentized_valkey::command::{Flag, NamedOption, NonEmpty, NumKeys}; +use componentized_valkey::commands::admin::{AclList, AclUserRules, ClientList}; +use componentized_valkey::commands::bitmap::Bit; +use componentized_valkey::commands::connection::{CommandDocsList, CommandInfoList, OnOff}; +use componentized_valkey::commands::hash::Fields; +use componentized_valkey::commands::keyspace::{Migrated, OptionalKeyType}; +use componentized_valkey::commands::scripting::KeysAndArgs; +use componentized_valkey::commands::shared::{DecimalSeconds, NextCursor}; +use componentized_valkey::commands::sortedset::{LexRange, ScoreRange, ScoredMembers, Zrange}; +use componentized_valkey::commands::stream::OrZeroId; +use componentized_valkey::value::{FlatPairs, OrNever}; use wit_bindgen::rt::Map; diff --git a/crates/componentized-valkey/src/command.rs b/crates/componentized-valkey/src/command.rs index 2023f22..750ffd3 100644 --- a/crates/componentized-valkey/src/command.rs +++ b/crates/componentized-valkey/src/command.rs @@ -1,18 +1,7 @@ -use crate::types::{ - AclChannelRule, AclCommandRule, AclDbRule, AclKeyRule, AclRule, AclSelectorRule, AclUserRule, - BgsaveOperation, BitRange, BitfieldEncoding, BitfieldGet, BitfieldIncrby, BitfieldOffset, - BitfieldOperation, BitfieldOverflow, BitfieldSet, ClientFilter, ClientPauseMode, - ClientSetinfoAttr, ClientTrackingMode, ClientTrackingOptions, ClientType, ClientUnblockType, - ClusterFailoverMode, ClusterMigrateslotsGroup, ClusterResetMode, ClusterSetslotState, - CommandListFilter, CommandlogType, Cursor, DurationMilliseconds, DurationSeconds, Error, - FailoverTarget, FlushMode, GeoCoordinates, GeoUnit, GeoaddCondition, GeosearchCount, - GeosearchFrom, GeosearchShape, GetexTtl, HsetexCondition, HsetexFieldCondition, KeyType, - LexBound, Limit, LinsertPosition, ListSide, MigrateAuth, MsetexPrecondition, ScoreBound, - SetPrecondition, SetTtl, ShutdownSave, SortOpts, SortOrder, StreamTrim, StreamTrimEfficiency, - TtlMode, UnixTimeMilliseconds, UnixTimeSeconds, XclaimOpts, ZaddComparison, ZaddCondition, - ZmpopEnd, ZrangeBy, ZsetAggregate, -}; -use crate::value::{OrZeroId, ZERO_ID}; +//! Converting Rust values into the words of a command, the impls for Rust's own types and the +//! helpers the commands share. The impls for the package's types are in [`crate::commands`]. + +use crate::types::Error; /// Converts a Rust type into the words of a command, e.g. `Option` into no word, or one. /// @@ -131,6 +120,7 @@ impl IntoCommand for Flag { } } +/// A value preceded by its name, e.g. `COUNT count`, no words for none. pub struct NamedOption(pub &'static str, pub Option); impl IntoCommand for NamedOption { @@ -143,260 +133,6 @@ impl IntoCommand for NamedOption { } } -/// `0-0` for none, e.g. to start XAUTOCLAIM from the beginning. -impl IntoCommand for OrZeroId { - fn into_command(self) -> Result, Error> { - Ok(vec![self.0.unwrap_or_else(|| ZERO_ID.to_string())]) - } -} - -/// Whole seconds, e.g. for EX. -impl IntoCommand for DurationSeconds { - fn into_command(self) -> Result, Error> { - self.ex.into_command() - } -} - -/// Whole milliseconds, e.g. for PX. -impl IntoCommand for DurationMilliseconds { - fn into_command(self) -> Result, Error> { - self.px.into_command() - } -} - -/// Whole seconds since the Unix epoch, e.g. for EXAT. -impl IntoCommand for UnixTimeSeconds { - fn into_command(self) -> Result, Error> { - self.exat.into_command() - } -} - -/// Whole milliseconds since the Unix epoch, e.g. for PXAT. -impl IntoCommand for UnixTimeMilliseconds { - fn into_command(self) -> Result, Error> { - self.pxat.into_command() - } -} - -impl IntoCommand for AclRule { - fn into_command(self) -> Result, Error> { - match self { - AclRule::Keys(rule) => rule.into_command(), - AclRule::Channel(rule) => rule.into_command(), - AclRule::Command(rule) => rule.into_command(), - AclRule::Db(rule) => rule.into_command(), - AclRule::User(rule) => rule.into_command(), - AclRule::Other(rule) => rule.into_command(), - } - } -} - -impl IntoCommand for AclSelectorRule { - fn into_command(self) -> Result, Error> { - match self { - AclSelectorRule::Keys(rule) => rule.into_command(), - AclSelectorRule::Channel(rule) => rule.into_command(), - AclSelectorRule::Command(rule) => rule.into_command(), - AclSelectorRule::Db(rule) => rule.into_command(), - AclSelectorRule::Other(rule) => rule.into_command(), - } - } -} - -impl IntoCommand for AclKeyRule { - fn into_command(self) -> Result, Error> { - Ok(vec![match self { - AclKeyRule::Add(pattern) => format!("~{pattern}"), - AclKeyRule::AddRead(pattern) => format!("%R~{pattern}"), - AclKeyRule::AddWrite(pattern) => format!("%W~{pattern}"), - AclKeyRule::AddReadWrite(pattern) => format!("%RW~{pattern}"), - AclKeyRule::All => "allkeys".to_string(), - AclKeyRule::Reset => "resetkeys".to_string(), - }]) - } -} - -impl IntoCommand for AclChannelRule { - fn into_command(self) -> Result, Error> { - Ok(vec![match self { - AclChannelRule::Add(pattern) => format!("&{pattern}"), - AclChannelRule::All => "allchannels".to_string(), - AclChannelRule::Reset => "resetchannels".to_string(), - }]) - } -} - -impl IntoCommand for AclCommandRule { - fn into_command(self) -> Result, Error> { - Ok(vec![match self { - AclCommandRule::Add(command) => format!("+{command}"), - AclCommandRule::AddCategory(category) => format!("+@{category}"), - AclCommandRule::AddFirstArg((command, arg)) => format!("+{command}|{arg}"), - AclCommandRule::All => "allcommands".to_string(), - AclCommandRule::Remove(command) => format!("-{command}"), - AclCommandRule::RemoveCategory(category) => format!("-@{category}"), - AclCommandRule::Reset => "nocommands".to_string(), - }]) - } -} - -/// `db=` requires at least one database. -impl IntoCommand for AclDbRule { - fn into_command(self) -> Result, Error> { - Ok(vec![match self { - AclDbRule::Db(dbs) if dbs.is_empty() => { - return Err(Error::Client("db requires at least one item".to_string())); - } - AclDbRule::Db(dbs) => format!( - "db={}", - dbs.iter() - .map(|db| db.to_string()) - .collect::>() - .join(",") - ), - AclDbRule::All => "alldbs".to_string(), - AclDbRule::Reset => "resetdbs".to_string(), - }]) - } -} - -impl IntoCommand for AclUserRule { - fn into_command(self) -> Result, Error> { - match self { - AclUserRule::On => "on".into_command(), - AclUserRule::Off => "off".into_command(), - AclUserRule::Nopass => "nopass".into_command(), - AclUserRule::SanitizePayload => "sanitize-payload".into_command(), - AclUserRule::SkipSanitizePayload => "skip-sanitize-payload".into_command(), - AclUserRule::Password(password) => format!(">{password}").into_command(), - AclUserRule::HashedPassword(hash) => format!("#{hash}").into_command(), - AclUserRule::RemovePassword(password) => format!("<{password}").into_command(), - AclUserRule::RemoveHashedPassword(hash) => format!("!{hash}").into_command(), - AclUserRule::Selector(rules) => ("(", rules, ")").into_command(), - AclUserRule::Clearselectors => "clearselectors".into_command(), - AclUserRule::Reset => "reset".into_command(), - } - } -} - -impl IntoCommand for TtlMode { - fn into_command(self) -> Result, Error> { - match self { - TtlMode::Nx => "NX", - TtlMode::Xx => "XX", - TtlMode::Gt => "GT", - TtlMode::Lt => "LT", - } - .into_command() - } -} - -impl IntoCommand for GetexTtl { - fn into_command(self) -> Result, Error> { - match self { - GetexTtl::Ex(duration) => ("EX", duration).into_command(), - GetexTtl::Px(duration) => ("PX", duration).into_command(), - GetexTtl::Exat(unix_time) => ("EXAT", unix_time).into_command(), - GetexTtl::Pxat(unix_time) => ("PXAT", unix_time).into_command(), - GetexTtl::Persist => "PERSIST".into_command(), - } - } -} - -/// Also `MsetexTtl`, the same type. -impl IntoCommand for SetTtl { - fn into_command(self) -> Result, Error> { - match self { - SetTtl::Ex(duration) => ("EX", duration).into_command(), - SetTtl::Px(duration) => ("PX", duration).into_command(), - SetTtl::Exat(unix_time) => ("EXAT", unix_time).into_command(), - SetTtl::Pxat(unix_time) => ("PXAT", unix_time).into_command(), - SetTtl::Keepttl => "KEEPTTL".into_command(), - } - } -} - -impl IntoCommand for SetPrecondition { - fn into_command(self) -> Result, Error> { - match self { - SetPrecondition::Nx => "NX".into_command(), - SetPrecondition::Xx => "XX".into_command(), - SetPrecondition::Ifeq(value) => ("IFEQ", value).into_command(), - SetPrecondition::Ifne(value) => ("IFNE", value).into_command(), - } - } -} - -impl IntoCommand for MsetexPrecondition { - fn into_command(self) -> Result, Error> { - match self { - MsetexPrecondition::Nx => "NX".into_command(), - MsetexPrecondition::Xx => "XX".into_command(), - } - } -} - -impl IntoCommand for StreamTrim { - fn into_command(self) -> Result, Error> { - //〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] - match self { - StreamTrim::Maxlen(trim) => ( - "MAXLEN", - trim.efficiency, - trim.threshold, - NamedOption("LIMIT", trim.limit), - ) - .into_command(), - StreamTrim::Minid(trim) => ( - "MINID", - trim.efficiency, - trim.threshold, - NamedOption("LIMIT", trim.limit), - ) - .into_command(), - } - } -} - -impl IntoCommand for StreamTrimEfficiency { - fn into_command(self) -> Result, Error> { - // = | ~ - (match self { - StreamTrimEfficiency::Exact => "=", - StreamTrimEfficiency::MearlyExact => "~", - }) - .into_command() - } -} - -impl IntoCommand for XclaimOpts { - fn into_command(self) -> Result, Error> { - // [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] [ LASTID lastid ] - ( - NamedOption("IDLE", self.idle), - NamedOption("TIME", self.time), - NamedOption("RETRYCOUNT", self.retrycount), - Flag("FORCE", self.force), - NamedOption("LASTID", self.lastid), - ) - .into_command() - } -} - -/// Milliseconds as seconds with a fraction, e.g. the timeout of BZPOPMIN, `1500` as `1.500`. -pub struct DecimalSeconds(pub DurationMilliseconds); - -impl IntoCommand for DecimalSeconds { - fn into_command(self) -> Result, Error> { - let milliseconds = self.0.px; - Ok(vec![format!( - "{}.{:03}", - milliseconds / 1_000, - milliseconds % 1_000 - )]) - } -} - /// A list the command requires at least one item of, e.g. the keys of DEL, named for the error when /// it is empty. pub struct NonEmpty(&'static str, Vec); @@ -436,619 +172,109 @@ impl IntoCommand for NumKeys { } } -/// `FIELDS numfields field [ field ... ]`, e.g. for HEXPIRE, a field and its value counts as one. -pub struct Fields(pub NonEmpty); - -impl IntoCommand for Fields { - fn into_command(self) -> Result, Error> { - ("FIELDS", NumKeys(self.0)).into_command() - } -} - -impl IntoCommand for Limit { - fn into_command(self) -> Result, Error> { - ("LIMIT", self.offset, self.count).into_command() - } -} - -/// The start and stop of a range of scores, as given, the start is the maximum with REV. -pub struct ScoreRange { - pub start: ScoreBound, - pub stop: ScoreBound, - pub rev: bool, -} - -impl IntoCommand for ScoreRange { - fn into_command(self) -> Result, Error> { - let bound = |bound, unbounded: &str| match bound { - ScoreBound::Inclusive(score) => score.to_string(), - ScoreBound::Exclusive(score) => format!("({score}"), - ScoreBound::Unbounded => unbounded.to_string(), - }; - let (low, high) = ("-inf", "+inf"); - let (start, stop) = if self.rev { (high, low) } else { (low, high) }; - Ok(vec![bound(self.start, start), bound(self.stop, stop)]) - } -} - -/// The start and stop of a range of members, as given, the start is the maximum with REV. -pub struct LexRange { - pub start: LexBound, - pub stop: LexBound, - pub rev: bool, -} - -impl IntoCommand for LexRange { - fn into_command(self) -> Result, Error> { - let bound = |bound, unbounded: &str| match bound { - LexBound::Inclusive(member) => format!("[{member}"), - LexBound::Exclusive(member) => format!("({member}"), - LexBound::Unbounded => unbounded.to_string(), - }; - let (low, high) = ("-", "+"); - let (start, stop) = if self.rev { (high, low) } else { (low, high) }; - Ok(vec![bound(self.start, start), bound(self.stop, stop)]) - } -} - -/// ZRANGE and ZRANGESTORE, `start stop [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ]`. -pub struct Zrange { - pub range: ZrangeBy, - pub rev: bool, - pub limit: Option, -} - -impl IntoCommand for Zrange { - fn into_command(self) -> Result, Error> { - let Zrange { range, rev, limit } = self; - let (range, by) = match range { - ZrangeBy::Rank((start, stop)) => ((start, stop).into_command()?, None), - ZrangeBy::Score((start, stop)) => ( - ScoreRange { start, stop, rev }.into_command()?, - Some("BYSCORE"), - ), - ZrangeBy::Lex((start, stop)) => { - (LexRange { start, stop, rev }.into_command()?, Some("BYLEX")) - } - }; - (range, by, Flag("REV", rev), limit).into_command() - } -} - -impl IntoCommand for ZsetAggregate { - fn into_command(self) -> Result, Error> { - let aggregate = match self { - ZsetAggregate::Sum => "SUM", - ZsetAggregate::Min => "MIN", - ZsetAggregate::Max => "MAX", - }; - ("AGGREGATE", aggregate).into_command() - } -} - -impl IntoCommand for ZmpopEnd { - fn into_command(self) -> Result, Error> { - match self { - ZmpopEnd::Min => "MIN", - ZmpopEnd::Max => "MAX", - } - .into_command() - } -} - -impl IntoCommand for ZaddCondition { - fn into_command(self) -> Result, Error> { - match self { - ZaddCondition::Nx => "NX", - ZaddCondition::Xx => "XX", - } - .into_command() - } -} - -impl IntoCommand for ZaddComparison { - fn into_command(self) -> Result, Error> { - match self { - ZaddComparison::Gt => "GT", - ZaddComparison::Lt => "LT", - } - .into_command() - } -} - -impl IntoCommand for SortOrder { - fn into_command(self) -> Result, Error> { - match self { - SortOrder::Asc => "ASC", - SortOrder::Desc => "DESC", - } - .into_command() - } -} - -/// `[ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ]` -impl IntoCommand for SortOpts { - fn into_command(self) -> Result, Error> { - ( - NamedOption("BY", self.by), - self.limit, - NonEmpty::optional("get", self.get).map(|NonEmpty(name, patterns)| { - let patterns = patterns.into_iter().map(|pattern| ("GET", pattern)); - NonEmpty(name, patterns.collect()) - }), - self.order, - Flag("ALPHA", self.alpha), - ) - .into_command() - } -} - -impl IntoCommand for HsetexCondition { - fn into_command(self) -> Result, Error> { - match self { - HsetexCondition::Nx => "NX", - HsetexCondition::Xx => "XX", - } - .into_command() - } -} - -impl IntoCommand for HsetexFieldCondition { - fn into_command(self) -> Result, Error> { - match self { - HsetexFieldCondition::Fnx => "FNX", - HsetexFieldCondition::Fxx => "FXX", - } - .into_command() - } -} - -/// `0` to start, failing to resume from `0`, which would restart the scan. -impl IntoCommand for Cursor { - fn into_command(self) -> Result, Error> { - match self { - Cursor::Start => "0".into_command(), - Cursor::Resume(cursor) if cursor == "0" => Err(Error::Client( - "resume requires the cursor a scan returned, use start to begin a scan".to_string(), - )), - Cursor::Resume(cursor) => cursor.into_command(), - } - } -} - -impl IntoCommand for ListSide { - fn into_command(self) -> Result, Error> { - match self { - ListSide::Left => "LEFT", - ListSide::Right => "RIGHT", - } - .into_command() - } -} - -impl IntoCommand for LinsertPosition { - fn into_command(self) -> Result, Error> { - match self { - LinsertPosition::Before => "BEFORE", - LinsertPosition::After => "AFTER", - } - .into_command() - } -} - -/// `start [ end [ BIT ] ]`, BYTE is the default, and left out for servers before it existed. -impl IntoCommand for BitRange { - fn into_command(self) -> Result, Error> { - match self { - BitRange::From(start) => start.into_command(), - BitRange::Bytes((start, end)) => (start, end).into_command(), - BitRange::Bits((start, end)) => (start, end, "BIT").into_command(), - } - } -} - -/// `i1` to `i64`, `u1` to `u63`, other widths fail. -impl IntoCommand for BitfieldEncoding { - fn into_command(self) -> Result, Error> { - match self { - BitfieldEncoding::Signed(width @ 1..=64) => format!("i{width}").into_command(), - BitfieldEncoding::Unsigned(width @ 1..=63) => format!("u{width}").into_command(), - BitfieldEncoding::Signed(width) => Err(Error::Client(format!( - "a signed bitfield is 1 to 64 bits wide, not {width}" +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{words, words_error}; + + #[test] + fn strings() { + assert_eq!(words("GET"), ["GET"]); + assert_eq!(words("key".to_string()), ["key"]); + assert_eq!(words(&"key".to_string()), ["key"]); + } + + #[test] + fn numbers() { + assert_eq!( + words((-1i32, -2i64, 3u8, 4u16, 5u32, 6u64, 7usize)), + ["-1", "-2", "3", "4", "5", "6", "7"] + ); + assert_eq!(words((1.5f64, 2f64, -0.25f64)), ["1.5", "2", "-0.25"]); + } + + #[test] + fn option() { + assert_eq!(words(None::<&str>), Vec::::new()); + assert_eq!(words(Some("value")), ["value"]); + } + + #[test] + fn lists() { + assert_eq!(words(vec!["a", "b"]), ["a", "b"]); + assert_eq!(words(Vec::<&str>::new()), Vec::::new()); + assert_eq!(words(["a", "b"]), ["a", "b"]); + } + + #[test] + fn tuples() { + assert_eq!( + words(("SET", "key", "value", None::<&str>, Some(("EX", 10u64)))), + ["SET", "key", "value", "EX", "10"] + ); + assert_eq!(words(vec![("field", "value")]), ["field", "value"]); + } + + #[test] + fn an_error_fails_the_whole_command() { + let command = ("DEL", NonEmpty::new("keys", Vec::::new())); + assert_eq!(words_error(command), "keys requires at least one item"); + } + + #[test] + fn flag() { + assert_eq!(words(Flag("FORCE", true)), ["FORCE"]); + assert_eq!(words(Flag("FORCE", false)), Vec::::new()); + } + + #[test] + fn named_option() { + assert_eq!(words(NamedOption("COUNT", Some(5i64))), ["COUNT", "5"]); + assert_eq!( + words(NamedOption("COUNT", None::)), + Vec::::new() + ); + } + + #[test] + fn non_empty() { + assert_eq!(words(NonEmpty::new("keys", vec!["a", "b"])), ["a", "b"]); + assert_eq!( + words_error(NonEmpty::new("keys", Vec::::new())), + "keys requires at least one item" + ); + } + + #[test] + fn non_empty_optional() { + assert_eq!( + words(NonEmpty::optional("args", None::>)), + Vec::::new() + ); + assert_eq!(words(NonEmpty::optional("args", Some(vec!["a"]))), ["a"]); + assert_eq!( + words_error(NonEmpty::optional("args", Some(Vec::::new()))), + "args requires at least one item" + ); + } + + #[test] + fn num_keys() { + assert_eq!( + words(NumKeys(NonEmpty::new("keys", vec!["a", "b"]))), + ["2", "a", "b"] + ); + // a pair counts as one + assert_eq!( + words(NumKeys(NonEmpty::new( + "keys", + vec![("k1", "v1"), ("k2", "v2")] ))), - BitfieldEncoding::Unsigned(width) => Err(Error::Client(format!( - "an unsigned bitfield is 1 to 63 bits wide, not {width}" - ))), - } - } -} - -impl IntoCommand for BitfieldOffset { - fn into_command(self) -> Result, Error> { - match self { - BitfieldOffset::Bit(offset) => offset.into_command(), - BitfieldOffset::Field(field) => format!("#{field}").into_command(), - } - } -} - -impl IntoCommand for BitfieldOverflow { - fn into_command(self) -> Result, Error> { - let overflow = match self { - BitfieldOverflow::Wrap => "WRAP", - BitfieldOverflow::Sat => "SAT", - BitfieldOverflow::Fail => "FAIL", - }; - ("OVERFLOW", overflow).into_command() - } -} - -impl IntoCommand for BitfieldGet { - fn into_command(self) -> Result, Error> { - ("GET", self.encoding, self.offset).into_command() - } -} - -impl IntoCommand for BitfieldSet { - fn into_command(self) -> Result, Error> { - ("SET", self.encoding, self.offset, self.value).into_command() - } -} - -impl IntoCommand for BitfieldIncrby { - fn into_command(self) -> Result, Error> { - ("INCRBY", self.encoding, self.offset, self.increment).into_command() - } -} - -/// A write is preceded by its overflow mode, when given. -impl IntoCommand for BitfieldOperation { - fn into_command(self) -> Result, Error> { - match self { - BitfieldOperation::Get(get) => get.into_command(), - BitfieldOperation::Set((overflow, set)) => (overflow, set).into_command(), - BitfieldOperation::Incrby((overflow, incrby)) => (overflow, incrby).into_command(), - } - } -} - -/// A bit, `1` or `0`, e.g. for SETBIT. -pub struct Bit(pub bool); - -impl IntoCommand for Bit { - fn into_command(self) -> Result, Error> { - if self.0 { "1" } else { "0" }.into_command() - } -} - -/// `longitude latitude` -impl IntoCommand for GeoCoordinates { - fn into_command(self) -> Result, Error> { - (self.longitude, self.latitude).into_command() - } -} - -impl IntoCommand for GeoUnit { - fn into_command(self) -> Result, Error> { - match self { - GeoUnit::M => "m", - GeoUnit::Km => "km", - GeoUnit::Ft => "ft", - GeoUnit::Mi => "mi", - } - .into_command() - } -} - -impl IntoCommand for GeoaddCondition { - fn into_command(self) -> Result, Error> { - match self { - GeoaddCondition::Nx => "NX", - GeoaddCondition::Xx => "XX", - } - .into_command() - } -} - -impl IntoCommand for GeosearchFrom { - fn into_command(self) -> Result, Error> { - match self { - GeosearchFrom::Member(member) => ("FROMMEMBER", member).into_command(), - GeosearchFrom::Lonlat(coordinates) => ("FROMLONLAT", coordinates).into_command(), - } - } -} - -/// The center, when the shape has one, then the shape. -impl IntoCommand for GeosearchShape { - fn into_command(self) -> Result, Error> { - match self { - GeosearchShape::Radius(circle) => { - (circle.from, "BYRADIUS", circle.radius, circle.unit).into_command() - } - GeosearchShape::Box(rectangle) => ( - rectangle.from, - "BYBOX", - rectangle.width, - rectangle.height, - rectangle.unit, - ) - .into_command(), - GeosearchShape::Polygon(vertices) => { - ("BYPOLYGON", NumKeys(NonEmpty::new("vertices", vertices))).into_command() - } - } - } -} - -/// `COUNT count [ ANY ]` -impl IntoCommand for GeosearchCount { - fn into_command(self) -> Result, Error> { - ("COUNT", self.count, Flag("ANY", self.any)).into_command() - } -} - -impl IntoCommand for FlushMode { - fn into_command(self) -> Result, Error> { - match self { - FlushMode::Async => "ASYNC", - FlushMode::Sync => "SYNC", - } - .into_command() - } -} - -/// `numkeys [ key ... ] [ arg ... ]`, e.g. for EVAL, `0` without keys. -pub struct KeysAndArgs(pub Option>, pub Option>); - -impl IntoCommand for KeysAndArgs { - fn into_command(self) -> Result, Error> { - let KeysAndArgs(keys, args) = self; - let numkeys = keys.as_ref().map_or(0, Vec::len); - ( - numkeys, - NonEmpty::optional("keys", keys), - NonEmpty::optional("args", args), - ) - .into_command() - } -} - -/// The name of the type, e.g. for SCAN's TYPE. -impl IntoCommand for KeyType { - fn into_command(self) -> Result, Error> { - match self { - KeyType::String => "string".to_string(), - KeyType::List => "list".to_string(), - KeyType::Set => "set".to_string(), - KeyType::Zset => "zset".to_string(), - KeyType::Hash => "hash".to_string(), - KeyType::Stream => "stream".to_string(), - KeyType::Other(name) => name, - } - .into_command() - } -} - -impl IntoCommand for MigrateAuth { - fn into_command(self) -> Result, Error> { - match self { - MigrateAuth::Password(password) => ("AUTH", password).into_command(), - MigrateAuth::UsernamePassword((username, password)) => { - ("AUTH2", username, password).into_command() - } - } - } -} - -impl IntoCommand for BgsaveOperation { - fn into_command(self) -> Result, Error> { - match self { - BgsaveOperation::Schedule => "SCHEDULE", - BgsaveOperation::Cancel => "CANCEL", - } - .into_command() - } -} - -/// `master` rather than `primary`, which servers before Valkey 8.0 do not know. -impl IntoCommand for ClientType { - fn into_command(self) -> Result, Error> { - match self { - ClientType::Normal => "normal", - ClientType::Primary => "master", - ClientType::Replica => "replica", - ClientType::Pubsub => "pubsub", - } - .into_command() - } -} - -impl IntoCommand for ClientPauseMode { - fn into_command(self) -> Result, Error> { - match self { - ClientPauseMode::Write => "WRITE", - ClientPauseMode::All => "ALL", - } - .into_command() - } -} - -impl IntoCommand for ClientUnblockType { - fn into_command(self) -> Result, Error> { - match self { - ClientUnblockType::Timeout => "TIMEOUT", - ClientUnblockType::Error => "ERROR", - } - .into_command() - } -} - -impl IntoCommand for ClusterFailoverMode { - fn into_command(self) -> Result, Error> { - match self { - ClusterFailoverMode::Force => "FORCE", - ClusterFailoverMode::Takeover => "TAKEOVER", - } - .into_command() - } -} - -impl IntoCommand for ClusterResetMode { - fn into_command(self) -> Result, Error> { - match self { - ClusterResetMode::Hard => "HARD", - ClusterResetMode::Soft => "SOFT", - } - .into_command() - } -} - -impl IntoCommand for CommandlogType { - fn into_command(self) -> Result, Error> { - match self { - CommandlogType::Slow => "slow", - CommandlogType::LargeRequest => "large-request", - CommandlogType::LargeReply => "large-reply", - } - .into_command() - } -} - -impl IntoCommand for ShutdownSave { - fn into_command(self) -> Result, Error> { - match self { - ShutdownSave::Nosave => "NOSAVE", - ShutdownSave::Save => "SAVE", - } - .into_command() - } -} - -impl IntoCommand for ClientFilter { - fn into_command(self) -> Result, Error> { - match self { - ClientFilter::Id(ids) => ("ID", NonEmpty::new("id", ids)).into_command(), - ClientFilter::Type(client_type) => ("TYPE", client_type).into_command(), - ClientFilter::User(user) => ("USER", user).into_command(), - ClientFilter::Addr(addr) => ("ADDR", addr).into_command(), - ClientFilter::Laddr(laddr) => ("LADDR", laddr).into_command(), - ClientFilter::Skipme(skip) => { - ("SKIPME", if skip { "YES" } else { "NO" }).into_command() - } - ClientFilter::Maxage(age) => ("MAXAGE", age).into_command(), - ClientFilter::Name(name) => ("NAME", name).into_command(), - ClientFilter::Idle(idle) => ("IDLE", idle).into_command(), - ClientFilter::Flags(flags) => ("FLAGS", flags).into_command(), - ClientFilter::LibName(name) => ("LIB-NAME", name).into_command(), - ClientFilter::LibVer(version) => ("LIB-VER", version).into_command(), - ClientFilter::Db(db) => ("DB", db).into_command(), - ClientFilter::Capa(capability) => ("CAPA", capability).into_command(), - ClientFilter::Ip(ip) => ("IP", ip).into_command(), - ClientFilter::NotId(ids) => ("NOT-ID", NonEmpty::new("not-id", ids)).into_command(), - ClientFilter::NotType(client_type) => ("NOT-TYPE", client_type).into_command(), - ClientFilter::NotUser(user) => ("NOT-USER", user).into_command(), - ClientFilter::NotAddr(addr) => ("NOT-ADDR", addr).into_command(), - ClientFilter::NotLaddr(laddr) => ("NOT-LADDR", laddr).into_command(), - ClientFilter::NotName(name) => ("NOT-NAME", name).into_command(), - ClientFilter::NotFlags(flags) => ("NOT-FLAGS", flags).into_command(), - ClientFilter::NotLibName(name) => ("NOT-LIB-NAME", name).into_command(), - ClientFilter::NotLibVer(version) => ("NOT-LIB-VER", version).into_command(), - ClientFilter::NotDb(db) => ("NOT-DB", db).into_command(), - ClientFilter::NotCapa(capability) => ("NOT-CAPA", capability).into_command(), - ClientFilter::NotIp(ip) => ("NOT-IP", ip).into_command(), - } - } -} - -impl IntoCommand for ClusterSetslotState { - fn into_command(self) -> Result, Error> { - match self { - ClusterSetslotState::Importing(node_id) => ("IMPORTING", node_id).into_command(), - ClusterSetslotState::Migrating(node_id) => ("MIGRATING", node_id).into_command(), - ClusterSetslotState::Node(node_id) => ("NODE", node_id).into_command(), - ClusterSetslotState::Stable => "STABLE".into_command(), - } - } -} - -/// `TO host port [ FORCE ]` -impl IntoCommand for FailoverTarget { - fn into_command(self) -> Result, Error> { - ("TO", self.host, self.port, Flag("FORCE", self.force)).into_command() - } -} - -/// `SLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] NODE node-id [ AUTH username password ]` -impl IntoCommand for ClusterMigrateslotsGroup { - fn into_command(self) -> Result, Error> { - ( - "SLOTSRANGE", - NonEmpty::new("ranges", self.ranges), - "NODE", - self.node_id, - self.auth - .map(|(username, password)| ("AUTH", username, password)), - ) - .into_command() - } -} - -impl IntoCommand for ClientSetinfoAttr { - fn into_command(self) -> Result, Error> { - match self { - ClientSetinfoAttr::LibName(name) => ("LIB-NAME", name).into_command(), - ClientSetinfoAttr::LibVer(version) => ("LIB-VER", version).into_command(), - } - } -} - -impl IntoCommand for ClientTrackingMode { - fn into_command(self) -> Result, Error> { - match self { - ClientTrackingMode::Optin => "OPTIN", - ClientTrackingMode::Optout => "OPTOUT", - } - .into_command() - } -} - -/// `[ REDIRECT client-id ] [ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN | OPTOUT ] [ NOLOOP ]` -impl IntoCommand for ClientTrackingOptions { - fn into_command(self) -> Result, Error> { - // PREFIX before each prefix - let prefixes = self.prefixes.map(|prefixes| { - let prefixes = prefixes.into_iter().map(|prefix| ("PREFIX", prefix)); - prefixes.collect::>() - }); - ( - NamedOption("REDIRECT", self.redirect), - NonEmpty::optional("prefixes", prefixes), - Flag("BCAST", self.bcast), - self.mode, - Flag("NOLOOP", self.noloop), - ) - .into_command() - } -} - -/// `FILTERBY 〈 MODULE module-name | ACLCAT category | PATTERN pattern 〉` -impl IntoCommand for CommandListFilter { - fn into_command(self) -> Result, Error> { - let (filter, value) = match self { - CommandListFilter::Module(name) => ("MODULE", name), - CommandListFilter::Aclcat(category) => ("ACLCAT", category), - CommandListFilter::Pattern(pattern) => ("PATTERN", pattern), - }; - ("FILTERBY", filter, value).into_command() - } -} - -/// `ON` or `OFF`, e.g. for CLIENT NO-TOUCH. -pub struct OnOff(pub bool); - -impl IntoCommand for OnOff { - fn into_command(self) -> Result, Error> { - if self.0 { "ON" } else { "OFF" }.into_command() + ["2", "k1", "v1", "k2", "v2"] + ); + assert_eq!( + words_error(NumKeys(NonEmpty::new("keys", Vec::::new()))), + "keys requires at least one item" + ); } } diff --git a/crates/componentized-valkey/src/commands/admin.rs b/crates/componentized-valkey/src/commands/admin.rs new file mode 100644 index 0000000..69f6518 --- /dev/null +++ b/crates/componentized-valkey/src/commands/admin.rs @@ -0,0 +1,1553 @@ +//! The admin commands: ACL, CLIENT, CLUSTER, COMMANDLOG, FAILOVER, LATENCY, MODULE, ROLE, SHUTDOWN +//! and the like. + +use crate::command::{Flag, IntoCommand, NonEmpty}; +use crate::types::{ + AclChannelRule, AclCommandRule, AclDbRule, AclDryrunResult, AclKeyRule, AclListEntry, + AclLogEntry, AclRule, AclSelectorRule, AclUserRule, BgsaveOperation, ClientFilter, + ClientPauseMode, ClientType, ClientUnblockType, ClusterBumpepochResult, ClusterFailoverMode, + ClusterGetslotmigrationsEntry, ClusterGetslotmigrationsOperation, ClusterMigrateslotsGroup, + ClusterResetMode, ClusterSetslotState, CommandlogEntry, CommandlogType, Error, FailoverTarget, + Integer, LatencyHistogramEntry, LatencyLatestEntry, ModuleListEntry, RespValue, RolePrimary, + RolePrimaryReplica, RoleReplica, RoleReply, ShutdownSave, +}; +use crate::value::{Fields, FlatPairs, FromValue, items, parse_string}; + +use wit_bindgen::rt::Map; + +impl IntoCommand for AclRule { + fn into_command(self) -> Result, Error> { + match self { + AclRule::Keys(rule) => rule.into_command(), + AclRule::Channel(rule) => rule.into_command(), + AclRule::Command(rule) => rule.into_command(), + AclRule::Db(rule) => rule.into_command(), + AclRule::User(rule) => rule.into_command(), + AclRule::Other(rule) => rule.into_command(), + } + } +} + +impl IntoCommand for AclSelectorRule { + fn into_command(self) -> Result, Error> { + match self { + AclSelectorRule::Keys(rule) => rule.into_command(), + AclSelectorRule::Channel(rule) => rule.into_command(), + AclSelectorRule::Command(rule) => rule.into_command(), + AclSelectorRule::Db(rule) => rule.into_command(), + AclSelectorRule::Other(rule) => rule.into_command(), + } + } +} + +impl IntoCommand for AclKeyRule { + fn into_command(self) -> Result, Error> { + Ok(vec![match self { + AclKeyRule::Add(pattern) => format!("~{pattern}"), + AclKeyRule::AddRead(pattern) => format!("%R~{pattern}"), + AclKeyRule::AddWrite(pattern) => format!("%W~{pattern}"), + AclKeyRule::AddReadWrite(pattern) => format!("%RW~{pattern}"), + AclKeyRule::All => "allkeys".to_string(), + AclKeyRule::Reset => "resetkeys".to_string(), + }]) + } +} + +impl IntoCommand for AclChannelRule { + fn into_command(self) -> Result, Error> { + Ok(vec![match self { + AclChannelRule::Add(pattern) => format!("&{pattern}"), + AclChannelRule::All => "allchannels".to_string(), + AclChannelRule::Reset => "resetchannels".to_string(), + }]) + } +} + +impl IntoCommand for AclCommandRule { + fn into_command(self) -> Result, Error> { + Ok(vec![match self { + AclCommandRule::Add(command) => format!("+{command}"), + AclCommandRule::AddCategory(category) => format!("+@{category}"), + AclCommandRule::AddFirstArg((command, arg)) => format!("+{command}|{arg}"), + AclCommandRule::All => "allcommands".to_string(), + AclCommandRule::Remove(command) => format!("-{command}"), + AclCommandRule::RemoveCategory(category) => format!("-@{category}"), + AclCommandRule::Reset => "nocommands".to_string(), + }]) + } +} + +/// `db=` requires at least one database. +impl IntoCommand for AclDbRule { + fn into_command(self) -> Result, Error> { + Ok(vec![match self { + AclDbRule::Db(dbs) if dbs.is_empty() => { + return Err(Error::Client("db requires at least one item".to_string())); + } + AclDbRule::Db(dbs) => format!( + "db={}", + dbs.iter() + .map(|db| db.to_string()) + .collect::>() + .join(",") + ), + AclDbRule::All => "alldbs".to_string(), + AclDbRule::Reset => "resetdbs".to_string(), + }]) + } +} + +impl IntoCommand for AclUserRule { + fn into_command(self) -> Result, Error> { + match self { + AclUserRule::On => "on".into_command(), + AclUserRule::Off => "off".into_command(), + AclUserRule::Nopass => "nopass".into_command(), + AclUserRule::SanitizePayload => "sanitize-payload".into_command(), + AclUserRule::SkipSanitizePayload => "skip-sanitize-payload".into_command(), + AclUserRule::Password(password) => format!(">{password}").into_command(), + AclUserRule::HashedPassword(hash) => format!("#{hash}").into_command(), + AclUserRule::RemovePassword(password) => format!("<{password}").into_command(), + AclUserRule::RemoveHashedPassword(hash) => format!("!{hash}").into_command(), + AclUserRule::Selector(rules) => ("(", rules, ")").into_command(), + AclUserRule::Clearselectors => "clearselectors".into_command(), + AclUserRule::Reset => "reset".into_command(), + } + } +} + +/// `OK` when the user may run the command, otherwise why not. +impl FromValue for AclDryrunResult { + fn from_value(value: RespValue) -> Result { + match value { + RespValue::String(reply) if reply == "OK" => Ok(AclDryrunResult::Allowed), + value => String::from_value(value).map(AclDryrunResult::Denied), + } + } +} + +/// The rules of ACL GETUSER: the flags, the hashed passwords, the root selector's rules, then the +/// selectors. +pub struct AclUserRules(pub Vec); + +impl FromValue for AclUserRules { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + let flags: Vec = fields.required("flags")?; + let passwords: Vec = fields.required("passwords")?; + let mut rules: Vec = flags.iter().map(|flag| rule(flag)).collect(); + rules.extend( + passwords + .into_iter() + .map(|hash| AclRule::User(AclUserRule::HashedPassword(hash))), + ); + rules.extend(selector_rules(&mut fields)?.into_iter().map(root_rule)); + let selectors: Vec = fields.required("selectors")?; + for mut selector in selectors { + let selector = selector_rules(&mut selector)?; + rules.push(AclRule::User(AclUserRule::Selector(selector))); + } + Ok(AclUserRules(rules)) + } +} + +/// The rules of a selector, from its `keys`, `channels`, `databases` and `commands` fields, each +/// rules separated by spaces. +/// +/// The channels are preceded by `resetchannels`, as ACL LIST writes them, so the server's +/// `acl-pubsub-default` does not add to them. +fn selector_rules(fields: &mut Fields) -> Result, Error> { + let keys: String = fields.required("keys")?; + let channels: String = fields.required("channels")?; + // since Valkey 9.1 + let databases: Option = fields.optional("databases")?; + let commands: String = fields.required("commands")?; + let mut rules: Vec = keys.split_whitespace().map(key_rule).collect(); + rules.push(AclSelectorRule::Channel(AclChannelRule::Reset)); + rules.extend(channels.split_whitespace().map(channel_rule)); + rules.extend( + databases + .iter() + .flat_map(|databases| databases.split_whitespace().map(db_rule)), + ); + rules.extend(commands.split_whitespace().map(command_rule)); + Ok(rules) +} + +fn key_rule(rule: &str) -> AclSelectorRule { + let key_rule = match rule { + "allkeys" => AclKeyRule::All, + "resetkeys" => AclKeyRule::Reset, + _ => { + if let Some(pattern) = rule.strip_prefix("%RW~") { + AclKeyRule::AddReadWrite(pattern.to_string()) + } else if let Some(pattern) = rule.strip_prefix("%R~") { + AclKeyRule::AddRead(pattern.to_string()) + } else if let Some(pattern) = rule.strip_prefix("%W~") { + AclKeyRule::AddWrite(pattern.to_string()) + } else if let Some(pattern) = rule.strip_prefix('~') { + AclKeyRule::Add(pattern.to_string()) + } else { + return AclSelectorRule::Other(rule.to_string()); + } + } + }; + AclSelectorRule::Keys(key_rule) +} + +fn channel_rule(rule: &str) -> AclSelectorRule { + let channel_rule = match rule { + "allchannels" => AclChannelRule::All, + "resetchannels" => AclChannelRule::Reset, + _ => match rule.strip_prefix('&') { + Some(pattern) => AclChannelRule::Add(pattern.to_string()), + None => return AclSelectorRule::Other(rule.to_string()), + }, + }; + AclSelectorRule::Channel(channel_rule) +} + +fn db_rule(rule: &str) -> AclSelectorRule { + let db_rule = match rule { + "alldbs" => AclDbRule::All, + "resetdbs" => AclDbRule::Reset, + _ => { + let dbs = rule + .strip_prefix("db=") + .map(|dbs| dbs.split(',').map(str::parse).collect::>()); + match dbs { + Some(Ok(dbs)) => AclDbRule::Db(dbs), + _ => return AclSelectorRule::Other(rule.to_string()), + } + } + }; + AclSelectorRule::Db(db_rule) +} + +/// `+config|get` is a subcommand, or a first argument, either way it is sent back as is. +fn command_rule(rule: &str) -> AclSelectorRule { + let command_rule = match rule { + "allcommands" | "+@all" => AclCommandRule::All, + "nocommands" | "-@all" => AclCommandRule::Reset, + _ => { + if let Some(category) = rule.strip_prefix("+@") { + AclCommandRule::AddCategory(category.to_string()) + } else if let Some(category) = rule.strip_prefix("-@") { + AclCommandRule::RemoveCategory(category.to_string()) + } else if let Some(command) = rule.strip_prefix('+') { + AclCommandRule::Add(command.to_string()) + } else if let Some(command) = rule.strip_prefix('-') { + AclCommandRule::Remove(command.to_string()) + } else { + return AclSelectorRule::Other(rule.to_string()); + } + } + }; + AclSelectorRule::Command(command_rule) +} + +/// The lines of ACL LIST, each `user ...`, with selectors as `( ...)`. +pub struct AclList(pub Vec); + +impl FromValue for AclList { + fn from_value(value: RespValue) -> Result { + let lines: Vec = FromValue::from_value(value)?; + lines + .iter() + .map(|line| acl_list_entry(line)) + .collect::>() + .map(AclList) + } +} + +fn acl_list_entry(line: &str) -> Result { + let invalid = || Error::Client(format!("unexpected ACL LIST line: {line:?}")); + let mut words = line + .strip_prefix("user ") + .ok_or_else(invalid)? + .split_whitespace(); + let username = words.next().ok_or_else(invalid)?.to_string(); + let mut rules = vec![]; + while let Some(word) = words.next() { + if !word.starts_with('(') { + rules.push(rule(word)); + continue; + } + // a selector, its rules up to the word that closes it + let mut selector = vec![word]; + while !selector.last().is_some_and(|word| word.ends_with(')')) { + selector.push(words.next().ok_or_else(invalid)?); + } + let selector = selector.join(" "); + let selector = selector + .strip_prefix('(') + .and_then(|selector| selector.strip_suffix(')')) + .ok_or_else(invalid)?; + let selector = selector.split_whitespace().map(selector_rule).collect(); + rules.push(AclRule::User(AclUserRule::Selector(selector))); + } + Ok(AclListEntry { username, rules }) +} + +/// A rule of the user or its root selector, e.g. `on`, `#` or `~app:*`. +fn rule(word: &str) -> AclRule { + let user_rule = match word { + "on" => AclUserRule::On, + "off" => AclUserRule::Off, + "nopass" => AclUserRule::Nopass, + "sanitize-payload" => AclUserRule::SanitizePayload, + "skip-sanitize-payload" => AclUserRule::SkipSanitizePayload, + "clearselectors" => AclUserRule::Clearselectors, + "reset" => AclUserRule::Reset, + _ => { + if let Some(hash) = word.strip_prefix('#') { + AclUserRule::HashedPassword(hash.to_string()) + } else if let Some(hash) = word.strip_prefix('!') { + AclUserRule::RemoveHashedPassword(hash.to_string()) + } else if let Some(password) = word.strip_prefix('>') { + AclUserRule::Password(password.to_string()) + } else if let Some(password) = word.strip_prefix('<') { + AclUserRule::RemovePassword(password.to_string()) + } else { + return root_rule(selector_rule(word)); + } + } + }; + AclRule::User(user_rule) +} + +/// A rule of a selector, e.g. `~app:*`, `&news:*`, `db=0` or `+get`. +fn selector_rule(word: &str) -> AclSelectorRule { + match word { + "allkeys" | "resetkeys" => key_rule(word), + "allchannels" | "resetchannels" => channel_rule(word), + "alldbs" | "resetdbs" => db_rule(word), + "allcommands" | "nocommands" => command_rule(word), + _ if word.starts_with('~') || word.starts_with('%') => key_rule(word), + _ if word.starts_with('&') => channel_rule(word), + _ if word.starts_with("db=") => db_rule(word), + _ if word.starts_with('+') || word.starts_with('-') => command_rule(word), + _ => AclSelectorRule::Other(word.to_string()), + } +} + +/// A rule of the root selector. +fn root_rule(rule: AclSelectorRule) -> AclRule { + match rule { + AclSelectorRule::Keys(rule) => AclRule::Keys(rule), + AclSelectorRule::Channel(rule) => AclRule::Channel(rule), + AclSelectorRule::Command(rule) => AclRule::Command(rule), + AclSelectorRule::Db(rule) => AclRule::Db(rule), + AclSelectorRule::Other(rule) => AclRule::Other(rule), + } +} + +impl FromValue for AclLogEntry { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + count: fields.required("count")?, + reason: fields.required("reason")?, + context: fields.required("context")?, + object: fields.required("object")?, + username: fields.required("username")?, + age_seconds: fields.required("age-seconds")?, + client_info: fields.required("client-info")?, + // since Valkey 7.2 + entry_id: fields.optional("entry-id")?, + timestamp_created: fields.optional("timestamp-created")?, + timestamp_last_updated: fields.optional("timestamp-last-updated")?, + }) + } +} + +impl IntoCommand for BgsaveOperation { + fn into_command(self) -> Result, Error> { + match self { + BgsaveOperation::Schedule => "SCHEDULE", + BgsaveOperation::Cancel => "CANCEL", + } + .into_command() + } +} + +/// `master` rather than `primary`, which servers before Valkey 8.0 do not know. +impl IntoCommand for ClientType { + fn into_command(self) -> Result, Error> { + match self { + ClientType::Normal => "normal", + ClientType::Primary => "master", + ClientType::Replica => "replica", + ClientType::Pubsub => "pubsub", + } + .into_command() + } +} + +impl IntoCommand for ClientFilter { + fn into_command(self) -> Result, Error> { + match self { + ClientFilter::Id(ids) => ("ID", NonEmpty::new("id", ids)).into_command(), + ClientFilter::Type(client_type) => ("TYPE", client_type).into_command(), + ClientFilter::User(user) => ("USER", user).into_command(), + ClientFilter::Addr(addr) => ("ADDR", addr).into_command(), + ClientFilter::Laddr(laddr) => ("LADDR", laddr).into_command(), + ClientFilter::Skipme(skip) => { + ("SKIPME", if skip { "YES" } else { "NO" }).into_command() + } + ClientFilter::Maxage(age) => ("MAXAGE", age).into_command(), + ClientFilter::Name(name) => ("NAME", name).into_command(), + ClientFilter::Idle(idle) => ("IDLE", idle).into_command(), + ClientFilter::Flags(flags) => ("FLAGS", flags).into_command(), + ClientFilter::LibName(name) => ("LIB-NAME", name).into_command(), + ClientFilter::LibVer(version) => ("LIB-VER", version).into_command(), + ClientFilter::Db(db) => ("DB", db).into_command(), + ClientFilter::Capa(capability) => ("CAPA", capability).into_command(), + ClientFilter::Ip(ip) => ("IP", ip).into_command(), + ClientFilter::NotId(ids) => ("NOT-ID", NonEmpty::new("not-id", ids)).into_command(), + ClientFilter::NotType(client_type) => ("NOT-TYPE", client_type).into_command(), + ClientFilter::NotUser(user) => ("NOT-USER", user).into_command(), + ClientFilter::NotAddr(addr) => ("NOT-ADDR", addr).into_command(), + ClientFilter::NotLaddr(laddr) => ("NOT-LADDR", laddr).into_command(), + ClientFilter::NotName(name) => ("NOT-NAME", name).into_command(), + ClientFilter::NotFlags(flags) => ("NOT-FLAGS", flags).into_command(), + ClientFilter::NotLibName(name) => ("NOT-LIB-NAME", name).into_command(), + ClientFilter::NotLibVer(version) => ("NOT-LIB-VER", version).into_command(), + ClientFilter::NotDb(db) => ("NOT-DB", db).into_command(), + ClientFilter::NotCapa(capability) => ("NOT-CAPA", capability).into_command(), + ClientFilter::NotIp(ip) => ("NOT-IP", ip).into_command(), + } + } +} + +/// The lines of CLIENT LIST, each of the fields of a connection, `name=value` separated by spaces. +#[derive(Debug)] +pub struct ClientList(pub Vec>); + +impl FromValue for ClientList { + fn from_value(value: RespValue) -> Result { + let text = String::from_value(value)?; + let clients = text + .lines() + .filter(|line| !line.is_empty()) + .map(|line| { + line.split(' ') + .filter_map(|field| field.split_once('=')) + .map(|(name, value)| (name.to_string(), value.to_string())) + .collect() + }) + .collect(); + Ok(ClientList(clients)) + } +} + +impl IntoCommand for ClientPauseMode { + fn into_command(self) -> Result, Error> { + match self { + ClientPauseMode::Write => "WRITE", + ClientPauseMode::All => "ALL", + } + .into_command() + } +} + +impl IntoCommand for ClientUnblockType { + fn into_command(self) -> Result, Error> { + match self { + ClientUnblockType::Timeout => "TIMEOUT", + ClientUnblockType::Error => "ERROR", + } + .into_command() + } +} + +/// `BUMPED epoch` or `STILL epoch`. +impl FromValue for ClusterBumpepochResult { + fn from_value(value: RespValue) -> Result { + let reply = String::from_value(value)?; + let invalid = || Error::Client(format!("unexpected reply: {reply:?}")); + let (outcome, epoch) = reply.split_once(' ').ok_or_else(invalid)?; + let epoch = epoch.parse().map_err(|_| invalid())?; + match outcome { + "BUMPED" => Ok(ClusterBumpepochResult::Bumped(epoch)), + "STILL" => Ok(ClusterBumpepochResult::Still(epoch)), + _ => Err(invalid()), + } + } +} + +impl IntoCommand for ClusterFailoverMode { + fn into_command(self) -> Result, Error> { + match self { + ClusterFailoverMode::Force => "FORCE", + ClusterFailoverMode::Takeover => "TAKEOVER", + } + .into_command() + } +} + +impl FromValue for ClusterGetslotmigrationsOperation { + fn from_value(value: RespValue) -> Result { + match String::from_value(value)?.as_str() { + "IMPORT" => Ok(ClusterGetslotmigrationsOperation::Import), + "EXPORT" => Ok(ClusterGetslotmigrationsOperation::Export), + operation => Err(Error::Client(format!( + "unexpected operation: {operation:?}" + ))), + } + } +} + +/// Ranges of slots, `start-end start-end ...`. +pub struct SlotRanges(pub Vec<(u16, u16)>); + +impl FromValue for SlotRanges { + fn from_value(value: RespValue) -> Result { + let ranges = String::from_value(value)?; + let invalid = || Error::Client(format!("unexpected slot ranges: {ranges:?}")); + ranges + .split_whitespace() + .map(|range| { + let (start, end) = range.split_once('-').ok_or_else(invalid)?; + Ok(( + start.parse().map_err(|_| invalid())?, + end.parse().map_err(|_| invalid())?, + )) + }) + .collect::>() + .map(SlotRanges) + } +} + +impl FromValue for ClusterGetslotmigrationsEntry { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + let slot_ranges: SlotRanges = fields.required("slot_ranges")?; + Ok(Self { + link_name: fields.required("link_name")?, + operation: fields.required("operation")?, + slot_ranges: slot_ranges.0, + target_node: fields.required("target_node")?, + source_node: fields.required("source_node")?, + create_time: fields.required("create_time")?, + last_update_time: fields.required("last_update_time")?, + last_ack_time: fields.required("last_ack_time")?, + state: fields.required("state")?, + message: fields.required("message")?, + cow_size: fields.required("cow_size")?, + }) + } +} + +/// `SLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] NODE node-id [ AUTH username password ]` +impl IntoCommand for ClusterMigrateslotsGroup { + fn into_command(self) -> Result, Error> { + ( + "SLOTSRANGE", + NonEmpty::new("ranges", self.ranges), + "NODE", + self.node_id, + self.auth + .map(|(username, password)| ("AUTH", username, password)), + ) + .into_command() + } +} + +impl IntoCommand for ClusterResetMode { + fn into_command(self) -> Result, Error> { + match self { + ClusterResetMode::Hard => "HARD", + ClusterResetMode::Soft => "SOFT", + } + .into_command() + } +} + +impl IntoCommand for ClusterSetslotState { + fn into_command(self) -> Result, Error> { + match self { + ClusterSetslotState::Importing(node_id) => ("IMPORTING", node_id).into_command(), + ClusterSetslotState::Migrating(node_id) => ("MIGRATING", node_id).into_command(), + ClusterSetslotState::Node(node_id) => ("NODE", node_id).into_command(), + ClusterSetslotState::Stable => "STABLE".into_command(), + } + } +} + +impl IntoCommand for CommandlogType { + fn into_command(self) -> Result, Error> { + match self { + CommandlogType::Slow => "slow", + CommandlogType::LargeRequest => "large-request", + CommandlogType::LargeReply => "large-reply", + } + .into_command() + } +} + +/// `[id, timestamp, value, [argument ...], client-address, client-name]`, of SLOWLOG GET and +/// COMMANDLOG GET. +impl FromValue for CommandlogEntry { + fn from_value(value: RespValue) -> Result { + match <[RespValue; 6]>::try_from(items(value)?) { + Ok([id, timestamp, value, arguments, client_address, client_name]) => Ok(Self { + id: FromValue::from_value(id)?, + timestamp: FromValue::from_value(timestamp)?, + value: FromValue::from_value(value)?, + arguments: FromValue::from_value(arguments)?, + client_address: FromValue::from_value(client_address)?, + client_name: FromValue::from_value(client_name)?, + }), + Err(items) => Err(Error::Client(format!( + "unexpected number of items, expected 6: {}", + items.len() + ))), + } + } +} + +/// `TO host port [ FORCE ]` +impl IntoCommand for FailoverTarget { + fn into_command(self) -> Result, Error> { + ("TO", self.host, self.port, Flag("FORCE", self.force)).into_command() + } +} + +impl FromValue for LatencyHistogramEntry { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + // a map of each bucket to its cumulative count + let histogram: FlatPairs = fields.required("histogram_usec")?; + Ok(Self { + calls: fields.required("calls")?, + histogram_usec: histogram.0, + }) + } +} + +/// `[event, timestamp, latest, max, sum, count]` +impl FromValue for LatencyLatestEntry { + fn from_value(value: RespValue) -> Result { + match <[RespValue; 6]>::try_from(items(value)?) { + Ok([event, timestamp, latest, max, sum, count]) => Ok(Self { + event: FromValue::from_value(event)?, + timestamp: FromValue::from_value(timestamp)?, + latest: FromValue::from_value(latest)?, + max: FromValue::from_value(max)?, + sum: FromValue::from_value(sum)?, + count: FromValue::from_value(count)?, + }), + Err(items) => Err(Error::Client(format!( + "unexpected number of items, expected 6: {}", + items.len() + ))), + } + } +} + +impl FromValue for ModuleListEntry { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + name: fields.required("name")?, + ver: fields.required("ver")?, + path: fields.required("path")?, + args: fields.required("args")?, + }) + } +} + +/// `[ip, port, offset]`, the port and offset are strings. +impl FromValue for RolePrimaryReplica { + fn from_value(value: RespValue) -> Result { + let (ip, port, offset) = FromValue::from_value(value)?; + Ok(Self { + ip, + port: parse_string(port)?, + offset: parse_string(offset)?, + }) + } +} + +/// `["master", offset, [replica ...]]`, `["slave", host, port, state, offset]` or +/// `["sentinel", [primary ...]]`. +impl FromValue for RoleReply { + fn from_value(value: RespValue) -> Result { + let mut items = items(value)?.into_iter(); + let role = String::from_value(items.next().unwrap_or(RespValue::Null))?; + let mut next = || { + items + .next() + .ok_or_else(|| Error::Client(format!("too few items for the role {role:?}"))) + }; + match role.as_str() { + "master" => Ok(RoleReply::Primary(RolePrimary { + offset: FromValue::from_value(next()?)?, + replicas: FromValue::from_value(next()?)?, + })), + "slave" => Ok(RoleReply::Replica(RoleReplica { + primary_host: FromValue::from_value(next()?)?, + primary_port: u64::from_value(next()?)? + .try_into() + .map_err(|_| Error::Client("unexpected port".to_string()))?, + state: FromValue::from_value(next()?)?, + offset: FromValue::from_value(next()?)?, + })), + "sentinel" => Ok(RoleReply::Sentinel(FromValue::from_value(next()?)?)), + role => Err(Error::Client(format!("unexpected role: {role:?}"))), + } + } +} + +impl IntoCommand for ShutdownSave { + fn into_command(self) -> Result, Error> { + match self { + ShutdownSave::Nosave => "NOSAVE", + ShutdownSave::Save => "SAVE", + } + .into_command() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{ + array, bulk, bulks, double, fields, from, from_error, int, ok, simple, words, words_error, + }; + use crate::types::{ + DurationMilliseconds, DurationSeconds, UnixTimeMilliseconds, UnixTimeSeconds, + }; + + fn key(rule: AclKeyRule) -> AclSelectorRule { + AclSelectorRule::Keys(rule) + } + + fn channel(rule: AclChannelRule) -> AclSelectorRule { + AclSelectorRule::Channel(rule) + } + + fn command(rule: AclCommandRule) -> AclSelectorRule { + AclSelectorRule::Command(rule) + } + + fn user(rule: AclUserRule) -> AclRule { + AclRule::User(rule) + } + + #[test] + fn acl_key_rules() { + assert_eq!( + words(vec![ + AclKeyRule::Add("a:*".to_string()), + AclKeyRule::AddRead("r:*".to_string()), + AclKeyRule::AddWrite("w:*".to_string()), + AclKeyRule::AddReadWrite("rw:*".to_string()), + AclKeyRule::All, + AclKeyRule::Reset, + ]), + [ + "~a:*", + "%R~r:*", + "%W~w:*", + "%RW~rw:*", + "allkeys", + "resetkeys" + ] + ); + } + + #[test] + fn acl_channel_rules() { + assert_eq!( + words(vec![ + AclChannelRule::Add("news:*".to_string()), + AclChannelRule::All, + AclChannelRule::Reset, + ]), + ["&news:*", "allchannels", "resetchannels"] + ); + } + + #[test] + fn acl_command_rules() { + assert_eq!( + words(vec![ + AclCommandRule::Add("get".to_string()), + AclCommandRule::AddCategory("read".to_string()), + AclCommandRule::AddFirstArg(("config".to_string(), "get".to_string())), + AclCommandRule::All, + AclCommandRule::Remove("del".to_string()), + AclCommandRule::RemoveCategory("dangerous".to_string()), + AclCommandRule::Reset, + ]), + [ + "+get", + "+@read", + "+config|get", + "allcommands", + "-del", + "-@dangerous", + "nocommands" + ] + ); + } + + #[test] + fn acl_db_rules() { + assert_eq!( + words(vec![ + AclDbRule::Db(vec![0, 2]), + AclDbRule::All, + AclDbRule::Reset + ]), + ["db=0,2", "alldbs", "resetdbs"] + ); + assert_eq!( + words_error(AclDbRule::Db(vec![])), + "db requires at least one item" + ); + } + + #[test] + fn acl_user_rules() { + assert_eq!( + words(vec![ + AclUserRule::On, + AclUserRule::Off, + AclUserRule::Nopass, + AclUserRule::SanitizePayload, + AclUserRule::SkipSanitizePayload, + AclUserRule::Password("secret".to_string()), + AclUserRule::HashedPassword("abc".to_string()), + AclUserRule::RemovePassword("old".to_string()), + AclUserRule::RemoveHashedPassword("def".to_string()), + AclUserRule::Clearselectors, + AclUserRule::Reset, + ]), + [ + "on", + "off", + "nopass", + "sanitize-payload", + "skip-sanitize-payload", + ">secret", + "#abc", + "(ok()), AclDryrunResult::Allowed); + assert_eq!( + from::(bulk("User app has no permissions to run the 'set' command")), + AclDryrunResult::Denied( + "User app has no permissions to run the 'set' command".to_string() + ) + ); + } + + #[test] + fn acl_getuser() { + let value = fields(vec![ + ("flags", bulks(&["on", "sanitize-payload"])), + ("passwords", bulks(&["abc"])), + ("commands", bulk("+@read -debug")), + ("keys", bulk("~app:* %R~ro:*")), + ("channels", bulk("&news:*")), + ("databases", bulk("db=0,1")), + ( + "selectors", + array(vec![fields(vec![ + ("commands", bulk("+get")), + ("keys", bulk("%W~w:*")), + ("channels", bulk("")), + ])]), + ), + ]); + assert_eq!( + from::(value).0, + [ + user(AclUserRule::On), + user(AclUserRule::SanitizePayload), + user(AclUserRule::HashedPassword("abc".to_string())), + AclRule::Keys(AclKeyRule::Add("app:*".to_string())), + AclRule::Keys(AclKeyRule::AddRead("ro:*".to_string())), + AclRule::Channel(AclChannelRule::Reset), + AclRule::Channel(AclChannelRule::Add("news:*".to_string())), + AclRule::Db(AclDbRule::Db(vec![0, 1])), + AclRule::Command(AclCommandRule::AddCategory("read".to_string())), + AclRule::Command(AclCommandRule::Remove("debug".to_string())), + user(AclUserRule::Selector(vec![ + key(AclKeyRule::AddWrite("w:*".to_string())), + channel(AclChannelRule::Reset), + command(AclCommandRule::Add("get".to_string())), + ])), + ] + ); + } + + #[test] + fn acl_getuser_missing_field() { + let value = fields(vec![("flags", bulks(&["on"]))]); + assert_eq!( + from_error::(value), + "missing field: passwords" + ); + } + + #[test] + fn acl_list() { + let value = bulks(&[ + "user default on nopass sanitize-payload ~* &* +@all", + "user app off #abc resetchannels db=3 -@all +get (%R~x resetchannels +get) (~y +set)", + ]); + let entries = from::(value).0; + assert_eq!( + entries, + [ + AclListEntry { + username: "default".to_string(), + rules: vec![ + user(AclUserRule::On), + user(AclUserRule::Nopass), + user(AclUserRule::SanitizePayload), + AclRule::Keys(AclKeyRule::Add("*".to_string())), + AclRule::Channel(AclChannelRule::Add("*".to_string())), + AclRule::Command(AclCommandRule::All), + ], + }, + AclListEntry { + username: "app".to_string(), + rules: vec![ + user(AclUserRule::Off), + user(AclUserRule::HashedPassword("abc".to_string())), + AclRule::Channel(AclChannelRule::Reset), + AclRule::Db(AclDbRule::Db(vec![3])), + AclRule::Command(AclCommandRule::Reset), + AclRule::Command(AclCommandRule::Add("get".to_string())), + user(AclUserRule::Selector(vec![ + key(AclKeyRule::AddRead("x".to_string())), + channel(AclChannelRule::Reset), + command(AclCommandRule::Add("get".to_string())), + ])), + user(AclUserRule::Selector(vec![ + key(AclKeyRule::Add("y".to_string())), + command(AclCommandRule::Add("set".to_string())), + ])), + ], + }, + ] + ); + } + + #[test] + fn acl_list_unknown_rules() { + let entries = from::(bulks(&["user app on ?newer db=x"])).0; + assert_eq!( + entries[0].rules, + [ + user(AclUserRule::On), + AclRule::Other("?newer".to_string()), + AclRule::Other("db=x".to_string()), + ] + ); + } + + #[test] + fn acl_list_invalid() { + assert_eq!( + from_error::(bulks(&["bogus"])), + r#"unexpected ACL LIST line: "bogus""# + ); + assert_eq!( + from_error::(bulks(&["user app (~x"])), + r#"unexpected ACL LIST line: "user app (~x""# + ); + } + + #[test] + fn acl_list_round_trip() { + let line = + "user app on #abc ~app:* resetchannels &news:* -@all +get (%R~x resetchannels +get)"; + let entries = from::(bulks(&[line])).0; + let rules = entries.into_iter().next().unwrap().rules; + // selectors are sent as words between parentheses + assert_eq!( + words(rules).join(" "), + "on #abc ~app:* resetchannels &news:* nocommands +get ( %R~x resetchannels +get )" + ); + } + + #[test] + fn acl_log_entry() { + let mut entry = vec![ + ("count", int(1)), + ("reason", bulk("command")), + ("context", bulk("toplevel")), + ("object", bulk("get")), + ("username", bulk("app")), + ("age-seconds", double(1.5)), + ("client-info", bulk("id=3 addr=127.0.0.1:5000")), + ]; + let expected = AclLogEntry { + count: 1, + reason: "command".to_string(), + context: "toplevel".to_string(), + object: "get".to_string(), + username: "app".to_string(), + age_seconds: 1.5, + client_info: "id=3 addr=127.0.0.1:5000".to_string(), + entry_id: None, + timestamp_created: None, + timestamp_last_updated: None, + }; + // before Valkey 7.2 + assert_eq!(from::(fields(entry.clone())), expected); + entry.extend([ + ("entry-id", int(7)), + ("timestamp-created", int(1_700_000_000_000)), + ("timestamp-last-updated", int(1_700_000_000_500)), + ]); + assert_eq!( + from::(fields(entry)), + AclLogEntry { + entry_id: Some(7), + timestamp_created: Some(UnixTimeMilliseconds { + pxat: 1_700_000_000_000 + }), + timestamp_last_updated: Some(UnixTimeMilliseconds { + pxat: 1_700_000_000_500 + }), + ..expected + } + ); + } + + #[test] + fn keywords() { + assert_eq!( + words([BgsaveOperation::Schedule, BgsaveOperation::Cancel]), + ["SCHEDULE", "CANCEL"] + ); + assert_eq!( + words([ + ClientType::Normal, + ClientType::Primary, + ClientType::Replica, + ClientType::Pubsub + ]), + ["normal", "master", "replica", "pubsub"] + ); + assert_eq!( + words([ClientPauseMode::Write, ClientPauseMode::All]), + ["WRITE", "ALL"] + ); + assert_eq!( + words([ClientUnblockType::Timeout, ClientUnblockType::Error]), + ["TIMEOUT", "ERROR"] + ); + assert_eq!( + words([ClusterFailoverMode::Force, ClusterFailoverMode::Takeover]), + ["FORCE", "TAKEOVER"] + ); + assert_eq!( + words([ClusterResetMode::Hard, ClusterResetMode::Soft]), + ["HARD", "SOFT"] + ); + assert_eq!( + words([ + CommandlogType::Slow, + CommandlogType::LargeRequest, + CommandlogType::LargeReply + ]), + ["slow", "large-request", "large-reply"] + ); + assert_eq!( + words([ShutdownSave::Nosave, ShutdownSave::Save]), + ["NOSAVE", "SAVE"] + ); + } + + #[test] + fn client_filter() { + let filters = vec![ + ClientFilter::Id(vec![1, 2]), + ClientFilter::Type(ClientType::Primary), + ClientFilter::User("app".to_string()), + ClientFilter::Addr("127.0.0.1:5000".to_string()), + ClientFilter::Laddr("127.0.0.1:6379".to_string()), + ClientFilter::Skipme(false), + ClientFilter::Maxage(DurationSeconds { ex: 60 }), + ClientFilter::Name("worker".to_string()), + ClientFilter::Idle(DurationSeconds { ex: 5 }), + ClientFilter::Flags("N".to_string()), + ClientFilter::LibName("lib".to_string()), + ClientFilter::LibVer("1.0".to_string()), + ClientFilter::Db(1), + ClientFilter::Capa("redirect".to_string()), + ClientFilter::Ip("127.0.0.1".to_string()), + ]; + assert_eq!( + words(filters), + [ + "ID", + "1", + "2", + "TYPE", + "master", + "USER", + "app", + "ADDR", + "127.0.0.1:5000", + "LADDR", + "127.0.0.1:6379", + "SKIPME", + "NO", + "MAXAGE", + "60", + "NAME", + "worker", + "IDLE", + "5", + "FLAGS", + "N", + "LIB-NAME", + "lib", + "LIB-VER", + "1.0", + "DB", + "1", + "CAPA", + "redirect", + "IP", + "127.0.0.1" + ] + ); + assert_eq!(words(ClientFilter::Skipme(true)), ["SKIPME", "YES"]); + assert_eq!( + words_error(ClientFilter::Id(vec![])), + "id requires at least one item" + ); + } + + #[test] + fn client_filter_negated() { + let filters = vec![ + ClientFilter::NotId(vec![3]), + ClientFilter::NotType(ClientType::Pubsub), + ClientFilter::NotUser("app".to_string()), + ClientFilter::NotAddr("a".to_string()), + ClientFilter::NotLaddr("l".to_string()), + ClientFilter::NotName("n".to_string()), + ClientFilter::NotFlags("f".to_string()), + ClientFilter::NotLibName("ln".to_string()), + ClientFilter::NotLibVer("lv".to_string()), + ClientFilter::NotDb(2), + ClientFilter::NotCapa("c".to_string()), + ClientFilter::NotIp("i".to_string()), + ]; + assert_eq!( + words(filters), + [ + "NOT-ID", + "3", + "NOT-TYPE", + "pubsub", + "NOT-USER", + "app", + "NOT-ADDR", + "a", + "NOT-LADDR", + "l", + "NOT-NAME", + "n", + "NOT-FLAGS", + "f", + "NOT-LIB-NAME", + "ln", + "NOT-LIB-VER", + "lv", + "NOT-DB", + "2", + "NOT-CAPA", + "c", + "NOT-IP", + "i" + ] + ); + assert_eq!( + words_error(ClientFilter::NotId(vec![])), + "not-id requires at least one item" + ); + } + + #[test] + fn client_list() { + let value = bulk("id=3 addr=127.0.0.1:5000 name=\nid=4 addr=127.0.0.1:5001 name=worker\n"); + let clients = from::(value).0; + let client = |id: &str, addr: &str, name: &str| -> Map { + [("id", id), ("addr", addr), ("name", name)] + .into_iter() + .map(|(field, value)| (field.to_string(), value.to_string())) + .collect() + }; + assert_eq!( + clients, + [ + client("3", "127.0.0.1:5000", ""), + client("4", "127.0.0.1:5001", "worker") + ] + ); + assert!(from::(bulk("")).0.is_empty()); + } + + #[test] + fn cluster_bumpepoch_result() { + assert_eq!( + from::(simple("BUMPED 5")), + ClusterBumpepochResult::Bumped(5) + ); + assert_eq!( + from::(simple("STILL 3")), + ClusterBumpepochResult::Still(3) + ); + assert_eq!( + from_error::(simple("STILL")), + r#"unexpected reply: "STILL""# + ); + assert_eq!( + from_error::(simple("OTHER 1")), + r#"unexpected reply: "OTHER 1""# + ); + } + + #[test] + fn cluster_getslotmigrations_entry() { + let value = fields(vec![ + ("link_name", bulk("abc")), + ("operation", bulk("EXPORT")), + ("slot_ranges", bulk("0-100 200-200")), + ("target_node", bulk("target")), + ("source_node", bulk("source")), + ("create_time", int(1_700_000_000)), + ("last_update_time", int(1_700_000_001)), + ("last_ack_time", int(1_700_000_002)), + ("state", bulk("success")), + ("message", bulk("")), + ("cow_size", int(0)), + ]); + assert_eq!( + from::(value), + ClusterGetslotmigrationsEntry { + link_name: "abc".to_string(), + operation: ClusterGetslotmigrationsOperation::Export, + slot_ranges: vec![(0, 100), (200, 200)], + target_node: "target".to_string(), + source_node: "source".to_string(), + create_time: UnixTimeSeconds { + exat: 1_700_000_000 + }, + last_update_time: UnixTimeSeconds { + exat: 1_700_000_001 + }, + last_ack_time: UnixTimeSeconds { + exat: 1_700_000_002 + }, + state: "success".to_string(), + message: String::new(), + cow_size: 0, + } + ); + } + + #[test] + fn cluster_getslotmigrations_operation() { + assert_eq!( + from::(bulk("IMPORT")), + ClusterGetslotmigrationsOperation::Import + ); + assert_eq!( + from_error::(bulk("MOVE")), + r#"unexpected operation: "MOVE""# + ); + } + + #[test] + fn slot_ranges() { + assert_eq!(from::(bulk("")).0, []); + assert_eq!( + from_error::(bulk("0-100 200")), + r#"unexpected slot ranges: "0-100 200""# + ); + assert_eq!( + from_error::(bulk("0-x")), + r#"unexpected slot ranges: "0-x""# + ); + } + + #[test] + fn cluster_migrateslots_group() { + let group = ClusterMigrateslotsGroup { + ranges: vec![(0, 100), (200, 300)], + node_id: "node".to_string(), + auth: Some(("user".to_string(), "secret".to_string())), + }; + assert_eq!( + words(group), + [ + "SLOTSRANGE", + "0", + "100", + "200", + "300", + "NODE", + "node", + "AUTH", + "user", + "secret" + ] + ); + let group = ClusterMigrateslotsGroup { + ranges: vec![], + node_id: "node".to_string(), + auth: None, + }; + assert_eq!(words_error(group), "ranges requires at least one item"); + } + + #[test] + fn cluster_setslot_state() { + assert_eq!( + words(vec![ + ClusterSetslotState::Importing("a".to_string()), + ClusterSetslotState::Migrating("b".to_string()), + ClusterSetslotState::Node("c".to_string()), + ClusterSetslotState::Stable, + ]), + ["IMPORTING", "a", "MIGRATING", "b", "NODE", "c", "STABLE"] + ); + } + + #[test] + fn commandlog_entry() { + let value = array(vec![ + int(1), + int(1_700_000_000), + int(15), + bulks(&["GET", "key"]), + bulk("127.0.0.1:5000"), + bulk(""), + ]); + assert_eq!( + from::(value), + CommandlogEntry { + id: 1, + timestamp: UnixTimeSeconds { + exat: 1_700_000_000 + }, + value: 15, + arguments: vec!["GET".to_string(), "key".to_string()], + client_address: "127.0.0.1:5000".to_string(), + client_name: String::new(), + } + ); + assert_eq!( + from_error::(array(vec![int(1)])), + "unexpected number of items, expected 6: 1" + ); + } + + #[test] + fn failover_target() { + let target = |force| FailoverTarget { + host: "replica".to_string(), + port: 6380, + force, + }; + assert_eq!(words(target(true)), ["TO", "replica", "6380", "FORCE"]); + assert_eq!(words(target(false)), ["TO", "replica", "6380"]); + } + + #[test] + fn latency_histogram_entry() { + let value = fields(vec![ + ("calls", int(3)), + ( + "histogram_usec", + crate::test_util::map(vec![(int(1), int(2)), (int(4), int(3))]), + ), + ]); + assert_eq!( + from::(value), + LatencyHistogramEntry { + calls: 3, + histogram_usec: vec![(1, 2), (4, 3)], + } + ); + } + + #[test] + fn latency_latest_entry() { + let value = array(vec![ + bulk("command"), + int(1_700_000_000), + int(250), + int(500), + int(1000), + int(4), + ]); + assert_eq!( + from::(value), + LatencyLatestEntry { + event: "command".to_string(), + timestamp: UnixTimeSeconds { + exat: 1_700_000_000 + }, + latest: DurationMilliseconds { px: 250 }, + max: DurationMilliseconds { px: 500 }, + sum: DurationMilliseconds { px: 1000 }, + count: 4, + } + ); + assert_eq!( + from_error::(array(vec![])), + "unexpected number of items, expected 6: 0" + ); + } + + #[test] + fn module_list_entry() { + let value = fields(vec![ + ("name", bulk("lua")), + ("ver", int(1)), + ("path", bulk("lua")), + ("args", array(vec![])), + ]); + assert_eq!( + from::(value), + ModuleListEntry { + name: "lua".to_string(), + ver: 1, + path: "lua".to_string(), + args: vec![], + } + ); + } + + #[test] + fn role_primary() { + let value = array(vec![ + bulk("master"), + int(3129659), + array(vec![bulks(&["127.0.0.1", "9001", "3129242"])]), + ]); + assert_eq!( + from::(value), + RoleReply::Primary(RolePrimary { + offset: 3129659, + replicas: vec![RolePrimaryReplica { + ip: "127.0.0.1".to_string(), + port: 9001, + offset: 3129242, + }], + }) + ); + } + + #[test] + fn role_replica() { + let value = array(vec![ + bulk("slave"), + bulk("127.0.0.1"), + int(9000), + bulk("connected"), + int(3167038), + ]); + assert_eq!( + from::(value), + RoleReply::Replica(RoleReplica { + primary_host: "127.0.0.1".to_string(), + primary_port: 9000, + state: "connected".to_string(), + offset: 3167038, + }) + ); + } + + #[test] + fn role_sentinel() { + let value = array(vec![bulk("sentinel"), bulks(&["primary"])]); + assert_eq!( + from::(value), + RoleReply::Sentinel(vec!["primary".to_string()]) + ); + } + + #[test] + fn role_invalid() { + assert_eq!( + from_error::(array(vec![bulk("master"), int(1)])), + r#"too few items for the role "master""# + ); + assert_eq!( + from_error::(array(vec![bulk("leader")])), + r#"unexpected role: "leader""# + ); + assert_eq!( + from_error::(array(vec![ + bulk("slave"), + bulk("h"), + int(70000), + bulk("connected"), + int(1), + ])), + "unexpected port" + ); + assert_eq!( + from_error::(bulks(&["127.0.0.1", "port", "1"])), + r#"unexpected number: "port""# + ); + } +} diff --git a/crates/componentized-valkey/src/commands/bitmap.rs b/crates/componentized-valkey/src/commands/bitmap.rs new file mode 100644 index 0000000..1c6a7c2 --- /dev/null +++ b/crates/componentized-valkey/src/commands/bitmap.rs @@ -0,0 +1,197 @@ +//! The bitmap commands: BITCOUNT, BITFIELD, BITPOS, SETBIT and the like. + +use crate::command::IntoCommand; +use crate::types::{ + BitRange, BitfieldEncoding, BitfieldGet, BitfieldIncrby, BitfieldOffset, BitfieldOperation, + BitfieldOverflow, BitfieldSet, Error, +}; + +/// `start [ end [ BIT ] ]`, BYTE is the default, and left out for servers before it existed. +impl IntoCommand for BitRange { + fn into_command(self) -> Result, Error> { + match self { + BitRange::From(start) => start.into_command(), + BitRange::Bytes((start, end)) => (start, end).into_command(), + BitRange::Bits((start, end)) => (start, end, "BIT").into_command(), + } + } +} + +/// `i1` to `i64`, `u1` to `u63`, other widths fail. +impl IntoCommand for BitfieldEncoding { + fn into_command(self) -> Result, Error> { + match self { + BitfieldEncoding::Signed(width @ 1..=64) => format!("i{width}").into_command(), + BitfieldEncoding::Unsigned(width @ 1..=63) => format!("u{width}").into_command(), + BitfieldEncoding::Signed(width) => Err(Error::Client(format!( + "a signed bitfield is 1 to 64 bits wide, not {width}" + ))), + BitfieldEncoding::Unsigned(width) => Err(Error::Client(format!( + "an unsigned bitfield is 1 to 63 bits wide, not {width}" + ))), + } + } +} + +/// The offset in bits, or `#` and the index of a field of the encoding's width. +impl IntoCommand for BitfieldOffset { + fn into_command(self) -> Result, Error> { + match self { + BitfieldOffset::Bit(offset) => offset.into_command(), + BitfieldOffset::Field(field) => format!("#{field}").into_command(), + } + } +} + +impl IntoCommand for BitfieldOverflow { + fn into_command(self) -> Result, Error> { + let overflow = match self { + BitfieldOverflow::Wrap => "WRAP", + BitfieldOverflow::Sat => "SAT", + BitfieldOverflow::Fail => "FAIL", + }; + ("OVERFLOW", overflow).into_command() + } +} + +/// Also `BitfieldRoGet`, the same type. +impl IntoCommand for BitfieldGet { + fn into_command(self) -> Result, Error> { + ("GET", self.encoding, self.offset).into_command() + } +} + +impl IntoCommand for BitfieldSet { + fn into_command(self) -> Result, Error> { + ("SET", self.encoding, self.offset, self.value).into_command() + } +} + +impl IntoCommand for BitfieldIncrby { + fn into_command(self) -> Result, Error> { + ("INCRBY", self.encoding, self.offset, self.increment).into_command() + } +} + +/// A write is preceded by its overflow mode, when given. +impl IntoCommand for BitfieldOperation { + fn into_command(self) -> Result, Error> { + match self { + BitfieldOperation::Get(get) => get.into_command(), + BitfieldOperation::Set((overflow, set)) => (overflow, set).into_command(), + BitfieldOperation::Incrby((overflow, incrby)) => (overflow, incrby).into_command(), + } + } +} + +/// A bit, `1` or `0`, e.g. for SETBIT. +pub struct Bit(pub bool); + +impl IntoCommand for Bit { + fn into_command(self) -> Result, Error> { + if self.0 { "1" } else { "0" }.into_command() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{words, words_error}; + + #[test] + fn bit_range() { + assert_eq!(words(BitRange::From(5)), ["5"]); + assert_eq!(words(BitRange::Bytes((0, -1))), ["0", "-1"]); + assert_eq!(words(BitRange::Bits((1, 8))), ["1", "8", "BIT"]); + } + + #[test] + fn bitfield_encoding() { + assert_eq!( + words([ + BitfieldEncoding::Signed(1), + BitfieldEncoding::Signed(64), + BitfieldEncoding::Unsigned(1), + BitfieldEncoding::Unsigned(63), + ]), + ["i1", "i64", "u1", "u63"] + ); + assert_eq!( + words_error(BitfieldEncoding::Signed(0)), + "a signed bitfield is 1 to 64 bits wide, not 0" + ); + assert_eq!( + words_error(BitfieldEncoding::Signed(65)), + "a signed bitfield is 1 to 64 bits wide, not 65" + ); + assert_eq!( + words_error(BitfieldEncoding::Unsigned(64)), + "an unsigned bitfield is 1 to 63 bits wide, not 64" + ); + } + + #[test] + fn bitfield_offset() { + assert_eq!(words(BitfieldOffset::Bit(100)), ["100"]); + assert_eq!(words(BitfieldOffset::Field(2)), ["#2"]); + } + + #[test] + fn bitfield_overflow() { + assert_eq!( + words([ + BitfieldOverflow::Wrap, + BitfieldOverflow::Sat, + BitfieldOverflow::Fail + ]), + ["OVERFLOW", "WRAP", "OVERFLOW", "SAT", "OVERFLOW", "FAIL"] + ); + } + + #[test] + fn bitfield_operations() { + let get = BitfieldGet { + encoding: BitfieldEncoding::Unsigned(8), + offset: BitfieldOffset::Bit(0), + }; + let set = BitfieldSet { + encoding: BitfieldEncoding::Signed(8), + offset: BitfieldOffset::Field(1), + value: -5, + }; + let incrby = BitfieldIncrby { + encoding: BitfieldEncoding::Unsigned(4), + offset: BitfieldOffset::Bit(16), + increment: 3, + }; + assert_eq!(words(get.clone()), ["GET", "u8", "0"]); + assert_eq!( + words(vec![ + BitfieldOperation::Get(get), + BitfieldOperation::Set((Some(BitfieldOverflow::Sat), set)), + BitfieldOperation::Incrby((None, incrby)), + ]), + [ + "GET", "u8", "0", "OVERFLOW", "SAT", "SET", "i8", "#1", "-5", "INCRBY", "u4", "16", + "3" + ] + ); + } + + #[test] + fn bitfield_invalid_encoding() { + let get = BitfieldGet { + encoding: BitfieldEncoding::Unsigned(64), + offset: BitfieldOffset::Bit(0), + }; + assert_eq!( + words_error(BitfieldOperation::Get(get)), + "an unsigned bitfield is 1 to 63 bits wide, not 64" + ); + } + + #[test] + fn bit() { + assert_eq!(words([Bit(true), Bit(false)]), ["1", "0"]); + } +} diff --git a/crates/componentized-valkey/src/commands/connection.rs b/crates/componentized-valkey/src/commands/connection.rs new file mode 100644 index 0000000..71b08bb --- /dev/null +++ b/crates/componentized-valkey/src/commands/connection.rs @@ -0,0 +1,737 @@ +//! The connection commands: CLIENT of the connection itself, and COMMAND. + +use crate::command::{Flag, IntoCommand, NamedOption, NonEmpty}; +use crate::types::{ + ClientSetinfoAttr, ClientTrackingMode, ClientTrackingOptions, ClientTrackingRedirect, + ClientTrackinginfoReply, CommandDocsArgument, CommandDocsArgumentType, CommandDocsReply, + CommandInfoReply, CommandKeySpec, CommandKeySpecBeginSearch, CommandKeySpecFindKeys, + CommandKeySpecKeynum, CommandKeySpecKeyword, CommandKeySpecRange, CommandListFilter, Error, + Integer, RespValue, +}; +use crate::value::{Fields, FlatPairs, FromValue, entries, items}; + +impl IntoCommand for ClientSetinfoAttr { + fn into_command(self) -> Result, Error> { + match self { + ClientSetinfoAttr::LibName(name) => ("LIB-NAME", name).into_command(), + ClientSetinfoAttr::LibVer(version) => ("LIB-VER", version).into_command(), + } + } +} + +impl IntoCommand for ClientTrackingMode { + fn into_command(self) -> Result, Error> { + match self { + ClientTrackingMode::Optin => "OPTIN", + ClientTrackingMode::Optout => "OPTOUT", + } + .into_command() + } +} + +/// `[ REDIRECT client-id ] [ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN | OPTOUT ] [ NOLOOP ]` +impl IntoCommand for ClientTrackingOptions { + fn into_command(self) -> Result, Error> { + // PREFIX before each prefix + let prefixes = self.prefixes.map(|prefixes| { + let prefixes = prefixes.into_iter().map(|prefix| ("PREFIX", prefix)); + prefixes.collect::>() + }); + ( + NamedOption("REDIRECT", self.redirect), + NonEmpty::optional("prefixes", prefixes), + Flag("BCAST", self.bcast), + self.mode, + Flag("NOLOOP", self.noloop), + ) + .into_command() + } +} + +/// `-1` when tracking is off, `0` when not redirected, otherwise the ID of the connection. +impl FromValue for ClientTrackingRedirect { + fn from_value(value: RespValue) -> Result { + match Integer::from_value(value)? { + -1 => Ok(ClientTrackingRedirect::NotTracking), + 0 => Ok(ClientTrackingRedirect::NotRedirected), + id if id > 0 => Ok(ClientTrackingRedirect::Redirected(id)), + code => Err(Error::Client(format!("unexpected reply: {code}"))), + } + } +} + +impl FromValue for ClientTrackinginfoReply { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + flags: fields.required("flags")?, + redirect: fields.required("redirect")?, + prefixes: fields.required("prefixes")?, + }) + } +} + +/// `FILTERBY 〈 MODULE module-name | ACLCAT category | PATTERN pattern 〉` +impl IntoCommand for CommandListFilter { + fn into_command(self) -> Result, Error> { + let (filter, value) = match self { + CommandListFilter::Module(name) => ("MODULE", name), + CommandListFilter::Aclcat(category) => ("ACLCAT", category), + CommandListFilter::Pattern(pattern) => ("PATTERN", pattern), + }; + ("FILTERBY", filter, value).into_command() + } +} + +/// `ON` or `OFF`, e.g. for CLIENT NO-TOUCH. +pub struct OnOff(pub bool); + +impl IntoCommand for OnOff { + fn into_command(self) -> Result, Error> { + if self.0 { "ON" } else { "OFF" }.into_command() + } +} + +/// The entries of COMMAND and COMMAND INFO, each followed by its subcommands, nulls, for commands +/// that do not exist, left out. +pub struct CommandInfoList(pub Vec); + +impl FromValue for CommandInfoList { + fn from_value(value: RespValue) -> Result { + let mut commands = vec![]; + for command in items(value)? { + command_info(command, &mut commands)?; + } + Ok(CommandInfoList(commands)) + } +} + +/// `[name, arity, flags, first-key, last-key, step, acl-categories, tips, key-specs, subcommands]`, +/// the ACL categories since 6.0, the rest since 7.0. +fn command_info(value: RespValue, commands: &mut Vec) -> Result<(), Error> { + if value == RespValue::Null { + return Ok(()); + } + let mut items = items(value)?.into_iter(); + let mut next = || { + items + .next() + .ok_or_else(|| Error::Client("too few items for a command".to_string())) + }; + let info = CommandInfoReply { + name: FromValue::from_value(next()?)?, + arity: FromValue::from_value(next()?)?, + flags: FromValue::from_value(next()?)?, + first_key: FromValue::from_value(next()?)?, + last_key: FromValue::from_value(next()?)?, + step: FromValue::from_value(next()?)?, + acl_categories: next().map_or(Ok(vec![]), FromValue::from_value)?, + tips: next().map_or(Ok(vec![]), FromValue::from_value)?, + key_specs: next().map_or(Ok(vec![]), FromValue::from_value)?, + }; + let subcommands: Vec = next().map_or(Ok(vec![]), FromValue::from_value)?; + commands.push(info); + for subcommand in subcommands { + command_info(subcommand, commands)?; + } + Ok(()) +} + +impl FromValue for CommandKeySpec { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + let mut begin_search: Fields = fields.required("begin_search")?; + let mut find_keys: Fields = fields.required("find_keys")?; + Ok(Self { + notes: fields.optional("notes")?, + flags: fields.required("flags")?, + begin_search: begin_search_spec(&mut begin_search)?, + find_keys: find_keys_spec(&mut find_keys)?, + }) + } +} + +/// `{type, spec}`, a type this interface does not know is `unknown`. +fn begin_search_spec(fields: &mut Fields) -> Result { + let kind: String = fields.required("type")?; + let mut spec: Fields = fields.required("spec")?; + Ok(match kind.as_str() { + "index" => CommandKeySpecBeginSearch::Index(spec.required("index")?), + "keyword" => CommandKeySpecBeginSearch::Keyword(CommandKeySpecKeyword { + keyword: spec.required("keyword")?, + startfrom: spec.required("startfrom")?, + }), + _ => CommandKeySpecBeginSearch::Unknown, + }) +} + +/// `{type, spec}`, a type this interface does not know is `unknown`. +fn find_keys_spec(fields: &mut Fields) -> Result { + let kind: String = fields.required("type")?; + let mut spec: Fields = fields.required("spec")?; + Ok(match kind.as_str() { + "range" => CommandKeySpecFindKeys::Range(CommandKeySpecRange { + lastkey: spec.required("lastkey")?, + keystep: spec.required("keystep")?, + limit: spec.required("limit")?, + }), + "keynum" => CommandKeySpecFindKeys::Keynum(CommandKeySpecKeynum { + keynumidx: spec.required("keynumidx")?, + firstkey: spec.required("firstkey")?, + keystep: spec.required("keystep")?, + }), + _ => CommandKeySpecFindKeys::Unknown, + }) +} + +/// The documentation of COMMAND DOCS, a map by name, each followed by its subcommands. +pub struct CommandDocsList(pub Vec); + +impl FromValue for CommandDocsList { + fn from_value(value: RespValue) -> Result { + let mut commands = vec![]; + for (name, doc) in entries::(value)? { + command_doc(name, doc, &mut commands)?; + } + Ok(CommandDocsList(commands)) + } +} + +fn command_doc( + name: String, + mut fields: Fields, + commands: &mut Vec, +) -> Result<(), Error> { + let mut arguments = vec![]; + let nested: Option> = fields.optional("arguments")?; + command_arguments(nested.unwrap_or_default(), None, &mut arguments)?; + commands.push(CommandDocsReply { + name, + summary: fields.required("summary")?, + since: fields.required("since")?, + group: fields.required("group")?, + complexity: fields.optional("complexity")?, + module: fields.optional("module")?, + doc_flags: fields.optional("doc_flags")?.unwrap_or_default(), + deprecated_since: fields.optional("deprecated_since")?, + replaced_by: fields.optional("replaced_by")?, + history: fields.optional("history")?.unwrap_or_default(), + arguments, + }); + let subcommands: Option> = fields + .optional::>("subcommands")? + .map(|subcommands| subcommands.0); + for (name, doc) in subcommands.unwrap_or_default() { + command_doc(name, doc, commands)?; + } + Ok(()) +} + +/// The arguments, depth first, each followed by those it contains, which point to it as their +/// parent. +fn command_arguments( + nested: Vec, + parent: Option, + arguments: &mut Vec, +) -> Result<(), Error> { + for mut fields in nested { + let index = u32::try_from(arguments.len()) + .map_err(|_| Error::Client("too many arguments".to_string()))?; + let kind: String = fields.required("type")?; + arguments.push(CommandDocsArgument { + parent, + name: fields.required("name")?, + type_: argument_type(kind), + display_text: fields.optional("display_text")?, + key_spec_index: fields.optional("key_spec_index")?, + token: fields.optional("token")?, + summary: fields.optional("summary")?, + since: fields.optional("since")?, + deprecated_since: fields.optional("deprecated_since")?, + flags: fields.optional("flags")?.unwrap_or_default(), + }); + let contained: Option> = fields.optional("arguments")?; + command_arguments(contained.unwrap_or_default(), Some(index), arguments)?; + } + Ok(()) +} + +fn argument_type(kind: String) -> CommandDocsArgumentType { + match kind.as_str() { + "string" => CommandDocsArgumentType::String, + "integer" => CommandDocsArgumentType::Integer, + "double" => CommandDocsArgumentType::Double, + "key" => CommandDocsArgumentType::Key, + "pattern" => CommandDocsArgumentType::Pattern, + "unix-time" => CommandDocsArgumentType::UnixTime, + "pure-token" => CommandDocsArgumentType::PureToken, + "oneof" => CommandDocsArgumentType::Oneof, + "block" => CommandDocsArgumentType::Block, + _ => CommandDocsArgumentType::Other(kind), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{ + array, bulk, bulks, fields, from, from_error, int, map, words, words_error, + }; + + #[test] + fn client_setinfo_attr() { + assert_eq!( + words(ClientSetinfoAttr::LibName("lib".to_string())), + ["LIB-NAME", "lib"] + ); + assert_eq!( + words(ClientSetinfoAttr::LibVer("1.0".to_string())), + ["LIB-VER", "1.0"] + ); + } + + #[test] + fn client_tracking_options() { + let options = ClientTrackingOptions { + redirect: Some(5), + prefixes: Some(vec!["a:".to_string(), "b:".to_string()]), + bcast: true, + mode: Some(ClientTrackingMode::Optin), + noloop: true, + }; + assert_eq!( + words(options), + [ + "REDIRECT", "5", "PREFIX", "a:", "PREFIX", "b:", "BCAST", "OPTIN", "NOLOOP" + ] + ); + let options = ClientTrackingOptions { + redirect: None, + prefixes: None, + bcast: false, + mode: Some(ClientTrackingMode::Optout), + noloop: false, + }; + assert_eq!(words(options), ["OPTOUT"]); + let options = ClientTrackingOptions { + redirect: None, + prefixes: Some(vec![]), + bcast: true, + mode: None, + noloop: false, + }; + assert_eq!(words_error(options), "prefixes requires at least one item"); + } + + #[test] + fn client_tracking_redirect() { + assert_eq!( + from::(int(-1)), + ClientTrackingRedirect::NotTracking + ); + assert_eq!( + from::(int(0)), + ClientTrackingRedirect::NotRedirected + ); + assert_eq!( + from::(int(7)), + ClientTrackingRedirect::Redirected(7) + ); + assert_eq!( + from_error::(int(-2)), + "unexpected reply: -2" + ); + } + + #[test] + fn client_trackinginfo_reply() { + let value = fields(vec![ + ("flags", bulks(&["on", "bcast"])), + ("redirect", int(0)), + ("prefixes", bulks(&["a:"])), + ]); + assert_eq!( + from::(value), + ClientTrackinginfoReply { + flags: vec!["on".to_string(), "bcast".to_string()], + redirect: ClientTrackingRedirect::NotRedirected, + prefixes: vec!["a:".to_string()], + } + ); + } + + #[test] + fn command_list_filter() { + assert_eq!( + words(CommandListFilter::Module("search".to_string())), + ["FILTERBY", "MODULE", "search"] + ); + assert_eq!( + words(CommandListFilter::Aclcat("read".to_string())), + ["FILTERBY", "ACLCAT", "read"] + ); + assert_eq!( + words(CommandListFilter::Pattern("x*".to_string())), + ["FILTERBY", "PATTERN", "x*"] + ); + } + + #[test] + fn on_off() { + assert_eq!(words([OnOff(true), OnOff(false)]), ["ON", "OFF"]); + } + + fn get_key_spec() -> RespValue { + fields(vec![ + ("flags", bulks(&["RO", "access"])), + ( + "begin_search", + fields(vec![ + ("type", bulk("index")), + ("spec", fields(vec![("index", int(1))])), + ]), + ), + ( + "find_keys", + fields(vec![ + ("type", bulk("range")), + ( + "spec", + fields(vec![ + ("lastkey", int(0)), + ("keystep", int(1)), + ("limit", int(0)), + ]), + ), + ]), + ), + ]) + } + + fn command_info_value( + name: &str, + key_specs: Vec, + subcommands: Vec, + ) -> RespValue { + array(vec![ + bulk(name), + int(2), + bulks(&["readonly"]), + int(1), + int(1), + int(1), + bulks(&["@read"]), + array(vec![]), + array(key_specs), + array(subcommands), + ]) + } + + fn command_info_reply(name: &str, key_specs: Vec) -> CommandInfoReply { + CommandInfoReply { + name: name.to_string(), + arity: 2, + flags: vec!["readonly".to_string()], + first_key: 1, + last_key: 1, + step: 1, + acl_categories: vec!["@read".to_string()], + tips: vec![], + key_specs, + } + } + + #[test] + fn command_info_list() { + let value = array(vec![ + command_info_value("get", vec![get_key_spec()], vec![]), + // a command that does not exist + RespValue::Null, + command_info_value( + "config", + vec![], + vec![command_info_value("config|get", vec![], vec![])], + ), + ]); + let get_key_spec = CommandKeySpec { + notes: None, + flags: vec!["RO".to_string(), "access".to_string()], + begin_search: CommandKeySpecBeginSearch::Index(1), + find_keys: CommandKeySpecFindKeys::Range(CommandKeySpecRange { + lastkey: 0, + keystep: 1, + limit: 0, + }), + }; + assert_eq!( + from::(value).0, + [ + command_info_reply("get", vec![get_key_spec]), + command_info_reply("config", vec![]), + command_info_reply("config|get", vec![]), + ] + ); + } + + #[test] + fn command_info_before_valkey_7() { + let value = array(vec![array(vec![ + bulk("get"), + int(2), + bulks(&["readonly"]), + int(1), + int(1), + int(1), + ])]); + assert_eq!( + from::(value).0, + [CommandInfoReply { + acl_categories: vec![], + ..command_info_reply("get", vec![]) + }] + ); + assert_eq!( + from_error::(array(vec![array(vec![bulk("get")])])), + "too few items for a command" + ); + } + + #[test] + fn command_key_spec_kinds() { + let value = fields(vec![ + ("notes", bulk("a note")), + ("flags", bulks(&["RW"])), + ( + "begin_search", + fields(vec![ + ("type", bulk("keyword")), + ( + "spec", + fields(vec![("keyword", bulk("STREAMS")), ("startfrom", int(1))]), + ), + ]), + ), + ( + "find_keys", + fields(vec![ + ("type", bulk("keynum")), + ( + "spec", + fields(vec![ + ("keynumidx", int(0)), + ("firstkey", int(1)), + ("keystep", int(1)), + ]), + ), + ]), + ), + ]); + assert_eq!( + from::(value), + CommandKeySpec { + notes: Some("a note".to_string()), + flags: vec!["RW".to_string()], + begin_search: CommandKeySpecBeginSearch::Keyword(CommandKeySpecKeyword { + keyword: "STREAMS".to_string(), + startfrom: 1, + }), + find_keys: CommandKeySpecFindKeys::Keynum(CommandKeySpecKeynum { + keynumidx: 0, + firstkey: 1, + keystep: 1, + }), + } + ); + } + + #[test] + fn command_key_spec_unknown_kinds() { + let unknown = || fields(vec![("type", bulk("newer")), ("spec", fields(vec![]))]); + let value = fields(vec![ + ("flags", array(vec![])), + ("begin_search", unknown()), + ("find_keys", unknown()), + ]); + assert_eq!( + from::(value), + CommandKeySpec { + notes: None, + flags: vec![], + begin_search: CommandKeySpecBeginSearch::Unknown, + find_keys: CommandKeySpecFindKeys::Unknown, + } + ); + } + + fn doc(name: &str) -> CommandDocsReply { + CommandDocsReply { + name: name.to_string(), + summary: format!("about {name}"), + since: "1.0.0".to_string(), + group: "server".to_string(), + complexity: None, + module: None, + doc_flags: vec![], + deprecated_since: None, + replaced_by: None, + history: vec![], + arguments: vec![], + } + } + + fn argument( + parent: Option, + name: &str, + type_: CommandDocsArgumentType, + ) -> CommandDocsArgument { + CommandDocsArgument { + parent, + name: name.to_string(), + type_, + display_text: None, + key_spec_index: None, + token: None, + summary: None, + since: None, + deprecated_since: None, + flags: vec![], + } + } + + #[test] + fn command_docs_list() { + let value = map(vec![ + ( + bulk("set"), + fields(vec![ + ("summary", bulk("about set")), + ("since", bulk("1.0.0")), + ("group", bulk("server")), + ("complexity", bulk("O(1)")), + ( + "history", + array(vec![bulks(&[ + "2.6.12", + "Added the EX, PX, NX and XX options.", + ])]), + ), + ( + "arguments", + array(vec![ + fields(vec![ + ("name", bulk("key")), + ("type", bulk("key")), + ("display_text", bulk("key")), + ("key_spec_index", int(0)), + ]), + fields(vec![ + ("name", bulk("condition")), + ("type", bulk("oneof")), + ("since", bulk("2.6.12")), + ("flags", bulks(&["optional"])), + ( + "arguments", + array(vec![fields(vec![ + ("name", bulk("nx")), + ("type", bulk("pure-token")), + ("token", bulk("NX")), + ])]), + ), + ]), + fields(vec![("name", bulk("newer")), ("type", bulk("vector"))]), + ]), + ), + ]), + ), + ( + bulk("config"), + fields(vec![ + ("summary", bulk("about config")), + ("since", bulk("1.0.0")), + ("group", bulk("server")), + ("doc_flags", bulks(&["syscmd"])), + ( + "subcommands", + map(vec![( + bulk("config|get"), + fields(vec![ + ("summary", bulk("about config|get")), + ("since", bulk("1.0.0")), + ("group", bulk("server")), + ("deprecated_since", bulk("9.0.0")), + ("replaced_by", bulk("`OTHER`")), + ]), + )]), + ), + ]), + ), + ]); + assert_eq!( + from::(value).0, + [ + CommandDocsReply { + complexity: Some("O(1)".to_string()), + history: vec![( + "2.6.12".to_string(), + "Added the EX, PX, NX and XX options.".to_string() + )], + arguments: vec![ + CommandDocsArgument { + display_text: Some("key".to_string()), + key_spec_index: Some(0), + ..argument(None, "key", CommandDocsArgumentType::Key) + }, + CommandDocsArgument { + since: Some("2.6.12".to_string()), + flags: vec!["optional".to_string()], + ..argument(None, "condition", CommandDocsArgumentType::Oneof) + }, + CommandDocsArgument { + token: Some("NX".to_string()), + ..argument(Some(1), "nx", CommandDocsArgumentType::PureToken) + }, + argument( + None, + "newer", + CommandDocsArgumentType::Other("vector".to_string()) + ), + ], + ..doc("set") + }, + CommandDocsReply { + doc_flags: vec!["syscmd".to_string()], + ..doc("config") + }, + CommandDocsReply { + deprecated_since: Some("9.0.0".to_string()), + replaced_by: Some("`OTHER`".to_string()), + ..doc("config|get") + }, + ] + ); + } + + #[test] + fn command_docs_argument_types() { + let kinds = [ + ("string", CommandDocsArgumentType::String), + ("integer", CommandDocsArgumentType::Integer), + ("double", CommandDocsArgumentType::Double), + ("key", CommandDocsArgumentType::Key), + ("pattern", CommandDocsArgumentType::Pattern), + ("unix-time", CommandDocsArgumentType::UnixTime), + ("pure-token", CommandDocsArgumentType::PureToken), + ("oneof", CommandDocsArgumentType::Oneof), + ("block", CommandDocsArgumentType::Block), + ]; + for (kind, expected) in kinds { + assert_eq!(argument_type(kind.to_string()), expected); + } + } + + #[test] + fn command_docs_missing_field() { + let value = map(vec![(bulk("get"), fields(vec![("summary", bulk("s"))]))]); + assert_eq!(from_error::(value), "missing field: since"); + } +} diff --git a/crates/componentized-valkey/src/commands/geo.rs b/crates/componentized-valkey/src/commands/geo.rs new file mode 100644 index 0000000..6965ed6 --- /dev/null +++ b/crates/componentized-valkey/src/commands/geo.rs @@ -0,0 +1,269 @@ +//! The geo commands: GEOADD, GEODIST, GEOPOS, GEOSEARCH and the like. + +use crate::command::{Flag, IntoCommand, NonEmpty, NumKeys}; +use crate::types::{ + Double, Error, GeoCoordinates, GeoUnit, GeoaddCondition, GeosearchCount, GeosearchFrom, + GeosearchShape, GeosearchWithResult, RespValue, +}; +use crate::value::{FromValue, items, unexpected}; + +/// `longitude latitude` +impl IntoCommand for GeoCoordinates { + fn into_command(self) -> Result, Error> { + (self.longitude, self.latitude).into_command() + } +} + +/// `[longitude, latitude]` +impl FromValue for GeoCoordinates { + fn from_value(value: RespValue) -> Result { + let (longitude, latitude) = FromValue::from_value(value)?; + Ok(GeoCoordinates { + longitude, + latitude, + }) + } +} + +impl IntoCommand for GeoUnit { + fn into_command(self) -> Result, Error> { + match self { + GeoUnit::M => "m", + GeoUnit::Km => "km", + GeoUnit::Ft => "ft", + GeoUnit::Mi => "mi", + } + .into_command() + } +} + +impl IntoCommand for GeoaddCondition { + fn into_command(self) -> Result, Error> { + match self { + GeoaddCondition::Nx => "NX", + GeoaddCondition::Xx => "XX", + } + .into_command() + } +} + +impl IntoCommand for GeosearchFrom { + fn into_command(self) -> Result, Error> { + match self { + GeosearchFrom::Member(member) => ("FROMMEMBER", member).into_command(), + GeosearchFrom::Lonlat(coordinates) => ("FROMLONLAT", coordinates).into_command(), + } + } +} + +/// The center, when the shape has one, then the shape. Also `GeosearchstoreShape`, the same type. +impl IntoCommand for GeosearchShape { + fn into_command(self) -> Result, Error> { + match self { + GeosearchShape::Radius(circle) => { + (circle.from, "BYRADIUS", circle.radius, circle.unit).into_command() + } + GeosearchShape::Box(rectangle) => ( + rectangle.from, + "BYBOX", + rectangle.width, + rectangle.height, + rectangle.unit, + ) + .into_command(), + GeosearchShape::Polygon(vertices) => { + ("BYPOLYGON", NumKeys(NonEmpty::new("vertices", vertices))).into_command() + } + } + } +} + +/// `COUNT count [ ANY ]`, also `GeosearchstoreCount`, the same type. +impl IntoCommand for GeosearchCount { + fn into_command(self) -> Result, Error> { + ("COUNT", self.count, Flag("ANY", self.any)).into_command() + } +} + +/// The member, or with any of WITHDIST, WITHHASH and WITHCOORD, an array of the member then the +/// details, in that order, told apart by their types. +impl FromValue for GeosearchWithResult { + fn from_value(value: RespValue) -> Result { + let mut result = GeosearchWithResult { + member: String::new(), + distance: None, + hash: None, + coordinates: None, + }; + let items = match value { + RespValue::Array(_) => items(value)?, + // without details, the member alone + value => vec![value], + }; + let mut items = items.into_iter(); + result.member = String::from_value(items.next().unwrap_or(RespValue::Null))?; + for item in items { + match item { + // a bulk string, as RESP3 has no other way to send a fixed precision double + RespValue::BulkString(_) => result.distance = Some(Double::from_value(item)?), + RespValue::Integer(hash) => result.hash = Some(hash), + RespValue::Array(_) => result.coordinates = Some(GeoCoordinates::from_value(item)?), + item => return Err(unexpected(&item)), + } + } + Ok(result) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{array, bulk, bulks, from, from_error, int, words, words_error}; + use crate::types::{GeosearchBox, GeosearchRadius}; + + fn coordinates(longitude: f64, latitude: f64) -> GeoCoordinates { + GeoCoordinates { + longitude, + latitude, + } + } + + #[test] + fn geo_coordinates() { + assert_eq!( + words(coordinates(13.361389, 38.115556)), + ["13.361389", "38.115556"] + ); + assert_eq!( + from::(bulks(&["13.5", "38.25"])), + coordinates(13.5, 38.25) + ); + assert_eq!( + from_error::(bulks(&["13.5"])), + "unexpected number of items, expected 2: 1" + ); + } + + #[test] + fn keywords() { + assert_eq!( + words([GeoUnit::M, GeoUnit::Km, GeoUnit::Ft, GeoUnit::Mi]), + ["m", "km", "ft", "mi"] + ); + assert_eq!( + words([GeoaddCondition::Nx, GeoaddCondition::Xx]), + ["NX", "XX"] + ); + } + + #[test] + fn geosearch_from() { + assert_eq!( + words(GeosearchFrom::Member("Palermo".to_string())), + ["FROMMEMBER", "Palermo"] + ); + assert_eq!( + words(GeosearchFrom::Lonlat(coordinates(15.0, 37.0))), + ["FROMLONLAT", "15", "37"] + ); + } + + #[test] + fn geosearch_shape() { + let radius = GeosearchShape::Radius(GeosearchRadius { + from: GeosearchFrom::Member("Palermo".to_string()), + radius: 200.5, + unit: GeoUnit::Km, + }); + assert_eq!( + words(radius), + ["FROMMEMBER", "Palermo", "BYRADIUS", "200.5", "km"] + ); + let rectangle = GeosearchShape::Box(GeosearchBox { + from: GeosearchFrom::Lonlat(coordinates(15.0, 37.0)), + width: 400.0, + height: 300.0, + unit: GeoUnit::Mi, + }); + assert_eq!( + words(rectangle), + ["FROMLONLAT", "15", "37", "BYBOX", "400", "300", "mi"] + ); + let polygon = GeosearchShape::Polygon(vec![ + coordinates(1.0, 2.0), + coordinates(3.0, 4.0), + coordinates(5.0, 6.0), + ]); + assert_eq!( + words(polygon), + ["BYPOLYGON", "3", "1", "2", "3", "4", "5", "6"] + ); + assert_eq!( + words_error(GeosearchShape::Polygon(vec![])), + "vertices requires at least one item" + ); + } + + #[test] + fn geosearch_count() { + assert_eq!( + words(GeosearchCount { + count: 5, + any: true + }), + ["COUNT", "5", "ANY"] + ); + assert_eq!( + words(GeosearchCount { + count: 5, + any: false + }), + ["COUNT", "5"] + ); + } + + #[test] + fn geosearch_with_result() { + assert_eq!( + from::(bulk("Palermo")), + GeosearchWithResult { + member: "Palermo".to_string(), + distance: None, + hash: None, + coordinates: None, + } + ); + let value = array(vec![ + bulk("Palermo"), + bulk("190.4424"), + int(3479099956230698), + bulks(&["13.5", "38.25"]), + ]); + assert_eq!( + from::(value), + GeosearchWithResult { + member: "Palermo".to_string(), + distance: Some(190.4424), + hash: Some(3479099956230698), + coordinates: Some(coordinates(13.5, 38.25)), + } + ); + let value = array(vec![bulk("Palermo"), int(3479099956230698)]); + assert_eq!( + from::(value), + GeosearchWithResult { + member: "Palermo".to_string(), + distance: None, + hash: Some(3479099956230698), + coordinates: None, + } + ); + assert_eq!( + from_error::(array(vec![ + bulk("Palermo"), + RespValue::Boolean(true) + ])), + "unexpected reply: RespValue::Boolean(true)" + ); + } +} diff --git a/crates/componentized-valkey/src/commands/hash.rs b/crates/componentized-valkey/src/commands/hash.rs new file mode 100644 index 0000000..a9f0c91 --- /dev/null +++ b/crates/componentized-valkey/src/commands/hash.rs @@ -0,0 +1,124 @@ +//! The hash commands: HEXPIRE, HPERSIST, HSETEX and the like. + +use crate::command::{IntoCommand, NonEmpty, NumKeys}; +use crate::types::{ + Error, HexpireResult, HpersistResult, HsetexCondition, HsetexFieldCondition, Integer, RespValue, +}; +use crate::value::FromValue; + +/// `FIELDS numfields field [ field ... ]`, e.g. for HEXPIRE, a field and its value counts as one. +pub struct Fields(pub NonEmpty); + +impl IntoCommand for Fields { + fn into_command(self) -> Result, Error> { + ("FIELDS", NumKeys(self.0)).into_command() + } +} + +/// Also `HexpireatResult`, `HpexpireResult` and `HpexpireatResult`, the same type. +impl FromValue for HexpireResult { + fn from_value(value: RespValue) -> Result { + match Integer::from_value(value)? { + -2 => Ok(Self::DoesNotExist), + 0 => Ok(Self::ConditionNotMet), + 1 => Ok(Self::Applied), + 2 => Ok(Self::ZeroSeconds), + code => Err(Error::Client(format!("unexpected reply: {code}"))), + } + } +} + +impl FromValue for HpersistResult { + fn from_value(value: RespValue) -> Result { + match Integer::from_value(value)? { + -2 => Ok(Self::DoesNotExist), + -1 => Ok(Self::NoExpiration), + 1 => Ok(Self::Removed), + code => Err(Error::Client(format!("unexpected reply: {code}"))), + } + } +} + +impl IntoCommand for HsetexCondition { + fn into_command(self) -> Result, Error> { + match self { + HsetexCondition::Nx => "NX", + HsetexCondition::Xx => "XX", + } + .into_command() + } +} + +impl IntoCommand for HsetexFieldCondition { + fn into_command(self) -> Result, Error> { + match self { + HsetexFieldCondition::Fnx => "FNX", + HsetexFieldCondition::Fxx => "FXX", + } + .into_command() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{from, from_error, int, words, words_error}; + + #[test] + fn fields() { + assert_eq!( + words(Fields(NonEmpty::new("fields", vec!["a", "b"]))), + ["FIELDS", "2", "a", "b"] + ); + // a field and its value counts as one + assert_eq!( + words(Fields(NonEmpty::new( + "fields", + vec![("f1", "v1"), ("f2", "v2")] + ))), + ["FIELDS", "2", "f1", "v1", "f2", "v2"] + ); + assert_eq!( + words_error(Fields(NonEmpty::new("fields", Vec::::new()))), + "fields requires at least one item" + ); + } + + #[test] + fn hexpire_result() { + assert_eq!(from::(int(-2)), HexpireResult::DoesNotExist); + assert_eq!( + from::(int(0)), + HexpireResult::ConditionNotMet + ); + assert_eq!(from::(int(1)), HexpireResult::Applied); + assert_eq!(from::(int(2)), HexpireResult::ZeroSeconds); + assert_eq!(from_error::(int(-1)), "unexpected reply: -1"); + } + + #[test] + fn hpersist_result() { + assert_eq!( + from::(int(-2)), + HpersistResult::DoesNotExist + ); + assert_eq!( + from::(int(-1)), + HpersistResult::NoExpiration + ); + assert_eq!(from::(int(1)), HpersistResult::Removed); + assert_eq!(from_error::(int(0)), "unexpected reply: 0"); + } + + #[test] + fn conditions() { + assert_eq!( + words([HsetexCondition::Nx, HsetexCondition::Xx]), + ["NX", "XX"] + ); + assert_eq!( + words([HsetexFieldCondition::Fnx, HsetexFieldCondition::Fxx]), + ["FNX", "FXX"] + ); + } +} diff --git a/crates/componentized-valkey/src/commands/keyspace.rs b/crates/componentized-valkey/src/commands/keyspace.rs new file mode 100644 index 0000000..6533e52 --- /dev/null +++ b/crates/componentized-valkey/src/commands/keyspace.rs @@ -0,0 +1,196 @@ +//! The keyspace commands: MIGRATE, SCAN, SORT, TYPE and the like. + +use crate::command::{Flag, IntoCommand, NamedOption, NonEmpty}; +use crate::types::{Error, KeyType, MigrateAuth, RespValue, SortOpts}; +use crate::value::{FromValue, unexpected}; + +/// The name of the type, e.g. for SCAN's TYPE. +impl IntoCommand for KeyType { + fn into_command(self) -> Result, Error> { + match self { + KeyType::String => "string".to_string(), + KeyType::List => "list".to_string(), + KeyType::Set => "set".to_string(), + KeyType::Zset => "zset".to_string(), + KeyType::Hash => "hash".to_string(), + KeyType::Stream => "stream".to_string(), + KeyType::Other(name) => name, + } + .into_command() + } +} + +/// The type of a key, by name, none for `none`, a key that does not exist. +pub struct OptionalKeyType(pub Option); + +impl FromValue for OptionalKeyType { + fn from_value(value: RespValue) -> Result { + // `none` rather than null, though the reply schema has null + let name: Option = FromValue::from_value(value)?; + Ok(OptionalKeyType(match name.as_deref() { + None | Some("none") => None, + Some("string") => Some(KeyType::String), + Some("list") => Some(KeyType::List), + Some("set") => Some(KeyType::Set), + Some("zset") => Some(KeyType::Zset), + Some("hash") => Some(KeyType::Hash), + Some("stream") => Some(KeyType::Stream), + Some(_) => name.map(KeyType::Other), + })) + } +} + +impl IntoCommand for MigrateAuth { + fn into_command(self) -> Result, Error> { + match self { + MigrateAuth::Password(password) => ("AUTH", password).into_command(), + MigrateAuth::UsernamePassword((username, password)) => { + ("AUTH2", username, password).into_command() + } + } + } +} + +/// `OK` when keys were moved, `NOKEY` when none of them exist. +pub struct Migrated(pub bool); + +impl FromValue for Migrated { + fn from_value(value: RespValue) -> Result { + match &value { + RespValue::String(reply) if reply == "OK" => Ok(Migrated(true)), + RespValue::String(reply) if reply == "NOKEY" => Ok(Migrated(false)), + _ => Err(unexpected(&value)), + } + } +} + +/// `[ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ]`, also +/// `SortRoOpts`, the same type. +impl IntoCommand for SortOpts { + fn into_command(self) -> Result, Error> { + // GET before each pattern + let get = self.get.map(|patterns| { + let patterns = patterns.into_iter().map(|pattern| ("GET", pattern)); + patterns.collect::>() + }); + ( + NamedOption("BY", self.by), + self.limit, + NonEmpty::optional("get", get), + self.order, + Flag("ALPHA", self.alpha), + ) + .into_command() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{bulk, from, from_error, ok, simple, words, words_error}; + use crate::types::{Limit, SortOrder}; + + #[test] + fn key_type() { + assert_eq!( + words(vec![ + KeyType::String, + KeyType::List, + KeyType::Set, + KeyType::Zset, + KeyType::Hash, + KeyType::Stream, + KeyType::Other("ReJSON-RL".to_string()), + ]), + [ + "string", + "list", + "set", + "zset", + "hash", + "stream", + "ReJSON-RL" + ] + ); + } + + #[test] + fn optional_key_type() { + let names = [ + ("string", KeyType::String), + ("list", KeyType::List), + ("set", KeyType::Set), + ("zset", KeyType::Zset), + ("hash", KeyType::Hash), + ("stream", KeyType::Stream), + ("ReJSON-RL", KeyType::Other("ReJSON-RL".to_string())), + ]; + for (name, expected) in names { + assert_eq!(from::(simple(name)).0, Some(expected)); + } + assert_eq!(from::(simple("none")).0, None); + assert_eq!(from::(RespValue::Null).0, None); + } + + #[test] + fn migrate_auth() { + assert_eq!( + words(MigrateAuth::Password("secret".to_string())), + ["AUTH", "secret"] + ); + assert_eq!( + words(MigrateAuth::UsernamePassword(( + "user".to_string(), + "secret".to_string() + ))), + ["AUTH2", "user", "secret"] + ); + } + + #[test] + fn migrated() { + assert!(from::(ok()).0); + assert!(!from::(simple("NOKEY")).0); + assert_eq!( + from_error::(bulk("OK")), + r#"unexpected reply: RespValue::BulkString("OK")"# + ); + } + + #[test] + fn sort_opts() { + let opts = SortOpts { + by: Some("weight_*".to_string()), + limit: Some(Limit { + offset: 0, + count: 10, + }), + get: Some(vec!["#".to_string(), "object_*".to_string()]), + order: Some(SortOrder::Desc), + alpha: true, + }; + assert_eq!( + words(opts), + [ + "BY", "weight_*", "LIMIT", "0", "10", "GET", "#", "GET", "object_*", "DESC", + "ALPHA" + ] + ); + let opts = SortOpts { + by: None, + limit: None, + get: None, + order: None, + alpha: false, + }; + assert_eq!(words(opts), Vec::::new()); + let opts = SortOpts { + by: None, + limit: None, + get: Some(vec![]), + order: None, + alpha: false, + }; + assert_eq!(words_error(opts), "get requires at least one item"); + } +} diff --git a/crates/componentized-valkey/src/commands/list.rs b/crates/componentized-valkey/src/commands/list.rs new file mode 100644 index 0000000..0b4601a --- /dev/null +++ b/crates/componentized-valkey/src/commands/list.rs @@ -0,0 +1,61 @@ +//! The list commands: LINSERT, LMOVE, LMPOP and the like. + +use crate::command::IntoCommand; +use crate::types::{Error, Integer, LinsertPosition, LinsertResult, ListSide, RespValue}; +use crate::value::FromValue; + +impl IntoCommand for ListSide { + fn into_command(self) -> Result, Error> { + match self { + ListSide::Left => "LEFT", + ListSide::Right => "RIGHT", + } + .into_command() + } +} + +impl IntoCommand for LinsertPosition { + fn into_command(self) -> Result, Error> { + match self { + LinsertPosition::Before => "BEFORE", + LinsertPosition::After => "AFTER", + } + .into_command() + } +} + +/// The length of the list after the insert, `0` when the key does not exist, `-1` when the pivot +/// was not found. +impl FromValue for LinsertResult { + fn from_value(value: RespValue) -> Result { + match Integer::from_value(value)? { + -1 => Ok(Self::PivotNotFound), + 0 => Ok(Self::NoSuchKey), + length if length > 0 => Ok(Self::Inserted(length)), + code => Err(Error::Client(format!("unexpected reply: {code}"))), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{from, from_error, int, words}; + + #[test] + fn keywords() { + assert_eq!(words([ListSide::Left, ListSide::Right]), ["LEFT", "RIGHT"]); + assert_eq!( + words([LinsertPosition::Before, LinsertPosition::After]), + ["BEFORE", "AFTER"] + ); + } + + #[test] + fn linsert_result() { + assert_eq!(from::(int(-1)), LinsertResult::PivotNotFound); + assert_eq!(from::(int(0)), LinsertResult::NoSuchKey); + assert_eq!(from::(int(4)), LinsertResult::Inserted(4)); + assert_eq!(from_error::(int(-2)), "unexpected reply: -2"); + } +} diff --git a/crates/componentized-valkey/src/commands/mod.rs b/crates/componentized-valkey/src/commands/mod.rs new file mode 100644 index 0000000..b452557 --- /dev/null +++ b/crates/componentized-valkey/src/commands/mod.rs @@ -0,0 +1,16 @@ +//! The conversions of the package's types, [`IntoCommand`](crate::IntoCommand) for those in a +//! command and [`FromValue`](crate::FromValue) for those in a reply, by the commands they are for, +//! as the sections of the `types` interface. + +pub mod admin; +pub mod bitmap; +pub mod connection; +pub mod geo; +pub mod hash; +pub mod keyspace; +pub mod list; +pub mod scripting; +pub mod shared; +pub mod sortedset; +pub mod stream; +pub mod string; diff --git a/crates/componentized-valkey/src/commands/scripting.rs b/crates/componentized-valkey/src/commands/scripting.rs new file mode 100644 index 0000000..9b4eda0 --- /dev/null +++ b/crates/componentized-valkey/src/commands/scripting.rs @@ -0,0 +1,225 @@ +//! The scripting commands: EVAL, FCALL, FUNCTION and SCRIPT, and the FLUSH modes they share. + +use crate::command::{IntoCommand, NonEmpty}; +use crate::types::{ + Error, FlushMode, FunctionListFunction, FunctionListLibrary, FunctionStatsEngine, + FunctionStatsReply, FunctionStatsRunning, RespValue, +}; +use crate::value::{Fields, FromValue}; + +impl IntoCommand for FlushMode { + fn into_command(self) -> Result, Error> { + match self { + FlushMode::Async => "ASYNC", + FlushMode::Sync => "SYNC", + } + .into_command() + } +} + +/// `numkeys [ key ... ] [ arg ... ]`, e.g. for EVAL, `0` without keys. +pub struct KeysAndArgs(pub Option>, pub Option>); + +impl IntoCommand for KeysAndArgs { + fn into_command(self) -> Result, Error> { + let KeysAndArgs(keys, args) = self; + let numkeys = keys.as_ref().map_or(0, Vec::len); + ( + numkeys, + NonEmpty::optional("keys", keys), + NonEmpty::optional("args", args), + ) + .into_command() + } +} + +impl FromValue for FunctionListFunction { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + name: fields.required("name")?, + description: fields.required("description")?, + flags: fields.required("flags")?, + }) + } +} + +impl FromValue for FunctionListLibrary { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + library_name: fields.required("library_name")?, + engine: fields.required("engine")?, + functions: fields.required("functions")?, + // only with WITHCODE + library_code: fields.optional("library_code")?, + }) + } +} + +impl FromValue for FunctionStatsRunning { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + name: fields.required("name")?, + command: fields.required("command")?, + duration: fields.required("duration_ms")?, + }) + } +} + +impl FromValue for FunctionStatsEngine { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + libraries_count: fields.required("libraries_count")?, + functions_count: fields.required("functions_count")?, + }) + } +} + +impl FromValue for FunctionStatsReply { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + running_script: fields.required("running_script")?, + engines: fields.required("engines")?, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{array, bulk, bulks, fields, from, from_error, int, words, words_error}; + use crate::types::DurationMilliseconds; + + #[test] + fn flush_mode() { + assert_eq!( + words([FlushMode::Async, FlushMode::Sync]), + ["ASYNC", "SYNC"] + ); + } + + #[test] + fn keys_and_args() { + assert_eq!(words(KeysAndArgs(None, None)), ["0"]); + assert_eq!( + words(KeysAndArgs( + Some(vec!["a".to_string(), "b".to_string()]), + Some(vec!["x".to_string()]) + )), + ["2", "a", "b", "x"] + ); + assert_eq!( + words(KeysAndArgs(None, Some(vec!["x".to_string()]))), + ["0", "x"] + ); + assert_eq!( + words_error(KeysAndArgs(Some(vec![]), None)), + "keys requires at least one item" + ); + assert_eq!( + words_error(KeysAndArgs(None, Some(vec![]))), + "args requires at least one item" + ); + } + + #[test] + fn function_list_library() { + let function = |name: &str, description: RespValue| { + fields(vec![ + ("name", bulk(name)), + ("description", description), + ("flags", bulks(&["no-writes"])), + ]) + }; + let value = fields(vec![ + ("library_name", bulk("mylib")), + ("engine", bulk("LUA")), + ( + "functions", + array(vec![ + function("first", bulk("the first")), + function("second", RespValue::Null), + ]), + ), + ]); + let expected = FunctionListLibrary { + library_name: "mylib".to_string(), + engine: "LUA".to_string(), + functions: vec![ + FunctionListFunction { + name: "first".to_string(), + description: Some("the first".to_string()), + flags: vec!["no-writes".to_string()], + }, + FunctionListFunction { + name: "second".to_string(), + description: None, + flags: vec!["no-writes".to_string()], + }, + ], + library_code: None, + }; + assert_eq!(from::(value), expected); + // WITHCODE + let value = fields(vec![ + ("library_name", bulk("mylib")), + ("engine", bulk("LUA")), + ("functions", array(vec![])), + ("library_code", bulk("#!lua name=mylib")), + ]); + assert_eq!( + from::(value), + FunctionListLibrary { + functions: vec![], + library_code: Some("#!lua name=mylib".to_string()), + ..expected + } + ); + } + + #[test] + fn function_stats_reply() { + let engines = fields(vec![( + "LUA", + fields(vec![ + ("libraries_count", int(2)), + ("functions_count", int(3)), + ]), + )]); + let value = fields(vec![ + ("running_script", RespValue::Null), + ("engines", engines.clone()), + ]); + let stats = from::(value); + assert_eq!(stats.running_script, None); + assert_eq!( + stats.engines.get("LUA"), + Some(&FunctionStatsEngine { + libraries_count: 2, + functions_count: 3, + }) + ); + let running = fields(vec![ + ("name", bulk("slow")), + ("command", bulks(&["FCALL", "slow", "0"])), + ("duration_ms", int(1500)), + ]); + let value = fields(vec![("running_script", running), ("engines", engines)]); + assert_eq!( + from::(value).running_script, + Some(FunctionStatsRunning { + name: "slow".to_string(), + command: vec!["FCALL".to_string(), "slow".to_string(), "0".to_string()], + duration: DurationMilliseconds { px: 1500 }, + }) + ); + assert_eq!( + from_error::(fields(vec![("engines", fields(vec![]))])), + "missing field: running_script" + ); + } +} diff --git a/crates/componentized-valkey/src/commands/shared.rs b/crates/componentized-valkey/src/commands/shared.rs new file mode 100644 index 0000000..7394236 --- /dev/null +++ b/crates/componentized-valkey/src/commands/shared.rs @@ -0,0 +1,309 @@ +//! Shared by the commands: times, cursors, the expiration of keys and fields, limits and sort order. + +use crate::command::IntoCommand; +use crate::types::{ + Cursor, DurationMilliseconds, DurationSeconds, Error, ExpiretimeReply, Integer, Limit, + PexpiretimeReply, PttlReply, RespValue, SortOrder, TtlMode, TtlReply, UnixTimeMilliseconds, + UnixTimeSeconds, +}; +use crate::value::FromValue; + +/// Whole seconds, e.g. for EX. +impl IntoCommand for DurationSeconds { + fn into_command(self) -> Result, Error> { + self.ex.into_command() + } +} + +/// Whole seconds, never negative. +impl FromValue for DurationSeconds { + fn from_value(value: RespValue) -> Result { + u64::from_value(value).map(|ex| DurationSeconds { ex }) + } +} + +/// Whole milliseconds, e.g. for PX. +impl IntoCommand for DurationMilliseconds { + fn into_command(self) -> Result, Error> { + self.px.into_command() + } +} + +/// Whole milliseconds, never negative. +impl FromValue for DurationMilliseconds { + fn from_value(value: RespValue) -> Result { + u64::from_value(value).map(|px| DurationMilliseconds { px }) + } +} + +/// Whole seconds since the Unix epoch, e.g. for EXAT. +impl IntoCommand for UnixTimeSeconds { + fn into_command(self) -> Result, Error> { + self.exat.into_command() + } +} + +/// Whole seconds since the Unix epoch. +impl FromValue for UnixTimeSeconds { + fn from_value(value: RespValue) -> Result { + Integer::from_value(value).map(|exat| UnixTimeSeconds { exat }) + } +} + +/// Whole milliseconds since the Unix epoch, e.g. for PXAT. +impl IntoCommand for UnixTimeMilliseconds { + fn into_command(self) -> Result, Error> { + self.pxat.into_command() + } +} + +/// Whole milliseconds since the Unix epoch. +impl FromValue for UnixTimeMilliseconds { + fn from_value(value: RespValue) -> Result { + Integer::from_value(value).map(|pxat| UnixTimeMilliseconds { pxat }) + } +} + +/// Milliseconds as seconds with a fraction, e.g. the timeout of BZPOPMIN, `1500` as `1.500`. +pub struct DecimalSeconds(pub DurationMilliseconds); + +impl IntoCommand for DecimalSeconds { + fn into_command(self) -> Result, Error> { + let milliseconds = self.0.px; + Ok(vec![format!( + "{}.{:03}", + milliseconds / 1_000, + milliseconds % 1_000 + )]) + } +} + +/// `0` to start, failing to resume from `0`, which would restart the scan. +impl IntoCommand for Cursor { + fn into_command(self) -> Result, Error> { + match self { + Cursor::Start => "0".into_command(), + Cursor::Resume(cursor) if cursor == "0" => Err(Error::Client( + "resume requires the cursor a scan returned, use start to begin a scan".to_string(), + )), + Cursor::Resume(cursor) => cursor.into_command(), + } + } +} + +/// The cursor a scan returns to resume from, none once the scan is complete, `0`. +pub struct NextCursor(pub Option); + +impl FromValue for NextCursor { + fn from_value(value: RespValue) -> Result { + let cursor = String::from_value(value)?; + Ok(NextCursor( + (cursor != "0").then_some(Cursor::Resume(cursor)), + )) + } +} + +impl IntoCommand for TtlMode { + fn into_command(self) -> Result, Error> { + match self { + TtlMode::Nx => "NX", + TtlMode::Xx => "XX", + TtlMode::Gt => "GT", + TtlMode::Lt => "LT", + } + .into_command() + } +} + +/// The codes of TTL, HTTL and the like, `-2` when the key or field does not exist, `-1` when it has +/// no expiration, otherwise the time. +macro_rules! from_value_ttl_replies { + ($($variant:ident($time:ident) => $case:ident),*) => { + $( + impl FromValue for $variant { + fn from_value(value: RespValue) -> Result { + match value { + RespValue::Integer(-2) => Ok($variant::DoesNotExist), + RespValue::Integer(-1) => Ok($variant::NoExpiration), + value => $time::from_value(value).map($variant::$case), + } + } + } + )* + }; +} + +from_value_ttl_replies!( + TtlReply(DurationSeconds) => ExpiresIn, + PttlReply(DurationMilliseconds) => ExpiresIn, + ExpiretimeReply(UnixTimeSeconds) => ExpiresAt, + PexpiretimeReply(UnixTimeMilliseconds) => ExpiresAt +); + +/// `LIMIT offset count` +impl IntoCommand for Limit { + fn into_command(self) -> Result, Error> { + ("LIMIT", self.offset, self.count).into_command() + } +} + +impl IntoCommand for SortOrder { + fn into_command(self) -> Result, Error> { + match self { + SortOrder::Asc => "ASC", + SortOrder::Desc => "DESC", + } + .into_command() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{bulk, from, from_error, int, words, words_error}; + + #[test] + fn durations() { + assert_eq!(words(DurationSeconds { ex: 10 }), ["10"]); + assert_eq!(words(DurationMilliseconds { px: 1500 }), ["1500"]); + assert_eq!(from::(int(10)), DurationSeconds { ex: 10 }); + assert_eq!( + from::(int(1500)), + DurationMilliseconds { px: 1500 } + ); + assert_eq!( + from_error::(int(-1)), + "unexpected negative reply: -1" + ); + assert_eq!( + from_error::(int(-1)), + "unexpected negative reply: -1" + ); + } + + #[test] + fn unix_times() { + assert_eq!( + words(UnixTimeSeconds { + exat: 1_700_000_000 + }), + ["1700000000"] + ); + assert_eq!( + words(UnixTimeMilliseconds { + pxat: 1_700_000_000_123 + }), + ["1700000000123"] + ); + assert_eq!( + from::(int(1_700_000_000)), + UnixTimeSeconds { + exat: 1_700_000_000 + } + ); + assert_eq!( + from::(int(1_700_000_000_123)), + UnixTimeMilliseconds { + pxat: 1_700_000_000_123 + } + ); + } + + #[test] + fn decimal_seconds() { + assert_eq!( + words(DecimalSeconds(DurationMilliseconds { px: 1500 })), + ["1.500"] + ); + assert_eq!( + words(DecimalSeconds(DurationMilliseconds { px: 5 })), + ["0.005"] + ); + assert_eq!( + words(DecimalSeconds(DurationMilliseconds { px: 0 })), + ["0.000"] + ); + assert_eq!( + words(DecimalSeconds(DurationMilliseconds { px: 61_010 })), + ["61.010"] + ); + } + + #[test] + fn cursor() { + assert_eq!(words(Cursor::Start), ["0"]); + assert_eq!(words(Cursor::Resume("17".to_string())), ["17"]); + assert_eq!( + words_error(Cursor::Resume("0".to_string())), + "resume requires the cursor a scan returned, use start to begin a scan" + ); + } + + #[test] + fn next_cursor() { + assert_eq!(from::(bulk("0")).0, None); + assert_eq!( + from::(bulk("17")).0, + Some(Cursor::Resume("17".to_string())) + ); + } + + #[test] + fn ttl_mode() { + assert_eq!( + words([TtlMode::Nx, TtlMode::Xx, TtlMode::Gt, TtlMode::Lt]), + ["NX", "XX", "GT", "LT"] + ); + } + + #[test] + fn ttl_replies() { + assert_eq!(from::(int(-2)), TtlReply::DoesNotExist); + assert_eq!(from::(int(-1)), TtlReply::NoExpiration); + assert_eq!( + from::(int(30)), + TtlReply::ExpiresIn(DurationSeconds { ex: 30 }) + ); + assert_eq!( + from_error::(int(-3)), + "unexpected negative reply: -3" + ); + assert_eq!(from::(int(-2)), PttlReply::DoesNotExist); + assert_eq!( + from::(int(1500)), + PttlReply::ExpiresIn(DurationMilliseconds { px: 1500 }) + ); + assert_eq!( + from::(int(-1)), + ExpiretimeReply::NoExpiration + ); + assert_eq!( + from::(int(1_700_000_000)), + ExpiretimeReply::ExpiresAt(UnixTimeSeconds { + exat: 1_700_000_000 + }) + ); + assert_eq!( + from::(int(1_700_000_000_123)), + PexpiretimeReply::ExpiresAt(UnixTimeMilliseconds { + pxat: 1_700_000_000_123 + }) + ); + } + + #[test] + fn limit() { + assert_eq!( + words(Limit { + offset: 0, + count: 10 + }), + ["LIMIT", "0", "10"] + ); + } + + #[test] + fn sort_order() { + assert_eq!(words([SortOrder::Asc, SortOrder::Desc]), ["ASC", "DESC"]); + } +} diff --git a/crates/componentized-valkey/src/commands/sortedset.rs b/crates/componentized-valkey/src/commands/sortedset.rs new file mode 100644 index 0000000..34c1e38 --- /dev/null +++ b/crates/componentized-valkey/src/commands/sortedset.rs @@ -0,0 +1,256 @@ +//! The sorted set commands: ZADD, ZINTER, ZMPOP, ZRANGE, ZUNION and the like. + +use crate::command::{Flag, IntoCommand}; +use crate::types::{ + Double, Error, LexBound, Limit, RespValue, ScoreBound, ZaddComparison, ZaddCondition, ZmpopEnd, + ZrangeBy, ZsetAggregate, +}; +use crate::value::{FromValue, items}; + +/// The start and stop of a range of scores, as given, the start is the maximum with REV. +pub struct ScoreRange { + pub start: ScoreBound, + pub stop: ScoreBound, + pub rev: bool, +} + +impl IntoCommand for ScoreRange { + fn into_command(self) -> Result, Error> { + let bound = |bound, unbounded: &str| match bound { + ScoreBound::Inclusive(score) => score.to_string(), + ScoreBound::Exclusive(score) => format!("({score}"), + ScoreBound::Unbounded => unbounded.to_string(), + }; + let (low, high) = ("-inf", "+inf"); + let (start, stop) = if self.rev { (high, low) } else { (low, high) }; + Ok(vec![bound(self.start, start), bound(self.stop, stop)]) + } +} + +/// The start and stop of a range of members, as given, the start is the maximum with REV. +pub struct LexRange { + pub start: LexBound, + pub stop: LexBound, + pub rev: bool, +} + +impl IntoCommand for LexRange { + fn into_command(self) -> Result, Error> { + let bound = |bound, unbounded: &str| match bound { + LexBound::Inclusive(member) => format!("[{member}"), + LexBound::Exclusive(member) => format!("({member}"), + LexBound::Unbounded => unbounded.to_string(), + }; + let (low, high) = ("-", "+"); + let (start, stop) = if self.rev { (high, low) } else { (low, high) }; + Ok(vec![bound(self.start, start), bound(self.stop, stop)]) + } +} + +/// ZRANGE and ZRANGESTORE, `start stop [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ]`. +pub struct Zrange { + pub range: ZrangeBy, + pub rev: bool, + pub limit: Option, +} + +impl IntoCommand for Zrange { + fn into_command(self) -> Result, Error> { + let Zrange { range, rev, limit } = self; + let (range, by) = match range { + ZrangeBy::Rank((start, stop)) => ((start, stop).into_command()?, None), + ZrangeBy::Score((start, stop)) => ( + ScoreRange { start, stop, rev }.into_command()?, + Some("BYSCORE"), + ), + ZrangeBy::Lex((start, stop)) => { + (LexRange { start, stop, rev }.into_command()?, Some("BYLEX")) + } + }; + (range, by, Flag("REV", rev), limit).into_command() + } +} + +impl IntoCommand for ZsetAggregate { + fn into_command(self) -> Result, Error> { + let aggregate = match self { + ZsetAggregate::Sum => "SUM", + ZsetAggregate::Min => "MIN", + ZsetAggregate::Max => "MAX", + }; + ("AGGREGATE", aggregate).into_command() + } +} + +/// Also `BzmpopEnd`, the same type. +impl IntoCommand for ZmpopEnd { + fn into_command(self) -> Result, Error> { + match self { + ZmpopEnd::Min => "MIN", + ZmpopEnd::Max => "MAX", + } + .into_command() + } +} + +impl IntoCommand for ZaddCondition { + fn into_command(self) -> Result, Error> { + match self { + ZaddCondition::Nx => "NX", + ZaddCondition::Xx => "XX", + } + .into_command() + } +} + +impl IntoCommand for ZaddComparison { + fn into_command(self) -> Result, Error> { + match self { + ZaddComparison::Gt => "GT", + ZaddComparison::Lt => "LT", + } + .into_command() + } +} + +/// Members and their scores, e.g. from ZPOPMIN, which replies with one pair, or none, without a +/// count, and a list of pairs with one. +pub struct ScoredMembers(pub Vec<(String, Double)>); + +impl FromValue for ScoredMembers { + fn from_value(value: RespValue) -> Result { + let items = items(value)?; + match items.first() { + None => Ok(ScoredMembers(vec![])), + Some(RespValue::Array(_)) => items + .into_iter() + .map(FromValue::from_value) + .collect::>() + .map(ScoredMembers), + Some(_) => match <[RespValue; 2]>::try_from(items) { + Ok([member, score]) => Ok(ScoredMembers(vec![( + String::from_value(member)?, + Double::from_value(score)?, + )])), + Err(items) => Err(Error::Client(format!( + "unexpected number of items, expected a member and its score: {}", + items.len() + ))), + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{array, bulk, bulks, double, from, from_error, words}; + + #[test] + fn score_range() { + let range = |start, stop, rev| ScoreRange { start, stop, rev }; + assert_eq!( + words(range( + ScoreBound::Inclusive(1.0), + ScoreBound::Exclusive(2.5), + false + )), + ["1", "(2.5"] + ); + assert_eq!( + words(range(ScoreBound::Unbounded, ScoreBound::Unbounded, false)), + ["-inf", "+inf"] + ); + // with REV, the start is the maximum + assert_eq!( + words(range(ScoreBound::Unbounded, ScoreBound::Unbounded, true)), + ["+inf", "-inf"] + ); + } + + #[test] + fn lex_range() { + let range = |start, stop, rev| LexRange { start, stop, rev }; + assert_eq!( + words(range( + LexBound::Inclusive("a".to_string()), + LexBound::Exclusive("c".to_string()), + false + )), + ["[a", "(c"] + ); + assert_eq!( + words(range(LexBound::Unbounded, LexBound::Unbounded, false)), + ["-", "+"] + ); + assert_eq!( + words(range(LexBound::Unbounded, LexBound::Unbounded, true)), + ["+", "-"] + ); + } + + #[test] + fn zrange() { + let by_rank = Zrange { + range: ZrangeBy::Rank((0, -1)), + rev: false, + limit: None, + }; + assert_eq!(words(by_rank), ["0", "-1"]); + let by_score = Zrange { + range: ZrangeBy::Score((ScoreBound::Unbounded, ScoreBound::Exclusive(1.0))), + rev: true, + limit: Some(Limit { + offset: 0, + count: 5, + }), + }; + assert_eq!( + words(by_score), + ["+inf", "(1", "BYSCORE", "REV", "LIMIT", "0", "5"] + ); + let by_lex = Zrange { + range: ZrangeBy::Lex((LexBound::Inclusive("a".to_string()), LexBound::Unbounded)), + rev: false, + limit: None, + }; + assert_eq!(words(by_lex), ["[a", "+", "BYLEX"]); + } + + #[test] + fn keywords() { + assert_eq!( + words([ZsetAggregate::Sum, ZsetAggregate::Min, ZsetAggregate::Max]), + ["AGGREGATE", "SUM", "AGGREGATE", "MIN", "AGGREGATE", "MAX"] + ); + assert_eq!(words([ZmpopEnd::Min, ZmpopEnd::Max]), ["MIN", "MAX"]); + assert_eq!(words([ZaddCondition::Nx, ZaddCondition::Xx]), ["NX", "XX"]); + assert_eq!( + words([ZaddComparison::Gt, ZaddComparison::Lt]), + ["GT", "LT"] + ); + } + + #[test] + fn scored_members() { + assert_eq!(from::(array(vec![])).0, []); + // one pair, flat + assert_eq!( + from::(array(vec![bulk("a"), double(1.5)])).0, + [("a".to_string(), 1.5)] + ); + // a list of pairs, the scores as doubles or, with RESP2, bulk strings + assert_eq!( + from::(array(vec![ + array(vec![bulk("a"), double(1.0)]), + bulks(&["b", "2"]), + ])) + .0, + [("a".to_string(), 1.0), ("b".to_string(), 2.0)] + ); + assert_eq!( + from_error::(bulks(&["a", "1", "b"])), + "unexpected number of items, expected a member and its score: 3" + ); + } +} diff --git a/crates/componentized-valkey/src/commands/stream.rs b/crates/componentized-valkey/src/commands/stream.rs new file mode 100644 index 0000000..32a4821 --- /dev/null +++ b/crates/componentized-valkey/src/commands/stream.rs @@ -0,0 +1,509 @@ +//! The stream commands: XADD, XAUTOCLAIM, XCLAIM, XINFO, XPENDING, XTRIM and the like. + +use crate::command::{Flag, IntoCommand, NamedOption}; +use crate::types::{ + DurationMilliseconds, Error, RespValue, StreamId, StreamTrim, StreamTrimEfficiency, + UnixTimeMilliseconds, XclaimOpts, XinfoConsumersEntry, XinfoGroupsEntry, XinfoStreamReply, + XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, + XpendingExtendedEntry, XpendingSummary, +}; +use crate::value::{Fields, FromValue, OrNever}; + +impl IntoCommand for StreamTrim { + fn into_command(self) -> Result, Error> { + //〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] + match self { + StreamTrim::Maxlen(trim) => ( + "MAXLEN", + trim.efficiency, + trim.threshold, + NamedOption("LIMIT", trim.limit), + ) + .into_command(), + StreamTrim::Minid(trim) => ( + "MINID", + trim.efficiency, + trim.threshold, + NamedOption("LIMIT", trim.limit), + ) + .into_command(), + } + } +} + +impl IntoCommand for StreamTrimEfficiency { + fn into_command(self) -> Result, Error> { + // = | ~ + (match self { + StreamTrimEfficiency::Exact => "=", + StreamTrimEfficiency::MearlyExact => "~", + }) + .into_command() + } +} + +impl IntoCommand for XclaimOpts { + fn into_command(self) -> Result, Error> { + // [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] [ LASTID lastid ] + ( + NamedOption("IDLE", self.idle), + NamedOption("TIME", self.time), + NamedOption("RETRYCOUNT", self.retrycount), + Flag("FORCE", self.force), + NamedOption("LASTID", self.lastid), + ) + .into_command() + } +} + +/// A stream ID, or `0-0` for none, e.g. the start of the next XAUTOCLAIM, `0-0` once the scan is +/// complete. +pub struct OrZeroId(pub Option); + +pub const ZERO_ID: &str = "0-0"; + +/// `0-0` for none, e.g. to start XAUTOCLAIM from the beginning. +impl IntoCommand for OrZeroId { + fn into_command(self) -> Result, Error> { + Ok(vec![self.0.unwrap_or_else(|| ZERO_ID.to_string())]) + } +} + +impl FromValue for OrZeroId { + fn from_value(value: RespValue) -> Result { + let id = StreamId::from_value(value)?; + Ok(OrZeroId((id != ZERO_ID).then_some(id))) + } +} + +/// `[count, lowest-id, highest-id, [[consumer, count]...]]`, the IDs and consumers are null when +/// nothing is pending, and each consumer's count is a bulk string. +impl FromValue for XpendingSummary { + fn from_value(value: RespValue) -> Result { + let (count, lowest_id, highest_id, consumers): (_, _, _, Option>) = + FromValue::from_value(value)?; + let consumers = consumers + .unwrap_or_default() + .into_iter() + .map(|(consumer, count)| { + let count = count.parse().map_err(|_| { + Error::Client(format!( + "unexpected pending count, not an integer: {count:?}" + )) + })?; + Ok((consumer, count)) + }) + .collect::>()?; + Ok(Self { + count, + lowest_id, + highest_id, + consumers, + }) + } +} + +/// `[id, consumer, milliseconds-since-delivery, delivery-count]`. +impl FromValue for XpendingExtendedEntry { + fn from_value(value: RespValue) -> Result { + let (id, name, since_delivery, delivery_count) = FromValue::from_value(value)?; + Ok(Self { + id, + name, + since_delivery, + delivery_count, + }) + } +} + +impl FromValue for XinfoConsumersEntry { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + name: fields.required("name")?, + pending: fields.required("pending")?, + idle: fields.required("idle")?, + inactive: fields + .required::>("inactive")? + .0, + }) + } +} + +impl FromValue for XinfoGroupsEntry { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + name: fields.required("name")?, + consumers: fields.required("consumers")?, + pending: fields.required("pending")?, + last_delivered_id: fields.required("last-delivered-id")?, + entries_read: fields.required("entries-read")?, + lag: fields.required("lag")?, + }) + } +} + +impl FromValue for XinfoStreamReply { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + length: fields.required("length")?, + radix_tree_keys: fields.required("radix-tree-keys")?, + radix_tree_nodes: fields.required("radix-tree-nodes")?, + groups: fields.required("groups")?, + last_generated_id: fields.required::("last-generated-id")?.0, + max_deleted_entry_id: fields.required::("max-deleted-entry-id")?.0, + entries_added: fields.required("entries-added")?, + first_entry: fields.required("first-entry")?, + last_entry: fields.required("last-entry")?, + }) + } +} + +impl FromValue for XinfoStreamReplyFull { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + length: fields.required("length")?, + radix_tree_keys: fields.required("radix-tree-keys")?, + radix_tree_nodes: fields.required("radix-tree-nodes")?, + last_generated_id: fields.required::("last-generated-id")?.0, + max_deleted_entry_id: fields.required::("max-deleted-entry-id")?.0, + entries_added: fields.required("entries-added")?, + entries: fields.required("entries")?, + groups: fields.required("groups")?, + }) + } +} + +impl FromValue for XinfoStreamReplyFullGroup { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + name: fields.required("name")?, + last_delivered_id: fields.required("last-delivered-id")?, + entries_read: fields.required("entries-read")?, + lag: fields.required("lag")?, + pel_count: fields.required("pel-count")?, + pending: fields.required("pending")?, + consumers: fields.required("consumers")?, + }) + } +} + +impl FromValue for XinfoStreamReplyFullConsumer { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + name: fields.required("name")?, + seen_time: fields.required("seen-time")?, + active_time: fields + .required::>("active-time")? + .0, + pel_count: fields.required("pel-count")?, + pending: fields.required("pending")?, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{array, bulk, bulks, fields, from, from_error, int, words}; + use crate::types::{StreamTrimMaxlen, StreamTrimMinid}; + + use wit_bindgen::rt::Map; + + fn entry(id: &str, field: &str, value: &str) -> (StreamId, Map) { + ( + id.to_string(), + [(field.to_string(), value.to_string())] + .into_iter() + .collect(), + ) + } + + fn entry_value(id: &str, field: &str, value: &str) -> RespValue { + array(vec![bulk(id), bulks(&[field, value])]) + } + + #[test] + fn stream_trim() { + let maxlen = StreamTrim::Maxlen(StreamTrimMaxlen { + efficiency: Some(StreamTrimEfficiency::MearlyExact), + threshold: 1000, + limit: Some(10), + }); + assert_eq!(words(maxlen), ["MAXLEN", "~", "1000", "LIMIT", "10"]); + let minid = StreamTrim::Minid(StreamTrimMinid { + efficiency: Some(StreamTrimEfficiency::Exact), + threshold: "1-0".to_string(), + limit: None, + }); + assert_eq!(words(minid), ["MINID", "=", "1-0"]); + let minid = StreamTrim::Minid(StreamTrimMinid { + efficiency: None, + threshold: "1-0".to_string(), + limit: None, + }); + assert_eq!(words(minid), ["MINID", "1-0"]); + } + + #[test] + fn xclaim_opts() { + let opts = XclaimOpts { + idle: Some(DurationMilliseconds { px: 5000 }), + time: Some(UnixTimeMilliseconds { + pxat: 1_700_000_000_000, + }), + retrycount: Some(2), + force: true, + lastid: Some("1-0".to_string()), + }; + assert_eq!( + words(opts), + [ + "IDLE", + "5000", + "TIME", + "1700000000000", + "RETRYCOUNT", + "2", + "FORCE", + "LASTID", + "1-0" + ] + ); + let opts = XclaimOpts { + idle: None, + time: None, + retrycount: None, + force: false, + lastid: None, + }; + assert_eq!(words(opts), Vec::::new()); + } + + #[test] + fn or_zero_id() { + assert_eq!(words(OrZeroId(None)), ["0-0"]); + assert_eq!(words(OrZeroId(Some("1-1".to_string()))), ["1-1"]); + assert_eq!(from::(bulk("0-0")).0, None); + assert_eq!(from::(bulk("1-1")).0, Some("1-1".to_string())); + } + + #[test] + fn xpending_summary() { + let value = array(vec![ + int(3), + bulk("1-0"), + bulk("3-0"), + array(vec![bulks(&["alice", "2"]), bulks(&["bob", "1"])]), + ]); + assert_eq!( + from::(value), + XpendingSummary { + count: 3, + lowest_id: Some("1-0".to_string()), + highest_id: Some("3-0".to_string()), + consumers: vec![("alice".to_string(), 2), ("bob".to_string(), 1)], + } + ); + // nothing pending + let value = array(vec![ + int(0), + RespValue::Null, + RespValue::Null, + RespValue::Null, + ]); + assert_eq!( + from::(value), + XpendingSummary { + count: 0, + lowest_id: None, + highest_id: None, + consumers: vec![], + } + ); + let value = array(vec![ + int(1), + bulk("1-0"), + bulk("1-0"), + array(vec![bulks(&["alice", "many"])]), + ]); + assert_eq!( + from_error::(value), + r#"unexpected pending count, not an integer: "many""# + ); + } + + #[test] + fn xpending_extended_entry() { + let value = array(vec![bulk("1-0"), bulk("alice"), int(5000), int(2)]); + assert_eq!( + from::(value), + XpendingExtendedEntry { + id: "1-0".to_string(), + name: "alice".to_string(), + since_delivery: DurationMilliseconds { px: 5000 }, + delivery_count: 2, + } + ); + } + + #[test] + fn xinfo_consumers_entry() { + let consumer = |inactive| { + fields(vec![ + ("name", bulk("alice")), + ("pending", int(1)), + ("idle", int(100)), + ("inactive", inactive), + ]) + }; + let expected = XinfoConsumersEntry { + name: "alice".to_string(), + pending: 1, + idle: DurationMilliseconds { px: 100 }, + inactive: Some(DurationMilliseconds { px: 50 }), + }; + assert_eq!(from::(consumer(int(50))), expected); + // never active + assert_eq!( + from::(consumer(int(-1))), + XinfoConsumersEntry { + inactive: None, + ..expected + } + ); + } + + #[test] + fn xinfo_groups_entry() { + let value = fields(vec![ + ("name", bulk("group")), + ("consumers", int(2)), + ("pending", int(3)), + ("last-delivered-id", bulk("5-0")), + ("entries-read", RespValue::Null), + ("lag", int(1)), + ]); + assert_eq!( + from::(value), + XinfoGroupsEntry { + name: "group".to_string(), + consumers: 2, + pending: 3, + last_delivered_id: "5-0".to_string(), + entries_read: None, + lag: Some(1), + } + ); + } + + #[test] + fn xinfo_stream_reply() { + let value = fields(vec![ + ("length", int(2)), + ("radix-tree-keys", int(1)), + ("radix-tree-nodes", int(2)), + ("groups", int(1)), + ("last-generated-id", bulk("2-0")), + ("max-deleted-entry-id", bulk("0-0")), + ("entries-added", int(2)), + ("recorded-first-entry-id", bulk("1-0")), + ("first-entry", entry_value("1-0", "a", "1")), + ("last-entry", RespValue::Null), + ]); + assert_eq!( + from::(value), + XinfoStreamReply { + length: 2, + radix_tree_keys: 1, + radix_tree_nodes: 2, + groups: 1, + last_generated_id: Some("2-0".to_string()), + max_deleted_entry_id: None, + entries_added: 2, + first_entry: Some(entry("1-0", "a", "1")), + last_entry: None, + } + ); + } + + #[test] + fn xinfo_stream_reply_full() { + let consumer = fields(vec![ + ("name", bulk("alice")), + ("seen-time", int(1_700_000_000_000)), + ("active-time", int(-1)), + ("pel-count", int(1)), + ( + "pending", + array(vec![array(vec![ + bulk("1-0"), + int(1_700_000_000_000), + int(1), + ])]), + ), + ]); + let group = fields(vec![ + ("name", bulk("group")), + ("last-delivered-id", bulk("1-0")), + ("entries-read", int(1)), + ("lag", int(0)), + ("pel-count", int(1)), + ( + "pending", + array(vec![array(vec![ + bulk("1-0"), + bulk("alice"), + int(1_700_000_000_000), + int(1), + ])]), + ), + ("consumers", array(vec![consumer])), + ]); + let value = fields(vec![ + ("length", int(1)), + ("radix-tree-keys", int(1)), + ("radix-tree-nodes", int(2)), + ("last-generated-id", bulk("1-0")), + ("max-deleted-entry-id", bulk("0-0")), + ("entries-added", int(1)), + ("entries", array(vec![entry_value("1-0", "a", "1")])), + ("groups", array(vec![group])), + ]); + let time = UnixTimeMilliseconds { + pxat: 1_700_000_000_000, + }; + assert_eq!( + from::(value), + XinfoStreamReplyFull { + length: 1, + radix_tree_keys: 1, + radix_tree_nodes: 2, + last_generated_id: Some("1-0".to_string()), + max_deleted_entry_id: None, + entries_added: 1, + entries: vec![entry("1-0", "a", "1")], + groups: vec![XinfoStreamReplyFullGroup { + name: "group".to_string(), + last_delivered_id: "1-0".to_string(), + entries_read: Some(1), + lag: Some(0), + pel_count: 1, + pending: vec![("1-0".to_string(), "alice".to_string(), time, 1)], + consumers: vec![XinfoStreamReplyFullConsumer { + name: "alice".to_string(), + seen_time: time, + active_time: None, + pel_count: 1, + pending: vec![("1-0".to_string(), time, 1)], + }], + }], + } + ); + } +} diff --git a/crates/componentized-valkey/src/commands/string.rs b/crates/componentized-valkey/src/commands/string.rs new file mode 100644 index 0000000..9ecb407 --- /dev/null +++ b/crates/componentized-valkey/src/commands/string.rs @@ -0,0 +1,214 @@ +//! The string commands: GETEX, LCS, MSETEX, SET and the like. + +use crate::command::IntoCommand; +use crate::types::{ + Error, GetexTtl, LcsIdxMatch, LcsIdxReply, MsetexPrecondition, RespValue, SetPrecondition, + SetTtl, +}; +use crate::value::{Fields, FromValue, items}; + +/// Also `HgetexTtl`, the same type. +impl IntoCommand for GetexTtl { + fn into_command(self) -> Result, Error> { + match self { + GetexTtl::Ex(duration) => ("EX", duration).into_command(), + GetexTtl::Px(duration) => ("PX", duration).into_command(), + GetexTtl::Exat(unix_time) => ("EXAT", unix_time).into_command(), + GetexTtl::Pxat(unix_time) => ("PXAT", unix_time).into_command(), + GetexTtl::Persist => "PERSIST".into_command(), + } + } +} + +/// A map of the matches and the length of the longest common subsequence. +impl FromValue for LcsIdxReply { + fn from_value(value: RespValue) -> Result { + let mut fields = Fields::from_value(value)?; + Ok(Self { + matches: fields.required("matches")?, + len: fields.required("len")?, + }) + } +} + +/// The inclusive ranges of a match in each key and, with `WITHMATCHLEN`, its length. +impl FromValue for LcsIdxMatch { + fn from_value(value: RespValue) -> Result { + let mut items = items(value)?.into_iter(); + let (Some(key1_range), Some(key2_range), len, None) = + (items.next(), items.next(), items.next(), items.next()) + else { + return Err(Error::Client( + "unexpected LCS match, expected 2 or 3 items".to_string(), + )); + }; + Ok(Self { + key1_range: FromValue::from_value(key1_range)?, + key2_range: FromValue::from_value(key2_range)?, + len: len.map(FromValue::from_value).transpose()?, + }) + } +} + +impl IntoCommand for MsetexPrecondition { + fn into_command(self) -> Result, Error> { + match self { + MsetexPrecondition::Nx => "NX".into_command(), + MsetexPrecondition::Xx => "XX".into_command(), + } + } +} + +impl IntoCommand for SetPrecondition { + fn into_command(self) -> Result, Error> { + match self { + SetPrecondition::Nx => "NX".into_command(), + SetPrecondition::Xx => "XX".into_command(), + SetPrecondition::Ifeq(value) => ("IFEQ", value).into_command(), + SetPrecondition::Ifne(value) => ("IFNE", value).into_command(), + } + } +} + +/// Also `MsetexTtl` and `HsetexTtl`, the same type. +impl IntoCommand for SetTtl { + fn into_command(self) -> Result, Error> { + match self { + SetTtl::Ex(duration) => ("EX", duration).into_command(), + SetTtl::Px(duration) => ("PX", duration).into_command(), + SetTtl::Exat(unix_time) => ("EXAT", unix_time).into_command(), + SetTtl::Pxat(unix_time) => ("PXAT", unix_time).into_command(), + SetTtl::Keepttl => "KEEPTTL".into_command(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{array, fields, from, from_error, int, words}; + use crate::types::{ + DurationMilliseconds, DurationSeconds, UnixTimeMilliseconds, UnixTimeSeconds, + }; + + #[test] + fn getex_ttl() { + assert_eq!( + words(vec![ + GetexTtl::Ex(DurationSeconds { ex: 10 }), + GetexTtl::Px(DurationMilliseconds { px: 1500 }), + GetexTtl::Exat(UnixTimeSeconds { + exat: 1_700_000_000 + }), + GetexTtl::Pxat(UnixTimeMilliseconds { + pxat: 1_700_000_000_123 + }), + GetexTtl::Persist, + ]), + [ + "EX", + "10", + "PX", + "1500", + "EXAT", + "1700000000", + "PXAT", + "1700000000123", + "PERSIST" + ] + ); + } + + #[test] + fn set_ttl() { + assert_eq!( + words(vec![ + SetTtl::Ex(DurationSeconds { ex: 10 }), + SetTtl::Px(DurationMilliseconds { px: 1500 }), + SetTtl::Exat(UnixTimeSeconds { + exat: 1_700_000_000 + }), + SetTtl::Pxat(UnixTimeMilliseconds { + pxat: 1_700_000_000_123 + }), + SetTtl::Keepttl, + ]), + [ + "EX", + "10", + "PX", + "1500", + "EXAT", + "1700000000", + "PXAT", + "1700000000123", + "KEEPTTL" + ] + ); + } + + #[test] + fn preconditions() { + assert_eq!( + words(vec![ + SetPrecondition::Nx, + SetPrecondition::Xx, + SetPrecondition::Ifeq("old".to_string()), + SetPrecondition::Ifne("other".to_string()), + ]), + ["NX", "XX", "IFEQ", "old", "IFNE", "other"] + ); + assert_eq!( + words(vec![MsetexPrecondition::Nx, MsetexPrecondition::Xx]), + ["NX", "XX"] + ); + } + + fn range(start: i64, end: i64) -> RespValue { + array(vec![int(start), int(end)]) + } + + #[test] + fn lcs_idx_reply() { + let value = fields(vec![ + ( + "matches", + array(vec![ + array(vec![range(4, 7), range(5, 8), int(4)]), + array(vec![range(2, 3), range(0, 1)]), + ]), + ), + ("len", int(6)), + ]); + assert_eq!( + from::(value), + LcsIdxReply { + matches: vec![ + LcsIdxMatch { + key1_range: (4, 7), + key2_range: (5, 8), + len: Some(4), + }, + LcsIdxMatch { + key1_range: (2, 3), + key2_range: (0, 1), + len: None, + }, + ], + len: 6, + } + ); + } + + #[test] + fn lcs_idx_match_invalid() { + assert_eq!( + from_error::(array(vec![range(4, 7)])), + "unexpected LCS match, expected 2 or 3 items" + ); + assert_eq!( + from_error::(array(vec![range(4, 7), range(5, 8), int(4), int(1)])), + "unexpected LCS match, expected 2 or 3 items" + ); + } +} diff --git a/crates/componentized-valkey/src/lib.rs b/crates/componentized-valkey/src/lib.rs index 8a400b6..6f44dea 100644 --- a/crates/componentized-valkey/src/lib.rs +++ b/crates/componentized-valkey/src/lib.rs @@ -5,6 +5,7 @@ //! - [`resp`]: RESP serialization of a [`types::RespValue`], to and from bytes. //! - [`command`]: [`IntoCommand`], converting Rust values into the words of a command. //! - [`value`]: [`FromValue`], converting a reply into Rust values. +//! - [`commands`]: both conversions of the package's types, by the commands they are for. //! //! A component's bindings use these types for the package's types interface, so every component //! shares them, e.g. @@ -37,8 +38,12 @@ mod bindings { pub use bindings::componentized::valkey::types; pub mod command; +pub mod commands; pub mod resp; pub mod value; +#[cfg(test)] +mod test_util; + pub use command::IntoCommand; pub use value::FromValue; diff --git a/crates/componentized-valkey/src/test_util.rs b/crates/componentized-valkey/src/test_util.rs new file mode 100644 index 0000000..650acd5 --- /dev/null +++ b/crates/componentized-valkey/src/test_util.rs @@ -0,0 +1,79 @@ +//! Helpers for the tests of the conversions, to build replies and check the outcomes. + +use crate::resp::encode; +use crate::types::{Error, RespValue}; +use crate::{FromValue, IntoCommand}; + +/// The words of a command that converts. +pub fn words(command: impl IntoCommand) -> Vec { + command.into_command().expect("the words of the command") +} + +/// The message of the client error a command fails to convert with. +pub fn words_error(command: impl IntoCommand) -> String { + match command.into_command() { + Err(Error::Client(message)) => message, + result => panic!("expected a client error: {result:?}"), + } +} + +/// The value of a reply that converts. +pub fn from(value: RespValue) -> T { + T::from_value(value).expect("the value of the reply") +} + +/// The message of the client error a reply fails to convert with. +pub fn from_error(value: RespValue) -> String { + match T::from_value(value) { + Err(Error::Client(message)) => message, + Err(error) => panic!("expected a client error: {error:?}"), + Ok(_) => panic!("expected a client error"), + } +} + +/// The simple string `OK`. +pub fn ok() -> RespValue { + simple("OK") +} + +pub fn simple(text: &str) -> RespValue { + RespValue::String(text.to_string()) +} + +pub fn bulk(text: &str) -> RespValue { + RespValue::BulkString(text.to_string()) +} + +pub fn int(value: i64) -> RespValue { + RespValue::Integer(value) +} + +pub fn double(value: f64) -> RespValue { + RespValue::Double(value) +} + +pub fn array(items: Vec) -> RespValue { + RespValue::Array(items.into_iter().map(encode).collect()) +} + +pub fn map(entries: Vec<(RespValue, RespValue)>) -> RespValue { + RespValue::Map( + entries + .into_iter() + .map(|(key, value)| (encode(key), encode(value))) + .collect(), + ) +} + +/// A map of fields by name, e.g. a record in a RESP3 reply. +pub fn fields(entries: Vec<(&str, RespValue)>) -> RespValue { + map(entries + .into_iter() + .map(|(name, value)| (bulk(name), value)) + .collect()) +} + +/// An array of bulk strings. +pub fn bulks(items: &[&str]) -> RespValue { + array(items.iter().map(|item| bulk(item)).collect()) +} diff --git a/crates/componentized-valkey/src/value.rs b/crates/componentized-valkey/src/value.rs index 345472e..ac8e5c9 100644 --- a/crates/componentized-valkey/src/value.rs +++ b/crates/componentized-valkey/src/value.rs @@ -1,20 +1,7 @@ -use crate::types::{ - AclChannelRule, AclCommandRule, AclDbRule, AclDryrunResult, AclKeyRule, AclListEntry, - AclLogEntry, AclRule, AclSelectorRule, AclUserRule, ClientTrackingRedirect, - ClientTrackinginfoReply, ClusterBumpepochResult, ClusterGetslotmigrationsEntry, - ClusterGetslotmigrationsOperation, CommandDocsArgument, CommandDocsArgumentType, - CommandDocsReply, CommandInfoReply, CommandKeySpec, CommandKeySpecBeginSearch, - CommandKeySpecFindKeys, CommandKeySpecKeynum, CommandKeySpecKeyword, CommandKeySpecRange, - CommandlogEntry, Cursor, Double, DurationMilliseconds, DurationSeconds, Error, ExpiretimeReply, - FunctionListFunction, FunctionListLibrary, FunctionStatsEngine, FunctionStatsReply, - FunctionStatsRunning, GeoCoordinates, GeosearchWithResult, HexpireResult, HpersistResult, - Integer, KeyType, LatencyHistogramEntry, LatencyLatestEntry, LcsIdxMatch, LcsIdxReply, - LinsertResult, ModuleListEntry, PexpiretimeReply, PttlReply, RespNestedValue, RespValue, - RolePrimary, RolePrimaryReplica, RoleReplica, RoleReply, StreamId, TtlReply, - UnixTimeMilliseconds, UnixTimeSeconds, XinfoConsumersEntry, XinfoGroupsEntry, XinfoStreamReply, - XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, - XpendingExtendedEntry, XpendingSummary, -}; +//! Converting a reply into Rust values, the impls for Rust's own types and the helpers the commands +//! share. The impls for the package's types are in [`crate::commands`]. + +use crate::types::{Double, Error, Integer, RespNestedValue, RespValue}; use wit_bindgen::rt::Map; @@ -168,34 +155,6 @@ impl Fields { } } -/// Whole seconds, never negative. -impl FromValue for DurationSeconds { - fn from_value(value: RespValue) -> Result { - u64::from_value(value).map(|ex| DurationSeconds { ex }) - } -} - -/// Whole milliseconds, never negative. -impl FromValue for DurationMilliseconds { - fn from_value(value: RespValue) -> Result { - u64::from_value(value).map(|px| DurationMilliseconds { px }) - } -} - -/// Whole seconds since the Unix epoch. -impl FromValue for UnixTimeSeconds { - fn from_value(value: RespValue) -> Result { - Integer::from_value(value).map(|exat| UnixTimeSeconds { exat }) - } -} - -/// Whole milliseconds since the Unix epoch. -impl FromValue for UnixTimeMilliseconds { - fn from_value(value: RespValue) -> Result { - Integer::from_value(value).map(|pxat| UnixTimeMilliseconds { pxat }) - } -} - /// A value, or `-1` for none, e.g. the `active-time` of a consumer that has never been active. pub struct OrNever(pub Option); @@ -208,87 +167,6 @@ impl FromValue for OrNever { } } -/// A stream ID, or `0-0` for none, e.g. the start of the next XAUTOCLAIM, `0-0` once the scan is -/// complete. -pub struct OrZeroId(pub Option); - -pub const ZERO_ID: &str = "0-0"; - -impl FromValue for OrZeroId { - fn from_value(value: RespValue) -> Result { - let id = StreamId::from_value(value)?; - Ok(OrZeroId((id != ZERO_ID).then_some(id))) - } -} - -/// `[count, lowest-id, highest-id, [[consumer, count]...]]`, the IDs and consumers are null when -/// nothing is pending, and each consumer's count is a bulk string. -impl FromValue for XpendingSummary { - fn from_value(value: RespValue) -> Result { - let (count, lowest_id, highest_id, consumers): (_, _, _, Option>) = - FromValue::from_value(value)?; - let consumers = consumers - .unwrap_or_default() - .into_iter() - .map(|(consumer, count)| { - let count = count.parse().map_err(|_| { - Error::Client(format!( - "unexpected pending count, not an integer: {count:?}" - )) - })?; - Ok((consumer, count)) - }) - .collect::>()?; - Ok(Self { - count, - lowest_id, - highest_id, - consumers, - }) - } -} - -/// `[id, consumer, milliseconds-since-delivery, delivery-count]`. -impl FromValue for XpendingExtendedEntry { - fn from_value(value: RespValue) -> Result { - let (id, name, since_delivery, delivery_count) = FromValue::from_value(value)?; - Ok(Self { - id, - name, - since_delivery, - delivery_count, - }) - } -} - -/// Members and their scores, e.g. from ZPOPMIN, which replies with one pair, or none, without a -/// count, and a list of pairs with one. -pub struct ScoredMembers(pub Vec<(String, Double)>); - -impl FromValue for ScoredMembers { - fn from_value(value: RespValue) -> Result { - let items = items(value)?; - match items.first() { - None => Ok(ScoredMembers(vec![])), - Some(RespValue::Array(_)) => items - .into_iter() - .map(FromValue::from_value) - .collect::>() - .map(ScoredMembers), - Some(_) => match <[RespValue; 2]>::try_from(items) { - Ok([member, score]) => Ok(ScoredMembers(vec![( - String::from_value(member)?, - Double::from_value(score)?, - )])), - Err(items) => Err(Error::Client(format!( - "unexpected number of items, expected a member and its score: {}", - items.len() - ))), - }, - } - } -} - /// Pairs in a flat array, `[key, value, key, value...]`, e.g. the members and scores of ZSCAN. pub struct FlatPairs(pub Vec<(K, V)>); @@ -298,1017 +176,219 @@ impl FromValue for FlatPairs { } } -/// The codes of HTTL and the like, `-2` when the field does not exist, `-1` when it has no -/// expiration, otherwise the time. -macro_rules! from_value_field_times { - ($($variant:ident($time:ident) => $case:ident),*) => { - $( - impl FromValue for $variant { - fn from_value(value: RespValue) -> Result { - match value { - RespValue::Integer(-2) => Ok($variant::DoesNotExist), - RespValue::Integer(-1) => Ok($variant::NoExpiration), - value => $time::from_value(value).map($variant::$case), - } - } - } - )* - }; -} - -from_value_field_times!( - TtlReply(DurationSeconds) => ExpiresIn, - PttlReply(DurationMilliseconds) => ExpiresIn, - ExpiretimeReply(UnixTimeSeconds) => ExpiresAt, - PexpiretimeReply(UnixTimeMilliseconds) => ExpiresAt -); - -impl FromValue for HpersistResult { - fn from_value(value: RespValue) -> Result { - match Integer::from_value(value)? { - -2 => Ok(Self::DoesNotExist), - -1 => Ok(Self::NoExpiration), - 1 => Ok(Self::Removed), - code => Err(Error::Client(format!("unexpected reply: {code}"))), - } - } -} - -/// The cursor a scan returns to resume from, none once the scan is complete, `0`. -pub struct NextCursor(pub Option); - -impl FromValue for NextCursor { - fn from_value(value: RespValue) -> Result { - let cursor = String::from_value(value)?; - Ok(NextCursor( - (cursor != "0").then_some(Cursor::Resume(cursor)), - )) - } -} - -/// The length of the list after the insert, `0` when the key does not exist, `-1` when the pivot -/// was not found. -impl FromValue for LinsertResult { - fn from_value(value: RespValue) -> Result { - match Integer::from_value(value)? { - -1 => Ok(Self::PivotNotFound), - 0 => Ok(Self::NoSuchKey), - length if length > 0 => Ok(Self::Inserted(length)), - code => Err(Error::Client(format!("unexpected reply: {code}"))), - } - } -} - -/// `[longitude, latitude]` -impl FromValue for GeoCoordinates { - fn from_value(value: RespValue) -> Result { - let (longitude, latitude) = FromValue::from_value(value)?; - Ok(GeoCoordinates { - longitude, - latitude, - }) - } -} - -/// The member, or with any of WITHDIST, WITHHASH and WITHCOORD, an array of the member then the -/// details, in that order, told apart by their types. -impl FromValue for GeosearchWithResult { - fn from_value(value: RespValue) -> Result { - let mut result = GeosearchWithResult { - member: String::new(), - distance: None, - hash: None, - coordinates: None, - }; - let items = match value { - RespValue::Array(_) => items(value)?, - // without details, the member alone - value => vec![value], - }; - let mut items = items.into_iter(); - result.member = String::from_value(items.next().unwrap_or(RespValue::Null))?; - for item in items { - match item { - // a bulk string, as RESP3 has no other way to send a fixed precision double - RespValue::BulkString(_) => result.distance = Some(Double::from_value(item)?), - RespValue::Integer(hash) => result.hash = Some(hash), - RespValue::Array(_) => result.coordinates = Some(GeoCoordinates::from_value(item)?), - item => return Err(unexpected(&item)), - } - } - Ok(result) - } -} - -impl FromValue for FunctionListFunction { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - Ok(Self { - name: fields.required("name")?, - description: fields.required("description")?, - flags: fields.required("flags")?, - }) - } -} - -impl FromValue for FunctionListLibrary { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - Ok(Self { - library_name: fields.required("library_name")?, - engine: fields.required("engine")?, - functions: fields.required("functions")?, - // only with WITHCODE - library_code: fields.optional("library_code")?, - }) - } -} - -impl FromValue for FunctionStatsRunning { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - Ok(Self { - name: fields.required("name")?, - command: fields.required("command")?, - duration: fields.required("duration_ms")?, - }) - } -} - -impl FromValue for FunctionStatsEngine { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - Ok(Self { - libraries_count: fields.required("libraries_count")?, - functions_count: fields.required("functions_count")?, - }) - } -} - -impl FromValue for FunctionStatsReply { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - Ok(Self { - running_script: fields.required("running_script")?, - engines: fields.required("engines")?, - }) - } -} - -/// The type of a key, by name, none for `none`, a key that does not exist. -pub struct OptionalKeyType(pub Option); - -impl FromValue for OptionalKeyType { - fn from_value(value: RespValue) -> Result { - // `none` rather than null, though the reply schema has null - let name: Option = FromValue::from_value(value)?; - Ok(OptionalKeyType(match name.as_deref() { - None | Some("none") => None, - Some("string") => Some(KeyType::String), - Some("list") => Some(KeyType::List), - Some("set") => Some(KeyType::Set), - Some("zset") => Some(KeyType::Zset), - Some("hash") => Some(KeyType::Hash), - Some("stream") => Some(KeyType::Stream), - Some(_) => name.map(KeyType::Other), - })) - } -} - -/// `OK` when keys were moved, `NOKEY` when none of them exist. -pub struct Migrated(pub bool); - -impl FromValue for Migrated { - fn from_value(value: RespValue) -> Result { - match &value { - RespValue::String(reply) if reply == "OK" => Ok(Migrated(true)), - RespValue::String(reply) if reply == "NOKEY" => Ok(Migrated(false)), - _ => Err(unexpected(&value)), - } - } -} - -/// `OK` when the user may run the command, otherwise why not. -impl FromValue for AclDryrunResult { - fn from_value(value: RespValue) -> Result { - match value { - RespValue::String(reply) if reply == "OK" => Ok(AclDryrunResult::Allowed), - value => String::from_value(value).map(AclDryrunResult::Denied), - } - } -} - -/// The rules of ACL GETUSER: the flags, the hashed passwords, the root selector's rules, then the -/// selectors. -pub struct AclUserRules(pub Vec); - -impl FromValue for AclUserRules { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - let flags: Vec = fields.required("flags")?; - let passwords: Vec = fields.required("passwords")?; - let mut rules: Vec = flags.iter().map(|flag| rule(flag)).collect(); - rules.extend( - passwords - .into_iter() - .map(|hash| AclRule::User(AclUserRule::HashedPassword(hash))), - ); - rules.extend(selector_rules(&mut fields)?.into_iter().map(root_rule)); - let selectors: Vec = fields.required("selectors")?; - for mut selector in selectors { - let selector = selector_rules(&mut selector)?; - rules.push(AclRule::User(AclUserRule::Selector(selector))); - } - Ok(AclUserRules(rules)) - } -} - -/// The rules of a selector, from its `keys`, `channels`, `databases` and `commands` fields, each -/// rules separated by spaces. -/// -/// The channels are preceded by `resetchannels`, as ACL LIST writes them, so the server's -/// `acl-pubsub-default` does not add to them. -fn selector_rules(fields: &mut Fields) -> Result, Error> { - let keys: String = fields.required("keys")?; - let channels: String = fields.required("channels")?; - // since Valkey 9.1 - let databases: Option = fields.optional("databases")?; - let commands: String = fields.required("commands")?; - let mut rules: Vec = keys.split_whitespace().map(key_rule).collect(); - rules.push(AclSelectorRule::Channel(AclChannelRule::Reset)); - rules.extend(channels.split_whitespace().map(channel_rule)); - rules.extend( - databases - .iter() - .flat_map(|databases| databases.split_whitespace().map(db_rule)), - ); - rules.extend(commands.split_whitespace().map(command_rule)); - Ok(rules) -} - -fn key_rule(rule: &str) -> AclSelectorRule { - let key_rule = match rule { - "allkeys" => AclKeyRule::All, - "resetkeys" => AclKeyRule::Reset, - _ => { - if let Some(pattern) = rule.strip_prefix("%RW~") { - AclKeyRule::AddReadWrite(pattern.to_string()) - } else if let Some(pattern) = rule.strip_prefix("%R~") { - AclKeyRule::AddRead(pattern.to_string()) - } else if let Some(pattern) = rule.strip_prefix("%W~") { - AclKeyRule::AddWrite(pattern.to_string()) - } else if let Some(pattern) = rule.strip_prefix('~') { - AclKeyRule::Add(pattern.to_string()) - } else { - return AclSelectorRule::Other(rule.to_string()); - } - } - }; - AclSelectorRule::Keys(key_rule) -} - -fn channel_rule(rule: &str) -> AclSelectorRule { - let channel_rule = match rule { - "allchannels" => AclChannelRule::All, - "resetchannels" => AclChannelRule::Reset, - _ => match rule.strip_prefix('&') { - Some(pattern) => AclChannelRule::Add(pattern.to_string()), - None => return AclSelectorRule::Other(rule.to_string()), - }, - }; - AclSelectorRule::Channel(channel_rule) -} - -fn db_rule(rule: &str) -> AclSelectorRule { - let db_rule = match rule { - "alldbs" => AclDbRule::All, - "resetdbs" => AclDbRule::Reset, - _ => { - let dbs = rule - .strip_prefix("db=") - .map(|dbs| dbs.split(',').map(str::parse).collect::>()); - match dbs { - Some(Ok(dbs)) => AclDbRule::Db(dbs), - _ => return AclSelectorRule::Other(rule.to_string()), - } - } - }; - AclSelectorRule::Db(db_rule) -} - -/// `+config|get` is a subcommand, or a first argument, either way it is sent back as is. -fn command_rule(rule: &str) -> AclSelectorRule { - let command_rule = match rule { - "allcommands" | "+@all" => AclCommandRule::All, - "nocommands" | "-@all" => AclCommandRule::Reset, - _ => { - if let Some(category) = rule.strip_prefix("+@") { - AclCommandRule::AddCategory(category.to_string()) - } else if let Some(category) = rule.strip_prefix("-@") { - AclCommandRule::RemoveCategory(category.to_string()) - } else if let Some(command) = rule.strip_prefix('+') { - AclCommandRule::Add(command.to_string()) - } else if let Some(command) = rule.strip_prefix('-') { - AclCommandRule::Remove(command.to_string()) - } else { - return AclSelectorRule::Other(rule.to_string()); - } - } - }; - AclSelectorRule::Command(command_rule) -} - -/// The lines of ACL LIST, each `user ...`, with selectors as `( ...)`. -pub struct AclList(pub Vec); - -impl FromValue for AclList { - fn from_value(value: RespValue) -> Result { - let lines: Vec = FromValue::from_value(value)?; - lines - .iter() - .map(|line| acl_list_entry(line)) - .collect::>() - .map(AclList) - } -} - -fn acl_list_entry(line: &str) -> Result { - let invalid = || Error::Client(format!("unexpected ACL LIST line: {line:?}")); - let mut words = line - .strip_prefix("user ") - .ok_or_else(invalid)? - .split_whitespace(); - let username = words.next().ok_or_else(invalid)?.to_string(); - let mut rules = vec![]; - while let Some(word) = words.next() { - if !word.starts_with('(') { - rules.push(rule(word)); - continue; - } - // a selector, its rules up to the word that closes it - let mut selector = vec![word]; - while !selector.last().is_some_and(|word| word.ends_with(')')) { - selector.push(words.next().ok_or_else(invalid)?); - } - let selector = selector.join(" "); - let selector = selector - .strip_prefix('(') - .and_then(|selector| selector.strip_suffix(')')) - .ok_or_else(invalid)?; - let selector = selector.split_whitespace().map(selector_rule).collect(); - rules.push(AclRule::User(AclUserRule::Selector(selector))); - } - Ok(AclListEntry { username, rules }) -} - -/// A rule of the user or its root selector, e.g. `on`, `#` or `~app:*`. -fn rule(word: &str) -> AclRule { - let user_rule = match word { - "on" => AclUserRule::On, - "off" => AclUserRule::Off, - "nopass" => AclUserRule::Nopass, - "sanitize-payload" => AclUserRule::SanitizePayload, - "skip-sanitize-payload" => AclUserRule::SkipSanitizePayload, - "clearselectors" => AclUserRule::Clearselectors, - "reset" => AclUserRule::Reset, - _ => { - if let Some(hash) = word.strip_prefix('#') { - AclUserRule::HashedPassword(hash.to_string()) - } else if let Some(hash) = word.strip_prefix('!') { - AclUserRule::RemoveHashedPassword(hash.to_string()) - } else if let Some(password) = word.strip_prefix('>') { - AclUserRule::Password(password.to_string()) - } else if let Some(password) = word.strip_prefix('<') { - AclUserRule::RemovePassword(password.to_string()) - } else { - return root_rule(selector_rule(word)); - } - } - }; - AclRule::User(user_rule) -} - -/// A rule of a selector, e.g. `~app:*`, `&news:*`, `db=0` or `+get`. -fn selector_rule(word: &str) -> AclSelectorRule { - match word { - "allkeys" | "resetkeys" => key_rule(word), - "allchannels" | "resetchannels" => channel_rule(word), - "alldbs" | "resetdbs" => db_rule(word), - "allcommands" | "nocommands" => command_rule(word), - _ if word.starts_with('~') || word.starts_with('%') => key_rule(word), - _ if word.starts_with('&') => channel_rule(word), - _ if word.starts_with("db=") => db_rule(word), - _ if word.starts_with('+') || word.starts_with('-') => command_rule(word), - _ => AclSelectorRule::Other(word.to_string()), - } -} - -/// A rule of the root selector. -fn root_rule(rule: AclSelectorRule) -> AclRule { - match rule { - AclSelectorRule::Keys(rule) => AclRule::Keys(rule), - AclSelectorRule::Channel(rule) => AclRule::Channel(rule), - AclSelectorRule::Command(rule) => AclRule::Command(rule), - AclSelectorRule::Db(rule) => AclRule::Db(rule), - AclSelectorRule::Other(rule) => AclRule::Other(rule), - } -} - -impl FromValue for AclLogEntry { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - Ok(Self { - count: fields.required("count")?, - reason: fields.required("reason")?, - context: fields.required("context")?, - object: fields.required("object")?, - username: fields.required("username")?, - age_seconds: fields.required("age-seconds")?, - client_info: fields.required("client-info")?, - // since Valkey 7.2 - entry_id: fields.optional("entry-id")?, - timestamp_created: fields.optional("timestamp-created")?, - timestamp_last_updated: fields.optional("timestamp-last-updated")?, - }) - } -} - -/// `BUMPED epoch` or `STILL epoch`. -impl FromValue for ClusterBumpepochResult { - fn from_value(value: RespValue) -> Result { - let reply = String::from_value(value)?; - let invalid = || Error::Client(format!("unexpected reply: {reply:?}")); - let (outcome, epoch) = reply.split_once(' ').ok_or_else(invalid)?; - let epoch = epoch.parse().map_err(|_| invalid())?; - match outcome { - "BUMPED" => Ok(ClusterBumpepochResult::Bumped(epoch)), - "STILL" => Ok(ClusterBumpepochResult::Still(epoch)), - _ => Err(invalid()), - } - } -} - -impl FromValue for ClusterGetslotmigrationsOperation { - fn from_value(value: RespValue) -> Result { - match String::from_value(value)?.as_str() { - "IMPORT" => Ok(ClusterGetslotmigrationsOperation::Import), - "EXPORT" => Ok(ClusterGetslotmigrationsOperation::Export), - operation => Err(Error::Client(format!( - "unexpected operation: {operation:?}" - ))), - } - } -} - -/// Ranges of slots, `start-end start-end ...`. -pub struct SlotRanges(pub Vec<(u16, u16)>); - -impl FromValue for SlotRanges { - fn from_value(value: RespValue) -> Result { - let ranges = String::from_value(value)?; - let invalid = || Error::Client(format!("unexpected slot ranges: {ranges:?}")); - ranges - .split_whitespace() - .map(|range| { - let (start, end) = range.split_once('-').ok_or_else(invalid)?; - Ok(( - start.parse().map_err(|_| invalid())?, - end.parse().map_err(|_| invalid())?, - )) - }) - .collect::>() - .map(SlotRanges) - } -} - -impl FromValue for ClusterGetslotmigrationsEntry { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - let slot_ranges: SlotRanges = fields.required("slot_ranges")?; - Ok(Self { - link_name: fields.required("link_name")?, - operation: fields.required("operation")?, - slot_ranges: slot_ranges.0, - target_node: fields.required("target_node")?, - source_node: fields.required("source_node")?, - create_time: fields.required("create_time")?, - last_update_time: fields.required("last_update_time")?, - last_ack_time: fields.required("last_ack_time")?, - state: fields.required("state")?, - message: fields.required("message")?, - cow_size: fields.required("cow_size")?, - }) - } -} - -/// `[id, timestamp, value, [argument ...], client-address, client-name]`, of SLOWLOG GET and -/// COMMANDLOG GET. -impl FromValue for CommandlogEntry { - fn from_value(value: RespValue) -> Result { - match <[RespValue; 6]>::try_from(items(value)?) { - Ok([id, timestamp, value, arguments, client_address, client_name]) => Ok(Self { - id: FromValue::from_value(id)?, - timestamp: FromValue::from_value(timestamp)?, - value: FromValue::from_value(value)?, - arguments: FromValue::from_value(arguments)?, - client_address: FromValue::from_value(client_address)?, - client_name: FromValue::from_value(client_name)?, - }), - Err(items) => Err(Error::Client(format!( - "unexpected number of items, expected 6: {}", - items.len() - ))), - } - } -} - -impl FromValue for LatencyHistogramEntry { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - // a map of each bucket to its cumulative count - let histogram: FlatPairs = fields.required("histogram_usec")?; - Ok(Self { - calls: fields.required("calls")?, - histogram_usec: histogram.0, - }) - } -} - -/// `[event, timestamp, latest, max, sum, count]` -impl FromValue for LatencyLatestEntry { - fn from_value(value: RespValue) -> Result { - match <[RespValue; 6]>::try_from(items(value)?) { - Ok([event, timestamp, latest, max, sum, count]) => Ok(Self { - event: FromValue::from_value(event)?, - timestamp: FromValue::from_value(timestamp)?, - latest: FromValue::from_value(latest)?, - max: FromValue::from_value(max)?, - sum: FromValue::from_value(sum)?, - count: FromValue::from_value(count)?, - }), - Err(items) => Err(Error::Client(format!( - "unexpected number of items, expected 6: {}", - items.len() - ))), - } - } -} - -impl FromValue for ModuleListEntry { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - Ok(Self { - name: fields.required("name")?, - ver: fields.required("ver")?, - path: fields.required("path")?, - args: fields.required("args")?, - }) - } -} - /// A number sent as a string, e.g. the port of a replica in ROLE. -fn parse_string(value: RespValue) -> Result { +pub(crate) fn parse_string(value: RespValue) -> Result { let text = String::from_value(value)?; text.parse() .map_err(|_| Error::Client(format!("unexpected number: {text:?}"))) } -/// `[ip, port, offset]`, the port and offset are strings. -impl FromValue for RolePrimaryReplica { - fn from_value(value: RespValue) -> Result { - let (ip, port, offset) = FromValue::from_value(value)?; - Ok(Self { - ip, - port: parse_string(port)?, - offset: parse_string(offset)?, - }) - } -} - -/// `["master", offset, [replica ...]]`, `["slave", host, port, state, offset]` or -/// `["sentinel", [primary ...]]`. -impl FromValue for RoleReply { - fn from_value(value: RespValue) -> Result { - let mut items = items(value)?.into_iter(); - let role = String::from_value(items.next().unwrap_or(RespValue::Null))?; - let mut next = || { - items - .next() - .ok_or_else(|| Error::Client(format!("too few items for the role {role:?}"))) - }; - match role.as_str() { - "master" => Ok(RoleReply::Primary(RolePrimary { - offset: FromValue::from_value(next()?)?, - replicas: FromValue::from_value(next()?)?, - })), - "slave" => Ok(RoleReply::Replica(RoleReplica { - primary_host: FromValue::from_value(next()?)?, - primary_port: u64::from_value(next()?)? - .try_into() - .map_err(|_| Error::Client("unexpected port".to_string()))?, - state: FromValue::from_value(next()?)?, - offset: FromValue::from_value(next()?)?, - })), - "sentinel" => Ok(RoleReply::Sentinel(FromValue::from_value(next()?)?)), - role => Err(Error::Client(format!("unexpected role: {role:?}"))), - } - } -} - -/// The lines of CLIENT LIST, each of the fields of a connection, `name=value` separated by spaces. -#[derive(Debug)] -pub struct ClientList(pub Vec>); - -impl FromValue for ClientList { - fn from_value(value: RespValue) -> Result { - let text = String::from_value(value)?; - let clients = text - .lines() - .filter(|line| !line.is_empty()) - .map(|line| { - line.split(' ') - .filter_map(|field| field.split_once('=')) - .map(|(name, value)| (name.to_string(), value.to_string())) - .collect() - }) - .collect(); - Ok(ClientList(clients)) - } +pub(crate) fn unexpected(reply: &RespValue) -> Error { + Error::Client(format!("unexpected reply: {reply:?}")) } -/// `-1` when tracking is off, `0` when not redirected, otherwise the ID of the connection. -impl FromValue for ClientTrackingRedirect { - fn from_value(value: RespValue) -> Result { - match Integer::from_value(value)? { - -1 => Ok(ClientTrackingRedirect::NotTracking), - 0 => Ok(ClientTrackingRedirect::NotRedirected), - id if id > 0 => Ok(ClientTrackingRedirect::Redirected(id)), - code => Err(Error::Client(format!("unexpected reply: {code}"))), - } - } +fn decode(value: RespNestedValue) -> Result { + crate::resp::decode(value).map_err(Error::Resp) } -impl FromValue for ClientTrackinginfoReply { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - Ok(Self { - flags: fields.required("flags")?, - redirect: fields.required("redirect")?, - prefixes: fields.required("prefixes")?, - }) +/// The nested values of an array, or a set. +pub(crate) fn items(reply: RespValue) -> Result, Error> { + match reply { + RespValue::Array(items) | RespValue::Set(items) => items.into_iter().map(decode).collect(), + reply => Err(unexpected(&reply)), } } -/// The entries of COMMAND and COMMAND INFO, each followed by its subcommands, nulls, for commands -/// that do not exist, left out. -pub struct CommandInfoList(pub Vec); - -impl FromValue for CommandInfoList { - fn from_value(value: RespValue) -> Result { - let mut commands = vec![]; - for command in items(value)? { - command_info(command, &mut commands)?; +/// The entries of a map, or of a flat array of keys and values. +pub fn entries(reply: RespValue) -> Result, Error> { + let entry = |key, value| Ok((K::from_value(decode(key)?)?, V::from_value(decode(value)?)?)); + match reply { + RespValue::Map(entries) => entries + .into_iter() + .map(|(key, value)| entry(key, value)) + .collect(), + RespValue::Array(items) => { + if items.len() % 2 != 0 { + return Err(Error::Client(format!( + "unexpected odd number of keys and values: {}", + items.len() + ))); + } + let mut items = items.into_iter(); + let mut entries = vec![]; + while let (Some(key), Some(value)) = (items.next(), items.next()) { + entries.push(entry(key, value)?); + } + Ok(entries) } - Ok(CommandInfoList(commands)) - } -} - -/// `[name, arity, flags, first-key, last-key, step, acl-categories, tips, key-specs, subcommands]`, -/// the ACL categories since 6.0, the rest since 7.0. -fn command_info(value: RespValue, commands: &mut Vec) -> Result<(), Error> { - if value == RespValue::Null { - return Ok(()); - } - let mut items = items(value)?.into_iter(); - let mut next = || { - items - .next() - .ok_or_else(|| Error::Client("too few items for a command".to_string())) - }; - let info = CommandInfoReply { - name: FromValue::from_value(next()?)?, - arity: FromValue::from_value(next()?)?, - flags: FromValue::from_value(next()?)?, - first_key: FromValue::from_value(next()?)?, - last_key: FromValue::from_value(next()?)?, - step: FromValue::from_value(next()?)?, - acl_categories: next().map_or(Ok(vec![]), FromValue::from_value)?, - tips: next().map_or(Ok(vec![]), FromValue::from_value)?, - key_specs: next().map_or(Ok(vec![]), FromValue::from_value)?, - }; - let subcommands: Vec = next().map_or(Ok(vec![]), FromValue::from_value)?; - commands.push(info); - for subcommand in subcommands { - command_info(subcommand, commands)?; - } - Ok(()) -} - -impl FromValue for CommandKeySpec { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - let mut begin_search: Fields = fields.required("begin_search")?; - let mut find_keys: Fields = fields.required("find_keys")?; - Ok(Self { - notes: fields.optional("notes")?, - flags: fields.required("flags")?, - begin_search: begin_search_spec(&mut begin_search)?, - find_keys: find_keys_spec(&mut find_keys)?, - }) + reply => Err(unexpected(&reply)), } } -/// `{type, spec}`, a type this interface does not know is `unknown`. -fn begin_search_spec(fields: &mut Fields) -> Result { - let kind: String = fields.required("type")?; - let mut spec: Fields = fields.required("spec")?; - Ok(match kind.as_str() { - "index" => CommandKeySpecBeginSearch::Index(spec.required("index")?), - "keyword" => CommandKeySpecBeginSearch::Keyword(CommandKeySpecKeyword { - keyword: spec.required("keyword")?, - startfrom: spec.required("startfrom")?, - }), - _ => CommandKeySpecBeginSearch::Unknown, - }) -} - -/// `{type, spec}`, a type this interface does not know is `unknown`. -fn find_keys_spec(fields: &mut Fields) -> Result { - let kind: String = fields.required("type")?; - let mut spec: Fields = fields.required("spec")?; - Ok(match kind.as_str() { - "range" => CommandKeySpecFindKeys::Range(CommandKeySpecRange { - lastkey: spec.required("lastkey")?, - keystep: spec.required("keystep")?, - limit: spec.required("limit")?, - }), - "keynum" => CommandKeySpecFindKeys::Keynum(CommandKeySpecKeynum { - keynumidx: spec.required("keynumidx")?, - firstkey: spec.required("firstkey")?, - keystep: spec.required("keystep")?, - }), - _ => CommandKeySpecFindKeys::Unknown, - }) -} - -/// The documentation of COMMAND DOCS, a map by name, each followed by its subcommands. -pub struct CommandDocsList(pub Vec); +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{array, bulk, from, from_error, int, map, ok, simple}; -impl FromValue for CommandDocsList { - fn from_value(value: RespValue) -> Result { - let mut commands = vec![]; - for (name, doc) in entries::(value)? { - command_doc(name, doc, &mut commands)?; - } - Ok(CommandDocsList(commands)) + #[test] + fn value() { + assert_eq!(from::(int(1)), int(1)); } -} -fn command_doc( - name: String, - mut fields: Fields, - commands: &mut Vec, -) -> Result<(), Error> { - let mut arguments = vec![]; - let nested: Option> = fields.optional("arguments")?; - command_arguments(nested.unwrap_or_default(), None, &mut arguments)?; - commands.push(CommandDocsReply { - name, - summary: fields.required("summary")?, - since: fields.required("since")?, - group: fields.required("group")?, - complexity: fields.optional("complexity")?, - module: fields.optional("module")?, - doc_flags: fields.optional("doc_flags")?.unwrap_or_default(), - deprecated_since: fields.optional("deprecated_since")?, - replaced_by: fields.optional("replaced_by")?, - history: fields.optional("history")?.unwrap_or_default(), - arguments, - }); - let subcommands: Option> = fields - .optional::>("subcommands")? - .map(|subcommands| subcommands.0); - for (name, doc) in subcommands.unwrap_or_default() { - command_doc(name, doc, commands)?; + #[test] + fn unit() { + from::<()>(ok()); + assert_eq!( + from_error::<()>(simple("QUEUED")), + r#"unexpected reply: RespValue::String("QUEUED")"# + ); } - Ok(()) -} -/// The arguments, depth first, each followed by those it contains, which point to it as their -/// parent. -fn command_arguments( - nested: Vec, - parent: Option, - arguments: &mut Vec, -) -> Result<(), Error> { - for mut fields in nested { - let index = u32::try_from(arguments.len()) - .map_err(|_| Error::Client("too many arguments".to_string()))?; - let kind: String = fields.required("type")?; - arguments.push(CommandDocsArgument { - parent, - name: fields.required("name")?, - type_: argument_type(kind), - display_text: fields.optional("display_text")?, - key_spec_index: fields.optional("key_spec_index")?, - token: fields.optional("token")?, - summary: fields.optional("summary")?, - since: fields.optional("since")?, - deprecated_since: fields.optional("deprecated_since")?, - flags: fields.optional("flags")?.unwrap_or_default(), - }); - let contained: Option> = fields.optional("arguments")?; - command_arguments(contained.unwrap_or_default(), Some(index), arguments)?; + #[test] + fn integer() { + assert_eq!(from::(int(-5)), -5); + assert_eq!( + from_error::(bulk("5")), + r#"unexpected reply: RespValue::BulkString("5")"# + ); } - Ok(()) -} -fn argument_type(kind: String) -> CommandDocsArgumentType { - match kind.as_str() { - "string" => CommandDocsArgumentType::String, - "integer" => CommandDocsArgumentType::Integer, - "double" => CommandDocsArgumentType::Double, - "key" => CommandDocsArgumentType::Key, - "pattern" => CommandDocsArgumentType::Pattern, - "unix-time" => CommandDocsArgumentType::UnixTime, - "pure-token" => CommandDocsArgumentType::PureToken, - "oneof" => CommandDocsArgumentType::Oneof, - "block" => CommandDocsArgumentType::Block, - _ => CommandDocsArgumentType::Other(kind), + #[test] + fn double() { + assert_eq!(from::(RespValue::Double(1.5)), 1.5); + assert_eq!(from::(bulk("2.5")), 2.5); + assert_eq!( + from_error::(bulk("two")), + r#"unexpected reply, not a double: "two""# + ); } -} -impl FromValue for HexpireResult { - fn from_value(value: RespValue) -> Result { - match Integer::from_value(value)? { - -2 => Ok(Self::DoesNotExist), - 0 => Ok(Self::ConditionNotMet), - 1 => Ok(Self::Applied), - 2 => Ok(Self::ZeroSeconds), - code => Err(Error::Client(format!("unexpected reply: {code}"))), - } + #[test] + fn unsigned() { + assert_eq!(from::(int(3)), 3); + assert_eq!(from_error::(int(-1)), "unexpected negative reply: -1"); } -} -/// A map of the matches and the length of the longest common subsequence. -impl FromValue for LcsIdxReply { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - Ok(Self { - matches: fields.required("matches")?, - len: fields.required("len")?, - }) + #[test] + fn boolean() { + assert!(!from::(int(0))); + assert!(from::(int(1))); + assert!(from::(RespValue::Boolean(true))); + assert_eq!( + from_error::(int(2)), + "unexpected reply: RespValue::Integer(2)" + ); } -} -/// The inclusive ranges of a match in each key and, with `WITHMATCHLEN`, its length. -impl FromValue for LcsIdxMatch { - fn from_value(value: RespValue) -> Result { - let mut items = items(value)?.into_iter(); - let (Some(key1_range), Some(key2_range), len, None) = - (items.next(), items.next(), items.next(), items.next()) - else { - return Err(Error::Client( - "unexpected LCS match, expected 2 or 3 items".to_string(), - )); - }; - Ok(Self { - key1_range: FromValue::from_value(key1_range)?, - key2_range: FromValue::from_value(key2_range)?, - len: len.map(FromValue::from_value).transpose()?, - }) + #[test] + fn string() { + assert_eq!(from::(bulk("bulk")), "bulk"); + assert_eq!(from::(simple("simple")), "simple"); + let verbatim = RespValue::VerbatimString(("txt".to_string(), "verbatim".to_string())); + assert_eq!(from::(verbatim), "verbatim"); + assert_eq!( + from_error::(int(1)), + "unexpected reply: RespValue::Integer(1)" + ); } -} -impl FromValue for XinfoConsumersEntry { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - Ok(Self { - name: fields.required("name")?, - pending: fields.required("pending")?, - idle: fields.required("idle")?, - inactive: fields - .required::>("inactive")? - .0, - }) + #[test] + fn option() { + assert_eq!(from::>(RespValue::Null), None); + assert_eq!(from::>(bulk("a")), Some("a".to_string())); } -} -impl FromValue for XinfoGroupsEntry { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - Ok(Self { - name: fields.required("name")?, - consumers: fields.required("consumers")?, - pending: fields.required("pending")?, - last_delivered_id: fields.required("last-delivered-id")?, - entries_read: fields.required("entries-read")?, - lag: fields.required("lag")?, - }) + #[test] + fn list() { + assert_eq!( + from::>(array(vec![bulk("a"), bulk("b")])), + ["a", "b"] + ); + let set = RespValue::Set(vec![crate::resp::encode(bulk("a"))]); + assert_eq!(from::>(set), ["a"]); + assert_eq!( + from_error::>(bulk("a")), + r#"unexpected reply: RespValue::BulkString("a")"# + ); } -} -impl FromValue for XinfoStreamReply { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - Ok(Self { - length: fields.required("length")?, - radix_tree_keys: fields.required("radix-tree-keys")?, - radix_tree_nodes: fields.required("radix-tree-nodes")?, - groups: fields.required("groups")?, - last_generated_id: fields.required::("last-generated-id")?.0, - max_deleted_entry_id: fields.required::("max-deleted-entry-id")?.0, - entries_added: fields.required("entries-added")?, - first_entry: fields.required("first-entry")?, - last_entry: fields.required("last-entry")?, - }) + #[test] + fn tuples() { + assert_eq!( + from::<(String, Integer)>(array(vec![bulk("a"), int(1)])), + ("a".to_string(), 1) + ); + assert_eq!( + from::<(Integer, Integer, Integer, Integer)>(array(vec![ + int(1), + int(2), + int(3), + int(4) + ])), + (1, 2, 3, 4) + ); + assert_eq!( + from_error::<(String, Integer)>(array(vec![bulk("a")])), + "unexpected number of items, expected 2: 1" + ); } -} -impl FromValue for XinfoStreamReplyFull { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - Ok(Self { - length: fields.required("length")?, - radix_tree_keys: fields.required("radix-tree-keys")?, - radix_tree_nodes: fields.required("radix-tree-nodes")?, - last_generated_id: fields.required::("last-generated-id")?.0, - max_deleted_entry_id: fields.required::("max-deleted-entry-id")?.0, - entries_added: fields.required("entries-added")?, - entries: fields.required("entries")?, - groups: fields.required("groups")?, - }) + #[test] + fn maps() { + let expected: Map = [("a".to_string(), 1)].into_iter().collect(); + // RESP3 + assert_eq!( + from::>(map(vec![(bulk("a"), int(1))])), + expected + ); + // RESP2, a flat array + assert_eq!( + from::>(array(vec![bulk("a"), int(1)])), + expected + ); + assert_eq!( + from_error::>(array(vec![bulk("a"), int(1), bulk("b")])), + "unexpected odd number of keys and values: 3" + ); } -} -impl FromValue for XinfoStreamReplyFullGroup { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - Ok(Self { - name: fields.required("name")?, - last_delivered_id: fields.required("last-delivered-id")?, - entries_read: fields.required("entries-read")?, - lag: fields.required("lag")?, - pel_count: fields.required("pel-count")?, - pending: fields.required("pending")?, - consumers: fields.required("consumers")?, - }) + #[test] + fn fields() { + let value = map(vec![ + (bulk("name"), bulk("a")), + (bulk("count"), int(2)), + (bulk("newer"), int(3)), + ]); + let mut fields = from::(value); + assert_eq!(fields.required::("name").unwrap(), "a"); + assert_eq!(fields.optional::("count").unwrap(), Some(2)); + assert_eq!(fields.optional::("missing").unwrap(), None); + match fields.required::("missing") { + Err(Error::Client(message)) => assert_eq!(message, "missing field: missing"), + result => panic!("expected a client error: {result:?}"), + } } -} -impl FromValue for XinfoStreamReplyFullConsumer { - fn from_value(value: RespValue) -> Result { - let mut fields = Fields::from_value(value)?; - Ok(Self { - name: fields.required("name")?, - seen_time: fields.required("seen-time")?, - active_time: fields - .required::>("active-time")? - .0, - pel_count: fields.required("pel-count")?, - pending: fields.required("pending")?, - }) + #[test] + fn or_never() { + assert_eq!(from::>(int(-1)).0, None); + assert_eq!(from::>(int(5)).0, Some(5)); } -} - -fn unexpected(reply: &RespValue) -> Error { - Error::Client(format!("unexpected reply: {reply:?}")) -} -fn decode(value: RespNestedValue) -> Result { - crate::resp::decode(value).map_err(Error::Resp) -} - -/// The nested values of an array, or a set. -fn items(reply: RespValue) -> Result, Error> { - match reply { - RespValue::Array(items) | RespValue::Set(items) => items.into_iter().map(decode).collect(), - reply => Err(unexpected(&reply)), + #[test] + fn flat_pairs() { + let pairs = + from::>(array(vec![bulk("b"), int(2), bulk("a"), int(1)])); + // in the order of the reply + assert_eq!(pairs.0, [("b".to_string(), 2), ("a".to_string(), 1)]); } -} -/// The entries of a map, or of a flat array of keys and values. -pub fn entries(reply: RespValue) -> Result, Error> { - let entry = |key, value| Ok((K::from_value(decode(key)?)?, V::from_value(decode(value)?)?)); - match reply { - RespValue::Map(entries) => entries - .into_iter() - .map(|(key, value)| entry(key, value)) - .collect(), - RespValue::Array(items) => { - if items.len() % 2 != 0 { - return Err(Error::Client(format!( - "unexpected odd number of keys and values: {}", - items.len() - ))); - } - let mut items = items.into_iter(); - let mut entries = vec![]; - while let (Some(key), Some(value)) = (items.next(), items.next()) { - entries.push(entry(key, value)?); - } - Ok(entries) + #[test] + fn number_in_a_string() { + assert_eq!(parse_string::(bulk("6379")).unwrap(), 6379); + match parse_string::(bulk("port")) { + Err(Error::Client(message)) => assert_eq!(message, r#"unexpected number: "port""#), + result => panic!("expected a client error: {result:?}"), } - reply => Err(unexpected(&reply)), } } diff --git a/wit/types.wit b/wit/types.wit index a6213df..a6f8ea2 100644 --- a/wit/types.wit +++ b/wit/types.wit @@ -60,6 +60,8 @@ interface types { keep-alive-interval: option, } + // shared by the commands + /// A duration in whole seconds, e.g. `EX seconds`. record duration-seconds { ex: u64, @@ -105,27 +107,6 @@ interface types { lt, } - /// The outcome of setting the expiration of a hash field, e.g. with HEXPIRE. - enum hexpire-result { - /// -2: key does not exist, or field does not exist in the HASH. - does-not-exist, - /// 0: the specified NX | XX | GT | LT condition has not been met. - condition-not-met, - /// 1: the expiration time was applied. - applied, - /// 2: when called with 0 seconds. - zero-seconds, - } - - /// The result for a field, see `hexpireat`. - type hexpireat-result = hexpire-result; - - /// The result for a field, see `hpexpire`. - type hpexpire-result = hexpire-result; - - /// The result for a field, see `hpexpireat`. - type hpexpireat-result = hexpire-result; - /// The remaining time to live of a key or hash field, to the second, see `ttl` and `httl`. variant ttl-reply { /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. @@ -178,6 +159,21 @@ interface types { /// The expiration time of a field, see `hpexpiretime`. type hpexpiretime-reply = pexpiretime-reply; + /// A range of a sorted result, `LIMIT offset count`. + record limit { + /// The number of elements skipped. + offset: integer, + /// The maximum number of elements returned, negative for all of them. + count: integer, + } + + enum sort-order { + /// ascending, smallest to largest. + asc, + /// descending, largest to smallest. + desc, + } + // admin commands /// Whether a user may run a command, see `acl-dryrun`. @@ -1064,6 +1060,27 @@ interface types { fxx, } + /// The outcome of setting the expiration of a hash field, e.g. with HEXPIRE. + enum hexpire-result { + /// -2: key does not exist, or field does not exist in the HASH. + does-not-exist, + /// 0: the specified NX | XX | GT | LT condition has not been met. + condition-not-met, + /// 1: the expiration time was applied. + applied, + /// 2: when called with 0 seconds. + zero-seconds, + } + + /// The result for a field, see `hexpireat`. + type hexpireat-result = hexpire-result; + + /// The result for a field, see `hpexpire`. + type hpexpire-result = hexpire-result; + + /// The result for a field, see `hpexpireat`. + type hpexpireat-result = hexpire-result; + // keyspace commands /// The type of the value stored at a key. @@ -1100,6 +1117,25 @@ interface types { username-password(tuple), } + /// How SORT orders and projects the elements of a list, set or sorted set. + record sort-opts { + /// `BY pattern`: sorts by the values of the keys the pattern names, `*` replaced with each + /// element, or `nosort` to skip sorting. + by: option, + /// `LIMIT offset count`: a range of the sorted elements. + limit: option, + /// `GET pattern`: returns the values of the keys each pattern names instead of the elements, + /// `#` for the element itself. + get: option>, + /// order to sort elements. + order: option, + /// `ALPHA`: sorts lexicographically, rather than numerically. + alpha: bool, + } + + /// The options of the sort, see `sort-ro`. + type sort-ro-opts = sort-opts; + // list commands /// An end of a list. @@ -1179,42 +1215,6 @@ interface types { engines: map, } - // set commands - - /// A range of a sorted result, `LIMIT offset count`. - record limit { - /// The number of elements skipped. - offset: integer, - /// The maximum number of elements returned, negative for all of them. - count: integer, - } - - /// How SORT orders and projects the elements of a list, set or sorted set. - record sort-opts { - /// `BY pattern`: sorts by the values of the keys the pattern names, `*` replaced with each - /// element, or `nosort` to skip sorting. - by: option, - /// `LIMIT offset count`: a range of the sorted elements. - limit: option, - /// `GET pattern`: returns the values of the keys each pattern names instead of the elements, - /// `#` for the element itself. - get: option>, - /// order to sort elements. - order: option, - /// `ALPHA`: sorts lexicographically, rather than numerically. - alpha: bool, - } - - /// The options of the sort, see `sort-ro`. - type sort-ro-opts = sort-opts; - - enum sort-order { - /// ascending, smallest to largest. - asc, - /// descending, largest to smallest. - desc, - } - // sortedset commands /// A bound of a range of scores. From 0235a71aa0c1422885c26657d7eda8c963e8db34 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 13 Oct 2026 23:54:58 -0400 Subject: [PATCH 25/37] revamp pubsub model Signed-off-by: Scott Andrews --- components/commands/src/lib.rs | 165 +-- components/connection/src/lib.rs | 1119 ++++++++++++++++- components/dep-cli/src/lib.rs | 22 +- .../src/commands/connection.rs | 40 +- wit/commands.wit | 62 +- wit/connection.wit | 138 +- wit/types.wit | 76 +- 7 files changed, 1331 insertions(+), 291 deletions(-) diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index ee1f458..79662ca 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -1,22 +1,21 @@ use crate::exports::componentized::valkey::commands::{ AclDryrunResult, AclListEntry, AclLogEntry, AclRule, BgsaveOperation, BitRange, BitfieldGet, BitfieldOperation, BitopOperation, ClientFilter, ClientPauseMode, ClientSetinfoAttr, - ClientTrackingOptions, ClientTrackingRedirect, ClientTrackinginfoReply, ClientUnblockType, - ClusterBumpepochResult, ClusterFailoverMode, ClusterGetslotmigrationsEntry, - ClusterMigrateslotsGroup, ClusterResetMode, ClusterSetslotState, CommandDocsReply, - CommandInfoReply, CommandListFilter, CommandlogEntry, CommandlogType, Connection, Cursor, - Double, DurationMilliseconds, DurationSeconds, Error, ExpiretimeReply, FailoverTarget, - FlushMode, FunctionListLibrary, FunctionStatsReply, GeoCoordinates, GeoUnit, GeoaddCondition, - GeosearchCount, GeosearchShape, GeosearchWithFlags, GeosearchWithResult, GetexTtl, Guest, - HexpireResult, HpersistResult, HsetexCondition, HsetexFieldCondition, Integer, KeyType, - LatencyHistogramEntry, LatencyLatestEntry, LcsIdxReply, LexBound, Limit, LinsertPosition, - LinsertResult, ListSide, MigrateAuth, MigrateFlags, ModuleListEntry, MsetexPrecondition, - MsetexTtl, PexpiretimeReply, PttlReply, RespValue, RoleReply, ScoreBound, SetPrecondition, - SetTtl, ShutdownFlags, ShutdownSave, SortOpts, SortOrder, StreamId, StreamTrim, TtlMode, - TtlReply, UnixTimeMilliseconds, UnixTimeSeconds, XclaimOpts, XinfoConsumersEntry, - XinfoGroupsEntry, XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, - XpendingSummary, ZaddComparison, ZaddCondition, ZmpopEnd, ZrangeBy, ZrangeWithscoresBy, - ZsetAggregate, + ClientTrackingOptions, ClientTrackinginfoReply, ClientUnblockType, ClusterBumpepochResult, + ClusterFailoverMode, ClusterGetslotmigrationsEntry, ClusterMigrateslotsGroup, ClusterResetMode, + ClusterSetslotState, CommandDocsReply, CommandInfoReply, CommandListFilter, CommandlogEntry, + CommandlogType, Connection, Cursor, Double, DurationMilliseconds, DurationSeconds, Error, + ExpiretimeReply, FailoverTarget, FlushMode, FunctionListLibrary, FunctionStatsReply, + GeoCoordinates, GeoUnit, GeoaddCondition, GeosearchCount, GeosearchShape, GeosearchWithFlags, + GeosearchWithResult, GetexTtl, Guest, HexpireResult, HpersistResult, HsetexCondition, + HsetexFieldCondition, Integer, KeyType, LatencyHistogramEntry, LatencyLatestEntry, LcsIdxReply, + LexBound, Limit, LinsertPosition, LinsertResult, ListSide, MigrateAuth, MigrateFlags, + ModuleListEntry, MsetexPrecondition, MsetexTtl, PexpiretimeReply, PttlReply, RespValue, + RoleReply, ScoreBound, SetPrecondition, SetTtl, ShutdownFlags, ShutdownSave, SortOpts, + SortOrder, StreamId, StreamTrim, TtlMode, TtlReply, UnixTimeMilliseconds, UnixTimeSeconds, + XclaimOpts, XinfoConsumersEntry, XinfoGroupsEntry, XinfoStreamReply, XinfoStreamReplyFull, + XpendingExtendedEntry, XpendingSummary, ZaddComparison, ZaddCondition, ZmpopEnd, ZrangeBy, + ZrangeWithscoresBy, ZsetAggregate, }; use componentized_valkey::command::{Flag, NamedOption, NonEmpty, NumKeys}; use componentized_valkey::commands::admin::{AclList, AclUserRules, ClientList}; @@ -45,13 +44,6 @@ async fn exchange( T::from_value(reply) } -/// Send a command, its words as bulk strings, without waiting for a reply. -/// -/// An error reply fails the command, see `connection.emit`. -async fn emit(connection: &Connection, command: impl IntoCommand) -> Result<(), Error> { - connection.emit(command.into_command()?).await -} - struct Commands; /// The number of values SORT returns for each element, one for each GET pattern, or the element. @@ -1060,30 +1052,6 @@ impl Guest for Commands { exchange(connection, ("AUTH", username, password)).await } - #[doc = " Switch to a different protocol, optionally authenticating and setting the connection\'s name, or provide a contextual client report."] - #[doc = ""] - #[doc = " RESP2 is not fully supported by this client."] - #[doc = ""] - #[doc = " HELLO 3 [ AUTH username password ] [ SETNAME clientname ]"] - #[doc = " https://valkey.io/commands/hello/"] - #[allow(async_fn_in_trait)] - async fn hello( - connection: &Connection, - auth: Option<(String, String)>, - client_name: Option, - ) -> Result, Error> { - exchange( - connection, - ( - "HELLO", - "3", - auth.map(|auth| ("AUTH", auth)), - client_name.map(|name| ("SETNAME", name)), - ), - ) - .await - } - #[doc = " Returns PONG if no argument is provided, otherwise return a copy of the argument as a bulk."] #[doc = ""] #[doc = " PING [ message ]"] @@ -1153,15 +1121,6 @@ impl Guest for Commands { exchange(connection, ("CLIENT", "GETNAME")).await } - #[doc = " Where the connection's tracking notifications are redirected."] - #[doc = ""] - #[doc = " CLIENT GETREDIR"] - #[doc = " https://valkey.io/commands/client-getredir/"] - #[allow(async_fn_in_trait)] - async fn client_getredir(connection: &Connection) -> Result { - exchange(connection, ("CLIENT", "GETREDIR")).await - } - #[doc = " The ID of the connection, unique until the server restarts."] #[doc = ""] #[doc = " CLIENT ID"] @@ -1226,11 +1185,11 @@ impl Guest for Commands { } #[doc = " Turns tracking on with the options, or off with none, for server assisted client side"] - #[doc = " caching. Invalidations arrive as push messages, see `connection.pushes`."] + #[doc = " caching. Invalidations arrive as push messages, see `connection.dead-letters`."] #[doc = ""] #[doc = " It applies to the connection, so to every caller sharing it."] #[doc = ""] - #[doc = " CLIENT TRACKING 〈 ON | OFF 〉 [ REDIRECT client-id ] [ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN ] [ OPTOUT ] [ NOLOOP ]"] + #[doc = " CLIENT TRACKING 〈 ON | OFF 〉 [ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN ] [ OPTOUT ] [ NOLOOP ]"] #[doc = " https://valkey.io/commands/client-tracking/"] #[allow(async_fn_in_trait)] async fn client_tracking( @@ -3043,19 +3002,6 @@ impl Guest for Commands { exchange(connection, ("UNLINK", NonEmpty::new("keys", keys))).await } - #[doc = " Subscribes the client to the given patterns."] - #[doc = ""] - #[doc = " PSUBSCRIBE pattern [ pattern ... ]"] - #[doc = " https://valkey.io/commands/psubscribe/"] - #[allow(async_fn_in_trait)] - async fn psubscribe(connection: &Connection, patterns: Vec) -> Result<(), Error> { - emit( - connection, - ("PSUBSCRIBE", NonEmpty::new("patterns", patterns)), - ) - .await - } - #[doc = " Posts a message to the given channel."] #[doc = ""] #[doc = " PUBLISH channel message"] @@ -3359,22 +3305,6 @@ impl Guest for Commands { exchange(connection, ("SCRIPT", "SHOW", sha1)).await } - #[doc = " Unsubscribes the client from the given patterns, or from all of them if none is given."] - #[doc = ""] - #[doc = " PUNSUBSCRIBE [ pattern [ pattern ... ] ]"] - #[doc = " https://valkey.io/commands/punsubscribe/"] - #[allow(async_fn_in_trait)] - async fn punsubscribe( - connection: &Connection, - patterns: Option>, - ) -> Result<(), Error> { - emit( - connection, - ("PUNSUBSCRIBE", NonEmpty::optional("patterns", patterns)), - ) - .await - } - #[doc = " Posts a message to the given shard channel."] #[doc = ""] #[doc = " SPUBLISH shardchannel message"] @@ -3388,67 +3318,6 @@ impl Guest for Commands { exchange(connection, ("SPUBLISH", shardchannel, message)).await } - #[doc = " Subscribes the client to the specified shard channels."] - #[doc = ""] - #[doc = " SSUBSCRIBE shardchannel [ shardchannel ... ]"] - #[doc = " https://valkey.io/commands/ssubscribe/"] - #[allow(async_fn_in_trait)] - async fn ssubscribe(connection: &Connection, shardchannels: Vec) -> Result<(), Error> { - emit( - connection, - ("SSUBSCRIBE", NonEmpty::new("shardchannels", shardchannels)), - ) - .await - } - - #[doc = " Subscribes the client to the specified channels."] - #[doc = ""] - #[doc = " SUBSCRIBE channel [ channel ... ]"] - #[doc = " https://valkey.io/commands/subscribe/"] - #[allow(async_fn_in_trait)] - async fn subscribe(connection: &Connection, channels: Vec) -> Result<(), Error> { - emit( - connection, - ("SUBSCRIBE", NonEmpty::new("channels", channels)), - ) - .await - } - - #[doc = " Unsubscribes the client from the given shard channels, or from all of them if none is given."] - #[doc = ""] - #[doc = " SUNSUBSCRIBE [ shardchannel ] [ [ shardchannel ] ... ]"] - #[doc = " https://valkey.io/commands/sunsubscribe/"] - #[allow(async_fn_in_trait)] - async fn sunsubscribe( - connection: &Connection, - shardchannels: Option>, - ) -> Result<(), Error> { - emit( - connection, - ( - "SUNSUBSCRIBE", - NonEmpty::optional("shardchannels", shardchannels), - ), - ) - .await - } - - #[doc = " Unsubscribes the client from the given channels, or from all of them if none is given."] - #[doc = ""] - #[doc = " UNSUBSCRIBE [ channel ] [ [ channel ] ... ]"] - #[doc = " https://valkey.io/commands/unsubscribe/"] - #[allow(async_fn_in_trait)] - async fn unsubscribe( - connection: &Connection, - channels: Option>, - ) -> Result<(), Error> { - emit( - connection, - ("UNSUBSCRIBE", NonEmpty::optional("channels", channels)), - ) - .await - } - #[doc = " Appends the value at the end of the string."] #[doc = ""] #[doc = " APPEND key value"] diff --git a/components/connection/src/lib.rs b/components/connection/src/lib.rs index a95417a..a856a21 100644 --- a/components/connection/src/lib.rs +++ b/components/connection/src/lib.rs @@ -5,38 +5,45 @@ //! //! - the writer writes queued commands to the requests stream, in order //! - the reader reads the replies stream, handing each reply to the oldest command waiting for one, -//! and each push message, or value the server sent unprompted, to every reader of push messages +//! each message to the subscriptions holding its target, and each other push message, or value +//! the server sent unprompted, to every reader of dead letters //! - the supervisor starts the work methods ask for that outlives them, e.g. keep-alive, since sync //! methods have no task of their own to spawn it into //! //! Dropping the connection drops its queue of commands, the writer then closes the requests stream -//! and the transport closes, which ends the reader. +//! and the transport closes, which ends the reader. A subscription keeps the connection open. // the test harness provides a main #![cfg_attr(not(test), no_main)] use std::cell::RefCell; -use std::collections::VecDeque; -use std::future::poll_fn; +use std::collections::{HashMap, VecDeque}; +use std::future::{Future, poll_fn}; use std::pin::Pin; use std::rc::{Rc, Weak}; -use componentized_valkey::types::{RespNestedValue, RespValue}; +use componentized_valkey::types::{ + RespNestedValue, RespValue, SubscriptionEvent, SubscriptionMessage, SubscriptionTarget, + UnsubscribeReason, +}; use exports::componentized::valkey::connection::{ ConnectOpts, Connection, DurationMilliseconds, Error, Guest, GuestConnection, + GuestSubscription, Subscription, }; use futures_channel::{mpsc, oneshot}; use futures_core::Stream; use wasi::clocks::monotonic_clock::wait_for; use wit_bindgen::{FutureReader, FutureWriter, StreamReader, StreamWriter, spawn_local}; -/// How many push messages a reader may fall behind before its stream is closed. +/// How many values a reader may fall behind before its stream is closed, and how many events a +/// subscription keeps while no stream is open. const PUSH_BUFFER: usize = 256; struct Component; impl Guest for Component { type Connection = ConnectionImpl; + type Subscription = SubscriptionImpl; } /// Where the reply to a command goes. @@ -45,18 +52,46 @@ enum Reply { Call(oneshot::Sender>), /// Nowhere, the command was sent without waiting for its reply. Discard, + /// A subscription's SUBSCRIBE, confirmed by a push message for each channel rather than a + /// reply. + Subscribe { + kind: Kind, + /// The channels not confirmed yet. + channels: Vec, + id: u64, + done: oneshot::Sender>, + }, + /// RESET, whose reply ends the subscriptions the server dropped. + Reset(oneshot::Sender>), + /// The HELLO after RESET, the writer waits for its reply before sending another command, and + /// the connection closes when it fails. + Rehello { + proceed: oneshot::Sender, + done: oneshot::Sender>, + }, + /// A subscription's UNSUBSCRIBE, confirmed by a push message for each channel rather than a + /// reply. + Unsubscribe { + kind: Kind, + /// The channels not confirmed yet. + channels: Vec, + done: oneshot::Sender>, + }, } /// A command for the writer, with a signal for once it is written. struct Request { command: RespValue, written: Option>, + /// Whether to go on writing after this command, e.g. once the HELLO after RESET succeeded, + /// otherwise the writer stops, and the commands after it are never sent. + barrier: Option>, } /// Work for the supervisor to start. enum Task { - /// Forward push messages from the channel to a reader's stream. - Pushes(mpsc::Receiver, StreamWriter), + /// Run the future in a task of its own. + Spawn(Pin>>), /// Keep the connection alive, until the keep-alive is set again. KeepAlive { generation: u64, @@ -74,18 +109,25 @@ struct State { /// Counts the commands written and the values read, it stays the same while the connection is /// idle. activity: u64, - /// The readers of push messages. - pushes: Vec>, + /// The readers of dead letters. + dead_letters: Vec>, + /// The subscriptions, and the channels they hold. + subscriptions: Subscriptions, /// The server's properties from the reply to HELLO. server_properties: Vec<(String, RespValue)>, + /// The HELLO the connection was established with, repeated after RESET. + hello: Vec, /// Bumped each time the keep-alive is set, a keep-alive task stops once it is outdated. keep_alive_generation: u64, + /// Set by the first `subscription`, and never cleared: from then on the connection + /// alone sends SUBSCRIBE and the like, so the subscriptions stay in step with the server. + owns_subscriptions: bool, } /// A connection's queues to its background tasks. /// -/// Only the connection holds it, the background tasks hold the state alone or a weak reference, -/// so dropping the connection drops the queues and ends the tasks. +/// Only the connection and its subscriptions hold it, the background tasks hold the state alone +/// or a weak reference, so dropping them drops the queues and ends the tasks. struct Inner { state: Rc>, requests: mpsc::UnboundedSender, @@ -159,7 +201,24 @@ impl GuestConnection for ConnectionImpl { transaction(&self.0, commands).await } - fn pushes(&self) -> StreamReader { + fn subscription(&self) -> Subscription { + let id = { + let mut state = self.0.state.borrow_mut(); + state.owns_subscriptions = true; + let closed = state.closed.clone(); + state.subscriptions.create(closed) + }; + Subscription::new(SubscriptionImpl(Handle { + inner: self.0.clone(), + id, + })) + } + + async fn reset(&self) -> Result<(), Error> { + reset(&self.0).await + } + + fn dead_letters(&self) -> StreamReader { let (writer, reader) = wit_stream::new(); let (tx, rx) = mpsc::channel(PUSH_BUFFER); let mut state = self.0.state.borrow_mut(); @@ -168,10 +227,10 @@ impl GuestConnection for ConnectionImpl { && self .0 .tasks - .unbounded_send(Task::Pushes(rx, writer)) + .unbounded_send(Task::Spawn(Box::pin(forward_values(rx, writer)))) .is_ok() { - state.pushes.push(tx); + state.dead_letters.push(tx); } reader } @@ -192,6 +251,720 @@ impl GuestConnection for ConnectionImpl { } } +pub struct SubscriptionImpl(Handle); + +impl GuestSubscription for SubscriptionImpl { + fn targets(&self) -> Vec { + let state = self.0.inner.state.borrow(); + let Some(sub) = state.subscriptions.subs.get(&self.0.id) else { + return vec![]; + }; + sub.targets + .iter() + .filter(|(_, hold)| *hold == Hold::Active) + .map(|((kind, name), _)| kind.target(name.clone())) + .collect() + } + + async fn subscribe(&self, targets: Vec) -> Result<(), Error> { + let groups = by_kind(targets)?; + subscribe(&self.0.inner, self.0.id, groups).await + } + + async fn unsubscribe(&self, targets: Option>) -> Result<(), Error> { + let groups = match targets { + Some(targets) => by_kind(targets)? + .into_iter() + .map(|(kind, names)| (kind, Some(names))) + .collect(), + None => Kind::ALL.into_iter().map(|kind| (kind, None)).collect(), + }; + unsubscribe(&self.0.inner, self.0.id, groups).await + } + + fn events(&self) -> (StreamReader, FutureReader) { + let (writer, stream) = wit_stream::new(); + // the default is only written if the subscription is gone before the stream ends + let (end_writer, end) = wit_future::new(|| Err(Error::Closed(None))); + let (mut events_tx, events_rx) = mpsc::channel(PUSH_BUFFER); + let (end_tx, end_rx) = oneshot::channel(); + { + let mut state = self.0.inner.state.borrow_mut(); + if let Some(sub) = state.subscriptions.subs.get_mut(&self.0.id) { + // the backlog fits, it is no larger than the channel + for event in sub.backlog.drain(..) { + let _ = events_tx.try_send(event); + } + match &sub.closed { + Some(error) => { + let _ = end_tx.send(Err(error.clone())); + } + None => sub.readers.push(Reader { + events: events_tx, + end: end_tx, + }), + } + } + } + let forward = forward_events(events_rx, end_rx, writer, end_writer); + let _ = self + .0 + .inner + .tasks + .unbounded_send(Task::Spawn(Box::pin(forward))); + (stream, end) + } +} + +/// The kind of a subscription's target, each with its own commands and push messages. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +enum Kind { + Channel, + Pattern, + Shard, +} + +impl Kind { + const ALL: [Kind; 3] = [Kind::Channel, Kind::Pattern, Kind::Shard]; + + fn subscribe(self) -> &'static str { + match self { + Kind::Channel => "SUBSCRIBE", + Kind::Pattern => "PSUBSCRIBE", + Kind::Shard => "SSUBSCRIBE", + } + } + + fn unsubscribe(self) -> &'static str { + match self { + Kind::Channel => "UNSUBSCRIBE", + Kind::Pattern => "PUNSUBSCRIBE", + Kind::Shard => "SUNSUBSCRIBE", + } + } + + fn target(self, name: String) -> SubscriptionTarget { + match self { + Kind::Channel => SubscriptionTarget::Channel(name), + Kind::Pattern => SubscriptionTarget::Pattern(name), + Kind::Shard => SubscriptionTarget::Shardchannel(name), + } + } +} + +/// The names of the targets of each kind, in the order each kind first appears, failing unless +/// there is at least one target. +fn by_kind(targets: Vec) -> Result)>, Error> { + if targets.is_empty() { + return Err(Error::Client( + "targets requires at least one item".to_string(), + )); + } + let mut groups: Vec<(Kind, Vec)> = vec![]; + for target in targets { + let (kind, name) = match target { + SubscriptionTarget::Channel(name) => (Kind::Channel, name), + SubscriptionTarget::Pattern(name) => (Kind::Pattern, name), + SubscriptionTarget::Shardchannel(name) => (Kind::Shard, name), + }; + match groups.iter_mut().find(|(grouped, _)| *grouped == kind) { + Some((_, names)) => names.push(name), + None => groups.push((kind, vec![name])), + } + } + Ok(groups) +} + +/// A channel, pattern or shard channel, by its kind and name. +type Target = (Kind, String); + +/// How a subscription holds a target. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Hold { + /// Waiting for the server to confirm SUBSCRIBE. + Pending, + /// Confirmed, its messages arrive. + Active, + /// Unsubscribing, its messages arrive until the server confirms UNSUBSCRIBE. + Leaving, +} + +/// How a stream of events ended, `ok` once the subscription is dropped. +type Ended = Result<(), Error>; + +/// A reader of a subscription's events, and where to send how its stream ended. +struct Reader { + events: mpsc::Sender, + end: oneshot::Sender, +} + +#[derive(Default)] +struct Sub { + /// The targets, in the order they were subscribed to. + targets: Vec<(Target, Hold)>, + readers: Vec, + /// How to tell readers whose stream was dropped how the subscription ended. + orphans: Vec>, + /// The events arriving while no stream is open, for the next stream. + backlog: VecDeque, + /// The error that closed the connection, once it has. + closed: Option, +} + +impl Sub { + fn hold(&self, target: &Target) -> Option { + self.targets + .iter() + .find(|(held, _)| held == target) + .map(|(_, hold)| *hold) + } + + fn set_hold(&mut self, target: &Target, hold: Hold) { + match self.targets.iter_mut().find(|(held, _)| held == target) { + Some((_, current)) => *current = hold, + None => self.targets.push((target.clone(), hold)), + } + } + + fn release(&mut self, target: &Target) { + self.targets.retain(|(held, _)| held != target); + } + + /// Fan the event out to every reader, a reader that is full is cut off, one whose stream was + /// dropped is forgotten. + fn emit(&mut self, event: SubscriptionEvent) { + if self.readers.is_empty() { + if self.backlog.len() < PUSH_BUFFER { + self.backlog.push_back(event); + } + return; + } + for mut reader in std::mem::take(&mut self.readers) { + match reader.events.try_send(event.clone()) { + Ok(()) => self.readers.push(reader), + Err(error) if error.is_full() => { + let _ = reader.end.send(Err(Error::Lagged)); + } + Err(_) => self.orphans.push(reader.end), + } + } + } + + /// End the readers' streams. + fn end(&mut self, ended: Ended) { + for reader in self.readers.drain(..) { + let _ = reader.end.send(ended.clone()); + } + for orphan in self.orphans.drain(..) { + let _ = orphan.send(ended.clone()); + } + } +} + +fn unsubscribed(target: &Target, reason: UnsubscribeReason) -> SubscriptionEvent { + SubscriptionEvent::Unsubscribed((target.0.target(target.1.clone()), reason)) +} + +#[derive(Default)] +struct Subscriptions { + next_id: u64, + subs: HashMap, + /// The subscriptions holding each target, in any way. + holders: HashMap>, +} + +impl Subscriptions { + fn create(&mut self, closed: Option) -> u64 { + let id = self.next_id; + self.next_id += 1; + self.subs.insert( + id, + Sub { + closed, + ..Sub::default() + }, + ); + id + } + + fn hold(&mut self, id: u64, target: &Target, hold: Hold) { + let Some(sub) = self.subs.get_mut(&id) else { + return; + }; + if sub.hold(target).is_none() { + self.holders.entry(target.clone()).or_default().push(id); + } + sub.set_hold(target, hold); + } + + /// The subscription no longer holds the target. + fn release(&mut self, id: u64, target: &Target) { + if let Some(sub) = self.subs.get_mut(&id) { + sub.release(target); + } + if let Some(ids) = self.holders.get_mut(target) { + ids.retain(|holder| *holder != id); + if ids.is_empty() { + self.holders.remove(target); + } + } + } + + /// The subscriptions holding the target in one of the ways. + fn holding(&self, target: &Target, holds: &[Hold]) -> Vec { + self.holders + .get(target) + .into_iter() + .flatten() + .copied() + .filter(|id| { + self.subs + .get(id) + .and_then(|sub| sub.hold(target)) + .is_some_and(|hold| holds.contains(&hold)) + }) + .collect() + } + + fn emit(&mut self, id: u64, event: SubscriptionEvent) { + if let Some(sub) = self.subs.get_mut(&id) { + sub.emit(event); + } + } + + /// The server confirmed SUBSCRIBE for the subscription's target. + fn subscribed(&mut self, id: u64, target: &Target) { + let Some(sub) = self.subs.get_mut(&id) else { + return; + }; + match sub.hold(target) { + Some(Hold::Pending) => sub.set_hold(target, Hold::Active), + // unsubscribed from while waiting, UNSUBSCRIBE follows + Some(Hold::Leaving) => {} + _ => return, + } + sub.emit(SubscriptionEvent::Subscribed( + target.0.target(target.1.clone()), + )); + } + + /// A message for the subscriptions receiving the target's messages, false when none holds it. + fn message(&mut self, target: &Target, message: SubscriptionMessage) -> bool { + if !self.holders.contains_key(target) { + return false; + } + for id in self.holding(target, &[Hold::Active, Hold::Leaving]) { + self.emit(id, SubscriptionEvent::Message(message.clone())); + } + true + } + + /// The server confirmed UNSUBSCRIBE for the target, the subscriptions leaving it let go. + fn unsubscribe_confirmed(&mut self, target: &Target) { + for id in self.holding(target, &[Hold::Leaving]) { + self.release(id, target); + self.emit(id, unsubscribed(target, UnsubscribeReason::Requested)); + } + } + + /// The server unsubscribed from the target on its own, false when none holds it. + fn unsubscribed_by_server(&mut self, target: &Target) -> bool { + if !self.holders.contains_key(target) { + return false; + } + for id in self.holding(target, &[Hold::Active, Hold::Leaving]) { + let leaving = + self.subs.get(&id).and_then(|sub| sub.hold(target)) == Some(Hold::Leaving); + self.release(id, target); + let reason = if leaving { + UnsubscribeReason::Requested + } else { + UnsubscribeReason::ByServer + }; + self.emit(id, unsubscribed(target, reason)); + } + true + } + + /// RESET unsubscribed from every target the server confirmed, without a push message. Those + /// still waiting for the server subscribe after RESET. + fn reset(&mut self) { + let mut reset = vec![]; + for (id, sub) in &self.subs { + for (target, hold) in &sub.targets { + if *hold != Hold::Pending { + reset.push((*id, target.clone())); + } + } + } + for (id, target) in reset { + self.release(id, &target); + self.emit(id, unsubscribed(&target, UnsubscribeReason::Reset)); + } + } + + /// The connection closed, ending every stream. + fn close(&mut self, error: &Error) { + for sub in self.subs.values_mut() { + sub.end(Err(error.clone())); + sub.closed = Some(error.clone()); + } + self.holders.clear(); + } + + /// The subscription was dropped and unsubscribed, ending its streams. + fn remove(&mut self, id: u64) { + let Some(mut sub) = self.subs.remove(&id) else { + return; + }; + sub.end(Ok(())); + for (target, _) in sub.targets { + if let Some(ids) = self.holders.get_mut(&target) { + ids.retain(|holder| *holder != id); + if ids.is_empty() { + self.holders.remove(&target); + } + } + } + } +} + +/// A subscription, for its resource. +/// +/// Dropping it unsubscribes from its targets, in the background, then ends its streams. +struct Handle { + inner: Rc, + id: u64, +} + +impl Drop for Handle { + fn drop(&mut self) { + let inner = self.inner.clone(); + let id = self.id; + let _ = self + .inner + .tasks + .unbounded_send(Task::Spawn(Box::pin(async move { + let every = Kind::ALL.into_iter().map(|kind| (kind, None)).collect(); + let _ = unsubscribe(&inner, id, every).await; + inner.state.borrow_mut().subscriptions.remove(id); + }))); + } +} + +/// The names given once each, in order. +fn unique(names: Vec) -> Vec { + let mut unique: Vec = vec![]; + for name in names { + if !unique.contains(&name) { + unique.push(name); + } + } + unique +} + +/// A command for one kind of targets, queued and waiting for the server to confirm it. +type Confirmation = oneshot::Receiver>; + +/// Subscribe to the targets, one command for each kind, queued with no other command between +/// them. Every command is sent, the error holds each that failed. +async fn subscribe(inner: &Inner, id: u64, groups: Vec<(Kind, Vec)>) -> Result<(), Error> { + let several = groups.len() > 1; + // no await until every command is queued + let queued: Vec<_> = groups + .into_iter() + .map(|(kind, names)| (kind, queue_subscribe(inner, id, kind, names))) + .collect(); + let mut errors = vec![]; + for (kind, queued) in queued { + let result = match queued { + Ok(None) => Ok(()), + // dropped unsent when the connection closes, the reader fails it first + Ok(Some((names, confirmation))) => { + let result = confirmation + .await + .unwrap_or_else(|_| Err(closed_error(&inner.state))); + if result.is_err() { + abandon( + &mut inner.state.borrow_mut().subscriptions, + id, + kind, + &names, + ); + } + result + } + Err(error) => Err(error), + }; + if let Err(error) = result { + errors.push((kind.subscribe(), error)); + } + } + aggregate(errors, several) +} + +/// Queue SUBSCRIBE for the targets the subscription does not hold yet, none when it holds them all. +fn queue_subscribe( + inner: &Inner, + id: u64, + kind: Kind, + names: Vec, +) -> Result, Confirmation)>, Error> { + let names: Vec = { + let mut state = inner.state.borrow_mut(); + // before the targets are touched, they stay as they were when the connection closed + if let Some(error) = &state.closed { + return Err(error.clone()); + } + let subscriptions = &mut state.subscriptions; + let Some(sub) = subscriptions.subs.get(&id) else { + return Ok(None); + }; + let names: Vec = unique(names) + .into_iter() + .filter(|name| { + // a target being left is subscribed to again, after its UNSUBSCRIBE + matches!(sub.hold(&(kind, name.clone())), None | Some(Hold::Leaving)) + }) + .collect(); + for name in &names { + subscriptions.hold(id, &(kind, name.clone()), Hold::Pending); + } + names + }; + if names.is_empty() { + return Ok(None); + } + let command = std::iter::once(kind.subscribe().to_string()) + .chain(names.iter().cloned()) + .collect(); + let (done_tx, done_rx) = oneshot::channel(); + let reply = Reply::Subscribe { + kind, + channels: names.clone(), + id, + done: done_tx, + }; + match enqueue(inner, command, Some(reply), None) { + Ok(()) => Ok(Some((names, done_rx))), + Err(error) => { + abandon( + &mut inner.state.borrow_mut().subscriptions, + id, + kind, + &names, + ); + Err(error) + } + } +} + +/// SUBSCRIBE failed, the subscription lets go of the targets it was waiting for. +fn abandon(subscriptions: &mut Subscriptions, id: u64, kind: Kind, names: &[String]) { + for name in names { + let target = (kind, name.clone()); + let pending = subscriptions + .subs + .get(&id) + .and_then(|sub| sub.hold(&target)) + == Some(Hold::Pending); + if pending { + subscriptions.release(id, &target); + } + } +} + +/// Unsubscribe from the targets, none for every target of the kind, one command for each kind, +/// queued with no other command between them. Every command is sent, the error holds each that +/// failed. +async fn unsubscribe( + inner: &Inner, + id: u64, + groups: Vec<(Kind, Option>)>, +) -> Result<(), Error> { + let several = groups.len() > 1; + // no await until every command is queued + let queued: Vec<_> = groups + .into_iter() + .map(|(kind, names)| (kind, queue_unsubscribe(inner, id, kind, names))) + .collect(); + let mut errors = vec![]; + for (kind, queued) in queued { + let result = match queued { + Ok(None) => Ok(()), + // dropped unsent when the connection closes, the reader fails it first + Ok(Some(confirmation)) => confirmation + .await + .unwrap_or_else(|_| Err(closed_error(&inner.state))), + Err(error) => Err(error), + }; + if let Err(error) = result { + errors.push((kind.unsubscribe(), error)); + } + } + aggregate(errors, several) +} + +/// Queue UNSUBSCRIBE for the targets no other subscription holds, the subscription lets go of the +/// others at once. None when there is nothing to send. +fn queue_unsubscribe( + inner: &Inner, + id: u64, + kind: Kind, + names: Option>, +) -> Result, Error> { + let release = { + let mut state = inner.state.borrow_mut(); + // before the targets are touched, they stay as they were when the connection closed + if let Some(error) = &state.closed { + return Err(error.clone()); + } + let subscriptions = &mut state.subscriptions; + let Some(sub) = subscriptions.subs.get(&id) else { + return Ok(None); + }; + let held = |name: &String| { + matches!( + sub.hold(&(kind, name.clone())), + Some(Hold::Pending | Hold::Active) + ) + }; + let names: Vec = match names { + Some(names) => unique(names).into_iter().filter(held).collect(), + None => sub + .targets + .iter() + .filter(|((held_kind, name), _)| *held_kind == kind && held(name)) + .map(|((_, name), _)| name.clone()) + .collect(), + }; + let mut release = vec![]; + for name in names { + let target = (kind, name); + let kept = subscriptions + .holding(&target, &[Hold::Pending, Hold::Active]) + .iter() + .any(|other| *other != id); + if kept { + subscriptions.release(id, &target); + subscriptions.emit(id, unsubscribed(&target, UnsubscribeReason::Requested)); + } else { + subscriptions.hold(id, &target, Hold::Leaving); + release.push(target.1); + } + } + release + }; + if release.is_empty() { + return Ok(None); + } + let command = std::iter::once(kind.unsubscribe().to_string()) + .chain(release.iter().cloned()) + .collect(); + let (done_tx, done_rx) = oneshot::channel(); + let reply = Reply::Unsubscribe { + kind, + channels: release, + done: done_tx, + }; + enqueue(inner, command, Some(reply), None)?; + Ok(Some(done_rx)) +} + +/// The errors of the commands of a call as one, each prefixed with its command when the call sent +/// several. An error of one kind keeps it, e.g. every command was refused by the server, otherwise +/// it is a client error. +fn aggregate(errors: Vec<(&str, Error)>, several: bool) -> Result<(), Error> { + let mut errors = errors.into_iter(); + let Some((command, first)) = errors.next() else { + return Ok(()); + }; + if !several { + return Err(first); + } + let kind = std::mem::discriminant(&first); + let mut same = true; + let mut messages = vec![format!("{command}: {}", describe_error(&first))]; + for (command, error) in errors { + same &= std::mem::discriminant(&error) == kind; + messages.push(format!("{command}: {}", describe_error(&error))); + } + if messages.len() == 1 { + return Err(match first { + Error::Valkey(_) => Error::Valkey(messages.remove(0)), + Error::Client(_) => Error::Client(messages.remove(0)), + Error::Resp(_) => Error::Resp(messages.remove(0)), + error => error, + }); + } + let message = messages.join("; "); + Err(match first { + // the connection closed, every command failed the same way + error @ (Error::Closed(_) | Error::Lagged) if same => error, + Error::Valkey(_) if same => Error::Valkey(message), + Error::Resp(_) if same => Error::Resp(message), + _ => Error::Client(message), + }) +} + +/// An error for people to read, in an aggregate of errors. +fn describe_error(error: &Error) -> String { + match error { + Error::Closed(None) => "connection closed".to_string(), + Error::Closed(Some(reason)) => format!("connection closed: {reason}"), + Error::Client(message) | Error::Resp(message) | Error::Valkey(message) => message.clone(), + Error::Lagged => "fell behind".to_string(), + } +} + +/// RESET, then HELLO again, with no other command between them, closing the connection when HELLO +/// fails. +/// +/// The writer sends no other command until HELLO succeeds, so none runs on the connection RESET +/// leaves: RESP2, the default user, no name. When HELLO fails, e.g. the credentials no longer work, +/// the reader closes the connection, and the commands queued after it fail without being sent. +async fn reset(inner: &Inner) -> Result<(), Error> { + let mut state = inner.state.borrow_mut(); + if let Some(error) = &state.closed { + return Err(error.clone()); + } + let hello = state.hello.clone(); + let (reset_tx, reset_rx) = oneshot::channel(); + let (proceed_tx, proceed_rx) = oneshot::channel(); + let (hello_tx, hello_rx) = oneshot::channel(); + let requests = [(words(&["RESET"]), None), (hello, Some(proceed_rx))]; + for (command, barrier) in requests { + let command = RespValue::Array( + command + .into_iter() + .map(|word| componentized_valkey::resp::encode(RespValue::BulkString(word))) + .collect(), + ); + inner + .requests + .unbounded_send(Request { + command, + written: None, + barrier, + }) + .map_err(|_| Error::Closed(None))?; + } + state.pending.push_back(Reply::Reset(reset_tx)); + state.pending.push_back(Reply::Rehello { + proceed: proceed_tx, + done: hello_tx, + }); + drop(state); + // the replies are dropped unsent when the connection closes, the reader fails them first + let reset = reset_rx + .await + .unwrap_or_else(|_| Err(closed_error(&inner.state))); + hello_rx + .await + .unwrap_or_else(|_| Err(closed_error(&inner.state)))?; + reset +} + /// Queue a command, and where its reply goes, as one step so replies pair with commands in order. /// A command without a reply takes no place in the order. fn enqueue( @@ -218,16 +991,27 @@ fn enqueue( ); inner .requests - .unbounded_send(Request { command, written }) + .unbounded_send(Request { + command, + written, + barrier: None, + }) .map_err(|_| Error::Closed(None))?; state.pending.extend(reply); Ok(()) } async fn exchange(inner: &Inner, command: Vec) -> Result { + reject_managed(inner, &command)?; if replies_with_pushes(&command) { return emit(inner, command).await.map(|()| RespValue::Null); } + request(inner, command).await +} + +/// Send a command and wait for its reply, for the commands the connection sends itself, e.g. HELLO, +/// which callers may not. +async fn request(inner: &Inner, command: Vec) -> Result { let (reply_tx, reply_rx) = oneshot::channel(); enqueue(inner, command, Some(Reply::Call(reply_tx)), None)?; // the reply is dropped unsent when the connection closes, the reader fails it first @@ -242,6 +1026,7 @@ async fn exchange_all(inner: &Inner, commands: Vec>) -> Vec>, Error>> = commands .into_iter() .map(|command| { + reject_managed(inner, &command)?; if replies_with_pushes(&command) { // no reply, as for `exchange` return enqueue(inner, command, None, None).map(|()| None); @@ -275,6 +1060,9 @@ async fn transaction( "a command requires at least one word".to_string(), )); } + for command in &commands { + reject_managed(inner, command)?; + } let batch = std::iter::once(words(&["MULTI"])) .chain(commands) .chain(std::iter::once(words(&["EXEC"]))) @@ -299,6 +1087,7 @@ async fn transaction( } async fn emit(inner: &Inner, command: Vec) -> Result<(), Error> { + reject_managed(inner, &command)?; let reply = (!replies_with_pushes(&command)).then_some(Reply::Discard); let (written_tx, written_rx) = oneshot::channel(); enqueue(inner, command, reply, Some(written_tx))?; @@ -306,6 +1095,37 @@ async fn emit(inner: &Inner, command: Vec) -> Result<(), Error> { written_rx.await.map_err(|_| closed_error(&inner.state)) } +/// Rejects the commands that would leave the server's state out of step with the connection's: +/// +/// - RESET, which switches the connection to RESP2, see `reset` +/// - HELLO, which may switch the connection to RESP2, or another user, see `server-properties` +/// - CLIENT REPLY, which stops replies from pairing with commands +/// - SUBSCRIBE and the like once the connection owns the subscriptions, see +/// `State::owns_subscriptions`, e.g. an UNSUBSCRIBE would end a subscription without it asking +fn reject_managed(inner: &Inner, command: &[String]) -> Result<(), Error> { + let word = |index: usize| command.get(index).map(|word| word.to_uppercase()); + let name = word(0).unwrap_or_default(); + let reason = match (name.as_str(), word(1).as_deref()) { + ("RESET", _) => Some("RESET is rejected, use the connection's reset"), + ("HELLO", _) => Some( + "HELLO is rejected, the connection sent it when it was established, see its server-properties", + ), + ("CLIENT", Some("REPLY")) => { + Some("CLIENT REPLY is rejected, every command has a reply, see the connection's emit") + } + _ => None, + }; + if let Some(reason) = reason { + return Err(Error::Client(reason.to_string())); + } + if replies_with_pushes(command) && inner.state.borrow().owns_subscriptions { + return Err(Error::Client(format!( + "{name} is rejected once the connection has a subscription, use the subscription's methods" + ))); + } + Ok(()) +} + /// Whether the command's replies are push messages, e.g. SUBSCRIBE, rather than a reply. /// /// In RESP3 the server confirms each channel subscribed to, or unsubscribed from, with a push @@ -327,22 +1147,157 @@ fn replies_with_pushes(command: &[String]) -> bool { }) } +/// A push message the connection understands. +enum Push { + /// The server subscribed to the target. + Subscribed(Target), + /// The server unsubscribed from the target, none when nothing was subscribed. + Unsubscribed(Kind, Option), + /// A message, for the target it was subscribed with. + Message(Target, SubscriptionMessage), +} + +fn parse_push(value: &RespValue) -> Option { + let RespValue::Push(items) = value else { + return None; + }; + let items = items + .iter() + .map(|item| decode(item.clone()).ok()) + .collect::>>()?; + let text = |index: usize| match items.get(index) { + Some(RespValue::String(text) | RespValue::BulkString(text)) => Some(text.clone()), + _ => None, + }; + let push = match text(0)?.as_str() { + "subscribe" => Push::Subscribed((Kind::Channel, text(1)?)), + "psubscribe" => Push::Subscribed((Kind::Pattern, text(1)?)), + "ssubscribe" => Push::Subscribed((Kind::Shard, text(1)?)), + "unsubscribe" => Push::Unsubscribed(Kind::Channel, text(1)), + "punsubscribe" => Push::Unsubscribed(Kind::Pattern, text(1)), + "sunsubscribe" => Push::Unsubscribed(Kind::Shard, text(1)), + "message" => { + let channel = text(1)?; + let message = SubscriptionMessage { + payload: text(2)?, + channel: channel.clone(), + target: SubscriptionTarget::Channel(channel.clone()), + }; + Push::Message((Kind::Channel, channel), message) + } + "pmessage" => { + let pattern = text(1)?; + let message = SubscriptionMessage { + payload: text(3)?, + channel: text(2)?, + target: SubscriptionTarget::Pattern(pattern.clone()), + }; + Push::Message((Kind::Pattern, pattern), message) + } + "smessage" => { + let shardchannel = text(1)?; + let message = SubscriptionMessage { + payload: text(2)?, + channel: shardchannel.clone(), + target: SubscriptionTarget::Shardchannel(shardchannel.clone()), + }; + Push::Message((Kind::Shard, shardchannel), message) + } + _ => return None, + }; + Some(push) +} + +/// Hand a push message to the subscriptions, or the command, it belongs to, false when it belongs +/// to none. +fn claim_push(state: &mut State, value: &RespValue) -> bool { + let State { + pending, + subscriptions, + .. + } = state; + match parse_push(value) { + Some(Push::Subscribed(target)) => { + // the confirmations of a SUBSCRIBE arrive in the place of its reply + let Some(Reply::Subscribe { + kind, channels, id, .. + }) = pending.front_mut() + else { + return false; + }; + if *kind != target.0 { + return false; + } + let Some(index) = channels.iter().position(|name| *name == target.1) else { + return false; + }; + channels.remove(index); + let (id, done) = (*id, channels.is_empty()); + subscriptions.subscribed(id, &target); + if done && let Some(Reply::Subscribe { done, .. }) = pending.pop_front() { + let _ = done.send(Ok(())); + } + true + } + Some(Push::Unsubscribed(kind, Some(name))) => { + let target = (kind, name); + // the confirmations of an UNSUBSCRIBE arrive in the place of its reply, otherwise the + // server unsubscribed on its own + let confirmation = match pending.front_mut() { + Some(Reply::Unsubscribe { kind, channels, .. }) if *kind == target.0 => channels + .iter() + .position(|name| *name == target.1) + .map(|index| { + channels.remove(index); + channels.is_empty() + }), + _ => None, + }; + match confirmation { + Some(done) => { + subscriptions.unsubscribe_confirmed(&target); + if done && let Some(Reply::Unsubscribe { done, .. }) = pending.pop_front() { + let _ = done.send(Ok(())); + } + true + } + None => subscriptions.unsubscribed_by_server(&target), + } + } + Some(Push::Message(target, message)) => subscriptions.message(&target, message), + Some(Push::Unsubscribed(_, None)) | None => false, + } +} + /// The error a command fails with once the connection closed. fn closed_error(state: &RefCell) -> Error { state.borrow().closed.clone().unwrap_or(Error::Closed(None)) } -/// Close the connection, failing every command waiting for a reply and closing the readers of push -/// messages. +/// Close the connection, failing every command waiting for a reply, closing the readers of dead +/// letters, and ending the subscriptions. fn close(state: &RefCell, error: Error) { let mut state = state.borrow_mut(); let error = state.closed.get_or_insert(error).clone(); - for reply in state.pending.drain(..) { - if let Reply::Call(reply) = reply { - let _ = reply.send(Err(error.clone())); + for reply in state.pending.drain(..).collect::>() { + match reply { + Reply::Call(reply) => { + let _ = reply.send(Err(error.clone())); + } + Reply::Subscribe { done, .. } + | Reply::Unsubscribe { done, .. } + | Reply::Reset(done) => { + let _ = done.send(Err(error.clone())); + } + Reply::Rehello { proceed, done } => { + let _ = proceed.send(false); + let _ = done.send(Err(error.clone())); + } + Reply::Discard => {} } } - state.pushes.clear(); + state.dead_letters.clear(); + state.subscriptions.close(&error); } /// The next item of a channel. @@ -360,7 +1315,12 @@ async fn write_requests( mut requests: mpsc::UnboundedReceiver, mut output: StreamWriter, ) { - while let Some(Request { command, written }) = next(&mut requests).await { + while let Some(Request { + command, + written, + barrier, + }) = next(&mut requests).await + { state.borrow_mut().activity += 1; if output.write_one(command).await.is_some() { // the transport stopped reading, the reader closes the connection once it ends @@ -369,35 +1329,104 @@ async fn write_requests( if let Some(written) = written { let _ = written.send(()); } + if let Some(barrier) = barrier + && !barrier.await.unwrap_or(false) + { + // the commands after it are never sent, those waiting to be written fail + requests.close(); + while requests.try_recv().is_ok() {} + // dropping the requests stream closes the transport, the reader closed the connection + break; + } } } -/// Hand each reply to the oldest command waiting for one, and each push message, or value the server -/// sent unprompted, to the readers of push messages, until the transport closes. +/// Hand each reply to the oldest command waiting for one, each message to its subscriptions, and +/// each other push message, or value the server sent unprompted, to the readers of dead letters, +/// until the transport closes. async fn read_replies( state: Rc>, mut replies: StreamReader, closed: FutureReader>, ) { + // the error to close the connection with, before the transport closes + let mut closing = None; while let Some(value) = replies.next().await { let mut state = state.borrow_mut(); + let state = &mut *state; state.activity += 1; let value = match value { - RespValue::Push(_) => value, + push @ RespValue::Push(_) => { + if claim_push(state, &push) { + continue; + } + push + } value => match state.pending.pop_front() { Some(Reply::Call(reply)) => { let _ = reply.send(reply_result(value)); continue; } Some(Reply::Discard) => continue, + Some(Reply::Rehello { proceed, done }) => { + match reply_result(value).and_then(server_properties) { + Ok(properties) => { + state.server_properties = properties; + let _ = proceed.send(true); + let _ = done.send(Ok(())); + } + Err(error) => { + // the connection is not the one it was established with + let _ = proceed.send(false); + let _ = done.send(Err(error.clone())); + closing = Some(error); + break; + } + } + continue; + } + Some(Reply::Reset(done)) => { + let result = match reply_result(value) { + Ok(RespValue::String(reply)) if reply == "RESET" => { + state.subscriptions.reset(); + Ok(()) + } + Ok(reply) => Err(unexpected(&reply)), + Err(error) => Err(error), + }; + let _ = done.send(result); + continue; + } + // confirmed with push messages, a reply in their place is an error + Some(Reply::Subscribe { + kind, + channels, + id, + done, + }) => { + abandon(&mut state.subscriptions, id, kind, &channels); + let _ = + done.send(reply_result(value).and_then(|reply| Err(unexpected(&reply)))); + continue; + } + Some(Reply::Unsubscribe { done, .. }) => { + let _ = + done.send(reply_result(value).and_then(|reply| Err(unexpected(&reply)))); + continue; + } // sent unprompted, e.g. the feed of MONITOR None => value, }, }; // a reader that is full or gone is dropped, rather than holding up replies state - .pushes - .retain_mut(|pushes| pushes.try_send(value.clone()).is_ok()); + .dead_letters + .retain_mut(|dead_letters| dead_letters.try_send(value.clone()).is_ok()); + } + if let Some(error) = closing { + // the writer stopped, dropping the requests stream closes the transport + close(&state, error); + return; } let reason = match closed.await { Ok(()) => None, @@ -410,7 +1439,7 @@ async fn read_replies( async fn supervise(mut tasks: mpsc::UnboundedReceiver, inner: Weak) { while let Some(task) = next(&mut tasks).await { match task { - Task::Pushes(pushes, writer) => spawn_local(forward_pushes(pushes, writer)), + Task::Spawn(future) => spawn_local(future), Task::KeepAlive { generation, interval, @@ -419,18 +1448,38 @@ async fn supervise(mut tasks: mpsc::UnboundedReceiver, inner: Weak) } } -/// Forward push messages to a reader's stream, until the reader or the connection is gone. -async fn forward_pushes( - mut pushes: mpsc::Receiver, +/// Forward values to a reader's stream, until the reader or the connection is gone. +async fn forward_values( + mut values: mpsc::Receiver, mut writer: StreamWriter, ) { - while let Some(value) = next(&mut pushes).await { + while let Some(value) = next(&mut values).await { if writer.write_one(value).await.is_some() { break; } } } +/// Forward a subscription's events to a reader's stream, then close the stream and tell the reader +/// how it ended. +async fn forward_events( + mut events: mpsc::Receiver, + end: oneshot::Receiver, + mut writer: StreamWriter, + end_writer: FutureWriter, +) { + while let Some(event) = next(&mut events).await { + if writer.write_one(event).await.is_some() { + // the stream was dropped, the subscription carries on without it + break; + } + } + drop(events); + drop(writer); + let end = end.await.unwrap_or_else(|_| Err(Error::Closed(None))); + let _ = end_writer.write(end).await; +} + /// Send PING whenever the connection was idle for `interval`, closing it if PING fails. async fn keep_alive(inner: Weak, generation: u64, interval: DurationMilliseconds) { loop { @@ -485,7 +1534,8 @@ async fn establish( hello.extend(["SETNAME".to_string(), client_name]); } - let result = match exchange(&inner, hello).await.and_then(server_properties) { + inner.state.borrow_mut().hello = hello.clone(); + let result = match request(&inner, hello).await.and_then(server_properties) { Ok(properties) => { inner.state.borrow_mut().server_properties = properties; let established = ConnectionImpl(inner); @@ -551,6 +1601,7 @@ fn describe(error: Error) -> Option { match error { Error::Closed(reason) => reason, Error::Client(message) | Error::Resp(message) | Error::Valkey(message) => Some(message), + Error::Lagged => Some("a stream fell behind".to_string()), } } diff --git a/components/dep-cli/src/lib.rs b/components/dep-cli/src/lib.rs index 2e5726d..7b558af 100644 --- a/components/dep-cli/src/lib.rs +++ b/components/dep-cli/src/lib.rs @@ -66,7 +66,7 @@ impl Guest for Cli { async fn exec(command: Vec, out: &mut Output) -> Result<(), Error> { let connection = connector::connect().await?; // before the command is sent, so none of the push messages it causes are missed - let mut pushes = connection.pushes(); + let mut pushes = connection.dead_letters(); let name = command[0].to_ascii_uppercase(); let channels = command.len() - 1; @@ -78,6 +78,11 @@ async fn exec(command: Vec, out: &mut Output) -> Result<(), Error> { // not subscribed to any connection.emit(command).await?; print_pushes(&mut pushes, out, Some(channels.max(1))).await + } else if name == "RESET" && channels == 0 { + // RESET is the connection's to send, it then repeats its handshake + connection.reset().await?; + out.line("RESET").await; + Ok(()) } else if name == "MONITOR" { // the feed follows the reply, values sent unprompted that the connection treats as push // messages @@ -119,7 +124,7 @@ async fn print_pushes( /// until stdin closes. async fn session(out: &mut Output) -> Result<(), Error> { let connection = connector::connect().await?; - let pushes = connection.pushes(); + let pushes = connection.dead_letters(); let (stdin, _stdin_result) = stdin::read_via_stream(); // replies and push messages arrive independently, both are printed by one writer @@ -172,10 +177,20 @@ async fn send_lines( connection.emit(words).await?; continue; } - let text = match connection.exchange(words).await { + let reply = if name == "RESET" && words.len() == 1 { + // RESET is the connection's to send, it then repeats its handshake + connection + .reset() + .await + .map(|()| RespValue::String("RESET".to_string())) + } else { + connection.exchange(words).await + }; + let text = match reply { Ok(reply) => render(reply), // the command failed, not the session Err(Error::Valkey(message)) => format!("(error) {message}"), + Err(Error::Client(message)) => format!("(error) {message}"), Err(error) => return Err(error), }; let _ = lines.unbounded_send(text); @@ -339,6 +354,7 @@ fn render_error(error: Error) -> String { Error::Closed(Some(reason)) => format!("connection closed: {reason}"), Error::Client(message) => message, Error::Resp(message) => format!("invalid RESP: {message}"), + Error::Lagged => "fell behind, values were lost".to_string(), } } diff --git a/crates/componentized-valkey/src/commands/connection.rs b/crates/componentized-valkey/src/commands/connection.rs index 71b08bb..a90f6b3 100644 --- a/crates/componentized-valkey/src/commands/connection.rs +++ b/crates/componentized-valkey/src/commands/connection.rs @@ -1,8 +1,8 @@ //! The connection commands: CLIENT of the connection itself, and COMMAND. -use crate::command::{Flag, IntoCommand, NamedOption, NonEmpty}; +use crate::command::{Flag, IntoCommand, NonEmpty}; use crate::types::{ - ClientSetinfoAttr, ClientTrackingMode, ClientTrackingOptions, ClientTrackingRedirect, + ClientSetinfoAttr, ClientTrackingMode, ClientTrackingOptions, ClientTrackinginfoRedirect, ClientTrackinginfoReply, CommandDocsArgument, CommandDocsArgumentType, CommandDocsReply, CommandInfoReply, CommandKeySpec, CommandKeySpecBeginSearch, CommandKeySpecFindKeys, CommandKeySpecKeynum, CommandKeySpecKeyword, CommandKeySpecRange, CommandListFilter, Error, @@ -29,7 +29,7 @@ impl IntoCommand for ClientTrackingMode { } } -/// `[ REDIRECT client-id ] [ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN | OPTOUT ] [ NOLOOP ]` +/// `[ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN | OPTOUT ] [ NOLOOP ]` impl IntoCommand for ClientTrackingOptions { fn into_command(self) -> Result, Error> { // PREFIX before each prefix @@ -38,7 +38,6 @@ impl IntoCommand for ClientTrackingOptions { prefixes.collect::>() }); ( - NamedOption("REDIRECT", self.redirect), NonEmpty::optional("prefixes", prefixes), Flag("BCAST", self.bcast), self.mode, @@ -49,12 +48,12 @@ impl IntoCommand for ClientTrackingOptions { } /// `-1` when tracking is off, `0` when not redirected, otherwise the ID of the connection. -impl FromValue for ClientTrackingRedirect { +impl FromValue for ClientTrackinginfoRedirect { fn from_value(value: RespValue) -> Result { match Integer::from_value(value)? { - -1 => Ok(ClientTrackingRedirect::NotTracking), - 0 => Ok(ClientTrackingRedirect::NotRedirected), - id if id > 0 => Ok(ClientTrackingRedirect::Redirected(id)), + -1 => Ok(ClientTrackinginfoRedirect::NotTracking), + 0 => Ok(ClientTrackinginfoRedirect::NotRedirected), + id if id > 0 => Ok(ClientTrackinginfoRedirect::Redirected(id)), code => Err(Error::Client(format!("unexpected reply: {code}"))), } } @@ -293,7 +292,6 @@ mod tests { #[test] fn client_tracking_options() { let options = ClientTrackingOptions { - redirect: Some(5), prefixes: Some(vec!["a:".to_string(), "b:".to_string()]), bcast: true, mode: Some(ClientTrackingMode::Optin), @@ -301,12 +299,9 @@ mod tests { }; assert_eq!( words(options), - [ - "REDIRECT", "5", "PREFIX", "a:", "PREFIX", "b:", "BCAST", "OPTIN", "NOLOOP" - ] + ["PREFIX", "a:", "PREFIX", "b:", "BCAST", "OPTIN", "NOLOOP"] ); let options = ClientTrackingOptions { - redirect: None, prefixes: None, bcast: false, mode: Some(ClientTrackingMode::Optout), @@ -314,7 +309,6 @@ mod tests { }; assert_eq!(words(options), ["OPTOUT"]); let options = ClientTrackingOptions { - redirect: None, prefixes: Some(vec![]), bcast: true, mode: None, @@ -324,21 +318,21 @@ mod tests { } #[test] - fn client_tracking_redirect() { + fn client_trackinginfo_redirect() { assert_eq!( - from::(int(-1)), - ClientTrackingRedirect::NotTracking + from::(int(-1)), + ClientTrackinginfoRedirect::NotTracking ); assert_eq!( - from::(int(0)), - ClientTrackingRedirect::NotRedirected + from::(int(0)), + ClientTrackinginfoRedirect::NotRedirected ); assert_eq!( - from::(int(7)), - ClientTrackingRedirect::Redirected(7) + from::(int(7)), + ClientTrackinginfoRedirect::Redirected(7) ); assert_eq!( - from_error::(int(-2)), + from_error::(int(-2)), "unexpected reply: -2" ); } @@ -354,7 +348,7 @@ mod tests { from::(value), ClientTrackinginfoReply { flags: vec!["on".to_string(), "bcast".to_string()], - redirect: ClientTrackingRedirect::NotRedirected, + redirect: ClientTrackinginfoRedirect::NotRedirected, prefixes: vec!["a:".to_string()], } ); diff --git a/wit/commands.wit b/wit/commands.wit index 652ae1f..20c91c5 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -27,10 +27,10 @@ interface commands { command-key-spec-begin-search, command-key-spec-keyword, command-key-spec-find-keys, command-key-spec-range, command-key-spec-keynum, command-key-spec, command-info-reply, command-reply, command-docs-argument-type, command-docs-argument, command-docs-reply, - client-tracking-redirect, client-setinfo-attr, client-tracking-mode, - client-tracking-options, client-trackinginfo-reply, command-list-filter, geo-coordinates, - geo-unit, geoadd-condition, geosearch-from, geosearch-radius, geosearch-box, - geosearch-shape, geosearchstore-shape, geosearch-count, geosearchstore-count, + client-setinfo-attr, client-tracking-mode, client-tracking-options, + client-trackinginfo-redirect, client-trackinginfo-reply, command-list-filter, + geo-coordinates, geo-unit, geoadd-condition, geosearch-from, geosearch-radius, + geosearch-box, geosearch-shape, geosearchstore-shape, geosearch-count, geosearchstore-count, geosearch-with-flags, geosearch-with-result, hpersist-result, hsetex-condition, hsetex-field-condition, key-type, migrate-flags, migrate-auth, list-side, linsert-position, linsert-result, flush-mode, function-list-function, function-list-library, @@ -562,12 +562,6 @@ interface commands { /// https://valkey.io/commands/client-getname/ client-getname: async func(connection: borrow) -> result, error>; - /// Where the connection's tracking notifications are redirected. - /// - /// CLIENT GETREDIR - /// https://valkey.io/commands/client-getredir/ - client-getredir: async func(connection: borrow) -> result; - /// The ID of the connection, unique until the server restarts. /// /// CLIENT ID @@ -608,11 +602,11 @@ interface commands { client-setname: async func(connection: borrow, connection-name: string) -> result<_, error>; /// Turns tracking on with the options, or off with none, for server assisted client side - /// caching. Invalidations arrive as push messages, see `connection.pushes`. + /// caching. Invalidations arrive as push messages, see `connection.dead-letters`. /// /// It applies to the connection, so to every caller sharing it. /// - /// CLIENT TRACKING 〈 ON | OFF 〉 [ REDIRECT client-id ] [ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN ] [ OPTOUT ] [ NOLOOP ] + /// CLIENT TRACKING 〈 ON | OFF 〉 [ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN ] [ OPTOUT ] [ NOLOOP ] /// https://valkey.io/commands/client-tracking/ client-tracking: async func(connection: borrow, options: option) -> result<_, error>; @@ -673,14 +667,6 @@ interface commands { /// https://valkey.io/commands/echo/ echo: async func(connection: borrow, message: string) -> result; - /// Switch to a different protocol, optionally authenticating and setting the connection's name, or provide a contextual client report. - /// - /// RESP2 is not fully supported by this client. - /// - /// HELLO 3 [ AUTH username password ] [ SETNAME clientname ] - /// https://valkey.io/commands/hello/ - hello: async func(connection: borrow, auth: option>, client-name: option) -> result, error>; - /// Returns PONG if no argument is provided, otherwise return a copy of the argument as a bulk. /// /// PING [ message ] @@ -1433,12 +1419,6 @@ interface commands { // pubsub commands - /// Subscribes the client to the given patterns. - /// - /// PSUBSCRIBE pattern [ pattern ... ] - /// https://valkey.io/commands/psubscribe/ - psubscribe: async func(connection: borrow, patterns: list) -> result<_, error>; - /// Posts a message to the given channel. /// /// PUBLISH channel message @@ -1475,42 +1455,12 @@ interface commands { /// https://valkey.io/commands/pubsub-shardnumsub/ pubsub-shardnumsub: async func(connection: borrow, shardchannels: option>) -> result, error>; - /// Unsubscribes the client from the given patterns, or from all of them if none is given. - /// - /// PUNSUBSCRIBE [ pattern [ pattern ... ] ] - /// https://valkey.io/commands/punsubscribe/ - punsubscribe: async func(connection: borrow, patterns: option>) -> result<_, error>; - /// Posts a message to the given shard channel. /// /// SPUBLISH shardchannel message /// https://valkey.io/commands/spublish/ spublish: async func(connection: borrow, shardchannel: string, message: string) -> result; - /// Subscribes the client to the specified shard channels. - /// - /// SSUBSCRIBE shardchannel [ shardchannel ... ] - /// https://valkey.io/commands/ssubscribe/ - ssubscribe: async func(connection: borrow, shardchannels: list) -> result<_, error>; - - /// Subscribes the client to the specified channels. - /// - /// SUBSCRIBE channel [ channel ... ] - /// https://valkey.io/commands/subscribe/ - subscribe: async func(connection: borrow, channels: list) -> result<_, error>; - - /// Unsubscribes the client from the given shard channels, or from all of them if none is given. - /// - /// SUNSUBSCRIBE [ shardchannel ] [ [ shardchannel ] ... ] - /// https://valkey.io/commands/sunsubscribe/ - sunsubscribe: async func(connection: borrow, shardchannels: option>) -> result<_, error>; - - /// Unsubscribes the client from the given channels, or from all of them if none is given. - /// - /// UNSUBSCRIBE [ channel ] [ [ channel ] ... ] - /// https://valkey.io/commands/unsubscribe/ - unsubscribe: async func(connection: borrow, channels: option>) -> result<_, error>; - // scripting commands diff --git a/wit/connection.wit b/wit/connection.wit index 9ece459..2c4cbf7 100644 --- a/wit/connection.wit +++ b/wit/connection.wit @@ -6,10 +6,10 @@ /// that does this for its caller. /// /// The connection is the mechanics of talking to a server: sending commands and pairing replies -/// with them, push messages, and transactions. Higher level, typed operations are functions in the -/// `commands` interface that borrow the connection. +/// with them, subscriptions, push messages, and transactions. Higher level, typed operations are +/// functions in the `commands` interface that borrow the connection. interface connection { - use types.{connect-opts, duration-milliseconds, error, resp-value}; + use types.{connect-opts, duration-milliseconds, error, resp-value, subscription-event, subscription-target}; resource connection { /// Establishes a connection over a transport. @@ -36,9 +36,14 @@ interface connection { /// Several commands may be in flight at once, replies are paired with commands in the order /// the commands were sent. /// - /// SUBSCRIBE, PSUBSCRIBE, SSUBSCRIBE and their UNSUBSCRIBE counterparts have no reply, the - /// server confirms them with push messages, see `pushes`. Their exchange resolves to null - /// once the command is written. + /// For SUBSCRIBE, PSUBSCRIBE, SSUBSCRIBE and their UNSUBSCRIBE counterparts, prefer a + /// `subscription`, which owns its targets' messages. Once the connection has one, it alone + /// sends these commands, and they fail with a client error before anything is sent, here, + /// with `exchange-all`, `emit` and `transaction`. + /// + /// RESET, HELLO and CLIENT REPLY always fail with a client error. See `reset` and + /// `server-properties`, CLIENT REPLY would stop replies from pairing with commands, see + /// `emit`. /// /// A command has at least one word, an empty command fails with a client error. exchange: async func(command: list) -> result; @@ -94,15 +99,47 @@ interface connection { /// https://valkey.io/commands/exec/ transaction: async func(commands: list>) -> result>, error>; - /// Push messages, e.g. from SUBSCRIBE or client tracking, kept apart from replies. + /// A new subscription, subscribed to nothing yet, see the subscription's `subscribe`. + /// + /// From the first subscription, the connection owns its subscriptions: SUBSCRIBE, + /// PSUBSCRIBE, SSUBSCRIBE and their UNSUBSCRIBE counterparts sent with `exchange` and the like + /// fail with a client error, so the server's subscriptions stay in step with the + /// connection's. Subscriptions made with `exchange` before are unsupported. They may keep + /// delivering to `dead-letters`, the exact behavior is undocumented. + subscription: func() -> subscription; + + /// The values nothing else claims, kept apart from replies. /// - /// Values the server sends unprompted, when no command is waiting for a reply, are - /// included, e.g. the feed of MONITOR, which are not RESP push values. + /// Push messages that no subscription owns, e.g. client tracking invalidations, messages + /// and confirmations of SUBSCRIBE sent with `exchange`, or kinds of push messages from a + /// newer server. Values the server sends unprompted, when no command is waiting for a + /// reply, are included, e.g. the feed of MONITOR, which are not RESP push values. /// - /// Each call returns a new stream that receives every push message arriving after the call, - /// push messages arriving while no stream is open are dropped. A stream that falls too far - /// behind is closed rather than holding up replies, the push messages it missed are lost. - pushes: func() -> stream; + /// Each call returns a new stream that receives every value arriving after the call, + /// values arriving while no stream is open are dropped. A stream that falls too far behind + /// is closed rather than holding up replies, the values it missed are lost. + dead-letters: func() -> stream; + + /// Resets the connection to the state of a new one, then repeats the handshake of `new`. + /// + /// RESET discards a transaction in progress, forgets the watched keys, unsubscribes from + /// every channel, turns off client tracking and the like, selects database 0, and forgets + /// the client name and the authenticated user. It also switches the connection to RESP2, + /// so it is followed at once by `HELLO 3` with the `auth` and `client-name` the connection + /// was established with, with no other command sent between them. `server-properties` + /// then holds the new reply to HELLO. + /// + /// The subscriptions are unsubscribed from every target, each with the reason `reset`, and + /// stay open for new targets. + /// + /// No other command is sent until the handshake succeeds, so none runs on the connection + /// RESET leaves. When the handshake fails, e.g. the credentials expired since the connection + /// was established, the connection closes with its error, and the commands queued after + /// the reset fail with it without being sent. Establish a new connection. + /// + /// RESET + /// https://valkey.io/commands/reset/ + reset: async func() -> result<_, error>; /// Sends PING when no command has been sent and no reply is outstanding for `interval`, /// `none` disables it. A PING that fails closes the connection. Disabled by default. @@ -111,4 +148,79 @@ interface connection { /// and works over any transport. set-keep-alive: func(interval: option); } + + /// The messages of a set of channels, patterns of channels and shard channels, that changes + /// as targets are subscribed to and unsubscribed from, see `connection.subscription`. + /// + /// Subscriptions sharing a connection may overlap, the server stays subscribed to a target while + /// any subscription has it. The subscription stays open until it is dropped, with or without + /// targets or streams, then it unsubscribes from its targets. It keeps its connection open. + resource subscription { + /// Subscribes to the targets, resolving once the server confirms each of them, see + /// `subscription-event.subscribed`. Targets already subscribed to are left as they are. The + /// server unsubscribes from a shard channel on its own when its slot moves to another node. + /// + /// Each kind of target is subscribed to with its own command, queued with no other command + /// between them. Every command is sent, even when another fails, and those that succeed + /// take effect, see `subscription-event.subscribed` and `targets`. The error then holds the + /// error of each command that failed, e.g. `PSUBSCRIBE: NOPERM ...`. A command is accepted + /// or refused as a whole, targets of one kind are subscribed to all or none. + /// + /// SUBSCRIBE channel [ channel ... ] + /// PSUBSCRIBE pattern [ pattern ... ] + /// SSUBSCRIBE shardchannel [ shardchannel ... ] + /// https://valkey.io/commands/subscribe/ + /// https://valkey.io/commands/psubscribe/ + /// https://valkey.io/commands/ssubscribe/ + /// + /// At least one target, no targets fails with a client error. + subscribe: async func(targets: list) -> result<_, error>; + + /// The events arriving after the call: targets subscribed to and unsubscribed from, and + /// messages, in the order they happened. + /// + /// Each call returns a new stream, every open stream receives every event. Events arriving + /// while no stream is open are kept for the next stream, up to 256, later ones are dropped. + /// Dropping a stream does not unsubscribe, call `events` again for another. + /// + /// Pub/sub delivers each message at most once, to all listeners. A stream receives + /// no events from before it opened, beyond those kept while no stream was open, and does + /// not start with the current targets, see `targets`. Messages published while nothing + /// listens are lost, e.g. before a target is subscribed to, after the connection closed, or + /// past what a lagging stream holds. For delivery that is guaranteed, use a Valkey stream, + /// see XADD and XREADGROUP. + /// + /// The stream closes once the subscription is dropped, the future then resolves to `ok`, + /// or to the error that closed the connection. A stream that falls too far behind is + /// closed rather than holding up the connection, its future resolves to `lagged`, and the + /// events it missed, messages and changes of targets alike, are lost. The future resolves + /// after the stream delivered what it holds, read the stream to its end first. + events: func() -> tuple, future>>; + + /// The targets the server confirmed, in the order they were subscribed to, until they are + /// unsubscribed from. + /// + /// Once the connection closes, the targets confirmed until then are still listed, e.g. to + /// subscribe to them on a new connection. `subscribe` and `unsubscribe` then fail with the + /// error that closed the connection, and leave the targets as they are. + targets: func() -> list; + + /// Unsubscribes from the targets, none for every target, resolving once each is + /// unsubscribed from, see `subscription-event.unsubscribed`. Targets not subscribed to are + /// left as they are. The server stays subscribed to targets other subscriptions have. + /// + /// Each kind of target is unsubscribed from with its own command, as for `subscribe`, e.g. + /// the `targets` may be given as they are. + /// + /// UNSUBSCRIBE [ channel [ channel ... ] ] + /// PUNSUBSCRIBE [ pattern [ pattern ... ] ] + /// SUNSUBSCRIBE [ shardchannel [ shardchannel ... ] ] + /// https://valkey.io/commands/unsubscribe/ + /// https://valkey.io/commands/punsubscribe/ + /// https://valkey.io/commands/sunsubscribe/ + /// + /// A list of targets has at least one target, otherwise fails with a client error. None + /// unsubscribes from every target. + unsubscribe: async func(targets: option>) -> result<_, error>; + } } diff --git a/wit/types.wit b/wit/types.wit index a6f8ea2..b187569 100644 --- a/wit/types.wit +++ b/wit/types.wit @@ -48,6 +48,9 @@ interface types { resp(resp-error), /// Valkey errors are Error or BulkError values returned directly from the server. valkey(string), + /// The reader of a stream fell too far behind and its stream was closed, rather than + /// holding up the connection. What the stream missed is lost. + lagged, } /// Options for establishing a connection. @@ -60,6 +63,51 @@ interface types { keep-alive-interval: option, } + /// What a subscription is subscribed to, see `connection.subscription`. + variant subscription-target { + /// A channel, see SUBSCRIBE. + channel(string), + /// A pattern of channels, see PSUBSCRIBE. + pattern(string), + /// A shard channel, see SSUBSCRIBE. + shardchannel(string), + } + + /// A message published to a channel. + /// + /// A subscription with overlapping targets, e.g. a channel and a pattern matching it, receives + /// a message once for each target. + record subscription-message { + payload: string, + /// The channel the message was published to: a channel, published with PUBLISH, or a shard + /// channel, published with SPUBLISH. Channels and shard channels are separate, a channel + /// and a shard channel of the same name are not the same. `target` tells them apart, a + /// `shardchannel` target for a shard channel, a `channel` or `pattern` target otherwise. + channel: string, + /// The subscription's target that matched the channel, the channel itself, or a pattern + /// matching it. + target: subscription-target, + } + + /// Why a target is no longer subscribed to. + enum unsubscribe-reason { + /// By the subscription, e.g. its `unsubscribe`. + requested, + /// By the server on its own, e.g. the slot of a shard channel moved to another node. + by-server, + /// By `connection.reset`. + reset, + } + + /// What a subscription's stream carries, in the order it happened. + variant subscription-event { + /// The server confirmed the target, its messages may arrive from now on. + subscribed(subscription-target), + /// The target is no longer subscribed to, none of its messages arrive after this. + unsubscribed(tuple), + message(subscription-message), + } + // shared by the commands /// A duration in whole seconds, e.g. `EX seconds`. @@ -872,17 +920,6 @@ interface types { arguments: list, } - /// Where a connection's tracking notifications are redirected, see `client-getredir` and - /// `client-trackinginfo`. - variant client-tracking-redirect { - /// -1: tracking is not on. - not-tracking, - /// 0: tracking is on, the notifications are not redirected. - not-redirected, - /// The ID of the connection the notifications are redirected to. - redirected(integer), - } - /// Information about the library of a connection, see `client-setinfo`. variant client-setinfo-attr { /// `LIB-NAME`: the name of the library, e.g. `valkey-wasm`. @@ -901,8 +938,6 @@ interface types { /// The options of CLIENT TRACKING ON. record client-tracking-options { - /// `REDIRECT client-id`: sends the notifications to the connection with the ID instead. - redirect: option, /// `PREFIX prefix`: with `bcast`, only notifies about keys with the prefixes. prefixes: option>, /// `BCAST`: notifies about every change of the keys with the prefixes, read or not. @@ -912,12 +947,25 @@ interface types { noloop: bool, } + /// Where a connection's tracking notifications are redirected, see `client-trackinginfo`. + /// + /// Redirecting is for RESP2, which the connection does not use, but CLIENT TRACKING REDIRECT may + /// still be sent with `connection.exchange`. + variant client-trackinginfo-redirect { + /// -1: tracking is not on. + not-tracking, + /// 0: tracking is on, the notifications are not redirected. + not-redirected, + /// The ID of the connection the notifications are redirected to. + redirected(integer), + } + /// A connection's tracking state, see `client-trackinginfo`. record client-trackinginfo-reply { /// The tracking flags, e.g. `on`, `bcast`, `optin` or `noloop`. %flags: list, /// Where the notifications are redirected. - redirect: client-tracking-redirect, + redirect: client-trackinginfo-redirect, /// The prefixes of BCAST. prefixes: list, } From 9b7c2ecb11d7422116938794006e490eb52e44cd Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 14 Oct 2026 00:01:39 -0400 Subject: [PATCH 26/37] advertise client name and version to server Signed-off-by: Scott Andrews --- Cargo.lock | 2 +- components/connection/Cargo.toml | 2 +- components/connection/src/lib.rs | 25 ++++++++++++++++++++++++- wit/connection.wit | 4 ++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34fec74..2de16d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -168,7 +168,7 @@ dependencies = [ [[package]] name = "connection" -version = "0.1.0" +version = "0.3.0-dev" dependencies = [ "componentized-valkey", "futures-channel", diff --git a/components/connection/Cargo.toml b/components/connection/Cargo.toml index 704394b..f7783b2 100644 --- a/components/connection/Cargo.toml +++ b/components/connection/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "connection" -version = "0.1.0" +version.workspace = true edition = "2024" license = "Apache-2.0" diff --git a/components/connection/src/lib.rs b/components/connection/src/lib.rs index a856a21..95defc9 100644 --- a/components/connection/src/lib.rs +++ b/components/connection/src/lib.rs @@ -35,6 +35,11 @@ use futures_core::Stream; use wasi::clocks::monotonic_clock::wait_for; use wit_bindgen::{FutureReader, FutureWriter, StreamReader, StreamWriter, spawn_local}; +/// The library the connection reports to the server, see CLIENT SETINFO. +const LIB_NAME: &str = "componentized:valkey"; +/// The version the connection reports to the server, that of the package. +const LIB_VER: &str = env!("CARGO_PKG_VERSION"); + /// How many values a reader may fall behind before its stream is closed, and how many events a /// subscription keeps while no stream is open. const PUSH_BUFFER: usize = 256; @@ -1535,7 +1540,25 @@ async fn establish( } inner.state.borrow_mut().hello = hello.clone(); - let result = match request(&inner, hello).await.and_then(server_properties) { + let (hello_tx, hello_rx) = oneshot::channel(); + // the library, best effort, a server before 7.2 does not know CLIENT SETINFO, its error is + // discarded. It survives RESET, so it is only sent once. + let setinfo = [("LIB-NAME", LIB_NAME), ("LIB-VER", LIB_VER)]; + let queued = enqueue(&inner, hello, Some(Reply::Call(hello_tx)), None).and_then(|()| { + for (attr, value) in setinfo { + let command = words(&["CLIENT", "SETINFO", attr, value]); + enqueue(&inner, command, Some(Reply::Discard), None)?; + } + Ok(()) + }); + let hello = match queued { + // dropped unsent when the connection closes, the reader fails it first + Ok(()) => hello_rx + .await + .unwrap_or_else(|_| Err(closed_error(&inner.state))), + Err(error) => Err(error), + }; + let result = match hello.and_then(server_properties) { Ok(properties) => { inner.state.borrow_mut().server_properties = properties; let established = ConnectionImpl(inner); diff --git a/wit/connection.wit b/wit/connection.wit index 2c4cbf7..7f04bec 100644 --- a/wit/connection.wit +++ b/wit/connection.wit @@ -24,6 +24,10 @@ interface connection { /// future resolves to the connection once HELLO succeeds, or to the error that stopped it. /// The rest of `opts` is applied before the connection is returned. /// + /// It then reports the library to the server, `CLIENT SETINFO LIB-NAME componentized:valkey` + /// and `LIB-VER` with the version of the package, ignoring an error from a server that does + /// not know CLIENT SETINFO. They survive `reset`, `client-setinfo` replaces them. + /// /// Async so the connection keeps working between commands after it returns, e.g. to read /// push messages and to send keep-alive pings. new: static async func(replies: stream, closed: future>, opts: option) -> tuple, future>>; From 2fde4367c6daf8bee8dd4cb385044cebbd76da58 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 14 Oct 2026 00:52:55 -0400 Subject: [PATCH 27/37] Restore the integration test suite Run tests against a real ephemeral valkey-severs running in docker. Signed-off-by: Scott Andrews --- Cargo.lock | 13 +- Cargo.toml | 1 + Makefile | 24 +- components/as-keyvalue/tests/keyvalue.rs | 258 +++- components/commands/tests/commands.rs | 776 +----------- components/connection/tests/connection.rs | 1180 ++++++++++++++++++- components/dep-cli/src/lib.rs | 181 +++ components/dep-test-commands/Cargo.toml | 13 + components/dep-test-commands/README.md | 5 + components/dep-test-commands/src/lib.rs | 732 ++++++++++++ components/tcp-connector/Cargo.toml | 5 + components/tcp-connector/tests/connector.rs | 115 ++ components/tcp/tests/tcp.rs | 49 +- components/test-commands/README.md | 3 + components/test-commands/test-commands.wac | 26 + components/test-tcp/README.md | 2 +- components/test-tcp/test-tcp.wac | 6 + components/wit/worlds.wit | 18 + crates/test-harness/Cargo.toml | 2 +- crates/test-harness/src/lib.rs | 235 ++-- 20 files changed, 2699 insertions(+), 945 deletions(-) create mode 100644 components/dep-test-commands/Cargo.toml create mode 100644 components/dep-test-commands/README.md create mode 100644 components/dep-test-commands/src/lib.rs create mode 100644 components/tcp-connector/tests/connector.rs create mode 100644 components/test-commands/README.md create mode 100644 components/test-commands/test-commands.wac diff --git a/Cargo.lock b/Cargo.lock index 2de16d0..f1a214c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -421,6 +421,14 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "dep-test-commands" +version = "0.3.0-dev" +dependencies = [ + "componentized-valkey", + "wit-bindgen", +] + [[package]] name = "digest" version = "0.10.7" @@ -1438,6 +1446,9 @@ name = "tcp-connector" version = "0.1.0" dependencies = [ "componentized-valkey", + "test-harness", + "tokio", + "wasmtime", "wit-bindgen", ] @@ -1465,7 +1476,7 @@ dependencies = [ [[package]] name = "test-harness" -version = "0.1.0" +version = "0.3.0-dev" dependencies = [ "componentized-valkey", "tokio", diff --git a/Cargo.toml b/Cargo.toml index f738cf4..a1011fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "components/commands", "components/connection", "components/dep-cli", + "components/dep-test-commands", "components/sample-http-incrementor", "components/tcp", "components/tcp-connector", diff --git a/Makefile b/Makefile index be1527a..6e3877f 100644 --- a/Makefile +++ b/Makefile @@ -26,19 +26,29 @@ clean: clean-components clean-wit clean-all: clean-components clean-tools clean-wit cargo clean -# runs the cli with the given arguments, e.g. `$(call run_cli,hello)` +# runs the cli with the given arguments, e.g. `$(call run_cli,ping)` run_cli = $(WASMTIME) run $(WASMTIME_RUN_FLAGS) ${COMPONENTS_DIR}/cli/cli.debug.wasm $(1) .PHONY: run ## Run the cli against a Valkey server, e.g. `make run cmd="keys '*'"` run: ${COMPONENTS_DIR}/cli/cli.debug.wasm | $(call tool,wasmtime-cli) @$(call run_cli,$(cmd)) -# the tests that need a Valkey server on localhost:6379, ignored tests in `with_valkey` modules, -# other ignored tests stay ignored -.PHONY: test-valkey -test-valkey: components - cargo test --workspace -- --ignored with_valkey:: - @$(call run_cli,hello) +# the Valkey server of `make test-valkey`, a container removed afterwards, e.g. `VALKEY_VERSION=8.1` +VALKEY_IMAGE ?= valkey/valkey +VALKEY_VERSION ?= 9.1 + +# the tests that need a Valkey server, ignored tests in `with_valkey` modules, other ignored tests +# stay ignored. The server runs in a new container, on a free port, so it starts empty. +.PHONY: test-valkey ## Run the tests against a Valkey server in a container, `VALKEY_VERSION` picks the version +test-valkey: components | $(call tool,wasmtime-cli) + @set -e; \ + container=$$(docker run --detach --rm --publish 127.0.0.1::6379 $(VALKEY_IMAGE):$(VALKEY_VERSION)); \ + trap 'docker stop $$container > /dev/null' EXIT; \ + port=$$(docker port $$container 6379/tcp | head -n 1 | sed 's/.*://'); \ + until docker exec $$container valkey-cli ping > /dev/null 2>&1; do sleep 0.1; done; \ + echo "Valkey $$(docker exec $$container valkey-server --version | sed 's/.* v=\([^ ]*\).*/\1/') on 127.0.0.1:$$port"; \ + VALKEY_HOST=127.0.0.1 VALKEY_PORT=$$port cargo test --workspace --no-fail-fast -- --ignored with_valkey::; \ + $(WASMTIME) run $(WASMTIME_RUN_FLAGS) -Sconfig-var=port=$$port ${COMPONENTS_DIR}/cli/cli.debug.wasm ping .PHONY: test test: components diff --git a/components/as-keyvalue/tests/keyvalue.rs b/components/as-keyvalue/tests/keyvalue.rs index ebb3c2a..938c791 100644 --- a/components/as-keyvalue/tests/keyvalue.rs +++ b/components/as-keyvalue/tests/keyvalue.rs @@ -3,8 +3,9 @@ use std::time::Duration; -use test_harness::Harness; use test_harness::keyvalue::types::{Error, KeyResponse, SetOptions}; +use test_harness::{Client, Harness, RespValue, Server, bulk, command, hello_reply, ok, setinfo}; +use wasmtime::component::ResourceAny; /// Each bucket is a hash, named for the test, with a prefix to keep them apart from other data. const KEY_PREFIX: &str = "as-keyvalue-test:"; @@ -26,16 +27,261 @@ async fn unreachable_servers_are_unavailable() -> wasmtime::Result<()> { .await } -/// The tests against a Valkey server on localhost:6379, ignored unless run with -/// `make test-valkey`, which only runs the ignored tests in `with_valkey` modules. +/// Answer the handshake of a connection. +async fn handshake(client: &mut Client) { + assert_eq!(client.request().await, Some(command(&["HELLO", "3"]))); + for setinfo in setinfo() { + assert_eq!(client.request().await, Some(setinfo)); + } + client.reply(hello_reply()).await; + for _ in setinfo() { + client.reply(ok()).await; + } +} + +fn array(items: Vec) -> RespValue { + RespValue::Array(items.into_iter().map(test_harness::encode).collect()) +} + +/// Answer each command the call sends with the next reply, returning the call's result and the +/// commands, as words. +async fn served( + client: &mut Client, + replies: Vec, + call: impl Future>, +) -> (T, Vec>) { + let serve = async { + let mut requests = vec![]; + for reply in replies { + requests.push(client.request().await.expect("a command")); + client.reply(reply).await; + } + requests + }; + let (result, requests) = tokio::join!(call, serve); + (result.expect("call"), requests) +} + +#[tokio::test(flavor = "multi_thread")] +async fn buckets_are_hashes() -> wasmtime::Result<()> { + let server = Server::bind().await?; + let port = server.port().to_string(); + let config = [("port", port.as_str()), ("key-prefix", "app:")]; + let mut harness = Harness::with_config("client", &config).await?; + harness + .run(async |accessor, exports| { + let s = |value: &str| value.to_string(); + let b = |value: &str| value.as_bytes().to_vec(); + let open = exports.keyvalue_store().call_open(accessor, s("bucket")); + let serve = async { + let mut client = server.accept().await.expect("accept"); + handshake(&mut client).await; + client + }; + let (bucket, mut client) = tokio::join!(open, serve); + let bucket: ResourceAny = + bucket?.map_err(|error| wasmtime::format_err!("{error:?}"))?; + let buckets = exports.keyvalue_types().bucket(); + + let (result, sent) = served( + &mut client, + vec![bulk("1")], + buckets.call_get(accessor, bucket, s("a")), + ) + .await; + assert_eq!(sent, vec![command(&["HGET", "app:bucket", "a"])]); + assert_eq!(result, Ok(Some(b("1")))); + + let (result, _) = served( + &mut client, + vec![RespValue::Null], + buckets.call_get(accessor, bucket, s("missing")), + ) + .await; + assert_eq!(result, Ok(None)); + + let (result, sent) = served( + &mut client, + vec![RespValue::Integer(1)], + buckets.call_set(accessor, bucket, s("a"), b("2"), None), + ) + .await; + assert_eq!(sent, vec![command(&["HSET", "app:bucket", "a", "2"])]); + assert_eq!(result, Ok(())); + + // if-not-exists only writes a new field + let if_not_exists = Some(SetOptions { + ttl_ms: None, + if_not_exists: true, + }); + let (result, sent) = served( + &mut client, + vec![RespValue::Integer(0)], + buckets.call_set(accessor, bucket, s("a"), b("3"), if_not_exists), + ) + .await; + assert_eq!(sent, vec![command(&["HSETNX", "app:bucket", "a", "3"])]); + assert_eq!(result, Err(Error::PreconditionFailed)); + + // a ttl to the millisecond, on the field + let ttl = Some(SetOptions { + ttl_ms: Some(1500), + if_not_exists: false, + }); + let (result, sent) = served( + &mut client, + vec![RespValue::Integer(1), array(vec![RespValue::Integer(1)])], + buckets.call_set(accessor, bucket, s("t"), b("1"), ttl), + ) + .await; + assert_eq!( + sent, + vec![ + command(&["HSET", "app:bucket", "t", "1"]), + command(&["HPEXPIRE", "app:bucket", "1500", "FIELDS", "1", "t"]), + ] + ); + assert_eq!(result, Ok(())); + + let (result, sent) = served( + &mut client, + vec![RespValue::Integer(1)], + buckets.call_exists(accessor, bucket, s("a")), + ) + .await; + assert_eq!(sent, vec![command(&["HEXISTS", "app:bucket", "a"])]); + assert_eq!(result, Ok(true)); + + let (result, sent) = served( + &mut client, + vec![RespValue::Integer(1)], + buckets.call_delete(accessor, bucket, s("a")), + ) + .await; + assert_eq!(sent, vec![command(&["HDEL", "app:bucket", "a"])]); + assert_eq!(result, Ok(())); + + // keys by prefix, glob characters in it match literally, resumed with the cursor + let (result, sent) = served( + &mut client, + vec![array(vec![bulk("17"), array(vec![bulk("a*1")])])], + buckets.call_list_keys(accessor, bucket, Some(s("a*")), None), + ) + .await; + assert_eq!( + sent, + vec![command(&[ + "HSCAN", + "app:bucket", + "0", + "MATCH", + "a\\**", + "NOVALUES" + ])] + ); + assert_eq!( + result, + Ok(KeyResponse { + keys: vec![s("a*1")], + cursor: Some(s("17")), + }) + ); + let (result, sent) = served( + &mut client, + vec![array(vec![bulk("0"), array(vec![])])], + buckets.call_list_keys(accessor, bucket, None, Some(s("17"))), + ) + .await; + assert_eq!( + sent, + vec![command(&["HSCAN", "app:bucket", "17", "NOVALUES"])] + ); + assert_eq!( + result, + Ok(KeyResponse { + keys: vec![], + cursor: None, + }) + ); + + let (result, sent) = served( + &mut client, + vec![RespValue::Integer(5)], + exports + .keyvalue_atomics() + .call_increment(accessor, bucket, s("count"), 5), + ) + .await; + assert_eq!( + sent, + vec![command(&["HINCRBY", "app:bucket", "count", "5"])] + ); + assert_eq!(result, Ok(5)); + + let batch = exports.keyvalue_batch(); + let (result, sent) = served( + &mut client, + vec![bulk("1"), RespValue::Null], + batch.call_get_many(accessor, bucket, vec![s("x"), s("z")]), + ) + .await; + assert_eq!( + sent, + vec![ + command(&["HGET", "app:bucket", "x"]), + command(&["HGET", "app:bucket", "z"]), + ] + ); + assert_eq!(result, Ok(vec![Some((s("x"), b("1"))), None])); + + let (result, sent) = served( + &mut client, + vec![RespValue::Integer(1), RespValue::Integer(1)], + batch.call_set_many(accessor, bucket, vec![(s("x"), b("1")), (s("y"), b("2"))]), + ) + .await; + assert_eq!( + sent, + vec![ + command(&["HSET", "app:bucket", "x", "1"]), + command(&["HSET", "app:bucket", "y", "2"]), + ] + ); + assert_eq!(result, Ok(())); + + let (result, sent) = served( + &mut client, + vec![RespValue::Integer(1), RespValue::Integer(1)], + batch.call_delete_many(accessor, bucket, vec![s("x"), s("y")]), + ) + .await; + assert_eq!( + sent, + vec![ + command(&["HDEL", "app:bucket", "x"]), + command(&["HDEL", "app:bucket", "y"]), + ] + ); + assert_eq!(result, Ok(())); + + // a closed connection is unavailable, a new bucket may connect again + client.close().await; + let result = buckets.call_get(accessor, bucket, s("a")).await?; + assert_eq!(result, Err(Error::StoreUnavailable)); + Ok(()) + }) + .await +} + +/// The tests against a Valkey server, ignored unless run with `make test-valkey`, which runs one in +/// a container and only runs the ignored tests in `with_valkey` modules. mod with_valkey { use super::*; - /// Against a Valkey server on localhost:6379, run with `cargo test -p as-keyvalue -- --ignored with_valkey::`. #[tokio::test(flavor = "multi_thread")] - #[ignore = "needs a Valkey server on localhost:6379"] + #[ignore = "needs a Valkey server, see make test-valkey"] async fn buckets_against_valkey() -> wasmtime::Result<()> { - let mut harness = Harness::with_config("client", &[("key-prefix", KEY_PREFIX)]).await?; + let mut harness = Harness::with_valkey("client", &[("key-prefix", KEY_PREFIX)]).await?; harness .run(async |accessor, exports| { let s = |value: &str| value.to_string(); diff --git a/components/commands/tests/commands.rs b/components/commands/tests/commands.rs index 018f2aa..9ca2402 100644 --- a/components/commands/tests/commands.rs +++ b/components/commands/tests/commands.rs @@ -1,771 +1,29 @@ -//! Tests for the commands, run against `test-commands`, `commands` composed with `connection`, -//! `resp` and `tcp`. The test plays the server over a stub transport, checking the command sent -//! and the conversion of its reply. +//! The tests of the commands, run inside `dep-test-commands`, composed with `commands` and +//! `connection` as `test-commands`. Each test plays the server over the streams of a connection, +//! checking the command a function sends and how it converts the reply. +//! +//! Wasmtime's bindings for the host cannot call `commands` itself, which has functions named for +//! Rust keywords, `move` and `type`, so the tests run in a component. -use std::{collections::HashMap, future::Future}; - -use test_harness::{ - Harness, Transport, Value, bulk, command, connection_commands, establish, hash_commands, - keyspace_commands, map, -}; - -/// Answer the next command with `reply` while `call` waits for it, returning the call's result -/// and the command sent. -async fn round_trip( - transport: &mut Transport, - reply: Value, - call: impl Future>, -) -> (T, Vec) { - let serve = async { - let request = transport.request().await.expect("a command"); - transport.reply(reply); - request - }; - let (result, request) = tokio::join!(call, serve); - (result.expect("call"), request) -} - -fn ok() -> Value { - Value::String("OK".to_string()) -} - -fn array(items: Vec) -> Value { - Value::Array(items.into_iter().map(test_harness::encode).collect()) -} - -#[tokio::test(flavor = "multi_thread")] -async fn string_commands() -> wasmtime::Result<()> { - let mut harness = Harness::new("test-commands").await?; - harness - .run(async |accessor, exports| { - let (mut transport, conn) = establish(accessor, exports).await?; - let strings = exports.string_commands(); - - let (result, sent) = round_trip( - &mut transport, - bulk("value"), - strings.call_get(accessor, conn, "key".to_string()), - ) - .await; - assert_eq!(sent, command(&["GET", "key"])); - assert_eq!(result, Ok(Some("value".to_string()))); - - let (result, _) = round_trip( - &mut transport, - Value::Null, - strings.call_get(accessor, conn, "missing".to_string()), - ) - .await; - assert_eq!(result, Ok(None)); - - let (result, sent) = round_trip( - &mut transport, - ok(), - strings.call_set( - accessor, - conn, - "key".to_string(), - "value".to_string(), - None, - None, - ), - ) - .await; - assert_eq!(sent, command(&["SET", "key", "value"])); - assert_eq!(result, Ok(())); - - let (result, sent) = round_trip( - &mut transport, - Value::Integer(1), - strings.call_incr(accessor, conn, "count".to_string()), - ) - .await; - assert_eq!(sent, command(&["INCR", "count"])); - assert_eq!(result, Ok(1)); - - let (result, sent) = round_trip( - &mut transport, - Value::Integer(-4), - strings.call_incrby(accessor, conn, "count".to_string(), -5), - ) - .await; - assert_eq!(sent, command(&["INCRBY", "count", "-5"])); - assert_eq!(result, Ok(-4)); - Ok(()) - }) - .await -} - -#[tokio::test(flavor = "multi_thread")] -async fn keyspace_commands() -> wasmtime::Result<()> { - use keyspace_commands::{ExpireMode, ExpireResult}; - let mut harness = Harness::new("test-commands").await?; - harness - .run(async |accessor, exports| { - let (mut transport, conn) = establish(accessor, exports).await?; - let keyspace = exports.keyspace_commands(); - - let (result, sent) = round_trip( - &mut transport, - Value::Integer(1), - keyspace.call_del(accessor, conn, "key".to_string()), - ) - .await; - assert_eq!(sent, command(&["DEL", "key"])); - assert_eq!(result, Ok(1)); - - let (result, sent) = round_trip( - &mut transport, - Value::Integer(0), - keyspace.call_exists(accessor, conn, "key".to_string()), - ) - .await; - assert_eq!(sent, command(&["EXISTS", "key"])); - assert_eq!(result, Ok(false)); - - // durations are nanoseconds, EXPIRE takes whole seconds - let (result, sent) = round_trip( - &mut transport, - Value::Integer(1), - keyspace.call_expire( - accessor, - conn, - "key".to_string(), - 90_500_000_000, - Some(ExpireMode::Nx), - ), - ) - .await; - assert_eq!(sent, command(&["EXPIRE", "key", "91", "NX"])); - assert_eq!(result, Ok(ExpireResult::Applied)); - - let (result, sent) = round_trip( - &mut transport, - Value::Integer(0), - keyspace.call_expireat( - accessor, - conn, - "key".to_string(), - keyspace_commands::Instant { - seconds: 1_900_000_000, - nanoseconds: 0, - }, - None, - ), - ) - .await; - assert_eq!(sent, command(&["EXPIREAT", "key", "1900000000"])); - assert_eq!(result, Ok(ExpireResult::ConditionNotMet)); - - let (result, sent) = round_trip( - &mut transport, - array(vec![bulk("a"), bulk("b")]), - keyspace.call_keys(accessor, conn, "*".to_string()), - ) - .await; - assert_eq!(sent, command(&["KEYS", "*"])); - assert_eq!(result, Ok(vec!["a".to_string(), "b".to_string()])); - Ok(()) - }) - .await -} - -#[tokio::test(flavor = "multi_thread")] -async fn hash_commands() -> wasmtime::Result<()> { - use hash_commands::{ExpireResult, HrandfieldOpts, HscanOpts}; - let mut harness = Harness::new("test-commands").await?; - harness - .run(async |accessor, exports| { - let (mut transport, conn) = establish(accessor, exports).await?; - let hashes = exports.hash_commands(); - let key = || "hash".to_string(); - - let (result, sent) = round_trip( - &mut transport, - Value::Integer(1), - hashes.call_hset(accessor, conn, key(), "f".to_string(), "v".to_string()), - ) - .await; - assert_eq!(sent, command(&["HSET", "hash", "f", "v"])); - assert_eq!(result, Ok(1)); - - let (result, sent) = round_trip( - &mut transport, - bulk("v"), - hashes.call_hget(accessor, conn, key(), "f".to_string()), - ) - .await; - assert_eq!(sent, command(&["HGET", "hash", "f"])); - assert_eq!(result, Ok(Some("v".to_string()))); - - // RESP3 replies to HGETALL with a map - let (result, sent) = round_trip( - &mut transport, - map(vec![(bulk("f"), bulk("v")), (bulk("g"), bulk("w"))]), - hashes.call_hgetall(accessor, conn, key()), - ) - .await; - assert_eq!(sent, command(&["HGETALL", "hash"])); - assert_eq!( - result, - Ok(HashMap::from([ - ("f".to_string(), "v".to_string()), - ("g".to_string(), "w".to_string()) - ])) - ); - - let (result, _) = round_trip( - &mut transport, - Value::Integer(1), - hashes.call_hsetnx(accessor, conn, key(), "f".to_string(), "v".to_string()), - ) - .await; - assert_eq!(result, Ok(true)); - - let (result, _) = round_trip( - &mut transport, - Value::Integer(1), - hashes.call_hexists(accessor, conn, key(), "f".to_string()), - ) - .await; - assert_eq!(result, Ok(true)); - - let (result, sent) = round_trip( - &mut transport, - Value::Integer(2), - hashes.call_hlen(accessor, conn, key()), - ) - .await; - assert_eq!(sent, command(&["HLEN", "hash"])); - assert_eq!(result, Ok(2)); - - let (result, sent) = round_trip( - &mut transport, - array(vec![bulk("v"), Value::Null]), - hashes.call_hmget( - accessor, - conn, - key(), - vec!["f".to_string(), "x".to_string()], - ), - ) - .await; - assert_eq!(sent, command(&["HMGET", "hash", "f", "x"])); - assert_eq!(result, Ok(vec![Some("v".to_string()), None])); - - let (result, sent) = round_trip( - &mut transport, - ok(), - hashes.call_hmset( - accessor, - conn, - key(), - vec![ - ("f".to_string(), "v".to_string()), - ("g".to_string(), "w".to_string()), - ], - ), - ) - .await; - assert_eq!(sent, command(&["HMSET", "hash", "f", "v", "g", "w"])); - assert_eq!(result, Ok(())); - - let (result, sent) = round_trip( - &mut transport, - Value::Integer(6), - hashes.call_hincrby(accessor, conn, key(), "n".to_string(), 5), - ) - .await; - assert_eq!(sent, command(&["HINCRBY", "hash", "n", "5"])); - assert_eq!(result, Ok(6)); - - let (result, sent) = round_trip( - &mut transport, - bulk("2.5"), - hashes.call_hincrbyfloat(accessor, conn, key(), "n".to_string(), 1.5), - ) - .await; - assert_eq!(sent, command(&["HINCRBYFLOAT", "hash", "n", "1.5"])); - assert_eq!(result, Ok("2.5".to_string())); - - let (result, _) = round_trip( - &mut transport, - array(vec![bulk("f"), bulk("g")]), - hashes.call_hkeys(accessor, conn, key()), - ) - .await; - assert_eq!(result, Ok(vec!["f".to_string(), "g".to_string()])); - - let (result, _) = round_trip( - &mut transport, - array(vec![bulk("v"), bulk("w")]), - hashes.call_hvals(accessor, conn, key()), - ) - .await; - assert_eq!(result, Ok(vec!["v".to_string(), "w".to_string()])); - - let (result, sent) = round_trip( - &mut transport, - Value::Integer(1), - hashes.call_hstrlen(accessor, conn, key(), "f".to_string()), - ) - .await; - assert_eq!(sent, command(&["HSTRLEN", "hash", "f"])); - assert_eq!(result, Ok(1)); - - let (result, sent) = round_trip( - &mut transport, - array(vec![Value::Integer(1), Value::Integer(-2)]), - hashes.call_hexpire( - accessor, - conn, - key(), - 60_000_000_000, - None, - vec!["f".to_string(), "x".to_string()], - ), - ) - .await; - assert_eq!( - sent, - command(&["HEXPIRE", "hash", "60", "FIELDS", "2", "f", "x"]) - ); - assert_eq!( - result, - Ok(vec![ExpireResult::Applied, ExpireResult::DoesNotExist]) - ); - - // without a count, one field or null - let (result, sent) = round_trip( - &mut transport, - bulk("f"), - hashes.call_hrandfield(accessor, conn, key(), None), - ) - .await; - assert_eq!(sent, command(&["HRANDFIELD", "hash"])); - assert_eq!(result, Ok(Some(vec![("f".to_string(), None)]))); - - // RESP3 replies with values as [field, value] pairs - let (result, sent) = round_trip( - &mut transport, - array(vec![ - array(vec![bulk("f"), bulk("v")]), - array(vec![bulk("g"), bulk("w")]), - ]), - hashes.call_hrandfield( - accessor, - conn, - key(), - Some(HrandfieldOpts { - count: Some(2), - with_values: Some(true), - }), - ), - ) - .await; - assert_eq!(sent, command(&["HRANDFIELD", "hash", "2", "WITHVALUES"])); - assert_eq!( - result, - Ok(Some(vec![ - ("f".to_string(), Some("v".to_string())), - ("g".to_string(), Some("w".to_string())) - ])) - ); - - let (result, sent) = round_trip( - &mut transport, - array(vec![bulk("17"), array(vec![bulk("f"), bulk("v")])]), - hashes.call_hscan( - accessor, - conn, - key(), - None, - Some(HscanOpts { - match_: Some("f*".to_string()), - count: Some(10), - no_values: None, - }), - ), - ) - .await; - assert_eq!( - sent, - command(&["HSCAN", "hash", "0", "MATCH", "f*", "COUNT", "10"]) - ); - assert_eq!( - result, - Ok(( - Some("17".to_string()), - vec![("f".to_string(), Some("v".to_string()))] - )) - ); - - // the last page has a cursor of 0 - let (result, sent) = round_trip( - &mut transport, - array(vec![bulk("0"), array(vec![bulk("g")])]), - hashes.call_hscan( - accessor, - conn, - key(), - Some("17".to_string()), - Some(HscanOpts { - match_: None, - count: None, - no_values: Some(true), - }), - ), - ) - .await; - assert_eq!(sent, command(&["HSCAN", "hash", "17", "NOVALUES"])); - assert_eq!(result, Ok((None, vec![("g".to_string(), None)]))); - - let (result, _) = round_trip( - &mut transport, - Value::Integer(1), - hashes.call_hdel(accessor, conn, key(), "f".to_string()), - ) - .await; - assert_eq!(result, Ok(1)); - Ok(()) - }) - .await -} +use test_harness::{Harness, run_suite}; +/// The tests that do not need a Valkey server. #[tokio::test(flavor = "multi_thread")] -async fn hrandfield_with_values_needs_a_count() -> wasmtime::Result<()> { - let mut harness = Harness::new("test-commands").await?; - harness - .run(async |accessor, exports| { - let (_transport, conn) = establish(accessor, exports).await?; - let result = exports - .hash_commands() - .call_hrandfield( - accessor, - conn, - "hash".to_string(), - Some(hash_commands::HrandfieldOpts { - count: None, - with_values: Some(true), - }), - ) - .await?; - assert!(matches!(result, Err(test_harness::Error::Client(_)))); - Ok(()) - }) - .await +async fn commands() -> wasmtime::Result<()> { + let harness = Harness::new("test-commands").await?; + run_suite(harness, |test| !test.starts_with("with-valkey ")).await } -#[tokio::test(flavor = "multi_thread")] -async fn connection_commands() -> wasmtime::Result<()> { - let mut harness = Harness::new("test-commands").await?; - harness - .run(async |accessor, exports| { - let (mut transport, conn) = establish(accessor, exports).await?; - let connections = exports.connection_commands(); - - let (result, sent) = round_trip( - &mut transport, - Value::String("PONG".to_string()), - connections.call_ping(accessor, conn), - ) - .await; - assert_eq!(sent, command(&["PING"])); - assert_eq!(result, Ok(())); - - let (result, sent) = round_trip( - &mut transport, - ok(), - connections.call_auth(accessor, conn, "user".to_string(), "secret".to_string()), - ) - .await; - assert_eq!(sent, command(&["AUTH", "user", "secret"])); - assert_eq!(result, Ok(())); - - let (result, sent) = round_trip( - &mut transport, - map(vec![(bulk("server"), bulk("valkey"))]), - connections.call_hello( - accessor, - conn, - Some(connection_commands::HelloOpts { - auth: None, - client_name: Some("tests".to_string()), - }), - ), - ) - .await; - assert_eq!(sent, command(&["HELLO", "3", "SETNAME", "tests"])); - assert_eq!(result, Ok(vec![("server".to_string(), bulk("valkey"))])); - Ok(()) - }) - .await -} - -#[tokio::test(flavor = "multi_thread")] -async fn admin_and_pubsub_commands() -> wasmtime::Result<()> { - let mut harness = Harness::new("test-commands").await?; - harness - .run(async |accessor, exports| { - let (mut transport, conn) = establish(accessor, exports).await?; - let admin = exports.admin_commands(); - - let (result, sent) = round_trip( - &mut transport, - ok(), - admin.call_acl_setuser( - accessor, - conn, - "user".to_string(), - vec!["on".to_string(), ">secret".to_string()], - ), - ) - .await; - assert_eq!(sent, command(&["ACL", "SETUSER", "user", "on", ">secret"])); - assert_eq!(result, Ok(())); - - let (result, sent) = round_trip( - &mut transport, - bulk("dd721260bfe1b3d9601e7fbab36de6d04e2e67b0ef1c53de59d45950db0dd3cc"), - admin.call_acl_genpass(accessor, conn), - ) - .await; - assert_eq!(sent, command(&["ACL", "GENPASS"])); - assert_eq!(result.map(|pass| pass.len()), Ok(64)); - - let (result, sent) = round_trip( - &mut transport, - Value::Integer(1), - admin.call_acl_deluser(accessor, conn, "user".to_string()), - ) - .await; - assert_eq!(sent, command(&["ACL", "DELUSER", "user"])); - assert_eq!(result, Ok(1)); - - let (result, sent) = round_trip( - &mut transport, - Value::Integer(2), - exports.pubsub_commands().call_publish( - accessor, - conn, - "news".to_string(), - "hello".to_string(), - ), - ) - .await; - assert_eq!(sent, command(&["PUBLISH", "news", "hello"])); - assert_eq!(result, Ok(2)); - Ok(()) - }) - .await -} - -#[tokio::test(flavor = "multi_thread")] -async fn unexpected_replies_fail_the_command() -> wasmtime::Result<()> { - let mut harness = Harness::new("test-commands").await?; - harness - .run(async |accessor, exports| { - let (mut transport, conn) = establish(accessor, exports).await?; - let (result, _) = round_trip( - &mut transport, - Value::Integer(7), - exports - .string_commands() - .call_get(accessor, conn, "key".to_string()), - ) - .await; - assert!(matches!(result, Err(test_harness::Error::Client(_)))); - Ok(()) - }) - .await -} - -/// The tests against a Valkey server on localhost:6379, ignored unless run with -/// `make test-valkey`, which only runs the ignored tests in `with_valkey` modules. +/// The tests against a Valkey server, ignored unless run with `make test-valkey`, which runs one in +/// a container and only runs the ignored tests in `with_valkey` modules. mod with_valkey { - use std::collections::HashMap; - use super::*; - /// Every command against a Valkey server on localhost:6379, run with - /// `cargo test -p commands -- --ignored with_valkey::`. + /// Against a Valkey server, see [`test_harness::valkey_server`]. #[tokio::test(flavor = "multi_thread")] - #[ignore = "needs a Valkey server on localhost:6379"] + #[ignore = "needs a Valkey server, see make test-valkey"] async fn against_valkey() -> wasmtime::Result<()> { - use hash_commands::{HrandfieldOpts, HscanOpts}; - let mut harness = Harness::new("test-commands").await?; - harness - .run(async |accessor, exports| { - let conn = exports - .tcp() - .call_connect(accessor, "localhost".to_string(), 6379, None) - .await? - .map_err(|error| wasmtime::format_err!("{error:?}"))?; - let strings = exports.string_commands(); - let keyspace = exports.keyspace_commands(); - let hashes = exports.hash_commands(); - let s = |value: &str| value.to_string(); - - exports - .connection_commands() - .call_ping(accessor, conn) - .await??; - let hello = exports - .connection_commands() - .call_hello(accessor, conn, None) - .await??; - assert!(hello.contains(&(s("proto"), Value::Integer(3)))); - - strings - .call_set( - accessor, - conn, - s("commands-test:string"), - s("1"), - None, - None, - ) - .await??; - assert_eq!( - strings - .call_get(accessor, conn, s("commands-test:string")) - .await??, - Some(s("1")) - ); - assert_eq!( - strings - .call_incr(accessor, conn, s("commands-test:string")) - .await??, - 2 - ); - assert_eq!( - strings - .call_incrby(accessor, conn, s("commands-test:string"), 3) - .await??, - 5 - ); - assert!( - keyspace - .call_exists(accessor, conn, s("commands-test:string")) - .await?? - ); - assert_eq!( - keyspace - .call_expire( - accessor, - conn, - s("commands-test:string"), - 60_000_000_000, - None - ) - .await??, - keyspace_commands::ExpireResult::Applied - ); - assert_eq!( - keyspace - .call_keys(accessor, conn, s("commands-test:str*")) - .await??, - vec![s("commands-test:string")] - ); - keyspace - .call_del(accessor, conn, s("commands-test:string")) - .await??; - assert_eq!( - strings - .call_get(accessor, conn, s("commands-test:string")) - .await??, - None - ); - - let key = || s("commands-test:hash"); - hashes - .call_hmset( - accessor, - conn, - key(), - vec![(s("f"), s("v")), (s("g"), s("w"))], - ) - .await??; - let all = hashes.call_hgetall(accessor, conn, key()).await??; - assert_eq!(all, HashMap::from([(s("f"), s("v")), (s("g"), s("w"))])); - assert_eq!(hashes.call_hlen(accessor, conn, key()).await??, 2); - assert_eq!( - hashes - .call_hmget(accessor, conn, key(), vec![s("f"), s("x")]) - .await??, - vec![Some(s("v")), None] - ); - let fields = hashes - .call_hrandfield( - accessor, - conn, - key(), - Some(HrandfieldOpts { - count: Some(2), - with_values: Some(true), - }), - ) - .await?? - .expect("fields"); - assert_eq!(fields.len(), 2); - assert!(fields.iter().all(|(_, value)| value.is_some())); - let (cursor, mut scanned) = hashes - .call_hscan( - accessor, - conn, - key(), - None, - Some(HscanOpts { - match_: None, - count: None, - no_values: None, - }), - ) - .await??; - scanned.sort(); - assert_eq!(cursor, None); - assert_eq!( - scanned, - vec![(s("f"), Some(s("v"))), (s("g"), Some(s("w")))] - ); - assert_eq!( - hashes - .call_hincrbyfloat(accessor, conn, key(), s("n"), 1.5) - .await??, - s("1.5") - ); - assert_eq!( - hashes - .call_hexpire( - accessor, - conn, - key(), - 60_000_000_000, - None, - vec![s("f"), s("x")] - ) - .await??, - vec![ - hash_commands::ExpireResult::Applied, - hash_commands::ExpireResult::DoesNotExist - ] - ); - keyspace.call_del(accessor, conn, key()).await??; - - assert_eq!( - exports - .pubsub_commands() - .call_publish(accessor, conn, s("commands-test"), s("hello")) - .await??, - 0 - ); - let pass = exports - .admin_commands() - .call_acl_genpass(accessor, conn) - .await??; - assert_eq!(pass.len(), 64); - Ok(()) - }) - .await + let harness = Harness::with_valkey("test-commands", &[]).await?; + run_suite(harness, |test| test.starts_with("with-valkey ")).await } } diff --git a/components/connection/tests/connection.rs b/components/connection/tests/connection.rs index a9394ee..670d2c6 100644 --- a/components/connection/tests/connection.rs +++ b/components/connection/tests/connection.rs @@ -2,14 +2,63 @@ use std::time::Duration; +use test_harness::types::{ + DurationMilliseconds, SubscriptionEvent, SubscriptionMessage, SubscriptionTarget, + UnsubscribeReason, +}; use test_harness::{ - ConnectOpts, Error, Harness, Value, bulk, command, connect, establish, hello_reply, push, - receive, resolve, words, + ConnectOpts, Error, Harness, RespValue, Transport, bulk, command, connect, establish, + handshake, hello_reply, ok, push, receive, resolve, words, }; /// Long enough for the connection to act on what it was given, short enough to keep tests quick. const SETTLE: Duration = Duration::from_millis(100); +fn simple(text: &str) -> RespValue { + RespValue::String(text.to_string()) +} + +fn channel(name: &str) -> SubscriptionTarget { + SubscriptionTarget::Channel(name.to_string()) +} + +fn pattern(name: &str) -> SubscriptionTarget { + SubscriptionTarget::Pattern(name.to_string()) +} + +fn shardchannel(name: &str) -> SubscriptionTarget { + SubscriptionTarget::Shardchannel(name.to_string()) +} + +/// A push message confirming a target, e.g. `confirm("subscribe", "news")`. +fn confirm(kind: &str, name: &str) -> RespValue { + push(vec![bulk(kind), bulk(name), RespValue::Integer(1)]) +} + +fn message(payload: &str, channel: &str, target: SubscriptionTarget) -> Option { + Some(SubscriptionEvent::Message(SubscriptionMessage { + payload: payload.to_string(), + channel: channel.to_string(), + target, + })) +} + +fn unsubscribed( + target: SubscriptionTarget, + reason: UnsubscribeReason, +) -> Option { + Some(SubscriptionEvent::Unsubscribed((target, reason))) +} + +/// The commands the connection wrote, until the server reads nothing more for a while. +async fn requests(transport: &mut Transport) -> Vec> { + let mut requests = vec![]; + while let Ok(Some(request)) = tokio::time::timeout(SETTLE, transport.request()).await { + requests.push(request); + } + requests +} + #[tokio::test(flavor = "multi_thread")] async fn establishes_with_hello() -> wasmtime::Result<()> { let mut harness = Harness::new("connection").await?; @@ -23,7 +72,7 @@ async fn establishes_with_hello() -> wasmtime::Result<()> { vec![ ("server".to_string(), bulk("valkey")), ("version".to_string(), bulk("9.1.2")), - ("proto".to_string(), Value::Integer(3)), + ("proto".to_string(), RespValue::Integer(3)), ] ); Ok(()) @@ -42,13 +91,8 @@ async fn hello_carries_auth_and_client_name() -> wasmtime::Result<()> { keep_alive_interval: None, }; let (mut transport, connection) = connect(accessor, exports, Some(opts)).await?; - assert_eq!( - transport.request().await, - Some(command(&[ - "HELLO", "3", "AUTH", "user", "secret", "SETNAME", "tests" - ])) - ); - transport.reply(hello_reply()); + let hello = ["HELLO", "3", "AUTH", "user", "secret", "SETNAME", "tests"]; + handshake(&mut transport, &hello).await; assert!(resolve(accessor, connection).await?.is_ok()); Ok(()) }) @@ -62,7 +106,7 @@ async fn hello_error_fails_to_establish() -> wasmtime::Result<()> { .run(async |accessor, exports| { let (mut transport, connection) = connect(accessor, exports, None).await?; transport.request().await; - transport.reply(Value::Error("WRONGPASS invalid password".to_string())); + transport.reply(RespValue::Error("WRONGPASS invalid password".to_string())); let Err(error) = resolve(accessor, connection).await? else { panic!("established despite the error reply"); }; @@ -70,7 +114,16 @@ async fn hello_error_fails_to_establish() -> wasmtime::Result<()> { error, Error::Valkey("WRONGPASS invalid password".to_string()) ); - // the connection is dropped, which closes the requests stream + // the CLIENT SETINFO commands queued with HELLO, then the connection is dropped, which + // closes the requests stream + assert_eq!( + transport.request().await, + Some(test_harness::setinfo()[0].clone()) + ); + assert_eq!( + transport.request().await, + Some(test_harness::setinfo()[1].clone()) + ); assert_eq!(transport.request().await, None); Ok(()) }) @@ -128,7 +181,7 @@ async fn error_replies_fail_the_exchange() -> wasmtime::Result<()> { let call = guest.call_exchange(accessor, connection, words(&["INCR", "text"])); let server = async { transport.request().await; - transport.reply(Value::Error("ERR value is not an integer".to_string())); + transport.reply(RespValue::Error("ERR value is not an integer".to_string())); }; let (result, ()) = tokio::join!(call, server); assert_eq!( @@ -155,7 +208,7 @@ async fn emit_discards_its_reply() -> wasmtime::Result<()> { assert_eq!(transport.request().await, Some(command(&["SET", "a", "1"]))); assert_eq!(transport.request().await, Some(command(&["GET", "a"]))); // the reply to SET is discarded, even an error - transport.reply(Value::Error("ERR discarded".to_string())); + transport.reply(RespValue::Error("ERR discarded".to_string())); transport.reply(bulk("1")); }; let (result, ()) = tokio::join!(call, server); @@ -166,27 +219,28 @@ async fn emit_discards_its_reply() -> wasmtime::Result<()> { } #[tokio::test(flavor = "multi_thread")] -async fn push_messages_go_to_every_reader() -> wasmtime::Result<()> { +async fn dead_letters_go_to_every_reader() -> wasmtime::Result<()> { let mut harness = Harness::new("connection").await?; harness .run(async |accessor, exports| { let guest = exports.connection(); let (mut transport, connection) = establish(accessor, exports).await?; - let first = guest.call_pushes(accessor, connection).await?; - let second = guest.call_pushes(accessor, connection).await?; + let first = guest.call_dead_letters(accessor, connection).await?; + let second = guest.call_dead_letters(accessor, connection).await?; let mut first = receive(accessor, first)?; let mut second = receive(accessor, second)?; + // a message no subscription owns let message = push(vec![bulk("message"), bulk("news"), bulk("hello")]); let call = guest.call_exchange(accessor, connection, words(&["PING"])); let server = async { transport.request().await; // a push message arriving ahead of a reply is not mistaken for it transport.reply(message.clone()); - transport.reply(Value::String("PONG".to_string())); + transport.reply(simple("PONG")); }; let (result, ()) = tokio::join!(call, server); - assert_eq!(result?, Ok(Value::String("PONG".to_string()))); + assert_eq!(result?, Ok(simple("PONG"))); assert_eq!(first.recv().await, Some(message.clone())); assert_eq!(second.recv().await, Some(message)); Ok(()) @@ -195,41 +249,41 @@ async fn push_messages_go_to_every_reader() -> wasmtime::Result<()> { } #[tokio::test(flavor = "multi_thread")] -async fn unprompted_values_go_to_push_readers() -> wasmtime::Result<()> { +async fn unprompted_values_are_dead_letters() -> wasmtime::Result<()> { let mut harness = Harness::new("connection").await?; harness .run(async |accessor, exports| { let guest = exports.connection(); let (mut transport, connection) = establish(accessor, exports).await?; - let pushes = guest.call_pushes(accessor, connection).await?; - let mut pushes = receive(accessor, pushes)?; + let dead_letters = guest.call_dead_letters(accessor, connection).await?; + let mut dead_letters = receive(accessor, dead_letters)?; let call = guest.call_exchange(accessor, connection, words(&["MONITOR"])); let server = async { transport.request().await; - transport.reply(Value::String("OK".to_string())); + transport.reply(ok()); }; let (result, ()) = tokio::join!(call, server); - assert_eq!(result?, Ok(Value::String("OK".to_string()))); + assert_eq!(result?, Ok(ok())); // the feed of MONITOR, simple strings with no command waiting for them - let feed = Value::String(r#"1760190000.123456 [0 127.0.0.1:52144] "ping""#.to_string()); + let feed = simple(r#"1760190000.123456 [0 127.0.0.1:52144] "ping""#); transport.reply(feed.clone()); - assert_eq!(pushes.recv().await, Some(feed)); + assert_eq!(dead_letters.recv().await, Some(feed)); Ok(()) }) .await } #[tokio::test(flavor = "multi_thread")] -async fn slow_push_readers_are_closed() -> wasmtime::Result<()> { +async fn slow_dead_letter_readers_are_closed() -> wasmtime::Result<()> { let mut harness = Harness::new("connection").await?; harness .run(async |accessor, exports| { let guest = exports.connection(); let (mut transport, connection) = establish(accessor, exports).await?; // a reader that never reads - let _slow = guest.call_pushes(accessor, connection).await?; + let _slow = guest.call_dead_letters(accessor, connection).await?; for n in 0..1000 { transport.reply(push(vec![ bulk("message"), @@ -241,10 +295,10 @@ async fn slow_push_readers_are_closed() -> wasmtime::Result<()> { let call = guest.call_exchange(accessor, connection, words(&["PING"])); let server = async { transport.request().await; - transport.reply(Value::String("PONG".to_string())); + transport.reply(simple("PONG")); }; let (result, ()) = tokio::join!(call, server); - assert_eq!(result?, Ok(Value::String("PONG".to_string()))); + assert_eq!(result?, Ok(simple("PONG"))); Ok(()) }) .await @@ -281,14 +335,14 @@ async fn keep_alive_pings_an_idle_connection() -> wasmtime::Result<()> { .run(async |accessor, exports| { let guest = exports.connection(); let (mut transport, connection) = establish(accessor, exports).await?; - let interval = Duration::from_millis(20).as_nanos() as u64; + let interval = DurationMilliseconds { px: 20 }; guest .call_set_keep_alive(accessor, connection, Some(interval)) .await?; assert_eq!(transport.request().await, Some(command(&["PING"]))); - transport.reply(Value::String("PONG".to_string())); + transport.reply(simple("PONG")); assert_eq!(transport.request().await, Some(command(&["PING"]))); - transport.reply(Value::String("PONG".to_string())); + transport.reply(simple("PONG")); // disabled again, no more pings guest @@ -316,3 +370,1063 @@ async fn dropping_the_connection_closes_the_requests() -> wasmtime::Result<()> { }) .await } + +#[tokio::test(flavor = "multi_thread")] +async fn commands_the_connection_manages_are_rejected() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + let rejected = |message: &str| Err(Error::Client(message.to_string())); + let reset = "RESET is rejected, use the connection's reset"; + let hello = "HELLO is rejected, the connection sent it when it was established, see its server-properties"; + let reply = "CLIENT REPLY is rejected, every command has a reply, see the connection's emit"; + for (command, message) in [ + (words(&["reset"]), reset), + (words(&["HELLO", "2"]), hello), + (words(&["client", "reply", "off"]), reply), + ] { + let result = guest.call_exchange(accessor, connection, command).await?; + assert_eq!(result, rejected(message)); + } + let result = guest + .call_transaction(accessor, connection, vec![words(&["RESET"])]) + .await?; + assert_eq!(result.err(), Some(Error::Client(reset.to_string()))); + + // subscribing with exchange is allowed until the connection has a subscription + let result = guest + .call_exchange(accessor, connection, words(&["SUBSCRIBE", "raw"])) + .await?; + assert_eq!(result, Ok(RespValue::Null)); + let _subscription = guest.call_subscription(accessor, connection).await?; + let result = guest + .call_exchange(accessor, connection, words(&["unsubscribe"])) + .await?; + assert_eq!( + result, + rejected( + "UNSUBSCRIBE is rejected once the connection has a subscription, use the subscription's methods" + ) + ); + // only the SUBSCRIBE from before the subscription was sent + assert_eq!(requests(&mut transport).await, vec![command(&["SUBSCRIBE", "raw"])]); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn subscriptions_are_confirmed_by_push_messages() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let subscriptions = exports.subscription(); + let (mut transport, connection) = establish(accessor, exports).await?; + let subscription = guest.call_subscription(accessor, connection).await?; + let (events, _ended) = subscriptions.call_events(accessor, subscription).await?; + let mut events = receive(accessor, events)?; + let dead_letters = guest.call_dead_letters(accessor, connection).await?; + let mut dead_letters = receive(accessor, dead_letters)?; + + let call = subscriptions.call_subscribe( + accessor, + subscription, + vec![channel("news"), channel("sports")], + ); + let server = async { + assert_eq!( + transport.request().await, + Some(command(&["SUBSCRIBE", "news", "sports"])) + ); + transport.reply(push(vec![ + bulk("subscribe"), + bulk("news"), + RespValue::Integer(1), + ])); + transport.reply(push(vec![ + bulk("subscribe"), + bulk("sports"), + RespValue::Integer(2), + ])); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(())); + assert_eq!( + events.recv().await, + Some(SubscriptionEvent::Subscribed(channel("news"))) + ); + assert_eq!( + events.recv().await, + Some(SubscriptionEvent::Subscribed(channel("sports"))) + ); + assert_eq!( + subscriptions.call_targets(accessor, subscription).await?, + vec![channel("news"), channel("sports")] + ); + + // a message for a target goes to the subscription, others are dead letters + transport.reply(push(vec![bulk("message"), bulk("news"), bulk("hello")])); + let unowned = push(vec![bulk("message"), bulk("weather"), bulk("rain")]); + transport.reply(unowned.clone()); + assert_eq!( + events.recv().await, + Some(SubscriptionEvent::Message(SubscriptionMessage { + payload: "hello".to_string(), + channel: "news".to_string(), + target: channel("news"), + })) + ); + assert_eq!(dead_letters.recv().await, Some(unowned)); + + let call = + subscriptions.call_unsubscribe(accessor, subscription, Some(vec![channel("news")])); + let server = async { + assert_eq!( + transport.request().await, + Some(command(&["UNSUBSCRIBE", "news"])) + ); + transport.reply(push(vec![ + bulk("unsubscribe"), + bulk("news"), + RespValue::Integer(1), + ])); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(())); + assert_eq!( + events.recv().await, + Some(SubscriptionEvent::Unsubscribed(( + channel("news"), + UnsubscribeReason::Requested + ))) + ); + assert_eq!( + subscriptions.call_targets(accessor, subscription).await?, + vec![channel("sports")] + ); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn error_replies_fail_a_subscribe() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let subscriptions = exports.subscription(); + let (mut transport, connection) = establish(accessor, exports).await?; + let subscription = guest.call_subscription(accessor, connection).await?; + let subscribe = + subscriptions.call_subscribe(accessor, subscription, vec![channel("secret")]); + let get = guest.call_exchange(accessor, connection, words(&["GET", "a"])); + let server = async { + transport.request().await; + transport.request().await; + // in the place of the confirmation, then the reply to GET + transport.reply(RespValue::Error( + "NOPERM No permissions to access a channel".to_string(), + )); + transport.reply(bulk("1")); + }; + let (subscribed, got, ()) = tokio::join!(subscribe, get, server); + assert_eq!( + subscribed?, + Err(Error::Valkey( + "NOPERM No permissions to access a channel".to_string() + )) + ); + assert_eq!(got?, Ok(bulk("1"))); + assert_eq!( + subscriptions.call_targets(accessor, subscription).await?, + Vec::::new() + ); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn dropping_a_subscription_unsubscribes() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + let (mut transport, subscription) = harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let subscriptions = exports.subscription(); + let (mut transport, connection) = establish(accessor, exports).await?; + let subscription = guest.call_subscription(accessor, connection).await?; + let call = subscriptions.call_subscribe(accessor, subscription, vec![channel("news")]); + let server = async { + transport.request().await; + transport.reply(push(vec![ + bulk("subscribe"), + bulk("news"), + RespValue::Integer(1), + ])); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(())); + Ok((transport, subscription)) + }) + .await?; + harness.drop_resource(subscription).await?; + harness + .run(async |_accessor, _exports| { + assert_eq!( + transport.request().await, + Some(command(&["UNSUBSCRIBE", "news"])) + ); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn reset_holds_commands_until_hello_succeeds() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + let reset = guest.call_reset(accessor, connection); + let get = guest.call_exchange(accessor, connection, words(&["GET", "a"])); + let server = async { + assert_eq!(transport.request().await, Some(command(&["RESET"]))); + assert_eq!(transport.request().await, Some(command(&["HELLO", "3"]))); + // GET is not sent while the connection is on RESP2, without its user + assert!( + tokio::time::timeout(SETTLE, transport.request()) + .await + .is_err() + ); + transport.reply(simple("RESET")); + transport.reply(hello_reply()); + assert_eq!(transport.request().await, Some(command(&["GET", "a"]))); + transport.reply(bulk("1")); + }; + let (reset, get, ()) = tokio::join!(reset, get, server); + assert_eq!(reset?, Ok(())); + assert_eq!(get?, Ok(bulk("1"))); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn reset_closes_the_connection_when_hello_fails() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + let wrongpass = Error::Valkey("WRONGPASS invalid username-password pair".to_string()); + let reset = guest.call_reset(accessor, connection); + let get = guest.call_exchange(accessor, connection, words(&["GET", "a"])); + let server = async { + assert_eq!(transport.request().await, Some(command(&["RESET"]))); + assert_eq!(transport.request().await, Some(command(&["HELLO", "3"]))); + transport.reply(simple("RESET")); + transport.reply(RespValue::Error( + "WRONGPASS invalid username-password pair".to_string(), + )); + // GET was never sent, the connection closed its requests stream + assert_eq!(transport.request().await, None); + }; + let (reset, get, ()) = tokio::join!(reset, get, server); + assert_eq!(reset?, Err(wrongpass.clone())); + assert_eq!(get?, Err(wrongpass.clone())); + let later = guest + .call_exchange(accessor, connection, words(&["PING"])) + .await?; + assert_eq!(later, Err(wrongpass)); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn exchange_all_sends_the_commands_together() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + let call = guest.call_exchange_all( + accessor, + connection, + vec![words(&["SET", "a", "1"]), vec![], words(&["GET", "a"])], + ); + let server = async { + assert_eq!(transport.request().await, Some(command(&["SET", "a", "1"]))); + assert_eq!(transport.request().await, Some(command(&["GET", "a"]))); + transport.reply(RespValue::Error("ERR oops".to_string())); + transport.reply(bulk("1")); + }; + let (results, ()) = tokio::join!(call, server); + assert_eq!( + results?, + vec![ + Err(Error::Valkey("ERR oops".to_string())), + // an empty command fails in its place, the others are sent + Err(Error::Client( + "a command requires at least one word".to_string() + )), + Ok(bulk("1")), + ] + ); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn transactions_wrap_the_commands_in_multi_and_exec() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + let queued = || simple("QUEUED"); + + // committed, a command that fails while running is an error value in its place + let call = guest.call_transaction( + accessor, + connection, + vec![words(&["SET", "a", "1"]), words(&["INCR", "text"])], + ); + let server = async { + assert_eq!(transport.request().await, Some(command(&["MULTI"]))); + assert_eq!(transport.request().await, Some(command(&["SET", "a", "1"]))); + assert_eq!(transport.request().await, Some(command(&["INCR", "text"]))); + assert_eq!(transport.request().await, Some(command(&["EXEC"]))); + transport.reply(ok()); + transport.reply(queued()); + transport.reply(queued()); + transport.reply(RespValue::Array(vec![ + test_harness::encode(ok()), + test_harness::encode(RespValue::Error("ERR not an integer".to_string())), + ])); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!( + result?, + Ok(Some(vec![ + ok(), + RespValue::Error("ERR not an integer".to_string()) + ])) + ); + + // a watched key changed + let call = + guest.call_transaction(accessor, connection, vec![words(&["SET", "a", "2"])]); + let server = async { + for _ in 0..3 { + transport.request().await; + } + transport.reply(ok()); + transport.reply(queued()); + transport.reply(RespValue::Null); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(None)); + + // a command rejected while queuing fails the transaction with its own error + let call = guest.call_transaction(accessor, connection, vec![words(&["NOPE"])]); + let server = async { + for _ in 0..3 { + transport.request().await; + } + transport.reply(ok()); + transport.reply(RespValue::Error("ERR unknown command 'NOPE'".to_string())); + transport.reply(RespValue::Error( + "EXECABORT Transaction discarded because of previous errors.".to_string(), + )); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!( + result?, + Err(Error::Valkey("ERR unknown command 'NOPE'".to_string())) + ); + + // an empty command fails before MULTI is sent + let result = guest + .call_transaction(accessor, connection, vec![words(&["GET", "a"]), vec![]]) + .await?; + assert_eq!( + result, + Err(Error::Client( + "a command requires at least one word".to_string() + )) + ); + assert_eq!(requests(&mut transport).await, Vec::>::new()); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn watch_and_unwatch() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + let call = guest.call_watch(accessor, connection, words(&["a", "b"])); + let server = async { + assert_eq!( + transport.request().await, + Some(command(&["WATCH", "a", "b"])) + ); + transport.reply(ok()); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(())); + + let call = guest.call_unwatch(accessor, connection); + let server = async { + assert_eq!(transport.request().await, Some(command(&["UNWATCH"]))); + transport.reply(ok()); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(())); + + let result = guest.call_watch(accessor, connection, vec![]).await?; + assert_eq!( + result, + Err(Error::Client("keys requires at least one item".to_string())) + ); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn mixed_kinds_are_subscribed_with_a_command_each() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let subscriptions = exports.subscription(); + let (mut transport, connection) = establish(accessor, exports).await?; + let subscription = guest.call_subscription(accessor, connection).await?; + let (events, _ended) = subscriptions.call_events(accessor, subscription).await?; + let mut events = receive(accessor, events)?; + + let targets = vec![ + channel("news"), + pattern("n*"), + shardchannel("orders"), + channel("sports"), + ]; + let call = subscriptions.call_subscribe(accessor, subscription, targets); + let server = async { + assert_eq!( + transport.request().await, + Some(command(&["SUBSCRIBE", "news", "sports"])) + ); + assert_eq!( + transport.request().await, + Some(command(&["PSUBSCRIBE", "n*"])) + ); + assert_eq!( + transport.request().await, + Some(command(&["SSUBSCRIBE", "orders"])) + ); + transport.reply(confirm("subscribe", "news")); + transport.reply(confirm("subscribe", "sports")); + // every command is sent, one failing does not stop the others + transport.reply(RespValue::Error("NOPERM no patterns".to_string())); + transport.reply(confirm("ssubscribe", "orders")); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!( + result?, + Err(Error::Valkey("PSUBSCRIBE: NOPERM no patterns".to_string())) + ); + for target in [channel("news"), channel("sports"), shardchannel("orders")] { + assert_eq!( + events.recv().await, + Some(SubscriptionEvent::Subscribed(target)) + ); + } + assert_eq!( + subscriptions.call_targets(accessor, subscription).await?, + vec![channel("news"), channel("sports"), shardchannel("orders")] + ); + + // the targets given back to unsubscribe, a command for each kind + let targets = subscriptions.call_targets(accessor, subscription).await?; + let call = subscriptions.call_unsubscribe(accessor, subscription, Some(targets)); + let server = async { + assert_eq!( + transport.request().await, + Some(command(&["UNSUBSCRIBE", "news", "sports"])) + ); + assert_eq!( + transport.request().await, + Some(command(&["SUNSUBSCRIBE", "orders"])) + ); + transport.reply(confirm("unsubscribe", "news")); + transport.reply(confirm("unsubscribe", "sports")); + transport.reply(confirm("sunsubscribe", "orders")); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(())); + assert_eq!( + subscriptions.call_targets(accessor, subscription).await?, + Vec::::new() + ); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn overlapping_subscriptions_share_the_servers() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let subscriptions = exports.subscription(); + let (mut transport, connection) = establish(accessor, exports).await?; + let first = guest.call_subscription(accessor, connection).await?; + let second = guest.call_subscription(accessor, connection).await?; + let (first_events, _) = subscriptions.call_events(accessor, first).await?; + let mut first_events = receive(accessor, first_events)?; + let (second_events, _) = subscriptions.call_events(accessor, second).await?; + let mut second_events = receive(accessor, second_events)?; + + for subscription in [first, second] { + let call = + subscriptions.call_subscribe(accessor, subscription, vec![channel("news")]); + let server = async { + assert_eq!( + transport.request().await, + Some(command(&["SUBSCRIBE", "news"])) + ); + transport.reply(confirm("subscribe", "news")); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(())); + } + transport.reply(push(vec![bulk("message"), bulk("news"), bulk("both")])); + for events in [&mut first_events, &mut second_events] { + assert_eq!( + events.recv().await, + Some(SubscriptionEvent::Subscribed(channel("news"))) + ); + assert_eq!( + events.recv().await, + message("both", "news", channel("news")) + ); + } + + // the other subscription keeps the channel, nothing is sent + let result = subscriptions + .call_unsubscribe(accessor, first, Some(vec![channel("news")])) + .await?; + assert_eq!(result, Ok(())); + assert_eq!( + first_events.recv().await, + unsubscribed(channel("news"), UnsubscribeReason::Requested) + ); + assert_eq!(requests(&mut transport).await, Vec::>::new()); + transport.reply(push(vec![bulk("message"), bulk("news"), bulk("second")])); + assert_eq!( + second_events.recv().await, + message("second", "news", channel("news")) + ); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn pattern_messages_carry_the_channel() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let subscriptions = exports.subscription(); + let (mut transport, connection) = establish(accessor, exports).await?; + let subscription = guest.call_subscription(accessor, connection).await?; + let (events, _) = subscriptions.call_events(accessor, subscription).await?; + let mut events = receive(accessor, events)?; + let call = subscriptions.call_subscribe(accessor, subscription, vec![pattern("n*")]); + let server = async { + transport.request().await; + transport.reply(confirm("psubscribe", "n*")); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(())); + transport.reply(push(vec![ + bulk("pmessage"), + bulk("n*"), + bulk("news"), + bulk("hello"), + ])); + assert_eq!( + events.recv().await, + Some(SubscriptionEvent::Subscribed(pattern("n*"))) + ); + assert_eq!(events.recv().await, message("hello", "news", pattern("n*"))); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn targets_end_by_the_server_reset_and_closing() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let subscriptions = exports.subscription(); + let (mut transport, connection) = establish(accessor, exports).await?; + let subscription = guest.call_subscription(accessor, connection).await?; + let (events, ended) = subscriptions.call_events(accessor, subscription).await?; + let mut events = receive(accessor, events)?; + let targets = vec![shardchannel("orders"), channel("news")]; + let call = subscriptions.call_subscribe(accessor, subscription, targets); + let server = async { + transport.request().await; + transport.request().await; + transport.reply(confirm("ssubscribe", "orders")); + transport.reply(confirm("subscribe", "news")); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(())); + events.recv().await; + events.recv().await; + + // the slot of the shard channel moved, the server unsubscribed on its own + transport.reply(confirm("sunsubscribe", "orders")); + assert_eq!( + events.recv().await, + unsubscribed(shardchannel("orders"), UnsubscribeReason::ByServer) + ); + + // RESET unsubscribes from every target, the subscription stays open + let call = guest.call_reset(accessor, connection); + let server = async { + transport.request().await; + transport.request().await; + transport.reply(simple("RESET")); + transport.reply(hello_reply()); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(())); + assert_eq!( + events.recv().await, + unsubscribed(channel("news"), UnsubscribeReason::Reset) + ); + + // closing the connection closes the stream with the error + let reason = Error::Closed(Some("connection reset".to_string())); + transport.close(Err(reason.clone())); + assert_eq!(events.recv().await, None); + assert_eq!(resolve(accessor, ended).await?, Err(reason)); + Ok(()) + }) + .await +} + +/// The tests against a Valkey server, ignored unless run with `make test-valkey`, which runs one in +/// a container and only runs the ignored tests in `with_valkey` modules. They connect with +/// the connector of `test-tcp`, and leave nothing behind on the server. +mod with_valkey { + use test_harness::{Ctx, Exports}; + use wasmtime::component::{Accessor, ResourceAny}; + + use super::*; + + async fn connect(accessor: &Accessor, exports: &Exports) -> wasmtime::Result { + exports + .connector() + .call_connect(accessor) + .await? + .map_err(|error| wasmtime::format_err!("failed to connect: {error:?}")) + } + + async fn exchange( + accessor: &Accessor, + exports: &Exports, + connection: ResourceAny, + command: &[&str], + ) -> wasmtime::Result> { + exports + .connection() + .call_exchange(accessor, connection, words(command)) + .await + } + + /// PUBSUB NUMSUB of one channel. + async fn numsub( + accessor: &Accessor, + exports: &Exports, + connection: ResourceAny, + channel: &str, + ) -> wasmtime::Result { + let reply = exchange( + accessor, + exports, + connection, + &["PUBSUB", "NUMSUB", channel], + ) + .await? + .map_err(|error| wasmtime::format_err!("{error:?}"))?; + let RespValue::Array(mut items) = reply else { + wasmtime::bail!("unexpected {reply:?}"); + }; + test_harness::decode(items.remove(1)).map_err(|error| wasmtime::format_err!("{error}")) + } + + #[tokio::test(flavor = "multi_thread")] + #[ignore = "needs a Valkey server, see make test-valkey"] + async fn subscriptions_against_valkey() -> wasmtime::Result<()> { + let mut harness = Harness::with_valkey("test-tcp", &[]).await?; + harness + .run(async |accessor, exports| { + let subscriptions = exports.subscription(); + let connection = connect(accessor, exports).await?; + let publisher = connect(accessor, exports).await?; + let subscription = exports + .connection() + .call_subscription(accessor, connection) + .await?; + let (events, ended) = subscriptions.call_events(accessor, subscription).await?; + let mut events = receive(accessor, events)?; + + let targets = vec![ + channel("t:conn:a"), + pattern("t:conn:p*"), + shardchannel("t:conn:s"), + ]; + let result = subscriptions + .call_subscribe(accessor, subscription, targets.clone()) + .await?; + assert_eq!(result, Ok(())); + for target in &targets { + assert_eq!( + events.recv().await, + Some(SubscriptionEvent::Subscribed(target.clone())) + ); + } + + exchange( + accessor, + exports, + publisher, + &["PUBLISH", "t:conn:a", "one"], + ) + .await??; + assert_eq!( + events.recv().await, + message("one", "t:conn:a", channel("t:conn:a")) + ); + exchange( + accessor, + exports, + publisher, + &["PUBLISH", "t:conn:p1", "two"], + ) + .await??; + assert_eq!( + events.recv().await, + message("two", "t:conn:p1", pattern("t:conn:p*")) + ); + exchange( + accessor, + exports, + publisher, + &["SPUBLISH", "t:conn:s", "three"], + ) + .await??; + assert_eq!( + events.recv().await, + message("three", "t:conn:s", shardchannel("t:conn:s")) + ); + + // another subscription to the same channel shares the server's subscription + let other = exports + .connection() + .call_subscription(accessor, connection) + .await?; + subscriptions + .call_subscribe(accessor, other, vec![channel("t:conn:a")]) + .await??; + assert_eq!( + numsub(accessor, exports, publisher, "t:conn:a").await?, + RespValue::Integer(1) + ); + subscriptions + .call_unsubscribe(accessor, subscription, None) + .await??; + for target in &targets { + assert_eq!( + events.recv().await, + unsubscribed(target.clone(), UnsubscribeReason::Requested) + ); + } + assert_eq!( + numsub(accessor, exports, publisher, "t:conn:a").await?, + RespValue::Integer(1), + "kept for the other subscription" + ); + subscriptions + .call_unsubscribe(accessor, other, None) + .await??; + assert_eq!( + numsub(accessor, exports, publisher, "t:conn:a").await?, + RespValue::Integer(0) + ); + + // the stream stays open until the subscription is dropped + assert!( + tokio::time::timeout(SETTLE, events.recv()).await.is_err(), + "the stream closed" + ); + drop(ended); + Ok(()) + }) + .await + } + + #[tokio::test(flavor = "multi_thread")] + #[ignore = "needs a Valkey server, see make test-valkey"] + async fn partial_subscribe_failure_against_valkey() -> wasmtime::Result<()> { + let mut harness = Harness::with_valkey("test-tcp", &[]).await?; + harness + .run(async |accessor, exports| { + let subscriptions = exports.subscription(); + let admin = connect(accessor, exports).await?; + let user = [ + "ACL", + "SETUSER", + "t:conn-limited", + "on", + "nopass", + "resetchannels", + "&t:conn:ok", + "+@all", + "~*", + ]; + exchange(accessor, exports, admin, &user).await??; + let result = async { + let connection = connect(accessor, exports).await?; + let auth = ["AUTH", "t:conn-limited", "any"]; + exchange(accessor, exports, connection, &auth).await??; + let subscription = exports + .connection() + .call_subscription(accessor, connection) + .await?; + let targets = vec![ + pattern("t:conn:no*"), + channel("t:conn:ok"), + shardchannel("t:conn:no"), + ]; + let result = subscriptions + .call_subscribe(accessor, subscription, targets) + .await?; + let Err(Error::Valkey(message)) = result else { + wasmtime::bail!("expected a valkey error, got {result:?}"); + }; + assert!( + message.starts_with("PSUBSCRIBE: NOPERM") + && message.contains("; SSUBSCRIBE: NOPERM"), + "{message}" + ); + assert_eq!( + subscriptions.call_targets(accessor, subscription).await?, + vec![channel("t:conn:ok")] + ); + Ok(()) + } + .await; + exchange( + accessor, + exports, + admin, + &["ACL", "DELUSER", "t:conn-limited"], + ) + .await??; + result + }) + .await + } + + #[tokio::test(flavor = "multi_thread")] + #[ignore = "needs a Valkey server, see make test-valkey"] + async fn reset_against_valkey() -> wasmtime::Result<()> { + let mut harness = Harness::with_valkey("test-tcp", &[]).await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let subscriptions = exports.subscription(); + let connection = connect(accessor, exports).await?; + exchange(accessor, exports, connection, &["SELECT", "1"]).await??; + exchange( + accessor, + exports, + connection, + &["SET", "t:conn:reset", "db1"], + ) + .await??; + let subscription = guest.call_subscription(accessor, connection).await?; + let (events, _ended) = subscriptions.call_events(accessor, subscription).await?; + let mut events = receive(accessor, events)?; + subscriptions + .call_subscribe(accessor, subscription, vec![channel("t:conn:r")]) + .await??; + events.recv().await; + + guest.call_reset(accessor, connection).await??; + assert_eq!( + events.recv().await, + unsubscribed(channel("t:conn:r"), UnsubscribeReason::Reset) + ); + assert_eq!( + exchange(accessor, exports, connection, &["GET", "t:conn:reset"]).await?, + Ok(RespValue::Null), + "reset selects database 0" + ); + let properties = guest.call_server_properties(accessor, connection).await?; + assert!(properties.contains(&("proto".to_string(), RespValue::Integer(3)))); + let info = exchange(accessor, exports, connection, &["CLIENT", "INFO"]).await??; + let RespValue::VerbatimString((_, info)) = info else { + wasmtime::bail!("unexpected {info:?}"); + }; + assert!(info.contains("lib-name=componentized:valkey"), "{info}"); + + // the subscription is usable after the reset + subscriptions + .call_subscribe(accessor, subscription, vec![channel("t:conn:r")]) + .await??; + assert_eq!( + events.recv().await, + Some(SubscriptionEvent::Subscribed(channel("t:conn:r"))) + ); + + exchange(accessor, exports, connection, &["SELECT", "1"]).await??; + exchange(accessor, exports, connection, &["DEL", "t:conn:reset"]).await??; + Ok(()) + }) + .await + } + + #[tokio::test(flavor = "multi_thread")] + #[ignore = "needs a Valkey server, see make test-valkey"] + async fn expired_credentials_close_on_reset_against_valkey() -> wasmtime::Result<()> { + let mut setup = Harness::with_valkey("test-tcp", &[]).await?; + let user = [ + "ACL", + "SETUSER", + "t:conn-expire", + "on", + ">old", + "~*", + "&*", + "+@all", + ]; + setup + .run(async |accessor, exports| { + let admin = connect(accessor, exports).await?; + exchange(accessor, exports, admin, &user).await??; + Ok(()) + }) + .await?; + let config = [("username", "t:conn-expire"), ("password", "old")]; + let mut harness = Harness::with_valkey("test-tcp", &config).await?; + let result = harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let connection = connect(accessor, exports).await?; + let other = connect(accessor, exports).await?; + // connections already authenticated stay so + let expire = ["ACL", "SETUSER", "t:conn-expire", "resetpass", ">new"]; + exchange(accessor, exports, other, &expire).await??; + let reset = guest.call_reset(accessor, connection); + let set = guest.call_exchange( + accessor, + connection, + words(&["SET", "t:conn:leak", "1"]), + ); + let (reset, set) = tokio::join!(reset, set); + let wrongpass = |result: &Result<(), Error>| { + matches!(result, Err(Error::Valkey(message)) if message.starts_with("WRONGPASS")) + }; + assert!(wrongpass(&reset?), "the reset fails"); + assert!(wrongpass(&set?.map(|_| ())), "the command after it fails"); + assert_eq!( + exchange(accessor, exports, other, &["GET", "t:conn:leak"]).await?, + Ok(RespValue::Null), + "and never ran" + ); + Ok(()) + }) + .await; + setup + .run(async |accessor, exports| { + let admin = connect(accessor, exports).await?; + exchange( + accessor, + exports, + admin, + &["ACL", "DELUSER", "t:conn-expire"], + ) + .await??; + Ok(()) + }) + .await?; + result + } + + #[tokio::test(flavor = "multi_thread")] + #[ignore = "needs a Valkey server, see make test-valkey"] + async fn killed_connections_keep_their_targets_against_valkey() -> wasmtime::Result<()> { + let mut harness = Harness::with_valkey("test-tcp", &[]).await?; + harness + .run(async |accessor, exports| { + let subscriptions = exports.subscription(); + let connection = connect(accessor, exports).await?; + let admin = connect(accessor, exports).await?; + let RespValue::Integer(id) = + exchange(accessor, exports, connection, &["CLIENT", "ID"]).await?? + else { + wasmtime::bail!("unexpected CLIENT ID"); + }; + let subscription = exports + .connection() + .call_subscription(accessor, connection) + .await?; + let (events, ended) = subscriptions.call_events(accessor, subscription).await?; + let mut events = receive(accessor, events)?; + let targets = vec![channel("t:conn:k"), pattern("t:conn:k*")]; + subscriptions + .call_subscribe(accessor, subscription, targets.clone()) + .await??; + let kill = ["CLIENT", "KILL", "ID", &id.to_string()]; + exchange(accessor, exports, admin, &kill).await??; + while events.recv().await.is_some() {} + assert!(matches!( + resolve(accessor, ended).await?, + Err(Error::Closed(_)) + )); + assert_eq!( + subscriptions.call_targets(accessor, subscription).await?, + targets, + "the confirmed targets are still listed" + ); + let result = subscriptions + .call_subscribe(accessor, subscription, vec![channel("t:conn:k2")]) + .await?; + assert!(matches!(result, Err(Error::Closed(_)))); + Ok(()) + }) + .await + } +} diff --git a/components/dep-cli/src/lib.rs b/components/dep-cli/src/lib.rs index 7b558af..8c76a7d 100644 --- a/components/dep-cli/src/lib.rs +++ b/components/dep-cli/src/lib.rs @@ -434,3 +434,184 @@ wit_bindgen::generate!({ }); export!(Cli); + +#[cfg(test)] +mod tests { + use super::*; + + fn nested(value: RespValue) -> RespNestedValue { + componentized_valkey::resp::encode(value) + } + + fn bulk(text: &str) -> RespValue { + RespValue::BulkString(text.to_string()) + } + + #[test] + fn split_on_whitespace() { + assert_eq!( + split(" SET key\tvalue "), + Ok(vec![ + "SET".to_string(), + "key".to_string(), + "value".to_string() + ]) + ); + assert_eq!(split(""), Ok(vec![])); + assert_eq!(split(" "), Ok(vec![])); + } + + #[test] + fn split_double_quotes() { + assert_eq!( + split(r#"SET key "hello world" "a\"b" "\n\r\t\b\a" "\x41\x7a" "\q""#), + Ok(vec![ + "SET".to_string(), + "key".to_string(), + "hello world".to_string(), + "a\"b".to_string(), + "\n\r\t\u{8}\u{7}".to_string(), + "Az".to_string(), + "q".to_string(), + ]) + ); + assert_eq!(split(r#""""#), Ok(vec![String::new()])); + } + + #[test] + fn split_single_quotes() { + assert_eq!( + split(r#"'hello world' 'it\'s' 'a\nb'"#), + Ok(vec![ + "hello world".to_string(), + "it's".to_string(), + "a\\nb".to_string() + ]) + ); + } + + #[test] + fn split_errors() { + assert_eq!(split(r#"SET "key"#), Err("unbalanced quotes".to_string())); + assert_eq!(split("'key"), Err("unbalanced quotes".to_string())); + assert_eq!(split(r#""key\"#), Err("unbalanced quotes".to_string())); + assert_eq!( + split(r#""key"value"#), + Err("closing quote must be followed by a space".to_string()) + ); + // a byte on its own is only a character when it is ASCII + assert_eq!( + split(r#""\xff""#), + Err("unsupported escape: \\xff".to_string()) + ); + assert_eq!( + split(r#""\xzz""#), + Err("unsupported escape: \\xzz".to_string()) + ); + } + + #[test] + fn render_scalars() { + assert_eq!(render(RespValue::String("OK".to_string())), "OK"); + assert_eq!( + render(RespValue::Error("ERR oops".to_string())), + "(error) ERR oops" + ); + assert_eq!( + render(RespValue::BulkError("SYNTAX".to_string())), + "(error) SYNTAX" + ); + assert_eq!(render(RespValue::Integer(-1)), "(integer) -1"); + assert_eq!(render(bulk("a\"b\n")), r#""a\"b\n""#); + assert_eq!(render(RespValue::Null), "(nil)"); + assert_eq!(render(RespValue::Boolean(true)), "(true)"); + assert_eq!(render(RespValue::Double(1.5)), "(double) 1.5"); + assert_eq!( + render(RespValue::BigNumber("12345678901234567890".to_string())), + "(big number) 12345678901234567890" + ); + assert_eq!( + render(RespValue::VerbatimString(( + "txt".to_string(), + "line 1\nline 2".to_string() + ))), + "line 1\nline 2" + ); + } + + #[test] + fn render_aggregates() { + assert_eq!(render(RespValue::Array(vec![])), "(empty array)"); + assert_eq!(render(RespValue::Set(vec![])), "(empty set)"); + assert_eq!(render(RespValue::Map(vec![])), "(empty map)"); + assert_eq!( + render(RespValue::Array(vec![ + nested(bulk("a")), + nested(RespValue::Integer(1)) + ])), + "1) \"a\"\n2) (integer) 1" + ); + assert_eq!(render(RespValue::Set(vec![nested(bulk("a"))])), "1~ \"a\""); + assert_eq!( + render(RespValue::Push(vec![ + nested(bulk("message")), + nested(bulk("news")) + ])), + "1) \"message\"\n2) \"news\"" + ); + assert_eq!( + render(RespValue::Map(vec![( + nested(bulk("proto")), + nested(RespValue::Integer(3)) + )])), + "1# \"proto\" => (integer) 3" + ); + } + + #[test] + fn render_nested_aggregates_line_up() { + let inner = RespValue::Array(vec![nested(bulk("x")), nested(bulk("y"))]); + assert_eq!( + render(RespValue::Array(vec![nested(bulk("a")), nested(inner)])), + "1) \"a\"\n2) 1) \"x\"\n 2) \"y\"" + ); + let entry = RespValue::Array(vec![nested(bulk("x")), nested(bulk("y"))]); + assert_eq!( + render(RespValue::Map(vec![(nested(bulk("k")), nested(entry))])), + "1# \"k\" => 1) \"x\"\n 2) \"y\"" + ); + } + + #[test] + fn render_numbers_right_aligned() { + let items = (0..10).map(|n| nested(RespValue::Integer(n))).collect(); + let rendered = render(RespValue::Array(items)); + let lines: Vec<&str> = rendered.lines().collect(); + assert_eq!(lines[0], " 1) (integer) 0"); + assert_eq!(lines[9], "10) (integer) 9"); + } + + #[test] + fn render_invalid_nested_values() { + assert!(render(RespValue::Array(vec![b"?".to_vec()])).starts_with("1) (invalid) ")); + } + + #[test] + fn render_errors() { + assert_eq!( + render_error(Error::Valkey("ERR oops".to_string())), + "(error) ERR oops" + ); + assert_eq!(render_error(Error::Closed(None)), "connection closed"); + assert_eq!( + render_error(Error::Closed(Some("reset".to_string()))), + "connection closed: reset" + ); + assert_eq!(render_error(Error::Client("bad".to_string())), "bad"); + assert_eq!( + render_error(Error::Resp("bad".to_string())), + "invalid RESP: bad" + ); + assert_eq!(render_error(Error::Lagged), "fell behind, values were lost"); + } +} diff --git a/components/dep-test-commands/Cargo.toml b/components/dep-test-commands/Cargo.toml new file mode 100644 index 0000000..5f5a054 --- /dev/null +++ b/components/dep-test-commands/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dep-test-commands" +version.workspace = true +edition = "2024" +license = "Apache-2.0" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +componentized-valkey = { workspace = true } +wit-bindgen = { workspace = true } diff --git a/components/dep-test-commands/README.md b/components/dep-test-commands/README.md new file mode 100644 index 0000000..16eaf89 --- /dev/null +++ b/components/dep-test-commands/README.md @@ -0,0 +1,5 @@ +# dep-test-commands + +The tests of the [`commands`](../commands/) component, a component that plays the server over the streams of a [`connection`](../connection/), checking the command each function sends and how it converts the reply. Composed as [`test-commands`](../test-commands/), and run by `cargo test -p commands`. + +Tests named `with-valkey ...` run against a Valkey server on localhost:6379, with `make test-valkey`. diff --git a/components/dep-test-commands/src/lib.rs b/components/dep-test-commands/src/lib.rs new file mode 100644 index 0000000..8ea867d --- /dev/null +++ b/components/dep-test-commands/src/lib.rs @@ -0,0 +1,732 @@ +//! The tests of the `commands` component, run inside a component by the test harness. +//! +//! Each test plays the server over the streams of a connection it establishes with +//! `connection.new`: it reads the command a function sends, answers it, and checks how the function +//! converts the reply. The tests named `with-valkey ...` run against a Valkey server instead, from +//! the connector. + +use std::future::{Future, poll_fn}; +use std::pin::pin; +use std::task::Poll; + +use componentized::valkey::commands; +use componentized::valkey::connection::{Connection, Error}; +use componentized::valkey::connector; +use componentized_valkey::types::{ + AclRule, AclUserRule, Cursor, DurationSeconds, HexpireResult, RespValue, TtlMode, + UnixTimeSeconds, +}; +use exports::componentized::valkey_components::test_suite::Guest; +use wit_bindgen::rt::Map; +use wit_bindgen::{FutureWriter, StreamReader, StreamWriter}; + +struct Suite; + +/// The tests, by name, and the function running each. +macro_rules! suite { + ($($name:literal => $test:ident),* $(,)?) => { + const TESTS: &[&str] = &[$($name),*]; + + async fn run_test(name: &str) -> Result<(), String> { + match name { + $($name => $test().await,)* + _ => Err(format!("no test named {name:?}")), + } + } + }; +} + +suite! { + "string commands" => string_commands, + "keyspace commands" => keyspace_commands, + "hash commands" => hash_commands, + "connection commands" => connection_commands, + "admin and pubsub commands" => admin_and_pubsub_commands, + "invalid arguments fail before anything is sent" => invalid_arguments, + "unexpected replies fail the command" => unexpected_replies, + "error replies fail the command" => error_replies, + "with-valkey commands" => with_valkey, +} + +impl Guest for Suite { + fn tests() -> Vec { + TESTS.iter().map(|name| name.to_string()).collect() + } + + async fn run(name: String) -> Result<(), String> { + run_test(&name).await + } +} + +/// Fails the test unless the values are equal, with where and both values. +macro_rules! check_eq { + ($left:expr, $right:expr $(,)?) => {{ + let (left, right) = (&$left, &$right); + if left != right { + return Err(format!( + "{}:{}: {:?} != {:?}", + file!(), + line!(), + left, + right + )); + } + }}; +} + +/// The words of a command, e.g. `words(&["GET", "key"])`. +fn words(words: &[&str]) -> Vec { + words.iter().map(|word| word.to_string()).collect() +} + +fn s(value: &str) -> String { + value.to_string() +} + +fn bulk(value: &str) -> RespValue { + RespValue::BulkString(value.to_string()) +} + +fn ok() -> RespValue { + RespValue::String("OK".to_string()) +} + +fn array(items: Vec) -> RespValue { + RespValue::Array( + items + .into_iter() + .map(componentized_valkey::resp::encode) + .collect(), + ) +} + +fn map(entries: Vec<(RespValue, RespValue)>) -> RespValue { + RespValue::Map( + entries + .into_iter() + .map(|(key, value)| { + ( + componentized_valkey::resp::encode(key), + componentized_valkey::resp::encode(value), + ) + }) + .collect(), + ) +} + +/// Poll both futures, `a` first each time, until both are ready. +async fn both(a: A, b: B) -> (A::Output, B::Output) { + let mut a = pin!(a); + let mut b = pin!(b); + let (mut a_output, mut b_output) = (None, None); + poll_fn(|cx| { + if a_output.is_none() + && let Poll::Ready(output) = a.as_mut().poll(cx) + { + a_output = Some(output); + } + if b_output.is_none() + && let Poll::Ready(output) = b.as_mut().poll(cx) + { + b_output = Some(output); + } + if a_output.is_some() && b_output.is_some() { + Poll::Ready(()) + } else { + Poll::Pending + } + }) + .await; + (a_output.expect("ready"), b_output.expect("ready")) +} + +/// The server's side of a connection, played by the test. +struct Server { + requests: StreamReader, + replies: StreamWriter, + /// Resolved to `ok` when the server is dropped, the transport closed gracefully. + _closed: FutureWriter>, +} + +impl Server { + /// The words of the next command the connection sent. + async fn request(&mut self) -> Result, String> { + let request = self.requests.next().await; + let Some(RespValue::Array(words)) = request else { + return Err(format!("expected a command, got {request:?}")); + }; + words + .into_iter() + .map(|word| match componentized_valkey::resp::decode(word) { + Ok(RespValue::BulkString(word)) => Ok(word), + word => Err(format!("expected a bulk string, got {word:?}")), + }) + .collect() + } + + async fn reply(&mut self, value: RespValue) -> Result<(), String> { + match self.replies.write_one(value).await { + None => Ok(()), + Some(_) => Err("the connection stopped reading".to_string()), + } + } +} + +/// A connection over streams the server is played on, once it answered the handshake. +async fn establish() -> Result<(Server, Connection), String> { + let (replies, replies_reader) = wit_stream::new(); + let (closed, closed_reader) = wit_future::new(|| Ok(())); + let (requests, connection) = Connection::new(replies_reader, closed_reader, None).await; + let mut server = Server { + requests, + replies, + _closed: closed, + }; + check_eq!(server.request().await?, words(&["HELLO", "3"])); + let version = env!("CARGO_PKG_VERSION"); + let setinfo = [ + words(&["CLIENT", "SETINFO", "LIB-NAME", "componentized:valkey"]), + words(&["CLIENT", "SETINFO", "LIB-VER", version]), + ]; + for expected in &setinfo { + check_eq!(&server.request().await?, expected); + } + let hello = map(vec![ + (bulk("server"), bulk("valkey")), + (bulk("version"), bulk("9.1.2")), + (bulk("proto"), RespValue::Integer(3)), + ]); + server.reply(hello).await?; + for _ in &setinfo { + server.reply(ok()).await?; + } + let connection = connection + .await + .map_err(|error| format!("failed to establish: {error:?}"))?; + Ok((server, connection)) +} + +/// Answer the next command with `reply` while `call` waits for it, returning the call's result and +/// the command sent. +async fn round_trip( + server: &mut Server, + reply: RespValue, + call: impl Future, +) -> Result<(T, Vec), String> { + let serve = async { + let request = server.request().await?; + server.reply(reply).await?; + Ok::<_, String>(request) + }; + let (result, request) = both(call, serve).await; + Ok((result, request?)) +} + +async fn string_commands() -> Result<(), String> { + let (mut server, conn) = establish().await?; + + let (result, sent) = + round_trip(&mut server, bulk("value"), commands::get(&conn, s("key"))).await?; + check_eq!(sent, words(&["GET", "key"])); + check_eq!(result, Ok(Some(s("value")))); + + let (result, _) = round_trip( + &mut server, + RespValue::Null, + commands::get(&conn, s("missing")), + ) + .await?; + check_eq!(result, Ok(None)); + + let (result, sent) = round_trip( + &mut server, + ok(), + commands::set(&conn, s("key"), s("value"), None, None), + ) + .await?; + check_eq!(sent, words(&["SET", "key", "value"])); + check_eq!(result, Ok(true)); + + // null when the precondition is not met + let (result, sent) = round_trip( + &mut server, + RespValue::Null, + commands::set( + &conn, + s("key"), + s("value"), + Some(componentized_valkey::types::SetPrecondition::Nx), + Some(componentized_valkey::types::SetTtl::Ex(DurationSeconds { + ex: 10, + })), + ), + ) + .await?; + check_eq!(sent, words(&["SET", "key", "value", "NX", "EX", "10"])); + check_eq!(result, Ok(false)); + + let (result, sent) = round_trip( + &mut server, + RespValue::Integer(1), + commands::incr(&conn, s("count")), + ) + .await?; + check_eq!(sent, words(&["INCR", "count"])); + check_eq!(result, Ok(1)); + + let (result, sent) = round_trip( + &mut server, + RespValue::Integer(-4), + commands::incrby(&conn, s("count"), -5), + ) + .await?; + check_eq!(sent, words(&["INCRBY", "count", "-5"])); + check_eq!(result, Ok(-4)); + Ok(()) +} + +async fn keyspace_commands() -> Result<(), String> { + let (mut server, conn) = establish().await?; + + let (result, sent) = round_trip( + &mut server, + RespValue::Integer(1), + commands::del(&conn, words(&["a", "b"])), + ) + .await?; + check_eq!(sent, words(&["DEL", "a", "b"])); + check_eq!(result, Ok(1)); + + let (result, sent) = round_trip( + &mut server, + RespValue::Integer(0), + commands::exists(&conn, words(&["key"])), + ) + .await?; + check_eq!(sent, words(&["EXISTS", "key"])); + check_eq!(result, Ok(0)); + + let (result, sent) = round_trip( + &mut server, + RespValue::Integer(1), + commands::expire( + &conn, + s("key"), + DurationSeconds { ex: 91 }, + Some(TtlMode::Nx), + ), + ) + .await?; + check_eq!(sent, words(&["EXPIRE", "key", "91", "NX"])); + check_eq!(result, Ok(true)); + + let (result, sent) = round_trip( + &mut server, + RespValue::Integer(0), + commands::expireat( + &conn, + s("key"), + UnixTimeSeconds { + exat: 1_900_000_000, + }, + None, + ), + ) + .await?; + check_eq!(sent, words(&["EXPIREAT", "key", "1900000000"])); + check_eq!(result, Ok(false)); + + let (result, sent) = round_trip( + &mut server, + array(vec![bulk("a"), bulk("b")]), + commands::keys(&conn, s("*")), + ) + .await?; + check_eq!(sent, words(&["KEYS", "*"])); + check_eq!(result, Ok(words(&["a", "b"]))); + + // TYPE replies `none` for a key that does not exist + let (result, sent) = round_trip( + &mut server, + RespValue::String(s("none")), + commands::type_(&conn, s("missing")), + ) + .await?; + check_eq!(sent, words(&["TYPE", "missing"])); + check_eq!(result, Ok(None)); + Ok(()) +} + +async fn hash_commands() -> Result<(), String> { + let (mut server, conn) = establish().await?; + let key = || s("hash"); + + let (result, sent) = round_trip( + &mut server, + RespValue::Integer(2), + commands::hset(&conn, key(), vec![(s("f"), s("v")), (s("g"), s("w"))]), + ) + .await?; + check_eq!(sent, words(&["HSET", "hash", "f", "v", "g", "w"])); + check_eq!(result, Ok(2)); + + let (result, sent) = + round_trip(&mut server, bulk("v"), commands::hget(&conn, key(), s("f"))).await?; + check_eq!(sent, words(&["HGET", "hash", "f"])); + check_eq!(result, Ok(Some(s("v")))); + + // RESP3 replies to HGETALL with a map + let (result, sent) = round_trip( + &mut server, + map(vec![(bulk("f"), bulk("v")), (bulk("g"), bulk("w"))]), + commands::hgetall(&conn, key()), + ) + .await?; + check_eq!(sent, words(&["HGETALL", "hash"])); + check_eq!(result, Ok(Map::from([(s("f"), s("v")), (s("g"), s("w"))]))); + + let (result, _) = round_trip( + &mut server, + RespValue::Integer(1), + commands::hsetnx(&conn, key(), s("f"), s("v")), + ) + .await?; + check_eq!(result, Ok(true)); + + let (result, _) = round_trip( + &mut server, + RespValue::Integer(0), + commands::hexists(&conn, key(), s("x")), + ) + .await?; + check_eq!(result, Ok(false)); + + let (result, sent) = round_trip( + &mut server, + array(vec![bulk("v"), RespValue::Null]), + commands::hmget(&conn, key(), words(&["f", "x"])), + ) + .await?; + check_eq!(sent, words(&["HMGET", "hash", "f", "x"])); + check_eq!(result, Ok(vec![Some(s("v")), None])); + + let (result, sent) = round_trip( + &mut server, + RespValue::Integer(6), + commands::hincrby(&conn, key(), s("n"), 5), + ) + .await?; + check_eq!(sent, words(&["HINCRBY", "hash", "n", "5"])); + check_eq!(result, Ok(6)); + + // a double sent as a bulk string, even with RESP3 + let (result, sent) = round_trip( + &mut server, + bulk("2.5"), + commands::hincrbyfloat(&conn, key(), s("n"), 1.5), + ) + .await?; + check_eq!(sent, words(&["HINCRBYFLOAT", "hash", "n", "1.5"])); + check_eq!(result, Ok(2.5)); + + let (result, sent) = round_trip( + &mut server, + array(vec![RespValue::Integer(1), RespValue::Integer(-2)]), + commands::hexpire( + &conn, + key(), + DurationSeconds { ex: 60 }, + None, + words(&["f", "x"]), + ), + ) + .await?; + check_eq!( + sent, + words(&["HEXPIRE", "hash", "60", "FIELDS", "2", "f", "x"]) + ); + check_eq!( + result, + Ok(vec![HexpireResult::Applied, HexpireResult::DoesNotExist]) + ); + + // without a count, the field itself, or null + let (result, sent) = round_trip( + &mut server, + bulk("f"), + commands::hrandfield(&conn, key(), None), + ) + .await?; + check_eq!(sent, words(&["HRANDFIELD", "hash"])); + check_eq!(result, Ok(words(&["f"]))); + + // RESP3 replies with values as [field, value] pairs + let (result, sent) = round_trip( + &mut server, + array(vec![ + array(vec![bulk("f"), bulk("v")]), + array(vec![bulk("g"), bulk("w")]), + ]), + commands::hrandfield_withvalues(&conn, key(), 2), + ) + .await?; + check_eq!(sent, words(&["HRANDFIELD", "hash", "2", "WITHVALUES"])); + check_eq!(result, Ok(vec![(s("f"), s("v")), (s("g"), s("w"))])); + + let (result, sent) = round_trip( + &mut server, + array(vec![bulk("17"), array(vec![bulk("f"), bulk("v")])]), + commands::hscan(&conn, key(), Cursor::Start, Some(s("f*")), Some(10)), + ) + .await?; + check_eq!( + sent, + words(&["HSCAN", "hash", "0", "MATCH", "f*", "COUNT", "10"]) + ); + check_eq!( + result, + Ok((Some(Cursor::Resume(s("17"))), Map::from([(s("f"), s("v"))]))) + ); + + // the last page has a cursor of 0 + let (result, sent) = round_trip( + &mut server, + array(vec![bulk("0"), array(vec![bulk("g")])]), + commands::hscan_novalues(&conn, key(), Cursor::Resume(s("17")), None, None), + ) + .await?; + check_eq!(sent, words(&["HSCAN", "hash", "17", "NOVALUES"])); + check_eq!(result, Ok((None, words(&["g"])))); + + let (result, sent) = round_trip( + &mut server, + RespValue::Integer(1), + commands::hdel(&conn, key(), words(&["f"])), + ) + .await?; + check_eq!(sent, words(&["HDEL", "hash", "f"])); + check_eq!(result, Ok(1)); + Ok(()) +} + +async fn connection_commands() -> Result<(), String> { + let (mut server, conn) = establish().await?; + + let (result, sent) = round_trip( + &mut server, + RespValue::String(s("PONG")), + commands::ping(&conn, None), + ) + .await?; + check_eq!(sent, words(&["PING"])); + check_eq!(result, Ok(())); + + let (result, sent) = round_trip( + &mut server, + ok(), + commands::auth(&conn, Some(s("user")), s("secret")), + ) + .await?; + check_eq!(sent, words(&["AUTH", "user", "secret"])); + check_eq!(result, Ok(())); + + let (result, sent) = round_trip( + &mut server, + ok(), + commands::client_setname(&conn, s("tests")), + ) + .await?; + check_eq!(sent, words(&["CLIENT", "SETNAME", "tests"])); + check_eq!(result, Ok(())); + Ok(()) +} + +async fn admin_and_pubsub_commands() -> Result<(), String> { + let (mut server, conn) = establish().await?; + + let rules = vec![ + AclRule::User(AclUserRule::On), + AclRule::User(AclUserRule::Password(s("secret"))), + ]; + let (result, sent) = round_trip( + &mut server, + ok(), + commands::acl_setuser(&conn, s("user"), Some(rules)), + ) + .await?; + check_eq!(sent, words(&["ACL", "SETUSER", "user", "on", ">secret"])); + check_eq!(result, Ok(())); + + let pass = "dd721260bfe1b3d9601e7fbab36de6d04e2e67b0ef1c53de59d45950db0dd3cc"; + let (result, sent) = + round_trip(&mut server, bulk(pass), commands::acl_genpass(&conn, None)).await?; + check_eq!(sent, words(&["ACL", "GENPASS"])); + check_eq!(result, Ok(s(pass))); + + let (result, sent) = round_trip( + &mut server, + RespValue::Integer(1), + commands::acl_deluser(&conn, words(&["user"])), + ) + .await?; + check_eq!(sent, words(&["ACL", "DELUSER", "user"])); + check_eq!(result, Ok(1)); + + let (result, sent) = round_trip( + &mut server, + RespValue::Integer(2), + commands::publish(&conn, s("news"), s("hello")), + ) + .await?; + check_eq!(sent, words(&["PUBLISH", "news", "hello"])); + check_eq!(result, Ok(2)); + Ok(()) +} + +async fn invalid_arguments() -> Result<(), String> { + let (mut server, conn) = establish().await?; + check_eq!( + commands::del(&conn, vec![]).await, + Err(Error::Client(s("keys requires at least one item"))) + ); + check_eq!( + commands::scan(&conn, Cursor::Resume(s("0")), None, None, None).await, + Err(Error::Client(s( + "resume requires the cursor a scan returned, use start to begin a scan" + ))) + ); + // nothing was sent, the next command is the first the server sees + let (_, sent) = round_trip( + &mut server, + RespValue::String(s("PONG")), + commands::ping(&conn, None), + ) + .await?; + check_eq!(sent, words(&["PING"])); + Ok(()) +} + +async fn unexpected_replies() -> Result<(), String> { + let (mut server, conn) = establish().await?; + let (result, _) = round_trip( + &mut server, + RespValue::Integer(7), + commands::get(&conn, s("key")), + ) + .await?; + check_eq!( + result, + Err(Error::Client(s("unexpected reply: RespValue::Integer(7)"))) + ); + Ok(()) +} + +async fn error_replies() -> Result<(), String> { + let (mut server, conn) = establish().await?; + let (result, _) = round_trip( + &mut server, + RespValue::Error(s( + "WRONGTYPE Operation against a key holding the wrong kind of value", + )), + commands::incr(&conn, s("key")), + ) + .await?; + check_eq!( + result, + Err(Error::Valkey(s( + "WRONGTYPE Operation against a key holding the wrong kind of value" + ))) + ); + Ok(()) +} + +/// Against a Valkey server, from the connector. +async fn with_valkey() -> Result<(), String> { + let conn = connector::connect() + .await + .map_err(|error| format!("failed to connect: {error:?}"))?; + let check = |result: Result<(), Error>| result.map_err(|error| format!("{error:?}")); + let key = || s("commands-test:string"); + + check(commands::ping(&conn, None).await)?; + check_eq!( + commands::set(&conn, key(), s("1"), None, None).await, + Ok(true) + ); + check_eq!(commands::get(&conn, key()).await, Ok(Some(s("1")))); + check_eq!(commands::incr(&conn, key()).await, Ok(2)); + check_eq!(commands::incrby(&conn, key(), 3).await, Ok(5)); + check_eq!(commands::exists(&conn, vec![key()]).await, Ok(1)); + check_eq!( + commands::expire(&conn, key(), DurationSeconds { ex: 60 }, None).await, + Ok(true) + ); + check_eq!( + commands::keys(&conn, s("commands-test:str*")).await, + Ok(vec![key()]) + ); + check_eq!(commands::del(&conn, vec![key()]).await, Ok(1)); + check_eq!(commands::get(&conn, key()).await, Ok(None)); + + let key = || s("commands-test:hash"); + check_eq!( + commands::hset(&conn, key(), vec![(s("f"), s("v")), (s("g"), s("w"))]).await, + Ok(2) + ); + check_eq!( + commands::hgetall(&conn, key()).await, + Ok(Map::from([(s("f"), s("v")), (s("g"), s("w"))])) + ); + check_eq!(commands::hlen(&conn, key()).await, Ok(2)); + check_eq!( + commands::hmget(&conn, key(), words(&["f", "x"])).await, + Ok(vec![Some(s("v")), None]) + ); + let fields = commands::hrandfield_withvalues(&conn, key(), 2) + .await + .map_err(|error| format!("{error:?}"))?; + check_eq!(fields.len(), 2); + check_eq!( + commands::hscan(&conn, key(), Cursor::Start, None, None).await, + Ok((None, Map::from([(s("f"), s("v")), (s("g"), s("w"))]))) + ); + check_eq!( + commands::hincrbyfloat(&conn, key(), s("n"), 1.5).await, + Ok(1.5) + ); + check_eq!( + commands::hexpire( + &conn, + key(), + DurationSeconds { ex: 60 }, + None, + words(&["f", "x"]) + ) + .await, + Ok(vec![HexpireResult::Applied, HexpireResult::DoesNotExist]) + ); + check_eq!(commands::del(&conn, vec![key()]).await, Ok(1)); + + check_eq!( + commands::publish(&conn, s("commands-test"), s("hello")).await, + Ok(0) + ); + let pass = commands::acl_genpass(&conn, None) + .await + .map_err(|error| format!("{error:?}"))?; + check_eq!(pass.len(), 64); + Ok(()) +} + +wit_bindgen::generate!({ + // `path` is relative to the manifest, `components/dep-test-commands/` + path: "../wit", + world: "test-commands", + // the resp types shared by every component + with: { + "componentized:valkey/types@0.3.0-dev": componentized_valkey::types, + }, + generate_all, +}); + +export!(Suite); diff --git a/components/tcp-connector/Cargo.toml b/components/tcp-connector/Cargo.toml index 3f1e4e0..8bc4138 100644 --- a/components/tcp-connector/Cargo.toml +++ b/components/tcp-connector/Cargo.toml @@ -10,3 +10,8 @@ crate-type = ["cdylib"] [dependencies] componentized-valkey = { workspace = true } wit-bindgen = { workspace = true } + +[dev-dependencies] +test-harness = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } diff --git a/components/tcp-connector/tests/connector.rs b/components/tcp-connector/tests/connector.rs new file mode 100644 index 0000000..5e84753 --- /dev/null +++ b/components/tcp-connector/tests/connector.rs @@ -0,0 +1,115 @@ +//! Tests for the `tcp-connector` component, composed with `tcp` and `connection` as `test-tcp`, +//! configured with `wasi:config`, against a server played by the test over TCP. + +use test_harness::{Client, Error, Harness, Server, command, hello_reply, ok, setinfo}; + +/// Answer the handshake of a connection, expecting `hello`. +async fn handshake(client: &mut Client, hello: &[&str]) { + assert_eq!(client.request().await, Some(command(hello))); + for setinfo in setinfo() { + assert_eq!(client.request().await, Some(setinfo)); + } + client.reply(hello_reply()).await; + for _ in setinfo() { + client.reply(ok()).await; + } +} + +/// Connect with the configuration, answering the handshake with `hello` expected. +async fn connects_with(config: &[(&str, &str)], hello: &[&str]) -> wasmtime::Result<()> { + let server = Server::bind().await?; + let port = server.port().to_string(); + let mut config = config.to_vec(); + config.push(("port", &port)); + let mut harness = Harness::with_config("test-tcp", &config).await?; + harness + .run(async |accessor, exports| { + let connect = exports.connector().call_connect(accessor); + let serve = async { + let mut client = server.accept().await.expect("accept"); + handshake(&mut client, hello).await; + client + }; + let (connection, _client) = tokio::join!(connect, serve); + assert!(connection?.is_ok()); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn connects_to_the_configured_server() -> wasmtime::Result<()> { + connects_with(&[("hostname", "localhost")], &["HELLO", "3"]).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn authenticates_and_names_the_connection() -> wasmtime::Result<()> { + let config = [ + ("username", "user"), + ("password", "secret"), + ("client-name", "tests"), + ]; + let hello = ["HELLO", "3", "AUTH", "user", "secret", "SETNAME", "tests"]; + connects_with(&config, &hello).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_password_alone_is_for_the_default_user() -> wasmtime::Result<()> { + let hello = ["HELLO", "3", "AUTH", "default", "secret"]; + connects_with(&[("password", "secret")], &hello).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn keep_alive_pings_an_idle_connection() -> wasmtime::Result<()> { + let server = Server::bind().await?; + let port = server.port().to_string(); + let config = [("port", port.as_str()), ("keep-alive-interval-ms", "20")]; + let mut harness = Harness::with_config("test-tcp", &config).await?; + harness + .run(async |accessor, exports| { + let connect = exports.connector().call_connect(accessor); + let serve = async { + let mut client = server.accept().await.expect("accept"); + handshake(&mut client, &["HELLO", "3"]).await; + client + }; + let (connection, mut client) = tokio::join!(connect, serve); + assert!(connection?.is_ok()); + assert_eq!(client.request().await, Some(command(&["PING"]))); + Ok(()) + }) + .await +} + +/// The error connecting with an invalid configuration, nothing is sent to a server. +async fn fails_with(config: &[(&str, &str)]) -> wasmtime::Result { + let mut harness = Harness::with_config("test-tcp", config).await?; + harness + .run(async |accessor, exports| { + let result = exports.connector().call_connect(accessor).await?; + Ok(result.err().expect("an error")) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn invalid_configuration_fails_to_connect() -> wasmtime::Result<()> { + assert_eq!( + fails_with(&[("port", "http")]).await?, + Error::Client("port must be a port number: http".to_string()) + ); + assert_eq!( + fails_with(&[("keep-alive-interval-ms", "soon")]).await?, + Error::Client("keep-alive-interval-ms must be a number of milliseconds: soon".to_string()) + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn unreachable_servers_fail_to_connect() -> wasmtime::Result<()> { + // a port nothing listens on + let port = Server::bind().await?.port().to_string(); + let error = fails_with(&[("port", &port)]).await?; + assert!(matches!(error, Error::Closed(Some(_))), "{error:?}"); + Ok(()) +} diff --git a/components/tcp/tests/tcp.rs b/components/tcp/tests/tcp.rs index a30c3e0..efc8ef5 100644 --- a/components/tcp/tests/tcp.rs +++ b/components/tcp/tests/tcp.rs @@ -1,12 +1,25 @@ -//! Tests for the `tcp` component, composed with `connection` and `resp` as `test-tcp`, against a -//! server played by the test over TCP. +//! Tests for the `tcp` component, composed with `connection` as `test-tcp`, against a server +//! played by the test over TCP. use test_harness::{ - Client, Ctx, Error, Exports, Harness, Server, Value, bulk, command, hello_reply, resolve, words, + Client, Ctx, Error, Exports, Harness, RespValue, Server, bulk, command, hello_reply, ok, + resolve, setinfo, words, }; use wasmtime::component::{Accessor, ResourceAny}; -/// Connect to the server and answer HELLO, returning the server's side and the connection. +/// Answer the handshake of a connection: HELLO, then the CLIENT SETINFO commands. +async fn handshake(client: &mut Client) { + assert_eq!(client.request().await, Some(command(&["HELLO", "3"]))); + for setinfo in setinfo() { + assert_eq!(client.request().await, Some(setinfo)); + } + client.reply(hello_reply()).await; + for _ in setinfo() { + client.reply(ok()).await; + } +} + +/// Connect to the server and answer the handshake, returning the server's side and the connection. async fn establish( accessor: &Accessor, exports: &Exports, @@ -18,8 +31,7 @@ async fn establish( .call_connect(accessor, host.to_string(), server.port(), None); let serve = async { let mut client = server.accept().await.expect("accept"); - assert_eq!(client.request().await, Some(command(&["HELLO", "3"]))); - client.reply(hello_reply()).await; + handshake(&mut client).await; client }; let (connection, client) = tokio::join!(connect, serve); @@ -39,10 +51,10 @@ async fn connects_and_exchanges() -> wasmtime::Result<()> { let call = guest.call_exchange(accessor, connection, words(&["PING"])); let serve = async { assert_eq!(client.request().await, Some(command(&["PING"]))); - client.reply(Value::String("PONG".to_string())).await; + client.reply(RespValue::String("PONG".to_string())).await; }; let (result, ()) = tokio::join!(call, serve); - assert_eq!(result?, Ok(Value::String("PONG".to_string()))); + assert_eq!(result?, Ok(RespValue::String("PONG".to_string()))); Ok(()) }) .await @@ -183,10 +195,7 @@ async fn transports_wire_to_connections() -> wasmtime::Result<()> { .await?; drop(sent); - let serve = async { - assert_eq!(client.request().await, Some(command(&["HELLO", "3"]))); - client.reply(hello_reply()).await; - }; + let serve = handshake(&mut client); let (connection, ()) = tokio::join!(resolve(accessor, connection), serve); assert!(connection?.is_ok()); @@ -217,26 +226,26 @@ async fn dropping_the_connection_closes_the_socket() -> wasmtime::Result<()> { .await } -/// The tests against a Valkey server on localhost:6379, ignored unless run with -/// `make test-valkey`, which only runs the ignored tests in `with_valkey` modules. +/// The tests against a Valkey server, ignored unless run with `make test-valkey`, which runs one in +/// a container and only runs the ignored tests in `with_valkey` modules. mod with_valkey { use super::*; - /// Against a Valkey server on localhost:6379, run with `cargo test -p tcp -- --ignored with_valkey::`. #[tokio::test(flavor = "multi_thread")] - #[ignore = "needs a Valkey server on localhost:6379"] + #[ignore = "needs a Valkey server, see make test-valkey"] async fn against_valkey() -> wasmtime::Result<()> { + let (host, port) = test_harness::valkey_server(); let mut harness = Harness::new("test-tcp").await?; harness .run(async |accessor, exports| { let connection = exports .tcp() - .call_connect(accessor, "localhost".to_string(), 6379, None) + .call_connect(accessor, host.clone(), port, None) .await? .map_err(|error| wasmtime::format_err!("{error:?}"))?; let guest = exports.connection(); let properties = guest.call_server_properties(accessor, connection).await?; - assert!(properties.contains(&("proto".to_string(), Value::Integer(3)))); + assert!(properties.contains(&("proto".to_string(), RespValue::Integer(3)))); let value: String = (0..100_000) .map(|n| char::from(b'a' + (n % 26) as u8)) @@ -244,7 +253,7 @@ mod with_valkey { let set = guest .call_exchange(accessor, connection, words(&["SET", "tcp-test", &value])) .await?; - assert_eq!(set, Ok(Value::String("OK".to_string()))); + assert_eq!(set, Ok(RespValue::String("OK".to_string()))); let get = guest .call_exchange(accessor, connection, words(&["GET", "tcp-test"])) .await?; @@ -252,7 +261,7 @@ mod with_valkey { let del = guest .call_exchange(accessor, connection, words(&["DEL", "tcp-test"])) .await?; - assert_eq!(del, Ok(Value::Integer(1))); + assert_eq!(del, Ok(RespValue::Integer(1))); Ok(()) }) .await diff --git a/components/test-commands/README.md b/components/test-commands/README.md new file mode 100644 index 0000000..827bf88 --- /dev/null +++ b/components/test-commands/README.md @@ -0,0 +1,3 @@ +# test-commands + +[`dep-test-commands`](../dep-test-commands/) composed with [`commands`](../commands/), [`connection`](../connection/), and a [`tcp-connector`](../tcp-connector/) for the tests against a Valkey server, to test `commands`. diff --git a/components/test-commands/test-commands.wac b/components/test-commands/test-commands.wac new file mode 100644 index 0000000..c74e7cf --- /dev/null +++ b/components/test-commands/test-commands.wac @@ -0,0 +1,26 @@ +package componentized:valkey; + +let connection = new local:connection { + ... +}; +let commands = new local:commands { + "componentized:valkey/connection@0.3.0-dev": connection.connection, + ... +}; +let tcp = new local:tcp { + "componentized:valkey/connection@0.3.0-dev": connection.connection, + ... +}; +let connector = new local:tcp-connector { + "componentized:valkey/connection@0.3.0-dev": connection.connection, + "componentized:valkey/tcp@0.3.0-dev": tcp.tcp, + ... +}; +let tests = new local:dep-test-commands { + "componentized:valkey/connection@0.3.0-dev": connection.connection, + "componentized:valkey/connector@0.3.0-dev": connector.connector, + "componentized:valkey/commands@0.3.0-dev": commands.commands, + ... +}; + +export tests...; diff --git a/components/test-tcp/README.md b/components/test-tcp/README.md index fed2bc4..339d956 100644 --- a/components/test-tcp/README.md +++ b/components/test-tcp/README.md @@ -1,3 +1,3 @@ # test-tcp -`tcp` composed with `connection` and `resp`, to test `tcp` against a server. +[`tcp`](../tcp/) composed with [`connection`](../connection/) and a [`tcp-connector`](../tcp-connector/), to test `tcp` and `tcp-connector` against a server played by the test. diff --git a/components/test-tcp/test-tcp.wac b/components/test-tcp/test-tcp.wac index c2b4cbc..f071861 100644 --- a/components/test-tcp/test-tcp.wac +++ b/components/test-tcp/test-tcp.wac @@ -7,6 +7,12 @@ let tcp = new local:tcp { "componentized:valkey/connection@0.3.0-dev": connection.connection, ... }; +let connector = new local:tcp-connector { + "componentized:valkey/connection@0.3.0-dev": connection.connection, + "componentized:valkey/tcp@0.3.0-dev": tcp.tcp, + ... +}; export connection.connection; export tcp.tcp; +export connector.connector; diff --git a/components/wit/worlds.wit b/components/wit/worlds.wit index 9e800c8..d16fb31 100644 --- a/components/wit/worlds.wit +++ b/components/wit/worlds.wit @@ -48,3 +48,21 @@ world cli { import componentized:valkey/connector@0.3.0-dev; export wasi:cli/run@0.3.0; } + +/// A suite of tests run inside a component, by the test harness, `crates/test-harness`. +interface test-suite { + /// The names of the tests. Those that need a Valkey server on localhost:6379 start with + /// `with-valkey `. + tests: func() -> list; + + /// Runs the named test, failing with why. + run: async func(name: string) -> result<_, string>; +} + +/// The tests of the commands, playing the server over the streams of a connection. +world test-commands { + import componentized:valkey/connection@0.3.0-dev; + import componentized:valkey/connector@0.3.0-dev; + import componentized:valkey/commands@0.3.0-dev; + export test-suite; +} diff --git a/crates/test-harness/Cargo.toml b/crates/test-harness/Cargo.toml index 0b7fa63..952e449 100644 --- a/crates/test-harness/Cargo.toml +++ b/crates/test-harness/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "test-harness" -version = "0.1.0" +version.workspace = true edition = "2024" license = "Apache-2.0" publish = false diff --git a/crates/test-harness/src/lib.rs b/crates/test-harness/src/lib.rs index 15d937a..44c0087 100644 --- a/crates/test-harness/src/lib.rs +++ b/crates/test-harness/src/lib.rs @@ -23,8 +23,9 @@ use wasmtime::error::Context as _; use wasmtime::{Config, Engine, Result, Store, StoreContextMut, bail, format_err}; use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; -pub use bindings::componentized::valkey::resp::{RespNestedValue, RespValue}; -pub use bindings::exports::componentized::valkey::connection::{ConnectOpts, Error}; +pub use bindings::componentized::valkey::types::{ + self, ConnectOpts, Error, RespNestedValue, RespValue, +}; mod server; @@ -37,16 +38,12 @@ pub mod bindings { package componentized:test-harness; world harness { - import componentized:valkey/resp@0.3.0-dev; + import componentized:valkey/types@0.3.0-dev; import wasi:config/store@0.2.0-rc.1; export componentized:valkey/connection@0.3.0-dev; export componentized:valkey/tcp@0.3.0-dev; - export componentized:valkey/admin-commands@0.3.0-dev; - export componentized:valkey/connection-commands@0.3.0-dev; - export componentized:valkey/hash-commands@0.3.0-dev; - export componentized:valkey/keyspace-commands@0.3.0-dev; - export componentized:valkey/pubsub-commands@0.3.0-dev; - export componentized:valkey/string-commands@0.3.0-dev; + export componentized:valkey/connector@0.3.0-dev; + export componentized:valkey-components/test-suite; export wasmcloud:keyvalue/types@0.2.0; export wasmcloud:keyvalue/store@0.2.0; export wasmcloud:keyvalue/atomics@0.2.0; @@ -59,25 +56,6 @@ pub mod bindings { }); } -/// Bindings for the `resp` component, which exports the interface the other components import. -pub mod resp_bindings { - wasmtime::component::bindgen!({ - path: "../../components/wit", - inline: " - package componentized:test-harness-resp; - - world resp { - export componentized:valkey/resp@0.3.0-dev; - } - ", - world: "componentized:test-harness-resp/resp", - exports: { default: async | store }, - additional_derives: [PartialEq], - }); -} - -pub type Resp = resp_bindings::exports::componentized::valkey::resp::Guest; - /// Encode a value as RESP, e.g. to nest it in an aggregate. pub fn encode(value: RespValue) -> RespNestedValue { componentized_valkey::resp::encode(to_codec(value)) @@ -130,11 +108,12 @@ fn from_codec(value: componentized_valkey::types::RespValue) -> RespValue { } pub type Connection<'a> = bindings::exports::componentized::valkey::connection::GuestConnection<'a>; +pub type Subscription<'a> = + bindings::exports::componentized::valkey::connection::GuestSubscription<'a>; pub type Tcp = bindings::exports::componentized::valkey::tcp::Guest; -pub use bindings::exports::componentized::valkey::{ - admin_commands, connection_commands, hash_commands, keyspace_commands, pubsub_commands, - string_commands, -}; +pub type Connector = bindings::exports::componentized::valkey::connector::Guest; +/// A suite of tests run inside a component, e.g. those of the commands, see [`run_suite`]. +pub type TestSuite = bindings::exports::componentized::valkey_components::test_suite::Guest; pub use bindings::exports::wasmcloud::keyvalue; /// A bulk string, the type commands are sent as. @@ -167,6 +146,11 @@ pub fn push(items: Vec) -> RespValue { RespValue::Push(items.into_iter().map(encode).collect()) } +/// A simple string `OK`. +pub fn ok() -> RespValue { + RespValue::String("OK".to_string()) +} + /// The server's properties in the canned reply to HELLO. pub fn hello_reply() -> RespValue { map(vec![ @@ -246,16 +230,35 @@ pub async fn connect( )) } -/// Start a connection and answer its HELLO with [`hello_reply`], returning the transport and the +/// The commands the connection sends when it is established, after HELLO, to report the library. +pub fn setinfo() -> [Vec; 2] { + [ + command(&["CLIENT", "SETINFO", "LIB-NAME", "componentized:valkey"]), + command(&["CLIENT", "SETINFO", "LIB-VER", env!("CARGO_PKG_VERSION")]), + ] +} + +/// Answer the handshake of a connection: HELLO with [`hello_reply`], then the CLIENT SETINFO +/// commands with OK. +pub async fn handshake(transport: &mut Transport, hello: &[&str]) { + assert_eq!(transport.request().await, Some(command(hello))); + for setinfo in setinfo() { + assert_eq!(transport.request().await, Some(setinfo)); + } + transport.reply(hello_reply()); + for _ in setinfo() { + transport.reply(ok()); + } +} + +/// Start a connection and answer its handshake, see [`handshake`], returning the transport and the /// established connection. pub async fn establish( accessor: &Accessor, exports: &Exports, ) -> Result<(Transport, ResourceAny)> { let (mut transport, connection) = connect(accessor, exports, None).await?; - let hello = transport.request().await.expect("HELLO"); - assert_eq!(hello, command(&["HELLO", "3"])); - transport.reply(hello_reply()); + handshake(&mut transport, &["HELLO", "3"]).await; let connection = resolve(accessor, connection) .await? .map_err(|error| format_err!("failed to establish the connection: {error:?}"))?; @@ -427,32 +430,16 @@ impl WasiView for Ctx { } } -// the components only import the types of `resp`, the functions are here for completeness -impl bindings::componentized::valkey::resp::Host for Ctx { - fn decode(&mut self, data: Vec) -> Result { - decode(data) - } - - fn encode(&mut self, value: RespValue) -> Result, String> { - Ok(encode(value)) - } -} - /// The interfaces a component exports. pub struct Exports { connection: Option, - tcp: Option, + tcp: Option, + connector: Option, + test_suite: Option, keyvalue_store: Option, keyvalue_types: Option, keyvalue_atomics: Option, keyvalue_batch: Option, - admin_commands: Option, - connection_commands: Option, - hash_commands: Option, - keyspace_commands: Option, - pubsub_commands: Option, - string_commands: Option, - resp: Option, } impl Exports { @@ -480,40 +467,16 @@ impl Exports { .expect("component exports wasmcloud:keyvalue/batch") } - pub fn admin_commands(&self) -> &admin_commands::Guest { - self.admin_commands - .as_ref() - .expect("component exports componentized:valkey/admin-commands") - } - - pub fn connection_commands(&self) -> &connection_commands::Guest { - self.connection_commands - .as_ref() - .expect("component exports componentized:valkey/connection-commands") - } - - pub fn hash_commands(&self) -> &hash_commands::Guest { - self.hash_commands + pub fn test_suite(&self) -> &TestSuite { + self.test_suite .as_ref() - .expect("component exports componentized:valkey/hash-commands") + .expect("component exports componentized:valkey-components/test-suite") } - pub fn keyspace_commands(&self) -> &keyspace_commands::Guest { - self.keyspace_commands + pub fn connector(&self) -> &Connector { + self.connector .as_ref() - .expect("component exports componentized:valkey/keyspace-commands") - } - - pub fn pubsub_commands(&self) -> &pubsub_commands::Guest { - self.pubsub_commands - .as_ref() - .expect("component exports componentized:valkey/pubsub-commands") - } - - pub fn string_commands(&self) -> &string_commands::Guest { - self.string_commands - .as_ref() - .expect("component exports componentized:valkey/string-commands") + .expect("component exports componentized:valkey/connector") } pub fn tcp(&self) -> &Tcp { @@ -521,11 +484,6 @@ impl Exports { .as_ref() .expect("component exports componentized:valkey/tcp") } - pub fn resp(&self) -> &Resp { - self.resp - .as_ref() - .expect("component exports componentized:valkey/resp") - } pub fn connection(&self) -> Connection<'_> { self.connection @@ -533,6 +491,13 @@ impl Exports { .expect("component exports componentized:valkey/connection") .connection() } + + pub fn subscription(&self) -> Subscription<'_> { + self.connection + .as_ref() + .expect("component exports componentized:valkey/connection") + .subscription() + } } /// An instantiated component, see [`Harness::run`]. @@ -559,10 +524,6 @@ impl Harness { let mut linker = Linker::new(&engine); wasmtime_wasi::p3::add_to_linker(&mut linker)?; - bindings::componentized::valkey::resp::add_to_linker::<_, HasSelf>( - &mut linker, - |ctx| ctx, - )?; bindings::wasi::config::store::add_to_linker::<_, HasSelf>(&mut linker, |ctx| ctx)?; let mut store = Store::new( @@ -596,6 +557,17 @@ impl Harness { tcp: bindings::exports::componentized::valkey::tcp::GuestIndices::new(&instance_pre) .ok() .and_then(|indices| indices.load(&mut store, &instance).ok()), + connector: bindings::exports::componentized::valkey::connector::GuestIndices::new( + &instance_pre, + ) + .ok() + .and_then(|indices| indices.load(&mut store, &instance).ok()), + test_suite: + bindings::exports::componentized::valkey_components::test_suite::GuestIndices::new( + &instance_pre, + ) + .ok() + .and_then(|indices| indices.load(&mut store, &instance).ok()), keyvalue_store: keyvalue::store::GuestIndices::new(&instance_pre) .ok() .and_then(|indices| indices.load(&mut store, &instance).ok()), @@ -608,33 +580,20 @@ impl Harness { keyvalue_batch: keyvalue::batch::GuestIndices::new(&instance_pre) .ok() .and_then(|indices| indices.load(&mut store, &instance).ok()), - admin_commands: admin_commands::GuestIndices::new(&instance_pre) - .ok() - .and_then(|indices| indices.load(&mut store, &instance).ok()), - connection_commands: connection_commands::GuestIndices::new(&instance_pre) - .ok() - .and_then(|indices| indices.load(&mut store, &instance).ok()), - hash_commands: hash_commands::GuestIndices::new(&instance_pre) - .ok() - .and_then(|indices| indices.load(&mut store, &instance).ok()), - keyspace_commands: keyspace_commands::GuestIndices::new(&instance_pre) - .ok() - .and_then(|indices| indices.load(&mut store, &instance).ok()), - pubsub_commands: pubsub_commands::GuestIndices::new(&instance_pre) - .ok() - .and_then(|indices| indices.load(&mut store, &instance).ok()), - string_commands: string_commands::GuestIndices::new(&instance_pre) - .ok() - .and_then(|indices| indices.load(&mut store, &instance).ok()), - resp: resp_bindings::exports::componentized::valkey::resp::GuestIndices::new( - &instance_pre, - ) - .ok() - .and_then(|indices| indices.load(&mut store, &instance).ok()), }; Ok(Self { store, exports }) } + /// Build and instantiate the named component, its connector configured for the Valkey server + /// of the `with_valkey` tests, see [`valkey_server`], with the values of `wasi:config`. + pub async fn with_valkey(name: &str, config_values: &[(&str, &str)]) -> Result { + let (host, port) = valkey_server(); + let port = port.to_string(); + let mut values = vec![("hostname", host.as_str()), ("port", port.as_str())]; + values.extend_from_slice(config_values); + Self::with_config(name, &values).await + } + /// Drop a resource the component exports, running its destructor. /// /// Between runs, the store is only free to drop a resource while no run is in progress. @@ -653,3 +612,45 @@ impl Harness { .await? } } + +/// The Valkey server of the `with_valkey` tests, from `VALKEY_HOST` and `VALKEY_PORT`, by default +/// 127.0.0.1:6379. `make test-valkey` runs one in a container, and sets them. +pub fn valkey_server() -> (String, u16) { + let host = std::env::var("VALKEY_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); + let port = std::env::var("VALKEY_PORT") + .ok() + .and_then(|port| port.parse().ok()) + .unwrap_or(6379); + (host, port) +} + +/// Run the tests of the suite the component exports that `filter` selects, each in turn, failing +/// with every test that failed. +pub async fn run_suite( + mut harness: Harness, + filter: impl Fn(&str) -> bool + Send + Sync, +) -> Result<()> { + let failures = harness + .run(async |accessor, exports| { + let suite = exports.test_suite(); + let mut failures = vec![]; + for test in suite.call_tests(accessor).await? { + if !filter(&test) { + continue; + } + match suite.call_run(accessor, test.clone()).await? { + Ok(()) => println!("ok - {test}"), + Err(why) => { + println!("FAILED - {test}: {why}"); + failures.push(format!("{test}: {why}")); + } + } + } + Ok(failures) + }) + .await?; + if !failures.is_empty() { + bail!("{} failed:\n{}", failures.len(), failures.join("\n")); + } + Ok(()) +} From a59568bf89afa439d2e4e971b2ea96b9676ff2fe Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 14 Oct 2026 01:11:39 -0400 Subject: [PATCH 28/37] Create connection-minimal Skip non-essential features, like pub/sub. Signed-off-by: Scott Andrews --- Cargo.lock | 10 + Cargo.toml | 1 + components/connection-minimal/Cargo.toml | 20 + components/connection-minimal/README.md | 5 + components/connection/Cargo.toml | 5 + components/connection/README.md | 4 +- components/connection/src/lib.rs | 888 +----------------- components/connection/src/pubsub.rs | 897 +++++++++++++++++++ components/connection/src/pubsub_disabled.rs | 74 ++ components/connection/tests/connection.rs | 101 ++- wit/connection.wit | 6 +- 11 files changed, 1132 insertions(+), 879 deletions(-) create mode 100644 components/connection-minimal/Cargo.toml create mode 100644 components/connection-minimal/README.md create mode 100644 components/connection/src/pubsub.rs create mode 100644 components/connection/src/pubsub_disabled.rs diff --git a/Cargo.lock b/Cargo.lock index f1a214c..55524e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,6 +179,16 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "connection-minimal" +version = "0.3.0-dev" +dependencies = [ + "componentized-valkey", + "futures-channel", + "futures-core", + "wit-bindgen", +] + [[package]] name = "core_detect" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index a1011fa..367f828 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "components/as-keyvalue", "components/commands", "components/connection", + "components/connection-minimal", "components/dep-cli", "components/dep-test-commands", "components/sample-http-incrementor", diff --git a/components/connection-minimal/Cargo.toml b/components/connection-minimal/Cargo.toml new file mode 100644 index 0000000..4746e90 --- /dev/null +++ b/components/connection-minimal/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "connection-minimal" +version.workspace = true +edition = "2024" +license = "Apache-2.0" + +# the source of `connection`, built without its default features, e.g. pub/sub +[lib] +crate-type = ["cdylib"] +path = "../connection/src/lib.rs" + +[features] +# off, see `connection` +pubsub = [] + +[dependencies] +futures-channel = { workspace = true } +futures-core = { workspace = true } +componentized-valkey = { workspace = true } +wit-bindgen = { workspace = true, features = ["async-spawn", "inter-task-wakeup"] } diff --git a/components/connection-minimal/README.md b/components/connection-minimal/README.md new file mode 100644 index 0000000..cb16a18 --- /dev/null +++ b/components/connection-minimal/README.md @@ -0,0 +1,5 @@ +# connection-minimal + +A connection component built without pub/sub, for a smaller component when subscriptions are not needed. + +The interface is the same: `connection.subscription` fails with a client error, every push message is a dead letter, and SUBSCRIBE and the like may still be sent with `exchange`. diff --git a/components/connection/Cargo.toml b/components/connection/Cargo.toml index f7783b2..1e1c550 100644 --- a/components/connection/Cargo.toml +++ b/components/connection/Cargo.toml @@ -7,6 +7,11 @@ license = "Apache-2.0" [lib] crate-type = ["cdylib"] +[features] +default = ["pubsub"] +# subscriptions, see `connection.subscription`, without it every push message is a dead letter +pubsub = [] + [dependencies] futures-channel = { workspace = true } futures-core = { workspace = true } diff --git a/components/connection/README.md b/components/connection/README.md index 781d1a3..0554365 100644 --- a/components/connection/README.md +++ b/components/connection/README.md @@ -2,4 +2,6 @@ The mechanics of a connection to a Valkey server, independent of the transport carrying it. -Pairs each command with its reply, keeps push messages apart for any number of readers, runs transactions with `WATCH`, `MULTI` and `EXEC`, establishes the connection with `HELLO 3` and optionally keeps it alive with `PING`. A transport, e.g. [`tcp`](../tcp/), carries the RESP values to and from the server. +Pairs each command with its reply, keeps push messages apart for any number of readers, owns pub/sub subscriptions, runs transactions with `WATCH`, `MULTI` and `EXEC`, establishes the connection with `HELLO 3`, resets it, and optionally keeps it alive with `PING`. A transport, e.g. [`tcp`](../tcp/), carries the RESP values to and from the server. + +[`connection-minimal`](../connection-minimal/) is a smaller build without pub/sub, with the same interface. diff --git a/components/connection/src/lib.rs b/components/connection/src/lib.rs index 95defc9..ce41073 100644 --- a/components/connection/src/lib.rs +++ b/components/connection/src/lib.rs @@ -16,19 +16,21 @@ // the test harness provides a main #![cfg_attr(not(test), no_main)] +#[cfg(feature = "pubsub")] +mod pubsub; +#[cfg(not(feature = "pubsub"))] +#[path = "pubsub_disabled.rs"] +mod pubsub; + use std::cell::RefCell; -use std::collections::{HashMap, VecDeque}; +use std::collections::VecDeque; use std::future::{Future, poll_fn}; use std::pin::Pin; use std::rc::{Rc, Weak}; -use componentized_valkey::types::{ - RespNestedValue, RespValue, SubscriptionEvent, SubscriptionMessage, SubscriptionTarget, - UnsubscribeReason, -}; +use componentized_valkey::types::{RespNestedValue, RespValue}; use exports::componentized::valkey::connection::{ - ConnectOpts, Connection, DurationMilliseconds, Error, Guest, GuestConnection, - GuestSubscription, Subscription, + ConnectOpts, Connection, DurationMilliseconds, Error, Guest, GuestConnection, Subscription, }; use futures_channel::{mpsc, oneshot}; use futures_core::Stream; @@ -48,7 +50,7 @@ struct Component; impl Guest for Component { type Connection = ConnectionImpl; - type Subscription = SubscriptionImpl; + type Subscription = pubsub::SubscriptionImpl; } /// Where the reply to a command goes. @@ -57,15 +59,9 @@ enum Reply { Call(oneshot::Sender>), /// Nowhere, the command was sent without waiting for its reply. Discard, - /// A subscription's SUBSCRIBE, confirmed by a push message for each channel rather than a - /// reply. - Subscribe { - kind: Kind, - /// The channels not confirmed yet. - channels: Vec, - id: u64, - done: oneshot::Sender>, - }, + /// A subscription's SUBSCRIBE, UNSUBSCRIBE or the like, see `pubsub::Pending`. + #[cfg_attr(not(feature = "pubsub"), allow(dead_code))] + Pubsub(pubsub::Pending), /// RESET, whose reply ends the subscriptions the server dropped. Reset(oneshot::Sender>), /// The HELLO after RESET, the writer waits for its reply before sending another command, and @@ -74,14 +70,6 @@ enum Reply { proceed: oneshot::Sender, done: oneshot::Sender>, }, - /// A subscription's UNSUBSCRIBE, confirmed by a push message for each channel rather than a - /// reply. - Unsubscribe { - kind: Kind, - /// The channels not confirmed yet. - channels: Vec, - done: oneshot::Sender>, - }, } /// A command for the writer, with a signal for once it is written. @@ -117,7 +105,7 @@ struct State { /// The readers of dead letters. dead_letters: Vec>, /// The subscriptions, and the channels they hold. - subscriptions: Subscriptions, + subscriptions: pubsub::Subscriptions, /// The server's properties from the reply to HELLO. server_properties: Vec<(String, RespValue)>, /// The HELLO the connection was established with, repeated after RESET. @@ -206,17 +194,8 @@ impl GuestConnection for ConnectionImpl { transaction(&self.0, commands).await } - fn subscription(&self) -> Subscription { - let id = { - let mut state = self.0.state.borrow_mut(); - state.owns_subscriptions = true; - let closed = state.closed.clone(); - state.subscriptions.create(closed) - }; - Subscription::new(SubscriptionImpl(Handle { - inner: self.0.clone(), - id, - })) + fn subscription(&self) -> Result { + pubsub::subscription(&self.0) } async fn reset(&self) -> Result<(), Error> { @@ -256,672 +235,6 @@ impl GuestConnection for ConnectionImpl { } } -pub struct SubscriptionImpl(Handle); - -impl GuestSubscription for SubscriptionImpl { - fn targets(&self) -> Vec { - let state = self.0.inner.state.borrow(); - let Some(sub) = state.subscriptions.subs.get(&self.0.id) else { - return vec![]; - }; - sub.targets - .iter() - .filter(|(_, hold)| *hold == Hold::Active) - .map(|((kind, name), _)| kind.target(name.clone())) - .collect() - } - - async fn subscribe(&self, targets: Vec) -> Result<(), Error> { - let groups = by_kind(targets)?; - subscribe(&self.0.inner, self.0.id, groups).await - } - - async fn unsubscribe(&self, targets: Option>) -> Result<(), Error> { - let groups = match targets { - Some(targets) => by_kind(targets)? - .into_iter() - .map(|(kind, names)| (kind, Some(names))) - .collect(), - None => Kind::ALL.into_iter().map(|kind| (kind, None)).collect(), - }; - unsubscribe(&self.0.inner, self.0.id, groups).await - } - - fn events(&self) -> (StreamReader, FutureReader) { - let (writer, stream) = wit_stream::new(); - // the default is only written if the subscription is gone before the stream ends - let (end_writer, end) = wit_future::new(|| Err(Error::Closed(None))); - let (mut events_tx, events_rx) = mpsc::channel(PUSH_BUFFER); - let (end_tx, end_rx) = oneshot::channel(); - { - let mut state = self.0.inner.state.borrow_mut(); - if let Some(sub) = state.subscriptions.subs.get_mut(&self.0.id) { - // the backlog fits, it is no larger than the channel - for event in sub.backlog.drain(..) { - let _ = events_tx.try_send(event); - } - match &sub.closed { - Some(error) => { - let _ = end_tx.send(Err(error.clone())); - } - None => sub.readers.push(Reader { - events: events_tx, - end: end_tx, - }), - } - } - } - let forward = forward_events(events_rx, end_rx, writer, end_writer); - let _ = self - .0 - .inner - .tasks - .unbounded_send(Task::Spawn(Box::pin(forward))); - (stream, end) - } -} - -/// The kind of a subscription's target, each with its own commands and push messages. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -enum Kind { - Channel, - Pattern, - Shard, -} - -impl Kind { - const ALL: [Kind; 3] = [Kind::Channel, Kind::Pattern, Kind::Shard]; - - fn subscribe(self) -> &'static str { - match self { - Kind::Channel => "SUBSCRIBE", - Kind::Pattern => "PSUBSCRIBE", - Kind::Shard => "SSUBSCRIBE", - } - } - - fn unsubscribe(self) -> &'static str { - match self { - Kind::Channel => "UNSUBSCRIBE", - Kind::Pattern => "PUNSUBSCRIBE", - Kind::Shard => "SUNSUBSCRIBE", - } - } - - fn target(self, name: String) -> SubscriptionTarget { - match self { - Kind::Channel => SubscriptionTarget::Channel(name), - Kind::Pattern => SubscriptionTarget::Pattern(name), - Kind::Shard => SubscriptionTarget::Shardchannel(name), - } - } -} - -/// The names of the targets of each kind, in the order each kind first appears, failing unless -/// there is at least one target. -fn by_kind(targets: Vec) -> Result)>, Error> { - if targets.is_empty() { - return Err(Error::Client( - "targets requires at least one item".to_string(), - )); - } - let mut groups: Vec<(Kind, Vec)> = vec![]; - for target in targets { - let (kind, name) = match target { - SubscriptionTarget::Channel(name) => (Kind::Channel, name), - SubscriptionTarget::Pattern(name) => (Kind::Pattern, name), - SubscriptionTarget::Shardchannel(name) => (Kind::Shard, name), - }; - match groups.iter_mut().find(|(grouped, _)| *grouped == kind) { - Some((_, names)) => names.push(name), - None => groups.push((kind, vec![name])), - } - } - Ok(groups) -} - -/// A channel, pattern or shard channel, by its kind and name. -type Target = (Kind, String); - -/// How a subscription holds a target. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Hold { - /// Waiting for the server to confirm SUBSCRIBE. - Pending, - /// Confirmed, its messages arrive. - Active, - /// Unsubscribing, its messages arrive until the server confirms UNSUBSCRIBE. - Leaving, -} - -/// How a stream of events ended, `ok` once the subscription is dropped. -type Ended = Result<(), Error>; - -/// A reader of a subscription's events, and where to send how its stream ended. -struct Reader { - events: mpsc::Sender, - end: oneshot::Sender, -} - -#[derive(Default)] -struct Sub { - /// The targets, in the order they were subscribed to. - targets: Vec<(Target, Hold)>, - readers: Vec, - /// How to tell readers whose stream was dropped how the subscription ended. - orphans: Vec>, - /// The events arriving while no stream is open, for the next stream. - backlog: VecDeque, - /// The error that closed the connection, once it has. - closed: Option, -} - -impl Sub { - fn hold(&self, target: &Target) -> Option { - self.targets - .iter() - .find(|(held, _)| held == target) - .map(|(_, hold)| *hold) - } - - fn set_hold(&mut self, target: &Target, hold: Hold) { - match self.targets.iter_mut().find(|(held, _)| held == target) { - Some((_, current)) => *current = hold, - None => self.targets.push((target.clone(), hold)), - } - } - - fn release(&mut self, target: &Target) { - self.targets.retain(|(held, _)| held != target); - } - - /// Fan the event out to every reader, a reader that is full is cut off, one whose stream was - /// dropped is forgotten. - fn emit(&mut self, event: SubscriptionEvent) { - if self.readers.is_empty() { - if self.backlog.len() < PUSH_BUFFER { - self.backlog.push_back(event); - } - return; - } - for mut reader in std::mem::take(&mut self.readers) { - match reader.events.try_send(event.clone()) { - Ok(()) => self.readers.push(reader), - Err(error) if error.is_full() => { - let _ = reader.end.send(Err(Error::Lagged)); - } - Err(_) => self.orphans.push(reader.end), - } - } - } - - /// End the readers' streams. - fn end(&mut self, ended: Ended) { - for reader in self.readers.drain(..) { - let _ = reader.end.send(ended.clone()); - } - for orphan in self.orphans.drain(..) { - let _ = orphan.send(ended.clone()); - } - } -} - -fn unsubscribed(target: &Target, reason: UnsubscribeReason) -> SubscriptionEvent { - SubscriptionEvent::Unsubscribed((target.0.target(target.1.clone()), reason)) -} - -#[derive(Default)] -struct Subscriptions { - next_id: u64, - subs: HashMap, - /// The subscriptions holding each target, in any way. - holders: HashMap>, -} - -impl Subscriptions { - fn create(&mut self, closed: Option) -> u64 { - let id = self.next_id; - self.next_id += 1; - self.subs.insert( - id, - Sub { - closed, - ..Sub::default() - }, - ); - id - } - - fn hold(&mut self, id: u64, target: &Target, hold: Hold) { - let Some(sub) = self.subs.get_mut(&id) else { - return; - }; - if sub.hold(target).is_none() { - self.holders.entry(target.clone()).or_default().push(id); - } - sub.set_hold(target, hold); - } - - /// The subscription no longer holds the target. - fn release(&mut self, id: u64, target: &Target) { - if let Some(sub) = self.subs.get_mut(&id) { - sub.release(target); - } - if let Some(ids) = self.holders.get_mut(target) { - ids.retain(|holder| *holder != id); - if ids.is_empty() { - self.holders.remove(target); - } - } - } - - /// The subscriptions holding the target in one of the ways. - fn holding(&self, target: &Target, holds: &[Hold]) -> Vec { - self.holders - .get(target) - .into_iter() - .flatten() - .copied() - .filter(|id| { - self.subs - .get(id) - .and_then(|sub| sub.hold(target)) - .is_some_and(|hold| holds.contains(&hold)) - }) - .collect() - } - - fn emit(&mut self, id: u64, event: SubscriptionEvent) { - if let Some(sub) = self.subs.get_mut(&id) { - sub.emit(event); - } - } - - /// The server confirmed SUBSCRIBE for the subscription's target. - fn subscribed(&mut self, id: u64, target: &Target) { - let Some(sub) = self.subs.get_mut(&id) else { - return; - }; - match sub.hold(target) { - Some(Hold::Pending) => sub.set_hold(target, Hold::Active), - // unsubscribed from while waiting, UNSUBSCRIBE follows - Some(Hold::Leaving) => {} - _ => return, - } - sub.emit(SubscriptionEvent::Subscribed( - target.0.target(target.1.clone()), - )); - } - - /// A message for the subscriptions receiving the target's messages, false when none holds it. - fn message(&mut self, target: &Target, message: SubscriptionMessage) -> bool { - if !self.holders.contains_key(target) { - return false; - } - for id in self.holding(target, &[Hold::Active, Hold::Leaving]) { - self.emit(id, SubscriptionEvent::Message(message.clone())); - } - true - } - - /// The server confirmed UNSUBSCRIBE for the target, the subscriptions leaving it let go. - fn unsubscribe_confirmed(&mut self, target: &Target) { - for id in self.holding(target, &[Hold::Leaving]) { - self.release(id, target); - self.emit(id, unsubscribed(target, UnsubscribeReason::Requested)); - } - } - - /// The server unsubscribed from the target on its own, false when none holds it. - fn unsubscribed_by_server(&mut self, target: &Target) -> bool { - if !self.holders.contains_key(target) { - return false; - } - for id in self.holding(target, &[Hold::Active, Hold::Leaving]) { - let leaving = - self.subs.get(&id).and_then(|sub| sub.hold(target)) == Some(Hold::Leaving); - self.release(id, target); - let reason = if leaving { - UnsubscribeReason::Requested - } else { - UnsubscribeReason::ByServer - }; - self.emit(id, unsubscribed(target, reason)); - } - true - } - - /// RESET unsubscribed from every target the server confirmed, without a push message. Those - /// still waiting for the server subscribe after RESET. - fn reset(&mut self) { - let mut reset = vec![]; - for (id, sub) in &self.subs { - for (target, hold) in &sub.targets { - if *hold != Hold::Pending { - reset.push((*id, target.clone())); - } - } - } - for (id, target) in reset { - self.release(id, &target); - self.emit(id, unsubscribed(&target, UnsubscribeReason::Reset)); - } - } - - /// The connection closed, ending every stream. - fn close(&mut self, error: &Error) { - for sub in self.subs.values_mut() { - sub.end(Err(error.clone())); - sub.closed = Some(error.clone()); - } - self.holders.clear(); - } - - /// The subscription was dropped and unsubscribed, ending its streams. - fn remove(&mut self, id: u64) { - let Some(mut sub) = self.subs.remove(&id) else { - return; - }; - sub.end(Ok(())); - for (target, _) in sub.targets { - if let Some(ids) = self.holders.get_mut(&target) { - ids.retain(|holder| *holder != id); - if ids.is_empty() { - self.holders.remove(&target); - } - } - } - } -} - -/// A subscription, for its resource. -/// -/// Dropping it unsubscribes from its targets, in the background, then ends its streams. -struct Handle { - inner: Rc, - id: u64, -} - -impl Drop for Handle { - fn drop(&mut self) { - let inner = self.inner.clone(); - let id = self.id; - let _ = self - .inner - .tasks - .unbounded_send(Task::Spawn(Box::pin(async move { - let every = Kind::ALL.into_iter().map(|kind| (kind, None)).collect(); - let _ = unsubscribe(&inner, id, every).await; - inner.state.borrow_mut().subscriptions.remove(id); - }))); - } -} - -/// The names given once each, in order. -fn unique(names: Vec) -> Vec { - let mut unique: Vec = vec![]; - for name in names { - if !unique.contains(&name) { - unique.push(name); - } - } - unique -} - -/// A command for one kind of targets, queued and waiting for the server to confirm it. -type Confirmation = oneshot::Receiver>; - -/// Subscribe to the targets, one command for each kind, queued with no other command between -/// them. Every command is sent, the error holds each that failed. -async fn subscribe(inner: &Inner, id: u64, groups: Vec<(Kind, Vec)>) -> Result<(), Error> { - let several = groups.len() > 1; - // no await until every command is queued - let queued: Vec<_> = groups - .into_iter() - .map(|(kind, names)| (kind, queue_subscribe(inner, id, kind, names))) - .collect(); - let mut errors = vec![]; - for (kind, queued) in queued { - let result = match queued { - Ok(None) => Ok(()), - // dropped unsent when the connection closes, the reader fails it first - Ok(Some((names, confirmation))) => { - let result = confirmation - .await - .unwrap_or_else(|_| Err(closed_error(&inner.state))); - if result.is_err() { - abandon( - &mut inner.state.borrow_mut().subscriptions, - id, - kind, - &names, - ); - } - result - } - Err(error) => Err(error), - }; - if let Err(error) = result { - errors.push((kind.subscribe(), error)); - } - } - aggregate(errors, several) -} - -/// Queue SUBSCRIBE for the targets the subscription does not hold yet, none when it holds them all. -fn queue_subscribe( - inner: &Inner, - id: u64, - kind: Kind, - names: Vec, -) -> Result, Confirmation)>, Error> { - let names: Vec = { - let mut state = inner.state.borrow_mut(); - // before the targets are touched, they stay as they were when the connection closed - if let Some(error) = &state.closed { - return Err(error.clone()); - } - let subscriptions = &mut state.subscriptions; - let Some(sub) = subscriptions.subs.get(&id) else { - return Ok(None); - }; - let names: Vec = unique(names) - .into_iter() - .filter(|name| { - // a target being left is subscribed to again, after its UNSUBSCRIBE - matches!(sub.hold(&(kind, name.clone())), None | Some(Hold::Leaving)) - }) - .collect(); - for name in &names { - subscriptions.hold(id, &(kind, name.clone()), Hold::Pending); - } - names - }; - if names.is_empty() { - return Ok(None); - } - let command = std::iter::once(kind.subscribe().to_string()) - .chain(names.iter().cloned()) - .collect(); - let (done_tx, done_rx) = oneshot::channel(); - let reply = Reply::Subscribe { - kind, - channels: names.clone(), - id, - done: done_tx, - }; - match enqueue(inner, command, Some(reply), None) { - Ok(()) => Ok(Some((names, done_rx))), - Err(error) => { - abandon( - &mut inner.state.borrow_mut().subscriptions, - id, - kind, - &names, - ); - Err(error) - } - } -} - -/// SUBSCRIBE failed, the subscription lets go of the targets it was waiting for. -fn abandon(subscriptions: &mut Subscriptions, id: u64, kind: Kind, names: &[String]) { - for name in names { - let target = (kind, name.clone()); - let pending = subscriptions - .subs - .get(&id) - .and_then(|sub| sub.hold(&target)) - == Some(Hold::Pending); - if pending { - subscriptions.release(id, &target); - } - } -} - -/// Unsubscribe from the targets, none for every target of the kind, one command for each kind, -/// queued with no other command between them. Every command is sent, the error holds each that -/// failed. -async fn unsubscribe( - inner: &Inner, - id: u64, - groups: Vec<(Kind, Option>)>, -) -> Result<(), Error> { - let several = groups.len() > 1; - // no await until every command is queued - let queued: Vec<_> = groups - .into_iter() - .map(|(kind, names)| (kind, queue_unsubscribe(inner, id, kind, names))) - .collect(); - let mut errors = vec![]; - for (kind, queued) in queued { - let result = match queued { - Ok(None) => Ok(()), - // dropped unsent when the connection closes, the reader fails it first - Ok(Some(confirmation)) => confirmation - .await - .unwrap_or_else(|_| Err(closed_error(&inner.state))), - Err(error) => Err(error), - }; - if let Err(error) = result { - errors.push((kind.unsubscribe(), error)); - } - } - aggregate(errors, several) -} - -/// Queue UNSUBSCRIBE for the targets no other subscription holds, the subscription lets go of the -/// others at once. None when there is nothing to send. -fn queue_unsubscribe( - inner: &Inner, - id: u64, - kind: Kind, - names: Option>, -) -> Result, Error> { - let release = { - let mut state = inner.state.borrow_mut(); - // before the targets are touched, they stay as they were when the connection closed - if let Some(error) = &state.closed { - return Err(error.clone()); - } - let subscriptions = &mut state.subscriptions; - let Some(sub) = subscriptions.subs.get(&id) else { - return Ok(None); - }; - let held = |name: &String| { - matches!( - sub.hold(&(kind, name.clone())), - Some(Hold::Pending | Hold::Active) - ) - }; - let names: Vec = match names { - Some(names) => unique(names).into_iter().filter(held).collect(), - None => sub - .targets - .iter() - .filter(|((held_kind, name), _)| *held_kind == kind && held(name)) - .map(|((_, name), _)| name.clone()) - .collect(), - }; - let mut release = vec![]; - for name in names { - let target = (kind, name); - let kept = subscriptions - .holding(&target, &[Hold::Pending, Hold::Active]) - .iter() - .any(|other| *other != id); - if kept { - subscriptions.release(id, &target); - subscriptions.emit(id, unsubscribed(&target, UnsubscribeReason::Requested)); - } else { - subscriptions.hold(id, &target, Hold::Leaving); - release.push(target.1); - } - } - release - }; - if release.is_empty() { - return Ok(None); - } - let command = std::iter::once(kind.unsubscribe().to_string()) - .chain(release.iter().cloned()) - .collect(); - let (done_tx, done_rx) = oneshot::channel(); - let reply = Reply::Unsubscribe { - kind, - channels: release, - done: done_tx, - }; - enqueue(inner, command, Some(reply), None)?; - Ok(Some(done_rx)) -} - -/// The errors of the commands of a call as one, each prefixed with its command when the call sent -/// several. An error of one kind keeps it, e.g. every command was refused by the server, otherwise -/// it is a client error. -fn aggregate(errors: Vec<(&str, Error)>, several: bool) -> Result<(), Error> { - let mut errors = errors.into_iter(); - let Some((command, first)) = errors.next() else { - return Ok(()); - }; - if !several { - return Err(first); - } - let kind = std::mem::discriminant(&first); - let mut same = true; - let mut messages = vec![format!("{command}: {}", describe_error(&first))]; - for (command, error) in errors { - same &= std::mem::discriminant(&error) == kind; - messages.push(format!("{command}: {}", describe_error(&error))); - } - if messages.len() == 1 { - return Err(match first { - Error::Valkey(_) => Error::Valkey(messages.remove(0)), - Error::Client(_) => Error::Client(messages.remove(0)), - Error::Resp(_) => Error::Resp(messages.remove(0)), - error => error, - }); - } - let message = messages.join("; "); - Err(match first { - // the connection closed, every command failed the same way - error @ (Error::Closed(_) | Error::Lagged) if same => error, - Error::Valkey(_) if same => Error::Valkey(message), - Error::Resp(_) if same => Error::Resp(message), - _ => Error::Client(message), - }) -} - -/// An error for people to read, in an aggregate of errors. -fn describe_error(error: &Error) -> String { - match error { - Error::Closed(None) => "connection closed".to_string(), - Error::Closed(Some(reason)) => format!("connection closed: {reason}"), - Error::Client(message) | Error::Resp(message) | Error::Valkey(message) => message.clone(), - Error::Lagged => "fell behind".to_string(), - } -} - /// RESET, then HELLO again, with no other command between them, closing the connection when HELLO /// fails. /// @@ -1152,128 +465,6 @@ fn replies_with_pushes(command: &[String]) -> bool { }) } -/// A push message the connection understands. -enum Push { - /// The server subscribed to the target. - Subscribed(Target), - /// The server unsubscribed from the target, none when nothing was subscribed. - Unsubscribed(Kind, Option), - /// A message, for the target it was subscribed with. - Message(Target, SubscriptionMessage), -} - -fn parse_push(value: &RespValue) -> Option { - let RespValue::Push(items) = value else { - return None; - }; - let items = items - .iter() - .map(|item| decode(item.clone()).ok()) - .collect::>>()?; - let text = |index: usize| match items.get(index) { - Some(RespValue::String(text) | RespValue::BulkString(text)) => Some(text.clone()), - _ => None, - }; - let push = match text(0)?.as_str() { - "subscribe" => Push::Subscribed((Kind::Channel, text(1)?)), - "psubscribe" => Push::Subscribed((Kind::Pattern, text(1)?)), - "ssubscribe" => Push::Subscribed((Kind::Shard, text(1)?)), - "unsubscribe" => Push::Unsubscribed(Kind::Channel, text(1)), - "punsubscribe" => Push::Unsubscribed(Kind::Pattern, text(1)), - "sunsubscribe" => Push::Unsubscribed(Kind::Shard, text(1)), - "message" => { - let channel = text(1)?; - let message = SubscriptionMessage { - payload: text(2)?, - channel: channel.clone(), - target: SubscriptionTarget::Channel(channel.clone()), - }; - Push::Message((Kind::Channel, channel), message) - } - "pmessage" => { - let pattern = text(1)?; - let message = SubscriptionMessage { - payload: text(3)?, - channel: text(2)?, - target: SubscriptionTarget::Pattern(pattern.clone()), - }; - Push::Message((Kind::Pattern, pattern), message) - } - "smessage" => { - let shardchannel = text(1)?; - let message = SubscriptionMessage { - payload: text(2)?, - channel: shardchannel.clone(), - target: SubscriptionTarget::Shardchannel(shardchannel.clone()), - }; - Push::Message((Kind::Shard, shardchannel), message) - } - _ => return None, - }; - Some(push) -} - -/// Hand a push message to the subscriptions, or the command, it belongs to, false when it belongs -/// to none. -fn claim_push(state: &mut State, value: &RespValue) -> bool { - let State { - pending, - subscriptions, - .. - } = state; - match parse_push(value) { - Some(Push::Subscribed(target)) => { - // the confirmations of a SUBSCRIBE arrive in the place of its reply - let Some(Reply::Subscribe { - kind, channels, id, .. - }) = pending.front_mut() - else { - return false; - }; - if *kind != target.0 { - return false; - } - let Some(index) = channels.iter().position(|name| *name == target.1) else { - return false; - }; - channels.remove(index); - let (id, done) = (*id, channels.is_empty()); - subscriptions.subscribed(id, &target); - if done && let Some(Reply::Subscribe { done, .. }) = pending.pop_front() { - let _ = done.send(Ok(())); - } - true - } - Some(Push::Unsubscribed(kind, Some(name))) => { - let target = (kind, name); - // the confirmations of an UNSUBSCRIBE arrive in the place of its reply, otherwise the - // server unsubscribed on its own - let confirmation = match pending.front_mut() { - Some(Reply::Unsubscribe { kind, channels, .. }) if *kind == target.0 => channels - .iter() - .position(|name| *name == target.1) - .map(|index| { - channels.remove(index); - channels.is_empty() - }), - _ => None, - }; - match confirmation { - Some(done) => { - subscriptions.unsubscribe_confirmed(&target); - if done && let Some(Reply::Unsubscribe { done, .. }) = pending.pop_front() { - let _ = done.send(Ok(())); - } - true - } - None => subscriptions.unsubscribed_by_server(&target), - } - } - Some(Push::Message(target, message)) => subscriptions.message(&target, message), - Some(Push::Unsubscribed(_, None)) | None => false, - } -} - /// The error a command fails with once the connection closed. fn closed_error(state: &RefCell) -> Error { state.borrow().closed.clone().unwrap_or(Error::Closed(None)) @@ -1289,9 +480,8 @@ fn close(state: &RefCell, error: Error) { Reply::Call(reply) => { let _ = reply.send(Err(error.clone())); } - Reply::Subscribe { done, .. } - | Reply::Unsubscribe { done, .. } - | Reply::Reset(done) => { + Reply::Pubsub(pending) => pending.fail(error.clone()), + Reply::Reset(done) => { let _ = done.send(Err(error.clone())); } Reply::Rehello { proceed, done } => { @@ -1362,7 +552,7 @@ async fn read_replies( state.activity += 1; let value = match value { push @ RespValue::Push(_) => { - if claim_push(state, &push) { + if pubsub::claim_push(state, &push) { continue; } push @@ -1403,20 +593,12 @@ async fn read_replies( continue; } // confirmed with push messages, a reply in their place is an error - Some(Reply::Subscribe { - kind, - channels, - id, - done, - }) => { - abandon(&mut state.subscriptions, id, kind, &channels); - let _ = - done.send(reply_result(value).and_then(|reply| Err(unexpected(&reply)))); - continue; - } - Some(Reply::Unsubscribe { done, .. }) => { - let _ = - done.send(reply_result(value).and_then(|reply| Err(unexpected(&reply)))); + Some(Reply::Pubsub(pending)) => { + let error = match reply_result(value) { + Ok(reply) => unexpected(&reply), + Err(error) => error, + }; + pending.refused(&mut state.subscriptions, error); continue; } // sent unprompted, e.g. the feed of MONITOR @@ -1465,26 +647,6 @@ async fn forward_values( } } -/// Forward a subscription's events to a reader's stream, then close the stream and tell the reader -/// how it ended. -async fn forward_events( - mut events: mpsc::Receiver, - end: oneshot::Receiver, - mut writer: StreamWriter, - end_writer: FutureWriter, -) { - while let Some(event) = next(&mut events).await { - if writer.write_one(event).await.is_some() { - // the stream was dropped, the subscription carries on without it - break; - } - } - drop(events); - drop(writer); - let end = end.await.unwrap_or_else(|_| Err(Error::Closed(None))); - let _ = end_writer.write(end).await; -} - /// Send PING whenever the connection was idle for `interval`, closing it if PING fails. async fn keep_alive(inner: Weak, generation: u64, interval: DurationMilliseconds) { loop { diff --git a/components/connection/src/pubsub.rs b/components/connection/src/pubsub.rs new file mode 100644 index 0000000..c174d6b --- /dev/null +++ b/components/connection/src/pubsub.rs @@ -0,0 +1,897 @@ +//! Pub/sub: subscriptions to channels, patterns and shard channels, see `subscription` in the +//! connection's WIT. Left out of builds without the `pubsub` feature, see `pubsub_disabled.rs`. + +use std::collections::{HashMap, VecDeque}; +use std::rc::Rc; + +use componentized_valkey::types::{ + RespValue, SubscriptionEvent, SubscriptionMessage, SubscriptionTarget, UnsubscribeReason, +}; +use futures_channel::{mpsc, oneshot}; +use wit_bindgen::{FutureReader, FutureWriter, StreamReader, StreamWriter}; + +use crate::exports::componentized::valkey::connection::{Error, GuestSubscription, Subscription}; +use crate::{ + Inner, PUSH_BUFFER, Reply, State, Task, closed_error, decode, enqueue, next, wit_future, + wit_stream, +}; + +/// A new subscription, setting the ratchet, see `State::owns_subscriptions`. +pub(crate) fn subscription(inner: &Rc) -> Result { + let id = { + let mut state = inner.state.borrow_mut(); + state.owns_subscriptions = true; + let closed = state.closed.clone(); + state.subscriptions.create(closed) + }; + Ok(Subscription::new(SubscriptionImpl(Handle { + inner: inner.clone(), + id, + }))) +} + +/// A subscription's command waiting for the server, confirmed by a push message for each target +/// rather than a reply. +pub(crate) enum Pending { + /// SUBSCRIBE and the like. + Subscribe { + kind: Kind, + /// The targets not confirmed yet. + channels: Vec, + id: u64, + done: oneshot::Sender>, + }, + /// UNSUBSCRIBE and the like. + Unsubscribe { + kind: Kind, + /// The targets not confirmed yet. + channels: Vec, + done: oneshot::Sender>, + }, +} + +impl Pending { + /// A reply arrived in the place of the confirmations, the server refused the command. + pub(crate) fn refused(self, subscriptions: &mut Subscriptions, error: Error) { + match self { + Pending::Subscribe { + kind, + channels, + id, + done, + } => { + abandon(subscriptions, id, kind, &channels); + let _ = done.send(Err(error)); + } + Pending::Unsubscribe { done, .. } => { + let _ = done.send(Err(error)); + } + } + } + + /// The connection closed. + pub(crate) fn fail(self, error: Error) { + match self { + Pending::Subscribe { done, .. } | Pending::Unsubscribe { done, .. } => { + let _ = done.send(Err(error)); + } + } + } +} + +pub struct SubscriptionImpl(Handle); + +impl GuestSubscription for SubscriptionImpl { + fn targets(&self) -> Vec { + let state = self.0.inner.state.borrow(); + let Some(sub) = state.subscriptions.subs.get(&self.0.id) else { + return vec![]; + }; + sub.targets + .iter() + .filter(|(_, hold)| *hold == Hold::Active) + .map(|((kind, name), _)| kind.target(name.clone())) + .collect() + } + + async fn subscribe(&self, targets: Vec) -> Result<(), Error> { + let groups = by_kind(targets)?; + subscribe(&self.0.inner, self.0.id, groups).await + } + + async fn unsubscribe(&self, targets: Option>) -> Result<(), Error> { + let groups = match targets { + Some(targets) => by_kind(targets)? + .into_iter() + .map(|(kind, names)| (kind, Some(names))) + .collect(), + None => Kind::ALL.into_iter().map(|kind| (kind, None)).collect(), + }; + unsubscribe(&self.0.inner, self.0.id, groups).await + } + + fn events(&self) -> (StreamReader, FutureReader) { + let (writer, stream) = wit_stream::new(); + // the default is only written if the subscription is gone before the stream ends + let (end_writer, end) = wit_future::new(|| Err(Error::Closed(None))); + let (mut events_tx, events_rx) = mpsc::channel(PUSH_BUFFER); + let (end_tx, end_rx) = oneshot::channel(); + { + let mut state = self.0.inner.state.borrow_mut(); + if let Some(sub) = state.subscriptions.subs.get_mut(&self.0.id) { + // the backlog fits, it is no larger than the channel + for event in sub.backlog.drain(..) { + let _ = events_tx.try_send(event); + } + match &sub.closed { + Some(error) => { + let _ = end_tx.send(Err(error.clone())); + } + None => sub.readers.push(Reader { + events: events_tx, + end: end_tx, + }), + } + } + } + let forward = forward_events(events_rx, end_rx, writer, end_writer); + let _ = self + .0 + .inner + .tasks + .unbounded_send(Task::Spawn(Box::pin(forward))); + (stream, end) + } +} + +/// The kind of a subscription's target, each with its own commands and push messages. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum Kind { + Channel, + Pattern, + Shard, +} + +impl Kind { + const ALL: [Kind; 3] = [Kind::Channel, Kind::Pattern, Kind::Shard]; + + fn subscribe(self) -> &'static str { + match self { + Kind::Channel => "SUBSCRIBE", + Kind::Pattern => "PSUBSCRIBE", + Kind::Shard => "SSUBSCRIBE", + } + } + + fn unsubscribe(self) -> &'static str { + match self { + Kind::Channel => "UNSUBSCRIBE", + Kind::Pattern => "PUNSUBSCRIBE", + Kind::Shard => "SUNSUBSCRIBE", + } + } + + fn target(self, name: String) -> SubscriptionTarget { + match self { + Kind::Channel => SubscriptionTarget::Channel(name), + Kind::Pattern => SubscriptionTarget::Pattern(name), + Kind::Shard => SubscriptionTarget::Shardchannel(name), + } + } +} + +/// The names of the targets of each kind, in the order each kind first appears, failing unless +/// there is at least one target. +fn by_kind(targets: Vec) -> Result)>, Error> { + if targets.is_empty() { + return Err(Error::Client( + "targets requires at least one item".to_string(), + )); + } + let mut groups: Vec<(Kind, Vec)> = vec![]; + for target in targets { + let (kind, name) = match target { + SubscriptionTarget::Channel(name) => (Kind::Channel, name), + SubscriptionTarget::Pattern(name) => (Kind::Pattern, name), + SubscriptionTarget::Shardchannel(name) => (Kind::Shard, name), + }; + match groups.iter_mut().find(|(grouped, _)| *grouped == kind) { + Some((_, names)) => names.push(name), + None => groups.push((kind, vec![name])), + } + } + Ok(groups) +} + +/// A channel, pattern or shard channel, by its kind and name. +type Target = (Kind, String); + +/// How a subscription holds a target. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Hold { + /// Waiting for the server to confirm SUBSCRIBE. + Pending, + /// Confirmed, its messages arrive. + Active, + /// Unsubscribing, its messages arrive until the server confirms UNSUBSCRIBE. + Leaving, +} + +/// How a stream of events ended, `ok` once the subscription is dropped. +type Ended = Result<(), Error>; + +/// A reader of a subscription's events, and where to send how its stream ended. +struct Reader { + events: mpsc::Sender, + end: oneshot::Sender, +} + +#[derive(Default)] +struct Sub { + /// The targets, in the order they were subscribed to. + targets: Vec<(Target, Hold)>, + readers: Vec, + /// How to tell readers whose stream was dropped how the subscription ended. + orphans: Vec>, + /// The events arriving while no stream is open, for the next stream. + backlog: VecDeque, + /// The error that closed the connection, once it has. + closed: Option, +} + +impl Sub { + fn hold(&self, target: &Target) -> Option { + self.targets + .iter() + .find(|(held, _)| held == target) + .map(|(_, hold)| *hold) + } + + fn set_hold(&mut self, target: &Target, hold: Hold) { + match self.targets.iter_mut().find(|(held, _)| held == target) { + Some((_, current)) => *current = hold, + None => self.targets.push((target.clone(), hold)), + } + } + + fn release(&mut self, target: &Target) { + self.targets.retain(|(held, _)| held != target); + } + + /// Fan the event out to every reader, a reader that is full is cut off, one whose stream was + /// dropped is forgotten. + fn emit(&mut self, event: SubscriptionEvent) { + if self.readers.is_empty() { + if self.backlog.len() < PUSH_BUFFER { + self.backlog.push_back(event); + } + return; + } + for mut reader in std::mem::take(&mut self.readers) { + match reader.events.try_send(event.clone()) { + Ok(()) => self.readers.push(reader), + Err(error) if error.is_full() => { + let _ = reader.end.send(Err(Error::Lagged)); + } + Err(_) => self.orphans.push(reader.end), + } + } + } + + /// End the readers' streams. + fn end(&mut self, ended: Ended) { + for reader in self.readers.drain(..) { + let _ = reader.end.send(ended.clone()); + } + for orphan in self.orphans.drain(..) { + let _ = orphan.send(ended.clone()); + } + } +} + +fn unsubscribed(target: &Target, reason: UnsubscribeReason) -> SubscriptionEvent { + SubscriptionEvent::Unsubscribed((target.0.target(target.1.clone()), reason)) +} + +#[derive(Default)] +pub(crate) struct Subscriptions { + next_id: u64, + subs: HashMap, + /// The subscriptions holding each target, in any way. + holders: HashMap>, +} + +impl Subscriptions { + fn create(&mut self, closed: Option) -> u64 { + let id = self.next_id; + self.next_id += 1; + self.subs.insert( + id, + Sub { + closed, + ..Sub::default() + }, + ); + id + } + + fn hold(&mut self, id: u64, target: &Target, hold: Hold) { + let Some(sub) = self.subs.get_mut(&id) else { + return; + }; + if sub.hold(target).is_none() { + self.holders.entry(target.clone()).or_default().push(id); + } + sub.set_hold(target, hold); + } + + /// The subscription no longer holds the target. + fn release(&mut self, id: u64, target: &Target) { + if let Some(sub) = self.subs.get_mut(&id) { + sub.release(target); + } + if let Some(ids) = self.holders.get_mut(target) { + ids.retain(|holder| *holder != id); + if ids.is_empty() { + self.holders.remove(target); + } + } + } + + /// The subscriptions holding the target in one of the ways. + fn holding(&self, target: &Target, holds: &[Hold]) -> Vec { + self.holders + .get(target) + .into_iter() + .flatten() + .copied() + .filter(|id| { + self.subs + .get(id) + .and_then(|sub| sub.hold(target)) + .is_some_and(|hold| holds.contains(&hold)) + }) + .collect() + } + + fn emit(&mut self, id: u64, event: SubscriptionEvent) { + if let Some(sub) = self.subs.get_mut(&id) { + sub.emit(event); + } + } + + /// The server confirmed SUBSCRIBE for the subscription's target. + fn subscribed(&mut self, id: u64, target: &Target) { + let Some(sub) = self.subs.get_mut(&id) else { + return; + }; + match sub.hold(target) { + Some(Hold::Pending) => sub.set_hold(target, Hold::Active), + // unsubscribed from while waiting, UNSUBSCRIBE follows + Some(Hold::Leaving) => {} + _ => return, + } + sub.emit(SubscriptionEvent::Subscribed( + target.0.target(target.1.clone()), + )); + } + + /// A message for the subscriptions receiving the target's messages, false when none holds it. + fn message(&mut self, target: &Target, message: SubscriptionMessage) -> bool { + if !self.holders.contains_key(target) { + return false; + } + for id in self.holding(target, &[Hold::Active, Hold::Leaving]) { + self.emit(id, SubscriptionEvent::Message(message.clone())); + } + true + } + + /// The server confirmed UNSUBSCRIBE for the target, the subscriptions leaving it let go. + fn unsubscribe_confirmed(&mut self, target: &Target) { + for id in self.holding(target, &[Hold::Leaving]) { + self.release(id, target); + self.emit(id, unsubscribed(target, UnsubscribeReason::Requested)); + } + } + + /// The server unsubscribed from the target on its own, false when none holds it. + fn unsubscribed_by_server(&mut self, target: &Target) -> bool { + if !self.holders.contains_key(target) { + return false; + } + for id in self.holding(target, &[Hold::Active, Hold::Leaving]) { + let leaving = + self.subs.get(&id).and_then(|sub| sub.hold(target)) == Some(Hold::Leaving); + self.release(id, target); + let reason = if leaving { + UnsubscribeReason::Requested + } else { + UnsubscribeReason::ByServer + }; + self.emit(id, unsubscribed(target, reason)); + } + true + } + + /// RESET unsubscribed from every target the server confirmed, without a push message. Those + /// still waiting for the server subscribe after RESET. + pub(crate) fn reset(&mut self) { + let mut reset = vec![]; + for (id, sub) in &self.subs { + for (target, hold) in &sub.targets { + if *hold != Hold::Pending { + reset.push((*id, target.clone())); + } + } + } + for (id, target) in reset { + self.release(id, &target); + self.emit(id, unsubscribed(&target, UnsubscribeReason::Reset)); + } + } + + /// The connection closed, ending every stream. + pub(crate) fn close(&mut self, error: &Error) { + for sub in self.subs.values_mut() { + sub.end(Err(error.clone())); + sub.closed = Some(error.clone()); + } + self.holders.clear(); + } + + /// The subscription was dropped and unsubscribed, ending its streams. + fn remove(&mut self, id: u64) { + let Some(mut sub) = self.subs.remove(&id) else { + return; + }; + sub.end(Ok(())); + for (target, _) in sub.targets { + if let Some(ids) = self.holders.get_mut(&target) { + ids.retain(|holder| *holder != id); + if ids.is_empty() { + self.holders.remove(&target); + } + } + } + } +} + +/// A subscription, for its resource. +/// +/// Dropping it unsubscribes from its targets, in the background, then ends its streams. +struct Handle { + inner: Rc, + id: u64, +} + +impl Drop for Handle { + fn drop(&mut self) { + let inner = self.inner.clone(); + let id = self.id; + let _ = self + .inner + .tasks + .unbounded_send(Task::Spawn(Box::pin(async move { + let every = Kind::ALL.into_iter().map(|kind| (kind, None)).collect(); + let _ = unsubscribe(&inner, id, every).await; + inner.state.borrow_mut().subscriptions.remove(id); + }))); + } +} + +/// The names given once each, in order. +fn unique(names: Vec) -> Vec { + let mut unique: Vec = vec![]; + for name in names { + if !unique.contains(&name) { + unique.push(name); + } + } + unique +} + +/// A command for one kind of targets, queued and waiting for the server to confirm it. +type Confirmation = oneshot::Receiver>; + +/// Subscribe to the targets, one command for each kind, queued with no other command between +/// them. Every command is sent, the error holds each that failed. +async fn subscribe(inner: &Inner, id: u64, groups: Vec<(Kind, Vec)>) -> Result<(), Error> { + let several = groups.len() > 1; + // no await until every command is queued + let queued: Vec<_> = groups + .into_iter() + .map(|(kind, names)| (kind, queue_subscribe(inner, id, kind, names))) + .collect(); + let mut errors = vec![]; + for (kind, queued) in queued { + let result = match queued { + Ok(None) => Ok(()), + // dropped unsent when the connection closes, the reader fails it first + Ok(Some((names, confirmation))) => { + let result = confirmation + .await + .unwrap_or_else(|_| Err(closed_error(&inner.state))); + if result.is_err() { + abandon( + &mut inner.state.borrow_mut().subscriptions, + id, + kind, + &names, + ); + } + result + } + Err(error) => Err(error), + }; + if let Err(error) = result { + errors.push((kind.subscribe(), error)); + } + } + aggregate(errors, several) +} + +/// Queue SUBSCRIBE for the targets the subscription does not hold yet, none when it holds them all. +fn queue_subscribe( + inner: &Inner, + id: u64, + kind: Kind, + names: Vec, +) -> Result, Confirmation)>, Error> { + let names: Vec = { + let mut state = inner.state.borrow_mut(); + // before the targets are touched, they stay as they were when the connection closed + if let Some(error) = &state.closed { + return Err(error.clone()); + } + let subscriptions = &mut state.subscriptions; + let Some(sub) = subscriptions.subs.get(&id) else { + return Ok(None); + }; + let names: Vec = unique(names) + .into_iter() + .filter(|name| { + // a target being left is subscribed to again, after its UNSUBSCRIBE + matches!(sub.hold(&(kind, name.clone())), None | Some(Hold::Leaving)) + }) + .collect(); + for name in &names { + subscriptions.hold(id, &(kind, name.clone()), Hold::Pending); + } + names + }; + if names.is_empty() { + return Ok(None); + } + let command = std::iter::once(kind.subscribe().to_string()) + .chain(names.iter().cloned()) + .collect(); + let (done_tx, done_rx) = oneshot::channel(); + let reply = Reply::Pubsub(Pending::Subscribe { + kind, + channels: names.clone(), + id, + done: done_tx, + }); + match enqueue(inner, command, Some(reply), None) { + Ok(()) => Ok(Some((names, done_rx))), + Err(error) => { + abandon( + &mut inner.state.borrow_mut().subscriptions, + id, + kind, + &names, + ); + Err(error) + } + } +} + +/// SUBSCRIBE failed, the subscription lets go of the targets it was waiting for. +fn abandon(subscriptions: &mut Subscriptions, id: u64, kind: Kind, names: &[String]) { + for name in names { + let target = (kind, name.clone()); + let pending = subscriptions + .subs + .get(&id) + .and_then(|sub| sub.hold(&target)) + == Some(Hold::Pending); + if pending { + subscriptions.release(id, &target); + } + } +} + +/// Unsubscribe from the targets, none for every target of the kind, one command for each kind, +/// queued with no other command between them. Every command is sent, the error holds each that +/// failed. +async fn unsubscribe( + inner: &Inner, + id: u64, + groups: Vec<(Kind, Option>)>, +) -> Result<(), Error> { + let several = groups.len() > 1; + // no await until every command is queued + let queued: Vec<_> = groups + .into_iter() + .map(|(kind, names)| (kind, queue_unsubscribe(inner, id, kind, names))) + .collect(); + let mut errors = vec![]; + for (kind, queued) in queued { + let result = match queued { + Ok(None) => Ok(()), + // dropped unsent when the connection closes, the reader fails it first + Ok(Some(confirmation)) => confirmation + .await + .unwrap_or_else(|_| Err(closed_error(&inner.state))), + Err(error) => Err(error), + }; + if let Err(error) = result { + errors.push((kind.unsubscribe(), error)); + } + } + aggregate(errors, several) +} + +/// Queue UNSUBSCRIBE for the targets no other subscription holds, the subscription lets go of the +/// others at once. None when there is nothing to send. +fn queue_unsubscribe( + inner: &Inner, + id: u64, + kind: Kind, + names: Option>, +) -> Result, Error> { + let release = { + let mut state = inner.state.borrow_mut(); + // before the targets are touched, they stay as they were when the connection closed + if let Some(error) = &state.closed { + return Err(error.clone()); + } + let subscriptions = &mut state.subscriptions; + let Some(sub) = subscriptions.subs.get(&id) else { + return Ok(None); + }; + let held = |name: &String| { + matches!( + sub.hold(&(kind, name.clone())), + Some(Hold::Pending | Hold::Active) + ) + }; + let names: Vec = match names { + Some(names) => unique(names).into_iter().filter(held).collect(), + None => sub + .targets + .iter() + .filter(|((held_kind, name), _)| *held_kind == kind && held(name)) + .map(|((_, name), _)| name.clone()) + .collect(), + }; + let mut release = vec![]; + for name in names { + let target = (kind, name); + let kept = subscriptions + .holding(&target, &[Hold::Pending, Hold::Active]) + .iter() + .any(|other| *other != id); + if kept { + subscriptions.release(id, &target); + subscriptions.emit(id, unsubscribed(&target, UnsubscribeReason::Requested)); + } else { + subscriptions.hold(id, &target, Hold::Leaving); + release.push(target.1); + } + } + release + }; + if release.is_empty() { + return Ok(None); + } + let command = std::iter::once(kind.unsubscribe().to_string()) + .chain(release.iter().cloned()) + .collect(); + let (done_tx, done_rx) = oneshot::channel(); + let reply = Reply::Pubsub(Pending::Unsubscribe { + kind, + channels: release, + done: done_tx, + }); + enqueue(inner, command, Some(reply), None)?; + Ok(Some(done_rx)) +} + +/// The errors of the commands of a call as one, each prefixed with its command when the call sent +/// several. An error of one kind keeps it, e.g. every command was refused by the server, otherwise +/// it is a client error. +fn aggregate(errors: Vec<(&str, Error)>, several: bool) -> Result<(), Error> { + let mut errors = errors.into_iter(); + let Some((command, first)) = errors.next() else { + return Ok(()); + }; + if !several { + return Err(first); + } + let kind = std::mem::discriminant(&first); + let mut same = true; + let mut messages = vec![format!("{command}: {}", describe_error(&first))]; + for (command, error) in errors { + same &= std::mem::discriminant(&error) == kind; + messages.push(format!("{command}: {}", describe_error(&error))); + } + if messages.len() == 1 { + return Err(match first { + Error::Valkey(_) => Error::Valkey(messages.remove(0)), + Error::Client(_) => Error::Client(messages.remove(0)), + Error::Resp(_) => Error::Resp(messages.remove(0)), + error => error, + }); + } + let message = messages.join("; "); + Err(match first { + // the connection closed, every command failed the same way + error @ (Error::Closed(_) | Error::Lagged) if same => error, + Error::Valkey(_) if same => Error::Valkey(message), + Error::Resp(_) if same => Error::Resp(message), + _ => Error::Client(message), + }) +} + +/// An error for people to read, in an aggregate of errors. +fn describe_error(error: &Error) -> String { + match error { + Error::Closed(None) => "connection closed".to_string(), + Error::Closed(Some(reason)) => format!("connection closed: {reason}"), + Error::Client(message) | Error::Resp(message) | Error::Valkey(message) => message.clone(), + Error::Lagged => "fell behind".to_string(), + } +} + +/// A push message the connection understands. +enum Push { + /// The server subscribed to the target. + Subscribed(Target), + /// The server unsubscribed from the target, none when nothing was subscribed. + Unsubscribed(Kind, Option), + /// A message, for the target it was subscribed with. + Message(Target, SubscriptionMessage), +} + +fn parse_push(value: &RespValue) -> Option { + let RespValue::Push(items) = value else { + return None; + }; + let items = items + .iter() + .map(|item| decode(item.clone()).ok()) + .collect::>>()?; + let text = |index: usize| match items.get(index) { + Some(RespValue::String(text) | RespValue::BulkString(text)) => Some(text.clone()), + _ => None, + }; + let push = match text(0)?.as_str() { + "subscribe" => Push::Subscribed((Kind::Channel, text(1)?)), + "psubscribe" => Push::Subscribed((Kind::Pattern, text(1)?)), + "ssubscribe" => Push::Subscribed((Kind::Shard, text(1)?)), + "unsubscribe" => Push::Unsubscribed(Kind::Channel, text(1)), + "punsubscribe" => Push::Unsubscribed(Kind::Pattern, text(1)), + "sunsubscribe" => Push::Unsubscribed(Kind::Shard, text(1)), + "message" => { + let channel = text(1)?; + let message = SubscriptionMessage { + payload: text(2)?, + channel: channel.clone(), + target: SubscriptionTarget::Channel(channel.clone()), + }; + Push::Message((Kind::Channel, channel), message) + } + "pmessage" => { + let pattern = text(1)?; + let message = SubscriptionMessage { + payload: text(3)?, + channel: text(2)?, + target: SubscriptionTarget::Pattern(pattern.clone()), + }; + Push::Message((Kind::Pattern, pattern), message) + } + "smessage" => { + let shardchannel = text(1)?; + let message = SubscriptionMessage { + payload: text(2)?, + channel: shardchannel.clone(), + target: SubscriptionTarget::Shardchannel(shardchannel.clone()), + }; + Push::Message((Kind::Shard, shardchannel), message) + } + _ => return None, + }; + Some(push) +} + +/// Hand a push message to the subscriptions, or the command, it belongs to, false when it belongs +/// to none. +pub(crate) fn claim_push(state: &mut State, value: &RespValue) -> bool { + let State { + pending, + subscriptions, + .. + } = state; + match parse_push(value) { + Some(Push::Subscribed(target)) => { + // the confirmations of a SUBSCRIBE arrive in the place of its reply + let Some(Reply::Pubsub(Pending::Subscribe { + kind, channels, id, .. + })) = pending.front_mut() + else { + return false; + }; + if *kind != target.0 { + return false; + } + let Some(index) = channels.iter().position(|name| *name == target.1) else { + return false; + }; + channels.remove(index); + let (id, done) = (*id, channels.is_empty()); + subscriptions.subscribed(id, &target); + if done + && let Some(Reply::Pubsub(Pending::Subscribe { done, .. })) = pending.pop_front() + { + let _ = done.send(Ok(())); + } + true + } + Some(Push::Unsubscribed(kind, Some(name))) => { + let target = (kind, name); + // the confirmations of an UNSUBSCRIBE arrive in the place of its reply, otherwise the + // server unsubscribed on its own + let confirmation = match pending.front_mut() { + Some(Reply::Pubsub(Pending::Unsubscribe { kind, channels, .. })) + if *kind == target.0 => + { + channels + .iter() + .position(|name| *name == target.1) + .map(|index| { + channels.remove(index); + channels.is_empty() + }) + } + _ => None, + }; + match confirmation { + Some(done) => { + subscriptions.unsubscribe_confirmed(&target); + if done + && let Some(Reply::Pubsub(Pending::Unsubscribe { done, .. })) = + pending.pop_front() + { + let _ = done.send(Ok(())); + } + true + } + None => subscriptions.unsubscribed_by_server(&target), + } + } + Some(Push::Message(target, message)) => subscriptions.message(&target, message), + Some(Push::Unsubscribed(_, None)) | None => false, + } +} + +/// Forward a subscription's events to a reader's stream, then close the stream and tell the reader +/// how it ended. +async fn forward_events( + mut events: mpsc::Receiver, + end: oneshot::Receiver, + mut writer: StreamWriter, + end_writer: FutureWriter, +) { + while let Some(event) = next(&mut events).await { + if writer.write_one(event).await.is_some() { + // the stream was dropped, the subscription carries on without it + break; + } + } + drop(events); + drop(writer); + let end = end.await.unwrap_or_else(|_| Err(Error::Closed(None))); + let _ = end_writer.write(end).await; +} diff --git a/components/connection/src/pubsub_disabled.rs b/components/connection/src/pubsub_disabled.rs new file mode 100644 index 0000000..20493da --- /dev/null +++ b/components/connection/src/pubsub_disabled.rs @@ -0,0 +1,74 @@ +//! Pub/sub left out, for builds without the `pubsub` feature, see `pubsub.rs`. +//! +//! The interface stays the same: the subscription resource is exported, but none is ever made, +//! `connection.subscription` fails, and every push message is a dead letter. + +use std::rc::Rc; + +use componentized_valkey::types::{RespValue, SubscriptionEvent, SubscriptionTarget}; +use wit_bindgen::{FutureReader, StreamReader}; + +use crate::exports::componentized::valkey::connection::{Error, GuestSubscription, Subscription}; +use crate::{Inner, State}; + +/// Fails, there are no subscriptions in this build. +pub(crate) fn subscription(_inner: &Rc) -> Result { + Err(Error::Client( + "subscriptions are not supported by this build of the connection, push messages arrive in dead-letters" + .to_string(), + )) +} + +/// A subscription's command waiting for the server, never queued in this build. +pub(crate) enum Pending {} + +impl Pending { + pub(crate) fn refused(self, _subscriptions: &mut Subscriptions, _error: Error) { + match self {} + } + + pub(crate) fn fail(self, _error: Error) { + match self {} + } +} + +/// No subscriptions. +#[derive(Default)] +pub(crate) struct Subscriptions; + +impl Subscriptions { + pub(crate) fn reset(&mut self) {} + + pub(crate) fn close(&mut self, _error: &Error) {} +} + +/// No push message belongs to a subscription, each is a dead letter. +pub(crate) fn claim_push(_state: &mut State, _value: &RespValue) -> bool { + false +} + +/// A subscription, of which this build never makes one. +pub enum SubscriptionImpl {} + +impl GuestSubscription for SubscriptionImpl { + fn targets(&self) -> Vec { + match *self {} + } + + async fn subscribe(&self, _targets: Vec) -> Result<(), Error> { + match *self {} + } + + async fn unsubscribe(&self, _targets: Option>) -> Result<(), Error> { + match *self {} + } + + fn events( + &self, + ) -> ( + StreamReader, + FutureReader>, + ) { + match *self {} + } +} diff --git a/components/connection/tests/connection.rs b/components/connection/tests/connection.rs index 670d2c6..3f64e71 100644 --- a/components/connection/tests/connection.rs +++ b/components/connection/tests/connection.rs @@ -400,7 +400,7 @@ async fn commands_the_connection_manages_are_rejected() -> wasmtime::Result<()> .call_exchange(accessor, connection, words(&["SUBSCRIBE", "raw"])) .await?; assert_eq!(result, Ok(RespValue::Null)); - let _subscription = guest.call_subscription(accessor, connection).await?; + let _subscription = guest.call_subscription(accessor, connection).await??; let result = guest .call_exchange(accessor, connection, words(&["unsubscribe"])) .await?; @@ -425,7 +425,7 @@ async fn subscriptions_are_confirmed_by_push_messages() -> wasmtime::Result<()> let guest = exports.connection(); let subscriptions = exports.subscription(); let (mut transport, connection) = establish(accessor, exports).await?; - let subscription = guest.call_subscription(accessor, connection).await?; + let subscription = guest.call_subscription(accessor, connection).await??; let (events, _ended) = subscriptions.call_events(accessor, subscription).await?; let mut events = receive(accessor, events)?; let dead_letters = guest.call_dead_letters(accessor, connection).await?; @@ -520,7 +520,7 @@ async fn error_replies_fail_a_subscribe() -> wasmtime::Result<()> { let guest = exports.connection(); let subscriptions = exports.subscription(); let (mut transport, connection) = establish(accessor, exports).await?; - let subscription = guest.call_subscription(accessor, connection).await?; + let subscription = guest.call_subscription(accessor, connection).await??; let subscribe = subscriptions.call_subscribe(accessor, subscription, vec![channel("secret")]); let get = guest.call_exchange(accessor, connection, words(&["GET", "a"])); @@ -558,7 +558,7 @@ async fn dropping_a_subscription_unsubscribes() -> wasmtime::Result<()> { let guest = exports.connection(); let subscriptions = exports.subscription(); let (mut transport, connection) = establish(accessor, exports).await?; - let subscription = guest.call_subscription(accessor, connection).await?; + let subscription = guest.call_subscription(accessor, connection).await??; let call = subscriptions.call_subscribe(accessor, subscription, vec![channel("news")]); let server = async { transport.request().await; @@ -812,7 +812,7 @@ async fn mixed_kinds_are_subscribed_with_a_command_each() -> wasmtime::Result<() let guest = exports.connection(); let subscriptions = exports.subscription(); let (mut transport, connection) = establish(accessor, exports).await?; - let subscription = guest.call_subscription(accessor, connection).await?; + let subscription = guest.call_subscription(accessor, connection).await??; let (events, _ended) = subscriptions.call_events(accessor, subscription).await?; let mut events = receive(accessor, events)?; @@ -893,8 +893,8 @@ async fn overlapping_subscriptions_share_the_servers() -> wasmtime::Result<()> { let guest = exports.connection(); let subscriptions = exports.subscription(); let (mut transport, connection) = establish(accessor, exports).await?; - let first = guest.call_subscription(accessor, connection).await?; - let second = guest.call_subscription(accessor, connection).await?; + let first = guest.call_subscription(accessor, connection).await??; + let second = guest.call_subscription(accessor, connection).await??; let (first_events, _) = subscriptions.call_events(accessor, first).await?; let mut first_events = receive(accessor, first_events)?; let (second_events, _) = subscriptions.call_events(accessor, second).await?; @@ -953,7 +953,7 @@ async fn pattern_messages_carry_the_channel() -> wasmtime::Result<()> { let guest = exports.connection(); let subscriptions = exports.subscription(); let (mut transport, connection) = establish(accessor, exports).await?; - let subscription = guest.call_subscription(accessor, connection).await?; + let subscription = guest.call_subscription(accessor, connection).await??; let (events, _) = subscriptions.call_events(accessor, subscription).await?; let mut events = receive(accessor, events)?; let call = subscriptions.call_subscribe(accessor, subscription, vec![pattern("n*")]); @@ -987,7 +987,7 @@ async fn targets_end_by_the_server_reset_and_closing() -> wasmtime::Result<()> { let guest = exports.connection(); let subscriptions = exports.subscription(); let (mut transport, connection) = establish(accessor, exports).await?; - let subscription = guest.call_subscription(accessor, connection).await?; + let subscription = guest.call_subscription(accessor, connection).await??; let (events, ended) = subscriptions.call_events(accessor, subscription).await?; let mut events = receive(accessor, events)?; let targets = vec![shardchannel("orders"), channel("news")]; @@ -1097,7 +1097,7 @@ mod with_valkey { let subscription = exports .connection() .call_subscription(accessor, connection) - .await?; + .await??; let (events, ended) = subscriptions.call_events(accessor, subscription).await?; let mut events = receive(accessor, events)?; @@ -1155,7 +1155,7 @@ mod with_valkey { let other = exports .connection() .call_subscription(accessor, connection) - .await?; + .await??; subscriptions .call_subscribe(accessor, other, vec![channel("t:conn:a")]) .await??; @@ -1223,7 +1223,7 @@ mod with_valkey { let subscription = exports .connection() .call_subscription(accessor, connection) - .await?; + .await??; let targets = vec![ pattern("t:conn:no*"), channel("t:conn:ok"), @@ -1276,7 +1276,7 @@ mod with_valkey { &["SET", "t:conn:reset", "db1"], ) .await??; - let subscription = guest.call_subscription(accessor, connection).await?; + let subscription = guest.call_subscription(accessor, connection).await??; let (events, _ended) = subscriptions.call_events(accessor, subscription).await?; let mut events = receive(accessor, events)?; subscriptions @@ -1402,7 +1402,7 @@ mod with_valkey { let subscription = exports .connection() .call_subscription(accessor, connection) - .await?; + .await??; let (events, ended) = subscriptions.call_events(accessor, subscription).await?; let mut events = receive(accessor, events)?; let targets = vec![channel("t:conn:k"), pattern("t:conn:k*")]; @@ -1430,3 +1430,76 @@ mod with_valkey { .await } } + +/// The tests of `connection-minimal`, the same component built without pub/sub. +mod minimal { + use super::*; + + #[tokio::test(flavor = "multi_thread")] + async fn subscriptions_are_not_supported() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection-minimal").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + let result = guest.call_subscription(accessor, connection).await?; + assert_eq!( + result.err(), + Some(Error::Client( + "subscriptions are not supported by this build of the connection, push messages arrive in dead-letters" + .to_string() + )) + ); + + // SUBSCRIBE and the like may be sent with exchange, every push message is a dead + // letter + let dead_letters = guest.call_dead_letters(accessor, connection).await?; + let mut dead_letters = receive(accessor, dead_letters)?; + let result = guest + .call_exchange(accessor, connection, words(&["SUBSCRIBE", "news"])) + .await?; + assert_eq!(result, Ok(RespValue::Null)); + assert_eq!( + transport.request().await, + Some(command(&["SUBSCRIBE", "news"])) + ); + let confirmation = confirm("subscribe", "news"); + let message = push(vec![bulk("message"), bulk("news"), bulk("hello")]); + transport.reply(confirmation.clone()); + transport.reply(message.clone()); + assert_eq!(dead_letters.recv().await, Some(confirmation)); + assert_eq!(dead_letters.recv().await, Some(message)); + Ok(()) + }) + .await + } + + #[tokio::test(flavor = "multi_thread")] + async fn exchanges_and_resets() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection-minimal").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + let call = guest.call_exchange(accessor, connection, words(&["GET", "a"])); + let server = async { + assert_eq!(transport.request().await, Some(command(&["GET", "a"]))); + transport.reply(bulk("1")); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(bulk("1"))); + + let call = guest.call_reset(accessor, connection); + let server = async { + assert_eq!(transport.request().await, Some(command(&["RESET"]))); + assert_eq!(transport.request().await, Some(command(&["HELLO", "3"]))); + transport.reply(simple("RESET")); + transport.reply(hello_reply()); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(())); + Ok(()) + }) + .await + } +} diff --git a/wit/connection.wit b/wit/connection.wit index 7f04bec..f5da08c 100644 --- a/wit/connection.wit +++ b/wit/connection.wit @@ -110,7 +110,11 @@ interface connection { /// fail with a client error, so the server's subscriptions stay in step with the /// connection's. Subscriptions made with `exchange` before are unsupported. They may keep /// delivering to `dead-letters`, the exact behavior is undocumented. - subscription: func() -> subscription; + /// + /// Fails with a client error when the connection component was built without pub/sub, + /// e.g. `connection-minimal`. Its push messages are then all dead letters, and SUBSCRIBE and + /// the like may still be sent with `exchange`. + subscription: func() -> result; /// The values nothing else claims, kept apart from replies. /// From 365d2bddcbbf53692f4b57c6f2438d0c9b66d43d Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 14 Oct 2026 01:12:59 -0400 Subject: [PATCH 29/37] add docker as a prereq Signed-off-by: Scott Andrews --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4b08bfd..2ae0847 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ A [dev container](https://containers.dev) is available that contains the necessa Prereqs: - a rust toolchain - [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall), optional, to download prebuilt tools instead of building them +- Docker, to run the integration tests `make test-valkey` ```sh make components From 094e1ff1776076409393d22dc2af26930e765498 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 14 Oct 2026 01:32:40 -0400 Subject: [PATCH 30/37] add @since gate Signed-off-by: Scott Andrews --- wit/connection.wit | 2 + wit/tcp.wit | 1 + wit/types.wit | 155 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+) diff --git a/wit/connection.wit b/wit/connection.wit index f5da08c..063264c 100644 --- a/wit/connection.wit +++ b/wit/connection.wit @@ -11,6 +11,7 @@ interface connection { use types.{connect-opts, duration-milliseconds, error, resp-value, subscription-event, subscription-target}; + @since(version = 0.3.0-dev) resource connection { /// Establishes a connection over a transport. /// @@ -163,6 +164,7 @@ interface connection { /// Subscriptions sharing a connection may overlap, the server stays subscribed to a target while /// any subscription has it. The subscription stays open until it is dropped, with or without /// targets or streams, then it unsubscribes from its targets. It keeps its connection open. + @since(version = 0.3.0-dev) resource subscription { /// Subscribes to the targets, resolving once the server confirms each of them, see /// `subscription-event.subscribed`. Targets already subscribed to are left as they are. The diff --git a/wit/tcp.wit b/wit/tcp.wit index ad67a14..bb91828 100644 --- a/wit/tcp.wit +++ b/wit/tcp.wit @@ -5,6 +5,7 @@ interface tcp { /// A TCP connection to a Valkey server, framing the bytes it carries into RESP values in each /// direction. + @since(version = 0.3.0-dev) resource transport { /// Opens a transport to `host`, a hostname or an IP address. Each address the host resolves /// to is tried in turn. diff --git a/wit/types.wit b/wit/types.wit index b187569..b5f8170 100644 --- a/wit/types.wit +++ b/wit/types.wit @@ -7,12 +7,16 @@ interface types { // resp /// An encoding error, converting RESP values to or from bytes. + @since(version = 0.3.0-dev) type resp-error = string; + @since(version = 0.3.0-dev) type integer = s64; + @since(version = 0.3.0-dev) type double = f64; + @since(version = 0.3.0-dev) variant resp-value { %string(string), error(string), @@ -32,11 +36,13 @@ interface types { /// Values nested within an aggregate type. Since WIT types are not allowed to depend on /// themselves, nested values are stored as encoded RESP. + @since(version = 0.3.0-dev) type resp-nested-value = list; // connection /// The errors of a connection and of every command interface. + @since(version = 0.3.0-dev) variant error { /// The transport closed while a reply was outstanding, or before the command was sent. Holds /// the transport's error when it did not close gracefully. @@ -54,6 +60,7 @@ interface types { } /// Options for establishing a connection. + @since(version = 0.3.0-dev) record connect-opts { /// The username and password to authenticate with. auth: option>, @@ -64,6 +71,7 @@ interface types { } /// What a subscription is subscribed to, see `connection.subscription`. + @since(version = 0.3.0-dev) variant subscription-target { /// A channel, see SUBSCRIBE. channel(string), @@ -77,6 +85,7 @@ interface types { /// /// A subscription with overlapping targets, e.g. a channel and a pattern matching it, receives /// a message once for each target. + @since(version = 0.3.0-dev) record subscription-message { payload: string, /// The channel the message was published to: a channel, published with PUBLISH, or a shard @@ -90,6 +99,7 @@ interface types { } /// Why a target is no longer subscribed to. + @since(version = 0.3.0-dev) enum unsubscribe-reason { /// By the subscription, e.g. its `unsubscribe`. requested, @@ -100,6 +110,7 @@ interface types { } /// What a subscription's stream carries, in the order it happened. + @since(version = 0.3.0-dev) variant subscription-event { /// The server confirmed the target, its messages may arrive from now on. subscribed(subscription-target), @@ -111,22 +122,26 @@ interface types { // shared by the commands /// A duration in whole seconds, e.g. `EX seconds`. + @since(version = 0.3.0-dev) record duration-seconds { ex: u64, } /// A duration in whole milliseconds, e.g. `PX milliseconds`. + @since(version = 0.3.0-dev) record duration-milliseconds { px: u64, } /// A point in time as whole seconds since the Unix epoch, e.g. `EXAT unix-time-seconds`. + @since(version = 0.3.0-dev) record unix-time-seconds { exat: s64, } /// A point in time as whole milliseconds since the Unix epoch, e.g. `PXAT /// unix-time-milliseconds`. + @since(version = 0.3.0-dev) record unix-time-milliseconds { pxat: s64, } @@ -135,6 +150,7 @@ interface types { /// /// A scan returns the cursor to resume from, always `resume`, none once the scan is complete. /// Pass it to the next call until then. + @since(version = 0.3.0-dev) variant cursor { /// Starts a new scan, the cursor `0`. start, @@ -144,6 +160,7 @@ interface types { } /// The condition for setting an expiration, e.g. with EXPIRE or HEXPIRE. + @since(version = 0.3.0-dev) enum ttl-mode { /// Set expiration only when the field has no expiration. nx, @@ -156,6 +173,7 @@ interface types { } /// The remaining time to live of a key or hash field, to the second, see `ttl` and `httl`. + @since(version = 0.3.0-dev) variant ttl-reply { /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. does-not-exist, @@ -166,9 +184,11 @@ interface types { } /// The remaining time to live of a field, see `httl`. + @since(version = 0.3.0-dev) type httl-reply = ttl-reply; /// The remaining time to live of a key or hash field, to the millisecond, see `pttl` and `hpttl`. + @since(version = 0.3.0-dev) variant pttl-reply { /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. does-not-exist, @@ -179,9 +199,11 @@ interface types { } /// The remaining time to live of a field, see `hpttl`. + @since(version = 0.3.0-dev) type hpttl-reply = pttl-reply; /// When a key or hash field expires, to the second, see `expiretime` and `hexpiretime`. + @since(version = 0.3.0-dev) variant expiretime-reply { /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. does-not-exist, @@ -192,9 +214,11 @@ interface types { } /// The expiration time of a field, see `hexpiretime`. + @since(version = 0.3.0-dev) type hexpiretime-reply = expiretime-reply; /// When a key or hash field expires, to the millisecond, see `pexpiretime` and `hpexpiretime`. + @since(version = 0.3.0-dev) variant pexpiretime-reply { /// -2: the key does not exist, or for a hash field, the field or the hash does not exist. does-not-exist, @@ -205,9 +229,11 @@ interface types { } /// The expiration time of a field, see `hpexpiretime`. + @since(version = 0.3.0-dev) type hpexpiretime-reply = pexpiretime-reply; /// A range of a sorted result, `LIMIT offset count`. + @since(version = 0.3.0-dev) record limit { /// The number of elements skipped. offset: integer, @@ -215,6 +241,7 @@ interface types { count: integer, } + @since(version = 0.3.0-dev) enum sort-order { /// ascending, smallest to largest. asc, @@ -225,6 +252,7 @@ interface types { // admin commands /// Whether a user may run a command, see `acl-dryrun`. + @since(version = 0.3.0-dev) variant acl-dryrun-result { /// The user may run the command. allowed, @@ -233,6 +261,7 @@ interface types { } /// A security event, a command or authentication the rules denied, see `acl-log`. + @since(version = 0.3.0-dev) record acl-log-entry { /// The number of events like this one, within 60 seconds of each other. count: integer, @@ -256,6 +285,7 @@ interface types { } /// What BGSAVE does. + @since(version = 0.3.0-dev) enum bgsave-operation { /// `SCHEDULE`: saves once the rewrite of the append-only file in progress is done. schedule, @@ -264,6 +294,7 @@ interface types { } /// The type of a connection, for the `client-filter`. + @since(version = 0.3.0-dev) enum client-type { /// `normal` normal, @@ -276,6 +307,7 @@ interface types { } /// A filter of connections, for CLIENT KILL and CLIENT LIST. + @since(version = 0.3.0-dev) variant client-filter { /// `ID client-id [ client-id ... ]` id(list), @@ -334,6 +366,7 @@ interface types { } /// Which commands CLIENT PAUSE suspends. + @since(version = 0.3.0-dev) enum client-pause-mode { /// `WRITE`: commands that may write. write, @@ -342,6 +375,7 @@ interface types { } /// How CLIENT UNBLOCK unblocks a connection. + @since(version = 0.3.0-dev) enum client-unblock-type { /// `TIMEOUT`: as if the command timed out, the default. timeout, @@ -350,6 +384,7 @@ interface types { } /// The outcome of CLUSTER BUMPEPOCH. + @since(version = 0.3.0-dev) variant cluster-bumpepoch-result { /// `BUMPED`: the config epoch was advanced, to this one. bumped(integer), @@ -358,6 +393,7 @@ interface types { } /// How CLUSTER FAILOVER fails over. + @since(version = 0.3.0-dev) enum cluster-failover-mode { /// `FORCE`: without the agreement of the primary, which may be down. force, @@ -366,6 +402,7 @@ interface types { } /// Which way a slot migration moves the slots. + @since(version = 0.3.0-dev) enum cluster-getslotmigrations-operation { /// `IMPORT`: to this node. %import, @@ -374,6 +411,7 @@ interface types { } /// A slot migration, see `cluster-getslotmigrations`. + @since(version = 0.3.0-dev) record cluster-getslotmigrations-entry { /// The ID of the migration. link-name: string, @@ -396,6 +434,7 @@ interface types { } /// Ranges of slots to migrate to a node, see `cluster-migrateslots`. + @since(version = 0.3.0-dev) record cluster-migrateslots-group { /// The ranges of slots, start and end both included. ranges: list>, @@ -406,6 +445,7 @@ interface types { } /// How CLUSTER RESET resets a node. + @since(version = 0.3.0-dev) enum cluster-reset-mode { /// `HARD`: also generates a new node ID and resets the epochs. hard, @@ -414,6 +454,7 @@ interface types { } /// The state to set a slot to, see `cluster-setslot`. + @since(version = 0.3.0-dev) variant cluster-setslot-state { /// `IMPORTING node-id`: importing the slot from the node. importing(string), @@ -426,6 +467,7 @@ interface types { } /// A command log, see `commandlog-get`. + @since(version = 0.3.0-dev) enum commandlog-type { /// `slow`: commands that took longer than `commandlog-execution-slower-than`. slow, @@ -436,6 +478,7 @@ interface types { } /// An entry of the slow log or a command log. + @since(version = 0.3.0-dev) record commandlog-entry { /// The ID of the entry, unique until the server restarts. id: integer, @@ -453,9 +496,11 @@ interface types { } /// An entry of the slow log, see `slowlog-get`. + @since(version = 0.3.0-dev) type slowlog-entry = commandlog-entry; /// Where FAILOVER fails over to. + @since(version = 0.3.0-dev) record failover-target { host: string, port: u16, @@ -464,6 +509,7 @@ interface types { } /// The latency histogram of a command, see `latency-histogram`. + @since(version = 0.3.0-dev) record latency-histogram-entry { /// The number of times the command ran. calls: integer, @@ -473,6 +519,7 @@ interface types { } /// The latest sample of the latency of an event, see `latency-latest`. + @since(version = 0.3.0-dev) record latency-latest-entry { event: string, /// When the latest sample was taken. @@ -487,6 +534,7 @@ interface types { } /// A loaded module, see `module-list`. + @since(version = 0.3.0-dev) record module-list-entry { name: string, /// The module's version. @@ -498,6 +546,7 @@ interface types { } /// A replica of a primary, see `role`. + @since(version = 0.3.0-dev) record role-primary-replica { ip: string, port: u16, @@ -506,6 +555,7 @@ interface types { } /// The replication state of a primary, see `role`. + @since(version = 0.3.0-dev) record role-primary { /// The primary's replication offset. offset: integer, @@ -513,6 +563,7 @@ interface types { } /// The replication state of a replica, see `role`. + @since(version = 0.3.0-dev) record role-replica { primary-host: string, primary-port: u16, @@ -523,6 +574,7 @@ interface types { } /// The server's role in replication, see `role`. + @since(version = 0.3.0-dev) variant role-reply { /// `master` primary(role-primary), @@ -533,6 +585,7 @@ interface types { } /// The modifiers of SHUTDOWN. + @since(version = 0.3.0-dev) flags shutdown-flags { /// `NOW`: does not wait for lagging replicas. now, @@ -545,6 +598,7 @@ interface types { } /// Whether SHUTDOWN saves to disk. + @since(version = 0.3.0-dev) enum shutdown-save { /// `NOSAVE`: does not save, even with save points configured. nosave, @@ -553,6 +607,7 @@ interface types { } /// A user and its rules, see `acl-list`. + @since(version = 0.3.0-dev) record acl-list-entry { username: string, /// The user's rules, as the server normalized them, e.g. `allkeys` is `~*`. Applied with @@ -563,6 +618,7 @@ interface types { /// A rule of an ACL user, see `acl-setuser`. /// /// https://valkey.io/topics/acl/ + @since(version = 0.3.0-dev) variant acl-rule { /// The keys the user may access. keys(acl-key-rule), @@ -580,6 +636,7 @@ interface types { /// A rule of a selector, a set of permissions matched independently of the user's root permissions, /// the user may run a command when either permits it. + @since(version = 0.3.0-dev) variant acl-selector-rule { /// The keys the selector permits access to. keys(acl-key-rule), @@ -594,6 +651,7 @@ interface types { } /// A rule for the keys a user may access. + @since(version = 0.3.0-dev) variant acl-key-rule { /// `~`: allows reading and writing the keys matching the glob-style pattern. add(string), @@ -611,6 +669,7 @@ interface types { } /// A rule for the Pub/Sub channels a user may access. + @since(version = 0.3.0-dev) variant acl-channel-rule { /// `&`: allows access to the Pub/Sub channels matching the glob-style pattern. add(string), @@ -622,6 +681,7 @@ interface types { } /// A rule for the commands a user may call. + @since(version = 0.3.0-dev) variant acl-command-rule { /// `+`: allows the command, or a subcommand, e.g. `config|get`. add(string), @@ -642,6 +702,7 @@ interface types { } /// A rule for the databases a user may access. + @since(version = 0.3.0-dev) variant acl-db-rule { /// `db=[,...]`: allows access to the databases with the given IDs. db(list), @@ -652,6 +713,7 @@ interface types { } /// A rule for the user itself. + @since(version = 0.3.0-dev) variant acl-user-rule { /// `on`: enables the user, it is possible to authenticate as this user. on, @@ -685,6 +747,7 @@ interface types { // bitmap commands /// A range of a string, negative from the end, for BITCOUNT and BITPOS. + @since(version = 0.3.0-dev) variant bit-range { /// `start`: the bytes from the start to the end of the string. %from(integer), @@ -695,6 +758,7 @@ interface types { } /// The type of an integer field of a string, for BITFIELD. + @since(version = 0.3.0-dev) variant bitfield-encoding { /// `i1` to `i64`: a signed integer of the width in bits. signed(u8), @@ -703,6 +767,7 @@ interface types { } /// Where an integer field of a string starts, for BITFIELD. + @since(version = 0.3.0-dev) variant bitfield-offset { /// The offset in bits. bit(u64), @@ -712,6 +777,7 @@ interface types { } /// What a write does when the field overflows, `OVERFLOW`. + @since(version = 0.3.0-dev) enum bitfield-overflow { /// `WRAP`: wraps around, the default. wrap, @@ -722,15 +788,18 @@ interface types { } /// `GET encoding offset`: the value of the field. + @since(version = 0.3.0-dev) record bitfield-get { encoding: bitfield-encoding, offset: bitfield-offset, } /// A GET operation, see `bitfield-ro`. + @since(version = 0.3.0-dev) type bitfield-ro-get = bitfield-get; /// `SET encoding offset value`: sets the field, the result is its previous value. + @since(version = 0.3.0-dev) record bitfield-set { encoding: bitfield-encoding, offset: bitfield-offset, @@ -738,6 +807,7 @@ interface types { } /// `INCRBY encoding offset increment`: increments the field, the result is its new value. + @since(version = 0.3.0-dev) record bitfield-incrby { encoding: bitfield-encoding, offset: bitfield-offset, @@ -748,6 +818,7 @@ interface types { /// /// A write is preceded by an optional overflow mode, `OVERFLOW WRAP | SAT | FAIL`, for it and /// the following writes. None keeps the current mode, `WRAP` at the start of the command. + @since(version = 0.3.0-dev) variant bitfield-operation { /// `GET encoding offset` get(bitfield-get), @@ -758,6 +829,7 @@ interface types { } /// A bitwise operation of BITOP, and its source keys. + @since(version = 0.3.0-dev) variant bitop-operation { /// `AND` and(list), @@ -772,6 +844,7 @@ interface types { // connection commands /// How a key spec finds the first key, see `command-key-spec`. + @since(version = 0.3.0-dev) variant command-key-spec-begin-search { /// `index`: the argument at the index. index(integer), @@ -782,6 +855,7 @@ interface types { } /// The keyword before the first key, see `command-key-spec-begin-search`. + @since(version = 0.3.0-dev) record command-key-spec-keyword { keyword: string, /// The index of the argument to start searching from, from the end when negative. @@ -789,6 +863,7 @@ interface types { } /// How a key spec finds the keys after the first one, see `command-key-spec`. + @since(version = 0.3.0-dev) variant command-key-spec-find-keys { /// `range`: a range of arguments. range(command-key-spec-range), @@ -799,6 +874,7 @@ interface types { } /// A range of keys, see `command-key-spec-find-keys`. + @since(version = 0.3.0-dev) record command-key-spec-range { /// The index of the last key, relative to the first, from the end when negative. lastkey: integer, @@ -809,6 +885,7 @@ interface types { } /// A number of keys, see `command-key-spec-find-keys`. + @since(version = 0.3.0-dev) record command-key-spec-keynum { /// The index of the argument with the number of keys, relative to the first. keynumidx: integer, @@ -819,6 +896,7 @@ interface types { } /// Where a command's keys are and how it uses them. + @since(version = 0.3.0-dev) record command-key-spec { notes: option, /// How the command uses the keys, e.g. `RW`, `access` and `update`. @@ -828,6 +906,7 @@ interface types { } /// A command, see `command` and `command-info`. + @since(version = 0.3.0-dev) record command-info-reply { /// The command's name, e.g. `get`, or for a subcommand, `config|get`. name: string, @@ -849,9 +928,11 @@ interface types { } /// The details of a command, see `command`. + @since(version = 0.3.0-dev) type command-reply = command-info-reply; /// The type of an argument of a command, see `command-docs-argument`. + @since(version = 0.3.0-dev) variant command-docs-argument-type { /// `string` %string, @@ -876,6 +957,7 @@ interface types { } /// An argument of a command, see `command-docs`. + @since(version = 0.3.0-dev) record command-docs-argument { /// The index of the `oneof` or `block` argument containing this one, in the command's /// arguments, none at the top level. @@ -897,6 +979,7 @@ interface types { } /// The documentation of a command, see `command-docs`. + @since(version = 0.3.0-dev) record command-docs-reply { /// The command's name, e.g. `get`, or for a subcommand, `config|get`. name: string, @@ -921,6 +1004,7 @@ interface types { } /// Information about the library of a connection, see `client-setinfo`. + @since(version = 0.3.0-dev) variant client-setinfo-attr { /// `LIB-NAME`: the name of the library, e.g. `valkey-wasm`. lib-name(string), @@ -929,6 +1013,7 @@ interface types { } /// Which keys tracking tracks without CLIENT CACHING. + @since(version = 0.3.0-dev) enum client-tracking-mode { /// `OPTIN`: none, only those of commands after CLIENT CACHING YES. optin, @@ -937,6 +1022,7 @@ interface types { } /// The options of CLIENT TRACKING ON. + @since(version = 0.3.0-dev) record client-tracking-options { /// `PREFIX prefix`: with `bcast`, only notifies about keys with the prefixes. prefixes: option>, @@ -951,6 +1037,7 @@ interface types { /// /// Redirecting is for RESP2, which the connection does not use, but CLIENT TRACKING REDIRECT may /// still be sent with `connection.exchange`. + @since(version = 0.3.0-dev) variant client-trackinginfo-redirect { /// -1: tracking is not on. not-tracking, @@ -961,6 +1048,7 @@ interface types { } /// A connection's tracking state, see `client-trackinginfo`. + @since(version = 0.3.0-dev) record client-trackinginfo-reply { /// The tracking flags, e.g. `on`, `bcast`, `optin` or `noloop`. %flags: list, @@ -971,6 +1059,7 @@ interface types { } /// The filter of COMMAND LIST. + @since(version = 0.3.0-dev) variant command-list-filter { /// `MODULE module-name`: the commands of the module. module(string), @@ -985,6 +1074,7 @@ interface types { /// A position on the Earth. /// /// The exact limits, as specified by EPSG:900913 / EPSG:3785 / OSGEO:41001 + @since(version = 0.3.0-dev) record geo-coordinates { /// The longitude, from -180 to 180 degrees. longitude: double, @@ -993,6 +1083,7 @@ interface types { } /// A unit of distance. + @since(version = 0.3.0-dev) enum geo-unit { /// `m`: meters. m, @@ -1005,6 +1096,7 @@ interface types { } /// Only add or only update members, GEOADD's `NX` and `XX`. + @since(version = 0.3.0-dev) enum geoadd-condition { /// Only add new members, never update existing ones. nx, @@ -1013,6 +1105,7 @@ interface types { } /// The center of a search, `FROMMEMBER` or `FROMLONLAT`. + @since(version = 0.3.0-dev) variant geosearch-from { /// `FROMMEMBER member`: the position of a member of the index. member(string), @@ -1021,6 +1114,7 @@ interface types { } /// `BYRADIUS`: a circle around the center. + @since(version = 0.3.0-dev) record geosearch-radius { %from: geosearch-from, radius: double, @@ -1028,6 +1122,7 @@ interface types { } /// `BYBOX`: a rectangle around the center. + @since(version = 0.3.0-dev) record geosearch-box { %from: geosearch-from, width: double, @@ -1036,6 +1131,7 @@ interface types { } /// The area of a search. + @since(version = 0.3.0-dev) variant geosearch-shape { /// `[ FROMMEMBER member | FROMLONLAT longitude latitude ] BYRADIUS radius unit` radius(geosearch-radius), @@ -1047,9 +1143,11 @@ interface types { } /// The area to search, see `geosearchstore`. + @since(version = 0.3.0-dev) type geosearchstore-shape = geosearch-shape; /// At most a count of the members, `COUNT count [ ANY ]`. + @since(version = 0.3.0-dev) record geosearch-count { count: integer, /// `ANY`: returns as soon as enough members are found, rather than the nearest ones. @@ -1057,9 +1155,11 @@ interface types { } /// The number of members to store, see `geosearchstore`. + @since(version = 0.3.0-dev) type geosearchstore-count = geosearch-count; /// The details GEOSEARCH returns of each member it found. + @since(version = 0.3.0-dev) flags geosearch-with-flags { /// `WITHCOORD`: the position. withcoord, @@ -1070,6 +1170,7 @@ interface types { } /// A member a search found, with the details asked for. + @since(version = 0.3.0-dev) record geosearch-with-result { member: string, /// `WITHDIST`: the distance from the center, in the unit of the shape. @@ -1083,6 +1184,7 @@ interface types { // hash commands /// The outcome of removing the expiration of a hash field. + @since(version = 0.3.0-dev) enum hpersist-result { /// -2: the field does not exist, or the hash does not exist. does-not-exist, @@ -1093,6 +1195,7 @@ interface types { } /// Only set the fields when the hash does, or does not, exist, HSETEX's `NX` and `XX`. + @since(version = 0.3.0-dev) enum hsetex-condition { /// Only set the fields when the hash does not exist. nx, @@ -1101,6 +1204,7 @@ interface types { } /// Only set the fields when none, or all, of them exist, HSETEX's `FNX` and `FXX`. + @since(version = 0.3.0-dev) enum hsetex-field-condition { /// Only set the fields when none of them exist. fnx, @@ -1109,6 +1213,7 @@ interface types { } /// The outcome of setting the expiration of a hash field, e.g. with HEXPIRE. + @since(version = 0.3.0-dev) enum hexpire-result { /// -2: key does not exist, or field does not exist in the HASH. does-not-exist, @@ -1121,17 +1226,21 @@ interface types { } /// The result for a field, see `hexpireat`. + @since(version = 0.3.0-dev) type hexpireat-result = hexpire-result; /// The result for a field, see `hpexpire`. + @since(version = 0.3.0-dev) type hpexpire-result = hexpire-result; /// The result for a field, see `hpexpireat`. + @since(version = 0.3.0-dev) type hpexpireat-result = hexpire-result; // keyspace commands /// The type of the value stored at a key. + @since(version = 0.3.0-dev) variant key-type { /// `string` %string, @@ -1150,6 +1259,7 @@ interface types { } /// The options of MIGRATE. + @since(version = 0.3.0-dev) flags migrate-flags { /// `COPY`: keeps the keys in this instance. copy, @@ -1158,6 +1268,7 @@ interface types { } /// The credentials to authenticate to the other instance with, for MIGRATE. + @since(version = 0.3.0-dev) variant migrate-auth { /// `AUTH password` password(string), @@ -1166,6 +1277,7 @@ interface types { } /// How SORT orders and projects the elements of a list, set or sorted set. + @since(version = 0.3.0-dev) record sort-opts { /// `BY pattern`: sorts by the values of the keys the pattern names, `*` replaced with each /// element, or `nosort` to skip sorting. @@ -1182,11 +1294,13 @@ interface types { } /// The options of the sort, see `sort-ro`. + @since(version = 0.3.0-dev) type sort-ro-opts = sort-opts; // list commands /// An end of a list. + @since(version = 0.3.0-dev) enum list-side { /// `LEFT`: the head, the first element. left, @@ -1195,6 +1309,7 @@ interface types { } /// Where LINSERT inserts an element, relative to the pivot. + @since(version = 0.3.0-dev) enum linsert-position { /// `BEFORE`: in front of the pivot. before, @@ -1203,6 +1318,7 @@ interface types { } /// The outcome of LINSERT. + @since(version = 0.3.0-dev) variant linsert-result { /// The element was inserted, the length of the list after it. inserted(integer), @@ -1215,6 +1331,7 @@ interface types { // scripting commands /// Whether a flush happens in the background, `ASYNC`, or before replying, `SYNC`. + @since(version = 0.3.0-dev) enum flush-mode { /// `ASYNC` %async, @@ -1223,6 +1340,7 @@ interface types { } /// A function of a library. + @since(version = 0.3.0-dev) record function-list-function { name: string, description: option, @@ -1231,6 +1349,7 @@ interface types { } /// A library of functions. + @since(version = 0.3.0-dev) record function-list-library { library-name: string, /// The engine that runs the library, e.g. `LUA`. @@ -1241,6 +1360,7 @@ interface types { } /// The function that is running. + @since(version = 0.3.0-dev) record function-stats-running { name: string, /// The command and its arguments that called the function. @@ -1250,12 +1370,14 @@ interface types { } /// The libraries and functions of an engine. + @since(version = 0.3.0-dev) record function-stats-engine { libraries-count: integer, functions-count: integer, } /// The function that is running, and the libraries and functions of each engine. + @since(version = 0.3.0-dev) record function-stats-reply { /// None when no function is running. running-script: option, @@ -1266,6 +1388,7 @@ interface types { // sortedset commands /// A bound of a range of scores. + @since(version = 0.3.0-dev) variant score-bound { /// The score, included in the range. inclusive(double), @@ -1276,6 +1399,7 @@ interface types { } /// A bound of a range of members, for members of equal score, ordered lexicographically. + @since(version = 0.3.0-dev) variant lex-bound { /// `[member`: the member, included in the range. inclusive(string), @@ -1286,6 +1410,7 @@ interface types { } /// Which end of a sorted set to pop from. + @since(version = 0.3.0-dev) enum zmpop-end { /// The members with the lowest scores. min, @@ -1294,9 +1419,11 @@ interface types { } /// The end to pop from, see `bzmpop`. + @since(version = 0.3.0-dev) type bzmpop-end = zmpop-end; /// How scores combine for a member in several sorted sets. + @since(version = 0.3.0-dev) enum zset-aggregate { /// The sum of the weighted scores, the default. sum, @@ -1308,6 +1435,7 @@ interface types { /// The range of ZRANGE and ZRANGESTORE, `start` and `stop`, given as they are sent: with `REV`, /// the higher bound first. + @since(version = 0.3.0-dev) variant zrange-by { /// Positions, both included, negative from the end. rank(tuple), @@ -1318,9 +1446,11 @@ interface types { } /// The range of members to store, see `zrangestore`. + @since(version = 0.3.0-dev) type zrangestore-by = zrange-by; /// The range of ZRANGE WITHSCORES, which does not support BYLEX, see `zrange-by`. + @since(version = 0.3.0-dev) variant zrange-withscores-by { /// Positions, both included, negative from the end. rank(tuple), @@ -1329,6 +1459,7 @@ interface types { } /// Only update or only add members, ZADD's `NX` and `XX`. + @since(version = 0.3.0-dev) enum zadd-condition { /// Only add new members, never update existing ones. nx, @@ -1337,6 +1468,7 @@ interface types { } /// Only update a member's score when the new score is greater or less, ZADD's `GT` and `LT`. + @since(version = 0.3.0-dev) enum zadd-comparison { /// Only update when the new score is greater than the current one. gt, @@ -1349,9 +1481,11 @@ interface types { /// The ID of a stream entry, `-`, e.g. `1526919030474-55`. /// /// Commands that add an entry also accept `*`, for the server to generate the ID. + @since(version = 0.3.0-dev) type stream-id = string; /// How a stream is trimmed, e.g. with XADD or XTRIM. + @since(version = 0.3.0-dev) variant stream-trim { /// Evicts entries as long as the stream's length exceeds the specified threshold, where threshold is a positive integer. maxlen(stream-trim-maxlen), @@ -1360,6 +1494,7 @@ interface types { } /// Trims the stream to a maximum length, `MAXLEN [ = | ~ ] threshold [ LIMIT count ]`. + @since(version = 0.3.0-dev) record stream-trim-maxlen { /// Exact, `=`, when none. efficiency: option, @@ -1370,6 +1505,7 @@ interface types { } /// Trims the entries with lower IDs, `MINID [ = | ~ ] threshold [ LIMIT count ]`. + @since(version = 0.3.0-dev) record stream-trim-minid { /// Exact, `=`, when none. efficiency: option, @@ -1381,6 +1517,7 @@ interface types { /// How exactly a stream is trimmed. Nearly exact trimming is more efficient, the stream may keep a /// few entries more than the threshold. + @since(version = 0.3.0-dev) enum stream-trim-efficiency { /// `=`: exact trimming exact, @@ -1389,6 +1526,7 @@ interface types { } /// Options of XCLAIM. + @since(version = 0.3.0-dev) record xclaim-opts { /// `IDLE ms`: sets the idle time of the entries, the time since they were last delivered, instead /// of resetting it. @@ -1405,6 +1543,7 @@ interface types { } /// A consumer of a group, see `xinfo-consumers`. + @since(version = 0.3.0-dev) record xinfo-consumers-entry { /// The consumer's name. name: string, @@ -1419,6 +1558,7 @@ interface types { } /// A consumer group of a stream, see `xinfo-groups`. + @since(version = 0.3.0-dev) record xinfo-groups-entry { /// The group's name. name: string, @@ -1436,6 +1576,7 @@ interface types { } /// Information about a stream, see `xinfo-stream`. + @since(version = 0.3.0-dev) record xinfo-stream-reply { /// The number of entries in the stream. length: integer, @@ -1460,6 +1601,7 @@ interface types { } /// Information about a stream, its entries and consumer groups, see `xinfo-stream-full`. + @since(version = 0.3.0-dev) record xinfo-stream-reply-full { /// The number of entries in the stream. length: integer, @@ -1480,6 +1622,7 @@ interface types { } /// A consumer group of a stream, with its pending entries and consumers. + @since(version = 0.3.0-dev) record xinfo-stream-reply-full-group { /// The group's name. name: string, @@ -1500,6 +1643,7 @@ interface types { } /// A consumer of a group, with its pending entries. + @since(version = 0.3.0-dev) record xinfo-stream-reply-full-consumer { /// The consumer's name. name: string, @@ -1516,6 +1660,7 @@ interface types { } /// The pending entries of a consumer group, see `xpending`. + @since(version = 0.3.0-dev) record xpending-summary { /// The number of pending entries. count: integer, @@ -1528,6 +1673,7 @@ interface types { } /// The pending entries of a consumer group, see `xpending-extended`. + @since(version = 0.3.0-dev) record xpending-extended-entry { /// The ID of the message. id: stream-id, @@ -1542,6 +1688,7 @@ interface types { // string commands /// The expiration to set on a key, see `getex`. + @since(version = 0.3.0-dev) variant getex-ttl { /// Set the specified expire time, in seconds. ex(duration-seconds), @@ -1556,9 +1703,11 @@ interface types { } /// The expiration to set on the fields, see `hgetex`. + @since(version = 0.3.0-dev) type hgetex-ttl = getex-ttl; /// The longest common subsequence of two strings, as the ranges matching in each, see `lcs-idx`. + @since(version = 0.3.0-dev) record lcs-idx-reply { /// The ranges that match, from the end of the strings to their start. matches: list, @@ -1567,6 +1716,7 @@ interface types { } /// A part of the longest common subsequence. + @since(version = 0.3.0-dev) record lcs-idx-match { /// The inclusive start and end positions of the match in the first key's string. key1-range: tuple, @@ -1577,6 +1727,7 @@ interface types { } /// The condition for setting the keys, see `msetex`. + @since(version = 0.3.0-dev) variant msetex-precondition { /// Only set the keys if all of them do not already exist. nx, @@ -1585,9 +1736,11 @@ interface types { } /// The expiration to set on the keys, see `msetex`. + @since(version = 0.3.0-dev) type msetex-ttl = set-ttl; /// The condition for setting a key, see `set`. + @since(version = 0.3.0-dev) variant set-precondition { /// Only set the key if it does not already exist. nx, @@ -1600,6 +1753,7 @@ interface types { } /// The expiration to set on a key, see `set`. + @since(version = 0.3.0-dev) variant set-ttl { /// Set the specified expire time, in seconds. ex(duration-seconds), @@ -1614,5 +1768,6 @@ interface types { } /// The expiration to set on the fields, see `hsetex`. + @since(version = 0.3.0-dev) type hsetex-ttl = set-ttl; } From fef687967b44cf6eeb7026bc75ad46b153148a23 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 14 Oct 2026 16:05:46 -0400 Subject: [PATCH 31/37] Add binary support BulkStrings cary double duty as actual utf-8 encoded strings and binary blobs. we now handle both cases. Any value that can be decodded as a string is returned as a string, otherwise the bytes are provided. When sending commands, either form is acceptable. Command values that are aways strings remain strings. Values that are always binary are list and values that may alternate depending on the content or context are the string-or-bytes variants. Users will need to detect which form was returned. Signed-off-by: Scott Andrews --- components/as-keyvalue/src/lib.rs | 13 +- components/commands/src/lib.rs | 473 ++++++++++++------ components/connection/src/lib.rs | 136 +++-- components/connection/src/pubsub.rs | 74 ++- components/connection/tests/connection.rs | 224 ++++++++- components/dep-cli/src/lib.rs | 153 +++--- components/dep-test-commands/src/lib.rs | 344 ++++++++++--- components/tcp/src/lib.rs | 11 +- crates/componentized-valkey/src/command.rs | 52 +- .../src/commands/admin.rs | 66 +-- .../src/commands/bitmap.rs | 20 +- .../src/commands/connection.rs | 12 +- .../componentized-valkey/src/commands/geo.rs | 32 +- .../componentized-valkey/src/commands/hash.rs | 8 +- .../src/commands/keyspace.rs | 10 +- .../componentized-valkey/src/commands/list.rs | 6 +- .../src/commands/scripting.rs | 40 +- .../src/commands/shared.rs | 26 +- .../src/commands/sortedset.rs | 50 +- .../src/commands/stream.rs | 17 +- .../src/commands/string.rs | 14 +- crates/componentized-valkey/src/lib.rs | 1 + crates/componentized-valkey/src/resp.rs | 182 ++++++- .../src/string_or_bytes.rs | 102 ++++ crates/componentized-valkey/src/test_util.rs | 4 +- crates/componentized-valkey/src/value.rs | 72 ++- crates/test-harness/src/lib.rs | 18 +- wit/commands.wit | 277 +++++----- wit/connection.wit | 10 +- wit/types.wit | 55 +- 30 files changed, 1786 insertions(+), 716 deletions(-) create mode 100644 crates/componentized-valkey/src/string_or_bytes.rs diff --git a/components/as-keyvalue/src/lib.rs b/components/as-keyvalue/src/lib.rs index 384c0cd..a15b4e3 100644 --- a/components/as-keyvalue/src/lib.rs +++ b/components/as-keyvalue/src/lib.rs @@ -1,4 +1,4 @@ -use componentized::valkey::commands::{self, Cursor, DurationMilliseconds}; +use componentized::valkey::commands::{self, Cursor, DurationMilliseconds, StringOrBytes}; use componentized::valkey::connection::{Connection, Error as ValkeyError}; use componentized::valkey::connector; use exports::wasmcloud::keyvalue::atomics::Guest as AtomicsGuest; @@ -43,10 +43,11 @@ struct KeyvalueToValkeyBucket { impl GuestBucket for KeyvalueToValkeyBucket { async fn get(&self, key: String) -> Result>, Error> { - match commands::hget(&self.connection, self.hash_key.clone(), key).await? { - Some(value) => Ok(Some(value.into_bytes())), - None => Ok(None), - } + let value = commands::hget(&self.connection, self.hash_key.clone(), key).await?; + Ok(value.map(|value| match value { + StringOrBytes::String(value) => value.into_bytes(), + StringOrBytes::Bytes(value) => value, + })) } async fn set( @@ -55,7 +56,7 @@ impl GuestBucket for KeyvalueToValkeyBucket { value: Vec, options: Option, ) -> Result<(), Error> { - let value = String::from_utf8(value).map_err(|e| Error::Other(e.to_string()))?; + let value = StringOrBytes::Bytes(value); let options = options.unwrap_or(SetOptions { ttl_ms: None, if_not_exists: false, diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index 79662ca..fd1fb93 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -1,19 +1,21 @@ use crate::exports::componentized::valkey::commands::{ AclDryrunResult, AclListEntry, AclLogEntry, AclRule, BgsaveOperation, BitRange, BitfieldGet, - BitfieldOperation, BitopOperation, ClientFilter, ClientPauseMode, ClientSetinfoAttr, - ClientTrackingOptions, ClientTrackinginfoReply, ClientUnblockType, ClusterBumpepochResult, - ClusterFailoverMode, ClusterGetslotmigrationsEntry, ClusterMigrateslotsGroup, ClusterResetMode, - ClusterSetslotState, CommandDocsReply, CommandInfoReply, CommandListFilter, CommandlogEntry, - CommandlogType, Connection, Cursor, Double, DurationMilliseconds, DurationSeconds, Error, - ExpiretimeReply, FailoverTarget, FlushMode, FunctionListLibrary, FunctionStatsReply, - GeoCoordinates, GeoUnit, GeoaddCondition, GeosearchCount, GeosearchShape, GeosearchWithFlags, - GeosearchWithResult, GetexTtl, Guest, HexpireResult, HpersistResult, HsetexCondition, - HsetexFieldCondition, Integer, KeyType, LatencyHistogramEntry, LatencyLatestEntry, LcsIdxReply, - LexBound, Limit, LinsertPosition, LinsertResult, ListSide, MigrateAuth, MigrateFlags, - ModuleListEntry, MsetexPrecondition, MsetexTtl, PexpiretimeReply, PttlReply, RespValue, - RoleReply, ScoreBound, SetPrecondition, SetTtl, ShutdownFlags, ShutdownSave, SortOpts, - SortOrder, StreamId, StreamTrim, TtlMode, TtlReply, UnixTimeMilliseconds, UnixTimeSeconds, - XclaimOpts, XinfoConsumersEntry, XinfoGroupsEntry, XinfoStreamReply, XinfoStreamReplyFull, + BitfieldOperation, BitopOperation, Bytes, BzmpopEnd, ClientFilter, ClientPauseMode, + ClientSetinfoAttr, ClientTrackingOptions, ClientTrackinginfoReply, ClientUnblockType, + ClusterBumpepochResult, ClusterFailoverMode, ClusterGetslotmigrationsEntry, + ClusterMigrateslotsGroup, ClusterResetMode, ClusterSetslotState, CommandDocsReply, + CommandInfoReply, CommandListFilter, CommandlogEntry, CommandlogType, Connection, Cursor, + Double, DurationMilliseconds, DurationSeconds, Error, ExpiretimeReply, FailoverTarget, + FlushMode, FunctionListLibrary, FunctionRestorePolicy, FunctionStatsReply, GeoCoordinates, + GeoUnit, GeoaddCondition, GeosearchCount, GeosearchShape, GeosearchWithFlags, + GeosearchWithResult, GetexTtl, Guest, HexpireResult, HgetexTtl, HpersistResult, + HsetexCondition, HsetexFieldCondition, HsetexTtl, Integer, KeyType, LatencyHistogramEntry, + LatencyLatestEntry, LcsIdxReply, LexBound, Limit, LinsertPosition, LinsertResult, ListSide, + MigrateAuth, MigrateFlags, ModuleListEntry, MsetexPrecondition, MsetexTtl, PexpiretimeReply, + PttlReply, RespValue, RestoreTtl, RoleReply, ScoreBound, SetPrecondition, SetTtl, + ShutdownFlags, ShutdownSave, SortOpts, SortOrder, SortRoOpts, StreamId, StreamTrim, + StringOrBytes, TtlMode, TtlReply, UnixTimeMilliseconds, UnixTimeSeconds, XclaimOpts, + XinfoConsumersEntry, XinfoGroupsEntry, XinfoStreamReply, XinfoStreamReplyFull, XpendingExtendedEntry, XpendingSummary, ZaddComparison, ZaddCondition, ZmpopEnd, ZrangeBy, ZrangeWithscoresBy, ZsetAggregate, }; @@ -27,7 +29,7 @@ use componentized_valkey::commands::scripting::KeysAndArgs; use componentized_valkey::commands::shared::{DecimalSeconds, NextCursor}; use componentized_valkey::commands::sortedset::{LexRange, ScoreRange, ScoredMembers, Zrange}; use componentized_valkey::commands::stream::OrZeroId; -use componentized_valkey::value::{FlatPairs, OrNever}; +use componentized_valkey::value::{Binary, FlatPairs, OrNever}; use wit_bindgen::rt::Map; @@ -46,6 +48,23 @@ async fn exchange( struct Commands; +/// The rows of SORT's values, `row` values each. +fn rows(values: Vec>, row: usize) -> Vec>> { + let mut values = values.into_iter(); + std::iter::from_fn(|| Some(values.by_ref().take(row).collect::>())) + .take_while(|row| !row.is_empty()) + .collect() +} + +/// The words of RESTORE's ttl, `0` for none, and whether it is absolute, ABSTTL. +fn restore_ttl(ttl: Option) -> (StringOrBytes, bool) { + match ttl { + None => ("0".into(), false), + Some(RestoreTtl::ExpireIn(duration)) => (duration.px.to_string().into(), false), + Some(RestoreTtl::ExpireAt(unix_time)) => (unix_time.pxat.to_string().into(), true), + } +} + /// The number of values SORT returns for each element, one for each GET pattern, or the element. fn sort_row_len(opts: &Option) -> usize { opts.as_ref() @@ -1057,9 +1076,9 @@ impl Guest for Commands { #[doc = " PING [ message ]"] #[doc = " https://valkey.io/commands/ping/"] #[allow(async_fn_in_trait)] - async fn ping(connection: &Connection, message: Option) -> Result<(), Error> { - let reply: String = exchange(connection, ("PING", message.clone())).await?; - if reply == message.unwrap_or("PONG".to_string()) { + async fn ping(connection: &Connection, message: Option) -> Result<(), Error> { + let reply: StringOrBytes = exchange(connection, ("PING", message.clone())).await?; + if reply == message.unwrap_or_else(|| "PONG".into()) { Ok(()) } else { Err(Error::Client(format!("unexpected reply: {reply:?}"))) @@ -1317,7 +1336,7 @@ impl Guest for Commands { #[doc = " ECHO message"] #[doc = " https://valkey.io/commands/echo/"] #[allow(async_fn_in_trait)] - async fn echo(connection: &Connection, message: String) -> Result { + async fn echo(connection: &Connection, message: StringOrBytes) -> Result { exchange(connection, ("ECHO", message)).await } @@ -1401,7 +1420,7 @@ impl Guest for Commands { key: String, condition: Option, changed: bool, - coordinates_and_members: Vec<(GeoCoordinates, String)>, + coordinates_and_members: Vec<(GeoCoordinates, StringOrBytes)>, ) -> Result { exchange( connection, @@ -1424,8 +1443,8 @@ impl Guest for Commands { async fn geodist( connection: &Connection, key: String, - member1: String, - member2: String, + member1: StringOrBytes, + member2: StringOrBytes, unit: Option, ) -> Result, Error> { exchange(connection, ("GEODIST", key, member1, member2, unit)).await @@ -1439,7 +1458,7 @@ impl Guest for Commands { async fn geohash( connection: &Connection, key: String, - members: Option>, + members: Option>, ) -> Result>, Error> { exchange( connection, @@ -1456,7 +1475,7 @@ impl Guest for Commands { async fn geopos( connection: &Connection, key: String, - members: Option>, + members: Option>, ) -> Result>, Error> { exchange( connection, @@ -1478,7 +1497,7 @@ impl Guest for Commands { shape: GeosearchShape, order: Option, count: Option, - ) -> Result, Error> { + ) -> Result, Error> { exchange(connection, ("GEOSEARCH", key, shape, order, count)).await } @@ -1591,7 +1610,7 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result>, Error> { + ) -> Result>, Error> { exchange( connection, ("HGETDEL", key, Fields(NonEmpty::new("fields", fields))), @@ -1608,9 +1627,9 @@ impl Guest for Commands { async fn hgetex( connection: &Connection, key: String, - ttl: Option, + ttl: Option, fields: Vec, - ) -> Result>, Error> { + ) -> Result>, Error> { exchange( connection, ("HGETEX", key, ttl, Fields(NonEmpty::new("fields", fields))), @@ -1728,8 +1747,8 @@ impl Guest for Commands { key: String, condition: Option, field_condition: Option, - ttl: Option, - field_and_values: Vec<(String, String)>, + ttl: Option, + field_and_values: Vec<(String, StringOrBytes)>, ) -> Result { let command = ( "HSETEX", @@ -1820,7 +1839,7 @@ impl Guest for Commands { connection: &Connection, key: String, field: String, - ) -> Result, Error> { + ) -> Result, Error> { exchange(connection, ("HGET", key, field)).await } @@ -1829,7 +1848,10 @@ impl Guest for Commands { #[doc = " HGETALL key"] #[doc = " https://valkey.io/commands/hgetall/"] #[allow(async_fn_in_trait)] - async fn hgetall(connection: &Connection, key: String) -> Result, Error> { + async fn hgetall( + connection: &Connection, + key: String, + ) -> Result, Error> { exchange(connection, ("HGETALL", key)).await } @@ -1888,7 +1910,7 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result>, Error> { + ) -> Result>, Error> { exchange(connection, ("HMGET", key, NonEmpty::new("fields", fields))).await } @@ -1900,7 +1922,7 @@ impl Guest for Commands { async fn hmset( connection: &Connection, key: String, - field_and_values: Vec<(String, String)>, + field_and_values: Vec<(String, StringOrBytes)>, ) -> Result<(), Error> { exchange( connection, @@ -1950,7 +1972,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Integer, - ) -> Result, Error> { + ) -> Result, Error> { exchange(connection, ("HRANDFIELD", key, count, "WITHVALUES")).await } @@ -1967,8 +1989,8 @@ impl Guest for Commands { cursor: Cursor, match_: Option, count: Option, - ) -> Result<(Option, Map), Error> { - let (cursor, items): (NextCursor, Map) = exchange( + ) -> Result<(Option, Map), Error> { + let (cursor, items): (NextCursor, Map) = exchange( connection, ( "HSCAN", @@ -2019,7 +2041,7 @@ impl Guest for Commands { async fn hset( connection: &Connection, key: String, - field_and_values: Vec<(String, String)>, + field_and_values: Vec<(String, StringOrBytes)>, ) -> Result { exchange( connection, @@ -2041,7 +2063,7 @@ impl Guest for Commands { connection: &Connection, key: String, field: String, - value: String, + value: StringOrBytes, ) -> Result { exchange(connection, ("HSETNX", key, field, value)).await } @@ -2064,7 +2086,7 @@ impl Guest for Commands { #[doc = " HVALS key"] #[doc = " https://valkey.io/commands/hvals/"] #[allow(async_fn_in_trait)] - async fn hvals(connection: &Connection, key: String) -> Result, Error> { + async fn hvals(connection: &Connection, key: String) -> Result, Error> { exchange(connection, ("HVALS", key)).await } @@ -2079,7 +2101,7 @@ impl Guest for Commands { async fn pfadd( connection: &Connection, key: String, - elements: Option>, + elements: Option>, ) -> Result { exchange( connection, @@ -2140,7 +2162,7 @@ impl Guest for Commands { wherefrom: ListSide, whereto: ListSide, timeout: DurationMilliseconds, - ) -> Result, Error> { + ) -> Result, Error> { exchange( connection, ( @@ -2172,7 +2194,7 @@ impl Guest for Commands { keys: Vec, end: ListSide, count: Option, - ) -> Result)>, Error> { + ) -> Result)>, Error> { exchange( connection, ( @@ -2201,7 +2223,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, timeout: DurationMilliseconds, - ) -> Result, Error> { + ) -> Result, Error> { exchange( connection, ( @@ -2228,7 +2250,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, timeout: DurationMilliseconds, - ) -> Result, Error> { + ) -> Result, Error> { exchange( connection, ( @@ -2258,7 +2280,7 @@ impl Guest for Commands { source: String, destination: String, timeout: DurationMilliseconds, - ) -> Result, Error> { + ) -> Result, Error> { exchange( connection, ("BRPOPLPUSH", source, destination, DecimalSeconds(timeout)), @@ -2276,7 +2298,7 @@ impl Guest for Commands { connection: &Connection, key: String, index: Integer, - ) -> Result, Error> { + ) -> Result, Error> { exchange(connection, ("LINDEX", key, index)).await } @@ -2290,8 +2312,8 @@ impl Guest for Commands { connection: &Connection, key: String, position: LinsertPosition, - pivot: String, - element: String, + pivot: StringOrBytes, + element: StringOrBytes, ) -> Result { exchange(connection, ("LINSERT", key, position, pivot, element)).await } @@ -2317,7 +2339,7 @@ impl Guest for Commands { destination: String, wherefrom: ListSide, whereto: ListSide, - ) -> Result, Error> { + ) -> Result, Error> { exchange( connection, ("LMOVE", source, destination, wherefrom, whereto), @@ -2336,7 +2358,7 @@ impl Guest for Commands { keys: Vec, end: ListSide, count: Option, - ) -> Result)>, Error> { + ) -> Result)>, Error> { let command = ( "LMPOP", NumKeys(NonEmpty::new("keys", keys)), @@ -2356,17 +2378,17 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result, Error> { + ) -> Result, Error> { match count { Some(count) => { // null when the key does not exist - let elements: Option> = + let elements: Option> = exchange(connection, ("LPOP", key, count)).await?; Ok(elements.unwrap_or_default()) } None => { // without a count the reply is the element itself, or null - let element: Option = exchange(connection, ("LPOP", key)).await?; + let element: Option = exchange(connection, ("LPOP", key)).await?; Ok(element.into_iter().collect()) } } @@ -2384,7 +2406,7 @@ impl Guest for Commands { async fn lpos( connection: &Connection, key: String, - element: String, + element: StringOrBytes, rank: Option, count: Option, maxlen: Option, @@ -2417,7 +2439,7 @@ impl Guest for Commands { async fn lpush( connection: &Connection, key: String, - elements: Vec, + elements: Vec, ) -> Result { exchange( connection, @@ -2435,7 +2457,7 @@ impl Guest for Commands { async fn lpushx( connection: &Connection, key: String, - elements: Vec, + elements: Vec, ) -> Result { exchange( connection, @@ -2455,7 +2477,7 @@ impl Guest for Commands { key: String, start: Integer, stop: Integer, - ) -> Result, Error> { + ) -> Result, Error> { exchange(connection, ("LRANGE", key, start, stop)).await } @@ -2471,7 +2493,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Integer, - element: String, + element: StringOrBytes, ) -> Result { exchange(connection, ("LREM", key, count, element)).await } @@ -2485,7 +2507,7 @@ impl Guest for Commands { connection: &Connection, key: String, index: Integer, - element: String, + element: StringOrBytes, ) -> Result<(), Error> { exchange(connection, ("LSET", key, index, element)).await } @@ -2515,17 +2537,17 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result, Error> { + ) -> Result, Error> { match count { Some(count) => { // null when the key does not exist - let elements: Option> = + let elements: Option> = exchange(connection, ("RPOP", key, count)).await?; Ok(elements.unwrap_or_default()) } None => { // without a count the reply is the element itself, or null - let element: Option = exchange(connection, ("RPOP", key)).await?; + let element: Option = exchange(connection, ("RPOP", key)).await?; Ok(element.into_iter().collect()) } } @@ -2543,7 +2565,7 @@ impl Guest for Commands { connection: &Connection, source: String, destination: String, - ) -> Result, Error> { + ) -> Result, Error> { exchange(connection, ("RPOPLPUSH", source, destination)).await } @@ -2556,7 +2578,7 @@ impl Guest for Commands { async fn rpush( connection: &Connection, key: String, - elements: Vec, + elements: Vec, ) -> Result { exchange( connection, @@ -2574,7 +2596,7 @@ impl Guest for Commands { async fn rpushx( connection: &Connection, key: String, - elements: Vec, + elements: Vec, ) -> Result { exchange( connection, @@ -2592,6 +2614,17 @@ impl Guest for Commands { exchange(connection, ("DEL", NonEmpty::new("keys", keys))).await } + #[doc = " The value of the key serialized in Valkey's own format, with its version and a checksum, see"] + #[doc = " `restore`, none when the key does not exist."] + #[doc = ""] + #[doc = " DUMP key"] + #[doc = " https://valkey.io/commands/dump/"] + #[allow(async_fn_in_trait)] + async fn dump(connection: &Connection, key: String) -> Result, Error> { + let serialized: Option = exchange(connection, ("DUMP", key)).await?; + Ok(serialized.map(|serialized| serialized.0)) + } + #[doc = " If key exists."] #[doc = ""] #[doc = " EXISTS key [ key ... ]"] @@ -2927,6 +2960,70 @@ impl Guest for Commands { exchange(connection, ("RENAMENX", key, newkey)).await } + #[doc = " Creates the key from a value serialized with `dump`, without an expiration when `ttl` is"] + #[doc = " none. Fails when the key exists, unless `replace`. `idletime` and `freq` set what eviction"] + #[doc = " knows of the key, its idle time for an LRU policy, or its access frequency for an LFU policy."] + #[doc = ""] + #[doc = " RESTORE key ttl serialized-value [ REPLACE ] [ ABSTTL ] [ IDLETIME seconds ] [ FREQ frequency ]"] + #[doc = " https://valkey.io/commands/restore/"] + #[allow(async_fn_in_trait)] + async fn restore( + connection: &Connection, + key: String, + ttl: Option, + serialized_value: Bytes, + replace: bool, + idletime: Option, + freq: Option, + ) -> Result<(), Error> { + let (ttl, absttl) = restore_ttl(ttl); + exchange( + connection, + ( + "RESTORE", + key, + ttl, + StringOrBytes::Bytes(serialized_value), + Flag("REPLACE", replace), + Flag("ABSTTL", absttl), + NamedOption("IDLETIME", idletime), + NamedOption("FREQ", freq), + ), + ) + .await + } + + #[doc = " `restore` on a node a key is migrated to, for a slot being imported, see `cluster-setslot`."] + #[doc = ""] + #[doc = " RESTORE-ASKING key ttl serialized-value [ REPLACE ] [ ABSTTL ] [ IDLETIME seconds ] [ FREQ frequency ]"] + #[doc = " https://valkey.io/commands/restore-asking/"] + #[allow(async_fn_in_trait)] + async fn restore_asking( + connection: &Connection, + key: String, + ttl: Option, + serialized_value: Bytes, + replace: bool, + idletime: Option, + freq: Option, + ) -> Result<(), Error> { + let (ttl, absttl) = restore_ttl(ttl); + exchange( + connection, + ( + "RESTORE-ASKING", + key, + ttl, + StringOrBytes::Bytes(serialized_value), + Flag("REPLACE", replace), + Flag("ABSTTL", absttl), + NamedOption("IDLETIME", idletime), + NamedOption("FREQ", freq), + ), + ) + .await + } + #[doc = " Incrementally iterate over the keys of the current database."] #[doc = ""] #[doc = " SCAN cursor [ MATCH pattern ] [ COUNT count ] [ TYPE type ]"] @@ -3010,7 +3107,7 @@ impl Guest for Commands { async fn publish( connection: &Connection, channel: String, - message: String, + message: StringOrBytes, ) -> Result { exchange(connection, ("PUBLISH", channel, message)).await } @@ -3096,7 +3193,7 @@ impl Guest for Commands { connection: &Connection, script: String, keys: Option>, - args: Option>, + args: Option>, ) -> Result { exchange(connection, ("EVAL", script, KeysAndArgs(keys, args))).await } @@ -3110,7 +3207,7 @@ impl Guest for Commands { connection: &Connection, script: String, keys: Option>, - args: Option>, + args: Option>, ) -> Result { exchange(connection, ("EVAL_RO", script, KeysAndArgs(keys, args))).await } @@ -3125,7 +3222,7 @@ impl Guest for Commands { connection: &Connection, sha1: String, keys: Option>, - args: Option>, + args: Option>, ) -> Result { exchange(connection, ("EVALSHA", sha1, KeysAndArgs(keys, args))).await } @@ -3140,7 +3237,7 @@ impl Guest for Commands { connection: &Connection, sha1: String, keys: Option>, - args: Option>, + args: Option>, ) -> Result { exchange(connection, ("EVALSHA_RO", sha1, KeysAndArgs(keys, args))).await } @@ -3157,7 +3254,7 @@ impl Guest for Commands { connection: &Connection, function: String, keys: Option>, - args: Option>, + args: Option>, ) -> Result { exchange(connection, ("FCALL", function, KeysAndArgs(keys, args))).await } @@ -3171,7 +3268,7 @@ impl Guest for Commands { connection: &Connection, function: String, keys: Option>, - args: Option>, + args: Option>, ) -> Result { exchange(connection, ("FCALL_RO", function, KeysAndArgs(keys, args))).await } @@ -3185,6 +3282,16 @@ impl Guest for Commands { exchange(connection, ("FUNCTION", "DELETE", library_name)).await } + #[doc = " The libraries, serialized in Valkey's own format, see `function-restore`."] + #[doc = ""] + #[doc = " FUNCTION DUMP"] + #[doc = " https://valkey.io/commands/function-dump/"] + #[allow(async_fn_in_trait)] + async fn function_dump(connection: &Connection) -> Result { + let serialized: Binary = exchange(connection, ("FUNCTION", "DUMP")).await?; + Ok(serialized.0) + } + #[doc = " Deletes all the libraries, in the mode, the server's `lazyfree-lazy-user-flush` without one."] #[doc = ""] #[doc = " FUNCTION FLUSH [ ASYNC | SYNC ]"] @@ -3243,6 +3350,28 @@ impl Guest for Commands { .await } + #[doc = " Loads the libraries serialized with `function-dump`, as `policy` says, by default `append`."] + #[doc = ""] + #[doc = " FUNCTION RESTORE serialized-value [ FLUSH | APPEND | REPLACE ]"] + #[doc = " https://valkey.io/commands/function-restore/"] + #[allow(async_fn_in_trait)] + async fn function_restore( + connection: &Connection, + serialized_value: Bytes, + policy: Option, + ) -> Result<(), Error> { + exchange( + connection, + ( + "FUNCTION", + "RESTORE", + StringOrBytes::Bytes(serialized_value), + policy, + ), + ) + .await + } + #[doc = " The function that is running, and the libraries and functions of each engine."] #[doc = ""] #[doc = " FUNCTION STATS"] @@ -3313,7 +3442,7 @@ impl Guest for Commands { async fn spublish( connection: &Connection, shardchannel: String, - message: String, + message: StringOrBytes, ) -> Result { exchange(connection, ("SPUBLISH", shardchannel, message)).await } @@ -3323,7 +3452,11 @@ impl Guest for Commands { #[doc = " APPEND key value"] #[doc = " https://valkey.io/commands/append/"] #[allow(async_fn_in_trait)] - async fn append(connection: &Connection, key: String, value: String) -> Result { + async fn append( + connection: &Connection, + key: String, + value: StringOrBytes, + ) -> Result { exchange(connection, ("APPEND", key, value)).await } @@ -3354,7 +3487,11 @@ impl Guest for Commands { #[doc = " DELIFEQ key value"] #[doc = " https://valkey.io/commands/delifeq/"] #[allow(async_fn_in_trait)] - async fn delifeq(connection: &Connection, key: String, value: String) -> Result { + async fn delifeq( + connection: &Connection, + key: String, + value: StringOrBytes, + ) -> Result { exchange(connection, ("DELIFEQ", key, value)).await } @@ -3363,7 +3500,7 @@ impl Guest for Commands { #[doc = " GET key"] #[doc = " https://valkey.io/commands/get/"] #[allow(async_fn_in_trait)] - async fn get(connection: &Connection, key: String) -> Result, Error> { + async fn get(connection: &Connection, key: String) -> Result, Error> { exchange(connection, ("GET", key)).await } @@ -3372,7 +3509,7 @@ impl Guest for Commands { #[doc = " GETDEL key"] #[doc = " https://valkey.io/commands/getdel/"] #[allow(async_fn_in_trait)] - async fn getdel(connection: &Connection, key: String) -> Result, Error> { + async fn getdel(connection: &Connection, key: String) -> Result, Error> { exchange(connection, ("GETDEL", key)).await } @@ -3385,7 +3522,7 @@ impl Guest for Commands { connection: &Connection, key: String, ttl: Option, - ) -> Result, Error> { + ) -> Result, Error> { exchange(connection, ("GETEX", key, ttl)).await } @@ -3399,7 +3536,7 @@ impl Guest for Commands { key: String, start: Integer, end: Integer, - ) -> Result { + ) -> Result { exchange(connection, ("GETRANGE", key, start, end)).await } @@ -3411,8 +3548,8 @@ impl Guest for Commands { async fn getset( connection: &Connection, key: String, - value: String, - ) -> Result, Error> { + value: StringOrBytes, + ) -> Result, Error> { exchange(connection, ("GETSET", key, value)).await } @@ -3458,7 +3595,11 @@ impl Guest for Commands { #[doc = " LCS key1 key2"] #[doc = " https://valkey.io/commands/lcs/"] #[allow(async_fn_in_trait)] - async fn lcs(connection: &Connection, key1: String, key2: String) -> Result { + async fn lcs( + connection: &Connection, + key1: String, + key2: String, + ) -> Result { exchange(connection, ("LCS", key1, key2)).await } @@ -3513,7 +3654,7 @@ impl Guest for Commands { async fn mget( connection: &Connection, keys: Vec, - ) -> Result>, Error> { + ) -> Result>, Error> { exchange(connection, ("MGET", NonEmpty::new("keys", keys))).await } @@ -3524,7 +3665,7 @@ impl Guest for Commands { #[allow(async_fn_in_trait)] async fn mset( connection: &Connection, - keys_and_values: Vec<(String, String)>, + keys_and_values: Vec<(String, StringOrBytes)>, ) -> Result<(), Error> { exchange( connection, @@ -3540,7 +3681,7 @@ impl Guest for Commands { #[allow(async_fn_in_trait)] async fn msetex( connection: &Connection, - keys_and_values: Vec<(String, String)>, + keys_and_values: Vec<(String, StringOrBytes)>, precondition: Option, ttl: Option, ) -> Result { @@ -3563,7 +3704,7 @@ impl Guest for Commands { #[allow(async_fn_in_trait)] async fn msetnx( connection: &Connection, - keys_and_values: Vec<(String, String)>, + keys_and_values: Vec<(String, StringOrBytes)>, ) -> Result { exchange( connection, @@ -3581,7 +3722,7 @@ impl Guest for Commands { connection: &Connection, key: String, milliseconds: DurationMilliseconds, - value: String, + value: StringOrBytes, ) -> Result<(), Error> { exchange(connection, ("PSETEX", key, milliseconds, value)).await } @@ -3596,7 +3737,7 @@ impl Guest for Commands { async fn set( connection: &Connection, key: String, - value: String, + value: StringOrBytes, precondition: Option, ttl: Option, ) -> Result { @@ -3614,10 +3755,10 @@ impl Guest for Commands { async fn set_get( connection: &Connection, key: String, - value: String, + value: StringOrBytes, precondition: Option, ttl: Option, - ) -> Result, Error> { + ) -> Result, Error> { exchange(connection, ("SET", key, value, precondition, "GET", ttl)).await } @@ -3630,7 +3771,7 @@ impl Guest for Commands { connection: &Connection, key: String, seconds: DurationSeconds, - value: String, + value: StringOrBytes, ) -> Result<(), Error> { exchange(connection, ("SETEX", key, seconds, value)).await } @@ -3640,7 +3781,11 @@ impl Guest for Commands { #[doc = " SETNX key value"] #[doc = " https://valkey.io/commands/setnx/"] #[allow(async_fn_in_trait)] - async fn setnx(connection: &Connection, key: String, value: String) -> Result { + async fn setnx( + connection: &Connection, + key: String, + value: StringOrBytes, + ) -> Result { exchange(connection, ("SETNX", key, value)).await } @@ -3653,7 +3798,7 @@ impl Guest for Commands { connection: &Connection, key: String, offset: u32, - value: String, + value: StringOrBytes, ) -> Result { exchange(connection, ("SETRANGE", key, offset, value)).await } @@ -3677,7 +3822,7 @@ impl Guest for Commands { key: String, start: Integer, end: Integer, - ) -> Result { + ) -> Result { exchange(connection, ("SUBSTR", key, start, end)).await } @@ -3706,7 +3851,7 @@ impl Guest for Commands { nomkstream: bool, trim: Option, id: StreamId, - field_and_values: Vec<(String, String)>, + field_and_values: Vec<(String, StringOrBytes)>, ) -> Result, Error> { exchange( connection, @@ -3740,7 +3885,7 @@ impl Guest for Commands { ) -> Result< ( Option, - Vec<(StreamId, Map)>, + Vec<(StreamId, Map)>, Vec, ), Error, @@ -3809,7 +3954,7 @@ impl Guest for Commands { min_idle_time: DurationMilliseconds, ids: Vec, opts: Option, - ) -> Result)>, Error> { + ) -> Result)>, Error> { exchange( connection, ( @@ -4091,7 +4236,7 @@ impl Guest for Commands { start: StreamId, end: StreamId, count: Option, - ) -> Result)>, Error> { + ) -> Result)>, Error> { exchange( connection, ("XRANGE", key, start, end, NamedOption("COUNT", count)), @@ -4110,7 +4255,7 @@ impl Guest for Commands { block: Option, keys: Vec, ids: Vec, - ) -> Result)>>>, Error> { + ) -> Result)>>>, Error> { exchange( connection, ( @@ -4142,7 +4287,8 @@ impl Guest for Commands { noack: bool, keys: Vec, ids: Vec, - ) -> Result>)>>>, Error> { + ) -> Result>)>>>, Error> + { exchange( connection, ( @@ -4172,7 +4318,7 @@ impl Guest for Commands { end: StreamId, start: StreamId, count: Option, - ) -> Result)>, Error> { + ) -> Result)>, Error> { exchange( connection, ("XREVRANGE", key, end, start, NamedOption("COUNT", count)), @@ -4226,7 +4372,7 @@ impl Guest for Commands { async fn sadd( connection: &Connection, key: String, - members: Vec, + members: Vec, ) -> Result { exchange(connection, ("SADD", key, NonEmpty::new("members", members))).await } @@ -4245,7 +4391,10 @@ impl Guest for Commands { #[doc = " SDIFF key [ key ... ]"] #[doc = " https://valkey.io/commands/sdiff/"] #[allow(async_fn_in_trait)] - async fn sdiff(connection: &Connection, keys: Vec) -> Result, Error> { + async fn sdiff( + connection: &Connection, + keys: Vec, + ) -> Result, Error> { exchange(connection, ("SDIFF", NonEmpty::new("keys", keys))).await } @@ -4271,7 +4420,10 @@ impl Guest for Commands { #[doc = " SINTER key [ key ... ]"] #[doc = " https://valkey.io/commands/sinter/"] #[allow(async_fn_in_trait)] - async fn sinter(connection: &Connection, keys: Vec) -> Result, Error> { + async fn sinter( + connection: &Connection, + keys: Vec, + ) -> Result, Error> { exchange(connection, ("SINTER", NonEmpty::new("keys", keys))).await } @@ -4323,7 +4475,7 @@ impl Guest for Commands { async fn sismember( connection: &Connection, key: String, - member: String, + member: StringOrBytes, ) -> Result { exchange(connection, ("SISMEMBER", key, member)).await } @@ -4338,7 +4490,7 @@ impl Guest for Commands { async fn sismember_xx( connection: &Connection, key: String, - member: String, + member: StringOrBytes, ) -> Result, Error> { // -1 when the key does not exist let member: OrNever = exchange(connection, ("SISMEMBER", key, member, "XX")).await?; @@ -4350,7 +4502,7 @@ impl Guest for Commands { #[doc = " SMEMBERS key"] #[doc = " https://valkey.io/commands/smembers/"] #[allow(async_fn_in_trait)] - async fn smembers(connection: &Connection, key: String) -> Result, Error> { + async fn smembers(connection: &Connection, key: String) -> Result, Error> { exchange(connection, ("SMEMBERS", key)).await } @@ -4362,7 +4514,7 @@ impl Guest for Commands { async fn smismember( connection: &Connection, key: String, - members: Vec, + members: Vec, ) -> Result, Error> { exchange( connection, @@ -4380,7 +4532,7 @@ impl Guest for Commands { connection: &Connection, source: String, destination: String, - member: String, + member: StringOrBytes, ) -> Result { exchange(connection, ("SMOVE", source, destination, member)).await } @@ -4396,12 +4548,12 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result, Error> { + ) -> Result, Error> { if count.is_some() { return exchange(connection, ("SPOP", key, count)).await; } // without a count the reply is the member itself, or null - let member: Option = exchange(connection, ("SPOP", key)).await?; + let member: Option = exchange(connection, ("SPOP", key)).await?; Ok(member.into_iter().collect()) } @@ -4417,12 +4569,12 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result, Error> { + ) -> Result, Error> { if count.is_some() { return exchange(connection, ("SRANDMEMBER", key, count)).await; } // without a count the reply is the member itself, or null - let member: Option = exchange(connection, ("SRANDMEMBER", key)).await?; + let member: Option = exchange(connection, ("SRANDMEMBER", key)).await?; Ok(member.into_iter().collect()) } @@ -4434,7 +4586,7 @@ impl Guest for Commands { async fn srem( connection: &Connection, key: String, - members: Vec, + members: Vec, ) -> Result { exchange(connection, ("SREM", key, NonEmpty::new("members", members))).await } @@ -4450,8 +4602,8 @@ impl Guest for Commands { cursor: Cursor, match_: Option, count: Option, - ) -> Result<(Option, Vec), Error> { - let (cursor, items): (NextCursor, Vec) = exchange( + ) -> Result<(Option, Vec), Error> { + let (cursor, items): (NextCursor, Vec) = exchange( connection, ( "SSCAN", @@ -4470,7 +4622,10 @@ impl Guest for Commands { #[doc = " SUNION key [ key ... ]"] #[doc = " https://valkey.io/commands/sunion/"] #[allow(async_fn_in_trait)] - async fn sunion(connection: &Connection, keys: Vec) -> Result, Error> { + async fn sunion( + connection: &Connection, + keys: Vec, + ) -> Result, Error> { exchange(connection, ("SUNION", NonEmpty::new("keys", keys))).await } @@ -4503,10 +4658,10 @@ impl Guest for Commands { connection: &Connection, key: String, opts: Option, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let row = sort_row_len(&opts); - let values: Vec> = exchange(connection, ("SORT", key, opts)).await?; - Ok(values.chunks(row).map(<[_]>::to_vec).collect()) + let values: Vec> = exchange(connection, ("SORT", key, opts)).await?; + Ok(rows(values, row)) } #[doc = " Sorts the elements of the list, set or sorted set stored at key, storing them as a list,"] @@ -4532,11 +4687,12 @@ impl Guest for Commands { async fn sort_ro( connection: &Connection, key: String, - opts: Option, - ) -> Result>>, Error> { + opts: Option, + ) -> Result>>, Error> { let row = sort_row_len(&opts); - let values: Vec> = exchange(connection, ("SORT_RO", key, opts)).await?; - Ok(values.chunks(row).map(<[_]>::to_vec).collect()) + let values: Vec> = + exchange(connection, ("SORT_RO", key, opts)).await?; + Ok(rows(values, row)) } #[doc = " Removes the members with the lowest or highest scores from the first non-empty sorted set,"] @@ -4554,9 +4710,9 @@ impl Guest for Commands { connection: &Connection, timeout: DurationMilliseconds, keys: Vec, - end: ZmpopEnd, + end: BzmpopEnd, count: Option, - ) -> Result)>, Error> { + ) -> Result)>, Error> { let command = ( "BZMPOP", DecimalSeconds(timeout), @@ -4582,7 +4738,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, timeout: DurationMilliseconds, - ) -> Result, Error> { + ) -> Result, Error> { exchange( connection, ( @@ -4609,7 +4765,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, timeout: DurationMilliseconds, - ) -> Result, Error> { + ) -> Result, Error> { exchange( connection, ( @@ -4635,7 +4791,7 @@ impl Guest for Commands { condition: Option, comparison: Option, changed: bool, - score_and_members: Vec<(Double, String)>, + score_and_members: Vec<(Double, StringOrBytes)>, ) -> Result, Error> { let command = ( "ZADD", @@ -4662,7 +4818,7 @@ impl Guest for Commands { condition: Option, comparison: Option, increment: Double, - member: String, + member: StringOrBytes, ) -> Result, Error> { let command = ( "ZADD", key, condition, comparison, "INCR", increment, member, @@ -4712,7 +4868,10 @@ impl Guest for Commands { #[doc = " ZDIFF numkeys key [ key ... ]"] #[doc = " https://valkey.io/commands/zdiff/"] #[allow(async_fn_in_trait)] - async fn zdiff(connection: &Connection, keys: Vec) -> Result, Error> { + async fn zdiff( + connection: &Connection, + keys: Vec, + ) -> Result, Error> { exchange(connection, ("ZDIFF", NumKeys(NonEmpty::new("keys", keys)))).await } @@ -4726,7 +4885,7 @@ impl Guest for Commands { async fn zdiff_withscores( connection: &Connection, keys: Vec, - ) -> Result, Error> { + ) -> Result, Error> { exchange( connection, ("ZDIFF", NumKeys(NonEmpty::new("keys", keys)), "WITHSCORES"), @@ -4765,7 +4924,7 @@ impl Guest for Commands { connection: &Connection, key: String, increment: Double, - member: String, + member: StringOrBytes, ) -> Result { exchange(connection, ("ZINCRBY", key, increment, member)).await } @@ -4784,7 +4943,7 @@ impl Guest for Commands { keys: Vec, weights: Option>, aggregate: Option, - ) -> Result, Error> { + ) -> Result, Error> { exchange( connection, ( @@ -4809,7 +4968,7 @@ impl Guest for Commands { keys: Vec, weights: Option>, aggregate: Option, - ) -> Result, Error> { + ) -> Result, Error> { let command = ( "ZINTER", NumKeys(NonEmpty::new("keys", keys)), @@ -4901,7 +5060,7 @@ impl Guest for Commands { keys: Vec, end: ZmpopEnd, count: Option, - ) -> Result)>, Error> { + ) -> Result)>, Error> { exchange( connection, ( @@ -4923,7 +5082,7 @@ impl Guest for Commands { async fn zmscore( connection: &Connection, key: String, - members: Vec, + members: Vec, ) -> Result>, Error> { exchange( connection, @@ -4942,7 +5101,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result, Error> { + ) -> Result, Error> { let members: ScoredMembers = exchange(connection, ("ZPOPMAX", key, count)).await?; Ok(members.0) } @@ -4957,7 +5116,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result, Error> { + ) -> Result, Error> { let members: ScoredMembers = exchange(connection, ("ZPOPMIN", key, count)).await?; Ok(members.0) } @@ -4976,12 +5135,12 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result, Error> { + ) -> Result, Error> { if count.is_some() { return exchange(connection, ("ZRANDMEMBER", key, count)).await; } // without a count the reply is the member itself, or null - let member: Option = exchange(connection, ("ZRANDMEMBER", key)).await?; + let member: Option = exchange(connection, ("ZRANDMEMBER", key)).await?; Ok(member.into_iter().collect()) } @@ -4999,7 +5158,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Integer, - ) -> Result, Error> { + ) -> Result, Error> { exchange(connection, ("ZRANDMEMBER", key, count, "WITHSCORES")).await } @@ -5022,7 +5181,7 @@ impl Guest for Commands { rev: bool, limit: Option, xx: bool, - ) -> Result>, Error> { + ) -> Result>, Error> { let range = Zrange { range, rev, limit }; exchange(connection, ("ZRANGE", key, range, Flag("XX", xx))).await } @@ -5045,7 +5204,7 @@ impl Guest for Commands { rev: bool, limit: Option, xx: bool, - ) -> Result>, Error> { + ) -> Result>, Error> { let range = match range { ZrangeWithscoresBy::Rank(rank) => ZrangeBy::Rank(rank), ZrangeWithscoresBy::Score(score) => ZrangeBy::Score(score), @@ -5085,7 +5244,7 @@ impl Guest for Commands { async fn zrank( connection: &Connection, key: String, - member: String, + member: StringOrBytes, ) -> Result, Error> { exchange(connection, ("ZRANK", key, member)).await } @@ -5101,7 +5260,7 @@ impl Guest for Commands { async fn zrank_withscore( connection: &Connection, key: String, - member: String, + member: StringOrBytes, ) -> Result, Error> { exchange(connection, ("ZRANK", key, member, "WITHSCORE")).await } @@ -5114,7 +5273,7 @@ impl Guest for Commands { async fn zrem( connection: &Connection, key: String, - members: Vec, + members: Vec, ) -> Result { exchange(connection, ("ZREM", key, NonEmpty::new("members", members))).await } @@ -5199,7 +5358,7 @@ impl Guest for Commands { async fn zrevrank( connection: &Connection, key: String, - member: String, + member: StringOrBytes, ) -> Result, Error> { exchange(connection, ("ZREVRANK", key, member)).await } @@ -5215,7 +5374,7 @@ impl Guest for Commands { async fn zrevrank_withscore( connection: &Connection, key: String, - member: String, + member: StringOrBytes, ) -> Result, Error> { exchange(connection, ("ZREVRANK", key, member, "WITHSCORE")).await } @@ -5233,9 +5392,9 @@ impl Guest for Commands { cursor: Cursor, match_: Option, count: Option, - ) -> Result<(Option, Vec<(String, Double)>), Error> { + ) -> Result<(Option, Vec<(StringOrBytes, Double)>), Error> { // the members and scores are a flat array, the scores bulk strings - let (cursor, members): (NextCursor, FlatPairs) = exchange( + let (cursor, members): (NextCursor, FlatPairs) = exchange( connection, ( "ZSCAN", @@ -5262,8 +5421,8 @@ impl Guest for Commands { cursor: Cursor, match_: Option, count: Option, - ) -> Result<(Option, Vec), Error> { - let (cursor, items): (NextCursor, Vec) = exchange( + ) -> Result<(Option, Vec), Error> { + let (cursor, items): (NextCursor, Vec) = exchange( connection, ( "ZSCAN", @@ -5286,7 +5445,7 @@ impl Guest for Commands { async fn zscore( connection: &Connection, key: String, - member: String, + member: StringOrBytes, ) -> Result, Error> { exchange(connection, ("ZSCORE", key, member)).await } @@ -5305,7 +5464,7 @@ impl Guest for Commands { keys: Vec, weights: Option>, aggregate: Option, - ) -> Result, Error> { + ) -> Result, Error> { exchange( connection, ( @@ -5330,7 +5489,7 @@ impl Guest for Commands { keys: Vec, weights: Option>, aggregate: Option, - ) -> Result, Error> { + ) -> Result, Error> { let command = ( "ZUNION", NumKeys(NonEmpty::new("keys", keys)), diff --git a/components/connection/src/lib.rs b/components/connection/src/lib.rs index ce41073..80bd447 100644 --- a/components/connection/src/lib.rs +++ b/components/connection/src/lib.rs @@ -28,7 +28,7 @@ use std::future::{Future, poll_fn}; use std::pin::Pin; use std::rc::{Rc, Weak}; -use componentized_valkey::types::{RespNestedValue, RespValue}; +use componentized_valkey::types::{RespValue, StringOrBytes}; use exports::componentized::valkey::connection::{ ConnectOpts, Connection, DurationMilliseconds, Error, Guest, GuestConnection, Subscription, }; @@ -42,6 +42,9 @@ const LIB_NAME: &str = "componentized:valkey"; /// The version the connection reports to the server, that of the package. const LIB_VER: &str = env!("CARGO_PKG_VERSION"); +/// A word of a command, as sent, UTF-8 or not. +type Word = Vec; + /// How many values a reader may fall behind before its stream is closed, and how many events a /// subscription keeps while no stream is open. const PUSH_BUFFER: usize = 256; @@ -109,7 +112,7 @@ struct State { /// The server's properties from the reply to HELLO. server_properties: Vec<(String, RespValue)>, /// The HELLO the connection was established with, repeated after RESET. - hello: Vec, + hello: Vec, /// Bumped each time the keep-alive is set, a keep-alive task stops once it is outdated. keep_alive_generation: u64, /// Set by the first `subscription`, and never cleared: from then on the connection @@ -163,23 +166,27 @@ impl GuestConnection for ConnectionImpl { self.0.state.borrow().server_properties.clone() } - async fn exchange(&self, command: Vec) -> Result { - exchange(&self.0, command).await + async fn exchange(&self, command: Vec) -> Result { + exchange(&self.0, bytes(command)).await } - async fn exchange_all(&self, commands: Vec>) -> Vec> { - exchange_all(&self.0, commands).await + async fn exchange_all( + &self, + commands: Vec>, + ) -> Vec> { + exchange_all(&self.0, commands.into_iter().map(bytes).collect()).await } - async fn emit(&self, command: Vec) -> Result<(), Error> { - emit(&self.0, command).await + async fn emit(&self, command: Vec) -> Result<(), Error> { + emit(&self.0, bytes(command)).await } async fn watch(&self, keys: Vec) -> Result<(), Error> { if keys.is_empty() { return Err(Error::Client("keys requires at least one item".to_string())); } - let command = std::iter::once("WATCH".to_string()).chain(keys).collect(); + let keys = keys.into_iter().map(String::into_bytes); + let command = std::iter::once(b"WATCH".to_vec()).chain(keys).collect(); ok(exchange(&self.0, command).await?) } @@ -189,9 +196,9 @@ impl GuestConnection for ConnectionImpl { async fn transaction( &self, - commands: Vec>, + commands: Vec>, ) -> Result>, Error> { - transaction(&self.0, commands).await + transaction(&self.0, commands.into_iter().map(bytes).collect()).await } fn subscription(&self) -> Result { @@ -287,7 +294,7 @@ async fn reset(inner: &Inner) -> Result<(), Error> { /// A command without a reply takes no place in the order. fn enqueue( inner: &Inner, - command: Vec, + command: Vec, reply: Option, written: Option>, ) -> Result<(), Error> { @@ -319,7 +326,7 @@ fn enqueue( Ok(()) } -async fn exchange(inner: &Inner, command: Vec) -> Result { +async fn exchange(inner: &Inner, command: Vec) -> Result { reject_managed(inner, &command)?; if replies_with_pushes(&command) { return emit(inner, command).await.map(|()| RespValue::Null); @@ -329,7 +336,7 @@ async fn exchange(inner: &Inner, command: Vec) -> Result) -> Result { +async fn request(inner: &Inner, command: Vec) -> Result { let (reply_tx, reply_rx) = oneshot::channel(); enqueue(inner, command, Some(Reply::Call(reply_tx)), None)?; // the reply is dropped unsent when the connection closes, the reader fails it first @@ -339,7 +346,7 @@ async fn request(inner: &Inner, command: Vec) -> Result>) -> Vec> { +async fn exchange_all(inner: &Inner, commands: Vec>) -> Vec> { // no await until every command is queued let queued: Vec>, Error>> = commands .into_iter() @@ -370,7 +377,7 @@ async fn exchange_all(inner: &Inner, commands: Vec>) -> Vec>, + commands: Vec>, ) -> Result>, Error> { // before MULTI is sent, so an invalid command fails the transaction rather than being left out if commands.iter().any(Vec::is_empty) { @@ -397,14 +404,14 @@ async fn transaction( RespValue::Null => Ok(None), RespValue::Array(replies) => replies .into_iter() - .map(|reply| componentized_valkey::resp::decode(reply).map_err(Error::Resp)) + .map(|reply| decode(&reply)) .collect::>() .map(Some), reply => Err(unexpected(&reply)), } } -async fn emit(inner: &Inner, command: Vec) -> Result<(), Error> { +async fn emit(inner: &Inner, command: Vec) -> Result<(), Error> { reject_managed(inner, &command)?; let reply = (!replies_with_pushes(&command)).then_some(Reply::Discard); let (written_tx, written_rx) = oneshot::channel(); @@ -420,23 +427,28 @@ async fn emit(inner: &Inner, command: Vec) -> Result<(), Error> { /// - CLIENT REPLY, which stops replies from pairing with commands /// - SUBSCRIBE and the like once the connection owns the subscriptions, see /// `State::owns_subscriptions`, e.g. an UNSUBSCRIBE would end a subscription without it asking -fn reject_managed(inner: &Inner, command: &[String]) -> Result<(), Error> { - let word = |index: usize| command.get(index).map(|word| word.to_uppercase()); - let name = word(0).unwrap_or_default(); - let reason = match (name.as_str(), word(1).as_deref()) { - ("RESET", _) => Some("RESET is rejected, use the connection's reset"), - ("HELLO", _) => Some( +fn reject_managed(inner: &Inner, command: &[Word]) -> Result<(), Error> { + let is = |index: usize, word: &str| { + command + .get(index) + .is_some_and(|actual| actual.eq_ignore_ascii_case(word.as_bytes())) + }; + let reason = if is(0, "RESET") { + Some("RESET is rejected, use the connection's reset") + } else if is(0, "HELLO") { + Some( "HELLO is rejected, the connection sent it when it was established, see its server-properties", - ), - ("CLIENT", Some("REPLY")) => { - Some("CLIENT REPLY is rejected, every command has a reply, see the connection's emit") - } - _ => None, + ) + } else if is(0, "CLIENT") && is(1, "REPLY") { + Some("CLIENT REPLY is rejected, every command has a reply, see the connection's emit") + } else { + None }; if let Some(reason) = reason { return Err(Error::Client(reason.to_string())); } if replies_with_pushes(command) && inner.state.borrow().owns_subscriptions { + let name = String::from_utf8_lossy(&command[0]).to_uppercase(); return Err(Error::Client(format!( "{name} is rejected once the connection has a subscription, use the subscription's methods" ))); @@ -449,14 +461,14 @@ fn reject_managed(inner: &Inner, command: &[String]) -> Result<(), Error> { /// In RESP3 the server confirms each channel subscribed to, or unsubscribed from, with a push /// message. An error, e.g. for a command missing its channels, is a reply the server sends /// unprompted. -fn replies_with_pushes(command: &[String]) -> bool { - const COMMANDS: [&str; 6] = [ - "SUBSCRIBE", - "PSUBSCRIBE", - "SSUBSCRIBE", - "UNSUBSCRIBE", - "PUNSUBSCRIBE", - "SUNSUBSCRIBE", +fn replies_with_pushes(command: &[Word]) -> bool { + const COMMANDS: [&[u8]; 6] = [ + b"SUBSCRIBE", + b"PSUBSCRIBE", + b"SSUBSCRIBE", + b"UNSUBSCRIBE", + b"PUNSUBSCRIBE", + b"SUNSUBSCRIBE", ]; command.first().is_some_and(|name| { COMMANDS @@ -500,8 +512,20 @@ async fn next(stream: &mut S) -> Option { poll_fn(|cx| Pin::new(&mut *stream).poll_next(cx)).await } -fn words(words: &[&str]) -> Vec { - words.iter().map(|word| word.to_string()).collect() +/// The words of a command the connection sends itself, e.g. `words(&["PING"])`. +fn words(words: &[&str]) -> Vec { + words.iter().map(|word| word.as_bytes().to_vec()).collect() +} + +/// The words of a command, taken from strings or bytes as they are, without copying. +fn bytes(command: Vec) -> Vec { + command + .into_iter() + .map(|word| match word { + StringOrBytes::String(text) => text.into_bytes(), + StringOrBytes::Bytes(bytes) => bytes, + }) + .collect() } /// Write each queued command to the requests stream, closing it once the connection is dropped. @@ -695,10 +719,14 @@ async fn establish( let mut hello = words(&["HELLO", "3"]); if let Some((username, password)) = auth { - hello.extend(["AUTH".to_string(), username, password]); + hello.extend([ + b"AUTH".to_vec(), + username.into_bytes(), + password.into_bytes(), + ]); } if let Some(client_name) = client_name { - hello.extend(["SETNAME".to_string(), client_name]); + hello.extend([b"SETNAME".to_vec(), client_name.into_bytes()]); } inner.state.borrow_mut().hello = hello.clone(); @@ -737,27 +765,29 @@ async fn establish( /// The server's properties from the reply to HELLO, a map of property names to values. fn server_properties(reply: RespValue) -> Result, Error> { let RespValue::Map(entries) = reply else { - return Err(Error::Client(format!( - "unexpected reply to HELLO: {reply:?}" - ))); + return Err(Error::Client(format!("unexpected reply to HELLO: {reply}"))); }; entries .into_iter() .map(|(name, value)| { - let name = match decode(name)? { - RespValue::String(name) | RespValue::BulkString(name) => name, - name => { - return Err(Error::Client(format!( - "unexpected property name in reply to HELLO: {name:?}" - ))); - } + let invalid = |name: RespValue| { + Error::Client(format!( + "unexpected property name in reply to HELLO: {name}" + )) + }; + let name = match decode(&name)? { + RespValue::String(name) => name, + // the bytes come back with the error, for its message + RespValue::BulkString(bytes) => String::from_utf8(bytes) + .map_err(|error| invalid(RespValue::BulkString(error.into_bytes())))?, + name => return Err(invalid(name)), }; - Ok((name, decode(value)?)) + Ok((name, decode(&value)?)) }) .collect() } -fn decode(value: RespNestedValue) -> Result { +fn decode(value: &[u8]) -> Result { componentized_valkey::resp::decode(value).map_err(Error::Resp) } diff --git a/components/connection/src/pubsub.rs b/components/connection/src/pubsub.rs index c174d6b..cf81c90 100644 --- a/components/connection/src/pubsub.rs +++ b/components/connection/src/pubsub.rs @@ -5,7 +5,8 @@ use std::collections::{HashMap, VecDeque}; use std::rc::Rc; use componentized_valkey::types::{ - RespValue, SubscriptionEvent, SubscriptionMessage, SubscriptionTarget, UnsubscribeReason, + RespValue, StringOrBytes, SubscriptionEvent, SubscriptionMessage, SubscriptionTarget, + UnsubscribeReason, }; use futures_channel::{mpsc, oneshot}; use wit_bindgen::{FutureReader, FutureWriter, StreamReader, StreamWriter}; @@ -563,8 +564,8 @@ fn queue_subscribe( if names.is_empty() { return Ok(None); } - let command = std::iter::once(kind.subscribe().to_string()) - .chain(names.iter().cloned()) + let command = std::iter::once(kind.subscribe().as_bytes().to_vec()) + .chain(names.iter().map(|name| name.as_bytes().to_vec())) .collect(); let (done_tx, done_rx) = oneshot::channel(); let reply = Reply::Pubsub(Pending::Subscribe { @@ -686,8 +687,8 @@ fn queue_unsubscribe( if release.is_empty() { return Ok(None); } - let command = std::iter::once(kind.unsubscribe().to_string()) - .chain(release.iter().cloned()) + let command = std::iter::once(kind.unsubscribe().as_bytes().to_vec()) + .chain(release.iter().map(|name| name.as_bytes().to_vec())) .collect(); let (done_tx, done_rx) = oneshot::channel(); let reply = Reply::Pubsub(Pending::Unsubscribe { @@ -759,43 +760,45 @@ fn parse_push(value: &RespValue) -> Option { let RespValue::Push(items) = value else { return None; }; - let items = items + let mut items = items .iter() - .map(|item| decode(item.clone()).ok()) + .map(|item| decode(item).ok()) .collect::>>()?; - let text = |index: usize| match items.get(index) { - Some(RespValue::String(text) | RespValue::BulkString(text)) => Some(text.clone()), - _ => None, + // each item is taken once, rather than cloned + let mut take = |index: usize| { + items + .get_mut(index) + .map(|item| std::mem::replace(item, RespValue::Null)) }; - let push = match text(0)?.as_str() { - "subscribe" => Push::Subscribed((Kind::Channel, text(1)?)), - "psubscribe" => Push::Subscribed((Kind::Pattern, text(1)?)), - "ssubscribe" => Push::Subscribed((Kind::Shard, text(1)?)), - "unsubscribe" => Push::Unsubscribed(Kind::Channel, text(1)), - "punsubscribe" => Push::Unsubscribed(Kind::Pattern, text(1)), - "sunsubscribe" => Push::Unsubscribed(Kind::Shard, text(1)), + let push = match text(take(0))?.as_str() { + "subscribe" => Push::Subscribed((Kind::Channel, text(take(1))?)), + "psubscribe" => Push::Subscribed((Kind::Pattern, text(take(1))?)), + "ssubscribe" => Push::Subscribed((Kind::Shard, text(take(1))?)), + "unsubscribe" => Push::Unsubscribed(Kind::Channel, text(take(1))), + "punsubscribe" => Push::Unsubscribed(Kind::Pattern, text(take(1))), + "sunsubscribe" => Push::Unsubscribed(Kind::Shard, text(take(1))), "message" => { - let channel = text(1)?; + let channel = text(take(1))?; let message = SubscriptionMessage { - payload: text(2)?, + payload: payload(take(2))?, channel: channel.clone(), target: SubscriptionTarget::Channel(channel.clone()), }; Push::Message((Kind::Channel, channel), message) } "pmessage" => { - let pattern = text(1)?; + let pattern = text(take(1))?; let message = SubscriptionMessage { - payload: text(3)?, - channel: text(2)?, + channel: text(take(2))?, + payload: payload(take(3))?, target: SubscriptionTarget::Pattern(pattern.clone()), }; Push::Message((Kind::Pattern, pattern), message) } "smessage" => { - let shardchannel = text(1)?; + let shardchannel = text(take(1))?; let message = SubscriptionMessage { - payload: text(2)?, + payload: payload(take(2))?, channel: shardchannel.clone(), target: SubscriptionTarget::Shardchannel(shardchannel.clone()), }; @@ -806,6 +809,29 @@ fn parse_push(value: &RespValue) -> Option { Some(push) } +/// The text of an item of a push message, a simple or bulk string that is UTF-8, e.g. its kind or +/// a channel. +fn text(item: Option) -> Option { + match item? { + RespValue::String(text) => Some(text), + RespValue::BulkString(bytes) => String::from_utf8(bytes).ok(), + _ => None, + } +} + +/// The payload of a message, a string when it is UTF-8, otherwise its bytes. +fn payload(item: Option) -> Option { + match item? { + // the bytes come back with the error, rather than being cloned to try + RespValue::BulkString(bytes) => Some(match String::from_utf8(bytes) { + Ok(text) => StringOrBytes::String(text), + Err(error) => StringOrBytes::Bytes(error.into_bytes()), + }), + RespValue::String(text) => Some(StringOrBytes::String(text)), + _ => None, + } +} + /// Hand a push message to the subscriptions, or the command, it belongs to, false when it belongs /// to none. pub(crate) fn claim_push(state: &mut State, value: &RespValue) -> bool { diff --git a/components/connection/tests/connection.rs b/components/connection/tests/connection.rs index 3f64e71..f789266 100644 --- a/components/connection/tests/connection.rs +++ b/components/connection/tests/connection.rs @@ -3,12 +3,12 @@ use std::time::Duration; use test_harness::types::{ - DurationMilliseconds, SubscriptionEvent, SubscriptionMessage, SubscriptionTarget, - UnsubscribeReason, + DurationMilliseconds, StringOrBytes, SubscriptionEvent, SubscriptionMessage, + SubscriptionTarget, UnsubscribeReason, }; use test_harness::{ ConnectOpts, Error, Harness, RespValue, Transport, bulk, command, connect, establish, - handshake, hello_reply, ok, push, receive, resolve, words, + handshake, hello_reply, ok, push, receive, resolve, strings, words, }; /// Long enough for the connection to act on what it was given, short enough to keep tests quick. @@ -37,7 +37,7 @@ fn confirm(kind: &str, name: &str) -> RespValue { fn message(payload: &str, channel: &str, target: SubscriptionTarget) -> Option { Some(SubscriptionEvent::Message(SubscriptionMessage { - payload: payload.to_string(), + payload: StringOrBytes::String(payload.to_string()), channel: channel.to_string(), target, })) @@ -171,6 +171,59 @@ async fn exchanges_are_paired_with_replies_in_order() -> wasmtime::Result<()> { .await } +#[tokio::test(flavor = "multi_thread")] +async fn binary_words_are_sent_as_given() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + let value = vec![0x80, 0x00, b'\r', b'\n', 0xff]; + let command = vec![ + StringOrBytes::String("SET".to_string()), + StringOrBytes::Bytes(b"key".to_vec()), + StringOrBytes::Bytes(value.clone()), + ]; + let call = guest.call_exchange(accessor, connection, command); + let server = async { + assert_eq!( + transport.request().await, + Some(vec![bulk("SET"), bulk("key"), RespValue::BulkString(value)]) + ); + transport.reply(ok()); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(ok())); + + // the commands the connection manages are recognized as bytes too + let reset = vec![StringOrBytes::Bytes(b"reset".to_vec())]; + let result = guest.call_emit(accessor, connection, reset).await?; + assert_eq!( + result, + Err(Error::Client( + "RESET is rejected, use the connection's reset".to_string() + )) + ); + // a name that is not UTF-8 is sent for the server to refuse + let call = + guest.call_exchange(accessor, connection, vec![StringOrBytes::Bytes(vec![0xff])]); + let server = async { + assert_eq!( + transport.request().await, + Some(vec![RespValue::BulkString(vec![0xff])]) + ); + transport.reply(RespValue::Error("ERR unknown command".to_string())); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!( + result?, + Err(Error::Valkey("ERR unknown command".to_string())) + ); + Ok(()) + }) + .await +} + #[tokio::test(flavor = "multi_thread")] async fn error_replies_fail_the_exchange() -> wasmtime::Result<()> { let mut harness = Harness::new("connection").await?; @@ -474,7 +527,7 @@ async fn subscriptions_are_confirmed_by_push_messages() -> wasmtime::Result<()> assert_eq!( events.recv().await, Some(SubscriptionEvent::Message(SubscriptionMessage { - payload: "hello".to_string(), + payload: StringOrBytes::String("hello".to_string()), channel: "news".to_string(), target: channel("news"), })) @@ -512,6 +565,92 @@ async fn subscriptions_are_confirmed_by_push_messages() -> wasmtime::Result<()> .await } +#[tokio::test(flavor = "multi_thread")] +async fn binary_messages_are_delivered() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let subscriptions = exports.subscription(); + let (mut transport, connection) = establish(accessor, exports).await?; + let subscription = guest.call_subscription(accessor, connection).await??; + let (events, _) = subscriptions.call_events(accessor, subscription).await?; + let mut events = receive(accessor, events)?; + let dead_letters = guest.call_dead_letters(accessor, connection).await?; + let mut dead_letters = receive(accessor, dead_letters)?; + let call = subscriptions.call_subscribe(accessor, subscription, vec![channel("news")]); + let server = async { + transport.request().await; + transport.reply(confirm("subscribe", "news")); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Ok(())); + assert_eq!( + events.recv().await, + Some(SubscriptionEvent::Subscribed(channel("news"))) + ); + + // not UTF-8, the bytes + let payload = vec![0x80, 0x00, b'\r', b'\n', 0xff]; + transport.reply(push(vec![ + bulk("message"), + bulk("news"), + RespValue::BulkString(payload.clone()), + ])); + assert_eq!( + events.recv().await, + Some(SubscriptionEvent::Message(SubscriptionMessage { + payload: StringOrBytes::Bytes(payload), + channel: "news".to_string(), + target: channel("news"), + })) + ); + // UTF-8, even with control characters and beyond ASCII, a string + transport.reply(push(vec![ + bulk("message"), + bulk("news"), + RespValue::BulkString("a\r\n€".as_bytes().to_vec()), + ])); + assert_eq!( + events.recv().await, + message("a\r\n€", "news", channel("news")) + ); + assert!( + tokio::time::timeout(SETTLE, dead_letters.recv()) + .await + .is_err(), + "a dead letter" + ); + Ok(()) + }) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn binary_property_names_fail_to_establish() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let (mut transport, connection) = connect(accessor, exports, None).await?; + for _ in 0..3 { + transport.request().await; + } + transport.reply(test_harness::map(vec![( + RespValue::BulkString(vec![0x80]), + bulk("valkey"), + )])); + assert_eq!( + resolve(accessor, connection).await?.err(), + Some(Error::Client( + r#"unexpected property name in reply to HELLO: bulk-string"# + .to_string() + )) + ); + Ok(()) + }) + .await +} + #[tokio::test(flavor = "multi_thread")] async fn error_replies_fail_a_subscribe() -> wasmtime::Result<()> { let mut harness = Harness::new("connection").await?; @@ -696,11 +835,25 @@ async fn transactions_wrap_the_commands_in_multi_and_exec() -> wasmtime::Result< let call = guest.call_transaction( accessor, connection, - vec![words(&["SET", "a", "1"]), words(&["INCR", "text"])], + vec![ + vec![ + StringOrBytes::String("SET".to_string()), + StringOrBytes::String("a".to_string()), + StringOrBytes::Bytes(vec![0x80, 0xff]), + ], + words(&["INCR", "text"]), + ], ); let server = async { assert_eq!(transport.request().await, Some(command(&["MULTI"]))); - assert_eq!(transport.request().await, Some(command(&["SET", "a", "1"]))); + assert_eq!( + transport.request().await, + Some(vec![ + bulk("SET"), + bulk("a"), + RespValue::BulkString(vec![0x80, 0xff]) + ]) + ); assert_eq!(transport.request().await, Some(command(&["INCR", "text"]))); assert_eq!(transport.request().await, Some(command(&["EXEC"]))); transport.reply(ok()); @@ -775,7 +928,7 @@ async fn watch_and_unwatch() -> wasmtime::Result<()> { .run(async |accessor, exports| { let guest = exports.connection(); let (mut transport, connection) = establish(accessor, exports).await?; - let call = guest.call_watch(accessor, connection, words(&["a", "b"])); + let call = guest.call_watch(accessor, connection, strings(&["a", "b"])); let server = async { assert_eq!( transport.request().await, @@ -1196,6 +1349,61 @@ mod with_valkey { .await } + #[tokio::test(flavor = "multi_thread")] + #[ignore = "needs a Valkey server, see make test-valkey"] + async fn binary_values_against_valkey() -> wasmtime::Result<()> { + let mut harness = Harness::with_valkey("test-tcp", &[]).await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let subscriptions = exports.subscription(); + let connection = connect(accessor, exports).await?; + let publisher = connect(accessor, exports).await?; + let binary = vec![0x80, 0x00, b'\r', b'\n', 0xff]; + let key = vec![b't', b':', 0xfe]; + let word = |bytes: &[u8]| StringOrBytes::Bytes(bytes.to_vec()); + + let set = vec![ + StringOrBytes::String("SET".to_string()), + word(&key), + word(&binary), + ]; + guest.call_exchange(accessor, publisher, set).await??; + let get = vec![StringOrBytes::String("GET".to_string()), word(&key)]; + assert_eq!( + guest.call_exchange(accessor, publisher, get).await?, + Ok(RespValue::BulkString(binary.clone())) + ); + let del = vec![StringOrBytes::String("DEL".to_string()), word(&key)]; + guest.call_exchange(accessor, publisher, del).await??; + + // a binary message, published and delivered as given + let subscription = guest.call_subscription(accessor, connection).await??; + let (events, _) = subscriptions.call_events(accessor, subscription).await?; + let mut events = receive(accessor, events)?; + subscriptions + .call_subscribe(accessor, subscription, vec![channel("t:conn:bin")]) + .await??; + events.recv().await; + let publish = vec![ + StringOrBytes::String("PUBLISH".to_string()), + StringOrBytes::String("t:conn:bin".to_string()), + word(&binary), + ]; + guest.call_exchange(accessor, publisher, publish).await??; + assert_eq!( + events.recv().await, + Some(SubscriptionEvent::Message(SubscriptionMessage { + payload: StringOrBytes::Bytes(binary), + channel: "t:conn:bin".to_string(), + target: channel("t:conn:bin"), + })) + ); + Ok(()) + }) + .await + } + #[tokio::test(flavor = "multi_thread")] #[ignore = "needs a Valkey server, see make test-valkey"] async fn partial_subscribe_failure_against_valkey() -> wasmtime::Result<()> { diff --git a/components/dep-cli/src/lib.rs b/components/dep-cli/src/lib.rs index 8c76a7d..ee590d3 100644 --- a/components/dep-cli/src/lib.rs +++ b/components/dep-cli/src/lib.rs @@ -12,7 +12,7 @@ use std::task::Poll; use componentized::valkey::connection::{Connection, Error}; use componentized::valkey::connector; -use componentized_valkey::types::{RespNestedValue, RespValue}; +use componentized_valkey::types::{RespNestedValue, RespValue, StringOrBytes}; use exports::wasi::cli::run::Guest; use futures_channel::mpsc; use futures_core::Stream; @@ -39,8 +39,12 @@ struct Cli; impl Guest for Cli { async fn run() -> Result<(), ()> { - let command: Vec = environment::get_arguments().into_iter().skip(1).collect(); - if let [flag] = command.as_slice() + let command: Vec = environment::get_arguments() + .into_iter() + .skip(1) + .map(StringOrBytes::String) + .collect(); + if let [StringOrBytes::String(flag)] = command.as_slice() && (flag == "-h" || flag == "--help") { print(stdout::write_via_stream, USAGE).await; @@ -63,11 +67,11 @@ impl Guest for Cli { } /// Sends the command, printing its reply, or the push messages a command without one causes. -async fn exec(command: Vec, out: &mut Output) -> Result<(), Error> { +async fn exec(command: Vec, out: &mut Output) -> Result<(), Error> { let connection = connector::connect().await?; // before the command is sent, so none of the push messages it causes are missed let mut pushes = connection.dead_letters(); - let name = command[0].to_ascii_uppercase(); + let name = name(&command); let channels = command.len() - 1; if SUBSCRIBE.contains(&name.as_str()) { @@ -162,16 +166,17 @@ async fn send_lines( ) -> Result<(), Error> { let mut input = Lines::new(stdin); while let Some(line) = input.next().await { - let words = match split(&line) { - Ok(words) => words, + let words: Vec = match split(&line) { + Ok(words) => words.into_iter().map(StringOrBytes::Bytes).collect(), Err(message) => { let _ = lines.unbounded_send(format!("(error) {message}")); continue; } }; - let Some(name) = words.first().map(|name| name.to_ascii_uppercase()) else { + if words.is_empty() { continue; - }; + } + let name = name(&words); if SUBSCRIBE.contains(&name.as_str()) || UNSUBSCRIBE.contains(&name.as_str()) { // no reply, the push messages confirming it are printed as they arrive connection.emit(words).await?; @@ -254,8 +259,9 @@ impl Lines { /// Splits a line into words, like valkey-cli. /// /// Words are separated by whitespace. A double quoted word may contain whitespace and escapes, e.g. -/// `\"`, `\n` and `\x41`, a single quoted word is taken as is, except for `\'`. -fn split(line: &str) -> Result, String> { +/// `\"`, `\n` and `\x41`, a single quoted word is taken as is, except for `\'`. A `\x` escape is a +/// byte, so a word may be binary, e.g. `"\x80\xff"`. +fn split(line: &str) -> Result>, String> { let mut words = vec![]; let mut chars = line.chars().peekable(); loop { @@ -263,18 +269,18 @@ fn split(line: &str) -> Result, String> { let Some(&first) = chars.peek() else { return Ok(words); }; - let mut word = String::new(); + let mut word = vec![]; if first == '"' || first == '\'' { chars.next(); loop { match (chars.next(), first) { (None, _) => return Err("unbalanced quotes".to_string()), (Some(c), quote) if c == quote => break, - (Some('\\'), '"') => word.push(unescape(&mut chars)?), + (Some('\\'), '"') => unescape(&mut chars, &mut word)?, (Some('\\'), '\'') if chars.peek() == Some(&'\'') => { - word.push(chars.next().unwrap_or_default()) + push(&mut word, chars.next().unwrap_or_default()) } - (Some(c), _) => word.push(c), + (Some(c), _) => push(&mut word, c), } } if chars.peek().is_some_and(|c| !c.is_whitespace()) { @@ -282,32 +288,46 @@ fn split(line: &str) -> Result, String> { } } else { while let Some(c) = chars.next_if(|c| !c.is_whitespace()) { - word.push(c); + push(&mut word, c); } } words.push(word); } } -/// The character of an escape in a double quoted word, after its `\`. -fn unescape(chars: &mut impl Iterator) -> Result { - Ok(match chars.next() { - Some('n') => '\n', - Some('r') => '\r', - Some('t') => '\t', - Some('b') => '\u{8}', - Some('a') => '\u{7}', +/// Appends the character, as UTF-8. +fn push(word: &mut Vec, c: char) { + word.extend_from_slice(c.encode_utf8(&mut [0; 4]).as_bytes()); +} + +/// Appends the escape in a double quoted word, after its `\`. +fn unescape(chars: &mut impl Iterator, word: &mut Vec) -> Result<(), String> { + match chars.next() { + Some('n') => word.push(b'\n'), + Some('r') => word.push(b'\r'), + Some('t') => word.push(b'\t'), + Some('b') => word.push(0x08), + Some('a') => word.push(0x07), Some('x') => { let hex: String = chars.take(2).collect(); match u8::from_str_radix(&hex, 16) { - // a string holds UTF-8, a byte on its own is only a character when it is ASCII - Ok(byte) if byte.is_ascii() => byte as char, + Ok(byte) if hex.len() == 2 => word.push(byte), _ => return Err(format!("unsupported escape: \\x{hex}")), } } - Some(c) => c, + Some(c) => push(word, c), None => return Err("unbalanced quotes".to_string()), - }) + } + Ok(()) +} + +/// The name of a command, to tell those that need handling apart, e.g. `SUBSCRIBE`. +fn name(command: &[StringOrBytes]) -> String { + match command.first() { + Some(StringOrBytes::String(name)) => name.to_ascii_uppercase(), + Some(StringOrBytes::Bytes(name)) => String::from_utf8_lossy(name).to_ascii_uppercase(), + None => String::new(), + } } /// A stream to stdout or stderr. @@ -364,8 +384,8 @@ fn render(value: RespValue) -> String { RespValue::String(text) => text, RespValue::Error(message) | RespValue::BulkError(message) => format!("(error) {message}"), RespValue::Integer(value) => format!("(integer) {value}"), - // quoted, with special characters escaped - RespValue::BulkString(text) => format!("{text:?}"), + // quoted, bytes outside printable ASCII escaped as valkey-cli does, binary or not + RespValue::BulkString(bytes) => format!("\"{}\"", bytes.escape_ascii()), RespValue::Null => "(nil)".to_string(), RespValue::Boolean(value) => format!("({value})"), RespValue::Double(value) => format!("(double) {value}"), @@ -395,7 +415,7 @@ fn render(value: RespValue) -> String { } fn render_nested(value: RespNestedValue) -> String { - match componentized_valkey::resp::decode(value) { + match componentized_valkey::resp::decode(&value) { Ok(value) => render(value), Err(error) => format!("(invalid) {error}"), } @@ -444,18 +464,19 @@ mod tests { } fn bulk(text: &str) -> RespValue { - RespValue::BulkString(text.to_string()) + RespValue::BulkString(text.as_bytes().to_vec()) + } + + /// The words a line splits into, as bytes. + fn split_words(words: &[&str]) -> Result>, String> { + Ok(words.iter().map(|word| word.as_bytes().to_vec()).collect()) } #[test] fn split_on_whitespace() { assert_eq!( split(" SET key\tvalue "), - Ok(vec![ - "SET".to_string(), - "key".to_string(), - "value".to_string() - ]) + split_words(&["SET", "key", "value"]) ); assert_eq!(split(""), Ok(vec![])); assert_eq!(split(" "), Ok(vec![])); @@ -464,29 +485,39 @@ mod tests { #[test] fn split_double_quotes() { assert_eq!( - split(r#"SET key "hello world" "a\"b" "\n\r\t\b\a" "\x41\x7a" "\q""#), + split(r#"SET key "hello world" "a\"b" "\n\r\t\b\a" "\x41\x7a" "\q" "€""#), + split_words(&[ + "SET", + "key", + "hello world", + "a\"b", + "\n\r\t\u{8}\u{7}", + "Az", + "q", + "€", + ]) + ); + assert_eq!(split(r#""""#), Ok(vec![vec![]])); + } + + #[test] + fn split_binary_escapes() { + // a `\x` escape is a byte, UTF-8 or not + assert_eq!( + split(r#"SET key "\x80\x00\xff""#), Ok(vec![ - "SET".to_string(), - "key".to_string(), - "hello world".to_string(), - "a\"b".to_string(), - "\n\r\t\u{8}\u{7}".to_string(), - "Az".to_string(), - "q".to_string(), + b"SET".to_vec(), + b"key".to_vec(), + vec![0x80, 0x00, 0xff] ]) ); - assert_eq!(split(r#""""#), Ok(vec![String::new()])); } #[test] fn split_single_quotes() { assert_eq!( split(r#"'hello world' 'it\'s' 'a\nb'"#), - Ok(vec![ - "hello world".to_string(), - "it's".to_string(), - "a\\nb".to_string() - ]) + split_words(&["hello world", "it's", "a\\nb"]) ); } @@ -499,15 +530,14 @@ mod tests { split(r#""key"value"#), Err("closing quote must be followed by a space".to_string()) ); - // a byte on its own is only a character when it is ASCII - assert_eq!( - split(r#""\xff""#), - Err("unsupported escape: \\xff".to_string()) - ); assert_eq!( split(r#""\xzz""#), Err("unsupported escape: \\xzz".to_string()) ); + assert_eq!( + split(r#""\x4""#), + Err("unsupported escape: \\x4\"".to_string()) + ); } #[test] @@ -539,6 +569,17 @@ mod tests { ); } + #[test] + fn render_bulk_strings_like_valkey_cli() { + // bytes outside printable ASCII are escaped, like valkey-cli, binary or not + assert_eq!( + render(RespValue::BulkString(vec![0x80, b'a', 0x00])), + r#""\x80a\x00""# + ); + assert_eq!(render(bulk("€")), r#""\xe2\x82\xac""#); + assert_eq!(render(bulk("tab\tquote\"")), r#""tab\tquote\"""#); + } + #[test] fn render_aggregates() { assert_eq!(render(RespValue::Array(vec![])), "(empty array)"); diff --git a/components/dep-test-commands/src/lib.rs b/components/dep-test-commands/src/lib.rs index 8ea867d..8a8ab15 100644 --- a/components/dep-test-commands/src/lib.rs +++ b/components/dep-test-commands/src/lib.rs @@ -13,8 +13,9 @@ use componentized::valkey::commands; use componentized::valkey::connection::{Connection, Error}; use componentized::valkey::connector; use componentized_valkey::types::{ - AclRule, AclUserRule, Cursor, DurationSeconds, HexpireResult, RespValue, TtlMode, - UnixTimeSeconds, + AclRule, AclUserRule, Bytes, Cursor, DurationMilliseconds, DurationSeconds, + FunctionRestorePolicy, HexpireResult, RespValue, RestoreTtl, StringOrBytes, TtlMode, + UnixTimeMilliseconds, UnixTimeSeconds, }; use exports::componentized::valkey_components::test_suite::Guest; use wit_bindgen::rt::Map; @@ -40,6 +41,8 @@ suite! { "string commands" => string_commands, "keyspace commands" => keyspace_commands, "hash commands" => hash_commands, + "binary values" => binary_values, + "dump and restore commands" => dump_and_restore_commands, "connection commands" => connection_commands, "admin and pubsub commands" => admin_and_pubsub_commands, "invalid arguments fail before anything is sent" => invalid_arguments, @@ -79,12 +82,24 @@ fn words(words: &[&str]) -> Vec { words.iter().map(|word| word.to_string()).collect() } +fn bwords(words: &[&str]) -> Vec { + words.iter().map(|word| word.as_bytes().to_vec()).collect() +} + fn s(value: &str) -> String { value.to_string() } +/// A value that is text. +fn v(value: &str) -> StringOrBytes { + value.into() +} + +/// Bytes that are not UTF-8, a value that is not text. +const BINARY: &[u8] = b"\x80\xff\x00value"; + fn bulk(value: &str) -> RespValue { - RespValue::BulkString(value.to_string()) + RespValue::BulkString(value.as_bytes().to_vec()) } fn ok() -> RespValue { @@ -150,16 +165,19 @@ struct Server { impl Server { /// The words of the next command the connection sent. - async fn request(&mut self) -> Result, String> { + async fn request(&mut self) -> Result, String> { let request = self.requests.next().await; - let Some(RespValue::Array(words)) = request else { - return Err(format!("expected a command, got {request:?}")); + let words = match request { + Some(RespValue::Array(words)) => words, + Some(request) => return Err(format!("expected a command, got {request}")), + None => return Err("expected a command, the requests closed".to_string()), }; words .into_iter() - .map(|word| match componentized_valkey::resp::decode(word) { + .map(|word| match componentized_valkey::resp::decode(&word) { Ok(RespValue::BulkString(word)) => Ok(word), - word => Err(format!("expected a bulk string, got {word:?}")), + Ok(word) => Err(format!("expected a bulk-string, got {word}")), + Err(err) => Err(format!("expected a bulk-string, got {err}")), }) .collect() } @@ -182,11 +200,11 @@ async fn establish() -> Result<(Server, Connection), String> { replies, _closed: closed, }; - check_eq!(server.request().await?, words(&["HELLO", "3"])); + check_eq!(server.request().await?, bwords(&["HELLO", "3"])); let version = env!("CARGO_PKG_VERSION"); let setinfo = [ - words(&["CLIENT", "SETINFO", "LIB-NAME", "componentized:valkey"]), - words(&["CLIENT", "SETINFO", "LIB-VER", version]), + bwords(&["CLIENT", "SETINFO", "LIB-NAME", "componentized:valkey"]), + bwords(&["CLIENT", "SETINFO", "LIB-VER", version]), ]; for expected in &setinfo { check_eq!(&server.request().await?, expected); @@ -212,7 +230,7 @@ async fn round_trip( server: &mut Server, reply: RespValue, call: impl Future, -) -> Result<(T, Vec), String> { +) -> Result<(T, Vec), String> { let serve = async { let request = server.request().await?; server.reply(reply).await?; @@ -227,8 +245,8 @@ async fn string_commands() -> Result<(), String> { let (result, sent) = round_trip(&mut server, bulk("value"), commands::get(&conn, s("key"))).await?; - check_eq!(sent, words(&["GET", "key"])); - check_eq!(result, Ok(Some(s("value")))); + check_eq!(sent, bwords(&["GET", "key"])); + check_eq!(result, Ok(Some(v("value")))); let (result, _) = round_trip( &mut server, @@ -241,10 +259,10 @@ async fn string_commands() -> Result<(), String> { let (result, sent) = round_trip( &mut server, ok(), - commands::set(&conn, s("key"), s("value"), None, None), + commands::set(&conn, s("key"), v("value"), None, None), ) .await?; - check_eq!(sent, words(&["SET", "key", "value"])); + check_eq!(sent, bwords(&["SET", "key", "value"])); check_eq!(result, Ok(true)); // null when the precondition is not met @@ -254,7 +272,7 @@ async fn string_commands() -> Result<(), String> { commands::set( &conn, s("key"), - s("value"), + v("value"), Some(componentized_valkey::types::SetPrecondition::Nx), Some(componentized_valkey::types::SetTtl::Ex(DurationSeconds { ex: 10, @@ -262,7 +280,7 @@ async fn string_commands() -> Result<(), String> { ), ) .await?; - check_eq!(sent, words(&["SET", "key", "value", "NX", "EX", "10"])); + check_eq!(sent, bwords(&["SET", "key", "value", "NX", "EX", "10"])); check_eq!(result, Ok(false)); let (result, sent) = round_trip( @@ -271,7 +289,7 @@ async fn string_commands() -> Result<(), String> { commands::incr(&conn, s("count")), ) .await?; - check_eq!(sent, words(&["INCR", "count"])); + check_eq!(sent, bwords(&["INCR", "count"])); check_eq!(result, Ok(1)); let (result, sent) = round_trip( @@ -280,7 +298,7 @@ async fn string_commands() -> Result<(), String> { commands::incrby(&conn, s("count"), -5), ) .await?; - check_eq!(sent, words(&["INCRBY", "count", "-5"])); + check_eq!(sent, bwords(&["INCRBY", "count", "-5"])); check_eq!(result, Ok(-4)); Ok(()) } @@ -294,7 +312,7 @@ async fn keyspace_commands() -> Result<(), String> { commands::del(&conn, words(&["a", "b"])), ) .await?; - check_eq!(sent, words(&["DEL", "a", "b"])); + check_eq!(sent, bwords(&["DEL", "a", "b"])); check_eq!(result, Ok(1)); let (result, sent) = round_trip( @@ -303,7 +321,7 @@ async fn keyspace_commands() -> Result<(), String> { commands::exists(&conn, words(&["key"])), ) .await?; - check_eq!(sent, words(&["EXISTS", "key"])); + check_eq!(sent, bwords(&["EXISTS", "key"])); check_eq!(result, Ok(0)); let (result, sent) = round_trip( @@ -317,7 +335,7 @@ async fn keyspace_commands() -> Result<(), String> { ), ) .await?; - check_eq!(sent, words(&["EXPIRE", "key", "91", "NX"])); + check_eq!(sent, bwords(&["EXPIRE", "key", "91", "NX"])); check_eq!(result, Ok(true)); let (result, sent) = round_trip( @@ -333,7 +351,7 @@ async fn keyspace_commands() -> Result<(), String> { ), ) .await?; - check_eq!(sent, words(&["EXPIREAT", "key", "1900000000"])); + check_eq!(sent, bwords(&["EXPIREAT", "key", "1900000000"])); check_eq!(result, Ok(false)); let (result, sent) = round_trip( @@ -342,7 +360,7 @@ async fn keyspace_commands() -> Result<(), String> { commands::keys(&conn, s("*")), ) .await?; - check_eq!(sent, words(&["KEYS", "*"])); + check_eq!(sent, bwords(&["KEYS", "*"])); check_eq!(result, Ok(words(&["a", "b"]))); // TYPE replies `none` for a key that does not exist @@ -352,7 +370,7 @@ async fn keyspace_commands() -> Result<(), String> { commands::type_(&conn, s("missing")), ) .await?; - check_eq!(sent, words(&["TYPE", "missing"])); + check_eq!(sent, bwords(&["TYPE", "missing"])); check_eq!(result, Ok(None)); Ok(()) } @@ -364,16 +382,16 @@ async fn hash_commands() -> Result<(), String> { let (result, sent) = round_trip( &mut server, RespValue::Integer(2), - commands::hset(&conn, key(), vec![(s("f"), s("v")), (s("g"), s("w"))]), + commands::hset(&conn, key(), vec![(s("f"), v("v")), (s("g"), v("w"))]), ) .await?; - check_eq!(sent, words(&["HSET", "hash", "f", "v", "g", "w"])); + check_eq!(sent, bwords(&["HSET", "hash", "f", "v", "g", "w"])); check_eq!(result, Ok(2)); let (result, sent) = round_trip(&mut server, bulk("v"), commands::hget(&conn, key(), s("f"))).await?; - check_eq!(sent, words(&["HGET", "hash", "f"])); - check_eq!(result, Ok(Some(s("v")))); + check_eq!(sent, bwords(&["HGET", "hash", "f"])); + check_eq!(result, Ok(Some(v("v")))); // RESP3 replies to HGETALL with a map let (result, sent) = round_trip( @@ -382,13 +400,13 @@ async fn hash_commands() -> Result<(), String> { commands::hgetall(&conn, key()), ) .await?; - check_eq!(sent, words(&["HGETALL", "hash"])); - check_eq!(result, Ok(Map::from([(s("f"), s("v")), (s("g"), s("w"))]))); + check_eq!(sent, bwords(&["HGETALL", "hash"])); + check_eq!(result, Ok(Map::from([(s("f"), v("v")), (s("g"), v("w"))]))); let (result, _) = round_trip( &mut server, RespValue::Integer(1), - commands::hsetnx(&conn, key(), s("f"), s("v")), + commands::hsetnx(&conn, key(), s("f"), v("v")), ) .await?; check_eq!(result, Ok(true)); @@ -407,8 +425,8 @@ async fn hash_commands() -> Result<(), String> { commands::hmget(&conn, key(), words(&["f", "x"])), ) .await?; - check_eq!(sent, words(&["HMGET", "hash", "f", "x"])); - check_eq!(result, Ok(vec![Some(s("v")), None])); + check_eq!(sent, bwords(&["HMGET", "hash", "f", "x"])); + check_eq!(result, Ok(vec![Some(v("v")), None])); let (result, sent) = round_trip( &mut server, @@ -416,7 +434,7 @@ async fn hash_commands() -> Result<(), String> { commands::hincrby(&conn, key(), s("n"), 5), ) .await?; - check_eq!(sent, words(&["HINCRBY", "hash", "n", "5"])); + check_eq!(sent, bwords(&["HINCRBY", "hash", "n", "5"])); check_eq!(result, Ok(6)); // a double sent as a bulk string, even with RESP3 @@ -426,7 +444,7 @@ async fn hash_commands() -> Result<(), String> { commands::hincrbyfloat(&conn, key(), s("n"), 1.5), ) .await?; - check_eq!(sent, words(&["HINCRBYFLOAT", "hash", "n", "1.5"])); + check_eq!(sent, bwords(&["HINCRBYFLOAT", "hash", "n", "1.5"])); check_eq!(result, Ok(2.5)); let (result, sent) = round_trip( @@ -443,7 +461,7 @@ async fn hash_commands() -> Result<(), String> { .await?; check_eq!( sent, - words(&["HEXPIRE", "hash", "60", "FIELDS", "2", "f", "x"]) + bwords(&["HEXPIRE", "hash", "60", "FIELDS", "2", "f", "x"]) ); check_eq!( result, @@ -457,7 +475,7 @@ async fn hash_commands() -> Result<(), String> { commands::hrandfield(&conn, key(), None), ) .await?; - check_eq!(sent, words(&["HRANDFIELD", "hash"])); + check_eq!(sent, bwords(&["HRANDFIELD", "hash"])); check_eq!(result, Ok(words(&["f"]))); // RESP3 replies with values as [field, value] pairs @@ -470,8 +488,8 @@ async fn hash_commands() -> Result<(), String> { commands::hrandfield_withvalues(&conn, key(), 2), ) .await?; - check_eq!(sent, words(&["HRANDFIELD", "hash", "2", "WITHVALUES"])); - check_eq!(result, Ok(vec![(s("f"), s("v")), (s("g"), s("w"))])); + check_eq!(sent, bwords(&["HRANDFIELD", "hash", "2", "WITHVALUES"])); + check_eq!(result, Ok(vec![(s("f"), v("v")), (s("g"), v("w"))])); let (result, sent) = round_trip( &mut server, @@ -481,11 +499,11 @@ async fn hash_commands() -> Result<(), String> { .await?; check_eq!( sent, - words(&["HSCAN", "hash", "0", "MATCH", "f*", "COUNT", "10"]) + bwords(&["HSCAN", "hash", "0", "MATCH", "f*", "COUNT", "10"]) ); check_eq!( result, - Ok((Some(Cursor::Resume(s("17"))), Map::from([(s("f"), s("v"))]))) + Ok((Some(Cursor::Resume(s("17"))), Map::from([(s("f"), v("v"))]))) ); // the last page has a cursor of 0 @@ -495,7 +513,7 @@ async fn hash_commands() -> Result<(), String> { commands::hscan_novalues(&conn, key(), Cursor::Resume(s("17")), None, None), ) .await?; - check_eq!(sent, words(&["HSCAN", "hash", "17", "NOVALUES"])); + check_eq!(sent, bwords(&["HSCAN", "hash", "17", "NOVALUES"])); check_eq!(result, Ok((None, words(&["g"])))); let (result, sent) = round_trip( @@ -504,11 +522,174 @@ async fn hash_commands() -> Result<(), String> { commands::hdel(&conn, key(), words(&["f"])), ) .await?; - check_eq!(sent, words(&["HDEL", "hash", "f"])); + check_eq!(sent, bwords(&["HDEL", "hash", "f"])); check_eq!(result, Ok(1)); Ok(()) } +async fn binary_values() -> Result<(), String> { + let (mut server, conn) = establish().await?; + let binary = || StringOrBytes::Bytes(BINARY.to_vec()); + + // sent as is + let (result, sent) = round_trip( + &mut server, + ok(), + commands::set(&conn, s("key"), binary(), None, None), + ) + .await?; + check_eq!( + sent, + vec![b"SET".to_vec(), b"key".to_vec(), BINARY.to_vec()] + ); + check_eq!(result, Ok(true)); + + // a reply that is not UTF-8 is bytes, one that is is a string + let (result, _) = round_trip( + &mut server, + RespValue::BulkString(BINARY.to_vec()), + commands::get(&conn, s("key")), + ) + .await?; + check_eq!(result, Ok(Some(binary()))); + + let (result, _) = round_trip( + &mut server, + RespValue::BulkString(b"text".to_vec()), + commands::get(&conn, s("key")), + ) + .await?; + check_eq!(result, Ok(Some(v("text")))); + + let (result, _) = round_trip( + &mut server, + map(vec![ + (bulk("f"), RespValue::BulkString(BINARY.to_vec())), + (bulk("g"), bulk("w")), + ]), + commands::hgetall(&conn, s("hash")), + ) + .await?; + check_eq!( + result, + Ok(Map::from([(s("f"), binary()), (s("g"), v("w"))])) + ); + + // a field, a string, that is not UTF-8 fails the command + let (result, _) = round_trip( + &mut server, + map(vec![(RespValue::BulkString(BINARY.to_vec()), bulk("v"))]), + commands::hgetall(&conn, s("hash")), + ) + .await?; + check_eq!(result.is_err(), true); + Ok(()) +} + +async fn dump_and_restore_commands() -> Result<(), String> { + let (mut server, conn) = establish().await?; + + let (result, sent) = round_trip( + &mut server, + RespValue::BulkString(BINARY.to_vec()), + commands::dump(&conn, s("key")), + ) + .await?; + check_eq!(sent, bwords(&["DUMP", "key"])); + check_eq!(result, Ok(Some(BINARY.to_vec()))); + + let (result, _) = round_trip( + &mut server, + RespValue::Null, + commands::dump(&conn, s("missing")), + ) + .await?; + check_eq!(result, Ok(None)); + + // without a ttl, 0 + let (result, sent) = round_trip( + &mut server, + ok(), + commands::restore(&conn, s("key"), None, BINARY.to_vec(), false, None, None), + ) + .await?; + check_eq!( + sent, + vec![ + b"RESTORE".to_vec(), + b"key".to_vec(), + b"0".to_vec(), + BINARY.to_vec() + ] + ); + check_eq!(result, Ok(())); + + let (result, sent) = round_trip( + &mut server, + ok(), + commands::restore( + &conn, + s("key"), + Some(RestoreTtl::ExpireIn(DurationMilliseconds { px: 1500 })), + BINARY.to_vec(), + true, + Some(DurationSeconds { ex: 10 }), + Some(5), + ), + ) + .await?; + let mut expected = bwords(&["RESTORE", "key", "1500"]); + expected.push(BINARY.to_vec()); + expected.extend(bwords(&["REPLACE", "IDLETIME", "10", "FREQ", "5"])); + check_eq!(sent, expected); + check_eq!(result, Ok(())); + + // an expiration at a time is ABSTTL + let (result, sent) = round_trip( + &mut server, + ok(), + commands::restore_asking( + &conn, + s("key"), + Some(RestoreTtl::ExpireAt(UnixTimeMilliseconds { + pxat: 1_900_000_000_000, + })), + BINARY.to_vec(), + false, + None, + None, + ), + ) + .await?; + let mut expected = bwords(&["RESTORE-ASKING", "key", "1900000000000"]); + expected.push(BINARY.to_vec()); + expected.extend(bwords(&["ABSTTL"])); + check_eq!(sent, expected); + check_eq!(result, Ok(())); + + let (result, sent) = round_trip( + &mut server, + RespValue::BulkString(BINARY.to_vec()), + commands::function_dump(&conn), + ) + .await?; + check_eq!(sent, bwords(&["FUNCTION", "DUMP"])); + check_eq!(result, Ok(BINARY.to_vec())); + + let (result, sent) = round_trip( + &mut server, + ok(), + commands::function_restore(&conn, BINARY.to_vec(), Some(FunctionRestorePolicy::Replace)), + ) + .await?; + let mut expected = bwords(&["FUNCTION", "RESTORE"]); + expected.push(BINARY.to_vec()); + expected.extend(bwords(&["REPLACE"])); + check_eq!(sent, expected); + check_eq!(result, Ok(())); + Ok(()) +} + async fn connection_commands() -> Result<(), String> { let (mut server, conn) = establish().await?; @@ -518,7 +699,7 @@ async fn connection_commands() -> Result<(), String> { commands::ping(&conn, None), ) .await?; - check_eq!(sent, words(&["PING"])); + check_eq!(sent, bwords(&["PING"])); check_eq!(result, Ok(())); let (result, sent) = round_trip( @@ -527,7 +708,7 @@ async fn connection_commands() -> Result<(), String> { commands::auth(&conn, Some(s("user")), s("secret")), ) .await?; - check_eq!(sent, words(&["AUTH", "user", "secret"])); + check_eq!(sent, bwords(&["AUTH", "user", "secret"])); check_eq!(result, Ok(())); let (result, sent) = round_trip( @@ -536,7 +717,7 @@ async fn connection_commands() -> Result<(), String> { commands::client_setname(&conn, s("tests")), ) .await?; - check_eq!(sent, words(&["CLIENT", "SETNAME", "tests"])); + check_eq!(sent, bwords(&["CLIENT", "SETNAME", "tests"])); check_eq!(result, Ok(())); Ok(()) } @@ -554,13 +735,13 @@ async fn admin_and_pubsub_commands() -> Result<(), String> { commands::acl_setuser(&conn, s("user"), Some(rules)), ) .await?; - check_eq!(sent, words(&["ACL", "SETUSER", "user", "on", ">secret"])); + check_eq!(sent, bwords(&["ACL", "SETUSER", "user", "on", ">secret"])); check_eq!(result, Ok(())); let pass = "dd721260bfe1b3d9601e7fbab36de6d04e2e67b0ef1c53de59d45950db0dd3cc"; let (result, sent) = round_trip(&mut server, bulk(pass), commands::acl_genpass(&conn, None)).await?; - check_eq!(sent, words(&["ACL", "GENPASS"])); + check_eq!(sent, bwords(&["ACL", "GENPASS"])); check_eq!(result, Ok(s(pass))); let (result, sent) = round_trip( @@ -569,16 +750,16 @@ async fn admin_and_pubsub_commands() -> Result<(), String> { commands::acl_deluser(&conn, words(&["user"])), ) .await?; - check_eq!(sent, words(&["ACL", "DELUSER", "user"])); + check_eq!(sent, bwords(&["ACL", "DELUSER", "user"])); check_eq!(result, Ok(1)); let (result, sent) = round_trip( &mut server, RespValue::Integer(2), - commands::publish(&conn, s("news"), s("hello")), + commands::publish(&conn, s("news"), v("hello")), ) .await?; - check_eq!(sent, words(&["PUBLISH", "news", "hello"])); + check_eq!(sent, bwords(&["PUBLISH", "news", "hello"])); check_eq!(result, Ok(2)); Ok(()) } @@ -602,7 +783,7 @@ async fn invalid_arguments() -> Result<(), String> { commands::ping(&conn, None), ) .await?; - check_eq!(sent, words(&["PING"])); + check_eq!(sent, bwords(&["PING"])); Ok(()) } @@ -616,7 +797,7 @@ async fn unexpected_replies() -> Result<(), String> { .await?; check_eq!( result, - Err(Error::Client(s("unexpected reply: RespValue::Integer(7)"))) + Err(Error::Client(s("unexpected reply: integer<7>"))) ); Ok(()) } @@ -650,10 +831,10 @@ async fn with_valkey() -> Result<(), String> { check(commands::ping(&conn, None).await)?; check_eq!( - commands::set(&conn, key(), s("1"), None, None).await, + commands::set(&conn, key(), v("1"), None, None).await, Ok(true) ); - check_eq!(commands::get(&conn, key()).await, Ok(Some(s("1")))); + check_eq!(commands::get(&conn, key()).await, Ok(Some(v("1")))); check_eq!(commands::incr(&conn, key()).await, Ok(2)); check_eq!(commands::incrby(&conn, key(), 3).await, Ok(5)); check_eq!(commands::exists(&conn, vec![key()]).await, Ok(1)); @@ -670,17 +851,17 @@ async fn with_valkey() -> Result<(), String> { let key = || s("commands-test:hash"); check_eq!( - commands::hset(&conn, key(), vec![(s("f"), s("v")), (s("g"), s("w"))]).await, + commands::hset(&conn, key(), vec![(s("f"), v("v")), (s("g"), v("w"))]).await, Ok(2) ); check_eq!( commands::hgetall(&conn, key()).await, - Ok(Map::from([(s("f"), s("v")), (s("g"), s("w"))])) + Ok(Map::from([(s("f"), v("v")), (s("g"), v("w"))])) ); check_eq!(commands::hlen(&conn, key()).await, Ok(2)); check_eq!( commands::hmget(&conn, key(), words(&["f", "x"])).await, - Ok(vec![Some(s("v")), None]) + Ok(vec![Some(v("v")), None]) ); let fields = commands::hrandfield_withvalues(&conn, key(), 2) .await @@ -688,7 +869,7 @@ async fn with_valkey() -> Result<(), String> { check_eq!(fields.len(), 2); check_eq!( commands::hscan(&conn, key(), Cursor::Start, None, None).await, - Ok((None, Map::from([(s("f"), s("v")), (s("g"), s("w"))]))) + Ok((None, Map::from([(s("f"), v("v")), (s("g"), v("w"))]))) ); check_eq!( commands::hincrbyfloat(&conn, key(), s("n"), 1.5).await, @@ -700,15 +881,52 @@ async fn with_valkey() -> Result<(), String> { key(), DurationSeconds { ex: 60 }, None, - words(&["f", "x"]) + vec!["f".to_string(), "x".to_string()] ) .await, Ok(vec![HexpireResult::Applied, HexpireResult::DoesNotExist]) ); check_eq!(commands::del(&conn, vec![key()]).await, Ok(1)); + // binary values round trip + let key = || s("commands-test:binary"); + let binary = || StringOrBytes::Bytes(BINARY.to_vec()); + check_eq!( + commands::set(&conn, key(), binary(), None, None).await, + Ok(true) + ); + check_eq!(commands::get(&conn, key()).await, Ok(Some(binary()))); + + // the serialized value restores the key, with its value + let serialized = commands::dump(&conn, key()) + .await + .map_err(|error| format!("{error:?}"))? + .ok_or("expected the key to dump")?; + check_eq!( + commands::restore(&conn, key(), None, serialized.clone(), false, None, None) + .await + .is_err(), + true + ); + let ttl = RestoreTtl::ExpireIn(DurationMilliseconds { px: 60_000 }); + check_eq!( + commands::restore(&conn, key(), Some(ttl), serialized, true, None, None).await, + Ok(()) + ); + check_eq!(commands::get(&conn, key()).await, Ok(Some(binary()))); + check_eq!(commands::del(&conn, vec![key()]).await, Ok(1)); + + // the serialized libraries restore, appending none when there are none + let serialized = commands::function_dump(&conn) + .await + .map_err(|error| format!("{error:?}"))?; + check_eq!( + commands::function_restore(&conn, serialized, Some(FunctionRestorePolicy::Append)).await, + Ok(()) + ); + check_eq!( - commands::publish(&conn, s("commands-test"), s("hello")).await, + commands::publish(&conn, s("commands-test"), v("hello")).await, Ok(0) ); let pass = commands::acl_genpass(&conn, None) diff --git a/components/tcp/src/lib.rs b/components/tcp/src/lib.rs index 6ff9491..9b58393 100644 --- a/components/tcp/src/lib.rs +++ b/components/tcp/src/lib.rs @@ -162,12 +162,14 @@ async fn receive_pump( // bytes read from the socket that are not part of a whole value yet let mut buffered: Vec = Vec::new(); let result = 'pump: loop { - // hand over each whole value buffered, a value may span several reads + // hand over each whole value buffered, a value may span several reads, then drop them from + // the buffer at once + let mut start = 0; loop { - match componentized_valkey::resp::frame_len(&buffered) { + match componentized_valkey::resp::frame_len(&buffered[start..]) { Ok(Some(len)) => { - let rest = buffered.split_off(len); - let frame = std::mem::replace(&mut buffered, rest); + let frame = &buffered[start..start + len]; + start += len; match componentized_valkey::resp::decode(frame) { Ok(value) => { if replies.write_one(value).await.is_some() { @@ -182,6 +184,7 @@ async fn receive_pump( Err(error) => break 'pump Err(Error::Resp(error)), } } + buffered.drain(..start); match bytes.read(Vec::with_capacity(READ_SIZE)).await { (StreamResult::Complete(_), chunk) => buffered.extend_from_slice(&chunk), (StreamResult::Dropped | StreamResult::Cancelled, _) => { diff --git a/crates/componentized-valkey/src/command.rs b/crates/componentized-valkey/src/command.rs index 750ffd3..90b910a 100644 --- a/crates/componentized-valkey/src/command.rs +++ b/crates/componentized-valkey/src/command.rs @@ -1,7 +1,10 @@ //! Converting Rust values into the words of a command, the impls for Rust's own types and the //! helpers the commands share. The impls for the package's types are in [`crate::commands`]. -use crate::types::Error; +use crate::types::{Error, StringOrBytes}; + +/// The words of a command, each a string, or bytes, e.g. a binary value. +pub type Words = Vec; /// Converts a Rust type into the words of a command, e.g. `Option` into no word, or one. /// @@ -11,24 +14,31 @@ use crate::types::Error; /// A value the command does not accept, e.g. an empty list where it requires an item, fails with a /// client error, and the command is not sent. pub trait IntoCommand { - fn into_command(self) -> Result, Error>; + fn into_command(self) -> Result; +} + +/// A value as given, UTF-8 or not. +impl IntoCommand for StringOrBytes { + fn into_command(self) -> Result { + Ok(vec![self]) + } } impl IntoCommand for &str { - fn into_command(self) -> Result, Error> { - Ok(vec![self.to_string()]) + fn into_command(self) -> Result { + Ok(vec![self.into()]) } } impl IntoCommand for String { - fn into_command(self) -> Result, Error> { - Ok(vec![self]) + fn into_command(self) -> Result { + Ok(vec![self.into()]) } } impl IntoCommand for &String { - fn into_command(self) -> Result, Error> { - Ok(vec![self.clone()]) + fn into_command(self) -> Result { + Ok(vec![self.as_str().into()]) } } @@ -37,8 +47,8 @@ macro_rules! into_command_numbers { ($($number:ty),*) => { $( impl IntoCommand for $number { - fn into_command(self) -> Result, Error> { - Ok(vec![self.to_string()]) + fn into_command(self) -> Result { + Ok(vec![self.to_string().into()]) } } )* @@ -49,13 +59,13 @@ into_command_numbers!(i32, i64, u8, u16, u32, u64, usize, f64); /// No word for none. impl IntoCommand for Option { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { self.map_or(Ok(vec![]), IntoCommand::into_command) } } impl IntoCommand for Vec { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { let mut command = vec![]; for item in self { command.extend(item.into_command()?); @@ -65,7 +75,7 @@ impl IntoCommand for Vec { } impl IntoCommand for [T; N] { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { let mut command = vec![]; for item in self { command.extend(item.into_command()?); @@ -80,7 +90,7 @@ macro_rules! into_command_tuples { $( impl<$($item: IntoCommand),+> IntoCommand for ($($item,)+) { #[allow(non_snake_case)] - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { let ($($item,)+) = self; let mut command = vec![]; $(command.extend($item.into_command()?);)+ @@ -110,13 +120,9 @@ into_command_tuples!( pub struct Flag(pub &'static str, pub bool); impl IntoCommand for Flag { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { let Flag(word, present) = self; - Ok(if present { - vec![word.to_string()] - } else { - vec![] - }) + Ok(if present { vec![word.into()] } else { vec![] }) } } @@ -124,7 +130,7 @@ impl IntoCommand for Flag { pub struct NamedOption(pub &'static str, pub Option); impl IntoCommand for NamedOption { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { let NamedOption(name, value) = self; match value { Some(_) => (name, value).into_command(), @@ -153,7 +159,7 @@ impl NonEmpty { } impl IntoCommand for NonEmpty { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { let NonEmpty(name, items) = self; if items.is_empty() { return Err(Error::Client(format!("{name} requires at least one item"))); @@ -167,7 +173,7 @@ impl IntoCommand for NonEmpty { pub struct NumKeys(pub NonEmpty); impl IntoCommand for NumKeys { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { (self.0.len(), self.0).into_command() } } diff --git a/crates/componentized-valkey/src/commands/admin.rs b/crates/componentized-valkey/src/commands/admin.rs index 69f6518..85062fa 100644 --- a/crates/componentized-valkey/src/commands/admin.rs +++ b/crates/componentized-valkey/src/commands/admin.rs @@ -1,7 +1,7 @@ //! The admin commands: ACL, CLIENT, CLUSTER, COMMANDLOG, FAILOVER, LATENCY, MODULE, ROLE, SHUTDOWN //! and the like. -use crate::command::{Flag, IntoCommand, NonEmpty}; +use crate::command::{Flag, IntoCommand, NonEmpty, Words}; use crate::types::{ AclChannelRule, AclCommandRule, AclDbRule, AclDryrunResult, AclKeyRule, AclListEntry, AclLogEntry, AclRule, AclSelectorRule, AclUserRule, BgsaveOperation, ClientFilter, @@ -16,7 +16,7 @@ use crate::value::{Fields, FlatPairs, FromValue, items, parse_string}; use wit_bindgen::rt::Map; impl IntoCommand for AclRule { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { AclRule::Keys(rule) => rule.into_command(), AclRule::Channel(rule) => rule.into_command(), @@ -29,7 +29,7 @@ impl IntoCommand for AclRule { } impl IntoCommand for AclSelectorRule { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { AclSelectorRule::Keys(rule) => rule.into_command(), AclSelectorRule::Channel(rule) => rule.into_command(), @@ -41,31 +41,33 @@ impl IntoCommand for AclSelectorRule { } impl IntoCommand for AclKeyRule { - fn into_command(self) -> Result, Error> { - Ok(vec![match self { + fn into_command(self) -> Result { + match self { AclKeyRule::Add(pattern) => format!("~{pattern}"), AclKeyRule::AddRead(pattern) => format!("%R~{pattern}"), AclKeyRule::AddWrite(pattern) => format!("%W~{pattern}"), AclKeyRule::AddReadWrite(pattern) => format!("%RW~{pattern}"), AclKeyRule::All => "allkeys".to_string(), AclKeyRule::Reset => "resetkeys".to_string(), - }]) + } + .into_command() } } impl IntoCommand for AclChannelRule { - fn into_command(self) -> Result, Error> { - Ok(vec![match self { + fn into_command(self) -> Result { + match self { AclChannelRule::Add(pattern) => format!("&{pattern}"), AclChannelRule::All => "allchannels".to_string(), AclChannelRule::Reset => "resetchannels".to_string(), - }]) + } + .into_command() } } impl IntoCommand for AclCommandRule { - fn into_command(self) -> Result, Error> { - Ok(vec![match self { + fn into_command(self) -> Result { + match self { AclCommandRule::Add(command) => format!("+{command}"), AclCommandRule::AddCategory(category) => format!("+@{category}"), AclCommandRule::AddFirstArg((command, arg)) => format!("+{command}|{arg}"), @@ -73,14 +75,15 @@ impl IntoCommand for AclCommandRule { AclCommandRule::Remove(command) => format!("-{command}"), AclCommandRule::RemoveCategory(category) => format!("-@{category}"), AclCommandRule::Reset => "nocommands".to_string(), - }]) + } + .into_command() } } /// `db=` requires at least one database. impl IntoCommand for AclDbRule { - fn into_command(self) -> Result, Error> { - Ok(vec![match self { + fn into_command(self) -> Result { + match self { AclDbRule::Db(dbs) if dbs.is_empty() => { return Err(Error::Client("db requires at least one item".to_string())); } @@ -93,12 +96,13 @@ impl IntoCommand for AclDbRule { ), AclDbRule::All => "alldbs".to_string(), AclDbRule::Reset => "resetdbs".to_string(), - }]) + } + .into_command() } } impl IntoCommand for AclUserRule { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { AclUserRule::On => "on".into_command(), AclUserRule::Off => "off".into_command(), @@ -362,7 +366,7 @@ impl FromValue for AclLogEntry { } impl IntoCommand for BgsaveOperation { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { BgsaveOperation::Schedule => "SCHEDULE", BgsaveOperation::Cancel => "CANCEL", @@ -373,7 +377,7 @@ impl IntoCommand for BgsaveOperation { /// `master` rather than `primary`, which servers before Valkey 8.0 do not know. impl IntoCommand for ClientType { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { ClientType::Normal => "normal", ClientType::Primary => "master", @@ -385,7 +389,7 @@ impl IntoCommand for ClientType { } impl IntoCommand for ClientFilter { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { ClientFilter::Id(ids) => ("ID", NonEmpty::new("id", ids)).into_command(), ClientFilter::Type(client_type) => ("TYPE", client_type).into_command(), @@ -442,7 +446,7 @@ impl FromValue for ClientList { } impl IntoCommand for ClientPauseMode { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { ClientPauseMode::Write => "WRITE", ClientPauseMode::All => "ALL", @@ -452,7 +456,7 @@ impl IntoCommand for ClientPauseMode { } impl IntoCommand for ClientUnblockType { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { ClientUnblockType::Timeout => "TIMEOUT", ClientUnblockType::Error => "ERROR", @@ -477,7 +481,7 @@ impl FromValue for ClusterBumpepochResult { } impl IntoCommand for ClusterFailoverMode { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { ClusterFailoverMode::Force => "FORCE", ClusterFailoverMode::Takeover => "TAKEOVER", @@ -541,7 +545,7 @@ impl FromValue for ClusterGetslotmigrationsEntry { /// `SLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] NODE node-id [ AUTH username password ]` impl IntoCommand for ClusterMigrateslotsGroup { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { ( "SLOTSRANGE", NonEmpty::new("ranges", self.ranges), @@ -555,7 +559,7 @@ impl IntoCommand for ClusterMigrateslotsGroup { } impl IntoCommand for ClusterResetMode { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { ClusterResetMode::Hard => "HARD", ClusterResetMode::Soft => "SOFT", @@ -565,7 +569,7 @@ impl IntoCommand for ClusterResetMode { } impl IntoCommand for ClusterSetslotState { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { ClusterSetslotState::Importing(node_id) => ("IMPORTING", node_id).into_command(), ClusterSetslotState::Migrating(node_id) => ("MIGRATING", node_id).into_command(), @@ -576,7 +580,7 @@ impl IntoCommand for ClusterSetslotState { } impl IntoCommand for CommandlogType { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { CommandlogType::Slow => "slow", CommandlogType::LargeRequest => "large-request", @@ -609,7 +613,7 @@ impl FromValue for CommandlogEntry { /// `TO host port [ FORCE ]` impl IntoCommand for FailoverTarget { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { ("TO", self.host, self.port, Flag("FORCE", self.force)).into_command() } } @@ -701,7 +705,7 @@ impl FromValue for RoleReply { } impl IntoCommand for ShutdownSave { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { ShutdownSave::Nosave => "NOSAVE", ShutdownSave::Save => "SAVE", @@ -1015,7 +1019,11 @@ mod tests { let rules = entries.into_iter().next().unwrap().rules; // selectors are sent as words between parentheses assert_eq!( - words(rules).join(" "), + words(rules) + .iter() + .map(|word| String::from_utf8_lossy(word.as_bytes())) + .collect::>() + .join(" "), "on #abc ~app:* resetchannels &news:* nocommands +get ( %R~x resetchannels +get )" ); } diff --git a/crates/componentized-valkey/src/commands/bitmap.rs b/crates/componentized-valkey/src/commands/bitmap.rs index 1c6a7c2..e552ee7 100644 --- a/crates/componentized-valkey/src/commands/bitmap.rs +++ b/crates/componentized-valkey/src/commands/bitmap.rs @@ -1,6 +1,6 @@ //! The bitmap commands: BITCOUNT, BITFIELD, BITPOS, SETBIT and the like. -use crate::command::IntoCommand; +use crate::command::{IntoCommand, Words}; use crate::types::{ BitRange, BitfieldEncoding, BitfieldGet, BitfieldIncrby, BitfieldOffset, BitfieldOperation, BitfieldOverflow, BitfieldSet, Error, @@ -8,7 +8,7 @@ use crate::types::{ /// `start [ end [ BIT ] ]`, BYTE is the default, and left out for servers before it existed. impl IntoCommand for BitRange { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { BitRange::From(start) => start.into_command(), BitRange::Bytes((start, end)) => (start, end).into_command(), @@ -19,7 +19,7 @@ impl IntoCommand for BitRange { /// `i1` to `i64`, `u1` to `u63`, other widths fail. impl IntoCommand for BitfieldEncoding { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { BitfieldEncoding::Signed(width @ 1..=64) => format!("i{width}").into_command(), BitfieldEncoding::Unsigned(width @ 1..=63) => format!("u{width}").into_command(), @@ -35,7 +35,7 @@ impl IntoCommand for BitfieldEncoding { /// The offset in bits, or `#` and the index of a field of the encoding's width. impl IntoCommand for BitfieldOffset { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { BitfieldOffset::Bit(offset) => offset.into_command(), BitfieldOffset::Field(field) => format!("#{field}").into_command(), @@ -44,7 +44,7 @@ impl IntoCommand for BitfieldOffset { } impl IntoCommand for BitfieldOverflow { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { let overflow = match self { BitfieldOverflow::Wrap => "WRAP", BitfieldOverflow::Sat => "SAT", @@ -56,26 +56,26 @@ impl IntoCommand for BitfieldOverflow { /// Also `BitfieldRoGet`, the same type. impl IntoCommand for BitfieldGet { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { ("GET", self.encoding, self.offset).into_command() } } impl IntoCommand for BitfieldSet { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { ("SET", self.encoding, self.offset, self.value).into_command() } } impl IntoCommand for BitfieldIncrby { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { ("INCRBY", self.encoding, self.offset, self.increment).into_command() } } /// A write is preceded by its overflow mode, when given. impl IntoCommand for BitfieldOperation { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { BitfieldOperation::Get(get) => get.into_command(), BitfieldOperation::Set((overflow, set)) => (overflow, set).into_command(), @@ -88,7 +88,7 @@ impl IntoCommand for BitfieldOperation { pub struct Bit(pub bool); impl IntoCommand for Bit { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { if self.0 { "1" } else { "0" }.into_command() } } diff --git a/crates/componentized-valkey/src/commands/connection.rs b/crates/componentized-valkey/src/commands/connection.rs index a90f6b3..0c11ce2 100644 --- a/crates/componentized-valkey/src/commands/connection.rs +++ b/crates/componentized-valkey/src/commands/connection.rs @@ -1,6 +1,6 @@ //! The connection commands: CLIENT of the connection itself, and COMMAND. -use crate::command::{Flag, IntoCommand, NonEmpty}; +use crate::command::{Flag, IntoCommand, NonEmpty, Words}; use crate::types::{ ClientSetinfoAttr, ClientTrackingMode, ClientTrackingOptions, ClientTrackinginfoRedirect, ClientTrackinginfoReply, CommandDocsArgument, CommandDocsArgumentType, CommandDocsReply, @@ -11,7 +11,7 @@ use crate::types::{ use crate::value::{Fields, FlatPairs, FromValue, entries, items}; impl IntoCommand for ClientSetinfoAttr { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { ClientSetinfoAttr::LibName(name) => ("LIB-NAME", name).into_command(), ClientSetinfoAttr::LibVer(version) => ("LIB-VER", version).into_command(), @@ -20,7 +20,7 @@ impl IntoCommand for ClientSetinfoAttr { } impl IntoCommand for ClientTrackingMode { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { ClientTrackingMode::Optin => "OPTIN", ClientTrackingMode::Optout => "OPTOUT", @@ -31,7 +31,7 @@ impl IntoCommand for ClientTrackingMode { /// `[ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN | OPTOUT ] [ NOLOOP ]` impl IntoCommand for ClientTrackingOptions { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { // PREFIX before each prefix let prefixes = self.prefixes.map(|prefixes| { let prefixes = prefixes.into_iter().map(|prefix| ("PREFIX", prefix)); @@ -72,7 +72,7 @@ impl FromValue for ClientTrackinginfoReply { /// `FILTERBY 〈 MODULE module-name | ACLCAT category | PATTERN pattern 〉` impl IntoCommand for CommandListFilter { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { let (filter, value) = match self { CommandListFilter::Module(name) => ("MODULE", name), CommandListFilter::Aclcat(category) => ("ACLCAT", category), @@ -86,7 +86,7 @@ impl IntoCommand for CommandListFilter { pub struct OnOff(pub bool); impl IntoCommand for OnOff { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { if self.0 { "ON" } else { "OFF" }.into_command() } } diff --git a/crates/componentized-valkey/src/commands/geo.rs b/crates/componentized-valkey/src/commands/geo.rs index 6965ed6..d409e88 100644 --- a/crates/componentized-valkey/src/commands/geo.rs +++ b/crates/componentized-valkey/src/commands/geo.rs @@ -1,15 +1,15 @@ //! The geo commands: GEOADD, GEODIST, GEOPOS, GEOSEARCH and the like. -use crate::command::{Flag, IntoCommand, NonEmpty, NumKeys}; +use crate::command::{Flag, IntoCommand, NonEmpty, NumKeys, Words}; use crate::types::{ Double, Error, GeoCoordinates, GeoUnit, GeoaddCondition, GeosearchCount, GeosearchFrom, - GeosearchShape, GeosearchWithResult, RespValue, + GeosearchShape, GeosearchWithResult, RespValue, StringOrBytes, }; use crate::value::{FromValue, items, unexpected}; /// `longitude latitude` impl IntoCommand for GeoCoordinates { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { (self.longitude, self.latitude).into_command() } } @@ -26,7 +26,7 @@ impl FromValue for GeoCoordinates { } impl IntoCommand for GeoUnit { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { GeoUnit::M => "m", GeoUnit::Km => "km", @@ -38,7 +38,7 @@ impl IntoCommand for GeoUnit { } impl IntoCommand for GeoaddCondition { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { GeoaddCondition::Nx => "NX", GeoaddCondition::Xx => "XX", @@ -48,7 +48,7 @@ impl IntoCommand for GeoaddCondition { } impl IntoCommand for GeosearchFrom { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { GeosearchFrom::Member(member) => ("FROMMEMBER", member).into_command(), GeosearchFrom::Lonlat(coordinates) => ("FROMLONLAT", coordinates).into_command(), @@ -58,7 +58,7 @@ impl IntoCommand for GeosearchFrom { /// The center, when the shape has one, then the shape. Also `GeosearchstoreShape`, the same type. impl IntoCommand for GeosearchShape { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { GeosearchShape::Radius(circle) => { (circle.from, "BYRADIUS", circle.radius, circle.unit).into_command() @@ -80,7 +80,7 @@ impl IntoCommand for GeosearchShape { /// `COUNT count [ ANY ]`, also `GeosearchstoreCount`, the same type. impl IntoCommand for GeosearchCount { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { ("COUNT", self.count, Flag("ANY", self.any)).into_command() } } @@ -90,7 +90,7 @@ impl IntoCommand for GeosearchCount { impl FromValue for GeosearchWithResult { fn from_value(value: RespValue) -> Result { let mut result = GeosearchWithResult { - member: String::new(), + member: StringOrBytes::String(String::new()), distance: None, hash: None, coordinates: None, @@ -101,7 +101,7 @@ impl FromValue for GeosearchWithResult { value => vec![value], }; let mut items = items.into_iter(); - result.member = String::from_value(items.next().unwrap_or(RespValue::Null))?; + result.member = StringOrBytes::from_value(items.next().unwrap_or(RespValue::Null))?; for item in items { match item { // a bulk string, as RESP3 has no other way to send a fixed precision double @@ -159,7 +159,7 @@ mod tests { #[test] fn geosearch_from() { assert_eq!( - words(GeosearchFrom::Member("Palermo".to_string())), + words(GeosearchFrom::Member("Palermo".into())), ["FROMMEMBER", "Palermo"] ); assert_eq!( @@ -171,7 +171,7 @@ mod tests { #[test] fn geosearch_shape() { let radius = GeosearchShape::Radius(GeosearchRadius { - from: GeosearchFrom::Member("Palermo".to_string()), + from: GeosearchFrom::Member("Palermo".into()), radius: 200.5, unit: GeoUnit::Km, }); @@ -227,7 +227,7 @@ mod tests { assert_eq!( from::(bulk("Palermo")), GeosearchWithResult { - member: "Palermo".to_string(), + member: "Palermo".into(), distance: None, hash: None, coordinates: None, @@ -242,7 +242,7 @@ mod tests { assert_eq!( from::(value), GeosearchWithResult { - member: "Palermo".to_string(), + member: "Palermo".into(), distance: Some(190.4424), hash: Some(3479099956230698), coordinates: Some(coordinates(13.5, 38.25)), @@ -252,7 +252,7 @@ mod tests { assert_eq!( from::(value), GeosearchWithResult { - member: "Palermo".to_string(), + member: "Palermo".into(), distance: None, hash: Some(3479099956230698), coordinates: None, @@ -263,7 +263,7 @@ mod tests { bulk("Palermo"), RespValue::Boolean(true) ])), - "unexpected reply: RespValue::Boolean(true)" + "unexpected reply: boolean" ); } } diff --git a/crates/componentized-valkey/src/commands/hash.rs b/crates/componentized-valkey/src/commands/hash.rs index a9f0c91..2fbf7b4 100644 --- a/crates/componentized-valkey/src/commands/hash.rs +++ b/crates/componentized-valkey/src/commands/hash.rs @@ -1,6 +1,6 @@ //! The hash commands: HEXPIRE, HPERSIST, HSETEX and the like. -use crate::command::{IntoCommand, NonEmpty, NumKeys}; +use crate::command::{IntoCommand, NonEmpty, NumKeys, Words}; use crate::types::{ Error, HexpireResult, HpersistResult, HsetexCondition, HsetexFieldCondition, Integer, RespValue, }; @@ -10,7 +10,7 @@ use crate::value::FromValue; pub struct Fields(pub NonEmpty); impl IntoCommand for Fields { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { ("FIELDS", NumKeys(self.0)).into_command() } } @@ -40,7 +40,7 @@ impl FromValue for HpersistResult { } impl IntoCommand for HsetexCondition { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { HsetexCondition::Nx => "NX", HsetexCondition::Xx => "XX", @@ -50,7 +50,7 @@ impl IntoCommand for HsetexCondition { } impl IntoCommand for HsetexFieldCondition { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { HsetexFieldCondition::Fnx => "FNX", HsetexFieldCondition::Fxx => "FXX", diff --git a/crates/componentized-valkey/src/commands/keyspace.rs b/crates/componentized-valkey/src/commands/keyspace.rs index 6533e52..231d25d 100644 --- a/crates/componentized-valkey/src/commands/keyspace.rs +++ b/crates/componentized-valkey/src/commands/keyspace.rs @@ -1,12 +1,12 @@ //! The keyspace commands: MIGRATE, SCAN, SORT, TYPE and the like. -use crate::command::{Flag, IntoCommand, NamedOption, NonEmpty}; +use crate::command::{Flag, IntoCommand, NamedOption, NonEmpty, Words}; use crate::types::{Error, KeyType, MigrateAuth, RespValue, SortOpts}; use crate::value::{FromValue, unexpected}; /// The name of the type, e.g. for SCAN's TYPE. impl IntoCommand for KeyType { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { KeyType::String => "string".to_string(), KeyType::List => "list".to_string(), @@ -41,7 +41,7 @@ impl FromValue for OptionalKeyType { } impl IntoCommand for MigrateAuth { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { MigrateAuth::Password(password) => ("AUTH", password).into_command(), MigrateAuth::UsernamePassword((username, password)) => { @@ -67,7 +67,7 @@ impl FromValue for Migrated { /// `[ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ]`, also /// `SortRoOpts`, the same type. impl IntoCommand for SortOpts { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { // GET before each pattern let get = self.get.map(|patterns| { let patterns = patterns.into_iter().map(|pattern| ("GET", pattern)); @@ -153,7 +153,7 @@ mod tests { assert!(!from::(simple("NOKEY")).0); assert_eq!( from_error::(bulk("OK")), - r#"unexpected reply: RespValue::BulkString("OK")"# + r#"unexpected reply: bulk-string<"OK">"# ); } diff --git a/crates/componentized-valkey/src/commands/list.rs b/crates/componentized-valkey/src/commands/list.rs index 0b4601a..6c02d8b 100644 --- a/crates/componentized-valkey/src/commands/list.rs +++ b/crates/componentized-valkey/src/commands/list.rs @@ -1,11 +1,11 @@ //! The list commands: LINSERT, LMOVE, LMPOP and the like. -use crate::command::IntoCommand; +use crate::command::{IntoCommand, Words}; use crate::types::{Error, Integer, LinsertPosition, LinsertResult, ListSide, RespValue}; use crate::value::FromValue; impl IntoCommand for ListSide { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { ListSide::Left => "LEFT", ListSide::Right => "RIGHT", @@ -15,7 +15,7 @@ impl IntoCommand for ListSide { } impl IntoCommand for LinsertPosition { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { LinsertPosition::Before => "BEFORE", LinsertPosition::After => "AFTER", diff --git a/crates/componentized-valkey/src/commands/scripting.rs b/crates/componentized-valkey/src/commands/scripting.rs index 9b4eda0..9e544de 100644 --- a/crates/componentized-valkey/src/commands/scripting.rs +++ b/crates/componentized-valkey/src/commands/scripting.rs @@ -1,14 +1,14 @@ //! The scripting commands: EVAL, FCALL, FUNCTION and SCRIPT, and the FLUSH modes they share. -use crate::command::{IntoCommand, NonEmpty}; +use crate::command::{IntoCommand, NonEmpty, Words}; use crate::types::{ Error, FlushMode, FunctionListFunction, FunctionListLibrary, FunctionStatsEngine, - FunctionStatsReply, FunctionStatsRunning, RespValue, + FunctionRestorePolicy, FunctionStatsReply, FunctionStatsRunning, RespValue, StringOrBytes, }; use crate::value::{Fields, FromValue}; impl IntoCommand for FlushMode { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { FlushMode::Async => "ASYNC", FlushMode::Sync => "SYNC", @@ -17,11 +17,22 @@ impl IntoCommand for FlushMode { } } +impl IntoCommand for FunctionRestorePolicy { + fn into_command(self) -> Result { + match self { + FunctionRestorePolicy::Flush => "FLUSH", + FunctionRestorePolicy::Append => "APPEND", + FunctionRestorePolicy::Replace => "REPLACE", + } + .into_command() + } +} + /// `numkeys [ key ... ] [ arg ... ]`, e.g. for EVAL, `0` without keys. -pub struct KeysAndArgs(pub Option>, pub Option>); +pub struct KeysAndArgs(pub Option>, pub Option>); impl IntoCommand for KeysAndArgs { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { let KeysAndArgs(keys, args) = self; let numkeys = keys.as_ref().map_or(0, Vec::len); ( @@ -94,6 +105,18 @@ mod tests { use crate::test_util::{array, bulk, bulks, fields, from, from_error, int, words, words_error}; use crate::types::DurationMilliseconds; + #[test] + fn function_restore_policy() { + assert_eq!( + words([ + FunctionRestorePolicy::Flush, + FunctionRestorePolicy::Append, + FunctionRestorePolicy::Replace, + ]), + ["FLUSH", "APPEND", "REPLACE"] + ); + } + #[test] fn flush_mode() { assert_eq!( @@ -108,14 +131,11 @@ mod tests { assert_eq!( words(KeysAndArgs( Some(vec!["a".to_string(), "b".to_string()]), - Some(vec!["x".to_string()]) + Some(vec!["x".into()]) )), ["2", "a", "b", "x"] ); - assert_eq!( - words(KeysAndArgs(None, Some(vec!["x".to_string()]))), - ["0", "x"] - ); + assert_eq!(words(KeysAndArgs(None, Some(vec!["x".into()]))), ["0", "x"]); assert_eq!( words_error(KeysAndArgs(Some(vec![]), None)), "keys requires at least one item" diff --git a/crates/componentized-valkey/src/commands/shared.rs b/crates/componentized-valkey/src/commands/shared.rs index 7394236..f00624e 100644 --- a/crates/componentized-valkey/src/commands/shared.rs +++ b/crates/componentized-valkey/src/commands/shared.rs @@ -1,6 +1,6 @@ //! Shared by the commands: times, cursors, the expiration of keys and fields, limits and sort order. -use crate::command::IntoCommand; +use crate::command::{IntoCommand, Words}; use crate::types::{ Cursor, DurationMilliseconds, DurationSeconds, Error, ExpiretimeReply, Integer, Limit, PexpiretimeReply, PttlReply, RespValue, SortOrder, TtlMode, TtlReply, UnixTimeMilliseconds, @@ -10,7 +10,7 @@ use crate::value::FromValue; /// Whole seconds, e.g. for EX. impl IntoCommand for DurationSeconds { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { self.ex.into_command() } } @@ -24,7 +24,7 @@ impl FromValue for DurationSeconds { /// Whole milliseconds, e.g. for PX. impl IntoCommand for DurationMilliseconds { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { self.px.into_command() } } @@ -38,7 +38,7 @@ impl FromValue for DurationMilliseconds { /// Whole seconds since the Unix epoch, e.g. for EXAT. impl IntoCommand for UnixTimeSeconds { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { self.exat.into_command() } } @@ -52,7 +52,7 @@ impl FromValue for UnixTimeSeconds { /// Whole milliseconds since the Unix epoch, e.g. for PXAT. impl IntoCommand for UnixTimeMilliseconds { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { self.pxat.into_command() } } @@ -68,19 +68,15 @@ impl FromValue for UnixTimeMilliseconds { pub struct DecimalSeconds(pub DurationMilliseconds); impl IntoCommand for DecimalSeconds { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { let milliseconds = self.0.px; - Ok(vec![format!( - "{}.{:03}", - milliseconds / 1_000, - milliseconds % 1_000 - )]) + format!("{}.{:03}", milliseconds / 1_000, milliseconds % 1_000).into_command() } } /// `0` to start, failing to resume from `0`, which would restart the scan. impl IntoCommand for Cursor { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { Cursor::Start => "0".into_command(), Cursor::Resume(cursor) if cursor == "0" => Err(Error::Client( @@ -104,7 +100,7 @@ impl FromValue for NextCursor { } impl IntoCommand for TtlMode { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { TtlMode::Nx => "NX", TtlMode::Xx => "XX", @@ -142,13 +138,13 @@ from_value_ttl_replies!( /// `LIMIT offset count` impl IntoCommand for Limit { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { ("LIMIT", self.offset, self.count).into_command() } } impl IntoCommand for SortOrder { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { SortOrder::Asc => "ASC", SortOrder::Desc => "DESC", diff --git a/crates/componentized-valkey/src/commands/sortedset.rs b/crates/componentized-valkey/src/commands/sortedset.rs index 34c1e38..24373fe 100644 --- a/crates/componentized-valkey/src/commands/sortedset.rs +++ b/crates/componentized-valkey/src/commands/sortedset.rs @@ -1,9 +1,9 @@ //! The sorted set commands: ZADD, ZINTER, ZMPOP, ZRANGE, ZUNION and the like. -use crate::command::{Flag, IntoCommand}; +use crate::command::{Flag, IntoCommand, Words}; use crate::types::{ - Double, Error, LexBound, Limit, RespValue, ScoreBound, ZaddComparison, ZaddCondition, ZmpopEnd, - ZrangeBy, ZsetAggregate, + Double, Error, LexBound, Limit, RespValue, ScoreBound, StringOrBytes, ZaddComparison, + ZaddCondition, ZmpopEnd, ZrangeBy, ZsetAggregate, }; use crate::value::{FromValue, items}; @@ -15,11 +15,11 @@ pub struct ScoreRange { } impl IntoCommand for ScoreRange { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { let bound = |bound, unbounded: &str| match bound { - ScoreBound::Inclusive(score) => score.to_string(), - ScoreBound::Exclusive(score) => format!("({score}"), - ScoreBound::Unbounded => unbounded.to_string(), + ScoreBound::Inclusive(score) => score.to_string().into(), + ScoreBound::Exclusive(score) => format!("({score}").into(), + ScoreBound::Unbounded => unbounded.into(), }; let (low, high) = ("-inf", "+inf"); let (start, stop) = if self.rev { (high, low) } else { (low, high) }; @@ -35,11 +35,12 @@ pub struct LexRange { } impl IntoCommand for LexRange { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { + // a member may be bytes, the prefix keeps it so let bound = |bound, unbounded: &str| match bound { - LexBound::Inclusive(member) => format!("[{member}"), - LexBound::Exclusive(member) => format!("({member}"), - LexBound::Unbounded => unbounded.to_string(), + LexBound::Inclusive(member) => StringOrBytes::prefixed(member, "["), + LexBound::Exclusive(member) => StringOrBytes::prefixed(member, "("), + LexBound::Unbounded => unbounded.into(), }; let (low, high) = ("-", "+"); let (start, stop) = if self.rev { (high, low) } else { (low, high) }; @@ -55,7 +56,7 @@ pub struct Zrange { } impl IntoCommand for Zrange { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { let Zrange { range, rev, limit } = self; let (range, by) = match range { ZrangeBy::Rank((start, stop)) => ((start, stop).into_command()?, None), @@ -72,7 +73,7 @@ impl IntoCommand for Zrange { } impl IntoCommand for ZsetAggregate { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { let aggregate = match self { ZsetAggregate::Sum => "SUM", ZsetAggregate::Min => "MIN", @@ -84,7 +85,7 @@ impl IntoCommand for ZsetAggregate { /// Also `BzmpopEnd`, the same type. impl IntoCommand for ZmpopEnd { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { ZmpopEnd::Min => "MIN", ZmpopEnd::Max => "MAX", @@ -94,7 +95,7 @@ impl IntoCommand for ZmpopEnd { } impl IntoCommand for ZaddCondition { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { ZaddCondition::Nx => "NX", ZaddCondition::Xx => "XX", @@ -104,7 +105,7 @@ impl IntoCommand for ZaddCondition { } impl IntoCommand for ZaddComparison { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { ZaddComparison::Gt => "GT", ZaddComparison::Lt => "LT", @@ -115,7 +116,7 @@ impl IntoCommand for ZaddComparison { /// Members and their scores, e.g. from ZPOPMIN, which replies with one pair, or none, without a /// count, and a list of pairs with one. -pub struct ScoredMembers(pub Vec<(String, Double)>); +pub struct ScoredMembers(pub Vec<(StringOrBytes, Double)>); impl FromValue for ScoredMembers { fn from_value(value: RespValue) -> Result { @@ -129,7 +130,7 @@ impl FromValue for ScoredMembers { .map(ScoredMembers), Some(_) => match <[RespValue; 2]>::try_from(items) { Ok([member, score]) => Ok(ScoredMembers(vec![( - String::from_value(member)?, + StringOrBytes::from_value(member)?, Double::from_value(score)?, )])), Err(items) => Err(Error::Client(format!( @@ -173,8 +174,8 @@ mod tests { let range = |start, stop, rev| LexRange { start, stop, rev }; assert_eq!( words(range( - LexBound::Inclusive("a".to_string()), - LexBound::Exclusive("c".to_string()), + LexBound::Inclusive("a".into()), + LexBound::Exclusive("c".into()), false )), ["[a", "(c"] @@ -210,7 +211,7 @@ mod tests { ["+inf", "(1", "BYSCORE", "REV", "LIMIT", "0", "5"] ); let by_lex = Zrange { - range: ZrangeBy::Lex((LexBound::Inclusive("a".to_string()), LexBound::Unbounded)), + range: ZrangeBy::Lex((LexBound::Inclusive("a".into()), LexBound::Unbounded)), rev: false, limit: None, }; @@ -237,7 +238,7 @@ mod tests { // one pair, flat assert_eq!( from::(array(vec![bulk("a"), double(1.5)])).0, - [("a".to_string(), 1.5)] + [(StringOrBytes::from("a"), 1.5)] ); // a list of pairs, the scores as doubles or, with RESP2, bulk strings assert_eq!( @@ -246,7 +247,10 @@ mod tests { bulks(&["b", "2"]), ])) .0, - [("a".to_string(), 1.0), ("b".to_string(), 2.0)] + [ + (StringOrBytes::from("a"), 1.0), + (StringOrBytes::from("b"), 2.0) + ] ); assert_eq!( from_error::(bulks(&["a", "1", "b"])), diff --git a/crates/componentized-valkey/src/commands/stream.rs b/crates/componentized-valkey/src/commands/stream.rs index 32a4821..d04aa6f 100644 --- a/crates/componentized-valkey/src/commands/stream.rs +++ b/crates/componentized-valkey/src/commands/stream.rs @@ -1,6 +1,6 @@ //! The stream commands: XADD, XAUTOCLAIM, XCLAIM, XINFO, XPENDING, XTRIM and the like. -use crate::command::{Flag, IntoCommand, NamedOption}; +use crate::command::{Flag, IntoCommand, NamedOption, Words}; use crate::types::{ DurationMilliseconds, Error, RespValue, StreamId, StreamTrim, StreamTrimEfficiency, UnixTimeMilliseconds, XclaimOpts, XinfoConsumersEntry, XinfoGroupsEntry, XinfoStreamReply, @@ -10,7 +10,7 @@ use crate::types::{ use crate::value::{Fields, FromValue, OrNever}; impl IntoCommand for StreamTrim { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { //〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] match self { StreamTrim::Maxlen(trim) => ( @@ -32,7 +32,7 @@ impl IntoCommand for StreamTrim { } impl IntoCommand for StreamTrimEfficiency { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { // = | ~ (match self { StreamTrimEfficiency::Exact => "=", @@ -43,7 +43,7 @@ impl IntoCommand for StreamTrimEfficiency { } impl IntoCommand for XclaimOpts { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { // [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] [ LASTID lastid ] ( NamedOption("IDLE", self.idle), @@ -64,8 +64,8 @@ pub const ZERO_ID: &str = "0-0"; /// `0-0` for none, e.g. to start XAUTOCLAIM from the beginning. impl IntoCommand for OrZeroId { - fn into_command(self) -> Result, Error> { - Ok(vec![self.0.unwrap_or_else(|| ZERO_ID.to_string())]) + fn into_command(self) -> Result { + self.0.unwrap_or_else(|| ZERO_ID.to_string()).into_command() } } @@ -213,12 +213,13 @@ mod tests { use crate::test_util::{array, bulk, bulks, fields, from, from_error, int, words}; use crate::types::{StreamTrimMaxlen, StreamTrimMinid}; + use crate::types::StringOrBytes; use wit_bindgen::rt::Map; - fn entry(id: &str, field: &str, value: &str) -> (StreamId, Map) { + fn entry(id: &str, field: &str, value: &str) -> (StreamId, Map) { ( id.to_string(), - [(field.to_string(), value.to_string())] + [(field.to_string(), StringOrBytes::from(value))] .into_iter() .collect(), ) diff --git a/crates/componentized-valkey/src/commands/string.rs b/crates/componentized-valkey/src/commands/string.rs index 9ecb407..06dcca7 100644 --- a/crates/componentized-valkey/src/commands/string.rs +++ b/crates/componentized-valkey/src/commands/string.rs @@ -1,6 +1,6 @@ //! The string commands: GETEX, LCS, MSETEX, SET and the like. -use crate::command::IntoCommand; +use crate::command::{IntoCommand, Words}; use crate::types::{ Error, GetexTtl, LcsIdxMatch, LcsIdxReply, MsetexPrecondition, RespValue, SetPrecondition, SetTtl, @@ -9,7 +9,7 @@ use crate::value::{Fields, FromValue, items}; /// Also `HgetexTtl`, the same type. impl IntoCommand for GetexTtl { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { GetexTtl::Ex(duration) => ("EX", duration).into_command(), GetexTtl::Px(duration) => ("PX", duration).into_command(), @@ -51,7 +51,7 @@ impl FromValue for LcsIdxMatch { } impl IntoCommand for MsetexPrecondition { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { MsetexPrecondition::Nx => "NX".into_command(), MsetexPrecondition::Xx => "XX".into_command(), @@ -60,7 +60,7 @@ impl IntoCommand for MsetexPrecondition { } impl IntoCommand for SetPrecondition { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { SetPrecondition::Nx => "NX".into_command(), SetPrecondition::Xx => "XX".into_command(), @@ -72,7 +72,7 @@ impl IntoCommand for SetPrecondition { /// Also `MsetexTtl` and `HsetexTtl`, the same type. impl IntoCommand for SetTtl { - fn into_command(self) -> Result, Error> { + fn into_command(self) -> Result { match self { SetTtl::Ex(duration) => ("EX", duration).into_command(), SetTtl::Px(duration) => ("PX", duration).into_command(), @@ -153,8 +153,8 @@ mod tests { words(vec![ SetPrecondition::Nx, SetPrecondition::Xx, - SetPrecondition::Ifeq("old".to_string()), - SetPrecondition::Ifne("other".to_string()), + SetPrecondition::Ifeq("old".into()), + SetPrecondition::Ifne("other".into()), ]), ["NX", "XX", "IFEQ", "old", "IFNE", "other"] ); diff --git a/crates/componentized-valkey/src/lib.rs b/crates/componentized-valkey/src/lib.rs index 6f44dea..bfddc84 100644 --- a/crates/componentized-valkey/src/lib.rs +++ b/crates/componentized-valkey/src/lib.rs @@ -40,6 +40,7 @@ pub use bindings::componentized::valkey::types; pub mod command; pub mod commands; pub mod resp; +mod string_or_bytes; pub mod value; #[cfg(test)] diff --git a/crates/componentized-valkey/src/resp.rs b/crates/componentized-valkey/src/resp.rs index 8089441..2c3342e 100644 --- a/crates/componentized-valkey/src/resp.rs +++ b/crates/componentized-valkey/src/resp.rs @@ -46,7 +46,7 @@ fn buf_encode(value: RespValue, buf: &mut Vec) { buf.push(b'$'); buf.extend_from_slice(val.len().to_string().as_bytes()); buf.extend_from_slice(CRLF_BYTES); - buf.extend_from_slice(val.as_bytes()); + buf.extend(val); buf.extend_from_slice(CRLF_BYTES); } RespValue::Array(val) => { @@ -127,12 +127,12 @@ fn buf_encode(value: RespValue, buf: &mut Vec) { /// ``` /// # use componentized_valkey::resp::{decode, encode}; /// # use componentized_valkey::types::RespValue; -/// let value = RespValue::BulkString("Hello".to_string()); -/// assert_eq!(decode(encode(value.clone())), Ok(value)); +/// let value = RespValue::BulkString(b"Hello".to_vec()); +/// assert_eq!(decode(&encode(value.clone())), Ok(value)); /// ``` -pub fn decode(value: Vec) -> Result { - match frame_end(&value, 0)? { - Some(end) if end == value.len() => decode_frame(&value), +pub fn decode(value: &[u8]) -> Result { + match frame_end(value, 0)? { + Some(end) if end == value.len() => decode_frame(value), Some(end) => Err(format!( "unexpected bytes after the RESP value: {}", value.len() - end @@ -250,7 +250,7 @@ fn decode_frame(buf: &[u8]) -> Result { b'$' => match parse_integer(header)? { // a null bulk string, RESP2's null -1 => Ok(RespValue::Null), - _ => parse_string(blob()?).map(RespValue::BulkString), + _ => Ok(RespValue::BulkString(blob()?.to_vec())), }, b'!' => parse_string(blob()?).map(RespValue::BulkError), b'=' => { @@ -282,6 +282,69 @@ fn decode_frame(buf: &[u8]) -> Result { } } +/// RESP values for people to read, e.g. in errors, each with its type, e.g. `integer<1>` and +/// `array, null>`. A bulk string that is not UTF-8 is shown escaped, e.g. +/// `bulk-string`. +impl std::fmt::Display for RespValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::String(value) => write!(f, "string<{value:?}>"), + Self::Error(value) => write!(f, "error<{value:?}>"), + Self::Integer(value) => write!(f, "integer<{value}>"), + Self::BulkString(value) => match std::str::from_utf8(value) { + Ok(text) => write!(f, "bulk-string<{text:?}>"), + Err(_) => write!(f, "bulk-string", value.escape_ascii()), + }, + Self::Array(items) => write_items(f, "array", items), + Self::Null => f.write_str("null"), + Self::Boolean(value) => write!(f, "boolean<{value}>"), + Self::Double(value) => write!(f, "double<{value}>"), + Self::BigNumber(value) => write!(f, "big-number<{value}>"), + Self::BulkError(value) => write!(f, "bulk-error<{value:?}>"), + Self::VerbatimString((format, value)) => { + write!(f, "verbatim-string<{format}, {value:?}>") + } + Self::Map(entries) => { + f.write_str("map<")?; + for (index, (key, value)) in entries.iter().enumerate() { + if index > 0 { + f.write_str(", ")?; + } + write_nested(f, key)?; + f.write_str(": ")?; + write_nested(f, value)?; + } + f.write_str(">") + } + Self::Set(items) => write_items(f, "set", items), + Self::Push(items) => write_items(f, "push", items), + } + } +} + +/// The nested values of an aggregate, e.g. `array, null>`. +fn write_items( + f: &mut std::fmt::Formatter<'_>, + kind: &str, + items: &[RespNestedValue], +) -> std::fmt::Result { + write!(f, "{kind}<")?; + for (index, item) in items.iter().enumerate() { + if index > 0 { + f.write_str(", ")?; + } + write_nested(f, item)?; + } + f.write_str(">") +} + +fn write_nested(f: &mut std::fmt::Formatter<'_>, value: &[u8]) -> std::fmt::Result { + match decode(value) { + Ok(value) => write!(f, "{value}"), + Err(_) => f.write_str("--invalid-resp--"), + } +} + #[inline] fn parse_string(bytes: &[u8]) -> Result { String::from_utf8(bytes.to_vec()).map_err(|err| err.to_string()) @@ -360,7 +423,7 @@ mod tests { RespValue::String("OK".to_string()), RespValue::Error("ERR oops".to_string()), RespValue::Integer(-42), - RespValue::BulkString("a\r\nb".to_string()), + RespValue::BulkString(b"a\r\nb".to_vec()), RespValue::Null, RespValue::Boolean(false), RespValue::Double(1.5), @@ -376,14 +439,14 @@ mod tests { )]), ]; for value in values { - assert_eq!(decode(encode(value.clone())), Ok(value)); + assert_eq!(decode(&encode(value.clone())), Ok(value)); } } #[test] fn decode_keeps_elements_as_received() { // a nested array, kept as its RESP rather than decoded and encoded again - let decoded = decode(b"*2\r\n*1\r\n:1\r\n$1\r\na\r\n".to_vec()); + let decoded = decode(b"*2\r\n*1\r\n:1\r\n$1\r\na\r\n"); assert_eq!( decoded, Ok(RespValue::Array(vec![ @@ -395,32 +458,111 @@ mod tests { #[test] fn decode_resp2_nulls() { - assert_eq!(decode(b"$-1\r\n".to_vec()), Ok(RespValue::Null)); - assert_eq!(decode(b"*-1\r\n".to_vec()), Ok(RespValue::Null)); + assert_eq!(decode(b"$-1\r\n"), Ok(RespValue::Null)); + assert_eq!(decode(b"*-1\r\n"), Ok(RespValue::Null)); } #[test] fn decode_needs_exactly_one_value() { - assert!(decode(b"+OK\r\n:1\r\n".to_vec()).is_err()); - assert!(decode(b"*2\r\n:1\r\n".to_vec()).is_err()); - assert!(decode(vec![]).is_err()); + assert!(decode(b"+OK\r\n:1\r\n").is_err()); + assert!(decode(b"*2\r\n:1\r\n").is_err()); + assert!(decode(&[]).is_err()); } #[test] fn decode_invalid_values() { - assert!(decode(b"$3\r\nabcde\r\n".to_vec()).is_err()); - assert!(decode(b"#x\r\n".to_vec()).is_err()); - assert!(decode(b"=3\r\ntxt\r\n".to_vec()).is_err()); - assert!(decode(b":one\r\n".to_vec()).is_err()); + assert!(decode(b"$3\r\nabcde\r\n").is_err()); + assert!(decode(b"#x\r\n").is_err()); + assert!(decode(b"=3\r\ntxt\r\n").is_err()); + assert!(decode(b":one\r\n").is_err()); } #[test] fn frame_len_matches_encode() { let value = RespValue::Array(vec![ - encode(RespValue::BulkString("a\r\nb".to_string())), + encode(RespValue::BulkString(b"a\r\nb".to_vec())), encode(RespValue::Integer(-3)), ]); let encoded = encode(value); assert_eq!(frame_len(&encoded).unwrap(), Some(encoded.len())); } + + #[test] + fn decode_binary_bulk_strings() { + let value = RespValue::BulkString(vec![0x80, 0x00, b'\r', b'\n', 0xff]); + assert_eq!(decode(&encode(value.clone())), Ok(value)); + } + + #[test] + fn display_scalars() { + let display = |value: RespValue| value.to_string(); + assert_eq!( + display(RespValue::String("OK".to_string())), + r#"string<"OK">"# + ); + assert_eq!( + display(RespValue::Error("ERR x".to_string())), + r#"error<"ERR x">"# + ); + assert_eq!(display(RespValue::Integer(-1)), "integer<-1>"); + assert_eq!( + display(RespValue::BulkString(b"a\"b".to_vec())), + r#"bulk-string<"a\"b">"# + ); + // bytes that are not UTF-8 are escaped + assert_eq!( + display(RespValue::BulkString(vec![0x80, b'a'])), + r#"bulk-string"# + ); + assert_eq!(display(RespValue::Null), "null"); + assert_eq!(display(RespValue::Boolean(true)), "boolean"); + assert_eq!(display(RespValue::Double(1.5)), "double<1.5>"); + assert_eq!( + display(RespValue::BigNumber("1".to_string())), + "big-number<1>" + ); + assert_eq!( + display(RespValue::BulkError("SYNTAX".to_string())), + r#"bulk-error<"SYNTAX">"# + ); + assert_eq!( + display(RespValue::VerbatimString(( + "txt".to_string(), + "hi".to_string() + ))), + r#"verbatim-string"# + ); + } + + #[test] + fn display_aggregates() { + let bulk = |text: &str| encode(RespValue::BulkString(text.as_bytes().to_vec())); + let int = |value: i64| encode(RespValue::Integer(value)); + assert_eq!(RespValue::Array(vec![]).to_string(), "array<>"); + assert_eq!( + RespValue::Array(vec![bulk("a"), int(1)]).to_string(), + r#"array, integer<1>>"# + ); + let nested = encode(RespValue::Array(vec![int(1), int(2)])); + assert_eq!( + RespValue::Array(vec![nested]).to_string(), + "array, integer<2>>>" + ); + assert_eq!( + RespValue::Map(vec![(bulk("k"), int(1)), (bulk("j"), int(2))]).to_string(), + r#"map: integer<1>, bulk-string<"j">: integer<2>>"# + ); + assert_eq!( + RespValue::Set(vec![bulk("a")]).to_string(), + r#"set>"# + ); + assert_eq!( + RespValue::Push(vec![bulk("message"), bulk("news")]).to_string(), + r#"push, bulk-string<"news">>"# + ); + assert_eq!( + RespValue::Array(vec![b"?".to_vec()]).to_string(), + "array<--invalid-resp-->" + ); + } } diff --git a/crates/componentized-valkey/src/string_or_bytes.rs b/crates/componentized-valkey/src/string_or_bytes.rs new file mode 100644 index 0000000..06e5177 --- /dev/null +++ b/crates/componentized-valkey/src/string_or_bytes.rs @@ -0,0 +1,102 @@ +//! Conversions of `string-or-bytes`, the type of the values Valkey keeps as given, UTF-8 or not. + +use crate::types::StringOrBytes; + +impl StringOrBytes { + /// A value from bytes, a string when they are UTF-8, without copying them. + pub fn from_bytes(bytes: Vec) -> Self { + match String::from_utf8(bytes) { + Ok(text) => StringOrBytes::String(text), + Err(error) => StringOrBytes::Bytes(error.into_bytes()), + } + } + + /// The bytes of the value, UTF-8 for a string. + pub fn as_bytes(&self) -> &[u8] { + match self { + StringOrBytes::String(text) => text.as_bytes(), + StringOrBytes::Bytes(bytes) => bytes, + } + } + + /// The bytes of the value, UTF-8 for a string, without copying them. + pub fn into_bytes(self) -> Vec { + match self { + StringOrBytes::String(text) => text.into_bytes(), + StringOrBytes::Bytes(bytes) => bytes, + } + } + + /// The value with a prefix, e.g. `[` for an inclusive bound of ZRANGE BYLEX, of the same kind. + pub fn prefixed(self, prefix: &str) -> Self { + match self { + StringOrBytes::String(text) => StringOrBytes::String(format!("{prefix}{text}")), + StringOrBytes::Bytes(bytes) => { + StringOrBytes::Bytes([prefix.as_bytes(), &bytes].concat()) + } + } + } +} + +impl From for StringOrBytes { + fn from(text: String) -> Self { + StringOrBytes::String(text) + } +} + +impl From<&str> for StringOrBytes { + fn from(text: &str) -> Self { + StringOrBytes::String(text.to_string()) + } +} + +/// Equal to a string when it is the same text, e.g. a word of a command in a test. +impl PartialEq for StringOrBytes { + fn eq(&self, other: &str) -> bool { + matches!(self, StringOrBytes::String(text) if text == other) + } +} + +impl PartialEq<&str> for StringOrBytes { + fn eq(&self, other: &&str) -> bool { + self == *other + } +} + +impl PartialEq for StringOrBytes { + fn eq(&self, other: &String) -> bool { + self == other.as_str() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_bytes() { + assert_eq!( + StringOrBytes::from_bytes(b"text".to_vec()), + StringOrBytes::String("text".to_string()) + ); + assert_eq!( + StringOrBytes::from_bytes(vec![0x80]), + StringOrBytes::Bytes(vec![0x80]) + ); + } + + #[test] + fn prefixed() { + assert_eq!(StringOrBytes::from("a").prefixed("["), "[a"); + assert_eq!( + StringOrBytes::Bytes(vec![0x80]).prefixed("("), + StringOrBytes::Bytes(vec![b'(', 0x80]) + ); + } + + #[test] + fn bytes() { + assert_eq!(StringOrBytes::from("a").as_bytes(), b"a"); + assert_eq!(StringOrBytes::Bytes(vec![0x80]).into_bytes(), vec![0x80]); + } +} diff --git a/crates/componentized-valkey/src/test_util.rs b/crates/componentized-valkey/src/test_util.rs index 650acd5..8d3e708 100644 --- a/crates/componentized-valkey/src/test_util.rs +++ b/crates/componentized-valkey/src/test_util.rs @@ -5,7 +5,7 @@ use crate::types::{Error, RespValue}; use crate::{FromValue, IntoCommand}; /// The words of a command that converts. -pub fn words(command: impl IntoCommand) -> Vec { +pub fn words(command: impl IntoCommand) -> crate::command::Words { command.into_command().expect("the words of the command") } @@ -41,7 +41,7 @@ pub fn simple(text: &str) -> RespValue { } pub fn bulk(text: &str) -> RespValue { - RespValue::BulkString(text.to_string()) + RespValue::BulkString(text.as_bytes().to_vec()) } pub fn int(value: i64) -> RespValue { diff --git a/crates/componentized-valkey/src/value.rs b/crates/componentized-valkey/src/value.rs index ac8e5c9..8cd10cd 100644 --- a/crates/componentized-valkey/src/value.rs +++ b/crates/componentized-valkey/src/value.rs @@ -1,7 +1,7 @@ //! Converting a reply into Rust values, the impls for Rust's own types and the helpers the commands //! share. The impls for the package's types are in [`crate::commands`]. -use crate::types::{Double, Error, Integer, RespNestedValue, RespValue}; +use crate::types::{Double, Error, Integer, RespNestedValue, RespValue, StringOrBytes}; use wit_bindgen::rt::Map; @@ -41,9 +41,13 @@ impl FromValue for Double { fn from_value(value: RespValue) -> Result { match value { RespValue::Double(value) => Ok(value), - RespValue::BulkString(text) => text - .parse() - .map_err(|_| Error::Client(format!("unexpected reply, not a double: {text:?}"))), + RespValue::BulkString(bytes) => std::str::from_utf8(&bytes) + .ok() + .and_then(|text| text.parse().ok()) + .ok_or_else(|| { + let reply = RespValue::BulkString(bytes); + Error::Client(format!("unexpected reply, not a double: {reply}")) + }), value => Err(unexpected(&value)), } } @@ -74,8 +78,38 @@ impl FromValue for bool { impl FromValue for String { fn from_value(value: RespValue) -> Result { match value { - RespValue::BulkString(value) | RespValue::String(value) => Ok(value), - RespValue::VerbatimString((_, value)) => Ok(value), + RespValue::BulkString(bytes) => String::from_utf8(bytes).map_err(|error| { + let reply = RespValue::BulkString(error.into_bytes()); + Error::Client(format!("unexpected reply, not UTF-8: {reply}")) + }), + RespValue::String(value) | RespValue::VerbatimString((_, value)) => Ok(value), + value => Err(unexpected(&value)), + } + } +} + +/// A bulk, simple or verbatim string, a string when it is UTF-8, otherwise its bytes, e.g. a value +/// of GET. +impl FromValue for StringOrBytes { + fn from_value(value: RespValue) -> Result { + match value { + RespValue::BulkString(bytes) => Ok(StringOrBytes::from_bytes(bytes)), + RespValue::String(text) | RespValue::VerbatimString((_, text)) => { + Ok(StringOrBytes::String(text)) + } + value => Err(unexpected(&value)), + } + } +} + +/// The bytes of a bulk string, binary by nature, e.g. the payload of DUMP. +#[derive(Debug, PartialEq)] +pub struct Binary(pub Vec); + +impl FromValue for Binary { + fn from_value(value: RespValue) -> Result { + match value { + RespValue::BulkString(bytes) => Ok(Binary(bytes)), value => Err(unexpected(&value)), } } @@ -184,11 +218,11 @@ pub(crate) fn parse_string(value: RespValue) -> Result Error { - Error::Client(format!("unexpected reply: {reply:?}")) + Error::Client(format!("unexpected reply: {reply}")) } fn decode(value: RespNestedValue) -> Result { - crate::resp::decode(value).map_err(Error::Resp) + crate::resp::decode(&value).map_err(Error::Resp) } /// The nested values of an array, or a set. @@ -240,7 +274,7 @@ mod tests { from::<()>(ok()); assert_eq!( from_error::<()>(simple("QUEUED")), - r#"unexpected reply: RespValue::String("QUEUED")"# + r#"unexpected reply: string<"QUEUED">"# ); } @@ -249,7 +283,7 @@ mod tests { assert_eq!(from::(int(-5)), -5); assert_eq!( from_error::(bulk("5")), - r#"unexpected reply: RespValue::BulkString("5")"# + r#"unexpected reply: bulk-string<"5">"# ); } @@ -259,7 +293,11 @@ mod tests { assert_eq!(from::(bulk("2.5")), 2.5); assert_eq!( from_error::(bulk("two")), - r#"unexpected reply, not a double: "two""# + r#"unexpected reply, not a double: bulk-string<"two">"# + ); + assert_eq!( + from_error::(RespValue::BulkString(vec![0x80])), + r#"unexpected reply, not a double: bulk-string"# ); } @@ -274,10 +312,7 @@ mod tests { assert!(!from::(int(0))); assert!(from::(int(1))); assert!(from::(RespValue::Boolean(true))); - assert_eq!( - from_error::(int(2)), - "unexpected reply: RespValue::Integer(2)" - ); + assert_eq!(from_error::(int(2)), "unexpected reply: integer<2>"); } #[test] @@ -286,9 +321,10 @@ mod tests { assert_eq!(from::(simple("simple")), "simple"); let verbatim = RespValue::VerbatimString(("txt".to_string(), "verbatim".to_string())); assert_eq!(from::(verbatim), "verbatim"); + assert_eq!(from_error::(int(1)), "unexpected reply: integer<1>"); assert_eq!( - from_error::(int(1)), - "unexpected reply: RespValue::Integer(1)" + from_error::(RespValue::BulkString(vec![0x80])), + r#"unexpected reply, not UTF-8: bulk-string"# ); } @@ -308,7 +344,7 @@ mod tests { assert_eq!(from::>(set), ["a"]); assert_eq!( from_error::>(bulk("a")), - r#"unexpected reply: RespValue::BulkString("a")"# + r#"unexpected reply: bulk-string<"a">"# ); } diff --git a/crates/test-harness/src/lib.rs b/crates/test-harness/src/lib.rs index 44c0087..ba31752 100644 --- a/crates/test-harness/src/lib.rs +++ b/crates/test-harness/src/lib.rs @@ -24,7 +24,7 @@ use wasmtime::{Config, Engine, Result, Store, StoreContextMut, bail, format_err} use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; pub use bindings::componentized::valkey::types::{ - self, ConnectOpts, Error, RespNestedValue, RespValue, + self, ConnectOpts, Error, RespNestedValue, RespValue, StringOrBytes, }; mod server; @@ -63,7 +63,7 @@ pub fn encode(value: RespValue) -> RespNestedValue { /// Decode a nested value. pub fn decode(value: RespNestedValue) -> Result { - componentized_valkey::resp::decode(value).map(from_codec) + componentized_valkey::resp::decode(&value).map(from_codec) } // a value is not recursive, nested values are encoded RESP, so each conversion is one match @@ -118,11 +118,19 @@ pub use bindings::exports::wasmcloud::keyvalue; /// A bulk string, the type commands are sent as. pub fn bulk(value: &str) -> RespValue { - RespValue::BulkString(value.to_string()) + RespValue::BulkString(value.as_bytes().to_vec()) } -/// The words of a command to send, e.g. `words(&["GET", "key"])`. -pub fn words(words: &[&str]) -> Vec { +/// The words of a command to send, e.g. `words(&["GET", "key"])`, see `connection.exchange`. +pub fn words(words: &[&str]) -> Vec { + words + .iter() + .map(|word| StringOrBytes::String(word.to_string())) + .collect() +} + +/// Strings, e.g. the keys of `connection.watch`, or the words of a command of a transaction. +pub fn strings(words: &[&str]) -> Vec { words.iter().map(|word| word.to_string()).collect() } diff --git a/wit/commands.wit b/wit/commands.wit index 20c91c5..5c38f04 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -41,7 +41,8 @@ interface commands { xinfo-consumers-entry, xinfo-groups-entry, xinfo-stream-reply, xinfo-stream-reply-full, xinfo-stream-reply-full-group, xinfo-stream-reply-full-consumer, xpending-summary, xpending-extended-entry, getex-ttl, hgetex-ttl, lcs-idx-reply, lcs-idx-match, - msetex-precondition, msetex-ttl, set-precondition, set-ttl, hsetex-ttl + msetex-precondition, msetex-ttl, set-precondition, set-ttl, hsetex-ttl, bytes, + string-or-bytes, restore-ttl, function-restore-policy }; @@ -665,13 +666,13 @@ interface commands { /// /// ECHO message /// https://valkey.io/commands/echo/ - echo: async func(connection: borrow, message: string) -> result; + echo: async func(connection: borrow, message: string-or-bytes) -> result; /// Returns PONG if no argument is provided, otherwise return a copy of the argument as a bulk. /// /// PING [ message ] /// https://valkey.io/commands/ping/ - ping: async func(connection: borrow, message: option) -> result<_, error>; + ping: async func(connection: borrow, message: option) -> result<_, error>; /// Allows the connection to read from a replica of a cluster, for the slots of its primary. /// @@ -726,25 +727,25 @@ interface commands { /// /// GEOADD key [ NX | XX ] [ CH ] longitude latitude member [ longitude latitude member ... ] /// https://valkey.io/commands/geoadd/ - geoadd: async func(connection: borrow, key: string, condition: option, changed: bool, coordinates-and-members: list>) -> result; + geoadd: async func(connection: borrow, key: string, condition: option, changed: bool, coordinates-and-members: list>) -> result; /// The distance between the members, in meters or the unit, none when either is not a member. /// /// GEODIST key member1 member2 [ m | km | ft | mi ] /// https://valkey.io/commands/geodist/ - geodist: async func(connection: borrow, key: string, member1: string, member2: string, unit: option) -> result, error>; + geodist: async func(connection: borrow, key: string, member1: string-or-bytes, member2: string-or-bytes, unit: option) -> result, error>; /// The geohash of each of the members, none for a member that is not in the index. /// /// GEOHASH key [ member [ member ... ] ] /// https://valkey.io/commands/geohash/ - geohash: async func(connection: borrow, key: string, members: option>) -> result>, error>; + geohash: async func(connection: borrow, key: string, members: option>) -> result>, error>; /// The position of each of the members, none for a member that is not in the index. /// /// GEOPOS key [ member [ member ... ] ] /// https://valkey.io/commands/geopos/ - geopos: async func(connection: borrow, key: string, members: option>) -> result>, error>; + geopos: async func(connection: borrow, key: string, members: option>) -> result>, error>; /// The members of the geospatial index stored at key in the shape. /// @@ -752,7 +753,7 @@ interface commands { /// /// GEOSEARCH key [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] /// https://valkey.io/commands/geosearch/ - geosearch: async func(connection: borrow, key: string, shape: geosearch-shape, order: option, count: option) -> result, error>; + geosearch: async func(connection: borrow, key: string, shape: geosearch-shape, order: option, count: option) -> result, error>; /// The members of the geospatial index stored at key in the shape, with the details asked for. /// @@ -809,26 +810,26 @@ interface commands { /// /// HGET key field /// https://valkey.io/commands/hget/ - hget: async func(connection: borrow, key: string, field: string) -> result, error>; + hget: async func(connection: borrow, key: string, field: string) -> result, error>; /// Returns all fields and values of the hash stored at key. /// /// HGETALL key /// https://valkey.io/commands/hgetall/ - hgetall: async func(connection: borrow, key: string) -> result, error>; + hgetall: async func(connection: borrow, key: string) -> result, error>; /// The values of the hash fields, none for a field that does not exist, deleting the fields. /// /// HGETDEL key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hgetdel/ - hgetdel: async func(connection: borrow, key: string, fields: list) -> result>, error>; + hgetdel: async func(connection: borrow, key: string, fields: list) -> result>, error>; /// The values of the hash fields, none for a field that does not exist, optionally setting or /// removing their expiration. /// /// HGETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hgetex/ - hgetex: async func(connection: borrow, key: string, ttl: option, fields: list) -> result>, error>; + hgetex: async func(connection: borrow, key: string, ttl: option, fields: list) -> result>, error>; /// Increments the number stored at field in the hash stored at key by increment. /// @@ -858,13 +859,13 @@ interface commands { /// /// HMGET key field [ field ... ] /// https://valkey.io/commands/hmget/ - hmget: async func(connection: borrow, key: string, fields: list) -> result>, error>; + hmget: async func(connection: borrow, key: string, fields: list) -> result>, error>; /// Sets the specified fields to their respective values in the hash stored at key. /// /// HMSET key field value [ field value ... ] /// https://valkey.io/commands/hmset/ - hmset: async func(connection: borrow, key: string, field-and-values: list>) -> result<_, error>; + hmset: async func(connection: borrow, key: string, field-and-values: list>) -> result<_, error>; /// Removes the expiration of the hash fields. /// @@ -920,7 +921,7 @@ interface commands { /// /// HRANDFIELD key count WITHVALUES /// https://valkey.io/commands/hrandfield/ - hrandfield-withvalues: async func(connection: borrow, key: string, count: integer) -> result>, error>; + hrandfield-withvalues: async func(connection: borrow, key: string, count: integer) -> result>, error>; /// Incrementally iterate over a collection of elements. /// @@ -928,7 +929,7 @@ interface commands { /// /// HSCAN key cursor [ MATCH pattern ] [ COUNT count ] /// https://valkey.io/commands/hscan/ - hscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, map>, error>; + hscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, map>, error>; /// Incrementally iterate over a collection of keys. /// @@ -942,20 +943,20 @@ interface commands { /// /// HSET key field value [ field value ... ] /// https://valkey.io/commands/hset/ - hset: async func(connection: borrow, key: string, field-and-values: list>) -> result; + hset: async func(connection: borrow, key: string, field-and-values: list>) -> result; /// Sets the fields to their values in the hash stored at key, optionally setting their /// expiration, returning whether they were set, false when a condition prevented it. /// /// HSETEX key [ NX | XX ] [ FNX | FXX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] FIELDS numfields field value [ field value ... ] /// https://valkey.io/commands/hsetex/ - hsetex: async func(connection: borrow, key: string, condition: option, field-condition: option, ttl: option, field-and-values: list>) -> result; + hsetex: async func(connection: borrow, key: string, condition: option, field-condition: option, ttl: option, field-and-values: list>) -> result; /// Sets field in the hash stored at key to value, only if field does not yet exist. /// /// HSETNX key field value /// https://valkey.io/commands/hsetnx/ - hsetnx: async func(connection: borrow, key: string, field: string, value: string) -> result; + hsetnx: async func(connection: borrow, key: string, field: string, value: string-or-bytes) -> result; /// Returns the string length of the value associated with field in the hash stored at key. /// @@ -975,7 +976,7 @@ interface commands { /// /// HVALS key /// https://valkey.io/commands/hvals/ - hvals: async func(connection: borrow, key: string) -> result, error>; + hvals: async func(connection: borrow, key: string) -> result, error>; // hyperloglog commands @@ -987,7 +988,7 @@ interface commands { /// /// PFADD key [ element [ element ... ] ] /// https://valkey.io/commands/pfadd/ - pfadd: async func(connection: borrow, key: string, elements: option>) -> result; + pfadd: async func(connection: borrow, key: string, elements: option>) -> result; /// The approximate number of unique elements added to the HyperLogLogs, of their union for /// more than one, 0 for a key that does not exist. @@ -1045,6 +1046,13 @@ interface commands { /// https://valkey.io/commands/del/ del: async func(connection: borrow, keys: list) -> result; + /// The value of the key serialized in Valkey's own format, with its version and a checksum, see + /// `restore`, none when the key does not exist. + /// + /// DUMP key + /// https://valkey.io/commands/dump/ + dump: async func(connection: borrow, key: string) -> result, error>; + /// If key exists. /// /// EXISTS key [ key ... ] @@ -1184,6 +1192,20 @@ interface commands { /// https://valkey.io/commands/renamenx/ renamenx: async func(connection: borrow, key: string, newkey: string) -> result; + /// Creates the key from a value serialized with `dump`, without an expiration when `ttl` is + /// none. Fails when the key exists, unless `replace`. `idletime` and `freq` set what eviction + /// knows of the key, its idle time for an LRU policy, or its access frequency for an LFU policy. + /// + /// RESTORE key ttl serialized-value [ REPLACE ] [ ABSTTL ] [ IDLETIME seconds ] [ FREQ frequency ] + /// https://valkey.io/commands/restore/ + restore: async func(connection: borrow, key: string, ttl: option, serialized-value: bytes, replace: bool, idletime: option, freq: option) -> result<_, error>; + + /// `restore` on a node a key is migrated to, for a slot being imported, see `cluster-setslot`. + /// + /// RESTORE-ASKING key ttl serialized-value [ REPLACE ] [ ABSTTL ] [ IDLETIME seconds ] [ FREQ frequency ] + /// https://valkey.io/commands/restore-asking/ + restore-asking: async func(connection: borrow, key: string, ttl: option, serialized-value: bytes, replace: bool, idletime: option, freq: option) -> result<_, error>; + /// Incrementally iterate over the keys of the current database. /// /// SCAN cursor [ MATCH pattern ] [ COUNT count ] [ TYPE type ] @@ -1222,12 +1244,6 @@ interface commands { /// https://valkey.io/commands/unlink/ unlink: async func(connection: borrow, keys: list) -> result; - // not implemented, from the @keyspace category: - // DUMP, needs binary support, the payload is not a string - // RESTORE, needs binary support, the payload is not a string - // RESTORE-ASKING, needs binary support, the payload is not a string - - // list commands /// Moves an element from one end of the source list to an end of the destination list, waiting @@ -1240,7 +1256,7 @@ interface commands { /// /// BLMOVE source destination 〈 LEFT | RIGHT 〉 〈 LEFT | RIGHT 〉 timeout /// https://valkey.io/commands/blmove/ - blmove: async func(connection: borrow, source: string, destination: string, wherefrom: list-side, whereto: list-side, timeout: duration-milliseconds) -> result, error>; + blmove: async func(connection: borrow, source: string, destination: string, wherefrom: list-side, whereto: list-side, timeout: duration-milliseconds) -> result, error>; /// Removes elements from an end of the first non-empty list, waiting up to the timeout for one, /// returning its key and the elements, one without a count. @@ -1252,7 +1268,7 @@ interface commands { /// /// BLMPOP timeout numkeys key [ key ... ] 〈 LEFT | RIGHT 〉 [ COUNT count ] /// https://valkey.io/commands/blmpop/ - blmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: list-side, count: option) -> result>>, error>; + blmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: list-side, count: option) -> result>>, error>; /// Removes the first element of the first non-empty list, waiting up to the timeout for one, /// returning its key and the element. @@ -1264,7 +1280,7 @@ interface commands { /// /// BLPOP key [ key ... ] timeout /// https://valkey.io/commands/blpop/ - blpop: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; + blpop: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; /// Removes the last element of the first non-empty list, waiting up to the timeout for one, /// returning its key and the element. @@ -1276,7 +1292,7 @@ interface commands { /// /// BRPOP key [ key ... ] timeout /// https://valkey.io/commands/brpop/ - brpop: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; + brpop: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; /// Moves the last element of the source list to the head of the destination list, waiting up /// to the timeout for the source to have one, returning it. @@ -1290,21 +1306,21 @@ interface commands { /// /// BRPOPLPUSH source destination timeout /// https://valkey.io/commands/brpoplpush/ - brpoplpush: async func(connection: borrow, source: string, destination: string, timeout: duration-milliseconds) -> result, error>; + brpoplpush: async func(connection: borrow, source: string, destination: string, timeout: duration-milliseconds) -> result, error>; /// The element at the index of the list stored at key, negative from the end, none when the /// index is out of range. /// /// LINDEX key index /// https://valkey.io/commands/lindex/ - lindex: async func(connection: borrow, key: string, index: integer) -> result, error>; + lindex: async func(connection: borrow, key: string, index: integer) -> result, error>; /// Inserts the element before or after the first occurrence of the pivot in the list stored at /// key. /// /// LINSERT key 〈 BEFORE | AFTER 〉 pivot element /// https://valkey.io/commands/linsert/ - linsert: async func(connection: borrow, key: string, position: linsert-position, pivot: string, element: string) -> result; + linsert: async func(connection: borrow, key: string, position: linsert-position, pivot: string-or-bytes, element: string-or-bytes) -> result; /// The length of the list stored at key, 0 when the key does not exist. /// @@ -1317,21 +1333,21 @@ interface commands { /// /// LMOVE source destination 〈 LEFT | RIGHT 〉 〈 LEFT | RIGHT 〉 /// https://valkey.io/commands/lmove/ - lmove: async func(connection: borrow, source: string, destination: string, wherefrom: list-side, whereto: list-side) -> result, error>; + lmove: async func(connection: borrow, source: string, destination: string, wherefrom: list-side, whereto: list-side) -> result, error>; /// Removes elements from an end of the first non-empty list, returning its key and the /// elements, one without a count, none when every list is empty. /// /// LMPOP numkeys key [ key ... ] 〈 LEFT | RIGHT 〉 [ COUNT count ] /// https://valkey.io/commands/lmpop/ - lmpop: async func(connection: borrow, keys: list, end: list-side, count: option) -> result>>, error>; + lmpop: async func(connection: borrow, keys: list, end: list-side, count: option) -> result>>, error>; /// Removes and returns the first elements of the list stored at key, up to the count, one /// without a count, none when the key does not exist. /// /// LPOP key [ count ] /// https://valkey.io/commands/lpop/ - lpop: async func(connection: borrow, key: string, count: option) -> result, error>; + lpop: async func(connection: borrow, key: string, count: option) -> result, error>; /// The positions of the element in the list stored at key, up to the count, 0 for every match, /// the first match without a count. @@ -1341,28 +1357,28 @@ interface commands { /// /// LPOS key element [ RANK rank ] [ COUNT num-matches ] [ MAXLEN len ] /// https://valkey.io/commands/lpos/ - lpos: async func(connection: borrow, key: string, element: string, rank: option, count: option, maxlen: option) -> result, error>; + lpos: async func(connection: borrow, key: string, element: string-or-bytes, rank: option, count: option, maxlen: option) -> result, error>; /// Inserts the elements at the head of the list stored at key, creating it when it does not /// exist, returning its length after. /// /// LPUSH key element [ element ... ] /// https://valkey.io/commands/lpush/ - lpush: async func(connection: borrow, key: string, elements: list) -> result; + lpush: async func(connection: borrow, key: string, elements: list) -> result; /// Inserts the elements at the head of the list stored at key, only when it exists, returning /// its length after, 0 when it does not exist. /// /// LPUSHX key element [ element ... ] /// https://valkey.io/commands/lpushx/ - lpushx: async func(connection: borrow, key: string, elements: list) -> result; + lpushx: async func(connection: borrow, key: string, elements: list) -> result; /// The elements of the list stored at key from the start to the stop, both included, negative /// from the end. /// /// LRANGE key start stop /// https://valkey.io/commands/lrange/ - lrange: async func(connection: borrow, key: string, start: integer, stop: integer) -> result, error>; + lrange: async func(connection: borrow, key: string, start: integer, stop: integer) -> result, error>; /// Removes occurrences of the element from the list stored at key, returning how many. /// @@ -1371,13 +1387,13 @@ interface commands { /// /// LREM key count element /// https://valkey.io/commands/lrem/ - lrem: async func(connection: borrow, key: string, count: integer, element: string) -> result; + lrem: async func(connection: borrow, key: string, count: integer, element: string-or-bytes) -> result; /// Sets the element at the index of the list stored at key, negative from the end. /// /// LSET key index element /// https://valkey.io/commands/lset/ - lset: async func(connection: borrow, key: string, index: integer, element: string) -> result<_, error>; + lset: async func(connection: borrow, key: string, index: integer, element: string-or-bytes) -> result<_, error>; /// Trims the list stored at key to the elements from the start to the stop, both included, /// negative from the end. @@ -1391,7 +1407,7 @@ interface commands { /// /// RPOP key [ count ] /// https://valkey.io/commands/rpop/ - rpop: async func(connection: borrow, key: string, count: option) -> result, error>; + rpop: async func(connection: borrow, key: string, count: option) -> result, error>; /// Moves the last element of the source list to the head of the destination list, returning /// it, none when the source does not exist. @@ -1400,21 +1416,21 @@ interface commands { /// /// RPOPLPUSH source destination /// https://valkey.io/commands/rpoplpush/ - rpoplpush: async func(connection: borrow, source: string, destination: string) -> result, error>; + rpoplpush: async func(connection: borrow, source: string, destination: string) -> result, error>; /// Inserts the elements at the tail of the list stored at key, creating it when it does not /// exist, returning its length after. /// /// RPUSH key element [ element ... ] /// https://valkey.io/commands/rpush/ - rpush: async func(connection: borrow, key: string, elements: list) -> result; + rpush: async func(connection: borrow, key: string, elements: list) -> result; /// Inserts the elements at the tail of the list stored at key, only when it exists, returning /// its length after, 0 when it does not exist. /// /// RPUSHX key element [ element ... ] /// https://valkey.io/commands/rpushx/ - rpushx: async func(connection: borrow, key: string, elements: list) -> result; + rpushx: async func(connection: borrow, key: string, elements: list) -> result; // pubsub commands @@ -1423,7 +1439,7 @@ interface commands { /// /// PUBLISH channel message /// https://valkey.io/commands/publish/ - publish: async func(connection: borrow, channel: string, message: string) -> result; + publish: async func(connection: borrow, channel: string, message: string-or-bytes) -> result; /// Lists the currently active channels. /// @@ -1459,7 +1475,7 @@ interface commands { /// /// SPUBLISH shardchannel message /// https://valkey.io/commands/spublish/ - spublish: async func(connection: borrow, shardchannel: string, message: string) -> result; + spublish: async func(connection: borrow, shardchannel: string, message: string-or-bytes) -> result; // scripting commands @@ -1471,27 +1487,27 @@ interface commands { /// /// EVAL script numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/eval/ - eval: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; + eval: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; /// Runs the Lua script, which may not write, returning its result, see `eval`. /// /// EVAL_RO script numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/eval_ro/ - eval-ro: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; + eval-ro: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; /// Runs the Lua script cached with the SHA1 digest, see `script-load`, returning its result, see /// `eval`. /// /// EVALSHA sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/evalsha/ - evalsha: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; + evalsha: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; /// Runs the Lua script cached with the SHA1 digest, which may not write, returning its result, /// see `eval`. /// /// EVALSHA_RO sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/evalsha_ro/ - evalsha-ro: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; + evalsha-ro: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; /// Calls the function, see `function-load`, returning its result, the keys it accesses and its /// arguments are passed to it. @@ -1500,13 +1516,13 @@ interface commands { /// /// FCALL function numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/fcall/ - fcall: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; + fcall: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; /// Calls the function, which must be flagged `no-writes`, returning its result, see `fcall`. /// /// FCALL_RO function numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/fcall_ro/ - fcall-ro: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; + fcall-ro: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; /// Deletes the library and its functions. /// @@ -1514,6 +1530,12 @@ interface commands { /// https://valkey.io/commands/function-delete/ function-delete: async func(connection: borrow, library-name: string) -> result<_, error>; + /// The libraries, serialized in Valkey's own format, see `function-restore`. + /// + /// FUNCTION DUMP + /// https://valkey.io/commands/function-dump/ + function-dump: async func(connection: borrow) -> result; + /// Deletes all the libraries, in the mode, the server's `lazyfree-lazy-user-flush` without one. /// /// FUNCTION FLUSH [ ASYNC | SYNC ] @@ -1539,6 +1561,12 @@ interface commands { /// https://valkey.io/commands/function-load/ function-load: async func(connection: borrow, replace: bool, function-code: string) -> result; + /// Loads the libraries serialized with `function-dump`, as `policy` says, by default `append`. + /// + /// FUNCTION RESTORE serialized-value [ FLUSH | APPEND | REPLACE ] + /// https://valkey.io/commands/function-restore/ + function-restore: async func(connection: borrow, serialized-value: bytes, policy: option) -> result<_, error>; + /// The function that is running, and the libraries and functions of each engine. /// /// FUNCTION STATS @@ -1577,7 +1605,6 @@ interface commands { script-show: async func(connection: borrow, sha1: string) -> result; // not implemented, from the @scripting category: - // FUNCTION DUMP and FUNCTION RESTORE, the payload is binary, not a string // SCRIPT DEBUG, the debugger is interactive, with a protocol of its own @@ -1587,7 +1614,7 @@ interface commands { /// /// SADD key member [ member ... ] /// https://valkey.io/commands/sadd/ - sadd: async func(connection: borrow, key: string, members: list) -> result; + sadd: async func(connection: borrow, key: string, members: list) -> result; /// The number of members in the set stored at key. /// @@ -1599,7 +1626,7 @@ interface commands { /// /// SDIFF key [ key ... ] /// https://valkey.io/commands/sdiff/ - sdiff: async func(connection: borrow, keys: list) -> result, error>; + sdiff: async func(connection: borrow, keys: list) -> result, error>; /// Stores the members of the first set that are not in any of the others, returning how many. /// @@ -1611,7 +1638,7 @@ interface commands { /// /// SINTER key [ key ... ] /// https://valkey.io/commands/sinter/ - sinter: async func(connection: borrow, keys: list) -> result, error>; + sinter: async func(connection: borrow, keys: list) -> result, error>; /// The number of members in all of the sets, counting stops at the limit. /// @@ -1631,7 +1658,7 @@ interface commands { /// /// SISMEMBER key member /// https://valkey.io/commands/sismember/ - sismember: async func(connection: borrow, key: string, member: string) -> result; + sismember: async func(connection: borrow, key: string, member: string-or-bytes) -> result; /// Whether the member is in the set stored at key, none when the key does not exist. /// @@ -1639,25 +1666,25 @@ interface commands { /// /// SISMEMBER key member XX /// https://valkey.io/commands/sismember/ - sismember-xx: async func(connection: borrow, key: string, member: string) -> result, error>; + sismember-xx: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; /// The members of the set stored at key. /// /// SMEMBERS key /// https://valkey.io/commands/smembers/ - smembers: async func(connection: borrow, key: string) -> result, error>; + smembers: async func(connection: borrow, key: string) -> result, error>; /// Whether each of the members is in the set stored at key. /// /// SMISMEMBER key member [ member ... ] /// https://valkey.io/commands/smismember/ - smismember: async func(connection: borrow, key: string, members: list) -> result, error>; + smismember: async func(connection: borrow, key: string, members: list) -> result, error>; /// Moves the member from one set to another, false when it was not in the source set. /// /// SMOVE source destination member /// https://valkey.io/commands/smove/ - smove: async func(connection: borrow, source: string, destination: string, member: string) -> result; + smove: async func(connection: borrow, source: string, destination: string, member: string-or-bytes) -> result; /// Removes and returns random members of the set stored at key, up to the count. /// @@ -1665,7 +1692,7 @@ interface commands { /// /// SPOP key [ count ] /// https://valkey.io/commands/spop/ - spop: async func(connection: borrow, key: string, count: option) -> result, error>; + spop: async func(connection: borrow, key: string, count: option) -> result, error>; /// Random members of the set stored at key, up to the count. /// @@ -1674,25 +1701,25 @@ interface commands { /// /// SRANDMEMBER key [ count ] /// https://valkey.io/commands/srandmember/ - srandmember: async func(connection: borrow, key: string, count: option) -> result, error>; + srandmember: async func(connection: borrow, key: string, count: option) -> result, error>; /// Removes the members from the set stored at key, returning how many were members. /// /// SREM key member [ member ... ] /// https://valkey.io/commands/srem/ - srem: async func(connection: borrow, key: string, members: list) -> result; + srem: async func(connection: borrow, key: string, members: list) -> result; /// Incrementally iterate over the members of the set stored at key. /// /// SSCAN key cursor [ MATCH pattern ] [ COUNT count ] /// https://valkey.io/commands/sscan/ - sscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; + sscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; /// The members in any of the sets. /// /// SUNION key [ key ... ] /// https://valkey.io/commands/sunion/ - sunion: async func(connection: borrow, keys: list) -> result, error>; + sunion: async func(connection: borrow, keys: list) -> result, error>; /// Stores the members in any of the sets, returning how many. /// @@ -1707,7 +1734,7 @@ interface commands { /// /// SORT key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] /// https://valkey.io/commands/sort/ - sort: async func(connection: borrow, key: string, opts: option) -> result>>, error>; + sort: async func(connection: borrow, key: string, opts: option) -> result>>, error>; /// Sorts the elements of the list, set or sorted set stored at key, storing them as a list, /// returning how many. @@ -1720,7 +1747,7 @@ interface commands { /// /// SORT_RO key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] /// https://valkey.io/commands/sort_ro/ - sort-ro: async func(connection: borrow, key: string, opts: option) -> result>>, error>; + sort-ro: async func(connection: borrow, key: string, opts: option) -> result>>, error>; // sortedset commands @@ -1735,7 +1762,7 @@ interface commands { /// /// BZMPOP timeout numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ] /// https://valkey.io/commands/bzmpop/ - bzmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: bzmpop-end, count: option) -> result>>>, error>; + bzmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: bzmpop-end, count: option) -> result>>>, error>; /// Removes the member with the highest score from the first non-empty sorted set, waiting up to /// the timeout for one, returning its key, the member and its score. @@ -1747,7 +1774,7 @@ interface commands { /// /// BZPOPMAX key [ key ... ] timeout /// https://valkey.io/commands/bzpopmax/ - bzpopmax: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; + bzpopmax: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; /// Removes the member with the lowest score from the first non-empty sorted set, waiting up to /// the timeout for one, returning its key, the member and its score. @@ -1759,7 +1786,7 @@ interface commands { /// /// BZPOPMIN key [ key ... ] timeout /// https://valkey.io/commands/bzpopmin/ - bzpopmin: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; + bzpopmin: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; /// Adds the members with their scores to the sorted set stored at key, or updates their scores, /// returning how many were added, or with `changed`, added or updated. @@ -1768,7 +1795,7 @@ interface commands { /// /// ZADD key [ NX | XX ] [ GT | LT ] [ CH ] score member [ score member ... ] /// https://valkey.io/commands/zadd/ - zadd: async func(connection: borrow, key: string, condition: option, comparison: option, changed: bool, score-and-members: list>) -> result, error>; + zadd: async func(connection: borrow, key: string, condition: option, comparison: option, changed: bool, score-and-members: list>) -> result, error>; /// Increments the score of the member in the sorted set stored at key, returning the new score, /// none when the condition or comparison prevented it. @@ -1777,7 +1804,7 @@ interface commands { /// /// ZADD key [ NX | XX ] [ GT | LT ] INCR score member /// https://valkey.io/commands/zadd/ - zadd-incr: async func(connection: borrow, key: string, condition: option, comparison: option, increment: double, member: string) -> result, error>; + zadd-incr: async func(connection: borrow, key: string, condition: option, comparison: option, increment: double, member: string-or-bytes) -> result, error>; /// The number of members in the sorted set stored at key. /// @@ -1797,7 +1824,7 @@ interface commands { /// /// ZDIFF numkeys key [ key ... ] /// https://valkey.io/commands/zdiff/ - zdiff: async func(connection: borrow, keys: list) -> result, error>; + zdiff: async func(connection: borrow, keys: list) -> result, error>; /// The members, and their scores, of the first sorted set that are not in any of the others. /// @@ -1805,7 +1832,7 @@ interface commands { /// /// ZDIFF numkeys key [ key ... ] WITHSCORES /// https://valkey.io/commands/zdiff/ - zdiff-withscores: async func(connection: borrow, keys: list) -> result>, error>; + zdiff-withscores: async func(connection: borrow, keys: list) -> result>, error>; /// Stores the members of the first sorted set that are not in any of the others, returning how /// many. @@ -1818,7 +1845,7 @@ interface commands { /// /// ZINCRBY key increment member /// https://valkey.io/commands/zincrby/ - zincrby: async func(connection: borrow, key: string, increment: double, member: string) -> result; + zincrby: async func(connection: borrow, key: string, increment: double, member: string-or-bytes) -> result; /// The members in all of the sorted sets. /// @@ -1828,7 +1855,7 @@ interface commands { /// /// ZINTER numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zinter/ - zinter: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; + zinter: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; /// The members, and their combined scores, in all of the sorted sets. /// @@ -1836,7 +1863,7 @@ interface commands { /// /// ZINTER numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] WITHSCORES /// https://valkey.io/commands/zinter/ - zinter-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; + zinter-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; /// The number of members in all of the sorted sets, counting stops at the limit. /// @@ -1862,28 +1889,28 @@ interface commands { /// /// ZMPOP numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ] /// https://valkey.io/commands/zmpop/ - zmpop: async func(connection: borrow, keys: list, end: zmpop-end, count: option) -> result>>>, error>; + zmpop: async func(connection: borrow, keys: list, end: zmpop-end, count: option) -> result>>>, error>; /// The score of each of the members of the sorted set stored at key, none for a member that is /// not in it. /// /// ZMSCORE key member [ member ... ] /// https://valkey.io/commands/zmscore/ - zmscore: async func(connection: borrow, key: string, members: list) -> result>, error>; + zmscore: async func(connection: borrow, key: string, members: list) -> result>, error>; /// Removes and returns the members with the highest scores, and their scores, up to the count, /// one without a count. /// /// ZPOPMAX key [ count ] /// https://valkey.io/commands/zpopmax/ - zpopmax: async func(connection: borrow, key: string, count: option) -> result>, error>; + zpopmax: async func(connection: borrow, key: string, count: option) -> result>, error>; /// Removes and returns the members with the lowest scores, and their scores, up to the count, /// one without a count. /// /// ZPOPMIN key [ count ] /// https://valkey.io/commands/zpopmin/ - zpopmin: async func(connection: borrow, key: string, count: option) -> result>, error>; + zpopmin: async func(connection: borrow, key: string, count: option) -> result>, error>; /// Random members of the sorted set stored at key, up to the count. /// @@ -1894,7 +1921,7 @@ interface commands { /// /// ZRANDMEMBER key [ count ] /// https://valkey.io/commands/zrandmember/ - zrandmember: async func(connection: borrow, key: string, count: option) -> result, error>; + zrandmember: async func(connection: borrow, key: string, count: option) -> result, error>; /// Random members of the sorted set stored at key, and their scores, up to the count. /// @@ -1905,7 +1932,7 @@ interface commands { /// /// ZRANDMEMBER key count WITHSCORES /// https://valkey.io/commands/zrandmember/ - zrandmember-withscores: async func(connection: borrow, key: string, count: integer) -> result>, error>; + zrandmember-withscores: async func(connection: borrow, key: string, count: integer) -> result>, error>; /// The members of the sorted set stored at key in the range, from the lowest score, or the /// highest with `rev`. @@ -1918,7 +1945,7 @@ interface commands { /// /// ZRANGE key start stop [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ] [ XX ] /// https://valkey.io/commands/zrange/ - zrange: async func(connection: borrow, key: string, range: zrange-by, rev: bool, limit: option, xx: bool) -> result>, error>; + zrange: async func(connection: borrow, key: string, range: zrange-by, rev: bool, limit: option, xx: bool) -> result>, error>; /// The members of the sorted set stored at key in the range, and their scores, from the lowest /// score, or the highest with `rev`. @@ -1930,7 +1957,7 @@ interface commands { /// /// ZRANGE key start stop [ BYSCORE ] [ REV ] [ LIMIT offset count ] WITHSCORES [ XX ] /// https://valkey.io/commands/zrange/ - zrange-withscores: async func(connection: borrow, key: string, range: zrange-withscores-by, rev: bool, limit: option, xx: bool) -> result>>, error>; + zrange-withscores: async func(connection: borrow, key: string, range: zrange-withscores-by, rev: bool, limit: option, xx: bool) -> result>>, error>; /// Stores the members of the sorted set stored at source in the range, returning how many. /// @@ -1947,7 +1974,7 @@ interface commands { /// /// ZRANK key member /// https://valkey.io/commands/zrank/ - zrank: async func(connection: borrow, key: string, member: string) -> result, error>; + zrank: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; /// The position, and the score, of the member in the sorted set stored at key, from the lowest /// score, none when it is not a member. @@ -1956,13 +1983,13 @@ interface commands { /// /// ZRANK key member WITHSCORE /// https://valkey.io/commands/zrank/ - zrank-withscore: async func(connection: borrow, key: string, member: string) -> result>, error>; + zrank-withscore: async func(connection: borrow, key: string, member: string-or-bytes) -> result>, error>; /// Removes the members from the sorted set stored at key, returning how many were members. /// /// ZREM key member [ member ... ] /// https://valkey.io/commands/zrem/ - zrem: async func(connection: borrow, key: string, members: list) -> result; + zrem: async func(connection: borrow, key: string, members: list) -> result; /// Removes the members of the sorted set stored at key in the range, for members of equal score, /// returning how many. @@ -1992,7 +2019,7 @@ interface commands { /// /// ZREVRANK key member /// https://valkey.io/commands/zrevrank/ - zrevrank: async func(connection: borrow, key: string, member: string) -> result, error>; + zrevrank: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; /// The position, and the score, of the member in the sorted set stored at key, from the highest /// score, none when it is not a member. @@ -2001,7 +2028,7 @@ interface commands { /// /// ZREVRANK key member WITHSCORE /// https://valkey.io/commands/zrevrank/ - zrevrank-withscore: async func(connection: borrow, key: string, member: string) -> result>, error>; + zrevrank-withscore: async func(connection: borrow, key: string, member: string-or-bytes) -> result>, error>; /// Incrementally iterate over the members of the sorted set stored at key, and their scores. /// @@ -2009,7 +2036,7 @@ interface commands { /// /// ZSCAN key cursor [ MATCH pattern ] [ COUNT count ] /// https://valkey.io/commands/zscan/ - zscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>>, error>; + zscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>>, error>; /// Incrementally iterate over the members of the sorted set stored at key. /// @@ -2017,13 +2044,13 @@ interface commands { /// /// ZSCAN key cursor [ MATCH pattern ] [ COUNT count ] NOSCORES /// https://valkey.io/commands/zscan/ - zscan-noscores: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; + zscan-noscores: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; /// The score of the member in the sorted set stored at key, none when it is not a member. /// /// ZSCORE key member /// https://valkey.io/commands/zscore/ - zscore: async func(connection: borrow, key: string, member: string) -> result, error>; + zscore: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; /// The members in any of the sorted sets. /// @@ -2033,7 +2060,7 @@ interface commands { /// /// ZUNION numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zunion/ - zunion: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; + zunion: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; /// The members, and their combined scores, in any of the sorted sets. /// @@ -2041,7 +2068,7 @@ interface commands { /// /// ZUNION numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] WITHSCORES /// https://valkey.io/commands/zunion/ - zunion-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; + zunion-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; /// Stores the members in any of the sorted sets, with their combined scores, returning how many. /// @@ -2062,7 +2089,7 @@ interface commands { /// /// XADD key [ NOMKSTREAM ] [ 〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] ] 〈 * | id 〉 field value [ field value ... ] /// https://valkey.io/commands/xadd/ - xadd: async func(connection: borrow, key: string, nomkstream: bool, trim: option, id: stream-id, field-and-values: list>) -> result, error>; + xadd: async func(connection: borrow, key: string, nomkstream: bool, trim: option, id: stream-id, field-and-values: list>) -> result, error>; /// Transfers ownership of pending stream entries that match the specified criteria. /// @@ -2074,7 +2101,7 @@ interface commands { /// /// XAUTOCLAIM key group consumer min-idle-time start [ COUNT count ] /// https://valkey.io/commands/xautoclaim/ - xautoclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, start: option, count: option) -> result, list>>, list>, error>; + xautoclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, start: option, count: option) -> result, list>>, list>, error>; /// Transfers ownership of pending stream entries that match the specified criteria. /// @@ -2095,7 +2122,7 @@ interface commands { /// /// XCLAIM key group consumer min-idle-time ID [ ID ... ] [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] [ LASTID lastid ] /// https://valkey.io/commands/xclaim/ - xclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, ids: list, opts: option) -> result>>, error>; + xclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, ids: list, opts: option) -> result>>, error>; /// Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument. /// @@ -2195,13 +2222,13 @@ interface commands { /// /// XRANGE key start end [ COUNT count ] /// https://valkey.io/commands/xrange/ - xrange: async func(connection: borrow, key: string, start: stream-id, end: stream-id, count: option) -> result>>, error>; + xrange: async func(connection: borrow, key: string, start: stream-id, end: stream-id, count: option) -> result>>, error>; /// Read data from one or multiple streams, only returning entries with an ID greater than the last received ID reported by the caller. /// /// XREAD [ COUNT count ] [ BLOCK milliseconds ] STREAMS key [ key ... ] ID [ ID ... ] /// https://valkey.io/commands/xread/ - xread: async func(connection: borrow, count: option, block: option, keys: list, ids: list) -> result>>>>, error>; + xread: async func(connection: borrow, count: option, block: option, keys: list, ids: list) -> result>>>>, error>; /// Special version of the XREAD command with support for consumer groups. /// @@ -2210,13 +2237,13 @@ interface commands { /// /// XREADGROUP GROUP group consumer [ COUNT count ] [ BLOCK milliseconds ] [ NOACK ] STREAMS key [ key ... ] ID [ ID ... ] /// https://valkey.io/commands/xreadgroup/ - xreadgroup: async func(connection: borrow, group: string, consumer: string, count: option, block: option, noack: bool, keys: list, ids: list) -> result>>>>>, error>; + xreadgroup: async func(connection: borrow, group: string, consumer: string, count: option, block: option, noack: bool, keys: list, ids: list) -> result>>>>>, error>; /// XRANGE, but returning the entries in reverse order. /// /// XREVRANGE key end start [ COUNT count ] /// https://valkey.io/commands/xrevrange/ - xrevrange: async func(connection: borrow, key: string, end: stream-id, start: stream-id, count: option) -> result>>, error>; + xrevrange: async func(connection: borrow, key: string, end: stream-id, start: stream-id, count: option) -> result>>, error>; /// Internal command. It is used by a Valkey primary to replicate the last delivered ID of streams. /// @@ -2238,7 +2265,7 @@ interface commands { /// /// APPEND key value /// https://valkey.io/commands/append/ - append: async func(connection: borrow, key: string, value: string) -> result; + append: async func(connection: borrow, key: string, value: string-or-bytes) -> result; /// Decrements the number stored at key by one. /// @@ -2256,38 +2283,38 @@ interface commands { /// /// DELIFEQ key value /// https://valkey.io/commands/delifeq/ - delifeq: async func(connection: borrow, key: string, value: string) -> result; + delifeq: async func(connection: borrow, key: string, value: string-or-bytes) -> result; /// Get the value of key. /// /// GET key /// https://valkey.io/commands/get/ - get: async func(connection: borrow, key: string) -> result, error>; + get: async func(connection: borrow, key: string) -> result, error>; /// Get the value of key and delete the key. /// /// GETDEL key /// https://valkey.io/commands/getdel/ - getdel: async func(connection: borrow, key: string) -> result, error>; + getdel: async func(connection: borrow, key: string) -> result, error>; /// Get the value of key and optionally set its expiration. /// /// GETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ] /// https://valkey.io/commands/getex/ - getex: async func(connection: borrow, key: string, ttl: option) -> result, error>; + getex: async func(connection: borrow, key: string, ttl: option) -> result, error>; /// Returns the substring of the string value stored at key, determined by the offsets start and end. /// /// GETRANGE key start end /// https://valkey.io/commands/getrange/ - getrange: async func(connection: borrow, key: string, start: integer, end: integer) -> result; + getrange: async func(connection: borrow, key: string, start: integer, end: integer) -> result; /// Atomically sets key to value and returns the old value stored at key. /// /// GETSET key value /// https://valkey.io/commands/getset/ - getset: async func(connection: borrow, key: string, value: string) -> result, error>; + getset: async func(connection: borrow, key: string, value: string-or-bytes) -> result, error>; /// Increments the number stored at key by one. /// @@ -2313,7 +2340,7 @@ interface commands { /// /// LCS key1 key2 /// https://valkey.io/commands/lcs/ - lcs: async func(connection: borrow, key1: string, key2: string) -> result; + lcs: async func(connection: borrow, key1: string, key2: string) -> result; /// The LCS command implements the longest common subsequence algorithm. /// @@ -2335,31 +2362,31 @@ interface commands { /// /// MGET key [ key ... ] /// https://valkey.io/commands/mget/ - mget: async func(connection: borrow, keys: list) -> result>, error>; + mget: async func(connection: borrow, keys: list) -> result>, error>; /// Sets the given keys to their respective values. /// /// MSET key value [ key value ... ] /// https://valkey.io/commands/mset/ - mset: async func(connection: borrow, keys-and-values: list>) -> result<_, error>; + mset: async func(connection: borrow, keys-and-values: list>) -> result<_, error>; /// Sets multiple key-value pairs while also assigning a shared expiration time to all the keys atomically. /// /// MSETEX numkeys key value [ key value ... ] [ NX | XX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] /// https://valkey.io/commands/msetex/ - msetex: async func(connection: borrow, keys-and-values: list>, precondition: option, ttl: option) -> result; + msetex: async func(connection: borrow, keys-and-values: list>, precondition: option, ttl: option) -> result; /// Sets the given keys to their respective values. /// /// MSETNX key value [ key value ... ] /// https://valkey.io/commands/msetnx/ - msetnx: async func(connection: borrow, keys-and-values: list>) -> result; + msetnx: async func(connection: borrow, keys-and-values: list>) -> result; /// Set key to hold the string value with expiration in milliseconds. /// /// PSETEX key milliseconds value /// https://valkey.io/commands/psetex/ - psetex: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, value: string) -> result<_, error>; + psetex: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, value: string-or-bytes) -> result<_, error>; /// Set key to hold the string value. /// @@ -2367,31 +2394,31 @@ interface commands { /// /// SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] /// https://valkey.io/commands/set/ - set: async func(connection: borrow, key: string, value: string, precondition: option, ttl: option) -> result; + set: async func(connection: borrow, key: string, value: string-or-bytes, precondition: option, ttl: option) -> result; /// Set key to hold the string value. Returning the previous value. /// /// SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] GET [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] /// https://valkey.io/commands/set/ - set-get: async func(connection: borrow, key: string, value: string, precondition: option, ttl: option) -> result, error>; + set-get: async func(connection: borrow, key: string, value: string-or-bytes, precondition: option, ttl: option) -> result, error>; /// Set key to hold the string value with expiration in seconds. /// /// SETEX key seconds value /// https://valkey.io/commands/setex/ - setex: async func(connection: borrow, key: string, seconds: duration-seconds, value: string) -> result<_, error>; + setex: async func(connection: borrow, key: string, seconds: duration-seconds, value: string-or-bytes) -> result<_, error>; /// Set key to hold string value if key does not exist. /// /// SETNX key value /// https://valkey.io/commands/setnx/ - setnx: async func(connection: borrow, key: string, value: string) -> result; + setnx: async func(connection: borrow, key: string, value: string-or-bytes) -> result; /// Overwrites part of the string stored at key, starting at the specified offset, for the entire length of value. /// /// SETRANGE key offset value /// https://valkey.io/commands/setrange/ - setrange: async func(connection: borrow, key: string, offset: u32, value: string) -> result; + setrange: async func(connection: borrow, key: string, offset: u32, value: string-or-bytes) -> result; /// Returns the length of the string value stored at key. /// @@ -2403,6 +2430,6 @@ interface commands { /// /// SUBSTR key start end /// https://valkey.io/commands/substr/ - substr: async func(connection: borrow, key: string, start: integer, end: integer) -> result; + substr: async func(connection: borrow, key: string, start: integer, end: integer) -> result; } diff --git a/wit/connection.wit b/wit/connection.wit index 063264c..67bfa95 100644 --- a/wit/connection.wit +++ b/wit/connection.wit @@ -9,7 +9,7 @@ /// with them, subscriptions, push messages, and transactions. Higher level, typed operations are /// functions in the `commands` interface that borrow the connection. interface connection { - use types.{connect-opts, duration-milliseconds, error, resp-value, subscription-event, subscription-target}; + use types.{connect-opts, duration-milliseconds, error, resp-value, string-or-bytes, subscription-event, subscription-target}; @since(version = 0.3.0-dev) resource connection { @@ -51,7 +51,7 @@ interface connection { /// `emit`. /// /// A command has at least one word, an empty command fails with a client error. - exchange: async func(command: list) -> result; + exchange: async func(command: list) -> result; /// Sends the commands one after another, with no other command sent between them, and waits /// for every reply. @@ -59,7 +59,7 @@ interface connection { /// Each command has its own result, in order, as if each was sent with `exchange`. Commands /// sharing a connection may otherwise be interleaved with other callers' commands, which /// matters for a sequence that must not be interrupted, e.g. MULTI, its commands and EXEC. - exchange-all: async func(commands: list>) -> list>; + exchange-all: async func(commands: list>) -> list>; /// Sends a command without waiting for its reply, resolving once the command is written. /// @@ -68,7 +68,7 @@ interface connection { /// `CLIENT REPLY SKIP`. /// /// A command has at least one word, an empty command fails with a client error. - emit: async func(command: list) -> result<_, error>; + emit: async func(command: list) -> result<_, error>; /// Marks the keys to watch, EXEC aborts the next transaction when any of them changed. /// @@ -102,7 +102,7 @@ interface connection { /// MULTI, EXEC /// https://valkey.io/commands/multi/ /// https://valkey.io/commands/exec/ - transaction: async func(commands: list>) -> result>, error>; + transaction: async func(commands: list>) -> result>, error>; /// A new subscription, subscribed to nothing yet, see the subscription's `subscribe`. /// diff --git a/wit/types.wit b/wit/types.wit index b5f8170..6aca647 100644 --- a/wit/types.wit +++ b/wit/types.wit @@ -16,12 +16,24 @@ interface types { @since(version = 0.3.0-dev) type double = f64; + /// Raw bytes, e.g. a bulk string, which Valkey keeps as given, UTF-8 or not. + @since(version = 0.3.0-dev) + type bytes = list; + + @since(version = 0.3.0-dev) + variant string-or-bytes { + /// utf-8 decoded bytes. + %string(string), + /// bytes that cannot be represented as a utf-8 string. + bytes(bytes), + } + @since(version = 0.3.0-dev) variant resp-value { %string(string), error(string), integer(integer), - bulk-string(string), + bulk-string(bytes), array(list), null, boolean(bool), @@ -87,7 +99,8 @@ interface types { /// a message once for each target. @since(version = 0.3.0-dev) record subscription-message { - payload: string, + /// The message as published, UTF-8 or not. + payload: string-or-bytes, /// The channel the message was published to: a channel, published with PUBLISH, or a shard /// channel, published with SPUBLISH. Channels and shard channels are separate, a channel /// and a shard channel of the same name are not the same. `target` tells them apart, a @@ -1108,7 +1121,7 @@ interface types { @since(version = 0.3.0-dev) variant geosearch-from { /// `FROMMEMBER member`: the position of a member of the index. - member(string), + member(string-or-bytes), /// `FROMLONLAT longitude latitude` lonlat(geo-coordinates), } @@ -1172,7 +1185,7 @@ interface types { /// A member a search found, with the details asked for. @since(version = 0.3.0-dev) record geosearch-with-result { - member: string, + member: string-or-bytes, /// `WITHDIST`: the distance from the center, in the unit of the shape. distance: option, /// `WITHHASH`: the geohash, as an integer. @@ -1239,6 +1252,15 @@ interface types { // keyspace commands + /// The expiration of a key `restore` creates. + @since(version = 0.3.0-dev) + variant restore-ttl { + /// `ttl`: the key expires after the duration. + expire-in(duration-milliseconds), + /// `ttl` with `ABSTTL`: the key expires at the time. + expire-at(unix-time-milliseconds), + } + /// The type of the value stored at a key. @since(version = 0.3.0-dev) variant key-type { @@ -1330,6 +1352,17 @@ interface types { // scripting commands + /// What `function-restore` does with the libraries already loaded. + @since(version = 0.3.0-dev) + enum function-restore-policy { + /// `FLUSH`: deletes them first. + flush, + /// `APPEND`: keeps them, failing when a library of the same name is loaded. + append, + /// `REPLACE`: keeps them, replacing those with a library of the same name. + replace, + } + /// Whether a flush happens in the background, `ASYNC`, or before replying, `SYNC`. @since(version = 0.3.0-dev) enum flush-mode { @@ -1402,9 +1435,9 @@ interface types { @since(version = 0.3.0-dev) variant lex-bound { /// `[member`: the member, included in the range. - inclusive(string), + inclusive(string-or-bytes), /// `(member`: the member, excluded from the range. - exclusive(string), + exclusive(string-or-bytes), /// `-` as the min, `+` as the max, the start is the max with `REV`. unbounded, } @@ -1594,10 +1627,10 @@ interface types { entries-added: integer, /// The ID and fields of the first entry. /// None when the stream is empty. - first-entry: option>>, + first-entry: option>>, /// The ID and fields of the last entry. /// None when the stream is empty. - last-entry: option>>, + last-entry: option>>, } /// Information about a stream, its entries and consumer groups, see `xinfo-stream-full`. @@ -1616,7 +1649,7 @@ interface types { /// The number of entries added to the stream over its lifetime. entries-added: integer, /// The ID and fields of the entries, in ascending ID order, up to the count given. - entries: list>>, + entries: list>>, /// The consumer groups of the stream. groups: list, } @@ -1747,9 +1780,9 @@ interface types { /// Only set the key if it already exists. xx, /// Set the key if the comparison value matches the existing value. - ifeq(string), + ifeq(string-or-bytes), /// Set the key if the comparison value does not match the existing value. - ifne(string), + ifne(string-or-bytes), } /// The expiration to set on a key, see `set`. From 0c09be4c7c9f0503b4be71923e0fe6cc3bf20e56 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 14 Oct 2026 17:18:33 -0400 Subject: [PATCH 32/37] shrink debug component sizes while preserving debugability Signed-off-by: Scott Andrews --- Cargo.toml | 9 +++++++++ make/components.mk | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 367f828..465d2ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,3 +32,12 @@ opt-level = "s" lto = true codegen-units = 1 strip = "debuginfo" + +[profile.component-debug] +inherits = "dev" +opt-level = 1 # most of the size win; locals mostly still visible +lto = true +codegen-units = 1 +incremental = false # no benefit with one codegen unit +debug = "full" # variables and types for lldb, file:line in backtraces +# kept from dev: debug-assertions, overflow-checks, no strip diff --git a/make/components.mk b/make/components.mk index 012e5b8..21e584b 100644 --- a/make/components.mk +++ b/make/components.mk @@ -57,8 +57,8 @@ ${COMPONENTS_DIR}/$1/$1.wasm: Cargo.toml Cargo.lock components/wit/deps $(shell $(WASM_TOOLS) component new target/wasm32-unknown-unknown/release/$(subst -,_,$1).wasm -o ${COMPONENTS_DIR}/$1/$1.wasm ${COMPONENTS_DIR}/$1/$1.debug.wasm: Cargo.toml Cargo.lock components/wit/deps $(shell find components/$1 -type f) $(shell find crates -type f 2> /dev/null) ${COMPONENTS_DIR}/$1/README.md | $(call tool,wasm-tools) - cargo build --target wasm32-unknown-unknown -p $1 - $(WASM_TOOLS) component new target/wasm32-unknown-unknown/debug/$(subst -,_,$1).wasm -o ${COMPONENTS_DIR}/$1/$1.debug.wasm + cargo build -p $1 --target wasm32-unknown-unknown --profile component-debug + $(WASM_TOOLS) component new target/wasm32-unknown-unknown/component-debug/$(subst -,_,$1).wasm -o ${COMPONENTS_DIR}/$1/$1.debug.wasm endif From 9285ab438735da8786c94203065f6ff56adc9f6d Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Thu, 15 Oct 2026 19:24:05 -0400 Subject: [PATCH 33/37] Use componentized/sockets gates to restrict network access The `tcp-connector-latch` limits network access to only the essential capabilities necessary to establish a connection with the `tcp-connector`. In order, it will only lookup IP address for the configured hostname. TCP sockets can only connect to the IP address returned by the hostname lookup, on the port specified by the config. No inbound connections are allow, nor any UDP activity. The gate and latch are installed by default into the `cli` component as that uses the `tcp-connector`. Other users of the `tcp` transport will need to configure a latch appropriate for their environment. Signed-off-by: Scott Andrews --- Cargo.lock | 73 +++-- Cargo.toml | 9 +- Makefile | 6 +- README.md | 47 ++- components/as-keyvalue/README.md | 2 +- components/cli/README.md | 28 +- components/cli/cli.wac | 66 ++++- components/client/README.md | 2 +- components/client/client.wac | 5 + components/commands/tests/commands.rs | 2 +- .../README.md | 3 + .../dep-componentized-logging-to-stderr.wkg | 2 + .../dep-componentized-sockets-gate/README.md | 3 + .../dep-componentized-sockets-gate.wkg | 2 + .../README.md | 3 + ...omponentized-sockets-latch-cidr-egress.wkg | 2 + .../README.md | 3 + ...-componentized-sockets-latch-deny-bind.wkg | 2 + .../README.md | 3 + ...tch-deny-connect-unless-lookup-address.wkg | 2 + .../README.md | 3 + ...p-componentized-sockets-latch-deny-udp.wkg | 2 + .../README.md | 3 + ...ized-sockets-latch-ip-name-lookup-glob.wkg | 2 + .../README.md | 3 + .../dep-componentized-sockets-latch-n5.wkg | 2 + .../{dep-cli => internal-cli}/Cargo.toml | 2 +- .../{dep-cli => internal-cli}/README.md | 2 +- .../{dep-cli => internal-cli}/src/lib.rs | 2 +- .../Cargo.toml | 11 + .../README.md | 11 + .../src/lib.rs | 78 +++++ .../Cargo.toml | 11 + .../README.md | 12 + .../src/lib.rs | 90 ++++++ components/sample-http-incrementor/README.md | 2 +- components/tcp-connector-constants/README.md | 7 + .../wit/tcp-connector-constants.constants.wit | 5 + components/tcp-connector-constants/wkg.lock | 4 + components/tcp-connector-constants/wkg.toml | 2 + components/tcp-connector-latch/README.md | 51 ++++ .../tcp-connector-latch.wac | 49 ++++ components/tcp-connector/README.md | 4 +- components/tcp-connector/src/lib.rs | 38 ++- components/tcp/README.md | 6 +- components/tcp/src/lib.rs | 23 +- .../Cargo.toml | 2 +- .../README.md | 2 +- .../src/lib.rs | 2 +- components/test-commands/README.md | 2 +- components/test-commands/test-commands.wac | 7 +- components/test-stub-sockets/Cargo.toml | 11 + components/test-stub-sockets/README.md | 5 + components/test-stub-sockets/src/lib.rs | 277 ++++++++++++++++++ .../test-tcp-connector-latch-suite/Cargo.toml | 11 + .../test-tcp-connector-latch-suite/README.md | 5 + .../test-tcp-connector-latch-suite/src/lib.rs | 253 ++++++++++++++++ .../test-tcp-connector-latch/Cargo.toml | 13 + components/test-tcp-connector-latch/README.md | 15 + .../test-tcp-connector-latch.wac | 34 +++ .../test-tcp-connector-latch/tests/latch.rs | 27 ++ components/test-tcp/test-tcp.wac | 5 + components/wit/worlds.wit | 32 ++ crates/test-harness/src/lib.rs | 3 + make/components.mk | 17 +- make/publish.mk | 6 +- scripts/wac-deps.sh | 46 +++ tools/Cargo.toml | 2 + wit/tcp-connector-constants.wit | 48 +++ wit/worlds.wit | 1 + 70 files changed, 1417 insertions(+), 99 deletions(-) create mode 100644 components/dep-componentized-logging-to-stderr/README.md create mode 100644 components/dep-componentized-logging-to-stderr/dep-componentized-logging-to-stderr.wkg create mode 100644 components/dep-componentized-sockets-gate/README.md create mode 100644 components/dep-componentized-sockets-gate/dep-componentized-sockets-gate.wkg create mode 100644 components/dep-componentized-sockets-latch-cidr-egress/README.md create mode 100644 components/dep-componentized-sockets-latch-cidr-egress/dep-componentized-sockets-latch-cidr-egress.wkg create mode 100644 components/dep-componentized-sockets-latch-deny-bind/README.md create mode 100644 components/dep-componentized-sockets-latch-deny-bind/dep-componentized-sockets-latch-deny-bind.wkg create mode 100644 components/dep-componentized-sockets-latch-deny-connect-unless-lookup-address/README.md create mode 100644 components/dep-componentized-sockets-latch-deny-connect-unless-lookup-address/dep-componentized-sockets-latch-deny-connect-unless-lookup-address.wkg create mode 100644 components/dep-componentized-sockets-latch-deny-udp/README.md create mode 100644 components/dep-componentized-sockets-latch-deny-udp/dep-componentized-sockets-latch-deny-udp.wkg create mode 100644 components/dep-componentized-sockets-latch-ip-name-lookup-glob/README.md create mode 100644 components/dep-componentized-sockets-latch-ip-name-lookup-glob/dep-componentized-sockets-latch-ip-name-lookup-glob.wkg create mode 100644 components/dep-componentized-sockets-latch-n5/README.md create mode 100644 components/dep-componentized-sockets-latch-n5/dep-componentized-sockets-latch-n5.wkg rename components/{dep-cli => internal-cli}/Cargo.toml (92%) rename components/{dep-cli => internal-cli}/README.md (97%) rename components/{dep-cli => internal-cli}/src/lib.rs (99%) create mode 100644 components/internal-tcp-connector-egress-config/Cargo.toml create mode 100644 components/internal-tcp-connector-egress-config/README.md create mode 100644 components/internal-tcp-connector-egress-config/src/lib.rs create mode 100644 components/internal-tcp-connector-lookup-config/Cargo.toml create mode 100644 components/internal-tcp-connector-lookup-config/README.md create mode 100644 components/internal-tcp-connector-lookup-config/src/lib.rs create mode 100644 components/tcp-connector-constants/README.md create mode 100644 components/tcp-connector-constants/wit/tcp-connector-constants.constants.wit create mode 100644 components/tcp-connector-constants/wkg.lock create mode 100644 components/tcp-connector-constants/wkg.toml create mode 100644 components/tcp-connector-latch/README.md create mode 100644 components/tcp-connector-latch/tcp-connector-latch.wac rename components/{dep-test-commands => test-commands-suite}/Cargo.toml (88%) rename components/{dep-test-commands => test-commands-suite}/README.md (94%) rename components/{dep-test-commands => test-commands-suite}/src/lib.rs (99%) create mode 100644 components/test-stub-sockets/Cargo.toml create mode 100644 components/test-stub-sockets/README.md create mode 100644 components/test-stub-sockets/src/lib.rs create mode 100644 components/test-tcp-connector-latch-suite/Cargo.toml create mode 100644 components/test-tcp-connector-latch-suite/README.md create mode 100644 components/test-tcp-connector-latch-suite/src/lib.rs create mode 100644 components/test-tcp-connector-latch/Cargo.toml create mode 100644 components/test-tcp-connector-latch/README.md create mode 100644 components/test-tcp-connector-latch/test-tcp-connector-latch.wac create mode 100644 components/test-tcp-connector-latch/tests/latch.rs create mode 100755 scripts/wac-deps.sh create mode 100644 wit/tcp-connector-constants.wit diff --git a/Cargo.lock b/Cargo.lock index 55524e3..a88cd69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -421,24 +421,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "dep-cli" -version = "0.1.0" -dependencies = [ - "componentized-valkey", - "futures-channel", - "futures-core", - "wit-bindgen", -] - -[[package]] -name = "dep-test-commands" -version = "0.3.0-dev" -dependencies = [ - "componentized-valkey", - "wit-bindgen", -] - [[package]] name = "digest" version = "0.10.7" @@ -847,6 +829,30 @@ dependencies = [ "serde_core", ] +[[package]] +name = "internal-cli" +version = "0.1.0" +dependencies = [ + "componentized-valkey", + "futures-channel", + "futures-core", + "wit-bindgen", +] + +[[package]] +name = "internal-tcp-connector-egress-config" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "internal-tcp-connector-lookup-config" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "io-extras" version = "0.19.0" @@ -1484,6 +1490,14 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test-commands-suite" +version = "0.3.0-dev" +dependencies = [ + "componentized-valkey", + "wit-bindgen", +] + [[package]] name = "test-harness" version = "0.3.0-dev" @@ -1494,6 +1508,29 @@ dependencies = [ "wasmtime-wasi", ] +[[package]] +name = "test-stub-sockets" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "test-tcp-connector-latch-suite" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "test-tcp-connector-latch-tests" +version = "0.1.0" +dependencies = [ + "test-harness", + "tokio", + "wasmtime", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/Cargo.toml b/Cargo.toml index 465d2ab..a1ca8ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,11 +5,16 @@ members = [ "components/commands", "components/connection", "components/connection-minimal", - "components/dep-cli", - "components/dep-test-commands", + "components/internal-cli", + "components/test-commands-suite", + "components/test-stub-sockets", + "components/test-tcp-connector-latch", + "components/test-tcp-connector-latch-suite", "components/sample-http-incrementor", "components/tcp", "components/tcp-connector", + "components/internal-tcp-connector-egress-config", + "components/internal-tcp-connector-lookup-config", "crates/componentized-valkey", "crates/test-harness", ] diff --git a/Makefile b/Makefile index 6e3877f..2ce84ee 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,8 @@ SHELL := /bin/bash export RUST_BACKTRACE ?= 1 export WASMTIME_BACKTRACE_DETAILS ?= 1 export WKG_CONFIG_FILE := $(abspath .config/wasm-pkg/config.toml) -WASMTIME_RUN_FLAGS ?= -Sinherit-network -Sallow-ip-name-lookup -Sconfig -Wcomponent-model-map +# the sockets gates of the `cli` need `implements` and `threading` +WASMTIME_RUN_FLAGS ?= -Sinherit-network -Sallow-ip-name-lookup -Sconfig -Wcomponent-model-map -Wcomponent-model-implements -Wcomponent-model-threading # `make` without a goal runs all, otherwise the first target of an included file would run .DEFAULT_GOAL := all @@ -48,7 +49,8 @@ test-valkey: components | $(call tool,wasmtime-cli) until docker exec $$container valkey-cli ping > /dev/null 2>&1; do sleep 0.1; done; \ echo "Valkey $$(docker exec $$container valkey-server --version | sed 's/.* v=\([^ ]*\).*/\1/') on 127.0.0.1:$$port"; \ VALKEY_HOST=127.0.0.1 VALKEY_PORT=$$port cargo test --workspace --no-fail-fast -- --ignored with_valkey::; \ - $(WASMTIME) run $(WASMTIME_RUN_FLAGS) -Sconfig-var=port=$$port ${COMPONENTS_DIR}/cli/cli.debug.wasm ping + $(WASMTIME) run $(WASMTIME_RUN_FLAGS) -Sconfig-var=port=$$port ${COMPONENTS_DIR}/cli/cli.debug.wasm ping; \ + $(WASMTIME) run $(WASMTIME_RUN_FLAGS) -Sconfig-var=hostname=localhost -Sconfig-var=port=$$port ${COMPONENTS_DIR}/cli/cli.debug.wasm ping .PHONY: test test: components diff --git a/README.md b/README.md index 2ae0847..b36efdc 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ A WASM component client for Valkey (and Redis). - [Components](#components) +- [Network access](#network-access) - [Build](#build) - [Test](#test) - [Run](#run) @@ -14,23 +15,34 @@ A WASM component client for Valkey (and Redis). - [Acknowledgements](#acknowledgements) - [License](#license) - ## Components The `componentized:valkey` WIT package, in [`wit`](./wit), separates the connection to a server from the transport carrying it, and from the commands sent over it. Connections always speak RESP3. -| Component | | -| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [`connection`](./components/connection/) | the mechanics of a connection: pairs commands with replies, keeps push messages apart, establishes it with `HELLO 3`, optionally keeps it alive | -| [`tcp`](./components/tcp/) | connections over wasi:sockets TCP | -| [`tcp-connector`](./components/tcp-connector/) | connections over TCP to the server configured with wasi:config, for components that need a connection without a say in how it is made | -| [`commands`](./components/commands/) | the commands, an interface for each of Valkey's ACL categories, e.g. `hash-commands`, each also available as a component of its own, e.g. [`hash-commands`](./components/hash-commands/) | -| [`as-keyvalue`](./components/as-keyvalue/) | wasmcloud:keyvalue, backed by Valkey hashes | -| [`client`](./components/client/) | everything above, composed into one component | -| [`cli`](./components/cli/) | a command line client | +| Component | | +| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`connection`](./components/connection/) | the mechanics of a connection: pairs commands with replies, keeps push messages apart, establishes it with `HELLO 3`, optionally keeps it alive | +| [`tcp`](./components/tcp/) | connections over wasi:sockets TCP | +| [`tcp-connector`](./components/tcp-connector/) | connections over TCP to the server configured with wasi:config, for components that need a connection without a say in how it is made | +| [`tcp-connector-constants`](./components/tcp-connector-constants/) | the keys of the `tcp-connector` config and their defaults, for components that derive their config from it | +| [`tcp-connector-latch`](./components/tcp-connector-latch/) | a componentized:sockets latch, for a gate, limiting sockets to the server in the `tcp-connector` config | +| [`commands`](./components/commands/) | the commands, an interface for each of Valkey's ACL categories, e.g. `hash-commands`, each also available as a component of its own, e.g. [`hash-commands`](./components/hash-commands/) | +| [`as-keyvalue`](./components/as-keyvalue/) | wasmcloud:keyvalue, backed by Valkey hashes | +| [`client`](./components/client/) | everything above, composed into one component | +| [`cli`](./components/cli/) | a command line client, behind a gate limiting it to the server it is configured for | A composition picks the transport, e.g. `tcp` and `tcp-connector`, other transports can provide connections to the same commands. The [`*.wac`](./components/client/client.wac) files show how the components are composed. +## Network access + +The network access of a client can be limited with a [componentized:sockets](https://github.com/componentized/sockets) gate. A gate imports `wasi:sockets` and exports it to the components it is put in front of, asking a latch before each socket operation, and denying the operation if the latch does. + +A critical aspect of gates and latches is how they protect a slice of the composed component. Behavior is selectively applied to parts of an application by wrapping that portion of the composition with a gate. For example, network access to a Valkey server can be segregated from network access to other services. Host based controls apply equally to all parts of the component. A gate can never increase privilege, only restrict otherwise granted capabilities. + +The [`tcp-connector-latch`](./components/tcp-connector-latch/) allows normal access to the Valkey server in the `tcp-connector` config, and denies every other socket. Suppose a client connects to a malicious server that is able to trigger a remote code execution within the client. The gate prevents the client from communicating with any other network resources, dramatically limiting the blast radius of exploitation. The [`cli`](./components/cli/) is always behind a gate with this latch. + +Many other latches are available in the [`componentized/sockets`](https://github.com/componentized/sockets) repo, or a custom latch can be implemented by exporting the `componentized:sockets/latch` interface. + ## Build A [dev container](https://containers.dev) is available that contains the necessary tools and configuration out of the box. @@ -46,7 +58,15 @@ make components The build creates each component in [`components`](./components) into `target/components`, e.g. the cli at `target/components/cli/cli.wasm`, along with `target/components/interface.wasm`, the `componentized:valkey` WIT package. Each component is also built with debug info, e.g. `target/components/cli/cli.debug.wasm`. -The cli tools the build uses, [`wasm-tools`](https://github.com/bytecodealliance/wasm-tools), [`wac`](https://github.com/bytecodealliance/wac), [`wasmtime`](https://github.com/bytecodealliance/wasmtime) and [`wkg`](https://github.com/bytecodealliance/wasm-pkg-tools), are pinned in [`tools/Cargo.toml`](./tools/Cargo.toml) and installed into `target/tools/`, e.g. `target/tools/aarch64-apple-darwin`, as needed, or ahead of time with `make tools`. Dependabot bumps the pinned versions. +Every component is published, except those with these prefixes: + +| Prefix | | +| ----------- | ---------------------------------------------------------------------------------- | +| `dep-` | pulled from elsewhere, named for its source, e.g. `dep-componentized-sockets-gate` | +| `internal-` | built here, for use by other components, e.g. `internal-cli` composed into `cli` | +| `test-` | tests, e.g. `test-commands` | + +The cli tools the build uses, [`constants`](https://github.com/componentized/constants), [`wasm-tools`](https://github.com/bytecodealliance/wasm-tools), [`wac`](https://github.com/bytecodealliance/wac), [`wasmtime`](https://github.com/bytecodealliance/wasmtime) and [`wkg`](https://github.com/bytecodealliance/wasm-pkg-tools), are pinned in [`tools/Cargo.toml`](./tools/Cargo.toml) and installed into `target/tools/`, e.g. `target/tools/aarch64-apple-darwin`, as needed, or ahead of time with `make tools`. Dependabot bumps the pinned versions. ## Test @@ -68,13 +88,14 @@ Prereqs: - access to a running [Valkey](https://valkey.io) server ```sh -wasmtime run -Sinherit-network -Sallow-ip-name-lookup -Sconfig target/components/cli/cli.wasm keys '*' +wasmtime run -Sinherit-network -Sallow-ip-name-lookup -Sconfig -Wcomponent-model-map -Wcomponent-model-implements -Wcomponent-model-threading \ + target/components/cli/cli.wasm keys '*' ``` The arguments are sent as a command and the reply is printed. A command without a reply, like `SUBSCRIBE`, prints the push messages that arrive until interrupted or the connection closes. - the server is configured with wasi:config variables, e.g. `-Sconfig-var=hostname=valkey.example.com -Sconfig-var=port=6380`, see [`tcp-connector`](./components/tcp-connector/) for the keys and their defaults -- `-Sallow-ip-name-lookup` is only required if a hostname is used for the connection instead of an IP address. +- `-Sallow-ip-name-lookup` is required, the `tcp` component looks up the `hostname`, an IP address too, which wasi:sockets returns as is To aid incremental development a make target is available to rebuild and run the CLI: diff --git a/components/as-keyvalue/README.md b/components/as-keyvalue/README.md index 030a1fc..d2b9576 100644 --- a/components/as-keyvalue/README.md +++ b/components/as-keyvalue/README.md @@ -1,6 +1,6 @@ # as-keyvalue -Adapter to back wasmcloud:keyvalue with the componentized:valkey client. +Adapter to back `wasmcloud:keyvalue` with the `componentized:valkey` client. > [!NOTE] > `wasi:keyvalue` is currently unsupportable with an async based client. It will be restored when either it updates it's own API to be async, or the component model is fully able to make async calls from sync functions. diff --git a/components/cli/README.md b/components/cli/README.md index 3c4c108..97a51e6 100644 --- a/components/cli/README.md +++ b/components/cli/README.md @@ -1,3 +1,29 @@ # Valkey CLI -A wasi:cli/command sending its arguments as a command to Valkey, composed of [`dep-cli`](../dep-cli/) and a [`tcp-connector`](../tcp-connector/) configured with wasi:config. +A command line client for Valkey, sending its arguments as a command and printing the reply. + +Composed of [`internal-cli`](../internal-cli/) and a [`tcp-connector`](../tcp-connector/) configured with wasi:config. + +Every connection the cli makes is through the connector, so the cli is behind a [componentized:sockets](https://github.com/componentized/sockets) [`gate`](../dep-componentized-sockets-gate/) with the [`tcp-connector-latch`](../tcp-connector-latch/). It can only reach the server it is configured for, see [network access](../../README.md#network-access). The gate and the latch log denials with [`to-stderr`](../dep-componentized-logging-to-stderr/), keeping them out of the cli's output. + +## Run + +Start a Valkey server: + +```sh +docker run --rm -p 6379:6379 valkey/valkey:9 +``` + +```sh +wasmtime run -Sinherit-network -Sallow-ip-name-lookup -Sconfig -Wcomponent-model-map -Wcomponent-model-implements -Wcomponent-model-threading \ + target/components/cli/cli.wasm ping +``` +```txt +PONG +``` + +The server is configured with wasi:config variables, e.g. `-Sconfig-var=hostname=localhost`, see [`tcp-connector`](../tcp-connector/) for the keys and their defaults. The latch follows the same config. + +The gate needs the component model's `implements` and `threading` features, `-Wcomponent-model-implements -Wcomponent-model-threading`. + +See [`cli.wac`](./cli.wac) for the composition. diff --git a/components/cli/cli.wac b/components/cli/cli.wac index 6ce69c1..0196198 100644 --- a/components/cli/cli.wac +++ b/components/cli/cli.wac @@ -1,21 +1,75 @@ -package componentized:valkey; +package componentized:valkey-cli; -let connection = new local:connection { +// The host interfaces the components behind the gate may use. Their imports are wired explicitly, +// without `...`, so an import of anything else, like wasi:sockets of any version, fails to compose +// instead of reaching the host around the gate. +import clocks-types: wasi:clocks/types@0.3.0; +import monotonic-clock: wasi:clocks/monotonic-clock@0.3.0; +import config: wasi:config/store@0.2.0-rc.1; +import environment: wasi:cli/environment@0.3.0; +import cli-types: wasi:cli/types@0.3.0; +import stdin: wasi:cli/stdin@0.3.0; +import stdout: wasi:cli/stdout@0.3.0; +import stderr: wasi:cli/stderr@0.3.0; +import valkey-types: componentized:valkey/types@0.3.0-dev; + +// The gate, the latch and the logger see the host, wasi:sockets included, the interfaces they +// share with the components behind the gate are wired to the same imports. + +// wasmtime does not provide wasi:logging, so the composition does, for the latch and the gate +let logging = new local:dep-componentized-logging-to-stderr { + "wasi:cli/types@0.3.0": cli-types, + "wasi:cli/stderr@0.3.0": stderr, + ... +}; + +// the latch, limiting sockets to the server in the tcp-connector's config +let latch = new local:tcp-connector-latch { + "wasi:config/store@0.2.0-rc.1": config, + "wasi:logging/logging@0.1.0-draft": logging["wasi:logging/logging@0.1.0-draft"], + ... +}; + +// wasi:sockets, behind the gate, opened by the latch +let gate = new local:dep-componentized-sockets-gate { + "wasi:clocks/types@0.3.0": clocks-types, + "componentized:sockets/latch@0.1.0": latch["componentized:sockets/latch@0.1.0"], + "wasi:logging/logging@0.1.0-draft": logging["wasi:logging/logging@0.1.0-draft"], ... }; + +// Behind the gate, every import is wired explicitly. + +let connection = new local:connection { + "wasi:clocks/types@0.3.0": clocks-types, + "wasi:clocks/monotonic-clock@0.3.0": monotonic-clock, + "componentized:valkey/types@0.3.0-dev": valkey-types, +}; +// connections over wasi:sockets from the gate instead of the host let tcp = new local:tcp { + "wasi:sockets/types@0.3.0": gate["wasi:sockets/types@0.3.0"], + "wasi:sockets/ip-name-lookup@0.3.0": gate["wasi:sockets/ip-name-lookup@0.3.0"], + "componentized:valkey/types@0.3.0-dev": valkey-types, "componentized:valkey/connection@0.3.0-dev": connection.connection, - ... }; +// the keys of the tcp-connector's config, and their defaults +let tcp-connector-constants = new local:tcp-connector-constants {}; let connector = new local:tcp-connector { + "wasi:config/store@0.2.0-rc.1": config, + "componentized:valkey/tcp-connector-constants@0.3.0-dev": tcp-connector-constants["componentized:valkey/tcp-connector-constants@0.3.0-dev"], + "componentized:valkey/types@0.3.0-dev": valkey-types, "componentized:valkey/connection@0.3.0-dev": connection.connection, "componentized:valkey/tcp@0.3.0-dev": tcp.tcp, - ... }; -let cli = new local:dep-cli { +let cli = new local:internal-cli { + "componentized:valkey/types@0.3.0-dev": valkey-types, "componentized:valkey/connection@0.3.0-dev": connection.connection, "componentized:valkey/connector@0.3.0-dev": connector.connector, - ... + "wasi:cli/environment@0.3.0": environment, + "wasi:cli/types@0.3.0": cli-types, + "wasi:cli/stdin@0.3.0": stdin, + "wasi:cli/stdout@0.3.0": stdout, + "wasi:cli/stderr@0.3.0": stderr, }; export cli...; diff --git a/components/client/README.md b/components/client/README.md index 89cf87e..5773f4f 100644 --- a/components/client/README.md +++ b/components/client/README.md @@ -1,6 +1,6 @@ # client -Client for interacting with a Valkey server, as componentized:valkey and wasmcloud:keyvalue. +Client for interacting with a Valkey server, as `componentized:valkey` and `wasmcloud:keyvalue`. > [!NOTE] > `wasi:keyvalue` is currently unsupportable with an async based client. It will be restored when either it updates it's own API to be async, or the component model is fully able to make async calls from sync functions. diff --git a/components/client/client.wac b/components/client/client.wac index 8a7b6dd..c199f8b 100644 --- a/components/client/client.wac +++ b/components/client/client.wac @@ -11,7 +11,12 @@ let commands = new local:commands { "componentized:valkey/connection@0.3.0-dev": connection.connection, ... }; +// the keys of the tcp-connector's config, and their defaults +let tcp-connector-constants = new local:tcp-connector-constants { + ... +}; let connector = new local:tcp-connector { + "componentized:valkey/tcp-connector-constants@0.3.0-dev": tcp-connector-constants["componentized:valkey/tcp-connector-constants@0.3.0-dev"], "componentized:valkey/connection@0.3.0-dev": connection.connection, "componentized:valkey/tcp@0.3.0-dev": tcp.tcp, ... diff --git a/components/commands/tests/commands.rs b/components/commands/tests/commands.rs index 9ca2402..48c30c9 100644 --- a/components/commands/tests/commands.rs +++ b/components/commands/tests/commands.rs @@ -1,4 +1,4 @@ -//! The tests of the commands, run inside `dep-test-commands`, composed with `commands` and +//! The tests of the commands, run inside `test-commands-suite`, composed with `commands` and //! `connection` as `test-commands`. Each test plays the server over the streams of a connection, //! checking the command a function sends and how it converts the reply. //! diff --git a/components/dep-componentized-logging-to-stderr/README.md b/components/dep-componentized-logging-to-stderr/README.md new file mode 100644 index 0000000..e8400a4 --- /dev/null +++ b/components/dep-componentized-logging-to-stderr/README.md @@ -0,0 +1,3 @@ +# dep-componentized-logging-to-stderr + +From [componentized logging](https://github.com/componentized/logging): [`to-stderr`](https://github.com/componentized/logging/tree/main/components/to-stderr), wasi:logging written to stderr, pulled from `ghcr.io/componentized/logging/to-stderr:0.3.1`. diff --git a/components/dep-componentized-logging-to-stderr/dep-componentized-logging-to-stderr.wkg b/components/dep-componentized-logging-to-stderr/dep-componentized-logging-to-stderr.wkg new file mode 100644 index 0000000..99d9410 --- /dev/null +++ b/components/dep-componentized-logging-to-stderr/dep-componentized-logging-to-stderr.wkg @@ -0,0 +1,2 @@ +ghcr.io/componentized/logging/to-stderr:0.3.1@sha256:f5bb0d492dc653fd90f78db78c6c6dcc4cf5f35115446bbe001c1ab34e09a1d8 +ghcr.io/componentized/logging/to-stderr:0.3.1_debug@sha256:bb0e846507776b8b9c1646c37ea4cd83c2f1dc4b01c670ed5ec51e1a7a059954 diff --git a/components/dep-componentized-sockets-gate/README.md b/components/dep-componentized-sockets-gate/README.md new file mode 100644 index 0000000..b459da7 --- /dev/null +++ b/components/dep-componentized-sockets-gate/README.md @@ -0,0 +1,3 @@ +# dep-componentized-sockets-gate + +From [componentized sockets](https://github.com/componentized/sockets): [`gate`](https://github.com/componentized/sockets/tree/main/components/gate), gates wasi:sockets tcp and udp sockets, and ip-name-lookup, consulting a latch before each operation, pulled from `ghcr.io/componentized/sockets/gate:0.1.0`. diff --git a/components/dep-componentized-sockets-gate/dep-componentized-sockets-gate.wkg b/components/dep-componentized-sockets-gate/dep-componentized-sockets-gate.wkg new file mode 100644 index 0000000..99de749 --- /dev/null +++ b/components/dep-componentized-sockets-gate/dep-componentized-sockets-gate.wkg @@ -0,0 +1,2 @@ +ghcr.io/componentized/sockets/gate:0.1.0@sha256:91be0efd2190305e0e0ff44bd824c2426986b510ae78fcecc0bf60fe23660237 +ghcr.io/componentized/sockets/gate:0.1.0_debug@sha256:205770707c75c041746d237626a6acedd686181a49a724b66b0b64a9c841e71b diff --git a/components/dep-componentized-sockets-latch-cidr-egress/README.md b/components/dep-componentized-sockets-latch-cidr-egress/README.md new file mode 100644 index 0000000..7abbdd0 --- /dev/null +++ b/components/dep-componentized-sockets-latch-cidr-egress/README.md @@ -0,0 +1,3 @@ +# dep-componentized-sockets-latch-cidr-egress + +From [componentized sockets](https://github.com/componentized/sockets): [`latch-cidr-egress`](https://github.com/componentized/sockets/tree/main/components/latch-cidr-egress), grants or denies outbound traffic by remote address and port ranges, configured with wasi:config, pulled from `ghcr.io/componentized/sockets/latch-cidr-egress:0.1.0`. diff --git a/components/dep-componentized-sockets-latch-cidr-egress/dep-componentized-sockets-latch-cidr-egress.wkg b/components/dep-componentized-sockets-latch-cidr-egress/dep-componentized-sockets-latch-cidr-egress.wkg new file mode 100644 index 0000000..6518ba6 --- /dev/null +++ b/components/dep-componentized-sockets-latch-cidr-egress/dep-componentized-sockets-latch-cidr-egress.wkg @@ -0,0 +1,2 @@ +ghcr.io/componentized/sockets/latch-cidr-egress:0.1.0@sha256:82e111dd0c7a75e7344e4d3d89778da5adf19d4b9c2ca15265c6c2254a2aeec5 +ghcr.io/componentized/sockets/latch-cidr-egress:0.1.0_debug@sha256:4330630236965724141f7fabe34ead6b0039abf87f6f3c31d41e651f0c3e6ad3 diff --git a/components/dep-componentized-sockets-latch-deny-bind/README.md b/components/dep-componentized-sockets-latch-deny-bind/README.md new file mode 100644 index 0000000..70ead51 --- /dev/null +++ b/components/dep-componentized-sockets-latch-deny-bind/README.md @@ -0,0 +1,3 @@ +# dep-componentized-sockets-latch-deny-bind + +From [componentized sockets](https://github.com/componentized/sockets): [`latch-deny-bind`](https://github.com/componentized/sockets/tree/main/components/latch-deny-bind), denies explicitly binding sockets to a local address, pulled from `ghcr.io/componentized/sockets/latch-deny-bind:0.1.0`. diff --git a/components/dep-componentized-sockets-latch-deny-bind/dep-componentized-sockets-latch-deny-bind.wkg b/components/dep-componentized-sockets-latch-deny-bind/dep-componentized-sockets-latch-deny-bind.wkg new file mode 100644 index 0000000..5c7495a --- /dev/null +++ b/components/dep-componentized-sockets-latch-deny-bind/dep-componentized-sockets-latch-deny-bind.wkg @@ -0,0 +1,2 @@ +ghcr.io/componentized/sockets/latch-deny-bind:0.1.0@sha256:51a11baab568e01ff1319971b7d1ec7a23f89c1d79b77fb9024ee34da7f5e578 +ghcr.io/componentized/sockets/latch-deny-bind:0.1.0_debug@sha256:b6851cf0ab2b5ca5d3cbc5b43b6eab5bef3b9d64d920035e3fa25daf62994611 diff --git a/components/dep-componentized-sockets-latch-deny-connect-unless-lookup-address/README.md b/components/dep-componentized-sockets-latch-deny-connect-unless-lookup-address/README.md new file mode 100644 index 0000000..1cbd0cd --- /dev/null +++ b/components/dep-componentized-sockets-latch-deny-connect-unless-lookup-address/README.md @@ -0,0 +1,3 @@ +# dep-componentized-sockets-latch-deny-connect-unless-lookup-address + +From [componentized sockets](https://github.com/componentized/sockets): [`latch-deny-connect-unless-lookup-address`](https://github.com/componentized/sockets/tree/main/components/latch-deny-connect-unless-lookup-address), denies connecting to addresses an allowed ip-name lookup did not return, pulled from `ghcr.io/componentized/sockets/latch-deny-connect-unless-lookup-address:0.1.0`. diff --git a/components/dep-componentized-sockets-latch-deny-connect-unless-lookup-address/dep-componentized-sockets-latch-deny-connect-unless-lookup-address.wkg b/components/dep-componentized-sockets-latch-deny-connect-unless-lookup-address/dep-componentized-sockets-latch-deny-connect-unless-lookup-address.wkg new file mode 100644 index 0000000..83f5849 --- /dev/null +++ b/components/dep-componentized-sockets-latch-deny-connect-unless-lookup-address/dep-componentized-sockets-latch-deny-connect-unless-lookup-address.wkg @@ -0,0 +1,2 @@ +ghcr.io/componentized/sockets/latch-deny-connect-unless-lookup-address:0.1.0@sha256:cb2ac10d0dfe34e5665f4ac86fa64ebb66d494e867dc4f3cc85d6a6f90a72bb7 +ghcr.io/componentized/sockets/latch-deny-connect-unless-lookup-address:0.1.0_debug@sha256:a259ec4f884c967f1a65198b9a8cdb61d8c4c3482c8ff6cf274e1ff835ef5238 diff --git a/components/dep-componentized-sockets-latch-deny-udp/README.md b/components/dep-componentized-sockets-latch-deny-udp/README.md new file mode 100644 index 0000000..72ce368 --- /dev/null +++ b/components/dep-componentized-sockets-latch-deny-udp/README.md @@ -0,0 +1,3 @@ +# dep-componentized-sockets-latch-deny-udp + +From [componentized sockets](https://github.com/componentized/sockets): [`latch-deny-udp`](https://github.com/componentized/sockets/tree/main/components/latch-deny-udp), denies all udp operations, pulled from `ghcr.io/componentized/sockets/latch-deny-udp:0.1.0`. diff --git a/components/dep-componentized-sockets-latch-deny-udp/dep-componentized-sockets-latch-deny-udp.wkg b/components/dep-componentized-sockets-latch-deny-udp/dep-componentized-sockets-latch-deny-udp.wkg new file mode 100644 index 0000000..fca2993 --- /dev/null +++ b/components/dep-componentized-sockets-latch-deny-udp/dep-componentized-sockets-latch-deny-udp.wkg @@ -0,0 +1,2 @@ +ghcr.io/componentized/sockets/latch-deny-udp:0.1.0@sha256:9e80506dc0ab0f4f4bfa90b2b64c531c265941b40806de7a15fd99e582ccab0f +ghcr.io/componentized/sockets/latch-deny-udp:0.1.0_debug@sha256:1e3145f51ee9545de1aad8ea9e9cdc68d881ba5f5323dfb10605281a572e172a diff --git a/components/dep-componentized-sockets-latch-ip-name-lookup-glob/README.md b/components/dep-componentized-sockets-latch-ip-name-lookup-glob/README.md new file mode 100644 index 0000000..fe157b1 --- /dev/null +++ b/components/dep-componentized-sockets-latch-ip-name-lookup-glob/README.md @@ -0,0 +1,3 @@ +# dep-componentized-sockets-latch-ip-name-lookup-glob + +From [componentized sockets](https://github.com/componentized/sockets): [`latch-ip-name-lookup-glob`](https://github.com/componentized/sockets/tree/main/components/latch-ip-name-lookup-glob), grants or denies ip-name lookups by host name glob patterns, configured with wasi:config, pulled from `ghcr.io/componentized/sockets/latch-ip-name-lookup-glob:0.1.0`. diff --git a/components/dep-componentized-sockets-latch-ip-name-lookup-glob/dep-componentized-sockets-latch-ip-name-lookup-glob.wkg b/components/dep-componentized-sockets-latch-ip-name-lookup-glob/dep-componentized-sockets-latch-ip-name-lookup-glob.wkg new file mode 100644 index 0000000..2666786 --- /dev/null +++ b/components/dep-componentized-sockets-latch-ip-name-lookup-glob/dep-componentized-sockets-latch-ip-name-lookup-glob.wkg @@ -0,0 +1,2 @@ +ghcr.io/componentized/sockets/latch-ip-name-lookup-glob:0.1.0@sha256:88eb4ad40292aabbf39984930739d6204b0e262d20503d783856d303ea501a55 +ghcr.io/componentized/sockets/latch-ip-name-lookup-glob:0.1.0_debug@sha256:8cfc4356143038bcd6e9fea77b10d65b886ef7072c0b3f9c8fbaf09d336de2e9 diff --git a/components/dep-componentized-sockets-latch-n5/README.md b/components/dep-componentized-sockets-latch-n5/README.md new file mode 100644 index 0000000..af43b06 --- /dev/null +++ b/components/dep-componentized-sockets-latch-n5/README.md @@ -0,0 +1,3 @@ +# dep-componentized-sockets-latch-n5 + +From [componentized sockets](https://github.com/componentized/sockets): [`latch-n5`](https://github.com/componentized/sockets/tree/main/components/latch-n5), aggregates five latches, any of them can deny an operation, pulled from `ghcr.io/componentized/sockets/latch-n5:0.1.0`. diff --git a/components/dep-componentized-sockets-latch-n5/dep-componentized-sockets-latch-n5.wkg b/components/dep-componentized-sockets-latch-n5/dep-componentized-sockets-latch-n5.wkg new file mode 100644 index 0000000..810d08c --- /dev/null +++ b/components/dep-componentized-sockets-latch-n5/dep-componentized-sockets-latch-n5.wkg @@ -0,0 +1,2 @@ +ghcr.io/componentized/sockets/latch-n5:0.1.0@sha256:b4b14d3a82b477c46f9642237831f21a5f53ee41727f7228d7c1ce94e68c1e58 +ghcr.io/componentized/sockets/latch-n5:0.1.0_debug@sha256:1bf2947ef40c18f578b49aac2afb100a870c585a77d5d4fc679e78ffbc63e93d diff --git a/components/dep-cli/Cargo.toml b/components/internal-cli/Cargo.toml similarity index 92% rename from components/dep-cli/Cargo.toml rename to components/internal-cli/Cargo.toml index f37eaeb..70f7960 100644 --- a/components/dep-cli/Cargo.toml +++ b/components/internal-cli/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "dep-cli" +name = "internal-cli" version = "0.1.0" edition = "2024" license = "Apache-2.0" diff --git a/components/dep-cli/README.md b/components/internal-cli/README.md similarity index 97% rename from components/dep-cli/README.md rename to components/internal-cli/README.md index 90b5cdc..db20bb0 100644 --- a/components/dep-cli/README.md +++ b/components/internal-cli/README.md @@ -1,4 +1,4 @@ -# dep-cli +# internal-cli The Valkey CLI before the client components are composed in, see [`cli`](../cli/). diff --git a/components/dep-cli/src/lib.rs b/components/internal-cli/src/lib.rs similarity index 99% rename from components/dep-cli/src/lib.rs rename to components/internal-cli/src/lib.rs index ee590d3..7c15684 100644 --- a/components/dep-cli/src/lib.rs +++ b/components/internal-cli/src/lib.rs @@ -443,7 +443,7 @@ fn indent(prefix: &str, text: &str) -> String { } wit_bindgen::generate!({ - // `path` is relative to the manifest, `components/dep-cli/` + // `path` is relative to the manifest, `components/internal-cli/` path: "../wit", world: "cli", // the resp types shared by every component diff --git a/components/internal-tcp-connector-egress-config/Cargo.toml b/components/internal-tcp-connector-egress-config/Cargo.toml new file mode 100644 index 0000000..2c93fdb --- /dev/null +++ b/components/internal-tcp-connector-egress-config/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "internal-tcp-connector-egress-config" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/internal-tcp-connector-egress-config/README.md b/components/internal-tcp-connector-egress-config/README.md new file mode 100644 index 0000000..e381a30 --- /dev/null +++ b/components/internal-tcp-connector-egress-config/README.md @@ -0,0 +1,11 @@ +# internal-tcp-connector-egress-config + +The config of a [`latch-cidr-egress`](https://github.com/componentized/sockets/tree/main/components/latch-cidr-egress), from the config of a [`tcp-connector`](../tcp-connector/): outbound traffic is denied, except to the server's `port`, on any address. + +| Connector config | Latch config | +| --- | --- | +| `port=6380` | `default=deny`, `defer=:6380` | +| unset | `default=deny`, `defer=:6379`, the connector's default | +| not a port number | `default=deny` | + +See [`tcp-connector-latch`](../tcp-connector-latch/). diff --git a/components/internal-tcp-connector-egress-config/src/lib.rs b/components/internal-tcp-connector-egress-config/src/lib.rs new file mode 100644 index 0000000..bd19c2f --- /dev/null +++ b/components/internal-tcp-connector-egress-config/src/lib.rs @@ -0,0 +1,78 @@ +//! The config of a `latch-cidr-egress`, from the config of a `tcp-connector`: outbound traffic is +//! denied, except to the server's port. + +use componentized::valkey::tcp_connector_constants as keys; +use exports::wasi::config::store::{Error, Guest}; +use wasi::config::store; + +struct Component; + +impl Guest for Component { + fn get(key: String) -> Result, Error> { + Ok(config()? + .into_iter() + .find_map(|(name, value)| (name == key).then_some(value))) + } + + fn get_all() -> Result, Error> { + config() + } +} + +fn config() -> Result, Error> { + let port = store::get(&keys::port_key()).map_err(|error| match error { + store::Error::Upstream(message) => Error::Upstream(message), + store::Error::Io(message) => Error::Io(message), + })?; + let port = match port { + Some(port) => port.parse::().ok(), + None => Some(keys::port_default()), + }; + Ok(cidr_egress_config(port)) +} + +/// Deny every port, but the server's, on any address. Without a port, one that is not a port +/// number and fails the connector, every port is denied. +fn cidr_egress_config(port: Option) -> Vec<(String, String)> { + let mut config = vec![("default".to_string(), "deny".to_string())]; + if let Some(port) = port { + // a port without an address matches every address + // addresses are guarded by latch-deny-connect-unless-lookup-address + config.push(("defer".to_string(), format!(":{port}"))); + } + config +} + +#[cfg(test)] +mod tests { + use super::*; + + fn config(entries: &[(&str, &str)]) -> Vec<(String, String)> { + entries + .iter() + .map(|(key, value)| (key.to_string(), value.to_string())) + .collect() + } + + #[test] + fn defers_the_port() { + assert_eq!( + cidr_egress_config(Some(6380)), + config(&[("default", "deny"), ("defer", ":6380")]) + ); + } + + #[test] + fn denies_everything_without_a_port() { + assert_eq!(cidr_egress_config(None), config(&[("default", "deny")])); + } +} + +wit_bindgen::generate!({ + // `path` is relative to the manifest, `components/internal-tcp-connector-egress-config/` + path: "../wit", + world: "tcp-connector-config", + generate_all, +}); + +export!(Component); diff --git a/components/internal-tcp-connector-lookup-config/Cargo.toml b/components/internal-tcp-connector-lookup-config/Cargo.toml new file mode 100644 index 0000000..a0d6def --- /dev/null +++ b/components/internal-tcp-connector-lookup-config/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "internal-tcp-connector-lookup-config" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/internal-tcp-connector-lookup-config/README.md b/components/internal-tcp-connector-lookup-config/README.md new file mode 100644 index 0000000..65a5ce3 --- /dev/null +++ b/components/internal-tcp-connector-lookup-config/README.md @@ -0,0 +1,12 @@ +# internal-tcp-connector-lookup-config + +The config of a [`latch-ip-name-lookup-glob`](https://github.com/componentized/sockets/tree/main/components/latch-ip-name-lookup-glob), from the config of a [`tcp-connector`](../tcp-connector/): name lookups are denied, except for the server's `hostname`. + +| Connector config | Latch config | +| --- | --- | +| `hostname=valkey.example.com` | `default=deny`, `defer=valkey.example.com` | +| `hostname=127.0.0.1`, an IP address | `default=deny`, `defer=127.0.0.1` | +| unset | `default=deny`, `defer=127.0.0.1`, the connector's default | +| `hostname=*.example.com`, with glob characters | `default=deny` | + +The [`tcp`](../tcp/) component looks up an IP address too, wasi:sockets returns it as is, so the address is allowed like a resolved hostname. See [`tcp-connector-latch`](../tcp-connector-latch/). diff --git a/components/internal-tcp-connector-lookup-config/src/lib.rs b/components/internal-tcp-connector-lookup-config/src/lib.rs new file mode 100644 index 0000000..8f0945d --- /dev/null +++ b/components/internal-tcp-connector-lookup-config/src/lib.rs @@ -0,0 +1,90 @@ +//! The config of a `latch-ip-name-lookup-glob`, from the config of a `tcp-connector`: name lookups +//! are denied, except for the server's hostname. + +use componentized::valkey::tcp_connector_constants as keys; +use exports::wasi::config::store::{Error, Guest}; +use wasi::config::store; + +struct Component; + +impl Guest for Component { + fn get(key: String) -> Result, Error> { + Ok(config()? + .into_iter() + .find_map(|(name, value)| (name == key).then_some(value))) + } + + fn get_all() -> Result, Error> { + config() + } +} + +fn config() -> Result, Error> { + let hostname = store::get(&keys::hostname_key()).map_err(|error| match error { + store::Error::Upstream(message) => Error::Upstream(message), + store::Error::Io(message) => Error::Io(message), + })?; + Ok(lookup_glob_config( + hostname.unwrap_or_else(keys::hostname_default), + )) +} + +/// Deny every name, but the hostname, an IP address too, which the `tcp` component looks up as +/// well. A hostname with glob characters would match other names, it is no hostname, so every +/// name is denied. +fn lookup_glob_config(hostname: String) -> Vec<(String, String)> { + let mut config = vec![("default".to_string(), "deny".to_string())]; + if !hostname.contains(['*', '?', '[', ']']) { + config.push(("defer".to_string(), hostname)); + } + config +} + +#[cfg(test)] +mod tests { + use super::*; + + fn config(entries: &[(&str, &str)]) -> Vec<(String, String)> { + entries + .iter() + .map(|(key, value)| (key.to_string(), value.to_string())) + .collect() + } + + #[test] + fn defers_the_hostname() { + assert_eq!( + lookup_glob_config("valkey.example.com".to_string()), + config(&[("default", "deny"), ("defer", "valkey.example.com")]) + ); + } + + #[test] + fn defers_an_ip_address() { + assert_eq!( + lookup_glob_config("127.0.0.1".to_string()), + config(&[("default", "deny"), ("defer", "127.0.0.1")]) + ); + assert_eq!( + lookup_glob_config("::1".to_string()), + config(&[("default", "deny"), ("defer", "::1")]) + ); + } + + #[test] + fn denies_everything_for_a_pattern() { + let deny = config(&[("default", "deny")]); + assert_eq!(lookup_glob_config("*.example.com".to_string()), deny); + assert_eq!(lookup_glob_config("valkey?.example.com".to_string()), deny); + assert_eq!(lookup_glob_config("[a-z].example.com".to_string()), deny); + } +} + +wit_bindgen::generate!({ + // `path` is relative to the manifest, `components/internal-tcp-connector-lookup-config/` + path: "../wit", + world: "tcp-connector-config", + generate_all, +}); + +export!(Component); diff --git a/components/sample-http-incrementor/README.md b/components/sample-http-incrementor/README.md index 6aa63d3..ba351fc 100644 --- a/components/sample-http-incrementor/README.md +++ b/components/sample-http-incrementor/README.md @@ -1,6 +1,6 @@ # Sample HTTP incrementor -Sample component that exposes wasi:http incrementing a counter based on the http path. +Sample component that exposes `wasi:http` incrementing a counter based on the http path. Compose with the valkey client: diff --git a/components/tcp-connector-constants/README.md b/components/tcp-connector-constants/README.md new file mode 100644 index 0000000..ab93883 --- /dev/null +++ b/components/tcp-connector-constants/README.md @@ -0,0 +1,7 @@ +# tcp-connector-constants + +The keys of the `tcp-connector` config and their defaults, as constants. + +Exports `componentized:valkey/tcp-connector-constants`, for the [`tcp-connector`](../tcp-connector/) and the components that derive their config from it, like [`internal-tcp-connector-lookup-config`](../internal-tcp-connector-lookup-config/) and [`internal-tcp-connector-egress-config`](../internal-tcp-connector-egress-config/). + +Generated by [constants](https://github.com/componentized/constants) from the `@value` of each function in [`tcp-connector-constants.wit`](../../wit/tcp-connector-constants.wit). diff --git a/components/tcp-connector-constants/wit/tcp-connector-constants.constants.wit b/components/tcp-connector-constants/wit/tcp-connector-constants.constants.wit new file mode 100644 index 0000000..7fd9d2d --- /dev/null +++ b/components/tcp-connector-constants/wit/tcp-connector-constants.constants.wit @@ -0,0 +1,5 @@ +package componentized:valkey-constants; + +world tcp-connector-constants { + export componentized:valkey/tcp-connector-constants@0.3.0-dev; +} diff --git a/components/tcp-connector-constants/wkg.lock b/components/tcp-connector-constants/wkg.lock new file mode 100644 index 0000000..2ad4aa8 --- /dev/null +++ b/components/tcp-connector-constants/wkg.lock @@ -0,0 +1,4 @@ +# This file is automatically generated. +# It is not intended for manual editing. +version = 1 +packages = [] diff --git a/components/tcp-connector-constants/wkg.toml b/components/tcp-connector-constants/wkg.toml new file mode 100644 index 0000000..d9669bf --- /dev/null +++ b/components/tcp-connector-constants/wkg.toml @@ -0,0 +1,2 @@ +[overrides] +"componentized:valkey" = { path = "../../wit" } diff --git a/components/tcp-connector-latch/README.md b/components/tcp-connector-latch/README.md new file mode 100644 index 0000000..52c4763 --- /dev/null +++ b/components/tcp-connector-latch/README.md @@ -0,0 +1,51 @@ +# tcp-connector-latch + +A `componentized:sockets` latch limiting network access to the Valkey server in the `tcp-connector` config, denying every other socket. + +A [componentized:sockets](https://github.com/componentized/sockets) [`gate`](../dep-componentized-sockets-gate/) asks the latch before each socket operation, and denies the operation if the latch does. The connector's config is read with wasi:config, see [`tcp-connector`](../tcp-connector/). + +Put the components that connect with the `tcp-connector` behind a gate with this latch, e.g. the [`tcp`](../tcp/) component. They can connect to the configured `hostname`, on the configured `port`, over TCP, and nothing else. + +The latch is a [`latch-n5`](../dep-componentized-sockets-latch-n5/), which denies an operation if any of these latches denies it: + +| Latch | Denies | +| -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| [`latch-deny-udp`](../dep-componentized-sockets-latch-deny-udp/) | all UDP, Valkey only uses TCP | +| [`latch-deny-bind`](../dep-componentized-sockets-latch-deny-bind/) | binding a socket to a local address, e.g. to listen for incoming connections, the client initiates all connections | +| [`latch-deny-connect-unless-lookup-address`](../dep-componentized-sockets-latch-deny-connect-unless-lookup-address/) | connections to an IP address, unless an allowed lookup returned it | +| [`latch-ip-name-lookup-glob`](../dep-componentized-sockets-latch-ip-name-lookup-glob/) | name lookups, except for the server's runtime configured `hostname` | +| [`latch-cidr-egress`](../dep-componentized-sockets-latch-cidr-egress/) | connections, except to the server's runtime configured `port` | + +The lookup and egress latches are configured from the connector's own config, with the connector's keys and defaults from [`tcp-connector-constants`](../tcp-connector-constants/): + +- [`internal-tcp-connector-lookup-config`](../internal-tcp-connector-lookup-config/): `hostname=valkey.example.com` becomes `default=deny` and `defer=valkey.example.com` +- [`internal-tcp-connector-egress-config`](../internal-tcp-connector-egress-config/): `port=6380` becomes `default=deny` and `defer=:6380`, any address on that port + +The server is configured in one place, and the latch follows it. + +A `hostname` that is an IP address, like the connector's default of `127.0.0.1`, is allowed too, to that address only. The `tcp` component looks up an IP address as well, wasi:sockets returns it as is, so the lookup of `127.0.0.1` is allowed and vouches for the address `127.0.0.1`. Another address, or a name that resolves to the same address, is still denied. + +The [`cli`](../cli/) is behind a gate with this latch, see also its test, [`test-tcp-connector-latch`](../test-tcp-connector-latch/). + +## Interfaces + +Imports: + +- `wasi:config/store@0.2.0-rc.1`: the connector's config, `hostname` and `port` +- `wasi:logging/logging@0.1.0-draft`: logs an invalid config +- `wasi:sockets/types@0.3.0`: the socket types of the operations being authorized + +Exports: + +- `componentized:sockets/latch@0.1.0`: the latch, for a gate + +The sockets components need the component model's `implements` and `threading` features, `-Wcomponent-model-implements -Wcomponent-model-threading`. + +## Custom latches + +The latches an environment needs and their config can differ. Attempting to use this latch without establishing the connection through the `tcp-connector` will likely fail. + +Client that need to open their own connections should build a custom latch from the same pieces, see [`tcp-connector-latch.wac`](./tcp-connector-latch.wac), and the other [latches](https://github.com/componentized/sockets#latches), or implement one by exporting the `componentized:sockets/latch` interface. For example: + +- `latch-cidr-egress` can also limit the addresses of the server's network, e.g. `defer=10.0.0.0/8:6379`. +- `latch-dry-run` and `latch-trace` let you roll out a latch before enforcing it. diff --git a/components/tcp-connector-latch/tcp-connector-latch.wac b/components/tcp-connector-latch/tcp-connector-latch.wac new file mode 100644 index 0000000..c8c8a7f --- /dev/null +++ b/components/tcp-connector-latch/tcp-connector-latch.wac @@ -0,0 +1,49 @@ +package componentized:valkey; + +// the latch, denying an operation when any of these latches does, wasi:logging and wasi:config +// are imported + +// the keys of the tcp-connector's config, and their defaults +let tcp-connector-constants = new local:tcp-connector-constants { + ... +}; +let lookup-config = new local:internal-tcp-connector-lookup-config { + "componentized:valkey/tcp-connector-constants@0.3.0-dev": tcp-connector-constants["componentized:valkey/tcp-connector-constants@0.3.0-dev"], + ... +}; +// lookups are denied, except for the server's hostname +let lookup-glob = new local:dep-componentized-sockets-latch-ip-name-lookup-glob { + "wasi:config/store@0.2.0-rc.1": lookup-config["wasi:config/store@0.2.0-rc.1"], + ... +}; +// connections are denied, except to the addresses of an allowed lookup +let connect = new local:dep-componentized-sockets-latch-deny-connect-unless-lookup-address { + ... +}; +let egress-config = new local:internal-tcp-connector-egress-config { + "componentized:valkey/tcp-connector-constants@0.3.0-dev": tcp-connector-constants["componentized:valkey/tcp-connector-constants@0.3.0-dev"], + ... +}; +// outbound traffic is denied, except to the server's port +let cidr-egress = new local:dep-componentized-sockets-latch-cidr-egress { + "wasi:config/store@0.2.0-rc.1": egress-config["wasi:config/store@0.2.0-rc.1"], + ... +}; +let udp = new local:dep-componentized-sockets-latch-deny-udp { + ... +}; +let bind = new local:dep-componentized-sockets-latch-deny-bind { + ... +}; +// asked in order until one denies, so the cheapest first, by component size as a proxy for the +// cost of asking, each still observes every final decision +let latch = new local:dep-componentized-sockets-latch-n5 { + latch0: udp["componentized:sockets/latch@0.1.0"], + latch1: bind["componentized:sockets/latch@0.1.0"], + latch2: connect["componentized:sockets/latch@0.1.0"], + latch3: lookup-glob["componentized:sockets/latch@0.1.0"], + latch4: cidr-egress["componentized:sockets/latch@0.1.0"], + ... +}; + +export latch["componentized:sockets/latch@0.1.0"]; diff --git a/components/tcp-connector/README.md b/components/tcp-connector/README.md index d7e6122..fe35b13 100644 --- a/components/tcp-connector/README.md +++ b/components/tcp-connector/README.md @@ -1,6 +1,6 @@ # tcp-connector -A connector for connections over TCP, to the server configured with wasi:config. +A connector for connections over TCP, to the server configured with `wasi:config`. | Key | Default | | | --- | --- | --- | @@ -11,4 +11,6 @@ A connector for connections over TCP, to the server configured with wasi:config. | `client-name` | | name of the connection, see `CLIENT SETNAME` | | `keep-alive-interval-ms` | | sends `PING` after this many milliseconds idle, disabled when unset or `0` | +The keys and their defaults are constants, from [`tcp-connector-constants`](../tcp-connector-constants/), for the components that derive their config from the connector's. + Compose with [`tcp`](../tcp/), e.g. to provide connections to [`as-keyvalue`](../as-keyvalue/). diff --git a/components/tcp-connector/src/lib.rs b/components/tcp-connector/src/lib.rs index b48c0fd..f2d9a88 100644 --- a/components/tcp-connector/src/lib.rs +++ b/components/tcp-connector/src/lib.rs @@ -5,40 +5,38 @@ use componentized::valkey::connection::{ConnectOpts, Connection, DurationMilliseconds, Error}; use componentized::valkey::tcp; +// the keys of the config and their defaults +use componentized::valkey::tcp_connector_constants as keys; use exports::componentized::valkey::connector::Guest; use wasi::config::store as config; -const HOSTNAME_KEY: &str = "hostname"; -const HOSTNAME_DEFAULT: &str = "127.0.0.1"; -const PORT_KEY: &str = "port"; -const PORT_DEFAULT: &str = "6379"; -const USERNAME_KEY: &str = "username"; -const USERNAME_DEFAULT: &str = "default"; -const PASSWORD_KEY: &str = "password"; -const CLIENT_NAME_KEY: &str = "client-name"; -const KEEP_ALIVE_INTERVAL_MS_KEY: &str = "keep-alive-interval-ms"; - struct Component; impl Guest for Component { async fn connect() -> Result { - let hostname = get(HOSTNAME_KEY)?.unwrap_or(HOSTNAME_DEFAULT.to_string()); - let port = get(PORT_KEY)?.unwrap_or(PORT_DEFAULT.to_string()); - let port: u16 = port - .parse() - .map_err(|_| Error::Client(format!("{PORT_KEY} must be a port number: {port}")))?; - let auth = match get(PASSWORD_KEY)? { + let hostname = get(&keys::hostname_key())?.unwrap_or_else(keys::hostname_default); + let port = match get(&keys::port_key())? { + Some(port) => port.parse().map_err(|_| { + Error::Client(format!( + "{} must be a port number: {port}", + keys::port_key() + )) + })?, + None => keys::port_default(), + }; + let auth = match get(&keys::password_key())? { Some(password) => { - let username = get(USERNAME_KEY)?.unwrap_or(USERNAME_DEFAULT.to_string()); + let username = get(&keys::username_key())?.unwrap_or_else(keys::username_default); Some((username, password)) } None => None, }; - let keep_alive_interval = match get(KEEP_ALIVE_INTERVAL_MS_KEY)? { + let keep_alive_interval = match get(&keys::keep_alive_interval_ms_key())? { Some(interval) => { let px = interval.parse().map_err(|_| { Error::Client(format!( - "{KEEP_ALIVE_INTERVAL_MS_KEY} must be a number of milliseconds: {interval}" + "{} must be a number of milliseconds: {interval}", + keys::keep_alive_interval_ms_key() )) })?; Some(DurationMilliseconds { px }) @@ -47,7 +45,7 @@ impl Guest for Component { }; let opts = ConnectOpts { auth, - client_name: get(CLIENT_NAME_KEY)?, + client_name: get(&keys::client_name_key())?, keep_alive_interval, }; tcp::connect(hostname, port, Some(opts)).await diff --git a/components/tcp/README.md b/components/tcp/README.md index 96ca6cd..7ef56dc 100644 --- a/components/tcp/README.md +++ b/components/tcp/README.md @@ -1,5 +1,7 @@ # tcp -Connections to a Valkey server over wasi:sockets TCP. +Connections to a Valkey server over `wasi:sockets` TCP. -Frames the bytes the socket carries into RESP values in each direction, and establishes a [`connection`](../connection/) over them. Compose with `connection` and [`resp`](../resp/). +Frames the bytes the socket carries into RESP values in each direction, and establishes a [`connection`](../connection/) over them. Compose with `connection`. + +The host is always looked up with wasi:sockets/ip-name-lookup, an IP address too, which wasi:sockets returns as is. So each address a socket connects to was returned by a lookup, e.g. for the [`tcp-connector-latch`](../tcp-connector-latch/). diff --git a/components/tcp/src/lib.rs b/components/tcp/src/lib.rs index 9b58393..ea33dbb 100644 --- a/components/tcp/src/lib.rs +++ b/components/tcp/src/lib.rs @@ -16,7 +16,6 @@ use std::cell::RefCell; use std::future::poll_fn; -use std::net::IpAddr; use std::pin::Pin; use std::rc::Rc; @@ -247,21 +246,15 @@ async fn connect_socket(host: &str, port: u16) -> Result { Err(Error::Closed(Some(reason))) } -/// The socket addresses of `host`, an IP address or a hostname to resolve. +/// The socket addresses of `host`, an IP address or a hostname. +/// +/// An IP address is resolved too, wasi:sockets returns it as is, so the lookup vouches for each +/// address a socket connects to, e.g. for a gate that only allows connecting to looked up +/// addresses. async fn resolve(host: &str, port: u16) -> Result, Error> { - let addresses = match host.parse() { - Ok(IpAddr::V4(address)) => { - let [a, b, c, d] = address.octets(); - vec![IpAddress::Ipv4((a, b, c, d))] - } - Ok(IpAddr::V6(address)) => { - let [a, b, c, d, e, f, g, h] = address.segments(); - vec![IpAddress::Ipv6((a, b, c, d, e, f, g, h))] - } - Err(_) => resolve_addresses(host.to_string()) - .await - .map_err(|error| lookup_error(host, error))?, - }; + let addresses = resolve_addresses(host.to_string()) + .await + .map_err(|error| lookup_error(host, error))?; Ok(addresses .into_iter() .map(|address| match address { diff --git a/components/dep-test-commands/Cargo.toml b/components/test-commands-suite/Cargo.toml similarity index 88% rename from components/dep-test-commands/Cargo.toml rename to components/test-commands-suite/Cargo.toml index 5f5a054..ca5d8e5 100644 --- a/components/dep-test-commands/Cargo.toml +++ b/components/test-commands-suite/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "dep-test-commands" +name = "test-commands-suite" version.workspace = true edition = "2024" license = "Apache-2.0" diff --git a/components/dep-test-commands/README.md b/components/test-commands-suite/README.md similarity index 94% rename from components/dep-test-commands/README.md rename to components/test-commands-suite/README.md index 16eaf89..4f7f5ee 100644 --- a/components/dep-test-commands/README.md +++ b/components/test-commands-suite/README.md @@ -1,4 +1,4 @@ -# dep-test-commands +# test-commands-suite The tests of the [`commands`](../commands/) component, a component that plays the server over the streams of a [`connection`](../connection/), checking the command each function sends and how it converts the reply. Composed as [`test-commands`](../test-commands/), and run by `cargo test -p commands`. diff --git a/components/dep-test-commands/src/lib.rs b/components/test-commands-suite/src/lib.rs similarity index 99% rename from components/dep-test-commands/src/lib.rs rename to components/test-commands-suite/src/lib.rs index 8a8ab15..27d1367 100644 --- a/components/dep-test-commands/src/lib.rs +++ b/components/test-commands-suite/src/lib.rs @@ -937,7 +937,7 @@ async fn with_valkey() -> Result<(), String> { } wit_bindgen::generate!({ - // `path` is relative to the manifest, `components/dep-test-commands/` + // `path` is relative to the manifest, `components/test-commands-suite/` path: "../wit", world: "test-commands", // the resp types shared by every component diff --git a/components/test-commands/README.md b/components/test-commands/README.md index 827bf88..94b4eed 100644 --- a/components/test-commands/README.md +++ b/components/test-commands/README.md @@ -1,3 +1,3 @@ # test-commands -[`dep-test-commands`](../dep-test-commands/) composed with [`commands`](../commands/), [`connection`](../connection/), and a [`tcp-connector`](../tcp-connector/) for the tests against a Valkey server, to test `commands`. +[`test-commands-suite`](../test-commands-suite/) composed with [`commands`](../commands/), [`connection`](../connection/), and a [`tcp-connector`](../tcp-connector/) for the tests against a Valkey server, to test `commands`. diff --git a/components/test-commands/test-commands.wac b/components/test-commands/test-commands.wac index c74e7cf..da48b96 100644 --- a/components/test-commands/test-commands.wac +++ b/components/test-commands/test-commands.wac @@ -11,12 +11,17 @@ let tcp = new local:tcp { "componentized:valkey/connection@0.3.0-dev": connection.connection, ... }; +// the keys of the tcp-connector's config, and their defaults +let tcp-connector-constants = new local:tcp-connector-constants { + ... +}; let connector = new local:tcp-connector { + "componentized:valkey/tcp-connector-constants@0.3.0-dev": tcp-connector-constants["componentized:valkey/tcp-connector-constants@0.3.0-dev"], "componentized:valkey/connection@0.3.0-dev": connection.connection, "componentized:valkey/tcp@0.3.0-dev": tcp.tcp, ... }; -let tests = new local:dep-test-commands { +let tests = new local:test-commands-suite { "componentized:valkey/connection@0.3.0-dev": connection.connection, "componentized:valkey/connector@0.3.0-dev": connector.connector, "componentized:valkey/commands@0.3.0-dev": commands.commands, diff --git a/components/test-stub-sockets/Cargo.toml b/components/test-stub-sockets/Cargo.toml new file mode 100644 index 0000000..89857e9 --- /dev/null +++ b/components/test-stub-sockets/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "test-stub-sockets" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/test-stub-sockets/README.md b/components/test-stub-sockets/README.md new file mode 100644 index 0000000..5764fbd --- /dev/null +++ b/components/test-stub-sockets/README.md @@ -0,0 +1,5 @@ +# test-stub-sockets + +`wasi:sockets` without a network, for tests of what a gate allows. + +Every socket operation succeeds, so an error can only come from a gate in front of it. Names resolve to fixed addresses: an IP address to itself, `localhost` to `127.0.0.1` and `::1`, any other name to `192.0.2.1`. Connections carry no data and listening sockets accept none. See [`test-tcp-connector-latch`](../test-tcp-connector-latch/). diff --git a/components/test-stub-sockets/src/lib.rs b/components/test-stub-sockets/src/lib.rs new file mode 100644 index 0000000..178d1ab --- /dev/null +++ b/components/test-stub-sockets/src/lib.rs @@ -0,0 +1,277 @@ +//! wasi:sockets without a network, for tests of what a gate allows: every operation succeeds, so an +//! error can only come from the gate. +//! +//! Names resolve to fixed addresses: an IP address to itself, `localhost` to `127.0.0.1` and `::1`, +//! any other name to `192.0.2.1`, an address for documentation. + +use std::cell::{Cell, RefCell}; +use std::net::IpAddr; + +use exports::wasi::sockets::ip_name_lookup::{self, ErrorCode as LookupErrorCode}; +use exports::wasi::sockets::types::{ + self, Duration, ErrorCode, GuestTcpSocket, GuestUdpSocket, IpAddress, IpAddressFamily, + IpSocketAddress, Ipv4SocketAddress, TcpSocket, UdpSocket, +}; +use wit_bindgen::{FutureReader, StreamReader}; + +/// The address names other than `localhost` resolve to. +const NAME_ADDRESS: IpAddress = IpAddress::Ipv4((192, 0, 2, 1)); + +struct Component; + +impl ip_name_lookup::Guest for Component { + async fn resolve_addresses(name: String) -> Result, LookupErrorCode> { + Ok(match name.parse::() { + Ok(address) => vec![ip_address(address)], + Err(_) if name == "localhost" => vec![ + IpAddress::Ipv4((127, 0, 0, 1)), + IpAddress::Ipv6((0, 0, 0, 0, 0, 0, 0, 1)), + ], + Err(_) => vec![NAME_ADDRESS], + }) + } +} + +fn ip_address(address: IpAddr) -> IpAddress { + match address { + IpAddr::V4(address) => { + let [a, b, c, d] = address.octets(); + IpAddress::Ipv4((a, b, c, d)) + } + IpAddr::V6(address) => { + let [a, b, c, d, e, f, g, h] = address.segments(); + IpAddress::Ipv6((a, b, c, d, e, f, g, h)) + } + } +} + +/// The address of a socket that was not bound, any address and port. +fn unspecified() -> IpSocketAddress { + IpSocketAddress::Ipv4(Ipv4SocketAddress { + port: 0, + address: (0, 0, 0, 0), + }) +} + +impl types::Guest for Component { + type TcpSocket = Tcp; + type UdpSocket = Udp; +} + +/// A socket that remembers its addresses, and succeeds at everything else. +struct Tcp { + family: IpAddressFamily, + local: Cell>, + remote: Cell>, + listening: Cell, +} + +impl GuestTcpSocket for Tcp { + fn create(address_family: IpAddressFamily) -> Result { + Ok(TcpSocket::new(Tcp { + family: address_family, + local: Cell::new(None), + remote: Cell::new(None), + listening: Cell::new(false), + })) + } + + fn bind(&self, local_address: IpSocketAddress) -> Result<(), ErrorCode> { + self.local.set(Some(local_address)); + Ok(()) + } + + async fn connect(&self, remote_address: IpSocketAddress) -> Result<(), ErrorCode> { + self.remote.set(Some(remote_address)); + Ok(()) + } + + /// No connections arrive. + fn listen(&self) -> Result, ErrorCode> { + self.listening.set(true); + let (_, connections) = wit_stream::new(); + Ok(connections) + } + + /// The data is dropped. + fn send(&self, _data: StreamReader) -> FutureReader> { + let (_, sent) = wit_future::new(|| Ok(())); + sent + } + + /// No data arrives. + fn receive(&self) -> (StreamReader, FutureReader>) { + let (_, data) = wit_stream::new(); + let (_, received) = wit_future::new(|| Ok(())); + (data, received) + } + + fn get_local_address(&self) -> Result { + Ok(self.local.get().unwrap_or_else(unspecified)) + } + + fn get_remote_address(&self) -> Result { + self.remote.get().ok_or(ErrorCode::InvalidState) + } + + fn get_is_listening(&self) -> bool { + self.listening.get() + } + + fn get_address_family(&self) -> IpAddressFamily { + self.family + } + + fn set_listen_backlog_size(&self, _value: u64) -> Result<(), ErrorCode> { + Ok(()) + } + + fn get_keep_alive_enabled(&self) -> Result { + Ok(false) + } + + fn set_keep_alive_enabled(&self, _value: bool) -> Result<(), ErrorCode> { + Ok(()) + } + + fn get_keep_alive_idle_time(&self) -> Result { + Ok(0) + } + + fn set_keep_alive_idle_time(&self, _value: Duration) -> Result<(), ErrorCode> { + Ok(()) + } + + fn get_keep_alive_interval(&self) -> Result { + Ok(0) + } + + fn set_keep_alive_interval(&self, _value: Duration) -> Result<(), ErrorCode> { + Ok(()) + } + + fn get_keep_alive_count(&self) -> Result { + Ok(0) + } + + fn set_keep_alive_count(&self, _value: u32) -> Result<(), ErrorCode> { + Ok(()) + } + + fn get_hop_limit(&self) -> Result { + Ok(64) + } + + fn set_hop_limit(&self, _value: u8) -> Result<(), ErrorCode> { + Ok(()) + } + + fn get_receive_buffer_size(&self) -> Result { + Ok(0) + } + + fn set_receive_buffer_size(&self, _value: u64) -> Result<(), ErrorCode> { + Ok(()) + } + + fn get_send_buffer_size(&self) -> Result { + Ok(0) + } + + fn set_send_buffer_size(&self, _value: u64) -> Result<(), ErrorCode> { + Ok(()) + } +} + +/// A socket that remembers its addresses, and succeeds at everything else. +struct Udp { + family: IpAddressFamily, + local: Cell>, + remote: RefCell>, +} + +impl GuestUdpSocket for Udp { + fn create(address_family: IpAddressFamily) -> Result { + Ok(UdpSocket::new(Udp { + family: address_family, + local: Cell::new(None), + remote: RefCell::new(None), + })) + } + + fn bind(&self, local_address: IpSocketAddress) -> Result<(), ErrorCode> { + self.local.set(Some(local_address)); + Ok(()) + } + + fn connect(&self, remote_address: IpSocketAddress) -> Result<(), ErrorCode> { + self.remote.replace(Some(remote_address)); + Ok(()) + } + + fn disconnect(&self) -> Result<(), ErrorCode> { + self.remote.replace(None); + Ok(()) + } + + /// The datagram is dropped. + async fn send( + &self, + _data: Vec, + _remote_address: Option, + ) -> Result<(), ErrorCode> { + Ok(()) + } + + /// No datagram arrives, there is no network to wait on. + async fn receive(&self) -> Result<(Vec, IpSocketAddress), ErrorCode> { + Err(ErrorCode::Other(Some( + "no datagrams without a network".to_string(), + ))) + } + + fn get_local_address(&self) -> Result { + Ok(self.local.get().unwrap_or_else(unspecified)) + } + + fn get_remote_address(&self) -> Result { + self.remote.borrow().ok_or(ErrorCode::InvalidState) + } + + fn get_address_family(&self) -> IpAddressFamily { + self.family + } + + fn get_unicast_hop_limit(&self) -> Result { + Ok(64) + } + + fn set_unicast_hop_limit(&self, _value: u8) -> Result<(), ErrorCode> { + Ok(()) + } + + fn get_receive_buffer_size(&self) -> Result { + Ok(0) + } + + fn set_receive_buffer_size(&self, _value: u64) -> Result<(), ErrorCode> { + Ok(()) + } + + fn get_send_buffer_size(&self) -> Result { + Ok(0) + } + + fn set_send_buffer_size(&self, _value: u64) -> Result<(), ErrorCode> { + Ok(()) + } +} + +wit_bindgen::generate!({ + // `path` is relative to the manifest, `components/test-stub-sockets/` + path: "../wit", + world: "test-stub-sockets", + generate_all, +}); + +export!(Component); diff --git a/components/test-tcp-connector-latch-suite/Cargo.toml b/components/test-tcp-connector-latch-suite/Cargo.toml new file mode 100644 index 0000000..42464e9 --- /dev/null +++ b/components/test-tcp-connector-latch-suite/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "test-tcp-connector-latch-suite" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/test-tcp-connector-latch-suite/README.md b/components/test-tcp-connector-latch-suite/README.md new file mode 100644 index 0000000..0a3c3ab --- /dev/null +++ b/components/test-tcp-connector-latch-suite/README.md @@ -0,0 +1,5 @@ +# test-tcp-connector-latch-suite + +The tests of the [`tcp-connector-latch`](../tcp-connector-latch/), run inside a component behind a gate with the latch. + +Each test looks up names, connects and binds with wasi:sockets, expecting each operation either allowed or denied with `access-denied`. The tests are named for the connector config they expect, `by hostname`, `by address` and `by default`. Composed as [`test-tcp-connector-latch`](../test-tcp-connector-latch/). diff --git a/components/test-tcp-connector-latch-suite/src/lib.rs b/components/test-tcp-connector-latch-suite/src/lib.rs new file mode 100644 index 0000000..494dacb --- /dev/null +++ b/components/test-tcp-connector-latch-suite/src/lib.rs @@ -0,0 +1,253 @@ +//! The tests of the `tcp-connector-latch`, run inside a component behind a gate with the latch, +//! over `test-stub-sockets`, wasi:sockets without a network. Every socket operation of the stub +//! succeeds, so a test that fails with `access-denied` was denied by the gate, and one that +//! succeeds was allowed. +//! +//! The tests are named for the connector config they expect, set by the test harness: +//! +//! - `by hostname ...`: `hostname=valkey.test`, `port=6380` +//! - `by address ...`: `hostname=127.0.0.1`, `port=6380` +//! - `by default ...`: neither, the connector's defaults, `127.0.0.1` and `6379` +//! +//! The stub resolves names other than `localhost` to `192.0.2.1`. + +use exports::componentized::valkey_components::test_suite::Guest; +use wasi::sockets::ip_name_lookup::{ErrorCode as LookupErrorCode, resolve_addresses}; +use wasi::sockets::types::{ + ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, + TcpSocket, UdpSocket, +}; + +struct Suite; + +/// The tests, by name, and the function running each. +macro_rules! suite { + ($($name:literal => $test:ident),* $(,)?) => { + const TESTS: &[&str] = &[$($name),*]; + + async fn run_test(name: &str) -> Result<(), String> { + match name { + $($name => $test().await,)* + _ => Err(format!("no test named {name:?}")), + } + } + }; +} + +suite! { + "by hostname looks up the hostname" => hostname_looks_up_hostname, + "by hostname connects to its addresses on the port" => hostname_connects, + "by hostname denies another port" => hostname_denies_another_port, + "by hostname denies looking up another name" => hostname_denies_another_name, + "by hostname denies looking up its address" => hostname_denies_looking_up_its_address, + "by hostname denies an address no lookup returned" => hostname_denies_address_without_lookup, + "by hostname denies binding" => denies_binding, + "by hostname denies udp" => denies_udp, + "by address looks up the address" => address_looks_up_address, + "by address connects to it on the port" => address_connects, + "by address denies another port" => address_denies_another_port, + "by address denies looking up a name" => address_denies_a_name, + "by address denies another address" => address_denies_another_address, + "by default connects to the default address and port" => default_connects, + "by default denies another port" => default_denies_another_port, +} + +impl Guest for Suite { + fn tests() -> Vec { + TESTS.iter().map(|name| name.to_string()).collect() + } + + async fn run(name: String) -> Result<(), String> { + run_test(&name).await + } +} + +const HOSTNAME: &str = "valkey.test"; +/// The address the stub resolves names to. +const HOSTNAME_ADDRESS: IpAddress = IpAddress::Ipv4((192, 0, 2, 1)); +const LOOPBACK: IpAddress = IpAddress::Ipv4((127, 0, 0, 1)); +const PORT: u16 = 6380; +const DEFAULT_PORT: u16 = 6379; + +async fn lookup(name: &str) -> Result, LookupErrorCode> { + resolve_addresses(name.to_string()).await +} + +async fn connect(address: IpAddress, port: u16) -> Result<(), ErrorCode> { + let address = socket_address(address, port); + let family = match address { + IpSocketAddress::Ipv4(_) => IpAddressFamily::Ipv4, + IpSocketAddress::Ipv6(_) => IpAddressFamily::Ipv6, + }; + TcpSocket::create(family)?.connect(address).await +} + +fn socket_address(address: IpAddress, port: u16) -> IpSocketAddress { + match address { + IpAddress::Ipv4(address) => IpSocketAddress::Ipv4(Ipv4SocketAddress { port, address }), + IpAddress::Ipv6(address) => IpSocketAddress::Ipv6(Ipv6SocketAddress { + port, + flow_info: 0, + address, + scope_id: 0, + }), + } +} + +/// Fails unless the lookup was allowed, returning the addresses. +async fn allowed_lookup(name: &str) -> Result, String> { + lookup(name) + .await + .map_err(|error| format!("looking up {name} failed with {error:?}, expected it allowed")) +} + +/// Fails unless the gate denied the lookup. +async fn denied_lookup(name: &str) -> Result<(), String> { + match lookup(name).await { + Err(LookupErrorCode::AccessDenied) => Ok(()), + Err(error) => Err(format!( + "looking up {name} failed with {error:?}, expected access-denied" + )), + Ok(addresses) => Err(format!( + "looking up {name} returned {addresses:?}, expected access-denied" + )), + } +} + +/// Fails unless the connection was allowed. +async fn allowed_connect(address: IpAddress, port: u16) -> Result<(), String> { + connect(address, port).await.map_err(|error| { + format!("connecting to {address:?}:{port} failed with {error:?}, expected it allowed") + }) +} + +/// Fails unless the gate denied the connection. +async fn denied_connect(address: IpAddress, port: u16) -> Result<(), String> { + match connect(address, port).await { + Err(ErrorCode::AccessDenied) => Ok(()), + Err(error) => Err(format!( + "connecting to {address:?}:{port} failed with {error:?}, expected access-denied" + )), + Ok(()) => Err(format!( + "connected to {address:?}:{port}, expected access-denied" + )), + } +} + +async fn hostname_looks_up_hostname() -> Result<(), String> { + let addresses = allowed_lookup(HOSTNAME).await?; + if addresses != [HOSTNAME_ADDRESS] { + return Err(format!("looking up {HOSTNAME} returned {addresses:?}")); + } + Ok(()) +} + +async fn hostname_connects() -> Result<(), String> { + for address in allowed_lookup(HOSTNAME).await? { + allowed_connect(address, PORT).await?; + } + Ok(()) +} + +async fn hostname_denies_another_port() -> Result<(), String> { + for address in allowed_lookup(HOSTNAME).await? { + denied_connect(address, DEFAULT_PORT).await?; + denied_connect(address, 22).await?; + } + Ok(()) +} + +async fn hostname_denies_another_name() -> Result<(), String> { + denied_lookup("example.com").await?; + denied_lookup("localhost").await?; + // a pattern is not a name + denied_lookup("*.test").await +} + +async fn hostname_denies_looking_up_its_address() -> Result<(), String> { + denied_lookup("192.0.2.1").await +} + +/// Connecting by hand, to an address no allowed lookup returned. +async fn hostname_denies_address_without_lookup() -> Result<(), String> { + denied_connect(IpAddress::Ipv4((198, 51, 100, 1)), PORT).await?; + denied_connect(LOOPBACK, PORT).await +} + +async fn denies_binding() -> Result<(), String> { + let socket = TcpSocket::create(IpAddressFamily::Ipv4) + .map_err(|error| format!("creating a socket failed with {error:?}"))?; + let local = socket_address(IpAddress::Ipv4((0, 0, 0, 0)), PORT); + match socket.bind(local) { + Err(ErrorCode::AccessDenied) => Ok(()), + Err(error) => Err(format!( + "binding failed with {error:?}, expected access-denied" + )), + Ok(()) => Err("bound, expected access-denied".to_string()), + } +} + +async fn denies_udp() -> Result<(), String> { + match UdpSocket::create(IpAddressFamily::Ipv4) { + Err(ErrorCode::AccessDenied) => Ok(()), + Err(error) => Err(format!( + "creating a udp socket failed with {error:?}, expected access-denied" + )), + Ok(_) => Err("created a udp socket, expected access-denied".to_string()), + } +} + +async fn address_looks_up_address() -> Result<(), String> { + let addresses = allowed_lookup("127.0.0.1").await?; + if addresses != [LOOPBACK] { + return Err(format!("looking up 127.0.0.1 returned {addresses:?}")); + } + Ok(()) +} + +async fn address_connects() -> Result<(), String> { + for address in allowed_lookup("127.0.0.1").await? { + allowed_connect(address, PORT).await?; + } + Ok(()) +} + +async fn address_denies_another_port() -> Result<(), String> { + allowed_lookup("127.0.0.1").await?; + denied_connect(LOOPBACK, DEFAULT_PORT).await +} + +async fn address_denies_a_name() -> Result<(), String> { + // localhost resolves to the same address, but is not the configured hostname + denied_lookup("localhost").await?; + denied_lookup(HOSTNAME).await +} + +async fn address_denies_another_address() -> Result<(), String> { + allowed_lookup("127.0.0.1").await?; + denied_connect(IpAddress::Ipv4((127, 0, 0, 2)), PORT).await?; + denied_connect(IpAddress::Ipv6((0, 0, 0, 0, 0, 0, 0, 1)), PORT).await +} + +async fn default_connects() -> Result<(), String> { + for address in allowed_lookup("127.0.0.1").await? { + allowed_connect(address, DEFAULT_PORT).await?; + } + Ok(()) +} + +async fn default_denies_another_port() -> Result<(), String> { + allowed_lookup("127.0.0.1").await?; + denied_connect(LOOPBACK, PORT).await +} + +wit_bindgen::generate!({ + // `path` is relative to the manifest, `components/test-tcp-connector-latch-suite/` + path: "../wit", + world: "test-tcp-connector-latch-suite", + // to compare the addresses a lookup returned + additional_derives: [PartialEq], + generate_all, +}); + +export!(Suite); diff --git a/components/test-tcp-connector-latch/Cargo.toml b/components/test-tcp-connector-latch/Cargo.toml new file mode 100644 index 0000000..8c090c7 --- /dev/null +++ b/components/test-tcp-connector-latch/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "test-tcp-connector-latch-tests" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" +publish = false + +# only tests, the component is composed from test-tcp-connector-latch.wac + +[dev-dependencies] +test-harness = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } diff --git a/components/test-tcp-connector-latch/README.md b/components/test-tcp-connector-latch/README.md new file mode 100644 index 0000000..227fc21 --- /dev/null +++ b/components/test-tcp-connector-latch/README.md @@ -0,0 +1,15 @@ +# test-tcp-connector-latch + +The tests of the [`tcp-connector-latch`](../tcp-connector-latch/), behind a [`gate`](../dep-componentized-sockets-gate/), over [`test-stub-sockets`](../test-stub-sockets/), wasi:sockets without a network. + +[`test-tcp-connector-latch-suite`](../test-tcp-connector-latch-suite/) gets its wasi:sockets from the gate. Every operation of the stub succeeds, so an operation that fails with `access-denied` was denied by the latch, and one that succeeds was allowed, with no server, DNS or firewall in the way. + +[`tests/latch.rs`](./tests/latch.rs) runs the suite with the test harness, once for each connector config, each in a new instance whose latch has seen no lookups: + +| Config | Allowed | Denied | +| --- | --- | --- | +| `hostname=valkey.test`, `port=6380` | looking up `valkey.test`, connecting to its address on `6380` | other ports, other names, looking up its address, addresses no lookup returned, binding, UDP | +| `hostname=127.0.0.1`, `port=6380` | looking up `127.0.0.1`, connecting to it on `6380` | other ports, names, even `localhost`, other addresses | +| neither, the connector's defaults | looking up `127.0.0.1`, connecting to it on `6379` | other ports | + +Run by `cargo test`, and `make test`. diff --git a/components/test-tcp-connector-latch/test-tcp-connector-latch.wac b/components/test-tcp-connector-latch/test-tcp-connector-latch.wac new file mode 100644 index 0000000..552e36b --- /dev/null +++ b/components/test-tcp-connector-latch/test-tcp-connector-latch.wac @@ -0,0 +1,34 @@ +package componentized:valkey-test; + +// wasi:sockets without a network, every operation succeeds, so only the gate can deny one +let sockets = new local:test-stub-sockets { + ... +}; + +let logging = new local:dep-componentized-logging-to-stderr { + ... +}; + +// the latch under test, reading the connector config from the host +let latch = new local:tcp-connector-latch { + "wasi:sockets/types@0.3.0": sockets["wasi:sockets/types@0.3.0"], + "wasi:logging/logging@0.1.0-draft": logging["wasi:logging/logging@0.1.0-draft"], + ... +}; + +let gate = new local:dep-componentized-sockets-gate { + "wasi:sockets/types@0.3.0": sockets["wasi:sockets/types@0.3.0"], + "wasi:sockets/ip-name-lookup@0.3.0": sockets["wasi:sockets/ip-name-lookup@0.3.0"], + "componentized:sockets/latch@0.1.0": latch["componentized:sockets/latch@0.1.0"], + "wasi:logging/logging@0.1.0-draft": logging["wasi:logging/logging@0.1.0-draft"], + ... +}; + +// the tests, their wasi:sockets from the gate +let suite = new local:test-tcp-connector-latch-suite { + "wasi:sockets/types@0.3.0": gate["wasi:sockets/types@0.3.0"], + "wasi:sockets/ip-name-lookup@0.3.0": gate["wasi:sockets/ip-name-lookup@0.3.0"], + ... +}; + +export suite...; diff --git a/components/test-tcp-connector-latch/tests/latch.rs b/components/test-tcp-connector-latch/tests/latch.rs new file mode 100644 index 0000000..7ca751d --- /dev/null +++ b/components/test-tcp-connector-latch/tests/latch.rs @@ -0,0 +1,27 @@ +//! The tests of the `tcp-connector-latch`, run inside `test-tcp-connector-latch-suite`, behind a +//! gate with the latch, over `test-stub-sockets`, composed as `test-tcp-connector-latch`. Each +//! config the suite's tests expect runs in a new instance, with a latch that has seen no lookups. + +use test_harness::{Harness, run_suite}; + +const COMPONENT: &str = "test-tcp-connector-latch"; + +#[tokio::test(flavor = "multi_thread")] +async fn by_hostname() -> wasmtime::Result<()> { + let config = [("hostname", "valkey.test"), ("port", "6380")]; + let harness = Harness::with_config(COMPONENT, &config).await?; + run_suite(harness, |test| test.starts_with("by hostname ")).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn by_address() -> wasmtime::Result<()> { + let config = [("hostname", "127.0.0.1"), ("port", "6380")]; + let harness = Harness::with_config(COMPONENT, &config).await?; + run_suite(harness, |test| test.starts_with("by address ")).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn by_default() -> wasmtime::Result<()> { + let harness = Harness::new(COMPONENT).await?; + run_suite(harness, |test| test.starts_with("by default ")).await +} diff --git a/components/test-tcp/test-tcp.wac b/components/test-tcp/test-tcp.wac index f071861..3fe30a5 100644 --- a/components/test-tcp/test-tcp.wac +++ b/components/test-tcp/test-tcp.wac @@ -7,7 +7,12 @@ let tcp = new local:tcp { "componentized:valkey/connection@0.3.0-dev": connection.connection, ... }; +// the keys of the tcp-connector's config, and their defaults +let tcp-connector-constants = new local:tcp-connector-constants { + ... +}; let connector = new local:tcp-connector { + "componentized:valkey/tcp-connector-constants@0.3.0-dev": tcp-connector-constants["componentized:valkey/tcp-connector-constants@0.3.0-dev"], "componentized:valkey/connection@0.3.0-dev": connection.connection, "componentized:valkey/tcp@0.3.0-dev": tcp.tcp, ... diff --git a/components/wit/worlds.wit b/components/wit/worlds.wit index d16fb31..e3a0e1f 100644 --- a/components/wit/worlds.wit +++ b/components/wit/worlds.wit @@ -22,10 +22,18 @@ world tcp { /// A connector for connections over TCP, to the server configured with wasi:config. world tcp-connector { import wasi:config/store@0.2.0-rc.1; + import componentized:valkey/tcp-connector-constants@0.3.0-dev; import componentized:valkey/tcp@0.3.0-dev; export componentized:valkey/connector@0.3.0-dev; } +world tcp-connector-config { + import wasi:config/store@0.2.0-rc.1; + import componentized:valkey/tcp-connector-constants@0.3.0-dev; + export wasi:config/store@0.2.0-rc.1; +} + + world as-keyvalue { import componentized:valkey/connector@0.3.0-dev; import componentized:valkey/commands@0.3.0-dev; @@ -49,6 +57,30 @@ world cli { export wasi:cli/run@0.3.0; } +/// Opens a TCP connection to a host and port, and reports whether it could, e.g. to show what a +/// gate allows. +world tcp-probe { + import wasi:sockets/types@0.3.0; + import wasi:sockets/ip-name-lookup@0.3.0; + import wasi:cli/environment@0.3.0; + import wasi:cli/stdout@0.3.0; + import wasi:cli/stderr@0.3.0; + export wasi:cli/run@0.3.0; +} + +/// wasi:sockets without a network, for tests of what a gate allows. +world test-stub-sockets { + export wasi:sockets/types@0.3.0; + export wasi:sockets/ip-name-lookup@0.3.0; +} + +/// The tests of the `tcp-connector-latch`, through a gate, over wasi:sockets without a network. +world test-tcp-connector-latch-suite { + import wasi:sockets/types@0.3.0; + import wasi:sockets/ip-name-lookup@0.3.0; + export test-suite; +} + /// A suite of tests run inside a component, by the test harness, `crates/test-harness`. interface test-suite { /// The names of the tests. Those that need a Valkey server on localhost:6379 start with diff --git a/crates/test-harness/src/lib.rs b/crates/test-harness/src/lib.rs index ba31752..065dabf 100644 --- a/crates/test-harness/src/lib.rs +++ b/crates/test-harness/src/lib.rs @@ -526,6 +526,9 @@ impl Harness { let mut config = Config::new(); config.wasm_component_model_map(true); + // for the sockets gate and latches + config.wasm_component_model_implements(true); + config.wasm_component_model_threading(true); let engine = Engine::new(&config)?; let component = Component::from_file(&engine, component_path(name)) .with_context(|| format!("failed to load {name}"))?; diff --git a/make/components.mk b/make/components.mk index 21e584b..a82da73 100644 --- a/make/components.mk +++ b/make/components.mk @@ -1,10 +1,14 @@ COMPONENTS_DIR := target/components +# the WIT packages of components/wit/deps, encoded where `wac compose` looks for a package a +# composition names without instantiating it, e.g. `import stderr: wasi:cli/stderr@0.3.0;` +WAC_DEPS_DIR := target/wac-deps + COMPONENTS = $(sort $(foreach file,$(wildcard $(addprefix components/*/,wit/*.constants.wit *.properties *.wac *.wkg Cargo.toml)),$(word 2,$(subst /, ,$(file))))) .PHONY: clean-components clean-components: clean-wit - rm -rf ${COMPONENTS_DIR} + rm -rf ${COMPONENTS_DIR} ${WAC_DEPS_DIR} .PHONY: components components: ${COMPONENTS_DIR}/interface.wasm $(foreach component,$(COMPONENTS),${COMPONENTS_DIR}/$(component)/$(component).wasm ${COMPONENTS_DIR}/$(component)/$(component).debug.wasm) @@ -35,11 +39,11 @@ else ifneq ($(wildcard components/$1/$1.wac),) # the local packages the composition instantiates, e.g. `new local:latch-n2 { ... }` WAC_DEPS_$1 := $$(shell grep -v '^\s*//' components/$1/$1.wac | grep -oE 'local:[a-z0-9-]+' | sed 's/^local://' | sort -u) -${COMPONENTS_DIR}/$1/$1.wasm: components/$1/$1.wac $$(foreach component,$$(WAC_DEPS_$1),$${COMPONENTS_DIR}/$$(component)/$$(component).wasm) ${COMPONENTS_DIR}/$1/README.md | $(call tool,wac-cli) - $(WAC) compose $$(foreach component,$$(WAC_DEPS_$1),-d local:$$(component)=$${COMPONENTS_DIR}/$$(component)/$$(component).wasm) -o ${COMPONENTS_DIR}/$1/$1.wasm components/$1/$1.wac +${COMPONENTS_DIR}/$1/$1.wasm: components/$1/$1.wac $$(foreach component,$$(WAC_DEPS_$1),$${COMPONENTS_DIR}/$$(component)/$$(component).wasm) ${COMPONENTS_DIR}/$1/README.md ${WAC_DEPS_DIR} | $(call tool,wac-cli) + $(WAC) compose --deps-dir ${WAC_DEPS_DIR} $$(foreach component,$$(WAC_DEPS_$1),-d local:$$(component)=$${COMPONENTS_DIR}/$$(component)/$$(component).wasm) -o ${COMPONENTS_DIR}/$1/$1.wasm components/$1/$1.wac -${COMPONENTS_DIR}/$1/$1.debug.wasm: components/$1/$1.wac $$(foreach component,$$(WAC_DEPS_$1),$${COMPONENTS_DIR}/$$(component)/$$(component).debug.wasm) ${COMPONENTS_DIR}/$1/README.md | $(call tool,wac-cli) - $(WAC) compose $$(foreach component,$$(WAC_DEPS_$1),-d local:$$(component)=$${COMPONENTS_DIR}/$$(component)/$$(component).debug.wasm) -o ${COMPONENTS_DIR}/$1/$1.debug.wasm components/$1/$1.wac +${COMPONENTS_DIR}/$1/$1.debug.wasm: components/$1/$1.wac $$(foreach component,$$(WAC_DEPS_$1),$${COMPONENTS_DIR}/$$(component)/$$(component).debug.wasm) ${COMPONENTS_DIR}/$1/README.md ${WAC_DEPS_DIR} | $(call tool,wac-cli) + $(WAC) compose --deps-dir ${WAC_DEPS_DIR} $$(foreach component,$$(WAC_DEPS_$1),-d local:$$(component)=$${COMPONENTS_DIR}/$$(component)/$$(component).debug.wasm) -o ${COMPONENTS_DIR}/$1/$1.debug.wasm components/$1/$1.wac else ifneq ($(wildcard components/$1/$1.wkg),) @@ -70,6 +74,9 @@ endef $(foreach component,$(COMPONENTS),$(eval $(call BUILD_COMPONENT,$(component)))) +${WAC_DEPS_DIR}: components/wit/deps scripts/wac-deps.sh | $(call tool,wasm-tools) + WASM_TOOLS=$(WASM_TOOLS) scripts/wac-deps.sh components/wit/deps ${WAC_DEPS_DIR} + ${COMPONENTS_DIR}/interface.wasm: wit/deps README.md | $(call tool,wkg) @mkdir -p ${COMPONENTS_DIR} $(WKG) build -o ${COMPONENTS_DIR}/interface.wasm diff --git a/make/publish.mk b/make/publish.mk index 2b3a472..87c2324 100644 --- a/make/publish.mk +++ b/make/publish.mk @@ -3,8 +3,10 @@ SIGN ?= true # append each published file and its image to this file, e.g. `gate.wasm ghcr.io/componentized/http/gate:0.1.0@sha256:...` PUBLISH_LOG ?= -# the files that can be published, e.g. gate.wasm, published from target/components/gate/gate.wasm -PUBLISH_FILES := interface.wasm $(foreach component,$(filter-out dep-% test-%,$(COMPONENTS)),$(component).wasm $(component).debug.wasm) +# the files that can be published, e.g. gate.wasm, published from target/components/gate/gate.wasm. +# Components are not published when they are pulled from elsewhere, `dep-`, built here for use by +# other components, `internal-`, or tests, `test-`. +PUBLISH_FILES := interface.wasm $(foreach component,$(filter-out dep-% internal-% test-%,$(COMPONENTS)),$(component).wasm $(component).debug.wasm) .PHONY: publish ## Publish each component in the target/components directory publish: $(addprefix publish-,$(PUBLISH_FILES)) diff --git a/scripts/wac-deps.sh b/scripts/wac-deps.sh new file mode 100755 index 0000000..7e10af8 --- /dev/null +++ b/scripts/wac-deps.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +# Encode each WIT package of a wit/deps directory where `wac compose --deps-dir` looks for it, so a +# composition can name the interfaces of a package it does not instantiate, e.g. +# `import stderr: wasi:cli/stderr@0.3.0;`. +# +# WASM_TOOLS= scripts/wac-deps.sh +# +# wac looks for a versioned package at `//.wasm`, e.g. +# +# wit/deps/wasi-cli-0.3.0 -> /wasi/cli/0.3.0.wasm +# +# Each package is encoded with the other packages as its dependencies. + +set -euo pipefail + +deps=$1 +out=$2 +wasm_tools=${WASM_TOOLS:-wasm-tools} + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +rm -rf "$out" +mkdir -p "$out" + +for package in "$deps"/*/; do + package=${package%/} + rm -rf "$work/package" + mkdir -p "$work/package/deps" + cp "$package"/*.wit "$work/package/" + for dep in "$deps"/*/; do + dep=${dep%/} + [[ "$dep" == "$package" ]] || cp -R "$dep" "$work/package/deps/" + done + "$wasm_tools" component wit "$work/package" --wasm -o "$work/package.wasm" + + # e.g. `package wasi:cli@0.3.0;` + id=$("$wasm_tools" component wit "$work/package.wasm" | sed -n 's/^package \(.*\);$/\1/p' | head -n 1) + namespace=${id%%:*} + name=${id#*:} + name=${name%%@*} + version=${id#*@} + mkdir -p "$out/$namespace/$name" + cp "$work/package.wasm" "$out/$namespace/$name/$version.wasm" +done diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 65632c5..fdf58f1 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -15,12 +15,14 @@ path = "lib.rs" # the binaries each tool installs, the Makefile runs each by path from a variable named after it, # e.g. `$(WASM_TOOLS)` for `wasm-tools` [package.metadata.bins] +componentized-constants-cli = ["constants"] wac-cli = ["wac"] wasm-tools = ["wasm-tools"] wasmtime-cli = ["wasmtime"] wkg = ["wkg"] [dependencies] +componentized-constants-cli = "=0.1.1" wac-cli = "=0.12.0" wasm-tools = "=1.261.0" wasmtime-cli = "=49.0.2" diff --git a/wit/tcp-connector-constants.wit b/wit/tcp-connector-constants.wit new file mode 100644 index 0000000..4b92e84 --- /dev/null +++ b/wit/tcp-connector-constants.wit @@ -0,0 +1,48 @@ +/// The keys of the `tcp-connector`'s wasi:config, and their defaults, for the components that derive +/// their config from it. The values are provided by the `tcp-connector-constants` component. +interface tcp-connector-constants { + /// The key of the server's hostname or IP address. + /// + /// @value "hostname" + hostname-key: func() -> string; + + /// The server's hostname or IP address without one. + /// + /// @value "127.0.0.1" + hostname-default: func() -> string; + + /// The key of the server's port. + /// + /// @value "port" + port-key: func() -> string; + + /// The server's port without one. + /// + /// @value 6379 + port-default: func() -> u16; + + /// The key of the user to authenticate as, with a password. + /// + /// @value "username" + username-key: func() -> string; + + /// The user to authenticate as without one. + /// + /// @value "default" + username-default: func() -> string; + + /// The key of the password, authenticating when set. + /// + /// @value "password" + password-key: func() -> string; + + /// The key of the connection's name, see `CLIENT SETNAME`. + /// + /// @value "client-name" + client-name-key: func() -> string; + + /// The key of the milliseconds idle before a `PING`, disabled when unset or `0`. + /// + /// @value "keep-alive-interval-ms" + keep-alive-interval-ms-key: func() -> string; +} diff --git a/wit/worlds.wit b/wit/worlds.wit index 69497a9..18125be 100644 --- a/wit/worlds.wit +++ b/wit/worlds.wit @@ -5,5 +5,6 @@ world imports { import connection; import connector; import tcp; + import tcp-connector-constants; import types; } From 2827878b3779b0d14ab1c075be748f745880cfc9 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Thu, 15 Oct 2026 20:00:35 -0400 Subject: [PATCH 34/37] Remove dedicated valkey server for CI The tests that need a server spin up an ephemeral container. Signed-off-by: Scott Andrews --- .github/workflows/bump-version.yaml | 298 ++++++++++++++-------------- .github/workflows/ci.yaml | 2 - README.md | 4 +- 3 files changed, 151 insertions(+), 153 deletions(-) diff --git a/.github/workflows/bump-version.yaml b/.github/workflows/bump-version.yaml index 8c153d6..7d97a1f 100644 --- a/.github/workflows/bump-version.yaml +++ b/.github/workflows/bump-version.yaml @@ -1,161 +1,159 @@ name: Bump version on: - workflow_dispatch: - inputs: - version: - description: The new version of the interface package and crates, e.g. 0.1.0 or 0.2.0-dev - required: true - type: string - default: "0.3.0-dev" # the current version, kept current by scripts/bump-version.sh + workflow_dispatch: + inputs: + version: + description: The new version of the interface package and crates, e.g. 0.1.0 or 0.2.0-dev + required: true + type: string + default: "0.3.0-dev" # the current version, kept current by scripts/bump-version.sh jobs: - # bumps the version with read only access, the changes are handed to the pull-request job as a - # patch so the third party actions used to build never run with write access - bump: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@v2 - - name: Install cargo binstall - uses: cargo-bins/cargo-binstall@main - - name: Install tools - run: | - make tools - make -s tools-path >> "${GITHUB_PATH}" - - name: Start Valkey service - run: docker run --rm --name valkey -d -p 6379:6379 valkey/valkey:9 - - name: Bump version - # also fetches the wit dependencies for the new version, and builds and tests the components - run: scripts/bump-version.sh "${VERSION}" - env: - VERSION: ${{ inputs.version }} - - name: Collect changes - run: | - git add --all - git diff --cached --binary > bump-version.patch - - name: Upload changes - uses: actions/upload-artifact@v7 - with: - name: bump-version.patch - path: bump-version.patch - if-no-files-found: error - retention-days: 1 + # bumps the version with read only access, the changes are handed to the pull-request job as a + # patch so the third party actions used to build never run with write access + bump: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions-rust-lang/setup-rust-toolchain@v2 + - name: Install cargo binstall + uses: cargo-bins/cargo-binstall@main + - name: Install tools + run: | + make tools + make -s tools-path >> "${GITHUB_PATH}" + - name: Bump version + # also fetches the wit dependencies for the new version, and builds and tests the components + run: scripts/bump-version.sh "${VERSION}" + env: + VERSION: ${{ inputs.version }} + - name: Collect changes + run: | + git add --all + git diff --cached --binary > bump-version.patch + - name: Upload changes + uses: actions/upload-artifact@v7 + with: + name: bump-version.patch + path: bump-version.patch + if-no-files-found: error + retention-days: 1 - # opens the pull request using only first party actions and the gh cli - pull-request: - needs: - - bump - runs-on: ubuntu-latest - # the branch and pull request are created with a token for the custodian GitHub App rather than - # the GITHUB_TOKEN, which can't change workflow files and doesn't trigger the CI workflow - permissions: - contents: read + # opens the pull request using only first party actions and the gh cli + pull-request: + needs: + - bump + runs-on: ubuntu-latest + # the branch and pull request are created with a token for the custodian GitHub App rather than + # the GITHUB_TOKEN, which can't change workflow files and doesn't trigger the CI workflow + permissions: + contents: read + env: + VERSION: ${{ inputs.version }} + steps: + - name: Check custodian app credentials + run: | + if [ -z "${CLIENT_ID}" ] || [ -z "${PRIVATE_KEY}" ] ; then + echo "::error::the CUSTODIAN_CLIENT_ID and CUSTODIAN_PRIVATE_KEY secrets must be available to this repository, the private key of the custodian GitHub App is needed to create a token" + exit 1 + fi + env: + CLIENT_ID: ${{ secrets.CUSTODIAN_CLIENT_ID }} + PRIVATE_KEY: ${{ secrets.CUSTODIAN_PRIVATE_KEY }} + - name: Create custodian app token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ secrets.CUSTODIAN_CLIENT_ID }} + private-key: ${{ secrets.CUSTODIAN_PRIVATE_KEY }} + # only this repository, with only the permissions the bump needs + repositories: ${{ github.event.repository.name }} + permission-contents: write + permission-pull-requests: write + # the bump changes the default version in this workflow + permission-workflows: write + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Download changes + uses: actions/download-artifact@v8 + with: + name: bump-version.patch + path: ${{ runner.temp }} + - name: Read current version + # the checkout is before the bump, the crates' workspace version is the current version + run: | + current=$( sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"$/\1/p' Cargo.toml ) + echo "CURRENT_VERSION=${current}" >> "${GITHUB_ENV}" + - name: Commit changes + # the commit is created with the REST API, as the app's token can't push. The patch is applied + # locally only to find the changed files and their modes. env: - VERSION: ${{ inputs.version }} - steps: - - name: Check custodian app credentials - run: | - if [ -z "${CLIENT_ID}" ] || [ -z "${PRIVATE_KEY}" ] ; then - echo "::error::the CUSTODIAN_CLIENT_ID and CUSTODIAN_PRIVATE_KEY secrets must be available to this repository, the private key of the custodian GitHub App is needed to create a token" - exit 1 - fi - env: - CLIENT_ID: ${{ secrets.CUSTODIAN_CLIENT_ID }} - PRIVATE_KEY: ${{ secrets.CUSTODIAN_PRIVATE_KEY }} - - name: Create custodian app token - id: app-token - uses: actions/create-github-app-token@v3 - with: - client-id: ${{ secrets.CUSTODIAN_CLIENT_ID }} - private-key: ${{ secrets.CUSTODIAN_PRIVATE_KEY }} - # only this repository, with only the permissions the bump needs - repositories: ${{ github.event.repository.name }} - permission-contents: write - permission-pull-requests: write - # the bump changes the default version in this workflow - permission-workflows: write - - uses: actions/checkout@v7 - with: - persist-credentials: false - - name: Download changes - uses: actions/download-artifact@v8 - with: - name: bump-version.patch - path: ${{ runner.temp }} - - name: Read current version - # the checkout is before the bump, the crates' workspace version is the current version - run: | - current=$( sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"$/\1/p' Cargo.toml ) - echo "CURRENT_VERSION=${current}" >> "${GITHUB_ENV}" - - name: Commit changes - # the commit is created with the REST API, as the app's token can't push. The patch is applied - # locally only to find the changed files and their modes. - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - APP_SLUG: ${{ steps.app-token.outputs.app-slug }} - run: | - branch="bump-version/${VERSION}" - api="repos/${GITHUB_REPOSITORY}" - base=$( git rev-parse HEAD ) - git apply --index "${RUNNER_TEMP}/bump-version.patch" + GH_TOKEN: ${{ steps.app-token.outputs.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + run: | + branch="bump-version/${VERSION}" + api="repos/${GITHUB_REPOSITORY}" + base=$( git rev-parse HEAD ) + git apply --index "${RUNNER_TEMP}/bump-version.patch" - # a blob for each changed file, or a null sha for a deleted file - entries="${RUNNER_TEMP}/tree-entries.json" - echo '[]' > "${entries}" - git diff --cached --no-renames --name-status "${base}" | while IFS=$'\t' read -r status path ; do - if [ "${status}" = "D" ] ; then - entry=$( jq -n --arg path "${path}" '{path: $path, mode: "100644", type: "blob", sha: null}' ) - else - mode=$( git ls-files --stage -- "${path}" | cut -d' ' -f1 ) - sha=$( base64 < "${path}" | tr -d '\n' | jq -Rs '{encoding: "base64", content: .}' | gh api --method POST "${api}/git/blobs" --input - --jq .sha ) - entry=$( jq -n --arg path "${path}" --arg mode "${mode}" --arg sha "${sha}" '{path: $path, mode: $mode, type: "blob", sha: $sha}' ) - fi - jq --argjson entry "${entry}" '. + [$entry]' "${entries}" > "${entries}.tmp" && mv "${entries}.tmp" "${entries}" - echo "${status} ${path}" - done - tree=$( jq --arg base "$( git rev-parse "${base}^{tree}" )" '{base_tree: $base, tree: .}' "${entries}" | gh api --method POST "${api}/git/trees" --input - --jq .sha ) + # a blob for each changed file, or a null sha for a deleted file + entries="${RUNNER_TEMP}/tree-entries.json" + echo '[]' > "${entries}" + git diff --cached --no-renames --name-status "${base}" | while IFS=$'\t' read -r status path ; do + if [ "${status}" = "D" ] ; then + entry=$( jq -n --arg path "${path}" '{path: $path, mode: "100644", type: "blob", sha: null}' ) + else + mode=$( git ls-files --stage -- "${path}" | cut -d' ' -f1 ) + sha=$( base64 < "${path}" | tr -d '\n' | jq -Rs '{encoding: "base64", content: .}' | gh api --method POST "${api}/git/blobs" --input - --jq .sha ) + entry=$( jq -n --arg path "${path}" --arg mode "${mode}" --arg sha "${sha}" '{path: $path, mode: $mode, type: "blob", sha: $sha}' ) + fi + jq --argjson entry "${entry}" '. + [$entry]' "${entries}" > "${entries}.tmp" && mv "${entries}.tmp" "${entries}" + echo "${status} ${path}" + done + tree=$( jq --arg base "$( git rev-parse "${base}^{tree}" )" '{base_tree: $base, tree: .}' "${entries}" | gh api --method POST "${api}/git/trees" --input - --jq .sha ) - # authored and signed off (DCO) by the user who triggered the workflow, with their GitHub - # noreply email so the commit is attributed to them without exposing their email address. - # Committed by the custodian app's bot, which made the commit on their behalf. The commit is - # unsigned, GitHub only signs commits it attributes entirely to the app. - name=$( gh api "users/${GITHUB_ACTOR}" --jq '.name // .login' ) - name="${name:-${GITHUB_ACTOR}}" - email="${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com" - bot="${APP_SLUG}[bot]" - bot_email="$( gh api "users/${bot}" --jq .id )+${bot}@users.noreply.github.com" - commit=$( jq -n \ - --arg message "$( printf 'Bump version from %s to %s\n\nSigned-off-by: %s <%s>' "${CURRENT_VERSION}" "${VERSION}" "${name}" "${email}" )" \ - --arg tree "${tree}" --arg parent "${base}" --arg name "${name}" --arg email "${email}" \ - --arg bot "${bot}" --arg bot_email "${bot_email}" \ - '{message: $message, tree: $tree, parents: [$parent], author: {name: $name, email: $email}, committer: {name: $bot, email: $bot_email}}' \ - | gh api --method POST "${api}/git/commits" --input - --jq .sha ) - echo "created commit ${commit}" + # authored and signed off (DCO) by the user who triggered the workflow, with their GitHub + # noreply email so the commit is attributed to them without exposing their email address. + # Committed by the custodian app's bot, which made the commit on their behalf. The commit is + # unsigned, GitHub only signs commits it attributes entirely to the app. + name=$( gh api "users/${GITHUB_ACTOR}" --jq '.name // .login' ) + name="${name:-${GITHUB_ACTOR}}" + email="${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com" + bot="${APP_SLUG}[bot]" + bot_email="$( gh api "users/${bot}" --jq .id )+${bot}@users.noreply.github.com" + commit=$( jq -n \ + --arg message "$( printf 'Bump version from %s to %s\n\nSigned-off-by: %s <%s>' "${CURRENT_VERSION}" "${VERSION}" "${name}" "${email}" )" \ + --arg tree "${tree}" --arg parent "${base}" --arg name "${name}" --arg email "${email}" \ + --arg bot "${bot}" --arg bot_email "${bot_email}" \ + '{message: $message, tree: $tree, parents: [$parent], author: {name: $name, email: $email}, committer: {name: $bot, email: $bot_email}}' \ + | gh api --method POST "${api}/git/commits" --input - --jq .sha ) + echo "created commit ${commit}" - # points the branch at the commit, replacing the branch left by an earlier run for the same version - if gh api "${api}/git/ref/heads/${branch}" --silent 2> /dev/null ; then - gh api --method PATCH "${api}/git/refs/heads/${branch}" -f sha="${commit}" -F force=true --silent - else - gh api --method POST "${api}/git/refs" -f ref="refs/heads/${branch}" -f sha="${commit}" --silent - fi - - name: Open pull request - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - run: | - branch="bump-version/${VERSION}" - if [ -n "$( gh pr list --head "${branch}" --state open --json number --jq '.[].number' )" ] ; then - echo "A pull request for ${branch} is already open, updated by the new commit" - exit 0 - fi - gh pr create \ - --base "${GITHUB_REF_NAME}" \ - --head "${branch}" \ - --title "Bump version from \`${CURRENT_VERSION}\` to \`${VERSION}\`" \ - --body "Bumps the wit package and crates from \`${CURRENT_VERSION}\` to \`${VERSION}\`. + # points the branch at the commit, replacing the branch left by an earlier run for the same version + if gh api "${api}/git/ref/heads/${branch}" --silent 2> /dev/null ; then + gh api --method PATCH "${api}/git/refs/heads/${branch}" -f sha="${commit}" -F force=true --silent + else + gh api --method POST "${api}/git/refs" -f ref="refs/heads/${branch}" -f sha="${commit}" --silent + fi + - name: Open pull request + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + branch="bump-version/${VERSION}" + if [ -n "$( gh pr list --head "${branch}" --state open --json number --jq '.[].number' )" ] ; then + echo "A pull request for ${branch} is already open, updated by the new commit" + exit 0 + fi + gh pr create \ + --base "${GITHUB_REF_NAME}" \ + --head "${branch}" \ + --title "Bump version from \`${CURRENT_VERSION}\` to \`${VERSION}\`" \ + --body "Bumps the wit package and crates from \`${CURRENT_VERSION}\` to \`${VERSION}\`. - Triggered by @${GITHUB_ACTOR} from the [Bump version](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) workflow." + Triggered by @${GITHUB_ACTOR} from the [Bump version](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) workflow." diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b6e068a..a84a9a1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -38,8 +38,6 @@ jobs: name: components.tar path: components.tar retention-days: 7 - - name: Start Valkey service - run: docker run --rm --name valkey -d -p 6379:6379 valkey/valkey:9 - name: Test run: make test - name: Test against Valkey diff --git a/README.md b/README.md index b36efdc..94abd01 100644 --- a/README.md +++ b/README.md @@ -74,12 +74,14 @@ The cli tools the build uses, [`constants`](https://github.com/componentized/con make test ``` -Tests the components without a Valkey server, playing the server themselves, then runs the cli against a Valkey server on `localhost:6379`. The tests against a Valkey server, ignored by `cargo test`, run with: +Tests the components without a Valkey server, playing the server themselves. The tests against a Valkey server, ignored by `cargo test`, run with: ```sh make test-valkey ``` +It runs Valkey in a new container, on a free port, removed afterwards. `VALKEY_VERSION` picks the version, e.g. `make test-valkey VALKEY_VERSION=8.1`. + ## Run Prereqs: From b50937f3e0a64bea7fc88f703f9d5cd056561059 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Thu, 15 Oct 2026 20:27:07 -0400 Subject: [PATCH 35/37] add missing since versions Signed-off-by: Scott Andrews --- wit/commands.wit | 388 ++++++++++++++++++++++++++++---- wit/connection.wit | 17 ++ wit/connector.wit | 4 + wit/tcp-connector-constants.wit | 10 + wit/tcp.wit | 6 + 5 files changed, 375 insertions(+), 50 deletions(-) diff --git a/wit/commands.wit b/wit/commands.wit index 5c38f04..5ea761a 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -8,7 +8,9 @@ /// client error before anything is sent. A list the command allows to be left out is an option, /// none rather than empty. interface commands { + @since(version = 0.3.0-dev) use connection.{connection}; + @since(version = 0.3.0-dev) use types.{ double, error, integer, resp-value, duration-seconds, duration-milliseconds, unix-time-seconds, unix-time-milliseconds, cursor, ttl-mode, hexpire-result, @@ -45,31 +47,32 @@ interface commands { string-or-bytes, restore-ttl, function-restore-policy }; - - // admin commands - /// The ACL categories, or with a category, the commands in it. /// /// ACL CAT [ category ] /// https://valkey.io/commands/acl-cat/ + @since(version = 0.3.0-dev) acl-cat: async func(connection: borrow, category: option) -> result, error>; /// Delete all the specified ACL users and terminate all the connections that are authenticated with such users. /// /// ACL DELUSER username [ username ... ] /// https://valkey.io/commands/acl-deluser/ + @since(version = 0.3.0-dev) acl-deluser: async func(connection: borrow, usernames: list) -> result; /// Whether the user may run the command, without running it. /// /// ACL DRYRUN username command [ arg [ arg ... ] ] /// https://valkey.io/commands/acl-dryrun/ + @since(version = 0.3.0-dev) acl-dryrun: async func(connection: borrow, username: string, command: list) -> result; /// Generates a random password. /// /// ACL GENPASS [ bits ] /// https://valkey.io/commands/acl-genpass/ + @since(version = 0.3.0-dev) acl-genpass: async func(connection: borrow, bits: option) -> result; /// The rules of the user, none when the user does not exist. @@ -80,18 +83,21 @@ interface commands { /// /// ACL GETUSER username /// https://valkey.io/commands/acl-getuser/ + @since(version = 0.3.0-dev) acl-getuser: async func(connection: borrow, username: string) -> result>, error>; /// The rules of every user, as written to an ACL file. /// /// ACL LIST /// https://valkey.io/commands/acl-list/ + @since(version = 0.3.0-dev) acl-list: async func(connection: borrow) -> result, error>; /// Replaces the users with those of the configured ACL file. /// /// ACL LOAD /// https://valkey.io/commands/acl-load/ + @since(version = 0.3.0-dev) acl-load: async func(connection: borrow) -> result<_, error>; /// The recent security events, commands or authentications the rules denied, up to the count, @@ -101,6 +107,7 @@ interface commands { /// /// ACL LOG [ count ] /// https://valkey.io/commands/acl-log/ + @since(version = 0.3.0-dev) acl-log: async func(connection: borrow, count: option) -> result, error>; /// Clears the security events. @@ -109,36 +116,42 @@ interface commands { /// /// ACL LOG RESET /// https://valkey.io/commands/acl-log/ + @since(version = 0.3.0-dev) acl-log-reset: async func(connection: borrow) -> result<_, error>; /// Saves the users to the configured ACL file. /// /// ACL SAVE /// https://valkey.io/commands/acl-save/ + @since(version = 0.3.0-dev) acl-save: async func(connection: borrow) -> result<_, error>; /// Create an ACL user with the specified rules or modify the rules of an existing user. /// /// ACL SETUSER username [ rule ] [ [ rule ] ... ] /// https://valkey.io/commands/acl-setuser/ + @since(version = 0.3.0-dev) acl-setuser: async func(connection: borrow, username: string, rules: option>) -> result<_, error>; /// The names of the users. /// /// ACL USERS /// https://valkey.io/commands/acl-users/ + @since(version = 0.3.0-dev) acl-users: async func(connection: borrow) -> result, error>; /// The name of the user the connection is authenticated as. /// /// ACL WHOAMI /// https://valkey.io/commands/acl-whoami/ + @since(version = 0.3.0-dev) acl-whoami: async func(connection: borrow) -> result; /// Starts rewriting the append-only file in the background, returning the server's message. /// /// BGREWRITEAOF /// https://valkey.io/commands/bgrewriteaof/ + @since(version = 0.3.0-dev) bgrewriteaof: async func(connection: borrow) -> result; /// Starts saving the databases to disk in the background, or schedules or cancels it, returning @@ -146,12 +159,14 @@ interface commands { /// /// BGSAVE [ SCHEDULE | CANCEL ] /// https://valkey.io/commands/bgsave/ + @since(version = 0.3.0-dev) bgsave: async func(connection: borrow, operation: option) -> result; /// Closes the connections that match every one of the filters, returning how many. /// /// CLIENT KILL filter [ filter ... ] /// https://valkey.io/commands/client-kill/ + @since(version = 0.3.0-dev) client-kill: async func(connection: borrow, filters: list) -> result; /// The connections that match every one of the filters, all of them without filters, each a @@ -159,18 +174,21 @@ interface commands { /// /// CLIENT LIST [ filter [ filter ... ] ] /// https://valkey.io/commands/client-list/ + @since(version = 0.3.0-dev) client-list: async func(connection: borrow, filters: option>) -> result>, error>; /// Whether the connection may be evicted to free memory, `ON` excludes it. /// /// CLIENT NO-EVICT 〈 ON | OFF 〉 /// https://valkey.io/commands/client-no-evict/ + @since(version = 0.3.0-dev) client-no-evict: async func(connection: borrow, enabled: bool) -> result<_, error>; /// Suspends running the commands of other connections, writes only with `WRITE`, for the time. /// /// CLIENT PAUSE timeout [ WRITE | ALL ] /// https://valkey.io/commands/client-pause/ + @since(version = 0.3.0-dev) client-pause: async func(connection: borrow, timeout: duration-milliseconds, mode: option) -> result<_, error>; /// Unblocks the connection with the ID from a blocking command, as if it timed out, or with an @@ -178,96 +196,112 @@ interface commands { /// /// CLIENT UNBLOCK client-id [ TIMEOUT | ERROR ] /// https://valkey.io/commands/client-unblock/ + @since(version = 0.3.0-dev) client-unblock: async func(connection: borrow, client-id: integer, unblock-type: option) -> result; /// Resumes running the commands of connections paused with `client-pause`. /// /// CLIENT UNPAUSE /// https://valkey.io/commands/client-unpause/ + @since(version = 0.3.0-dev) client-unpause: async func(connection: borrow) -> result<_, error>; /// Assigns the slots to the node. /// /// CLUSTER ADDSLOTS slot [ slot ... ] /// https://valkey.io/commands/cluster-addslots/ + @since(version = 0.3.0-dev) cluster-addslots: async func(connection: borrow, slots: list) -> result<_, error>; /// Assigns the ranges of slots, start and end both included, to the node. /// /// CLUSTER ADDSLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] /// https://valkey.io/commands/cluster-addslotsrange/ + @since(version = 0.3.0-dev) cluster-addslotsrange: async func(connection: borrow, ranges: list>) -> result<_, error>; /// Advances the node's config epoch, unless it is already the greatest in the cluster. /// /// CLUSTER BUMPEPOCH /// https://valkey.io/commands/cluster-bumpepoch/ + @since(version = 0.3.0-dev) cluster-bumpepoch: async func(connection: borrow) -> result; /// Cancels the slot migrations in progress. /// /// CLUSTER CANCELSLOTMIGRATIONS /// https://valkey.io/commands/cluster-cancelslotmigrations/ + @since(version = 0.3.0-dev) cluster-cancelslotmigrations: async func(connection: borrow) -> result<_, error>; /// The number of active failure reports for the node. /// /// CLUSTER COUNT-FAILURE-REPORTS node-id /// https://valkey.io/commands/cluster-count-failure-reports/ + @since(version = 0.3.0-dev) cluster-count-failure-reports: async func(connection: borrow, node-id: string) -> result; /// Unassigns the slots from the node. /// /// CLUSTER DELSLOTS slot [ slot ... ] /// https://valkey.io/commands/cluster-delslots/ + @since(version = 0.3.0-dev) cluster-delslots: async func(connection: borrow, slots: list) -> result<_, error>; /// Unassigns the ranges of slots, start and end both included, from the node. /// /// CLUSTER DELSLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] /// https://valkey.io/commands/cluster-delslotsrange/ + @since(version = 0.3.0-dev) cluster-delslotsrange: async func(connection: borrow, ranges: list>) -> result<_, error>; /// Fails over the primary of this replica, coordinated with it unless `FORCE` or `TAKEOVER`. /// /// CLUSTER FAILOVER [ FORCE | TAKEOVER ] /// https://valkey.io/commands/cluster-failover/ + @since(version = 0.3.0-dev) cluster-failover: async func(connection: borrow, mode: option) -> result<_, error>; /// Unassigns all the slots of the node. /// /// CLUSTER FLUSHSLOTS /// https://valkey.io/commands/cluster-flushslots/ + @since(version = 0.3.0-dev) cluster-flushslots: async func(connection: borrow) -> result<_, error>; /// Removes the node from the nodes this node knows of. /// /// CLUSTER FORGET node-id /// https://valkey.io/commands/cluster-forget/ + @since(version = 0.3.0-dev) cluster-forget: async func(connection: borrow, node-id: string) -> result<_, error>; /// The slot migrations in progress, and those recently finished. /// /// CLUSTER GETSLOTMIGRATIONS /// https://valkey.io/commands/cluster-getslotmigrations/ + @since(version = 0.3.0-dev) cluster-getslotmigrations: async func(connection: borrow) -> result, error>; /// Connects the node to another, adding it to the cluster. /// /// CLUSTER MEET ip port [ cluster-bus-port ] /// https://valkey.io/commands/cluster-meet/ + @since(version = 0.3.0-dev) cluster-meet: async func(connection: borrow, ip: string, port: u16, cluster-bus-port: option) -> result<_, error>; /// Migrates the ranges of slots from this node to other nodes. /// /// CLUSTER MIGRATESLOTS SLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] NODE node-id [ AUTH username password ] [ SLOTSRANGE ... ] /// https://valkey.io/commands/cluster-migrateslots/ + @since(version = 0.3.0-dev) cluster-migrateslots: async func(connection: borrow, migrations: list) -> result<_, error>; /// The replicas of the primary node, each a line in the format of CLUSTER NODES. /// /// CLUSTER REPLICAS node-id /// https://valkey.io/commands/cluster-replicas/ + @since(version = 0.3.0-dev) cluster-replicas: async func(connection: borrow, node-id: string) -> result, error>; /// Makes the node a replica of the primary node, or with none, detaches it from its primary, @@ -275,72 +309,84 @@ interface commands { /// /// CLUSTER REPLICATE 〈 node-id | NO ONE 〉 /// https://valkey.io/commands/cluster-replicate/ + @since(version = 0.3.0-dev) cluster-replicate: async func(connection: borrow, node-id: option) -> result<_, error>; /// Resets the node, forgetting the other nodes and its slots, `SOFT` without a mode. /// /// CLUSTER RESET [ HARD | SOFT ] /// https://valkey.io/commands/cluster-reset/ + @since(version = 0.3.0-dev) cluster-reset: async func(connection: borrow, mode: option) -> result<_, error>; /// Saves the node's cluster configuration to disk. /// /// CLUSTER SAVECONFIG /// https://valkey.io/commands/cluster-saveconfig/ + @since(version = 0.3.0-dev) cluster-saveconfig: async func(connection: borrow) -> result<_, error>; /// Sets the config epoch of a new node. /// /// CLUSTER SET-CONFIG-EPOCH config-epoch /// https://valkey.io/commands/cluster-set-config-epoch/ + @since(version = 0.3.0-dev) cluster-set-config-epoch: async func(connection: borrow, config-epoch: integer) -> result<_, error>; /// Changes the state of the slot on the node. /// /// CLUSTER SETSLOT slot 〈 IMPORTING node-id | MIGRATING node-id | NODE node-id | STABLE 〉 [ TIMEOUT timeout ] /// https://valkey.io/commands/cluster-setslot/ + @since(version = 0.3.0-dev) cluster-setslot: async func(connection: borrow, slot: u16, state: cluster-setslot-state, timeout: option) -> result<_, error>; /// The entries of the command log of the type, up to the count, all of them for -1. /// /// COMMANDLOG GET count 〈 slow | large-request | large-reply 〉 /// https://valkey.io/commands/commandlog-get/ + @since(version = 0.3.0-dev) commandlog-get: async func(connection: borrow, count: integer, %type: commandlog-type) -> result, error>; /// The number of entries in the command log of the type. /// /// COMMANDLOG LEN 〈 slow | large-request | large-reply 〉 /// https://valkey.io/commands/commandlog-len/ + @since(version = 0.3.0-dev) commandlog-len: async func(connection: borrow, %type: commandlog-type) -> result; /// Clears the command log of the type. /// /// COMMANDLOG RESET 〈 slow | large-request | large-reply 〉 /// https://valkey.io/commands/commandlog-reset/ + @since(version = 0.3.0-dev) commandlog-reset: async func(connection: borrow, %type: commandlog-type) -> result<_, error>; /// The values of the configuration parameters matching the glob-style patterns, by name. /// /// CONFIG GET parameter [ parameter ... ] /// https://valkey.io/commands/config-get/ + @since(version = 0.3.0-dev) config-get: async func(connection: borrow, parameters: list) -> result, error>; /// Resets the server's statistics, e.g. those of INFO. /// /// CONFIG RESETSTAT /// https://valkey.io/commands/config-resetstat/ + @since(version = 0.3.0-dev) config-resetstat: async func(connection: borrow) -> result<_, error>; /// Rewrites the configuration file with the current configuration. /// /// CONFIG REWRITE /// https://valkey.io/commands/config-rewrite/ + @since(version = 0.3.0-dev) config-rewrite: async func(connection: borrow) -> result<_, error>; /// Sets the configuration parameters to the values, all or none of them. /// /// CONFIG SET parameter value [ parameter value ... ] /// https://valkey.io/commands/config-set/ + @since(version = 0.3.0-dev) config-set: async func(connection: borrow, parameters-and-values: list>) -> result<_, error>; /// Fails over from this primary to a replica, the one at the target, or aborts a failover in @@ -348,72 +394,84 @@ interface commands { /// /// FAILOVER [ TO host port [ FORCE ] ] [ ABORT ] [ TIMEOUT milliseconds ] /// https://valkey.io/commands/failover/ + @since(version = 0.3.0-dev) failover: async func(connection: borrow, target: option, abort: bool, timeout: option) -> result<_, error>; /// When the databases were last saved to disk. /// /// LASTSAVE /// https://valkey.io/commands/lastsave/ + @since(version = 0.3.0-dev) lastsave: async func(connection: borrow) -> result; /// A report of the latency events, for people to read. /// /// LATENCY DOCTOR /// https://valkey.io/commands/latency-doctor/ + @since(version = 0.3.0-dev) latency-doctor: async func(connection: borrow) -> result; /// A graph of the latency of the event, as text. /// /// LATENCY GRAPH event /// https://valkey.io/commands/latency-graph/ + @since(version = 0.3.0-dev) latency-graph: async func(connection: borrow, event: string) -> result; /// The latency histogram of the commands, all of them without commands, by command name. /// /// LATENCY HISTOGRAM [ command [ command ... ] ] /// https://valkey.io/commands/latency-histogram/ + @since(version = 0.3.0-dev) latency-histogram: async func(connection: borrow, commands: option>) -> result, error>; /// The samples of the latency of the event, when each was taken and the latency. /// /// LATENCY HISTORY event /// https://valkey.io/commands/latency-history/ + @since(version = 0.3.0-dev) latency-history: async func(connection: borrow, event: string) -> result>, error>; /// The latest sample of the latency of each event. /// /// LATENCY LATEST /// https://valkey.io/commands/latency-latest/ + @since(version = 0.3.0-dev) latency-latest: async func(connection: borrow) -> result, error>; /// Clears the samples of the events, all of them without events, returning how many events. /// /// LATENCY RESET [ event [ event ... ] ] /// https://valkey.io/commands/latency-reset/ + @since(version = 0.3.0-dev) latency-reset: async func(connection: borrow, events: option>) -> result; /// The loaded modules. /// /// MODULE LIST /// https://valkey.io/commands/module-list/ + @since(version = 0.3.0-dev) module-list: async func(connection: borrow) -> result, error>; /// Loads the module at the path, with the arguments. /// /// MODULE LOAD path [ arg [ arg ... ] ] /// https://valkey.io/commands/module-load/ + @since(version = 0.3.0-dev) module-load: async func(connection: borrow, path: string, args: option>) -> result<_, error>; /// Loads the module at the path, with the configuration parameters and arguments. /// /// MODULE LOADEX path [ CONFIG name value [ CONFIG name value ... ] ] [ ARGS args [ args ... ] ] /// https://valkey.io/commands/module-loadex/ + @since(version = 0.3.0-dev) module-loadex: async func(connection: borrow, path: string, configs: option>>, args: option>) -> result<_, error>; /// Unloads the module, by name. /// /// MODULE UNLOAD name /// https://valkey.io/commands/module-unload/ + @since(version = 0.3.0-dev) module-unload: async func(connection: borrow, name: string) -> result<_, error>; /// Makes the server a replica of the primary at the host and port, or with none, a primary, @@ -421,18 +479,21 @@ interface commands { /// /// REPLICAOF 〈 host port | NO ONE 〉 /// https://valkey.io/commands/replicaof/ + @since(version = 0.3.0-dev) replicaof: async func(connection: borrow, primary: option>) -> result; /// The server's role in replication, and its state. /// /// ROLE /// https://valkey.io/commands/role/ + @since(version = 0.3.0-dev) role: async func(connection: borrow) -> result; /// Saves the databases to disk, blocking the server until done. /// /// SAVE /// https://valkey.io/commands/save/ + @since(version = 0.3.0-dev) save: async func(connection: borrow) -> result<_, error>; /// Stops the server, saving to disk as configured, or as the save mode says. @@ -444,6 +505,7 @@ interface commands { /// /// SHUTDOWN [ NOSAVE | SAVE ] [ NOW ] [ FORCE ] [ SAFE ] [ FAILOVER ] /// https://valkey.io/commands/shutdown/ + @since(version = 0.3.0-dev) shutdown: async func(connection: borrow, save: option, modifiers: shutdown-flags) -> result<_, error>; /// Aborts a shutdown in progress. @@ -452,34 +514,36 @@ interface commands { /// /// SHUTDOWN ABORT /// https://valkey.io/commands/shutdown/ + @since(version = 0.3.0-dev) shutdown-abort: async func(connection: borrow) -> result<_, error>; /// The entries of the slow log, up to the count, 10 without one, all of them for -1. /// /// SLOWLOG GET [ count ] /// https://valkey.io/commands/slowlog-get/ + @since(version = 0.3.0-dev) slowlog-get: async func(connection: borrow, count: option) -> result, error>; /// The number of entries in the slow log. /// /// SLOWLOG LEN /// https://valkey.io/commands/slowlog-len/ + @since(version = 0.3.0-dev) slowlog-len: async func(connection: borrow) -> result; /// Clears the slow log. /// /// SLOWLOG RESET /// https://valkey.io/commands/slowlog-reset/ + @since(version = 0.3.0-dev) slowlog-reset: async func(connection: borrow) -> result<_, error>; - - // bitmap commands - /// The number of bits set to 1 in the string stored at key, in the range, 0 when the key does /// not exist. /// /// BITCOUNT key [ start [ end [ BYTE | BIT ] ] ] /// https://valkey.io/commands/bitcount/ + @since(version = 0.3.0-dev) bitcount: async func(connection: borrow, key: string, range: option) -> result; /// Reads and writes integer fields of the string stored at key, returning the result of each @@ -487,12 +551,14 @@ interface commands { /// /// BITFIELD key [ GET encoding offset | [ OVERFLOW WRAP | SAT | FAIL ] 〈 SET encoding offset value | INCRBY encoding offset increment 〉 [ GET encoding offset | [ OVERFLOW WRAP | SAT | FAIL ] 〈 SET encoding offset value | INCRBY encoding offset increment 〉 ... ] ] /// https://valkey.io/commands/bitfield/ + @since(version = 0.3.0-dev) bitfield: async func(connection: borrow, key: string, operations: option>) -> result>, error>; /// Reads integer fields of the string stored at key, the read-only variant of `bitfield`. /// /// BITFIELD_RO key [ GET encoding offset [ GET encoding offset ... ] ] /// https://valkey.io/commands/bitfield_ro/ + @since(version = 0.3.0-dev) bitfield-ro: async func(connection: borrow, key: string, gets: option>) -> result, error>; /// Stores the bitwise operation of the strings stored at the source keys in the destination, @@ -500,6 +566,7 @@ interface commands { /// /// BITOP 〈 AND | OR | XOR | NOT 〉 destkey key [ key ... ] /// https://valkey.io/commands/bitop/ + @since(version = 0.3.0-dev) bitop: async func(connection: borrow, destination: string, operation: bitop-operation) -> result; /// The position of the first bit set to the bit in the string stored at key, in the range, @@ -507,6 +574,7 @@ interface commands { /// /// BITPOS key bit [ start [ end [ BYTE | BIT ] ] ] /// https://valkey.io/commands/bitpos/ + @since(version = 0.3.0-dev) bitpos: async func(connection: borrow, key: string, bit: bool, range: option) -> result, error>; /// The bit at the offset of the string stored at key, false beyond its end, or when the key @@ -514,6 +582,7 @@ interface commands { /// /// GETBIT key offset /// https://valkey.io/commands/getbit/ + @since(version = 0.3.0-dev) getbit: async func(connection: borrow, key: string, offset: u64) -> result; /// Sets the bit at the offset of the string stored at key, growing it as needed, returning the @@ -521,11 +590,9 @@ interface commands { /// /// SETBIT key offset value /// https://valkey.io/commands/setbit/ + @since(version = 0.3.0-dev) setbit: async func(connection: borrow, key: string, offset: u64, value: bool) -> result; - - // connection commands - /// Signals that the next command follows an ASK redirect, for a slot being imported. /// /// It applies to the next command on the connection, which may be another caller's when the @@ -533,12 +600,14 @@ interface commands { /// /// ASKING /// https://valkey.io/commands/asking/ + @since(version = 0.3.0-dev) asking: async func(connection: borrow) -> result<_, error>; /// Authenticates the current connection. /// /// AUTH [ username ] password /// https://valkey.io/commands/auth/ + @since(version = 0.3.0-dev) auth: async func(connection: borrow, username: option, password: string) -> result<_, error>; /// Whether the keys the next command reads are tracked, with tracking in `optin` or `optout` @@ -549,24 +618,28 @@ interface commands { /// /// CLIENT CACHING 〈 YES | NO 〉 /// https://valkey.io/commands/client-caching/ + @since(version = 0.3.0-dev) client-caching: async func(connection: borrow, enabled: bool) -> result<_, error>; /// Declares the capabilities the client supports, e.g. `redirect`. /// /// CLIENT CAPA capability [ capability ... ] /// https://valkey.io/commands/client-capa/ + @since(version = 0.3.0-dev) client-capa: async func(connection: borrow, capabilities: list) -> result<_, error>; /// The name of the connection, none when it has none. /// /// CLIENT GETNAME /// https://valkey.io/commands/client-getname/ + @since(version = 0.3.0-dev) client-getname: async func(connection: borrow) -> result, error>; /// The ID of the connection, unique until the server restarts. /// /// CLIENT ID /// https://valkey.io/commands/client-id/ + @since(version = 0.3.0-dev) client-id: async func(connection: borrow) -> result; /// Whether the connection is an import source, which may write while the server is in import @@ -574,12 +647,14 @@ interface commands { /// /// CLIENT IMPORT-SOURCE 〈 ON | OFF 〉 /// https://valkey.io/commands/client-import-source/ + @since(version = 0.3.0-dev) client-import-source: async func(connection: borrow, enabled: bool) -> result<_, error>; /// The fields of the connection, as CLIENT LIST has them, e.g. `id`, `addr` and `name`. /// /// CLIENT INFO /// https://valkey.io/commands/client-info/ + @since(version = 0.3.0-dev) client-info: async func(connection: borrow) -> result, error>; /// Whether the connection's commands leave the access time of the keys they read unchanged. @@ -588,18 +663,21 @@ interface commands { /// /// CLIENT NO-TOUCH 〈 ON | OFF 〉 /// https://valkey.io/commands/client-no-touch/ + @since(version = 0.3.0-dev) client-no-touch: async func(connection: borrow, enabled: bool) -> result<_, error>; /// Sets the name or version of the library of the connection, see CLIENT LIST. /// /// CLIENT SETINFO 〈 LIB-NAME libname | LIB-VER libver 〉 /// https://valkey.io/commands/client-setinfo/ + @since(version = 0.3.0-dev) client-setinfo: async func(connection: borrow, attr: client-setinfo-attr) -> result<_, error>; /// Sets the name of the connection. /// /// CLIENT SETNAME connection-name /// https://valkey.io/commands/client-setname/ + @since(version = 0.3.0-dev) client-setname: async func(connection: borrow, connection-name: string) -> result<_, error>; /// Turns tracking on with the options, or off with none, for server assisted client side @@ -609,24 +687,28 @@ interface commands { /// /// CLIENT TRACKING 〈 ON | OFF 〉 [ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN ] [ OPTOUT ] [ NOLOOP ] /// https://valkey.io/commands/client-tracking/ + @since(version = 0.3.0-dev) client-tracking: async func(connection: borrow, options: option) -> result<_, error>; /// The connection's tracking state. /// /// CLIENT TRACKINGINFO /// https://valkey.io/commands/client-trackinginfo/ + @since(version = 0.3.0-dev) client-trackinginfo: async func(connection: borrow) -> result; /// Every command of the server, a container followed by its subcommands. /// /// COMMAND /// https://valkey.io/commands/command/ + @since(version = 0.3.0-dev) command: async func(connection: borrow) -> result, error>; /// The number of commands of the server. /// /// COMMAND COUNT /// https://valkey.io/commands/command-count/ + @since(version = 0.3.0-dev) command-count: async func(connection: borrow) -> result; /// The documentation of the commands, all of them without names, a container followed by its @@ -634,12 +716,14 @@ interface commands { /// /// COMMAND DOCS [ command-name [ command-name ... ] ] /// https://valkey.io/commands/command-docs/ + @since(version = 0.3.0-dev) command-docs: async func(connection: borrow, command-names: option>) -> result, error>; /// The keys of the command, its name and arguments. /// /// COMMAND GETKEYS command [ arg [ arg ... ] ] /// https://valkey.io/commands/command-getkeys/ + @since(version = 0.3.0-dev) command-getkeys: async func(connection: borrow, command: list) -> result, error>; /// The keys of the command, its name and arguments, and how the command uses each, e.g. `RW` @@ -647,6 +731,7 @@ interface commands { /// /// COMMAND GETKEYSANDFLAGS command [ arg [ arg ... ] ] /// https://valkey.io/commands/command-getkeysandflags/ + @since(version = 0.3.0-dev) command-getkeysandflags: async func(connection: borrow, command: list) -> result>>, error>; /// The commands, all of them without names, a container followed by its subcommands, commands @@ -654,24 +739,28 @@ interface commands { /// /// COMMAND INFO [ command-name [ command-name ... ] ] /// https://valkey.io/commands/command-info/ + @since(version = 0.3.0-dev) command-info: async func(connection: borrow, command-names: option>) -> result, error>; /// The names of the commands of the server, those that match the filter with one. /// /// COMMAND LIST [ FILTERBY 〈 MODULE module-name | ACLCAT category | PATTERN pattern 〉 ] /// https://valkey.io/commands/command-list/ + @since(version = 0.3.0-dev) command-list: async func(connection: borrow, filter: option) -> result, error>; /// Returns the message. /// /// ECHO message /// https://valkey.io/commands/echo/ + @since(version = 0.3.0-dev) echo: async func(connection: borrow, message: string-or-bytes) -> result; /// Returns PONG if no argument is provided, otherwise return a copy of the argument as a bulk. /// /// PING [ message ] /// https://valkey.io/commands/ping/ + @since(version = 0.3.0-dev) ping: async func(connection: borrow, message: option) -> result<_, error>; /// Allows the connection to read from a replica of a cluster, for the slots of its primary. @@ -680,6 +769,7 @@ interface commands { /// /// READONLY /// https://valkey.io/commands/readonly/ + @since(version = 0.3.0-dev) readonly: async func(connection: borrow) -> result<_, error>; /// Disallows the connection to read from a replica of a cluster, the default. @@ -688,6 +778,7 @@ interface commands { /// /// READWRITE /// https://valkey.io/commands/readwrite/ + @since(version = 0.3.0-dev) readwrite: async func(connection: borrow) -> result<_, error>; /// Changes the database of the connection. @@ -696,6 +787,7 @@ interface commands { /// /// SELECT index /// https://valkey.io/commands/select/ + @since(version = 0.3.0-dev) select: async func(connection: borrow, index: integer) -> result<_, error>; /// Waits until the connection's writes reached the number of replicas, or the timeout passed, @@ -706,6 +798,7 @@ interface commands { /// /// WAIT numreplicas timeout /// https://valkey.io/commands/wait/ + @since(version = 0.3.0-dev) wait: async func(connection: borrow, numreplicas: integer, timeout: duration-milliseconds) -> result; /// Waits until the connection's writes are in the append-only file of the primary, and of the @@ -717,34 +810,36 @@ interface commands { /// /// WAITAOF numlocal numreplicas timeout /// https://valkey.io/commands/waitaof/ + @since(version = 0.3.0-dev) waitaof: async func(connection: borrow, numlocal: integer, numreplicas: integer, timeout: duration-milliseconds) -> result, error>; - - // geo commands - /// Adds the members at their positions to the geospatial index stored at key, or updates their /// positions, returning how many were added, or with `changed`, added or updated. /// /// GEOADD key [ NX | XX ] [ CH ] longitude latitude member [ longitude latitude member ... ] /// https://valkey.io/commands/geoadd/ + @since(version = 0.3.0-dev) geoadd: async func(connection: borrow, key: string, condition: option, changed: bool, coordinates-and-members: list>) -> result; /// The distance between the members, in meters or the unit, none when either is not a member. /// /// GEODIST key member1 member2 [ m | km | ft | mi ] /// https://valkey.io/commands/geodist/ + @since(version = 0.3.0-dev) geodist: async func(connection: borrow, key: string, member1: string-or-bytes, member2: string-or-bytes, unit: option) -> result, error>; /// The geohash of each of the members, none for a member that is not in the index. /// /// GEOHASH key [ member [ member ... ] ] /// https://valkey.io/commands/geohash/ + @since(version = 0.3.0-dev) geohash: async func(connection: borrow, key: string, members: option>) -> result>, error>; /// The position of each of the members, none for a member that is not in the index. /// /// GEOPOS key [ member [ member ... ] ] /// https://valkey.io/commands/geopos/ + @since(version = 0.3.0-dev) geopos: async func(connection: borrow, key: string, members: option>) -> result>, error>; /// The members of the geospatial index stored at key in the shape. @@ -753,6 +848,7 @@ interface commands { /// /// GEOSEARCH key [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] /// https://valkey.io/commands/geosearch/ + @since(version = 0.3.0-dev) geosearch: async func(connection: borrow, key: string, shape: geosearch-shape, order: option, count: option) -> result, error>; /// The members of the geospatial index stored at key in the shape, with the details asked for. @@ -761,6 +857,7 @@ interface commands { /// /// GEOSEARCH key [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] [ WITHCOORD ] [ WITHDIST ] [ WITHHASH ] /// https://valkey.io/commands/geosearch/ + @since(version = 0.3.0-dev) geosearch-with: async func(connection: borrow, key: string, shape: geosearch-shape, order: option, count: option, details: geosearch-with-flags) -> result, error>; /// Stores the members of the geospatial index stored at source in the shape, returning how @@ -769,33 +866,35 @@ interface commands { /// /// GEOSEARCHSTORE destination source [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] [ STOREDIST ] /// https://valkey.io/commands/geosearchstore/ + @since(version = 0.3.0-dev) geosearchstore: async func(connection: borrow, destination: string, source: string, shape: geosearchstore-shape, order: option, count: option, storedist: bool) -> result; - - // hash commands - /// Removes the specified fields from the hash stored at key. /// /// HDEL key field [ field ... ] /// https://valkey.io/commands/hdel/ + @since(version = 0.3.0-dev) hdel: async func(connection: borrow, key: string, fields: list) -> result; /// Returns if field is an existing field in the hash stored at key. /// /// HEXISTS key field /// https://valkey.io/commands/hexists/ + @since(version = 0.3.0-dev) hexists: async func(connection: borrow, key: string, field: string) -> result; /// Manipulate the expiration time for existing hash fields. /// /// HEXPIRE key seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpire/ + @since(version = 0.3.0-dev) hexpire: async func(connection: borrow, key: string, seconds: duration-seconds, mode: option, fields: list) -> result, error>; /// Manipulate the expiration time for existing hash fields. /// /// HEXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpireat/ + @since(version = 0.3.0-dev) hexpireat: async func(connection: borrow, key: string, seconds: unix-time-seconds, mode: option, fields: list) -> result, error>; /// When each of the hash fields expires, to the second. @@ -804,24 +903,28 @@ interface commands { /// /// HEXPIRETIME key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpiretime/ + @since(version = 0.3.0-dev) hexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; /// Returns the value associated with field in the hash stored at key. /// /// HGET key field /// https://valkey.io/commands/hget/ + @since(version = 0.3.0-dev) hget: async func(connection: borrow, key: string, field: string) -> result, error>; /// Returns all fields and values of the hash stored at key. /// /// HGETALL key /// https://valkey.io/commands/hgetall/ + @since(version = 0.3.0-dev) hgetall: async func(connection: borrow, key: string) -> result, error>; /// The values of the hash fields, none for a field that does not exist, deleting the fields. /// /// HGETDEL key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hgetdel/ + @since(version = 0.3.0-dev) hgetdel: async func(connection: borrow, key: string, fields: list) -> result>, error>; /// The values of the hash fields, none for a field that does not exist, optionally setting or @@ -829,60 +932,70 @@ interface commands { /// /// HGETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hgetex/ + @since(version = 0.3.0-dev) hgetex: async func(connection: borrow, key: string, ttl: option, fields: list) -> result>, error>; /// Increments the number stored at field in the hash stored at key by increment. /// /// HINCRBY key field increment /// https://valkey.io/commands/hincrby/ + @since(version = 0.3.0-dev) hincrby: async func(connection: borrow, key: string, field: string, increment: integer) -> result; /// Increment the specified field of a hash stored at key, and representing a floating point number, by the specified increment. /// /// HINCRBYFLOAT key field increment /// https://valkey.io/commands/hincrbyfloat/ + @since(version = 0.3.0-dev) hincrbyfloat: async func(connection: borrow, key: string, field: string, increment: double) -> result; /// Returns all field names in the hash stored at key. /// /// HKEYS key /// https://valkey.io/commands/hkeys/ + @since(version = 0.3.0-dev) hkeys: async func(connection: borrow, key: string) -> result, error>; /// Returns the number of fields contained in the hash stored at key. /// /// HLEN key /// https://valkey.io/commands/hlen/ + @since(version = 0.3.0-dev) hlen: async func(connection: borrow, key: string) -> result; /// Returns the values associated with the specified fields in the hash stored at key. /// /// HMGET key field [ field ... ] /// https://valkey.io/commands/hmget/ + @since(version = 0.3.0-dev) hmget: async func(connection: borrow, key: string, fields: list) -> result>, error>; /// Sets the specified fields to their respective values in the hash stored at key. /// /// HMSET key field value [ field value ... ] /// https://valkey.io/commands/hmset/ + @since(version = 0.3.0-dev) hmset: async func(connection: borrow, key: string, field-and-values: list>) -> result<_, error>; /// Removes the expiration of the hash fields. /// /// HPERSIST key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpersist/ + @since(version = 0.3.0-dev) hpersist: async func(connection: borrow, key: string, fields: list) -> result, error>; /// Manipulate the expiration time for existing hash fields, in milliseconds. /// /// HPEXPIRE key milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpire/ + @since(version = 0.3.0-dev) hpexpire: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, mode: option, fields: list) -> result, error>; /// Manipulate the expiration time for existing hash fields, as a Unix time in milliseconds. /// /// HPEXPIREAT key unix-time-milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpireat/ + @since(version = 0.3.0-dev) hpexpireat: async func(connection: borrow, key: string, milliseconds: unix-time-milliseconds, mode: option, fields: list) -> result, error>; /// When each of the hash fields expires, to the millisecond. @@ -891,6 +1004,7 @@ interface commands { /// /// HPEXPIRETIME key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpiretime/ + @since(version = 0.3.0-dev) hpexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; /// The remaining time to live of each of the hash fields, to the millisecond. @@ -899,6 +1013,7 @@ interface commands { /// /// HPTTL key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpttl/ + @since(version = 0.3.0-dev) hpttl: async func(connection: borrow, key: string, fields: list) -> result, error>; /// Random fields from the hash value stored at key, up to the count. @@ -910,6 +1025,7 @@ interface commands { /// /// HRANDFIELD key [ count ] /// https://valkey.io/commands/hrandfield/ + @since(version = 0.3.0-dev) hrandfield: async func(connection: borrow, key: string, count: option) -> result, error>; /// Random fields and their values from the hash value stored at key, up to the count. @@ -921,6 +1037,7 @@ interface commands { /// /// HRANDFIELD key count WITHVALUES /// https://valkey.io/commands/hrandfield/ + @since(version = 0.3.0-dev) hrandfield-withvalues: async func(connection: borrow, key: string, count: integer) -> result>, error>; /// Incrementally iterate over a collection of elements. @@ -929,6 +1046,7 @@ interface commands { /// /// HSCAN key cursor [ MATCH pattern ] [ COUNT count ] /// https://valkey.io/commands/hscan/ + @since(version = 0.3.0-dev) hscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, map>, error>; /// Incrementally iterate over a collection of keys. @@ -937,12 +1055,14 @@ interface commands { /// /// HSCAN key cursor [ MATCH pattern ] [ COUNT count ] NOVALUES /// https://valkey.io/commands/hscan/ + @since(version = 0.3.0-dev) hscan-novalues: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; /// Sets the specified fields to their respective values in the hash stored at key. /// /// HSET key field value [ field value ... ] /// https://valkey.io/commands/hset/ + @since(version = 0.3.0-dev) hset: async func(connection: borrow, key: string, field-and-values: list>) -> result; /// Sets the fields to their values in the hash stored at key, optionally setting their @@ -950,18 +1070,21 @@ interface commands { /// /// HSETEX key [ NX | XX ] [ FNX | FXX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] FIELDS numfields field value [ field value ... ] /// https://valkey.io/commands/hsetex/ + @since(version = 0.3.0-dev) hsetex: async func(connection: borrow, key: string, condition: option, field-condition: option, ttl: option, field-and-values: list>) -> result; /// Sets field in the hash stored at key to value, only if field does not yet exist. /// /// HSETNX key field value /// https://valkey.io/commands/hsetnx/ + @since(version = 0.3.0-dev) hsetnx: async func(connection: borrow, key: string, field: string, value: string-or-bytes) -> result; /// Returns the string length of the value associated with field in the hash stored at key. /// /// HSTRLEN key field /// https://valkey.io/commands/hstrlen/ + @since(version = 0.3.0-dev) hstrlen: async func(connection: borrow, key: string, field: string) -> result; /// The remaining time to live of each of the hash fields, to the second. @@ -970,17 +1093,16 @@ interface commands { /// /// HTTL key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/httl/ + @since(version = 0.3.0-dev) httl: async func(connection: borrow, key: string, fields: list) -> result, error>; /// All values in the hash stored at key. /// /// HVALS key /// https://valkey.io/commands/hvals/ + @since(version = 0.3.0-dev) hvals: async func(connection: borrow, key: string) -> result, error>; - - // hyperloglog commands - /// Adds the elements to the HyperLogLog stored at key, creating it when it does not exist, /// returning whether its estimate may have changed. /// @@ -988,6 +1110,7 @@ interface commands { /// /// PFADD key [ element [ element ... ] ] /// https://valkey.io/commands/pfadd/ + @since(version = 0.3.0-dev) pfadd: async func(connection: borrow, key: string, elements: option>) -> result; /// The approximate number of unique elements added to the HyperLogLogs, of their union for @@ -995,6 +1118,7 @@ interface commands { /// /// PFCOUNT key [ key ... ] /// https://valkey.io/commands/pfcount/ + @since(version = 0.3.0-dev) pfcount: async func(connection: borrow, keys: list) -> result; /// Merges the HyperLogLogs stored at the source keys into the one stored at destination, @@ -1004,19 +1128,15 @@ interface commands { /// /// PFMERGE destkey [ sourcekey [ sourcekey ... ] ] /// https://valkey.io/commands/pfmerge/ + @since(version = 0.3.0-dev) pfmerge: async func(connection: borrow, destination: string, sources: option>) -> result<_, error>; - // PFDEBUG and PFSELFTEST are internal commands, for debugging and testing Valkey itself, and - // are not implemented - - - // keyspace commands - /// Removes all the keys of the slot, in the mode, the server's `lazyfree-lazy-user-flush` /// without one. /// /// CLUSTER FLUSHSLOT slot [ ASYNC | SYNC ] /// https://valkey.io/commands/cluster-flushslot/ + @since(version = 0.3.0-dev) cluster-flushslot: async func(connection: borrow, slot: u16, mode: option) -> result<_, error>; /// Incrementally iterate over the keys of a cluster, slot by slot, or within the slot. @@ -1025,6 +1145,7 @@ interface commands { /// /// CLUSTERSCAN cursor [ MATCH match-pattern ] [ COUNT count ] [ TYPE type ] [ SLOT slot ] /// https://valkey.io/commands/clusterscan/ + @since(version = 0.3.0-dev) clusterscan: async func(connection: borrow, cursor: cursor, match: option, count: option, %type: option, slot: option) -> result, list>, error>; /// Copies the value of the source key to the destination key, in the database, or the current @@ -1032,18 +1153,21 @@ interface commands { /// /// COPY source destination [ DB destination-db ] [ REPLACE ] /// https://valkey.io/commands/copy/ + @since(version = 0.3.0-dev) copy: async func(connection: borrow, source: string, destination: string, destination-db: option, replace: bool) -> result; /// The number of keys in the current database. /// /// DBSIZE /// https://valkey.io/commands/dbsize/ + @since(version = 0.3.0-dev) dbsize: async func(connection: borrow) -> result; /// Removes the specified keys. /// /// DEL key [ key ... ] /// https://valkey.io/commands/del/ + @since(version = 0.3.0-dev) del: async func(connection: borrow, keys: list) -> result; /// The value of the key serialized in Valkey's own format, with its version and a checksum, see @@ -1051,24 +1175,28 @@ interface commands { /// /// DUMP key /// https://valkey.io/commands/dump/ + @since(version = 0.3.0-dev) dump: async func(connection: borrow, key: string) -> result, error>; /// If key exists. /// /// EXISTS key [ key ... ] /// https://valkey.io/commands/exists/ + @since(version = 0.3.0-dev) exists: async func(connection: borrow, keys: list) -> result; /// Set a timeout on key. /// /// EXPIRE key seconds [ NX | XX | GT | LT ] /// https://valkey.io/commands/expire/ + @since(version = 0.3.0-dev) expire: async func(connection: borrow, key: string, seconds: duration-seconds, mode: option) -> result; /// Set a timeout on key. /// /// EXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] /// https://valkey.io/commands/expireat/ + @since(version = 0.3.0-dev) expireat: async func(connection: borrow, key: string, seconds: unix-time-seconds, mode: option) -> result; /// When the key expires, to the second. @@ -1077,6 +1205,7 @@ interface commands { /// /// EXPIRETIME key /// https://valkey.io/commands/expiretime/ + @since(version = 0.3.0-dev) expiretime: async func(connection: borrow, key: string) -> result; /// Removes all the keys of all the databases, in the mode, the server's @@ -1084,6 +1213,7 @@ interface commands { /// /// FLUSHALL [ ASYNC | SYNC ] /// https://valkey.io/commands/flushall/ + @since(version = 0.3.0-dev) flushall: async func(connection: borrow, mode: option) -> result<_, error>; /// Removes all the keys of the current database, in the mode, the server's @@ -1091,12 +1221,14 @@ interface commands { /// /// FLUSHDB [ ASYNC | SYNC ] /// https://valkey.io/commands/flushdb/ + @since(version = 0.3.0-dev) flushdb: async func(connection: borrow, mode: option) -> result<_, error>; /// Returns all keys matching pattern. /// /// KEYS pattern /// https://valkey.io/commands/keys/ + @since(version = 0.3.0-dev) keys: async func(connection: borrow, pattern: string) -> result, error>; /// Moves the keys to the database of another instance, returning whether any were moved, false @@ -1104,6 +1236,7 @@ interface commands { /// /// MIGRATE host port "" destination-db timeout [ COPY ] [ REPLACE ] [ AUTH password | AUTH2 username password ] KEYS key [ key ... ] /// https://valkey.io/commands/migrate/ + @since(version = 0.3.0-dev) migrate: async func(connection: borrow, host: string, port: u16, destination-db: integer, timeout: duration-milliseconds, options: migrate-flags, auth: option, keys: list) -> result; /// Moves the key to the database, returning whether it was moved, false when it does not @@ -1111,6 +1244,7 @@ interface commands { /// /// MOVE key db [ REPLACE ] /// https://valkey.io/commands/move/ + @since(version = 0.3.0-dev) move: async func(connection: borrow, key: string, db: integer, replace: bool) -> result; /// The internal encoding of the value stored at key, e.g. `listpack`, none when the key does @@ -1118,6 +1252,7 @@ interface commands { /// /// OBJECT ENCODING key /// https://valkey.io/commands/object-encoding/ + @since(version = 0.3.0-dev) object-encoding: async func(connection: borrow, key: string) -> result, error>; /// The logarithmic access frequency counter of the key, none when it does not exist. @@ -1126,36 +1261,42 @@ interface commands { /// /// OBJECT FREQ key /// https://valkey.io/commands/object-freq/ + @since(version = 0.3.0-dev) object-freq: async func(connection: borrow, key: string) -> result, error>; /// The time since the key was last accessed, none when it does not exist. /// /// OBJECT IDLETIME key /// https://valkey.io/commands/object-idletime/ + @since(version = 0.3.0-dev) object-idletime: async func(connection: borrow, key: string) -> result, error>; /// The number of references to the value stored at key, none when the key does not exist. /// /// OBJECT REFCOUNT key /// https://valkey.io/commands/object-refcount/ + @since(version = 0.3.0-dev) object-refcount: async func(connection: borrow, key: string) -> result, error>; /// Removes the expiration of the key, returning whether it had one. /// /// PERSIST key /// https://valkey.io/commands/persist/ + @since(version = 0.3.0-dev) persist: async func(connection: borrow, key: string) -> result; /// Set a timeout on key, in milliseconds. /// /// PEXPIRE key milliseconds [ NX | XX | GT | LT ] /// https://valkey.io/commands/pexpire/ + @since(version = 0.3.0-dev) pexpire: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, mode: option) -> result; /// Set a timeout on key, as a Unix time in milliseconds. /// /// PEXPIREAT key unix-time-milliseconds [ NX | XX | GT | LT ] /// https://valkey.io/commands/pexpireat/ + @since(version = 0.3.0-dev) pexpireat: async func(connection: borrow, key: string, milliseconds: unix-time-milliseconds, mode: option) -> result; /// When the key expires, to the millisecond. @@ -1164,6 +1305,7 @@ interface commands { /// /// PEXPIRETIME key /// https://valkey.io/commands/pexpiretime/ + @since(version = 0.3.0-dev) pexpiretime: async func(connection: borrow, key: string) -> result; /// The remaining time to live of the key, to the millisecond. @@ -1172,24 +1314,28 @@ interface commands { /// /// PTTL key /// https://valkey.io/commands/pttl/ + @since(version = 0.3.0-dev) pttl: async func(connection: borrow, key: string) -> result; /// A random key of the current database, none when it is empty. /// /// RANDOMKEY /// https://valkey.io/commands/randomkey/ + @since(version = 0.3.0-dev) randomkey: async func(connection: borrow) -> result, error>; /// Renames the key, replacing the new key when it exists. /// /// RENAME key newkey /// https://valkey.io/commands/rename/ + @since(version = 0.3.0-dev) rename: async func(connection: borrow, key: string, newkey: string) -> result<_, error>; /// Renames the key, only when the new key does not exist, returning whether it was renamed. /// /// RENAMENX key newkey /// https://valkey.io/commands/renamenx/ + @since(version = 0.3.0-dev) renamenx: async func(connection: borrow, key: string, newkey: string) -> result; /// Creates the key from a value serialized with `dump`, without an expiration when `ttl` is @@ -1198,30 +1344,35 @@ interface commands { /// /// RESTORE key ttl serialized-value [ REPLACE ] [ ABSTTL ] [ IDLETIME seconds ] [ FREQ frequency ] /// https://valkey.io/commands/restore/ + @since(version = 0.3.0-dev) restore: async func(connection: borrow, key: string, ttl: option, serialized-value: bytes, replace: bool, idletime: option, freq: option) -> result<_, error>; /// `restore` on a node a key is migrated to, for a slot being imported, see `cluster-setslot`. /// /// RESTORE-ASKING key ttl serialized-value [ REPLACE ] [ ABSTTL ] [ IDLETIME seconds ] [ FREQ frequency ] /// https://valkey.io/commands/restore-asking/ + @since(version = 0.3.0-dev) restore-asking: async func(connection: borrow, key: string, ttl: option, serialized-value: bytes, replace: bool, idletime: option, freq: option) -> result<_, error>; /// Incrementally iterate over the keys of the current database. /// /// SCAN cursor [ MATCH pattern ] [ COUNT count ] [ TYPE type ] /// https://valkey.io/commands/scan/ + @since(version = 0.3.0-dev) scan: async func(connection: borrow, cursor: cursor, match: option, count: option, %type: option) -> result, list>, error>; /// Swaps the two databases, for connections using either. /// /// SWAPDB index1 index2 /// https://valkey.io/commands/swapdb/ + @since(version = 0.3.0-dev) swapdb: async func(connection: borrow, index1: integer, index2: integer) -> result<_, error>; /// Updates the last access time of the keys, returning how many exist. /// /// TOUCH key [ key ... ] /// https://valkey.io/commands/touch/ + @since(version = 0.3.0-dev) touch: async func(connection: borrow, keys: list) -> result; /// The remaining time to live of the key, to the second. @@ -1230,22 +1381,23 @@ interface commands { /// /// TTL key /// https://valkey.io/commands/ttl/ + @since(version = 0.3.0-dev) ttl: async func(connection: borrow, key: string) -> result; /// The type of the value stored at key, none when the key does not exist. /// /// TYPE key /// https://valkey.io/commands/type/ + @since(version = 0.3.0-dev) %type: async func(connection: borrow, key: string) -> result, error>; /// Removes the keys, freeing their memory in the background, returning how many existed. /// /// UNLINK key [ key ... ] /// https://valkey.io/commands/unlink/ + @since(version = 0.3.0-dev) unlink: async func(connection: borrow, keys: list) -> result; - // list commands - /// Moves an element from one end of the source list to an end of the destination list, waiting /// up to the timeout for the source to have one, returning it. /// @@ -1256,6 +1408,7 @@ interface commands { /// /// BLMOVE source destination 〈 LEFT | RIGHT 〉 〈 LEFT | RIGHT 〉 timeout /// https://valkey.io/commands/blmove/ + @since(version = 0.3.0-dev) blmove: async func(connection: borrow, source: string, destination: string, wherefrom: list-side, whereto: list-side, timeout: duration-milliseconds) -> result, error>; /// Removes elements from an end of the first non-empty list, waiting up to the timeout for one, @@ -1268,6 +1421,7 @@ interface commands { /// /// BLMPOP timeout numkeys key [ key ... ] 〈 LEFT | RIGHT 〉 [ COUNT count ] /// https://valkey.io/commands/blmpop/ + @since(version = 0.3.0-dev) blmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: list-side, count: option) -> result>>, error>; /// Removes the first element of the first non-empty list, waiting up to the timeout for one, @@ -1280,6 +1434,7 @@ interface commands { /// /// BLPOP key [ key ... ] timeout /// https://valkey.io/commands/blpop/ + @since(version = 0.3.0-dev) blpop: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; /// Removes the last element of the first non-empty list, waiting up to the timeout for one, @@ -1292,6 +1447,7 @@ interface commands { /// /// BRPOP key [ key ... ] timeout /// https://valkey.io/commands/brpop/ + @since(version = 0.3.0-dev) brpop: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; /// Moves the last element of the source list to the head of the destination list, waiting up @@ -1306,6 +1462,7 @@ interface commands { /// /// BRPOPLPUSH source destination timeout /// https://valkey.io/commands/brpoplpush/ + @since(version = 0.3.0-dev) brpoplpush: async func(connection: borrow, source: string, destination: string, timeout: duration-milliseconds) -> result, error>; /// The element at the index of the list stored at key, negative from the end, none when the @@ -1313,6 +1470,7 @@ interface commands { /// /// LINDEX key index /// https://valkey.io/commands/lindex/ + @since(version = 0.3.0-dev) lindex: async func(connection: borrow, key: string, index: integer) -> result, error>; /// Inserts the element before or after the first occurrence of the pivot in the list stored at @@ -1320,12 +1478,14 @@ interface commands { /// /// LINSERT key 〈 BEFORE | AFTER 〉 pivot element /// https://valkey.io/commands/linsert/ + @since(version = 0.3.0-dev) linsert: async func(connection: borrow, key: string, position: linsert-position, pivot: string-or-bytes, element: string-or-bytes) -> result; /// The length of the list stored at key, 0 when the key does not exist. /// /// LLEN key /// https://valkey.io/commands/llen/ + @since(version = 0.3.0-dev) llen: async func(connection: borrow, key: string) -> result; /// Moves an element from one end of the source list to an end of the destination list, @@ -1333,6 +1493,7 @@ interface commands { /// /// LMOVE source destination 〈 LEFT | RIGHT 〉 〈 LEFT | RIGHT 〉 /// https://valkey.io/commands/lmove/ + @since(version = 0.3.0-dev) lmove: async func(connection: borrow, source: string, destination: string, wherefrom: list-side, whereto: list-side) -> result, error>; /// Removes elements from an end of the first non-empty list, returning its key and the @@ -1340,6 +1501,7 @@ interface commands { /// /// LMPOP numkeys key [ key ... ] 〈 LEFT | RIGHT 〉 [ COUNT count ] /// https://valkey.io/commands/lmpop/ + @since(version = 0.3.0-dev) lmpop: async func(connection: borrow, keys: list, end: list-side, count: option) -> result>>, error>; /// Removes and returns the first elements of the list stored at key, up to the count, one @@ -1347,6 +1509,7 @@ interface commands { /// /// LPOP key [ count ] /// https://valkey.io/commands/lpop/ + @since(version = 0.3.0-dev) lpop: async func(connection: borrow, key: string, count: option) -> result, error>; /// The positions of the element in the list stored at key, up to the count, 0 for every match, @@ -1357,6 +1520,7 @@ interface commands { /// /// LPOS key element [ RANK rank ] [ COUNT num-matches ] [ MAXLEN len ] /// https://valkey.io/commands/lpos/ + @since(version = 0.3.0-dev) lpos: async func(connection: borrow, key: string, element: string-or-bytes, rank: option, count: option, maxlen: option) -> result, error>; /// Inserts the elements at the head of the list stored at key, creating it when it does not @@ -1364,6 +1528,7 @@ interface commands { /// /// LPUSH key element [ element ... ] /// https://valkey.io/commands/lpush/ + @since(version = 0.3.0-dev) lpush: async func(connection: borrow, key: string, elements: list) -> result; /// Inserts the elements at the head of the list stored at key, only when it exists, returning @@ -1371,6 +1536,7 @@ interface commands { /// /// LPUSHX key element [ element ... ] /// https://valkey.io/commands/lpushx/ + @since(version = 0.3.0-dev) lpushx: async func(connection: borrow, key: string, elements: list) -> result; /// The elements of the list stored at key from the start to the stop, both included, negative @@ -1378,6 +1544,7 @@ interface commands { /// /// LRANGE key start stop /// https://valkey.io/commands/lrange/ + @since(version = 0.3.0-dev) lrange: async func(connection: borrow, key: string, start: integer, stop: integer) -> result, error>; /// Removes occurrences of the element from the list stored at key, returning how many. @@ -1387,12 +1554,14 @@ interface commands { /// /// LREM key count element /// https://valkey.io/commands/lrem/ + @since(version = 0.3.0-dev) lrem: async func(connection: borrow, key: string, count: integer, element: string-or-bytes) -> result; /// Sets the element at the index of the list stored at key, negative from the end. /// /// LSET key index element /// https://valkey.io/commands/lset/ + @since(version = 0.3.0-dev) lset: async func(connection: borrow, key: string, index: integer, element: string-or-bytes) -> result<_, error>; /// Trims the list stored at key to the elements from the start to the stop, both included, @@ -1400,6 +1569,7 @@ interface commands { /// /// LTRIM key start stop /// https://valkey.io/commands/ltrim/ + @since(version = 0.3.0-dev) ltrim: async func(connection: borrow, key: string, start: integer, stop: integer) -> result<_, error>; /// Removes and returns the last elements of the list stored at key, up to the count, one @@ -1407,6 +1577,7 @@ interface commands { /// /// RPOP key [ count ] /// https://valkey.io/commands/rpop/ + @since(version = 0.3.0-dev) rpop: async func(connection: borrow, key: string, count: option) -> result, error>; /// Moves the last element of the source list to the head of the destination list, returning @@ -1416,6 +1587,7 @@ interface commands { /// /// RPOPLPUSH source destination /// https://valkey.io/commands/rpoplpush/ + @since(version = 0.3.0-dev) rpoplpush: async func(connection: borrow, source: string, destination: string) -> result, error>; /// Inserts the elements at the tail of the list stored at key, creating it when it does not @@ -1423,6 +1595,7 @@ interface commands { /// /// RPUSH key element [ element ... ] /// https://valkey.io/commands/rpush/ + @since(version = 0.3.0-dev) rpush: async func(connection: borrow, key: string, elements: list) -> result; /// Inserts the elements at the tail of the list stored at key, only when it exists, returning @@ -1430,56 +1603,58 @@ interface commands { /// /// RPUSHX key element [ element ... ] /// https://valkey.io/commands/rpushx/ + @since(version = 0.3.0-dev) rpushx: async func(connection: borrow, key: string, elements: list) -> result; - - // pubsub commands - /// Posts a message to the given channel. /// /// PUBLISH channel message /// https://valkey.io/commands/publish/ + @since(version = 0.3.0-dev) publish: async func(connection: borrow, channel: string, message: string-or-bytes) -> result; /// Lists the currently active channels. /// /// PUBSUB CHANNELS [ pattern ] /// https://valkey.io/commands/pubsub-channels/ + @since(version = 0.3.0-dev) pubsub-channels: async func(connection: borrow, pattern: option) -> result, error>; /// Returns the number of unique patterns that are subscribed to by clients. /// /// PUBSUB NUMPAT /// https://valkey.io/commands/pubsub-numpat/ + @since(version = 0.3.0-dev) pubsub-numpat: async func(connection: borrow) -> result; /// Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels. /// /// PUBSUB NUMSUB [ channel ] [ [ channel ] ... ] /// https://valkey.io/commands/pubsub-numsub/ + @since(version = 0.3.0-dev) pubsub-numsub: async func(connection: borrow, channels: option>) -> result, error>; /// Lists the currently active shard channels. /// /// PUBSUB SHARDCHANNELS [ pattern ] /// https://valkey.io/commands/pubsub-shardchannels/ + @since(version = 0.3.0-dev) pubsub-shardchannels: async func(connection: borrow, pattern: option) -> result, error>; /// Returns the number of subscribers for the specified shard channels. /// /// PUBSUB SHARDNUMSUB [ shardchannel [ shardchannel ... ] ] /// https://valkey.io/commands/pubsub-shardnumsub/ + @since(version = 0.3.0-dev) pubsub-shardnumsub: async func(connection: borrow, shardchannels: option>) -> result, error>; /// Posts a message to the given shard channel. /// /// SPUBLISH shardchannel message /// https://valkey.io/commands/spublish/ + @since(version = 0.3.0-dev) spublish: async func(connection: borrow, shardchannel: string, message: string-or-bytes) -> result; - - // scripting commands - /// Runs the Lua script, returning its result, the keys it accesses and its arguments are /// `KEYS` and `ARGV` in the script. /// @@ -1487,12 +1662,14 @@ interface commands { /// /// EVAL script numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/eval/ + @since(version = 0.3.0-dev) eval: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; /// Runs the Lua script, which may not write, returning its result, see `eval`. /// /// EVAL_RO script numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/eval_ro/ + @since(version = 0.3.0-dev) eval-ro: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; /// Runs the Lua script cached with the SHA1 digest, see `script-load`, returning its result, see @@ -1500,6 +1677,7 @@ interface commands { /// /// EVALSHA sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/evalsha/ + @since(version = 0.3.0-dev) evalsha: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; /// Runs the Lua script cached with the SHA1 digest, which may not write, returning its result, @@ -1507,6 +1685,7 @@ interface commands { /// /// EVALSHA_RO sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/evalsha_ro/ + @since(version = 0.3.0-dev) evalsha-ro: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; /// Calls the function, see `function-load`, returning its result, the keys it accesses and its @@ -1516,42 +1695,49 @@ interface commands { /// /// FCALL function numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/fcall/ + @since(version = 0.3.0-dev) fcall: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; /// Calls the function, which must be flagged `no-writes`, returning its result, see `fcall`. /// /// FCALL_RO function numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/fcall_ro/ + @since(version = 0.3.0-dev) fcall-ro: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; /// Deletes the library and its functions. /// /// FUNCTION DELETE library-name /// https://valkey.io/commands/function-delete/ + @since(version = 0.3.0-dev) function-delete: async func(connection: borrow, library-name: string) -> result<_, error>; /// The libraries, serialized in Valkey's own format, see `function-restore`. /// /// FUNCTION DUMP /// https://valkey.io/commands/function-dump/ + @since(version = 0.3.0-dev) function-dump: async func(connection: borrow) -> result; /// Deletes all the libraries, in the mode, the server's `lazyfree-lazy-user-flush` without one. /// /// FUNCTION FLUSH [ ASYNC | SYNC ] /// https://valkey.io/commands/function-flush/ + @since(version = 0.3.0-dev) function-flush: async func(connection: borrow, mode: option) -> result<_, error>; /// Kills the function that is running, as long as it has not written. /// /// FUNCTION KILL /// https://valkey.io/commands/function-kill/ + @since(version = 0.3.0-dev) function-kill: async func(connection: borrow) -> result<_, error>; /// The libraries, those with names matching the glob-style pattern with one. /// /// FUNCTION LIST [ LIBRARYNAME library-name-pattern ] [ WITHCODE ] /// https://valkey.io/commands/function-list/ + @since(version = 0.3.0-dev) function-list: async func(connection: borrow, library-name-pattern: option, withcode: bool) -> result, error>; /// Loads the library from its source code, replacing a library of the same name with @@ -1559,24 +1745,28 @@ interface commands { /// /// FUNCTION LOAD [ REPLACE ] function-code /// https://valkey.io/commands/function-load/ + @since(version = 0.3.0-dev) function-load: async func(connection: borrow, replace: bool, function-code: string) -> result; /// Loads the libraries serialized with `function-dump`, as `policy` says, by default `append`. /// /// FUNCTION RESTORE serialized-value [ FLUSH | APPEND | REPLACE ] /// https://valkey.io/commands/function-restore/ + @since(version = 0.3.0-dev) function-restore: async func(connection: borrow, serialized-value: bytes, policy: option) -> result<_, error>; /// The function that is running, and the libraries and functions of each engine. /// /// FUNCTION STATS /// https://valkey.io/commands/function-stats/ + @since(version = 0.3.0-dev) function-stats: async func(connection: borrow) -> result; /// Whether each of the scripts is cached, by SHA1 digest. /// /// SCRIPT EXISTS sha1 [ sha1 ... ] /// https://valkey.io/commands/script-exists/ + @since(version = 0.3.0-dev) script-exists: async func(connection: borrow, sha1s: list) -> result, error>; /// Removes all the scripts from the cache, in the mode, the server's `lazyfree-lazy-user-flush` @@ -1584,72 +1774,77 @@ interface commands { /// /// SCRIPT FLUSH [ ASYNC | SYNC ] /// https://valkey.io/commands/script-flush/ + @since(version = 0.3.0-dev) script-flush: async func(connection: borrow, mode: option) -> result<_, error>; /// Kills the script that is running, as long as it has not written. /// /// SCRIPT KILL /// https://valkey.io/commands/script-kill/ + @since(version = 0.3.0-dev) script-kill: async func(connection: borrow) -> result<_, error>; /// Caches the script without running it, returning its SHA1 digest, see `evalsha`. /// /// SCRIPT LOAD script /// https://valkey.io/commands/script-load/ + @since(version = 0.3.0-dev) script-load: async func(connection: borrow, script: string) -> result; /// The source of the cached script with the SHA1 digest. /// /// SCRIPT SHOW sha1 /// https://valkey.io/commands/script-show/ + @since(version = 0.3.0-dev) script-show: async func(connection: borrow, sha1: string) -> result; - // not implemented, from the @scripting category: - // SCRIPT DEBUG, the debugger is interactive, with a protocol of its own - - - // set commands - /// Adds the members to the set stored at key, returning how many were not already members. /// /// SADD key member [ member ... ] /// https://valkey.io/commands/sadd/ + @since(version = 0.3.0-dev) sadd: async func(connection: borrow, key: string, members: list) -> result; /// The number of members in the set stored at key. /// /// SCARD key /// https://valkey.io/commands/scard/ + @since(version = 0.3.0-dev) scard: async func(connection: borrow, key: string) -> result; /// The members of the first set that are not in any of the others. /// /// SDIFF key [ key ... ] /// https://valkey.io/commands/sdiff/ + @since(version = 0.3.0-dev) sdiff: async func(connection: borrow, keys: list) -> result, error>; /// Stores the members of the first set that are not in any of the others, returning how many. /// /// SDIFFSTORE destination key [ key ... ] /// https://valkey.io/commands/sdiffstore/ + @since(version = 0.3.0-dev) sdiffstore: async func(connection: borrow, destination: string, keys: list) -> result; /// The members in all of the sets. /// /// SINTER key [ key ... ] /// https://valkey.io/commands/sinter/ + @since(version = 0.3.0-dev) sinter: async func(connection: borrow, keys: list) -> result, error>; /// The number of members in all of the sets, counting stops at the limit. /// /// SINTERCARD numkeys key [ key ... ] [ LIMIT limit ] /// https://valkey.io/commands/sintercard/ + @since(version = 0.3.0-dev) sintercard: async func(connection: borrow, keys: list, limit: option) -> result; /// Stores the members in all of the sets, returning how many. /// /// SINTERSTORE destination key [ key ... ] /// https://valkey.io/commands/sinterstore/ + @since(version = 0.3.0-dev) sinterstore: async func(connection: borrow, destination: string, keys: list) -> result; /// Whether the member is in the set stored at key, false when the key does not exist. @@ -1658,6 +1853,7 @@ interface commands { /// /// SISMEMBER key member /// https://valkey.io/commands/sismember/ + @since(version = 0.3.0-dev) sismember: async func(connection: borrow, key: string, member: string-or-bytes) -> result; /// Whether the member is in the set stored at key, none when the key does not exist. @@ -1666,24 +1862,28 @@ interface commands { /// /// SISMEMBER key member XX /// https://valkey.io/commands/sismember/ + @since(version = 0.3.0-dev) sismember-xx: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; /// The members of the set stored at key. /// /// SMEMBERS key /// https://valkey.io/commands/smembers/ + @since(version = 0.3.0-dev) smembers: async func(connection: borrow, key: string) -> result, error>; /// Whether each of the members is in the set stored at key. /// /// SMISMEMBER key member [ member ... ] /// https://valkey.io/commands/smismember/ + @since(version = 0.3.0-dev) smismember: async func(connection: borrow, key: string, members: list) -> result, error>; /// Moves the member from one set to another, false when it was not in the source set. /// /// SMOVE source destination member /// https://valkey.io/commands/smove/ + @since(version = 0.3.0-dev) smove: async func(connection: borrow, source: string, destination: string, member: string-or-bytes) -> result; /// Removes and returns random members of the set stored at key, up to the count. @@ -1692,6 +1892,7 @@ interface commands { /// /// SPOP key [ count ] /// https://valkey.io/commands/spop/ + @since(version = 0.3.0-dev) spop: async func(connection: borrow, key: string, count: option) -> result, error>; /// Random members of the set stored at key, up to the count. @@ -1701,30 +1902,35 @@ interface commands { /// /// SRANDMEMBER key [ count ] /// https://valkey.io/commands/srandmember/ + @since(version = 0.3.0-dev) srandmember: async func(connection: borrow, key: string, count: option) -> result, error>; /// Removes the members from the set stored at key, returning how many were members. /// /// SREM key member [ member ... ] /// https://valkey.io/commands/srem/ + @since(version = 0.3.0-dev) srem: async func(connection: borrow, key: string, members: list) -> result; /// Incrementally iterate over the members of the set stored at key. /// /// SSCAN key cursor [ MATCH pattern ] [ COUNT count ] /// https://valkey.io/commands/sscan/ + @since(version = 0.3.0-dev) sscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; /// The members in any of the sets. /// /// SUNION key [ key ... ] /// https://valkey.io/commands/sunion/ + @since(version = 0.3.0-dev) sunion: async func(connection: borrow, keys: list) -> result, error>; /// Stores the members in any of the sets, returning how many. /// /// SUNIONSTORE destination key [ key ... ] /// https://valkey.io/commands/sunionstore/ + @since(version = 0.3.0-dev) sunionstore: async func(connection: borrow, destination: string, keys: list) -> result; /// Sorts the elements of the list, set or sorted set stored at key. @@ -1734,6 +1940,7 @@ interface commands { /// /// SORT key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] /// https://valkey.io/commands/sort/ + @since(version = 0.3.0-dev) sort: async func(connection: borrow, key: string, opts: option) -> result>>, error>; /// Sorts the elements of the list, set or sorted set stored at key, storing them as a list, @@ -1741,17 +1948,16 @@ interface commands { /// /// SORT key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] STORE destination /// https://valkey.io/commands/sort/ + @since(version = 0.3.0-dev) sort-store: async func(connection: borrow, key: string, opts: option, destination: string) -> result; /// The read-only variant of `sort`, e.g. for replicas. /// /// SORT_RO key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] /// https://valkey.io/commands/sort_ro/ + @since(version = 0.3.0-dev) sort-ro: async func(connection: borrow, key: string, opts: option) -> result>>, error>; - - // sortedset commands - /// Removes the members with the lowest or highest scores from the first non-empty sorted set, /// waiting up to the timeout for one, returning its key and the members and their scores. /// @@ -1762,6 +1968,7 @@ interface commands { /// /// BZMPOP timeout numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ] /// https://valkey.io/commands/bzmpop/ + @since(version = 0.3.0-dev) bzmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: bzmpop-end, count: option) -> result>>>, error>; /// Removes the member with the highest score from the first non-empty sorted set, waiting up to @@ -1774,6 +1981,7 @@ interface commands { /// /// BZPOPMAX key [ key ... ] timeout /// https://valkey.io/commands/bzpopmax/ + @since(version = 0.3.0-dev) bzpopmax: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; /// Removes the member with the lowest score from the first non-empty sorted set, waiting up to @@ -1786,6 +1994,7 @@ interface commands { /// /// BZPOPMIN key [ key ... ] timeout /// https://valkey.io/commands/bzpopmin/ + @since(version = 0.3.0-dev) bzpopmin: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; /// Adds the members with their scores to the sorted set stored at key, or updates their scores, @@ -1795,6 +2004,7 @@ interface commands { /// /// ZADD key [ NX | XX ] [ GT | LT ] [ CH ] score member [ score member ... ] /// https://valkey.io/commands/zadd/ + @since(version = 0.3.0-dev) zadd: async func(connection: borrow, key: string, condition: option, comparison: option, changed: bool, score-and-members: list>) -> result, error>; /// Increments the score of the member in the sorted set stored at key, returning the new score, @@ -1804,18 +2014,21 @@ interface commands { /// /// ZADD key [ NX | XX ] [ GT | LT ] INCR score member /// https://valkey.io/commands/zadd/ + @since(version = 0.3.0-dev) zadd-incr: async func(connection: borrow, key: string, condition: option, comparison: option, increment: double, member: string-or-bytes) -> result, error>; /// The number of members in the sorted set stored at key. /// /// ZCARD key /// https://valkey.io/commands/zcard/ + @since(version = 0.3.0-dev) zcard: async func(connection: borrow, key: string) -> result; /// The number of members of the sorted set stored at key with scores in the range. /// /// ZCOUNT key min max /// https://valkey.io/commands/zcount/ + @since(version = 0.3.0-dev) zcount: async func(connection: borrow, key: string, min: score-bound, max: score-bound) -> result; /// The members of the first sorted set that are not in any of the others. @@ -1824,6 +2037,7 @@ interface commands { /// /// ZDIFF numkeys key [ key ... ] /// https://valkey.io/commands/zdiff/ + @since(version = 0.3.0-dev) zdiff: async func(connection: borrow, keys: list) -> result, error>; /// The members, and their scores, of the first sorted set that are not in any of the others. @@ -1832,6 +2046,7 @@ interface commands { /// /// ZDIFF numkeys key [ key ... ] WITHSCORES /// https://valkey.io/commands/zdiff/ + @since(version = 0.3.0-dev) zdiff-withscores: async func(connection: borrow, keys: list) -> result>, error>; /// Stores the members of the first sorted set that are not in any of the others, returning how @@ -1839,12 +2054,14 @@ interface commands { /// /// ZDIFFSTORE destination numkeys key [ key ... ] /// https://valkey.io/commands/zdiffstore/ + @since(version = 0.3.0-dev) zdiffstore: async func(connection: borrow, destination: string, keys: list) -> result; /// Increments the score of the member in the sorted set stored at key, returning the new score. /// /// ZINCRBY key increment member /// https://valkey.io/commands/zincrby/ + @since(version = 0.3.0-dev) zincrby: async func(connection: borrow, key: string, increment: double, member: string-or-bytes) -> result; /// The members in all of the sorted sets. @@ -1855,6 +2072,7 @@ interface commands { /// /// ZINTER numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zinter/ + @since(version = 0.3.0-dev) zinter: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; /// The members, and their combined scores, in all of the sorted sets. @@ -1863,18 +2081,21 @@ interface commands { /// /// ZINTER numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] WITHSCORES /// https://valkey.io/commands/zinter/ + @since(version = 0.3.0-dev) zinter-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; /// The number of members in all of the sorted sets, counting stops at the limit. /// /// ZINTERCARD numkeys key [ key ... ] [ LIMIT limit ] /// https://valkey.io/commands/zintercard/ + @since(version = 0.3.0-dev) zintercard: async func(connection: borrow, keys: list, limit: option) -> result; /// Stores the members in all of the sorted sets, with their combined scores, returning how many. /// /// ZINTERSTORE destination numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zinterstore/ + @since(version = 0.3.0-dev) zinterstore: async func(connection: borrow, destination: string, keys: list, weights: option>, aggregate: option) -> result; /// The number of members of the sorted set stored at key in the range, for members of equal @@ -1882,6 +2103,7 @@ interface commands { /// /// ZLEXCOUNT key min max /// https://valkey.io/commands/zlexcount/ + @since(version = 0.3.0-dev) zlexcount: async func(connection: borrow, key: string, min: lex-bound, max: lex-bound) -> result; /// Removes the members with the lowest or highest scores from the first non-empty sorted set, @@ -1889,6 +2111,7 @@ interface commands { /// /// ZMPOP numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ] /// https://valkey.io/commands/zmpop/ + @since(version = 0.3.0-dev) zmpop: async func(connection: borrow, keys: list, end: zmpop-end, count: option) -> result>>>, error>; /// The score of each of the members of the sorted set stored at key, none for a member that is @@ -1896,6 +2119,7 @@ interface commands { /// /// ZMSCORE key member [ member ... ] /// https://valkey.io/commands/zmscore/ + @since(version = 0.3.0-dev) zmscore: async func(connection: borrow, key: string, members: list) -> result>, error>; /// Removes and returns the members with the highest scores, and their scores, up to the count, @@ -1903,6 +2127,7 @@ interface commands { /// /// ZPOPMAX key [ count ] /// https://valkey.io/commands/zpopmax/ + @since(version = 0.3.0-dev) zpopmax: async func(connection: borrow, key: string, count: option) -> result>, error>; /// Removes and returns the members with the lowest scores, and their scores, up to the count, @@ -1910,6 +2135,7 @@ interface commands { /// /// ZPOPMIN key [ count ] /// https://valkey.io/commands/zpopmin/ + @since(version = 0.3.0-dev) zpopmin: async func(connection: borrow, key: string, count: option) -> result>, error>; /// Random members of the sorted set stored at key, up to the count. @@ -1921,6 +2147,7 @@ interface commands { /// /// ZRANDMEMBER key [ count ] /// https://valkey.io/commands/zrandmember/ + @since(version = 0.3.0-dev) zrandmember: async func(connection: borrow, key: string, count: option) -> result, error>; /// Random members of the sorted set stored at key, and their scores, up to the count. @@ -1932,6 +2159,7 @@ interface commands { /// /// ZRANDMEMBER key count WITHSCORES /// https://valkey.io/commands/zrandmember/ + @since(version = 0.3.0-dev) zrandmember-withscores: async func(connection: borrow, key: string, count: integer) -> result>, error>; /// The members of the sorted set stored at key in the range, from the lowest score, or the @@ -1945,6 +2173,7 @@ interface commands { /// /// ZRANGE key start stop [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ] [ XX ] /// https://valkey.io/commands/zrange/ + @since(version = 0.3.0-dev) zrange: async func(connection: borrow, key: string, range: zrange-by, rev: bool, limit: option, xx: bool) -> result>, error>; /// The members of the sorted set stored at key in the range, and their scores, from the lowest @@ -1957,6 +2186,7 @@ interface commands { /// /// ZRANGE key start stop [ BYSCORE ] [ REV ] [ LIMIT offset count ] WITHSCORES [ XX ] /// https://valkey.io/commands/zrange/ + @since(version = 0.3.0-dev) zrange-withscores: async func(connection: borrow, key: string, range: zrange-withscores-by, rev: bool, limit: option, xx: bool) -> result>>, error>; /// Stores the members of the sorted set stored at source in the range, returning how many. @@ -1965,6 +2195,7 @@ interface commands { /// /// ZRANGESTORE dst src min max [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ] /// https://valkey.io/commands/zrangestore/ + @since(version = 0.3.0-dev) zrangestore: async func(connection: borrow, destination: string, source: string, range: zrangestore-by, rev: bool, limit: option) -> result; /// The position of the member in the sorted set stored at key, from the lowest score, none when @@ -1974,6 +2205,7 @@ interface commands { /// /// ZRANK key member /// https://valkey.io/commands/zrank/ + @since(version = 0.3.0-dev) zrank: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; /// The position, and the score, of the member in the sorted set stored at key, from the lowest @@ -1983,12 +2215,14 @@ interface commands { /// /// ZRANK key member WITHSCORE /// https://valkey.io/commands/zrank/ + @since(version = 0.3.0-dev) zrank-withscore: async func(connection: borrow, key: string, member: string-or-bytes) -> result>, error>; /// Removes the members from the sorted set stored at key, returning how many were members. /// /// ZREM key member [ member ... ] /// https://valkey.io/commands/zrem/ + @since(version = 0.3.0-dev) zrem: async func(connection: borrow, key: string, members: list) -> result; /// Removes the members of the sorted set stored at key in the range, for members of equal score, @@ -1996,6 +2230,7 @@ interface commands { /// /// ZREMRANGEBYLEX key min max /// https://valkey.io/commands/zremrangebylex/ + @since(version = 0.3.0-dev) zremrangebylex: async func(connection: borrow, key: string, min: lex-bound, max: lex-bound) -> result; /// Removes the members of the sorted set stored at key at the positions, both included, @@ -2003,6 +2238,7 @@ interface commands { /// /// ZREMRANGEBYRANK key start stop /// https://valkey.io/commands/zremrangebyrank/ + @since(version = 0.3.0-dev) zremrangebyrank: async func(connection: borrow, key: string, start: integer, stop: integer) -> result; /// Removes the members of the sorted set stored at key with scores in the range, returning how @@ -2010,6 +2246,7 @@ interface commands { /// /// ZREMRANGEBYSCORE key min max /// https://valkey.io/commands/zremrangebyscore/ + @since(version = 0.3.0-dev) zremrangebyscore: async func(connection: borrow, key: string, min: score-bound, max: score-bound) -> result; /// The position of the member in the sorted set stored at key, from the highest score, none @@ -2019,6 +2256,7 @@ interface commands { /// /// ZREVRANK key member /// https://valkey.io/commands/zrevrank/ + @since(version = 0.3.0-dev) zrevrank: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; /// The position, and the score, of the member in the sorted set stored at key, from the highest @@ -2028,6 +2266,7 @@ interface commands { /// /// ZREVRANK key member WITHSCORE /// https://valkey.io/commands/zrevrank/ + @since(version = 0.3.0-dev) zrevrank-withscore: async func(connection: borrow, key: string, member: string-or-bytes) -> result>, error>; /// Incrementally iterate over the members of the sorted set stored at key, and their scores. @@ -2036,6 +2275,7 @@ interface commands { /// /// ZSCAN key cursor [ MATCH pattern ] [ COUNT count ] /// https://valkey.io/commands/zscan/ + @since(version = 0.3.0-dev) zscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>>, error>; /// Incrementally iterate over the members of the sorted set stored at key. @@ -2044,12 +2284,14 @@ interface commands { /// /// ZSCAN key cursor [ MATCH pattern ] [ COUNT count ] NOSCORES /// https://valkey.io/commands/zscan/ + @since(version = 0.3.0-dev) zscan-noscores: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; /// The score of the member in the sorted set stored at key, none when it is not a member. /// /// ZSCORE key member /// https://valkey.io/commands/zscore/ + @since(version = 0.3.0-dev) zscore: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; /// The members in any of the sorted sets. @@ -2060,6 +2302,7 @@ interface commands { /// /// ZUNION numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zunion/ + @since(version = 0.3.0-dev) zunion: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; /// The members, and their combined scores, in any of the sorted sets. @@ -2068,27 +2311,28 @@ interface commands { /// /// ZUNION numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] WITHSCORES /// https://valkey.io/commands/zunion/ + @since(version = 0.3.0-dev) zunion-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; /// Stores the members in any of the sorted sets, with their combined scores, returning how many. /// /// ZUNIONSTORE destination numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zunionstore/ + @since(version = 0.3.0-dev) zunionstore: async func(connection: borrow, destination: string, keys: list, weights: option>, aggregate: option) -> result; - - // stream commands - /// Removes one or multiple messages from the Pending Entries List (PEL) of a stream consumer group. /// /// XACK key group ID [ ID ... ] /// https://valkey.io/commands/xack/ + @since(version = 0.3.0-dev) xack: async func(connection: borrow, key: string, group: string, ids: list) -> result; /// Appends the specified stream entry to the stream at the specified key. /// /// XADD key [ NOMKSTREAM ] [ 〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] ] 〈 * | id 〉 field value [ field value ... ] /// https://valkey.io/commands/xadd/ + @since(version = 0.3.0-dev) xadd: async func(connection: borrow, key: string, nomkstream: bool, trim: option, id: stream-id, field-and-values: list>) -> result, error>; /// Transfers ownership of pending stream entries that match the specified criteria. @@ -2101,6 +2345,7 @@ interface commands { /// /// XAUTOCLAIM key group consumer min-idle-time start [ COUNT count ] /// https://valkey.io/commands/xautoclaim/ + @since(version = 0.3.0-dev) xautoclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, start: option, count: option) -> result, list>>, list>, error>; /// Transfers ownership of pending stream entries that match the specified criteria. @@ -2113,15 +2358,16 @@ interface commands { /// /// XAUTOCLAIM key group consumer min-idle-time start [ COUNT count ] JUSTID /// https://valkey.io/commands/xautoclaim/ + @since(version = 0.3.0-dev) xautoclaim-justid: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, start: option, count: option) -> result, list, list>, error>; - /// Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument. /// /// Also see `xclaim-justid` /// /// XCLAIM key group consumer min-idle-time ID [ ID ... ] [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] [ LASTID lastid ] /// https://valkey.io/commands/xclaim/ + @since(version = 0.3.0-dev) xclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, ids: list, opts: option) -> result>>, error>; /// Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument. @@ -2130,54 +2376,63 @@ interface commands { /// /// XCLAIM key group consumer min-idle-time ID [ ID ... ] [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] JUSTID [ LASTID lastid ] /// https://valkey.io/commands/xclaim/ + @since(version = 0.3.0-dev) xclaim-justid: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, ids: list, opts: option) -> result, error>; /// Removes the specified entries from a stream, and returns the number of entries deleted. /// /// XDEL key ID [ ID ... ] /// https://valkey.io/commands/xdel/ + @since(version = 0.3.0-dev) xdel: async func(connection: borrow, key: string, ids: list) -> result; /// Create a new consumer group uniquely identified by for the stream stored at . /// /// XGROUP CREATE key group 〈 id | $ 〉 [ MKSTREAM ] [ ENTRIESREAD entriesread ] /// https://valkey.io/commands/xgroup-create/ + @since(version = 0.3.0-dev) xgroup-create: async func(connection: borrow, key: string, group: string, id: stream-id, mkstream: bool, entriesread: option) -> result<_, error>; /// Create a consumer named in the consumer group of the stream that's stored at . /// /// XGROUP CREATECONSUMER key group consumer /// https://valkey.io/commands/xgroup-createconsumer/ + @since(version = 0.3.0-dev) xgroup-createconsumer: async func(connection: borrow, key: string, group: string, consumer: string) -> result; /// The XGROUP DELCONSUMER command deletes a consumer from the consumer group. /// /// XGROUP DELCONSUMER key group consumer /// https://valkey.io/commands/xgroup-delconsumer/ + @since(version = 0.3.0-dev) xgroup-delconsumer: async func(connection: borrow, key: string, group: string, consumer: string) -> result; /// Completely destroys the consumer group. /// /// XGROUP DESTROY key group /// https://valkey.io/commands/xgroup-destroy/ + @since(version = 0.3.0-dev) xgroup-destroy: async func(connection: borrow, key: string, group: string) -> result; /// Set the last delivered ID for a consumer group. /// /// XGROUP SETID key group 〈 id | $ 〉 [ ENTRIESREAD entriesread ] /// https://valkey.io/commands/xgroup-setid/ + @since(version = 0.3.0-dev) xgroup-setid: async func(connection: borrow, key: string, group: string, id: stream-id, entriesread: option) -> result<_, error>; /// Consumers that belong to the consumer group of the stream stored at . /// /// XINFO CONSUMERS key group /// https://valkey.io/commands/xinfo-consumers/ + @since(version = 0.3.0-dev) xinfo-consumers: async func(connection: borrow, key: string, group: string) -> result, error>; /// Consumer groups of the stream stored at . /// /// XINFO GROUPS key /// https://valkey.io/commands/xinfo-groups/ + @since(version = 0.3.0-dev) xinfo-groups: async func(connection: borrow, key: string) -> result, error>; /// Information about the stream stored at . @@ -2186,6 +2441,7 @@ interface commands { /// /// XINFO STREAM key /// https://valkey.io/commands/xinfo-stream/ + @since(version = 0.3.0-dev) xinfo-stream: async func(connection: borrow, key: string) -> result; /// Information about the stream stored at . @@ -2194,12 +2450,14 @@ interface commands { /// /// XINFO STREAM key FULL [ COUNT count ] /// https://valkey.io/commands/xinfo-stream/ + @since(version = 0.3.0-dev) xinfo-stream-full: async func(connection: borrow, key: string, count: option) -> result; /// Number of entries inside a stream. /// /// XLEN key /// https://valkey.io/commands/xlen/ + @since(version = 0.3.0-dev) xlen: async func(connection: borrow, key: string) -> result; /// List of pending messages. @@ -2208,6 +2466,7 @@ interface commands { /// /// XPENDING key group /// https://valkey.io/commands/xpending/ + @since(version = 0.3.0-dev) xpending: async func(connection: borrow, key: string, group: string) -> result; /// List of pending messages. @@ -2216,18 +2475,21 @@ interface commands { /// /// XPENDING key group [ IDLE min-idle-time ] start end count [ consumer ] /// https://valkey.io/commands/xpending/ + @since(version = 0.3.0-dev) xpending-extended: async func(connection: borrow, key: string, group: string, min-idle-time: option, start: stream-id, end: stream-id, count: integer, consumer: option) -> result, error>; /// Stream entries matching a given range of IDs. /// /// XRANGE key start end [ COUNT count ] /// https://valkey.io/commands/xrange/ + @since(version = 0.3.0-dev) xrange: async func(connection: borrow, key: string, start: stream-id, end: stream-id, count: option) -> result>>, error>; /// Read data from one or multiple streams, only returning entries with an ID greater than the last received ID reported by the caller. /// /// XREAD [ COUNT count ] [ BLOCK milliseconds ] STREAMS key [ key ... ] ID [ ID ... ] /// https://valkey.io/commands/xread/ + @since(version = 0.3.0-dev) xread: async func(connection: borrow, count: option, block: option, keys: list, ids: list) -> result>>>>, error>; /// Special version of the XREAD command with support for consumer groups. @@ -2237,101 +2499,112 @@ interface commands { /// /// XREADGROUP GROUP group consumer [ COUNT count ] [ BLOCK milliseconds ] [ NOACK ] STREAMS key [ key ... ] ID [ ID ... ] /// https://valkey.io/commands/xreadgroup/ + @since(version = 0.3.0-dev) xreadgroup: async func(connection: borrow, group: string, consumer: string, count: option, block: option, noack: bool, keys: list, ids: list) -> result>>>>>, error>; /// XRANGE, but returning the entries in reverse order. /// /// XREVRANGE key end start [ COUNT count ] /// https://valkey.io/commands/xrevrange/ + @since(version = 0.3.0-dev) xrevrange: async func(connection: borrow, key: string, end: stream-id, start: stream-id, count: option) -> result>>, error>; /// Internal command. It is used by a Valkey primary to replicate the last delivered ID of streams. /// /// XSETID key last-id [ ENTRIESADDED entries-added ] [ MAXDELETEDID max-deleted-id ] /// https://valkey.io/commands/xsetid/ + @since(version = 0.3.0-dev) xsetid: async func(connection: borrow, key: string, last-id: stream-id, entries-added: option, max-deleted-id: option) -> result<_, error>; /// Trims the stream by evicting older entries. /// /// XTRIM key 〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] /// https://valkey.io/commands/xtrim/ + @since(version = 0.3.0-dev) xtrim: async func(connection: borrow, key: string, trim: stream-trim) -> result; - - // string commands - - /// Appends the value at the end of the string. /// /// APPEND key value /// https://valkey.io/commands/append/ + @since(version = 0.3.0-dev) append: async func(connection: borrow, key: string, value: string-or-bytes) -> result; /// Decrements the number stored at key by one. /// /// DECR key /// https://valkey.io/commands/decr/ + @since(version = 0.3.0-dev) decr: async func(connection: borrow, key: string) -> result; /// Decrements the number stored at key by decrement. /// /// DECRBY key decrement /// https://valkey.io/commands/decrby/ + @since(version = 0.3.0-dev) decrby: async func(connection: borrow, key: string, decrement: integer) -> result; /// Deletes the key if its value matches the given string. /// /// DELIFEQ key value /// https://valkey.io/commands/delifeq/ + @since(version = 0.3.0-dev) delifeq: async func(connection: borrow, key: string, value: string-or-bytes) -> result; - /// Get the value of key. /// /// GET key /// https://valkey.io/commands/get/ + @since(version = 0.3.0-dev) get: async func(connection: borrow, key: string) -> result, error>; /// Get the value of key and delete the key. /// /// GETDEL key /// https://valkey.io/commands/getdel/ + @since(version = 0.3.0-dev) getdel: async func(connection: borrow, key: string) -> result, error>; /// Get the value of key and optionally set its expiration. /// /// GETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ] /// https://valkey.io/commands/getex/ + @since(version = 0.3.0-dev) getex: async func(connection: borrow, key: string, ttl: option) -> result, error>; /// Returns the substring of the string value stored at key, determined by the offsets start and end. /// /// GETRANGE key start end /// https://valkey.io/commands/getrange/ + @since(version = 0.3.0-dev) getrange: async func(connection: borrow, key: string, start: integer, end: integer) -> result; /// Atomically sets key to value and returns the old value stored at key. /// /// GETSET key value /// https://valkey.io/commands/getset/ + @since(version = 0.3.0-dev) getset: async func(connection: borrow, key: string, value: string-or-bytes) -> result, error>; /// Increments the number stored at key by one. /// /// INCR key /// https://valkey.io/commands/incr/ + @since(version = 0.3.0-dev) incr: async func(connection: borrow, key: string) -> result; /// Increments the number stored at key by increment. /// /// INCRBY key increment /// https://valkey.io/commands/incrby/ + @since(version = 0.3.0-dev) incrby: async func(connection: borrow, key: string, increment: integer) -> result; /// Increment the string representing a floating point number stored at key by the specified increment. /// /// INCRBYFLOAT key increment /// https://valkey.io/commands/incrbyfloat/ + @since(version = 0.3.0-dev) incrbyfloat: async func(connection: borrow, key: string, increment: double) -> result; /// The LCS command implements the longest common subsequence algorithm. @@ -2340,6 +2613,7 @@ interface commands { /// /// LCS key1 key2 /// https://valkey.io/commands/lcs/ + @since(version = 0.3.0-dev) lcs: async func(connection: borrow, key1: string, key2: string) -> result; /// The LCS command implements the longest common subsequence algorithm. @@ -2348,6 +2622,7 @@ interface commands { /// /// LCS key1 key2 LEN /// https://valkey.io/commands/lcs/ + @since(version = 0.3.0-dev) lcs-len: async func(connection: borrow, key1: string, key2: string) -> result; /// The LCS command implements the longest common subsequence algorithm. @@ -2356,36 +2631,42 @@ interface commands { /// /// LCS key1 key2 IDX [ MINMATCHLEN min-match-len ] [ WITHMATCHLEN ] /// https://valkey.io/commands/lcs/ + @since(version = 0.3.0-dev) lcs-idx: async func(connection: borrow, key1: string, key2: string, min-match-len: option, with-match-len: bool ) -> result; /// Returns the values of all specified keys. /// /// MGET key [ key ... ] /// https://valkey.io/commands/mget/ + @since(version = 0.3.0-dev) mget: async func(connection: borrow, keys: list) -> result>, error>; /// Sets the given keys to their respective values. /// /// MSET key value [ key value ... ] /// https://valkey.io/commands/mset/ + @since(version = 0.3.0-dev) mset: async func(connection: borrow, keys-and-values: list>) -> result<_, error>; /// Sets multiple key-value pairs while also assigning a shared expiration time to all the keys atomically. /// /// MSETEX numkeys key value [ key value ... ] [ NX | XX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] /// https://valkey.io/commands/msetex/ + @since(version = 0.3.0-dev) msetex: async func(connection: borrow, keys-and-values: list>, precondition: option, ttl: option) -> result; /// Sets the given keys to their respective values. /// /// MSETNX key value [ key value ... ] /// https://valkey.io/commands/msetnx/ + @since(version = 0.3.0-dev) msetnx: async func(connection: borrow, keys-and-values: list>) -> result; /// Set key to hold the string value with expiration in milliseconds. /// /// PSETEX key milliseconds value /// https://valkey.io/commands/psetex/ + @since(version = 0.3.0-dev) psetex: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, value: string-or-bytes) -> result<_, error>; /// Set key to hold the string value. @@ -2394,42 +2675,49 @@ interface commands { /// /// SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] /// https://valkey.io/commands/set/ + @since(version = 0.3.0-dev) set: async func(connection: borrow, key: string, value: string-or-bytes, precondition: option, ttl: option) -> result; /// Set key to hold the string value. Returning the previous value. /// /// SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] GET [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] /// https://valkey.io/commands/set/ + @since(version = 0.3.0-dev) set-get: async func(connection: borrow, key: string, value: string-or-bytes, precondition: option, ttl: option) -> result, error>; /// Set key to hold the string value with expiration in seconds. /// /// SETEX key seconds value /// https://valkey.io/commands/setex/ + @since(version = 0.3.0-dev) setex: async func(connection: borrow, key: string, seconds: duration-seconds, value: string-or-bytes) -> result<_, error>; /// Set key to hold string value if key does not exist. /// /// SETNX key value /// https://valkey.io/commands/setnx/ + @since(version = 0.3.0-dev) setnx: async func(connection: borrow, key: string, value: string-or-bytes) -> result; /// Overwrites part of the string stored at key, starting at the specified offset, for the entire length of value. /// /// SETRANGE key offset value /// https://valkey.io/commands/setrange/ + @since(version = 0.3.0-dev) setrange: async func(connection: borrow, key: string, offset: u32, value: string-or-bytes) -> result; /// Returns the length of the string value stored at key. /// /// STRLEN key /// hthttps://valkey.io/commands/strlen/ + @since(version = 0.3.0-dev) strlen: async func(connection: borrow, key: string) -> result; /// Returns the substring of the string value stored at key, determined by the offsets start and end. /// /// SUBSTR key start end /// https://valkey.io/commands/substr/ + @since(version = 0.3.0-dev) substr: async func(connection: borrow, key: string, start: integer, end: integer) -> result; } diff --git a/wit/connection.wit b/wit/connection.wit index 67bfa95..d9c0862 100644 --- a/wit/connection.wit +++ b/wit/connection.wit @@ -9,6 +9,7 @@ /// with them, subscriptions, push messages, and transactions. Higher level, typed operations are /// functions in the `commands` interface that borrow the connection. interface connection { + @since(version = 0.3.0-dev) use types.{connect-opts, duration-milliseconds, error, resp-value, string-or-bytes, subscription-event, subscription-target}; @since(version = 0.3.0-dev) @@ -31,9 +32,11 @@ interface connection { /// /// Async so the connection keeps working between commands after it returns, e.g. to read /// push messages and to send keep-alive pings. + @since(version = 0.3.0-dev) new: static async func(replies: stream, closed: future>, opts: option) -> tuple, future>>; /// The server's properties from the reply to HELLO, e.g. `server`, `version` and `mode`. + @since(version = 0.3.0-dev) server-properties: func() -> list>; /// Sends a command and waits for its reply. @@ -51,6 +54,7 @@ interface connection { /// `emit`. /// /// A command has at least one word, an empty command fails with a client error. + @since(version = 0.3.0-dev) exchange: async func(command: list) -> result; /// Sends the commands one after another, with no other command sent between them, and waits @@ -59,6 +63,7 @@ interface connection { /// Each command has its own result, in order, as if each was sent with `exchange`. Commands /// sharing a connection may otherwise be interleaved with other callers' commands, which /// matters for a sequence that must not be interrupted, e.g. MULTI, its commands and EXEC. + @since(version = 0.3.0-dev) exchange-all: async func(commands: list>) -> list>; /// Sends a command without waiting for its reply, resolving once the command is written. @@ -68,6 +73,7 @@ interface connection { /// `CLIENT REPLY SKIP`. /// /// A command has at least one word, an empty command fails with a client error. + @since(version = 0.3.0-dev) emit: async func(command: list) -> result<_, error>; /// Marks the keys to watch, EXEC aborts the next transaction when any of them changed. @@ -80,12 +86,14 @@ interface connection { /// https://valkey.io/commands/watch/ /// /// At least one key, no keys fails with a client error. + @since(version = 0.3.0-dev) watch: async func(keys: list) -> result<_, error>; /// Forgets all the watched keys. /// /// UNWATCH /// https://valkey.io/commands/unwatch/ + @since(version = 0.3.0-dev) unwatch: async func() -> result<_, error>; /// Runs the commands as a transaction, MULTI, each command, then EXEC, with no other command @@ -102,6 +110,7 @@ interface connection { /// MULTI, EXEC /// https://valkey.io/commands/multi/ /// https://valkey.io/commands/exec/ + @since(version = 0.3.0-dev) transaction: async func(commands: list>) -> result>, error>; /// A new subscription, subscribed to nothing yet, see the subscription's `subscribe`. @@ -115,6 +124,7 @@ interface connection { /// Fails with a client error when the connection component was built without pub/sub, /// e.g. `connection-minimal`. Its push messages are then all dead letters, and SUBSCRIBE and /// the like may still be sent with `exchange`. + @since(version = 0.3.0-dev) subscription: func() -> result; /// The values nothing else claims, kept apart from replies. @@ -127,6 +137,7 @@ interface connection { /// Each call returns a new stream that receives every value arriving after the call, /// values arriving while no stream is open are dropped. A stream that falls too far behind /// is closed rather than holding up replies, the values it missed are lost. + @since(version = 0.3.0-dev) dead-letters: func() -> stream; /// Resets the connection to the state of a new one, then repeats the handshake of `new`. @@ -148,6 +159,7 @@ interface connection { /// /// RESET /// https://valkey.io/commands/reset/ + @since(version = 0.3.0-dev) reset: async func() -> result<_, error>; /// Sends PING when no command has been sent and no reply is outstanding for `interval`, @@ -155,6 +167,7 @@ interface connection { /// /// Unlike TCP keep-alive, a PING counts as activity for the server's idle client timeout, /// and works over any transport. + @since(version = 0.3.0-dev) set-keep-alive: func(interval: option); } @@ -184,6 +197,7 @@ interface connection { /// https://valkey.io/commands/ssubscribe/ /// /// At least one target, no targets fails with a client error. + @since(version = 0.3.0-dev) subscribe: async func(targets: list) -> result<_, error>; /// The events arriving after the call: targets subscribed to and unsubscribed from, and @@ -205,6 +219,7 @@ interface connection { /// closed rather than holding up the connection, its future resolves to `lagged`, and the /// events it missed, messages and changes of targets alike, are lost. The future resolves /// after the stream delivered what it holds, read the stream to its end first. + @since(version = 0.3.0-dev) events: func() -> tuple, future>>; /// The targets the server confirmed, in the order they were subscribed to, until they are @@ -213,6 +228,7 @@ interface connection { /// Once the connection closes, the targets confirmed until then are still listed, e.g. to /// subscribe to them on a new connection. `subscribe` and `unsubscribe` then fail with the /// error that closed the connection, and leave the targets as they are. + @since(version = 0.3.0-dev) targets: func() -> list; /// Unsubscribes from the targets, none for every target, resolving once each is @@ -231,6 +247,7 @@ interface connection { /// /// A list of targets has at least one target, otherwise fails with a client error. None /// unsubscribes from every target. + @since(version = 0.3.0-dev) unsubscribe: async func(targets: option>) -> result<_, error>; } } diff --git a/wit/connector.wit b/wit/connector.wit index 3414a4e..6a5b3a7 100644 --- a/wit/connector.wit +++ b/wit/connector.wit @@ -3,10 +3,14 @@ /// /// A component that needs a connection, but not a say in how it is made, imports a connector, the /// composition decides which one, e.g. `tcp-connector`. +@since(version = 0.3.0-dev) interface connector { + @since(version = 0.3.0-dev) use types.{error}; + @since(version = 0.3.0-dev) use connection.{connection}; /// Establishes a new connection. + @since(version = 0.3.0-dev) connect: async func() -> result; } diff --git a/wit/tcp-connector-constants.wit b/wit/tcp-connector-constants.wit index 4b92e84..6a67dd7 100644 --- a/wit/tcp-connector-constants.wit +++ b/wit/tcp-connector-constants.wit @@ -1,48 +1,58 @@ /// The keys of the `tcp-connector`'s wasi:config, and their defaults, for the components that derive /// their config from it. The values are provided by the `tcp-connector-constants` component. +@since(version = 0.3.0-dev) interface tcp-connector-constants { /// The key of the server's hostname or IP address. /// /// @value "hostname" + @since(version = 0.3.0-dev) hostname-key: func() -> string; /// The server's hostname or IP address without one. /// /// @value "127.0.0.1" + @since(version = 0.3.0-dev) hostname-default: func() -> string; /// The key of the server's port. /// /// @value "port" + @since(version = 0.3.0-dev) port-key: func() -> string; /// The server's port without one. /// /// @value 6379 + @since(version = 0.3.0-dev) port-default: func() -> u16; /// The key of the user to authenticate as, with a password. /// /// @value "username" + @since(version = 0.3.0-dev) username-key: func() -> string; /// The user to authenticate as without one. /// /// @value "default" + @since(version = 0.3.0-dev) username-default: func() -> string; /// The key of the password, authenticating when set. /// /// @value "password" + @since(version = 0.3.0-dev) password-key: func() -> string; /// The key of the connection's name, see `CLIENT SETNAME`. /// /// @value "client-name" + @since(version = 0.3.0-dev) client-name-key: func() -> string; /// The key of the milliseconds idle before a `PING`, disabled when unset or `0`. /// /// @value "keep-alive-interval-ms" + @since(version = 0.3.0-dev) keep-alive-interval-ms-key: func() -> string; } diff --git a/wit/tcp.wit b/wit/tcp.wit index bb91828..2377e2e 100644 --- a/wit/tcp.wit +++ b/wit/tcp.wit @@ -1,6 +1,8 @@ /// A transport for connections over wasi:sockets TCP. interface tcp { + @since(version = 0.3.0-dev) use types.{connect-opts, error, resp-value}; + @since(version = 0.3.0-dev) use connection.{connection}; /// A TCP connection to a Valkey server, framing the bytes it carries into RESP values in each @@ -9,6 +11,7 @@ interface tcp { resource transport { /// Opens a transport to `host`, a hostname or an IP address. Each address the host resolves /// to is tried in turn. + @since(version = 0.3.0-dev) open: static async func(host: string, port: u16) -> result; /// Writes each value from `requests` to the server, in order. @@ -16,6 +19,7 @@ interface tcp { /// May be called at most once, like `tcp-socket.send`. Closing `requests` half-closes the /// socket. The future resolves once every value is written, or with the error that stopped /// writing. + @since(version = 0.3.0-dev) send: func(requests: stream) -> future>; /// The values the server sends, in order. @@ -23,11 +27,13 @@ interface tcp { /// May be called at most once, like `tcp-socket.receive`. The stream closes when the server /// closes the socket, the future resolves to `ok` for a graceful close or to the error /// otherwise. + @since(version = 0.3.0-dev) receive: func() -> tuple, future>>; } /// Opens a transport and establishes a connection over it with `opts`, see `connection.new`. /// /// The connection keeps the transport alive. + @since(version = 0.3.0-dev) connect: async func(host: string, port: u16, opts: option) -> result; } From be7cb24ea8ff4d313eb05c479a71ab4024094a0d Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Thu, 15 Oct 2026 21:24:16 -0400 Subject: [PATCH 36/37] refine command docs Signed-off-by: Scott Andrews --- components/commands/src/lib.rs | 1027 +++++++++++++++--------------- wit/commands.wit | 1064 ++++++++++++++++---------------- 2 files changed, 1024 insertions(+), 1067 deletions(-) diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index fd1fb93..3eba690 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -73,8 +73,7 @@ fn sort_row_len(opts: &Option) -> usize { } impl Guest for Commands { - #[doc = " admin commands"] - #[doc = " Delete all the specified ACL users and terminate all the connections that are authenticated with such users."] + #[doc = " Deletes ACL users, and terminates their connections."] #[doc = ""] #[doc = " ACL DELUSER username [ username ... ]"] #[doc = " https://valkey.io/commands/acl-deluser/"] @@ -90,7 +89,7 @@ impl Guest for Commands { .await } - #[doc = " Generates a random password."] + #[doc = " Generates a pseudorandom, secure password that can be used to identify ACL users."] #[doc = ""] #[doc = " ACL GENPASS [ bits ]"] #[doc = " https://valkey.io/commands/acl-genpass/"] @@ -99,7 +98,7 @@ impl Guest for Commands { exchange(connection, ("ACL", "GENPASS", bits)).await } - #[doc = " Create an ACL user with the specified rules or modify the rules of an existing user."] + #[doc = " Creates and modifies an ACL user and its rules."] #[doc = ""] #[doc = " ACL SETUSER username [ rule ] [ [ rule ] ... ]"] #[doc = " https://valkey.io/commands/acl-setuser/"] @@ -121,7 +120,7 @@ impl Guest for Commands { .await } - #[doc = " The ACL categories, or with a category, the commands in it."] + #[doc = " Lists the ACL categories, or the commands inside a category."] #[doc = ""] #[doc = " ACL CAT [ category ]"] #[doc = " https://valkey.io/commands/acl-cat/"] @@ -133,7 +132,7 @@ impl Guest for Commands { exchange(connection, ("ACL", "CAT", category)).await } - #[doc = " Whether the user may run the command, without running it."] + #[doc = " Simulates the execution of a command by a user, without executing the command."] #[doc = ""] #[doc = " ACL DRYRUN username command [ arg [ arg ... ] ]"] #[doc = " https://valkey.io/commands/acl-dryrun/"] @@ -150,7 +149,10 @@ impl Guest for Commands { .await } - #[doc = " The rules of the user, none when the user does not exist."] + #[doc = " Lists the ACL rules of a user."] + #[doc = ""] + #[doc = " None when the user does not exist. The rules are as the server normalized them, e.g."] + #[doc = " `allkeys` is `~*`. Applied with `acl-setuser`, after `reset`, they recreate the user."] #[doc = ""] #[doc = " ACL GETUSER username"] #[doc = " https://valkey.io/commands/acl-getuser/"] @@ -164,7 +166,7 @@ impl Guest for Commands { Ok(rules.map(|rules| rules.0)) } - #[doc = " The rules of every user, in the format of an ACL file."] + #[doc = " Dumps the effective rules in ACL file format."] #[doc = ""] #[doc = " ACL LIST"] #[doc = " https://valkey.io/commands/acl-list/"] @@ -174,7 +176,7 @@ impl Guest for Commands { Ok(users.0) } - #[doc = " Replaces the users with those of the configured ACL file."] + #[doc = " Reloads the rules from the configured ACL file."] #[doc = ""] #[doc = " ACL LOAD"] #[doc = " https://valkey.io/commands/acl-load/"] @@ -183,8 +185,7 @@ impl Guest for Commands { exchange(connection, ("ACL", "LOAD")).await } - #[doc = " The recent security events, commands or authentications the rules denied, up to the count,"] - #[doc = " 10 without one."] + #[doc = " Lists recent security events generated due to ACL rules."] #[doc = ""] #[doc = " See also `acl-log-reset`."] #[doc = ""] @@ -198,7 +199,9 @@ impl Guest for Commands { exchange(connection, ("ACL", "LOG", count)).await } - #[doc = " Clears the security events."] + #[doc = " Lists recent security events generated due to ACL rules."] + #[doc = ""] + #[doc = " The `RESET` form, clearing the security events."] #[doc = ""] #[doc = " See also `acl-log`."] #[doc = ""] @@ -209,7 +212,7 @@ impl Guest for Commands { exchange(connection, ("ACL", "LOG", "RESET")).await } - #[doc = " Saves the users to the configured ACL file."] + #[doc = " Saves the effective ACL rules in the configured ACL file."] #[doc = ""] #[doc = " ACL SAVE"] #[doc = " https://valkey.io/commands/acl-save/"] @@ -218,7 +221,7 @@ impl Guest for Commands { exchange(connection, ("ACL", "SAVE")).await } - #[doc = " The names of the users."] + #[doc = " Lists all ACL users."] #[doc = ""] #[doc = " ACL USERS"] #[doc = " https://valkey.io/commands/acl-users/"] @@ -227,7 +230,7 @@ impl Guest for Commands { exchange(connection, ("ACL", "USERS")).await } - #[doc = " The name of the user the connection is authenticated as."] + #[doc = " Returns the authenticated username of the current connection."] #[doc = ""] #[doc = " ACL WHOAMI"] #[doc = " https://valkey.io/commands/acl-whoami/"] @@ -236,7 +239,7 @@ impl Guest for Commands { exchange(connection, ("ACL", "WHOAMI")).await } - #[doc = " Starts rewriting the append-only file in the background, returning the server's message."] + #[doc = " Asynchronously rewrites the append-only file to disk."] #[doc = ""] #[doc = " BGREWRITEAOF"] #[doc = " https://valkey.io/commands/bgrewriteaof/"] @@ -245,8 +248,7 @@ impl Guest for Commands { exchange(connection, "BGREWRITEAOF").await } - #[doc = " Starts saving the databases to disk in the background, or schedules or cancels it, returning"] - #[doc = " the server's message."] + #[doc = " Asynchronously saves the database(s) to disk."] #[doc = ""] #[doc = " BGSAVE [ SCHEDULE | CANCEL ]"] #[doc = " https://valkey.io/commands/bgsave/"] @@ -258,7 +260,7 @@ impl Guest for Commands { exchange(connection, ("BGSAVE", operation)).await } - #[doc = " Closes the connections that match every one of the filters, returning how many."] + #[doc = " Terminates open connections."] #[doc = ""] #[doc = " CLIENT KILL filter [ filter ... ]"] #[doc = " https://valkey.io/commands/client-kill/"] @@ -274,8 +276,10 @@ impl Guest for Commands { .await } - #[doc = " The connections that match every one of the filters, all of them without filters, each a"] - #[doc = " map of its fields, e.g. `id`, `addr` and `name`."] + #[doc = " Lists open connections."] + #[doc = ""] + #[doc = " Each connection is a map of its fields, parsed from the server's text, e.g. `id`, `addr` and"] + #[doc = " `name`."] #[doc = ""] #[doc = " CLIENT LIST [ filter [ filter ... ] ]"] #[doc = " https://valkey.io/commands/client-list/"] @@ -289,7 +293,9 @@ impl Guest for Commands { Ok(clients.0) } - #[doc = " Whether the connection may be evicted to free memory, `ON` excludes it."] + #[doc = " Sets the client eviction mode of the connection."] + #[doc = ""] + #[doc = " It applies to the connection, so to every caller sharing it."] #[doc = ""] #[doc = " CLIENT NO-EVICT 〈 ON | OFF 〉"] #[doc = " https://valkey.io/commands/client-no-evict/"] @@ -302,7 +308,7 @@ impl Guest for Commands { .await } - #[doc = " Suspends running the commands of other connections, writes only with `WRITE`, for the time."] + #[doc = " Suspends commands processing."] #[doc = ""] #[doc = " CLIENT PAUSE timeout [ WRITE | ALL ]"] #[doc = " https://valkey.io/commands/client-pause/"] @@ -315,8 +321,7 @@ impl Guest for Commands { exchange(connection, ("CLIENT", "PAUSE", timeout, mode)).await } - #[doc = " Unblocks the connection with the ID from a blocking command, as if it timed out, or with an"] - #[doc = " error, returning whether it was blocked."] + #[doc = " Unblocks a client blocked by a blocking command from a different connection."] #[doc = ""] #[doc = " CLIENT UNBLOCK client-id [ TIMEOUT | ERROR ]"] #[doc = " https://valkey.io/commands/client-unblock/"] @@ -329,7 +334,7 @@ impl Guest for Commands { exchange(connection, ("CLIENT", "UNBLOCK", client_id, unblock_type)).await } - #[doc = " Resumes running the commands of connections paused with `client-pause`."] + #[doc = " Resumes processing commands from paused clients."] #[doc = ""] #[doc = " CLIENT UNPAUSE"] #[doc = " https://valkey.io/commands/client-unpause/"] @@ -338,7 +343,7 @@ impl Guest for Commands { exchange(connection, ("CLIENT", "UNPAUSE")).await } - #[doc = " Assigns the slots to the node."] + #[doc = " Assigns new hash slots to a node."] #[doc = ""] #[doc = " CLUSTER ADDSLOTS slot [ slot ... ]"] #[doc = " https://valkey.io/commands/cluster-addslots/"] @@ -351,7 +356,7 @@ impl Guest for Commands { .await } - #[doc = " Assigns the ranges of slots, start and end both included, to the node."] + #[doc = " Assigns new hash slot ranges to a node."] #[doc = ""] #[doc = " CLUSTER ADDSLOTSRANGE start-slot end-slot [ start-slot end-slot ... ]"] #[doc = " https://valkey.io/commands/cluster-addslotsrange/"] @@ -367,7 +372,7 @@ impl Guest for Commands { .await } - #[doc = " Advances the node's config epoch, unless it is already the greatest in the cluster."] + #[doc = " Advances the cluster config epoch."] #[doc = ""] #[doc = " CLUSTER BUMPEPOCH"] #[doc = " https://valkey.io/commands/cluster-bumpepoch/"] @@ -376,7 +381,7 @@ impl Guest for Commands { exchange(connection, ("CLUSTER", "BUMPEPOCH")).await } - #[doc = " Cancels the slot migrations in progress."] + #[doc = " Cancel all current ongoing slot migration operations."] #[doc = ""] #[doc = " CLUSTER CANCELSLOTMIGRATIONS"] #[doc = " https://valkey.io/commands/cluster-cancelslotmigrations/"] @@ -385,7 +390,7 @@ impl Guest for Commands { exchange(connection, ("CLUSTER", "CANCELSLOTMIGRATIONS")).await } - #[doc = " The number of active failure reports for the node."] + #[doc = " Returns the number of active failure reports for a node."] #[doc = ""] #[doc = " CLUSTER COUNT-FAILURE-REPORTS node-id"] #[doc = " https://valkey.io/commands/cluster-count-failure-reports/"] @@ -397,7 +402,7 @@ impl Guest for Commands { exchange(connection, ("CLUSTER", "COUNT-FAILURE-REPORTS", node_id)).await } - #[doc = " Unassigns the slots from the node."] + #[doc = " Sets hash slots as unbound for a node."] #[doc = ""] #[doc = " CLUSTER DELSLOTS slot [ slot ... ]"] #[doc = " https://valkey.io/commands/cluster-delslots/"] @@ -410,7 +415,7 @@ impl Guest for Commands { .await } - #[doc = " Unassigns the ranges of slots, start and end both included, from the node."] + #[doc = " Sets hash slot ranges as unbound for a node."] #[doc = ""] #[doc = " CLUSTER DELSLOTSRANGE start-slot end-slot [ start-slot end-slot ... ]"] #[doc = " https://valkey.io/commands/cluster-delslotsrange/"] @@ -426,7 +431,7 @@ impl Guest for Commands { .await } - #[doc = " Fails over the primary of this replica, coordinated with it unless `FORCE` or `TAKEOVER`."] + #[doc = " Forces a replica to perform a manual failover of its primary."] #[doc = ""] #[doc = " CLUSTER FAILOVER [ FORCE | TAKEOVER ]"] #[doc = " https://valkey.io/commands/cluster-failover/"] @@ -438,7 +443,7 @@ impl Guest for Commands { exchange(connection, ("CLUSTER", "FAILOVER", mode)).await } - #[doc = " Unassigns all the slots of the node."] + #[doc = " Deletes all slots information from a node."] #[doc = ""] #[doc = " CLUSTER FLUSHSLOTS"] #[doc = " https://valkey.io/commands/cluster-flushslots/"] @@ -447,7 +452,7 @@ impl Guest for Commands { exchange(connection, ("CLUSTER", "FLUSHSLOTS")).await } - #[doc = " Removes the node from the nodes this node knows of."] + #[doc = " Removes a node from the nodes table."] #[doc = ""] #[doc = " CLUSTER FORGET node-id"] #[doc = " https://valkey.io/commands/cluster-forget/"] @@ -456,7 +461,7 @@ impl Guest for Commands { exchange(connection, ("CLUSTER", "FORGET", node_id)).await } - #[doc = " The slot migrations in progress, and those recently finished."] + #[doc = " Gets the status of ongoing and recently finished slot import and export operations."] #[doc = ""] #[doc = " CLUSTER GETSLOTMIGRATIONS"] #[doc = " https://valkey.io/commands/cluster-getslotmigrations/"] @@ -467,7 +472,7 @@ impl Guest for Commands { exchange(connection, ("CLUSTER", "GETSLOTMIGRATIONS")).await } - #[doc = " Connects the node to another, adding it to the cluster."] + #[doc = " Forces a node to handshake with another node."] #[doc = ""] #[doc = " CLUSTER MEET ip port [ cluster-bus-port ]"] #[doc = " https://valkey.io/commands/cluster-meet/"] @@ -481,7 +486,7 @@ impl Guest for Commands { exchange(connection, ("CLUSTER", "MEET", ip, port, cluster_bus_port)).await } - #[doc = " Migrates the ranges of slots from this node to other nodes."] + #[doc = " Migrate the given slots from this node to the specified nodes."] #[doc = ""] #[doc = " CLUSTER MIGRATESLOTS SLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] NODE node-id [ AUTH username password ] [ SLOTSRANGE ... ]"] #[doc = " https://valkey.io/commands/cluster-migrateslots/"] @@ -501,7 +506,7 @@ impl Guest for Commands { .await } - #[doc = " The replicas of the primary node, each a line in the format of CLUSTER NODES."] + #[doc = " Lists the replica nodes of a primary node."] #[doc = ""] #[doc = " CLUSTER REPLICAS node-id"] #[doc = " https://valkey.io/commands/cluster-replicas/"] @@ -513,8 +518,9 @@ impl Guest for Commands { exchange(connection, ("CLUSTER", "REPLICAS", node_id)).await } - #[doc = " Makes the node a replica of the primary node, or with none, detaches it from its primary,"] - #[doc = " `NO ONE`."] + #[doc = " Configures a node as replica of a primary node or detaches a replica from its primary."] + #[doc = ""] + #[doc = " None detaches the node from its primary, `NO ONE`."] #[doc = ""] #[doc = " CLUSTER REPLICATE 〈 node-id | NO ONE 〉"] #[doc = " https://valkey.io/commands/cluster-replicate/"] @@ -531,7 +537,7 @@ impl Guest for Commands { exchange(connection, ("CLUSTER", "REPLICATE", node)).await } - #[doc = " Resets the node, forgetting the other nodes and its slots, `SOFT` without a mode."] + #[doc = " Resets a node."] #[doc = ""] #[doc = " CLUSTER RESET [ HARD | SOFT ]"] #[doc = " https://valkey.io/commands/cluster-reset/"] @@ -543,7 +549,7 @@ impl Guest for Commands { exchange(connection, ("CLUSTER", "RESET", mode)).await } - #[doc = " Saves the node's cluster configuration to disk."] + #[doc = " Forces a node to save the cluster configuration to disk."] #[doc = ""] #[doc = " CLUSTER SAVECONFIG"] #[doc = " https://valkey.io/commands/cluster-saveconfig/"] @@ -552,7 +558,7 @@ impl Guest for Commands { exchange(connection, ("CLUSTER", "SAVECONFIG")).await } - #[doc = " Sets the config epoch of a new node."] + #[doc = " Sets the configuration epoch for a new node."] #[doc = ""] #[doc = " CLUSTER SET-CONFIG-EPOCH config-epoch"] #[doc = " https://valkey.io/commands/cluster-set-config-epoch/"] @@ -564,7 +570,7 @@ impl Guest for Commands { exchange(connection, ("CLUSTER", "SET-CONFIG-EPOCH", config_epoch)).await } - #[doc = " Changes the state of the slot on the node."] + #[doc = " Binds a hash slot to a node."] #[doc = ""] #[doc = " CLUSTER SETSLOT slot 〈 IMPORTING node-id | MIGRATING node-id | NODE node-id | STABLE 〉 [ TIMEOUT timeout ]"] #[doc = " https://valkey.io/commands/cluster-setslot/"] @@ -588,7 +594,7 @@ impl Guest for Commands { .await } - #[doc = " The entries of the command log of the type, up to the count, all of them for -1."] + #[doc = " Returns the specified command log's entries."] #[doc = ""] #[doc = " COMMANDLOG GET count 〈 slow | large-request | large-reply 〉"] #[doc = " https://valkey.io/commands/commandlog-get/"] @@ -601,7 +607,7 @@ impl Guest for Commands { exchange(connection, ("COMMANDLOG", "GET", count, type_)).await } - #[doc = " The number of entries in the command log of the type."] + #[doc = " Returns the number of entries in the specified type of command log."] #[doc = ""] #[doc = " COMMANDLOG LEN 〈 slow | large-request | large-reply 〉"] #[doc = " https://valkey.io/commands/commandlog-len/"] @@ -613,7 +619,7 @@ impl Guest for Commands { exchange(connection, ("COMMANDLOG", "LEN", type_)).await } - #[doc = " Clears the command log of the type."] + #[doc = " Clears all entries from the specified type of command log."] #[doc = ""] #[doc = " COMMANDLOG RESET 〈 slow | large-request | large-reply 〉"] #[doc = " https://valkey.io/commands/commandlog-reset/"] @@ -622,7 +628,7 @@ impl Guest for Commands { exchange(connection, ("COMMANDLOG", "RESET", type_)).await } - #[doc = " The values of the configuration parameters matching the glob-style patterns, by name."] + #[doc = " Returns the effective values of configuration parameters."] #[doc = ""] #[doc = " CONFIG GET parameter [ parameter ... ]"] #[doc = " https://valkey.io/commands/config-get/"] @@ -638,7 +644,7 @@ impl Guest for Commands { .await } - #[doc = " Resets the server's statistics, e.g. those of INFO."] + #[doc = " Resets the server's statistics."] #[doc = ""] #[doc = " CONFIG RESETSTAT"] #[doc = " https://valkey.io/commands/config-resetstat/"] @@ -647,7 +653,7 @@ impl Guest for Commands { exchange(connection, ("CONFIG", "RESETSTAT")).await } - #[doc = " Rewrites the configuration file with the current configuration."] + #[doc = " Persists the effective configuration to file."] #[doc = ""] #[doc = " CONFIG REWRITE"] #[doc = " https://valkey.io/commands/config-rewrite/"] @@ -656,7 +662,7 @@ impl Guest for Commands { exchange(connection, ("CONFIG", "REWRITE")).await } - #[doc = " Sets the configuration parameters to the values, all or none of them."] + #[doc = " Sets configuration parameters in-flight."] #[doc = ""] #[doc = " CONFIG SET parameter value [ parameter value ... ]"] #[doc = " https://valkey.io/commands/config-set/"] @@ -669,8 +675,7 @@ impl Guest for Commands { exchange(connection, ("CONFIG", "SET", parameters_and_values)).await } - #[doc = " Fails over from this primary to a replica, the one at the target, or aborts a failover in"] - #[doc = " progress."] + #[doc = " Starts a coordinated failover from a server to one of its replicas."] #[doc = ""] #[doc = " FAILOVER [ TO host port [ FORCE ] ] [ ABORT ] [ TIMEOUT milliseconds ]"] #[doc = " https://valkey.io/commands/failover/"] @@ -690,7 +695,7 @@ impl Guest for Commands { exchange(connection, command).await } - #[doc = " When the databases were last saved to disk."] + #[doc = " Returns the Unix timestamp of the last successful save to disk."] #[doc = ""] #[doc = " LASTSAVE"] #[doc = " https://valkey.io/commands/lastsave/"] @@ -699,7 +704,7 @@ impl Guest for Commands { exchange(connection, "LASTSAVE").await } - #[doc = " A report of the latency events, for people to read."] + #[doc = " Returns a human-readable latency analysis report."] #[doc = ""] #[doc = " LATENCY DOCTOR"] #[doc = " https://valkey.io/commands/latency-doctor/"] @@ -708,7 +713,7 @@ impl Guest for Commands { exchange(connection, ("LATENCY", "DOCTOR")).await } - #[doc = " A graph of the latency of the event, as text."] + #[doc = " Returns a latency graph for an event."] #[doc = ""] #[doc = " LATENCY GRAPH event"] #[doc = " https://valkey.io/commands/latency-graph/"] @@ -717,7 +722,7 @@ impl Guest for Commands { exchange(connection, ("LATENCY", "GRAPH", event)).await } - #[doc = " The latency histogram of the commands, all of them without commands, by command name."] + #[doc = " Returns the cumulative distribution of latencies of a subset or all commands."] #[doc = ""] #[doc = " LATENCY HISTOGRAM [ command [ command ... ] ]"] #[doc = " https://valkey.io/commands/latency-histogram/"] @@ -737,7 +742,7 @@ impl Guest for Commands { .await } - #[doc = " The samples of the latency of the event, when each was taken and the latency."] + #[doc = " Returns timestamp-latency samples for an event."] #[doc = ""] #[doc = " LATENCY HISTORY event"] #[doc = " https://valkey.io/commands/latency-history/"] @@ -749,7 +754,7 @@ impl Guest for Commands { exchange(connection, ("LATENCY", "HISTORY", event)).await } - #[doc = " The latest sample of the latency of each event."] + #[doc = " Returns the latest latency samples for all events."] #[doc = ""] #[doc = " LATENCY LATEST"] #[doc = " https://valkey.io/commands/latency-latest/"] @@ -758,7 +763,7 @@ impl Guest for Commands { exchange(connection, ("LATENCY", "LATEST")).await } - #[doc = " Clears the samples of the events, all of them without events, returning how many events."] + #[doc = " Resets the latency data for one or more events."] #[doc = ""] #[doc = " LATENCY RESET [ event [ event ... ] ]"] #[doc = " https://valkey.io/commands/latency-reset/"] @@ -774,7 +779,7 @@ impl Guest for Commands { .await } - #[doc = " The loaded modules."] + #[doc = " Returns all loaded modules."] #[doc = ""] #[doc = " MODULE LIST"] #[doc = " https://valkey.io/commands/module-list/"] @@ -783,7 +788,7 @@ impl Guest for Commands { exchange(connection, ("MODULE", "LIST")).await } - #[doc = " Loads the module at the path, with the arguments."] + #[doc = " Loads a module."] #[doc = ""] #[doc = " MODULE LOAD path [ arg [ arg ... ] ]"] #[doc = " https://valkey.io/commands/module-load/"] @@ -800,7 +805,7 @@ impl Guest for Commands { .await } - #[doc = " Loads the module at the path, with the configuration parameters and arguments."] + #[doc = " Loads a module using extended parameters."] #[doc = ""] #[doc = " MODULE LOADEX path [ CONFIG name value [ CONFIG name value ... ] ] [ ARGS args [ args ... ] ]"] #[doc = " https://valkey.io/commands/module-loadex/"] @@ -823,7 +828,7 @@ impl Guest for Commands { exchange(connection, ("MODULE", "LOADEX", path, configs, args)).await } - #[doc = " Unloads the module, by name."] + #[doc = " Unloads a module."] #[doc = ""] #[doc = " MODULE UNLOAD name"] #[doc = " https://valkey.io/commands/module-unload/"] @@ -832,8 +837,9 @@ impl Guest for Commands { exchange(connection, ("MODULE", "UNLOAD", name)).await } - #[doc = " Makes the server a replica of the primary at the host and port, or with none, a primary,"] - #[doc = " `NO ONE`, returning the server's message."] + #[doc = " Configures a server as replica of another, or promotes it to a primary."] + #[doc = ""] + #[doc = " None makes the server a primary, `NO ONE`."] #[doc = ""] #[doc = " REPLICAOF 〈 host port | NO ONE 〉"] #[doc = " https://valkey.io/commands/replicaof/"] @@ -850,7 +856,7 @@ impl Guest for Commands { exchange(connection, ("REPLICAOF", primary)).await } - #[doc = " The server's role in replication, and its state."] + #[doc = " Returns the replication role."] #[doc = ""] #[doc = " ROLE"] #[doc = " https://valkey.io/commands/role/"] @@ -859,7 +865,7 @@ impl Guest for Commands { exchange(connection, "ROLE").await } - #[doc = " Saves the databases to disk, blocking the server until done."] + #[doc = " Synchronously saves the database(s) to disk."] #[doc = ""] #[doc = " SAVE"] #[doc = " https://valkey.io/commands/save/"] @@ -868,7 +874,7 @@ impl Guest for Commands { exchange(connection, "SAVE").await } - #[doc = " Stops the server, saving to disk as configured, or as the save mode says."] + #[doc = " Synchronously saves the database(s) to disk and shuts down the server."] #[doc = ""] #[doc = " The server closes the connection once it stops, so a shutdown that succeeds fails the call"] #[doc = " with a closed error."] @@ -894,7 +900,9 @@ impl Guest for Commands { exchange(connection, command).await } - #[doc = " Aborts a shutdown in progress."] + #[doc = " Synchronously saves the database(s) to disk and shuts down the server."] + #[doc = ""] + #[doc = " The `ABORT` form, aborting a shutdown in progress."] #[doc = ""] #[doc = " See also `shutdown`."] #[doc = ""] @@ -905,7 +913,7 @@ impl Guest for Commands { exchange(connection, ("SHUTDOWN", "ABORT")).await } - #[doc = " The entries of the slow log, up to the count, 10 without one, all of them for -1."] + #[doc = " Returns the slow log's entries."] #[doc = ""] #[doc = " SLOWLOG GET [ count ]"] #[doc = " https://valkey.io/commands/slowlog-get/"] @@ -917,7 +925,7 @@ impl Guest for Commands { exchange(connection, ("SLOWLOG", "GET", count)).await } - #[doc = " The number of entries in the slow log."] + #[doc = " Returns the number of entries in the slow log."] #[doc = ""] #[doc = " SLOWLOG LEN"] #[doc = " https://valkey.io/commands/slowlog-len/"] @@ -926,7 +934,7 @@ impl Guest for Commands { exchange(connection, ("SLOWLOG", "LEN")).await } - #[doc = " Clears the slow log."] + #[doc = " Clears all entries from the slow log."] #[doc = ""] #[doc = " SLOWLOG RESET"] #[doc = " https://valkey.io/commands/slowlog-reset/"] @@ -935,8 +943,7 @@ impl Guest for Commands { exchange(connection, ("SLOWLOG", "RESET")).await } - #[doc = " The number of bits set to 1 in the string stored at key, in the range, 0 when the key does"] - #[doc = " not exist."] + #[doc = " Counts the number of set bits (population counting) in a string."] #[doc = ""] #[doc = " BITCOUNT key [ start [ end [ BYTE | BIT ] ] ]"] #[doc = " https://valkey.io/commands/bitcount/"] @@ -949,10 +956,9 @@ impl Guest for Commands { exchange(connection, ("BITCOUNT", key, range)).await } - #[doc = " Reads and writes integer fields of the string stored at key, returning the result of each"] - #[doc = " operation in order, none for a write that failed with `OVERFLOW FAIL`."] + #[doc = " Performs arbitrary bitfield integer operations on strings."] #[doc = ""] - #[doc = " See also `bitfield-ro`."] + #[doc = " None for a write that failed with `OVERFLOW FAIL`."] #[doc = ""] #[doc = " BITFIELD key [ GET encoding offset | [ OVERFLOW WRAP | SAT | FAIL ] 〈 SET encoding offset value | INCRBY encoding offset increment 〉 [ GET encoding offset | [ OVERFLOW WRAP | SAT | FAIL ] 〈 SET encoding offset value | INCRBY encoding offset increment 〉 ... ] ]"] #[doc = " https://valkey.io/commands/bitfield/"] @@ -973,7 +979,7 @@ impl Guest for Commands { .await } - #[doc = " Reads integer fields of the string stored at key, the read-only variant of `bitfield`."] + #[doc = " Performs arbitrary read-only bitfield integer operations on strings."] #[doc = ""] #[doc = " BITFIELD_RO key [ GET encoding offset [ GET encoding offset ... ] ]"] #[doc = " https://valkey.io/commands/bitfield_ro/"] @@ -990,8 +996,7 @@ impl Guest for Commands { .await } - #[doc = " Stores the bitwise operation of the strings stored at the source keys in the destination,"] - #[doc = " returning its length, the length of the longest source."] + #[doc = " Performs bitwise operations on multiple strings, and stores the result."] #[doc = ""] #[doc = " BITOP 〈 AND | OR | XOR | NOT 〉 destkey key [ key ... ]"] #[doc = " https://valkey.io/commands/bitop/"] @@ -1015,8 +1020,9 @@ impl Guest for Commands { .await } - #[doc = " The position of the first bit set to the bit in the string stored at key, in the range,"] - #[doc = " none when there is none."] + #[doc = " Finds the first set (1) or clear (0) bit in a string."] + #[doc = ""] + #[doc = " None when no bit matches, the server's `-1`."] #[doc = ""] #[doc = " BITPOS key bit [ start [ end [ BYTE | BIT ] ] ]"] #[doc = " https://valkey.io/commands/bitpos/"] @@ -1033,8 +1039,7 @@ impl Guest for Commands { Ok(position.0) } - #[doc = " The bit at the offset of the string stored at key, false beyond its end, or when the key"] - #[doc = " does not exist."] + #[doc = " Returns a bit value by offset."] #[doc = ""] #[doc = " GETBIT key offset"] #[doc = " https://valkey.io/commands/getbit/"] @@ -1043,8 +1048,7 @@ impl Guest for Commands { exchange(connection, ("GETBIT", key, offset)).await } - #[doc = " Sets the bit at the offset of the string stored at key, growing it as needed, returning the"] - #[doc = " previous bit."] + #[doc = " Sets or clears the bit at offset of the string value."] #[doc = ""] #[doc = " SETBIT key offset value"] #[doc = " https://valkey.io/commands/setbit/"] @@ -1058,7 +1062,7 @@ impl Guest for Commands { exchange(connection, ("SETBIT", key, offset, Bit(value))).await } - #[doc = " Authenticates the current connection."] + #[doc = " Authenticates the connection."] #[doc = ""] #[doc = " AUTH [ username ] password"] #[doc = " https://valkey.io/commands/auth/"] @@ -1071,7 +1075,7 @@ impl Guest for Commands { exchange(connection, ("AUTH", username, password)).await } - #[doc = " Returns PONG if no argument is provided, otherwise return a copy of the argument as a bulk."] + #[doc = " Returns the server's liveliness response."] #[doc = ""] #[doc = " PING [ message ]"] #[doc = " https://valkey.io/commands/ping/"] @@ -1085,7 +1089,7 @@ impl Guest for Commands { } } - #[doc = " Signals that the next command follows an ASK redirect, for a slot being imported."] + #[doc = " Signals that a cluster client is following an -ASK redirect."] #[doc = ""] #[doc = " It applies to the next command on the connection, which may be another caller's when the"] #[doc = " connection is shared. Use a connection of your own."] @@ -1097,8 +1101,7 @@ impl Guest for Commands { exchange(connection, "ASKING").await } - #[doc = " Whether the keys the next command reads are tracked, with tracking in `optin` or `optout`"] - #[doc = " mode."] + #[doc = " Instructs the server whether to track the keys in the next request."] #[doc = ""] #[doc = " It applies to the next command on the connection, which may be another caller's when the"] #[doc = " connection is shared. Use a connection of your own."] @@ -1114,7 +1117,7 @@ impl Guest for Commands { .await } - #[doc = " Declares the capabilities the client supports, e.g. `redirect`."] + #[doc = " A client claims its capability."] #[doc = ""] #[doc = " CLIENT CAPA capability [ capability ... ]"] #[doc = " https://valkey.io/commands/client-capa/"] @@ -1131,7 +1134,7 @@ impl Guest for Commands { .await } - #[doc = " The name of the connection, none when it has none."] + #[doc = " Returns the name of the connection."] #[doc = ""] #[doc = " CLIENT GETNAME"] #[doc = " https://valkey.io/commands/client-getname/"] @@ -1140,7 +1143,7 @@ impl Guest for Commands { exchange(connection, ("CLIENT", "GETNAME")).await } - #[doc = " The ID of the connection, unique until the server restarts."] + #[doc = " Returns the unique client ID of the connection."] #[doc = ""] #[doc = " CLIENT ID"] #[doc = " https://valkey.io/commands/client-id/"] @@ -1149,8 +1152,7 @@ impl Guest for Commands { exchange(connection, ("CLIENT", "ID")).await } - #[doc = " Whether the connection is an import source, which may write while the server is in import"] - #[doc = " mode."] + #[doc = " Marks this client as an import source when the server is in import mode."] #[doc = ""] #[doc = " CLIENT IMPORT-SOURCE 〈 ON | OFF 〉"] #[doc = " https://valkey.io/commands/client-import-source/"] @@ -1159,7 +1161,9 @@ impl Guest for Commands { exchange(connection, ("CLIENT", "IMPORT-SOURCE", OnOff(enabled))).await } - #[doc = " The fields of the connection, as CLIENT LIST has them, e.g. `id`, `addr` and `name`."] + #[doc = " Returns information about the connection."] + #[doc = ""] + #[doc = " A map of the fields, parsed from the server's text, e.g. `id`, `addr` and `name`."] #[doc = ""] #[doc = " CLIENT INFO"] #[doc = " https://valkey.io/commands/client-info/"] @@ -1174,7 +1178,7 @@ impl Guest for Commands { .ok_or_else(|| Error::Client("unexpected empty reply".to_string())) } - #[doc = " Whether the connection's commands leave the access time of the keys they read unchanged."] + #[doc = " Controls whether commands sent by the client affect the LRU/LFU of accessed keys."] #[doc = ""] #[doc = " It applies to the connection, so to every caller sharing it."] #[doc = ""] @@ -1185,7 +1189,7 @@ impl Guest for Commands { exchange(connection, ("CLIENT", "NO-TOUCH", OnOff(enabled))).await } - #[doc = " Sets the name or version of the library of the connection, see CLIENT LIST."] + #[doc = " Sets information specific to the client or connection."] #[doc = ""] #[doc = " CLIENT SETINFO 〈 LIB-NAME libname | LIB-VER libver 〉"] #[doc = " https://valkey.io/commands/client-setinfo/"] @@ -1194,7 +1198,7 @@ impl Guest for Commands { exchange(connection, ("CLIENT", "SETINFO", attr)).await } - #[doc = " Sets the name of the connection."] + #[doc = " Sets the connection name."] #[doc = ""] #[doc = " CLIENT SETNAME connection-name"] #[doc = " https://valkey.io/commands/client-setname/"] @@ -1203,10 +1207,10 @@ impl Guest for Commands { exchange(connection, ("CLIENT", "SETNAME", connection_name)).await } - #[doc = " Turns tracking on with the options, or off with none, for server assisted client side"] - #[doc = " caching. Invalidations arrive as push messages, see `connection.dead-letters`."] + #[doc = " Controls server-assisted client-side caching for the connection."] #[doc = ""] - #[doc = " It applies to the connection, so to every caller sharing it."] + #[doc = " Invalidations arrive as push messages, see `connection.dead-letters`. It applies to the"] + #[doc = " connection, so to every caller sharing it."] #[doc = ""] #[doc = " CLIENT TRACKING 〈 ON | OFF 〉 [ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN ] [ OPTOUT ] [ NOLOOP ]"] #[doc = " https://valkey.io/commands/client-tracking/"] @@ -1220,7 +1224,7 @@ impl Guest for Commands { exchange(connection, ("CLIENT", "TRACKING", status, options)).await } - #[doc = " The connection's tracking state."] + #[doc = " Returns information about server-assisted client-side caching for the connection."] #[doc = ""] #[doc = " CLIENT TRACKINGINFO"] #[doc = " https://valkey.io/commands/client-trackinginfo/"] @@ -1231,7 +1235,7 @@ impl Guest for Commands { exchange(connection, ("CLIENT", "TRACKINGINFO")).await } - #[doc = " The number of commands of the server."] + #[doc = " Returns a count of commands."] #[doc = ""] #[doc = " COMMAND COUNT"] #[doc = " https://valkey.io/commands/command-count/"] @@ -1240,7 +1244,7 @@ impl Guest for Commands { exchange(connection, ("COMMAND", "COUNT")).await } - #[doc = " Every command of the server, a container followed by its subcommands."] + #[doc = " Returns detailed information about all commands."] #[doc = ""] #[doc = " COMMAND"] #[doc = " https://valkey.io/commands/command/"] @@ -1250,8 +1254,9 @@ impl Guest for Commands { Ok(commands.0) } - #[doc = " The documentation of the commands, all of them without names, a container followed by its"] - #[doc = " subcommands, commands that do not exist left out."] + #[doc = " Returns documentary information about one, multiple or all commands."] + #[doc = ""] + #[doc = " A container is followed by its subcommands. Commands that do not exist are left out."] #[doc = ""] #[doc = " COMMAND DOCS [ command-name [ command-name ... ] ]"] #[doc = " https://valkey.io/commands/command-docs/"] @@ -1266,8 +1271,9 @@ impl Guest for Commands { Ok(commands.0) } - #[doc = " The commands, all of them without names, a container followed by its subcommands, commands"] - #[doc = " that do not exist left out."] + #[doc = " Returns information about one, multiple or all commands."] + #[doc = ""] + #[doc = " A container is followed by its subcommands. Commands that do not exist are left out."] #[doc = ""] #[doc = " COMMAND INFO [ command-name [ command-name ... ] ]"] #[doc = " https://valkey.io/commands/command-info/"] @@ -1282,7 +1288,7 @@ impl Guest for Commands { Ok(commands.0) } - #[doc = " The keys of the command, its name and arguments."] + #[doc = " Extracts the key names from an arbitrary command."] #[doc = ""] #[doc = " COMMAND GETKEYS command [ arg [ arg ... ] ]"] #[doc = " https://valkey.io/commands/command-getkeys/"] @@ -1298,8 +1304,7 @@ impl Guest for Commands { .await } - #[doc = " The keys of the command, its name and arguments, and how the command uses each, e.g. `RW`"] - #[doc = " and `access`."] + #[doc = " Extracts the key names and access flags for an arbitrary command."] #[doc = ""] #[doc = " COMMAND GETKEYSANDFLAGS command [ arg [ arg ... ] ]"] #[doc = " https://valkey.io/commands/command-getkeysandflags/"] @@ -1319,7 +1324,7 @@ impl Guest for Commands { .await } - #[doc = " The names of the commands of the server, those that match the filter with one."] + #[doc = " Returns a list of command names."] #[doc = ""] #[doc = " COMMAND LIST [ FILTERBY 〈 MODULE module-name | ACLCAT category | PATTERN pattern 〉 ]"] #[doc = " https://valkey.io/commands/command-list/"] @@ -1331,7 +1336,7 @@ impl Guest for Commands { exchange(connection, ("COMMAND", "LIST", filter)).await } - #[doc = " Returns the message."] + #[doc = " Returns the given string."] #[doc = ""] #[doc = " ECHO message"] #[doc = " https://valkey.io/commands/echo/"] @@ -1340,7 +1345,7 @@ impl Guest for Commands { exchange(connection, ("ECHO", message)).await } - #[doc = " Allows the connection to read from a replica of a cluster, for the slots of its primary."] + #[doc = " Enables read-only queries for a connection to a Valkey replica node."] #[doc = ""] #[doc = " It applies to the connection, so to every caller sharing it."] #[doc = ""] @@ -1351,7 +1356,7 @@ impl Guest for Commands { exchange(connection, "READONLY").await } - #[doc = " Disallows the connection to read from a replica of a cluster, the default."] + #[doc = " Enables read-write queries for a connection to a Valkey replica node."] #[doc = ""] #[doc = " It applies to the connection, so to every caller sharing it."] #[doc = ""] @@ -1362,7 +1367,7 @@ impl Guest for Commands { exchange(connection, "READWRITE").await } - #[doc = " Changes the database of the connection."] + #[doc = " Changes the selected database."] #[doc = ""] #[doc = " It applies to the connection, so to every caller sharing it."] #[doc = ""] @@ -1373,8 +1378,7 @@ impl Guest for Commands { exchange(connection, ("SELECT", index)).await } - #[doc = " Waits until the connection's writes reached the number of replicas, or the timeout passed,"] - #[doc = " returning how many replicas they reached. A timeout of zero waits indefinitely."] + #[doc = " Blocks until the asynchronous replication of all preceding write commands sent by the connection is completed."] #[doc = ""] #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] #[doc = " it. Use a connection of your own."] @@ -1390,9 +1394,7 @@ impl Guest for Commands { exchange(connection, ("WAIT", numreplicas, timeout)).await } - #[doc = " Waits until the connection's writes are in the append-only file of the primary, and of the"] - #[doc = " number of replicas, or the timeout passed, returning how many of each they reached. A timeout"] - #[doc = " of zero waits indefinitely."] + #[doc = " Blocks until all of the preceding write commands sent by the connection are written to the append-only file of the primary and/or replicas."] #[doc = ""] #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] #[doc = " it. Use a connection of your own."] @@ -1409,8 +1411,7 @@ impl Guest for Commands { exchange(connection, ("WAITAOF", numlocal, numreplicas, timeout)).await } - #[doc = " Adds the members at their positions to the geospatial index stored at key, or updates their"] - #[doc = " positions, returning how many were added, or with `changed`, added or updated."] + #[doc = " Adds one or more members to a geospatial index."] #[doc = ""] #[doc = " GEOADD key [ NX | XX ] [ CH ] longitude latitude member [ longitude latitude member ... ]"] #[doc = " https://valkey.io/commands/geoadd/"] @@ -1435,7 +1436,7 @@ impl Guest for Commands { .await } - #[doc = " The distance between the members, in meters or the unit, none when either is not a member."] + #[doc = " Returns the distance between two members of a geospatial index."] #[doc = ""] #[doc = " GEODIST key member1 member2 [ m | km | ft | mi ]"] #[doc = " https://valkey.io/commands/geodist/"] @@ -1450,7 +1451,7 @@ impl Guest for Commands { exchange(connection, ("GEODIST", key, member1, member2, unit)).await } - #[doc = " The geohash of each of the members, none for a member that is not in the index."] + #[doc = " Returns members from a geospatial index as geohash strings."] #[doc = ""] #[doc = " GEOHASH key [ member [ member ... ] ]"] #[doc = " https://valkey.io/commands/geohash/"] @@ -1467,7 +1468,7 @@ impl Guest for Commands { .await } - #[doc = " The position of each of the members, none for a member that is not in the index."] + #[doc = " Returns the longitude and latitude of members from a geospatial index."] #[doc = ""] #[doc = " GEOPOS key [ member [ member ... ] ]"] #[doc = " https://valkey.io/commands/geopos/"] @@ -1484,7 +1485,7 @@ impl Guest for Commands { .await } - #[doc = " The members of the geospatial index stored at key in the shape."] + #[doc = " Queries a geospatial index for members inside an area of a box, circle, or a polygon."] #[doc = ""] #[doc = " See also `geosearch-with`."] #[doc = ""] @@ -1501,7 +1502,9 @@ impl Guest for Commands { exchange(connection, ("GEOSEARCH", key, shape, order, count)).await } - #[doc = " The members of the geospatial index stored at key in the shape, with the details asked for."] + #[doc = " Queries a geospatial index for members inside an area of a box, circle, or a polygon."] + #[doc = ""] + #[doc = " Returns each member with the details asked for, `WITHCOORD`, `WITHDIST` and `WITHHASH`."] #[doc = ""] #[doc = " See also `geosearch`."] #[doc = ""] @@ -1529,9 +1532,7 @@ impl Guest for Commands { exchange(connection, command).await } - #[doc = " Stores the members of the geospatial index stored at source in the shape, returning how"] - #[doc = " many, with their positions, or with `storedist`, their distance from the center as the"] - #[doc = " score."] + #[doc = " Queries a geospatial index for members inside an area of a box, a circle, or a polygon, optionally stores the result."] #[doc = ""] #[doc = " GEOSEARCHSTORE destination source [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] [ STOREDIST ]"] #[doc = " https://valkey.io/commands/geosearchstore/"] @@ -1560,7 +1561,7 @@ impl Guest for Commands { .await } - #[doc = " Removes the specified fields from the hash stored at key."] + #[doc = " Deletes one or more fields and their values from a hash."] #[doc = ""] #[doc = " HDEL key field [ field ... ]"] #[doc = " https://valkey.io/commands/hdel/"] @@ -1573,7 +1574,7 @@ impl Guest for Commands { exchange(connection, ("HDEL", key, NonEmpty::new("fields", fields))).await } - #[doc = " Returns if field is an existing field in the hash stored at key."] + #[doc = " Determines whether a field exists in a hash."] #[doc = ""] #[doc = " HEXISTS key field"] #[doc = " https://valkey.io/commands/hexists/"] @@ -1582,9 +1583,7 @@ impl Guest for Commands { exchange(connection, ("HEXISTS", key, field)).await } - #[doc = " When each of the hash fields expires, to the second."] - #[doc = ""] - #[doc = " See also `hpexpiretime`."] + #[doc = " Returns Unix timestamps in seconds since the epoch at which the given key's field(s) will expire."] #[doc = ""] #[doc = " HEXPIRETIME key FIELDS numfields field [ field ... ]"] #[doc = " https://valkey.io/commands/hexpiretime/"] @@ -1601,7 +1600,7 @@ impl Guest for Commands { .await } - #[doc = " The values of the hash fields, none for a field that does not exist, deleting the fields."] + #[doc = " Returns the values of one or more fields and deletes them from a hash."] #[doc = ""] #[doc = " HGETDEL key FIELDS numfields field [ field ... ]"] #[doc = " https://valkey.io/commands/hgetdel/"] @@ -1618,8 +1617,7 @@ impl Guest for Commands { .await } - #[doc = " The values of the hash fields, none for a field that does not exist, optionally setting or"] - #[doc = " removing their expiration."] + #[doc = " Gets the value of one or more fields of a given hash key, and optionally sets their expiration time or time-to-live (TTL)."] #[doc = ""] #[doc = " HGETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ] FIELDS numfields field [ field ... ]"] #[doc = " https://valkey.io/commands/hgetex/"] @@ -1637,7 +1635,7 @@ impl Guest for Commands { .await } - #[doc = " Removes the expiration of the hash fields."] + #[doc = " Remove the existing expiration on a hash key's field(s)."] #[doc = ""] #[doc = " HPERSIST key FIELDS numfields field [ field ... ]"] #[doc = " https://valkey.io/commands/hpersist/"] @@ -1654,7 +1652,7 @@ impl Guest for Commands { .await } - #[doc = " Manipulate the expiration time for existing hash fields, in milliseconds."] + #[doc = " Sets the expiration time of one or more hash fields in milliseconds."] #[doc = ""] #[doc = " HPEXPIRE key milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ]"] #[doc = " https://valkey.io/commands/hpexpire/"] @@ -1676,7 +1674,7 @@ impl Guest for Commands { exchange(connection, command).await } - #[doc = " Manipulate the expiration time for existing hash fields, as a Unix time in milliseconds."] + #[doc = " Sets the expiration time of one or more hash fields to a Unix timestamp in milliseconds."] #[doc = ""] #[doc = " HPEXPIREAT key unix-time-milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ]"] #[doc = " https://valkey.io/commands/hpexpireat/"] @@ -1698,9 +1696,7 @@ impl Guest for Commands { exchange(connection, command).await } - #[doc = " When each of the hash fields expires, to the millisecond."] - #[doc = ""] - #[doc = " See also `hexpiretime`."] + #[doc = " Returns the Unix timestamp in milliseconds since Unix epoch at which the given key's field(s) will expire."] #[doc = ""] #[doc = " HPEXPIRETIME key FIELDS numfields field [ field ... ]"] #[doc = " https://valkey.io/commands/hpexpiretime/"] @@ -1717,9 +1713,7 @@ impl Guest for Commands { .await } - #[doc = " The remaining time to live of each of the hash fields, to the millisecond."] - #[doc = ""] - #[doc = " See also `httl`."] + #[doc = " Returns the remaining time to live in milliseconds of a hash key's field(s) that have an associated expiration."] #[doc = ""] #[doc = " HPTTL key FIELDS numfields field [ field ... ]"] #[doc = " https://valkey.io/commands/hpttl/"] @@ -1736,8 +1730,7 @@ impl Guest for Commands { .await } - #[doc = " Sets the fields to their values in the hash stored at key, optionally setting their"] - #[doc = " expiration, returning whether they were set, false when a condition prevented it."] + #[doc = " Sets the value of one or more fields of a given hash key, and optionally sets their expiration time."] #[doc = ""] #[doc = " HSETEX key [ NX | XX ] [ FNX | FXX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] FIELDS numfields field value [ field value ... ]"] #[doc = " https://valkey.io/commands/hsetex/"] @@ -1761,9 +1754,7 @@ impl Guest for Commands { exchange(connection, command).await } - #[doc = " The remaining time to live of each of the hash fields, to the second."] - #[doc = ""] - #[doc = " See also `hpttl`."] + #[doc = " Returns the remaining time to live (in seconds) of a hash key's field(s) that have an associated expiration."] #[doc = ""] #[doc = " HTTL key FIELDS numfields field [ field ... ]"] #[doc = " https://valkey.io/commands/httl/"] @@ -1780,7 +1771,7 @@ impl Guest for Commands { .await } - #[doc = " Manipulate the expiration time for existing hash fields."] + #[doc = " Sets the expiration time of one or more hash fields in seconds."] #[doc = ""] #[doc = " HEXPIRE key seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ]"] #[doc = " https://valkey.io/commands/hexpire/"] @@ -1805,7 +1796,7 @@ impl Guest for Commands { .await } - #[doc = " Manipulate the expiration time for existing hash fields."] + #[doc = " Sets the expiration time of one or more hash fields to a Unix timestamp."] #[doc = ""] #[doc = " HEXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ]"] #[doc = " https://valkey.io/commands/hexpireat/"] @@ -1830,7 +1821,7 @@ impl Guest for Commands { .await } - #[doc = " Returns the value associated with field in the hash stored at key."] + #[doc = " Returns the value of a field in a hash."] #[doc = ""] #[doc = " HGET key field"] #[doc = " https://valkey.io/commands/hget/"] @@ -1843,7 +1834,7 @@ impl Guest for Commands { exchange(connection, ("HGET", key, field)).await } - #[doc = " Returns all fields and values of the hash stored at key."] + #[doc = " Returns all fields and values in a hash."] #[doc = ""] #[doc = " HGETALL key"] #[doc = " https://valkey.io/commands/hgetall/"] @@ -1855,7 +1846,7 @@ impl Guest for Commands { exchange(connection, ("HGETALL", key)).await } - #[doc = " Increments the number stored at field in the hash stored at key by increment."] + #[doc = " Increments the integer value of a field in a hash by a number."] #[doc = ""] #[doc = " HINCRBY key field increment"] #[doc = " https://valkey.io/commands/hincrby/"] @@ -1869,7 +1860,7 @@ impl Guest for Commands { exchange(connection, ("HINCRBY", key, field, increment)).await } - #[doc = " Increment the specified field of a hash stored at key, and representing a floating point number, by the specified increment."] + #[doc = " Increments the floating point value of a field by a number."] #[doc = ""] #[doc = " HINCRBYFLOAT key field increment"] #[doc = " https://valkey.io/commands/hincrbyfloat/"] @@ -1883,7 +1874,7 @@ impl Guest for Commands { exchange(connection, ("HINCRBYFLOAT", key, field, increment)).await } - #[doc = " Returns all field names in the hash stored at key."] + #[doc = " Returns all fields in a hash."] #[doc = ""] #[doc = " HKEYS key"] #[doc = " https://valkey.io/commands/hkeys/"] @@ -1892,7 +1883,7 @@ impl Guest for Commands { exchange(connection, ("HKEYS", key)).await } - #[doc = " Returns the number of fields contained in the hash stored at key."] + #[doc = " Returns the number of fields in a hash."] #[doc = ""] #[doc = " HLEN key"] #[doc = " https://valkey.io/commands/hlen/"] @@ -1901,7 +1892,7 @@ impl Guest for Commands { exchange(connection, ("HLEN", key)).await } - #[doc = " Returns the values associated with the specified fields in the hash stored at key."] + #[doc = " Returns the values of all fields in a hash."] #[doc = ""] #[doc = " HMGET key field [ field ... ]"] #[doc = " https://valkey.io/commands/hmget/"] @@ -1914,7 +1905,7 @@ impl Guest for Commands { exchange(connection, ("HMGET", key, NonEmpty::new("fields", fields))).await } - #[doc = " Sets the specified fields to their respective values in the hash stored at key."] + #[doc = " Sets the values of multiple fields."] #[doc = ""] #[doc = " HMSET key field value [ field value ... ]"] #[doc = " https://valkey.io/commands/hmset/"] @@ -1935,12 +1926,11 @@ impl Guest for Commands { .await } - #[doc = " Random fields from the hash value stored at key, up to the count."] + #[doc = " Returns one or more random fields from a hash."] #[doc = ""] - #[doc = " Without a count, one field, or none when the key does not exist. A negative count may return"] - #[doc = " the same field more than once, its absolute value is the number of fields returned."] + #[doc = " Without a count, at most one field."] #[doc = ""] - #[doc = " Also see `hrandfield-withvalues`."] + #[doc = " See also `hrandfield-withvalues`."] #[doc = ""] #[doc = " HRANDFIELD key [ count ]"] #[doc = " https://valkey.io/commands/hrandfield/"] @@ -1958,12 +1948,11 @@ impl Guest for Commands { Ok(field.into_iter().collect()) } - #[doc = " Random fields and their values from the hash value stored at key, up to the count."] + #[doc = " Returns one or more random fields from a hash."] #[doc = ""] - #[doc = " A negative count may return the same field more than once, its absolute value is the number of"] - #[doc = " fields returned."] + #[doc = " The `WITHVALUES` form, the fields and their values."] #[doc = ""] - #[doc = " Also see `hrandfield`."] + #[doc = " See also `hrandfield`."] #[doc = ""] #[doc = " HRANDFIELD key count WITHVALUES"] #[doc = " https://valkey.io/commands/hrandfield/"] @@ -1976,7 +1965,7 @@ impl Guest for Commands { exchange(connection, ("HRANDFIELD", key, count, "WITHVALUES")).await } - #[doc = " Incrementally iterate over a collection of elements."] + #[doc = " Iterates over fields and values of a hash."] #[doc = ""] #[doc = " See also `hscan-novalues`."] #[doc = ""] @@ -2004,7 +1993,9 @@ impl Guest for Commands { Ok((cursor.0, items)) } - #[doc = " Incrementally iterate over a collection of keys."] + #[doc = " Iterates over fields and values of a hash."] + #[doc = ""] + #[doc = " The `NOVALUES` form, the fields only."] #[doc = ""] #[doc = " See also `hscan`."] #[doc = ""] @@ -2033,7 +2024,7 @@ impl Guest for Commands { Ok((cursor.0, items)) } - #[doc = " Sets the specified fields to their respective values in the hash stored at key."] + #[doc = " Creates or modifies the value of a field in a hash."] #[doc = ""] #[doc = " HSET key field value [ field value ... ]"] #[doc = " https://valkey.io/commands/hset/"] @@ -2054,7 +2045,7 @@ impl Guest for Commands { .await } - #[doc = " Sets field in the hash stored at key to value, only if field does not yet exist."] + #[doc = " Sets the value of a field in a hash only when the field doesn't exist."] #[doc = ""] #[doc = " HSETNX key field value"] #[doc = " https://valkey.io/commands/hsetnx/"] @@ -2068,7 +2059,7 @@ impl Guest for Commands { exchange(connection, ("HSETNX", key, field, value)).await } - #[doc = " Returns the string length of the value associated with field in the hash stored at key."] + #[doc = " Returns the length of the value of a field."] #[doc = ""] #[doc = " HSTRLEN key field"] #[doc = " https://valkey.io/commands/hstrlen/"] @@ -2081,7 +2072,7 @@ impl Guest for Commands { exchange(connection, ("HSTRLEN", key, field)).await } - #[doc = " All values in the hash stored at key."] + #[doc = " Returns all values in a hash."] #[doc = ""] #[doc = " HVALS key"] #[doc = " https://valkey.io/commands/hvals/"] @@ -2090,10 +2081,7 @@ impl Guest for Commands { exchange(connection, ("HVALS", key)).await } - #[doc = " Adds the elements to the HyperLogLog stored at key, creating it when it does not exist,"] - #[doc = " returning whether its estimate may have changed."] - #[doc = ""] - #[doc = " Without elements, creates an empty HyperLogLog, true only when the key did not exist."] + #[doc = " Adds elements to a HyperLogLog key."] #[doc = ""] #[doc = " PFADD key [ element [ element ... ] ]"] #[doc = " https://valkey.io/commands/pfadd/"] @@ -2110,8 +2098,7 @@ impl Guest for Commands { .await } - #[doc = " The approximate number of unique elements added to the HyperLogLogs, of their union for"] - #[doc = " more than one, 0 for a key that does not exist."] + #[doc = " Returns the approximated cardinality of the set(s) observed by the HyperLogLog key(s)."] #[doc = ""] #[doc = " PFCOUNT key [ key ... ]"] #[doc = " https://valkey.io/commands/pfcount/"] @@ -2120,10 +2107,7 @@ impl Guest for Commands { exchange(connection, ("PFCOUNT", NonEmpty::new("keys", keys))).await } - #[doc = " Merges the HyperLogLogs stored at the source keys into the one stored at destination,"] - #[doc = " creating it when it does not exist."] - #[doc = ""] - #[doc = " Without source keys, creates an empty HyperLogLog when destination does not exist."] + #[doc = " Merges one or more HyperLogLog values into a single key."] #[doc = ""] #[doc = " PFMERGE destkey [ sourcekey [ sourcekey ... ] ]"] #[doc = " https://valkey.io/commands/pfmerge/"] @@ -2144,13 +2128,10 @@ impl Guest for Commands { .await } - #[doc = " Moves an element from one end of the source list to an end of the destination list, waiting"] - #[doc = " up to the timeout for the source to have one, returning it."] - #[doc = ""] - #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] + #[doc = " Pops an element from a list, pushes it to another list and returns it."] #[doc = ""] - #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] - #[doc = " it. Use a connection of your own."] + #[doc = " Returns none when the timeout passed. The connection waits too, replies to commands sent"] + #[doc = " after it on the connection wait behind it. Use a connection of your own."] #[doc = ""] #[doc = " BLMOVE source destination 〈 LEFT | RIGHT 〉 〈 LEFT | RIGHT 〉 timeout"] #[doc = " https://valkey.io/commands/blmove/"] @@ -2177,13 +2158,10 @@ impl Guest for Commands { .await } - #[doc = " Removes elements from an end of the first non-empty list, waiting up to the timeout for one,"] - #[doc = " returning its key and the elements, one without a count."] + #[doc = " Pops the first element from one of multiple lists."] #[doc = ""] - #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] - #[doc = ""] - #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] - #[doc = " it. Use a connection of your own."] + #[doc = " Returns none when the timeout passed. The connection waits too, replies to commands sent"] + #[doc = " after it on the connection wait behind it. Use a connection of your own."] #[doc = ""] #[doc = " BLMPOP timeout numkeys key [ key ... ] 〈 LEFT | RIGHT 〉 [ COUNT count ]"] #[doc = " https://valkey.io/commands/blmpop/"] @@ -2208,13 +2186,10 @@ impl Guest for Commands { .await } - #[doc = " Removes the first element of the first non-empty list, waiting up to the timeout for one,"] - #[doc = " returning its key and the element."] + #[doc = " Removes and returns the first element in a list."] #[doc = ""] - #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] - #[doc = ""] - #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] - #[doc = " it. Use a connection of your own."] + #[doc = " Returns none when the timeout passed. The connection waits too, replies to commands sent"] + #[doc = " after it on the connection wait behind it. Use a connection of your own."] #[doc = ""] #[doc = " BLPOP key [ key ... ] timeout"] #[doc = " https://valkey.io/commands/blpop/"] @@ -2235,13 +2210,10 @@ impl Guest for Commands { .await } - #[doc = " Removes the last element of the first non-empty list, waiting up to the timeout for one,"] - #[doc = " returning its key and the element."] + #[doc = " Removes and returns the last element in a list."] #[doc = ""] - #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] - #[doc = ""] - #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] - #[doc = " it. Use a connection of your own."] + #[doc = " Returns none when the timeout passed. The connection waits too, replies to commands sent"] + #[doc = " after it on the connection wait behind it. Use a connection of your own."] #[doc = ""] #[doc = " BRPOP key [ key ... ] timeout"] #[doc = " https://valkey.io/commands/brpop/"] @@ -2262,15 +2234,10 @@ impl Guest for Commands { .await } - #[doc = " Moves the last element of the source list to the head of the destination list, waiting up"] - #[doc = " to the timeout for the source to have one, returning it."] - #[doc = ""] - #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] - #[doc = ""] - #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] - #[doc = " it. Use a connection of your own."] + #[doc = " Pops an element from a list, pushes it to another list and returns it."] #[doc = ""] - #[doc = " See also `blmove`."] + #[doc = " Returns none when the timeout passed. The connection waits too, replies to commands sent"] + #[doc = " after it on the connection wait behind it. Use a connection of your own."] #[doc = ""] #[doc = " BRPOPLPUSH source destination timeout"] #[doc = " https://valkey.io/commands/brpoplpush/"] @@ -2288,8 +2255,7 @@ impl Guest for Commands { .await } - #[doc = " The element at the index of the list stored at key, negative from the end, none when the"] - #[doc = " index is out of range."] + #[doc = " Returns an element from a list by its index."] #[doc = ""] #[doc = " LINDEX key index"] #[doc = " https://valkey.io/commands/lindex/"] @@ -2302,8 +2268,7 @@ impl Guest for Commands { exchange(connection, ("LINDEX", key, index)).await } - #[doc = " Inserts the element before or after the first occurrence of the pivot in the list stored at"] - #[doc = " key."] + #[doc = " Inserts an element before or after another element in a list."] #[doc = ""] #[doc = " LINSERT key 〈 BEFORE | AFTER 〉 pivot element"] #[doc = " https://valkey.io/commands/linsert/"] @@ -2318,7 +2283,7 @@ impl Guest for Commands { exchange(connection, ("LINSERT", key, position, pivot, element)).await } - #[doc = " The length of the list stored at key, 0 when the key does not exist."] + #[doc = " Returns the length of a list."] #[doc = ""] #[doc = " LLEN key"] #[doc = " https://valkey.io/commands/llen/"] @@ -2327,8 +2292,7 @@ impl Guest for Commands { exchange(connection, ("LLEN", key)).await } - #[doc = " Moves an element from one end of the source list to an end of the destination list,"] - #[doc = " returning it, none when the source does not exist."] + #[doc = " Returns an element after popping it from one list and pushing it to another."] #[doc = ""] #[doc = " LMOVE source destination 〈 LEFT | RIGHT 〉 〈 LEFT | RIGHT 〉"] #[doc = " https://valkey.io/commands/lmove/"] @@ -2347,8 +2311,7 @@ impl Guest for Commands { .await } - #[doc = " Removes elements from an end of the first non-empty list, returning its key and the"] - #[doc = " elements, one without a count, none when every list is empty."] + #[doc = " Returns multiple elements from a list after removing them."] #[doc = ""] #[doc = " LMPOP numkeys key [ key ... ] 〈 LEFT | RIGHT 〉 [ COUNT count ]"] #[doc = " https://valkey.io/commands/lmpop/"] @@ -2368,8 +2331,9 @@ impl Guest for Commands { exchange(connection, command).await } - #[doc = " Removes and returns the first elements of the list stored at key, up to the count, one"] - #[doc = " without a count, none when the key does not exist."] + #[doc = " Returns and removes one or more elements from the beginning of a list."] + #[doc = ""] + #[doc = " Without a count, at most one element."] #[doc = ""] #[doc = " LPOP key [ count ]"] #[doc = " https://valkey.io/commands/lpop/"] @@ -2394,11 +2358,9 @@ impl Guest for Commands { } } - #[doc = " The positions of the element in the list stored at key, up to the count, 0 for every match,"] - #[doc = " the first match without a count."] + #[doc = " Returns the index of matching elements in a list."] #[doc = ""] - #[doc = " The rank skips matches, the second match for 2, counting from the end when negative. The"] - #[doc = " max length limits how many elements are compared."] + #[doc = " Without a count, at most one position."] #[doc = ""] #[doc = " LPOS key element [ RANK rank ] [ COUNT num-matches ] [ MAXLEN len ]"] #[doc = " https://valkey.io/commands/lpos/"] @@ -2430,8 +2392,7 @@ impl Guest for Commands { } } - #[doc = " Inserts the elements at the head of the list stored at key, creating it when it does not"] - #[doc = " exist, returning its length after."] + #[doc = " Prepends one or more elements to a list."] #[doc = ""] #[doc = " LPUSH key element [ element ... ]"] #[doc = " https://valkey.io/commands/lpush/"] @@ -2448,8 +2409,7 @@ impl Guest for Commands { .await } - #[doc = " Inserts the elements at the head of the list stored at key, only when it exists, returning"] - #[doc = " its length after, 0 when it does not exist."] + #[doc = " Prepends one or more elements to a list only when the list exists."] #[doc = ""] #[doc = " LPUSHX key element [ element ... ]"] #[doc = " https://valkey.io/commands/lpushx/"] @@ -2466,8 +2426,7 @@ impl Guest for Commands { .await } - #[doc = " The elements of the list stored at key from the start to the stop, both included, negative"] - #[doc = " from the end."] + #[doc = " Returns a range of elements from a list."] #[doc = ""] #[doc = " LRANGE key start stop"] #[doc = " https://valkey.io/commands/lrange/"] @@ -2481,10 +2440,7 @@ impl Guest for Commands { exchange(connection, ("LRANGE", key, start, stop)).await } - #[doc = " Removes occurrences of the element from the list stored at key, returning how many."] - #[doc = ""] - #[doc = " A positive count removes up to that many from the head, a negative count from the tail, and"] - #[doc = " 0 removes them all."] + #[doc = " Removes elements from a list."] #[doc = ""] #[doc = " LREM key count element"] #[doc = " https://valkey.io/commands/lrem/"] @@ -2498,7 +2454,7 @@ impl Guest for Commands { exchange(connection, ("LREM", key, count, element)).await } - #[doc = " Sets the element at the index of the list stored at key, negative from the end."] + #[doc = " Sets the value of an element in a list by its index."] #[doc = ""] #[doc = " LSET key index element"] #[doc = " https://valkey.io/commands/lset/"] @@ -2512,8 +2468,7 @@ impl Guest for Commands { exchange(connection, ("LSET", key, index, element)).await } - #[doc = " Trims the list stored at key to the elements from the start to the stop, both included,"] - #[doc = " negative from the end."] + #[doc = " Removes elements from both ends of a list."] #[doc = ""] #[doc = " LTRIM key start stop"] #[doc = " https://valkey.io/commands/ltrim/"] @@ -2527,8 +2482,9 @@ impl Guest for Commands { exchange(connection, ("LTRIM", key, start, stop)).await } - #[doc = " Removes and returns the last elements of the list stored at key, up to the count, one"] - #[doc = " without a count, none when the key does not exist."] + #[doc = " Returns and removes one or more elements from the end of a list."] + #[doc = ""] + #[doc = " Without a count, at most one element."] #[doc = ""] #[doc = " RPOP key [ count ]"] #[doc = " https://valkey.io/commands/rpop/"] @@ -2553,10 +2509,7 @@ impl Guest for Commands { } } - #[doc = " Moves the last element of the source list to the head of the destination list, returning"] - #[doc = " it, none when the source does not exist."] - #[doc = ""] - #[doc = " See also `lmove`."] + #[doc = " Returns the last element of a list after removing and pushing it to another list."] #[doc = ""] #[doc = " RPOPLPUSH source destination"] #[doc = " https://valkey.io/commands/rpoplpush/"] @@ -2569,8 +2522,7 @@ impl Guest for Commands { exchange(connection, ("RPOPLPUSH", source, destination)).await } - #[doc = " Inserts the elements at the tail of the list stored at key, creating it when it does not"] - #[doc = " exist, returning its length after."] + #[doc = " Appends one or more elements to a list."] #[doc = ""] #[doc = " RPUSH key element [ element ... ]"] #[doc = " https://valkey.io/commands/rpush/"] @@ -2587,8 +2539,7 @@ impl Guest for Commands { .await } - #[doc = " Inserts the elements at the tail of the list stored at key, only when it exists, returning"] - #[doc = " its length after, 0 when it does not exist."] + #[doc = " Appends one or more elements to a list only when the list exists."] #[doc = ""] #[doc = " RPUSHX key element [ element ... ]"] #[doc = " https://valkey.io/commands/rpushx/"] @@ -2605,7 +2556,7 @@ impl Guest for Commands { .await } - #[doc = " Removes the specified keys."] + #[doc = " Deletes one or more keys."] #[doc = ""] #[doc = " DEL key [ key ... ]"] #[doc = " https://valkey.io/commands/del/"] @@ -2614,8 +2565,7 @@ impl Guest for Commands { exchange(connection, ("DEL", NonEmpty::new("keys", keys))).await } - #[doc = " The value of the key serialized in Valkey's own format, with its version and a checksum, see"] - #[doc = " `restore`, none when the key does not exist."] + #[doc = " Returns a serialized representation of the value stored at a key."] #[doc = ""] #[doc = " DUMP key"] #[doc = " https://valkey.io/commands/dump/"] @@ -2625,7 +2575,7 @@ impl Guest for Commands { Ok(serialized.map(|serialized| serialized.0)) } - #[doc = " If key exists."] + #[doc = " Determines whether one or more keys exist."] #[doc = ""] #[doc = " EXISTS key [ key ... ]"] #[doc = " https://valkey.io/commands/exists/"] @@ -2634,7 +2584,7 @@ impl Guest for Commands { exchange(connection, ("EXISTS", NonEmpty::new("keys", keys))).await } - #[doc = " Set a timeout on key."] + #[doc = " Sets the expiration time of a key in seconds."] #[doc = ""] #[doc = " EXPIRE key seconds [ NX | XX | GT | LT ]"] #[doc = " https://valkey.io/commands/expire/"] @@ -2648,7 +2598,7 @@ impl Guest for Commands { exchange(connection, ("EXPIRE", key, seconds, mode)).await } - #[doc = " Set a timeout on key."] + #[doc = " Sets the expiration time of a key to a Unix timestamp."] #[doc = ""] #[doc = " EXPIREAT key unix-time-seconds [ NX | XX | GT | LT ]"] #[doc = " https://valkey.io/commands/expireat/"] @@ -2662,7 +2612,7 @@ impl Guest for Commands { exchange(connection, ("EXPIREAT", key, seconds, mode)).await } - #[doc = " Returns all keys matching pattern."] + #[doc = " Returns all key names that match a pattern."] #[doc = ""] #[doc = " KEYS pattern"] #[doc = " https://valkey.io/commands/keys/"] @@ -2671,8 +2621,7 @@ impl Guest for Commands { exchange(connection, ("KEYS", pattern)).await } - #[doc = " Removes all the keys of the slot, in the mode, the server's `lazyfree-lazy-user-flush`"] - #[doc = " without one."] + #[doc = " Removes all keys from the target slot."] #[doc = ""] #[doc = " CLUSTER FLUSHSLOT slot [ ASYNC | SYNC ]"] #[doc = " https://valkey.io/commands/cluster-flushslot/"] @@ -2685,7 +2634,7 @@ impl Guest for Commands { exchange(connection, ("CLUSTER", "FLUSHSLOT", slot, mode)).await } - #[doc = " Incrementally iterate over the keys of a cluster, slot by slot, or within the slot."] + #[doc = " Iterates over the keys in the cluster."] #[doc = ""] #[doc = " The cursor carries the slot, for clients to route each call to the node that owns it."] #[doc = ""] @@ -2712,8 +2661,7 @@ impl Guest for Commands { Ok((cursor.0, keys)) } - #[doc = " Copies the value of the source key to the destination key, in the database, or the current"] - #[doc = " one, returning whether it was copied, false when the destination exists without `replace`."] + #[doc = " Copies the value of a key to a new key."] #[doc = ""] #[doc = " COPY source destination [ DB destination-db ] [ REPLACE ]"] #[doc = " https://valkey.io/commands/copy/"] @@ -2738,7 +2686,7 @@ impl Guest for Commands { .await } - #[doc = " The number of keys in the current database."] + #[doc = " Returns the number of keys in the database."] #[doc = ""] #[doc = " DBSIZE"] #[doc = " https://valkey.io/commands/dbsize/"] @@ -2747,9 +2695,7 @@ impl Guest for Commands { exchange(connection, "DBSIZE").await } - #[doc = " When the key expires, to the second."] - #[doc = ""] - #[doc = " See also `pexpiretime`."] + #[doc = " Returns the expiration time of a key as a Unix timestamp."] #[doc = ""] #[doc = " EXPIRETIME key"] #[doc = " https://valkey.io/commands/expiretime/"] @@ -2758,8 +2704,7 @@ impl Guest for Commands { exchange(connection, ("EXPIRETIME", key)).await } - #[doc = " Removes all the keys of all the databases, in the mode, the server's"] - #[doc = " `lazyfree-lazy-user-flush` without one."] + #[doc = " Removes all keys from all databases."] #[doc = ""] #[doc = " FLUSHALL [ ASYNC | SYNC ]"] #[doc = " https://valkey.io/commands/flushall/"] @@ -2768,8 +2713,7 @@ impl Guest for Commands { exchange(connection, ("FLUSHALL", mode)).await } - #[doc = " Removes all the keys of the current database, in the mode, the server's"] - #[doc = " `lazyfree-lazy-user-flush` without one."] + #[doc = " Removes all keys from the current database."] #[doc = ""] #[doc = " FLUSHDB [ ASYNC | SYNC ]"] #[doc = " https://valkey.io/commands/flushdb/"] @@ -2778,8 +2722,9 @@ impl Guest for Commands { exchange(connection, ("FLUSHDB", mode)).await } - #[doc = " Moves the keys to the database of another instance, returning whether any were moved, false"] - #[doc = " when none of the keys exist."] + #[doc = " Atomically transfers a key from one instance to another."] + #[doc = ""] + #[doc = " False when none of the keys exist, the server's `NOKEY`."] #[doc = ""] #[doc = " MIGRATE host port \"\" destination-db timeout [ COPY ] [ REPLACE ] [ AUTH password | AUTH2 username password ] KEYS key [ key ... ]"] #[doc = " https://valkey.io/commands/migrate/"] @@ -2811,8 +2756,7 @@ impl Guest for Commands { Ok(migrated.0) } - #[doc = " Moves the key to the database, returning whether it was moved, false when it does not"] - #[doc = " exist, or exists in the database without `replace`."] + #[doc = " Moves a key to another database."] #[doc = ""] #[doc = " MOVE key db [ REPLACE ]"] #[doc = " https://valkey.io/commands/move/"] @@ -2826,8 +2770,7 @@ impl Guest for Commands { exchange(connection, ("MOVE", key, db, Flag("REPLACE", replace))).await } - #[doc = " The internal encoding of the value stored at key, e.g. `listpack`, none when the key does"] - #[doc = " not exist."] + #[doc = " Returns the internal encoding of an object."] #[doc = ""] #[doc = " OBJECT ENCODING key"] #[doc = " https://valkey.io/commands/object-encoding/"] @@ -2839,9 +2782,7 @@ impl Guest for Commands { exchange(connection, ("OBJECT", "ENCODING", key)).await } - #[doc = " The logarithmic access frequency counter of the key, none when it does not exist."] - #[doc = ""] - #[doc = " Only with an LFU `maxmemory-policy`, otherwise it fails."] + #[doc = " Returns the logarithmic access frequency counter of an object."] #[doc = ""] #[doc = " OBJECT FREQ key"] #[doc = " https://valkey.io/commands/object-freq/"] @@ -2850,7 +2791,7 @@ impl Guest for Commands { exchange(connection, ("OBJECT", "FREQ", key)).await } - #[doc = " The time since the key was last accessed, none when it does not exist."] + #[doc = " Returns the time since the last access to an object."] #[doc = ""] #[doc = " OBJECT IDLETIME key"] #[doc = " https://valkey.io/commands/object-idletime/"] @@ -2862,7 +2803,7 @@ impl Guest for Commands { exchange(connection, ("OBJECT", "IDLETIME", key)).await } - #[doc = " The number of references to the value stored at key, none when the key does not exist."] + #[doc = " Returns the reference count of a value of a key."] #[doc = ""] #[doc = " OBJECT REFCOUNT key"] #[doc = " https://valkey.io/commands/object-refcount/"] @@ -2874,7 +2815,7 @@ impl Guest for Commands { exchange(connection, ("OBJECT", "REFCOUNT", key)).await } - #[doc = " Removes the expiration of the key, returning whether it had one."] + #[doc = " Removes the expiration time of a key."] #[doc = ""] #[doc = " PERSIST key"] #[doc = " https://valkey.io/commands/persist/"] @@ -2883,7 +2824,7 @@ impl Guest for Commands { exchange(connection, ("PERSIST", key)).await } - #[doc = " Set a timeout on key, in milliseconds."] + #[doc = " Sets the expiration time of a key in milliseconds."] #[doc = ""] #[doc = " PEXPIRE key milliseconds [ NX | XX | GT | LT ]"] #[doc = " https://valkey.io/commands/pexpire/"] @@ -2897,7 +2838,7 @@ impl Guest for Commands { exchange(connection, ("PEXPIRE", key, milliseconds, mode)).await } - #[doc = " Set a timeout on key, as a Unix time in milliseconds."] + #[doc = " Sets the expiration time of a key to a Unix milliseconds timestamp."] #[doc = ""] #[doc = " PEXPIREAT key unix-time-milliseconds [ NX | XX | GT | LT ]"] #[doc = " https://valkey.io/commands/pexpireat/"] @@ -2911,9 +2852,7 @@ impl Guest for Commands { exchange(connection, ("PEXPIREAT", key, milliseconds, mode)).await } - #[doc = " When the key expires, to the millisecond."] - #[doc = ""] - #[doc = " See also `expiretime`."] + #[doc = " Returns the expiration time of a key as a Unix milliseconds timestamp."] #[doc = ""] #[doc = " PEXPIRETIME key"] #[doc = " https://valkey.io/commands/pexpiretime/"] @@ -2922,9 +2861,7 @@ impl Guest for Commands { exchange(connection, ("PEXPIRETIME", key)).await } - #[doc = " The remaining time to live of the key, to the millisecond."] - #[doc = ""] - #[doc = " See also `ttl`."] + #[doc = " Returns the expiration time in milliseconds of a key."] #[doc = ""] #[doc = " PTTL key"] #[doc = " https://valkey.io/commands/pttl/"] @@ -2933,7 +2870,7 @@ impl Guest for Commands { exchange(connection, ("PTTL", key)).await } - #[doc = " A random key of the current database, none when it is empty."] + #[doc = " Returns a random key name from the database."] #[doc = ""] #[doc = " RANDOMKEY"] #[doc = " https://valkey.io/commands/randomkey/"] @@ -2942,7 +2879,7 @@ impl Guest for Commands { exchange(connection, "RANDOMKEY").await } - #[doc = " Renames the key, replacing the new key when it exists."] + #[doc = " Renames a key and overwrites the destination."] #[doc = ""] #[doc = " RENAME key newkey"] #[doc = " https://valkey.io/commands/rename/"] @@ -2951,7 +2888,7 @@ impl Guest for Commands { exchange(connection, ("RENAME", key, newkey)).await } - #[doc = " Renames the key, only when the new key does not exist, returning whether it was renamed."] + #[doc = " Renames a key only when the target key name doesn't exist."] #[doc = ""] #[doc = " RENAMENX key newkey"] #[doc = " https://valkey.io/commands/renamenx/"] @@ -2960,9 +2897,9 @@ impl Guest for Commands { exchange(connection, ("RENAMENX", key, newkey)).await } - #[doc = " Creates the key from a value serialized with `dump`, without an expiration when `ttl` is"] - #[doc = " none. Fails when the key exists, unless `replace`. `idletime` and `freq` set what eviction"] - #[doc = " knows of the key, its idle time for an LRU policy, or its access frequency for an LFU policy."] + #[doc = " Creates a key from the serialized representation of a value."] + #[doc = ""] + #[doc = " A `ttl` of none restores the key without an expiration, `0`, `expire-at` adds `ABSTTL`."] #[doc = ""] #[doc = " RESTORE key ttl serialized-value [ REPLACE ] [ ABSTTL ] [ IDLETIME seconds ] [ FREQ frequency ]"] #[doc = " https://valkey.io/commands/restore/"] @@ -2993,7 +2930,9 @@ impl Guest for Commands { .await } - #[doc = " `restore` on a node a key is migrated to, for a slot being imported, see `cluster-setslot`."] + #[doc = " An internal command for migrating keys in a cluster."] + #[doc = ""] + #[doc = " A `ttl` of none restores the key without an expiration, `0`, `expire-at` adds `ABSTTL`."] #[doc = ""] #[doc = " RESTORE-ASKING key ttl serialized-value [ REPLACE ] [ ABSTTL ] [ IDLETIME seconds ] [ FREQ frequency ]"] #[doc = " https://valkey.io/commands/restore-asking/"] @@ -3024,7 +2963,7 @@ impl Guest for Commands { .await } - #[doc = " Incrementally iterate over the keys of the current database."] + #[doc = " Iterates over the key names in the database."] #[doc = ""] #[doc = " SCAN cursor [ MATCH pattern ] [ COUNT count ] [ TYPE type ]"] #[doc = " https://valkey.io/commands/scan/"] @@ -3047,7 +2986,7 @@ impl Guest for Commands { Ok((cursor.0, keys)) } - #[doc = " Swaps the two databases, for connections using either."] + #[doc = " Swaps two databases."] #[doc = ""] #[doc = " SWAPDB index1 index2"] #[doc = " https://valkey.io/commands/swapdb/"] @@ -3060,7 +2999,7 @@ impl Guest for Commands { exchange(connection, ("SWAPDB", index1, index2)).await } - #[doc = " Updates the last access time of the keys, returning how many exist."] + #[doc = " Returns the number of existing keys out of those specified after updating the time they were last accessed."] #[doc = ""] #[doc = " TOUCH key [ key ... ]"] #[doc = " https://valkey.io/commands/touch/"] @@ -3069,9 +3008,7 @@ impl Guest for Commands { exchange(connection, ("TOUCH", NonEmpty::new("keys", keys))).await } - #[doc = " The remaining time to live of the key, to the second."] - #[doc = ""] - #[doc = " See also `pttl`."] + #[doc = " Returns the expiration time in seconds of a key."] #[doc = ""] #[doc = " TTL key"] #[doc = " https://valkey.io/commands/ttl/"] @@ -3080,7 +3017,9 @@ impl Guest for Commands { exchange(connection, ("TTL", key)).await } - #[doc = " The type of the value stored at key, none when the key does not exist."] + #[doc = " Determines the type of value stored at a key."] + #[doc = ""] + #[doc = " None when the key does not exist, the server's `none`."] #[doc = ""] #[doc = " TYPE key"] #[doc = " https://valkey.io/commands/type/"] @@ -3090,7 +3029,7 @@ impl Guest for Commands { Ok(key_type.0) } - #[doc = " Removes the keys, freeing their memory in the background, returning how many existed."] + #[doc = " Asynchronously deletes one or more keys."] #[doc = ""] #[doc = " UNLINK key [ key ... ]"] #[doc = " https://valkey.io/commands/unlink/"] @@ -3099,7 +3038,7 @@ impl Guest for Commands { exchange(connection, ("UNLINK", NonEmpty::new("keys", keys))).await } - #[doc = " Posts a message to the given channel."] + #[doc = " Posts a message to a channel."] #[doc = ""] #[doc = " PUBLISH channel message"] #[doc = " https://valkey.io/commands/publish/"] @@ -3112,7 +3051,7 @@ impl Guest for Commands { exchange(connection, ("PUBLISH", channel, message)).await } - #[doc = " Lists the currently active channels."] + #[doc = " Returns the active channels."] #[doc = ""] #[doc = " PUBSUB CHANNELS [ pattern ]"] #[doc = " https://valkey.io/commands/pubsub-channels/"] @@ -3124,7 +3063,7 @@ impl Guest for Commands { exchange(connection, ("PUBSUB", "CHANNELS", pattern)).await } - #[doc = " Returns the number of unique patterns that are subscribed to by clients."] + #[doc = " Returns a count of unique pattern subscriptions."] #[doc = ""] #[doc = " PUBSUB NUMPAT"] #[doc = " https://valkey.io/commands/pubsub-numpat/"] @@ -3133,7 +3072,7 @@ impl Guest for Commands { exchange(connection, ("PUBSUB", "NUMPAT")).await } - #[doc = " Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels."] + #[doc = " Returns a count of subscribers to channels."] #[doc = ""] #[doc = " PUBSUB NUMSUB [ channel ] [ [ channel ] ... ]"] #[doc = " https://valkey.io/commands/pubsub-numsub/"] @@ -3149,7 +3088,7 @@ impl Guest for Commands { .await } - #[doc = " Lists the currently active shard channels."] + #[doc = " Returns the active shard channels."] #[doc = ""] #[doc = " PUBSUB SHARDCHANNELS [ pattern ]"] #[doc = " https://valkey.io/commands/pubsub-shardchannels/"] @@ -3161,7 +3100,7 @@ impl Guest for Commands { exchange(connection, ("PUBSUB", "SHARDCHANNELS", pattern)).await } - #[doc = " Returns the number of subscribers for the specified shard channels."] + #[doc = " Returns the count of subscribers of shard channels."] #[doc = ""] #[doc = " PUBSUB SHARDNUMSUB [ shardchannel [ shardchannel ... ] ]"] #[doc = " https://valkey.io/commands/pubsub-shardnumsub/"] @@ -3181,8 +3120,7 @@ impl Guest for Commands { .await } - #[doc = " Runs the Lua script, returning its result, the keys it accesses and its arguments are"] - #[doc = " `KEYS` and `ARGV` in the script."] + #[doc = " Executes a server-side Lua script."] #[doc = ""] #[doc = " An error the script returns fails the call."] #[doc = ""] @@ -3198,7 +3136,9 @@ impl Guest for Commands { exchange(connection, ("EVAL", script, KeysAndArgs(keys, args))).await } - #[doc = " Runs the Lua script, which may not write, returning its result, see `eval`."] + #[doc = " Executes a read-only server-side Lua script."] + #[doc = ""] + #[doc = " An error the script returns fails the call."] #[doc = ""] #[doc = " EVAL_RO script numkeys [ key [ key ... ] ] [ arg [ arg ... ] ]"] #[doc = " https://valkey.io/commands/eval_ro/"] @@ -3212,8 +3152,9 @@ impl Guest for Commands { exchange(connection, ("EVAL_RO", script, KeysAndArgs(keys, args))).await } - #[doc = " Runs the Lua script cached with the SHA1 digest, see `script-load`, returning its result, see"] - #[doc = " `eval`."] + #[doc = " Executes a server-side Lua script by SHA1 digest."] + #[doc = ""] + #[doc = " An error the script returns fails the call."] #[doc = ""] #[doc = " EVALSHA sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ]"] #[doc = " https://valkey.io/commands/evalsha/"] @@ -3227,8 +3168,9 @@ impl Guest for Commands { exchange(connection, ("EVALSHA", sha1, KeysAndArgs(keys, args))).await } - #[doc = " Runs the Lua script cached with the SHA1 digest, which may not write, returning its result,"] - #[doc = " see `eval`."] + #[doc = " Executes a read-only server-side Lua script by SHA1 digest."] + #[doc = ""] + #[doc = " An error the script returns fails the call."] #[doc = ""] #[doc = " EVALSHA_RO sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ]"] #[doc = " https://valkey.io/commands/evalsha_ro/"] @@ -3242,8 +3184,7 @@ impl Guest for Commands { exchange(connection, ("EVALSHA_RO", sha1, KeysAndArgs(keys, args))).await } - #[doc = " Calls the function, see `function-load`, returning its result, the keys it accesses and its"] - #[doc = " arguments are passed to it."] + #[doc = " Invokes a function."] #[doc = ""] #[doc = " An error the function returns fails the call."] #[doc = ""] @@ -3259,7 +3200,9 @@ impl Guest for Commands { exchange(connection, ("FCALL", function, KeysAndArgs(keys, args))).await } - #[doc = " Calls the function, which must be flagged `no-writes`, returning its result, see `fcall`."] + #[doc = " Invokes a read-only function."] + #[doc = ""] + #[doc = " An error the function returns fails the call."] #[doc = ""] #[doc = " FCALL_RO function numkeys [ key [ key ... ] ] [ arg [ arg ... ] ]"] #[doc = " https://valkey.io/commands/fcall_ro/"] @@ -3273,7 +3216,7 @@ impl Guest for Commands { exchange(connection, ("FCALL_RO", function, KeysAndArgs(keys, args))).await } - #[doc = " Deletes the library and its functions."] + #[doc = " Deletes a library and its functions."] #[doc = ""] #[doc = " FUNCTION DELETE library-name"] #[doc = " https://valkey.io/commands/function-delete/"] @@ -3282,7 +3225,7 @@ impl Guest for Commands { exchange(connection, ("FUNCTION", "DELETE", library_name)).await } - #[doc = " The libraries, serialized in Valkey's own format, see `function-restore`."] + #[doc = " Dumps all libraries into a serialized binary payload."] #[doc = ""] #[doc = " FUNCTION DUMP"] #[doc = " https://valkey.io/commands/function-dump/"] @@ -3292,7 +3235,7 @@ impl Guest for Commands { Ok(serialized.0) } - #[doc = " Deletes all the libraries, in the mode, the server's `lazyfree-lazy-user-flush` without one."] + #[doc = " Deletes all libraries and functions."] #[doc = ""] #[doc = " FUNCTION FLUSH [ ASYNC | SYNC ]"] #[doc = " https://valkey.io/commands/function-flush/"] @@ -3301,7 +3244,7 @@ impl Guest for Commands { exchange(connection, ("FUNCTION", "FLUSH", mode)).await } - #[doc = " Kills the function that is running, as long as it has not written."] + #[doc = " Terminates a function during execution."] #[doc = ""] #[doc = " FUNCTION KILL"] #[doc = " https://valkey.io/commands/function-kill/"] @@ -3310,7 +3253,7 @@ impl Guest for Commands { exchange(connection, ("FUNCTION", "KILL")).await } - #[doc = " The libraries, those with names matching the glob-style pattern with one."] + #[doc = " Returns information about all libraries."] #[doc = ""] #[doc = " FUNCTION LIST [ LIBRARYNAME library-name-pattern ] [ WITHCODE ]"] #[doc = " https://valkey.io/commands/function-list/"] @@ -3332,8 +3275,7 @@ impl Guest for Commands { .await } - #[doc = " Loads the library from its source code, replacing a library of the same name with"] - #[doc = " `replace`, returning its name."] + #[doc = " Creates a library."] #[doc = ""] #[doc = " FUNCTION LOAD [ REPLACE ] function-code"] #[doc = " https://valkey.io/commands/function-load/"] @@ -3350,7 +3292,7 @@ impl Guest for Commands { .await } - #[doc = " Loads the libraries serialized with `function-dump`, as `policy` says, by default `append`."] + #[doc = " Restores all libraries from a payload."] #[doc = ""] #[doc = " FUNCTION RESTORE serialized-value [ FLUSH | APPEND | REPLACE ]"] #[doc = " https://valkey.io/commands/function-restore/"] @@ -3372,7 +3314,7 @@ impl Guest for Commands { .await } - #[doc = " The function that is running, and the libraries and functions of each engine."] + #[doc = " Returns information about a function during execution."] #[doc = ""] #[doc = " FUNCTION STATS"] #[doc = " https://valkey.io/commands/function-stats/"] @@ -3381,7 +3323,7 @@ impl Guest for Commands { exchange(connection, ("FUNCTION", "STATS")).await } - #[doc = " Whether each of the scripts is cached, by SHA1 digest."] + #[doc = " Determines whether server-side Lua scripts exist in the script cache."] #[doc = ""] #[doc = " SCRIPT EXISTS sha1 [ sha1 ... ]"] #[doc = " https://valkey.io/commands/script-exists/"] @@ -3397,8 +3339,7 @@ impl Guest for Commands { .await } - #[doc = " Removes all the scripts from the cache, in the mode, the server's `lazyfree-lazy-user-flush`"] - #[doc = " without one."] + #[doc = " Removes all server-side Lua scripts from the script cache."] #[doc = ""] #[doc = " SCRIPT FLUSH [ ASYNC | SYNC ]"] #[doc = " https://valkey.io/commands/script-flush/"] @@ -3407,7 +3348,7 @@ impl Guest for Commands { exchange(connection, ("SCRIPT", "FLUSH", mode)).await } - #[doc = " Kills the script that is running, as long as it has not written."] + #[doc = " Terminates a server-side Lua script during execution."] #[doc = ""] #[doc = " SCRIPT KILL"] #[doc = " https://valkey.io/commands/script-kill/"] @@ -3416,7 +3357,7 @@ impl Guest for Commands { exchange(connection, ("SCRIPT", "KILL")).await } - #[doc = " Caches the script without running it, returning its SHA1 digest, see `evalsha`."] + #[doc = " Loads a server-side Lua script to the script cache."] #[doc = ""] #[doc = " SCRIPT LOAD script"] #[doc = " https://valkey.io/commands/script-load/"] @@ -3425,7 +3366,7 @@ impl Guest for Commands { exchange(connection, ("SCRIPT", "LOAD", script)).await } - #[doc = " The source of the cached script with the SHA1 digest."] + #[doc = " Show server-side Lua script in the script cache."] #[doc = ""] #[doc = " SCRIPT SHOW sha1"] #[doc = " https://valkey.io/commands/script-show/"] @@ -3434,7 +3375,7 @@ impl Guest for Commands { exchange(connection, ("SCRIPT", "SHOW", sha1)).await } - #[doc = " Posts a message to the given shard channel."] + #[doc = " Posts a message to a shard channel."] #[doc = ""] #[doc = " SPUBLISH shardchannel message"] #[doc = " https://valkey.io/commands/spublish/"] @@ -3447,7 +3388,7 @@ impl Guest for Commands { exchange(connection, ("SPUBLISH", shardchannel, message)).await } - #[doc = " Appends the value at the end of the string."] + #[doc = " Appends a string to the value of a key."] #[doc = ""] #[doc = " APPEND key value"] #[doc = " https://valkey.io/commands/append/"] @@ -3460,7 +3401,7 @@ impl Guest for Commands { exchange(connection, ("APPEND", key, value)).await } - #[doc = " Decrements the number stored at key by one."] + #[doc = " Decrements the integer value of a key by one."] #[doc = ""] #[doc = " DECR key"] #[doc = " https://valkey.io/commands/decr/"] @@ -3469,7 +3410,7 @@ impl Guest for Commands { exchange(connection, ("DECR", key)).await } - #[doc = " Decrements the number stored at key by decrement."] + #[doc = " Decrements the integer value of a key by a number."] #[doc = ""] #[doc = " DECRBY key decrement"] #[doc = " https://valkey.io/commands/decrby/"] @@ -3482,7 +3423,7 @@ impl Guest for Commands { exchange(connection, ("DECRBY", key, decrement)).await } - #[doc = " Deletes the key if its value matches the given string."] + #[doc = " Delete key if value matches string."] #[doc = ""] #[doc = " DELIFEQ key value"] #[doc = " https://valkey.io/commands/delifeq/"] @@ -3495,7 +3436,7 @@ impl Guest for Commands { exchange(connection, ("DELIFEQ", key, value)).await } - #[doc = " Get the value of key."] + #[doc = " Returns the string value of a key."] #[doc = ""] #[doc = " GET key"] #[doc = " https://valkey.io/commands/get/"] @@ -3504,7 +3445,7 @@ impl Guest for Commands { exchange(connection, ("GET", key)).await } - #[doc = " Get the value of key and delete the key."] + #[doc = " Returns the string value of a key after deleting the key."] #[doc = ""] #[doc = " GETDEL key"] #[doc = " https://valkey.io/commands/getdel/"] @@ -3513,7 +3454,7 @@ impl Guest for Commands { exchange(connection, ("GETDEL", key)).await } - #[doc = " Get the value of key and optionally set its expiration."] + #[doc = " Returns the string value of a key after setting its expiration time."] #[doc = ""] #[doc = " GETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ]"] #[doc = " https://valkey.io/commands/getex/"] @@ -3526,7 +3467,7 @@ impl Guest for Commands { exchange(connection, ("GETEX", key, ttl)).await } - #[doc = " Returns the substring of the string value stored at key, determined by the offsets start and end."] + #[doc = " Returns a substring of the string stored at a key."] #[doc = ""] #[doc = " GETRANGE key start end"] #[doc = " https://valkey.io/commands/getrange/"] @@ -3540,7 +3481,7 @@ impl Guest for Commands { exchange(connection, ("GETRANGE", key, start, end)).await } - #[doc = " Atomically sets key to value and returns the old value stored at key."] + #[doc = " Returns the previous string value of a key after setting it to a new value."] #[doc = ""] #[doc = " GETSET key value"] #[doc = " https://valkey.io/commands/getset/"] @@ -3553,7 +3494,7 @@ impl Guest for Commands { exchange(connection, ("GETSET", key, value)).await } - #[doc = " Increments the number stored at key by one."] + #[doc = " Increments the integer value of a key by one."] #[doc = ""] #[doc = " INCR key"] #[doc = " https://valkey.io/commands/incr/"] @@ -3562,7 +3503,7 @@ impl Guest for Commands { exchange(connection, ("INCR", key)).await } - #[doc = " Increments the number stored at key by increment."] + #[doc = " Increments the integer value of a key by a number."] #[doc = ""] #[doc = " INCRBY key increment"] #[doc = " https://valkey.io/commands/incrby/"] @@ -3575,7 +3516,7 @@ impl Guest for Commands { exchange(connection, ("INCRBY", key, increment)).await } - #[doc = " Increment the string representing a floating point number stored at key by the specified increment."] + #[doc = " Increments the floating point value of a key by a number."] #[doc = ""] #[doc = " INCRBYFLOAT key increment"] #[doc = " https://valkey.io/commands/incrbyfloat/"] @@ -3588,9 +3529,9 @@ impl Guest for Commands { exchange(connection, ("INCRBYFLOAT", key, increment)).await } - #[doc = " The LCS command implements the longest common subsequence algorithm."] + #[doc = " Finds the longest common substring."] #[doc = ""] - #[doc = " See also `lcs-len` and `lcs-idx`"] + #[doc = " See also `lcs-len` and `lcs-idx`."] #[doc = ""] #[doc = " LCS key1 key2"] #[doc = " https://valkey.io/commands/lcs/"] @@ -3603,9 +3544,11 @@ impl Guest for Commands { exchange(connection, ("LCS", key1, key2)).await } - #[doc = " The LCS command implements the longest common subsequence algorithm."] + #[doc = " Finds the longest common substring."] + #[doc = ""] + #[doc = " The `LEN` form, the length only."] #[doc = ""] - #[doc = " See also `lcs` and `lcs-idx`"] + #[doc = " See also `lcs` and `lcs-idx`."] #[doc = ""] #[doc = " LCS key1 key2 LEN"] #[doc = " https://valkey.io/commands/lcs/"] @@ -3618,9 +3561,11 @@ impl Guest for Commands { exchange(connection, ("LCS", key1, key2, "LEN")).await } - #[doc = " The LCS command implements the longest common subsequence algorithm."] + #[doc = " Finds the longest common substring."] #[doc = ""] - #[doc = " See also `lcs` and `lcs-len`"] + #[doc = " The `IDX` form, the ranges of the matches."] + #[doc = ""] + #[doc = " See also `lcs` and `lcs-len`."] #[doc = ""] #[doc = " LCS key1 key2 IDX [ MINMATCHLEN min-match-len ] [ WITHMATCHLEN ]"] #[doc = " https://valkey.io/commands/lcs/"] @@ -3646,7 +3591,7 @@ impl Guest for Commands { .await } - #[doc = " Returns the values of all specified keys."] + #[doc = " Atomically returns the string values of one or more keys."] #[doc = ""] #[doc = " MGET key [ key ... ]"] #[doc = " https://valkey.io/commands/mget/"] @@ -3658,7 +3603,7 @@ impl Guest for Commands { exchange(connection, ("MGET", NonEmpty::new("keys", keys))).await } - #[doc = " Sets the given keys to their respective values."] + #[doc = " Atomically creates or modifies the string values of one or more keys."] #[doc = ""] #[doc = " MSET key value [ key value ... ]"] #[doc = " https://valkey.io/commands/mset/"] @@ -3674,7 +3619,7 @@ impl Guest for Commands { .await } - #[doc = " Sets multiple key-value pairs while also assigning a shared expiration time to all the keys atomically."] + #[doc = " Atomically creates or modifies the string values of one or more keys, and optionally set their expiration."] #[doc = ""] #[doc = " MSETEX numkeys key value [ key value ... ] [ NX | XX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ]"] #[doc = " https://valkey.io/commands/msetex/"] @@ -3697,7 +3642,7 @@ impl Guest for Commands { .await } - #[doc = " Sets the given keys to their respective values."] + #[doc = " Atomically modifies the string values of one or more keys only when all keys don't exist."] #[doc = ""] #[doc = " MSETNX key value [ key value ... ]"] #[doc = " https://valkey.io/commands/msetnx/"] @@ -3713,7 +3658,7 @@ impl Guest for Commands { .await } - #[doc = " Set key to hold the string value with expiration in milliseconds."] + #[doc = " Sets both string value and expiration time in milliseconds of a key."] #[doc = ""] #[doc = " PSETEX key milliseconds value"] #[doc = " https://valkey.io/commands/psetex/"] @@ -3727,10 +3672,12 @@ impl Guest for Commands { exchange(connection, ("PSETEX", key, milliseconds, value)).await } - #[doc = " Set key to hold the string value."] + #[doc = " Sets the string value of a key, ignoring its type."] #[doc = ""] #[doc = " Returns false when the precondition is not met, the key is unchanged."] #[doc = ""] + #[doc = " See also `set-get`."] + #[doc = ""] #[doc = " SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ]"] #[doc = " https://valkey.io/commands/set/"] #[allow(async_fn_in_trait)] @@ -3747,7 +3694,11 @@ impl Guest for Commands { Ok(reply.is_some()) } - #[doc = " Set key to hold the string value. Returning the previous value."] + #[doc = " Sets the string value of a key, ignoring its type."] + #[doc = ""] + #[doc = " The `GET` form, returning the previous value, none when the key did not exist."] + #[doc = ""] + #[doc = " See also `set`."] #[doc = ""] #[doc = " SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] GET [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ]"] #[doc = " https://valkey.io/commands/set/"] @@ -3762,7 +3713,7 @@ impl Guest for Commands { exchange(connection, ("SET", key, value, precondition, "GET", ttl)).await } - #[doc = " Set key to hold the string value with expiration in seconds."] + #[doc = " Sets the string value and expiration time of a key."] #[doc = ""] #[doc = " SETEX key seconds value"] #[doc = " https://valkey.io/commands/setex/"] @@ -3776,7 +3727,7 @@ impl Guest for Commands { exchange(connection, ("SETEX", key, seconds, value)).await } - #[doc = " Set key to hold string value if key does not exist."] + #[doc = " Sets the string value of a key only when the key doesn't exist."] #[doc = ""] #[doc = " SETNX key value"] #[doc = " https://valkey.io/commands/setnx/"] @@ -3789,7 +3740,7 @@ impl Guest for Commands { exchange(connection, ("SETNX", key, value)).await } - #[doc = " Overwrites part of the string stored at key, starting at the specified offset, for the entire length of value."] + #[doc = " Overwrites a part of a string value with another by an offset."] #[doc = ""] #[doc = " SETRANGE key offset value"] #[doc = " https://valkey.io/commands/setrange/"] @@ -3803,16 +3754,16 @@ impl Guest for Commands { exchange(connection, ("SETRANGE", key, offset, value)).await } - #[doc = " Returns the length of the string value stored at key."] + #[doc = " Returns the length of a string value."] #[doc = ""] #[doc = " STRLEN key"] - #[doc = " hthttps://valkey.io/commands/strlen/"] + #[doc = " https://valkey.io/commands/strlen/"] #[allow(async_fn_in_trait)] async fn strlen(connection: &Connection, key: String) -> Result { exchange(connection, ("STRLEN", key)).await } - #[doc = " Returns the substring of the string value stored at key, determined by the offsets start and end."] + #[doc = " Returns a substring from a string value."] #[doc = ""] #[doc = " SUBSTR key start end"] #[doc = " https://valkey.io/commands/substr/"] @@ -3826,7 +3777,7 @@ impl Guest for Commands { exchange(connection, ("SUBSTR", key, start, end)).await } - #[doc = " Removes one or multiple messages from the Pending Entries List (PEL) of a stream consumer group."] + #[doc = " Returns the number of messages that were successfully acknowledged by the consumer group member of a stream."] #[doc = ""] #[doc = " XACK key group ID [ ID ... ]"] #[doc = " https://valkey.io/commands/xack/"] @@ -3840,7 +3791,7 @@ impl Guest for Commands { exchange(connection, ("XACK", key, group, NonEmpty::new("ids", ids))).await } - #[doc = " Appends the specified stream entry to the stream at the specified key."] + #[doc = " Appends a new message to a stream."] #[doc = ""] #[doc = " XADD key [ NOMKSTREAM ] [ 〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] ] 〈 * | id 〉 field value [ field value ... ]"] #[doc = " https://valkey.io/commands/xadd/"] @@ -3867,9 +3818,12 @@ impl Guest for Commands { .await } - #[doc = " Transfers ownership of pending stream entries that match the specified criteria."] + #[doc = " Changes, or acquires, ownership of messages in a consumer group, as if the messages were delivered to a consumer group member."] + #[doc = ""] + #[doc = " A start of none scans from the beginning, `0-0`. The next start is returned, none once the"] + #[doc = " scan is complete."] #[doc = ""] - #[doc = " See also `xautoclaim-justid`"] + #[doc = " See also `xautoclaim-justid`."] #[doc = ""] #[doc = " XAUTOCLAIM key group consumer min-idle-time start [ COUNT count ]"] #[doc = " https://valkey.io/commands/xautoclaim/"] @@ -3906,9 +3860,12 @@ impl Guest for Commands { Ok((next.0, claimed, deleted)) } - #[doc = " Transfers ownership of pending stream entries that match the specified criteria."] + #[doc = " Changes, or acquires, ownership of messages in a consumer group, as if the messages were delivered to a consumer group member."] + #[doc = ""] + #[doc = " The `JUSTID` form, the IDs only. A start of none scans from the beginning, `0-0`. The next"] + #[doc = " start is returned, none once the scan is complete."] #[doc = ""] - #[doc = " See also `xautoclaim`"] + #[doc = " See also `xautoclaim`."] #[doc = ""] #[doc = " XAUTOCLAIM key group consumer min-idle-time start [ COUNT count ] JUSTID"] #[doc = " https://valkey.io/commands/xautoclaim/"] @@ -3939,9 +3896,9 @@ impl Guest for Commands { Ok((next.0, claimed, deleted)) } - #[doc = " Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument."] + #[doc = " Changes, or acquires, ownership of a message in a consumer group, as if the message was delivered to a consumer group member."] #[doc = ""] - #[doc = " Also see `xclaim-justid`"] + #[doc = " See also `xclaim-justid`."] #[doc = ""] #[doc = " XCLAIM key group consumer min-idle-time ID [ ID ... ] [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] [ LASTID lastid ]"] #[doc = " https://valkey.io/commands/xclaim/"] @@ -3970,9 +3927,11 @@ impl Guest for Commands { .await } - #[doc = " Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument."] + #[doc = " Changes, or acquires, ownership of a message in a consumer group, as if the message was delivered to a consumer group member."] #[doc = ""] - #[doc = " Also see `xclaim`"] + #[doc = " The `JUSTID` form, the IDs only."] + #[doc = ""] + #[doc = " See also `xclaim`."] #[doc = ""] #[doc = " XCLAIM key group consumer min-idle-time ID [ ID ... ] [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] JUSTID [ LASTID lastid ]"] #[doc = " https://valkey.io/commands/xclaim/"] @@ -4001,7 +3960,7 @@ impl Guest for Commands { .await } - #[doc = " Removes the specified entries from a stream, and returns the number of entries deleted."] + #[doc = " Returns the number of messages after removing them from a stream."] #[doc = ""] #[doc = " XDEL key ID [ ID ... ]"] #[doc = " https://valkey.io/commands/xdel/"] @@ -4014,7 +3973,7 @@ impl Guest for Commands { exchange(connection, ("XDEL", key, NonEmpty::new("ids", ids))).await } - #[doc = " Create a new consumer group uniquely identified by for the stream stored at ."] + #[doc = " Creates a consumer group."] #[doc = ""] #[doc = " XGROUP CREATE key group 〈 id | $ 〉 [ MKSTREAM ] [ ENTRIESREAD entriesread ]"] #[doc = " https://valkey.io/commands/xgroup-create/"] @@ -4042,7 +4001,7 @@ impl Guest for Commands { .await } - #[doc = " Create a consumer named in the consumer group of the stream that\'s stored at ."] + #[doc = " Creates a consumer in a consumer group."] #[doc = ""] #[doc = " XGROUP CREATECONSUMER key group consumer"] #[doc = " https://valkey.io/commands/xgroup-createconsumer/"] @@ -4060,7 +4019,7 @@ impl Guest for Commands { .await } - #[doc = " The XGROUP DELCONSUMER command deletes a consumer from the consumer group."] + #[doc = " Deletes a consumer from a consumer group."] #[doc = ""] #[doc = " XGROUP DELCONSUMER key group consumer"] #[doc = " https://valkey.io/commands/xgroup-delconsumer/"] @@ -4074,7 +4033,7 @@ impl Guest for Commands { exchange(connection, ("XGROUP", "DELCONSUMER", key, group, consumer)).await } - #[doc = " Completely destroys the consumer group."] + #[doc = " Destroys a consumer group."] #[doc = ""] #[doc = " XGROUP DESTROY key group"] #[doc = " https://valkey.io/commands/xgroup-destroy/"] @@ -4087,7 +4046,7 @@ impl Guest for Commands { exchange(connection, ("XGROUP", "DESTROY", key, group)).await } - #[doc = " Set the last delivered ID for a consumer group."] + #[doc = " Sets the last-delivered ID of a consumer group."] #[doc = ""] #[doc = " XGROUP SETID key group 〈 id | $ 〉 [ ENTRIESREAD entriesread ]"] #[doc = " https://valkey.io/commands/xgroup-setid/"] @@ -4113,7 +4072,7 @@ impl Guest for Commands { .await } - #[doc = " Consumers that belong to the consumer group of the stream stored at ."] + #[doc = " Returns a list of the consumers in a consumer group."] #[doc = ""] #[doc = " XINFO CONSUMERS key group"] #[doc = " https://valkey.io/commands/xinfo-consumers/"] @@ -4126,7 +4085,7 @@ impl Guest for Commands { exchange(connection, ("XINFO", "CONSUMERS", key, group)).await } - #[doc = " Consumer groups of the stream stored at ."] + #[doc = " Returns a list of the consumer groups of a stream."] #[doc = ""] #[doc = " XINFO GROUPS key"] #[doc = " https://valkey.io/commands/xinfo-groups/"] @@ -4138,9 +4097,9 @@ impl Guest for Commands { exchange(connection, ("XINFO", "GROUPS", key)).await } - #[doc = " Information about the stream stored at ."] + #[doc = " Returns information about a stream."] #[doc = ""] - #[doc = " See also `xinfo-stream-full`"] + #[doc = " See also `xinfo-stream-full`."] #[doc = ""] #[doc = " XINFO STREAM key"] #[doc = " https://valkey.io/commands/xinfo-stream/"] @@ -4149,9 +4108,11 @@ impl Guest for Commands { exchange(connection, ("XINFO", "STREAM", key)).await } - #[doc = " Information about the stream stored at ."] + #[doc = " Returns information about a stream."] + #[doc = ""] + #[doc = " The `FULL` form, with the entries, groups and consumers."] #[doc = ""] - #[doc = " See also `xinfo-stream`"] + #[doc = " See also `xinfo-stream`."] #[doc = ""] #[doc = " XINFO STREAM key FULL [ COUNT count ]"] #[doc = " https://valkey.io/commands/xinfo-stream/"] @@ -4168,7 +4129,7 @@ impl Guest for Commands { .await } - #[doc = " Number of entries inside a stream."] + #[doc = " Returns the number of messages in a stream."] #[doc = ""] #[doc = " XLEN key"] #[doc = " https://valkey.io/commands/xlen/"] @@ -4177,7 +4138,9 @@ impl Guest for Commands { exchange(connection, ("XLEN", key)).await } - #[doc = " List of pending messages."] + #[doc = " Returns the information and entries from a stream consumer group's pending entries list."] + #[doc = ""] + #[doc = " The summary form."] #[doc = ""] #[doc = " See also `xpending-extended`."] #[doc = ""] @@ -4192,7 +4155,9 @@ impl Guest for Commands { exchange(connection, ("XPENDING", key, group)).await } - #[doc = " List of pending messages."] + #[doc = " Returns the information and entries from a stream consumer group's pending entries list."] + #[doc = ""] + #[doc = " The extended form, the pending entries in the range."] #[doc = ""] #[doc = " See also `xpending`."] #[doc = ""] @@ -4225,7 +4190,7 @@ impl Guest for Commands { .await } - #[doc = " Stream entries matching a given range of IDs."] + #[doc = " Returns the messages from a stream within a range of IDs."] #[doc = ""] #[doc = " XRANGE key start end [ COUNT count ]"] #[doc = " https://valkey.io/commands/xrange/"] @@ -4244,7 +4209,10 @@ impl Guest for Commands { .await } - #[doc = " Read data from one or multiple streams, only returning entries with an ID greater than the last received ID reported by the caller."] + #[doc = " Returns messages from multiple streams with IDs greater than the ones requested."] + #[doc = ""] + #[doc = " With `block`, returns none when the timeout passed. The connection waits too, replies to"] + #[doc = " commands sent after it on the connection wait behind it. Use a connection of your own."] #[doc = ""] #[doc = " XREAD [ COUNT count ] [ BLOCK milliseconds ] STREAMS key [ key ... ] ID [ ID ... ]"] #[doc = " https://valkey.io/commands/xread/"] @@ -4270,10 +4238,13 @@ impl Guest for Commands { .await } - #[doc = " Special version of the XREAD command with support for consumer groups."] + #[doc = " Returns new or historical messages from a stream for a consumer in a group."] + #[doc = ""] + #[doc = " The fields of an entry are none when it was deleted after it was delivered, e.g. when"] + #[doc = " reading the pending entries with the ID `0`."] #[doc = ""] - #[doc = " The fields of an entry are none when it was deleted after it was delivered, e.g. when reading"] - #[doc = " the pending entries with the ID `0`."] + #[doc = " With `block`, returns none when the timeout passed. The connection waits too, replies to"] + #[doc = " commands sent after it on the connection wait behind it. Use a connection of your own."] #[doc = ""] #[doc = " XREADGROUP GROUP group consumer [ COUNT count ] [ BLOCK milliseconds ] [ NOACK ] STREAMS key [ key ... ] ID [ ID ... ]"] #[doc = " https://valkey.io/commands/xreadgroup/"] @@ -4307,7 +4278,7 @@ impl Guest for Commands { .await } - #[doc = " XRANGE, but returning the entries in reverse order."] + #[doc = " Returns the messages from a stream within a range of IDs in reverse order."] #[doc = ""] #[doc = " XREVRANGE key end start [ COUNT count ]"] #[doc = " https://valkey.io/commands/xrevrange/"] @@ -4326,7 +4297,7 @@ impl Guest for Commands { .await } - #[doc = " Internal command. It is used by a Valkey primary to replicate the last delivered ID of streams."] + #[doc = " An internal command for replicating stream values."] #[doc = ""] #[doc = " XSETID key last-id [ ENTRIESADDED entries-added ] [ MAXDELETEDID max-deleted-id ]"] #[doc = " https://valkey.io/commands/xsetid/"] @@ -4351,7 +4322,7 @@ impl Guest for Commands { .await } - #[doc = " Trims the stream by evicting older entries."] + #[doc = " Deletes messages from the beginning of a stream."] #[doc = ""] #[doc = " XTRIM key 〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ]"] #[doc = " https://valkey.io/commands/xtrim/"] @@ -4364,7 +4335,7 @@ impl Guest for Commands { exchange(connection, ("XTRIM", key, trim)).await } - #[doc = " Adds the members to the set stored at key, returning how many were not already members."] + #[doc = " Adds one or more members to a set."] #[doc = ""] #[doc = " SADD key member [ member ... ]"] #[doc = " https://valkey.io/commands/sadd/"] @@ -4377,7 +4348,7 @@ impl Guest for Commands { exchange(connection, ("SADD", key, NonEmpty::new("members", members))).await } - #[doc = " The number of members in the set stored at key."] + #[doc = " Returns the number of members in a set."] #[doc = ""] #[doc = " SCARD key"] #[doc = " https://valkey.io/commands/scard/"] @@ -4386,7 +4357,7 @@ impl Guest for Commands { exchange(connection, ("SCARD", key)).await } - #[doc = " The members of the first set that are not in any of the others."] + #[doc = " Returns the difference of multiple sets."] #[doc = ""] #[doc = " SDIFF key [ key ... ]"] #[doc = " https://valkey.io/commands/sdiff/"] @@ -4398,7 +4369,7 @@ impl Guest for Commands { exchange(connection, ("SDIFF", NonEmpty::new("keys", keys))).await } - #[doc = " Stores the members of the first set that are not in any of the others, returning how many."] + #[doc = " Stores the difference of multiple sets in a key."] #[doc = ""] #[doc = " SDIFFSTORE destination key [ key ... ]"] #[doc = " https://valkey.io/commands/sdiffstore/"] @@ -4415,7 +4386,7 @@ impl Guest for Commands { .await } - #[doc = " The members in all of the sets."] + #[doc = " Returns the intersect of multiple sets."] #[doc = ""] #[doc = " SINTER key [ key ... ]"] #[doc = " https://valkey.io/commands/sinter/"] @@ -4427,7 +4398,7 @@ impl Guest for Commands { exchange(connection, ("SINTER", NonEmpty::new("keys", keys))).await } - #[doc = " The number of members in all of the sets, counting stops at the limit."] + #[doc = " Returns the number of members of the intersect of multiple sets."] #[doc = ""] #[doc = " SINTERCARD numkeys key [ key ... ] [ LIMIT limit ]"] #[doc = " https://valkey.io/commands/sintercard/"] @@ -4448,7 +4419,7 @@ impl Guest for Commands { .await } - #[doc = " Stores the members in all of the sets, returning how many."] + #[doc = " Stores the intersect of multiple sets in a key."] #[doc = ""] #[doc = " SINTERSTORE destination key [ key ... ]"] #[doc = " https://valkey.io/commands/sinterstore/"] @@ -4465,7 +4436,9 @@ impl Guest for Commands { .await } - #[doc = " Whether the member is in the set stored at key, false when the key does not exist."] + #[doc = " Determines whether a member belongs to a set."] + #[doc = ""] + #[doc = " False when the key does not exist."] #[doc = ""] #[doc = " See also `sismember-xx`."] #[doc = ""] @@ -4480,7 +4453,9 @@ impl Guest for Commands { exchange(connection, ("SISMEMBER", key, member)).await } - #[doc = " Whether the member is in the set stored at key, none when the key does not exist."] + #[doc = " Determines whether a member belongs to a set."] + #[doc = ""] + #[doc = " The `XX` form, none when the key does not exist, rather than false."] #[doc = ""] #[doc = " See also `sismember`."] #[doc = ""] @@ -4497,7 +4472,7 @@ impl Guest for Commands { Ok(member.0) } - #[doc = " The members of the set stored at key."] + #[doc = " Returns all members of a set."] #[doc = ""] #[doc = " SMEMBERS key"] #[doc = " https://valkey.io/commands/smembers/"] @@ -4506,7 +4481,7 @@ impl Guest for Commands { exchange(connection, ("SMEMBERS", key)).await } - #[doc = " Whether each of the members is in the set stored at key."] + #[doc = " Determines whether multiple members belong to a set."] #[doc = ""] #[doc = " SMISMEMBER key member [ member ... ]"] #[doc = " https://valkey.io/commands/smismember/"] @@ -4523,7 +4498,7 @@ impl Guest for Commands { .await } - #[doc = " Moves the member from one set to another, false when it was not in the source set."] + #[doc = " Moves a member from one set to another."] #[doc = ""] #[doc = " SMOVE source destination member"] #[doc = " https://valkey.io/commands/smove/"] @@ -4537,9 +4512,9 @@ impl Guest for Commands { exchange(connection, ("SMOVE", source, destination, member)).await } - #[doc = " Removes and returns random members of the set stored at key, up to the count."] + #[doc = " Returns one or more random members from a set after removing them."] #[doc = ""] - #[doc = " Without a count, one member, or none when the key does not exist."] + #[doc = " Without a count, at most one member."] #[doc = ""] #[doc = " SPOP key [ count ]"] #[doc = " https://valkey.io/commands/spop/"] @@ -4557,10 +4532,9 @@ impl Guest for Commands { Ok(member.into_iter().collect()) } - #[doc = " Random members of the set stored at key, up to the count."] + #[doc = " Gets one or multiple random members from a set."] #[doc = ""] - #[doc = " Without a count, one member, or none when the key does not exist. A negative count may return"] - #[doc = " the same member more than once, its absolute value is the number of members returned."] + #[doc = " Without a count, at most one member."] #[doc = ""] #[doc = " SRANDMEMBER key [ count ]"] #[doc = " https://valkey.io/commands/srandmember/"] @@ -4578,7 +4552,7 @@ impl Guest for Commands { Ok(member.into_iter().collect()) } - #[doc = " Removes the members from the set stored at key, returning how many were members."] + #[doc = " Removes one or more members from a set."] #[doc = ""] #[doc = " SREM key member [ member ... ]"] #[doc = " https://valkey.io/commands/srem/"] @@ -4591,7 +4565,7 @@ impl Guest for Commands { exchange(connection, ("SREM", key, NonEmpty::new("members", members))).await } - #[doc = " Incrementally iterate over the members of the set stored at key."] + #[doc = " Iterates over members of a set."] #[doc = ""] #[doc = " SSCAN key cursor [ MATCH pattern ] [ COUNT count ]"] #[doc = " https://valkey.io/commands/sscan/"] @@ -4617,7 +4591,7 @@ impl Guest for Commands { Ok((cursor.0, items)) } - #[doc = " The members in any of the sets."] + #[doc = " Returns the union of multiple sets."] #[doc = ""] #[doc = " SUNION key [ key ... ]"] #[doc = " https://valkey.io/commands/sunion/"] @@ -4629,7 +4603,7 @@ impl Guest for Commands { exchange(connection, ("SUNION", NonEmpty::new("keys", keys))).await } - #[doc = " Stores the members in any of the sets, returning how many."] + #[doc = " Stores the union of multiple sets in a key."] #[doc = ""] #[doc = " SUNIONSTORE destination key [ key ... ]"] #[doc = " https://valkey.io/commands/sunionstore/"] @@ -4646,10 +4620,12 @@ impl Guest for Commands { .await } - #[doc = " Sorts the elements of the list, set or sorted set stored at key."] + #[doc = " Sorts the elements in a list, a set, or a sorted set, optionally storing the result."] + #[doc = ""] + #[doc = " Each row is the element, or with `get`, the value of each pattern, none when its key does"] + #[doc = " not exist."] #[doc = ""] - #[doc = " Each row is the element, or with `get`, the value of each pattern, none when its key does not"] - #[doc = " exist."] + #[doc = " See also `sort-store`."] #[doc = ""] #[doc = " SORT key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ]"] #[doc = " https://valkey.io/commands/sort/"] @@ -4664,8 +4640,11 @@ impl Guest for Commands { Ok(rows(values, row)) } - #[doc = " Sorts the elements of the list, set or sorted set stored at key, storing them as a list,"] - #[doc = " returning how many."] + #[doc = " Sorts the elements in a list, a set, or a sorted set, optionally storing the result."] + #[doc = ""] + #[doc = " The `STORE` form, storing the elements as a list, returning how many."] + #[doc = ""] + #[doc = " See also `sort`."] #[doc = ""] #[doc = " SORT key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] STORE destination"] #[doc = " https://valkey.io/commands/sort/"] @@ -4679,7 +4658,10 @@ impl Guest for Commands { exchange(connection, ("SORT", key, opts, "STORE", destination)).await } - #[doc = " The read-only variant of `sort`, e.g. for replicas."] + #[doc = " Returns the sorted elements of a list, a set, or a sorted set."] + #[doc = ""] + #[doc = " Each row is the element, or with `get`, the value of each pattern, none when its key does"] + #[doc = " not exist."] #[doc = ""] #[doc = " SORT_RO key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ]"] #[doc = " https://valkey.io/commands/sort_ro/"] @@ -4695,13 +4677,10 @@ impl Guest for Commands { Ok(rows(values, row)) } - #[doc = " Removes the members with the lowest or highest scores from the first non-empty sorted set,"] - #[doc = " waiting up to the timeout for one, returning its key and the members and their scores."] + #[doc = " Removes and returns a member by score from one or more sorted sets."] #[doc = ""] - #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] - #[doc = ""] - #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] - #[doc = " it. Use a connection of your own."] + #[doc = " Returns none when the timeout passed. The connection waits too, replies to commands sent"] + #[doc = " after it on the connection wait behind it. Use a connection of your own."] #[doc = ""] #[doc = " BZMPOP timeout numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ]"] #[doc = " https://valkey.io/commands/bzmpop/"] @@ -4723,13 +4702,10 @@ impl Guest for Commands { exchange(connection, command).await } - #[doc = " Removes the member with the highest score from the first non-empty sorted set, waiting up to"] - #[doc = " the timeout for one, returning its key, the member and its score."] - #[doc = ""] - #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] + #[doc = " Removes and returns the member with the highest score from one or more sorted sets."] #[doc = ""] - #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] - #[doc = " it. Use a connection of your own."] + #[doc = " Returns none when the timeout passed. The connection waits too, replies to commands sent"] + #[doc = " after it on the connection wait behind it. Use a connection of your own."] #[doc = ""] #[doc = " BZPOPMAX key [ key ... ] timeout"] #[doc = " https://valkey.io/commands/bzpopmax/"] @@ -4750,13 +4726,10 @@ impl Guest for Commands { .await } - #[doc = " Removes the member with the lowest score from the first non-empty sorted set, waiting up to"] - #[doc = " the timeout for one, returning its key, the member and its score."] + #[doc = " Removes and returns the member with the lowest score from one or more sorted sets."] #[doc = ""] - #[doc = " Returns none when the timeout passed. A timeout of zero waits indefinitely."] - #[doc = ""] - #[doc = " The connection waits too, replies to commands sent after it on the connection wait behind"] - #[doc = " it. Use a connection of your own."] + #[doc = " Returns none when the timeout passed. The connection waits too, replies to commands sent"] + #[doc = " after it on the connection wait behind it. Use a connection of your own."] #[doc = ""] #[doc = " BZPOPMIN key [ key ... ] timeout"] #[doc = " https://valkey.io/commands/bzpopmin/"] @@ -4777,8 +4750,7 @@ impl Guest for Commands { .await } - #[doc = " Adds the members with their scores to the sorted set stored at key, or updates their scores,"] - #[doc = " returning how many were added, or with `changed`, added or updated."] + #[doc = " Adds one or more members to a sorted set, or updates their scores."] #[doc = ""] #[doc = " See also `zadd-incr`."] #[doc = ""] @@ -4804,10 +4776,12 @@ impl Guest for Commands { exchange(connection, command).await } - #[doc = " Increments the score of the member in the sorted set stored at key, returning the new score,"] - #[doc = " none when the condition or comparison prevented it."] + #[doc = " Adds one or more members to a sorted set, or updates their scores."] + #[doc = ""] + #[doc = " The `INCR` form, returning the new score, none when the condition or comparison prevented"] + #[doc = " it."] #[doc = ""] - #[doc = " See also `zadd` and `zincrby`."] + #[doc = " See also `zadd`."] #[doc = ""] #[doc = " ZADD key [ NX | XX ] [ GT | LT ] INCR score member"] #[doc = " https://valkey.io/commands/zadd/"] @@ -4826,7 +4800,7 @@ impl Guest for Commands { exchange(connection, command).await } - #[doc = " The number of members in the sorted set stored at key."] + #[doc = " Returns the number of members in a sorted set."] #[doc = ""] #[doc = " ZCARD key"] #[doc = " https://valkey.io/commands/zcard/"] @@ -4835,7 +4809,7 @@ impl Guest for Commands { exchange(connection, ("ZCARD", key)).await } - #[doc = " The number of members of the sorted set stored at key with scores in the range."] + #[doc = " Returns the count of members in a sorted set that have scores within a range."] #[doc = ""] #[doc = " ZCOUNT key min max"] #[doc = " https://valkey.io/commands/zcount/"] @@ -4861,7 +4835,7 @@ impl Guest for Commands { .await } - #[doc = " The members of the first sorted set that are not in any of the others."] + #[doc = " Returns the difference between multiple sorted sets."] #[doc = ""] #[doc = " See also `zdiff-withscores`."] #[doc = ""] @@ -4875,7 +4849,9 @@ impl Guest for Commands { exchange(connection, ("ZDIFF", NumKeys(NonEmpty::new("keys", keys)))).await } - #[doc = " The members, and their scores, of the first sorted set that are not in any of the others."] + #[doc = " Returns the difference between multiple sorted sets."] + #[doc = ""] + #[doc = " The `WITHSCORES` form, the members and their scores."] #[doc = ""] #[doc = " See also `zdiff`."] #[doc = ""] @@ -4893,8 +4869,7 @@ impl Guest for Commands { .await } - #[doc = " Stores the members of the first sorted set that are not in any of the others, returning how"] - #[doc = " many."] + #[doc = " Stores the difference of multiple sorted sets in a key."] #[doc = ""] #[doc = " ZDIFFSTORE destination numkeys key [ key ... ]"] #[doc = " https://valkey.io/commands/zdiffstore/"] @@ -4915,7 +4890,7 @@ impl Guest for Commands { .await } - #[doc = " Increments the score of the member in the sorted set stored at key, returning the new score."] + #[doc = " Increments the score of a member in a sorted set."] #[doc = ""] #[doc = " ZINCRBY key increment member"] #[doc = " https://valkey.io/commands/zincrby/"] @@ -4929,9 +4904,7 @@ impl Guest for Commands { exchange(connection, ("ZINCRBY", key, increment, member)).await } - #[doc = " The members in all of the sorted sets."] - #[doc = ""] - #[doc = " The weights, when given, multiply the scores of each sorted set, one for each key."] + #[doc = " Returns the intersect of multiple sorted sets."] #[doc = ""] #[doc = " See also `zinter-withscores`."] #[doc = ""] @@ -4956,7 +4929,9 @@ impl Guest for Commands { .await } - #[doc = " The members, and their combined scores, in all of the sorted sets."] + #[doc = " Returns the intersect of multiple sorted sets."] + #[doc = ""] + #[doc = " The `WITHSCORES` form, the members and their combined scores."] #[doc = ""] #[doc = " See also `zinter`."] #[doc = ""] @@ -4979,7 +4954,7 @@ impl Guest for Commands { exchange(connection, command).await } - #[doc = " The number of members in all of the sorted sets, counting stops at the limit."] + #[doc = " Returns the number of members of the intersect of multiple sorted sets."] #[doc = ""] #[doc = " ZINTERCARD numkeys key [ key ... ] [ LIMIT limit ]"] #[doc = " https://valkey.io/commands/zintercard/"] @@ -5000,7 +4975,7 @@ impl Guest for Commands { .await } - #[doc = " Stores the members in all of the sorted sets, with their combined scores, returning how many."] + #[doc = " Stores the intersect of multiple sorted sets in a key."] #[doc = ""] #[doc = " ZINTERSTORE destination numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ]"] #[doc = " https://valkey.io/commands/zinterstore/"] @@ -5022,8 +4997,7 @@ impl Guest for Commands { exchange(connection, command).await } - #[doc = " The number of members of the sorted set stored at key in the range, for members of equal"] - #[doc = " score."] + #[doc = " Returns the number of members in a sorted set within a lexicographical range."] #[doc = ""] #[doc = " ZLEXCOUNT key min max"] #[doc = " https://valkey.io/commands/zlexcount/"] @@ -5049,8 +5023,7 @@ impl Guest for Commands { .await } - #[doc = " Removes the members with the lowest or highest scores from the first non-empty sorted set,"] - #[doc = " returning its key and the members and their scores, none when every sorted set is empty."] + #[doc = " Returns the highest- or lowest-scoring members from one or more sorted sets after removing them."] #[doc = ""] #[doc = " ZMPOP numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ]"] #[doc = " https://valkey.io/commands/zmpop/"] @@ -5073,8 +5046,7 @@ impl Guest for Commands { .await } - #[doc = " The score of each of the members of the sorted set stored at key, none for a member that is"] - #[doc = " not in it."] + #[doc = " Returns the score of one or more members in a sorted set."] #[doc = ""] #[doc = " ZMSCORE key member [ member ... ]"] #[doc = " https://valkey.io/commands/zmscore/"] @@ -5091,8 +5063,9 @@ impl Guest for Commands { .await } - #[doc = " Removes and returns the members with the highest scores, and their scores, up to the count,"] - #[doc = " one without a count."] + #[doc = " Returns the highest-scoring members from a sorted set after removing them."] + #[doc = ""] + #[doc = " Without a count, at most one member."] #[doc = ""] #[doc = " ZPOPMAX key [ count ]"] #[doc = " https://valkey.io/commands/zpopmax/"] @@ -5106,8 +5079,9 @@ impl Guest for Commands { Ok(members.0) } - #[doc = " Removes and returns the members with the lowest scores, and their scores, up to the count,"] - #[doc = " one without a count."] + #[doc = " Returns the lowest-scoring members from a sorted set after removing them."] + #[doc = ""] + #[doc = " Without a count, at most one member."] #[doc = ""] #[doc = " ZPOPMIN key [ count ]"] #[doc = " https://valkey.io/commands/zpopmin/"] @@ -5121,10 +5095,9 @@ impl Guest for Commands { Ok(members.0) } - #[doc = " Random members of the sorted set stored at key, up to the count."] + #[doc = " Returns one or more random members from a sorted set."] #[doc = ""] - #[doc = " Without a count, one member, or none when the key does not exist. A negative count may return"] - #[doc = " the same member more than once, its absolute value is the number of members returned."] + #[doc = " Without a count, at most one member."] #[doc = ""] #[doc = " See also `zrandmember-withscores`."] #[doc = ""] @@ -5144,10 +5117,9 @@ impl Guest for Commands { Ok(member.into_iter().collect()) } - #[doc = " Random members of the sorted set stored at key, and their scores, up to the count."] + #[doc = " Returns one or more random members from a sorted set."] #[doc = ""] - #[doc = " A negative count may return the same member more than once, its absolute value is the number"] - #[doc = " of members returned."] + #[doc = " The `WITHSCORES` form, the members and their scores."] #[doc = ""] #[doc = " See also `zrandmember`."] #[doc = ""] @@ -5162,14 +5134,13 @@ impl Guest for Commands { exchange(connection, ("ZRANDMEMBER", key, count, "WITHSCORES")).await } - #[doc = " The members of the sorted set stored at key in the range, from the lowest score, or the"] - #[doc = " highest with `rev`."] + #[doc = " Returns members in a sorted set within a range of indexes."] #[doc = ""] - #[doc = " The limit is a range of the members in the range, only with BYSCORE or BYLEX. With `xx`,"] - #[doc = " returns none when the key does not exist."] + #[doc = " Also covers ZRANGEBYLEX, ZRANGEBYSCORE, ZREVRANGE, ZREVRANGEBYLEX and ZREVRANGEBYSCORE. A"] + #[doc = " limit fails the command unless the range is by score or by lex. With `xx`, returns none when"] + #[doc = " the key does not exist."] #[doc = ""] - #[doc = " Also covers the deprecated ZRANGEBYLEX, ZRANGEBYSCORE, ZREVRANGE, ZREVRANGEBYLEX and"] - #[doc = " ZREVRANGEBYSCORE. See also `zrange-withscores`."] + #[doc = " See also `zrange-withscores`."] #[doc = ""] #[doc = " ZRANGE key start stop [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ] [ XX ]"] #[doc = " https://valkey.io/commands/zrange/"] @@ -5186,11 +5157,10 @@ impl Guest for Commands { exchange(connection, ("ZRANGE", key, range, Flag("XX", xx))).await } - #[doc = " The members of the sorted set stored at key in the range, and their scores, from the lowest"] - #[doc = " score, or the highest with `rev`."] + #[doc = " Returns members in a sorted set within a range of indexes."] #[doc = ""] - #[doc = " The limit is a range of the members in the range, only with BYSCORE. With `xx`, returns none"] - #[doc = " when the key does not exist."] + #[doc = " The `WITHSCORES` form, the members and their scores. A limit fails the command unless the"] + #[doc = " range is by score. With `xx`, returns none when the key does not exist."] #[doc = ""] #[doc = " See also `zrange`."] #[doc = ""] @@ -5214,9 +5184,9 @@ impl Guest for Commands { exchange(connection, command).await } - #[doc = " Stores the members of the sorted set stored at source in the range, returning how many."] + #[doc = " Stores a range of members from sorted set in a key."] #[doc = ""] - #[doc = " The limit is a range of the members in the range, only with BYSCORE or BYLEX."] + #[doc = " A limit fails the command unless the range is by score or by lex."] #[doc = ""] #[doc = " ZRANGESTORE dst src min max [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ]"] #[doc = " https://valkey.io/commands/zrangestore/"] @@ -5233,10 +5203,9 @@ impl Guest for Commands { exchange(connection, ("ZRANGESTORE", destination, source, range)).await } - #[doc = " The position of the member in the sorted set stored at key, from the lowest score, none when"] - #[doc = " it is not a member."] + #[doc = " Returns the index of a member in a sorted set ordered by ascending scores."] #[doc = ""] - #[doc = " See also `zrank-withscore` and `zrevrank`."] + #[doc = " See also `zrank-withscore`."] #[doc = ""] #[doc = " ZRANK key member"] #[doc = " https://valkey.io/commands/zrank/"] @@ -5249,8 +5218,9 @@ impl Guest for Commands { exchange(connection, ("ZRANK", key, member)).await } - #[doc = " The position, and the score, of the member in the sorted set stored at key, from the lowest"] - #[doc = " score, none when it is not a member."] + #[doc = " Returns the index of a member in a sorted set ordered by ascending scores."] + #[doc = ""] + #[doc = " The `WITHSCORE` form, the position and the score."] #[doc = ""] #[doc = " See also `zrank`."] #[doc = ""] @@ -5265,7 +5235,7 @@ impl Guest for Commands { exchange(connection, ("ZRANK", key, member, "WITHSCORE")).await } - #[doc = " Removes the members from the sorted set stored at key, returning how many were members."] + #[doc = " Removes one or more members from a sorted set."] #[doc = ""] #[doc = " ZREM key member [ member ... ]"] #[doc = " https://valkey.io/commands/zrem/"] @@ -5278,8 +5248,7 @@ impl Guest for Commands { exchange(connection, ("ZREM", key, NonEmpty::new("members", members))).await } - #[doc = " Removes the members of the sorted set stored at key in the range, for members of equal score,"] - #[doc = " returning how many."] + #[doc = " Removes members in a sorted set within a lexicographical range."] #[doc = ""] #[doc = " ZREMRANGEBYLEX key min max"] #[doc = " https://valkey.io/commands/zremrangebylex/"] @@ -5305,8 +5274,7 @@ impl Guest for Commands { .await } - #[doc = " Removes the members of the sorted set stored at key at the positions, both included,"] - #[doc = " negative from the end, returning how many."] + #[doc = " Removes members in a sorted set within a range of indexes."] #[doc = ""] #[doc = " ZREMRANGEBYRANK key start stop"] #[doc = " https://valkey.io/commands/zremrangebyrank/"] @@ -5320,8 +5288,7 @@ impl Guest for Commands { exchange(connection, ("ZREMRANGEBYRANK", key, start, stop)).await } - #[doc = " Removes the members of the sorted set stored at key with scores in the range, returning how"] - #[doc = " many."] + #[doc = " Removes members in a sorted set within a range of scores."] #[doc = ""] #[doc = " ZREMRANGEBYSCORE key min max"] #[doc = " https://valkey.io/commands/zremrangebyscore/"] @@ -5347,10 +5314,9 @@ impl Guest for Commands { .await } - #[doc = " The position of the member in the sorted set stored at key, from the highest score, none"] - #[doc = " when it is not a member."] + #[doc = " Returns the index of a member in a sorted set ordered by descending scores."] #[doc = ""] - #[doc = " See also `zrevrank-withscore` and `zrank`."] + #[doc = " See also `zrevrank-withscore`."] #[doc = ""] #[doc = " ZREVRANK key member"] #[doc = " https://valkey.io/commands/zrevrank/"] @@ -5363,8 +5329,9 @@ impl Guest for Commands { exchange(connection, ("ZREVRANK", key, member)).await } - #[doc = " The position, and the score, of the member in the sorted set stored at key, from the highest"] - #[doc = " score, none when it is not a member."] + #[doc = " Returns the index of a member in a sorted set ordered by descending scores."] + #[doc = ""] + #[doc = " The `WITHSCORE` form, the position and the score."] #[doc = ""] #[doc = " See also `zrevrank`."] #[doc = ""] @@ -5379,7 +5346,7 @@ impl Guest for Commands { exchange(connection, ("ZREVRANK", key, member, "WITHSCORE")).await } - #[doc = " Incrementally iterate over the members of the sorted set stored at key, and their scores."] + #[doc = " Iterates over members and scores of a sorted set."] #[doc = ""] #[doc = " See also `zscan-noscores`."] #[doc = ""] @@ -5408,7 +5375,9 @@ impl Guest for Commands { Ok((cursor.0, members.0)) } - #[doc = " Incrementally iterate over the members of the sorted set stored at key."] + #[doc = " Iterates over members and scores of a sorted set."] + #[doc = ""] + #[doc = " The `NOSCORES` form, the members only."] #[doc = ""] #[doc = " See also `zscan`."] #[doc = ""] @@ -5437,7 +5406,7 @@ impl Guest for Commands { Ok((cursor.0, items)) } - #[doc = " The score of the member in the sorted set stored at key, none when it is not a member."] + #[doc = " Returns the score of a member in a sorted set."] #[doc = ""] #[doc = " ZSCORE key member"] #[doc = " https://valkey.io/commands/zscore/"] @@ -5450,9 +5419,7 @@ impl Guest for Commands { exchange(connection, ("ZSCORE", key, member)).await } - #[doc = " The members in any of the sorted sets."] - #[doc = ""] - #[doc = " The weights, when given, multiply the scores of each sorted set, one for each key."] + #[doc = " Returns the union of multiple sorted sets."] #[doc = ""] #[doc = " See also `zunion-withscores`."] #[doc = ""] @@ -5477,7 +5444,9 @@ impl Guest for Commands { .await } - #[doc = " The members, and their combined scores, in any of the sorted sets."] + #[doc = " Returns the union of multiple sorted sets."] + #[doc = ""] + #[doc = " The `WITHSCORES` form, the members and their combined scores."] #[doc = ""] #[doc = " See also `zunion`."] #[doc = ""] @@ -5500,7 +5469,7 @@ impl Guest for Commands { exchange(connection, command).await } - #[doc = " Stores the members in any of the sorted sets, with their combined scores, returning how many."] + #[doc = " Stores the union of multiple sorted sets in a key."] #[doc = ""] #[doc = " ZUNIONSTORE destination numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ]"] #[doc = " https://valkey.io/commands/zunionstore/"] diff --git a/wit/commands.wit b/wit/commands.wit index 5ea761a..f9a2fed 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -1,12 +1,40 @@ /// Typed functions for Valkey's commands, over the mechanics of a connection. /// +/// Each function sends one command and converts its reply. The docs of a function cover what +/// differs from the raw command, see the linked Valkey docs for what the command does. Commands +/// that change the state of the connection itself are on the connection, e.g. SUBSCRIBE, MULTI and +/// EXEC, WATCH, RESET and HELLO. +/// /// The types are those of the `types` interface, used here under names that may change, e.g. /// renamed to avoid clashing with a function for a new command. Refer to a type through `types` /// for a name that does not change. /// -/// A list argument must have at least one item, as the command requires, an empty list fails with a -/// client error before anything is sent. A list the command allows to be left out is an option, -/// none rather than empty. +/// The functions follow these idioms: +/// +/// - Arguments are in the order of the command's words. +/// - A list argument must have at least one item, as the command requires, an empty list fails with +/// a client error before anything is sent. A list the command allows to be left out is an +/// option, none rather than empty. +/// - An optional argument is an option, a flag is a bool, e.g. `replace` for `REPLACE`, and a +/// choice of keywords is an enum or a variant, e.g. `list-side` for `LEFT | RIGHT`. +/// - Durations and points in time are records named for their unit, e.g. `duration-seconds` and +/// `unix-time-milliseconds`. +/// - Keys, fields, patterns and names are `string`. Values, members and payloads are +/// `string-or-bytes`, a reply that is not UTF-8 is `bytes`. Values that are binary by nature, like +/// the serialized values of DUMP and RESTORE, are `bytes`. A reply where a `string` is expected +/// that is not UTF-8 fails with a client error. +/// - A null reply is none, e.g. GET of a key that does not exist. +/// - An integer reply that is `0` or `1` is a bool, e.g. whether HSETNX set the field. +/// - An integer reply with sentinel values is a variant or an enum with a case for each, e.g. +/// `ttl-reply` for TTL's `-2` and `-1`, or none when it stands for nothing, e.g. BITPOS's `-1`. +/// - A reply whose shape an option changes, e.g. with `WITHSCORES`, has a function of its own for +/// that form, e.g. `zrange-withscores`, its docs naming the other forms. +/// - A scan starts with the `start` cursor and resumes with the one it returned, none once it is +/// complete. +/// - An error reply fails with a `valkey` error. Arguments the client rejects, and replies it cannot +/// convert, fail with a `client` error. +/// - A command that blocks, e.g. BLPOP, or changes the state of the connection, e.g. SELECT, +/// affects every caller sharing the connection, see the command's docs. interface commands { @since(version = 0.3.0-dev) use connection.{connection}; @@ -47,61 +75,59 @@ interface commands { string-or-bytes, restore-ttl, function-restore-policy }; - /// The ACL categories, or with a category, the commands in it. + /// Lists the ACL categories, or the commands inside a category. /// /// ACL CAT [ category ] /// https://valkey.io/commands/acl-cat/ @since(version = 0.3.0-dev) acl-cat: async func(connection: borrow, category: option) -> result, error>; - /// Delete all the specified ACL users and terminate all the connections that are authenticated with such users. + /// Deletes ACL users, and terminates their connections. /// /// ACL DELUSER username [ username ... ] /// https://valkey.io/commands/acl-deluser/ @since(version = 0.3.0-dev) acl-deluser: async func(connection: borrow, usernames: list) -> result; - /// Whether the user may run the command, without running it. + /// Simulates the execution of a command by a user, without executing the command. /// /// ACL DRYRUN username command [ arg [ arg ... ] ] /// https://valkey.io/commands/acl-dryrun/ @since(version = 0.3.0-dev) acl-dryrun: async func(connection: borrow, username: string, command: list) -> result; - /// Generates a random password. + /// Generates a pseudorandom, secure password that can be used to identify ACL users. /// /// ACL GENPASS [ bits ] /// https://valkey.io/commands/acl-genpass/ @since(version = 0.3.0-dev) acl-genpass: async func(connection: borrow, bits: option) -> result; - /// The rules of the user, none when the user does not exist. + /// Lists the ACL rules of a user. /// - /// The flags, the hashed passwords, the root selector's rules, then the selectors, as the server - /// normalized them, e.g. `allkeys` is `~*`. Applied with `acl-setuser`, after `reset`, they - /// recreate the user. The roles of Valkey 9.2 are not included. + /// None when the user does not exist. The rules are as the server normalized them, e.g. + /// `allkeys` is `~*`. Applied with `acl-setuser`, after `reset`, they recreate the user. /// /// ACL GETUSER username /// https://valkey.io/commands/acl-getuser/ @since(version = 0.3.0-dev) acl-getuser: async func(connection: borrow, username: string) -> result>, error>; - /// The rules of every user, as written to an ACL file. + /// Dumps the effective rules in ACL file format. /// /// ACL LIST /// https://valkey.io/commands/acl-list/ @since(version = 0.3.0-dev) acl-list: async func(connection: borrow) -> result, error>; - /// Replaces the users with those of the configured ACL file. + /// Reloads the rules from the configured ACL file. /// /// ACL LOAD /// https://valkey.io/commands/acl-load/ @since(version = 0.3.0-dev) acl-load: async func(connection: borrow) -> result<_, error>; - /// The recent security events, commands or authentications the rules denied, up to the count, - /// 10 without one. + /// Lists recent security events generated due to ACL rules. /// /// See also `acl-log-reset`. /// @@ -110,7 +136,9 @@ interface commands { @since(version = 0.3.0-dev) acl-log: async func(connection: borrow, count: option) -> result, error>; - /// Clears the security events. + /// Lists recent security events generated due to ACL rules. + /// + /// The `RESET` form, clearing the security events. /// /// See also `acl-log`. /// @@ -119,384 +147,387 @@ interface commands { @since(version = 0.3.0-dev) acl-log-reset: async func(connection: borrow) -> result<_, error>; - /// Saves the users to the configured ACL file. + /// Saves the effective ACL rules in the configured ACL file. /// /// ACL SAVE /// https://valkey.io/commands/acl-save/ @since(version = 0.3.0-dev) acl-save: async func(connection: borrow) -> result<_, error>; - /// Create an ACL user with the specified rules or modify the rules of an existing user. + /// Creates and modifies an ACL user and its rules. /// /// ACL SETUSER username [ rule ] [ [ rule ] ... ] /// https://valkey.io/commands/acl-setuser/ @since(version = 0.3.0-dev) acl-setuser: async func(connection: borrow, username: string, rules: option>) -> result<_, error>; - /// The names of the users. + /// Lists all ACL users. /// /// ACL USERS /// https://valkey.io/commands/acl-users/ @since(version = 0.3.0-dev) acl-users: async func(connection: borrow) -> result, error>; - /// The name of the user the connection is authenticated as. + /// Returns the authenticated username of the current connection. /// /// ACL WHOAMI /// https://valkey.io/commands/acl-whoami/ @since(version = 0.3.0-dev) acl-whoami: async func(connection: borrow) -> result; - /// Starts rewriting the append-only file in the background, returning the server's message. + /// Asynchronously rewrites the append-only file to disk. /// /// BGREWRITEAOF /// https://valkey.io/commands/bgrewriteaof/ @since(version = 0.3.0-dev) bgrewriteaof: async func(connection: borrow) -> result; - /// Starts saving the databases to disk in the background, or schedules or cancels it, returning - /// the server's message. + /// Asynchronously saves the database(s) to disk. /// /// BGSAVE [ SCHEDULE | CANCEL ] /// https://valkey.io/commands/bgsave/ @since(version = 0.3.0-dev) bgsave: async func(connection: borrow, operation: option) -> result; - /// Closes the connections that match every one of the filters, returning how many. + /// Terminates open connections. /// /// CLIENT KILL filter [ filter ... ] /// https://valkey.io/commands/client-kill/ @since(version = 0.3.0-dev) client-kill: async func(connection: borrow, filters: list) -> result; - /// The connections that match every one of the filters, all of them without filters, each a - /// map of its fields, e.g. `id`, `addr` and `name`. + /// Lists open connections. + /// + /// Each connection is a map of its fields, parsed from the server's text, e.g. `id`, `addr` and + /// `name`. /// /// CLIENT LIST [ filter [ filter ... ] ] /// https://valkey.io/commands/client-list/ @since(version = 0.3.0-dev) client-list: async func(connection: borrow, filters: option>) -> result>, error>; - /// Whether the connection may be evicted to free memory, `ON` excludes it. + /// Sets the client eviction mode of the connection. + /// + /// It applies to the connection, so to every caller sharing it. /// /// CLIENT NO-EVICT 〈 ON | OFF 〉 /// https://valkey.io/commands/client-no-evict/ @since(version = 0.3.0-dev) client-no-evict: async func(connection: borrow, enabled: bool) -> result<_, error>; - /// Suspends running the commands of other connections, writes only with `WRITE`, for the time. + /// Suspends commands processing. /// /// CLIENT PAUSE timeout [ WRITE | ALL ] /// https://valkey.io/commands/client-pause/ @since(version = 0.3.0-dev) client-pause: async func(connection: borrow, timeout: duration-milliseconds, mode: option) -> result<_, error>; - /// Unblocks the connection with the ID from a blocking command, as if it timed out, or with an - /// error, returning whether it was blocked. + /// Unblocks a client blocked by a blocking command from a different connection. /// /// CLIENT UNBLOCK client-id [ TIMEOUT | ERROR ] /// https://valkey.io/commands/client-unblock/ @since(version = 0.3.0-dev) client-unblock: async func(connection: borrow, client-id: integer, unblock-type: option) -> result; - /// Resumes running the commands of connections paused with `client-pause`. + /// Resumes processing commands from paused clients. /// /// CLIENT UNPAUSE /// https://valkey.io/commands/client-unpause/ @since(version = 0.3.0-dev) client-unpause: async func(connection: borrow) -> result<_, error>; - /// Assigns the slots to the node. + /// Assigns new hash slots to a node. /// /// CLUSTER ADDSLOTS slot [ slot ... ] /// https://valkey.io/commands/cluster-addslots/ @since(version = 0.3.0-dev) cluster-addslots: async func(connection: borrow, slots: list) -> result<_, error>; - /// Assigns the ranges of slots, start and end both included, to the node. + /// Assigns new hash slot ranges to a node. /// /// CLUSTER ADDSLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] /// https://valkey.io/commands/cluster-addslotsrange/ @since(version = 0.3.0-dev) cluster-addslotsrange: async func(connection: borrow, ranges: list>) -> result<_, error>; - /// Advances the node's config epoch, unless it is already the greatest in the cluster. + /// Advances the cluster config epoch. /// /// CLUSTER BUMPEPOCH /// https://valkey.io/commands/cluster-bumpepoch/ @since(version = 0.3.0-dev) cluster-bumpepoch: async func(connection: borrow) -> result; - /// Cancels the slot migrations in progress. + /// Cancel all current ongoing slot migration operations. /// /// CLUSTER CANCELSLOTMIGRATIONS /// https://valkey.io/commands/cluster-cancelslotmigrations/ @since(version = 0.3.0-dev) cluster-cancelslotmigrations: async func(connection: borrow) -> result<_, error>; - /// The number of active failure reports for the node. + /// Returns the number of active failure reports for a node. /// /// CLUSTER COUNT-FAILURE-REPORTS node-id /// https://valkey.io/commands/cluster-count-failure-reports/ @since(version = 0.3.0-dev) cluster-count-failure-reports: async func(connection: borrow, node-id: string) -> result; - /// Unassigns the slots from the node. + /// Sets hash slots as unbound for a node. /// /// CLUSTER DELSLOTS slot [ slot ... ] /// https://valkey.io/commands/cluster-delslots/ @since(version = 0.3.0-dev) cluster-delslots: async func(connection: borrow, slots: list) -> result<_, error>; - /// Unassigns the ranges of slots, start and end both included, from the node. + /// Sets hash slot ranges as unbound for a node. /// /// CLUSTER DELSLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] /// https://valkey.io/commands/cluster-delslotsrange/ @since(version = 0.3.0-dev) cluster-delslotsrange: async func(connection: borrow, ranges: list>) -> result<_, error>; - /// Fails over the primary of this replica, coordinated with it unless `FORCE` or `TAKEOVER`. + /// Forces a replica to perform a manual failover of its primary. /// /// CLUSTER FAILOVER [ FORCE | TAKEOVER ] /// https://valkey.io/commands/cluster-failover/ @since(version = 0.3.0-dev) cluster-failover: async func(connection: borrow, mode: option) -> result<_, error>; - /// Unassigns all the slots of the node. + /// Deletes all slots information from a node. /// /// CLUSTER FLUSHSLOTS /// https://valkey.io/commands/cluster-flushslots/ @since(version = 0.3.0-dev) cluster-flushslots: async func(connection: borrow) -> result<_, error>; - /// Removes the node from the nodes this node knows of. + /// Removes a node from the nodes table. /// /// CLUSTER FORGET node-id /// https://valkey.io/commands/cluster-forget/ @since(version = 0.3.0-dev) cluster-forget: async func(connection: borrow, node-id: string) -> result<_, error>; - /// The slot migrations in progress, and those recently finished. + /// Gets the status of ongoing and recently finished slot import and export operations. /// /// CLUSTER GETSLOTMIGRATIONS /// https://valkey.io/commands/cluster-getslotmigrations/ @since(version = 0.3.0-dev) cluster-getslotmigrations: async func(connection: borrow) -> result, error>; - /// Connects the node to another, adding it to the cluster. + /// Forces a node to handshake with another node. /// /// CLUSTER MEET ip port [ cluster-bus-port ] /// https://valkey.io/commands/cluster-meet/ @since(version = 0.3.0-dev) cluster-meet: async func(connection: borrow, ip: string, port: u16, cluster-bus-port: option) -> result<_, error>; - /// Migrates the ranges of slots from this node to other nodes. + /// Migrate the given slots from this node to the specified nodes. /// /// CLUSTER MIGRATESLOTS SLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] NODE node-id [ AUTH username password ] [ SLOTSRANGE ... ] /// https://valkey.io/commands/cluster-migrateslots/ @since(version = 0.3.0-dev) cluster-migrateslots: async func(connection: borrow, migrations: list) -> result<_, error>; - /// The replicas of the primary node, each a line in the format of CLUSTER NODES. + /// Lists the replica nodes of a primary node. /// /// CLUSTER REPLICAS node-id /// https://valkey.io/commands/cluster-replicas/ @since(version = 0.3.0-dev) cluster-replicas: async func(connection: borrow, node-id: string) -> result, error>; - /// Makes the node a replica of the primary node, or with none, detaches it from its primary, - /// `NO ONE`. + /// Configures a node as replica of a primary node or detaches a replica from its primary. + /// + /// None detaches the node from its primary, `NO ONE`. /// /// CLUSTER REPLICATE 〈 node-id | NO ONE 〉 /// https://valkey.io/commands/cluster-replicate/ @since(version = 0.3.0-dev) cluster-replicate: async func(connection: borrow, node-id: option) -> result<_, error>; - /// Resets the node, forgetting the other nodes and its slots, `SOFT` without a mode. + /// Resets a node. /// /// CLUSTER RESET [ HARD | SOFT ] /// https://valkey.io/commands/cluster-reset/ @since(version = 0.3.0-dev) cluster-reset: async func(connection: borrow, mode: option) -> result<_, error>; - /// Saves the node's cluster configuration to disk. + /// Forces a node to save the cluster configuration to disk. /// /// CLUSTER SAVECONFIG /// https://valkey.io/commands/cluster-saveconfig/ @since(version = 0.3.0-dev) cluster-saveconfig: async func(connection: borrow) -> result<_, error>; - /// Sets the config epoch of a new node. + /// Sets the configuration epoch for a new node. /// /// CLUSTER SET-CONFIG-EPOCH config-epoch /// https://valkey.io/commands/cluster-set-config-epoch/ @since(version = 0.3.0-dev) cluster-set-config-epoch: async func(connection: borrow, config-epoch: integer) -> result<_, error>; - /// Changes the state of the slot on the node. + /// Binds a hash slot to a node. /// /// CLUSTER SETSLOT slot 〈 IMPORTING node-id | MIGRATING node-id | NODE node-id | STABLE 〉 [ TIMEOUT timeout ] /// https://valkey.io/commands/cluster-setslot/ @since(version = 0.3.0-dev) cluster-setslot: async func(connection: borrow, slot: u16, state: cluster-setslot-state, timeout: option) -> result<_, error>; - /// The entries of the command log of the type, up to the count, all of them for -1. + /// Returns the specified command log's entries. /// /// COMMANDLOG GET count 〈 slow | large-request | large-reply 〉 /// https://valkey.io/commands/commandlog-get/ @since(version = 0.3.0-dev) commandlog-get: async func(connection: borrow, count: integer, %type: commandlog-type) -> result, error>; - /// The number of entries in the command log of the type. + /// Returns the number of entries in the specified type of command log. /// /// COMMANDLOG LEN 〈 slow | large-request | large-reply 〉 /// https://valkey.io/commands/commandlog-len/ @since(version = 0.3.0-dev) commandlog-len: async func(connection: borrow, %type: commandlog-type) -> result; - /// Clears the command log of the type. + /// Clears all entries from the specified type of command log. /// /// COMMANDLOG RESET 〈 slow | large-request | large-reply 〉 /// https://valkey.io/commands/commandlog-reset/ @since(version = 0.3.0-dev) commandlog-reset: async func(connection: borrow, %type: commandlog-type) -> result<_, error>; - /// The values of the configuration parameters matching the glob-style patterns, by name. + /// Returns the effective values of configuration parameters. /// /// CONFIG GET parameter [ parameter ... ] /// https://valkey.io/commands/config-get/ @since(version = 0.3.0-dev) config-get: async func(connection: borrow, parameters: list) -> result, error>; - /// Resets the server's statistics, e.g. those of INFO. + /// Resets the server's statistics. /// /// CONFIG RESETSTAT /// https://valkey.io/commands/config-resetstat/ @since(version = 0.3.0-dev) config-resetstat: async func(connection: borrow) -> result<_, error>; - /// Rewrites the configuration file with the current configuration. + /// Persists the effective configuration to file. /// /// CONFIG REWRITE /// https://valkey.io/commands/config-rewrite/ @since(version = 0.3.0-dev) config-rewrite: async func(connection: borrow) -> result<_, error>; - /// Sets the configuration parameters to the values, all or none of them. + /// Sets configuration parameters in-flight. /// /// CONFIG SET parameter value [ parameter value ... ] /// https://valkey.io/commands/config-set/ @since(version = 0.3.0-dev) config-set: async func(connection: borrow, parameters-and-values: list>) -> result<_, error>; - /// Fails over from this primary to a replica, the one at the target, or aborts a failover in - /// progress. + /// Starts a coordinated failover from a server to one of its replicas. /// /// FAILOVER [ TO host port [ FORCE ] ] [ ABORT ] [ TIMEOUT milliseconds ] /// https://valkey.io/commands/failover/ @since(version = 0.3.0-dev) failover: async func(connection: borrow, target: option, abort: bool, timeout: option) -> result<_, error>; - /// When the databases were last saved to disk. + /// Returns the Unix timestamp of the last successful save to disk. /// /// LASTSAVE /// https://valkey.io/commands/lastsave/ @since(version = 0.3.0-dev) lastsave: async func(connection: borrow) -> result; - /// A report of the latency events, for people to read. + /// Returns a human-readable latency analysis report. /// /// LATENCY DOCTOR /// https://valkey.io/commands/latency-doctor/ @since(version = 0.3.0-dev) latency-doctor: async func(connection: borrow) -> result; - /// A graph of the latency of the event, as text. + /// Returns a latency graph for an event. /// /// LATENCY GRAPH event /// https://valkey.io/commands/latency-graph/ @since(version = 0.3.0-dev) latency-graph: async func(connection: borrow, event: string) -> result; - /// The latency histogram of the commands, all of them without commands, by command name. + /// Returns the cumulative distribution of latencies of a subset or all commands. /// /// LATENCY HISTOGRAM [ command [ command ... ] ] /// https://valkey.io/commands/latency-histogram/ @since(version = 0.3.0-dev) latency-histogram: async func(connection: borrow, commands: option>) -> result, error>; - /// The samples of the latency of the event, when each was taken and the latency. + /// Returns timestamp-latency samples for an event. /// /// LATENCY HISTORY event /// https://valkey.io/commands/latency-history/ @since(version = 0.3.0-dev) latency-history: async func(connection: borrow, event: string) -> result>, error>; - /// The latest sample of the latency of each event. + /// Returns the latest latency samples for all events. /// /// LATENCY LATEST /// https://valkey.io/commands/latency-latest/ @since(version = 0.3.0-dev) latency-latest: async func(connection: borrow) -> result, error>; - /// Clears the samples of the events, all of them without events, returning how many events. + /// Resets the latency data for one or more events. /// /// LATENCY RESET [ event [ event ... ] ] /// https://valkey.io/commands/latency-reset/ @since(version = 0.3.0-dev) latency-reset: async func(connection: borrow, events: option>) -> result; - /// The loaded modules. + /// Returns all loaded modules. /// /// MODULE LIST /// https://valkey.io/commands/module-list/ @since(version = 0.3.0-dev) module-list: async func(connection: borrow) -> result, error>; - /// Loads the module at the path, with the arguments. + /// Loads a module. /// /// MODULE LOAD path [ arg [ arg ... ] ] /// https://valkey.io/commands/module-load/ @since(version = 0.3.0-dev) module-load: async func(connection: borrow, path: string, args: option>) -> result<_, error>; - /// Loads the module at the path, with the configuration parameters and arguments. + /// Loads a module using extended parameters. /// /// MODULE LOADEX path [ CONFIG name value [ CONFIG name value ... ] ] [ ARGS args [ args ... ] ] /// https://valkey.io/commands/module-loadex/ @since(version = 0.3.0-dev) module-loadex: async func(connection: borrow, path: string, configs: option>>, args: option>) -> result<_, error>; - /// Unloads the module, by name. + /// Unloads a module. /// /// MODULE UNLOAD name /// https://valkey.io/commands/module-unload/ @since(version = 0.3.0-dev) module-unload: async func(connection: borrow, name: string) -> result<_, error>; - /// Makes the server a replica of the primary at the host and port, or with none, a primary, - /// `NO ONE`, returning the server's message. + /// Configures a server as replica of another, or promotes it to a primary. + /// + /// None makes the server a primary, `NO ONE`. /// /// REPLICAOF 〈 host port | NO ONE 〉 /// https://valkey.io/commands/replicaof/ @since(version = 0.3.0-dev) replicaof: async func(connection: borrow, primary: option>) -> result; - /// The server's role in replication, and its state. + /// Returns the replication role. /// /// ROLE /// https://valkey.io/commands/role/ @since(version = 0.3.0-dev) role: async func(connection: borrow) -> result; - /// Saves the databases to disk, blocking the server until done. + /// Synchronously saves the database(s) to disk. /// /// SAVE /// https://valkey.io/commands/save/ @since(version = 0.3.0-dev) save: async func(connection: borrow) -> result<_, error>; - /// Stops the server, saving to disk as configured, or as the save mode says. + /// Synchronously saves the database(s) to disk and shuts down the server. /// /// The server closes the connection once it stops, so a shutdown that succeeds fails the call /// with a closed error. @@ -508,7 +539,9 @@ interface commands { @since(version = 0.3.0-dev) shutdown: async func(connection: borrow, save: option, modifiers: shutdown-flags) -> result<_, error>; - /// Aborts a shutdown in progress. + /// Synchronously saves the database(s) to disk and shuts down the server. + /// + /// The `ABORT` form, aborting a shutdown in progress. /// /// See also `shutdown`. /// @@ -517,83 +550,81 @@ interface commands { @since(version = 0.3.0-dev) shutdown-abort: async func(connection: borrow) -> result<_, error>; - /// The entries of the slow log, up to the count, 10 without one, all of them for -1. + /// Returns the slow log's entries. /// /// SLOWLOG GET [ count ] /// https://valkey.io/commands/slowlog-get/ @since(version = 0.3.0-dev) slowlog-get: async func(connection: borrow, count: option) -> result, error>; - /// The number of entries in the slow log. + /// Returns the number of entries in the slow log. /// /// SLOWLOG LEN /// https://valkey.io/commands/slowlog-len/ @since(version = 0.3.0-dev) slowlog-len: async func(connection: borrow) -> result; - /// Clears the slow log. + /// Clears all entries from the slow log. /// /// SLOWLOG RESET /// https://valkey.io/commands/slowlog-reset/ @since(version = 0.3.0-dev) slowlog-reset: async func(connection: borrow) -> result<_, error>; - /// The number of bits set to 1 in the string stored at key, in the range, 0 when the key does - /// not exist. + /// Counts the number of set bits (population counting) in a string. /// /// BITCOUNT key [ start [ end [ BYTE | BIT ] ] ] /// https://valkey.io/commands/bitcount/ @since(version = 0.3.0-dev) bitcount: async func(connection: borrow, key: string, range: option) -> result; - /// Reads and writes integer fields of the string stored at key, returning the result of each - /// operation in order, none for a write that failed with `OVERFLOW FAIL`. + /// Performs arbitrary bitfield integer operations on strings. + /// + /// None for a write that failed with `OVERFLOW FAIL`. /// /// BITFIELD key [ GET encoding offset | [ OVERFLOW WRAP | SAT | FAIL ] 〈 SET encoding offset value | INCRBY encoding offset increment 〉 [ GET encoding offset | [ OVERFLOW WRAP | SAT | FAIL ] 〈 SET encoding offset value | INCRBY encoding offset increment 〉 ... ] ] /// https://valkey.io/commands/bitfield/ @since(version = 0.3.0-dev) bitfield: async func(connection: borrow, key: string, operations: option>) -> result>, error>; - /// Reads integer fields of the string stored at key, the read-only variant of `bitfield`. + /// Performs arbitrary read-only bitfield integer operations on strings. /// /// BITFIELD_RO key [ GET encoding offset [ GET encoding offset ... ] ] /// https://valkey.io/commands/bitfield_ro/ @since(version = 0.3.0-dev) bitfield-ro: async func(connection: borrow, key: string, gets: option>) -> result, error>; - /// Stores the bitwise operation of the strings stored at the source keys in the destination, - /// returning its length, the length of the longest source. + /// Performs bitwise operations on multiple strings, and stores the result. /// /// BITOP 〈 AND | OR | XOR | NOT 〉 destkey key [ key ... ] /// https://valkey.io/commands/bitop/ @since(version = 0.3.0-dev) bitop: async func(connection: borrow, destination: string, operation: bitop-operation) -> result; - /// The position of the first bit set to the bit in the string stored at key, in the range, - /// none when there is none. + /// Finds the first set (1) or clear (0) bit in a string. + /// + /// None when no bit matches, the server's `-1`. /// /// BITPOS key bit [ start [ end [ BYTE | BIT ] ] ] /// https://valkey.io/commands/bitpos/ @since(version = 0.3.0-dev) bitpos: async func(connection: borrow, key: string, bit: bool, range: option) -> result, error>; - /// The bit at the offset of the string stored at key, false beyond its end, or when the key - /// does not exist. + /// Returns a bit value by offset. /// /// GETBIT key offset /// https://valkey.io/commands/getbit/ @since(version = 0.3.0-dev) getbit: async func(connection: borrow, key: string, offset: u64) -> result; - /// Sets the bit at the offset of the string stored at key, growing it as needed, returning the - /// previous bit. + /// Sets or clears the bit at offset of the string value. /// /// SETBIT key offset value /// https://valkey.io/commands/setbit/ @since(version = 0.3.0-dev) setbit: async func(connection: borrow, key: string, offset: u64, value: bool) -> result; - /// Signals that the next command follows an ASK redirect, for a slot being imported. + /// Signals that a cluster client is following an -ASK redirect. /// /// It applies to the next command on the connection, which may be another caller's when the /// connection is shared. Use a connection of your own. @@ -603,15 +634,14 @@ interface commands { @since(version = 0.3.0-dev) asking: async func(connection: borrow) -> result<_, error>; - /// Authenticates the current connection. + /// Authenticates the connection. /// /// AUTH [ username ] password /// https://valkey.io/commands/auth/ @since(version = 0.3.0-dev) auth: async func(connection: borrow, username: option, password: string) -> result<_, error>; - /// Whether the keys the next command reads are tracked, with tracking in `optin` or `optout` - /// mode. + /// Instructs the server whether to track the keys in the next request. /// /// It applies to the next command on the connection, which may be another caller's when the /// connection is shared. Use a connection of your own. @@ -621,43 +651,44 @@ interface commands { @since(version = 0.3.0-dev) client-caching: async func(connection: borrow, enabled: bool) -> result<_, error>; - /// Declares the capabilities the client supports, e.g. `redirect`. + /// A client claims its capability. /// /// CLIENT CAPA capability [ capability ... ] /// https://valkey.io/commands/client-capa/ @since(version = 0.3.0-dev) client-capa: async func(connection: borrow, capabilities: list) -> result<_, error>; - /// The name of the connection, none when it has none. + /// Returns the name of the connection. /// /// CLIENT GETNAME /// https://valkey.io/commands/client-getname/ @since(version = 0.3.0-dev) client-getname: async func(connection: borrow) -> result, error>; - /// The ID of the connection, unique until the server restarts. + /// Returns the unique client ID of the connection. /// /// CLIENT ID /// https://valkey.io/commands/client-id/ @since(version = 0.3.0-dev) client-id: async func(connection: borrow) -> result; - /// Whether the connection is an import source, which may write while the server is in import - /// mode. + /// Marks this client as an import source when the server is in import mode. /// /// CLIENT IMPORT-SOURCE 〈 ON | OFF 〉 /// https://valkey.io/commands/client-import-source/ @since(version = 0.3.0-dev) client-import-source: async func(connection: borrow, enabled: bool) -> result<_, error>; - /// The fields of the connection, as CLIENT LIST has them, e.g. `id`, `addr` and `name`. + /// Returns information about the connection. + /// + /// A map of the fields, parsed from the server's text, e.g. `id`, `addr` and `name`. /// /// CLIENT INFO /// https://valkey.io/commands/client-info/ @since(version = 0.3.0-dev) client-info: async func(connection: borrow) -> result, error>; - /// Whether the connection's commands leave the access time of the keys they read unchanged. + /// Controls whether commands sent by the client affect the LRU/LFU of accessed keys. /// /// It applies to the connection, so to every caller sharing it. /// @@ -666,104 +697,105 @@ interface commands { @since(version = 0.3.0-dev) client-no-touch: async func(connection: borrow, enabled: bool) -> result<_, error>; - /// Sets the name or version of the library of the connection, see CLIENT LIST. + /// Sets information specific to the client or connection. /// /// CLIENT SETINFO 〈 LIB-NAME libname | LIB-VER libver 〉 /// https://valkey.io/commands/client-setinfo/ @since(version = 0.3.0-dev) client-setinfo: async func(connection: borrow, attr: client-setinfo-attr) -> result<_, error>; - /// Sets the name of the connection. + /// Sets the connection name. /// /// CLIENT SETNAME connection-name /// https://valkey.io/commands/client-setname/ @since(version = 0.3.0-dev) client-setname: async func(connection: borrow, connection-name: string) -> result<_, error>; - /// Turns tracking on with the options, or off with none, for server assisted client side - /// caching. Invalidations arrive as push messages, see `connection.dead-letters`. + /// Controls server-assisted client-side caching for the connection. /// - /// It applies to the connection, so to every caller sharing it. + /// Invalidations arrive as push messages, see `connection.dead-letters`. It applies to the + /// connection, so to every caller sharing it. /// /// CLIENT TRACKING 〈 ON | OFF 〉 [ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN ] [ OPTOUT ] [ NOLOOP ] /// https://valkey.io/commands/client-tracking/ @since(version = 0.3.0-dev) client-tracking: async func(connection: borrow, options: option) -> result<_, error>; - /// The connection's tracking state. + /// Returns information about server-assisted client-side caching for the connection. /// /// CLIENT TRACKINGINFO /// https://valkey.io/commands/client-trackinginfo/ @since(version = 0.3.0-dev) client-trackinginfo: async func(connection: borrow) -> result; - /// Every command of the server, a container followed by its subcommands. + /// Returns detailed information about all commands. /// /// COMMAND /// https://valkey.io/commands/command/ @since(version = 0.3.0-dev) command: async func(connection: borrow) -> result, error>; - /// The number of commands of the server. + /// Returns a count of commands. /// /// COMMAND COUNT /// https://valkey.io/commands/command-count/ @since(version = 0.3.0-dev) command-count: async func(connection: borrow) -> result; - /// The documentation of the commands, all of them without names, a container followed by its - /// subcommands, commands that do not exist left out. + /// Returns documentary information about one, multiple or all commands. + /// + /// A container is followed by its subcommands. Commands that do not exist are left out. /// /// COMMAND DOCS [ command-name [ command-name ... ] ] /// https://valkey.io/commands/command-docs/ @since(version = 0.3.0-dev) command-docs: async func(connection: borrow, command-names: option>) -> result, error>; - /// The keys of the command, its name and arguments. + /// Extracts the key names from an arbitrary command. /// /// COMMAND GETKEYS command [ arg [ arg ... ] ] /// https://valkey.io/commands/command-getkeys/ @since(version = 0.3.0-dev) command-getkeys: async func(connection: borrow, command: list) -> result, error>; - /// The keys of the command, its name and arguments, and how the command uses each, e.g. `RW` - /// and `access`. + /// Extracts the key names and access flags for an arbitrary command. /// /// COMMAND GETKEYSANDFLAGS command [ arg [ arg ... ] ] /// https://valkey.io/commands/command-getkeysandflags/ @since(version = 0.3.0-dev) command-getkeysandflags: async func(connection: borrow, command: list) -> result>>, error>; - /// The commands, all of them without names, a container followed by its subcommands, commands - /// that do not exist left out. + /// Returns information about one, multiple or all commands. + /// + /// A container is followed by its subcommands. Commands that do not exist are left out. /// /// COMMAND INFO [ command-name [ command-name ... ] ] /// https://valkey.io/commands/command-info/ @since(version = 0.3.0-dev) command-info: async func(connection: borrow, command-names: option>) -> result, error>; - /// The names of the commands of the server, those that match the filter with one. + /// Returns a list of command names. /// /// COMMAND LIST [ FILTERBY 〈 MODULE module-name | ACLCAT category | PATTERN pattern 〉 ] /// https://valkey.io/commands/command-list/ @since(version = 0.3.0-dev) command-list: async func(connection: borrow, filter: option) -> result, error>; - /// Returns the message. + /// Returns the given string. /// /// ECHO message /// https://valkey.io/commands/echo/ @since(version = 0.3.0-dev) echo: async func(connection: borrow, message: string-or-bytes) -> result; - /// Returns PONG if no argument is provided, otherwise return a copy of the argument as a bulk. + /// Returns the server's liveliness response. /// /// PING [ message ] /// https://valkey.io/commands/ping/ @since(version = 0.3.0-dev) ping: async func(connection: borrow, message: option) -> result<_, error>; - /// Allows the connection to read from a replica of a cluster, for the slots of its primary. + /// Enables read-only queries for a connection to a Valkey replica node. /// /// It applies to the connection, so to every caller sharing it. /// @@ -772,7 +804,7 @@ interface commands { @since(version = 0.3.0-dev) readonly: async func(connection: borrow) -> result<_, error>; - /// Disallows the connection to read from a replica of a cluster, the default. + /// Enables read-write queries for a connection to a Valkey replica node. /// /// It applies to the connection, so to every caller sharing it. /// @@ -781,7 +813,7 @@ interface commands { @since(version = 0.3.0-dev) readwrite: async func(connection: borrow) -> result<_, error>; - /// Changes the database of the connection. + /// Changes the selected database. /// /// It applies to the connection, so to every caller sharing it. /// @@ -790,8 +822,7 @@ interface commands { @since(version = 0.3.0-dev) select: async func(connection: borrow, index: integer) -> result<_, error>; - /// Waits until the connection's writes reached the number of replicas, or the timeout passed, - /// returning how many replicas they reached. A timeout of zero waits indefinitely. + /// Blocks until the asynchronous replication of all preceding write commands sent by the connection is completed. /// /// The connection waits too, replies to commands sent after it on the connection wait behind /// it. Use a connection of your own. @@ -801,9 +832,7 @@ interface commands { @since(version = 0.3.0-dev) wait: async func(connection: borrow, numreplicas: integer, timeout: duration-milliseconds) -> result; - /// Waits until the connection's writes are in the append-only file of the primary, and of the - /// number of replicas, or the timeout passed, returning how many of each they reached. A timeout - /// of zero waits indefinitely. + /// Blocks until all of the preceding write commands sent by the connection are written to the append-only file of the primary and/or replicas. /// /// The connection waits too, replies to commands sent after it on the connection wait behind /// it. Use a connection of your own. @@ -813,36 +842,35 @@ interface commands { @since(version = 0.3.0-dev) waitaof: async func(connection: borrow, numlocal: integer, numreplicas: integer, timeout: duration-milliseconds) -> result, error>; - /// Adds the members at their positions to the geospatial index stored at key, or updates their - /// positions, returning how many were added, or with `changed`, added or updated. + /// Adds one or more members to a geospatial index. /// /// GEOADD key [ NX | XX ] [ CH ] longitude latitude member [ longitude latitude member ... ] /// https://valkey.io/commands/geoadd/ @since(version = 0.3.0-dev) geoadd: async func(connection: borrow, key: string, condition: option, changed: bool, coordinates-and-members: list>) -> result; - /// The distance between the members, in meters or the unit, none when either is not a member. + /// Returns the distance between two members of a geospatial index. /// /// GEODIST key member1 member2 [ m | km | ft | mi ] /// https://valkey.io/commands/geodist/ @since(version = 0.3.0-dev) geodist: async func(connection: borrow, key: string, member1: string-or-bytes, member2: string-or-bytes, unit: option) -> result, error>; - /// The geohash of each of the members, none for a member that is not in the index. + /// Returns members from a geospatial index as geohash strings. /// /// GEOHASH key [ member [ member ... ] ] /// https://valkey.io/commands/geohash/ @since(version = 0.3.0-dev) geohash: async func(connection: borrow, key: string, members: option>) -> result>, error>; - /// The position of each of the members, none for a member that is not in the index. + /// Returns the longitude and latitude of members from a geospatial index. /// /// GEOPOS key [ member [ member ... ] ] /// https://valkey.io/commands/geopos/ @since(version = 0.3.0-dev) geopos: async func(connection: borrow, key: string, members: option>) -> result>, error>; - /// The members of the geospatial index stored at key in the shape. + /// Queries a geospatial index for members inside an area of a box, circle, or a polygon. /// /// See also `geosearch-with`. /// @@ -851,7 +879,9 @@ interface commands { @since(version = 0.3.0-dev) geosearch: async func(connection: borrow, key: string, shape: geosearch-shape, order: option, count: option) -> result, error>; - /// The members of the geospatial index stored at key in the shape, with the details asked for. + /// Queries a geospatial index for members inside an area of a box, circle, or a polygon. + /// + /// Returns each member with the details asked for, `WITHCOORD`, `WITHDIST` and `WITHHASH`. /// /// See also `geosearch`. /// @@ -860,187 +890,176 @@ interface commands { @since(version = 0.3.0-dev) geosearch-with: async func(connection: borrow, key: string, shape: geosearch-shape, order: option, count: option, details: geosearch-with-flags) -> result, error>; - /// Stores the members of the geospatial index stored at source in the shape, returning how - /// many, with their positions, or with `storedist`, their distance from the center as the - /// score. + /// Queries a geospatial index for members inside an area of a box, a circle, or a polygon, optionally stores the result. /// /// GEOSEARCHSTORE destination source [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] [ STOREDIST ] /// https://valkey.io/commands/geosearchstore/ @since(version = 0.3.0-dev) geosearchstore: async func(connection: borrow, destination: string, source: string, shape: geosearchstore-shape, order: option, count: option, storedist: bool) -> result; - /// Removes the specified fields from the hash stored at key. + /// Deletes one or more fields and their values from a hash. /// /// HDEL key field [ field ... ] /// https://valkey.io/commands/hdel/ @since(version = 0.3.0-dev) hdel: async func(connection: borrow, key: string, fields: list) -> result; - /// Returns if field is an existing field in the hash stored at key. + /// Determines whether a field exists in a hash. /// /// HEXISTS key field /// https://valkey.io/commands/hexists/ @since(version = 0.3.0-dev) hexists: async func(connection: borrow, key: string, field: string) -> result; - /// Manipulate the expiration time for existing hash fields. + /// Sets the expiration time of one or more hash fields in seconds. /// /// HEXPIRE key seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpire/ @since(version = 0.3.0-dev) hexpire: async func(connection: borrow, key: string, seconds: duration-seconds, mode: option, fields: list) -> result, error>; - /// Manipulate the expiration time for existing hash fields. + /// Sets the expiration time of one or more hash fields to a Unix timestamp. /// /// HEXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpireat/ @since(version = 0.3.0-dev) hexpireat: async func(connection: borrow, key: string, seconds: unix-time-seconds, mode: option, fields: list) -> result, error>; - /// When each of the hash fields expires, to the second. - /// - /// See also `hpexpiretime`. + /// Returns Unix timestamps in seconds since the epoch at which the given key's field(s) will expire. /// /// HEXPIRETIME key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpiretime/ @since(version = 0.3.0-dev) hexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; - /// Returns the value associated with field in the hash stored at key. + /// Returns the value of a field in a hash. /// /// HGET key field /// https://valkey.io/commands/hget/ @since(version = 0.3.0-dev) hget: async func(connection: borrow, key: string, field: string) -> result, error>; - /// Returns all fields and values of the hash stored at key. + /// Returns all fields and values in a hash. /// /// HGETALL key /// https://valkey.io/commands/hgetall/ @since(version = 0.3.0-dev) hgetall: async func(connection: borrow, key: string) -> result, error>; - /// The values of the hash fields, none for a field that does not exist, deleting the fields. + /// Returns the values of one or more fields and deletes them from a hash. /// /// HGETDEL key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hgetdel/ @since(version = 0.3.0-dev) hgetdel: async func(connection: borrow, key: string, fields: list) -> result>, error>; - /// The values of the hash fields, none for a field that does not exist, optionally setting or - /// removing their expiration. + /// Gets the value of one or more fields of a given hash key, and optionally sets their expiration time or time-to-live (TTL). /// /// HGETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hgetex/ @since(version = 0.3.0-dev) hgetex: async func(connection: borrow, key: string, ttl: option, fields: list) -> result>, error>; - /// Increments the number stored at field in the hash stored at key by increment. + /// Increments the integer value of a field in a hash by a number. /// /// HINCRBY key field increment /// https://valkey.io/commands/hincrby/ @since(version = 0.3.0-dev) hincrby: async func(connection: borrow, key: string, field: string, increment: integer) -> result; - /// Increment the specified field of a hash stored at key, and representing a floating point number, by the specified increment. + /// Increments the floating point value of a field by a number. /// /// HINCRBYFLOAT key field increment /// https://valkey.io/commands/hincrbyfloat/ @since(version = 0.3.0-dev) hincrbyfloat: async func(connection: borrow, key: string, field: string, increment: double) -> result; - /// Returns all field names in the hash stored at key. + /// Returns all fields in a hash. /// /// HKEYS key /// https://valkey.io/commands/hkeys/ @since(version = 0.3.0-dev) hkeys: async func(connection: borrow, key: string) -> result, error>; - /// Returns the number of fields contained in the hash stored at key. + /// Returns the number of fields in a hash. /// /// HLEN key /// https://valkey.io/commands/hlen/ @since(version = 0.3.0-dev) hlen: async func(connection: borrow, key: string) -> result; - /// Returns the values associated with the specified fields in the hash stored at key. + /// Returns the values of all fields in a hash. /// /// HMGET key field [ field ... ] /// https://valkey.io/commands/hmget/ @since(version = 0.3.0-dev) hmget: async func(connection: borrow, key: string, fields: list) -> result>, error>; - /// Sets the specified fields to their respective values in the hash stored at key. + /// Sets the values of multiple fields. /// /// HMSET key field value [ field value ... ] /// https://valkey.io/commands/hmset/ @since(version = 0.3.0-dev) hmset: async func(connection: borrow, key: string, field-and-values: list>) -> result<_, error>; - /// Removes the expiration of the hash fields. + /// Remove the existing expiration on a hash key's field(s). /// /// HPERSIST key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpersist/ @since(version = 0.3.0-dev) hpersist: async func(connection: borrow, key: string, fields: list) -> result, error>; - /// Manipulate the expiration time for existing hash fields, in milliseconds. + /// Sets the expiration time of one or more hash fields in milliseconds. /// /// HPEXPIRE key milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpire/ @since(version = 0.3.0-dev) hpexpire: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, mode: option, fields: list) -> result, error>; - /// Manipulate the expiration time for existing hash fields, as a Unix time in milliseconds. + /// Sets the expiration time of one or more hash fields to a Unix timestamp in milliseconds. /// /// HPEXPIREAT key unix-time-milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpireat/ @since(version = 0.3.0-dev) hpexpireat: async func(connection: borrow, key: string, milliseconds: unix-time-milliseconds, mode: option, fields: list) -> result, error>; - /// When each of the hash fields expires, to the millisecond. - /// - /// See also `hexpiretime`. + /// Returns the Unix timestamp in milliseconds since Unix epoch at which the given key's field(s) will expire. /// /// HPEXPIRETIME key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpiretime/ @since(version = 0.3.0-dev) hpexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; - /// The remaining time to live of each of the hash fields, to the millisecond. - /// - /// See also `httl`. + /// Returns the remaining time to live in milliseconds of a hash key's field(s) that have an associated expiration. /// /// HPTTL key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpttl/ @since(version = 0.3.0-dev) hpttl: async func(connection: borrow, key: string, fields: list) -> result, error>; - /// Random fields from the hash value stored at key, up to the count. + /// Returns one or more random fields from a hash. /// - /// Without a count, one field, or none when the key does not exist. A negative count may return - /// the same field more than once, its absolute value is the number of fields returned. + /// Without a count, at most one field. /// - /// Also see `hrandfield-withvalues`. + /// See also `hrandfield-withvalues`. /// /// HRANDFIELD key [ count ] /// https://valkey.io/commands/hrandfield/ @since(version = 0.3.0-dev) hrandfield: async func(connection: borrow, key: string, count: option) -> result, error>; - /// Random fields and their values from the hash value stored at key, up to the count. + /// Returns one or more random fields from a hash. /// - /// A negative count may return the same field more than once, its absolute value is the number of - /// fields returned. + /// The `WITHVALUES` form, the fields and their values. /// - /// Also see `hrandfield`. + /// See also `hrandfield`. /// /// HRANDFIELD key count WITHVALUES /// https://valkey.io/commands/hrandfield/ @since(version = 0.3.0-dev) hrandfield-withvalues: async func(connection: borrow, key: string, count: integer) -> result>, error>; - /// Incrementally iterate over a collection of elements. + /// Iterates over fields and values of a hash. /// /// See also `hscan-novalues`. /// @@ -1049,7 +1068,9 @@ interface commands { @since(version = 0.3.0-dev) hscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, map>, error>; - /// Incrementally iterate over a collection of keys. + /// Iterates over fields and values of a hash. + /// + /// The `NOVALUES` form, the fields only. /// /// See also `hscan`. /// @@ -1058,88 +1079,77 @@ interface commands { @since(version = 0.3.0-dev) hscan-novalues: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; - /// Sets the specified fields to their respective values in the hash stored at key. + /// Creates or modifies the value of a field in a hash. /// /// HSET key field value [ field value ... ] /// https://valkey.io/commands/hset/ @since(version = 0.3.0-dev) hset: async func(connection: borrow, key: string, field-and-values: list>) -> result; - /// Sets the fields to their values in the hash stored at key, optionally setting their - /// expiration, returning whether they were set, false when a condition prevented it. + /// Sets the value of one or more fields of a given hash key, and optionally sets their expiration time. /// /// HSETEX key [ NX | XX ] [ FNX | FXX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] FIELDS numfields field value [ field value ... ] /// https://valkey.io/commands/hsetex/ @since(version = 0.3.0-dev) hsetex: async func(connection: borrow, key: string, condition: option, field-condition: option, ttl: option, field-and-values: list>) -> result; - /// Sets field in the hash stored at key to value, only if field does not yet exist. + /// Sets the value of a field in a hash only when the field doesn't exist. /// /// HSETNX key field value /// https://valkey.io/commands/hsetnx/ @since(version = 0.3.0-dev) hsetnx: async func(connection: borrow, key: string, field: string, value: string-or-bytes) -> result; - /// Returns the string length of the value associated with field in the hash stored at key. + /// Returns the length of the value of a field. /// /// HSTRLEN key field /// https://valkey.io/commands/hstrlen/ @since(version = 0.3.0-dev) hstrlen: async func(connection: borrow, key: string, field: string) -> result; - /// The remaining time to live of each of the hash fields, to the second. - /// - /// See also `hpttl`. + /// Returns the remaining time to live (in seconds) of a hash key's field(s) that have an associated expiration. /// /// HTTL key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/httl/ @since(version = 0.3.0-dev) httl: async func(connection: borrow, key: string, fields: list) -> result, error>; - /// All values in the hash stored at key. + /// Returns all values in a hash. /// /// HVALS key /// https://valkey.io/commands/hvals/ @since(version = 0.3.0-dev) hvals: async func(connection: borrow, key: string) -> result, error>; - /// Adds the elements to the HyperLogLog stored at key, creating it when it does not exist, - /// returning whether its estimate may have changed. - /// - /// Without elements, creates an empty HyperLogLog, true only when the key did not exist. + /// Adds elements to a HyperLogLog key. /// /// PFADD key [ element [ element ... ] ] /// https://valkey.io/commands/pfadd/ @since(version = 0.3.0-dev) pfadd: async func(connection: borrow, key: string, elements: option>) -> result; - /// The approximate number of unique elements added to the HyperLogLogs, of their union for - /// more than one, 0 for a key that does not exist. + /// Returns the approximated cardinality of the set(s) observed by the HyperLogLog key(s). /// /// PFCOUNT key [ key ... ] /// https://valkey.io/commands/pfcount/ @since(version = 0.3.0-dev) pfcount: async func(connection: borrow, keys: list) -> result; - /// Merges the HyperLogLogs stored at the source keys into the one stored at destination, - /// creating it when it does not exist. - /// - /// Without source keys, creates an empty HyperLogLog when destination does not exist. + /// Merges one or more HyperLogLog values into a single key. /// /// PFMERGE destkey [ sourcekey [ sourcekey ... ] ] /// https://valkey.io/commands/pfmerge/ @since(version = 0.3.0-dev) pfmerge: async func(connection: borrow, destination: string, sources: option>) -> result<_, error>; - /// Removes all the keys of the slot, in the mode, the server's `lazyfree-lazy-user-flush` - /// without one. + /// Removes all keys from the target slot. /// /// CLUSTER FLUSHSLOT slot [ ASYNC | SYNC ] /// https://valkey.io/commands/cluster-flushslot/ @since(version = 0.3.0-dev) cluster-flushslot: async func(connection: borrow, slot: u16, mode: option) -> result<_, error>; - /// Incrementally iterate over the keys of a cluster, slot by slot, or within the slot. + /// Iterates over the keys in the cluster. /// /// The cursor carries the slot, for clients to route each call to the node that owns it. /// @@ -1148,515 +1158,470 @@ interface commands { @since(version = 0.3.0-dev) clusterscan: async func(connection: borrow, cursor: cursor, match: option, count: option, %type: option, slot: option) -> result, list>, error>; - /// Copies the value of the source key to the destination key, in the database, or the current - /// one, returning whether it was copied, false when the destination exists without `replace`. + /// Copies the value of a key to a new key. /// /// COPY source destination [ DB destination-db ] [ REPLACE ] /// https://valkey.io/commands/copy/ @since(version = 0.3.0-dev) copy: async func(connection: borrow, source: string, destination: string, destination-db: option, replace: bool) -> result; - /// The number of keys in the current database. + /// Returns the number of keys in the database. /// /// DBSIZE /// https://valkey.io/commands/dbsize/ @since(version = 0.3.0-dev) dbsize: async func(connection: borrow) -> result; - /// Removes the specified keys. + /// Deletes one or more keys. /// /// DEL key [ key ... ] /// https://valkey.io/commands/del/ @since(version = 0.3.0-dev) del: async func(connection: borrow, keys: list) -> result; - /// The value of the key serialized in Valkey's own format, with its version and a checksum, see - /// `restore`, none when the key does not exist. + /// Returns a serialized representation of the value stored at a key. /// /// DUMP key /// https://valkey.io/commands/dump/ @since(version = 0.3.0-dev) dump: async func(connection: borrow, key: string) -> result, error>; - /// If key exists. + /// Determines whether one or more keys exist. /// /// EXISTS key [ key ... ] /// https://valkey.io/commands/exists/ @since(version = 0.3.0-dev) exists: async func(connection: borrow, keys: list) -> result; - /// Set a timeout on key. + /// Sets the expiration time of a key in seconds. /// /// EXPIRE key seconds [ NX | XX | GT | LT ] /// https://valkey.io/commands/expire/ @since(version = 0.3.0-dev) expire: async func(connection: borrow, key: string, seconds: duration-seconds, mode: option) -> result; - /// Set a timeout on key. + /// Sets the expiration time of a key to a Unix timestamp. /// /// EXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] /// https://valkey.io/commands/expireat/ @since(version = 0.3.0-dev) expireat: async func(connection: borrow, key: string, seconds: unix-time-seconds, mode: option) -> result; - /// When the key expires, to the second. - /// - /// See also `pexpiretime`. + /// Returns the expiration time of a key as a Unix timestamp. /// /// EXPIRETIME key /// https://valkey.io/commands/expiretime/ @since(version = 0.3.0-dev) expiretime: async func(connection: borrow, key: string) -> result; - /// Removes all the keys of all the databases, in the mode, the server's - /// `lazyfree-lazy-user-flush` without one. + /// Removes all keys from all databases. /// /// FLUSHALL [ ASYNC | SYNC ] /// https://valkey.io/commands/flushall/ @since(version = 0.3.0-dev) flushall: async func(connection: borrow, mode: option) -> result<_, error>; - /// Removes all the keys of the current database, in the mode, the server's - /// `lazyfree-lazy-user-flush` without one. + /// Removes all keys from the current database. /// /// FLUSHDB [ ASYNC | SYNC ] /// https://valkey.io/commands/flushdb/ @since(version = 0.3.0-dev) flushdb: async func(connection: borrow, mode: option) -> result<_, error>; - /// Returns all keys matching pattern. + /// Returns all key names that match a pattern. /// /// KEYS pattern /// https://valkey.io/commands/keys/ @since(version = 0.3.0-dev) keys: async func(connection: borrow, pattern: string) -> result, error>; - /// Moves the keys to the database of another instance, returning whether any were moved, false - /// when none of the keys exist. + /// Atomically transfers a key from one instance to another. + /// + /// False when none of the keys exist, the server's `NOKEY`. /// /// MIGRATE host port "" destination-db timeout [ COPY ] [ REPLACE ] [ AUTH password | AUTH2 username password ] KEYS key [ key ... ] /// https://valkey.io/commands/migrate/ @since(version = 0.3.0-dev) migrate: async func(connection: borrow, host: string, port: u16, destination-db: integer, timeout: duration-milliseconds, options: migrate-flags, auth: option, keys: list) -> result; - /// Moves the key to the database, returning whether it was moved, false when it does not - /// exist, or exists in the database without `replace`. + /// Moves a key to another database. /// /// MOVE key db [ REPLACE ] /// https://valkey.io/commands/move/ @since(version = 0.3.0-dev) move: async func(connection: borrow, key: string, db: integer, replace: bool) -> result; - /// The internal encoding of the value stored at key, e.g. `listpack`, none when the key does - /// not exist. + /// Returns the internal encoding of an object. /// /// OBJECT ENCODING key /// https://valkey.io/commands/object-encoding/ @since(version = 0.3.0-dev) object-encoding: async func(connection: borrow, key: string) -> result, error>; - /// The logarithmic access frequency counter of the key, none when it does not exist. - /// - /// Only with an LFU `maxmemory-policy`, otherwise it fails. + /// Returns the logarithmic access frequency counter of an object. /// /// OBJECT FREQ key /// https://valkey.io/commands/object-freq/ @since(version = 0.3.0-dev) object-freq: async func(connection: borrow, key: string) -> result, error>; - /// The time since the key was last accessed, none when it does not exist. + /// Returns the time since the last access to an object. /// /// OBJECT IDLETIME key /// https://valkey.io/commands/object-idletime/ @since(version = 0.3.0-dev) object-idletime: async func(connection: borrow, key: string) -> result, error>; - /// The number of references to the value stored at key, none when the key does not exist. + /// Returns the reference count of a value of a key. /// /// OBJECT REFCOUNT key /// https://valkey.io/commands/object-refcount/ @since(version = 0.3.0-dev) object-refcount: async func(connection: borrow, key: string) -> result, error>; - /// Removes the expiration of the key, returning whether it had one. + /// Removes the expiration time of a key. /// /// PERSIST key /// https://valkey.io/commands/persist/ @since(version = 0.3.0-dev) persist: async func(connection: borrow, key: string) -> result; - /// Set a timeout on key, in milliseconds. + /// Sets the expiration time of a key in milliseconds. /// /// PEXPIRE key milliseconds [ NX | XX | GT | LT ] /// https://valkey.io/commands/pexpire/ @since(version = 0.3.0-dev) pexpire: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, mode: option) -> result; - /// Set a timeout on key, as a Unix time in milliseconds. + /// Sets the expiration time of a key to a Unix milliseconds timestamp. /// /// PEXPIREAT key unix-time-milliseconds [ NX | XX | GT | LT ] /// https://valkey.io/commands/pexpireat/ @since(version = 0.3.0-dev) pexpireat: async func(connection: borrow, key: string, milliseconds: unix-time-milliseconds, mode: option) -> result; - /// When the key expires, to the millisecond. - /// - /// See also `expiretime`. + /// Returns the expiration time of a key as a Unix milliseconds timestamp. /// /// PEXPIRETIME key /// https://valkey.io/commands/pexpiretime/ @since(version = 0.3.0-dev) pexpiretime: async func(connection: borrow, key: string) -> result; - /// The remaining time to live of the key, to the millisecond. - /// - /// See also `ttl`. + /// Returns the expiration time in milliseconds of a key. /// /// PTTL key /// https://valkey.io/commands/pttl/ @since(version = 0.3.0-dev) pttl: async func(connection: borrow, key: string) -> result; - /// A random key of the current database, none when it is empty. + /// Returns a random key name from the database. /// /// RANDOMKEY /// https://valkey.io/commands/randomkey/ @since(version = 0.3.0-dev) randomkey: async func(connection: borrow) -> result, error>; - /// Renames the key, replacing the new key when it exists. + /// Renames a key and overwrites the destination. /// /// RENAME key newkey /// https://valkey.io/commands/rename/ @since(version = 0.3.0-dev) rename: async func(connection: borrow, key: string, newkey: string) -> result<_, error>; - /// Renames the key, only when the new key does not exist, returning whether it was renamed. + /// Renames a key only when the target key name doesn't exist. /// /// RENAMENX key newkey /// https://valkey.io/commands/renamenx/ @since(version = 0.3.0-dev) renamenx: async func(connection: borrow, key: string, newkey: string) -> result; - /// Creates the key from a value serialized with `dump`, without an expiration when `ttl` is - /// none. Fails when the key exists, unless `replace`. `idletime` and `freq` set what eviction - /// knows of the key, its idle time for an LRU policy, or its access frequency for an LFU policy. + /// Creates a key from the serialized representation of a value. + /// + /// A `ttl` of none restores the key without an expiration, `0`, `expire-at` adds `ABSTTL`. /// /// RESTORE key ttl serialized-value [ REPLACE ] [ ABSTTL ] [ IDLETIME seconds ] [ FREQ frequency ] /// https://valkey.io/commands/restore/ @since(version = 0.3.0-dev) restore: async func(connection: borrow, key: string, ttl: option, serialized-value: bytes, replace: bool, idletime: option, freq: option) -> result<_, error>; - /// `restore` on a node a key is migrated to, for a slot being imported, see `cluster-setslot`. + /// An internal command for migrating keys in a cluster. + /// + /// A `ttl` of none restores the key without an expiration, `0`, `expire-at` adds `ABSTTL`. /// /// RESTORE-ASKING key ttl serialized-value [ REPLACE ] [ ABSTTL ] [ IDLETIME seconds ] [ FREQ frequency ] /// https://valkey.io/commands/restore-asking/ @since(version = 0.3.0-dev) restore-asking: async func(connection: borrow, key: string, ttl: option, serialized-value: bytes, replace: bool, idletime: option, freq: option) -> result<_, error>; - /// Incrementally iterate over the keys of the current database. + /// Iterates over the key names in the database. /// /// SCAN cursor [ MATCH pattern ] [ COUNT count ] [ TYPE type ] /// https://valkey.io/commands/scan/ @since(version = 0.3.0-dev) scan: async func(connection: borrow, cursor: cursor, match: option, count: option, %type: option) -> result, list>, error>; - /// Swaps the two databases, for connections using either. + /// Swaps two databases. /// /// SWAPDB index1 index2 /// https://valkey.io/commands/swapdb/ @since(version = 0.3.0-dev) swapdb: async func(connection: borrow, index1: integer, index2: integer) -> result<_, error>; - /// Updates the last access time of the keys, returning how many exist. + /// Returns the number of existing keys out of those specified after updating the time they were last accessed. /// /// TOUCH key [ key ... ] /// https://valkey.io/commands/touch/ @since(version = 0.3.0-dev) touch: async func(connection: borrow, keys: list) -> result; - /// The remaining time to live of the key, to the second. - /// - /// See also `pttl`. + /// Returns the expiration time in seconds of a key. /// /// TTL key /// https://valkey.io/commands/ttl/ @since(version = 0.3.0-dev) ttl: async func(connection: borrow, key: string) -> result; - /// The type of the value stored at key, none when the key does not exist. + /// Determines the type of value stored at a key. + /// + /// None when the key does not exist, the server's `none`. /// /// TYPE key /// https://valkey.io/commands/type/ @since(version = 0.3.0-dev) %type: async func(connection: borrow, key: string) -> result, error>; - /// Removes the keys, freeing their memory in the background, returning how many existed. + /// Asynchronously deletes one or more keys. /// /// UNLINK key [ key ... ] /// https://valkey.io/commands/unlink/ @since(version = 0.3.0-dev) unlink: async func(connection: borrow, keys: list) -> result; - /// Moves an element from one end of the source list to an end of the destination list, waiting - /// up to the timeout for the source to have one, returning it. - /// - /// Returns none when the timeout passed. A timeout of zero waits indefinitely. + /// Pops an element from a list, pushes it to another list and returns it. /// - /// The connection waits too, replies to commands sent after it on the connection wait behind - /// it. Use a connection of your own. + /// Returns none when the timeout passed. The connection waits too, replies to commands sent + /// after it on the connection wait behind it. Use a connection of your own. /// /// BLMOVE source destination 〈 LEFT | RIGHT 〉 〈 LEFT | RIGHT 〉 timeout /// https://valkey.io/commands/blmove/ @since(version = 0.3.0-dev) blmove: async func(connection: borrow, source: string, destination: string, wherefrom: list-side, whereto: list-side, timeout: duration-milliseconds) -> result, error>; - /// Removes elements from an end of the first non-empty list, waiting up to the timeout for one, - /// returning its key and the elements, one without a count. + /// Pops the first element from one of multiple lists. /// - /// Returns none when the timeout passed. A timeout of zero waits indefinitely. - /// - /// The connection waits too, replies to commands sent after it on the connection wait behind - /// it. Use a connection of your own. + /// Returns none when the timeout passed. The connection waits too, replies to commands sent + /// after it on the connection wait behind it. Use a connection of your own. /// /// BLMPOP timeout numkeys key [ key ... ] 〈 LEFT | RIGHT 〉 [ COUNT count ] /// https://valkey.io/commands/blmpop/ @since(version = 0.3.0-dev) blmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: list-side, count: option) -> result>>, error>; - /// Removes the first element of the first non-empty list, waiting up to the timeout for one, - /// returning its key and the element. - /// - /// Returns none when the timeout passed. A timeout of zero waits indefinitely. + /// Removes and returns the first element in a list. /// - /// The connection waits too, replies to commands sent after it on the connection wait behind - /// it. Use a connection of your own. + /// Returns none when the timeout passed. The connection waits too, replies to commands sent + /// after it on the connection wait behind it. Use a connection of your own. /// /// BLPOP key [ key ... ] timeout /// https://valkey.io/commands/blpop/ @since(version = 0.3.0-dev) blpop: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; - /// Removes the last element of the first non-empty list, waiting up to the timeout for one, - /// returning its key and the element. - /// - /// Returns none when the timeout passed. A timeout of zero waits indefinitely. + /// Removes and returns the last element in a list. /// - /// The connection waits too, replies to commands sent after it on the connection wait behind - /// it. Use a connection of your own. + /// Returns none when the timeout passed. The connection waits too, replies to commands sent + /// after it on the connection wait behind it. Use a connection of your own. /// /// BRPOP key [ key ... ] timeout /// https://valkey.io/commands/brpop/ @since(version = 0.3.0-dev) brpop: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; - /// Moves the last element of the source list to the head of the destination list, waiting up - /// to the timeout for the source to have one, returning it. - /// - /// Returns none when the timeout passed. A timeout of zero waits indefinitely. - /// - /// The connection waits too, replies to commands sent after it on the connection wait behind - /// it. Use a connection of your own. + /// Pops an element from a list, pushes it to another list and returns it. /// - /// See also `blmove`. + /// Returns none when the timeout passed. The connection waits too, replies to commands sent + /// after it on the connection wait behind it. Use a connection of your own. /// /// BRPOPLPUSH source destination timeout /// https://valkey.io/commands/brpoplpush/ @since(version = 0.3.0-dev) brpoplpush: async func(connection: borrow, source: string, destination: string, timeout: duration-milliseconds) -> result, error>; - /// The element at the index of the list stored at key, negative from the end, none when the - /// index is out of range. + /// Returns an element from a list by its index. /// /// LINDEX key index /// https://valkey.io/commands/lindex/ @since(version = 0.3.0-dev) lindex: async func(connection: borrow, key: string, index: integer) -> result, error>; - /// Inserts the element before or after the first occurrence of the pivot in the list stored at - /// key. + /// Inserts an element before or after another element in a list. /// /// LINSERT key 〈 BEFORE | AFTER 〉 pivot element /// https://valkey.io/commands/linsert/ @since(version = 0.3.0-dev) linsert: async func(connection: borrow, key: string, position: linsert-position, pivot: string-or-bytes, element: string-or-bytes) -> result; - /// The length of the list stored at key, 0 when the key does not exist. + /// Returns the length of a list. /// /// LLEN key /// https://valkey.io/commands/llen/ @since(version = 0.3.0-dev) llen: async func(connection: borrow, key: string) -> result; - /// Moves an element from one end of the source list to an end of the destination list, - /// returning it, none when the source does not exist. + /// Returns an element after popping it from one list and pushing it to another. /// /// LMOVE source destination 〈 LEFT | RIGHT 〉 〈 LEFT | RIGHT 〉 /// https://valkey.io/commands/lmove/ @since(version = 0.3.0-dev) lmove: async func(connection: borrow, source: string, destination: string, wherefrom: list-side, whereto: list-side) -> result, error>; - /// Removes elements from an end of the first non-empty list, returning its key and the - /// elements, one without a count, none when every list is empty. + /// Returns multiple elements from a list after removing them. /// /// LMPOP numkeys key [ key ... ] 〈 LEFT | RIGHT 〉 [ COUNT count ] /// https://valkey.io/commands/lmpop/ @since(version = 0.3.0-dev) lmpop: async func(connection: borrow, keys: list, end: list-side, count: option) -> result>>, error>; - /// Removes and returns the first elements of the list stored at key, up to the count, one - /// without a count, none when the key does not exist. + /// Returns and removes one or more elements from the beginning of a list. + /// + /// Without a count, at most one element. /// /// LPOP key [ count ] /// https://valkey.io/commands/lpop/ @since(version = 0.3.0-dev) lpop: async func(connection: borrow, key: string, count: option) -> result, error>; - /// The positions of the element in the list stored at key, up to the count, 0 for every match, - /// the first match without a count. + /// Returns the index of matching elements in a list. /// - /// The rank skips matches, the second match for 2, counting from the end when negative. The - /// max length limits how many elements are compared. + /// Without a count, at most one position. /// /// LPOS key element [ RANK rank ] [ COUNT num-matches ] [ MAXLEN len ] /// https://valkey.io/commands/lpos/ @since(version = 0.3.0-dev) lpos: async func(connection: borrow, key: string, element: string-or-bytes, rank: option, count: option, maxlen: option) -> result, error>; - /// Inserts the elements at the head of the list stored at key, creating it when it does not - /// exist, returning its length after. + /// Prepends one or more elements to a list. /// /// LPUSH key element [ element ... ] /// https://valkey.io/commands/lpush/ @since(version = 0.3.0-dev) lpush: async func(connection: borrow, key: string, elements: list) -> result; - /// Inserts the elements at the head of the list stored at key, only when it exists, returning - /// its length after, 0 when it does not exist. + /// Prepends one or more elements to a list only when the list exists. /// /// LPUSHX key element [ element ... ] /// https://valkey.io/commands/lpushx/ @since(version = 0.3.0-dev) lpushx: async func(connection: borrow, key: string, elements: list) -> result; - /// The elements of the list stored at key from the start to the stop, both included, negative - /// from the end. + /// Returns a range of elements from a list. /// /// LRANGE key start stop /// https://valkey.io/commands/lrange/ @since(version = 0.3.0-dev) lrange: async func(connection: borrow, key: string, start: integer, stop: integer) -> result, error>; - /// Removes occurrences of the element from the list stored at key, returning how many. - /// - /// A positive count removes up to that many from the head, a negative count from the tail, and - /// 0 removes them all. + /// Removes elements from a list. /// /// LREM key count element /// https://valkey.io/commands/lrem/ @since(version = 0.3.0-dev) lrem: async func(connection: borrow, key: string, count: integer, element: string-or-bytes) -> result; - /// Sets the element at the index of the list stored at key, negative from the end. + /// Sets the value of an element in a list by its index. /// /// LSET key index element /// https://valkey.io/commands/lset/ @since(version = 0.3.0-dev) lset: async func(connection: borrow, key: string, index: integer, element: string-or-bytes) -> result<_, error>; - /// Trims the list stored at key to the elements from the start to the stop, both included, - /// negative from the end. + /// Removes elements from both ends of a list. /// /// LTRIM key start stop /// https://valkey.io/commands/ltrim/ @since(version = 0.3.0-dev) ltrim: async func(connection: borrow, key: string, start: integer, stop: integer) -> result<_, error>; - /// Removes and returns the last elements of the list stored at key, up to the count, one - /// without a count, none when the key does not exist. + /// Returns and removes one or more elements from the end of a list. + /// + /// Without a count, at most one element. /// /// RPOP key [ count ] /// https://valkey.io/commands/rpop/ @since(version = 0.3.0-dev) rpop: async func(connection: borrow, key: string, count: option) -> result, error>; - /// Moves the last element of the source list to the head of the destination list, returning - /// it, none when the source does not exist. - /// - /// See also `lmove`. + /// Returns the last element of a list after removing and pushing it to another list. /// /// RPOPLPUSH source destination /// https://valkey.io/commands/rpoplpush/ @since(version = 0.3.0-dev) rpoplpush: async func(connection: borrow, source: string, destination: string) -> result, error>; - /// Inserts the elements at the tail of the list stored at key, creating it when it does not - /// exist, returning its length after. + /// Appends one or more elements to a list. /// /// RPUSH key element [ element ... ] /// https://valkey.io/commands/rpush/ @since(version = 0.3.0-dev) rpush: async func(connection: borrow, key: string, elements: list) -> result; - /// Inserts the elements at the tail of the list stored at key, only when it exists, returning - /// its length after, 0 when it does not exist. + /// Appends one or more elements to a list only when the list exists. /// /// RPUSHX key element [ element ... ] /// https://valkey.io/commands/rpushx/ @since(version = 0.3.0-dev) rpushx: async func(connection: borrow, key: string, elements: list) -> result; - /// Posts a message to the given channel. + /// Posts a message to a channel. /// /// PUBLISH channel message /// https://valkey.io/commands/publish/ @since(version = 0.3.0-dev) publish: async func(connection: borrow, channel: string, message: string-or-bytes) -> result; - /// Lists the currently active channels. + /// Returns the active channels. /// /// PUBSUB CHANNELS [ pattern ] /// https://valkey.io/commands/pubsub-channels/ @since(version = 0.3.0-dev) pubsub-channels: async func(connection: borrow, pattern: option) -> result, error>; - /// Returns the number of unique patterns that are subscribed to by clients. + /// Returns a count of unique pattern subscriptions. /// /// PUBSUB NUMPAT /// https://valkey.io/commands/pubsub-numpat/ @since(version = 0.3.0-dev) pubsub-numpat: async func(connection: borrow) -> result; - /// Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels. + /// Returns a count of subscribers to channels. /// /// PUBSUB NUMSUB [ channel ] [ [ channel ] ... ] /// https://valkey.io/commands/pubsub-numsub/ @since(version = 0.3.0-dev) pubsub-numsub: async func(connection: borrow, channels: option>) -> result, error>; - /// Lists the currently active shard channels. + /// Returns the active shard channels. /// /// PUBSUB SHARDCHANNELS [ pattern ] /// https://valkey.io/commands/pubsub-shardchannels/ @since(version = 0.3.0-dev) pubsub-shardchannels: async func(connection: borrow, pattern: option) -> result, error>; - /// Returns the number of subscribers for the specified shard channels. + /// Returns the count of subscribers of shard channels. /// /// PUBSUB SHARDNUMSUB [ shardchannel [ shardchannel ... ] ] /// https://valkey.io/commands/pubsub-shardnumsub/ @since(version = 0.3.0-dev) pubsub-shardnumsub: async func(connection: borrow, shardchannels: option>) -> result, error>; - /// Posts a message to the given shard channel. + /// Posts a message to a shard channel. /// /// SPUBLISH shardchannel message /// https://valkey.io/commands/spublish/ @since(version = 0.3.0-dev) spublish: async func(connection: borrow, shardchannel: string, message: string-or-bytes) -> result; - /// Runs the Lua script, returning its result, the keys it accesses and its arguments are - /// `KEYS` and `ARGV` in the script. + /// Executes a server-side Lua script. /// /// An error the script returns fails the call. /// @@ -1665,31 +1630,34 @@ interface commands { @since(version = 0.3.0-dev) eval: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; - /// Runs the Lua script, which may not write, returning its result, see `eval`. + /// Executes a read-only server-side Lua script. + /// + /// An error the script returns fails the call. /// /// EVAL_RO script numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/eval_ro/ @since(version = 0.3.0-dev) eval-ro: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; - /// Runs the Lua script cached with the SHA1 digest, see `script-load`, returning its result, see - /// `eval`. + /// Executes a server-side Lua script by SHA1 digest. + /// + /// An error the script returns fails the call. /// /// EVALSHA sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/evalsha/ @since(version = 0.3.0-dev) evalsha: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; - /// Runs the Lua script cached with the SHA1 digest, which may not write, returning its result, - /// see `eval`. + /// Executes a read-only server-side Lua script by SHA1 digest. + /// + /// An error the script returns fails the call. /// /// EVALSHA_RO sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/evalsha_ro/ @since(version = 0.3.0-dev) evalsha-ro: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; - /// Calls the function, see `function-load`, returning its result, the keys it accesses and its - /// arguments are passed to it. + /// Invokes a function. /// /// An error the function returns fails the call. /// @@ -1698,156 +1666,158 @@ interface commands { @since(version = 0.3.0-dev) fcall: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; - /// Calls the function, which must be flagged `no-writes`, returning its result, see `fcall`. + /// Invokes a read-only function. + /// + /// An error the function returns fails the call. /// /// FCALL_RO function numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/fcall_ro/ @since(version = 0.3.0-dev) fcall-ro: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; - /// Deletes the library and its functions. + /// Deletes a library and its functions. /// /// FUNCTION DELETE library-name /// https://valkey.io/commands/function-delete/ @since(version = 0.3.0-dev) function-delete: async func(connection: borrow, library-name: string) -> result<_, error>; - /// The libraries, serialized in Valkey's own format, see `function-restore`. + /// Dumps all libraries into a serialized binary payload. /// /// FUNCTION DUMP /// https://valkey.io/commands/function-dump/ @since(version = 0.3.0-dev) function-dump: async func(connection: borrow) -> result; - /// Deletes all the libraries, in the mode, the server's `lazyfree-lazy-user-flush` without one. + /// Deletes all libraries and functions. /// /// FUNCTION FLUSH [ ASYNC | SYNC ] /// https://valkey.io/commands/function-flush/ @since(version = 0.3.0-dev) function-flush: async func(connection: borrow, mode: option) -> result<_, error>; - /// Kills the function that is running, as long as it has not written. + /// Terminates a function during execution. /// /// FUNCTION KILL /// https://valkey.io/commands/function-kill/ @since(version = 0.3.0-dev) function-kill: async func(connection: borrow) -> result<_, error>; - /// The libraries, those with names matching the glob-style pattern with one. + /// Returns information about all libraries. /// /// FUNCTION LIST [ LIBRARYNAME library-name-pattern ] [ WITHCODE ] /// https://valkey.io/commands/function-list/ @since(version = 0.3.0-dev) function-list: async func(connection: borrow, library-name-pattern: option, withcode: bool) -> result, error>; - /// Loads the library from its source code, replacing a library of the same name with - /// `replace`, returning its name. + /// Creates a library. /// /// FUNCTION LOAD [ REPLACE ] function-code /// https://valkey.io/commands/function-load/ @since(version = 0.3.0-dev) function-load: async func(connection: borrow, replace: bool, function-code: string) -> result; - /// Loads the libraries serialized with `function-dump`, as `policy` says, by default `append`. + /// Restores all libraries from a payload. /// /// FUNCTION RESTORE serialized-value [ FLUSH | APPEND | REPLACE ] /// https://valkey.io/commands/function-restore/ @since(version = 0.3.0-dev) function-restore: async func(connection: borrow, serialized-value: bytes, policy: option) -> result<_, error>; - /// The function that is running, and the libraries and functions of each engine. + /// Returns information about a function during execution. /// /// FUNCTION STATS /// https://valkey.io/commands/function-stats/ @since(version = 0.3.0-dev) function-stats: async func(connection: borrow) -> result; - /// Whether each of the scripts is cached, by SHA1 digest. + /// Determines whether server-side Lua scripts exist in the script cache. /// /// SCRIPT EXISTS sha1 [ sha1 ... ] /// https://valkey.io/commands/script-exists/ @since(version = 0.3.0-dev) script-exists: async func(connection: borrow, sha1s: list) -> result, error>; - /// Removes all the scripts from the cache, in the mode, the server's `lazyfree-lazy-user-flush` - /// without one. + /// Removes all server-side Lua scripts from the script cache. /// /// SCRIPT FLUSH [ ASYNC | SYNC ] /// https://valkey.io/commands/script-flush/ @since(version = 0.3.0-dev) script-flush: async func(connection: borrow, mode: option) -> result<_, error>; - /// Kills the script that is running, as long as it has not written. + /// Terminates a server-side Lua script during execution. /// /// SCRIPT KILL /// https://valkey.io/commands/script-kill/ @since(version = 0.3.0-dev) script-kill: async func(connection: borrow) -> result<_, error>; - /// Caches the script without running it, returning its SHA1 digest, see `evalsha`. + /// Loads a server-side Lua script to the script cache. /// /// SCRIPT LOAD script /// https://valkey.io/commands/script-load/ @since(version = 0.3.0-dev) script-load: async func(connection: borrow, script: string) -> result; - /// The source of the cached script with the SHA1 digest. + /// Show server-side Lua script in the script cache. /// /// SCRIPT SHOW sha1 /// https://valkey.io/commands/script-show/ @since(version = 0.3.0-dev) script-show: async func(connection: borrow, sha1: string) -> result; - /// Adds the members to the set stored at key, returning how many were not already members. + /// Adds one or more members to a set. /// /// SADD key member [ member ... ] /// https://valkey.io/commands/sadd/ @since(version = 0.3.0-dev) sadd: async func(connection: borrow, key: string, members: list) -> result; - /// The number of members in the set stored at key. + /// Returns the number of members in a set. /// /// SCARD key /// https://valkey.io/commands/scard/ @since(version = 0.3.0-dev) scard: async func(connection: borrow, key: string) -> result; - /// The members of the first set that are not in any of the others. + /// Returns the difference of multiple sets. /// /// SDIFF key [ key ... ] /// https://valkey.io/commands/sdiff/ @since(version = 0.3.0-dev) sdiff: async func(connection: borrow, keys: list) -> result, error>; - /// Stores the members of the first set that are not in any of the others, returning how many. + /// Stores the difference of multiple sets in a key. /// /// SDIFFSTORE destination key [ key ... ] /// https://valkey.io/commands/sdiffstore/ @since(version = 0.3.0-dev) sdiffstore: async func(connection: borrow, destination: string, keys: list) -> result; - /// The members in all of the sets. + /// Returns the intersect of multiple sets. /// /// SINTER key [ key ... ] /// https://valkey.io/commands/sinter/ @since(version = 0.3.0-dev) sinter: async func(connection: borrow, keys: list) -> result, error>; - /// The number of members in all of the sets, counting stops at the limit. + /// Returns the number of members of the intersect of multiple sets. /// /// SINTERCARD numkeys key [ key ... ] [ LIMIT limit ] /// https://valkey.io/commands/sintercard/ @since(version = 0.3.0-dev) sintercard: async func(connection: borrow, keys: list, limit: option) -> result; - /// Stores the members in all of the sets, returning how many. + /// Stores the intersect of multiple sets in a key. /// /// SINTERSTORE destination key [ key ... ] /// https://valkey.io/commands/sinterstore/ @since(version = 0.3.0-dev) sinterstore: async func(connection: borrow, destination: string, keys: list) -> result; - /// Whether the member is in the set stored at key, false when the key does not exist. + /// Determines whether a member belongs to a set. + /// + /// False when the key does not exist. /// /// See also `sismember-xx`. /// @@ -1856,7 +1826,9 @@ interface commands { @since(version = 0.3.0-dev) sismember: async func(connection: borrow, key: string, member: string-or-bytes) -> result; - /// Whether the member is in the set stored at key, none when the key does not exist. + /// Determines whether a member belongs to a set. + /// + /// The `XX` form, none when the key does not exist, rather than false. /// /// See also `sismember`. /// @@ -1865,140 +1837,137 @@ interface commands { @since(version = 0.3.0-dev) sismember-xx: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; - /// The members of the set stored at key. + /// Returns all members of a set. /// /// SMEMBERS key /// https://valkey.io/commands/smembers/ @since(version = 0.3.0-dev) smembers: async func(connection: borrow, key: string) -> result, error>; - /// Whether each of the members is in the set stored at key. + /// Determines whether multiple members belong to a set. /// /// SMISMEMBER key member [ member ... ] /// https://valkey.io/commands/smismember/ @since(version = 0.3.0-dev) smismember: async func(connection: borrow, key: string, members: list) -> result, error>; - /// Moves the member from one set to another, false when it was not in the source set. + /// Moves a member from one set to another. /// /// SMOVE source destination member /// https://valkey.io/commands/smove/ @since(version = 0.3.0-dev) smove: async func(connection: borrow, source: string, destination: string, member: string-or-bytes) -> result; - /// Removes and returns random members of the set stored at key, up to the count. + /// Returns one or more random members from a set after removing them. /// - /// Without a count, one member, or none when the key does not exist. + /// Without a count, at most one member. /// /// SPOP key [ count ] /// https://valkey.io/commands/spop/ @since(version = 0.3.0-dev) spop: async func(connection: borrow, key: string, count: option) -> result, error>; - /// Random members of the set stored at key, up to the count. + /// Gets one or multiple random members from a set. /// - /// Without a count, one member, or none when the key does not exist. A negative count may return - /// the same member more than once, its absolute value is the number of members returned. + /// Without a count, at most one member. /// /// SRANDMEMBER key [ count ] /// https://valkey.io/commands/srandmember/ @since(version = 0.3.0-dev) srandmember: async func(connection: borrow, key: string, count: option) -> result, error>; - /// Removes the members from the set stored at key, returning how many were members. + /// Removes one or more members from a set. /// /// SREM key member [ member ... ] /// https://valkey.io/commands/srem/ @since(version = 0.3.0-dev) srem: async func(connection: borrow, key: string, members: list) -> result; - /// Incrementally iterate over the members of the set stored at key. + /// Iterates over members of a set. /// /// SSCAN key cursor [ MATCH pattern ] [ COUNT count ] /// https://valkey.io/commands/sscan/ @since(version = 0.3.0-dev) sscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; - /// The members in any of the sets. + /// Returns the union of multiple sets. /// /// SUNION key [ key ... ] /// https://valkey.io/commands/sunion/ @since(version = 0.3.0-dev) sunion: async func(connection: borrow, keys: list) -> result, error>; - /// Stores the members in any of the sets, returning how many. + /// Stores the union of multiple sets in a key. /// /// SUNIONSTORE destination key [ key ... ] /// https://valkey.io/commands/sunionstore/ @since(version = 0.3.0-dev) sunionstore: async func(connection: borrow, destination: string, keys: list) -> result; - /// Sorts the elements of the list, set or sorted set stored at key. + /// Sorts the elements in a list, a set, or a sorted set, optionally storing the result. /// - /// Each row is the element, or with `get`, the value of each pattern, none when its key does not - /// exist. + /// Each row is the element, or with `get`, the value of each pattern, none when its key does + /// not exist. + /// + /// See also `sort-store`. /// /// SORT key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] /// https://valkey.io/commands/sort/ @since(version = 0.3.0-dev) sort: async func(connection: borrow, key: string, opts: option) -> result>>, error>; - /// Sorts the elements of the list, set or sorted set stored at key, storing them as a list, - /// returning how many. + /// Sorts the elements in a list, a set, or a sorted set, optionally storing the result. + /// + /// The `STORE` form, storing the elements as a list, returning how many. + /// + /// See also `sort`. /// /// SORT key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] STORE destination /// https://valkey.io/commands/sort/ @since(version = 0.3.0-dev) sort-store: async func(connection: borrow, key: string, opts: option, destination: string) -> result; - /// The read-only variant of `sort`, e.g. for replicas. + /// Returns the sorted elements of a list, a set, or a sorted set. + /// + /// Each row is the element, or with `get`, the value of each pattern, none when its key does + /// not exist. /// /// SORT_RO key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] /// https://valkey.io/commands/sort_ro/ @since(version = 0.3.0-dev) sort-ro: async func(connection: borrow, key: string, opts: option) -> result>>, error>; - /// Removes the members with the lowest or highest scores from the first non-empty sorted set, - /// waiting up to the timeout for one, returning its key and the members and their scores. + /// Removes and returns a member by score from one or more sorted sets. /// - /// Returns none when the timeout passed. A timeout of zero waits indefinitely. - /// - /// The connection waits too, replies to commands sent after it on the connection wait behind - /// it. Use a connection of your own. + /// Returns none when the timeout passed. The connection waits too, replies to commands sent + /// after it on the connection wait behind it. Use a connection of your own. /// /// BZMPOP timeout numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ] /// https://valkey.io/commands/bzmpop/ @since(version = 0.3.0-dev) bzmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: bzmpop-end, count: option) -> result>>>, error>; - /// Removes the member with the highest score from the first non-empty sorted set, waiting up to - /// the timeout for one, returning its key, the member and its score. + /// Removes and returns the member with the highest score from one or more sorted sets. /// - /// Returns none when the timeout passed. A timeout of zero waits indefinitely. - /// - /// The connection waits too, replies to commands sent after it on the connection wait behind - /// it. Use a connection of your own. + /// Returns none when the timeout passed. The connection waits too, replies to commands sent + /// after it on the connection wait behind it. Use a connection of your own. /// /// BZPOPMAX key [ key ... ] timeout /// https://valkey.io/commands/bzpopmax/ @since(version = 0.3.0-dev) bzpopmax: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; - /// Removes the member with the lowest score from the first non-empty sorted set, waiting up to - /// the timeout for one, returning its key, the member and its score. + /// Removes and returns the member with the lowest score from one or more sorted sets. /// - /// Returns none when the timeout passed. A timeout of zero waits indefinitely. - /// - /// The connection waits too, replies to commands sent after it on the connection wait behind - /// it. Use a connection of your own. + /// Returns none when the timeout passed. The connection waits too, replies to commands sent + /// after it on the connection wait behind it. Use a connection of your own. /// /// BZPOPMIN key [ key ... ] timeout /// https://valkey.io/commands/bzpopmin/ @since(version = 0.3.0-dev) bzpopmin: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; - /// Adds the members with their scores to the sorted set stored at key, or updates their scores, - /// returning how many were added, or with `changed`, added or updated. + /// Adds one or more members to a sorted set, or updates their scores. /// /// See also `zadd-incr`. /// @@ -2007,31 +1976,33 @@ interface commands { @since(version = 0.3.0-dev) zadd: async func(connection: borrow, key: string, condition: option, comparison: option, changed: bool, score-and-members: list>) -> result, error>; - /// Increments the score of the member in the sorted set stored at key, returning the new score, - /// none when the condition or comparison prevented it. + /// Adds one or more members to a sorted set, or updates their scores. + /// + /// The `INCR` form, returning the new score, none when the condition or comparison prevented + /// it. /// - /// See also `zadd` and `zincrby`. + /// See also `zadd`. /// /// ZADD key [ NX | XX ] [ GT | LT ] INCR score member /// https://valkey.io/commands/zadd/ @since(version = 0.3.0-dev) zadd-incr: async func(connection: borrow, key: string, condition: option, comparison: option, increment: double, member: string-or-bytes) -> result, error>; - /// The number of members in the sorted set stored at key. + /// Returns the number of members in a sorted set. /// /// ZCARD key /// https://valkey.io/commands/zcard/ @since(version = 0.3.0-dev) zcard: async func(connection: borrow, key: string) -> result; - /// The number of members of the sorted set stored at key with scores in the range. + /// Returns the count of members in a sorted set that have scores within a range. /// /// ZCOUNT key min max /// https://valkey.io/commands/zcount/ @since(version = 0.3.0-dev) zcount: async func(connection: borrow, key: string, min: score-bound, max: score-bound) -> result; - /// The members of the first sorted set that are not in any of the others. + /// Returns the difference between multiple sorted sets. /// /// See also `zdiff-withscores`. /// @@ -2040,7 +2011,9 @@ interface commands { @since(version = 0.3.0-dev) zdiff: async func(connection: borrow, keys: list) -> result, error>; - /// The members, and their scores, of the first sorted set that are not in any of the others. + /// Returns the difference between multiple sorted sets. + /// + /// The `WITHSCORES` form, the members and their scores. /// /// See also `zdiff`. /// @@ -2049,24 +2022,21 @@ interface commands { @since(version = 0.3.0-dev) zdiff-withscores: async func(connection: borrow, keys: list) -> result>, error>; - /// Stores the members of the first sorted set that are not in any of the others, returning how - /// many. + /// Stores the difference of multiple sorted sets in a key. /// /// ZDIFFSTORE destination numkeys key [ key ... ] /// https://valkey.io/commands/zdiffstore/ @since(version = 0.3.0-dev) zdiffstore: async func(connection: borrow, destination: string, keys: list) -> result; - /// Increments the score of the member in the sorted set stored at key, returning the new score. + /// Increments the score of a member in a sorted set. /// /// ZINCRBY key increment member /// https://valkey.io/commands/zincrby/ @since(version = 0.3.0-dev) zincrby: async func(connection: borrow, key: string, increment: double, member: string-or-bytes) -> result; - /// The members in all of the sorted sets. - /// - /// The weights, when given, multiply the scores of each sorted set, one for each key. + /// Returns the intersect of multiple sorted sets. /// /// See also `zinter-withscores`. /// @@ -2075,7 +2045,9 @@ interface commands { @since(version = 0.3.0-dev) zinter: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; - /// The members, and their combined scores, in all of the sorted sets. + /// Returns the intersect of multiple sorted sets. + /// + /// The `WITHSCORES` form, the members and their combined scores. /// /// See also `zinter`. /// @@ -2084,64 +2056,62 @@ interface commands { @since(version = 0.3.0-dev) zinter-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; - /// The number of members in all of the sorted sets, counting stops at the limit. + /// Returns the number of members of the intersect of multiple sorted sets. /// /// ZINTERCARD numkeys key [ key ... ] [ LIMIT limit ] /// https://valkey.io/commands/zintercard/ @since(version = 0.3.0-dev) zintercard: async func(connection: borrow, keys: list, limit: option) -> result; - /// Stores the members in all of the sorted sets, with their combined scores, returning how many. + /// Stores the intersect of multiple sorted sets in a key. /// /// ZINTERSTORE destination numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zinterstore/ @since(version = 0.3.0-dev) zinterstore: async func(connection: borrow, destination: string, keys: list, weights: option>, aggregate: option) -> result; - /// The number of members of the sorted set stored at key in the range, for members of equal - /// score. + /// Returns the number of members in a sorted set within a lexicographical range. /// /// ZLEXCOUNT key min max /// https://valkey.io/commands/zlexcount/ @since(version = 0.3.0-dev) zlexcount: async func(connection: borrow, key: string, min: lex-bound, max: lex-bound) -> result; - /// Removes the members with the lowest or highest scores from the first non-empty sorted set, - /// returning its key and the members and their scores, none when every sorted set is empty. + /// Returns the highest- or lowest-scoring members from one or more sorted sets after removing them. /// /// ZMPOP numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ] /// https://valkey.io/commands/zmpop/ @since(version = 0.3.0-dev) zmpop: async func(connection: borrow, keys: list, end: zmpop-end, count: option) -> result>>>, error>; - /// The score of each of the members of the sorted set stored at key, none for a member that is - /// not in it. + /// Returns the score of one or more members in a sorted set. /// /// ZMSCORE key member [ member ... ] /// https://valkey.io/commands/zmscore/ @since(version = 0.3.0-dev) zmscore: async func(connection: borrow, key: string, members: list) -> result>, error>; - /// Removes and returns the members with the highest scores, and their scores, up to the count, - /// one without a count. + /// Returns the highest-scoring members from a sorted set after removing them. + /// + /// Without a count, at most one member. /// /// ZPOPMAX key [ count ] /// https://valkey.io/commands/zpopmax/ @since(version = 0.3.0-dev) zpopmax: async func(connection: borrow, key: string, count: option) -> result>, error>; - /// Removes and returns the members with the lowest scores, and their scores, up to the count, - /// one without a count. + /// Returns the lowest-scoring members from a sorted set after removing them. + /// + /// Without a count, at most one member. /// /// ZPOPMIN key [ count ] /// https://valkey.io/commands/zpopmin/ @since(version = 0.3.0-dev) zpopmin: async func(connection: borrow, key: string, count: option) -> result>, error>; - /// Random members of the sorted set stored at key, up to the count. + /// Returns one or more random members from a sorted set. /// - /// Without a count, one member, or none when the key does not exist. A negative count may return - /// the same member more than once, its absolute value is the number of members returned. + /// Without a count, at most one member. /// /// See also `zrandmember-withscores`. /// @@ -2150,10 +2120,9 @@ interface commands { @since(version = 0.3.0-dev) zrandmember: async func(connection: borrow, key: string, count: option) -> result, error>; - /// Random members of the sorted set stored at key, and their scores, up to the count. + /// Returns one or more random members from a sorted set. /// - /// A negative count may return the same member more than once, its absolute value is the number - /// of members returned. + /// The `WITHSCORES` form, the members and their scores. /// /// See also `zrandmember`. /// @@ -2162,25 +2131,23 @@ interface commands { @since(version = 0.3.0-dev) zrandmember-withscores: async func(connection: borrow, key: string, count: integer) -> result>, error>; - /// The members of the sorted set stored at key in the range, from the lowest score, or the - /// highest with `rev`. + /// Returns members in a sorted set within a range of indexes. /// - /// The limit is a range of the members in the range, only with BYSCORE or BYLEX. With `xx`, - /// returns none when the key does not exist. + /// Also covers ZRANGEBYLEX, ZRANGEBYSCORE, ZREVRANGE, ZREVRANGEBYLEX and ZREVRANGEBYSCORE. A + /// limit fails the command unless the range is by score or by lex. With `xx`, returns none when + /// the key does not exist. /// - /// Also covers ZRANGEBYLEX, ZRANGEBYSCORE, ZREVRANGE, ZREVRANGEBYLEX and ZREVRANGEBYSCORE. See - /// also `zrange-withscores`. + /// See also `zrange-withscores`. /// /// ZRANGE key start stop [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ] [ XX ] /// https://valkey.io/commands/zrange/ @since(version = 0.3.0-dev) zrange: async func(connection: borrow, key: string, range: zrange-by, rev: bool, limit: option, xx: bool) -> result>, error>; - /// The members of the sorted set stored at key in the range, and their scores, from the lowest - /// score, or the highest with `rev`. + /// Returns members in a sorted set within a range of indexes. /// - /// The limit is a range of the members in the range, only with BYSCORE. With `xx`, returns none - /// when the key does not exist. + /// The `WITHSCORES` form, the members and their scores. A limit fails the command unless the + /// range is by score. With `xx`, returns none when the key does not exist. /// /// See also `zrange`. /// @@ -2189,27 +2156,27 @@ interface commands { @since(version = 0.3.0-dev) zrange-withscores: async func(connection: borrow, key: string, range: zrange-withscores-by, rev: bool, limit: option, xx: bool) -> result>>, error>; - /// Stores the members of the sorted set stored at source in the range, returning how many. + /// Stores a range of members from sorted set in a key. /// - /// The limit is a range of the members in the range, only with BYSCORE or BYLEX. + /// A limit fails the command unless the range is by score or by lex. /// /// ZRANGESTORE dst src min max [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ] /// https://valkey.io/commands/zrangestore/ @since(version = 0.3.0-dev) zrangestore: async func(connection: borrow, destination: string, source: string, range: zrangestore-by, rev: bool, limit: option) -> result; - /// The position of the member in the sorted set stored at key, from the lowest score, none when - /// it is not a member. + /// Returns the index of a member in a sorted set ordered by ascending scores. /// - /// See also `zrank-withscore` and `zrevrank`. + /// See also `zrank-withscore`. /// /// ZRANK key member /// https://valkey.io/commands/zrank/ @since(version = 0.3.0-dev) zrank: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; - /// The position, and the score, of the member in the sorted set stored at key, from the lowest - /// score, none when it is not a member. + /// Returns the index of a member in a sorted set ordered by ascending scores. + /// + /// The `WITHSCORE` form, the position and the score. /// /// See also `zrank`. /// @@ -2218,39 +2185,35 @@ interface commands { @since(version = 0.3.0-dev) zrank-withscore: async func(connection: borrow, key: string, member: string-or-bytes) -> result>, error>; - /// Removes the members from the sorted set stored at key, returning how many were members. + /// Removes one or more members from a sorted set. /// /// ZREM key member [ member ... ] /// https://valkey.io/commands/zrem/ @since(version = 0.3.0-dev) zrem: async func(connection: borrow, key: string, members: list) -> result; - /// Removes the members of the sorted set stored at key in the range, for members of equal score, - /// returning how many. + /// Removes members in a sorted set within a lexicographical range. /// /// ZREMRANGEBYLEX key min max /// https://valkey.io/commands/zremrangebylex/ @since(version = 0.3.0-dev) zremrangebylex: async func(connection: borrow, key: string, min: lex-bound, max: lex-bound) -> result; - /// Removes the members of the sorted set stored at key at the positions, both included, - /// negative from the end, returning how many. + /// Removes members in a sorted set within a range of indexes. /// /// ZREMRANGEBYRANK key start stop /// https://valkey.io/commands/zremrangebyrank/ @since(version = 0.3.0-dev) zremrangebyrank: async func(connection: borrow, key: string, start: integer, stop: integer) -> result; - /// Removes the members of the sorted set stored at key with scores in the range, returning how - /// many. + /// Removes members in a sorted set within a range of scores. /// /// ZREMRANGEBYSCORE key min max /// https://valkey.io/commands/zremrangebyscore/ @since(version = 0.3.0-dev) zremrangebyscore: async func(connection: borrow, key: string, min: score-bound, max: score-bound) -> result; - /// The position of the member in the sorted set stored at key, from the highest score, none - /// when it is not a member. + /// Returns the index of a member in a sorted set ordered by descending scores. /// /// See also `zrevrank-withscore`. /// @@ -2259,8 +2222,9 @@ interface commands { @since(version = 0.3.0-dev) zrevrank: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; - /// The position, and the score, of the member in the sorted set stored at key, from the highest - /// score, none when it is not a member. + /// Returns the index of a member in a sorted set ordered by descending scores. + /// + /// The `WITHSCORE` form, the position and the score. /// /// See also `zrevrank`. /// @@ -2269,7 +2233,7 @@ interface commands { @since(version = 0.3.0-dev) zrevrank-withscore: async func(connection: borrow, key: string, member: string-or-bytes) -> result>, error>; - /// Incrementally iterate over the members of the sorted set stored at key, and their scores. + /// Iterates over members and scores of a sorted set. /// /// See also `zscan-noscores`. /// @@ -2278,7 +2242,9 @@ interface commands { @since(version = 0.3.0-dev) zscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>>, error>; - /// Incrementally iterate over the members of the sorted set stored at key. + /// Iterates over members and scores of a sorted set. + /// + /// The `NOSCORES` form, the members only. /// /// See also `zscan`. /// @@ -2287,16 +2253,14 @@ interface commands { @since(version = 0.3.0-dev) zscan-noscores: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; - /// The score of the member in the sorted set stored at key, none when it is not a member. + /// Returns the score of a member in a sorted set. /// /// ZSCORE key member /// https://valkey.io/commands/zscore/ @since(version = 0.3.0-dev) zscore: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; - /// The members in any of the sorted sets. - /// - /// The weights, when given, multiply the scores of each sorted set, one for each key. + /// Returns the union of multiple sorted sets. /// /// See also `zunion-withscores`. /// @@ -2305,7 +2269,9 @@ interface commands { @since(version = 0.3.0-dev) zunion: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; - /// The members, and their combined scores, in any of the sorted sets. + /// Returns the union of multiple sorted sets. + /// + /// The `WITHSCORES` form, the members and their combined scores. /// /// See also `zunion`. /// @@ -2314,153 +2280,157 @@ interface commands { @since(version = 0.3.0-dev) zunion-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; - /// Stores the members in any of the sorted sets, with their combined scores, returning how many. + /// Stores the union of multiple sorted sets in a key. /// /// ZUNIONSTORE destination numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zunionstore/ @since(version = 0.3.0-dev) zunionstore: async func(connection: borrow, destination: string, keys: list, weights: option>, aggregate: option) -> result; - /// Removes one or multiple messages from the Pending Entries List (PEL) of a stream consumer group. + /// Returns the number of messages that were successfully acknowledged by the consumer group member of a stream. /// /// XACK key group ID [ ID ... ] /// https://valkey.io/commands/xack/ @since(version = 0.3.0-dev) xack: async func(connection: borrow, key: string, group: string, ids: list) -> result; - /// Appends the specified stream entry to the stream at the specified key. + /// Appends a new message to a stream. /// /// XADD key [ NOMKSTREAM ] [ 〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] ] 〈 * | id 〉 field value [ field value ... ] /// https://valkey.io/commands/xadd/ @since(version = 0.3.0-dev) xadd: async func(connection: borrow, key: string, nomkstream: bool, trim: option, id: stream-id, field-and-values: list>) -> result, error>; - /// Transfers ownership of pending stream entries that match the specified criteria. + /// Changes, or acquires, ownership of messages in a consumer group, as if the messages were delivered to a consumer group member. /// - /// See also `xautoclaim-justid` + /// A start of none scans from the beginning, `0-0`. The next start is returned, none once the + /// scan is complete. /// - /// A start of none scans from the beginning, `0-0`. The next start is returned, none once the scan - /// is complete, along with the IDs of claimed entries that no longer exist, which are removed - /// from the pending entries list. + /// See also `xautoclaim-justid`. /// /// XAUTOCLAIM key group consumer min-idle-time start [ COUNT count ] /// https://valkey.io/commands/xautoclaim/ @since(version = 0.3.0-dev) xautoclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, start: option, count: option) -> result, list>>, list>, error>; - /// Transfers ownership of pending stream entries that match the specified criteria. + /// Changes, or acquires, ownership of messages in a consumer group, as if the messages were delivered to a consumer group member. /// - /// See also `xautoclaim` + /// The `JUSTID` form, the IDs only. A start of none scans from the beginning, `0-0`. The next + /// start is returned, none once the scan is complete. /// - /// A start of none scans from the beginning, `0-0`. The next start is returned, none once the scan - /// is complete, along with the IDs of claimed entries that no longer exist, which are removed - /// from the pending entries list. + /// See also `xautoclaim`. /// /// XAUTOCLAIM key group consumer min-idle-time start [ COUNT count ] JUSTID /// https://valkey.io/commands/xautoclaim/ @since(version = 0.3.0-dev) xautoclaim-justid: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, start: option, count: option) -> result, list, list>, error>; - /// Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument. + /// Changes, or acquires, ownership of a message in a consumer group, as if the message was delivered to a consumer group member. /// - /// Also see `xclaim-justid` + /// See also `xclaim-justid`. /// /// XCLAIM key group consumer min-idle-time ID [ ID ... ] [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] [ LASTID lastid ] /// https://valkey.io/commands/xclaim/ @since(version = 0.3.0-dev) xclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, ids: list, opts: option) -> result>>, error>; - /// Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument. + /// Changes, or acquires, ownership of a message in a consumer group, as if the message was delivered to a consumer group member. + /// + /// The `JUSTID` form, the IDs only. /// - /// Also see `xclaim` + /// See also `xclaim`. /// /// XCLAIM key group consumer min-idle-time ID [ ID ... ] [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] JUSTID [ LASTID lastid ] /// https://valkey.io/commands/xclaim/ @since(version = 0.3.0-dev) xclaim-justid: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, ids: list, opts: option) -> result, error>; - /// Removes the specified entries from a stream, and returns the number of entries deleted. + /// Returns the number of messages after removing them from a stream. /// /// XDEL key ID [ ID ... ] /// https://valkey.io/commands/xdel/ @since(version = 0.3.0-dev) xdel: async func(connection: borrow, key: string, ids: list) -> result; - /// Create a new consumer group uniquely identified by for the stream stored at . + /// Creates a consumer group. /// /// XGROUP CREATE key group 〈 id | $ 〉 [ MKSTREAM ] [ ENTRIESREAD entriesread ] /// https://valkey.io/commands/xgroup-create/ @since(version = 0.3.0-dev) xgroup-create: async func(connection: borrow, key: string, group: string, id: stream-id, mkstream: bool, entriesread: option) -> result<_, error>; - /// Create a consumer named in the consumer group of the stream that's stored at . + /// Creates a consumer in a consumer group. /// /// XGROUP CREATECONSUMER key group consumer /// https://valkey.io/commands/xgroup-createconsumer/ @since(version = 0.3.0-dev) xgroup-createconsumer: async func(connection: borrow, key: string, group: string, consumer: string) -> result; - /// The XGROUP DELCONSUMER command deletes a consumer from the consumer group. + /// Deletes a consumer from a consumer group. /// /// XGROUP DELCONSUMER key group consumer /// https://valkey.io/commands/xgroup-delconsumer/ @since(version = 0.3.0-dev) xgroup-delconsumer: async func(connection: borrow, key: string, group: string, consumer: string) -> result; - /// Completely destroys the consumer group. + /// Destroys a consumer group. /// /// XGROUP DESTROY key group /// https://valkey.io/commands/xgroup-destroy/ @since(version = 0.3.0-dev) xgroup-destroy: async func(connection: borrow, key: string, group: string) -> result; - /// Set the last delivered ID for a consumer group. + /// Sets the last-delivered ID of a consumer group. /// /// XGROUP SETID key group 〈 id | $ 〉 [ ENTRIESREAD entriesread ] /// https://valkey.io/commands/xgroup-setid/ @since(version = 0.3.0-dev) xgroup-setid: async func(connection: borrow, key: string, group: string, id: stream-id, entriesread: option) -> result<_, error>; - /// Consumers that belong to the consumer group of the stream stored at . + /// Returns a list of the consumers in a consumer group. /// /// XINFO CONSUMERS key group /// https://valkey.io/commands/xinfo-consumers/ @since(version = 0.3.0-dev) xinfo-consumers: async func(connection: borrow, key: string, group: string) -> result, error>; - /// Consumer groups of the stream stored at . + /// Returns a list of the consumer groups of a stream. /// /// XINFO GROUPS key /// https://valkey.io/commands/xinfo-groups/ @since(version = 0.3.0-dev) xinfo-groups: async func(connection: borrow, key: string) -> result, error>; - /// Information about the stream stored at . + /// Returns information about a stream. /// - /// See also `xinfo-stream-full` + /// See also `xinfo-stream-full`. /// /// XINFO STREAM key /// https://valkey.io/commands/xinfo-stream/ @since(version = 0.3.0-dev) xinfo-stream: async func(connection: borrow, key: string) -> result; - /// Information about the stream stored at . + /// Returns information about a stream. + /// + /// The `FULL` form, with the entries, groups and consumers. /// - /// See also `xinfo-stream` + /// See also `xinfo-stream`. /// /// XINFO STREAM key FULL [ COUNT count ] /// https://valkey.io/commands/xinfo-stream/ @since(version = 0.3.0-dev) xinfo-stream-full: async func(connection: borrow, key: string, count: option) -> result; - /// Number of entries inside a stream. + /// Returns the number of messages in a stream. /// /// XLEN key /// https://valkey.io/commands/xlen/ @since(version = 0.3.0-dev) xlen: async func(connection: borrow, key: string) -> result; - /// List of pending messages. + /// Returns the information and entries from a stream consumer group's pending entries list. + /// + /// The summary form. /// /// See also `xpending-extended`. /// @@ -2469,7 +2439,9 @@ interface commands { @since(version = 0.3.0-dev) xpending: async func(connection: borrow, key: string, group: string) -> result; - /// List of pending messages. + /// Returns the information and entries from a stream consumer group's pending entries list. + /// + /// The extended form, the pending entries in the range. /// /// See also `xpending`. /// @@ -2478,242 +2450,258 @@ interface commands { @since(version = 0.3.0-dev) xpending-extended: async func(connection: borrow, key: string, group: string, min-idle-time: option, start: stream-id, end: stream-id, count: integer, consumer: option) -> result, error>; - /// Stream entries matching a given range of IDs. + /// Returns the messages from a stream within a range of IDs. /// /// XRANGE key start end [ COUNT count ] /// https://valkey.io/commands/xrange/ @since(version = 0.3.0-dev) xrange: async func(connection: borrow, key: string, start: stream-id, end: stream-id, count: option) -> result>>, error>; - /// Read data from one or multiple streams, only returning entries with an ID greater than the last received ID reported by the caller. + /// Returns messages from multiple streams with IDs greater than the ones requested. + /// + /// With `block`, returns none when the timeout passed. The connection waits too, replies to + /// commands sent after it on the connection wait behind it. Use a connection of your own. /// /// XREAD [ COUNT count ] [ BLOCK milliseconds ] STREAMS key [ key ... ] ID [ ID ... ] /// https://valkey.io/commands/xread/ @since(version = 0.3.0-dev) xread: async func(connection: borrow, count: option, block: option, keys: list, ids: list) -> result>>>>, error>; - /// Special version of the XREAD command with support for consumer groups. + /// Returns new or historical messages from a stream for a consumer in a group. /// - /// The fields of an entry are none when it was deleted after it was delivered, e.g. when reading - /// the pending entries with the ID `0`. + /// The fields of an entry are none when it was deleted after it was delivered, e.g. when + /// reading the pending entries with the ID `0`. + /// + /// With `block`, returns none when the timeout passed. The connection waits too, replies to + /// commands sent after it on the connection wait behind it. Use a connection of your own. /// /// XREADGROUP GROUP group consumer [ COUNT count ] [ BLOCK milliseconds ] [ NOACK ] STREAMS key [ key ... ] ID [ ID ... ] /// https://valkey.io/commands/xreadgroup/ @since(version = 0.3.0-dev) xreadgroup: async func(connection: borrow, group: string, consumer: string, count: option, block: option, noack: bool, keys: list, ids: list) -> result>>>>>, error>; - /// XRANGE, but returning the entries in reverse order. + /// Returns the messages from a stream within a range of IDs in reverse order. /// /// XREVRANGE key end start [ COUNT count ] /// https://valkey.io/commands/xrevrange/ @since(version = 0.3.0-dev) xrevrange: async func(connection: borrow, key: string, end: stream-id, start: stream-id, count: option) -> result>>, error>; - /// Internal command. It is used by a Valkey primary to replicate the last delivered ID of streams. + /// An internal command for replicating stream values. /// /// XSETID key last-id [ ENTRIESADDED entries-added ] [ MAXDELETEDID max-deleted-id ] /// https://valkey.io/commands/xsetid/ @since(version = 0.3.0-dev) xsetid: async func(connection: borrow, key: string, last-id: stream-id, entries-added: option, max-deleted-id: option) -> result<_, error>; - /// Trims the stream by evicting older entries. + /// Deletes messages from the beginning of a stream. /// /// XTRIM key 〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] /// https://valkey.io/commands/xtrim/ @since(version = 0.3.0-dev) xtrim: async func(connection: borrow, key: string, trim: stream-trim) -> result; - /// Appends the value at the end of the string. + /// Appends a string to the value of a key. /// /// APPEND key value /// https://valkey.io/commands/append/ @since(version = 0.3.0-dev) append: async func(connection: borrow, key: string, value: string-or-bytes) -> result; - /// Decrements the number stored at key by one. + /// Decrements the integer value of a key by one. /// /// DECR key /// https://valkey.io/commands/decr/ @since(version = 0.3.0-dev) decr: async func(connection: borrow, key: string) -> result; - /// Decrements the number stored at key by decrement. + /// Decrements the integer value of a key by a number. /// /// DECRBY key decrement /// https://valkey.io/commands/decrby/ @since(version = 0.3.0-dev) decrby: async func(connection: borrow, key: string, decrement: integer) -> result; - /// Deletes the key if its value matches the given string. + /// Delete key if value matches string. /// /// DELIFEQ key value /// https://valkey.io/commands/delifeq/ @since(version = 0.3.0-dev) delifeq: async func(connection: borrow, key: string, value: string-or-bytes) -> result; - /// Get the value of key. + /// Returns the string value of a key. /// /// GET key /// https://valkey.io/commands/get/ @since(version = 0.3.0-dev) get: async func(connection: borrow, key: string) -> result, error>; - /// Get the value of key and delete the key. + /// Returns the string value of a key after deleting the key. /// /// GETDEL key /// https://valkey.io/commands/getdel/ @since(version = 0.3.0-dev) getdel: async func(connection: borrow, key: string) -> result, error>; - /// Get the value of key and optionally set its expiration. + /// Returns the string value of a key after setting its expiration time. /// /// GETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ] /// https://valkey.io/commands/getex/ @since(version = 0.3.0-dev) getex: async func(connection: borrow, key: string, ttl: option) -> result, error>; - /// Returns the substring of the string value stored at key, determined by the offsets start and end. + /// Returns a substring of the string stored at a key. /// /// GETRANGE key start end /// https://valkey.io/commands/getrange/ @since(version = 0.3.0-dev) getrange: async func(connection: borrow, key: string, start: integer, end: integer) -> result; - /// Atomically sets key to value and returns the old value stored at key. + /// Returns the previous string value of a key after setting it to a new value. /// /// GETSET key value /// https://valkey.io/commands/getset/ @since(version = 0.3.0-dev) getset: async func(connection: borrow, key: string, value: string-or-bytes) -> result, error>; - /// Increments the number stored at key by one. + /// Increments the integer value of a key by one. /// /// INCR key /// https://valkey.io/commands/incr/ @since(version = 0.3.0-dev) incr: async func(connection: borrow, key: string) -> result; - /// Increments the number stored at key by increment. + /// Increments the integer value of a key by a number. /// /// INCRBY key increment /// https://valkey.io/commands/incrby/ @since(version = 0.3.0-dev) incrby: async func(connection: borrow, key: string, increment: integer) -> result; - /// Increment the string representing a floating point number stored at key by the specified increment. + /// Increments the floating point value of a key by a number. /// /// INCRBYFLOAT key increment /// https://valkey.io/commands/incrbyfloat/ @since(version = 0.3.0-dev) incrbyfloat: async func(connection: borrow, key: string, increment: double) -> result; - /// The LCS command implements the longest common subsequence algorithm. + /// Finds the longest common substring. /// - /// See also `lcs-len` and `lcs-idx` + /// See also `lcs-len` and `lcs-idx`. /// /// LCS key1 key2 /// https://valkey.io/commands/lcs/ @since(version = 0.3.0-dev) lcs: async func(connection: borrow, key1: string, key2: string) -> result; - /// The LCS command implements the longest common subsequence algorithm. + /// Finds the longest common substring. + /// + /// The `LEN` form, the length only. /// - /// See also `lcs` and `lcs-idx` + /// See also `lcs` and `lcs-idx`. /// /// LCS key1 key2 LEN /// https://valkey.io/commands/lcs/ @since(version = 0.3.0-dev) lcs-len: async func(connection: borrow, key1: string, key2: string) -> result; - /// The LCS command implements the longest common subsequence algorithm. + /// Finds the longest common substring. + /// + /// The `IDX` form, the ranges of the matches. /// - /// See also `lcs` and `lcs-len` + /// See also `lcs` and `lcs-len`. /// /// LCS key1 key2 IDX [ MINMATCHLEN min-match-len ] [ WITHMATCHLEN ] /// https://valkey.io/commands/lcs/ @since(version = 0.3.0-dev) lcs-idx: async func(connection: borrow, key1: string, key2: string, min-match-len: option, with-match-len: bool ) -> result; - /// Returns the values of all specified keys. + /// Atomically returns the string values of one or more keys. /// /// MGET key [ key ... ] /// https://valkey.io/commands/mget/ @since(version = 0.3.0-dev) mget: async func(connection: borrow, keys: list) -> result>, error>; - /// Sets the given keys to their respective values. + /// Atomically creates or modifies the string values of one or more keys. /// /// MSET key value [ key value ... ] /// https://valkey.io/commands/mset/ @since(version = 0.3.0-dev) mset: async func(connection: borrow, keys-and-values: list>) -> result<_, error>; - /// Sets multiple key-value pairs while also assigning a shared expiration time to all the keys atomically. + /// Atomically creates or modifies the string values of one or more keys, and optionally set their expiration. /// /// MSETEX numkeys key value [ key value ... ] [ NX | XX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] /// https://valkey.io/commands/msetex/ @since(version = 0.3.0-dev) msetex: async func(connection: borrow, keys-and-values: list>, precondition: option, ttl: option) -> result; - /// Sets the given keys to their respective values. + /// Atomically modifies the string values of one or more keys only when all keys don't exist. /// /// MSETNX key value [ key value ... ] /// https://valkey.io/commands/msetnx/ @since(version = 0.3.0-dev) msetnx: async func(connection: borrow, keys-and-values: list>) -> result; - /// Set key to hold the string value with expiration in milliseconds. + /// Sets both string value and expiration time in milliseconds of a key. /// /// PSETEX key milliseconds value /// https://valkey.io/commands/psetex/ @since(version = 0.3.0-dev) psetex: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, value: string-or-bytes) -> result<_, error>; - /// Set key to hold the string value. + /// Sets the string value of a key, ignoring its type. /// /// Returns false when the precondition is not met, the key is unchanged. /// + /// See also `set-get`. + /// /// SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] /// https://valkey.io/commands/set/ @since(version = 0.3.0-dev) set: async func(connection: borrow, key: string, value: string-or-bytes, precondition: option, ttl: option) -> result; - /// Set key to hold the string value. Returning the previous value. + /// Sets the string value of a key, ignoring its type. + /// + /// The `GET` form, returning the previous value, none when the key did not exist. + /// + /// See also `set`. /// /// SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] GET [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] /// https://valkey.io/commands/set/ @since(version = 0.3.0-dev) set-get: async func(connection: borrow, key: string, value: string-or-bytes, precondition: option, ttl: option) -> result, error>; - /// Set key to hold the string value with expiration in seconds. + /// Sets the string value and expiration time of a key. /// /// SETEX key seconds value /// https://valkey.io/commands/setex/ @since(version = 0.3.0-dev) setex: async func(connection: borrow, key: string, seconds: duration-seconds, value: string-or-bytes) -> result<_, error>; - /// Set key to hold string value if key does not exist. + /// Sets the string value of a key only when the key doesn't exist. /// /// SETNX key value /// https://valkey.io/commands/setnx/ @since(version = 0.3.0-dev) setnx: async func(connection: borrow, key: string, value: string-or-bytes) -> result; - /// Overwrites part of the string stored at key, starting at the specified offset, for the entire length of value. + /// Overwrites a part of a string value with another by an offset. /// /// SETRANGE key offset value /// https://valkey.io/commands/setrange/ @since(version = 0.3.0-dev) setrange: async func(connection: borrow, key: string, offset: u32, value: string-or-bytes) -> result; - /// Returns the length of the string value stored at key. + /// Returns the length of a string value. /// /// STRLEN key - /// hthttps://valkey.io/commands/strlen/ + /// https://valkey.io/commands/strlen/ @since(version = 0.3.0-dev) strlen: async func(connection: borrow, key: string) -> result; - /// Returns the substring of the string value stored at key, determined by the offsets start and end. + /// Returns a substring from a string value. /// /// SUBSTR key start end /// https://valkey.io/commands/substr/ From 2ac7bb77fb2dc952f7a96c9a4527518a9dccc42e Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Thu, 15 Oct 2026 21:58:28 -0400 Subject: [PATCH 37/37] refine error codes Signed-off-by: Scott Andrews --- components/as-keyvalue/src/lib.rs | 2 +- components/commands/src/lib.rs | 738 ++++++++++-------- components/connection/src/lib.rs | 118 +-- components/connection/src/pubsub.rs | 77 +- components/connection/src/pubsub_disabled.rs | 23 +- components/connection/tests/connection.rs | 107 ++- components/internal-cli/src/lib.rs | 67 +- components/tcp-connector/src/lib.rs | 16 +- components/tcp-connector/tests/connector.rs | 12 +- components/tcp/src/lib.rs | 62 +- components/tcp/tests/tcp.rs | 8 +- components/test-commands-suite/src/lib.rs | 18 +- crates/componentized-valkey/src/command.rs | 34 +- .../src/commands/admin.rs | 100 +-- .../src/commands/bitmap.rs | 24 +- .../src/commands/connection.rs | 38 +- .../componentized-valkey/src/commands/geo.rs | 18 +- .../componentized-valkey/src/commands/hash.rs | 17 +- .../src/commands/keyspace.rs | 12 +- .../componentized-valkey/src/commands/list.rs | 10 +- .../src/commands/scripting.rs | 20 +- .../src/commands/shared.rs | 34 +- .../src/commands/sortedset.rs | 20 +- .../src/commands/stream.rs | 32 +- .../src/commands/string.rs | 16 +- crates/componentized-valkey/src/resp.rs | 42 +- .../src/string_or_bytes.rs | 20 + crates/componentized-valkey/src/test_util.rs | 6 +- crates/componentized-valkey/src/value.rs | 72 +- crates/test-harness/src/lib.rs | 8 +- wit/commands.wit | 676 ++++++++-------- wit/connection.wit | 26 +- wit/connector.wit | 4 +- wit/tcp.wit | 10 +- wit/types.wit | 9 +- 35 files changed, 1339 insertions(+), 1157 deletions(-) diff --git a/components/as-keyvalue/src/lib.rs b/components/as-keyvalue/src/lib.rs index a15b4e3..e56f951 100644 --- a/components/as-keyvalue/src/lib.rs +++ b/components/as-keyvalue/src/lib.rs @@ -1,5 +1,5 @@ use componentized::valkey::commands::{self, Cursor, DurationMilliseconds, StringOrBytes}; -use componentized::valkey::connection::{Connection, Error as ValkeyError}; +use componentized::valkey::connection::{Connection, ErrorCode as ValkeyError}; use componentized::valkey::connector; use exports::wasmcloud::keyvalue::atomics::Guest as AtomicsGuest; use exports::wasmcloud::keyvalue::batch::Guest as BatchGuest; diff --git a/components/commands/src/lib.rs b/components/commands/src/lib.rs index 3eba690..632f3ee 100644 --- a/components/commands/src/lib.rs +++ b/components/commands/src/lib.rs @@ -5,7 +5,7 @@ use crate::exports::componentized::valkey::commands::{ ClusterBumpepochResult, ClusterFailoverMode, ClusterGetslotmigrationsEntry, ClusterMigrateslotsGroup, ClusterResetMode, ClusterSetslotState, CommandDocsReply, CommandInfoReply, CommandListFilter, CommandlogEntry, CommandlogType, Connection, Cursor, - Double, DurationMilliseconds, DurationSeconds, Error, ExpiretimeReply, FailoverTarget, + Double, DurationMilliseconds, DurationSeconds, ErrorCode, ExpiretimeReply, FailoverTarget, FlushMode, FunctionListLibrary, FunctionRestorePolicy, FunctionStatsReply, GeoCoordinates, GeoUnit, GeoaddCondition, GeosearchCount, GeosearchShape, GeosearchWithFlags, GeosearchWithResult, GetexTtl, Guest, HexpireResult, HgetexTtl, HpersistResult, @@ -41,7 +41,7 @@ use componentized_valkey::{FromValue, IntoCommand}; async fn exchange( connection: &Connection, command: impl IntoCommand, -) -> Result { +) -> Result { let reply = connection.exchange(command.into_command()?).await?; T::from_value(reply) } @@ -81,7 +81,7 @@ impl Guest for Commands { async fn acl_deluser( connection: &Connection, usernames: Vec, - ) -> Result { + ) -> Result { exchange( connection, ("ACL", "DELUSER", NonEmpty::new("usernames", usernames)), @@ -94,7 +94,10 @@ impl Guest for Commands { #[doc = " ACL GENPASS [ bits ]"] #[doc = " https://valkey.io/commands/acl-genpass/"] #[allow(async_fn_in_trait)] - async fn acl_genpass(connection: &Connection, bits: Option) -> Result { + async fn acl_genpass( + connection: &Connection, + bits: Option, + ) -> Result { exchange(connection, ("ACL", "GENPASS", bits)).await } @@ -107,7 +110,7 @@ impl Guest for Commands { connection: &Connection, username: String, rules: Option>, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange( connection, ( @@ -128,7 +131,7 @@ impl Guest for Commands { async fn acl_cat( connection: &Connection, category: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("ACL", "CAT", category)).await } @@ -141,7 +144,7 @@ impl Guest for Commands { connection: &Connection, username: String, command: Vec, - ) -> Result { + ) -> Result { exchange( connection, ("ACL", "DRYRUN", username, NonEmpty::new("command", command)), @@ -160,7 +163,7 @@ impl Guest for Commands { async fn acl_getuser( connection: &Connection, username: String, - ) -> Result>, Error> { + ) -> Result>, ErrorCode> { let rules: Option = exchange(connection, ("ACL", "GETUSER", username)).await?; Ok(rules.map(|rules| rules.0)) @@ -171,7 +174,7 @@ impl Guest for Commands { #[doc = " ACL LIST"] #[doc = " https://valkey.io/commands/acl-list/"] #[allow(async_fn_in_trait)] - async fn acl_list(connection: &Connection) -> Result, Error> { + async fn acl_list(connection: &Connection) -> Result, ErrorCode> { let users: AclList = exchange(connection, ("ACL", "LIST")).await?; Ok(users.0) } @@ -181,7 +184,7 @@ impl Guest for Commands { #[doc = " ACL LOAD"] #[doc = " https://valkey.io/commands/acl-load/"] #[allow(async_fn_in_trait)] - async fn acl_load(connection: &Connection) -> Result<(), Error> { + async fn acl_load(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, ("ACL", "LOAD")).await } @@ -195,7 +198,7 @@ impl Guest for Commands { async fn acl_log( connection: &Connection, count: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("ACL", "LOG", count)).await } @@ -208,7 +211,7 @@ impl Guest for Commands { #[doc = " ACL LOG RESET"] #[doc = " https://valkey.io/commands/acl-log/"] #[allow(async_fn_in_trait)] - async fn acl_log_reset(connection: &Connection) -> Result<(), Error> { + async fn acl_log_reset(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, ("ACL", "LOG", "RESET")).await } @@ -217,7 +220,7 @@ impl Guest for Commands { #[doc = " ACL SAVE"] #[doc = " https://valkey.io/commands/acl-save/"] #[allow(async_fn_in_trait)] - async fn acl_save(connection: &Connection) -> Result<(), Error> { + async fn acl_save(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, ("ACL", "SAVE")).await } @@ -226,7 +229,7 @@ impl Guest for Commands { #[doc = " ACL USERS"] #[doc = " https://valkey.io/commands/acl-users/"] #[allow(async_fn_in_trait)] - async fn acl_users(connection: &Connection) -> Result, Error> { + async fn acl_users(connection: &Connection) -> Result, ErrorCode> { exchange(connection, ("ACL", "USERS")).await } @@ -235,7 +238,7 @@ impl Guest for Commands { #[doc = " ACL WHOAMI"] #[doc = " https://valkey.io/commands/acl-whoami/"] #[allow(async_fn_in_trait)] - async fn acl_whoami(connection: &Connection) -> Result { + async fn acl_whoami(connection: &Connection) -> Result { exchange(connection, ("ACL", "WHOAMI")).await } @@ -244,7 +247,7 @@ impl Guest for Commands { #[doc = " BGREWRITEAOF"] #[doc = " https://valkey.io/commands/bgrewriteaof/"] #[allow(async_fn_in_trait)] - async fn bgrewriteaof(connection: &Connection) -> Result { + async fn bgrewriteaof(connection: &Connection) -> Result { exchange(connection, "BGREWRITEAOF").await } @@ -256,7 +259,7 @@ impl Guest for Commands { async fn bgsave( connection: &Connection, operation: Option, - ) -> Result { + ) -> Result { exchange(connection, ("BGSAVE", operation)).await } @@ -268,7 +271,7 @@ impl Guest for Commands { async fn client_kill( connection: &Connection, filters: Vec, - ) -> Result { + ) -> Result { exchange( connection, ("CLIENT", "KILL", NonEmpty::new("filters", filters)), @@ -287,7 +290,7 @@ impl Guest for Commands { async fn client_list( connection: &Connection, filters: Option>, - ) -> Result>, Error> { + ) -> Result>, ErrorCode> { let command = ("CLIENT", "LIST", NonEmpty::optional("filters", filters)); let clients: ClientList = exchange(connection, command).await?; Ok(clients.0) @@ -300,7 +303,7 @@ impl Guest for Commands { #[doc = " CLIENT NO-EVICT 〈 ON | OFF 〉"] #[doc = " https://valkey.io/commands/client-no-evict/"] #[allow(async_fn_in_trait)] - async fn client_no_evict(connection: &Connection, enabled: bool) -> Result<(), Error> { + async fn client_no_evict(connection: &Connection, enabled: bool) -> Result<(), ErrorCode> { exchange( connection, ("CLIENT", "NO-EVICT", if enabled { "ON" } else { "OFF" }), @@ -317,7 +320,7 @@ impl Guest for Commands { connection: &Connection, timeout: DurationMilliseconds, mode: Option, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange(connection, ("CLIENT", "PAUSE", timeout, mode)).await } @@ -330,7 +333,7 @@ impl Guest for Commands { connection: &Connection, client_id: Integer, unblock_type: Option, - ) -> Result { + ) -> Result { exchange(connection, ("CLIENT", "UNBLOCK", client_id, unblock_type)).await } @@ -339,7 +342,7 @@ impl Guest for Commands { #[doc = " CLIENT UNPAUSE"] #[doc = " https://valkey.io/commands/client-unpause/"] #[allow(async_fn_in_trait)] - async fn client_unpause(connection: &Connection) -> Result<(), Error> { + async fn client_unpause(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, ("CLIENT", "UNPAUSE")).await } @@ -348,7 +351,7 @@ impl Guest for Commands { #[doc = " CLUSTER ADDSLOTS slot [ slot ... ]"] #[doc = " https://valkey.io/commands/cluster-addslots/"] #[allow(async_fn_in_trait)] - async fn cluster_addslots(connection: &Connection, slots: Vec) -> Result<(), Error> { + async fn cluster_addslots(connection: &Connection, slots: Vec) -> Result<(), ErrorCode> { exchange( connection, ("CLUSTER", "ADDSLOTS", NonEmpty::new("slots", slots)), @@ -364,7 +367,7 @@ impl Guest for Commands { async fn cluster_addslotsrange( connection: &Connection, ranges: Vec<(u16, u16)>, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange( connection, ("CLUSTER", "ADDSLOTSRANGE", NonEmpty::new("ranges", ranges)), @@ -377,7 +380,9 @@ impl Guest for Commands { #[doc = " CLUSTER BUMPEPOCH"] #[doc = " https://valkey.io/commands/cluster-bumpepoch/"] #[allow(async_fn_in_trait)] - async fn cluster_bumpepoch(connection: &Connection) -> Result { + async fn cluster_bumpepoch( + connection: &Connection, + ) -> Result { exchange(connection, ("CLUSTER", "BUMPEPOCH")).await } @@ -386,7 +391,7 @@ impl Guest for Commands { #[doc = " CLUSTER CANCELSLOTMIGRATIONS"] #[doc = " https://valkey.io/commands/cluster-cancelslotmigrations/"] #[allow(async_fn_in_trait)] - async fn cluster_cancelslotmigrations(connection: &Connection) -> Result<(), Error> { + async fn cluster_cancelslotmigrations(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, ("CLUSTER", "CANCELSLOTMIGRATIONS")).await } @@ -398,7 +403,7 @@ impl Guest for Commands { async fn cluster_count_failure_reports( connection: &Connection, node_id: String, - ) -> Result { + ) -> Result { exchange(connection, ("CLUSTER", "COUNT-FAILURE-REPORTS", node_id)).await } @@ -407,7 +412,7 @@ impl Guest for Commands { #[doc = " CLUSTER DELSLOTS slot [ slot ... ]"] #[doc = " https://valkey.io/commands/cluster-delslots/"] #[allow(async_fn_in_trait)] - async fn cluster_delslots(connection: &Connection, slots: Vec) -> Result<(), Error> { + async fn cluster_delslots(connection: &Connection, slots: Vec) -> Result<(), ErrorCode> { exchange( connection, ("CLUSTER", "DELSLOTS", NonEmpty::new("slots", slots)), @@ -423,7 +428,7 @@ impl Guest for Commands { async fn cluster_delslotsrange( connection: &Connection, ranges: Vec<(u16, u16)>, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange( connection, ("CLUSTER", "DELSLOTSRANGE", NonEmpty::new("ranges", ranges)), @@ -439,7 +444,7 @@ impl Guest for Commands { async fn cluster_failover( connection: &Connection, mode: Option, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange(connection, ("CLUSTER", "FAILOVER", mode)).await } @@ -448,7 +453,7 @@ impl Guest for Commands { #[doc = " CLUSTER FLUSHSLOTS"] #[doc = " https://valkey.io/commands/cluster-flushslots/"] #[allow(async_fn_in_trait)] - async fn cluster_flushslots(connection: &Connection) -> Result<(), Error> { + async fn cluster_flushslots(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, ("CLUSTER", "FLUSHSLOTS")).await } @@ -457,7 +462,7 @@ impl Guest for Commands { #[doc = " CLUSTER FORGET node-id"] #[doc = " https://valkey.io/commands/cluster-forget/"] #[allow(async_fn_in_trait)] - async fn cluster_forget(connection: &Connection, node_id: String) -> Result<(), Error> { + async fn cluster_forget(connection: &Connection, node_id: String) -> Result<(), ErrorCode> { exchange(connection, ("CLUSTER", "FORGET", node_id)).await } @@ -468,7 +473,7 @@ impl Guest for Commands { #[allow(async_fn_in_trait)] async fn cluster_getslotmigrations( connection: &Connection, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("CLUSTER", "GETSLOTMIGRATIONS")).await } @@ -482,7 +487,7 @@ impl Guest for Commands { ip: String, port: u16, cluster_bus_port: Option, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange(connection, ("CLUSTER", "MEET", ip, port, cluster_bus_port)).await } @@ -494,7 +499,7 @@ impl Guest for Commands { async fn cluster_migrateslots( connection: &Connection, migrations: Vec, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange( connection, ( @@ -514,7 +519,7 @@ impl Guest for Commands { async fn cluster_replicas( connection: &Connection, node_id: String, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("CLUSTER", "REPLICAS", node_id)).await } @@ -528,7 +533,7 @@ impl Guest for Commands { async fn cluster_replicate( connection: &Connection, node_id: Option, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { // NO ONE detaches the replica from its primary let node = match node_id { Some(node_id) => vec![node_id], @@ -545,7 +550,7 @@ impl Guest for Commands { async fn cluster_reset( connection: &Connection, mode: Option, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange(connection, ("CLUSTER", "RESET", mode)).await } @@ -554,7 +559,7 @@ impl Guest for Commands { #[doc = " CLUSTER SAVECONFIG"] #[doc = " https://valkey.io/commands/cluster-saveconfig/"] #[allow(async_fn_in_trait)] - async fn cluster_saveconfig(connection: &Connection) -> Result<(), Error> { + async fn cluster_saveconfig(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, ("CLUSTER", "SAVECONFIG")).await } @@ -566,7 +571,7 @@ impl Guest for Commands { async fn cluster_set_config_epoch( connection: &Connection, config_epoch: Integer, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange(connection, ("CLUSTER", "SET-CONFIG-EPOCH", config_epoch)).await } @@ -580,7 +585,7 @@ impl Guest for Commands { slot: u16, state: ClusterSetslotState, timeout: Option, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange( connection, ( @@ -603,7 +608,7 @@ impl Guest for Commands { connection: &Connection, count: Integer, type_: CommandlogType, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("COMMANDLOG", "GET", count, type_)).await } @@ -615,7 +620,7 @@ impl Guest for Commands { async fn commandlog_len( connection: &Connection, type_: CommandlogType, - ) -> Result { + ) -> Result { exchange(connection, ("COMMANDLOG", "LEN", type_)).await } @@ -624,7 +629,10 @@ impl Guest for Commands { #[doc = " COMMANDLOG RESET 〈 slow | large-request | large-reply 〉"] #[doc = " https://valkey.io/commands/commandlog-reset/"] #[allow(async_fn_in_trait)] - async fn commandlog_reset(connection: &Connection, type_: CommandlogType) -> Result<(), Error> { + async fn commandlog_reset( + connection: &Connection, + type_: CommandlogType, + ) -> Result<(), ErrorCode> { exchange(connection, ("COMMANDLOG", "RESET", type_)).await } @@ -636,7 +644,7 @@ impl Guest for Commands { async fn config_get( connection: &Connection, parameters: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ("CONFIG", "GET", NonEmpty::new("parameters", parameters)), @@ -649,7 +657,7 @@ impl Guest for Commands { #[doc = " CONFIG RESETSTAT"] #[doc = " https://valkey.io/commands/config-resetstat/"] #[allow(async_fn_in_trait)] - async fn config_resetstat(connection: &Connection) -> Result<(), Error> { + async fn config_resetstat(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, ("CONFIG", "RESETSTAT")).await } @@ -658,7 +666,7 @@ impl Guest for Commands { #[doc = " CONFIG REWRITE"] #[doc = " https://valkey.io/commands/config-rewrite/"] #[allow(async_fn_in_trait)] - async fn config_rewrite(connection: &Connection) -> Result<(), Error> { + async fn config_rewrite(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, ("CONFIG", "REWRITE")).await } @@ -670,7 +678,7 @@ impl Guest for Commands { async fn config_set( connection: &Connection, parameters_and_values: Vec<(String, String)>, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { let parameters_and_values = NonEmpty::new("parameters-and-values", parameters_and_values); exchange(connection, ("CONFIG", "SET", parameters_and_values)).await } @@ -685,7 +693,7 @@ impl Guest for Commands { target: Option, abort: bool, timeout: Option, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { let command = ( "FAILOVER", target, @@ -700,7 +708,7 @@ impl Guest for Commands { #[doc = " LASTSAVE"] #[doc = " https://valkey.io/commands/lastsave/"] #[allow(async_fn_in_trait)] - async fn lastsave(connection: &Connection) -> Result { + async fn lastsave(connection: &Connection) -> Result { exchange(connection, "LASTSAVE").await } @@ -709,7 +717,7 @@ impl Guest for Commands { #[doc = " LATENCY DOCTOR"] #[doc = " https://valkey.io/commands/latency-doctor/"] #[allow(async_fn_in_trait)] - async fn latency_doctor(connection: &Connection) -> Result { + async fn latency_doctor(connection: &Connection) -> Result { exchange(connection, ("LATENCY", "DOCTOR")).await } @@ -718,7 +726,7 @@ impl Guest for Commands { #[doc = " LATENCY GRAPH event"] #[doc = " https://valkey.io/commands/latency-graph/"] #[allow(async_fn_in_trait)] - async fn latency_graph(connection: &Connection, event: String) -> Result { + async fn latency_graph(connection: &Connection, event: String) -> Result { exchange(connection, ("LATENCY", "GRAPH", event)).await } @@ -730,7 +738,7 @@ impl Guest for Commands { async fn latency_histogram( connection: &Connection, commands: Option>, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ( @@ -750,7 +758,7 @@ impl Guest for Commands { async fn latency_history( connection: &Connection, event: String, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("LATENCY", "HISTORY", event)).await } @@ -759,7 +767,7 @@ impl Guest for Commands { #[doc = " LATENCY LATEST"] #[doc = " https://valkey.io/commands/latency-latest/"] #[allow(async_fn_in_trait)] - async fn latency_latest(connection: &Connection) -> Result, Error> { + async fn latency_latest(connection: &Connection) -> Result, ErrorCode> { exchange(connection, ("LATENCY", "LATEST")).await } @@ -771,7 +779,7 @@ impl Guest for Commands { async fn latency_reset( connection: &Connection, events: Option>, - ) -> Result { + ) -> Result { exchange( connection, ("LATENCY", "RESET", NonEmpty::optional("events", events)), @@ -784,7 +792,7 @@ impl Guest for Commands { #[doc = " MODULE LIST"] #[doc = " https://valkey.io/commands/module-list/"] #[allow(async_fn_in_trait)] - async fn module_list(connection: &Connection) -> Result, Error> { + async fn module_list(connection: &Connection) -> Result, ErrorCode> { exchange(connection, ("MODULE", "LIST")).await } @@ -797,7 +805,7 @@ impl Guest for Commands { connection: &Connection, path: String, args: Option>, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange( connection, ("MODULE", "LOAD", path, NonEmpty::optional("args", args)), @@ -815,7 +823,7 @@ impl Guest for Commands { path: String, configs: Option>, args: Option>, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { // CONFIG before each name and value, ARGS once before the arguments let configs = configs.map(|configs| { let configs = configs @@ -833,7 +841,7 @@ impl Guest for Commands { #[doc = " MODULE UNLOAD name"] #[doc = " https://valkey.io/commands/module-unload/"] #[allow(async_fn_in_trait)] - async fn module_unload(connection: &Connection, name: String) -> Result<(), Error> { + async fn module_unload(connection: &Connection, name: String) -> Result<(), ErrorCode> { exchange(connection, ("MODULE", "UNLOAD", name)).await } @@ -847,7 +855,7 @@ impl Guest for Commands { async fn replicaof( connection: &Connection, primary: Option<(String, u16)>, - ) -> Result { + ) -> Result { // NO ONE makes the server a primary let primary = match primary { Some((host, port)) => vec![host, port.to_string()], @@ -861,7 +869,7 @@ impl Guest for Commands { #[doc = " ROLE"] #[doc = " https://valkey.io/commands/role/"] #[allow(async_fn_in_trait)] - async fn role(connection: &Connection) -> Result { + async fn role(connection: &Connection) -> Result { exchange(connection, "ROLE").await } @@ -870,7 +878,7 @@ impl Guest for Commands { #[doc = " SAVE"] #[doc = " https://valkey.io/commands/save/"] #[allow(async_fn_in_trait)] - async fn save(connection: &Connection) -> Result<(), Error> { + async fn save(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, "SAVE").await } @@ -888,7 +896,7 @@ impl Guest for Commands { connection: &Connection, save: Option, modifiers: ShutdownFlags, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { let command = ( "SHUTDOWN", save, @@ -909,7 +917,7 @@ impl Guest for Commands { #[doc = " SHUTDOWN ABORT"] #[doc = " https://valkey.io/commands/shutdown/"] #[allow(async_fn_in_trait)] - async fn shutdown_abort(connection: &Connection) -> Result<(), Error> { + async fn shutdown_abort(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, ("SHUTDOWN", "ABORT")).await } @@ -921,7 +929,7 @@ impl Guest for Commands { async fn slowlog_get( connection: &Connection, count: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("SLOWLOG", "GET", count)).await } @@ -930,7 +938,7 @@ impl Guest for Commands { #[doc = " SLOWLOG LEN"] #[doc = " https://valkey.io/commands/slowlog-len/"] #[allow(async_fn_in_trait)] - async fn slowlog_len(connection: &Connection) -> Result { + async fn slowlog_len(connection: &Connection) -> Result { exchange(connection, ("SLOWLOG", "LEN")).await } @@ -939,7 +947,7 @@ impl Guest for Commands { #[doc = " SLOWLOG RESET"] #[doc = " https://valkey.io/commands/slowlog-reset/"] #[allow(async_fn_in_trait)] - async fn slowlog_reset(connection: &Connection) -> Result<(), Error> { + async fn slowlog_reset(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, ("SLOWLOG", "RESET")).await } @@ -952,7 +960,7 @@ impl Guest for Commands { connection: &Connection, key: String, range: Option, - ) -> Result { + ) -> Result { exchange(connection, ("BITCOUNT", key, range)).await } @@ -967,7 +975,7 @@ impl Guest for Commands { connection: &Connection, key: String, operations: Option>, - ) -> Result>, Error> { + ) -> Result>, ErrorCode> { exchange( connection, ( @@ -988,7 +996,7 @@ impl Guest for Commands { connection: &Connection, key: String, gets: Option>, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ("BITFIELD_RO", key, NonEmpty::optional("gets", gets)), @@ -1005,7 +1013,7 @@ impl Guest for Commands { connection: &Connection, destination: String, operation: BitopOperation, - ) -> Result { + ) -> Result { // the operation and its keys are either side of the destination let (operation, keys) = match operation { BitopOperation::And(keys) => ("AND", keys), @@ -1032,7 +1040,7 @@ impl Guest for Commands { key: String, bit: bool, range: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { // -1 when there is no such bit let position: OrNever = exchange(connection, ("BITPOS", key, Bit(bit), range)).await?; @@ -1044,7 +1052,7 @@ impl Guest for Commands { #[doc = " GETBIT key offset"] #[doc = " https://valkey.io/commands/getbit/"] #[allow(async_fn_in_trait)] - async fn getbit(connection: &Connection, key: String, offset: u64) -> Result { + async fn getbit(connection: &Connection, key: String, offset: u64) -> Result { exchange(connection, ("GETBIT", key, offset)).await } @@ -1058,7 +1066,7 @@ impl Guest for Commands { key: String, offset: u64, value: bool, - ) -> Result { + ) -> Result { exchange(connection, ("SETBIT", key, offset, Bit(value))).await } @@ -1071,7 +1079,7 @@ impl Guest for Commands { connection: &Connection, username: Option, password: String, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange(connection, ("AUTH", username, password)).await } @@ -1080,12 +1088,15 @@ impl Guest for Commands { #[doc = " PING [ message ]"] #[doc = " https://valkey.io/commands/ping/"] #[allow(async_fn_in_trait)] - async fn ping(connection: &Connection, message: Option) -> Result<(), Error> { + async fn ping( + connection: &Connection, + message: Option, + ) -> Result<(), ErrorCode> { let reply: StringOrBytes = exchange(connection, ("PING", message.clone())).await?; if reply == message.unwrap_or_else(|| "PONG".into()) { Ok(()) } else { - Err(Error::Client(format!("unexpected reply: {reply:?}"))) + Err(ErrorCode::Client(format!("unexpected reply: {reply:?}"))) } } @@ -1097,7 +1108,7 @@ impl Guest for Commands { #[doc = " ASKING"] #[doc = " https://valkey.io/commands/asking/"] #[allow(async_fn_in_trait)] - async fn asking(connection: &Connection) -> Result<(), Error> { + async fn asking(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, "ASKING").await } @@ -1109,7 +1120,7 @@ impl Guest for Commands { #[doc = " CLIENT CACHING 〈 YES | NO 〉"] #[doc = " https://valkey.io/commands/client-caching/"] #[allow(async_fn_in_trait)] - async fn client_caching(connection: &Connection, enabled: bool) -> Result<(), Error> { + async fn client_caching(connection: &Connection, enabled: bool) -> Result<(), ErrorCode> { exchange( connection, ("CLIENT", "CACHING", if enabled { "YES" } else { "NO" }), @@ -1122,7 +1133,10 @@ impl Guest for Commands { #[doc = " CLIENT CAPA capability [ capability ... ]"] #[doc = " https://valkey.io/commands/client-capa/"] #[allow(async_fn_in_trait)] - async fn client_capa(connection: &Connection, capabilities: Vec) -> Result<(), Error> { + async fn client_capa( + connection: &Connection, + capabilities: Vec, + ) -> Result<(), ErrorCode> { exchange( connection, ( @@ -1139,7 +1153,7 @@ impl Guest for Commands { #[doc = " CLIENT GETNAME"] #[doc = " https://valkey.io/commands/client-getname/"] #[allow(async_fn_in_trait)] - async fn client_getname(connection: &Connection) -> Result, Error> { + async fn client_getname(connection: &Connection) -> Result, ErrorCode> { exchange(connection, ("CLIENT", "GETNAME")).await } @@ -1148,7 +1162,7 @@ impl Guest for Commands { #[doc = " CLIENT ID"] #[doc = " https://valkey.io/commands/client-id/"] #[allow(async_fn_in_trait)] - async fn client_id(connection: &Connection) -> Result { + async fn client_id(connection: &Connection) -> Result { exchange(connection, ("CLIENT", "ID")).await } @@ -1157,7 +1171,7 @@ impl Guest for Commands { #[doc = " CLIENT IMPORT-SOURCE 〈 ON | OFF 〉"] #[doc = " https://valkey.io/commands/client-import-source/"] #[allow(async_fn_in_trait)] - async fn client_import_source(connection: &Connection, enabled: bool) -> Result<(), Error> { + async fn client_import_source(connection: &Connection, enabled: bool) -> Result<(), ErrorCode> { exchange(connection, ("CLIENT", "IMPORT-SOURCE", OnOff(enabled))).await } @@ -1168,14 +1182,14 @@ impl Guest for Commands { #[doc = " CLIENT INFO"] #[doc = " https://valkey.io/commands/client-info/"] #[allow(async_fn_in_trait)] - async fn client_info(connection: &Connection) -> Result, Error> { + async fn client_info(connection: &Connection) -> Result, ErrorCode> { // a line of CLIENT LIST let clients: ClientList = exchange(connection, ("CLIENT", "INFO")).await?; clients .0 .into_iter() .next() - .ok_or_else(|| Error::Client("unexpected empty reply".to_string())) + .ok_or_else(|| ErrorCode::Client("unexpected empty reply".to_string())) } #[doc = " Controls whether commands sent by the client affect the LRU/LFU of accessed keys."] @@ -1185,7 +1199,7 @@ impl Guest for Commands { #[doc = " CLIENT NO-TOUCH 〈 ON | OFF 〉"] #[doc = " https://valkey.io/commands/client-no-touch/"] #[allow(async_fn_in_trait)] - async fn client_no_touch(connection: &Connection, enabled: bool) -> Result<(), Error> { + async fn client_no_touch(connection: &Connection, enabled: bool) -> Result<(), ErrorCode> { exchange(connection, ("CLIENT", "NO-TOUCH", OnOff(enabled))).await } @@ -1194,7 +1208,10 @@ impl Guest for Commands { #[doc = " CLIENT SETINFO 〈 LIB-NAME libname | LIB-VER libver 〉"] #[doc = " https://valkey.io/commands/client-setinfo/"] #[allow(async_fn_in_trait)] - async fn client_setinfo(connection: &Connection, attr: ClientSetinfoAttr) -> Result<(), Error> { + async fn client_setinfo( + connection: &Connection, + attr: ClientSetinfoAttr, + ) -> Result<(), ErrorCode> { exchange(connection, ("CLIENT", "SETINFO", attr)).await } @@ -1203,7 +1220,10 @@ impl Guest for Commands { #[doc = " CLIENT SETNAME connection-name"] #[doc = " https://valkey.io/commands/client-setname/"] #[allow(async_fn_in_trait)] - async fn client_setname(connection: &Connection, connection_name: String) -> Result<(), Error> { + async fn client_setname( + connection: &Connection, + connection_name: String, + ) -> Result<(), ErrorCode> { exchange(connection, ("CLIENT", "SETNAME", connection_name)).await } @@ -1218,7 +1238,7 @@ impl Guest for Commands { async fn client_tracking( connection: &Connection, options: Option, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { // ON with options, OFF without let status = OnOff(options.is_some()); exchange(connection, ("CLIENT", "TRACKING", status, options)).await @@ -1231,7 +1251,7 @@ impl Guest for Commands { #[allow(async_fn_in_trait)] async fn client_trackinginfo( connection: &Connection, - ) -> Result { + ) -> Result { exchange(connection, ("CLIENT", "TRACKINGINFO")).await } @@ -1240,7 +1260,7 @@ impl Guest for Commands { #[doc = " COMMAND COUNT"] #[doc = " https://valkey.io/commands/command-count/"] #[allow(async_fn_in_trait)] - async fn command_count(connection: &Connection) -> Result { + async fn command_count(connection: &Connection) -> Result { exchange(connection, ("COMMAND", "COUNT")).await } @@ -1249,7 +1269,7 @@ impl Guest for Commands { #[doc = " COMMAND"] #[doc = " https://valkey.io/commands/command/"] #[allow(async_fn_in_trait)] - async fn command(connection: &Connection) -> Result, Error> { + async fn command(connection: &Connection) -> Result, ErrorCode> { let commands: CommandInfoList = exchange(connection, "COMMAND").await?; Ok(commands.0) } @@ -1264,7 +1284,7 @@ impl Guest for Commands { async fn command_docs( connection: &Connection, command_names: Option>, - ) -> Result, Error> { + ) -> Result, ErrorCode> { let command_names = NonEmpty::optional("command-names", command_names); let commands: CommandDocsList = exchange(connection, ("COMMAND", "DOCS", command_names)).await?; @@ -1281,7 +1301,7 @@ impl Guest for Commands { async fn command_info( connection: &Connection, command_names: Option>, - ) -> Result, Error> { + ) -> Result, ErrorCode> { let command_names = NonEmpty::optional("command-names", command_names); let commands: CommandInfoList = exchange(connection, ("COMMAND", "INFO", command_names)).await?; @@ -1296,7 +1316,7 @@ impl Guest for Commands { async fn command_getkeys( connection: &Connection, command: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ("COMMAND", "GETKEYS", NonEmpty::new("command", command)), @@ -1312,7 +1332,7 @@ impl Guest for Commands { async fn command_getkeysandflags( connection: &Connection, command: Vec, - ) -> Result)>, Error> { + ) -> Result)>, ErrorCode> { exchange( connection, ( @@ -1332,7 +1352,7 @@ impl Guest for Commands { async fn command_list( connection: &Connection, filter: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("COMMAND", "LIST", filter)).await } @@ -1341,7 +1361,10 @@ impl Guest for Commands { #[doc = " ECHO message"] #[doc = " https://valkey.io/commands/echo/"] #[allow(async_fn_in_trait)] - async fn echo(connection: &Connection, message: StringOrBytes) -> Result { + async fn echo( + connection: &Connection, + message: StringOrBytes, + ) -> Result { exchange(connection, ("ECHO", message)).await } @@ -1352,7 +1375,7 @@ impl Guest for Commands { #[doc = " READONLY"] #[doc = " https://valkey.io/commands/readonly/"] #[allow(async_fn_in_trait)] - async fn readonly(connection: &Connection) -> Result<(), Error> { + async fn readonly(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, "READONLY").await } @@ -1363,7 +1386,7 @@ impl Guest for Commands { #[doc = " READWRITE"] #[doc = " https://valkey.io/commands/readwrite/"] #[allow(async_fn_in_trait)] - async fn readwrite(connection: &Connection) -> Result<(), Error> { + async fn readwrite(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, "READWRITE").await } @@ -1374,7 +1397,7 @@ impl Guest for Commands { #[doc = " SELECT index"] #[doc = " https://valkey.io/commands/select/"] #[allow(async_fn_in_trait)] - async fn select(connection: &Connection, index: Integer) -> Result<(), Error> { + async fn select(connection: &Connection, index: Integer) -> Result<(), ErrorCode> { exchange(connection, ("SELECT", index)).await } @@ -1390,7 +1413,7 @@ impl Guest for Commands { connection: &Connection, numreplicas: Integer, timeout: DurationMilliseconds, - ) -> Result { + ) -> Result { exchange(connection, ("WAIT", numreplicas, timeout)).await } @@ -1407,7 +1430,7 @@ impl Guest for Commands { numlocal: Integer, numreplicas: Integer, timeout: DurationMilliseconds, - ) -> Result<(Integer, Integer), Error> { + ) -> Result<(Integer, Integer), ErrorCode> { exchange(connection, ("WAITAOF", numlocal, numreplicas, timeout)).await } @@ -1422,7 +1445,7 @@ impl Guest for Commands { condition: Option, changed: bool, coordinates_and_members: Vec<(GeoCoordinates, StringOrBytes)>, - ) -> Result { + ) -> Result { exchange( connection, ( @@ -1447,7 +1470,7 @@ impl Guest for Commands { member1: StringOrBytes, member2: StringOrBytes, unit: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("GEODIST", key, member1, member2, unit)).await } @@ -1460,7 +1483,7 @@ impl Guest for Commands { connection: &Connection, key: String, members: Option>, - ) -> Result>, Error> { + ) -> Result>, ErrorCode> { exchange( connection, ("GEOHASH", key, NonEmpty::optional("members", members)), @@ -1477,7 +1500,7 @@ impl Guest for Commands { connection: &Connection, key: String, members: Option>, - ) -> Result>, Error> { + ) -> Result>, ErrorCode> { exchange( connection, ("GEOPOS", key, NonEmpty::optional("members", members)), @@ -1498,7 +1521,7 @@ impl Guest for Commands { shape: GeosearchShape, order: Option, count: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("GEOSEARCH", key, shape, order, count)).await } @@ -1518,7 +1541,7 @@ impl Guest for Commands { order: Option, count: Option, details: GeosearchWithFlags, - ) -> Result, Error> { + ) -> Result, ErrorCode> { let command = ( "GEOSEARCH", key, @@ -1545,7 +1568,7 @@ impl Guest for Commands { order: Option, count: Option, storedist: bool, - ) -> Result { + ) -> Result { exchange( connection, ( @@ -1570,7 +1593,7 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result { + ) -> Result { exchange(connection, ("HDEL", key, NonEmpty::new("fields", fields))).await } @@ -1579,7 +1602,11 @@ impl Guest for Commands { #[doc = " HEXISTS key field"] #[doc = " https://valkey.io/commands/hexists/"] #[allow(async_fn_in_trait)] - async fn hexists(connection: &Connection, key: String, field: String) -> Result { + async fn hexists( + connection: &Connection, + key: String, + field: String, + ) -> Result { exchange(connection, ("HEXISTS", key, field)).await } @@ -1592,7 +1619,7 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ("HEXPIRETIME", key, Fields(NonEmpty::new("fields", fields))), @@ -1609,7 +1636,7 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result>, Error> { + ) -> Result>, ErrorCode> { exchange( connection, ("HGETDEL", key, Fields(NonEmpty::new("fields", fields))), @@ -1627,7 +1654,7 @@ impl Guest for Commands { key: String, ttl: Option, fields: Vec, - ) -> Result>, Error> { + ) -> Result>, ErrorCode> { exchange( connection, ("HGETEX", key, ttl, Fields(NonEmpty::new("fields", fields))), @@ -1644,7 +1671,7 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ("HPERSIST", key, Fields(NonEmpty::new("fields", fields))), @@ -1663,7 +1690,7 @@ impl Guest for Commands { milliseconds: DurationMilliseconds, mode: Option, fields: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { let command = ( "HPEXPIRE", key, @@ -1685,7 +1712,7 @@ impl Guest for Commands { milliseconds: UnixTimeMilliseconds, mode: Option, fields: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { let command = ( "HPEXPIREAT", key, @@ -1705,7 +1732,7 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ("HPEXPIRETIME", key, Fields(NonEmpty::new("fields", fields))), @@ -1722,7 +1749,7 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ("HPTTL", key, Fields(NonEmpty::new("fields", fields))), @@ -1742,7 +1769,7 @@ impl Guest for Commands { field_condition: Option, ttl: Option, field_and_values: Vec<(String, StringOrBytes)>, - ) -> Result { + ) -> Result { let command = ( "HSETEX", key, @@ -1763,7 +1790,7 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ("HTTL", key, Fields(NonEmpty::new("fields", fields))), @@ -1782,7 +1809,7 @@ impl Guest for Commands { seconds: DurationSeconds, mode: Option, fields: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ( @@ -1807,7 +1834,7 @@ impl Guest for Commands { seconds: UnixTimeSeconds, mode: Option, fields: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ( @@ -1830,7 +1857,7 @@ impl Guest for Commands { connection: &Connection, key: String, field: String, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("HGET", key, field)).await } @@ -1842,7 +1869,7 @@ impl Guest for Commands { async fn hgetall( connection: &Connection, key: String, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("HGETALL", key)).await } @@ -1856,7 +1883,7 @@ impl Guest for Commands { key: String, field: String, increment: Integer, - ) -> Result { + ) -> Result { exchange(connection, ("HINCRBY", key, field, increment)).await } @@ -1870,7 +1897,7 @@ impl Guest for Commands { key: String, field: String, increment: Double, - ) -> Result { + ) -> Result { exchange(connection, ("HINCRBYFLOAT", key, field, increment)).await } @@ -1879,7 +1906,7 @@ impl Guest for Commands { #[doc = " HKEYS key"] #[doc = " https://valkey.io/commands/hkeys/"] #[allow(async_fn_in_trait)] - async fn hkeys(connection: &Connection, key: String) -> Result, Error> { + async fn hkeys(connection: &Connection, key: String) -> Result, ErrorCode> { exchange(connection, ("HKEYS", key)).await } @@ -1888,7 +1915,7 @@ impl Guest for Commands { #[doc = " HLEN key"] #[doc = " https://valkey.io/commands/hlen/"] #[allow(async_fn_in_trait)] - async fn hlen(connection: &Connection, key: String) -> Result { + async fn hlen(connection: &Connection, key: String) -> Result { exchange(connection, ("HLEN", key)).await } @@ -1901,7 +1928,7 @@ impl Guest for Commands { connection: &Connection, key: String, fields: Vec, - ) -> Result>, Error> { + ) -> Result>, ErrorCode> { exchange(connection, ("HMGET", key, NonEmpty::new("fields", fields))).await } @@ -1914,7 +1941,7 @@ impl Guest for Commands { connection: &Connection, key: String, field_and_values: Vec<(String, StringOrBytes)>, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange( connection, ( @@ -1939,7 +1966,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { if count.is_some() { return exchange(connection, ("HRANDFIELD", key, count)).await; } @@ -1961,7 +1988,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Integer, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("HRANDFIELD", key, count, "WITHVALUES")).await } @@ -1978,7 +2005,7 @@ impl Guest for Commands { cursor: Cursor, match_: Option, count: Option, - ) -> Result<(Option, Map), Error> { + ) -> Result<(Option, Map), ErrorCode> { let (cursor, items): (NextCursor, Map) = exchange( connection, ( @@ -2008,7 +2035,7 @@ impl Guest for Commands { cursor: Cursor, match_: Option, count: Option, - ) -> Result<(Option, Vec), Error> { + ) -> Result<(Option, Vec), ErrorCode> { let (cursor, items): (NextCursor, Vec) = exchange( connection, ( @@ -2033,7 +2060,7 @@ impl Guest for Commands { connection: &Connection, key: String, field_and_values: Vec<(String, StringOrBytes)>, - ) -> Result { + ) -> Result { exchange( connection, ( @@ -2055,7 +2082,7 @@ impl Guest for Commands { key: String, field: String, value: StringOrBytes, - ) -> Result { + ) -> Result { exchange(connection, ("HSETNX", key, field, value)).await } @@ -2068,7 +2095,7 @@ impl Guest for Commands { connection: &Connection, key: String, field: String, - ) -> Result { + ) -> Result { exchange(connection, ("HSTRLEN", key, field)).await } @@ -2077,7 +2104,7 @@ impl Guest for Commands { #[doc = " HVALS key"] #[doc = " https://valkey.io/commands/hvals/"] #[allow(async_fn_in_trait)] - async fn hvals(connection: &Connection, key: String) -> Result, Error> { + async fn hvals(connection: &Connection, key: String) -> Result, ErrorCode> { exchange(connection, ("HVALS", key)).await } @@ -2090,7 +2117,7 @@ impl Guest for Commands { connection: &Connection, key: String, elements: Option>, - ) -> Result { + ) -> Result { exchange( connection, ("PFADD", key, NonEmpty::optional("elements", elements)), @@ -2103,7 +2130,7 @@ impl Guest for Commands { #[doc = " PFCOUNT key [ key ... ]"] #[doc = " https://valkey.io/commands/pfcount/"] #[allow(async_fn_in_trait)] - async fn pfcount(connection: &Connection, keys: Vec) -> Result { + async fn pfcount(connection: &Connection, keys: Vec) -> Result { exchange(connection, ("PFCOUNT", NonEmpty::new("keys", keys))).await } @@ -2116,7 +2143,7 @@ impl Guest for Commands { connection: &Connection, destination: String, sources: Option>, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange( connection, ( @@ -2143,7 +2170,7 @@ impl Guest for Commands { wherefrom: ListSide, whereto: ListSide, timeout: DurationMilliseconds, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ( @@ -2172,7 +2199,7 @@ impl Guest for Commands { keys: Vec, end: ListSide, count: Option, - ) -> Result)>, Error> { + ) -> Result)>, ErrorCode> { exchange( connection, ( @@ -2198,7 +2225,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, timeout: DurationMilliseconds, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ( @@ -2222,7 +2249,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, timeout: DurationMilliseconds, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ( @@ -2247,7 +2274,7 @@ impl Guest for Commands { source: String, destination: String, timeout: DurationMilliseconds, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ("BRPOPLPUSH", source, destination, DecimalSeconds(timeout)), @@ -2264,7 +2291,7 @@ impl Guest for Commands { connection: &Connection, key: String, index: Integer, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("LINDEX", key, index)).await } @@ -2279,7 +2306,7 @@ impl Guest for Commands { position: LinsertPosition, pivot: StringOrBytes, element: StringOrBytes, - ) -> Result { + ) -> Result { exchange(connection, ("LINSERT", key, position, pivot, element)).await } @@ -2288,7 +2315,7 @@ impl Guest for Commands { #[doc = " LLEN key"] #[doc = " https://valkey.io/commands/llen/"] #[allow(async_fn_in_trait)] - async fn llen(connection: &Connection, key: String) -> Result { + async fn llen(connection: &Connection, key: String) -> Result { exchange(connection, ("LLEN", key)).await } @@ -2303,7 +2330,7 @@ impl Guest for Commands { destination: String, wherefrom: ListSide, whereto: ListSide, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ("LMOVE", source, destination, wherefrom, whereto), @@ -2321,7 +2348,7 @@ impl Guest for Commands { keys: Vec, end: ListSide, count: Option, - ) -> Result)>, Error> { + ) -> Result)>, ErrorCode> { let command = ( "LMPOP", NumKeys(NonEmpty::new("keys", keys)), @@ -2342,7 +2369,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { match count { Some(count) => { // null when the key does not exist @@ -2372,7 +2399,7 @@ impl Guest for Commands { rank: Option, count: Option, maxlen: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { let command = ( "LPOS", key, @@ -2401,7 +2428,7 @@ impl Guest for Commands { connection: &Connection, key: String, elements: Vec, - ) -> Result { + ) -> Result { exchange( connection, ("LPUSH", key, NonEmpty::new("elements", elements)), @@ -2418,7 +2445,7 @@ impl Guest for Commands { connection: &Connection, key: String, elements: Vec, - ) -> Result { + ) -> Result { exchange( connection, ("LPUSHX", key, NonEmpty::new("elements", elements)), @@ -2436,7 +2463,7 @@ impl Guest for Commands { key: String, start: Integer, stop: Integer, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("LRANGE", key, start, stop)).await } @@ -2450,7 +2477,7 @@ impl Guest for Commands { key: String, count: Integer, element: StringOrBytes, - ) -> Result { + ) -> Result { exchange(connection, ("LREM", key, count, element)).await } @@ -2464,7 +2491,7 @@ impl Guest for Commands { key: String, index: Integer, element: StringOrBytes, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange(connection, ("LSET", key, index, element)).await } @@ -2478,7 +2505,7 @@ impl Guest for Commands { key: String, start: Integer, stop: Integer, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange(connection, ("LTRIM", key, start, stop)).await } @@ -2493,7 +2520,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { match count { Some(count) => { // null when the key does not exist @@ -2518,7 +2545,7 @@ impl Guest for Commands { connection: &Connection, source: String, destination: String, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("RPOPLPUSH", source, destination)).await } @@ -2531,7 +2558,7 @@ impl Guest for Commands { connection: &Connection, key: String, elements: Vec, - ) -> Result { + ) -> Result { exchange( connection, ("RPUSH", key, NonEmpty::new("elements", elements)), @@ -2548,7 +2575,7 @@ impl Guest for Commands { connection: &Connection, key: String, elements: Vec, - ) -> Result { + ) -> Result { exchange( connection, ("RPUSHX", key, NonEmpty::new("elements", elements)), @@ -2561,7 +2588,7 @@ impl Guest for Commands { #[doc = " DEL key [ key ... ]"] #[doc = " https://valkey.io/commands/del/"] #[allow(async_fn_in_trait)] - async fn del(connection: &Connection, keys: Vec) -> Result { + async fn del(connection: &Connection, keys: Vec) -> Result { exchange(connection, ("DEL", NonEmpty::new("keys", keys))).await } @@ -2570,7 +2597,7 @@ impl Guest for Commands { #[doc = " DUMP key"] #[doc = " https://valkey.io/commands/dump/"] #[allow(async_fn_in_trait)] - async fn dump(connection: &Connection, key: String) -> Result, Error> { + async fn dump(connection: &Connection, key: String) -> Result, ErrorCode> { let serialized: Option = exchange(connection, ("DUMP", key)).await?; Ok(serialized.map(|serialized| serialized.0)) } @@ -2580,7 +2607,7 @@ impl Guest for Commands { #[doc = " EXISTS key [ key ... ]"] #[doc = " https://valkey.io/commands/exists/"] #[allow(async_fn_in_trait)] - async fn exists(connection: &Connection, keys: Vec) -> Result { + async fn exists(connection: &Connection, keys: Vec) -> Result { exchange(connection, ("EXISTS", NonEmpty::new("keys", keys))).await } @@ -2594,7 +2621,7 @@ impl Guest for Commands { key: String, seconds: DurationSeconds, mode: Option, - ) -> Result { + ) -> Result { exchange(connection, ("EXPIRE", key, seconds, mode)).await } @@ -2608,7 +2635,7 @@ impl Guest for Commands { key: String, seconds: UnixTimeSeconds, mode: Option, - ) -> Result { + ) -> Result { exchange(connection, ("EXPIREAT", key, seconds, mode)).await } @@ -2617,7 +2644,7 @@ impl Guest for Commands { #[doc = " KEYS pattern"] #[doc = " https://valkey.io/commands/keys/"] #[allow(async_fn_in_trait)] - async fn keys(connection: &Connection, pattern: String) -> Result, Error> { + async fn keys(connection: &Connection, pattern: String) -> Result, ErrorCode> { exchange(connection, ("KEYS", pattern)).await } @@ -2630,7 +2657,7 @@ impl Guest for Commands { connection: &Connection, slot: u16, mode: Option, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange(connection, ("CLUSTER", "FLUSHSLOT", slot, mode)).await } @@ -2648,7 +2675,7 @@ impl Guest for Commands { count: Option, type_: Option, slot: Option, - ) -> Result<(Option, Vec), Error> { + ) -> Result<(Option, Vec), ErrorCode> { let command = ( "CLUSTERSCAN", cursor, @@ -2672,7 +2699,7 @@ impl Guest for Commands { destination: String, destination_db: Option, replace: bool, - ) -> Result { + ) -> Result { exchange( connection, ( @@ -2691,7 +2718,7 @@ impl Guest for Commands { #[doc = " DBSIZE"] #[doc = " https://valkey.io/commands/dbsize/"] #[allow(async_fn_in_trait)] - async fn dbsize(connection: &Connection) -> Result { + async fn dbsize(connection: &Connection) -> Result { exchange(connection, "DBSIZE").await } @@ -2700,7 +2727,10 @@ impl Guest for Commands { #[doc = " EXPIRETIME key"] #[doc = " https://valkey.io/commands/expiretime/"] #[allow(async_fn_in_trait)] - async fn expiretime(connection: &Connection, key: String) -> Result { + async fn expiretime( + connection: &Connection, + key: String, + ) -> Result { exchange(connection, ("EXPIRETIME", key)).await } @@ -2709,7 +2739,7 @@ impl Guest for Commands { #[doc = " FLUSHALL [ ASYNC | SYNC ]"] #[doc = " https://valkey.io/commands/flushall/"] #[allow(async_fn_in_trait)] - async fn flushall(connection: &Connection, mode: Option) -> Result<(), Error> { + async fn flushall(connection: &Connection, mode: Option) -> Result<(), ErrorCode> { exchange(connection, ("FLUSHALL", mode)).await } @@ -2718,7 +2748,7 @@ impl Guest for Commands { #[doc = " FLUSHDB [ ASYNC | SYNC ]"] #[doc = " https://valkey.io/commands/flushdb/"] #[allow(async_fn_in_trait)] - async fn flushdb(connection: &Connection, mode: Option) -> Result<(), Error> { + async fn flushdb(connection: &Connection, mode: Option) -> Result<(), ErrorCode> { exchange(connection, ("FLUSHDB", mode)).await } @@ -2738,7 +2768,7 @@ impl Guest for Commands { options: MigrateFlags, auth: Option, keys: Vec, - ) -> Result { + ) -> Result { // "" in place of the key, the keys follow KEYS let command = ( "MIGRATE", @@ -2766,7 +2796,7 @@ impl Guest for Commands { key: String, db: Integer, replace: bool, - ) -> Result { + ) -> Result { exchange(connection, ("MOVE", key, db, Flag("REPLACE", replace))).await } @@ -2778,7 +2808,7 @@ impl Guest for Commands { async fn object_encoding( connection: &Connection, key: String, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("OBJECT", "ENCODING", key)).await } @@ -2787,7 +2817,10 @@ impl Guest for Commands { #[doc = " OBJECT FREQ key"] #[doc = " https://valkey.io/commands/object-freq/"] #[allow(async_fn_in_trait)] - async fn object_freq(connection: &Connection, key: String) -> Result, Error> { + async fn object_freq( + connection: &Connection, + key: String, + ) -> Result, ErrorCode> { exchange(connection, ("OBJECT", "FREQ", key)).await } @@ -2799,7 +2832,7 @@ impl Guest for Commands { async fn object_idletime( connection: &Connection, key: String, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("OBJECT", "IDLETIME", key)).await } @@ -2811,7 +2844,7 @@ impl Guest for Commands { async fn object_refcount( connection: &Connection, key: String, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("OBJECT", "REFCOUNT", key)).await } @@ -2820,7 +2853,7 @@ impl Guest for Commands { #[doc = " PERSIST key"] #[doc = " https://valkey.io/commands/persist/"] #[allow(async_fn_in_trait)] - async fn persist(connection: &Connection, key: String) -> Result { + async fn persist(connection: &Connection, key: String) -> Result { exchange(connection, ("PERSIST", key)).await } @@ -2834,7 +2867,7 @@ impl Guest for Commands { key: String, milliseconds: DurationMilliseconds, mode: Option, - ) -> Result { + ) -> Result { exchange(connection, ("PEXPIRE", key, milliseconds, mode)).await } @@ -2848,7 +2881,7 @@ impl Guest for Commands { key: String, milliseconds: UnixTimeMilliseconds, mode: Option, - ) -> Result { + ) -> Result { exchange(connection, ("PEXPIREAT", key, milliseconds, mode)).await } @@ -2857,7 +2890,10 @@ impl Guest for Commands { #[doc = " PEXPIRETIME key"] #[doc = " https://valkey.io/commands/pexpiretime/"] #[allow(async_fn_in_trait)] - async fn pexpiretime(connection: &Connection, key: String) -> Result { + async fn pexpiretime( + connection: &Connection, + key: String, + ) -> Result { exchange(connection, ("PEXPIRETIME", key)).await } @@ -2866,7 +2902,7 @@ impl Guest for Commands { #[doc = " PTTL key"] #[doc = " https://valkey.io/commands/pttl/"] #[allow(async_fn_in_trait)] - async fn pttl(connection: &Connection, key: String) -> Result { + async fn pttl(connection: &Connection, key: String) -> Result { exchange(connection, ("PTTL", key)).await } @@ -2875,7 +2911,7 @@ impl Guest for Commands { #[doc = " RANDOMKEY"] #[doc = " https://valkey.io/commands/randomkey/"] #[allow(async_fn_in_trait)] - async fn randomkey(connection: &Connection) -> Result, Error> { + async fn randomkey(connection: &Connection) -> Result, ErrorCode> { exchange(connection, "RANDOMKEY").await } @@ -2884,7 +2920,7 @@ impl Guest for Commands { #[doc = " RENAME key newkey"] #[doc = " https://valkey.io/commands/rename/"] #[allow(async_fn_in_trait)] - async fn rename(connection: &Connection, key: String, newkey: String) -> Result<(), Error> { + async fn rename(connection: &Connection, key: String, newkey: String) -> Result<(), ErrorCode> { exchange(connection, ("RENAME", key, newkey)).await } @@ -2893,7 +2929,11 @@ impl Guest for Commands { #[doc = " RENAMENX key newkey"] #[doc = " https://valkey.io/commands/renamenx/"] #[allow(async_fn_in_trait)] - async fn renamenx(connection: &Connection, key: String, newkey: String) -> Result { + async fn renamenx( + connection: &Connection, + key: String, + newkey: String, + ) -> Result { exchange(connection, ("RENAMENX", key, newkey)).await } @@ -2912,7 +2952,7 @@ impl Guest for Commands { replace: bool, idletime: Option, freq: Option, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { let (ttl, absttl) = restore_ttl(ttl); exchange( connection, @@ -2945,7 +2985,7 @@ impl Guest for Commands { replace: bool, idletime: Option, freq: Option, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { let (ttl, absttl) = restore_ttl(ttl); exchange( connection, @@ -2974,7 +3014,7 @@ impl Guest for Commands { match_: Option, count: Option, type_: Option, - ) -> Result<(Option, Vec), Error> { + ) -> Result<(Option, Vec), ErrorCode> { let command = ( "SCAN", cursor, @@ -2995,7 +3035,7 @@ impl Guest for Commands { connection: &Connection, index1: Integer, index2: Integer, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange(connection, ("SWAPDB", index1, index2)).await } @@ -3004,7 +3044,7 @@ impl Guest for Commands { #[doc = " TOUCH key [ key ... ]"] #[doc = " https://valkey.io/commands/touch/"] #[allow(async_fn_in_trait)] - async fn touch(connection: &Connection, keys: Vec) -> Result { + async fn touch(connection: &Connection, keys: Vec) -> Result { exchange(connection, ("TOUCH", NonEmpty::new("keys", keys))).await } @@ -3013,7 +3053,7 @@ impl Guest for Commands { #[doc = " TTL key"] #[doc = " https://valkey.io/commands/ttl/"] #[allow(async_fn_in_trait)] - async fn ttl(connection: &Connection, key: String) -> Result { + async fn ttl(connection: &Connection, key: String) -> Result { exchange(connection, ("TTL", key)).await } @@ -3024,7 +3064,7 @@ impl Guest for Commands { #[doc = " TYPE key"] #[doc = " https://valkey.io/commands/type/"] #[allow(async_fn_in_trait)] - async fn type_(connection: &Connection, key: String) -> Result, Error> { + async fn type_(connection: &Connection, key: String) -> Result, ErrorCode> { let key_type: OptionalKeyType = exchange(connection, ("TYPE", key)).await?; Ok(key_type.0) } @@ -3034,7 +3074,7 @@ impl Guest for Commands { #[doc = " UNLINK key [ key ... ]"] #[doc = " https://valkey.io/commands/unlink/"] #[allow(async_fn_in_trait)] - async fn unlink(connection: &Connection, keys: Vec) -> Result { + async fn unlink(connection: &Connection, keys: Vec) -> Result { exchange(connection, ("UNLINK", NonEmpty::new("keys", keys))).await } @@ -3047,7 +3087,7 @@ impl Guest for Commands { connection: &Connection, channel: String, message: StringOrBytes, - ) -> Result { + ) -> Result { exchange(connection, ("PUBLISH", channel, message)).await } @@ -3059,7 +3099,7 @@ impl Guest for Commands { async fn pubsub_channels( connection: &Connection, pattern: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("PUBSUB", "CHANNELS", pattern)).await } @@ -3068,7 +3108,7 @@ impl Guest for Commands { #[doc = " PUBSUB NUMPAT"] #[doc = " https://valkey.io/commands/pubsub-numpat/"] #[allow(async_fn_in_trait)] - async fn pubsub_numpat(connection: &Connection) -> Result { + async fn pubsub_numpat(connection: &Connection) -> Result { exchange(connection, ("PUBSUB", "NUMPAT")).await } @@ -3080,7 +3120,7 @@ impl Guest for Commands { async fn pubsub_numsub( connection: &Connection, channels: Option>, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ("PUBSUB", "NUMSUB", NonEmpty::optional("channels", channels)), @@ -3096,7 +3136,7 @@ impl Guest for Commands { async fn pubsub_shardchannels( connection: &Connection, pattern: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("PUBSUB", "SHARDCHANNELS", pattern)).await } @@ -3108,7 +3148,7 @@ impl Guest for Commands { async fn pubsub_shardnumsub( connection: &Connection, shardchannels: Option>, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ( @@ -3132,7 +3172,7 @@ impl Guest for Commands { script: String, keys: Option>, args: Option>, - ) -> Result { + ) -> Result { exchange(connection, ("EVAL", script, KeysAndArgs(keys, args))).await } @@ -3148,7 +3188,7 @@ impl Guest for Commands { script: String, keys: Option>, args: Option>, - ) -> Result { + ) -> Result { exchange(connection, ("EVAL_RO", script, KeysAndArgs(keys, args))).await } @@ -3164,7 +3204,7 @@ impl Guest for Commands { sha1: String, keys: Option>, args: Option>, - ) -> Result { + ) -> Result { exchange(connection, ("EVALSHA", sha1, KeysAndArgs(keys, args))).await } @@ -3180,7 +3220,7 @@ impl Guest for Commands { sha1: String, keys: Option>, args: Option>, - ) -> Result { + ) -> Result { exchange(connection, ("EVALSHA_RO", sha1, KeysAndArgs(keys, args))).await } @@ -3196,7 +3236,7 @@ impl Guest for Commands { function: String, keys: Option>, args: Option>, - ) -> Result { + ) -> Result { exchange(connection, ("FCALL", function, KeysAndArgs(keys, args))).await } @@ -3212,7 +3252,7 @@ impl Guest for Commands { function: String, keys: Option>, args: Option>, - ) -> Result { + ) -> Result { exchange(connection, ("FCALL_RO", function, KeysAndArgs(keys, args))).await } @@ -3221,7 +3261,10 @@ impl Guest for Commands { #[doc = " FUNCTION DELETE library-name"] #[doc = " https://valkey.io/commands/function-delete/"] #[allow(async_fn_in_trait)] - async fn function_delete(connection: &Connection, library_name: String) -> Result<(), Error> { + async fn function_delete( + connection: &Connection, + library_name: String, + ) -> Result<(), ErrorCode> { exchange(connection, ("FUNCTION", "DELETE", library_name)).await } @@ -3230,7 +3273,7 @@ impl Guest for Commands { #[doc = " FUNCTION DUMP"] #[doc = " https://valkey.io/commands/function-dump/"] #[allow(async_fn_in_trait)] - async fn function_dump(connection: &Connection) -> Result { + async fn function_dump(connection: &Connection) -> Result { let serialized: Binary = exchange(connection, ("FUNCTION", "DUMP")).await?; Ok(serialized.0) } @@ -3240,7 +3283,10 @@ impl Guest for Commands { #[doc = " FUNCTION FLUSH [ ASYNC | SYNC ]"] #[doc = " https://valkey.io/commands/function-flush/"] #[allow(async_fn_in_trait)] - async fn function_flush(connection: &Connection, mode: Option) -> Result<(), Error> { + async fn function_flush( + connection: &Connection, + mode: Option, + ) -> Result<(), ErrorCode> { exchange(connection, ("FUNCTION", "FLUSH", mode)).await } @@ -3249,7 +3295,7 @@ impl Guest for Commands { #[doc = " FUNCTION KILL"] #[doc = " https://valkey.io/commands/function-kill/"] #[allow(async_fn_in_trait)] - async fn function_kill(connection: &Connection) -> Result<(), Error> { + async fn function_kill(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, ("FUNCTION", "KILL")).await } @@ -3262,7 +3308,7 @@ impl Guest for Commands { connection: &Connection, library_name_pattern: Option, withcode: bool, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ( @@ -3284,7 +3330,7 @@ impl Guest for Commands { connection: &Connection, replace: bool, function_code: String, - ) -> Result { + ) -> Result { exchange( connection, ("FUNCTION", "LOAD", Flag("REPLACE", replace), function_code), @@ -3301,7 +3347,7 @@ impl Guest for Commands { connection: &Connection, serialized_value: Bytes, policy: Option, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange( connection, ( @@ -3319,7 +3365,7 @@ impl Guest for Commands { #[doc = " FUNCTION STATS"] #[doc = " https://valkey.io/commands/function-stats/"] #[allow(async_fn_in_trait)] - async fn function_stats(connection: &Connection) -> Result { + async fn function_stats(connection: &Connection) -> Result { exchange(connection, ("FUNCTION", "STATS")).await } @@ -3331,7 +3377,7 @@ impl Guest for Commands { async fn script_exists( connection: &Connection, sha1s: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ("SCRIPT", "EXISTS", NonEmpty::new("sha1s", sha1s)), @@ -3344,7 +3390,10 @@ impl Guest for Commands { #[doc = " SCRIPT FLUSH [ ASYNC | SYNC ]"] #[doc = " https://valkey.io/commands/script-flush/"] #[allow(async_fn_in_trait)] - async fn script_flush(connection: &Connection, mode: Option) -> Result<(), Error> { + async fn script_flush( + connection: &Connection, + mode: Option, + ) -> Result<(), ErrorCode> { exchange(connection, ("SCRIPT", "FLUSH", mode)).await } @@ -3353,7 +3402,7 @@ impl Guest for Commands { #[doc = " SCRIPT KILL"] #[doc = " https://valkey.io/commands/script-kill/"] #[allow(async_fn_in_trait)] - async fn script_kill(connection: &Connection) -> Result<(), Error> { + async fn script_kill(connection: &Connection) -> Result<(), ErrorCode> { exchange(connection, ("SCRIPT", "KILL")).await } @@ -3362,7 +3411,7 @@ impl Guest for Commands { #[doc = " SCRIPT LOAD script"] #[doc = " https://valkey.io/commands/script-load/"] #[allow(async_fn_in_trait)] - async fn script_load(connection: &Connection, script: String) -> Result { + async fn script_load(connection: &Connection, script: String) -> Result { exchange(connection, ("SCRIPT", "LOAD", script)).await } @@ -3371,7 +3420,7 @@ impl Guest for Commands { #[doc = " SCRIPT SHOW sha1"] #[doc = " https://valkey.io/commands/script-show/"] #[allow(async_fn_in_trait)] - async fn script_show(connection: &Connection, sha1: String) -> Result { + async fn script_show(connection: &Connection, sha1: String) -> Result { exchange(connection, ("SCRIPT", "SHOW", sha1)).await } @@ -3384,7 +3433,7 @@ impl Guest for Commands { connection: &Connection, shardchannel: String, message: StringOrBytes, - ) -> Result { + ) -> Result { exchange(connection, ("SPUBLISH", shardchannel, message)).await } @@ -3397,7 +3446,7 @@ impl Guest for Commands { connection: &Connection, key: String, value: StringOrBytes, - ) -> Result { + ) -> Result { exchange(connection, ("APPEND", key, value)).await } @@ -3406,7 +3455,7 @@ impl Guest for Commands { #[doc = " DECR key"] #[doc = " https://valkey.io/commands/decr/"] #[allow(async_fn_in_trait)] - async fn decr(connection: &Connection, key: String) -> Result { + async fn decr(connection: &Connection, key: String) -> Result { exchange(connection, ("DECR", key)).await } @@ -3419,7 +3468,7 @@ impl Guest for Commands { connection: &Connection, key: String, decrement: Integer, - ) -> Result { + ) -> Result { exchange(connection, ("DECRBY", key, decrement)).await } @@ -3432,7 +3481,7 @@ impl Guest for Commands { connection: &Connection, key: String, value: StringOrBytes, - ) -> Result { + ) -> Result { exchange(connection, ("DELIFEQ", key, value)).await } @@ -3441,7 +3490,7 @@ impl Guest for Commands { #[doc = " GET key"] #[doc = " https://valkey.io/commands/get/"] #[allow(async_fn_in_trait)] - async fn get(connection: &Connection, key: String) -> Result, Error> { + async fn get(connection: &Connection, key: String) -> Result, ErrorCode> { exchange(connection, ("GET", key)).await } @@ -3450,7 +3499,10 @@ impl Guest for Commands { #[doc = " GETDEL key"] #[doc = " https://valkey.io/commands/getdel/"] #[allow(async_fn_in_trait)] - async fn getdel(connection: &Connection, key: String) -> Result, Error> { + async fn getdel( + connection: &Connection, + key: String, + ) -> Result, ErrorCode> { exchange(connection, ("GETDEL", key)).await } @@ -3463,7 +3515,7 @@ impl Guest for Commands { connection: &Connection, key: String, ttl: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("GETEX", key, ttl)).await } @@ -3477,7 +3529,7 @@ impl Guest for Commands { key: String, start: Integer, end: Integer, - ) -> Result { + ) -> Result { exchange(connection, ("GETRANGE", key, start, end)).await } @@ -3490,7 +3542,7 @@ impl Guest for Commands { connection: &Connection, key: String, value: StringOrBytes, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("GETSET", key, value)).await } @@ -3499,7 +3551,7 @@ impl Guest for Commands { #[doc = " INCR key"] #[doc = " https://valkey.io/commands/incr/"] #[allow(async_fn_in_trait)] - async fn incr(connection: &Connection, key: String) -> Result { + async fn incr(connection: &Connection, key: String) -> Result { exchange(connection, ("INCR", key)).await } @@ -3512,7 +3564,7 @@ impl Guest for Commands { connection: &Connection, key: String, increment: Integer, - ) -> Result { + ) -> Result { exchange(connection, ("INCRBY", key, increment)).await } @@ -3525,7 +3577,7 @@ impl Guest for Commands { connection: &Connection, key: String, increment: Double, - ) -> Result { + ) -> Result { exchange(connection, ("INCRBYFLOAT", key, increment)).await } @@ -3540,7 +3592,7 @@ impl Guest for Commands { connection: &Connection, key1: String, key2: String, - ) -> Result { + ) -> Result { exchange(connection, ("LCS", key1, key2)).await } @@ -3557,7 +3609,7 @@ impl Guest for Commands { connection: &Connection, key1: String, key2: String, - ) -> Result { + ) -> Result { exchange(connection, ("LCS", key1, key2, "LEN")).await } @@ -3576,7 +3628,7 @@ impl Guest for Commands { key2: String, min_match_len: Option, with_match_len: bool, - ) -> Result { + ) -> Result { exchange( connection, ( @@ -3599,7 +3651,7 @@ impl Guest for Commands { async fn mget( connection: &Connection, keys: Vec, - ) -> Result>, Error> { + ) -> Result>, ErrorCode> { exchange(connection, ("MGET", NonEmpty::new("keys", keys))).await } @@ -3611,7 +3663,7 @@ impl Guest for Commands { async fn mset( connection: &Connection, keys_and_values: Vec<(String, StringOrBytes)>, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange( connection, ("MSET", NonEmpty::new("keys-and-values", keys_and_values)), @@ -3629,7 +3681,7 @@ impl Guest for Commands { keys_and_values: Vec<(String, StringOrBytes)>, precondition: Option, ttl: Option, - ) -> Result { + ) -> Result { exchange( connection, ( @@ -3650,7 +3702,7 @@ impl Guest for Commands { async fn msetnx( connection: &Connection, keys_and_values: Vec<(String, StringOrBytes)>, - ) -> Result { + ) -> Result { exchange( connection, ("MSETNX", NonEmpty::new("keys-and-values", keys_and_values)), @@ -3668,7 +3720,7 @@ impl Guest for Commands { key: String, milliseconds: DurationMilliseconds, value: StringOrBytes, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange(connection, ("PSETEX", key, milliseconds, value)).await } @@ -3687,7 +3739,7 @@ impl Guest for Commands { value: StringOrBytes, precondition: Option, ttl: Option, - ) -> Result { + ) -> Result { // OK once set, null when the precondition is not met let reply: Option<()> = exchange(connection, ("SET", key, value, precondition, ttl)).await?; @@ -3709,7 +3761,7 @@ impl Guest for Commands { value: StringOrBytes, precondition: Option, ttl: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("SET", key, value, precondition, "GET", ttl)).await } @@ -3723,7 +3775,7 @@ impl Guest for Commands { key: String, seconds: DurationSeconds, value: StringOrBytes, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange(connection, ("SETEX", key, seconds, value)).await } @@ -3736,7 +3788,7 @@ impl Guest for Commands { connection: &Connection, key: String, value: StringOrBytes, - ) -> Result { + ) -> Result { exchange(connection, ("SETNX", key, value)).await } @@ -3750,7 +3802,7 @@ impl Guest for Commands { key: String, offset: u32, value: StringOrBytes, - ) -> Result { + ) -> Result { exchange(connection, ("SETRANGE", key, offset, value)).await } @@ -3759,7 +3811,7 @@ impl Guest for Commands { #[doc = " STRLEN key"] #[doc = " https://valkey.io/commands/strlen/"] #[allow(async_fn_in_trait)] - async fn strlen(connection: &Connection, key: String) -> Result { + async fn strlen(connection: &Connection, key: String) -> Result { exchange(connection, ("STRLEN", key)).await } @@ -3773,7 +3825,7 @@ impl Guest for Commands { key: String, start: Integer, end: Integer, - ) -> Result { + ) -> Result { exchange(connection, ("SUBSTR", key, start, end)).await } @@ -3787,7 +3839,7 @@ impl Guest for Commands { key: String, group: String, ids: Vec, - ) -> Result { + ) -> Result { exchange(connection, ("XACK", key, group, NonEmpty::new("ids", ids))).await } @@ -3803,7 +3855,7 @@ impl Guest for Commands { trim: Option, id: StreamId, field_and_values: Vec<(String, StringOrBytes)>, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ( @@ -3842,7 +3894,7 @@ impl Guest for Commands { Vec<(StreamId, Map)>, Vec, ), - Error, + ErrorCode, > { let (next, claimed, deleted): (OrZeroId, _, _) = exchange( connection, @@ -3878,7 +3930,7 @@ impl Guest for Commands { min_idle_time: DurationMilliseconds, start: Option, count: Option, - ) -> Result<(Option, Vec, Vec), Error> { + ) -> Result<(Option, Vec, Vec), ErrorCode> { let (next, claimed, deleted): (OrZeroId, _, _) = exchange( connection, ( @@ -3911,7 +3963,7 @@ impl Guest for Commands { min_idle_time: DurationMilliseconds, ids: Vec, opts: Option, - ) -> Result)>, Error> { + ) -> Result)>, ErrorCode> { exchange( connection, ( @@ -3944,7 +3996,7 @@ impl Guest for Commands { min_idle_time: DurationMilliseconds, ids: Vec, opts: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ( @@ -3969,7 +4021,7 @@ impl Guest for Commands { connection: &Connection, key: String, ids: Vec, - ) -> Result { + ) -> Result { exchange(connection, ("XDEL", key, NonEmpty::new("ids", ids))).await } @@ -3985,7 +4037,7 @@ impl Guest for Commands { id: StreamId, mkstream: bool, entriesread: Option, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange( connection, ( @@ -4011,7 +4063,7 @@ impl Guest for Commands { key: String, group: String, consumer: String, - ) -> Result { + ) -> Result { exchange( connection, ("XGROUP", "CREATECONSUMER", key, group, consumer), @@ -4029,7 +4081,7 @@ impl Guest for Commands { key: String, group: String, consumer: String, - ) -> Result { + ) -> Result { exchange(connection, ("XGROUP", "DELCONSUMER", key, group, consumer)).await } @@ -4042,7 +4094,7 @@ impl Guest for Commands { connection: &Connection, key: String, group: String, - ) -> Result { + ) -> Result { exchange(connection, ("XGROUP", "DESTROY", key, group)).await } @@ -4057,7 +4109,7 @@ impl Guest for Commands { group: String, id: StreamId, entriesread: Option, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange( connection, ( @@ -4081,7 +4133,7 @@ impl Guest for Commands { connection: &Connection, key: String, group: String, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("XINFO", "CONSUMERS", key, group)).await } @@ -4093,7 +4145,7 @@ impl Guest for Commands { async fn xinfo_groups( connection: &Connection, key: String, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("XINFO", "GROUPS", key)).await } @@ -4104,7 +4156,10 @@ impl Guest for Commands { #[doc = " XINFO STREAM key"] #[doc = " https://valkey.io/commands/xinfo-stream/"] #[allow(async_fn_in_trait)] - async fn xinfo_stream(connection: &Connection, key: String) -> Result { + async fn xinfo_stream( + connection: &Connection, + key: String, + ) -> Result { exchange(connection, ("XINFO", "STREAM", key)).await } @@ -4121,7 +4176,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result { + ) -> Result { exchange( connection, ("XINFO", "STREAM", key, "FULL", NamedOption("COUNT", count)), @@ -4134,7 +4189,7 @@ impl Guest for Commands { #[doc = " XLEN key"] #[doc = " https://valkey.io/commands/xlen/"] #[allow(async_fn_in_trait)] - async fn xlen(connection: &Connection, key: String) -> Result { + async fn xlen(connection: &Connection, key: String) -> Result { exchange(connection, ("XLEN", key)).await } @@ -4151,7 +4206,7 @@ impl Guest for Commands { connection: &Connection, key: String, group: String, - ) -> Result { + ) -> Result { exchange(connection, ("XPENDING", key, group)).await } @@ -4173,7 +4228,7 @@ impl Guest for Commands { end: StreamId, count: Integer, consumer: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ( @@ -4201,7 +4256,7 @@ impl Guest for Commands { start: StreamId, end: StreamId, count: Option, - ) -> Result)>, Error> { + ) -> Result)>, ErrorCode> { exchange( connection, ("XRANGE", key, start, end, NamedOption("COUNT", count)), @@ -4223,7 +4278,7 @@ impl Guest for Commands { block: Option, keys: Vec, ids: Vec, - ) -> Result)>>>, Error> { + ) -> Result)>>>, ErrorCode> { exchange( connection, ( @@ -4258,7 +4313,7 @@ impl Guest for Commands { noack: bool, keys: Vec, ids: Vec, - ) -> Result>)>>>, Error> + ) -> Result>)>>>, ErrorCode> { exchange( connection, @@ -4289,7 +4344,7 @@ impl Guest for Commands { end: StreamId, start: StreamId, count: Option, - ) -> Result)>, Error> { + ) -> Result)>, ErrorCode> { exchange( connection, ("XREVRANGE", key, end, start, NamedOption("COUNT", count)), @@ -4308,7 +4363,7 @@ impl Guest for Commands { last_id: StreamId, entries_added: Option, max_deleted_id: Option, - ) -> Result<(), Error> { + ) -> Result<(), ErrorCode> { exchange( connection, ( @@ -4331,7 +4386,7 @@ impl Guest for Commands { connection: &Connection, key: String, trim: StreamTrim, - ) -> Result { + ) -> Result { exchange(connection, ("XTRIM", key, trim)).await } @@ -4344,7 +4399,7 @@ impl Guest for Commands { connection: &Connection, key: String, members: Vec, - ) -> Result { + ) -> Result { exchange(connection, ("SADD", key, NonEmpty::new("members", members))).await } @@ -4353,7 +4408,7 @@ impl Guest for Commands { #[doc = " SCARD key"] #[doc = " https://valkey.io/commands/scard/"] #[allow(async_fn_in_trait)] - async fn scard(connection: &Connection, key: String) -> Result { + async fn scard(connection: &Connection, key: String) -> Result { exchange(connection, ("SCARD", key)).await } @@ -4365,7 +4420,7 @@ impl Guest for Commands { async fn sdiff( connection: &Connection, keys: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("SDIFF", NonEmpty::new("keys", keys))).await } @@ -4378,7 +4433,7 @@ impl Guest for Commands { connection: &Connection, destination: String, keys: Vec, - ) -> Result { + ) -> Result { exchange( connection, ("SDIFFSTORE", destination, NonEmpty::new("keys", keys)), @@ -4394,7 +4449,7 @@ impl Guest for Commands { async fn sinter( connection: &Connection, keys: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("SINTER", NonEmpty::new("keys", keys))).await } @@ -4407,7 +4462,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, limit: Option, - ) -> Result { + ) -> Result { exchange( connection, ( @@ -4428,7 +4483,7 @@ impl Guest for Commands { connection: &Connection, destination: String, keys: Vec, - ) -> Result { + ) -> Result { exchange( connection, ("SINTERSTORE", destination, NonEmpty::new("keys", keys)), @@ -4449,7 +4504,7 @@ impl Guest for Commands { connection: &Connection, key: String, member: StringOrBytes, - ) -> Result { + ) -> Result { exchange(connection, ("SISMEMBER", key, member)).await } @@ -4466,7 +4521,7 @@ impl Guest for Commands { connection: &Connection, key: String, member: StringOrBytes, - ) -> Result, Error> { + ) -> Result, ErrorCode> { // -1 when the key does not exist let member: OrNever = exchange(connection, ("SISMEMBER", key, member, "XX")).await?; Ok(member.0) @@ -4477,7 +4532,10 @@ impl Guest for Commands { #[doc = " SMEMBERS key"] #[doc = " https://valkey.io/commands/smembers/"] #[allow(async_fn_in_trait)] - async fn smembers(connection: &Connection, key: String) -> Result, Error> { + async fn smembers( + connection: &Connection, + key: String, + ) -> Result, ErrorCode> { exchange(connection, ("SMEMBERS", key)).await } @@ -4490,7 +4548,7 @@ impl Guest for Commands { connection: &Connection, key: String, members: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ("SMISMEMBER", key, NonEmpty::new("members", members)), @@ -4508,7 +4566,7 @@ impl Guest for Commands { source: String, destination: String, member: StringOrBytes, - ) -> Result { + ) -> Result { exchange(connection, ("SMOVE", source, destination, member)).await } @@ -4523,7 +4581,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { if count.is_some() { return exchange(connection, ("SPOP", key, count)).await; } @@ -4543,7 +4601,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { if count.is_some() { return exchange(connection, ("SRANDMEMBER", key, count)).await; } @@ -4561,7 +4619,7 @@ impl Guest for Commands { connection: &Connection, key: String, members: Vec, - ) -> Result { + ) -> Result { exchange(connection, ("SREM", key, NonEmpty::new("members", members))).await } @@ -4576,7 +4634,7 @@ impl Guest for Commands { cursor: Cursor, match_: Option, count: Option, - ) -> Result<(Option, Vec), Error> { + ) -> Result<(Option, Vec), ErrorCode> { let (cursor, items): (NextCursor, Vec) = exchange( connection, ( @@ -4599,7 +4657,7 @@ impl Guest for Commands { async fn sunion( connection: &Connection, keys: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("SUNION", NonEmpty::new("keys", keys))).await } @@ -4612,7 +4670,7 @@ impl Guest for Commands { connection: &Connection, destination: String, keys: Vec, - ) -> Result { + ) -> Result { exchange( connection, ("SUNIONSTORE", destination, NonEmpty::new("keys", keys)), @@ -4634,7 +4692,7 @@ impl Guest for Commands { connection: &Connection, key: String, opts: Option, - ) -> Result>>, Error> { + ) -> Result>>, ErrorCode> { let row = sort_row_len(&opts); let values: Vec> = exchange(connection, ("SORT", key, opts)).await?; Ok(rows(values, row)) @@ -4654,7 +4712,7 @@ impl Guest for Commands { key: String, opts: Option, destination: String, - ) -> Result { + ) -> Result { exchange(connection, ("SORT", key, opts, "STORE", destination)).await } @@ -4670,7 +4728,7 @@ impl Guest for Commands { connection: &Connection, key: String, opts: Option, - ) -> Result>>, Error> { + ) -> Result>>, ErrorCode> { let row = sort_row_len(&opts); let values: Vec> = exchange(connection, ("SORT_RO", key, opts)).await?; @@ -4691,7 +4749,7 @@ impl Guest for Commands { keys: Vec, end: BzmpopEnd, count: Option, - ) -> Result)>, Error> { + ) -> Result)>, ErrorCode> { let command = ( "BZMPOP", DecimalSeconds(timeout), @@ -4714,7 +4772,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, timeout: DurationMilliseconds, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ( @@ -4738,7 +4796,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, timeout: DurationMilliseconds, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ( @@ -4764,7 +4822,7 @@ impl Guest for Commands { comparison: Option, changed: bool, score_and_members: Vec<(Double, StringOrBytes)>, - ) -> Result, Error> { + ) -> Result, ErrorCode> { let command = ( "ZADD", key, @@ -4793,7 +4851,7 @@ impl Guest for Commands { comparison: Option, increment: Double, member: StringOrBytes, - ) -> Result, Error> { + ) -> Result, ErrorCode> { let command = ( "ZADD", key, condition, comparison, "INCR", increment, member, ); @@ -4805,7 +4863,7 @@ impl Guest for Commands { #[doc = " ZCARD key"] #[doc = " https://valkey.io/commands/zcard/"] #[allow(async_fn_in_trait)] - async fn zcard(connection: &Connection, key: String) -> Result { + async fn zcard(connection: &Connection, key: String) -> Result { exchange(connection, ("ZCARD", key)).await } @@ -4819,7 +4877,7 @@ impl Guest for Commands { key: String, min: ScoreBound, max: ScoreBound, - ) -> Result { + ) -> Result { exchange( connection, ( @@ -4845,7 +4903,7 @@ impl Guest for Commands { async fn zdiff( connection: &Connection, keys: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("ZDIFF", NumKeys(NonEmpty::new("keys", keys)))).await } @@ -4861,7 +4919,7 @@ impl Guest for Commands { async fn zdiff_withscores( connection: &Connection, keys: Vec, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ("ZDIFF", NumKeys(NonEmpty::new("keys", keys)), "WITHSCORES"), @@ -4878,7 +4936,7 @@ impl Guest for Commands { connection: &Connection, destination: String, keys: Vec, - ) -> Result { + ) -> Result { exchange( connection, ( @@ -4900,7 +4958,7 @@ impl Guest for Commands { key: String, increment: Double, member: StringOrBytes, - ) -> Result { + ) -> Result { exchange(connection, ("ZINCRBY", key, increment, member)).await } @@ -4916,7 +4974,7 @@ impl Guest for Commands { keys: Vec, weights: Option>, aggregate: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ( @@ -4943,7 +5001,7 @@ impl Guest for Commands { keys: Vec, weights: Option>, aggregate: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { let command = ( "ZINTER", NumKeys(NonEmpty::new("keys", keys)), @@ -4963,7 +5021,7 @@ impl Guest for Commands { connection: &Connection, keys: Vec, limit: Option, - ) -> Result { + ) -> Result { exchange( connection, ( @@ -4986,7 +5044,7 @@ impl Guest for Commands { keys: Vec, weights: Option>, aggregate: Option, - ) -> Result { + ) -> Result { let command = ( "ZINTERSTORE", destination, @@ -5007,7 +5065,7 @@ impl Guest for Commands { key: String, min: LexBound, max: LexBound, - ) -> Result { + ) -> Result { exchange( connection, ( @@ -5033,7 +5091,7 @@ impl Guest for Commands { keys: Vec, end: ZmpopEnd, count: Option, - ) -> Result)>, Error> { + ) -> Result)>, ErrorCode> { exchange( connection, ( @@ -5055,7 +5113,7 @@ impl Guest for Commands { connection: &Connection, key: String, members: Vec, - ) -> Result>, Error> { + ) -> Result>, ErrorCode> { exchange( connection, ("ZMSCORE", key, NonEmpty::new("members", members)), @@ -5074,7 +5132,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { let members: ScoredMembers = exchange(connection, ("ZPOPMAX", key, count)).await?; Ok(members.0) } @@ -5090,7 +5148,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { let members: ScoredMembers = exchange(connection, ("ZPOPMIN", key, count)).await?; Ok(members.0) } @@ -5108,7 +5166,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { if count.is_some() { return exchange(connection, ("ZRANDMEMBER", key, count)).await; } @@ -5130,7 +5188,7 @@ impl Guest for Commands { connection: &Connection, key: String, count: Integer, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("ZRANDMEMBER", key, count, "WITHSCORES")).await } @@ -5152,7 +5210,7 @@ impl Guest for Commands { rev: bool, limit: Option, xx: bool, - ) -> Result>, Error> { + ) -> Result>, ErrorCode> { let range = Zrange { range, rev, limit }; exchange(connection, ("ZRANGE", key, range, Flag("XX", xx))).await } @@ -5174,7 +5232,7 @@ impl Guest for Commands { rev: bool, limit: Option, xx: bool, - ) -> Result>, Error> { + ) -> Result>, ErrorCode> { let range = match range { ZrangeWithscoresBy::Rank(rank) => ZrangeBy::Rank(rank), ZrangeWithscoresBy::Score(score) => ZrangeBy::Score(score), @@ -5198,7 +5256,7 @@ impl Guest for Commands { range: ZrangeBy, rev: bool, limit: Option, - ) -> Result { + ) -> Result { let range = Zrange { range, rev, limit }; exchange(connection, ("ZRANGESTORE", destination, source, range)).await } @@ -5214,7 +5272,7 @@ impl Guest for Commands { connection: &Connection, key: String, member: StringOrBytes, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("ZRANK", key, member)).await } @@ -5231,7 +5289,7 @@ impl Guest for Commands { connection: &Connection, key: String, member: StringOrBytes, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("ZRANK", key, member, "WITHSCORE")).await } @@ -5244,7 +5302,7 @@ impl Guest for Commands { connection: &Connection, key: String, members: Vec, - ) -> Result { + ) -> Result { exchange(connection, ("ZREM", key, NonEmpty::new("members", members))).await } @@ -5258,7 +5316,7 @@ impl Guest for Commands { key: String, min: LexBound, max: LexBound, - ) -> Result { + ) -> Result { exchange( connection, ( @@ -5284,7 +5342,7 @@ impl Guest for Commands { key: String, start: Integer, stop: Integer, - ) -> Result { + ) -> Result { exchange(connection, ("ZREMRANGEBYRANK", key, start, stop)).await } @@ -5298,7 +5356,7 @@ impl Guest for Commands { key: String, min: ScoreBound, max: ScoreBound, - ) -> Result { + ) -> Result { exchange( connection, ( @@ -5325,7 +5383,7 @@ impl Guest for Commands { connection: &Connection, key: String, member: StringOrBytes, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("ZREVRANK", key, member)).await } @@ -5342,7 +5400,7 @@ impl Guest for Commands { connection: &Connection, key: String, member: StringOrBytes, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("ZREVRANK", key, member, "WITHSCORE")).await } @@ -5359,7 +5417,7 @@ impl Guest for Commands { cursor: Cursor, match_: Option, count: Option, - ) -> Result<(Option, Vec<(StringOrBytes, Double)>), Error> { + ) -> Result<(Option, Vec<(StringOrBytes, Double)>), ErrorCode> { // the members and scores are a flat array, the scores bulk strings let (cursor, members): (NextCursor, FlatPairs) = exchange( connection, @@ -5390,7 +5448,7 @@ impl Guest for Commands { cursor: Cursor, match_: Option, count: Option, - ) -> Result<(Option, Vec), Error> { + ) -> Result<(Option, Vec), ErrorCode> { let (cursor, items): (NextCursor, Vec) = exchange( connection, ( @@ -5415,7 +5473,7 @@ impl Guest for Commands { connection: &Connection, key: String, member: StringOrBytes, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange(connection, ("ZSCORE", key, member)).await } @@ -5431,7 +5489,7 @@ impl Guest for Commands { keys: Vec, weights: Option>, aggregate: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { exchange( connection, ( @@ -5458,7 +5516,7 @@ impl Guest for Commands { keys: Vec, weights: Option>, aggregate: Option, - ) -> Result, Error> { + ) -> Result, ErrorCode> { let command = ( "ZUNION", NumKeys(NonEmpty::new("keys", keys)), @@ -5480,7 +5538,7 @@ impl Guest for Commands { keys: Vec, weights: Option>, aggregate: Option, - ) -> Result { + ) -> Result { let command = ( "ZUNIONSTORE", destination, diff --git a/components/connection/src/lib.rs b/components/connection/src/lib.rs index 80bd447..d02fc3a 100644 --- a/components/connection/src/lib.rs +++ b/components/connection/src/lib.rs @@ -30,7 +30,7 @@ use std::rc::{Rc, Weak}; use componentized_valkey::types::{RespValue, StringOrBytes}; use exports::componentized::valkey::connection::{ - ConnectOpts, Connection, DurationMilliseconds, Error, Guest, GuestConnection, Subscription, + ConnectOpts, Connection, DurationMilliseconds, ErrorCode, Guest, GuestConnection, Subscription, }; use futures_channel::{mpsc, oneshot}; use futures_core::Stream; @@ -59,19 +59,19 @@ impl Guest for Component { /// Where the reply to a command goes. enum Reply { /// To the caller waiting for it. - Call(oneshot::Sender>), + Call(oneshot::Sender>), /// Nowhere, the command was sent without waiting for its reply. Discard, /// A subscription's SUBSCRIBE, UNSUBSCRIBE or the like, see `pubsub::Pending`. #[cfg_attr(not(feature = "pubsub"), allow(dead_code))] Pubsub(pubsub::Pending), /// RESET, whose reply ends the subscriptions the server dropped. - Reset(oneshot::Sender>), + Reset(oneshot::Sender>), /// The HELLO after RESET, the writer waits for its reply before sending another command, and /// the connection closes when it fails. Rehello { proceed: oneshot::Sender, - done: oneshot::Sender>, + done: oneshot::Sender>, }, } @@ -101,7 +101,7 @@ struct State { /// Where each reply goes, in the order the commands were sent. pending: VecDeque, /// Why the connection closed, once it has. - closed: Option, + closed: Option, /// Counts the commands written and the values read, it stays the same while the connection is /// idle. activity: u64, @@ -135,15 +135,15 @@ pub struct ConnectionImpl(Rc); impl GuestConnection for ConnectionImpl { async fn new( replies: StreamReader, - closed: FutureReader>, + closed: FutureReader>, opts: Option, ) -> ( StreamReader, - FutureReader>, + FutureReader>, ) { let (output, requests) = wit_stream::new(); // the default is only written if the connection is dropped before it is established - let (connection_writer, connection) = wit_future::new(|| Err(Error::Closed(None))); + let (connection_writer, connection) = wit_future::new(|| Err(ErrorCode::Closed(None))); let state = Rc::new(RefCell::new(State::default())); let (requests_tx, requests_rx) = mpsc::unbounded(); @@ -166,46 +166,48 @@ impl GuestConnection for ConnectionImpl { self.0.state.borrow().server_properties.clone() } - async fn exchange(&self, command: Vec) -> Result { + async fn exchange(&self, command: Vec) -> Result { exchange(&self.0, bytes(command)).await } async fn exchange_all( &self, commands: Vec>, - ) -> Vec> { + ) -> Vec> { exchange_all(&self.0, commands.into_iter().map(bytes).collect()).await } - async fn emit(&self, command: Vec) -> Result<(), Error> { + async fn emit(&self, command: Vec) -> Result<(), ErrorCode> { emit(&self.0, bytes(command)).await } - async fn watch(&self, keys: Vec) -> Result<(), Error> { + async fn watch(&self, keys: Vec) -> Result<(), ErrorCode> { if keys.is_empty() { - return Err(Error::Client("keys requires at least one item".to_string())); + return Err(ErrorCode::Client( + "keys requires at least one item".to_string(), + )); } let keys = keys.into_iter().map(String::into_bytes); let command = std::iter::once(b"WATCH".to_vec()).chain(keys).collect(); ok(exchange(&self.0, command).await?) } - async fn unwatch(&self) -> Result<(), Error> { + async fn unwatch(&self) -> Result<(), ErrorCode> { ok(exchange(&self.0, words(&["UNWATCH"])).await?) } async fn transaction( &self, commands: Vec>, - ) -> Result>, Error> { + ) -> Result>, ErrorCode> { transaction(&self.0, commands.into_iter().map(bytes).collect()).await } - fn subscription(&self) -> Result { + fn subscription(&self) -> Result { pubsub::subscription(&self.0) } - async fn reset(&self) -> Result<(), Error> { + async fn reset(&self) -> Result<(), ErrorCode> { reset(&self.0).await } @@ -248,7 +250,7 @@ impl GuestConnection for ConnectionImpl { /// The writer sends no other command until HELLO succeeds, so none runs on the connection RESET /// leaves: RESP2, the default user, no name. When HELLO fails, e.g. the credentials no longer work, /// the reader closes the connection, and the commands queued after it fail without being sent. -async fn reset(inner: &Inner) -> Result<(), Error> { +async fn reset(inner: &Inner) -> Result<(), ErrorCode> { let mut state = inner.state.borrow_mut(); if let Some(error) = &state.closed { return Err(error.clone()); @@ -272,7 +274,7 @@ async fn reset(inner: &Inner) -> Result<(), Error> { written: None, barrier, }) - .map_err(|_| Error::Closed(None))?; + .map_err(|_| ErrorCode::Closed(None))?; } state.pending.push_back(Reply::Reset(reset_tx)); state.pending.push_back(Reply::Rehello { @@ -297,9 +299,9 @@ fn enqueue( command: Vec, reply: Option, written: Option>, -) -> Result<(), Error> { +) -> Result<(), ErrorCode> { if command.is_empty() { - return Err(Error::Client( + return Err(ErrorCode::Client( "a command requires at least one word".to_string(), )); } @@ -321,12 +323,12 @@ fn enqueue( written, barrier: None, }) - .map_err(|_| Error::Closed(None))?; + .map_err(|_| ErrorCode::Closed(None))?; state.pending.extend(reply); Ok(()) } -async fn exchange(inner: &Inner, command: Vec) -> Result { +async fn exchange(inner: &Inner, command: Vec) -> Result { reject_managed(inner, &command)?; if replies_with_pushes(&command) { return emit(inner, command).await.map(|()| RespValue::Null); @@ -336,7 +338,7 @@ async fn exchange(inner: &Inner, command: Vec) -> Result /// Send a command and wait for its reply, for the commands the connection sends itself, e.g. HELLO, /// which callers may not. -async fn request(inner: &Inner, command: Vec) -> Result { +async fn request(inner: &Inner, command: Vec) -> Result { let (reply_tx, reply_rx) = oneshot::channel(); enqueue(inner, command, Some(Reply::Call(reply_tx)), None)?; // the reply is dropped unsent when the connection closes, the reader fails it first @@ -346,9 +348,12 @@ async fn request(inner: &Inner, command: Vec) -> Result } /// Queue every command before waiting for any reply, so no other command is sent between them. -async fn exchange_all(inner: &Inner, commands: Vec>) -> Vec> { +async fn exchange_all( + inner: &Inner, + commands: Vec>, +) -> Vec> { // no await until every command is queued - let queued: Vec>, Error>> = commands + let queued: Vec>, ErrorCode>> = commands .into_iter() .map(|command| { reject_managed(inner, &command)?; @@ -378,10 +383,10 @@ async fn exchange_all(inner: &Inner, commands: Vec>) -> Vec>, -) -> Result>, Error> { +) -> Result>, ErrorCode> { // before MULTI is sent, so an invalid command fails the transaction rather than being left out if commands.iter().any(Vec::is_empty) { - return Err(Error::Client( + return Err(ErrorCode::Client( "a command requires at least one word".to_string(), )); } @@ -411,7 +416,7 @@ async fn transaction( } } -async fn emit(inner: &Inner, command: Vec) -> Result<(), Error> { +async fn emit(inner: &Inner, command: Vec) -> Result<(), ErrorCode> { reject_managed(inner, &command)?; let reply = (!replies_with_pushes(&command)).then_some(Reply::Discard); let (written_tx, written_rx) = oneshot::channel(); @@ -427,7 +432,7 @@ async fn emit(inner: &Inner, command: Vec) -> Result<(), Error> { /// - CLIENT REPLY, which stops replies from pairing with commands /// - SUBSCRIBE and the like once the connection owns the subscriptions, see /// `State::owns_subscriptions`, e.g. an UNSUBSCRIBE would end a subscription without it asking -fn reject_managed(inner: &Inner, command: &[Word]) -> Result<(), Error> { +fn reject_managed(inner: &Inner, command: &[Word]) -> Result<(), ErrorCode> { let is = |index: usize, word: &str| { command .get(index) @@ -445,11 +450,11 @@ fn reject_managed(inner: &Inner, command: &[Word]) -> Result<(), Error> { None }; if let Some(reason) = reason { - return Err(Error::Client(reason.to_string())); + return Err(ErrorCode::Client(reason.to_string())); } if replies_with_pushes(command) && inner.state.borrow().owns_subscriptions { let name = String::from_utf8_lossy(&command[0]).to_uppercase(); - return Err(Error::Client(format!( + return Err(ErrorCode::Client(format!( "{name} is rejected once the connection has a subscription, use the subscription's methods" ))); } @@ -478,13 +483,17 @@ fn replies_with_pushes(command: &[Word]) -> bool { } /// The error a command fails with once the connection closed. -fn closed_error(state: &RefCell) -> Error { - state.borrow().closed.clone().unwrap_or(Error::Closed(None)) +fn closed_error(state: &RefCell) -> ErrorCode { + state + .borrow() + .closed + .clone() + .unwrap_or(ErrorCode::Closed(None)) } /// Close the connection, failing every command waiting for a reply, closing the readers of dead /// letters, and ending the subscriptions. -fn close(state: &RefCell, error: Error) { +fn close(state: &RefCell, error: ErrorCode) { let mut state = state.borrow_mut(); let error = state.closed.get_or_insert(error).clone(); for reply in state.pending.drain(..).collect::>() { @@ -566,7 +575,7 @@ async fn write_requests( async fn read_replies( state: Rc>, mut replies: StreamReader, - closed: FutureReader>, + closed: FutureReader>, ) { // the error to close the connection with, before the transport closes let mut closing = None; @@ -643,7 +652,7 @@ async fn read_replies( Ok(()) => None, Err(error) => describe(error), }; - close(&state, Error::Closed(reason)); + close(&state, ErrorCode::Closed(reason)); } /// Start the work methods ask for that outlives them. @@ -710,7 +719,7 @@ fn keep_alive_activity(inner: &Weak, generation: u64) -> Option { async fn establish( inner: Rc, opts: Option, - connection: FutureWriter>, + connection: FutureWriter>, ) { let (auth, client_name, keep_alive_interval) = match opts { Some(opts) => (opts.auth, opts.client_name, opts.keep_alive_interval), @@ -763,15 +772,17 @@ async fn establish( } /// The server's properties from the reply to HELLO, a map of property names to values. -fn server_properties(reply: RespValue) -> Result, Error> { +fn server_properties(reply: RespValue) -> Result, ErrorCode> { let RespValue::Map(entries) = reply else { - return Err(Error::Client(format!("unexpected reply to HELLO: {reply}"))); + return Err(ErrorCode::Client(format!( + "unexpected reply to HELLO: {reply}" + ))); }; entries .into_iter() .map(|(name, value)| { let invalid = |name: RespValue| { - Error::Client(format!( + ErrorCode::Client(format!( "unexpected property name in reply to HELLO: {name}" )) }; @@ -787,36 +798,39 @@ fn server_properties(reply: RespValue) -> Result, Error .collect() } -fn decode(value: &[u8]) -> Result { - componentized_valkey::resp::decode(value).map_err(Error::Resp) +fn decode(value: &[u8]) -> Result { + componentized_valkey::resp::decode(value).map_err(ErrorCode::Resp) } /// An error value from the server fails the command. /// A simple string `OK`. -fn ok(reply: RespValue) -> Result<(), Error> { +fn ok(reply: RespValue) -> Result<(), ErrorCode> { match reply { RespValue::String(text) if text == "OK" => Ok(()), reply => Err(unexpected(&reply)), } } -fn unexpected(reply: &RespValue) -> Error { - Error::Client(format!("unexpected reply: {reply:?}")) +fn unexpected(reply: &RespValue) -> ErrorCode { + ErrorCode::Client(format!("unexpected reply: {reply:?}")) } -fn reply_result(value: RespValue) -> Result { +fn reply_result(value: RespValue) -> Result { match value { - RespValue::Error(message) | RespValue::BulkError(message) => Err(Error::Valkey(message)), + RespValue::Error(message) => Err(ErrorCode::Server(StringOrBytes::String(message))), + // binary safe, like a bulk string + RespValue::BulkError(message) => Err(ErrorCode::Server(StringOrBytes::from_bytes(message))), value => Ok(value), } } /// A transport's error as the reason a connection closed. -fn describe(error: Error) -> Option { +fn describe(error: ErrorCode) -> Option { match error { - Error::Closed(reason) => reason, - Error::Client(message) | Error::Resp(message) | Error::Valkey(message) => Some(message), - Error::Lagged => Some("a stream fell behind".to_string()), + ErrorCode::Closed(reason) => reason, + ErrorCode::Client(message) | ErrorCode::Resp(message) => Some(message), + ErrorCode::Server(message) => Some(message.to_string()), + ErrorCode::Lagged => Some("a stream fell behind".to_string()), } } diff --git a/components/connection/src/pubsub.rs b/components/connection/src/pubsub.rs index cf81c90..70ccf4c 100644 --- a/components/connection/src/pubsub.rs +++ b/components/connection/src/pubsub.rs @@ -11,14 +11,16 @@ use componentized_valkey::types::{ use futures_channel::{mpsc, oneshot}; use wit_bindgen::{FutureReader, FutureWriter, StreamReader, StreamWriter}; -use crate::exports::componentized::valkey::connection::{Error, GuestSubscription, Subscription}; +use crate::exports::componentized::valkey::connection::{ + ErrorCode, GuestSubscription, Subscription, +}; use crate::{ Inner, PUSH_BUFFER, Reply, State, Task, closed_error, decode, enqueue, next, wit_future, wit_stream, }; /// A new subscription, setting the ratchet, see `State::owns_subscriptions`. -pub(crate) fn subscription(inner: &Rc) -> Result { +pub(crate) fn subscription(inner: &Rc) -> Result { let id = { let mut state = inner.state.borrow_mut(); state.owns_subscriptions = true; @@ -40,20 +42,20 @@ pub(crate) enum Pending { /// The targets not confirmed yet. channels: Vec, id: u64, - done: oneshot::Sender>, + done: oneshot::Sender>, }, /// UNSUBSCRIBE and the like. Unsubscribe { kind: Kind, /// The targets not confirmed yet. channels: Vec, - done: oneshot::Sender>, + done: oneshot::Sender>, }, } impl Pending { /// A reply arrived in the place of the confirmations, the server refused the command. - pub(crate) fn refused(self, subscriptions: &mut Subscriptions, error: Error) { + pub(crate) fn refused(self, subscriptions: &mut Subscriptions, error: ErrorCode) { match self { Pending::Subscribe { kind, @@ -71,7 +73,7 @@ impl Pending { } /// The connection closed. - pub(crate) fn fail(self, error: Error) { + pub(crate) fn fail(self, error: ErrorCode) { match self { Pending::Subscribe { done, .. } | Pending::Unsubscribe { done, .. } => { let _ = done.send(Err(error)); @@ -95,12 +97,12 @@ impl GuestSubscription for SubscriptionImpl { .collect() } - async fn subscribe(&self, targets: Vec) -> Result<(), Error> { + async fn subscribe(&self, targets: Vec) -> Result<(), ErrorCode> { let groups = by_kind(targets)?; subscribe(&self.0.inner, self.0.id, groups).await } - async fn unsubscribe(&self, targets: Option>) -> Result<(), Error> { + async fn unsubscribe(&self, targets: Option>) -> Result<(), ErrorCode> { let groups = match targets { Some(targets) => by_kind(targets)? .into_iter() @@ -114,7 +116,7 @@ impl GuestSubscription for SubscriptionImpl { fn events(&self) -> (StreamReader, FutureReader) { let (writer, stream) = wit_stream::new(); // the default is only written if the subscription is gone before the stream ends - let (end_writer, end) = wit_future::new(|| Err(Error::Closed(None))); + let (end_writer, end) = wit_future::new(|| Err(ErrorCode::Closed(None))); let (mut events_tx, events_rx) = mpsc::channel(PUSH_BUFFER); let (end_tx, end_rx) = oneshot::channel(); { @@ -183,9 +185,9 @@ impl Kind { /// The names of the targets of each kind, in the order each kind first appears, failing unless /// there is at least one target. -fn by_kind(targets: Vec) -> Result)>, Error> { +fn by_kind(targets: Vec) -> Result)>, ErrorCode> { if targets.is_empty() { - return Err(Error::Client( + return Err(ErrorCode::Client( "targets requires at least one item".to_string(), )); } @@ -219,7 +221,7 @@ enum Hold { } /// How a stream of events ended, `ok` once the subscription is dropped. -type Ended = Result<(), Error>; +type Ended = Result<(), ErrorCode>; /// A reader of a subscription's events, and where to send how its stream ended. struct Reader { @@ -237,7 +239,7 @@ struct Sub { /// The events arriving while no stream is open, for the next stream. backlog: VecDeque, /// The error that closed the connection, once it has. - closed: Option, + closed: Option, } impl Sub { @@ -272,7 +274,7 @@ impl Sub { match reader.events.try_send(event.clone()) { Ok(()) => self.readers.push(reader), Err(error) if error.is_full() => { - let _ = reader.end.send(Err(Error::Lagged)); + let _ = reader.end.send(Err(ErrorCode::Lagged)); } Err(_) => self.orphans.push(reader.end), } @@ -303,7 +305,7 @@ pub(crate) struct Subscriptions { } impl Subscriptions { - fn create(&mut self, closed: Option) -> u64 { + fn create(&mut self, closed: Option) -> u64 { let id = self.next_id; self.next_id += 1; self.subs.insert( @@ -433,7 +435,7 @@ impl Subscriptions { } /// The connection closed, ending every stream. - pub(crate) fn close(&mut self, error: &Error) { + pub(crate) fn close(&mut self, error: &ErrorCode) { for sub in self.subs.values_mut() { sub.end(Err(error.clone())); sub.closed = Some(error.clone()); @@ -493,11 +495,15 @@ fn unique(names: Vec) -> Vec { } /// A command for one kind of targets, queued and waiting for the server to confirm it. -type Confirmation = oneshot::Receiver>; +type Confirmation = oneshot::Receiver>; /// Subscribe to the targets, one command for each kind, queued with no other command between /// them. Every command is sent, the error holds each that failed. -async fn subscribe(inner: &Inner, id: u64, groups: Vec<(Kind, Vec)>) -> Result<(), Error> { +async fn subscribe( + inner: &Inner, + id: u64, + groups: Vec<(Kind, Vec)>, +) -> Result<(), ErrorCode> { let several = groups.len() > 1; // no await until every command is queued let queued: Vec<_> = groups @@ -538,7 +544,7 @@ fn queue_subscribe( id: u64, kind: Kind, names: Vec, -) -> Result, Confirmation)>, Error> { +) -> Result, Confirmation)>, ErrorCode> { let names: Vec = { let mut state = inner.state.borrow_mut(); // before the targets are touched, they stay as they were when the connection closed @@ -610,7 +616,7 @@ async fn unsubscribe( inner: &Inner, id: u64, groups: Vec<(Kind, Option>)>, -) -> Result<(), Error> { +) -> Result<(), ErrorCode> { let several = groups.len() > 1; // no await until every command is queued let queued: Vec<_> = groups @@ -641,7 +647,7 @@ fn queue_unsubscribe( id: u64, kind: Kind, names: Option>, -) -> Result, Error> { +) -> Result, ErrorCode> { let release = { let mut state = inner.state.borrow_mut(); // before the targets are touched, they stay as they were when the connection closed @@ -703,7 +709,7 @@ fn queue_unsubscribe( /// The errors of the commands of a call as one, each prefixed with its command when the call sent /// several. An error of one kind keeps it, e.g. every command was refused by the server, otherwise /// it is a client error. -fn aggregate(errors: Vec<(&str, Error)>, several: bool) -> Result<(), Error> { +fn aggregate(errors: Vec<(&str, ErrorCode)>, several: bool) -> Result<(), ErrorCode> { let mut errors = errors.into_iter(); let Some((command, first)) = errors.next() else { return Ok(()); @@ -720,29 +726,30 @@ fn aggregate(errors: Vec<(&str, Error)>, several: bool) -> Result<(), Error> { } if messages.len() == 1 { return Err(match first { - Error::Valkey(_) => Error::Valkey(messages.remove(0)), - Error::Client(_) => Error::Client(messages.remove(0)), - Error::Resp(_) => Error::Resp(messages.remove(0)), + ErrorCode::Server(_) => ErrorCode::Server(messages.remove(0).into()), + ErrorCode::Client(_) => ErrorCode::Client(messages.remove(0)), + ErrorCode::Resp(_) => ErrorCode::Resp(messages.remove(0)), error => error, }); } let message = messages.join("; "); Err(match first { // the connection closed, every command failed the same way - error @ (Error::Closed(_) | Error::Lagged) if same => error, - Error::Valkey(_) if same => Error::Valkey(message), - Error::Resp(_) if same => Error::Resp(message), - _ => Error::Client(message), + error @ (ErrorCode::Closed(_) | ErrorCode::Lagged) if same => error, + ErrorCode::Server(_) if same => ErrorCode::Server(message.into()), + ErrorCode::Resp(_) if same => ErrorCode::Resp(message), + _ => ErrorCode::Client(message), }) } /// An error for people to read, in an aggregate of errors. -fn describe_error(error: &Error) -> String { +fn describe_error(error: &ErrorCode) -> String { match error { - Error::Closed(None) => "connection closed".to_string(), - Error::Closed(Some(reason)) => format!("connection closed: {reason}"), - Error::Client(message) | Error::Resp(message) | Error::Valkey(message) => message.clone(), - Error::Lagged => "fell behind".to_string(), + ErrorCode::Closed(None) => "connection closed".to_string(), + ErrorCode::Closed(Some(reason)) => format!("connection closed: {reason}"), + ErrorCode::Client(message) | ErrorCode::Resp(message) => message.clone(), + ErrorCode::Server(message) => message.to_string(), + ErrorCode::Lagged => "fell behind".to_string(), } } @@ -918,6 +925,6 @@ async fn forward_events( } drop(events); drop(writer); - let end = end.await.unwrap_or_else(|_| Err(Error::Closed(None))); + let end = end.await.unwrap_or_else(|_| Err(ErrorCode::Closed(None))); let _ = end_writer.write(end).await; } diff --git a/components/connection/src/pubsub_disabled.rs b/components/connection/src/pubsub_disabled.rs index 20493da..9594f3a 100644 --- a/components/connection/src/pubsub_disabled.rs +++ b/components/connection/src/pubsub_disabled.rs @@ -8,12 +8,14 @@ use std::rc::Rc; use componentized_valkey::types::{RespValue, SubscriptionEvent, SubscriptionTarget}; use wit_bindgen::{FutureReader, StreamReader}; -use crate::exports::componentized::valkey::connection::{Error, GuestSubscription, Subscription}; +use crate::exports::componentized::valkey::connection::{ + ErrorCode, GuestSubscription, Subscription, +}; use crate::{Inner, State}; /// Fails, there are no subscriptions in this build. -pub(crate) fn subscription(_inner: &Rc) -> Result { - Err(Error::Client( +pub(crate) fn subscription(_inner: &Rc) -> Result { + Err(ErrorCode::Client( "subscriptions are not supported by this build of the connection, push messages arrive in dead-letters" .to_string(), )) @@ -23,11 +25,11 @@ pub(crate) fn subscription(_inner: &Rc) -> Result { pub(crate) enum Pending {} impl Pending { - pub(crate) fn refused(self, _subscriptions: &mut Subscriptions, _error: Error) { + pub(crate) fn refused(self, _subscriptions: &mut Subscriptions, _error: ErrorCode) { match self {} } - pub(crate) fn fail(self, _error: Error) { + pub(crate) fn fail(self, _error: ErrorCode) { match self {} } } @@ -39,7 +41,7 @@ pub(crate) struct Subscriptions; impl Subscriptions { pub(crate) fn reset(&mut self) {} - pub(crate) fn close(&mut self, _error: &Error) {} + pub(crate) fn close(&mut self, _error: &ErrorCode) {} } /// No push message belongs to a subscription, each is a dead letter. @@ -55,11 +57,14 @@ impl GuestSubscription for SubscriptionImpl { match *self {} } - async fn subscribe(&self, _targets: Vec) -> Result<(), Error> { + async fn subscribe(&self, _targets: Vec) -> Result<(), ErrorCode> { match *self {} } - async fn unsubscribe(&self, _targets: Option>) -> Result<(), Error> { + async fn unsubscribe( + &self, + _targets: Option>, + ) -> Result<(), ErrorCode> { match *self {} } @@ -67,7 +72,7 @@ impl GuestSubscription for SubscriptionImpl { &self, ) -> ( StreamReader, - FutureReader>, + FutureReader>, ) { match *self {} } diff --git a/components/connection/tests/connection.rs b/components/connection/tests/connection.rs index f789266..b915721 100644 --- a/components/connection/tests/connection.rs +++ b/components/connection/tests/connection.rs @@ -7,7 +7,7 @@ use test_harness::types::{ SubscriptionTarget, UnsubscribeReason, }; use test_harness::{ - ConnectOpts, Error, Harness, RespValue, Transport, bulk, command, connect, establish, + ConnectOpts, ErrorCode, Harness, RespValue, Transport, bulk, command, connect, establish, handshake, hello_reply, ok, push, receive, resolve, strings, words, }; @@ -112,7 +112,9 @@ async fn hello_error_fails_to_establish() -> wasmtime::Result<()> { }; assert_eq!( error, - Error::Valkey("WRONGPASS invalid password".to_string()) + ErrorCode::Server(StringOrBytes::String( + "WRONGPASS invalid password".to_string() + )) ); // the CLIENT SETINFO commands queued with HELLO, then the connection is dropped, which // closes the requests stream @@ -137,10 +139,12 @@ async fn closing_before_hello_fails_to_establish() -> wasmtime::Result<()> { .run(async |accessor, exports| { let (mut transport, connection) = connect(accessor, exports, None).await?; transport.request().await; - transport.close(Err(Error::Closed(Some("connection refused".to_string())))); + transport.close(Err(ErrorCode::Closed(Some( + "connection refused".to_string(), + )))); assert_eq!( resolve(accessor, connection).await?.err(), - Some(Error::Closed(Some("connection refused".to_string()))) + Some(ErrorCode::Closed(Some("connection refused".to_string()))) ); Ok(()) }) @@ -200,7 +204,7 @@ async fn binary_words_are_sent_as_given() -> wasmtime::Result<()> { let result = guest.call_emit(accessor, connection, reset).await?; assert_eq!( result, - Err(Error::Client( + Err(ErrorCode::Client( "RESET is rejected, use the connection's reset".to_string() )) ); @@ -217,7 +221,9 @@ async fn binary_words_are_sent_as_given() -> wasmtime::Result<()> { let (result, ()) = tokio::join!(call, server); assert_eq!( result?, - Err(Error::Valkey("ERR unknown command".to_string())) + Err(ErrorCode::Server(StringOrBytes::String( + "ERR unknown command".to_string() + ))) ); Ok(()) }) @@ -239,13 +245,46 @@ async fn error_replies_fail_the_exchange() -> wasmtime::Result<()> { let (result, ()) = tokio::join!(call, server); assert_eq!( result?, - Err(Error::Valkey("ERR value is not an integer".to_string())) + Err(ErrorCode::Server(StringOrBytes::String( + "ERR value is not an integer".to_string() + ))) ); Ok(()) }) .await } +#[tokio::test(flavor = "multi_thread")] +async fn bulk_errors_may_be_binary() -> wasmtime::Result<()> { + let mut harness = Harness::new("connection").await?; + harness + .run(async |accessor, exports| { + let guest = exports.connection(); + let (mut transport, connection) = establish(accessor, exports).await?; + for (reply, expected) in [ + // binary safe, like a bulk string + ( + b"ERR \x80".to_vec(), + StringOrBytes::Bytes(b"ERR \x80".to_vec()), + ), + ( + b"ERR text".to_vec(), + StringOrBytes::String("ERR text".to_string()), + ), + ] { + let call = guest.call_exchange(accessor, connection, words(&["PING"])); + let server = async { + transport.request().await; + transport.reply(RespValue::BulkError(reply)); + }; + let (result, ()) = tokio::join!(call, server); + assert_eq!(result?, Err(ErrorCode::Server(expected))); + } + Ok(()) + }) + .await +} + #[tokio::test(flavor = "multi_thread")] async fn emit_discards_its_reply() -> wasmtime::Result<()> { let mut harness = Harness::new("connection").await?; @@ -364,7 +403,7 @@ async fn closing_fails_waiting_and_later_exchanges() -> wasmtime::Result<()> { .run(async |accessor, exports| { let guest = exports.connection(); let (mut transport, connection) = establish(accessor, exports).await?; - let reset = Error::Closed(Some("connection reset".to_string())); + let reset = ErrorCode::Closed(Some("connection reset".to_string())); let call = guest.call_exchange(accessor, connection, words(&["GET", "a"])); let server = async { transport.request().await; @@ -431,7 +470,7 @@ async fn commands_the_connection_manages_are_rejected() -> wasmtime::Result<()> .run(async |accessor, exports| { let guest = exports.connection(); let (mut transport, connection) = establish(accessor, exports).await?; - let rejected = |message: &str| Err(Error::Client(message.to_string())); + let rejected = |message: &str| Err(ErrorCode::Client(message.to_string())); let reset = "RESET is rejected, use the connection's reset"; let hello = "HELLO is rejected, the connection sent it when it was established, see its server-properties"; let reply = "CLIENT REPLY is rejected, every command has a reply, see the connection's emit"; @@ -446,7 +485,7 @@ async fn commands_the_connection_manages_are_rejected() -> wasmtime::Result<()> let result = guest .call_transaction(accessor, connection, vec![words(&["RESET"])]) .await?; - assert_eq!(result.err(), Some(Error::Client(reset.to_string()))); + assert_eq!(result.err(), Some(ErrorCode::Client(reset.to_string()))); // subscribing with exchange is allowed until the connection has a subscription let result = guest @@ -641,7 +680,7 @@ async fn binary_property_names_fail_to_establish() -> wasmtime::Result<()> { )])); assert_eq!( resolve(accessor, connection).await?.err(), - Some(Error::Client( + Some(ErrorCode::Client( r#"unexpected property name in reply to HELLO: bulk-string"# .to_string() )) @@ -675,9 +714,9 @@ async fn error_replies_fail_a_subscribe() -> wasmtime::Result<()> { let (subscribed, got, ()) = tokio::join!(subscribe, get, server); assert_eq!( subscribed?, - Err(Error::Valkey( + Err(ErrorCode::Server(StringOrBytes::String( "NOPERM No permissions to access a channel".to_string() - )) + ))) ); assert_eq!(got?, Ok(bulk("1"))); assert_eq!( @@ -762,7 +801,9 @@ async fn reset_closes_the_connection_when_hello_fails() -> wasmtime::Result<()> .run(async |accessor, exports| { let guest = exports.connection(); let (mut transport, connection) = establish(accessor, exports).await?; - let wrongpass = Error::Valkey("WRONGPASS invalid username-password pair".to_string()); + let wrongpass = ErrorCode::Server(StringOrBytes::String( + "WRONGPASS invalid username-password pair".to_string(), + )); let reset = guest.call_reset(accessor, connection); let get = guest.call_exchange(accessor, connection, words(&["GET", "a"])); let server = async { @@ -809,9 +850,11 @@ async fn exchange_all_sends_the_commands_together() -> wasmtime::Result<()> { assert_eq!( results?, vec![ - Err(Error::Valkey("ERR oops".to_string())), + Err(ErrorCode::Server(StringOrBytes::String( + "ERR oops".to_string() + ))), // an empty command fails in its place, the others are sent - Err(Error::Client( + Err(ErrorCode::Client( "a command requires at least one word".to_string() )), Ok(bulk("1")), @@ -902,7 +945,9 @@ async fn transactions_wrap_the_commands_in_multi_and_exec() -> wasmtime::Result< let (result, ()) = tokio::join!(call, server); assert_eq!( result?, - Err(Error::Valkey("ERR unknown command 'NOPE'".to_string())) + Err(ErrorCode::Server(StringOrBytes::String( + "ERR unknown command 'NOPE'".to_string() + ))) ); // an empty command fails before MULTI is sent @@ -911,7 +956,7 @@ async fn transactions_wrap_the_commands_in_multi_and_exec() -> wasmtime::Result< .await?; assert_eq!( result, - Err(Error::Client( + Err(ErrorCode::Client( "a command requires at least one word".to_string() )) ); @@ -950,7 +995,9 @@ async fn watch_and_unwatch() -> wasmtime::Result<()> { let result = guest.call_watch(accessor, connection, vec![]).await?; assert_eq!( result, - Err(Error::Client("keys requires at least one item".to_string())) + Err(ErrorCode::Client( + "keys requires at least one item".to_string() + )) ); Ok(()) }) @@ -998,7 +1045,9 @@ async fn mixed_kinds_are_subscribed_with_a_command_each() -> wasmtime::Result<() let (result, ()) = tokio::join!(call, server); assert_eq!( result?, - Err(Error::Valkey("PSUBSCRIBE: NOPERM no patterns".to_string())) + Err(ErrorCode::Server(StringOrBytes::String( + "PSUBSCRIBE: NOPERM no patterns".to_string() + ))) ); for target in [channel("news"), channel("sports"), shardchannel("orders")] { assert_eq!( @@ -1179,7 +1228,7 @@ async fn targets_end_by_the_server_reset_and_closing() -> wasmtime::Result<()> { ); // closing the connection closes the stream with the error - let reason = Error::Closed(Some("connection reset".to_string())); + let reason = ErrorCode::Closed(Some("connection reset".to_string())); transport.close(Err(reason.clone())); assert_eq!(events.recv().await, None); assert_eq!(resolve(accessor, ended).await?, Err(reason)); @@ -1210,7 +1259,7 @@ mod with_valkey { exports: &Exports, connection: ResourceAny, command: &[&str], - ) -> wasmtime::Result> { + ) -> wasmtime::Result> { exports .connection() .call_exchange(accessor, connection, words(command)) @@ -1440,8 +1489,8 @@ mod with_valkey { let result = subscriptions .call_subscribe(accessor, subscription, targets) .await?; - let Err(Error::Valkey(message)) = result else { - wasmtime::bail!("expected a valkey error, got {result:?}"); + let Err(ErrorCode::Server(StringOrBytes::String(message))) = result else { + wasmtime::bail!("expected a server error, got {result:?}"); }; assert!( message.starts_with("PSUBSCRIBE: NOPERM") @@ -1564,8 +1613,8 @@ mod with_valkey { words(&["SET", "t:conn:leak", "1"]), ); let (reset, set) = tokio::join!(reset, set); - let wrongpass = |result: &Result<(), Error>| { - matches!(result, Err(Error::Valkey(message)) if message.starts_with("WRONGPASS")) + let wrongpass = |result: &Result<(), ErrorCode>| { + matches!(result, Err(ErrorCode::Server(StringOrBytes::String(message))) if message.starts_with("WRONGPASS")) }; assert!(wrongpass(&reset?), "the reset fails"); assert!(wrongpass(&set?.map(|_| ())), "the command after it fails"); @@ -1622,7 +1671,7 @@ mod with_valkey { while events.recv().await.is_some() {} assert!(matches!( resolve(accessor, ended).await?, - Err(Error::Closed(_)) + Err(ErrorCode::Closed(_)) )); assert_eq!( subscriptions.call_targets(accessor, subscription).await?, @@ -1632,7 +1681,7 @@ mod with_valkey { let result = subscriptions .call_subscribe(accessor, subscription, vec![channel("t:conn:k2")]) .await?; - assert!(matches!(result, Err(Error::Closed(_)))); + assert!(matches!(result, Err(ErrorCode::Closed(_)))); Ok(()) }) .await @@ -1653,7 +1702,7 @@ mod minimal { let result = guest.call_subscription(accessor, connection).await?; assert_eq!( result.err(), - Some(Error::Client( + Some(ErrorCode::Client( "subscriptions are not supported by this build of the connection, push messages arrive in dead-letters" .to_string() )) diff --git a/components/internal-cli/src/lib.rs b/components/internal-cli/src/lib.rs index 7c15684..ec6ff06 100644 --- a/components/internal-cli/src/lib.rs +++ b/components/internal-cli/src/lib.rs @@ -10,13 +10,13 @@ use std::future::{Future, poll_fn}; use std::pin::{Pin, pin}; use std::task::Poll; -use componentized::valkey::connection::{Connection, Error}; +use componentized::valkey::connection::{Connection, ErrorCode}; use componentized::valkey::connector; use componentized_valkey::types::{RespNestedValue, RespValue, StringOrBytes}; use exports::wasi::cli::run::Guest; use futures_channel::mpsc; use futures_core::Stream; -use wasi::cli::{environment, stderr, stdin, stdout, types::ErrorCode}; +use wasi::cli::{environment, stderr, stdin, stdout, types::ErrorCode as CliErrorCode}; use wit_bindgen::{FutureReader, StreamReader, StreamResult, StreamWriter}; const USAGE: &str = "\ @@ -67,7 +67,7 @@ impl Guest for Cli { } /// Sends the command, printing its reply, or the push messages a command without one causes. -async fn exec(command: Vec, out: &mut Output) -> Result<(), Error> { +async fn exec(command: Vec, out: &mut Output) -> Result<(), ErrorCode> { let connection = connector::connect().await?; // before the command is sent, so none of the push messages it causes are missed let mut pushes = connection.dead_letters(); @@ -105,16 +105,19 @@ async fn print_pushes( pushes: &mut StreamReader, out: &mut Output, limit: Option, -) -> Result<(), Error> { +) -> Result<(), ErrorCode> { for _ in 0..limit.unwrap_or(usize::MAX) { match pushes.next().await { // sent unprompted, e.g. for a SUBSCRIBE missing its channels - Some(RespValue::Error(message) | RespValue::BulkError(message)) => { - return Err(Error::Valkey(message)); + Some(RespValue::Error(message)) => { + return Err(ErrorCode::Server(StringOrBytes::String(message))); + } + Some(RespValue::BulkError(message)) => { + return Err(ErrorCode::Server(StringOrBytes::from_bytes(message))); } Some(push) => out.line(&render(push)).await, None => { - return Err(Error::Client( + return Err(ErrorCode::Client( "push messages stopped, the connection closed or they were not read fast enough" .to_string(), )); @@ -126,7 +129,7 @@ async fn print_pushes( /// Sends each line of stdin as a command, printing the replies, and push messages as they arrive, /// until stdin closes. -async fn session(out: &mut Output) -> Result<(), Error> { +async fn session(out: &mut Output) -> Result<(), ErrorCode> { let connection = connector::connect().await?; let pushes = connection.dead_letters(); let (stdin, _stdin_result) = stdin::read_via_stream(); @@ -163,7 +166,7 @@ async fn send_lines( connection: &Connection, stdin: StreamReader, lines: mpsc::UnboundedSender, -) -> Result<(), Error> { +) -> Result<(), ErrorCode> { let mut input = Lines::new(stdin); while let Some(line) = input.next().await { let words: Vec = match split(&line) { @@ -194,8 +197,8 @@ async fn send_lines( let text = match reply { Ok(reply) => render(reply), // the command failed, not the session - Err(Error::Valkey(message)) => format!("(error) {message}"), - Err(Error::Client(message)) => format!("(error) {message}"), + Err(ErrorCode::Server(message)) => format!("(error) {message}"), + Err(ErrorCode::Client(message)) => format!("(error) {message}"), Err(error) => return Err(error), }; let _ = lines.unbounded_send(text); @@ -333,11 +336,13 @@ fn name(command: &[StringOrBytes]) -> String { /// A stream to stdout or stderr. struct Output { writer: StreamWriter, - written: FutureReader>, + written: FutureReader>, } impl Output { - fn new(write_via_stream: fn(StreamReader) -> FutureReader>) -> Self { + fn new( + write_via_stream: fn(StreamReader) -> FutureReader>, + ) -> Self { let (writer, reader) = wit_stream::new(); let written = write_via_stream(reader); Output { writer, written } @@ -359,7 +364,7 @@ impl Output { /// Prints a line to a new stream to stdout or stderr. async fn print( - write_via_stream: fn(StreamReader) -> FutureReader>, + write_via_stream: fn(StreamReader) -> FutureReader>, text: &str, ) { let mut output = Output::new(write_via_stream); @@ -367,14 +372,14 @@ async fn print( output.close().await; } -fn render_error(error: Error) -> String { +fn render_error(error: ErrorCode) -> String { match error { - Error::Valkey(message) => format!("(error) {message}"), - Error::Closed(None) => "connection closed".to_string(), - Error::Closed(Some(reason)) => format!("connection closed: {reason}"), - Error::Client(message) => message, - Error::Resp(message) => format!("invalid RESP: {message}"), - Error::Lagged => "fell behind, values were lost".to_string(), + ErrorCode::Server(message) => format!("(error) {message}"), + ErrorCode::Closed(None) => "connection closed".to_string(), + ErrorCode::Closed(Some(reason)) => format!("connection closed: {reason}"), + ErrorCode::Client(message) => message, + ErrorCode::Resp(message) => format!("invalid RESP: {message}"), + ErrorCode::Lagged => "fell behind, values were lost".to_string(), } } @@ -382,7 +387,8 @@ fn render_error(error: Error) -> String { fn render(value: RespValue) -> String { match value { RespValue::String(text) => text, - RespValue::Error(message) | RespValue::BulkError(message) => format!("(error) {message}"), + RespValue::Error(message) => format!("(error) {message}"), + RespValue::BulkError(message) => format!("(error) {}", StringOrBytes::from_bytes(message)), RespValue::Integer(value) => format!("(integer) {value}"), // quoted, bytes outside printable ASCII escaped as valkey-cli does, binary or not RespValue::BulkString(bytes) => format!("\"{}\"", bytes.escape_ascii()), @@ -548,7 +554,7 @@ mod tests { "(error) ERR oops" ); assert_eq!( - render(RespValue::BulkError("SYNTAX".to_string())), + render(RespValue::BulkError(b"SYNTAX".to_vec())), "(error) SYNTAX" ); assert_eq!(render(RespValue::Integer(-1)), "(integer) -1"); @@ -640,19 +646,22 @@ mod tests { #[test] fn render_errors() { assert_eq!( - render_error(Error::Valkey("ERR oops".to_string())), + render_error(ErrorCode::Server("ERR oops".into())), "(error) ERR oops" ); - assert_eq!(render_error(Error::Closed(None)), "connection closed"); + assert_eq!(render_error(ErrorCode::Closed(None)), "connection closed"); assert_eq!( - render_error(Error::Closed(Some("reset".to_string()))), + render_error(ErrorCode::Closed(Some("reset".to_string()))), "connection closed: reset" ); - assert_eq!(render_error(Error::Client("bad".to_string())), "bad"); + assert_eq!(render_error(ErrorCode::Client("bad".to_string())), "bad"); assert_eq!( - render_error(Error::Resp("bad".to_string())), + render_error(ErrorCode::Resp("bad".to_string())), "invalid RESP: bad" ); - assert_eq!(render_error(Error::Lagged), "fell behind, values were lost"); + assert_eq!( + render_error(ErrorCode::Lagged), + "fell behind, values were lost" + ); } } diff --git a/components/tcp-connector/src/lib.rs b/components/tcp-connector/src/lib.rs index f2d9a88..da198fd 100644 --- a/components/tcp-connector/src/lib.rs +++ b/components/tcp-connector/src/lib.rs @@ -3,7 +3,7 @@ // the test harness provides a main #![cfg_attr(not(test), no_main)] -use componentized::valkey::connection::{ConnectOpts, Connection, DurationMilliseconds, Error}; +use componentized::valkey::connection::{ConnectOpts, Connection, DurationMilliseconds, ErrorCode}; use componentized::valkey::tcp; // the keys of the config and their defaults use componentized::valkey::tcp_connector_constants as keys; @@ -13,11 +13,11 @@ use wasi::config::store as config; struct Component; impl Guest for Component { - async fn connect() -> Result { + async fn connect() -> Result { let hostname = get(&keys::hostname_key())?.unwrap_or_else(keys::hostname_default); let port = match get(&keys::port_key())? { Some(port) => port.parse().map_err(|_| { - Error::Client(format!( + ErrorCode::Client(format!( "{} must be a port number: {port}", keys::port_key() )) @@ -34,7 +34,7 @@ impl Guest for Component { let keep_alive_interval = match get(&keys::keep_alive_interval_ms_key())? { Some(interval) => { let px = interval.parse().map_err(|_| { - Error::Client(format!( + ErrorCode::Client(format!( "{} must be a number of milliseconds: {interval}", keys::keep_alive_interval_ms_key() )) @@ -52,10 +52,12 @@ impl Guest for Component { } } -fn get(key: &str) -> Result, Error> { +fn get(key: &str) -> Result, ErrorCode> { config::get(key).map_err(|error| match error { - config::Error::Upstream(message) => Error::Client(format!("config upstream: {message}")), - config::Error::Io(message) => Error::Client(format!("config io: {message}")), + config::Error::Upstream(message) => { + ErrorCode::Client(format!("config upstream: {message}")) + } + config::Error::Io(message) => ErrorCode::Client(format!("config io: {message}")), }) } diff --git a/components/tcp-connector/tests/connector.rs b/components/tcp-connector/tests/connector.rs index 5e84753..e04fc8b 100644 --- a/components/tcp-connector/tests/connector.rs +++ b/components/tcp-connector/tests/connector.rs @@ -1,7 +1,7 @@ //! Tests for the `tcp-connector` component, composed with `tcp` and `connection` as `test-tcp`, //! configured with `wasi:config`, against a server played by the test over TCP. -use test_harness::{Client, Error, Harness, Server, command, hello_reply, ok, setinfo}; +use test_harness::{Client, ErrorCode, Harness, Server, command, hello_reply, ok, setinfo}; /// Answer the handshake of a connection, expecting `hello`. async fn handshake(client: &mut Client, hello: &[&str]) { @@ -82,7 +82,7 @@ async fn keep_alive_pings_an_idle_connection() -> wasmtime::Result<()> { } /// The error connecting with an invalid configuration, nothing is sent to a server. -async fn fails_with(config: &[(&str, &str)]) -> wasmtime::Result { +async fn fails_with(config: &[(&str, &str)]) -> wasmtime::Result { let mut harness = Harness::with_config("test-tcp", config).await?; harness .run(async |accessor, exports| { @@ -96,11 +96,13 @@ async fn fails_with(config: &[(&str, &str)]) -> wasmtime::Result { async fn invalid_configuration_fails_to_connect() -> wasmtime::Result<()> { assert_eq!( fails_with(&[("port", "http")]).await?, - Error::Client("port must be a port number: http".to_string()) + ErrorCode::Client("port must be a port number: http".to_string()) ); assert_eq!( fails_with(&[("keep-alive-interval-ms", "soon")]).await?, - Error::Client("keep-alive-interval-ms must be a number of milliseconds: soon".to_string()) + ErrorCode::Client( + "keep-alive-interval-ms must be a number of milliseconds: soon".to_string() + ) ); Ok(()) } @@ -110,6 +112,6 @@ async fn unreachable_servers_fail_to_connect() -> wasmtime::Result<()> { // a port nothing listens on let port = Server::bind().await?.port().to_string(); let error = fails_with(&[("port", &port)]).await?; - assert!(matches!(error, Error::Closed(Some(_))), "{error:?}"); + assert!(matches!(error, ErrorCode::Closed(Some(_))), "{error:?}"); Ok(()) } diff --git a/components/tcp/src/lib.rs b/components/tcp/src/lib.rs index ea33dbb..3a971e3 100644 --- a/components/tcp/src/lib.rs +++ b/components/tcp/src/lib.rs @@ -19,15 +19,15 @@ use std::future::poll_fn; use std::pin::Pin; use std::rc::Rc; -use componentized::valkey::connection::{ConnectOpts, Connection, Error}; +use componentized::valkey::connection::{ConnectOpts, Connection, ErrorCode}; use componentized_valkey::types::RespValue; use exports::componentized::valkey::tcp::{Guest, GuestTransport, Transport}; use futures_channel::{mpsc, oneshot}; use futures_core::Stream; use wasi::sockets::ip_name_lookup::{ErrorCode as LookupErrorCode, resolve_addresses}; use wasi::sockets::types::{ - ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, - TcpSocket, + ErrorCode as SocketErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, + Ipv6SocketAddress, TcpSocket, }; use wit_bindgen::{ FutureReader, FutureWriter, StreamReader, StreamResult, StreamWriter, spawn_local, @@ -45,7 +45,7 @@ impl Guest for Component { host: String, port: u16, opts: Option, - ) -> Result { + ) -> Result { let transport = open(&host, port).await?; let (replies, closed) = transport.receive(); let (requests, connection) = Connection::new(replies, closed, opts).await; @@ -60,18 +60,18 @@ pub struct TransportImpl { requests: RefCell< Option<( oneshot::Sender>, - FutureReader>, + FutureReader>, )>, >, - replies: RefCell, FutureReader>)>>, + replies: RefCell, FutureReader>)>>, /// Futures for the resolver to fail, a sync method can't write them itself. - failures: mpsc::UnboundedSender<(FutureWriter>, &'static str)>, + failures: mpsc::UnboundedSender<(FutureWriter>, &'static str)>, } impl TransportImpl { /// A future the resolver fails with `message`. - fn failed(&self, message: &'static str) -> FutureReader> { - let (writer, reader) = wit_future::new(|| Err(Error::Closed(None))); + fn failed(&self, message: &'static str) -> FutureReader> { + let (writer, reader) = wit_future::new(|| Err(ErrorCode::Closed(None))); // a transport whose resolver stopped drops the writer, resolving it to the default let _ = self.failures.unbounded_send((writer, message)); reader @@ -79,11 +79,11 @@ impl TransportImpl { } impl GuestTransport for TransportImpl { - async fn open(host: String, port: u16) -> Result { + async fn open(host: String, port: u16) -> Result { Ok(Transport::new(open(&host, port).await?)) } - fn send(&self, requests: StreamReader) -> FutureReader> { + fn send(&self, requests: StreamReader) -> FutureReader> { match self.requests.borrow_mut().take() { Some((pump, sent)) => { // the pump only stops waiting for the requests once the socket is closed @@ -94,7 +94,7 @@ impl GuestTransport for TransportImpl { } } - fn receive(&self) -> (StreamReader, FutureReader>) { + fn receive(&self) -> (StreamReader, FutureReader>) { self.replies.borrow_mut().take().unwrap_or_else(|| { let (_, replies) = wit_stream::new(); (replies, self.failed("receive may be called at most once")) @@ -104,15 +104,17 @@ impl GuestTransport for TransportImpl { /// Fail each future handed over, until the transport is dropped. async fn resolve_failures( - mut failures: mpsc::UnboundedReceiver<(FutureWriter>, &'static str)>, + mut failures: mpsc::UnboundedReceiver<(FutureWriter>, &'static str)>, ) { while let Some((writer, message)) = poll_fn(|cx| Pin::new(&mut failures).poll_next(cx)).await { - let _ = writer.write(Err(Error::Client(message.to_string()))).await; + let _ = writer + .write(Err(ErrorCode::Client(message.to_string()))) + .await; } } /// Connect a socket to `host` and start its pumps. -async fn open(host: &str, port: u16) -> Result { +async fn open(host: &str, port: u16) -> Result { let socket = Rc::new(connect_socket(host, port).await?); let (bytes_out, bytes_out_reader) = wit_stream::new(); @@ -121,8 +123,8 @@ async fn open(host: &str, port: u16) -> Result { let (replies_writer, replies) = wit_stream::new(); // the defaults are only written if a pump stops without a result, the socket closed under it - let (closed_writer, closed) = wit_future::new(|| Err(Error::Closed(None))); - let (sent_writer, sent) = wit_future::new(|| Err(Error::Closed(None))); + let (closed_writer, closed) = wit_future::new(|| Err(ErrorCode::Closed(None))); + let (sent_writer, sent) = wit_future::new(|| Err(ErrorCode::Closed(None))); let (requests_tx, requests_rx) = oneshot::channel(); let (failures_tx, failures_rx) = mpsc::unbounded(); @@ -154,9 +156,9 @@ async fn open(host: &str, port: u16) -> Result { async fn receive_pump( _socket: Rc, mut bytes: StreamReader, - received: FutureReader>, + received: FutureReader>, mut replies: StreamWriter, - closed: FutureWriter>, + closed: FutureWriter>, ) { // bytes read from the socket that are not part of a whole value yet let mut buffered: Vec = Vec::new(); @@ -176,11 +178,11 @@ async fn receive_pump( break 'pump Ok(()); } } - Err(error) => break 'pump Err(Error::Resp(error)), + Err(error) => break 'pump Err(ErrorCode::Resp(error)), } } Ok(None) => break, - Err(error) => break 'pump Err(Error::Resp(error)), + Err(error) => break 'pump Err(ErrorCode::Resp(error)), } } buffered.drain(..start); @@ -203,8 +205,8 @@ async fn send_pump( _socket: Rc, requests: oneshot::Receiver>, mut bytes: StreamWriter, - socket_sent: FutureReader>, - sent: FutureWriter>, + socket_sent: FutureReader>, + sent: FutureWriter>, ) { // a transport dropped without sending half-closes the socket let Ok(mut requests) = requests.await else { @@ -226,7 +228,7 @@ async fn send_pump( } /// Connect a socket to the first address of `host` that accepts it. -async fn connect_socket(host: &str, port: u16) -> Result { +async fn connect_socket(host: &str, port: u16) -> Result { let mut last_error = None; for address in resolve(host, port).await? { let family = match address { @@ -243,7 +245,7 @@ async fn connect_socket(host: &str, port: u16) -> Result { Some(error) => format!("unable to connect to {host}:{port}: {error:?}"), None => format!("unable to connect to {host}:{port}: no addresses"), }; - Err(Error::Closed(Some(reason))) + Err(ErrorCode::Closed(Some(reason))) } /// The socket addresses of `host`, an IP address or a hostname. @@ -251,7 +253,7 @@ async fn connect_socket(host: &str, port: u16) -> Result { /// An IP address is resolved too, wasi:sockets returns it as is, so the lookup vouches for each /// address a socket connects to, e.g. for a gate that only allows connecting to looked up /// addresses. -async fn resolve(host: &str, port: u16) -> Result, Error> { +async fn resolve(host: &str, port: u16) -> Result, ErrorCode> { let addresses = resolve_addresses(host.to_string()) .await .map_err(|error| lookup_error(host, error))?; @@ -269,12 +271,12 @@ async fn resolve(host: &str, port: u16) -> Result, Error> { .collect()) } -fn socket_error(error: ErrorCode) -> Error { - Error::Closed(Some(format!("{error:?}"))) +fn socket_error(error: SocketErrorCode) -> ErrorCode { + ErrorCode::Closed(Some(format!("{error:?}"))) } -fn lookup_error(host: &str, error: LookupErrorCode) -> Error { - Error::Closed(Some(format!("unable to resolve {host}: {error:?}"))) +fn lookup_error(host: &str, error: LookupErrorCode) -> ErrorCode { + ErrorCode::Closed(Some(format!("unable to resolve {host}: {error:?}"))) } wit_bindgen::generate!({ diff --git a/components/tcp/tests/tcp.rs b/components/tcp/tests/tcp.rs index efc8ef5..a3d5345 100644 --- a/components/tcp/tests/tcp.rs +++ b/components/tcp/tests/tcp.rs @@ -2,7 +2,7 @@ //! played by the test over TCP. use test_harness::{ - Client, Ctx, Error, Exports, Harness, RespValue, Server, bulk, command, hello_reply, ok, + Client, Ctx, ErrorCode, Exports, Harness, RespValue, Server, bulk, command, hello_reply, ok, resolve, setinfo, words, }; use wasmtime::component::{Accessor, ResourceAny}; @@ -140,7 +140,7 @@ async fn server_closing_fails_exchanges() -> wasmtime::Result<()> { .call_exchange(accessor, connection, words(&["PING"])) .await?; assert!( - matches!(result, Err(Error::Closed(_))), + matches!(result, Err(ErrorCode::Closed(_))), "expected a closed error, got {result:?}" ); Ok(()) @@ -159,7 +159,7 @@ async fn refused_connections_fail_to_connect() -> wasmtime::Result<()> { .tcp() .call_connect(accessor, "127.0.0.1".to_string(), port, None) .await?; - let Err(Error::Closed(Some(reason))) = result else { + let Err(ErrorCode::Closed(Some(reason))) = result else { panic!("expected a closed error, got {result:?}"); }; assert!( @@ -203,7 +203,7 @@ async fn transports_wire_to_connections() -> wasmtime::Result<()> { let (_, again) = transport.call_receive(accessor, transport_handle).await?; assert!(matches!( resolve(accessor, again).await?, - Err(Error::Client(_)) + Err(ErrorCode::Client(_)) )); Ok(()) }) diff --git a/components/test-commands-suite/src/lib.rs b/components/test-commands-suite/src/lib.rs index 27d1367..9082069 100644 --- a/components/test-commands-suite/src/lib.rs +++ b/components/test-commands-suite/src/lib.rs @@ -10,7 +10,7 @@ use std::pin::pin; use std::task::Poll; use componentized::valkey::commands; -use componentized::valkey::connection::{Connection, Error}; +use componentized::valkey::connection::{Connection, ErrorCode}; use componentized::valkey::connector; use componentized_valkey::types::{ AclRule, AclUserRule, Bytes, Cursor, DurationMilliseconds, DurationSeconds, @@ -160,7 +160,7 @@ struct Server { requests: StreamReader, replies: StreamWriter, /// Resolved to `ok` when the server is dropped, the transport closed gracefully. - _closed: FutureWriter>, + _closed: FutureWriter>, } impl Server { @@ -768,11 +768,11 @@ async fn invalid_arguments() -> Result<(), String> { let (mut server, conn) = establish().await?; check_eq!( commands::del(&conn, vec![]).await, - Err(Error::Client(s("keys requires at least one item"))) + Err(ErrorCode::Client(s("keys requires at least one item"))) ); check_eq!( commands::scan(&conn, Cursor::Resume(s("0")), None, None, None).await, - Err(Error::Client(s( + Err(ErrorCode::Client(s( "resume requires the cursor a scan returned, use start to begin a scan" ))) ); @@ -797,7 +797,7 @@ async fn unexpected_replies() -> Result<(), String> { .await?; check_eq!( result, - Err(Error::Client(s("unexpected reply: integer<7>"))) + Err(ErrorCode::Client(s("unexpected reply: integer<7>"))) ); Ok(()) } @@ -814,9 +814,9 @@ async fn error_replies() -> Result<(), String> { .await?; check_eq!( result, - Err(Error::Valkey(s( - "WRONGTYPE Operation against a key holding the wrong kind of value" - ))) + Err(ErrorCode::Server( + "WRONGTYPE Operation against a key holding the wrong kind of value".into() + )) ); Ok(()) } @@ -826,7 +826,7 @@ async fn with_valkey() -> Result<(), String> { let conn = connector::connect() .await .map_err(|error| format!("failed to connect: {error:?}"))?; - let check = |result: Result<(), Error>| result.map_err(|error| format!("{error:?}")); + let check = |result: Result<(), ErrorCode>| result.map_err(|error| format!("{error:?}")); let key = || s("commands-test:string"); check(commands::ping(&conn, None).await)?; diff --git a/crates/componentized-valkey/src/command.rs b/crates/componentized-valkey/src/command.rs index 90b910a..90c39e0 100644 --- a/crates/componentized-valkey/src/command.rs +++ b/crates/componentized-valkey/src/command.rs @@ -1,7 +1,7 @@ //! Converting Rust values into the words of a command, the impls for Rust's own types and the //! helpers the commands share. The impls for the package's types are in [`crate::commands`]. -use crate::types::{Error, StringOrBytes}; +use crate::types::{ErrorCode, StringOrBytes}; /// The words of a command, each a string, or bytes, e.g. a binary value. pub type Words = Vec; @@ -14,30 +14,30 @@ pub type Words = Vec; /// A value the command does not accept, e.g. an empty list where it requires an item, fails with a /// client error, and the command is not sent. pub trait IntoCommand { - fn into_command(self) -> Result; + fn into_command(self) -> Result; } /// A value as given, UTF-8 or not. impl IntoCommand for StringOrBytes { - fn into_command(self) -> Result { + fn into_command(self) -> Result { Ok(vec![self]) } } impl IntoCommand for &str { - fn into_command(self) -> Result { + fn into_command(self) -> Result { Ok(vec![self.into()]) } } impl IntoCommand for String { - fn into_command(self) -> Result { + fn into_command(self) -> Result { Ok(vec![self.into()]) } } impl IntoCommand for &String { - fn into_command(self) -> Result { + fn into_command(self) -> Result { Ok(vec![self.as_str().into()]) } } @@ -47,7 +47,7 @@ macro_rules! into_command_numbers { ($($number:ty),*) => { $( impl IntoCommand for $number { - fn into_command(self) -> Result { + fn into_command(self) -> Result { Ok(vec![self.to_string().into()]) } } @@ -59,13 +59,13 @@ into_command_numbers!(i32, i64, u8, u16, u32, u64, usize, f64); /// No word for none. impl IntoCommand for Option { - fn into_command(self) -> Result { + fn into_command(self) -> Result { self.map_or(Ok(vec![]), IntoCommand::into_command) } } impl IntoCommand for Vec { - fn into_command(self) -> Result { + fn into_command(self) -> Result { let mut command = vec![]; for item in self { command.extend(item.into_command()?); @@ -75,7 +75,7 @@ impl IntoCommand for Vec { } impl IntoCommand for [T; N] { - fn into_command(self) -> Result { + fn into_command(self) -> Result { let mut command = vec![]; for item in self { command.extend(item.into_command()?); @@ -90,7 +90,7 @@ macro_rules! into_command_tuples { $( impl<$($item: IntoCommand),+> IntoCommand for ($($item,)+) { #[allow(non_snake_case)] - fn into_command(self) -> Result { + fn into_command(self) -> Result { let ($($item,)+) = self; let mut command = vec![]; $(command.extend($item.into_command()?);)+ @@ -120,7 +120,7 @@ into_command_tuples!( pub struct Flag(pub &'static str, pub bool); impl IntoCommand for Flag { - fn into_command(self) -> Result { + fn into_command(self) -> Result { let Flag(word, present) = self; Ok(if present { vec![word.into()] } else { vec![] }) } @@ -130,7 +130,7 @@ impl IntoCommand for Flag { pub struct NamedOption(pub &'static str, pub Option); impl IntoCommand for NamedOption { - fn into_command(self) -> Result { + fn into_command(self) -> Result { let NamedOption(name, value) = self; match value { Some(_) => (name, value).into_command(), @@ -159,10 +159,12 @@ impl NonEmpty { } impl IntoCommand for NonEmpty { - fn into_command(self) -> Result { + fn into_command(self) -> Result { let NonEmpty(name, items) = self; if items.is_empty() { - return Err(Error::Client(format!("{name} requires at least one item"))); + return Err(ErrorCode::Client(format!( + "{name} requires at least one item" + ))); } items.into_command() } @@ -173,7 +175,7 @@ impl IntoCommand for NonEmpty { pub struct NumKeys(pub NonEmpty); impl IntoCommand for NumKeys { - fn into_command(self) -> Result { + fn into_command(self) -> Result { (self.0.len(), self.0).into_command() } } diff --git a/crates/componentized-valkey/src/commands/admin.rs b/crates/componentized-valkey/src/commands/admin.rs index 85062fa..a7a318f 100644 --- a/crates/componentized-valkey/src/commands/admin.rs +++ b/crates/componentized-valkey/src/commands/admin.rs @@ -7,16 +7,16 @@ use crate::types::{ AclLogEntry, AclRule, AclSelectorRule, AclUserRule, BgsaveOperation, ClientFilter, ClientPauseMode, ClientType, ClientUnblockType, ClusterBumpepochResult, ClusterFailoverMode, ClusterGetslotmigrationsEntry, ClusterGetslotmigrationsOperation, ClusterMigrateslotsGroup, - ClusterResetMode, ClusterSetslotState, CommandlogEntry, CommandlogType, Error, FailoverTarget, - Integer, LatencyHistogramEntry, LatencyLatestEntry, ModuleListEntry, RespValue, RolePrimary, - RolePrimaryReplica, RoleReplica, RoleReply, ShutdownSave, + ClusterResetMode, ClusterSetslotState, CommandlogEntry, CommandlogType, ErrorCode, + FailoverTarget, Integer, LatencyHistogramEntry, LatencyLatestEntry, ModuleListEntry, RespValue, + RolePrimary, RolePrimaryReplica, RoleReplica, RoleReply, ShutdownSave, }; use crate::value::{Fields, FlatPairs, FromValue, items, parse_string}; use wit_bindgen::rt::Map; impl IntoCommand for AclRule { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { AclRule::Keys(rule) => rule.into_command(), AclRule::Channel(rule) => rule.into_command(), @@ -29,7 +29,7 @@ impl IntoCommand for AclRule { } impl IntoCommand for AclSelectorRule { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { AclSelectorRule::Keys(rule) => rule.into_command(), AclSelectorRule::Channel(rule) => rule.into_command(), @@ -41,7 +41,7 @@ impl IntoCommand for AclSelectorRule { } impl IntoCommand for AclKeyRule { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { AclKeyRule::Add(pattern) => format!("~{pattern}"), AclKeyRule::AddRead(pattern) => format!("%R~{pattern}"), @@ -55,7 +55,7 @@ impl IntoCommand for AclKeyRule { } impl IntoCommand for AclChannelRule { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { AclChannelRule::Add(pattern) => format!("&{pattern}"), AclChannelRule::All => "allchannels".to_string(), @@ -66,7 +66,7 @@ impl IntoCommand for AclChannelRule { } impl IntoCommand for AclCommandRule { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { AclCommandRule::Add(command) => format!("+{command}"), AclCommandRule::AddCategory(category) => format!("+@{category}"), @@ -82,10 +82,12 @@ impl IntoCommand for AclCommandRule { /// `db=` requires at least one database. impl IntoCommand for AclDbRule { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { AclDbRule::Db(dbs) if dbs.is_empty() => { - return Err(Error::Client("db requires at least one item".to_string())); + return Err(ErrorCode::Client( + "db requires at least one item".to_string(), + )); } AclDbRule::Db(dbs) => format!( "db={}", @@ -102,7 +104,7 @@ impl IntoCommand for AclDbRule { } impl IntoCommand for AclUserRule { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { AclUserRule::On => "on".into_command(), AclUserRule::Off => "off".into_command(), @@ -122,7 +124,7 @@ impl IntoCommand for AclUserRule { /// `OK` when the user may run the command, otherwise why not. impl FromValue for AclDryrunResult { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match value { RespValue::String(reply) if reply == "OK" => Ok(AclDryrunResult::Allowed), value => String::from_value(value).map(AclDryrunResult::Denied), @@ -135,7 +137,7 @@ impl FromValue for AclDryrunResult { pub struct AclUserRules(pub Vec); impl FromValue for AclUserRules { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; let flags: Vec = fields.required("flags")?; let passwords: Vec = fields.required("passwords")?; @@ -160,7 +162,7 @@ impl FromValue for AclUserRules { /// /// The channels are preceded by `resetchannels`, as ACL LIST writes them, so the server's /// `acl-pubsub-default` does not add to them. -fn selector_rules(fields: &mut Fields) -> Result, Error> { +fn selector_rules(fields: &mut Fields) -> Result, ErrorCode> { let keys: String = fields.required("keys")?; let channels: String = fields.required("channels")?; // since Valkey 9.1 @@ -254,7 +256,7 @@ fn command_rule(rule: &str) -> AclSelectorRule { pub struct AclList(pub Vec); impl FromValue for AclList { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let lines: Vec = FromValue::from_value(value)?; lines .iter() @@ -264,8 +266,8 @@ impl FromValue for AclList { } } -fn acl_list_entry(line: &str) -> Result { - let invalid = || Error::Client(format!("unexpected ACL LIST line: {line:?}")); +fn acl_list_entry(line: &str) -> Result { + let invalid = || ErrorCode::Client(format!("unexpected ACL LIST line: {line:?}")); let mut words = line .strip_prefix("user ") .ok_or_else(invalid)? @@ -347,7 +349,7 @@ fn root_rule(rule: AclSelectorRule) -> AclRule { } impl FromValue for AclLogEntry { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { count: fields.required("count")?, @@ -366,7 +368,7 @@ impl FromValue for AclLogEntry { } impl IntoCommand for BgsaveOperation { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { BgsaveOperation::Schedule => "SCHEDULE", BgsaveOperation::Cancel => "CANCEL", @@ -377,7 +379,7 @@ impl IntoCommand for BgsaveOperation { /// `master` rather than `primary`, which servers before Valkey 8.0 do not know. impl IntoCommand for ClientType { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { ClientType::Normal => "normal", ClientType::Primary => "master", @@ -389,7 +391,7 @@ impl IntoCommand for ClientType { } impl IntoCommand for ClientFilter { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { ClientFilter::Id(ids) => ("ID", NonEmpty::new("id", ids)).into_command(), ClientFilter::Type(client_type) => ("TYPE", client_type).into_command(), @@ -429,7 +431,7 @@ impl IntoCommand for ClientFilter { pub struct ClientList(pub Vec>); impl FromValue for ClientList { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let text = String::from_value(value)?; let clients = text .lines() @@ -446,7 +448,7 @@ impl FromValue for ClientList { } impl IntoCommand for ClientPauseMode { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { ClientPauseMode::Write => "WRITE", ClientPauseMode::All => "ALL", @@ -456,7 +458,7 @@ impl IntoCommand for ClientPauseMode { } impl IntoCommand for ClientUnblockType { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { ClientUnblockType::Timeout => "TIMEOUT", ClientUnblockType::Error => "ERROR", @@ -467,9 +469,9 @@ impl IntoCommand for ClientUnblockType { /// `BUMPED epoch` or `STILL epoch`. impl FromValue for ClusterBumpepochResult { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let reply = String::from_value(value)?; - let invalid = || Error::Client(format!("unexpected reply: {reply:?}")); + let invalid = || ErrorCode::Client(format!("unexpected reply: {reply:?}")); let (outcome, epoch) = reply.split_once(' ').ok_or_else(invalid)?; let epoch = epoch.parse().map_err(|_| invalid())?; match outcome { @@ -481,7 +483,7 @@ impl FromValue for ClusterBumpepochResult { } impl IntoCommand for ClusterFailoverMode { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { ClusterFailoverMode::Force => "FORCE", ClusterFailoverMode::Takeover => "TAKEOVER", @@ -491,11 +493,11 @@ impl IntoCommand for ClusterFailoverMode { } impl FromValue for ClusterGetslotmigrationsOperation { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match String::from_value(value)?.as_str() { "IMPORT" => Ok(ClusterGetslotmigrationsOperation::Import), "EXPORT" => Ok(ClusterGetslotmigrationsOperation::Export), - operation => Err(Error::Client(format!( + operation => Err(ErrorCode::Client(format!( "unexpected operation: {operation:?}" ))), } @@ -506,9 +508,9 @@ impl FromValue for ClusterGetslotmigrationsOperation { pub struct SlotRanges(pub Vec<(u16, u16)>); impl FromValue for SlotRanges { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let ranges = String::from_value(value)?; - let invalid = || Error::Client(format!("unexpected slot ranges: {ranges:?}")); + let invalid = || ErrorCode::Client(format!("unexpected slot ranges: {ranges:?}")); ranges .split_whitespace() .map(|range| { @@ -524,7 +526,7 @@ impl FromValue for SlotRanges { } impl FromValue for ClusterGetslotmigrationsEntry { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; let slot_ranges: SlotRanges = fields.required("slot_ranges")?; Ok(Self { @@ -545,7 +547,7 @@ impl FromValue for ClusterGetslotmigrationsEntry { /// `SLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] NODE node-id [ AUTH username password ]` impl IntoCommand for ClusterMigrateslotsGroup { - fn into_command(self) -> Result { + fn into_command(self) -> Result { ( "SLOTSRANGE", NonEmpty::new("ranges", self.ranges), @@ -559,7 +561,7 @@ impl IntoCommand for ClusterMigrateslotsGroup { } impl IntoCommand for ClusterResetMode { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { ClusterResetMode::Hard => "HARD", ClusterResetMode::Soft => "SOFT", @@ -569,7 +571,7 @@ impl IntoCommand for ClusterResetMode { } impl IntoCommand for ClusterSetslotState { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { ClusterSetslotState::Importing(node_id) => ("IMPORTING", node_id).into_command(), ClusterSetslotState::Migrating(node_id) => ("MIGRATING", node_id).into_command(), @@ -580,7 +582,7 @@ impl IntoCommand for ClusterSetslotState { } impl IntoCommand for CommandlogType { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { CommandlogType::Slow => "slow", CommandlogType::LargeRequest => "large-request", @@ -593,7 +595,7 @@ impl IntoCommand for CommandlogType { /// `[id, timestamp, value, [argument ...], client-address, client-name]`, of SLOWLOG GET and /// COMMANDLOG GET. impl FromValue for CommandlogEntry { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match <[RespValue; 6]>::try_from(items(value)?) { Ok([id, timestamp, value, arguments, client_address, client_name]) => Ok(Self { id: FromValue::from_value(id)?, @@ -603,7 +605,7 @@ impl FromValue for CommandlogEntry { client_address: FromValue::from_value(client_address)?, client_name: FromValue::from_value(client_name)?, }), - Err(items) => Err(Error::Client(format!( + Err(items) => Err(ErrorCode::Client(format!( "unexpected number of items, expected 6: {}", items.len() ))), @@ -613,13 +615,13 @@ impl FromValue for CommandlogEntry { /// `TO host port [ FORCE ]` impl IntoCommand for FailoverTarget { - fn into_command(self) -> Result { + fn into_command(self) -> Result { ("TO", self.host, self.port, Flag("FORCE", self.force)).into_command() } } impl FromValue for LatencyHistogramEntry { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; // a map of each bucket to its cumulative count let histogram: FlatPairs = fields.required("histogram_usec")?; @@ -632,7 +634,7 @@ impl FromValue for LatencyHistogramEntry { /// `[event, timestamp, latest, max, sum, count]` impl FromValue for LatencyLatestEntry { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match <[RespValue; 6]>::try_from(items(value)?) { Ok([event, timestamp, latest, max, sum, count]) => Ok(Self { event: FromValue::from_value(event)?, @@ -642,7 +644,7 @@ impl FromValue for LatencyLatestEntry { sum: FromValue::from_value(sum)?, count: FromValue::from_value(count)?, }), - Err(items) => Err(Error::Client(format!( + Err(items) => Err(ErrorCode::Client(format!( "unexpected number of items, expected 6: {}", items.len() ))), @@ -651,7 +653,7 @@ impl FromValue for LatencyLatestEntry { } impl FromValue for ModuleListEntry { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { name: fields.required("name")?, @@ -664,7 +666,7 @@ impl FromValue for ModuleListEntry { /// `[ip, port, offset]`, the port and offset are strings. impl FromValue for RolePrimaryReplica { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let (ip, port, offset) = FromValue::from_value(value)?; Ok(Self { ip, @@ -677,13 +679,13 @@ impl FromValue for RolePrimaryReplica { /// `["master", offset, [replica ...]]`, `["slave", host, port, state, offset]` or /// `["sentinel", [primary ...]]`. impl FromValue for RoleReply { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut items = items(value)?.into_iter(); let role = String::from_value(items.next().unwrap_or(RespValue::Null))?; let mut next = || { items .next() - .ok_or_else(|| Error::Client(format!("too few items for the role {role:?}"))) + .ok_or_else(|| ErrorCode::Client(format!("too few items for the role {role:?}"))) }; match role.as_str() { "master" => Ok(RoleReply::Primary(RolePrimary { @@ -694,18 +696,18 @@ impl FromValue for RoleReply { primary_host: FromValue::from_value(next()?)?, primary_port: u64::from_value(next()?)? .try_into() - .map_err(|_| Error::Client("unexpected port".to_string()))?, + .map_err(|_| ErrorCode::Client("unexpected port".to_string()))?, state: FromValue::from_value(next()?)?, offset: FromValue::from_value(next()?)?, })), "sentinel" => Ok(RoleReply::Sentinel(FromValue::from_value(next()?)?)), - role => Err(Error::Client(format!("unexpected role: {role:?}"))), + role => Err(ErrorCode::Client(format!("unexpected role: {role:?}"))), } } } impl IntoCommand for ShutdownSave { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { ShutdownSave::Nosave => "NOSAVE", ShutdownSave::Save => "SAVE", diff --git a/crates/componentized-valkey/src/commands/bitmap.rs b/crates/componentized-valkey/src/commands/bitmap.rs index e552ee7..9a9984f 100644 --- a/crates/componentized-valkey/src/commands/bitmap.rs +++ b/crates/componentized-valkey/src/commands/bitmap.rs @@ -3,12 +3,12 @@ use crate::command::{IntoCommand, Words}; use crate::types::{ BitRange, BitfieldEncoding, BitfieldGet, BitfieldIncrby, BitfieldOffset, BitfieldOperation, - BitfieldOverflow, BitfieldSet, Error, + BitfieldOverflow, BitfieldSet, ErrorCode, }; /// `start [ end [ BIT ] ]`, BYTE is the default, and left out for servers before it existed. impl IntoCommand for BitRange { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { BitRange::From(start) => start.into_command(), BitRange::Bytes((start, end)) => (start, end).into_command(), @@ -19,14 +19,14 @@ impl IntoCommand for BitRange { /// `i1` to `i64`, `u1` to `u63`, other widths fail. impl IntoCommand for BitfieldEncoding { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { BitfieldEncoding::Signed(width @ 1..=64) => format!("i{width}").into_command(), BitfieldEncoding::Unsigned(width @ 1..=63) => format!("u{width}").into_command(), - BitfieldEncoding::Signed(width) => Err(Error::Client(format!( + BitfieldEncoding::Signed(width) => Err(ErrorCode::Client(format!( "a signed bitfield is 1 to 64 bits wide, not {width}" ))), - BitfieldEncoding::Unsigned(width) => Err(Error::Client(format!( + BitfieldEncoding::Unsigned(width) => Err(ErrorCode::Client(format!( "an unsigned bitfield is 1 to 63 bits wide, not {width}" ))), } @@ -35,7 +35,7 @@ impl IntoCommand for BitfieldEncoding { /// The offset in bits, or `#` and the index of a field of the encoding's width. impl IntoCommand for BitfieldOffset { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { BitfieldOffset::Bit(offset) => offset.into_command(), BitfieldOffset::Field(field) => format!("#{field}").into_command(), @@ -44,7 +44,7 @@ impl IntoCommand for BitfieldOffset { } impl IntoCommand for BitfieldOverflow { - fn into_command(self) -> Result { + fn into_command(self) -> Result { let overflow = match self { BitfieldOverflow::Wrap => "WRAP", BitfieldOverflow::Sat => "SAT", @@ -56,26 +56,26 @@ impl IntoCommand for BitfieldOverflow { /// Also `BitfieldRoGet`, the same type. impl IntoCommand for BitfieldGet { - fn into_command(self) -> Result { + fn into_command(self) -> Result { ("GET", self.encoding, self.offset).into_command() } } impl IntoCommand for BitfieldSet { - fn into_command(self) -> Result { + fn into_command(self) -> Result { ("SET", self.encoding, self.offset, self.value).into_command() } } impl IntoCommand for BitfieldIncrby { - fn into_command(self) -> Result { + fn into_command(self) -> Result { ("INCRBY", self.encoding, self.offset, self.increment).into_command() } } /// A write is preceded by its overflow mode, when given. impl IntoCommand for BitfieldOperation { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { BitfieldOperation::Get(get) => get.into_command(), BitfieldOperation::Set((overflow, set)) => (overflow, set).into_command(), @@ -88,7 +88,7 @@ impl IntoCommand for BitfieldOperation { pub struct Bit(pub bool); impl IntoCommand for Bit { - fn into_command(self) -> Result { + fn into_command(self) -> Result { if self.0 { "1" } else { "0" }.into_command() } } diff --git a/crates/componentized-valkey/src/commands/connection.rs b/crates/componentized-valkey/src/commands/connection.rs index 0c11ce2..8121b99 100644 --- a/crates/componentized-valkey/src/commands/connection.rs +++ b/crates/componentized-valkey/src/commands/connection.rs @@ -5,13 +5,13 @@ use crate::types::{ ClientSetinfoAttr, ClientTrackingMode, ClientTrackingOptions, ClientTrackinginfoRedirect, ClientTrackinginfoReply, CommandDocsArgument, CommandDocsArgumentType, CommandDocsReply, CommandInfoReply, CommandKeySpec, CommandKeySpecBeginSearch, CommandKeySpecFindKeys, - CommandKeySpecKeynum, CommandKeySpecKeyword, CommandKeySpecRange, CommandListFilter, Error, + CommandKeySpecKeynum, CommandKeySpecKeyword, CommandKeySpecRange, CommandListFilter, ErrorCode, Integer, RespValue, }; use crate::value::{Fields, FlatPairs, FromValue, entries, items}; impl IntoCommand for ClientSetinfoAttr { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { ClientSetinfoAttr::LibName(name) => ("LIB-NAME", name).into_command(), ClientSetinfoAttr::LibVer(version) => ("LIB-VER", version).into_command(), @@ -20,7 +20,7 @@ impl IntoCommand for ClientSetinfoAttr { } impl IntoCommand for ClientTrackingMode { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { ClientTrackingMode::Optin => "OPTIN", ClientTrackingMode::Optout => "OPTOUT", @@ -31,7 +31,7 @@ impl IntoCommand for ClientTrackingMode { /// `[ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN | OPTOUT ] [ NOLOOP ]` impl IntoCommand for ClientTrackingOptions { - fn into_command(self) -> Result { + fn into_command(self) -> Result { // PREFIX before each prefix let prefixes = self.prefixes.map(|prefixes| { let prefixes = prefixes.into_iter().map(|prefix| ("PREFIX", prefix)); @@ -49,18 +49,18 @@ impl IntoCommand for ClientTrackingOptions { /// `-1` when tracking is off, `0` when not redirected, otherwise the ID of the connection. impl FromValue for ClientTrackinginfoRedirect { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match Integer::from_value(value)? { -1 => Ok(ClientTrackinginfoRedirect::NotTracking), 0 => Ok(ClientTrackinginfoRedirect::NotRedirected), id if id > 0 => Ok(ClientTrackinginfoRedirect::Redirected(id)), - code => Err(Error::Client(format!("unexpected reply: {code}"))), + code => Err(ErrorCode::Client(format!("unexpected reply: {code}"))), } } } impl FromValue for ClientTrackinginfoReply { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { flags: fields.required("flags")?, @@ -72,7 +72,7 @@ impl FromValue for ClientTrackinginfoReply { /// `FILTERBY 〈 MODULE module-name | ACLCAT category | PATTERN pattern 〉` impl IntoCommand for CommandListFilter { - fn into_command(self) -> Result { + fn into_command(self) -> Result { let (filter, value) = match self { CommandListFilter::Module(name) => ("MODULE", name), CommandListFilter::Aclcat(category) => ("ACLCAT", category), @@ -86,7 +86,7 @@ impl IntoCommand for CommandListFilter { pub struct OnOff(pub bool); impl IntoCommand for OnOff { - fn into_command(self) -> Result { + fn into_command(self) -> Result { if self.0 { "ON" } else { "OFF" }.into_command() } } @@ -96,7 +96,7 @@ impl IntoCommand for OnOff { pub struct CommandInfoList(pub Vec); impl FromValue for CommandInfoList { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut commands = vec![]; for command in items(value)? { command_info(command, &mut commands)?; @@ -107,7 +107,7 @@ impl FromValue for CommandInfoList { /// `[name, arity, flags, first-key, last-key, step, acl-categories, tips, key-specs, subcommands]`, /// the ACL categories since 6.0, the rest since 7.0. -fn command_info(value: RespValue, commands: &mut Vec) -> Result<(), Error> { +fn command_info(value: RespValue, commands: &mut Vec) -> Result<(), ErrorCode> { if value == RespValue::Null { return Ok(()); } @@ -115,7 +115,7 @@ fn command_info(value: RespValue, commands: &mut Vec) -> Resul let mut next = || { items .next() - .ok_or_else(|| Error::Client("too few items for a command".to_string())) + .ok_or_else(|| ErrorCode::Client("too few items for a command".to_string())) }; let info = CommandInfoReply { name: FromValue::from_value(next()?)?, @@ -137,7 +137,7 @@ fn command_info(value: RespValue, commands: &mut Vec) -> Resul } impl FromValue for CommandKeySpec { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; let mut begin_search: Fields = fields.required("begin_search")?; let mut find_keys: Fields = fields.required("find_keys")?; @@ -151,7 +151,7 @@ impl FromValue for CommandKeySpec { } /// `{type, spec}`, a type this interface does not know is `unknown`. -fn begin_search_spec(fields: &mut Fields) -> Result { +fn begin_search_spec(fields: &mut Fields) -> Result { let kind: String = fields.required("type")?; let mut spec: Fields = fields.required("spec")?; Ok(match kind.as_str() { @@ -165,7 +165,7 @@ fn begin_search_spec(fields: &mut Fields) -> Result Result { +fn find_keys_spec(fields: &mut Fields) -> Result { let kind: String = fields.required("type")?; let mut spec: Fields = fields.required("spec")?; Ok(match kind.as_str() { @@ -187,7 +187,7 @@ fn find_keys_spec(fields: &mut Fields) -> Result pub struct CommandDocsList(pub Vec); impl FromValue for CommandDocsList { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut commands = vec![]; for (name, doc) in entries::(value)? { command_doc(name, doc, &mut commands)?; @@ -200,7 +200,7 @@ fn command_doc( name: String, mut fields: Fields, commands: &mut Vec, -) -> Result<(), Error> { +) -> Result<(), ErrorCode> { let mut arguments = vec![]; let nested: Option> = fields.optional("arguments")?; command_arguments(nested.unwrap_or_default(), None, &mut arguments)?; @@ -232,10 +232,10 @@ fn command_arguments( nested: Vec, parent: Option, arguments: &mut Vec, -) -> Result<(), Error> { +) -> Result<(), ErrorCode> { for mut fields in nested { let index = u32::try_from(arguments.len()) - .map_err(|_| Error::Client("too many arguments".to_string()))?; + .map_err(|_| ErrorCode::Client("too many arguments".to_string()))?; let kind: String = fields.required("type")?; arguments.push(CommandDocsArgument { parent, diff --git a/crates/componentized-valkey/src/commands/geo.rs b/crates/componentized-valkey/src/commands/geo.rs index d409e88..954c04d 100644 --- a/crates/componentized-valkey/src/commands/geo.rs +++ b/crates/componentized-valkey/src/commands/geo.rs @@ -2,21 +2,21 @@ use crate::command::{Flag, IntoCommand, NonEmpty, NumKeys, Words}; use crate::types::{ - Double, Error, GeoCoordinates, GeoUnit, GeoaddCondition, GeosearchCount, GeosearchFrom, + Double, ErrorCode, GeoCoordinates, GeoUnit, GeoaddCondition, GeosearchCount, GeosearchFrom, GeosearchShape, GeosearchWithResult, RespValue, StringOrBytes, }; use crate::value::{FromValue, items, unexpected}; /// `longitude latitude` impl IntoCommand for GeoCoordinates { - fn into_command(self) -> Result { + fn into_command(self) -> Result { (self.longitude, self.latitude).into_command() } } /// `[longitude, latitude]` impl FromValue for GeoCoordinates { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let (longitude, latitude) = FromValue::from_value(value)?; Ok(GeoCoordinates { longitude, @@ -26,7 +26,7 @@ impl FromValue for GeoCoordinates { } impl IntoCommand for GeoUnit { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { GeoUnit::M => "m", GeoUnit::Km => "km", @@ -38,7 +38,7 @@ impl IntoCommand for GeoUnit { } impl IntoCommand for GeoaddCondition { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { GeoaddCondition::Nx => "NX", GeoaddCondition::Xx => "XX", @@ -48,7 +48,7 @@ impl IntoCommand for GeoaddCondition { } impl IntoCommand for GeosearchFrom { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { GeosearchFrom::Member(member) => ("FROMMEMBER", member).into_command(), GeosearchFrom::Lonlat(coordinates) => ("FROMLONLAT", coordinates).into_command(), @@ -58,7 +58,7 @@ impl IntoCommand for GeosearchFrom { /// The center, when the shape has one, then the shape. Also `GeosearchstoreShape`, the same type. impl IntoCommand for GeosearchShape { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { GeosearchShape::Radius(circle) => { (circle.from, "BYRADIUS", circle.radius, circle.unit).into_command() @@ -80,7 +80,7 @@ impl IntoCommand for GeosearchShape { /// `COUNT count [ ANY ]`, also `GeosearchstoreCount`, the same type. impl IntoCommand for GeosearchCount { - fn into_command(self) -> Result { + fn into_command(self) -> Result { ("COUNT", self.count, Flag("ANY", self.any)).into_command() } } @@ -88,7 +88,7 @@ impl IntoCommand for GeosearchCount { /// The member, or with any of WITHDIST, WITHHASH and WITHCOORD, an array of the member then the /// details, in that order, told apart by their types. impl FromValue for GeosearchWithResult { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut result = GeosearchWithResult { member: StringOrBytes::String(String::new()), distance: None, diff --git a/crates/componentized-valkey/src/commands/hash.rs b/crates/componentized-valkey/src/commands/hash.rs index 2fbf7b4..62e56c8 100644 --- a/crates/componentized-valkey/src/commands/hash.rs +++ b/crates/componentized-valkey/src/commands/hash.rs @@ -2,7 +2,8 @@ use crate::command::{IntoCommand, NonEmpty, NumKeys, Words}; use crate::types::{ - Error, HexpireResult, HpersistResult, HsetexCondition, HsetexFieldCondition, Integer, RespValue, + ErrorCode, HexpireResult, HpersistResult, HsetexCondition, HsetexFieldCondition, Integer, + RespValue, }; use crate::value::FromValue; @@ -10,37 +11,37 @@ use crate::value::FromValue; pub struct Fields(pub NonEmpty); impl IntoCommand for Fields { - fn into_command(self) -> Result { + fn into_command(self) -> Result { ("FIELDS", NumKeys(self.0)).into_command() } } /// Also `HexpireatResult`, `HpexpireResult` and `HpexpireatResult`, the same type. impl FromValue for HexpireResult { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match Integer::from_value(value)? { -2 => Ok(Self::DoesNotExist), 0 => Ok(Self::ConditionNotMet), 1 => Ok(Self::Applied), 2 => Ok(Self::ZeroSeconds), - code => Err(Error::Client(format!("unexpected reply: {code}"))), + code => Err(ErrorCode::Client(format!("unexpected reply: {code}"))), } } } impl FromValue for HpersistResult { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match Integer::from_value(value)? { -2 => Ok(Self::DoesNotExist), -1 => Ok(Self::NoExpiration), 1 => Ok(Self::Removed), - code => Err(Error::Client(format!("unexpected reply: {code}"))), + code => Err(ErrorCode::Client(format!("unexpected reply: {code}"))), } } } impl IntoCommand for HsetexCondition { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { HsetexCondition::Nx => "NX", HsetexCondition::Xx => "XX", @@ -50,7 +51,7 @@ impl IntoCommand for HsetexCondition { } impl IntoCommand for HsetexFieldCondition { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { HsetexFieldCondition::Fnx => "FNX", HsetexFieldCondition::Fxx => "FXX", diff --git a/crates/componentized-valkey/src/commands/keyspace.rs b/crates/componentized-valkey/src/commands/keyspace.rs index 231d25d..8760662 100644 --- a/crates/componentized-valkey/src/commands/keyspace.rs +++ b/crates/componentized-valkey/src/commands/keyspace.rs @@ -1,12 +1,12 @@ //! The keyspace commands: MIGRATE, SCAN, SORT, TYPE and the like. use crate::command::{Flag, IntoCommand, NamedOption, NonEmpty, Words}; -use crate::types::{Error, KeyType, MigrateAuth, RespValue, SortOpts}; +use crate::types::{ErrorCode, KeyType, MigrateAuth, RespValue, SortOpts}; use crate::value::{FromValue, unexpected}; /// The name of the type, e.g. for SCAN's TYPE. impl IntoCommand for KeyType { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { KeyType::String => "string".to_string(), KeyType::List => "list".to_string(), @@ -24,7 +24,7 @@ impl IntoCommand for KeyType { pub struct OptionalKeyType(pub Option); impl FromValue for OptionalKeyType { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { // `none` rather than null, though the reply schema has null let name: Option = FromValue::from_value(value)?; Ok(OptionalKeyType(match name.as_deref() { @@ -41,7 +41,7 @@ impl FromValue for OptionalKeyType { } impl IntoCommand for MigrateAuth { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { MigrateAuth::Password(password) => ("AUTH", password).into_command(), MigrateAuth::UsernamePassword((username, password)) => { @@ -55,7 +55,7 @@ impl IntoCommand for MigrateAuth { pub struct Migrated(pub bool); impl FromValue for Migrated { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match &value { RespValue::String(reply) if reply == "OK" => Ok(Migrated(true)), RespValue::String(reply) if reply == "NOKEY" => Ok(Migrated(false)), @@ -67,7 +67,7 @@ impl FromValue for Migrated { /// `[ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ]`, also /// `SortRoOpts`, the same type. impl IntoCommand for SortOpts { - fn into_command(self) -> Result { + fn into_command(self) -> Result { // GET before each pattern let get = self.get.map(|patterns| { let patterns = patterns.into_iter().map(|pattern| ("GET", pattern)); diff --git a/crates/componentized-valkey/src/commands/list.rs b/crates/componentized-valkey/src/commands/list.rs index 6c02d8b..67d5963 100644 --- a/crates/componentized-valkey/src/commands/list.rs +++ b/crates/componentized-valkey/src/commands/list.rs @@ -1,11 +1,11 @@ //! The list commands: LINSERT, LMOVE, LMPOP and the like. use crate::command::{IntoCommand, Words}; -use crate::types::{Error, Integer, LinsertPosition, LinsertResult, ListSide, RespValue}; +use crate::types::{ErrorCode, Integer, LinsertPosition, LinsertResult, ListSide, RespValue}; use crate::value::FromValue; impl IntoCommand for ListSide { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { ListSide::Left => "LEFT", ListSide::Right => "RIGHT", @@ -15,7 +15,7 @@ impl IntoCommand for ListSide { } impl IntoCommand for LinsertPosition { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { LinsertPosition::Before => "BEFORE", LinsertPosition::After => "AFTER", @@ -27,12 +27,12 @@ impl IntoCommand for LinsertPosition { /// The length of the list after the insert, `0` when the key does not exist, `-1` when the pivot /// was not found. impl FromValue for LinsertResult { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match Integer::from_value(value)? { -1 => Ok(Self::PivotNotFound), 0 => Ok(Self::NoSuchKey), length if length > 0 => Ok(Self::Inserted(length)), - code => Err(Error::Client(format!("unexpected reply: {code}"))), + code => Err(ErrorCode::Client(format!("unexpected reply: {code}"))), } } } diff --git a/crates/componentized-valkey/src/commands/scripting.rs b/crates/componentized-valkey/src/commands/scripting.rs index 9e544de..ab08c1a 100644 --- a/crates/componentized-valkey/src/commands/scripting.rs +++ b/crates/componentized-valkey/src/commands/scripting.rs @@ -2,13 +2,13 @@ use crate::command::{IntoCommand, NonEmpty, Words}; use crate::types::{ - Error, FlushMode, FunctionListFunction, FunctionListLibrary, FunctionStatsEngine, - FunctionRestorePolicy, FunctionStatsReply, FunctionStatsRunning, RespValue, StringOrBytes, + ErrorCode, FlushMode, FunctionListFunction, FunctionListLibrary, FunctionRestorePolicy, + FunctionStatsEngine, FunctionStatsReply, FunctionStatsRunning, RespValue, StringOrBytes, }; use crate::value::{Fields, FromValue}; impl IntoCommand for FlushMode { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { FlushMode::Async => "ASYNC", FlushMode::Sync => "SYNC", @@ -18,7 +18,7 @@ impl IntoCommand for FlushMode { } impl IntoCommand for FunctionRestorePolicy { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { FunctionRestorePolicy::Flush => "FLUSH", FunctionRestorePolicy::Append => "APPEND", @@ -32,7 +32,7 @@ impl IntoCommand for FunctionRestorePolicy { pub struct KeysAndArgs(pub Option>, pub Option>); impl IntoCommand for KeysAndArgs { - fn into_command(self) -> Result { + fn into_command(self) -> Result { let KeysAndArgs(keys, args) = self; let numkeys = keys.as_ref().map_or(0, Vec::len); ( @@ -45,7 +45,7 @@ impl IntoCommand for KeysAndArgs { } impl FromValue for FunctionListFunction { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { name: fields.required("name")?, @@ -56,7 +56,7 @@ impl FromValue for FunctionListFunction { } impl FromValue for FunctionListLibrary { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { library_name: fields.required("library_name")?, @@ -69,7 +69,7 @@ impl FromValue for FunctionListLibrary { } impl FromValue for FunctionStatsRunning { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { name: fields.required("name")?, @@ -80,7 +80,7 @@ impl FromValue for FunctionStatsRunning { } impl FromValue for FunctionStatsEngine { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { libraries_count: fields.required("libraries_count")?, @@ -90,7 +90,7 @@ impl FromValue for FunctionStatsEngine { } impl FromValue for FunctionStatsReply { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { running_script: fields.required("running_script")?, diff --git a/crates/componentized-valkey/src/commands/shared.rs b/crates/componentized-valkey/src/commands/shared.rs index f00624e..fc9cc49 100644 --- a/crates/componentized-valkey/src/commands/shared.rs +++ b/crates/componentized-valkey/src/commands/shared.rs @@ -2,7 +2,7 @@ use crate::command::{IntoCommand, Words}; use crate::types::{ - Cursor, DurationMilliseconds, DurationSeconds, Error, ExpiretimeReply, Integer, Limit, + Cursor, DurationMilliseconds, DurationSeconds, ErrorCode, ExpiretimeReply, Integer, Limit, PexpiretimeReply, PttlReply, RespValue, SortOrder, TtlMode, TtlReply, UnixTimeMilliseconds, UnixTimeSeconds, }; @@ -10,56 +10,56 @@ use crate::value::FromValue; /// Whole seconds, e.g. for EX. impl IntoCommand for DurationSeconds { - fn into_command(self) -> Result { + fn into_command(self) -> Result { self.ex.into_command() } } /// Whole seconds, never negative. impl FromValue for DurationSeconds { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { u64::from_value(value).map(|ex| DurationSeconds { ex }) } } /// Whole milliseconds, e.g. for PX. impl IntoCommand for DurationMilliseconds { - fn into_command(self) -> Result { + fn into_command(self) -> Result { self.px.into_command() } } /// Whole milliseconds, never negative. impl FromValue for DurationMilliseconds { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { u64::from_value(value).map(|px| DurationMilliseconds { px }) } } /// Whole seconds since the Unix epoch, e.g. for EXAT. impl IntoCommand for UnixTimeSeconds { - fn into_command(self) -> Result { + fn into_command(self) -> Result { self.exat.into_command() } } /// Whole seconds since the Unix epoch. impl FromValue for UnixTimeSeconds { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { Integer::from_value(value).map(|exat| UnixTimeSeconds { exat }) } } /// Whole milliseconds since the Unix epoch, e.g. for PXAT. impl IntoCommand for UnixTimeMilliseconds { - fn into_command(self) -> Result { + fn into_command(self) -> Result { self.pxat.into_command() } } /// Whole milliseconds since the Unix epoch. impl FromValue for UnixTimeMilliseconds { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { Integer::from_value(value).map(|pxat| UnixTimeMilliseconds { pxat }) } } @@ -68,7 +68,7 @@ impl FromValue for UnixTimeMilliseconds { pub struct DecimalSeconds(pub DurationMilliseconds); impl IntoCommand for DecimalSeconds { - fn into_command(self) -> Result { + fn into_command(self) -> Result { let milliseconds = self.0.px; format!("{}.{:03}", milliseconds / 1_000, milliseconds % 1_000).into_command() } @@ -76,10 +76,10 @@ impl IntoCommand for DecimalSeconds { /// `0` to start, failing to resume from `0`, which would restart the scan. impl IntoCommand for Cursor { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { Cursor::Start => "0".into_command(), - Cursor::Resume(cursor) if cursor == "0" => Err(Error::Client( + Cursor::Resume(cursor) if cursor == "0" => Err(ErrorCode::Client( "resume requires the cursor a scan returned, use start to begin a scan".to_string(), )), Cursor::Resume(cursor) => cursor.into_command(), @@ -91,7 +91,7 @@ impl IntoCommand for Cursor { pub struct NextCursor(pub Option); impl FromValue for NextCursor { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let cursor = String::from_value(value)?; Ok(NextCursor( (cursor != "0").then_some(Cursor::Resume(cursor)), @@ -100,7 +100,7 @@ impl FromValue for NextCursor { } impl IntoCommand for TtlMode { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { TtlMode::Nx => "NX", TtlMode::Xx => "XX", @@ -117,7 +117,7 @@ macro_rules! from_value_ttl_replies { ($($variant:ident($time:ident) => $case:ident),*) => { $( impl FromValue for $variant { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match value { RespValue::Integer(-2) => Ok($variant::DoesNotExist), RespValue::Integer(-1) => Ok($variant::NoExpiration), @@ -138,13 +138,13 @@ from_value_ttl_replies!( /// `LIMIT offset count` impl IntoCommand for Limit { - fn into_command(self) -> Result { + fn into_command(self) -> Result { ("LIMIT", self.offset, self.count).into_command() } } impl IntoCommand for SortOrder { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { SortOrder::Asc => "ASC", SortOrder::Desc => "DESC", diff --git a/crates/componentized-valkey/src/commands/sortedset.rs b/crates/componentized-valkey/src/commands/sortedset.rs index 24373fe..67ad851 100644 --- a/crates/componentized-valkey/src/commands/sortedset.rs +++ b/crates/componentized-valkey/src/commands/sortedset.rs @@ -2,7 +2,7 @@ use crate::command::{Flag, IntoCommand, Words}; use crate::types::{ - Double, Error, LexBound, Limit, RespValue, ScoreBound, StringOrBytes, ZaddComparison, + Double, ErrorCode, LexBound, Limit, RespValue, ScoreBound, StringOrBytes, ZaddComparison, ZaddCondition, ZmpopEnd, ZrangeBy, ZsetAggregate, }; use crate::value::{FromValue, items}; @@ -15,7 +15,7 @@ pub struct ScoreRange { } impl IntoCommand for ScoreRange { - fn into_command(self) -> Result { + fn into_command(self) -> Result { let bound = |bound, unbounded: &str| match bound { ScoreBound::Inclusive(score) => score.to_string().into(), ScoreBound::Exclusive(score) => format!("({score}").into(), @@ -35,7 +35,7 @@ pub struct LexRange { } impl IntoCommand for LexRange { - fn into_command(self) -> Result { + fn into_command(self) -> Result { // a member may be bytes, the prefix keeps it so let bound = |bound, unbounded: &str| match bound { LexBound::Inclusive(member) => StringOrBytes::prefixed(member, "["), @@ -56,7 +56,7 @@ pub struct Zrange { } impl IntoCommand for Zrange { - fn into_command(self) -> Result { + fn into_command(self) -> Result { let Zrange { range, rev, limit } = self; let (range, by) = match range { ZrangeBy::Rank((start, stop)) => ((start, stop).into_command()?, None), @@ -73,7 +73,7 @@ impl IntoCommand for Zrange { } impl IntoCommand for ZsetAggregate { - fn into_command(self) -> Result { + fn into_command(self) -> Result { let aggregate = match self { ZsetAggregate::Sum => "SUM", ZsetAggregate::Min => "MIN", @@ -85,7 +85,7 @@ impl IntoCommand for ZsetAggregate { /// Also `BzmpopEnd`, the same type. impl IntoCommand for ZmpopEnd { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { ZmpopEnd::Min => "MIN", ZmpopEnd::Max => "MAX", @@ -95,7 +95,7 @@ impl IntoCommand for ZmpopEnd { } impl IntoCommand for ZaddCondition { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { ZaddCondition::Nx => "NX", ZaddCondition::Xx => "XX", @@ -105,7 +105,7 @@ impl IntoCommand for ZaddCondition { } impl IntoCommand for ZaddComparison { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { ZaddComparison::Gt => "GT", ZaddComparison::Lt => "LT", @@ -119,7 +119,7 @@ impl IntoCommand for ZaddComparison { pub struct ScoredMembers(pub Vec<(StringOrBytes, Double)>); impl FromValue for ScoredMembers { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let items = items(value)?; match items.first() { None => Ok(ScoredMembers(vec![])), @@ -133,7 +133,7 @@ impl FromValue for ScoredMembers { StringOrBytes::from_value(member)?, Double::from_value(score)?, )])), - Err(items) => Err(Error::Client(format!( + Err(items) => Err(ErrorCode::Client(format!( "unexpected number of items, expected a member and its score: {}", items.len() ))), diff --git a/crates/componentized-valkey/src/commands/stream.rs b/crates/componentized-valkey/src/commands/stream.rs index d04aa6f..9cfd81f 100644 --- a/crates/componentized-valkey/src/commands/stream.rs +++ b/crates/componentized-valkey/src/commands/stream.rs @@ -2,7 +2,7 @@ use crate::command::{Flag, IntoCommand, NamedOption, Words}; use crate::types::{ - DurationMilliseconds, Error, RespValue, StreamId, StreamTrim, StreamTrimEfficiency, + DurationMilliseconds, ErrorCode, RespValue, StreamId, StreamTrim, StreamTrimEfficiency, UnixTimeMilliseconds, XclaimOpts, XinfoConsumersEntry, XinfoGroupsEntry, XinfoStreamReply, XinfoStreamReplyFull, XinfoStreamReplyFullConsumer, XinfoStreamReplyFullGroup, XpendingExtendedEntry, XpendingSummary, @@ -10,7 +10,7 @@ use crate::types::{ use crate::value::{Fields, FromValue, OrNever}; impl IntoCommand for StreamTrim { - fn into_command(self) -> Result { + fn into_command(self) -> Result { //〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] match self { StreamTrim::Maxlen(trim) => ( @@ -32,7 +32,7 @@ impl IntoCommand for StreamTrim { } impl IntoCommand for StreamTrimEfficiency { - fn into_command(self) -> Result { + fn into_command(self) -> Result { // = | ~ (match self { StreamTrimEfficiency::Exact => "=", @@ -43,7 +43,7 @@ impl IntoCommand for StreamTrimEfficiency { } impl IntoCommand for XclaimOpts { - fn into_command(self) -> Result { + fn into_command(self) -> Result { // [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] [ LASTID lastid ] ( NamedOption("IDLE", self.idle), @@ -64,13 +64,13 @@ pub const ZERO_ID: &str = "0-0"; /// `0-0` for none, e.g. to start XAUTOCLAIM from the beginning. impl IntoCommand for OrZeroId { - fn into_command(self) -> Result { + fn into_command(self) -> Result { self.0.unwrap_or_else(|| ZERO_ID.to_string()).into_command() } } impl FromValue for OrZeroId { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let id = StreamId::from_value(value)?; Ok(OrZeroId((id != ZERO_ID).then_some(id))) } @@ -79,7 +79,7 @@ impl FromValue for OrZeroId { /// `[count, lowest-id, highest-id, [[consumer, count]...]]`, the IDs and consumers are null when /// nothing is pending, and each consumer's count is a bulk string. impl FromValue for XpendingSummary { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let (count, lowest_id, highest_id, consumers): (_, _, _, Option>) = FromValue::from_value(value)?; let consumers = consumers @@ -87,13 +87,13 @@ impl FromValue for XpendingSummary { .into_iter() .map(|(consumer, count)| { let count = count.parse().map_err(|_| { - Error::Client(format!( + ErrorCode::Client(format!( "unexpected pending count, not an integer: {count:?}" )) })?; Ok((consumer, count)) }) - .collect::>()?; + .collect::>()?; Ok(Self { count, lowest_id, @@ -105,7 +105,7 @@ impl FromValue for XpendingSummary { /// `[id, consumer, milliseconds-since-delivery, delivery-count]`. impl FromValue for XpendingExtendedEntry { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let (id, name, since_delivery, delivery_count) = FromValue::from_value(value)?; Ok(Self { id, @@ -117,7 +117,7 @@ impl FromValue for XpendingExtendedEntry { } impl FromValue for XinfoConsumersEntry { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { name: fields.required("name")?, @@ -131,7 +131,7 @@ impl FromValue for XinfoConsumersEntry { } impl FromValue for XinfoGroupsEntry { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { name: fields.required("name")?, @@ -145,7 +145,7 @@ impl FromValue for XinfoGroupsEntry { } impl FromValue for XinfoStreamReply { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { length: fields.required("length")?, @@ -162,7 +162,7 @@ impl FromValue for XinfoStreamReply { } impl FromValue for XinfoStreamReplyFull { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { length: fields.required("length")?, @@ -178,7 +178,7 @@ impl FromValue for XinfoStreamReplyFull { } impl FromValue for XinfoStreamReplyFullGroup { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { name: fields.required("name")?, @@ -193,7 +193,7 @@ impl FromValue for XinfoStreamReplyFullGroup { } impl FromValue for XinfoStreamReplyFullConsumer { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { name: fields.required("name")?, diff --git a/crates/componentized-valkey/src/commands/string.rs b/crates/componentized-valkey/src/commands/string.rs index 06dcca7..7632c84 100644 --- a/crates/componentized-valkey/src/commands/string.rs +++ b/crates/componentized-valkey/src/commands/string.rs @@ -2,14 +2,14 @@ use crate::command::{IntoCommand, Words}; use crate::types::{ - Error, GetexTtl, LcsIdxMatch, LcsIdxReply, MsetexPrecondition, RespValue, SetPrecondition, + ErrorCode, GetexTtl, LcsIdxMatch, LcsIdxReply, MsetexPrecondition, RespValue, SetPrecondition, SetTtl, }; use crate::value::{Fields, FromValue, items}; /// Also `HgetexTtl`, the same type. impl IntoCommand for GetexTtl { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { GetexTtl::Ex(duration) => ("EX", duration).into_command(), GetexTtl::Px(duration) => ("PX", duration).into_command(), @@ -22,7 +22,7 @@ impl IntoCommand for GetexTtl { /// A map of the matches and the length of the longest common subsequence. impl FromValue for LcsIdxReply { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut fields = Fields::from_value(value)?; Ok(Self { matches: fields.required("matches")?, @@ -33,12 +33,12 @@ impl FromValue for LcsIdxReply { /// The inclusive ranges of a match in each key and, with `WITHMATCHLEN`, its length. impl FromValue for LcsIdxMatch { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let mut items = items(value)?.into_iter(); let (Some(key1_range), Some(key2_range), len, None) = (items.next(), items.next(), items.next(), items.next()) else { - return Err(Error::Client( + return Err(ErrorCode::Client( "unexpected LCS match, expected 2 or 3 items".to_string(), )); }; @@ -51,7 +51,7 @@ impl FromValue for LcsIdxMatch { } impl IntoCommand for MsetexPrecondition { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { MsetexPrecondition::Nx => "NX".into_command(), MsetexPrecondition::Xx => "XX".into_command(), @@ -60,7 +60,7 @@ impl IntoCommand for MsetexPrecondition { } impl IntoCommand for SetPrecondition { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { SetPrecondition::Nx => "NX".into_command(), SetPrecondition::Xx => "XX".into_command(), @@ -72,7 +72,7 @@ impl IntoCommand for SetPrecondition { /// Also `MsetexTtl` and `HsetexTtl`, the same type. impl IntoCommand for SetTtl { - fn into_command(self) -> Result { + fn into_command(self) -> Result { match self { SetTtl::Ex(duration) => ("EX", duration).into_command(), SetTtl::Px(duration) => ("PX", duration).into_command(), diff --git a/crates/componentized-valkey/src/resp.rs b/crates/componentized-valkey/src/resp.rs index 2c3342e..cbd303b 100644 --- a/crates/componentized-valkey/src/resp.rs +++ b/crates/componentized-valkey/src/resp.rs @@ -1,6 +1,6 @@ //! RESP serialization, from . -use crate::types::{RespError as Error, RespNestedValue, RespValue}; +use crate::types::{RespError as ErrorCode, RespNestedValue, RespValue}; /// up to 512 MB in length const RESP_MAX_SIZE: i64 = 512 * 1024 * 1024; @@ -79,7 +79,7 @@ fn buf_encode(value: RespValue, buf: &mut Vec) { buf.push(b'!'); buf.extend_from_slice(val.len().to_string().as_bytes()); buf.extend_from_slice(CRLF_BYTES); - buf.extend_from_slice(val.as_bytes()); + buf.extend_from_slice(val); buf.extend_from_slice(CRLF_BYTES); } RespValue::VerbatimString((ref encoding, ref value)) => { @@ -130,7 +130,7 @@ fn buf_encode(value: RespValue, buf: &mut Vec) { /// let value = RespValue::BulkString(b"Hello".to_vec()); /// assert_eq!(decode(&encode(value.clone())), Ok(value)); /// ``` -pub fn decode(value: &[u8]) -> Result { +pub fn decode(value: &[u8]) -> Result { match frame_end(value, 0)? { Some(end) if end == value.len() => decode_frame(value), Some(end) => Err(format!( @@ -143,12 +143,12 @@ pub fn decode(value: &[u8]) -> Result { /// The length of the first complete RESP value in `buf`, or `None` when the value is incomplete /// and more bytes are needed. Only the framing is checked, the value is not decoded. -pub fn frame_len(buf: &[u8]) -> Result, Error> { +pub fn frame_len(buf: &[u8]) -> Result, ErrorCode> { frame_end(buf, 0) } /// The end of the RESP value starting at `start`, or `None` when it is incomplete. -fn frame_end(buf: &[u8], start: usize) -> Result, Error> { +fn frame_end(buf: &[u8], start: usize) -> Result, ErrorCode> { let Some(prefix) = buf.get(start) else { return Ok(None); }; @@ -159,14 +159,14 @@ fn frame_end(buf: &[u8], start: usize) -> Result, Error> { let header = &buf[start + 1..start + crlf]; let end = start + crlf + 2; - let length = |min: i64| -> Result { + let length = |min: i64| -> Result { let int = parse_integer(header)?; if int < min || int >= RESP_MAX_SIZE { Err(format!("invalid length: {}", int))? } Ok(int) }; - let children = |end: usize, count: i64| -> Result, Error> { + let children = |end: usize, count: i64| -> Result, ErrorCode> { let mut end = end; for _ in 0..count { match frame_end(buf, end)? { @@ -210,7 +210,7 @@ fn frame_end(buf: &[u8], start: usize) -> Result, Error> { } /// Decodes the whole RESP value at the start of `buf`, its framing already checked by `frame_end`. -fn decode_frame(buf: &[u8]) -> Result { +fn decode_frame(buf: &[u8]) -> Result { let crlf = buf .windows(2) .position(|w| w == CRLF_BYTES) @@ -219,12 +219,12 @@ fn decode_frame(buf: &[u8]) -> Result { let body = crlf + 2; // the bytes of a blob value, after its length line - let blob = || -> Result<&[u8], Error> { + let blob = || -> Result<&[u8], ErrorCode> { let len = parse_integer(header)? as usize; Ok(&buf[body..body + len]) }; // the elements of an aggregate value, each kept as its RESP - let elements = |count: i64| -> Result, Error> { + let elements = |count: i64| -> Result, ErrorCode> { let mut start = body; let mut elements = Vec::with_capacity(count as usize); for _ in 0..count { @@ -252,7 +252,7 @@ fn decode_frame(buf: &[u8]) -> Result { -1 => Ok(RespValue::Null), _ => Ok(RespValue::BulkString(blob()?.to_vec())), }, - b'!' => parse_string(blob()?).map(RespValue::BulkError), + b'!' => Ok(RespValue::BulkError(blob()?.to_vec())), b'=' => { let value = parse_string(blob()?)?; let Some((encoding, value)) = value.split_once(':') else { @@ -300,7 +300,10 @@ impl std::fmt::Display for RespValue { Self::Boolean(value) => write!(f, "boolean<{value}>"), Self::Double(value) => write!(f, "double<{value}>"), Self::BigNumber(value) => write!(f, "big-number<{value}>"), - Self::BulkError(value) => write!(f, "bulk-error<{value:?}>"), + Self::BulkError(value) => match std::str::from_utf8(value) { + Ok(text) => write!(f, "bulk-error<{text:?}>"), + Err(_) => write!(f, "bulk-error", value.escape_ascii()), + }, Self::VerbatimString((format, value)) => { write!(f, "verbatim-string<{format}, {value:?}>") } @@ -346,18 +349,18 @@ fn write_nested(f: &mut std::fmt::Formatter<'_>, value: &[u8]) -> std::fmt::Resu } #[inline] -fn parse_string(bytes: &[u8]) -> Result { +fn parse_string(bytes: &[u8]) -> Result { String::from_utf8(bytes.to_vec()).map_err(|err| err.to_string()) } #[inline] -fn parse_integer(bytes: &[u8]) -> Result { +fn parse_integer(bytes: &[u8]) -> Result { let str_integer = parse_string(bytes)?; (str_integer.parse::()).map_err(|err| err.to_string()) } #[inline] -fn parse_double(bytes: &[u8]) -> Result { +fn parse_double(bytes: &[u8]) -> Result { let str_double = parse_string(bytes)?; (str_double.parse::()).map_err(|err| err.to_string()) } @@ -428,7 +431,8 @@ mod tests { RespValue::Boolean(false), RespValue::Double(1.5), RespValue::BigNumber("12345678901234567890".to_string()), - RespValue::BulkError("SYNTAX invalid".to_string()), + RespValue::BulkError(b"SYNTAX invalid".to_vec()), + RespValue::BulkError(b"ERR \x80\r\n".to_vec()), RespValue::VerbatimString(("txt".to_string(), "a:b".to_string())), RespValue::Array(vec![encode(RespValue::Integer(1)), encode(RespValue::Null)]), RespValue::Set(vec![encode(RespValue::String("a".to_string()))]), @@ -522,9 +526,13 @@ mod tests { "big-number<1>" ); assert_eq!( - display(RespValue::BulkError("SYNTAX".to_string())), + display(RespValue::BulkError(b"SYNTAX".to_vec())), r#"bulk-error<"SYNTAX">"# ); + assert_eq!( + display(RespValue::BulkError(b"ERR \x80".to_vec())), + r#"bulk-error"# + ); assert_eq!( display(RespValue::VerbatimString(( "txt".to_string(), diff --git a/crates/componentized-valkey/src/string_or_bytes.rs b/crates/componentized-valkey/src/string_or_bytes.rs index 06e5177..a2ec469 100644 --- a/crates/componentized-valkey/src/string_or_bytes.rs +++ b/crates/componentized-valkey/src/string_or_bytes.rs @@ -38,6 +38,17 @@ impl StringOrBytes { } } +/// The text of a string, bytes escaped as in a Rust byte string literal, e.g. `\x80` and `\r`, for +/// people to read, e.g. in an error message. +impl std::fmt::Display for StringOrBytes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StringOrBytes::String(text) => f.write_str(text), + StringOrBytes::Bytes(bytes) => write!(f, "{}", bytes.escape_ascii()), + } + } +} + impl From for StringOrBytes { fn from(text: String) -> Self { StringOrBytes::String(text) @@ -73,6 +84,15 @@ impl PartialEq for StringOrBytes { mod tests { use super::*; + #[test] + fn display() { + assert_eq!(StringOrBytes::from("ERR oops").to_string(), "ERR oops"); + assert_eq!( + StringOrBytes::Bytes(b"ERR \x80\r\n".to_vec()).to_string(), + r"ERR \x80\r\n" + ); + } + #[test] fn from_bytes() { assert_eq!( diff --git a/crates/componentized-valkey/src/test_util.rs b/crates/componentized-valkey/src/test_util.rs index 8d3e708..1a95389 100644 --- a/crates/componentized-valkey/src/test_util.rs +++ b/crates/componentized-valkey/src/test_util.rs @@ -1,7 +1,7 @@ //! Helpers for the tests of the conversions, to build replies and check the outcomes. use crate::resp::encode; -use crate::types::{Error, RespValue}; +use crate::types::{ErrorCode, RespValue}; use crate::{FromValue, IntoCommand}; /// The words of a command that converts. @@ -12,7 +12,7 @@ pub fn words(command: impl IntoCommand) -> crate::command::Words { /// The message of the client error a command fails to convert with. pub fn words_error(command: impl IntoCommand) -> String { match command.into_command() { - Err(Error::Client(message)) => message, + Err(ErrorCode::Client(message)) => message, result => panic!("expected a client error: {result:?}"), } } @@ -25,7 +25,7 @@ pub fn from(value: RespValue) -> T { /// The message of the client error a reply fails to convert with. pub fn from_error(value: RespValue) -> String { match T::from_value(value) { - Err(Error::Client(message)) => message, + Err(ErrorCode::Client(message)) => message, Err(error) => panic!("expected a client error: {error:?}"), Ok(_) => panic!("expected a client error"), } diff --git a/crates/componentized-valkey/src/value.rs b/crates/componentized-valkey/src/value.rs index 8cd10cd..5cd2a6a 100644 --- a/crates/componentized-valkey/src/value.rs +++ b/crates/componentized-valkey/src/value.rs @@ -1,25 +1,25 @@ //! Converting a reply into Rust values, the impls for Rust's own types and the helpers the commands //! share. The impls for the package's types are in [`crate::commands`]. -use crate::types::{Double, Error, Integer, RespNestedValue, RespValue, StringOrBytes}; +use crate::types::{Double, ErrorCode, Integer, RespNestedValue, RespValue, StringOrBytes}; use wit_bindgen::rt::Map; /// Converts a reply into a Rust type, e.g. `Option` for a bulk string or null. pub trait FromValue: Sized { - fn from_value(value: RespValue) -> Result; + fn from_value(value: RespValue) -> Result; } /// The reply itself, e.g. for a field of mixed types. impl FromValue for RespValue { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { Ok(value) } } /// A simple string `OK`. impl FromValue for () { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match &value { RespValue::String(reply) if reply == "OK" => Ok(()), _ => Err(unexpected(&value)), @@ -28,7 +28,7 @@ impl FromValue for () { } impl FromValue for Integer { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match value { RespValue::Integer(value) => Ok(value), value => Err(unexpected(&value)), @@ -38,7 +38,7 @@ impl FromValue for Integer { /// A double, or a bulk string holding one, e.g. the reply of INCRBYFLOAT, even with RESP3. impl FromValue for Double { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match value { RespValue::Double(value) => Ok(value), RespValue::BulkString(bytes) => std::str::from_utf8(&bytes) @@ -46,7 +46,7 @@ impl FromValue for Double { .and_then(|text| text.parse().ok()) .ok_or_else(|| { let reply = RespValue::BulkString(bytes); - Error::Client(format!("unexpected reply, not a double: {reply}")) + ErrorCode::Client(format!("unexpected reply, not a double: {reply}")) }), value => Err(unexpected(&value)), } @@ -55,16 +55,16 @@ impl FromValue for Double { /// A non-negative integer. impl FromValue for u64 { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { let value = Integer::from_value(value)?; u64::try_from(value) - .map_err(|_| Error::Client(format!("unexpected negative reply: {value}"))) + .map_err(|_| ErrorCode::Client(format!("unexpected negative reply: {value}"))) } } /// An integer `0` or `1`, or a boolean. impl FromValue for bool { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match value { RespValue::Integer(0) => Ok(false), RespValue::Integer(1) => Ok(true), @@ -76,11 +76,11 @@ impl FromValue for bool { /// A bulk, simple or verbatim string. impl FromValue for String { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match value { RespValue::BulkString(bytes) => String::from_utf8(bytes).map_err(|error| { let reply = RespValue::BulkString(error.into_bytes()); - Error::Client(format!("unexpected reply, not UTF-8: {reply}")) + ErrorCode::Client(format!("unexpected reply, not UTF-8: {reply}")) }), RespValue::String(value) | RespValue::VerbatimString((_, value)) => Ok(value), value => Err(unexpected(&value)), @@ -91,7 +91,7 @@ impl FromValue for String { /// A bulk, simple or verbatim string, a string when it is UTF-8, otherwise its bytes, e.g. a value /// of GET. impl FromValue for StringOrBytes { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match value { RespValue::BulkString(bytes) => Ok(StringOrBytes::from_bytes(bytes)), RespValue::String(text) | RespValue::VerbatimString((_, text)) => { @@ -107,7 +107,7 @@ impl FromValue for StringOrBytes { pub struct Binary(pub Vec); impl FromValue for Binary { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match value { RespValue::BulkString(bytes) => Ok(Binary(bytes)), value => Err(unexpected(&value)), @@ -117,7 +117,7 @@ impl FromValue for Binary { /// Null, or a value. impl FromValue for Option { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match value { RespValue::Null => Ok(None), value => T::from_value(value).map(Some), @@ -127,7 +127,7 @@ impl FromValue for Option { /// An array, or a set. impl FromValue for Vec { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { items(value)?.into_iter().map(T::from_value).collect() } } @@ -138,10 +138,10 @@ macro_rules! from_value_tuples { $( impl<$($item: FromValue),+> FromValue for ($($item,)+) { #[allow(non_snake_case)] - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match <[RespValue; $len]>::try_from(items(value)?) { Ok([$($item),+]) => Ok(($($item::from_value($item)?,)+)), - Err(items) => Err(Error::Client(format!( + Err(items) => Err(ErrorCode::Client(format!( "unexpected number of items, expected {}: {}", $len, items.len() @@ -157,7 +157,7 @@ from_value_tuples!((2; A, B), (3; A, B, C), (4; A, B, C, D)); /// A map, or a flat array of keys and values. impl FromValue for Map { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { Ok(entries(value)?.into_iter().collect()) } } @@ -168,14 +168,14 @@ impl FromValue for Map { pub struct Fields(Vec<(String, RespValue)>); impl FromValue for Fields { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { entries(value).map(Fields) } } impl Fields { /// Take the named field, if present. - pub fn optional(&mut self, name: &str) -> Result, Error> { + pub fn optional(&mut self, name: &str) -> Result, ErrorCode> { match self.0.iter().position(|(key, _)| key == name) { Some(index) => T::from_value(self.0.swap_remove(index).1).map(Some), None => Ok(None), @@ -183,9 +183,9 @@ impl Fields { } /// Take the named field, failing if it is missing. - pub fn required(&mut self, name: &str) -> Result { + pub fn required(&mut self, name: &str) -> Result { self.optional(name)? - .ok_or_else(|| Error::Client(format!("missing field: {name}"))) + .ok_or_else(|| ErrorCode::Client(format!("missing field: {name}"))) } } @@ -193,7 +193,7 @@ impl Fields { pub struct OrNever(pub Option); impl FromValue for OrNever { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { match value { RespValue::Integer(-1) => Ok(OrNever(None)), value => T::from_value(value).map(|value| OrNever(Some(value))), @@ -205,28 +205,28 @@ impl FromValue for OrNever { pub struct FlatPairs(pub Vec<(K, V)>); impl FromValue for FlatPairs { - fn from_value(value: RespValue) -> Result { + fn from_value(value: RespValue) -> Result { entries(value).map(FlatPairs) } } /// A number sent as a string, e.g. the port of a replica in ROLE. -pub(crate) fn parse_string(value: RespValue) -> Result { +pub(crate) fn parse_string(value: RespValue) -> Result { let text = String::from_value(value)?; text.parse() - .map_err(|_| Error::Client(format!("unexpected number: {text:?}"))) + .map_err(|_| ErrorCode::Client(format!("unexpected number: {text:?}"))) } -pub(crate) fn unexpected(reply: &RespValue) -> Error { - Error::Client(format!("unexpected reply: {reply}")) +pub(crate) fn unexpected(reply: &RespValue) -> ErrorCode { + ErrorCode::Client(format!("unexpected reply: {reply}")) } -fn decode(value: RespNestedValue) -> Result { - crate::resp::decode(&value).map_err(Error::Resp) +fn decode(value: RespNestedValue) -> Result { + crate::resp::decode(&value).map_err(ErrorCode::Resp) } /// The nested values of an array, or a set. -pub(crate) fn items(reply: RespValue) -> Result, Error> { +pub(crate) fn items(reply: RespValue) -> Result, ErrorCode> { match reply { RespValue::Array(items) | RespValue::Set(items) => items.into_iter().map(decode).collect(), reply => Err(unexpected(&reply)), @@ -234,7 +234,7 @@ pub(crate) fn items(reply: RespValue) -> Result, Error> { } /// The entries of a map, or of a flat array of keys and values. -pub fn entries(reply: RespValue) -> Result, Error> { +pub fn entries(reply: RespValue) -> Result, ErrorCode> { let entry = |key, value| Ok((K::from_value(decode(key)?)?, V::from_value(decode(value)?)?)); match reply { RespValue::Map(entries) => entries @@ -243,7 +243,7 @@ pub fn entries(reply: RespValue) -> Result { if items.len() % 2 != 0 { - return Err(Error::Client(format!( + return Err(ErrorCode::Client(format!( "unexpected odd number of keys and values: {}", items.len() ))); @@ -400,7 +400,7 @@ mod tests { assert_eq!(fields.optional::("count").unwrap(), Some(2)); assert_eq!(fields.optional::("missing").unwrap(), None); match fields.required::("missing") { - Err(Error::Client(message)) => assert_eq!(message, "missing field: missing"), + Err(ErrorCode::Client(message)) => assert_eq!(message, "missing field: missing"), result => panic!("expected a client error: {result:?}"), } } @@ -423,7 +423,7 @@ mod tests { fn number_in_a_string() { assert_eq!(parse_string::(bulk("6379")).unwrap(), 6379); match parse_string::(bulk("port")) { - Err(Error::Client(message)) => assert_eq!(message, r#"unexpected number: "port""#), + Err(ErrorCode::Client(message)) => assert_eq!(message, r#"unexpected number: "port""#), result => panic!("expected a client error: {result:?}"), } } diff --git a/crates/test-harness/src/lib.rs b/crates/test-harness/src/lib.rs index 065dabf..3b0ff18 100644 --- a/crates/test-harness/src/lib.rs +++ b/crates/test-harness/src/lib.rs @@ -24,7 +24,7 @@ use wasmtime::{Config, Engine, Result, Store, StoreContextMut, bail, format_err} use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; pub use bindings::componentized::valkey::types::{ - self, ConnectOpts, Error, RespNestedValue, RespValue, StringOrBytes, + self, ConnectOpts, ErrorCode, RespNestedValue, RespValue, StringOrBytes, }; mod server; @@ -172,7 +172,7 @@ pub fn hello_reply() -> RespValue { pub struct Transport { requests: mpsc::UnboundedReceiver, replies: Option>, - closed: Option>>, + closed: Option>>, } impl Transport { @@ -198,7 +198,7 @@ impl Transport { } /// Close the transport, gracefully with `Ok(())`, or with the error. - pub fn close(&mut self, result: Result<(), Error>) { + pub fn close(&mut self, result: Result<(), ErrorCode>) { self.replies.take(); if let Some(closed) = self.closed.take() { let _ = closed.send(result); @@ -212,7 +212,7 @@ pub async fn connect( accessor: &Accessor, exports: &Exports, opts: Option, -) -> Result<(Transport, FutureReader>)> { +) -> Result<(Transport, FutureReader>)> { let (replies_tx, replies_rx) = mpsc::unbounded_channel(); let (closed_tx, closed_rx) = oneshot::channel(); let replies = accessor.with(|store| StreamReader::new(store, ChannelProducer(replies_rx)))?; diff --git a/wit/commands.wit b/wit/commands.wit index f9a2fed..8d6339b 100644 --- a/wit/commands.wit +++ b/wit/commands.wit @@ -31,7 +31,7 @@ /// that form, e.g. `zrange-withscores`, its docs naming the other forms. /// - A scan starts with the `start` cursor and resumes with the one it returned, none once it is /// complete. -/// - An error reply fails with a `valkey` error. Arguments the client rejects, and replies it cannot +/// - An error reply fails with a `server` error. Arguments the client rejects, and replies it cannot /// convert, fail with a `client` error. /// - A command that blocks, e.g. BLPOP, or changes the state of the connection, e.g. SELECT, /// affects every caller sharing the connection, see the command's docs. @@ -40,7 +40,7 @@ interface commands { use connection.{connection}; @since(version = 0.3.0-dev) use types.{ - double, error, integer, resp-value, duration-seconds, duration-milliseconds, + double, error-code, integer, resp-value, duration-seconds, duration-milliseconds, unix-time-seconds, unix-time-milliseconds, cursor, ttl-mode, hexpire-result, hpexpireat-result, hpexpire-result, hexpireat-result, ttl-reply, httl-reply, pttl-reply, hpttl-reply, expiretime-reply, hexpiretime-reply, pexpiretime-reply, hpexpiretime-reply, @@ -80,28 +80,28 @@ interface commands { /// ACL CAT [ category ] /// https://valkey.io/commands/acl-cat/ @since(version = 0.3.0-dev) - acl-cat: async func(connection: borrow, category: option) -> result, error>; + acl-cat: async func(connection: borrow, category: option) -> result, error-code>; /// Deletes ACL users, and terminates their connections. /// /// ACL DELUSER username [ username ... ] /// https://valkey.io/commands/acl-deluser/ @since(version = 0.3.0-dev) - acl-deluser: async func(connection: borrow, usernames: list) -> result; + acl-deluser: async func(connection: borrow, usernames: list) -> result; /// Simulates the execution of a command by a user, without executing the command. /// /// ACL DRYRUN username command [ arg [ arg ... ] ] /// https://valkey.io/commands/acl-dryrun/ @since(version = 0.3.0-dev) - acl-dryrun: async func(connection: borrow, username: string, command: list) -> result; + acl-dryrun: async func(connection: borrow, username: string, command: list) -> result; /// Generates a pseudorandom, secure password that can be used to identify ACL users. /// /// ACL GENPASS [ bits ] /// https://valkey.io/commands/acl-genpass/ @since(version = 0.3.0-dev) - acl-genpass: async func(connection: borrow, bits: option) -> result; + acl-genpass: async func(connection: borrow, bits: option) -> result; /// Lists the ACL rules of a user. /// @@ -111,21 +111,21 @@ interface commands { /// ACL GETUSER username /// https://valkey.io/commands/acl-getuser/ @since(version = 0.3.0-dev) - acl-getuser: async func(connection: borrow, username: string) -> result>, error>; + acl-getuser: async func(connection: borrow, username: string) -> result>, error-code>; /// Dumps the effective rules in ACL file format. /// /// ACL LIST /// https://valkey.io/commands/acl-list/ @since(version = 0.3.0-dev) - acl-list: async func(connection: borrow) -> result, error>; + acl-list: async func(connection: borrow) -> result, error-code>; /// Reloads the rules from the configured ACL file. /// /// ACL LOAD /// https://valkey.io/commands/acl-load/ @since(version = 0.3.0-dev) - acl-load: async func(connection: borrow) -> result<_, error>; + acl-load: async func(connection: borrow) -> result<_, error-code>; /// Lists recent security events generated due to ACL rules. /// @@ -134,7 +134,7 @@ interface commands { /// ACL LOG [ count ] /// https://valkey.io/commands/acl-log/ @since(version = 0.3.0-dev) - acl-log: async func(connection: borrow, count: option) -> result, error>; + acl-log: async func(connection: borrow, count: option) -> result, error-code>; /// Lists recent security events generated due to ACL rules. /// @@ -145,56 +145,56 @@ interface commands { /// ACL LOG RESET /// https://valkey.io/commands/acl-log/ @since(version = 0.3.0-dev) - acl-log-reset: async func(connection: borrow) -> result<_, error>; + acl-log-reset: async func(connection: borrow) -> result<_, error-code>; /// Saves the effective ACL rules in the configured ACL file. /// /// ACL SAVE /// https://valkey.io/commands/acl-save/ @since(version = 0.3.0-dev) - acl-save: async func(connection: borrow) -> result<_, error>; + acl-save: async func(connection: borrow) -> result<_, error-code>; /// Creates and modifies an ACL user and its rules. /// /// ACL SETUSER username [ rule ] [ [ rule ] ... ] /// https://valkey.io/commands/acl-setuser/ @since(version = 0.3.0-dev) - acl-setuser: async func(connection: borrow, username: string, rules: option>) -> result<_, error>; + acl-setuser: async func(connection: borrow, username: string, rules: option>) -> result<_, error-code>; /// Lists all ACL users. /// /// ACL USERS /// https://valkey.io/commands/acl-users/ @since(version = 0.3.0-dev) - acl-users: async func(connection: borrow) -> result, error>; + acl-users: async func(connection: borrow) -> result, error-code>; /// Returns the authenticated username of the current connection. /// /// ACL WHOAMI /// https://valkey.io/commands/acl-whoami/ @since(version = 0.3.0-dev) - acl-whoami: async func(connection: borrow) -> result; + acl-whoami: async func(connection: borrow) -> result; /// Asynchronously rewrites the append-only file to disk. /// /// BGREWRITEAOF /// https://valkey.io/commands/bgrewriteaof/ @since(version = 0.3.0-dev) - bgrewriteaof: async func(connection: borrow) -> result; + bgrewriteaof: async func(connection: borrow) -> result; /// Asynchronously saves the database(s) to disk. /// /// BGSAVE [ SCHEDULE | CANCEL ] /// https://valkey.io/commands/bgsave/ @since(version = 0.3.0-dev) - bgsave: async func(connection: borrow, operation: option) -> result; + bgsave: async func(connection: borrow, operation: option) -> result; /// Terminates open connections. /// /// CLIENT KILL filter [ filter ... ] /// https://valkey.io/commands/client-kill/ @since(version = 0.3.0-dev) - client-kill: async func(connection: borrow, filters: list) -> result; + client-kill: async func(connection: borrow, filters: list) -> result; /// Lists open connections. /// @@ -204,7 +204,7 @@ interface commands { /// CLIENT LIST [ filter [ filter ... ] ] /// https://valkey.io/commands/client-list/ @since(version = 0.3.0-dev) - client-list: async func(connection: borrow, filters: option>) -> result>, error>; + client-list: async func(connection: borrow, filters: option>) -> result>, error-code>; /// Sets the client eviction mode of the connection. /// @@ -213,126 +213,126 @@ interface commands { /// CLIENT NO-EVICT 〈 ON | OFF 〉 /// https://valkey.io/commands/client-no-evict/ @since(version = 0.3.0-dev) - client-no-evict: async func(connection: borrow, enabled: bool) -> result<_, error>; + client-no-evict: async func(connection: borrow, enabled: bool) -> result<_, error-code>; /// Suspends commands processing. /// /// CLIENT PAUSE timeout [ WRITE | ALL ] /// https://valkey.io/commands/client-pause/ @since(version = 0.3.0-dev) - client-pause: async func(connection: borrow, timeout: duration-milliseconds, mode: option) -> result<_, error>; + client-pause: async func(connection: borrow, timeout: duration-milliseconds, mode: option) -> result<_, error-code>; /// Unblocks a client blocked by a blocking command from a different connection. /// /// CLIENT UNBLOCK client-id [ TIMEOUT | ERROR ] /// https://valkey.io/commands/client-unblock/ @since(version = 0.3.0-dev) - client-unblock: async func(connection: borrow, client-id: integer, unblock-type: option) -> result; + client-unblock: async func(connection: borrow, client-id: integer, unblock-type: option) -> result; /// Resumes processing commands from paused clients. /// /// CLIENT UNPAUSE /// https://valkey.io/commands/client-unpause/ @since(version = 0.3.0-dev) - client-unpause: async func(connection: borrow) -> result<_, error>; + client-unpause: async func(connection: borrow) -> result<_, error-code>; /// Assigns new hash slots to a node. /// /// CLUSTER ADDSLOTS slot [ slot ... ] /// https://valkey.io/commands/cluster-addslots/ @since(version = 0.3.0-dev) - cluster-addslots: async func(connection: borrow, slots: list) -> result<_, error>; + cluster-addslots: async func(connection: borrow, slots: list) -> result<_, error-code>; /// Assigns new hash slot ranges to a node. /// /// CLUSTER ADDSLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] /// https://valkey.io/commands/cluster-addslotsrange/ @since(version = 0.3.0-dev) - cluster-addslotsrange: async func(connection: borrow, ranges: list>) -> result<_, error>; + cluster-addslotsrange: async func(connection: borrow, ranges: list>) -> result<_, error-code>; /// Advances the cluster config epoch. /// /// CLUSTER BUMPEPOCH /// https://valkey.io/commands/cluster-bumpepoch/ @since(version = 0.3.0-dev) - cluster-bumpepoch: async func(connection: borrow) -> result; + cluster-bumpepoch: async func(connection: borrow) -> result; /// Cancel all current ongoing slot migration operations. /// /// CLUSTER CANCELSLOTMIGRATIONS /// https://valkey.io/commands/cluster-cancelslotmigrations/ @since(version = 0.3.0-dev) - cluster-cancelslotmigrations: async func(connection: borrow) -> result<_, error>; + cluster-cancelslotmigrations: async func(connection: borrow) -> result<_, error-code>; /// Returns the number of active failure reports for a node. /// /// CLUSTER COUNT-FAILURE-REPORTS node-id /// https://valkey.io/commands/cluster-count-failure-reports/ @since(version = 0.3.0-dev) - cluster-count-failure-reports: async func(connection: borrow, node-id: string) -> result; + cluster-count-failure-reports: async func(connection: borrow, node-id: string) -> result; /// Sets hash slots as unbound for a node. /// /// CLUSTER DELSLOTS slot [ slot ... ] /// https://valkey.io/commands/cluster-delslots/ @since(version = 0.3.0-dev) - cluster-delslots: async func(connection: borrow, slots: list) -> result<_, error>; + cluster-delslots: async func(connection: borrow, slots: list) -> result<_, error-code>; /// Sets hash slot ranges as unbound for a node. /// /// CLUSTER DELSLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] /// https://valkey.io/commands/cluster-delslotsrange/ @since(version = 0.3.0-dev) - cluster-delslotsrange: async func(connection: borrow, ranges: list>) -> result<_, error>; + cluster-delslotsrange: async func(connection: borrow, ranges: list>) -> result<_, error-code>; /// Forces a replica to perform a manual failover of its primary. /// /// CLUSTER FAILOVER [ FORCE | TAKEOVER ] /// https://valkey.io/commands/cluster-failover/ @since(version = 0.3.0-dev) - cluster-failover: async func(connection: borrow, mode: option) -> result<_, error>; + cluster-failover: async func(connection: borrow, mode: option) -> result<_, error-code>; /// Deletes all slots information from a node. /// /// CLUSTER FLUSHSLOTS /// https://valkey.io/commands/cluster-flushslots/ @since(version = 0.3.0-dev) - cluster-flushslots: async func(connection: borrow) -> result<_, error>; + cluster-flushslots: async func(connection: borrow) -> result<_, error-code>; /// Removes a node from the nodes table. /// /// CLUSTER FORGET node-id /// https://valkey.io/commands/cluster-forget/ @since(version = 0.3.0-dev) - cluster-forget: async func(connection: borrow, node-id: string) -> result<_, error>; + cluster-forget: async func(connection: borrow, node-id: string) -> result<_, error-code>; /// Gets the status of ongoing and recently finished slot import and export operations. /// /// CLUSTER GETSLOTMIGRATIONS /// https://valkey.io/commands/cluster-getslotmigrations/ @since(version = 0.3.0-dev) - cluster-getslotmigrations: async func(connection: borrow) -> result, error>; + cluster-getslotmigrations: async func(connection: borrow) -> result, error-code>; /// Forces a node to handshake with another node. /// /// CLUSTER MEET ip port [ cluster-bus-port ] /// https://valkey.io/commands/cluster-meet/ @since(version = 0.3.0-dev) - cluster-meet: async func(connection: borrow, ip: string, port: u16, cluster-bus-port: option) -> result<_, error>; + cluster-meet: async func(connection: borrow, ip: string, port: u16, cluster-bus-port: option) -> result<_, error-code>; /// Migrate the given slots from this node to the specified nodes. /// /// CLUSTER MIGRATESLOTS SLOTSRANGE start-slot end-slot [ start-slot end-slot ... ] NODE node-id [ AUTH username password ] [ SLOTSRANGE ... ] /// https://valkey.io/commands/cluster-migrateslots/ @since(version = 0.3.0-dev) - cluster-migrateslots: async func(connection: borrow, migrations: list) -> result<_, error>; + cluster-migrateslots: async func(connection: borrow, migrations: list) -> result<_, error-code>; /// Lists the replica nodes of a primary node. /// /// CLUSTER REPLICAS node-id /// https://valkey.io/commands/cluster-replicas/ @since(version = 0.3.0-dev) - cluster-replicas: async func(connection: borrow, node-id: string) -> result, error>; + cluster-replicas: async func(connection: borrow, node-id: string) -> result, error-code>; /// Configures a node as replica of a primary node or detaches a replica from its primary. /// @@ -341,168 +341,168 @@ interface commands { /// CLUSTER REPLICATE 〈 node-id | NO ONE 〉 /// https://valkey.io/commands/cluster-replicate/ @since(version = 0.3.0-dev) - cluster-replicate: async func(connection: borrow, node-id: option) -> result<_, error>; + cluster-replicate: async func(connection: borrow, node-id: option) -> result<_, error-code>; /// Resets a node. /// /// CLUSTER RESET [ HARD | SOFT ] /// https://valkey.io/commands/cluster-reset/ @since(version = 0.3.0-dev) - cluster-reset: async func(connection: borrow, mode: option) -> result<_, error>; + cluster-reset: async func(connection: borrow, mode: option) -> result<_, error-code>; /// Forces a node to save the cluster configuration to disk. /// /// CLUSTER SAVECONFIG /// https://valkey.io/commands/cluster-saveconfig/ @since(version = 0.3.0-dev) - cluster-saveconfig: async func(connection: borrow) -> result<_, error>; + cluster-saveconfig: async func(connection: borrow) -> result<_, error-code>; /// Sets the configuration epoch for a new node. /// /// CLUSTER SET-CONFIG-EPOCH config-epoch /// https://valkey.io/commands/cluster-set-config-epoch/ @since(version = 0.3.0-dev) - cluster-set-config-epoch: async func(connection: borrow, config-epoch: integer) -> result<_, error>; + cluster-set-config-epoch: async func(connection: borrow, config-epoch: integer) -> result<_, error-code>; /// Binds a hash slot to a node. /// /// CLUSTER SETSLOT slot 〈 IMPORTING node-id | MIGRATING node-id | NODE node-id | STABLE 〉 [ TIMEOUT timeout ] /// https://valkey.io/commands/cluster-setslot/ @since(version = 0.3.0-dev) - cluster-setslot: async func(connection: borrow, slot: u16, state: cluster-setslot-state, timeout: option) -> result<_, error>; + cluster-setslot: async func(connection: borrow, slot: u16, state: cluster-setslot-state, timeout: option) -> result<_, error-code>; /// Returns the specified command log's entries. /// /// COMMANDLOG GET count 〈 slow | large-request | large-reply 〉 /// https://valkey.io/commands/commandlog-get/ @since(version = 0.3.0-dev) - commandlog-get: async func(connection: borrow, count: integer, %type: commandlog-type) -> result, error>; + commandlog-get: async func(connection: borrow, count: integer, %type: commandlog-type) -> result, error-code>; /// Returns the number of entries in the specified type of command log. /// /// COMMANDLOG LEN 〈 slow | large-request | large-reply 〉 /// https://valkey.io/commands/commandlog-len/ @since(version = 0.3.0-dev) - commandlog-len: async func(connection: borrow, %type: commandlog-type) -> result; + commandlog-len: async func(connection: borrow, %type: commandlog-type) -> result; /// Clears all entries from the specified type of command log. /// /// COMMANDLOG RESET 〈 slow | large-request | large-reply 〉 /// https://valkey.io/commands/commandlog-reset/ @since(version = 0.3.0-dev) - commandlog-reset: async func(connection: borrow, %type: commandlog-type) -> result<_, error>; + commandlog-reset: async func(connection: borrow, %type: commandlog-type) -> result<_, error-code>; /// Returns the effective values of configuration parameters. /// /// CONFIG GET parameter [ parameter ... ] /// https://valkey.io/commands/config-get/ @since(version = 0.3.0-dev) - config-get: async func(connection: borrow, parameters: list) -> result, error>; + config-get: async func(connection: borrow, parameters: list) -> result, error-code>; /// Resets the server's statistics. /// /// CONFIG RESETSTAT /// https://valkey.io/commands/config-resetstat/ @since(version = 0.3.0-dev) - config-resetstat: async func(connection: borrow) -> result<_, error>; + config-resetstat: async func(connection: borrow) -> result<_, error-code>; /// Persists the effective configuration to file. /// /// CONFIG REWRITE /// https://valkey.io/commands/config-rewrite/ @since(version = 0.3.0-dev) - config-rewrite: async func(connection: borrow) -> result<_, error>; + config-rewrite: async func(connection: borrow) -> result<_, error-code>; /// Sets configuration parameters in-flight. /// /// CONFIG SET parameter value [ parameter value ... ] /// https://valkey.io/commands/config-set/ @since(version = 0.3.0-dev) - config-set: async func(connection: borrow, parameters-and-values: list>) -> result<_, error>; + config-set: async func(connection: borrow, parameters-and-values: list>) -> result<_, error-code>; /// Starts a coordinated failover from a server to one of its replicas. /// /// FAILOVER [ TO host port [ FORCE ] ] [ ABORT ] [ TIMEOUT milliseconds ] /// https://valkey.io/commands/failover/ @since(version = 0.3.0-dev) - failover: async func(connection: borrow, target: option, abort: bool, timeout: option) -> result<_, error>; + failover: async func(connection: borrow, target: option, abort: bool, timeout: option) -> result<_, error-code>; /// Returns the Unix timestamp of the last successful save to disk. /// /// LASTSAVE /// https://valkey.io/commands/lastsave/ @since(version = 0.3.0-dev) - lastsave: async func(connection: borrow) -> result; + lastsave: async func(connection: borrow) -> result; /// Returns a human-readable latency analysis report. /// /// LATENCY DOCTOR /// https://valkey.io/commands/latency-doctor/ @since(version = 0.3.0-dev) - latency-doctor: async func(connection: borrow) -> result; + latency-doctor: async func(connection: borrow) -> result; /// Returns a latency graph for an event. /// /// LATENCY GRAPH event /// https://valkey.io/commands/latency-graph/ @since(version = 0.3.0-dev) - latency-graph: async func(connection: borrow, event: string) -> result; + latency-graph: async func(connection: borrow, event: string) -> result; /// Returns the cumulative distribution of latencies of a subset or all commands. /// /// LATENCY HISTOGRAM [ command [ command ... ] ] /// https://valkey.io/commands/latency-histogram/ @since(version = 0.3.0-dev) - latency-histogram: async func(connection: borrow, commands: option>) -> result, error>; + latency-histogram: async func(connection: borrow, commands: option>) -> result, error-code>; /// Returns timestamp-latency samples for an event. /// /// LATENCY HISTORY event /// https://valkey.io/commands/latency-history/ @since(version = 0.3.0-dev) - latency-history: async func(connection: borrow, event: string) -> result>, error>; + latency-history: async func(connection: borrow, event: string) -> result>, error-code>; /// Returns the latest latency samples for all events. /// /// LATENCY LATEST /// https://valkey.io/commands/latency-latest/ @since(version = 0.3.0-dev) - latency-latest: async func(connection: borrow) -> result, error>; + latency-latest: async func(connection: borrow) -> result, error-code>; /// Resets the latency data for one or more events. /// /// LATENCY RESET [ event [ event ... ] ] /// https://valkey.io/commands/latency-reset/ @since(version = 0.3.0-dev) - latency-reset: async func(connection: borrow, events: option>) -> result; + latency-reset: async func(connection: borrow, events: option>) -> result; /// Returns all loaded modules. /// /// MODULE LIST /// https://valkey.io/commands/module-list/ @since(version = 0.3.0-dev) - module-list: async func(connection: borrow) -> result, error>; + module-list: async func(connection: borrow) -> result, error-code>; /// Loads a module. /// /// MODULE LOAD path [ arg [ arg ... ] ] /// https://valkey.io/commands/module-load/ @since(version = 0.3.0-dev) - module-load: async func(connection: borrow, path: string, args: option>) -> result<_, error>; + module-load: async func(connection: borrow, path: string, args: option>) -> result<_, error-code>; /// Loads a module using extended parameters. /// /// MODULE LOADEX path [ CONFIG name value [ CONFIG name value ... ] ] [ ARGS args [ args ... ] ] /// https://valkey.io/commands/module-loadex/ @since(version = 0.3.0-dev) - module-loadex: async func(connection: borrow, path: string, configs: option>>, args: option>) -> result<_, error>; + module-loadex: async func(connection: borrow, path: string, configs: option>>, args: option>) -> result<_, error-code>; /// Unloads a module. /// /// MODULE UNLOAD name /// https://valkey.io/commands/module-unload/ @since(version = 0.3.0-dev) - module-unload: async func(connection: borrow, name: string) -> result<_, error>; + module-unload: async func(connection: borrow, name: string) -> result<_, error-code>; /// Configures a server as replica of another, or promotes it to a primary. /// @@ -511,21 +511,21 @@ interface commands { /// REPLICAOF 〈 host port | NO ONE 〉 /// https://valkey.io/commands/replicaof/ @since(version = 0.3.0-dev) - replicaof: async func(connection: borrow, primary: option>) -> result; + replicaof: async func(connection: borrow, primary: option>) -> result; /// Returns the replication role. /// /// ROLE /// https://valkey.io/commands/role/ @since(version = 0.3.0-dev) - role: async func(connection: borrow) -> result; + role: async func(connection: borrow) -> result; /// Synchronously saves the database(s) to disk. /// /// SAVE /// https://valkey.io/commands/save/ @since(version = 0.3.0-dev) - save: async func(connection: borrow) -> result<_, error>; + save: async func(connection: borrow) -> result<_, error-code>; /// Synchronously saves the database(s) to disk and shuts down the server. /// @@ -537,7 +537,7 @@ interface commands { /// SHUTDOWN [ NOSAVE | SAVE ] [ NOW ] [ FORCE ] [ SAFE ] [ FAILOVER ] /// https://valkey.io/commands/shutdown/ @since(version = 0.3.0-dev) - shutdown: async func(connection: borrow, save: option, modifiers: shutdown-flags) -> result<_, error>; + shutdown: async func(connection: borrow, save: option, modifiers: shutdown-flags) -> result<_, error-code>; /// Synchronously saves the database(s) to disk and shuts down the server. /// @@ -548,35 +548,35 @@ interface commands { /// SHUTDOWN ABORT /// https://valkey.io/commands/shutdown/ @since(version = 0.3.0-dev) - shutdown-abort: async func(connection: borrow) -> result<_, error>; + shutdown-abort: async func(connection: borrow) -> result<_, error-code>; /// Returns the slow log's entries. /// /// SLOWLOG GET [ count ] /// https://valkey.io/commands/slowlog-get/ @since(version = 0.3.0-dev) - slowlog-get: async func(connection: borrow, count: option) -> result, error>; + slowlog-get: async func(connection: borrow, count: option) -> result, error-code>; /// Returns the number of entries in the slow log. /// /// SLOWLOG LEN /// https://valkey.io/commands/slowlog-len/ @since(version = 0.3.0-dev) - slowlog-len: async func(connection: borrow) -> result; + slowlog-len: async func(connection: borrow) -> result; /// Clears all entries from the slow log. /// /// SLOWLOG RESET /// https://valkey.io/commands/slowlog-reset/ @since(version = 0.3.0-dev) - slowlog-reset: async func(connection: borrow) -> result<_, error>; + slowlog-reset: async func(connection: borrow) -> result<_, error-code>; /// Counts the number of set bits (population counting) in a string. /// /// BITCOUNT key [ start [ end [ BYTE | BIT ] ] ] /// https://valkey.io/commands/bitcount/ @since(version = 0.3.0-dev) - bitcount: async func(connection: borrow, key: string, range: option) -> result; + bitcount: async func(connection: borrow, key: string, range: option) -> result; /// Performs arbitrary bitfield integer operations on strings. /// @@ -585,21 +585,21 @@ interface commands { /// BITFIELD key [ GET encoding offset | [ OVERFLOW WRAP | SAT | FAIL ] 〈 SET encoding offset value | INCRBY encoding offset increment 〉 [ GET encoding offset | [ OVERFLOW WRAP | SAT | FAIL ] 〈 SET encoding offset value | INCRBY encoding offset increment 〉 ... ] ] /// https://valkey.io/commands/bitfield/ @since(version = 0.3.0-dev) - bitfield: async func(connection: borrow, key: string, operations: option>) -> result>, error>; + bitfield: async func(connection: borrow, key: string, operations: option>) -> result>, error-code>; /// Performs arbitrary read-only bitfield integer operations on strings. /// /// BITFIELD_RO key [ GET encoding offset [ GET encoding offset ... ] ] /// https://valkey.io/commands/bitfield_ro/ @since(version = 0.3.0-dev) - bitfield-ro: async func(connection: borrow, key: string, gets: option>) -> result, error>; + bitfield-ro: async func(connection: borrow, key: string, gets: option>) -> result, error-code>; /// Performs bitwise operations on multiple strings, and stores the result. /// /// BITOP 〈 AND | OR | XOR | NOT 〉 destkey key [ key ... ] /// https://valkey.io/commands/bitop/ @since(version = 0.3.0-dev) - bitop: async func(connection: borrow, destination: string, operation: bitop-operation) -> result; + bitop: async func(connection: borrow, destination: string, operation: bitop-operation) -> result; /// Finds the first set (1) or clear (0) bit in a string. /// @@ -608,21 +608,21 @@ interface commands { /// BITPOS key bit [ start [ end [ BYTE | BIT ] ] ] /// https://valkey.io/commands/bitpos/ @since(version = 0.3.0-dev) - bitpos: async func(connection: borrow, key: string, bit: bool, range: option) -> result, error>; + bitpos: async func(connection: borrow, key: string, bit: bool, range: option) -> result, error-code>; /// Returns a bit value by offset. /// /// GETBIT key offset /// https://valkey.io/commands/getbit/ @since(version = 0.3.0-dev) - getbit: async func(connection: borrow, key: string, offset: u64) -> result; + getbit: async func(connection: borrow, key: string, offset: u64) -> result; /// Sets or clears the bit at offset of the string value. /// /// SETBIT key offset value /// https://valkey.io/commands/setbit/ @since(version = 0.3.0-dev) - setbit: async func(connection: borrow, key: string, offset: u64, value: bool) -> result; + setbit: async func(connection: borrow, key: string, offset: u64, value: bool) -> result; /// Signals that a cluster client is following an -ASK redirect. /// @@ -632,14 +632,14 @@ interface commands { /// ASKING /// https://valkey.io/commands/asking/ @since(version = 0.3.0-dev) - asking: async func(connection: borrow) -> result<_, error>; + asking: async func(connection: borrow) -> result<_, error-code>; /// Authenticates the connection. /// /// AUTH [ username ] password /// https://valkey.io/commands/auth/ @since(version = 0.3.0-dev) - auth: async func(connection: borrow, username: option, password: string) -> result<_, error>; + auth: async func(connection: borrow, username: option, password: string) -> result<_, error-code>; /// Instructs the server whether to track the keys in the next request. /// @@ -649,35 +649,35 @@ interface commands { /// CLIENT CACHING 〈 YES | NO 〉 /// https://valkey.io/commands/client-caching/ @since(version = 0.3.0-dev) - client-caching: async func(connection: borrow, enabled: bool) -> result<_, error>; + client-caching: async func(connection: borrow, enabled: bool) -> result<_, error-code>; /// A client claims its capability. /// /// CLIENT CAPA capability [ capability ... ] /// https://valkey.io/commands/client-capa/ @since(version = 0.3.0-dev) - client-capa: async func(connection: borrow, capabilities: list) -> result<_, error>; + client-capa: async func(connection: borrow, capabilities: list) -> result<_, error-code>; /// Returns the name of the connection. /// /// CLIENT GETNAME /// https://valkey.io/commands/client-getname/ @since(version = 0.3.0-dev) - client-getname: async func(connection: borrow) -> result, error>; + client-getname: async func(connection: borrow) -> result, error-code>; /// Returns the unique client ID of the connection. /// /// CLIENT ID /// https://valkey.io/commands/client-id/ @since(version = 0.3.0-dev) - client-id: async func(connection: borrow) -> result; + client-id: async func(connection: borrow) -> result; /// Marks this client as an import source when the server is in import mode. /// /// CLIENT IMPORT-SOURCE 〈 ON | OFF 〉 /// https://valkey.io/commands/client-import-source/ @since(version = 0.3.0-dev) - client-import-source: async func(connection: borrow, enabled: bool) -> result<_, error>; + client-import-source: async func(connection: borrow, enabled: bool) -> result<_, error-code>; /// Returns information about the connection. /// @@ -686,7 +686,7 @@ interface commands { /// CLIENT INFO /// https://valkey.io/commands/client-info/ @since(version = 0.3.0-dev) - client-info: async func(connection: borrow) -> result, error>; + client-info: async func(connection: borrow) -> result, error-code>; /// Controls whether commands sent by the client affect the LRU/LFU of accessed keys. /// @@ -695,21 +695,21 @@ interface commands { /// CLIENT NO-TOUCH 〈 ON | OFF 〉 /// https://valkey.io/commands/client-no-touch/ @since(version = 0.3.0-dev) - client-no-touch: async func(connection: borrow, enabled: bool) -> result<_, error>; + client-no-touch: async func(connection: borrow, enabled: bool) -> result<_, error-code>; /// Sets information specific to the client or connection. /// /// CLIENT SETINFO 〈 LIB-NAME libname | LIB-VER libver 〉 /// https://valkey.io/commands/client-setinfo/ @since(version = 0.3.0-dev) - client-setinfo: async func(connection: borrow, attr: client-setinfo-attr) -> result<_, error>; + client-setinfo: async func(connection: borrow, attr: client-setinfo-attr) -> result<_, error-code>; /// Sets the connection name. /// /// CLIENT SETNAME connection-name /// https://valkey.io/commands/client-setname/ @since(version = 0.3.0-dev) - client-setname: async func(connection: borrow, connection-name: string) -> result<_, error>; + client-setname: async func(connection: borrow, connection-name: string) -> result<_, error-code>; /// Controls server-assisted client-side caching for the connection. /// @@ -719,28 +719,28 @@ interface commands { /// CLIENT TRACKING 〈 ON | OFF 〉 [ PREFIX prefix [ PREFIX prefix ... ] ] [ BCAST ] [ OPTIN ] [ OPTOUT ] [ NOLOOP ] /// https://valkey.io/commands/client-tracking/ @since(version = 0.3.0-dev) - client-tracking: async func(connection: borrow, options: option) -> result<_, error>; + client-tracking: async func(connection: borrow, options: option) -> result<_, error-code>; /// Returns information about server-assisted client-side caching for the connection. /// /// CLIENT TRACKINGINFO /// https://valkey.io/commands/client-trackinginfo/ @since(version = 0.3.0-dev) - client-trackinginfo: async func(connection: borrow) -> result; + client-trackinginfo: async func(connection: borrow) -> result; /// Returns detailed information about all commands. /// /// COMMAND /// https://valkey.io/commands/command/ @since(version = 0.3.0-dev) - command: async func(connection: borrow) -> result, error>; + command: async func(connection: borrow) -> result, error-code>; /// Returns a count of commands. /// /// COMMAND COUNT /// https://valkey.io/commands/command-count/ @since(version = 0.3.0-dev) - command-count: async func(connection: borrow) -> result; + command-count: async func(connection: borrow) -> result; /// Returns documentary information about one, multiple or all commands. /// @@ -749,21 +749,21 @@ interface commands { /// COMMAND DOCS [ command-name [ command-name ... ] ] /// https://valkey.io/commands/command-docs/ @since(version = 0.3.0-dev) - command-docs: async func(connection: borrow, command-names: option>) -> result, error>; + command-docs: async func(connection: borrow, command-names: option>) -> result, error-code>; /// Extracts the key names from an arbitrary command. /// /// COMMAND GETKEYS command [ arg [ arg ... ] ] /// https://valkey.io/commands/command-getkeys/ @since(version = 0.3.0-dev) - command-getkeys: async func(connection: borrow, command: list) -> result, error>; + command-getkeys: async func(connection: borrow, command: list) -> result, error-code>; /// Extracts the key names and access flags for an arbitrary command. /// /// COMMAND GETKEYSANDFLAGS command [ arg [ arg ... ] ] /// https://valkey.io/commands/command-getkeysandflags/ @since(version = 0.3.0-dev) - command-getkeysandflags: async func(connection: borrow, command: list) -> result>>, error>; + command-getkeysandflags: async func(connection: borrow, command: list) -> result>>, error-code>; /// Returns information about one, multiple or all commands. /// @@ -772,28 +772,28 @@ interface commands { /// COMMAND INFO [ command-name [ command-name ... ] ] /// https://valkey.io/commands/command-info/ @since(version = 0.3.0-dev) - command-info: async func(connection: borrow, command-names: option>) -> result, error>; + command-info: async func(connection: borrow, command-names: option>) -> result, error-code>; /// Returns a list of command names. /// /// COMMAND LIST [ FILTERBY 〈 MODULE module-name | ACLCAT category | PATTERN pattern 〉 ] /// https://valkey.io/commands/command-list/ @since(version = 0.3.0-dev) - command-list: async func(connection: borrow, filter: option) -> result, error>; + command-list: async func(connection: borrow, filter: option) -> result, error-code>; /// Returns the given string. /// /// ECHO message /// https://valkey.io/commands/echo/ @since(version = 0.3.0-dev) - echo: async func(connection: borrow, message: string-or-bytes) -> result; + echo: async func(connection: borrow, message: string-or-bytes) -> result; /// Returns the server's liveliness response. /// /// PING [ message ] /// https://valkey.io/commands/ping/ @since(version = 0.3.0-dev) - ping: async func(connection: borrow, message: option) -> result<_, error>; + ping: async func(connection: borrow, message: option) -> result<_, error-code>; /// Enables read-only queries for a connection to a Valkey replica node. /// @@ -802,7 +802,7 @@ interface commands { /// READONLY /// https://valkey.io/commands/readonly/ @since(version = 0.3.0-dev) - readonly: async func(connection: borrow) -> result<_, error>; + readonly: async func(connection: borrow) -> result<_, error-code>; /// Enables read-write queries for a connection to a Valkey replica node. /// @@ -811,7 +811,7 @@ interface commands { /// READWRITE /// https://valkey.io/commands/readwrite/ @since(version = 0.3.0-dev) - readwrite: async func(connection: borrow) -> result<_, error>; + readwrite: async func(connection: borrow) -> result<_, error-code>; /// Changes the selected database. /// @@ -820,7 +820,7 @@ interface commands { /// SELECT index /// https://valkey.io/commands/select/ @since(version = 0.3.0-dev) - select: async func(connection: borrow, index: integer) -> result<_, error>; + select: async func(connection: borrow, index: integer) -> result<_, error-code>; /// Blocks until the asynchronous replication of all preceding write commands sent by the connection is completed. /// @@ -830,7 +830,7 @@ interface commands { /// WAIT numreplicas timeout /// https://valkey.io/commands/wait/ @since(version = 0.3.0-dev) - wait: async func(connection: borrow, numreplicas: integer, timeout: duration-milliseconds) -> result; + wait: async func(connection: borrow, numreplicas: integer, timeout: duration-milliseconds) -> result; /// Blocks until all of the preceding write commands sent by the connection are written to the append-only file of the primary and/or replicas. /// @@ -840,35 +840,35 @@ interface commands { /// WAITAOF numlocal numreplicas timeout /// https://valkey.io/commands/waitaof/ @since(version = 0.3.0-dev) - waitaof: async func(connection: borrow, numlocal: integer, numreplicas: integer, timeout: duration-milliseconds) -> result, error>; + waitaof: async func(connection: borrow, numlocal: integer, numreplicas: integer, timeout: duration-milliseconds) -> result, error-code>; /// Adds one or more members to a geospatial index. /// /// GEOADD key [ NX | XX ] [ CH ] longitude latitude member [ longitude latitude member ... ] /// https://valkey.io/commands/geoadd/ @since(version = 0.3.0-dev) - geoadd: async func(connection: borrow, key: string, condition: option, changed: bool, coordinates-and-members: list>) -> result; + geoadd: async func(connection: borrow, key: string, condition: option, changed: bool, coordinates-and-members: list>) -> result; /// Returns the distance between two members of a geospatial index. /// /// GEODIST key member1 member2 [ m | km | ft | mi ] /// https://valkey.io/commands/geodist/ @since(version = 0.3.0-dev) - geodist: async func(connection: borrow, key: string, member1: string-or-bytes, member2: string-or-bytes, unit: option) -> result, error>; + geodist: async func(connection: borrow, key: string, member1: string-or-bytes, member2: string-or-bytes, unit: option) -> result, error-code>; /// Returns members from a geospatial index as geohash strings. /// /// GEOHASH key [ member [ member ... ] ] /// https://valkey.io/commands/geohash/ @since(version = 0.3.0-dev) - geohash: async func(connection: borrow, key: string, members: option>) -> result>, error>; + geohash: async func(connection: borrow, key: string, members: option>) -> result>, error-code>; /// Returns the longitude and latitude of members from a geospatial index. /// /// GEOPOS key [ member [ member ... ] ] /// https://valkey.io/commands/geopos/ @since(version = 0.3.0-dev) - geopos: async func(connection: borrow, key: string, members: option>) -> result>, error>; + geopos: async func(connection: borrow, key: string, members: option>) -> result>, error-code>; /// Queries a geospatial index for members inside an area of a box, circle, or a polygon. /// @@ -877,7 +877,7 @@ interface commands { /// GEOSEARCH key [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] /// https://valkey.io/commands/geosearch/ @since(version = 0.3.0-dev) - geosearch: async func(connection: borrow, key: string, shape: geosearch-shape, order: option, count: option) -> result, error>; + geosearch: async func(connection: borrow, key: string, shape: geosearch-shape, order: option, count: option) -> result, error-code>; /// Queries a geospatial index for members inside an area of a box, circle, or a polygon. /// @@ -888,154 +888,154 @@ interface commands { /// GEOSEARCH key [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] [ WITHCOORD ] [ WITHDIST ] [ WITHHASH ] /// https://valkey.io/commands/geosearch/ @since(version = 0.3.0-dev) - geosearch-with: async func(connection: borrow, key: string, shape: geosearch-shape, order: option, count: option, details: geosearch-with-flags) -> result, error>; + geosearch-with: async func(connection: borrow, key: string, shape: geosearch-shape, order: option, count: option, details: geosearch-with-flags) -> result, error-code>; /// Queries a geospatial index for members inside an area of a box, a circle, or a polygon, optionally stores the result. /// /// GEOSEARCHSTORE destination source [ FROMMEMBER member | FROMLONLAT longitude latitude ] 〈 BYRADIUS radius 〈 m | km | ft | mi 〉 | BYBOX width height 〈 m | km | ft | mi 〉 | BYPOLYGON num-vertices longitude latitude [ longitude latitude ... ] 〉 [ ASC | DESC ] [ COUNT count [ ANY ] ] [ STOREDIST ] /// https://valkey.io/commands/geosearchstore/ @since(version = 0.3.0-dev) - geosearchstore: async func(connection: borrow, destination: string, source: string, shape: geosearchstore-shape, order: option, count: option, storedist: bool) -> result; + geosearchstore: async func(connection: borrow, destination: string, source: string, shape: geosearchstore-shape, order: option, count: option, storedist: bool) -> result; /// Deletes one or more fields and their values from a hash. /// /// HDEL key field [ field ... ] /// https://valkey.io/commands/hdel/ @since(version = 0.3.0-dev) - hdel: async func(connection: borrow, key: string, fields: list) -> result; + hdel: async func(connection: borrow, key: string, fields: list) -> result; /// Determines whether a field exists in a hash. /// /// HEXISTS key field /// https://valkey.io/commands/hexists/ @since(version = 0.3.0-dev) - hexists: async func(connection: borrow, key: string, field: string) -> result; + hexists: async func(connection: borrow, key: string, field: string) -> result; /// Sets the expiration time of one or more hash fields in seconds. /// /// HEXPIRE key seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpire/ @since(version = 0.3.0-dev) - hexpire: async func(connection: borrow, key: string, seconds: duration-seconds, mode: option, fields: list) -> result, error>; + hexpire: async func(connection: borrow, key: string, seconds: duration-seconds, mode: option, fields: list) -> result, error-code>; /// Sets the expiration time of one or more hash fields to a Unix timestamp. /// /// HEXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpireat/ @since(version = 0.3.0-dev) - hexpireat: async func(connection: borrow, key: string, seconds: unix-time-seconds, mode: option, fields: list) -> result, error>; + hexpireat: async func(connection: borrow, key: string, seconds: unix-time-seconds, mode: option, fields: list) -> result, error-code>; /// Returns Unix timestamps in seconds since the epoch at which the given key's field(s) will expire. /// /// HEXPIRETIME key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hexpiretime/ @since(version = 0.3.0-dev) - hexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; + hexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error-code>; /// Returns the value of a field in a hash. /// /// HGET key field /// https://valkey.io/commands/hget/ @since(version = 0.3.0-dev) - hget: async func(connection: borrow, key: string, field: string) -> result, error>; + hget: async func(connection: borrow, key: string, field: string) -> result, error-code>; /// Returns all fields and values in a hash. /// /// HGETALL key /// https://valkey.io/commands/hgetall/ @since(version = 0.3.0-dev) - hgetall: async func(connection: borrow, key: string) -> result, error>; + hgetall: async func(connection: borrow, key: string) -> result, error-code>; /// Returns the values of one or more fields and deletes them from a hash. /// /// HGETDEL key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hgetdel/ @since(version = 0.3.0-dev) - hgetdel: async func(connection: borrow, key: string, fields: list) -> result>, error>; + hgetdel: async func(connection: borrow, key: string, fields: list) -> result>, error-code>; /// Gets the value of one or more fields of a given hash key, and optionally sets their expiration time or time-to-live (TTL). /// /// HGETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hgetex/ @since(version = 0.3.0-dev) - hgetex: async func(connection: borrow, key: string, ttl: option, fields: list) -> result>, error>; + hgetex: async func(connection: borrow, key: string, ttl: option, fields: list) -> result>, error-code>; /// Increments the integer value of a field in a hash by a number. /// /// HINCRBY key field increment /// https://valkey.io/commands/hincrby/ @since(version = 0.3.0-dev) - hincrby: async func(connection: borrow, key: string, field: string, increment: integer) -> result; + hincrby: async func(connection: borrow, key: string, field: string, increment: integer) -> result; /// Increments the floating point value of a field by a number. /// /// HINCRBYFLOAT key field increment /// https://valkey.io/commands/hincrbyfloat/ @since(version = 0.3.0-dev) - hincrbyfloat: async func(connection: borrow, key: string, field: string, increment: double) -> result; + hincrbyfloat: async func(connection: borrow, key: string, field: string, increment: double) -> result; /// Returns all fields in a hash. /// /// HKEYS key /// https://valkey.io/commands/hkeys/ @since(version = 0.3.0-dev) - hkeys: async func(connection: borrow, key: string) -> result, error>; + hkeys: async func(connection: borrow, key: string) -> result, error-code>; /// Returns the number of fields in a hash. /// /// HLEN key /// https://valkey.io/commands/hlen/ @since(version = 0.3.0-dev) - hlen: async func(connection: borrow, key: string) -> result; + hlen: async func(connection: borrow, key: string) -> result; /// Returns the values of all fields in a hash. /// /// HMGET key field [ field ... ] /// https://valkey.io/commands/hmget/ @since(version = 0.3.0-dev) - hmget: async func(connection: borrow, key: string, fields: list) -> result>, error>; + hmget: async func(connection: borrow, key: string, fields: list) -> result>, error-code>; /// Sets the values of multiple fields. /// /// HMSET key field value [ field value ... ] /// https://valkey.io/commands/hmset/ @since(version = 0.3.0-dev) - hmset: async func(connection: borrow, key: string, field-and-values: list>) -> result<_, error>; + hmset: async func(connection: borrow, key: string, field-and-values: list>) -> result<_, error-code>; /// Remove the existing expiration on a hash key's field(s). /// /// HPERSIST key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpersist/ @since(version = 0.3.0-dev) - hpersist: async func(connection: borrow, key: string, fields: list) -> result, error>; + hpersist: async func(connection: borrow, key: string, fields: list) -> result, error-code>; /// Sets the expiration time of one or more hash fields in milliseconds. /// /// HPEXPIRE key milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpire/ @since(version = 0.3.0-dev) - hpexpire: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, mode: option, fields: list) -> result, error>; + hpexpire: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, mode: option, fields: list) -> result, error-code>; /// Sets the expiration time of one or more hash fields to a Unix timestamp in milliseconds. /// /// HPEXPIREAT key unix-time-milliseconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpireat/ @since(version = 0.3.0-dev) - hpexpireat: async func(connection: borrow, key: string, milliseconds: unix-time-milliseconds, mode: option, fields: list) -> result, error>; + hpexpireat: async func(connection: borrow, key: string, milliseconds: unix-time-milliseconds, mode: option, fields: list) -> result, error-code>; /// Returns the Unix timestamp in milliseconds since Unix epoch at which the given key's field(s) will expire. /// /// HPEXPIRETIME key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpexpiretime/ @since(version = 0.3.0-dev) - hpexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error>; + hpexpiretime: async func(connection: borrow, key: string, fields: list) -> result, error-code>; /// Returns the remaining time to live in milliseconds of a hash key's field(s) that have an associated expiration. /// /// HPTTL key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/hpttl/ @since(version = 0.3.0-dev) - hpttl: async func(connection: borrow, key: string, fields: list) -> result, error>; + hpttl: async func(connection: borrow, key: string, fields: list) -> result, error-code>; /// Returns one or more random fields from a hash. /// @@ -1046,7 +1046,7 @@ interface commands { /// HRANDFIELD key [ count ] /// https://valkey.io/commands/hrandfield/ @since(version = 0.3.0-dev) - hrandfield: async func(connection: borrow, key: string, count: option) -> result, error>; + hrandfield: async func(connection: borrow, key: string, count: option) -> result, error-code>; /// Returns one or more random fields from a hash. /// @@ -1057,7 +1057,7 @@ interface commands { /// HRANDFIELD key count WITHVALUES /// https://valkey.io/commands/hrandfield/ @since(version = 0.3.0-dev) - hrandfield-withvalues: async func(connection: borrow, key: string, count: integer) -> result>, error>; + hrandfield-withvalues: async func(connection: borrow, key: string, count: integer) -> result>, error-code>; /// Iterates over fields and values of a hash. /// @@ -1066,7 +1066,7 @@ interface commands { /// HSCAN key cursor [ MATCH pattern ] [ COUNT count ] /// https://valkey.io/commands/hscan/ @since(version = 0.3.0-dev) - hscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, map>, error>; + hscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, map>, error-code>; /// Iterates over fields and values of a hash. /// @@ -1077,77 +1077,77 @@ interface commands { /// HSCAN key cursor [ MATCH pattern ] [ COUNT count ] NOVALUES /// https://valkey.io/commands/hscan/ @since(version = 0.3.0-dev) - hscan-novalues: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; + hscan-novalues: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error-code>; /// Creates or modifies the value of a field in a hash. /// /// HSET key field value [ field value ... ] /// https://valkey.io/commands/hset/ @since(version = 0.3.0-dev) - hset: async func(connection: borrow, key: string, field-and-values: list>) -> result; + hset: async func(connection: borrow, key: string, field-and-values: list>) -> result; /// Sets the value of one or more fields of a given hash key, and optionally sets their expiration time. /// /// HSETEX key [ NX | XX ] [ FNX | FXX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] FIELDS numfields field value [ field value ... ] /// https://valkey.io/commands/hsetex/ @since(version = 0.3.0-dev) - hsetex: async func(connection: borrow, key: string, condition: option, field-condition: option, ttl: option, field-and-values: list>) -> result; + hsetex: async func(connection: borrow, key: string, condition: option, field-condition: option, ttl: option, field-and-values: list>) -> result; /// Sets the value of a field in a hash only when the field doesn't exist. /// /// HSETNX key field value /// https://valkey.io/commands/hsetnx/ @since(version = 0.3.0-dev) - hsetnx: async func(connection: borrow, key: string, field: string, value: string-or-bytes) -> result; + hsetnx: async func(connection: borrow, key: string, field: string, value: string-or-bytes) -> result; /// Returns the length of the value of a field. /// /// HSTRLEN key field /// https://valkey.io/commands/hstrlen/ @since(version = 0.3.0-dev) - hstrlen: async func(connection: borrow, key: string, field: string) -> result; + hstrlen: async func(connection: borrow, key: string, field: string) -> result; /// Returns the remaining time to live (in seconds) of a hash key's field(s) that have an associated expiration. /// /// HTTL key FIELDS numfields field [ field ... ] /// https://valkey.io/commands/httl/ @since(version = 0.3.0-dev) - httl: async func(connection: borrow, key: string, fields: list) -> result, error>; + httl: async func(connection: borrow, key: string, fields: list) -> result, error-code>; /// Returns all values in a hash. /// /// HVALS key /// https://valkey.io/commands/hvals/ @since(version = 0.3.0-dev) - hvals: async func(connection: borrow, key: string) -> result, error>; + hvals: async func(connection: borrow, key: string) -> result, error-code>; /// Adds elements to a HyperLogLog key. /// /// PFADD key [ element [ element ... ] ] /// https://valkey.io/commands/pfadd/ @since(version = 0.3.0-dev) - pfadd: async func(connection: borrow, key: string, elements: option>) -> result; + pfadd: async func(connection: borrow, key: string, elements: option>) -> result; /// Returns the approximated cardinality of the set(s) observed by the HyperLogLog key(s). /// /// PFCOUNT key [ key ... ] /// https://valkey.io/commands/pfcount/ @since(version = 0.3.0-dev) - pfcount: async func(connection: borrow, keys: list) -> result; + pfcount: async func(connection: borrow, keys: list) -> result; /// Merges one or more HyperLogLog values into a single key. /// /// PFMERGE destkey [ sourcekey [ sourcekey ... ] ] /// https://valkey.io/commands/pfmerge/ @since(version = 0.3.0-dev) - pfmerge: async func(connection: borrow, destination: string, sources: option>) -> result<_, error>; + pfmerge: async func(connection: borrow, destination: string, sources: option>) -> result<_, error-code>; /// Removes all keys from the target slot. /// /// CLUSTER FLUSHSLOT slot [ ASYNC | SYNC ] /// https://valkey.io/commands/cluster-flushslot/ @since(version = 0.3.0-dev) - cluster-flushslot: async func(connection: borrow, slot: u16, mode: option) -> result<_, error>; + cluster-flushslot: async func(connection: borrow, slot: u16, mode: option) -> result<_, error-code>; /// Iterates over the keys in the cluster. /// @@ -1156,84 +1156,84 @@ interface commands { /// CLUSTERSCAN cursor [ MATCH match-pattern ] [ COUNT count ] [ TYPE type ] [ SLOT slot ] /// https://valkey.io/commands/clusterscan/ @since(version = 0.3.0-dev) - clusterscan: async func(connection: borrow, cursor: cursor, match: option, count: option, %type: option, slot: option) -> result, list>, error>; + clusterscan: async func(connection: borrow, cursor: cursor, match: option, count: option, %type: option, slot: option) -> result, list>, error-code>; /// Copies the value of a key to a new key. /// /// COPY source destination [ DB destination-db ] [ REPLACE ] /// https://valkey.io/commands/copy/ @since(version = 0.3.0-dev) - copy: async func(connection: borrow, source: string, destination: string, destination-db: option, replace: bool) -> result; + copy: async func(connection: borrow, source: string, destination: string, destination-db: option, replace: bool) -> result; /// Returns the number of keys in the database. /// /// DBSIZE /// https://valkey.io/commands/dbsize/ @since(version = 0.3.0-dev) - dbsize: async func(connection: borrow) -> result; + dbsize: async func(connection: borrow) -> result; /// Deletes one or more keys. /// /// DEL key [ key ... ] /// https://valkey.io/commands/del/ @since(version = 0.3.0-dev) - del: async func(connection: borrow, keys: list) -> result; + del: async func(connection: borrow, keys: list) -> result; /// Returns a serialized representation of the value stored at a key. /// /// DUMP key /// https://valkey.io/commands/dump/ @since(version = 0.3.0-dev) - dump: async func(connection: borrow, key: string) -> result, error>; + dump: async func(connection: borrow, key: string) -> result, error-code>; /// Determines whether one or more keys exist. /// /// EXISTS key [ key ... ] /// https://valkey.io/commands/exists/ @since(version = 0.3.0-dev) - exists: async func(connection: borrow, keys: list) -> result; + exists: async func(connection: borrow, keys: list) -> result; /// Sets the expiration time of a key in seconds. /// /// EXPIRE key seconds [ NX | XX | GT | LT ] /// https://valkey.io/commands/expire/ @since(version = 0.3.0-dev) - expire: async func(connection: borrow, key: string, seconds: duration-seconds, mode: option) -> result; + expire: async func(connection: borrow, key: string, seconds: duration-seconds, mode: option) -> result; /// Sets the expiration time of a key to a Unix timestamp. /// /// EXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] /// https://valkey.io/commands/expireat/ @since(version = 0.3.0-dev) - expireat: async func(connection: borrow, key: string, seconds: unix-time-seconds, mode: option) -> result; + expireat: async func(connection: borrow, key: string, seconds: unix-time-seconds, mode: option) -> result; /// Returns the expiration time of a key as a Unix timestamp. /// /// EXPIRETIME key /// https://valkey.io/commands/expiretime/ @since(version = 0.3.0-dev) - expiretime: async func(connection: borrow, key: string) -> result; + expiretime: async func(connection: borrow, key: string) -> result; /// Removes all keys from all databases. /// /// FLUSHALL [ ASYNC | SYNC ] /// https://valkey.io/commands/flushall/ @since(version = 0.3.0-dev) - flushall: async func(connection: borrow, mode: option) -> result<_, error>; + flushall: async func(connection: borrow, mode: option) -> result<_, error-code>; /// Removes all keys from the current database. /// /// FLUSHDB [ ASYNC | SYNC ] /// https://valkey.io/commands/flushdb/ @since(version = 0.3.0-dev) - flushdb: async func(connection: borrow, mode: option) -> result<_, error>; + flushdb: async func(connection: borrow, mode: option) -> result<_, error-code>; /// Returns all key names that match a pattern. /// /// KEYS pattern /// https://valkey.io/commands/keys/ @since(version = 0.3.0-dev) - keys: async func(connection: borrow, pattern: string) -> result, error>; + keys: async func(connection: borrow, pattern: string) -> result, error-code>; /// Atomically transfers a key from one instance to another. /// @@ -1242,98 +1242,98 @@ interface commands { /// MIGRATE host port "" destination-db timeout [ COPY ] [ REPLACE ] [ AUTH password | AUTH2 username password ] KEYS key [ key ... ] /// https://valkey.io/commands/migrate/ @since(version = 0.3.0-dev) - migrate: async func(connection: borrow, host: string, port: u16, destination-db: integer, timeout: duration-milliseconds, options: migrate-flags, auth: option, keys: list) -> result; + migrate: async func(connection: borrow, host: string, port: u16, destination-db: integer, timeout: duration-milliseconds, options: migrate-flags, auth: option, keys: list) -> result; /// Moves a key to another database. /// /// MOVE key db [ REPLACE ] /// https://valkey.io/commands/move/ @since(version = 0.3.0-dev) - move: async func(connection: borrow, key: string, db: integer, replace: bool) -> result; + move: async func(connection: borrow, key: string, db: integer, replace: bool) -> result; /// Returns the internal encoding of an object. /// /// OBJECT ENCODING key /// https://valkey.io/commands/object-encoding/ @since(version = 0.3.0-dev) - object-encoding: async func(connection: borrow, key: string) -> result, error>; + object-encoding: async func(connection: borrow, key: string) -> result, error-code>; /// Returns the logarithmic access frequency counter of an object. /// /// OBJECT FREQ key /// https://valkey.io/commands/object-freq/ @since(version = 0.3.0-dev) - object-freq: async func(connection: borrow, key: string) -> result, error>; + object-freq: async func(connection: borrow, key: string) -> result, error-code>; /// Returns the time since the last access to an object. /// /// OBJECT IDLETIME key /// https://valkey.io/commands/object-idletime/ @since(version = 0.3.0-dev) - object-idletime: async func(connection: borrow, key: string) -> result, error>; + object-idletime: async func(connection: borrow, key: string) -> result, error-code>; /// Returns the reference count of a value of a key. /// /// OBJECT REFCOUNT key /// https://valkey.io/commands/object-refcount/ @since(version = 0.3.0-dev) - object-refcount: async func(connection: borrow, key: string) -> result, error>; + object-refcount: async func(connection: borrow, key: string) -> result, error-code>; /// Removes the expiration time of a key. /// /// PERSIST key /// https://valkey.io/commands/persist/ @since(version = 0.3.0-dev) - persist: async func(connection: borrow, key: string) -> result; + persist: async func(connection: borrow, key: string) -> result; /// Sets the expiration time of a key in milliseconds. /// /// PEXPIRE key milliseconds [ NX | XX | GT | LT ] /// https://valkey.io/commands/pexpire/ @since(version = 0.3.0-dev) - pexpire: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, mode: option) -> result; + pexpire: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, mode: option) -> result; /// Sets the expiration time of a key to a Unix milliseconds timestamp. /// /// PEXPIREAT key unix-time-milliseconds [ NX | XX | GT | LT ] /// https://valkey.io/commands/pexpireat/ @since(version = 0.3.0-dev) - pexpireat: async func(connection: borrow, key: string, milliseconds: unix-time-milliseconds, mode: option) -> result; + pexpireat: async func(connection: borrow, key: string, milliseconds: unix-time-milliseconds, mode: option) -> result; /// Returns the expiration time of a key as a Unix milliseconds timestamp. /// /// PEXPIRETIME key /// https://valkey.io/commands/pexpiretime/ @since(version = 0.3.0-dev) - pexpiretime: async func(connection: borrow, key: string) -> result; + pexpiretime: async func(connection: borrow, key: string) -> result; /// Returns the expiration time in milliseconds of a key. /// /// PTTL key /// https://valkey.io/commands/pttl/ @since(version = 0.3.0-dev) - pttl: async func(connection: borrow, key: string) -> result; + pttl: async func(connection: borrow, key: string) -> result; /// Returns a random key name from the database. /// /// RANDOMKEY /// https://valkey.io/commands/randomkey/ @since(version = 0.3.0-dev) - randomkey: async func(connection: borrow) -> result, error>; + randomkey: async func(connection: borrow) -> result, error-code>; /// Renames a key and overwrites the destination. /// /// RENAME key newkey /// https://valkey.io/commands/rename/ @since(version = 0.3.0-dev) - rename: async func(connection: borrow, key: string, newkey: string) -> result<_, error>; + rename: async func(connection: borrow, key: string, newkey: string) -> result<_, error-code>; /// Renames a key only when the target key name doesn't exist. /// /// RENAMENX key newkey /// https://valkey.io/commands/renamenx/ @since(version = 0.3.0-dev) - renamenx: async func(connection: borrow, key: string, newkey: string) -> result; + renamenx: async func(connection: borrow, key: string, newkey: string) -> result; /// Creates a key from the serialized representation of a value. /// @@ -1342,7 +1342,7 @@ interface commands { /// RESTORE key ttl serialized-value [ REPLACE ] [ ABSTTL ] [ IDLETIME seconds ] [ FREQ frequency ] /// https://valkey.io/commands/restore/ @since(version = 0.3.0-dev) - restore: async func(connection: borrow, key: string, ttl: option, serialized-value: bytes, replace: bool, idletime: option, freq: option) -> result<_, error>; + restore: async func(connection: borrow, key: string, ttl: option, serialized-value: bytes, replace: bool, idletime: option, freq: option) -> result<_, error-code>; /// An internal command for migrating keys in a cluster. /// @@ -1351,35 +1351,35 @@ interface commands { /// RESTORE-ASKING key ttl serialized-value [ REPLACE ] [ ABSTTL ] [ IDLETIME seconds ] [ FREQ frequency ] /// https://valkey.io/commands/restore-asking/ @since(version = 0.3.0-dev) - restore-asking: async func(connection: borrow, key: string, ttl: option, serialized-value: bytes, replace: bool, idletime: option, freq: option) -> result<_, error>; + restore-asking: async func(connection: borrow, key: string, ttl: option, serialized-value: bytes, replace: bool, idletime: option, freq: option) -> result<_, error-code>; /// Iterates over the key names in the database. /// /// SCAN cursor [ MATCH pattern ] [ COUNT count ] [ TYPE type ] /// https://valkey.io/commands/scan/ @since(version = 0.3.0-dev) - scan: async func(connection: borrow, cursor: cursor, match: option, count: option, %type: option) -> result, list>, error>; + scan: async func(connection: borrow, cursor: cursor, match: option, count: option, %type: option) -> result, list>, error-code>; /// Swaps two databases. /// /// SWAPDB index1 index2 /// https://valkey.io/commands/swapdb/ @since(version = 0.3.0-dev) - swapdb: async func(connection: borrow, index1: integer, index2: integer) -> result<_, error>; + swapdb: async func(connection: borrow, index1: integer, index2: integer) -> result<_, error-code>; /// Returns the number of existing keys out of those specified after updating the time they were last accessed. /// /// TOUCH key [ key ... ] /// https://valkey.io/commands/touch/ @since(version = 0.3.0-dev) - touch: async func(connection: borrow, keys: list) -> result; + touch: async func(connection: borrow, keys: list) -> result; /// Returns the expiration time in seconds of a key. /// /// TTL key /// https://valkey.io/commands/ttl/ @since(version = 0.3.0-dev) - ttl: async func(connection: borrow, key: string) -> result; + ttl: async func(connection: borrow, key: string) -> result; /// Determines the type of value stored at a key. /// @@ -1388,14 +1388,14 @@ interface commands { /// TYPE key /// https://valkey.io/commands/type/ @since(version = 0.3.0-dev) - %type: async func(connection: borrow, key: string) -> result, error>; + %type: async func(connection: borrow, key: string) -> result, error-code>; /// Asynchronously deletes one or more keys. /// /// UNLINK key [ key ... ] /// https://valkey.io/commands/unlink/ @since(version = 0.3.0-dev) - unlink: async func(connection: borrow, keys: list) -> result; + unlink: async func(connection: borrow, keys: list) -> result; /// Pops an element from a list, pushes it to another list and returns it. /// @@ -1405,7 +1405,7 @@ interface commands { /// BLMOVE source destination 〈 LEFT | RIGHT 〉 〈 LEFT | RIGHT 〉 timeout /// https://valkey.io/commands/blmove/ @since(version = 0.3.0-dev) - blmove: async func(connection: borrow, source: string, destination: string, wherefrom: list-side, whereto: list-side, timeout: duration-milliseconds) -> result, error>; + blmove: async func(connection: borrow, source: string, destination: string, wherefrom: list-side, whereto: list-side, timeout: duration-milliseconds) -> result, error-code>; /// Pops the first element from one of multiple lists. /// @@ -1415,7 +1415,7 @@ interface commands { /// BLMPOP timeout numkeys key [ key ... ] 〈 LEFT | RIGHT 〉 [ COUNT count ] /// https://valkey.io/commands/blmpop/ @since(version = 0.3.0-dev) - blmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: list-side, count: option) -> result>>, error>; + blmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: list-side, count: option) -> result>>, error-code>; /// Removes and returns the first element in a list. /// @@ -1425,7 +1425,7 @@ interface commands { /// BLPOP key [ key ... ] timeout /// https://valkey.io/commands/blpop/ @since(version = 0.3.0-dev) - blpop: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; + blpop: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error-code>; /// Removes and returns the last element in a list. /// @@ -1435,7 +1435,7 @@ interface commands { /// BRPOP key [ key ... ] timeout /// https://valkey.io/commands/brpop/ @since(version = 0.3.0-dev) - brpop: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; + brpop: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error-code>; /// Pops an element from a list, pushes it to another list and returns it. /// @@ -1445,42 +1445,42 @@ interface commands { /// BRPOPLPUSH source destination timeout /// https://valkey.io/commands/brpoplpush/ @since(version = 0.3.0-dev) - brpoplpush: async func(connection: borrow, source: string, destination: string, timeout: duration-milliseconds) -> result, error>; + brpoplpush: async func(connection: borrow, source: string, destination: string, timeout: duration-milliseconds) -> result, error-code>; /// Returns an element from a list by its index. /// /// LINDEX key index /// https://valkey.io/commands/lindex/ @since(version = 0.3.0-dev) - lindex: async func(connection: borrow, key: string, index: integer) -> result, error>; + lindex: async func(connection: borrow, key: string, index: integer) -> result, error-code>; /// Inserts an element before or after another element in a list. /// /// LINSERT key 〈 BEFORE | AFTER 〉 pivot element /// https://valkey.io/commands/linsert/ @since(version = 0.3.0-dev) - linsert: async func(connection: borrow, key: string, position: linsert-position, pivot: string-or-bytes, element: string-or-bytes) -> result; + linsert: async func(connection: borrow, key: string, position: linsert-position, pivot: string-or-bytes, element: string-or-bytes) -> result; /// Returns the length of a list. /// /// LLEN key /// https://valkey.io/commands/llen/ @since(version = 0.3.0-dev) - llen: async func(connection: borrow, key: string) -> result; + llen: async func(connection: borrow, key: string) -> result; /// Returns an element after popping it from one list and pushing it to another. /// /// LMOVE source destination 〈 LEFT | RIGHT 〉 〈 LEFT | RIGHT 〉 /// https://valkey.io/commands/lmove/ @since(version = 0.3.0-dev) - lmove: async func(connection: borrow, source: string, destination: string, wherefrom: list-side, whereto: list-side) -> result, error>; + lmove: async func(connection: borrow, source: string, destination: string, wherefrom: list-side, whereto: list-side) -> result, error-code>; /// Returns multiple elements from a list after removing them. /// /// LMPOP numkeys key [ key ... ] 〈 LEFT | RIGHT 〉 [ COUNT count ] /// https://valkey.io/commands/lmpop/ @since(version = 0.3.0-dev) - lmpop: async func(connection: borrow, keys: list, end: list-side, count: option) -> result>>, error>; + lmpop: async func(connection: borrow, keys: list, end: list-side, count: option) -> result>>, error-code>; /// Returns and removes one or more elements from the beginning of a list. /// @@ -1489,7 +1489,7 @@ interface commands { /// LPOP key [ count ] /// https://valkey.io/commands/lpop/ @since(version = 0.3.0-dev) - lpop: async func(connection: borrow, key: string, count: option) -> result, error>; + lpop: async func(connection: borrow, key: string, count: option) -> result, error-code>; /// Returns the index of matching elements in a list. /// @@ -1498,49 +1498,49 @@ interface commands { /// LPOS key element [ RANK rank ] [ COUNT num-matches ] [ MAXLEN len ] /// https://valkey.io/commands/lpos/ @since(version = 0.3.0-dev) - lpos: async func(connection: borrow, key: string, element: string-or-bytes, rank: option, count: option, maxlen: option) -> result, error>; + lpos: async func(connection: borrow, key: string, element: string-or-bytes, rank: option, count: option, maxlen: option) -> result, error-code>; /// Prepends one or more elements to a list. /// /// LPUSH key element [ element ... ] /// https://valkey.io/commands/lpush/ @since(version = 0.3.0-dev) - lpush: async func(connection: borrow, key: string, elements: list) -> result; + lpush: async func(connection: borrow, key: string, elements: list) -> result; /// Prepends one or more elements to a list only when the list exists. /// /// LPUSHX key element [ element ... ] /// https://valkey.io/commands/lpushx/ @since(version = 0.3.0-dev) - lpushx: async func(connection: borrow, key: string, elements: list) -> result; + lpushx: async func(connection: borrow, key: string, elements: list) -> result; /// Returns a range of elements from a list. /// /// LRANGE key start stop /// https://valkey.io/commands/lrange/ @since(version = 0.3.0-dev) - lrange: async func(connection: borrow, key: string, start: integer, stop: integer) -> result, error>; + lrange: async func(connection: borrow, key: string, start: integer, stop: integer) -> result, error-code>; /// Removes elements from a list. /// /// LREM key count element /// https://valkey.io/commands/lrem/ @since(version = 0.3.0-dev) - lrem: async func(connection: borrow, key: string, count: integer, element: string-or-bytes) -> result; + lrem: async func(connection: borrow, key: string, count: integer, element: string-or-bytes) -> result; /// Sets the value of an element in a list by its index. /// /// LSET key index element /// https://valkey.io/commands/lset/ @since(version = 0.3.0-dev) - lset: async func(connection: borrow, key: string, index: integer, element: string-or-bytes) -> result<_, error>; + lset: async func(connection: borrow, key: string, index: integer, element: string-or-bytes) -> result<_, error-code>; /// Removes elements from both ends of a list. /// /// LTRIM key start stop /// https://valkey.io/commands/ltrim/ @since(version = 0.3.0-dev) - ltrim: async func(connection: borrow, key: string, start: integer, stop: integer) -> result<_, error>; + ltrim: async func(connection: borrow, key: string, start: integer, stop: integer) -> result<_, error-code>; /// Returns and removes one or more elements from the end of a list. /// @@ -1549,77 +1549,77 @@ interface commands { /// RPOP key [ count ] /// https://valkey.io/commands/rpop/ @since(version = 0.3.0-dev) - rpop: async func(connection: borrow, key: string, count: option) -> result, error>; + rpop: async func(connection: borrow, key: string, count: option) -> result, error-code>; /// Returns the last element of a list after removing and pushing it to another list. /// /// RPOPLPUSH source destination /// https://valkey.io/commands/rpoplpush/ @since(version = 0.3.0-dev) - rpoplpush: async func(connection: borrow, source: string, destination: string) -> result, error>; + rpoplpush: async func(connection: borrow, source: string, destination: string) -> result, error-code>; /// Appends one or more elements to a list. /// /// RPUSH key element [ element ... ] /// https://valkey.io/commands/rpush/ @since(version = 0.3.0-dev) - rpush: async func(connection: borrow, key: string, elements: list) -> result; + rpush: async func(connection: borrow, key: string, elements: list) -> result; /// Appends one or more elements to a list only when the list exists. /// /// RPUSHX key element [ element ... ] /// https://valkey.io/commands/rpushx/ @since(version = 0.3.0-dev) - rpushx: async func(connection: borrow, key: string, elements: list) -> result; + rpushx: async func(connection: borrow, key: string, elements: list) -> result; /// Posts a message to a channel. /// /// PUBLISH channel message /// https://valkey.io/commands/publish/ @since(version = 0.3.0-dev) - publish: async func(connection: borrow, channel: string, message: string-or-bytes) -> result; + publish: async func(connection: borrow, channel: string, message: string-or-bytes) -> result; /// Returns the active channels. /// /// PUBSUB CHANNELS [ pattern ] /// https://valkey.io/commands/pubsub-channels/ @since(version = 0.3.0-dev) - pubsub-channels: async func(connection: borrow, pattern: option) -> result, error>; + pubsub-channels: async func(connection: borrow, pattern: option) -> result, error-code>; /// Returns a count of unique pattern subscriptions. /// /// PUBSUB NUMPAT /// https://valkey.io/commands/pubsub-numpat/ @since(version = 0.3.0-dev) - pubsub-numpat: async func(connection: borrow) -> result; + pubsub-numpat: async func(connection: borrow) -> result; /// Returns a count of subscribers to channels. /// /// PUBSUB NUMSUB [ channel ] [ [ channel ] ... ] /// https://valkey.io/commands/pubsub-numsub/ @since(version = 0.3.0-dev) - pubsub-numsub: async func(connection: borrow, channels: option>) -> result, error>; + pubsub-numsub: async func(connection: borrow, channels: option>) -> result, error-code>; /// Returns the active shard channels. /// /// PUBSUB SHARDCHANNELS [ pattern ] /// https://valkey.io/commands/pubsub-shardchannels/ @since(version = 0.3.0-dev) - pubsub-shardchannels: async func(connection: borrow, pattern: option) -> result, error>; + pubsub-shardchannels: async func(connection: borrow, pattern: option) -> result, error-code>; /// Returns the count of subscribers of shard channels. /// /// PUBSUB SHARDNUMSUB [ shardchannel [ shardchannel ... ] ] /// https://valkey.io/commands/pubsub-shardnumsub/ @since(version = 0.3.0-dev) - pubsub-shardnumsub: async func(connection: borrow, shardchannels: option>) -> result, error>; + pubsub-shardnumsub: async func(connection: borrow, shardchannels: option>) -> result, error-code>; /// Posts a message to a shard channel. /// /// SPUBLISH shardchannel message /// https://valkey.io/commands/spublish/ @since(version = 0.3.0-dev) - spublish: async func(connection: borrow, shardchannel: string, message: string-or-bytes) -> result; + spublish: async func(connection: borrow, shardchannel: string, message: string-or-bytes) -> result; /// Executes a server-side Lua script. /// @@ -1628,7 +1628,7 @@ interface commands { /// EVAL script numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/eval/ @since(version = 0.3.0-dev) - eval: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; + eval: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; /// Executes a read-only server-side Lua script. /// @@ -1637,7 +1637,7 @@ interface commands { /// EVAL_RO script numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/eval_ro/ @since(version = 0.3.0-dev) - eval-ro: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; + eval-ro: async func(connection: borrow, script: string, keys: option>, args: option>) -> result; /// Executes a server-side Lua script by SHA1 digest. /// @@ -1646,7 +1646,7 @@ interface commands { /// EVALSHA sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/evalsha/ @since(version = 0.3.0-dev) - evalsha: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; + evalsha: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; /// Executes a read-only server-side Lua script by SHA1 digest. /// @@ -1655,7 +1655,7 @@ interface commands { /// EVALSHA_RO sha1 numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/evalsha_ro/ @since(version = 0.3.0-dev) - evalsha-ro: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; + evalsha-ro: async func(connection: borrow, sha1: string, keys: option>, args: option>) -> result; /// Invokes a function. /// @@ -1664,7 +1664,7 @@ interface commands { /// FCALL function numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/fcall/ @since(version = 0.3.0-dev) - fcall: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; + fcall: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; /// Invokes a read-only function. /// @@ -1673,147 +1673,147 @@ interface commands { /// FCALL_RO function numkeys [ key [ key ... ] ] [ arg [ arg ... ] ] /// https://valkey.io/commands/fcall_ro/ @since(version = 0.3.0-dev) - fcall-ro: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; + fcall-ro: async func(connection: borrow, function: string, keys: option>, args: option>) -> result; /// Deletes a library and its functions. /// /// FUNCTION DELETE library-name /// https://valkey.io/commands/function-delete/ @since(version = 0.3.0-dev) - function-delete: async func(connection: borrow, library-name: string) -> result<_, error>; + function-delete: async func(connection: borrow, library-name: string) -> result<_, error-code>; /// Dumps all libraries into a serialized binary payload. /// /// FUNCTION DUMP /// https://valkey.io/commands/function-dump/ @since(version = 0.3.0-dev) - function-dump: async func(connection: borrow) -> result; + function-dump: async func(connection: borrow) -> result; /// Deletes all libraries and functions. /// /// FUNCTION FLUSH [ ASYNC | SYNC ] /// https://valkey.io/commands/function-flush/ @since(version = 0.3.0-dev) - function-flush: async func(connection: borrow, mode: option) -> result<_, error>; + function-flush: async func(connection: borrow, mode: option) -> result<_, error-code>; /// Terminates a function during execution. /// /// FUNCTION KILL /// https://valkey.io/commands/function-kill/ @since(version = 0.3.0-dev) - function-kill: async func(connection: borrow) -> result<_, error>; + function-kill: async func(connection: borrow) -> result<_, error-code>; /// Returns information about all libraries. /// /// FUNCTION LIST [ LIBRARYNAME library-name-pattern ] [ WITHCODE ] /// https://valkey.io/commands/function-list/ @since(version = 0.3.0-dev) - function-list: async func(connection: borrow, library-name-pattern: option, withcode: bool) -> result, error>; + function-list: async func(connection: borrow, library-name-pattern: option, withcode: bool) -> result, error-code>; /// Creates a library. /// /// FUNCTION LOAD [ REPLACE ] function-code /// https://valkey.io/commands/function-load/ @since(version = 0.3.0-dev) - function-load: async func(connection: borrow, replace: bool, function-code: string) -> result; + function-load: async func(connection: borrow, replace: bool, function-code: string) -> result; /// Restores all libraries from a payload. /// /// FUNCTION RESTORE serialized-value [ FLUSH | APPEND | REPLACE ] /// https://valkey.io/commands/function-restore/ @since(version = 0.3.0-dev) - function-restore: async func(connection: borrow, serialized-value: bytes, policy: option) -> result<_, error>; + function-restore: async func(connection: borrow, serialized-value: bytes, policy: option) -> result<_, error-code>; /// Returns information about a function during execution. /// /// FUNCTION STATS /// https://valkey.io/commands/function-stats/ @since(version = 0.3.0-dev) - function-stats: async func(connection: borrow) -> result; + function-stats: async func(connection: borrow) -> result; /// Determines whether server-side Lua scripts exist in the script cache. /// /// SCRIPT EXISTS sha1 [ sha1 ... ] /// https://valkey.io/commands/script-exists/ @since(version = 0.3.0-dev) - script-exists: async func(connection: borrow, sha1s: list) -> result, error>; + script-exists: async func(connection: borrow, sha1s: list) -> result, error-code>; /// Removes all server-side Lua scripts from the script cache. /// /// SCRIPT FLUSH [ ASYNC | SYNC ] /// https://valkey.io/commands/script-flush/ @since(version = 0.3.0-dev) - script-flush: async func(connection: borrow, mode: option) -> result<_, error>; + script-flush: async func(connection: borrow, mode: option) -> result<_, error-code>; /// Terminates a server-side Lua script during execution. /// /// SCRIPT KILL /// https://valkey.io/commands/script-kill/ @since(version = 0.3.0-dev) - script-kill: async func(connection: borrow) -> result<_, error>; + script-kill: async func(connection: borrow) -> result<_, error-code>; /// Loads a server-side Lua script to the script cache. /// /// SCRIPT LOAD script /// https://valkey.io/commands/script-load/ @since(version = 0.3.0-dev) - script-load: async func(connection: borrow, script: string) -> result; + script-load: async func(connection: borrow, script: string) -> result; /// Show server-side Lua script in the script cache. /// /// SCRIPT SHOW sha1 /// https://valkey.io/commands/script-show/ @since(version = 0.3.0-dev) - script-show: async func(connection: borrow, sha1: string) -> result; + script-show: async func(connection: borrow, sha1: string) -> result; /// Adds one or more members to a set. /// /// SADD key member [ member ... ] /// https://valkey.io/commands/sadd/ @since(version = 0.3.0-dev) - sadd: async func(connection: borrow, key: string, members: list) -> result; + sadd: async func(connection: borrow, key: string, members: list) -> result; /// Returns the number of members in a set. /// /// SCARD key /// https://valkey.io/commands/scard/ @since(version = 0.3.0-dev) - scard: async func(connection: borrow, key: string) -> result; + scard: async func(connection: borrow, key: string) -> result; /// Returns the difference of multiple sets. /// /// SDIFF key [ key ... ] /// https://valkey.io/commands/sdiff/ @since(version = 0.3.0-dev) - sdiff: async func(connection: borrow, keys: list) -> result, error>; + sdiff: async func(connection: borrow, keys: list) -> result, error-code>; /// Stores the difference of multiple sets in a key. /// /// SDIFFSTORE destination key [ key ... ] /// https://valkey.io/commands/sdiffstore/ @since(version = 0.3.0-dev) - sdiffstore: async func(connection: borrow, destination: string, keys: list) -> result; + sdiffstore: async func(connection: borrow, destination: string, keys: list) -> result; /// Returns the intersect of multiple sets. /// /// SINTER key [ key ... ] /// https://valkey.io/commands/sinter/ @since(version = 0.3.0-dev) - sinter: async func(connection: borrow, keys: list) -> result, error>; + sinter: async func(connection: borrow, keys: list) -> result, error-code>; /// Returns the number of members of the intersect of multiple sets. /// /// SINTERCARD numkeys key [ key ... ] [ LIMIT limit ] /// https://valkey.io/commands/sintercard/ @since(version = 0.3.0-dev) - sintercard: async func(connection: borrow, keys: list, limit: option) -> result; + sintercard: async func(connection: borrow, keys: list, limit: option) -> result; /// Stores the intersect of multiple sets in a key. /// /// SINTERSTORE destination key [ key ... ] /// https://valkey.io/commands/sinterstore/ @since(version = 0.3.0-dev) - sinterstore: async func(connection: borrow, destination: string, keys: list) -> result; + sinterstore: async func(connection: borrow, destination: string, keys: list) -> result; /// Determines whether a member belongs to a set. /// @@ -1824,7 +1824,7 @@ interface commands { /// SISMEMBER key member /// https://valkey.io/commands/sismember/ @since(version = 0.3.0-dev) - sismember: async func(connection: borrow, key: string, member: string-or-bytes) -> result; + sismember: async func(connection: borrow, key: string, member: string-or-bytes) -> result; /// Determines whether a member belongs to a set. /// @@ -1835,28 +1835,28 @@ interface commands { /// SISMEMBER key member XX /// https://valkey.io/commands/sismember/ @since(version = 0.3.0-dev) - sismember-xx: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; + sismember-xx: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error-code>; /// Returns all members of a set. /// /// SMEMBERS key /// https://valkey.io/commands/smembers/ @since(version = 0.3.0-dev) - smembers: async func(connection: borrow, key: string) -> result, error>; + smembers: async func(connection: borrow, key: string) -> result, error-code>; /// Determines whether multiple members belong to a set. /// /// SMISMEMBER key member [ member ... ] /// https://valkey.io/commands/smismember/ @since(version = 0.3.0-dev) - smismember: async func(connection: borrow, key: string, members: list) -> result, error>; + smismember: async func(connection: borrow, key: string, members: list) -> result, error-code>; /// Moves a member from one set to another. /// /// SMOVE source destination member /// https://valkey.io/commands/smove/ @since(version = 0.3.0-dev) - smove: async func(connection: borrow, source: string, destination: string, member: string-or-bytes) -> result; + smove: async func(connection: borrow, source: string, destination: string, member: string-or-bytes) -> result; /// Returns one or more random members from a set after removing them. /// @@ -1865,7 +1865,7 @@ interface commands { /// SPOP key [ count ] /// https://valkey.io/commands/spop/ @since(version = 0.3.0-dev) - spop: async func(connection: borrow, key: string, count: option) -> result, error>; + spop: async func(connection: borrow, key: string, count: option) -> result, error-code>; /// Gets one or multiple random members from a set. /// @@ -1874,35 +1874,35 @@ interface commands { /// SRANDMEMBER key [ count ] /// https://valkey.io/commands/srandmember/ @since(version = 0.3.0-dev) - srandmember: async func(connection: borrow, key: string, count: option) -> result, error>; + srandmember: async func(connection: borrow, key: string, count: option) -> result, error-code>; /// Removes one or more members from a set. /// /// SREM key member [ member ... ] /// https://valkey.io/commands/srem/ @since(version = 0.3.0-dev) - srem: async func(connection: borrow, key: string, members: list) -> result; + srem: async func(connection: borrow, key: string, members: list) -> result; /// Iterates over members of a set. /// /// SSCAN key cursor [ MATCH pattern ] [ COUNT count ] /// https://valkey.io/commands/sscan/ @since(version = 0.3.0-dev) - sscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; + sscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error-code>; /// Returns the union of multiple sets. /// /// SUNION key [ key ... ] /// https://valkey.io/commands/sunion/ @since(version = 0.3.0-dev) - sunion: async func(connection: borrow, keys: list) -> result, error>; + sunion: async func(connection: borrow, keys: list) -> result, error-code>; /// Stores the union of multiple sets in a key. /// /// SUNIONSTORE destination key [ key ... ] /// https://valkey.io/commands/sunionstore/ @since(version = 0.3.0-dev) - sunionstore: async func(connection: borrow, destination: string, keys: list) -> result; + sunionstore: async func(connection: borrow, destination: string, keys: list) -> result; /// Sorts the elements in a list, a set, or a sorted set, optionally storing the result. /// @@ -1914,7 +1914,7 @@ interface commands { /// SORT key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] /// https://valkey.io/commands/sort/ @since(version = 0.3.0-dev) - sort: async func(connection: borrow, key: string, opts: option) -> result>>, error>; + sort: async func(connection: borrow, key: string, opts: option) -> result>>, error-code>; /// Sorts the elements in a list, a set, or a sorted set, optionally storing the result. /// @@ -1925,7 +1925,7 @@ interface commands { /// SORT key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] STORE destination /// https://valkey.io/commands/sort/ @since(version = 0.3.0-dev) - sort-store: async func(connection: borrow, key: string, opts: option, destination: string) -> result; + sort-store: async func(connection: borrow, key: string, opts: option, destination: string) -> result; /// Returns the sorted elements of a list, a set, or a sorted set. /// @@ -1935,7 +1935,7 @@ interface commands { /// SORT_RO key [ BY pattern ] [ LIMIT offset count ] [ GET pattern ... ] [ ASC | DESC ] [ ALPHA ] /// https://valkey.io/commands/sort_ro/ @since(version = 0.3.0-dev) - sort-ro: async func(connection: borrow, key: string, opts: option) -> result>>, error>; + sort-ro: async func(connection: borrow, key: string, opts: option) -> result>>, error-code>; /// Removes and returns a member by score from one or more sorted sets. /// @@ -1945,7 +1945,7 @@ interface commands { /// BZMPOP timeout numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ] /// https://valkey.io/commands/bzmpop/ @since(version = 0.3.0-dev) - bzmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: bzmpop-end, count: option) -> result>>>, error>; + bzmpop: async func(connection: borrow, timeout: duration-milliseconds, keys: list, end: bzmpop-end, count: option) -> result>>>, error-code>; /// Removes and returns the member with the highest score from one or more sorted sets. /// @@ -1955,7 +1955,7 @@ interface commands { /// BZPOPMAX key [ key ... ] timeout /// https://valkey.io/commands/bzpopmax/ @since(version = 0.3.0-dev) - bzpopmax: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; + bzpopmax: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error-code>; /// Removes and returns the member with the lowest score from one or more sorted sets. /// @@ -1965,7 +1965,7 @@ interface commands { /// BZPOPMIN key [ key ... ] timeout /// https://valkey.io/commands/bzpopmin/ @since(version = 0.3.0-dev) - bzpopmin: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error>; + bzpopmin: async func(connection: borrow, keys: list, timeout: duration-milliseconds) -> result>, error-code>; /// Adds one or more members to a sorted set, or updates their scores. /// @@ -1974,7 +1974,7 @@ interface commands { /// ZADD key [ NX | XX ] [ GT | LT ] [ CH ] score member [ score member ... ] /// https://valkey.io/commands/zadd/ @since(version = 0.3.0-dev) - zadd: async func(connection: borrow, key: string, condition: option, comparison: option, changed: bool, score-and-members: list>) -> result, error>; + zadd: async func(connection: borrow, key: string, condition: option, comparison: option, changed: bool, score-and-members: list>) -> result, error-code>; /// Adds one or more members to a sorted set, or updates their scores. /// @@ -1986,21 +1986,21 @@ interface commands { /// ZADD key [ NX | XX ] [ GT | LT ] INCR score member /// https://valkey.io/commands/zadd/ @since(version = 0.3.0-dev) - zadd-incr: async func(connection: borrow, key: string, condition: option, comparison: option, increment: double, member: string-or-bytes) -> result, error>; + zadd-incr: async func(connection: borrow, key: string, condition: option, comparison: option, increment: double, member: string-or-bytes) -> result, error-code>; /// Returns the number of members in a sorted set. /// /// ZCARD key /// https://valkey.io/commands/zcard/ @since(version = 0.3.0-dev) - zcard: async func(connection: borrow, key: string) -> result; + zcard: async func(connection: borrow, key: string) -> result; /// Returns the count of members in a sorted set that have scores within a range. /// /// ZCOUNT key min max /// https://valkey.io/commands/zcount/ @since(version = 0.3.0-dev) - zcount: async func(connection: borrow, key: string, min: score-bound, max: score-bound) -> result; + zcount: async func(connection: borrow, key: string, min: score-bound, max: score-bound) -> result; /// Returns the difference between multiple sorted sets. /// @@ -2009,7 +2009,7 @@ interface commands { /// ZDIFF numkeys key [ key ... ] /// https://valkey.io/commands/zdiff/ @since(version = 0.3.0-dev) - zdiff: async func(connection: borrow, keys: list) -> result, error>; + zdiff: async func(connection: borrow, keys: list) -> result, error-code>; /// Returns the difference between multiple sorted sets. /// @@ -2020,21 +2020,21 @@ interface commands { /// ZDIFF numkeys key [ key ... ] WITHSCORES /// https://valkey.io/commands/zdiff/ @since(version = 0.3.0-dev) - zdiff-withscores: async func(connection: borrow, keys: list) -> result>, error>; + zdiff-withscores: async func(connection: borrow, keys: list) -> result>, error-code>; /// Stores the difference of multiple sorted sets in a key. /// /// ZDIFFSTORE destination numkeys key [ key ... ] /// https://valkey.io/commands/zdiffstore/ @since(version = 0.3.0-dev) - zdiffstore: async func(connection: borrow, destination: string, keys: list) -> result; + zdiffstore: async func(connection: borrow, destination: string, keys: list) -> result; /// Increments the score of a member in a sorted set. /// /// ZINCRBY key increment member /// https://valkey.io/commands/zincrby/ @since(version = 0.3.0-dev) - zincrby: async func(connection: borrow, key: string, increment: double, member: string-or-bytes) -> result; + zincrby: async func(connection: borrow, key: string, increment: double, member: string-or-bytes) -> result; /// Returns the intersect of multiple sorted sets. /// @@ -2043,7 +2043,7 @@ interface commands { /// ZINTER numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zinter/ @since(version = 0.3.0-dev) - zinter: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; + zinter: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error-code>; /// Returns the intersect of multiple sorted sets. /// @@ -2054,42 +2054,42 @@ interface commands { /// ZINTER numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] WITHSCORES /// https://valkey.io/commands/zinter/ @since(version = 0.3.0-dev) - zinter-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; + zinter-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error-code>; /// Returns the number of members of the intersect of multiple sorted sets. /// /// ZINTERCARD numkeys key [ key ... ] [ LIMIT limit ] /// https://valkey.io/commands/zintercard/ @since(version = 0.3.0-dev) - zintercard: async func(connection: borrow, keys: list, limit: option) -> result; + zintercard: async func(connection: borrow, keys: list, limit: option) -> result; /// Stores the intersect of multiple sorted sets in a key. /// /// ZINTERSTORE destination numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zinterstore/ @since(version = 0.3.0-dev) - zinterstore: async func(connection: borrow, destination: string, keys: list, weights: option>, aggregate: option) -> result; + zinterstore: async func(connection: borrow, destination: string, keys: list, weights: option>, aggregate: option) -> result; /// Returns the number of members in a sorted set within a lexicographical range. /// /// ZLEXCOUNT key min max /// https://valkey.io/commands/zlexcount/ @since(version = 0.3.0-dev) - zlexcount: async func(connection: borrow, key: string, min: lex-bound, max: lex-bound) -> result; + zlexcount: async func(connection: borrow, key: string, min: lex-bound, max: lex-bound) -> result; /// Returns the highest- or lowest-scoring members from one or more sorted sets after removing them. /// /// ZMPOP numkeys key [ key ... ] 〈 MIN | MAX 〉 [ COUNT count ] /// https://valkey.io/commands/zmpop/ @since(version = 0.3.0-dev) - zmpop: async func(connection: borrow, keys: list, end: zmpop-end, count: option) -> result>>>, error>; + zmpop: async func(connection: borrow, keys: list, end: zmpop-end, count: option) -> result>>>, error-code>; /// Returns the score of one or more members in a sorted set. /// /// ZMSCORE key member [ member ... ] /// https://valkey.io/commands/zmscore/ @since(version = 0.3.0-dev) - zmscore: async func(connection: borrow, key: string, members: list) -> result>, error>; + zmscore: async func(connection: borrow, key: string, members: list) -> result>, error-code>; /// Returns the highest-scoring members from a sorted set after removing them. /// @@ -2098,7 +2098,7 @@ interface commands { /// ZPOPMAX key [ count ] /// https://valkey.io/commands/zpopmax/ @since(version = 0.3.0-dev) - zpopmax: async func(connection: borrow, key: string, count: option) -> result>, error>; + zpopmax: async func(connection: borrow, key: string, count: option) -> result>, error-code>; /// Returns the lowest-scoring members from a sorted set after removing them. /// @@ -2107,7 +2107,7 @@ interface commands { /// ZPOPMIN key [ count ] /// https://valkey.io/commands/zpopmin/ @since(version = 0.3.0-dev) - zpopmin: async func(connection: borrow, key: string, count: option) -> result>, error>; + zpopmin: async func(connection: borrow, key: string, count: option) -> result>, error-code>; /// Returns one or more random members from a sorted set. /// @@ -2118,7 +2118,7 @@ interface commands { /// ZRANDMEMBER key [ count ] /// https://valkey.io/commands/zrandmember/ @since(version = 0.3.0-dev) - zrandmember: async func(connection: borrow, key: string, count: option) -> result, error>; + zrandmember: async func(connection: borrow, key: string, count: option) -> result, error-code>; /// Returns one or more random members from a sorted set. /// @@ -2129,7 +2129,7 @@ interface commands { /// ZRANDMEMBER key count WITHSCORES /// https://valkey.io/commands/zrandmember/ @since(version = 0.3.0-dev) - zrandmember-withscores: async func(connection: borrow, key: string, count: integer) -> result>, error>; + zrandmember-withscores: async func(connection: borrow, key: string, count: integer) -> result>, error-code>; /// Returns members in a sorted set within a range of indexes. /// @@ -2142,7 +2142,7 @@ interface commands { /// ZRANGE key start stop [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ] [ XX ] /// https://valkey.io/commands/zrange/ @since(version = 0.3.0-dev) - zrange: async func(connection: borrow, key: string, range: zrange-by, rev: bool, limit: option, xx: bool) -> result>, error>; + zrange: async func(connection: borrow, key: string, range: zrange-by, rev: bool, limit: option, xx: bool) -> result>, error-code>; /// Returns members in a sorted set within a range of indexes. /// @@ -2154,7 +2154,7 @@ interface commands { /// ZRANGE key start stop [ BYSCORE ] [ REV ] [ LIMIT offset count ] WITHSCORES [ XX ] /// https://valkey.io/commands/zrange/ @since(version = 0.3.0-dev) - zrange-withscores: async func(connection: borrow, key: string, range: zrange-withscores-by, rev: bool, limit: option, xx: bool) -> result>>, error>; + zrange-withscores: async func(connection: borrow, key: string, range: zrange-withscores-by, rev: bool, limit: option, xx: bool) -> result>>, error-code>; /// Stores a range of members from sorted set in a key. /// @@ -2163,7 +2163,7 @@ interface commands { /// ZRANGESTORE dst src min max [ BYSCORE | BYLEX ] [ REV ] [ LIMIT offset count ] /// https://valkey.io/commands/zrangestore/ @since(version = 0.3.0-dev) - zrangestore: async func(connection: borrow, destination: string, source: string, range: zrangestore-by, rev: bool, limit: option) -> result; + zrangestore: async func(connection: borrow, destination: string, source: string, range: zrangestore-by, rev: bool, limit: option) -> result; /// Returns the index of a member in a sorted set ordered by ascending scores. /// @@ -2172,7 +2172,7 @@ interface commands { /// ZRANK key member /// https://valkey.io/commands/zrank/ @since(version = 0.3.0-dev) - zrank: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; + zrank: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error-code>; /// Returns the index of a member in a sorted set ordered by ascending scores. /// @@ -2183,35 +2183,35 @@ interface commands { /// ZRANK key member WITHSCORE /// https://valkey.io/commands/zrank/ @since(version = 0.3.0-dev) - zrank-withscore: async func(connection: borrow, key: string, member: string-or-bytes) -> result>, error>; + zrank-withscore: async func(connection: borrow, key: string, member: string-or-bytes) -> result>, error-code>; /// Removes one or more members from a sorted set. /// /// ZREM key member [ member ... ] /// https://valkey.io/commands/zrem/ @since(version = 0.3.0-dev) - zrem: async func(connection: borrow, key: string, members: list) -> result; + zrem: async func(connection: borrow, key: string, members: list) -> result; /// Removes members in a sorted set within a lexicographical range. /// /// ZREMRANGEBYLEX key min max /// https://valkey.io/commands/zremrangebylex/ @since(version = 0.3.0-dev) - zremrangebylex: async func(connection: borrow, key: string, min: lex-bound, max: lex-bound) -> result; + zremrangebylex: async func(connection: borrow, key: string, min: lex-bound, max: lex-bound) -> result; /// Removes members in a sorted set within a range of indexes. /// /// ZREMRANGEBYRANK key start stop /// https://valkey.io/commands/zremrangebyrank/ @since(version = 0.3.0-dev) - zremrangebyrank: async func(connection: borrow, key: string, start: integer, stop: integer) -> result; + zremrangebyrank: async func(connection: borrow, key: string, start: integer, stop: integer) -> result; /// Removes members in a sorted set within a range of scores. /// /// ZREMRANGEBYSCORE key min max /// https://valkey.io/commands/zremrangebyscore/ @since(version = 0.3.0-dev) - zremrangebyscore: async func(connection: borrow, key: string, min: score-bound, max: score-bound) -> result; + zremrangebyscore: async func(connection: borrow, key: string, min: score-bound, max: score-bound) -> result; /// Returns the index of a member in a sorted set ordered by descending scores. /// @@ -2220,7 +2220,7 @@ interface commands { /// ZREVRANK key member /// https://valkey.io/commands/zrevrank/ @since(version = 0.3.0-dev) - zrevrank: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; + zrevrank: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error-code>; /// Returns the index of a member in a sorted set ordered by descending scores. /// @@ -2231,7 +2231,7 @@ interface commands { /// ZREVRANK key member WITHSCORE /// https://valkey.io/commands/zrevrank/ @since(version = 0.3.0-dev) - zrevrank-withscore: async func(connection: borrow, key: string, member: string-or-bytes) -> result>, error>; + zrevrank-withscore: async func(connection: borrow, key: string, member: string-or-bytes) -> result>, error-code>; /// Iterates over members and scores of a sorted set. /// @@ -2240,7 +2240,7 @@ interface commands { /// ZSCAN key cursor [ MATCH pattern ] [ COUNT count ] /// https://valkey.io/commands/zscan/ @since(version = 0.3.0-dev) - zscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>>, error>; + zscan: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>>, error-code>; /// Iterates over members and scores of a sorted set. /// @@ -2251,14 +2251,14 @@ interface commands { /// ZSCAN key cursor [ MATCH pattern ] [ COUNT count ] NOSCORES /// https://valkey.io/commands/zscan/ @since(version = 0.3.0-dev) - zscan-noscores: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error>; + zscan-noscores: async func(connection: borrow, key: string, cursor: cursor, match: option, count: option) -> result, list>, error-code>; /// Returns the score of a member in a sorted set. /// /// ZSCORE key member /// https://valkey.io/commands/zscore/ @since(version = 0.3.0-dev) - zscore: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error>; + zscore: async func(connection: borrow, key: string, member: string-or-bytes) -> result, error-code>; /// Returns the union of multiple sorted sets. /// @@ -2267,7 +2267,7 @@ interface commands { /// ZUNION numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zunion/ @since(version = 0.3.0-dev) - zunion: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error>; + zunion: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result, error-code>; /// Returns the union of multiple sorted sets. /// @@ -2278,28 +2278,28 @@ interface commands { /// ZUNION numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] WITHSCORES /// https://valkey.io/commands/zunion/ @since(version = 0.3.0-dev) - zunion-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error>; + zunion-withscores: async func(connection: borrow, keys: list, weights: option>, aggregate: option) -> result>, error-code>; /// Stores the union of multiple sorted sets in a key. /// /// ZUNIONSTORE destination numkeys key [ key ... ] [ WEIGHTS weight [ weight ... ] ] [ AGGREGATE SUM | MIN | MAX ] /// https://valkey.io/commands/zunionstore/ @since(version = 0.3.0-dev) - zunionstore: async func(connection: borrow, destination: string, keys: list, weights: option>, aggregate: option) -> result; + zunionstore: async func(connection: borrow, destination: string, keys: list, weights: option>, aggregate: option) -> result; /// Returns the number of messages that were successfully acknowledged by the consumer group member of a stream. /// /// XACK key group ID [ ID ... ] /// https://valkey.io/commands/xack/ @since(version = 0.3.0-dev) - xack: async func(connection: borrow, key: string, group: string, ids: list) -> result; + xack: async func(connection: borrow, key: string, group: string, ids: list) -> result; /// Appends a new message to a stream. /// /// XADD key [ NOMKSTREAM ] [ 〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] ] 〈 * | id 〉 field value [ field value ... ] /// https://valkey.io/commands/xadd/ @since(version = 0.3.0-dev) - xadd: async func(connection: borrow, key: string, nomkstream: bool, trim: option, id: stream-id, field-and-values: list>) -> result, error>; + xadd: async func(connection: borrow, key: string, nomkstream: bool, trim: option, id: stream-id, field-and-values: list>) -> result, error-code>; /// Changes, or acquires, ownership of messages in a consumer group, as if the messages were delivered to a consumer group member. /// @@ -2311,7 +2311,7 @@ interface commands { /// XAUTOCLAIM key group consumer min-idle-time start [ COUNT count ] /// https://valkey.io/commands/xautoclaim/ @since(version = 0.3.0-dev) - xautoclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, start: option, count: option) -> result, list>>, list>, error>; + xautoclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, start: option, count: option) -> result, list>>, list>, error-code>; /// Changes, or acquires, ownership of messages in a consumer group, as if the messages were delivered to a consumer group member. /// @@ -2323,7 +2323,7 @@ interface commands { /// XAUTOCLAIM key group consumer min-idle-time start [ COUNT count ] JUSTID /// https://valkey.io/commands/xautoclaim/ @since(version = 0.3.0-dev) - xautoclaim-justid: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, start: option, count: option) -> result, list, list>, error>; + xautoclaim-justid: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, start: option, count: option) -> result, list, list>, error-code>; /// Changes, or acquires, ownership of a message in a consumer group, as if the message was delivered to a consumer group member. /// @@ -2332,7 +2332,7 @@ interface commands { /// XCLAIM key group consumer min-idle-time ID [ ID ... ] [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] [ LASTID lastid ] /// https://valkey.io/commands/xclaim/ @since(version = 0.3.0-dev) - xclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, ids: list, opts: option) -> result>>, error>; + xclaim: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, ids: list, opts: option) -> result>>, error-code>; /// Changes, or acquires, ownership of a message in a consumer group, as if the message was delivered to a consumer group member. /// @@ -2343,63 +2343,63 @@ interface commands { /// XCLAIM key group consumer min-idle-time ID [ ID ... ] [ IDLE ms ] [ TIME unix-time-milliseconds ] [ RETRYCOUNT count ] [ FORCE ] JUSTID [ LASTID lastid ] /// https://valkey.io/commands/xclaim/ @since(version = 0.3.0-dev) - xclaim-justid: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, ids: list, opts: option) -> result, error>; + xclaim-justid: async func(connection: borrow, key: string, group: string, consumer: string, min-idle-time: duration-milliseconds, ids: list, opts: option) -> result, error-code>; /// Returns the number of messages after removing them from a stream. /// /// XDEL key ID [ ID ... ] /// https://valkey.io/commands/xdel/ @since(version = 0.3.0-dev) - xdel: async func(connection: borrow, key: string, ids: list) -> result; + xdel: async func(connection: borrow, key: string, ids: list) -> result; /// Creates a consumer group. /// /// XGROUP CREATE key group 〈 id | $ 〉 [ MKSTREAM ] [ ENTRIESREAD entriesread ] /// https://valkey.io/commands/xgroup-create/ @since(version = 0.3.0-dev) - xgroup-create: async func(connection: borrow, key: string, group: string, id: stream-id, mkstream: bool, entriesread: option) -> result<_, error>; + xgroup-create: async func(connection: borrow, key: string, group: string, id: stream-id, mkstream: bool, entriesread: option) -> result<_, error-code>; /// Creates a consumer in a consumer group. /// /// XGROUP CREATECONSUMER key group consumer /// https://valkey.io/commands/xgroup-createconsumer/ @since(version = 0.3.0-dev) - xgroup-createconsumer: async func(connection: borrow, key: string, group: string, consumer: string) -> result; + xgroup-createconsumer: async func(connection: borrow, key: string, group: string, consumer: string) -> result; /// Deletes a consumer from a consumer group. /// /// XGROUP DELCONSUMER key group consumer /// https://valkey.io/commands/xgroup-delconsumer/ @since(version = 0.3.0-dev) - xgroup-delconsumer: async func(connection: borrow, key: string, group: string, consumer: string) -> result; + xgroup-delconsumer: async func(connection: borrow, key: string, group: string, consumer: string) -> result; /// Destroys a consumer group. /// /// XGROUP DESTROY key group /// https://valkey.io/commands/xgroup-destroy/ @since(version = 0.3.0-dev) - xgroup-destroy: async func(connection: borrow, key: string, group: string) -> result; + xgroup-destroy: async func(connection: borrow, key: string, group: string) -> result; /// Sets the last-delivered ID of a consumer group. /// /// XGROUP SETID key group 〈 id | $ 〉 [ ENTRIESREAD entriesread ] /// https://valkey.io/commands/xgroup-setid/ @since(version = 0.3.0-dev) - xgroup-setid: async func(connection: borrow, key: string, group: string, id: stream-id, entriesread: option) -> result<_, error>; + xgroup-setid: async func(connection: borrow, key: string, group: string, id: stream-id, entriesread: option) -> result<_, error-code>; /// Returns a list of the consumers in a consumer group. /// /// XINFO CONSUMERS key group /// https://valkey.io/commands/xinfo-consumers/ @since(version = 0.3.0-dev) - xinfo-consumers: async func(connection: borrow, key: string, group: string) -> result, error>; + xinfo-consumers: async func(connection: borrow, key: string, group: string) -> result, error-code>; /// Returns a list of the consumer groups of a stream. /// /// XINFO GROUPS key /// https://valkey.io/commands/xinfo-groups/ @since(version = 0.3.0-dev) - xinfo-groups: async func(connection: borrow, key: string) -> result, error>; + xinfo-groups: async func(connection: borrow, key: string) -> result, error-code>; /// Returns information about a stream. /// @@ -2408,7 +2408,7 @@ interface commands { /// XINFO STREAM key /// https://valkey.io/commands/xinfo-stream/ @since(version = 0.3.0-dev) - xinfo-stream: async func(connection: borrow, key: string) -> result; + xinfo-stream: async func(connection: borrow, key: string) -> result; /// Returns information about a stream. /// @@ -2419,14 +2419,14 @@ interface commands { /// XINFO STREAM key FULL [ COUNT count ] /// https://valkey.io/commands/xinfo-stream/ @since(version = 0.3.0-dev) - xinfo-stream-full: async func(connection: borrow, key: string, count: option) -> result; + xinfo-stream-full: async func(connection: borrow, key: string, count: option) -> result; /// Returns the number of messages in a stream. /// /// XLEN key /// https://valkey.io/commands/xlen/ @since(version = 0.3.0-dev) - xlen: async func(connection: borrow, key: string) -> result; + xlen: async func(connection: borrow, key: string) -> result; /// Returns the information and entries from a stream consumer group's pending entries list. /// @@ -2437,7 +2437,7 @@ interface commands { /// XPENDING key group /// https://valkey.io/commands/xpending/ @since(version = 0.3.0-dev) - xpending: async func(connection: borrow, key: string, group: string) -> result; + xpending: async func(connection: borrow, key: string, group: string) -> result; /// Returns the information and entries from a stream consumer group's pending entries list. /// @@ -2448,14 +2448,14 @@ interface commands { /// XPENDING key group [ IDLE min-idle-time ] start end count [ consumer ] /// https://valkey.io/commands/xpending/ @since(version = 0.3.0-dev) - xpending-extended: async func(connection: borrow, key: string, group: string, min-idle-time: option, start: stream-id, end: stream-id, count: integer, consumer: option) -> result, error>; + xpending-extended: async func(connection: borrow, key: string, group: string, min-idle-time: option, start: stream-id, end: stream-id, count: integer, consumer: option) -> result, error-code>; /// Returns the messages from a stream within a range of IDs. /// /// XRANGE key start end [ COUNT count ] /// https://valkey.io/commands/xrange/ @since(version = 0.3.0-dev) - xrange: async func(connection: borrow, key: string, start: stream-id, end: stream-id, count: option) -> result>>, error>; + xrange: async func(connection: borrow, key: string, start: stream-id, end: stream-id, count: option) -> result>>, error-code>; /// Returns messages from multiple streams with IDs greater than the ones requested. /// @@ -2465,7 +2465,7 @@ interface commands { /// XREAD [ COUNT count ] [ BLOCK milliseconds ] STREAMS key [ key ... ] ID [ ID ... ] /// https://valkey.io/commands/xread/ @since(version = 0.3.0-dev) - xread: async func(connection: borrow, count: option, block: option, keys: list, ids: list) -> result>>>>, error>; + xread: async func(connection: borrow, count: option, block: option, keys: list, ids: list) -> result>>>>, error-code>; /// Returns new or historical messages from a stream for a consumer in a group. /// @@ -2478,112 +2478,112 @@ interface commands { /// XREADGROUP GROUP group consumer [ COUNT count ] [ BLOCK milliseconds ] [ NOACK ] STREAMS key [ key ... ] ID [ ID ... ] /// https://valkey.io/commands/xreadgroup/ @since(version = 0.3.0-dev) - xreadgroup: async func(connection: borrow, group: string, consumer: string, count: option, block: option, noack: bool, keys: list, ids: list) -> result>>>>>, error>; + xreadgroup: async func(connection: borrow, group: string, consumer: string, count: option, block: option, noack: bool, keys: list, ids: list) -> result>>>>>, error-code>; /// Returns the messages from a stream within a range of IDs in reverse order. /// /// XREVRANGE key end start [ COUNT count ] /// https://valkey.io/commands/xrevrange/ @since(version = 0.3.0-dev) - xrevrange: async func(connection: borrow, key: string, end: stream-id, start: stream-id, count: option) -> result>>, error>; + xrevrange: async func(connection: borrow, key: string, end: stream-id, start: stream-id, count: option) -> result>>, error-code>; /// An internal command for replicating stream values. /// /// XSETID key last-id [ ENTRIESADDED entries-added ] [ MAXDELETEDID max-deleted-id ] /// https://valkey.io/commands/xsetid/ @since(version = 0.3.0-dev) - xsetid: async func(connection: borrow, key: string, last-id: stream-id, entries-added: option, max-deleted-id: option) -> result<_, error>; + xsetid: async func(connection: borrow, key: string, last-id: stream-id, entries-added: option, max-deleted-id: option) -> result<_, error-code>; /// Deletes messages from the beginning of a stream. /// /// XTRIM key 〈 MAXLEN | MINID 〉 [ = | ~ ] threshold [ LIMIT count ] /// https://valkey.io/commands/xtrim/ @since(version = 0.3.0-dev) - xtrim: async func(connection: borrow, key: string, trim: stream-trim) -> result; + xtrim: async func(connection: borrow, key: string, trim: stream-trim) -> result; /// Appends a string to the value of a key. /// /// APPEND key value /// https://valkey.io/commands/append/ @since(version = 0.3.0-dev) - append: async func(connection: borrow, key: string, value: string-or-bytes) -> result; + append: async func(connection: borrow, key: string, value: string-or-bytes) -> result; /// Decrements the integer value of a key by one. /// /// DECR key /// https://valkey.io/commands/decr/ @since(version = 0.3.0-dev) - decr: async func(connection: borrow, key: string) -> result; + decr: async func(connection: borrow, key: string) -> result; /// Decrements the integer value of a key by a number. /// /// DECRBY key decrement /// https://valkey.io/commands/decrby/ @since(version = 0.3.0-dev) - decrby: async func(connection: borrow, key: string, decrement: integer) -> result; + decrby: async func(connection: borrow, key: string, decrement: integer) -> result; /// Delete key if value matches string. /// /// DELIFEQ key value /// https://valkey.io/commands/delifeq/ @since(version = 0.3.0-dev) - delifeq: async func(connection: borrow, key: string, value: string-or-bytes) -> result; + delifeq: async func(connection: borrow, key: string, value: string-or-bytes) -> result; /// Returns the string value of a key. /// /// GET key /// https://valkey.io/commands/get/ @since(version = 0.3.0-dev) - get: async func(connection: borrow, key: string) -> result, error>; + get: async func(connection: borrow, key: string) -> result, error-code>; /// Returns the string value of a key after deleting the key. /// /// GETDEL key /// https://valkey.io/commands/getdel/ @since(version = 0.3.0-dev) - getdel: async func(connection: borrow, key: string) -> result, error>; + getdel: async func(connection: borrow, key: string) -> result, error-code>; /// Returns the string value of a key after setting its expiration time. /// /// GETEX key [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST ] /// https://valkey.io/commands/getex/ @since(version = 0.3.0-dev) - getex: async func(connection: borrow, key: string, ttl: option) -> result, error>; + getex: async func(connection: borrow, key: string, ttl: option) -> result, error-code>; /// Returns a substring of the string stored at a key. /// /// GETRANGE key start end /// https://valkey.io/commands/getrange/ @since(version = 0.3.0-dev) - getrange: async func(connection: borrow, key: string, start: integer, end: integer) -> result; + getrange: async func(connection: borrow, key: string, start: integer, end: integer) -> result; /// Returns the previous string value of a key after setting it to a new value. /// /// GETSET key value /// https://valkey.io/commands/getset/ @since(version = 0.3.0-dev) - getset: async func(connection: borrow, key: string, value: string-or-bytes) -> result, error>; + getset: async func(connection: borrow, key: string, value: string-or-bytes) -> result, error-code>; /// Increments the integer value of a key by one. /// /// INCR key /// https://valkey.io/commands/incr/ @since(version = 0.3.0-dev) - incr: async func(connection: borrow, key: string) -> result; + incr: async func(connection: borrow, key: string) -> result; /// Increments the integer value of a key by a number. /// /// INCRBY key increment /// https://valkey.io/commands/incrby/ @since(version = 0.3.0-dev) - incrby: async func(connection: borrow, key: string, increment: integer) -> result; + incrby: async func(connection: borrow, key: string, increment: integer) -> result; /// Increments the floating point value of a key by a number. /// /// INCRBYFLOAT key increment /// https://valkey.io/commands/incrbyfloat/ @since(version = 0.3.0-dev) - incrbyfloat: async func(connection: borrow, key: string, increment: double) -> result; + incrbyfloat: async func(connection: borrow, key: string, increment: double) -> result; /// Finds the longest common substring. /// @@ -2592,7 +2592,7 @@ interface commands { /// LCS key1 key2 /// https://valkey.io/commands/lcs/ @since(version = 0.3.0-dev) - lcs: async func(connection: borrow, key1: string, key2: string) -> result; + lcs: async func(connection: borrow, key1: string, key2: string) -> result; /// Finds the longest common substring. /// @@ -2603,7 +2603,7 @@ interface commands { /// LCS key1 key2 LEN /// https://valkey.io/commands/lcs/ @since(version = 0.3.0-dev) - lcs-len: async func(connection: borrow, key1: string, key2: string) -> result; + lcs-len: async func(connection: borrow, key1: string, key2: string) -> result; /// Finds the longest common substring. /// @@ -2614,42 +2614,42 @@ interface commands { /// LCS key1 key2 IDX [ MINMATCHLEN min-match-len ] [ WITHMATCHLEN ] /// https://valkey.io/commands/lcs/ @since(version = 0.3.0-dev) - lcs-idx: async func(connection: borrow, key1: string, key2: string, min-match-len: option, with-match-len: bool ) -> result; + lcs-idx: async func(connection: borrow, key1: string, key2: string, min-match-len: option, with-match-len: bool ) -> result; /// Atomically returns the string values of one or more keys. /// /// MGET key [ key ... ] /// https://valkey.io/commands/mget/ @since(version = 0.3.0-dev) - mget: async func(connection: borrow, keys: list) -> result>, error>; + mget: async func(connection: borrow, keys: list) -> result>, error-code>; /// Atomically creates or modifies the string values of one or more keys. /// /// MSET key value [ key value ... ] /// https://valkey.io/commands/mset/ @since(version = 0.3.0-dev) - mset: async func(connection: borrow, keys-and-values: list>) -> result<_, error>; + mset: async func(connection: borrow, keys-and-values: list>) -> result<_, error-code>; /// Atomically creates or modifies the string values of one or more keys, and optionally set their expiration. /// /// MSETEX numkeys key value [ key value ... ] [ NX | XX ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] /// https://valkey.io/commands/msetex/ @since(version = 0.3.0-dev) - msetex: async func(connection: borrow, keys-and-values: list>, precondition: option, ttl: option) -> result; + msetex: async func(connection: borrow, keys-and-values: list>, precondition: option, ttl: option) -> result; /// Atomically modifies the string values of one or more keys only when all keys don't exist. /// /// MSETNX key value [ key value ... ] /// https://valkey.io/commands/msetnx/ @since(version = 0.3.0-dev) - msetnx: async func(connection: borrow, keys-and-values: list>) -> result; + msetnx: async func(connection: borrow, keys-and-values: list>) -> result; /// Sets both string value and expiration time in milliseconds of a key. /// /// PSETEX key milliseconds value /// https://valkey.io/commands/psetex/ @since(version = 0.3.0-dev) - psetex: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, value: string-or-bytes) -> result<_, error>; + psetex: async func(connection: borrow, key: string, milliseconds: duration-milliseconds, value: string-or-bytes) -> result<_, error-code>; /// Sets the string value of a key, ignoring its type. /// @@ -2660,7 +2660,7 @@ interface commands { /// SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] /// https://valkey.io/commands/set/ @since(version = 0.3.0-dev) - set: async func(connection: borrow, key: string, value: string-or-bytes, precondition: option, ttl: option) -> result; + set: async func(connection: borrow, key: string, value: string-or-bytes, precondition: option, ttl: option) -> result; /// Sets the string value of a key, ignoring its type. /// @@ -2671,41 +2671,41 @@ interface commands { /// SET key value [ NX | XX | IFEQ comparison-value | IFNE comparison-not-equal ] GET [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] /// https://valkey.io/commands/set/ @since(version = 0.3.0-dev) - set-get: async func(connection: borrow, key: string, value: string-or-bytes, precondition: option, ttl: option) -> result, error>; + set-get: async func(connection: borrow, key: string, value: string-or-bytes, precondition: option, ttl: option) -> result, error-code>; /// Sets the string value and expiration time of a key. /// /// SETEX key seconds value /// https://valkey.io/commands/setex/ @since(version = 0.3.0-dev) - setex: async func(connection: borrow, key: string, seconds: duration-seconds, value: string-or-bytes) -> result<_, error>; + setex: async func(connection: borrow, key: string, seconds: duration-seconds, value: string-or-bytes) -> result<_, error-code>; /// Sets the string value of a key only when the key doesn't exist. /// /// SETNX key value /// https://valkey.io/commands/setnx/ @since(version = 0.3.0-dev) - setnx: async func(connection: borrow, key: string, value: string-or-bytes) -> result; + setnx: async func(connection: borrow, key: string, value: string-or-bytes) -> result; /// Overwrites a part of a string value with another by an offset. /// /// SETRANGE key offset value /// https://valkey.io/commands/setrange/ @since(version = 0.3.0-dev) - setrange: async func(connection: borrow, key: string, offset: u32, value: string-or-bytes) -> result; + setrange: async func(connection: borrow, key: string, offset: u32, value: string-or-bytes) -> result; /// Returns the length of a string value. /// /// STRLEN key /// https://valkey.io/commands/strlen/ @since(version = 0.3.0-dev) - strlen: async func(connection: borrow, key: string) -> result; + strlen: async func(connection: borrow, key: string) -> result; /// Returns a substring from a string value. /// /// SUBSTR key start end /// https://valkey.io/commands/substr/ @since(version = 0.3.0-dev) - substr: async func(connection: borrow, key: string, start: integer, end: integer) -> result; + substr: async func(connection: borrow, key: string, start: integer, end: integer) -> result; } diff --git a/wit/connection.wit b/wit/connection.wit index d9c0862..a27712a 100644 --- a/wit/connection.wit +++ b/wit/connection.wit @@ -10,7 +10,7 @@ /// functions in the `commands` interface that borrow the connection. interface connection { @since(version = 0.3.0-dev) - use types.{connect-opts, duration-milliseconds, error, resp-value, string-or-bytes, subscription-event, subscription-target}; + use types.{connect-opts, duration-milliseconds, error-code, resp-value, string-or-bytes, subscription-event, subscription-target}; @since(version = 0.3.0-dev) resource connection { @@ -33,7 +33,7 @@ interface connection { /// Async so the connection keeps working between commands after it returns, e.g. to read /// push messages and to send keep-alive pings. @since(version = 0.3.0-dev) - new: static async func(replies: stream, closed: future>, opts: option) -> tuple, future>>; + new: static async func(replies: stream, closed: future>, opts: option) -> tuple, future>>; /// The server's properties from the reply to HELLO, e.g. `server`, `version` and `mode`. @since(version = 0.3.0-dev) @@ -55,7 +55,7 @@ interface connection { /// /// A command has at least one word, an empty command fails with a client error. @since(version = 0.3.0-dev) - exchange: async func(command: list) -> result; + exchange: async func(command: list) -> result; /// Sends the commands one after another, with no other command sent between them, and waits /// for every reply. @@ -64,7 +64,7 @@ interface connection { /// sharing a connection may otherwise be interleaved with other callers' commands, which /// matters for a sequence that must not be interrupted, e.g. MULTI, its commands and EXEC. @since(version = 0.3.0-dev) - exchange-all: async func(commands: list>) -> list>; + exchange-all: async func(commands: list>) -> list>; /// Sends a command without waiting for its reply, resolving once the command is written. /// @@ -74,7 +74,7 @@ interface connection { /// /// A command has at least one word, an empty command fails with a client error. @since(version = 0.3.0-dev) - emit: async func(command: list) -> result<_, error>; + emit: async func(command: list) -> result<_, error-code>; /// Marks the keys to watch, EXEC aborts the next transaction when any of them changed. /// @@ -87,14 +87,14 @@ interface connection { /// /// At least one key, no keys fails with a client error. @since(version = 0.3.0-dev) - watch: async func(keys: list) -> result<_, error>; + watch: async func(keys: list) -> result<_, error-code>; /// Forgets all the watched keys. /// /// UNWATCH /// https://valkey.io/commands/unwatch/ @since(version = 0.3.0-dev) - unwatch: async func() -> result<_, error>; + unwatch: async func() -> result<_, error-code>; /// Runs the commands as a transaction, MULTI, each command, then EXEC, with no other command /// sent between them, see `exchange-all`. @@ -111,7 +111,7 @@ interface connection { /// https://valkey.io/commands/multi/ /// https://valkey.io/commands/exec/ @since(version = 0.3.0-dev) - transaction: async func(commands: list>) -> result>, error>; + transaction: async func(commands: list>) -> result>, error-code>; /// A new subscription, subscribed to nothing yet, see the subscription's `subscribe`. /// @@ -125,7 +125,7 @@ interface connection { /// e.g. `connection-minimal`. Its push messages are then all dead letters, and SUBSCRIBE and /// the like may still be sent with `exchange`. @since(version = 0.3.0-dev) - subscription: func() -> result; + subscription: func() -> result; /// The values nothing else claims, kept apart from replies. /// @@ -160,7 +160,7 @@ interface connection { /// RESET /// https://valkey.io/commands/reset/ @since(version = 0.3.0-dev) - reset: async func() -> result<_, error>; + reset: async func() -> result<_, error-code>; /// Sends PING when no command has been sent and no reply is outstanding for `interval`, /// `none` disables it. A PING that fails closes the connection. Disabled by default. @@ -198,7 +198,7 @@ interface connection { /// /// At least one target, no targets fails with a client error. @since(version = 0.3.0-dev) - subscribe: async func(targets: list) -> result<_, error>; + subscribe: async func(targets: list) -> result<_, error-code>; /// The events arriving after the call: targets subscribed to and unsubscribed from, and /// messages, in the order they happened. @@ -220,7 +220,7 @@ interface connection { /// events it missed, messages and changes of targets alike, are lost. The future resolves /// after the stream delivered what it holds, read the stream to its end first. @since(version = 0.3.0-dev) - events: func() -> tuple, future>>; + events: func() -> tuple, future>>; /// The targets the server confirmed, in the order they were subscribed to, until they are /// unsubscribed from. @@ -248,6 +248,6 @@ interface connection { /// A list of targets has at least one target, otherwise fails with a client error. None /// unsubscribes from every target. @since(version = 0.3.0-dev) - unsubscribe: async func(targets: option>) -> result<_, error>; + unsubscribe: async func(targets: option>) -> result<_, error-code>; } } diff --git a/wit/connector.wit b/wit/connector.wit index 6a5b3a7..a421b09 100644 --- a/wit/connector.wit +++ b/wit/connector.wit @@ -6,11 +6,11 @@ @since(version = 0.3.0-dev) interface connector { @since(version = 0.3.0-dev) - use types.{error}; + use types.{error-code}; @since(version = 0.3.0-dev) use connection.{connection}; /// Establishes a new connection. @since(version = 0.3.0-dev) - connect: async func() -> result; + connect: async func() -> result; } diff --git a/wit/tcp.wit b/wit/tcp.wit index 2377e2e..c0d441e 100644 --- a/wit/tcp.wit +++ b/wit/tcp.wit @@ -1,7 +1,7 @@ /// A transport for connections over wasi:sockets TCP. interface tcp { @since(version = 0.3.0-dev) - use types.{connect-opts, error, resp-value}; + use types.{connect-opts, error-code, resp-value}; @since(version = 0.3.0-dev) use connection.{connection}; @@ -12,7 +12,7 @@ interface tcp { /// Opens a transport to `host`, a hostname or an IP address. Each address the host resolves /// to is tried in turn. @since(version = 0.3.0-dev) - open: static async func(host: string, port: u16) -> result; + open: static async func(host: string, port: u16) -> result; /// Writes each value from `requests` to the server, in order. /// @@ -20,7 +20,7 @@ interface tcp { /// socket. The future resolves once every value is written, or with the error that stopped /// writing. @since(version = 0.3.0-dev) - send: func(requests: stream) -> future>; + send: func(requests: stream) -> future>; /// The values the server sends, in order. /// @@ -28,12 +28,12 @@ interface tcp { /// closes the socket, the future resolves to `ok` for a graceful close or to the error /// otherwise. @since(version = 0.3.0-dev) - receive: func() -> tuple, future>>; + receive: func() -> tuple, future>>; } /// Opens a transport and establishes a connection over it with `opts`, see `connection.new`. /// /// The connection keeps the transport alive. @since(version = 0.3.0-dev) - connect: async func(host: string, port: u16, opts: option) -> result; + connect: async func(host: string, port: u16, opts: option) -> result; } diff --git a/wit/types.wit b/wit/types.wit index 6aca647..9f4deae 100644 --- a/wit/types.wit +++ b/wit/types.wit @@ -39,7 +39,7 @@ interface types { boolean(bool), double(double), big-number(string), - bulk-error(string), + bulk-error(bytes), verbatim-string(tuple), %map(list>), set(list), @@ -55,7 +55,7 @@ interface types { /// The errors of a connection and of every command interface. @since(version = 0.3.0-dev) - variant error { + variant error-code { /// The transport closed while a reply was outstanding, or before the command was sent. Holds /// the transport's error when it did not close gracefully. closed(option), @@ -64,8 +64,9 @@ interface types { client(string), /// RESP errors are encoding issues converting values to/from bytes. resp(resp-error), - /// Valkey errors are Error or BulkError values returned directly from the server. - valkey(string), + /// Server errors are Error or BulkError RESP values returned directly from the server. An Error + /// is a string, a BulkError a string when it is UTF-8, bytes otherwise. + server(string-or-bytes), /// The reader of a stream fell too far behind and its stream was closed, rather than /// holding up the connection. What the stream missed is lost. lagged,