この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
declineではオプション引数ごとに詳細を指定してOptsを生成する必要があります。case classのフィールド名をそのままオプション名にするので十分な場合にはOptsを生成するコードは冗長になります。今回はshapelessを使ってcase classの定義からOptsを導出してみます。
やること
case classの各フィールドから対応するOptsを導出し、case class自体のOptsへ合成します。以下のコードのようなイメージです。
final case class Config(name: String, favoriteNumber:Int)
val name = Opts
val favoriteNumber = Opts
val configOpts = (name, favoriteNumber).mapN(Config.apply)
Optsの導出はshaplessを使った型クラスインスタンスの生成における典型的なパターンです。Optsを直接導出することはできないのでOptsを生成するOptGenを導出します。
import cats.implicits._
import com.monovore.decline.{Argument, Opts}
import shapeless.labelled.{FieldType, field}
import shapeless.{::, HList, HNil, LabelledGeneric, Lazy, Witness}
trait OptGen[A] {
def opts: Opts[A]
}
object OptGen {
def apply[A](implicit opts: Lazy[OptGen[A]]): OptGen[A] = opts.value
private final case class Gen[A](opts: Opts[A]) extends OptGen[A]
implicit def genericOpt[A, R <: HList](implicit
generic: LabelledGeneric.Aux[A, R],
opts: Lazy[OptGen[R]]
): OptGen[A] =
Gen(opts.value.opts.map(generic.from))
implicit def hlistOpt[K <: Symbol, H, T <: HList](implicit
hOpts: Lazy[OptGen[FieldType[K, H]]],
tOpts: OptGen[T]
): OptGen[FieldType[K, H] :: T] = Gen(
(hOpts.value.opts, tOpts.opts).mapN(_ :: _)
)
implicit def fieldOpt[K <: Symbol, T](implicit
witness: Witness.Aux[K],
arg: Argument[T]
): OptGen[FieldType[K, T]] =
//ヘルプ文字列は割り切り!!
Gen(Opts.option[T](witness.value.name, witness.value.name).map(field[K](_)))
implicit val hnilOpt: OptGen[HNil] = Gen(Opts.unit.as(HNil))
}
実行してみる
導出したOptsでのパースも明示的に生成した場合と同様に行えます。
final case class Config(name: String, favoriteNumber:Int)
val opts = OptGen[Config].opts
val command = Command("opts example", "")(opts)
val result = command.parse(Seq("--name","foobar", "--favoriteNumber", "128"))
println(result) //Right(Config(foobar,128))
まとめ
いつもながらshapelessは強力だと思います。