icentris-rules 0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +15 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +3 -0
  4. data/Rakefile +33 -0
  5. data/app/assets/images/pyr_rules/ui-anim_basic_16x16.gif +0 -0
  6. data/app/assets/javascripts/pyr_rules/rules.js +135 -0
  7. data/app/assets/stylesheets/pyr_rules/rules.css.scss +258 -0
  8. data/app/assets/stylesheets/scaffold.css +57 -0
  9. data/app/controllers/pyr_rules/actions_controller.rb +22 -0
  10. data/app/controllers/pyr_rules/events_controller.rb +17 -0
  11. data/app/controllers/pyr_rules/rules_controller.rb +233 -0
  12. data/app/helpers/pyr_rules/actions_helper.rb +2 -0
  13. data/app/helpers/pyr_rules/events_helper.rb +95 -0
  14. data/app/helpers/pyr_rules/rules_helper.rb +10 -0
  15. data/app/models/pyr_rules.rb +3 -0
  16. data/app/models/pyr_rules/action.rb +116 -0
  17. data/app/models/pyr_rules/rule.rb +201 -0
  18. data/app/rules/pyr_rules/action_handler.rb +94 -0
  19. data/app/rules/pyr_rules/controller_event_emitter.rb +34 -0
  20. data/app/rules/pyr_rules/email_action_handler.rb +6 -0
  21. data/app/rules/pyr_rules/event_concerns.rb +23 -0
  22. data/app/rules/pyr_rules/event_config_loader.rb +97 -0
  23. data/app/rules/pyr_rules/event_subscriber.rb +36 -0
  24. data/app/rules/pyr_rules/model_creation_handler.rb +25 -0
  25. data/app/rules/pyr_rules/model_event_emitter.rb +80 -0
  26. data/app/rules/pyr_rules/rule_context.rb +20 -0
  27. data/app/rules/pyr_rules/rules_config.rb +87 -0
  28. data/app/rules/pyr_rules/rules_engine.rb +38 -0
  29. data/app/rules/pyr_rules/web_request_logger.rb +6 -0
  30. data/app/views/pyr_rules/actions/index.html.erb +12 -0
  31. data/app/views/pyr_rules/events/_sidebar.html.erb +8 -0
  32. data/app/views/pyr_rules/events/index.html.erb +72 -0
  33. data/app/views/pyr_rules/events/show.html.erb +11 -0
  34. data/app/views/pyr_rules/rules/_form.html.erb +28 -0
  35. data/app/views/pyr_rules/rules/_ready_headers.js.erb +11 -0
  36. data/app/views/pyr_rules/rules/_rule_context_field.html.erb +10 -0
  37. data/app/views/pyr_rules/rules/_rules.html.erb +34 -0
  38. data/app/views/pyr_rules/rules/add_action_mapping.js.erb +13 -0
  39. data/app/views/pyr_rules/rules/edit.html.erb +14 -0
  40. data/app/views/pyr_rules/rules/index.html.erb +5 -0
  41. data/app/views/pyr_rules/rules/lookup_sub_properties.js.erb +13 -0
  42. data/app/views/pyr_rules/rules/new.html.erb +11 -0
  43. data/app/views/pyr_rules/rules/remove_action_mapping.js.erb +15 -0
  44. data/app/views/pyr_rules/rules/show.html.erb +98 -0
  45. data/app/views/shared/_action.html.erb +41 -0
  46. data/app/views/shared/_action_field_mapping.html.erb +33 -0
  47. data/app/views/shared/_event.html.erb +21 -0
  48. data/app/views/shared/_rule.html.erb +27 -0
  49. data/config/routes.rb +32 -0
  50. data/config/security.yml +9 -0
  51. data/db/seeds.rb +1 -0
  52. data/lib/pyr_rules.rb +34 -0
  53. data/lib/pyr_rules/engine.rb +72 -0
  54. data/lib/pyr_rules/generators/dummy_generator.rb +27 -0
  55. data/lib/pyr_rules/version.rb +3 -0
  56. data/lib/tasks/pyr_rules_tasks.rake +57 -0
  57. metadata +224 -0
