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と呼ぼうという話があった気がするけどどうなったんだろう。