The Path to Generic Endpoints using Shapeless

 
Maria Livia Chiorean
 

Me

@MariaLiviaCh

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.

Atom Workshop

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

Generic API Endpoint

PATCH /api/:atomType/:id/:path
val atomData = MediaAtom(
     url = "gu.com",
     metadata = Some(Metadata( 
       commentsEnabled = Some(true), 
       channelId = Some("happy-friday"))))

val atomData = MediaAtom(
     title = "Tech time video",
     metadata = Some(Metadata( 
       commentsEnabled = Some(true), 
       channelId = Some("Scala"))))
/api/media/some-id/metadata.channelId
body: "Scala"

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)
Generic[Employee].to(iceCream)

First approach

val atomData = MediaAtom(url = "gu.com",
     metadata = Some(Metadata( 
       commentsEnabled = Some(true), 
       channelId = Some("happy-friday"))))

val mediaAtom = Atom("some-id", "media", "Scala fun", AtomData.Media(atomData))

val genericRepr = LabelledGeneric[Atom].to(mediaAtom) 
// "some-id" :: "media" :: "Scala fun" :: Media(MediaAtom(...)) :: HNil

val updatedGeneric = genericRepr + (Symbol("title") ->> "Updated title")) 
// "some-id" :: "media" :: "Updated title" :: Media(MediaAtom(...)) :: HNil

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

title: String

data: AtomData

media: MediaAtom

metadata: Metadata

url: String

comments: Boolean

channelId: String

Second approach

Convert to nested maps

Update the field in the map

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


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] = ???
}

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(
  "metadata" -> Some(Map(
    "commentsEnabled" -> Some(false), 
    "channelId" -> Some("happy-friday"))), 
  "url" -> "gu.com")

Final approach

Summary

Thank you.

Short - The Path to Generic Endpoints using Shapeless

By Maria Livia

Short - The Path to Generic Endpoints using Shapeless

WHC meetup

  • 1,024