Class: Rools::RuleSet

Inherits:
Base
  • Object
show all
Defined in:
lib/rools/rule_set.rb

Constant Summary collapse

PASS =
:pass
FAIL =
:fail
UNDETERMINED =
:undetermined

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#logger, logger=

Constructor Details

#initialize(file = nil, &b) ⇒ RuleSet

You can pass a set of Rools::Rules with a block parameter, or you can pass a file-path to evaluate.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/rools/rule_set.rb', line 19

def initialize(file = nil, &b)
  
  @rules = {}
  @facts = {}
  @dependencies = {}
  
  if block_given?
    instance_eval(&b)
  elsif file
    # loading a file, check extension
    name,ext = file.split(".")
    logger.debug("loading ext: #{name}.#{ext}") if logger
    case ext
      when 'csv'
        load_csv( file )
        
      when 'xml'
        load_xml( file )
        
      when 'rb'
        load_rb( file )
          
      when 'rules'  # for backwards compatibility
        load_rb(file)
        
      else
        raise RuleLoadingError, "invalid file extension: #{ext}"
    end
   end
end

Instance Attribute Details

#facts(name, &b) ⇒ Object (readonly)

facts can be created in a similar manner to rules all names are converted to strings and downcased. Facts name is equivalent to a Class Name

Example

require 'rools'

rules = Rools::RuleSet.new do

  facts 'Countries' do
  	["China", "USSR", "France", "Great Britain", "USA"]
  end

  rule 'Is it on Security Council?' do
    parameter String
  	condition { countries.include?(string) }
  	consequence { puts "Yes, #{string} is in the country list"}
  end
end

rules.assert 'France'


178
179
180
# File 'lib/rools/rule_set.rb', line 178

def facts
  @facts
end

#num_evaluatedObject (readonly)

Returns the value of attribute num_evaluated.



11
12
13
# File 'lib/rools/rule_set.rb', line 11

def num_evaluated
  @num_evaluated
end

#num_executedObject (readonly)

Returns the value of attribute num_executed.



11
12
13
# File 'lib/rools/rule_set.rb', line 11

def num_executed
  @num_executed
end

#statusObject (readonly)

Returns the value of attribute status.



11
12
13
# File 'lib/rools/rule_set.rb', line 11

def status
  @status
end

Instance Method Details

#add_relevant_rules_for_fact(fact) ⇒ Object

for a particular fact, we need to retrieve the relevant rules and add them to the relevant list



272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/rools/rule_set.rb', line 272

def add_relevant_rules_for_fact fact
  @rules.values.select { |rule| 
    if !@relevant_rules.include?( rule)
        if rule.parameters_match?(fact.value) 
          @relevant_rules << rule 
          logger.debug "#{rule} is relevant" if logger
        else
          logger.debug "#{rule} is not relevant" if logger          
        end 
    end
  } 
end

#assert(*objs) ⇒ Object

Turn passed object into facts and evaluate all relevant rules Previous facts of same type are removed



261
262
263
264
265
266
# File 'lib/rools/rule_set.rb', line 261

def assert( *objs )
  objs.each { |obj| 
    fact(obj)
  }
  return evaluate()
end

#delete_factsObject

Delete all existing facts



209
210
211
# File 'lib/rools/rule_set.rb', line 209

def delete_facts
    @facts = {}
end

#evaluateObject

evaluate all relevant rules for specified facts



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/rools/rule_set.rb', line 305

