rools 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
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