redis-objects 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.rdoc 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: