redis-objects 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,13 +4,12 @@ This is *not* an ORM. People that are wrapping ORM's around Redis are missing
4
4
  the point.
5
5
 
6
6
  The killer feature of Redis that it allows you to perform atomic operations
7
- on _individual_ data structures, like counters, lists, and sets, that you can use
8
- *with* your existing ActiveRecord/DataMapper/etc models, or in classes that have
7
+ on _individual_ data structures, like counters, lists, and sets. You can then use
8
+ these *with* your existing ActiveRecord/DataMapper/etc models, or in classes that have
9
9
  nothing to do with an ORM or even a database. That's where this gem comes in.
10
10
 
11
-
12
- This gem originally arose out of a need for high-concurrency operations for online
13
- games; for a fun rant on the topic, see
11
+ This gem originally arose out of a need for high-concurrency atomic operations;
12
+ for a fun rant on the topic, see
14
13
  {ATOMICITY}[http://github.com/nateware/redis-objects/blob/master/ATOMICITY.rdoc],
15
14
  or scroll down to "Atomicity" in this README.
16
15
 
@@ -23,6 +22,8 @@ by using +new+ with the type of data structure you want to create.
23
22
  gem tumble
24
23
  gem install redis-objects
25
24
 
25
+ == Example 1: Class Usage
26
+
26
27
  === Initialization
27
28
 
28
29
  # If on Rails, config/initializers/redis.rb is a good place for this
@@ -30,42 +31,76 @@ by using +new+ with the type of data structure you want to create.
30
31
  require 'redis/objects'
31
32
  Redis::Objects.redis = Redis.new(:host => 127.0.0.1, :port => 6379)
32
33
 
33
- == Examples
34
+ === Model Class
34
35
 
35
- === Model Class Usage
36
+ Include in any type of class:
36
37
 
37
38
  class Team < ActiveRecord::Base
38
39
  include Redis::Objects
39
40
 
40
- counter :drafted_players
41
- counter :active_players
42
- counter :total_online_players, :global => true
41
+ counter :hits
42
+ counter :runs
43
+ counter :outs
44
+ counter :inning, :start => 1
43
45
  list :on_base
44
46
  set :outfielders
45
- value :coach
47
+ value :at_bat
46
48
  end
47
49
 
48
- Familiar Ruby operations Just Work:
50
+ Familiar Ruby array operations Just Work (TM):
49
51
 
50
52
  @team = Team.find(1)
51
53
  @team.on_base << 'player1'
52
54
  @team.on_base << 'player2'
53
- puts @team.on_base # ['player1', 'player2']
55
+ @team.on_base << 'player3'
56
+ puts @team.on_base # ['player1', 'player2']
54
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['1b'] = 'outfielder1'
65
+ @team.outfielders['lf'] = 'outfielder3'
66
+ @team.outfielders['lf'] = 'outfielder2'
67
+ @team.outfielders.keys
68
+ @team.outfielders.each do |position,player|
69
+ puts "#{player} is playing #{position}"
70
+ end
71
+ position = @team.outfielders.detect{|pos,of| of == 'outfielder3'}
72
+
73
+ Note counters cannot be assigned to, only incremented/decremented:
74
+
75
+ @team.hits.increment # or incr
76
+ @team.hits.decrement # or decr
77
+ @team.runs = 4 # exception
78
+ @team.runs += 1 # exception
55
79
 
56
- With one purposeful exception - counters cannot be set, only incremented/decremented:
80
+ It would be cool to get that last one working, but Ruby's implementation of +=
81
+ is problematic.
57
82
 
58
- @team.drafted_players.increment # or incr
59
- @team.drafted_players.decrement # or decr
60
- @team.drafted_players = 4 # exception
61
- @team.drafted_players += 1 # exception
83
+ == Example 2: Instance Usage
62
84
 
63
- === Instance Usage
85
+ Each data type can be used independently.
86
+
87
+ === Initialization
88
+
89
+ Can follow the +$redis+ global variable pattern:
90
+
91
+ $redis = Redis.new(:host => 'localhost', :port => 6379)
92
+ @value = Redis::Value.new('myvalue')
93
+
94
+ Or can pass the handle into the new method:
95
+
96
+ redis = Redis.new(:host => 'localhost', :port => 6379)
97
+ @value = Redis::Value.new('myvalue', redis)
64
98
 
65
99
  === Counters
66
100
 
67
101
  @counter = Redis::Counter.new('counter_name')
68
102
  @counter.increment
103
+ @counter.decrement
69
104
  puts @counter
70
105
  puts @counter.get # force re-fetch
71
106
 
@@ -8,12 +8,11 @@ class Redis
8
8
  #
9
9
  class Counter
10
10
  attr_reader :key, :options, :redis
11
- def initialize(key, options={})
11
+ def initialize(key, redis=$redis, options={})
12
12
  @key = key
13
+ @redis = redis
13
14
  @options = options
14
- @redis = options[:redis] || $redis || Redis::Objects.redis
15
15
  @options[:start] ||= 0
16
- @options[:type] ||= @options[:start] == 0 ? :increment : :decrement
17
16
  @redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
18
17
  end
19
18
 
@@ -4,71 +4,77 @@ class Redis
4
4
  # behave as much like Ruby arrays as possible.
5
5
  #
6
6
  class List
7
+ require 'enumerator'
8
+ include Enumerable
9
+
7
10
  attr_reader :key, :options, :redis
8
- def initialize(key, options={})
11
+ def initialize(key, redis=$redis, options={})
9
12
  @key = key
13
+ @redis = redis
10
14
  @options = options
11
- @redis = options[:redis] || $redis || Redis::Objects.redis
12
- @options[:start] ||= 0
13
- @options[:type] ||= @options[:start] == 0 ? :increment : :decrement
14
- @redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
15
15
  end
16
-
16
+
17
+ # Works like push. Can chain together: list << 'a' << 'b'
17
18
  def <<(value)
18
19
  push(value)
20
+ self # for << 'a' << 'b'
19
21
  end
20
-
22
+
23
+ # Add a member to the end of the list. Redis: RPUSH
21
24
  def push(value)
22
25
  redis.rpush(key, value)
23
- @values << value
24
26
  end
25
27
 
28
+ # Remove a member from the end of the list. Redis: RPOP
26
29
  def pop
27
30
  redis.rpop(key)
28
- @values.pop
29
31
  end
30
32
 
33
+ # Add a member to the start of the list. Redis: LPUSH
31
34
  def unshift(value)
32
35
  redis.lpush(key, value)
33
- @values.unshift value
34
36
  end
35
37
 
38
+ # Remove a member from the start of the list. Redis: LPOP
36
39
  def shift
37
40
  redis.lpop(key)
38
41
  end
39
42
 
43
+ # Return all values in the list. Redis: LRANGE(0,-1)
40
44
  def values
41
- @values ||= get
45
+ range(0, -1)
42
46
  end
47
+ alias_method :get, :values
43
48
 
44
- def value=(val)
45
- redis.set(key, val)
46
- @values = val
47
- end
48
-
49
- def get
50
- @values = range(0, -1)
51
- end
52
-
53
- def [](index)
54
- case index
55
- when Range
49
+ # Same functionality as Ruby arrays. If a single number is given, return
50
+ # just the element at that index using Redis: LINDEX. Otherwise, return
51
+ # a range of values using Redis: LRANGE.
52
+ def [](index, length=nil)
53
+ if index.is_a? Range
56
54
  range(index.first, index.last)
55
+ elsif length
56
+ range(index, length)
57
57
  else
58
- range(index, index)
58
+ at(index)
59
59
  end
60
60
  end
61
61
 
62
62
  def delete(name, count=0)
63
- redis.lrem(name, count)
63
+ redis.lrem(key, count, name) # weird api
64
+ get
65
+ end
66
+
67
+ def each(&block)
68
+ values.each(&block)
64
69
  end
65
70
 
66
71
  def range(start_index, end_index)
67
72
  redis.lrange(key, start_index, end_index)
68
73
  end
69
-
74
+
75
+ # Return the value at the given index.
70
76
  def at(index)
71
- redis.lrange(key, index, index)
77
+ redis.lindex(key, index)
72
78
  end
73
79
 
74
80
  def last
@@ -80,16 +86,20 @@ class Redis
80
86
  end
81
87
 
82
88
  def length
83
- redis.length
89
+ redis.llen(key)
84
90
  end
85
91
  alias_method :size, :length
86
92
 
87
93
  def empty?
88
- values.empty?
94
+ length == 0
89
95
  end
90
-
96
+
91
97
  def ==(x)
92
98
  values == x
93
99
  end
100
+
101
+ def to_s
102
+ values.join(', ')
103
+ end
94
104
  end
95
105
  end
@@ -10,11 +10,11 @@ class Redis
10
10
  class LockTimeout < StandardError; end #:nodoc:
11
11
 
12
12
  attr_reader :key, :options, :redis
13
- def initialize(key, options={})
13
+ def initialize(key, redis=$redis, options={})
14
14
  @key = key
15
+ @redis = redis
15
16
  @options = options
16
17
  @options[:timeout] ||= 5
17
- @redis = options[:redis] || $redis || Redis::Objects.redis
18
18
  @redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
19
19
  end
20
20
 
@@ -24,7 +24,7 @@ class Redis
24
24
  @counters[name] = options
25
25
  class_eval <<-EndMethods
26
26
  def #{name}
27
- @#{name} ||= Redis::Counter.new(field_key(:#{name}), self.class.counters[:#{name}].merge(:redis => redis))
27
+ @#{name} ||= Redis::Counter.new(field_key(:#{name}), redis, self.class.counters[:#{name}])
28
28
  end
29
29
  EndMethods
30
30
  end
@@ -20,7 +20,7 @@ class Redis
20
20
  @lists[name] = options
21
21
  class_eval <<-EndMethods
22
22
  def #{name}
23
- @#{name} ||= Redis::List.new(field_key(:#{name}), self.class.lists[:#{name}].merge(:redis => redis))
23
+ @#{name} ||= Redis::List.new(field_key(:#{name}), redis, self.class.lists[:#{name}])
24
24
  end
25
25
  EndMethods
26
26
  end
@@ -22,7 +22,7 @@ class Redis
22
22
  @locks[name] = options
23
23
  class_eval <<-EndMethods
24
24
  def #{name}_lock(&block)
25
- @#{name}_lock ||= Redis::Lock.new(field_key(:#{name}_lock), self.class.locks[:#{name}].merge(:redis => redis))
25
+ @#{name}_lock ||= Redis::Lock.new(field_key(:#{name}_lock), redis, self.class.locks[:#{name}])
26
26
  end
27
27
  EndMethods
28
28
  end
@@ -4,6 +4,31 @@ require 'redis/set'
4
4
  class Redis
5
5
  module Objects
6
6
  module Sets
7
+ def self.included(klass)
8
+ klass.instance_variable_set('@sets', {})
9
+ klass.send :include, InstanceMethods
10
+ klass.extend ClassMethods
11
+ end
12
+
13
+ # Class methods that appear in your class when you include Redis::Objects.
14
+ module ClassMethods
15
+ attr_reader :sets
16
+
17
+ # Define a new list. It will function like a regular instance
18
+ # method, so it can be used alongside ActiveRecord, DataMapper, etc.
19
+ def set(name, options={})
20
+ @sets[name] = options
21
+ class_eval <<-EndMethods
22
+ def #{name}
23
+ @#{name} ||= Redis::Set.new(field_key(:#{name}), redis, self.class.sets[:#{name}])
24
+ end
25
+ EndMethods
26
+ end
27
+ end
28
+
29
+ # Instance methods that appear in your class when you include Redis::Objects.
30
+ module InstanceMethods
31
+ end
7
32
  end
8
33
  end
9
34
  end
@@ -20,7 +20,7 @@ class Redis
20
20
  @values[name] = options
21
21
  class_eval <<-EndMethods
22
22
  def #{name}
23
- @#{name} ||= Redis::Value.new(field_key(:#{name}), self.class.values[:#{name}].merge(:redis => redis))
23
+ @#{name} ||= Redis::Value.new(field_key(:#{name}), redis, self.class.values[:#{name}])
24
24
  end
25
25
  def #{name}=(value)
26
26
  #{name}.value = value
@@ -3,5 +3,73 @@ class Redis
3
3
  # Class representing a set.
4
4
  #
5
5
  class Set
6
+ require 'enumerator'
7
+ include Enumerable
8
+
9
+ attr_reader :key, :options, :redis
10
+ def initialize(key, redis=$redis, options={})
11
+ @key = key
12
+ @redis = redis
13
+ @options = options
14
+ end
15
+
16
+ # Works like add. Can chain together: list << 'a' << 'b'
17
+ def <<(value)
18
+ add(value)
19
+ self # for << 'a' << 'b'
20
+ end
21
+
22
+ # Add the specified value to the set only if it does not exist already.
23
+ # Redis: SADD
24
+ def add(value)
25
+ redis.sadd(key, value)
26
+ end
27
+
28
+ # Return all members in the set. Redis: SMEMBERS
29
+ def members
30
+ redis.smembers(key)
31
+ end
32
+ alias_method :get, :members
33
+
34
+ # Returns true if the specified value is in the set. Redis: SISMEMBER
35
+ def member?(value)
36
+ redis.sismember(key, value)
37
+ end
38
+
39
+ # Delete the value from the set. Redis: SREM
40
+ def delete(name)
41
+ redis.srem(key, name)
42
+ get
43
+ end
44
+
45
+ # Wipe the set entirely. Redis: DEL
46
+ def clear
47
+ redis.del(key)
48
+ end
49
+
50
+ # Iterate through each member of the set. Redis::Objects mixes in Enumerable,
51
+ # so you can also use familiar methods like +collect+, +detect+, and so forth.
52
+ def each(&block)
53
+ members.each(&block)
54
+ end
55
+
56
+ # The number of members in the set. Aliased as size. Redis: SCARD
57
+ def length
58
+ redis.scard(key)
59
+ end
60
+ alias_method :size, :length
61
+
62
+ # Returns true if the set has no members. Redis: SCARD == 0
63
+ def empty?
64
+ length == 0
65
+ end
66
+
67
+ def ==(x)
68
+ members == x
69
+ end
70
+
71
+ def to_s
72
+ members.join(', ')
73
+ end
6
74
  end
7
75
  end
@@ -4,10 +4,10 @@ class Redis
4
4
  #
5
5
  class Value
6
6
  attr_reader :key, :options, :redis
7
- def initialize(key, options={})
7
+ def initialize(key, redis=$redis, options={})
8
8
  @key = key
9
+ @redis = redis
9
10
  @options = options
10
- @redis = options[:redis] || $redis || Redis::Objects.redis
11
11
  @redis.setnx(key, @options[:default]) if @options[:default]
12
12
  end
13
13
 
@@ -10,6 +10,7 @@ class Roster
10
10
  lock :resort, :timeout => 2
11
11
  value :starting_pitcher
12
12
  list :player_stats
13
+ set :outfielders
13
14
 
14
15
  def initialize(id=1) @id = id end
15
16
  def id; @id; end
@@ -29,6 +30,7 @@ describe Redis::Objects do
29
30
  @roster.resort_lock.clear
30
31
  @roster.starting_pitcher.delete
31
32
  @roster.player_stats.clear
33
+ @roster.outfielders.clear
32
34
  end
33
35
 
34
36
  it "should provide a connection method" do
@@ -254,6 +256,104 @@ describe Redis::Objects do
254
256
  @roster.player_stats.should be_empty
255
257
  @roster.player_stats << 'a'
256
258
  @roster.player_stats.should == ['a']
259
+ @roster.player_stats.get.should == ['a']
260
+ @roster.player_stats.unshift 'b'
261
+ @roster.player_stats.to_s.should == 'b, a'
262
+ @roster.player_stats.should == ['b','a']
263
+ @roster.player_stats.get.should == ['b','a']
264
+ @roster.player_stats.push 'c'
265
+ @roster.player_stats.should == ['b','a','c']
266
+ @roster.player_stats.get.should == ['b','a','c']
267
+ @roster.player_stats << 'd'
268
+ @roster.player_stats.should == ['b','a','c','d']
269
+ @roster.player_stats[1].should == 'a'
270
+ @roster.player_stats[0].should == 'b'
271
+ @roster.player_stats[2].should == 'c'
272
+ @roster.player_stats[3].should == 'd'
273
+ @roster.player_stats.pop
274
+ @roster.player_stats[0].should == @roster.player_stats.at(0)
275
+ @roster.player_stats[1].should == @roster.player_stats.at(1)
276
+ @roster.player_stats[2].should == @roster.player_stats.at(2)
277
+ @roster.player_stats.should == ['b','a','c']
278
+ @roster.player_stats.get.should == ['b','a','c']
279
+ @roster.player_stats.shift
280
+ @roster.player_stats.should == ['a','c']
281
+ @roster.player_stats.get.should == ['a','c']
282
+ @roster.player_stats << 'e' << 'f' << 'e'
283
+ @roster.player_stats.should == ['a','c','e','f','e']
284
+ @roster.player_stats.get.should == ['a','c','e','f','e']
285
+ @roster.player_stats.delete('e')
286
+ @roster.player_stats.should == ['a','c','f']
287
+ @roster.player_stats.get.should == ['a','c','f']
288
+ @roster.player_stats << 'j'
289
+ @roster.player_stats.should == ['a','c','f','j']
290
+ @roster.player_stats[0..2].should == ['a','c','f']
291
+ @roster.player_stats[1, 3].should == ['c','f','j']
292
+ @roster.player_stats.length.should == 4
293
+ @roster.player_stats.size.should == 4
294
+ @roster.player_stats.should == ['a','c','f','j']
295
+ @roster.player_stats.get.should == ['a','c','f','j']
296
+
297
+ i = -1
298
+ @roster.player_stats.each do |st|
299
+ st.should == @roster.player_stats[i += 1]
300
+ end
301
+ @roster.player_stats.should == ['a','c','f','j']
302
+ @roster.player_stats.get.should == ['a','c','f','j']
303
+
304
+ @roster.player_stats.each_with_index do |st,i|
305
+ st.should == @roster.player_stats[i]
306
+ end
307
+ @roster.player_stats.should == ['a','c','f','j']
308
+ @roster.player_stats.get.should == ['a','c','f','j']
309
+
310
+ coll = @roster.player_stats.collect{|st| st}
311
+ coll.should == ['a','c','f','j']
312
+ @roster.player_stats.should == ['a','c','f','j']
313
+ @roster.player_stats.get.should == ['a','c','f','j']
314
+
315
+ @roster.player_stats << 'a'
316
+ coll = @roster.player_stats.select{|st| st == 'a'}
317
+ coll.should == ['a','a']
318
+ @roster.player_stats.should == ['a','c','f','j','a']
319
+ @roster.player_stats.get.should == ['a','c','f','j','a']
320
+ end
321
+
322
+ it "should handle sets of simple values" do
323
+ @roster.outfielders.should be_empty
324
+ @roster.outfielders << 'a' << 'a' << 'a'
325
+ @roster.outfielders.should == ['a']
326
+ @roster.outfielders.get.should == ['a']
327
+ @roster.outfielders << 'b' << 'b'
328
+ @roster.outfielders.to_s.should == 'a, b'
329
+ @roster.outfielders.should == ['a','b']
330
+ @roster.outfielders.members.should == ['a','b']
331
+ @roster.outfielders.get.should == ['a','b']
332
+ @roster.outfielders << 'c'
333
+ @roster.outfielders.sort.should == ['a','b','c']
334
+ @roster.outfielders.get.sort.should == ['a','b','c']
335
+ @roster.outfielders.delete('c')
336
+ @roster.outfielders.should == ['a','b']
337
+ @roster.outfielders.get.sort.should == ['a','b']
338
+ @roster.outfielders.length.should == 2
339
+ @roster.outfielders.size.should == 2
340
+
341
+ i = 0
342
+ @roster.outfielders.each do |st|
343
+ i += 1
344
+ end
345
+ i.should == @roster.outfielders.length
346
+
347
+ coll = @roster.outfielders.collect{|st| st}
348
+ coll.should == ['a','b']
349
+ @roster.outfielders.should == ['a','b']
350
+ @roster.outfielders.get.should == ['a','b']
351
+
352
+ @roster.outfielders << 'c'
353
+ @roster.outfielders.member? 'c'
354
+ coll = @roster.outfielders.select{|st| st == 'c'}
355
+ coll.should == ['c']
356
+ @roster.outfielders.sort.should == ['a','b','c']
257
357
  end
258
358
 
259
359
  it "should provide a lock method that accepts a block" do
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.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Wiger
@@ -9,11 +9,11 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-24 00:00:00 -08:00
12
+ date: 2009-11-26 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
16
- description: Redis::Objects is a lightweight library that lets you use Redis primitives as Ruby objects from any class or ORM
16
+ description: Map Redis types directly to Ruby objects. Works with any class or ORM.
17
17
  email: nate@wiger.org
18
18
  executables: []
19
19
 
@@ -68,6 +68,6 @@ rubyforge_project: redis-objects
68
68
  rubygems_version: 1.3.5
69
69
  signing_key:
70
70
  specification_version: 3
71
- summary: Lightweight map of Redis types to Ruby objects
71
+ summary: Maps Redis types to Ruby objects
72
72
  test_files: []
73
73