ohm 0.0.34 → 0.0.35

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
@@ -1,14 +1,15 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  require "base64"
4
- require File.join(File.dirname(__FILE__), "ohm", "redis")
4
+ require "redis"
5
+
5
6
  require File.join(File.dirname(__FILE__), "ohm", "validations")
6
7
  require File.join(File.dirname(__FILE__), "ohm", "compat-1.8.6")
7
8
  require File.join(File.dirname(__FILE__), "ohm", "key")
8
9
  require File.join(File.dirname(__FILE__), "ohm", "collection")
9
10
 
10
11
  module Ohm
11
- VERSION = "0.0.34"
12
+ VERSION = "0.0.35"
12
13
 
13
14
  # Provides access to the Redis database. This is shared accross all models and instances.
14
15
  def redis
@@ -39,9 +40,9 @@ module Ohm
39
40
 
40
41
  # Return a connection to Redis.
41
42
  #
42
- # This is a wapper around Ohm::Redis.new(options)
43
+ # This is a wapper around Redis.new(options)
43
44
  def connection(*options)
44
- Ohm::Redis.new(*options)
45
+ Redis.new(*options)
45
46
  end
46
47
 
47
48
  def options
@@ -53,7 +54,6 @@ module Ohm
53
54
  redis.flushdb
54
55
  end
55
56
 
56
- # Join the parameters with ":" to create a key.
57
57
  def key(*args)
58
58
  Key[*args]
59
59
  end
@@ -63,15 +63,47 @@ module Ohm
63
63
  Error = Class.new(StandardError)
64
64
 
65
65
  class Model
66
+
67
+ # Wraps a model name for lazy evaluation.
68
+ class Wrapper < BasicObject
69
+ def initialize(name, &block)
70
+ @name = name
71
+ @caller = ::Kernel.caller[2]
72
+ @block = block
73
+
74
+ class << self
75
+ def method_missing(method_id, *args)
76
+ ::Kernel.raise ::NoMethodError, "You tried to call #{@name}##{method_id}, but #{@name} is not defined on #{@caller}"
77
+ end
78
+ end
79
+ end
80
+
81
+ def self.wrap(object)
82
+ object.class == self ? object : new(object.inspect) { object }
83
+ end
84
+
85
+ def unwrap
86
+ @block.call
87
+ end
88
+
89
+ def class
90
+ Wrapper
91
+ end
92
+
93
+ def inspect
94
+ "<Wrapper for #{@name} (in #{@caller})>"
95
+ end
96
+ end
97
+
66
98
  class Collection
67
99
  include Enumerable
68
100
 
69
101
  attr :raw
70
102
  attr :model
71
103
 
72
- def initialize(key, model, db = model.db)
73
- @raw = self.class::Raw.new(key, db)
74
- @model = model
104
+ def initialize(key, model, db = nil)
105
+ @model = model.unwrap
106
+ @raw = self.class::Raw.new(key, db || @model.db)
75
107
  end
76
108
 
77
109
  def <<(model)
@@ -197,7 +229,7 @@ module Ohm
197
229
  def apply(operation, hash, glue)
198
230
  target = key.volatile.group(glue).append(*keys(hash))
199
231
  model.db.send(operation, target, *target.sub_keys)
200
- Set.new(target, model)
232
+ Set.new(target, Wrapper.wrap(model))
201
233
  end
202
234
 
203
235
  # Transform a hash of attribute/values into an array of keys.
@@ -240,7 +272,7 @@ module Ohm
240
272
  class Index < Set
241
273
  def apply(operation, hash, glue)
242
274
  if hash.keys.size == 1
243
- return Set.new(keys(hash).first, model)
275
+ return Set.new(keys(hash).first, Wrapper.wrap(model))
244
276
  else
245
277
  super
246
278
  end
@@ -390,6 +422,8 @@ module Ohm
390
422
  #
391
423
  # @see Ohm::Model::collection
392
424
  def self.reference(name, model)
425
+ model = Wrapper.wrap(model)
426
+
393
427
  reader = :"#{name}_id"
394
428
  writer = :"#{name}_id="
395
429
 
@@ -397,7 +431,7 @@ module Ohm
397
431
  index reader
398
432
 
399
433
  define_memoized_method(name) do
400
- model[send(reader)]
434
+ model.unwrap[send(reader)]
401
435
  end
402
436
 
403
437
  define_method(:"#{name}=") do |value|
