ohm 0.0.32 → 0.0.33

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ohm.rb CHANGED
@@ -4,16 +4,22 @@ require "base64"
4
4
  require File.join(File.dirname(__FILE__), "ohm", "redis")
5
5
  require File.join(File.dirname(__FILE__), "ohm", "validations")
6
6
  require File.join(File.dirname(__FILE__), "ohm", "compat-1.8.6")
7
+ require File.join(File.dirname(__FILE__), "ohm", "key")
8
+ require File.join(File.dirname(__FILE__), "ohm", "collection")
7
9
 
8
10
  module Ohm
9
11
 
10
12
  # Provides access to the Redis database. This is shared accross all models and instances.
11
13
  def redis
12
- Thread.current[:redis] ||= Ohm::Redis.new(*options)
14
+ threaded[:redis] ||= connection(*options)
13
15
  end
14
16
 
15
17
  def redis=(connection)
16
- Thread.current[:redis] = connection
18
+ threaded[:redis] = connection
19
+ end
20
+
21
+ def threaded
22
+ Thread.current[:ohm] ||= {}
17
23
  end
18
24
 
19
25
  # Connect to a redis database.
@@ -30,8 +36,15 @@ module Ohm
30
36
  @options = options
31
37
  end
32
38
 
39
+ # Return a connection to Redis.
40
+ #
41
+ # This is a wapper around Ohm::Redis.new(options)
42
+ def connection(*options)
43
+ Ohm::Redis.new(*options)
44
+ end
45
+
33
46
  def options
34
- @options
47
+ @options || []
35
48
  end
36
49
 
37
50
  # Clear the database.
@@ -41,54 +54,55 @@ module Ohm
41
54
 
42
55
  # Join the parameters with ":" to create a key.
43
56
  def key(*args)
44
- args.join(":")
57
+ Key[*args]
45
58
  end
46
59
 
47
- module_function :key, :connect, :flush, :redis, :redis=, :options
60
+ module_function :key, :connect, :connection, :flush, :redis, :redis=, :options, :threaded
48
61
 
49
- module Attributes
62
+ Error = Class.new(StandardError)
63
+
64
+ class Model
50
65
  class Collection
51
66
  include Enumerable
52
67
 
53
- attr_accessor :key, :db, :model
68
+ attr :raw
69
+ attr :model
70
+
71
+ def initialize(key, model, db = model.db)
72
+ @raw = self.class::Raw.new(key, db)
73
+ @model = model
74
+ end
54
75
 
55
- def initialize(db, key, model = nil)
56
- self.db = db
57
- self.key = key
58
- self.model = model
76
+ def <<(model)
77
+ raw << model.id
59
78
  end
60
79
 
80
+ alias add <<
81
+
61
82
  def each(&block)
62
- all.each(&block)
83
+ raw.each do |id|
84
+ block.call(model[id])
85
+ end
63
86
  end
64
87
 
65
- # Return instances of model for all the ids contained in the collection.
66
- def all
67
- instantiate(raw)
88
+ def key
89
+ raw.key
68
90
  end
69
91
 
70
- # Return the values as model instances, ordered by the options supplied.
71
- # Check redis documentation to see what values you can provide to each option.
72
- #
73
- # @param options [Hash] options to sort the collection.
74
- # @option options [#to_s] :by Model attribute to sort the instances by.
75
- # @option options [#to_s] :order (ASC) Sorting order, which can be ASC or DESC.
76
- # @option options [Integer] :limit (all) Number of items to return.
77
- # @option options [Integer] :start (0) An offset from where the limit will be applied.
78
- #
79
- # @example Get the first ten users sorted alphabetically by name:
80
- #
81
- # @event.attendees.sort(:by => :name, :order => "ALPHA", :limit => 10)
82
- #
83
- # @example Get five posts sorted by number of votes and starting from the number 5 (zero based):
84
- #
85
- # @blog.posts.sort(:by => :votes, :start => 5, :limit => 10")
86
- def sort(options = {})
87
- return [] if empty?
88
- options[:start] ||= 0
89
- options[:limit] = [options[:start], options[:limit]] if options[:limit]
90
- result = db.sort(key, options)
91
- options[:get] ? result : instantiate(result)
92
+ def first(options = {})
93
+ if options[:by]
94
+ sort_by(options.delete(:by), options.merge(:limit => 1)).first
95
+ else
96
+ model[raw.first(options)]
97
+ end
98
+ end
99
+
100
+ def [](index)
101
+ model[raw[index]]
102
+ end
103
+
104
+ def sort(*args)
105
+ raw.sort(*args).map(&model)
92
106
  end
