Skip to content

Commit 3ce24c1

Browse files
authored
Remove BodySerializer (#2362)
1 parent b8ddedd commit 3ce24c1

File tree

32 files changed

+226
-208
lines changed

32 files changed

+226
-208
lines changed

core/src/main/scala/sttp/client4/SttpApi.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,8 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
200200
)
201201

202202
/** Content type will be set to `application/octet-stream`, can be overridden later using the `contentType` method. */
203-
def multipart[B: BodySerializer](name: String, b: B): Part[BasicBodyPart] =
204-
Part(name, implicitly[BodySerializer[B]].apply(b), contentType = Some(MediaType.ApplicationXWwwFormUrlencoded))
203+
def multipart(name: String, b: BasicBodyPart): Part[BasicBodyPart] =
204+
Part(name, b, contentType = Some(MediaType.ApplicationXWwwFormUrlencoded))
205205

206206
// stream response specifications
207207

core/src/main/scala/sttp/client4/requestBuilder.scala

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -155,87 +155,108 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R]
155155
private[client4] def setContentLengthIfMissing(l: => Long): PR =
156156
if (hasContentLength) this else contentLength(l)
157157

158-
/** Uses the `utf-8` encoding.
158+
/** Sets the body of this request to the given string, using the UTF-8 encoding.
159159
*
160-
* If content type is not yet specified, will be set to `text/plain` with `utf-8` encoding.
160+
* If content type is not yet specified, will be set to `text/plain` with UTF-8 encoding.
161161
*
162-
* If content length is not yet specified, will be set to the number of bytes in the string using the `utf-8`
163-
* encoding.
162+
* If content length is not yet specified, will be set to the number of bytes in the string using the UTF-8 encoding.
164163
*/
165164
def body(b: String): PR = body(b, Utf8)
166165

167-
/** If content type is not yet specified, will be set to `text/plain` with the given encoding.
166+
/** Sets the body of this request to the given string, using the given encoding.
167+
*
168+
* If content type is not yet specified, will be set to `text/plain` with the given encoding.
168169
*
169170
* If content length is not yet specified, will be set to the number of bytes in the string using the given encoding.
170171
*/
171172
def body(b: String, encoding: String): PR =
172-
withBody(StringBody(b, encoding)).setContentLengthIfMissing(b.getBytes(encoding).length.toLong)
173+
body(StringBody(b, encoding)).setContentLengthIfMissing(b.getBytes(encoding).length.toLong)
173174

174-
/** If content type is not yet specified, will be set to `application/octet-stream`.
175+
/** Sets the body of this request to the given byte array.
176+
*
177+
* If content type is not yet specified, will be set to `application/octet-stream`.
175178
*
176179
* If content length is not yet specified, will be set to the length of the given array.
177180
*/
178-
def body(b: Array[Byte]): PR = withBody(ByteArrayBody(b)).setContentLengthIfMissing(b.length.toLong)
181+
def body(b: Array[Byte]): PR = body(ByteArrayBody(b)).setContentLengthIfMissing(b.length.toLong)
179182

180-
/** If content type is not yet specified, will be set to `application/octet-stream`. */
181-
def body(b: ByteBuffer): PR = withBody(ByteBufferBody(b))
183+
/** Sets the body of this request to the given byte buffer.
184+
*
185+
* If content type is not yet specified, will be set to `application/octet-stream`.
186+
*/
187+
def body(b: ByteBuffer): PR = body(ByteBufferBody(b))
182188

183-
/** If content type is not yet specified, will be set to `application/octet-stream`.
189+
/** Sets the body of this request to the given input stream.
190+
*
191+
* If content type is not yet specified, will be set to `application/octet-stream`.
184192
*/
185-
def body(b: InputStream): PR = withBody(InputStreamBody(b))
193+
def body(b: InputStream): PR = body(InputStreamBody(b))
186194

