crnixon-mongomapper 0.2.0

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.
Files changed (41) hide show
  1. data/.gitignore +6 -0
  2. data/History +24 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +37 -0
  5. data/Rakefile +71 -0
  6. data/VERSION +1 -0
  7. data/lib/mongomapper.rb +60 -0
  8. data/lib/mongomapper/associations.rb +69 -0
  9. data/lib/mongomapper/associations/array_proxy.rb +6 -0
  10. data/lib/mongomapper/associations/base.rb +50 -0
  11. data/lib/mongomapper/associations/belongs_to_proxy.rb +26 -0
  12. data/lib/mongomapper/associations/has_many_embedded_proxy.rb +19 -0
  13. data/lib/mongomapper/associations/has_many_proxy.rb +28 -0
  14. data/lib/mongomapper/associations/polymorphic_belongs_to_proxy.rb +31 -0
  15. data/lib/mongomapper/associations/proxy.rb +60 -0
  16. data/lib/mongomapper/callbacks.rb +106 -0
  17. data/lib/mongomapper/document.rb +262 -0
  18. data/lib/mongomapper/embedded_document.rb +226 -0
  19. data/lib/mongomapper/finder_options.rb +81 -0
  20. data/lib/mongomapper/key.rb +82 -0
  21. data/lib/mongomapper/observing.rb +50 -0
  22. data/lib/mongomapper/rails_compatibility.rb +23 -0
  23. data/lib/mongomapper/save_with_validation.rb +19 -0
  24. data/lib/mongomapper/serialization.rb +55 -0
  25. data/lib/mongomapper/serializers/json_serializer.rb +77 -0
  26. data/lib/mongomapper/validations.rb +47 -0
  27. data/mongomapper.gemspec +104 -0
  28. data/test/serializers/test_json_serializer.rb +104 -0
  29. data/test/test_associations.rb +174 -0
  30. data/test/test_callbacks.rb +84 -0
  31. data/test/test_document.rb +944 -0
  32. data/test/test_embedded_document.rb +253 -0
  33. data/test/test_finder_options.rb +148 -0
  34. data/test/test_helper.rb +62 -0
  35. data/test/test_key.rb +200 -0
  36. data/test/test_mongomapper.rb +28 -0
  37. data/test/test_observing.rb +101 -0
  38. data/test/test_rails_compatibility.rb +29 -0
  39. data/test/test_serializations.rb +54 -0
  40. data/test/test_validations.rb +393 -0
  41. metadata +154 -0
