mm_dirtier 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,6 @@
1
+ # encoding: UTF-8
2
+ module MongoMapper
3
+ module Dirtier
4
+ Version = '0.1.0'
5
+ end
6
+ 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
@@ -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 }
@@ -0,0 +1,9 @@
1
+ require 'test_helper'
2
+
3
+ class TestMongoMapperDirtier < Test::Unit::TestCase
4
+ context "including mm_dirtier" do
5
+ should "include something or other" do
6
+ assert_equal defined?(MongoMapper::Dirtier), "constant"
7
+ end
8
+ end
9
+ end
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
+