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>が文字列として返されます。
ソースコードの全体は以下のリンクから参照してみてください。