ohm 0.1.0.rc6 → 0.1.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/lib/ohm.rb CHANGED
@@ -11,26 +11,97 @@ require File.join(File.dirname(__FILE__), "ohm", "key")
11
11
 
12
12
  module Ohm
13
13
 
14
- # Provides access to the Redis database. This is shared accross all models and instances.
14
+ # Provides access to the _Redis_ database. It is highly recommended that you
15
+ # use this sparingly, and only if you really know what you're doing.
16
+ #
17
+ # The better way to access the _Redis_ database and do raw _Redis_
18
+ # commands would be one of the following:
19
+ #
20
+ # 1. Use {Ohm::Model.key} or {Ohm::Model#key}. So if the name of your
21
+ # model is *Post*, it would be *Post.key* or the protected method
22
+ # *#key* which should be used within your *Post* model.
23
+ #
24
+ # 2. Use {Ohm::Model.db} or {Ohm::Model#db}. Although this is also
25
+ # accessible, it is much cleaner and terse to use {Ohm::Model.key}.
26
+ #
27
+ # @example
28
+ #
29
+ # class Post < Ohm::Model
30
+ # def comment_ids
31
+ # key[:comments].zrange(0, -1)
32
+ # end
33
+ #
34
+ # def add_comment_id(id)
35
+ # key[:comments].zadd(Time.now.to_i, id)
36
+ # end
37
+ #
38
+ # def remove_comment_id(id)
39
+ # # Let's use the db style here just to demonstrate.
40
+ # db.zrem key[:comments], id
41
+ # end
42
+ # end
43
+ #
44
+ # Post.key[:latest].sadd(1)
45
+ # Post.key[:latest].smembers == ["1"]
46
+ # # => true
47
+ #
48
+ # Post.key[:latest] == "Post:latest"
49
+ # # => true
50
+ #
51
+ # p = Post.create
52
+ # p.comment_ids == []
53
+ # # => true
54
+ #
55
+ # p.add_comment_id(101)
56
+ # p.comment_ids == ["101"]
57
+ # # => true
58
+ #
59
+ # p.remove_comment_id(101)
60
+ # p.comment_ids == []
61
+ # # => true
15
62
  def self.redis
16
63
  threaded[:redis] ||= connection(*options)
17
64
  end
18
65
 
66
+ # Assign a new _Redis_ connection. Internally used by {Ohm.connect}
67
+ # to clear the cached _Redis_ instance.
68
+ #
69
+ # If you're looking to change the connection or reconnect with different
70
+ # parameters, try {Ohm.connect} or {Ohm::Model.connect}.
71
+ # @see connect
72
+ # @see Model.connect
73
+ # @param connection [Redis] an instance created using `Redis.new`.
19
74
  def self.redis=(connection)
20
75
  threaded[:redis] = connection
21
76
  end
22
77
 
78
+ # @private Used internally by Ohm for thread safety.
23
79
  def self.threaded
24
80
  Thread.current[:ohm] ||= {}
25
81
  end
26
82
 
27
- # Connect to a redis database.
83
+ # Connect to a _Redis_ database.
84
+ #
85
+ # It is also worth mentioning that you can pass in a *URI* e.g.
86
+ #
87
+ # Ohm.connect :url => "redis://127.0.0.1:6379/0"
88
+ #
89
+ # Note that the value *0* refers to the database number for the given
90
+ # _Redis_ instance.
91
+ #
92
+ # Also you can use {Ohm.connect} without any arguments. The behavior will
93
+ # be as follows:
94
+ #
95
+ # # Connect to redis://127.0.0.1:6379/0
96
+ # Ohm.connect
97
+ #
98
+ # # Connect to redis://10.0.0.100:22222/5
99
+ # ENV["REDIS_URL"] = "redis://10.0.0.100:22222/5"
100
+ # Ohm.connect
101
+ #
102
+ # @param options [{Symbol => #to_s}] An options hash.
103
+ # @see file:README.html#connecting Ohm.connect options documentation.
28
104
  #
29
- # @param options [Hash] options to create a message with.
30
- # @option options [#to_s] :host ('127.0.0.1') Host of the redis database.
31
- # @option options [#to_s] :port (6379) Port number.
32
- # @option options [#to_s] :db (0) Database number.
33
- # @option options [#to_s] :timeout (0) Database timeout in seconds.
34
105
  # @example Connect to a database in port 6380.
35
106
  # Ohm.connect(:port => 6380)
36
107
  def self.connect(*options)
@@ -38,29 +109,102 @@ module Ohm
38
109
  @options = options
39
110
  end
40
111
 
41
- # Return a connection to Redis.
112
+ # @private Return a connection to Redis.
42
113
  #
43
- # This is a wapper around Redis.connect(options)
114
+ # This is a wrapper around Redis.connect(options)
44
115
  def self.connection(*options)
45
116
  Redis.connect(*options)
46
117
  end
47
118
 
119
+ # @private Stores the connection options for Ohm.redis.
48
120
  def self.options
49
121
  @options = [] unless defined? @options
50
122
  @options
51
123
  end
52
124
 
53
- # Clear the database.
125
+ # Clear the database. You typically use this only during testing,
126
+ # or when you seed your site.
127
+ #
128
+ # @see http://code.google.com/p/redis/wiki/FlushdbCommand FLUSHDB in the
129
+ # Redis Command Reference.
54
130
  def self.flush
55
131
  redis.flushdb
56
132
  end
57
133
 
134
+ # The base class of all *Ohm* errors. Can be used as a catch all for
135
+ # Ohm related errors.
58
136
  class Error < StandardError; end
59
137
 
138
+ # This is the class that you need to extend in order to define your
139
+ # own models.
140
+ #
141
+ # Probably the most magic happening within {Ohm::Model} is the catching
142
+ # of {Ohm::Model.const_missing} exceptions to allow the use of constants
143
+ # even before they are defined.
144
+ #
145
+ # @example
146
+ #
147
+ # class Post < Ohm::Model
148
+ # reference :author, User # no User definition yet!
149
+ # end
150
+ #
151
+ # class User < Ohm::Model
152
+ # end
153
+ #
154
+ # @see Model.const_missing
60
155
  class Model
61
156
 
62
157
  # Wraps a model name for lazy evaluation.
63
158
  class Wrapper < BasicObject
159
+
160
+ # Allows you to use a constant even before it is defined. This solves
161
+ # the issue of having to require inter-project dependencies in a very
162
+ # simple and "magic-free" manner.
163
+ #
164
+ # Example of how it was done before Wrapper existed:
165
+ #
166
+ # require "./app/models/user"
167
+ # require "./app/models/comment"
168
+ #
169
+ # class Post < Ohm::Model
170
+ # reference :author, User
171
+ # list :comments, Comment
172
+ # end
173
+ #
174
+ # Now, you can simply do the following:
175
+ # class Post < Ohm::Model
176
+ # reference :author, User
177
+ # list :comments, Comment
178
+ # end
179
+ #
180
+ # @example
181
+ #
182
+ # module Commenting
183
+ # def self.included(base)
184
+ # base.list :comments, Ohm::Model::Wrapper.new(:Comment) {
185
+ # Object.const_get(:Comment)
186
+ # }
187
+ # end
188
+ # end
189
+ #
190
+ # # In your classes:
191
+ # class Post < Ohm::Model
192
+ # include Commenting
193
+ # end
194
+ #
195
+ # class Comment < Ohm::Model
196
+ # end
197
+ #
198
+ # p = Post.create
199
+ # p.comments.empty?
200
+ # # => true
201
+ #
202
+ # p.comments.push(Comment.create)
203
+ # p.comments.size == 1
204
+ # # => true
205
+ #
206
+ # @param [Symbol, String] name Canonical name of wrapped class.
207
+ # @param [#to_proc] block Closure for getting the name of the constant.
64
208
  def initialize(name, &block)
65
209
  @name = name
66
210
  @caller = ::Kernel.caller[2]
@@ -68,51 +212,134 @@ module Ohm
68
212
 
69
213
  class << self
70
214
  def method_missing(method_id, *args)
71
- ::Kernel.raise ::NoMethodError, "You tried to call #{@name}##{method_id}, but #{@name} is not defined on #{@caller}"
215
+ ::Kernel.raise(
216
+ ::NoMethodError,
217
+ "You tried to call %s#%s, but %s is not defined on %s" % [
218
+ @name, method_id, @name, @caller
219
+ ]
220
+ )
72
221
  end
73
222
  end
74
223
  end
75
224
 
