redis-objects 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/ChangeLog ADDED
@@ -0,0 +1,15 @@
1
+
2
+ *0.2.2 [Final] (14 December 2009)*
3
+
4
+ * Added @set.diff(@set2) with "^" and "-" synonyms (oversight). [Nate Wiger]
5
+
6
+ * Implemented Redis core commands in all data types, such as rename. [Nate Wiger]
7
+
8
+ * Renamed Redis::Serialize to Redis::Helpers::Serialize to keep Redis:: cleaner. [Nate Wiger]
9
+
10
+ * More spec coverage. [Nate Wiger]
11
+
12
+ *0.2.1 [Final] (27 November 2009)*
13
+
14
+ * First worthwhile public release, with good spec coverage and functionality. [Nate Wiger]
15
+
data/README.rdoc CHANGED
@@ -6,8 +6,8 @@ the point.
6
6
  The killer feature of Redis that it allows you to perform atomic operations
7
7
  on _individual_ data structures, like counters, lists, and sets. You can then use
8
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.
9
+ nothing to do with an ORM or even a database. This gem maps {Redis types}[http://code.google.com/p/redis/wiki/CommandReference]
10
+ to Ruby objects, via a thin layer over Ezra's +redis+ gem.
11
11
 
12
12
  This gem originally arose out of a need for high-concurrency atomic operations;
13
13
  for a fun rant on the topic, see
@@ -23,13 +23,14 @@ or by using +new+ with the type of data structure you want to create.
23
23
  gem tumble
24
24
  gem install redis-objects
25
25
 
26
- == Example 1: Individual Usage
26
+ == Example 1: Standalone Usage
27
27
 
28
28
  There is a Ruby object that maps to each Redis type.
29
29
 
30
30
  === Initialization
31
31
 
32
- You can either set the $redis global variable to your Redis handle:
32
+ This gem needs a handle to the +redis+ server. For standalone use, you can
33
+ either set the $redis global variable to your Redis.new handle:
33
34
 
34
35
  $redis = Redis.new(:host => 'localhost', :port => 6379)
35
36
  @value = Redis::Value.new('myvalue')
@@ -43,6 +44,7 @@ Or you can pass the Redis handle into the new method:
43
44
 
44
45
  Create a new counter. The +counter_name+ is the key stored in Redis.
45
46
 
47
+ require 'redis/counter'
46
48
  @counter = Redis::Counter.new('counter_name')
47
49
  @counter.increment
48
50
  @counter.decrement
@@ -60,6 +62,7 @@ See the section on "Atomicity" for cool uses of atomic counter blocks.
60
62
 
61
63
  Lists work just like Ruby arrays:
62
64
 
65
+ require 'redis/list'
63
66
  @list = Redis::List.new('list_name')
64
67
  @list << 'a'
65
68
  @list << 'b'
@@ -87,6 +90,7 @@ Complex data types are no problem:
87
90
 
88
91
  Sets work like the Ruby {Set}[http://ruby-doc.org/core/classes/Set.html] class:
89
92
 
93
+ require 'redis/set'
90
94
  @set = Redis::Set.new('set_name')
91
95
  @set << 'a'
92
96
  @set << 'b'
@@ -99,7 +103,7 @@ Sets work like the Ruby {Set}[http://ruby-doc.org/core/classes/Set.html] class:
99
103
  @set.clear
100
104
  # etc
101
105
 
102
- You can perform Redis intersections/unions easily:
106
+ You can perform Redis intersections/unions/diffs easily:
103
107
 
104
108
  @set1 = Redis::Set.new('set1')
105
109
  @set2 = Redis::Set.new('set2')
@@ -107,8 +111,11 @@ You can perform Redis intersections/unions easily:
107
111
  members = @set1 & @set2 # intersection
108
112
  members = @set1 | @set2 # union
109
113
  members = @set1 + @set2 # union
114
+ members = @set1 ^ @set2 # union
115
+ members = @set1 - @set2 # union
110
116
  members = @set1.intersection(@set2, @set3) # multiple
111
117
  members = @set1.union(@set2, @set3) # multiple
118
+ members = @set1.difference(@set2, @set3) # multiple
112
119
 
113
120
  Or store them in Redis:
114
121
 
@@ -116,6 +123,8 @@ Or store them in Redis:
116
123
  members = @set1.redis.get('intername')
117
124
  @set1.unionstore('unionname', @set2, @set3)
118
125
  members = @set1.redis.get('unionname')
126
+ @set1.diffstore('diffname', @set2, @set3)
127
+ members = @set1.redis.get('diffname')
119
128
 
120
129
  And use complex data types too:
121
130
 
@@ -125,12 +134,14 @@ And use complex data types too:
125
134
  @set2 << {:name => "Jeff", :city => "Del Mar"}
126
135
 
127
136
  @set1 & @set2 # Nate
137
+ @set1 - @set2 # Peter
128
138
  @set1 | @set2 # all 3 people
129
139
 
130
140
  === Values
131
141
 
132
142
  Simple values are easy as well:
133
143
 
144
+ require 'redis/value'
134
145
  @value = Redis::Value.new('value_name')
135
146
  @value.value = 'a'
136
147
  @value.delete
@@ -141,11 +152,11 @@ Of course complex data is no problem:
141
152
  @newest = Redis::Value.new('newest_account')
142
153
  @newest.value = @account
143
154
 
144
- == Example 2: Class Usage
155
+ == Example 2: Model Class Usage
145
156
 
146
157
  Using Redis::Objects this way makes it trivial to integrate Redis types with an
147
158
  existing ActiveRecord model, DataMapper resource, or other class. Redis::Objects
148
- will work with any class that provides an +id+ method that returns a unique
159
+ will work with _any_ class that provides an +id+ method that returns a unique
149
160
  value. Redis::Objects will automatically create keys that are unique to
150
161
  each object.
151
162
 
@@ -185,7 +196,7 @@ Familiar Ruby array operations Just Work (TM):
185
196
  @team.on_base.length # 1
186
197
  @team.on_base.delete('player3')
187
198
 
188
- Sets operations work too:
199
+ Sets work too:
189
200
 
190
201
  @team.outfielders << 'outfielder1' << 'outfielder1'
191
202
  @team.outfielders << 'outfielder2'
@@ -195,6 +206,11 @@ Sets operations work too:
195
206
  end
196
207
  player = @team.outfielders.detect{|of| of == 'outfielder2'}
197
208
 
209
+ And you can do intersections between ORM objects (kinda cool):
210
+
211
+ @team1.outfielders | @team2.outfielders # all outfielders
212
+ @team1.outfielders & @team2.outfielders # should be empty
213
+
198
214
  Counters can be atomically incremented/decremented (but not assigned):
199
215
 
200
216
  @team.hits.increment # or incr
@@ -202,14 +218,15 @@ Counters can be atomically incremented/decremented (but not assigned):
202
218
  @team.hits.incr(3) # add 3
203
219
  @team.runs = 4 # exception
204
220
 
205
- For free, you get a +redis+ handle usable in your class:
221
+ Finally, for free, you get a +redis+ handle usable in your class that
222
+ points directly to a Redis API object:
206
223
 
207
224
  @team.redis.get('somekey')
208
225
  @team.redis.smembers('someset')
209
226
 
210
- You can call any operation supported by {Redis}[http://code.google.com/p/redis/wiki/CommandReference]
227
+ You can use the +redis+ handle to directly call any {Redis command}[http://code.google.com/p/redis/wiki/CommandReference]
211
228
 
212
- == Atomicity
229
+ == Atomic Counters and Locks
213
230
 
214
231
  You are probably not handling atomicity correctly in your app. For a fun rant
215
232
  on the topic, see
data/lib/redis/counter.rb CHANGED
@@ -7,6 +7,9 @@ class Redis
7
7
  # class to define a counter.
8
8
  #
9
9
  class Counter
10
+ require 'redis/helpers/core_commands'
11
+ include Redis::Helpers::CoreCommands
12
+
10
13
  attr_reader :key, :options, :redis
11
14
  def initialize(key, redis=$redis, options={})
12
15
  @key = key
@@ -21,7 +24,7 @@ class Redis
21
24
  # with a parent and starting over (for example, restarting a game and
22
25
  # disconnecting all players).
23
26
  def reset(to=options[:start])
24
- redis.set(key, to.to_i)
27
+ redis.set key, to.to_i
25
28
  end
26
29
 
27
30
  # Returns the current value of the counter. Normally just calling the
@@ -33,12 +36,6 @@ class Redis
33
36
  end
34
37
  alias_method :get, :value
35
38
 
36
- # Delete a counter. Usage discouraged. Consider +reset+ instead.
37
- def delete
38
- redis.del(key)
39
- end
40
- alias_method :del, :delete
41
-
42
39
  # Increment the counter atomically and return the new value. If passed
43
40
  # a block, that block will be evaluated with the new value of the counter
44
41
  # as an argument. If the block returns nil or throws an exception, the
@@ -0,0 +1,46 @@
1
+ class Redis
2
+ module Helpers
3
+ # These are core commands that all types share (rename, etc)
4
+ module CoreCommands
5
+ def exists?
6
+ redis.exists key
7
+ end
8
+
9
+ def delete
10
+ redis.del key
11
+ end
12
+ alias_method :del, :delete
13
+ alias_method :clear, :delete
14
+
15
+ def type
16
+ redis.type key
17
+ end
18
+
19
+ def rename(name, setkey=true)
20
+ dest = name.is_a?(self.class) ? name.key : name
21
+ ret = redis.rename key, dest
22
+ @key = dest if ret && setkey
23
+ ret
24
+ end
25
+
26
+ def renamenx(name, setkey=true)
27
+ dest = name.is_a?(self.class) ? name.key : name
28
+ ret = redis.renamenx key, dest
29
+ @key = dest if ret && setkey
30
+ ret
31
+ end
32
+
33
+ def expire(seconds)
34
+ redis.expire key, seconds
35
+ end
36
+
37
+ def expireat(unixtime)
38
+ redis.expire key, unixtime
39
+ end
40
+
41
+ def move(dbindex)
42
+ redis.move key, dbindex
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,25 @@
1
+ class Redis
2
+ module Helpers
3
+ module Serialize
4
+ include Marshal
5
+
6
+ def to_redis(value)
7
+ case value
8
+ when String, Fixnum, Bignum, Float
9
+ value
10
+ else
11
+ dump(value)
12
+ end
13
+ end
14
+
15
+ def from_redis(value)
16
+ case value
17
+ when Array
18
+ value.collect{|v| from_redis(v)}
19
+ else
20
+ restore(value) rescue value
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
data/lib/redis/list.rb CHANGED
@@ -6,8 +6,10 @@ class Redis
6
6
  class List
7
7
  require 'enumerator'
8
8
  include Enumerable
9
- require 'redis/serialize'
10
- include Redis::Serialize
9
+ require 'redis/helpers/core_commands'
10
+ include Redis::Helpers::CoreCommands
11
+ require 'redis/helpers/serialize'
12
+ include Redis::Helpers::Serialize
11
13
 
12
14
  attr_reader :key, :options, :redis
13
15
  def initialize(key, redis=$redis, options={})
@@ -63,6 +65,7 @@ class Redis
63
65
 
64
66
  # Delete the element(s) from the list that match name. If count is specified,
65
67
  # only the first-N (if positive) or last-N (if negative) will be removed.
68
+ # Use .del to completely delete the entire key.
66
69
  # Redis: LREM
67
70
  def delete(name, count=0)
68
71
  redis.lrem(key, count, name) # weird api
@@ -96,11 +99,6 @@ class Redis
96
99
  at(-1)
97
100
  end
98
101
 
99
- # Clear the list entirely. Redis: DEL
100
- def clear
101
- redis.del(key)
102
- end
103
-
104
102
  # Return the length of the list. Aliased as size. Redis: LLEN
105
103
  def length
106
104
  redis.llen(key)
@@ -4,9 +4,10 @@ require 'redis/counter'
4
4
  class Redis
5
5
  module Objects
6
6
  class UndefinedCounter < StandardError; end #:nodoc:
7
+ class MissingID < StandardError; end #:nodoc:
8
+
7
9
  module Counters
8
10
  def self.included(klass)
9
- klass.instance_variable_set('@counters', {})
10
11
  klass.instance_variable_set('@initialized_counters', {})
11
12
  klass.send :include, InstanceMethods
12
13
  klass.extend ClassMethods
@@ -14,33 +15,46 @@ class Redis
14
15
 
15
16
  # Class methods that appear in your class when you include Redis::Objects.
16
17
  module ClassMethods
17
- attr_reader :counters, :initialized_counters
18
+ attr_reader :initialized_counters
18
19
 
19
20
  # Define a new counter. It will function like a regular instance
20
21
  # method, so it can be used alongside ActiveRecord, DataMapper, etc.
21
22
  def counter(name, options={})
22
23
  options[:start] ||= 0
23
24
  options[:type] ||= options[:start] == 0 ? :increment : :decrement
24
- @counters[name] = options
25
- class_eval <<-EndMethods
26
- def #{name}
27
- @#{name} ||= Redis::Counter.new(field_key(:#{name}), redis, self.class.counters[:#{name}])
28
- end
29
- EndMethods
25
+ @redis_objects[name] = options.merge(:type => :counter)
26
+ if options[:global]
27
+ instance_eval <<-EndMethods
28
+ def #{name}
29
+ @#{name} ||= Redis::Counter.new(field_key(:#{name}, ''), redis, @redis_objects[:#{name}])
30
+ end
31
+ EndMethods
32
+ class_eval <<-EndMethods
33
+ def #{name}
34
+ self.class.#{name}
35
+ end
36
+ EndMethods
37
+ else
38
+ class_eval <<-EndMethods
39
+ def #{name}
40
+ @#{name} ||= Redis::Counter.new(field_key(:#{name}), redis, self.class.redis_objects[:#{name}])
41
+ end
42
+ EndMethods
43
+ end
30
44
  end
31
45
 
32
46
  # Get the current value of the counter. It is more efficient
33
47
  # to use the instance method if possible.
34
- def get_counter(name, id)
35
- verify_counter_defined!(name)
48
+ def get_counter(name, id=nil)
49
+ verify_counter_defined!(name, id)
36
50
  initialize_counter!(name, id)
37
51
  redis.get(field_key(name, id)).to_i
38
52
  end
39
53
 
40
54
  # Increment a counter with the specified name and id. Accepts a block
41
55
  # like the instance method. See Redis::Objects::Counter for details.
42
- def increment_counter(name, id, by=1, &block)
43
- verify_counter_defined!(name)
56
+ def increment_counter(name, id=nil, by=1, &block)
57
+ verify_counter_defined!(name, id)
44
58
  initialize_counter!(name, id)
45
59
  value = redis.incr(field_key(name, id), by).to_i
46
60
  block_given? ? rewindable_block(:decrement_counter, name, id, value, &block) : value
@@ -48,30 +62,33 @@ class Redis
48
62
 
49
63
  # Decrement a counter with the specified name and id. Accepts a block
50
64
  # like the instance method. See Redis::Objects::Counter for details.
51
- def decrement_counter(name, id, by=1, &block)
52
- verify_counter_defined!(name)
65
+ def decrement_counter(name, id=nil, by=1, &block)
66
+ verify_counter_defined!(name, id)
53
67
  initialize_counter!(name, id)
54
68
  value = redis.decr(field_key(name, id), by).to_i
55
69
  block_given? ? rewindable_block(:increment_counter, name, id, value, &block) : value
56
70
  end
57
71
 
58
72
  # Reset a counter to its starting value.
59
- def reset_counter(name, id, to=nil)
60
- verify_counter_defined!(name)
61
- to = @counters[name][:start] if to.nil?
73
+ def reset_counter(name, id=nil, to=nil)
74
+ verify_counter_defined!(name, id)
75
+ to = @redis_objects[name][:start] if to.nil?
62
76
  redis.set(field_key(name, id), to)
63
77
  end
64
78
 
65
79
  private
66
80
 
67
- def verify_counter_defined!(name) #:nodoc:
68
- raise Redis::Objects::UndefinedCounter, "Undefined counter :#{name} for class #{self.name}" unless @counters.has_key?(name)
81
+ def verify_counter_defined!(name, id) #:nodoc:
82
+ raise Redis::Objects::UndefinedCounter, "Undefined counter :#{name} for class #{self.name}" unless @redis_objects.has_key?(name)
83
+ if id.nil? and !@redis_objects[name][:global]
84
+ raise Redis::Objects::MissingID, "Missing ID for non-global counter #{self.name}##{name}"
85
+ end
69
86
  end
70
87
 
71
88
  def initialize_counter!(name, id) #:nodoc:
72
89
  key = field_key(name, id)
73
90
  unless @initialized_counters[key]
74
- redis.setnx(key, @counters[name][:start])
91
+ redis.setnx(key, @redis_objects[name][:start])
75
92
  end
76
93
  @initialized_counters[key] = true
77
94
  end
@@ -5,24 +5,34 @@ class Redis
5
5
  module Objects
6
6
  module Lists
7
7
  def self.included(klass)
8
- klass.instance_variable_set('@lists', {})
9
8
  klass.send :include, InstanceMethods
10
9
  klass.extend ClassMethods
11
10
  end
12
11
 
13
12
  # Class methods that appear in your class when you include Redis::Objects.
14
13
  module ClassMethods
15
- attr_reader :lists
16
-
17
14
  # Define a new list. It will function like a regular instance
18
15
  # method, so it can be used alongside ActiveRecord, DataMapper, etc.
19
16
  def list(name, options={})
20
- @lists[name] = options
21
- class_eval <<-EndMethods
22
- def #{name}
23
- @#{name} ||= Redis::List.new(field_key(:#{name}), redis, self.class.lists[:#{name}])
24
- end
25
- EndMethods
17
+ @redis_objects[name] = options.merge(:type => :list)
18
+ if options[:global]
19
+ instance_eval <<-EndMethods
20
+ def #{name}
21
+ @#{name} ||= Redis::List.new(field_key(:#{name}, ''), redis, @redis_objects[:#{name}])
22
+ end
23
+ EndMethods
24
+ class_eval <<-EndMethods
25
+ def #{name}
26
+ self.class.#{name}
27
+ end
28
+ EndMethods
29
+ else
30
+ class_eval <<-EndMethods
31
+ def #{name}
32
+ @#{name} ||= Redis::List.new(field_key(:#{name}), redis, self.class.redis_objects[:#{name}])
33
+ end
34
+ EndMethods
35
+ end
26
36
  end
27
37
  end
28
38
 
@@ -6,25 +6,38 @@ class Redis
6
6
  class UndefinedLock < StandardError; end #:nodoc:
7
7
  module Locks
8
8
  def self.included(klass)
9
- klass.instance_variable_set('@locks', {})
10
9
  klass.send :include, InstanceMethods
11
10
  klass.extend ClassMethods
12
11
  end
13
12
 
14
13
  # Class methods that appear in your class when you include Redis::Objects.
15
14
  module ClassMethods
16
- attr_reader :locks
17
-
18
15
  # Define a new lock. It will function like a model attribute,
19
16
  # so it can be used alongside ActiveRecord/DataMapper, etc.
20
17
  def lock(name, options={})
21
18
  options[:timeout] ||= 5 # seconds
22
- @locks[name] = options
23
- class_eval <<-EndMethods
24
- def #{name}_lock(&block)
25
- @#{name}_lock ||= Redis::Lock.new(field_key(:#{name}_lock), redis, self.class.locks[:#{name}])
26
- end
27
- EndMethods
19
+ @redis_objects[name] = options.merge(:type => :lock)
20
+ if options[:global]
21
+ instance_eval <<-EndMethods
22
+ def #{name}_lock(&block)
23
+ @#{name} ||= Redis::Lock.new(field_key(:#{name}_lock, ''), redis, @redis_objects[:#{name}])
24
+ end
25
+ EndMethods
26
+ class_eval <<-EndMethods
27
+ def #{name}_lock(&block)
28
+ self.class.#{name}(block)
29
+ end
30
+ EndMethods
31
+ else
32
+ class_eval <<-EndMethods
33
+ def #{name}_lock(&block)
34
+ @#{name} ||= Redis::Lock.new(field_key(:#{name}_lock), redis, self.class.redis_objects[:#{name}])
35
+ end
36
+ EndMethods
37
+ end
38
+
39
+
40
+
28
41
  end
29
42
 
30
43
  # Obtain a lock, and execute the block synchronously. Any other code
@@ -34,7 +47,7 @@ class Redis
34
47
  verify_lock_defined!(name)
35
48
  raise ArgumentError, "Missing block to #{self.name}.obtain_lock" unless block_given?
36
49
  lock_name = field_key("#{name}_lock", id)
37
- Redis::Lock.new(redis, lock_name, self.class.locks[name]).lock(&block)
50
+ Redis::Lock.new(redis, lock_name, self.class.redis_objects[name]).lock(&block)
38
51
  end
39
52
 
40
53
  # Clear the lock. Use with care - usually only in an Admin page to clear
@@ -48,7 +61,7 @@ class Redis
48
61
  private
49
62
 
50
63
  def verify_lock_defined!(name)
51
- raise Redis::Objects::UndefinedLock, "Undefined lock :#{name} for class #{self.name}" unless @locks.has_key?(name)
64
+ raise Redis::Objects::UndefinedLock, "Undefined lock :#{name} for class #{self.name}" unless @redis_objects.has_key?(name)
52
65
  end
53
66
  end
54
67
  end
@@ -5,24 +5,35 @@ class Redis
5
5
  module Objects
6
6
  module Sets
7
7
  def self.included(klass)
8
- klass.instance_variable_set('@sets', {})
9
8
  klass.send :include, InstanceMethods
10
9
  klass.extend ClassMethods
11
10
  end
12
11
 
13
12
  # Class methods that appear in your class when you include Redis::Objects.
14
13
  module ClassMethods
15
- attr_reader :sets
16
-
17
14
  # Define a new list. It will function like a regular instance
18
15
  # method, so it can be used alongside ActiveRecord, DataMapper, etc.
19
16
  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
17
+ @redis_objects[name] = options.merge(:type => :set)
18
+ if options[:global]
19
+ instance_eval <<-EndMethods
20
+ def #{name}
21
+ @#{name} ||= Redis::Set.new(field_key(:#{name}, ''), redis, @redis_objects[:#{name}])
22
+ end
23
+ EndMethods
24
+ class_eval <<-EndMethods
25
+ def #{name}
26
+ self.class.#{name}
27
+ end
28
+ EndMethods
29
+ else
30
+ class_eval <<-EndMethods
31
+ def #{name}
32
+ @#{name} ||= Redis::Set.new(field_key(:#{name}), redis, self.class.redis_objects[:#{name}])
33
+ end
34
+ EndMethods
35
+ end
36
+
26
37
  end
27
38
  end
28
39
 
@@ -5,27 +5,44 @@ class Redis
5
5
  module Objects
6
6
  module Values
7
7
  def self.included(klass)
8
- klass.instance_variable_set('@values', {})
9
8
  klass.send :include, InstanceMethods
10
9
  klass.extend ClassMethods
11
10
  end
12
11
 
13
12
  # Class methods that appear in your class when you include Redis::Objects.
14
13
  module ClassMethods
15
- attr_reader :values
16
-
17
14
  # Define a new simple value. It will function like a regular instance
18
15
  # method, so it can be used alongside ActiveRecord, DataMapper, etc.
19
16
  def value(name, options={})
20
- @values[name] = options
21
- class_eval <<-EndMethods
22
- def #{name}
23
- @#{name} ||= Redis::Value.new(field_key(:#{name}), redis, self.class.values[:#{name}])
24
- end
25
- def #{name}=(value)
26
- #{name}.value = value
27
- end
28
- EndMethods
17
+ @redis_objects[name] = options.merge(:type => :value)
18
+ if options[:global]
19
+ instance_eval <<-EndMethods
20
+ def #{name}
21
+ @#{name} ||= Redis::Value.new(field_key(:#{name}, ''), redis, @redis_objects[:#{name}])
22
+ end
23
+ def #{name}=(value)
24
+ #{name}.value = value
25
+ end
26
+ EndMethods
27
+ class_eval <<-EndMethods
28
+ def #{name}
29
+ self.class.#{name}
30
+ end
31
+ def #{name}=(value)
32
+ self.class.#{name} = value
33
+ end
34
+ EndMethods
35
+ else
36
+ class_eval <<-EndMethods
37
+ def #{name}
38
+ @#{name} ||= Redis::Value.new(field_key(:#{name}), redis, self.class.redis_objects[:#{name}])
39
+ end
40
+ def #{name}=(value)
41
+ #{name}.value = value
42
+ end
43
+ EndMethods
44
+ end
45
+
29
46
  end
30
47
  end
31
48
 
data/lib/redis/objects.rb CHANGED
@@ -55,6 +55,7 @@ class Redis
55
55
  def included(klass)
56
56
  # Core (this file)
57
57
  klass.instance_variable_set('@redis', @redis)
58
+ klass.instance_variable_set('@redis_objects', {})
58
59
  klass.send :include, InstanceMethods
59
60
  klass.extend ClassMethods
60
61
 
@@ -69,8 +70,7 @@ class Redis
69
70
 
70
71
  # Class methods that appear in your class when you include Redis::Objects.
71
72
  module ClassMethods
72
- attr_accessor :redis
73
-
73
+ attr_accessor :redis, :redis_objects
74
74
 
75
75
  # Set the Redis prefix to use. Defaults to model_name
76
76
  def prefix=(prefix) @prefix = prefix end
data/lib/redis/set.rb CHANGED
@@ -5,8 +5,10 @@ class Redis
5
5
  class Set
6
6
  require 'enumerator'
7
7
  include Enumerable
8
- require 'redis/serialize'
9
- include Redis::Serialize
8
+ require 'redis/helpers/core_commands'
9
+ include Redis::Helpers::CoreCommands
10
+ require 'redis/helpers/serialize'
11
+ include Redis::Helpers::Serialize
10
12
 
11
13
  attr_reader :key, :options, :redis
12
14
 
@@ -46,11 +48,6 @@ class Redis
46
48
  redis.srem(key, value)
47
49
  end
48
50
 
49
- # Wipe the set entirely. Redis: DEL
50
- def clear
51
- redis.del(key)
52
- end
53
-
54
51
  # Iterate through each member of the set. Redis::Objects mixes in Enumerable,
55
52
  # so you can also use familiar methods like +collect+, +detect+, and so forth.
56
53
  def each(&block)
@@ -99,11 +96,36 @@ class Redis
99
96
  alias_method :+, :union
100
97
 
101
98
  # Calculate the union and store it in Redis as +name+. Returns the number
102
- # of elements in the stored union. Redis: SINTERSTORE
99
+ # of elements in the stored union. Redis: SUNIONSTORE
103
100
  def unionstore(name, *sets)
104
101
  redis.sunionstore(name, key, *keys_from_objects(sets))
105
102
  end
106
103
 
104
+ # Return the difference vs another set. Can pass it either another set
105
+ # object or set name. Also available as ^ or - which is a bit cleaner:
106
+ #
107
+ # members_difference = set1 ^ set2
108
+ # members_difference = set1 - set2
109
+ #
110
+ # If you want to specify multiple sets, you must use +difference+:
111
+ #
112
+ # members_difference = set1.difference(set2, set3, set4)
113
+ # members_difference = set1.diff(set2, set3, set4)
114
+ #
115
+ # Redis: SDIFF
116
+ def difference(*sets)
117
+ from_redis redis.sdiff(key, *keys_from_objects(sets))
118
+ end
119
+ alias_method :diff, :difference
120
+ alias_method :^, :difference
121
+ alias_method :-, :difference
122
+
123
+ # Calculate the diff and store it in Redis as +name+. Returns the number
124
+ # of elements in the stored union. Redis: SDIFFSTORE
125
+ def diffstore(name, *sets)
126
+ redis.sdiffstore(name, key, *keys_from_objects(sets))
127
+ end
128
+
107
129
  # The number of members in the set. Aliased as size. Redis: SCARD
108
130
  def length
109
131
  redis.scard(key)
data/lib/redis/value.rb CHANGED
@@ -3,8 +3,10 @@ class Redis
3
3
  # Class representing a simple value. You can use standard Ruby operations on it.
4
4
  #
5
5
  class Value
6
- require 'redis/serialize'
7
- include Redis::Serialize
6
+ require 'redis/helpers/core_commands'
7
+ include Redis::Helpers::CoreCommands
8
+ require 'redis/helpers/serialize'
9
+ include Redis::Helpers::Serialize
8
10
 
9
11
  attr_reader :key, :options, :redis
10
12
  def initialize(key, redis=$redis, options={})
@@ -15,19 +17,15 @@ class Redis
15
17
  end
16
18
 
17
19
  def value=(val)
18
- redis.set(key, to_redis(val))
20
+ redis.set key, to_redis(val)
19
21
  end
20
-
22
+ alias_method :set, :value=
23
+
21
24
  def value
22
25
  from_redis redis.get(key)
23
26
  end
24
27
  alias_method :get, :value
25
28
 
26
- def delete
27
- redis.del(key)
28
- end
29
- alias_method :del, :delete
30
-
31
29
  def to_s; value.to_s; end
32
30
  alias_method :to_str, :to_s
33
31
 
@@ -36,6 +36,18 @@ describe Redis::Value do
36
36
  @value.should be_nil
37
37
  end
38
38
 
39
+ it "should support renaming values" do
40
+ @value.value = 'Peter Pan'
41
+ @value.key.should == 'spec/value'
42
+ @value.rename('spec/value2').should be_true
43
+ @value.key.should == 'spec/value2'
44
+ @value.should == 'Peter Pan'
45
+ old = Redis::Value.new('spec/value')
46
+ old.should be_nil
47
+ old.value = 'Tuff'
48
+ @value.renamenx('spec/value').should be_false
49
+ end
50
+
39
51
  after :all do
40
52
  @value.delete
41
53
  end
@@ -131,6 +143,28 @@ describe Redis::List do
131
143
  @list.last.should == [1,2,3,[4,5]]
132
144
  @list.shift.should == {:json => 'data'}
133
145
  end
146
+
147
+ it "should support renaming lists" do
148
+ @list.should be_empty
149
+ @list << 'a' << 'b' << 'a' << 3
150
+ @list.should == ['a','b','a','3']
151
+ @list.key.should == 'spec/list'
152
+ @list.rename('spec/list3', false).should be_true
153
+ @list.key.should == 'spec/list'
154
+ @list.redis.del('spec/list3')
155
+ @list << 'a' << 'b' << 'a' << 3
156
+ @list.rename('spec/list2').should be_true
157
+ @list.key.should == 'spec/list2'
158
+ @list.redis.lrange(@list.key, 0, 3).should == ['a','b','a','3']
159
+ old = Redis::List.new('spec/list')
160
+ old.should be_empty
161
+ old << 'Tuff'
162
+ @list.renamenx('spec/list').should be_false
163
+ @list.renamenx(old).should be_false
164
+ @list.renamenx('spec/foo').should be_true
165
+ @list.clear
166
+ @list.redis.del('spec/list2')
167
+ end
134
168
 
135
169
  after :all do
136
170
  @list.clear
@@ -191,7 +225,7 @@ describe Redis::Set do
191
225
  @set.sort.should == ['a','b','c']
192
226
  end
193
227
 
194
- it "should handle set intersections and unions" do
228
+ it "should handle set intersections, unions, and diffs" do
195
229
  @set_1 << 'a' << 'b' << 'c' << 'd' << 'e'
196
230
  @set_2 << 'c' << 'd' << 'e' << 'f' << 'g'
197
231
  @set_3 << 'a' << 'd' << 'g' << 'l' << 'm'
@@ -216,6 +250,34 @@ describe Redis::Set do
216
250
  @set_1.redis.smembers(UNIONSTORE_KEY).sort.should == ['a','b','c','d','e','f','g']
217
251
  @set_1.unionstore(UNIONSTORE_KEY, @set_2, @set_3).should == 9
218
252
  @set_1.redis.smembers(UNIONSTORE_KEY).sort.should == ['a','b','c','d','e','f','g','l','m']
253
+
254
+ (@set_1 ^ @set_2).sort.should == ["a", "b"]
255
+ (@set_1 - @set_2).sort.should == ["a", "b"]
256
+ (@set_2 - @set_1).sort.should == ["f", "g"]
257
+ @set_1.difference(@set_2).sort.should == ["a", "b"]
258
+ @set_1.diff(@set_2).sort.should == ["a", "b"]
259
+ @set_1.difference(@set_2, @set_3).sort.should == ['b']
260
+ @set_1.diffstore(DIFFSTORE_KEY, @set_2).should == 2
261
+ @set_1.redis.smembers(DIFFSTORE_KEY).sort.should == ['a','b']
262
+ @set_1.diffstore(DIFFSTORE_KEY, @set_2, @set_3).should == 1
263
+ @set_1.redis.smembers(DIFFSTORE_KEY).sort.should == ['b']
264
+ end
265
+
266
+ it "should support renaming sets" do
267
+ @set.should be_empty
268
+ @set << 'a' << 'b' << 'a' << 3
269
+ @set.sort.should == ['3','a','b']
270
+ @set.key.should == 'spec/set'
271
+ @set.rename('spec/set2').should be_true
272
+ @set.key.should == 'spec/set2'
273
+ old = Redis::Set.new('spec/set')
274
+ old.should be_empty
275
+ old << 'Tuff'
276
+ @set.renamenx('spec/set').should be_false
277
+ @set.renamenx(old).should be_false
278
+ @set.renamenx('spec/foo').should be_true
279
+ @set.clear
280
+ @set.redis.del('spec/set2')
219
281
  end
220
282
 
221
283
  after :all do
@@ -9,12 +9,17 @@ class Roster
9
9
  counter :available_slots, :start => 10
10
10
  counter :pitchers, :limit => :max_pitchers
11
11
  counter :basic
12
- counter :all_players_online, :global => true
13
12
  lock :resort, :timeout => 2
14
13
  value :starting_pitcher
15
14
  list :player_stats
16
15
  set :outfielders
17
16
 
17
+ # global class counters
18
+ counter :total_players_online, :global => true
19
+ list :all_player_stats, :global => true
20
+ set :all_players_online, :global => true
21
+ value :last_player, :global => true
22
+
18
23
  def initialize(id=1) @id = id end
19
24
  def id; @id; end
20
25
  def max_pitchers; 3; end
@@ -43,6 +48,12 @@ describe Redis::Objects do
43
48
  @roster_3.outfielders.clear
44
49
  @roster.redis.del(UNIONSTORE_KEY)
45
50
  @roster.redis.del(INTERSTORE_KEY)
51
+ @roster.redis.del(DIFFSTORE_KEY)
52
+
53
+ Roster.total_players_online.reset
54
+ Roster.all_player_stats.clear
55
+ Roster.all_players_online.clear
56
+ Roster.last_player.delete
46
57
  end
47
58
 
48
59
  it "should provide a connection method" do
@@ -97,6 +108,24 @@ describe Redis::Objects do
97
108
  Roster.get_counter(:available_slots, @roster.id).should == 10
98
109
  end
99
110
 
111
+ it "should support class-level increment/decrement of global counters" do
112
+ Roster.total_players_online.should == 0
113
+ Roster.total_players_online.increment.should == 1
114
+ Roster.total_players_online.decrement.should == 0
115
+ Roster.total_players_online.increment(3).should == 3
116
+ Roster.total_players_online.decrement(2).should == 1
117
+ Roster.total_players_online.reset.should be_true
118
+ Roster.total_players_online.should == 0
119
+
120
+ Roster.get_counter(:total_players_online).should == 0
121
+ Roster.increment_counter(:total_players_online).should == 1
122
+ Roster.increment_counter(:total_players_online, nil, 3).should == 4
123
+ Roster.decrement_counter(:total_players_online, nil, 2).should == 2
124
+ Roster.decrement_counter(:total_players_online).should == 1
125
+ Roster.reset_counter(:total_players_online).should == true
126
+ Roster.get_counter(:total_players_online).should == 0
127
+ end
128
+
100
129
  it "should take an atomic block for increment/decrement" do
101
130
  a = false
102
131
  @roster.available_slots.should == 10
@@ -259,6 +288,8 @@ describe Redis::Objects do
259
288
  @roster.starting_pitcher = 'Trevor Hoffman'
260
289
  @roster.starting_pitcher.should == 'Trevor Hoffman'
261
290
  @roster.starting_pitcher.get.should == 'Trevor Hoffman'
291
+ @roster.starting_pitcher = 'Tom Selleck'
292
+ @roster.starting_pitcher.should == 'Tom Selleck'
262
293
  @roster.starting_pitcher.del.should be_true
263
294
  @roster.starting_pitcher.should be_nil
264
295
  end
@@ -409,6 +440,162 @@ describe Redis::Objects do
409
440
  @roster_1.redis.smembers(UNIONSTORE_KEY).sort.should == ['a','b','c','d','e','f','g','l','m']
410
441
  end
411
442
 
443
+ it "should handle class-level global lists of simple values" do
444
+ Roster.all_player_stats.should be_empty
445
+ Roster.all_player_stats << 'a'
446
+ Roster.all_player_stats.should == ['a']
447
+ Roster.all_player_stats.get.should == ['a']
448
+ Roster.all_player_stats.unshift 'b'
449
+ Roster.all_player_stats.to_s.should == 'b, a'
450
+ Roster.all_player_stats.should == ['b','a']
451
+ Roster.all_player_stats.get.should == ['b','a']
452
+ Roster.all_player_stats.push 'c'
453
+ Roster.all_player_stats.should == ['b','a','c']
454
+ Roster.all_player_stats.get.should == ['b','a','c']
455
+ Roster.all_player_stats.first.should == 'b'
456
+ Roster.all_player_stats.last.should == 'c'
457
+ Roster.all_player_stats << 'd'
458
+ Roster.all_player_stats.should == ['b','a','c','d']
459
+ Roster.all_player_stats[1].should == 'a'
460
+ Roster.all_player_stats[0].should == 'b'
461
+ Roster.all_player_stats[2].should == 'c'
462
+ Roster.all_player_stats[3].should == 'd'
463
+ Roster.all_player_stats.include?('c').should be_true
464
+ Roster.all_player_stats.include?('no').should be_false
465
+ Roster.all_player_stats.pop.should == 'd'
466
+ Roster.all_player_stats[0].should == Roster.all_player_stats.at(0)
467
+ Roster.all_player_stats[1].should == Roster.all_player_stats.at(1)
468
+ Roster.all_player_stats[2].should == Roster.all_player_stats.at(2)
469
+ Roster.all_player_stats.should == ['b','a','c']
470
+ Roster.all_player_stats.get.should == ['b','a','c']
471
+ Roster.all_player_stats.shift.should == 'b'
472
+ Roster.all_player_stats.should == ['a','c']
473
+ Roster.all_player_stats.get.should == ['a','c']
474
+ Roster.all_player_stats << 'e' << 'f' << 'e'
475
+ Roster.all_player_stats.should == ['a','c','e','f','e']
476
+ Roster.all_player_stats.get.should == ['a','c','e','f','e']
477
+ Roster.all_player_stats.delete('e').should == 2
478
+ Roster.all_player_stats.should == ['a','c','f']
479
+ Roster.all_player_stats.get.should == ['a','c','f']
480
+ Roster.all_player_stats << 'j'
481
+ Roster.all_player_stats.should == ['a','c','f','j']
482
+ Roster.all_player_stats[0..2].should == ['a','c','f']
483
+ Roster.all_player_stats[1, 3].should == ['c','f','j']
484
+ Roster.all_player_stats.length.should == 4
485
+ Roster.all_player_stats.size.should == 4
486
+ Roster.all_player_stats.should == ['a','c','f','j']
487
+ Roster.all_player_stats.get.should == ['a','c','f','j']
488
+
489
+ i = -1
490
+ Roster.all_player_stats.each do |st|
491
+ st.should == Roster.all_player_stats[i += 1]
492
+ end
493
+ Roster.all_player_stats.should == ['a','c','f','j']
494
+ Roster.all_player_stats.get.should == ['a','c','f','j']
495
+
496
+ Roster.all_player_stats.each_with_index do |st,i|
497
+ st.should == Roster.all_player_stats[i]
498
+ end
499
+ Roster.all_player_stats.should == ['a','c','f','j']
500
+ Roster.all_player_stats.get.should == ['a','c','f','j']
501
+
502
+ coll = Roster.all_player_stats.collect{|st| st}
503
+ coll.should == ['a','c','f','j']
504
+ Roster.all_player_stats.should == ['a','c','f','j']
505
+ Roster.all_player_stats.get.should == ['a','c','f','j']
506
+
507
+ Roster.all_player_stats << 'a'
508
+ coll = Roster.all_player_stats.select{|st| st == 'a'}
509
+ coll.should == ['a','a']
510
+ Roster.all_player_stats.should == ['a','c','f','j','a']
511
+ Roster.all_player_stats.get.should == ['a','c','f','j','a']
512
+ end
513
+
514
+ it "should handle class-level global sets of simple values" do
515
+ Roster.all_players_online.should be_empty
516
+ Roster.all_players_online << 'a' << 'a' << 'a'
517
+ Roster.all_players_online.should == ['a']
518
+ Roster.all_players_online.get.should == ['a']
519
+ Roster.all_players_online << 'b' << 'b'
520
+ Roster.all_players_online.to_s.should == 'a, b'
521
+ Roster.all_players_online.should == ['a','b']
522
+ Roster.all_players_online.members.should == ['a','b']
523
+ Roster.all_players_online.get.should == ['a','b']
524
+ Roster.all_players_online << 'c'
525
+ Roster.all_players_online.sort.should == ['a','b','c']
526
+ Roster.all_players_online.get.sort.should == ['a','b','c']
527
+ Roster.all_players_online.delete('c')
528
+ Roster.all_players_online.should == ['a','b']
529
+ Roster.all_players_online.get.sort.should == ['a','b']
530
+ Roster.all_players_online.length.should == 2
531
+ Roster.all_players_online.size.should == 2
532
+
533
+ i = 0
534
+ Roster.all_players_online.each do |st|
535
+ i += 1
536
+ end
537
+ i.should == Roster.all_players_online.length
538
+
539
+ coll = Roster.all_players_online.collect{|st| st}
540
+ coll.should == ['a','b']
541
+ Roster.all_players_online.should == ['a','b']
542
+ Roster.all_players_online.get.should == ['a','b']
543
+
544
+ Roster.all_players_online << 'c'
545
+ Roster.all_players_online.member?('c').should be_true
546
+ Roster.all_players_online.include?('c').should be_true
547
+ Roster.all_players_online.member?('no').should be_false
548
+ coll = Roster.all_players_online.select{|st| st == 'c'}
549
+ coll.should == ['c']
550
+ Roster.all_players_online.sort.should == ['a','b','c']
551
+ end
552
+
553
+ it "should handle class-level global values" do
554
+ Roster.last_player.should == nil
555
+ Roster.last_player = 'Trevor Hoffman'
556
+ Roster.last_player.should == 'Trevor Hoffman'
557
+ Roster.last_player.get.should == 'Trevor Hoffman'
558
+ Roster.last_player = 'Tom Selleck'
559
+ Roster.last_player.should == 'Tom Selleck'
560
+ Roster.last_player.del.should be_true
561
+ Roster.last_player.should be_nil
562
+ end
563
+
564
+ it "should easily enable @object.class.global_objects" do
565
+ @roster.class.all_players_online.should be_empty
566
+ @roster.class.all_players_online << 'a' << 'a' << 'a'
567
+ @roster.class.all_players_online.should == ['a']
568
+ @roster2.class.all_players_online.should == ['a']
569
+
570
+ @roster.all_players_online.should == ['a']
571
+ @roster2.all_players_online.should == ['a']
572
+
573
+ @roster.class.all_player_stats.should be_empty
574
+ @roster.class.all_player_stats << 'a'
575
+ @roster.class.all_player_stats.should == ['a']
576
+ @roster.class.all_player_stats.get.should == ['a']
577
+ @roster.class.all_player_stats.unshift 'b'
578
+ @roster.class.all_player_stats.to_s.should == 'b, a'
579
+ @roster.class.all_player_stats.should == ['b','a']
580
+ @roster2.class.all_player_stats.should == ['b','a']
581
+
582
+ @roster.all_player_stats.should == ['b','a']
583
+ @roster2.all_player_stats.should == ['b','a']
584
+ @roster2.all_player_stats << 'b'
585
+ @roster.all_player_stats.should == ['b','a','b']
586
+
587
+ @roster.last_player.should == nil
588
+ @roster.class.last_player = 'Trevor Hoffman'
589
+ @roster.last_player.should == 'Trevor Hoffman'
590
+ @roster.last_player.get.should == 'Trevor Hoffman'
591
+ @roster2.last_player.get.should == 'Trevor Hoffman'
592
+ @roster2.last_player = 'Tom Selleck'
593
+ @roster.last_player.should == 'Tom Selleck'
594
+ @roster.last_player.del.should be_true
595
+ @roster.last_player.should be_nil
596
+ @roster2.last_player.should be_nil
597
+ end
598
+
412
599
  it "should handle lists of complex data types" do
413
600
  @roster.player_stats << {:json => 'data'}
414
601
  @roster.player_stats << {:json2 => 'data2'}
data/spec/spec_helper.rb CHANGED
@@ -5,3 +5,4 @@ $redis = Redis.new(:host => ENV['REDIS_HOST'], :port => ENV['REDIS_PORT'])
5
5
 
6
6
  UNIONSTORE_KEY = 'test:unionstore'
7
7
  INTERSTORE_KEY = 'test:interstore'
8
+ DIFFSTORE_KEY = 'test:diffstore'
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.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Wiger
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-27 00:00:00 -08:00
12
+ date: 2009-12-14 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -30,25 +30,27 @@ extensions: []
30
30
 
31
31
  extra_rdoc_files:
32
32
  - ATOMICITY.rdoc
33
+ - ChangeLog
33
34
  - README.rdoc
34
35
  files:
35
36
  - lib/redis/counter.rb
37
+ - lib/redis/helpers/core_commands.rb
38
+ - lib/redis/helpers/serialize.rb
36
39
  - lib/redis/list.rb
37
40
  - lib/redis/lock.rb
38
- - lib/redis/objects/core.rb
39
41
  - lib/redis/objects/counters.rb
40
42
  - lib/redis/objects/lists.rb
41
43
  - lib/redis/objects/locks.rb
42
44
  - lib/redis/objects/sets.rb
43
45
  - lib/redis/objects/values.rb
44
46
  - lib/redis/objects.rb
45
- - lib/redis/serialize.rb
46
47
  - lib/redis/set.rb
47
48
  - lib/redis/value.rb
48
49
  - spec/redis_objects_instance_spec.rb
49
50
  - spec/redis_objects_model_spec.rb
50
51
  - spec/spec_helper.rb
51
52
  - ATOMICITY.rdoc
53
+ - ChangeLog
52
54
  - README.rdoc
53
55
  has_rdoc: true
54
56
  homepage: http://github.com/nateware/redis-objects
File without changes
@@ -1,23 +0,0 @@
1
- class Redis
2
- module Serialize
3
- include Marshal
4
-
5
- def to_redis(value)
6
- case value
7
- when String, Fixnum, Bignum, Float
8
- value
9
- else
10
- dump(value)
11
- end
12
- end
13
-
14
- def from_redis(value)
15
- case value
16
- when Array
17
- value.collect{|v| from_redis(v)}
18
- else
19
- restore(value) rescue value
20
- end
21
- end
22
- end
23
- end