Sinatraのソースコードを読む(1)
Rubyの軽量フレームワークSinatraのソースコードがイケテルらしいので、がんばって読む事にしました。
githubからgit cloneし、v1.3.0.dにチェックアウト
# ターミナルで $ git clone https://github.com/sinatra/sinatra.git $ cd sinatra $ git checkout v1.3.0.d
ソースコード読む準備。手元にソースコードを持ってきて、値をダンプしながら読み進めた方が効率的なので、git cloneしてきます。その後sinatraディレクトリに移動し、v1.3.0.dにチェックアウトします。
cloneしたsinatraを動かす
# hoge.rb require './sinatra' get '/' do 'hello' end
libディレクトリ直下にhoge.rbみたいなファイルを作ります。それをターミナルでruby hoge.rb。するとwebrickの4567ポートが立ち上がるので、ブラウザでlocalhost:4567にアクセスすると、helloと表示されます。
ソースコードリーディング
本題のソースコードリーディング。さきほどのhoge.rbで実行したメソッドgetを対象に
を調べます
libdir = File.dirname(__FILE__) $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir) require 'sinatra/base' require 'sinatra/main' enable :inline_templates
まずは起点となるsinatra.rb。ロードパスを追加した上で、sinatra/baseとsinatra/mainをrequire。
sinatra / lib / sinatra / base.rb
require 'thread' require 'time' require 'uri' require 'sinatra/rack' require 'sinatra/showexceptions' require 'tilt' module Sinatra ...1000行以上 end
requireされたbase.rb。rackなどをrequireしている。その後Sinatraの名前空間配下に色々なクラスやモジュールを定義してる。
sinatra / lib / sinatra / main.rb
require 'sinatra/base' module Sinatra class Application < Base ... end at_exit { Application.run! if $!.nil? && Application.run? } end # ポイント include Sinatra::Delegator
お次はmain.rb。module Sinatraとclass Applicationはbase.rbでも定義されていたので、再オープンして定義を追加しているということになる。
参考:http://d.hatena.ne.jp/kitokitoki/20120627/p2
ポイントはinclude Sinatra::Delegator。トップレベルでモジュールをインクルードするということは、モジュール内で定義されたメソッドをトップレベルでレシーバを明示せず実行できるようになることを意味する。つまり、Sinatra::Delegatorをトップレベルでインクルードすれば、Sinatra::Delegatorで定義されたメソッドをトップレベルでレシーバを明示せず(get '/'のような形で)実行できるようになる。
このことから、Sinatra::Delegatorにgetなどのメソッドが定義されていると推測できる。
https://github.com/sinatra/sinatra/blob/v1.3.0.d/lib/sinatra/base.rb#L1482
module Delegator #:nodoc: def self.delegate(*methods) methods.each do |method_name| # 動的にメソッドを定義 define_method(method_name) do |*args, &block| # メソッドgetを実行した時、if respond_to? method_nameはfalseなので一旦無視 return super(*args, &block) if respond_to? method_name # Delegator.targetをレシーバにしてmethod_nameを実行 # Delegator.targetは、メソッドgetを実行した時はSinatra::Application Delegator.target.send(method_name, *args, &block) end private method_name end end # def self.delegateを呼び出し delegate :get, :patch, :put, :post, :delete, :head, :options, :template, :layout, :before, :after, :error, :not_found, :configure, :set, :mime_type, :enable, :disable, :use, :development?, :test?, :production?, :helpers, :settings class << self attr_accessor :target end self.target = Application end
Sinatra::Delegatorはbase.rbで定義されている。で、この中ではdelegate :get, :patch,....でself.delegateを呼び出している。このメソッドは:getなどのシンボルを配列でうけとり、1つ1つのシンボルに対して、define_methodを実行している。
define_methodはrubyのメタプログラミングでよく使うメソッドで、動的にメソッドを定義するもの。def get, def patch...と定義していっていると思えばいい。
参考: http://rurema.clear-code.com/1.9.2/method/Module/i/define_method.html
ではメソッドの内容はどうか。まず謎めいたreturn super(*args, &block) if respond_to? method_nameの部分であるが、メソッドgetを実行した際にはif respond_to? method_nameの値がfalseとなるので、一旦無視する。
次にDelegator.target.send(method_name, *args, &block)というコード。sendというのは、オブジェクトをレシーバにして第1引数に与えられたメソッド名を実行するもの。
参考: http://rurema.clear-code.com/1.9.2/method/Object/i/__send__.html
で、レシーバのDelegator.targetだが、メソッドgetを実行した際にダンプしてみると、Sinatra::Applicationであることがわかる。つまり、Sinatra::Application内のdef getが実行されるということになる。なので、Sinatra::Application内でdef getが定義されている箇所を探す。
https://github.com/sinatra/sinatra/blob/v1.3.0.d/lib/sinatra/base.rb#L1462
class Application < Base set :logging, Proc.new { ! test? } set :method_override, true set :run, Proc.new { ! test? } set :session_secret, Proc.new { super() unless development? } def self.register(*extensions, &block) #:nodoc: added_methods = extensions.map {|m| m.public_instance_methods }.flatten Delegator.delegate(*added_methods) super(*extensions, &block) end end
ここにdef getはない
https://github.com/sinatra/sinatra/blob/v1.3.0.d/lib/sinatra/main.rb
class Application < Base # we assume that the first file that requires 'sinatra' is the # app_file. all other path related options are calculated based # on this path by default. set :app_file, caller_files.first || $0 set :run, Proc.new { $0 == app_file } if run? && ARGV.any? require 'optparse' OptionParser.new { |op| op.on('-x') { set :lock, true } op.on('-e env') { |val| set :environment, val.to_sym } op.on('-s server') { |val| set :server, val } op.on('-p port') { |val| set :port, val.to_i } op.on('-o addr') { |val| set :bind, val } }.parse!(ARGV.dup) end end
こちらにも無い。。。ということは、スーパークラスであるBaseに書かれているのか
https://github.com/sinatra/sinatra/blob/v1.3.0.d/lib/sinatra/base.rb#L1108
# 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メソッドなどを実行している。routeメソッドなどの中身についてはさらに読み込んで行く必要がありそうなので、今日のところは一旦ここまでにしよう。