Sinatraのソースを読んでみる

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

# まずは入門サンプル
ググるとよく出てくるやつ

* クラシックスタイル(Classic Style)

```rb
require 'sinatra'

get '/' do
  "Hello World"
end
```

* モジュラースタイル(Modular Style)

```rb
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は自動的に呼び出されているよう

```rb
# 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とか出てくるあたりでいろいろ分からなくなる。


```rb
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メソッドを呼び出す。

```rb
# 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```配列に追加している。

```rb
# 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を生成して返す。

```rb
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```によって実行する。

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

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

```rb
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があらかじめ定義されているが、
下記の要領で自分で定義することも可能。

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

```rb
def somehing args
  coditions do
    # 処理
  end
end

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

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



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

```rb
  # 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に記載するのをよく見かける

```rb
require './app'
run Sinatra::Application
```


## module Delegator

Mixin委譲モジュール。

```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
```

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

```rb
Delegator.target.send(method_name, *args, &block)
```
により、target(委譲先はSinatra::Application)でメソッドを呼んでいる。

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

```rb
self.target = Application
```


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