Passage à RuleSetProcessor

This commit is contained in:
Jérémy Lecour 2021-01-25 14:32:26 +01:00 committed by Jérémy Lecour
parent 61cb650014
commit 518e94bed7
7 changed files with 156 additions and 136 deletions

View file

@ -2,11 +2,11 @@ class InMailbox < ApplicationMailbox
def process
email_importer = EmailImporter.new
repository = EmailRepository.new
rules_evaluator = RulesEvaluator.new
email = email_importer.import(mail)
email = rules_evaluator.evaluate(email, RuleSet.enabled)
processor = RuleSetProcessor.new(email: email)
email = processor.process_all(RuleSet.enabled)
repository.save(email)
end

View file

@ -10,6 +10,7 @@ module EmailAction
def process(email)
fail NotImplementedError
end
end
end

View file

@ -0,0 +1,137 @@
class RuleSetProcessor
class InvalidRule < ::ArgumentError
end
def process_all(rule_sets, email)
rule_sets.each { |rule_set|
email = process(rule_set, email)
}
email
end
def process(rule_set, email)
return email unless rule_set.enabled?
if evaluate_rules(rule_set, email)
email = execute_actions(rule_set.actions, email)
end
email
end
def evaluate_rules(rule_set, email)
rule_set_result = true
rule_set_result = catch(:done) {
rule_set.rules.each do |rule|
next unless rule.enabled?
subjects = prepare_subjects(rule, email)
rule_result = apply_rule(subjects, rule)
rule_result = !rule_result if rule.inverted?
rule_set_result = apply_operator(rule_set_result, rule_set.operator, rule_result)
rescue InvalidRule => ex
Rails.logger.error "Skipped rule##{rule.id} '#{rule.name}' - #{ex.inspect}"
next
end
}
rule_set_result
end
def execute_actions(actions, email)
actions.each do |action|
next unless action.enabled?
klass = action.class_name.constantize
email_action = klass.new
email = email_action.process(email)
rescue NameError => ex
Rails.logger.error "Skipped action##{action.id} '#{action.name}' - #{ex.inspect}"
raise InvalidRule, ex.message
end
email
end
def prepare_subjects(rule, email)
case rule.subject_type.downcase
when "header"
Array(email.header_values(rule.subject_value))
when "body"
Array(email.plain_body)
else
raise InvalidRule, "Unrecognized subject type '#{rule.subject_type}'"
end
end
private
def apply_rule(subjects, rule)
case rule.condition_type.downcase
when "match", "matches"
subjects.any? { |subject|
pattern = Regexp.new(rule.condition_value)
subject.match?
}
when "equal", "equals"
subjects.any? { |subject|
subject == rule.condition_value
}
when "start", "starts"
subjects.any? { |subject|
subject.starts_with? rule.condition_value
}
when "end", "ends"
subjects.any? { |subject|
subject.ends_with? rule.condition_value
}
when "contain", "contains"
subjects.any? { |subject|
subject.include? rule.condition_value
}
when "exist", "exists"
subjects.any? { |subject|
subject.exists?
}
when "empty"
subjects.all? { |subject|
subject.empty?
}
when "date_before"
# subjects.all? { |subject|
# subject.empty?
# }
when "date_after"
# subjects.all? { |subject|
# subject.empty?
# }
else
raise InvalidRule, "Unrecognized condition type '#{rule. condition_type}'"
end
end
def apply_operator(state, operator, result)
case operator.upcase
when "AND"
if result
(state and result)
else
throw :done, false
end
when "OR"
if result
throw :done, true
else
(state or result)
end
# when "XOR"
# (state or result) and !(rules_state and result)
else
raise InvalidRule, "Unrecognized operator '#{operator}'"
end
end
end

View file