93
107
 
94
108
  # Sort the model instances by the given attribute.
@@ -102,163 +116,58 @@ module Ohm
102
116
  # user.name == "A"
103
117
  # # => true
104
118
  def sort_by(att, options = {})
105
- sort(options.merge(:by => model.key("*", att)))
106
- end
107
-
108
- # Sort the model instances by id and return the first instance
109
- # found. If a :by option is provided with a valid attribute name, the
110
- # method sort_by is used instead and the option provided is passed as the
111
- # first parameter.
112
- #
113
- # @see #sort
114
- # @see #sort_by
115
- # @return [Ohm::Model, nil] Returns the first instance found or nil.
116
- def first(options = {})
117
- options = options.merge(:limit => 1)
118
- options[:by] ?
119
- sort_by(options.delete(:by), options).first :
120
- sort(options).first
121
- end
119
+ options.merge!(:by => model.key("*", att))
122
120
 
123
- def to_ary
124
- all
121
+ if options[:get]
122
+ raw.sort(options.merge(:get => model.key("*", options[:get])))
123
+ else
124
+ sort(options)
125
+ end
125
126
  end
126
127
 
127
- def ==(other)
128
- to_ary == other
128
+ def delete(model)
129
+ raw.delete(model.id)
130
+ model
129
131
  end
130
132
 
131
- # @return [true, false] Returns whether or not the collection is empty.
132
- def empty?
133
- size.zero?
134
- end
135
-
136
- # Clears the values in the collection.
137
133
  def clear
138
- db.del(key)
139
- self
134
+ raw.clear
140
135
  end
141
136
 
142
- # Appends the given values to the collection.
143
- def concat(values)
144
- values.each { |value| self << value }
137
+ def concat(models)
138
+ raw.concat(models.map { |model| model.id })
145
139
  self
146
140
  end
147
141
 
148
- # Replaces the collection with the passed values.
149
- def replace(values)
150
- clear
151
- concat(values)
152
- end
153
-
154
- # @param value [Ohm::Model#id] Adds the id of the object if it's an Ohm::Model.
155
- def add(model)
156
- self << model.id
157
- end
158
-
159
- private
160
-
161
- def instantiate(raw)
162
- model ? raw.collect { |id| model[id] } : raw
163
- end
164
- end
165
-
166
- # Represents a Redis list.
167
- #
168
- # @example Use a list attribute.
169
- #
170
- # class Event < Ohm::Model
171
- # attribute :name
172
- # list :participants
173
- # end
174
- #
175
- # event = Event.create :name => "Redis Meeting"
176
- # event.participants << "Albert"
177
- # event.participants << "Benoit"
178
- # event.participants.all #=> ["Albert", "Benoit"]
179
- class List < Collection
180
-
181
- # @param value [#to_s] Pushes value to the tail of the list.
182
- def << value
183
- db.rpush(key, value)
184
- end
185
-
186
- alias push <<
187
-
188
- # @return [String] Return and remove the last element of the list.
189
- def pop
190
- db.rpop(key)
191
- end
192
-
193
- # @return [String] Return and remove the first element of the list.
194
- def shift
195
- db.lpop(key)
142
+ def replace(models)
143
+ raw.replace(models.map { |model| model.id })
144
+ self
196
145
  end
197
146
 
198
- # @param value [#to_s] Pushes value to the head of the list.
199
- def unshift(value)
200
- db.lpush(key, value)
147
+ def include?(model)
148
+ raw.include?(model.id)
201
149
  end
202
150
 
203
- # @return [Array] Elements of the list.
204
- def raw
205
- db.list(key)
151
+ def empty?
152
+ raw.empty?
206
153
  end
