Create "enriched" type from case class with Shapeless in Scala

448 Views Asked by At

I have this example code:

import java.util.UUID

import shapeless.LabelledGeneric
import shapeless.record._
import shapeless.syntax.singleton._

object LabelTest extends App {

  case class IncomingThing(name: String, age: Int)
  case class DatabaseIncomingThing(name: String, age: Int, id: UUID)

  val genIncoming = LabelledGeneric[IncomingThing]
  val genDatabase = LabelledGeneric[DatabaseIncomingThing]

  val thing = IncomingThing("John", 42)

  val structuralVersionOfIncomingThing = genIncoming.to(thing)

  val updated = genDatabase.from(structuralVersionOfIncomingThing + ('id ->> UUID.randomUUID()))

  println(updated) // DatabaseIncomingThing(John,42,a45081f2-4ed5-4d2b-8fd9-4d8986875ed7)

}

Which is nice because I don't have to write the boilerplate that copies over all the fields from IncomingThing to DatabaseIncomingThing. However, I would love not having to maintain both those types since there is a very clear relationship between the two (one has id, the other doesn't).

Is there a way to create a type from a given case class by either adding or removing one field? I imagine something like

type IncomingThing = withoutField[DatabaseIncomingThing]('id)

Or something to that effect.

1

There are 1 best solutions below

4
On BEST ANSWER

Instead of DatabaseIncomingThing

val updated: DatabaseIncomingThing = 
  genDatabase.from(structuralVersionOfIncomingThing + ('id ->> UUID.randomUUID()))

you can work with raw HList

val updated1: FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: FieldType[Witness.`'id`.T, UUID] :: HNil = 
  structuralVersionOfIncomingThing + ('id ->> UUID.randomUUID())
val updated2: FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: HNil = 
  updated1 - 'id

On type level

implicitly[Remover.Aux[FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: FieldType[Witness.`'id`.T, UUID] :: HNil,
  Witness.`'id`.T,
  (UUID, FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: HNil)]]

implicitly[Updater.Aux[
  FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: HNil,
  FieldType[Witness.`'id`.T, UUID],
  FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: FieldType[Witness.`'id`.T, UUID] :: HNil]]

You can create your type class

trait WithoutField[A, K] {
  type Out <: HList
}

object WithoutField {
  type Aux[A, K, Out0 <: HList] = WithoutField[A, K] { type Out = Out0 }
  def instance[A, K, Out0 <: HList]: Aux[A, K, Out0] = new WithoutField[A, K] { type Out = Out0 }

  implicit def mkWithoutField[A, L <: HList, K, T, L1 <: HList](implicit
    labelledGeneric: LabelledGeneric.Aux[A, L],
    remover: Remover.Aux[L, K, (T, L1)]): Aux[A, K, L1] = instance
}

and use it

def foo[Out <: HList](implicit withoutField: WithoutField.Aux[DatabaseIncomingThing, Witness.`'id`.T, Out]) = {
  // now you can use type Out inside
  type IncomingThing = Out
  ???
}