association_observers 0.0.3

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.
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ .rvmrc
20
+ .idea
21
+ vendor/bundler_gems
data/.pryrc ADDED
@@ -0,0 +1,24 @@
1
+ Pry.config.prompt = proc do |obj, level, _|
2
+ prompt = ""
3
+ prompt << "#{Rails.version}@" if defined?(Rails)
4
+ prompt << "sinatra@" if defined?(Sinatra)
5
+ prompt << "#{RUBY_VERSION}"
6
+ "#{prompt} (#{obj})> "
7
+ end
8
+
9
+ Pry.config.exception_handler = proc do |output, exception, _|
10
+ output.puts "\e[31m#{exception.class}: #{exception.message}"
11
+ output.puts "from #{exception.backtrace.first}\e[0m"
12
+ end
13
+
14
+
15
+
16
+ begin
17
+ require "awesome_print"
18
+ Pry.config.print = proc {|output, value| Pry::Helpers::BaseHelpers.stagger_output("=> #{value.ai}", output)}
19
+ rescue LoadError => err
20
+ warn "=> Unable to load awesome_print"
21
+ end
22
+
23
+
24
+ require "./lib/association_observers.rb"
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --profile
@@ -0,0 +1,14 @@
1
+ language: ruby
2
+ notifications:
3
+ disabled: true
4
+ only:
5
+ - master
6
+ rvm:
7
+ - 1.8.7
8
+ - 1.9.2
9
+ - 1.9.3
10
+ - jruby
11
+ - rbx
12
+ script: bundle exec rspec spec
13
+ before_script:
14
+ - mysql -e 'create database association_observers;'
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ group :development do
5
+ gem "yard", "0.8.2.1", :require => false
6
+ end
7
+
8
+ gem 'activerecord'
9
+
10
+ platforms :ruby do
11
+ gem 'mysql2'
12
+ end
13
+
14
+
15
+ platforms :jruby do
16
+ gem 'jruby-openssl'
17
+ gem 'activerecord-jdbc-adapter'
18
+ gem 'activerecord-jdbcsqlite3-adapter'
19
+ gem 'jdbc-mysql'
20
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Tiago Cardoso
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,214 @@
1
+ # AssociationObservers
2
+
3
+ This is an alternative implementation of the observer pattern. As you may know, Ruby (and Rails/ActiveRecord) already have an
4
+ implementation of it. This implementation is a variation of the pattern, so it is not supposed to supersede the existing
5
+ implementations, but "complete" them for the specific use-cases addressed.
6
+
7
+ [![Build Status](https://travis-ci.org/TiagoCardoso1983/association_observers.png?branch=master)](https://travis-ci.org/TiagoCardoso1983/association_observers)
8
+
9
+
10
+ ## Comparison with the Observer Pattern
11
+
12
+ The Observer Pattern clearly defines two roles: the observer and the observed. The observer registers itself by the
13
+ observed. The observed decides when (for which "actions") to notify the observer. The observer knows what to do when notified.
14
+
15
+ What's the limitation? The observed has to know when and whom to notify. The observer has to know what to do. For this
16
+ logic to be implemented for two other separate entities, behaviour has to be copied from one place to the other. So, why
17
+ not delegate this information (to whom, when, behaviour) to a third role, the notifier?
18
+
19
+ ## Comparison with Ruby Observable library
20
+
21
+ Great library, which works great for POROs, but not for models (specifically ActiveRecord, which overwrites a lot of its
22
+ functionality)
23
+
24
+ ## Comparison with ActiveRecord Observers
25
+
26
+ Observers there are external entities which observe models. They don't exactly work as links between two models, just
27
+ extract functionality (callbacks) which would otherwise flood the model. For that, they're great. For the rest, not really.
28
+
29
+
30
+ ### Installation
31
+
32
+ Add this line to your application's Gemfile:
33
+
34
+ gem 'association_observers'
35
+
36
+ And then execute:
37
+
38
+ $ bundle
39
+
40
+ Or install it yourself as:
41
+
42
+ $ gem install association_observers
43
+
44
+ ### Usage
45
+
46
+ Here is a functional example:
47
+
48
+ require 'logger'
49
+
50
+ LOGGER = Logger.new(STDOUT)
51
+
52
+ # Notifiers
53
+ class HaveSliceNotifier < Notifier::Base
54
+
55
+ def action(cake, kid)
56
+ cake.update_column(:slices, cake.slices - 1)
57
+ cake.destroy if cake.slices == 0
58
+ kid.increment(:slices).save
59
+ end
60
+
61
+ end
62
+
63
+ class BustKidsAssNotifier < Notifier::Base
64
+
65
+ module ObserverMethods
66
+ def bust_kids_ass!
67
+ LOGGER.info("Slam!!")
68
+ end
69
+ end
70
+
71
+ module ObservableMethods
72
+ def is_for_grandpa?
73
+ true # it is always for grandpa
74
+ end
75
+ end
76
+
77
+ def conditions(cake, mom)
78
+ cake.is_for_grandpa?
79
+ end
80
+
81
+ def action(cake, mom)
82
+ mom.bust_kids_ass!
83
+ end
84
+
85
+ end
86
+
87
+ class TellKidHesFatNotifier < Notifier::Base
88
+
89
+ module ObservableMethods
90
+
91
+ def cry!
92
+ LOGGER.info(":'(")
93
+ end
94
+
95
+ def throw_slices_away!
96
+ update_column(:slices, 0)
97
+ end
98
+ end
99
+
100
+ def conditions(kid, mom)
101
+ kid.slices > 20
102
+ end
103
+
104
+ def action(kid, mom)
105
+ LOGGER.info("Hey Fatty, BEEFCAKE!!!!!")
106
+ kid.cry!
107
+ kid.throw_slices_away!
108
+ end
109
+
110
+ end
111
+
112
+
113
+ # TABLES
114
+
115
+ ActiveRecord::Schema.define do
116
+ create_table :cakes, :force => true do |t|
117
+ t.integer :slices
118
+ t.integer :mom_id
119
+ end
120
+ create_table :kids, :force => true do |t|
121
+ t.integer :mom_id
122
+ t.integer :slices, :default => 0
123
+ end
124
+ create_table :moms, :force => true do |t|
125
+ end
126
+ end
127
+
128
+ # ENTITIES
129
+
130
+ class Cake < ActiveRecord::Base
131
+
132
+ def self.default_slices ; 8 ; end
133
+ belongs_to :mom
134
+ has_one :kid, :through => :mom
135
+ before_create do |record|
136
+ record.slices ||= record.class.default_slices
137
+ end
138
+ end
139
+
140
+ class Mom < ActiveRecord::Base
141
+ has_one :kid
142
+ has_many :cakes
143
+ end
144
+
145
+ class Kid < ActiveRecord::Base
146
+ belongs_to :mom
147
+ has_many :cakes, :through => :mom
148
+
149
+ observes :cakes, :notifier => :have_slice, :on => :create
150
+ end
151
+
152
+ class Mom < ActiveRecord::Base
153
+ observes :cakes, :on => :destroy, :notifier => :bust_kids_ass
154
+ observes :kid, :on => :update, :notifier => :tell_kid_hes_fat
155
+ end
156
+
157
+ You can find this under the examples.
158
+
159
+ The #observes method for the models accepts as argument the association being observed and a set of options:
160
+
161
+ * as : if the association is polymorphic, then this parameter has to be filled with the name by which the observer is recognized by the observed
162
+ * on : accepts one event or a set of events to observe (:create, :update, :save, :destroy) for the observed association (default: :save)
163
+ * notifier(s?): accepts one notifier or a set of notifiers that will handle the events; notifier name has to match the name of the notifier being defined by yourself;
164
+ if you don't set any notifier, then the events on the observed will only propagate to the observers of the observer (if it is being observed)
165
+
166
+
167
+ The other important task is to define your own notifiers. First, where. For Rails, the gem expects a "notifiers" folder to exist under the "app" folder.
168
+ Everywhere else, it's entirely up to you where you should define them.
169
+ Second, how. Your notifier must inherit from Notifier::Base. This class provides you with an API you should define your way:
170
+
171
+ Methods to overwrite:
172
+ * action(observable, observer) : where you should define the behaviour which results of the observation of an event
173
+ * conditions(observable, observer) : checks whether the action should be run (defaults to true if not overwritten)
174
+
175
+ Additionally, you can optimize the behaviour for collection associations. Let's say a brand has many products which know
176
+ its owner, and when the brand changes from owner, you want to update a certain flag on products. Per default, the action
177
+ will be run individually for every product. If we are talking about DB statements and 100 products, it will be 100 sequential
178
+ statements... That's a drag if you can accomplish that in one DB statement. for that, you can overwrite these two methods:
179
+
180
+ * notify_many(observable, observers)
181
+ * conditions_many(observable, observers)
182
+
183
+ the observers parameters is a container of not-yet loaded collection associations. Check your ORM's documentation to know what you can
184
+ do with it and whether you can achieve your result without populating it (there is such a use under the examples).
185
+
186
+ Purpose of the Notifier is to abstract the behaviour from the Observer relationship between associations. But if you still
187
+ need to complement/overwrite behaviour from your observer/observable models, you can write it in notifier-specific modules,
188
+ the ObserverMethods and the ObservableMethods, which will be included in the respective models.
189
+
190
+ ### TODOs
191
+
192
+ * Support for other ORM's (currently only supporting ActiveRecord)
193
+ * Support for other Message Queue libraries (only supporting DelayedJob, rescue, everything that "#delay"s)
194
+ * Action routine definition on the "#observes" declaration (sometimes one does not need the overhead of writing a notifier)
195
+ * Observe method calls (currently only observing model callbacks)
196
+ * Overall spec readability
197
+
198
+ ### Rails
199
+
200
+ The observer models have to be eager-loaded for the observer/observable behaviour to be extended in the respective associations.
201
+ It is kind of a drag, but a drag the Rails Observers already suffer from (these have to be declared in the application configuration).
202
+
203
+ ### Non-Rails
204
+
205
+ If you are auto-loading your models, the same logic from the paragraph above applies. If you are requiring your models,
206
+ just proceed, this is not your concern :)
207
+
208
+ ### Contributing
209
+
210
+ 1. Fork it
211
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
212
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
213
+ 4. Push to the branch (`git push origin my-new-feature`)
214
+ 5. Create new Pull Request
@@ -0,0 +1,3 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :default do ;; end
@@ -0,0 +1,35 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'association_observers/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "association_observers"
8
+ gem.version = AssociationObservers::VERSION
9
+ gem.authors = ["Tiago Cardoso"]
10
+ gem.email = ["tiago@restorm.com"]
11
+ gem.description = %q{This is an alternative implementation of the observer pattern. As you may know, Ruby (and Rails/ActiveRecord) already have an
12
+ implementation of it. This implementation is a variation of the pattern, so it is not supposed to supersede the existing
13
+ implementations, but "complete" them for the specific use-cases addressed.}
14
+ gem.summary = %q{The Observer Pattern clearly defines two roles: the observer and the observed. The observer registers itself by the
15
+ observed. The observed decides when (for which "actions") to notify the observer. The observer knows what to do when notified.
16
+
17
+ What's the limitation? The observed has to know when and whom to notify. The observer has to know what to do. For this
18
+ logic to be implemented for two other separate entities, behaviour has to be copied from one place to the other. So, why
19
+ not delegate this information (to whom, when, behaviour) to a third role, the notifier?}
20
+ gem.homepage = "https://github.com/TiagoCardoso1983/association_observers"
21
+
22
+ gem.files = `git ls-files`.split($/)
23
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
24
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
25
+ gem.require_paths = ["lib"]
26
+
27
+ gem.add_development_dependency("rake",["~> 0.9.2.2"])
28
+ gem.add_development_dependency("rack-test",["=0.6.2"])
29
+ gem.add_development_dependency("rspec",["~> 2.11.0"])
30
+ gem.add_development_dependency("database_cleaner",["=0.8.0"])
31
+ gem.add_development_dependency("colorize",["=0.5.8"])
32
+ gem.add_development_dependency("pry")
33
+ gem.add_development_dependency("pry-doc")
34
+ gem.add_development_dependency("awesome_print")
35
+ end
@@ -0,0 +1,5 @@
1
+ activerecord:
2
+ adapter: mysql2
3
+ encoding: utf8
4
+ username: root
5
+ database: association_observers
@@ -0,0 +1,199 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require "association_observers/version"
3
+ require "association_observers/notifiers/base"
4
+ require "association_observers/notifiers/propagation_notifier"
5
+
6
+ require "association_observers/ruby18" if RUBY_VERSION < "1.9"
7
+
8
+ # Here it is defined the basic behaviour of how observer/observable model associations are set. There are here three
9
+ # main roles defined: The observer associations, the observable associations, and the notifiers (the real observers).
10
+ # Observer Associations: those are the associations of an observable which will be "listening/observing" to updates
11
+ #
12
+ # Observable Associations: those are the associations of an observer which will trigger "listening/observing" events on these
13
+ #
14
+ # Notifiers: These are the handlers which will implement the behaviour desired, knowing who observes and who is observed
15
+ #
16
+ # Purpose of these role definitions is to separate the listening/observing behaviour from the implementation of the logic
17
+ # associated to it. Examples of these: model A has many Bs. The Bs from A are so many that you'd like to want to store
18
+ # a count of the Bs on A. For that you would like A to be informed each time a B is added/deleted, so you could update
19
+ # the counter accordingly. So in this case, A observes B, and B is observed by A. But The CounterNotifier, which is an
20
+ # entity independent from A and B, will implement the logic of updating the counter from Bs on A. This way we can achieve
21
+ # multiple behaviour implementation.
22
+ #
23
+ # @author Tiago Cardoso
24
+ module AssociationObservers
25
+ # translation of AR callbacks to collection callbacks; we want to ignore the update on collections because neither
26
+ # add nor remove shall be considered an update event in the observables
27
+ # @example
28
+ # class RichMan < ActiveRecord::Base
29
+ # has_many :cars
30
+ # observes :cars, :on => :update
31
+ # ...
32
+ # end
33
+ #
34
+ # in this example, for instance, the rich man wants only to be notified when the cars are update, not when he
35
+ # gets a new one or when one of them goes to the dumpster
36
+ COLLECTION_CALLBACKS_MAPPER = {:create => :add, :save => :add, :destroy => :remove}
37
+
38
+ # Methods to be added to observer associations
39
+ module IsObserverMethods
40
+ def self.included(base) ; base.extend(ClassMethods) ; end
41
+
42
+ module ClassMethods
43
+ def observer? ; true ; end
44
+ end
45
+ end
46
+
47
+ # Methods to be added to observable associations
48
+ module IsObservableMethods
49
+ def self.included(base) ; base.extend(ClassMethods) ; end
50
+
51
+ module ClassMethods
52
+ def observable? ; true ; end
53
+ end
54
+
55
+ def unobservable! ; @unobservable = true ; end
56
+ def observable! ; @unobservable = false ; end
57
+
58
+ private
59
+
60
+ # informs the observers that something happened on this observable, passing all the observers to it
61
+ # @param [Symbol] callback key of the callback being notified; only the observers for this callback will be run
62
+ def notify! callback
63
+ notify_observers(callback) unless @unobservable
64
+ end
65
+ end
66
+
67
+ module InstanceMethods
68
+ def observer? ; self.class.observer? ; end
69
+ def observable? ; self.class.observable? ; end
70
+ end
71
+
72
+ module ClassMethods
73
+ def observer? ; false ; end
74
+ def observable? ; false ; end
75
+
76
+ # DSL method which triggers all the behaviour. It sets self as an observer association from defined observable
77
+ # associations in it (these have to be defined in the model).
78
+ #
79
+ # @param [Symbol [, Symbol...]] args the self associations which will be observed
80
+ # @param [Hash] opts additional options
81
+ # @option opts [Symbol] :as name of the polymorphic association on the observables if it is defined like that
82
+ # @option opts [Symbol, Array] :observers name of the observers to be applied (in underscore notation: EmailNotifier would be :email_notifier)
83
+ # @option opts [Symbol, Array] :on which callbacks should the observers be aware of (options are :create, :save and :destroy; callback triggered will always be an after_)
84
+ def observes(*args)
85
+ opts = args.extract_options!
86
+ observer_class = self
87
+
88
+ plural_associations = args.select{ |arg| self.reflections[arg].collection? }
89
+
90
+ association_name = (opts[:as] || self.name.demodulize.underscore).to_s
91
+ notifier_classes = Array(opts[:notifiers] || opts[:notifier]).map{|notifier| notifier.to_s.end_with?("_notifier") ? notifier : "#{notifier}_notifier".to_s }
92
+ observer_callbacks = Array(opts[:on] || [:save, :destroy])
93
+
94
+ # no observer, how are you supposed to observe?
95
+ raise "Invalid callback; possible options: :create, :update, :save, :destroy" unless observer_callbacks.all?{|o|[:create,:update,:save,:destroy].include?(o.to_sym)}
96
+
97
+ # standard observer association methods
98
+ include IsObserverMethods
99
+
100
+ notifier_classes.map!{|notifier_class|notifier_class.to_s.classify.constantize} << PropagationNotifier
101
+
102
+ # observer association methods per observer
103
+ notifier_classes.each do |notifier_class|
104
+ include "#{notifier_class.name}::ObserverMethods".constantize if notifier_class.constants.map(&:to_sym).include?(:ObserverMethods)
105
+ end
106
+
107
+ # 1: for each observed association, define behaviour
108
+ self.reflect_on_all_associations.select{ |r| args.include?(r.name) }.map{|r| [r.klass, r.options] }.each do |klass, options|
109
+ klass.instance_eval do
110
+
111
+ include ActiveModel::Observing
112
+
113
+ # load observers from this observable association
114
+ observer_association_name = (options[:as] || association_name).to_s
115
+ notifier_classes.each do |notifier_class|
116
+ observer_callbacks.each do |callback|
117
+ options = {}
118
+ observer_association = self.reflect_on_association(observer_association_name.to_sym) ||
119
+ self.reflect_on_association(observer_association_name.pluralize.to_sym)
120
+ options[:observer_class] = observer_class.base_class if observer_association.options[:polymorphic]
121
+
122
+ self.add_observer notifier_class.new(callback, observer_association.name, options)
123
+ include "#{notifier_class.name}::ObservableMethods".constantize if notifier_class.constants.map(&:to_sym).include?(:ObservableMethods)
124
+ end
125
+ end
126
+
127
+ # sets the callbacks to inform observers
128
+ observer_callbacks.each do |callback|
129
+ if [:create, :update].include?(callback)
130
+ real_callback = :save
131
+ callback_opts = {:on => callback}
132
+ else
133
+ real_callback = callback
134
+ callback_opts = {}
135
+ end
136
+ send("after_#{real_callback}", callback_opts) do
137
+ notify! callback
138
+ end
139
+ end
140
+
141
+ attr_reader :unobservable
142
+
143
+ include IsObservableMethods
144
+ end
145
+
146
+ end
147
+
148
+ # 2. for each collection association, insert after add and after remove callbacks
149
+
150
+ # first step is defining the methods which will be called by the collection callbacks.
151
+ callback_procs = observer_callbacks.map do |callback|
152
+ notifier_classes.map do |notifier_class|
153
+ routine_name = :"__observer_#{callback}_callback_for_#{notifier_class.name.demodulize.underscore}__"
154
+ class_eval <<-END
155
+ def #{routine_name}(element)
156
+ callback = element.class.observer_instances.detect do |notifier|
157
+ notifier.class.name == '#{notifier_class}' and notifier.callback == :#{callback}
158
+ end
159
+ callback.notify(element, [self]) unless callback.nil?
160
+ end
161
+ private :#{routine_name}
162
+ END
163
+ [callback, routine_name]
164
+ end
165
+ end.flatten(1)
166
+
167
+ # second step is redefining the associations with the proper callbacks to be triggered
168
+ plural_associations.each do |assoc|
169
+ a = self.reflect_on_association(assoc)
170
+ callbacks = Hash[callback_procs.group_by{|code, proc| COLLECTION_CALLBACKS_MAPPER[code] }.reject{|k, v| k.nil? }.map{ |code, val| [:"after_#{code}", val.map(&:last)] }]
171
+ next if callbacks.empty?
172
+ # this snippet takes care that the array of callbacks which will get inserted in the association will not
173
+ # overwrite whatever callbacks you may have already defined
174
+ callbacks.each do |callback, procs|
175
+ callbacks[callback] += Array(a.options[callback])
176
+ end
177
+
178
+ if RUBY_VERSION < "1.9"
179
+ assoc_options = AssociationObservers::extended_to_s(a.options)
180
+ callback_options = AssociationObservers::extended_to_s(callbacks)
181
+ else
182
+ assoc_options = a.options.to_s
183
+ callback_options = callbacks
184
+ end
185
+
186
+ class_eval <<-END
187
+ #{a.macro} :#{assoc}, #{assoc_options}.merge(#{callback_options})
188
+ END
189
+ end
190
+
191
+ end
192
+ end
193
+ end
194
+
195
+ if defined?(Rails::Railtie) # RAILS
196
+ require 'association_observers/railtie'
197
+ else
198
+ require 'association_observers/activerecord'
199
+ end