Skip to content

Commit

Permalink
Support Redis-rb v5 (#314)
Browse files Browse the repository at this point in the history
This is an attempt to fix #281.
This PR was also tested using the test suite of our service.

There are a few decisions I made regarding this PR, please let me know
if it's not appropriate:

1. Error from Redis now contains the redis connection URL
- To fix this, I created `MockRedis::Error` module which decorate
`Redis::CommandError` and related errors with the connection URL
- I replace most of the `Redis::CommandError` with the new module
wrapper
2. Some methods now return integer as output instead of boolean and
`exists_returns_integer` is removed
  - I replace the output as is
3. Interfaces of most data structures (Hash, Set, ...) now validate type
to be 4 main primitive types
- `assert_type` is added to validate the arguments. Currently it doesn't
look very clean, so any improvements would be great.
4. `sadd`, `srem` now support multiple arguments
5. Transaction: `multi` now needs to call with a block and
`discard`/`exec` cannot be called outside that block anymore
- I tried to refactor the logic to make the tests pass, but I'm not
entirely confident that it works correctly
  • Loading branch information
hieuk09 authored Nov 27, 2024
1 parent ab8638b commit 615c184
Show file tree
Hide file tree
Showing 63 changed files with 415 additions and 269 deletions.
1 change: 1 addition & 0 deletions lib/mock_redis.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'set'

require 'mock_redis/assertions'
require 'mock_redis/error'
require 'mock_redis/database'
require 'mock_redis/expire_wrapper'
require 'mock_redis/future'
Expand Down
22 changes: 19 additions & 3 deletions lib/mock_redis/assertions.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
require 'mock_redis/error'

class MockRedis
DUMP_TYPES = RedisClient::RESP3::DUMP_TYPES

module Assertions
private

def assert_has_args(args, command)
unless args.any?
raise Redis::CommandError,
"ERR wrong number of arguments for '#{command}' command"
if args.empty?
raise Error.command_error(
"ERR wrong number of arguments for '#{command}' command",
self
)
end
end

def assert_type(*args)
args.each do |arg|
DUMP_TYPES.fetch(arg.class) do |unexpected_class|
unless DUMP_TYPES.keys.find { |t| t > unexpected_class }
raise TypeError, "Unsupported command argument type: #{unexpected_class}"
end
end
end
end
end
Expand Down
39 changes: 17 additions & 22 deletions lib/mock_redis/database.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,32 +95,33 @@ def echo(msg)
end

def expire(key, seconds, nx: nil, xx: nil, lt: nil, gt: nil) # rubocop:disable Metrics/ParameterLists
assert_valid_integer(seconds)
seconds = Integer(seconds)

pexpire(key, seconds.to_i * 1000, nx: nx, xx: xx, lt: lt, gt: gt)
end

def pexpire(key, ms, nx: nil, xx: nil, lt: nil, gt: nil) # rubocop:disable Metrics/ParameterLists
assert_valid_integer(ms)
ms = Integer(ms)

now, miliseconds = @base.now
now_ms = (now * 1000) + miliseconds
pexpireat(key, now_ms + ms.to_i, nx: nx, xx: xx, lt: lt, gt: gt)
end

def expireat(key, timestamp, nx: nil, xx: nil, lt: nil, gt: nil) # rubocop:disable Metrics/ParameterLists
assert_valid_integer(timestamp)
timestamp = Integer(timestamp)

pexpireat(key, timestamp.to_i * 1000, nx: nx, xx: xx, lt: lt, gt: gt)
end

def pexpireat(key, timestamp_ms, nx: nil, xx: nil, lt: nil, gt: nil) # rubocop:disable Metrics/ParameterLists
assert_valid_integer(timestamp_ms)
timestamp_ms = Integer(timestamp_ms)

if nx && gt || gt && lt || lt && nx || nx && xx
raise Redis::CommandError, <<~TXT.chomp
ERR NX and XX, GT or LT options at the same time are not compatible
TXT
raise Error.command_error(
'ERR NX and XX, GT or LT options at the same time are not compatible',
self
)
end

return false unless exists?(key)
Expand Down Expand Up @@ -157,7 +158,7 @@ def dump(key)

def restore(key, ttl, value, replace: false)
if !replace && exists?(key)
raise Redis::CommandError, 'BUSYKEY Target key name already exists.'
raise Error.command_error('BUSYKEY Target key name already exists.', self)
end
data[key] = Marshal.load(value) # rubocop:disable Security/MarshalLoad
if ttl > 0
Expand Down Expand Up @@ -211,7 +212,7 @@ def randomkey

def rename(key, newkey)
unless data.include?(key)
raise Redis::CommandError, 'ERR no such key'
raise Error.command_error('ERR no such key', self)
end

if key != newkey
Expand All @@ -227,7 +228,7 @@ def rename(key, newkey)

def renamenx(key, newkey)
unless data.include?(key)
raise Redis::CommandError, 'ERR no such key'
raise Error.command_error('ERR no such key', self)
end

if exists?(newkey)
Expand Down Expand Up @@ -301,19 +302,13 @@ def eval(*args); end

private

def assert_valid_integer(integer)
unless looks_like_integer?(integer.to_s)
raise Redis::CommandError, 'ERR value is not an integer or out of range'
end
integer
end

def assert_valid_timeout(timeout)
if !looks_like_integer?(timeout.to_s)
raise Redis::CommandError, 'ERR timeout is not an integer or out of range'
elsif timeout < 0
raise Redis::CommandError, 'ERR timeout is negative'
timeout = Integer(timeout)

if timeout < 0
raise ArgumentError, 'time interval must not be negative'
end

timeout
end

Expand Down Expand Up @@ -347,7 +342,7 @@ def has_expiration?(key)
end

def looks_like_integer?(str)
str =~ /^-?\d+$/
!!Integer(str) rescue false
end

def looks_like_float?(str)
Expand Down
27 changes: 27 additions & 0 deletions lib/mock_redis/error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class MockRedis
module Error
module_function

def build(error_class, message, database)
connection = database.connection
url = "redis://#{connection[:host]}:#{connection[:port]}"
error_class.new("#{message} (#{url})")
end

def wrong_type_error(database)
build(
Redis::WrongTypeError,
'WRONGTYPE Operation against a key holding the wrong kind of value',
database
)
end

def syntax_error(database)
command_error('ERR syntax error', database)
end

def command_error(message, database)
build(Redis::CommandError, message, database)
end
end
end
16 changes: 9 additions & 7 deletions lib/mock_redis/geospatial_methods.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'mock_redis/error'

class MockRedis
module GeospatialMethods
LNG_RANGE = (-180..180)
Expand Down Expand Up @@ -81,8 +83,7 @@ def parse_points(args)
points = args.each_slice(3).to_a

if points.last.size != 3
raise Redis::CommandError,
"ERR wrong number of arguments for 'geoadd' command"
raise Error.command_error("ERR wrong number of arguments for 'geoadd' command", self)
end

points.map do |point|
Expand All @@ -97,13 +98,15 @@ def parse_point(point)
unless LNG_RANGE.include?(lng) && LAT_RANGE.include?(lat)
lng = format('%<long>.6f', long: lng)
lat = format('%<lat>.6f', lat: lat)
raise Redis::CommandError,
"ERR invalid longitude,latitude pair #{lng},#{lat}"
raise Error.command_error(
"ERR invalid longitude,latitude pair #{lng},#{lat}",
self
)
end

{ key: point[2], lng: lng, lat: lat }
rescue ArgumentError
raise Redis::CommandError, 'ERR value is not a valid float'
raise Error.command_error('ERR value is not a valid float', self)
end

# Returns ZSET score for passed coordinates
Expand Down Expand Up @@ -212,8 +215,7 @@ def parse_unit(unit)
unit = unit.to_sym
return UNITS[unit] if UNITS[unit]

raise Redis::CommandError,
'ERR unsupported unit provided. please use m, km, ft, mi'
raise Error.command_error('ERR unsupported unit provided. please use m, km, ft, mi', self)
end

def geohash_distance(lng1d, lat1d, lng2d, lat2d)
Expand Down
Loading

0 comments on commit 615c184

Please sign in to comment.