225
+ # Used as a convenience for wrapping an existing constant into a
226
+ # {Ohm::Model::Wrapper wrapper object}.
227
+ #
228
+ # This is used extensively within the library for points where a user
229
+ # defined class (e.g. _Post_, _User_, _Comment_) is expected.
230
+ #
231
+ # You can also use this if you need to do uncommon things, such as
232
+ # creating your own {Ohm::Model::Set Set}, {Ohm::Model::List List}, etc.
233
+ #
234
+ # (*NOTE:* Keep in mind that the following code is given only as an
235
+ # educational example, and is in no way prescribed as good design.)
236
+ #
237
+ # class User < Ohm::Model
238
+ # end
239
+ #
240
+ # User.create(:id => "1001")
241
+ #
242
+ # Ohm.redis.sadd("myset", 1001)
243
+ #
244
+ # key = Ohm::Key.new("myset", Ohm.redis)
245
+ # set = Ohm::Model::Set.new(key, Ohm::Model::Wrapper.wrap(User))
246
+ #
247
+ # [User[1001]] == set.all.to_a
248
+ # # => true
249
+ #
250
+ # @see http://ohm.keyvalue.org/tutorials/chaining Chaining Ohm Sets
76
251
  def self.wrap(object)
77
252
  object.class == self ? object : new(object.inspect) { object }
78
253
  end
79
254
 
255
+ # Evaluates the passed block in {Ohm::Model::Wrapper#initialize}.
256
+ #
257
+ # @return [Class] The wrapped class.
80
258
  def unwrap
81
259
  @block.call
82
260
  end
83
261
 
262
+ # Since {Ohm::Model::Wrapper} is a subclass of _BasicObject_ we have
263
+ # to manually declare this.
264
+ #
265
+ # @return [Wrapper]
84
266
  def class
85
267
  Wrapper
86
268
  end
87
269
 
270
+ # @return [String] A string describing this lazy object.
88
271
  def inspect
89
272
  "<Wrapper for #{@name} (in #{@caller})>"
90
273
  end
91
274
  end
92
275
 
276
+ # Defines the base implementation for all enumerable types in Ohm,
277
+ # which includes {Ohm::Model::Set Sets}, {Ohm::Model::List Lists} and
278
+ # {Ohm::Model::Index Indices}.
93
279
  class Collection
94
280
  include Enumerable
95
281
 
282
+ # An instance of {Ohm::Key}.
96
283
  attr :key
284
+
285
+ # A subclass of {Ohm::Model}.
97
286
  attr :model
98
287
 
288
+ # @param [Key] key A key which includes a _Redis_ connection.
289
+ # @param [Ohm::Model::Wrapper] model A wrapped subclass of {Ohm::Model}.
99
290
  def initialize(key, model)
100
291
  @key = key
101
292
  @model = model.unwrap
102
293
  end
103
294
 
295
+ # Adds an instance of {Ohm::Model} to this collection.
296
+ #
297
+ # @param [#id] model A model with an ID.
104
298
  def add(model)
105
299
  self << model
106
300
  end
107
301
 
108
- def sort(_options = {})
302
+ # Sort this collection using the ID by default, or an attribute defined
303
+ # in the elements of this collection.
304
+ #
305
+ # *NOTE:* It is worth mentioning that if you want to sort by a specific
306
+ # attribute instead of an ID, you would probably want to use
307
+ # {Ohm::Model::Collection#sort_by sort_by} instead.
308
+ #
309
+ # @example
310
+ # class Post < Ohm::Model
311
+ # attribute :title
312
+ # end
313
+ #
314
+ # p1 = Post.create(:title => "Alpha")
315
+ # p2 = Post.create(:title => "Beta")
316
+ # p3 = Post.create(:title => "Gamma")
317
+ #
318
+ # [p1, p2, p3] == Post.all.sort.to_a
319
+ # # => true
320
+ #
321
+ # [p3, p2, p1] == Post.all.sort(:order => "DESC").to_a
322
+ # # => true
323
+ #
324
+ # [p1, p2, p3] == Post.all.sort(:by => "Post:*->title",
325
+ # :order => "ASC ALPHA").to_a
326
+ # # => true
327
+ #
328
+ # [p3, p2, p1] == Post.all.sort(:by => "Post:*->title",
329
+ # :order => "DESC ALPHA").to_a
330
+ # # => true
331
+ #
332
+ # @see file:README.html#sorting Sorting in the README.
333
+ # @see http://code.google.com/p/redis/wiki/SortCommand SORT in the
334
+ # Redis Command Reference.
335
+ def sort(options = {})
109
336
  return [] unless key.exists
110
337
 
111
- options = _options.dup
112
- options[:start] ||= 0
113
- options[:limit] = [options[:start], options[:limit]] if options[:limit]
338
+ opts = options.dup
339
+ opts[:start] ||= 0
340
+ opts[:limit] = [opts[:start], opts[:limit]] if opts[:limit]
114
341
 
115
- key.sort(options).map(&model)
342
+ key.sort(opts).map(&model)
116
343
  end
117
344
 
118
345
  # Sort the model instances by the given attribute.
@@ -125,23 +352,73 @@ module Ohm
125
352
  # user = User.all.sort_by(:name, :order => "ALPHA").first
126
353
  # user.name == "A"
127
354
  # # => true
128
- def sort_by(att, _options = {})
355
+ #
356
+ # @see file:README.html#sorting Sorting in the README.
357
+ def sort_by(att, options = {})
129
358
  return [] unless key.exists
130
359
 
131
- options = _options.dup
132
- options.merge!(:by => model.key["*->#{att}"])
360
+ opts = options.dup
361
+ opts.merge!(:by => model.key["*->#{att}"])
133
362
 
134
- if options[:get]
135
- key.sort(options.merge(:get => model.key["*->#{options[:get]}"]))
363
+ if opts[:get]
364
+ key.sort(opts.merge(:get => model.key["*->#{opts[:get]}"]))
136
365
  else
137
- sort(options)
366
+ sort(opts)
138
367
  end
139
368
  end
140
369
 
370
+ # Delete this collection.
371
+ #
372
+ # @example
373
+ #
374
+ # class Post < Ohm::Model
375
+ # list :comments, Comment
376
+ # end
377
+ #
378
+ # class Comment < Ohm::Model
379
+ # end
380
+ #
381
+ # post = Post.create
382
+ # post.comments << Comment.create
383
+ #
384
+ # post.comments.size == 1
385
+ # # => true
386
+ #
387
+ # post.comments.clear
388
+ # post.comments.size == 0
389
+ # # => true
390
+ # @see http://code.google.com/p/redis/wiki/DelCommand DEL in the Redis
391
+ # Command Reference.
141
392
  def clear
142
393
  key.del
143
394
  end
144
395
 
396
+ # Simultaneously clear and add all models. This wraps all operations
397
+ # in a MULTI EXEC block to make the whole operation atomic.
398
+ #
399
+ # @example
400
+ #
401
+ # class Post < Ohm::Model
402
+ # list :comments, Comment
403
+ # end
404
+ #
405
+ # class Comment < Ohm::Model
406
+ # end
407
+ #
408
+ # post = Post.create
409
+ # post.comments << Comment.create(:id => 100)
410
+ #
411
+ # post.comments.map(&:id) == ["100"]
412
+ # # => true
413
+ #
414
+ # comments = (101..103).to_a.map { |i| Comment.create(:id => i) }
415
+ #
416
+ # post.comments.replace(comments)
417
+ # post.comments.map(&:id) == ["101", "102", "103"]
418
+ # # => true
419
+ #
420
+ # @see http://code.google.com/p/redis/wiki/MultiExecCommand MULTI EXEC
421
+ # in the Redis Command Reference.
145
422
  def replace(models)
146
423
  model.db.multi do
147
424
  clear
@@ -149,65 +426,265 @@ module Ohm
149
426
  end
150
427
  end
151
428
 
429
+ # @return [true, false] Whether or not this collection is empty.
152
430
  def empty?
153
431
  !key.exists
154
432
  end
155
433
 
434
+ # @return [Array] Array representation of this collection.
156
435
  def to_a
157
436
  all
158
437
  end
159
438
  end
160
439
 
440
+ # Provides a Ruby-esque interface to a _Redis_ *SET*. The *SET* is assumed
441
+ # to be composed of ids which maps to {#model}.
161
442
  class Set < Collection
443
+ # An implementation which relies on *SMEMBERS* and yields an instance
444
+ # of {#model}.
445
+ #
446
+ # @example
447
+ #
448
+ # class Author < Ohm::Model
449
+ # set :poems, Poem
450
+ # end
451
+ #
452
+ # class Poem < Ohm::Model
453
+ # end
454
+ #
455
+ # neruda = Author.create
456
+ # neruda.poems.add(Poem.create)
457
+ #
458
+ # neruda.poems.each do |poem|
459
+ # # do something with the poem
460
+ # end
461
+ #
462
+ # # if you look at the source, you'll quickly see that this can
463
+ # # easily be achieved by doing the following:
464
+ #
465
+ # neruda.poems.key.smembers.each do |id|
466
+ # poem = Poem[id]
467
+ # # do something with the poem
468
+ # end
469
+ #
470
+ # @see http://code.google.com/p/redis/wiki/SmembersCommand SMEMBERS
471
+ # in Redis Command Reference.
162
472
  def each(&block)