@@ -451,7 +485,8 @@ module Ohm
451
485
  # @param model [Constant] Model where the reference is defined.
452
486
  # @param reference [Symbol] Reference as defined in the associated model.
453
487
  def self.collection(name, model, reference = to_reference)
454
- define_method(name) { model.find(:"#{reference}_id" => send(:id)) }
488
+ model = Wrapper.wrap(model)
489
+ define_method(name) { model.unwrap.find(:"#{reference}_id" => send(:id)) }
455
490
  end
456
491
 
457
492
  def self.to_reference
@@ -460,6 +495,7 @@ module Ohm
460
495
 
461
496
  def self.attr_collection_reader(name, type, model)
462
497
  if model
498
+ model = Wrapper.wrap(model)
463
499
  define_memoized_method(name) { Ohm::Model::const_get(type).new(key(name), model, db) }
464
500
  else
465
501
  define_memoized_method(name) { Ohm::const_get(type).new(key(name), db) }
@@ -482,7 +518,7 @@ module Ohm
482
518
  end
483
519
 
484
520
  def self.all
485
- @all ||= Ohm::Model::Index.new(key(:all), self)
521
+ @all ||= Ohm::Model::Index.new(key(:all), Wrapper.wrap(self))
486
522
  end
487
523
 
488
524
  def self.attributes
@@ -659,35 +695,20 @@ module Ohm
659
695
  self.class.key(id, *args)
660
696
  end
661
697
 
662
- # Use MSET if possible, SET otherwise.
663
- def write
664
- db.support_mset? ?
665
- write_with_mset :
666
- write_with_set
667
- end
668
-
669
- # Write attributes using SET
670
- # This method will be removed once MSET becomes standard.
671
- def write_with_set
672
- attributes.each do |att|
673
- value = send(att)
674
- value.to_s.empty? ?
675
- db.set(key(att), value) :
676
- db.del(key(att))
677
- end
678
- end
679
-
680
698
  # Write attributes using MSET
681
- # This is the preferred method, and will be the only option
682
- # available once MSET becomes standard.
683
- def write_with_mset
699
+ def write
684
700
  unless attributes.empty?
685
701
  rems, adds = attributes.map { |a| [key(a), send(a)] }.partition { |t| t.last.to_s.empty? }
702
+
686
703
  db.del(*rems.flatten.compact) unless rems.empty?
687
- db.mset(adds.flatten) unless adds.empty?
704
+ db.mapped_mset(adds.flatten) unless adds.empty?
688
705
  end
689
706
  end
690
707
 
708
+ def self.const_missing(name)
709
+ Wrapper.new(name) { const_get(name) }
710
+ end
711
+
691
712
  private
692
713
 
693
714
  # Provides access to the Redis database. This is shared accross all models and instances.
@@ -759,6 +780,7 @@ module Ohm
759
780
  def delete_from_indices
760
781
  db.smembers(key(:_indices)).each do |index|
761
782
  db.srem(index, id)
783
+ db.srem(key(:_indices), index)
762
784
  end
763
785
  end
764
786
 
@@ -771,7 +793,12 @@ module Ohm
771
793
  end
772
794
 
773
795
  def read_remote(att)
774
- db.get(key(att)) unless new?
796
+ unless new?
797
+ value = db.get(key(att))
798
+ value.respond_to?(:force_encoding) ?
799
+ value.force_encoding("UTF-8") :
800
+ value
801
+ end
775
802
  end
776
803
 
777
804
  def read_locals(attrs)
@@ -124,7 +124,7 @@ module Ohm
124
124
 
125
125
  # @return [Array] Elements of the list.
126
126
  def all
127
- db.list(key)
127
+ db.lrange(key, 0, -1)
128
128
  end
129
129
 
130
130
  # @return [Integer] Returns the number of elements in the list.
@@ -14,7 +14,7 @@ unless "".respond_to?(:lines)
14
14
  end
15
15
  end
16
16
 
17
- unless Object.new.respond_to?(:tap)
17
+ unless respond_to?(:tap)
18
18
  class Object
19
19
  def tap
20
20
  yield(self)
@@ -22,3 +22,18 @@ unless Object.new.respond_to?(:tap)
22
22
  end
23
23
  end
24
24
  end
