observables 0.1.1

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