rools 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG CHANGED
@@ -1,5 +1,18 @@
1
1
  = Rools CHANGELOG
2
2
 
3
+ == Rools - 0.1.6 released 2007/05/20
4
+ * todo #1319 use Rails conventions for rules parameters.
5
+ WARNING: no more free for all.
6
+ The variable used in the rule has to match the define parameter (a Class type)
7
+ therefore the variable has to be a lowercase singulr version of the parameter. This is
8
+ required now to support multiple facts
9
+ * todo #1318 expand assert to accept more than one object. You can also use a fact notation
10
+ to add facts and call evaluate
11
+ * todo #1283 rule priority completed
12
+ * todo #1286 added facts that can be asserted
13
+ * todo #1288 decision tables have been completed
14
+
15
+
3
16
  == Rools - 0.1.5 released 2007/04/27
4
17
  * todo #1282 Added unit tests for baseline
5
18
  * todo #1283 Added rule priority (not completed)
data/RAKEFILE CHANGED
@@ -131,7 +131,7 @@ end
131
131
  desc "Publish RDOC to RubyForge"
132
132
  task :rubyforge => [:rdoc, :gem] do
133
133
  Rake::SshDirPublisher.new(ENV['RUBYFORGE_USER'], ENV['RUBYFORGE_PROJECT'], 'html').upload
134
- Rake::SshFilePublisher.new(ENV['RUBYFORGE_USER'], ENV['RUBYFORGE_PROJECT'], 'pkg', "#{PROJECT}-#{PACKAGE_VERSION}.gem").upload
134
+ #Rake::SshFilePublisher.new(ENV['RUBYFORGE_USER'], ENV['RUBYFORGE_PROJECT'], 'pkg', "#{PROJECT}-#{PACKAGE_VERSION}.gem").upload
135
135
  end
136
136
 
137
137
  # Builds the website and uploads it to Rubyforge.org
@@ -30,7 +30,7 @@ module Rools
30
30
  parameter = arrs[1][1]
31
31
  #puts "parameter:#{parameter}"
32
32
 
33
- # get rule elements
33
+ # get rule elements Conditions/Consequences
34
34
  rule_elements = arrs[2]
35
35
 
36
36
  # get code
@@ -47,34 +47,43 @@ module Rools
47
47
  @rules = ""
48
48
  arrs[5..arrs.size].each { |arr|
49
49
  rule_name = "rule_#{index}"
50
- #puts "arr:#{arr}"
50
+ #puts "arr:#{arr} index: #{index}"
51
51
 
52
- @rules << "rule '#{rule_name}' do \n"
52
+ #if rule_elements[index] != nil
53
+ @rules << "rule '#{rule_name}' do \n"
53
54
  @rules << " parameter #{parameter}\n"
54
55
  column = 0
56
+
55
57
  rule_elements.each do |element|
56
58
 
57
- field = headers[column].downcase
59
+ field = headers[column].downcase if headers[column]
58
60
  str = arr[column]
59
61
 
60
- if str != nil
62
+ if str != nil && element != nil
61
63
  #puts ("eval: #{field} = '#{str}'")
62
64
  #eval( "#{field} = '#{str}'" )
63
65
 
64
66
  @rules << "\t" + element.downcase + "{ "
65
67
  pattern = "\#\{#{field}\}"
66
68
 
67
- statement = rule_code[column].gsub(pattern,quote(str))
68
-
69
- #puts "statement:#{statement}"
69
+ #puts rule_code[column]
70
+ #puts "pattern: #{pattern} str:#{str}"
71
+ if rule_code[column] == pattern
72
+ statement = str # straight replace
73
+ else
74
+ statement = rule_code[column].gsub(pattern,quote(str))
75
+ end
70
76
 
71
- @rules << statement
77
+ #puts "statement:#{statement}"
72
78
 
79
+ @rules << statement
73
80
  @rules << "}\n"
74
81
  end
82
+
75
83
  column += 1
76
84
  end
77
- @rules << "end\n"
85
+ @rules << "end\n"
86
+ #end
78
87
  index += 1
79
88
  }
80
89
  end
@@ -45,18 +45,19 @@ module Rools
45
45
  # Parameterless method calls by the attached block are assumed to
46
46
  # be references to the working object
47
47
  def method_missing(sym, *args)
48
- # puts "method missing: #{sym}"
48
+ #puts "method missing: #{sym}"
49
49
  # check if it is a fact first
50
50
  begin
51
51
  facts = @rule.rule_set.get_facts
52
52
  if facts.has_key?( sym.to_s )
