A Ruby rule engine
Yet another rule engine, first there was the java, smalltalk, and python versions, and after James Robertson mentioned I'd also done a Ruby version I thought I best actually do one...
I've been following alot of Ruby blog posts but as yet havn't actually written anything in it. I'd seen enough sample code on blogs to know the basic layout of the structure, so I fire up gedit and define a class:My, that was hard :) Now, theres two bits of functionality I know I want: 1) The ability to make an assertion against the rule and 2) The ability to run all defined assertions against a value. One of the strengths of Ruby is its support for blocks, and the basic design follows the Smalltalk version pretty much identically:class RuleSet end
class RuleSet
def initialize()
@assertions = []
end
def assert(&assertion)
@assertions.insert(0, assertion)
end
def for(value)
@assertions.each {|assertion|
if !assertion.call(value)
return false
end
}
return true
end
end
Again this is looking very simple, its not complex code, but the lack of static typing, blocks, and a collections API that takes advantage of it make for clean and consise code.
rule = RuleSet.new
rule.assert {|value| value == "mark"}
ruleSet = {:nameIsMark, rule}
Create a RuleSet, make some assertions, put it in a map keys by a symbol (I initially used a string, but I think a symbol is more appropriate here). Simple.
if ruleSet[:nameIsMark].for("mark")
puts "rule passed"
end
Look ma - NO CASTING!
This rule engine seems to be somewhat related to the kind of contracts I have implemented at http://ruby-contract.rubyforge.org/ -- perhaps you will find it interesting.
The Ruby code itself was pretty idiomatic. I think I would slightly change the style, though:class RuleSet def initialize(&block) @assertions = [] instance_eval(&block) if block end def assert(&assertion) # You can also do Array#unshift if the original semantics are important @assertions << assertion end def for(value) @assertions.all? do |assertion| assertion.call(value) end end endYou might also want to provide the === method and instance_eval a supplied block in initialize:class RuleSet alias :=== :for end positive_even_number = RuleSet.new do assert { |obj| obj.is_a?(Numeric) } assert { |obj| obj > 0 } assert { |obj| obj % 2 == 0 } end case gets.to_i when 42 puts "Totally good choice." when positive_even_number then puts "Not too bad." else puts "What are you doing!?" end puts [1, 4, 16, 9, "foo", 1.5].grep(positive_even_number) # outputs 4 and 16I wasn't able to comment on your weblog with the popular Mozilla Firefox browser. Can you please fix this?