Skip to content

Commit 3956457

Browse files
committed
Move .getRight, .mapLeft etc. to ResponseAs for better discoverability
1 parent 492bba7 commit 3956457

File tree

15 files changed

+115
-74
lines changed

15 files changed

+115
-74
lines changed

async-http-client-backend/zio/src/test/scala/sttp/client4/asynchttpclient/zio/AsyncHttpClientZioHttpTest.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ package sttp.client4.asynchttpclient.zio
33
import sttp.client4._
44
import sttp.client4.asynchttpclient.AsyncHttpClientHttpTest
55
import sttp.client4.impl.zio.ZioTestBase
6-
import sttp.client4.testing.{ConvertToFuture, HttpTest}
7-
import zio.{Task, ZIO}
6+
import sttp.client4.testing.ConvertToFuture
7+
import zio.Task
8+
import zio.ZIO
89

910
class AsyncHttpClientZioHttpTest extends AsyncHttpClientHttpTest[Task] with ZioTestBase {
1011

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

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -41,47 +41,71 @@ trait ResponseAsDelegate[+T, -R] {
4141
* Target type as which the response will be read.
4242
*/
4343
case class ResponseAs[+T](delegate: GenericResponseAs[T, Any]) extends ResponseAsDelegate[T, Any] {
44+
45+
/** Applies the given function `f` to the deserialized value `T`. */
4446
def map[T2](f: T => T2): ResponseAs[T2] = ResponseAs(delegate.mapWithMetadata { case (t, _) => f(t) })
47+
48+
/** Applies the given function `f` to the deserialized value `T`, and the response's metadata. */
4549
def mapWithMetadata[T2](f: (T, ResponseMetadata) => T2): ResponseAs[T2] = ResponseAs(delegate.mapWithMetadata(f))
4650

51+
/** If the type to which the response body should be deserialized is an `Either[A, B]`, applies the given function `f`
52+
* to `Left` values.
53+
*
54+
* Because of type inference, the type of `f` must be fully provided, e.g.
55+
*
56+
* ```
57+
* asString.mapLeft((s: String) => new CustomHttpError(s))`
58+
* ```
59+
*/
60+
def mapLeft[A, B, A2](f: A => A2)(implicit tIsEither: T <:< Either[A, B]): ResponseAs[Either[A2, B]] = map(
61+
_.left.map(f)
62+
)
63+
64+
/** If the type to which the response body should be deserialized is an `Either[A, B]`, applies the given function `f`
65+
* to `Right` values.
66+
*
67+
* Because of type inference, the type of `f` must be fully provided, e.g.
68+
*
69+
* ```
70+
* asString.mapRight((s: String) => parse(s))`
71+
* ```
72+
*/
73+
def mapRight[A, B, B2](f: B => B2)(implicit tIsEither: T <:< Either[A, B]): ResponseAs[Either[A, B2]] = map(
74+
_.right.map(f)
75+
)
76+
77+
/** If the type to which the response body should be deserialized is an `Either[A, B]`:
78+
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is not
79+
* yet an exception)
80+
* - in case of `B`, returns the value directly
81+
*/
82+
def getRight[A, B](implicit tIsEither: T <:< Either[A, B]): ResponseAs[B] =
83+
mapWithMetadata { case (t, meta) =>
84+
(t: Either[A, B]) match {
85+
case Left(a: Exception) => throw a
86+
case Left(a) => throw HttpError(a, meta.code)
87+
case Right(b) => b
88+
}
89+
}
90+
91+
/** If the type to which the response body should be deserialized is an `Either[ResponseException[HE, DE], B]`, either
92+
* throws the [[DeserializationException]], returns the deserialized body from the [[HttpError]], or the deserialized
93+
* successful body `B`.
94+
*/
95+
def getEither[HE, DE, B](implicit
96+
tIsEither: T <:< Either[ResponseException[HE, DE], B]
97+
): ResponseAs[Either[HE, B]] = map { t =>
98+
(t: Either[ResponseException[HE, DE], B]) match {
99+
case Left(HttpError(he, _)) => Left(he)
100+
case Left(d: DeserializationException[_]) => throw d
101+
case Right(b) => Right(b)
102+
}
103+
}
104+
47105
def showAs(s: String): ResponseAs[T] = ResponseAs(delegate.showAs(s))
48106
}
49107

50108
object ResponseAs {
51-
implicit class RichResponseAsEither[A, B](ra: ResponseAs[Either[A, B]]) {
52-
def mapLeft[L2](f: A => L2): ResponseAs[Either[L2, B]] = ra.map(_.left.map(f))
53-
def mapRight[R2](f: B => R2): ResponseAs[Either[A, R2]] = ra.map(_.right.map(f))
54-
55-
/** If the type to which the response body should be deserialized is an `Either[A, B]`:
56-
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is
57-
* not yet an exception)
58-
* - in case of `B`, returns the value directly
59-
*/
60-
def getRight: ResponseAs[B] =
61-
ra.mapWithMetadata { case (t, meta) =>
62-
t match {
63-
case Left(a: Exception) => throw a
64-
case Left(a) => throw HttpError(a, meta.code)
65-
case Right(b) => b
66-
}
67-
}
68-
}
69-
70-
implicit class RichResponseAsEitherResponseException[HE, DE, B](
71-
ra: ResponseAs[Either[ResponseException[HE, DE], B]]
72-
) {
73-
74-
/** If the type to which the response body should be deserialized is an `Either[ResponseException[HE, DE], B]`,
75-
* either throws the [[DeserializationException]], returns the deserialized body from the [[HttpError]], or the
76-
* deserialized successful body `B`.
77-
*/
78-
def getEither: ResponseAs[Either[HE, B]] =
79-
ra.map {
80-
case Left(HttpError(he, _)) => Left(he)
81-
case Left(d: DeserializationException[_]) => throw d
82-
case Right(b) => Right(b)
83-
}
84-
}
85109

86110
/** Returns a function, which maps `Left` values to [[HttpError]] s, and attempts to deserialize `Right` values using
87111
* the given function, catching any exceptions and representing them as [[DeserializationException]] s.

core/src/test/scala/sttp/client4/testing/BackendStubTests.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class BackendStubTests extends AnyFlatSpec with Matchers with ScalaFutures {
6060
val backend = testingStub
6161
val r = basicRequest
6262
.get(uri"http://example.org/d?p=v")
63-
.response(asString.mapRight(_.toInt))
63+
.response(asString.mapRight((_: String).toInt))
6464
.send(backend)
6565
r.body should be(Right(10))
6666
}
@@ -253,7 +253,7 @@ class BackendStubTests extends AnyFlatSpec with Matchers with ScalaFutures {
253253
val backend = BackendStub.synchronous.whenAnyRequest.thenRespond("1234")
254254
basicRequest
255255
.get(uri"http://example.org")
256-
.response(asBoth(asString.mapRight(_.toInt), asStringAlways))
256+
.response(asBoth(asString.mapRight((_: String).toInt), asStringAlways))
257257
.send(backend)
258258
.body shouldBe ((Right(1234), "1234"))
259259
}
@@ -452,8 +452,8 @@ class BackendStubTests extends AnyFlatSpec with Matchers with ScalaFutures {
452452
(s.getBytes(Utf8), asString(Utf8), Some(Right(s))),
453453
(new ByteArrayInputStream(s.getBytes(Utf8)), asString(Utf8), Some(Right(s))),
454454
(10, asString(Utf8), None),
455-
("10", asString(Utf8).mapRight(_.toInt), Some(Right(10))),
456-
(11, asString(Utf8).mapRight(_.toInt), None),
455+
("10", asString(Utf8).mapRight((_: String).toInt), Some(Right(10))),
456+
(11, asString(Utf8).mapRight((_: String).toInt), None),
457457
((), asString(Utf8), Some(Right("")))
458458
)
459459

core/src/test/scala/sttp/client4/testing/HttpTest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ trait HttpTest[F[_]]
8383
"as string with mapping using map" in {
8484
postEcho
8585
.body(testBody)
86-
.response(asString.mapRight(_.length))
86+
.response(asString.mapRight((_: String).length))
8787
.send(backend)
8888
.toFuture()
8989
.map(response => response.body should be(Right(expectedPostEchoResponse.length)))
@@ -172,7 +172,7 @@ trait HttpTest[F[_]]
172172
"as both string and mapped string" in {
173173
postEcho
174174
.body(testBody)
175-
.response(asBoth(asStringAlways, asByteArray.mapRight(_.length)))
175+
.response(asBoth(asStringAlways, asByteArray.mapRight((_: Array[Byte]).length)))
176176
.send(backend)
177177
.toFuture()
178178
.map { response =>

core/src/test/scalanative/sttp/client4/testing/SyncHttpTest.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ trait SyncHttpTest
5353
"as string with mapping using map" in {
5454
val response = postEcho
5555
.body(testBody)
56-
.response(asString.mapRight(_.length))
56+
.response(asString.mapRight((_: String).length))
5757
.send(backend)
5858
response.body should be(Right(expectedPostEchoResponse.length))
5959
}
@@ -119,7 +119,7 @@ trait SyncHttpTest
119119
"as both string and mapped string" in {
120120
val response = postEcho
121121
.body(testBody)
122-
.response(asBoth(asStringAlways, asByteArray.mapRight(_.length)))
122+
.response(asBoth(asStringAlways, asByteArray.mapRight((_: Array[Byte]).length)))
123123
.send(backend)
124124

125125
response.body shouldBe ((expectedPostEchoResponse, Right(expectedPostEchoResponse.getBytes.length)))
@@ -367,7 +367,7 @@ trait SyncHttpTest
367367
}
368368

369369
"redirect when redirects should be followed, and the response is parsed" in {
370-
val resp = r2.response(asString.mapRight(_.toInt)).send(backend)
370+
val resp = r2.response(asString.mapRight((_: String).toInt)).send(backend)
371371
resp.code shouldBe StatusCode.Ok
372372
resp.body should be(Right(r4response.toInt))
373373
}

docs/json.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Each integration is available as an import, which brings `asJson` methods into s
77
The following variants of `asJson` methods are available:
88

99
* `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])`
10+
* `asJson[B]` - specifies that the body should be deserialized to json, but only if the response is successful (2xx); should be used to specify how a response should be handled, e.g. `basicRequest.response(asJson[T])`
1111
* `asJsonAlways[B]` - specifies that the body should be deserialized to json, regardless of the status code
1212
* `asJsonEither[E, B]` - specifies that the body should be deserialized to json, using different deserializers for error and successful (2xx) responses
1313

docs/responses/body.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ As an example, to read the response body as an int, the following response descr
110110
```scala mdoc:compile-only
111111
import sttp.client4._
112112

113-
val asInt: ResponseAs[Either[String, Int]] = asString.mapRight(_.toInt)
113+
val asInt: ResponseAs[Either[String, Int]] = asString.mapRight((_: String).toInt)
114114

115115
basicRequest
116116
.get(uri"http://example.com")

json/circe/src/main/scala/sttp/client4/circe/SttpCirceApi.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ trait SttpCirceApi {
3939
*/
4040
def asJsonEither[E: Decoder: IsOption, B: Decoder: IsOption]
4141
: ResponseAs[Either[ResponseException[E, io.circe.Error], B]] =
42-
asJson[B].mapLeft {
43-
case HttpError(e, code) => deserializeJson[E].apply(e).fold(DeserializationException(e, _), HttpError(_, code))
44-
case de @ DeserializationException(_, _) => de
42+
asJson[B].mapLeft { (l: ResponseException[String, io.circe.Error]) =>
43+
l match {
44+
case HttpError(e, code) => deserializeJson[E].apply(e).fold(DeserializationException(e, _), HttpError(_, code))
45+
case de @ DeserializationException(_, _) => de
46+
}
4547
}.showAsJsonEither
4648

4749
def deserializeJson[B: Decoder: IsOption]: String => Either[io.circe.Error, B] =

json/json4s/src/main/scala/sttp/client4/json4s/SttpJson4sApi.scala

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ trait SttpJson4sApi {
4646
formats: Formats,
4747
serialization: Serialization
4848
): ResponseAs[Either[ResponseException[E, Exception], B]] =
49-
asJson[B].mapLeft {
50-
case HttpError(e, code) =>
51-
ResponseAs.deserializeCatchingExceptions(deserializeJson[E])(e).fold(identity, HttpError(_, code))
52-
case de @ DeserializationException(_, _) => de
49+
asJson[B].mapLeft { (l: ResponseException[String, Exception]) =>
50+
l match {
51+
case HttpError(e, code) =>
52+
ResponseAs.deserializeCatchingExceptions(deserializeJson[E])(e).fold(identity, HttpError(_, code))
53+
case de @ DeserializationException(_, _) => de
54+
}
5355
}.showAsJsonEither
5456

5557
def deserializeJson[B: Manifest](implicit

json/jsoniter/src/main/scala/sttp/client4/jsoniter/SttpJsoniterJsonApi.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ trait SttpJsoniterJsonApi {
4747
E: JsonValueCodec: IsOption,
4848
B: JsonValueCodec: IsOption
4949
]: ResponseAs[Either[ResponseException[E, Exception], B]] =
50-
asJson[B].mapLeft {
51-
case de @ DeserializationException(_, _) => de
52-
case HttpError(e, code) => deserializeJson[E].apply(e).fold(DeserializationException(e, _), HttpError(_, code))
50+
asJson[B].mapLeft { (l: ResponseException[String, Exception]) =>
51+
l match {
52+
case de @ DeserializationException(_, _) => de
53+
case HttpError(e, code) => deserializeJson[E].apply(e).fold(DeserializationException(e, _), HttpError(_, code))
54+
}
5355
}.showAsJsonEither
5456

5557
def deserializeJson[B: JsonValueCodec: IsOption]: String => Either[Exception, B] = { (s: String) =>

0 commit comments

Comments
 (0)