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.
- data/README.markdown +25 -0
- data/Rakefile +2 -10
- data/lib/ohm.rb +33 -40
- data/lib/ohm/collection.rb +2 -2
- data/lib/ohm/key.rb +13 -35
- data/lib/ohm/utils/upgrade.rb +53 -0
- data/test/indices_test.rb +1 -1
- data/test/model_module_test.rb +951 -0
- data/test/model_test.rb +41 -15
- data/test/test.conf +1 -1
- data/test/test_helper.rb +3 -3
- data/test/upgrade_script_test.rb +68 -0
- metadata +19 -11
- data/test/benchmarks.rb +0 -39
- data/test/circular_reference_test.rb +0 -28
- data/test/wrapper_test.rb +0 -20
data/README.markdown
CHANGED
@@ -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
|
-
|
28
|
-
|
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.
|
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("
|
152
|
+
options.merge!(:by => model.key("*->#{att}"))
|
153
153
|
|
154
154
|
if options[:get]
|
155
|
-
raw.sort(options.merge(:get => model.key("
|
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}): #{
|
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
|
228
|
+
# Apply a Redis operation on a collection of sets.
|
229
229
|
def apply(operation, hash, glue)
|
230
|
-
|
231
|
-
|
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}): #{
|
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(
|
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.
|
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
|
-
|
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
|
-
|
698
|
+
attributes.each_with_index do |att, index|
|
699
|
+
value = send(att).to_s
|
702
700
|
|
703
|
-
|
704
|
-
|
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
|
-
|
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.
|
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
|
-
|
746
|
+
self.class.all << self
|
755
747
|
end
|
756
748
|
|
757
749
|
def delete_model_membership
|
758
|
-
db.
|
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
|
-
|
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.
|
799
|
+
value = db.hget(key, att)
|
807
800
|
value.respond_to?(:force_encoding) ?
|
808
801
|
value.force_encoding("UTF-8") :
|
809
802
|
value
|
data/lib/ohm/collection.rb
CHANGED
@@ -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.
|
data/lib/ohm/key.rb
CHANGED
@@ -1,50 +1,28 @@
|
|
1
1
|
module Ohm
|
2
2
|
|
3
3
|
# Represents a key in Redis.
|
4
|
-
class Key
|
5
|
-
|
6
|
-
attr :glue
|
7
|
-
attr :namespace
|
4
|
+
class Key < String
|
5
|
+
Volatile = new("~")
|
8
6
|
|
9
|
-
def self.[](*
|
10
|
-
|
7
|
+
def self.[](*args)
|
8
|
+
new(args.join(":"))
|
11
9
|
end
|
12
10
|
|
13
|
-
def
|
14
|
-
|
15
|
-
@glue = glue
|
16
|
-
@namespace = namespace
|
11
|
+
def [](key)
|
12
|
+
self.class[self, key]
|
17
13
|
end
|
18
14
|
|
19
|
-
def
|
20
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
def volatile
|
42
|
-
@namespace = [:~]
|
43
|
-
self
|
19
|
+
def +(other)
|
20
|
+
self.class.new("#{self}+#{other}")
|
44
21
|
end
|
45
22
|
|
46
|
-
def
|
47
|
-
|
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
|
data/test/indices_test.rb
CHANGED
@@ -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 "
|
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
|