Skip to content

Commit

Permalink
Add as...OrFail response-as variants
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw committed Dec 11, 2024
1 parent ae08044 commit 62ead1f
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 12 deletions.
54 changes: 54 additions & 0 deletions core/src/main/scala/sttp/client4/ResponseAs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,29 @@ object ResponseAs {
* [[ResponseAs]]
*/
case class StreamResponseAs[+T, S](delegate: GenericResponseAs[T, S]) extends ResponseAsDelegate[T, S] {

/** Applies the given function `f` to the deserialized value `T`. */
def map[T2](f: T => T2): StreamResponseAs[T2, S] =
StreamResponseAs(delegate.mapWithMetadata { case (t, _) => f(t) })

/** Applies the given function `f` to the deserialized value `T`, and the response's metadata. */
def mapWithMetadata[T2](f: (T, ResponseMetadata) => T2): StreamResponseAs[T2, S] =
StreamResponseAs(delegate.mapWithMetadata(f))

/** If the type to which the response body should be deserialized is an `Either[A, B]`:
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is not
* yet an exception)
* - in case of `B`, returns the value directly
*/
def orFail[A, B](implicit tIsEither: T <:< Either[A, B]): StreamResponseAs[B, S] =
mapWithMetadata { case (t, meta) =>
(t: Either[A, B]) match {
case Left(a: Exception) => throw a
case Left(a) => throw HttpError(a, meta.code)
case Right(b) => b
}
}

def showAs(s: String): StreamResponseAs[T, S] = new StreamResponseAs(delegate.showAs(s))
}

Expand All @@ -215,11 +233,29 @@ case class StreamResponseAs[+T, S](delegate: GenericResponseAs[T, S]) extends Re
*/
case class WebSocketResponseAs[F[_], +T](delegate: GenericResponseAs[T, Effect[F] with WebSockets])
extends ResponseAsDelegate[T, Effect[F] with WebSockets] {

/** Applies the given function `f` to the deserialized value `T`. */
def map[T2](f: T => T2): WebSocketResponseAs[F, T2] =
WebSocketResponseAs(delegate.mapWithMetadata { case (t, _) => f(t) })

/** Applies the given function `f` to the deserialized value `T`, and the response's metadata. */
def mapWithMetadata[T2](f: (T, ResponseMetadata) => T2): WebSocketResponseAs[F, T2] =
WebSocketResponseAs(delegate.mapWithMetadata(f))

/** If the type to which the response body should be deserialized is an `Either[A, B]`:
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is not
* yet an exception)
* - in case of `B`, returns the value directly
*/
def orFail[A, B](implicit tIsEither: T <:< Either[A, B]): WebSocketResponseAs[F, B] =
mapWithMetadata { case (t, meta) =>
(t: Either[A, B]) match {
case Left(a: Exception) => throw a
case Left(a) => throw HttpError(a, meta.code)
case Right(b) => b
}
}

def showAs(s: String): WebSocketResponseAs[F, T] = new WebSocketResponseAs(delegate.showAs(s))
}

Expand All @@ -238,11 +274,29 @@ case class WebSocketResponseAs[F[_], +T](delegate: GenericResponseAs[T, Effect[F
*/
case class WebSocketStreamResponseAs[+T, S](delegate: GenericResponseAs[T, S with WebSockets])
extends ResponseAsDelegate[T, S with WebSockets] {

/** Applies the given function `f` to the deserialized value `T`. */
def map[T2](f: T => T2): WebSocketStreamResponseAs[T2, S] =
WebSocketStreamResponseAs[T2, S](delegate.mapWithMetadata { case (t, _) => f(t) })

/** Applies the given function `f` to the deserialized value `T`, and the response's metadata. */
def mapWithMetadata[T2](f: (T, ResponseMetadata) => T2): WebSocketStreamResponseAs[T2, S] =
WebSocketStreamResponseAs[T2, S](delegate.mapWithMetadata(f))

/** If the type to which the response body should be deserialized is an `Either[A, B]`:
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is not
* yet an exception)
* - in case of `B`, returns the value directly
*/
def orFail[A, B](implicit tIsEither: T <:< Either[A, B]): WebSocketStreamResponseAs[B, S] =
mapWithMetadata { case (t, meta) =>
(t: Either[A, B]) match {
case Left(a: Exception) => throw a
case Left(a) => throw HttpError(a, meta.code)
case Right(b) => b
}
}

def showAs(s: String): WebSocketStreamResponseAs[T, S] = new WebSocketStreamResponseAs[T, S](delegate.showAs(s))
}

Expand Down
55 changes: 50 additions & 5 deletions core/src/main/scala/sttp/client4/SttpApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,16 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
}
.showAs("as string")

/** Reads the response as either a string (for non-2xx responses), or othweise as an array of bytes (without any
/** Reads the response as a `String`, if the status code is 2xx. Otherwise, throws an [[HttpError]] / returns a failed
* effect. Use the `utf-8` charset by default, unless specified otherwise in the response headers.
*
* @see
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
* an exception-throwing variant.
*/
def asStringOrFail: ResponseAs[String] = asString.orFail

/** Reads the response as either a string (for non-2xx responses), or otherwise as an array of bytes (without any
* processing). The entire response is loaded into memory.
*/
def asByteArray: ResponseAs[Either[String, Array[Byte]]] = asEither(asStringAlways, asByteArrayAlways)
Expand All @@ -103,6 +112,15 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
*/
def asByteArrayAlways: ResponseAs[Array[Byte]] = ResponseAs(ResponseAsByteArray)

/** Reads the response as an array of bytes, without any processing, if the status code is 2xx. Otherwise, throws an
* [[HttpError]] / returns a failed effect.
*
* @see
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
* an exception-throwing variant.
*/
def asByteArrayOrFail: ResponseAs[Array[Byte]] = asByteArray.orFail

/** Deserializes the response as either a string (for non-2xx responses), or otherwise as form parameters. Uses the
* `utf-8` charset by default, unless specified otherwise in the response headers.
*/
Expand All @@ -127,6 +145,15 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
asStringAlways(charset2).map(GenericResponseAs.parseParams(_, charset2)).showAs("as params")
}

/** Deserializes the response as form parameters, if the status code is 2xx. Otherwise, throws an [[HttpError]] /
* returns a failed effect. Uses the `utf-8` charset by default, unless specified otherwise in the response headers.
*
* @see
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
* an exception-throwing variant.
*/
def asParamsOrFail: ResponseAs[String] = asString.orFail

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

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

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

/** Handles the response body by providing a stream with the response's data to `f`, if the status code is 2xx.
* Otherwise, returns a failed effect (with [[HttpError]]). The effect type used by `f` must be compatible with the
* effect type of the backend. The stream is always closed after `f` completes.
*
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
*
* @see
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
* an exception-throwing variant.
*/
def asStreamOrFail[F[_], T, S](s: Streams[S])(
f: s.BinaryStream => F[T]
): StreamResponseAs[T, S with Effect[F]] = asStream(s)(f).orFail

/** Handles the response body by either reading a string (for non-2xx responses), or otherwise providing a stream with
* the response's data, along with the response metadata, to `f`. The stream is always closed after `f` completes.
* the response's data, along with the response metadata, to `f`. The effect type used by `f` must be compatible with
* the effect type of the backend. The stream is always closed after `f` completes.
*
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
*/
Expand All @@ -263,15 +306,17 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
asEither(asStringAlways, asStreamAlwaysWithMetadata(s)(f))

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

/** Handles the response body by providing a stream with the response's data, along with the response metadata, to
* `f`, regardless of the status code. The stream is always closed after `f` completes.
* `f`, regardless of the status code. The effect type used by `f` must be compatible with the effect type of the
* backend. The stream is always closed after `f` completes.
*
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
*/
Expand Down
60 changes: 58 additions & 2 deletions core/src/main/scala/sttp/client4/SttpWebSocketAsyncApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,90 @@ import sttp.model.ResponseMetadata
import sttp.ws.WebSocket

trait SttpWebSocketAsyncApi {

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

/** Handles the response as a web socket, providing an open [[WebSocket]] instance to the `f` function, if the status
* code is 2xx. Otherwise, returns a failed effect (with [[HttpError]]).
*
* The effect type used by `f` must be compatible with the effect type of the backend. The web socket is always
* closed after `f` completes.
*
* @see
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
* an exception-throwing variant.
*/
def asWebSocketOrFail[F[_], T](f: WebSocket[F] => F[T]): WebSocketResponseAs[F, T] = asWebSocket(f).orFail

/** Handles the response body by either reading a string (for non-2xx responses), or otherwise providing an open
* [[WebSocket]] instance, along with the response metadata, to the `f` function.
*
* The effect type used by `f` must be compatible with the effect type of the backend. The web socket is always
* closed after `f` completes.
*/
def asWebSocketWithMetadata[F[_], T](
f: (WebSocket[F], ResponseMetadata) => F[T]
): WebSocketResponseAs[F, Either[String, T]] =
asWebSocketEither(asStringAlways, asWebSocketAlwaysWithMetadata(f))

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

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

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

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

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

/** Uses the `onSuccess` response specification for 101 responses (switching protocols) on JVM/Native, 200 responses
* on JS. Otherwise, use the `onError` specification.
/** Uses the `onSuccess` response description for 101 responses (switching protocols) on JVM/Native, 200 responses on
* JS. Otherwise, use the `onError` description.
*
* The effect type `F` must be compatible with the effect type of the backend.
*/
def asWebSocketEither[F[_], A, B](
onError: ResponseAs[A],
Expand Down
42 changes: 40 additions & 2 deletions core/src/main/scala/sttp/client4/SttpWebSocketStreamApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,53 @@ import sttp.model.StatusCode
import sttp.ws.WebSocketFrame

trait SttpWebSocketStreamApi {

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

/** Handles the response as a web socket, using the given `p` stream processing pipe to handle the incoming & produce
* the outgoing web socket frames, if the status code is 2xx. Otherwise, returns a failed effect (with
* [[HttpError]]).
*
* The effect type used by `f` must be compatible with the effect type of the backend. The web socket is always
* closed after `p` completes.
*
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
*
* @see
* the [[ResponseAs.orFail]] method can be used to convert any response description which returns an `Either` into
* an exception-throwing variant.
*/
def asWebSocketStreamOrFail[S](
s: Streams[S]
)(p: s.Pipe[WebSocketFrame.Data[_], WebSocketFrame]): WebSocketStreamResponseAs[Unit, S] =
asWebSocketStream(s)(p).orFail

/** Handles the response body by using the given `p` stream processing pipe to handle the incoming & produce the
* outgoing web socket frames, regardless of the status code.
*
* The web socket is always closed after `p` completes.
*
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
*/
def asWebSocketStreamAlways[S](s: Streams[S])(
p: s.Pipe[WebSocketFrame.Data[_], WebSocketFrame]
): WebSocketStreamResponseAs[Unit, S] = WebSocketStreamResponseAs[Unit, S](ResponseAsWebSocketStream(s, p))

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

/** Uses the `onSuccess` response specification for 101 responses (switching protocols), and the `onError`
* specification otherwise.
/** Uses the `onSuccess` response description for 101 responses (switching protocols) on JVM/Native, 200 responses on
* JS. Otherwise, use the `onError` description.
*/
def asWebSocketEither[A, B, S](
onError: ResponseAs[A],
Expand Down
Loading

0 comments on commit 62ead1f

Please sign in to comment.