163
473
  key.smembers.each { |id| block.call(model[id]) }
164
474
  end
165
475
 
476
+ # Convenient way to scope access to a predefined set, useful for access
477
+ # control.
478
+ #
479
+ # @example
480
+ #
481
+ # class User < Ohm::Model
482
+ # set :photos, Photo
483
+ # end
484
+ #
485
+ # class Photo < Ohm::Model
486
+ # end
487
+ #
488
+ # @user = User.create
489
+ # @user.photos.add(Photo.create(:id => "101"))
490
+ # @user.photos.add(Photo.create(:id => "102"))
491
+ #
492
+ # Photo.create(:id => "500")
493
+ #
494
+ # @user.photos[101] == Photo[101]
495
+ # # => true
496
+ #
497
+ # @user.photos[500] == nil
498
+ # # => true
499
+ #
500
+ # @param [#to_s] id Any id existing within this set.
501
+ # @return [Ohm::Model, nil] The model if it exists.
166
502
  def [](id)
167
503
  model[id] if key.sismember(id)
168
504
  end
169
505
 
170
- def << model
506
+ # Adds a model to this set.
507
+ #
508
+ # @param [#id] model Typically an instance of an {Ohm::Model} subclass.
509
+ #
510
+ # @see http://code.google.com/p/redis/wiki/SaddCommand SADD in Redis
511
+ # Command Reference.
512
+ def <<(model)
171
513
  key.sadd(model.id)
172
514
  end
173
-
174
515
  alias add <<
175
516
 
517
+ # Thin Ruby interface wrapper for *SCARD*.
518
+ #
519
+ # @return [Fixnum] The total number of members for this set.
520
+ # @see http://code.google.com/p/redis/wiki/ScardCommand SCARD in Redis
521
+ # Command Reference.
176
522
  def size
177
523
  key.scard
178
524
  end
179
525
 
526
+ # Thin Ruby interface wrapper for *SREM*.
527
+ #
528
+ # @param [#id] member a member of this set.
529
+ # @see http://code.google.com/p/redis/wiki/SremCommand SREM in Redis
530
+ # Command Reference.
180
531
  def delete(member)
181
532
  key.srem(member.id)
182
533
  end
183
534
 
535
+ # Array representation of this set.
536
+ #
537
+ # @example
538
+ #
539
+ # class Author < Ohm::Model
540
+ # set :posts, Post
541
+ # end
542
+ #
543
+ # class Post < Ohm::Model
544
+ # end
545
+ #
546
+ # author = Author.create
547
+ # author.posts.add(Author.create(:id => "101"))
548
+ # author.posts.add(Author.create(:id => "102"))
549
+ #
550
+ # author.posts.all.is_a?(Array)
551
+ # # => true
552
+ #
553
+ # author.posts.all.include?(Author[101])
554
+ # # => true
555
+ #
556
+ # author.posts.all.include?(Author[102])
557
+ # # => true
558
+ #
559
+ # @return [Array<Ohm::Model>] All members of this set.
184
560
  def all
185
561
  key.smembers.map(&model)
186
562
  end
187
563
 
564
+ # Allows you to find members of this set which fits the given criteria.
565
+ #
566
+ # @example
567
+ #
568
+ # class Post < Ohm::Model
569
+ # attribute :title
570
+ # attribute :tags
571
+ #
572
+ # index :title
573
+ # index :tag
574
+ #
575
+ # def tag
576
+ # tags.split(/\s+/)
577
+ # end
578
+ # end
579
+ #
580
+ # post = Post.create(:title => "Ohm", :tags => "ruby ohm redis")
581
+ # Post.all.is_a?(Ohm::Model::Set)
582
+ # # => true
583
+ #
584
+ # Post.all.find(:tag => "ruby").include?(post)
585
+ # # => true
586
+ #
587
+ # # Post.find is actually just a wrapper around Post.all.find
588
+ # Post.find(:tag => "ohm", :title => "Ohm").include?(post)
589
+ # # => true
590
+ #
591
+ # Post.find(:tag => ["ruby", "python"]).empty?
592
+ # # => true
593
+ #
594
+ # # Alternatively, you may choose to chain them later on.
595
+ # ruby = Post.find(:tag => "ruby")
596
+ # ruby.find(:title => "Ohm").include?(post)
597
+ # # => true
598
+ #
599
+ # @param [Hash] options A hash of key value pairs.
600
+ # @return [Ohm::Model::Set] A set satisfying the filter passed.
188
601
  def find(options)
189
602
  source = keys(options)
190
603
  target = source.inject(key.volatile) { |chain, other| chain + other }
191
604
  apply(:sinterstore, key, source, target)
192
605
  end
193
606
 
607
+ # Similar to find except that it negates the criteria.
608
+ #
609
+ # @example
610
+ # class Post < Ohm::Model
611
+ # attribute :title
612
+ # end
613
+ #
614
+ # ohm = Post.create(:title => "Ohm")
615
+ # ruby = Post.create(:title => "Ruby")
616
+ #
617
+ # Post.except(:title => "Ohm").include?(ruby)
618
+ # # => true
619
+ #
620
+ # Post.except(:title => "Ohm").size == 1
621
+ # # => true
622
+ #
623
+ # @param [Hash] options A hash of key value pairs.
624
+ # @return [Ohm::Model::Set] A set satisfying the filter passed.
194
625
  def except(options)
195
626
  source = keys(options)
196
627
  target = source.inject(key.volatile) { |chain, other| chain - other }
197
628
  apply(:sdiffstore, key, source, target)
198
629
  end
199
630
 
200
- def first(_options = {})
201
- options = _options.dup
202
- options.merge!(:limit => 1)
631
+ # Returns by default the lowest id value for this set. You may also
632
+ # pass in options similar to {#sort}.
633
+ #
634
+ # @example
635
+ #
636
+ # class Post < Ohm::Model
637
+ # attribute :title
638
+ # end
639
+ #
640
+ # p1 = Post.create(:id => "101", :title => "Alpha")
641
+ # p2 = Post.create(:id => "100", :title => "Beta")
642
+ # p3 = Post.create(:id => "99", :title => "Gamma")
643
+ #
644
+ # Post.all.is_a?(Ohm::Model::Set)
645
+ # # => true
646
+ #
647
+ # p3 == Post.all.first
648
+ # # => true
649
+ #
650
+ # p1 == Post.all.first(:order => "DESC")
651
+ # # => true
652
+ #
653
+ # p1 == Post.all.first(:by => :title, :order => "ASC ALPHA")
654
+ # # => true
655
+ #
656
+ # # just ALPHA also means ASC ALPHA, for brevity.
657
+ # p1 == Post.all.first(:by => :title, :order => "ALPHA")
658
+ # # => true
659
+ #
660
+ # p3 == Post.all.first(:by => :title, :order => "DESC ALPHA")
661
+ # # => true
662
+ #
663
+ # @param [Hash] options Sort options hash.
664
+ # @return [Ohm::Model, nil] an {Ohm::Model} instance or nil if this
665
+ # set is empty.
666
+ #
667
+ # @see file:README.html#sorting Sorting in the README.
668
+ def first(options = {})
669
+ opts = options.dup
670
+ opts.merge!(:limit => 1)
203
671
 
204
- if options[:by]
205
- sort_by(options.delete(:by), options).first
672
+ if opts[:by]
673
+ sort_by(opts.delete(:by), opts).first
206
674
  else
207
- sort(options).first
675
+ sort(opts).first
208
676
  end
209
677
  end
210
678
 
679
+ # Ruby-like interface wrapper around *SISMEMBER*.
680
+ #
681
+ # @param [#id] model Typically an {Ohm::Model} instance.
682
+ #
683
+ # @return [true, false] Whether or not the {Ohm::Model model} instance
684
+ # is a member of this set.
685
+ #
686
+ # @see http://code.google.com/p/redis/wiki/SismemberCommand SISMEMBER
687
+ # in Redis Command Reference.
211
688
  def include?(model)
212
689
  key.sismember(model.id)
213
690
  end
@@ -217,17 +694,19 @@ module Ohm
217
694
  end
218
695
 
219
696
  protected
220
-
697
+ # @private
221
698
  def apply(operation, key, source, target)
222
699
  target.send(operation, key, *source)
223
700
  Set.new(target, Wrapper.wrap(model))
224
701
  end
225
702
 
703
+ # @private
704
+ #
226
705
  # Transform a hash of attribute/values into an array of keys.
227
706
  def keys(hash)
228
707
  [].tap do |keys|
229
708
  hash.each do |key, values|
230
- values = [values] unless values.kind_of?(Array) # Yes, Array() is different in 1.8.x.
709
+ values = [values] unless values.kind_of?(Array)
231
710
  values.each do |v|
232
711
  keys << model.index_key_for(key, v)
233
712
  end
@@ -237,6 +716,34 @@ module Ohm
237
716
  end
238
717
 
239
718
  class Index < Set
719
+ # This method is here primarily as an optimization. Let's say you have
720
+ # the following model:
721
+ #
722
+ # class Post < Ohm::Model
723
+ # attribute :title
724
+ # index :title
725
+ # end
726
+ #
727
+ # ruby = Post.create(:title => "ruby")
728
+ # redis = Post.create(:title => "redis")
729
+ #
730
+ # Post.key[:all].smembers == [ruby.id, redis.id]
731
+ # # => true
732
+ #
733
+ # Post.index_key_for(:title, "ruby").smembers == [ruby.id]
734
+ # # => true
735
+ #
736
+ # Post.index_key_for(:title, "redis").smembers == [redis.id]
737
+ # # => true
738
+ #
739
+ # If we want to search for example all `Posts` entitled "ruby" or
740
+ # "redis", then it doesn't make sense to do an INTERSECTION with
741
+ # `Post.key[:all]` since it would be redundant.
742
+ #
743
+ # The implementation of {Ohm::Model::Index#find} avoids this redundancy.
744
+ #
745
+ # @see Ohm::Model::Set#find find in Ohm::Model::Set.
746
+ # @see Ohm::Model.find find in Ohm::Model.
240
747
  def find(options)
241
748
  keys = keys(options)
242
749
  return super(options) if keys.size > 1
@@ -245,22 +752,96 @@ module Ohm
245
752
  end
246
753
  end
247
754
 
755
+ # Provides a Ruby-esque interface to a _Redis_ *LIST*. The *LIST* is
756
+ # assumed to be composed of ids which maps to {#model}.
248
757
  class List < Collection
758
+ # An implementation which relies on *LRANGE* and yields an instance
759
+ # of {#model}.
760
+ #
761
+ # @example
762
+ #
763
+ # class Post < Ohm::Model
764
+ # list :comments, Comment
765
+ # end
766
+ #
767
+ # class Comment < Ohm::Model
768
+ # end
769
+ #
770
+ # post = Post.create
771
+ # post.comments.add(Comment.create)
772
+ # post.comments.add(Comment.create)
773
+ #
774
+ # post.comments.each do |comment|
775
+ # # do something with the comment
776
+ # end
777
+ #
778
+ # # reading the source reveals that this is achieved by doing:
779
+ # post.comments.key.lrange(0, -1).each do |id|
780
+ # comment = Comment[id]
781
+ # # do something with the comment
782
+ # end
783
+ #
784
+ # @see http://code.google.com/p/redis/wiki/LrangeCommand LRANGE
785
+ # in Redis Command Reference.
249
786
  def each(&block)
250
787
  key.lrange(0, -1).each { |id| block.call(model[id]) }
251
788
  end
252
789
 
790
+ # Thin wrapper around *RPUSH*.
791
+ #
792
+ # @example
793
+ #
794
+ # class Post < Ohm::Model
795
+ # list :comments, Comment
796
+ # end
797
+ #
798
+ # class Comment < Ohm::Model
799
+ # end
800
+ #
801
+ # p = Post.create
802
+ # p.comments << Comment.create
803
+ #
804
+ # @param [#id] model Typically an {Ohm::Model} instance.
805
+ #
806
+ # @see http://code.google.com/p/redis/wiki/RpushCommand RPUSH
807
+ # in Redis Command Reference.
253
808
  def <<(model)
254
809
  key.rpush(model.id)
255
810
  end
256
-
257
811
  alias push <<
258
812
 
259
813
  # Returns the element at index, or returns a subarray starting at
260
- # start and continuing for length elements, or returns a subarray
261
- # specified by range. Negative indices count backward from the end
814
+ # `start` and continuing for `length` elements, or returns a subarray
815
+ # specified by `range`. Negative indices count backward from the end
262
816
  # of the array (-1 is the last element). Returns nil if the index
263
817
  # (or starting index) are out of range.
818
+ #
819
+ # @example
820
+ # class Post < Ohm::Model
821
+ # list :comments, Comment
822
+ # end
823
+ #
824
+ # class Comment < Ohm::Model
825
+ # end
826
+ #
827
+ # post = Post.create
828
+ #
829
+ # 10.times { post.comments << Comment.create }
830
+ #
831
+ # post.comments[0] == Comment[1]
832
+ # # => true
833
+ #
834
+ # post.comments[0, 4] == (1..5).map { |i| Comment[i] }
835
+ # # => true
836
+ #
837
+ # post.comments[0, 4] == post.comments[0..4]
838
+ # # => true
839
+ #
840
+ # post.comments.all == post.comments[0, -1]
841
+ # # => true
842
+ #
843
+ # @see http://code.google.com/p/redis/wiki/LrangeCommand LRANGE
844
+ # in Redis Command Reference.
264
845
  def [](index, limit = nil)
265
846
  case [index, limit]
266
847
  when Pattern[Fixnum, Fixnum] then
@@ -272,30 +853,76 @@ module Ohm
272
853
  end
273
854
  end
274
855
 
856
+ # Convience method for doing list[0], similar to Ruby's Array#first
857
+ # method.
858
+ #
859
+ # @return [Ohm::Model, nil] An {Ohm::Model} instance or nil if the list
860
+ # is empty.
275
861
  def first
276
862
  self[0]
277
863
  end
278
864
 
865
+ # Returns the model at the tail of this list, while simultaneously
866
+ # removing it from the list.
867
+ #
868
+ # @return [Ohm::Model, nil] an {Ohm::Model} instance or nil if the list
869
+ # is empty.
870
+ #
871
+ # @see http://code.google.com/p/redis/wiki/LpopCommand RPOP
872
+ # in Redis Command Reference.
279
873
  def pop
280
874
  model[key.rpop]
281
875
  end
282
876
 
877
+ # Returns the model at the head of this list, while simultaneously
878
+ # removing it from the list.
879
+ #
880
+ # @return [Ohm::Model, nil] An {Ohm::Model} instance or nil if the list
881
+ # is empty.
882
+ #
883
+ # @see http://code.google.com/p/redis/wiki/LpopCommand LPOP
884
+ # in Redis Command Reference.
283
885
  def shift
284
886
  model[key.lpop]
285
887
  end
286
888
 
889
+ # Prepends an {Ohm::Model} instance at the beginning of this list.
890
+ #
891
+ # @param [#id] model Typically an {Ohm::Model} instance.
892
+ #
893
+ # @see http://code.google.com/p/redis/wiki/RpushCommand LPUSH
894
+ # in Redis Command Reference.
287
895
  def unshift(model)
288
896
  key.lpush(model.id)
289
897
  end
290
898
 
899
+ # Returns an array representation of this list, with elements of the
900
+ # array being an instance of {#model}.
901
+ #
902
+ # @return [Array<Ohm::Model>] Instances of {Ohm::Model}.
291
903
  def all
292
904
  key.lrange(0, -1).map(&model)
293
905
  end
294
906
 
907
+ # Thin Ruby interface wrapper for *LLEN*.
908
+ #
909
+ # @return [Fixnum] The total number of elements for this list.
910
+ #
911
+ # @see http://code.google.com/p/redis/wiki/LlenCommand LLEN in Redis
912
+ # Command Reference.
295
913
  def size
296
914
  key.llen
297
915
  end
298
916
 
917
+ # Ruby-like interface wrapper around *LRANGE*.
918
+ #
919
+ # @param [#id] model Typically an {Ohm::Model} instance.
920
+ #
921
+ # @return [true, false] Whether or not the {Ohm::Model} instance is
922
+ # an element of this list.
923
+ #
924
+ # @see http://code.google.com/p/redis/wiki/LrangeCommand LRANGE
925
+ # in Redis Command Reference.
299
926
  def include?(model)
300
927
  key.lrange(0, -1).include?(model.id)
301
928
  end
@@ -305,30 +932,104 @@ module Ohm
305
932
  end
306
933
  end
307
934
 
935
+ # All validations which need to access the _Redis_ database goes here.
936
+ # As of this writing, {Ohm::Model::Validations#assert_unique} is the only
937
+ # assertion contained within this module.
308
938
  module Validations
309
939
  include Ohm::Validations
310
940
 
311
- # Validates that the attribute or array of attributes are unique. For this,
312
- # an index of the same kind must exist.
941
+ # Validates that the attribute or array of attributes are unique. For
942
+ # this, an index of the same kind must exist.
313
943
  #
314
944
  # @overload assert_unique :name
315
945
  # Validates that the name attribute is unique.
