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