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 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
+