rools 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +9 -0
- data/RAKEFILE +67 -0
- data/README +179 -0
- data/lib/rools.rb +5 -0
- data/lib/rools/default_parameter_proc.rb +51 -0
- data/lib/rools/errors.rb +27 -0
- data/lib/rools/rule.rb +107 -0
- data/lib/rools/rule_set.rb +113 -0
- metadata +58 -0
data/CHANGELOG
ADDED
data/RAKEFILE
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'rake/gempackagetask'
|
5
|
+
require 'rake/contrib/rubyforgepublisher'
|
6
|
+
require 'pscp'
|
7
|
+
|
8
|
+
PACKAGE_VERSION = '0.1.3'
|
9
|
+
|
10
|
+
PACKAGE_FILES = FileList[
|
11
|
+
'README',
|
12
|
+
'CHANGELOG',
|
13
|
+
'RAKEFILE',
|
14
|
+
'lib/rools.rb',
|
15
|
+
'lib/**/*.rb'
|
16
|
+
].to_a
|
17
|
+
|
18
|
+
PROJECT = 'rools'
|
19
|
+
|
20
|
+
ENV['RUBYFORGE_USER'] = "ssmoot@rubyforge.org"
|
21
|
+
ENV['RUBYFORGE_PROJECT'] = "/var/www/gforge-projects/#{PROJECT}"
|
22
|
+
|
23
|
+
desc 'Release Files'
|
24
|
+
task :default => [:rdoc, :gem]
|
25
|
+
|
26
|
+
# Generate the RDoc documentation
|
27
|
+
rd = Rake::RDocTask.new do |rdoc|
|
28
|
+
rdoc.rdoc_dir = 'doc'
|
29
|
+
rdoc.title = 'Rools -- A Pure Ruby Rules Engine'
|
30
|
+
rdoc.options << '--line-numbers --inline-source --main README'
|
31
|
+
rdoc.rdoc_files.include(PACKAGE_FILES)
|
32
|
+
end
|
33
|
+
|
34
|
+
gem_spec = Gem::Specification.new do |s|
|
35
|
+
s.platform = Gem::Platform::RUBY
|
36
|
+
s.name = PROJECT
|
37
|
+
s.summary = "A Rules Engine written in Ruby"
|
38
|
+
s.description = "Can be used for program-flow, ideally suited to processing applications"
|
39
|
+
s.version = PACKAGE_VERSION
|
40
|
+
|
41
|
+
s.authors = 'Sam Smoot', 'Scott Bauer'
|
42
|
+
s.email = 'ssmoot@gmail.com; bauer.mail@gmail.com'
|
43
|
+
s.rubyforge_project = PROJECT
|
44
|
+
s.homepage = 'http://substantiality.net'
|
45
|
+
|
46
|
+
s.files = PACKAGE_FILES
|
47
|
+
|
48
|
+
s.require_path = 'lib'
|
49
|
+
s.requirements << 'none'
|
50
|
+
s.autorequire = 'rools'
|
51
|
+
|
52
|
+
s.has_rdoc = true
|
53
|
+
s.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README'
|
54
|
+
s.extra_rdoc_files = rd.rdoc_files.reject { |fn| fn =~ /\.rb$/ }.to_a
|
55
|
+
end
|
56
|
+
|
57
|
+
Rake::GemPackageTask.new(gem_spec) do |p|
|
58
|
+
p.gem_spec = gem_spec
|
59
|
+
p.need_tar = true
|
60
|
+
p.need_zip = true
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "Publish RDOC to RubyForge"
|
64
|
+
task :rubyforge => [:rdoc, :gem] do
|
65
|
+
Rake::SshDirPublisher.new(ENV['RUBYFORGE_USER'], ENV['RUBYFORGE_PROJECT'], 'doc').upload
|
66
|
+
Rake::SshFilePublisher.new(ENV['RUBYFORGE_USER'], ENV['RUBYFORGE_PROJECT'], 'pkg', "#{PROJECT}-#{PACKAGE_VERSION}.gem").upload
|
67
|
+
end
|
data/README
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
= rools -- A pure-ruby rules engine
|
2
|
+
|
3
|
+
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.
|
4
|
+
|
5
|
+
== Example
|
6
|
+
|
7
|
+
require 'rools'
|
8
|
+
|
9
|
+
rules = Rools::RuleSet.new do
|
10
|
+
|
11
|
+
rule 'Is it a String?' do
|
12
|
+
parameter String
|
13
|
+
consequence { puts "Yes, it's a String" }
|
14
|
+
end
|
15
|
+
|
16
|
+
rule 'Is it a country?' do
|
17
|
+
condition { Country.find_all.collect { |c| c.name }.include?(string) }
|
18
|
+
consequence { puts "Yes, #{string} is in the country list"}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
rules.assert 'Japan'
|
23
|
+
|
24
|
+
> Yes, it's a String
|
25
|
+
> Yes, Japan is in the country list
|
26
|
+
|
27
|
+
You can also store your rules in a seperate file, and pass a path to Rools::RuleSet#new instead of a block. e.g.
|
28
|
+
|
29
|
+
require 'rools'
|
30
|
+
|
31
|
+
rules = Rools::RuleSet.new 'countries.rules'
|
32
|
+
rules.assert 'Japan'
|
33
|
+
|
34
|
+
== Parameter
|
35
|
+
|
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:
|
37
|
+
|
38
|
+
parameter Person, :name, :occupation
|
39
|
+
|
40
|
+
Is effectively the same as:
|
41
|
+
|
42
|
+
condition { object.is_a?(Person) && object.responds_to?(:name) && object.responds_to?(:occupation) }
|
43
|
+
|
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 (to-do), those rules whose parameters match the asserted object.
|
45
|
+
|
46
|
+
== Condition
|
47
|
+
|
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:
|
49
|
+
|
50
|
+
rule 'Programmer' do
|
51
|
+
condition { person.occupation == 'coder' }
|
52
|
+
consequence { puts "#{person} is a coder" }
|
53
|
+
end
|
54
|
+
|
55
|
+
Here's an example of something you might want to avoid:
|
56
|
+
|
57
|
+
rule 'Manager' do
|
58
|
+
condition { obj.occupation == 'manager' }
|
59
|
+
consequence { puts "#{manager} is a manager" }
|
60
|
+
end
|
61
|
+
|
62
|
+
Both examples are syntactically correct, but the first is easier to read.
|
63
|
+
|
64
|
+
== Consequence
|
65
|
+
|
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.
|
67
|
+
|
68
|
+
rule 'User' do
|
69
|
+
parameter User, :failed_login_attempts
|
70
|
+
condition { user.failed_login_attempts > 3 }
|
71
|
+
consequence { user.lockout! }
|
72
|
+
end
|
73
|
+
|
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.
|
75
|
+
|
76
|
+
== Assert
|
77
|
+
|
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:
|
79
|
+
|
80
|
+
rule 'Message is Referral?' do
|
81
|
+
parameter :subject, :body
|
82
|
+
|
83
|
+
condition { message.subject == 'Sales Referral' }
|
84
|
+
condition { message.body =~ /^How\sdid\syou\shear\sabout\sbrand\sX\?/m }
|
85
|
+
|
86
|
+
consequence { assert Referral.new(message) }
|
87
|
+
end
|
88
|
+
|
89
|
+
rule 'Referral is Large Account' do
|
90
|
+
parameter Referral
|
91
|
+
|
92
|
+
condition { referral.annual_sales_volume > 100_000_000 }
|
93
|
+
consequence { referral.prioritize :high }
|
94
|
+
end
|
95
|
+
|
96
|
+
The first rule asserted a new +Referral+ object into the RuleSet. You could also assert into a completey different RuleSet however if you need to split them up to keep them manageable:
|
97
|
+
|
98
|
+
consequence { RuleSet.new('referral.rules').assert Referral.new(message) }
|
99
|
+
|
100
|
+
== Extend
|
101
|
+
|
102
|
+
Problem: You have a sequence of rules that depend on each other:
|
103
|
+
|
104
|
+
rule 'Valid User' do
|
105
|
+
condition { user.valid? }
|
106
|
+
condition { user.password.size > 6 }
|
107
|
+
consequence { puts "#{user} is valid" }
|
108
|
+
end
|
109
|
+
|
110
|
+
rule 'Active User' do
|
111
|
+
condition { user.valid? }
|
112
|
+
condition { user.password.size > 6 }
|
113
|
+
condition { user.last_logon < 1.month.ago }
|
114
|
+
condition do
|
115
|
+
user.logins_for(:january).inject(0) do |sum, duration|
|
116
|
+
sum += duration
|
117
|
+
end > 5.minutes
|
118
|
+
end
|
119
|
+
|
120
|
+
consequence { puts "#{user} is active" }
|
121
|
+
end
|
122
|
+
|
123
|
+
rule 'Administrative User' do
|
124
|
+
condition { user.valid? }
|
125
|
+
condition { user.password.size > 6 }
|
126
|
+
condition { user.last_logon < 1.month.ago }
|
127
|
+
condition do
|
128
|
+
user.logins_for(:january).inject(0) do |sum, duration|
|
129
|
+
sum += duration
|
130
|
+
end > 5.minutes
|
131
|
+
end
|
132
|
+
condition { user.admin? }
|
133
|
+
|
134
|
+
consequence { puts "#{user} is an admin" }
|
135
|
+
end
|
136
|
+
|
137
|
+
This chain of rules quickly becomes rather cumbersome. To help, you can use the +extend+ -> +with+ syntax to enable chaining and branching rules. The same example as above could also be written this way: (extend rule_name with new_rule_name)
|
138
|
+
|
139
|
+
rule 'Valid User' do
|
140
|
+
condition { user.valid? }
|
141
|
+
condition { user.password.size > 6 }
|
142
|
+
consequence { puts "#{user} is valid" }
|
143
|
+
end
|
144
|
+
|
145
|
+
extend('Valid User').with('Active User') do
|
146
|
+
condition { user.last_logon < 1.month.ago }
|
147
|
+
condition do
|
148
|
+
user.logins_for(:january).inject(0) do |sum, duration|
|
149
|
+
sum += duration
|
150
|
+
end > 5.minutes
|
151
|
+
end
|
152
|
+
|
153
|
+
consequence { puts "#{user} is active" }
|
154
|
+
end
|
155
|
+
|
156
|
+
extend('Active User').with('Administrative User') do
|
157
|
+
condition { user.admin? }
|
158
|
+
consequence { puts "#{user} is an admin" }
|
159
|
+
end
|
160
|
+
|
161
|
+
In this simplistic example, it saves 11 lines (about a third), but in more complex examples you could easily end up with half the code. The first example could perform up to 11 condition checks for a valid, active, admin user. Because the extend syntax defers adding the dependent rules to the working set until the extended rule evaluates to true, the extend syntax would do the same work with only 5 comparisons (less than half the work). For expensive operations, such as file operations, regular expressions, or database calls, the extend syntax is potentially several times faster than the "brute-force" method, and the difference only becomes greater the larger your RuleSet.
|
162
|
+
|
163
|
+
It's important to remember that you can only extend rules within the same Rools::RuleSet.
|
164
|
+
|
165
|
+
You can also +extend+ a target rule +with+ several additional rules at once with a block:
|
166
|
+
|
167
|
+
extend('Active User') do
|
168
|
+
with 'Administrative User' do
|
169
|
+
condition { user.admin? }
|
170
|
+
consequence { puts "#{user} is an admin" }
|
171
|
+
end
|
172
|
+
|
173
|
+
with 'Customer User' do
|
174
|
+
condition { user.customer? }
|
175
|
+
consequence { puts "#{user} is a customer" }
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
Rule names are case-insensitive. It's important to give good descriptive names to rules as not only does it make maintaining them much easier, but it also gives you better logging, and makes +extend+ calls easier to follow.
|
data/lib/rools.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Rools
|
2
|
+
|
3
|
+
# The DefaultParameterProc binds to a Rule and
|
4
|
+
# is allows the block to use method_missing to
|
5
|
+
# refer to the asserted object.
|
6
|
+
class DefaultParameterProc
|
7
|
+
|
8
|
+
# Determines whether a method is vital to the functionality
|
9
|
+
# of the class.
|
10
|
+
def self.is_vital(method)
|
11
|
+
return method =~ /__(.+)__|method_missing|instance_eval/
|
12
|
+
end
|
13
|
+
|
14
|
+
# Removes unnecessary methods from the class to minimize
|
15
|
+
# name collisions with method_missing.
|
16
|
+
for method in instance_methods
|
17
|
+
undef_method(method) unless is_vital(method)
|
18
|
+
end
|
19
|
+
|
20
|
+
# The "rule" parameter must respond to an :assert method.
|
21
|
+
# The "b" parameter is a block that will be rebound to this
|
22
|
+
# instance.
|
23
|
+
def initialize(rule, b)
|
24
|
+
raise ArgumentError.new('The "rule" parameter must respond to an :assert method') unless rule.respond_to?(:assert)
|
25
|
+
@rule = rule
|
26
|
+
@proc = b
|
27
|
+
@working_object = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
# Call the bound block and set the working object so that it
|
31
|
+
# can be referred to by method_missing
|
32
|
+
def call(obj)
|
33
|
+
@working_object = obj
|
34
|
+
status = instance_eval(&@proc)
|
35
|
+
@working_object = nil
|
36
|
+
return status
|
37
|
+
end
|
38
|
+
|
39
|
+
# Assert a new object up the composition-chain into the current RuleSet
|
40
|
+
def assert(obj)
|
41
|
+
@rule.assert(obj)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Parameterless method calls by the attached block are assumed to
|
45
|
+
# be references to the working object
|
46
|
+
def method_missing(sym, *args)
|
47
|
+
return @working_object if @working_object && args.size == 0
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
data/lib/rools/errors.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Rools
|
2
|
+
|
3
|
+
class RuleError < StandardError
|
4
|
+
attr_reader :rule, :inner_error
|
5
|
+
|
6
|
+
# Pass the Rools::Rule that caused the error, and an optional inner_error
|
7
|
+
def initialize(rule, inner_error = nil)
|
8
|
+
@rule = rule
|
9
|
+
@inner_error = inner_error
|
10
|
+
end
|
11
|
+
|
12
|
+
# returns the name of the associated Rools::Rule, and the message of the inner_error
|
13
|
+
def to_s
|
14
|
+
"#{@rule.name}\n#{@inner_error.to_s}"
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
# See: Rools::RuleError (RuleCheckError is only a default derivation)
|
20
|
+
class RuleCheckError < RuleError
|
21
|
+
end
|
22
|
+
|
23
|
+
# See: Rools::RuleError (RuleConsequenceError is only a default derivation)
|
24
|
+
class RuleConsequenceError < RuleError
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
data/lib/rools/rule.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'rools/errors'
|
2
|
+
require 'rools/default_parameter_proc'
|
3
|
+
|
4
|
+
module Rools
|
5
|
+
class Rule
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
# A Rule requires a Rools::RuleSet, a name, and an associated block
|
9
|
+
# which will be executed at initialization
|
10
|
+
def initialize(rule_set, name, b)
|
11
|
+
@rule_set = rule_set
|
12
|
+
@name = name
|
13
|
+
|
14
|
+
@conditions = []
|
15
|
+
@consequences = []
|
16
|
+
@parameters = []
|
17
|
+
|
18
|
+
instance_eval(&b)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Adds a condition to the Rule.
|
22
|
+
# For readability, it might be preferrable to use a
|
23
|
+
# new condition for every evaluation when possible.
|
24
|
+
# ==Example
|
25
|
+
# condition { person.name == 'Fred' }
|
26
|
+
# condition { person.age > 18 }
|
27
|
+
# As opposed to:
|
28
|
+
# condition { person.name == 'Fred' && person.age > 18 }
|
29
|
+
def condition(&b)
|
30
|
+
@conditions << DefaultParameterProc.new(self, b)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Adds a consequence to the Rule
|
34
|
+
# ==Example
|
35
|
+
# consequence { user.failed_logins += 1; user.save! }
|
36
|
+
def consequence(&b)
|
37
|
+
@consequences << DefaultParameterProc.new(self, b)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Sets the parameters of the Rule
|
41
|
+
# ==Example
|
42
|
+
# parameters Person, :name, :occupation
|
43
|
+
# The arguments passed are evaluated with :is_a? for each
|
44
|
+
# constant-name passed, and :responds_to? for each symbol.
|
45
|
+
# So the above example would be the same as:
|
46
|
+
# obj.is_a?(Person) &&
|
47
|
+
# obj.responds_to?(:name) &&
|
48
|
+
# obj.responds_to?(:occupation)
|
49
|
+
# You can pass any combination of symbols or constants.
|
50
|
+
# You might want to refrain from referencing constants to
|
51
|
+
# aid in "duck-typing", or you might want to use parameters such as:
|
52
|
+
# parameters Person, Employee, :department
|
53
|
+
# To verify that the asserted object is an Employee, that inherits from
|
54
|
+
# Person, and responds to :department
|
55
|
+
def parameters(*matches)
|
56
|
+
@parameters += matches
|
57
|
+
end
|
58
|
+
|
59
|
+
# parameters is aliased to aid in readability if you decide
|
60
|
+
# to pass only one parameter.
|
61
|
+
alias :parameter :parameters
|
62
|
+
|
63
|
+
# Checks to see if this Rule's parameters match the asserted object
|
64
|
+
def parameters_match?(obj)
|
65
|
+
@parameters.each do |p|
|
66
|
+
if p.is_a?(Symbol)
|
67
|
+
return false unless obj.respond_to?(p)
|
68
|
+
else
|
69
|
+
return false unless obj.is_a?(p)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
return true
|
74
|
+
end
|
75
|
+
|
76
|
+
# Checks to see if this Rule's conditions match the asserted object
|
77
|
+
# Any StandardErrors are caught and wrapped in RuleCheckErrors, then raised.
|
78
|
+
def conditions_match?(obj)
|
79
|
+
begin
|
80
|
+
@conditions.each { |c| return false unless c.call(obj) }
|
81
|
+
rescue StandardError => e
|
82
|
+
raise RuleCheckError.new(self, e)
|
83
|
+
end
|
84
|
+
|
85
|
+
return true
|
86
|
+
end
|
87
|
+
|
88
|
+
# Calls Rools::RuleSet#assert in the bound working-set
|
89
|
+
def assert(obj)
|
90
|
+
@rule_set.assert(obj)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Execute each consequence.
|
94
|
+
# Any StandardErrors are caught and wrapped in Rools::RuleConsequenceError,
|
95
|
+
# then raised to break out of the current assertion.
|
96
|
+
def call(obj)
|
97
|
+
begin
|
98
|
+
@consequences.each do |c|
|
99
|
+
c.call(obj)
|
100
|
+
end
|
101
|
+
rescue StandardError => e
|
102
|
+
# discontinue the Rools::RuleSet#assert if any consequence fails
|
103
|
+
raise RuleConsequenceError.new(rule, e)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'rools/errors'
|
2
|
+
require 'rools/rule'
|
3
|
+
|
4
|
+
module Rools
|
5
|
+
class RuleSet
|
6
|
+
# You can pass a set of Rools::Rules with a block parameter,
|
7
|
+
# or you can pass a file-path to evaluate.
|
8
|
+
def initialize(file = nil, &b)
|
9
|
+
|
10
|
+
@rules = {}
|
11
|
+
@dependencies = {}
|
12
|
+
|
13
|
+
if block_given?
|
14
|
+
instance_eval(&b)
|
15
|
+
else
|
16
|
+
instance_eval(File::open(file).read)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# rule creates a Rools::Rule. Make sure to use a descriptive name or symbol.
|
21
|
+
# For the purposes of extending Rules, all names are converted to
|
22
|
+
# strings and downcased.
|
23
|
+
# ==Example
|
24
|
+
# rule 'ruby is the best' do
|
25
|
+
# condition { language.name.downcase == 'ruby' }
|
26
|
+
# consequence { "#{language.name} is the best!" }
|
27
|
+
# end
|
28
|
+
def rule(name, &b)
|
29
|
+
name.to_s.downcase!
|
30
|
+
@rules[name] = Rule.new(self, name, b)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Use in conjunction with Rools::RuleSet#with to create a Rools::Rule dependent on
|
34
|
+
# another. Dependencies are created through names (converted to
|
35
|
+
# strings and downcased), so lax naming can get you into trouble with
|
36
|
+
# creating dependencies or overwriting rules you didn't mean to.
|
37
|
+
def extend(name, &b)
|
38
|
+
name.to_s.downcase!
|
39
|
+
@extend_rule_name = name
|
40
|
+
instance_eval(&b) if block_given?
|
41
|
+
return self
|
42
|
+
end
|
43
|
+
|
44
|
+
# Used in conjunction with Rools::RuleSet#extend to create a dependent Rools::Rule
|
45
|
+
# ==Example
|
46
|
+
# extend('ruby is the best').with('ruby rules the world') do
|
47
|
+
# condition { language.age > 15 }
|
48
|
+
# consequence { "In the year 2008 Ruby conquered the known universe" }
|
49
|
+
# end
|
50
|
+
def with(name, &b)
|
51
|
+
name.to_s.downcase!
|
52
|
+
(@dependencies[@extend_rule_name] ||= []) << Rule.new(self, name, b)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Used to create a working-set of rules for an object, and evaluate it
|
56
|
+
# against them.
|
57
|
+
def assert(obj)
|
58
|
+
|
59
|
+
# create a working-set of all parameter-matching, non-dependent rules
|
60
|
+
available_rules = @rules.values.select { |rule| rule.parameters_match?(obj) }
|
61
|
+
|
62
|
+
begin
|
63
|
+
|
64
|
+
# loop through the available_rules, evaluating each one,
|
65
|
+
# until there are no more matching rules available
|
66
|
+
begin # loop
|
67
|
+
|
68
|
+
# the loop condition is reset to break by default after every iteration
|
69
|
+
matches = false
|
70
|
+
|
71
|
+
available_rules.each do |rule|
|
72
|
+
# RuleCheckErrors are caught and swallowed and the rule that
|
73
|
+
# raised the error is removed from the working-set.
|
74
|
+
begin
|
75
|
+
if rule.conditions_match?(obj)
|
76
|
+
matches = true
|
77
|
+
|
78
|
+
# remove the rule from the working-set so it's not re-evaluated
|
79
|
+
available_rules.delete(rule)
|
80
|
+
|
81
|
+
# find all parameter-matching dependencies of this rule and
|
82
|
+
# add them to the working-set.
|
83
|
+
if @dependencies.has_key?(rule.name)
|
84
|
+
available_rules += @dependencies[rule.name].select do |dependency|
|
85
|
+
dependency.parameters_match?(obj)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# execute this rule
|
90
|
+
rule.call(obj)
|
91
|
+
|
92
|
+
# break the current iteration and start back from the first rule defined.
|
93
|
+
break
|
94
|
+
end # if rule.conditions_match?(obj)
|
95
|
+
|
96
|
+
rescue RuleCheckError => e
|
97
|
+
# log da error or sumpin
|
98
|
+
available_rules.delete(e.rule)
|
99
|
+
end # begin/rescue
|
100
|
+
|
101
|
+
end # available_rules.each
|
102
|
+
|
103
|
+
end while(matches)
|
104
|
+
|
105
|
+
rescue RuleConsequenceError => rce
|
106
|
+
# RuleConsequenceErrors are allowed to break out of the current assertion,
|
107
|
+
# then the inner error is bubbled-up to the asserting code.
|
108
|
+
raise rce.inner_error
|
109
|
+
end
|
110
|
+
end # def assert
|
111
|
+
|
112
|
+
end # class RuleSet
|
113
|
+
end # module Rools
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
!ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.11
|
3
|
+
specification_version: 1
|
4
|
+
name: rools
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.1.3
|
7
|
+
date: 2005-12-12 00:00:00 -06:00
|
8
|
+
summary: A Rules Engine written in Ruby
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: ssmoot@gmail.com; bauer.mail@gmail.com
|
12
|
+
homepage: http://substantiality.net
|
13
|
+
rubyforge_project: rools
|
14
|
+
description: Can be used for program-flow, ideally suited to processing applications
|
15
|
+
autorequire: rools
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
authors:
|
29
|
+
- Sam Smoot
|
30
|
+
- Scott Bauer
|
31
|
+
files:
|
32
|
+
- README
|
33
|
+
- CHANGELOG
|
34
|
+
- RAKEFILE
|
35
|
+
- lib/rools.rb
|
36
|
+
- lib/rools/default_parameter_proc.rb
|
37
|
+
- lib/rools/errors.rb
|
38
|
+
- lib/rools/rule.rb
|
39
|
+
- lib/rools/rule_set.rb
|
40
|
+
test_files: []
|
41
|
+
|
42
|
+
rdoc_options:
|
43
|
+
- --line-numbers
|
44
|
+
- --inline-source
|
45
|
+
- --main
|
46
|
+
- README
|
47
|
+
extra_rdoc_files:
|
48
|
+
- README
|
49
|
+
- CHANGELOG
|
50
|
+
- RAKEFILE
|
51
|
+
executables: []
|
52
|
+
|
53
|
+
extensions: []
|
54
|
+
|
55
|
+
requirements:
|
56
|
+
- none
|
57
|
+
dependencies: []
|
58
|
+
|