ohm 0.1.0.rc5 → 0.1.0.rc6

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -20,22 +20,11 @@ desc "Stop the Redis server"
20
20
  task :stop do
21
21
  if File.exists?(REDIS_PID)
22
22
  system "kill #{File.read(REDIS_PID)}"
23
- system "rm #{REDIS_PID}"
24
23
  end
25
24
  end
26
25
 
27
26
  task :test do
28
- require File.expand_path(File.join(File.dirname(__FILE__), "test", "test_helper"))
27
+ require File.expand_path("./test/helper", File.dirname(__FILE__))
29
28
 
30
- Dir["test/**/*_test.rb"].each_with_index do |file, index|
31
- ENV["REDIS_URL"] = "redis://127.0.0.1:6379/#{index}"
32
-
33
- fork do
34
- load file
35
- end
36
-
37
- exit $?.exitstatus unless $?.success?
38
- end
39
-
40
- Process.waitall
29
+ Cutest.run(Dir["test/*_test.rb"])
41
30
  end
data/lib/ohm.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "base64"
4
4
  require "redis"
5
+ require "nest"
5
6
 
6
7
  require File.join(File.dirname(__FILE__), "ohm", "pattern")
7
8
  require File.join(File.dirname(__FILE__), "ohm", "validations")
@@ -11,15 +12,15 @@ require File.join(File.dirname(__FILE__), "ohm", "key")
11
12
  module Ohm
12
13
 
13
14
  # Provides access to the Redis database. This is shared accross all models and instances.
14
- def redis
15
+ def self.redis
15
16
  threaded[:redis] ||= connection(*options)
16
17
  end
17
18
 
18
- def redis=(connection)
19
+ def self.redis=(connection)
19
20
  threaded[:redis] = connection
20
21
  end
21
22
 
22
- def threaded
23
+ def self.threaded
23
24
  Thread.current[:ohm] ||= {}
24
25
  end
25
26
 
@@ -32,31 +33,29 @@ module Ohm
32
33
  # @option options [#to_s] :timeout (0) Database timeout in seconds.
33
34
  # @example Connect to a database in port 6380.
34
35
  # Ohm.connect(:port => 6380)
35
- def connect(*options)
36
+ def self.connect(*options)
36
37
  self.redis = nil
37
38
  @options = options
38
39
  end
39
40
 
40
41
  # Return a connection to Redis.
41
42
  #
42
- # This is a wapper around Redis.new(options)
43
- def connection(*options)
44
- Redis.new(*options)
43
+ # This is a wapper around Redis.connect(options)
44
+ def self.connection(*options)
45
+ Redis.connect(*options)
45
46
  end
46
47
 
47
- def options
48
+ def self.options
48
49
  @options = [] unless defined? @options
49
50
  @options
50
51
  end
51
52
 
52
53
  # Clear the database.
53
- def flush
54
+ def self.flush
54
55
  redis.flushdb
55
56
  end
56
57
 
57
- module_function :connect, :connection, :flush, :redis, :redis=, :options, :threaded
58
-
59
- Error = Class.new(StandardError)
58
+ class Error < StandardError; end
60
59
 
61
60
  class Model
62
61
 
@@ -106,20 +105,14 @@ module Ohm
106
105
  self << model
107
106
  end
108
107
 
109
- def first(options = {})
110
- if options[:by]
111
- sort_by(options.delete(:by), options.merge(:limit => 1)).first
112
- else
113
- model[key.first(options)]
114
- end
115
- end
108
+ def sort(_options = {})
109
+ return [] unless key.exists
116
110
 
117
- def [](index)
118
- model[key[index]]
119
- end
111
+ options = _options.dup
112
+ options[:start] ||= 0
113
+ options[:limit] = [options[:start], options[:limit]] if options[:limit]
120
114
 
121
- def sort(*args)
122
- key.sort(*args).map(&model)
115
+ key.sort(options).map(&model)
123
116
  end
124
117
 
125
118
  # Sort the model instances by the given attribute.
@@ -132,11 +125,14 @@ module Ohm
132
125
  # user = User.all.sort_by(:name, :order => "ALPHA").first
133
126
  # user.name == "A"
134
127
  # # => true
135
- def sort_by(att, options = {})
136
- options.merge!(:by => model.key("*->#{att}"))
128
+ def sort_by(att, _options = {})
129
+ return [] unless key.exists
130
+
131
+ options = _options.dup
132
+ options.merge!(:by => model.key["*->#{att}"])
137
133
 
138
134
  if options[:get]
139
- key.sort(options.merge(:get => model.key("*->#{options[:get]}")))
135
+ key.sort(options.merge(:get => model.key["*->#{options[:get]}"]))
140
136
  else
141
137
  sort(options)
142
138
  end
