- install sbt
- add scalatra dependency
- add scalatra-json dependency
- add json4s dependency
Jetty - Eclipse Jetty provides a Web server and javax.servlet container, plus support for HTTP/2, WebSocket etc
- provides http methods as function. request handler is bit weird as it returns
Any
instead of justT
. meaning single resource can respond different types. also it should beaction: HttpRequest => [T]
def get(transformers: RouteTransformer*)(action: => Any): Route = addRoute(Get, transformers, action)
- has to read headers/cookies/ request payload etc explicitly inside a action function
example,
import org.scalatra.{AsyncResult, FutureSupport, ScalatraServlet}
import scala.concurrent.{ExecutionContext, Future}
import org.json4s.{DefaultFormats, Formats}
import org.scalatra.json.JacksonJsonSupport
import org.scalatra.{AsyncResult, FutureSupport, ScalatraServlet}
import scala.concurrent.ExecutionContext.Implicits
scala> final case class ApiResponse(correlationId: String, message: String)
defined class ApiResponse
scala> class RestServer extends ScalatraServlet with JacksonJsonSupport with FutureSupport {
protected implicit val jsonFormats: Formats = DefaultFormats
protected implicit def executor: ExecutionContext = Implicits.global
post("/api/chat") {
new AsyncResult() {
override val is: Future[ApiResponse] = Future {
val correlationId = request.getHeader("correlationId")
val body = request.body
ApiResponse(correlationId, "hi, how can i help ya")
}
}
}
}
defined class RestServer
Scalatra includes a very sophisticated set of validation commands.
These allow you to parse incoming data, instantiate command objects, and automatically apply validations to the objects.
http://scalatra.org/guides/2.3/formats/commands.html
provides generic error handler for response 500 as a partial function
final case class ApiError(errorCode: String, errorMessage: String)
error {
case e: Exception => ApiError("InternalApiError", e.getMessage)
}
Uses json4s which default encoders and decoders
http://scalatra.org/guides/2.3/formats/json.html
No a clean way to expose API definition to consumers as headers/ cookies/ req payload are not part of API definition
jetty perf - 182,147 req/sec
jetty - 1.2 ms for above perf
https://github.com/duwamish-os/config-management-ui
- install sbt
- add plugin PlayScala
- add play-json
- Akka-HTTP
- Netty
Two places for endpoint definition. Play provides routing DSL as partial function
trait Router {
def routes: Router.Routes
//
}
object Router {
type Routes = PartialFunction[RequestHeader, Handler]
}
- has to read headers/cookies/ request payload etc explicitly inside a Handler function
example,
Endopint routes
import api.GameApi
import play.api.libs.json.Json
import play.api.mvc.{Action, AnyContent, RequestHeader}
import play.api.routing._
import play.api.routing.sird._
import play.api.mvc._
class ApiRouter extends SimpleRouter {
override def routes: PartialFunction[RequestHeader, Action[AnyContent]] = {
case GET(p"/game/$correlationId") =>
Action {
Results.Ok(Json.toJson(GameApi.instance.scores(correlationId)))
}
}
}
conf/routes
-> /api route.ApiRouter
API
import play.api.libs.json.{Json, OWrites, Reads}
final case class ApiResponse(correlationID: String, scores: List[Map[String, String]])
object ApiResponse {
implicit val reads: Reads[ApiResponse] = Json.reads[ApiResponse]
implicit val writes: OWrites[ApiResponse] = Json.writes[ApiResponse]
}
class GameApi {
def scores(correlationId: String): ApiResponse = {
val response = ApiResponse(correlationId, List(
Map("player" -> "upd",
"score" -> "1000"),
Map("player" -> "dude",
"score" -> "999")))
response
}
}
object GameApi {
lazy val instance = new GameApi
}
https://www.playframework.com/documentation/2.6.x/ScalaActionsComposition#Validating-requests
https://www.playframework.com/documentation/2.6.x/ScalaErrorHandling
Uses play-json which demands deserializers and serializers for every type
No a clean way to expose API definition to consumers as headers/ cookies/ req payload are not part of API definition
Provides play-ws async http clinet - https://www.playframework.com/documentation/2.6.x/ScalaWS which uses netty-client
93,002 req/sec
6.3 ms/req
https://github.com/duwamish-os/scala-play-war
- install sbt
- add finch-core
- add finch-circe
Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.
https://blog.twitter.com/engineering/en_us/a/2014/netty-at-twitter-with-finagle.html
provides clean routing API as a function Input => EndpointResult[T]
- https://finagle.github.io/finch/user-guide.html#understanding-endpoints
private[finch] trait EndpointMappers {
def get[A](e: Endpoint[A]): EndpointMapper[A] = new EndpointMapper[A](Method.Get, e)
//
}
example,
Endopint routes
scala> final case class PurchaseRequest(items: List[String])
defined class PurchaseRequest
scala> final case class PurchaseResponse(correlationId: String)
defined class PurchaseResponse
import com.twitter.finagle.http.{Request, Response}
import com.twitter.finagle.{Http, Service}
import com.twitter.util.{Await, Future}
import io.circe.generic.auto._
import io.finch._
import io.finch.circe._
import io.finch.syntax._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
import io.finch.syntax.scalaFutures._
scala> object PurchasesServer {
val chatEndpoint: Endpoint[PurchaseResponse] = post("api" :: "purchases" :: header[String]("correlationId") :: jsonBody[PurchaseRequest]) {
(correlationId: String, purchaseRequest: PurchaseRequest) => {
concurrent.Future {
PurchaseResponse(correlationId)
}.map(Ok)
}
}
}
defined object PurchasesServer
Provides well designed request validation API
https://finagle.github.io/finch/user-guide.html#validation
scala> val purchases = post("api" :: "purchases" :: header[Int]("purchaseId").should("be greater than length 5"){_ > 5})
purchases: io.finch.syntax.EndpointMapper[Int] = POST /api :: purchases :: header(purchaseId)
Provides error handler (takes partial function) for each API endpoint
https://finagle.github.io/finch/user-guide.html#errors
Uses circe which provides compile time derivation of deserializers and serializers
Also provides support for other poular functional json libraries - https://finagle.github.io/finch/user-guide.html#json
Headers/Cookies are part of part of API definition. It helps the API definition to be exposed cleanly to the consumers as a jar dependency with small change
Provides featherbed as async http-client - https://finagle.github.io/featherbed/doc/06-building-rest-clients.html
6.3 ms
https://github.com/duwamish-os/chat-server
- install sbt
- add http4s-blaze-server dependency
- add http4s-circe dependency
- add http4s-dsl dependency
blaze - blaze is a scala library for building async pipelines, with a focus on network IO
netty support is coming on as well - http4s/http4s#1831
- provides dsl for routes
Http4sDsl
.
object -> {
def unapply[F[_]](req: Request[F]): Some[(Method, Path)] =
Some((req.method, Path(req.pathInfo)))
}
case class /(parent: Path, child: String) extends Path {
lazy val toList: List[String] = parent.toList ++ List(child)
def lastOption: Some[String] = Some(child)
lazy val asString: String = s"$parent/${UrlCodingUtils.pathEncode(child)}"
override def toString: String = asString
def startsWith(other: Path): Boolean = {
val components = other.toList
toList.take(components.length) === components
}
}
- has to read headers/cookies/ request payload etc explicitly inside a match function
example,
import cats.effect.IO
import fs2.StreamApp
import io.circe._
import io.circe.generic.auto._
import io.circe.syntax._
import org.http4s._
import org.http4s.circe._
import org.http4s.dsl.Http4sDsl
import org.http4s.server.blaze.BlazeBuilder
import schema.{ChatHistory, ChatRequest, ChatResponse}
import scala.concurrent.ExecutionContext.Implicits.global
object RestServer extends StreamApp[IO] with Http4sDsl[IO] {
val service: HttpService[IO] = HttpService[IO] {
case req@GET -> Root / "api" / "chat" / "history" / customerID =>
val correlationId = req.headers.find(_.name == "correlationId").map(_.value).getOrElse("")
Ok(Future(ChatHistory(correlationId, List("hi, how can i help you?", "here is coffee shop"))))
case request@POST -> Root / "api" / "chat" =>
for {
chatRequest <- request.as[ChatRequest]
chatResponse <- Ok(Future(ChatResponse(chatRequest.correlationId, "Here are near by coffee shops")))
} yield chatResponse
}
///
}
uses circe so should be able to auto encode/decode. also provides jsonEncoderOf
def jsonEncoderOf[F[_]: EntityEncoder[?[_], String]: Applicative, A](
implicit encoder: Encoder[A]): EntityEncoder[F, A] =
jsonEncoderWithPrinterOf(defaultPrinter)
https://http4s.org/v0.15/json/
Headers/Cookies are not part of part of API definition.
maybe https://github.com/http4s/rho
2.3 ms