207
154
 
208
- # @return [Integer] Returns the number of elements in the list.
209
155
  def size
210
- db.llen(key)
156
+ raw.size
211
157
  end
212
158
 
213
- def include?(value)
214
- raw.include?(value)
159
+ def all
160
+ raw.to_a.map(&model)
215
161
  end
216
162
 
217
- def inspect
218
- "#<List: #{raw.inspect}>"
219
- end
163
+ alias to_a all
220
164
  end
221
165
 
222
- # Represents a Redis set.
223
- #
224
- # @example Use a set attribute.
225
- #
226
- # class Company < Ohm::Model
227
- # attribute :name
228
- # set :employees
229
- # end
230
- #
231
- # company = Company.create :name => "Redis Co."
232
- # company.employees << "Albert"
233
- # company.employees << "Benoit"
234
- # company.employees.all #=> ["Albert", "Benoit"]
235
- # company.include?("Albert") #=> true
236
166
  class Set < Collection
237
-
238
- # @param value [#to_s] Adds value to the list.
239
- def << value
240
- db.sadd(key, value)
241
- end
242
-
243
- def delete(value)
244
- db.srem(key, value)
245
- end
246
-
247
- def include?(value)
248
- db.sismember(key, value)
249
- end
250
-
251
- def raw
252
- db.smembers(key)
253
- end
254
-
255
- # @return [Integer] Returns the number of elements in the set.
256
- def size
257
- db.scard(key)
258
- end
167
+ Raw = Ohm::Set
259
168
 
260
169
  def inspect
261
- "#<Set: #{raw.inspect}>"
170
+ "#<Set (#{model}): #{all.inspect}>"
262
171
  end
263
172
 
264
173
  # Returns an intersection with the sets generated from the passed hash.
@@ -285,36 +194,42 @@ module Ohm
285
194
 
286
195
  # Apply a redis operation on a collection of sets.
287
196
  def apply(operation, hash, glue)
288
- indices = keys(hash).unshift(key).uniq
289
- target = indices.join(glue)
290
- db.send(operation, target, *indices)
291
- self.class.new(db, target, model)
197
+ target = key.volatile.group(glue).append(*keys(hash))
198
+ model.db.send(operation, target, *target.parts)
199
+ Set.new(target, model)
292
200
  end
293
201
 
294
202
  # Transform a hash of attribute/values into an array of keys.
295
203
  def keys(hash)
296
- hash.inject([]) do |acc, t|
297
- acc + Array(t[1]).map do |v|
298
- model.index_key_for(t[0], v)
204
+ [].tap do |keys|
205
+ hash.each do |key, values|
206
+ values = [values] unless values.kind_of?(Array) # Yes, Array() is different in 1.8.x.
207
+ values.each do |v|
208
+ keys << model.index_key_for(key, v)
209
+ end
299
210
  end
300
211
  end
301
212
  end
302
213
  end
303
214
 
304
- class Index < Set
215
+ class List < Collection
216
+ Raw = Ohm::List
217
+
305
218
  def inspect
306
- "#<Index: #{raw.inspect}>"
219
+ "#<List (#{model}): #{all.inspect}>"
307
220
  end
221
+ end
308
222
 
309
- def clear
310
- raise Ohm::Model::CannotDeleteIndex
223
+ class Index < Set
224
+ def apply(operation, hash, glue)
225
+ if hash.keys.size == 1
226
+ return Set.new(keys(hash).first, model)
227
+ else
228
+ super
229
+ end
311
230
  end
312
231
  end
313
- end
314
232
 
315
- Error = Class.new(StandardError)
316
-
317
- class Model
318
233
  module Validations
319
234
  include Ohm::Validations
320
235
 
@@ -339,12 +254,6 @@ module Ohm
339
254
  end
340
255
  end
341
256
 
342
- class CannotDeleteIndex < Error
343
- def message
344
- "You tried to delete an internal index used by Ohm."
345
- end
346
- end
347
-
348
257
  class IndexNotFound < Error
349
258
  def initialize(att)
350
259
  @att = att
