-
Notifications
You must be signed in to change notification settings - Fork 302
/
ResponseAs.scala
250 lines (221 loc) · 10.8 KB
/
ResponseAs.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
package sttp.client3
import sttp.capabilities.{Effect, Streams, WebSockets}
import sttp.client3.internal._
import sttp.model.{ResponseMetadata, StatusCode}
import sttp.model.internal.Rfc3986
import sttp.ws.{WebSocket, WebSocketFrame}
import scala.annotation.tailrec
import scala.collection.immutable.Seq
import scala.util.{Failure, Success, Try}
/** Describes how response body should be handled.
*
* Apart from the basic cases (ignoring, reading as a byte array or file), response body descriptions can be mapped
* over, to support custom types. The mapping can take into account the [[ResponseMetadata]], that is the headers and
* 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.client3._`.
*
* @tparam T
* Target type as which the response will be read.
* @tparam R
* The backend capabilities required by the response description. This might be `Any` (no requirements), [[Effect]]
* (the backend must support the given effect type), [[Streams]] (the ability to send and receive streaming bodies)
* or [[WebSockets]] (the ability to handle websocket requests).
*/
sealed trait ResponseAs[+T, -R] {
def map[T2](f: T => T2): ResponseAs[T2, R] = mapWithMetadata { case (t, _) => f(t) }
def mapWithMetadata[T2](f: (T, ResponseMetadata) => T2): ResponseAs[T2, R] =
MappedResponseAs[T, T2, R](this, f, None)
def show: String
def showAs(s: String): ResponseAs[T, R] = MappedResponseAs[T, T, R](this, (t, _) => t, Some(s))
}
case object IgnoreResponse extends ResponseAs[Unit, Any] {
override def show: String = "ignore"
}
case object ResponseAsByteArray extends ResponseAs[Array[Byte], Any] {
override def show: String = "as byte array"
}
// Path-dependent types are not supported in constructor arguments or the extends clause. Thus we cannot express the
// fact that `BinaryStream =:= s.BinaryStream`. We have to rely on correct construction via the companion object and
// perform typecasts when the request is deconstructed.
case class ResponseAsStream[F[_], T, Stream, S] private (s: Streams[S], f: (Stream, ResponseMetadata) => F[T])
extends ResponseAs[T, Effect[F] with S] {
override def show: String = "as stream"
}
object ResponseAsStream {
def apply[F[_], T, S](s: Streams[S])(f: (s.BinaryStream, ResponseMetadata) => F[T]): ResponseAs[T, Effect[F] with S] =
new ResponseAsStream(s, f)
}
case class ResponseAsStreamUnsafe[BinaryStream, S] private (s: Streams[S]) extends ResponseAs[BinaryStream, S] {
override def show: String = "as stream unsafe"
}
object ResponseAsStreamUnsafe {
def apply[S](s: Streams[S]): ResponseAs[s.BinaryStream, S] = new ResponseAsStreamUnsafe(s)
}
case class ResponseAsFile(output: SttpFile) extends ResponseAs[SttpFile, Any] {
override def show: String = s"as file: ${output.name}"
}
sealed trait WebSocketResponseAs[T, -R] extends ResponseAs[T, R]
case class ResponseAsWebSocket[F[_], T](f: (WebSocket[F], ResponseMetadata) => F[T])
extends WebSocketResponseAs[T, Effect[F] with WebSockets] {
override def show: String = "as web socket"
}
case class ResponseAsWebSocketUnsafe[F[_]]() extends WebSocketResponseAs[WebSocket[F], Effect[F] with WebSockets] {
override def show: String = "as web socket unsafe"
}
case class ResponseAsWebSocketStream[S, Pipe[_, _]](s: Streams[S], p: Pipe[WebSocketFrame.Data[_], WebSocketFrame])
extends WebSocketResponseAs[Unit, S with WebSockets] {
override def show: String = "as web socket stream"
}
case class ConditionalResponseAs[+T, R](condition: ResponseMetadata => Boolean, responseAs: ResponseAs[T, R])
case class ResponseAsFromMetadata[T, R](conditions: List[ConditionalResponseAs[T, R]], default: ResponseAs[T, R])
extends ResponseAs[T, R] {
def apply(meta: ResponseMetadata): ResponseAs[T, R] =
conditions.find(mapping => mapping.condition(meta)).map(_.responseAs).getOrElse(default)
override def show: String = s"either(${(default.show :: conditions.map(_.responseAs.show)).mkString(", ")})"
}
case class MappedResponseAs[T, T2, R](raw: ResponseAs[T, R], g: (T, ResponseMetadata) => T2, showAs: Option[String])
extends ResponseAs[T2, R] {
override def mapWithMetadata[T3](f: (T2, ResponseMetadata) => T3): ResponseAs[T3, R] =
MappedResponseAs[T, T3, R](raw, (t, h) => f(g(t, h), h), showAs.map(s => s"mapped($s)"))
override def showAs(s: String): ResponseAs[T2, R] = this.copy(showAs = Some(s))
override def show: String = showAs.getOrElse(s"mapped(${raw.show})")
}
case class ResponseAsBoth[A, B, R](l: ResponseAs[A, R], r: ResponseAs[B, Any]) extends ResponseAs[(A, Option[B]), R] {
override def show: String = s"(${l.show}, optionally ${r.show})"
}
object ResponseAs {
implicit class RichResponseAsEither[A, B, R](ra: ResponseAs[Either[A, B], R]) {
def mapLeft[L2](f: A => L2): ResponseAs[Either[L2, B], R] = ra.map(_.left.map(f))
def mapRight[R2](f: B => R2): ResponseAs[Either[A, R2], R] = ra.map(_.right.map(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 getRight: ResponseAs[B, R] =
ra.mapWithMetadata { case (t, meta) =>
t match {
case Left(a: Exception) => throw a
case Left(a) => throw HttpError(a, meta.code)
case Right(b) => b
}
}
}
implicit class RichResponseAsEitherResponseException[HE, DE, B, R](
ra: ResponseAs[Either[ResponseException[HE, DE], B], R]
) {
/** If the type to which the response body should be deserialized is an `Either[ResponseException[HE, DE], B]`,
* either throws the [[DeserializationException]], returns the deserialized body from the [[HttpError]], or the
* deserialized successful body `B`.
*/
def getEither: ResponseAs[Either[HE, B], R] =
ra.map {
case Left(HttpError(he, _)) => Left(he)
case Left(d: DeserializationException[_]) => throw d
case Right(b) => Right(b)
}
}
private[client3] def parseParams(s: String, charset: String): Seq[(String, String)] = {
s.split("&")
.toList
.flatMap(kv =>
kv.split("=", 2) match {
case Array(k, v) =>
Some((Rfc3986.decode()(k, charset), Rfc3986.decode()(v, charset)))
case _ => None
}
)
}
/** Returns a function, which maps `Left` values to [[HttpError]] s, and attempts to deserialize `Right` values using
* the given function, catching any exceptions and representing them as [[DeserializationException]] s.
*/
def deserializeRightCatchingExceptions[T](
doDeserialize: String => T
): (Either[String, String], ResponseMetadata) => Either[ResponseException[String, Exception], T] = {
case (Left(s), meta) => Left(HttpError(s, meta.code))
case (Right(s), _) => deserializeCatchingExceptions(doDeserialize)(s)
}
/** Returns a function, which attempts to deserialize `Right` values using the given function, catching any exceptions
* and representing them as [[DeserializationException]] s.
*/
def deserializeCatchingExceptions[T](
doDeserialize: String => T
): String => Either[DeserializationException[Exception], T] =
deserializeWithError((s: String) =>
Try(doDeserialize(s)) match {
case Failure(e: Exception) => Left(e)
case Failure(t: Throwable) => throw t
case Success(t) => Right(t): Either[Exception, T]
}
)
/** Returns a function, which maps `Left` values to [[HttpError]] s, and attempts to deserialize `Right` values using
* the given function.
*/
def deserializeRightWithError[E: ShowError, T](
doDeserialize: String => Either[E, T]
): (Either[String, String], ResponseMetadata) => Either[ResponseException[String, E], T] = {
case (Left(s), meta) => Left(HttpError(s, meta.code))
case (Right(s), _) => deserializeWithError(doDeserialize)(implicitly[ShowError[E]])(s)
}
/** Returns a function, which keeps `Left` unchanged, and attempts to deserialize `Right` values using the given
* function. If deserialization fails, an exception is thrown
*/
def deserializeRightOrThrow[E: ShowError, T](
doDeserialize: String => Either[E, T]
): Either[String, String] => Either[String, T] = {
case Left(s) => Left(s)
case Right(s) => Right(deserializeOrThrow(doDeserialize)(implicitly[ShowError[E]])(s))
}
/** Converts a deserialization function, which returns errors of type `E`, into a function where errors are wrapped
* using [[DeserializationException]].
*/
def deserializeWithError[E: ShowError, T](
doDeserialize: String => Either[E, T]
): String => Either[DeserializationException[E], T] =
s =>
doDeserialize(s) match {
case Left(e) => Left(DeserializationException(s, e))
case Right(b) => Right(b)
}
/** Converts a deserialization function, which returns errors of type `E`, into a function where errors are thrown as
* exceptions, and results are returned unwrapped.
*/
def deserializeOrThrow[E: ShowError, T](doDeserialize: String => Either[E, T]): String => T =
s =>
doDeserialize(s) match {
case Left(e) => throw DeserializationException(s, e)
case Right(b) => b
}
def isWebSocket[T, R](ra: ResponseAs[_, _]): Boolean =
ra match {
case _: WebSocketResponseAs[_, _] => true
case ResponseAsFromMetadata(conditions, default) =>
conditions.exists(c => isWebSocket(c.responseAs)) || isWebSocket(default)
case MappedResponseAs(raw, _, _) => isWebSocket(raw)
case ResponseAsBoth(l, r) => isWebSocket(l) || isWebSocket(r)
case _ => false
}
}
sealed abstract class ResponseException[+HE, +DE](error: String) extends Exception(error)
case class HttpError[HE](body: HE, statusCode: StatusCode)
extends ResponseException[HE, Nothing](s"statusCode: $statusCode, response: $body")
case class DeserializationException[DE: ShowError](body: String, error: DE)
extends ResponseException[Nothing, DE](implicitly[ShowError[DE]].show(error))
object HttpError {
@tailrec def find(exception: Throwable): Option[HttpError[_]] =
Option(exception) match {
case Some(error: HttpError[_]) => Some(error)
case Some(_) => find(exception.getCause)
case None => Option.empty
}
}
trait ShowError[-T] {
def show(t: T): String
}
object ShowError {
implicit val showErrorMessageFromException: ShowError[Exception] = new ShowError[Exception] {
override def show(t: Exception): String = t.getMessage
}
}