Skip to content

Commit e4d8d95

Browse files
committed
Add asJsonOrFail methods
1 parent 62ead1f commit e4d8d95

File tree

27 files changed

+591
-203
lines changed

27 files changed

+591
-203
lines changed

build.sbt

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -800,7 +800,8 @@ lazy val armeriaZioBackend =
800800
//----- json
801801
lazy val jsonCommon = (projectMatrix in (file("json/common")))
802802
.settings(
803-
name := "json-common"
803+
name := "json-common",
804+
scalaTest
804805
)
805806
.jvmPlatform(
806807
scalaVersions = scala2 ++ scala3,
@@ -826,7 +827,7 @@ lazy val circe = (projectMatrix in file("json/circe"))
826827
)
827828
.jsPlatform(scalaVersions = scala2 ++ scala3, settings = commonJsSettings)
828829
.nativePlatform(scalaVersions = scala2 ++ scala3, settings = commonNativeSettings)
829-
.dependsOn(core, jsonCommon)
830+
.dependsOn(core, jsonCommon % compileAndTest)
830831

831832
lazy val jsoniter = (projectMatrix in file("json/jsoniter"))
832833
.settings(
@@ -842,7 +843,7 @@ lazy val jsoniter = (projectMatrix in file("json/jsoniter"))
842843
settings = commonJvmSettings
843844
)
844845
.jsPlatform(scalaVersions = scala2 ++ scala3, settings = commonJsSettings)
845-
.dependsOn(core, jsonCommon)
846+
.dependsOn(core, jsonCommon % compileAndTest)
846847

847848
lazy val zioJson = (projectMatrix in file("json/zio-json"))
848849
.settings(
@@ -858,7 +859,7 @@ lazy val zioJson = (projectMatrix in file("json/zio-json"))
858859
settings = commonJvmSettings
859860
)
860861
.jsPlatform(scalaVersions = scala2 ++ scala3, settings = commonJsSettings)
861-
.dependsOn(core, jsonCommon)
862+
.dependsOn(core, jsonCommon % compileAndTest)
862863

863864
lazy val zio1Json = (projectMatrix in file("json/zio1-json"))
864865
.settings(
@@ -874,7 +875,7 @@ lazy val zio1Json = (projectMatrix in file("json/zio1-json"))
874875
settings = commonJvmSettings
875876
)
876877
.jsPlatform(scalaVersions = scala2 ++ scala3, settings = commonJsSettings)
877-
.dependsOn(core, jsonCommon)
878+
.dependsOn(core, jsonCommon % compileAndTest)
878879

879880
lazy val tethysJson = (projectMatrix in file("json/tethys-json"))
880881
.settings(
@@ -890,7 +891,7 @@ lazy val tethysJson = (projectMatrix in file("json/tethys-json"))
890891
scalaVersions = scala2 ++ scala3,
891892
settings = commonJvmSettings
892893
)
893-
.dependsOn(core, jsonCommon)
894+
.dependsOn(core, jsonCommon % compileAndTest)
894895

895896
lazy val upickle = (projectMatrix in file("json/upickle"))
896897
.settings(
@@ -908,7 +909,7 @@ lazy val upickle = (projectMatrix in file("json/upickle"))
908909
)
909910
.jsPlatform(scalaVersions = scala2 ++ scala3, settings = commonJsSettings)
910911
.nativePlatform(scalaVersions = scala2 ++ scala3, settings = commonNativeSettings)
911-
.dependsOn(core, jsonCommon)
912+
.dependsOn(core, jsonCommon % compileAndTest)
912913

913914
lazy val json4sVersion = "4.0.7"
914915

@@ -923,7 +924,7 @@ lazy val json4s = (projectMatrix in file("json/json4s"))
923924
scalaTest
924925
)
925926
.jvmPlatform(scalaVersions = scala2 ++ scala3)
926-
.dependsOn(core, jsonCommon)
927+
.dependsOn(core, jsonCommon % compileAndTest)
927928

