From d7d26fbda8559adad2737c2df73076f002c194f9 Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 13 Nov 2017 15:39:31 +0100 Subject: #44: supporting type conversion in the backend stub --- .../asynchttpclient/AsyncHttpClientBackend.scala | 4 +- .../sttp/HttpURLConnectionBackend.scala | 6 +-- .../main/scala/com/softwaremill/sttp/package.scala | 6 +++ .../sttp/testing/SttpBackendStub.scala | 49 ++++++++++++++++++++-- .../sttp/testing/SttpBackendStubTests.scala | 46 ++++++++++++++++++-- docs/backends/testing.rst | 31 ++++++++++++++ 6 files changed, 128 insertions(+), 14 deletions(-) diff --git a/async-http-client-backend/src/main/scala/com/softwaremill/sttp/asynchttpclient/AsyncHttpClientBackend.scala b/async-http-client-backend/src/main/scala/com/softwaremill/sttp/asynchttpclient/AsyncHttpClientBackend.scala index a19dc1a..ddf5570 100644 --- a/async-http-client-backend/src/main/scala/com/softwaremill/sttp/asynchttpclient/AsyncHttpClientBackend.scala +++ b/async-http-client-backend/src/main/scala/com/softwaremill/sttp/asynchttpclient/AsyncHttpClientBackend.scala @@ -223,9 +223,7 @@ abstract class AsyncHttpClientBackend[R[_], S](asyncHttpClient: AsyncHttpClient, case InputStreamBody(b, _) => // sadly async http client only supports parts that are strings, // byte arrays or files - val baos = new ByteArrayOutputStream() - transfer(b, baos) - new ByteArrayPart(nameWithFilename, baos.toByteArray) + new ByteArrayPart(nameWithFilename, toByteArray(b)) case PathBody(b, _) => new FilePart(mp.name, b.toFile, null, null, mp.fileName.orNull) } diff --git a/core/src/main/scala/com/softwaremill/sttp/HttpURLConnectionBackend.scala b/core/src/main/scala/com/softwaremill/sttp/HttpURLConnectionBackend.scala index f35bd40..083b1c3 100644 --- a/core/src/main/scala/com/softwaremill/sttp/HttpURLConnectionBackend.scala +++ b/core/src/main/scala/com/softwaremill/sttp/HttpURLConnectionBackend.scala @@ -241,11 +241,7 @@ class HttpURLConnectionBackend private ( asString(enc) case ResponseAsByteArray => - val os = new ByteArrayOutputStream - - transfer(is, os) - - os.toByteArray + toByteArray(is) case ResponseAsStream() => // only possible when the user requests the response as a stream of diff --git a/core/src/main/scala/com/softwaremill/sttp/package.scala b/core/src/main/scala/com/softwaremill/sttp/package.scala index ed685fa..b641a77 100644 --- a/core/src/main/scala/com/softwaremill/sttp/package.scala +++ b/core/src/main/scala/com/softwaremill/sttp/package.scala @@ -257,6 +257,12 @@ package object sttp { transfer() } + private[sttp] def toByteArray(is: InputStream): Array[Byte] = { + val os = new ByteArrayOutputStream + transfer(is, os) + os.toByteArray + } + private[sttp] def codeIsSuccess(c: Int): Boolean = c >= 200 && c < 300 private[sttp] def concatByteBuffers(bb1: ByteBuffer, diff --git a/core/src/main/scala/com/softwaremill/sttp/testing/SttpBackendStub.scala b/core/src/main/scala/com/softwaremill/sttp/testing/SttpBackendStub.scala index 553afff..8a91e36 100644 --- a/core/src/main/scala/com/softwaremill/sttp/testing/SttpBackendStub.scala +++ b/core/src/main/scala/com/softwaremill/sttp/testing/SttpBackendStub.scala @@ -1,7 +1,9 @@ package com.softwaremill.sttp.testing +import java.io.{File, InputStream} + import com.softwaremill.sttp.testing.SttpBackendStub._ -import com.softwaremill.sttp.{MonadError, Request, Response, SttpBackend} +import com.softwaremill.sttp._ import scala.language.higherKinds import scala.util.{Failure, Success, Try} @@ -46,8 +48,9 @@ class SttpBackendStub[R[_], S] private (rm: MonadError[R], case matcher: Matcher[T @unchecked] if matcher(request) => Try(matcher.response(request).get) } match { - case Some(Success(response)) => wrapResponse(response) - case Some(Failure(e)) => rm.error(e) + case Some(Success(response)) => + wrapResponse(tryAdjustResponseType(request.response, response)) + case Some(Failure(e)) => rm.error(e) case None => fallback match { case None => wrapResponse(DefaultResponse) @@ -133,4 +136,44 @@ object SttpBackendStub { }) } } + + private[sttp] def tryAdjustResponseType[T, U](ra: ResponseAs[T, _], + r: Response[U]): Response[_] = { + r.body match { + case Left(_) => r + case Right(body) => + val newBody: Any = tryAdjustResponseBody(ra, body).getOrElse(body) + r.copy(body = Right(newBody)) + } + } + + private[sttp] def tryAdjustResponseBody[T, U](ra: ResponseAs[T, _], + b: U): Option[T] = { + ra match { + case IgnoreResponse => Some(()) + case ResponseAsString(enc) => + b match { + case s: String => Some(s) + case a: Array[Byte] => Some(new String(a, enc)) + case is: InputStream => Some(new String(toByteArray(is), enc)) + case _ => None + } + case ResponseAsByteArray => + b match { + case s: String => Some(s.getBytes(Utf8)) + case a: Array[Byte] => Some(a) + case is: InputStream => Some(toByteArray(is)) + case _ => None + } + case ras @ ResponseAsStream() => + None + case ResponseAsFile(file, overwrite) => + b match { + case f: File => Some(f) + case _ => None + } + case MappedResponseAs(raw, g) => + tryAdjustResponseBody(raw, b).map(g) + } + } } diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/SttpBackendStubTests.scala b/core/src/test/scala/com/softwaremill/sttp/testing/SttpBackendStubTests.scala index 292d324..7e6f15f 100644 --- a/core/src/test/scala/com/softwaremill/sttp/testing/SttpBackendStubTests.scala +++ b/core/src/test/scala/com/softwaremill/sttp/testing/SttpBackendStubTests.scala @@ -1,15 +1,15 @@ package com.softwaremill.sttp.testing +import java.io.ByteArrayInputStream import java.util.concurrent.TimeoutException import scala.concurrent.ExecutionContext.Implicits.global - import com.softwaremill.sttp._ import org.scalatest.concurrent.ScalaFutures import org.scalatest.{FlatSpec, Matchers} class SttpBackendStubTests extends FlatSpec with Matchers with ScalaFutures { - val testingStub = SttpBackendStub(HttpURLConnectionBackend()) + private val testingStub = SttpBackendStub(HttpURLConnectionBackend()) .whenRequestMatches(_.uri.path.startsWith(List("a", "b"))) .thenRespondOk() .whenRequestMatches(_.uri.paramsMap.get("p").contains("v")) @@ -93,7 +93,21 @@ class SttpBackendStubTests extends FlatSpec with Matchers with ScalaFutures { result.failed.futureValue shouldBe a[TimeoutException] } - val testingStubWithFallback = SttpBackendStub + it should "try to convert a basic response to a mapped one" in { + implicit val s = SttpBackendStub(HttpURLConnectionBackend()) + .whenRequestMatches(_ => true) + .thenRespond("10") + + val result = sttp + .get(uri"http://example.org") + .mapResponse(_.toInt) + .mapResponse(_ * 2) + .send() + + result.body should be(Right(20)) + } + + private val testingStubWithFallback = SttpBackendStub .withFallback(testingStub) .whenRequestMatches(_.uri.path.startsWith(List("c"))) .thenRespond("ok") @@ -111,4 +125,30 @@ class SttpBackendStubTests extends FlatSpec with Matchers with ScalaFutures { val r = sttp.post(uri"http://example.org/a/b").send() r.is200 should be(true) } + + private val s = "Hello, world!" + private val adjustTestData = List[(Any, ResponseAs[_, _], Any)]( + (s, IgnoreResponse, Some(())), + (s, ResponseAsString(Utf8), Some(s)), + (s.getBytes(Utf8), ResponseAsString(Utf8), Some(s)), + (new ByteArrayInputStream(s.getBytes(Utf8)), + ResponseAsString(Utf8), + Some(s)), + (10, ResponseAsString(Utf8), None), + ("10", + MappedResponseAs(ResponseAsString(Utf8), (_: String).toInt), + Some(10)), + (10, MappedResponseAs(ResponseAsString(Utf8), (_: String).toInt), None) + ) + + behavior of "tryAdjustResponseBody" + + for { + (body, responseAs, expectedResult) <- adjustTestData + } { + it should s"adjust $body to $expectedResult when specified as $responseAs" in { + SttpBackendStub.tryAdjustResponseBody(responseAs, body) should be( + expectedResult) + } + } } diff --git a/docs/backends/testing.rst b/docs/backends/testing.rst index d1e99da..d082f40 100644 --- a/docs/backends/testing.rst +++ b/docs/backends/testing.rst @@ -44,6 +44,37 @@ If you want to simulate an exception being thrown by a backend, e.g. a socket ti .whenRequestMatches(_ => true) .thenRespond(throw new TimeoutException()) +Adjusting the response body type +-------------------------------- + +If the type of the response body returned by the stub's rules (as specified using the ``.when...`` methods) doesn't match what was specified in the request, the stub will attempt to convert the body to the desired type. This might be useful when: + +* testing code which maps a basic response body to a custom type, e.g. mapping a raw json string using a decoder to a domain type +* reading a classpath resource (which results in an ``InputStream``) and requesting a response of e.g. type ``String`` + +The following conversions are supported: + +* anything to ``()`` (unit), when the response is ignored +* ``InputStream`` and ``Array[Byte]`` to ``String`` +* ``InputStream`` and ``String`` to ``Array[Byte]`` +* ``InputStream``, ``String`` and ``Array[Byte]`` to custom types through mapped response specifications + +For example:: + + implicit val testingBackend = SttpBackendStub(HttpURLConnectionBackend()) + .whenRequestMatches(_ => true) + .thenRespond(""" {"username": "john", "age": 65 } """) + + def parseUserJson(a: Array[Byte]): User = ... + + val response = sttp.get(uri"http://example.com") + .response(asByteArray.map(parseUserJson)) + .send() + +In the example above, the stub's rules specify that a response with a ``String``-body should be returned for any request; the request, on the other hand, specifies that responses should be parsed from a byte array to a custom ``User`` type. These type don't match, so the ``SttpBackendStub`` will in this case convert the body to the desired type. + +Note that no conversions will be attempted for streaming response bodies. + Delegating to another backend ----------------------------- -- cgit v1.2.3