LoginSignup
15
14

More than 5 years have passed since last update.

Sinatraのソースを読んでみる

Posted at

Ruby製の軽量フレームワーク、Sinatraについて。
Rails同様、Sinatra初心者の私がソースを読んでみようと思い立った。
結果、Sinatraの理解というよりRubyの勉強になった。

まずは入門サンプル

ググるとよく出てくるやつ

  • クラシックスタイル(Classic Style)
require 'sinatra'

get '/' do
  "Hello World"
end
  • モジュラースタイル(Modular Style)
require 'sinatra/base'

class MyApp < Sinatra::Base
  get '/' do
    'Hello, world!'
  end
end

こんな感じで、すごく簡単に作れてしまう。

ソースを読む

https://github.com/sinatra/sinatra
の中のsinatra/lib/sinatra/base.rbで大体主要な処理を行っている。
この中に書かれているクラスをそれぞれ読んでいってみた。
とりあえず、主要なクラスのみ

class Base

https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb#L875
Sinatraのベースクラス。Modularスタイルで使う場合にこれを継承する。
上のサンプルのgetの実態は下記のようになっているが、次々とよく分からないメソッドが呼び出されていたので追ってみる。

getが呼び出されるとrouteメソッドが呼び出される。内部的にHEADは自動的に呼び出されているよう

# Defining a `GET` handler also automatically defines
# a `HEAD` handler.
def get(path, opts = {}, &block)
  conditions = @conditions.dup
  ## ①routeメソッド呼び出し
  route('GET', path, opts, &block)

  @conditions = conditions
  ## ①routeメソッド呼び出し
  route('HEAD', path, opts, &block)
end

①routeメソッド
getのoptionsにはcondition(条件)が指定できる。
optionsパラメータに:hostのキーがあれば、:hostを削除しroute conditionを追加する??この辺のconditionとか出てくるあたりでいろいろ分からなくなる。

def route(verb, path, options = {}, &block)
  # Because of self.options.host
  ## optionに:hostがあれば②host_nameメソッド呼び出し
  host_name(options.delete(:host)) if options.key?(:host)
  enable :empty_path_info if path == "" and empty_path_info.nil?
  ## ⑥compileメソッド呼び出してsignatureを返す
  signature = compile!(verb, path, block, options)
  (@routes[verb] ||= []) << signature
  ## ⑤invoke_hookメソッド呼び出し
  invoke_hook(:route_added, verb, path, block)
  signature
end

②host_nameメソッドの中身
conditionメソッドを呼び出す。

# Condition for matching host name. Parameter might be String or Regexp.
def host_name(pattern)
  ## ③conditionメソッドの呼び出し
  condition { pattern === request.host }
end

③conditionメソッドの中身
引数のnameとブロックを元に、generate_methodによりメソッドを生成し@conditions配列に追加している。

# Add a route condition. The route is considered non-matching when the
# block returns false.
def condition(name = "#{caller.first[/`.*'/]} condition", &block)
  ## ④generate_methodの呼び出し
  @conditions << generate_method(name, &block)
end

④generate_methodの中身
instance_methodによりUnboundMethodを生成して返す。

def generate_method(method_name, &block)
  method_name = method_name.to_sym
  define_method(method_name, &block)
  method = instance_method method_name
  remove_method method_name
  method
end

⑤invoke_hookの中身
extensionsに対して、引数のメソッドを持っていればそれをe.sendによって実行する。

def invoke_hook(name, *args)
  extensions.each { |e| e.send(name, *args) if e.respond_to?(name) }
end

⑥compile!の中身
genarate_methodにより生成されたUnboundMethodをbindしコールする。(レシーバはなに・・・?)

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, []

  wrapper                 = block.arity != 0 ?
    proc { |a,p| unbound_method.bind(a).call(*p) } :
    proc { |a,p| unbound_method.bind(a).call }
  wrapper.instance_variable_set(:@route_name, method_name)

    [ pattern, keys, conditions, wrapper ]
end

・・・追ってるうちに混乱。
ブロックで定義した部分は最終的にcompile!でメソッドに変換されているような感じがする。

Conditionについてよく分からなかったので調べてみた。
パスの次の引数Hashで渡すもの。
:host_name, :user_agent, :providesがあらかじめ定義されているが、
下記の要領で自分で定義することも可能。

下記のように定義ができる。

def somehing args
  coditions do
    # 処理
  end
end

get '/some_path' :something => :args do
  # 処理
end

実際にリクエストが呼ばれた時に、
routeパスとともに"something"というメソッドが実行され引数には"args"が渡される。
メソッドの定義はconditionメソッドにより、メソッド名とブロックが渡されて、@conditionsarrayに格納されて、実行される。

class Application < Base

Classicスタイルアプリケーションの基本となる実行クラス。

  # Execution context for classic style (top-level) applications. All
  # DSL methods executed on main are delegated to this class.
  #
  # The Application class should not be subclassed, unless you want to
  # inherit all settings, routes, handlers, and error pages from the
  # top-level. Subclassing Sinatra::Base is highly recommended for
  # modular applications.
  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? }
    set :app_file, nil

    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

Classicスタイルで使う時に、こんな感じでconfig.ruに記載するのをよく見かける

require './app'
run Sinatra::Application

module Delegator

Mixin委譲モジュール。

  # 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

delegateメソッドの引数にgetやらpostやらのhttpメソッドを渡し、

Delegator.target.send(method_name, *args, &block)

により、target(委譲先はSinatra::Application)でメソッドを呼んでいる。

尚、委譲先がSinatra::Applicationになるのはこうなっているため

self.target = Application

ここまで細かく読む必要はないかもしれないが、
ちょっと時間を置いてまた読み返す必要がありそう。

あまりrubyを触ったことがないため、読むにはちょっと難ありだったかも。

15
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
14