ohm 0.1.0.rc4 → 0.1.0.rc5

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -25,13 +25,17 @@ task :stop do
25
25
  end
26
26
 
27
27
  task :test do
28
- Dir["test/**/*_test.rb"].each do |file|
28
+ require File.expand_path(File.join(File.dirname(__FILE__), "test", "test_helper"))
29
+
30
+ Dir["test/**/*_test.rb"].each_with_index do |file, index|
31
+ ENV["REDIS_URL"] = "redis://127.0.0.1:6379/#{index}"
32
+
29
33
  fork do
30
34
  load file
31
35
  end
32
36
 
33
- Process.wait
34
-
35
37
  exit $?.exitstatus unless $?.success?
36
38
  end
39
+
40
+ Process.waitall
37
41
  end
data/lib/ohm/key.rb CHANGED
@@ -2,27 +2,43 @@ module Ohm
2
2
 
3
3
  # Represents a key in Redis.
4
4
  class Key < String
5
- Volatile = new("~")
5
+ attr :redis
6
6
 
7
- def self.[](*args)
8
- new(args.join(":"))
7
+ def initialize(name, redis = nil)
8
+ @redis = redis
9
+ super(name.to_s)
9
10
  end
10
11
 
11
12
  def [](key)
12
- self.class[self, key]
13
+ self.class.new("#{self}:#{key}", @redis)
13
14
  end
14
15
 
15
16
  def volatile
16
- self.index(Volatile) == 0 ? self : Volatile[self]
17
+ self.index("~") == 0 ? self : self.class.new("~", @redis)[self]
17
18
  end
18
19
 
19
20
  def +(other)
20
- self.class.new("#{self}+#{other}")
21
+ self.class.new("#{self}+#{other}", @redis)
21
22
  end
22
23
 
23
24
  def -(other)
24
- self.class.new("#{self}-#{other}")
25
+ self.class.new("#{self}-#{other}", @redis)
25
26
  end
26
- end
27
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
42
+ end
43
+ end
28
44
  end
@@ -0,0 +1,37 @@
1
+ # Pattern extends Array with case equality to provide meaningful semantics in
2
+ # case statements.
3
+ #
4
+ # After this change, pattern matching-like behavior is possible with
5
+ # Arrays:
6
+ #
7
+ # Pattern[Fixnum, String] === [1, "foo"]
8
+ # Pattern[Symbol, Array] === [:foo, [1, 2]]
9
+ #
10
+ # When used in a case statement, it provides a functionality close to that of
11
+ # languages with proper pattern matching. It becomes useful when implementing
12
+ # a polymorphic method:
13
+ #
14
+ # def [](index, limit = nil)
15
+ # case [index, limit]
16
+ # when Pattern[Fixnum, Fixnum] then
17
+ # key.lrange(index, limit).collect { |id| model[id] }
18
+ # when Pattern[Range, nil] then
19
+ # key.lrange(index.first, index.last).collect { |id| model[id] }
20
+ # when Pattern[Fixnum, nil] then
21
+ # model[key.lindex(index)]
22
+ # end
23
+ # end
24
+ #
25
+ module Ohm
26
+ class Pattern < Array
27
+ def ===(other)
28
+ return false if size != other.size
29
+
30
+ each_with_index do |item, index|
31
+ return false unless item === other[index]
32
+ end
33
+
34
+ true
35
+ end
36
+ end
37
+ end
@@ -21,11 +21,11 @@ module Ohm
21
21
 
22
22
  def run
23
23
  models.each do |model|
24
- ns = Ohm::Key[model]
24
+ ns = Ohm::Key.new(model, redis)
25
25
 
26
26
  puts "Upgrading #{model}..."
27
27
 
28
- Batch.each(redis.smembers(ns[:all])) do |id|
28
+ Batch.each(ns[:all].smembers) do |id|
29
29
  instance = ns[id]
30
30
 
31
31
  attrs = []
data/lib/ohm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  module Ohm
4
- VERSION = "0.1.0.rc4"
4
+ VERSION = "0.1.0.rc5"
5
5
  end
data/lib/ohm.rb CHANGED
@@ -3,10 +3,10 @@
3
3
  require "base64"
4
4
  require "redis"
5
5
 
