primer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/README.rdoc +213 -0
  2. data/example/README.rdoc +69 -0
  3. data/example/application.rb +26 -0
  4. data/example/config.ru +5 -0
  5. data/example/environment.rb +31 -0
  6. data/example/models/blog_post.rb +4 -0
  7. data/example/models/connection.rb +10 -0
  8. data/example/public/style.css +75 -0
  9. data/example/script/setup_database.rb +11 -0
  10. data/example/views/index.erb +13 -0
  11. data/example/views/layout.erb +26 -0
  12. data/example/views/show.erb +7 -0
  13. data/example/worker.rb +3 -0
  14. data/lib/javascript/primer.js +36 -0
  15. data/lib/primer/bus/amqp.rb +43 -0
  16. data/lib/primer/bus/memory.rb +12 -0
  17. data/lib/primer/bus.rb +30 -0
  18. data/lib/primer/cache/memory.rb +60 -0
  19. data/lib/primer/cache/redis.rb +70 -0
  20. data/lib/primer/cache.rb +84 -0
  21. data/lib/primer/enabler.rb +18 -0
  22. data/lib/primer/helpers.rb +66 -0
  23. data/lib/primer/real_time.rb +80 -0
  24. data/lib/primer/route_set.rb +50 -0
  25. data/lib/primer/watcher/active_record_macros.rb +70 -0
  26. data/lib/primer/watcher/macros.rb +70 -0
  27. data/lib/primer/watcher.rb +62 -0
  28. data/lib/primer/worker/active_record_agent.rb +120 -0
  29. data/lib/primer/worker.rb +34 -0
  30. data/lib/primer.rb +31 -0
  31. data/spec/models/artist.rb +10 -0
  32. data/spec/models/blog_post.rb +5 -0
  33. data/spec/models/calendar.rb +7 -0
  34. data/spec/models/concert.rb +6 -0
  35. data/spec/models/performance.rb +6 -0
  36. data/spec/models/person.rb +14 -0
  37. data/spec/models/watchable.rb +17 -0
  38. data/spec/primer/bus_spec.rb +31 -0
  39. data/spec/primer/cache_spec.rb +309 -0
  40. data/spec/primer/helpers/erb_spec.rb +89 -0
  41. data/spec/primer/watcher/active_record_spec.rb +189 -0
  42. data/spec/primer/watcher_spec.rb +101 -0
  43. data/spec/schema.rb +31 -0
  44. data/spec/spec_helper.rb +60 -0
  45. data/spec/templates/page.erb +3 -0
  46. metadata +235 -0
