rools 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,9 @@
1
+ - 0.1.1
2
+ Added RuleConsequenceError
3
+
4
+ - 0.1.2
5
+ Added RAKEFILE, README, CHANGELOG
6
+
7
+ - 0.1.3
8
+ Finished most documentation
9
+ Tweaked the rakefile
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,5 @@
1
+ require 'rools/rule_set'
2
+
3
+ # Test Documentation
4
+ module Rools
5
+ end
@@ -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
@@ -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
+