6
+ require File.join(File.dirname(__FILE__), "ohm", "pattern")
6
7
  require File.join(File.dirname(__FILE__), "ohm", "validations")
7
8
  require File.join(File.dirname(__FILE__), "ohm", "compat-1.8.6")
8
9
  require File.join(File.dirname(__FILE__), "ohm", "key")
9
- require File.join(File.dirname(__FILE__), "ohm", "collection")
10
10
 
11
11
  module Ohm
12
12
 
@@ -45,7 +45,8 @@ module Ohm
45
45
  end
46
46
 
47
47
  def options
48
- @options || []
48
+ @options = [] unless defined? @options
49
+ @options
49
50
  end
50
51
 
51
52
  # Clear the database.
@@ -53,11 +54,7 @@ module Ohm
53
54
  redis.flushdb
54
55
  end
55
56
 
56
- def key(*args)
57
- Key[*args]
58
- end
59
-
60
- module_function :key, :connect, :connection, :flush, :redis, :redis=, :options, :threaded
57
+ module_function :connect, :connection, :flush, :redis, :redis=, :options, :threaded
61
58
 
62
59
  Error = Class.new(StandardError)
63
60
 
@@ -97,44 +94,32 @@ module Ohm
97
94
  class Collection
98
95
  include Enumerable
99
96
 
100
- attr :raw
97
+ attr :key
101
98
  attr :model
102
99
 
103
- def initialize(key, model, db = nil)
100
+ def initialize(key, model)
101
+ @key = key
104
102
  @model = model.unwrap
105
- @raw = self.class::Raw.new(key, db || @model.db)
106
103
  end
107
104
 
108
- def <<(model)
109
- raw << model.id
110
- end
111
-
112
- alias add <<
113
-
114
- def each(&block)
115
- raw.each do |id|
116
- block.call(model[id])
117
- end
118
- end
119
-
120
- def key
121
- raw.key
105
+ def add(model)
106
+ self << model
122
107
  end
123
108
 
124
109
  def first(options = {})
125
110
  if options[:by]
126
111
  sort_by(options.delete(:by), options.merge(:limit => 1)).first
127
112
  else
128
- model[raw.first(options)]
113
+ model[key.first(options)]
129
114
  end
130
115
  end
131
116
 
132
117
  def [](index)
133
- model[raw[index]]
118
+ model[key[index]]
134
119
  end
135
120
 
136
121
  def sort(*args)
137
- raw.sort(*args).map(&model)
122
+ key.sort(*args).map(&model)
138
123
  end
139
124
 
140
125
  # Sort the model instances by the given attribute.
@@ -151,84 +136,127 @@ module Ohm
151
136
  options.merge!(:by => model.key("*->#{att}"))
152
137
 
153
138
  if options[:get]
154
- raw.sort(options.merge(:get => model.key("*->#{options[:get]}")))
139
+ key.sort(options.merge(:get => model.key("*->#{options[:get]}")))
155
140
  else
156
141
  sort(options)
157
142
  end
158
143
  end
159
144
 
160
- def delete(model)
161
- raw.delete(model.id)
162
- model
163
- end
164
-
165
145
  def clear
166
- raw.clear
146
+ key.del
167
147
  end
168
148
 
169
149
  def concat(models)
170
- raw.concat(models.map { |model| model.id })
150
+ models.each { |model| add(model) }
171
151
  self
172
152
  end
173
153
 
174
154
  def replace(models)
175
- raw.replace(models.map { |model| model.id })
176
- self
155
+ clear
156
+ concat(models)
177
157
  end
178
158
 
179
- def include?(model)
180
- raw.include?(model.id)
159
+ def empty?
160
+ !key.exists
181
161
  end
182
162
 
183
- def empty?
184
- raw.empty?
163
+ def to_a
164
+ all
165
+ end
166
+ end
167
+
168
+ class Set < Collection
169
+ def each(&block)
170
+ key.smembers.each { |id| block.call(model[id]) }
171
+ end
172
+
173
+ def [](id)
174
+ model[id] if key.sismember(id)
175
+ end
176
+
177
+ def << model
178
+ key.sadd(model.id)
185
179
  end
186
180
 
181
+ alias add <<
182
+
187
183
  def size
188
- raw.size
184
+ key.scard
185
+ end
186
+
187
+ def delete(member)
188
+ key.srem(member.id)
189
189
  end
