kuende-fakeredis 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +19 -0
  4. data/Gemfile +13 -0
  5. data/Guardfile +8 -0
  6. data/LICENSE +21 -0
  7. data/README.md +102 -0
  8. data/Rakefile +27 -0
  9. data/fakeredis.gemspec +24 -0
  10. data/gemfiles/redisrb-master.gemfile +14 -0
  11. data/lib/fake_redis.rb +1 -0
  12. data/lib/fakeredis.rb +6 -0
  13. data/lib/fakeredis/bitop_command.rb +56 -0
  14. data/lib/fakeredis/command_executor.rb +25 -0
  15. data/lib/fakeredis/expiring_hash.rb +70 -0
  16. data/lib/fakeredis/minitest.rb +24 -0
  17. data/lib/fakeredis/rspec.rb +24 -0
  18. data/lib/fakeredis/sort_method.rb +117 -0
  19. data/lib/fakeredis/sorted_set_argument_handler.rb +74 -0
  20. data/lib/fakeredis/sorted_set_store.rb +80 -0
  21. data/lib/fakeredis/transaction_commands.rb +83 -0
  22. data/lib/fakeredis/version.rb +3 -0
  23. data/lib/fakeredis/zset.rb +39 -0
  24. data/lib/redis/connection/memory.rb +1375 -0
  25. data/spec/bitop_command_spec.rb +209 -0
  26. data/spec/compatibility_spec.rb +9 -0
  27. data/spec/connection_spec.rb +85 -0
  28. data/spec/hashes_spec.rb +261 -0
  29. data/spec/keys_spec.rb +488 -0
  30. data/spec/lists_spec.rb +229 -0
  31. data/spec/memory_spec.rb +28 -0
  32. data/spec/server_spec.rb +100 -0
  33. data/spec/sets_spec.rb +280 -0
  34. data/spec/sort_method_spec.rb +74 -0
  35. data/spec/sorted_sets_spec.rb +578 -0
  36. data/spec/spec_helper.rb +29 -0
  37. data/spec/spec_helper_live_redis.rb +14 -0
  38. data/spec/strings_spec.rb +289 -0
  39. data/spec/subscription_spec.rb +107 -0
  40. data/spec/support/shared_examples/bitwise_operation.rb +59 -0
  41. data/spec/support/shared_examples/sortable.rb +69 -0
  42. data/spec/transactions_spec.rb +92 -0
  43. data/spec/upcase_method_name_spec.rb +18 -0
  44. metadata +148 -0
