diff options
author | Bjørn Madsen <bm@aeons.dk> | 2017-08-06 00:28:33 +0200 |
---|---|---|
committer | Bjørn Madsen <bm@aeons.dk> | 2017-08-06 00:28:33 +0200 |
commit | 060cd745cdaa72ef433720fa09845c1132a516d8 (patch) | |
tree | 391629ed92d1973b048c84159df375dac391b26f | |
parent | ff76a1737bb88c2664927db309d196e677ba3e98 (diff) | |
download | sttp-060cd745cdaa72ef433720fa09845c1132a516d8.tar.gz sttp-060cd745cdaa72ef433720fa09845c1132a516d8.tar.bz2 sttp-060cd745cdaa72ef433720fa09845c1132a516d8.zip |
Add circe support module
-rw-r--r-- | build.sbt | 11 | ||||
-rw-r--r-- | circe/src/main/scala/com/softwaremill/sttp/circe/circe.scala | 32 | ||||
-rw-r--r-- | circe/src/test/scala/com/softwaremill/sttp/CirceTests.scala | 113 | ||||
-rw-r--r-- | core/src/main/scala/com/softwaremill/sttp/RequestT.scala | 4 | ||||
-rw-r--r-- | core/src/main/scala/com/softwaremill/sttp/model/ResponseAs.scala | 1 |
5 files changed, 158 insertions, 3 deletions
@@ -126,6 +126,17 @@ lazy val okhttpClientHandler: Project = (project in file( ) ) dependsOn core +lazy val circe: Project = (project in file("circe")) + .settings(commonSettings: _*) + .settings( + name := "circe", + libraryDependencies ++= Seq( + "io.circe" %% "circe-core" % "0.8.0", + "io.circe" %% "circe-parser" % "0.8.0", + scalaTest % "test" + ) + ) dependsOn core + lazy val tests: Project = (project in file("tests")) .settings(commonSettings: _*) .settings( diff --git a/circe/src/main/scala/com/softwaremill/sttp/circe/circe.scala b/circe/src/main/scala/com/softwaremill/sttp/circe/circe.scala new file mode 100644 index 0000000..0233f08 --- /dev/null +++ b/circe/src/main/scala/com/softwaremill/sttp/circe/circe.scala @@ -0,0 +1,32 @@ +package com.softwaremill.sttp + +import com.softwaremill.sttp.model.{ + RequestBody, + ResponseAs, + SerializableBody, + StringBody +} +import io.circe.parser._ +import io.circe.{Decoder, Encoder} + +import scala.language.higherKinds + +package object circe { + private[sttp] val ApplicationJsonContentType = "application/json" + + private def serializeBody[B](body: B)( + implicit encoder: Encoder[B]): RequestBody[Nothing] = + SerializableBody((b: B) => StringBody(encoder(b).noSpaces, Utf8), body) + + implicit final class CirceRequestTOps[U[_], T, +S](val req: RequestT[U, T, S]) + extends AnyVal { + def jsonBody[B: Encoder](body: B): RequestT[U, T, S] = + req + .setContentTypeIfMissing(ApplicationJsonContentType) + .copy(body = serializeBody(body)) + } + + def asJson[B: Decoder]: ResponseAs[Either[io.circe.Error, B], Nothing] = + asString(Utf8).map(decode[B]) + +} diff --git a/circe/src/test/scala/com/softwaremill/sttp/CirceTests.scala b/circe/src/test/scala/com/softwaremill/sttp/CirceTests.scala new file mode 100644 index 0000000..abd20ef --- /dev/null +++ b/circe/src/test/scala/com/softwaremill/sttp/CirceTests.scala @@ -0,0 +1,113 @@ +package com.softwaremill.sttp + +import com.softwaremill.sttp.model._ +import io.circe._ +import org.scalatest._ + +import scala.language.higherKinds + +class CirceTests extends FlatSpec with Matchers with EitherValues { + import circe._ + + "The circe module" should "encode arbitrary bodies given an encoder" in { + val body = Outer(Inner(42, true, "horses"), "cats") + val expected = """{"foo":{"a":42,"b":true,"c":"horses"},"bar":"cats"}""" + + val req = sttp.jsonBody(body) + + extractBody(req) shouldBe expected + } + + it should "decode arbitrary bodies given a decoder" in { + val body = """{"foo":{"a":42,"b":true,"c":"horses"},"bar":"cats"}""" + val expected = Outer(Inner(42, true, "horses"), "cats") + + val responseAs = asJson[Outer] + + runJsonResponseAs(responseAs)(body).right.value shouldBe expected + } + + it should "fail to decode invalid json" in { + val body = """not valid json""" + + val responseAs = asJson[Outer] + + runJsonResponseAs(responseAs)(body).left.value shouldBe an[io.circe.Error] + } + + it should "should encode and decode back to the same thing" in { + val outer = Outer(Inner(42, true, "horses"), "cats") + + val encoded = extractBody(sttp.jsonBody(outer)) + val decoded = runJsonResponseAs(asJson[Outer])(encoded) + + decoded.right.value shouldBe outer + } + + it should "set the content type" in { + val body = Outer(Inner(42, true, "horses"), "cats") + val req = sttp.jsonBody(body) + + val ct = req.headers.toMap.get("Content-Type") + + ct shouldBe Some("application/json") + } + + it should "only set the content type if it was not set earlier" in { + val body = Outer(Inner(42, true, "horses"), "cats") + val req = sttp.contentType("horses/cats").jsonBody(body) + + val ct = req.headers.toMap.get("Content-Type") + + ct shouldBe Some("horses/cats") + } + + case class Inner(a: Int, b: Boolean, c: String) + + object Inner { + implicit val encoder: Encoder[Inner] = + Encoder.forProduct3("a", "b", "c")(i => (i.a, i.b, i.c)) + implicit val decoder: Decoder[Inner] = + Decoder.forProduct3("a", "b", "c")(Inner.apply) + } + + case class Outer(foo: Inner, bar: String) + + object Outer { + implicit val encoder: Encoder[Outer] = + Encoder.forProduct2("foo", "bar")(o => (o.foo, o.bar)) + implicit val decoder: Decoder[Outer] = + Decoder.forProduct2("foo", "bar")(Outer.apply) + } + + def extractBody[A[_], B, C](request: RequestT[A, B, C]): String = + request.body match { + case SerializableBody(serializer, body) => + serializer(body) match { + case StringBody(body, "utf-8") => + body + case StringBody(_, encoding) => + fail( + s"Request body serializes to StringBody with wrong encoding: $encoding") + case _ => + fail("Request body does not serialize to StringBody") + } + case _ => + fail("Request body is not SerializableBody") + } + + def runJsonResponseAs[A](responseAs: ResponseAs[A, Nothing]): String => A = + responseAs match { + case responseAs: MappedResponseAs[_, A, Nothing] => + responseAs.raw match { + case ResponseAsString("utf-8") => + responseAs.g + case ResponseAsString(encoding) => + fail( + s"MappedResponseAs wraps a ResponseAsString with wrong encoding: $encoding") + case _ => + fail("MappedResponseAs does not wrap a ResponseAsString") + } + case _ => fail("ResponseAs is not a MappedResponseAs") + } +} diff --git a/core/src/main/scala/com/softwaremill/sttp/RequestT.scala b/core/src/main/scala/com/softwaremill/sttp/RequestT.scala index ff80dd5..4e38662 100644 --- a/core/src/main/scala/com/softwaremill/sttp/RequestT.scala +++ b/core/src/main/scala/com/softwaremill/sttp/RequestT.scala @@ -234,9 +234,9 @@ case class RequestT[U[_], T, +S]( handler.send(this.asInstanceOf[RequestT[Id, T, S]]) } - private def hasContentType: Boolean = + private[sttp] def hasContentType: Boolean = headers.exists(_._1.equalsIgnoreCase(ContentTypeHeader)) - private def setContentTypeIfMissing(ct: String): RequestT[U, T, S] = + private[sttp] def setContentTypeIfMissing(ct: String): RequestT[U, T, S] = if (hasContentType) this else contentType(ct) private def hasContentLength: Boolean = diff --git a/core/src/main/scala/com/softwaremill/sttp/model/ResponseAs.scala b/core/src/main/scala/com/softwaremill/sttp/model/ResponseAs.scala index 24b9b2b..5a008ed 100644 --- a/core/src/main/scala/com/softwaremill/sttp/model/ResponseAs.scala +++ b/core/src/main/scala/com/softwaremill/sttp/model/ResponseAs.scala @@ -2,7 +2,6 @@ package com.softwaremill.sttp.model import java.io.{File, FileOutputStream, IOException, InputStream} import java.net.URLDecoder -import java.nio.file.Path import com.softwaremill.sttp.{MonadError, transfer} |