Skip to content

Instantly share code, notes, and snippets.

@SystemFw
Last active June 7, 2019 05:15
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save SystemFw/6664bd6fe4e23c37b81cbb64249f2c0e to your computer and use it in GitHub Desktop.
Save SystemFw/6664bd6fe4e23c37b81cbb64249f2c0e to your computer and use it in GitHub Desktop.
Shapeless: Convert between any two compatible case classes, selecting a subset of the fields
object conversions {
// it requires the cats, shapeless, and kittens libraries
import cats._, implicits._, data.Reader
import cats.sequence._
import shapeless._, labelled._
implicit class Convert[A](a: A) {
def convertTo[B](fields: Set[String])(
implicit ev: Conversion.Between[A, B]) = Conversion.to[B](a, fields)
}
trait Conversion[T] {
type Out
def apply(t: T): Out
}
object Conversion {
@annotation.implicitNotFound(
"Make sure that ${O} has the same field names as ${I}, and Options of the field types")
type Between[I, O] = Conversion[I] { type Out = Reader[Set[String], O] }
class Converter[B] {
def apply[A](a: A, fields: Set[String])(
implicit ev: Conversion.Between[A, B]): B =
ev(a).run(fields)
}
def to[B] = new Converter[B]
implicit def all[A, Repr <: HList, B, O <: HList](
implicit genA: LabelledGeneric.Aux[A, Repr],
genB: LabelledGeneric.Aux[B, O],
ev: Traverser.Aux[Repr, selectFields.type, Reader[Set[String], O]])
: Conversion.Between[A, B] = new Conversion[A] {
type Out = Reader[Set[String], B]
def apply(a: A): Out =
genA.to(a).traverse(selectFields).map(genB.from)
}
object selectFields extends Poly1 {
implicit def caseField[K <: Symbol, V](implicit key: Witness.Aux[K])
: Case.Aux[FieldType[K, V],
Reader[Set[String], FieldType[K, Option[V]]]] =
at[FieldType[K, V]] { v =>
Reader { (fields: Set[String]) =>
field[K] {
if (fields contains key.value.name) Some(v)
else None
}
}
}
}
}
}
object Test {
case class User(name: String, age: Int)
case class UserDTO(name: Option[String], age: Option[Int])
import conversions._
def a = User("John", 24).convertTo[UserDTO](Set("name"))
// res0: Test.UserDTO = UserDTO(Some(John),None)
case class WrongFieldNames(surname: Option[String], age: Option[Int])
def b = User("John", 24).convertTo[WrongFieldNames](Set("name"))
/* Compile Error
* [error] Make sure that Test.WrongFieldNames has the same field names as Test.User, and Options of the field types
* [error] def b = User("John", 24).convertTo[WrongFieldNames](Set("id")) // compile error
*/
case class WrongTypes(name: Option[String], age: Int)
def c = User("John", 24).convertTo[WrongTypes](Set("name"))
/* Compile Error
* [error] Make sure that Test.WrongTypes has the same field names as Test.User, and Options of the field types
* [error] def c = User("John", 24).convertTo[WrongFieldNames](Set("id")) // compile error
*/
}
@dsebban
Copy link

dsebban commented Apr 19, 2019

I am getting an error while trying a normal copy, any ideas ? I am using ammonite as follows

import $ivy.`org.typelevel::kittens:1.2.1`
//copy paste your code here
def a = User("John", 24).convertTo[UserDTO](Set("name"))
//cmd5.sc:1: Make sure that ammonite.$sess.cmd3.UserDTO has the same field names as ammonite.$sess.cmd2.User, and Options of the field types def a = User("John", 24).convertTo[UserDTO](Set("name"))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment