In my last post I wrote about how to write your own mini-language in Ruby
by abusing method_missing
and operator overloading. I know, I know, it totally
blew your mind and whatever, but I missed out this huge part of the language I
was demonstrating: the rules. And without the rules, all it’s good for is
some silly little shorthands in ActionController
. Big deal.
So let’s make some rules. We’re going to do this by creating an extension of the expression language that can create rules and store them in some environment. These will be our shiny new building blocks:
module Consent
class Rule
class Expression < Consent::Expression
class Group < Consent::Expression::Group
end
end
module Generator
end
end
end
We’ve got a Rule
class to store rules, and it has a Generator
; we will embed
Rule::Generator
in environments that should generate rules, just like we did
for Expression::Generator
. And, we have some new Expression
and Group
classes that extend the old ones and provide rule-specific functionality.
To get started, we need to figure how rules are stored. Well, we’ve got
Expression
s and we’re going to add blocks, so let’s say a Rule
has an
expression and a block. You can add your own methods to figure out whether a
request matches the expression and run the block to test the rule, but for now
we’ll just figure out how to store things:
class Consent::Rule
def initialize(expression, block)
@expression, @predicate = expression, block
end
end
Note I’m not using the &block
parameter format because we’re not going to be
calling Rule.new(expr) { code }
; the blocks will be called on methods in the
expression language then passed around for storage as Proc
s. Also, omitting
the ampersand makes the Proc
a required parameter.
The other little bit of setup is to note that the generated rules need to be stored somewhere, but the language does not use assignment. For example, if we call this in a controller:
redirect_to admin/users.find(:id => 'twitter')
The expression generated by admin/users.find(:id => 'twitter')
is passed as a
parameter to the redirect_to
method where it can be interpreted. In contrast,
our rule language is declarative: you simply say
admin/users.find(:id => 'twitter') { deny unless has_dictionary? }
and this should store a rule somewhere, even though there’s no assignment. The
way I’ve chosen to do this is to make sure that when we mix in
Rule::Generator
, a @rules
array is created in the host class, and
expressions generated have a reference to the environment in which they were
created so they can push rules into it. In other words:
class Consent::Rule
class Expression
def initialize(env, name, params = {})
@env = env
super(name, params)
end
end
module Generator
def self.included(host)
host.module_eval do
def rules
@rules ||= []
end
end
end
def method_missing(name, params = {})
Rule::Expression.new(self, name, params)
end
end
end
As before, Generator#method_missing
simply creates an expression of the
correct type, only this time the expression is passed a reference to the object
where it was created. This object will have a rules
accessor thanks to
Generator.included
, so the expression can use @env.rules
to push new rules
into the host environment.
At this point we need to stop and make some changes. Generator#method_missing
,
as we know, is responsible for creating controller-only expressions. The
language needs to support blocks at this level so we can write rules like:
profiles { deny if session[:user].nil? }
Remember, the blocks don’t just float in space; Ruby will pass them to the
preceeding word-based method call in the expression (it won’t pass them to
operator methods). So we’d better let Generator#method_missing
take a block,
and use it to generate a rule. I’m going to add a method to Expression
for
doing just that:
class Consent::Rule
class Expression
def rule!(block)
return if block.nil?
@env.rules << Rule.new(self, block)
end
end
module Generator
def method_missing(name, params = {}, &block)
expression = Rule::Expression.new(self, name, params)
expression.rule!(block)
expression
end
end
end
See how here we use the ampersand in &block
, making the block optional. We
need to do the same thing in Expression#method_missing
to support rules with
actions:
class Consent::Rule::Expression
def method_missing(name, params = {}, &block)
rule!(block)
super(name, params)
end
end
Okay, we’ve dealt with the easy stuff, so we can handle single expressions with
controllers, actions and params. (We can’t handle formats properly yet, as I’ll
explain later.) The next step is to handle Group
s. Take this expression:
users(:source => 'facebook') + tags.find { request.get? }
This will evalutate users(:source => 'facebook')
, then tags.find {
request.get? }
(the latter of which will generate a rule), then combines them
using addition. We need to somehow propagate the block back to the other
Expression
s in a Group
so we can generate Rule
s for them. We can do this
by storing a reference to the block in the final expression, and overriding
Group#+
so that it checks the incoming Expression
for a block. If the
Expression
has a block, we can iterate over the Group
and generate Rule
s
for its Expression
s.
class Consent::Rule::Expression
attr_reader :block
def rule!(block)
return if block.nil?
@block = block
@env.rules << Rule.new(self, block)
end
class Group
attr_reader :block
def +(expression)
rule!(expression.block)
super
end
def rule!(block)
return if block.nil?
@block = block
each { |exp| exp.rule!(block) }
end
end
end
Notice how both Expression
and Group
now support the methods +
, block
and rule!
. This means the addition operator can cope with adding any
combination of Expression
s and Group
s. Group#rule!
method comes in
particularly handy when implementing HTTP verb filters, as shown below. But
before we get there, there’s one tricky issue to sort out: response formats.
Consider the following expression:
categories * xml { params[:debug].nil? }
Ruby will evaluate categories
and xml { params[:debug].nil? }
, then multiply
the resulting Expression
s. Clearly we need to override Expression#*
to
propagate the rule, but there’s another problem: we’ve generated a Rule
in
memory matching XmlController
, which is not what we wanted (the above
expression should match any CategoriesController
action where the response
format is XML). So, we need to tell this Rule
that it’s no longer valid, but
we don’t have a reference to it (this is also why we can’t just remove it from
@env.rules
). Expression
s don’t hold references to Rule
s, it’s the other
way around!
One way to solve this is to use the observer pattern. The Rule
can
observe its @expression
, and the expression can publish messages to declare
itself invalid.
require 'observer'
class Consent::Rule
def initialize(expression, block)
@expression, @predicate = expression, block
@expression.add_observer(self)
end
def update(message)
@invalid = true if message == :destroyed
end
class Expression
include Observable
def *(expression)
expression.destroy!
rule!(expression.block)
super
end
def destroy!
changed(true)
notify_observers(:destroyed)
end
end
end
This sets up a Rule
to observe the Expression
it relates to, and to render
itself invalid if the Expression
is “destroyed”. The multiplication operator
destroys the incoming Expression
, creates a new rule using self
if the
expression had a block, then calls super
to carry out the inherited meaning of
Expression#*
.
Phew! Almost there, just one final piece of the puzzle: HTTP verbs. These are
top-level methods in the expression language, and it didn’t really make sense to
put them in Expression::Generator
since you can’t, say, call redirect_to
post(tags.list)
, it just doesn’t make any sense. But they can be used to filter
incoming requests, so we’ll put them in Rule::Generator
. Each HTTP method
takes a list of Expression
s and/or Group
s, combines them into a single
Group
, sets the verb on that group and returns it. Since they’re top-level
functions, they also need to take blocks and generate rules (using
Group#rule!
). I’m going to put the verb setter methods on the base
Expression
classes as they’re not really specific to Rule::Expression
.
class Consent
class Expression
def verb=(verb)
@verb = verb.to_s
end
class Group
def verb=(verb)
each { |exp| exp.verb = verb }
end
end
end
module Rule::Generator
%w(get post put head delete).each do |verb|
module_eval <<-EOS
def #{verb}(*exprs, &block)
group = exprs.inject { |grp, exp| grp + exp }
group.verb = :#{verb}
group.rule!(block)
group
end
EOS
end
end
end
And that pretty much wraps it up. Interpreting rules is another problem in its own right, and to be honest you’re better off reading the Consent source code if you want the complete picture. In the meantime, I’m doing a JSON and JSONQuery interpreter for Ruby that I hope to release soonish, and also a Scheme implementation which will almost certainly never see the light of day but is a lot of fun nonetheless. Looks like 2009 is “tool around with languages” year for me.
Source code for this and the previous post is up on Github if you want to
grab it all at once. I’ve added a few handy inspection methods so rules look
nicer in irb
and I’ve done some cursory testing but if any of it is broken
please let me know.