From a3abbd502783df6df4de98c5dbf0c8eff81cb511 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 18 Oct 2017 16:25:07 +0200 Subject: json4s support --- .../com/softwaremill/sttp/circe/package.scala | 15 +++ .../scala/com/softwaremill/sttp/CirceTests.scala | 105 +++++++++++++++++++++ .../com/softwaremill/sttp/json4s/package.scala | 14 +++ .../scala/com/softwaremill/sttp/Json4sTests.scala | 75 +++++++++++++++ 4 files changed, 209 insertions(+) create mode 100644 json/circe/src/main/scala/com/softwaremill/sttp/circe/package.scala create mode 100644 json/circe/src/test/scala/com/softwaremill/sttp/CirceTests.scala create mode 100644 json/json4s/src/main/scala/com/softwaremill/sttp/json4s/package.scala create mode 100644 json/json4s/src/test/scala/com/softwaremill/sttp/Json4sTests.scala (limited to 'json') diff --git a/json/circe/src/main/scala/com/softwaremill/sttp/circe/package.scala b/json/circe/src/main/scala/com/softwaremill/sttp/circe/package.scala new file mode 100644 index 0000000..ddc9583 --- /dev/null +++ b/json/circe/src/main/scala/com/softwaremill/sttp/circe/package.scala @@ -0,0 +1,15 @@ +package com.softwaremill.sttp + +import io.circe.parser._ +import io.circe.{Decoder, Encoder} + +package object circe { + + implicit def circeBodySerializer[B]( + implicit encoder: Encoder[B]): BodySerializer[B] = + b => StringBody(encoder(b).noSpaces, Utf8, Some(ApplicationJsonContentType)) + + def asJson[B: Decoder]: ResponseAs[Either[io.circe.Error, B], Nothing] = + asString(Utf8).map(decode[B]) + +} diff --git a/json/circe/src/test/scala/com/softwaremill/sttp/CirceTests.scala b/json/circe/src/test/scala/com/softwaremill/sttp/CirceTests.scala new file mode 100644 index 0000000..1e7dddc --- /dev/null +++ b/json/circe/src/test/scala/com/softwaremill/sttp/CirceTests.scala @@ -0,0 +1,105 @@ +package com.softwaremill.sttp + +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.body(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 "encode and decode back to the same thing" in { + val outer = Outer(Inner(42, true, "horses"), "cats") + + val encoded = extractBody(sttp.body(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.body(body) + + val ct = req.headers.toMap.get("Content-Type") + + ct shouldBe Some(contentTypeWithEncoding(ApplicationJsonContentType, Utf8)) + } + + 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").body(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 StringBody(body, "utf-8", Some(ApplicationJsonContentType)) => + body + case wrongBody => + fail( + s"Request body does not serialize to correct StringBody: $wrongBody") + } + + 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/json/json4s/src/main/scala/com/softwaremill/sttp/json4s/package.scala b/json/json4s/src/main/scala/com/softwaremill/sttp/json4s/package.scala new file mode 100644 index 0000000..4c7aa36 --- /dev/null +++ b/json/json4s/src/main/scala/com/softwaremill/sttp/json4s/package.scala @@ -0,0 +1,14 @@ +package com.softwaremill.sttp + +import org.json4s._ +import org.json4s.native.Serialization.{read, write} + +package object json4s { + implicit def json4sBodySerializer[B <: AnyRef]( + implicit formats: Formats = DefaultFormats): BodySerializer[B] = + b => StringBody(write(b), Utf8, Some(ApplicationJsonContentType)) + + def asJson[B: Manifest]( + implicit formats: Formats = DefaultFormats): ResponseAs[B, Nothing] = + asString(Utf8).map(s => read[B](s)) +} diff --git a/json/json4s/src/test/scala/com/softwaremill/sttp/Json4sTests.scala b/json/json4s/src/test/scala/com/softwaremill/sttp/Json4sTests.scala new file mode 100644 index 0000000..bb4a774 --- /dev/null +++ b/json/json4s/src/test/scala/com/softwaremill/sttp/Json4sTests.scala @@ -0,0 +1,75 @@ +package com.softwaremill.sttp + +import org.json4s.ParserUtil.ParseException +import org.scalatest._ + +import scala.language.higherKinds + +class Json4sTests extends FlatSpec with Matchers with EitherValues { + import json4s._ + import Json4sTests._ + + "The json4s module" should "encode arbitrary json bodies" in { + val body = Outer(Inner(42, true, "horses"), "cats") + val expected = """{"foo":{"a":42,"b":true,"c":"horses"},"bar":"cats"}""" + + val req = sttp.body(body) + + extractBody(req) shouldBe expected + } + + it should "decode arbitrary bodies" 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) shouldBe expected + } + + it should "fail to decode invalid json" in { + val body = """not valid json""" + + val responseAs = asJson[Outer] + + an[ParseException] should be thrownBy runJsonResponseAs(responseAs)(body) + } + + it should "set the content type" in { + val body = Outer(Inner(42, true, "horses"), "cats") + val req = sttp.body(body) + + val ct = req.headers.toMap.get("Content-Type") + + ct shouldBe Some(contentTypeWithEncoding(ApplicationJsonContentType, Utf8)) + } + + def extractBody[A[_], B, C](request: RequestT[A, B, C]): String = + request.body match { + case StringBody(body, "utf-8", Some(ApplicationJsonContentType)) => + body + case wrongBody => + fail( + s"Request body does not serialize to correct StringBody: $wrongBody") + } + + 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") + } +} + +object Json4sTests { + case class Inner(a: Int, b: Boolean, c: String) + case class Outer(foo: Inner, bar: String) +} -- cgit v1.2.3