53
- #puts "return fact #{rsfacts[sym.to_s].fact_value}"
54
- return facts[sym.to_s].fact_value
53
+ #puts "return fact #{facts[sym.to_s].value}"
54
+ return facts[sym.to_s].value
55
55
  else
56
- #puts "#{sym} not in facts"
56
+ #puts "#{sym} not in facts"
57
57
  end
58
58
  rescue Exception => e
59
- logger.error "miss exception #{e} #{e.backtrace.join("\n")}" if logger
59
+ #logger.error "miss exception #{e} #{e.backtrace.join("\n")}" if logger
60
+ #puts "miss exception #{e} #{e.backtrace.join("\n")}"
60
61
  end
61
62
  return @working_object if @working_object && args.size == 0
62
63
  return nil
@@ -2,6 +2,9 @@ require 'rools/errors'
2
2
  require 'rools/default_parameter_proc'
3
3
  require 'rools/base'
4
4
 
5
+ #
6
+ # Facts are collections or recordsets created by the user
7
+ #
5
8
  module Rools
6
9
  class Facts < Base
7
10
  attr_reader :name, :fact_value
@@ -9,7 +12,15 @@ module Rools
9
12
  def initialize(rule_set, name, b)
10
13
  @name = name
11
14
  @fact_value = instance_eval( &b )
12
- logger.debug "New Facts: #{@fact_value}" if logger
15
+ #logger.debug "New Facts: #{@fact_value}" if logger
16
+ end
17
+
18
+ def value
19
+ if @fact_value.size == 1
20
+ @fact_value[0]
21
+ else
22
+ @fact_value
23
+ end
13
24
  end
14
25
  end
15
26
  end
@@ -10,9 +10,9 @@ module Rools
10
10
  # A Rule requires a Rools::RuleSet, a name, and an associated block
11
11
  # which will be executed at initialization
12
12
  def initialize(rule_set, name, priority, b)
13
- @rule_set = rule_set
14
- @name = name
15
- @priority = priority
13
+ @rule_set = rule_set
14
+ @name = name
15
+ @priority = priority
16
16
  @conditions = []
17
17
  @consequences = []
18
18
  @parameters = []
@@ -55,6 +55,7 @@ module Rools
55
55
  # To verify that the asserted object is an Employee, that inherits from
56
56
  # Person, and responds to :department
57
57
  def parameters(*matches)
58
+ logger.debug( "Adding parameters: #{matches}") if logger
58
59
  @parameters += matches
59
60
  end
60
61
 
@@ -65,16 +66,25 @@ module Rools
65
66
  # Checks to see if this Rule's parameters match the asserted object
66
67
  def parameters_match?(obj)
67
68
  @parameters.each do |p|
68
- logger.debug( "match p:#{p} obj:#{obj} sym:#{Symbol}") if logger
69
+ logger.debug( "#{self} match p:#{p} obj:#{obj} sym:#{Symbol}") if logger
69
70
  if p.is_a?(Symbol)
70
- return false unless obj.respond_to?(p)
71
+ #return false unless obj.respond_to?(p)
72
+ return true if obj.respond_to?(p)
71
73
  else
72
- logger.debug( "is_a p:#{p} obj:#{obj} #{obj.is_a?(p)}") if logger
73
- return false unless obj.is_a?(p)
74
+ logger.debug( "#{self} is_a p:#{p} obj:#{obj} #{obj.is_a?(p)}") if logger
75
+ #return false unless obj.is_a?(p)
76
+ return true if obj.is_a?(p)
74
77
  end
75
78
  end
76
79
 
77
- return true
80
+ # if parameters are not specified, let's assume that the rule is always relevant
81
+ if @parameters.size == 0
82
+ logger.debug "no parameters defined for rule: #{self}" if logger
83
+ return true
84
+ end
85
+
86
+ logger.debug( "no parameter match") if logger
87
+ return false
78
88
  end
79
89
 
80
90
  # Checks to see if this Rule's conditions match the asserted object
@@ -105,6 +115,7 @@ module Rools
105
115
  end
106
116
  rescue StandardError => e
107
117
  # discontinue the Rools::RuleSet#assert if any consequence fails
118
+ logger.error( "rule RuleConsequenceError #{e.to_s} #{e.backtrace.join("\n")}") if logger
108
119
  raise RuleConsequenceError.new(rule, e)
109
120
  end
110
121
  end
@@ -27,7 +27,7 @@ module Rools
27
27
  else
28
28
  # loading a file, check extension
29
29
  name,ext = file.split(".")
30
- logger.debug("loading ext: #{ext}") if logger
30
+ logger.debug("loading ext: #{name}.#{ext}") if logger
31
31
  case ext
32
32
  when 'csv'
33
33
  load_csv( file )
