kuende-fakeredis 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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