mm_partial_update 0.1.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.
data/test/models.rb ADDED
@@ -0,0 +1,20 @@
1
+ class Person
2
+ include MongoMapper::Document
3
+ key :name, String
4
+ many :pets
5
+ one :favorite_pet, :class_name=>'Pet'
6
+ end
7
+
8
+ class Pet
9
+ include MongoMapper::EmbeddedDocument
10
+ key :name, String
11
+ key :age, Integer
12
+ many :fleas
13
+ one :favorite_flea, :class_name=>'Flea'
14
+ end
15
+
16
+ class Flea
17
+ include MongoMapper::EmbeddedDocument
18
+ key :name, String
19
+ end
20
+
@@ -0,0 +1,96 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ $:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
5
+ require 'mm_partial_update'
6
+ require 'fileutils'
7
+ require 'ostruct'
8
+
9
+ require 'log_buddy'
10
+ require 'matchy'
11
+ require 'shoulda'
12
+
13
+ class Test::Unit::TestCase
14
+ def Doc(name='Class', &block)
15
+ klass = Class.new
16
+ klass.class_eval do
17
+ include MongoMapper::Document
18
+ set_collection_name :test
19
+
20
+ if name
21
+ class_eval "def self.name; '#{name}' end"
22
+ class_eval "def self.to_s; '#{name}' end"
23
+ end
24
+ end
25
+
26
+ klass.class_eval(&block) if block_given?
27
+ klass.collection.remove
28
+ klass
29
+ end
30
+
31
+ def EDoc(name='Class', &block)
32
+ klass = Class.new
33
+ klass.class_eval do
34
+ include MongoMapper::EmbeddedDocument
35
+
36
+ if name
37
+ class_eval "def self.name; '#{name}' end"
38
+ class_eval "def self.to_s; '#{name}' end"
39
+ end
40
+ end
41
+
42
+ klass.class_eval(&block) if block_given?
43
+ klass
44
+ end
45
+
46
+ def drop_indexes(klass)
47
+ if klass.database.collection_names.include?(klass.collection.name)
48
+ klass.collection.drop_indexes
49
+ end
50
+ end
51
+
52
+ custom_matcher :be_true do |receiver, matcher, args|
53
+ matcher.positive_failure_message = "Expected #{receiver} to be true but it wasn't"
54
+ matcher.negative_failure_message = "Expected #{receiver} not to be true but it was"
55
+ receiver.eql?(true)
56
+ end
57
+
58
+ custom_matcher :be_false do |receiver, matcher, args|
59
+ matcher.positive_failure_message = "Expected #{receiver} to be false but it wasn't"
60
+ matcher.negative_failure_message = "Expected #{receiver} not to be false but it was"
61
+ receiver.eql?(false)
62
+ end
63
+
64
+ custom_matcher :have_error_on do |receiver, matcher, args|
65
+ receiver.valid?
66
+ attribute = args[0]
67
+ expected_message = args[1]
68
+
69
+ if expected_message.nil?
70
+ matcher.positive_failure_message = "#{receiver} had no errors on #{attribute}"
71
+ matcher.negative_failure_message = "#{receiver} had errors on #{attribute} #{receiver.errors.inspect}"
72
+ !receiver.errors[attribute].blank?
73
+ else
74
+ actual = receiver.errors[attribute]
75
+ matcher.positive_failure_message = %Q(Expected error on #{attribute} to be "#{expected_message}" but was "#{actual}")
76
+ matcher.negative_failure_message = %Q(Expected error on #{attribute} not to be "#{expected_message}" but was "#{actual}")
77
+ actual.include? expected_message
78
+ end
79
+ end
80
+
81
+ custom_matcher :have_index do |receiver, matcher, args|
82
+ index_name = args[0]
83
+ matcher.positive_failure_message = "#{receiver} does not have index named #{index_name}, but should"
84
+ matcher.negative_failure_message = "#{receiver} does have index named #{index_name}, but should not"
85
+ !receiver.collection.index_information.detect { |index| index[0] == index_name }.nil?
86
+ end
87
+ end
88
+
89
+ log_dir = File.expand_path('../../log', __FILE__)
90
+ FileUtils.mkdir_p(log_dir) unless File.exist?(log_dir)
91
+ logger = Logger.new(log_dir + '/test.log')
92
+
93
+ LogBuddy.init(:logger => logger)
94
+ MongoMapper.connection = Mongo::Connection.new('127.0.0.1', 27017, :logger => logger)
95
+ MongoMapper.database = "mm-test-#{RUBY_VERSION.gsub('.', '-')}"
96
+ MongoMapper.database.collections.each { |c| c.drop_indexes }
@@ -0,0 +1,9 @@
1
+ require 'test_helper'
2
+
3
+ class TestMmPartialUpdate < Test::Unit::TestCase
4
+ context "including mm_partial_update" do
5
+ should "include something or other" do
6
+ assert_equal defined?(MmPartialUpdate::Plugins::PartialUpdate), "constant"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,221 @@
1
+ require 'test_helper'
2
+ require "models"
3
+
4
+ class TestPartialUpdate < Test::Unit::TestCase
5
+
6
+ context "#prepare_update_command" do
7
+
8
+ context "on root documents" do
9
+
10
+ should "return an empty command if not changed" do
11
+ doc = Person.create!
12
+ doc.send(:prepare_update_command).to_h.empty?.should be_true
13
+ end
14
+
15
+ should "return to_mongo if the entity is new" do
16
+ doc = Person.new
17
+ doc.name = "hello"
18
+ doc.send(:prepare_update_command).to_h.should ==
19
+ {"$set"=>doc.to_mongo}
20
+ end
21
+
22
+ should "provide set changed fields" do
23
+ doc = Person.create
24
+ doc.name = "hello"
25
+ doc.send(:prepare_update_command).to_h.should ==
26
+ {"$set"=>{"name"=>"hello"}}
27
+ end
28
+
29
+ context "with changes to 'one' embedded associations" do
30
+ should "report a new child" do
31
+ doc = Person.create
32
+ doc.favorite_pet.build :name=>"Magma"
33
+ doc.send(:prepare_update_command).to_h.should == {
34
+ "$set"=>{'favorite_pet'=>doc.favorite_pet.to_mongo}
35
+ }
36
+ end
37
+
38
+ should "report changed child" do
39
+ doc = Person.new
40
+ doc.favorite_pet.build :name=>"Magma"
41
+ doc.save
42
+ doc.favorite_pet.name = "Ugly"
43
+ doc.send(:prepare_update_command).to_h.should == {
44
+ "$set"=>{"favorite_pet.name"=>"Ugly"}
45
+ }
46
+ end
47
+
48
+ should "report a deleted child" do
49
+ doc = Person.new
50
+ doc.favorite_pet.build :name=>"Magma"
51
+ doc.save
52
+ doc.favorite_pet = nil
53
+ doc.send(:prepare_update_command).to_h.should == {
54
+ "$set"=>{'favorite_pet'=>nil}
55
+ }
56
+ end
57
+
58
+ end
59
+ context "with changes to 'many' embedded associations" do
60
+ should "report a new child" do
61
+ doc = Person.create
62
+ doc.pets.build :name=>"Magma"
63
+ doc.send(:prepare_update_command).to_h.should == {
64
+ :pushes=>{"pets"=>[doc.pets[0].to_mongo]}
65
+ }
66
+ end
67
+
68
+ should "report a changed child" do
69
+ doc = Person.new
70
+ doc.pets.build :name=>"Magma"
71
+ doc.save!
72
+ doc.pets[0].name = "Ugly"
73
+ doc.send(:prepare_update_command).to_h.should == {
74
+ "$set"=>{"pets.0.name"=>"Ugly"}
75
+ }
76
+ end
77
+
78
+ should "report a deleted child" do
79
+ doc = Person.new
80
+ deleted = doc.pets.build :name=>"Magma"
81
+ doc.save!
82
+ doc.pets.pop
83
+ doc.send(:prepare_update_command).to_h.should == {
84
+ :pulls=>{"pets"=>[deleted._id]}
85
+ }
86
+ end
87
+
88
+ should "report a mix of added, modified and deleted" do
89
+ doc = Person.new
90
+ doc.pets.build :name => "Magma"
91
+ deleted = doc.pets.build :name => "Ugly"
92
+ doc.pets.build :name => "Bipolar"
93
+ doc.save!
94
+ doc.pets.reject! {|p|p.name=="Ugly"}
95
+ doc.pets[0].name = "Debris"
96
+ doc.pets.build :name => "Hades"
97
+ doc.send(:prepare_update_command).to_h.should == {
98
+ "$set"=>{"pets.0.name"=>"Debris"},
99
+ :pulls=>{"pets"=>[deleted._id]},
100
+ :pushes=>{"pets"=>[doc.pets[-1].to_mongo]}
101
+ }
102
+ end
103
+
104
+ should "update database selectors after save" do
105
+ doc = Person.new
106
+ doc.pets.build :name=>"Magma"
107
+ doc.pets.build :name=>"Ugly"
108
+ doc.pets.build :name=>"Bipolar"
109
+ doc.save!
110
+ doc.pets.reject! {|p|p.name=="Ugly"}
111
+ doc.save!
112
+ doc.pets.length.should == 2
113
+ doc.pets.each_with_index do |pet,index|
114
+ pet.instance_variable_get("@_database_position").should == index
115
+ end
116
+ end
117
+ end
118
+
119
+ context "with changes to nested one associations" do
120
+ should "report a new grandchild" do
121
+ doc = Person.new
122
+ pet = doc.pets.build :name=>"Magma"
123
+ doc.save!
124
+ pet.favorite_flea.build :name=>"Fleatus"
125
+ doc.send(:prepare_update_command).to_h.should == {
126
+ "$set"=>{"pets.0.favorite_flea"=>pet.favorite_flea.to_mongo}
127
+ }
128
+ end
129
+
130
+ should "report a modified grandchild" do
131
+ doc = Person.new
132
+ pet = doc.pets.build :name=>"Magma"
133
+ flea = pet.favorite_flea.build :name=>"Fleatus"
134
+ doc.save!
135
+ flea.name = "Fleatasia"
136
+ doc.send(:prepare_update_command).to_h.should == {
137
+ "$set"=>{"pets.0.favorite_flea.name"=>"Fleatasia"}
138
+ }
139
+ end
140
+
141
+ should "report a deleted grandchild" do
142
+ doc = Person.new
143
+ pet = doc.pets.build :name=>"Magma"
144
+ pet.favorite_flea.build :name=>"Fleatus"
145
+ doc.save!
146
+ pet.favorite_flea = nil
147
+ doc.send(:prepare_update_command).to_h.should == {
148
+ "$set"=>{"pets.0.favorite_flea"=>nil}
149
+ }
150
+ end
151
+ end
152
+
153
+ context "with changes to nested many associations" do
154
+ should "report a new grandchild" do
155
+ doc = Person.create
156
+ pet = doc.pets.build :name=>"Magma"
157
+ doc.save!
158
+ pet.fleas.build :name=>'Fleatus'
159
+ doc.send(:prepare_update_command).to_h.should == {
160
+ :pushes=>{"pets.0.fleas"=>[pet.fleas[0].to_mongo]}
161
+ }
162
+ end
163
+ should "report a changed grandchild" do
164
+ doc = Person.new
165
+ pet = doc.pets.build :name=>"Magma"
166
+ pet.fleas.build :name=>"Fleatus"
167
+ doc.save!
168
+ pet.fleas[0].name = "Fleatasia"
169
+ doc.send(:prepare_update_command).to_h.should == {
170
+ "$set"=>{"pets.0.fleas.0.name"=>"Fleatasia"}
171
+ }
172
+ end
173
+
174
+ should "report a deleted child" do
175
+ doc = Person.new
176
+ doc.pets.build :name=>"Magma"
177
+ doc.pets[0].fleas.build :name=>"Fleatus"
178
+ doc.save!
179
+ deleted = doc.pets[0].fleas.pop
180
+ doc.send(:prepare_update_command).to_h.should == {
181
+ :pulls=>{"pets.0.fleas"=>[deleted._id]}
182
+ }
183
+ end
184
+
185
+ should "report a mix of added, modified and deleted" do
186
+ doc = Person.new
187
+ pet = doc.pets.build :name=>"Magma"
188
+ pet.fleas.build :name => "Fleatus"
189
+ deleted = pet.fleas.build :name => "Fleatasia"
190
+ pet.fleas.build :name => "Soto"
191
+ doc.save!
192
+ pet.fleas.reject! {|f|f.name=="Fleatasia"}
193
+ pet.fleas[0].name = "Rinsai"
194
+ pet.fleas.build :name => "Dogen"
195
+ doc.send(:prepare_update_command).to_h.should == {
196
+ :pulls=>{"pets.0.fleas"=>[deleted._id]},
197
+ :pushes=>{"pets.0.fleas"=>[doc.pets[0].fleas[-1].to_mongo]},
198
+ "$set"=>{"pets.0.fleas.0.name"=>"Rinsai"}
199
+ }
200
+ end
201
+
202
+ should "update database selectors after save" do
203
+ doc = Person.new
204
+ pet = doc.pets.build :name=>"Magma"
205
+ pet.fleas.build :name=>"Fleatus"
206
+ pet.fleas.build :name=>"Soto"
207
+ pet.fleas.build :name=>"Rinsai"
208
+ doc.save!
209
+ pet.fleas.reject! {|p|p.name=="Soto"}
210
+ doc.save!
211
+ pet.fleas.length.should == 2
212
+ pet.fleas.each_with_index do |flea,index|
213
+ flea.instance_variable_get("@_database_position").should == index
214
+ end
215
+ end
216
+
217
+ end
218
+
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,133 @@
1
+ require "test_helper"
2
+
3
+ class TestUpdateCommand < Test::Unit::TestCase
4
+
5
+ context "UpdateCommand" do
6
+ setup do
7
+ @edoc = EDoc { key :name, String }
8
+ @doc = Doc { key :name, String }
9
+ @doc.many :children, :class=>@edoc
10
+
11
+ @command = MmPartialUpdate::UpdateCommand.new(@doc.new)
12
+ @defaults = {"a_field"=>2, "another_field"=>2}
13
+ @command.set nil, @defaults
14
+ end
15
+
16
+ context "#document_selector" do
17
+ should "be derived from the id of the root document" do
18
+ doc = @doc.new
19
+ edoc = doc.children.build
20
+ MmPartialUpdate::UpdateCommand.new(doc).document_selector.should == {:_id=>doc._id}
21
+ MmPartialUpdate::UpdateCommand.new(edoc).document_selector.should == {:_id=>doc._id}
22
+ end
23
+ end
24
+
25
+ context "#set" do
26
+
27
+ context "with a blank selector and replace = true" do
28
+ should "set the fields hash directly onto $set" do
29
+ @command.set(nil, {"a_different_field"=>3}, :replace=>{})
30
+ @command.to_h["$set"].should == {"a_different_field"=>3}
31
+ end
32
+ end
33
+
34
+ context "with a blank selector" do
35
+ should "merge the fields hash directly into $set" do
36
+ @command.set nil, {"a_different_field"=>3}
37
+ @command.to_h["$set"].should == {"a_different_field"=>3}.merge(@defaults)
38
+ end
39
+ end
40
+
41
+ context "with replace = true" do
42
+ should "replace the key at 'selector' with the fields hash" do
43
+ @command.set "sel", {"this"=>:that}
44
+ @command.set "sel", {"that"=>:this}, :replace=>true
45
+ @command.to_h["$set"]["sel"].should == {"that"=>:this}
46
+ end
47
+ end
48
+
49
+ context "with a selector and replace = false" do
50
+ should "merge each key of fields into $set, namespaced with the selector" do
51
+ @command.set "sel", {"that"=>:this, "the"=>:other}
52
+ @command.to_h["$set"].should == {"sel.that"=>:this, "sel.the"=>:other}.merge(@defaults)
53
+ @command.set "sel", {"that"=>:not_this, "b"=>:c}
54
+ @command.to_h["$set"].should == {"sel.that" => :not_this, "sel.the"=>:other,"sel.b"=>:c}.merge(@defaults)
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ context "#unset" do
61
+ should "add the selector to the $unset hash" do
62
+ @command.unset "a_field"
63
+ @command.to_h["$unset"].should == {"a_field"=>true}
64
+ end
65
+
66
+ should "nullify a field when :nullify=>true" do
67
+ @command.unset "a_field", :nullify=>true
68
+ @command.to_h["$set"].should == @defaults.merge("a_field"=>nil)
69
+ end
70
+
71
+ end
72
+
73
+ context "#push" do
74
+ should "add the field hash to the $pushAll array for the selector" do
75
+ @command.push "a_collection", {"a"=>:b, "c"=>:d}
76
+ @command.to_h[:pushes].should == {"a_collection" => [{"a"=>:b, "c"=>:d}]}
77
+ end
78
+
79
+ should "append the field hash to the$pushAll array for the selector" do
80
+ @command.push "a_collection", {"a"=>:b, "c"=>:d}
81
+ @command.push "a_collection", {"a"=>:e, "f"=>:g}
82
+ @command.to_h[:pushes].should == {"a_collection" => [{"a"=>:b, "c"=>:d},
83
+ {"a"=>:e, "f"=>:g}]}
84
+ end
85
+ end
86
+
87
+ context "#pull" do
88
+ should "add the document id to the $pull array for the selector" do
89
+ @command.pull "a_collection", "123"
90
+ @command.to_h[:pulls].should == {"a_collection" => ["123"]}
91
+ end
92
+
93
+ should "append the doc id to the $pull array for the selector" do
94
+ @command.pull "a_collection", "123"
95
+ @command.pull "a_collection", "456"
96
+ @command.to_h[:pulls].should == {"a_collection" => ["123","456"]}
97
+ end
98
+ end
99
+
100
+ context "#merge" do
101
+ should "merge the commands hash with an incoming UpdateCommand" do
102
+ another = MmPartialUpdate::UpdateCommand.new(@doc.new)
103
+ another.set "this", {"that"=>"the other"}
104
+ another.unset "me"
105
+ another.push "a", {"a"=>:b}
106
+ another.pull "b", "c"
107
+ @command.merge(another).should == @command.to_h.merge(another.to_h)
108
+ end
109
+ end
110
+
111
+ context "#prepare_mongodb_commands" do
112
+ should "separate operations into sets, pulls and pushes" do
113
+ @command.set "a", {:b=>2}
114
+ @command.set "a.c", {"c"=>"d"}
115
+ @command.push "hi", {:ho=>true}
116
+ @command.push "hi", {:snack=>"yummy"}
117
+ @command.push "ho", {:cheeri=>0}
118
+ @command.pull "zig", "zag"
119
+ db_commands = @command.send(:prepare_mongodb_commands)
120
+ db_commands.length.should == 4
121
+
122
+ db_commands[0].should == {"$set"=>{"a.b"=>2, "a.c.c"=>"d"}.merge(@defaults)}
123
+ db_commands[1].should == {"$pull"=>{"zig"=>{"_id"=>{"$in"=>["zag"]}}}}
124
+ db_commands[2].should == {"$pushAll"=>{"hi"=>[{:ho=>true},{:snack=>"yummy"}]}}
125
+ db_commands[3].should == {"$pushAll"=>{"ho"=>[{:cheeri=>0}]}}
126
+ end
127
+
128
+ end
129
+
130
+
131
+ end
132
+ end
133
+