187195
/** If content type is not yet specified, will be set to `application/octet-stream`.
188196
*
189197
* If content length is not yet specified, will be set to the length of the given file.
190198
*/
191-
private[client4] def body(f: SttpFile): PR = withBody(FileBody(f)).setContentLengthIfMissing(f.size)
199+
private[client4] def body(f: SttpFile): PR = body(FileBody(f)).setContentLengthIfMissing(f.size)
192200

193-
/** Encodes the given parameters as form data using `utf-8`. If content type is not yet specified, will be set to
194-
* `application/x-www-form-urlencoded`.
201+
/** Sets the body of this request to the given form-data parameters. The parameters are encoded using UTF-8.
202+
*
203+
* If content type is not yet specified, will be set to `application/x-www-form-urlencoded`.
195204
*
196205
* If content length is not yet specified, will be set to the length of the number of bytes in the url-encoded
197206
* parameter string.
198207
*/
199208
def body(fs: Map[String, String]): PR = formDataBody(fs.toList, Utf8)
200209

201-
/** Encodes the given parameters as form data. If content type is not yet specified, will be set to
202-
* `application/x-www-form-urlencoded`.
210+
/** Sets the body of this request to the given form-data parameters. The parameters are encoded using the given
211+
* encoding.
212+
*
213+
* If content type is not yet specified, will be set to `application/x-www-form-urlencoded`.
203214
*
204215
* If content length is not yet specified, will be set to the length of the number of bytes in the url-encoded
205216
* parameter string.
206217
*/
207218
def body(fs: Map[String, String], encoding: String): PR = formDataBody(fs.toList, encoding)
208219

209-
/** Encodes the given parameters as form data using `utf-8`. If content type is not yet specified, will be set to
210-
* `application/x-www-form-urlencoded`.
220+
/** Sets the body of this request to the given form-data parameters. The parameters are encoded using UTF-8.
221+
*
222+
* If content type is not yet specified, will be set to `application/x-www-form-urlencoded`.
211223
*
212224
* If content length is not yet specified, will be set to the length of the number of bytes in the url-encoded
213225
* parameter string.
214226
*/
215227
def body(fs: (String, String)*): PR = formDataBody(fs.toList, Utf8)
216228

217-
/** Encodes the given parameters as form data. If content type is not yet specified, will be set to
218-
* `application/x-www-form-urlencoded`.
229+
/** Sets the body of this request to the given form-data parameters. The parameters are encoded using the given
230+
* encoding.
231+
*
232+
* If content type is not yet specified, will be set to `application/x-www-form-urlencoded`.
219233
*
220234
* If content length is not yet specified, will be set to the length of the number of bytes in the url-encoded
221235
* parameter string.
222236
*/
223237
def body(fs: Seq[(String, String)], encoding: String): PR = formDataBody(fs, encoding)
224238

225-
def multipartBody(ps: Seq[Part[BasicBodyPart]]): PR = copyWithBody(BasicMultipartBody(ps))
226-
227-
def multipartBody(p1: Part[BasicBodyPart], ps: Part[BasicBodyPart]*): PR = copyWithBody(
228-
BasicMultipartBody(p1 :: ps.toList)
229-
)
230-
231239
private def formDataBody(fs: Seq[(String, String)], encoding: String): PR = {
232240
val b = BasicBody.paramsToStringBody(fs, encoding)
233241
copyWithBody(b)
234242
.setContentTypeIfMissing(MediaType.ApplicationXWwwFormUrlencoded)
235243
.setContentLengthIfMissing(b.s.getBytes(encoding).length.toLong)
236244
}
237245

