Class: Rools::RuleSet

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

Constant Summary

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

Instance Attribute Summary (collapse)

Instance Method Summary (collapse)

Methods inherited from Base

#logger, logger=

Constructor Details

- (RuleSet) initialize(file = nil, &b)

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

- (Object) facts(name, &b) (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
181
182
183
# File 'lib/rools/rule_set.rb', line 178

def facts(name, &b)
  name.gsub!(/:/, '_')
  name.to_s.downcase!
  @facts[name] = Facts.new(self, name, b)
 logger.debug( "created facts: #{name}") if logger
end

- (Object) num_evaluated (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

- (Object) num_executed (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

- (Object) status (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

- (Object) add_relevant_rules_for_fact(fact)

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

- (Object) assert(*objs)

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

- (Object) delete_facts

Delete all existing facts



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

def delete_facts
    @facts = {}
end

- (Object) evaluate

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

- (Object) extend(name, &b)

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

- (Object) fact(obj)

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

- (Object) fail(message = nil)

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

- (Object) get_facts

returns an array of facts



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

def get_facts
  @facts
end

- (Object) get_relevant_rules

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

- (Object) get_rules

returns all the rules defined for that set



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

def get_rules
  @rules
end

- (Object) load_csv(file)

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

- (Object) load_rb(file)

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

- (Object) load_rb_rules_as_string(str)

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

- (Object) load_xml(fileName)

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

- (Object) load_xml_rules_as_string(str)

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

- (Object) rule(name, priority = 0, &b)

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

- (Object) rule_assert(obj)

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

- (Object) sort_relevant_rules

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

- (Object) stop(message = nil)

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

- (Object) with(name, prio = 0, &b)

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