Endpoints

Julien Richard-Foy

http://julienrf.github.io/zrm-endpoints-2016

Outline

Motivation

First-class values

Duplication reduction

POST   /items       myshop.Items.create
GET    /items/:id   myshop.Items.read(id)

Name binding

POST   /items       myshop.Items.create
GET    /items/:id   myshop.Items.read(id)
val itemsPath = /items
val itemsCtl = myshop.Items

POST   itemsPath      itemsCtl.create
GET    itemsPath/:id  itemsCtl.read(id)

Name binding

POST   /items       myshop.Items.create
GET    /items/:id   myshop.Items.read(id)
val itemsPath = path / "items"
val itemsCtl = myshop.Items

val create = endpoint(post(itemsPath),                   itemsCtl.create)
val read   = endpoint(get (itemsPath / segment[String]), itemsCtl.read _)

Abstract over a controller instance

class ItemsEndpoints(itemsCtl: Items) {
  val itemsPath = path / "items"

  val create = endpoint(post(itemsPath),                   itemsCtl.create)
  val read   = endpoint(get (itemsPath / segment[String]), itemsCtl.read _)
}

Seamless integration with existing code

trait ResourceEndpoints {
  def create: Endpoint
  def read: Endpoint
}

Seamless integration with existing code

trait ResourceEndpoints {
  def create: Endpoint
  def read: Endpoint
}
class ItemsEndpoints(itemsCtl: Items) extends ResourceEndpoints {
  val itemsPath = path / "items"

  val create = endpoint(post(itemsPath),                   itemsCtl.create)
  val read   = endpoint(get (itemsPath / segment[String]), itemsCtl.read _)
}

First-class values: summary

Multiple interpretations

Multiple interpretations

Going from a router to an HTTP protocol specification

class ItemsEndpoints(itemsCtl: Items) {
  val itemsPath = path / "items"

  val create = endpoint(post(itemsPath),                   itemsCtl.create)
  val read   = endpoint(get (itemsPath / segment[String]), itemsCtl.read _)
}

What else do we want to include in our specification?

object ItemsEndpoints {
  val itemsPath = path / "items"

  val create = endpoint(post(itemsPath))
  val read   = endpoint(get (itemsPath / segment[String]))
}

Complete HTTP protocol specification

object ItemsEndpoints {
  val itemsPath = path / "items"

  val create = endpoint(post(itemsPath, jsonRequest[CreateItem]), jsonResponse[Item])
  val read   = endpoint(get(itemsPath / segment[UUID]), jsonResponse[Item])
}

case class Item(id: UUID, name: String, price: Int)

case class CreateItem(name: String, price: Int)

Deriving useful programs from a specification

HTTP server

class ItemsRouter(items: Items) {

  val router = playRouterFromEndpoints(
    playRoute(ItemsEndpoints.create, items.create),
    playRoute(ItemsEndpoints.read, items.read)
  )

}

HTTP server

class ItemsRouter(items: Items) {

  val router = playRouterFromEndpoints(
    playRoute(ItemsEndpoints.create, items.create),
    playRoute(ItemsEndpoints.read, items.read)
  )

}
class Items {
  def create(createItem: CreateItem): Future[Item] = …
  def read(id: UUID): Future[Item] = …
}

HTTP client

class ItemsClient(wsClient: WSClient) {
  val create = playClient(ItemsEndpoints.create, wsClient)
  val read = playClient(ItemsEndpoints.read, wsClient)
}

HTTP client

class ItemsClient(wsClient: WSClient) {
  val create = playClient(ItemsEndpoints.create, wsClient)
  val read = playClient(ItemsEndpoints.read, wsClient)
}
val client: ItemsClient = …
for {
  itemCreated <- client.create(CreateItem("foo", 42))
  itemRead <- client.read(itemCreated.id)
} yield itemRead

(Another) HTTP client

object ItemsClient {
  val create = xhrClient(ItemsEndpoints.create)
  val read = xhrClient(ItemsEndpoints.read)
}
val eventuallyItemCreated: js.Promise[Item] =
  ItemsClient.create(CreateItem("foo", 42))

Extensibility

Extensibility

Adding support for authentication

val create = endpoint(
  post(path / "items", jsonRequest[CreateItem]),
  jsonResponse[Item]
)
val read = endpoint(
  get(path / "items" / segment[UUID]),
  jsonResponse[Item]
)

Adding support for authentication

