C0 code coverage information
Generated on Sat Jul 21 11:12:32 -0400 2007 with rcov 0.8.0
Code reported as executed by Ruby looks like this...
and this: this line is also marked as covered.
Lines considered as run by rcov, but not reported by Ruby, look like this,
and this: these lines were inferred by rcov (using simple heuristics).
Finally, here's a line marked as not executed.
1 require 'rools/errors'
2 require 'rools/rule'
3 require 'rools/base'
4 require 'rools/facts'
5 require 'rools/csv_table'
6
7 require 'rexml/document'
8
9 module Rools
10 class RuleSet < Base
11 attr_reader :num_executed, :num_evaluated, :facts, :status
12
13 PASS = :pass
14 FAIL = :fail
15 UNDETERMINED = :undetermined
16
17 # You can pass a set of Rools::Rules with a block parameter,
18 # or you can pass a file-path to evaluate.
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
49
50 #
51 # Loads decision table
52 #
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
58
59 #
60 # XML File format loading
61 #
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
70
71 # load xml rules as a string
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
110
111 #
112 # Ruby File format loading
113 #
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
122
123 # load ruby rules as a string
124 def load_rb_rules_as_string( str )
125 instance_eval(str)
126 end
127
128 #
129 # returns an array of facts
130 #
131 def get_facts
132 @facts
133 end
134
135 #
136 # returns all the rules defined for that set
137 #
138 def get_rules
139 @rules
140 end
141
142 # rule creates a Rools::Rule. Make sure to use a descriptive name or symbol.
143 # For the purposes of extending Rules, all names are converted to
144 # strings and downcased.
145 # ==Example
146 # rule 'ruby is the best' do
147 # condition { language.name.downcase == 'ruby' }
148 # consequence { "#{language.name} is the best!" }
149 # end
150 def rule(name, priority=0, &b)
151 name.to_s.downcase!
152 @rules[name] = Rule.new(self, name, priority, b)
153 end
154
155 # facts can be created in a similar manner to rules
156 # all names are converted to strings and downcased.
157 # Facts name is equivalent to a Class Name
158 #
159 # ==Example
160 #
161 # require 'rools'
162 #
163 # rules = Rools::RuleSet.new do
164 #
165 # facts 'Countries' do
166 # ["China", "USSR", "France", "Great Britain", "USA"]
167 # end
168 #
169 # rule 'Is it on Security Council?' do
170 # parameter String
171 # condition { countries.include?(string) }
172 # consequence { puts "Yes, #{string} is in the country list"}
173 # end
174 # end
175 #
176 # rules.assert 'France'
177 #
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
184
185 # A single fact can be an single object of a particular class type
186 # or a collection of objects of a particular type
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
207
208 # Delete all existing facts
209 def delete_facts
210 @facts = {}
211 end
212
213 # Use in conjunction with Rools::RuleSet#with to create a Rools::Rule dependent on
214 # another. Dependencies are created through names (converted to
215 # strings and downcased), so lax naming can get you into trouble with
216 # creating dependencies or overwriting rules you didn't mean to.
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
223
224 # Used in conjunction with Rools::RuleSet#extend to create a dependent Rools::Rule
225 # ==Example
226 # extend('ruby is the best').with('ruby rules the world') do
227 # condition { language.age > 15 }
228 # consequence { "In the year 2008 Ruby conquered the known universe" }
229 # end
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
235
236 # Stops the current assertion. Does not indicate failure.
237 def stop(message = nil)
238 @assert = false
239 end
240
241 # Stops the current assertion and change status to :fail
242 def fail(message = nil)
243 @status = FAIL
244 @assert = false
245 end
246
247 #
248 # an assert has been made within a rule
249 #
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
258
259 # Turn passed object into facts and evaluate all relevant rules
260 # Previous facts of same type are removed
261 def assert( *objs )
262 objs.each { |obj|
263 fact(obj)
264 }
265 return evaluate()
266 end
267
268 #
269 # for a particular fact, we need to retrieve the relevant rules
270 # and add them to the relevant list
271 #
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
284
285 #
286 # relevant rules need to be sorted in priority order
287 #
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
294
295 # get all relevant rules for all specified facts
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
303
304 # evaluate all relevant rules for specified facts
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
377
378 end # class RuleSet
379 end # module Rools
Generated using the rcov code coverage analysis tool for Ruby version 0.8.0.