190
190
 
191
191
  def all
192
- raw.to_a.map(&model)
192
+ key.smembers.map(&model)
193
193
  end
194
194
 
195
- alias to_a all
196
- end
195
+ def find(options)
196
+ source = keys(options)
197
+ target = source.inject(key.volatile) { |chain, other| chain + other }
198
+ apply(:sinterstore, key, source, target)
199
+ end
197
200
 
198
- class Set < Collection
199
- Raw = Ohm::Set
201
+ def except(options)
202
+ source = keys(options)
203
+ target = source.inject(key.volatile) { |chain, other| chain - other }
204
+ apply(:sdiffstore, key, source, target)
205
+ end
200
206
 
201
- def inspect
202
- "#<Set (#{model}): #{all.inspect}>"
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)
203
214
  end
204
215
 
205
- # Returns an intersection with the sets generated from the passed hash.
216
+ # Sort the model instances by the given attribute.
206
217
  #
207
- # @see Ohm::Model.find
208
- # @example
209
- # @events = Event.find(public: true)
218
+ # @example Sorting elements by name:
219
+ #
220
+ # User.create :name => "B"
221
+ # User.create :name => "A"
210
222
  #
211
- # # You can combine the result with sort and other set operations:
212
- # @events.sort_by(:name)
213
- def find(hash)
214
- apply(:sinterstore, hash, :+)
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
215
236
  end
216
237
 
217
- # Returns the difference between the receiver and the passed sets.
218
- #
219
- # @example
220
- # @events = Event.find(public: true).except(status: "sold_out")
221
- def except(hash)
222
- apply(:sdiffstore, hash, :-)
238
+ def first(options = {})
239
+ options.merge!(:limit => 1)
240
+
241
+ if options[:by]
242
+ sort_by(options.delete(:by), options).first
243
+ else
244
+ sort(options).first
245
+ end
246
+ end
247
+
248
+ def include?(model)
249
+ key.sismember(model.id)
223
250
  end
224
251
 
225
- private
252
+ def inspect
253
+ "#<Set (#{model}): #{key.smembers.inspect}>"
254
+ end
226
255
 
227
- # Apply a Redis operation on a collection of sets.
228
- def apply(operation, hash, glue)
229
- keys = keys(hash)
230
- target = key.volatile.send(glue, Key[*keys])
231
- model.db.send(operation, target, key, *keys)
256
+ protected
257
+
258
+ def apply(operation, key, source, target)
259
+ target.send(operation, key, *source)
232
260
  Set.new(target, Wrapper.wrap(model))
233
261
  end
234
262
 
@@ -245,37 +273,74 @@ module Ohm
245
273
  end
246
274
  end
247
275
 
276
+ class Index < Set
277
+ def find(options)
278
+ keys = keys(options)
279
+ return super(options) if keys.size > 1
280
+
281
+ Set.new(keys.first, Wrapper.wrap(model))
282
+ end
283
+ end
284
+
248
285
  class List < Collection
249
- Raw = Ohm::List
286
+ def each(&block)
287
+ key.lrange(0, -1).each { |id| block.call(model[id]) }
288
+ end
250
289
 
251
- def shift
252
- if id = raw.shift
253
- model[id]
290
+ def <<(model)
291
+ key.rpush(model.id)
292
+ end
293
+
294
+ alias push <<
295
+
296
+ # Returns the element at index, or returns a subarray starting at
297
+ # start and continuing for length elements, or returns a subarray
298
+ # specified by range. Negative indices count backward from the end
299
+ # of the array (-1 is the last element). Returns nil if the index
300
+ # (or starting index) are out of range.
301
+ def [](index, limit = nil)
302
+ case [index, limit]
303
+ when Pattern[Fixnum, Fixnum] then
304
+ key.lrange(index, limit).collect { |id| model[id] }
305
+ when Pattern[Range, nil] then
306
+ key.lrange(index.first, index.last).collect { |id| model[id] }
307
+ when Pattern[Fixnum, nil] then
308
+ model[key.lindex(index)]
254
309
  end
255
310
  end
256
311
 
312
+ def first
313
+ self[0]
314
+ end
315
+
257
316
  def pop
