rools 0.1.4 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +16 -6
- data/RAKEFILE +119 -11
- data/README +19 -20
- data/lib/rools/base.rb +13 -0
- data/lib/rools/csv_table.rb +85 -0
- data/lib/rools/default_parameter_proc.rb +16 -1
- data/lib/rools/facts.rb +15 -0
- data/lib/rools/rule.rb +17 -8
- data/lib/rools/rule_set.rb +124 -8
- data/lib/rools/version.rb +3 -0
- metadata +41 -30
data/CHANGELOG
CHANGED
@@ -1,17 +1,27 @@
|
|
1
|
-
|
1
|
+
= Rools CHANGELOG
|
2
|
+
|
3
|
+
== Rools - 0.1.5 released 2007/04/27
|
4
|
+
* todo #1282 Added unit tests for baseline
|
5
|
+
* todo #1283 Added rule priority (not completed)
|
6
|
+
* todo #1284 Added XML rule format
|
7
|
+
* todo #1286 Added facts support
|
8
|
+
* todo #1287 Added decision table capability
|
9
|
+
* todo #1288 Added logging capability to trace engine
|
10
|
+
* todo #1292 Major Rakefile upgrade for site auto-generation (Thanks to John Metttraux)
|
11
|
+
|
12
|
+
== Rools - 0.1.4 released 2005/12/15
|
2
13
|
* Removed "examples" folder since my RDoc kung-fu cannot be denied and the examples were redundant
|
3
14
|
* Added Rools::open to rools.rb
|
4
15
|
* Added pscp.rb to create SshPublishers on Win32 with PuttySCP
|
5
16
|
* Added Rools::RuleSet#assert return codes, :pass and :fail
|
6
|
-
* Added stop and fail methods to RuleSet, Rules, and DefaultParameterProc
|
7
|
-
* Built Gem under Ruby 1.8.2 to fix yaml bug for RubyForge distribution
|
17
|
+
* Added stop and fail methods to RuleSet, Rules, and DefaultParameterProc
|
8
18
|
|
9
|
-
- 0.1.3
|
19
|
+
== Rools - 0.1.3
|
10
20
|
Finished most documentation
|
11
21
|
Tweaked the rakefile
|
12
22
|
|
13
|
-
- 0.1.2
|
23
|
+
== Rools - 0.1.2
|
14
24
|
Added RAKEFILE, README, CHANGELOG
|
15
25
|
|
16
|
-
- 0.1.1
|
26
|
+
== Rools - 0.1.1
|
17
27
|
Added RuleConsequenceError
|
data/RAKEFILE
CHANGED
@@ -1,35 +1,77 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'rake'
|
3
|
-
require 'rake/
|
3
|
+
require 'rake/clean'
|
4
|
+
require 'rake/packagetask'
|
4
5
|
require 'rake/gempackagetask'
|
6
|
+
require 'rake/rdoctask'
|
7
|
+
require 'rake/testtask'
|
8
|
+
|
5
9
|
require 'rake/contrib/rubyforgepublisher'
|
6
|
-
require 'pscp'
|
7
10
|
|
8
|
-
|
11
|
+
#require 'pscp'
|
12
|
+
require 'rote'
|
13
|
+
require 'rote/filters'
|
14
|
+
require 'rote/filters/redcloth'
|
15
|
+
require 'rote/filters/tidy'
|
16
|
+
require 'rote/format/html'
|
17
|
+
require 'rote/extratasks'
|
18
|
+
|
19
|
+
include Rote
|
20
|
+
|
21
|
+
load 'lib/rools/version.rb'
|
22
|
+
|
23
|
+
PACKAGE_VERSION = Rools::ROOLS_VERSION
|
24
|
+
|
25
|
+
CLEAN.include("pkg", "html", "rdoc", "engine.log")
|
9
26
|
|
10
27
|
PACKAGE_FILES = FileList[
|
11
28
|
'README',
|
12
29
|
'CHANGELOG',
|
13
30
|
'RAKEFILE',
|
31
|
+
'lib/rools.rb',
|
14
32
|
'lib/**/*.rb'
|
15
33
|
].to_a
|
16
34
|
|
17
35
|
PROJECT = 'rools'
|
18
36
|
|
19
|
-
ENV['RUBYFORGE_USER']
|
37
|
+
ENV['RUBYFORGE_USER'] = "cappelaere@rubyforge.org"
|
20
38
|
ENV['RUBYFORGE_PROJECT'] = "/var/www/gforge-projects/#{PROJECT}"
|
21
39
|
|
22
40
|
desc 'Release Files'
|
23
|
-
task :default => [:rdoc]
|
41
|
+
task :default => [:rdoc, :doc, :gem]
|
24
42
|
|
25
43
|
# Generate the RDoc documentation
|
26
44
|
rd = Rake::RDocTask.new do |rdoc|
|
27
|
-
rdoc.rdoc_dir = 'doc'
|
45
|
+
rdoc.rdoc_dir = 'html/doc'
|
28
46
|
rdoc.title = 'Rools -- A Pure Ruby Rules Engine'
|
29
|
-
rdoc.options << '--line-numbers --inline-source --main README'
|
30
47
|
rdoc.rdoc_files.include(PACKAGE_FILES)
|
48
|
+
rdoc.options << '-N'
|
49
|
+
rdoc.options << '-S'
|
50
|
+
end
|
51
|
+
|
52
|
+
# Create a task to build the static docs (html)
|
53
|
+
ws = Rote::DocTask.new(:doc) do |site|
|
54
|
+
site.output_dir = 'html'
|
55
|
+
site.layout_dir = 'doc/layouts'
|
56
|
+
site.pages.dir = 'doc/pages'
|
57
|
+
site.pages.include('**/*')
|
58
|
+
|
59
|
+
site.ext_mapping(/thtml|textile/, 'html') do |page|
|
60
|
+
page.extend Format::HTML
|
61
|
+
page.page_filter Filters::RedCloth.new
|
62
|
+
page.page_filter Filters::Syntax.new
|
63
|
+
end
|
64
|
+
|
65
|
+
site.res.dir = 'doc/res'
|
66
|
+
site.res.include('**/*.png')
|
67
|
+
site.res.include('**/*.gif')
|
68
|
+
site.res.include('**/*.jpg')
|
69
|
+
site.res.include('**/*.css')
|
31
70
|
end
|
32
71
|
|
72
|
+
# Add rdoc deps to doc task
|
73
|
+
task :doc => [:rdoc]
|
74
|
+
|
33
75
|
gem_spec = Gem::Specification.new do |s|
|
34
76
|
s.platform = Gem::Platform::RUBY
|
35
77
|
s.name = PROJECT
|
@@ -37,10 +79,10 @@ gem_spec = Gem::Specification.new do |s|
|
|
37
79
|
s.description = "Can be used for program-flow, ideally suited to processing applications"
|
38
80
|
s.version = PACKAGE_VERSION
|
39
81
|
|
40
|
-
s.authors = 'Sam Smoot', 'Scott Bauer'
|
41
|
-
s.email = 'ssmoot@gmail.com; bauer.mail@gmail.com'
|
82
|
+
s.authors = 'Sam Smoot', 'Scott Bauer', 'Pat Cappelaere'
|
83
|
+
s.email = 'ssmoot@gmail.com; bauer.mail@gmail.com cappelaere@gmail.com'
|
42
84
|
s.rubyforge_project = PROJECT
|
43
|
-
s.homepage = 'http://
|
85
|
+
s.homepage = 'http://rools.rubyforge.org/'
|
44
86
|
|
45
87
|
s.files = PACKAGE_FILES
|
46
88
|
|
@@ -53,13 +95,79 @@ gem_spec = Gem::Specification.new do |s|
|
|
53
95
|
s.extra_rdoc_files = rd.rdoc_files.reject { |fn| fn =~ /\.rb$/ }.to_a
|
54
96
|
end
|
55
97
|
|
98
|
+
#
|
99
|
+
# Create a task for creating a ruby gem
|
100
|
+
#
|
56
101
|
Rake::GemPackageTask.new(gem_spec) do |p|
|
57
102
|
p.gem_spec = gem_spec
|
58
103
|
p.need_tar = true
|
59
104
|
p.need_zip = true
|
60
105
|
end
|
61
106
|
|
107
|
+
#
|
108
|
+
# Packaging the source
|
109
|
+
#
|
110
|
+
Rake::PackageTask.new("rools", Rools::ROOLS_VERSION) do |pkg|
|
111
|
+
pkg.need_zip = true
|
112
|
+
pkg.package_files = FileList[
|
113
|
+
"RAKEFILE",
|
114
|
+
"CHANGELOG",
|
115
|
+
"README",
|
116
|
+
"*.txt",
|
117
|
+
"doc/**/*",
|
118
|
+
"examples/**/*",
|
119
|
+
"lib/**/*",
|
120
|
+
"test/**/*",
|
121
|
+
].to_a
|
122
|
+
pkg.package_files.delete("rc.txt")
|
123
|
+
pkg.package_files.delete("MISC.txt")
|
124
|
+
class << pkg
|
125
|
+
def package_name
|
126
|
+
"#{@name}-#{@version}-src"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
62
131
|
desc "Publish RDOC to RubyForge"
|
63
132
|
task :rubyforge => [:rdoc, :gem] do
|
64
|
-
Rake::SshDirPublisher.new(ENV['RUBYFORGE_USER'], ENV['RUBYFORGE_PROJECT'], '
|
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
|
135
|
+
end
|
136
|
+
|
137
|
+
# Builds the website and uploads it to Rubyforge.org
|
138
|
+
task :upload_website => [:doc] do
|
139
|
+
sh """
|
140
|
+
rsync -azv -e ssh \
|
141
|
+
html/ \
|
142
|
+
ENV['RUBYFORGE_USER']:ENV['RUBYFORGE_PROJECT']
|
143
|
+
"""
|
144
|
+
sh """
|
145
|
+
rsync -azv -e ssh \
|
146
|
+
--exclude='.svn' --delete-excluded \
|
147
|
+
doc/res/defs \
|
148
|
+
ENV['RUBYFORGE_USER']:ENV['RUBYFORGE_PROJECT']
|
149
|
+
"""
|
150
|
+
sh """
|
151
|
+
rsync -azv -e ssh \
|
152
|
+
--exclude='.svn' --delete-excluded \
|
153
|
+
examples \
|
154
|
+
ENV['RUBYFORGE_USER']:ENV['RUBYFORGE_PROJECT']
|
155
|
+
"""
|
156
|
+
end
|
157
|
+
|
158
|
+
#
|
159
|
+
# TEST TASKS
|
160
|
+
#
|
161
|
+
class RoolsTestTask < Rake::TestTask
|
162
|
+
def initialize (name=nil)
|
163
|
+
File.delete "engine.log" if File.exist? "engine.log"
|
164
|
+
super(name)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Create a task for handling Unit Tests
|
169
|
+
RoolsTestTask.new(:test) do |t|
|
170
|
+
t.libs << "test"
|
171
|
+
t.test_files = FileList['test/rake_test.rb']
|
172
|
+
t.verbose = true
|
65
173
|
end
|
data/README
CHANGED
@@ -3,36 +3,35 @@
|
|
3
3
|
|
4
4
|
Rools is a rules engine for abstracting business logic and program-flow. It's ideally suited to processing applications where the business logic undergoes frequent modification.
|
5
5
|
|
6
|
+
== RubyForge
|
7
|
+
This documentation can be found at http://rools.rubyforge.org
|
8
|
+
The project page can be found at http://rubyforge.org/projects/rools
|
9
|
+
|
6
10
|
== Example
|
7
11
|
|
8
12
|
require 'rools'
|
9
13
|
|
10
14
|
rules = Rools::RuleSet.new do
|
11
15
|
|
12
|
-
rule '
|
16
|
+
rule 'Hello?' do
|
13
17
|
parameter String
|
14
|
-
consequence { puts "
|
15
|
-
end
|
16
|
-
|
17
|
-
rule 'Is it a country?' do
|
18
|
-
condition { Country.find_all.collect { |c| c.name }.include?(string) }
|
19
|
-
consequence { puts "Yes, #{string} is in the country list"}
|
18
|
+
consequence { puts "Hello, Rools!" }
|
20
19
|
end
|
21
20
|
end
|
22
21
|
|
23
|
-
rules.assert '
|
22
|
+
rules.assert 'Heya'
|
24
23
|
|
25
|
-
>
|
26
|
-
|
24
|
+
> Hello, Rools!
|
25
|
+
|
27
26
|
|
28
|
-
You can also store your rules in a
|
27
|
+
You can also store your rules in a separate file, and pass a path to Rools::RuleSet#new instead of a block. e.g.
|
29
28
|
|
30
29
|
require 'rools'
|
31
30
|
|
32
|
-
rules = Rools::RuleSet.new '
|
33
|
-
rules.assert '
|
34
|
-
|
35
|
-
|
31
|
+
rules = Rools::RuleSet.new 'test/data/hello.rules'
|
32
|
+
rules.assert 'heya'
|
33
|
+
|
34
|
+
=== Parameter
|
36
35
|
|
37
36
|
The +parameter+ method accepts Constants and/or Symbols. Every constant in the list is called with :is_a? on the asserted object, while every symbol in the list is passed to :respond_to? on the asserted object. In other words:
|
38
37
|
|
@@ -42,9 +41,9 @@ Is effectively the same as:
|
|
42
41
|
|
43
42
|
condition { object.is_a?(Person) && object.responds_to?(:name) && object.responds_to?(:occupation) }
|
44
43
|
|
45
|
-
The +parameter+ method is obviously preferred for it's conciseness, and because the working set of rules can be optimized to only include for evaluation
|
44
|
+
The +parameter+ method is obviously preferred for it's conciseness, and because the working set of rules can be optimized to only include for evaluation those rules whose parameters match the asserted object.
|
46
45
|
|
47
|
-
|
46
|
+
=== Condition
|
48
47
|
|
49
48
|
The +condition+ method is used to evaluate the asserted object. You can have any number of conditions. Rools::DefaultParameterProc#method_missing is used so that you can refer to the asserted object by practically any +lower_case_underscore+ name. Generally you'll want to use names that make sense in the context of the +Rule+, and be consistent through-out the +Rule+. Here's an example:
|
50
49
|
|
@@ -62,7 +61,7 @@ Here's an example of something you might want to avoid:
|
|
62
61
|
|
63
62
|
Both examples are syntactically correct, but the first is easier to read.
|
64
63
|
|
65
|
-
|
64
|
+
=== Consequence
|
66
65
|
|
67
66
|
A consequence is a block of code that executes if the conditions evaluate to true. You can have one or more consequences. In the above examples we're doing something simple such as just printing something to the string in the consequences. Usually the consequence is something that will actually change the state of the asserted object in some way however.
|
68
67
|
|
@@ -74,7 +73,7 @@ A consequence is a block of code that executes if the conditions evaluate to tru
|
|
74
73
|
|
75
74
|
Nevermind that this is application logic you'd probably keep in your domain model. The intent isn't to show you best-practices here, only to make the point that consequences usually modify state.
|
76
75
|
|
77
|
-
|
76
|
+
=== Assert
|
78
77
|
|
79
78
|
What happens if in a +consequence+, you need to create other objects though? You can use the +assert+ method in a +consequence+. Consider the following:
|
80
79
|
|
@@ -98,7 +97,7 @@ The first rule asserted a new +Referral+ object into the RuleSet. You could also
|
|
98
97
|
|
99
98
|
consequence { RuleSet.new('referral.rules').assert Referral.new(message) }
|
100
99
|
|
101
|
-
|
100
|
+
=== Extend
|
102
101
|
|
103
102
|
Problem: You have a sequence of rules that depend on each other:
|
104
103
|
|
data/lib/rools/base.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
#require 'rubygems'
|
2
|
+
#require_gem 'fastercsv'
|
3
|
+
require 'csv'
|
4
|
+
require 'rools/base'
|
5
|
+
|
6
|
+
module Rools
|
7
|
+
|
8
|
+
class CsvTable < Base
|
9
|
+
attr_reader :rules
|
10
|
+
|
11
|
+
#
|
12
|
+
# return quoted String or Number
|
13
|
+
# There is probably a more elagant way of doing this but...
|
14
|
+
#
|
15
|
+
def quote( str )
|
16
|
+
return str if (str.to_i.to_s == str)
|
17
|
+
return str if (str.to_f.to_s == str)
|
18
|
+
'"' + str + '"'
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize( fileName )
|
22
|
+
csv_data = IO.read( fileName)
|
23
|
+
arrs = []
|
24
|
+
CSV::Reader.parse(csv_data, ",", "\r") do |row|
|
25
|
+
#puts "row:#{row.inspect}"
|
26
|
+
arrs << row
|
27
|
+
end
|
28
|
+
|
29
|
+
# get rule parameter
|
30
|
+
parameter = arrs[1][1]
|
31
|
+
#puts "parameter:#{parameter}"
|
32
|
+
|
33
|
+
# get rule elements
|
34
|
+
rule_elements = arrs[2]
|
35
|
+
|
36
|
+
# get code
|
37
|
+
rule_code = arrs[3]
|
38
|
+
|
39
|
+
# get headers
|
40
|
+
headers = arrs[4]
|
41
|
+
|
42
|
+
#get number of rules
|
43
|
+
num_rules = arrs.size-5
|
44
|
+
#puts "num rules: #{num_rules}"
|
45
|
+
|
46
|
+
index = 0
|
47
|
+
@rules = ""
|
48
|
+
arrs[5..arrs.size].each { |arr|
|
49
|
+
rule_name = "rule_#{index}"
|
50
|
+
#puts "arr:#{arr}"
|
51
|
+
|
52
|
+
@rules << "rule '#{rule_name}' do \n"
|
53
|
+
@rules << " parameter #{parameter}\n"
|
54
|
+
column = 0
|
55
|
+
rule_elements.each do |element|
|
56
|
+
|
57
|
+
field = headers[column].downcase
|
58
|
+
str = arr[column]
|
59
|
+
|
60
|
+
if str != nil
|
61
|
+
#puts ("eval: #{field} = '#{str}'")
|
62
|
+
#eval( "#{field} = '#{str}'" )
|
63
|
+
|
64
|
+
@rules << "\t" + element.downcase + "{ "
|
65
|
+
pattern = "\#\{#{field}\}"
|
66
|
+
|
67
|
+
statement = rule_code[column].gsub(pattern,quote(str))
|
68
|
+
|
69
|
+
#puts "statement:#{statement}"
|
70
|
+
|
71
|
+
@rules << statement
|
72
|
+
|
73
|
+
@rules << "}\n"
|
74
|
+
end
|
75
|
+
column += 1
|
76
|
+
end
|
77
|
+
@rules << "end\n"
|
78
|
+
index += 1
|
79
|
+
}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
|
85
|
+
end
|
@@ -1,9 +1,10 @@
|
|
1
|
+
require 'rools/base'
|
1
2
|
module Rools
|
2
3
|
|
3
4
|
# The DefaultParameterProc binds to a Rule and
|
4
5
|
# is allows the block to use method_missing to
|
5
6
|
# refer to the asserted object.
|
6
|
-
class DefaultParameterProc
|
7
|
+
class DefaultParameterProc < Base
|
7
8
|
|
8
9
|
# Determines whether a method is vital to the functionality
|
9
10
|
# of the class.
|
@@ -44,7 +45,21 @@ module Rools
|
|
44
45
|
# Parameterless method calls by the attached block are assumed to
|
45
46
|
# be references to the working object
|
46
47
|
def method_missing(sym, *args)
|
48
|
+
# puts "method missing: #{sym}"
|
49
|
+
# check if it is a fact first
|
50
|
+
begin
|
51
|
+
facts = @rule.rule_set.get_facts
|
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
|
55
|
+
else
|
56
|
+
#puts "#{sym} not in facts"
|
57
|
+
end
|
58
|
+
rescue Exception => e
|
59
|
+
logger.error "miss exception #{e} #{e.backtrace.join("\n")}" if logger
|
60
|
+
end
|
47
61
|
return @working_object if @working_object && args.size == 0
|
62
|
+
return nil
|
48
63
|
end
|
49
64
|
|
50
65
|
# Stops the current assertion. Does not indicate failure.
|
data/lib/rools/facts.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rools/errors'
|
2
|
+
require 'rools/default_parameter_proc'
|
3
|
+
require 'rools/base'
|
4
|
+
|
5
|
+
module Rools
|
6
|
+
class Facts < Base
|
7
|
+
attr_reader :name, :fact_value
|
8
|
+
|
9
|
+
def initialize(rule_set, name, b)
|
10
|
+
@name = name
|
11
|
+
@fact_value = instance_eval( &b )
|
12
|
+
logger.debug "New Facts: #{@fact_value}" if logger
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/rools/rule.rb
CHANGED
@@ -1,21 +1,23 @@
|
|
1
1
|
require 'rools/errors'
|
2
2
|
require 'rools/default_parameter_proc'
|
3
|
+
require 'rools/base'
|
3
4
|
|
4
5
|
module Rools
|
5
|
-
class Rule
|
6
|
-
attr_reader :name
|
6
|
+
class Rule < Base
|
7
|
+
attr_reader :name, :priority, :rule_set
|
8
|
+
|
7
9
|
|
8
10
|
# A Rule requires a Rools::RuleSet, a name, and an associated block
|
9
11
|
# which will be executed at initialization
|
10
|
-
def initialize(rule_set, name, b)
|
12
|
+
def initialize(rule_set, name, priority, b)
|
11
13
|
@rule_set = rule_set
|
12
|
-
@name
|
13
|
-
|
14
|
-
@conditions
|
14
|
+
@name = name
|
15
|
+
@priority = priority
|
16
|
+
@conditions = []
|
15
17
|
@consequences = []
|
16
|
-
@parameters
|
18
|
+
@parameters = []
|
17
19
|
|
18
|
-
instance_eval(&b)
|
20
|
+
instance_eval(&b) if b
|
19
21
|
end
|
20
22
|
|
21
23
|
# Adds a condition to the Rule.
|
@@ -63,9 +65,11 @@ module Rools
|
|
63
65
|
# Checks to see if this Rule's parameters match the asserted object
|
64
66
|
def parameters_match?(obj)
|
65
67
|
@parameters.each do |p|
|
68
|
+
logger.debug( "match p:#{p} obj:#{obj} sym:#{Symbol}") if logger
|
66
69
|
if p.is_a?(Symbol)
|
67
70
|
return false unless obj.respond_to?(p)
|
68
71
|
else
|
72
|
+
logger.debug( "is_a p:#{p} obj:#{obj} #{obj.is_a?(p)}") if logger
|
69
73
|
return false unless obj.is_a?(p)
|
70
74
|
end
|
71
75
|
end
|
@@ -79,6 +83,7 @@ module Rools
|
|
79
83
|
begin
|
80
84
|
@conditions.each { |c| return false unless c.call(obj) }
|
81
85
|
rescue StandardError => e
|
86
|
+
logger.error( "rule StandardError #{e} #{e.backtrace.join("\n")}") if logger
|
82
87
|
raise RuleCheckError.new(self, e)
|
83
88
|
end
|
84
89
|
|
@@ -113,5 +118,9 @@ module Rools
|
|
113
118
|
def fail(message = nil)
|
114
119
|
@rule_set.fail(message)
|
115
120
|
end
|
121
|
+
|
122
|
+
def to_s
|
123
|
+
@name
|
124
|
+
end
|
116
125
|
end
|
117
126
|
end
|
data/lib/rools/rule_set.rb
CHANGED
@@ -1,26 +1,106 @@
|
|
1
1
|
require 'rools/errors'
|
2
2
|
require 'rools/rule'
|
3
|
+
require 'rools/base'
|
4
|
+
require 'rools/facts'
|
5
|
+
require 'rools/csv_table'
|
6
|
+
|
7
|
+
require 'rexml/document'
|
3
8
|
|
4
9
|
module Rools
|
5
|
-
class RuleSet
|
6
|
-
|
10
|
+
class RuleSet < Base
|
11
|
+
attr_reader :num_executed, :num_evaluated, :facts
|
12
|
+
|
7
13
|
PASS = :pass
|
8
14
|
FAIL = :fail
|
9
15
|
|
16
|
+
|
10
17
|
# You can pass a set of Rools::Rules with a block parameter,
|
11
18
|
# or you can pass a file-path to evaluate.
|
12
19
|
def initialize(file = nil, &b)
|
13
20
|
|
14
21
|
@rules = {}
|
22
|
+
@facts = {}
|
15
23
|
@dependencies = {}
|
16
24
|
|
17
25
|
if block_given?
|
18
26
|
instance_eval(&b)
|
19
27
|
else
|
20
|
-
|
21
|
-
|
28
|
+
# loading a file, check extension
|
29
|
+
name,ext = file.split(".")
|
30
|
+
logger.debug("loading ext: #{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 "invalid file extension: #{ext}"
|
46
|
+
end
|
47
|
+
end
|
22
48
|
end
|
23
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
|
+
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
|
89
|
+
}
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Ruby File format
|
95
|
+
#
|
96
|
+
def load_rb( file )
|
97
|
+
instance_eval(File::open(file).read)
|
98
|
+
end
|
99
|
+
|
100
|
+
def get_facts
|
101
|
+
@facts
|
102
|
+
end
|
103
|
+
|
24
104
|
# rule creates a Rools::Rule. Make sure to use a descriptive name or symbol.
|
25
105
|
# For the purposes of extending Rules, all names are converted to
|
26
106
|
# strings and downcased.
|
@@ -29,11 +109,38 @@ module Rools
|
|
29
109
|
# condition { language.name.downcase == 'ruby' }
|
30
110
|
# consequence { "#{language.name} is the best!" }
|
31
111
|
# end
|
32
|
-
def rule(name, &b)
|
112
|
+
def rule(name, priority=0, &b)
|
33
113
|
name.to_s.downcase!
|
34
|
-
@rules[name] = Rule.new(self, name, b)
|
114
|
+
@rules[name] = Rule.new(self, name, priority, b)
|
35
115
|
end
|
36
116
|
|
117
|
+
# facts can be created in a similar manner to rules
|
118
|
+
# all names are converted to strings and downcased.
|
119
|
+
# Facts name is equivalent to a Class Name
|
120
|
+
# ==Example
|
121
|
+
# require 'rools'
|
122
|
+
#
|
123
|
+
# rules = Rools::RuleSet.new do
|
124
|
+
#
|
125
|
+
# facts 'Countries' do
|
126
|
+
# ["China", "USSR", "France", "Great Britain", "USA"]
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
# rule 'Is it on Security Council?' do
|
130
|
+
# parameter String
|
131
|
+
# condition { countries.include?(string) }
|
132
|
+
# consequence { puts "Yes, #{string} is in the country list"}
|
133
|
+
# end
|
134
|
+
# end
|
135
|
+
#
|
136
|
+
# rules.assert 'France'
|
137
|
+
#
|
138
|
+
def facts(name, &b)
|
139
|
+
name.to_s.downcase!
|
140
|
+
@facts[name] = Facts.new(self, name, b)
|
141
|
+
end
|
142
|
+
|
143
|
+
|
37
144
|
# Use in conjunction with Rools::RuleSet#with to create a Rools::Rule dependent on
|
38
145
|
# another. Dependencies are created through names (converted to
|
39
146
|
# strings and downcased), so lax naming can get you into trouble with
|
@@ -72,6 +179,8 @@ module Rools
|
|
72
179
|
def assert(obj)
|
73
180
|
@status = PASS
|
74
181
|
@assert = true
|
182
|
+
@num_executed = 0;
|
183
|
+
@num_evaluated = 0;
|
75
184
|
|
76
185
|
# create a working-set of all parameter-matching, non-dependent rules
|
77
186
|
available_rules = @rules.values.select { |rule| rule.parameters_match?(obj) }
|
@@ -84,12 +193,15 @@ module Rools
|
|
84
193
|
|
85
194
|
# the loop condition is reset to break by default after every iteration
|
86
195
|
matches = false
|
87
|
-
|
196
|
+
#logger.debug("available rules: #{available_rules.size.to_s}") if logger
|
88
197
|
available_rules.each do |rule|
|
89
198
|
# RuleCheckErrors are caught and swallowed and the rule that
|
90
199
|
# raised the error is removed from the working-set.
|
200
|
+
logger.debug("evaluating: #{rule}") if logger
|
91
201
|
begin
|
202
|
+
@num_evaluated += 1
|
92
203
|
if rule.conditions_match?(obj)
|
204
|
+
logger.debug("rule #{rule} matched") if logger
|
93
205
|
matches = true
|
94
206
|
|
95
207
|
# remove the rule from the working-set so it's not re-evaluated
|
@@ -104,7 +216,9 @@ module Rools
|
|
104
216
|
end
|
105
217
|
|
106
218
|
# execute this rule
|
219
|
+
logger.debug("executing rule #{rule}") if logger
|
107
220
|
rule.call(obj)
|
221
|
+
@num_executed += 1
|
108
222
|
|
109
223
|
# break the current iteration and start back from the first rule defined.
|
110
224
|
break
|
@@ -113,6 +227,7 @@ module Rools
|
|
113
227
|
rescue RuleCheckError => e
|
114
228
|
# log da error or sumpin
|
115
229
|
available_rules.delete(e.rule)
|
230
|
+
@status = fail
|
116
231
|
end # begin/rescue
|
117
232
|
|
118
233
|
end # available_rules.each
|
@@ -122,12 +237,13 @@ module Rools
|
|
122
237
|
rescue RuleConsequenceError => rce
|
123
238
|
# RuleConsequenceErrors are allowed to break out of the current assertion,
|
124
239
|
# then the inner error is bubbled-up to the asserting code.
|
240
|
+
@status = fail
|
125
241
|
raise rce.inner_error
|
126
242
|
end
|
127
243
|
|
128
244
|
@assert = false
|
129
245
|
|
130
|
-
return status
|
246
|
+
return @status
|
131
247
|
end # def assert
|
132
248
|
|
133
249
|
end # class RuleSet
|
metadata
CHANGED
@@ -1,53 +1,64 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
|
-
rubygems_version: 0.
|
2
|
+
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:
|
6
|
+
version: 0.1.5
|
7
|
+
date: 2007-04-27 00:00:00 -04:00
|
8
8
|
summary: A Rules Engine written in Ruby
|
9
9
|
require_paths:
|
10
|
-
|
11
|
-
email: ssmoot@gmail.com; bauer.mail@gmail.com
|
12
|
-
homepage: http://
|
10
|
+
- lib
|
11
|
+
email: ssmoot@gmail.com; bauer.mail@gmail.com cappelaere@gmail.com
|
12
|
+
homepage: http://rools.rubyforge.org/
|
13
13
|
rubyforge_project: rools
|
14
|
-
description:
|
14
|
+
description: Can be used for program-flow, ideally suited to processing applications
|
15
15
|
autorequire: rools
|
16
16
|
default_executable:
|
17
17
|
bindir: bin
|
18
18
|
has_rdoc: true
|
19
19
|
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
20
|
requirements:
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
version: 0.0.0
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
25
24
|
version:
|
26
25
|
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
27
29
|
authors:
|
28
|
-
|
29
|
-
|
30
|
+
- Sam Smoot
|
31
|
+
- Scott Bauer
|
32
|
+
- Pat Cappelaere
|
30
33
|
files:
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
34
|
+
- README
|
35
|
+
- CHANGELOG
|
36
|
+
- RAKEFILE
|
37
|
+
- lib/rools.rb
|
38
|
+
- lib/rools/base.rb
|
39
|
+
- lib/rools/csv_table.rb
|
40
|
+
- lib/rools/default_parameter_proc.rb
|
41
|
+
- lib/rools/errors.rb
|
42
|
+
- lib/rools/facts.rb
|
43
|
+
- lib/rools/rule.rb
|
44
|
+
- lib/rools/rule_set.rb
|
45
|
+
- lib/rools/version.rb
|
39
46
|
test_files: []
|
47
|
+
|
40
48
|
rdoc_options:
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
49
|
+
- --line-numbers
|
50
|
+
- --inline-source
|
51
|
+
- --main
|
52
|
+
- README
|
45
53
|
extra_rdoc_files:
|
46
|
-
|
47
|
-
|
48
|
-
|
54
|
+
- README
|
55
|
+
- CHANGELOG
|
56
|
+
- RAKEFILE
|
49
57
|
executables: []
|
58
|
+
|
50
59
|
extensions: []
|
60
|
+
|
51
61
|
requirements:
|
52
|
-
|
53
|
-
dependencies: []
|
62
|
+
- none
|
63
|
+
dependencies: []
|
64
|
+
|