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,116 @@
|
|
1
|
+
class PyrRules::Action
|
2
|
+
include Mongoid::Document
|
3
|
+
|
4
|
+
@@context_mapping_equivalencies ||= {} # {:messaging_user => [:user, :tree_user, :contact] }
|
5
|
+
|
6
|
+
field :title, type: String
|
7
|
+
field :type, type: String # class name of handler
|
8
|
+
field :context_mapping, type: Hash, default: {} # Map of actionNeed => ruleContextField {"actor:user"=>"actor:user"}
|
9
|
+
field :template, type: Hash, default: {} # Map of templateName => templateValue { "email" => "hello {name}" }
|
10
|
+
|
11
|
+
embedded_in :rule, inverse_of: :actions
|
12
|
+
|
13
|
+
before_save :set_default_mappings
|
14
|
+
|
15
|
+
# An action is ready if all of it's needs are mapped
|
16
|
+
def ready?
|
17
|
+
needs.each do |field, type|
|
18
|
+
return false unless lookup_context_mapping(field,type)
|
19
|
+
end
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def needs
|
24
|
+
n = (type.constantize.configuration[:needs].dup rescue {}) || {}
|
25
|
+
class_lookup_cols = {}
|
26
|
+
n.each do |field,type|
|
27
|
+
if type == :class_lookup
|
28
|
+
klazz = self.lookup_context_mapping(field,:class_lookup)[0] rescue nil
|
29
|
+
class_lookup_cols.merge! PyrRules::Rule.get_columns(klazz.camelcase.constantize) if klazz
|
30
|
+
end
|
31
|
+
end
|
32
|
+
class_lookup_cols.each do |column, col_type|
|
33
|
+
n[column] = col_type
|
34
|
+
end
|
35
|
+
template_names.each do |name|
|
36
|
+
template_fields(name).each do |f|
|
37
|
+
n[f.to_sym] = :string
|
38
|
+
end
|
39
|
+
end
|
40
|
+
n
|
41
|
+
end
|
42
|
+
|
43
|
+
# What templates are defined for this Action's ActionHandler?
|
44
|
+
# This reads from template DSL inside of ActionHandler class
|
45
|
+
# Templates have to be defined through the DSL!!!
|
46
|
+
def template_configuration
|
47
|
+
(type.constantize.configuration[:templates] rescue {}) || {}
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
def template_names
|
52
|
+
template_configuration.keys rescue []
|
53
|
+
end
|
54
|
+
|
55
|
+
def template_body(template_name)
|
56
|
+
body = template[template_name.to_s] # First try to read from Mongo Document template Hash
|
57
|
+
body ||= template_configuration[template_name.to_sym] # Not found, use the default configuration from the ActionHandler DSL
|
58
|
+
end
|
59
|
+
|
60
|
+
# Extract the template fields for the provided template
|
61
|
+
#
|
62
|
+
# eg: body = "Hello {name}" - the template field is :name
|
63
|
+
#
|
64
|
+
def template_fields(template_name)
|
65
|
+
body = template_body(template_name)
|
66
|
+
body.scan(/\{([^\s]*)\}/).flatten rescue []
|
67
|
+
end
|
68
|
+
|
69
|
+
# returns an array [field,type]
|
70
|
+
def lookup_context_mapping(field, type = "string")
|
71
|
+
context_mapping["#{field}:#{type}"].split(":") rescue nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def set_default_mappings(rule_context = rule.context)
|
75
|
+
needs.each do |action_field, type|
|
76
|
+
puts "...Considering mapping for #{action_field}(#{type})"
|
77
|
+
rule_context.keys.each do |rule_field|
|
78
|
+
unless try_rule_mapping(rule_context,rule_field,rule_field,action_field,type)
|
79
|
+
try_rule_mapping(rule_context,rule_field,rule_field.split(".").last.to_sym,action_field,type)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# A mapping equivalency is used to indicate two types can be mapped to each other, even though
|
86
|
+
# they do not have the same exact name
|
87
|
+
def self.add_mapping_type_equivalency(target_type, equivalent_types)
|
88
|
+
@@context_mapping_equivalencies[target_type] = equivalent_types
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.check_mapping_type?(action_type, rule_type)
|
92
|
+
return true if action_type == rule_type || action_type == "string"
|
93
|
+
equivalencies = @@context_mapping_equivalencies[action_type.to_sym]
|
94
|
+
if equivalencies && equivalencies.index(rule_type.to_sym)
|
95
|
+
return true
|
96
|
+
end
|
97
|
+
false
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
private
|
102
|
+
def try_rule_mapping(rule_context,rule_field,rule_field_check,action_field,type)
|
103
|
+
if rule_field_check == action_field
|
104
|
+
rule_type = rule_context[rule_field]
|
105
|
+
if rule_type && rule_type.to_sym == type.to_sym
|
106
|
+
# The name and type matched - make the default entry
|
107
|
+
puts "...Adding default mapping for #{action_field}(#{type} ==> #{rule_field}:#{type}"
|
108
|
+
context_mapping["#{action_field}:#{type}"] = "#{rule_field}:#{type}"
|
109
|
+
return true
|
110
|
+
end
|
111
|
+
return false
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'pyr_rules/action'
|
2
|
+
|
3
|
+
|
4
|
+
class PyrRules::Rule
|
5
|
+
include Mongoid::Document
|
6
|
+
include Mongoid::Timestamps::Short # For c_at and u_at.
|
7
|
+
include Mongoid::Versioning
|
8
|
+
include PyrRules::ModelEventEmitter
|
9
|
+
|
10
|
+
before_save :set_updated_by
|
11
|
+
|
12
|
+
max_versions 25
|
13
|
+
|
14
|
+
field :name, type: String
|
15
|
+
field :description, type: String
|
16
|
+
field :events, type: Array, default: []
|
17
|
+
field :criteria, type: String
|
18
|
+
field :active, type: Boolean
|
19
|
+
field :updated_by, type: String
|
20
|
+
field :start_date, type: Time
|
21
|
+
field :end_date, type: Time
|
22
|
+
|
23
|
+
# These can programatically be used to populate the events
|
24
|
+
field :event_inclusion_matcher, type: Regexp
|
25
|
+
field :event_exclusion_matcher, type: Regexp
|
26
|
+
|
27
|
+
embeds_many :actions, inverse_of: :rule #cascade_callbacks: true
|
28
|
+
|
29
|
+
accepts_nested_attributes_for :actions
|
30
|
+
|
31
|
+
validates :name, presence: true
|
32
|
+
|
33
|
+
# validates_each :events do |record, attr, event_list|
|
34
|
+
# if event_list
|
35
|
+
# event_list.each do |e|
|
36
|
+
# record.errors.add(attr, 'should be a valid event') unless PyrRules::RulesConfig.event e
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
|
41
|
+
def ready?
|
42
|
+
return false unless actions
|
43
|
+
! actions.collect{|a| a.ready?}.index false rescue false
|
44
|
+
end
|
45
|
+
|
46
|
+
def process_rule(event)
|
47
|
+
return unless check_criteria event
|
48
|
+
actions.each do |action|
|
49
|
+
begin
|
50
|
+
action_handler = action.type
|
51
|
+
klass = action_handler if action_handler.is_a? Class
|
52
|
+
#Module.const_get(action_handler) doesn't work for nested classes
|
53
|
+
#We can use String.constantize (ActiveSupport extension for string)
|
54
|
+
#or eval works for sure.
|
55
|
+
klass = action_handler.constantize rescue nil if action_handler.is_a? String
|
56
|
+
klass.new(event, action).handle if klass
|
57
|
+
rescue Exception => e
|
58
|
+
puts "\nERROR: Running action #{action.type} for rule #{name}: #{e.message}\n"
|
59
|
+
puts e.backtrace
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
# Determine overall Rule-scoped context via intersection all event-scoped contexts
|
66
|
+
def context
|
67
|
+
rc = {}
|
68
|
+
return rc if events == nil || events.size == 0
|
69
|
+
events.each_with_index do |e,i|
|
70
|
+
ec = PyrRules::RulesConfig.event_config(e)[:context] rescue {}
|
71
|
+
if i == 0
|
72
|
+
rc.merge!(ec) # start with the first event's context
|
73
|
+
else
|
74
|
+
rc.each do |field, metadata|
|
75
|
+
if ec[field] == nil
|
76
|
+
rc.delete field # this event did not have the field, it cannot be in our interesected context
|
77
|
+
elsif ec[field] != rc[field]
|
78
|
+
rc.delete field # this event had the field, but it was a different type, it cannot be in our intersected context
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
HashWithIndifferentAccess.new(rc)
|
84
|
+
end
|
85
|
+
|
86
|
+
def context_field_mapped?(field,type)
|
87
|
+
lookup = "#{field}:#{type}"
|
88
|
+
actions.each do |a|
|
89
|
+
return true if a.context_mapping.values.index lookup rescue false
|
90
|
+
end
|
91
|
+
false
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
def set_default_mappings
|
96
|
+
puts "Setting default action mappings"
|
97
|
+
rule_context = context # {:actor=>:user, :target=>:object, etc...}
|
98
|
+
actions.each do |a|
|
99
|
+
a.set_default_mappings(rule_context)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.rule_context(rule_event, extra_fields = {})
|
104
|
+
PyrRules::RuleContext.new rule_event, extra_fields
|
105
|
+
end
|
106
|
+
|
107
|
+
def check_criteria(rule_event)
|
108
|
+
return false unless events && events.index("#{rule_event[:klazz]}::#{rule_event[:action]}")
|
109
|
+
return true if criteria.blank?
|
110
|
+
result = eval criteria, PyrRules::Rule.rule_context(rule_event).get_binding
|
111
|
+
result
|
112
|
+
end
|
113
|
+
|
114
|
+
# called like : lookup_path("target")
|
115
|
+
# or : lookup_path("target.user")
|
116
|
+
def lookup_path(path, sub_context = nil)
|
117
|
+
return context if (path.blank? || path == "/") && !sub_context # Default for retrieving root context path
|
118
|
+
# path = "target.user" parts[0]="target" parts[1]="user"
|
119
|
+
parts = path.split(".")
|
120
|
+
if sub_context.nil?
|
121
|
+
if parts.first == "actor"
|
122
|
+
parts.shift
|
123
|
+
path=parts.join(".")
|
124
|
+
klazz = User
|
125
|
+
return lookup_path(path, User) unless path.blank?
|
126
|
+
elsif parts.first == "target"
|
127
|
+
parts.shift
|
128
|
+
path=parts.join(".")
|
129
|
+
cols = nil
|
130
|
+
# We need to get all the possible path values from all the input events
|
131
|
+
events.each do |event|
|
132
|
+
klazz = (event.split("::")[0...-1].join("::")).constantize
|
133
|
+
puts klazz
|
134
|
+
sub_cols = lookup_path(path, klazz)
|
135
|
+
puts sub_cols
|
136
|
+
cols ||= sub_cols
|
137
|
+
# This will perform an intersection across all the event fields
|
138
|
+
cols.each do |k,v|
|
139
|
+
unless sub_cols[k]==v
|
140
|
+
puts "Removing due to intersection #{k}"
|
141
|
+
cols.delete k
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
puts "Returning"
|
146
|
+
puts cols
|
147
|
+
return cols
|
148
|
+
end
|
149
|
+
end
|
150
|
+
klazz ||= sub_context || self
|
151
|
+
|
152
|
+
cols = {}
|
153
|
+
if parts.length == 0
|
154
|
+
cols = get_columns(klazz)
|
155
|
+
else
|
156
|
+
puts "DBH: What is this use case? I can't remember why I coded it?"
|
157
|
+
if klazz.respond_to? :reflect_on_all_associations
|
158
|
+
(klazz.reflect_on_all_associations(:has_one) +
|
159
|
+
klazz.reflect_on_all_associations(:belongs_to)).each do |assoc|
|
160
|
+
if (assoc.name.to_s == parts.first)
|
161
|
+
parts.shift
|
162
|
+
path=parts.join(".")
|
163
|
+
klazz = assoc.klass
|
164
|
+
return lookup_path(path, klazz)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
cols
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.get_columns(klazz)
|
173
|
+
cols = {}
|
174
|
+
if klazz.respond_to? :reflect_on_all_associations
|
175
|
+
(klazz.reflect_on_all_associations(:has_one) +
|
176
|
+
klazz.reflect_on_all_associations(:belongs_to)).each do |assoc|
|
177
|
+
cols[assoc.name] = assoc.name.to_sym
|
178
|
+
end
|
179
|
+
end
|
180
|
+
if klazz.respond_to? :columns # ActiveRecord
|
181
|
+
klazz.columns.each{|c| cols[c.name] = c.type.to_sym }
|
182
|
+
end
|
183
|
+
if klazz.respond_to? :fields # Mongoid
|
184
|
+
fields.each{|k,v| cols[v.name] = v.options[:type].name.downcase.to_sym }
|
185
|
+
end
|
186
|
+
cols
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
def set_updated_by
|
191
|
+
if Thread.current[:user]
|
192
|
+
self.updated_by = Thread.current[:user].try(:username)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
|
199
|
+
|
200
|
+
|
201
|
+
|
@@ -0,0 +1,94 @@
|
|
1
|
+
class PyrRules::ActionHandler
|
2
|
+
#
|
3
|
+
# needs is a map of required fields for each and every type of ActionHandler
|
4
|
+
#
|
5
|
+
@@needs ||= {}
|
6
|
+
@@action_configuration ||= {}
|
7
|
+
|
8
|
+
@@templates ||= {}
|
9
|
+
|
10
|
+
def self.needs(name, type=:string)
|
11
|
+
@@needs[self] ||= {}
|
12
|
+
@@needs[self][name] = type
|
13
|
+
end
|
14
|
+
|
15
|
+
# def self.needs_map
|
16
|
+
# @@needs[self]
|
17
|
+
# end
|
18
|
+
|
19
|
+
def self.template(name)
|
20
|
+
@@templates[self] ||= {}
|
21
|
+
@@templates[self][name] = (block_given?) ? yield : ""
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.reload_configuration
|
25
|
+
puts "Reloading ActionHandler configuration"
|
26
|
+
@@action_configuration = {}
|
27
|
+
configuration
|
28
|
+
@@action_configuration.keys
|
29
|
+
end
|
30
|
+
|
31
|
+
# def self.templates
|
32
|
+
# @@templates[self]
|
33
|
+
# end
|
34
|
+
def self.configuration
|
35
|
+
return @@action_configuration[self] unless @@action_configuration.empty?
|
36
|
+
#if Rails.env.development?
|
37
|
+
Dir["#{Rails.root}/../**/*/app/**/action_handler/*.rb"].each do |p|
|
38
|
+
puts "require_dependency #{p}"
|
39
|
+
require_dependency p
|
40
|
+
end
|
41
|
+
#end
|
42
|
+
_add_needs_and_recurse(PyrRules::ActionHandler)
|
43
|
+
@@action_configuration[self]
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.action_listing
|
47
|
+
configuration # force load
|
48
|
+
@@action_configuration.keys
|
49
|
+
end
|
50
|
+
|
51
|
+
def self._add_needs_and_recurse(klazz)
|
52
|
+
klazz.subclasses.each do |c|
|
53
|
+
@@action_configuration[c] ||= {}
|
54
|
+
@@action_configuration[c][:needs] = @@needs[c]
|
55
|
+
@@action_configuration[c][:templates] = @@templates[c]
|
56
|
+
_add_needs_and_recurse(c)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Our context methods will be accessible through here
|
61
|
+
def method_missing(meth, *args, &block)
|
62
|
+
mapping_type = @action.needs[meth]
|
63
|
+
if mapping_type
|
64
|
+
rule_field = @action.lookup_context_mapping(meth, mapping_type)[0]
|
65
|
+
value = eval rule_field, @context.get_binding rescue nil
|
66
|
+
return value
|
67
|
+
end
|
68
|
+
super
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
attr_accessor :event, :action
|
73
|
+
def initialize(event, action)
|
74
|
+
@event = event
|
75
|
+
@action = action # Allow use of symbols or strings as keys target = event_hash[:klazz].constantize.find event_hash[:id].to_i rescue nil
|
76
|
+
@context = PyrRules::Rule.rule_context(@event)
|
77
|
+
end
|
78
|
+
|
79
|
+
def handle
|
80
|
+
puts "\n\n------- #{self.class.name} ------ \n\nHANDLE called for #{@event.inspect} \n\n---- #{@action.inspect}\n\n"
|
81
|
+
end
|
82
|
+
|
83
|
+
def eval_template(template_name)
|
84
|
+
template_name = template_name.to_s
|
85
|
+
puts "DEBUG: Looking up #{template_name} from #{@action.template}"
|
86
|
+
t = @action.template_body(template_name)
|
87
|
+
interpolated_fields = @action.template_fields(template_name)
|
88
|
+
interpolated_fields.each do |f|
|
89
|
+
t.gsub!("{#{f}}","\#{#{f}}")
|
90
|
+
end
|
91
|
+
puts "DEBUG: Using #{t}"
|
92
|
+
eval '"' + t + '"'
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module PyrRules
|
2
|
+
module ControllerEventEmitter
|
3
|
+
|
4
|
+
@@registered_classes = []
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
#base.send :extend, ClassMethods
|
8
|
+
#unless ( File.basename($0) == "rake" && ARGV.include?("db:migrate") )
|
9
|
+
#end
|
10
|
+
base.around_filter :raise_controller_event
|
11
|
+
@@registered_classes << base if !@@registered_classes.index(base)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.registered_classes
|
15
|
+
@@registered_classes
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def raise_controller_event(extras = {})
|
20
|
+
action = params[:action]
|
21
|
+
payload = build_event_payload("ControllerEvent", self.class.name, action, extras)
|
22
|
+
ActiveSupport::Notifications.instrument("PyrRules_#{self.class.name}_#{action}", payload ) do
|
23
|
+
yield
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_event_payload(event_type, class_name, action, extras={})
|
28
|
+
payload = { type: event_type, klazz: self.class.name, action: action, data: params,
|
29
|
+
user: Thread.current[:user] }.merge(extras)
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|