Class: Torm::RulesEngine

Inherits:
Object
  • Object
show all
Defined in:
lib/torm/rules_engine.rb

Defined Under Namespace

Classes: RuleVariationHelper

Constant Summary collapse

DEFAULT_POLICIES =

Policies (priorities) in order of important -> least important.

[:law, :coc, :experiment, :default].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(rules: {}, dirty: false, policies: DEFAULT_POLICIES.dup, rules_file: Torm.default_rules_file) ⇒ RulesEngine

Returns a new instance of RulesEngine.



10
11
12
13
14
15
16
# File 'lib/torm/rules_engine.rb', line 10

def initialize(rules: {}, dirty: false, policies: DEFAULT_POLICIES.dup, rules_file: Torm.default_rules_file)
  @rules                = rules
  @dirty                = dirty
  @policies             = policies
  @rules_file           = rules_file
  @conditions_whitelist = {}
end

Instance Attribute Details

#conditions_whitelistObject (readonly)

Returns the value of attribute conditions_whitelist.



6
7
8
# File 'lib/torm/rules_engine.rb', line 6

def conditions_whitelist
  @conditions_whitelist
end

#dirtyObject

Returns the value of attribute dirty.



8
9
10
# File 'lib/torm/rules_engine.rb', line 8

def dirty
  @dirty
end

#policiesObject

Returns the value of attribute policies.



7
8
9
# File 'lib/torm/rules_engine.rb', line 7

def policies
  @policies
end

#rulesObject (readonly)

Returns the value of attribute rules.



6
7
8
# File 'lib/torm/rules_engine.rb', line 6

def rules
  @rules
end

#rules_fileObject

Returns the value of attribute rules_file.



8
9
10
# File 'lib/torm/rules_engine.rb', line 8

def rules_file
  @rules_file
end

Class Method Details

.from_json(json) ⇒ Object

Load an engine from JSON. This means we can export rules engines across systems: store rules in 1 place, run them ‘everywhere’ at native speed. Due to the high number of symbols we use, we have to convert the JSON string data for each rule on import. Good thing: we should only have to do this once on boot.



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/torm/rules_engine.rb', line 126

def self.from_json(json)
  dump   = MultiJson.load(json)
  data   = {
    policies: dump['policies'].map(&:to_sym),
  }
  engine = new(**data)
  dump['rules'].each do |name, rules|
    rules.each do |rule|
      value      = rule['value']
      value      = Torm.symbolize_keys(value) if Hash === value
      policy     = rule['policy'].to_sym
      conditions = Torm.symbolize_keys(rule['conditions'])
      engine.add_rule(name, value, policy, conditions)
    end
  end
  engine.dirty = false
  engine
end

.load(rules_file: Torm.default_rules_file) ⇒ Torm::RulesEngine

Load rules from a file and create a new engine for it. Note: this does not replace the Torm::RulesEngine.instance, you have to do this yourself if required.

Returns:



149
150
151
152
153
154
155
156
157
158
# File 'lib/torm/rules_engine.rb', line 149

def self.load(rules_file: Torm.default_rules_file)
  if File.exist?(rules_file)
    json              = File.read(rules_file)
    engine            = self.from_json(json)
    engine.rules_file = rules_file
    engine
  else
    nil
  end
end

Instance Method Details

#add_rule(name, value, policy, conditions = {}) ⇒ Torm::RulesEngine

Add a new rule. Will mark the engine as dirty when a rules was added.

Parameters:

  • name (String)
  • value (true, false, String, Numeric, Range, Hash)

    Either a simple type, or a Range, or a Hash with a :minimum or :maximum key to represent a Range extreme.

  • policy (Symbol)

    The source of the rule and thus how heavy it weighs.

  • conditions (Hash) (defaults to: {})

    Conditions that must be met before a rule evaluates to return this value.

Returns:



33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/torm/rules_engine.rb', line 33

def add_rule(name, value, policy, conditions={})
  raise "Illegal policy: #{policy.inspect}, must be one of: #{policies.inspect}" unless policies.include?(policy)
  rules_array = rules_for(name)
  value       = { minimum: value.min, maximum: value.max } if Range === value
  new_rule    = { value: value.freeze, policy: policy, conditions: conditions.freeze }.freeze
  unless rules_array.include?(new_rule)
    rules_array << new_rule
    # Sort rules so that the highest policy level is sorted first and then the most complex rule before the more general ones
    rules_array.sort_by! { |rule| [policies.index(rule[:policy]), -rule[:conditions].size] }
    conditions_whitelist_for(name).merge conditions.keys
    @dirty = true
  end
  self
end

#add_rules(name, value, policy) {|Torm::RulesEngine::RuleVariationHelper| ... } ⇒ Torm::RulesEngine

Add multiple rules via the block syntax:

Examples:


engine = Torm::RulesEngine.new
engine.add_rules 'Happy', true, :default do |rule|
  rule.variant false, :default, rain: true
end

Parameters:

  • name (String)
  • value (true, false, String, Numeric, Range, Hash)

    Either a simple type, or a Range, or a Hash with a :minimum or :maximum key to represent a Range extreme.

  • policy (Symbol)

    The source of the rule and thus how heavy it weighs.

Yields:

Returns:



85
86
87
88
89
90
91
92
93
# File 'lib/torm/rules_engine.rb', line 85

def add_rules(name, value, policy)
  # Add the default rule
  add_rule(name, value, policy)

  rule_variation = RuleVariationHelper.new(self, name)
  yield rule_variation if block_given?

  self
end

#as_hashHash

Return a hash with all rules and policies, useful for serialisation.

Returns:

  • (Hash)


109
110
111
112
113
114
# File 'lib/torm/rules_engine.rb', line 109

def as_hash
  {
    policies: policies,
    rules:    rules
  }
end

#decide(name, environment = {}) ⇒ Object

Evaluate a rule and return its result. Depending on the rule, different values are returned.

Raises:

  • (RuntimeError)

    Raise when the rule is not defined.



98
99
100
101
102
103
104
# File 'lib/torm/rules_engine.rb', line 98

def decide(name, environment={})
  raise "Unknown rule: #{name.inspect}" unless rules.has_key?(name)
  environment          = Torm.symbolize_keys(environment)
  decision_environment = Torm.slice(environment, *conditions_whitelist_for(name))
  answer               = make_decision(name, decision_environment)
  answer
end

#dirty?true, false

Have any rules been added since the last save or load?

Returns:

  • (true, false)


20
21
22
# File 'lib/torm/rules_engine.rb', line 20

def dirty?
  @dirty
end

#saveObject

Save the current rules to the file.



161
162
163
164
165
# File 'lib/torm/rules_engine.rb', line 161

def save
  Torm.atomic_save(rules_file, to_json + "\n")
  @dirty = false
  nil
end

#to_jsonString

Serialise the data from as_hash.

Returns:

  • (String)


119
120
121
# File 'lib/torm/rules_engine.rb', line 119

def to_json
  MultiJson.dump(as_hash)
end