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