Wednesday, April 06, 2011

Ruby Light #6

This is the most complicated bit of meta-programming in Ruby Light so far. Johan and I cooked it up; the beautiful plumbing is his, the basis of the instance_exec rebinding trick is mine.

The intent is as follows: for each request to a Rails controller, redirect unless all of the URL query parameters you get are valid.

And this is how it actually works:
  1. When Rails loads the FooController class:
    1. FooController mixes in the ParamsFilter module, resulting in it getting the known_params and parse_known_params class methods and the redirect_if_unknown_params and select_unknown instance methods (et al.).
    2. FooController calls the known_params class method to always accept the "limit" and "offset" params, but only accept the "bar" param when the Foo object is of type "bar".
  2. On each request:
    1. FooController's before_filter calls its redirect_if_unknown_params instance method.
    2. redirect_if_unknown_params rejects unknown parameters using the private select_unknown instance method.
    3. select_unknown calls known_param? on each param.
    4. If the param has a :when component, known_param? executes its value (which is a block)--and here's the cool part--in the context of the controller instance! This is cool because Proc objects normally operate in the context in which they were defined (i.e. they are closures). Calling instance_eval or instance_exec with a Proc argument (which is not the same thing as a block!) results in the Proc's binding being changed to context where it is called. This results in self, which was the FooController class when the Proc was defined, being set to the FooController instance on which known_param? is executing.


And now, the code:

app/controllers/foo_controller.rb
class FooController < ApplicationController
include ParamsFilter

before_filter :redirect_if_unknown_params
known_params :limit, :offset, :only => :bar, :when => lambda {@type == 'bar'}
# [...]


lib/params_filter.rb
module ParamsFilter
def self.included(base)
base.send :extend, ClassMethods
base.send :include, InstanceMethods
end

module ClassMethods
attr_reader :known_params

def known_params(*args, &block)
@@known_params ||= HashWithIndifferentAccess.new
@@known_params.merge! parse_known_params(*args, &block)
end

def parse_known_params(*args, &block)
HashWithIndifferentAccess.new.tap do |known_params|
options = (args.last.is_a? Hash) ? args.pop.dup : {}
options[:when] = block if block_given?

args.each do |param|
known_params[param] = options
end
end
end
end

module InstanceMethods
def known_params(*args)
@known_params ||= HashWithIndifferentAccess.new
@known_params.merge! self.class.parse_known_params(*args)
end

def redirect_if_unknown_params
unknown = select_unknown request.query_parameters, request.path_parameters
redirect_without_unknown_params unknown unless unknown.empty?
end

private

def select_unknown(query_params = {}, path_params = {})
query_params.reject {|param, _| known_param? param, path_params}
end

def known_param?(param, params)
if options = param_options(param)
if action_matches_params? options[:only], params
options[:when].nil? || instance_exec(param, &options[:when])
end
end
end

def redirect_without_unknown_params(unknown_params)
# Actually perform redirect
end

# [...]
end
end

No comments: