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
メソッドにより、メソッド名とブロックが渡されて、@conditions
arrayに格納されて、実行される。
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を触ったことがないため、読むにはちょっと難ありだったかも。