316
946
  # @overload assert_unique [:street, :city]
317
947
  # Validates that the :street and :city pair is unique.
318
- def assert_unique(attrs)
319
- result = db.sinter(*Array(attrs).map { |att| index_key_for(att, send(att)) })
320
- assert result.empty? || !new? && result.include?(id.to_s), [attrs, :not_unique]
948
+ def assert_unique(atts, error = [atts, :not_unique])
949
+ indices = Array(atts).map { |att| index_key_for(att, send(att)) }
950
+ result = db.sinter(*indices)
951
+
952
+ assert result.empty? || !new? && result.include?(id.to_s), error
321
953
  end
322
954
  end
323
955
 
324
956
  include Validations
325
957
 
958
+ # Raised when you try and get the *id* of an {Ohm::Model} without an id.
959
+ #
960
+ # class Post < Ohm::Model
961
+ # list :comments, Comment
962
+ # end
963
+ #
964
+ # class Comment < Ohm::Model
965
+ # end
966
+ #
967
+ # ex = nil
968
+ # begin
969
+ # Post.new.id
970
+ # rescue Exception => e
971
+ # ex = e
972
+ # end
973
+ #
974
+ # ex.kind_of?(Ohm::Model::MissingID)
975
+ # # => true
976
+ #
977
+ # This is also one of the most common errors you'll be faced with when
978
+ # you're new to {Ohm} coming from an ActiveRecord background, where you
979
+ # are used to just assigning associations even before the base model is
980
+ # persisted.
981
+ #
982
+ # # following from the example above:
983
+ # post = Post.new
984
+ #
985
+ # ex = nil
986
+ # begin
987
+ # post.comments << Comment.new
988
+ # rescue Exception => e
989
+ # ex = e
990
+ # end
991
+ #
992
+ # ex.kind_of?(Ohm::Model::MissingID)
993
+ # # => true
994
+ #
995
+ # # Correct way:
996
+ # post = Post.new
997
+ #
998
+ # if post.save
999
+ # post.comments << Comment.create
1000
+ # end
326
1001
  class MissingID < Error
327
1002
  def message
328
- "You tried to perform an operation that needs the model ID, but it's not present."
1003
+ "You tried to perform an operation that needs the model ID, " +
1004
+ "but it's not present."
329
1005
  end
330
1006
  end
331
1007
 
1008
+ # Raised when you try and do an {Ohm::Model::Set#find} operation and use
1009
+ # a key which you did not define as an index.
1010
+ #
1011
+ # class Post < Ohm::Model
1012
+ # attribute :title
1013
+ # end
1014
+ #
1015
+ # post = Post.create(:title => "Ohm")
1016
+ #
1017
+ # ex = nil
1018
+ # begin
1019
+ # Post.find(:title => "Ohm")
1020
+ # rescue Exception => e
1021
+ # ex = e
1022
+ # end
1023
+ #
1024
+ # ex.kind_of?(Ohm::Model::IndexNotFound)
1025
+ # # => true
1026
+ #
1027
+ # To correct this problem, simply define a _:title_ *index* in your class.
1028
+ #
1029
+ # class Post < Ohm::Model
1030
+ # attribute :title
1031
+ # index :title
1032
+ # end
332
1033
  class IndexNotFound < Error
333
1034
  def initialize(att)
334
1035
  @att = att
@@ -339,21 +1040,24 @@ module Ohm
339
1040
  end
340
1041
  end
341
1042
 
342
- @@attributes = Hash.new { |hash, key| hash[key] = [] }
1043
+ @@attributes = Hash.new { |hash, key| hash[key] = [] }
343
1044
  @@collections = Hash.new { |hash, key| hash[key] = [] }
344
- @@counters = Hash.new { |hash, key| hash[key] = [] }
345
- @@indices = Hash.new { |hash, key| hash[key] = [] }
346
-
347
- attr_writer :id
1045
+ @@counters = Hash.new { |hash, key| hash[key] = [] }
1046
+ @@indices = Hash.new { |hash, key| hash[key] = [] }
348
1047
 
349
1048
  def id
350
1049
  @id or raise MissingID
351
1050
  end
352
1051
 
353
- # Defines a string attribute for the model. This attribute will be persisted by Redis
354
- # as a string. Any value stored here will be retrieved in its string representation.
1052
+ # Defines a string attribute for the model. This attribute will be
1053
+ # persisted by _Redis_ as a string. Any value stored here will be
1054
+ # retrieved in its string representation.
1055
+ #
1056
+ # If you're looking to have typecasting built in, you may want to look at
1057
+ # Ohm::Typecast in Ohm::Contrib.
355
1058
  #
356
1059
  # @param name [Symbol] Name of the attribute.
1060
+ # @see http://cyx.github.com/ohm-contrib/doc/Ohm/Typecast.html
357
1061
  def self.attribute(name)
358
1062
  define_method(name) do
359
1063
  read_local(name)
@@ -366,10 +1070,10 @@ module Ohm
366
1070
  attributes << name unless attributes.include?(name)
367
1071
  end
368
1072
 
369
- # Defines a counter attribute for the model. This attribute can't be assigned, only incremented
370
- # or decremented. It will be zero by default.
1073
+ # Defines a counter attribute for the model. This attribute can't be
1074
+ # assigned, only incremented or decremented. It will be zero by default.
371
1075
  #
372
- # @param name [Symbol] Name of the counter.
1076
+ # @param [Symbol] name Name of the counter.
373
1077
  def self.counter(name)
374
1078
  define_method(name) do
375
1079
  read_local(name).to_i
@@ -378,20 +1082,44 @@ module Ohm
378
1082
  counters << name unless counters.include?(name)
379
1083
  end
380
1084
 
381
- # Defines a list attribute for the model. It can be accessed only after the model instance
382
- # is created.
1085
+ # Defines a list attribute for the model. It can be accessed only after
1086
+ # the model instance is created, or if you assign an :id during object
1087
+ # construction.
1088
+ #
1089
+ # @example
1090
+ #
1091
+ # class Post < Ohm::Model
1092
+ # list :comments, Comment
1093
+ # end
1094
+ #
1095
+ # class Comment < Ohm::Model
1096
+ # end
1097
+ #
1098
+ # # WRONG!!!
1099
+ # post = Post.new
1100
+ # post.comments << Comment.create
1101
+ #
1102
+ # # Right :-)
1103
+ # post = Post.create
1104
+ # post.comments << Comment.create
383
1105
  #
384
- # @param name [Symbol] Name of the list.
1106
+ # # Alternative way if you want to have custom ids.
1107
+ # post = Post.new(:id => "my-id")
1108
+ # post.comments << Comment.create
1109
+ # post.create
1110
+ #
1111
+ # @param [Symbol] name Name of the list.
385
1112
  def self.list(name, model)
386
1113
  define_memoized_method(name) { List.new(key[name], Wrapper.wrap(model)) }
387
1114
  collections << name unless collections.include?(name)
388
1115
  end
389
1116
 
390
- # Defines a set attribute for the model. It can be accessed only after the model instance
391
- # is created. Sets are recommended when insertion and retrival order is irrelevant, and
392
- # operations like union, join, and membership checks are important.
1117
+ # Defines a set attribute for the model. It can be accessed only after
1118
+ # the model instance is created. Sets are recommended when insertion and
1119
+ # retreival order is irrelevant, and operations like union, join, and
1120
+ # membership checks are important.
393
1121
  #
394
- # @param name [Symbol] Name of the set.
1122
+ # @param [Symbol] name Name of the set.
395
1123
  def self.set(name, model)
396
1124
  define_memoized_method(name) { Set.new(key[name], Wrapper.wrap(model)) }
397
1125
  collections << name unless collections.include?(name)
@@ -399,19 +1127,20 @@ module Ohm
399
1127
 
400
1128
  # Creates an index (a set) that will be used for finding instances.
401
1129
  #
402
- # If you want to find a model instance by some attribute value, then an index for that
403
- # attribute must exist.
1130
+ # If you want to find a model instance by some attribute value, then an
1131
+ # index for that attribute must exist.
404
1132
  #
405
1133
  # @example
1134
+ #
406
1135
  # class User < Ohm::Model
407
1136
  # attribute :email
408
1137
  # index :email
409
1138
  # end
410
1139
  #
411
1140
  # # Now this is possible:
412
- # User.find email: "ohm@example.com"
1141
+ # User.find :email => "ohm@example.com"
413
1142
  #
414
- # @param name [Symbol] Name of the attribute to be indexed.
1143
+ # @param [Symbol] name Name of the attribute to be indexed.
415
1144
  def self.index(att)
416
1145
  indices << att unless indices.include?(att)
417
1146
  end
@@ -419,6 +1148,7 @@ module Ohm
419
1148
  # Define a reference to another object.
420
1149
  #
421
1150
  # @example
