valkey-module 0.1.12

A toolkit for building valkey modules in Rust
Documentation
[![license](https://img.shields.io/github/license/RedisLabsModules/redismodule-rs.svg)](https://github.com/valkey-io/valkeymodule-rs/blob/main/LICENSE)
[![Releases](https://img.shields.io/github/release/RedisLabsModules/redismodule-rs.svg)](https://github.com/valkey-io/valkeymodule-rs/releases)
[![crates.io](https://img.shields.io/crates/v/redis-module.svg)](https://crates.io/crates/valkey-module)
[![docs](https://docs.rs/redis-module/badge.svg)](https://docs.rs/valkey-module)
[![CircleCI](https://circleci.com/gh/RedisLabsModules/redismodule-rs/tree/master.svg?style=svg)](https://circleci.com/gh/RedisLabsModules/redismodule-rs/tree/master)

# valkeymodule-rs

This crate provides an idiomatic Rust API for the [Valkey Modules API](https://valkey.io/topics/modules-api-ref/).
It allows writing Valkey modules in Rust, without needing to use raw pointers or unsafe code. See [here](https://docs.rs/valkey-module/latest) for the most recent API documentation.

This repo was forked from [redismodule-rs](https://github.com/RedisLabsModules/redismodule-rs).  We appreciate the contributions of the original authors.  

# Running the example module

1. [Install Rust]https://www.rust-lang.org/tools/install
2. [Install Valkey]https://valkey.io/download/, most likely using your favorite package manager (Homebrew on Mac, APT or YUM on Linux)
3. Run `cargo build --example hello`
4. Start a valkey server with the `hello` module
   * Linux: `valkey-server --loadmodule ./target/debug/examples/libhello.so`
   * Mac: `valkey-server --loadmodule ./target/debug/examples/libhello.dylib`
5. Open a valkey-cli, and run `HELLO.MUL 31 11`.

# Writing your own module

See the [examples](examples) directory for some sample modules.

This crate tries to provide high-level wrappers around the standard Valkey Modules API, while preserving the API's basic concepts.
Therefore, following the [Valkey Modules API](https://valkey.io/topics/modules-api-ref/) documentation will be mostly relevant here as well.

## Feature Flags

### System Allocator

This feature flag is ideal for unit testing where the engine server is not running, and we do not have access to the Valkey engine allocator, so we can use the system allocator instead.
To optionally enter the `System.alloc` code paths in `alloc.rs` specify this in `Cargo.toml` of your module:
```toml
[features]
enable-system-alloc = ["valkey-module/enable-system-alloc"]
```
For unit tests with `System.alloc` use this:
```sh
cargo test --features enable-system-alloc
```
For integration tests with `ValkeyAlloc` use this:
```sh
cargo test
```

### Unit testing without a Valkey server

The `test-shims` feature provides `Context::test()`, `CommandFilterCtx::test()`, and `ValkeyString::test()` for unit tests that run without starting a Valkey process. Add `valkey-module` with this feature to your development dependencies, using the same version or source as your normal dependency:

```toml
[dev-dependencies]
valkey-module = { version = "...", features = ["test-shims"] }
```

The feature automatically enables the system allocator required by tests. When configured as a development dependency, the test-only APIs are available to unit tests without enabling them in production builds.

Use `Context::test()` when a command handler needs a context. Its `expect_*` methods configure values returned by supported context APIs. Use `ValkeyString::test()` to construct binary-safe command arguments; it accepts any value implementing `Into<Vec<u8>>`, including strings and arbitrary bytes:

```rust
#[cfg(test)]
mod tests {
    use valkey_module::{Context, ValkeyString};

    #[test]
    fn uses_test_context_and_strings_without_valkey() {
        let mut context = Context::test();
        context
            .expect_get_client_id(42)
            .expect_get_server_version(8, 1, 2);

        let text = ValkeyString::test("hello");
        let binary = ValkeyString::test(vec![0x00, 0xff]);
        let version = context
            .get_server_version()
            .expect("configured server version should be returned");

        assert_eq!(context.get_client_id(), 42);
        assert_eq!((version.major, version.minor, version.patch), (8, 1, 2));
        assert_eq!(text.as_slice(), b"hello");
        assert_eq!(binary.as_slice(), &[0x00, 0xff]);
    }
}
```

The test context currently supports configuring the server version, client IDs, client names, client usernames, the current user, ACL-user authentication, and client deauthentication. Configure `Context::get_server_version()` with `expect_get_server_version(major, minor, patch)`. Calls to `set_module_options` are accepted as a no-op because their effects require a running server. `Context::create_string()` also works with a test context:

```rust
let context = Context::test();
let value = context.create_string("hello");

assert_eq!(value.as_slice(), b"hello");
```

Use `CommandFilterCtx::test()` to create a `TestCommandFilterCtx` for testing command-filter logic. The test wrapper dereferences to `CommandFilterCtx`, so it can be passed directly to a helper that contains the filter behavior:

```rust
use valkey_module::CommandFilterCtx;

fn rewrite_set(context: &CommandFilterCtx) {
    if context.args_count() == 3
        && context.cmd_get_try_as_str() == Ok("SET")
    {
        context.arg_replace(1, "new-key");
    }
}

let mut context = CommandFilterCtx::test();
context
    .expect_args_count(3)
    .expect_arg_get(0, "SET")
    .expect_arg_get(1, "key")
    .expect_arg_get(2, "value")
    .expect_get_client_id(42);

rewrite_set(&context);

assert_eq!(context.args_count(), 3);
assert_eq!(context.arg_get_try_as_str(1), Ok("new-key"));
assert_eq!(context.get_client_id(), 42);
```

`expect_args_count()` configures the reported number of arguments, while `expect_arg_get()` configures the binary-safe value at an individual position. The test context also supports command lookup, client ID lookup, and argument replacement, insertion, and deletion. Insertions and deletions update the argument count and shift subsequent arguments.

Run these tests normally:

```sh
cargo test
```

The first call to `Context::test()`, `CommandFilterCtx::test()`, or `ValkeyString::test()` installs process-wide test callbacks. Do not invoke the test shims inside a running Valkey process; setup rejects installation after the real Valkey API has been initialized. Only explicitly shimmed APIs work without Valkey. Other APIs, including `ValkeyString::append()`, still require a running server.

### Redis Compatibility

This feature flag is useful in case you have a Module that needs to be loaded on both Valkey and Redis Servers. In this case, you can use the `use-redismodule-api` flag so that the Module is loaded using the RedisModule API Initialization for compatibility.

To use this feature by conditionally, specify the following in your `Cargo.toml`:
```toml
[features]
use-redismodule-api = ["valkey-module/use-redismodule-api"]
default = []
```

```sh
cargo build --release --features use-redismodule-api
```