@@ -399,7 +308,7 @@ module Ohm
399
308
  #
400
309
  # @param name [Symbol] Name of the list.
401
310
  def self.list(name, model = nil)
402
- attr_list_reader(name, model)
311
+ attr_collection_reader(name, :List, model)
403
312
  collections << name unless collections.include?(name)
404
313
  end
405
314
 
@@ -409,7 +318,7 @@ module Ohm
409
318
  #
410
319
  # @param name [Symbol] Name of the set.
411
320
  def self.set(name, model = nil)
412
- attr_set_reader(name, model)
321
+ attr_collection_reader(name, :Set, model)
413
322
  collections << name unless collections.include?(name)
414
323
  end
415
324
 
@@ -432,17 +341,118 @@ module Ohm
432
341
  indices << att unless indices.include?(att)
433
342
  end
434
343
 
435
- def self.attr_list_reader(name, model = nil)
436
- define_method(name) do
437
- instance_variable_get("@#{name}") ||
438
- instance_variable_set("@#{name}", Attributes::List.new(db, key(name), model))
344
+ # Define a reference to another object.
345
+ #
346
+ # @example
347
+ # class Comment < Ohm::Model
348
+ # attribute :content
349
+ # reference :post, Post
350
+ # end
351
+ #
352
+ # @post = Post.create :content => "Interesting stuff"
353
+ #
354
+ # @comment = Comment.create(:content => "Indeed!", :post => @post)
355
+ #
356
+ # @comment.post.content
357
+ # # => "Interesting stuff"
358
+ #
359
+ # @comment.post = Post.create(:content => "Wonderful stuff")
360
+ #
361
+ # @comment.post.content
362
+ # # => "Wonderful stuff"
363
+ #
364
+ # @comment.post.update(:content => "Magnific stuff")
365
+ #
366
+ # @comment.post.content
367
+ # # => "Magnific stuff"
368
+ #
369
+ # @comment.post = nil
370
+ #
371
+ # @comment.post
372
+ # # => nil
373
+ #
374
+ # @see Ohm::Model::collection
375
+ def self.reference(name, model)
376
+ reader = :"#{name}_id"
377
+ writer = :"#{name}_id="
378
+
379
+ attribute reader
380
+ index reader
381
+
382
+ define_memoized_method(name) do
383
+ model[send(reader)]
384
+ end
385
+
386
+ define_method(:"#{name}=") do |value|
387
+ instance_variable_set("@#{name}", nil)
388
+ send(writer, value ? value.id : nil)
389
+ end
390
+
391
+ define_method(writer) do |value|
392
+ instance_variable_set("@#{name}", nil)
393
+ write_local(reader, value)
394
+ end
395
+ end
396
+
397
+ # Define a collection of objects which have a {Ohm::Model::reference reference}
398
+ # to this model.
399
+ #
400
+ # class Comment < Ohm::Model
401
+ # attribute :content
402
+ # reference :post, Post
403
+ # end
404
+ #
405
+ # class Post < Ohm::Model
406
+ # attribute :content
407
+ # collection :comments, Comment
408
+ # reference :author, Person
409
+ # end
410
+ #
411
+ # class Person < Ohm::Model
412
+ # attribute :name
413
+ #
414
+ # # When the name of the reference cannot be inferred,
415
+ # # you need to specify it in the third param.
416
+ # collection :posts, Post, :author
417
+ # end
418
+ #
419
+ # @person = Person.create :name => "Albert"
420
+ # @post = Post.create :content => "Interesting stuff", :author => @person
421
+ # @comment = Comment.create :content => "Indeed!", :post => @post
422
+ #
423
+ # @post.comments.first.content
424
+ # # => "Indeed!"
425
+ #
426
+ # @post.author.name
427
+ # # => "Albert"
428
+ #
429
+ # *Important*: please note that even though a collection is a {Ohm::Set Set},
430
+ # you should not add or remove objects from this collection directly.
431
+ #
432
+ # @see Ohm::Model::reference
433
+ # @param name [Symbol] Name of the collection.
434
+ # @param model [Constant] Model where the reference is defined.
435
+ # @param reference [Symbol] Reference as defined in the associated model.
436
+ def self.collection(name, model, reference = to_reference)
437
+ define_method(name) { model.find(:"#{reference}_id" => send(:id)) }
438
+ end
439
+
440
+ def self.to_reference
441
+ name.to_s.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
442
+ end
443
+
444
+ def self.attr_collection_reader(name, type, model)
445
+ if model
446
+ define_memoized_method(name) { Ohm::Model::const_get(type).new(key(name), model, db) }
447
+ else
448
+ define_memoized_method(name) { Ohm::const_get(type).new(key(name), db) }
439
449
  end
