Scalatra-JSONがレスポンスを返す仕組みについて書こうとしたら、その前にそもそもScalatraがレスポンスを返す仕組みを解説しないと分かりづらいな、と思ったので、まとめます。
Scalatraがレスポンスを返す仕組み
Scalatraでは、下記のようにget
やpost
といったメソッド名に、対応するパスとコードブロックを渡すことで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型
であればそのままステータスコードとしてのみ使われます。特に適合するものが無ければ、最後に文字列化されてresponse
のwriter
に渡されます。先ほどの例でいけば特にXML型特有のコードはここには無いので、case x =>
のパターンにマッチして<h1>Hello, {params("name")}</h1>
が文字列として返されます。
ソースコードの全体は以下のリンクから参照してみてください。