はじめに
SinatraはWebアプリケーションを記述するためのDSL(を実装するライブラリ)です。
READMEでは以下のようにアプリケーションを記述し、
require 'sinatra'
get '/' do
'Hello world!'
end
このrbファイルを実行すればすぐにサーバを立ち上げることができると書かれています(もちろんこれだけじゃ実用的ではないというところは置いといて)
ruby myapp.rb
この記事の目的はSinatraの使い方を説明することではなく、上のように書いたDSLが「どうやって動いているのか(動かされているのか)」を調べることで、明日役に立つ(ことがあるかもしれない)知識を学ぶことにあります。
Sinatraを読んでいくにあたって以下の三段階に分けて説明していきます。
- Sinatraはルーティングをどのように登録しているのか(この記事)
- Sinatraはサーバをどのように起動しているのか
- Sinatraはリクエストをどのように処理しているのか
なお、読解対象とする(記事でリンクを張る)Sinatraのバージョンは2.0.5とします。
sinatra/main.rbとextend
アプリケーションのファイルがrequireしているsinatra.rbは、sinatra/main.rbをrequireしています。さらにsinatra/main.rbはsinatra/base.rbをrequireしており、このsinatra/base.rbが一番核となるファイルなのですがまずはsinatra/main.rbを見てみましょう。以下の記述があります(上の方に書いてある部分は起動編で説明します)
# include would include the module in Object
# extend only extends the `main` object
extend Sinatra::Delegator
Sinatra::Delegatorがextendされています。extendは「モジュールに定義されたメソッドをオブジェクトの特異メソッドとして追加する(オブジェクトのクラス全部にではなく特定のオブジェクトのみにメソッドを追加する)」という動作をします。
上記ではextendはオブジェクトを指定することなく書かれています。実はRubyのトップレベル(class内とかじゃない単にputsとか書くところ)はmainというオブジェクトがself(メソッドのレシーバー)になっています。
つまり、「extend Sinatra::Delegator」により、Delegatorに定義されているメソッドがトップレベルにメソッドとして定義されます。先に書いておくとこのDelegatorにgetなどのルーティングメソッドが定義されています。
Delegator(移譲者)という名前から想像できるようにDelegatorには単純にメソッドが定義されているわけではありません。というわけで次にsinatra/base.rbに入りSinatra::Delegatorを見てみましょう。
Sinatra::Delegator
Sinatra::Delegatorの定義はsinatra/base.rbの最後の方にあります。
# Sinatra delegation mixin. Mixing this module into an object causes all
# methods to be delegated to the Sinatra::Application class. Used primarily
# at the top-level.
module Delegator #:nodoc:
def self.delegate(*methods)
methods.each do |method_name|
define_method(method_name) do |*args, &block|
return super(*args, &block) if respond_to? method_name
Delegator.target.send(method_name, *args, &block)
end
private method_name
end
end
delegate :get, :patch, :put, :post, :delete, :head, :options, :link, :unlink,
:template, :layout, :before, :after, :error, :not_found, :configure,
:set, :mime_type, :enable, :disable, :use, :development?, :test?,
:production?, :helpers, :settings, :register
class << self
attr_accessor :target
end
self.target = Application
end
Delegatorの動作についてはコメントに書かれている通りです。日本語に訳すと「このモジュールをオブジェクトにミックスする(Ruby的に言うとextendする)とそのメソッド呼び出しはSinatra::Applicationクラスのメソッド呼び出しに移譲される」となります。
Delegatorメソッドで行われている処理は以下のようになります。難解なのは「moduleでselfってどういうこと?」ということだと思います。module~endの間でのselfはモジュールオブジェクトになります。
- target属性の定義と初期値設定
- attr_accessorを使用し、target属性(ゲッターメソッドとセッターメソッド)を定義する。「class << self」とあるがここではクラスとは関係なくメソッドが実行されるコンテキストをself(=Delegatorモジュール)にするという意味。つまり、Delegatorモジュールにクラス変数的1にtarget属性が追加される。このような書き方になっているのはattr_accessorがprivateメソッド(レシーバを指定してのメソッド呼び出しができないメソッド)のため。
- targetの初期値としてApplicationクラス(Sinatra::Application)を設定。
- delegateメソッドの定義と実行
- delegateメソッドを定義する。これは「self.delegate」なので*Delegatorモジュールの特異メソッド(クラスで言うとクラスメソッド)*になる。このメソッドはextendしてもオブジェクトにメソッドは追加されない。
- 定義したdelegateメソッドを呼び出す。delegateメソッドではdefine_methodを使い「targetで設定されたクラスのメソッドを呼び出す移譲メソッド」が定義されている。つまり、例えばgetメソッドを呼び出すとSinatra::Applicationクラスのgetクラスメソッドが呼び出されるようになる。なおここで定義されるメソッドはextend時にオブジェクトに追加される。
トップレベル(グローバル)でルーティングを定義するのだから裏側にオブジェクトがいてそいつに登録されるのだろうなとは思ってましたが、予想よりもかなり複雑なことをしているなという印象です。
さて次はgetメソッドが定義されているSinatra::Applicationですね。と言いたいところですが、Applicationはあまり処理が書かれておらず、Sinatraの本体と言えるのはその親クラスのSinatra::Baseです。
Sinatra::Base(登録処理周り)
Sinatra::Baseは長い(ほぼ1000行)のでこの記事ではルーティング登録に関する部分のみを説明します。それ以外の部分については起動編、処理編で説明します。
getメソッドは以下のようになっています。routeメソッドでルーティングが設定されているのだろうなと予測できます。
class << self
# Defining a `GET` handler also automatically defines
# a `HEAD` handler.
def get(path, opts = {}, &block)
conditions = @conditions.dup
route('GET', path, opts, &block)
@conditions = conditions
route('HEAD', path, opts, &block)
end
routeメソッド(念のためですが、これ以降もすべて特異メソッド=クラスメソッドです)。compile!メソッドを呼び出して処理を行ったうえで@routes
に追加しています。@routes
にはHTTPメソッド(動詞)ごとに処理が登録されているということがわかります。
def route(verb, path, options = {}, &block)
enable :empty_path_info if path == "" and empty_path_info.nil?
signature = compile!(verb, path, block, options)
(@routes[verb] ||= []) << signature
invoke_hook(:route_added, verb, path, block)
signature
end
compile!に進む。ちなみにSinatraコードの特徴として、メソッドに「!」を付けているものが多く、Rubyのお決まり的には「オブジェクト状態が変わるメソッドは!を付ける」だと思いますが、読んでる分にinternal的な意味で付けているように思います。
def compile!(verb, path, block, **options)
# Because of self.options.host
host_name(options.delete(:host)) if options.key?(:host)
# Pass Mustermann opts to compile()
route_mustermann_opts = options.key?(:mustermann_opts) ? options.delete(:mustermann_opts) : {}.freeze
options.each_pair { |option, args| send(option, *args) }
pattern = compile(path, route_mustermann_opts)
method_name = "#{verb} #{path}"
unbound_method = generate_method(method_name, &block)
conditions, @conditions = @conditions, []
wrapper = block.arity != 0 ?
proc { |a, p| unbound_method.bind(a).call(*p) } :
proc { |a, p| unbound_method.bind(a).call }
[ pattern, conditions, wrapper ]
end
def compile(path, route_mustermann_opts = {})
Mustermann.new(path, mustermann_opts.merge(route_mustermann_opts))
end
compileメソッドではパターンマッチングに使うMustermannオブジェクトが作成されています。今回の本題ではないのでこちらは置いといて、generate_methodに進みます。
def generate_method(method_name, &block)
define_method(method_name, &block)
method = instance_method method_name
remove_method method_name
method
end
書いてあるままですが何をしているかというと、
- 渡されたブロック(getメソッドに渡されるブロック。つまり、パスに対する処理)を「インスタンスメソッドとして」定義する。
- 定義したメソッド(メソッドオブジェクト)を取得する。なおcompile!での受け取り側変数が示すようにここで取得されるのはUnboundMethod(特定のインスタンスに結び付いていないメソッド)
- 定義したメソッドを削除する。なお「クラスのインスタンスメソッド一覧」から削除されるだけで一つ前で「オブジェクト参照」が取得されているのでガーベジコレクトされることはありません。ちなみに削除しているのは「一覧からは消す」ためと思われます。
最後にcompile!の一部を再掲します。ここで作られているprocが実行されるのは実際にパスに対するリクエストが来たときですが、unboundなメソッドにインスタンスを関連付けて実行しているということが推測できます。
wrapper = block.arity != 0 ?
proc { |a, p| unbound_method.bind(a).call(*p) } :
proc { |a, p| unbound_method.bind(a).call }
うーんだいぶややこしい。「ブロックをインスタンスのコンテキストで実行してるのかな」と思ってはいたのですが、それを実現するにはこんな風に書かないといけないのですね・・・
ここまでのまとめ
以上ここまでルーティングの登録処理について見てきました。「普通のオブジェクト」以外のselfについて理解することがここまでを読み解く鍵と言えるでしょう。
- DSLで使うメソッドはSinatra::Delegatorに定義されており2、mainオブジェクトがSinatra::Delegatorをextendすることでトップレベルに定義される。
- module~end内のselfはモジュールオブジェクトになる。このselfを利用し、モジュールオブジェクトにクラス変数的に属性を定義したり、クラスメソッド的にメソッドを定義したりできる。
- 渡されたブロックはSinatra::Baseクラスのインスタンスメソッドとして登録される(ただしメソッド一覧からはすぐに削除される)。リクエスト処理時にインスタンスをバインドして実行すると推測される。
起動編へ続く。