feat: Add PostgreSQL and Redis client support
- Add PostgreSQL client functionality for database interactions. - Add Redis client functionality for cache and data store operations. - Extend Rhai scripting with PostgreSQL and Redis client modules. - Add documentation and test cases for both clients.
This commit is contained in:
@@ -6,10 +6,13 @@ A robust Redis client wrapper for Rust applications that provides connection man
|
||||
|
||||
- **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**:
|
||||
- **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
|
||||
|
||||
@@ -52,9 +55,51 @@ let result: redis::RedisResult<()> = client.execute(&mut cmd);
|
||||
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
|
||||
@@ -77,6 +122,25 @@ The module includes both unit tests and integration tests:
|
||||
- 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.
|
@@ -1,9 +1,149 @@
|
||||
use redis::{Client, Connection, RedisError, RedisResult, Cmd};
|
||||
use lazy_static::lazy_static;
|
||||
use redis::{Client, Cmd, Connection, RedisError, RedisResult};
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex, Once};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use lazy_static::lazy_static;
|
||||
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! {
|
||||
@@ -33,7 +173,7 @@ impl RedisClientWrapper {
|
||||
// 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 {
|
||||
@@ -55,22 +195,25 @@ impl RedisClientWrapper {
|
||||
}
|
||||
|
||||
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")));
|
||||
return Err(RedisError::from((
|
||||
redis::ErrorKind::ResponseError,
|
||||
"Failed to ping Redis server",
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
// Select the database
|
||||
redis::cmd("SELECT").arg(self.db).execute(&mut conn);
|
||||
|
||||
|
||||
self.initialized.store(true, Ordering::Relaxed);
|
||||
|
||||
|
||||
// Store the connection
|
||||
let mut conn_guard = self.connection.lock().unwrap();
|
||||
*conn_guard = Some(conn);
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -84,65 +227,91 @@ pub fn get_redis_client() -> RedisResult<Arc<RedisClientWrapper>> {
|
||||
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>> {
|
||||
// First try: Connect via Unix socket
|
||||
// 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_url = format!("unix://{}", socket_path);
|
||||
match Client::open(socket_url) {
|
||||
Ok(client) => {
|
||||
let db = get_redis_db();
|
||||
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);
|
||||
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);
|
||||
eprintln!(
|
||||
"Socket exists at {} but connection failed: {}",
|
||||
socket_path, err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second try: Connect via TCP to localhost
|
||||
let tcp_url = "redis://127.0.0.1/";
|
||||
match Client::open(tcp_url) {
|
||||
Ok(client) => {
|
||||
let db = get_redis_db();
|
||||
|
||||
// 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 localhost: {}", socket_path, err)
|
||||
)))
|
||||
}
|
||||
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
|
||||
),
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +330,7 @@ pub fn reset() -> RedisResult<()> {
|
||||
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()?;
|
||||
@@ -175,4 +344,18 @@ where
|
||||
{
|
||||
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)
|
||||
}
|
||||
|
@@ -1,25 +1,25 @@
|
||||
use super::*;
|
||||
use std::env;
|
||||
use redis::RedisResult;
|
||||
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);
|
||||
@@ -27,21 +27,21 @@ mod redis_client_tests {
|
||||
env::remove_var("REDISDB");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_redis_client_creation_mock() {
|
||||
// This is a simplified test that doesn't require an actual Redis server
|
||||
// It just verifies that the function handles environment variables correctly
|
||||
|
||||
|
||||
// Save original HOME value to restore later
|
||||
let original_home = env::var("HOME").ok();
|
||||
|
||||
|
||||
// Set HOME to a test value
|
||||
env::set_var("HOME", "/tmp");
|
||||
|
||||
|
||||
// The actual client creation would be tested in integration tests
|
||||
// with a real Redis server or a mock
|
||||
|
||||
|
||||
// Restore original HOME value
|
||||
if let Some(home) = original_home {
|
||||
env::set_var("HOME", home);
|
||||
@@ -49,12 +49,12 @@ mod redis_client_tests {
|
||||
env::remove_var("HOME");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_reset_mock() {
|
||||
// This is a simplified test that doesn't require an actual Redis server
|
||||
// In a real test, we would need to mock the Redis client
|
||||
|
||||
|
||||
// Just verify that the reset function doesn't panic
|
||||
// This is a minimal test - in a real scenario, we would use mocking
|
||||
// to verify that the client is properly reset
|
||||
@@ -63,6 +63,77 @@ mod redis_client_tests {
|
||||
// So we don't assert anything here
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
@@ -70,7 +141,7 @@ mod redis_client_tests {
|
||||
#[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() {
|
||||
@@ -78,49 +149,200 @@ mod redis_integration_tests {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user