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 +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
|