複数のコンフィギュレーション

ここ最近 scalaxb を複数のコンフィギュレーションで使う sbt ビルドの設定の方法について何人かの方に聞かれたので、ちょっとみてみよう。
コードは以下から入手してほしい:

$ git clone -b multipleconfigs git://github.com/eed3si9n/scalaxb-sample.git
$ cd scalaxb-sample/multipleconfigs/

好きなエディタでディレクトリごと開く。src/ 内には main/scala/main.scalamain/xsd/ipo.xsdmain/xsd/w.xsd が入っている。

複数の config はいつ必要?

単一 config の場合は、全てのスキーマの型クラスのインスタンスが xmlprotocol.scala に含まれる。現行では、参照されているスキーマは一緒にコンパイルされないければいけないので、通常はこの方法を取ることになる。

それでは、いつ複数の config を使うべきだろうか? 複数のコンフィギュレーションを使うことで、スキーマを別々にコンパイルして xmlprotocol.scala を独立して生成することができる。分けることでコードのサイズを小さく抑えたり、別のパッケージ下に生成したい場合に有効だ。

スキーマ

サンプル同様に、ipo.xsdw.xsd という二つのスキーマが src/main/xsd/ 下にあることを仮定する。一つは international purchase order で、もう一つは weather だ。

project/plugins.sbt

第一に、ビルドに sbt-scalaxb を加えるために、以下を project/plugins.sbt に書く:

addSbtPlugin("org.scalaxb" % "sbt-scalaxb" % "X.X.X")
 
resolvers += Resolver.sonatypeRepo("public")

build.sbt

次に、build.sbt をみていく。マルチ・プロジェクト build.sbt ビルド定義ファイルは以下のようになる:

lazy val commonSettings = Seq(
  version := "0.1",
  organization := "com.example",
  scalaVersion := "2.11.5"
)
 
lazy val scalaXml = "org.scala-lang.modules" %% "scala-xml" % "1.0.2"
lazy val scalaParser = "org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.1"
 
lazy val app = (project in file(".")).
  settings(commonSettings: _*).
  settings(
    name := "scalaxb-multipleconfigs-sample",
    libraryDependencies ++= Seq(scalaXml, scalaParser)
  )

取り敢えず import 節を加えて sbt-scalaxb を使えるようにする:

import ScalaxbKeys._

sbt-scalaxb は通常 scalaxb タスクと Compile コンフィギュレーションにスコープ付けされた sources キーを用いてどのスキーマがコンパイルされるかを決定する。カスタムのコンフィギュレーションを作ることで、複数セットの sources を作れる。使う予定のパッケージ名にあわせて config 名を決めた:

lazy val Ipo = config("ipo") extend(Compile)
lazy val W = config("w") extend(Compile)

次に、config 内での scalaxb のセッティングをある程度一般化できるようならば、それを作るメソッドを定義する。ipo.xsdw.xsd という名前にしてあるので、それを base に使ってスキーマのファイル名、パッケージ名、プロトコルファイル名を決める:

def customScalaxbSettings(base: String): Seq[Def.Setting[_]] = Seq(
  sourceManaged := (sourceManaged in Compile).value / "sbt-scalaxb" / base,
  generateRuntime := false,
  sources := Seq(xsdSource.value / (base + ".xsd")),
  packageName := base,
  protocolFileName := base + "_xmlprotocol.scala"
)

次に、セッティングを組み合わせていく:

def codeGenSettings: Seq[Def.Setting[_]] =
  inConfig(Ipo)(baseScalaxbSettings ++ inTask(scalaxb)(customScalaxbSettings("ipo"))) ++
  inConfig(W)(baseScalaxbSettings ++ inTask(scalaxb)(customScalaxbSettings("w"))) ++
  Seq(
    sourceGenerators in Compile += (scalaxb in Ipo).taskValue,
    sourceGenerators in Compile += (scalaxb in W).taskValue,
    // Runtime needs to be generated only once
    generateRuntime in (Ipo, scalaxb) := true
  )

上記のとおり、カスタムのセッティングは scalaxb タスクとカスタムのコンフィギュレーションにスコープ付けされている。後半で、scalaxb in Iposscalaxb in W のタスクを source generator として登録している。最後に、codeGenSettings をプロジェクトに追加する:

lazy val app = (project in file(".")).
  settings(commonSettings: _*).
  settings(codeGenSettings: _*).
  settings(
    name := "scalaxb-multipleconfigs-sample",
    libraryDependencies ++= Seq(scalaXml, scalaParser)
  )

まとめると、こうなる:

import ScalaxbKeys._
 
lazy val Ipo = config("ipo") extend(Compile)
lazy val W = config("w") extend(Compile)
 
lazy val commonSettings = Seq(
  version := "0.1",
  organization := "com.example",
  scalaVersion := "2.11.5"
)
 
lazy val scalaXml = "org.scala-lang.modules" %% "scala-xml" % "1.0.2"
lazy val scalaParser = "org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.1"
 
lazy val app = (project in file(".")).
  settings(commonSettings: _*).
  settings(codeGenSettings: _*).
  settings(
    name := "scalaxb-multipleconfigs-sample",
    libraryDependencies ++= Seq(scalaXml, scalaParser)
  )
 
def codeGenSettings: Seq[Def.Setting[_]] =
  inConfig(Ipo)(baseScalaxbSettings ++ inTask(scalaxb)(customScalaxbSettings("ipo"))) ++
  inConfig(W)(baseScalaxbSettings ++ inTask(scalaxb)(customScalaxbSettings("w"))) ++
  Seq(
    sourceGenerators in Compile += (scalaxb in Ipo).taskValue,
    sourceGenerators in Compile += (scalaxb in W).taskValue,
    // Runtime needs to be generated only once
    generateRuntime in (Ipo, scalaxb) := true
  )
 
def customScalaxbSettings(base: String): Seq[Def.Setting[_]] = Seq(
  sourceManaged := (sourceManaged in Compile).value / "sbt-scalaxb" / base,
  generateRuntime := false,
  sources := Seq(xsdSource.value / (base + ".xsd")),
  packageName := base,
  protocolFileName := base + "_xmlprotocol.scala"
)

これで、compile を sbt シェルから呼び出すと ipo.xsd と w.xsd に対して別々にデータバインディングが生成される。

使ってみる

生成された型クラスのインスタンスは自動的にパッケージオブジェクトに入っているので、import 節無しで使えるようになっている:

object Main extends App {
  val shipto = <shipto xmlns="http://www.example.com/IPO">
      <name>foo</name>
      <street>1537 Paper Street</street>
      <city>Wilmington</city>
      <state>DE</state>
      <zip>19808</zip>
    </shipto>
  println(scalaxb.fromXML[ipo.USAddress](shipto))
 
  val weather = <weather xmlns="http://www.example.com/weather">
      <zip>19808</zip>
      <status>Sunny</status>
    </weather>
  println(scalaxb.fromXML[w.Weather](weather))
}

これを以下のように sbt シェルから実行する:

> run
....
USAddress(foo,1537 Paper Street,Wilmington,DE,19808)
Weather(19808,Sunny)