fakeredis 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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