fakeredis 0.4.1 → 0.4.2

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.
@@ -2,7 +2,7 @@ rvm:
2
2
  - 1.8.7
3
3
  - 1.9.2
4
4
  - 1.9.3
5
- - ruby-head
5
+ - 2.0.0
6
6
  - ree
7
7
  - jruby
8
- - rbx
8
+ - rbx-19mode
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011-2012 Guillermo Iguaran
1
+ Copyright (c) 2011-2013 Guillermo Iguaran
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # FakeRedis [![Build Status](http://travis-ci.org/guilleiguaran/fakeredis.png)](http://travis-ci.org/guilleiguaran/fakeredis)
1
+ # FakeRedis [![Build Status](https://secure.travis-ci.org/guilleiguaran/fakeredis.png)](http://travis-ci.org/guilleiguaran/fakeredis)
2
2
  This a fake implementation of redis-rb for machines without Redis or test environments
3
3
 
4
4
 
@@ -65,6 +65,7 @@ Or:
65
65
  * [jredville](https://github.com/jredville)
66
66
  * [redsquirrel](https://github.com/redsquirrel)
67
67
  * [dpick](https://github.com/dpick)
68
+ * [caius](https://github.com/caius)
68
69
  * [Travis-CI](http://travis-ci.org/) (Travis-CI also uses Fakeredis in its tests!!!)
69
70
 
70
71
 
@@ -81,5 +82,5 @@ Or:
81
82
 
82
83
  ## Copyright
83
84
 
84
- Copyright (c) 2011-2012 Guillermo Iguaran. See LICENSE for
85
+ Copyright (c) 2011-2013 Guillermo Iguaran. See LICENSE for
85
86
  further details.
@@ -8,12 +8,11 @@ Gem::Specification.new do |s|
8
8
  s.platform = Gem::Platform::RUBY
9
9
  s.authors = ["Guillermo Iguaran"]
10
10
  s.email = ["guilleiguaran@gmail.com"]
11
- s.homepage = "https://github.com/guilleiguaran/fakeredis"
11
+ s.homepage = "https://guilleiguaran.github.com/fakeredis"
12
+ s.license = "MIT"
12
13
  s.summary = %q{Fake (In-memory) driver for redis-rb.}
13
14
  s.description = %q{Fake (In-memory) driver for redis-rb. Useful for testing environment and machines without Redis.}
14
15
 
15
- s.rubyforge_project = "fakeredis"
16
-
17
16
  s.files = `git ls-files`.split("\n")
18
17
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
@@ -0,0 +1 @@
1
+ require "fakeredis"
@@ -0,0 +1,70 @@
1
+ module FakeRedis
2
+ # Represents a normal hash with some additional expiration information
3
+ # associated with each key
4
+ class ExpiringHash < Hash
5
+ attr_reader :expires
6
+
7
+ def initialize(*)
8
+ super
9
+ @expires = {}
10
+ end
11
+
12
+ def [](key)
13
+ key = normalize key
14
+ delete(key) if expired?(key)
15
+ super
16
+ end
17
+
18
+ def []=(key, val)
19
+ key = normalize key
20
+ expire(key)
21
+ super
22
+ end
23
+
24
+ def delete(key)
25
+ key = normalize key
26
+ expire(key)
27
+ super
28
+ end
29
+
30
+ def expire(key)
31
+ key = normalize key
32
+ expires.delete(key)
33
+ end
34
+
35
+ def expired?(key)
36
+ key = normalize key
37
+ expires.include?(key) && expires[key] < Time.now
38
+ end
39
+
40
+ def key?(key)
41
+ key = normalize key
42
+ delete(key) if expired?(key)
43
+ super
44
+ end
45
+
46
+ def values_at(*keys)
47
+ keys.each do |key|
48
+ key = normalize(key)
49
+ delete(key) if expired?(key)
50
+ end
51
+ super
52
+ end
53
+
54
+ def keys
55
+ super.select do |key|
56
+ key = normalize(key)
57
+ if expired?(key)
58
+ delete(key)
59
+ false
60
+ else
61
+ true
62
+ end
63
+ end
64
+ end
65
+
66
+ def normalize key
67
+ key.to_s
68
+ end
69
+ end
70
+ end
@@ -17,7 +17,7 @@ require 'fakeredis'
17
17
  RSpec.configure do |c|
18
18
 
19
19
  c.before do
20
- Redis::Connection::Memory.instances.values.each &:flushdb
20
+ Redis::Connection::Memory.reset_all_databases
21
21
  end
22
22
 
23
23
  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
@@ -1,3 +1,3 @@
1
1
  module FakeRedis
2
- VERSION = "0.4.1"
2
+ VERSION = "0.4.2"
3
3
  end
@@ -0,0 +1,29 @@
1
+ module FakeRedis
2
+ class ZSet < Hash
3
+
4
+ def []=(key, val)
5
+ super(key, _floatify(val))
6
+ end
7
+
8
+ # Increments the value of key by val
9
+ def increment(key, val)
10
+ self[key] += _floatify(val)
11
+ end
12
+
13
+ def select_by_score min, max
14
+ min = _floatify(min)
15
+ max = _floatify(max)
16
+ reject {|_,v| v < min || v > max }
17
+ end
18
+
19
+ # Originally lifted from redis-rb
20
+ def _floatify(str)
21
+ if (( inf = str.to_s.match(/^([+-])?inf/i) ))
22
+ (inf[1] == "-" ? -1.0 : 1.0) / 0.0
23
+ else
24
+ Float str
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -1,104 +1,88 @@
1
1
  require 'set'
2
2
  require 'redis/connection/registry'
3
3
  require 'redis/connection/command_helper'
4
+ require "fakeredis/expiring_hash"
5
+ require "fakeredis/sorted_set_argument_handler"
6
+ require "fakeredis/sorted_set_store"
7
+ require "fakeredis/zset"
4
8
 
5
9
  class Redis
6
10
  module Connection
7
11
  class Memory
8
- # Represents a normal hash with some additional expiration information
9
- # associated with each key
10
- class ExpiringHash < Hash
11
- attr_reader :expires
12
-
13
- def initialize(*)
14
- super
15
- @expires = {}
16
- end
17
-
18
- def [](key)
19
- delete(key) if expired?(key)
20
- super
21
- end
22
-
23
- def []=(key, val)
24
- expire(key)
25
- super
26
- end
12
+ include Redis::Connection::CommandHelper
13
+ include FakeRedis
27
14
 
28
- def delete(key)
29
- expire(key)
30
- super
31
- end
15
+ attr_accessor :buffer, :options
32
16
 
33
- def expire(key)
34
- expires.delete(key)
35
- end
17
+ # Tracks all databases for all instances across the current process.
18
+ # We have to be able to handle two clients with the same host/port accessing
19
+ # different databases at once without overwriting each other. So we store our
20
+ # "data" outside the client instances, in this class level instance method.
21
+ # Client instances access it with a key made up of their host/port, and then select
22
+ # which DB out of the array of them they want. Allows the access we need.
23
+ def self.databases
24
+ @databases ||= Hash.new {|h,k| h[k] = [] }
25
+ end
36
26
 
37
- def expired?(key)
38
- expires.include?(key) && expires[key] < Time.now
39
- end
27
+ # Used for resetting everything in specs
28
+ def self.reset_all_databases
29
+ @databases = nil
30
+ end
40
31
 
41
- def key?(key)
42
- delete(key) if expired?(key)
43
- super
44
- end
32
+ def self.connect(options = {})
33
+ new(options)
34
+ end
45
35
 
46
- def values_at(*keys)
47
- keys.each {|key| delete(key) if expired?(key)}
48
- super
49
- end
36
+ def initialize(options = {})
37
+ self.options = options
38
+ end
50
39
 
51
- def keys
52
- super.select do |key|
53
- if expired?(key)
54
- delete(key)
55
- false
56
- else
57
- true
58
- end
59
- end
60
- end
40
+ def database_id
41
+ @database_id ||= 0
61
42
  end
43
+ attr_writer :database_id
62
44
 
63
- class ZSet < Hash
45
+ def database_instance_key
46
+ [options[:host], options[:port]].hash
64
47
  end
65
48
 
66
- include Redis::Connection::CommandHelper
49
+ def databases
50
+ self.class.databases[database_instance_key]
51
+ end
67
52
 
68
- def initialize(connected = false)
69
- @data = ExpiringHash.new
70
- @connected = connected
71
- @replies = []
72
- @buffer = nil
53
+ def find_database id=database_id
54
+ databases[id] ||= ExpiringHash.new
73
55
  end
74
56
 
75
- def connected?
76
- @connected
57
+ def data
58
+ find_database
77
59
  end
78
60
 
79
- def self.instances
80
- @instances ||= {}
61
+ def replies
62
+ @replies ||= []
81
63
  end
64
+ attr_writer :replies
82
65
 
83
- def self.connect(options = {})
84
- self.instances[options] ||= self.new(true)
66
+ def connected?
67
+ true
85
68
  end
86
69
 
87
70
  def connect_unix(path, timeout)
88
- @connected = true
89
71
  end
90
72
 
91
73
  def disconnect
92
- @connected = false
93
- nil
94
74
  end
95
75
 
96
76
  def timeout=(usecs)
97
77
  end
98
78
 
99
79
  def write(command)
100
- method = command.shift
101
- reply = send(method, *command)
80
+ meffod = command.shift
81
+ if respond_to?(meffod)
82
+ reply = send(meffod, *command)
83
+ else
84
+ raise Redis::CommandError, "ERR unknown command '#{meffod}'"
85
+ end
102
86
 
103
87
  if reply == true
104
88
  reply = 1
@@ -106,13 +90,13 @@ class Redis
106
90
  reply = 0
107
91
  end
108
92
 
109
- @replies << reply
110
- @buffer << reply if @buffer && method != :multi
93
+ replies << reply
94
+ buffer << reply if buffer && meffod != :multi
111
95
  nil
112
96
  end
113
97
 
114
98
  def read
115
- @replies.shift
99
+ replies.shift
116
100
  end
117
101
 
118
102
  # NOT IMPLEMENTED:
@@ -120,25 +104,30 @@ class Redis
120
104
  # * brpop
121
105
  # * brpoplpush
122
106
  # * discard
123
- # * move
107
+ # * sort
124
108
  # * subscribe
125
109
  # * psubscribe
126
110
  # * publish
127
- # * zremrangebyrank
128
- # * zunionstore
111
+
129
112
  def flushdb
130
- @data = ExpiringHash.new
113
+ databases.delete_at(database_id)
114
+ "OK"
131
115
  end
132
116
 
133
117
  def flushall
134
- flushdb
118
+ self.class.databases[database_instance_key] = []
119
+ "OK"
135
120
  end
136
121
 
137
122
  def auth(password)
138
123
  "OK"
139
124
  end
140
125
 
141
- def select(index) ; end
126
+ def select(index)
127
+ data_type_check(index, Integer)
128
+ self.database_id = index
129
+ "OK"
130
+ end
142
131
 
143
132
  def info
144
133
  {
@@ -163,71 +152,84 @@ class Redis
163
152
 
164
153
  def bgreriteaof ; end
165
154
 
155
+ def move key, destination_id
156
+ raise Redis::CommandError, "ERR source and destination objects are the same" if destination_id == database_id
157
+ destination = find_database(destination_id)
158
+ return false unless data.has_key?(key)
159
+ return false if destination.has_key?(key)
160
+ destination[key] = data.delete(key)
161
+ true
162
+ end
163
+
166
164
  def get(key)
167
- @data[key]
165
+ data_type_check(key, String)
166
+ data[key]
168
167
  end
169
168
 
170
169
  def getbit(key, offset)
171
- return unless @data[key]
172
- @data[key].unpack('B*')[0].split("")[offset].to_i
170
+ return unless data[key]
171
+ data[key].unpack('B*')[0].split("")[offset].to_i
173
172
  end
174
173
 
175
174
  def getrange(key, start, ending)
176
- return unless @data[key]
177
- @data[key][start..ending]
175
+ return unless data[key]
176
+ data[key][start..ending]
178
177
  end
179
178
  alias :substr :getrange
180
179
 
181
180
  def getset(key, value)
182
- old_value = @data[key]
183
- @data[key] = value
184
- return old_value
181
+ data_type_check(key, String)
182
+ data[key].tap do
183
+ set(key, value)
184
+ end
185
185
  end
186
186
 
187
187
  def mget(*keys)
188
188
  raise Redis::CommandError, "wrong number of arguments for 'mget' command" if keys.empty?
189
- @data.values_at(*keys)
189
+ # We work with either an array, or list of arguments
190
+ keys = keys.first if keys.size == 1
191
+ data.values_at(*keys)
190
192
  end
191
193
 
192
194
  def append(key, value)
193
- @data[key] = (@data[key] || "")
194
- @data[key] = @data[key] + value.to_s
195
+ data[key] = (data[key] || "")
196
+ data[key] = data[key] + value.to_s
195
197
  end
196
198
 
197
199
  def strlen(key)
198
- return unless @data[key]
199
- @data[key].size
200
+ return unless data[key]
201
+ data[key].size
200
202
  end
201
203
 
202
204
  def hgetall(key)
203
205
  data_type_check(key, Hash)
204
- @data[key].to_a.flatten || {}
206
+ data[key].to_a.flatten || {}
205
207
  end
206
208
 
207
209
  def hget(key, field)
208
210
  data_type_check(key, Hash)
209
- @data[key] && @data[key][field.to_s]
211
+ data[key] && data[key][field.to_s]
210
212
  end
211
213
 
212
214
  def hdel(key, field)
213
215
  data_type_check(key, Hash)
214
- @data[key] && @data[key].delete(field)
216
+ data[key] && data[key].delete(field)
215
217
  remove_key_for_empty_collection(key)
216
218
  end
217
219
 
218
220
  def hkeys(key)
219
221
  data_type_check(key, Hash)
220
- return [] if @data[key].nil?
221
- @data[key].keys
222
+ return [] if data[key].nil?
223
+ data[key].keys
222
224
  end
223
225
 
224
226
  def keys(pattern = "*")
225
227
  regexp = Regexp.new(pattern.split("*").map { |r| Regexp.escape(r) }.join(".*"))
226
- @data.keys.select { |key| key =~ regexp }
228
+ data.keys.select { |key| key =~ regexp }
227
229
  end
228
230
 
229
231
  def randomkey
230
- @data.keys[rand(dbsize)]
232
+ data.keys[rand(dbsize)]
231
233
  end
232
234
 
233
235
  def echo(string)
@@ -243,66 +245,66 @@ class Redis
243
245
  end
244
246
 
245
247
  def dbsize
246
- @data.keys.count
248
+ data.keys.count
247
249
  end
248
250
 
249
251
  def exists(key)
250
- @data.key?(key)
252
+ data.key?(key)
251
253
  end
252
254
 
253
255
  def llen(key)
254
256
  data_type_check(key, Array)
255
- return 0 unless @data[key]
256
- @data[key].size
257
+ return 0 unless data[key]
258
+ data[key].size
257
259
  end
258
260
 
259
261
  def lrange(key, startidx, endidx)
260
262
  data_type_check(key, Array)
261
- (@data[key] && @data[key][startidx..endidx]) || []
263
+ (data[key] && data[key][startidx..endidx]) || []
262
264
  end
263
265
 
264
266
  def ltrim(key, start, stop)
265
267
  data_type_check(key, Array)
266
- return unless @data[key]
267
- @data[key] = @data[key][start..stop]
268
+ return unless data[key]
269
+ data[key] = data[key][start..stop]
268
270
  end
269
271
 
270
272
  def lindex(key, index)
271
273
  data_type_check(key, Array)
272
- @data[key] && @data[key][index]
274
+ data[key] && data[key][index]
273
275
  end
274
276
 
275
277
  def linsert(key, where, pivot, value)
276
278
  data_type_check(key, Array)
277
- return unless @data[key]
278
- index = @data[key].index(pivot)
279
+ return unless data[key]
280
+ index = data[key].index(pivot)
279
281
  case where
280
- when :before then @data[key].insert(index, value)
281
- when :after then @data[key].insert(index + 1, value)
282
+ when :before then data[key].insert(index, value)
283
+ when :after then data[key].insert(index + 1, value)
282
284
  else raise Redis::CommandError, "ERR syntax error"
283
285
  end
284
286
  end
285
287
 
286
288
  def lset(key, index, value)
287
289
  data_type_check(key, Array)
288
- return unless @data[key]
289
- raise RuntimeError if index >= @data[key].size
290
- @data[key][index] = value
290
+ return unless data[key]
291
+ raise(Redis::CommandError, "ERR index out of range") if index >= data[key].size
292
+ data[key][index] = value
291
293
  end
292
294
 
293
295
  def lrem(key, count, value)
294
296
  data_type_check(key, Array)
295
- return unless @data[key]
296
- old_size = @data[key].size
297
+ return unless data[key]
298
+ old_size = data[key].size
297
299
  diff =
298
300
  if count == 0
299
- @data[key].delete(value)
300
- old_size - @data[key].size
301
+ data[key].delete(value)
302
+ old_size - data[key].size
301
303
  else
302
- array = count > 0 ? @data[key].dup : @data[key].reverse
304
+ array = count > 0 ? data[key].dup : data[key].reverse
303
305
  count.abs.times{ array.delete_at(array.index(value) || array.length) }
304
- @data[key] = count > 0 ? array.dup : array.reverse
305
- old_size - @data[key].size
306
+ data[key] = count > 0 ? array.dup : array.reverse
307
+ old_size - data[key].size
306
308
  end
307
309
  remove_key_for_empty_collection(key)
308
310
  diff
@@ -310,75 +312,76 @@ class Redis
310
312
 
311
313
  def rpush(key, value)
312
314
  data_type_check(key, Array)
313
- @data[key] ||= []
315
+ data[key] ||= []
314
316
  [value].flatten.each do |val|
315
- @data[key].push(val.to_s)
317
+ data[key].push(val.to_s)
316
318
  end
317
- @data[key].size
319
+ data[key].size
318
320
  end
319
321
 
320
322
  def rpushx(key, value)
321
323
  data_type_check(key, Array)
322
- return unless @data[key]
324
+ return unless data[key]
323
325
  rpush(key, value)
324
326
  end
325
327
 
326
328
  def lpush(key, value)
327
329
  data_type_check(key, Array)
328
- @data[key] ||= []
330
+ data[key] ||= []
329
331
  [value].flatten.each do |val|
330
- @data[key].unshift(val.to_s)
332
+ data[key].unshift(val.to_s)
331
333
  end
332
- @data[key].size
334
+ data[key].size
333
335
  end
334
336
 
335
337
  def lpushx(key, value)
336
338
  data_type_check(key, Array)
337
- return unless @data[key]
339
+ return unless data[key]
338
340
  lpush(key, value)
339
341
  end
340
342
 
341
343
  def rpop(key)
342
344
  data_type_check(key, Array)
343
- return unless @data[key]
344
- @data[key].pop
345
+ return unless data[key]
346
+ data[key].pop
345
347
  end
346
348
 
347
349
  def rpoplpush(key1, key2)
348
350
  data_type_check(key1, Array)
349
- elem = rpop(key1)
350
- lpush(key2, elem)
351
+ rpop(key1).tap do |elem|
352
+ lpush(key2, elem)
353
+ end
351
354
  end
352
355
 
353
356
  def lpop(key)
354
357
  data_type_check(key, Array)
355
- return unless @data[key]
356
- @data[key].shift
358
+ return unless data[key]
359
+ data[key].shift
357
360
  end
358
361
 
359
362
  def smembers(key)
360
363
  data_type_check(key, ::Set)
361
- return [] unless @data[key]
362
- @data[key].to_a.reverse
364
+ return [] unless data[key]
365
+ data[key].to_a.reverse
363
366
  end
364
367
 
365
368
  def sismember(key, value)
366
369
  data_type_check(key, ::Set)
367
- return false unless @data[key]
368
- @data[key].include?(value.to_s)
370
+ return false unless data[key]
371
+ data[key].include?(value.to_s)
369
372
  end
370
373
 
371
374
  def sadd(key, value)
372
375
  data_type_check(key, ::Set)
373
376
  value = Array(value)
374
377
 
375
- result = if @data[key]
376
- old_set = @data[key].dup
377
- @data[key].merge(value.map(&:to_s))
378
- (@data[key] - old_set).size
378
+ result = if data[key]
379
+ old_set = data[key].dup
380
+ data[key].merge(value.map(&:to_s))
381
+ (data[key] - old_set).size
379
382
  else
380
- @data[key] = ::Set.new(value.map(&:to_s))
381
- @data[key].size
383
+ data[key] = ::Set.new(value.map(&:to_s))
384
+ data[key].size
382
385
  end
383
386
 
384
387
  # 0 = false, 1 = true, 2+ untouched
@@ -388,7 +391,7 @@ class Redis
388
391
 
389
392
  def srem(key, value)
390
393
  data_type_check(key, ::Set)
391
- deleted = !!(@data[key] && @data[key].delete?(value.to_s))
394
+ deleted = !!(data[key] && data[key].delete?(value.to_s))
392
395
  remove_key_for_empty_collection(key)
393
396
  deleted
394
397
  end
@@ -409,14 +412,14 @@ class Redis
409
412
 
410
413
  def scard(key)
411
414
  data_type_check(key, ::Set)
412
- return 0 unless @data[key]
413
- @data[key].size
415
+ return 0 unless data[key]
416
+ data[key].size
414
417
  end
415
418
 
416
419
  def sinter(*keys)
417
420
  keys.each { |k| data_type_check(k, ::Set) }
418
- return ::Set.new if keys.any? { |k| @data[k].nil? }
419
- keys = keys.map { |k| @data[k] || ::Set.new }
421
+ return ::Set.new if keys.any? { |k| data[k].nil? }
422
+ keys = keys.map { |k| data[k] || ::Set.new }
420
423
  keys.inject do |set, key|
421
424
  set & key
422
425
  end.to_a
@@ -425,12 +428,12 @@ class Redis
425
428
  def sinterstore(destination, *keys)
426
429
  data_type_check(destination, ::Set)
427
430
  result = sinter(*keys)
428
- @data[destination] = ::Set.new(result)
431
+ data[destination] = ::Set.new(result)
429
432
  end
430
433
 
431
434
  def sunion(*keys)
432
435
  keys.each { |k| data_type_check(k, ::Set) }
433
- keys = keys.map { |k| @data[k] || ::Set.new }
436
+ keys = keys.map { |k| data[k] || ::Set.new }
434
437
  keys.inject(::Set.new) do |set, key|
435
438
  set | key
436
439
  end.to_a
@@ -439,13 +442,13 @@ class Redis
439
442
  def sunionstore(destination, *keys)
440
443
  data_type_check(destination, ::Set)
441
444
  result = sunion(*keys)
442
- @data[destination] = ::Set.new(result)
445
+ data[destination] = ::Set.new(result)
443
446
  end
444
447
 
445
448
  def sdiff(key1, *keys)
446
449
  [key1, *keys].each { |k| data_type_check(k, ::Set) }
447
- keys = keys.map { |k| @data[k] || ::Set.new }
448
- keys.inject(@data[key1]) do |memo, set|
450
+ keys = keys.map { |k| data[k] || ::Set.new }
451
+ keys.inject(data[key1]) do |memo, set|
449
452
  memo - set
450
453
  end.to_a
451
454
  end
@@ -453,23 +456,23 @@ class Redis
453
456
  def sdiffstore(destination, key1, *keys)
454
457
  data_type_check(destination, ::Set)
455
458
  result = sdiff(key1, *keys)
456
- @data[destination] = ::Set.new(result)
459
+ data[destination] = ::Set.new(result)
457
460
  end
458
461
 
459
462
  def srandmember(key)
460
463
  data_type_check(key, ::Set)
461
- return nil unless @data[key]
462
- @data[key].to_a[rand(@data[key].size)]
464
+ return nil unless data[key]
465
+ data[key].to_a[rand(data[key].size)]
463
466
  end
464
467
 
465
468
  def del(*keys)
466
469
  keys = keys.flatten(1)
467
470
  raise Redis::CommandError, "ERR wrong number of arguments for 'del' command" if keys.empty?
468
- old_count = @data.keys.size
471
+ old_count = data.keys.size
469
472
  keys.each do |key|
470
- @data.delete(key)
473
+ data.delete(key)
471
474
  end
472
- deleted_count = old_count - @data.keys.size
475
+ old_count - data.keys.size
473
476
  end
474
477
 
475
478
  def setnx(key, value)
@@ -482,10 +485,10 @@ class Redis
482
485
  end
483
486
 
484
487
  def rename(key, new_key)
485
- return unless @data[key]
486
- @data[new_key] = @data[key]
487
- @data.expires[new_key] = @data.expires[key] if @data.expires.include?(key)
488
- @data.delete(key)
488
+ return unless data[key]
489
+ data[new_key] = data[key]
490
+ data.expires[new_key] = data.expires[key] if data.expires.include?(key)
491
+ data.delete(key)
489
492
  end
490
493
 
491
494
  def renamenx(key, new_key)
@@ -498,13 +501,13 @@ class Redis
498
501
  end
499
502
 
500
503
  def expire(key, ttl)
501
- return unless @data[key]
502
- @data.expires[key] = Time.now + ttl
504
+ return unless data[key]
505
+ data.expires[key] = Time.now + ttl
503
506
  true
504
507
  end
505
508
 
506
509
  def ttl(key)
507
- if @data.expires.include?(key) && (ttl = @data.expires[key].to_i - Time.now.to_i) > 0
510
+ if data.expires.include?(key) && (ttl = data.expires[key].to_i - Time.now.to_i) > 0
508
511
  ttl
509
512
  else
510
513
  -1
@@ -512,23 +515,23 @@ class Redis
512
515
  end
513
516
 
514
517
  def expireat(key, timestamp)
515
- @data.expires[key] = Time.at(timestamp)
518
+ data.expires[key] = Time.at(timestamp)
516
519
  true
517
520
  end
518
521
 
519
522
  def persist(key)
520
- !!@data.expires.delete(key)
523
+ !!data.expires.delete(key)
521
524
  end
522
525
 
523
526
  def hset(key, field, value)
524
527
  data_type_check(key, Hash)
525
528
  field = field.to_s
526
- if @data[key]
527
- result = !@data[key].include?(field)
528
- @data[key][field] = value.to_s
529
+ if data[key]
530
+ result = !data[key].include?(field)
531
+ data[key][field] = value.to_s
529
532
  result
530
533
  else
531
- @data[key] = { field => value.to_s }
534
+ data[key] = { field => value.to_s }
532
535
  true
533
536
  end
534
537
  end
@@ -536,27 +539,28 @@ class Redis
536
539
  def hsetnx(key, field, value)
537
540
  data_type_check(key, Hash)
538
541
  field = field.to_s
539
- return false if @data[key] && @data[key][field]
542
+ return false if data[key] && data[key][field]
540
543
  hset(key, field, value)
541
544
  end
542
545
 
543
546
  def hmset(key, *fields)
544
- raise Redis::CommandError, "wrong number of arguments for 'hmset' command" if fields.empty? || fields.size.odd?
547
+ # mapped_hmset gives us [[:k1, "v1", :k2, "v2"]] for `fields`. Fix that.
548
+ fields = fields[0] if mapped_param?(fields)
549
+ raise Redis::CommandError, "ERR wrong number of arguments for HMSET" if fields.empty? || fields.size.odd?
545
550
  data_type_check(key, Hash)
546
- @data[key] ||= {}
551
+ data[key] ||= {}
547
552
  fields.each_slice(2) do |field|
548
- @data[key][field[0].to_s] = field[1].to_s
553
+ data[key][field[0].to_s] = field[1].to_s
549
554
  end
550
555
  end
551
556
 
552
557
  def hmget(key, *fields)
553
558
  raise Redis::CommandError, "wrong number of arguments for 'hmget' command" if fields.empty?
554
559
  data_type_check(key, Hash)
555
- values = []
556
560
  fields.map do |field|
557
561
  field = field.to_s
558
- if @data[key]
559
- @data[key][field]
562
+ if data[key]
563
+ data[key][field]
560
564
  else
561
565
  nil
562
566
  end
@@ -565,30 +569,30 @@ class Redis
565
569
 
566
570
  def hlen(key)
567
571
  data_type_check(key, Hash)
568
- return 0 unless @data[key]
569
- @data[key].size
572
+ return 0 unless data[key]
573
+ data[key].size
570
574
  end
571
575
 
572
576
  def hvals(key)
573
577
  data_type_check(key, Hash)
574
- return [] unless @data[key]
575
- @data[key].values
578
+ return [] unless data[key]
579
+ data[key].values
576
580
  end
577
581
 
578
582
  def hincrby(key, field, increment)
579
583
  data_type_check(key, Hash)
580
- if @data[key]
581
- @data[key][field] = (@data[key][field.to_s].to_i + increment.to_i).to_s
584
+ if data[key]
585
+ data[key][field] = (data[key][field.to_s].to_i + increment.to_i).to_s
582
586
  else
583
- @data[key] = { field => increment.to_s }
587
+ data[key] = { field => increment.to_s }
584
588
  end
585
- @data[key][field].to_i
589
+ data[key][field].to_i
586
590
  end
587
591
 
588
592
  def hexists(key, field)
589
593
  data_type_check(key, Hash)
590
- return false unless @data[key]
591
- @data[key].key?(field)
594
+ return false unless data[key]
595
+ data[key].key?(field)
592
596
  end
593
597
 
594
598
  def sync ; end
@@ -602,44 +606,48 @@ class Redis
602
606
  end
603
607
 
604
608
  def set(key, value)
605
- @data[key] = value.to_s
609
+ data[key] = value.to_s
606
610
  "OK"
607
611
  end
608
612
 
609
613
  def setbit(key, offset, bit)
610
- old_val = @data[key] ? @data[key].unpack('B*')[0].split("") : []
614
+ old_val = data[key] ? data[key].unpack('B*')[0].split("") : []
611
615
  size_increment = [((offset/8)+1)*8-old_val.length, 0].max
612
616
  old_val += Array.new(size_increment).map{"0"}
613
617
  original_val = old_val[offset]
614
618
  old_val[offset] = bit.to_s
615
619
  new_val = ""
616
620
  old_val.each_slice(8){|b| new_val = new_val + b.join("").to_i(2).chr }
617
- @data[key] = new_val
621
+ data[key] = new_val
618
622
  original_val
619
623
  end
620
624
 
621
625
  def setex(key, seconds, value)
622
- @data[key] = value.to_s
626
+ data[key] = value.to_s
623
627
  expire(key, seconds)
624
628
  end
625
629
 
626
630
  def setrange(key, offset, value)
627
- return unless @data[key]
628
- s = @data[key][offset,value.size]
629
- @data[key][s] = value
631
+ return unless data[key]
632
+ s = data[key][offset,value.size]
633
+ data[key][s] = value
630
634
  end
631
635
 
632
636
  def mset(*pairs)
637
+ # Handle pairs for mapped_mset command
638
+ pairs = pairs[0] if mapped_param?(pairs)
633
639
  pairs.each_slice(2) do |pair|
634
- @data[pair[0].to_s] = pair[1].to_s
640
+ data[pair[0].to_s] = pair[1].to_s
635
641
  end
636
642
  "OK"
637
643
  end
638
644
 
639
645
  def msetnx(*pairs)
646
+ # Handle pairs for mapped_msetnx command
647
+ pairs = pairs[0] if mapped_param?(pairs)
640
648
  keys = []
641
649
  pairs.each_with_index{|item, index| keys << item.to_s if index % 2 == 0}
642
- return if keys.any?{|key| @data.key?(key) }
650
+ return false if keys.any?{|key| data.key?(key) }
643
651
  mset(*pairs)
644
652
  true
645
653
  end
@@ -649,27 +657,27 @@ class Redis
649
657
  end
650
658
 
651
659
  def incr(key)
652
- @data.merge!({ key => (@data[key].to_i + 1).to_s || "1"})
653
- @data[key].to_i
660
+ data.merge!({ key => (data[key].to_i + 1).to_s || "1"})
661
+ data[key].to_i
654
662
  end
655
663
 
656
664
  def incrby(key, by)
657
- @data.merge!({ key => (@data[key].to_i + by.to_i).to_s || by })
658
- @data[key].to_i
665
+ data.merge!({ key => (data[key].to_i + by.to_i).to_s || by })
666
+ data[key].to_i
659
667
  end
660
668
 
661
669
  def decr(key)
662
- @data.merge!({ key => (@data[key].to_i - 1).to_s || "-1"})
663
- @data[key].to_i
670
+ data.merge!({ key => (data[key].to_i - 1).to_s || "-1"})
671
+ data[key].to_i
664
672
  end
665
673
 
666
674
  def decrby(key, by)
667
- @data.merge!({ key => ((@data[key].to_i - by.to_i) || (by.to_i * -1)).to_s })
668
- @data[key].to_i
675
+ data.merge!({ key => ((data[key].to_i - by.to_i) || (by.to_i * -1)).to_s })
676
+ data[key].to_i
669
677
  end
670
678
 
671
679
  def type(key)
672
- case value = @data[key]
680
+ case data[key]
673
681
  when nil then "none"
674
682
  when String then "string"
675
683
  when Hash then "hash"
@@ -685,13 +693,11 @@ class Redis
685
693
  def slaveof(host, port) ; end
686
694
 
687
695
  def exec
688
- buffer = @buffer
689
- @buffer = nil
690
- buffer
696
+ buffer.tap {|x| self.buffer = nil }
691
697
  end
692
698
 
693
699
  def multi
694
- @buffer = []
700
+ self.buffer = []
695
701
  yield if block_given?
696
702
  "OK"
697
703
  end
@@ -718,17 +724,17 @@ class Redis
718
724
  end
719
725
 
720
726
  data_type_check(key, ZSet)
721
- @data[key] ||= ZSet.new
727
+ data[key] ||= ZSet.new
722
728
 
723
729
  if args.size == 2
724
730
  score, value = args
725
- exists = !@data[key].key?(value.to_s)
726
- @data[key][value.to_s] = score
731
+ exists = !data[key].key?(value.to_s)
732
+ data[key][value.to_s] = score
727
733
  else
728
734
  # Turn [1, 2, 3, 4] into [[1, 2], [3, 4]] unless it is already
729
735
  args = args.each_slice(2).to_a unless args.first.is_a?(Array)
730
- exists = args.map(&:last).map { |el| @data[key].key?(el.to_s) }.count(false)
731
- args.each { |score, value| @data[key][value.to_s] = score }
736
+ exists = args.map(&:last).map { |el| data[key].key?(el.to_s) }.count(false)
737
+ args.each { |s, v| data[key][v.to_s] = s }
732
738
  end
733
739
 
734
740
  exists
@@ -736,73 +742,85 @@ class Redis
736
742
 
737
743
  def zrem(key, value)
738
744
  data_type_check(key, ZSet)
739
- exists = false
740
- exists = @data[key].delete(value.to_s) if @data[key]
745
+ values = Array(value)
746
+ return 0 unless data[key]
747
+
748
+ response = values.map do |v|
749
+ data[key].delete(v) if data[key].has_key?(v)
750
+ end.compact.size
751
+
741
752
  remove_key_for_empty_collection(key)
742
- !!exists
753
+ response
743
754
  end
744
755
 
745
756
  def zcard(key)
746
757
  data_type_check(key, ZSet)
747
- @data[key] ? @data[key].size : 0
758
+ data[key] ? data[key].size : 0
748
759
  end
749
760
 
750
761
  def zscore(key, value)
751
762
  data_type_check(key, ZSet)
752
- @data[key] && @data[key][value.to_s].to_s
763
+ value = data[key] && data[key][value.to_s]
764
+ value && value.to_s
753
765
  end
754
766
 
755
767
  def zcount(key, min, max)
756
768
  data_type_check(key, ZSet)
757
- return 0 unless @data[key]
758
- zrange_select_by_score(key, min, max).size
769
+ return 0 unless data[key]
770
+ data[key].select_by_score(min, max).size
759
771
  end
760
772
 
761
773
  def zincrby(key, num, value)
762
774
  data_type_check(key, ZSet)
763
- @data[key] ||= ZSet.new
764
- @data[key][value.to_s] ||= 0
765
- @data[key][value.to_s] += num
766
- @data[key][value.to_s].to_s
775
+ data[key] ||= ZSet.new
776
+ data[key][value.to_s] ||= 0
777
+ data[key].increment(value.to_s, num)
778
+ data[key][value.to_s].to_s
767
779
  end
768
780
 
769
781
  def zrank(key, value)
770
782
  data_type_check(key, ZSet)
771
- @data[key].keys.sort_by {|k| @data[key][k] }.index(value.to_s)
783
+ data[key].keys.sort_by {|k| data[key][k] }.index(value.to_s)
772
784
  end
773
785
 
774
786
  def zrevrank(key, value)
775
787
  data_type_check(key, ZSet)
776
- @data[key].keys.sort_by {|k| -@data[key][k] }.index(value.to_s)
788
+ data[key].keys.sort_by {|k| -data[key][k] }.index(value.to_s)
777
789
  end
778
790
 
779
791
  def zrange(key, start, stop, with_scores = nil)
780
792
  data_type_check(key, ZSet)
781
- return [] unless @data[key]
793
+ return [] unless data[key]
782
794
 
783
- if with_scores
784
- @data[key].sort_by {|_,v| v }
785
- else
786
- @data[key].keys.sort_by {|k| @data[key][k] }
787
- end[start..stop].flatten.map(&:to_s)
795
+ # Sort by score, or if scores are equal, key alphanum
796
+ results = data[key].sort do |(k1, v1), (k2, v2)|
797
+ if v1 == v2
798
+ k1 <=> k2
799
+ else
800
+ v1 <=> v2
801
+ end
802
+ end
803
+ # Select just the keys unless we want scores
804
+ results = results.map(&:first) unless with_scores
805
+ results[start..stop].flatten.map(&:to_s)
788
806
  end
789
807
 
790
808
  def zrevrange(key, start, stop, with_scores = nil)
791
809
  data_type_check(key, ZSet)
792
- return [] unless @data[key]
810
+ return [] unless data[key]
793
811
 
794
812
  if with_scores
795
- @data[key].sort_by {|_,v| -v }
813
+ data[key].sort_by {|_,v| -v }
796
814
  else
797
- @data[key].keys.sort_by {|k| -@data[key][k] }
815
+ data[key].keys.sort_by {|k| -data[key][k] }
798
816
  end[start..stop].flatten.map(&:to_s)
799
817
  end
800
818
 
801
819
  def zrangebyscore(key, min, max, *opts)
802
820
  data_type_check(key, ZSet)
803
- return [] unless @data[key]
821
+ return [] unless data[key]
804
822
 
805
- range = zrange_select_by_score(key, min, max)
823
+ range = data[key].select_by_score(min, max)
806
824
  vals = if opts.include?('WITHSCORES')
807
825
  range.sort_by {|_,v| v }
808
826
  else
@@ -817,9 +835,9 @@ class Redis
817
835
 
818
836
  def zrevrangebyscore(key, max, min, *opts)
819
837
  data_type_check(key, ZSet)
820
- return [] unless @data[key]
838
+ return [] unless data[key]
821
839
 
822
- range = zrange_select_by_score(key, min, max)
840
+ range = data[key].select_by_score(min, max)
823
841
  vals = if opts.include?('WITHSCORES')
824
842
  range.sort_by {|_,v| -v }
825
843
  else
@@ -834,66 +852,45 @@ class Redis
834
852
 
835
853
  def zremrangebyscore(key, min, max)
836
854
  data_type_check(key, ZSet)
837
- return 0 unless @data[key]
855
+ return 0 unless data[key]
838
856
 
839
- range = zrange_select_by_score(key, min, max)
840
- range.each {|k,_| @data[key].delete(k) }
857
+ range = data[key].select_by_score(min, max)
858
+ range.each {|k,_| data[key].delete(k) }
841
859
  range.size
842
860
  end
843
861
 
844
- def zinterstore(out, _, *keys)
862
+ def zinterstore(out, *args)
845
863
  data_type_check(out, ZSet)
864
+ args_handler = SortedSetArgumentHandler.new(args)
865
+ data[out] = SortedSetIntersectStore.new(args_handler, data).call
866
+ data[out].size
867
+ end
846
868
 
847
- hashes = keys.map do |src|
848
- case @data[src]
849
- when ::Set
850
- # Every value has a score of 1
851
- Hash[@data[src].map {|k,v| [k, 1]}]
852
- when Hash
853
- @data[src]
854
- else
855
- {}
856
- end
857
- end
858
-
859
- @data[out] = ZSet.new
860
- values = hashes.inject([]) {|r, h| r.empty? ? h.keys : r & h.keys }
861
- values.each do |value|
862
- @data[out][value] = hashes.inject(0) {|n, h| n + h[value].to_i }
863
- end
864
-
865
- @data[out].size
869
+ def zunionstore(out, *args)
870
+ data_type_check(out, ZSet)
871
+ args_handler = SortedSetArgumentHandler.new(args)
872
+ data[out] = SortedSetUnionStore.new(args_handler, data).call
873
+ data[out].size
866
874
  end
867
875
 
868
876
  def zremrangebyrank(key, start, stop)
869
- sorted_elements = @data[key].sort { |(v_a, r_a), (v_b, r_b)| r_a <=> r_b }
877
+ sorted_elements = data[key].sort_by { |k, v| v }
870
878
  start = sorted_elements.length if start > sorted_elements.length
871
879
  elements_to_delete = sorted_elements[start..stop]
872
- elements_to_delete.each { |elem, rank| @data[key].delete(elem) }
880
+ elements_to_delete.each { |elem, rank| data[key].delete(elem) }
873
881
  elements_to_delete.size
874
882
  end
875
883
 
876
884
  private
877
885
 
878
- def zrange_select_by_score(key, min, max)
879
- if min == '-inf' && max == '+inf'
880
- @data[key]
881
- elsif max == '+inf'
882
- @data[key].reject { |_,v| v < min }
883
- elsif min == '-inf'
884
- @data[key].reject { |_,v| v > max }
885
- else
886
- @data[key].reject {|_,v| v < min || v > max }
887
- end
888
- end
889
-
890
886
  def remove_key_for_empty_collection(key)
891
- del(key) if @data[key] && @data[key].empty?
887
+ del(key) if data[key] && data[key].empty?
892
888
  end
893
889
 
894
890
  def data_type_check(key, klass)
895
- if @data[key] && !@data[key].is_a?(klass)
896
- fail "Operation against a key holding the wrong kind of value: Expected #{klass} at #{key}."
891
+ if data[key] && !data[key].is_a?(klass)
892
+ warn "Operation against a key holding the wrong kind of value: Expected #{klass} at #{key}."
893
+ raise Redis::CommandError.new("ERR Operation against a key holding the wrong kind of value")
897
894
  end
898
895
  end
899
896
 
@@ -909,6 +906,10 @@ class Redis
909
906
  [offset, count]
910
907
  end
911
908
  end
909
+
910
+ def mapped_param? param
911
+ param.size == 1 && param[0].is_a?(Array)
912
+ end
912
913
  end
913
914
  end
914
915
  end