observables 0.1.1

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 observables
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,15 @@
1
+ require 'rubygems'
2
+
3
+ base_dir = File.dirname(__FILE__)
4
+ [
5
+ 'base',
6
+ 'array_watcher',
7
+ 'hash_watcher',
8
+ 'collections',
9
+ 'version'
10
+ ].each {|req| require File.join(base_dir,'observables',req)}
11
+
12
+ Dir[File.join(base_dir,"observables","extensions","*.rb")].each {|ext|require ext}
13
+
14
+ module Observables
15
+ end
@@ -0,0 +1,50 @@
1
+
2
+ module Observables
3
+ module ArrayWatcher
4
+ include Observables::Base
5
+
6
+ ADD_METHODS = :<<, :push, :concat, :insert, :unshift
7
+ MODIFIER_METHODS = :collect!, :map!, :flatten!, :replace, :reverse!, :sort!, :fill
8
+ REMOVE_METHODS = :clear, :compact!, :delete, :delete_at, :delete_if, :pop, :reject!, :shift, :slice!, :uniq!
9
+
10
+ #[]= can either be an add method or a modifier method depending on
11
+ #if the previous key exists
12
+ def []=(*args)
13
+ change_type = args[0] >= length ? :added : :modified
14
+ changes = changes_for(change_type,:"[]=",*args)
15
+ changing(change_type,:trigger=>:"[]=", :changes=>changes) {super}
16
+ end
17
+
18
+ override_mutators :added=> ADD_METHODS,
19
+ :modified=> MODIFIER_METHODS,
20
+ :removed=> REMOVE_METHODS
21
+
22
+ def changes_for(change_type, trigger_method, *args, &block)
23
+ prev = self.dup.to_a
24
+ if change_type == :added
25
+ case trigger_method
26
+ when :"[]=" then lambda {{:added=>args[-1]}}
27
+ when :<<, :push, :unshift then lambda {{:added=>args}}
28
+ when :concat then lambda {{:added=>args[0]}}
29
+ when :insert then lambda {{:added=>args[1..-1]}}
30
+ else lambda { |cur| {:added=>(cur - prev).uniq }}
31
+ end
32
+ elsif change_type == :removed
33
+ case trigger_method
34
+ when :delete then lambda {{:removed=>args}}
35
+ when :delete_at then lambda {{:removed=>[prev[args[0]]]}}
36
+ when :delete_if, :reject! then lambda {{:removed=>prev.select(&block)}}
37
+ when :pop then lambda {{:removed=>[prev[-1]]}}
38
+ when :shift then lambda {{:removed=>[prev[0]]}}
39
+ else lambda { |cur| {:removed=>(prev - cur).uniq }}
40
+ end
41
+ else
42
+ case trigger_method
43
+ when :replace then lambda {{:removed=>prev, :added=>args[0]}}
44
+ when :"[]=" then lambda {{:removed=>[prev[*args[0..-2]]].flatten, :added=>[args[-1]].flatten}}
45
+ else lambda {|cur|{:removed=>prev.uniq, :added=>cur}}
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,94 @@
1
+ require 'active_support/concern'
2
+ require "active_support/notifications/fanout"
3
+ require 'active_support/core_ext/object/duplicable'
4
+ require 'active_support/core_ext/array/extract_options'
5
+
6
+ module Observables
7
+ module Base
8
+ extend ActiveSupport::Concern
9
+
10
+ module InstanceMethods
11
+ def notifier
12
+ @notifier ||= ActiveSupport::Notifications::Fanout.new
13
+ end
14
+
15
+ def subscribe(pattern=nil,&block)
16
+ notifier.subscribe(pattern,&block)
17
+ end
18
+
19
+ def unsubscribe(subscriber)
20
+ notifier.unsubscribe(subscriber)
21
+ end
22
+
23
+ def dup
24
+ super.tap {|s|s.make_observable}
25
+ end
26
+
27
+ def set_observer(*args,&block)
28
+ clear_observer
29
+ opts = args.extract_options!
30
+ @_observer_owner = args.pop
31
+ pattern = opts[:pattern] || /.*/
32
+ callback_method = opts[:callback_method] || :child_changed
33
+ @_owner_subscription = subscribe(pattern) do |*args|
34
+ block ? block.call(self,*args) :
35
+ (@_observer_owner.send(callback_method,self,*args) if @_observer_owner && @_observer_owner.respond_to?(callback_method))
36
+ end
37
+ end
38
+
39
+ def clear_observer
40
+ unsubscribe(@_owner_subscription) if @_owner_subscription
41
+ @_owner_subscription = nil
42
+ end
43
+
44
+ protected
45
+
46
+ def changing(change_type,opts={})
47
+ args = create_event_args(change_type,opts)
48
+ notifier.publish "before_#{change_type}".to_sym, args
49
+ yield.tap do
50
+ notifier.publish "after_#{change_type}".to_sym, args
51
+ end
52
+ end
53
+
54
+ def create_event_args(change_type,opts={})
55
+ args = {:change_type=>change_type, :current_values=>self}.merge(opts)
56
+ class << args
57
+ def changes
58
+ chgs, cur_values = self[:changes], self[:current_values]
59
+ chgs && chgs.respond_to?(:call) ? chgs.call(cur_values) : chgs
60
+ end
61
+
62
+ def method_missing(method)
63
+ self.keys.include?(method) ? self[method] : super
64
+ end
65
+ end
66
+ args.delete(:current)
67
+ args
68
+ end
69
+
70
+ def changes_for(change_type,trigger_method,*args,&block)
71
+ #This method should return a lambda that takes the current
72
+ #value of the collection as an argument, and returns
73
+ #the expected changes that will result from trigger_method
74
+ nil
75
+ end
76
+ end
77
+
78
+ module ClassMethods
79
+ def override_mutators(change_groups)
80
+ change_groups.each_pair do |change_type,methods|
81
+ methods.each do |method|
82
+ class_eval <<-EOS
83
+ def #{method}(*args,&block)
84
+ changes = changes_for(:#{change_type},:#{method},*args,&block)
85
+ changing(:#{change_type},:trigger=>:#{method}, :changes=>changes){super}
86
+ end
87
+ EOS
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,15 @@
1
+ module Observables
2
+ class Array < ::Array
3
+ include ArrayWatcher
4
+ def initialize(*args)
5
+ super(*args)
6
+ end
7
+ end
8
+
9
+ class Hash < ::Hash
10
+ include HashWatcher
11
+ def initialize(*args)
12
+ super(*args)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ class ::Array
2
+ def make_observable
3
+ class << self; include Observables::ArrayWatcher; end unless observable?
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class ::Hash
2
+ def make_observable
3
+ class << self; include Observables::HashWatcher; end unless observable?
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ class ::Object
2
+ def can_be_observable?
3
+ respond_to?(:make_observable)
4
+ end
5
+
6
+ def observable?
7
+ kind_of?(Observables::Base)
8
+ end
9
+
10
+ end
@@ -0,0 +1,40 @@
1
+ module Observables
2
+ module HashWatcher
3
+ include Observables::Base
4
+
5
+ MODIFIER_METHODS = :replace, :merge!, :update
6
+ REMOVE_METHODS = :clear, :delete, :delete_if, :reject!, :shift
7
+
8
+ #[]= can either be an add method or a modifier method depending on
9
+ #if the previous key exists
10
+ def []=(key,val)
11
+ change_type = keys.include?(key) ? :modified : :added
12
+ changes = changes_for(change_type,:[]=,key,val)
13
+ changing(change_type,:trigger=>:[]=, :changes=>changes) {super}
14
+ end
15
+ alias :store :[]=
16
+
17
+ override_mutators :modified=>MODIFIER_METHODS,
18
+ :removed=>REMOVE_METHODS
19
+
20
+ def changes_for(change_type, trigger_method, *args, &block)
21
+ prev = self.dup
22
+ if change_type == :added
23
+ lambda {{:added=>[args]}}
24
+ elsif change_type == :removed
25
+ case trigger_method
26
+ when :clear then lambda{{:removed=>prev.to_a}}
27
+ when :delete then lambda{{:removed=>[[args[0],prev[args[0]]]]}}
28
+ when :delete_if, :reject! then lambda{{:removed=>prev.select(&block)}}
29
+ when :shift then lambda { {:removed=>[prev.keys[0],prev.values[0]]}}
30
+ end
31
+ else
32
+ case trigger_method
33
+ when :[]= then lambda{{:removed=>[[args[0],prev[args[0]]]],:added=>[args]}}
34
+ when :replace then lambda{{:removed=>prev.to_a, :added=>args[0].to_a}}
35
+ when :merge!, :update then lambda{{:removed=>prev.select{|k,_|args[0].keys.include?(k)},:added=>args[0].to_a}}
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,4 @@
1
+ # encoding: UTF-8
2
+ module Observables
3
+ Version = '0.1.1'
4
+ end
@@ -0,0 +1,21 @@
1
+ require "test_helper"
2
+
3
+ class TestExtensions < Test::Unit::TestCase
4
+ context "Array" do
5
+ context "#make_observable" do
6
+
7
+ should "detect an observable array" do
8
+ x, y = [], [].tap{|a|a.make_observable}
9
+ assert_equal false, x.observable?
10
+ assert y.observable?
11
+ end
12
+
13
+ should "make an instance of an array observable" do
14
+ x = []
15
+ assert_equal false, x.observable?
16
+ x.make_observable
17
+ assert x.observable?
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,137 @@
1
+ require "test_helper"
2
+
3
+ class TestArrayWatcher < Test::Unit::TestCase
4
+ context "An array which has included Observables::ArrayWatcher" do
5
+ setup do
6
+ @ary = Array.new([1,2,3]).tap do |a|
7
+ a.make_observable
8
+ end
9
+ end
10
+
11
+ should "notify observers of any change that adds elements to itself" do
12
+ before_methods, after_methods = [],[]
13
+ method_list = Observables::ArrayWatcher::ADD_METHODS
14
+ @ary.subscribe(/before_added/){|_,args|before_methods<<args[:trigger]}
15
+ @ary.subscribe(/after_added/) {|_,args|after_methods<<args[:trigger]}
16
+ method_list.each do |method|
17
+ @ary.send(method,*args_for(method))
18
+ end
19
+
20
+ assert_equal method_list, before_methods
21
+ assert_equal method_list, after_methods
22
+ end
23
+
24
+ should "notify observers of any change that modifies elements" do
25
+ before_methods, after_methods = [],[]
26
+ method_list = Observables::ArrayWatcher::MODIFIER_METHODS
27
+ @ary.subscribe(/before_modified/){|_,args|before_methods<<args[:trigger]}
28
+ @ary.subscribe(/after_modified/) {|_,args|after_methods<<args[:trigger]}
29
+ method_list.each do |method|
30
+ args = args_for(method)
31
+ args ? @ary.send(method,*args) : @ary.send(method)
32
+ end
33
+ assert_equal method_list, before_methods
34
+ assert_equal method_list, after_methods
35
+ end
36
+
37
+ should "notify observers of any change that removes elements" do
38
+ before_methods, after_methods = [],[]
39
+ method_list = Observables::ArrayWatcher::REMOVE_METHODS
40
+ @ary.subscribe(/before_removed/){|_,args|before_methods<<args[:trigger]}
41
+ @ary.subscribe(/after_removed/) {|_,args|after_methods<<args[:trigger]}
42
+ method_list.each do |method|
43
+ args = args_for(method)
44
+ args ? @ary.send(method,*args) : @ary.send(method)
45
+ end
46
+ assert_equal method_list, before_methods
47
+ assert_equal method_list, after_methods
48
+ end
49
+
50
+ context "Calling #changes on the event args" do
51
+ should "calculate changes for #<<" do
52
+ assert_equal [9],get_changes(@ary){ @ary << 9}[:added]
53
+ end
54
+ should "calculate changes for #push, #unshift" do
55
+ [:push,:unshift].each do|method|
56
+ ary = @ary.dup
57
+ assert_equal [1,2,3],get_changes(ary){ary.send(method,1,2,3)}[:added]
58
+ end
59
+ end
60
+ should "calculate changes for #concat" do
61
+ assert_equal [1,2,3],get_changes(@ary){@ary.concat([1,2,3])}[:added]
62
+ end
63
+ should "calculate changes for insert" do
64
+ assert_equal [3,4,5],get_changes(@ary){@ary.insert(2,3,4,5)}[:added]
65
+ end
66
+ should "calculate changes for delete" do
67
+ assert_equal [2],get_changes(@ary){@ary.delete(2)}[:removed]
68
+ end
69
+ should "calculate changes for delete_at" do
70
+ assert_equal [2],get_changes(@ary){@ary.delete_at(1)}[:removed]
71
+ end
72
+ should "calculate changes for delete_if, reject!" do
73
+ [:delete_if, :reject!].each do |method|
74
+ ary = @ary.dup
75
+ assert_equal [2], get_changes(ary){ary.send(method){|i|i%2==0}}[:removed]
76
+ end
77
+ end
78
+ should "calculate changes for pop" do
79
+ assert_equal [3], get_changes(@ary){@ary.pop}[:removed]
80
+ end
81
+ should "calculate changes for shift" do
82
+ assert_equal [1], get_changes(@ary){@ary.shift}[:removed]
83
+ end
84
+ should "calculate changes for clear" do
85
+ assert_equal [1,2,3], get_changes(@ary){@ary.clear}[:removed]
86
+ end
87
+ should "calculate changes for compact!" do
88
+ @ary = [1,2,nil,3,nil,4].tap{|a|a.make_observable}
89
+ assert_equal [nil], get_changes(@ary){@ary.compact!}[:removed]
90
+ end
91
+ should "calculate changes for slice!" do
92
+ assert_equal [2,3], get_changes(@ary){@ary.slice!(1,2)}[:removed]
93
+ end
94
+ should "calculate changes for uniq!" do
95
+ @ary = [1,2,2,3,3,4,4,5].tap{|a|a.make_observable}
96
+ assert_equal [], get_changes(@ary){@ary.uniq!}[:removed]
97
+ end
98
+ should "calculate changes for replace" do
99
+ assert_equal({:removed=>@ary.dup,:added=>[4,5,6,7]}, get_changes(@ary){@ary.replace([4,5,6,7])})
100
+ end
101
+ should "calculate changes for []=" do
102
+ assert_equal [6,7,8,9], get_changes(@ary){@ary[3,4]=[6,7,8,9]}[:added]
103
+ end
104
+ should "calculated changes for []= when []= is a modification method" do
105
+ assert_equal({:removed=>[1],:added=>[9]},get_changes(@ary){@ary[0]=9})
106
+ end
107
+ should "return the original array as changes for other modification methods of array" do
108
+ [:collect!, :map!, :flatten!, :reverse!, :sort!].each do |method|
109
+ ary = @ary.dup
110
+ assert_equal({:removed=>ary.dup, :added=>ary.dup.tap{|a|a.send(method)}}, get_changes(ary){ary.send(method)})
111
+ end
112
+ end
113
+ should "return the original array as changes for fill" do
114
+ assert_equal({:removed=>@ary.dup, :added=>@ary.dup.fill("*")}, get_changes(@ary){@ary.fill("*")})
115
+ end
116
+ end
117
+ end
118
+
119
+ def args_for(method)
120
+ case method
121
+ when :<<, :push, :unshift, :delete, :delete_at then [1]
122
+ when :concat, :replace then [[1,2]]
123
+ when :fill then ["*"]
124
+ when :insert, :"[]=", :slice! then [1,1]
125
+ when :flatten!, :collect!, :map!, :reverse!, :sort!,
126
+ :clear, :compact!, :pop, :reject!, :uniq!, :delete_if, :shift then nil
127
+ end
128
+ end
129
+
130
+ def get_changes(ary)
131
+ changes = []
132
+ sub = ary.subscribe(/after/){|_,args|changes << args.changes}
133
+ yield
134
+ ary.unsubscribe(sub)
135
+ changes.pop
136
+ end
137
+ end
data/test/test_base.rb ADDED
@@ -0,0 +1,127 @@
1
+ require 'test_helper'
2
+
3
+ class TestBase < Test::Unit::TestCase
4
+
5
+ context "An instance of a class that include Observables:Base" do
6
+ setup do
7
+ @obs = Class.new{include Observables::Base}.new
8
+ end
9
+
10
+ should "have a notifier" do
11
+ assert @obs.notifier.is_a?(ActiveSupport::Notifications::Fanout)
12
+ end
13
+
14
+ should "allow subscriptions with just a block" do
15
+ assert @obs.subscribe{}.is_a?(ActiveSupport::Notifications::Fanout::Subscriber)
16
+ end
17
+
18
+ should "allow subscriptions with a pattern and a block" do
19
+ assert @obs.subscribe(/whatever/){}.is_a?(ActiveSupport::Notifications::Fanout::Subscriber)
20
+ end
21
+
22
+ should "allow unsubscribing" do
23
+ sub = @obs.subscribe(/hi/){}
24
+ assert @obs.notifier.listening?("hi")
25
+ @obs.unsubscribe(sub)
26
+ assert_equal false, @obs.notifier.listening?("hi")
27
+ end
28
+
29
+ should "publish a before notification prior to executing a change" do
30
+ vals = []
31
+ x = 0
32
+ @obs.subscribe(/before/){|c,a|vals << c << a << x}
33
+ @obs.send(:changing,:a_change,:this=>:that){x+=1}
34
+ assert_equal :before_a_change, vals[0]
35
+ assert_equal @obs.send(:create_event_args,:a_change,:this=>:that),vals[1]
36
+ assert_equal 0, vals[2]
37
+ assert_equal 1, x
38
+ end
39
+
40
+ should "publish an after notification after executing a change" do
41
+ vals = []
42
+ x = 0
43
+ @obs.subscribe(/after/){|c,a|vals << c << a << x}
44
+ @obs.send(:changing,:a_change,:this=>:that){x+=1}
45
+ assert_equal :after_a_change, vals[0]
46
+ assert_equal @obs.send(:create_event_args,:a_change,:this=>:that),vals[1]
47
+ assert_equal 1, vals[2]
48
+ assert_equal 1, x
49
+ end
50
+
51
+ should "execute a proc passed in as changes to the event args" do
52
+ vals = []
53
+ @obs.subscribe(/after/){|_,a|vals << a.changes}
54
+ @obs.send(:changing, :a_change, :changes=>lambda {[1,2,3]}){1==1}
55
+ assert_equal [1,2,3], vals[0]
56
+ end
57
+
58
+ should "provide method level access to change args" do
59
+ vals = []
60
+ @obs.subscribe(/after/){|_,a|vals << a.haha}
61
+ @obs.send(:changing, :a_change, :haha=>"hoho"){1==1}
62
+ assert_equal "hoho", vals[0]
63
+ end
64
+
65
+ context "Taking ownership of an observable collection" do
66
+ setup do
67
+ @owner = Class.new do
68
+ def child_changed(*args)
69
+ @changed_args = args
70
+ end
71
+ def another_child_changed(*args)
72
+ @changed_args = args
73
+ end
74
+ def changed_args; @changed_args; end
75
+ end
76
+ @parent = @owner.new
77
+ end
78
+ should "notify the parent via standard callback method" do
79
+ @obs.set_observer @parent
80
+ @obs.send(:changing, :a_change){1==1}
81
+ assert_equal @obs, @parent.changed_args[0]
82
+ end
83
+ should "notify the parent via custom callback method when specified" do
84
+ @obs.set_observer @parent, :callback_method=>:another_child_changed
85
+ @obs.send(:changing, :a_change) {1==1}
86
+ assert_equal @obs, @parent.changed_args[0]
87
+ end
88
+ should "notify the parent via a block if provided" do
89
+ changed_args = []
90
+ @obs.set_observer(@parent) { |obs,*_| changed_args << obs }
91
+ @obs.send(:changing, :a_change) {1==1}
92
+ assert_equal @obs, changed_args.pop
93
+ end
94
+ should "respect a subscription pattern when notifying the parent" do
95
+ events = []
96
+ @obs.set_observer(@parent, :pattern=>/before/){|_,evt,*_| events << evt}
97
+ @obs.send(:changing,:a_change){1==1}
98
+ assert_equal 1, events.length
99
+ assert_equal :before_a_change, events.pop
100
+ end
101
+ should "notify the parent via argless block" do
102
+ events = []
103
+ @obs.set_observer(@parent, :pattern=>/before/){events << 1}
104
+ @obs.send(:changing, :a_change){1==1}
105
+ assert_equal 1, events.length
106
+ end
107
+ should "notify via block when no owner is given" do
108
+ events = []
109
+ my_ary = [1,2,3]
110
+ my_ary.make_observable
111
+ my_ary.set_observer(:pattern=>/before/){events << 1}
112
+ my_ary << 1
113
+ assert_equal 1, events.length
114
+ end
115
+ should "stop notifying the parent after clear_observer is called" do
116
+ events = []
117
+ @obs.set_observer(@parent){|*args|events << args}
118
+ @obs.send(:changing,:a_change){1==1}
119
+ assert_equal 2, events.length
120
+ @obs.clear_observer
121
+ @obs.send(:changing,:a_change){1==1}
122
+ assert_equal 2, events.length
123
+ end
124
+
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,88 @@
1
+ require "test_helper"
2
+
3
+ class TestHashWatcher < Test::Unit::TestCase
4
+ context "A hash which has included Observables::HashWatcher" do
5
+ setup do
6
+ @hash = {:a=>1,:b=>2,:c=>"3"}.tap do |h|
7
+ class << h
8
+ include Observables::HashWatcher
9
+ end
10
+ end
11
+ end
12
+
13
+ should "notify observers of any change that modifies elements" do
14
+ before_methods, after_methods = [],[]
15
+ method_list = Observables::HashWatcher::MODIFIER_METHODS
16
+ @hash.subscribe(/before_modified/){|_,args|before_methods<<args[:trigger]}
17
+ @hash.subscribe(/after_modified/) {|_,args|after_methods<<args[:trigger]}
18
+ method_list.each do |method|
19
+ args = args_for(method)
20
+ args ? @hash.send(method,args) : @hash.send(method)
21
+ end
22
+ assert_equal method_list, before_methods
23
+ assert_equal method_list, after_methods
24
+ end
25
+
26
+ should "notify observers of any change that removes elements" do
27
+ before_methods, after_methods = [],[]
28
+ method_list = Observables::HashWatcher::REMOVE_METHODS
29
+ @hash.subscribe(/before_removed/){|_,args|before_methods<<args[:trigger]}
30
+ @hash.subscribe(/after_removed/) {|_,args|after_methods<<args[:trigger]}
31
+ method_list.each do |method|
32
+ args = args_for(method)
33
+ args ? @hash.send(method,*args) : @hash.send(method)
34
+ end
35
+ assert_equal method_list, before_methods
36
+ assert_equal method_list, after_methods
37
+ end
38
+
39
+ context "Calling #changes on the event args" do
40
+ should "calculate changes for #[]= as an addition" do
41
+ assert_equal [[:f,9]],get_changes(@hash){ @hash[:f] = 9}[:added]
42
+ end
43
+ should "calculate changes for #[]= as a modification" do
44
+ assert_equal({:removed=>[[:a,1]],:added=>[[:a,9]]}, get_changes(@hash){@hash[:a]=9})
45
+ end
46
+ should "calculate changes for #replace" do
47
+ assert_equal({:removed=>@hash.dup.to_a,:added=>{:t=>9,:u=>10}.to_a},get_changes(@hash){@hash.replace(:t=>9,:u=>10)})
48
+ end
49
+ should "calculate changes for #merge!, #update" do
50
+ [:merge!, :update].each do |method|
51
+ hash = @hash.dup
52
+ assert_equal({:removed=>{:c=>"3"}.to_a, :added=>{:c=>"4",:d=>5}.to_a},get_changes(hash){hash.send(method,{:c=>"4",:d=>5})})
53
+ end
54
+ end
55
+ should "calculate changes for #clear" do
56
+ assert_equal @hash.to_a, get_changes(@hash){@hash.clear}[:removed]
57
+ end
58
+ should "calculate changes for #delete" do
59
+ assert_equal({:a=>1}.to_a, get_changes(@hash){@hash.delete(:a)}[:removed])
60
+ end
61
+ should "calculate changes for #delete_if, #reject!" do
62
+ [:delete_if,:reject!].each do |method|
63
+ hash = @hash.dup
64
+ assert_equal({:a=>1,:c=>"3"}.to_a,get_changes(hash){hash.send(method){|k,v|[:a,:c].include?(k)}}[:removed])
65
+ end
66
+ end
67
+ should "calculate changes for #shift" do
68
+ assert_equal(@hash.dup.shift,get_changes(@hash){@hash.shift}[:removed])
69
+ end
70
+ end
71
+ end
72
+
73
+ def args_for(method)
74
+ case method
75
+ when :replace, :merge!, :update then {:e=>5}
76
+ when :delete then :a
77
+ else nil
78
+ end
79
+ end
80
+
81
+ def get_changes(hash)
82
+ changes = []
83
+ sub = hash.subscribe(/after/){|_,args|changes << args.changes}
84
+ yield
85
+ hash.unsubscribe(sub)
86
+ changes.pop
87
+ end
88
+ end
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ $:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
5
+
6
+ require 'observables'
7
+
8
+ require 'shoulda'
9
+
10
+ class Test::Unit::TestCase
11
+
12
+ end
13
+
@@ -0,0 +1,13 @@
1
+ require "test_helper"
2
+
3
+ class TestObservablesArray < Test::Unit::TestCase
4
+ context "An Observables::Array" do
5
+ should "be observable" do
6
+ assert Observables::Array.new.observable?
7
+ end
8
+ should "be an array" do
9
+ assert Observables::Array.new.kind_of?(::Array)
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ require 'test_helper'
2
+
3
+ class TestObservables < Test::Unit::TestCase
4
+ context "including observables" do
5
+ should "include something or other" do
6
+ assert_equal defined?(Observables), "constant"
7
+ end
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: observables
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 1
9
+ version: 0.1.1
10
+ platform: ruby
11
+ authors:
12
+ - Nathan Stults
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-10-05 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ version_requirements: &id001 !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ~>
24
+ - !ruby/object:Gem::Version
25
+ segments:
26
+ - 3
27
+ - 0
28
+ - 0
29
+ version: 3.0.0
30
+ requirement: *id001
31
+ prerelease: false
32
+ name: activesupport
33
+ type: :runtime
34
+ - !ruby/object:Gem::Dependency
35
+ version_requirements: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ segments:
40
+ - 0
41
+ version: "0"
42
+ requirement: *id002
43
+ prerelease: false
44
+ name: i18n
45
+ type: :runtime
46
+ - !ruby/object:Gem::Dependency
47
+ version_requirements: &id003 !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ segments:
52
+ - 0
53
+ version: "0"
54
+ requirement: *id003
55
+ prerelease: false
56
+ name: rake
57
+ type: :development
58
+ - !ruby/object:Gem::Dependency
59
+ version_requirements: &id004 !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ~>
62
+ - !ruby/object:Gem::Version
63
+ segments:
64
+ - 2
65
+ - 11
66
+ version: "2.11"
67
+ requirement: *id004
68
+ prerelease: false
69
+ name: shoulda
70
+ type: :development
71
+ description:
72
+ email:
73
+ - hereiam@sonic.net
74
+ executables: []
75
+
76
+ extensions: []
77
+
78
+ extra_rdoc_files: []
79
+
80
+ files:
81
+ - lib/observables/array_watcher.rb
82
+ - lib/observables/base.rb
83
+ - lib/observables/collections.rb
84
+ - lib/observables/extensions/array.rb
85
+ - lib/observables/extensions/hash.rb
86
+ - lib/observables/extensions/object.rb
87
+ - lib/observables/hash_watcher.rb
88
+ - lib/observables/version.rb
89
+ - lib/observables.rb
90
+ - test/extensions/test_extensions.rb
91
+ - test/test_array_watcher.rb
92
+ - test/test_base.rb
93
+ - test/test_hash_watcher.rb
94
+ - test/test_helper.rb
95
+ - test/test_observables.rb
96
+ - test/test_observable_array.rb
97
+ - LICENSE
98
+ - README.rdoc
99
+ has_rdoc: true
100
+ homepage: http://github.com/PlasticLizard/observables
101
+ licenses: []
102
+
103
+ post_install_message:
104
+ rdoc_options: []
105
+
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ segments:
113
+ - 0
114
+ version: "0"
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ segments:
120
+ - 0
121
+ version: "0"
122
+ requirements: []
123
+
124
+ rubyforge_project:
125
+ rubygems_version: 1.3.6
126
+ signing_key:
127
+ specification_version: 3
128
+ summary: Observable arrays and hashes
129
+ test_files: []
130
+