val create = endpoint(
  authenticatedPost(path / "items", jsonRequest[CreateItem]),
  jsonResponse[Item]
)
val read = endpoint(
  authenticatedGet(path / "items" / segment[UUID]),
  jsonResponse[Item]
)

Implementation

Abstract descriptions and multiple interpreters

EndpointsAlg interface

trait EndpointsAlg {
  def get[A](url: Url[A]): Request[A]
  type Url[A]
  type Request[A]
  …
}

What is the “carried information”?

Client interpreter

trait PlayClient extends EndpointsAlg {

}

Client interpreter

trait PlayClient extends EndpointsAlg {

  type Request[A] = A => Future[WSResponse]

}

Client interpreter

trait PlayClient extends EndpointsAlg {

  type Request[A] = A => Future[WSResponse]

  type Url[A] = A => String

}

Client interpreter

trait PlayClient extends EndpointsAlg {

  type Request[A] = A => Future[WSResponse]

  type Url[A] = A => String
  
  def get(url: Url[A]): Request[A] =
    a => wsClient.url(url(a)).get()

  def wsClient: WSClient
    
}

Server interpreter

trait PlayRouter extends EndpointsAlg {

}

Server interpreter

trait PlayRouter extends EndpointsAlg {

  type Request[A] = RequestHeader => Option[BodyParser[A]]

}

Server interpreter

trait PlayRouter extends EndpointsAlg {

  type Request[A] = RequestHeader => Option[BodyParser[A]]

  type Url[A] = RequestHeader => Option[A]

}

Server interpreter

trait PlayRouter extends EndpointsAlg {

  type Request[A] = RequestHeader => Option[BodyParser[A]]

  type Url[A] = RequestHeader => Option[A]

  def get(url: Url[A]): Request[A] =
    request =>
      if (request.method == "GET")
        url(request)
          .map(a => BodyParser(_ => Accumulator.done(Right(a))))
      else None

}

Interpreters: summary

Retroactive addition of new vocabulary

Authenticated requests

trait AuthRequests extends EndpointsAlg {
  def authenticatedGet[A](url: Url[A]): Request[(A, String)]
  …
}

Client interpreter of authenticated requests

trait AuthRequestsClient extends EndpointsAlg with PlayClient {
  def authenticatedGet[A](url: Url[A]): Request[(A, String)] = {
    case (a, apiToken) =>
      wsClient
        .url(url(a))
        .withHeaders("X-ApiToken" -> apiToken)
        .get()
  }
}

Server interpretation of authenticated requests

trait AuthRequestsRouter extends EndpointsAlg with PlayRouter {
  def authenticatedGet[A](url: Url[A]): Request[(A, String)] =
    request =>
      for {
        _ <- if (request.method == "GET") Some(()) else None
        a <- url(request)
        apiToken <- request.headers.get("X-ApiToken")
      } yield BodyParser(_ => Accumulator.done(Right((a, apiToken))))
}

Extensibility: summary

Usage

1. Define your HTTP protocol specification

trait ItemsEndpoints extends EndpointsAlg with AuthRequests {
  val create = endpoint(
    authenticatedPost(path / "items", jsonRequest[CreateItem]),
    jsonResponse[Item]
  )
  val read = endpoint(
    authenticatedGet(path / "items" / segment[UUID]),
    jsonResponse[Item]
  )
}

case class Item(id: UUID, name: String, price: Int)

object Item {
  implicit val oformat: OFormat[Item] = …
}

case class CreateItem(name: String, price: Int)

object CreateItem {
  implicit val oformat: OFormat[CreateItem] = …
}

2. Implement the server

class ItemsRouter(items: ItemsService) extends ItemsEndpoints
  with PlayRouter with AuthRequestsRouter {

  val router = routerFromEndpoints(
    create.implementedBy(items.create),
    read.implementedBy(items.read)
  )

}
class ItemsService {
  def create(apiToken: String, createItem: CreateItem): Item = …
  def read(apiToken: String, itemId: UUID): Item = …
}

3. Make the server executable

object Main extends App {
  val itemsService = new ItemsService
  val itemsRouter = new ItemsRouter(itemsService)
  NettyServer.fromRouter()(itemsRouter.router)
}

4. Define a client

val client =
  new ItemsEndpoints with PlayClient with AuthRequestsClient {
    val wsClient = …
  }
val eventuallyItem: Future[Item] =
  client.create("foo123", CreateItem("foo", 42))
…

Usage: summary

Examples