@@ -0,0 +1,70 @@
1
+ require 'redis'
2
+
3
+ module Primer
4
+ class Cache
5
+
6
+ class Redis < Cache
7
+ REDIS_CONFIG = {:thread_safe => true}
8
+
9
+ def initialize(config = {})
10
+ config = REDIS_CONFIG.merge(config)
11
+ @redis = ::Redis.new(config)
12
+ end
13
+
14
+ def clear
15
+ @redis.flushdb
16
+ end
17
+
18
+ def put(cache_key, value)
19
+ validate_key(cache_key)
20
+ @redis.set(cache_key, YAML.dump(value))
21
+ publish_change(cache_key)
22
+ RealTime.publish(cache_key, value)
23
+ end
24
+
25
+ def get(cache_key)
26
+ validate_key(cache_key)
27
+ string = @redis.get(cache_key)
28
+ string ? YAML.load(string) : nil
29
+ end
30
+
31
+ def has_key?(cache_key)
32
+ @redis.exists(cache_key)
33
+ end
34
+
35
+ def invalidate(cache_key)
36
+ @redis.del(cache_key)
37
+
38
+ return unless has_key?('deps' + cache_key)
39
+ @redis.smembers('deps' + cache_key).each do |attribute|
40
+ @redis.srem(attribute, cache_key)
41
+ end
42
+ @redis.del('deps' + cache_key)
43
+ end
44
+
45
+ def relate(cache_key, attributes)
46
+ attributes.each do |attribute|
47
+ serial = attribute.join('/')
48
+ @redis.sadd('deps' + cache_key, serial)
49
+ @redis.sadd(serial, cache_key)
50
+ end
51
+ end
52
+
53
+ def keys_for_attribute(attribute)
54
+ serial = attribute.join('/')
55
+ has_key?(serial) ? @redis.smembers(serial) : []
56
+ end
57
+
58
+ def timeout(cache_key, &block)
59
+ return if has_key?('timeouts' + cache_key)
60
+ @redis.set('timeouts' + cache_key, 'true')
61
+ add_timeout(cache_key, @throttle) do
62
+ block.call
63
+ @redis.del('timeouts' + cache_key)
64
+ end
65
+ end
66
+ end
67
+
68
+ end
69
+ end
70
+
@@ -0,0 +1,84 @@
1
+ module Primer
2
+ class Cache
3
+
4
+ autoload :Memory, ROOT + '/primer/cache/memory'
5
+ autoload :Redis, ROOT + '/primer/cache/redis'
6
+
7
+ include Watcher
8
+ watch_calls_to :get
9
+
10
+ include Faye::Timeouts
11
+
12
+ attr_accessor :throttle
13
+ attr_writer :routes
14
+
15
+ def primer_identifier
16
+ [Cache.name]
17
+ end
18
+
19
+ def routes(&block)
20
+ @routes ||= RouteSet.new
21
+ @routes.instance_eval(&block) if block_given?
22
+ @routes
23
+ end
24
+
25
+ def bind_to_bus
26
+ Primer.bus.subscribe(:changes) { |attribute| changed(attribute) }
27
+ end
28
+
29
+ def compute(cache_key)
30
+ return get(cache_key) if has_key?(cache_key)
31
+
32
+ unless block_given? or @routes
33
+ message = "Cannot call Cache#compute(#{cache_key}) with no block: no routes have been configured"
34
+ raise RouteNotFound.new(message)
35
+ end
36
+
37
+ calls = []
38
+ result = Watcher.watching(calls) do
39
+ block_given? ? yield : @routes.evaluate(cache_key)
40
+ end
41
+
42
+ attributes = calls.map do |(receiver, method_name, args, block, return_value)|
43
+ receiver.primer_identifier + [method_name.to_s] + args
44
+ end
45
+
46
+ unless result.nil?
47
+ relate(cache_key, attributes)
48
+ put(cache_key, result)
49
+ end
50
+
51
+ result
52
+ end
53
+
54
+ def changed(attribute)
55
+ keys_for_attribute(attribute).each do |cache_key|
56
+ block = lambda do
57
+ invalidate(cache_key)
58
+ regenerate(cache_key)
59
+ end
60
+ @throttle ? timeout(cache_key, &block) : block.call
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def publish_change(cache_key)
67
+ Primer.bus.publish(:changes, primer_identifier + ['get', cache_key])
68
+ end
69
+
70
+ def regenerate(cache_key)
71
+ compute(cache_key) rescue nil
72
+ end
73
+
74
+ def validate_key(cache_key)
75
+ raise InvalidKey.new(cache_key) unless Cache.valid_key?(cache_key)
76
+ end
77
+
78
+ def self.valid_key?(cache_key)
79
+ Faye::Channel.valid?(cache_key)
80
+ end
81
+
82
+ end
83
+ end
84
+
@@ -0,0 +1,18 @@
1
+ module Primer
2
+ module Enabler
3
+ def enable!
4
+ @enabled = true
5
+ on_enable
6
+ end
7
+
8
+ def disable!
9
+ @enabled = false
10
+ on_disable
11
+ end
12
+
13
+ def enabled?
14
+ !!@enabled
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,66 @@
1
+ module Primer
2
+ module Helpers
3
+
4
+ module ERB
5
+ def primer(cache_key, tag_name = :div, &block)
6
+ result = Primer.cache.compute(cache_key) do
7
+ block_given? ?
8
+ primer_capture_output(&block) :
9
+ Primer.cache.routes.evaluate(cache_key)
10
+ end
11
+
12
+ if Primer.real_time and not block_given?
13
+ result = primer_real_time(result, tag_name, cache_key)
14
+ end
15
+
16
+ return result unless block_given?
17
+ primer_detect_buffer.concat(result)
18
+ nil
19
+ end
20
+
21
+ private
22
+
23
+ def primer_capture_output(&block)
24
+ return primer_capture_output_from_rails(&block) if primer_rails?
25
+ return primer_capture_output_from_sinatra(&block) if primer_sinatra?
26
+ end
27
+
28
+ def primer_real_time(fragment, tag_name, cache_key)
29
+ <<-HTML
30
+ <#{ tag_name } id="#{ RealTime.dom_id(cache_key) }">
31
+ #{ fragment }
32
+ </#{ tag_name }>
33
+ <script type="text/javascript">PRIMER_CHANNELS.push(#{ cache_key.inspect })</script>
34
+ HTML
35
+ end
36
+
37
+ def primer_detect_buffer
38
+ [@output_buffer, @_out_buf].compact.first
39
+ end
40
+
41
+ def primer_rails?
42
+ return false unless respond_to?(:capture)
43
+ return true if defined?(ActionView::OutputBuffer)
44
+ String === @output_buffer
45
+ end
46
+
47
+ def primer_capture_output_from_rails(&block)
48
+ capture(&block)
49
+ end
50
+
51
+ def primer_sinatra?
52
+ defined?(@_out_buf)
53
+ end
54
+
55
+ def primer_capture_output_from_sinatra(&block)
56
+ original_buffer = @_out_buf
57
+ result = @_out_buf = ''
58
+ block.call
59
+ @_out_buf = original_buffer
60
+ result.to_s
61
+ end
62
+ end
63
+
64
+ end
65
+ end
66
+
@@ -0,0 +1,80 @@
1
+ module Primer
2
+ class RealTime < Faye::RackAdapter
3
+
4
+ BAYEUX_CONFIG = {:mount => '/primer/bayeux', :timeout => 25}
5
+ SCRIPT_PATH = '/primer.js'
6
+ TYPE_SCRIPT = {'Content-Type' => 'text/javascript'}
7
+ SCRIPT_SOURCE = File.read(ROOT + '/javascript/primer.js')
8
+
9
+ class ServerAuth
10
+ def incoming(message, callback)
11
+ channel = message['channel']
12
+ return callback.call(message) if Faye::Channel.meta?(channel)
13
+
14
+ password = message['ext'] && message['ext']['password']
15
+ unless password == RealTime.password
16
+ message['error'] = Faye::Error.ext_mismatch
17
+ end
18
+
19
+ message['ext'].delete('password') if password
20
+ callback.call(message)
21
+ end
22
+ end
23
+
24
+ class ClientAuth
25
+ def outgoing(message, callback)
26
+ channel = message['channel']
27
+ return callback.call(message) if Faye::Channel.meta?(channel)
28
+
29
+ message['ext'] ||= {}
30
+ message['ext']['password'] = RealTime.password
31
+
32
+ callback.call(message)
33
+ end
34
+ end
35
+
36
+ def initialize(app)
37
+ super(app, BAYEUX_CONFIG)
38
+ add_extension(ServerAuth.new)
39
+ end
40
+
41
+ def call(env)
42
+ request = Rack::Request.new(env)
43
+ return super unless request.path_info == SCRIPT_PATH
44
+ [200, TYPE_SCRIPT, [SCRIPT_SOURCE]]
45
+ end
46
+
47
+ class << self
48
+ attr_accessor :bayeux_server, :password
49
+
50
+ def dom_id(cache_key)
51
+ "primer#{ ('-' + cache_key).gsub(/[^a-z0-9]+/i, '-') }"
52
+ end
53
+
54
+ def publish(cache_key, value)
55
+ return unless Primer.real_time
56
+ client.publish(cache_key,
57
+ :dom_id => dom_id(cache_key),
58
+ :content => value
59
+ )
60
+ end
61
+
62
+ private
63
+
64
+ def client
65
+ raise NotConfigured.new unless bayeux_server
66
+
67
+ Faye.ensure_reactor_running!
68
+ return @client if @client
69
+
70
+ endpoint = "#{ bayeux_server }#{ BAYEUX_CONFIG[:mount] }"
71
+ @client = Faye::Client.new(endpoint)
72
+ @client.add_extension(ClientAuth.new)
73
+
74
+ @client
75
+ end
76
+ end
77
+
78
+ end
79
+ end
80
+
@@ -0,0 +1,50 @@
1
+ require 'sinatra'
2
+
3
+ module Primer
4
+ class RouteSet
5
+ def initialize(&routes)
6
+ @app = Class.new(Router)
7
+ instance_eval(&routes) if block_given?
8
+ end
9
+
10
+ def get(path, &block)
11
+ @app.get(path, &block)
12
+ end
13
+
14
+ def evaluate(path)
15
+ @app.new(path).evaluate
16
+ end
17
+ end
18
+
19
+ class Router < Sinatra::Base
20
+ class Request
21
+ attr_reader :path_info
22
+ def initialize(path)
23
+ @path_info = path
24
+ end
25
+ end
26
+
27
+ # Circumvent the fact that Sinatra::Base.new creates a Rack stack
28
+ def self.new(*args)
29
+ Class.instance_method(:new).bind(self).call(*args)
30
+ end
31
+
32
+ def initialize(path)
33
+ @request = Request.new(path)
34
+ @original_params = indifferent_hash
35
+ end
36
+
37
+ def evaluate
38
+ routes = self.class.routes['GET']
39
+ catch(:halt) {
40
+ routes.each do |pattern, keys, conditions, block|
41
+ process_route(pattern, keys, conditions) do
42
+ throw(:halt, instance_eval(&block))
43
+ end
44
+ end
45
+ raise RouteNotFound.new("No route match found for key #{@request.path_info}")
46
+ }
47
+ end
48
+ end
49
+ end
50
+
@@ -0,0 +1,70 @@
1
+ module Primer
2
+ module Watcher
3
+
4
+ module ActiveRecordMacros
5
+ def self.extended(klass)
6
+ klass.watch_calls_to(*klass.attributes_watchable_by_primer)
7
+ klass.__send__(:include, InstanceMethods)
8
+ klass.after_create(:notify_primer_after_create)
9
+ klass.after_update(:notify_primer_after_update)
10
+ klass.after_destroy(:notify_primer_after_destroy)
11
+ end
12
+
13
+ def attributes_watchable_by_primer
14
+ attributes = columns + reflect_on_all_associations
15
+ attributes.map { |c| c.name.to_s }
16
+ end
17
+
18
+ def primer_foreign_key_mappings
19
+ return @primer_foreign_key_mappings if defined?(@primer_foreign_key_mappings)
20
+
21
+ foreign_keys = reflect_on_all_associations.
22
+ select { |a| a.macro == :belongs_to }.
23
+ map { |a| [a.primary_key_name.to_s, a.name] }
24
+
25
+ @primer_foreign_key_mappings = Hash[foreign_keys]
26
+ end
27
+
28
+ def patch_method_for_primer(method_name)
29
+ method = instance_method(method_name) rescue nil
30
+ return super if method
31
+ class_eval <<-RUBY
32
+ def #{method_name}
33
+ result = read_attribute(:#{method_name})
34
+ Primer::Watcher.log(self, :#{method_name}, [], nil, result)
35
+ result
36
+ end
37
+ RUBY
38
+ end
39
+
40
+ def unpatch_method_for_primer(method_name)
41
+ alias_name = Macros.alias_name(method_name)
42
+ method = instance_method(alias_name) rescue nil
43
+ return super if method
44
+ class_eval <<-RUBY
45
+ undef_method :#{method_name}
46
+ RUBY
47
+ end
48
+
49
+ module InstanceMethods
50
+ def primer_identifier
51
+ ['ActiveRecord', self.class.name, read_attribute(self.class.primary_key)]
52
+ end
53
+
54
+ def notify_primer_after_create
55
+ Primer.bus.publish(:active_record, [:create, self.class.name, attributes])
56
+ end
57
+
58
+ def notify_primer_after_update
59
+ Primer.bus.publish(:active_record, [:update, self.class.name, attributes, changes])
60
+ end
61
+
62
+ def notify_primer_after_destroy
63
+ Primer.bus.publish(:active_record, [:destroy, self.class.name, attributes])
64
+ end
65
+ end
66
+ end
67
+
68
+ end
69
+ end
70
+
@@ -0,0 +1,70 @@
1
+ module Primer
2
+ module Watcher
3
+
4
+ module Macros
5
+ attr_reader :primer_watched_calls
6
+
7
+ def self.extended(klass)
8
+ if defined?(ActiveRecord) and klass < ActiveRecord::Base
9
+ klass.extend(ActiveRecordMacros)
10
+ end
11
+ end
12
+
13
+ def self.alias_name(method_name)
14
+ method_name.to_s.gsub(/[^a-z0-9_]$/i, '') + '_before_primer_patch'
15
+ end
16
+
17
+ def watch_calls_to(*methods)
18
+ Watcher.register(self)
19
+ @primer_watched_calls ||= []
20
+ @primer_watched_calls += methods
21
+ end
22
+
23
+ def inherited(subclass)
24
+ super
25
+ calls = @primer_watched_calls || []
26
+ subclass.watch_calls_to(*calls)
27
+ end
28
+
29
+ def patch_for_primer!
30
+ return if @primer_watched_calls.nil? or @primer_patched
31
+ @primer_patched = true
32
+ @primer_watched_calls.each do |method_name|
33
+ patch_method_for_primer(method_name)
34
+ end
35
+ end
36
+
37
+ def patch_method_for_primer(method_name)
38
+ alias_name = Macros.alias_name(method_name)
39
+ return unless method = instance_method(method_name) rescue nil
40
+ class_eval <<-RUBY
41
+ alias :#{alias_name} :#{method_name}
42
+ def #{method_name}(*args, &block)
43
+ result = #{alias_name}(*args, &block)
44
+ Primer::Watcher.log(self, :#{method_name}, args, block, result)
45
+ result
46
+ end
47
+ RUBY
48
+ end
49
+
50
+ def unpatch_for_primer!
51
+ return if @primer_watched_calls.nil? or not @primer_patched
52
+ @primer_patched = false
53
+ @primer_watched_calls.each do |method_name|
54
+ unpatch_method_for_primer(method_name)
55
+ end
56
+ end
57
+
58
+ def unpatch_method_for_primer(method_name)
59
+ alias_name = Macros.alias_name(method_name)
60
+ return unless method = instance_method(alias_name) rescue nil
61
+ class_eval <<-RUBY
62
+ alias :#{method_name} :#{alias_name}
63
+ undef_method :#{alias_name}
64
+ RUBY
65
+ end
66
+ end
67
+
68
+ end
69
+ end
70
+
@@ -0,0 +1,62 @@
1
+ module Primer
2
+ module Watcher
3
+
4
+ extend Enabler
5
+ autoload :Macros, ROOT + '/primer/watcher/macros'
6
+ autoload :ActiveRecordMacros, ROOT + '/primer/watcher/active_record_macros'
7
+
8
+ def self.included(klass)
9
+ klass.extend(Macros)
10
+ end
11
+
12
+ def self.register(klass)
13
+ @classes ||= Set.new
14
+ @classes.add(klass)
15
+ end
16
+
17
+ def self.reset!
18
+ Thread.current[:primer_call_log] = []
19
+ Thread.current[:primer_loggers] = nil
20
+ end
21
+
22
+ def self.call_log
23
+ Thread.current[:primer_call_log] ||= []
24
+ end
25
+
26
+ def self.log(receiver, method_name, args, block, result)
27
+ call = [receiver, method_name, args, block, result]
28
+ loggers.each { |logger| logger << call }
29
+ end
30
+
31
+ def self.loggers
32
+ Thread.current[:primer_loggers] ||= [call_log]
33
+ end
34
+
35
+ def self.on_enable
36
+ @classes.each { |klass| klass.patch_for_primer! }
37
+ end
38
+
39
+ def self.on_disable
40
+ @classes.each { |klass| klass.unpatch_for_primer! }
41
+ end
42
+
43
+ def self.watching(calls = [])
44
+ @active_watching_blocks ||= 0
45
+ @active_watching_blocks += 1
46
+ was_enabled = enabled?
47
+ enable!
48
+ loggers << calls
49
+ result = yield
50
+ loggers.pop
51
+ @active_watching_blocks -= 1
52
+ disable! if @active_watching_blocks.zero? and not was_enabled
53
+ result
54
+ end
55
+
56
+ def primer_identifier
57
+ ['Object', self.class.name, object_id]
58
+ end
59
+
60
+ end
61
+ end
62
+
@@ -0,0 +1,120 @@
1
+ module Primer
2
+ class Worker
3
+
4
+ class ActiveRecordAgent
5
+ def self.bind_to_bus
6
+ Primer.bus.subscribe :active_record do |event, class_name, attributes, changes|
7
+ model = class_name.constantize.new(attributes)
8
+ model.instance_eval do
9
+ @attributes = attributes
10
+ @changed_attributes = changes if changes
11
+ end
12
+ __send__("on_#{event}", model)
13
+ end
14
+ end
15
+
16
+ def self.macros
17
+ Watcher::ActiveRecordMacros
18
+ end
19
+
20
+ def self.on_create(model)
21
+ notify_belongs_to_associations(model)
22
+ end
23
+
24
+ def self.on_update(model)
25
+ notify_attributes(model, model.changes)
26
+ end
27
+
28
+ def self.on_destroy(model)
29
+ notify_attributes(model, model.attributes)
30
+ notify_has_many_associations(model)
31
+ end
32
+
33
+ def self.notify_attributes(model, fields)
34
+ foreign_keys = model.class.primer_foreign_key_mappings
35
+
36
+ fields.each do |attribute, value|
37
+ Primer.bus.publish(:changes, model.primer_identifier + [attribute.to_s])
38
+
39
+ next unless assoc = foreign_keys[attribute.to_s]
40
+ Primer.bus.publish(:changes, model.primer_identifier + [assoc.to_s])
41
+ notify_belongs_to_association(model, assoc, value)
42
+ end
43
+ end
44
+
45
+ def self.notify_belongs_to_associations(model)
46
+ model.class.reflect_on_all_associations.each do |assoc|
47
+ next unless assoc.macro == :belongs_to
48
+ notify_belongs_to_association(model, assoc.name)
49
+ end
50
+ end
51
+
52
+ def self.notify_belongs_to_association(model, assoc_name, change = nil)
53
+ assoc = model.class.reflect_on_association(assoc_name)
54
+ owner_class = assoc.class_name.constantize
55
+
56
+ mirror = mirror_association(model.class, owner_class, :has_many)
57
+
58
+ if owner = model.__send__(assoc_name)
59
+ Primer.bus.publish(:changes, owner.primer_identifier + [mirror.name.to_s])
60
+ notify_has_many_through_association(owner, mirror.name)
61
+ end
62
+
63
+ return unless Array === change and change.first.any?
64
+ old_id = change.first.first
65
+ previous = owner_class.find(:first, :conditions => {owner_class.primary_key => old_id})
66
+ return unless previous
67
+
68
+ Primer.bus.publish(:changes, previous.primer_identifier + [mirror.name.to_s])
69
+ notify_has_many_through_association(previous, mirror.name)
70
+ end
71
+
72
+ def self.notify_has_many_associations(model)
73
+ model.class.reflect_on_all_associations.each do |assoc|
74
+ next unless assoc.macro == :has_many
75
+ next if assoc.options[:dependent] == :destroy
76
+
77
+ model_id = model.__send__(model.class.primary_key)
78
+ klass = assoc.class_name.constantize
79
+ related = klass.find(:all, :conditions => {assoc.primary_key_name => model_id})
80
+
81
+ related.each do |object|
82
+ mirror = mirror_association(model.class, object.class, :belongs_to)
83
+ next unless mirror
84
+
85
+ Primer.bus.publish(:changes, object.primer_identifier + [mirror.name.to_s])
86
+ end
87
+ end
88
+ end
89
+
90
+ def self.notify_has_many_through_association(model, through_name)
91
+ model.class.reflect_on_all_associations.each do |assoc|
92
+ next unless assoc.macro == :has_many
93
+
94
+ if assoc.options[:through] == through_name
95
+ Primer.bus.publish(:changes, model.primer_identifier + [assoc.name.to_s])
96
+ end
97
+
98
+ assoc.class_name.constantize.reflect_on_all_associations.each do |secondary|
99
+ next unless secondary.macro == :has_many and secondary.options[:through] and
100
+ secondary.source_reflection.active_record == model.class and
101
+ secondary.source_reflection.name == through_name
102
+
103
+ model.__send__(assoc.name).each do |related|
104
+ Primer.bus.publish(:changes, related.primer_identifier + [secondary.name.to_s])
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ def self.mirror_association(object_class, related_class, macro)
111
+ related_class.reflect_on_all_associations.find do |mirror_assoc|
112
+ mirror_assoc.macro == macro and
113
+ mirror_assoc.class_name == object_class.name
114
+ end
115
+ end
116
+ end
117
+
118
+ end
119
+ end
120
+