active_spy 1.0.0.rc

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