1151
+ #
422
1152
  # class Comment < Ohm::Model
423
1153
  # attribute :content
424
1154
  # reference :post, Post
@@ -446,7 +1176,8 @@ module Ohm
446
1176
  # @comment.post
447
1177
  # # => nil
448
1178
  #
449
- # @see Ohm::Model::collection
1179
+ # @see file:README.html#references References Explained.
1180
+ # @see Ohm::Model.collection
450
1181
  def self.reference(name, model)
451
1182
  model = Wrapper.wrap(model)
452
1183
 
@@ -476,8 +1207,8 @@ module Ohm
476
1207
  end
477
1208
  end
478
1209
 
479
- # Define a collection of objects which have a {Ohm::Model::reference reference}
480
- # to this model.
1210
+ # Define a collection of objects which have a
1211
+ # {Ohm::Model.reference reference} to this model.
481
1212
  #
482
1213
  # class Comment < Ohm::Model
483
1214
  # attribute :content
@@ -499,7 +1230,8 @@ module Ohm
499
1230
  # end
500
1231
  #
501
1232
  # @person = Person.create :name => "Albert"
502
- # @post = Post.create :content => "Interesting stuff", :author => @person
1233
+ # @post = Post.create :content => "Interesting stuff",
1234
+ # :author => @person
503
1235
  # @comment = Comment.create :content => "Indeed!", :post => @post
504
1236
  #
505
1237
  # @post.comments.first.content
@@ -508,56 +1240,126 @@ module Ohm
508
1240
  # @post.author.name
509
1241
  # # => "Albert"
510
1242
  #
511
- # *Important*: please note that even though a collection is a {Ohm::Set Set},
512
- # you should not add or remove objects from this collection directly.
1243
+ # *Important*: Please note that even though a collection is a
1244
+ # {Ohm::Model::Set set}, you should not add or remove objects from this
1245
+ # collection directly.
513
1246
  #
514
- # @see Ohm::Model::reference
1247
+ # @see Ohm::Model.reference
515
1248
  # @param name [Symbol] Name of the collection.
516
1249
  # @param model [Constant] Model where the reference is defined.
517
- # @param reference [Symbol] Reference as defined in the associated model.
1250
+ # @param reference [Symbol] Reference as defined in the associated
1251
+ # model.
1252
+ #
1253
+ # @see file:README.html#collections Collections Explained.
518
1254
  def self.collection(name, model, reference = to_reference)
519
1255
  model = Wrapper.wrap(model)
520
- define_method(name) { model.unwrap.find(:"#{reference}_id" => send(:id)) }
1256
+ define_method(name) {
1257
+ model.unwrap.find(:"#{reference}_id" => send(:id))
1258
+ }
521
1259
  end
522
1260
 
1261
+ # Used by {Ohm::Model.collection} to infer the reference.
1262
+ #
1263
+ # @return [Symbol] Representation of this class in an all-lowercase
1264
+ # format, separated by underscores and demodulized.
523
1265
  def self.to_reference
524
- name.to_s.match(/^(?:.*::)*(.*)$/)[1].gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
1266
+ name.to_s.
1267
+ match(/^(?:.*::)*(.*)$/)[1].
1268
+ gsub(/([a-z\d])([A-Z])/, '\1_\2').
1269
+ downcase.to_sym
525
1270
  end
526
1271
 
1272
+ # @private
527
1273
  def self.define_memoized_method(name, &block)
528
1274
  define_method(name) do
529
1275
  @_memo[name] ||= instance_eval(&block)
530
1276
  end
531
1277
  end
532
1278
 
1279
+ # Allows you to find an {Ohm::Model} instance by its *id*.
1280
+ #
1281
+ # @param [#to_s] id The id of the model you want to find.
1282
+ # @return [Ohm::Model, nil] The instance of Ohm::Model or nil of it does
1283
+ # not exist.
533
1284
  def self.[](id)
534
1285
  new(:id => id) if id && exists?(id)
535
1286
  end
536
1287
 
1288
+ # @private Used for conveniently doing [1, 2].map(&Post) for example.
537
1289
  def self.to_proc
538
1290
  Proc.new { |id| self[id] }
539
1291
  end
540
1292
 
1293
+ # Returns a {Ohm::Model::Set set} containing all the members of a given
1294
+ # class.
1295
+ #
1296
+ # @example
1297
+ #
1298
+ # class Post < Ohm::Model
1299
+ # end
1300
+ #
1301
+ # post = Post.create
1302
+ #
1303
+ # Post.all.include?(post)
1304
+ # # => true
1305
+ #
1306
+ # post.delete
1307
+ #
1308
+ # Post.all.include?(post)
1309
+ # # => false
541
1310
  def self.all
542
1311
  Ohm::Model::Index.new(key[:all], Wrapper.wrap(self))
543
1312
  end
544
1313
 
1314
+ # All the defined attributes within a class.
1315
+ # @see Ohm::Model.attribute
545
1316
  def self.attributes
546
1317
  @@attributes[self]
547
1318
  end
548
1319
 
1320
+ # All the defined counters within a class.
1321
+ # @see Ohm::Model.counter
549
1322
  def self.counters
550
1323
  @@counters[self]
551
1324
  end
552
1325
 
1326
+ # All the defined collections within a class. This will be comprised of
1327
+ # all {Ohm::Model::Set sets} and {Ohm::Model::List lists} defined within
1328
+ # your class.
1329
+ #
1330
+ # @example
1331
+ # class Post < Ohm::Model
1332
+ # set :authors, Author
1333
+ # list :comments, Comment
1334
+ # end
1335
+ #
1336
+ # Post.collections == [:authors, :comments]
1337
+ # # => true
1338
+ #
1339
+ # @see Ohm::Model.list
1340
+ # @see Ohm::Model.set
553
1341
  def self.collections
554
1342
  @@collections[self]
555
1343
  end
556
1344
 
1345
+ # All the defined indices within a class.
1346
+ # @see Ohm::Model.index
557
1347
  def self.indices
558
1348
  @@indices[self]
559
1349
  end
560
1350
 
1351
+ # Convenience method to create and return the newly created object.
1352
+ #
1353
+ # @example
1354
+ #
1355
+ # class Post < Ohm::Model
1356
+ # attribute :title
1357
+ # end
1358
+ #
1359
+ # post = Post.create(:title => "A new post")
1360
+ #
1361
+ # @param [Hash] args attribute-value pairs for the object.
1362
+ # @return [Ohm::Model] an instance of the class you're trying to create.
561
1363
  def self.create(*args)
562
1364
  model = new(*args)
563
1365
  model.create
@@ -571,16 +1373,66 @@ module Ohm
571
1373
  # event2 = Event.create day: "2009-09-09", author: "Benoit"
572
1374
  # event3 = Event.create day: "2009-09-10", author: "Albert"
573
1375
  #
574
- # assert_equal [event1], Event.find(author: "Albert", day: "2009-09-09")
1376
+ # [event1] == Event.find(author: "Albert", day: "2009-09-09").to_a
1377
+ # # => true
575
1378
  def self.find(hash)
576
- raise ArgumentError, "You need to supply a hash with filters. If you want to find by ID, use #{self}[id] instead." unless hash.kind_of?(Hash)
1379
+ unless hash.kind_of?(Hash)
1380
+ raise ArgumentError,
1381
+ "You need to supply a hash with filters. " +
1382
+ "If you want to find by ID, use #{self}[id] instead."
1383
+ end
1384
+
577
1385
  all.find(hash)
578
1386
  end
579
1387
 
1388
+ # Encode a value, making it safe to use as a key. Internally used by
1389
+ # {Ohm::Model.index_key_for} to canonicalize the indexed values.
1390
+ #
1391
+ # @param [#to_s] value Any object you want to be able to use as a key.
1392
+ # @return [String] A string which is safe to use as a key.
1393
+ # @see Ohm::Model.index_key_for
580
1394
  def self.encode(value)
581
1395
  Base64.encode64(value.to_s).gsub("\n", "")
582
1396
  end
583
1397
 
1398
+ # Constructor for all subclasses of {Ohm::Model}, which optionally
1399
+ # takes a Hash of attribute value pairs.
1400
+ #
1401
+ # Starting with Ohm 0.1.0, you can use custom ids instead of being forced
1402
+ # to use auto incrementing numeric ids, but keep in mind that you have
1403
+ # to pass in the preferred id during object initialization.
1404
+ #
1405
+ # @example
1406
+ #
1407
+ # class User < Ohm::Model
1408
+ # end
1409
+ #
1410
+ # class Post < Ohm::Model
1411
+ # attribute :title
1412
+ # reference :user, User
1413
+ # end
1414
+ #
1415
+ # user = User.create
1416
+ # p1 = Post.new(:title => "Redis", :user_id => user.id)
1417
+ # p1.save
1418
+ #
1419
+ # p1.user_id == user.id
1420
+ # # => true
1421
+ #
1422
+ # p1.user == user
1423
+ # # => true
1424
+ #
1425
+ # # You can also just pass the actual User object, which is the better
1426
+ # # way to do it:
1427
+ # Post.new(:title => "Different way", :user => user).user == user
1428
+ # # => true
1429
+ #
1430
+ # # Let's try and generate custom ids
1431
+ # p2 = Post.new(:id => "ohm-redis-library", :title => "Lib")
1432
+ # p2 == Post["ohm-redis-library"]
1433
+ # # => true
1434
+ #
1435
+ # @param [Hash] attrs Attribute value pairs.
584
1436
  def initialize(attrs = {})
