sinatraでbefore :onlyみたいなこと

SinatraのbeforeフィルタだとRailsみたいに:onlyや:exceptで適用するルーティングを指定できない。 ただそれだとbeforeフィルタのなかでガリガリとifで分岐する必要があるのでそれもちょっと微妙。

で、A Sinatra Before Only Filterにbefore :onlyについて処理するプラグイン(?)を書いている人がいたので、ちょっと真似してみた。

module Sinatra
  module BeforeOnlyFilter
    def before_only(*routes, &block)
      before do
        routes_regex = routes.map do |route|
                          route.is_a?(Regexp) ? route : /^#{route.gsub(/\*/, '[^/]*')}$/
                        end
        instance_eval(&block) if routes_regex.any? {|regex| (request.path =~ regex) != nil}
      end
    end
  end
  register Sinatra::BeforeOnlyFilter
end

使うときはこんな感じ

require 'sinatra/before_only_filter'

before_only %r{/users/.+} do
  # /users/aaa とか
  # /users/aaa/bbb とか
  # /users/aaa.ccc とか
end

before_only "/users/*" do
  # /users/aaa はマッチするけど
  # /users/aaa/bbb や
  # /users/aaa.ccc はマッチしない
end

でも、ちょっと不便なところが。

  • HTTPメソッドでの区別ができない
  • URLからパラメータを取り出すには自分でパースする必要がある

というか、sinatraのソースを見るとそもそもbeforeメソッドの引数でパスが指定できる…?

def before(path = nil, options = {}, &block)
  add_filter(:before, path, options, &block)
end

というわけでちょっとsinatraのソースを追ってみた。

ここの pathはどうやら正規表現が使えるっぽい。 で、add_filterされたあとはどうなるかというと、

def add_filter(type, path = nil, options = {}, &block)
  path, options = //, path if path.respond_to?(:each_pair)
  filters[type] << compile!(type, path || //, block, options)
end

compile!の引数から察するにpathは正規表現が使えるらしい。さてそのcompile!はというと、

def compile!(verb, path, block, options = {})
  options.each_pair { |option, args| send(option, *args) }
  method_name             = "#{verb} #{path}"
  unbound_method          = generate_method(method_name, &block)
  pattern, keys           = compile path
  conditions, @conditions = @conditions, []

  [ pattern, keys, conditions, block.arity != 0 ?
      proc { |a,p| unbound_method.bind(a).call(*p) } :
      proc { |a,p| unbound_method.bind(a).call } ]
end

なんか難しい…。 いまverbはadd_filterのtype、つまり:beforeがセットされているから、ブロックを処理する"before //"みたいなインスタンスメソッドがつくられているんだろう。

ここで呼ばれているパスを正規表現に返還するためのcompileメソッドはbeforeやafter以外のget/post/put/deleteといったルートコンテキスト(?)でも使われているみたいだから、実はbeforeでも/users/:idみたいなものが指定できてparamsから取得できちゃったりするっかも…? でもやっぱりHTTPメソッドでの切り分けは対応していなさそうだ。

ちょっと力尽きたので、次回試してみよう。

ちなみに呼び出しはdispatch!、さらにその中のfilter!で行われる。 見た感じbase.filters[:before]で複数フィルタの処理がされてるようなので、beforeは何度呼び出しても大丈夫。before_onlyも同様。

def dispatch!
  static! if settings.static? && (request.get? || request.head?)
  filter! :before
  route!
rescue ::Exception => boom
  handle_exception!(boom)
ensure
  filter! :after unless env['sinatra.static_file']
end

def filter!(type, base = settings)
  filter! type, base.superclass if base.superclass.respond_to?(:filters)
  base.filters[type].each { |args| process_route(*args) }
end

追記 2011-11-15

今年3月に出た1.2.0の時点でbefore/afterのパターンマッチ機能はサポートされてたみたい。(@komagata さん、ありがとうございます!)

というかREADMEにサンプルとして載ってる…何度も見たのに気づかなかった…。