redis-objects 0.1.0 → 0.2.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.
- data/README.rdoc +146 -39
- data/lib/redis/counter.rb +2 -3
- data/lib/redis/list.rb +59 -32
- data/lib/redis/lock.rb +2 -2
- data/lib/redis/objects/counters.rb +1 -1
- data/lib/redis/objects/lists.rb +1 -1
- data/lib/redis/objects/locks.rb +1 -1
- data/lib/redis/objects/sets.rb +25 -0
- data/lib/redis/objects/values.rb +1 -1
- data/lib/redis/set.rb +126 -0
- data/lib/redis/value.rb +2 -2
- data/spec/redis_objects_instance_spec.rb +18 -0
- data/spec/redis_objects_model_spec.rb +146 -1
- metadata +18 -8
data/README.rdoc
CHANGED
|
@@ -1,85 +1,192 @@
|
|
|
1
|
-
= Redis::Objects -
|
|
1
|
+
= Redis::Objects - Map Redis types directly to Ruby objects
|
|
2
2
|
|
|
3
3
|
This is *not* an ORM. People that are wrapping ORM's around Redis are missing
|
|
4
4
|
the point.
|
|
5
5
|
|
|
6
6
|
The killer feature of Redis that it allows you to perform atomic operations
|
|
7
|
-
on _individual_ data structures, like counters, lists, and sets
|
|
8
|
-
*with* your existing ActiveRecord/DataMapper/etc models, or in classes that have
|
|
9
|
-
nothing to do with an ORM or even a database.
|
|
7
|
+
on _individual_ data structures, like counters, lists, and sets. You can then use
|
|
8
|
+
these *with* your existing ActiveRecord/DataMapper/etc models, or in classes that have
|
|
9
|
+
nothing to do with an ORM or even a database. This gem maps Ezra's +redis+ library
|
|
10
|
+
to Ruby objects to make use seamless.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
games; for a fun rant on the topic, see
|
|
12
|
+
This gem originally arose out of a need for high-concurrency atomic operations;
|
|
13
|
+
for a fun rant on the topic, see
|
|
14
14
|
{ATOMICITY}[http://github.com/nateware/redis-objects/blob/master/ATOMICITY.rdoc],
|
|
15
15
|
or scroll down to "Atomicity" in this README.
|
|
16
16
|
|
|
17
|
-
There are two ways to use Redis::Objects, either as an +include+ in a class,
|
|
18
|
-
by using +new+ with the type of data structure you want to create.
|
|
17
|
+
There are two ways to use Redis::Objects, either as an +include+ in a model class,
|
|
18
|
+
or by using +new+ with the type of data structure you want to create.
|
|
19
19
|
|
|
20
20
|
== Installation
|
|
21
21
|
|
|
22
22
|
gem install gemcutter
|
|
23
23
|
gem tumble
|
|
24
24
|
gem install redis-objects
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
== Example 1: Individual Usage
|
|
27
|
+
|
|
28
|
+
There is a Ruby object that maps to each Redis type.
|
|
29
|
+
|
|
26
30
|
=== Initialization
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
You can either set the $redis global variable to your Redis handle:
|
|
33
|
+
|
|
34
|
+
$redis = Redis.new(:host => 'localhost', :port => 6379)
|
|
35
|
+
@value = Redis::Value.new('myvalue')
|
|
36
|
+
|
|
37
|
+
Or you can pass the Redis handle into the new method:
|
|
38
|
+
|
|
39
|
+
redis = Redis.new(:host => 'localhost', :port => 6379)
|
|
40
|
+
@value = Redis::Value.new('myvalue', redis)
|
|
41
|
+
|
|
42
|
+
=== Counters
|
|
43
|
+
|
|
44
|
+
Create a new counter. The +counter_name+ is the key stored in Redis.
|
|
45
|
+
|
|
46
|
+
@counter = Redis::Counter.new('counter_name')
|
|
47
|
+
@counter.increment
|
|
48
|
+
@counter.decrement
|
|
49
|
+
puts @counter.value
|
|
50
|
+
@counter.increment do |val|
|
|
51
|
+
raise "Full" if val > MAX_VAL # rewind counter
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
See the section on "Atomicity" for cool uses of atomic counter blocks.
|
|
55
|
+
|
|
56
|
+
=== Lists
|
|
57
|
+
|
|
58
|
+
Lists work just like Ruby arrays:
|
|
59
|
+
|
|
60
|
+
@list = Redis::List.new('list_name')
|
|
61
|
+
@list << 'a'
|
|
62
|
+
@list << 'b'
|
|
63
|
+
@list.include? 'c' # false
|
|
64
|
+
@list.values # ['a','b']
|
|
65
|
+
@list << 'c'
|
|
66
|
+
@list.delete('c')
|
|
67
|
+
@list[0]
|
|
68
|
+
@list[0,1]
|
|
69
|
+
@list[0..1]
|
|
70
|
+
@list.shift
|
|
71
|
+
@list.pop
|
|
72
|
+
@list.clear
|
|
73
|
+
# etc
|
|
74
|
+
|
|
75
|
+
=== Sets
|
|
76
|
+
|
|
77
|
+
Sets work like the Ruby {Set}[http://ruby-doc.org/core/classes/Set.html] class:
|
|
78
|
+
|
|
79
|
+
@set = Redis::Set.new('set_name')
|
|
80
|
+
@set << 'a'
|
|
81
|
+
@set << 'b'
|
|
82
|
+
@set << 'a' # dup ignored
|
|
83
|
+
@set.member? 'c' # false
|
|
84
|
+
@set.members # ['a','b']
|
|
85
|
+
@set.each do |member|
|
|
86
|
+
puts member
|
|
87
|
+
end
|
|
88
|
+
@set.clear
|
|
89
|
+
# etc
|
|
90
|
+
|
|
91
|
+
You can perform Redis intersections/unions easily:
|
|
92
|
+
|
|
93
|
+
@set1 = Redis::Set.new('set1')
|
|
94
|
+
@set2 = Redis::Set.new('set2')
|
|
95
|
+
@set3 = Redis::Set.new('set3')
|
|
96
|
+
members = @set1 & @set2 # intersection
|
|
97
|
+
members = @set1 | @set2 # union
|
|
98
|
+
members = @set1 + @set2 # union
|
|
99
|
+
members = @set1.intersection(@set2, @set3) # multiple
|
|
100
|
+
members = @set1.union(@set2, @set3) # multiple
|
|
101
|
+
|
|
102
|
+
Or store them in Redis:
|
|
103
|
+
|
|
104
|
+
@set1.interstore('intername', @set2, @set3)
|
|
105
|
+
members = @set1.redis.get('intername')
|
|
106
|
+
@set1.unionstore('unionname', @set2, @set3)
|
|
107
|
+
members = @set1.redis.get('unionname')
|
|
108
|
+
|
|
109
|
+
=== Values
|
|
110
|
+
|
|
111
|
+
Simple values are easy as well:
|
|
112
|
+
|
|
113
|
+
@value = Redis::Value.new('value_name')
|
|
114
|
+
@value.value = 'a'
|
|
115
|
+
@value.delete
|
|
116
|
+
|
|
117
|
+
== Example 2: Class Usage
|
|
118
|
+
|
|
119
|
+
This method of use makes it easy to integrate Redis types with an existing
|
|
120
|
+
ActiveRecord model, DataMapper resource, or other class. Redis::Objects
|
|
121
|
+
will work with any class that provides an +id+ method that returns a unique
|
|
122
|
+
value. Redis::Objects will automatically create keys that are unique to
|
|
123
|
+
each object.
|
|
124
|
+
|
|
125
|
+
=== Initialization
|
|
126
|
+
|
|
127
|
+
If on Rails, config/initializers/redis.rb is a good place for this:
|
|
128
|
+
|
|
29
129
|
require 'redis'
|
|
30
130
|
require 'redis/objects'
|
|
31
131
|
Redis::Objects.redis = Redis.new(:host => 127.0.0.1, :port => 6379)
|
|
32
132
|
|
|
33
|
-
|
|
133
|
+
=== Model Class
|
|
34
134
|
|
|
35
|
-
|
|
135
|
+
Include Redis::Objects in any type of class:
|
|
36
136
|
|
|
37
137
|
class Team < ActiveRecord::Base
|
|
38
138
|
include Redis::Objects
|
|
39
139
|
|
|
40
|
-
counter :
|
|
41
|
-
counter :
|
|
42
|
-
counter :
|
|
140
|
+
counter :hits
|
|
141
|
+
counter :runs
|
|
142
|
+
counter :outs
|
|
143
|
+
counter :inning, :start => 1
|
|
43
144
|
list :on_base
|
|
44
145
|
set :outfielders
|
|
45
|
-
value :
|
|
146
|
+
value :at_bat
|
|
46
147
|
end
|
|
47
|
-
|
|
48
|
-
Familiar Ruby operations Just Work:
|
|
49
148
|
|
|
50
|
-
|
|
149
|
+
Familiar Ruby array operations Just Work (TM):
|
|
150
|
+
|
|
151
|
+
@team = Team.find_by_name('New York Yankees')
|
|
51
152
|
@team.on_base << 'player1'
|
|
52
153
|
@team.on_base << 'player2'
|
|
53
|
-
|
|
154
|
+
@team.on_base << 'player3'
|
|
155
|
+
@team.on_base # ['player1', 'player2']
|
|
54
156
|
@team.on_base.pop
|
|
157
|
+
@team.on_base.shift
|
|
158
|
+
@team.on_base.length # 1
|
|
159
|
+
@team.on_base.delete('player3')
|
|
55
160
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@team.
|
|
59
|
-
@team.
|
|
60
|
-
@team.
|
|
61
|
-
@team.
|
|
161
|
+
Sets work like the Ruby {Set}[http://ruby-doc.org/core/classes/Set.html] class:
|
|
162
|
+
|
|
163
|
+
@team.outfielders << 'outfielder1' << 'outfielder1'
|
|
164
|
+
@team.outfielders << 'outfielder2'
|
|
165
|
+
@team.outfielders # ['outfielder1', 'outfielder2']
|
|
166
|
+
@team.outfielders.each do |player|
|
|
167
|
+
puts player
|
|
168
|
+
end
|
|
169
|
+
player = @team.outfielders.detect{|of| of == 'outfielder2'}
|
|
62
170
|
|
|
63
|
-
|
|
171
|
+
Note counters cannot be assigned to, only incremented/decremented:
|
|
64
172
|
|
|
65
|
-
|
|
173
|
+
@team.hits.increment # or incr
|
|
174
|
+
@team.hits.decrement # or decr
|
|
175
|
+
@team.hits.incr(3) # add 3
|
|
176
|
+
@team.runs = 4 # exception
|
|
66
177
|
|
|
67
|
-
|
|
68
|
-
@counter.increment
|
|
69
|
-
puts @counter
|
|
70
|
-
puts @counter.get # force re-fetch
|
|
178
|
+
For free, you get a +redis+ handle usable in your class:
|
|
71
179
|
|
|
72
|
-
|
|
180
|
+
@team.redis.get('somekey')
|
|
181
|
+
@team.redis.smembers('someset')
|
|
73
182
|
|
|
74
|
-
|
|
75
|
-
@list << 'a'
|
|
76
|
-
@list << 'b'
|
|
77
|
-
puts @list
|
|
183
|
+
You can call any operation supported by {Redis}[http://code.google.com/p/redis/wiki/CommandReference]
|
|
78
184
|
|
|
79
185
|
== Atomicity
|
|
80
186
|
|
|
81
187
|
You are probably not handling atomicity correctly in your app. For a fun rant
|
|
82
|
-
on the topic, see
|
|
188
|
+
on the topic, see
|
|
189
|
+
{ATOMICITY}[http://github.com/nateware/redis-objects/blob/master/ATOMICITY.rdoc].
|
|
83
190
|
|
|
84
191
|
Atomic counters are a good way to handle concurrency:
|
|
85
192
|
|
data/lib/redis/counter.rb
CHANGED
|
@@ -8,12 +8,11 @@ class Redis
|
|
|
8
8
|
#
|
|
9
9
|
class Counter
|
|
10
10
|
attr_reader :key, :options, :redis
|
|
11
|
-
def initialize(key, options={})
|
|
11
|
+
def initialize(key, redis=$redis, options={})
|
|
12
12
|
@key = key
|
|
13
|
+
@redis = redis
|
|
13
14
|
@options = options
|
|
14
|
-
@redis = options[:redis] || $redis || Redis::Objects.redis
|
|
15
15
|
@options[:start] ||= 0
|
|
16
|
-
@options[:type] ||= @options[:start] == 0 ? :increment : :decrement
|
|
17
16
|
@redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
|
|
18
17
|
end
|
|
19
18
|
|
data/lib/redis/list.rb
CHANGED
|
@@ -4,92 +4,119 @@ class Redis
|
|
|
4
4
|
# behave as much like Ruby arrays as possible.
|
|
5
5
|
#
|
|
6
6
|
class List
|
|
7
|
+
require 'enumerator'
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
7
10
|
attr_reader :key, :options, :redis
|
|
8
|
-
def initialize(key, options={})
|
|
11
|
+
def initialize(key, redis=$redis, options={})
|
|
9
12
|
@key = key
|
|
13
|
+
@redis = redis
|
|
10
14
|
@options = options
|
|
11
|
-
@redis = options[:redis] || $redis || Redis::Objects.redis
|
|
12
|
-
@options[:start] ||= 0
|
|
13
|
-
@options[:type] ||= @options[:start] == 0 ? :increment : :decrement
|
|
14
|
-
@redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
|
|
15
15
|
end
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
# Works like push. Can chain together: list << 'a' << 'b'
|
|
17
18
|
def <<(value)
|
|
18
19
|
push(value)
|
|
20
|
+
self # for << 'a' << 'b'
|
|
19
21
|
end
|
|
20
|
-
|
|
22
|
+
|
|
23
|
+
# Add a member to the end of the list. Redis: RPUSH
|
|
21
24
|
def push(value)
|
|
22
25
|
redis.rpush(key, value)
|
|
23
|
-
@values << value
|
|
24
26
|
end
|
|
25
27
|
|
|
28
|
+
# Remove a member from the end of the list. Redis: RPOP
|
|
26
29
|
def pop
|
|
27
30
|
redis.rpop(key)
|
|
28
|
-
@values.pop
|
|
29
31
|
end
|
|
30
32
|
|
|
33
|
+
# Add a member to the start of the list. Redis: LPUSH
|
|
31
34
|
def unshift(value)
|
|
32
35
|
redis.lpush(key, value)
|
|
33
|
-
@values.unshift value
|
|
34
36
|
end
|
|
35
37
|
|
|
38
|
+
# Remove a member from the start of the list. Redis: LPOP
|
|
36
39
|
def shift
|
|
37
40
|
redis.lpop(key)
|
|
38
41
|
end
|
|
39
42
|
|
|
43
|
+
# Return all values in the list. Redis: LRANGE(0,-1)
|
|
40
44
|
def values
|
|
41
|
-
|
|
45
|
+
range(0, -1)
|
|
42
46
|
end
|
|
47
|
+
alias_method :get, :values
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def get
|
|
50
|
-
@values = range(0, -1)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def [](index)
|
|
54
|
-
case index
|
|
55
|
-
when Range
|
|
49
|
+
# Same functionality as Ruby arrays. If a single number is given, return
|
|
50
|
+
# just the element at that index using Redis: LINDEX. Otherwise, return
|
|
51
|
+
# a range of values using Redis: LRANGE.
|
|
52
|
+
def [](index, length=nil)
|
|
53
|
+
if index.is_a? Range
|
|
56
54
|
range(index.first, index.last)
|
|
55
|
+
elsif length
|
|
56
|
+
range(index, length)
|
|
57
57
|
else
|
|
58
|
-
|
|
58
|
+
at(index)
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# Delete the element(s) from the list that match name. If count is specified,
|
|
63
|
+
# only the first-N (if positive) or last-N (if negative) will be removed.
|
|
64
|
+
# Redis: LREM
|
|
62
65
|
def delete(name, count=0)
|
|
63
|
-
redis.lrem(
|
|
66
|
+
redis.lrem(key, count, name) # weird api
|
|
67
|
+
get
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Iterate through each member of the set. Redis::Objects mixes in Enumerable,
|
|
71
|
+
# so you can also use familiar methods like +collect+, +detect+, and so forth.
|
|
72
|
+
def each(&block)
|
|
73
|
+
values.each(&block)
|
|
64
74
|
end
|
|
65
75
|
|
|
76
|
+
# Return a range of values from +start_index+ to +end_index+. Can also use
|
|
77
|
+
# the familiar list[start,end] Ruby syntax. Redis: LRANGE
|
|
66
78
|
def range(start_index, end_index)
|
|
67
79
|
redis.lrange(key, start_index, end_index)
|
|
68
80
|
end
|
|
69
|
-
|
|
81
|
+
|
|
82
|
+
# Return the value at the given index. Can also use familiar list[index] syntax.
|
|
83
|
+
# Redis: LINDEX
|
|
70
84
|
def at(index)
|
|
71
|
-
redis.
|
|
85
|
+
redis.lindex(key, index)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Return the first element in the list. Redis: LINDEX(0)
|
|
89
|
+
def first
|
|
90
|
+
at(0)
|
|
72
91
|
end
|
|
73
92
|
|
|
93
|
+
# Return the last element in the list. Redis: LINDEX(-1)
|
|
74
94
|
def last
|
|
75
|
-
|
|
95
|
+
at(-1)
|
|
76
96
|
end
|
|
77
97
|
|
|
98
|
+
# Clear the list entirely. Redis: DEL
|
|
78
99
|
def clear
|
|
79
100
|
redis.del(key)
|
|
80
101
|
end
|
|
81
102
|
|
|
103
|
+
# Return the length of the list. Aliased as size. Redis: LLEN
|
|
82
104
|
def length
|
|
83
|
-
redis.
|
|
105
|
+
redis.llen(key)
|
|
84
106
|
end
|
|
85
107
|
alias_method :size, :length
|
|
86
|
-
|
|
108
|
+
|
|
109
|
+
# Returns true if there are no elements in the list. Redis: LLEN == 0
|
|
87
110
|
def empty?
|
|
88
|
-
|
|
111
|
+
length == 0
|
|
89
112
|
end
|
|
90
|
-
|
|
113
|
+
|
|
91
114
|
def ==(x)
|
|
92
115
|
values == x
|
|
93
116
|
end
|
|
117
|
+
|
|
118
|
+
def to_s
|
|
119
|
+
values.join(', ')
|
|
120
|
+
end
|
|
94
121
|
end
|
|
95
122
|
end
|
data/lib/redis/lock.rb
CHANGED
|
@@ -10,11 +10,11 @@ class Redis
|
|
|
10
10
|
class LockTimeout < StandardError; end #:nodoc:
|
|
11
11
|
|
|
12
12
|
attr_reader :key, :options, :redis
|
|
13
|
-
def initialize(key, options={})
|
|
13
|
+
def initialize(key, redis=$redis, options={})
|
|
14
14
|
@key = key
|
|
15
|
+
@redis = redis
|
|
15
16
|
@options = options
|
|
16
17
|
@options[:timeout] ||= 5
|
|
17
|
-
@redis = options[:redis] || $redis || Redis::Objects.redis
|
|
18
18
|
@redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -24,7 +24,7 @@ class Redis
|
|
|
24
24
|
@counters[name] = options
|
|
25
25
|
class_eval <<-EndMethods
|
|
26
26
|
def #{name}
|
|
27
|
-
@#{name} ||= Redis::Counter.new(field_key(:#{name}), self.class.counters[:#{name}]
|
|
27
|
+
@#{name} ||= Redis::Counter.new(field_key(:#{name}), redis, self.class.counters[:#{name}])
|
|
28
28
|
end
|
|
29
29
|
EndMethods
|
|
30
30
|
end
|
data/lib/redis/objects/lists.rb
CHANGED
|
@@ -20,7 +20,7 @@ class Redis
|
|
|
20
20
|
@lists[name] = options
|
|
21
21
|
class_eval <<-EndMethods
|
|
22
22
|
def #{name}
|
|
23
|
-
@#{name} ||= Redis::List.new(field_key(:#{name}), self.class.lists[:#{name}]
|
|
23
|
+
@#{name} ||= Redis::List.new(field_key(:#{name}), redis, self.class.lists[:#{name}])
|
|
24
24
|
end
|
|
25
25
|
EndMethods
|
|
26
26
|
end
|
data/lib/redis/objects/locks.rb
CHANGED
|
@@ -22,7 +22,7 @@ class Redis
|
|
|
22
22
|
@locks[name] = options
|
|
23
23
|
class_eval <<-EndMethods
|
|
24
24
|
def #{name}_lock(&block)
|
|
25
|
-
@#{name}_lock ||= Redis::Lock.new(field_key(:#{name}_lock), self.class.locks[:#{name}]
|
|
25
|
+
@#{name}_lock ||= Redis::Lock.new(field_key(:#{name}_lock), redis, self.class.locks[:#{name}])
|
|
26
26
|
end
|
|
27
27
|
EndMethods
|
|
28
28
|
end
|
data/lib/redis/objects/sets.rb
CHANGED
|
@@ -4,6 +4,31 @@ require 'redis/set'
|
|
|
4
4
|
class Redis
|
|
5
5
|
module Objects
|
|
6
6
|
module Sets
|
|
7
|
+
def self.included(klass)
|
|
8
|
+
klass.instance_variable_set('@sets', {})
|
|
9
|
+
klass.send :include, InstanceMethods
|
|
10
|
+
klass.extend ClassMethods
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Class methods that appear in your class when you include Redis::Objects.
|
|
14
|
+
module ClassMethods
|
|
15
|
+
attr_reader :sets
|
|
16
|
+
|
|
17
|
+
# Define a new list. It will function like a regular instance
|
|
18
|
+
# method, so it can be used alongside ActiveRecord, DataMapper, etc.
|
|
19
|
+
def set(name, options={})
|
|
20
|
+
@sets[name] = options
|
|
21
|
+
class_eval <<-EndMethods
|
|
22
|
+
def #{name}
|
|
23
|
+
@#{name} ||= Redis::Set.new(field_key(:#{name}), redis, self.class.sets[:#{name}])
|
|
24
|
+
end
|
|
25
|
+
EndMethods
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Instance methods that appear in your class when you include Redis::Objects.
|
|
30
|
+
module InstanceMethods
|
|
31
|
+
end
|
|
7
32
|
end
|
|
8
33
|
end
|
|
9
34
|
end
|
data/lib/redis/objects/values.rb
CHANGED
|
@@ -20,7 +20,7 @@ class Redis
|
|
|
20
20
|
@values[name] = options
|
|
21
21
|
class_eval <<-EndMethods
|
|
22
22
|
def #{name}
|
|
23
|
-
@#{name} ||= Redis::Value.new(field_key(:#{name}), self.class.values[:#{name}]
|
|
23
|
+
@#{name} ||= Redis::Value.new(field_key(:#{name}), redis, self.class.values[:#{name}])
|
|
24
24
|
end
|
|
25
25
|
def #{name}=(value)
|
|
26
26
|
#{name}.value = value
|
data/lib/redis/set.rb
CHANGED
|
@@ -3,5 +3,131 @@ class Redis
|
|
|
3
3
|
# Class representing a set.
|
|
4
4
|
#
|
|
5
5
|
class Set
|
|
6
|
+
require 'enumerator'
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
attr_reader :key, :options, :redis
|
|
10
|
+
|
|
11
|
+
# Create a new Set.
|
|
12
|
+
def initialize(key, redis=$redis, options={})
|
|
13
|
+
@key = key
|
|
14
|
+
@redis = redis
|
|
15
|
+
@options = options
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Works like add. Can chain together: list << 'a' << 'b'
|
|
19
|
+
def <<(value)
|
|
20
|
+
add(value)
|
|
21
|
+
self # for << 'a' << 'b'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Add the specified value to the set only if it does not exist already.
|
|
25
|
+
# Redis: SADD
|
|
26
|
+
def add(value)
|
|
27
|
+
redis.sadd(key, value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Return all members in the set. Redis: SMEMBERS
|
|
31
|
+
def members
|
|
32
|
+
redis.smembers(key)
|
|
33
|
+
end
|
|
34
|
+
alias_method :get, :members
|
|
35
|
+
|
|
36
|
+
# Returns true if the specified value is in the set. Redis: SISMEMBER
|
|
37
|
+
def member?(value)
|
|
38
|
+
redis.sismember(key, value)
|
|
39
|
+
end
|
|
40
|
+
alias_method :include?, :member?
|
|
41
|
+
|
|
42
|
+
# Delete the value from the set. Redis: SREM
|
|
43
|
+
def delete(name)
|
|
44
|
+
redis.srem(key, name)
|
|
45
|
+
get
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Wipe the set entirely. Redis: DEL
|
|
49
|
+
def clear
|
|
50
|
+
redis.del(key)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Iterate through each member of the set. Redis::Objects mixes in Enumerable,
|
|
54
|
+
# so you can also use familiar methods like +collect+, +detect+, and so forth.
|
|
55
|
+
def each(&block)
|
|
56
|
+
members.each(&block)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Return the intersection with another set. Can pass it either another set
|
|
60
|
+
# object or set name. Also available as & which is a bit cleaner:
|
|
61
|
+
#
|
|
62
|
+
# members_in_both = set1 & set2
|
|
63
|
+
#
|
|
64
|
+
# If you want to specify multiple sets, you must use +intersection+:
|
|
65
|
+
#
|
|
66
|
+
# members_in_all = set1.intersection(set2, set3, set4)
|
|
67
|
+
# members_in_all = set1.inter(set2, set3, set4) # alias
|
|
68
|
+
#
|
|
69
|
+
# Redis: SINTER
|
|
70
|
+
def intersection(*sets)
|
|
71
|
+
redis.sinter(key, *keys_from_objects(sets))
|
|
72
|
+
end
|
|
73
|
+
alias_method :intersect, :intersection
|
|
74
|
+
alias_method :inter, :intersection
|
|
75
|
+
alias_method :&, :intersection
|
|
76
|
+
|
|
77
|
+
# Calculate the intersection and store it in Redis as +name+. Returns the number
|
|
78
|
+
# of elements in the stored intersection. Redis: SUNIONSTORE
|
|
79
|
+
def interstore(name, *sets)
|
|
80
|
+
redis.sinterstore(name, key, *keys_from_objects(sets))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Return the union with another set. Can pass it either another set
|
|
84
|
+
# object or set name. Also available as | and + which are a bit cleaner:
|
|
85
|
+
#
|
|
86
|
+
# members_in_either = set1 | set2
|
|
87
|
+
# members_in_either = set1 + set2
|
|
88
|
+
#
|
|
89
|
+
# If you want to specify multiple sets, you must use +union+:
|
|
90
|
+
#
|
|
91
|
+
# members_in_all = set1.union(set2, set3, set4)
|
|
92
|
+
#
|
|
93
|
+
# Redis: SUNION
|
|
94
|
+
def union(*sets)
|
|
95
|
+
redis.sunion(key, *keys_from_objects(sets))
|
|
96
|
+
end
|
|
97
|
+
alias_method :|, :union
|
|
98
|
+
alias_method :+, :union
|
|
99
|
+
|
|
100
|
+
# Calculate the union and store it in Redis as +name+. Returns the number
|
|
101
|
+
# of elements in the stored union. Redis: SINTERSTORE
|
|
102
|
+
def unionstore(name, *sets)
|
|
103
|
+
redis.sunionstore(name, key, *keys_from_objects(sets))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The number of members in the set. Aliased as size. Redis: SCARD
|
|
107
|
+
def length
|
|
108
|
+
redis.scard(key)
|
|
109
|
+
end
|
|
110
|
+
alias_method :size, :length
|
|
111
|
+
|
|
112
|
+
# Returns true if the set has no members. Redis: SCARD == 0
|
|
113
|
+
def empty?
|
|
114
|
+
length == 0
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def ==(x)
|
|
118
|
+
members == x
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def to_s
|
|
122
|
+
members.join(', ')
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def keys_from_objects(sets)
|
|
128
|
+
raise ArgumentError, "Must pass in one or more set names" if sets.empty?
|
|
129
|
+
sets.collect{|set| set.is_a?(Redis::Set) ? set.key : set}
|
|
130
|
+
end
|
|
131
|
+
|
|
6
132
|
end
|
|
7
133
|
end
|
data/lib/redis/value.rb
CHANGED
|
@@ -4,10 +4,10 @@ class Redis
|
|
|
4
4
|
#
|
|
5
5
|
class Value
|
|
6
6
|
attr_reader :key, :options, :redis
|
|
7
|
-
def initialize(key, options={})
|
|
7
|
+
def initialize(key, redis=$redis, options={})
|
|
8
8
|
@key = key
|
|
9
|
+
@redis = redis
|
|
9
10
|
@options = options
|
|
10
|
-
@redis = options[:redis] || $redis || Redis::Objects.redis
|
|
11
11
|
@redis.setnx(key, @options[:default]) if @options[:default]
|
|
12
12
|
end
|
|
13
13
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
3
3
|
|
|
4
|
+
UNIONSTORE_KEY = 'test:unionstore'
|
|
5
|
+
INTERSTORE_KEY = 'test:interstore'
|
|
6
|
+
|
|
4
7
|
class Roster
|
|
5
8
|
include Redis::Objects
|
|
6
9
|
counter :available_slots, :start => 10
|
|
@@ -10,6 +13,7 @@ class Roster
|
|
|
10
13
|
lock :resort, :timeout => 2
|
|
11
14
|
value :starting_pitcher
|
|
12
15
|
list :player_stats
|
|
16
|
+
set :outfielders
|
|
13
17
|
|
|
14
18
|
def initialize(id=1) @id = id end
|
|
15
19
|
def id; @id; end
|
|
@@ -20,6 +24,10 @@ describe Redis::Objects do
|
|
|
20
24
|
before :all do
|
|
21
25
|
@roster = Roster.new
|
|
22
26
|
@roster2 = Roster.new
|
|
27
|
+
|
|
28
|
+
@roster_1 = Roster.new(1)
|
|
29
|
+
@roster_2 = Roster.new(2)
|
|
30
|
+
@roster_3 = Roster.new(3)
|
|
23
31
|
end
|
|
24
32
|
|
|
25
33
|
before :each do
|
|
@@ -29,6 +37,12 @@ describe Redis::Objects do
|
|
|
29
37
|
@roster.resort_lock.clear
|
|
30
38
|
@roster.starting_pitcher.delete
|
|
31
39
|
@roster.player_stats.clear
|
|
40
|
+
@roster.outfielders.clear
|
|
41
|
+
@roster_1.outfielders.clear
|
|
42
|
+
@roster_2.outfielders.clear
|
|
43
|
+
@roster_3.outfielders.clear
|
|
44
|
+
@roster.redis.del(UNIONSTORE_KEY)
|
|
45
|
+
@roster.redis.del(INTERSTORE_KEY)
|
|
32
46
|
end
|
|
33
47
|
|
|
34
48
|
it "should provide a connection method" do
|
|
@@ -254,6 +268,137 @@ describe Redis::Objects do
|
|
|
254
268
|
@roster.player_stats.should be_empty
|
|
255
269
|
@roster.player_stats << 'a'
|
|
256
270
|
@roster.player_stats.should == ['a']
|
|
271
|
+
@roster.player_stats.get.should == ['a']
|
|
272
|
+
@roster.player_stats.unshift 'b'
|
|
273
|
+
@roster.player_stats.to_s.should == 'b, a'
|
|
274
|
+
@roster.player_stats.should == ['b','a']
|
|
275
|
+
@roster.player_stats.get.should == ['b','a']
|
|
276
|
+
@roster.player_stats.push 'c'
|
|
277
|
+
@roster.player_stats.should == ['b','a','c']
|
|
278
|
+
@roster.player_stats.get.should == ['b','a','c']
|
|
279
|
+
@roster.player_stats.first.should == 'b'
|
|
280
|
+
@roster.player_stats.last.should == 'c'
|
|
281
|
+
@roster.player_stats << 'd'
|
|
282
|
+
@roster.player_stats.should == ['b','a','c','d']
|
|
283
|
+
@roster.player_stats[1].should == 'a'
|
|
284
|
+
@roster.player_stats[0].should == 'b'
|
|
285
|
+
@roster.player_stats[2].should == 'c'
|
|
286
|
+
@roster.player_stats[3].should == 'd'
|
|
287
|
+
@roster.player_stats.include?('c').should be_true
|
|
288
|
+
@roster.player_stats.include?('no').should be_false
|
|
289
|
+
@roster.player_stats.pop
|
|
290
|
+
@roster.player_stats[0].should == @roster.player_stats.at(0)
|
|
291
|
+
@roster.player_stats[1].should == @roster.player_stats.at(1)
|
|
292
|
+
@roster.player_stats[2].should == @roster.player_stats.at(2)
|
|
293
|
+
@roster.player_stats.should == ['b','a','c']
|
|
294
|
+
@roster.player_stats.get.should == ['b','a','c']
|
|
295
|
+
@roster.player_stats.shift
|
|
296
|
+
@roster.player_stats.should == ['a','c']
|
|
297
|
+
@roster.player_stats.get.should == ['a','c']
|
|
298
|
+
@roster.player_stats << 'e' << 'f' << 'e'
|
|
299
|
+
@roster.player_stats.should == ['a','c','e','f','e']
|
|
300
|
+
@roster.player_stats.get.should == ['a','c','e','f','e']
|
|
301
|
+
@roster.player_stats.delete('e')
|
|
302
|
+
@roster.player_stats.should == ['a','c','f']
|
|
303
|
+
@roster.player_stats.get.should == ['a','c','f']
|
|
304
|
+
@roster.player_stats << 'j'
|
|
305
|
+
@roster.player_stats.should == ['a','c','f','j']
|
|
306
|
+
@roster.player_stats[0..2].should == ['a','c','f']
|
|
307
|
+
@roster.player_stats[1, 3].should == ['c','f','j']
|
|
308
|
+
@roster.player_stats.length.should == 4
|
|
309
|
+
@roster.player_stats.size.should == 4
|
|
310
|
+
@roster.player_stats.should == ['a','c','f','j']
|
|
311
|
+
@roster.player_stats.get.should == ['a','c','f','j']
|
|
312
|
+
|
|
313
|
+
i = -1
|
|
314
|
+
@roster.player_stats.each do |st|
|
|
315
|
+
st.should == @roster.player_stats[i += 1]
|
|
316
|
+
end
|
|
317
|
+
@roster.player_stats.should == ['a','c','f','j']
|
|
318
|
+
@roster.player_stats.get.should == ['a','c','f','j']
|
|
319
|
+
|
|
320
|
+
@roster.player_stats.each_with_index do |st,i|
|
|
321
|
+
st.should == @roster.player_stats[i]
|
|
322
|
+
end
|
|
323
|
+
@roster.player_stats.should == ['a','c','f','j']
|
|
324
|
+
@roster.player_stats.get.should == ['a','c','f','j']
|
|
325
|
+
|
|
326
|
+
coll = @roster.player_stats.collect{|st| st}
|
|
327
|
+
coll.should == ['a','c','f','j']
|
|
328
|
+
@roster.player_stats.should == ['a','c','f','j']
|
|
329
|
+
@roster.player_stats.get.should == ['a','c','f','j']
|
|
330
|
+
|
|
331
|
+
@roster.player_stats << 'a'
|
|
332
|
+
coll = @roster.player_stats.select{|st| st == 'a'}
|
|
333
|
+
coll.should == ['a','a']
|
|
334
|
+
@roster.player_stats.should == ['a','c','f','j','a']
|
|
335
|
+
@roster.player_stats.get.should == ['a','c','f','j','a']
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
it "should handle sets of simple values" do
|
|
339
|
+
@roster.outfielders.should be_empty
|
|
340
|
+
@roster.outfielders << 'a' << 'a' << 'a'
|
|
341
|
+
@roster.outfielders.should == ['a']
|
|
342
|
+
@roster.outfielders.get.should == ['a']
|
|
343
|
+
@roster.outfielders << 'b' << 'b'
|
|
344
|
+
@roster.outfielders.to_s.should == 'a, b'
|
|
345
|
+
@roster.outfielders.should == ['a','b']
|
|
346
|
+
@roster.outfielders.members.should == ['a','b']
|
|
347
|
+
@roster.outfielders.get.should == ['a','b']
|
|
348
|
+
@roster.outfielders << 'c'
|
|
349
|
+
@roster.outfielders.sort.should == ['a','b','c']
|
|
350
|
+
@roster.outfielders.get.sort.should == ['a','b','c']
|
|
351
|
+
@roster.outfielders.delete('c')
|
|
352
|
+
@roster.outfielders.should == ['a','b']
|
|
353
|
+
@roster.outfielders.get.sort.should == ['a','b']
|
|
354
|
+
@roster.outfielders.length.should == 2
|
|
355
|
+
@roster.outfielders.size.should == 2
|
|
356
|
+
|
|
357
|
+
i = 0
|
|
358
|
+
@roster.outfielders.each do |st|
|
|
359
|
+
i += 1
|
|
360
|
+
end
|
|
361
|
+
i.should == @roster.outfielders.length
|
|
362
|
+
|
|
363
|
+
coll = @roster.outfielders.collect{|st| st}
|
|
364
|
+
coll.should == ['a','b']
|
|
365
|
+
@roster.outfielders.should == ['a','b']
|
|
366
|
+
@roster.outfielders.get.should == ['a','b']
|
|
367
|
+
|
|
368
|
+
@roster.outfielders << 'c'
|
|
369
|
+
@roster.outfielders.member?('c').should be_true
|
|
370
|
+
@roster.outfielders.include?('c').should be_true
|
|
371
|
+
@roster.outfielders.member?('no').should be_false
|
|
372
|
+
coll = @roster.outfielders.select{|st| st == 'c'}
|
|
373
|
+
coll.should == ['c']
|
|
374
|
+
@roster.outfielders.sort.should == ['a','b','c']
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
it "should handle set intersections and unions" do
|
|
378
|
+
@roster_1.outfielders << 'a' << 'b' << 'c' << 'd' << 'e'
|
|
379
|
+
@roster_2.outfielders << 'c' << 'd' << 'e' << 'f' << 'g'
|
|
380
|
+
@roster_3.outfielders << 'a' << 'd' << 'g' << 'l' << 'm'
|
|
381
|
+
@roster_1.outfielders.sort.should == %w(a b c d e)
|
|
382
|
+
@roster_2.outfielders.sort.should == %w(c d e f g)
|
|
383
|
+
@roster_3.outfielders.sort.should == %w(a d g l m)
|
|
384
|
+
(@roster_1.outfielders & @roster_2.outfielders).sort.should == ['c','d','e']
|
|
385
|
+
@roster_1.outfielders.intersection(@roster_2.outfielders).sort.should == ['c','d','e']
|
|
386
|
+
@roster_1.outfielders.intersection(@roster_2.outfielders, @roster_3.outfielders).sort.should == ['d']
|
|
387
|
+
@roster_1.outfielders.intersect(@roster_2.outfielders).sort.should == ['c','d','e']
|
|
388
|
+
@roster_1.outfielders.inter(@roster_2.outfielders, @roster_3.outfielders).sort.should == ['d']
|
|
389
|
+
@roster_1.outfielders.interstore(INTERSTORE_KEY, @roster_2.outfielders).should == 3
|
|
390
|
+
@roster_1.redis.smembers(INTERSTORE_KEY).sort.should == ['c','d','e']
|
|
391
|
+
@roster_1.outfielders.interstore(INTERSTORE_KEY, @roster_2.outfielders, @roster_3.outfielders).should == 1
|
|
392
|
+
@roster_1.redis.smembers(INTERSTORE_KEY).sort.should == ['d']
|
|
393
|
+
|
|
394
|
+
(@roster_1.outfielders | @roster_2.outfielders).sort.should == ['a','b','c','d','e','f','g']
|
|
395
|
+
(@roster_1.outfielders + @roster_2.outfielders).sort.should == ['a','b','c','d','e','f','g']
|
|
396
|
+
@roster_1.outfielders.union(@roster_2.outfielders).sort.should == ['a','b','c','d','e','f','g']
|
|
397
|
+
@roster_1.outfielders.union(@roster_2.outfielders, @roster_3.outfielders).sort.should == ['a','b','c','d','e','f','g','l','m']
|
|
398
|
+
@roster_1.outfielders.unionstore(UNIONSTORE_KEY, @roster_2.outfielders).should == 7
|
|
399
|
+
@roster_1.redis.smembers(UNIONSTORE_KEY).sort.should == ['a','b','c','d','e','f','g']
|
|
400
|
+
@roster_1.outfielders.unionstore(UNIONSTORE_KEY, @roster_2.outfielders, @roster_3.outfielders).should == 9
|
|
401
|
+
@roster_1.redis.smembers(UNIONSTORE_KEY).sort.should == ['a','b','c','d','e','f','g','l','m']
|
|
257
402
|
end
|
|
258
403
|
|
|
259
404
|
it "should provide a lock method that accepts a block" do
|
|
@@ -265,7 +410,7 @@ describe Redis::Objects do
|
|
|
265
410
|
a.should be_true
|
|
266
411
|
end
|
|
267
412
|
|
|
268
|
-
|
|
413
|
+
it "should raise an exception if the timeout is exceeded" do
|
|
269
414
|
@roster.redis.set(@roster.resort_lock.key, 1)
|
|
270
415
|
error = nil
|
|
271
416
|
begin
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: redis-objects
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nate Wiger
|
|
@@ -9,11 +9,20 @@ autorequire:
|
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
11
|
|
|
12
|
-
date: 2009-11-
|
|
12
|
+
date: 2009-11-26 00:00:00 -08:00
|
|
13
13
|
default_executable:
|
|
14
|
-
dependencies:
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: redis
|
|
17
|
+
type: :runtime
|
|
18
|
+
version_requirement:
|
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
20
|
+
requirements:
|
|
21
|
+
- - ">="
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: "0.1"
|
|
24
|
+
version:
|
|
25
|
+
description: Map Redis types directly to Ruby objects. Works with any class or ORM.
|
|
17
26
|
email: nate@wiger.org
|
|
18
27
|
executables: []
|
|
19
28
|
|
|
@@ -36,6 +45,7 @@ files:
|
|
|
36
45
|
- lib/redis/objects.rb
|
|
37
46
|
- lib/redis/set.rb
|
|
38
47
|
- lib/redis/value.rb
|
|
48
|
+
- spec/redis_objects_instance_spec.rb
|
|
39
49
|
- spec/redis_objects_model_spec.rb
|
|
40
50
|
- spec/spec_helper.rb
|
|
41
51
|
- ATOMICITY.rdoc
|
|
@@ -62,12 +72,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
62
72
|
- !ruby/object:Gem::Version
|
|
63
73
|
version: "0"
|
|
64
74
|
version:
|
|
65
|
-
requirements:
|
|
66
|
-
|
|
75
|
+
requirements:
|
|
76
|
+
- redis, v0.1 or greater
|
|
67
77
|
rubyforge_project: redis-objects
|
|
68
78
|
rubygems_version: 1.3.5
|
|
69
79
|
signing_key:
|
|
70
80
|
specification_version: 3
|
|
71
|
-
summary:
|
|
81
|
+
summary: Maps Redis types to Ruby objects
|
|
72
82
|
test_files: []
|
|
73
83
|
|