ohm 0.1.0.rc4 → 0.1.0.rc5

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/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