icentris-rules 0.9

Sign up to get free protection for your applications and to get access to all the features.
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,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
@@ -0,0 +1,6 @@
1
+ class PyrRules::EmailActionHandler < PyrRules::ActionHandler
2
+ def handle
3
+ super
4
+ puts "This is in Pyr-Rules. It echos template ------- #{@action.template}"
5
+ end
6
+ end