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にサンプルとして載ってる…何度も見たのに気づかなかった…。