Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
WSClient powered backend for sttp.
organization := "com.mcl"
name := "sttpbackend-play24"
version := "0.0.0-SNAPSHOT"
scalaVersion := "2.11.7"
lazy val root = (project in file(".")).enablePlugins(PlayScala)
libraryDependencies ++= Seq(
"org.scalatestplus" %% "play" % "1.4.0" % "test",
"com.softwaremill.sttp" %% "core" % "1.3.8",
ws,
// Test dependencies for sttp https://sttp.readthedocs.io/en/latest/backends/custom.html
"com.softwaremill.sttp" %% "core" % "1.3.8" % "test" classifier "tests",
"com.typesafe.akka" %% "akka-http" % "10.1.1" % "test",
"ch.megard" %% "akka-http-cors" % "0.3.0" % "test",
"com.typesafe.akka" %% "akka-stream" % "2.5.12" % "test",
"org.scalatest" %% "scalatest" % "3.0.5" % "test",
// Avoid errors with SL4J
"com.typesafe.akka" %% "akka-slf4j" % "2.4.16"
)
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.11")
[info] Tests: succeeded 37, failed 10, canceled 0, ignored 0, pending 0
[info] *** 1 SUITE ABORTED ***
[info] *** 10 TESTS FAILED ***
[error] Error: Total 1, Failed 0, Errors 1, Passed 0
[error] Error during tests:
[error] module.WSClientBackendTest
[error] (test:test) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 11 s, completed Oct 16, 2018 3:29:04 PM
package module
/* Don't import ._ because there's naming conflicts between sttp and ws for request bodies and whatnot */
import play.api.libs.ws
import ws.{ WSClient, WSClientConfig, WSRequest, WSResponse }
import play.api.libs.iteratee.Enumerator
import com.softwaremill.sttp._
import com.softwaremill.sttp.internal.SttpFile
import com.softwaremill.sttp.monadSyntax._
import scala.concurrent.{ Future, ExecutionContext }
import java.nio.file.Files
import java.nio.file.StandardOpenOption
class WSClientBackend(
wsClient: WSClient,
wsClientConfig: WSClientConfig
)(implicit MAE: MonadAsyncError[Future], executionContext: ExecutionContext) extends SttpBackend[Future, Nothing] {
override def responseMonad: MonadError[Future] = MAE
override def close(): Unit = {
wsClient.close()
}
override def send[T](request: Request[T, Nothing]): Future[Response[T]] = {
val wsRequest = createClientRequest(request)
for {
response <- wsRequest.execute()
body <- if (StatusCodes.isSuccess(response.status)) {
responseToBody(response, request.response /*.response is actually a ResponseAs*/ ).map(Right(_))
} else {
Future.successful(Left(response.bodyAsBytes))
}
} yield {
val headers = response.allHeaders.flatMap {
case (k, values) => values.map(v => (k, v))
}.toList
Response(
rawErrorBody = body,
code = response.status,
statusText = response.statusText,
headers = headers,
history = Nil
)
}
}
private def whyMustWeWriteTheseIn2018(inputStream: java.io.InputStream): Array[Byte] = {
collection.Iterator.continually(inputStream.read).takeWhile(_ != -1).map(_.toByte).toArray
}
protected def createClientRequest[T](request: Request[T, Nothing]): WSRequest = {
val body = request.body match {
case NoBody => ws.EmptyBody
case rBody: FileBody => ws.FileBody(rBody.f.toFile)
case rBody: ByteArrayBody => ws.InMemoryBody(rBody.b)
case rBody: ByteBufferBody => ws.InMemoryBody(rBody.b.array())
case rBody: InputStreamBody => ws.InMemoryBody(
whyMustWeWriteTheseIn2018(rBody.b)
)
case rBody: StringBody => ws.InMemoryBody(rBody.s.getBytes(rBody.encoding))
case rBody: MultipartBody =>
throw new UnsupportedOperationException("WSClientBackend does not support MultipartBody bodies yet")
case rBody: StreamBody[Nothing] =>
// TODO: study more to implement this https://github.com/guymers/sttp-vertx/blob/master/src/main/scala/com/github/guymers/sttp/vertx/VertxBackend.scala
// A stream of nothing? Means... there's nothing to send?
ws.EmptyBody
}
// TODO:
//withAuth ?
//withProxyServer
//withVirtualHost
/* Should be handled by .url already? TODO: test that this is the case
.withQueryString(
request.uri.queryFragments.map(queryFragment => ):_*
)*/
wsClient
.url(request.uri.toString)
.withHeaders(
request.headers: _*
)
.withMethod(
request.method.m
)
.withBody(body)
.withFollowRedirects(request.options.followRedirects)
.withRequestTimeout(request.options.readTimeout.toMillis)
}
protected def responseToBody[T](wsResponse: WSResponse, responseAs: ResponseAs[T, Nothing]): Future[T] = {
/** Inner function to apply because when we hit a MappedResponseAs we need to parse the basic part the same way */
def inner[T2](basicResponseAs: BasicResponseAs[T2, Nothing]): Future[T2] = {
basicResponseAs match {
case IgnoreResponse => MAE.unit(())
case ResponseAsByteArray => Future.successful(wsResponse.bodyAsBytes)
case ResponseAsString(encoding) => Future.successful(wsResponse.body)
case ResponseAsFile(output, overwrite) =>
if (output.toFile.exists() && !overwrite) {
Future.failed(new IllegalStateException(s"${output.toPath.toString} already exists and you didn't specify to overwrite it!"))
} else {
/* Basing this on what I see here:
* https://github.com/softwaremill/sttp/blob/1e7cd584f749bfbae03d30ea123e6a50e3cd23e2/core/jvm/src/main/scala/com/softwaremill/sttp/HttpURLConnectionBackend.scala#L250
* https://github.com/softwaremill/sttp/blob/7f77a1356616a33dab72bb0fbc0242f5774a1d0a/core/jvm/src/main/scala/com/softwaremill/sttp/FileHelpers.scala#L9
*/
val flags = if (overwrite) {
StandardOpenOption.TRUNCATE_EXISTING ::
StandardOpenOption.WRITE :: Nil
} else {
StandardOpenOption.CREATE_NEW :: StandardOpenOption.WRITE :: Nil
}
Files.write(output.toPath, wsResponse.bodyAsBytes, flags: _*)
Future.successful(com.softwaremill.sttp.internal.SttpFile.fromPath(output.toPath))
}
case ras @ ResponseAsStream() => {
MAE.error(new IllegalStateException("Client does not support streaming responses yet"))
}
}
}
responseAs match {
case bras: BasicResponseAs[t, s] => inner(bras)
case MappedResponseAs(raw, f) => inner(raw).map(f)
}
}
}
package module
import org.scalatest._
import org.scalatestplus.play._
import play.api.libs.ws._
import com.softwaremill.sttp.{ SttpBackend, FollowRedirectsBackend }
import com.softwaremill.sttp.testing.{ ConvertToFuture, HttpTest }
import scala.concurrent.Future
class WSClientBackendTest extends HttpTest[Future] with OneAppPerSuite {
lazy val wsClient = WS.client(app)
lazy val conf = WSClientConfig()
implicit val mae = new com.softwaremill.sttp.FutureMonad()
override implicit val convertToFuture: ConvertToFuture[Future] = ConvertToFuture.future
override implicit lazy val backend: SttpBackend[Future, Nothing] = new FollowRedirectsBackend[Future, Nothing](new WSClientBackend(wsClient, conf))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment