feat: Add redisclient package to the monorepo
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run

- Integrate the redisclient package into the workspace.
- Update the MONOREPO_CONVERSION_PLAN.md to reflect the
  completion of the redisclient package conversion.
  This includes marking its conversion as complete and
  updating the success metrics.
- Add the redisclient package's Cargo.toml file.
- Add the redisclient package's source code files.
- Add tests for the redisclient package.
- Add README file for the redisclient package.
This commit is contained in:
Mahmoud-Emad
2025-06-18 17:53:03 +03:00
parent 4d51518f31
commit 3e617c2489
16 changed files with 361 additions and 63 deletions

26
redisclient/Cargo.toml Normal file
View File

@@ -0,0 +1,26 @@
[package]
name = "sal-redisclient"
version = "0.1.0"
edition = "2021"
authors = ["PlanetFirst <info@incubaid.com>"]
description = "SAL Redis Client - Redis client wrapper with connection management and Rhai integration"
repository = "https://git.threefold.info/herocode/sal"
license = "Apache-2.0"
keywords = ["redis", "client", "database", "cache"]
categories = ["database", "caching", "api-bindings"]
[dependencies]
# Core Redis functionality
redis = "0.31.0"
lazy_static = "1.4.0"
# Rhai integration (optional)
rhai = { version = "1.12.0", features = ["sync"], optional = true }
[features]
default = ["rhai"]
rhai = ["dep:rhai"]
[dev-dependencies]
# For testing
tempfile = "3.5"

146
redisclient/README.md Normal file
View File

@@ -0,0 +1,146 @@
# Redis Client Module
A robust Redis client wrapper for Rust applications that provides connection management, automatic reconnection, and a simple interface for executing Redis commands.
## Features
- **Singleton Pattern**: Maintains a global Redis client instance, so we don't re-int all the time.
- **Connection Management**: Automatically handles connection creation and reconnection
- **Flexible Connectivity**:
- Tries Unix socket connection first (`$HOME/hero/var/myredis.sock`)
- Falls back to TCP connection (localhost) if socket connection fails
- **Database Selection**: Uses the `REDISDB` environment variable to select the Redis database (defaults to 0)
- **Authentication Support**: Supports username/password authentication
- **Builder Pattern**: Flexible configuration with a builder pattern
- **TLS Support**: Optional TLS encryption for secure connections
- **Error Handling**: Comprehensive error handling with detailed error messages
- **Thread Safety**: Safe to use in multi-threaded applications
## Usage
### Basic Usage
```rust
use crate::redisclient::execute;
use redis::cmd;
// Execute a simple SET command
let mut set_cmd = redis::cmd("SET");
set_cmd.arg("my_key").arg("my_value");
let result: redis::RedisResult<()> = execute(&mut set_cmd);
// Execute a GET command
let mut get_cmd = redis::cmd("GET");
get_cmd.arg("my_key");
let value: redis::RedisResult<String> = execute(&mut get_cmd);
if let Ok(val) = value {
println!("Value: {}", val);
}
```
### Advanced Usage
```rust
use crate::redisclient::{get_redis_client, reset};
// Get the Redis client directly
let client = get_redis_client()?;
// Execute a command using the client
let mut cmd = redis::cmd("HSET");
cmd.arg("my_hash").arg("field1").arg("value1");
let result: redis::RedisResult<()> = client.execute(&mut cmd);
// Reset the Redis client connection
reset()?;
```
### Builder Pattern
The module provides a builder pattern for flexible configuration:
```rust
use crate::redisclient::{RedisConfigBuilder, with_config};
// Create a configuration builder
let config = RedisConfigBuilder::new()
.host("redis.example.com")
.port(6379)
.db(1)
.username("user")
.password("secret")
.use_tls(true)
.connection_timeout(30);
// Connect with the configuration
let client = with_config(config)?;
```
### Unix Socket Connection
You can explicitly configure a Unix socket connection:
```rust
use crate::redisclient::{RedisConfigBuilder, with_config};
// Create a configuration builder for Unix socket
let config = RedisConfigBuilder::new()
.use_unix_socket(true)
.socket_path("/path/to/redis.sock")
.db(1);
// Connect with the configuration
let client = with_config(config)?;
```
## Environment Variables
- `REDISDB`: Specifies the Redis database number to use (default: 0)
- `REDIS_HOST`: Specifies the Redis host (default: 127.0.0.1)
- `REDIS_PORT`: Specifies the Redis port (default: 6379)
- `REDIS_USERNAME`: Specifies the Redis username for authentication
- `REDIS_PASSWORD`: Specifies the Redis password for authentication
- `HOME`: Used to determine the path to the Redis Unix socket
## Connection Strategy
1. First attempts to connect via Unix socket at `$HOME/hero/var/myredis.sock`
2. If socket connection fails, falls back to TCP connection at `redis://127.0.0.1/`
3. If both connection methods fail, returns an error
## Error Handling
The module provides detailed error messages that include:
- The connection method that failed
- The path to the socket that was attempted
- The underlying Redis error
## Testing
The module includes both unit tests and integration tests:
- Unit tests that mock Redis functionality
- Integration tests that require a real Redis server
- Tests automatically skip if Redis is not available
### Unit Tests
- Tests for the builder pattern and configuration
- Tests for connection URL building
- Tests for environment variable handling
### Integration Tests
- Tests for basic Redis operations (SET, GET, EXPIRE)
- Tests for hash operations (HSET, HGET, HGETALL, HDEL)
- Tests for list operations (RPUSH, LLEN, LRANGE, LPOP)
- Tests for error handling (invalid commands, wrong data types)
Run the tests with:
```bash
cargo test --lib redisclient::tests
```
## Thread Safety
The Redis client is wrapped in an `Arc<Mutex<>>` to ensure thread safety when accessing the global instance.

36
redisclient/src/lib.rs Normal file
View File

@@ -0,0 +1,36 @@
//! SAL Redis Client
//!
//! A robust Redis client wrapper for Rust applications that provides connection management,
//! automatic reconnection, and a simple interface for executing Redis commands.
//!
//! ## Features
//!
//! - **Connection Management**: Automatic connection handling with lazy initialization
//! - **Reconnection**: Automatic reconnection on connection failures
//! - **Builder Pattern**: Flexible configuration with authentication support
//! - **Environment Configuration**: Support for environment variables
//! - **Thread Safety**: Safe to use in multi-threaded applications
//! - **Rhai Integration**: Scripting support for Redis operations
//!
//! ## Usage
//!
//! ```rust
//! use sal_redisclient::{execute, get_redis_client};
//! use redis::cmd;
//!
//! // Execute a simple SET command
//! let mut set_cmd = redis::cmd("SET");
//! set_cmd.arg("my_key").arg("my_value");
//! let result: redis::RedisResult<()> = execute(&mut set_cmd);
//!
//! // Get the Redis client directly
//! let client = get_redis_client()?;
//! ```
mod redisclient;
pub use redisclient::*;
// Rhai integration module
#[cfg(feature = "rhai")]
pub mod rhai;

View File

@@ -0,0 +1,361 @@
use lazy_static::lazy_static;
use redis::{Client, Cmd, Connection, RedisError, RedisResult};
use std::env;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, Once};
/// Redis connection configuration builder
///
/// This struct is used to build a Redis connection configuration.
/// It follows the builder pattern to allow for flexible configuration.
#[derive(Clone)]
pub struct RedisConfigBuilder {
pub host: String,
pub port: u16,
pub db: i64,
pub username: Option<String>,
pub password: Option<String>,
pub use_tls: bool,
pub use_unix_socket: bool,
pub socket_path: Option<String>,
pub connection_timeout: Option<u64>,
}
impl Default for RedisConfigBuilder {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 6379,
db: 0,
username: None,
password: None,
use_tls: false,
use_unix_socket: false,
socket_path: None,
connection_timeout: None,
}
}
}
impl RedisConfigBuilder {
/// Create a new Redis connection configuration builder with default values
pub fn new() -> Self {
Self::default()
}
/// Set the host for the Redis connection
pub fn host(mut self, host: &str) -> Self {
self.host = host.to_string();
self
}
/// Set the port for the Redis connection
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
/// Set the database for the Redis connection
pub fn db(mut self, db: i64) -> Self {
self.db = db;
self
}
/// Set the username for the Redis connection (Redis 6.0+)
pub fn username(mut self, username: &str) -> Self {
self.username = Some(username.to_string());
self
}
/// Set the password for the Redis connection
pub fn password(mut self, password: &str) -> Self {
self.password = Some(password.to_string());
self
}
/// Enable TLS for the Redis connection
pub fn use_tls(mut self, use_tls: bool) -> Self {
self.use_tls = use_tls;
self
}
/// Use Unix socket for the Redis connection
pub fn use_unix_socket(mut self, use_unix_socket: bool) -> Self {
self.use_unix_socket = use_unix_socket;
self
}
/// Set the Unix socket path for the Redis connection
pub fn socket_path(mut self, socket_path: &str) -> Self {
self.socket_path = Some(socket_path.to_string());
self.use_unix_socket = true;
self
}
/// Set the connection timeout in seconds
pub fn connection_timeout(mut self, seconds: u64) -> Self {
self.connection_timeout = Some(seconds);
self
}
/// Build the connection URL from the configuration
pub fn build_connection_url(&self) -> String {
if self.use_unix_socket {
if let Some(ref socket_path) = self.socket_path {
return format!("unix://{}", socket_path);
} else {
// Default socket path
let home_dir = env::var("HOME").unwrap_or_else(|_| String::from("/root"));
return format!("unix://{}/hero/var/myredis.sock", home_dir);
}
}
let mut url = if self.use_tls {
format!("rediss://{}:{}", self.host, self.port)
} else {
format!("redis://{}:{}", self.host, self.port)
};
// Add authentication if provided
if let Some(ref username) = self.username {
if let Some(ref password) = self.password {
url = format!(
"redis://{}:{}@{}:{}",
username, password, self.host, self.port
);
} else {
url = format!("redis://{}@{}:{}", username, self.host, self.port);
}
} else if let Some(ref password) = self.password {
url = format!("redis://:{}@{}:{}", password, self.host, self.port);
}
// Add database
url = format!("{}/{}", url, self.db);
url
}
/// Build a Redis client from the configuration
pub fn build(&self) -> RedisResult<(Client, i64)> {
let url = self.build_connection_url();
let client = Client::open(url)?;
Ok((client, self.db))
}
}
// Global Redis client instance using lazy_static
lazy_static! {
static ref REDIS_CLIENT: Mutex<Option<Arc<RedisClientWrapper>>> = Mutex::new(None);
static ref INIT: Once = Once::new();
}
// Wrapper for Redis client to handle connection and DB selection
pub struct RedisClientWrapper {
client: Client,
connection: Mutex<Option<Connection>>,
db: i64,
initialized: AtomicBool,
}
impl RedisClientWrapper {
// Create a new Redis client wrapper
fn new(client: Client, db: i64) -> Self {
RedisClientWrapper {
client,
connection: Mutex::new(None),
db,
initialized: AtomicBool::new(false),
}
}
// Execute a command on the Redis connection
pub fn execute<T: redis::FromRedisValue>(&self, cmd: &mut Cmd) -> RedisResult<T> {
let mut conn_guard = self.connection.lock().unwrap();
// If we don't have a connection or it's not working, create a new one
if conn_guard.is_none() || {
if let Some(ref mut conn) = *conn_guard {
let ping_result: RedisResult<String> = redis::cmd("PING").query(conn);
ping_result.is_err()
} else {
true
}
} {
*conn_guard = Some(self.client.get_connection()?);
}
cmd.query(&mut conn_guard.as_mut().unwrap())
}
// Initialize the client (ping and select DB)
fn initialize(&self) -> RedisResult<()> {
if self.initialized.load(Ordering::Relaxed) {
return Ok(());
}
let mut conn = self.client.get_connection()?;
// Ping Redis to ensure it works
let ping_result: String = redis::cmd("PING").query(&mut conn)?;
if ping_result != "PONG" {
return Err(RedisError::from((
redis::ErrorKind::ResponseError,
"Failed to ping Redis server",
)));
}
// Select the database
let _ = redis::cmd("SELECT").arg(self.db).exec(&mut conn);
self.initialized.store(true, Ordering::Relaxed);
// Store the connection
let mut conn_guard = self.connection.lock().unwrap();
*conn_guard = Some(conn);
Ok(())
}
}
// Get the Redis client instance
pub fn get_redis_client() -> RedisResult<Arc<RedisClientWrapper>> {
// Check if we already have a client
{
let guard = REDIS_CLIENT.lock().unwrap();
if let Some(ref client) = &*guard {
return Ok(Arc::clone(client));
}
}
// Create a new client
let client = create_redis_client()?;
// Store the client globally
{
let mut guard = REDIS_CLIENT.lock().unwrap();
*guard = Some(Arc::clone(&client));
}
Ok(client)
}
// Create a new Redis client
fn create_redis_client() -> RedisResult<Arc<RedisClientWrapper>> {
// Get Redis configuration from environment variables
let db = get_redis_db();
let password = env::var("REDIS_PASSWORD").ok();
let username = env::var("REDIS_USERNAME").ok();
let host = env::var("REDIS_HOST").unwrap_or_else(|_| String::from("127.0.0.1"));
let port = env::var("REDIS_PORT")
.ok()
.and_then(|p| p.parse::<u16>().ok())
.unwrap_or(6379);
// Create a builder with environment variables
let mut builder = RedisConfigBuilder::new().host(&host).port(port).db(db);
if let Some(user) = username {
builder = builder.username(&user);
}
if let Some(pass) = password {
builder = builder.password(&pass);
}
// First try: Connect via Unix socket if it exists
let home_dir = env::var("HOME").unwrap_or_else(|_| String::from("/root"));
let socket_path = format!("{}/hero/var/myredis.sock", home_dir);
if Path::new(&socket_path).exists() {
// Try to connect via Unix socket
let socket_builder = builder.clone().socket_path(&socket_path);
match socket_builder.build() {
Ok((client, db)) => {
let wrapper = Arc::new(RedisClientWrapper::new(client, db));
// Initialize the client
if let Err(err) = wrapper.initialize() {
eprintln!(
"Socket exists at {} but connection failed: {}",
socket_path, err
);
} else {
return Ok(wrapper);
}
}
Err(err) => {
eprintln!(
"Socket exists at {} but connection failed: {}",
socket_path, err
);
}
}
}
// Second try: Connect via TCP
match builder.clone().build() {
Ok((client, db)) => {
let wrapper = Arc::new(RedisClientWrapper::new(client, db));
// Initialize the client
wrapper.initialize()?;
Ok(wrapper)
}
Err(err) => Err(RedisError::from((
redis::ErrorKind::IoError,
"Failed to connect to Redis",
format!(
"Could not connect via socket at {} or via TCP to {}:{}: {}",
socket_path, host, port, err
),
))),
}
}
// Get the Redis DB number from environment variable
fn get_redis_db() -> i64 {
env::var("REDISDB")
.ok()
.and_then(|db_str| db_str.parse::<i64>().ok())
.unwrap_or(0)
}
// Reload the Redis client
pub fn reset() -> RedisResult<()> {
// Clear the existing client
{
let mut client_guard = REDIS_CLIENT.lock().unwrap();
*client_guard = None;
}
// Create a new client, only return error if it fails
// We don't need to return the client itself
get_redis_client()?;
Ok(())
}
// Execute a Redis command
pub fn execute<T>(cmd: &mut Cmd) -> RedisResult<T>
where
T: redis::FromRedisValue,
{
let client = get_redis_client()?;
client.execute(cmd)
}
/// Create a new Redis client with custom configuration
///
/// # Arguments
///
/// * `config` - The Redis connection configuration builder
///
/// # Returns
///
/// * `RedisResult<Client>` - The Redis client if successful, error otherwise
pub fn with_config(config: RedisConfigBuilder) -> RedisResult<Client> {
let (client, _) = config.build()?;
Ok(client)
}

323
redisclient/src/rhai.rs Normal file
View File

@@ -0,0 +1,323 @@
//! Rhai wrappers for Redis client module functions
//!
//! This module provides Rhai wrappers for the functions in the Redis client module.
use crate::redisclient;
use rhai::{Engine, EvalAltResult, Map};
use std::collections::HashMap;
/// Register Redis client module functions with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the functions with
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_redisclient_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register basic Redis operations
engine.register_fn("redis_ping", redis_ping);
engine.register_fn("redis_set", redis_set);
engine.register_fn("redis_get", redis_get);
engine.register_fn("redis_del", redis_del);
// Register hash operations
engine.register_fn("redis_hset", redis_hset);
engine.register_fn("redis_hget", redis_hget);
engine.register_fn("redis_hgetall", redis_hgetall);
engine.register_fn("redis_hdel", redis_hdel);
// Register list operations
engine.register_fn("redis_rpush", redis_rpush);
engine.register_fn("redis_lpush", redis_lpush);
engine.register_fn("redis_llen", redis_llen);
engine.register_fn("redis_lrange", redis_lrange);
// Register other operations
engine.register_fn("redis_reset", redis_reset);
Ok(())
}
/// Ping the Redis server
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - "PONG" if successful, error otherwise
pub fn redis_ping() -> Result<String, Box<EvalAltResult>> {
let mut cmd = redis::cmd("PING");
redisclient::execute(&mut cmd).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))
})
}
/// Set a key-value pair in Redis
///
/// # Arguments
///
/// * `key` - The key to set
/// * `value` - The value to set
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise
pub fn redis_set(key: &str, value: &str) -> Result<bool, Box<EvalAltResult>> {
let mut cmd = redis::cmd("SET");
cmd.arg(key).arg(value);
let result: redis::RedisResult<String> = redisclient::execute(&mut cmd);
match result {
Ok(s) if s == "OK" => Ok(true),
Ok(_) => Ok(false),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Get a value from Redis by key
///
/// # Arguments
///
/// * `key` - The key to get
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - The value if found, empty string if not found, error otherwise
pub fn redis_get(key: &str) -> Result<String, Box<EvalAltResult>> {
let mut cmd = redis::cmd("GET");
cmd.arg(key);
let result: redis::RedisResult<Option<String>> = redisclient::execute(&mut cmd);
match result {
Ok(Some(value)) => Ok(value),
Ok(None) => Ok(String::new()),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Delete a key from Redis
///
/// # Arguments
///
/// * `key` - The key to delete
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise
pub fn redis_del(key: &str) -> Result<bool, Box<EvalAltResult>> {
let mut cmd = redis::cmd("DEL");
cmd.arg(key);
let result: redis::RedisResult<i64> = redisclient::execute(&mut cmd);
match result {
Ok(n) => Ok(n > 0),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Set a field in a hash
///
/// # Arguments
///
/// * `key` - The hash key
/// * `field` - The field to set
/// * `value` - The value to set
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise
pub fn redis_hset(key: &str, field: &str, value: &str) -> Result<bool, Box<EvalAltResult>> {
let mut cmd = redis::cmd("HSET");
cmd.arg(key).arg(field).arg(value);
let result: redis::RedisResult<i64> = redisclient::execute(&mut cmd);
match result {
Ok(_) => Ok(true),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Get a field from a hash
///
/// # Arguments
///
/// * `key` - The hash key
/// * `field` - The field to get
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - The value if found, empty string if not found, error otherwise
pub fn redis_hget(key: &str, field: &str) -> Result<String, Box<EvalAltResult>> {
let mut cmd = redis::cmd("HGET");
cmd.arg(key).arg(field);
let result: redis::RedisResult<Option<String>> = redisclient::execute(&mut cmd);
match result {
Ok(Some(value)) => Ok(value),
Ok(None) => Ok(String::new()),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Get all fields and values from a hash
///
/// # Arguments
///
/// * `key` - The hash key
///
/// # Returns
///
/// * `Result<Map, Box<EvalAltResult>>` - A map of field-value pairs, error otherwise
pub fn redis_hgetall(key: &str) -> Result<Map, Box<EvalAltResult>> {
let mut cmd = redis::cmd("HGETALL");
cmd.arg(key);
let result: redis::RedisResult<HashMap<String, String>> = redisclient::execute(&mut cmd);
match result {
Ok(hash_map) => {
let mut map = Map::new();
for (k, v) in hash_map {
map.insert(k.into(), v.into());
}
Ok(map)
}
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Delete a field from a hash
///
/// # Arguments
///
/// * `key` - The hash key
/// * `field` - The field to delete
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise
pub fn redis_hdel(key: &str, field: &str) -> Result<bool, Box<EvalAltResult>> {
let mut cmd = redis::cmd("HDEL");
cmd.arg(key).arg(field);
let result: redis::RedisResult<i64> = redisclient::execute(&mut cmd);
match result {
Ok(n) => Ok(n > 0),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Push an element to the end of a list
///
/// # Arguments
///
/// * `key` - The list key
/// * `value` - The value to push
///
/// # Returns
///
/// * `Result<i64, Box<EvalAltResult>>` - The new length of the list, error otherwise
pub fn redis_rpush(key: &str, value: &str) -> Result<i64, Box<EvalAltResult>> {
let mut cmd = redis::cmd("RPUSH");
cmd.arg(key).arg(value);
redisclient::execute(&mut cmd).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))
})
}
/// Push an element to the beginning of a list
///
/// # Arguments
///
/// * `key` - The list key
/// * `value` - The value to push
///
/// # Returns
///
/// * `Result<i64, Box<EvalAltResult>>` - The new length of the list, error otherwise
pub fn redis_lpush(key: &str, value: &str) -> Result<i64, Box<EvalAltResult>> {
let mut cmd = redis::cmd("LPUSH");
cmd.arg(key).arg(value);
redisclient::execute(&mut cmd).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))
})
}
/// Get the length of a list
///
/// # Arguments
///
/// * `key` - The list key
///
/// # Returns
///
/// * `Result<i64, Box<EvalAltResult>>` - The length of the list, error otherwise
pub fn redis_llen(key: &str) -> Result<i64, Box<EvalAltResult>> {
let mut cmd = redis::cmd("LLEN");
cmd.arg(key);
redisclient::execute(&mut cmd).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))
})
}
/// Get a range of elements from a list
///
/// # Arguments
///
/// * `key` - The list key
/// * `start` - The start index
/// * `stop` - The stop index
///
/// # Returns
///
/// * `Result<Vec<String>, Box<EvalAltResult>>` - The elements in the range, error otherwise
pub fn redis_lrange(key: &str, start: i64, stop: i64) -> Result<Vec<String>, Box<EvalAltResult>> {
let mut cmd = redis::cmd("LRANGE");
cmd.arg(key).arg(start).arg(stop);
redisclient::execute(&mut cmd).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))
})
}
/// Reset the Redis client connection
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise
pub fn redis_reset() -> Result<bool, Box<EvalAltResult>> {
match redisclient::reset() {
Ok(_) => Ok(true),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}

View File

@@ -0,0 +1,384 @@
use redis::RedisResult;
use sal_redisclient::*;
use std::env;
#[cfg(test)]
mod redis_client_tests {
use super::*;
#[test]
fn test_env_vars() {
// Save original REDISDB value to restore later
let original_redisdb = env::var("REDISDB").ok();
// Set test environment variables
env::set_var("REDISDB", "5");
// Test with invalid value
env::set_var("REDISDB", "invalid");
// Test with unset value
env::remove_var("REDISDB");
// Restore original REDISDB value
if let Some(redisdb) = original_redisdb {
env::set_var("REDISDB", redisdb);
} else {
env::remove_var("REDISDB");
}
}
#[test]
fn test_redis_config_environment_variables() {
// Test that environment variables are properly handled
let original_home = env::var("HOME").ok();
let original_redis_host = env::var("REDIS_HOST").ok();
let original_redis_port = env::var("REDIS_PORT").ok();
// Set test environment variables
env::set_var("HOME", "/tmp/test");
env::set_var("REDIS_HOST", "test.redis.com");
env::set_var("REDIS_PORT", "6380");
// Test that the configuration builder respects environment variables
let config = RedisConfigBuilder::new()
.host(&env::var("REDIS_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()))
.port(
env::var("REDIS_PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(6379),
);
assert_eq!(config.host, "test.redis.com");
assert_eq!(config.port, 6380);
// Restore original environment variables
if let Some(home) = original_home {
env::set_var("HOME", home);
} else {
env::remove_var("HOME");
}
if let Some(host) = original_redis_host {
env::set_var("REDIS_HOST", host);
} else {
env::remove_var("REDIS_HOST");
}
if let Some(port) = original_redis_port {
env::set_var("REDIS_PORT", port);
} else {
env::remove_var("REDIS_PORT");
}
}
#[test]
fn test_redis_config_validation() {
// Test configuration validation and edge cases
// Test invalid port handling
let config = RedisConfigBuilder::new().port(0);
assert_eq!(config.port, 0); // Should accept any port value
// Test empty strings
let config = RedisConfigBuilder::new().host("").username("").password("");
assert_eq!(config.host, "");
assert_eq!(config.username, Some("".to_string()));
assert_eq!(config.password, Some("".to_string()));
// Test chaining methods
let config = RedisConfigBuilder::new()
.host("localhost")
.port(6379)
.db(1)
.use_tls(true)
.connection_timeout(30);
assert_eq!(config.host, "localhost");
assert_eq!(config.port, 6379);
assert_eq!(config.db, 1);
assert_eq!(config.use_tls, true);
assert_eq!(config.connection_timeout, Some(30));
}
#[test]
fn test_redis_config_builder() {
// Test the Redis configuration builder
// Test default values
let config = RedisConfigBuilder::new();
assert_eq!(config.host, "127.0.0.1");
assert_eq!(config.port, 6379);
assert_eq!(config.db, 0);
assert_eq!(config.username, None);
assert_eq!(config.password, None);
assert_eq!(config.use_tls, false);
assert_eq!(config.use_unix_socket, false);
assert_eq!(config.socket_path, None);
assert_eq!(config.connection_timeout, None);
// Test setting values
let config = RedisConfigBuilder::new()
.host("redis.example.com")
.port(6380)
.db(1)
.username("user")
.password("pass")
.use_tls(true)
.connection_timeout(30);
assert_eq!(config.host, "redis.example.com");
assert_eq!(config.port, 6380);
assert_eq!(config.db, 1);
assert_eq!(config.username, Some("user".to_string()));
assert_eq!(config.password, Some("pass".to_string()));
assert_eq!(config.use_tls, true);
assert_eq!(config.connection_timeout, Some(30));
// Test socket path setting
let config = RedisConfigBuilder::new().socket_path("/tmp/redis.sock");
assert_eq!(config.use_unix_socket, true);
assert_eq!(config.socket_path, Some("/tmp/redis.sock".to_string()));
}
#[test]
fn test_connection_url_building() {
// Test building connection URLs
// Test default URL
let config = RedisConfigBuilder::new();
let url = config.build_connection_url();
assert_eq!(url, "redis://127.0.0.1:6379/0");
// Test with authentication
let config = RedisConfigBuilder::new().username("user").password("pass");
let url = config.build_connection_url();
assert_eq!(url, "redis://user:pass@127.0.0.1:6379/0");
// Test with password only
let config = RedisConfigBuilder::new().password("pass");
let url = config.build_connection_url();
assert_eq!(url, "redis://:pass@127.0.0.1:6379/0");
// Test with TLS
let config = RedisConfigBuilder::new().use_tls(true);
let url = config.build_connection_url();
assert_eq!(url, "rediss://127.0.0.1:6379/0");
// Test with Unix socket
let config = RedisConfigBuilder::new().socket_path("/tmp/redis.sock");
let url = config.build_connection_url();
assert_eq!(url, "unix:///tmp/redis.sock");
}
}
// Integration tests that require a real Redis server
// These tests will be skipped if Redis is not available
#[cfg(test)]
mod redis_integration_tests {
use super::*;
// Helper function to check if Redis is available
fn is_redis_available() -> bool {
match get_redis_client() {
Ok(_) => true,
Err(_) => false,
}
}
#[test]
fn test_redis_client_integration() {
if !is_redis_available() {
println!("Skipping Redis integration tests - Redis server not available");
return;
}
println!("Running Redis integration tests...");
// Test basic operations
test_basic_redis_operations();
// Test more complex operations
test_hash_operations();
test_list_operations();
// Test error handling
test_error_handling();
}
fn test_basic_redis_operations() {
if !is_redis_available() {
return;
}
// Test setting and getting values
let client_result = get_redis_client();
if client_result.is_err() {
// Skip the test if we can't connect to Redis
return;
}
// Create SET command
let mut set_cmd = redis::cmd("SET");
set_cmd.arg("test_key").arg("test_value");
// Execute SET command
let set_result: RedisResult<()> = execute(&mut set_cmd);
assert!(set_result.is_ok());
// Create GET command
let mut get_cmd = redis::cmd("GET");
get_cmd.arg("test_key");
// Execute GET command and check the result
if let Ok(value) = execute::<String>(&mut get_cmd) {
assert_eq!(value, "test_value");
}
// Test expiration
let mut expire_cmd = redis::cmd("EXPIRE");
expire_cmd.arg("test_key").arg(1); // Expire in 1 second
let expire_result: RedisResult<i32> = execute(&mut expire_cmd);
assert!(expire_result.is_ok());
assert_eq!(expire_result.unwrap(), 1);
// Sleep for 2 seconds to let the key expire
std::thread::sleep(std::time::Duration::from_secs(2));
// Check that the key has expired
let mut exists_cmd = redis::cmd("EXISTS");
exists_cmd.arg("test_key");
let exists_result: RedisResult<i32> = execute(&mut exists_cmd);
assert!(exists_result.is_ok());
assert_eq!(exists_result.unwrap(), 0);
// Clean up
let _: RedisResult<()> = execute(&mut redis::cmd("DEL").arg("test_key"));
}
fn test_hash_operations() {
if !is_redis_available() {
return;
}
// Test hash operations
let hash_key = "test_hash";
// Set hash fields
let mut hset_cmd = redis::cmd("HSET");
hset_cmd
.arg(hash_key)
.arg("field1")
.arg("value1")
.arg("field2")
.arg("value2");
let hset_result: RedisResult<i32> = execute(&mut hset_cmd);
assert!(hset_result.is_ok());
assert_eq!(hset_result.unwrap(), 2);
// Get hash field
let mut hget_cmd = redis::cmd("HGET");
hget_cmd.arg(hash_key).arg("field1");
let hget_result: RedisResult<String> = execute(&mut hget_cmd);
assert!(hget_result.is_ok());
assert_eq!(hget_result.unwrap(), "value1");
// Get all hash fields
let mut hgetall_cmd = redis::cmd("HGETALL");
hgetall_cmd.arg(hash_key);
let hgetall_result: RedisResult<Vec<String>> = execute(&mut hgetall_cmd);
assert!(hgetall_result.is_ok());
let hgetall_values = hgetall_result.unwrap();
assert_eq!(hgetall_values.len(), 4); // field1, value1, field2, value2
// Delete hash field
let mut hdel_cmd = redis::cmd("HDEL");
hdel_cmd.arg(hash_key).arg("field1");
let hdel_result: RedisResult<i32> = execute(&mut hdel_cmd);
assert!(hdel_result.is_ok());
assert_eq!(hdel_result.unwrap(), 1);
// Clean up
let _: RedisResult<()> = execute(&mut redis::cmd("DEL").arg(hash_key));
}
fn test_list_operations() {
if !is_redis_available() {
return;
}
// Test list operations
let list_key = "test_list";
// Push items to list
let mut rpush_cmd = redis::cmd("RPUSH");
rpush_cmd
.arg(list_key)
.arg("item1")
.arg("item2")
.arg("item3");
let rpush_result: RedisResult<i32> = execute(&mut rpush_cmd);
assert!(rpush_result.is_ok());
assert_eq!(rpush_result.unwrap(), 3);
// Get list length
let mut llen_cmd = redis::cmd("LLEN");
llen_cmd.arg(list_key);
let llen_result: RedisResult<i32> = execute(&mut llen_cmd);
assert!(llen_result.is_ok());
assert_eq!(llen_result.unwrap(), 3);
// Get list range
let mut lrange_cmd = redis::cmd("LRANGE");
lrange_cmd.arg(list_key).arg(0).arg(-1);
let lrange_result: RedisResult<Vec<String>> = execute(&mut lrange_cmd);
assert!(lrange_result.is_ok());
let lrange_values = lrange_result.unwrap();
assert_eq!(lrange_values.len(), 3);
assert_eq!(lrange_values[0], "item1");
assert_eq!(lrange_values[1], "item2");
assert_eq!(lrange_values[2], "item3");
// Pop item from list
let mut lpop_cmd = redis::cmd("LPOP");
lpop_cmd.arg(list_key);
let lpop_result: RedisResult<String> = execute(&mut lpop_cmd);
assert!(lpop_result.is_ok());
assert_eq!(lpop_result.unwrap(), "item1");
// Clean up
let _: RedisResult<()> = execute(&mut redis::cmd("DEL").arg(list_key));
}
fn test_error_handling() {
if !is_redis_available() {
return;
}
// Test error handling
// Test invalid command
let mut invalid_cmd = redis::cmd("INVALID_COMMAND");
let invalid_result: RedisResult<()> = execute(&mut invalid_cmd);
assert!(invalid_result.is_err());
// Test wrong data type
let key = "test_wrong_type";
// Set a string value
let mut set_cmd = redis::cmd("SET");
set_cmd.arg(key).arg("string_value");
let set_result: RedisResult<()> = execute(&mut set_cmd);
assert!(set_result.is_ok());
// Try to use a hash command on a string
let mut hget_cmd = redis::cmd("HGET");
hget_cmd.arg(key).arg("field");
let hget_result: RedisResult<String> = execute(&mut hget_cmd);
assert!(hget_result.is_err());
// Clean up
let _: RedisResult<()> = execute(&mut redis::cmd("DEL").arg(key));
}
}

View File

@@ -0,0 +1,68 @@
// 01_redis_connection.rhai
// Tests for Redis client connection and basic operations
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Helper function to check if Redis is available
fn is_redis_available() {
try {
// Try to execute a simple PING command
let ping_result = redis_ping();
return ping_result == "PONG";
} catch(err) {
print(`Redis connection error: ${err}`);
return false;
}
}
print("=== Testing Redis Client Connection ===");
// Check if Redis is available
let redis_available = is_redis_available();
if !redis_available {
print("Redis server is not available. Skipping Redis tests.");
// Exit gracefully without error
return;
}
print("✓ Redis server is available");
// Test redis_ping function
print("Testing redis_ping()...");
let ping_result = redis_ping();
assert_true(ping_result == "PONG", "PING should return PONG");
print(`✓ redis_ping(): Returned ${ping_result}`);
// Test redis_set and redis_get functions
print("Testing redis_set() and redis_get()...");
let test_key = "rhai_test_key";
let test_value = "Hello from Rhai test";
// Set a value
let set_result = redis_set(test_key, test_value);
assert_true(set_result, "SET operation should succeed");
print(`✓ redis_set(): Successfully set key ${test_key}`);
// Get the value back
let get_result = redis_get(test_key);
assert_true(get_result == test_value, "GET should return the value we set");
print(`✓ redis_get(): Successfully retrieved value for key ${test_key}`);
// Test redis_del function
print("Testing redis_del()...");
let del_result = redis_del(test_key);
assert_true(del_result, "DEL operation should succeed");
print(`✓ redis_del(): Successfully deleted key ${test_key}`);
// Verify the key was deleted
let get_after_del = redis_get(test_key);
assert_true(get_after_del == "", "Key should not exist after deletion");
print("✓ Key was successfully deleted");
print("All Redis connection tests completed successfully!");

View File

@@ -0,0 +1,109 @@
// 02_redis_operations.rhai
// Tests for advanced Redis operations
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Helper function to check if Redis is available
fn is_redis_available() {
try {
// Try to execute a simple PING command
let ping_result = redis_ping();
return ping_result == "PONG";
} catch(err) {
print(`Redis connection error: ${err}`);
return false;
}
}
print("=== Testing Advanced Redis Operations ===");
// Check if Redis is available
let redis_available = is_redis_available();
if !redis_available {
print("Redis server is not available. Skipping Redis tests.");
// Exit gracefully without error
return;
}
print("✓ Redis server is available");
// Test prefix for all keys to avoid conflicts
let prefix = "rhai_test_";
// Test redis_hset and redis_hget functions
print("Testing redis_hset() and redis_hget()...");
let hash_key = prefix + "hash";
let field1 = "field1";
let value1 = "value1";
let field2 = "field2";
let value2 = "value2";
// Set hash fields
let hset_result1 = redis_hset(hash_key, field1, value1);
assert_true(hset_result1, "HSET operation should succeed for field1");
let hset_result2 = redis_hset(hash_key, field2, value2);
assert_true(hset_result2, "HSET operation should succeed for field2");
print(`✓ redis_hset(): Successfully set fields in hash ${hash_key}`);
// Get hash fields
let hget_result1 = redis_hget(hash_key, field1);
assert_true(hget_result1 == value1, "HGET should return the value we set for field1");
let hget_result2 = redis_hget(hash_key, field2);
assert_true(hget_result2 == value2, "HGET should return the value we set for field2");
print(`✓ redis_hget(): Successfully retrieved values from hash ${hash_key}`);
// Test redis_hgetall function
print("Testing redis_hgetall()...");
let hgetall_result = redis_hgetall(hash_key);
assert_true(hgetall_result.len() == 2, "HGETALL should return 2 fields");
assert_true(hgetall_result[field1] == value1, "HGETALL should include field1 with correct value");
assert_true(hgetall_result[field2] == value2, "HGETALL should include field2 with correct value");
print(`✓ redis_hgetall(): Successfully retrieved all fields from hash ${hash_key}`);
// Test redis_hdel function
print("Testing redis_hdel()...");
let hdel_result = redis_hdel(hash_key, field1);
assert_true(hdel_result, "HDEL operation should succeed");
print(`✓ redis_hdel(): Successfully deleted field from hash ${hash_key}`);
// Verify the field was deleted
let hget_after_del = redis_hget(hash_key, field1);
assert_true(hget_after_del == "", "Field should not exist after deletion");
print("✓ Field was successfully deleted from hash");
// Test redis_list operations
print("Testing redis list operations...");
let list_key = prefix + "list";
// Push items to list
let rpush_result = redis_rpush(list_key, "item1");
assert_true(rpush_result > 0, "RPUSH operation should succeed");
redis_rpush(list_key, "item2");
redis_rpush(list_key, "item3");
print(`✓ redis_rpush(): Successfully pushed items to list ${list_key}`);
// Get list length
let llen_result = redis_llen(list_key);
assert_true(llen_result == 3, "List should have 3 items");
print(`✓ redis_llen(): List has ${llen_result} items`);
// Get list range
let lrange_result = redis_lrange(list_key, 0, -1);
assert_true(lrange_result.len() == 3, "LRANGE should return 3 items");
assert_true(lrange_result[0] == "item1", "First item should be 'item1'");
assert_true(lrange_result[2] == "item3", "Last item should be 'item3'");
print(`✓ redis_lrange(): Successfully retrieved all items from list ${list_key}`);
// Clean up
print("Cleaning up...");
redis_del(hash_key);
redis_del(list_key);
print("✓ Cleanup: All test keys removed");
print("All Redis operations tests completed successfully!");

View File

@@ -0,0 +1,59 @@
// 03_redis_authentication.rhai
// Tests for Redis client authentication (placeholder for future implementation)
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Helper function to check if Redis is available
fn is_redis_available() {
try {
// Try to execute a simple ping
let ping_result = redis_ping();
return ping_result == "PONG";
} catch(err) {
print(`Redis connection error: ${err}`);
return false;
}
}
print("=== Testing Redis Client Authentication ===");
// Check if Redis is available
let redis_available = is_redis_available();
if !redis_available {
print("Redis server is not available. Skipping Redis authentication tests.");
// Exit gracefully without error
return;
}
print("✓ Redis server is available");
print("Authentication support will be implemented in a future update.");
print("The backend implementation is ready, but the Rhai bindings are still in development.");
// For now, just test basic Redis functionality
print("\nTesting basic Redis functionality...");
// Test a simple operation
let test_key = "auth_test_key";
let test_value = "auth_test_value";
let set_result = redis_set(test_key, test_value);
assert_true(set_result, "Should be able to set a key");
print("✓ Set key");
let get_result = redis_get(test_key);
assert_true(get_result == test_value, "Should be able to get the key");
print("✓ Got key");
// Clean up
let del_result = redis_del(test_key);
assert_true(del_result, "Should be able to delete the key");
print("✓ Deleted test key");
print("All Redis tests completed successfully!");

View File

@@ -0,0 +1,154 @@
// run_all_tests.rhai
// Runs all Redis client module tests
print("=== Running Redis Client Module Tests ===");
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Helper function to check if Redis is available
fn is_redis_available() {
try {
// Try to execute a simple PING command
let ping_result = redis_ping();
return ping_result == "PONG";
} catch(err) {
print(`Redis connection error: ${err}`);
return false;
}
}
// Run each test directly
let passed = 0;
let failed = 0;
let skipped = 0;
// Check if Redis is available
let redis_available = is_redis_available();
if !redis_available {
print("Redis server is not available. Skipping all Redis tests.");
skipped = 3; // Skip all three tests
} else {
// Test 1: Redis Connection
print("\n--- Running Redis Connection Tests ---");
try {
// Test redis_ping function
print("Testing redis_ping()...");
let ping_result = redis_ping();
assert_true(ping_result == "PONG", "PING should return PONG");
print(`✓ redis_ping(): Returned ${ping_result}`);
// Test redis_set and redis_get functions
print("Testing redis_set() and redis_get()...");
let test_key = "rhai_test_key";
let test_value = "Hello from Rhai test";
// Set a value
let set_result = redis_set(test_key, test_value);
assert_true(set_result, "SET operation should succeed");
print(`✓ redis_set(): Successfully set key ${test_key}`);
// Get the value back
let get_result = redis_get(test_key);
assert_true(get_result == test_value, "GET should return the value we set");
print(`✓ redis_get(): Successfully retrieved value for key ${test_key}`);
// Clean up
redis_del(test_key);
print("--- Redis Connection Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Redis Connection Tests: ${err}`);
failed += 1;
}
// Test 2: Redis Operations
print("\n--- Running Redis Operations Tests ---");
try {
// Test prefix for all keys to avoid conflicts
let prefix = "rhai_test_";
// Test redis_hset and redis_hget functions
print("Testing redis_hset() and redis_hget()...");
let hash_key = prefix + "hash";
let field = "field1";
let value = "value1";
// Set hash field
let hset_result = redis_hset(hash_key, field, value);
assert_true(hset_result, "HSET operation should succeed");
print(`✓ redis_hset(): Successfully set field in hash ${hash_key}`);
// Get hash field
let hget_result = redis_hget(hash_key, field);
assert_true(hget_result == value, "HGET should return the value we set");
print(`✓ redis_hget(): Successfully retrieved value from hash ${hash_key}`);
// Clean up
redis_del(hash_key);
print("--- Redis Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Redis Operations Tests: ${err}`);
failed += 1;
}
// Test 3: Redis Authentication
print("\n--- Running Redis Authentication Tests ---");
try {
print("Authentication support will be implemented in a future update.");
print("The backend implementation is ready, but the Rhai bindings are still in development.");
// For now, just test basic Redis functionality
print("\nTesting basic Redis functionality...");
// Test a simple operation
let test_key = "auth_test_key";
let test_value = "auth_test_value";
let set_result = redis_set(test_key, test_value);
assert_true(set_result, "Should be able to set a key");
print("✓ Set key");
let get_result = redis_get(test_key);
assert_true(get_result == test_value, "Should be able to get the key");
print("✓ Got key");
// Clean up
let del_result = redis_del(test_key);
assert_true(del_result, "Should be able to delete the key");
print("✓ Deleted test key");
print("--- Redis Authentication Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Redis Authentication Tests: ${err}`);
failed += 1;
}
}
print("\n=== Test Summary ===");
print(`Passed: ${passed}`);
print(`Failed: ${failed}`);
print(`Skipped: ${skipped}`);
print(`Total: ${passed + failed + skipped}`);
if failed == 0 {
if skipped > 0 {
print("\n⚠ All tests skipped or passed!");
} else {
print("\n✅ All tests passed!");
}
} else {
print("\n❌ Some tests failed!");
}
// Return the number of failed tests (0 means success)
failed;

View File

@@ -0,0 +1,200 @@
use rhai::{Engine, EvalAltResult};
use sal_redisclient::rhai::*;
#[cfg(test)]
mod rhai_integration_tests {
use super::*;
fn create_test_engine() -> Engine {
let mut engine = Engine::new();
register_redisclient_module(&mut engine).expect("Failed to register redisclient module");
engine
}
#[test]
fn test_rhai_module_registration() {
let engine = create_test_engine();
// Test that the functions are registered
let script = r#"
// Just test that the functions exist and can be called
// We don't test actual Redis operations here since they require a server
true
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_rhai_redis_functions_exist() {
let engine = create_test_engine();
// Test that all expected functions are registered by attempting to call them
// We expect them to either succeed or fail with Redis connection errors,
// but NOT with "function not found" errors
let function_tests = [
("redis_ping()", "redis_ping"),
("redis_set(\"test\", \"value\")", "redis_set"),
("redis_get(\"test\")", "redis_get"),
("redis_del(\"test\")", "redis_del"),
("redis_hset(\"hash\", \"field\", \"value\")", "redis_hset"),
("redis_hget(\"hash\", \"field\")", "redis_hget"),
("redis_hgetall(\"hash\")", "redis_hgetall"),
("redis_hdel(\"hash\", \"field\")", "redis_hdel"),
("redis_rpush(\"list\", \"value\")", "redis_rpush"),
("redis_llen(\"list\")", "redis_llen"),
("redis_lrange(\"list\", 0, -1)", "redis_lrange"),
("redis_reset()", "redis_reset"),
];
for (script, func_name) in &function_tests {
let result = engine.eval::<rhai::Dynamic>(script);
// The function should be registered - if not, we'd get "Function not found"
// If Redis is not available, we might get connection errors, which is fine
if let Err(err) = result {
let error_msg = err.to_string();
assert!(
!error_msg.contains("Function not found")
&& !error_msg.contains("Variable not found"),
"Function {} should be registered but got: {}",
func_name,
error_msg
);
}
// If it succeeds, that's even better - the function is registered and working
}
}
#[test]
fn test_rhai_function_signatures() {
let engine = create_test_engine();
// Test function signatures by calling them with mock/invalid data
// This verifies they're properly registered and have correct parameter counts
// Test functions that should fail gracefully with invalid Redis connection
let test_cases = vec![
(
"redis_set(\"test\", \"value\")",
"redis_set should accept 2 string parameters",
),
(
"redis_get(\"test\")",
"redis_get should accept 1 string parameter",
),
(
"redis_del(\"test\")",
"redis_del should accept 1 string parameter",
),
(
"redis_hset(\"hash\", \"field\", \"value\")",
"redis_hset should accept 3 string parameters",
),
(
"redis_hget(\"hash\", \"field\")",
"redis_hget should accept 2 string parameters",
),
(
"redis_hgetall(\"hash\")",
"redis_hgetall should accept 1 string parameter",
),
(
"redis_hdel(\"hash\", \"field\")",
"redis_hdel should accept 2 string parameters",
),
(
"redis_rpush(\"list\", \"value\")",
"redis_rpush should accept 2 string parameters",
),
(
"redis_llen(\"list\")",
"redis_llen should accept 1 string parameter",
),
(
"redis_lrange(\"list\", 0, -1)",
"redis_lrange should accept string and 2 integers",
),
];
for (script, description) in test_cases {
let result = engine.eval::<rhai::Dynamic>(script);
// We expect these to either succeed (if Redis is available) or fail with Redis connection error
// But they should NOT fail with "function not found" or "wrong number of parameters"
if let Err(err) = result {
let error_msg = err.to_string();
assert!(
!error_msg.contains("Function not found")
&& !error_msg.contains("wrong number of arguments")
&& !error_msg.contains("expects")
&& !error_msg.contains("parameters"),
"{}: Got parameter error: {}",
description,
error_msg
);
}
}
}
// Helper function to check if Redis is available for integration tests
fn is_redis_available() -> bool {
match sal_redisclient::get_redis_client() {
Ok(_) => true,
Err(_) => false,
}
}
#[test]
fn test_rhai_redis_ping_integration() {
if !is_redis_available() {
println!("Skipping Redis integration test - Redis server not available");
return;
}
let engine = create_test_engine();
let script = r#"
let result = redis_ping();
result == "PONG"
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
if result.is_ok() {
assert_eq!(result.unwrap(), true);
} else {
println!("Redis ping test failed: {:?}", result.err());
}
}
#[test]
fn test_rhai_redis_set_get_integration() {
if !is_redis_available() {
println!("Skipping Redis integration test - Redis server not available");
return;
}
let engine = create_test_engine();
let script = r#"
// Set a test value
redis_set("rhai_test_key", "rhai_test_value");
// Get the value back
let value = redis_get("rhai_test_key");
// Clean up
redis_del("rhai_test_key");
value == "rhai_test_value"
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
if result.is_ok() {
assert_eq!(result.unwrap(), true);
} else {
println!("Redis set/get test failed: {:?}", result.err());
}
}
}