Skip to content

Commit

Permalink
Merge pull request #280 from UdashFramework/udash-rest-jetty
Browse files Browse the repository at this point in the history
rest-jetty module with Jetty based REST client
  • Loading branch information
Roman Janusz authored Apr 26, 2019
2 parents de6c16b + 5b4da03 commit d89265a
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 42 deletions.
8 changes: 7 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ lazy val udash = project.in(file("."))
utils, `utils-js`,
core, `core-js`,
rpc, `rpc-js`,
rest, `rest-js`,
rest, `rest-js`, `rest-jetty`,
i18n, `i18n-js`,
auth, `auth-js`,
css, `css-js`,
Expand Down Expand Up @@ -196,6 +196,12 @@ lazy val `rest-js` = jsProjectFor(project, rest)
libraryDependencies ++= Dependencies.restSjsDeps.value,
)

lazy val `rest-jetty` = jvmProject(project.in(file("rest/jetty")))
.dependsOn(rest % CompileAndTest)
.settings(
libraryDependencies ++= Dependencies.restJettyDeps.value,
)

lazy val i18n = jvmProject(project)
.dependsOn(core % CompileAndTest, rpc % CompileAndTest)

Expand Down
4 changes: 4 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ object Dependencies {
))

val restSjsDeps = restCrossDeps

val restJettyDeps = Def.setting(Seq(
"org.eclipse.jetty" % "jetty-client" % jettyVersion
))

