Typesafe configのScalaラッパーを公開しました

configがJavaのライブラリでちょっと使いづらいのでScala用にラッパーを書きました。

https://github.com/kxbmap/configs

configz というのもあったんだけど、これはScalaz力が足りず思ったように使えなかったです。

使い方

SBTのビルド定義に以下を追加します。

libraryDependencies += "com.github.kxbmap" %% "configs" % "0.1.0"

Sample

// sample.conf
int.value = 42
int.values = [1, 2, 3]
duration = 10ms
bytes = 10k
obj {
  foo = 23
  bar = Hello
}
scala> import com.typesafe.config._
import com.typesafe.config._

scala> import com.github.kxbmap.configs._
import com.github.kxbmap.configs._

scala> val config = ConfigFactory.load("sample")
config: com.typesafe.config.Config = ...

configsパッケージをimportするとPimp my library*1でConfigにgetメソッドが追加されます。

Simple value

scala> val n = config.get[Int]("int.value")
n: Int = 42

config.get[Int]("key")config.getInt("key") と同じ動きです。

List

scala> val ns = config.get[List[Int]]("int.values")
ns: List[Int] = List(1, 2, 3)

ScalaのListを返します。

Option/Either/Try

型引数に変なものを渡すと例外になります。

scala> config.get[String]("int.values")
com.typesafe.config.ConfigException$WrongType: sample.conf: 2: int.values has type LIST rather than STRING
    at com.typesafe.config.impl.SimpleConfig.findKey(SimpleConfig.java:124)
...

例外を捕捉したい場合はOption/Either/Tryを指定します。

scala> val opt = config.get[Option[String]]("int.values")
opt: Option[String] = None

scala> val eth = config.get[Either[Throwable, String]]("int.values")
eth: Either[Throwable,String] = Left(com.typesafe.config.ConfigException$WrongType: sample.conf: 2: int.values has type LIST rather than STRING)

scala> import scala.util.Try
import scala.util.Try

scala> val tr = config.get[Try[String]]("int.values")
tr: scala.util.Try[String] = Failure(com.typesafe.config.ConfigException$WrongType: sample.conf: 2: int.values has type LIST rather than STRING)

Missing

PlayのConfigurationのように値がない場合だけNoneで他は例外にして欲しい場合のために、orMissingというメソッドを用意しています。

scala> val just = config.orMissing[Int]("int.value")
just: Option[Int] = Some(42)

scala> val missing = config.orMissing[Int]("missing.value")
missing: Option[Int] = None

scala> config.orMissing[String]("int.values")
com.typesafe.config.ConfigException$WrongType: sample.conf: 2: int.values has type LIST rather than STRING
    at com.typesafe.config.impl.SimpleConfig.findKey(SimpleConfig.java:124)
...

あるいはEitherを使うこともできます。

scala> val em = config.get[Either[ConfigException.Missing, Int]]("missing.value")
em: Either[com.typesafe.config.ConfigException.Missing,Int] = Left(com.typesafe.config.ConfigException$Missing: No configuration setting found for key 'missing')

scala> config.get[Either[ConfigException.Missing, Int]]("int.values")
com.typesafe.config.ConfigException$WrongType: application.conf: 2: int.values has type LIST rather than NUMBER
    at com.typesafe.config.impl.SimpleConfig.findKey(SimpleConfig.java:124)
...

Duration

Longではなくscala.concurrent.duration.Durationで取得します。

scala> import scala.concurrent.duration.Duration
import scala.concurrent.duration.Duration

scala> val d = config.get[Duration]("duration")
d: scala.concurrent.duration.Duration = 10 milliseconds

Bytes

bytesの取得にはそれ用のcase classを使います。

scala> val bs = config.get[Bytes]("bytes")
bs: com.github.kxbmap.configs.Bytes = Bytes(10240)

Map

ConfigObjectをMapとして取得することができます。

scala> val m1 = config.get[Map[String, String]]("obj")
m1: Map[String,String] = Map(bar -> Hello, foo -> 23)

scala> val m2 = config.get[Map[Symbol, String]]("obj")
m2: Map[Symbol,String] = Map('bar -> Hello, 'foo -> 23)

scala> val m3 = config.get[Map[String, Try[Int]]]("obj")
m3: Map[String,scala.util.Try[Int]] = Map(bar -> Failure(com.typesafe.config.ConfigException$WrongType: sample.conf: 7: bar has type STRING rather than NUMBER), foo -> Success(23))

詳しく

getの型は以下のようになっています。

def get[T: AtPath](path: String): T = ...

また、AtPathは以下です。

type AtPath[T] = Configs[String => T]

そしてConfigs。

trait Configs[T] {
  def get(config: Config): T
}

Configs[T]はConfigから型Tの値を得るものです。 つまりAtPath[T]は、Configから「String(path)を与えると型Tの値を返す関数」を得るものです。

ユーザーはAtPathのインスタンスを定義することで、getを利用してConfigから好きな型の値を取得することができます。

例えば以下のようにInetAddressのためのインスタンスを定義します。(AtPath.applyは (Config, String) => T を取り AtPath[T] を返すヘルパーです)

import java.net.InetAddress

implicit val inetAddressAtPath: AtPath[InetAddress] = AtPath { (config, path) =>
  InetAddress.getByName(config.getString(path))
}

implicit val inetAddressListAtPath: AtPath[List[InetAddress]] = AtPath { (config, path) =>
  config.get[List[String]](path).map(InetAddress.getByName)
}

すると以下のようなコードが書けます。

inet  = "127.0.0.1"
inets = ["127.0.0.1", "::1"]
scala> val inet = config.get[InetAddress]("inet")
inet: java.net.InetAddress = /127.0.0.1

scala> val inets = config.get[List[InetAddress]]("inets")
inets: List[java.net.InetAddress] = List(/127.0.0.1, /0:0:0:0:0:0:0:1)

また、Configs[T]AtPath[T]及びAtPath[List[T]]に暗黙的に変換する仕組みが用意されているので、Configsのインスタンスを定義できるなら(Config =>Tという関数を書けるなら)、その方が楽です。

implicit val inetSocketAddressConfigs: Configs[InetSocketAddress] = Configs { config =>
  new InetSocketAddress(
    config.get[InetAddress]("host"),
    config.get[Int]("port")
  )
}
address = { host = "127.0.0.1", port = 8080 }
addresses = [
  { host = "127.0.0.1", port = 8080 }
  { host = "::1", port = 9000 }
]
scala> val address = config.get[InetSocketAddress]("address")
address: java.net.InetSocketAddress = /127.0.0.1:8080

scala> val addresses = config.get[List[InetSocketAddress]]("addresses")
addresses: List[java.net.InetSocketAddress] = List(/127.0.0.1:8080, /0:0:0:0:0:0:0:1:9000)

今後

インスタンスをもっと楽に定義できるようにしておきたい。 自前のcase class用の定義なんかは多分マクロでやるのがいいんだけど、そっち方面にはまだ手を付けていないので勉強するところから。

*1:Enrich my libraryと呼ぼうという話があった気がするけどどうなったんだろう。