Julien Richard-Foy
http://julienrf.github.io/zrm-endpoints-2016
POST /items myshop.Items.create
GET /items/:id myshop.Items.read(id)
myshop.Items valuePOST /items myshop.Items.create
GET /items/:id myshop.Items.read(id)
myshop.Items valueval itemsPath = /items
val itemsCtl = myshop.Items
POST itemsPath itemsCtl.create
GET itemsPath/:id itemsCtl.read(id)POST /items myshop.Items.create
GET /items/:id myshop.Items.read(id)
myshop.Items valueval itemsPath = path / "items"
val itemsCtl = myshop.Items
val create = endpoint(post(itemsPath), itemsCtl.create)
val read = endpoint(get (itemsPath / segment[String]), itemsCtl.read _)Items controller is a class, instead of an object?class ItemsEndpoints(itemsCtl: Items) {
val itemsPath = path / "items"
val create = endpoint(post(itemsPath), itemsCtl.create)
val read = endpoint(get (itemsPath / segment[String]), itemsCtl.read _)
}create and read?trait ResourceEndpoints {
def create: Endpoint
def read: Endpoint
}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 _)
}class ItemsEndpoints(itemsCtl: Items) {
val itemsPath = path / "items"
val create = endpoint(post(itemsPath), itemsCtl.create)
val read = endpoint(get (itemsPath / segment[String]), itemsCtl.read _)
}object ItemsEndpoints {
val itemsPath = path / "items"
val create = endpoint(post(itemsPath))
val read = endpoint(get (itemsPath / segment[String]))
}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)endpoint, post, path, jsonRequest, etc. each return an abstract representation of a part of the specificationclass ItemsRouter(items: Items) {
val router = playRouterFromEndpoints(
playRoute(ItemsEndpoints.create, items.create),
playRoute(ItemsEndpoints.read, items.read)
)
}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] = …
}Items class) directly manipulate DTOs instead of JSONclass ItemsClient(wsClient: WSClient) {
val create = playClient(ItemsEndpoints.create, wsClient)
val read = playClient(ItemsEndpoints.read, wsClient)
}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 itemReadobject ItemsClient {
val create = xhrClient(ItemsEndpoints.create)
val read = xhrClient(ItemsEndpoints.read)
}val eventuallyItemCreated: js.Promise[Item] =
ItemsClient.create(CreateItem("foo", 42))val create = endpoint(
post(path / "items", jsonRequest[CreateItem]),
jsonResponse[Item]
)
val read = endpoint(
get(path / "items" / segment[UUID]),
jsonResponse[Item]
)GET and POST requests must provide the appropriate credentials (in a way that is specific to our app – e.g. using HTTP basic authentication, or an API token, or a cookie, etc.)val create = endpoint(
authenticatedPost(path / "items", jsonRequest[CreateItem]),
jsonResponse[Item]
)
val read = endpoint(
authenticatedGet(path / "items" / segment[UUID]),
jsonResponse[Item]
)authenticatedGet and authenticatedPost terms (methods) to the mix
EndpointsAlg interfacetrait EndpointsAlg {
def get[A](url: Url[A]): Request[A]
type Url[A]
type Request[A]
…
}EndpointsAlg provides vocabulary for defining HTTP protocol specifications:
EndpointAlg has no JVM-only dependency, so it can also be used with Scala.js.Request[String]
String is the information to supply in order to build such a request ;String is the information that is provided when processing such a request.Request[(String, Int)]trait PlayClient extends EndpointsAlg {
}trait PlayClient extends EndpointsAlg {
type Request[A] = A => Future[WSResponse]
}Request[A] is a function that, given an A, eventually returns an HTTP responsetrait PlayClient extends EndpointsAlg {
type Request[A] = A => Future[WSResponse]
type Url[A] = A => String
}Url[A] is a function that, given an A, returns an URL as a Stringtrait 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
}get takes the URL function as parameter, and returns a function that, given an A computes the URL and performs an HTTP request using an HTTP clienttrait PlayRouter extends EndpointsAlg {
}trait PlayRouter extends EndpointsAlg {
type Request[A] = RequestHeader => Option[BodyParser[A]]
}Request[A] is a function that, given an incoming request’s headers, optionally returns a BodyParser[A] (defining how to get an A out of the request) or None if the incoming request does not match the request specificationtrait PlayRouter extends EndpointsAlg {
type Request[A] = RequestHeader => Option[BodyParser[A]]
type Url[A] = RequestHeader => Option[A]
}Url[A] is a function that, given an incoming request’s headers, optionally returns an A, or None if the incoming request does not match the request specification ;Url[A] attempts to extract an A from request headers.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
}get takes the URL function as parameter and returns a function that, given an incoming request’s headers, checks that the request method is GET, applies the URL function, and finally returns a BodyParser that immediately succeeds with the A decoded from the URLEndpointsAlg and implements the type members and methods according to the desired semanticstrait AuthRequests extends EndpointsAlg {
def authenticatedGet[A](url: Url[A]): Request[(A, String)]
…
}EndpointsAlgtrait 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()
}
}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))))
}EndpointsAlgtrait 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] = …
}class ItemsRouter(items: ItemsService) extends ItemsEndpoints
with PlayRouter with AuthRequestsRouter {
val router = routerFromEndpoints(
create.implementedBy(items.create),
read.implementedBy(items.read)
)
}ItemsService class:)class ItemsService {
def create(apiToken: String, createItem: CreateItem): Item = …
def read(apiToken: String, itemId: UUID): Item = …
}object Main extends App {
val itemsService = new ItemsService
val itemsRouter = new ItemsRouter(itemsService)
NettyServer.fromRouter()(itemsRouter.router)
}val client =
new ItemsEndpoints with PlayClient with AuthRequestsClient {
val wsClient = …
}val eventuallyItem: Future[Item] =
client.create("foo123", CreateItem("foo", 42))
…EndpointsAlg (or any other abstract trait that brings additional vocabulary) ;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.
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] = nulltrait 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"))
}case class Result[A](status: Int, entity: A)(implicit val owrites: OWrites[A])EndpointsAlg trait: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))
}sealed trait Expr
case class Add(lhs: Expr, rhs: Expr) extends Expr
case class Lit(value: Int) extends ExprLit(1), Add(Lit(1), Lit(2))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
}Expr ;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)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 into a proper type…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)
}FoldAlg is a fold algebra ;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)
}ExprAlg[A] is an object algebra interfaceval 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
}eval and print are object algebrasfold traverses an Expr and calls the corresponding methods of ExprAlg ;Expr data type, let’s directly use ExprAlg as an abstract factory:def onePlusTwo[A](alg: ExprAlg[A]): A =
alg.add(alg.lit(1), alg.lit(2))onePlusTwo is a Church encoding for Add(Lit(1), Lit(2)) ;onePlusTwo, to get an actual value:onePlusTwo(eval) == 3onePlusTwo(print) == "1 + 2"trait MulAlg[A] extends ExprAlg[A] {
def mul(lhs: A, rhs: A): A
}mul is defined independently of add and lit ;