ryansch-mock_redis 0.2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. data/.gitignore +4 -0
  2. data/.rspec +1 -0
  3. data/CHANGELOG.md +20 -0
  4. data/Gemfile +9 -0
  5. data/LICENSE +19 -0
  6. data/README.md +94 -0
  7. data/Rakefile +10 -0
  8. data/lib/mock_redis.rb +32 -0
  9. data/lib/mock_redis/assertions.rb +13 -0
  10. data/lib/mock_redis/database.rb +432 -0
  11. data/lib/mock_redis/exceptions.rb +3 -0
  12. data/lib/mock_redis/expire_wrapper.rb +25 -0
  13. data/lib/mock_redis/hash_methods.rb +102 -0
  14. data/lib/mock_redis/list_methods.rb +187 -0
  15. data/lib/mock_redis/multi_db_wrapper.rb +86 -0
  16. data/lib/mock_redis/set_methods.rb +125 -0
  17. data/lib/mock_redis/string_methods.rb +195 -0
  18. data/lib/mock_redis/transaction_wrapper.rb +80 -0
  19. data/lib/mock_redis/undef_redis_methods.rb +11 -0
  20. data/lib/mock_redis/utility_methods.rb +22 -0
  21. data/lib/mock_redis/version.rb +3 -0
  22. data/lib/mock_redis/zset.rb +110 -0
  23. data/lib/mock_redis/zset_methods.rb +209 -0
  24. data/mock_redis.gemspec +24 -0
  25. data/spec/cloning_spec.rb +96 -0
  26. data/spec/commands/append_spec.rb +24 -0
  27. data/spec/commands/auth_spec.rb +7 -0
  28. data/spec/commands/bgrewriteaof_spec.rb +7 -0
  29. data/spec/commands/bgsave_spec.rb +7 -0
  30. data/spec/commands/blpop_spec.rb +55 -0
  31. data/spec/commands/brpop_spec.rb +54 -0
  32. data/spec/commands/brpoplpush_spec.rb +53 -0
  33. data/spec/commands/dbsize_spec.rb +18 -0
  34. data/spec/commands/decr_spec.rb +34 -0
  35. data/spec/commands/decrby_spec.rb +34 -0
  36. data/spec/commands/del_spec.rb +20 -0
  37. data/spec/commands/echo_spec.rb +11 -0
  38. data/spec/commands/exists_spec.rb +14 -0
  39. data/spec/commands/expire_spec.rb +83 -0
  40. data/spec/commands/expireat_spec.rb +48 -0
  41. data/spec/commands/flushall_spec.rb +38 -0
  42. data/spec/commands/flushdb_spec.rb +38 -0
  43. data/spec/commands/get_spec.rb +23 -0
  44. data/spec/commands/getbit_spec.rb +34 -0
  45. data/spec/commands/getrange_spec.rb +22 -0
  46. data/spec/commands/getset_spec.rb +23 -0
  47. data/spec/commands/hdel_spec.rb +35 -0
  48. data/spec/commands/hexists_spec.rb +22 -0
  49. data/spec/commands/hget_spec.rb +23 -0
  50. data/spec/commands/hgetall_spec.rb +22 -0
  51. data/spec/commands/hincrby_spec.rb +52 -0
  52. data/spec/commands/hkeys_spec.rb +19 -0
  53. data/spec/commands/hlen_spec.rb +19 -0
  54. data/spec/commands/hmget_spec.rb +30 -0
  55. data/spec/commands/hmset_spec.rb +43 -0
  56. data/spec/commands/hset_spec.rb +23 -0
  57. data/spec/commands/hsetnx_spec.rb +39 -0
  58. data/spec/commands/hvals_spec.rb +19 -0
  59. data/spec/commands/incr_spec.rb +34 -0
  60. data/spec/commands/incrby_spec.rb +44 -0
  61. data/spec/commands/info_spec.rb +13 -0
  62. data/spec/commands/keys_spec.rb +87 -0
  63. data/spec/commands/lastsave_spec.rb +8 -0
  64. data/spec/commands/lindex_spec.rb +39 -0
  65. data/spec/commands/linsert_spec.rb +68 -0
  66. data/spec/commands/llen_spec.rb +16 -0
  67. data/spec/commands/lpop_spec.rb +34 -0
  68. data/spec/commands/lpush_spec.rb +30 -0
  69. data/spec/commands/lpushx_spec.rb +33 -0
  70. data/spec/commands/lrange_spec.rb +35 -0
  71. data/spec/commands/lrem_spec.rb +79 -0
  72. data/spec/commands/lset_spec.rb +38 -0
  73. data/spec/commands/ltrim_spec.rb +35 -0
  74. data/spec/commands/mget_spec.rb +34 -0
  75. data/spec/commands/move_spec.rb +147 -0
  76. data/spec/commands/mset_spec.rb +29 -0
  77. data/spec/commands/msetnx_spec.rb +40 -0
  78. data/spec/commands/persist_spec.rb +49 -0
  79. data/spec/commands/ping_spec.rb +7 -0
  80. data/spec/commands/quit_spec.rb +7 -0
  81. data/spec/commands/randomkey_spec.rb +20 -0
  82. data/spec/commands/rename_spec.rb +31 -0
  83. data/spec/commands/renamenx_spec.rb +36 -0
  84. data/spec/commands/rpop_spec.rb +34 -0
  85. data/spec/commands/rpoplpush_spec.rb +45 -0
  86. data/spec/commands/rpush_spec.rb +30 -0
  87. data/spec/commands/rpushx_spec.rb +33 -0
  88. data/spec/commands/sadd_spec.rb +22 -0
  89. data/spec/commands/save_spec.rb +7 -0
  90. data/spec/commands/scard_spec.rb +18 -0
  91. data/spec/commands/sdiff_spec.rb +47 -0
  92. data/spec/commands/sdiffstore_spec.rb +58 -0
  93. data/spec/commands/select_spec.rb +53 -0
  94. data/spec/commands/set_spec.rb +7 -0
  95. data/spec/commands/setbit_spec.rb +46 -0
  96. data/spec/commands/setex_spec.rb +22 -0
  97. data/spec/commands/setnx_spec.rb +25 -0
  98. data/spec/commands/setrange_spec.rb +30 -0
  99. data/spec/commands/sinter_spec.rb +41 -0
  100. data/spec/commands/sinterstore_spec.rb +53 -0
  101. data/spec/commands/sismember_spec.rb +29 -0
  102. data/spec/commands/smembers_spec.rb +18 -0
  103. data/spec/commands/smove_spec.rb +41 -0
  104. data/spec/commands/spop_spec.rb +25 -0
  105. data/spec/commands/srandmember_spec.rb +25 -0
  106. data/spec/commands/srem_spec.rb +35 -0
  107. data/spec/commands/strlen_spec.rb +19 -0
  108. data/spec/commands/sunion_spec.rb +40 -0
  109. data/spec/commands/sunionstore_spec.rb +53 -0
  110. data/spec/commands/ttl_spec.rb +36 -0
  111. data/spec/commands/type_spec.rb +36 -0
  112. data/spec/commands/unwatch_spec.rb +7 -0
  113. data/spec/commands/watch_spec.rb +7 -0
  114. data/spec/commands/zadd_spec.rb +29 -0
  115. data/spec/commands/zcard_spec.rb +19 -0
  116. data/spec/commands/zcount_spec.rb +23 -0
  117. data/spec/commands/zincrby_spec.rb +24 -0
  118. data/spec/commands/zinterstore_spec.rb +96 -0
  119. data/spec/commands/zrange_spec.rb +31 -0
  120. data/spec/commands/zrangebyscore_spec.rb +68 -0
  121. data/spec/commands/zrank_spec.rb +23 -0
  122. data/spec/commands/zrem_spec.rb +25 -0
  123. data/spec/commands/zremrangebyrank_spec.rb +22 -0
  124. data/spec/commands/zremrangebyscore_spec.rb +28 -0
  125. data/spec/commands/zrevrange_spec.rb +31 -0
  126. data/spec/commands/zrevrangebyscore_spec.rb +47 -0
  127. data/spec/commands/zrevrank_spec.rb +23 -0
  128. data/spec/commands/zscore_spec.rb +16 -0
  129. data/spec/commands/zunionstore_spec.rb +104 -0
  130. data/spec/spec_helper.rb +44 -0
  131. data/spec/support/redis_multiplexer.rb +91 -0
  132. data/spec/support/shared_examples/only_operates_on_hashes.rb +13 -0
  133. data/spec/support/shared_examples/only_operates_on_lists.rb +13 -0
  134. data/spec/support/shared_examples/only_operates_on_sets.rb +13 -0
  135. data/spec/support/shared_examples/only_operates_on_strings.rb +13 -0
  136. data/spec/support/shared_examples/only_operates_on_zsets.rb +57 -0
  137. data/spec/transactions_spec.rb +96 -0
  138. metadata +361 -0
