Scala用のRisonパーサを作ってみた

RisonというのはJSONライクかつURLに埋め込みやすいようURLエンコーディングが最小限になるよう設計されたデータフォーマットだそうで、Kibanaなどで使われているそうです。日本語だと以下の記事が詳しいです(自分もこの記事を見てRisonを知りました)。

qiita.com

すでにJavaScriptPythonGolangなど様々な言語向けのライブラリが存在するようですが、Scala用のライブラリが見当たらなかったので久しぶりにScalaのパーサコンビネータライブラリを使って実装してみました。

github.com

こんな感じで使います。

import com.github.takezoe.rison._

val parser = new RisonParser()

// parse
parser.parse("(name:Lacazette,age:27)") match {
  case Right(node) => println(node.toScala) // => Map(name -> Lacazette, age -> 27)
  case Left(error) => println(error)
}

// convert from Scala's Map
val node: RisonNode = RisonNode.fromScala(
  Map(
    "name" -> "Alexandre Lacazette", 
    "twitter" -> "@LacazetteAlex"
  )
)
println(node.toRisonString) // => (name:'Alexandre Lacazette',twitter:'@LacazetteAlex')

// URL encode
val encoded: String = node.toUrlEncodedString
println(encoded) // => (name:'Alexandre+Lacazette',twitter:'@LacazetteAlex')

O-risonやA-risonにも対応しています。

// O-rison
val orison: ObjectNode = parser.parseObject("name:Lacazette,age:27")
println(orison.toObjectString) // => name:Lacazette,age:27

// A-rison
val arison: ArrayNode = parser.parseArray("Lacazette,Aubameyang,Ozil")
println(arison.toArrayString) // => Lacazette,Aubameyang,Ozil

上記のサンプルではMapSeqを使っていますが、ケースクラスを使うこともできます。

// case class
case class Player(name: String, age: Int)

// convert from case class
val node: RisonNode = RisonNode.fromScala(Player("Lacazette", 27))
println(node.toRisonString) // => (name:Lacazette',age:27)


// convert to case class
val obj: Player = node.to[Player]
println(node.) // => Player(Lacazette, 27)

ケースクラスとの相互変換にはairframe-surfaceを使いました。

airframe-surfaceは型情報を取得してごにょごにょするためのScalaライブラリで、リフレクション関連の面倒な処理を隠蔽してくれます。Scalaだと同様の処理にはShapelessのようなライブラリを使う方法もありますが、airframe-surfaceはリフレクションならではの制限はあるものの気軽に使えて便利です。

このRisonパーサは冬休み中に作っていたのですが、パーサコンビネータやairframe-surfaceなどいろいろ試すことができてなかなか良い題材でした。