mm_dirtier 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/LICENSE +20 -0
- data/README.rdoc +117 -0
- data/lib/mm_dirtier/extensions.rb +43 -0
- data/lib/mm_dirtier/in_array_proxy_listener.rb +20 -0
- data/lib/mm_dirtier/many_embedded_proxy_listener.rb +13 -0
- data/lib/mm_dirtier/one_embedded_proxy_listener.rb +30 -0
- data/lib/mm_dirtier/one_proxy_listener.rb +25 -0
- data/lib/mm_dirtier/plugins/dirtier.rb +83 -0
- data/lib/mm_dirtier/version.rb +6 -0
- data/lib/mm_dirtier.rb +17 -0
- data/test/functional/test_in_array_proxy.rb +83 -0
- data/test/functional/test_many_embedded_polymorphic_proxy.rb +86 -0
- data/test/functional/test_many_embedded_proxy.rb +81 -0
- data/test/functional/test_non_embedded_proxies.rb +21 -0
- data/test/functional/test_observable_keys.rb +58 -0
- data/test/functional/test_one_embedded_proxy.rb +68 -0
- data/test/test_helper.rb +95 -0
- data/test/test_mm_dirtier.rb +9 -0
- metadata +159 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Nathan Stults
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
= Observables
|
2
|
+
|
3
|
+
Observables implements observable arrays and hashes by way of the ActiveModel::Notifier, the same mechanism
|
4
|
+
underlying the instrumentation API in Rails3. Observables collections broadcast detailed change information for
|
5
|
+
any state-modifying operations, including specific elements added or removed, when that information is practically
|
6
|
+
obtainable.
|
7
|
+
|
8
|
+
== Installation
|
9
|
+
|
10
|
+
Observables is available as a RubyGem:
|
11
|
+
|
12
|
+
gem install mm_dirtier
|
13
|
+
|
14
|
+
== Observing the Observables
|
15
|
+
|
16
|
+
=== Example:
|
17
|
+
|
18
|
+
#Getting an observable array
|
19
|
+
|
20
|
+
ary = [1,2,3] #or, ary = Observables::Array.new([1,2,3]), or, hsh = Observables::Hash
|
21
|
+
ary.observable? # => false
|
22
|
+
ary.can_be_observable? # => true
|
23
|
+
ary.make_observable => #<Class:#<Array:0x56a84a8>>
|
24
|
+
|
25
|
+
#Setting up a subscription
|
26
|
+
|
27
|
+
subscription = ary.subscribe do |change_type,args|
|
28
|
+
puts "Change type: #{change_type}"
|
29
|
+
puts "Trigger: #{args.trigger}"
|
30
|
+
puts "Changes: #{args.changes.inspect}"
|
31
|
+
puts "---"
|
32
|
+
end
|
33
|
+
|
34
|
+
#Do stuff
|
35
|
+
|
36
|
+
ary << 3
|
37
|
+
# Change type: before_added
|
38
|
+
# Trigger: <<
|
39
|
+
# Changes: {:added=>[3]}
|
40
|
+
# ---
|
41
|
+
# Change type: after_added
|
42
|
+
# Trigger: <<
|
43
|
+
# Changes: {:added=>[3]}
|
44
|
+
# => [1,2,3,3]
|
45
|
+
|
46
|
+
#Clean up
|
47
|
+
|
48
|
+
ary.unsubscribe(subscription)
|
49
|
+
ary << 4 # => [1,2,3,3,4]
|
50
|
+
|
51
|
+
#Only listen to after_xxx
|
52
|
+
|
53
|
+
subscription = ary.subscribe(/after/) do |change_type,_|
|
54
|
+
puts "Change type:#{change_type}"
|
55
|
+
end
|
56
|
+
|
57
|
+
ary.concat([9,10,11])
|
58
|
+
|
59
|
+
# Change type: after_added, changes: {:added=>[9,10,11]}
|
60
|
+
# => [1,2,3,3,4,9,10,11]
|
61
|
+
|
62
|
+
ary.replace([3,2,1])
|
63
|
+
|
64
|
+
# Change type: after_modified, changed: {:added=>[3,2,1], :removed=>[1,2,3,3,4,9,10,11]}
|
65
|
+
# => [3,2,1]
|
66
|
+
|
67
|
+
#Hashes work too
|
68
|
+
|
69
|
+
hsh = {:a=>:b}
|
70
|
+
hsh.can_be_observable? # => true
|
71
|
+
hsh.make_observable
|
72
|
+
hsh.subscribe { |type,args| ... }
|
73
|
+
|
74
|
+
== Special case: ownership
|
75
|
+
|
76
|
+
Observables was created to assist in the implementation of proper dirty tracking for in-place modifications
|
77
|
+
to embedded collections in ORM's, particularly for documented oriented databases, where
|
78
|
+
this is a common situation. In this scenario and similar scenarios, observable collections
|
79
|
+
will only be subscribed to by the object that owns them. However, the parent object
|
80
|
+
may own any number of child collections. To avoid having to manage myriad subscription
|
81
|
+
objects, each observable collection can have a single 'observer' - and will manage the
|
82
|
+
subscription to that observer like so:
|
83
|
+
|
84
|
+
class Owner
|
85
|
+
def my_array
|
86
|
+
@my_array
|
87
|
+
end
|
88
|
+
|
89
|
+
def my_array=(new_array)
|
90
|
+
@my_array.clear_observer if @my_array
|
91
|
+
@my_array = new_array.tap {|a|a.make_observable}
|
92
|
+
@my_array.set_observer(self, :pattern=>/before/, :callback_method=>:my_array_before_change)
|
93
|
+
#Acceptable alernatives are:
|
94
|
+
# @my_array.set_observer { |type,args| ... }
|
95
|
+
# @my_array.set_observer(self, :pattern=>/before/) { |sender,type,args| ... }
|
96
|
+
end
|
97
|
+
|
98
|
+
def my_array_before_change(sender,type,args)
|
99
|
+
#sender = @my_array
|
100
|
+
#do something interesting, like, say, attribute_will_change!(:my_array)
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
== Note on Patches/Pull Requests
|
106
|
+
|
107
|
+
* Fork the project.
|
108
|
+
* Make your feature addition or bug fix.
|
109
|
+
* Add tests for it. This is important so I don't break it in a
|
110
|
+
future version unintentionally.
|
111
|
+
* Commit, do not mess with rakefile, version, or history.
|
112
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
113
|
+
* Send me a pull request. Bonus points for topic branches.
|
114
|
+
|
115
|
+
== Copyright
|
116
|
+
|
117
|
+
Copyright (c) 2010 Nathan Stults. See LICENSE for details.
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module MongoMapper
|
2
|
+
module Plugins
|
3
|
+
module Associations
|
4
|
+
#By default, proxies should not be observable
|
5
|
+
class Proxy
|
6
|
+
def can_be_observable?
|
7
|
+
false
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class ManyEmbeddedProxy
|
12
|
+
def can_be_observable?; true; end
|
13
|
+
|
14
|
+
def make_observable
|
15
|
+
class << self; include MmDirtier::ManyEmbeddedProxyListener; end unless observable?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class ManyEmbeddedPolymorphicProxy
|
20
|
+
def can_be_observable?; true; end
|
21
|
+
|
22
|
+
def make_observable
|
23
|
+
class << self; include MmDirtier::ManyEmbeddedProxyListener; end unless observable?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class InArrayProxy
|
28
|
+
def can_be_observable?; true; end
|
29
|
+
def make_observable
|
30
|
+
class << self; include MmDirtier::InArrayProxyListener;end unless observable?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class OneEmbeddedProxy
|
35
|
+
def can_be_observable?; true; end
|
36
|
+
def make_observable
|
37
|
+
class << self; include MmDirtier::OneEmbeddedProxyListener;end unless observable?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module MmDirtier
|
2
|
+
module InArrayProxyListener
|
3
|
+
include Observables::Base
|
4
|
+
|
5
|
+
def ids
|
6
|
+
super.tap do |ids|
|
7
|
+
|
8
|
+
unless ids.observable?
|
9
|
+
ids.make_observable
|
10
|
+
ids.set_observer do |sender,type,args|
|
11
|
+
notifier.publish type, args
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module MmDirtier
|
2
|
+
module ManyEmbeddedProxyListener
|
3
|
+
include Observables::ArrayWatcher
|
4
|
+
|
5
|
+
#It appears that #make_observable operates on the underlying
|
6
|
+
#target, not on the proxy. Therefore, 'replace', which is proxied,
|
7
|
+
#does not get properly overriden. this is intended to fix that.
|
8
|
+
def replace(*args)
|
9
|
+
changes = changes_for(:modified,:replace,*args)
|
10
|
+
changing(:modified,:trigger=>:replace, :changes=>changes) {super}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module MmDirtier
|
2
|
+
module OneEmbeddedProxyListener
|
3
|
+
include Observables::Base
|
4
|
+
|
5
|
+
def replace(val)
|
6
|
+
changing(:modifier) {super}
|
7
|
+
end
|
8
|
+
|
9
|
+
#This is a dirty hack.
|
10
|
+
#duplicable? has to return true,
|
11
|
+
#or the ActiveModel::Dirty will store
|
12
|
+
# the proxy in its change set,
|
13
|
+
# and then when you check for changes
|
14
|
+
# it will always reflect the latest value
|
15
|
+
# of its target, which isn't what is desired.
|
16
|
+
# However, duplicating an embedded document
|
17
|
+
# gives it a new object ID, which ruins the
|
18
|
+
# identity of the child and isn't what
|
19
|
+
# we want either. Thus this nasty, nasty
|
20
|
+
# business. There must be a better way,
|
21
|
+
# but at the moment it escapes me.
|
22
|
+
def duplicable?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def clone
|
27
|
+
target
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module MmDirtier
|
2
|
+
module OneProxyListener
|
3
|
+
include Observables::Base
|
4
|
+
|
5
|
+
def replace(val)
|
6
|
+
change_type = target.nil? ? :modified : :added
|
7
|
+
changes = changes_for(change_type,:replace,val)
|
8
|
+
changing(change_type,:trigger=>:replace, :changes=>changes) {super}
|
9
|
+
end
|
10
|
+
|
11
|
+
def changes_for(change_type, trigger_method, *args, &block)
|
12
|
+
prev = target.nil? ? nil : target.dup
|
13
|
+
if change_type == :added
|
14
|
+
lambda {{:added=>args}}
|
15
|
+
else
|
16
|
+
lambda{{:removed=>[prev], :added=>args}}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def dup
|
21
|
+
target.dup
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module MmDirtier
|
3
|
+
module Plugins
|
4
|
+
module Dirtier
|
5
|
+
|
6
|
+
def self.included(model)
|
7
|
+
model.plugin MmDirtier::Plugins::Dirtier
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.configure(model)
|
11
|
+
model.plugin MongoMapper::Plugins::Dirty unless
|
12
|
+
model.plugins.include?(MongoMapper::Plugins::Dirty)
|
13
|
+
end
|
14
|
+
|
15
|
+
module InstanceMethods
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def attribute_method?(attr)
|
20
|
+
#Since associations are storing their changes in the models
|
21
|
+
# normal dirty tracking system, then association names are
|
22
|
+
# valid attributes as far as dirty is concerned
|
23
|
+
super || !!associations.keys.detect { |a| a.to_s == attr.to_s }
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def get_proxy(association)
|
29
|
+
#I can't imagine why, but super.tap{...} is causing errors here.
|
30
|
+
#Proxy meta monkey wierdness, no doubt.
|
31
|
+
proxy = super
|
32
|
+
key_name = proxy_key_name(proxy)
|
33
|
+
unless association.observable?
|
34
|
+
observe_if_observable(key_name,proxy)
|
35
|
+
end
|
36
|
+
return proxy
|
37
|
+
end
|
38
|
+
|
39
|
+
def proxy_key_name(proxy)
|
40
|
+
proxy.options[:in] ? proxy.options[:in] : proxy.association.name
|
41
|
+
end
|
42
|
+
|
43
|
+
def write_key(key, value)
|
44
|
+
old_value = read_key(key)
|
45
|
+
old_value.clear_observer if old_value && old_value.observable?
|
46
|
+
observe_if_observable(key, value) if value
|
47
|
+
super(key,value)
|
48
|
+
end
|
49
|
+
|
50
|
+
def value_changed?(key_name, old, value)
|
51
|
+
value = nil if keys[key_name] && keys[key_name].number? && value.blank?
|
52
|
+
old != value
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def observe_if_observable(key, value)
|
57
|
+
key = key.to_s
|
58
|
+
|
59
|
+
if value.observable? || value.can_be_observable?
|
60
|
+
|
61
|
+
value.make_observable unless value.observable?
|
62
|
+
previous_values = nil
|
63
|
+
value.set_observer do |_,change_type,args|
|
64
|
+
if change_type.to_s =~ /before/
|
65
|
+
previous_values = attribute_was(key)
|
66
|
+
#previous_values = previous_values.nil? ? nil : previous_values.dup
|
67
|
+
previous_values = dup_if_required(previous_values)
|
68
|
+
attribute_will_change!(key) unless attribute_changed?(key)
|
69
|
+
else
|
70
|
+
changed_attributes.delete(key) unless value_changed?(key,previous_values,args.current_values)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def dup_if_required(val)
|
77
|
+
val.nil? || val.respond_to?(:_id) ? val : val.dup
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/mm_dirtier.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require "mongo_mapper"
|
3
|
+
require "observables"
|
4
|
+
|
5
|
+
base_dir = File.dirname(__FILE__)
|
6
|
+
[
|
7
|
+
'version',
|
8
|
+
'many_embedded_proxy_listener',
|
9
|
+
'in_array_proxy_listener',
|
10
|
+
'one_embedded_proxy_listener',
|
11
|
+
'extensions',
|
12
|
+
'plugins/dirtier'
|
13
|
+
].each {|req| require File.join(base_dir,'mm_dirtier',req)}
|
14
|
+
|
15
|
+
|
16
|
+
MongoMapper::Document.append_inclusions(MmDirtier::Plugins::Dirtier)
|
17
|
+
MongoMapper::EmbeddedDocument.append_inclusions(MmDirtier::Plugins::Dirtier)
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class InArrayProxyTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@document = Doc { key :ary, Array }
|
6
|
+
end
|
7
|
+
|
8
|
+
context "marking changes on many in array proxies" do
|
9
|
+
setup do
|
10
|
+
@child = Doc { key :name, String }
|
11
|
+
@document.key :child_ids, Array
|
12
|
+
@document.many :children, :class=>@child, :in=>:child_ids
|
13
|
+
end
|
14
|
+
|
15
|
+
should "not happen if there are none" do
|
16
|
+
doc = @document.new
|
17
|
+
doc.child_ids_changed?.should be_false
|
18
|
+
doc.child_ids_change.should be_nil
|
19
|
+
end
|
20
|
+
|
21
|
+
should "happen when change happens" do
|
22
|
+
doc = @document.new
|
23
|
+
child = @child.create
|
24
|
+
doc.children << child
|
25
|
+
doc.child_ids_changed?.should be_true
|
26
|
+
doc.child_ids_was.should == []
|
27
|
+
doc.child_ids_change.should == [[], [child.id]]
|
28
|
+
end
|
29
|
+
|
30
|
+
should "track changes for the id array, not the association itself" do
|
31
|
+
doc = @document.new
|
32
|
+
child = @child.create
|
33
|
+
doc.children << child
|
34
|
+
doc.children_changed?.should be_false
|
35
|
+
doc.child_ids_changed?.should be_true
|
36
|
+
end
|
37
|
+
|
38
|
+
should "happen when modified in place" do
|
39
|
+
doc = @document.new
|
40
|
+
child = @child.create
|
41
|
+
doc.children << child
|
42
|
+
doc.save!
|
43
|
+
new_child = @child.new
|
44
|
+
doc.children << new_child
|
45
|
+
doc.child_ids_changed?.should be_true
|
46
|
+
doc.child_ids_was.should == [child.id]
|
47
|
+
doc.child_ids_change.should == [[child.id],[child.id,new_child.id]]
|
48
|
+
end
|
49
|
+
|
50
|
+
should "not be changed when loaded from the database" do
|
51
|
+
doc = @document.new
|
52
|
+
doc.children << @child.create
|
53
|
+
doc.changed?.should be_true
|
54
|
+
doc.save!
|
55
|
+
doc = @document.find(doc.id)
|
56
|
+
doc.changed?.should be_false
|
57
|
+
doc.child_ids_changed?.should be_false
|
58
|
+
end
|
59
|
+
|
60
|
+
should "happen when replaced" do
|
61
|
+
doc = @document.new
|
62
|
+
child = @child.create
|
63
|
+
doc.children << child
|
64
|
+
doc.save!
|
65
|
+
doc.child_ids_changed?.should be_false
|
66
|
+
new_child = @child.create
|
67
|
+
doc.children = [new_child]
|
68
|
+
doc.child_ids_changed?.should be_true
|
69
|
+
doc.child_ids_was.should == [child.id]
|
70
|
+
doc.child_ids_change.should == [[child.id],[new_child.id]]
|
71
|
+
end
|
72
|
+
|
73
|
+
should "detect when a collection is set to []" do
|
74
|
+
doc = @document.new
|
75
|
+
child = @child.create
|
76
|
+
doc.children << child
|
77
|
+
doc.save!
|
78
|
+
doc.children = []
|
79
|
+
doc.child_ids_changed?.should be_true
|
80
|
+
doc.child_ids_change.should == [[child.id],[]]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class Child
|
4
|
+
include MongoMapper::EmbeddedDocument
|
5
|
+
end
|
6
|
+
|
7
|
+
class ManyEmbeddedPolymorphicProxyTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
def setup
|
10
|
+
@document = Doc { key :name, String }
|
11
|
+
end
|
12
|
+
|
13
|
+
context "marking changes on many embedded proxies" do
|
14
|
+
setup do
|
15
|
+
@document.many :children, :polymorphic=>true, :class_name=>'Child'
|
16
|
+
end
|
17
|
+
|
18
|
+
should "not happen if there are none" do
|
19
|
+
doc = @document.new
|
20
|
+
doc.children_changed?.should be_false
|
21
|
+
doc.children_change.should be_nil
|
22
|
+
end
|
23
|
+
|
24
|
+
should "happen when change happens" do
|
25
|
+
doc = @document.new
|
26
|
+
child = doc.children.build
|
27
|
+
doc.children_changed?.should be_true
|
28
|
+
doc.children_was.should == []
|
29
|
+
doc.children_change.should == [[], [child]]
|
30
|
+
end
|
31
|
+
|
32
|
+
should "happen when modified in place" do
|
33
|
+
doc = @document.new
|
34
|
+
child = doc.children.build
|
35
|
+
doc.save!
|
36
|
+
new_child = Child.new
|
37
|
+
doc.children << new_child
|
38
|
+
doc.children_changed?.should be_true
|
39
|
+
doc.children_was.should == [child]
|
40
|
+
doc.children_change.should == [[child],[child,new_child]]
|
41
|
+
end
|
42
|
+
|
43
|
+
should "not be changed when loaded from the database" do
|
44
|
+
doc = @document.new
|
45
|
+
child = doc.children.build
|
46
|
+
doc.changed?.should be_true
|
47
|
+
doc.save!
|
48
|
+
doc = @document.find(doc.id)
|
49
|
+
doc.changed?.should be_false
|
50
|
+
doc.children_changed?.should be_false
|
51
|
+
end
|
52
|
+
|
53
|
+
should "happen when replaced" do
|
54
|
+
doc = @document.new
|
55
|
+
child = doc.children.build
|
56
|
+
doc.save!
|
57
|
+
doc.children_changed?.should be_false
|
58
|
+
new_child = Child.new
|
59
|
+
|
60
|
+
doc.children = [new_child]
|
61
|
+
doc.children_changed?.should be_true
|
62
|
+
doc.children_was.should == [child]
|
63
|
+
doc.children_change.should == [[child],[new_child]]
|
64
|
+
end
|
65
|
+
|
66
|
+
should "clear when modified in place back to the original state" do
|
67
|
+
doc = @document.new
|
68
|
+
child = doc.children.build
|
69
|
+
doc.save!
|
70
|
+
doc.children.build
|
71
|
+
doc.children_changed?.should be_true
|
72
|
+
doc.children.pop
|
73
|
+
doc.children_changed?.should be_false
|
74
|
+
end
|
75
|
+
|
76
|
+
should "detect when a collection is set to []" do
|
77
|
+
doc = @document.new
|
78
|
+
child = doc.children.build
|
79
|
+
doc.save!
|
80
|
+
doc.children = []
|
81
|
+
doc.children_changed?.should be_true
|
82
|
+
doc.children_change.should == [[child],[]]
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ManyEmbeddedProxyTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@document = Doc { key :ary, Array }
|
6
|
+
end
|
7
|
+
|
8
|
+
context "marking changes on many embedded proxies" do
|
9
|
+
setup do
|
10
|
+
@child = EDoc { key :name, String }
|
11
|
+
@document.many :children, :class=>@child
|
12
|
+
end
|
13
|
+
|
14
|
+
should "not happen if there are none" do
|
15
|
+
doc = @document.new
|
16
|
+
doc.children_changed?.should be_false
|
17
|
+
doc.children_change.should be_nil
|
18
|
+
end
|
19
|
+
|
20
|
+
should "happen when change happens" do
|
21
|
+
doc = @document.new
|
22
|
+
child = doc.children.build
|
23
|
+
doc.children_changed?.should be_true
|
24
|
+
doc.children_was.should == []
|
25
|
+
doc.children_change.should == [[], [child]]
|
26
|
+
end
|
27
|
+
|
28
|
+
should "happen when modified in place" do
|
29
|
+
doc = @document.new
|
30
|
+
child = doc.children.build
|
31
|
+
doc.save!
|
32
|
+
new_child = @child.new
|
33
|
+
doc.children << new_child
|
34
|
+
doc.children_changed?.should be_true
|
35
|
+
doc.children_was.should == [child]
|
36
|
+
doc.children_change.should == [[child],[child,new_child]]
|
37
|
+
end
|
38
|
+
|
39
|
+
should "not be changed when loaded from the database" do
|
40
|
+
doc = @document.new
|
41
|
+
child = doc.children.build
|
42
|
+
doc.changed?.should be_true
|
43
|
+
doc.save!
|
44
|
+
doc = @document.find(doc.id)
|
45
|
+
doc.changed?.should be_false
|
46
|
+
doc.children_changed?.should be_false
|
47
|
+
end
|
48
|
+
|
49
|
+
should "happen when replaced" do
|
50
|
+
doc = @document.new
|
51
|
+
child = doc.children.build
|
52
|
+
doc.save!
|
53
|
+
doc.children_changed?.should be_false
|
54
|
+
new_child = @child.new
|
55
|
+
doc.children = [new_child]
|
56
|
+
doc.children_changed?.should be_true
|
57
|
+
doc.children_was.should == [child]
|
58
|
+
doc.children_change.should == [[child],[new_child]]
|
59
|
+
end
|
60
|
+
|
61
|
+
should "clear when modified in place back to the original state" do
|
62
|
+
doc = @document.new
|
63
|
+
child = doc.children.build
|
64
|
+
doc.save!
|
65
|
+
doc.children.build
|
66
|
+
doc.children_changed?.should be_true
|
67
|
+
doc.children.pop
|
68
|
+
doc.children_changed?.should be_false
|
69
|
+
end
|
70
|
+
|
71
|
+
should "detect when a collection is set to []" do
|
72
|
+
doc = @document.new
|
73
|
+
child = doc.children.build
|
74
|
+
doc.save!
|
75
|
+
doc.children = []
|
76
|
+
doc.children_changed?.should be_true
|
77
|
+
doc.children_change.should == [[child],[]]
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class NonEmbeddedProxyTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@document = Doc { key :name, String }
|
6
|
+
@refdoc = Doc { key :name, String }
|
7
|
+
end
|
8
|
+
|
9
|
+
context "changes on many documents proxy" do
|
10
|
+
setup do
|
11
|
+
@document.many :refs, :class=>@refdoc
|
12
|
+
end
|
13
|
+
|
14
|
+
should "not happen" do
|
15
|
+
doc = @document.new
|
16
|
+
doc.refs.build
|
17
|
+
doc.changed?.should be_false
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
class ObservableKeysTest < Test::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
@document = Doc { key :ary, Array }
|
7
|
+
end
|
8
|
+
|
9
|
+
context "marking changes on observable keys" do
|
10
|
+
should "not happen if there are none" do
|
11
|
+
doc = @document.new
|
12
|
+
doc.ary_changed?.should be_false
|
13
|
+
doc.ary_change.should be_nil
|
14
|
+
end
|
15
|
+
|
16
|
+
should "happen when change happens" do
|
17
|
+
doc = @document.new
|
18
|
+
doc.ary = %w(Golly Gee Willikers Batman)
|
19
|
+
doc.ary_changed?.should be_true
|
20
|
+
doc.ary_was.should == []
|
21
|
+
doc.ary_change.should == [[], %w(Golly Gee Willikers Batman)]
|
22
|
+
end
|
23
|
+
|
24
|
+
should "happen when modified in place" do
|
25
|
+
doc = @document.new
|
26
|
+
doc.ary = %w(Golly Gee Willikers Batman)
|
27
|
+
doc.save!
|
28
|
+
doc.ary.push('POW!')
|
29
|
+
doc.ary_changed?.should be_true
|
30
|
+
doc.ary_was.should == %w(Golly Gee Willikers Batman)
|
31
|
+
doc.ary_change.should == [%w(Golly Gee Willikers Batman),%w(Golly Gee Willikers Batman POW!)]
|
32
|
+
end
|
33
|
+
|
34
|
+
should "clear when modified in place back to the original state" do
|
35
|
+
doc = @document.new
|
36
|
+
doc.ary = %w(Golly Gee Willikers Batman)
|
37
|
+
doc.save!
|
38
|
+
doc.ary.push('POW!')
|
39
|
+
doc.ary_changed?.should be_true
|
40
|
+
doc.ary.pop
|
41
|
+
doc.ary_changed?.should be_false
|
42
|
+
end
|
43
|
+
|
44
|
+
should "not flag changes when an array removed from a doc is changed" do
|
45
|
+
doc = @document.new
|
46
|
+
doc.ary = %w(hi there)
|
47
|
+
a1 = doc.ary
|
48
|
+
doc.ary = %w(huggy bear)
|
49
|
+
a2 = doc.ary
|
50
|
+
doc.save!
|
51
|
+
a1 << "huggy bear"
|
52
|
+
doc.ary_changed?.should be_false
|
53
|
+
a2.unshift("hi there")
|
54
|
+
doc.ary_changed?.should be_true
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class OneEmbeddedProxyTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@document = Doc { key :ary, Array }
|
6
|
+
end
|
7
|
+
|
8
|
+
context "marking changes on one embedded proxies" do
|
9
|
+
setup do
|
10
|
+
@child = EDoc { key :name, String }
|
11
|
+
@document.one :child, :class=>@child
|
12
|
+
end
|
13
|
+
|
14
|
+
should "not happen if there are none" do
|
15
|
+
doc = @document.new
|
16
|
+
doc.child_changed?.should be_false
|
17
|
+
doc.child_change.should be_nil
|
18
|
+
end
|
19
|
+
|
20
|
+
should "happen when change happens" do
|
21
|
+
doc = @document.new
|
22
|
+
child = @child.new
|
23
|
+
doc.child = child
|
24
|
+
doc.child_changed?.should be_true
|
25
|
+
doc.child_was.should == nil
|
26
|
+
doc.child_change.should == [nil, child]
|
27
|
+
end
|
28
|
+
|
29
|
+
should "not be changed when loaded from the database" do
|
30
|
+
doc = @document.new
|
31
|
+
doc.child = @child.new
|
32
|
+
doc.changed?.should be_true
|
33
|
+
doc.save!
|
34
|
+
doc = @document.find(doc.id)
|
35
|
+
doc.changed?.should be_false
|
36
|
+
doc.child_changed?.should be_false
|
37
|
+
end
|
38
|
+
|
39
|
+
should "detect when a collection is set to nil" do
|
40
|
+
doc = @document.new
|
41
|
+
c = doc.child.build
|
42
|
+
doc.save!
|
43
|
+
doc.child = nil
|
44
|
+
doc.child_changed?.should be_true
|
45
|
+
changes = doc.child_change
|
46
|
+
changes[0].should == c
|
47
|
+
changes[1].should be_nil
|
48
|
+
end
|
49
|
+
|
50
|
+
should "remove changes when set back to its original value" do
|
51
|
+
doc = @document.new
|
52
|
+
child = doc.child.build
|
53
|
+
doc.save!
|
54
|
+
doc.child = @child.new
|
55
|
+
doc.child_changed?.should be_true
|
56
|
+
doc.child = child
|
57
|
+
doc.child_changed?.should be_false
|
58
|
+
end
|
59
|
+
|
60
|
+
should "ignore in place changes to child attributes" do
|
61
|
+
doc = @document.new
|
62
|
+
child = doc.child.build
|
63
|
+
doc.save!
|
64
|
+
doc.child.name = "hi there"
|
65
|
+
doc.child_changed?.should be_false
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
$:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
5
|
+
require 'mm_dirtier'
|
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 do
|
33
|
+
include MongoMapper::EmbeddedDocument
|
34
|
+
|
35
|
+
if name
|
36
|
+
class_eval "def self.name; '#{name}' end"
|
37
|
+
class_eval "def self.to_s; '#{name}' end"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
klass.class_eval(&block) if block_given?
|
42
|
+
klass
|
43
|
+
end
|
44
|
+
|
45
|
+
def drop_indexes(klass)
|
46
|
+
if klass.database.collection_names.include?(klass.collection.name)
|
47
|
+
klass.collection.drop_indexes
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
custom_matcher :be_true do |receiver, matcher, args|
|
52
|
+
matcher.positive_failure_message = "Expected #{receiver} to be true but it wasn't"
|
53
|
+
matcher.negative_failure_message = "Expected #{receiver} not to be true but it was"
|
54
|
+
receiver.eql?(true)
|
55
|
+
end
|
56
|
+
|
57
|
+
custom_matcher :be_false do |receiver, matcher, args|
|
58
|
+
matcher.positive_failure_message = "Expected #{receiver} to be false but it wasn't"
|
59
|
+
matcher.negative_failure_message = "Expected #{receiver} not to be false but it was"
|
60
|
+
receiver.eql?(false)
|
61
|
+
end
|
62
|
+
|
63
|
+
custom_matcher :have_error_on do |receiver, matcher, args|
|
64
|
+
receiver.valid?
|
65
|
+
attribute = args[0]
|
66
|
+
expected_message = args[1]
|
67
|
+
|
68
|
+
if expected_message.nil?
|
69
|
+
matcher.positive_failure_message = "#{receiver} had no errors on #{attribute}"
|
70
|
+
matcher.negative_failure_message = "#{receiver} had errors on #{attribute} #{receiver.errors.inspect}"
|
71
|
+
!receiver.errors[attribute].blank?
|
72
|
+
else
|
73
|
+
actual = receiver.errors[attribute]
|
74
|
+
matcher.positive_failure_message = %Q(Expected error on #{attribute} to be "#{expected_message}" but was "#{actual}")
|
75
|
+
matcher.negative_failure_message = %Q(Expected error on #{attribute} not to be "#{expected_message}" but was "#{actual}")
|
76
|
+
actual.include? expected_message
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
custom_matcher :have_index do |receiver, matcher, args|
|
81
|
+
index_name = args[0]
|
82
|
+
matcher.positive_failure_message = "#{receiver} does not have index named #{index_name}, but should"
|
83
|
+
matcher.negative_failure_message = "#{receiver} does have index named #{index_name}, but should not"
|
84
|
+
!receiver.collection.index_information.detect { |index| index[0] == index_name }.nil?
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
log_dir = File.expand_path('../../log', __FILE__)
|
89
|
+
FileUtils.mkdir_p(log_dir) unless File.exist?(log_dir)
|
90
|
+
logger = Logger.new(log_dir + '/test.log')
|
91
|
+
|
92
|
+
LogBuddy.init(:logger => logger)
|
93
|
+
MongoMapper.connection = Mongo::Connection.new('127.0.0.1', 27017, :logger => logger)
|
94
|
+
MongoMapper.database = "mm-test-#{RUBY_VERSION.gsub('.', '-')}"
|
95
|
+
MongoMapper.database.collections.each { |c| c.drop_indexes }
|
metadata
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mm_dirtier
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Nathan Stults
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-10-16 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ~>
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
hash: 31
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
- 1
|
31
|
+
- 2
|
32
|
+
version: 0.1.2
|
33
|
+
type: :runtime
|
34
|
+
name: observables
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
type: :development
|
48
|
+
name: rake
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
type: :development
|
62
|
+
name: log_buddy
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: *id003
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ~>
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
hash: 15
|
72
|
+
segments:
|
73
|
+
- 0
|
74
|
+
- 4
|
75
|
+
- 0
|
76
|
+
version: 0.4.0
|
77
|
+
type: :development
|
78
|
+
name: jnunemaker-matchy
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: *id004
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ~>
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
hash: 21
|
88
|
+
segments:
|
89
|
+
- 2
|
90
|
+
- 11
|
91
|
+
version: "2.11"
|
92
|
+
type: :development
|
93
|
+
name: shoulda
|
94
|
+
prerelease: false
|
95
|
+
version_requirements: *id005
|
96
|
+
description:
|
97
|
+
email:
|
98
|
+
- hereiam@sonic.net
|
99
|
+
executables: []
|
100
|
+
|
101
|
+
extensions: []
|
102
|
+
|
103
|
+
extra_rdoc_files: []
|
104
|
+
|
105
|
+
files:
|
106
|
+
- lib/mm_dirtier/in_array_proxy_listener.rb
|
107
|
+
- lib/mm_dirtier/extensions.rb
|
108
|
+
- lib/mm_dirtier/one_embedded_proxy_listener.rb
|
109
|
+
- lib/mm_dirtier/plugins/dirtier.rb
|
110
|
+
- lib/mm_dirtier/version.rb
|
111
|
+
- lib/mm_dirtier/one_proxy_listener.rb
|
112
|
+
- lib/mm_dirtier/many_embedded_proxy_listener.rb
|
113
|
+
- lib/mm_dirtier.rb
|
114
|
+
- test/test_helper.rb
|
115
|
+
- test/test_mm_dirtier.rb
|
116
|
+
- test/functional/test_many_embedded_polymorphic_proxy.rb
|
117
|
+
- test/functional/test_one_embedded_proxy.rb
|
118
|
+
- test/functional/test_observable_keys.rb
|
119
|
+
- test/functional/test_many_embedded_proxy.rb
|
120
|
+
- test/functional/test_in_array_proxy.rb
|
121
|
+
- test/functional/test_non_embedded_proxies.rb
|
122
|
+
- LICENSE
|
123
|
+
- README.rdoc
|
124
|
+
has_rdoc: true
|
125
|
+
homepage: http://github.com/PlasticLizard/mm_dirtier
|
126
|
+
licenses: []
|
127
|
+
|
128
|
+
post_install_message:
|
129
|
+
rdoc_options: []
|
130
|
+
|
131
|
+
require_paths:
|
132
|
+
- lib
|
133
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
134
|
+
none: false
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
hash: 3
|
139
|
+
segments:
|
140
|
+
- 0
|
141
|
+
version: "0"
|
142
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
143
|
+
none: false
|
144
|
+
requirements:
|
145
|
+
- - ">="
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
hash: 3
|
148
|
+
segments:
|
149
|
+
- 0
|
150
|
+
version: "0"
|
151
|
+
requirements: []
|
152
|
+
|
153
|
+
rubyforge_project:
|
154
|
+
rubygems_version: 1.3.7
|
155
|
+
signing_key:
|
156
|
+
specification_version: 3
|
157
|
+
summary: Even dirtier dirty tracking for MongoMapper
|
158
|
+
test_files: []
|
159
|
+
|