observables 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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
- 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.
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
@@ -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
- 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
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