Class Rools::RuleSet
In: lib/rools/rule_set.rb
Parent: Base

Methods

Constants

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

Attributes

facts  [R] 
num_evaluated  [R] 
num_executed  [R] 
status  [R] 

Public Class methods

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

[Source]

    # File lib/rools/rule_set.rb, line 19
19:     def initialize(file = nil, &b)
20:       
21:       @rules = {}
22:       @facts = {}
23:       @dependencies = {}
24:       
25:       if block_given?
26:         instance_eval(&b)
27:       elsif file
28:         # loading a file, check extension

29:         name,ext = file.split(".")
30:         logger.debug("loading ext: #{name}.#{ext}") if logger
31:         case ext
32:           when 'csv'
33:             load_csv( file )
34:             
35:           when 'xml'
36:             load_xml( file )
37:             
38:           when 'rb'
39:             load_rb( file )
40:               
41:           when 'rules'  # for backwards compatibility

42:             load_rb(file)
43:             
44:           else
45:             raise RuleLoadingError, "invalid file extension: #{ext}"
46:         end
47:        end
48:     end

Public Instance methods

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

[Source]

     # File lib/rools/rule_set.rb, line 272
272:     def add_relevant_rules_for_fact fact
273:       @rules.values.select { |rule| 
274:         if !@relevant_rules.include?( rule)
275:             if rule.parameters_match?(fact.value) 
276:               @relevant_rules << rule 
277:               logger.debug "#{rule} is relevant" if logger
278:             else
279:               logger.debug "#{rule} is not relevant" if logger          
280:             end 
281:         end
282:       } 
283:     end

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

[Source]

     # File lib/rools/rule_set.rb, line 261
261:     def assert( *objs )
262:       objs.each { |obj| 
263:         fact(obj)
264:       }
265:       return evaluate()
266:     end

Delete all existing facts

[Source]

     # File lib/rools/rule_set.rb, line 209
209:     def delete_facts
210:         @facts = {}
211:     end

evaluate all relevant rules for specified facts

[Source]

     # File lib/rools/rule_set.rb, line 305
305:     def evaluate
306:       @status = PASS
307:       @assert = true
308:       @num_executed = 0;
309:       @num_evaluated = 0;
310:       
311:       get_relevant_rules()
312:       logger.debug("no relevant rules") if logger && @relevant_rules.size==0
313:       
314:       #begin #rescue

315:         
316:         # loop through the available_rules, evaluating each one,

317:         # until there are no more matching rules available

318:         begin # loop

319:           
320:           # the loop condition is reset to break by default after every iteration

321:           matches = false
322:           obj     = nil #deprecated

323:  
324:           #logger.debug("available rules: #{available_rules.size.to_s}") if logger

325:           @relevant_rules.each do |rule|
326:             # RuleCheckErrors are caught and swallowed and the rule that

327:             # raised the error is removed from the working-set.

328:             logger.debug("evaluating: #{rule}") if logger
329:             begin
330:               @num_evaluated += 1
331:               if rule.conditions_match?(obj)
332:                 logger.debug("rule #{rule} matched") if logger
333:                 matches = true
334:                 
335:                 # remove the rule from the working-set so it's not re-evaluated

336:                 @relevant_rules.delete(rule)
337:                 
338:                 # find all parameter-matching dependencies of this rule and

339:                 # add them to the working-set.

340:                 if @dependencies.has_key?(rule.name)
341:                   logger.debug( "found dependant rules to #{rule}") if logger
342:                   @relevant_rules += @dependencies[rule.name].select do |dependency|
343:                     dependency.parameters_match?(obj)
344:                   end
345:                 end
346:                 
347:                 # execute this rule

348:                 logger.debug("executing rule #{rule}") if logger
349:                 rule.call(obj)
350:                 @num_executed += 1
351:                 
352:                 # break the current iteration and start back from the first rule defined.

353:                 break
354:               end # if rule.conditions_match?(obj)

355:               
356:             rescue RuleConsequenceError
357:               fail
358:             rescue RuleCheckError => e
359:               fail
360:             end # begin/rescue

361:             
362:           end # available_rules.each

363:           
364:         end while(matches && @assert)
365:         
366:       #rescue RuleConsequenceError => rce

367:         # RuleConsequenceErrors are allowed to break out of the current assertion,

368:         # then the inner error is bubbled-up to the asserting code.

369:       #  @status = FAIL

370:       #  raise rce.inner_error

371:       #end

372:       
373:       @assert = false
374:       
375:       return @status
376:     end

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.

[Source]

     # File lib/rools/rule_set.rb, line 217
217:     def extend(name, &b)
218:       name.to_s.downcase!
219:       @extend_rule_name = name
220:       instance_eval(&b) if block_given?
221:       return self
222:     end

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

[Source]

     # File lib/rools/rule_set.rb, line 187
187:     def fact( obj )
188:       #begin

189:         # check if facts already exist for that class

190:         # if so, we need to add it to the existing list

191:         cls = obj.class.to_s.downcase
192:         cls.gsub!(/:/, '_')
193:         if @facts.key? cls
194:           logger.debug( "adding to facts: #{cls}") if logger
195:           @facts[cls].fact_value << obj
196:         else
197:           logger.debug( "creating facts: #{cls}") if logger
198:           arr = Array.new
199:           arr << obj
200:           proc = Proc.new { arr }
201:           @facts[cls] = Facts.new(self, cls, proc ) 
202:         end
203:       #rescue Exception=> e