258
- if id = raw.pop
259
- model[id]
260
- end
317
+ id = key.rpop
318
+ model[id] if id
319
+ end
320
+
321
+ def shift
322
+ id = key.lpop
323
+ model[id] if id
261
324
  end
262
325
 
263
326
  def unshift(model)
264
- raw.unshift(model.id)
327
+ key.lpush(model.id)
265
328
  end
266
329
 
267
- def inspect
268
- "#<List (#{model}): #{all.inspect}>"
330
+ def all
331
+ key.lrange(0, -1).map(&model)
269
332
  end
270
- end
271
333
 
272
- class Index < Set
273
- def apply(operation, hash, glue)
274
- if hash.keys.size == 1
275
- return Set.new(keys(hash).first, Wrapper.wrap(model))
276
- else
277
- super
278
- end
334
+ def size
335
+ key.llen
336
+ end
337
+
338
+ def include?(model)
339
+ key.lrange(0, -1).include?(model.id)
340
+ end
341
+
342
+ def inspect
343
+ "#<List (#{model}): #{key.lrange(0, -1).inspect}>"
279
344
  end
280
345
  end
281
346
 
@@ -356,8 +421,8 @@ module Ohm
356
421
  # is created.
357
422
  #
358
423
  # @param name [Symbol] Name of the list.
359
- def self.list(name, model = nil)
360
- attr_collection_reader(name, :List, model)
424
+ def self.list(name, model)
425
+ define_memoized_method(name) { List.new(key[name], Wrapper.wrap(model)) }
361
426
  collections << name unless collections.include?(name)
362
427
  end
363
428
 
@@ -366,8 +431,8 @@ module Ohm
366
431
  # operations like union, join, and membership checks are important.
367
432
  #
368
433
  # @param name [Symbol] Name of the set.
369
- def self.set(name, model = nil)
370
- attr_collection_reader(name, :Set, model)
434
+ def self.set(name, model)
435
+ define_memoized_method(name) { Set.new(key[name], Wrapper.wrap(model)) }
371
436
  collections << name unless collections.include?(name)
372
437
  end
373
438
 
@@ -427,7 +492,8 @@ module Ohm
427
492
  reader = :"#{name}_id"
428
493
  writer = :"#{name}_id="
429
494
 
430
- attribute reader
495
+ attributes << reader unless attributes.include?(reader)
496
+
431
497
  index reader
432
498
 
433
499
  define_memoized_method(name) do
@@ -435,12 +501,16 @@ module Ohm
435
501
  end
436
502
 
437
503
  define_method(:"#{name}=") do |value|
438
- instance_variable_set("@#{name}", nil)
504
+ @_memo.delete(name)
439
505
  send(writer, value ? value.id : nil)
440
506
  end
441
507
 
508
+ define_method(reader) do
509
+ read_local(reader)
510
+ end
511
+
442
512
  define_method(writer) do |value|
443
- instance_variable_set("@#{name}", nil)
513
+ @_memo.delete(name)
444
514
  write_local(reader, value)
445
515
  end
446
516
  end
@@ -493,19 +563,9 @@ module Ohm
493
563
  name.to_s.match(/^(?:.*::)*(.*)$/)[1].gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
494
564
  end
495
565
 
496
- def self.attr_collection_reader(name, type, model)
497
- if model
498
- model = Wrapper.wrap(model)
499
- define_memoized_method(name) { Ohm::Model::const_get(type).new(key(name), model, db) }
500
- else
501
- define_memoized_method(name) { Ohm::const_get(type).new(key(name), db) }
502
- end
503
- end
504
-
505
566
  def self.define_memoized_method(name, &block)
506
567
  define_method(name) do
507
- instance_variable_get("@#{name}") ||
508
- instance_variable_set("@#{name}", instance_eval(&block))
568
+ @_memo[name] ||= instance_eval(&block)
509
569
  end
510
570
  end
511
571
 
@@ -518,7 +578,7 @@ module Ohm
518
578
  end
519
579
 
520
580
  def self.all
521
- Ohm::Model::Index.new(key(:all), Wrapper.wrap(self))
581
+ Ohm::Model::Index.new(key[:all], Wrapper.wrap(self))
522
582
  end
523
583
 
524
584
  def self.attributes
@@ -561,6 +621,8 @@ module Ohm
561
621
  end
