And now, the rules

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 Expressions 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 Procs. 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 Groups. 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 Expressions in a Group so we can generate Rules 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 Rules for its Expressions.

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 Expressions and Groups. 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 Expressions. 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). Expressions don’t hold references to Rules, 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 Expressions and/or Groups, 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.