@@ -146,14 +142,11 @@ module Ohm
146
142
  key.del
147
143
  end
148
144
 
149
- def concat(models)
150
- models.each { |model| add(model) }
151
- self
152
- end
153
-
154
145
  def replace(models)
155
- clear
156
- concat(models)
146
+ model.db.multi do
147
+ clear
148
+ models.each { |model| add(model) }
149
+ end
157
150
  end
158
151
 
159
152
  def empty?
@@ -204,38 +197,8 @@ module Ohm
204
197
  apply(:sdiffstore, key, source, target)
205
198
  end
206
199
 
207
- def sort(options = {})
208
- return [] unless key.exists
209
-
210
- options[:start] ||= 0
211
- options[:limit] = [options[:start], options[:limit]] if options[:limit]
212
-
213
- key.sort(options).map(&model)
214
- end
215
-
216
- # Sort the model instances by the given attribute.
217
- #
218
- # @example Sorting elements by name:
219
- #
220
- # User.create :name => "B"
221
- # User.create :name => "A"
222
- #
223
- # user = User.all.sort_by(:name, :order => "ALPHA").first
224
- # user.name == "A"
225
- # # => true
226
- def sort_by(att, options = {})
227
- return [] unless key.exists
228
-
229
- options.merge!(:by => model.key["*->#{att}"])
230
-
231
- if options[:get]
232
- key.sort(options.merge(:get => model.key["*->#{options[:get]}"]))
233
- else
234
- sort(options)
235
- end
236
- end
237
-
238
- def first(options = {})
200
+ def first(_options = {})
201
+ options = _options.dup
239
202
  options.merge!(:limit => 1)
240
203
 
241
204
  if options[:by]
@@ -314,13 +277,11 @@ module Ohm
314
277
  end
315
278
 
316
279
  def pop
317
- id = key.rpop
318
- model[id] if id
280
+ model[key.rpop]
319
281
  end
320
282
 
321
283
  def shift
322
- id = key.lpop
323
- model[id] if id
284
+ model[key.lpop]
324
285
  end
325
286
 
326
287
  def unshift(model)
@@ -570,7 +531,7 @@ module Ohm
570
531
  end
571
532
 
572
533
  def self.[](id)
573
- new(:id => id) if exists?(id)
534
+ new(:id => id) if id && exists?(id)
574
535
  end
575
536
 
576
537
  def self.to_proc
@@ -675,7 +636,7 @@ module Ohm
675
636
  # @param att [Symbol] Attribute to increment.
676
637
  def incr(att, count = 1)
677
638
  raise ArgumentError, "#{att.inspect} is not a counter." unless counters.include?(att)
678
- write_local(att, db.hincrby(key, att, count))
639
+ write_local(att, key.hincrby(att, count))
679
640
  end
680
641
 
681
642
  # Decrement the counter denoted by :att.
@@ -807,13 +768,13 @@ module Ohm
807
768
  atts = (attributes + counters).inject([]) { |ret, att|
808
769
  value = send(att).to_s
809
770
 
810
- ret.push(att, value) if not value.empty?
771
+ ret.push(att, value) if not value.empty?
811
772
  ret
812
773
  }
813
774
 
814
775
  db.multi do
815
- db.del(key)
816
- db.hmset(key, *atts.flatten) if atts.any?
776
+ key.del
777
+ key.hmset(*atts.flatten) if atts.any?
817
778
  end
818
779
  end
819
780
  end
@@ -822,9 +783,9 @@ module Ohm
822
783
  write_local(att, value)
823
784
 
824
785
  if value.to_s.empty?
825
- db.hdel(key, att)
786
+ key.hdel(att)
826
787
  else
827
- db.hset(key, att, value)
788
+ key.hset(att, value)
828
789
  end
829
790
  end
830
791
 
@@ -856,11 +817,11 @@ module Ohm
856
817
  end
857
818
 
858
819
  def self.exists?(id)
859
- db.sismember(key[:all], id)
820
+ key[:all].sismember(id)
860
821
  end
861
822
 
862
823
  def initialize_id
863
- self.id = db.incr(self.class.key[:id]).to_s
824
+ @id ||= self.class.key[:id].incr.to_s
864
825
  end
865
826
 
866
827
  def db
@@ -876,7 +837,7 @@ module Ohm
876
837
  end
877
838
 
878
839
  def delete_model_membership
879
- db.del(key)
840
+ key.del
880
841
  self.class.all.delete(self)
881
842
  end
882
843
 
@@ -903,16 +864,16 @@ module Ohm
903
864
 
904
865
  def add_to_index(att, value = send(att))
905
866
  index = index_key_for(att, value)
906
- db.sadd(index, id)
907
- db.sadd(key[:_indices], index)
867
+ index.sadd(id)
868
+ key[:_indices].sadd(index)
908
869
  end