def evaluate
  @status = PASS
  @assert = true
  @num_executed = 0;
  @num_evaluated = 0;
  
  get_relevant_rules()
  logger.debug("no relevant rules") if logger && @relevant_rules.size==0
  
  #begin #rescue
    
    # loop through the available_rules, evaluating each one,
    # until there are no more matching rules available
    begin # loop
      
      # the loop condition is reset to break by default after every iteration
      matches = false
      obj     = nil #deprecated
 
      #logger.debug("available rules: #{available_rules.size.to_s}") if logger
      @relevant_rules.each do |rule|
        # RuleCheckErrors are caught and swallowed and the rule that
        # raised the error is removed from the working-set.
        logger.debug("evaluating: #{rule}") if logger
        begin
          @num_evaluated += 1
          if rule.conditions_match?(obj)
            logger.debug("rule #{rule} matched") if logger
            matches = true
            
            # remove the rule from the working-set so it's not re-evaluated
            @relevant_rules.delete(rule)
            
            # find all parameter-matching dependencies of this rule and
            # add them to the working-set.
            if @dependencies.has_key?(rule.name)
              logger.debug( "found dependant rules to #{rule}") if logger
              @relevant_rules += @dependencies[rule.name].select do |dependency|
                dependency.parameters_match?(obj)
              end
            end
            
            # execute this rule
            logger.debug("executing rule #{rule}") if logger
            rule.call(obj)
            @num_executed += 1
            
            # break the current iteration and start back from the first rule defined.
            break
          end # if rule.conditions_match?(obj)
          
        rescue RuleConsequenceError
          fail
        rescue RuleCheckError => e
          fail
        end # begin/rescue
        
      end # available_rules.each
      
    end while(matches && @assert)
    
  #rescue RuleConsequenceError => rce
    # RuleConsequenceErrors are allowed to break out of the current assertion,
    # then the inner error is bubbled-up to the asserting code.
  #  @status = FAIL
  #  raise rce.inner_error
  #end
  
  @assert = false
  
  return @status
end

#extend(name, &b) ⇒ Object

Use in conjunction with Rools::RuleSet#with to create a Rools::Rule dependent on another. Dependencies are created through names (converted to strings and downcased), so lax naming can get you into trouble with creating dependencies or overwriting rules you didn’t mean to.



217
218
219
220
221
222
# File 'lib/rools/rule_set.rb', line 217

def extend(name, &b)
  name.to_s.downcase!
  @extend_rule_name = name
  instance_eval(&b) if block_given?
  return self
end

#fact(obj) ⇒ Object

A single fact can be an single object of a particular class type or a collection of objects of a particular type



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/rools/rule_set.rb', line 187

def fact( obj )
  #begin
    # check if facts already exist for that class
    # if so, we need to add it to the existing list
    cls = obj.class.to_s.downcase
    cls.gsub!(/:/, '_')
    if @facts.key? cls
      logger.debug( "adding to facts: #{cls}") if logger
      @facts[cls].fact_value << obj
    else
      logger.debug( "creating facts: #{cls}") if logger
      arr = Array.new
      arr << obj
      proc = Proc.new { arr }
      @facts[cls] = Facts.new(self, cls, proc ) 
    end
  #rescue Exception=> e
  #  logger.error e if logger
  #end
end

#fail(message = nil) ⇒ Object

Stops the current assertion and change status to :fail



242
243
244
245
# File 'lib/rools/rule_set.rb', line 242

def fail(message = nil)
  @status = FAIL
  @assert = false
end

#get_factsObject

returns an array of facts



131
132
133
# File 'lib/rools/rule_set.rb', line 131

def get_facts
  @facts
end

#get_relevant_rulesObject

get all relevant rules for all specified facts



296
297
298
299
300
301
302
# File 'lib/rools/rule_set.rb', line 296

def get_relevant_rules
  @relevant_rules = Array.new
  @facts.each { |k,f| 
    add_relevant_rules_for_fact f
  }
  sort_relevant_rules
end

#get_rulesObject

returns all the rules defined for that set



138
139
140
# File 'lib/rools/rule_set.rb', line 138

def get_rules
  @rules
end

#load_csv(file) ⇒ Object

Loads decision table



53
54
55
56
57
# File 'lib/rools/rule_set.rb', line 53

def load_csv( file )
  csv = CsvTable.new( file )
  logger.debug "csv rules: #{csv.rules}" if logger
  instance_eval(csv.rules)
end

#load_rb(file) ⇒ Object

Ruby File format loading



114
115
116
117
118
119
120
121
# File 'lib/rools/rule_set.rb', line 114

def load_rb( file )
  begin
    str = IO.read(file)
    load_rb_rules_as_string(str)
  rescue Exception => e
    raise RuleLoadingError, "loading ruby file"
  end