928929
lazy val sprayJson = (projectMatrix in file("json/spray-json"))
929930
.settings(commonJvmSettings)
@@ -935,7 +936,7 @@ lazy val sprayJson = (projectMatrix in file("json/spray-json"))
935936
scalaTest
936937
)
937938
.jvmPlatform(scalaVersions = scala2 ++ scala3)
938-
.dependsOn(core, jsonCommon)
939+
.dependsOn(core, jsonCommon % compileAndTest)
939940

940941
lazy val play29Json = (projectMatrix in file("json/play29-json"))
941942
.settings(
@@ -952,7 +953,7 @@ lazy val play29Json = (projectMatrix in file("json/play29-json"))
952953
settings = commonJvmSettings
953954
)
954955
.jsPlatform(scalaVersions = scala2, settings = commonJsSettings)
955-
.dependsOn(core, jsonCommon)
956+
.dependsOn(core, jsonCommon % compileAndTest)
956957

957958
lazy val playJson = (projectMatrix in file("json/play-json"))
958959
.settings(
@@ -967,7 +968,7 @@ lazy val playJson = (projectMatrix in file("json/play-json"))
967968
settings = commonJvmSettings
968969
)
969970
.jsPlatform(scalaVersions = scala2 ++ scala3, settings = commonJsSettings)
970-
.dependsOn(core, jsonCommon)
971+
.dependsOn(core, jsonCommon % compileAndTest)
971972

972973
lazy val prometheusBackend = (projectMatrix in file("observability/prometheus-backend"))
973974
.settings(commonJvmSettings)

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,30 @@ object ResponseAs {
174174
case Left(e) => throw DeserializationException(s, e)
175175
case Right(b) => b
176176
}
177+
178+
/** Converts deserialization functions, which both return errors of type `E`, into a function where errors are thrown
179+
* as exceptions, and results are parsed using either of the functions, depending if the response was successfull, or
180+
* not.
181+
*/
182+
def deserializeEitherWithErrorOrThrow[E: ShowError, T, T2](
183+
doDeserializeHttpError: String => Either[E, T],
184+
doDeserializeHttpSuccess: String => Either[E, T2]
185+
): (String, ResponseMetadata) => Either[T, T2] =
186+
(s, m) =>
187+
if (m.isSuccess) Right(deserializeOrThrow(doDeserializeHttpSuccess).apply(s))
188+
else Left(deserializeOrThrow(doDeserializeHttpError).apply(s))
189+
190+
/** Converts deserialization functions, which both throw exceptions upon errors, into a function where errors still
191+
* thrown as exceptions, and results are parsed using either of the functions, depending if the response was
192+
* successfull, or not.
193+
*/
194+
def deserializeEitherOrThrow[T, T2](
195+
doDeserializeHttpError: String => T,
196+
doDeserializeHttpSuccess: String => T2
197+
): (String, ResponseMetadata) => Either[T, T2] =
198+
(s, m) =>
199+
if (m.isSuccess) Right(doDeserializeHttpSuccess(s))
200+
else Left(doDeserializeHttpError(s))
177201
}
178202

179203
/** Describes how the response body of a [[StreamRequest]] should be handled.

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,10 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
9797
* effect. Use the `utf-8` charset by default, unless specified otherwise in the response headers.
9898
*
9999
* @see
100-
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
100+
* the [[ResponseAs#orFail]] method can be used to convert any response description which returns an `Either` into
101101
* an exception-throwing variant.
102102
*/
103-
def asStringOrFail: ResponseAs[String] = asString.orFail
103+
def asStringOrFail: ResponseAs[String] = asString.orFail.showAs("as string or fail")
104104

105105
/** Reads the response as either a string (for non-2xx responses), or otherwise as an array of bytes (without any
106106
* processing). The entire response is loaded into memory.
@@ -116,10 +116,10 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
116116
* [[HttpError]] / returns a failed effect.
117117
*
118118
* @see
119-
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
119+
* the [[ResponseAs#orFail]] method can be used to convert any response description which returns an `Either` into
120120
* an exception-throwing variant.
121121
*/
122-
def asByteArrayOrFail: ResponseAs[Array[Byte]] = asByteArray.orFail
122+
def asByteArrayOrFail: ResponseAs[Array[Byte]] = asByteArray.orFail.showAs("as byte array or fail")
123123

