ChimneyでRefined typeを含むオブジェクトの変換をする

オブジェクト間のフィールドコピーにおけるボイラープレートを削減できるライブラリChimneyとRefined typeを一緒に使ってみました。
2020.02.28

はじめに

こんにちは、リングフィットアドベンチャーで一番好きなエクササイズはモモアゲアゲの佐々木です。

今回はScaladexで見つけて気になっていたChimneyを試してみました。

Chimneyとは

Chimney は似てるけどちょっと違うフィールドをもつオブジェクト間のフィールドコピーにおけるボイラープレートを削減できるライブラリです。

例えば以下のようにあるオブジェクトから同じ名前、同じ型のフィールドを持つ別のオブジェクトが生成できます。

import io.scalaland.chimney.dsl._

final case class ReadOrderResponse(orderId:String, vat:BigDecimal, totalExVat:BigDecimal)

final case class Order(orderId:String, vat:BigDecimal, totalExVat:BigDecimal)

val readOrderResponse = ReadOrderResponse("order_001", 30, 300)
val order = readOrderResponse.transformInto[Order]

println(readOrderResponse) // ReadOrderResponse(order_001,30,300)
println(order) // Order(order_001,30,300)

他にもネストしたコレクションの変換やValue Class の変換、フィールド名の変更などかなり柔軟に変換を行えます。公式ドキュメントにはコード例も交えて紹介されているのでわかりやすいです。

Refined型への変換

Chimneyを試していてStringやLongなどの組み込み型からRefined 型への変換をしたくなったので試してみました。

Refined型→Refined型への変換(同じ型どうし)

これは特に工夫することなく行えました。

組み込み型→Refined型への変換

結論からいうとこのパターンではTransformerを定義する必要がありました。具体的には以下の例(String → UserId)のようになります。

これはRefinedTypeOps#unsafeFromを使った一番単純な実装ですが#from を使うことで例外翻訳することもできると思います。

いずれにしても実行時例外をスローするしかないので設計によってはChimney以外の場所でバリデーションを行なったり、例外をハンドリングする必要があると思います。

import io.scalaland.chimney.Transformer
import io.scalaland.chimney.dsl._
import eu.timepit.refined._
import eu.timepit.refined.api._
import eu.timepit.refined.string._

// case class with Refined type field.
final case class User(name: UserName, id:UserId)

object User {
  type UserId = String Refined MatchesRegex[W.`"[0-9]+"`.T]
  object UserId extends RefinedTypeOps[UserId, String]
}

final case class UserName(firstName:String, lastName:String)

// Response from GET User API
final case class ReadUserResponse(id:String, firstName:String, lastName:String)

val response:ReadUserResponse = ReadUserResponse("123", "firstName", "lastName")

//Transformerを定義
implicit val stringToUserId:Transformer[String, UserId] = (src: String) => UserId.unsafeFrom(src)

val user:User = response.into[User]
    .withFieldComputed(_.name, f => UserName(f.firstName, f.lastName))
    .transform

println(response) // ReadUserResponse(123,firstName,lastName)
println(user) // User(UserName(firstName,lastName),123)

最後に

Chimneyを上手く使えばコード量をかなり削減できると思います。