Sinatraコードリーディングの第二回目です🎩
前回は、ruby app.rb実行後にServerが起動するまでのコードを読んでみましたが、今回はServerが起動したあとに特定のURLにアクセス際にapp.rbで定義した処理が実行されるまで、どのような動きになっているのかを追ってみました🕵️♀️
👇ちなみに前回の内容はこちらです。
今回のサンプルアプリケーション
ではまず、今回のサンプル実装を見ていきます。今回はなるべくシンプルにルート/にアクセスしたらHello Worldが標準出力されるようなアプリケーションで実際にルートパスにアクセスして標準出力されるまでを見ていきます。
require 'sinatra' get '/' do puts "Hello World" end
routingの定義
ではまずrouting(URLと処理の紐付け)を行っている。#getの実装を見てみることにします。このメソッドで実行してるのは下記の2つ。
- 条件の取得・設定
- routeの定義
# 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('HEAD', path, opts, &block) end
今回は条件を指定していないので、routeの定義を行っている#route見ていきます。
ここで行われているのは下記です。
- pathに空文字が指定されていた場合に、singleton_classに
empty_path_infoを定義 #compile!を実行してurlと処理を解決するためのsignature(URLと処理を持つもの)を取得して@routesに登録- 拡張機能(http://sinatrarb.com/extensions-wild.html)の実行
- signatureを返却
def route(verb, path, options = {}, &block) # pathに空文字が指定されていた場合に、singleton_classに`empty_path_info`を定義 enable :empty_path_info if path == "" and empty_path_info.nil? # sigature(URLパターンと処理の組み合わせ)の取得 signature = compile!(verb, path, block, options) # routesへの追加 (@routes[verb] ||= []) << signature # 拡張機能の実行 invoke_hook(:route_added, verb, path, block) signature end
@routesにsignatureが追加されることによりSinatraにapp.rbで定義したroutingが追加されるようですね👀
では実際のsignatureを取得している#compile!中身を見ていきます。
行っているのは下記です。ざっくりいうとオプションを解決して、URLと処理を持つオブジェクトを返しているようです。
- host名やURLパターン、その他のオプションの反映
- URLパターン解決用のオブジェクトを生成
- verbとpathでメソッド名を生成(例:
GET /) - メソッド名とapp.rbで定義したblockからメソッドを作成
- メソッドを実行するProcオブジェクトをwrapperとして定義
- URLパターン解決用のオブジェクト、条件、wrapperを返却
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) } # URLパターンマッチ用のオブジェクトの作成 # https://github.com/sinatra/mustermann/tree/master/mustermann pattern = compile(path, route_mustermann_opts) # blockとHTTPメソッド、URLからメソッドを生成してwrapperに登録 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 } # URLパターンと条件とwrapper(処理)を配列にして返却 [ pattern, conditions, wrapper ] end def compile(path, route_mustermann_opts = {}) Mustermann.new(path, mustermann_opts.merge(route_mustermann_opts)) end
ここまでで、app.rbにgetで定義した内容がGET /メソッドで実行されるようになり、URLパターンを解決するオブジェクトも作成されました🙌
Sinatraは、URLパターンの解析にMustermannという独自のGemを使っているようですね👀
リクエストの取得、処理の開始
ではここから具体的にリクエストが発生してどう定義したメソッドが実行されるのか見ていきます🕵️♀️
Rackが起動しているときに実行される処理が#callなので、そこから見ていきましょう。
To use Rack, provide an "app": an object that responds to the call method, taking the environment hash as a parameter, and returning an Array with three elements: https://rack.github.io/
色々やっているのですが、今回のURLの解決と処理の実行に関するところで#callでやっていることは下記の通りです。
- Rackからの引数(env)を取得
- リクエストのオブジェクト(
Request < Rack::Request)を生成 dispatch!でURLパターンに紐づく処理を実行- responseを返却
# Rack call interface. def call(env) dup.call!(env) end def call!(env) # :nodoc: # Rackから渡されたリクエストの内容を取得 @env = env @params = IndifferentHash.new # Responseオブジェクトの生成 @request = Request.new(env) @response = Response.new template_cache.clear if settings.reload_templates @response['Content-Type'] = nil # 処理の実行 invoke { dispatch! } invoke { error_block!(response.status) } unless @env['sinatra.error'] unless @response['Content-Type'] if Array === body and body[0].respond_to? :content_type content_type body[0].content_type else content_type :html end end @response.finish end
@requestにURL等のリクエストの情報が格納され、#dispatch!で処理が実行されるようです👀
ちなみにenvに渡される値は下記のようなhashです。
{"GATEWAY_INTERFACE"=>"CGI/1.1",
"PATH_INFO"=>"/",
"QUERY_STRING"=>"",
"REMOTE_ADDR"=>"::1",
"REMOTE_HOST"=>"::1",
"REQUEST_METHOD"=>"GET",
"REQUEST_URI"=>"http://localhost:4567/",
"SCRIPT_NAME"=>"",
"SERVER_NAME"=>"localhost",
"SERVER_PORT"=>"4567",
"SERVER_PROTOCOL"=>"HTTP/1.1",
"SERVER_SOFTWARE"=>"WEBrick/1.4.2 (Ruby/2.6.3/2019-04-16)",
"HTTP_HOST"=>"localhost:4567",
"HTTP_CONNECTION"=>"keep-alive",
"HTTP_CACHE_CONTROL"=>"max-age=0",
"HTTP_UPGRADE_INSECURE_REQUESTS"=>"1",
"HTTP_USER_AGENT"=>
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
"HTTP_ACCEPT"=>
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
"HTTP_ACCEPT_ENCODING"=>"gzip, deflate, br",
"HTTP_ACCEPT_LANGUAGE"=>"ja,en-US;q=0.9,en;q=0.8",
"HTTP_COOKIE"=>
"_dogfeeds_session=jU5sXy4p7LtgpehIempTj%2BTlernru9duDYax8nYGoPo%2BRTOkOK2hB0SY19cYYlCz32yoHKBx%2FFok8bGRc6oUZ%2BS51S2DqpnvvpU6ZB65looxi1wqJVm15tMJLw9BZa4Hon67ewTt7RxsgXDlNxM%3D--ZK%2FIVj5c7IQkes2%2F--9Tv%2Bcdzo8OAPtAGO%2BaofLw%3D%3D",
"rack.version"=>[1, 3],
"rack.input"=>#<StringIO:0x00007f86651ba308>,
"rack.errors"=>#<IO:<STDERR>>,
"rack.multithread"=>true,
"rack.multiprocess"=>false,
"rack.run_once"=>false,
"rack.url_scheme"=>"http",
"rack.hijack?"=>true,
"rack.hijack"=>
#<Proc:0x00007f86651b9cc8@sinatora_sample/vendor/bundle/ruby/2.6.0/gems/rack-2.0.7/lib/rack/handler/webrick.rb:74 (lambda)>,
"rack.hijack_io"=>nil,
"HTTP_VERSION"=>"HTTP/1.1",
"REQUEST_PATH"=>"/",
"sinatra.commonlogger"=>true,
"rack.logger"=>
#<Logger:0x00007f86651b99d0
@default_formatter=
#<Logger::Formatter:0x00007f86651b9930 @datetime_format=nil>,
@formatter=nil,
@level=1,
@logdev=
#<Logger::LogDevice:0x00007f86651b9868
@dev=#<IO:<STDERR>>,
@filename=nil,
@mon_count=0,
@mon_mutex=#<Thread::Mutex:0x00007f86651b9750>,
@mon_mutex_owner_object_id=70107599326260,
@mon_owner=nil,
@shift_age=nil,
@shift_period_suffix=nil,
@shift_size=nil>,
@progname=nil>}
URL解決と処理の実行
では実際に処理が行われる#dispatch!を見ていきます。実行されるのは下記のような内容です。(Errorハンドリング等は一旦無視します😇)
- requestのparamsをparamsに追加
#route!を呼び出し紐づく処理の実行
def dispatch! # requestのparamsをparamsに追加 @params.merge!(@request.params).each { |key, val| @params[key] = force_encoding(val.dup) } invoke do # 静的ファイルへのアクセスに関する処理 static! if settings.static? && (request.get? || request.head?) # こういうbefore_action的なやつの実行 # before do # @note = 'Hi!' # request.path_info = '/foo/bar/baz' # end filter! :before # URLの解決と処理の実行 route! end rescue ::Exception => boom invoke { handle_exception!(boom) } ensure begin filter! :after unless env['sinatra.static_file'] rescue ::Exception => boom invoke { handle_exception!(boom) } unless @env['sinatra.error'] end end
#route!で実際に処理が実行されているので中身を見ていきます。下記が実行されているようです👀
request.request_method(GET etc...)を持つrouteを取得- routeの中身は
compile!で作ったURL解決用のオブジェクトと条件と処理の配列 [[#<Mustermann::Sinatra:"/">,[], #<Proc:0x00007ff033a1e7a0...]]
- routeの中身は
- envの上書きと処理の実行を行うblock、URLパターン、条件を引数に
#process_routeを呼び出す。
def route!(base = settings, pass_block = nil) # GETとかリクエストメソッド単位でrouteの一覧を取得 if routes = base.routes[@request.request_method] routes.each do |pattern, conditions, block| 取得したrouteの数だけ実行 returned_pass_block = process_route(pattern, conditions) do |*args| env['sinatra.route'] = "#{@request.request_method} #{pattern}" route_eval { block[*args] } end # don't wipe out pass_block in superclass pass_block = returned_pass_block if returned_pass_block end end end def route_eval throw :halt, yield end
いよいよ最後です!#process_route内のblock ? block[self, values] : yield(self, values)の部分でURLパターンの解決と処理の実行がされます!🎉
def process_route(pattern, conditions, block = nil, values = []) # リクエストのURLの取得 route = @request.path_info # routeが空文字だったら`/`に置換する。 route = '/' if route.empty? and not settings.empty_path_info? # 最後に/がついてたら除去(-2は最後以外と取得する書き方) route = route[0..-2] if !settings.strict_paths? && route != '/' && route.end_with?('/') # パターンに一致しなかったらreturn return unless params = pattern.params(route) params.delete("ignore") # TODO: better params handling, maybe turn it into "smart" object or detect changes # encodingの設定 force_encoding(params) # パラメーターの取得 original, @params = @params, @params.merge(params) if params.any? # なんかパラメーターオブジェクトの属性をチェックして処理を切り替えてる?よくわからなかった・・・。 regexp_exists = pattern.is_a?(Mustermann::Regular) || (pattern.respond_to?(:patterns) && pattern.patterns.any? {|subpattern| subpattern.is_a?(Mustermann::Regular)} ) if regexp_exists captures = pattern.match(route).captures.map { |c| URI_INSTANCE.unescape(c) if c } values += captures @params[:captures] = force_encoding(captures) unless captures.nil? || captures.empty? else values += params.values.flatten end catch(:pass) do # 条件をクリアしなったら処理を抜ける conditions.each { |c| throw :pass if c.bind(self).call == false } # yieldで#routeから渡されたblockを実行 block ? block[self, values] : yield(self, values) end rescue @env['sinatra.error.params'] = @params raise ensure @params = original if original end
ここで実際にapp.rbで定義した処理が実行されました!🎊
おさらい
大まかな流れをおさらいすると、
- まずapp.rbに
getメソッドでURLと実行する処理をblockで定義する #getメソッドで#routeメソッド呼び出し、#complile!メソッドが実行される。complile!メソッド内で、URL解決用のオブジェクト、条件、Procオブジェクトを@routeに登録される。Rackの仕組みでServerへリクエストが発生した際に#callが実行され、@request等の取得と#dispatch!メソッドが実行される#dispatch!メソッドで、パラメーターが処理され、#route!メソッドが呼ばれる。#route!は、@requestのメソッドからrouteの一覧を取得し、#route_eval!メソッドにblockを渡して実行するblock、URLパターン、条件を引数に#process_routeを呼び出す。#process_routeが実際にURLの解決と条件のチェック、そして処理の実行を行う🎉
こんな感じのようですね!🕵️♀️
おわりに
実際に処理が実行される流れをみてみましたが、Server起動よりも結構複雑ですね💦(特にURL解決周り)
ですがRackを継承したSinatra独自のリクエストクラスを使っていたものの、リクエストの取得等がRackの部分で処理が行われていて以外とシンプルでした(Rackすごい・・・!)👀
引き続きRackは理解を深めていきたいですね💪
しかしSinatraでも結構複雑だからrailsはどうなってるんだろう🙄
次回はView(erb)の解決とResponseのbodyの生成部分を読んでみようと思います(これで一通り動きの概要は理解出来そうです)🙋
それでは👋