end

#load_rb_rules_as_string(str) ⇒ Object

load ruby rules as a string



124
125
126
# File 'lib/rools/rule_set.rb', line 124

def load_rb_rules_as_string( str )
  instance_eval(str)    
end

#load_xml(fileName) ⇒ Object

XML File format loading



62
63
64
65
66
67
68
69
# File 'lib/rools/rule_set.rb', line 62

def load_xml( fileName )
  begin
    str = IO.read(fileName)
    load_xml_rules_as_string(str)
  rescue Exception => e
    raise RuleLoadingError, "loading xml file"
  end
end

#load_xml_rules_as_string(str) ⇒ Object

load xml rules as a string



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/rools/rule_set.rb', line 72

def load_xml_rules_as_string( str )
  begin
    doc = REXML::Document.new str
    doc.elements.each( "rule-set") { |rs| 
      facts = rs.elements.each( "facts") { |f| 
        facts( f.attributes["name"] ) do f.text.strip end
      }
      
      rules = rs.elements.each( "rule") { |rule_node|
         rule_name  = rule_node.attributes["name"]
         priority   = rule_node.attributes["priority"]
         
         rule       = Rule.new(self, rule_name, priority, nil)
         
         parameters = rule_node.elements.each("parameter") { |param|
            #logger.debug "xml parameter: #{param.text.strip}"
            rule.parameters(eval(param.text.strip))
         } 
         
         conditions = rule_node.elements.each("condition") { |cond|
            #logger.debug "xml condition #{cond}"
            rule.condition do eval(cond.text.strip) end
         } 
   
         consequences = rule_node.elements.each("consequence") { |cons|
           #logger.debug "xml consequence #{cons}"
           rule.consequence do eval(cons.text.strip) end
         } 
         
         @rules[rule_name] = rule
      }
      logger.debug( "loaded #{rules.size} rules") if logger
    }
  rescue Exception => e
    raise RuleLoadingError, "loading xml file"
  end
  
end

#rule(name, priority = 0, &b) ⇒ Object

rule creates a Rools::Rule. Make sure to use a descriptive name or symbol. For the purposes of extending Rules, all names are converted to strings and downcased.

Example

rule 'ruby is the best' do
  condition { language.name.downcase == 'ruby' }
  consequence { "#{language.name} is the best!" }
end


150
151
152
153
# File 'lib/rools/rule_set.rb', line 150

def rule(name, priority=0, &b)
  name.to_s.downcase!
  @rules[name] = Rule.new(self, name, priority, b)
end

#rule_assert(obj) ⇒ Object

an assert has been made within a rule



250
251
252
253
254
255
256
257
# File 'lib/rools/rule_set.rb', line 250

def rule_assert( obj )
  # add object as a new fact
  f = fact(obj)
  # get_relevant_rules
  logger.debug( "Check if we need to add more rules") if logger
  add_relevant_rules_for_fact(f)
  sort_relevant_rules
end

#sort_relevant_rulesObject

relevant rules need to be sorted in priority order



288
289
290
291
292
293
# File 'lib/rools/rule_set.rb', line 288

def sort_relevant_rules
  # sort array in rule priority order
  @relevant_rules = @relevant_rules.sort do  |r1, r2| 
    r2.priority <=> r1.priority 
  end
end

#stop(message = nil) ⇒ Object

Stops the current assertion. Does not indicate failure.



237
238
239
# File 'lib/rools/rule_set.rb', line 237

def stop(message = nil)
  @assert = false
end

#with(name, prio = 0, &b) ⇒ Object

Used in conjunction with Rools::RuleSet#extend to create a dependent Rools::Rule

Example

extend('ruby is the best').with('ruby rules the world') do
  condition { language.age > 15 }
  consequence { "In the year 2008 Ruby conquered the known universe" }
end


230
231
232
233
234
# File 'lib/rools/rule_set.rb', line 230

def with(name, prio=0, &b)
  name.to_s.downcase!
   (@dependencies[@extend_rule_name] ||= []) << Rule.new(self, name, prio, b)
   #@rules[name] = Rule.new(self, name, prio, b)
end