562
622
 
563
623
  def initialize(attrs = {})
624
+ @id = nil
625
+ @_memo = {}
564
626
  @_attributes = Hash.new { |hash, key| hash[key] = read_remote(key) }
565
627
  update_attributes(attrs)
566
628
  end
@@ -736,8 +798,8 @@ module Ohm
736
798
 
737
799
  protected
738
800
 
739
- def key(*args)
740
- self.class.key(id, *args)
801
+ def key
802
+ self.class.key[id]
741
803
  end
742
804
 
743
805
  def write
@@ -789,16 +851,16 @@ module Ohm
789
851
  Ohm.threaded[self] = connection
790
852
  end
791
853
 
792
- def self.key(*args)
793
- Ohm.key(*args.unshift(self))
854
+ def self.key
855
+ Key.new(self, db)
794
856
  end
795
857
 
796
858
  def self.exists?(id)
797
- db.sismember(key(:all), id)
859
+ db.sismember(key[:all], id)
798
860
  end
799
861
 
800
862
  def initialize_id
801
- self.id = db.incr(self.class.key("id")).to_s
863
+ self.id = db.incr(self.class.key[:id]).to_s
802
864
  end
803
865
 
804
866
  def db
@@ -806,7 +868,7 @@ module Ohm
806
868
  end
807
869
 
808
870
  def delete_attributes(atts)
809
- db.del(*atts.map { |att| key(att) })
871
+ db.del(*atts.map { |att| key[att] })
810
872
  end
811
873
 
812
874
  def create_model_membership
@@ -842,15 +904,15 @@ module Ohm
842
904
  def add_to_index(att, value = send(att))
843
905
  index = index_key_for(att, value)
844
906
  db.sadd(index, id)
845
- db.sadd(key(:_indices), index)
907
+ db.sadd(key[:_indices], index)
846
908
  end
847
909
 
848
910
  def delete_from_indices
849
- db.smembers(key(:_indices)).each do |index|
911
+ db.smembers(key[:_indices]).each do |index|
850
912
  db.srem(index, id)
851
913
  end
852
914
 
853
- db.del(key(:_indices))
915
+ db.del(key[:_indices])
854
916
  end
855
917
 
856
918
  def read_local(att)
@@ -884,7 +946,7 @@ module Ohm
884
946
 
885
947
  def self.index_key_for(att, value)
886
948
  raise IndexNotFound, att unless indices.include?(att)
887
- key(att, encode(value))
949
+ key[att][encode(value)]
888
950
  end
889
951
 
890
952
  def index_key_for(att, value)
@@ -897,11 +959,11 @@ module Ohm
897
959
  #
898
960
  # @see Model#mutex
899
961
  def lock!
900
- until db.setnx(key(:_lock), lock_timeout)
901
- next unless lock = db.get(key(:_lock))
962
+ until db.setnx(key[:_lock], lock_timeout)
963
+ next unless lock = db.get(key[:_lock])
902
964
  sleep(0.5) and next unless lock_expired?(lock)
903
965
 
904
- break unless lock = db.getset(key(:_lock), lock_timeout)
966
+ break unless lock = db.getset(key[:_lock], lock_timeout)
905
967
  break if lock_expired?(lock)
906
968
  end
907
969
  end
@@ -909,7 +971,7 @@ module Ohm
909
971
  # Release the lock.
910
972
  # @see Model#mutex
911
973
  def unlock!
912
- db.del(key(:_lock))
974
+ db.del(key[:_lock])
913
975
  end
914
976
 
915
977
  def lock_timeout
@@ -1,10 +1,6 @@
1
1
  require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))
2
2
 
3
3
  class ConnectionTest < Test::Unit::TestCase
4
- setup do
5
- @options = Ohm.options
6
- end
7
-
8
4
  test "connects lazily" do
9
5
  assert_nothing_raised do
10
6
  Ohm.connect(:port => 9876)
@@ -32,10 +28,6 @@ class ConnectionTest < Test::Unit::TestCase
32
28
 
33
29
  threads.each { |t| t.join }
34
30
 
35
- assert (conn1 != conn2)
36
- end
37
-
38
- teardown do
39
- Ohm.connect(*@options)
31
+ assert(conn1 != conn2)
40
32
  end
41
33
  end