icentris-rules 0.9
Sign up to get free protection for your applications and to get access to all the features.
- 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>
|