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 +13 -0
- data/RAKEFILE +1 -1
- data/lib/rools/csv_table.rb +19 -10
- data/lib/rools/default_parameter_proc.rb +6 -5
- data/lib/rools/facts.rb +12 -1
- data/lib/rools/rule.rb +19 -8
- data/lib/rools/rule_set.rb +177 -29
- data/lib/rools/version.rb +1 -1
- metadata +2 -2
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
|
data/lib/rools/csv_table.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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 #{
|
54
|
-
return facts[sym.to_s].
|
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
|
data/lib/rools/facts.rb
CHANGED
@@ -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
|
data/lib/rools/rule.rb
CHANGED
@@ -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
|
14
|
-
@name
|
15
|
-
@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
|
-
|
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
|
data/lib/rools/rule_set.rb
CHANGED
@@ -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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
facts(
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
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
|
data/lib/rools/version.rb
CHANGED
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.
|
7
|
-
date: 2007-
|
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
|