@@ -0,0 +1,24 @@
1
+ # Require this either in your Gemfile or in RSpec's
2
+ # support scripts. Examples:
3
+ #
4
+ # # Gemfile
5
+ # group :test do
6
+ # gem "rspec"
7
+ # gem "fakeredis", :require => "fakeredis/rspec"
8
+ # end
9
+ #
10
+ # # spec/support/fakeredis.rb
11
+ # require 'fakeredis/rspec'
12
+ #
13
+
14
+ require 'rspec/core'
15
+ require 'fakeredis'
16
+
17
+ RSpec.configure do |c|
18
+
19
+ c.before do
20
+ Redis::Connection::Memory.reset_all_databases
21
+ Redis::Connection::Memory.reset_all_channels
22
+ end
23
+
24
+ end
@@ -0,0 +1,117 @@
1
+ # Codes are mostly referenced from MockRedis' implementation.
2
+ module FakeRedis
3
+ module SortMethod
4
+ def sort(key, *redis_options_array)
5
+ return [] unless key
6
+ return [] if type(key) == 'none'
7
+
8
+ unless %w(list set zset).include? type(key)
9
+ warn "Operation against a key holding the wrong kind of value: Expected list, set or zset at #{key}."
10
+ raise Redis::CommandError.new("WRONGTYPE Operation against a key holding the wrong kind of value")
11
+ end
12
+
13
+ # redis_options is an array of format [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]
14
+ # Lets nibble it back into a hash
15
+ options = extract_options_from(redis_options_array)
16
+
17
+ # And now to actually do the work of this method
18
+
19
+ projected = project(data[key], options[:by], options[:get])
20
+ sorted = sort_by(projected, options[:order])
21
+ sliced = slice(sorted, options[:limit])
22
+ # We have to flatten it down as redis-rb adds back the array to the return value
23
+ result = sliced.flatten(1)
24
+
25
+ options[:store] ? rpush(options[:store], sliced) : result
26
+ end
27
+
28
+ private
29
+
30
+ ASCENDING_SORT = Proc.new { |a, b| a.first <=> b.first }
31
+ DESCENDING_SORT = Proc.new { |a, b| b.first <=> a.first }
32
+
33
+ def extract_options_from(options_array)
34
+ # Defaults
35
+ options = {
36
+ :limit => [],
37
+ :order => "ASC",
38
+ :get => []
39
+ }
40
+
41
+ if options_array.first == "BY"
42
+ options_array.shift
43
+ options[:by] = options_array.shift
44
+ end
45
+
46
+ if options_array.first == "LIMIT"
47
+ options_array.shift
48
+ options[:limit] = [options_array.shift, options_array.shift]
49
+ end
50
+
51
+ while options_array.first == "GET"
52
+ options_array.shift
53
+ options[:get] << options_array.shift
54
+ end
55
+
56
+ if %w(ASC DESC ALPHA).include?(options_array.first)
57
+ options[:order] = options_array.shift
58
+ options[:order] = "ASC" if options[:order] == "ALPHA"
59
+ end
60
+
61
+ if options_array.first == "STORE"
62
+ options_array.shift
63
+ options[:store] = options_array.shift
64
+ end
65
+
66
+ options
67
+ end
68
+
69
+ def project(enumerable, by, get_patterns)
70
+ enumerable.map do |*elements|
71
+ element = elements.flatten.first
72
+ weight = by ? lookup_from_pattern(by, element) : element
73
+ value = element
74
+
75
+ if get_patterns.length > 0
76
+ value = get_patterns.map do |pattern|
77
+ pattern == "#" ? element : lookup_from_pattern(pattern, element)
78
+ end
79
+ value = value.first if value.length == 1
80
+ end
81
+
82
+ [weight, value]
83
+ end
84
+ end
85
+
86
+ def sort_by(projected, direction)
87
+ sorter =
88
+ case direction.upcase
89
+ when "DESC"
90
+ DESCENDING_SORT
91
+ when "ASC", "ALPHA"
92
+ ASCENDING_SORT
93
+ else
94
+ raise "Invalid direction '#{direction}'"
95
+ end
96
+
97
+ projected.sort(&sorter).map(&:last)
98
+ end
99
+
100
+ def slice(sorted, limit)
101
+ skip = limit.first || 0
102
+ take = limit.last || sorted.length
103
+
104
+ sorted[skip...(skip + take)] || sorted
105
+ end
106
+
107
+ def lookup_from_pattern(pattern, element)
108
+ key = pattern.sub('*', element)
109
+
110
+ if (hash_parts = key.split('->')).length > 1
111
+ hget hash_parts.first, hash_parts.last
112
+ else
113
+ get key
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,74 @@
1
+ module FakeRedis
2
+ # Takes in the variable length array of arguments for a zinterstore/zunionstore method
3
+ # and parses them into a few attributes for the method to access.
4
+ #
5
+ # Handles throwing errors for various scenarios (matches redis):
6
+ # * Custom weights specified, but not enough or too many given
7
+ # * Invalid aggregate value given
8
+ # * Multiple aggregate values given
9
+ class SortedSetArgumentHandler
10
+ # [Symbol] The aggregate method to use for the output values. One of %w(sum min max) expected
11
+ attr_reader :aggregate
12
+ # [Integer] Number of keys in the argument list
13
+ attr_accessor :number_of_keys
14
+ # [Array] The actual keys in the argument list
15
+ attr_accessor :keys
16
+ # [Array] integers for weighting the values of each key - one number per key expected
17
+ attr_accessor :weights
18
+
19
+ # Used internally
20
+ attr_accessor :type
21
+
22
+ # Expects all the argments for the method to be passed as an array
23
+ def initialize args
24
+ # Pull out known lengths of data
25
+ self.number_of_keys = args.shift
26
+ self.keys = args.shift(number_of_keys)
27
+ # Handle the variable lengths of data (WEIGHTS/AGGREGATE)
28
+ args.inject(self) {|handler, item| handler.handle(item) }
29
+
30
+ # Defaults for unspecified things
31
+ self.weights ||= Array.new(number_of_keys) { 1 }
32
+ self.aggregate ||= :sum
33
+
34
+ # Validate values
35
+ raise(Redis::CommandError, "ERR syntax error") unless weights.size == number_of_keys
36
+ raise(Redis::CommandError, "ERR syntax error") unless [:min, :max, :sum].include?(aggregate)
37
+ end
38
+
39
+ # Only allows assigning a value *once* - raises Redis::CommandError if a second is given
40
+ def aggregate=(str)
41
+ raise(Redis::CommandError, "ERR syntax error") if (defined?(@aggregate) && @aggregate)
42
+ @aggregate = str.to_s.downcase.to_sym
43
+ end
44
+
45
+ # Decides how to handle an item, depending on where we are in the arguments
46
+ def handle(item)
47
+ case item
48
+ when "WEIGHTS"
49
+ self.type = :weights
50
+ self.weights = []
51
+ when "AGGREGATE"
52
+ self.type = :aggregate
53
+ when nil
54
+ # This should never be called, raise a syntax error if we manage to hit it
55
+ raise(Redis::CommandError, "ERR syntax error")
56
+ else
57
+ send "handle_#{type}", item
58
+ end
59
+ self
60
+ end
61
+
62
+ def handle_weights(item)
63
+ self.weights << item
64
+ end
65
+
66
+ def handle_aggregate(item)
67
+ self.aggregate = item
68
+ end
69
+
70
+ def inject_block
71
+ lambda { |handler, item| handler.handle(item) }
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,80 @@
1
+ module FakeRedis
2
+ class SortedSetStore
3
+ attr_accessor :data, :weights, :aggregate, :keys
4
+
5
+ def initialize params, data
6
+ self.data = data
7
+ self.weights = params.weights
8
+ self.aggregate = params.aggregate
9
+ self.keys = params.keys
10
+ end
11
+
12
+ def hashes
13
+ @hashes ||= keys.map do |src|
14
+ case data[src]
15
+ when ::Set
16
+ # Every value has a score of 1
17
+ Hash[data[src].map {|k,v| [k, 1]}]
18
+ when Hash
19
+ data[src]
20
+ else
21
+ {}
22
+ end
23
+ end
24
+ end
25
+
26
+ # Apply the weightings to the hashes
27
+ def computed_values
28
+ unless defined?(@computed_values) && @computed_values
29
+ # Do nothing if all weights are 1, as n * 1 is n
30
+ @computed_values = hashes if weights.all? {|weight| weight == 1 }
31
+ # Otherwise, multiply the values in each hash by that hash's weighting
32
+ @computed_values ||= hashes.each_with_index.map do |hash, index|
33
+ weight = weights[index]
34
+ Hash[hash.map {|k, v| [k, (v * weight)]}]
35
+ end
36
+ end
37
+ @computed_values
38
+ end
39
+
40
+ def aggregate_sum out
41
+ selected_keys.each do |key|
42
+ out[key] = computed_values.inject(0) do |n, hash|
43
+ n + (hash[key] || 0)
44
+ end
45
+ end
46
+ end
47
+
48
+ def aggregate_min out
49
+ selected_keys.each do |key|
50
+ out[key] = computed_values.map {|h| h[key] }.compact.min
51
+ end
52
+ end
53
+
54
+ def aggregate_max out
55
+ selected_keys.each do |key|
56
+ out[key] = computed_values.map {|h| h[key] }.compact.max
57
+ end
58
+ end
59
+
60
+ def selected_keys
61
+ raise NotImplemented, "subclass needs to implement #selected_keys"
62
+ end
63
+
64
+ def call
65
+ ZSet.new.tap {|out| send("aggregate_#{aggregate}", out) }
66
+ end
67
+ end
68
+
69
+ class SortedSetIntersectStore < SortedSetStore
70
+ def selected_keys
71
+ @values ||= hashes.inject([]) { |r, h| r.empty? ? h.keys : (r & h.keys) }
72
+ end
73
+ end
74
+
75
+ class SortedSetUnionStore < SortedSetStore
76
+ def selected_keys
77
+ @values ||= hashes.map(&:keys).flatten.uniq
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,83 @@
1
+ module FakeRedis
2
+ TRANSACTION_COMMANDS = [:discard, :exec, :multi, :watch, :unwatch]
3
+
4
+ module TransactionCommands
5
+ def self.included(klass)
6
+ klass.class_eval do
7
+ def self.queued_commands
8
+ @queued_commands ||= Hash.new {|h,k| h[k] = [] }
9
+ end
10
+
11
+ def self.in_multi
12
+ @in_multi ||= Hash.new{|h,k| h[k] = false}
13
+ end
14
+
15
+ def queued_commands
16
+ self.class.queued_commands[database_instance_key]
17
+ end
18
+
19
+ def queued_commands=(cmds)
20
+ self.class.queued_commands[database_instance_key] = cmds
21
+ end
22
+
23
+ def in_multi
24
+ self.class.in_multi[database_instance_key]
25
+ end
26
+
27
+ def in_multi=(multi_state)
28
+ self.class.in_multi[database_instance_key] = multi_state
29
+ end
30
+ end
31
+ end
32
+
33
+ def discard
34
+ unless in_multi
35
+ raise Redis::CommandError, "ERR DISCARD without MULTI"
36
+ end
37
+
38
+ self.in_multi = false
39
+ self.queued_commands = []
40
+
41
+ 'OK'
42
+ end
43
+
44
+ def exec
45
+ unless in_multi
46
+ raise Redis::CommandError, "ERR EXEC without MULTI"
47
+ end
48
+
49
+ responses = queued_commands.map do |cmd|
50
+ begin
51
+ send(*cmd)
52
+ rescue => e
53
+ e
54
+ end
55
+ end
56
+
57
+ self.queued_commands = [] # reset queued_commands
58
+ self.in_multi = false # reset in_multi state
59
+
60
+ responses
61
+ end
62
+
63
+ def multi
64
+ if in_multi
65
+ raise Redis::CommandError, "ERR MULTI calls can not be nested"
66
+ end
67
+
68
+ self.in_multi = true
69
+
70
+ yield(self) if block_given?
71
+
72
+ "OK"
73
+ end
74
+
75
+ def watch(*_)
76
+ "OK"
77
+ end
78
+
79
+ def unwatch
80
+ "OK"
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,3 @@
1
+ module FakeRedis
2
+ VERSION = "0.6.0"
3
+ end
@@ -0,0 +1,39 @@
1
+ module FakeRedis
2
+ class ZSet < Hash
3
+
4
+ def []=(key, val)
5
+ super(key, _floatify(val))
6
+ end
7
+
8
+ def identical_scores?
9
+ values.uniq.size == 1
10
+ end
11
+
12
+ # Increments the value of key by val
13
+ def increment(key, val)
14
+ self[key] += _floatify(val)
15
+ end
16
+
17
+ def select_by_score min, max
18
+ min = _floatify(min, true)
19
+ max = _floatify(max, false)
20
+ reject {|_,v| v < min || v > max }
21
+ end
22
+
23
+ private
24
+
25
+ # Originally lifted from redis-rb
26
+ def _floatify(str, increment = true)
27
+ if (( inf = str.to_s.match(/^([+-])?inf/i) ))
28
+ (inf[1] == "-" ? -1.0 : 1.0) / 0.0
29
+ elsif (( number = str.to_s.match(/^\((\d+)/i) ))
30
+ number[1].to_i + (increment ? 1 : -1)
31
+ else
32
+ Float str.to_s
33
+ end
34
+ rescue ArgumentError
35
+ raise Redis::CommandError, "ERR value is not a valid float"
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,1375 @@
1
+ require 'set'
2
+ require 'redis/connection/registry'
3
+ require 'redis/connection/command_helper'
4
+ require "fakeredis/command_executor"
5
+ require "fakeredis/expiring_hash"
6
+ require "fakeredis/sort_method"
7
+ require "fakeredis/sorted_set_argument_handler"
8
+ require "fakeredis/sorted_set_store"
9
+ require "fakeredis/transaction_commands"
10
+ require "fakeredis/zset"
11
+ require "fakeredis/bitop_command"
12
+ require "fakeredis/version"
13
+
14
+ class Redis
15
+ module Connection
16
+ class Memory
17
+ include Redis::Connection::CommandHelper
18
+ include FakeRedis
19
+ include SortMethod
20
+ include TransactionCommands
21
+ include BitopCommand
22
+ include CommandExecutor
23
+
24
+ attr_accessor :options
25
+
26
+ # Tracks all databases for all instances across the current process.
27
+ # We have to be able to handle two clients with the same host/port accessing
28
+ # different databases at once without overwriting each other. So we store our
29
+ # "data" outside the client instances, in this class level instance method.
30
+ # Client instances access it with a key made up of their host/port, and then select
31
+ # which DB out of the array of them they want. Allows the access we need.
32
+ def self.databases
33
+ @databases ||= Hash.new {|h,k| h[k] = [] }
34
+ end
35
+
36
+ # Used for resetting everything in specs
37
+ def self.reset_all_databases
38
+ @databases = nil
39
+ end
40
+
41
+ def self.channels
42
+ @channels ||= Hash.new {|h,k| h[k] = [] }
43
+ end
44
+
45
+ def self.reset_all_channels
46
+ @channels = nil
47
+ end
48
+
49
+ def self.connect(options = {})
50
+ new(options)
51
+ end
52
+
53
+ def initialize(options = {})
54
+ self.options = options
55
+ end
56
+
57
+ def database_id
58
+ @database_id ||= 0
59
+ end
60
+ attr_writer :database_id
61
+
62
+ def database_instance_key
63
+ [options[:host], options[:port]].hash
64
+ end
65
+
66
+ def databases
67
+ self.class.databases[database_instance_key]
68
+ end
69
+
70
+ def find_database id=database_id
71
+ databases[id] ||= ExpiringHash.new
72
+ end
73
+
74
+ def data
75
+ find_database
76
+ end
77
+
78
+ def replies
79
+ @replies ||= []
80
+ end
81
+ attr_writer :replies
82
+
83
+ def connected?
84
+ true
85
+ end
86
+
87
+ def connect_unix(path, timeout)
88
+ end
89
+
90
+ def disconnect
91
+ end
92
+
93
+ def timeout=(usecs)
94
+ end
95
+
96
+ def read
97
+ replies.shift
98
+ end
99
+
100
+ def flushdb
101
+ databases.delete_at(database_id)
102
+ "OK"
103
+ end
104
+
105
+ def flushall
106
+ self.class.databases[database_instance_key] = []
107
+ "OK"
108
+ end
109
+
110
+ def auth(password)
111
+ "OK"
112
+ end
113
+
114
+ def select(index)
115
+ data_type_check(index, Integer)
116
+ self.database_id = index
117
+ "OK"
118
+ end
119
+
120
+ def info
121
+ {
122
+ "redis_version" => "2.6.16",
123
+ "connected_clients" => "1",
124
+ "connected_slaves" => "0",
125
+ "used_memory" => "3187",
126
+ "changes_since_last_save" => "0",
127
+ "last_save_time" => "1237655729",
128
+ "total_connections_received" => "1",
129
+ "total_commands_processed" => "1",
130
+ "uptime_in_seconds" => "36000",
131
+ "uptime_in_days" => 0
132
+ }
133
+ end
134
+
135
+ def monitor; end
136
+
137
+ def save; end
138
+
139
+ def bgsave ; end
140
+
141
+ def bgrewriteaof ; end
142
+
143
+ def move key, destination_id
144
+ raise Redis::CommandError, "ERR source and destination objects are the same" if destination_id == database_id
145
+ destination = find_database(destination_id)
146
+ return false unless data.has_key?(key)
147
+ return false if destination.has_key?(key)
148
+ destination[key] = data.delete(key)
149
+ true
150
+ end
151
+
152
+ def dump(key)
153
+ return nil unless exists(key)
154
+
155
+ value = data[key]
156
+
157
+ Marshal.dump(
158
+ value: value,
159
+ version: FakeRedis::VERSION, # Redis includes the version, so we might as well
160
+ )
161
+ end
162
+
163
+ def restore(key, ttl, serialized_value)
164
+ raise Redis::CommandError, "ERR Target key name is busy." if exists(key)
165
+
166
+ raise Redis::CommandError, "ERR DUMP payload version or checksum are wrong" if serialized_value.nil?
167
+
168
+ parsed_value = begin
169
+ Marshal.load(serialized_value)
170
+ rescue TypeError
171
+ raise Redis::CommandError, "ERR DUMP payload version or checksum are wrong"
172
+ end
173
+
174
+ if parsed_value[:version] != FakeRedis::VERSION
175
+ raise Redis::CommandError, "ERR DUMP payload version or checksum are wrong"
176
+ end
177
+
178
+ # We could figure out what type the key was and set it with the public API here,
179
+ # or we could just assign the value. If we presume the serialized_value is only ever
180
+ # a return value from `dump` then we've only been given something that was in
181
+ # the internal data structure anyway.
182
+ data[key] = parsed_value[:value]
183
+
184
+ # Set a TTL if one has been passed
185
+ ttl = ttl.to_i # Makes nil into 0
186
+ expire(key, ttl / 1000) unless ttl.zero?
187
+
188
+ "OK"
189
+ end
190
+
191
+ def get(key)
192
+ data_type_check(key, String)
193
+ data[key]
194
+ end
195
+
196
+ def getbit(key, offset)
197
+ return unless data[key]
198
+ data[key].unpack('B*')[0].split("")[offset].to_i
199
+ end
200
+
201
+ def bitcount(key, start_index = 0, end_index = -1)
202
+ return 0 unless data[key]
203
+ data[key][start_index..end_index].unpack('B*')[0].count("1")
204
+ end
205
+
206
+ def getrange(key, start, ending)
207
+ return unless data[key]
208
+ data[key][start..ending]
209
+ end
210
+ alias :substr :getrange
211
+
212
+ def getset(key, value)
213
+ data_type_check(key, String)
214
+ data[key].tap do
215
+ set(key, value)
216
+ end
217
+ end
218
+
219
+ def mget(*keys)
220
+ raise_argument_error('mget') if keys.empty?
221
+ # We work with either an array, or list of arguments
222
+ keys = keys.first if keys.size == 1
223
+ data.values_at(*keys)
224
+ end
225
+
226
+ def append(key, value)
227
+ data[key] = (data[key] || "")
228
+ data[key] = data[key] + value.to_s
229
+ end
230
+
231
+ def strlen(key)
232
+ return unless data[key]
233
+ data[key].size
234
+ end
235
+
236
+ def hgetall(key)
237
+ data_type_check(key, Hash)
238
+ data[key].to_a.flatten || {}
239
+ end
240
+
241
+ def hget(key, field)
242
+ data_type_check(key, Hash)
243
+ data[key] && data[key][field.to_s]
244
+ end
245
+
246
+ def hdel(key, field)
247
+ data_type_check(key, Hash)
248
+ return 0 unless data[key]
249
+
250
+ if field.is_a?(Array)
251
+ old_keys_count = data[key].size
252
+ fields = field.map(&:to_s)
253
+
254
+ data[key].delete_if { |k, v| fields.include? k }
255
+ deleted = old_keys_count - data[key].size
256
+ else
257
+ field = field.to_s
258
+ deleted = data[key].delete(field) ? 1 : 0
259
+ end
260
+
261
+ remove_key_for_empty_collection(key)
262
+ deleted
263
+ end
264
+
265
+ def hkeys(key)
266
+ data_type_check(key, Hash)
267
+ return [] if data[key].nil?
268
+ data[key].keys
269
+ end
270
+
271
+ def hscan(key, start_cursor, *args)
272
+ data_type_check(key, Hash)
273
+ return ["0", []] unless data[key]
274
+
275
+ match = "*"
276
+ count = 10
277
+
278
+ if args.size.odd?
279
+ raise_argument_error('hscan')
280
+ end
281
+
282
+ if idx = args.index("MATCH")
283
+ match = args[idx + 1]
284
+ end
285
+
286
+ if idx = args.index("COUNT")
287
+ count = args[idx + 1]
288
+ end
289
+
290
+ start_cursor = start_cursor.to_i
291
+
292
+ cursor = start_cursor
293
+ next_keys = []
294
+
295
+ if start_cursor + count >= data[key].length
296
+ next_keys = (data[key].to_a)[start_cursor..-1]
297
+ cursor = 0
298
+ else
299
+ cursor = start_cursor + count
300
+ next_keys = (data[key].to_a)[start_cursor..cursor-1]
301
+ end
302
+
303
+ filtered_next_keys = next_keys.select{|k,v| File.fnmatch(match, k)}
304
+ result = filtered_next_keys.flatten.map(&:to_s)
305
+
306
+ return ["#{cursor}", result]
307
+ end
308
+
309
+ def keys(pattern = "*")
310
+ data.keys.select { |key| File.fnmatch(pattern, key) }
311
+ end
312
+
313
+ def randomkey
314
+ data.keys[rand(dbsize)]
315
+ end
316
+
317
+ def echo(string)
318
+ string
319
+ end
320
+
321
+ def ping
322
+ "PONG"
323
+ end
324
+
325
+ def lastsave
326
+ Time.now.to_i
327
+ end
328
+
329
+ def time
330
+ microseconds = (Time.now.to_f * 1000000).to_i
331
+ [ microseconds / 1000000, microseconds % 1000000 ]
332
+ end
333
+
334
+ def dbsize
335
+ data.keys.count
336
+ end
337
+
338
+ def exists(key)
339
+ data.key?(key)
340
+ end
341
+
342
+ def llen(key)
343
+ data_type_check(key, Array)
344
+ return 0 unless data[key]
345
+ data[key].size
346
+ end
347
+
348
+ def lrange(key, startidx, endidx)
349
+ data_type_check(key, Array)
350
+ (data[key] && data[key][startidx..endidx]) || []
351
+ end
352
+
353
+ def ltrim(key, start, stop)
354
+ data_type_check(key, Array)
355
+ return unless data[key]
356
+
357
+ # Example: we have a list of 3 elements and
358
+ # we give it a ltrim list, -5, -1. This means
359
+ # it should trim to a max of 5. Since 3 < 5
360
+ # we should not touch the list. This is consistent
361
+ # with behavior of real Redis's ltrim with a negative
362
+ # start argument.
363
+ unless start < 0 && data[key].count < start.abs
364
+ data[key] = data[key][start..stop]
365
+ end
366
+
367
+ "OK"
368
+ end
369
+
370
+ def lindex(key, index)
371
+ data_type_check(key, Array)
372
+ data[key] && data[key][index]
373
+ end
374
+
375
+ def linsert(key, where, pivot, value)
376
+ data_type_check(key, Array)
377
+ return unless data[key]
378
+
379
+ value = value.to_s
380
+ index = data[key].index(pivot.to_s)
381
+ return -1 if index.nil?
382
+
383
+ case where
384
+ when :before then data[key].insert(index, value)
385
+ when :after then data[key].insert(index + 1, value)
386
+ else raise_syntax_error
387
+ end
388
+ end
389
+
390
+ def lset(key, index, value)
391
+ data_type_check(key, Array)
392
+ return unless data[key]
393
+ raise Redis::CommandError, "ERR index out of range" if index >= data[key].size
394
+ data[key][index] = value.to_s
395
+ end
396
+
397
+ def lrem(key, count, value)
398
+ data_type_check(key, Array)
399
+ return 0 unless data[key]
400
+
401
+ value = value.to_s
402
+ old_size = data[key].size
403
+ diff =
404
+ if count == 0
405
+ data[key].delete(value)
406
+ old_size - data[key].size
407
+ else
408
+ array = count > 0 ? data[key].dup : data[key].reverse
409
+ count.abs.times{ array.delete_at(array.index(value) || array.length) }
410
+ data[key] = count > 0 ? array.dup : array.reverse
411
+ old_size - data[key].size
412
+ end
413
+ remove_key_for_empty_collection(key)
414
+ diff
415
+ end
416
+
417
+ def rpush(key, value)
418
+ raise_argument_error('rpush') if value.respond_to?(:each) && value.empty?
419
+ data_type_check(key, Array)
420
+ data[key] ||= []
421
+ [value].flatten.each do |val|
422
+ data[key].push(val.to_s)
423
+ end
424
+ data[key].size
425
+ end
426
+
427
+ def rpushx(key, value)
428
+ raise_argument_error('rpushx') if value.respond_to?(:each) && value.empty?
429
+ data_type_check(key, Array)
430
+ return unless data[key]
431
+ rpush(key, value)
432
+ end
433
+
434
+ def lpush(key, value)
435
+ raise_argument_error('lpush') if value.respond_to?(:each) && value.empty?
436
+ data_type_check(key, Array)
437
+ data[key] ||= []
438
+ [value].flatten.each do |val|
439
+ data[key].unshift(val.to_s)
440
+ end
441
+ data[key].size
442
+ end
443
+
444
+ def lpushx(key, value)
445
+ raise_argument_error('lpushx') if value.respond_to?(:each) && value.empty?
446
+ data_type_check(key, Array)
447
+ return unless data[key]
448
+ lpush(key, value)
449
+ end
450
+
451
+ def rpop(key)
452
+ data_type_check(key, Array)
453
+ return unless data[key]
454
+ data[key].pop
455
+ end
456
+
457
+ def brpop(keys, timeout=0)
458
+ #todo threaded mode
459
+ keys = Array(keys)
460
+ keys.each do |key|
461
+ if data[key] && data[key].size > 0
462
+ return [key, data[key].pop]
463
+ end
464
+ end
465
+ sleep(timeout.to_f)
466
+ nil
467
+ end
468
+
469
+ def rpoplpush(key1, key2)
470
+ data_type_check(key1, Array)
471
+ rpop(key1).tap do |elem|
472
+ lpush(key2, elem) unless elem.nil?
473
+ end
474
+ end
475
+
476
+ def brpoplpush(key1, key2, opts={})
477
+ data_type_check(key1, Array)
478
+ brpop(key1).tap do |elem|
479
+ lpush(key2, elem) unless elem.nil?
480
+ end
481
+ end
482
+
483
+ def lpop(key)
484
+ data_type_check(key, Array)
485
+ return unless data[key]
486
+ data[key].shift
487
+ end
488
+
489
+ def blpop(keys, timeout=0)
490
+ #todo threaded mode
491
+ keys = Array(keys)
492
+ keys.each do |key|
493
+ if data[key] && data[key].size > 0
494
+ return [key, data[key].shift]
495
+ end
496
+ end
497
+ sleep(timeout.to_f)
498
+ nil
499
+ end
500
+
501
+ def smembers(key)
502
+ data_type_check(key, ::Set)
503
+ return [] unless data[key]
504
+ data[key].to_a.reverse
505
+ end
506
+
507
+ def sismember(key, value)
508
+ data_type_check(key, ::Set)
509
+ return false unless data[key]
510
+ data[key].include?(value.to_s)
511
+ end
512
+
513
+ def sadd(key, value)
514
+ data_type_check(key, ::Set)
515
+ value = Array(value)
516
+ raise_argument_error('sadd') if value.empty?
517
+
518
+ result = if data[key]
519
+ old_set = data[key].dup
520
+ data[key].merge(value.map(&:to_s))
521
+ (data[key] - old_set).size
522
+ else
523
+ data[key] = ::Set.new(value.map(&:to_s))
524
+ data[key].size
525
+ end
526
+
527
+ # 0 = false, 1 = true, 2+ untouched
528
+ return result == 1 if result < 2
529
+ result
530
+ end
531
+
532
+ def srem(key, value)
533
+ data_type_check(key, ::Set)
534
+ return false unless data[key]
535
+
536
+ if value.is_a?(Array)
537
+ old_size = data[key].size
538
+ values = value.map(&:to_s)
539
+ values.each { |v| data[key].delete(v) }
540
+ deleted = old_size - data[key].size
541
+ else
542
+ deleted = !!data[key].delete?(value.to_s)
543
+ end
544
+
545
+ remove_key_for_empty_collection(key)
546
+ deleted
547
+ end
548
+
549
+ def smove(source, destination, value)
550
+ data_type_check(destination, ::Set)
551
+ result = self.srem(source, value)
552
+ self.sadd(destination, value) if result
553
+ result
554
+ end
555
+
556
+ def spop(key)
557
+ data_type_check(key, ::Set)
558
+ elem = srandmember(key)
559
+ srem(key, elem)
560
+ elem
561
+ end
562
+
563
+ def scard(key)
564
+ data_type_check(key, ::Set)
565
+ return 0 unless data[key]
566
+ data[key].size
567
+ end
568
+
569
+ def sinter(*keys)
570
+ keys = keys[0] if flatten?(keys)
571
+ raise_argument_error('sinter') if keys.empty?
572
+
573
+ keys.each { |k| data_type_check(k, ::Set) }
574
+ return ::Set.new if keys.any? { |k| data[k].nil? }
575
+ keys = keys.map { |k| data[k] || ::Set.new }
576
+ keys.inject do |set, key|
577
+ set & key
578
+ end.to_a
579
+ end
580
+
581
+ def sinterstore(destination, *keys)
582
+ data_type_check(destination, ::Set)
583
+ result = sinter(*keys)
584
+ data[destination] = ::Set.new(result)
585
+ end
586
+
587
+ def sunion(*keys)
588
+ keys = keys[0] if flatten?(keys)
589
+ raise_argument_error('sunion') if keys.empty?
590
+
591
+ keys.each { |k| data_type_check(k, ::Set) }
592
+ keys = keys.map { |k| data[k] || ::Set.new }
593
+ keys.inject(::Set.new) do |set, key|
594
+ set | key
595
+ end.to_a
596
+ end
597
+
598
+ def sunionstore(destination, *keys)
599
+ data_type_check(destination, ::Set)
600
+ result = sunion(*keys)
601
+ data[destination] = ::Set.new(result)
602
+ end
603
+
604
+ def sdiff(key1, *keys)
605
+ keys = keys[0] if flatten?(keys)
606
+ [key1, *keys].each { |k| data_type_check(k, ::Set) }
607
+ keys = keys.map { |k| data[k] || ::Set.new }
608
+ keys.inject(data[key1] || Set.new) do |memo, set|
609
+ memo - set
610
+ end.to_a
611
+ end
612
+
613
+ def sdiffstore(destination, key1, *keys)
614
+ data_type_check(destination, ::Set)
615
+ result = sdiff(key1, *keys)
616
+ data[destination] = ::Set.new(result)
617
+ end
618
+
619
+ def srandmember(key, number=nil)
620
+ number.nil? ? srandmember_single(key) : srandmember_multiple(key, number)
621
+ end
622
+
623
+ def del(*keys)
624
+ keys = keys.flatten(1)
625
+ raise_argument_error('del') if keys.empty?
626
+
627
+ old_count = data.keys.size
628
+ keys.each do |key|
629
+ data.delete(key)
630
+ end
631
+ old_count - data.keys.size
632
+ end
633
+
634
+ def setnx(key, value)
635
+ if exists(key)
636
+ 0
637
+ else
638
+ set(key, value)
639
+ 1
640
+ end
641
+ end
642
+
643
+ def rename(key, new_key)
644
+ return unless data[key]
645
+ data[new_key] = data[key]
646
+ data.expires[new_key] = data.expires[key] if data.expires.include?(key)
647
+ data.delete(key)
648
+ end
649
+
650
+ def renamenx(key, new_key)
651
+ if exists(new_key)
652
+ false
653
+ else
654
+ rename(key, new_key)
655
+ true
656
+ end
657
+ end
658
+
659
+ def expire(key, ttl)
660
+ return 0 unless data[key]
661
+ data.expires[key] = Time.now + ttl
662
+ 1
663
+ end
664
+
665
+ def pexpire(key, ttl)
666
+ return 0 unless data[key]
667
+ data.expires[key] = Time.now + (ttl / 1000.0)
668
+ 1
669
+ end
670
+
671
+ def ttl(key)
672
+ if data.expires.include?(key) && (ttl = data.expires[key].to_i - Time.now.to_i) > 0
673
+ ttl
674
+ else
675
+ exists(key) ? -1 : -2
676
+ end
677
+ end
678
+
679
+ def pttl(key)
680
+ if data.expires.include?(key) && (ttl = data.expires[key].to_f - Time.now.to_f) > 0
681
+ ttl * 1000
682
+ else
683
+ exists(key) ? -1 : -2
684
+ end
685
+ end
686
+
687
+ def expireat(key, timestamp)
688
+ data.expires[key] = Time.at(timestamp)
689
+ true
690
+ end
691
+
692
+ def persist(key)
693
+ !!data.expires.delete(key)
694
+ end
695
+
696
+ def hset(key, field, value)
697
+ data_type_check(key, Hash)
698
+ field = field.to_s
699
+ if data[key]
700
+ result = !data[key].include?(field)
701
+ data[key][field] = value.to_s
702
+ result ? 1 : 0
703
+ else
704
+ data[key] = { field => value.to_s }
705
+ 1
706
+ end
707
+ end
708
+
709
+ def hsetnx(key, field, value)
710
+ data_type_check(key, Hash)
711
+ field = field.to_s
712
+ return false if data[key] && data[key][field]
713
+ hset(key, field, value)
714
+ end
715
+
716
+ def hmset(key, *fields)
717
+ # mapped_hmset gives us [[:k1, "v1", :k2, "v2"]] for `fields`. Fix that.
718
+ fields = fields[0] if mapped_param?(fields)
719
+ raise_argument_error('hmset') if fields.empty?
720
+
721
+ is_list_of_arrays = fields.all?{|field| field.instance_of?(Array)}
722
+
723
+ raise_argument_error('hmset') if fields.size.odd? and !is_list_of_arrays
724
+ raise_argument_error('hmset') if is_list_of_arrays and !fields.all?{|field| field.length == 2}
725
+
726
+ data_type_check(key, Hash)
727
+ data[key] ||= {}
728
+
729
+ if is_list_of_arrays
730
+ fields.each do |pair|
731
+ data[key][pair[0].to_s] = pair[1].to_s
732
+ end
733
+ else
734
+ fields.each_slice(2) do |field|
735
+ data[key][field[0].to_s] = field[1].to_s
736
+ end
737
+ end
738
+ end
739
+
740
+ def hmget(key, *fields)
741
+ raise_argument_error('hmget') if fields.empty?
742
+
743
+ data_type_check(key, Hash)
744
+ fields.flatten.map do |field|
745
+ field = field.to_s
746
+ if data[key]
747
+ data[key][field]
748
+ else
749
+ nil
750
+ end
751
+ end
752
+ end
753
+
754
+ def hlen(key)
755
+ data_type_check(key, Hash)
756
+ return 0 unless data[key]
757
+ data[key].size
758
+ end
759
+
760
+ def hvals(key)
761
+ data_type_check(key, Hash)
762
+ return [] unless data[key]
763
+ data[key].values
764
+ end
765
+
766
+ def hincrby(key, field, increment)
767
+ data_type_check(key, Hash)
768
+ field = field.to_s
769
+ if data[key]
770
+ data[key][field] = (data[key][field].to_i + increment.to_i).to_s
771
+ else
772
+ data[key] = { field => increment.to_s }
773
+ end
774
+ data[key][field].to_i
775
+ end
776
+
777
+ def hincrbyfloat(key, field, increment)
778
+ data_type_check(key, Hash)
779
+ field = field.to_s
780
+ if data[key]
781
+ data[key][field] = (data[key][field].to_f + increment.to_f).to_s
782
+ else
783
+ data[key] = { field => increment.to_s }
784
+ end
785
+ data[key][field]
786
+ end
787
+
788
+ def hexists(key, field)
789
+ data_type_check(key, Hash)
790
+ return false unless data[key]
791
+ data[key].key?(field.to_s)
792
+ end
793
+
794
+ def sync ; end
795
+
796
+ def [](key)
797
+ get(key)
798
+ end
799
+
800
+ def []=(key, value)
801
+ set(key, value)
802
+ end
803
+
804
+ def set(key, value, *array_options)
805
+ option_nx = array_options.delete("NX")
806
+ option_xx = array_options.delete("XX")
807
+
808
+ return false if option_nx && option_xx
809
+
810
+ return false if option_nx && exists(key)
811
+ return false if option_xx && !exists(key)
812
+
813
+ data[key] = value.to_s
814
+
815
+ options = Hash[array_options.each_slice(2).to_a]
816
+ ttl_in_seconds = options["EX"] if options["EX"]
817
+ ttl_in_seconds = options["PX"] / 1000.0 if options["PX"]
818
+
819
+ expire(key, ttl_in_seconds) if ttl_in_seconds
820
+
821
+ "OK"
822
+ end
823
+
824
+ def setbit(key, offset, bit)
825
+ old_val = data[key] ? data[key].unpack('B*')[0].split("") : []
826
+ size_increment = [((offset/8)+1)*8-old_val.length, 0].max
827
+ old_val += Array.new(size_increment).map{"0"}
828
+ original_val = old_val[offset].to_i
829
+ old_val[offset] = bit.to_s
830
+ new_val = ""
831
+ old_val.each_slice(8){|b| new_val = new_val + b.join("").to_i(2).chr }
832
+ data[key] = new_val
833
+ original_val
834
+ end
835
+
836
+ def setex(key, seconds, value)
837
+ data[key] = value.to_s
838
+ expire(key, seconds)
839
+ "OK"
840
+ end
841
+
842
+ def setrange(key, offset, value)
843
+ return unless data[key]
844
+ s = data[key][offset,value.size]
845
+ data[key][s] = value
846
+ end
847
+
848
+ def mset(*pairs)
849
+ # Handle pairs for mapped_mset command
850
+ pairs = pairs[0] if mapped_param?(pairs)
851
+ raise_argument_error('mset') if pairs.empty? || pairs.size == 1
852
+ # We have to reply with a different error message here to be consistent with redis-rb 3.0.6 / redis-server 2.8.1
853
+ raise_argument_error("mset", "mset_odd") if pairs.size.odd?
854
+
855
+ pairs.each_slice(2) do |pair|
856
+ data[pair[0].to_s] = pair[1].to_s
857
+ end
858
+ "OK"
859
+ end
860
+
861
+ def msetnx(*pairs)
862
+ # Handle pairs for mapped_msetnx command
863
+ pairs = pairs[0] if mapped_param?(pairs)
864
+ keys = []
865
+ pairs.each_with_index{|item, index| keys << item.to_s if index % 2 == 0}
866
+ return false if keys.any?{|key| data.key?(key) }
867
+ mset(*pairs)
868
+ true
869
+ end
870
+
871
+ def incr(key)
872
+ data.merge!({ key => (data[key].to_i + 1).to_s || "1"})
873
+ data[key].to_i
874
+ end
875
+
876
+ def incrby(key, by)
877
+ data.merge!({ key => (data[key].to_i + by.to_i).to_s || by })
878
+ data[key].to_i
879
+ end
880
+
881
+ def incrbyfloat(key, by)
882
+ data.merge!({ key => (data[key].to_f + by.to_f).to_s || by })
883
+ data[key]
884
+ end
885
+
886
+ def decr(key)
887
+ data.merge!({ key => (data[key].to_i - 1).to_s || "-1"})
888
+ data[key].to_i
889
+ end
890
+
891
+ def decrby(key, by)
892
+ data.merge!({ key => ((data[key].to_i - by.to_i) || (by.to_i * -1)).to_s })
893
+ data[key].to_i
894
+ end
895
+
896
+ def type(key)
897
+ case data[key]
898
+ when nil then "none"
899
+ when String then "string"
900
+ when ZSet then "zset"
901
+ when Hash then "hash"
902
+ when Array then "list"
903
+ when ::Set then "set"
904
+ end
905
+ end
906
+
907
+ def quit ; end
908
+
909
+ def shutdown; end
910
+
911
+ def slaveof(host, port) ; end
912
+
913
+ def scan(start_cursor, *args)
914
+ match = "*"
915
+ count = 10
916
+
917
+ if args.size.odd?
918
+ raise_argument_error('scan')
919
+ end
920
+
921
+ if idx = args.index("MATCH")
922
+ match = args[idx + 1]
923
+ end
924
+
925
+ if idx = args.index("COUNT")
926
+ count = args[idx + 1]
927
+ end
928
+
929
+ start_cursor = start_cursor.to_i
930
+ data_type_check(start_cursor, Fixnum)
931
+
932
+ cursor = start_cursor
933
+ next_keys = []
934
+
935
+ if start_cursor + count >= data.length
936
+ next_keys = keys(match)[start_cursor..-1]
937
+ cursor = 0
938
+ else
939
+ cursor = start_cursor + 10
940
+ next_keys = keys(match)[start_cursor..cursor]
941
+ end
942
+
943
+ return "#{cursor}", next_keys
944
+ end
945
+
946
+ def zadd(key, *args)
947
+ if !args.first.is_a?(Array)
948
+ if args.size < 2
949
+ raise_argument_error('zadd')
950
+ elsif args.size.odd?
951
+ raise_syntax_error
952
+ end
953
+ else
954
+ unless args.all? {|pair| pair.size == 2 }
955
+ raise_syntax_error
956
+ end
957
+ end
958
+
959
+ data_type_check(key, ZSet)
960
+ data[key] ||= ZSet.new
961
+
962
+ if args.size == 2 && !(Array === args.first)
963
+ score, value = args
964
+ exists = !data[key].key?(value.to_s)
965
+ data[key][value.to_s] = score
966
+ else
967
+ # Turn [1, 2, 3, 4] into [[1, 2], [3, 4]] unless it is already
968
+ args = args.each_slice(2).to_a unless args.first.is_a?(Array)
969
+ exists = args.map(&:last).map { |el| data[key].key?(el.to_s) }.count(false)
970
+ args.each { |s, v| data[key][v.to_s] = s }
971
+ end
972
+
973
+ exists
974
+ end
975
+
976
+ def zrem(key, value)
977
+ data_type_check(key, ZSet)
978
+ values = Array(value)
979
+ return 0 unless data[key]
980
+
981
+ response = values.map do |v|
982
+ data[key].delete(v.to_s) if data[key].has_key?(v.to_s)
983
+ end.compact.size
984
+
985
+ remove_key_for_empty_collection(key)
986
+ response
987
+ end
988
+
989
+ def zcard(key)
990
+ data_type_check(key, ZSet)
991
+ data[key] ? data[key].size : 0
992
+ end
993
+
994
+ def zscore(key, value)
995
+ data_type_check(key, ZSet)
996
+ value = data[key] && data[key][value.to_s]
997
+ value && value.to_s
998
+ end
999
+
1000
+ def zcount(key, min, max)
1001
+ data_type_check(key, ZSet)
1002
+ return 0 unless data[key]
1003
+ data[key].select_by_score(min, max).size
1004
+ end
1005
+
1006
+ def zincrby(key, num, value)
1007
+ data_type_check(key, ZSet)
1008
+ data[key] ||= ZSet.new
1009
+ data[key][value.to_s] ||= 0
1010
+ data[key].increment(value.to_s, num)
1011
+ data[key][value.to_s].to_s
1012
+ end
1013
+
1014
+ def zrank(key, value)
1015
+ data_type_check(key, ZSet)
1016
+ z = data[key]
1017
+ return unless z
1018
+ z.keys.sort_by {|k| z[k] }.index(value.to_s)
1019
+ end
1020
+
1021
+ def zrevrank(key, value)
1022
+ data_type_check(key, ZSet)
1023
+ z = data[key]
1024
+ return unless z
1025
+ z.keys.sort_by {|k| -z[k] }.index(value.to_s)
1026
+ end
1027
+
1028
+ def zrange(key, start, stop, with_scores = nil)
1029
+ data_type_check(key, ZSet)
1030
+ return [] unless data[key]
1031
+
1032
+ results = sort_keys(data[key])
1033
+ # Select just the keys unless we want scores
1034
+ results = results.map(&:first) unless with_scores
1035
+ results[start..stop].flatten.map(&:to_s)
1036
+ end
1037
+
1038
+ def zrangebylex(key, start, stop, *opts)
1039
+ data_type_check(key, ZSet)
1040
+ return [] unless data[key]
1041
+ zset = data[key]
1042
+
1043
+ sorted = if zset.identical_scores?
1044
+ zset.keys.sort { |x, y| x.to_s <=> y.to_s }
1045
+ else
1046
+ zset.keys
1047
+ end
1048
+
1049
+ range = get_range start, stop, sorted.first, sorted.last
1050
+
1051
+ filtered = []
1052
+ sorted.each do |element|
1053
+ filtered << element if (range[0][:value]..range[1][:value]).cover?(element)
1054
+ end
1055
+ filtered.shift if filtered[0] == range[0][:value] && !range[0][:inclusive]
1056
+ filtered.pop if filtered.last == range[1][:value] && !range[1][:inclusive]
1057
+
1058
+ limit = get_limit(opts, filtered)
1059
+ if limit
1060
+ filtered = filtered[limit[0]..-1].take(limit[1])
1061
+ end
1062
+
1063
+ filtered
1064
+ end
1065
+
1066
+ def zrevrangebylex(key, start, stop, *args)
1067
+ zrangebylex(key, stop, start, args).reverse
1068
+ end
1069
+
1070
+ def zrevrange(key, start, stop, with_scores = nil)
1071
+ data_type_check(key, ZSet)
1072
+ return [] unless data[key]
1073
+
1074
+ if with_scores
1075
+ data[key].sort_by {|_,v| -v }
1076
+ else
1077
+ data[key].keys.sort_by {|k| -data[key][k] }
1078
+ end[start..stop].flatten.map(&:to_s)
1079
+ end
1080
+
1081
+ def zrangebyscore(key, min, max, *opts)
1082
+ data_type_check(key, ZSet)
1083
+ return [] unless data[key]
1084
+
1085
+ range = data[key].select_by_score(min, max)
1086
+ vals = if opts.include?('WITHSCORES')
1087
+ range.sort_by {|_,v| v }
1088
+ else
1089
+ range.keys.sort_by {|k| range[k] }
1090
+ end
1091
+
1092
+ limit = get_limit(opts, vals)
1093
+ vals = vals[*limit] if limit
1094
+
1095
+ vals.flatten.map(&:to_s)
1096
+ end
1097
+
1098
+ def zrevrangebyscore(key, max, min, *opts)
1099
+ opts = opts.flatten
1100
+ data_type_check(key, ZSet)
1101
+ return [] unless data[key]
1102
+
1103
+ range = data[key].select_by_score(min, max)
1104
+ vals = if opts.include?('WITHSCORES')
1105
+ range.sort_by {|_,v| -v }
1106
+ else
1107
+ range.keys.sort_by {|k| -range[k] }
1108
+ end
1109
+
1110
+ limit = get_limit(opts, vals)
1111
+ vals = vals[*limit] if limit
1112
+
1113
+ vals.flatten.map(&:to_s)
1114
+ end
1115
+
1116
+ def zremrangebyscore(key, min, max)
1117
+ data_type_check(key, ZSet)
1118
+ return 0 unless data[key]
1119
+
1120
+ range = data[key].select_by_score(min, max)
1121
+ range.each {|k,_| data[key].delete(k) }
1122
+ range.size
1123
+ end
1124
+
1125
+ def zremrangebyrank(key, start, stop)
1126
+ data_type_check(key, ZSet)
1127
+ return 0 unless data[key]
1128
+
1129
+ sorted_elements = data[key].sort_by { |k, v| v }
1130
+ start = sorted_elements.length if start > sorted_elements.length
1131
+ elements_to_delete = sorted_elements[start..stop]
1132
+ elements_to_delete.each { |elem, rank| data[key].delete(elem) }
1133
+ elements_to_delete.size
1134
+ end
1135
+
1136
+ def zinterstore(out, *args)
1137
+ data_type_check(out, ZSet)
1138
+ args_handler = SortedSetArgumentHandler.new(args)
1139
+ data[out] = SortedSetIntersectStore.new(args_handler, data).call
1140
+ data[out].size
1141
+ end
1142
+
1143
+ def zunionstore(out, *args)
1144
+ data_type_check(out, ZSet)
1145
+ args_handler = SortedSetArgumentHandler.new(args)
1146
+ data[out] = SortedSetUnionStore.new(args_handler, data).call
1147
+ data[out].size
1148
+ end
1149
+
1150
+ def subscribe(*channels)
1151
+ raise_argument_error('subscribe') if channels.empty?()
1152
+
1153
+ #Create messages for all data from the channels
1154
+ channel_replies = channels.map do |channel|
1155
+ self.class.channels[channel].slice!(0..-1).map!{|v| ["message", channel, v]}
1156
+ end
1157
+ channel_replies.flatten!(1)
1158
+ channel_replies.compact!()
1159
+
1160
+ #Put messages into the replies for the future
1161
+ channels.each_with_index do |channel,index|
1162
+ replies << ["subscribe", channel, index+1]
1163
+ end
1164
+ replies.push(*channel_replies)
1165
+
1166
+ #Add unsubscribe message to stop blocking (see https://github.com/redis/redis-rb/blob/v3.2.1/lib/redis/subscribe.rb#L38)
1167
+ replies.push(self.unsubscribe())
1168
+
1169
+ replies.pop() #Last reply will be pushed back on
1170
+ end
1171
+
1172
+ def psubscribe(*patterns)
1173
+ raise_argument_error('psubscribe') if patterns.empty?()
1174
+
1175
+ #Create messages for all data from the channels
1176
+ channel_replies = self.class.channels.keys.map do |channel|
1177
+ pattern = patterns.find{|p| File.fnmatch(p, channel) }
1178
+ unless pattern.nil?()
1179
+ self.class.channels[channel].slice!(0..-1).map!{|v| ["pmessage", pattern, channel, v]}
1180
+ end
1181
+ end
1182
+ channel_replies.flatten!(1)
1183
+ channel_replies.compact!()
1184
+
1185
+ #Put messages into the replies for the future
1186
+ patterns.each_with_index do |pattern,index|
1187
+ replies << ["psubscribe", pattern, index+1]
1188
+ end
1189
+ replies.push(*channel_replies)
1190
+
1191
+ #Add unsubscribe to stop blocking
1192
+ replies.push(self.punsubscribe())
1193
+
1194
+ replies.pop() #Last reply will be pushed back on
1195
+ end
1196
+
1197
+ def publish(channel, message)
1198
+ self.class.channels[channel] << message
1199
+ 0 #Just fake number of subscribers
1200
+ end
1201
+
1202
+ def unsubscribe(*channels)
1203
+ if channels.empty?()
1204
+ replies << ["unsubscribe", nil, 0]
1205
+ else
1206
+ channels.each do |channel|
1207
+ replies << ["unsubscribe", channel, 0]
1208
+ end
1209
+ end
1210
+ replies.pop() #Last reply will be pushed back on
1211
+ end
1212
+
1213
+ def punsubscribe(*patterns)
1214
+ if patterns.empty?()
1215
+ replies << ["punsubscribe", nil, 0]
1216
+ else
1217
+ patterns.each do |pattern|
1218
+ replies << ["punsubscribe", pattern, 0]
1219
+ end
1220
+ end
1221
+ replies.pop() #Last reply will be pushed back on
1222
+ end
1223
+
1224
+ def zscan(key, start_cursor, *args)
1225
+ data_type_check(key, ZSet)
1226
+ return [] unless data[key]
1227
+
1228
+ match = "*"
1229
+ count = 10
1230
+
1231
+ if args.size.odd?
1232
+ raise_argument_error('zscan')
1233
+ end
1234
+
1235
+ if idx = args.index("MATCH")
1236
+ match = args[idx + 1]
1237
+ end
1238
+
1239
+ if idx = args.index("COUNT")
1240
+ count = args[idx + 1]
1241
+ end
1242
+
1243
+ start_cursor = start_cursor.to_i
1244
+ data_type_check(start_cursor, Fixnum)
1245
+
1246
+ cursor = start_cursor
1247
+ next_keys = []
1248
+
1249
+ sorted_keys = sort_keys(data[key])
1250
+
1251
+ if start_cursor + count >= sorted_keys.length
1252
+ next_keys = sorted_keys.to_a.select { |k| File.fnmatch(match, k[0]) } [start_cursor..-1]
1253
+ cursor = 0
1254
+ else
1255
+ cursor = start_cursor + count
1256
+ next_keys = sorted_keys.to_a.select { |k| File.fnmatch(match, k[0]) } [start_cursor..cursor-1]
1257
+ end
1258
+ return "#{cursor}", next_keys.flatten.map(&:to_s)
1259
+ end
1260
+
1261
+ # Originally from redis-rb
1262
+ def zscan_each(key, *args, &block)
1263
+ data_type_check(key, ZSet)
1264
+ return [] unless data[key]
1265
+
1266
+ return to_enum(:zscan_each, key, options) unless block_given?
1267
+ cursor = 0
1268
+ loop do
1269
+ cursor, values = zscan(key, cursor, options)
1270
+ values.each(&block)
1271
+ break if cursor == "0"
1272
+ end
1273
+ end
1274
+
1275
+ private
1276
+ def raise_argument_error(command, match_string=command)
1277
+ error_message = if %w(hmset mset_odd).include?(match_string.downcase)
1278
+ "ERR wrong number of arguments for #{command.upcase}"
1279
+ else
1280
+ "ERR wrong number of arguments for '#{command}' command"
1281
+ end
1282
+
1283
+ raise Redis::CommandError, error_message
1284
+ end
1285
+
1286
+ def raise_syntax_error
1287
+ raise Redis::CommandError, "ERR syntax error"
1288
+ end
1289
+
1290
+ def remove_key_for_empty_collection(key)
1291
+ del(key) if data[key] && data[key].empty?
1292
+ end
1293
+
1294
+ def data_type_check(key, klass)
1295
+ if data[key] && !data[key].is_a?(klass)
1296
+ warn "Operation against a key holding the wrong kind of value: Expected #{klass} at #{key}."
1297
+ raise Redis::CommandError.new("WRONGTYPE Operation against a key holding the wrong kind of value")
1298
+ end
1299
+ end
1300
+
1301
+ def get_range(start, stop, min = -Float::INFINITY, max = Float::INFINITY)
1302
+ range_options = []
1303
+
1304
+ [start, stop].each do |value|
1305
+ case value[0]
1306
+ when "-"
1307
+ range_options << { value: min, inclusive: true }
1308
+ when "+"
1309
+ range_options << { value: max, inclusive: true }
1310
+ when "["
1311
+ range_options << { value: value[1..-1], inclusive: true }
1312
+ when "("
1313
+ range_options << { value: value[1..-1], inclusive: false }
1314
+ else
1315
+ raise Redis::CommandError, "ERR min or max not valid string range item"
1316
+ end
1317
+ end
1318
+
1319
+ range_options
1320
+ end
1321
+
1322
+ def get_limit(opts, vals)
1323
+ index = opts.index('LIMIT')
1324
+
1325
+ if index
1326
+ offset = opts[index + 1]
1327
+
1328
+ count = opts[index + 2]
1329
+ count = vals.size if count < 0
1330
+
1331
+ [offset, count]
1332
+ end
1333
+ end
1334
+
1335
+ def mapped_param? param
1336
+ param.size == 1 && param[0].is_a?(Array)
1337
+ end
1338
+ # NOTE : Redis-rb 3.x will flatten *args, so method(["a", "b", "c"])
1339
+ # should be handled the same way as method("a", "b", "c")
1340
+ alias_method :flatten?, :mapped_param?
1341
+
1342
+ def srandmember_single(key)
1343
+ data_type_check(key, ::Set)
1344
+ return nil unless data[key]
1345
+ data[key].to_a[rand(data[key].size)]
1346
+ end
1347
+
1348
+ def srandmember_multiple(key, number)
1349
+ return [] unless data[key]
1350
+ if number >= 0
1351
+ # replace with `data[key].to_a.sample(number)` when 1.8.7 is deprecated
1352
+ (1..number).inject([]) do |selected, _|
1353
+ available_elements = data[key].to_a - selected
1354
+ selected << available_elements[rand(available_elements.size)]
1355
+ end.compact
1356
+ else
1357
+ (1..-number).map { data[key].to_a[rand(data[key].size)] }.flatten
1358
+ end
1359
+ end
1360
+
1361
+ def sort_keys(arr)
1362
+ # Sort by score, or if scores are equal, key alphanum
1363
+ sorted_keys = arr.sort do |(k1, v1), (k2, v2)|
1364
+ if v1 == v2
1365
+ k1 <=> k2
1366
+ else
1367
+ v1 <=> v2
1368
+ end
1369
+ end
1370
+ end
1371
+ end
1372
+ end
1373
+ end
1374
+
1375
+ Redis::Connection.drivers << Redis::Connection::Memory