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