ohm 0.0.38 → 0.1.0.rc1

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.
@@ -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