active_spy 1.0.0.rc

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,90 @@
1
+ require 'rest-client'
2
+ require 'singleton'
3
+
4
+ module ActiveSpy
5
+ # Module to hold Rails specific classes and helpers.
6
+ #
7
+ module Rails
8
+ # Default template for callbacks handlers.
9
+ #
10
+ class Base
11
+ def initialize(object)
12
+ @object = object
13
+ end
14
+
15
+ # Overriding to avoid sending the object to server 2 times (in both
16
+ # before and after callabcks).
17
+ #
18
+ def respond_to?(method)
19
+ method.include?('after_') || method == 'before_save'
20
+ end
21
+
22
+ # Set a flag in the object to tell us wether it's a new record or not.
23
+ #
24
+ def before_save
25
+ inject_is_new_method(@object)
26
+ @object.is_new = true if @object.new_record?
27
+ end
28
+
29
+ # Inject an attribute in the +object+, called +is_new?+ and a setter
30
+ # for it.
31
+ #
32
+ def inject_is_new_method(object)
33
+ object.instance_eval do
34
+ def is_new=(value)
35
+ @is_new = value
36
+ end
37
+
38
+ def is_new?
39
+ @is_new
40
+ end
41
+ end
42
+ end
43
+
44
+ # Overriding to always send the object to the server, even though the
45
+ # after callback is not explicitly defined.
46
+ #
47
+ def method_missing(method, *_args, &_block)
48
+ host = ActiveSpy::Configuration.event_host
49
+ port = ActiveSpy::Configuration.event_port
50
+
51
+ RestClient.post "#{host}:#{port}/",
52
+ event: get_request_params(method)
53
+ remove_is_new_method(@object)
54
+ end
55
+
56
+ # Get the event request params for a given +method+.
57
+ #
58
+ def get_request_params(method)
59
+ real_method = method.to_s.split('_').last
60
+ action = get_action(real_method)
61
+ {
62
+ type: @object.class.name,
63
+ actor: @object.actor,
64
+ realm: @object.realm,
65
+ payload: @object.payload_for(action),
66
+ action: action
67
+ }
68
+ end
69
+
70
+ # Remove a previously added +is_new+ attribute from a given object.
71
+ #
72
+ def remove_is_new_method(object)
73
+ object.instance_eval do
74
+ undef :is_new=
75
+ undef :is_new?
76
+ end
77
+ end
78
+
79
+ # Returns the correct action for the method called in the model.
80
+ #
81
+ def get_action(real_method)
82
+ if real_method == 'save'
83
+ return 'create' if @object.is_new?
84
+ return 'update'
85
+ end
86
+ 'destroy'
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,7 @@
1
+ module ActiveSpy
2
+ # Class responsible for the controller and routes integration with Rails
3
+ #
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace ActiveSpy
6
+ end
7
+ end
@@ -0,0 +1,114 @@
1
+ module ActiveSpy
2
+ module Rails
3
+ # Class used to hold all the events hook's paths and later sync
4
+ # them with an event runner instance.
5
+ #
6
+ class HookList
7
+ include Singleton
8
+
9
+ # Initialize an empty hook list
10
+ #
11
+ def initialize
12
+ host = ActiveSpy::Configuration.event_host
13
+ port = ActiveSpy::Configuration.event_port
14
+ name = ActiveSpy::Configuration.name.downcase.gsub(' ', '-').strip
15
+ @base_service_url = "#{host}:#{port}/services/#{name}"
16
+ @hooks = []
17
+ end
18
+
19
+ # Proxy all methods called in the {ActiveSpy::Hook} to
20
+ # {ActiveSpy::Hook} instance. Just a syntax sugar.
21
+ #
22
+ def self.method_missing(method, *args, &block)
23
+ instance.send(method, *args, &block)
24
+ end
25
+
26
+ # Clear the hook list.
27
+ #
28
+ def clear
29
+ @hooks = []
30
+ end
31
+
32
+ # forward {<<} method to the hook list.
33
+ #
34
+ def <<(other)
35
+ @hooks << other
36
+ end
37
+
38
+ # Register in event runner all the hooks defined in the list. If some of
39
+ # them already exists, they will be excluded and readded.
40
+ #
41
+ def register
42
+ old_hooks = get_old_hooks
43
+ hooks_to_delete = get_hooks_to_delete(old_hooks)
44
+ hooks_to_add = get_hooks_to_add(old_hooks)
45
+ delete_hooks(hooks_to_delete) if hooks_to_delete.any?
46
+ add_hooks(hooks_to_add) unless hooks_to_add.empty?
47
+ end
48
+
49
+ # Get the old hooks list for this service from the event-runner
50
+ #
51
+ def get_old_hooks
52
+
53
+ JSON.load(RestClient.get(@base_service_url))['hooks']
54
+ end
55
+
56
+ # Select from old hooks those that should be deleted from event runner.
57
+ #
58
+ def get_hooks_to_delete(old_hooks)
59
+ hooks_to_delete = []
60
+ old_hooks.each do |old_hook|
61
+ found = false
62
+ @hooks.each do |hook|
63
+ if hook['class'] == old_hook['class'] && old_hook['active']
64
+ found = true
65
+ break
66
+ end
67
+ end
68
+ next if found
69
+ hooks_to_delete << old_hook
70
+ end
71
+ hooks_to_delete
72
+ end
73
+
74
+ # Select from the hooks defined in the app those that should be created
75
+ # in the event runner.
76
+ #
77
+ def get_hooks_to_add(old_hooks)
78
+ hooks_to_add = []
79
+ @hooks.each do |hook|
80
+ found = false
81
+ old_hooks.each do |old_hook|
82
+ if hook['class'] == old_hook['class'] && old_hook['active']
83
+ found = true
84
+ break
85
+ end
86
+ end
87
+ next if found
88
+ hooks_to_add << hook
89
+ end
90
+ hooks_to_add
91
+ end
92
+
93
+ # Properly delete the +hooks_to_delete+ in the event runner.
94
+ #
95
+ def delete_hooks(hooks_to_delete)
96
+ hooks_to_delete.each do |hook|
97
+ RestClient.delete "#{@base_service_url}/hooks/#{hook['id']}"
98
+ end
99
+ end
100
+
101
+ # # Properly creates the +hooks_to_add+ in the event runner..
102
+ #
103
+ def add_hooks(hooks_to_add)
104
+ hooks_to_add.each do |hook|
105
+ RestClient.post "#{@base_service_url}/hooks", {
106
+ 'class'=> hook['class'],
107
+ 'postPath' => ActiveSpy::Engine.routes.url_helpers.notifications_path(hook['class'].downcase),
108
+ 'active' => true
109
+ }
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,87 @@
1
+ require 'rest-client'
2
+ require 'json'
3
+
4
+ module ActiveSpy
5
+ module Rails
6
+ # Base class used to process the events received.
7
+ #
8
+ class Listener
9
+ include ActiveSupport::Inflector
10
+
11
+
12
+ # Constant to hold the model translations. The key is the incoming
13
+ # +ref_type+ and the value is the matching model class.
14
+ #
15
+ MODEL_HANDLER = {}
16
+
17
+ # Store the event handler hook in the {ActiveSpy::Rails::HookList} for
18
+ # later registration of them within the event runner.
19
+ #
20
+ def self.inherited(child)
21
+ if child.name.include? 'Listener'
22
+ ActiveSpy::Rails::HookList << {
23
+ 'class' => child.name.split('Listener')[0]
24
+ }
25
+ end
26
+ end
27
+
28
+ # Handle a request with +params+ and sync the database according to
29
+ # them.
30
+ #
31
+ def handle(params)
32
+ object_type = params.delete(:type)
33
+ callback = params[:payload].delete(:action)
34
+ payload_content = params.delete(:payload)[object_type.downcase.to_sym]
35
+ actor = params.delete(:actor)
36
+ realm = params.delete(:realm)
37
+
38
+ sync_database(callback, object_type, payload_content, actor, realm)
39
+ end
40
+
41
+ # Calls the proper method to sync the database. It will manipulate
42
+ # objects of the class +object_type+, with the attributes sent in the
43
+ # +payload+, triggered by the callback +callback+.
44
+ #
45
+ def sync_database(callback, object_type, payload, actor, realm)
46
+ send(callback, object_type, payload, actor, realm)
47
+ end
48
+
49
+ # Logic to handle object's creation. You can override this, as you wish,
50
+ # to suit your own needs
51
+ #
52
+ def create(object_type, payload, _actor, _realm)
53
+ klass = get_object_class(object_type)
54
+ klass.new.update_attributes(payload)
55
+ end
56
+
57
+ # Logic to handle object's update. You can override this, as you wish,
58
+ # to suit your own needs
59
+ #
60
+ def update(object_type, payload, _actor, _realm)
61
+ klass = get_object_class(object_type)
62
+ guid = payload.delete(:guid)
63
+ klass.find_by(guid: guid).update_attributes(payload)
64
+ end
65
+
66
+ # Destroy a record from our database. You can override this, as you wish,
67
+ # to suit your own needs
68
+ #
69
+ def destroy(klass, payload, _actor, _realm)
70
+ klass = get_object_class(klass)
71
+ guid = payload.delete(:guid)
72
+ klass.find_by(guid: guid).destroy!
73
+ end
74
+
75
+ # Gets the object class. First, it'll look the {MODEL_HANDLER} hash and
76
+ # see if there is any translation for a given +object_type+. If it does
77
+ # not have a translation, this method will try to +constantize+ the
78
+ # +object_type+.
79
+ #
80
+ def get_object_class(object_type)
81
+ translated_object_type = MODEL_HANDLER[object_type]
82
+ return constantize(translated_object_type) if translated_object_type
83
+ constantize(object_type)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,17 @@
1
+ require 'rails'
2
+
3
+ # Railtie class to automatically include {ActiveSpy::Spy} in all
4
+ # +ActiveRecord::Base+
5
+ #
6
+ class Railtie < Rails::Railtie
7
+ initializer 'active_spy.spies' do
8
+ ActiveSupport.on_load(:active_record) do
9
+ include ActiveSpy::Spy
10
+ include ActiveSpy::Rails::Spy
11
+ end
12
+ end
13
+
14
+ config.after_initialize do
15
+ Rails.application.eager_load!
16
+ end
17
+ end
@@ -0,0 +1,60 @@
1
+ require 'active_support'
2
+
3
+ module ActiveSpy
4
+ # Module used to hold Rails specific logic.
5
+ #
6
+ module Rails
7
+ # Module that defines methods used to spy on some class methods.
8
+ #
9
+ module Spy
10
+ # Default snippet to extends the class with
11
+ # {ActiveSpy::Spy::ClassMethods} when {ActiveSpy::Spy} is included in
12
+ # it.
13
+ #
14
+ def self.included(base)
15
+ base.extend ClassMethods
16
+ end
17
+ # Class methods to be defined in classes that includes {ActiveSpy::Spy}
18
+ #
19
+ module ClassMethods
20
+ # Class method to define the realm of the model.
21
+ #
22
+ def model_realm(realm_name = nil, &block)
23
+ realm = -> { send(realm_name) } if realm_name
24
+ realm = block if block_given?
25
+ define_method :realm do
26
+ realm.call
27
+ end
28
+ end
29
+
30
+ # Class method to define the actor of the model.
31
+ #
32
+ def model_actor(actor_name = nil, &block)
33
+ actor = -> { send(actor_name) } if actor_name
34
+ actor = block if block_given?
35
+ define_method :actor do
36
+ actor.call
37
+ end
38
+ end
39
+
40
+ # Helper to use on Rails app and watch for model creation, update and
41
+ # destruction.
42
+ #
43
+ def watch_model_changes
44
+ watch_method :save, :destroy
45
+ inject_payload_for_method
46
+ end
47
+
48
+ # Helper to inject the method +payload_for(method)+ in the model
49
+ # with the default behavior: all attributes are sent in all
50
+ # actions.
51
+ #
52
+ def inject_payload_for_method
53
+ define_method :payload_for do |_method|
54
+ { self.class.name.downcase.to_sym => attributes }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,42 @@
1
+ require 'active_support'
2
+
3
+ module ActiveSpy
4
+ # Module that defines methods used to spy on some class methods
5
+ #
6
+ module Spy
7
+ # Default snippet to extends the class with {ActiveSpy::Spy::ClassMethods}
8
+ # when {ActiveSpy::Spy} is included in it.
9
+ #
10
+ def self.included(base)
11
+ base.extend ClassMethods
12
+ end
13
+
14
+ # Invokes the callback method on the invoker class. The +callback_type+
15
+ # param tells wether it will be called +:after+ or +before+.
16
+ #
17
+ def invoke_callback(method, callback_type)
18
+ callback_invoker = callback_invoker_class.new(self)
19
+ callback = "#{callback_type}_#{method}"
20
+ return unless callback_invoker.respond_to?(callback)
21
+ callback_invoker.send(callback)
22
+ end
23
+
24
+ # Gets the invoker class based on the class' name
25
+ #
26
+ def callback_invoker_class
27
+ ActiveSupport::Inflector.constantize "#{self.class.name}Events"
28
+ end
29
+
30
+ # Class methods to be defined in classes that includes {ActiveSpy::Spy}
31
+ #
32
+ module ClassMethods
33
+ # Set watchers for the +method+
34
+ #
35
+ def watch_method(*methods)
36
+ methods.each do |method|
37
+ ActiveSpy::SpyList << { 'class' => name, 'method' => method }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,64 @@
1
+ require 'active_support'
2
+ require 'singleton'
3
+
4
+ module ActiveSpy
5
+ # Singleton used to hold the spies and lazely active these spies by patching
6
+ # the methods in the specified classes.
7
+ #
8
+ class SpyList
9
+ include Singleton
10
+
11
+ # Just to initiliaze the spy list.
12
+ #
13
+ def initialize
14
+ @spies = []
15
+ end
16
+
17
+ attr_reader :spies
18
+
19
+ # Proxy all methods called in the {ActiveSpy::SpyList} to
20
+ # {ActiveSpy::SpyList} instance. Just a syntax sugar.
21
+ #
22
+ def self.method_missing(method, *args, &block)
23
+ instance.send(method, *args, &block)
24
+ end
25
+
26
+ # Active all the spies defined in the spy list by patching the methods
27
+ # in their classes.
28
+ #
29
+ def activate
30
+ @spies.each do |spy|
31
+ spied_class = spy['class']
32
+ spied_method = spy['method']
33
+
34
+ patch(spied_class, spied_method)
35
+ end
36
+ end
37
+
38
+ # forward {<<} method to the spy list.
39
+ #
40
+ def <<(other)
41
+ @spies << other
42
+ end
43
+
44
+ private
45
+
46
+ # This method patches the +method+ in the class +klass+ to invoke the
47
+ # callbacks defined in the respective class, that should be named using
48
+ # appending 'Events' to the class' name, and inherites from
49
+ # {ActiveSpy::Base}.
50
+ #
51
+ def patch(klass, method)
52
+ ActiveSupport::Inflector.constantize(klass).class_eval do
53
+
54
+ old_method = instance_method(method)
55
+ define_method method do
56
+ send(:invoke_callback, method, :before)
57
+ result = old_method.bind(self).call
58
+ send(:invoke_callback, method, :after)
59
+ result
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
data/lib/active_spy.rb ADDED
@@ -0,0 +1,48 @@
1
+ require 'active_spy/configuration'
2
+ require 'active_spy/base'
3
+ require 'active_spy/spy/spy'
4
+ require 'active_spy/spy/spy_list'
5
+ require 'active_spy/rails/base' if defined?(Rails)
6
+ require 'active_spy/rails/spy' if defined?(Rails)
7
+ require 'active_spy/rails/railtie' if defined?(Rails)
8
+ require 'active_spy/rails/engine' if defined?(Rails)
9
+ require 'active_spy/rails/engine' if defined?(Rails)
10
+ require 'active_spy/rails/hook_list' if defined?(Rails)
11
+ require 'active_spy/rails/listener'
12
+
13
+ # Base module for the gem
14
+ #
15
+ module ActiveSpy
16
+ if defined?(Rails)
17
+ # @!method self.configure
18
+ # Class method to set the service's name, host and port.
19
+ #
20
+ def self.configure
21
+ Configuration.instance_eval do
22
+ yield(self)
23
+ end
24
+ end
25
+
26
+ # @!method self.register_service
27
+ # Class method to register the service in an event-runner instance.
28
+ #
29
+ def self.register_service
30
+ host = ActiveSpy::Configuration.event_host
31
+ port = ActiveSpy::Configuration.event_port
32
+ @@base_url = "#{host}:#{port}/services"
33
+
34
+ return if self.service_registered?
35
+ RestClient.post @@base_url, service: ActiveSpy::Configuration.settings
36
+ end
37
+
38
+ # @!method self.service_registered?
39
+ # Check if the service was already registetered in the configured event
40
+ # runner instance.
41
+ #
42
+ def self.service_registered?
43
+ name = ActiveSpy::Configuration.name
44
+ r = RestClient.get "#{@@base_url}/#{name.downcase.gsub(' ', '-').strip}"
45
+ r.code == 200
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ module ActiveSpy
4
+ module Generators
5
+ class InstallGenerator < ::Rails::Generators::Base
6
+ desc 'Creates an active_spy gem configuration file at config/active_spy.yml, and an initializer at config/initializers/active_spy.rb'
7
+
8
+ def self.source_root
9
+ @_active_spy_source_root ||= File.expand_path("../templates", __FILE__)
10
+ end
11
+
12
+ def create_config_file
13
+ template 'active_spy.yml', File.join('config', 'active_spy.yml')
14
+ end
15
+
16
+ def create_initializer_file
17
+ template 'initializer.rb', File.join('config', 'initializers', 'active_spy.rb')
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ test:
2
+ name: dummy
3
+ host: http://dummy.com
4
+ port: 80
5
+ event_host: http://event-runner.com
6
+ event_port: 443
7
+
8
+ development:
9
+ name: dummy
10
+ host: http://dummy.com
11
+ port: 80
12
+ event_host: http://event-runner.com
13
+ event_port: 443
14
+
15
+ production:
16
+ name: dummy
17
+ host: http://dummy.com
18
+ port: 80
19
+ event_host: http://event-runner.com
20
+ event_port: 443
@@ -0,0 +1,7 @@
1
+ ActiveSpy::SpyList.activate
2
+
3
+ if Rails.env.production?
4
+ ActiveSpy.register_service
5
+ ActiveSpy::Rails::HookList.register
6
+ end
7
+