Magnolia Tech

いつもコードのことばかり考えている人のために。

Scalatraがレスポンスを返す仕組み

Scalatra-JSONがレスポンスを返す仕組みについて書こうとしたら、その前にそもそもScalatraがレスポンスを返す仕組みを解説しないと分かりづらいな、と思ったので、まとめます。

Scalatraがレスポンスを返す仕組み

Scalatraでは、下記のようにgetpostといったメソッド名に、対応するパスとコードブロックを渡すことでWebアプリケーションとしての振る舞いを定義していきます。

package com.example.app
import org.scalatra._

class HelloWorldApp extends ScalatraFilter {
  get("/") {
    <h1>Hello, {params("name")}</h1>
  }
}

このメソッド名形式での定義は、CoreDslというtraitの中で定義されています。例えばgetメソッドは以下のように定義されていて、transformersがパス名を、Any型を返すコードブロックが実際のWebアプリケーションの挙動を定義するコードブロックを示します。

def get(transformers: RouteTransformer*)(block: => Any): Route

Scalaでは、コードブロックの最後の値が、コードブロック全体の返り値となるので、上記のHelloWorldAppの例で言えば、最後の値である<h1>Hello, {params("name")}</h1>がコードブロック全体の帰り値となります。ScalaではXMLリテラルが有るので、このコードブロックの値はXML型(scala.xml.Elem)になります(型推論される)。

メソッドの引数定義が=> Anyになっているので、getメソッドはAny型として受け取ります。

そして、最終的にScalatraBaseというtraitのrenderPipelineというメソッドで処理されます。詳細は省きますが、renderPipelineはpartial functionとして定義されていて、後から色々なパターン(case)への対応を追加できるようになっています(例えば、JSONのASTを表す型への対応はScalatraBaseには含まれていません)。

renderPipelineメソッドの一部を引用します。

protected def renderPipeline: RenderPipeline = {
    case 404 =>
      doNotFound()
    case ActionResult(status, x: Int, resultHeaders) =>
      response.status = status
      resultHeaders foreach {
        case (name, value) => response.addHeader(name, value)
      }
      response.writer.print(x.toString)
    case status: Int =>
      response.status = status
    case bytes: Array[Byte] =>
      if (contentType != null && contentType.startsWith("text")) response.setCharacterEncoding(FileCharset(bytes).name)
      response.outputStream.write(bytes)

    case x =>
      response.writer.print(x.toString)
  }

パターンマッチは上から順に適合性をチェックしていくので、コードブロックの返り値が404だったらdoNotFoundメソッドが実行され、単なるInt型であればそのままステータスコードとしてのみ使われます。特に適合するものが無ければ、最後に文字列化されてresponsewriterに渡されます。先ほどの例でいけば特にXML型特有のコードはここには無いので、case x =>のパターンにマッチして<h1>Hello, {params("name")}</h1>が文字列として返されます。

ソースコードの全体は以下のリンクから参照してみてください。

github.com

Actions - Scalatra