204:       #  logger.error e if logger

205:       #end

206:     end

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'

[Source]

     # File lib/rools/rule_set.rb, line 178
178:     def facts(name, &b)
179:       name.gsub!(/:/, '_')
180:       name.to_s.downcase!
181:       @facts[name] = Facts.new(self, name, b)
182:      logger.debug( "created facts: #{name}") if logger
183:     end

Stops the current assertion and change status to :fail

[Source]

     # File lib/rools/rule_set.rb, line 242
242:     def fail(message = nil)
243:       @status = FAIL
244:       @assert = false
245:     end

returns an array of facts

[Source]

     # File lib/rools/rule_set.rb, line 131
131:     def get_facts
132:       @facts
133:     end

get all relevant rules for all specified facts

[Source]

     # File lib/rools/rule_set.rb, line 296
296:     def get_relevant_rules
297:       @relevant_rules = Array.new
298:       @facts.each { |k,f| 
299:         add_relevant_rules_for_fact f
300:       }
301:       sort_relevant_rules
302:     end

returns all the rules defined for that set

[Source]

     # File lib/rools/rule_set.rb, line 138
138:     def get_rules
139:       @rules
140:     end

Loads decision table

[Source]

    # File lib/rools/rule_set.rb, line 53
53:     def load_csv( file )
54:       csv = CsvTable.new( file )
55:       logger.debug "csv rules: #{csv.rules}" if logger
56:       instance_eval(csv.rules)
57:     end

Ruby File format loading

[Source]

     # File lib/rools/rule_set.rb, line 114
114:     def load_rb( file )
115:       begin
116:         str = IO.read(file)
117:         load_rb_rules_as_string(str)
118:       rescue Exception => e
119:         raise RuleLoadingError, "loading ruby file"
120:       end
121:     end

load ruby rules as a string

[Source]

     # File lib/rools/rule_set.rb, line 124
124:     def load_rb_rules_as_string( str )
125:       instance_eval(str)    
126:     end

XML File format loading

[Source]

    # File lib/rools/rule_set.rb, line 62
62:     def load_xml( fileName )
63:       begin
64:         str = IO.read(fileName)
65:         load_xml_rules_as_string(str)
66:       rescue Exception => e
67:         raise RuleLoadingError, "loading xml file"
68:       end
69:     end

load xml rules as a string

[Source]

     # File lib/rools/rule_set.rb, line 72
 72:     def load_xml_rules_as_string( str )
 73:       begin
 74:         doc = REXML::Document.new str
 75:         doc.elements.each( "rule-set") { |rs| 
 76:           facts = rs.elements.each( "facts") { |f| 
 77:             facts( f.attributes["name"] ) do f.text.strip end
 78:           }
 79:           
 80:           rules = rs.elements.each( "rule") { |rule_node|
 81:              rule_name  = rule_node.attributes["name"]
 82:              priority   = rule_node.attributes["priority"]
 83:              
 84:              rule       = Rule.new(self, rule_name, priority, nil)
 85:              
 86:              parameters = rule_node.elements.each("parameter") { |param|
 87:                 #logger.debug "xml parameter: #{param.text.strip}"

 88:                 rule.parameters(eval(param.text.strip))
 89:              } 
 90:              
 91:              conditions = rule_node.elements.each("condition") { |cond|
 92:                 #logger.debug "xml condition #{cond}"

 93:                 rule.condition do eval(cond.text.strip) end
 94:              } 
 95:    
 96:              consequences = rule_node.elements.each("consequence") { |cons|
 97:                #logger.debug "xml consequence #{cons}"

 98:                rule.consequence do eval(cons.text.strip) end
 99:              } 
100:              
101:              @rules[rule_name] = rule
102:           }
103:           logger.debug( "loaded #{rules.size} rules") if logger
104:         }
105:       rescue Exception => e
106:         raise RuleLoadingError, "loading xml file"
107:       end
108:       
109:     end

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

[Source]

     # File lib/rools/rule_set.rb, line 150
150:     def rule(name, priority=0, &b)
151:       name.to_s.downcase!
152:       @rules[name] = Rule.new(self, name, priority, b)
153:     end

an assert has been made within a rule

[Source]

     # File lib/rools/rule_set.rb, line 250
250:     def rule_assert( obj )
251:       # add object as a new fact

252:       f = fact(obj)
253:       # get_relevant_rules

254:       logger.debug( "Check if we need to add more rules") if logger
255:       add_relevant_rules_for_fact(f)
256:       sort_relevant_rules
257:     end

relevant rules need to be sorted in priority order

[Source]

     # File lib/rools/rule_set.rb, line 288
288:     def sort_relevant_rules
289:       # sort array in rule priority order

290:       @relevant_rules = @relevant_rules.sort do  |r1, r2| 
291:         r2.priority <=> r1.priority 
292:       end
293:     end

Stops the current assertion. Does not indicate failure.

[Source]

     # File lib/rools/rule_set.rb, line 237
237:     def stop(message = nil)
238:       @assert = false
239:     end

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

[Source]

     # File lib/rools/rule_set.rb, line 230
230:     def with(name, prio=0, &b)
231:       name.to_s.downcase!
232:        (@dependencies[@extend_rule_name] ||= []) << Rule.new(self, name, prio, b)
233:        #@rules[name] = Rule.new(self, name, prio, b)

234:     end

[Validate]