ohm 0.0.34 → 0.0.35

Sign up to get free protection for your applications and to get access to all the features.
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