The Path to Generic Endpoints using Shapeless

 
Scala IO, November 2017
Maria Livia Chiorean
 

Me

@MariaLiviaCh

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

Content Atoms

  • Structured data
  • Reusable content
  • Simultaneous updates

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec eleifend condimentum metus, at pretium orci sagittis et. Cras euismod justo ut tellus venenatis, et fermentum lectus feugiat. Nam pellentesque massa ac nulla semper ultricies. In eleifend tempor cursus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec eleifend condimentum metus, at pretium orci sagittis et.

 Cras euismod justo ut tellus venenatis, et fermentum lectus feugiat. Nam pellentesque massa ac nulla semper ultricies. In eleifend tempor cursus.

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

Atom Workshop

  • Currently we have > 10 types of atoms
  • Common components
  • Shared services
  • Built on Scala, Play and React

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

Generic API Endpoint

PATCH /api/:atomType/:id/:path
val atomData = QAndAAtom(
     typeLabel = "Q&A",
     item = QAndAItem(
       title = Some("What is a 
               gravitational wave?"),
       body = "I don't know."))
/api/qanda/some-id/item.body
body: "Einstein’s general theory of relativity predicts that 
       the presence of mass causes a curvature in spacetime."

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

val atomData = QAndAAtom(
     typeLabel = "Q&A",
     item = QAndAItem(
       title = Some("What is a gravitational wave?"),
       body = "Einstein’s general theory of relativity 
               predicts that the presence of mass causes 
               a curvature in spacetime."))

Scala Reflection?

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

Shapeless

  • Library for generic programming in Scala
  • Leverages the similarities between types
case class Employee(name: String,         age: Int, manager: Boolean)​
case class IceCream(name: String, numCherries: Int,  inCone: Boolean)​
String :: Int :: Boolean :: HNil

HList

Generic[IceCream].to(iceCream)

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

Generic[Employee].to(employee)

First approach

case class Atom(id: String, atomType: String, data: AtomData)

sealed trait AtomData
object AtomData {
  case class QandA(media: QAndAAtom) extends AtomData
}
case class QAndAAtom(typeLabel: Option[String], item: QAndAItem)
case class QAndAItem(title: Option[String], body:String)
val atomData = QAndAAtom(
     typeLabel = Some("Q&A"),
     item = QAndAItem(
       title = Some("What is a gravitational wave?"),
       body = "I don't know."))

val qaAtom = Atom("some-id", "QandA", AtomData.QandA(atomData))

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

val genericRepr = LabelledGeneric[Atom].to(qaAtom) 
// "some-id" :: "QandA" :: QandA(QandAAtom(...)) :: HNil
val updatedGeneric = genericRepr + (Symbol("atomType") ->> "Updated type")) 
// "some-id" :: "Updated type" :: QandA(QandAAtom(...)) :: HNil
val nestedField = "item.title"
val updatedGeneric2 = genericRepr + (Symbol(nestedField) ->> "Updated field"))
// error: Expression scala.Symbol.apply(ScalaFiddle.this.nestedField) does 
// not evaluate to a constant or a stable reference value

Atom

id: String

type: String

data: AtomData

qa: QandAAtom

item: QandAItem

typeLabel: Option[String]

title: Option[String]

body: String

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

Second approach

Convert to nested map: Map[String, Any]

Update the field recursively

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

Type Classes

trait Greetings[A] {
  def message(creature: A): String
}

object Greetings {
    implicit def humanMessage: Greetings[Human] = new Greetings[Human] {
        override def message(human: Human): String = s"${human.name} says hello."
    }
    implicit def catMessage: Greetings[Cat] = new Greetings[Cat] {
        override def message(cat: Cat): String = s"${cat.name} says meow."
    }

    implicit class GreetingsOps[A](creature: A) {
        def greet(implicit greetings: Greetings[A]) = {
          greetings.message(creature)
        }
    }
}

Human("Maria").greet // "Maria says hello."
Cat("Mistoffelees").greet // "Mistoffelees says meow."
Dog("Pollicle").greet // error: could not find implicit value for parameter message


    The Path  to Generic Endpoints using Shapeless | @marry16_08    

Class to map

trait ToMapRec[L <: HList] {
  def apply(l: L): Map[String, Any]
}

object ToMapRec {
    implicit def hconsToMapRecOption[K <: Symbol, V, R <: HList, T <: HList]
    (implicit
     wit: Witness.Aux[K],
     gen: LabelledGeneric.Aux[V, R],
     tmrT: Lazy[ToMapRec[T]],
     tmrH: Lazy[ToMapRec[R]]
    ): ToMapRec[FieldType[K, Option[V]] :: T] = ???
}

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

Class to map

trait ToMapRec[L <: HList] {
  def apply(l: L): Map[String, Any]
}
...

implicit def hconsToMapRecOption[K <: Symbol, V, R <: HList, T <: HList]
(implicit
 wit: Witness.Aux[K],
 gen: LabelledGeneric.Aux[V, R],
 tmrT: Lazy[ToMapRec[T]],
 tmrH: Lazy[ToMapRec[R]]
): ToMapRec[FieldType[K, Option[V]] :: T] = new ToMapRec[FieldType[K, Option[V]] :: T] {
 override def apply(l: FieldType[K, Option[V]] :: T): Map[String, Any] = {

   tmrT.value(l.tail) + (wit.value.name -> l.head.map(elem => tmrH.value(gen.to(elem))))

 }
}
...

val atomMap = atomData.toMap
val atomDataMap = Map(
  "item" -> Map(
    "title" -> Some("What is a gravitational wave?"), 
    "body" -> "I don't know."),
  "typeLabel" -> "Q&A")

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

Map to class

trait FromMap[L <: HList] {
    def apply(m: Map[String, Any]): Option[L]
}
...

implicit def hconsFromMapOption[K <: Symbol, V, R <: HList, T <: HList]
(implicit
 witness: Witness.Aux[K],
 gen: LabelledGeneric.Aux[V, R],
 fromMapH: Lazy[FromMap[R]],
 fromMapT: Lazy[FromMap[T]]
): FromMap[FieldType[K, Option[V]] :: T] = new FromMap[FieldType[K, Option[V]] :: T] {
    def apply(m: Map[String, Any]): Option[FieldType[K, Option[V]] :: T] = {
        m(witness.value.name) match {
            case Some(v) =>
                for {
                  r <- Typeable[Option[Map[String, Any]]].cast(Some(v))
                  h <- r.map(fromMapH.value(_))
                  t <- fromMapT.value(m)
                } yield field[K](h.map(gen.from)) :: t
            case None =>
                for {
                  t <- fromMapT.value(m)
                } yield field[K](None) :: t
        }
    }
}
...

val atomObj = to[Atom.Immutable].from(updatedAtom)

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

Final approach

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

Summary

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

Thank you.

    The Path  to Generic Endpoints using Shapeless | @marry16_08    

The Path to Generic Endpoints using Shapeless

By Maria Livia

The Path to Generic Endpoints using Shapeless

Scala IO - Lyon

  • 1,952