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 itemRead
object 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 String
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
}
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)]
…
}
EndpointsAlg
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()
}
}
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))))
}
EndpointsAlg
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] = …
}
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] = null
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"))
}
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 Expr
Lit(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) == 3
onePlusTwo(print) == "1 + 2"
trait MulAlg[A] extends ExprAlg[A] {
def mul(lhs: A, rhs: A): A
}
mul
is defined independently of add
and lit
;