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.
- checksums.yaml +15 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +33 -0
- data/app/assets/images/pyr_rules/ui-anim_basic_16x16.gif +0 -0
- data/app/assets/javascripts/pyr_rules/rules.js +135 -0
- data/app/assets/stylesheets/pyr_rules/rules.css.scss +258 -0
- data/app/assets/stylesheets/scaffold.css +57 -0
- data/app/controllers/pyr_rules/actions_controller.rb +22 -0
- data/app/controllers/pyr_rules/events_controller.rb +17 -0
- data/app/controllers/pyr_rules/rules_controller.rb +233 -0
- data/app/helpers/pyr_rules/actions_helper.rb +2 -0
- data/app/helpers/pyr_rules/events_helper.rb +95 -0
- data/app/helpers/pyr_rules/rules_helper.rb +10 -0
- data/app/models/pyr_rules.rb +3 -0
- data/app/models/pyr_rules/action.rb +116 -0
- data/app/models/pyr_rules/rule.rb +201 -0
- data/app/rules/pyr_rules/action_handler.rb +94 -0
- data/app/rules/pyr_rules/controller_event_emitter.rb +34 -0
- data/app/rules/pyr_rules/email_action_handler.rb +6 -0
- data/app/rules/pyr_rules/event_concerns.rb +23 -0
- data/app/rules/pyr_rules/event_config_loader.rb +97 -0
- data/app/rules/pyr_rules/event_subscriber.rb +36 -0
- data/app/rules/pyr_rules/model_creation_handler.rb +25 -0
- data/app/rules/pyr_rules/model_event_emitter.rb +80 -0
- data/app/rules/pyr_rules/rule_context.rb +20 -0
- data/app/rules/pyr_rules/rules_config.rb +87 -0
- data/app/rules/pyr_rules/rules_engine.rb +38 -0
- data/app/rules/pyr_rules/web_request_logger.rb +6 -0
- data/app/views/pyr_rules/actions/index.html.erb +12 -0
- data/app/views/pyr_rules/events/_sidebar.html.erb +8 -0
- data/app/views/pyr_rules/events/index.html.erb +72 -0
- data/app/views/pyr_rules/events/show.html.erb +11 -0
- data/app/views/pyr_rules/rules/_form.html.erb +28 -0
- data/app/views/pyr_rules/rules/_ready_headers.js.erb +11 -0
- data/app/views/pyr_rules/rules/_rule_context_field.html.erb +10 -0
- data/app/views/pyr_rules/rules/_rules.html.erb +34 -0
- data/app/views/pyr_rules/rules/add_action_mapping.js.erb +13 -0
- data/app/views/pyr_rules/rules/edit.html.erb +14 -0
- data/app/views/pyr_rules/rules/index.html.erb +5 -0
- data/app/views/pyr_rules/rules/lookup_sub_properties.js.erb +13 -0
- data/app/views/pyr_rules/rules/new.html.erb +11 -0
- data/app/views/pyr_rules/rules/remove_action_mapping.js.erb +15 -0
- data/app/views/pyr_rules/rules/show.html.erb +98 -0
- data/app/views/shared/_action.html.erb +41 -0
- data/app/views/shared/_action_field_mapping.html.erb +33 -0
- data/app/views/shared/_event.html.erb +21 -0
- data/app/views/shared/_rule.html.erb +27 -0
- data/config/routes.rb +32 -0
- data/config/security.yml +9 -0
- data/db/seeds.rb +1 -0
- data/lib/pyr_rules.rb +34 -0
- data/lib/pyr_rules/engine.rb +72 -0
- data/lib/pyr_rules/generators/dummy_generator.rb +27 -0
- data/lib/pyr_rules/version.rb +3 -0
- data/lib/tasks/pyr_rules_tasks.rake +57 -0
- 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,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>
|