redis-objects 0.2.1 → 0.2.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.
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