aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoradamw <adam@warski.org>2017-11-13 15:39:31 +0100
committeradamw <adam@warski.org>2017-11-13 15:39:31 +0100
commitd7d26fbda8559adad2737c2df73076f002c194f9 (patch)
tree04ff4bfa0b9d0806c32f20033f6c8536cf47a260
parenta0fdfa6f6114aec289e46c4261522244b160e522 (diff)
downloadsttp-d7d26fbda8559adad2737c2df73076f002c194f9.tar.gz
sttp-d7d26fbda8559adad2737c2df73076f002c194f9.tar.bz2
sttp-d7d26fbda8559adad2737c2df73076f002c194f9.zip
#44: supporting type conversion in the backend stub
-rw-r--r--async-http-client-backend/src/main/scala/com/softwaremill/sttp/asynchttpclient/AsyncHttpClientBackend.scala4
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/HttpURLConnectionBackend.scala6
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/package.scala6
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/testing/SttpBackendStub.scala49
-rw-r--r--core/src/test/scala/com/softwaremill/sttp/testing/SttpBackendStubTests.scala46
-rw-r--r--docs/backends/testing.rst31
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
-----------------------------