primer 0.1.0

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