@@ -0,0 +1,104 @@
1
+ require 'test_helper'
2
+
3
+ class JsonSerializationTest < Test::Unit::TestCase
4
+ class Contact
5
+ include MongoMapper::EmbeddedDocument
6
+ key :name, String
7
+ key :age, Integer
8
+ key :created_at, Time
9
+ key :awesome, Boolean
10
+ key :preferences, Hash
11
+ end
12
+
13
+ def setup
14
+ Contact.include_root_in_json = false
15
+ @contact = Contact.new(
16
+ :name => 'Konata Izumi',
17
+ :age => 16,
18
+ :created_at => Time.utc(2006, 8, 1),
19
+ :awesome => true,
20
+ :preferences => { :shows => 'anime' }
21
+ )
22
+ end
23
+
24
+ should "include demodulized root" do
25
+ Contact.include_root_in_json = true
26
+ assert_match %r{^\{"contact": \{}, @contact.to_json
27
+ end
28
+
29
+ should "encode all encodable attributes" do
30
+ json = @contact.to_json
31
+
32
+ assert_match %r{"name": "Konata Izumi"}, json
33
+ assert_match %r{"age": 16}, json
34
+ assert json.include?(%("created_at": #{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
35
+ assert_match %r{"awesome": true}, json
36
+ assert_match %r{"preferences": \{"shows": "anime"\}}, json
37
+ end
38
+
39
+ should "allow attribute filtering with only" do
40
+ json = @contact.to_json(:only => [:name, :age])
41
+
42
+ assert_match %r{"name": "Konata Izumi"}, json
43
+ assert_match %r{"age": 16}, json
44
+ assert_no_match %r{"awesome"}, json
45
+ assert_no_match %r{"created_at"}, json
46
+ assert_no_match %r{"preferences"}, json
47
+ end
48
+
49
+ should "allow attribute filtering with except" do
50
+ json = @contact.to_json(:except => [:name, :age])
51
+
52
+ assert_no_match %r{"name"}, json
53
+ assert_no_match %r{"age"}, json
54
+ assert_match %r{"awesome"}, json
55
+ assert_match %r{"created_at"}, json
56
+ assert_match %r{"preferences"}, json
57
+ end
58
+
59
+ context "including methods" do
60
+ setup do
61
+ def @contact.label; "Has cheezburger"; end
62
+ def @contact.favorite_quote; "Constraints are liberating"; end
63
+ end
64
+
65
+ should "include single method" do
66
+ # Single method.
67
+ assert_match %r{"label": "Has cheezburger"}, @contact.to_json(:only => :name, :methods => :label)
68
+ end
69
+
70
+ should "include multiple methods" do
71
+ json = @contact.to_json(:only => :name, :methods => [:label, :favorite_quote])
72
+ assert_match %r{"label": "Has cheezburger"}, json
73
+ assert_match %r{"favorite_quote": "Constraints are liberating"}, json
74
+ end
75
+ end
76
+
77
+ context "array of records" do
78
+ setup do
79
+ @contacts = [
80
+ Contact.new(:name => 'David', :age => 39),
81
+ Contact.new(:name => 'Mary', :age => 14)
82
+ ]
83
+ end
84
+
85
+ should "allow attribute filtering with only" do
86
+ assert_equal %([{"name": "David"}, {"name": "Mary"}]), @contacts.to_json(:only => :name)
87
+ end
88
+
89
+ should "allow attribute filtering with except" do
90
+ json = @contacts.to_json(:except => [:name, :preferences, :awesome, :created_at])
91
+ assert_equal %([{"age": 39}, {"age": 14}]), json
92
+ end
93
+ end
94
+
95
+ should "allow options for hash of records" do
96
+ contacts = {
97
+ 1 => Contact.new(:name => 'David', :age => 39),
98
+ 2 => Contact.new(:name => 'Mary', :age => 14)
99
+ }
100
+
101
+ assert_equal %({"1": {"name": "David"}}), contacts.to_json(:only => [1, :name])
102
+ end
103
+
104
+ end
@@ -0,0 +1,174 @@
1
+ require 'test_helper'
2
+
3
+ class Address
4
+ include MongoMapper::EmbeddedDocument
5
+
6
+ key :address, String
7
+ key :city, String
8
+ key :state, String
9
+ key :zip, Integer
10
+ end
11
+
12
+ class Project
13
+ include MongoMapper::Document
14
+
15
+ key :name, String
16
+
17
+ many :statuses
18
+ many :addresses
19
+ end
20
+
21
+ class Status
22
+ include MongoMapper::Document
23
+
24
+ belongs_to :project
25
+ belongs_to :target, :polymorphic => true
26
+
27
+ key :name, String
28
+ end
29
+
30
+ class Person
31
+ include MongoMapper::EmbeddedDocument
32
+ key :name, String
33
+ key :child, Person
34
+ end
35
+
36
+ class AssociationsTest < Test::Unit::TestCase
37
+ def setup
38
+ Project.collection.clear
39
+ Status.collection.clear
40
+ end
41
+
42
+ context "Polymorphic Belongs To" do
43
+ should "default to nil" do
44
+ status = Status.new
45
+ status.target.should be_nil
46
+ end
47
+
48
+ should "store the association" do
49
+ status = Status.new
50
+ project = Project.new(:name => "mongomapper")
51
+ status.target = project
52
+ status.save.should be_true
53
+
54
+ from_db = Status.find(status.id)
55
+ from_db.target.should_not be_nil
56
+ from_db.target_id.should == project.id
57
+ from_db.target_type.should == "Project"
58
+ from_db.target.name.should == "mongomapper"
59
+ end
60
+
61
+ should "unset the association" do
62
+ status = Status.new
63
+ project = Project.new(:name => "mongomapper")
64
+ status.target = project
65
+ status.save.should be_true
66
+
67
+ from_db = Status.find(status.id)
68
+ from_db.target = nil
69
+ from_db.target_type.should be_nil
70
+ from_db.target_id.should be_nil
71
+ from_db.target.should be_nil
72
+ end
73
+ end
74
+
75
+ context "Belongs To" do
76
+ should "default to nil" do
77
+ status = Status.new
78
+ status.project.should be_nil
79
+ end
80
+
81
+ should "store the association" do
82
+ status = Status.new
83
+ project = Project.new(:name => "mongomapper")
84
+ status.project = project
85
+ status.save.should be_true
86
+
87
+ from_db = Status.find(status.id)
88
+ from_db.project.should_not be_nil
89
+ from_db.project.name.should == "mongomapper"
90
+ end
91
+
92
+ should "unset the association" do
93
+ status = Status.new
94
+ project = Project.new(:name => "mongomapper")
95
+ status.project = project
96
+ status.save.should be_true
97
+
98
+ from_db = Status.find(status.id)
99
+ from_db.project = nil
100
+ from_db.project.should be_nil
101
+ end
102
+ end
103
+
104
+ context "Many documents" do
105
+ should "default reader to empty array" do
106
+ project = Project.new
107
+ project.statuses.should == []
108
+ end
109
+
110
+ should "allow adding to association like it was an array" do
111
+ project = Project.new
112
+ project.statuses << Status.new
113
+ project.statuses.push Status.new
114
+ project.statuses.size.should == 2
115
+ end
116
+
117
+ should "store the association" do
118
+ project = Project.new
119
+ project.statuses = [Status.new("name" => "ready")]
120
+ project.save.should be_true
121
+
122
+ from_db = Project.find(project.id)
123
+ from_db.statuses.size.should == 1
124
+ from_db.statuses[0].name.should == "ready"
125
+ end
126
+ end
127
+
128
+ context "Many embedded documents" do
129
+ should "default reader to empty array" do
130
+ project = Project.new
131
+ project.addresses.should == []
132
+ end
133
+
134
+ should "allow adding to association like it was an array" do
135
+ project = Project.new
136
+ project.addresses << Address.new
137
+ project.addresses.push Address.new
138
+ project.addresses.size.should == 2
139
+ end
140
+
141
+ should "be embedded in document on save" do
142
+ sb = Address.new(:city => 'South Bend', :state => 'IN')
143
+ chi = Address.new(:city => 'Chicago', :state => 'IL')
144
+ project = Project.new
145
+ project.addresses << sb
146
+ project.addresses << chi
147
+ project.save
148
+
149
+ from_db = Project.find(project.id)
150
+ from_db.addresses.size.should == 2
151
+ from_db.addresses[0].should == sb
152
+ from_db.addresses[1].should == chi
153
+ end
154
+
155
+ should "allow embedding arbitrarily deep" do
156
+ @document = Class.new do
157
+ include MongoMapper::Document
158
+ key :person, Person
159
+ end
160
+
161
+ meg = Person.new(:name => "Meg")
162
+ meg.child = Person.new(:name => "Steve")
163
+ meg.child.child = Person.new(:name => "Linda")
164
+
165
+ doc = @document.new(:person => meg)
166
+ doc.save
167
+
168
+ from_db = @document.find(doc.id)
169
+ from_db.person.name.should == 'Meg'
170
+ from_db.person.child.name.should == 'Steve'
171
+ from_db.person.child.child.name.should == 'Linda'
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,84 @@
1
+ require 'test_helper'
2
+
3
+ class CallbacksTest < Test::Unit::TestCase
4
+ context "Defining and running callbacks" do
5
+ setup do
6
+ @document = Class.new do
7
+ include MongoMapper::Document
8
+
9
+ key :name, String
10
+
11
+ [ :before_validation_on_create, :before_validation_on_update,
12
+ :before_validation, :after_validation,
13
+ :before_create, :after_create,
14
+ :before_update, :after_update,
15
+ :before_save, :after_save,
16
+ :before_destroy, :after_destroy].each do |callback|
17
+ callback_method = "#{callback}_callback"
18
+ send(callback, callback_method)
19
+ define_method(callback_method) do
20
+ history << callback.to_sym
21
+ end
22
+ end
23
+
24
+ def history
25
+ @history ||= []
26
+ end
27
+
28
+ def clear_history
29
+ @history = nil
30
+ end
31
+ end
32
+ @document.collection.clear
33
+ end
34
+
35
+ should "get the order right for creating documents" do
36
+ doc = @document.create(:name => 'John Nunemaker')
37
+ doc.history.should == [:before_validation, :before_validation_on_create, :after_validation, :before_save, :before_create, :after_create, :after_save]
38
+ end
39
+
40
+ should "get the order right for updating documents" do
41
+ doc = @document.create(:name => 'John Nunemaker')
42
+ doc.clear_history
43
+ doc.name = 'John'
44
+ doc.save
45
+ doc.history.should == [:before_validation, :before_validation_on_update, :after_validation, :before_save, :before_update, :after_update, :after_save]
46
+ end
47
+
48
+ should "work for before and after validation" do
49
+ doc = @document.new(:name => 'John Nunemaker')
50
+ doc.valid?
51
+ doc.history.should include(:before_validation)
52
+ doc.history.should include(:after_validation)
53
+ end
54
+
55
+ should "work for before and after create" do
56
+ doc = @document.create(:name => 'John Nunemaker')
57
+ doc.history.should include(:before_create)
58
+ doc.history.should include(:after_create)
59
+ end
60
+
61
+ should "work for before and after update" do
62
+ doc = @document.create(:name => 'John Nunemaker')
63
+ doc.name = 'John Doe'
64
+ doc.save
65
+ doc.history.should include(:before_update)
66
+ doc.history.should include(:after_update)
67
+ end
68
+
69
+ should "work for before and after save" do
70
+ doc = @document.new
71
+ doc.name = 'John Doe'
72
+ doc.save
73
+ doc.history.should include(:before_save)
74
+ doc.history.should include(:after_save)
75
+ end
76
+
77
+ should "work for before and after destroy" do
78
+ doc = @document.create(:name => 'John Nunemaker')
79
+ doc.destroy
80
+ doc.history.should include(:before_destroy)
81
+ doc.history.should include(:after_destroy)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,944 @@
1
+ require 'test_helper'
2
+
3
+ class Address
4
+ include MongoMapper::EmbeddedDocument
5
+ key :city, String
6
+ key :state, String
7
+ end
8
+
9
+ class DocumentTest < Test::Unit::TestCase
10
+ context "The Document Class" do
11
+ setup do
12
+ @document = Class.new do
13
+ include MongoMapper::Document
14
+ end
15
+ end
16
+
17
+ should "track its descendants" do
18
+ MongoMapper::Document.descendants.should include(@document)
19
+ end
20
+
21
+ should "be able to define a key" do
22
+ key = @document.key(:name, String)
23
+ key.name.should == 'name'
24
+ key.type.should == String
25
+ key.should be_instance_of(MongoMapper::Key)
26
+ end
27
+
28
+ should "be able to define a key with options" do
29
+ key = @document.key(:name, String, :required => true)
30
+ key.options[:required].should be(true)
31
+ end
32
+
33
+ should "know what keys have been defined" do
34
+ @document.key(:name, String)
35
+ @document.key(:age, Integer)
36
+ @document.keys['name'].name.should == 'name'
37
+ @document.keys['name'].type.should == String
38
+ @document.keys['age'].name.should == 'age'
39
+ @document.keys['age'].type.should == Integer
40
+ end
41
+
42
+ should "use default database by default" do
43
+ @document.database.should == MongoMapper.database
44
+ end
45
+
46
+ should "have a connection" do
47
+ @document.connection.should be_instance_of(XGen::Mongo::Driver::Mongo)
48
+ end
49
+
50
+ should "allow setting different connection without affecting the default" do
51
+ conn = XGen::Mongo::Driver::Mongo.new
52
+ @document.connection conn
53
+ @document.connection.should == conn
54
+ @document.connection.should_not == MongoMapper.connection
55
+ end
56
+
57
+ should "allow setting a different database without affecting the default" do
58
+ @document.database AlternateDatabase
59
+ @document.database.name.should == AlternateDatabase
60
+
61
+ another_document = Class.new do
62
+ include MongoMapper::Document
63
+ end
64
+ another_document.database.should == MongoMapper.database
65
+ end
66
+
67
+ class Item; include MongoMapper::Document; end
68
+ should "default collection name to class name tableized" do
69
+ Item.collection.should be_instance_of(XGen::Mongo::Driver::Collection)
70
+ Item.collection.name.should == 'items'
71
+ end
72
+
73
+ should "allow setting the collection name" do
74
+ @document.collection('foobar')
75
+ @document.collection.should be_instance_of(XGen::Mongo::Driver::Collection)
76
+ @document.collection.name.should == 'foobar'
77
+ end
78
+ end # Document class
79
+
80
+ context "Database operations" do
81
+ setup do
82
+ @document = Class.new do
83
+ include MongoMapper::Document
84
+ collection 'users'
85
+
86
+ key :fname, String
87
+ key :lname, String
88
+ key :age, Integer
89
+ end
90
+
91
+ @document.collection.clear
92
+ end
93
+
94
+ context "Using key with type Array" do
95
+ setup do
96
+ @document.key :tags, Array
97
+ end
98
+
99
+ should "work" do
100
+ doc = @document.new
101
+ doc.tags.should == []
102
+ doc.tags = %w(foo bar)
103
+ doc.save
104
+ doc.tags.should == %w(foo bar)
105
+ @document.find(doc.id).tags.should == %w(foo bar)
106
+ end
107
+ end
108
+
109
+ context "Using key with type Hash" do
110
+ setup do
111
+ @document.key :foo, Hash
112
+ end
113
+
114
+ should "work with indifferent access" do
115
+ doc = @document.new
116
+ doc.foo = {:baz => 'bar'}
117
+ doc.save
118
+
119
+ doc = @document.find(doc.id)
120
+ doc.foo[:baz].should == 'bar'
121
+ doc.foo['baz'].should == 'bar'
122
+ end
123
+ end
124
+
125
+ context "Saving a document with a key that is an embedded document" do
126
+ setup do
127
+ @document.class_eval do
128
+ key :foo, Address
129
+ end
130
+ end
131
+
132
+ should "embed embedded document" do
133
+ address = Address.new(:city => 'South Bend', :state => 'IN')
134
+ doc = @document.new(:foo => address)
135
+ doc.save
136
+ doc.foo.city.should == 'South Bend'
137
+ doc.foo.state.should == 'IN'
138
+
139
+ from_db = @document.find(doc.id)
140
+ from_db.foo.city.should == 'South Bend'
141
+ from_db.foo.state.should == 'IN'
142
+ end
143
+ end
144
+
145
+ context "Creating a single document" do
146
+ setup do
147
+ @doc_instance = @document.create({:fname => 'John', :lname => 'Nunemaker', :age => '27'})
148
+ end
149
+
150
+ should "create a document in correct collection" do
151
+ @document.count.should == 1
152
+ end
153
+
154
+ should "automatically set id" do
155
+ @doc_instance.id.should_not be_nil
156
+ @doc_instance.id.size.should == 24
157
+ end
158
+
159
+ should "return instance of document" do
160
+ @doc_instance.should be_instance_of(@document)
161
+ @doc_instance.fname.should == 'John'
162
+ @doc_instance.lname.should == 'Nunemaker'
163
+ @doc_instance.age.should == 27
164
+ end
165
+ end
166
+
167
+ context "Creating multiple documents" do
168
+ setup do
169
+ @doc_instances = @document.create([
170
+ {:fname => 'John', :lname => 'Nunemaker', :age => '27'},
171
+ {:fname => 'Steve', :lname => 'Smith', :age => '28'},
172
+ ])
173
+ end
174
+
175
+ should "create multiple documents" do
176
+ @document.count.should == 2
177
+ end
178
+
179
+ should "return an array of doc instances" do
180
+ @doc_instances.map do |doc_instance|
181
+ doc_instance.should be_instance_of(@document)
182
+ end
183
+ end
184
+ end
185
+
186
+ context "Updating a document" do
187
+ setup do
188
+ doc = @document.create({:fname => 'John', :lname => 'Nunemaker', :age => '27'})
189
+ @doc_instance = @document.update(doc.id, {:age => 40})
190
+ end
191
+
192
+ should "update attributes provided" do
193
+ @doc_instance.age.should == 40
194
+ end
195
+
196
+ should "not update existing attributes that were not set to update" do
197
+ @doc_instance.fname.should == 'John'
198
+ @doc_instance.lname.should == 'Nunemaker'
199
+ end
200
+
201
+ should "not create new document" do
202
+ @document.count.should == 1
203
+ end
204
+ end
205
+
206
+ should "raise error when updating single doc if not provided id and attributes" do
207
+ doc = @document.create({:fname => 'John', :lname => 'Nunemaker', :age => '27'})
208
+ lambda { @document.update }.should raise_error(ArgumentError)
209
+ lambda { @document.update(doc.id) }.should raise_error(ArgumentError)
210
+ lambda { @document.update(doc.id, [1]) }.should raise_error(ArgumentError)
211
+ end
212
+
213
+ context "Updating multiple documents" do
214
+ setup do
215
+ @doc1 = @document.create({:fname => 'John', :lname => 'Nunemaker', :age => '27'})
216
+ @doc2 = @document.create({:fname => 'Steve', :lname => 'Smith', :age => '28'})
217
+
218
+ @doc_instances = @document.update({
219
+ @doc1.id => {:age => 30},
220
+ @doc2.id => {:age => 30},
221
+ })
222
+ end
223
+
224
+ should "not create any new documents" do
225
+ @document.count.should == 2
226
+ end
227
+
228
+ should "should return an array of doc instances" do
229
+ @doc_instances.map do |doc_instance|
230
+ doc_instance.should be_instance_of(@document)
231
+ end
232
+ end
233
+
234
+ should "update the documents" do
235
+ @document.find(@doc1.id).age.should == 30
236
+ @document.find(@doc2.id).age.should == 30
237
+ end
238
+ end
239
+
240
+ should "raise error when updating multiple documents if not a hash" do
241
+ lambda { @document.update([1, 2]) }.should raise_error(ArgumentError)
242
+ end
243
+
244
+ context "Finding documents" do
245
+ setup do
246
+ @doc1 = @document.create({:fname => 'John', :lname => 'Nunemaker', :age => '27'})
247
+ @doc2 = @document.create({:fname => 'Steve', :lname => 'Smith', :age => '28'})
248
+ @doc3 = @document.create({:fname => 'Steph', :lname => 'Nunemaker', :age => '26'})
249
+ end
250
+
251
+ should "raise document not found if nothing provided" do
252
+ lambda { @document.find }.should raise_error(MongoMapper::DocumentNotFound)
253
+ end
254
+
255
+ context "with a single id" do
256
+ should "work" do
257
+ @document.find(@doc1.id).should == @doc1
258
+ end
259
+
260
+ should "raise error if document not found" do
261
+ lambda { @document.find(1) }.should raise_error(MongoMapper::DocumentNotFound)
262
+ end
263
+ end
264
+
265
+ context "with multiple id's" do
266
+ should "work as arguments" do
267
+ @document.find(@doc1.id, @doc2.id).should == [@doc1, @doc2]
268
+ end
269
+
270
+ should "work as array" do
271
+ @document.find([@doc1.id, @doc2.id]).should == [@doc1, @doc2]
272
+ end
273
+ end
274
+
275
+ context "with :all" do
276
+ should "find all documents" do
277
+ @document.find(:all).should == [@doc1, @doc2, @doc3]
278
+ end
279
+
280
+ should "be able to add conditions" do
281
+ @document.find(:all, :conditions => {:fname => 'John'}).should == [@doc1]
282
+ end
283
+ end
284
+
285
+ context "with #all" do
286
+ should "find all documents based on criteria" do
287
+ @document.all.should == [@doc1, @doc2, @doc3]
288
+ @document.all(:conditions => {:lname => 'Nunemaker'}).should == [@doc1, @doc3]
289
+ end
290
+ end
291
+
292
+ context "with :first" do
293
+ should "find first document" do
294
+ @document.find(:first).should == @doc1
295
+ end
296
+ end
297
+
298
+ context "with #first" do
299
+ should "find first document based on criteria" do
300
+ @document.first.should == @doc1
301
+ @document.first(:conditions => {:age => 28}).should == @doc2
302
+ end
303
+ end
304
+
305
+ context "with :last" do
306
+ should "find last document" do
307
+ @document.find(:last).should == @doc3
308
+ end
309
+ end
310
+
311
+ context "with #last" do
312
+ should "find last document based on criteria" do
313
+ @document.last.should == @doc3
314
+ @document.last(:conditions => {:age => 28}).should == @doc2
315
+ end
316
+ end
317
+ end # finding documents
318
+
319
+ context "Finding document by id" do
320
+ setup do
321
+ @doc1 = @document.create({:fname => 'John', :lname => 'Nunemaker', :age => '27'})
322
+ @doc2 = @document.create({:fname => 'Steve', :lname => 'Smith', :age => '28'})
323
+ end
324
+
325
+ should "be able to find by id" do
326
+ @document.find_by_id(@doc1.id).should == @doc1
327
+ @document.find_by_id(@doc2.id).should == @doc2
328
+ end
329
+
330
+ should "return nil if document not found" do
331
+ @document.find_by_id(1234).should be(nil)
332
+ end
333
+ end
334
+
335
+ context "Deleting a document" do
336
+ setup do
337
+ @doc1 = @document.create({:fname => 'John', :lname => 'Nunemaker', :age => '27'})
338
+ @doc2 = @document.create({:fname => 'Steve', :lname => 'Smith', :age => '28'})
339
+ @document.delete(@doc1.id)
340
+ end
341
+
342
+ should "remove document from collection" do
343
+ @document.count.should == 1
344
+ end
345
+
346
+ should "not remove other documents" do
347
+ @document.find(@doc2.id).should_not be(nil)
348
+ end
349
+ end
350
+
351
+ context "Deleting multiple documents" do
352
+ should "work with multiple arguments" do
353
+ @doc1 = @document.create({:fname => 'John', :lname => 'Nunemaker', :age => '27'})
354
+ @doc2 = @document.create({:fname => 'Steve', :lname => 'Smith', :age => '28'})
355
+ @doc3 = @document.create({:fname => 'Steph', :lname => 'Nunemaker', :age => '26'})
356
+ @document.delete(@doc1.id, @doc2.id)
357
+
358
+ @document.count.should == 1
359
+ end
360
+
361
+ should "work with array as argument" do
362
+ @doc1 = @document.create({:fname => 'John', :lname => 'Nunemaker', :age => '27'})
363
+ @doc2 = @document.create({:fname => 'Steve', :lname => 'Smith', :age => '28'})
364
+ @doc3 = @document.create({:fname => 'Steph', :lname => 'Nunemaker', :age => '26'})
365
+ @document.delete([@doc1.id, @doc2.id])
366
+
367
+ @document.count.should == 1
368
+ end
369
+ end
370
+
371
+ context "Deleting all documents" do
372
+ setup do
373
+ @doc1 = @document.create({:fname => 'John', :lname => 'Nunemaker', :age => '27'})
374
+ @doc2 = @document.create({:fname => 'Steve', :lname => 'Smith', :age => '28'})
375
+ @doc3 = @document.create({:fname => 'Steph', :lname => 'Nunemaker', :age => '26'})
376
+ end
377
+
378
+ should "remove all documents when given no conditions" do
379
+ @document.delete_all
380
+ @document.count.should == 0
381
+ end
382
+
383
+ should "only remove matching documents when given conditions" do
384
+ @document.delete_all({:fname => 'John'})
385
+ @document.count.should == 2
386
+ end
387
+
388
+ should "convert the conditions to mongo criteria" do
389
+ @document.delete_all(:age => [26, 27])
390
+ @document.count.should == 1
391
+ end
392
+ end
393
+
394
+ context "Destroying a document" do
395
+ setup do
396
+ @doc1 = @document.create({:fname => 'John', :lname => 'Nunemaker', :age => '27'})
397
+ @doc2 = @document.create({:fname => 'Steve', :lname => 'Smith', :age => '28'})
398
+ @document.destroy(@doc1.id)
399
+ end
400
+
401
+ should "remove document from collection" do
402
+ @document.count.should == 1
403
+ end
404
+
405
+ should "not remove other documents" do
406
+ @document.find(@doc2.id).should_not be(nil)
407
+ end
408
+ end
409
+
410
+ context "Destroying multiple documents" do
411
+ should "work with multiple arguments" do
412
+ @doc1 = @document.create({:fname => 'John', :lname => 'Nunemaker', :age => '27'})
413
+ @doc2 = @document.create({:fname => 'Steve', :lname => 'Smith', :age => '28'})
414
+ @doc3 = @document.create({:fname => 'Steph', :lname => 'Nunemaker', :age => '26'})
415
+ @document.destroy(@doc1.id, @doc2.id)
416
+
417
+ @document.count.should == 1
418
+ end
419
+
420
+ should "work with array as argument" do
421
+ @doc1 = @document.create({:fname => 'John', :lname => 'Nunemaker', :age => '27'})
422
+ @doc2 = @document.create({:fname => 'Steve', :lname => 'Smith', :age => '28'})
423
+ @doc3 = @document.create({:fname => 'Steph', :lname => 'Nunemaker', :age => '26'})
424
+ @document.destroy([@doc1.id, @doc2.id])
425
+
426
+ @document.count.should == 1
427
+ end
428
+ end
429
+
430
+ context "Destroying all documents" do
431
+ setup do
432
+ @doc1 = @document.create({:fname => 'John', :lname => 'Nunemaker', :age => '27'})
433
+ @doc2 = @document.create({:fname => 'Steve', :lname => 'Smith', :age => '28'})
434
+ @doc3 = @document.create({:fname => 'Steph', :lname => 'Nunemaker', :age => '26'})
435
+ end
436
+
437
+ should "remove all documents when given no conditions" do
438
+ @document.destroy_all
439
+ @document.count.should == 0
440
+ end
441
+
442
+ should "only remove matching documents when given conditions" do
443
+ @document.destroy_all(:fname => 'John')
444
+ @document.count.should == 2
445
+ @document.destroy_all(:age => 26)
446
+ @document.count.should == 1
447
+ end
448
+
449
+ should "convert the conditions to mongo criteria" do
450
+ @document.destroy_all(:age => [26, 27])
451
+ @document.count.should == 1
452
+ end
453
+ end
454
+
455
+ context "Counting documents in collection" do
456
+ setup do
457
+ @doc1 = @document.create({:fname => 'John', :lname => 'Nunemaker', :age => '27'})
458
+ @doc2 = @document.create({:fname => 'Steve', :lname => 'Smith', :age => '28'})
459
+ @doc3 = @document.create({:fname => 'Steph', :lname => 'Nunemaker', :age => '26'})
460
+ end
461
+
462
+ should "count all with no arguments" do
463
+ @document.count.should == 3
464
+ end
465
+
466
+ should "return 0 if there are no documents in the collection" do
467
+ @document.delete_all
468
+ @document.count.should == 0
469
+ end
470
+
471
+ should "return 0 if the collection does not exist" do
472
+ klass = Class.new do
473
+ include MongoMapper::Document
474
+ collection 'foobarbazwickdoesnotexist'
475
+ end
476
+
477
+ klass.count.should == 0
478
+ end
479
+
480
+ should "return count for matching documents if conditions provided" do
481
+ @document.count(:age => 27).should == 1
482
+ end
483
+
484
+ should "convert the conditions to mongo criteria" do
485
+ @document.count(:age => [26, 27]).should == 2
486
+ end
487
+ end
488
+
489
+ context "Indexing" do
490
+ setup do
491
+ @document.collection.drop_indexes
492
+ end
493
+
494
+ should "allow creating index for a key" do
495
+ lambda {
496
+ @document.ensure_index :fname
497
+ }.should change { @document.collection.index_information.size }.by(1)
498
+
499
+ index = @document.collection.index_information['fname_1']
500
+ index.should_not be_nil
501
+ index.should include(['fname', 1])
502
+ end
503
+
504
+ should "allow creating unique index for a key" do
505
+ @document.collection.expects(:create_index).with(:fname, true)
506
+ @document.ensure_index :fname, :unique => true
507
+ end
508
+
509
+ should "allow creating index on multiple keys" do
510
+ lambda {
511
+ @document.ensure_index [[:fname, 1], [:lname, -1]]
512
+ }.should change { @document.collection.index_information.size }.by(1)
513
+
514
+ index = @document.collection.index_information['lname_-1_fname_1']
515
+ index.should_not be_nil
516
+ index.should include(['fname', 1])
517
+ index.should include(['lname', -1])
518
+ end
519
+
520
+ should "work with :index shortcut when defining key" do
521
+ lambda {
522
+ @document.key :father, String, :index => true
523
+ }.should change { @document.collection.index_information.size }.by(1)
524
+
525
+ index = @document.collection.index_information['father_1']
526
+ index.should_not be_nil
527
+ index.should include(['father', 1])
528
+ end
529
+ end
530
+ end # Database operations
531
+
532
+ context "An instance of a document" do
533
+ setup do
534
+ @document = Class.new do
535
+ include MongoMapper::Document
536
+
537
+ key :name, String
538
+ key :age, Integer
539
+ end
540
+ @document.collection.clear
541
+ end
542
+
543
+ should "have access to the class's collection" do
544
+ doc = @document.new
545
+ doc.collection.should == @document.collection
546
+ end
547
+
548
+ should "automatically have an _id key" do
549
+ @document.keys.keys.should include('_id')
550
+ end
551
+
552
+ should "automatically have a created_at key" do
553
+ @document.keys.keys.should include('created_at')
554
+ end
555
+
556
+ should "automatically have an updated_at key" do
557
+ @document.keys.keys.should include('updated_at')
558
+ end
559
+
560
+ should "use default values if defined for keys" do
561
+ @document.key :active, Boolean, :default => true
562
+
563
+ @document.new.active.should be_true
564
+ @document.new(:active => false).active.should be_false
565
+ end
566
+
567
+ context "new?" do
568
+ should "be true if no id" do
569
+ @document.new.new?.should be(true)
570
+ end
571
+
572
+ should "be true if has id but id not in database" do
573
+ @document.new('_id' => 1).new?.should be(true)
574
+ end
575
+
576
+ should "be false if has id and id is in database" do
577
+ doc = @document.create(:name => 'John Nunemaker', :age => 27)
578
+ doc.new?.should be(false)
579
+ end
580
+ end
581
+
582
+ context "mass assigning keys" do
583
+ should "update values for keys provided" do
584
+ doc = @document.new(:name => 'foobar', :age => 10)
585
+ doc.attributes = {:name => 'new value', :age => 5}
586
+ doc.attributes[:name].should == 'new value'
587
+ doc.attributes[:age].should == 5
588
+ end
589
+
590
+ should "not update values for keys that were not provided" do
591
+ doc = @document.new(:name => 'foobar', :age => 10)
592
+ doc.attributes = {:name => 'new value'}
593
+ doc.attributes[:name].should == 'new value'
594
+ doc.attributes[:age].should == 10
595
+ end
596
+
597
+ should "ignore keys that do not exist" do
598
+ doc = @document.new(:name => 'foobar', :age => 10)
599
+ doc.attributes = {:name => 'new value', :foobar => 'baz'}
600
+ doc.attributes[:name].should == 'new value'
601
+ doc.attributes[:foobar].should be(nil)
602
+ end
603
+
604
+ should "typecast key values" do
605
+ doc = @document.new(:name => 1234, :age => '21')
606
+ doc.name.should == '1234'
607
+ doc.age.should == 21
608
+ end
609
+ end
610
+
611
+ context "requesting keys" do
612
+ should "default to empty hash" do
613
+ doc = @document.new
614
+ doc.attributes.should == {}
615
+ end
616
+
617
+ should "return all keys that aren't nil" do
618
+ doc = @document.new(:name => 'string', :age => nil)
619
+ doc.attributes.should == {'name' => 'string'}
620
+ end
621
+ end
622
+
623
+ context "key shorcuts" do
624
+ should "be able to read key with []" do
625
+ doc = @document.new(:name => 'string')
626
+ doc[:name].should == 'string'
627
+ end
628
+
629
+ should "be able to write key value with []=" do
630
+ doc = @document.new
631
+ doc[:name] = 'string'
632
+ doc[:name].should == 'string'
633
+ end
634
+ end
635
+
636
+ context "indifferent access" do
637
+ should "be enabled for keys" do
638
+ doc = @document.new(:name => 'string')
639
+ doc.attributes[:name].should == 'string'
640
+ doc.attributes['name'].should == 'string'
641
+ end
642
+ end
643
+
644
+ context "reading an attribute" do
645
+ should "work for defined keys" do
646
+ doc = @document.new(:name => 'string')
647
+ doc.name.should == 'string'
648
+ end
649
+
650
+ should "raise no method error for undefined keys" do
651
+ doc = @document.new
652
+ lambda { doc.fart }.should raise_error(NoMethodError)
653
+ end
654
+
655
+ should "know if reader defined" do
656
+ doc = @document.new
657
+ doc.reader?('name').should be(true)
658
+ doc.reader?(:name).should be(true)
659
+ doc.reader?('age').should be(true)
660
+ doc.reader?(:age).should be(true)
661
+ doc.reader?('foobar').should be(false)
662
+ doc.reader?(:foobar).should be(false)
663
+ end
664
+
665
+ should "be accessible for use in the model" do
666
+ @document.class_eval do
667
+ def name_and_age
668
+ "#{read_attribute(:name)} (#{read_attribute(:age)})"
669
+ end
670
+ end
671
+
672
+ doc = @document.new(:name => 'John', :age => 27)
673
+ doc.name_and_age.should == 'John (27)'
674
+ end
675
+ end
676
+
677
+ context "reading an attribute before typcasting" do
678
+ should "work for defined keys" do
679
+ doc = @document.new(:name => 12)
680
+ doc.name_before_typecast.should == 12
681
+ end
682
+
683
+ should "raise no method error for undefined keys" do
684
+ doc = @document.new
685
+ lambda { doc.foo_before_typecast }.should raise_error(NoMethodError)
686
+ end
687
+
688
+ should "be accessible for use in a document" do
689
+ @document.class_eval do
690
+ def untypcasted_name
691
+ read_attribute_before_typecast(:name)
692
+ end
693
+ end
694
+
695
+ doc = @document.new(:name => 12)
696
+ doc.name.should == '12'
697
+ doc.untypcasted_name.should == 12
698
+ end
699
+ end
700
+
701
+ context "writing an attribute" do
702
+ should "work for defined keys" do
703
+ doc = @document.new
704
+ doc.name = 'John'
705
+ doc.name.should == 'John'
706
+ end
707
+
708
+ should "raise no method error for undefined keys" do
709
+ doc = @document.new
710
+ lambda { doc.fart = 'poof!' }.should raise_error(NoMethodError)
711
+ end
712
+
713
+ should "typecast value" do
714
+ doc = @document.new
715
+ doc.name = 1234
716
+ doc.name.should == '1234'
717
+ doc.age = '21'
718
+ doc.age.should == 21
719
+ end
720
+
721
+ should "know if writer defined" do
722
+ doc = @document.new
723
+ doc.writer?('name').should be(true)
724
+ doc.writer?('name=').should be(true)
725
+ doc.writer?(:name).should be(true)
726
+ doc.writer?('age').should be(true)
727
+ doc.writer?('age=').should be(true)
728
+ doc.writer?(:age).should be(true)
729
+ doc.writer?('foobar').should be(false)
730
+ doc.writer?('foobar=').should be(false)
731
+ doc.writer?(:foobar).should be(false)
732
+ end
733
+
734
+ should "be accessible for use in the model" do
735
+ @document.class_eval do
736
+ def name_and_age=(new_value)
737
+ new_value.match(/([^\(\s]+) \((.*)\)/)
738
+ write_attribute :name, $1
739
+ write_attribute :age, $2
740
+ end
741
+ end
742
+
743
+ doc = @document.new
744
+ doc.name_and_age = 'Frank (62)'
745
+ doc.name.should == 'Frank'
746
+ doc.age.should == 62
747
+ end
748
+ end # writing an attribute
749
+
750
+ context "respond_to?" do
751
+ setup do
752
+ @doc = @document.new
753
+ end
754
+
755
+ should "work for readers" do
756
+ @doc.respond_to?(:name).should be_true
757
+ @doc.respond_to?('name').should be_true
758
+ end
759
+
760
+ should "work for writers" do
761
+ @doc.respond_to?(:name=).should be_true
762
+ @doc.respond_to?('name=').should be_true
763
+ end
764
+
765
+ should "work for readers before typecast" do
766
+ @doc.respond_to?(:name_before_typecast).should be_true
767
+ @doc.respond_to?('name_before_typecast').should be_true
768
+ end
769
+ end
770
+
771
+ context "equality" do
772
+ should "be equal if id and class are the same" do
773
+ (@document.new('_id' => 1) == @document.new('_id' => 1)).should be(true)
774
+ end
775
+
776
+ should "not be equal if class same but id different" do
777
+ (@document.new('_id' => 1) == @document.new('_id' => 2)).should be(false)
778
+ end
779
+
780
+ should "not be equal if id same but class different" do
781
+ @another_document = Class.new do
782
+ include MongoMapper::Document
783
+ end
784
+
785
+ (@document.new('_id' => 1) == @another_document.new('_id' => 1)).should be(false)
786
+ end
787
+ end
788
+
789
+ context "Saving a new document" do
790
+ setup do
791
+ @doc = @document.new(:name => 'John Nunemaker', :age => '27')
792
+ @doc.save
793
+ end
794
+
795
+ should "insert document into the collection" do
796
+ @document.count.should == 1
797
+ end
798
+
799
+ should "assign an id for the document" do
800
+ @doc.id.should_not be(nil)
801
+ @doc.id.size.should == 24
802
+ end
803
+
804
+ should "save attributes" do
805
+ @doc.name.should == 'John Nunemaker'
806
+ @doc.age.should == 27
807
+ end
808
+
809
+ should "update attributes in the database" do
810
+ from_db = @document.find(@doc.id)
811
+ from_db.should == @doc
812
+ from_db.name.should == 'John Nunemaker'
813
+ from_db.age.should == 27
814
+ end
815
+ end
816
+
817
+ context "Saving an existing document" do
818
+ setup do
819
+ @doc = @document.create(:name => 'John Nunemaker', :age => '27')
820
+ @doc.name = 'John Doe'
821
+ @doc.age = 30
822
+ @doc.save
823
+ end
824
+
825
+ should "not insert document into collection" do
826
+ @document.count.should == 1
827
+ end
828
+
829
+ should "update attributes" do
830
+ @doc.name.should == 'John Doe'
831
+ @doc.age.should == 30
832
+ end
833
+
834
+ should "update attributes in the database" do
835
+ from_db = @document.find(@doc.id)
836
+ from_db.name.should == 'John Doe'
837
+ from_db.age.should == 30
838
+ end
839
+ end
840
+
841
+ context "Calling update attributes on a new document" do
842
+ setup do
843
+ @doc = @document.new(:name => 'John Nunemaker', :age => '27')
844
+ @doc.update_attributes(:name => 'John Doe', :age => 30)
845
+ end
846
+
847
+ should "insert document into the collection" do
848
+ @document.count.should == 1
849
+ end
850
+
851
+ should "assign an id for the document" do
852
+ @doc.id.should_not be(nil)
853
+ @doc.id.size.should == 24
854
+ end
855
+
856
+ should "save attributes" do
857
+ @doc.name.should == 'John Doe'
858
+ @doc.age.should == 30
859
+ end
860
+
861
+ should "update attributes in the database" do
862
+ from_db = @document.find(@doc.id)
863
+ from_db.should == @doc
864
+ from_db.name.should == 'John Doe'
865
+ from_db.age.should == 30
866
+ end
867
+ end
868
+
869
+ context "Updating an existing document using update attributes" do
870
+ setup do
871
+ @doc = @document.create(:name => 'John Nunemaker', :age => '27')
872
+ @doc.update_attributes(:name => 'John Doe', :age => 30)
873
+ end
874
+
875
+ should "not insert document into collection" do
876
+ @document.count.should == 1
877
+ end
878
+
879
+ should "update attributes" do
880
+ @doc.name.should == 'John Doe'
881
+ @doc.age.should == 30
882
+ end
883
+
884
+ should "update attributes in the database" do
885
+ from_db = @document.find(@doc.id)
886
+ from_db.name.should == 'John Doe'
887
+ from_db.age.should == 30
888
+ end
889
+ end
890
+
891
+ context "Destroying a document that exists" do
892
+ setup do
893
+ @doc = @document.create(:name => 'John Nunemaker', :age => '27')
894
+ @doc.destroy
895
+ end
896
+
897
+ should "remove the document from the collection" do
898
+ @document.count.should == 0
899
+ end
900
+
901
+ should "raise error if assignment is attempted" do
902
+ lambda { @doc.name = 'Foo' }.should raise_error(TypeError)
903
+ end
904
+ end
905
+
906
+ context "Destroying a document that is a new" do
907
+ setup do
908
+ setup do
909
+ @doc = @document.new(:name => 'John Nunemaker', :age => '27')
910
+ @doc.destroy
911
+ end
912
+
913
+ should "not affect collection count" do
914
+ @document.collection.count.should == 0
915
+ end
916
+
917
+ should "raise error if assignment is attempted" do
918
+ lambda { @doc.name = 'Foo' }.should raise_error(TypeError)
919
+ end
920
+ end
921
+ end
922
+
923
+ context "timestamping" do
924
+ should "set created_at and updated_at on create" do
925
+ doc = @document.new(:name => 'John Nunemaker', :age => 27)
926
+ doc.created_at.should be(nil)
927
+ doc.updated_at.should be(nil)
928
+ doc.save
929
+ doc.created_at.should_not be(nil)
930
+ doc.updated_at.should_not be(nil)
931
+ end
932
+
933
+ should "set updated_at on update but leave created_at alone" do
934
+ doc = @document.create(:name => 'John Nunemaker', :age => 27)
935
+ old_created_at = doc.created_at
936
+ old_updated_at = doc.updated_at
937
+ doc.name = 'John Doe'
938
+ doc.save
939
+ doc.created_at.should == old_created_at
940
+ doc.updated_at.should_not == old_updated_at
941
+ end
942
+ end
943
+ end # instance of a document
944
+ end # DocumentTest