| Class | Rools::RuleSet |
| In: |
lib/rools/rule_set.rb
|
| Parent: | Base |
| PASS | = | :pass |
| FAIL | = | :fail |
| UNDETERMINED | = | :undetermined |
| facts | [R] | |
| num_evaluated | [R] | |
| num_executed | [R] | |
| status | [R] |
You can pass a set of Rools::Rules with a block parameter, or you can pass a file-path to evaluate.
# 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
for a particular fact, we need to retrieve the relevant rules and add them to the relevant list
# 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
evaluate all relevant rules for specified facts
# 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.
# 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
# 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
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'
# 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
returns all the rules defined for that set
# File lib/rools/rule_set.rb, line 138
138: def get_rules
139: @rules
140: end
Loads decision table
# 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
# 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
# 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
# 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
# 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.
rule 'ruby is the best' do
condition { language.name.downcase == 'ruby' }
consequence { "#{language.name} is the best!" }
end
# 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
# 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
# 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.
# 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
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
# 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