はじめに
Play Frameworkにはcase classからJSONへ変換するためのインスタンスを自動で作るマクロがある。一方でなんらかの型をXMLへ変換する機能はない。そこでこの記事では、まずPlayがどのようにcase classをJSONへ変換しているのかを軽く解説してそれのXML版を作り、最後にScalaのマクロを用いてcase classからXMLを生成するためのインスタンスを自動生成する。
Playが型A
をJSONへ変換するとき
Playが型A
をJSONへ変換する際にはWrites[A]
という型クラスのインスタンスを要求する。このWrites
は次のような型クラスになっている。
trait Writes[-A] {
/**
* Convert the object into a JsValue
*/
def writes(o: A): JsValue
/**
* Transforms the resulting [[JsValue]] using transformer function
*/
def transform(transformer: JsValue => JsValue): Writes[A] = Writes[A] { a => transformer(this.writes(a)) }
/**
* Transforms resulting [[JsValue]] using Writes[JsValue]
*/
def transform(transformer: Writes[JsValue]): Writes[A] = Writes[A] { a => transformer.writes(this.writes(a)) }
}
transform
はwrites
から作られているので、ひとまずここでは無視すると、型クラスWrites[A]
のインスタンスは次のようなメソッドを持つ。
def writes(o: A): JsValue
このwrites
は引数として型A
の値o
を取り、JSONを表す型JsValue
を返しているので、writes
は型A
からJSONへ変換する関数であるといえる。例えば、Playでよく使うJson.toJson
という関数は次のようになる。
def toJson[T](o: T)(implicit tjs: Writes[T]): JsValue = tjs.writes(o)
このように、implicitパラメーターでWrites[T]
のインスタンスを受け取って、それを用いてJSONへの変換を行う。
これをそっくりXMLへ移植すればよい。
型A
をXMLへ変換する型クラスXmlWrites[A]
JSONへ変換する際に用いた型クラスWrites[A]
とほとんど同じXmlWrites[A]
を次のように定義する。ScalaにはXMLを表す型があらかじめ用意されているので、それをそのまま用いればよい。
trait XmlWrites[-A] {
def writes(o: A): scala.xml.NodeSeq
}
そして、Json.toJson
のような関数を定義する。
object Xml {
def toXml[W](o: W)(implicit X: XmlWrites[W]): scala.xml.NodeSeq = X.writes(o)
}
これは次のように使うことができる。
case class Test(a: String, b: Int)
implicit val testWrites: XmlWrites[Test] = new Writes[Test] {
def writes(o: Test): NodeSeq =
<a>
{o.a}
</a>
<b>
{o.b.toString}
</b>
}
Xml.toXml(Test("hoge", 123))
やや冗長だが、これで一応動作はする。
マクロを用いたインスタンスの自動生成
PlayのWrites
にはマクロを使ってcase classのWrites
インスタンスを自動生成することができる。例えば先ほどのTest
のWrites
は次のように書ける。
case class Test(a: String, b: Int)
implicit val testJsonWrites: Writes[Test] = Json.writes[Test]
Json.toJson(Test("hoge", 123))
このように一行でインスタンスを生成できる。Playの実装を調べるとScalaのマクロを使ってインスタンスを生成していたので、こちらの記事を参考にしつつ、case classのインスタンスを自動生成するマクロを次のように実装する。
object Xml {
def toXml[W](o: W)(implicit X: XmlWrites[W]): scala.xml.NodeSeq = X.writes(o)
def xmlWrites[A]: XmlWrites[A] = macro XmlMacroImpl.impl[A]
}
// macroでcase classのXmlWritesインスタンスを自動導出する
// http://matsu-chara.hatenablog.com/entry/2015/06/21/110000
object XmlMacroImpl {
def impl[A: c.WeakTypeTag](c: blackbox.Context): c.Expr[XmlWrites[A]] ={
import c.universe._
// case classのクラス名
val caseClassSym: c.universe.Symbol = c.weakTypeOf[A].typeSymbol
if (!caseClassSym.isClass || !caseClassSym.asClass.isCaseClass) c.abort(c.enclosingPosition, s"$caseClassSym is not a case class")
// 各フィールドのシンボル
val syms: List[TermSymbol] = caseClassSym.typeSignature.decls.toList.collect { case x: TermSymbol if x.isVal && x.isCaseAccessor => x }
val xmlTreeList: List[Tree] = syms.map { e =>
val name = e.name.toString.trim
val elemTree = q"_root_.scala.xml.Elem(null, $name, _root_.scala.xml.Null, _root_.scala.xml.TopScope, false, implicitly[XmlWrites[${e.typeSignature}]].writes(o.${TermName(name)}): _*)"
q"$$removeEmptyNode($elemTree)"
}
val xmlTree: Tree = xmlTreeList.tail.foldLeft(xmlTreeList.head)((x, y) =>
q"$x ++ $y"
)
val finalTree: Tree =
q"""
def $$removeEmptyNode(node: _root_.scala.xml.Elem): _root_.scala.xml.NodeSeq = node match {
case _root_.scala.xml.Elem(_, _, _, _) => _root_.scala.xml.NodeSeq.Empty
case _ => node
}
new XmlWrites[$caseClassSym] {
def writes(o: $caseClassSym): scala.xml.NodeSeq = $xmlTree
}
"""
c.Expr[XmlWrites[A]](finalTree)
}
}
case classかどうかを判定して、case classならばフィールドと型情報を取得し、それを使ってコードを組み立てていく。また、$$removeEmptyNode
関数は<a>NodeSeq.Empty</a>
のようなXMLノードが発生した際に、その要素を消去する関数である。
あとは、よく使いそうなインスタンスをXmlWrites
のコンパニオンオブジェクトに用意しておく。
object XmlWrites {
implicit val stringWrites: XmlWrites[String] = new XmlWrites[String] {
def writes(o: String): NodeSeq = Text(o)
}
implicit val intWrites: XmlWrites[Int] = new XmlWrites[Int] {
def writes(o: Int): NodeSeq = Text(o.toString)
}
implicit val floatWrites: XmlWrites[Float] = new XmlWrites[Float] {
def writes(o: Float): NodeSeq = Text(o.toString)
}
implicit val doubleWrites: XmlWrites[Double] = new XmlWrites[Double] {
def writes(o: Double): NodeSeq = Text(o.toString)
}
implicit val booleanWrites: XmlWrites[Boolean] = new XmlWrites[Boolean] {
def writes(o: Boolean): NodeSeq = Text(o.toString)
}
implicit def listWrites[A](implicit W: XmlWrites[A]): XmlWrites[List[A]] = new XmlWrites[List[A]] {
def writes(o: List[A]): NodeSeq = NodeSeq.fromSeq(o.flatMap(W.writes(_).toSeq))
}
implicit def optionWrites[A](implicit W: XmlWrites[A]): XmlWrites[Option[A]] = new XmlWrites[Option[A]] {
def writes(o: Option[A]): NodeSeq = o match {
case Some(a) => W.writes(a)
case None => NodeSeq.Empty
}
}
}
これらを使えば、先ほどのcase classTest
のXmlWrites[Test]
は次のように生成できる。
case class Test(a: String, b: Int)
implicit val testJsonWrites: XmlWrites[Test] = Xml.xmlWrites[Test]
Xml.toXml(Test("hoge", 123))
まとめ
この記事ではcase classからXMLへ変換する型クラスを用いて、XMLを生成し、かつインスタンスをマクロで生成するということを行った。はじめてのマクロにはいろいろ苦労したが、実用的なものが少ない行数で実装できてよかった。