rediska 0.0.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.
@@ -0,0 +1,54 @@
1
+ module Rediska
2
+ class SortedSetArgumentHandler
3
+ attr_reader :aggregate
4
+ attr_accessor :number_of_keys, :keys, :weights, :type
5
+
6
+ def initialize(args)
7
+ @number_of_keys = args.shift
8
+ @keys = args.shift(number_of_keys)
9
+ args.inject(self) {|handler, item| handler.handle(item) }
10
+
11
+ # Defaults.
12
+ @weights ||= Array.new(number_of_keys) { 1 }
13
+ @aggregate ||= :sum
14
+
15
+ # Validation.
16
+ raise Redis::CommandError, 'ERR syntax error' unless weights.size == number_of_keys
17
+ raise Redis::CommandError, 'ERR syntax error' unless [:min, :max, :sum].include?(aggregate)
18
+ end
19
+
20
+ def aggregate=(str)
21
+ raise Redis::CommandError, 'ERR syntax error' if @aggregate
22
+
23
+ @aggregate = str.to_s.downcase.to_sym
24
+ end
25
+
26
+ def handle(item)
27
+ case item
28
+ when 'WEIGHTS'
29
+ @type = :weights
30
+ @weights = []
31
+ when 'AGGREGATE'
32
+ @type = :aggregate
33
+ when nil
34
+ raise Redis::CommandError, 'ERR syntax error'
35
+ else
36
+ send "handle_#{type}", item
37
+ end
38
+
39
+ self
40
+ end
41
+
42
+ def handle_weights(item)
43
+ @weights << item
44
+ end
45
+
46
+ def handle_aggregate(item)
47
+ @aggregate = item
48
+ end
49
+
50
+ def inject_block
51
+ lambda { |handler, item| handler.handle(item) }
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,80 @@
1
+ module Rediska
2
+ class SortedSetStore
3
+ attr_accessor :data, :weights, :aggregate, :keys
4
+
5
+ def initialize(params, data)
6
+ @data = data
7
+ @weights = params.weights
8
+ @aggregate = params.aggregate
9
+ @keys = params.keys
10
+ end
11
+
12
+ def hashes
13
+ @hashes ||= keys.map do |src|
14
+ case data[src]
15
+ when ::Set
16
+ Hash[data[src].map {|k,v| [k, 1]}]
17
+ when Hash
18
+ data[src]
19
+ else
20
+ {}
21
+ end
22
+ end
23
+ end
24
+
25
+ def computed_values
26
+ @computed_values ||= begin
27
+ # Do nothing if all weights are 1, as n * 1 is n.
28
+ if weights.all? {|weight| weight == 1 }
29
+ values = hashes
30
+ # Otherwise, multiply the values in each hash by that hash's weighting
31
+ else
32
+ 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
+ end
38
+ end
39
+
40
+ def aggregate_sum(out)
41
+ selected_keys.each do |key|
42
+ out[key] = computed_values.inject(0) do |n, hash|
43
+ n + (hash[key] || 0)
44
+ end
45
+ end
46
+ end
47
+
48
+ def aggregate_min(out)
49
+ selected_keys.each do |key|
50
+ out[key] = computed_values.map {|h| h[key] }.compact.min
51
+ end
52
+ end
53
+
54
+ def aggregate_max(out)
55
+ selected_keys.each do |key|
56
+ out[key] = computed_values.map {|h| h[key] }.compact.max
57
+ end
58
+ end
59
+
60
+ def selected_keys
61
+ raise NotImplemented, "subclass needs to implement #selected_keys"
62
+ end
63
+
64
+ def call
65
+ ZSet.new.tap {|out| send("aggregate_#{aggregate}", out) }
66
+ end
67
+ end
68
+
69
+ class SortedSetIntersectStore < SortedSetStore
70
+ def selected_keys
71
+ @values ||= hashes.inject([]) { |r, h| r.empty? ? h.keys : (r & h.keys) }
72
+ end
73
+ end
74
+
75
+ class SortedSetUnionStore < SortedSetStore
76
+ def selected_keys
77
+ @values ||= hashes.map(&:keys).flatten.uniq
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,3 @@
1
+ module Rediska
2
+ VERSION = '0.0.2'
3
+ end
@@ -0,0 +1,26 @@
1
+ module Rediska
2
+ class ZSet < Hash
3
+ def []=(key, val)
4
+ super(key, floatify(val))
5
+ end
6
+
7
+ def increment(key, val)
8
+ self[key] += floatify(val)
9
+ end
10
+
11
+ def select_by_score(min, max)
12
+ min = floatify(min)
13
+ max = floatify(max)
14
+ reject {|_,v| v < min || v > max }
15
+ end
16
+
17
+ private
18
+ def floatify(str)
19
+ if inf = str.to_s.match(/^([+-])?inf/i)
20
+ (inf[1] == '-' ? -1.0 : 1.0) / 0.0
21
+ else
22
+ Float(str)
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/rediska.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'redis'
2
+ require 'rediska/configuration'
3
+ require 'rediska/connection'
4
+
5
+ module Rediska
6
+ extend self
7
+
8
+ attr_accessor :configuration
9
+
10
+ def configure
11
+ Redis::Connection.drivers << Rediska::Connection
12
+
13
+ self.configuration ||= Configuration.new
14
+ yield configuration
15
+ end
16
+ end
17
+
18
+ Rediska.configure {}
data/rediska.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ $:.push File.expand_path('../lib', __FILE__)
2
+ require 'rediska/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'rediska'
6
+ s.version = Rediska::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ['Leonid Beder']
9
+ s.email = ['leonid.beder@gmail.com']
10
+ s.license = 'MIT'
11
+ s.homepage = 'https://github.com/lbeder/rediska'
12
+ s.summary = 'A light-weighted redis driver for testing, development, and minimal environments'
13
+ s.description = 'A light-weighted redis driver for testing, development, and minimal environments,
14
+ which supports various data storage strategies'
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ['lib']
20
+
21
+ s.add_runtime_dependency 'redis', '~> 3.0.0'
22
+ s.add_development_dependency 'rspec', '~> 2.14'
23
+ s.add_development_dependency 'coveralls'
24
+ s.add_development_dependency 'pry'
25
+ s.add_development_dependency 'pry-debugger'
26
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples 'redis' do
4
+ it_behaves_like 'compatibility'
5
+ it_behaves_like 'hashes'
6
+ it_behaves_like 'connection'
7
+ it_behaves_like 'keys'
8
+ it_behaves_like 'lists'
9
+ it_behaves_like 'server'
10
+ it_behaves_like 'sets'
11
+ it_behaves_like 'strings'
12
+ it_behaves_like 'transactions'
13
+ it_behaves_like 'sorted sets'
14
+ it_behaves_like 'upcase method names'
15
+ end
16
+
17
+ describe 'Rediska' do
18
+ subject { Redis.new }
19
+
20
+ before do
21
+ Rediska.configure do |config|
22
+ config.namespace = 'rediska_test'
23
+ end
24
+ end
25
+
26
+ context 'fake redis' do
27
+ context 'memory' do
28
+ before do
29
+ subject.flushall
30
+ end
31
+
32
+ it_behaves_like 'redis'
33
+ end
34
+
35
+ context 'PStore' do
36
+ before do
37
+ Rediska.configure do |config|
38
+ config.database = :filesystem
39
+ end
40
+
41
+ subject.flushall
42
+ end
43
+
44
+ it_behaves_like 'redis'
45
+ end
46
+ end
47
+
48
+ # Remove the pending declaration in order to test interoperability with a local instance of redis.
49
+ pending 'real redis (interoperability)' do
50
+ before do
51
+ subject.flushall
52
+ end
53
+
54
+ before(:all) do
55
+ Redis::Connection.drivers.pop
56
+ end
57
+
58
+ after(:all) do
59
+ Redis::Connection.drivers << Rediska::Connection
60
+ end
61
+
62
+ it_behaves_like 'redis'
63
+ end
64
+ end
@@ -0,0 +1,15 @@
1
+ ENV['RACK_ENV'] = 'test'
2
+
3
+ require 'rubygems'
4
+ require 'coveralls'
5
+ require 'pry'
6
+ require 'pry-debugger'
7
+ require 'rediska'
8
+
9
+ Coveralls.wear!
10
+
11
+ Dir['./spec/support/*.rb'].each { |f| require f }
12
+
13
+ RSpec.configure do |config|
14
+ config.order = 'random'
15
+ end
@@ -0,0 +1,7 @@
1
+ shared_examples 'compatibility' do
2
+ it 'should be accessible through Rediska::Redis' do
3
+ expect {
4
+ Rediska::Redis.new
5
+ }.not_to raise_error
6
+ end
7
+ end
@@ -0,0 +1,65 @@
1
+ shared_examples 'connection' do
2
+ it 'should authenticate to the server' do
3
+ begin
4
+ subject.auth('pass').should eq('OK')
5
+ rescue Redis::CommandError => e
6
+ raise unless e.message == 'ERR Client sent AUTH, but no password is set'
7
+ end
8
+ end
9
+
10
+ it 'should re-use the same instance with the same host and port' do
11
+ subject1 = Redis.new(host: '127.0.0.1', port: 6379)
12
+ subject2 = Redis.new(host: '127.0.0.1', port: 6379)
13
+
14
+ subject1.set('key1', '1')
15
+ subject2.get('key1').should eq('1')
16
+
17
+ subject2.set('key2', '2')
18
+ subject1.get('key2').should eq('2')
19
+
20
+ subject1.get('key3').should be_nil
21
+ subject2.get('key3').should be_nil
22
+
23
+ subject1.dbsize.should eq(2)
24
+ subject2.dbsize.should eq(2)
25
+ end
26
+
27
+ it 'should connect to a specific database' do
28
+ subject1 = Redis.new(host: '127.0.0.1', port: 6379, db: 0)
29
+ subject1.set('key1', '1')
30
+ subject1.select(0)
31
+ subject1.get('key1').should eq('1')
32
+
33
+ subject2 = Redis.new(host: '127.0.0.1', port: 6379, db: 1)
34
+ subject2.set('key1', '1')
35
+ subject2.select(1)
36
+ subject2.get('key1').should eq('1')
37
+ end
38
+
39
+ it 'should handle multiple clients using the same db instance' do
40
+ subject1 = Redis.new(host: '127.0.0.1', port: 6379, db: 1)
41
+ subject2 = Redis.new(host: '127.0.0.1', port: 6379, db: 2)
42
+
43
+ subject1.set('key1', 'one')
44
+ subject1.get('key1').should eq('one')
45
+
46
+ subject2.set('key2', 'two')
47
+ subject2.get('key2').should eq('two')
48
+
49
+ subject1.get('key1').should eq('one')
50
+ end
51
+
52
+ it 'should not error with a disconnected client' do
53
+ subject1 = Redis.new
54
+ subject1.client.disconnect
55
+ subject1.get('key1').should be_nil
56
+ end
57
+
58
+ it 'should echo the given string' do
59
+ subject.echo('something').should eq('something')
60
+ end
61
+
62
+ it 'should ping the server' do
63
+ subject.ping.should eq('PONG')
64
+ end
65
+ end
@@ -0,0 +1,189 @@
1
+ shared_examples 'hashes' do
2
+ it 'should delete a hash field' do
3
+ subject.hset('key1', 'k1', 'val1')
4
+ subject.hset('key1', 'k2', 'val2')
5
+ subject.hdel('key1', 'k1')
6
+
7
+ subject.hget('key1', 'k1').should be_nil
8
+ subject.hget('key1', 'k2').should eq('val2')
9
+ end
10
+
11
+ it 'should remove a hash with no keys left' do
12
+ subject.hset('key1', 'k1', 'val1')
13
+ subject.hset('key1', 'k2', 'val2')
14
+ subject.hdel('key1', 'k1')
15
+ subject.hdel('key1', 'k2')
16
+
17
+ subject.exists('key1').should be_false
18
+ end
19
+
20
+ it 'should convert key to a string for hset' do
21
+ m = double('key')
22
+ m.stub(:to_s).and_return('foo')
23
+
24
+ subject.hset('key1', m, 'bar')
25
+ subject.hget('key1', 'foo').should eq('bar')
26
+ end
27
+
28
+ it 'should convert key to a string for hget' do
29
+ m = double('key')
30
+ m.stub(:to_s).and_return('foo')
31
+
32
+ subject.hset('key1', 'foo', 'bar')
33
+ subject.hget('key1', m).should eq('bar')
34
+ end
35
+
36
+ it 'should determine if a hash field exists' do
37
+ subject.hset('key1', 'index', 'value')
38
+
39
+ subject.hexists('key1', 'index').should be_true
40
+ subject.hexists('key2', 'i2').should be_false
41
+ end
42
+
43
+ it 'should get the value of a hash field' do
44
+ subject.hset('key1', 'index', 'value')
45
+
46
+ subject.hget('key1', 'index').should eq('value')
47
+ end
48
+
49
+ it 'should get all the fields and values in a hash' do
50
+ subject.hset('key1', 'i1', 'val1')
51
+ subject.hset('key1', 'i2', 'val2')
52
+
53
+ subject.hgetall('key1').should eq({'i1' => 'val1', 'i2' => 'val2'})
54
+ end
55
+
56
+ it 'should increment the integer value of a hash field by the given number' do
57
+ subject.hset('key1', 'cont1', '5')
58
+ subject.hincrby('key1', 'cont1', '5').should eq(10)
59
+ subject.hget('key1', 'cont1').should eq('10')
60
+ end
61
+
62
+ it 'should increment non existing hash keys' do
63
+ subject.hget('key1', 'cont2').should be_nil
64
+ subject.hincrby('key1', 'cont2', '5').should eq(5)
65
+ end
66
+
67
+ it 'should get all the fields in a hash' do
68
+ subject.hset('key1', 'i1', 'val1')
69
+ subject.hset('key1', 'i2', 'val2')
70
+
71
+ subject.hkeys('key1').should eq(['i1', 'i2'])
72
+ subject.hkeys('key2').should be_empty
73
+ end
74
+
75
+ it 'should get the number of fields in a hash' do
76
+ subject.hset('key1', 'i1', 'val1')
77
+ subject.hset('key1', 'i2', 'val2')
78
+
79
+ subject.hlen('key1').should eq(2)
80
+ end
81
+
82
+ it 'should get the values of all the given hash fields' do
83
+ subject.hset('key1', 'i1', 'val1')
84
+ subject.hset('key1', 'i2', 'val2')
85
+
86
+ subject.hmget('key1', 'i1', 'i2', 'i3').should eq(['val1', 'val2', nil])
87
+ subject.hmget('key2', 'i1', 'i2').should eq([nil, nil])
88
+ end
89
+
90
+ it 'should throw an argument error when you do not ask for any keys' do
91
+ expect {
92
+ subject.hmget('key1')
93
+ }.to raise_error(Redis::CommandError)
94
+ end
95
+
96
+ it 'should reject an empty list of values' do
97
+ expect {
98
+ subject.hmset('key')
99
+ }.to raise_error(Redis::CommandError)
100
+
101
+ subject.exists('key').should be_false
102
+ end
103
+
104
+ it 'rejects an insert with a key but no value' do
105
+ expect {
106
+ subject.hmset('key', 'foo')
107
+ }.to raise_error(Redis::CommandError)
108
+
109
+ expect {
110
+ subject.hmset('key', 'foo', 3, 'bar')
111
+ }.to raise_error(Redis::CommandError)
112
+
113
+ subject.exists('key').should be_false
114
+ end
115
+
116
+ it 'should reject the wrong number of arguments' do
117
+ expect {
118
+ subject.hmset('hash', 'foo1', 'bar1', 'foo2', 'bar2', 'foo3')
119
+ }.to raise_error(Redis::CommandError)
120
+ end
121
+
122
+ it 'should set multiple hash fields to multiple values' do
123
+ subject.hmset('key', 'k1', 'value1', 'k2', 'value2')
124
+
125
+ subject.hget('key', 'k1').should eq('value1')
126
+ subject.hget('key', 'k2').should eq('value2')
127
+ end
128
+
129
+ it 'should set multiple hash fields from a ruby hash to multiple values' do
130
+ subject.mapped_hmset('foo', k1: 'value1', k2: 'value2')
131
+
132
+ subject.hget('foo', 'k1').should eq('value1')
133
+ subject.hget('foo', 'k2').should eq('value2')
134
+ end
135
+
136
+ it 'should set the string value of a hash field' do
137
+ subject.hset('key1', 'k1', 'val1').should be_true
138
+ subject.hset('key1', 'k1', 'val1').should be_false
139
+
140
+ subject.hget('key1', 'k1').should eq('val1')
141
+ end
142
+
143
+ it 'should set the value of a hash field, only if the field does not exist' do
144
+ subject.hset('key1', 'k1', 'val1')
145
+ subject.hsetnx('key1', 'k1', 'value').should be_false
146
+ subject.hsetnx('key1', 'k2', 'val2').should be_true
147
+ subject.hsetnx('key1', :k1, 'value').should be_false
148
+ subject.hsetnx('key1', :k3, 'val3').should be_true
149
+
150
+ subject.hget('key1', 'k1').should eq('val1')
151
+ subject.hget('key1', 'k2').should eq('val2')
152
+ subject.hget('key1', 'k3').should eq('val3')
153
+ end
154
+
155
+ it 'should get all the values in a hash' do
156
+ subject.hset('key1', 'k1', 'val1')
157
+ subject.hset('key1', 'k2', 'val2')
158
+
159
+ subject.hvals('key1').should eq(['val1', 'val2'])
160
+ end
161
+
162
+ it 'should accept a list of array pairs as arguments and not throw an invalid argument number error' do
163
+ subject.hmset('key1', [:k1, 'val1'], [:k2, 'val2'], [:k3, 'val3'])
164
+ subject.hget('key1', :k1).should eq('val1')
165
+ subject.hget('key1', :k2).should eq('val2')
166
+ subject.hget('key1', :k3).should eq('val3')
167
+ end
168
+
169
+ it 'should reject a list of arrays that contain an invalid number of arguments' do
170
+ expect {
171
+ subject.hmset('key1', [:k1, 'val1'], [:k2, 'val2', 'bogus val'])
172
+ }.to raise_error(Redis::CommandError)
173
+ end
174
+
175
+ it 'should convert a integer field name to string for hdel' do
176
+ subject.hset('key1', '1', 1)
177
+ subject.hdel('key1', 1).should eq(1)
178
+ end
179
+
180
+ it 'should convert a integer field name to string for hexists' do
181
+ subject.hset('key1', '1', 1)
182
+ subject.hexists('key1', 1).should be_true
183
+ end
184
+
185
+ it 'should convert a integer field name to string for hincrby' do
186
+ subject.hset('key1', 1, 0)
187
+ subject.hincrby('key1', 1, 1).should eq(1)
188
+ end
189
+ end