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 +20 -0
- data/README.rdoc +117 -0
- data/lib/observables.rb +15 -0
- data/lib/observables/array_watcher.rb +50 -0
- data/lib/observables/base.rb +94 -0
- data/lib/observables/collections.rb +15 -0
- data/lib/observables/extensions/array.rb +5 -0
- data/lib/observables/extensions/hash.rb +5 -0
- data/lib/observables/extensions/object.rb +10 -0
- data/lib/observables/hash_watcher.rb +40 -0
- data/lib/observables/version.rb +4 -0
- data/test/extensions/test_extensions.rb +21 -0
- data/test/test_array_watcher.rb +137 -0
- data/test/test_base.rb +127 -0
- data/test/test_hash_watcher.rb +88 -0
- data/test/test_helper.rb +13 -0
- data/test/test_observable_array.rb +13 -0
- data/test/test_observables.rb +9 -0
- metadata +130 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Nathan Stults
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
= Observables
|
2
|
+
|
3
|
+
Observables implements observable arrays and hashes by way of the ActiveModel::Notifier, the same mechanism
|
4
|
+
underlying the instrumentation API in Rails3. Observables collections broadcast detailed change information for
|
5
|
+
any state-modifying operations, including specific elements added or removed, when that information is practically
|
6
|
+
obtainable.
|
7
|
+
|
8
|
+
== Installation
|
9
|
+
|
10
|
+
Observables is available as a RubyGem:
|
11
|
+
|
12
|
+
gem install 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.
|
data/lib/observables.rb
ADDED
@@ -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,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,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
|
data/test/test_helper.rb
ADDED
@@ -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
|
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
|
+
|