icentris-rules 0.9

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 (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>