From 060cd745cdaa72ef433720fa09845c1132a516d8 Mon Sep 17 00:00:00 2001 From: Bjørn Madsen Date: Sun, 6 Aug 2017 00:28:33 +0200 Subject: Add circe support module --- .../scala/com/softwaremill/sttp/circe/circe.scala | 32 ++++++ .../scala/com/softwaremill/sttp/CirceTests.scala | 113 +++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 circe/src/main/scala/com/softwaremill/sttp/circe/circe.scala create mode 100644 circe/src/test/scala/com/softwaremill/sttp/CirceTests.scala (limited to 'circe') 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") + } +} -- cgit v1.2.3