@@ -60,34 +60,44 @@ module Rools
60
60
  # XML File format loading
61
61
  #
62
62
  def load_xml( fileName )
63
- file = File.new( fileName )
64
- doc = REXML::Document.new file
65
- doc.elements.each( "rule-set") { |rs|
66
- facts = rs.elements.each( "facts") { |f|
67
- facts( f.attributes["name"] ) do f.text.strip end
68
- }
69
-
70
- rules = rs.elements.each( "rule") { |rule_node|
71
- rule_name = rule_node.attributes["name"]
72
- priority = rule_node.attributes["priority"]
73
-
74
- rule = Rule.new(self, rule_name, priority, nil)
75
-
76
- parameters = rule_node.elements.each("parameter") { |param|
77
- rule.parameter do eval(param.text.strip) end
78
- }
79
-
80
- conditions = rule_node.elements.each("condition") { |cond|
81
- rule.condition do eval(cond.text.strip) end
82
- }
83
-
84
- consequences = rule_node.elements.each("consequence") { |cons|
85
- rule.consequence do eval(cons.text.strip) end
86
- }
87
-
88
- @rules[rule_name] = rule
63
+ begin
64
+ file = File.new( fileName )
65
+ doc = REXML::Document.new file
66
+ doc.elements.each( "rule-set") { |rs|
67
+ facts = rs.elements.each( "facts") { |f|
68
+ facts( f.attributes["name"] ) do f.text.strip end
69
+ }
70
+
71
+ rules = rs.elements.each( "rule") { |rule_node|
72
+ rule_name = rule_node.attributes["name"]
73
+ priority = rule_node.attributes["priority"]
74
+
75
+ rule = Rule.new(self, rule_name, priority, nil)
76
+
77
+ parameters = rule_node.elements.each("parameter") { |param|
78
+ #logger.debug "xml parameter: #{param.text.strip}"
79
+ rule.parameters(eval(param.text.strip))
80
+ }
81
+
82
+ conditions = rule_node.elements.each("condition") { |cond|
83
+ #logger.debug "xml condition #{cond}"
84
+ rule.condition do eval(cond.text.strip) end
85
+ }
86
+
87
+ consequences = rule_node.elements.each("consequence") { |cons|
88
+ #logger.debug "xml consequence #{cons}"
89
+ rule.consequence do eval(cons.text.strip) end
90
+ }
91
+
92
+ @rules[rule_name] = rule
93
+ }
94
+ logger.debug( "laoded #{rules.size} rules") if logger
89
95
  }
90
- }
96
+ rescue Exception => e
97
+ puts "Load XML Exception: #{e.to_s}"
98
+ puts e.backtrace.join("\n")
99
+ end
100
+
91
101
  end
92
102
 
93
103
  #
@@ -138,8 +148,36 @@ module Rools
138
148
  def facts(name, &b)
139
149
  name.to_s.downcase!
140
150
  @facts[name] = Facts.new(self, name, b)
151
+ logger.debug( "created facts: #{name}") if logger
152
+ end
153
+
154
+ # A single fact can be an single object of a particular class type
155
+ # or a collection of objects of a particular type
156
+ def fact( obj )
157
+ begin
158
+ # check if facts already exist for that class
159
+ # if so, we need to add it to the existing list
160
+ cls = obj.class.to_s.downcase
161
+
162
+ if @facts.key? cls
163
+ logger.debug( "adding to facts: #{cls}") if logger
164
+ @facts[cls].fact_value << obj
165
+ else
166
+ logger.debug( "creating facts: #{cls}") if logger
167
+ arr = Array.new
168
+ arr << obj
169
+ proc = Proc.new { arr }
170
+ @facts[cls] = Facts.new(self, cls, proc )
171
+ end
172
+ rescue Exception=> e
173
+ logger.error e if logger
174
+ end
141
175
  end
142
176
 
177
+ # Delete all existing facts
178
+ def delete_facts
179
+ @facts = {}
180
+ end
143
181
 
144
182
  # Use in conjunction with Rools::RuleSet#with to create a Rools::Rule dependent on
145
183
  # another. Dependencies are created through names (converted to
@@ -174,9 +212,11 @@ module Rools
174
212
  @assert = false
175
213
  end
176
214
 
215
+
216
+
177
217
  # Used to create a working-set of rules for an object, and evaluate it
178
218
  # against them. Returns a status, simply PASS or FAIL
179
- def assert(obj)
219
+ def assert_1(obj)
180
220
  @status = PASS
181
221
  @assert = true
182
222
  @num_executed = 0;
@@ -185,6 +225,10 @@ module Rools
185
225
  # create a working-set of all parameter-matching, non-dependent rules
186
226
  available_rules = @rules.values.select { |rule| rule.parameters_match?(obj) }
187
227
 
228
+ available_rules = available_rules.sort do |r1, r2|
229
+ r2.priority <=> r1.priority
230
+ end
231
+
188
232
  begin
189
233
 
190
234
  # loop through the available_rules, evaluating each one,
@@ -246,5 +290,109 @@ module Rools
246
290
  return @status
247
291
  end # def assert
248
292
 
293
+ # Turn passed object into facts and evaluate all relevant rules
294
+ # Previous facts of same type are removed
295
+ def assert( *objs )
296
+ objs.each { |obj|
297
+ fact(obj)
298
+ }
299
+ return evaluate()
300
+ end
301
+
302
+ # get all relevant rules for all specified facts
303
+ def get_relevant_rules
304
+ @relevant_rules = Array.new
305
+ @facts.each { |k,f|
306
+ @rules.values.select { |rule|
307
+ if !@relevant_rules.include?( rule)
308
+ if rule.parameters_match?(f.value)
309
+ @relevant_rules << rule
310
+ logger.debug "#{rule} is relevant" if logger
311
+ else
312
+ logger.debug "#{rule} is not relevant" if logger
313
+ end
314
+ end
315
+ }
316
+ }
317
+
318
+ # sort array in rule priority order
319
+ @relevant_rules = @relevant_rules.sort do |r1, r2|
320
+ r2.priority <=> r1.priority
321
+ end
322
+ end
323
+
324
+ # evaluate all relevant rules for specified facts
325
+ def evaluate
326
+ @status = PASS
327
+ @assert = true
328
+ @num_executed = 0;
329
+ @num_evaluated = 0;
330
+
331
+ get_relevant_rules()
332
+ logger.debug("no relevant rules") if logger && @relevant_rules.size==0
333
+
334
+ begin #rescue
335
+
336
+ # loop through the available_rules, evaluating each one,
337
+ # until there are no more matching rules available
338
+ begin # loop
339
+
340
+ # the loop condition is reset to break by default after every iteration
341
+ matches = false
342
+ obj = nil #deprecated
343
+
344
+ #logger.debug("available rules: #{available_rules.size.to_s}") if logger
345
+ @relevant_rules.each do |rule|
346
+ # RuleCheckErrors are caught and swallowed and the rule that
347
+ # raised the error is removed from the working-set.
348
+ logger.debug("evaluating: #{rule}") if logger
349
+ begin
350
+ @num_evaluated += 1
351
+ if rule.conditions_match?(obj)
352
+ logger.debug("rule #{rule} matched") if logger
353
+ matches = true
354
+
355
+ # remove the rule from the working-set so it's not re-evaluated
356
+ @relevant_rules.delete(rule)
357
+
358
+ # find all parameter-matching dependencies of this rule and
359
+ # add them to the working-set.
360
+ #if @dependencies.has_key?(rule.name)
361
+ # @relevant_rules += @dependencies[rule.name].select do |dependency|
362
+ # dependency.parameters_match?(obj)
363
+ # end
364
+ #end
365
+
366
+ # execute this rule
367
+ logger.debug("executing rule #{rule}") if logger
368
+ rule.call(obj)
369
+ @num_executed += 1
370
+
371
+ # break the current iteration and start back from the first rule defined.
372
+ break
373
+ end # if rule.conditions_match?(obj)
374
+
375
+ rescue RuleCheckError => e
376
+ logger.debug( "RuleCheckError")
377
+ @relevant_rules.delete(e.rule)
378
+ @status = fail
379
+ end # begin/rescue
380
+
381
+ end # available_rules.each
382
+
383
+ end while(matches && @assert)
384
+
385
+ rescue RuleConsequenceError => rce
386
+ # RuleConsequenceErrors are allowed to break out of the current assertion,
387
+ # then the inner error is bubbled-up to the asserting code.
388
+ @status = fail
389
+ raise rce.inner_error
390
+ end
391
+
392
+ @assert = false
393
+
394
+ return @status
395
+ end
396
+
249
397
  end # class RuleSet
250
398
  end # module Rools
@@ -1,3 +1,3 @@
1
1
  module Rools
2
- ROOLS_VERSION = '0.1.5'
2
+ ROOLS_VERSION = '0.1.6'
3
3
  end
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.2
3
3
  specification_version: 1
4
4
  name: rools
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.1.5
7
- date: 2007-04-27 00:00:00 -04:00
6
+ version: 0.1.6
7
+ date: 2007-05-20 00:00:00 -04:00
8
8
  summary: A Rules Engine written in Ruby
9
9
  require_paths:
10
10
  - lib