909
870
 
910
871
  def delete_from_indices
911
- db.smembers(key[:_indices]).each do |index|
872
+ key[:_indices].smembers.each do |index|
912
873
  db.srem(index, id)
913
874
  end
914
875
 
915
- db.del(key[:_indices])
876
+ key[:_indices].del
916
877
  end
917
878
 
918
879
  def read_local(att)
@@ -925,7 +886,7 @@ module Ohm
925
886
 
926
887
  def read_remote(att)
927
888
  unless new?
928
- value = db.hget(key, att)
889
+ value = key.hget(att)
929
890
  value.respond_to?(:force_encoding) ?
930
891
  value.force_encoding("UTF-8") :
931
892
  value
@@ -959,27 +920,23 @@ module Ohm
959
920
  #
960
921
  # @see Model#mutex
961
922
  def lock!
962
- until db.setnx(key[:_lock], lock_timeout)
963
- next unless lock = db.get(key[:_lock])
964
- sleep(0.5) and next unless lock_expired?(lock)
923
+ until key[:_lock].setnx(Time.now.to_f + 0.5)
924
+ next unless timestamp = key[:_lock].get
925
+ sleep(0.1) and next unless lock_expired?(timestamp)
965
926
 
966
- break unless lock = db.getset(key[:_lock], lock_timeout)
967
- break if lock_expired?(lock)
927
+ break unless timestamp = key[:_lock].getset(Time.now.to_f + 0.5)
928
+ break if lock_expired?(timestamp)
968
929
  end
969
930
  end
970
931
 
971
932
  # Release the lock.
972
933
  # @see Model#mutex
973
934
  def unlock!
974
- db.del(key[:_lock])
975
- end
976
-
977
- def lock_timeout
978
- Time.now.to_f + 1
935
+ key[:_lock].del
979
936
  end
980
937
 
981
- def lock_expired? lock
982
- lock.to_f < Time.now.to_f
938
+ def lock_expired? timestamp
939
+ timestamp.to_f < Time.now.to_f
983
940
  end
984
941
  end
985
942
  end
@@ -1,44 +1,17 @@
1
1
  module Ohm
2
2
 
3
3
  # Represents a key in Redis.
4
- class Key < String
5
- attr :redis
6
-
7
- def initialize(name, redis = nil)
8
- @redis = redis
9
- super(name.to_s)
10
- end
11
-
12
- def [](key)
13
- self.class.new("#{self}:#{key}", @redis)
14
- end
15
-
4
+ class Key < Nest
16
5
  def volatile
17
- self.index("~") == 0 ? self : self.class.new("~", @redis)[self]
6
+ self.index("~") == 0 ? self : self.class.new("~", redis)[self]
18
7
  end
19
8
 
20
9
  def +(other)
21
- self.class.new("#{self}+#{other}", @redis)
10
+ self.class.new("#{self}+#{other}", redis)
22
11
  end
23
12
 
24
13
  def -(other)
25
- self.class.new("#{self}-#{other}", @redis)
26
- end
27
-
28
- [:append, :blpop, :brpop, :decr, :decrby, :del, :exists, :expire,
29
- :expireat, :get, :getset, :hdel, :hexists, :hget, :hgetall,
30
- :hincrby, :hkeys, :hlen, :hmget, :hmset, :hset, :hvals, :incr,
31
- :incrby, :lindex, :llen, :lpop, :lpush, :lrange, :lrem, :lset,
32
- :ltrim, :move, :rename, :renamenx, :rpop, :rpoplpush, :rpush,
33
- :sadd, :scard, :sdiff, :sdiffstore, :set, :setex, :setnx, :sinter,
34
- :sinterstore, :sismember, :smembers, :smove, :sort, :spop,
35
- :srandmember, :srem, :substr, :sunion, :sunionstore, :ttl, :type,
36
- :zadd, :zcard, :zincrby, :zinterstore, :zrange, :zrangebyscore,
37
- :zrank, :zrem, :zremrangebyrank, :zremrangebyscore, :zrevrange,
38
- :zrevrank, :zscore, :zunionstore].each do |meth|
39
- define_method(meth) do |*args|
40
- redis.send(meth, self, *args)
41
- end
14
+ self.class.new("#{self}-#{other}", redis)
42
15
  end
43
16
  end
44
17
  end
@@ -1,5 +1,5 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  module Ohm
4
- VERSION = "0.1.0.rc5"
4
+ VERSION = "0.1.0.rc6"
5
5
  end
@@ -1,25 +1,25 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))
1
+ # encoding: UTF-8
2
2
 
3
- class TestString < Test::Unit::TestCase
4
- context "String#lines" do
5
- should "return the parts when separated with \\n" do
6
- assert_equal ["a\n", "b\n", "c\n"], "a\nb\nc\n".lines.to_a
7
- end
3
+ require File.expand_path("./helper", File.dirname(__FILE__))
8
4
 
