From 2d099f6832f6e362b9a4cd48e81a16c8d77adeaf Mon Sep 17 00:00:00 2001 From: adamw Date: Fri, 21 Jul 2017 13:08:18 +0200 Subject: Initial support for async-http-client --- README.md | 29 +++++- .../asynchttpclient/AsyncHttpClientHandler.scala | 113 +++++++++++++++++++++ build.sbt | 12 ++- .../scala/com/softwaremill/sttp/SttpHandler.scala | 6 ++ .../scala/com/softwaremill/sttp/BasicTests.scala | 3 + 5 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 async-http-client-handler/src/main/scala/com/softwaremill/sttp/asynchttpclient/AsyncHttpClientHandler.scala diff --git a/README.md b/README.md index 1647afe..811da8b 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ This handler depends on [akka-http](http://doc.akka.io/docs/akka-http/current/sc A fully **asynchronous** handler. Sending a request returns a response wrapped in a `Future`. -To use, add an implicit value: +Next you'll need to add an implicit value: ```scala implicit val sttpHandler = new AkkaHttpSttpHandler() @@ -235,6 +235,33 @@ val response: Future[Response[Source[ByteString, Any]]] = .send() ``` +### `AsyncHttpClientHandler` + +To use, add the following dependency to your project: + +```scala +"com.softwaremill.sttp" %% "async-http-client-handler" % version +``` + +This handler depends on [async-http-handler](https://github.com/AsyncHttpClient/async-http-client). +A fully **asynchronous** handler, which uses [Netty](http://netty.io) behind the +scenes. Sending a request returns a response wrapped in a `Future`. Different +wrappers will be added in the future. + +Next you'll need to add an implicit value: + +```scala +implicit val sttpHandler = new AsyncHttpClientHandler() + +// or, if you'd like to use custom configuration: +implicit val sttpHandler = new AsyncHttpClientHandler(asyncHttpClientConfig) + +// or, if you'd like to instantiate the AsyncHttpClient yourself: +implicit val sttpHandler = new AsyncHttpClientHandler(asyncHttpClient) +``` + +Streaming is not (yet) supported. + ## Request type All request descriptions have type `RequestT[U, T, S]` (T as in Template). diff --git a/async-http-client-handler/src/main/scala/com/softwaremill/sttp/asynchttpclient/AsyncHttpClientHandler.scala b/async-http-client-handler/src/main/scala/com/softwaremill/sttp/asynchttpclient/AsyncHttpClientHandler.scala new file mode 100644 index 0000000..ecd49cd --- /dev/null +++ b/async-http-client-handler/src/main/scala/com/softwaremill/sttp/asynchttpclient/AsyncHttpClientHandler.scala @@ -0,0 +1,113 @@ +package com.softwaremill.sttp.asynchttpclient + +import java.nio.charset.Charset + +import com.softwaremill.sttp.model._ +import com.softwaremill.sttp.{Request, Response, SttpHandler} +import org.asynchttpclient.{ + AsyncCompletionHandler, + AsyncHttpClient, + AsyncHttpClientConfig, + DefaultAsyncHttpClient, + RequestBuilder, + Request => AsyncRequest, + Response => AsyncResponse +} + +import scala.concurrent.{Future, Promise} +import scala.collection.JavaConverters._ + +class AsyncHttpClientHandler(asyncHttpClient: AsyncHttpClient) + extends SttpHandler[Future, Nothing] { + def this() = this(new DefaultAsyncHttpClient()) + def this(cfg: AsyncHttpClientConfig) = this(new DefaultAsyncHttpClient(cfg)) + + override def send[T](r: Request[T, Nothing]): Future[Response[T]] = { + val p = Promise[Response[T]]() + asyncHttpClient + .prepareRequest(requestToAsync(r)) + .execute(new AsyncCompletionHandler[AsyncResponse] { + override def onCompleted(response: AsyncResponse): AsyncResponse = { + p.success(readResponse(response, r.responseAs)) + response + } + override def onThrowable(t: Throwable): Unit = p.failure(t) + }) + + p.future + } + + private def requestToAsync(r: Request[_, Nothing]): AsyncRequest = { + val rb = new RequestBuilder(r.method.m).setUrl(r.uri.toString) + r.headers.foreach { case (k, v) => rb.setHeader(k, v) } + setBody(r.body, rb) + rb.build() + } + + private def setBody(body: RequestBody[Nothing], rb: RequestBuilder): Unit = { + body match { + case NoBody => // skip + + case StringBody(b, encoding) => + rb.setBody(b.getBytes(encoding)) + + case ByteArrayBody(b) => + rb.setBody(b) + + case ByteBufferBody(b) => + rb.setBody(b) + + case InputStreamBody(b) => + rb.setBody(b) + + case PathBody(b) => + rb.setBody(b.toFile) + + case SerializableBody(f, t) => + setBody(f(t), rb) + + case StreamBody(s) => + // we have an instance of nothing - everything's possible! + s + } + } + + private def readResponse[T]( + response: AsyncResponse, + responseAs: ResponseAs[T, Nothing]): Response[T] = { + Response(readResponseBody(response, responseAs), + response.getStatusCode, + response.getHeaders + .iterator() + .asScala + .map(e => (e.getKey, e.getValue)) + .toList) + } + + private def readResponseBody[T](response: AsyncResponse, + responseAs: ResponseAs[T, Nothing]): T = { + + def asString(enc: String) = response.getResponseBody(Charset.forName(enc)) + + responseAs match { + case IgnoreResponse(g) => + // getting the body and discarding it + response.getResponseBodyAsBytes + g(()) + + case ResponseAsString(enc, g) => + g(asString(enc)) + + case ResponseAsByteArray(g) => + g(response.getResponseBodyAsBytes) + + case r @ ResponseAsParams(enc, g) => + g(r.parse(asString(enc))) + + case ResponseAsStream(_) => + // only possible when the user requests the response as a stream of + // Nothing. Oh well ... + throw new IllegalStateException() + } + } +} diff --git a/build.sbt b/build.sbt index 17fa08d..581a443 100644 --- a/build.sbt +++ b/build.sbt @@ -68,6 +68,16 @@ lazy val akkaHttpHandler: Project = (project in file("akka-http-handler")) ) ) dependsOn (core) +lazy val asyncHttpClientHandler: Project = (project in file( + "async-http-client-handler")) + .settings(commonSettings: _*) + .settings( + name := "async-http-client-handler", + libraryDependencies ++= Seq( + "org.asynchttpclient" % "async-http-client" % "2.0.33" + ) + ) dependsOn (core) + lazy val tests: Project = (project in file("tests")) .settings(commonSettings: _*) .settings( @@ -81,4 +91,4 @@ lazy val tests: Project = (project in file("tests")) "ch.qos.logback" % "logback-classic" % "1.2.3" ).map(_ % "test"), libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value % "test" - ) dependsOn (core, akkaHttpHandler) + ) dependsOn (core, akkaHttpHandler, asyncHttpClientHandler) diff --git a/core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala b/core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala index f0f1fc9..0562fc2 100644 --- a/core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala +++ b/core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala @@ -2,6 +2,12 @@ package com.softwaremill.sttp import scala.language.higherKinds +/** + * @tparam R The type constructor in which responses are wrapped. E.g. `Id` + * for synchronous handlers, `Future` for asynchronous handlers. + * @tparam S The type of streams that are supported by the handler. `Nothing`, + * if streaming requests/responses is not supported by this handler. + */ trait SttpHandler[R[_], -S] { def send[T](request: Request[T, S]): R[Response[T]] } diff --git a/tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala b/tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala index 2b3bbab..0f66cc8 100644 --- a/tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala +++ b/tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala @@ -15,6 +15,7 @@ import com.typesafe.scalalogging.StrictLogging import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures} import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} import better.files._ +import com.softwaremill.sttp.asynchttpclient.AsyncHttpClientHandler import scala.language.higherKinds @@ -111,6 +112,8 @@ class BasicTests ForceWrappedValue.id) runTests("Akka HTTP")(new AkkaHttpSttpHandler(actorSystem), ForceWrappedValue.future) + runTests("Async Http Client")(new AsyncHttpClientHandler(), + ForceWrappedValue.future) def runTests[R[_]](name: String)( implicit handler: SttpHandler[R, Nothing], -- cgit v1.2.3