124124
/** Deserializes the response as either a string (for non-2xx responses), or otherwise as form parameters. Uses the
125125
* `utf-8` charset by default, unless specified otherwise in the response headers.
@@ -149,10 +149,10 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
149149
* returns a failed effect. Uses the `utf-8` charset by default, unless specified otherwise in the response headers.
150150
*
151151
* @see
152-
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
152+
* the [[ResponseAs#orFail]] method can be used to convert any response description which returns an `Either` into
153153
* an exception-throwing variant.
154154
*/
155-
def asParamsOrFail: ResponseAs[String] = asString.orFail
155+
def asParamsOrFail: ResponseAs[String] = asString.orFail.showAs("as params or fail")
156156

157157
private[client4] def asSttpFile(file: SttpFile): ResponseAs[SttpFile] = ResponseAs(ResponseAsFile(file))
158158

@@ -287,12 +287,12 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
287287
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
288288
*
289289
* @see
290-
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
290+
* the [[ResponseAs#orFail]] method can be used to convert any response description which returns an `Either` into
291291
* an exception-throwing variant.
292292
*/
293293
def asStreamOrFail[F[_], T, S](s: Streams[S])(
294294
f: s.BinaryStream => F[T]
295-
): StreamResponseAs[T, S with Effect[F]] = asStream(s)(f).orFail
295+
): StreamResponseAs[T, S with Effect[F]] = asStream(s)(f).orFail.showAs("as stream or fail")
296296

297297
/** Handles the response body by either reading a string (for non-2xx responses), or otherwise providing a stream with
298298
* the response's data, along with the response metadata, to `f`. The effect type used by `f` must be compatible with

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ trait SttpWebSocketAsyncApi {
2121
* closed after `f` completes.
2222
*
2323
* @see
24-
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
24+
* the [[ResponseAs#orFail]] method can be used to convert any response description which returns an `Either` into
2525
* an exception-throwing variant.
2626
*/
27-
def asWebSocketOrFail[F[_], T](f: WebSocket[F] => F[T]): WebSocketResponseAs[F, T] = asWebSocket(f).orFail
27+
def asWebSocketOrFail[F[_], T](f: WebSocket[F] => F[T]): WebSocketResponseAs[F, T] =
28+
asWebSocket(f).orFail.showAs("as web socket or fail")
2829

2930
/** Handles the response body by either reading a string (for non-2xx responses), or otherwise providing an open
3031
* [[WebSocket]] instance, along with the response metadata, to the `f` function.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ trait SttpWebSocketStreamApi {
2828
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
2929
*
3030
* @see
31-
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
31+
* the [[ResponseAs#orFail]] method can be used to convert any response description which returns an `Either` into
3232
* an exception-throwing variant.
3333
*/
3434
def asWebSocketStreamOrFail[S](
3535
s: Streams[S]
3636
)(p: s.Pipe[WebSocketFrame.Data[_], WebSocketFrame]): WebSocketStreamResponseAs[Unit, S] =
37-
asWebSocketStream(s)(p).orFail
37+
asWebSocketStream(s)(p).orFail.showAs("as web socket stream or fail")
3838

3939
/** Handles the response body by using the given `p` stream processing pipe to handle the incoming & produce the
4040
* outgoing web socket frames, regardless of the status code.

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ trait SttpWebSocketSyncApi {
2121
* The web socket is always closed after `f` completes.
2222
*
2323
* @see
24-
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
24+
* the [[ResponseAs#orFail]] method can be used to convert any response description which returns an `Either` into
2525
* an exception-throwing variant.
2626
*/
27-
def asWebSocketOrFail[T](f: SyncWebSocket => T): WebSocketResponseAs[Identity, T] = asWebSocket(f).orFail
27+
def asWebSocketOrFail[T](f: SyncWebSocket => T): WebSocketResponseAs[Identity, T] =
28+
asWebSocket(f).orFail.showAs("as web socket or fail")
2829

2930
/** Handles the response body by either reading a string (for non-2xx responses), or otherwise providing an open
3031
* [[WebSocket]] instance, along with the response metadata, to the `f` function.

docs/json.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,27 @@ Each integration is available as an import, which brings `asJson` methods into s
66

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

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); should be used to specify how a response should be handled, e.g. `basicRequest.response(asJson[T])`
9+
* `asJson(b: B)` - to be used when specifying the body of a request: serializes the body so that it can be used as a request's body, e.g. using `basicRequest.body(asJson(myValue))`
10+
* `asJson[B]` - to be used when specifying how the response body should be handled: specifies that the body should be deserialized to json, but only if the response is successful (2xx); otherwise, a `Left` is returned, with body as a string
11+
* `asJsonOrFail[B]` - specifies that the body should be deserialized to json, if the response is successful (2xx); throws an exception/returns a failed effect if the response code is other than 2xx, or if deserialization fails
1112
* `asJsonAlways[B]` - specifies that the body should be deserialized to json, regardless of the status code
1213
* `asJsonEither[E, B]` - specifies that the body should be deserialized to json, using different deserializers for error and successful (2xx) responses
14+
* `asJsonEitherOrFail[E, B]` - specifies that the body should be deserialized to json, using different deserializers for error and successful (2xx) responses; throws an exception/returns a failed effect, if deserialization fails
1315

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

1618
```scala mdoc:compile-only
1719
import sttp.client4._
1820

21+
// request bodies
1922
def asJson[B](b: B): StringBody = ???
23+
24+
// response handling description
2025
def asJson[B]: ResponseAs[Either[ResponseException[String, Exception], B]] = ???
26+
def asJsonOrFail[B]: ResponseAs[B] = ???
2127
def asJsonAlways[B]: ResponseAs[Either[DeserializationException[Exception], B]] = ???
2228
def asJsonEither[E, B]: ResponseAs[Either[ResponseException[E, Exception], B]] = ???
29+
def asJsonEitherOrFail[E, B]: ResponseAs[Either[E, B]] = ???
2330
```
2431

2532
The response specifications can be further refined using `.orFail` and `.orFailDeserialization`, see [response body specifications](responses/body.md).

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import io.circe.{Decoder, Encoder, Printer}
66
import sttp.client4.internal.Utf8
77
import sttp.model.MediaType
88
import sttp.client4.json._
9+
import sttp.client4.ResponseAs.deserializeEitherWithErrorOrThrow
910

1011
trait SttpCirceApi {
1112

@@ -24,6 +25,12 @@ trait SttpCirceApi {
2425
def asJson[B: Decoder: IsOption]: ResponseAs[Either[ResponseException[String, io.circe.Error], B]] =
2526
asString.mapWithMetadata(ResponseAs.deserializeRightWithError(deserializeJson)).showAsJson
2627

28+
/** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Otherwise, if the
29+
* response code is other than 2xx, or a deserialization error occurs, throws an [[ResponseException]] / returns a
30+
* failed effect.
31+
*/
32+
def asJsonOrFail[B: Decoder: IsOption]: ResponseAs[B] = asJson[B].orFail.showAsJsonOrFail
33+
2734
/** Tries to deserialize the body from a string into JSON, regardless of the response code. Returns:
2835
* - `Right(b)` if the parsing was successful
2936
* - `Left(DeserializationException)` if there's an error during deserialization
@@ -46,6 +53,14 @@ trait SttpCirceApi {
4653
}
4754
}.showAsJsonEither
4855

56+
/** Deserializes the body from a string into JSON, using different deserializers depending on the status code. If a
57+
* deserialization error occurs, throws a [[DeserializationException]] / returns a failed effect.
58+
*/
59+
def asJsonEitherOrFail[E: Decoder: IsOption, B: Decoder: IsOption]: ResponseAs[Either[E, B]] =
60+
asStringAlways
61+
.mapWithMetadata(deserializeEitherWithErrorOrThrow(deserializeJson[E], deserializeJson[B]))
62+
.showAsJsonEitherOrFail
63+
4964
def deserializeJson[B: Decoder: IsOption]: String => Either[io.circe.Error, B] =
5065
JsonInput.sanitize[B].andThen(decode[B])
5166
}

0 commit comments

Comments
 (0)