Allows you to define a set of 'rules' or dynamic methods to apply to a class.
> gem update --system
> gem install rulebook
require 'rulebook'
class User
follows_the_rules!
def initialize(name)
@name = name
end
def_rule(/say_(.+)/) do |what_to_say|
puts "#{@name} says '#{what_to_say.gsub(/_/, ' ')}'"
end
end
User.new('Ryan').say_hello_world # => Ryan says 'hello world'
An instance of Rulebook::Rule
simply holds two attributes, @regexp
and @block
:
rule = Rulebook::Rule.new(/^regexp$/) { puts "Hello" } # => #<Rule:0x0000>
rule.regexp # => /^regexp$/
rule.block # => #<Proc:0x0000>
It's a simple as that.
There are a couple of helper methods as well:
rule.match('regexp') # => #<MatchData "regexp">
rule['regexp'] # => #<MatchData "regexp">
rule.matches?('regexp') # => true
An instance of Rulebook
holds a single attribute, @rules
, which is a simple Array
of Rulebook::Rule
instances:
rulebook = Rulebook.new
rule = Rulebook::Rule.new(/^regexp$/) { "Hello" }
rulebook.rules << rule
rulebook.rules # => [#<Rule:0x0000>]
Rulebook
also contains a few helper methods:
rulebook.add(/testing/) { "testing" }
rulebook.rules # => [#<Rule:0x0000>, #<Rule:0x0eb6>]
rulebook.rules_that_match_against("testing") # => [#<Rule:0x0eb6>]
rulebook.match("testing") # => [#<Rule:0x0eb6>]
rulebook["testing"] # => [#<Rule:0x0eb6>]
When you require 'rulebook'
, we automatically define a method within Module
named follows_the_rules!
.
This defines two new class methods; def_rule
and def_class_rule
.
class User
end
p User.methods.grep(/follows|def_/) # => ["follows_the_rules!"]
class User
follows_the_rules!
end
p User.methods.grep(/follows|def_/) # => ["follows_the_rules!", "def_rule", "def_class_rule"]
This ensures that only the Objects that you want to follow rules will have these methods available.
You may call follows_the_rules!
Outside of the class as well:
class User
end
User.follows_the_rules!
p User.methods.grep(/follows|def_/) # => ["follows_the_rules!", "def_rule", "def_class_rule"]
When you define a rule on a class using def_rule
the first time, we will add an instance variable to the class
named @rulebook
that contains an instance of Rulebook
. We then define the rule within that rulebook instance.
Now, when you call a method that does not yet exist in your class, we check the rulebook to see if the method called matches any of the defined rules.
If it doesn't we just go ahead and let method_missing
do it's thing (A.K.A. throw a NoMethodError
), otherwise
we find the first rule that matches the method, take the block associated with that rule and define the
method. The next step is to simply call the method.
Take the following for example (although a bit useless, it describes the usage pretty clearly):
class User
follows_the_rules!
def_rule(/foo(?:bar)?/) { "FOOBAR!" }
end
user = User.new
p user.methods - Object.new.methods # => []
As you can see, no instance methods are defined within the User
object.
So when I call a method that matches the Regexp
within the defined rule,
it will actually define the instance method on the User
object:
p user.foo # => "FOOBAR"
p user.methods - Object.new.methods # => [:foo]
p user.foobar # => "FOOBAR"
p user.methods - Object.new.methods # => [:foo, :foobar]
def_class_rule
works in much the same way as def_rule
, except on the class.
Here's a small example if def_class_rule
in action:
class User
follows_the_rules!
attr :role
def_class_rule(/new_(admin|moderator)/) do |role|
instance = new
instance.role = role.to_sym
instance
end
def initialize
@role = :user
end
def role=(new_role)
@role = new_role.to_sym
end
end
User.new.role # => :user
User.new_moderator.role # => :moderator
User.new_admin.role # => :admin
As seen in the last example, when using def_rule
or def_class_rule
, you can pass
the captures from the match to the block given.
Here is a nice example of that
usage:
class User
follows_the_rules!
def_class_rule(/new_(admin|moderator|user)/) do |role|
new.instance_eval { @role = role.to_sym; self }
end
def_rule(/is_(admin|moderator|user)\?/) do |role|
@role == role.to_sym
end
def initialize
@role = :user # default
end
end
user = User.new_admin
user.is_user? # => false
user.is_admin? # => true
If you would like to add arguments to the rule, simply tack them on the end
of the block arguments. Just like you would define_method
:
class Book
def_rule(/^page_(\d+)$/) do |page_number, line_number=1|
puts "This is the contents of page #{page_number}, line #{line_number}."
end
end
book = Book.new
book.page_15 # => This is the contents of page 15, line 1.
book.page_15(3) # => This is the contents of page 15, line 3.
Don't use it.
Say you have the following class:
class Foo
def bar
"foobar"
end
def baz(str_to_append="!!!")
"foobaz#{str_to_append}"
end
end
And another class that wants to delegate missing methods to an instance of Foo
:
class DelegateMeToFoo
def initialize
@foo = Foo.new
end
def method_missing(meth, *args, &blk)
@foo.send(meth, *args, &blk)
end
end
dmtf = DelegateMeToFoo.new
p dmtf.bar # => "foobar"
p dmtf.baz("??") # => "foobaz??"
This could easily be converted to use Rulebook
:
class DelegateMeToFoo
def initialize
@foo = Foo.new
end
def_rule(/(.*)/) do |meth, *args, &blk|
@foo.send(meth, *args, &blk)
end
end
dmtf = DelegateMeToFoo.new
p dmtf.bar # => "foobar"
p dmtf.baz("??") # => "foobaz??"
The advantages of doing so aren't apparent at first, but you must
realize that every single method (in this case, since no instance methods
are defined within Foo
) that is sent to your Foo
instance must
check all of it's defined methods for the method you are calling.
When it isn't found, it goes to the parent class to see if the instance
method is defined there. It continues up the chain in that fashion
until it hits the top-level class. It then loops back around to
your Foo
instance to check for method_missing
which is defined
in the example above. Since it found method_missing
, it calls it.
Ruby must do this every single time.
With Rulebook
, when you call dmtf.bar
the first time, we actually
define the bar
method on your DelegateMeToFoo
instance, then calls it.
Now the next time dmtf.bar
is called, it no longer has to do the
method_missing
dance and search the inheritance chain for the
bar
method. Ruby finds the newly defined method and runs it.
- Fork the project.
- Make your feature addition or bug fix.
- Add tests for it. This is important so I don't break it in a future version unintentionally.
- Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
- Send me a pull request. Bonus points for topic branches.
Copyright (c) 2010-2011 Ryan Lewis. See LICENSE for details.