association_observers 0.0.3

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