Comment tu exprimes un endpoint qui peut prendre plusieurs types de body, et qu’en fonction du body, tu peux avoir des types de retour différents ?

Antoine Michel, CTO, Zengularity.

Response type depending on the request type (rough idea)

trait ReqResp[A, B]
def myEndpoint[A, B](implicit
  reqResp: ReqResp[A, B],
  oformatA: OFormat[A],
  oformatB: OFormat[B]
): Endpoint[A, B] =
  endpoint(post(path / "foo", jsonRequest[A]), jsonResponse[B])
implicit val reqFooRespBar: ReqResp[CreateItem, ItemCreated] = null

implicit val reqBazRespQuux: ReqResp[UpdateItem, ItemUpdated] = null

How to control the HTTP response status?

  1. Weak (but simple) solution:
trait PlayResults extends EndpointsAlg {
  type Result
  def playResult: Response[Result]
}
trait Foo extends PlayResults {
  val foo = endpoint(get(path / "foo"), playResult)
}
trait PlayResultsRouter extends PlayRouter {
  type Result = play.api.mvc.Result
  val playResult = identity
}
trait FooRouter extends Foo with PlayResultsRouter {
  val action = foo.implementedBy(_ => Ok("Hello"))
}

How to control the HTTP response status?

  1. Better solution:
case class Result[A](status: Int, entity: A)(implicit val owrites: OWrites[A])
trait MyAppResult extends EndpointsAlg {
  def result[A]: Response[Result[A]]
}
trait MyAppResultRouter extends MyAppResult with PlayRouter {
  def result[A] = result => Status(result.status)(result.owrites(result.entity))
}

Questions?

Bonus

Object algebras (Oliveira, 2014)

Data types and folds

sealed trait Expr
case class Add(lhs: Expr, rhs: Expr) extends Expr
case class Lit(value: Int) extends Expr

Data types and folds

def eval(expr: Expr): Int =
  expr match {
    case Add(lhs, rhs) => eval(lhs) + eval(rhs)
    case Lit(x) => x
  }
def print(expr: Expr): String =
  expr match {
    case Add(lhs, rhs) => s"${print(lhs)} + ${print(rhs)}"
    case Lit(x) => x.toString
  }

Data types and folds

def fold[A](add: (A, A) => A, lit: Int => A)(expr: Expr): A =
  expr match {
    case Add(lhs, rhs) => add(fold(add, lit)(lhs), fold(add, lit)(rhs))
    case Lit(x) => lit(x)
  }
val eval: Expr => Int =
  fold[Int](_ + _, identity)
val print: Expr => String =
  fold[String]((lhs, rhs) => s"$lhs + $rhs", _.toString)

Data types and folds

Fold-algebras

def fold[A](add: (A, A) => A, lit: Int => A)(expr: Expr): A =
  expr match {
    case Add(lhs, rhs) => add(fold(add, lit)(lhs), fold(add, lit)(rhs))
    case Lit(x) => lit(x)
  }

Fold-algebras

type FoldAlg[A] = ((A, A) => A, Int => A)
def fold[A](alg: FoldAlg[A]): Expr => A = {
  case Add(lhs, rhs) => alg._1(fold(alg)(lhs), fold(alg)(rhs))
  case Lit(x) => alg._2(x)
}

Object algebra interfaces

trait ExprAlg[A] {
  def add(lhs: A, rhs: A): A
  def lit(value: Int): A
}
def fold[A](alg: ExprAlg[A])(expr: Expr): A =
  expr match {
    case Add(lhs, rhs) => alg.add(fold(alg)(lhs), fold(alg)(rhs))
    case Lit(x) => alg.lit(x)
  }

Object algebras

val eval: ExprAlg[Int] =
  new ExprAlg[Int] {
    def add(lhs: Int, rhs: Int) = lhs + rhs
    def lit(value: Int) = value
  }
val print: ExprAlg[String] =
  new ExprAlg[String] {
    def add(lhs: String, rhs: String) = s"$lhs + $rhs"
    def lit(value: Int) = value.toString
  }

Getting rid of the intermediate data type using Church encodings

def onePlusTwo[A](alg: ExprAlg[A]): A =
  alg.add(alg.lit(1), alg.lit(2))
onePlusTwo(eval) == 3
onePlusTwo(print) == "1 + 2"

So what?

trait MulAlg[A] extends ExprAlg[A] {
  def mul(lhs: A, rhs: A): A
}

Object algebras: summary