ohm 0.0.38 → 0.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -364,3 +364,28 @@ values. The result of the block is used as the error message:
364
364
 
365
365
  error_messages
366
366
  # => ["The email foo@example.com is already registered."]
367
+
368
+ Versions
369
+ ========
370
+
371
+ Ohm uses features from Redis > 1.3.10. If you are stuck in previous
372
+ versions, please use Ohm 0.0.35 instead.
373
+
374
+ Upgrading from 0.0.x to 0.1
375
+ ---------------------------
376
+
377
+ Since Ohm 0.1 changes the persistence strategy (from 1-key-per-attribute
378
+ to Hashes), you'll need to run a script to upgrade your old data set.
379
+ Fortunately, it is built in:
380
+
381
+ require "ohm/utils/upgrade"
382
+
383
+ Ohm.connect :port => 6380
384
+
385
+ Ohm::Utils::Upgrade.new([:User, :Post, :Comment]).run
386
+
387
+ Yes, you need to provide the model names. The good part is that you
388
+ don't have to load your application environment. Since we assume it's
389
+ very likely that you have a bunch of data, the script uses
390
+ [Batch](http://github.com/djanowski/batch) to show you some progress
391
+ while the process runs.
data/Rakefile CHANGED
@@ -24,14 +24,6 @@ task :stop do
24
24
  end
25
25
  end
26
26
 
27
- task :test do
28
- Dir["test/**/*_test.rb"].each do |file|
29
- fork do
30
- load file
31
- end
32
-
33
- Process.wait
34
-
35
- exit $?.exitstatus unless $?.success?
36
- end
27
+ Rake::TestTask.new(:test) do |t|
28
+ t.pattern = 'test/**/*_test.rb'
37
29
  end
data/lib/ohm.rb CHANGED
@@ -9,7 +9,7 @@ require File.join(File.dirname(__FILE__), "ohm", "key")
9
9
  require File.join(File.dirname(__FILE__), "ohm", "collection")
10
10
 
11
11
  module Ohm
12
- VERSION = "0.0.38"
12
+ VERSION = "0.1.0.rc1"
13
13
 
14
14
  # Provides access to the Redis database. This is shared accross all models and instances.
15
15
  def redis
@@ -149,10 +149,10 @@ module Ohm
149
149
  # user.name == "A"
150
150
  # # => true
151
151
  def sort_by(att, options = {})
152
- options.merge!(:by => model.key("*", att))
152
+ options.merge!(:by => model.key("*->#{att}"))
153
153
 
154
154
  if options[:get]
155
- raw.sort(options.merge(:get => model.key("*", options[:get])))
155
+ raw.sort(options.merge(:get => model.key("*->#{options[:get]}")))
156
156
  else
157
157
  sort(options)
158
158
  end
@@ -200,7 +200,7 @@ module Ohm
200
200
  Raw = Ohm::Set
201
201
 
202
202
  def inspect
203
- "#<Set (#{model}): #{raw.to_a.inspect}>"
203
+ "#<Set (#{model}): #{all.inspect}>"
204
204
  end
205
205
 
206
206
  # Returns an intersection with the sets generated from the passed hash.
@@ -212,7 +212,7 @@ module Ohm
212
212
  # # You can combine the result with sort and other set operations:
213
213
  # @events.sort_by(:name)
214
214
  def find(hash)
215
- apply(:sinterstore, hash, "+")
215
+ apply(:sinterstore, hash, :+)
216
216
  end
217
217
 
218
218
  # Returns the difference between the receiver and the passed sets.
@@ -220,15 +220,16 @@ module Ohm
220
220
  # @example
221
221
  # @events = Event.find(public: true).except(status: "sold_out")
222
222
  def except(hash)
223
- apply(:sdiffstore, hash, "-")
223
+ apply(:sdiffstore, hash, :-)
224
224
  end
225
225
 
226
226
  private
227
227
 
228
- # Apply a redis operation on a collection of sets.
228
+ # Apply a Redis operation on a collection of sets.
229
229
  def apply(operation, hash, glue)
230
- target = key.volatile.group(glue).append(*keys(hash))
231
- model.db.send(operation, target, *target.sub_keys)
230
+ keys = keys(hash)
231
+ target = key.volatile.send(glue, Key[*keys])
232
+ model.db.send(operation, target, key, *keys)
232
233
  Set.new(target, Wrapper.wrap(model))
233
234
  end
234
235
 
@@ -265,7 +266,7 @@ module Ohm
265
266
  end
266
267
 
267
268
  def inspect
268
- "#<List (#{model}): #{raw.to_a.inspect}>"
269
+ "#<List (#{model}): #{all.inspect}>"
269
270
  end
270
271
  end
271
272
 
@@ -290,7 +291,7 @@ module Ohm
290
291
  # @overload assert_unique [:street, :city]
291
292
  # Validates that the :street and :city pair is unique.
292
293
  def assert_unique(attrs)
293
- result = db.sinter(*Array(attrs).map { |att| index_key_for(att, send(att)) }) || []
294
+ result = db.sinter(*Array(attrs).map { |att| index_key_for(att, send(att)) })
294
295
  assert result.empty? || !new? && result.include?(id.to_s), [attrs, :not_unique]
295
296
  end
296
297
  end
@@ -490,7 +491,7 @@ module Ohm
490
491
  end
491
492
 
492
493
  def self.to_reference
493
- name.to_s.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
494
+ name.to_s.match(/^(?:.*::)*(.*)$/)[1].gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
494
495
  end
495
496
 
496
497
  def self.attr_collection_reader(name, type, model)
@@ -602,9 +603,7 @@ module Ohm
602
603
 
603
604
  def delete
604
605
  delete_from_indices
605
- delete_attributes(attributes)
606
- delete_attributes(counters)
607
- delete_attributes(collections)
606
+ delete_attributes(collections) unless collections.empty?
608
607
  delete_model_membership
609
608
  self
610
609
  end
@@ -612,17 +611,16 @@ module Ohm
612
611
  # Increment the counter denoted by :att.
613
612
  #
614
613
  # @param att [Symbol] Attribute to increment.
615
- def incr(att)
614
+ def incr(att, count = 1)
616
615
  raise ArgumentError, "#{att.inspect} is not a counter." unless counters.include?(att)
617
- write_local(att, db.incr(key(att)))
616
+ write_local(att, db.hincrby(key, att, count))
618
617
  end
619
618
 
620
619
  # Decrement the counter denoted by :att.
621
620
  #
622
621
  # @param att [Symbol] Attribute to decrement.
623
- def decr(att)
624
- raise ArgumentError, "#{att.inspect} is not a counter." unless counters.include?(att)
625
- write_local(att, db.decr(key(att)))
622
+ def decr(att, count = 1)
623
+ incr(att, -count)
626
624
  end
627
625
 
628
626
  def attributes
@@ -695,26 +693,22 @@ module Ohm
695
693
  self.class.key(id, *args)
696
694
  end
697
695
 
698
- # Write attributes using MSET
699
696
  def write
700
697
  unless attributes.empty?
701
- rems, adds = attributes.map { |a| [key(a), send(a)] }.partition { |t| t.last.to_s.empty? }
698
+ attributes.each_with_index do |att, index|
699
+ value = send(att).to_s
702
700
 
703
- db.del(*rems.flatten.compact) unless rems.empty?
704
- db.mapped_mset(adds.flatten) unless adds.empty?
701
+ if value.empty?
702
+ db.hdel(key, att)
703
+ else
704
+ db.hset(key, att, value)
705
+ end
706
+ end
705
707
  end
706
708
  end
707
709
 
708
710
  def self.const_missing(name)
709
- wrapper = Wrapper.new(name) { const_get(name) }
710
-
711
- # Allow others to hook to const_missing.
712
- begin
713
- super(name)
714
- rescue NameError
715
- end
716
-
717
- wrapper
711
+ Wrapper.new(name) { const_get(name) }
718
712
  end
719
713
 
720
714
  private
@@ -745,17 +739,16 @@ module Ohm
745
739
  end
746
740
 
747
741
  def delete_attributes(atts)
748
- atts.each do |att|
749
- db.del(key(att))
750
- end
742
+ db.del(*atts.map { |att| key(att) })
751
743
  end
752
744
 
753
745
  def create_model_membership
754
- db.sadd(self.class.key(:all), id)
746
+ self.class.all << self
755
747
  end
756
748
 
757
749
  def delete_model_membership
758
- db.srem(self.class.key(:all), id)
750
+ db.del(key)
751
+ self.class.all.delete(self)
759
752
  end
760
753
 
761
754
  def update_indices
@@ -786,7 +779,7 @@ module Ohm
786
779
  end
787
780
 
788
781
  def delete_from_indices
789
- (db.smembers(key(:_indices)) || []).each do |index|
782
+ db.smembers(key(:_indices)).each do |index|
790
783
  db.srem(index, id)
791
784
  end
792
785
 
@@ -803,7 +796,7 @@ module Ohm
803
796
 
804
797
  def read_remote(att)
805
798
  unless new?
806
- value = db.get(key(att))
799
+ value = db.hget(key, att)
807
800
  value.respond_to?(:force_encoding) ?
808
801
  value.force_encoding("UTF-8") :
809
802
  value
@@ -124,7 +124,7 @@ module Ohm
124
124
 
125
125
  # @return [Array] Elements of the list.
126
126
  def all
127
- db.lrange(key, 0, -1) || []
127
+ db.lrange(key, 0, -1)
128
128
  end
129
129
 
130
130
  # @return [Integer] Returns the number of elements in the list.
@@ -171,7 +171,7 @@ module Ohm
171
171
  end
172
172
 
173
173
  def all
174
- db.smembers(key) || []
174
+ db.smembers(key)
175
175
  end
176
176
 
177
177
  # @return [Integer] Returns the number of elements in the set.
@@ -1,50 +1,28 @@
1
1
  module Ohm
2
2
 
3
3
  # Represents a key in Redis.
4
- class Key
5
- attr :parts
6
- attr :glue
7
- attr :namespace
4
+ class Key < String
5
+ Volatile = new("~")
8
6
 
9
- def self.[](*parts)
10
- Key.new(parts)
7
+ def self.[](*args)
8
+ new(args.join(":"))
11
9
  end
12
10
 
13
- def initialize(parts, glue = ":", namespace = [])
14
- @parts = parts
15
- @glue = glue
16
- @namespace = namespace
11
+ def [](key)
12
+ self.class[self, key]
17
13
  end
18
14
 
19
- def sub_keys
20
- parts.map {|k| k.glue == ":" ? k : k.volatile }
21
- end
22
-
23
- def append(*parts)
24
- @parts += parts
25
- self
26
- end
27
-
28
- def eql?(other)
29
- to_s == other.to_s
30
- end
31
-
32
- alias == eql?
33
-
34
- def to_s
35
- (namespace + [@parts.join(glue)]).join(":")
15
+ def volatile
16
+ self.index(Volatile) == 0 ? self : Volatile[self]
36
17
  end
37
18
 
38
- alias inspect to_s
39
- alias to_str to_s
40
-
41
- def volatile
42
- @namespace = [:~]
43
- self
19
+ def +(other)
20
+ self.class.new("#{self}+#{other}")
44
21
  end
45
22
 
46
- def group(glue = self.glue)
47
- Key.new([self], glue, namespace.slice!(0, namespace.size))
23
+ def -(other)
24
+ self.class.new("#{self}-#{other}")
48
25
  end
49
26
  end
27
+
50
28
  end
@@ -0,0 +1,53 @@
1
+ begin
2
+ require "batch"
3
+ rescue LoadError => e
4
+ e.message << "\nTry `gem install batch`."
5
+ end
6
+
7
+ module Ohm
8
+ module Utils
9
+ class Upgrade
10
+ def redis
11
+ Ohm.redis
12
+ end
13
+
14
+ attr :models
15
+ attr :types
16
+
17
+ def initialize(models)
18
+ @models = models
19
+ @types = Hash.new { |hash, model| hash[model] = {} }
20
+ end
21
+
22
+ def run
23
+ models.each do |model|
24
+ ns = Ohm::Key[model]
25
+
26
+ puts "Upgrading #{model}..."
27
+
28
+ Batch.each(redis.smembers(ns[:all])) do |id|
29
+ instance = ns[id]
30
+
31
+ attrs = []
32
+ deletes = []
33
+
34
+ redis.keys(instance["*"]).each do |key|
35
+ field = key[instance.size.succ..-1]
36
+
37
+ type = (types[model][field] ||= redis.type(key).to_sym)
38
+
39
+ if type == :string
40
+ attrs << field
41
+ attrs << redis.get(key)
42
+ deletes << key
43
+ end
44
+ end
45
+
46
+ redis.hmset(instance, *attrs)
47
+ redis.del(*deletes)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -48,7 +48,7 @@ class IndicesTest < Test::Unit::TestCase
48
48
  assert_equal "~:IndicesTest::User:email:Zm9v+IndicesTest::User:activation_code:",
49
49
  User.find(:email => "foo").find(:activation_code => "").key.to_s
50
50
 
51
- assert_equal "~:~:IndicesTest::User:email:Zm9v+IndicesTest::User:activation_code:YmFy+IndicesTest::User:update:YmF6",
51
+ assert_equal "~:IndicesTest::User:email:Zm9v+IndicesTest::User:activation_code:YmFy+IndicesTest::User:update:YmF6",
52
52
  result = User.find(:email => "foo").find(:activation_code => "bar").find(:update => "baz").key.to_s
53
53
  end
54
54
 
@@ -0,0 +1,951 @@
1
+ # encoding: UTF-8
2
+
3
+ require File.join(File.dirname(__FILE__), "test_helper")
4
+ require "ostruct"
5
+
6
+ module Model
7
+ class Post < Ohm::Model
8
+ attribute :body
9
+ list :comments
10
+ list :related, Post
11
+ end
12
+
13
+ class User < Ohm::Model
14
+ attribute :email
15
+ set :posts, Post
16
+ end
17
+
18
+ class Person < Ohm::Model
19
+ attribute :name
20
+ index :initial
21
+
22
+ def validate
23
+ assert_present :name
24
+ end
25
+
26
+ def initial
27
+ name[0, 1].upcase
28
+ end
29
+ end
30
+
31
+ class Event < Ohm::Model
32
+ attribute :name
33
+ counter :votes
34
+ set :attendees, Person
35
+
36
+ attribute :slug
37
+
38
+ def write
39
+ self.slug = name.to_s.downcase
40
+ super
41
+ end
42
+ end
43
+ end
44
+
45
+ class ScopedModelsTest < Test::Unit::TestCase
46
+ setup do
47
+ Ohm.flush
48
+ end
49
+
50
+ context "An event initialized with a hash of attributes" do
51
+ should "assign the passed attributes" do
52
+ event = Model::Event.new(:name => "Ruby Tuesday")
53
+ assert_equal event.name, "Ruby Tuesday"
54
+ end
55
+ end
56
+
57
+ context "An event created from a hash of attributes" do
58
+ should "assign an id and save the object" do
59
+ event1 = Model::Event.create(:name => "Ruby Tuesday")
60
+ event2 = Model::Event.create(:name => "Ruby Meetup")
61
+
62
+ assert_equal "1", event1.id
63
+ assert_equal "2", event2.id
64
+ end
65
+
66
+ should "return the unsaved object if validation fails" do
67
+ assert Model::Person.create(:name => nil).kind_of?(Model::Person)
68
+ end
69
+ end
70
+
71
+ context "An event updated from a hash of attributes" do
72
+ class ::Model::Meetup < Ohm::Model
73
+ attribute :name
74
+ attribute :location
75
+
76
+ def validate
77
+ assert_present :name
78
+ end
79
+ end
80
+
81
+ should "assign an id and save the object" do
82
+ event = Model::Meetup.create(:name => "Ruby Tuesday")
83
+ event.update(:name => "Ruby Meetup")
84
+ assert_equal "Ruby Meetup", event.name
85
+ end
86
+
87
+ should "return false if the validation fails" do
88
+ event = Model::Meetup.create(:name => "Ruby Tuesday")
89
+ assert !event.update(:name => nil)
90
+ end
91
+
92
+ should "save the attributes in UTF8" do
93
+ event = Model::Meetup.create(:name => "32° Kisei-sen")
94
+ assert_equal "32° Kisei-sen", Model::Meetup[event.id].name
95
+ end
96
+
97
+ should "delete the attribute if set to nil" do
98
+ event = Model::Meetup.create(:name => "Ruby Tuesday", :location => "Los Angeles")
99
+ assert_equal "Los Angeles", Model::Meetup[event.id].location
100
+ assert event.update(:location => nil)
101
+ assert_equal nil, Model::Meetup[event.id].location
102
+ end
103
+
104
+ should "delete the attribute if set to an empty string" do
105
+ event = Model::Meetup.create(:name => "Ruby Tuesday", :location => "Los Angeles")
106
+ assert_equal "Los Angeles", Model::Meetup[event.id].location
107
+ assert event.update(:location => "")
108
+ assert_equal nil, Model::Meetup[event.id].location
109
+ end
110
+ end
111
+
112
+ context "Model definition" do
113
+ should "not raise if an attribute is redefined" do
114
+ assert_nothing_raised do
115
+ class ::Model::RedefinedModel < Ohm::Model
116
+ attribute :name
117
+ attribute :name
118
+ end
119
+ end
120
+ end
121
+
122
+ should "not raise if a counter is redefined" do
123
+ assert_nothing_raised do
124
+ class ::Model::RedefinedModel < Ohm::Model
125
+ counter :age
126
+ counter :age
127
+ end
128
+ end
129
+ end
130
+
131
+ should "not raise if a list is redefined" do
132
+ assert_nothing_raised do
133
+ class ::Model::RedefinedModel < Ohm::Model
134
+ list :todo
135
+ list :todo
136
+ end
137
+ end
138
+ end
139
+
140
+ should "not raise if a set is redefined" do
141
+ assert_nothing_raised do
142
+ class ::Model::RedefinedModel < Ohm::Model
143
+ set :friends
144
+ set :friends
145
+ end
146
+ end
147
+ end
148
+
149
+ should "not raise if a collection is redefined" do
150
+ assert_nothing_raised do
151
+ class ::Model::RedefinedModel < Ohm::Model
152
+ list :toys
153
+ set :toys
154
+ end
155
+ end
156
+ end
157
+
158
+ should "not raise if a index is redefined" do
159
+ assert_nothing_raised do
160
+ class ::Model::RedefinedModel < Ohm::Model
161
+ attribute :color
162
+ index :color
163
+ index :color
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ context "Finding an event" do
170
+ setup do
171
+ Ohm.redis.sadd("Model::Event:all", 1)
172
+ Ohm.redis.hset("Model::Event:1", "name", "Concert")
173
+ end
174
+
175
+ should "return an instance of Event" do
176
+ assert Model::Event[1].kind_of?(Model::Event)
177
+ assert_equal 1, Model::Event[1].id
178
+ assert_equal "Concert", Model::Event[1].name
179
+ end
180
+ end
181
+
182
+ context "Finding a user" do
183
+ setup do
184
+ Ohm.redis.sadd("Model::User:all", 1)
185
+ Ohm.redis.hset("Model::User:1", "email", "albert@example.com")
186
+ end
187
+
188
+ should "return an instance of User" do
189
+ assert Model::User[1].kind_of?(Model::User)
190
+ assert_equal 1, Model::User[1].id
191
+ assert_equal "albert@example.com", Model::User[1].email
192
+ end
193
+
194
+ should "allow to map ids to models" do
195
+ assert_equal [Model::User[1]], [1].map(&Model::User)
196
+ end
197
+ end
198
+
199
+ context "Updating a user" do
200
+ setup do
201
+ Ohm.redis.sadd("Model::User:all", 1)
202
+ Ohm.redis.set("Model::User:1:email", "albert@example.com")
203
+
204
+ @user = Model::User[1]
205
+ end
206
+
207
+ should "change its attributes" do
208
+ @user.email = "maria@example.com"
209
+ assert_equal "maria@example.com", @user.email
210
+ end
211
+
212
+ should "save the new values" do
213
+ @user.email = "maria@example.com"
214
+ @user.save
215
+
216
+ @user.email = "maria@example.com"
217
+ @user.save
218
+
219
+ assert_equal "maria@example.com", Model::User[1].email
220
+ end
221
+ end
222
+
223
+ context "Creating a new model" do
224
+ should "assign a new id to the event" do
225
+ event1 = Model::Event.new
226
+ event1.create
227
+
228
+ event2 = Model::Event.new
229
+ event2.create
230
+
231
+ assert !event1.new?
232
+ assert !event2.new?
233
+
234
+ assert_equal "1", event1.id
235
+ assert_equal "2", event2.id
236
+ end
237
+ end
238
+
239
+ context "Saving a model" do
240
+ should "create the model if it is new" do
241
+ event = Model::Event.new(:name => "Foo").save
242
+ assert_equal "Foo", Model::Event[event.id].name
243
+ end
244
+
245
+ should "save it only if it was previously created" do
246
+ event = Model::Event.new
247
+ event.name = "Lorem ipsum"
248
+ event.create
249
+
250
+ event.name = "Lorem"
251
+ event.save
252
+
253
+ assert_equal "Lorem", Model::Event[event.id].name
254
+ end
255
+
256
+ should "allow to hook into write" do
257
+ event = Model::Event.create(:name => "Foo")
258
+
259
+ assert_equal "foo", event.slug
260
+ end
261
+ end
262
+
263
+ context "Delete" do
264
+ should "delete an existing model" do
265
+ class ::Model::ModelToBeDeleted < Ohm::Model
266
+ attribute :name
267
+ set :foos
268
+ list :bars
269
+ end
270
+
271
+ @model = Model::ModelToBeDeleted.create(:name => "Lorem")
272
+
273
+ @model.foos << "foo"
274
+ @model.bars << "bar"
275
+
276
+ id = @model.id
277
+
278
+ @model.delete
279
+
280
+ assert_nil Ohm.redis.get(Model::ModelToBeDeleted.key(id))
281
+ assert_nil Ohm.redis.get(Model::ModelToBeDeleted.key(id, :name))
282
+ assert_equal Array.new, Ohm.redis.smembers(Model::ModelToBeDeleted.key(id, :foos))
283
+ assert_equal Array.new, Ohm.redis.lrange(Model::ModelToBeDeleted.key(id, :bars), 0, -1)
284
+
285
+ assert Model::ModelToBeDeleted.all.empty?
286
+ end
287
+
288
+ should "be no leftover keys" do
289
+ class ::Model::Foo < Ohm::Model
290
+ attribute :name
291
+ index :name
292
+ end
293
+
294
+ assert_equal [], Ohm.redis.keys("*")
295
+
296
+ Model::Foo.create(:name => "Bar")
297
+
298
+ assert_equal ["Model::Foo:1", "Model::Foo:1:_indices", "Model::Foo:all", "Model::Foo:id", "Model::Foo:name:QmFy"], Ohm.redis.keys("*").sort
299
+
300
+ Model::Foo[1].delete
301
+
302
+ assert_equal ["Model::Foo:id"], Ohm.redis.keys("*")
303
+ end
304
+ end
305
+
306
+ context "Listing" do
307
+ should "find all" do
308
+ event1 = Model::Event.new
309
+ event1.name = "Ruby Meetup"
310
+ event1.create
311
+
312
+ event2 = Model::Event.new
313
+ event2.name = "Ruby Tuesday"
314
+ event2.create
315
+
316
+ all = Model::Event.all
317
+
318
+ assert all.detect {|e| e.name == "Ruby Meetup" }
319
+ assert all.detect {|e| e.name == "Ruby Tuesday" }
320
+ end
321
+ end
322
+
323
+ context "Sorting" do
324
+ should "sort all" do
325
+ Model::Person.create :name => "D"
326
+ Model::Person.create :name => "C"
327
+ Model::Person.create :name => "B"
328
+ Model::Person.create :name => "A"
329
+
330
+ assert_equal %w[A B C D], Model::Person.all.sort_by(:name, :order => "ALPHA").map { |person| person.name }
331
+ end
332
+
333
+ should "return an empty array if there are no elements to sort" do
334
+ assert_equal [], Model::Person.all.sort_by(:name)
335
+ end
336
+
337
+ should "return the first element sorted by id when using first" do
338
+ Model::Person.create :name => "A"
339
+ Model::Person.create :name => "B"
340
+ assert_equal "A", Model::Person.all.first.name
341
+ end
342
+
343
+ should "return the first element sorted by name if first receives a sorting option" do
344
+ Model::Person.create :name => "B"
345
+ Model::Person.create :name => "A"
346
+ assert_equal "A", Model::Person.all.first(:by => :name, :order => "ALPHA").name
347
+ end
348
+
349
+ should "return attribute values when the get parameter is specified" do
350
+ Model::Person.create :name => "B"
351
+ Model::Person.create :name => "A"
352
+
353
+ assert_equal "A", Model::Person.all.sort_by(:name, :get => :name, :order => "ALPHA").first
354
+ end
355
+ end
356
+
357
+ context "Loading attributes" do
358
+ setup do
359
+ event = Model::Event.new
360
+ event.name = "Ruby Tuesday"
361
+ @id = event.create.id
362
+ end
363
+
364
+ should "load attributes lazily" do
365
+ event = Model::Event[@id]
366
+
367
+ assert_nil event.send(:instance_variable_get, "@name")
368
+ assert_equal "Ruby Tuesday", event.name
369
+ end
370
+
371
+ should "load attributes as a strings" do
372
+ event = Model::Event.create(:name => 1)
373
+
374
+ assert_equal "1", Model::Event[event.id].name
375
+ end
376
+ end
377
+
378
+ context "Attributes of type Set" do
379
+ setup do
380
+ @person1 = Model::Person.create(:name => "Albert")
381
+ @person2 = Model::Person.create(:name => "Bertrand")
382
+ @person3 = Model::Person.create(:name => "Charles")
383
+
384
+ @event = Model::Event.new
385
+ @event.name = "Ruby Tuesday"
386
+ end
387
+
388
+ should "not be available if the model is new" do
389
+ assert_raise Ohm::Model::MissingID do
390
+ @event.attendees << Model::Person.new
391
+ end
392
+ end
393
+
394
+ should "remove an element if sent :delete" do
395
+ @event.create
396
+ @event.attendees << @person1
397
+ @event.attendees << @person2
398
+ @event.attendees << @person3
399
+ assert_equal ["1", "2", "3"], @event.attendees.raw.sort
400
+ @event.attendees.delete(@person2)
401
+ assert_equal ["1", "3"], Model::Event[@event.id].attendees.raw.sort
402
+ end
403
+
404
+ should "return true if the set includes some member" do
405
+ @event.create
406
+ @event.attendees << @person1
407
+ @event.attendees << @person2
408
+ assert @event.attendees.include?(@person2)
409
+ assert !@event.attendees.include?(@person3)
410
+ end
411
+
412
+ should "return instances of the passed model" do
413
+ @event.create
414
+ @event.attendees << @person1
415
+
416
+ assert_equal [@person1], @event.attendees.all
417
+ assert_equal @person1, @event.attendees[0]
418
+ end
419
+
420
+ should "return the size of the set" do
421
+ @event.create
422
+ @event.attendees << @person1
423
+ @event.attendees << @person2
424
+ @event.attendees << @person3
425
+ assert_equal 3, @event.attendees.size
426
+ end
427
+
428
+ should "empty the set" do
429
+ @event.create
430
+ @event.attendees << @person1
431
+
432
+ @event.attendees.clear
433
+
434
+ assert @event.attendees.empty?
435
+ end
436
+
437
+ should "replace the values in the set" do
438
+ @event.create
439
+ @event.attendees << @person1
440
+
441
+ assert_equal [@person1], @event.attendees.all
442
+
443
+ @event.attendees.replace([@person2, @person3])
444
+
445
+ assert_equal [@person2, @person3], @event.attendees.sort
446
+ end
447
+
448
+ should "filter elements" do
449
+ @event.create
450
+ @event.attendees.add(@person1)
451
+ @event.attendees.add(@person2)
452
+
453
+ assert_equal [@person1], @event.attendees.find(:initial => "A").all
454
+ assert_equal [@person2], @event.attendees.find(:initial => "B").all
455
+ assert_equal [], @event.attendees.find(:initial => "Z").all
456
+ end
457
+ end
458
+
459
+ context "Attributes of type List" do
460
+ setup do
461
+ @post = Model::Post.new
462
+ @post.body = "Hello world!"
463
+ @post.create
464
+ end
465
+
466
+ should "return an array" do
467
+ assert @post.comments.all.kind_of?(Array)
468
+ end
469
+
470
+ should "append elements with push" do
471
+ @post.comments.push "1"
472
+ @post.comments << "2"
473
+
474
+ assert_equal ["1", "2"], @post.comments.all
475
+ end
476
+
477
+ should "keep the inserting order" do
478
+ @post.comments << "1"
479
+ @post.comments << "2"
480
+ @post.comments << "3"
481
+ assert_equal ["1", "2", "3"], @post.comments.all
482
+ end
483
+
484
+ should "keep the inserting order after saving" do
485
+ @post.comments << "1"
486
+ @post.comments << "2"
487
+ @post.comments << "3"
488
+ @post.save
489
+ assert_equal ["1", "2", "3"], Model::Post[@post.id].comments.all
490
+ end
491
+
492
+ should "respond to each" do
493
+ @post.comments << "1"
494
+ @post.comments << "2"
495
+ @post.comments << "3"
496
+
497
+ i = 1
498
+ @post.comments.each do |c|
499
+ assert_equal i, c.to_i
500
+ i += 1
501
+ end
502
+ end
503
+
504
+ should "return the size of the list" do
505
+ @post.comments << "1"
506
+ @post.comments << "2"
507
+ @post.comments << "3"
508
+ assert_equal 3, @post.comments.size
509
+ end
510
+
511
+ should "return the last element with pop" do
512
+ @post.comments << "1"
513
+ @post.comments << "2"
514
+ assert_equal "2", @post.comments.pop
515
+ assert_equal "1", @post.comments.pop
516
+ assert @post.comments.empty?
517
+ end
518
+
519
+ should "return the first element with shift" do
520
+ @post.comments << "1"
521
+ @post.comments << "2"
522
+ assert_equal "1", @post.comments.shift
523
+ assert_equal "2", @post.comments.shift
524
+ assert @post.comments.empty?
525
+ end
526
+
527
+ should "push to the head of the list with unshift" do
528
+ @post.comments.unshift "1"
529
+ @post.comments.unshift "2"
530
+ assert_equal "1", @post.comments.pop
531
+ assert_equal "2", @post.comments.pop
532
+ assert @post.comments.empty?
533
+ end
534
+
535
+ should "empty the list" do
536
+ @post.comments.unshift "1"
537
+ @post.comments.clear
538
+
539
+ assert @post.comments.empty?
540
+ end
541
+
542
+ should "replace the values in the list" do
543
+ @post.comments.replace(["1", "2"])
544
+
545
+ assert_equal ["1", "2"], @post.comments
546
+ end
547
+
548
+ should "add models" do
549
+ @post.related.add(Model::Post.create(:body => "Hello"))
550
+
551
+ assert_equal ["2"], @post.related.raw
552
+ end
553
+
554
+ should "find elements in the list" do
555
+ another_post = Model::Post.create
556
+
557
+ @post.related.add(another_post)
558
+
559
+ assert @post.related.include?(another_post)
560
+ assert !@post.related.include?(Model::Post.create)
561
+ end
562
+
563
+ should "unshift models" do
564
+ @post.related.unshift(Model::Post.create(:body => "Hello"))
565
+ @post.related.unshift(Model::Post.create(:body => "Goodbye"))
566
+
567
+ assert_equal ["3", "2"], @post.related.raw
568
+
569
+ assert_equal "3", @post.related.shift.id
570
+
571
+ assert_equal "2", @post.related.pop.id
572
+
573
+ assert_nil @post.related.pop
574
+ end
575
+ end
576
+
577
+ context "Applying arbitrary transformations" do
578
+ require "date"
579
+
580
+ class MyActiveRecordModel
581
+ def self.find(id)
582
+ return new if id.to_i == 1
583
+ end
584
+
585
+ def id
586
+ 1
587
+ end
588
+
589
+ def ==(other)
590
+ id == other.id
591
+ end
592
+ end
593
+
594
+ class ::Model::Appointment < Ohm::Model
595
+ end
596
+
597
+ class ::Model::Calendar < Ohm::Model
598
+ list :holidays, lambda { |v| Date.parse(v) }
599
+ list :subscribers, lambda { |id| MyActiveRecordModel.find(id) }
600
+ list :appointments, ::Model::Appointment
601
+ end
602
+
603
+ class ::Model::Appointment
604
+ attribute :text
605
+ reference :subscriber, lambda { |id| MyActiveRecordModel.find(id) }
606
+ end
607
+
608
+ setup do
609
+ @calendar = Model::Calendar.create
610
+
611
+ @calendar.holidays.raw << "2009-05-25"
612
+ @calendar.holidays.raw << "2009-07-09"
613
+
614
+ @calendar.subscribers << MyActiveRecordModel.find(1)
615
+ end
616
+
617
+ should "apply a transformation" do
618
+ assert_equal [Date.new(2009, 5, 25), Date.new(2009, 7, 9)], @calendar.holidays.all
619
+
620
+ assert_equal ["1"], @calendar.subscribers.raw.all
621
+ assert_equal [MyActiveRecordModel.find(1)], @calendar.subscribers.all
622
+ end
623
+
624
+ should "allow lambdas in references" do
625
+ appointment = Model::Appointment.create(:subscriber => MyActiveRecordModel.find(1))
626
+ assert_equal MyActiveRecordModel.find(1), appointment.subscriber
627
+ end
628
+
629
+ should "work with models too" do
630
+ @calendar.appointments.add(Model::Appointment.create(:text => "Meet with Bertrand"))
631
+
632
+ assert_equal [Model::Appointment[1]], Model::Calendar[1].appointments.sort
633
+ end
634
+ end
635
+
636
+ context "Sorting lists and sets" do
637
+ setup do
638
+ @post = Model::Post.create(:body => "Lorem")
639
+ @post.comments << 2
640
+ @post.comments << 3
641
+ @post.comments << 1
642
+ end
643
+
644
+ should "sort values" do
645
+ assert_equal %w{1 2 3}, @post.comments.sort
646
+ end
647
+ end
648
+
649
+ context "Sorting lists and sets by model attributes" do
650
+ setup do
651
+ @event = Model::Event.create(:name => "Ruby Tuesday")
652
+ @event.attendees << Model::Person.create(:name => "D")
653
+ @event.attendees << Model::Person.create(:name => "C")
654
+ @event.attendees << Model::Person.create(:name => "B")
655
+ @event.attendees << Model::Person.create(:name => "A")
656
+ end
657
+
658
+ should "sort the model instances by the values provided" do
659
+ people = @event.attendees.sort_by(:name, :order => "ALPHA")
660
+ assert_equal %w[A B C D], people.map { |person| person.name }
661
+ end
662
+
663
+ should "accept a number in the limit parameter" do
664
+ people = @event.attendees.sort_by(:name, :limit => 2, :order => "ALPHA")
665
+ assert_equal %w[A B], people.map { |person| person.name }
666
+ end
667
+
668
+ should "use the start parameter as an offset if the limit is provided" do
669
+ people = @event.attendees.sort_by(:name, :limit => 2, :start => 1, :order => "ALPHA")
670
+ assert_equal %w[B C], people.map { |person| person.name }
671
+ end
672
+ end
673
+
674
+ context "Collections initialized with a Model parameter" do
675
+ setup do
676
+ @user = Model::User.create(:email => "albert@example.com")
677
+ @user.posts.add Model::Post.create(:body => "D")
678
+ @user.posts.add Model::Post.create(:body => "C")
679
+ @user.posts.add Model::Post.create(:body => "B")
680
+ @user.posts.add Model::Post.create(:body => "A")
681
+ end
682
+
683
+ should "return instances of the passed model" do
684
+ assert_equal Model::Post, @user.posts.first.class
685
+ end
686
+ end
687
+
688
+ context "Counters" do
689
+ setup do
690
+ @event = Model::Event.create(:name => "Ruby Tuesday")
691
+ end
692
+
693
+ should "raise ArgumentError if the attribute is not a counter" do
694
+ assert_raise ArgumentError do
695
+ @event.incr(:name)
696
+ end
697
+ end
698
+
699
+ should "be zero if not initialized" do
700
+ assert_equal 0, @event.votes
701
+ end
702
+
703
+ should "be able to increment a counter" do
704
+ @event.incr(:votes)
705
+ assert_equal 1, @event.votes
706
+ end
707
+
708
+ should "be able to decrement a counter" do
709
+ @event.decr(:votes)
710
+ assert_equal -1, @event.votes
711
+ end
712
+ end
713
+
714
+ context "Comparison" do
715
+ setup do
716
+ @user = Model::User.create(:email => "foo")
717
+ end
718
+
719
+ should "be comparable to other instances" do
720
+ assert_equal @user, Model::User[@user.id]
721
+
722
+ assert_not_equal @user, Model::User.create
723
+ assert_not_equal Model::User.new, Model::User.new
724
+ end
725
+
726
+ should "not be comparable to instances of other models" do
727
+ assert_not_equal @user, Model::Event.create(:name => "Ruby Tuesday")
728
+ end
729
+
730
+ should "be comparable to non-models" do
731
+ assert_not_equal @user, 1
732
+ assert_not_equal @user, true
733
+
734
+ # Not equal although the other object responds to #key.
735
+ assert_not_equal @user, OpenStruct.new(:key => @user.send(:key))
736
+ end
737
+ end
738
+
739
+ context "Debugging" do
740
+ class ::Model::Bar < Ohm::Model
741
+ attribute :name
742
+ counter :visits
743
+ set :friends
744
+ list :comments
745
+
746
+ def foo
747
+ bar.foo
748
+ end
749
+
750
+ def baz
751
+ bar.new.foo
752
+ end
753
+
754
+ def bar
755
+ SomeMissingConstant
756
+ end
757
+ end
758
+
759
+ should "provide a meaningful inspect" do
760
+ bar = Model::Bar.new
761
+
762
+ assert_equal "#<Model::Bar:? name=nil friends=nil comments=nil visits=0>", bar.inspect
763
+
764
+ bar.update(:name => "Albert")
765
+ bar.friends << 1
766
+ bar.friends << 2
767
+ bar.comments << "A"
768
+ bar.incr(:visits)
769
+
770
+ assert_equal %Q{#<Model::Bar:#{bar.id} name="Albert" friends=#<Set: ["1", "2"]> comments=#<List: ["A"]> visits=1>}, Model::Bar[bar.id].inspect
771
+ end
772
+
773
+ def assert_wrapper_exception(&block)
774
+ begin
775
+ block.call
776
+ rescue NoMethodError => exception_raised
777
+ end
778
+
779
+ assert_match /You tried to call SomeMissingConstant#\w+, but SomeMissingConstant is not defined on #{__FILE__}:\d+:in `bar'/, exception_raised.message
780
+ end
781
+
782
+ should "inform about a miscatch by Wrapper when calling class methods" do
783
+ assert_wrapper_exception { Model::Bar.new.baz }
784
+ end
785
+
786
+ should "inform about a miscatch by Wrapper when calling instance methods" do
787
+ assert_wrapper_exception { Model::Bar.new.foo }
788
+ end
789
+ end
790
+
791
+ context "Overwriting write" do
792
+ class ::Model::Baz < Ohm::Model
793
+ attribute :name
794
+
795
+ def write
796
+ self.name = "Foobar"
797
+ super
798
+ end
799
+ end
800
+
801
+ should "work properly" do
802
+ baz = Model::Baz.new
803
+ baz.name = "Foo"
804
+ baz.save
805
+ baz.name = "Foo"
806
+ baz.save
807
+ assert_equal "Foobar", Model::Baz[baz.id].name
808
+ end
809
+ end
810
+
811
+ context "References to other objects" do
812
+ class ::Model::Comment < Ohm::Model
813
+ end
814
+
815
+ class ::Model::Rating < Ohm::Model
816
+ end
817
+
818
+ class ::Model::Note < Ohm::Model
819
+ attribute :content
820
+ reference :source, Model::Post
821
+ collection :comments, Model::Comment
822
+ list :ratings, Model::Rating
823
+ end
824
+
825
+ class ::Model::Comment
826
+ reference :note, Model::Note
827
+ end
828
+
829
+ class ::Model::Rating
830
+ attribute :value
831
+ end
832
+
833
+ class ::Model::Editor < Ohm::Model
834
+ attribute :name
835
+ reference :post, Model::Post
836
+ end
837
+
838
+ class ::Model::Post < Ohm::Model
839
+ reference :author, Model::Person
840
+ collection :notes, Model::Note, :source
841
+ collection :editors, Model::Editor
842
+ end
843
+
844
+ setup do
845
+ @post = Model::Post.create
846
+ end
847
+
848
+ context "a reference to another object" do
849
+ should "return an instance of Person if author_id has a valid id" do
850
+ @post.author_id = Model::Person.create(:name => "Albert").id
851
+ @post.save
852
+ assert_equal "Albert", Model::Post[@post.id].author.name
853
+ end
854
+
855
+ should "assign author_id if author is sent a valid instance" do
856
+ @post.author = Model::Person.create(:name => "Albert")
857
+ @post.save
858
+ assert_equal "Albert", Model::Post[@post.id].author.name
859
+ end
860
+
861
+ should "assign nil if nil is passed to author" do
862
+ @post.author = nil
863
+ @post.save
864
+ assert_nil Model::Post[@post.id].author
865
+ end
866
+
867
+ should "be cached in an instance variable" do
868
+ @author = Model::Person.create(:name => "Albert")
869
+ @post.update(:author => @author)
870
+
871
+ assert_equal @author, @post.author
872
+ assert @post.author.object_id == @post.author.object_id
873
+
874
+ @post.update(:author => Model::Person.create(:name => "Bertrand"))
875
+
876
+ assert_equal "Bertrand", @post.author.name
877
+ assert @post.author.object_id == @post.author.object_id
878
+
879
+ @post.update(:author_id => Model::Person.create(:name => "Charles").id)
880
+
881
+ assert_equal "Charles", @post.author.name
882
+ end
883
+ end
884
+
885
+ context "a collection of other objects" do
886
+ setup do
887
+ @note = Model::Note.create(:content => "Interesting stuff", :source => @post)
888
+ @comment = Model::Comment.create(:note => @note)
889
+ end
890
+
891
+ should "return a set of notes" do
892
+ assert_equal @note.source, @post
893
+ assert_equal @note, @post.notes.first
894
+ end
895
+
896
+ should "return a set of comments" do
897
+ assert_equal @comment, @note.comments.first
898
+ end
899
+
900
+ should "return a list of ratings" do
901
+ @rating = Model::Rating.create(:value => 5)
902
+ @note.ratings << @rating
903
+
904
+ assert_equal @rating, @note.ratings.first
905
+ end
906
+
907
+ should "default to the current class name" do
908
+ @editor = Model::Editor.create(:name => "Albert", :post => @post)
909
+
910
+ assert_equal @editor, @post.editors.first
911
+ end
912
+ end
913
+ end
914
+
915
+ context "Models connected to different databases" do
916
+ class ::Model::Car < Ohm::Model
917
+ attribute :name
918
+ end
919
+
920
+ class ::Model::Make < Ohm::Model
921
+ attribute :name
922
+ end
923
+
924
+ setup do
925
+ Model::Car.connect(:port => 6379, :db => 14)
926
+ end
927
+
928
+ teardown do
929
+ Model::Car.db.flushdb
930
+ end
931
+
932
+ should "save to the selected database" do
933
+ car = Model::Car.create(:name => "Twingo")
934
+ make = Model::Make.create(:name => "Renault")
935
+
936
+ assert_equal ["1"], Redis.new(:db => 15).smembers("Model::Make:all")
937
+ assert_equal [], Redis.new(:db => 15).smembers("Model::Car:all")
938
+
939
+ assert_equal ["1"], Redis.new(:db => 14).smembers("Model::Car:all")
940
+ assert_equal [], Redis.new(:db => 14).smembers("Model::Make:all")
941
+
942
+ assert_equal car, Model::Car[1]
943
+ assert_equal make, Model::Make[1]
944
+
945
+ Model::Make.db.flushdb
946
+
947
+ assert_equal car, Model::Car[1]
948
+ assert_nil Model::Make[1]
949
+ end
950
+ end
951
+ end