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 +2 -13
- data/lib/ohm.rb +54 -97
- data/lib/ohm/key.rb +4 -31
- data/lib/ohm/version.rb +1 -1
- data/test/1.8.6_test.rb +18 -18
- data/test/associations_test.rb +100 -0
- data/test/connection_test.rb +33 -21
- data/test/errors_test.rb +88 -88
- data/test/hash_key_test.rb +16 -23
- data/test/helper.rb +25 -0
- data/test/indices_test.rb +177 -183
- data/test/json_test.rb +67 -0
- data/test/model_test.rb +675 -868
- data/test/mutex_test.rb +65 -70
- data/test/pattern_test.rb +8 -6
- data/test/upgrade_script_test.rb +41 -47
- data/test/validations_test.rb +180 -164
- data/test/wrapper_test.rb +7 -5
- metadata +26 -10
- data/test/all_tests.rb +0 -2
- data/test/test_helper.rb +0 -53
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.
|
27
|
+
require File.expand_path("./test/helper", File.dirname(__FILE__))
|
29
28
|
|
30
|
-
Dir["test
|
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.
|
43
|
-
def connection(*options)
|
44
|
-
Redis.
|
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
|
-
|
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
|
110
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
111
|
+
options = _options.dup
|
112
|
+
options[:start] ||= 0
|
113
|
+
options[:limit] = [options[:start], options[:limit]] if options[:limit]
|
120
114
|
|
121
|
-
|
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,
|
136
|
-
|
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
|
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
|
-
|
156
|
-
|
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
|
208
|
-
|
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
|
-
|
318
|
-
model[id] if id
|
280
|
+
model[key.rpop]
|
319
281
|
end
|
320
282
|
|
321
283
|
def shift
|
322
|
-
|
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,
|
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)
|
771
|
+
ret.push(att, value) if not value.empty?
|
811
772
|
ret
|
812
773
|
}
|
813
774
|
|
814
775
|
db.multi do
|
815
|
-
|
816
|
-
|
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
|
-
|
786
|
+
key.hdel(att)
|
826
787
|
else
|
827
|
-
|
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
|
-
|
820
|
+
key[:all].sismember(id)
|
860
821
|
end
|
861
822
|
|
862
823
|
def initialize_id
|
863
|
-
|
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
|
-
|
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
|
-
|
907
|
-
|
867
|
+
index.sadd(id)
|
868
|
+
key[:_indices].sadd(index)
|
908
869
|
end
|
909
870
|
|
910
871
|
def delete_from_indices
|
911
|
-
|
872
|
+
key[:_indices].smembers.each do |index|
|
912
873
|
db.srem(index, id)
|
913
874
|
end
|
914
875
|
|
915
|
-
|
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 =
|
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
|
963
|
-
next unless
|
964
|
-
sleep(0.
|
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
|
967
|
-
break if lock_expired?(
|
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
|
-
|
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?
|
982
|
-
|
938
|
+
def lock_expired? timestamp
|
939
|
+
timestamp.to_f < Time.now.to_f
|
983
940
|
end
|
984
941
|
end
|
985
942
|
end
|
data/lib/ohm/key.rb
CHANGED
@@ -1,44 +1,17 @@
|
|
1
1
|
module Ohm
|
2
2
|
|
3
3
|
# Represents a key in Redis.
|
4
|
-
class Key <
|
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("~",
|
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}",
|
10
|
+
self.class.new("#{self}+#{other}", redis)
|
22
11
|
end
|
23
12
|
|
24
13
|
def -(other)
|
25
|
-
self.class.new("#{self}-#{other}",
|
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
|
data/lib/ohm/version.rb
CHANGED
data/test/1.8.6_test.rb
CHANGED
@@ -1,25 +1,25 @@
|
|
1
|
-
|
1
|
+
# encoding: UTF-8
|
2
2
|
|
3
|
-
|
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
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
18
|
-
|
19
|
+
test "String#lines execute the passed block" do
|
20
|
+
lines = ["a\r\n", "b\r\n", "c\r\n"]
|
19
21
|
|
20
|
-
|
21
|
-
|
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
|