observables 0.1.1 → 0.1.2
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 -20
- data/README.rdoc +117 -117
- data/lib/observables.rb +14 -14
- data/lib/observables/array_watcher.rb +50 -50
- data/lib/observables/base.rb +97 -94
- data/lib/observables/collections.rb +14 -14
- data/lib/observables/extensions/array.rb +4 -4
- data/lib/observables/extensions/hash.rb +4 -4
- data/lib/observables/extensions/object.rb +9 -9
- data/lib/observables/hash_watcher.rb +39 -39
- data/lib/observables/version.rb +4 -4
- data/test/extensions/test_extensions.rb +20 -20
- data/test/{test_observables.rb → test_Observables.rb} +9 -9
- data/test/test_array_watcher.rb +136 -136
- data/test/test_base.rb +126 -126
- data/test/test_hash_watcher.rb +88 -88
- data/test/test_helper.rb +13 -13
- data/test/test_observable_array.rb +12 -12
- metadata +41 -28
data/LICENSE
CHANGED
@@ -1,20 +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.
|
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
CHANGED
@@ -1,117 +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
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
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.
|
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,args|
|
54
|
+
puts "Change type:#{change_type}, changes: #{args.changes}"
|
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 { |sender,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
CHANGED
@@ -1,15 +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
|
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
15
|
end
|
@@ -1,50 +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
|
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
|
data/lib/observables/base.rb
CHANGED
@@ -1,94 +1,97 @@
|
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
@
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
end
|
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
|
+
#This check is necessary in case a proxy is made observable,
|
25
|
+
#with an impementation of dup that returns its non-observable
|
26
|
+
#target
|
27
|
+
super.tap {|s| s.make_observable if s.respond_to?(:make_observable) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def set_observer(*args,&block)
|
31
|
+
clear_observer
|
32
|
+
opts = args.extract_options!
|
33
|
+
@_observer_owner = args.pop
|
34
|
+
pattern = opts[:pattern] || /.*/
|
35
|
+
callback_method = opts[:callback_method] || :child_changed
|
36
|
+
@_owner_subscription = subscribe(pattern) do |*args|
|
37
|
+
block ? block.call(self,*args) :
|
38
|
+
(@_observer_owner.send(callback_method,self,*args) if @_observer_owner && @_observer_owner.respond_to?(callback_method))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def clear_observer
|
43
|
+
unsubscribe(@_owner_subscription) if @_owner_subscription
|
44
|
+
@_owner_subscription = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
protected
|
48
|
+
|
49
|
+
def changing(change_type,opts={})
|
50
|
+
args = create_event_args(change_type,opts)
|
51
|
+
notifier.publish "before_#{change_type}".to_sym, args
|
52
|
+
yield.tap do
|
53
|
+
notifier.publish "after_#{change_type}".to_sym, args
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_event_args(change_type,opts={})
|
58
|
+
args = {:change_type=>change_type, :current_values=>self}.merge(opts)
|
59
|
+
class << args
|
60
|
+
def changes
|
61
|
+
chgs, cur_values = self[:changes], self[:current_values]
|
62
|
+
chgs && chgs.respond_to?(:call) ? chgs.call(cur_values) : chgs
|
63
|
+
end
|
64
|
+
|
65
|
+
def method_missing(method)
|
66
|
+
self.keys.include?(method) ? self[method] : super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
args.delete(:current)
|
70
|
+
args
|
71
|
+
end
|
72
|
+
|
73
|
+
def changes_for(change_type,trigger_method,*args,&block)
|
74
|
+
#This method should return a lambda that takes the current
|
75
|
+
#value of the collection as an argument, and returns
|
76
|
+
#the expected changes that will result from trigger_method
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
module ClassMethods
|
82
|
+
def override_mutators(change_groups)
|
83
|
+
change_groups.each_pair do |change_type,methods|
|
84
|
+
methods.each do |method|
|
85
|
+
class_eval <<-EOS
|
86
|
+
def #{method}(*args,&block)
|
87
|
+
changes = changes_for(:#{change_type},:#{method},*args,&block)
|
88
|
+
changing(:#{change_type},:trigger=>:#{method}, :changes=>changes){super}
|
89
|
+
end
|
90
|
+
EOS
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|