585
1437
  @id = nil
586
1438
  @_memo = {}
@@ -588,10 +1440,15 @@ module Ohm
588
1440
  update_attributes(attrs)
589
1441
  end
590
1442
 
1443
+ # @return [true, false] Whether or not this object has an id.
591
1444
  def new?
592
1445
  !@id
593
1446
  end
594
1447
 
1448
+ # Create this model if it passes all validations.
1449
+ #
1450
+ # @return [Ohm::Model, nil] The newly created object or nil if it fails
1451
+ # validation.
595
1452
  def create
596
1453
  return unless valid?
597
1454
  initialize_id
@@ -603,6 +1460,10 @@ module Ohm
603
1460
  end
604
1461
  end
605
1462
 
1463
+ # Create or update this object based on the state of #new?.
1464
+ #
1465
+ # @return [Ohm::Model, nil] The saved object or nil if it fails
1466
+ # validation.
606
1467
  def save
607
1468
  return create if new?
608
1469
  return unless valid?
@@ -613,17 +1474,32 @@ module Ohm
613
1474
  end
614
1475
  end
615
1476
 
1477
+ # Update this object, optionally accepting new attributes.
1478
+ #
1479
+ # @param [Hash] attrs Attribute value pairs to use for the updated
1480
+ # version
1481
+ # @return [Ohm::Model, nil] The updated object or nil if it fails
1482
+ # validation.
616
1483
  def update(attrs)
617
1484
  update_attributes(attrs)
618
1485
  save
619
1486
  end
620
1487
 
1488
+ # Locally update all attributes without persisting the changes.
1489
+ # Internally used by {Ohm::Model#initialize} and {Ohm::Model#update}
1490
+ # to set attribute value pairs.
1491
+ #
1492
+ # @param [Hash] attrs Attribute value pairs.
621
1493
  def update_attributes(attrs)
622
1494
  attrs.each do |key, value|
623
1495
  send(:"#{key}=", value)
624
1496
  end
625
1497
  end
626
1498
 
1499
+ # Delete this object from the _Redis_ datastore, ensuring that all
1500
+ # indices, attributes, collections, etc are also deleted with it.
1501
+ #
1502
+ # @return [Ohm::Model] Returns a reference of itself.
627
1503
  def delete
628
1504
  delete_from_indices
629
1505
  delete_attributes(collections) unless collections.empty?
@@ -633,22 +1509,27 @@ module Ohm
633
1509
 
634
1510
  # Increment the counter denoted by :att.
635
1511
  #
636
- # @param att [Symbol] Attribute to increment.
1512
+ # @param [Symbol] att Attribute to increment.
1513
+ # @param [Fixnum] count An optional increment step to use.
637
1514
  def incr(att, count = 1)
638
- raise ArgumentError, "#{att.inspect} is not a counter." unless counters.include?(att)
1515
+ unless counters.include?(att)
1516
+ raise ArgumentError, "#{att.inspect} is not a counter."
1517
+ end
1518
+
639
1519
  write_local(att, key.hincrby(att, count))
640
1520
  end
641
1521
 
642
1522
  # Decrement the counter denoted by :att.
643
1523
  #
644
- # @param att [Symbol] Attribute to decrement.
1524
+ # @param [Symbol] att Attribute to decrement.
1525
+ # @param [Fixnum] count An optional decrement step to use.
645
1526
  def decr(att, count = 1)
646
1527
  incr(att, -count)
647
1528
  end
648
1529
 
649
1530
  # Export the id and errors of the object. The `to_hash` takes the opposite
650
- # approach of providing all the attributes and instead favors a
651
- # white listed approach.
1531
+ # approach of providing all the attributes and instead favors a white
1532
+ # listed approach.
652
1533
  #
653
1534
  # @example
654
1535
  #
@@ -684,26 +1565,62 @@ module Ohm
684
1565
  attrs
685
1566
  end
686
1567
 
1568
+ # Returns the JSON representation of the {#to_hash} for this object.
1569
+ # Defining a custom {#to_hash} method will also affect this and return
1570
+ # a corresponding JSON representation of whatever you have in your
1571
+ # {#to_hash}.
1572
+ #
1573
+ # @example
1574
+ # require "json"
1575
+ #
1576
+ # class Post < Ohm::Model
1577
+ # attribute :title
1578
+ #
1579
+ # def to_hash
1580
+ # super.merge(:title => title)
1581
+ # end
1582
+ # end
1583
+ #
1584
+ # p1 = Post.create(:title => "Delta Force")
1585
+ # p1.to_hash == { :id => "1", :title => "Delta Force" }
1586
+ # # => true
1587
+ #
1588
+ # p1.to_json == "{\"id\":\"1\",\"title\":\"Delta Force\"}"
1589
+ # # => true
1590
+ #
1591
+ # @return [String] The JSON representation of this object defined in
1592
+ # terms of {#to_hash}.
687
1593
  def to_json(*args)
688
1594
  to_hash.to_json(*args)
689
1595
  end
690
1596
 
1597
+ # Convenience wrapper for {Ohm::Model.attributes}.
691
1598
  def attributes
692
1599
  self.class.attributes
693
1600
  end
694
1601
 
1602
+ # Convenience wrapper for {Ohm::Model.counters}.
695
1603
  def counters
696
1604
  self.class.counters
697
1605
  end
698
1606
 
1607
+ # Convenience wrapper for {Ohm::Model.collections}.
699
1608
  def collections
700
1609
  self.class.collections
701
1610
  end
702
1611
 
1612
+ # Convenience wrapper for {Ohm::Model.indices}.
703
1613
  def indices
704
1614
  self.class.indices
705
1615
  end
706
1616
 
1617
+ # Implementation of equality checking. Equality is defined by two simple
1618
+ # rules:
1619
+ #
1620
+ # 1. They have the same class.
1621
+ # 2. They have the same key (_Redis_ key e.g. Post:1 == Post:1).
1622
+ #
1623
+ # @return [true, false] Whether or not the passed object is equal.
707
1624
  def ==(other)
708
1625
  other.kind_of?(self.class) && other.key == key
709
1626
  rescue MissingID
@@ -711,11 +1628,38 @@ module Ohm
711
1628
  end
712
1629
  alias :eql? :==
713
1630
 
1631
+ # Allows you to safely use an instance of {Ohm::Model} as a key in a
1632
+ # Ruby hash without running into weird scenarios.
1633
+ #
1634
+ # @example
1635
+ #
1636
+ # class Post < Ohm::Model
1637
+ # end
1638
+ #
1639
+ # h = {}
1640
+ # p1 = Post.new
1641
+ # h[p1] = "Ruby"
1642
+ # h[p1] == "Ruby"
1643
+ # # => true
1644
+ #
1645
+ # p1.save
1646
+ # h[p1] == "Ruby"
1647
+ # # => false
1648
+ #
1649
+ # @return [Fixnum] An integer representing this object to be used
1650
+ # as the index for hashes in Ruby.
714
1651
  def hash
715
1652
  new? ? super : key.hash
716
1653
  end
717
1654
 
718
- # Lock the object before executing the block, and release it once the block is done.
1655
+ # Lock the object before executing the block, and release it once the
1656
+ # block is done.
1657
+ #
1658
+ # This is used during {#create} and {#save} to ensure that no race
1659
+ # conditions occur.
1660
+ #
1661
+ # @see http://code.google.com/p/redis/wiki/SetnxCommand SETNX in the
1662
+ # Redis Command Reference.
719
1663
  def mutex
720
1664
  lock!
721
1665
  yield
@@ -724,6 +1668,11 @@ module Ohm
724
1668
  unlock!
725
1669
  end
726
1670
 
1671
+ # Returns everything, including {Ohm::Model.attributes attributes},
1672
+ # {Ohm::Model.collections collections}, {Ohm::Model.counters counters},
1673
+ # and the id of this object.
1674
+ #
1675
+ # Useful for debugging and for doing irb work.
727
1676
  def inspect
728
1677
  everything = (attributes + collections + counters).map do |att|
729
1678
  value = begin