238-
def withBody(body: BasicBody): PR = {
246+
/** Sets the body of this request to the given multipart form parts. */
247+
def multipartBody(ps: Seq[Part[BasicBodyPart]]): PR = copyWithBody(BasicMultipartBody(ps))
248+
249+
/** Sets the body of this request to the given multipart form parts. */
250+
def multipartBody(p1: Part[BasicBodyPart], ps: Part[BasicBodyPart]*): PR = copyWithBody(
251+
BasicMultipartBody(p1 :: ps.toList)
252+
)
253+
254+
/** Sets the body of this request to the given [[BasicBody]] implementation.
255+
*
256+
* If content type is not yet specified, it will be set to the default content type of the body, including the
257+
* encoding in case of a string body.
258+
*/
259+
def body(body: BasicBody): PR = {
239260
val defaultCt = body match {
240261
case StringBody(_, encoding, ct) =>
241262
ct.copy(charset = Some(encoding))
@@ -254,8 +275,8 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R]
254275
def followRedirects(fr: Boolean): PR = withOptions(options.copy(followRedirects = fr))
255276

256277
def maxRedirects(n: Int): PR =
257-
if (n <= 0) withOptions(options.copy(followRedirects = false))
258-
else withOptions(options.copy(followRedirects = true, maxRedirects = n))
278+
if (n <= 0) withOptions(options.copy(followRedirects = false))
279+
else withOptions(options.copy(followRedirects = true, maxRedirects = n))
259280

260281
/** When a POST or PUT request is redirected, should the redirect be a POST/PUT as well (with the original body), or
261282
* should the request be converted to a GET without a body.

core/src/main/scala/sttp/client4/wrappers/FollowRedirectsBackend.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ abstract class FollowRedirectsBackend[F[_], P] private (
7777
if (applicable && (r.options.redirectToGet || alwaysChanged) && !neverChanged) {
7878
// when transforming POST or PUT into a get, content is dropped, also filter out content-related request headers
7979
r.method(Method.GET, r.uri)
80-
.withBody(NoBody)
80+
.body(NoBody)
8181
.withHeaders(r.headers.filterNot(header => config.contentHeaders.contains(header.name.toLowerCase())))
8282
} else r
8383
}

core/src/main/scalajs/sttp/client4/PartialRequestExtensions.scala

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,4 @@ trait PartialRequestExtensions[+R <: PartialRequestBuilder[R, _]] { self: R =>
1010
* If content length is not yet specified, will be set to the length of the given file.
1111
*/
1212
def body(file: File): R = body(SttpFile.fromDomFile(file))
13-
14-
// this method needs to be in the extensions, so that it has lowest priority when considering overloading options
15-
/** If content type is not yet specified, will be set to `application/octet-stream`.
16-
*/
17-
def body[B: BodySerializer](b: B): R =
18-
withBody(implicitly[BodySerializer[B]].apply(b))
1913
}

core/src/main/scalajvm/sttp/client4/PartialRequestExtensions.scala

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ import java.io.File
44
import java.nio.file.Path
55

66
import sttp.client4.internal.SttpFile
7-
import sttp.client4.BodySerializer
87

98
trait PartialRequestExtensions[+R <: PartialRequestBuilder[R, _]] { self: R =>
109

1110
/** If content type is not yet specified, will be set to `application/octet-stream`.
1211
*
13-
* If content length is noBodySerializert yet specified, will be set to the length of the given file.
12+
* If content length is not yet specified, will be set to the length of the given file.
1413
*/
1514
def body(file: File): R = body(SttpFile.fromFile(file))
1615

@@ -19,10 +18,4 @@ trait PartialRequestExtensions[+R <: PartialRequestBuilder[R, _]] { self: R =>
1918
* If content length is not yet specified, will be set to the length of the given file.
2019
*/
2120
def body(path: Path): R = body(SttpFile.fromPath(path))
22-
23-
// this method needs to be in the extensions, so that it has lowest priority when considering overloading options
24-
/** If content type is not yet specified, will be set to `application/octet-stream`.
25-
*/
26-
def body[B: BodySerializer](b: B): R =
27-
withBody(implicitly[BodySerializer[B]].apply(b))
2821
}

core/src/main/scalanative/sttp/client4/PartialRequestExtensions.scala

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,4 @@ trait PartialRequestExtensions[+R <: PartialRequestBuilder[R, _]] { self: R =>
1818
* If content length is not yet specified, will be set to the length of the given file.
1919
*/
2020
def body(path: Path): R = body(SttpFile.fromPath(path))
21-
22-
// this method needs to be in the extensions, so that it has lowest priority when considering overloading options
23-
/** If content type is not yet specified, will be set to `application/octet-stream`.
24-
*/
25-
def body[B: BodySerializer](b: B): R =
26-
withBody(implicitly[BodySerializer[B]].apply(b))
2721
}

docs/json.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@
22

33
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.
44

5-
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.
5+
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.
66

77
The following variants of `asJson` methods are available:
88

9-
* regular - deserializes the body to json, only if the response is successful (2xx)
10-
* `always` - deserializes the body to json regardless of the status code
11-
* `either` - uses different deserializers for error and successful (2xx) responses
9+
* `asJson(b: B)` - serializes the body so that it can be used as a request's body, e.g. using `basicRequest.body(asJson(myValue))`
10+
* `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])`
11+
* `asJsonAlways[B]` - specifies that the body should be deserialized to json, regardless of the status code
12+
* `asJsonEither[E, B]` - specifies that the body should be deserialized to json, using different deserializers for error and successful (2xx) responses
1213

1314
The type signatures vary depending on the underlying library (required implicits and error representation differs), but they obey the following pattern:
1415

1516
```scala mdoc:compile-only
1617
import sttp.client4._
1718

19+
def asJson[B](b: B): StringBody = ???
1820
def asJson[B]: ResponseAs[Either[ResponseException[String, Exception], B]] = ???
1921
def asJsonAlways[B]: ResponseAs[Either[DeserializationException[Exception], B]] = ???
2022
def asJsonEither[E, B]: ResponseAs[Either[ResponseException[E, Exception], B]] = ???
@@ -54,7 +56,7 @@ val requestPayload = RequestPayload("some data")
5456
val response: Response[Either[ResponseException[String, io.circe.Error], ResponsePayload]] =
5557
basicRequest
5658
.post(uri"...")
57-
.body(requestPayload)
59+
.body(asJson(requestPayload))
5860
.response(asJson[ResponsePayload])
5961
.send(backend)
6062
```
@@ -90,7 +92,7 @@ implicit val formats = org.json4s.DefaultFormats
9092
val response: Response[Either[ResponseException[String, Exception], ResponsePayload]] =
9193
basicRequest
9294
.post(uri"...")
93-
.body(requestPayload)
95+
.body(asJson(requestPayload))
9496
.response(asJson[ResponsePayload])
9597
.send(backend)
9698
```
@@ -122,7 +124,7 @@ val requestPayload = RequestPayload("some data")
122124
val response: Response[Either[ResponseException[String, Exception], ResponsePayload]] =
123125
basicRequest
124126
.post(uri"...")
125-
.body(requestPayload)
127+
.body(asJson(requestPayload))
126128
.response(asJson[ResponsePayload])
127129
.send(backend)
128130
```
@@ -179,7 +181,7 @@ val requestPayload = RequestPayload("some data")
179181
val response: Response[Either[ResponseException[String, String], ResponsePayload]] =
180182
basicRequest
181183
.post(uri"...")
182-
.body(requestPayload)
184+
.body(asJson(requestPayload))
183185
.response(asJson[ResponsePayload])
184186
.send(backend)
185187
```
@@ -219,7 +221,7 @@ val requestPayload = RequestPayload("some data")
219221
val response: Response[Either[ResponseException[String, Exception], ResponsePayload]] =
220222
basicRequest
221223
.post(uri"...")
222-
.body(requestPayload)
224+
.body(asJson(requestPayload))
223225
.response(asJson[ResponsePayload])
224226
.send(backend)
225227
```
@@ -256,7 +258,7 @@ val requestPayload = RequestPayload("some data")
256258
val response: Response[Either[ResponseException[String, Exception], ResponsePayload]] =
257259
basicRequest
258260
.post(uri"...")
259-
.body(requestPayload)
261+
.body(asJson(requestPayload))
260262
.response(asJson[ResponsePayload])
261263
.send(backend)
262264
```

docs/quickstart.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ implicit val responseRW: ReadWriter[HttpBinResponse] = macroRW[HttpBinResponse]
9393

9494
val request = basicRequest
9595
.post(uri"https://httpbin.org/post")
96-
.body(MyRequest("test", 42))
96+
.body(asJson(MyRequest("test", 42)))
9797
.response(asJson[HttpBinResponse])
9898
val response = request.send(backend)
9999

docs/requests/body.md

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,11 @@ basicRequest.body("k1" -> "v1", "k2" -> "v2")
8383
basicRequest.body(Seq("k1" -> "v1", "k2" -> "v2"), "utf-8")
8484
```
8585

86-
## Custom body serializers
86+
## Custom serializers
8787

88-
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:
89-
90-
```scala
91-
type BodySerializer[B] = B => BasicRequestBody
92-
```
93-
94-
A `BasicRequestBody` is a wrapper for one of the supported request body types: a `String`/byte array or an input stream.
88+
It is also possible to write custom serializers, which return arbitrary body representations. These should be
89+
methods/functions which return instances of `BasicBody`, which is a wrapper for one of the supported request body
90+
types: a `String`, byte array, an input stream, etc.
9591

9692
For example, here's how to write a custom serializer for a case class, with serializer-specific default content type:
9793

@@ -101,12 +97,12 @@ import sttp.model.MediaType
10197
case class Person(name: String, surname: String, age: Int)
10298

10399
// for this example, assuming names/surnames can't contain commas
104-
implicit val personSerializer: BodySerializer[Person] = { p: Person =>
100+
def serializePerson(p: Person): BasicBody = {
105101
val serialized = s"${p.name},${p.surname},${p.age}"
106102
StringBody(serialized, "UTF-8", MediaType.TextCsv)
107103
}
108104

109-
basicRequest.body(Person("mary", "smith", 67))
105+
basicRequest.body(serializePerson(Person("mary", "smith", 67)))
110106
```
111107

112-
See the implementations of the `BasicRequestBody` trait for more options.
108+
See the implementations of the `BasicBody` trait for more options.

docs/xml.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ After code generation, create the `SttpScalaxbApi` trait (or trait with another
1212
import generated.defaultScope // import may differ depending on location of generated code
1313
import scalaxb.`package`.{fromXML, toXML} // import may differ depending on location of generated code
1414
import scalaxb.{CanWriteXML, XMLFormat} // import may differ depending on location of generated code
15-
import sttp.client4.{BodySerializer, ResponseAs, ResponseException, StringBody, asString}
15+
import sttp.client4.{ResponseAs, ResponseException, StringBody, asString}
1616
import sttp.model.MediaType
1717

1818
import scala.xml.{NodeSeq, XML}
1919

2020
trait SttpScalaxbApi {
2121
case class XmlElementLabel(label: String)
2222

23-
implicit def scalaxbBodySerializer[B](implicit format: CanWriteXML[B], label: XmlElementLabel): BodySerializer[B] = { (b: B) =>
23+
def asXml[B](b: B)(implicit format: CanWriteXML[B], label: XmlElementLabel): StringBody = {
2424
val nodeSeq: NodeSeq = toXML[B](obj = b, elementLabel = label.label, scope = defaultScope)
2525
StringBody(nodeSeq.toString(), "utf-8", MediaType.ApplicationXml)
2626
}
@@ -38,18 +38,19 @@ trait SttpScalaxbApi {
3838
.showAs("either(as string, as xml)")
3939
}
4040
```
41-
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.
4241

42+
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.
43+
44+
Next to this trait, you might want to introduce `sttpScalaxb` package object to simplify imports.
4345

44-
Next to this trait, you might want to introduce `sttpScalaxb`
45-
package object to simplify imports.
4646
```scala
4747
package object sttpScalaxb extends SttpScalaxbApi
4848
```
4949

5050
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.
5151

5252
Usage example:
53+
5354
```scala
5455
val backend: SyncBackend = DefaultSyncBackend()
5556
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
6162
val response: Response[Either[ResponseException[String, Exception], Outer]] =
6263
basicRequest
6364
.post(uri"...")
64-
.body(requestPayload)
65+
.body(asXml(requestPayload))
6566
.response(asXml[Outer])
6667
.send(backend)
6768
```

0 commit comments

Comments
 (0)