@ -1,110 +0,0 @@
class RulesEvaluator
def evaluate(email, rule_set_or_sets)
if rule_set_or_sets.respond_to? :each
rule_set_or_sets.each do |rule_set|
email = evaluate_rule_set(email, rule_set)
end
else
email = evaluate_rule_set(email, rule_set_or_sets)
end
email
end
private
def evaluate_rule_set(email, rule_set)
rules_state = true
rule_set.rules.enabled.each do |rule|
subjects = prepare_subjects(email, rule)
result = apply_rule(subjects, rule)
result = ! result if rule.inverted?
rules_state = apply_operator(rules_state, rule_set.operator, result)
break unless rules_state
end
if rules_state
rule_set.actions.enabled.each do |action|
klass = action.class_name.constantize
email = klass.new.process(email)
# rescue NameError => ex
# # TODO: log a warning
end
end
email
end
def prepare_subjects(email, rule)
case rule.subject_type.downcase
when "header"
Array(email.header_values(rule.subject_value))
when "body"
Array(email.plain_body)
else
raise ArgumentError, "Unrecognized subject type '#{rule.subject_type}'"
end
end
def apply_rule(subjects, rule)
case rule.condition_type.downcase
when "match", "matches"
subjects.any? { |subject|
pattern = Regexp.new(rule.condition_value)
subject.match?
}
when "equal", "equals"
subjects.any? { |subject|
subject == rule.condition_value
}
when "start", "starts"
subjects.any? { |subject|
subject.starts_with? rule.condition_value
}
when "end", "ends"
subjects.any? { |subject|
subject.ends_with? rule.condition_value
}
when "contain", "contains"
subjects.any? { |subject|
subject.include? rule.condition_value
}
when "exist", "exists"
subjects.any? { |subject|
subject.exists?
}
when "empty"
subjects.all? { |subject|
subject.empty?
}
when "date_before"
# subjects.all? { |subject|
# subject.empty?
# }
when "date_after"
# subjects.all? { |subject|
# subject.empty?
# }
else
raise ArgumentError, "Unrecognized condition type '#{rule. condition_type}'"
end
def apply_operator(state, operator, result)
case operator.upcase
when "AND"
(state and result)
when "OR"
(state or result)
when "XOR"
(state or result) and !(rules_state and result)
else
raise ArgumentError, "Unrecognized operator '#{operator}'"
end
end
end
end

View file

@ -23,19 +23,19 @@ or_rules:
invalid_subject:
name: Rules with an invalid rule subject
enabled: false
enabled: true
operator: "AND"
inverted: false
invalid_condition_type:
name: Rules with an invalid rule condition type
enabled: false
enabled: true
operator: "AND"
inverted: false
invalid_operator:
name: Rules with an invalid rule operator
enabled: false
enabled: true
operator: "FOO"
inverted: false

View file

@ -1,6 +1,6 @@
require 'test_helper'
class RulesEvaluatorTest < ActiveSupport::TestCase
class RuleSetProcessorTest < ActiveSupport::TestCase
test "mark cron from subject" do
email = email_from_eml_with_rules("cron_subject.eml")
@ -57,33 +57,27 @@ class RulesEvaluatorTest < ActiveSupport::TestCase
end
test "invalid subject type" do
rules_evaluator = RulesEvaluator.new
email = Email.new
processor = RuleSetProcessor.new
email = processor.process(rule_sets(:invalid_subject), email)
exception = assert_raise ArgumentError do
rules_evaluator.evaluate(email, rule_sets(:invalid_subject))
end
assert_match(/^Unrecognized subject type/, exception.message)
assert_not_predicate email, :changed?
end
test "invalid condition type" do
rules_evaluator = RulesEvaluator.new
email = Email.new
processor = RuleSetProcessor.new
email = processor.process(rule_sets(:invalid_condition_type), email)
exception = assert_raise ArgumentError do
rules_evaluator.evaluate(email, rule_sets(:invalid_condition_type))
end
assert_match(/^Unrecognized condition type/, exception.message)
assert_not_predicate email, :changed?
end
test "invalid operator" do
rules_evaluator = RulesEvaluator.new
email = Email.new
processor = RuleSetProcessor.new
email = processor.process(rule_sets(:invalid_operator), email)
exception = assert_raise ArgumentError do
rules_evaluator.evaluate(email, rule_sets(:invalid_operator))
end
assert_match(/^Unrecognized operator/, exception.message)
assert_not_predicate email, :changed?
end
end

View file

@ -4,7 +4,7 @@ require 'rails/test_help'
class ActiveSupport::TestCase
# Run tests in parallel with specified workers
parallelize(workers: :number_of_processors)
# parallelize(workers: :number_of_processors)
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
@ -21,10 +21,8 @@ class ActiveSupport::TestCase
end
def email_from_eml_with_rules(file_fixture_name)
email = email_from_eml(file_fixture_name)
rules_evaluator = RulesEvaluator.new
email = rules_evaluator.evaluate(email, RuleSet.enabled)
email
processor = RuleSetProcessor.new
email = processor.process_all(RuleSet.enabled, email)
end
def assert_no_html(text)