前から名前は聞いたことはありましたが、使ったことのなかったTypesafeの提供するConfigでちょっと遊んでみました。
typesafehub/config(Configuration library for JVM languages)
https://github.com/typesafehub/config
Typesafeって、あのTypesafe社ですね。
Scala用のライブラリかと思いきや、Javaで実装されたものなので、ふつうにJavaから使えます。とはいっても、今回Javaからは呼んではいませんが…。
どんなものなのか?
Javaで設定ファイルといえば、Properties形式が有名(あとはXMLとか…)ですが、もう少しフォーマットが扱えます。
単純なPropertiesライクな形式で書いても、Listを返却することができたり、構造を扱えたりとそこそこ便利です。
その他、設定項目の変数展開、別ファイルのインクルードも可能だったりします。
使用する設定ファイルですが、デフォルトでは「application.conf」という名前のファイルを読みにいきますが、自分でファイル名を指定したり、Stringからパースすることも可能です。
Javadocは、こちら。
使ってみる
というわけで、使ってみましょう。
build.sbt
name := "typesafe-config-example" version := "0.0.1-SNAPSHOT" scalaVersion := "2.10.3" organization := "org.littlewings" scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked") libraryDependencies ++= Seq( "com.typesafe" % "config" % "1.0.2", "org.scalatest" %% "scalatest" % "2.0" % "test" )
Scalaで試します。
こんなコードを用意しまして、中でいろいろ試していきます。
src/test/scala/org/littlewings/config/TypeSafeConfigSpec.scala
package org.littlewings.config import com.typesafe.config.{Config, ConfigFactory} import org.scalatest.FunSpec import org.scalatest.Matchers._ class TypeSafeConfigSpec extends FunSpec { describe("typesafe-config spec") { // ここで、設定ファイルをロードして確認するテストコードを書く! } }
では、順次試していきます。
デフォルトの設定ファイルを読む
デフォルトのファイル名は、「application.conf」ということでしたが、こんなProperties形式で用意してみます。
src/test/resources/application.conf
app.name=ConfigTest multibyte.value=こんにちは 世界 int.value=10 strings=["foo", bar, hoge]
これに対する、テストコードはこちら。
it("default configration load") { val config: Config = ConfigFactory.load() config.getString("app.name") should be ("ConfigTest") config.getString("multibyte.value") should be ("こんにちは 世界") config.getInt("int.value") should be (10) config.getStringList("strings") should contain theSameElementsInOrderAs Array("foo", "bar", "hoge") }
ConfigFactory#loadで、デフォルトの設定ファイルをロードします。ここを変えることで、他のファイルなどを読み込んだりすることが可能です。
ConfigFactory
http://typesafehub.github.io/config/latest/api/com/typesafe/config/ConfigFactory.html
ConfigFactory#loadの戻り値はConfigで、ここからgetStringやgetInt、getStringListなどで設定値を取得することができます。
Config
http://typesafehub.github.io/config/latest/api/com/typesafe/config/Config.html
なお、指定した項目名に該当する設定がなかった場合は、com.typesafe.config.ConfigException$Missingがスローされます。
JSON形式の設定ファイルを読む
今度は、設定ファイルをJSON形式で用意します。
src/test/resources/configtest.json
{ "config": { "string": "こんにちは、世界", "array": [1, 2, 3], "more-nested": { "numeric": 10 } } }
これに対する、テストコードはこちら。
it("json configuration load") { // ファイル名を指定 val config = ConfigFactory.load("configtest.json") config.getString("config.string") should be ("こんにちは、世界") config.getIntList("config.array") should contain theSameElementsInOrderAs Array(1, 2, 3) config.getInt("config.more-nested.numeric") should be (10) // ネストした部分を、Configとしても取得できる val c = config.getConfig("config") c.getString("string") should be ("こんにちは、世界") c.getIntList("array") should contain theSameElementsInOrderAs Array(1, 2, 3) // Config#getListした場合は、Listを実装したConfigListが返却される // ConfigListの中身は、ConfigValueでunrappedで戻せる config.getList("config.array").unwrapped should contain theSameElementsInOrderAs Array(1, 2, 3) config.getList("config.array").get(0).unwrapped should be (1) config.getList("config.array").get(1).unwrapped should be (2) config.getList("config.array").get(2).unwrapped should be (3) }
先の例とは変わった試みとして、ネストされた構造をConfigとして取得したり、ConfigListを使用したりしています。
ネストされた構造をConfigとして取得した場合は、そこからgetStringしたりする場合は、相対表記になります。
Propertiesライクな形式を、もう少し
今度は、こんなファイルを用意。
src/test/resources/propertylike1.conf
ns.string.value=Hello World ns.intvalue=20 ns.nested.intvalue=10 ns.nested.stringvalue=string ns.nested.substitutions=${ns.string.value} ${ns.intvalue}
テストコード。
it("property like configuration load") { // Property形式でも、ネストした構造を扱える val config = ConfigFactory.load("propertylike1.conf") val c = config.getConfig("ns") c.getString("string.value") should be ("Hello World") c.getInt("intvalue") should be (20) val nested = config.getConfig("ns.nested") nested.getInt("intvalue") should be (10) nested.getString("stringvalue") should be ("string") // 設定項目の展開も可能 nested.getString("substitutions") should be ("Hello World 20") }
キーの命名さえ合わせておけば、Propertiesライクな形式でも、構造化して扱えます。また、ここでは変数展開の例も示しました。
HOCON(Human-Optimized Config Object Notation)形式の設定ファイルを読む
最後は、HOCON形式。
先ほどのページに、HOCONについてはいろいろ書かれています。
typesafehub/config(Configuration library for JVM languages)
https://github.com/typesafehub/config
JSONのスーパーセット?書き方もいろいろあるみたいなので、興味のある方は詳細をご覧になるとよいでしょう。コメントが書けたり、インクルードができたりするようです。
では、簡単な例でお試し。
src/test/resources/hocon.conf
hocon-config { name = "HOCON Presentation Configuration" int-value = 1 nested { list-values = ["foo", "bar", "fuga"] numeric = 10.5 } }
利用する側は、特に変わりなく…。
it("HOCON configuration load") { // HOCON(Human-Optimized Config Object Notation)という形式でも書けるらしい val config = ConfigFactory.load("hocon.conf") config.getNumber("hocon-config.nested.numeric") should be (10.5) val hc = config.getConfig("hocon-config") hc.getString("name") should be ("HOCON Presentation Configuration") hc.getInt("int-value") should be (1) hc.getStringList("nested.list-values") should contain theSameElementsInOrderAs Array("foo", "bar", "fuga") val nested = hc.getConfig("nested") nested.getNumber("numeric") should be (10.5) }
構文を覚える必要がありますが、これはこれで便利そう。どのくらい使われてるのかな?
Ficus
Typesafe Configは、完全にJava向けなので戻ってくるListなどの型はJavaのListだったり、Scala向けのものではありません。
今回、Typesafe Configのラッパーとして、Ficusというものを見つけたので、こちらでも遊んでみました。
Ficus
https://github.com/ceedubs/ficus/tree/master
同様のものとして、configzというものもあるそうな。
configz
https://github.com/arosien/configz
今回は、Ficusを使用します。
依存関係の定義などは、こんな感じ。
build.sbt
name := "ficus-example" version := "0.0.1-SNAPSHOT" scalaVersion := "2.10.3" organization := "org.littlewings" scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked") resolvers += "Sonatype OSS Releases" at "http://oss.sonatype.org/content/repositories/releases/" libraryDependencies ++= Seq( "com.typesafe" % "config" % "1.0.2", "net.ceedubs" %% "ficus" % "1.0.0", "org.scalatest" %% "scalatest" % "2.0" % "test" )
Sonatypeのリポジトリを加える必要があります。また、Ficusの依存しているTypesafe Configが1.0だったので、好奇心的に上げてみました…。
使用する設定ファイルは、すべて先ほどのサンプルと同じものとして、利用するコードだけFicusで置き換えます。
テストコードの骨格。
src/test/scala/org/littlewings/config/FicusSpec.scala
package org.littlewings.config import com.typesafe.config.{Config, ConfigFactory} import net.ceedubs.ficus.FicusConfig._ import net.ceedubs.ficus.SimpleConfigKey import net.ceedubs.ficus.readers.ValueReader import org.scalatest.FunSpec import org.scalatest.Matchers._ class FicusConfigSpec extends FunSpec { describe("typesafe-config spec") { // ここに、Ficusを使ったテストコードを書く! } }
使用するクラスにもよりますが、最低限以下はimportしておいてください。
import net.ceedubs.ficus.FicusConfig._
では、先ほどのサンプルを順次置き換えていきます。
it("default configration load") { val config: Config = ConfigFactory.load() // as、getAs、applyのいずれかを使う config.as[String]("app.name") should be ("ConfigTest") config.getAs[String]("app.name") should be (Some("ConfigTest")) // Optionになる config.apply[String](SimpleConfigKey("app.name")) should be ("ConfigTest") // Optionとして受けとることができる config.as[Option[String]]("multibyte.value") should be (Some("こんにちは 世界")) config.as[Int]("int.value") should be (10) config.as[List[String]]("strings") should contain theSameElementsInOrderAs Array("foo", "bar", "hoge") // Optionが使用できるため、存在しない項目に対しても安全 an [com.typesafe.config.ConfigException$Missing] should be thrownBy config.as[String]("missing.entry") config.as[Option[String]]("missing.entry") should be (None) // Setとして受け取ることも可能 config.as[Set[String]]("strings") should contain only ("foo", "bar", "hoge") }
TypesafeのConfigに対して、as、getAs、applyのいずれかで取得可能になります。getAsは、戻り値がOptionになります。
その他、ScalaのListやSetでも受け取れたりと、確かにScalaフレンドリーですね。
Optionが使えるため、存在しない項目はOptionで取得してハンドリングすることができます。
本家のサンプルコードがこちらにあるので、詳しく見たい方はそちらへ…。
https://github.com/ceedubs/ficus/blob/master/src/test/scala/net/ceedubs/ficus/ExampleSpec.scala
残りのサンプルも、置き換えましょう。
it("json configuration load") { // ファイル名を指定 val config = ConfigFactory.load("configtest.json") config.as[Option[String]]("config.string") should be (Some("こんにちは、世界")) // OptionのListとして受けることも可能 config.as[Option[List[Int]]]("config.array") should be (Some(List(1, 2, 3))) config.as[Int]("config.more-nested.numeric") should be (10) // ネストした部分を、Configとしても取得できる val c = config.as[Config]("config") c.as[String]("string") should be ("こんにちは、世界") c.as[Set[Int]]("array") should contain only (1, 2, 3) // さすがに、ConfigListのサポートはなさそう… config.as[Array[Int]]("config.array") should contain theSameElementsInOrderAs Array(1, 2, 3) config.as[Array[Int]]("config.array").apply(0) should be (1) config.as[Array[Int]]("config.array").apply(1) should be (2) config.as[Array[Int]]("config.array").apply(2) should be (3) } it("property like configuration load") { // Property形式でも、ネストした構造を扱える val config = ConfigFactory.load("propertylike1.conf") val c = config.as[Config]("ns") c.as[String]("string.value") should be ("Hello World") c.as[Int]("intvalue") should be (20) val nested = config.as[Config]("ns.nested") nested.as[Int]("intvalue") should be (10) nested.as[String]("stringvalue") should be ("string") nested.as[String]("substitutions") should be ("Hello World 20") } it("HOCON configuration load") { // HOCON(Human-Optimized Config Object Notation)という形式でも書けるらしい val config = ConfigFactory.load("hocon.conf") // Doubleのような場合は、Numberではなくて具体的な型になるらしい config.as[Double]("hocon-config.nested.numeric") should be (10.5) val hc = config.as[Config]("hocon-config") hc.as[String]("name") should be ("HOCON Presentation Configuration") hc.as[Int]("int-value") should be (1) hc.as[Set[String]]("nested.list-values") should contain only ("foo", "bar", "fuga") val nested = hc.as[Config]("nested") nested.as[Double]("numeric") should be (10.5) }
さすがに、ConfigListのサポートはなさそうですね。
あと、拡張として、Configの内容を任意の型に変換することができます。今回はConfigを自分で定義したCase Classに変換しました。…まあ、元のサイトにそんなサンプルもありますけどね。
it("using Case Class") { // 設定ファイルの内容を、独自のCase Classで扱うパターン val config = ConfigFactory.load("configtest.json") case class MyConfig(string: String, array: Array[Int], numeric: Int) // Implicit Valとして、ValueReaderを定義すれば任意の型に変換可能 implicit val myConfigReader: ValueReader[MyConfig] = ValueReader.relative { config => MyConfig(config.as[String]("string"), config.as[Array[Int]]("array"), config.as[Int]("more-nested.numeric")) } val myConfig = config.as[MyConfig]("config") myConfig.string should be ("こんにちは、世界") myConfig.array should contain theSameElementsInOrderAs Array(1, 2, 3) myConfig.numeric should be (10) }
実は、Config#asの後ろにImplicit Parameterが隠れていて、これに対して自分で定義したValueReaderを使わせるようにすれば、変換が可能になるというわけです。
// Implicit Valとして、ValueReaderを定義すれば任意の型に変換可能 implicit val myConfigReader: ValueReader[MyConfig] = ValueReader.relative { config => MyConfig(config.as[String]("string"), config.as[Array[Int]]("array"), config.as[Int]("more-nested.numeric")) }
こんな感じで。
設定ファイルにはこれまであまり拘ってこなかったのですが、こういうのもいいのかなぁとちょっと思いました。
今回作成したサンプルは、こちらにアップしています。