25
+
26
+ module Ohm
27
+ if defined?(BasicObject)
28
+ BasicObject = ::BasicObject
29
+ elsif defined?(BlankSlate)
30
+ BasicObject = ::BlankSlate
31
+ else
32
+
33
+ # If neither BasicObject (Ruby 1.9) nor BlankSlate (typically provided by Builder)
34
+ # are present, define our simple implementation inside the Ohm module.
35
+ class BasicObject
36
+ instance_methods.each { |meth| undef_method(meth) unless meth =~ /\A(__|instance_eval)/ }
37
+ end
38
+ end
39
+ end
data/test/benchmarks.rb CHANGED
@@ -7,55 +7,33 @@ Ohm.flush
7
7
 
8
8
  class Event < Ohm::Model
9
9
  attribute :name
10
- set :attendees
10
+ attribute :location
11
+
12
+ index :name
13
+ index :location
11
14
 
12
15
  def validate
13
16
  assert_present :name
17
+ assert_present :location
14
18
  end
15
19
  end
16
20
 
17
- event = Event.create(:name => "Ruby Tuesday")
18
- array = []
19
-
20
- benchmark "add to set with ohm redis" do
21
- Ohm.redis.sadd("foo", 1)
22
- end
23
-
24
- benchmark "add to set with ohm" do
25
- event.attendees << 1
26
- end
27
-
28
- Ohm.redis.sadd("bar", 1)
29
- Ohm.redis.sadd("bar", 2)
30
-
31
- benchmark "retrieve a set of two members with ohm redis" do
32
- Ohm.redis.sadd("bar", 3)
33
- Ohm.redis.srem("bar", 3)
34
- Ohm.redis.smembers("bar")
35
- end
36
-
37
- Ohm.redis.del("Event:#{event.id}:attendees")
38
-
39
- event.attendees << 1
40
- event.attendees << 2
21
+ i = 0
41
22
 
42
- benchmark "retrieve a set of two members with ohm" do
43
- event.attendees << 3
44
- event.attendees.delete(3)
45
- event.attendees
23
+ benchmark "Create Events" do
24
+ Event.create(:name => "Redis Meetup #{i}", :location => "London #{i}")
46
25
  end
47
26
 
48
- benchmark "retrieve membership status and set count" do
49
- Ohm.redis.scard("bar")
50
- Ohm.redis.sismember("bar", "1")
27
+ benchmark "Find by indexed attribute" do
28
+ Event.find(:name => "Redis Meetup #{i}").first
51
29
  end
52
30
 
53
- benchmark "retrieve set count" do
54
- Ohm.redis.scard("bar").zero?
31
+ benchmark "Mass update" do
32
+ Event[1].update(:name => "Redis Meetup II")
55
33
  end
56
34
 
57
- benchmark "retrieve membership status" do
58
- Ohm.redis.sismember("bar", "1")
35
+ benchmark "Load events" do
36
+ Event[1].name
59
37
  end
60
38
 
61
- run 10_000
39
+ run 5000
data/test/model_test.rb CHANGED
@@ -41,6 +41,10 @@ class Event < Ohm::Model
41
41
  end
42
42
 
43
43
  class TestRedis < Test::Unit::TestCase
44
+ setup do
45
+ Ohm.flush
46
+ end
47
+
44
48
  context "An event initialized with a hash of attributes" do
45
49
  should "assign the passed attributes" do
46
50
  event = Event.new(:name => "Ruby Tuesday")
@@ -50,8 +54,6 @@ class TestRedis < Test::Unit::TestCase
50
54
 
51
55
  context "An event created from a hash of attributes" do
52
56
  should "assign an id and save the object" do
53
- Ohm.flush
54
-
55
57
  event1 = Event.create(:name => "Ruby Tuesday")
56
58
  event2 = Event.create(:name => "Ruby Meetup")
57
59
 
@@ -218,8 +220,6 @@ class TestRedis < Test::Unit::TestCase
218
220
 
219
221
  context "Creating a new model" do
220
222
  should "assign a new id to the event" do
221
- Ohm.flush
222
-
223
223
  event1 = Event.new
224
224
  event1.create
225
225
 
@@ -259,20 +259,18 @@ class TestRedis < Test::Unit::TestCase
259
259
  end
260
260
 
261
261
  context "Delete" do
262
- class ModelToBeDeleted < Ohm::Model
263
- attribute :name
264
- set :foos
265
- list :bars
266
- end
262
+ should "delete an existing model" do
263
+ class ModelToBeDeleted < Ohm::Model
264
+ attribute :name
265
+ set :foos
266
+ list :bars
267
+ end
267
268
 
