fakeredis 0.5.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 20688b94f039d268518697ede7cdfad15f26cf73
4
- data.tar.gz: ab1982d7e9ed2f7c6b864d2beadd63d6041bb59a
3
+ metadata.gz: 30c50ee5b3390908b28564542cf9d6ad6c1dfb20
4
+ data.tar.gz: 30c85e12c5689721c1ead41da72a91c47a010a0b
5
5
  SHA512:
6
- metadata.gz: 080b44a262bae01a09dcd2660cb35e1a52543cad7d3780fc58224854b6007adb8a0598a0b892560988f7a86c921a1ea1d3b36fa19248542017ffb97ce8caa0ef
7
- data.tar.gz: 687896c96f26f389776216ef6397ce9bb025e9fb04008860dc377f8ba0965245185b02bb4381e7e560a5d76621b8c87ee4989878fbe0bd086e5fc30e2e3c6442
6
+ metadata.gz: 62ec40ecbbf36d198b0a8b95d5772f6d1e79a9709378a67a9e5aaba8cc15f6122fba3966524831ce5a8e7b6ebdbceaf620a4320f338497deff52c8361d8c41c7
7
+ data.tar.gz: 7d7c9066f97f7accfc95e40819b726ffcbf318e5e6f1af9fda405ce3104c56e546e2a2c6b5e61cac58a283a4d56f868c8c1c6bb5ca811ca440cc0eb62adf599a
data/.gitignore CHANGED
@@ -1,6 +1,10 @@
1
1
  *.gem
2
2
  .bundle
3
3
  Gemfile.lock