@@ -0,0 +1,195 @@
1
+ require 'mock_redis/assertions'
2
+
3
+ class MockRedis
4
+ module StringMethods
5
+ include Assertions
6
+
7
+ def append(key, value)
8
+ assert_stringy(key)
9
+ data[key] ||= ""
10
+ data[key] << value
11
+ data[key].length
12
+ end
13
+
14
+ def decr(key)
15
+ decrby(key, 1)
16
+ end
17
+
18
+ def decrby(key, n)
19
+ incrby(key, -n)
20
+ end
21
+
22
+ def get(key)
23
+ assert_stringy(key)
24
+ data[key]
25
+ end
26
+
27
+ def getbit(key, offset)
28
+ assert_stringy(key)
29
+
30
+ offset_of_byte = offset / 8
31
+ offset_within_byte = offset % 8
32
+
33
+ # String#getbyte would be lovely, but it's not in 1.8.7.
34
+ byte = (data[key] || "").each_byte.drop(offset_of_byte).first
35
+
36
+ if byte
37
+ (byte & (2**7 >> offset_within_byte)) > 0 ? 1 : 0
38
+ else
39
+ 0
40
+ end
41
+ end
42
+
43
+ def getrange(key, start, stop)
44
+ assert_stringy(key)
45
+ (data[key] || "")[start..stop]
46
+ end
47
+
48
+ def getset(key, value)
49
+ retval = get(key)
50
+ set(key, value)
51
+ retval
52
+ end
53
+
54
+ def incr(key)
55
+ incrby(key, 1)
56
+ end
57
+
58
+ def incrby(key, n)
59
+ assert_stringy(key)
60
+ unless can_incr?(data[key])
61
+ raise RuntimeError, "ERR value is not an integer or out of range"
62
+ end
63
+
64
+ unless looks_like_integer?(n.to_s)
65
+ raise RuntimeError, "ERR value is not an integer or out of range"
66
+ end
67
+
68
+ new_value = data[key].to_i + n.to_i
69
+ data[key] = new_value.to_s
70
+ # for some reason, redis-rb doesn't return this as a string.
71
+ new_value
72
+ end
73
+
74
+ def mget(*keys)
75
+ assert_has_args(keys, 'mget')
76
+
77
+ keys.map do |key|
78
+ get(key) if stringy?(key)
79
+ end
80
+ end
81
+
82
+ def mset(*kvpairs)
83
+ assert_has_args(kvpairs, 'mset')
84
+ if kvpairs.length.odd?
85
+ raise RuntimeError, "ERR wrong number of arguments for MSET"
86
+ end
87
+
88
+ kvpairs.each_slice(2) do |(k,v)|
89
+ set(k,v)
90
+ end
91
+
92
+ "OK"
93
+ end
94
+
95
+ def msetnx(*kvpairs)
96
+ assert_has_args(kvpairs, 'msetnx')
97
+
98
+ if kvpairs.each_slice(2).any? {|(k,v)| exists(k)}
99
+ 0
100
+ else
101
+ mset(*kvpairs)
102
+ 1
103
+ end
104
+ end
105
+
106
+ def set(key, value)
107
+ data[key] = value.to_s
108
+ 'OK'
109
+ end
110
+
111
+ def setbit(key, offset, value)
112
+ assert_stringy(key, "ERR bit is not an integer or out of range")
113
+ retval = getbit(key, offset)
114
+
115
+ str = data[key] || ""
116
+
117
+ offset_of_byte = offset / 8
118
+ offset_within_byte = offset % 8
119
+
120
+ if offset_of_byte >= str.bytesize
121
+ str = zero_pad(str, offset_of_byte+1)
122
+ end
123
+
124
+ char_index = byte_index = offset_within_char = 0
125
+ str.each_char do |c|
126
+ if byte_index < offset_of_byte
127
+ char_index += 1
128
+ byte_index += c.bytesize
129
+ else
130
+ offset_within_char = byte_index - offset_of_byte
131
+ break
132
+ end
133
+ end
134
+
135
+ char = str[char_index]
136
+ char = char.chr if char.respond_to?(:chr) # ruby 1.8 vs 1.9
137
+ char_as_number = char.each_byte.reduce(0) do |a, byte|
138
+ (a << 8) + byte
139
+ end
140
+ char_as_number |=
141
+ (2**((char.bytesize * 8)-1) >>
142
+ (offset_within_char * 8 + offset_within_byte))
143
+ str[char_index] = char_as_number.chr
144
+
145
+ data[key] = str
146
+ retval
147
+ end
148
+
149
+ def setex(key, seconds, value)
150
+ set(key, value)
151
+ expire(key, seconds)
152
+ 'OK'
153
+ end
154
+
155
+ def setnx(key, value)
156
+ if exists(key)
157
+ false
158
+ else
159
+ set(key, value)
160
+ true
161
+ end
162
+ end
163
+
164
+ def setrange(key, offset, value)
165
+ assert_stringy(key)
166
+ value = value.to_s
167
+ old_value = (data[key] || "")
168
+
169
+ prefix = zero_pad(old_value[0...offset], offset)
170
+ data[key] = prefix + value + (old_value[(offset + value.length)..-1] || "")
171
+ data[key].length
172
+ end
173
+
174
+ def strlen(key)
175
+ assert_stringy(key)
176
+ (data[key] || "").bytesize
177
+ end
178
+
179
+
180
+
181
+
182
+ private
183
+ def stringy?(key)
184
+ data[key].nil? || data[key].kind_of?(String)
185
+ end
186
+
187
+ def assert_stringy(key,
188
+ message="ERR Operation against a key holding the wrong kind of value")
189
+ unless stringy?(key)
190
+ raise RuntimeError, message
191
+ end
192
+ end
193
+
194
+ end
195
+ end
@@ -0,0 +1,80 @@
1
+ require 'mock_redis/undef_redis_methods'
2
+
3
+ class MockRedis
4
+ class TransactionWrapper
5
+ include UndefRedisMethods
6
+
7
+ def respond_to?(method, include_private=false)
8
+ super || @db.respond_to?(method)
9
+ end
10
+
11
+ def initialize(db)
12
+ @db = db
13
+ @queued_commands = []
14
+ @in_multi = false
15
+ end
16
+
17
+ def method_missing(method, *args)
18
+ if @in_multi
19
+ @queued_commands << [method, *args]
20
+ 'QUEUED'
21
+ else
22
+ @db.expire_keys
23
+ @db.send(method, *args)
24
+ end
25
+ end
26
+
27
+ def initialize_copy(source)
28
+ super
29
+ @db = @db.clone
30
+ @queued_commands = @queued_commands.clone
31
+ end
32
+
33
+ def discard
34
+ unless @in_multi
35
+ raise RuntimeError, "ERR DISCARD without MULTI"
36
+ end
37
+ @in_multi = false
38
+ @queued_commands = []
39
+ 'OK'
40
+ end
41
+
42
+ def exec
43
+ unless @in_multi
44
+ raise RuntimeError, "ERR EXEC without MULTI"
45
+ end
46
+ @in_multi = false
47
+ responses = @queued_commands.map do |cmd|
48
+ begin
49
+ send(*cmd)
50
+ rescue => e
51
+ e
52
+ end
53
+ end
54
+ @queued_commands = []
55
+ responses
56
+ end
57
+
58
+ def multi
59
+ if @in_multi
60
+ raise RuntimeError, "ERR MULTI calls can not be nested"
61
+ end
62
+ @in_multi = true
63
+ if block_given?
64
+ yield(self)
65
+ self.exec
66
+ else
67
+ 'OK'
68
+ end
69
+ end
70
+
71
+ def unwatch
72
+ 'OK'
73
+ end
74
+
75
+ def watch(_)
76
+ 'OK'
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,11 @@
1
+ class MockRedis
2
+ module UndefRedisMethods
3
+ def self.included(klass)
4
+ if klass.instance_methods.map(&:to_s).include?('type')
5
+ klass.send(:undef_method, 'type')
6
+ end
7
+ klass.send(:undef_method, 'exec')
8
+ klass.send(:undef_method, 'select')
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ class MockRedis
2
+ module UtilityMethods
3
+ private
4
+
5
+ def with_thing_at(key, assertion, empty_thing_generator)
6
+ begin
7
+ send(assertion, key)
8
+ data[key] ||= empty_thing_generator.call
9
+ yield data[key]
10
+ ensure
11
+ clean_up_empties_at(key)
12
+ end
13
+ end
14
+
15
+ def clean_up_empties_at(key)
16
+ if data[key] && data[key].empty?
17
+ del(key)
18
+ end
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ class MockRedis
2
+ VERSION = "0.2.0.1"
3
+ end
@@ -0,0 +1,110 @@
1
+ require 'forwardable'
2
+ require 'set'
3
+
4
+ class MockRedis
5
+ class Zset
6
+ include Enumerable
7
+ extend Forwardable
8
+
9
+ attr_reader :members, :scores
10
+
11
+ def_delegators :members, :empty?, :include?, :size
12
+
13
+ def initialize
14
+ @members = Set.new
15
+ @scores = Hash.new
16
+ end
17
+
18
+ def initialize_copy(source)
19
+ super
20
+ @members = @members.clone
21
+ @scores = @scores.clone
22
+ end
23
+
24
+ def add(score, member)
25
+ members.add(member)
26
+ if score.to_f.to_i == score.to_f
27
+ scores[member] = score.to_f.to_i
28
+ else
29
+ scores[member] = score.to_f
30
+ end
31
+ self
32
+ end
33
+
34
+ def delete?(member)
35
+ scores.delete(member)
36
+ members.delete?(member) and self
37
+ end
38
+
39
+ def each
40
+ members.each {|m| yield score(m), m}
41
+ end
42
+
43
+ def in_range(min, max)
44
+ in_from_the_left = case min
45
+ when "-inf"
46
+ lambda {|_| true }
47
+ when "+inf"
48
+ lambda {|_| false }
49
+ when /\((.*)$/
50
+ val = $1.to_f
51
+ lambda {|x| x.to_f > val }
52
+ else
53
+ lambda {|x| x.to_f >= min.to_f }
54
+ end
55
+
56
+ in_from_the_right = case max
57
+ when "-inf"
58
+ lambda {|_| false }
59
+ when "+inf"
60
+ lambda {|_| true }
61
+ when /\((.*)$/
62
+ val = $1.to_f
63
+ lambda {|x| x.to_f < val }
64
+ else
65
+ lambda {|x| x.to_f <= max.to_f }
66
+ end
67
+
68
+ sorted.find_all do |(score, member)|
69
+ in_from_the_left[score] && in_from_the_right[score]
70
+ end
71
+ end
72
+
73
+ def intersection(other)
74
+ if !block_given?
75
+ intersection(other, &:+)
76
+ else
77
+ self.members.intersection(other.members).reduce(self.class.new) do |acc, m|
78
+ new_score = yield(self.score(m), other.score(m))
79
+ acc.add(new_score, m)
80
+ end
81
+ end
82
+ end
83
+
84
+ def score(member)
85
+ scores[member]
86
+ end
87
+
88
+ def sorted
89
+ members.map do |m|
90
+ [score(m), m]
91
+ end.sort_by(&:first)
92
+ end
93
+
94
+ def sorted_members
95
+ sorted.map(&:last)
96
+ end
97
+
98
+ def union(other)
99
+ if !block_given?
100
+ union(other, &:+)
101
+ else
102
+ self.members.union(other.members).reduce(self.class.new) do |acc, m|
103
+ new_score = yield(self.score(m), other.score(m))
104
+ acc.add(new_score, m)
105
+ end
106
+ end
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,209 @@
1
+ require 'mock_redis/assertions'
2
+ require 'mock_redis/utility_methods'
3
+ require 'mock_redis/zset'
4
+
5
+ class MockRedis
6
+ module ZsetMethods
7
+ include Assertions
8
+ include UtilityMethods
9
+
10
+ def zadd(key, score, member)
11
+ assert_scorey(score)
12
+
13
+ retval = !zscore(key, member)
14
+ with_zset_at(key) {|z| z.add(score, member)}
15
+ retval
16
+ end
17
+
18
+ def zcard(key)
19
+ with_zset_at(key, &:size)
20
+ end
21
+
22
+ def zcount(key, min, max)
23
+ assert_scorey(min, 'min or max')
24
+ assert_scorey(max, 'min or max')
25
+
26
+ with_zset_at(key) do |z|
27
+ z.count do |score, _|
28
+ score >= min && score <= max
29
+ end
30
+ end
31
+ end
32
+
33
+ def zincrby(key, increment, member)
34
+ assert_scorey(increment)
35
+ with_zset_at(key) do |z|
36
+ old_score = z.include?(member) ? z.score(member) : 0
37
+ new_score = old_score + increment
38
+ z.add(new_score, member)
39
+ new_score.to_s
40
+ end
41
+ end
42
+
43
+ def zinterstore(destination, keys, options={})
44
+ assert_has_args(keys, 'zinterstore')
45
+
46
+ data[destination] = combine_weighted_zsets(keys, options, :intersection)
47
+ zcard(destination)
48
+ end
49
+
50
+ def zrange(key, start, stop, options={})
51
+ with_zset_at(key) do |z|
52
+ to_response(z.sorted[start..stop], options)
53
+ end
54
+ end
55
+
56
+ def zrangebyscore(key, min, max, options={})
57
+ with_zset_at(key) do |zset|
58
+ all_results = zset.in_range(min, max)
59
+ to_response(apply_limit(all_results, options[:limit]), options)
60
+ end
61
+ end
62
+
63
+ def zrank(key, member)
64
+ with_zset_at(key) {|z| z.sorted_members.index(member) }
65
+ end
66
+
67
+ def zrem(key, member)
68
+ with_zset_at(key) {|z| !!z.delete?(member)}
69
+ end
70
+
71
+ def zrevrange(key, start, stop, options={})
72
+ with_zset_at(key) do |z|
73
+ to_response(z.sorted.reverse[start..stop], options)
74
+ end
75
+ end
76
+
77
+ def zremrangebyrank(key, start, stop)
78
+ zrange(key, start, stop).
79
+ each {|member| zrem(key, member)}.
80
+ size
81
+ end
82
+
83
+ def zremrangebyscore(key, min, max)
84
+ zrangebyscore(key, min, max).
85
+ each {|member| zrem(key, member)}.
86
+ size
87
+ end
88
+
89
+ def zrevrangebyscore(key, max, min, options={})
90
+ with_zset_at(key) do |zset|
91
+ to_response(
92
+ apply_limit(
93
+ zset.in_range(min, max).reverse,
94
+ options[:limit]),
95
+ options)
96
+ end
97
+ end
98
+
99
+ def zrevrank(key, member)
100
+ with_zset_at(key) {|z| z.sorted_members.reverse.index(member) }
101
+ end
102
+
103
+ def zscore(key, member)
104
+ with_zset_at(key) do |z|
105
+ score = z.score(member)
106
+ score.to_s if score
107
+ end
108
+ end
109
+
110
+ def zunionstore(destination, keys, options={})
111
+ assert_has_args(keys, 'zunionstore')
112
+
113
+ data[destination] = combine_weighted_zsets(keys, options, :union)
114
+ zcard(destination)
115
+ end
116
+
117
+ private
118
+ def apply_limit(collection, limit)
119
+ if limit
120
+ if limit.is_a?(Array) && limit.length == 2
121
+ offset, count = limit
122
+ collection.drop(offset).take(count)
123
+ else
124
+ raise RuntimeError, "ERR syntax error"
125
+ end
126
+ else
127
+ collection
128
+ end
129
+ end
130
+
131
+ def to_response(score_member_pairs, options)
132
+ score_member_pairs.map do |(score,member)|
133
+ if options[:with_scores] || options[:withscores]
134
+ [member, score.to_s]
135
+ else
136
+ member
137
+ end
138
+ end.flatten
139
+ end
140
+
141
+ def combine_weighted_zsets(keys, options, how)
142
+ weights = options.fetch(:weights, keys.map { 1 })
143
+ if weights.length != keys.length
144
+ raise RuntimeError, "ERR syntax error"
145
+ end
146
+
147
+ aggregator = case options.fetch(:aggregate, :sum).to_s.downcase.to_sym
148
+ when :sum
149
+ proc {|a,b| [a,b].compact.reduce(&:+)}
150
+ when :min
151
+ proc {|a,b| [a,b].compact.min}
152
+ when :max
153
+ proc {|a,b| [a,b].compact.max}
154
+ else
155
+ raise RuntimeError, "ERR syntax error"
156
+ end
157
+
158
+ with_zsets_at(*keys) do |*zsets|
159
+ zsets.zip(weights).map do |(zset, weight)|
160
+ zset.reduce(Zset.new) do |acc, (score, member)|
161
+ acc.add(score * weight, member)
162
+ end
163
+ end.reduce do |za, zb|
164
+ za.send(how, zb, &aggregator)
165
+ end
166
+ end
167
+
168
+ end
169
+
170
+ def with_zset_at(key, &blk)
171
+ with_thing_at(key, :assert_zsety, proc {Zset.new}, &blk)
172
+ end
173
+
174
+ def with_zsets_at(*keys, &blk)
175
+ if keys.length == 1
176
+ with_zset_at(keys.first, &blk)
177
+ else
178
+ with_zset_at(keys.first) do |set|
179
+ with_zsets_at(*(keys[1..-1])) do |*sets|
180
+ blk.call(*([set] + sets))
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ def zsety?(key)
187
+ data[key].nil? || data[key].kind_of?(Zset)
188
+ end
189
+
190
+ def assert_zsety(key)
191
+ unless zsety?(key)
192
+ raise RuntimeError,
193
+ "ERR Operation against a key holding the wrong kind of value"
194
+ end
195
+ end
196
+
197
+ def looks_like_float?(x)
198
+ # ugh, exceptions for flow control.
199
+ !!Float(x) rescue false
200
+ end
201
+
202
+ def assert_scorey(value, what='value')
203
+ unless looks_like_float?(value)
204
+ raise RuntimeError, "ERR #{what} is not a double"
205
+ end
206
+ end
207
+
208
+ end
209
+ end