440
450
  end
441
451
 
442
- def self.attr_set_reader(name, model)
452
+ def self.define_memoized_method(name, &block)
443
453
  define_method(name) do
444
454
  instance_variable_get("@#{name}") ||
445
- instance_variable_set("@#{name}", Attributes::Set.new(db, key(name), model))
455
+ instance_variable_set("@#{name}", instance_eval(&block))
446
456
  end
447
457
  end
448
458
 
@@ -455,7 +465,7 @@ module Ohm
455
465
  end
456
466
 
457
467
  def self.all
458
- @all ||= Attributes::Index.new(db, key(:all), self)
468
+ @all ||= Ohm::Model::Index.new(key(:all), self)
459
469
  end
460
470
 
461
471
  def self.attributes
@@ -588,8 +598,9 @@ module Ohm
588
598
  def mutex
589
599
  lock!
590
600
  yield
591
- unlock!
592
601
  self
602
+ ensure
603
+ unlock!
593
604
  end
594
605
 
595
606
  def inspect
@@ -606,6 +617,25 @@ module Ohm
606
617
  "#<#{self.class}:#{new? ? "?" : id} #{everything.map {|e| e.join("=") }.join(" ")}>"
607
618
  end
608
619
 
620
+ # Makes the model connect to a different Redis instance.
621
+ #
622
+ # @example
623
+ #
624
+ # class Post < Ohm::Model
625
+ # connect :port => 6380, :db => 2
626
+ #
627
+ # attribute :body
628
+ # end
629
+ #
630
+ # # Since these settings are usually environment-specific,
631
+ # # you may want to call this method from outside of the class
632
+ # # definition:
633
+ # Post.connect(:port => 6380, :db => 2)
634
+ #
635
+ def self.connect(*options)
636
+ self.db = Ohm.connection(*options)
637
+ end
638
+
609
639
  protected
610
640
 
611
641
  def key(*args)
@@ -623,7 +653,8 @@ module Ohm
623
653
  # This method will be removed once MSET becomes standard.
624
654
  def write_with_set
625
655
  attributes.each do |att|
626
- (value = send(att)) ?
656
+ value = send(att)
657
+ value.to_s.empty? ?
627
658
  db.set(key(att), value) :
628
659
  db.del(key(att))
629
660
  end
@@ -634,7 +665,7 @@ module Ohm
634
665
  # available once MSET becomes standard.
635
666
  def write_with_mset
636
667
  unless attributes.empty?
637
- rems, adds = attributes.map { |a| [key(a), send(a)] }.partition { |t| t.last.nil? }
668
+ rems, adds = attributes.map { |a| [key(a), send(a)] }.partition { |t| t.last.to_s.empty? }
638
669
  db.del(*rems.flatten.compact) unless rems.empty?
639
670
  db.mset(adds.flatten) unless adds.empty?
640
671
  end
@@ -642,8 +673,13 @@ module Ohm
642
673
 
643
674
  private
644
675
 
676
+ # Provides access to the Redis database. This is shared accross all models and instances.
645
677
  def self.db
646
- Ohm.redis
678
+ Ohm.threaded[self] || Ohm.redis
679
+ end
680
+
681
+ def self.db=(connection)
682
+ Ohm.threaded[self] = connection
647
683
  end
648
684
 
649
685
  def self.key(*args)
@@ -659,7 +695,7 @@ module Ohm
659
695
  end
660
696
 
661
697
  def db
662
- Ohm.redis
698
+ self.class.db
663
699
  end
664
700
 
665
701
  def delete_attributes(atts)