@@ -735,10 +1684,21 @@ module Ohm
735
1684
  [att, value.inspect]
736
1685
  end
737
1686
 
738
- "#<#{self.class}:#{new? ? "?" : id} #{everything.map {|e| e.join("=") }.join(" ")}>"
1687
+ sprintf("#<%s:%s %s>",
1688
+ self.class,
1689
+ new? ? "?" : id,
1690
+ everything.map {|e| e.join("=") }.join(" ")
1691
+ )
739
1692
  end
740
1693
 
741
- # Makes the model connect to a different Redis instance.
1694
+ # Makes the model connect to a different Redis instance. This is useful
1695
+ # for scaling a large application, where one model can be stored in a
1696
+ # different Redis instance, and some other groups of models can be
1697
+ # in another Redis instance.
1698
+ #
1699
+ # This approach of splitting models is a lot simpler than doing a
1700
+ # distributed *Redis* solution and may well be the right solution for
1701
+ # certain cases.
742
1702
  #
743
1703
  # @example
744
1704
  #
@@ -753,16 +1713,37 @@ module Ohm
753
1713
  # # definition:
754
1714
  # Post.connect(:port => 6380, :db => 2)
755
1715
  #
1716
+ # @see file:README.html#connecting Ohm.connect options documentation.
756
1717
  def self.connect(*options)
757
1718
  self.db = Ohm.connection(*options)
758
1719
  end
759
1720
 
760
1721
  protected
1722
+ attr_writer :id
761
1723
 
1724
+ # @return [Ohm::Key] A key scoped to the model which uses this object's
1725
+ # id.
1726
+ #
1727
+ # @see http://github.com/soveran/nest The Nest library.
762
1728
  def key
763
1729
  self.class.key[id]
764
1730
  end
765
1731
 
1732
+ # Write all the attributes and counters of this object. The operation
1733
+ # is actually a 2-step process:
1734
+ #
1735
+ # 1. Delete the current key, e.g. Post:2.
1736
+ # 2. Set all of the new attributes (using HMSET).
1737
+ #
1738
+ # The DEL and HMSET operations are wrapped in a MULTI EXEC block to ensure
1739
+ # the atomicity of the write operation.
1740
+ #
1741
+ # @see http://code.google.com/p/redis/wiki/DelCommand DEL in the
1742
+ # Redis Command Reference.
1743
+ # @see http://code.google.com/p/redis/wiki/HmsetCommand HMSET in the
1744
+ # Redis Command Reference.
1745
+ # @see http://code.google.com/p/redis/wiki/MultiExecCommand MULTI EXEC
1746
+ # in the Redis Command Reference.
766
1747
  def write
767
1748
  unless (attributes + counters).empty?
768
1749
  atts = (attributes + counters).inject([]) { |ret, att|
@@ -779,6 +1760,17 @@ module Ohm
779
1760
  end
780
1761
  end
781
1762
 
1763
+ # Write a single attribute both locally and remotely. It's very important
1764
+ # to know that this method skips validation checks, therefore you must
1765
+ # ensure data integrity and validity in your application code.
1766
+ #
1767
+ # @param [Symbol, String] att The name of the attribute to write.
1768
+ # @param [#to_s] value The value of the attribute to write.
1769
+ #
1770
+ # @see http://code.google.com/p/redis/wiki/HdelCommand HDEL in the
1771
+ # Redis Command Reference.
1772
+ # @see http://code.google.com/p/redis/wiki/HsetCommand HSET in the
1773
+ # Redis Command Reference.
782
1774
  def write_remote(att, value)
783
1775
  write_local(att, value)
784
1776
 
@@ -789,6 +1781,11 @@ module Ohm
789
1781
  end
790
1782
  end
791
1783
 
1784
+ # Wraps any missing constants lazily in {Ohm::Model::Wrapper} delaying
1785
+ # the evaluation of constants until they are actually needed.
1786
+ #
1787
+ # @see Ohm::Model::Wrapper
1788
+ # @see http://en.wikipedia.org/wiki/Lazy_evaluation Lazy evaluation
792
1789
  def self.const_missing(name)
793
1790
  wrapper = Wrapper.new(name) { const_get(name) }
794
1791
 
@@ -812,6 +1809,7 @@ module Ohm
812
1809
  Ohm.threaded[self] = connection
813
1810
  end
814
1811
 
1812
+ # Allows you to do key manipulations scoped solely to your class.
815
1813
  def self.key
816
1814
  Key.new(self, db)
817
1815
  end
@@ -820,6 +1818,26 @@ module Ohm
820
1818
  key[:all].sismember(id)
821
1819
  end
822
1820
 
1821
+ # The meat of the ID generation code for Ohm. For cases where you want to
1822
+ # customize ID generation (i.e. use GUIDs or Base62 ids) then you simply
1823
+ # override this method in your model.
1824
+ #
1825
+ # @example
1826
+ #
1827
+ # module UUID
1828
+ # def self.new
1829
+ # `uuidgen`.strip
1830
+ # end
1831
+ # end
1832
+ #
1833
+ # class Post < Ohm::Model
1834
+ #
1835
+ # private
1836
+ # def initialize_id
1837
+ # @id ||= UUID.new
1838
+ # end
1839
+ # end
1840
+ #
823
1841
  def initialize_id
824
1842
  @id ||= self.class.key[:id].incr.to_s
825
1843
  end
@@ -876,14 +1894,29 @@ module Ohm
876
1894
  key[:_indices].del
877
1895
  end
878
1896
 
1897
+ # Get the value of a specific attribute. An important fact about
1898
+ # attributes in Ohm is that they are all loaded lazily.
1899
+ #
1900
+ # @param [Symbol] att The attribute you you want to get.
1901
+ # @return [String] The value of att.
879
1902
  def read_local(att)
880
1903
  @_attributes[att]
881
1904
  end
882
1905
 
1906
+ # Write the value of an attribute locally, without persisting it.
1907
+ #
1908
+ # @param [Symbol] att The attribute you want to set.
1909
+ # @param [#to_s] value The value of the attribute you want to set.
883
1910
  def write_local(att, value)
884
1911
  @_attributes[att] = value
885
1912
  end
886
1913
 
1914
+ # Used internally be the @_attributes hash to lazily load attributes
1915
+ # when you need them. You may also use this in your code if you know what
1916
+ # you are doing.
1917
+ #
1918
+ # @param [Symbol] att The attribute you you want to get.
1919
+ # @return [String] The value of att.
887
1920
  def read_remote(att)
888
1921
  unless new?
889
1922
  value = key.hget(att)
@@ -893,23 +1926,48 @@ module Ohm
893
1926
  end
894
1927
  end
895
1928
 
1929
+ # Read attributes en masse locally.
896
1930
  def read_locals(attrs)
897
1931
  attrs.map do |att|
898
1932
  send(att)
899
1933
  end
900
1934
  end
901
1935
 
1936
+ # Read attributes en masse remotely.
902
1937
  def read_remotes(attrs)
903
1938
  attrs.map do |att|
904
1939
  read_remote(att)
905
1940
  end
906
1941
  end
907
1942
 
908
- def self.index_key_for(att, value)
909
- raise IndexNotFound, att unless indices.include?(att)
910
- key[att][encode(value)]
1943
+ # Get the index name for a specific index and value pair. The return value
1944
+ # is an instance of {Ohm::Key}, which you can readily do Redis operations
1945
+ # on.
1946
+ #
1947
+ # @example
1948
+ #
1949
+ # class Post < Ohm::Model
1950
+ # attribute :title
1951
+ # index :title
1952
+ # end
1953
+ #
1954
+ # post = Post.create(:title => "Foo")
1955
+ # key = Post.index_key_for(:title, "Foo")
1956
+ # key == "Post:title:Rm9v"
1957
+ # key.scard == 1
1958
+ # key.smembers == [post.id]
1959
+ # # => true
1960
+ #
1961
+ # @param [Symbol] name The name of the index.
1962
+ # @param [#to_s] value The value for the index.
1963
+ # @return [Ohm::Key] A {Ohm::Key key} which you can treat as a string,
1964
+ # but also do Redis operations on.
1965
+ def self.index_key_for(name, value)
1966
+ raise IndexNotFound, name unless indices.include?(name)
1967
+ key[name][encode(value)]
911
1968
  end
912
1969
 
1970
+ # Thin wrapper around {Ohm::Model.index_key_for}.
913
1971
  def index_key_for(att, value)
914
1972
  self.class.index_key_for(att, value)
915
1973
  end
@@ -935,8 +1993,9 @@ module Ohm
935
1993
  key[:_lock].del
936
1994
  end
937
1995
 
938
- def lock_expired? timestamp
1996
+ def lock_expired?(timestamp)
939
1997
  timestamp.to_f < Time.now.to_f
940
1998
  end
941
1999
  end
942
2000
  end
2001
+