268
- setup do
269
269
  @model = ModelToBeDeleted.create(:name => "Lorem")
270
270
 
271
271
  @model.foos << "foo"
272
272
  @model.bars << "bar"
273
- end
274
273
 
275
- should "delete an existing model" do
276
274
  id = @model.id
277
275
 
278
276
  @model.delete
@@ -280,10 +278,27 @@ class TestRedis < Test::Unit::TestCase
280
278
  assert_nil Ohm.redis.get(ModelToBeDeleted.key(id))
281
279
  assert_nil Ohm.redis.get(ModelToBeDeleted.key(id, :name))
282
280
  assert_equal Array.new, Ohm.redis.smembers(ModelToBeDeleted.key(id, :foos))
283
- assert_equal Array.new, Ohm.redis.list(ModelToBeDeleted.key(id, :bars))
281
+ assert_equal Array.new, Ohm.redis.lrange(ModelToBeDeleted.key(id, :bars), 0, -1)
284
282
 
285
283
  assert ModelToBeDeleted.all.empty?
286
284
  end
285
+
286
+ should "be no leftover keys" do
287
+ class ::Foo < Ohm::Model
288
+ attribute :name
289
+ index :name
290
+ end
291
+
292
+ assert_equal [], Ohm.redis.keys("*")
293
+
294
+ Foo.create(:name => "Bar")
295
+
296
+ assert_equal ["Foo:1:_indices", "Foo:1:name", "Foo:all", "Foo:id", "Foo:name:QmFy"], Ohm.redis.keys("*").sort
297
+
298
+ Foo[1].delete
299
+
300
+ assert_equal ["Foo:id"], Ohm.redis.keys("*")
301
+ end
287
302
  end
288
303
 
289
304
  context "Listing" do
@@ -305,7 +320,6 @@ class TestRedis < Test::Unit::TestCase
305
320
 
306
321
  context "Sorting" do
307
322
  should "sort all" do
308
- Ohm.flush
309
323
  Person.create :name => "D"
310
324
  Person.create :name => "C"
311
325
  Person.create :name => "B"
@@ -315,26 +329,22 @@ class TestRedis < Test::Unit::TestCase
315
329
  end
316
330
 
317
331
  should "return an empty array if there are no elements to sort" do
318
- Ohm.flush
319
332
  assert_equal [], Person.all.sort_by(:name)
320
333
  end
321
334
 
322
335
  should "return the first element sorted by id when using first" do
323
- Ohm.flush
324
336
  Person.create :name => "A"
325
337
  Person.create :name => "B"
326
338
  assert_equal "A", Person.all.first.name
327
339
  end
328
340
 
329
341
  should "return the first element sorted by name if first receives a sorting option" do
330
- Ohm.flush
331
342
  Person.create :name => "B"
332
343
  Person.create :name => "A"
333
344
  assert_equal "A", Person.all.first(:by => :name, :order => "ALPHA").name
334
345
  end
335
346
 
336
347
  should "return attribute values when the get parameter is specified" do
337
- Ohm.flush
338
348
  Person.create :name => "B"
339
349
  Person.create :name => "A"
340
350
 
@@ -344,8 +354,6 @@ class TestRedis < Test::Unit::TestCase
344
354
 
345
355
  context "Loading attributes" do
346
356
  setup do
347
- Ohm.flush
348
-
349
357
  event = Event.new
350
358
  event.name = "Ruby Tuesday"
351
359
  @id = event.create.id
@@ -367,8 +375,6 @@ class TestRedis < Test::Unit::TestCase
367
375
 
368
376
  context "Attributes of type Set" do
369
377
  setup do
370
- Ohm.flush
371
-
372
378
  @person1 = Person.create(:name => "Albert")
373
379
  @person2 = Person.create(:name => "Bertrand")
374
380
  @person3 = Person.create(:name => "Charles")
@@ -450,8 +456,6 @@ class TestRedis < Test::Unit::TestCase
450
456
 
451
457
  context "Attributes of type List" do
452
458
  setup do
453
- Ohm.flush
454
-
455
459
  @post = Post.new
456
460
  @post.body = "Hello world!"
457
461
  @post.create
@@ -585,13 +589,19 @@ class TestRedis < Test::Unit::TestCase
585
589
  end
586
590
  end
587
591
 
588
- class Calendar < Ohm::Model
592
+ class ::Calendar < Ohm::Model
589
593
  list :holidays, lambda { |v| Date.parse(v) }