9
- should "return the parts when separated with \\r\\n" do
10
- assert_equal ["a\r\n", "b\r\n", "c\r\n"], "a\r\nb\r\nc\r\n".lines.to_a
11
- end
5
+ prepare.clear
12
6
 
13
- should "accept a record separator" do
14
- assert_equal ["ax", "bx", "cx"], "axbxcx".lines("x").to_a
15
- end
7
+ test "String#lines should return the parts when separated with \\n" do
8
+ assert ["a\n", "b\n", "c\n"] == "a\nb\nc\n".lines.to_a
9
+ end
10
+
11
+ test "String#lines return the parts when separated with \\r\\n" do
12
+ assert ["a\r\n", "b\r\n", "c\r\n"] == "a\r\nb\r\nc\r\n".lines.to_a
13
+ end
14
+
15
+ test "String#lines accept a record separator" do
16
+ assert ["ax", "bx", "cx"] == "axbxcx".lines("x").to_a
17
+ end
16
18
 
17
- should "execute the passed block" do
18
- lines = ["a\r\n", "b\r\n", "c\r\n"]
19
+ test "String#lines execute the passed block" do
20
+ lines = ["a\r\n", "b\r\n", "c\r\n"]
19
21
 
20
- "a\r\nb\r\nc\r\n".lines do |line|
21
- assert_equal lines.shift, line
22
- end
23
- end
22
+ "a\r\nb\r\nc\r\n".lines do |line|
23
+ assert lines.shift == line
24
24
  end
25
25
  end
@@ -0,0 +1,100 @@
1
+ # encoding: UTF-8
2
+
3
+ require File.expand_path("./helper", File.dirname(__FILE__))
4
+
5
+ class Person < Ohm::Model
6
+ attribute :name
7
+ end
8
+
9
+ class ::Note < Ohm::Model
10
+ attribute :content
11
+ reference :source, Post
12
+ collection :comments, Comment
13
+ list :ratings, Rating
14
+ end
15
+
16
+ class ::Comment < Ohm::Model
17
+ reference :note, Note
18
+ end
19
+
20
+ class ::Rating < Ohm::Model
21
+ attribute :value
22
+ end
23
+
24
+ class ::Editor < Ohm::Model
25
+ attribute :name
26
+ reference :post, Post
27
+ end
28
+
29
+ class ::Post < Ohm::Model
30
+ reference :author, Person
31
+ collection :notes, Note, :source
32
+ collection :editors, Editor
33
+ end
34
+
35
+ setup do
36
+ @post = Post.create
37
+ end
38
+
39
+ test "return an instance of Person if author_id has a valid id" do
40
+ @post.author_id = Person.create(:name => "Albert").id
41
+ @post.save
42
+ assert "Albert" == Post[@post.id].author.name
43
+ end
44
+
45
+ test "assign author_id if author is sent a valid instance" do
46
+ @post.author = Person.create(:name => "Albert")
47
+ @post.save
48
+ assert "Albert" == Post[@post.id].author.name
49
+ end
50
+
51
+ test "assign nil if nil is passed to author" do
52
+ @post.author = nil
53
+ @post.save
54
+ assert Post[@post.id].author.nil?
55
+ end
56
+
57
+ test "be cached in an instance variable" do
58
+ @author = Person.create(:name => "Albert")
59
+ @post.update(:author => @author)
60
+
61
+ assert @author == @post.author
62
+ assert @post.author.object_id == @post.author.object_id
63
+
64
+ @post.update(:author => Person.create(:name => "Bertrand"))
65
+
66
+ assert "Bertrand" == @post.author.name
67
+ assert @post.author.object_id == @post.author.object_id
68
+
69
+ @post.update(:author_id => Person.create(:name => "Charles").id)
70
+
71
+ assert "Charles" == @post.author.name
72
+ end
73
+
74
+ setup do
75
+ @post = Post.create
76
+ @note = Note.create(:content => "Interesting stuff", :source => @post)
77
+ @comment = Comment.create(:note => @note)
78
+ end
79
+
80
+ test "return a set of notes" do
81
+ assert @note.source == @post
82
+ assert @note == @post.notes.first
83
+ end
84
+
85
+ test "return a set of comments" do
86
+ assert @comment == @note.comments.first
87
+ end
88
+
89
+ test "return a list of ratings" do
90
+ @rating = Rating.create(:value => 5)
91
+ @note.ratings << @rating
92
+
93
+ assert @rating == @note.ratings.first
94
+ end
95
+
96
+ test "default to the current class name" do
97
+ @editor = Editor.create(:name => "Albert", :post => @post)
98
+
99
+ assert @editor == @post.editors.first
100
+ end