Ruby用API生成フレームワーク「grape」のコードを読んでみる (後半)


前回からの続きでRuby用のAPI生成フレームワーク grape のコードを読んで見たのでメモ。

前半は初期化処理を見た。今回は、リクエスト処理の部分を見ていく。

まとめ

  • マウントしたクラス => マウントしたクラスのシングルトンなインスタンス => Grape::Routerと呼び出していき、ここでリクエストに対応する Grape::Router::Routeインスタンスを検索する。その後、Grape::Router::Routeが内部で保持するGrape::Endpointのインスタンスに処理を委譲している。
  • リクエストに対応するRouteの検索は正規表現で一発、さらに名前付きのキャプチャを使用して高速化している。ここは、こういった方法があるのかと勉強になった。
  • 各Endpointは最初のリクエストが来た時に初期化処理を行うため、1回目のリクエストの処理は2回目以降に比べて少し時間がかかると思われる。
  • Grape::Endpointのインスタンスをリクエストごとにdupし #call! を実行する。
  • チェーンされている各種Middlewareを実行し、最終的に Grape::Endpoint#run に到達する。
  • Grape::Endpoint#run では各種callbackの実行しつつ、自身がAPIの処理として定義したブロックを実行し、Rackレスポンスとしての結果を返却する。
  • 前半に比べると読みやすい印象。

詳細

リクエスト到達時の挙動

前回同様 Grape::API.call が呼ばれるところからスタートです。

今回からコード中に「※番号」を記載して、そこの説明をしていく形にしていきます。

  1. 前回リーディングした部分。Grape::APIのシングルトンなインスタンスが作成済みで、コンパイル済み(リクエストのメソッドとパスから、対応するAPIを決定可能)のGrape::Routerを @router に保持している。
  2. 今回はここの中身を見ていく。Rackアプリケーションとしてマウントされているので、3つの要素(ステータスコード、HTTPヘッダーのHash、ボディの配列)を持つArrayを返さないといけない。
  3. Grape::API.call はこの結果をそのまま返している。 さらにこのメソッドでは、@router で保持しているGrape::Routerインスタンスの #call メソッドの結果をほぼそのまま返している。次節でこの中身をリーディングする。
  4. カスケードオプションがfalseの場合、ここで「X-Cascade」ヘッダーを取り除いている。 「X-Cascade」ヘッダーに 'pass' という文字列が入っていると、404エラーを返しても次のrouteを検索するらしい。デフォルトはtrue。

Grape::Routerでの処理

  1. #with_optimization をブロック付きで実行。routerのコンパイルしていない場合コンパイルし、ブロックの結果を返却する。
  2. #identity でリクエストに対する処理とレスポンスを取得している。今回は、ここでresponseがちゃんと返却されるケースを見ていく。

Grape::Router#identity

  1. #transaction をブロック付きで実行。このメソッドでは、パスとHTTPメソッドを渡してブロックを実行し、結果のレスポンスを返す。しかし、routeが見つからなかった場合、その代替策の処理も合わせて実行している。
  2. メイン処理の1つ。パス(/healthcheck)とHTTPメソッド(GET)に対応する Grape::Router::Route を探索し返す。
  3. メイン処理のもう1つ。routeが見つかった場合、リクエストを処理する。
  4. ルート探索は正規表現1回で行われている。正規表現のどの名前付きキャプチャにヒットしたかで、対応するrouteを高速に検索できる様な工夫がされている。
  5. パスに含まれるパラメーター(例えば「/users/:id」の:id)を抽出して、 'grape.routing_args' というキーにそのHashをenvに登録している。これは mustermann というGemを使って実現している。
  6. メイン処理の続き。次節で内容を見る。

Grape::Router::Routeでの処理

  1. @app.call に処理を委譲。 @app には「GET /healthcheck」に対応する Grape::Endpoint インスタンスが入っている。

Grape::Endpointでの処理

Grape::Endpoint#call

  1. 遅延初期化している模様。これから1回目のリクエスト処理時は、2回目以降に比べ少し時間がかかると思われる。
  2. このインスタンスを複製して #call! を実行する。インスタンスを複製するのは、インスタンス変数をリクエストごとに独立して管理できるようにするためと想定。
  3. build_helpersはAPIクラスで定義したhelperメソッドを全てincludeした無名Module。これを Grape::Endpoint の無名サブクラスである self.class にincludeする。これにより、このクラスのインスタンス内でhelperで定義したメソッドが使用可能になる。
  4. 今回のケースでは、 options[:app] はnilなので、 #build_stack が実行される。中身の詳細は省くが、結果としては各Middlewareがチェーン状に繋がっていて、最終的に env[Grape::Env::API_ENDPOINT].run を実行するオブジェクトを返却する。これを @app にセットする。

Grape::Endpoint#call!

  1. 前節の builder.run ->(env) { env[Grape::Env::API_ENDPOINT].run } に対応。
  2. これによりMiddlewareの処理を実行し、最終的に #run が呼び出される。
  3. ここで #params#headers で情報を取得できるようにしている。
  4. 前回リーディングした内容で、ここでようやく自分が get 'healthcheck' で定義したブロックをselfにbindして実行している。結果のHashオブジェクトがresponse_objectに格納される。
  5. ステータス、HTTPヘッダーの内容、ボディの配列の3つの配列を返して終了。ボディの内容は、Middlewareを通る際にHashからJSON文字列に変換され、さらに Rack::Response でラップされる。

以上でもっとも簡単なAPIの一通りの処理を見ることができた。疲れた…。