590
594
  list :subscribers, lambda { |id| MyActiveRecordModel.find(id) }
595
+ list :appointments, Appointment
596
+ end
597
+
598
+ class ::Appointment < Ohm::Model
599
+ attribute :text
591
600
  end
592
601
 
593
602
  setup do
594
603
  @calendar = Calendar.create
604
+
595
605
  @calendar.holidays.raw << "2009-05-25"
596
606
  @calendar.holidays.raw << "2009-07-09"
597
607
 
@@ -604,6 +614,12 @@ class TestRedis < Test::Unit::TestCase
604
614
  assert_equal ["1"], @calendar.subscribers.raw.all
605
615
  assert_equal [MyActiveRecordModel.find(1)], @calendar.subscribers.all
606
616
  end
617
+
618
+ should "work with models too" do
619
+ @calendar.appointments.add(Appointment.create(:text => "Meet with Bertrand"))
620
+
621
+ assert_equal [Appointment[1]], Calendar[1].appointments.sort
622
+ end
607
623
  end
608
624
 
609
625
  context "Sorting lists and sets" do
@@ -715,6 +731,18 @@ class TestRedis < Test::Unit::TestCase
715
731
  counter :visits
716
732
  set :friends
717
733
  list :comments
734
+
735
+ def foo
736
+ bar.foo
737
+ end
738
+
739
+ def baz
740
+ bar.new.foo
741
+ end
742
+
743
+ def bar
744
+ SomeMissingConstant
745
+ end
718
746
  end
719
747
 
720
748
  should "provide a meaningful inspect" do
@@ -730,9 +758,26 @@ class TestRedis < Test::Unit::TestCase
730
758
 
731
759
  assert_equal %Q{#<Bar:#{bar.id} name="Albert" friends=#<Set: ["1", "2"]> comments=#<List: ["A"]> visits=1>}, Bar[bar.id].inspect
732
760
  end
761
+
762
+ def assert_wrapper_exception(&block)
763
+ begin
764
+ block.call
765
+ rescue NoMethodError => exception_raised
766
+ end
767
+
768
+ assert_match /You tried to call SomeMissingConstant#\w+, but SomeMissingConstant is not defined on #{__FILE__}:\d+:in `bar'/, exception_raised.message
769
+ end
770
+
771
+ should "inform about a miscatch by Wrapper when calling class methods" do
772
+ assert_wrapper_exception { Bar.new.baz }
773
+ end
774
+
775
+ should "inform about a miscatch by Wrapper when calling instance methods" do
776
+ assert_wrapper_exception { Bar.new.foo }
777
+ end
733
778
  end
734
779
 
735
- context "Overwritting write" do
780
+ context "Overwriting write" do
736
781
  class ::Baz < Ohm::Model
737
782
  attribute :name
738
783
 
@@ -756,6 +801,16 @@ class TestRedis < Test::Unit::TestCase
756
801
  class ::Note < Ohm::Model
757
802
  attribute :content
758
803
  reference :source, Post
804
+ collection :comments, Comment
805
+ list :ratings, Rating
806
+ end
807
+
808
+ class ::Comment < Ohm::Model
809
+ reference :note, Note
810
+ end
811
+
812
+ class ::Rating < Ohm::Model
813
+ attribute :value
759
814
  end
760
815
 
761
816
  class ::Editor < Ohm::Model
@@ -813,6 +868,7 @@ class TestRedis < Test::Unit::TestCase
813
868
  context "a collection of other objects" do
814
869
  setup do
815
870
  @note = Note.create(:content => "Interesting stuff", :source => @post)
871
+ @comment = Comment.create(:note => @note)
816
872
  end
817
873
 
818
874
  should "return a set of notes" do
@@ -820,6 +876,17 @@ class TestRedis < Test::Unit::TestCase
820
876
  assert_equal @note, @post.notes.first
821
877
  end
822
878
 
879
+ should "return a set of comments" do
880
+ assert_equal @comment, @note.comments.first
881
+ end
882
+
883
+ should "return a list of ratings" do
884
+ @rating = Rating.create(:value => 5)
885
+ @note.ratings << @rating
886
+
887
+ assert_equal @rating, @note.ratings.first
888
+ end
889
+
823
890
  should "default to the current class name" do
824
891
  @editor = Editor.create(:name => "Albert", :post => @post)
825
892