redis-objects 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -6,97 +6,43 @@ 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. That's where this gem comes in.
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
  This gem originally arose out of a need for high-concurrency atomic operations;
12
13
  for a fun rant on the topic, see
13
14
  {ATOMICITY}[http://github.com/nateware/redis-objects/blob/master/ATOMICITY.rdoc],
14
15
  or scroll down to "Atomicity" in this README.
15
16
 
16
- There are two ways to use Redis::Objects, either as an +include+ in a class, or
17
- 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.
18
19
 
19
20
  == Installation
20
21
 
21
22
  gem install gemcutter
22
23
  gem tumble
23
24
  gem install redis-objects
24
-
25
- == Example 1: Class Usage
26
25
 
27
- === Initialization
28
-
29
- # If on Rails, config/initializers/redis.rb is a good place for this
30
- require 'redis'
31
- require 'redis/objects'
32
- Redis::Objects.redis = Redis.new(:host => 127.0.0.1, :port => 6379)
33
-
34
- === Model Class
26
+ == Example 1: Individual Usage
35
27
 
36
- Include in any type of class:
37
-
38
- class Team < ActiveRecord::Base
39
- include Redis::Objects
40
-
41
- counter :hits
42
- counter :runs
43
- counter :outs
44
- counter :inning, :start => 1
45
- list :on_base
46
- set :outfielders
47
- value :at_bat
48
- end
49
-
50
- Familiar Ruby array operations Just Work (TM):
51
-
52
- @team = Team.find(1)
53
- @team.on_base << 'player1'
54
- @team.on_base << 'player2'
55
- @team.on_base << 'player3'
56
- @team.on_base # ['player1', 'player2']
57
- @team.on_base.pop
58
- @team.on_base.shift
59
- @team.on_base.length # 1
60
- @team.on_base.delete('player3')
61
-
62
- Sets work like the Ruby {Set}[http://ruby-doc.org/core/classes/Set.html] class:
63
-
64
- @team.outfielders << 'outfielder1' << 'outfielder1'
65
- @team.outfielders << 'outfielder2'
66
- @team.outfielders # ['outfielder1', 'outfielder2']
67
- @team.outfielders.each do |player|
68
- puts player
69
- end
70
- player = @team.outfielders.detect{|of| of == 'outfielder2'}
71
-
72
- Note counters cannot be assigned to, only incremented/decremented:
73
-
74
- @team.hits.increment # or incr
75
- @team.hits.decrement # or decr
76
- @team.runs = 4 # exception
77
- @team.runs += 1 # exception
78
-
79
- It would be cool to get that last one working, but Ruby's implementation of +=
80
- is problematic.
81
-
82
- == Example 2: Instance Usage
83
-
84
- Each data type can be used independently.
28
+ There is a Ruby object that maps to each Redis type.
85
29
 
86
30
  === Initialization
87
31
 
88
- Can follow the +$redis+ global variable pattern:
32
+ You can either set the $redis global variable to your Redis handle:
89
33
 
90
34
  $redis = Redis.new(:host => 'localhost', :port => 6379)
91
35
  @value = Redis::Value.new('myvalue')
92
36
 
93
- Or can pass the handle into the new method:
94
-
37
+ Or you can pass the Redis handle into the new method:
38
+
95
39
  redis = Redis.new(:host => 'localhost', :port => 6379)
96
40
  @value = Redis::Value.new('myvalue', redis)
97
41
 
98
42
  === Counters
99
43
 
44
+ Create a new counter. The +counter_name+ is the key stored in Redis.
45
+
100
46
  @counter = Redis::Counter.new('counter_name')
101
47
  @counter.increment
102
48
  @counter.decrement
@@ -109,13 +55,18 @@ See the section on "Atomicity" for cool uses of atomic counter blocks.
109
55
 
110
56
  === Lists
111
57
 
112
- These work just like Ruby arrays:
58
+ Lists work just like Ruby arrays:
113
59
 
114
60
  @list = Redis::List.new('list_name')
115
61
  @list << 'a'
116
62
  @list << 'b'
117
63
  @list.include? 'c' # false
118
64
  @list.values # ['a','b']
65
+ @list << 'c'
66
+ @list.delete('c')
67
+ @list[0]
68
+ @list[0,1]
69
+ @list[0..1]
119
70
  @list.shift
120
71
  @list.pop
121
72
  @list.clear
@@ -128,7 +79,7 @@ Sets work like the Ruby {Set}[http://ruby-doc.org/core/classes/Set.html] class:
128
79
  @set = Redis::Set.new('set_name')
129
80
  @set << 'a'
130
81
  @set << 'b'
131
- @set << 'a'
82
+ @set << 'a' # dup ignored
132
83
  @set.member? 'c' # false
133
84
  @set.members # ['a','b']
134
85
  @set.each do |member|
@@ -137,16 +88,105 @@ Sets work like the Ruby {Set}[http://ruby-doc.org/core/classes/Set.html] class:
137
88
  @set.clear
138
89
  # etc
139
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
+
140
109
  === Values
141
110
 
111
+ Simple values are easy as well:
112
+
142
113
  @value = Redis::Value.new('value_name')
143
114
  @value.value = 'a'
144
115
  @value.delete
145
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
+
129
+ require 'redis'
130
+ require 'redis/objects'
131
+ Redis::Objects.redis = Redis.new(:host => 127.0.0.1, :port => 6379)
132
+
133
+ === Model Class
134
+
135
+ Include Redis::Objects in any type of class:
136
+
137
+ class Team < ActiveRecord::Base
138
+ include Redis::Objects
139
+
140
+ counter :hits
141
+ counter :runs
142
+ counter :outs
143
+ counter :inning, :start => 1
144
+ list :on_base
145
+ set :outfielders
146
+ value :at_bat
147
+ end
148
+
149
+ Familiar Ruby array operations Just Work (TM):
150
+
151
+ @team = Team.find_by_name('New York Yankees')
152
+ @team.on_base << 'player1'
153
+ @team.on_base << 'player2'
154
+ @team.on_base << 'player3'
155
+ @team.on_base # ['player1', 'player2']
156
+ @team.on_base.pop
157
+ @team.on_base.shift
158
+ @team.on_base.length # 1
159
+ @team.on_base.delete('player3')
160
+
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'}
170
+
171
+ Note counters cannot be assigned to, only incremented/decremented:
172
+
173
+ @team.hits.increment # or incr
174
+ @team.hits.decrement # or decr
175
+ @team.hits.incr(3) # add 3
176
+ @team.runs = 4 # exception
177
+
178
+ For free, you get a +redis+ handle usable in your class:
179
+
180
+ @team.redis.get('somekey')
181
+ @team.redis.smembers('someset')
182
+
183
+ You can call any operation supported by {Redis}[http://code.google.com/p/redis/wiki/CommandReference]
184
+
146
185
  == Atomicity
147
186
 
148
187
  You are probably not handling atomicity correctly in your app. For a fun rant
149
- on the topic, see {ATOMICITY}[ATOMICITY.doc]
188
+ on the topic, see
189
+ {ATOMICITY}[http://github.com/nateware/redis-objects/blob/master/ATOMICITY.rdoc].
150
190
 
151
191
  Atomic counters are a good way to handle concurrency:
152
192
 
data/lib/redis/set.rb CHANGED
@@ -7,6 +7,8 @@ class Redis
7
7
  include Enumerable
8
8
 
9
9
  attr_reader :key, :options, :redis
10
+
11
+ # Create a new Set.
10
12
  def initialize(key, redis=$redis, options={})
11
13
  @key = key
12
14
  @redis = redis
@@ -54,6 +56,53 @@ class Redis
54
56
  members.each(&block)
55
57
  end
56
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
+
57
106
  # The number of members in the set. Aliased as size. Redis: SCARD
58
107
  def length
59
108
  redis.scard(key)
@@ -72,5 +121,13 @@ class Redis
72
121
  def to_s
73
122
  members.join(', ')
74
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
+
75
132
  end
76
133
  end
@@ -0,0 +1,18 @@
1
+
2
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
+
4
+ describe Redis::List do
5
+
6
+
7
+ end
8
+
9
+ describe Redis::Set do
10
+
11
+
12
+ end
13
+
14
+ describe Redis::Value do
15
+
16
+
17
+ end
18
+
@@ -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
@@ -21,6 +24,10 @@ describe Redis::Objects do
21
24
  before :all do
22
25
  @roster = Roster.new
23
26
  @roster2 = Roster.new
27
+
28
+ @roster_1 = Roster.new(1)
29
+ @roster_2 = Roster.new(2)
30
+ @roster_3 = Roster.new(3)
24
31
  end
25
32
 
26
33
  before :each do
@@ -31,6 +38,11 @@ describe Redis::Objects do
31
38
  @roster.starting_pitcher.delete
32
39
  @roster.player_stats.clear
33
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)
34
46
  end
35
47
 
36
48
  it "should provide a connection method" do
@@ -361,6 +373,33 @@ describe Redis::Objects do
361
373
  coll.should == ['c']
362
374
  @roster.outfielders.sort.should == ['a','b','c']
363
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']
402
+ end
364
403
 
365
404
  it "should provide a lock method that accepts a block" do
366
405
  @roster.resort_lock.key.should == 'roster:1:resort_lock'
@@ -371,7 +410,7 @@ describe Redis::Objects do
371
410
  a.should be_true
372
411
  end
373
412
 
374
- xit "should raise an exception if the timeout is exceeded" do
413
+ it "should raise an exception if the timeout is exceeded" do
375
414
  @roster.redis.set(@roster.resort_lock.key, 1)
376
415
  error = nil
377
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.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Wiger
@@ -11,8 +11,17 @@ cert_chain: []
11
11
 
12
12
  date: 2009-11-26 00:00:00 -08:00
13
13
  default_executable:
14
- dependencies: []
15
-
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:
16
25
  description: Map Redis types directly to Ruby objects. Works with any class or ORM.
17
26
  email: nate@wiger.org
18
27
  executables: []
@@ -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,8 +72,8 @@ 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: