# DEVELOPER guide — `valkey-glide`
## Prerequisites
- **Rust** — install via [rustup](https://rustup.rs)
- The crate depends on other in-repo crates via **path dependencies** (see the
Crates & dependencies section below), so **a monorepo checkout is required** —
it builds from a `valkey-io/valkey-glide` checkout where those crates sit
alongside it (this crate lives under `rust/`). The internal crates resolve
from the checkout rather than from crates.io.
- A `valkey-server` (or `redis-server`) binary for integration tests.
The harness auto-discovers one on `PATH`; override with:
```bash
export VALKEY_SERVER_PATH=/path/to/valkey-server
```
## Crates & dependencies
The `valkey-glide` crate depends on several internal crates:
| `valkey-glide` | `glide` | `rust/` | `glide-core`, `glide-core-engine`, `glide-logger` |
| `glide-core` | `glide_core` | `glide-core/` | `glide-core-engine`, `glide-telemetry`, `glide-logger` |
| `glide-core-engine` | `redis` | `glide-core/redis-rs/redis/` | `glide-telemetry`, `glide-logger` |
| `glide-telemetry` | `glide_telemetry` | `glide-telemetry/` | `glide-logger` |
| `glide-logger` | `glide_logger` | `glide-logger/` | None |
The `glide-core-engine` crate is forked from redis-rs 0.25.2, so it uses the `redis` library name.
### Publishing and dual versioning
Cargo requires every dependency of a published crate to also be published, so
the internal crates it depends on must be published too. They are published
with `0.x` versions and marked as internal, consistent with Rust conventions.
In order to allow developers to build against the local source rather than a
released version, the internal dependencies are declared with **both** `path`
and `version` in `Cargo.toml`:
```toml
glide-core = { path = "../glide-core", version = "0.1" }
```
Cargo uses `path` when building locally, and `version` when the crate is resolved
from crates.io after publishing.
## Build
```bash
cargo build # debug
cargo build --release # optimized
```
## Test
The suite has three layers, all run in CI:
- **Unit tests (server-free)** — pure logic with no server: config to
`ConnectionRequest` lowering, route to `RoutingInfo` mapping, option/argument
encoding, value conversion, and error mapping; plus a **command-family mock
suite** that drives every typed command through an in-process executor to
assert exact **request encoding** and **response decoding**.
- **Integration tests (live server)** — real round-trips against a spawned
`valkey-server`, one `tests/it_<family>.rs` per command family with
edge/error cases (wrong-type, missing key, bounds, expiry conditions),
**parametrized over RESP2 and RESP3**, plus suites for batches, scan,
pub/sub, auth, TLS, and a **native multi-shard cluster** harness.
- **Doctests** — the examples in `README.md` and the API docs are compiled.
```bash
cargo unit-tests # unit tests
cargo doc-tests # doctests
cargo integration-tests # integration tests
cargo test # all tests (unit, docs, and integration)
```
To run only some tests, pass a filter. A test runs if its full name (e.g.
`value::from_valkey_value_tests::from_owned_valkey_value_array`) contains the filter:
```bash
cargo unit-tests value:: # unit tests containing `value::`
cargo doc-tests cmd::Cmd # doctests containing `cmd::Cmd`
cargo integration-tests get_missing # integration tests containing `get_missing`
```
To run one integration test file, use `cargo test --test <file>`, with an
optional filter:
```bash
cargo test --test it_string # every test in tests/it_string.rs
cargo test --test it_string get_missing # only those containing `get_missing`
```
Integration tests each boot their own ephemeral server on a free port and tear it
down on drop. The test fails if no server binary is found (see
`VALKEY_SERVER_PATH` above) or the server cannot be started.
## Lint & format
Recommended checks before opening a PR:
```bash
cargo fmt --all -- --check
cargo clippy --all-features --all-targets -- -D warnings
cargo clippy --all-targets -- -D warnings
cargo deny --config ../deny.toml check
cargo doc --no-deps --document-private-items
```
## Layout
```text
src/
lib.rs crate root + public re-exports
error.rs GlideError (mirrors Python exceptions)
cmd.rs Cmd, GLIDE's owned command builder (cmd(), query_async)
pipeline.rs Pipeline, GLIDE's owned pipeline / transaction builder
types.rs typed reply types (ValueType, IntegerReplyOrNoOp)
config/ client configuration -> glide_core ConnectionRequest
common.rs shared types + builder-setter macro + request lowering
standalone.rs GlideClientConfiguration
cluster.rs GlideClusterClientConfiguration
routes.rs cluster routing (Route -> RoutingInfo)
value.rs ValkeyValue and typed conversions (RESP2 + RESP3)
write.rs ToValkeyArgs and ValkeyWrite argument encoding
executor.rs CommandExecutor seam + custom_command
client/
mod.rs GlideClient / GlideClusterClient (async)
pipeline.rs pipeline dispatch + typed execution (PipelineExt::query_async)
pipeline_options.rs Pipeline execution options (exec)
script.rs Script (SHA-caching EVALSHA with EVAL fallback)
telemetry.rs OpenTelemetry config + init
sync/
mod.rs blocking clients over a shared runtime
pipeline.rs pipeline dispatch + typed execution (sync::PipelineExt::query)
mock_tests/ server-free encoding/decoding tests for the extensions
parity_tests/ redis-rs signature-parity guard
mod.rs parser + comparison (the redis_parity_check test)
redis_parity.json cached redis-rs command-table snapshot
differences.json pinned deliberate differences
test_utils.rs shared unit-test helpers (assert_args)
commands/
core.rs the command table, generating Cmd constructors, Pipeline methods,
and AsyncCommands / Commands and their typed counterparts
(including the scan methods)
scan.rs GLIDE-owned scan iterators (ScanIter, SyncScanIter)
options.rs option types shared across command families
<family>.rs extension traits (blanket impls over CommandExecutor)
tests/
common/ shared harness (server, cluster, timeout, pubsub, macros)
it_*.rs per-family live tests (one file per command family)
```
## Adding a command
If redis-rs's command table has the command:
1. Add an entry to the `implement_commands!` table in `src/commands/core.rs`,
copying redis-rs's signature, typed return type, and command body.
2. Remove its entry from `src/parity_tests/differences.json`. If GLIDE must
differ, update the entry's `glide` side and `reason` instead, and document
the difference in `migration.md`.
3. Add an integration test in `tests/it_<family>.rs` (use the `matrix_test!`
macro for standalone and cluster with RESP2 and RESP3).
Otherwise, add it to the family's extension trait:
1. Pick the family module in `src/commands/`.
2. Add an `async fn` to that family's trait following the template in
`string.rs`: build a `Cmd`, call `self.execute_command(cmd, None)`,
convert with a `crate::value::*` helper.
3. Add an integration test in the family's `tests/it_<family>.rs` (use the
`resp_test!` macro for RESP2/RESP3 coverage), and a server-free encoding test
in `src/mock_tests/<family>.rs`.
Then run `cargo test && cargo clippy --all-targets`.
## Extending value conversion
Because the client negotiates **RESP3** by default, replies may arrive as
`ValkeyValue::Map`, `ValkeyValue::Double`, `ValkeyValue::Boolean`, or
`ValkeyValue::VerbatimString`.
Prefer the helpers in `src/value.rs`, which already normalize these, and add new
shapes there rather than in individual commands.
## Maintaining the unified command table
The unified `AsyncCommands` / `Commands` traits and their typed counterparts
(`AsyncTypedCommands` / `TypedCommands`) are defined by the
**hand-maintained** command table in `src/commands/core.rs` (one
`implement_commands!` invocation; each `fn name<G: Bound>(args) -> (T) { body }`
entry expands to the async and blocking methods, the typed async and blocking
methods returning `T`, the pipeline method, and a `Cmd::<name>()` constructor).
Copy the return annotation from redis-rs: `-> (T)` for a concrete type, or
`-> Generic` to keep a caller-chosen `RV` in the typed traits too.
To add or change an entry, edit the table directly — then run the
signature-parity guard. It parses GLIDE's table (`src/commands/core.rs`)
and compares it against a committed snapshot of the redis-rs surface
(`src/parity_tests/redis_parity.json`).
```bash
cargo test --lib parity_tests
```
The snapshot is a **trusted baseline**: the guard does not re-verify it against
the redis-rs source on every run — it only rebuilds the snapshot when the file
is absent, fetching the upstream redis-rs sources for the targeted release tag
from GitHub (via `curl`). Normal runs are offline.
The targeted redis-rs version is `REDIS_RS_VERSION` in `src/parity_tests/mod.rs`
(currently `1.7.0`). Bumping the constant makes the committed snapshot's version
mismatch and the guard fail until the snapshot is regenerated (delete it and
re-run).
The guard is fail-closed: every redis-rs method must be implemented with a
matching signature (including the declared return type), and GLIDE's table
must not add methods. The only exceptions are the deliberate differences pinned
in `src/parity_tests/differences.json`, indexed by method name. Each entry has a
`reason` and the method as each side declares it, `redis` and `glide`, either of
which may be `null`:
- **Both set:** GLIDE declares a different signature. Usually redis-rs's
return type cannot decode the reply GLIDE receives (e.g. `zpopmin`, whose
reply glide-core normalizes to a map); `hset_multiple` differs by design (see
`migration.md`).
- **Only `redis` set:** GLIDE does not implement the method yet.
- **Only `glide` set:** GLIDE adds a command-table method redis-rs does not
have.
The guard allows exactly the pinned entries, and fails if one no longer holds
(either side's method changed, appeared or disappeared, or the two signatures
now match), so update or remove the entry when that happens. For example,
remove a method's entry once GLIDE implements it.
Commands beyond the redis-rs surface belong in the per-family extension traits
(`src/commands/<family>.rs`), not in the table.
## Publishing to crates.io
Crates are published with the **Rust - Continuous Deployment** workflow
(`.github/workflows/rust-cd.yml`):
1. Bump `version` in the `Cargo.toml` of each crate to release (see
[versioning](#versioning) below), and update the `Cargo.lock` files that
reference it.
2. Run the workflow with `publish=false` to perform a dry run. A crate's dry run
fails if it depends on a version of another GLIDE crate that isn't published
yet.
3. Run the workflow from a `release-*` branch with `publish=true`. Publishing
requires maintainer approval.
### Versioning
Each crate is versioned independently:
- The `valkey-glide` crate follows [Semantic Versioning](https://semver.org/).
- The internal crates are published with `0.x` versions, consistent
with Rust conventions. For `0.x` versions, Cargo treats a minor bump
(e.g. `0.1.1` to `0.2.0`) as breaking, and a patch bump (e.g. `0.1.1`
to `0.1.2`) as compatible.