Skip to content

Commit

Permalink
Docs
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw committed Dec 11, 2024
1 parent 1557c5f commit bdb8d00
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 52 deletions.
30 changes: 19 additions & 11 deletions core/src/main/scala/sttp/client4/ResponseAs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ import scala.util.{Failure, Success, Try}
/** Describes how the response body of a request should be handled. A number of `as<Type>` helper methods are available
* as part of [[SttpApi]] and when importing `sttp.client4._`. These methods yield specific implementations of this
* trait, which can then be set on a [[Request]], [[StreamRequest]], [[WebSocketRequest]] or
* [[WebSocketStreamRequest]], depending on the response type.
* [[WebSocketStreamRequest]].
*
* @tparam T
* Target type as which the response will be read.
* Target type as which the response will be deserialized.
* @tparam R
* The backend capabilities required by the response description. This might be `Any` (no requirements),
* [[sttp.capabilities.Effect]] (the backend must support the given effect type), [[sttp.capabilities.Streams]] (the
* ability to send and receive streaming bodies) or [[sttp.capabilities.WebSockets]] (the ability to handle websocket
* requests).
* @see
* [[ResponseAs]]
*/
trait ResponseAsDelegate[+T, -R] {
def delegate: GenericResponseAs[T, R]
Expand All @@ -35,10 +37,10 @@ trait ResponseAsDelegate[+T, -R] {
* status code. Responses can also be handled depending on the response metadata. Finally, two response body
* descriptions can be combined (with some restrictions).
*
* A number of `as<Type>` helper methods are available as part of [[SttpApi]] and when importing `sttp.client4._`.
* A number of `as<Type>` helper methods are available as part of [[SttpApi]] and when importing `sttp.client4.*`.
*
* @tparam T
* Target type as which the response will be read.
* Target type as which the response will be read/deserialized.
*/
case class ResponseAs[+T](delegate: GenericResponseAs[T, Any]) extends ResponseAsDelegate[T, Any] {

Expand Down Expand Up @@ -89,7 +91,7 @@ case class ResponseAs[+T](delegate: GenericResponseAs[T, Any]) extends ResponseA
}

/** If the type to which the response body should be deserialized is an `Either[ResponseException[HE, DE], B]`, either
* throws /returns a failed effect with the [[DeserializationException]], returns the deserialized body from the
* throws / returns a failed effect with the [[DeserializationException]], returns the deserialized body from the
* [[HttpError]], or the deserialized successful body `B`.
*/
def orFailDeserialization[HE, DE, B](implicit
Expand Down Expand Up @@ -180,12 +182,14 @@ object ResponseAs {
* [[ResponseMetadata]], that is the headers and status code.
*
* A number of `asStream[Type]` helper methods are available as part of [[SttpApi]] and when importing
* `sttp.client4._`.
* `sttp.client4.*`.
*
* @tparam T
* Target type as which the response will be read.
* Target type as which the response will be read/deserialized.
* @tparam S
* The type of stream, used to receive the response body bodies.
* @see
* [[ResponseAs]]
*/
case class StreamResponseAs[+T, S](delegate: GenericResponseAs[T, S]) extends ResponseAsDelegate[T, S] {
def map[T2](f: T => T2): StreamResponseAs[T2, S] =
Expand All @@ -202,10 +206,12 @@ case class StreamResponseAs[+T, S](delegate: GenericResponseAs[T, S]) extends Re
* [[ResponseMetadata]], that is the headers and status code. Responses can also be handled depending on the response
* metadata.
*
* A number of `asWebSocket` helper methods are available as part of [[SttpApi]] and when importing `sttp.client4._`.
* A number of `asWebSocket` helper methods are available as part of [[SttpApi]] and when importing `sttp.client4.*`.
*
* @tparam T
* Target type as which the response will be read.
* Target type as which the response will be read/deserialized.
* @see
* [[ResponseAs]]
*/
case class WebSocketResponseAs[F[_], +T](delegate: GenericResponseAs[T, Effect[F] with WebSockets])
extends ResponseAsDelegate[T, Effect[F] with WebSockets] {
Expand All @@ -223,10 +229,12 @@ case class WebSocketResponseAs[F[_], +T](delegate: GenericResponseAs[T, Effect[F
* [[ResponseMetadata]], that is the headers and status code. Responses can also be handled depending on the response
* metadata.
*
* A number of `asWebSocket` helper methods are available as part of [[SttpApi]] and when importing `sttp.client4._`.
* A number of `asWebSocket` helper methods are available as part of [[SttpApi]] and when importing `sttp.client4.*`.
*
* @tparam T
* Target type as which the response will be read.
* Target type as which the response will be read/deserialized.
* @see
* [[ResponseAs]]
*/
case class WebSocketStreamResponseAs[+T, S](delegate: GenericResponseAs[T, S with WebSockets])
extends ResponseAsDelegate[T, S with WebSockets] {
Expand Down
29 changes: 24 additions & 5 deletions core/src/main/scala/sttp/client4/ResponseException.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,37 @@ import sttp.model.StatusCode

import scala.annotation.tailrec

/** Used to represent errors, that might occur when handling the response body. Either:
* - a [[HttpError]], when the response code is different than the expected one; desrialization is not attempted
* - a [[DeserializationException]], when there's an error during deserialization
/** Used to represent errors, that might occur when handling the response body. Typically, this type is used as the
* left-side of a top-level either (where the right-side represents a successfull request and deserialization).
*
* A response exception can itself either be one of two cases:
* - a [[HttpError]], when the response code is other than 2xx (or whatever is considered "success" by the response
* handling description); the body is deserialized to `HE`
* - a [[DeserializationException]], when there's an error during deserialization (this might include both
* deserialization exceptions of the success and error branches)
*
* @tparam HE
* The type of the body to which the response is read, when the resposne code is different than the expected one
* The type of the body to which the response is deserialized, when the response code is different than success
* (typically 2xx status code).
* @tparam DE
* A deserialization-library-specific error type, describing the deserialization error in more detail
* A deserialization-library-specific error type, describing the deserialization error in more detail.
*/
sealed abstract class ResponseException[+HE, +DE](error: String) extends Exception(error)

/** Represents an http error, where the response was received successfully, but the status code is other than the
* expected one (typically other than 2xx).
*
* @tparam HE
* The type of the body to which the error response is deserialized.
*/
case class HttpError[+HE](body: HE, statusCode: StatusCode)
extends ResponseException[HE, Nothing](s"statusCode: $statusCode, response: $body")

/** Represents an error that occured during deserialization of `body`.
*
* @tparam DE
* A deserialization-library-specific error type, describing the deserialization error in more detail.
*/
case class DeserializationException[+DE: ShowError](body: String, error: DE)
extends ResponseException[Nothing, DE](implicitly[ShowError[DE]].show(error))

Expand Down
109 changes: 90 additions & 19 deletions core/src/main/scala/sttp/client4/SttpApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,36 +38,52 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
AttributeMap.Empty
)

/** A starting request, with the following modification comparing to [[emptyRequest]]: `Accept-Encoding` is set to
* `gzip, deflate` (compression/decompression is handled automatically by the library).
/** A starting request, with the `Accept-Encoding` header set to `gzip, deflate` (compression/decompression is handled
* automatically by the library).
*
* Reads the response body as an `Either[String, String]`, where `Left` is used if the status code is non-2xx, and
* `Right` otherwise.
*
* @see
* [[emptyRequest]] for a starting request which has no headers set
* @see
* [[quickRequest]] for a starting request which always reads the response body as a [[String]], without the
* [[Either]] wrapper
*/
val basicRequest: PartialRequest[Either[String, String]] =
emptyRequest.acceptEncoding("gzip, deflate")

/** A starting request which always reads the response body as a string, regardless of the status code. */
val quickRequest: PartialRequest[String] = basicRequest.response(asStringAlways)

// response specifications
// response descriptions

/** Ignores (discards) the response. */
def ignore: ResponseAs[Unit] = ResponseAs(IgnoreResponse)

/** Use the `utf-8` charset by default, unless specified otherwise in the response headers. */
/** Reads the response as an `Either[String, String]`, where `Left` is used if the status code is non-2xx, and `Right`
* otherwise. Uses the `utf-8` charset by default, unless specified otherwise in the response headers.
*/
def asString: ResponseAs[Either[String, String]] = asString(Utf8)

/** Use the `utf-8` charset by default, unless specified otherwise in the response headers. */
/** Reads the response as a `String`, regardless of the status code. Use the `utf-8` charset by default, unless
* specified otherwise in the response headers.
*/
def asStringAlways: ResponseAs[String] = asStringAlways(Utf8)

/** Use the given charset by default, unless specified otherwise in the response headers. */
/** Reads the response as an `Either[String, String]`, where `Left` is used if the status code is non-2xx, and `Right`
* otherwise. Uses the given charset by default, unless specified otherwise in the response headers.
*/
def asString(charset: String): ResponseAs[Either[String, String]] =
asStringAlways(charset)
.mapWithMetadata { (s, m) =>
if (m.isSuccess) Right(s) else Left(s)
}
.showAs("either(as string, as string)")

/** Reads the response as a `String`, regardless of the status code. Uses the given charset by default, unless
* specified otherwise in the response headers.
*/
def asStringAlways(charset: String): ResponseAs[String] =
asByteArrayAlways
.mapWithMetadata { (bytes, metadata) =>
Expand All @@ -77,39 +93,57 @@ 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
* processing). The entire response is loaded into memory.
*/
def asByteArray: ResponseAs[Either[String, Array[Byte]]] = asEither(asStringAlways, asByteArrayAlways)

/** Reads the response as an array of bytes, without any processing, regardless of the status code. The entire
* response is loaded into memory.
*/
def asByteArrayAlways: ResponseAs[Array[Byte]] = ResponseAs(ResponseAsByteArray)

/** Use the `utf-8` charset by default, unless specified otherwise in the response headers. */
/** 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.
*/
def asParams: ResponseAs[Either[String, Seq[(String, String)]]] = asParams(Utf8)

/** Use the `utf-8` charset by default, unless specified otherwise in the response headers. */
/** Deserializes the response as form parameters, regardless of the status code. Uses the `utf-8` charset by default,
* unless specified otherwise in the response headers.
*/
def asParamsAlways: ResponseAs[Seq[(String, String)]] = asParamsAlways(Utf8)

/** Use the given charset by default, unless specified otherwise in the response headers. */
/** Deserializes the response as either a string (for non-2xx responses), or otherwise as form parameters. Uses the
* given charset by default, unless specified otherwise in the response headers.
*/
def asParams(charset: String): ResponseAs[Either[String, Seq[(String, String)]]] =
asEither(asStringAlways, asParamsAlways(charset)).showAs("either(as string, as params)")

/** Use the given charset by default, unless specified otherwise in the response headers. */
/** Deserializes the response as form parameters, regardless of the status code. Uses the given charset by default,
* unless specified otherwise in the response headers.
*/
def asParamsAlways(charset: String): ResponseAs[Seq[(String, String)]] = {
val charset2 = sanitizeCharset(charset)
asStringAlways(charset2).map(GenericResponseAs.parseParams(_, charset2)).showAs("as params")
}

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

/** 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](default: ResponseAs[T], conditions: ConditionalResponseAs[ResponseAs[T]]*): ResponseAs[T] =
ResponseAs(ResponseAsFromMetadata(conditions.map(_.map(_.delegate)).toList, default.delegate))

/** Uses the `onSuccess` response specification for successful responses (2xx), and the `onError` specification
* otherwise.
/** Uses the `onSuccess` response description for successful responses (2xx), and the `onError` description otherwise.
*/
def asEither[A, B](onError: ResponseAs[A], onSuccess: ResponseAs[B]): ResponseAs[Either[A, B]] =
fromMetadata(onError.map(Left(_)), ConditionalResponseAs(_.isSuccess, onSuccess.map(Right(_))))
.showAs(s"either(${onError.show}, ${onSuccess.show})")

/** Use both `l` and `r` to read the response body. Neither response specifications may use streaming or web sockets.
/** Uses both `l` and `r` to handle the response body. Neither response descriptions may use streaming or web sockets.
*/
def asBoth[A, B](l: ResponseAs[A], r: ResponseAs[B]): ResponseAs[(A, B)] =
asBothOption(l, r)
Expand All @@ -119,8 +153,8 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
}
.showAs(s"(${l.show}, ${r.show})")

/** Use `l` to read the response body. If the raw body value which is used by `l` is replayable (a file or byte
* array), also use `r` to read the response body. Otherwise ignore `r` (if the raw body is a stream).
/** Uses `l` to handle the response body. If the raw body value which is used by `l` is replayable (a file or byte
* array), also uses `r` to read the response body. Otherwise ignores `r` (if the raw body is a stream).
*/
def asBothOption[A, B](l: ResponseAs[A], r: ResponseAs[B]): ResponseAs[(A, Option[B])] =
ResponseAs(ResponseAsBoth(l.delegate, r.delegate))
Expand Down Expand Up @@ -208,44 +242,81 @@ 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.
*
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
*/
def asStream[F[_], T, S](s: Streams[S])(
f: s.BinaryStream => F[T]
): StreamResponseAs[Either[String, T], S with Effect[F]] =
asEither(asStringAlways, asStreamAlways(s)(f))

/** 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.
*
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
*/
def asStreamWithMetadata[F[_], T, S](s: Streams[S])(
f: (s.BinaryStream, ResponseMetadata) => F[T]
): StreamResponseAs[Either[String, T], S with Effect[F]] =
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.
*
* 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.
*
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
*/
def asStreamAlwaysWithMetadata[F[_], T, S](s: Streams[S])(
f: (s.BinaryStream, ResponseMetadata) => F[T]
): StreamResponseAs[T, S with Effect[F]] = StreamResponseAs(ResponseAsStream(s)(f))

/** Handles the response body by either reading a string (for non-2xx responses), or otherwise returning a stream with
* the response's data. It's the responsibility of the caller to consume & close the stream.
*
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
*/
def asStreamUnsafe[S](s: Streams[S]): StreamResponseAs[Either[String, s.BinaryStream], S] =
asEither(asStringAlways, asStreamAlwaysUnsafe(s))

/** Handles the response body by returning a stream with the response's data, regardless of the status code. It's the
* responsibility of the caller to consume & close the stream.
*
* A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter.
*/
def asStreamAlwaysUnsafe[S](s: Streams[S]): StreamResponseAs[s.BinaryStream, S] =
StreamResponseAs(ResponseAsStreamUnsafe(s))

/** Uses the [[StreamResponseAs]] description that matches the condition (using the response's metadata). The
* conditional response descriptions might include handling the response as a non-blocking, asynchronous stream.
*
* 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[StreamResponseAs[T, S]]*
): StreamResponseAs[T, S] =
StreamResponseAs[T, S](ResponseAsFromMetadata(conditions.map(_.map(_.delegate)).toList, default.delegate))

/** Uses the `onSuccess` response specification for successful responses (2xx), and the `onError` specification
* otherwise.
/** Uses the `onSuccess` response description for successful responses (2xx), and the `onError` description otherwise.
*
* The sucessful response description might include handling the response as a non-blocking, asynchronous stream.
*/
def asEither[A, B, S](onError: ResponseAs[A], onSuccess: StreamResponseAs[B, S]): StreamResponseAs[Either[A, B], S] =
fromMetadata[Either[A, B], S](onError.map(Left(_)), ConditionalResponseAs(_.isSuccess, onSuccess.map(Right(_))))
.showAs(s"either(${onError.show}, ${onSuccess.show})")

/** Use `l` to read the response body. If the raw body value which is used by `l` is replayable (a file or byte
* array), also use `r` to read the response body. Otherwise ignore `r` (if the raw body is a stream).
/** Uses `l` to handle the response body. If the raw body value which is used by `l` is replayable (a file or byte
* array), also uses `r` to read the response body. Otherwise ignores `r` (if the raw body is a stream).
*/
def asBothOption[A, B, S](l: StreamResponseAs[A, S], r: ResponseAs[B]): StreamResponseAs[(A, Option[B]), S] =
StreamResponseAs[(A, Option[B]), S](ResponseAsBoth(l.delegate, r.delegate))
Expand Down
Loading

0 comments on commit bdb8d00

Please sign in to comment.