scalaxb-appengine

scalaxb-appengine は scalaxb を web上で使うための RESTful API です.n8han/Unfiltered を使ってて,ソースは eed3si9n/scalaxb-appengine にあります.

compile

URL:
http://scalaxb.appspot.com/compile/{output}.{format}
フォーマット:
scala, zip
HTTP メソッド:
POST
HTTP エンコーディング:
multipart/form-data
パラメータ:
- arg: 省略可,複数化.スキーマファイルの URL.
- argf: 省略可,複数化.アップロードするスキーマファイル.
- defaultPackageName: 省略可.対象パッケージ.
- namespaceURI: 省略可,複数化.パッケージする名前空間 URI.
- packageName: 省略可,複数化.namespaceURI によって指定された名前空間の対象パッケージ.
- classPrefix: 省略可,生成されるクラス名のプレフィックスを指定します.
- paramPrefix: 省略可,生成される仮引数名のプレフィックスを指定します.
- wrapContents: 省略可,複数化.別の case class に追い出す複合型を指定します.
備考:
- argargf かのどちらか一つは指定して下さい.
- n番目の packageName が n番目の namespaceURI の対象パッケージを指定します.

開発ノート

これは Scala で書かれたコンソールアプリの適切なデプロイ方法を僕が分からないために生まれました.Greg が scalaxb を web service にしてはどうかと勧めてくれ,そのうち書こうとは思っていましたが折角のチャンスだということで勢いで書いてしまいました.

まず Play! frameworkScala moduleGoogle App Engine module を一緒に使って scalaxb を包んでみました.ところが残念なことに Scala module Play 1.1-unstable でしか動かず,GAE module は 1.0-stable でしか動きません.一日かけてフレームワークを使ってみて,コードを書き終えて,App Engine にデプロイしてからの撃破だったので,ちょっと残念でした.Play そのものに関しては面白かったと思います.デプロイ無し開発はコード-チェックのサイクルが短くなって,Rails とか JRebel とかがこんな感じなのかと思いました.Groovy ベースのテンプレート言語はどうかなと思いましたが,Scalate で取り替えることができるみたいです.ちょっと気になるのが,こんだけ機能豊富だと App Engine に搭載した時の spin-up 時間も気になります.次に,pinky も試してみましたが.Scala 2.8.0 ではコンパイルできず.

気づいたのが,このサービス書くのに別に MVC フレームワークいらないってことです.REST しか要らないなら Servlet でも噛んでくることはないと思います.まずは NetBeans 6.9 を試しがてらに書きましたが,TextMate と sbt に戻っちゃいました.@yasushia氏の sbt-appengine-plugin を使っています.

import javax.servlet.http.{HttpServlet,
  HttpServletRequest => HSReqeust,
  HttpServletResponse => HSResponse}
 
class CompilerServlet extends HttpServlet {
  override def doPost(request : HSReqeust, response : HSResponse) {
    // grab the input from the params
    val args = request.getParameterValues("arg")
    ...    
 
    // call scalaxb
    val module = org.scalaxb.compiler.xsd.Driver
    module.processReaders(inputToOutput, packageNames)
 
    // return the output
    response.setContentType("application/octet-stream")
    response.setHeader("Content-disposition", "attachment; filename=\"" +
                       outFileName + "\"")
    response.getWriter.print(scalaFiles(0).content)
  }
}

構造としてはだいたい以上の感じで /compile/a.scala と来るリクエストをさばきます.フロントエンドに関しては別に html と ajax でいけそうなので,わざわざ jsp で作ったりとかはしませんでした.ちょっと問題も色々あって,例えばパラメータの検証ロジックとアウトプットのロジックがごちゃごちゃ混ざってたり,Servlet API が Java 的なくどい感じだったりして Scalaっぽくないのが嫌かなと思います.

ここで,@n8han から tweet をいただきました

@eed3si9n You should try this thing, which is like that thing but better: http://github.com/n8han/unfiltered

@eed3si9n アレに似てるけどもっといけるから使ってみて: http://github.com/n8han/unfiltered

これはヤバい.自分でも気づいていませんでしたが,まさに必要としているものでした.要約すると,MVC の controller を Scala のパターンマッチングで表現したものです.パターンマッチングを使うことで type-safe な検証を簡単に書く事ができ,また組み込みの DSL によってレスポンスも簡潔に表現することができます.

例えば,arg というパラーメータが渡されたかどうかのチェックを前は以下のようにやっていました:

val args = request.getParameterValues("arg")
if (args == null) {
  response.sendError(HSResponse.SC_BAD_REQUEST)
    return
}

Scala を書いているときは基本的には null とか return とか見たくないですね.Unfiltered で書くとこうなりました:

{
  req @ POST(UFPath(Seg("compile" :: what :: Nil)) & Params(params)) =>
  params("arg") match {
    case Nil => BadRequest ~> ResponseString("missing arg.")
    case args => ...
  }
}

実は "/compile/*" をマップする振り分け処理の一部として検証が行われています.以下が CompilerFilter の全ソースです:

package org.scalaxb.servlet
 
import org.scalaxb.servlet.model.{Driver, ScalaFile}
import java.net.{URL, MalformedURLException}
import java.io.{File}
import unfiltered.request._
import unfiltered.response._
import unfiltered.request.{Path => UFPath}
 
class CompilerFilter extends unfiltered.Planify ({
  case req @ POST(UFPath(Seg("compile" :: what :: Nil)) & Params(params)) => 
      params("arg") filter { "" != } match {
    case Nil => BadRequest ~> ResponseString("missing arg.")
    case args =>
      try {
        val urls = args map { new URL(_) }
        val defaultPackageName = params("defaultPackageName").headOption
        val scalaFiles = Driver.process(urls, defaultPackageName)
 
        ContentType("application/octet-stream") ~> 
        ResponseHeader("Content-disposition",
          Seq("attachment; filename=\"" + what + "\"")) ~>
        ("""([.]\w+)$""".r.findFirstIn(what) match {
          case Some(".scala") => ResponseString(scalaFiles.head.content)
          case Some(".zip") => ResponseBytes(ScalaFile.zip(scalaFiles))
          case _ => BadRequest ~> ResponseString("unsupported file extension.")
        })
      }
      catch {
        case e: MalformedURLException =>
          BadRequest ~> ResponseString(e.getMessage)
      }
  }
})

面白いのが,以上で全てってことです.最初に書いてしまえば web.xml には手を加える必要はなくて,他に特に設定ファイルもありません.またテンプレート言語処理のようなものもありません.RESTful API の実装なんかには Unfiltered というのも選択肢に入ってくるんじゃないかなと思います.今回気づかないで使いませんでしたが,プロジェクトを始めるための unfiltered-template もありました. 2011年現在では softprops / unfiltered.g8

参考にしたリンクです.