Passage à RuleSetProcessor
This commit is contained in:
parent
61cb650014
commit
518e94bed7
|
@ -2,11 +2,11 @@ class InMailbox < ApplicationMailbox
|
||||||
def process
|
def process
|
||||||
email_importer = EmailImporter.new
|
email_importer = EmailImporter.new
|
||||||
repository = EmailRepository.new
|
repository = EmailRepository.new
|
||||||
rules_evaluator = RulesEvaluator.new
|
|
||||||
|
|
||||||
email = email_importer.import(mail)
|
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)
|
repository.save(email)
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,7 @@ module EmailAction
|
||||||
def process(email)
|
def process(email)
|
||||||
fail NotImplementedError
|
fail NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
137
app/services/rule_set_processor.rb
Normal file
137
app/services/rule_set_processor.rb
Normal 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
|
|
@ -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
|
|
6
test/fixtures/rule_sets.yml
vendored
6
test/fixtures/rule_sets.yml
vendored
|
@ -23,19 +23,19 @@ or_rules:
|
||||||
|
|
||||||
invalid_subject:
|
invalid_subject:
|
||||||
name: Rules with an invalid rule subject
|
name: Rules with an invalid rule subject
|
||||||
enabled: false
|
enabled: true
|
||||||
operator: "AND"
|
operator: "AND"
|
||||||
inverted: false
|
inverted: false
|
||||||
|
|
||||||
invalid_condition_type:
|
invalid_condition_type:
|
||||||
name: Rules with an invalid rule condition type
|
name: Rules with an invalid rule condition type
|
||||||
enabled: false
|
enabled: true
|
||||||
operator: "AND"
|
operator: "AND"
|
||||||
inverted: false
|
inverted: false
|
||||||
|
|
||||||
invalid_operator:
|
invalid_operator:
|
||||||
name: Rules with an invalid rule operator
|
name: Rules with an invalid rule operator
|
||||||
enabled: false
|
enabled: true
|
||||||
operator: "FOO"
|
operator: "FOO"
|
||||||
inverted: false
|
inverted: false
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
require 'test_helper'
|
require 'test_helper'
|
||||||
|
|
||||||
class RulesEvaluatorTest < ActiveSupport::TestCase
|
class RuleSetProcessorTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
test "mark cron from subject" do
|
test "mark cron from subject" do
|
||||||
email = email_from_eml_with_rules("cron_subject.eml")
|
email = email_from_eml_with_rules("cron_subject.eml")
|
||||||
|
@ -57,33 +57,27 @@ class RulesEvaluatorTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "invalid subject type" do
|
test "invalid subject type" do
|
||||||
rules_evaluator = RulesEvaluator.new
|
|
||||||
email = Email.new
|
email = Email.new
|
||||||
|
processor = RuleSetProcessor.new
|
||||||
|
email = processor.process(rule_sets(:invalid_subject), email)
|
||||||
|
|
||||||
exception = assert_raise ArgumentError do
|
assert_not_predicate email, :changed?
|
||||||
rules_evaluator.evaluate(email, rule_sets(:invalid_subject))
|
|
||||||
end
|
|
||||||
assert_match(/^Unrecognized subject type/, exception.message)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "invalid condition type" do
|
test "invalid condition type" do
|
||||||
rules_evaluator = RulesEvaluator.new
|
|
||||||
email = Email.new
|
email = Email.new
|
||||||
|
processor = RuleSetProcessor.new
|
||||||
|
email = processor.process(rule_sets(:invalid_condition_type), email)
|
||||||
|
|
||||||
exception = assert_raise ArgumentError do
|
assert_not_predicate email, :changed?
|
||||||
rules_evaluator.evaluate(email, rule_sets(:invalid_condition_type))
|
|
||||||
end
|
|
||||||
assert_match(/^Unrecognized condition type/, exception.message)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "invalid operator" do
|
test "invalid operator" do
|
||||||
rules_evaluator = RulesEvaluator.new
|
|
||||||
email = Email.new
|
email = Email.new
|
||||||
|
processor = RuleSetProcessor.new
|
||||||
|
email = processor.process(rule_sets(:invalid_operator), email)
|
||||||
|
|
||||||
exception = assert_raise ArgumentError do
|
assert_not_predicate email, :changed?
|
||||||
rules_evaluator.evaluate(email, rule_sets(:invalid_operator))
|
|
||||||
end
|
|
||||||
assert_match(/^Unrecognized operator/, exception.message)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
|
@ -4,7 +4,7 @@ require 'rails/test_help'
|
||||||
|
|
||||||
class ActiveSupport::TestCase
|
class ActiveSupport::TestCase
|
||||||
# Run tests in parallel with specified workers
|
# 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.
|
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
|
||||||
fixtures :all
|
fixtures :all
|
||||||
|
@ -21,10 +21,8 @@ class ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
def email_from_eml_with_rules(file_fixture_name)
|
def email_from_eml_with_rules(file_fixture_name)
|
||||||
email = email_from_eml(file_fixture_name)
|
email = email_from_eml(file_fixture_name)
|
||||||
rules_evaluator = RulesEvaluator.new
|
processor = RuleSetProcessor.new
|
||||||
email = rules_evaluator.evaluate(email, RuleSet.enabled)
|
email = processor.process_all(RuleSet.enabled, email)
|
||||||
|
|
||||||
email
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def assert_no_html(text)
|
def assert_no_html(text)
|
||||||
|
|
Loading…
Reference in a new issue