redis-objects-legacy 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,600 @@
1
+ Redis::Objects - Map Redis types directly to Ruby objects
2
+ =========================================================
3
+
4
+ [![Build Status](https://app.travis-ci.com/nateware/redis-objects.svg?branch=master)](https://travis-ci.com/nateware/redis-objects)
5
+ [![Code Coverage](https://codecov.io/gh/nateware/redis-objects/branch/master/graph/badge.svg)](https://codecov.io/gh/nateware/redis-objects)
6
+ [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MJF7JU5M7F8VL)
7
+
8
+ This is **not** an ORM. People that are wrapping ORM’s around Redis are missing the point.
9
+
10
+ The killer feature of Redis is that it allows you to perform _atomic_ operations
11
+ on _individual_ data structures, like counters, lists, and sets. The **atomic** part is HUGE.
12
+ Using an ORM wrapper that retrieves a "record", updates values, then sends those values back,
13
+ _removes_ the atomicity, and thus the major advantage of Redis. Just use MySQL, k?
14
+
15
+ This gem provides a Rubyish interface to Redis, by mapping [Redis data types](http://redis.io/commands)
16
+ to Ruby objects, via a thin layer over the `redis` gem. It offers several advantages
17
+ over the lower-level redis-rb API:
18
+
19
+ 1. Easy to integrate directly with existing ORMs - ActiveRecord, DataMapper, etc. Add counters to your model!
20
+ 2. Complex data structures are automatically Marshaled (if you set :marshal => true)
21
+ 3. Integers are returned as integers, rather than '17'
22
+ 4. Higher-level types are provided, such as Locks, that wrap multiple calls
23
+
24
+ This gem originally arose out of a need for high-concurrency atomic operations;
25
+ for a fun rant on the topic, see [An Atomic Rant](http://nateware.com/2010/02/18/an-atomic-rant),
26
+ or scroll down to [Atomic Counters and Locks](#atomicity) in this README.
27
+
28
+ There are two ways to use Redis::Objects, either as an include in a model class (to
29
+ tightly integrate with ORMs or other classes), or standalone by using classes such
30
+ as `Redis::List` and `Redis::SortedSet`.
31
+
32
+ Installation and Setup
33
+ ----------------------
34
+ Add it to your Gemfile as:
35
+
36
+ ~~~ruby
37
+ gem 'redis-objects'
38
+ ~~~
39
+
40
+ Redis::Objects needs a handle created by `Redis.new` or a [ConnectionPool](https://github.com/mperham/connection_pool):
41
+
42
+ The recommended approach is to use a `ConnectionPool` since this guarantees that most timeouts in the `redis` client
43
+ do not pollute your existing connection. However, you need to make sure that both `:timeout` and `:size` are set appropriately
44
+ in a multithreaded environment.
45
+ ~~~ruby
46
+ require 'connection_pool'
47
+ Redis::Objects.redis = ConnectionPool.new(size: 5, timeout: 5) { Redis.new(:host => '127.0.0.1', :port => 6379) }
48
+ ~~~
49
+
50
+ Redis::Objects can also default to `Redis.current` if `Redis::Objects.redis` is not set.
51
+ ~~~ruby
52
+ Redis.current = Redis.new(:host => '127.0.0.1', :port => 6379)
53
+ ~~~
54
+
55
+ (If you're on Rails, `config/initializers/redis.rb` is a good place for this.)
56
+ Remember you can use Redis::Objects in any Ruby code. There are **no** dependencies
57
+ on Rails. Standalone, Sinatra, Resque - no problem.
58
+
59
+ Alternatively, you can set the `redis` handle directly:
60
+
61
+ ~~~ruby
62
+ Redis::Objects.redis = Redis.new(...)
63
+ ~~~
64
+
65
+ Finally, you can even set different handles for different classes:
66
+
67
+ ~~~ruby
68
+ class User
69
+ include Redis::Objects
70
+ end
71
+ class Post
72
+ include Redis::Objects
73
+ end
74
+
75
+ # you can also use a ConnectionPool here as well
76
+ User.redis = Redis.new(:host => '1.2.3.4')
77
+ Post.redis = Redis.new(:host => '5.6.7.8')
78
+ ~~~
79
+
80
+ As of `0.7.0`, `redis-objects` now autoloads the appropriate `Redis::Whatever`
81
+ classes on demand. Previous strategies of individually requiring `redis/list`
82
+ or `redis/set` are no longer required.
83
+
84
+ Option 1: Model Class Include
85
+ =============================
86
+ Including Redis::Objects in a model class makes it trivial to integrate Redis types
87
+ with an existing ActiveRecord, DataMapper, Mongoid, or similar class. **Redis::Objects
88
+ will work with _any_ class that provides an `id` method that returns a unique value.**
89
+ Redis::Objects automatically creates keys that are unique to each object, in the format:
90
+
91
+ model_name:id:field_name
92
+
93
+ For illustration purposes, consider this stub class:
94
+
95
+ ~~~ruby
96
+ class User
97
+ include Redis::Objects
98
+ counter :my_posts
99
+ def id
100
+ 1
101
+ end
102
+ end
103
+
104
+ user = User.new
105
+ user.id # 1
106
+ user.my_posts.increment
107
+ user.my_posts.increment
108
+ user.my_posts.increment
109
+ puts user.my_posts.value # 3
110
+ user.my_posts.reset
111
+ puts user.my_posts.value # 0
112
+ user.my_posts.reset 5
113
+ puts user.my_posts.value # 5
114
+ ~~~
115
+
116
+ Here's an example that integrates several data types with an ActiveRecord model:
117
+
118
+ ~~~ruby
119
+ class Team < ActiveRecord::Base
120
+ include Redis::Objects
121
+
122
+ lock :trade_players, :expiration => 15 # sec
123
+ value :at_bat
124
+ counter :hits
125
+ counter :runs
126
+ counter :outs
127
+ counter :inning, :start => 1
128
+ list :on_base
129
+ list :coaches, :marshal => true
130
+ set :outfielders
131
+ hash_key :pitchers_faced # "hash" is taken by Ruby
132
+ sorted_set :rank, :global => true
133
+ end
134
+ ~~~
135
+
136
+ Familiar Ruby array operations Just Work (TM):
137
+
138
+ ~~~ruby
139
+ @team = Team.find_by_name('New York Yankees')
140
+ @team.on_base << 'player1'
141
+ @team.on_base << 'player2'
142
+ @team.on_base << 'player3'
143
+ @team.on_base # ['player1', 'player2', 'player3']
144
+ @team.on_base.pop
145
+ @team.on_base.shift
146
+ @team.on_base.length # 1
147
+ @team.on_base.delete('player2')
148
+ @team.on_base = ['player1', 'player2'] # ['player1', 'player2']
149
+ ~~~
150
+
151
+ Sets work too:
152
+
153
+ ~~~ruby
154
+ @team.outfielders << 'outfielder1'
155
+ @team.outfielders << 'outfielder2'
156
+ @team.outfielders << 'outfielder1' # dup ignored
157
+ @team.outfielders # ['outfielder1', 'outfielder2']
158
+ @team.outfielders.each do |player|
159
+ puts player
160
+ end
161
+ player = @team.outfielders.detect{|of| of == 'outfielder2'}
162
+ @team.outfielders = ['outfielder1', 'outfielder3'] # ['outfielder1', 'outfielder3']
163
+ ~~~
164
+
165
+ Hashes work too:
166
+
167
+ ~~~ruby
168
+ @team.pitchers_faced['player1'] = 'pitcher2'
169
+ @team.pitchers_faced['player2'] = 'pitcher1'
170
+ @team.pitchers_faced = { 'player1' => 'pitcher2', 'player2' => 'pitcher1' }
171
+ ~~~
172
+
173
+ And you can do unions and intersections between objects (kinda cool):
174
+
175
+ ~~~ruby
176
+ @team1.outfielders | @team2.outfielders # outfielders on both teams
177
+ @team1.outfielders & @team2.outfielders # in baseball, should be empty :-)
178
+ ~~~
179
+
180
+ Counters can be atomically incremented/decremented (but not assigned):
181
+
182
+ ~~~ruby
183
+ @team.hits.increment # or incr
184
+ @team.hits.decrement # or decr
185
+ @team.hits.incr(3) # add 3
186
+ @team.runs = 4 # exception
187
+ ~~~
188
+
189
+ Defining a different method as the `id` field is easy
190
+
191
+ ~~~ruby
192
+ class User
193
+ include Redis::Objects
194
+ redis_id_field :uid
195
+ counter :my_posts
196
+ end
197
+
198
+ user.uid # 195137a1bdea4473
199
+ user.my_posts.increment # 1
200
+ ~~~
201
+
202
+ Finally, for free, you get a `redis` method that points directly to a Redis connection:
203
+
204
+ ~~~ruby
205
+ Team.redis.get('somekey')
206
+ @team = Team.new
207
+ @team.redis.get('somekey')
208
+ @team.redis.smembers('someset')
209
+ ~~~
210
+
211
+ You can use the `redis` handle to directly call any [Redis API command](http://redis.io/commands).
212
+
213
+ Option 2: Standalone Usage
214
+ ===========================
215
+ There is a Ruby class that maps to each Redis type, with methods for each
216
+ [Redis API command](http://redis.io/commands).
217
+ Note that calling `new` does not imply it's actually a "new" value - it just
218
+ creates a mapping between that Ruby object and the corresponding Redis data
219
+ structure, which may already exist on the `redis-server`.
220
+
221
+ Counters
222
+ --------
223
+ The `counter_name` is the key stored in Redis.
224
+
225
+ ~~~ruby
226
+ @counter = Redis::Counter.new('counter_name')
227
+ @counter.increment # or incr
228
+ @counter.decrement # or decr
229
+ @counter.increment(3)
230
+ puts @counter.value
231
+ ~~~
232
+
233
+ This gem provides a clean way to do atomic blocks as well:
234
+
235
+ ~~~ruby
236
+ @counter.increment do |val|
237
+ raise "Full" if val > MAX_VAL # rewind counter
238
+ end
239
+ ~~~
240
+
241
+ See the section on [Atomic Counters and Locks](#atomicity) for cool uses of atomic counter blocks.
242
+
243
+ Locks
244
+ -----
245
+ A convenience class that wraps the pattern of [using setnx to perform locking](http://redis.io/commands/setnx).
246
+
247
+ ~~~ruby
248
+ @lock = Redis::Lock.new('serialize_stuff', :expiration => 15, :timeout => 0.1)
249
+ @lock.lock do
250
+ # do work
251
+ end
252
+ ~~~
253
+
254
+ This can be especially useful if you're running batch jobs spread across multiple hosts.
255
+
256
+ Values
257
+ ------
258
+ Simple values are easy as well:
259
+
260
+ ~~~ruby
261
+ @value = Redis::Value.new('value_name')
262
+ @value.value = 'a'
263
+ @value.delete
264
+ ~~~
265
+
266
+ Complex data is no problem with :marshal => true:
267
+
268
+ ~~~ruby
269
+ @account = Account.create!(params[:account])
270
+ @newest = Redis::Value.new('newest_account', :marshal => true)
271
+ @newest.value = @account.attributes
272
+ puts @newest.value['username']
273
+ ~~~
274
+
275
+ Compress data to save memory usage on Redis with :compress => true:
276
+
277
+ ~~~ruby
278
+ @account = Account.create!(params[:account])
279
+ @marshaled_value = Redis::Value.new('marshaled', :marshal => true, :compress => true)
280
+ @marshaled_value.value = @account.attributes
281
+ @unmarshaled_value = Redis::Value.new('unmarshaled', :compress => true)
282
+ @unmarshaled_value = 'Really Long String'
283
+ puts @marshaled_value.value['username']
284
+ puts @unmarshaled_value.value
285
+ ~~~
286
+
287
+ Lists
288
+ -----
289
+ Lists work just like Ruby arrays:
290
+
291
+ ~~~ruby
292
+ @list = Redis::List.new('list_name')
293
+ @list << 'a'
294
+ @list << 'b'
295
+ @list.include? 'c' # false
296
+ @list.values # ['a','b']
297
+ @list << 'c'
298
+ @list.delete('c')
299
+ @list[0]
300
+ @list[0,1]
301
+ @list[0..1]
302
+ @list.shift
303
+ @list.pop
304
+ @list.clear
305
+ # etc
306
+ ~~~
307
+
308
+ You can bound the size of the list to only hold N elements like so:
309
+
310
+ ~~~ruby
311
+ # Only holds 10 elements, throws out old ones when you reach :maxlength.
312
+ @list = Redis::List.new('list_name', :maxlength => 10)
313
+ ~~~
314
+
315
+ Complex data types are serialized with :marshal => true:
316
+
317
+ ~~~ruby
318
+ @list = Redis::List.new('list_name', :marshal => true)
319
+ @list << {:name => "Nate", :city => "San Diego"}
320
+ @list << {:name => "Peter", :city => "Oceanside"}
321
+ @list.each do |el|
322
+ puts "#{el[:name]} lives in #{el[:city]}"
323
+ end
324
+ ~~~
325
+
326
+ Note: If you run into issues, with Marshal errors, refer to the fix in [Issue #176](https://github.com/nateware/redis-objects/issues/176).
327
+
328
+ Hashes
329
+ ------
330
+ Hashes work like a Ruby [Hash](http://ruby-doc.org/core/classes/Hash.html), with
331
+ a few Redis-specific additions. (The class name is "HashKey" not just "Hash", due to
332
+ conflicts with the Ruby core Hash class in other gems.)
333
+
334
+ ~~~ruby
335
+ @hash = Redis::HashKey.new('hash_name')
336
+ @hash['a'] = 1
337
+ @hash['b'] = 2
338
+ @hash.each do |k,v|
339
+ puts "#{k} = #{v}"
340
+ end
341
+ @hash['c'] = 3
342
+ puts @hash.all # {"a"=>"1","b"=>"2","c"=>"3"}
343
+ @hash.clear
344
+ ~~~
345
+
346
+ Redis also adds incrementing and bulk operations:
347
+
348
+ ~~~ruby
349
+ @hash.incr('c', 6) # 9
350
+ @hash.bulk_set('d' => 5, 'e' => 6)
351
+ @hash.bulk_get('d','e') # "5", "6"
352
+ ~~~
353
+
354
+ Remember that numbers become strings in Redis. Unlike with other Redis data types,
355
+ `redis-objects` can't guess at your data type in this situation, since you may
356
+ actually mean to store "1.5".
357
+
358
+ Sets
359
+ ----
360
+ Sets work like the Ruby [Set](http://ruby-doc.org/core/classes/Set.html) class.
361
+ They are unordered, but guarantee uniqueness of members.
362
+
363
+ ~~~ruby
364
+ @set = Redis::Set.new('set_name')
365
+ @set << 'a'
366
+ @set << 'b'
367
+ @set << 'a' # dup ignored
368
+ @set.member? 'c' # false
369
+ @set.members # ['a','b']
370
+ @set.members.reverse # ['b','a']
371
+ @set.each do |member|
372
+ puts member
373
+ end
374
+ @set.clear
375
+ # etc
376
+ ~~~
377
+
378
+ You can perform Redis intersections/unions/diffs easily:
379
+
380
+ ~~~ruby
381
+ @set1 = Redis::Set.new('set1')
382
+ @set2 = Redis::Set.new('set2')
383
+ @set3 = Redis::Set.new('set3')
384
+ members = @set1 & @set2 # intersection
385
+ members = @set1 | @set2 # union
386
+ members = @set1 + @set2 # union
387
+ members = @set1 ^ @set2 # difference
388
+ members = @set1 - @set2 # difference
389
+ members = @set1.intersection(@set2, @set3) # multiple
390
+ members = @set1.union(@set2, @set3) # multiple
391
+ members = @set1.difference(@set2, @set3) # multiple
392
+ ~~~
393
+
394
+ Or store them in Redis:
395
+
396
+ ~~~ruby
397
+ @set1.interstore('intername', @set2, @set3)
398
+ members = @set1.redis.get('intername')
399
+ @set1.unionstore('unionname', @set2, @set3)
400
+ members = @set1.redis.get('unionname')
401
+ @set1.diffstore('diffname', @set2, @set3)
402
+ members = @set1.redis.get('diffname')
403
+ ~~~
404
+
405
+ And use complex data types too, with :marshal => true:
406
+
407
+ ~~~ruby
408
+ @set1 = Redis::Set.new('set1', :marshal => true)
409
+ @set2 = Redis::Set.new('set2', :marshal => true)
410
+ @set1 << {:name => "Nate", :city => "San Diego"}
411
+ @set1 << {:name => "Peter", :city => "Oceanside"}
412
+ @set2 << {:name => "Nate", :city => "San Diego"}
413
+ @set2 << {:name => "Jeff", :city => "Del Mar"}
414
+
415
+ @set1 & @set2 # Nate
416
+ @set1 - @set2 # Peter
417
+ @set1 | @set2 # all 3 people
418
+ ~~~
419
+
420
+ Sorted Sets
421
+ -----------
422
+ Due to their unique properties, Sorted Sets work like a hybrid between
423
+ a Hash and an Array. You assign like a Hash, but retrieve like an Array:
424
+
425
+ ~~~ruby
426
+ @sorted_set = Redis::SortedSet.new('number_of_posts')
427
+ @sorted_set['Nate'] = 15
428
+ @sorted_set['Peter'] = 75
429
+ @sorted_set['Jeff'] = 24
430
+
431
+ # Array access to get sorted order
432
+ @sorted_set[0..2] # => ["Nate", "Jeff", "Peter"]
433
+ @sorted_set[0,2] # => ["Nate", "Jeff"]
434
+
435
+ @sorted_set['Peter'] # => 75
436
+ @sorted_set['Jeff'] # => 24
437
+ @sorted_set.score('Jeff') # same thing (24)
438
+
439
+ @sorted_set.rank('Peter') # => 2
440
+ @sorted_set.rank('Jeff') # => 1
441
+
442
+ @sorted_set.first # => "Nate"
443
+ @sorted_set.last # => "Peter"
444
+ @sorted_set.revrange(0,2) # => ["Peter", "Jeff", "Nate"]
445
+
446
+ @sorted_set['Newbie'] = 1
447
+ @sorted_set.members # => ["Newbie", "Nate", "Jeff", "Peter"]
448
+ @sorted_set.members.reverse # => ["Peter", "Jeff", "Nate", "Newbie"]
449
+
450
+ @sorted_set.rangebyscore(10, 100, :limit => 2) # => ["Nate", "Jeff"]
451
+ @sorted_set.members(:with_scores => true) # => [["Newbie", 1], ["Nate", 16], ["Jeff", 28], ["Peter", 76]]
452
+
453
+ # atomic increment
454
+ @sorted_set.increment('Nate')
455
+ @sorted_set.incr('Peter') # shorthand
456
+ @sorted_set.incr('Jeff', 4)
457
+ ~~~
458
+
459
+ The other Redis Sorted Set commands are supported as well; see [Sorted Sets API](http://redis.io/commands#sorted_set).
460
+
461
+ <a name="atomicity"></a>
462
+ Atomic Counters and Locks
463
+ -------------------------
464
+ You are probably not handling atomicity correctly in your app. For a fun rant
465
+ on the topic, see [An Atomic Rant](http://nateware.com/an-atomic-rant.html).
466
+
467
+ Atomic counters are a good way to handle concurrency:
468
+
469
+ ~~~ruby
470
+ @team = Team.find(1)
471
+ if @team.drafted_players.increment <= @team.max_players
472
+ # do stuff
473
+ @team.team_players.create!(:player_id => 221)
474
+ @team.active_players.increment
475
+ else
476
+ # reset counter state
477
+ @team.drafted_players.decrement
478
+ end
479
+ ~~~
480
+
481
+ An _atomic block_ gives you a cleaner way to do the above. Exceptions or returning nil
482
+ will rewind the counter back to its previous state:
483
+
484
+ ~~~ruby
485
+ @team.drafted_players.increment do |val|
486
+ raise Team::TeamFullError if val > @team.max_players # rewind
487
+ @team.team_players.create!(:player_id => 221)
488
+ @team.active_players.increment
489
+ end
490
+ ~~~
491
+
492
+ Here's a similar approach, using an if block (failure rewinds counter):
493
+
494
+ ~~~ruby
495
+ @team.drafted_players.increment do |val|
496
+ if val <= @team.max_players
497
+ @team.team_players.create!(:player_id => 221)
498
+ @team.active_players.increment
499
+ end
500
+ end
501
+ ~~~
502
+
503
+ Class methods work too, using the familiar ActiveRecord counter syntax:
504
+
505
+ ~~~ruby
506
+ Team.increment_counter :drafted_players, team_id
507
+ Team.decrement_counter :drafted_players, team_id, 2
508
+ Team.increment_counter :total_online_players # no ID on global counter
509
+ ~~~
510
+
511
+ Class-level atomic blocks can also be used. This may save a DB fetch, if you have
512
+ a record ID and don't need any other attributes from the DB table:
513
+
514
+ ~~~ruby
515
+ Team.increment_counter(:drafted_players, team_id) do |val|
516
+ TeamPitcher.create!(:team_id => team_id, :pitcher_id => 181)
517
+ Team.increment_counter(:active_players, team_id)
518
+ end
519
+ ~~~
520
+
521
+ ### Locks ###
522
+
523
+ Locks work similarly. On completion or exception the lock is released:
524
+
525
+ ~~~ruby
526
+ class Team < ActiveRecord::Base
527
+ lock :reorder # declare a lock
528
+ end
529
+
530
+ @team.reorder_lock.lock do
531
+ @team.reorder_all_players
532
+ end
533
+ ~~~
534
+
535
+ Class-level lock (same concept)
536
+
537
+ ~~~ruby
538
+ Team.obtain_lock(:reorder, team_id) do
539
+ Team.reorder_all_players(team_id)
540
+ end
541
+ ~~~
542
+
543
+ Lock expiration. Sometimes you want to make sure your locks are cleaned up should
544
+ the unthinkable happen (server failure). You can set lock expirations to handle
545
+ this. Expired locks are released by the next process to attempt lock. Just
546
+ make sure you expiration value is sufficiently large compared to your expected
547
+ lock time.
548
+
549
+ ~~~ruby
550
+ class Team < ActiveRecord::Base
551
+ lock :reorder, :expiration => 15.minutes
552
+ end
553
+ ~~~
554
+
555
+ Keep in mind that true locks serialize your entire application at that point. As
556
+ such, atomic counters are strongly preferred.
557
+
558
+ ### Expiration ###
559
+
560
+ Use :expiration and :expireat options to set default expiration.
561
+
562
+ ~~~ruby
563
+ value :value_with_expiration, :expiration => 1.hour
564
+ value :value_with_expireat, :expireat => lambda { Time.now + 1.hour }
565
+ ~~~
566
+
567
+ :warning: In the above example, `expiration` is evaluated at class load time.
568
+ In this example, it will be one hour after loading the class, not after one hour
569
+ after setting a value. If you want to expire one hour after setting the value,
570
+ please use `:expireat` with `lambda`.
571
+
572
+ Custom serialization
573
+ --------------------
574
+ You can customize how values are serialized by setting `serializer: CustomSerializer`.
575
+ The default is `Marshal` from the standard lib, but it can be anything that responds to `dump` and
576
+ `load`. `JSON` and `YAML` are popular options.
577
+
578
+ If you need to pass extra arguments to `dump` or `load`, you can set
579
+ `marshal_dump_args: { foo: 'bar' }` and `marshal_load_args: { foo: 'bar' }` respectively.
580
+
581
+ ~~~ruby
582
+ class CustomSerializer
583
+ def self.dump(value)
584
+ # custom code for serializing
585
+ end
586
+
587
+ def self.load(value)
588
+ # custom code for deserializing
589
+ end
590
+ end
591
+
592
+ @account = Account.create!(params[:account])
593
+ @newest = Redis::Value.new('custom_serializer', marshal: true, serializer: CustomSerializer)
594
+ @newest.value = @account.attributes
595
+ ~~~
596
+
597
+ Author
598
+ =======
599
+ Copyright (c) 2009-2019 [Nate Wiger](http://nateware.com). All Rights Reserved.
600
+ Released under the [Artistic License](http://www.opensource.org/licenses/artistic-license-2.0.php).
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ desc "run all the specs"
4
+ task :test do
5
+ sh "bacon spec/*_spec.rb"
6
+ end
7
+ task :default => :test
8
+ task :spec => :test
9
+
10
+ desc "show changelog"
11
+ task :changelog do
12
+ latest = `git tag |tail -1`.chomp
13
+ sh "git log --pretty=format:'* %s %b [%an]' #{latest}..HEAD"
14
+ end
@@ -0,0 +1,62 @@
1
+ require 'redis/helpers/core_commands'
2
+
3
+ class Redis
4
+ # Defines base functionality for all redis-objects.
5
+ class BaseObject
6
+ include Redis::Helpers::CoreCommands
7
+
8
+ attr_reader :key, :options
9
+
10
+ def initialize(key, *args)
11
+ @key = key.is_a?(Array) ? key.flatten.join(':') : key
12
+ @options = args.last.is_a?(Hash) ? args.pop : {}
13
+ @myredis = Objects::ConnectionPoolProxy.proxy_if_needed(args.first)
14
+ end
15
+
16
+ # Dynamically query the handle to enable resetting midstream
17
+ def redis
18
+ @myredis || ::Redis::Objects.redis
19
+ end
20
+
21
+ alias :inspect :to_s # Ruby 1.9.2
22
+
23
+ def set_expiration
24
+ if !@options[:expiration].nil?
25
+ redis.expire(@key, @options[:expiration]) if redis.ttl(@key) < 0
26
+ elsif !@options[:expireat].nil?
27
+ expireat = @options[:expireat]
28
+ at = expireat.respond_to?(:call) ? expireat.call.to_i : expireat.to_i
29
+ redis.expireat(@key, at) if redis.ttl(@key) < 0
30
+ end
31
+ end
32
+
33
+ def allow_expiration(&block)
34
+ result = block.call
35
+ set_expiration
36
+ result
37
+ end
38
+
39
+ def to_json(*args)
40
+ to_hash.to_json(*args)
41
+ rescue NoMethodError => e
42
+ raise e.class, "The current runtime does not provide a `to_json` implementation. Require 'json' or another JSON library and try again."
43
+ end
44
+
45
+ def as_json(*)
46
+ to_hash
47
+ end
48
+
49
+ def to_hash
50
+ { "key" => @key, "options" => @options, "value" => value }
51
+ end
52
+
53
+ # Math ops - delegate to value method
54
+ %w(== < > <= >=).each do |m|
55
+ class_eval <<-EndOverload
56
+ def #{m}(what)
57
+ value #{m} what
58
+ end
59
+ EndOverload
60
+ end
61
+ end
62
+ end