4
+ gemfiles/*.gemfile.lock
4
5
  pkg/*
5
6
  .rvmrc
6
7
  *.rbc
8
+ .ruby-version
9
+ .ruby-gemset
10
+ bin
@@ -1,8 +1,19 @@
1
1
  language: ruby
2
+ before_install:
3
+ - travis_retry gem install bundler
2
4
  rvm:
3
- - 1.9.2
4
- - 1.9.3
5
- - 2.0.0
6
- - 2.1.1
7
- - jruby-19mode
5
+ - 2.1
6
+ - 2.2
7
+ - 2.3.1
8
+ - ruby-head
9
+ - jruby
8
10
  - rbx-2
11
+ gemfile:
12
+ - Gemfile
13
+ - gemfiles/redisrb-master.gemfile
14
+ matrix:
15
+ allow_failures:
16
+ - rvm: rbx-2
17
+ # Use the faster container based infrastructure
18
+ # http://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/
19
+ sudo: false
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011-2014 Guillermo Iguaran
1
+ Copyright (c) 2011-2016 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
@@ -39,7 +39,7 @@ You can use FakeRedis without any changes:
39
39
  >> redis.get "foo"
40
40
  => "bar"
41
41
 
42
- Read [redis-rb](https://github.com/ezmobius/redis-rb) documentation and
42
+ Read [redis-rb](https://github.com/redis/redis-rb) documentation and
43
43
  [Redis](http://redis.io) homepage for more info about commands
44
44
 
45
45
  ## Usage with RSpec
@@ -57,6 +57,22 @@ Or:
57
57
  # spec/support/fakeredis.rb
58
58
  require 'fakeredis/rspec'
59
59
 
60
+ ## Usage with Minitest
61
+
62
+ Require this either in your Gemfile or in Minitest's support scripts. So
63
+ either:
64
+
65
+ # Gemfile
66
+ group :test do
67
+ gem "minitest"
68
+ gem "fakeredis", :require => "fakeredis/minitest"
69
+ end
70
+
71
+ Or:
72
+
73
+ # test/test_helper.rb (or test/minitest_config.rb)
74
+ require 'fakeredis/minitest'
75
+
60
76
  ## Acknowledgements
61
77
 
62
78
  * [dim](https://github.com/dim)
@@ -82,5 +98,5 @@ Or:
82
98
 
83
99
  ## Copyright
84
100
 
85
- Copyright (c) 2011-2014 Guillermo Iguaran. See LICENSE for
101
+ Copyright (c) 2011-2016 Guillermo Iguaran. See LICENSE for
86
102
  further details.
@@ -18,6 +18,6 @@ Gem::Specification.new do |s|
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
20
 
21
- s.add_runtime_dependency(%q<redis>, ["~> 3.0"])
21
+ s.add_runtime_dependency(%q<redis>, ["~> 3.2"])
22
22
  s.add_development_dependency(%q<rspec>, ["~> 3.0"])
23
23
  end
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rake'
4
+ gem 'rdoc'
5
+ gem "redis", github: "redis/redis-rb"
6
+
7
+ platforms :rbx do
8
+ gem 'racc'
9
+ gem 'rubysl', '~> 2.0'
10
+ gem 'psych'
11
+ end
12
+
13
+ # Specify your gem's dependencies in fakeredis.gemspec
14
+ gemspec :path => ".."
@@ -0,0 +1,56 @@
1
+ module FakeRedis
2
+ module BitopCommand
3
+ BIT_OPERATORS = {
4
+ 'or' => :|,
5
+ 'and' => :&,
6
+ 'xor' => :'^',
7
+ 'not' => :~,
8
+ }
9
+
10
+ def bitop(operation, destkey, *keys)
11
+ if result = apply(operator(operation), keys)
12
+ set(destkey, result)
13
+ result.length
14
+ else
15
+ 0
16
+ end
17
+ rescue ArgumentError => _
18
+ raise_argument_error('bitop')
19
+ end
20
+
21
+ private
22
+
23
+ def operator(operation)
24
+ BIT_OPERATORS[operation.to_s.downcase]
25
+ end
26
+
27
+ def apply(operator, keys)
28
+ case operator
29
+ when :~
30
+ raise ArgumentError if keys.count != 1
31
+ bitwise_not(keys.first)
32
+ when :&, :|, :'^'
33
+ raise ArgumentError if keys.empty?
34
+ bitwise_operation(operator, keys)
35
+ else
36
+ raise ArgumentError
37
+ end
38
+ end
39
+
40
+ def bitwise_not(key)
41
+ if value = get(keys.first)
42
+ value.bytes.map { |byte| ~ byte }.pack('c*')
43
+ end
44
+ end
45
+
46
+ def bitwise_operation(operation, keys)
47
+ apply_onto, *values = keys.map { |key| get(key) }.reject(&:nil?)
48
+ values.reduce(apply_onto) do |memo, value|
49
+ shorter, longer = [memo, value].sort_by(&:length).map(&:bytes).map(&:to_a)
50
+ longer.each_with_index.map do |byte, index|
51
+ byte.send(operation, shorter[index] || 0)
52
+ end.pack('c*')
53
+ end
54
+ end
55
+ end
56
+ end
@@ -34,7 +34,7 @@ module FakeRedis
34
34
 
35
35
  def expired?(key)
36
36
  key = normalize key
37
- expires.include?(key) && expires[key] < Time.now
37
+ expires.include?(key) && expires[key] <= Time.now
38
38
  end
39
39
 
40
40
  def key?(key)
@@ -0,0 +1,24 @@
1
+ # Require this either in your Gemfile or in your minitest configuration.
2
+ # Examples:
3
+ #
4
+ # # Gemfile
5
+ # group :test do
6
+ # gem 'minitest'
7
+ # gem 'fakeredis', :require => 'fakeredis/minitest'
8
+ # end
9
+ #
10
+ # # test/test_helper.rb (or test/minitest_config.rb)
11
+ # require 'fakeredis/minitest'
12
+
13
+ require 'fakeredis'
14
+
15
+ module FakeRedis
16
+ module Minitest
17
+ def setup
18
+ super
19
+ Redis::Connection::Memory.reset_all_databases
20
+ end
21
+
22
+ ::Minitest::Test.send(:include, self)
23
+ end
24
+ end
@@ -18,6 +18,7 @@ RSpec.configure do |c|
18
18
 
19
19
  c.before do
20
20
  Redis::Connection::Memory.reset_all_databases
21
+ Redis::Connection::Memory.reset_all_channels
21
22
  end
22
23
 
23
24
  end
@@ -3,6 +3,7 @@ module FakeRedis
3
3
  module SortMethod
4
4
  def sort(key, *redis_options_array)
5
5
  return [] unless key
6
+ return [] if type(key) == 'none'
6
7
 
7
8
  unless %w(list set zset).include? type(key)
8
9
  warn "Operation against a key holding the wrong kind of value: Expected list, set or zset at #{key}."
@@ -21,7 +22,7 @@ module FakeRedis
21
22
  # We have to flatten it down as redis-rb adds back the array to the return value
22
23
  result = sliced.flatten(1)
23
24
 
24
- options[:store] ? rpush(options[:store], sliced) : sliced.flatten(1)
25
+ options[:store] ? rpush(options[:store], sliced) : result
25
26
  end
26
27
 
27
28
  private
@@ -100,7 +101,7 @@ module FakeRedis
100
101
  skip = limit.first || 0
101
102
  take = limit.last || sorted.length
102
103
 
103
- sorted[skip...(skip + take)] || sorted
104
+ sorted[skip...(skip + take)] || []
104
105
  end
105
106
 
106
107
  def lookup_from_pattern(pattern, element)
@@ -72,7 +72,7 @@ module FakeRedis
72
72
  "OK"
73
73
  end
74
74
 
75
- def watch(_)
75
+ def watch(*_)
76
76
  "OK"
77
77
  end
78
78
 
@@ -1,3 +1,3 @@
1
1
  module FakeRedis
2
- VERSION = "0.5.0"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -5,6 +5,10 @@ module FakeRedis
5
5
  super(key, _floatify(val))
6
6
  end
7
7
 
8
+ def identical_scores?
9
+ values.uniq.size == 1
10
+ end
11
+
8
12
  # Increments the value of key by val
9
13
  def increment(key, val)
10
14
  self[key] += _floatify(val)
@@ -25,8 +29,10 @@ module FakeRedis
25
29
  elsif (( number = str.to_s.match(/^\((\d+)/i) ))
26
30
  number[1].to_i + (increment ? 1 : -1)
27
31
  else
28
- Float str
32
+ Float str.to_s
29
33
  end
34
+ rescue ArgumentError
35
+ raise Redis::CommandError, "ERR value is not a valid float"
30
36
  end
31
37
 
32
38
  end
@@ -8,6 +8,7 @@ require "fakeredis/sorted_set_argument_handler"
8
8
  require "fakeredis/sorted_set_store"
9
9
  require "fakeredis/transaction_commands"
10
10
  require "fakeredis/zset"
11
+ require "fakeredis/bitop_command"
11
12
 
12
13
  class Redis
13
14
  module Connection
@@ -16,6 +17,7 @@ class Redis
16
17
  include FakeRedis
17
18
  include SortMethod
18
19
  include TransactionCommands
20
+ include BitopCommand
19
21
  include CommandExecutor
20
22
 
21
23
  attr_accessor :options
@@ -35,6 +37,14 @@ class Redis
35
37
  @databases = nil
36
38
  end
37
39
 
40
+ def self.channels
41
+ @channels ||= Hash.new {|h,k| h[k] = [] }
42
+ end
43
+
44
+ def self.reset_all_channels
45
+ @channels = nil
46
+ end
47
+
38
48
  def self.connect(options = {})
39
49
  new(options)
40
50
  end
@@ -86,14 +96,6 @@ class Redis
86
96
  replies.shift
87
97
  end
88
98
 
89
- # NOT IMPLEMENTED:
90
- # * blpop
91
- # * brpop
92
- # * brpoplpush
93
- # * subscribe
94
- # * psubscribe
95
- # * publish
96
-
97
99
  def flushdb
98
100
  databases.delete_at(database_id)
99
101
  "OK"
@@ -146,6 +148,45 @@ class Redis
146
148
  true
147
149
  end
148
150
 
151
+ def dump(key)
152
+ return nil unless exists(key)
153
+
154
+ value = data[key]
155
+
156
+ Marshal.dump(
157
+ value: value,
158
+ version: FakeRedis::VERSION, # Redis includes the version, so we might as well
159
+ )
160
+ end
161
+
162
+ def restore(key, ttl, serialized_value)
163
+ raise Redis::CommandError, "ERR Target key name is busy." if exists(key)
164
+
165
+ raise Redis::CommandError, "ERR DUMP payload version or checksum are wrong" if serialized_value.nil?
166
+
167
+ parsed_value = begin
168
+ Marshal.load(serialized_value)
169
+ rescue TypeError
170
+ raise Redis::CommandError, "ERR DUMP payload version or checksum are wrong"
171
+ end
172
+
173
+ if parsed_value[:version] != FakeRedis::VERSION
174
+ raise Redis::CommandError, "ERR DUMP payload version or checksum are wrong"
175
+ end
176
+
177
+ # We could figure out what type the key was and set it with the public API here,
178
+ # or we could just assign the value. If we presume the serialized_value is only ever
179
+ # a return value from `dump` then we've only been given something that was in
180
+ # the internal data structure anyway.
181
+ data[key] = parsed_value[:value]
182
+
183
+ # Set a TTL if one has been passed
184
+ ttl = ttl.to_i # Makes nil into 0
185
+ expire(key, ttl / 1000) unless ttl.zero?
186
+
187
+ "OK"
188
+ end
189
+
149
190
  def get(key)
150
191
  data_type_check(key, String)
151
192
  data[key]
@@ -202,11 +243,22 @@ class Redis
202
243
  end
203
244
 
204
245
  def hdel(key, field)
205
- field = field.to_s
206
246
  data_type_check(key, Hash)
207
- deleted = data[key] && data[key].delete(field)
247
+ return 0 unless data[key]
248
+
249
+ if field.is_a?(Array)
250
+ old_keys_count = data[key].size
251
+ fields = field.map(&:to_s)
252
+
253
+ data[key].delete_if { |k, v| fields.include? k }
254
+ deleted = old_keys_count - data[key].size
255
+ else
256
+ field = field.to_s
257
+ deleted = data[key].delete(field) ? 1 : 0
258
+ end
259
+
208
260
  remove_key_for_empty_collection(key)
209
- deleted ? 1 : 0
261
+ deleted
210
262
  end
211
263
 
212
264
  def hkeys(key)
@@ -215,6 +267,44 @@ class Redis
215
267
  data[key].keys
216
268
  end
217
269
 
270
+ def hscan(key, start_cursor, *args)
271
+ data_type_check(key, Hash)
272
+ return ["0", []] unless data[key]
273
+
274
+ match = "*"
275
+ count = 10
276
+
277
+ if args.size.odd?
278
+ raise_argument_error('hscan')
279
+ end
280
+
281
+ if idx = args.index("MATCH")
282
+ match = args[idx + 1]
283
+ end
284
+
285
+ if idx = args.index("COUNT")
286
+ count = args[idx + 1]
287
+ end
288
+
289
+ start_cursor = start_cursor.to_i
290
+
291
+ cursor = start_cursor
292
+ next_keys = []
293
+
294
+ if start_cursor + count >= data[key].length
295
+ next_keys = (data[key].to_a)[start_cursor..-1]
296
+ cursor = 0
297
+ else
298
+ cursor = start_cursor + count
299
+ next_keys = (data[key].to_a)[start_cursor..cursor-1]
300
+ end
301
+
302
+ filtered_next_keys = next_keys.select{|k,v| File.fnmatch(match, k)}
303
+ result = filtered_next_keys.flatten.map(&:to_s)
304
+
305
+ return ["#{cursor}", result]
306
+ end
307
+
218
308
  def keys(pattern = "*")
219
309
  data.keys.select { |key| File.fnmatch(pattern, key) }
220
310
  end
@@ -256,7 +346,14 @@ class Redis
256
346
 
257
347
  def lrange(key, startidx, endidx)
258
348
  data_type_check(key, Array)
259
- (data[key] && data[key][startidx..endidx]) || []
349
+ if data[key]
350
+ # In Ruby when negative start index is out of range Array#slice returns
351
+ # nil which is not the case for lrange in Redis.
352
+ startidx = 0 if startidx < 0 && startidx.abs > data[key].size
353
+ data[key][startidx..endidx] || []
354
+ else
355
+ []
356
+ end
260
357
  end
261
358
 
262
359
  def ltrim(key, start, stop)
@@ -284,7 +381,11 @@ class Redis
284
381
  def linsert(key, where, pivot, value)
285
382
  data_type_check(key, Array)
286
383
  return unless data[key]
287
- index = data[key].index(pivot)
384
+
385
+ value = value.to_s
386
+ index = data[key].index(pivot.to_s)
387
+ return -1 if index.nil?
388
+
288
389
  case where
289
390
  when :before then data[key].insert(index, value)
290
391
  when :after then data[key].insert(index + 1, value)
@@ -296,12 +397,14 @@ class Redis
296
397
  data_type_check(key, Array)
297
398
  return unless data[key]
298
399
  raise Redis::CommandError, "ERR index out of range" if index >= data[key].size
299
- data[key][index] = value
400
+ data[key][index] = value.to_s
300
401
  end
301
402
 
302
403
  def lrem(key, count, value)
303
404
  data_type_check(key, Array)
304
- return unless data[key]
405
+ return 0 unless data[key]
406
+
407
+ value = value.to_s
305
408
  old_size = data[key].size
306
409
  diff =
307
410
  if count == 0
@@ -318,6 +421,7 @@ class Redis
318
421
  end
319
422
 
320
423
  def rpush(key, value)
424
+ raise_argument_error('rpush') if value.respond_to?(:each) && value.empty?
321
425
  data_type_check(key, Array)
322
426
  data[key] ||= []
323
427
  [value].flatten.each do |val|
@@ -327,12 +431,14 @@ class Redis
327
431
  end
328
432
 
329
433
  def rpushx(key, value)
434
+ raise_argument_error('rpushx') if value.respond_to?(:each) && value.empty?
330
435
  data_type_check(key, Array)
331
436
  return unless data[key]
332
437
  rpush(key, value)
333
438
  end
334
439
 
335
440
  def lpush(key, value)
441
+ raise_argument_error('lpush') if value.respond_to?(:each) && value.empty?
336
442
  data_type_check(key, Array)
337
443
  data[key] ||= []
338
444
  [value].flatten.each do |val|
@@ -342,6 +448,7 @@ class Redis
342
448
  end
343
449
 
344
450
  def lpushx(key, value)
451
+ raise_argument_error('lpushx') if value.respond_to?(:each) && value.empty?
345
452
  data_type_check(key, Array)
346
453
  return unless data[key]
347
454
  lpush(key, value)
@@ -353,6 +460,18 @@ class Redis
353
460
  data[key].pop
354
461
  end
355
462
 
463
+ def brpop(keys, timeout=0)
464
+ #todo threaded mode
465
+ keys = Array(keys)
466
+ keys.each do |key|
467
+ if data[key] && data[key].size > 0
468
+ return [key, data[key].pop]
469
+ end
470
+ end
471
+ sleep(timeout.to_f)
472
+ nil
473
+ end
474
+
356
475
  def rpoplpush(key1, key2)
357
476
  data_type_check(key1, Array)
358
477
  rpop(key1).tap do |elem|
@@ -360,12 +479,31 @@ class Redis
360
479
  end
361
480
  end
362
481
 
482
+ def brpoplpush(key1, key2, opts={})
483
+ data_type_check(key1, Array)
484
+ brpop(key1).tap do |elem|
485
+ lpush(key2, elem) unless elem.nil?
486
+ end
487
+ end
488
+
363
489
  def lpop(key)
364
490
  data_type_check(key, Array)
365
491
  return unless data[key]
366
492
  data[key].shift
367
493
  end
368
494
 
495
+ def blpop(keys, timeout=0)
496
+ #todo threaded mode
497
+ keys = Array(keys)
498
+ keys.each do |key|
499
+ if data[key] && data[key].size > 0
500
+ return [key, data[key].shift]
501
+ end
502
+ end
503
+ sleep(timeout.to_f)
504
+ nil
505
+ end
506
+
369
507
  def smembers(key)
370
508
  data_type_check(key, ::Set)
371
509
  return [] unless data[key]
@@ -404,7 +542,7 @@ class Redis
404
542
  if value.is_a?(Array)
405
543
  old_size = data[key].size
406
544
  values = value.map(&:to_s)
407
- values.each { |value| data[key].delete(value) }
545
+ values.each { |v| data[key].delete(v) }
408
546
  deleted = old_size - data[key].size
409
547
  else
410
548
  deleted = !!data[key].delete?(value.to_s)
@@ -435,6 +573,7 @@ class Redis
435
573
  end
436
574
 
437
575
  def sinter(*keys)
576
+ keys = keys[0] if flatten?(keys)
438
577
  raise_argument_error('sinter') if keys.empty?
439
578
 
440
579
  keys.each { |k| data_type_check(k, ::Set) }
@@ -452,6 +591,9 @@ class Redis
452
591
  end
453
592
 
454
593
  def sunion(*keys)
594
+ keys = keys[0] if flatten?(keys)
595
+ raise_argument_error('sunion') if keys.empty?
596
+
455
597
  keys.each { |k| data_type_check(k, ::Set) }
456
598
  keys = keys.map { |k| data[k] || ::Set.new }
457
599
  keys.inject(::Set.new) do |set, key|
@@ -466,6 +608,7 @@ class Redis
466
608
  end
467
609
 
468
610
  def sdiff(key1, *keys)
611
+ keys = keys[0] if flatten?(keys)
469
612
  [key1, *keys].each { |k| data_type_check(k, ::Set) }
470
613
  keys = keys.map { |k| data[k] || ::Set.new }
471
614
  keys.inject(data[key1] || Set.new) do |memo, set|
@@ -483,6 +626,44 @@ class Redis
483
626
  number.nil? ? srandmember_single(key) : srandmember_multiple(key, number)
484
627
  end
485
628
 
629
+ def sscan(key, start_cursor, *args)
630
+ data_type_check(key, ::Set)
631
+ return ["0", []] unless data[key]
632
+
633
+ match = "*"
634
+ count = 10
635
+
636
+ if args.size.odd?
637
+ raise_argument_error('sscan')
638
+ end
639
+
640
+ if idx = args.index("MATCH")
641
+ match = args[idx + 1]
642
+ end
643
+
644
+ if idx = args.index("COUNT")
645
+ count = args[idx + 1]
646
+ end
647
+
648
+ start_cursor = start_cursor.to_i
649
+
650
+ cursor = start_cursor
651
+ next_keys = []
652
+
653
+ if start_cursor + count >= data[key].length
654
+ next_keys = (data[key].to_a)[start_cursor..-1]
655
+ cursor = 0
656
+ else
657
+ cursor = start_cursor + count
658
+ next_keys = (data[key].to_a)[start_cursor..cursor-1]
659
+ end
660
+
661
+ filtered_next_keys = next_keys.select{ |k,v| File.fnmatch(match, k)}
662
+ result = filtered_next_keys.flatten.map(&:to_s)
663
+
664
+ return ["#{cursor}", result]
665
+ end
666
+
486
667
  def del(*keys)
487
668
  keys = keys.flatten(1)
488
669
  raise_argument_error('del') if keys.empty?
@@ -496,10 +677,10 @@ class Redis
496
677
 
497
678
  def setnx(key, value)
498
679
  if exists(key)
499
- false
680
+ 0
500
681
  else
501
682
  set(key, value)
502
- true
683
+ 1
503
684
  end
504
685
  end
505
686
 
@@ -525,6 +706,12 @@ class Redis
525
706
  1
526
707
  end
527
708
 
709
+ def pexpire(key, ttl)
710
+ return 0 unless data[key]
711
+ data.expires[key] = Time.now + (ttl / 1000.0)
712
+ 1
713
+ end
714
+
528
715
  def ttl(key)
529
716
  if data.expires.include?(key) && (ttl = data.expires[key].to_i - Time.now.to_i) > 0
530
717
  ttl
@@ -533,6 +720,14 @@ class Redis
533
720
  end
534
721
  end
535
722
 
723
+ def pttl(key)
724
+ if data.expires.include?(key) && (ttl = data.expires[key].to_f - Time.now.to_f) > 0
725
+ ttl * 1000
726
+ else
727
+ exists(key) ? -1 : -2
728
+ end
729
+ end
730
+
536
731
  def expireat(key, timestamp)
537
732
  data.expires[key] = Time.at(timestamp)
538
733
  true
@@ -548,10 +743,10 @@ class Redis
548
743
  if data[key]
549
744
  result = !data[key].include?(field)
550
745
  data[key][field] = value.to_s
551
- result
746
+ result ? 1 : 0
552
747
  else
553
748
  data[key] = { field => value.to_s }
554
- true
749
+ 1
555
750
  end
556
751
  end
557
752
 
@@ -727,6 +922,11 @@ class Redis
727
922
  data[key].to_i
728
923
  end
729
924
 
925
+ def incrbyfloat(key, by)
926
+ data.merge!({ key => (data[key].to_f + by.to_f).to_s || by })
927
+ data[key]
928
+ end
929
+
730
930
  def decr(key)
731
931
  data.merge!({ key => (data[key].to_i - 1).to_s || "-1"})
732
932
  data[key].to_i
@@ -758,10 +958,6 @@ class Redis
758
958
  match = "*"
759
959
  count = 10
760
960
 
761
- if args.size.odd?
762
- raise_argument_error('scan')
763
- end
764
-
765
961
  if idx = args.index("MATCH")
766
962
  match = args[idx + 1]
767
963
  end
@@ -774,17 +970,22 @@ class Redis
774
970
  data_type_check(start_cursor, Fixnum)
775
971
 
776
972
  cursor = start_cursor
777
- next_keys = []
973
+ returned_keys = []
974
+ final_page = start_cursor + count >= keys(match).length
975
+
976
+ if final_page
977
+ previous_keys_been_deleted = (count >= keys(match).length)
978
+ start_index = previous_keys_been_deleted ? 0 : cursor
778
979
 
779
- if start_cursor + count >= data.length
780
- next_keys = keys(match)[start_cursor..-1]
980
+ returned_keys = keys(match)[start_index..-1]
781
981
  cursor = 0
782
982
  else
783
- cursor = start_cursor + 10
784
- next_keys = keys(match)[start_cursor..cursor]
983
+ end_index = start_cursor + (count - 1)
984
+ returned_keys = keys(match)[start_cursor..end_index]
985
+ cursor = start_cursor + count
785
986
  end
786
987
 
787
- return "#{cursor}", next_keys
988
+ return "#{cursor}", returned_keys
788
989
  end
789
990
 
790
991
  def zadd(key, *args)
@@ -873,19 +1074,44 @@ class Redis
873
1074
  data_type_check(key, ZSet)
874
1075
  return [] unless data[key]
875
1076
 
876
- # Sort by score, or if scores are equal, key alphanum
877
- results = data[key].sort do |(k1, v1), (k2, v2)|
878
- if v1 == v2
879
- k1 <=> k2
880
- else
881
- v1 <=> v2
882
- end
883
- end
1077
+ results = sort_keys(data[key])
884
1078
  # Select just the keys unless we want scores
885
1079
  results = results.map(&:first) unless with_scores
886
1080
  results[start..stop].flatten.map(&:to_s)
887
1081
  end
888
1082
 
1083
+ def zrangebylex(key, start, stop, *opts)
1084
+ data_type_check(key, ZSet)
1085
+ return [] unless data[key]
1086
+ zset = data[key]
1087
+
1088
+ sorted = if zset.identical_scores?
1089
+ zset.keys.sort { |x, y| x.to_s <=> y.to_s }
1090
+ else
1091
+ zset.keys
1092
+ end
1093
+
1094
+ range = get_range start, stop, sorted.first, sorted.last
1095
+
1096
+ filtered = []
1097
+ sorted.each do |element|
1098
+ filtered << element if (range[0][:value]..range[1][:value]).cover?(element)
1099
+ end
1100
+ filtered.shift if filtered[0] == range[0][:value] && !range[0][:inclusive]
1101
+ filtered.pop if filtered.last == range[1][:value] && !range[1][:inclusive]
1102
+
1103
+ limit = get_limit(opts, filtered)
1104
+ if limit
1105
+ filtered = filtered[limit[0]..-1].take(limit[1])
1106
+ end
1107
+
1108
+ filtered
1109
+ end
1110
+
1111
+ def zrevrangebylex(key, start, stop, *args)
1112
+ zrangebylex(key, stop, start, args).reverse
1113
+ end
1114
+
889
1115
  def zrevrange(key, start, stop, with_scores = nil)
890
1116
  data_type_check(key, ZSet)
891
1117
  return [] unless data[key]
@@ -966,6 +1192,131 @@ class Redis
966
1192
  data[out].size
967
1193
  end
968
1194
 
1195
+ def subscribe(*channels)
1196
+ raise_argument_error('subscribe') if channels.empty?()
1197
+
1198
+ #Create messages for all data from the channels
1199
+ channel_replies = channels.map do |channel|
1200
+ self.class.channels[channel].slice!(0..-1).map!{|v| ["message", channel, v]}
1201
+ end
1202
+ channel_replies.flatten!(1)
1203
+ channel_replies.compact!()
1204
+
1205
+ #Put messages into the replies for the future
1206
+ channels.each_with_index do |channel,index|
1207
+ replies << ["subscribe", channel, index+1]
1208
+ end
1209
+ replies.push(*channel_replies)
1210
+
1211
+ #Add unsubscribe message to stop blocking (see https://github.com/redis/redis-rb/blob/v3.2.1/lib/redis/subscribe.rb#L38)
1212
+ replies.push(self.unsubscribe())
1213
+
1214
+ replies.pop() #Last reply will be pushed back on
1215
+ end
1216
+
1217
+ def psubscribe(*patterns)
1218
+ raise_argument_error('psubscribe') if patterns.empty?()
1219
+
1220
+ #Create messages for all data from the channels
1221
+ channel_replies = self.class.channels.keys.map do |channel|
1222
+ pattern = patterns.find{|p| File.fnmatch(p, channel) }
1223
+ unless pattern.nil?()
1224
+ self.class.channels[channel].slice!(0..-1).map!{|v| ["pmessage", pattern, channel, v]}
1225
+ end
1226
+ end
1227
+ channel_replies.flatten!(1)
1228
+ channel_replies.compact!()
1229
+
1230
+ #Put messages into the replies for the future
1231
+ patterns.each_with_index do |pattern,index|
1232
+ replies << ["psubscribe", pattern, index+1]
1233
+ end
1234
+ replies.push(*channel_replies)
1235
+
1236
+ #Add unsubscribe to stop blocking
1237
+ replies.push(self.punsubscribe())
1238
+
1239
+ replies.pop() #Last reply will be pushed back on
1240
+ end
1241
+
1242
+ def publish(channel, message)
1243
+ self.class.channels[channel] << message
1244
+ 0 #Just fake number of subscribers
1245
+ end
1246
+
1247
+ def unsubscribe(*channels)
1248
+ if channels.empty?()
1249
+ replies << ["unsubscribe", nil, 0]
1250
+ else
1251
+ channels.each do |channel|
1252
+ replies << ["unsubscribe", channel, 0]
1253
+ end
1254
+ end
1255
+ replies.pop() #Last reply will be pushed back on
1256
+ end
1257
+
1258
+ def punsubscribe(*patterns)
1259
+ if patterns.empty?()
1260
+ replies << ["punsubscribe", nil, 0]
1261
+ else
1262
+ patterns.each do |pattern|
1263
+ replies << ["punsubscribe", pattern, 0]
1264
+ end
1265
+ end
1266
+ replies.pop() #Last reply will be pushed back on
1267
+ end
1268
+
1269
+ def zscan(key, start_cursor, *args)
1270
+ data_type_check(key, ZSet)
1271
+ return [] unless data[key]
1272
+
1273
+ match = "*"
1274
+ count = 10
1275
+
1276
+ if args.size.odd?
1277
+ raise_argument_error('zscan')
1278
+ end
1279
+
1280
+ if idx = args.index("MATCH")
1281
+ match = args[idx + 1]
1282
+ end
1283
+
1284
+ if idx = args.index("COUNT")
1285
+ count = args[idx + 1]
1286
+ end
1287
+
1288
+ start_cursor = start_cursor.to_i
1289
+ data_type_check(start_cursor, Fixnum)
1290
+
1291
+ cursor = start_cursor
1292
+ next_keys = []
1293
+
1294
+ sorted_keys = sort_keys(data[key])
1295
+
1296
+ if start_cursor + count >= sorted_keys.length
1297
+ next_keys = sorted_keys.to_a.select { |k| File.fnmatch(match, k[0]) } [start_cursor..-1]
1298
+ cursor = 0
1299
+ else
1300
+ cursor = start_cursor + count
1301
+ next_keys = sorted_keys.to_a.select { |k| File.fnmatch(match, k[0]) } [start_cursor..cursor-1]
1302
+ end
1303
+ return "#{cursor}", next_keys.flatten.map(&:to_s)
1304
+ end
1305
+
1306
+ # Originally from redis-rb
1307
+ def zscan_each(key, *args, &block)
1308
+ data_type_check(key, ZSet)
1309
+ return [] unless data[key]
1310
+
1311
+ return to_enum(:zscan_each, key, options) unless block_given?
1312
+ cursor = 0
1313
+ loop do
1314
+ cursor, values = zscan(key, cursor, options)
1315
+ values.each(&block)
1316
+ break if cursor == "0"
1317
+ end
1318
+ end
1319
+
969
1320
  private
970
1321
  def raise_argument_error(command, match_string=command)
971
1322
  error_message = if %w(hmset mset_odd).include?(match_string.downcase)
@@ -992,6 +1343,27 @@ class Redis
992
1343
  end
993
1344
  end
994
1345
 
1346
+ def get_range(start, stop, min = -Float::INFINITY, max = Float::INFINITY)
1347
+ range_options = []
1348
+
1349
+ [start, stop].each do |value|
1350
+ case value[0]
1351
+ when "-"
1352
+ range_options << { value: min, inclusive: true }
1353
+ when "+"
1354
+ range_options << { value: max, inclusive: true }
1355
+ when "["
1356
+ range_options << { value: value[1..-1], inclusive: true }
1357
+ when "("
1358
+ range_options << { value: value[1..-1], inclusive: false }
1359
+ else
1360
+ raise Redis::CommandError, "ERR min or max not valid string range item"
1361
+ end
1362
+ end
1363
+
1364
+ range_options
1365
+ end
1366
+
995
1367
  def get_limit(opts, vals)
996
1368
  index = opts.index('LIMIT')
997
1369
 
@@ -1008,6 +1380,9 @@ class Redis
1008
1380
  def mapped_param? param
1009
1381
  param.size == 1 && param[0].is_a?(Array)
1010
1382
  end
1383
+ # NOTE : Redis-rb 3.x will flatten *args, so method(["a", "b", "c"])
1384
+ # should be handled the same way as method("a", "b", "c")
1385
+ alias_method :flatten?, :mapped_param?
1011
1386
 
1012
1387
  def srandmember_single(key)
1013
1388
  data_type_check(key, ::Set)
@@ -1027,6 +1402,17 @@ class Redis
1027
1402
  (1..-number).map { data[key].to_a[rand(data[key].size)] }.flatten
1028
1403
  end
1029
1404
  end
1405
+
1406
+ def sort_keys(arr)
1407
+ # Sort by score, or if scores are equal, key alphanum
1408
+ sorted_keys = arr.sort do |(k1, v1), (k2, v2)|
1409
+ if v1 == v2
1410
+ k1 <=> k2
1411
+ else
1412
+ v1 <=> v2
1413
+ end
1414
+ end
1415
+ end
1030
1416
  end
1031
1417
  end
1032
1418
  end