diff --git a/core/src/main/scala/sttp/client4/SttpApi.scala b/core/src/main/scala/sttp/client4/SttpApi.scala index 8e06ce9070..b6f588e1ac 100644 --- a/core/src/main/scala/sttp/client4/SttpApi.scala +++ b/core/src/main/scala/sttp/client4/SttpApi.scala @@ -200,8 +200,8 @@ trait SttpApi extends SttpExtensions with UriInterpolator { ) /** Content type will be set to `application/octet-stream`, can be overridden later using the `contentType` method. */ - def multipart[B: BodySerializer](name: String, b: B): Part[BasicBodyPart] = - Part(name, implicitly[BodySerializer[B]].apply(b), contentType = Some(MediaType.ApplicationXWwwFormUrlencoded)) + def multipart(name: String, b: BasicBodyPart): Part[BasicBodyPart] = + Part(name, b, contentType = Some(MediaType.ApplicationXWwwFormUrlencoded)) // stream response specifications diff --git a/core/src/main/scala/sttp/client4/requestBuilder.scala b/core/src/main/scala/sttp/client4/requestBuilder.scala index 36d7f0d22d..143f70e517 100644 --- a/core/src/main/scala/sttp/client4/requestBuilder.scala +++ b/core/src/main/scala/sttp/client4/requestBuilder.scala @@ -155,79 +155,87 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R] private[client4] def setContentLengthIfMissing(l: => Long): PR = if (hasContentLength) this else contentLength(l) - /** Uses the `utf-8` encoding. + /** Sets the body of this request to the given string, using the UTF-8 encoding. * - * If content type is not yet specified, will be set to `text/plain` with `utf-8` encoding. + * If content type is not yet specified, will be set to `text/plain` with UTF-8 encoding. * - * If content length is not yet specified, will be set to the number of bytes in the string using the `utf-8` - * encoding. + * If content length is not yet specified, will be set to the number of bytes in the string using the UTF-8 encoding. */ def body(b: String): PR = body(b, Utf8) - /** If content type is not yet specified, will be set to `text/plain` with the given encoding. + /** Sets the body of this request to the given string, using the given encoding. + * + * If content type is not yet specified, will be set to `text/plain` with the given encoding. * * If content length is not yet specified, will be set to the number of bytes in the string using the given encoding. */ def body(b: String, encoding: String): PR = - withBody(StringBody(b, encoding)).setContentLengthIfMissing(b.getBytes(encoding).length.toLong) + body(StringBody(b, encoding)).setContentLengthIfMissing(b.getBytes(encoding).length.toLong) - /** If content type is not yet specified, will be set to `application/octet-stream`. + /** Sets the body of this request to the given byte array. + * + * If content type is not yet specified, will be set to `application/octet-stream`. * * If content length is not yet specified, will be set to the length of the given array. */ - def body(b: Array[Byte]): PR = withBody(ByteArrayBody(b)).setContentLengthIfMissing(b.length.toLong) + def body(b: Array[Byte]): PR = body(ByteArrayBody(b)).setContentLengthIfMissing(b.length.toLong) - /** If content type is not yet specified, will be set to `application/octet-stream`. */ - def body(b: ByteBuffer): PR = withBody(ByteBufferBody(b)) + /** Sets the body of this request to the given byte buffer. + * + * If content type is not yet specified, will be set to `application/octet-stream`. + */ + def body(b: ByteBuffer): PR = body(ByteBufferBody(b)) - /** If content type is not yet specified, will be set to `application/octet-stream`. + /** Sets the body of this request to the given input stream. + * + * If content type is not yet specified, will be set to `application/octet-stream`. */ - def body(b: InputStream): PR = withBody(InputStreamBody(b)) + def body(b: InputStream): PR = body(InputStreamBody(b)) /** If content type is not yet specified, will be set to `application/octet-stream`. * * If content length is not yet specified, will be set to the length of the given file. */ - private[client4] def body(f: SttpFile): PR = withBody(FileBody(f)).setContentLengthIfMissing(f.size) + private[client4] def body(f: SttpFile): PR = body(FileBody(f)).setContentLengthIfMissing(f.size) - /** Encodes the given parameters as form data using `utf-8`. If content type is not yet specified, will be set to - * `application/x-www-form-urlencoded`. + /** Sets the body of this request to the given form-data parameters. The parameters are encoded using UTF-8. + * + * If content type is not yet specified, will be set to `application/x-www-form-urlencoded`. * * If content length is not yet specified, will be set to the length of the number of bytes in the url-encoded * parameter string. */ def body(fs: Map[String, String]): PR = formDataBody(fs.toList, Utf8) - /** Encodes the given parameters as form data. If content type is not yet specified, will be set to - * `application/x-www-form-urlencoded`. + /** Sets the body of this request to the given form-data parameters. The parameters are encoded using the given + * encoding. + * + * If content type is not yet specified, will be set to `application/x-www-form-urlencoded`. * * If content length is not yet specified, will be set to the length of the number of bytes in the url-encoded * parameter string. */ def body(fs: Map[String, String], encoding: String): PR = formDataBody(fs.toList, encoding) - /** Encodes the given parameters as form data using `utf-8`. If content type is not yet specified, will be set to - * `application/x-www-form-urlencoded`. + /** Sets the body of this request to the given form-data parameters. The parameters are encoded using UTF-8. + * + * If content type is not yet specified, will be set to `application/x-www-form-urlencoded`. * * If content length is not yet specified, will be set to the length of the number of bytes in the url-encoded * parameter string. */ def body(fs: (String, String)*): PR = formDataBody(fs.toList, Utf8) - /** Encodes the given parameters as form data. If content type is not yet specified, will be set to - * `application/x-www-form-urlencoded`. + /** Sets the body of this request to the given form-data parameters. The parameters are encoded using the given + * encoding. + * + * If content type is not yet specified, will be set to `application/x-www-form-urlencoded`. * * If content length is not yet specified, will be set to the length of the number of bytes in the url-encoded * parameter string. */ def body(fs: Seq[(String, String)], encoding: String): PR = formDataBody(fs, encoding) - def multipartBody(ps: Seq[Part[BasicBodyPart]]): PR = copyWithBody(BasicMultipartBody(ps)) - - def multipartBody(p1: Part[BasicBodyPart], ps: Part[BasicBodyPart]*): PR = copyWithBody( - BasicMultipartBody(p1 :: ps.toList) - ) - private def formDataBody(fs: Seq[(String, String)], encoding: String): PR = { val b = BasicBody.paramsToStringBody(fs, encoding) copyWithBody(b) @@ -235,7 +243,20 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R] .setContentLengthIfMissing(b.s.getBytes(encoding).length.toLong) } - def withBody(body: BasicBody): PR = { + /** Sets the body of this request to the given multipart form parts. */ + def multipartBody(ps: Seq[Part[BasicBodyPart]]): PR = copyWithBody(BasicMultipartBody(ps)) + + /** Sets the body of this request to the given multipart form parts. */ + def multipartBody(p1: Part[BasicBodyPart], ps: Part[BasicBodyPart]*): PR = copyWithBody( + BasicMultipartBody(p1 :: ps.toList) + ) + + /** Sets the body of this request to the given [[BasicBody]] implementation. + * + * If content type is not yet specified, it will be set to the default content type of the body, including the + * encoding in case of a string body. + */ + def body(body: BasicBody): PR = { val defaultCt = body match { case StringBody(_, encoding, ct) => ct.copy(charset = Some(encoding)) @@ -254,8 +275,8 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R] def followRedirects(fr: Boolean): PR = withOptions(options.copy(followRedirects = fr)) def maxRedirects(n: Int): PR = - if (n <= 0) withOptions(options.copy(followRedirects = false)) - else withOptions(options.copy(followRedirects = true, maxRedirects = n)) + if (n <= 0) withOptions(options.copy(followRedirects = false)) + else withOptions(options.copy(followRedirects = true, maxRedirects = n)) /** When a POST or PUT request is redirected, should the redirect be a POST/PUT as well (with the original body), or * should the request be converted to a GET without a body. diff --git a/core/src/main/scala/sttp/client4/wrappers/FollowRedirectsBackend.scala b/core/src/main/scala/sttp/client4/wrappers/FollowRedirectsBackend.scala index 69215a4d1f..b99bac11a6 100644 --- a/core/src/main/scala/sttp/client4/wrappers/FollowRedirectsBackend.scala +++ b/core/src/main/scala/sttp/client4/wrappers/FollowRedirectsBackend.scala @@ -77,7 +77,7 @@ abstract class FollowRedirectsBackend[F[_], P] private ( if (applicable && (r.options.redirectToGet || alwaysChanged) && !neverChanged) { // when transforming POST or PUT into a get, content is dropped, also filter out content-related request headers r.method(Method.GET, r.uri) - .withBody(NoBody) + .body(NoBody) .withHeaders(r.headers.filterNot(header => config.contentHeaders.contains(header.name.toLowerCase()))) } else r } diff --git a/core/src/main/scalajs/sttp/client4/PartialRequestExtensions.scala b/core/src/main/scalajs/sttp/client4/PartialRequestExtensions.scala index c8167fce82..0c74e80937 100644 --- a/core/src/main/scalajs/sttp/client4/PartialRequestExtensions.scala +++ b/core/src/main/scalajs/sttp/client4/PartialRequestExtensions.scala @@ -10,10 +10,4 @@ trait PartialRequestExtensions[+R <: PartialRequestBuilder[R, _]] { self: R => * If content length is not yet specified, will be set to the length of the given file. */ def body(file: File): R = body(SttpFile.fromDomFile(file)) - - // this method needs to be in the extensions, so that it has lowest priority when considering overloading options - /** If content type is not yet specified, will be set to `application/octet-stream`. - */ - def body[B: BodySerializer](b: B): R = - withBody(implicitly[BodySerializer[B]].apply(b)) } diff --git a/core/src/main/scalajvm/sttp/client4/PartialRequestExtensions.scala b/core/src/main/scalajvm/sttp/client4/PartialRequestExtensions.scala index dc3777e722..1281ef3570 100644 --- a/core/src/main/scalajvm/sttp/client4/PartialRequestExtensions.scala +++ b/core/src/main/scalajvm/sttp/client4/PartialRequestExtensions.scala @@ -4,13 +4,12 @@ import java.io.File import java.nio.file.Path import sttp.client4.internal.SttpFile -import sttp.client4.BodySerializer trait PartialRequestExtensions[+R <: PartialRequestBuilder[R, _]] { self: R => /** If content type is not yet specified, will be set to `application/octet-stream`. * - * If content length is noBodySerializert yet specified, will be set to the length of the given file. + * If content length is not yet specified, will be set to the length of the given file. */ def body(file: File): R = body(SttpFile.fromFile(file)) @@ -19,10 +18,4 @@ trait PartialRequestExtensions[+R <: PartialRequestBuilder[R, _]] { self: R => * If content length is not yet specified, will be set to the length of the given file. */ def body(path: Path): R = body(SttpFile.fromPath(path)) - - // this method needs to be in the extensions, so that it has lowest priority when considering overloading options - /** If content type is not yet specified, will be set to `application/octet-stream`. - */ - def body[B: BodySerializer](b: B): R = - withBody(implicitly[BodySerializer[B]].apply(b)) } diff --git a/core/src/main/scalanative/sttp/client4/PartialRequestExtensions.scala b/core/src/main/scalanative/sttp/client4/PartialRequestExtensions.scala index b0df1ea12d..93f1b21b99 100644 --- a/core/src/main/scalanative/sttp/client4/PartialRequestExtensions.scala +++ b/core/src/main/scalanative/sttp/client4/PartialRequestExtensions.scala @@ -18,10 +18,4 @@ trait PartialRequestExtensions[+R <: PartialRequestBuilder[R, _]] { self: R => * If content length is not yet specified, will be set to the length of the given file. */ def body(path: Path): R = body(SttpFile.fromPath(path)) - - // this method needs to be in the extensions, so that it has lowest priority when considering overloading options - /** If content type is not yet specified, will be set to `application/octet-stream`. - */ - def body[B: BodySerializer](b: B): R = - withBody(implicitly[BodySerializer[B]].apply(b)) } diff --git a/docs/json.md b/docs/json.md index 6e297dd425..dd83ee4aed 100644 --- a/docs/json.md +++ b/docs/json.md @@ -2,19 +2,21 @@ Adding support for JSON (or other format) bodies in requests/responses is a matter of providing a [body serializer](requests/body.md) and/or a [response body specification](responses/body.md). Both are quite straightforward to implement, so integrating with your favorite JSON library shouldn't be a problem. However, there are some integrations available out-of-the-box. -Each integration is available as an import, which brings the implicit `BodySerializer`s and `asJson` methods into scope. Alternatively, these values are grouped intro traits (e.g. `sttp.client4.circe.SttpCirceApi`), which can be extended to group multiple integrations in one object, and thus reduce the number of necessary imports. +Each integration is available as an import, which brings `asJson` methods into scope. Alternatively, these values are grouped intro traits (e.g. `sttp.client4.circe.SttpCirceApi`), which can be extended to group multiple integrations in one object, and thus reduce the number of necessary imports. The following variants of `asJson` methods are available: -* regular - deserializes the body to json, only if the response is successful (2xx) -* `always` - deserializes the body to json regardless of the status code -* `either` - uses different deserializers for error and successful (2xx) responses +* `asJson(b: B)` - serializes the body so that it can be used as a request's body, e.g. using `basicRequest.body(asJson(myValue))` +* `asJson[B]` - specifies that the body should be deserialized to json, but only if the response is successful (2xx); shoud be used to specify how a response should be handled, e.g. `basicRequest.response(asJson[T])` +* `asJsonAlways[B]` - specifies that the body should be deserialized to json, regardless of the status code +* `asJsonEither[E, B]` - specifies that the body should be deserialized to json, using different deserializers for error and successful (2xx) responses The type signatures vary depending on the underlying library (required implicits and error representation differs), but they obey the following pattern: ```scala mdoc:compile-only import sttp.client4._ +def asJson[B](b: B): StringBody = ??? def asJson[B]: ResponseAs[Either[ResponseException[String, Exception], B]] = ??? def asJsonAlways[B]: ResponseAs[Either[DeserializationException[Exception], B]] = ??? def asJsonEither[E, B]: ResponseAs[Either[ResponseException[E, Exception], B]] = ??? @@ -54,7 +56,7 @@ val requestPayload = RequestPayload("some data") val response: Response[Either[ResponseException[String, io.circe.Error], ResponsePayload]] = basicRequest .post(uri"...") - .body(requestPayload) + .body(asJson(requestPayload)) .response(asJson[ResponsePayload]) .send(backend) ``` @@ -90,7 +92,7 @@ implicit val formats = org.json4s.DefaultFormats val response: Response[Either[ResponseException[String, Exception], ResponsePayload]] = basicRequest .post(uri"...") - .body(requestPayload) + .body(asJson(requestPayload)) .response(asJson[ResponsePayload]) .send(backend) ``` @@ -122,7 +124,7 @@ val requestPayload = RequestPayload("some data") val response: Response[Either[ResponseException[String, Exception], ResponsePayload]] = basicRequest .post(uri"...") - .body(requestPayload) + .body(asJson(requestPayload)) .response(asJson[ResponsePayload]) .send(backend) ``` @@ -179,7 +181,7 @@ val requestPayload = RequestPayload("some data") val response: Response[Either[ResponseException[String, String], ResponsePayload]] = basicRequest .post(uri"...") - .body(requestPayload) + .body(asJson(requestPayload)) .response(asJson[ResponsePayload]) .send(backend) ``` @@ -219,7 +221,7 @@ val requestPayload = RequestPayload("some data") val response: Response[Either[ResponseException[String, Exception], ResponsePayload]] = basicRequest .post(uri"...") - .body(requestPayload) + .body(asJson(requestPayload)) .response(asJson[ResponsePayload]) .send(backend) ``` @@ -256,7 +258,7 @@ val requestPayload = RequestPayload("some data") val response: Response[Either[ResponseException[String, Exception], ResponsePayload]] = basicRequest .post(uri"...") - .body(requestPayload) + .body(asJson(requestPayload)) .response(asJson[ResponsePayload]) .send(backend) ``` diff --git a/docs/quickstart.md b/docs/quickstart.md index 29c3a6c1e2..47e371d5dd 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -93,7 +93,7 @@ implicit val responseRW: ReadWriter[HttpBinResponse] = macroRW[HttpBinResponse] val request = basicRequest .post(uri"https://httpbin.org/post") - .body(MyRequest("test", 42)) + .body(asJson(MyRequest("test", 42))) .response(asJson[HttpBinResponse]) val response = request.send(backend) diff --git a/docs/requests/body.md b/docs/requests/body.md index bbfe39569c..364be57495 100644 --- a/docs/requests/body.md +++ b/docs/requests/body.md @@ -83,15 +83,11 @@ basicRequest.body("k1" -> "v1", "k2" -> "v2") basicRequest.body(Seq("k1" -> "v1", "k2" -> "v2"), "utf-8") ``` -## Custom body serializers +## Custom serializers -It is also possible to set custom types as request bodies, as long as there's an implicit `BodySerializer[B]` value in scope, which is simply an alias for a function: - -```scala -type BodySerializer[B] = B => BasicRequestBody -``` - -A `BasicRequestBody` is a wrapper for one of the supported request body types: a `String`/byte array or an input stream. +It is also possible to write custom serializers, which return arbitrary body representations. These should be +methods/functions which return instances of `BasicBody`, which is a wrapper for one of the supported request body +types: a `String`, byte array, an input stream, etc. For example, here's how to write a custom serializer for a case class, with serializer-specific default content type: @@ -101,12 +97,12 @@ import sttp.model.MediaType case class Person(name: String, surname: String, age: Int) // for this example, assuming names/surnames can't contain commas -implicit val personSerializer: BodySerializer[Person] = { p: Person => +def serializePerson(p: Person): BasicBody = { val serialized = s"${p.name},${p.surname},${p.age}" StringBody(serialized, "UTF-8", MediaType.TextCsv) } -basicRequest.body(Person("mary", "smith", 67)) +basicRequest.body(serializePerson(Person("mary", "smith", 67))) ``` -See the implementations of the `BasicRequestBody` trait for more options. +See the implementations of the `BasicBody` trait for more options. diff --git a/docs/xml.md b/docs/xml.md index c5286377b9..96bea7e2e1 100644 --- a/docs/xml.md +++ b/docs/xml.md @@ -12,7 +12,7 @@ After code generation, create the `SttpScalaxbApi` trait (or trait with another import generated.defaultScope // import may differ depending on location of generated code import scalaxb.`package`.{fromXML, toXML} // import may differ depending on location of generated code import scalaxb.{CanWriteXML, XMLFormat} // import may differ depending on location of generated code -import sttp.client4.{BodySerializer, ResponseAs, ResponseException, StringBody, asString} +import sttp.client4.{ResponseAs, ResponseException, StringBody, asString} import sttp.model.MediaType import scala.xml.{NodeSeq, XML} @@ -20,7 +20,7 @@ import scala.xml.{NodeSeq, XML} trait SttpScalaxbApi { case class XmlElementLabel(label: String) - implicit def scalaxbBodySerializer[B](implicit format: CanWriteXML[B], label: XmlElementLabel): BodySerializer[B] = { (b: B) => + def asXml[B](b: B)(implicit format: CanWriteXML[B], label: XmlElementLabel): StringBody = { val nodeSeq: NodeSeq = toXML[B](obj = b, elementLabel = label.label, scope = defaultScope) StringBody(nodeSeq.toString(), "utf-8", MediaType.ApplicationXml) } @@ -38,11 +38,11 @@ trait SttpScalaxbApi { .showAs("either(as string, as xml)") } ``` -This would add `BodySerializer` needed for serialization and `asXml` method needed for deserialization. Please notice, that `fromXML`, `toXML`, `CanWriteXML`, `XMLFormat` and `defaultScope` are members of code generated by scalaxb. +This would add `asXml` methods needed for serialization and deserialization. Please notice, that `fromXML`, `toXML`, `CanWriteXML`, `XMLFormat` and `defaultScope` are members of code generated by scalaxb. + +Next to this trait, you might want to introduce `sttpScalaxb` package object to simplify imports. -Next to this trait, you might want to introduce `sttpScalaxb` -package object to simplify imports. ```scala package object sttpScalaxb extends SttpScalaxbApi ``` @@ -50,6 +50,7 @@ package object sttpScalaxb extends SttpScalaxbApi From now on, XML serialization/deserialization would work for all classes generated from `.xsd` file as long as `XMLFormat` for the type in the question and `XmlElementLabel` for the top XML node would be implicitly provided in the scope. Usage example: + ```scala val backend: SyncBackend = DefaultSyncBackend() val requestPayload = Outer(Inner(42, b = true, "horses"), "cats") // `Outer` and `Inner` classes are generated by scalaxb from xsd file @@ -61,7 +62,7 @@ import generated.Generated_OuterFormat // imports member of code generated by sc val response: Response[Either[ResponseException[String, Exception], Outer]] = basicRequest .post(uri"...") - .body(requestPayload) + .body(asXml(requestPayload)) .response(asXml[Outer]) .send(backend) ``` \ No newline at end of file diff --git a/examples-ce2/src/main/scala/sttp/client4/examples/PostSerializeJsonMonixHttpClientCirce.scala b/examples-ce2/src/main/scala/sttp/client4/examples/PostSerializeJsonMonixHttpClientCirce.scala index e747d0f9af..2444118a25 100644 --- a/examples-ce2/src/main/scala/sttp/client4/examples/PostSerializeJsonMonixHttpClientCirce.scala +++ b/examples-ce2/src/main/scala/sttp/client4/examples/PostSerializeJsonMonixHttpClientCirce.scala @@ -11,7 +11,7 @@ object PostSerializeJsonMonixHttpClientCirce extends App { val postTask = HttpClientMonixBackend().flatMap { backend => val r = basicRequest - .body(Info(91, "abc")) + .body(asJson(Info(91, "abc"))) .post(uri"https://httpbin.org/post") r.send(backend) diff --git a/json/circe/src/main/scala/sttp/client4/circe/SttpCirceApi.scala b/json/circe/src/main/scala/sttp/client4/circe/SttpCirceApi.scala index ca2cdae638..a43016505a 100644 --- a/json/circe/src/main/scala/sttp/client4/circe/SttpCirceApi.scala +++ b/json/circe/src/main/scala/sttp/client4/circe/SttpCirceApi.scala @@ -9,11 +9,12 @@ import sttp.client4.json._ trait SttpCirceApi { - implicit def circeBodySerializer[B](implicit + /** Serialize the given value as JSON, to be used as a request's body using [[sttp.client4.Request.body]]. */ + def asJson[B](b: B)(implicit encoder: Encoder[B], printer: Printer = Printer.noSpaces - ): BodySerializer[B] = - b => StringBody(encoder(b).printWith(printer), Utf8, MediaType.ApplicationJson) + ): StringBody = + StringBody(encoder(b).printWith(printer), Utf8, MediaType.ApplicationJson) /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful diff --git a/json/circe/src/test/scala/sttp/client4/circe/CirceTests.scala b/json/circe/src/test/scala/sttp/client4/circe/CirceTests.scala index 3b61a690dc..8ad141755a 100644 --- a/json/circe/src/test/scala/sttp/client4/circe/CirceTests.scala +++ b/json/circe/src/test/scala/sttp/client4/circe/CirceTests.scala @@ -14,7 +14,7 @@ class CirceTests extends AnyFlatSpec with Matchers with EitherValues { val body = Outer(Inner(42, true, "horses"), "cats") val expected = """{"foo":{"a":42,"b":true,"c":"horses"},"bar":"cats"}""" - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) extractBody(req) shouldBe expected } @@ -23,7 +23,7 @@ class CirceTests extends AnyFlatSpec with Matchers with EitherValues { val body = Outer(Inner(42, true, "horses"), "cats") implicit val printer = Printer.spaces4 - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) extractBody(req) should include("\n \"foo") } @@ -77,7 +77,7 @@ class CirceTests extends AnyFlatSpec with Matchers with EitherValues { it should "encode and decode back to the same thing" in { val outer = Outer(Inner(42, true, "horses"), "cats") - val encoded = extractBody(basicRequest.body(outer)) + val encoded = extractBody(basicRequest.body(asJson(outer))) val decoded = runJsonResponseAs(asJson[Outer])(encoded) decoded.right.value shouldBe outer @@ -85,7 +85,7 @@ class CirceTests extends AnyFlatSpec with Matchers with EitherValues { it should "set the content type" in { val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") @@ -94,20 +94,20 @@ class CirceTests extends AnyFlatSpec with Matchers with EitherValues { it should "only set the content type if it was not set earlier" in { val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.contentType("horses/cats").body(body) + val req = basicRequest.contentType("horses/cats").body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") ct shouldBe Some("horses/cats") } - it should "serialize from JsonObject using implicit circeBodySerializer" in { + it should "serialize from JsonObject" in { import io.circe.syntax.EncoderOps import io.circe.JsonObject import sttp.model.Uri val jObject: JsonObject = JsonObject(("location", "hometown".asJson), ("bio", "Scala programmer".asJson)) - val request: Request[Either[String, String]] = basicRequest.get(Uri("http://example.org")).body(jObject) + val request: Request[Either[String, String]] = basicRequest.get(Uri("http://example.org")).body(asJson(jObject)) val actualBody: String = request.body.show val actualContentType: Option[String] = request.contentType diff --git a/json/json4s/src/main/scala/sttp/client4/json4s/SttpJson4sApi.scala b/json/json4s/src/main/scala/sttp/client4/json4s/SttpJson4sApi.scala index 4c289cd792..4e037e4b11 100644 --- a/json/json4s/src/main/scala/sttp/client4/json4s/SttpJson4sApi.scala +++ b/json/json4s/src/main/scala/sttp/client4/json4s/SttpJson4sApi.scala @@ -7,11 +7,13 @@ import sttp.client4.json._ import sttp.model._ trait SttpJson4sApi { - implicit def json4sBodySerializer[B <: AnyRef](implicit + + /** Serialize the given value as JSON, to be used as a request's body using [[sttp.client4.Request.body]]. */ + def asJson[B <: AnyRef](b: B)(implicit formats: Formats, serialization: Serialization - ): BodySerializer[B] = - b => StringBody(serialization.write(b), Utf8, MediaType.ApplicationJson) + ): StringBody = + StringBody(serialization.write(b), Utf8, MediaType.ApplicationJson) /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful diff --git a/json/json4s/src/test/scala/sttp/client4/Json4sTests.scala b/json/json4s/src/test/scala/sttp/client4/Json4sTests.scala index cf8d34c851..08cc91f3fc 100644 --- a/json/json4s/src/test/scala/sttp/client4/Json4sTests.scala +++ b/json/json4s/src/test/scala/sttp/client4/Json4sTests.scala @@ -23,7 +23,7 @@ class Json4sTests extends AnyFlatSpec with Matchers with EitherValues { val body = Outer(Inner(42, true, "horses"), "cats") val expected = """{"foo":{"a":42,"b":true,"c":"horses"},"bar":"cats"}""" - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) extractBody(req) shouldBe expected } @@ -75,16 +75,16 @@ class Json4sTests extends AnyFlatSpec with Matchers with EitherValues { it should "set the content type" in { val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") ct shouldBe Some(MediaType.ApplicationJson.copy(charset = Some(Utf8)).toString) } - it should "serialize from JObject using implicit json4sBodySerializer" in { + it should "serialize from JObject using" in { val jObject: JObject = JObject(JField("location", JString("hometown")), JField("bio", JString("Scala programmer"))) - val request: Request[Either[String, String]] = basicRequest.get(Uri("http://example.org")).body(jObject) + val request: Request[Either[String, String]] = basicRequest.get(Uri("http://example.org")).body(asJson(jObject)) val actualBody: String = request.body.show val actualContentType: Option[String] = request.contentType diff --git a/json/jsoniter/src/main/scala/sttp/client4/jsoniter/SttpJsoniterJsonApi.scala b/json/jsoniter/src/main/scala/sttp/client4/jsoniter/SttpJsoniterJsonApi.scala index 6d3db0206b..b2038472f2 100644 --- a/json/jsoniter/src/main/scala/sttp/client4/jsoniter/SttpJsoniterJsonApi.scala +++ b/json/jsoniter/src/main/scala/sttp/client4/jsoniter/SttpJsoniterJsonApi.scala @@ -1,27 +1,26 @@ package sttp.client4.jsoniter +import sttp.client4.DeserializationException +import sttp.client4.HttpError +import sttp.client4.IsOption +import sttp.client4.JsonInput +import sttp.client4.ResponseAs +import sttp.client4.ResponseException +import sttp.client4.ShowError +import sttp.client4.StringBody +import sttp.client4.asString +import sttp.client4.asStringAlways import sttp.client4.internal.Utf8 import sttp.client4.json.RichResponseAs -import sttp.client4.{ - asString, - asStringAlways, - BodySerializer, - DeserializationException, - HttpError, - IsOption, - JsonInput, - ResponseAs, - ResponseException, - ShowError, - StringBody -} import sttp.model.MediaType trait SttpJsoniterJsonApi { import com.github.plokhotnyuk.jsoniter_scala.core._ import ShowError.showErrorMessageFromException - implicit def jsoniterBodySerializer[B](implicit encoder: JsonValueCodec[B]): BodySerializer[B] = - b => StringBody(writeToString(b), Utf8, MediaType.ApplicationJson) + + /** Serialize the given value as JSON, to be used as a request's body using [[sttp.client4.Request.body]]. */ + def asJson[B](b: B)(implicit encoder: JsonValueCodec[B]): StringBody = + StringBody(writeToString(b), Utf8, MediaType.ApplicationJson) /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful diff --git a/json/jsoniter/src/test/scala/sttp/client4/jsoniter/JsoniterJsonTests.scala b/json/jsoniter/src/test/scala/sttp/client4/jsoniter/JsoniterJsonTests.scala index 9ea8f9578b..d1cfa2ea43 100644 --- a/json/jsoniter/src/test/scala/sttp/client4/jsoniter/JsoniterJsonTests.scala +++ b/json/jsoniter/src/test/scala/sttp/client4/jsoniter/JsoniterJsonTests.scala @@ -15,7 +15,7 @@ class JsoniterJsonTests extends AnyFlatSpec with Matchers with EitherValues { "The jsoniter 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 = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) extractBody(req) shouldBe expected } @@ -57,7 +57,7 @@ class JsoniterJsonTests extends AnyFlatSpec with Matchers with EitherValues { it should "read what it writes" in { val outer = Outer(Inner(42, true, "horses"), "cats") - val encoded = extractBody(basicRequest.body(outer)) + val encoded = extractBody(basicRequest.body(asJson(outer))) val decoded = runJsonResponseAs(asJson[Outer])(encoded) decoded.right.value shouldBe outer @@ -65,7 +65,7 @@ class JsoniterJsonTests extends AnyFlatSpec with Matchers with EitherValues { it should "set the content type" in { val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") @@ -74,16 +74,16 @@ class JsoniterJsonTests extends AnyFlatSpec with Matchers with EitherValues { it should "only set the content type if it was not set earlier" in { val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.contentType("horses/cats").body(body) + val req = basicRequest.contentType("horses/cats").body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") ct shouldBe Some("horses/cats") } - it should "serialize from case class Person using implicit jsoniterBodySerializer" in { + it should "serialize from case class Person" in { val person = Person("John") - val request = basicRequest.get(Uri("http://example.org")).body(person) + val request = basicRequest.get(Uri("http://example.org")).body(asJson(person)) val actualBody: String = request.body.show val actualContentType: Option[String] = request.contentType diff --git a/json/play-json/src/main/scala/sttp/client4/playJson/SttpPlayJsonApi.scala b/json/play-json/src/main/scala/sttp/client4/playJson/SttpPlayJsonApi.scala index daf10f665e..38071b3072 100644 --- a/json/play-json/src/main/scala/sttp/client4/playJson/SttpPlayJsonApi.scala +++ b/json/play-json/src/main/scala/sttp/client4/playJson/SttpPlayJsonApi.scala @@ -13,8 +13,9 @@ trait SttpPlayJsonApi { override def show(t: JsError): String = t.errors.mkString(",") } - implicit def playJsonBodySerializer[B: Writes]: BodySerializer[B] = - b => StringBody(Json.stringify(Json.toJson(b)), Utf8, MediaType.ApplicationJson) + /** Serialize the given value as JSON, to be used as a request's body using [[sttp.client4.Request.body]]. */ + def asJson[B: Writes](b: B): StringBody = + StringBody(Json.stringify(Json.toJson(b)), Utf8, MediaType.ApplicationJson) /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful diff --git a/json/play-json/src/test/scala/sttp/client4/PlayJsonTests.scala b/json/play-json/src/test/scala/sttp/client4/PlayJsonTests.scala index 5ab4ee0935..957b5ee27b 100644 --- a/json/play-json/src/test/scala/sttp/client4/PlayJsonTests.scala +++ b/json/play-json/src/test/scala/sttp/client4/PlayJsonTests.scala @@ -17,7 +17,7 @@ class PlayJsonTests extends AnyFlatSpec with Matchers with EitherValues { val body = Outer(Inner(42, true, "horses"), "cats") val expected = """{"foo":{"a":42,"b":true,"c":"horses"},"bar":"cats"}""" - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) extractBody(req) shouldBe expected } @@ -76,7 +76,7 @@ class PlayJsonTests extends AnyFlatSpec with Matchers with EitherValues { it should "read and write back to the same thing" in { val outer = Outer(Inner(42, true, "horses"), "cats") - val encoded = extractBody(basicRequest.body(outer)) + val encoded = extractBody(basicRequest.body(asJson(outer))) val decoded = runJsonResponseAs(asJson[Outer])(encoded) decoded.value shouldBe outer @@ -84,7 +84,7 @@ class PlayJsonTests extends AnyFlatSpec with Matchers with EitherValues { it should "set the content type" in { val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") @@ -93,18 +93,18 @@ class PlayJsonTests extends AnyFlatSpec with Matchers with EitherValues { it should "only set the content type if it was not set earlier" in { val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.contentType("horses/cats").body(body) + val req = basicRequest.contentType("horses/cats").body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") ct shouldBe Some("horses/cats") } - it should "serialize from JsObject using implicit playJsonBodySerializer" in { + it should "serialize from JsObject" in { val fields: Seq[(String, JsValue)] = Seq[(String, JsValue)](("location", JsString("hometown")), ("bio", JsString("Scala programmer"))) val json: JsObject = JsObject(fields) - val request: Request[Either[String, String]] = basicRequest.get(Uri("http://example.org")).body(json) + val request: Request[Either[String, String]] = basicRequest.get(Uri("http://example.org")).body(asJson(json)) val actualBody: String = request.body.show val actualContentType: Option[String] = request.contentType diff --git a/json/spray-json/src/main/scala/sttp/client4/sprayJson/SttpSprayJsonApi.scala b/json/spray-json/src/main/scala/sttp/client4/sprayJson/SttpSprayJsonApi.scala index 249fad6742..49e02d4ae2 100644 --- a/json/spray-json/src/main/scala/sttp/client4/sprayJson/SttpSprayJsonApi.scala +++ b/json/spray-json/src/main/scala/sttp/client4/sprayJson/SttpSprayJsonApi.scala @@ -7,8 +7,10 @@ import sttp.client4.json._ import sttp.model._ trait SttpSprayJsonApi { - implicit def sprayBodySerializer[B: JsonWriter](implicit printer: JsonPrinter = CompactPrinter): BodySerializer[B] = - b => StringBody(printer(b.toJson), Utf8, MediaType.ApplicationJson) + + /** Serialize the given value as JSON, to be used as a request's body using [[sttp.client4.Request.body]]. */ + def asJson[B: JsonWriter](b: B)(implicit printer: JsonPrinter = CompactPrinter): StringBody = + StringBody(printer(b.toJson), Utf8, MediaType.ApplicationJson) /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful diff --git a/json/spray-json/src/test/scala/sttp/client4/BackendStubSprayJsonTests.scala b/json/spray-json/src/test/scala/sttp/client4/sprayJson/BackendStubSprayJsonTests.scala similarity index 92% rename from json/spray-json/src/test/scala/sttp/client4/BackendStubSprayJsonTests.scala rename to json/spray-json/src/test/scala/sttp/client4/sprayJson/BackendStubSprayJsonTests.scala index f40627c05e..a2d50c9bba 100644 --- a/json/spray-json/src/test/scala/sttp/client4/BackendStubSprayJsonTests.scala +++ b/json/spray-json/src/test/scala/sttp/client4/sprayJson/BackendStubSprayJsonTests.scala @@ -1,4 +1,4 @@ -package sttp.client4 +package sttp.client4.sprayJson import org.scalatest.concurrent.ScalaFutures import org.scalatest.flatspec.AnyFlatSpec @@ -7,7 +7,7 @@ import sttp.client4.testing.SyncBackendStub import sttp.model.Uri import spray.json.DefaultJsonProtocol.{jsonFormat1, StringJsonFormat} import spray.json.RootJsonFormat -import sprayJson._ +import sttp.client4.basicRequest case class Person(name: String) diff --git a/json/spray-json/src/test/scala/sttp/client4/SprayJsonTests.scala b/json/spray-json/src/test/scala/sttp/client4/sprayJson/SprayJsonTests.scala similarity index 85% rename from json/spray-json/src/test/scala/sttp/client4/SprayJsonTests.scala rename to json/spray-json/src/test/scala/sttp/client4/sprayJson/SprayJsonTests.scala index 88086c4c0d..a1cf3843c4 100644 --- a/json/spray-json/src/test/scala/sttp/client4/SprayJsonTests.scala +++ b/json/spray-json/src/test/scala/sttp/client4/sprayJson/SprayJsonTests.scala @@ -1,26 +1,33 @@ -package sttp.client4 +package sttp.client4.sprayJson import org.scalatest.EitherValues import spray.json.DefaultJsonProtocol._ -import spray.json.DefaultJsonProtocol.RootJsObjectFormat -import spray.json.JsonParser.ParsingException import spray.json.{DeserializationException => _, _} -import sttp.client4.SprayJsonTests._ import sttp.client4.internal.Utf8 -import sttp.client4.sprayJson.{sprayBodySerializer, _} import sttp.model._ import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers +import sttp.client4.basicRequest +import sttp.client4.ResponseAs +import sttp.client4.PartialRequest +import sttp.client4.StringBody +import sttp.client4.MappedResponseAs +import sttp.client4.ResponseAsByteArray +import sttp.client4.Request +import sttp.client4.DeserializationException +import spray.json.JsonParser.ParsingException class SprayJsonTests extends AnyFlatSpec with Matchers with EitherValues { + import SprayJsonTests._ + behavior of "The spray-json module" - implicit private val jsObjectSerializer: BodySerializer[JsObject] = sprayBodySerializer(RootJsObjectFormat) + // implicit private val jsObjectSerializer: BodySerializer[JsObject] = sprayBodySerializer(RootJsObjectFormat) it should "encode arbitrary json bodies" in { val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) extractBody(req) should include(""""foo":{"a":42,"b":true,"c":"horses"}""") extractBody(req) should include(""""bar":"cats"""") @@ -73,7 +80,7 @@ class SprayJsonTests extends AnyFlatSpec with Matchers with EitherValues { it should "set the content type" in { val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") @@ -85,7 +92,7 @@ class SprayJsonTests extends AnyFlatSpec with Matchers with EitherValues { "location" -> "hometown".toJson, "bio" -> "Scala programmer".toJson ) - val request: Request[Either[String, String]] = basicRequest.get(Uri("http://example.org")).body(json) + val request: Request[Either[String, String]] = basicRequest.get(Uri("http://example.org")).body(asJson(json)) val actualBody: String = request.body.show val actualContentType: Option[String] = request.contentType @@ -107,7 +114,7 @@ class SprayJsonTests extends AnyFlatSpec with Matchers with EitherValues { def runJsonResponseAs[A](responseAs: ResponseAs[A]): String => A = responseAs.delegate match { - case responseAs: MappedResponseAs[_, A, Nothing] => + case responseAs: MappedResponseAs[_, A, Nothing] @unchecked => responseAs.raw match { case ResponseAsByteArray => s => responseAs.g(s.getBytes(Utf8), ResponseMetadata(StatusCode.Ok, "", Nil)) diff --git a/json/tethys-json/src/main/scala/sttp/client4/tethysJson/SttpTethysApi.scala b/json/tethys-json/src/main/scala/sttp/client4/tethysJson/SttpTethysApi.scala index a646b56fc6..607917815c 100644 --- a/json/tethys-json/src/main/scala/sttp/client4/tethysJson/SttpTethysApi.scala +++ b/json/tethys-json/src/main/scala/sttp/client4/tethysJson/SttpTethysApi.scala @@ -11,11 +11,11 @@ import tethys.writers.tokens.TokenWriterProducer trait SttpTethysApi { - implicit def tethysBodySerializer[B](implicit + /** Serialize the given value as JSON, to be used as a request's body using [[sttp.client4.Request.body]]. */ + def asJson[B](b: B)(implicit jsonWriter: JsonWriter[B], tokenWriterProducer: TokenWriterProducer - ): BodySerializer[B] = - b => StringBody(b.asJson, Utf8, MediaType.ApplicationJson) + ): StringBody = StringBody(b.asJson, Utf8, MediaType.ApplicationJson) /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful diff --git a/json/tethys-json/src/test/scala/sttp/client4/tethysJson/TethysTests.scala b/json/tethys-json/src/test/scala/sttp/client4/tethysJson/TethysTests.scala index adf34a56a8..e7312ec4e0 100644 --- a/json/tethys-json/src/test/scala/sttp/client4/tethysJson/TethysTests.scala +++ b/json/tethys-json/src/test/scala/sttp/client4/tethysJson/TethysTests.scala @@ -20,7 +20,7 @@ class TethysTests extends AnyFlatSpec with Matchers with EitherValues { val body = Outer(Inner(42, true, "horses"), "cats") val expected = """{"foo":{"a":42,"b":true,"c":"horses"},"bar":"cats"}""" - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) extractBody(req, MediaType.ApplicationJson) shouldBe expected } @@ -73,7 +73,7 @@ class TethysTests extends AnyFlatSpec with Matchers with EitherValues { it should "encode and decode back to the same thing" in { val outer = Outer(Inner(42, true, "horses"), "cats") - val encoded = extractBody(basicRequest.body(outer), MediaType.ApplicationJson) + val encoded = extractBody(basicRequest.body(asJson(outer)), MediaType.ApplicationJson) val decoded = runJsonResponseAs(asJson[Outer])(encoded) decoded.right.value shouldBe outer @@ -81,7 +81,7 @@ class TethysTests extends AnyFlatSpec with Matchers with EitherValues { it should "set the content type" in { val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") @@ -90,7 +90,7 @@ class TethysTests extends AnyFlatSpec with Matchers with EitherValues { it should "only set the content type if it was not set earlier" in { val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.contentType("horses/cats").body(body) + val req = basicRequest.contentType("horses/cats").body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") diff --git a/json/upickle/src/main/scala/sttp/client4/upicklejson/SttpUpickleApi.scala b/json/upickle/src/main/scala/sttp/client4/upicklejson/SttpUpickleApi.scala index a91273092f..8984e5cfde 100644 --- a/json/upickle/src/main/scala/sttp/client4/upicklejson/SttpUpickleApi.scala +++ b/json/upickle/src/main/scala/sttp/client4/upicklejson/SttpUpickleApi.scala @@ -8,8 +8,9 @@ import sttp.client4.json._ trait SttpUpickleApi { val upickleApi: upickle.Api - implicit def upickleBodySerializer[B](implicit encoder: upickleApi.Writer[B]): BodySerializer[B] = - b => StringBody(upickleApi.write(b), Utf8, MediaType.ApplicationJson) + /** Serialize the given value as JSON, to be used as a request's body using [[sttp.client4.Request.body]]. */ + def asJson[B](b: B)(implicit encoder: upickleApi.Writer[B]): StringBody = + StringBody(upickleApi.write(b), Utf8, MediaType.ApplicationJson) /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful diff --git a/json/upickle/src/test/scala/sttp/client4/upicklejson/UpickleTests.scala b/json/upickle/src/test/scala/sttp/client4/upicklejson/UpickleTests.scala index cd26902f49..1247cd9990 100644 --- a/json/upickle/src/test/scala/sttp/client4/upicklejson/UpickleTests.scala +++ b/json/upickle/src/test/scala/sttp/client4/upicklejson/UpickleTests.scala @@ -16,7 +16,7 @@ class UpickleTests extends AnyFlatSpec with Matchers with EitherValues { val body = Outer(Inner(42, true, "horses"), "cats") val expected = """{"foo":{"a":42,"b":true,"c":"horses"},"bar":"cats"}""" - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) extractBody(req) shouldBe expected } @@ -87,7 +87,7 @@ class UpickleTests extends AnyFlatSpec with Matchers with EitherValues { val outer = Outer(Inner(42, true, "horses"), "cats") - val encoded = extractBody(basicRequest.body(outer)) + val encoded = extractBody(basicRequest.body(asJson(outer))) val decoded = runJsonResponseAs(asJson[Outer])(encoded) decoded.right.value shouldBe outer @@ -98,7 +98,7 @@ class UpickleTests extends AnyFlatSpec with Matchers with EitherValues { import sttp.client4.upicklejson.default._ val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") @@ -110,14 +110,14 @@ class UpickleTests extends AnyFlatSpec with Matchers with EitherValues { import sttp.client4.upicklejson.default._ val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.contentType("horses/cats").body(body) + val req = basicRequest.contentType("horses/cats").body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") ct shouldBe Some("horses/cats") } - it should "serialize ujson.Obj using implicit upickleBodySerializer" in { + it should "serialize ujson.Obj" in { import UsingDefaultReaderWriters._ import sttp.client4.upicklejson.default._ @@ -125,7 +125,7 @@ class UpickleTests extends AnyFlatSpec with Matchers with EitherValues { "location" -> "hometown", "bio" -> "Scala programmer" ) - val request: Request[Either[String, String]] = basicRequest.get(Uri("http://example.org")).body(json) + val request: Request[Either[String, String]] = basicRequest.get(Uri("http://example.org")).body(asJson(json)) val actualBody: String = request.body.show val actualContentType: Option[String] = request.contentType @@ -147,7 +147,7 @@ class UpickleTests extends AnyFlatSpec with Matchers with EitherValues { val body = Outer(Inner(42, true, "horses"), "cats") val expected = """{"foo":{"a":42,"b":true,"c":"horses"},"bar":"cats"}""" - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) extractBody(req) shouldBe expected } @@ -177,7 +177,7 @@ class UpickleTests extends AnyFlatSpec with Matchers with EitherValues { def runJsonResponseAs[A](responseAs: ResponseAs[A]): String => A = responseAs.delegate match { - case responseAs: MappedResponseAs[_, A, Nothing] => + case responseAs: MappedResponseAs[_, A, Nothing] @unchecked => responseAs.raw match { case ResponseAsByteArray => s => responseAs.g(s.getBytes(Utf8), ResponseMetadata(StatusCode.Ok, "", Nil)) diff --git a/json/zio-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala b/json/zio-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala index c27c023c61..daff87e65d 100644 --- a/json/zio-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala +++ b/json/zio-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala @@ -1,28 +1,25 @@ package sttp.client4.ziojson +import sttp.client4.DeserializationException +import sttp.client4.HttpError +import sttp.client4.IsOption +import sttp.client4.JsonInput +import sttp.client4.ResponseAs +import sttp.client4.ResponseException +import sttp.client4.ShowError +import sttp.client4.StringBody +import sttp.client4.asString +import sttp.client4.asStringAlways import sttp.client4.internal.Utf8 import sttp.client4.json.RichResponseAs -import sttp.client4.{ - asString, - asStringAlways, - BodySerializer, - DeserializationException, - HttpError, - IsOption, - JsonInput, - ResponseAs, - ResponseException, - ShowError, - StringBody -} import sttp.model.MediaType trait SttpZioJsonApi extends SttpZioJsonApiExtensions { import zio.json._ private[ziojson] implicit val stringShowError: ShowError[String] = t => t - implicit def zioJsonBodySerializer[B: JsonEncoder]: BodySerializer[B] = - b => StringBody(b.toJson, Utf8, MediaType.ApplicationJson) + /** Serialize the given value as JSON, to be used as a request's body using [[sttp.client4.Request.body]]. */ + def asJson[B: JsonEncoder](b: B): StringBody = StringBody(b.toJson, Utf8, MediaType.ApplicationJson) /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful diff --git a/json/zio-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala b/json/zio-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala index d4ac15ba72..acaeba644c 100644 --- a/json/zio-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala +++ b/json/zio-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala @@ -2,13 +2,18 @@ package sttp.client4.ziojson import sttp.capabilities.Effect import sttp.capabilities.zio.ZioStreams -import sttp.client4.{asStream, DeserializationException, HttpError, IsOption, ResponseException, StreamResponseAs} +import sttp.client4.DeserializationException +import sttp.client4.HttpError +import sttp.client4.ResponseException +import sttp.client4.StreamResponseAs +import sttp.client4.asStream +import zio.Task +import zio.ZIO import zio.json.JsonDecoder import zio.stream.ZPipeline -import zio.{Task, ZIO} trait SttpZioJsonApiExtensions { this: SttpZioJsonApi => - def asJsonStream[B: JsonDecoder: IsOption] + def asJsonStream[B: JsonDecoder] : StreamResponseAs[Either[ResponseException[String, String], B], ZioStreams with Effect[Task]] = asStream(ZioStreams)(s => JsonDecoder[B] diff --git a/json/zio-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala b/json/zio-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala index 23f9bf589a..31153d1e4d 100644 --- a/json/zio-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala +++ b/json/zio-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala @@ -15,7 +15,7 @@ class ZioJsonTests extends AnyFlatSpec with Matchers with EitherValues { "The ziojson 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 = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) extractBody(req) shouldBe expected } @@ -67,7 +67,7 @@ class ZioJsonTests extends AnyFlatSpec with Matchers with EitherValues { it should "read what it writes" in { val outer = Outer(Inner(42, true, "horses"), "cats") - val encoded = extractBody(basicRequest.body(outer)) + val encoded = extractBody(basicRequest.body(asJson(outer))) val decoded = runJsonResponseAs(asJson[Outer])(encoded) decoded.right.value shouldBe outer @@ -75,7 +75,7 @@ class ZioJsonTests extends AnyFlatSpec with Matchers with EitherValues { it should "set the content type" in { val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") @@ -84,17 +84,17 @@ class ZioJsonTests extends AnyFlatSpec with Matchers with EitherValues { it should "only set the content type if it was not set earlier" in { val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.contentType("horses/cats").body(body) + val req = basicRequest.contentType("horses/cats").body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") ct shouldBe Some("horses/cats") } - it should "serialize from Json.Obj using implicit zioJsonBodySerializer" in { + it should "serialize from Json.Obj" in { val fields: Chunk[(String, Json)] = Chunk(("location", Json.Str("hometown")), ("bio", Json.Str("Scala programmer"))) val jObject: Json.Obj = Json.Obj(fields) - val request = basicRequest.get(Uri("http://example.org")).body(jObject) + val request = basicRequest.get(Uri("http://example.org")).body(asJson(jObject)) val actualBody: String = request.body.show val actualContentType: Option[String] = request.contentType @@ -116,7 +116,7 @@ class ZioJsonTests extends AnyFlatSpec with Matchers with EitherValues { def runJsonResponseAs[A](responseAs: ResponseAs[A]): String => A = responseAs.delegate match { - case responseAs: MappedResponseAs[_, A, Nothing] => + case responseAs: MappedResponseAs[_, A, Nothing] @unchecked => responseAs.raw match { case ResponseAsByteArray => s => responseAs.g(s.getBytes(Utf8), ResponseMetadata(StatusCode.Ok, "", Nil)) diff --git a/json/zio1-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala b/json/zio1-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala index c27c023c61..daff87e65d 100644 --- a/json/zio1-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala +++ b/json/zio1-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala @@ -1,28 +1,25 @@ package sttp.client4.ziojson +import sttp.client4.DeserializationException +import sttp.client4.HttpError +import sttp.client4.IsOption +import sttp.client4.JsonInput +import sttp.client4.ResponseAs +import sttp.client4.ResponseException +import sttp.client4.ShowError +import sttp.client4.StringBody +import sttp.client4.asString +import sttp.client4.asStringAlways import sttp.client4.internal.Utf8 import sttp.client4.json.RichResponseAs -import sttp.client4.{ - asString, - asStringAlways, - BodySerializer, - DeserializationException, - HttpError, - IsOption, - JsonInput, - ResponseAs, - ResponseException, - ShowError, - StringBody -} import sttp.model.MediaType trait SttpZioJsonApi extends SttpZioJsonApiExtensions { import zio.json._ private[ziojson] implicit val stringShowError: ShowError[String] = t => t - implicit def zioJsonBodySerializer[B: JsonEncoder]: BodySerializer[B] = - b => StringBody(b.toJson, Utf8, MediaType.ApplicationJson) + /** Serialize the given value as JSON, to be used as a request's body using [[sttp.client4.Request.body]]. */ + def asJson[B: JsonEncoder](b: B): StringBody = StringBody(b.toJson, Utf8, MediaType.ApplicationJson) /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful diff --git a/json/zio1-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala b/json/zio1-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala index b21f8e2050..27f16ac1b2 100644 --- a/json/zio1-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala +++ b/json/zio1-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala @@ -2,14 +2,19 @@ package sttp.client4.ziojson import sttp.capabilities.Effect import sttp.capabilities.zio.ZioStreams -import sttp.client4.{asStream, DeserializationException, HttpError, IsOption, ResponseException, StreamResponseAs} +import sttp.client4.DeserializationException +import sttp.client4.HttpError +import sttp.client4.ResponseException +import sttp.client4.StreamResponseAs +import sttp.client4.asStream +import zio.RIO +import zio.ZIO import zio.blocking.Blocking import zio.json.JsonDecoder import zio.stream.ZTransducer -import zio.{RIO, ZIO} trait SttpZioJsonApiExtensions { this: SttpZioJsonApi => - def asJsonStream[B: JsonDecoder: IsOption] + def asJsonStream[B: JsonDecoder] : StreamResponseAs[Either[ResponseException[String, String], B], ZioStreams with Effect[RIO[Blocking, *]]] = asStream(ZioStreams)(s => JsonDecoder[B] diff --git a/json/zio1-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala b/json/zio1-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala index 23f9bf589a..4067062487 100644 --- a/json/zio1-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala +++ b/json/zio1-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala @@ -15,7 +15,7 @@ class ZioJsonTests extends AnyFlatSpec with Matchers with EitherValues { "The ziojson 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 = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) extractBody(req) shouldBe expected } @@ -67,7 +67,7 @@ class ZioJsonTests extends AnyFlatSpec with Matchers with EitherValues { it should "read what it writes" in { val outer = Outer(Inner(42, true, "horses"), "cats") - val encoded = extractBody(basicRequest.body(outer)) + val encoded = extractBody(basicRequest.body(asJson(outer))) val decoded = runJsonResponseAs(asJson[Outer])(encoded) decoded.right.value shouldBe outer @@ -75,7 +75,7 @@ class ZioJsonTests extends AnyFlatSpec with Matchers with EitherValues { it should "set the content type" in { val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.body(body) + val req = basicRequest.body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") @@ -84,17 +84,17 @@ class ZioJsonTests extends AnyFlatSpec with Matchers with EitherValues { it should "only set the content type if it was not set earlier" in { val body = Outer(Inner(42, true, "horses"), "cats") - val req = basicRequest.contentType("horses/cats").body(body) + val req = basicRequest.contentType("horses/cats").body(asJson(body)) val ct = req.headers.map(h => (h.name, h.value)).toMap.get("Content-Type") ct shouldBe Some("horses/cats") } - it should "serialize from Json.Obj using implicit zioJsonBodySerializer" in { + it should "serialize from Json.Obj" in { val fields: Chunk[(String, Json)] = Chunk(("location", Json.Str("hometown")), ("bio", Json.Str("Scala programmer"))) val jObject: Json.Obj = Json.Obj(fields) - val request = basicRequest.get(Uri("http://example.org")).body(jObject) + val request = basicRequest.get(Uri("http://example.org")).body(asJson(jObject)) val actualBody: String = request.body.show val actualContentType: Option[String] = request.contentType