private val cssCrossDeps = Def.setting(Seq(
"com.github.japgolly.scalacss" %%% "core" % scalaCssVersion,
Expand Down
13 changes: 12 additions & 1 deletion rest/.jvm/src/main/scala/io/udash/rest/RestServlet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.typesafe.scalalogging.LazyLogging
import io.udash.rest.RestServlet._
import io.udash.rest.raw._
import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse}
import javax.servlet.{AsyncEvent, AsyncListener}

import scala.annotation.tailrec
import scala.concurrent.duration._
Expand Down Expand Up @@ -44,7 +45,17 @@ class RestServlet(
import RestServlet._

override def service(request: HttpServletRequest, response: HttpServletResponse): Unit = {
val asyncContext = request.startAsync().setup(_.setTimeout(handleTimeout.toMillis))
val asyncContext = request.startAsync()
asyncContext.setTimeout(handleTimeout.toMillis)
asyncContext.addListener(new AsyncListener {
def onComplete(event: AsyncEvent): Unit = ()
def onTimeout(event: AsyncEvent): Unit = {
writeFailure(response, Opt("server operation timed out"))
asyncContext.complete()
}
def onError(event: AsyncEvent): Unit = ()
def onStartAsync(event: AsyncEvent): Unit = ()
})
RawRest.safeAsync(handleRequest(readRequest(request))) {
case Success(restResponse) =>
writeResponse(response, restResponse)
Expand Down
10 changes: 10 additions & 0 deletions rest/.jvm/src/test/resources/RestTestApi.json
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,16 @@
}
}
},
"/neverGet": {
"get": {
"operationId": "neverGet",
"responses": {
"204": {
"description": "Success"
}
}
}
},
"/prefix/{p0}/subget/{p1}": {
"summary": "summary for prefix paths",
"get": {
Expand Down
36 changes: 0 additions & 36 deletions rest/.jvm/src/test/scala/io/udash/rest/HttpRestCallTest.scala

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.udash
package rest

import org.eclipse.jetty.server.Server
import org.eclipse.jetty.servlet.{ServletHandler, ServletHolder}

import scala.concurrent.duration._

abstract class ServletBasedRestApiTest extends RestApiTest with UsesHttpServer {
override implicit def patienceConfig: PatienceConfig = PatienceConfig(10.seconds)

def maxPayloadSize: Int = 1024 * 1024
def serverTimeout: FiniteDuration = 10.seconds

protected def setupServer(server: Server): Unit = {
val servlet = new RestServlet(serverHandle, serverTimeout, maxPayloadSize)
val holder = new ServletHolder(servlet)
val handler = new ServletHandler
handler.addServletWithMapping(holder, "/api/*")
server.setHandler(handler)
}
}
41 changes: 41 additions & 0 deletions rest/.jvm/src/test/scala/io/udash/rest/SttpRestCallTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.udash
package rest

import com.softwaremill.sttp.SttpBackend
import io.udash.rest.raw.HttpErrorException
import io.udash.rest.raw.RawRest.HandleRequest

import scala.concurrent.Future
import scala.concurrent.duration._

trait SttpClientRestTest extends ServletBasedRestApiTest {
implicit val backend: SttpBackend[Future, Nothing] = SttpRestClient.defaultBackend()

def clientHandle: HandleRequest =
SttpRestClient.asHandleRequest(s"$baseUrl/api")

override protected def afterAll(): Unit = {
backend.close()
super.afterAll()
}
}

class SttpRestCallTest extends SttpClientRestTest with RestApiTestScenarios {
def port: Int = 9090

test("too large binary request") {
val future = proxy.binaryEcho(Array.fill[Byte](maxPayloadSize + 1)(5))
val exception = future.failed.futureValue
assert(exception == HttpErrorException(413, "Payload is larger than maximum 1048576 bytes (1048577)"))
}
}

class ServletTimeoutTest extends SttpClientRestTest {
def port: Int = 9091
override def serverTimeout: FiniteDuration = 1.millisecond

test("rest method timeout") {
val exception = proxy.neverGet.failed.futureValue
assert(exception == HttpErrorException(500, "server operation timed out"))
}
}
4 changes: 2 additions & 2 deletions rest/.jvm/src/test/scala/io/udash/rest/UsesHttpServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import org.eclipse.jetty.server.Server
import org.scalatest.{BeforeAndAfterAll, Suite}

trait UsesHttpServer extends BeforeAndAfterAll { this: Suite =>
val port: Int = 9090
def port: Int
val server: Server = new Server(port)
val baseUrl = s"http://localhost:$port"
def baseUrl = s"http://localhost:$port"

protected def setupServer(server: Server): Unit

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package io.udash
package rest.jetty

import java.net.HttpCookie
import java.nio.charset.Charset

import com.avsystem.commons._
import com.avsystem.commons.annotation.explicitGenerics
import io.udash.rest.raw._
import org.eclipse.jetty.client.HttpClient
import org.eclipse.jetty.client.api.Result
import org.eclipse.jetty.client.util.{BufferingResponseListener, BytesContentProvider, StringContentProvider}
import org.eclipse.jetty.http.{HttpHeader, MimeTypes}

import scala.util.{Failure, Success}
import scala.concurrent.duration._

object JettyRestClient {
final val DefaultMaxResponseLength = 2 * 1024 * 1024
final val DefaultTimeout = 10.seconds

@explicitGenerics def apply[RestApi: RawRest.AsRealRpc : RestMetadata](
client: HttpClient,
baseUri: String,
maxResponseLength: Int = DefaultMaxResponseLength,
timeout: Duration = DefaultTimeout
): RestApi =
RawRest.fromHandleRequest[RestApi](asHandleRequest(client, baseUri, maxResponseLength, timeout))

def asHandleRequest(
client: HttpClient,
baseUrl: String,
maxResponseLength: Int = DefaultMaxResponseLength,
timeout: Duration = DefaultTimeout
): RawRest.HandleRequest =
RawRest.safeHandle { request =>
callback =>
val path = baseUrl + PlainValue.encodePath(request.parameters.path)
val httpReq = client.newRequest(baseUrl).method(request.method.name)

httpReq.path(path)
request.parameters.query.entries.foreach {
case (name, PlainValue(value)) => httpReq.param(name, value)
}
request.parameters.headers.entries.foreach {
case (name, PlainValue(value)) => httpReq.header(name, value)
}
request.parameters.cookies.entries.foreach {
case (name, PlainValue(value)) => httpReq.cookie(new HttpCookie(name, value))
}

request.body match {
case HttpBody.Empty =>
case tb: HttpBody.Textual =>
httpReq.content(new StringContentProvider(tb.contentType, tb.content, Charset.forName(tb.charset)))
case bb: HttpBody.Binary =>
httpReq.content(new BytesContentProvider(bb.contentType, bb.bytes))
}

timeout match {
case fd: FiniteDuration => httpReq.timeout(fd.length, fd.unit)
case _ =>
}

httpReq.send(new BufferingResponseListener(maxResponseLength) {
override def onComplete(result: Result): Unit =
if (result.isSucceeded) {
val httpResp = result.getResponse
val contentTypeOpt = httpResp.getHeaders.get(HttpHeader.CONTENT_TYPE).opt
val charsetOpt = contentTypeOpt.map(MimeTypes.getCharsetFromContentType)
val body = (contentTypeOpt, charsetOpt) match {
case (Opt(contentType), Opt(charset)) =>
HttpBody.textual(getContentAsString, MimeTypes.getContentTypeWithoutCharset(contentType), charset)
case (Opt(contentType), Opt.Empty) =>
HttpBody.binary(getContent, contentType)
case _ =>
HttpBody.Empty
}
val headers = httpResp.getHeaders.iterator.asScala.map(h => (h.getName, PlainValue(h.getValue))).toList
val response = RestResponse(httpResp.getStatus, IMapping(headers), body)
callback(Success(response))
} else {
callback(Failure(result.getFailure))
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.udash
package rest.jetty

import io.udash.rest.raw.RawRest.HandleRequest
import io.udash.rest.{RestApiTestScenarios, ServletBasedRestApiTest}
import org.eclipse.jetty.client.HttpClient

final class JettyRestCallTest extends ServletBasedRestApiTest with RestApiTestScenarios {
def port: Int = 9092
val client: HttpClient = new HttpClient

def clientHandle: HandleRequest =
JettyRestClient.asHandleRequest(client, s"$baseUrl/api", maxPayloadSize)

override protected def beforeAll(): Unit = {
super.beforeAll()
client.start()
}

override protected def afterAll(): Unit = {
client.stop()
super.afterAll()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import org.scalactic.source.Position
import org.scalatest.FunSuite
import org.scalatest.concurrent.ScalaFutures

abstract class AbstractRestCallTest extends FunSuite with ScalaFutures {
abstract class RestApiTest extends FunSuite with ScalaFutures {
final val serverHandle: RawRest.HandleRequest =
RawRest.asHandleRequest[RestTestApi](RestTestApi.Impl)

Expand All @@ -27,7 +27,9 @@ abstract class AbstractRestCallTest extends FunSuite with ScalaFutures {
case arr: Array[_] => arr.deep
case _ => value
}
}

trait RestApiTestScenarios extends RestApiTest {
test("trivial GET") {
testCall(_.trivialGet)
}
Expand Down Expand Up @@ -73,6 +75,6 @@ abstract class AbstractRestCallTest extends FunSuite with ScalaFutures {
}
}

class DirectRestCallTest extends AbstractRestCallTest {
class DirectRestApiTest extends RestApiTestScenarios {
def clientHandle: HandleRequest = serverHandle
}
2 changes: 2 additions & 0 deletions rest/src/test/scala/io/udash/rest/RestTestApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ trait RestTestApi {
@GET def trivialGet: Future[Unit]
@GET def failingGet: Future[Unit]
@GET def moreFailingGet: Future[Unit]
@GET def neverGet: Future[Unit]

@GET def getEntity(id: RestEntityId): Future[RestEntity]

Expand Down Expand Up @@ -122,6 +123,7 @@ object RestTestApi extends DefaultRestApiCompanion[RestTestApi] {
def trivialGet: Future[Unit] = Future.unit
def failingGet: Future[Unit] = Future.failed(HttpErrorException(503, "nie"))
def moreFailingGet: Future[Unit] = throw HttpErrorException(503, "nie")
def neverGet: Future[Unit] = Promise[Unit].future // Future.never if it wasn't for Scala 2.11
def getEntity(id: RestEntityId): Future[RestEntity] = Future.successful(RestEntity(id, s"${id.value}-name"))
def complexGet(p1: Int, p2: String, h1: Int, h2: String, q1: Int, q2: String, c1: Int, c2: String): Future[RestEntity] =
Future.successful(RestEntity(RestEntityId(s"$p1-$h1-$q1-$c1"), s"$p2-$h2-$q2-$c2"))
Expand Down

0 comments on commit d89265a

Please sign in to comment.