@@ -0,0 +1,23 @@
1
+ module PyrRules
2
+ module EventConcerns
3
+
4
+
5
+
6
+ def self.included(base)
7
+ _register(base)
8
+ end
9
+
10
+ def self._register(klazz)
11
+ self.registered_classes ||= []
12
+ self.registered_classes << klazz if !self.registered_classes.index(klazz)
13
+ end
14
+
15
+ def build_event_payload(event_type, class_name, action, extras={})
16
+ payload = { type: event_type, klazz: self.class.name, action: action, id: self.id, data: self,
17
+ user: Thread.current[:user] }.merge(extras)
18
+ ActiveSupport::Notifications.instrument("PyrRules_#{event_type}", payload )
19
+
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,97 @@
1
+ module PyrRules
2
+ class EventConfigLoader
3
+ @@events_dictionary ||= nil
4
+
5
+ def self.reload_events_dictionary
6
+ puts "Reloading EventsDictionary"
7
+ @@events_dictionary = nil
8
+ events_list(true)
9
+ end
10
+
11
+ # A simple list containing every Event known to the system...
12
+ def self.events_list(refresh = false)
13
+ PyrRules::EventConfigLoader.events_dictionary(refresh).map do |et,et_v|
14
+ et_v.map do |k,v|
15
+ v[:actions].map do |a|
16
+ "#{k}::#{a}"
17
+ end
18
+ end
19
+ end.flatten
20
+ end
21
+
22
+
23
+ def self.events_dictionary(refresh = false)
24
+ return @@events_dictionary if @@events_dictionary
25
+ # Force load of models and controllers when eager class loading isn't enabled by default
26
+ # http://stackoverflow.com/questions/516579/is-there-a-way-to-get-a-collection-of-all-the-models-in-your-rails-app
27
+ puts "DEBUG[icentris-rules] Building Events Dictionary"
28
+ start_time = Time.new
29
+ if PyrRules.active_record
30
+ ActiveRecord::Base.send :include, PyrRules::ModelEventEmitter
31
+ model_classes = ActiveRecord::Base.connection.tables.collect{|t| t.classify.constantize rescue nil }.compact
32
+ model_classes.each do |mc|
33
+ puts "PyrRules::EventConfigLoader loading #{mc.to_s}"
34
+ mc.class_eval {}
35
+ end
36
+ end
37
+
38
+ if Rails.env.development?
39
+ PyrRules.development_model_paths.each do |path|
40
+ puts "PyrRules::EventConfigLoader - scanning models via #{path}"
41
+ Dir.glob(path).sort.each do |entry|
42
+ puts " - Loading model: #{entry}"
43
+ require_dependency "#{entry}"
44
+ end
45
+ end
46
+ end
47
+
48
+ puts "\n\nPyrRules::ModelEventEmitter.registered_classes = #{PyrRules::ModelEventEmitter.registered_classes}\n\n"
49
+ @@events_dictionary = {
50
+ model_events: add_model_events({},PyrRules::ModelEventEmitter.registered_classes),
51
+ controller_events: add_controller_events
52
+ }
53
+ puts "DEBUG[icentris-rules] Events Dictionary Built in #{Time.new - start_time} seconds\n\n\n"
54
+ @@events_dictionary
55
+ end
56
+
57
+ def self.add_controller_events
58
+ results = {}
59
+ r=Rails.application.routes.routes.to_a.dup
60
+ i=ActionDispatch::Routing::RoutesInspector.new r
61
+ m=i.send(:collect_routes,r)
62
+ events = m.map do |a|
63
+ p=a[:reqs].split("#")
64
+ class_name = "#{p[0].camelcase}Controller"
65
+ c = (results[class_name] ||= {context:{"system" => :messaging_user, "actor" => :user}, actions:[]})
66
+ if p[1]
67
+ action = p[1].split(" ").first # it might have extra routing info on the end (subdomain, format, etc...) - we don't use this yet
68
+ c[:actions] << action unless c[:actions].include? action
69
+ end
70
+ end
71
+ results
72
+ end
73
+
74
+ # One liner to dump the rules config in consoles
75
+ # PyrRules::RulesConfig.events.each{|k,v| puts k.to_s.titleize; v.keys.sort.eac:qh{|key| puts " #{key}"; v[key][:context].each{|col_name, col_config| puts " #{col_name}(#{col_config[:type]})"}}};nil
76
+ private
77
+ def self.add_model_events(results={}, event_classes)
78
+ puts "DEBUG[icentris-rules] add_model_events"
79
+ event_classes.each do |klazz|
80
+ puts "DEBUG[icentris-rules] #{klazz}"
81
+ cols={};
82
+ if klazz != ActiveRecord::Base # Don't add ActiveRecord::Base as an event type, but recurse its subclasses later
83
+ cols["system"] = :messaging_user
84
+ cols["actor"] = :user
85
+ cols["klazz"] = :string
86
+ cols["target"] = :object
87
+ results[klazz.name] = {context: cols, actions: ModelEventEmitter.model_actions }
88
+ end
89
+ klazz.subclasses.each do |sc|
90
+ add_model_events(results, [sc])
91
+ end
92
+ puts "DEBUG[icentris-rules] #{klazz} :: #{results[klazz.name]}"
93
+ end
94
+ results
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,36 @@
1
+ module PyrRules
2
+ class EventSubscriber
3
+
4
+ def initialize
5
+ end
6
+
7
+ def connect
8
+ @redis = Redis.new
9
+ begin
10
+ @redis.subscribe( "pyr_events" ) do |on|
11
+ on.subscribe do |channel, subscriptions|
12
+ puts "Subscribed to ##{channel} (#{subscriptions} subscriptions)"
13
+ end
14
+
15
+ on.message do |channel, message|
16
+ puts "##{channel}: #{message}"
17
+ @redis.unsubscribe if message == "exit"
18
+ end
19
+
20
+ on.unsubscribe do |channel, subscriptions|
21
+ puts "Unsubscribed from ##{channel} (#{subscriptions} subscriptions)"
22
+ end
23
+ end
24
+ rescue Redis::BaseConnectionError => error
25
+ puts "#{error}, retrying in 1s"
26
+ sleep 1
27
+ retry
28
+ end
29
+ end
30
+
31
+ def disconnect
32
+ @redis.unsubscribe
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ class PyrRules::ModelCreationHandler < PyrRules::ActionHandler
2
+
3
+ needs :type, :class_lookup
4
+
5
+ def handle
6
+ super
7
+ model_instance = model_class.new
8
+ if model_instance.respond_to? :user=
9
+ model_instance.user = owner
10
+ end
11
+ set_model_props model_instance
12
+ puts "Saving instance #{model_instance.attributes}"
13
+ model_instance.save!
14
+ end
15
+
16
+ # Subclassers add your mappings here
17
+ def set_model_props(m)
18
+
19
+ end
20
+
21
+ def model_class
22
+ raise "Subclassers please implement model_class"
23
+ end
24
+
25
+ end
@@ -0,0 +1,80 @@
1
+ # This will hook into ActiveRecordCallbacks and raise the appropriate crud events
2
+ #
3
+ # For reference:
4
+ # http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
5
+ #
6
+ # For sanity:
7
+ # (-) save
8
+ # (-) valid
9
+ # (1) before_validation
10
+ # (-) validate
11
+ # (2) after_validation
12
+ # (3) before_save
13
+ # (4) before_create
14
+ # (-) create
15
+ # (5) after_create
16
+ # (6) after_save
17
+ # (7) after_commit -or- after_rollback
18
+ #
19
+ # (1) before_destroy
20
+ # (-) destroy
21
+ # (2) after_destroy
22
+ #
23
+ # (-) find
24
+ # (1) after_find
25
+ # (2) after_initialize
26
+
27
+ module PyrRules
28
+ module ModelEventEmitter
29
+
30
+ @@registered_classes = []
31
+
32
+ def self.included(base)
33
+ #base.send :extend, ClassMethods
34
+ #unless ( File.basename($0) == "rake" && ARGV.include?("db:migrate") )
35
+ #end
36
+ base.after_save :raise_updated
37
+ base.after_create :raise_created
38
+ base.before_destroy :raise_destroyed
39
+ @@registered_classes << base if !registered_classes.index(base)
40
+ end
41
+
42
+ def self.registered_classes
43
+ @@registered_classes
44
+ end
45
+
46
+ def self.model_actions
47
+ [:create, :update, :delete]
48
+ end
49
+
50
+ private
51
+ def raise_updated
52
+ _notify("update", changes: self.changes)
53
+ end
54
+
55
+ def raise_created
56
+ _notify("create")
57
+ end
58
+
59
+ def raise_destroyed
60
+ _notify("delete")
61
+ end
62
+
63
+ def _notify(action, extras = {})
64
+ begin
65
+ puts "DEBUG: DBH: ModelEventEmitter action[#{action}] class[#{self.class.name}]"
66
+ payload = build_event_payload( "ModelEvent", self.class.name, action, extras)
67
+ payload[:id] = self.id
68
+ ActiveSupport::Notifications.instrument("PyrRules_#{self.class.name}_#{action}", payload )
69
+ rescue Exception => e
70
+ puts "Unable to raise event payload on #{self} : #{e.message}"
71
+ end
72
+ end
73
+
74
+ def build_event_payload(event_type, class_name, action, extras={})
75
+ payload = { type: event_type, klazz: self.class.name, action: action, id: self.id, data: self,
76
+ user: Thread.current[:user] }.merge(extras)
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,20 @@
1
+ class PyrRules::RuleContext
2
+ def initialize(event_hash, extras = {})
3
+ event_hash.each do |key, value|
4
+ define_singleton_method(key.to_sym) {value}
5
+ end
6
+ extras.each do |key, value|
7
+ define_singleton_method(key.to_sym) {value}
8
+ end
9
+ define_singleton_method(:system) {:system_messaging_user}
10
+ target = event_hash[:klazz].constantize.find event_hash[:id].to_i rescue nil
11
+ actor = User.find event_hash[:user][:id].to_i rescue nil
12
+ actor ||= target.try(:user) rescue nil
13
+ define_singleton_method(:target) {target}
14
+ define_singleton_method(:actor) {actor}
15
+ end
16
+ def get_binding
17
+ return binding
18
+ end
19
+ end
20
+
@@ -0,0 +1,87 @@
1
+ module PyrRules
2
+ class RulesConfig
3
+
4
+ @@action_type_handlers ||= {}
5
+
6
+ def self.events_config
7
+ EventConfigLoader.events_dictionary
8
+ end
9
+
10
+ def self.event_config(event_name)
11
+ ec = (events_config[:model_events][event_name] || events_config[:controller_events][event_name])
12
+ unless ec
13
+ event_name = event_name.match(/(.*)::.*/)[1] #
14
+ ec = (events_config[:model_events][event_name] || events_config[:controller_events][event_name])
15
+ end
16
+ ec
17
+ end
18
+
19
+ def self.events
20
+ EventConfigLoader.events_list
21
+ end
22
+
23
+ # A very nice hierarchical Hash of all the events
24
+ def self.events_tree
25
+ tree={}
26
+ events_config.keys.sort.each do |event_type|
27
+ events_config[event_type].keys.sort.each do |class_name|
28
+ insert_here = (tree[event_type] ||= {})
29
+ parts = class_name.split("::")
30
+ parts[0...-1].each do |p|
31
+ insert_here = (insert_here[p] ||= {})
32
+ end
33
+ insert_here[parts.last] = events_config[event_type][class_name]
34
+ end
35
+ end
36
+ tree
37
+ end
38
+
39
+ def self.add_rule(rule_hash)
40
+ puts "\n\nAdding rule #{rule_hash}"
41
+ event_inclusion = rule_hash.delete(:event_inclusion) rescue nil
42
+ event_exclusion = rule_hash.delete(:event_exclusion) rescue nil
43
+ rule = PyrRules::Rule.where(:name => rule_hash[:name]).first
44
+ puts " Updating existing rule..." if rule
45
+ action_configs = rule_hash.delete(:action_configs)
46
+ rule ||= PyrRules::Rule.create rule_hash
47
+ # Match regexp for inclusions
48
+ events_to_add = EventConfigLoader.events_list.select{|x| x if x.match event_inclusion} if event_inclusion
49
+ # Match regexp for exclusions
50
+ events_to_restrict = EventConfigLoader.events_list.select{|x| x if x.match event_exclusion} if event_exclusion
51
+ events_to_add = (events_to_add - events_to_restrict) if events_to_restrict
52
+ rule.events = events_to_add
53
+ puts " Mapped to events: #{rule.events}"
54
+ action_configs.each do |action_config|
55
+ existing_action = rule.actions.detect{|action| action.title == action_config[:title] rescue false}
56
+ rule.actions.create!(action_config) unless existing_action
57
+ end
58
+ rule.set_default_mappings
59
+ rule.save!
60
+ puts "Rule added\n"
61
+ end
62
+
63
+ def self.delete_rules
64
+ PyrRules::Rule.delete_all
65
+ end
66
+
67
+ # User friendly dump of the event configuration
68
+ def self.dump_events_config
69
+ events_config.each do |k,v|
70
+ puts "\n\n\n#{k.to_s.titleize}"
71
+ v.keys.sort.each do |class_name|
72
+ puts "\n #{class_name}"
73
+ puts " - actions"
74
+ v[class_name][:actions].each do |action|
75
+ puts " #{action}"
76
+ end
77
+ puts " - context"
78
+ v[class_name][:context].each do |col_name, col_config|
79
+ puts " #{col_name}(#{col_config[:type]})"
80
+ end
81
+ end
82
+ end
83
+ nil
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,38 @@
1
+ require_dependency 'pyr_rules/rule'
2
+
3
+ module PyrRules
4
+ class RulesEngine
5
+ def self.reload_configuration
6
+ events = PyrRules::EventConfigLoader.reload_events_dictionary
7
+ actions = PyrRules::ActionHandler.reload_configuration
8
+ {events: events, actions: actions}
9
+ end
10
+
11
+ def self.handle_event(event)
12
+ event = event_from_string(event) if event.class == String
13
+ # Get super event rules tooo?
14
+ rules = PyrRules::Rule.where(events: "#{event[:klazz]}::#{event[:action]}" ).all
15
+ rules.each do |rule|
16
+ begin
17
+ rule.process_rule(event)
18
+ rescue Exception => e
19
+ puts "\n\n ERROR ENCOUNTERED processing rule: #{rule.name} ==> #{e.message}"
20
+ puts e.backtrace
21
+ end
22
+ end
23
+ end
24
+
25
+
26
+ private
27
+ # Takes a hash and turns all hash keys into symbols. It does
28
+ # this recursively into nested hashes and arrays of hashes
29
+ def event_from_string(event_string)
30
+ event = JSON.parse(event_string)
31
+ HashWithIndifferentAccess.new(event) # Allow use of symbols or strings as keys
32
+ end
33
+
34
+ end
35
+
36
+
37
+ end
38
+
@@ -0,0 +1,6 @@
1
+ class PyrRules::WebRequestLogger < PyrRules::ActionHandler
2
+
3
+ def handle
4
+ super
5
+ end
6
+ end
@@ -0,0 +1,12 @@
1
+ <h3>Rule Actions</h3>
2
+
3
+ <div class="row">
4
+ <% @actions.each_with_index do |a,idx| %>
5
+
6
+ <%= render 'shared/action', action: a %>
7
+
8
+ <% if idx % 3 == 2 %>
9
+ </div><div class="row">
10
+ <% end %>
11
+ <% end %>
12
+ </div>
@@ -0,0 +1,8 @@
1
+ <%events = PyrRules::RulesConfig.events%>
2
+ <table>
3
+ <% events.each do |event| %>
4
+ <tr>
5
+ <td><% if event[:name] == params[:event_id] %><b><% end %><%= link_to event[:name], pyr_rules_event_rules_path(event_id: event[:name]) %><% if event[:name] == params[:event_id] %></b><% end%></td>
6
+ </tr>
7
+ <% end %>
8
+ </table>