aboutsummaryrefslogtreecommitdiff
path: root/circe
diff options
context:
space:
mode:
authorBjørn Madsen <bm@aeons.dk>2017-08-06 00:28:33 +0200
committerBjørn Madsen <bm@aeons.dk>2017-08-06 00:28:33 +0200
commit060cd745cdaa72ef433720fa09845c1132a516d8 (patch)
tree391629ed92d1973b048c84159df375dac391b26f /circe
parentff76a1737bb88c2664927db309d196e677ba3e98 (diff)
downloadsttp-060cd745cdaa72ef433720fa09845c1132a516d8.tar.gz
sttp-060cd745cdaa72ef433720fa09845c1132a516d8.tar.bz2
sttp-060cd745cdaa72ef433720fa09845c1132a516d8.zip
Add circe support module
Diffstat (limited to 'circe')
-rw-r--r--circe/src/main/scala/com/softwaremill/sttp/circe/circe.scala32
-rw-r--r--circe/src/test/scala/com/softwaremill/sttp/CirceTests.scala113
2 files changed, 145 insertions, 0 deletions
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")
+ }
+}