Skip to content

Commit 62ead1f

Browse files
committed
Add as...OrFail response-as variants
1 parent ae08044 commit 62ead1f

File tree

7 files changed

+304
-12
lines changed

7 files changed

+304
-12
lines changed

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,29 @@ object ResponseAs {
192192
* [[ResponseAs]]
193193
*/
194194
case class StreamResponseAs[+T, S](delegate: GenericResponseAs[T, S]) extends ResponseAsDelegate[T, S] {
195+
196+
/** Applies the given function `f` to the deserialized value `T`. */
195197
def map[T2](f: T => T2): StreamResponseAs[T2, S] =
196198
StreamResponseAs(delegate.mapWithMetadata { case (t, _) => f(t) })
199+
200+
/** Applies the given function `f` to the deserialized value `T`, and the response's metadata. */
197201
def mapWithMetadata[T2](f: (T, ResponseMetadata) => T2): StreamResponseAs[T2, S] =
198202
StreamResponseAs(delegate.mapWithMetadata(f))
199203

204+
/** If the type to which the response body should be deserialized is an `Either[A, B]`:
205+
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is not
206+
* yet an exception)
207+
* - in case of `B`, returns the value directly
208+
*/
209+
def orFail[A, B](implicit tIsEither: T <:< Either[A, B]): StreamResponseAs[B, S] =
210+
mapWithMetadata { case (t, meta) =>
211+
(t: Either[A, B]) match {
212+
case Left(a: Exception) => throw a
213+
case Left(a) => throw HttpError(a, meta.code)
214+
case Right(b) => b
215+
}
216+
}
217+
200218
def showAs(s: String): StreamResponseAs[T, S] = new StreamResponseAs(delegate.showAs(s))
201219
}
202220

@@ -215,11 +233,29 @@ case class StreamResponseAs[+T, S](delegate: GenericResponseAs[T, S]) extends Re
215233
*/
216234
case class WebSocketResponseAs[F[_], +T](delegate: GenericResponseAs[T, Effect[F] with WebSockets])
217235
extends ResponseAsDelegate[T, Effect[F] with WebSockets] {
236+
237+
/** Applies the given function `f` to the deserialized value `T`. */
218238
def map[T2](f: T => T2): WebSocketResponseAs[F, T2] =
219239
WebSocketResponseAs(delegate.mapWithMetadata { case (t, _) => f(t) })
240+
241+
/** Applies the given function `f` to the deserialized value `T`, and the response's metadata. */
220242
def mapWithMetadata[T2](f: (T, ResponseMetadata) => T2): WebSocketResponseAs[F, T2] =
221243
WebSocketResponseAs(delegate.mapWithMetadata(f))
222244

245+
/** If the type to which the response body should be deserialized is an `Either[A, B]`:
246+
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is not
247+
* yet an exception)
248+
* - in case of `B`, returns the value directly
249+
*/
250+
def orFail[A, B](implicit tIsEither: T <:< Either[A, B]): WebSocketResponseAs[F, B] =
251+
mapWithMetadata { case (t, meta) =>
252+
(t: Either[A, B]) match {
253+
case Left(a: Exception) => throw a
254+
case Left(a) => throw HttpError(a, meta.code)
255+
case Right(b) => b
256+
}
257+
}
258+
223259
def showAs(s: String): WebSocketResponseAs[F, T] = new WebSocketResponseAs(delegate.showAs(s))
224260
}
225261

@@ -238,11 +274,29 @@ case class WebSocketResponseAs[F[_], +T](delegate: GenericResponseAs[T, Effect[F
238274
*/
239275
case class WebSocketStreamResponseAs[+T, S](delegate: GenericResponseAs[T, S with WebSockets])
240276
extends ResponseAsDelegate[T, S with WebSockets] {
277+
278+
/** Applies the given function `f` to the deserialized value `T`. */
241279
def map[T2](f: T => T2): WebSocketStreamResponseAs[T2, S] =
242280
WebSocketStreamResponseAs[T2, S](delegate.mapWithMetadata { case (t, _) => f(t) })
281+
282+
/** Applies the given function `f` to the deserialized value `T`, and the response's metadata. */
243283
def mapWithMetadata[T2](f: (T, ResponseMetadata) => T2): WebSocketStreamResponseAs[T2, S] =
244284
WebSocketStreamResponseAs[T2, S](delegate.mapWithMetadata(f))
245285

286+
/** If the type to which the response body should be deserialized is an `Either[A, B]`:
287+
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is not
288+
* yet an exception)
289+
* - in case of `B`, returns the value directly
290+
*/
291+
def orFail[A, B](implicit tIsEither: T <:< Either[A, B]): WebSocketStreamResponseAs[B, S] =
292+
mapWithMetadata { case (t, meta) =>
293+
(t: Either[A, B]) match {
294+
case Left(a: Exception) => throw a
295+
case Left(a) => throw HttpError(a, meta.code)
296+
case Right(b) => b
297+
}
298+
}
299+
246300
def showAs(s: String): WebSocketStreamResponseAs[T, S] = new WebSocketStreamResponseAs[T, S](delegate.showAs(s))
247301
}
248302

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

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,16 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
9393
}
9494
.showAs("as string")
9595

96-
/** Reads the response as either a string (for non-2xx responses), or othweise as an array of bytes (without any
96+
/** Reads the response as a `String`, if the status code is 2xx. Otherwise, throws an [[HttpError]] / returns a failed
97+
* effect. Use the `utf-8` charset by default, unless specified otherwise in the response headers.
98+
*
99+
* @see
100+
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
101+
* an exception-throwing variant.
102+
*/
103+
def asStringOrFail: ResponseAs[String] = asString.orFail
104+
105+
/** Reads the response as either a string (for non-2xx responses), or otherwise as an array of bytes (without any
97106
* processing). The entire response is loaded into memory.
98107
*/
99108
def asByteArray: ResponseAs[Either[String, Array[Byte]]] = asEither(asStringAlways, asByteArrayAlways)
@@ -103,6 +112,15 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
103112
*/
104113
def asByteArrayAlways: ResponseAs[Array[Byte]] = ResponseAs(ResponseAsByteArray)
105114

115+
/** Reads the response as an array of bytes, without any processing, if the status code is 2xx. Otherwise, throws an
116+
* [[HttpError]] / returns a failed effect.
117+
*
118+
* @see
119+
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
120+
* an exception-throwing variant.
121+
*/
122+
def asByteArrayOrFail: ResponseAs[Array[Byte]] = asByteArray.orFail
123+
106124
/** Deserializes the response as either a string (for non-2xx responses), or otherwise as form parameters. Uses the
107125
* `utf-8` charset by default, unless specified otherwise in the response headers.
108126
*/
@@ -127,6 +145,15 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
127145
asStringAlways(charset2).map(GenericResponseAs.parseParams(_, charset2)).showAs("as params")
128146
}
129147

148+
/** Deserializes the response as form parameters, if the status code is 2xx. Otherwise, throws an [[HttpError]] /
149+
* returns a failed effect. Uses the `utf-8` charset by default, unless specified otherwise in the response headers.
150+
*
151+
* @see
152+
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
153+
* an exception-throwing variant.
154+
*/
155+
def asParamsOrFail: ResponseAs[String] = asString.orFail
156+
130157
private[client4] def asSttpFile(file: SttpFile): ResponseAs[SttpFile] = ResponseAs(ResponseAsFile(file))
131158

132159
/** Uses the [[ResponseAs]] description that matches the condition (using the response's metadata).
@@ -243,7 +270,8 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
243270
// stream response specifications
244271

245272
/** Handles the response body by either reading a string (for non-2xx responses), or otherwise providing a stream with
246-
* the response's data to `f`. The stream is always closed after `f` completes.
273+
* the response's data to `f`. The effect type used by `f` must be compatible with the effect type of the backend.
274+
* The stream is always closed after `f` completes.
247275
*
248276
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
249277
*/
@@ -252,8 +280,23 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
252280
): StreamResponseAs[Either[String, T], S with Effect[F]] =
253281
asEither(asStringAlways, asStreamAlways(s)(f))
254282

283+
/** Handles the response body by providing a stream with the response's data to `f`, if the status code is 2xx.
284+
* Otherwise, returns a failed effect (with [[HttpError]]). The effect type used by `f` must be compatible with the
285+
* effect type of the backend. The stream is always closed after `f` completes.
286+
*
287+
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
288+
*
289+
* @see
290+
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
291+
* an exception-throwing variant.
292+
*/
293+
def asStreamOrFail[F[_], T, S](s: Streams[S])(
294+
f: s.BinaryStream => F[T]
295+
): StreamResponseAs[T, S with Effect[F]] = asStream(s)(f).orFail
296+
255297
/** Handles the response body by either reading a string (for non-2xx responses), or otherwise providing a stream with
256-
* the response's data, along with the response metadata, to `f`. The stream is always closed after `f` completes.
298+
* the response's data, along with the response metadata, to `f`. The effect type used by `f` must be compatible with
299+
* the effect type of the backend. The stream is always closed after `f` completes.
257300
*
258301
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
259302
*/
@@ -263,15 +306,17 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
263306
asEither(asStringAlways, asStreamAlwaysWithMetadata(s)(f))
264307

265308
/** Handles the response body by providing a stream with the response's data to `f`, regardless of the status code.
266-
* The stream is always closed after `f` completes.
309+
* The effect type used by `f` must be compatible with the effect type of the backend. The stream is always closed
310+
* after `f` completes.
267311
*
268312
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
269313
*/
270314
def asStreamAlways[F[_], T, S](s: Streams[S])(f: s.BinaryStream => F[T]): StreamResponseAs[T, S with Effect[F]] =
271315
asStreamAlwaysWithMetadata(s)((s, _) => f(s))
272316

273317
/** Handles the response body by providing a stream with the response's data, along with the response metadata, to
274-
* `f`, regardless of the status code. The stream is always closed after `f` completes.
318+
* `f`, regardless of the status code. The effect type used by `f` must be compatible with the effect type of the
319+
* backend. The stream is always closed after `f` completes.
275320
*
276321
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
277322
*/

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,90 @@ import sttp.model.ResponseMetadata
44
import sttp.ws.WebSocket
55

66
trait SttpWebSocketAsyncApi {
7+
8+
/** Handles the response body by either reading a string (for non-2xx responses), or otherwise providing an open
9+
* [[WebSocket]] instance to the `f` function.
10+
*
11+
* The effect type used by `f` must be compatible with the effect type of the backend. The web socket is always
12+
* closed after `f` completes.
13+
*/
714
def asWebSocket[F[_], T](f: WebSocket[F] => F[T]): WebSocketResponseAs[F, Either[String, T]] =
815
asWebSocketEither(asStringAlways, asWebSocketAlways(f))
916

17+
/** Handles the response as a web socket, providing an open [[WebSocket]] instance to the `f` function, if the status
18+
* code is 2xx. Otherwise, returns a failed effect (with [[HttpError]]).
19+
*
20+
* The effect type used by `f` must be compatible with the effect type of the backend. The web socket is always
21+
* closed after `f` completes.
22+
*
23+
* @see
24+
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
25+
* an exception-throwing variant.
26+
*/
27+
def asWebSocketOrFail[F[_], T](f: WebSocket[F] => F[T]): WebSocketResponseAs[F, T] = asWebSocket(f).orFail
28+
29+
/** Handles the response body by either reading a string (for non-2xx responses), or otherwise providing an open
30+
* [[WebSocket]] instance, along with the response metadata, to the `f` function.
31+
*
32+
* The effect type used by `f` must be compatible with the effect type of the backend. The web socket is always
33+
* closed after `f` completes.
34+
*/
1035
def asWebSocketWithMetadata[F[_], T](
1136
f: (WebSocket[F], ResponseMetadata) => F[T]
1237
): WebSocketResponseAs[F, Either[String, T]] =
1338
asWebSocketEither(asStringAlways, asWebSocketAlwaysWithMetadata(f))
1439

40+
/** Handles the response body by providing an open [[WebSocket]] instance to the `f` function, regardless of the
41+
* status code.
42+
*
43+
* The effect type used by `f` must be compatible with the effect type of the backend. The web socket is always
44+
* closed after `f` completes.
45+
*/
1546
def asWebSocketAlways[F[_], T](f: WebSocket[F] => F[T]): WebSocketResponseAs[F, T] =
1647
asWebSocketAlwaysWithMetadata((w, _) => f(w))
1748

49+
/** Handles the response body by providing an open [[WebSocket]] instance to the `f` function, along with the response
50+
* metadata, regardless of the status code.
51+
*
52+
* The effect type used by `f` must be compatible with the effect type of the backend. The web socket is always
53+
* closed after `f` completes.
54+
*/
1855
def asWebSocketAlwaysWithMetadata[F[_], T](f: (WebSocket[F], ResponseMetadata) => F[T]): WebSocketResponseAs[F, T] =
1956
WebSocketResponseAs(ResponseAsWebSocket(f))
2057

58+
/** Handles the response body by either reading a string (for non-2xx responses), or otherwise returning an open
59+
* [[WebSocket]] instance. It is the responsibility of the caller to consume & close the web socket.
60+
*
61+
* The effect type `F` must be compatible with the effect type of the backend.
62+
*/
2163
def asWebSocketUnsafe[F[_]]: WebSocketResponseAs[F, Either[String, WebSocket[F]]] =
2264
asWebSocketEither(asStringAlways, asWebSocketAlwaysUnsafe)
2365

66+
/** Handles the response body by returning an open [[WebSocket]] instance, regardless of the status code. It is the
67+
* responsibility of the caller to consume & close the web socket.
68+
*
69+
* The effect type `F` must be compatible with the effect type of the backend.
70+
*/
2471
def asWebSocketAlwaysUnsafe[F[_]]: WebSocketResponseAs[F, WebSocket[F]] =
2572
WebSocketResponseAs(ResponseAsWebSocketUnsafe())
2673

74+
/** Uses the [[ResponseAs]] description that matches the condition (using the response's metadata).
75+
*
76+
* This allows using different response description basing on the status code, for example. If none of the conditions
77+
* match, the default response handling description is used.
78+
*
79+
* The effect type `F` must be compatible with the effect type of the backend.
80+
*/
2781
def fromMetadata[F[_], T](
2882
default: ResponseAs[T],
2983
conditions: ConditionalResponseAs[WebSocketResponseAs[F, T]]*
3084
): WebSocketResponseAs[F, T] =
3185
WebSocketResponseAs(ResponseAsFromMetadata(conditions.map(_.map(_.delegate)).toList, default.delegate))
3286

33-
/** Uses the `onSuccess` response specification for 101 responses (switching protocols) on JVM/Native, 200 responses
34-
* on JS. Otherwise, use the `onError` specification.
87+
/** Uses the `onSuccess` response description for 101 responses (switching protocols) on JVM/Native, 200 responses on
88+
* JS. Otherwise, use the `onError` description.
89+
*
90+
* The effect type `F` must be compatible with the effect type of the backend.
3591
*/
3692
def asWebSocketEither[F[_], A, B](
3793
onError: ResponseAs[A],

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

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,53 @@ import sttp.model.StatusCode
55
import sttp.ws.WebSocketFrame
66

77
trait SttpWebSocketStreamApi {
8+
9+
/** Handles the response body by either reading a string (for non-2xx responses), or otherwise using the given `p`
10+
* stream processing pipe to handle the incoming & produce the outgoing web socket frames.
11+
*
12+
* The web socket is always closed after `p` completes.
13+
*
14+
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
15+
*/
816
def asWebSocketStream[S](
917
s: Streams[S]
1018
)(p: s.Pipe[WebSocketFrame.Data[_], WebSocketFrame]): WebSocketStreamResponseAs[Either[String, Unit], S] =
1119
asWebSocketEither(asStringAlways, asWebSocketStreamAlways(s)(p))
1220

21+
/** Handles the response as a web socket, using the given `p` stream processing pipe to handle the incoming & produce
22+
* the outgoing web socket frames, if the status code is 2xx. Otherwise, returns a failed effect (with
23+
* [[HttpError]]).
24+
*
25+
* The effect type used by `f` must be compatible with the effect type of the backend. The web socket is always
26+
* closed after `p` completes.
27+
*
28+
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
29+
*
30+
* @see
31+
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
32+
* an exception-throwing variant.
33+
*/
34+
def asWebSocketStreamOrFail[S](
35+
s: Streams[S]
36+
)(p: s.Pipe[WebSocketFrame.Data[_], WebSocketFrame]): WebSocketStreamResponseAs[Unit, S] =
37+
asWebSocketStream(s)(p).orFail
38+
39+
/** Handles the response body by using the given `p` stream processing pipe to handle the incoming & produce the
40+
* outgoing web socket frames, regardless of the status code.
41+
*
42+
* The web socket is always closed after `p` completes.
43+
*
44+
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
45+
*/
1346
def asWebSocketStreamAlways[S](s: Streams[S])(
1447
p: s.Pipe[WebSocketFrame.Data[_], WebSocketFrame]
1548
): WebSocketStreamResponseAs[Unit, S] = WebSocketStreamResponseAs[Unit, S](ResponseAsWebSocketStream(s, p))
1649

50+
/** Uses the [[ResponseAs]] description that matches the condition (using the response's metadata).
51+
*
52+
* This allows using different response description basing on the status code, for example. If none of the conditions
53+
* match, the default response handling description is used.
54+
*/
1755
def fromMetadata[T, S](
1856
default: ResponseAs[T],
1957
conditions: ConditionalResponseAs[WebSocketStreamResponseAs[T, S]]*
@@ -22,8 +60,8 @@ trait SttpWebSocketStreamApi {
2260
ResponseAsFromMetadata(conditions.map(_.map(_.delegate)).toList, default.delegate)
2361
)
2462

25-
/** Uses the `onSuccess` response specification for 101 responses (switching protocols), and the `onError`
26-
* specification otherwise.
63+
/** Uses the `onSuccess` response description for 101 responses (switching protocols) on JVM/Native, 200 responses on
64+
* JS. Otherwise, use the `onError` description.
2765
*/
2866
def asWebSocketEither[A, B, S](
2967
onError: ResponseAs[A],

0 commit comments

Comments
 (0)