primer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +213 -0
- data/example/README.rdoc +69 -0
- data/example/application.rb +26 -0
- data/example/config.ru +5 -0
- data/example/environment.rb +31 -0
- data/example/models/blog_post.rb +4 -0
- data/example/models/connection.rb +10 -0
- data/example/public/style.css +75 -0
- data/example/script/setup_database.rb +11 -0
- data/example/views/index.erb +13 -0
- data/example/views/layout.erb +26 -0
- data/example/views/show.erb +7 -0
- data/example/worker.rb +3 -0
- data/lib/javascript/primer.js +36 -0
- data/lib/primer/bus/amqp.rb +43 -0
- data/lib/primer/bus/memory.rb +12 -0
- data/lib/primer/bus.rb +30 -0
- data/lib/primer/cache/memory.rb +60 -0
- data/lib/primer/cache/redis.rb +70 -0
- data/lib/primer/cache.rb +84 -0
- data/lib/primer/enabler.rb +18 -0
- data/lib/primer/helpers.rb +66 -0
- data/lib/primer/real_time.rb +80 -0
- data/lib/primer/route_set.rb +50 -0
- data/lib/primer/watcher/active_record_macros.rb +70 -0
- data/lib/primer/watcher/macros.rb +70 -0
- data/lib/primer/watcher.rb +62 -0
- data/lib/primer/worker/active_record_agent.rb +120 -0
- data/lib/primer/worker.rb +34 -0
- data/lib/primer.rb +31 -0
- data/spec/models/artist.rb +10 -0
- data/spec/models/blog_post.rb +5 -0
- data/spec/models/calendar.rb +7 -0
- data/spec/models/concert.rb +6 -0
- data/spec/models/performance.rb +6 -0
- data/spec/models/person.rb +14 -0
- data/spec/models/watchable.rb +17 -0
- data/spec/primer/bus_spec.rb +31 -0
- data/spec/primer/cache_spec.rb +309 -0
- data/spec/primer/helpers/erb_spec.rb +89 -0
- data/spec/primer/watcher/active_record_spec.rb +189 -0
- data/spec/primer/watcher_spec.rb +101 -0
- data/spec/schema.rb +31 -0
- data/spec/spec_helper.rb +60 -0
- data/spec/templates/page.erb +3 -0
- 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
|
+
|
data/lib/primer/cache.rb
ADDED
@@ -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,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
|
+
|