@@ -12,17 +12,15 @@ import sttp.ws.WebSocket
12
12
import sttp .ws .testing .WebSocketStub
13
13
14
14
import scala .util .{Failure , Success , Try }
15
+ import sttp .model .StatusText
15
16
16
17
abstract class AbstractBackendStub [F [_], P ](
17
18
_monad : MonadError [F ],
18
- matchers : PartialFunction [GenericRequest [_, _], F [Response [_ ]]],
19
+ matchers : PartialFunction [GenericRequest [_, _], F [Response [StubBody ]]],
19
20
fallback : Option [GenericBackend [F , P ]]
20
21
) extends GenericBackend [F , P ] {
21
-
22
22
type Self
23
-
24
- protected def withMatchers (matchers : PartialFunction [GenericRequest [_, _], F [Response [_]]]): Self
25
-
23
+ protected def withMatchers (matchers : PartialFunction [GenericRequest [_, _], F [Response [StubBody ]]]): Self
26
24
override def monad : MonadError [F ] = _monad
27
25
28
26
/** Specify how the stub backend should respond to requests matching the given predicate.
@@ -42,9 +40,9 @@ abstract class AbstractBackendStub[F[_], P](
42
40
*
43
41
* Note that the stubs are immutable, and each new specification that is added yields a new stub instance.
44
42
*/
45
- def whenRequestMatchesPartial (partial : PartialFunction [GenericRequest [_, _], Response [_ ]]): Self = {
46
- val wrappedPartial : PartialFunction [GenericRequest [_, _], F [Response [_ ]]] =
47
- partial.andThen((r : Response [_ ]) => monad.unit(r))
43
+ def whenRequestMatchesPartial (partial : PartialFunction [GenericRequest [_, _], Response [StubBody ]]): Self = {
44
+ val wrappedPartial : PartialFunction [GenericRequest [_, _], F [Response [StubBody ]]] =
45
+ partial.andThen((r : Response [StubBody ]) => monad.unit(r))
48
46
withMatchers(matchers.orElse(wrappedPartial))
49
47
}
50
48
@@ -54,7 +52,14 @@ abstract class AbstractBackendStub[F[_], P](
54
52
adjustExceptions(request) {
55
53
monad.flatMap(response) { r =>
56
54
request.options.onBodyReceived(r)
57
- tryAdjustResponseType(request.response, r.asInstanceOf [Response [T ]])(monad)
55
+
56
+ r.body match {
57
+ case StubBody .Exact (v) => monad.unit(r.copy(body = v.asInstanceOf [T ]))
58
+ case StubBody .Adjust (v) =>
59
+ monad.map(adjustResponseBody(request.response.delegate, v, r.asInstanceOf [Response [T ]])(monad))(b =>
60
+ r.copy(body = b)
61
+ )
62
+ }
58
63
}
59
64
}
60
65
case Success (None ) =>
@@ -80,35 +85,70 @@ abstract class AbstractBackendStub[F[_], P](
80
85
override def close (): F [Unit ] = monad.unit(())
81
86
82
87
class WhenRequest (p : GenericRequest [_, _] => Boolean ) {
83
- def thenRespondOk (): Self = thenRespondWithCode(StatusCode .Ok , " OK" )
84
- def thenRespondNotFound (): Self = thenRespondWithCode(StatusCode .NotFound , " Not found" )
85
- def thenRespondServerError (): Self = thenRespondWithCode(StatusCode .InternalServerError , " Internal server error" )
86
- def thenRespondWithCode (status : StatusCode , msg : String = " " ): Self = thenRespond(ResponseStub (msg, status, msg))
87
- def thenRespond [T ](body : T ): Self = thenRespond(ResponseStub [T ](body, StatusCode .Ok , " OK" ))
88
- def thenRespond [T ](body : T , statusCode : StatusCode ): Self = thenRespond(ResponseStub [T ](body, statusCode))
89
- def thenRespond [T ](resp : => Response [T ]): Self = {
90
- val m : PartialFunction [GenericRequest [_, _], F [Response [_]]] = {
91
- case r if p(r) => monad.eval(resp.copy(request = r.onlyMetadata))
88
+
89
+ /** Respond with an empty body and the 200 status code */
90
+ def thenRespondOk (): Self = thenRespondWithCode(StatusCode .Ok )
91
+ def thenRespondBadRequest (): Self = thenRespondWithCode(StatusCode .BadRequest )
92
+ def thenRespondNotFound (): Self = thenRespondWithCode(StatusCode .NotFound )
93
+ def thenRespondServerError (): Self = thenRespondWithCode(StatusCode .InternalServerError )
94
+ def thenRespondUnauthorized (): Self = thenRespondWithCode(StatusCode .Unauthorized )
95
+ def thenRespondForbidden (): Self = thenRespondWithCode(StatusCode .Forbidden )
96
+
97
+ /** Respond with an empty body (for 1xx/2xx responses), or a body with an error message (for 4xx/5xx responses) and
98
+ * the given status code.
99
+ */
100
+ def thenRespondWithCode (code : StatusCode ): Self =
101
+ thenRespondAdjust(
102
+ if (code.isClientError || code.isServerError) StatusText .default(code).getOrElse(" " ) else " " ,
103
+ code
104
+ )
105
+
106
+ /** Adjust the given body, as specified in the request's response handling description. */
107
+ def thenRespondAdjust (body : Any ): Self = thenRespond(ResponseStub .adjust(body))
108
+
109
+ /** Adjust the given body, as specified in the request's response handling description. */
110
+ def thenRespondAdjust (body : Any , code : StatusCode ): Self = thenRespond(ResponseStub .adjust(body, code))
111
+
112
+ /** Respond with the given body, regardless of what's specified in the request's response handling description. */
113
+ def thenRespondExact (body : Any ): Self = thenRespond(ResponseStub .exact(body))
114
+
115
+ /** Respond with the given body, regardless of what's specified in the request's response handling description. */
116
+ def thenRespondExact (body : Any , code : StatusCode ): Self = thenRespond(ResponseStub .exact(body, code))
117
+
118
+ def thenThrow (e : Throwable ): Self = {
119
+ val m : PartialFunction [GenericRequest [_, _], F [Response [StubBody ]]] = {
120
+ case r if p(r) => monad.error(e)
92
121
}
93
122
withMatchers(matchers.orElse(m))
94
123
}
95
124
96
- def thenRespondCyclic [T ](bodies : T * ): Self =
97
- thenRespondCyclicResponses(bodies.map(body => ResponseStub [T ](body, StatusCode .Ok , " OK" )): _* )
125
+ /** Response with the given response (lazily evaluated). To create responses, use [[ResponseStub ]]. */
126
+ def thenRespond [T ](resp : => Response [StubBody ]): Self = {
127
+ val m : PartialFunction [GenericRequest [_, _], F [Response [StubBody ]]] = {
128
+ case r if p(r) => monad.eval(resp.copy(request = r.onlyMetadata))
129
+ }
130
+ withMatchers(matchers.orElse(m))
131
+ }
98
132
99
- def thenRespondCyclicResponses [T ](responses : Response [T ]* ): Self = {
133
+ /** Response with the given responses, in a loop. To create responses, use [[ResponseStub ]]. */
134
+ def thenRespondCyclic (responses : Response [StubBody ]* ): Self = {
100
135
val iterator = AtomicCyclicIterator .unsafeFrom(responses)
101
136
thenRespond(iterator.next())
102
137
}
103
138
104
- def thenRespondF (resp : => F [Response [_]]): Self = {
105
- val m : PartialFunction [GenericRequest [_, _], F [Response [_]]] = {
139
+ /** Response with the given response, given as an F-effect. To create responses, use [[ResponseStub ]]. */
140
+ def thenRespondF (resp : => F [Response [StubBody ]]): Self = {
141
+ val m : PartialFunction [GenericRequest [_, _], F [Response [StubBody ]]] = {
106
142
case r if p(r) => resp
107
143
}
108
144
withMatchers(matchers.orElse(m))
109
145
}
110
- def thenRespondF (resp : GenericRequest [_, _] => F [Response [_]]): Self = {
111
- val m : PartialFunction [GenericRequest [_, _], F [Response [_]]] = {
146
+
147
+ /** Response with the given response, given as an F-effect, created basing on the received request. To create
148
+ * responses, use [[ResponseStub ]].
149
+ */
150
+ def thenRespondF (resp : GenericRequest [_, _] => F [Response [StubBody ]]): Self = {
151
+ val m : PartialFunction [GenericRequest [_, _], F [Response [StubBody ]]] = {
112
152
case r if p(r) => resp(r)
113
153
}
114
154
withMatchers(matchers.orElse(m))
@@ -117,79 +157,81 @@ abstract class AbstractBackendStub[F[_], P](
117
157
}
118
158
119
159
object AbstractBackendStub {
120
-
121
- private [client4] def tryAdjustResponseType [DesiredRType , RType , F [_]](
122
- ra : ResponseAsDelegate [DesiredRType , _],
123
- m : Response [RType ]
124
- )(implicit monad : MonadError [F ]): F [Response [DesiredRType ]] =
125
- tryAdjustResponseBody(ra.delegate, m.body, m).getOrElse(monad.unit(m.body)).map { nb =>
126
- m.copy(body = nb.asInstanceOf [DesiredRType ])
127
- }
128
-
129
- private [client4] def tryAdjustResponseBody [F [_], T , U ](
160
+ private def adjustResponseBody [F [_], T , U ](
130
161
ra : GenericResponseAs [T , _],
131
162
b : U ,
132
163
meta : ResponseMetadata
133
- )(implicit monad : MonadError [F ]): Option [ F [T ] ] = {
164
+ )(implicit monad : MonadError [F ]): F [T ] = {
134
165
def bAsInputStream = b match {
135
- case s : String => Some (new ByteArrayInputStream (s.getBytes(Utf8 )))
136
- case a : Array [Byte ] => Some (new ByteArrayInputStream (a))
137
- case is : InputStream => Some (is)
138
- case () => Some (new ByteArrayInputStream (new Array [Byte ](0 )))
139
- case _ => None
166
+ case s : String => (new ByteArrayInputStream (s.getBytes(Utf8 ))).unit
167
+ case a : Array [Byte ] => (new ByteArrayInputStream (a)).unit
168
+ case is : InputStream => is.unit
169
+ case () => (new ByteArrayInputStream (new Array [Byte ](0 ))).unit
170
+ case _ =>
171
+ monad.error(throw new IllegalArgumentException (s " Provided body: $b, cannot be adjusted to an input stream " ))
140
172
}
141
173
142
174
ra match {
143
- case IgnoreResponse => Some (( ).unit.asInstanceOf [F [T ]])
175
+ case IgnoreResponse => ( ).unit.asInstanceOf [F [T ]]
144
176
case ResponseAsByteArray =>
145
177
b match {
146
- case s : String => Some (s.getBytes(Utf8 ).unit.asInstanceOf [F [T ]])
147
- case a : Array [Byte ] => Some (a.unit.asInstanceOf [F [T ]])
148
- case is : InputStream => Some (toByteArray(is).unit.asInstanceOf [F [T ]])
149
- case () => Some (Array [Byte ]().unit.asInstanceOf [F [T ]])
150
- case _ => None
151
- }
152
- case ResponseAsStream (_, f) =>
153
- b match {
154
- case RawStream (s) => Some (monad.suspend(f.asInstanceOf [(Any , ResponseMetadata ) => F [T ]](s, meta)))
155
- case _ => None
178
+ case s : String => s.getBytes(Utf8 ).unit.asInstanceOf [F [T ]]
179
+ case a : Array [Byte ] => a.unit.asInstanceOf [F [T ]]
180
+ case is : InputStream => toByteArray(is).unit.asInstanceOf [F [T ]]
181
+ case () => Array [Byte ]().unit.asInstanceOf [F [T ]]
182
+ case _ => monad.error(new IllegalArgumentException (s " Provided body: $b, cannot be adjusted to a byte array " ))
156
183
}
157
- case ResponseAsStreamUnsafe (_) =>
158
- b match {
159
- case RawStream (s) => Some (s.unit.asInstanceOf [F [T ]])
160
- case _ => None
161
- }
162
- case ResponseAsInputStream (f) => bAsInputStream.map(f).map(_.unit.asInstanceOf [F [T ]])
163
- case ResponseAsInputStreamUnsafe => bAsInputStream.map(_.unit.asInstanceOf [F [T ]])
184
+ case ResponseAsStream (_, f) => monad.suspend(f.asInstanceOf [(Any , ResponseMetadata ) => F [T ]](b, meta))
185
+ case ResponseAsStreamUnsafe (_) => b.unit.asInstanceOf [F [T ]]
186
+ case ResponseAsInputStream (f) => bAsInputStream.map(f).asInstanceOf [F [T ]]
187
+ case ResponseAsInputStreamUnsafe => bAsInputStream.asInstanceOf [F [T ]]
164
188
case ResponseAsFile (_) =>
165
189
b match {
166
- case f : SttpFile => Some ( f.unit.asInstanceOf [F [T ]])
167
- case _ => None
190
+ case f : SttpFile => f.unit.asInstanceOf [F [T ]]
191
+ case _ => monad.error( new IllegalArgumentException ( s " Provided body: $b , cannot be adjusted to a file " ))
168
192
}
169
193
case ResponseAsWebSocket (f) =>
170
194
b match {
171
195
case wss : WebSocketStub [_] =>
172
- Some ( f.asInstanceOf [(WebSocket [F ], ResponseMetadata ) => F [T ]](wss.build[F ](monad), meta) )
196
+ f.asInstanceOf [(WebSocket [F ], ResponseMetadata ) => F [T ]](wss.build[F ](monad), meta)
173
197
case ws : WebSocket [_] =>
174
- Some (f.asInstanceOf [(WebSocket [F ], ResponseMetadata ) => F [T ]](ws.asInstanceOf [WebSocket [F ]], meta))
175
- case _ => None
198
+ f.asInstanceOf [(WebSocket [F ], ResponseMetadata ) => F [T ]](ws.asInstanceOf [WebSocket [F ]], meta)
199
+ case _ =>
200
+ monad.error(
201
+ new IllegalArgumentException (
202
+ s " Provided body: $b is neither a WebSocket, nor a WebSocketStub instance "
203
+ )
204
+ )
176
205
}
177
206
case ResponseAsWebSocketUnsafe () =>
178
207
b match {
179
- case wss : WebSocketStub [_] => Some (wss.build[F ](monad).unit.asInstanceOf [F [T ]])
180
- case _ => None
208
+ case wss : WebSocketStub [_] => wss.build[F ](monad).unit.asInstanceOf [F [T ]]
209
+ case ws : WebSocket [_] => ws.asInstanceOf [WebSocket [F ]].unit.asInstanceOf [F [T ]]
210
+ case _ =>
211
+ monad.error(
212
+ new IllegalArgumentException (
213
+ s " Provided body: $b is neither a WebSocket, nor a WebSocketStub instance "
214
+ )
215
+ )
216
+ }
217
+ case ResponseAsWebSocketStream (_, pipe) =>
218
+ b match {
219
+ case WebSocketStreamConsumer (consume) => consume.asInstanceOf [Any => F [T ]].apply(pipe)
220
+ case _ => monad.error(new IllegalArgumentException (s " Provided body: $b is not a WebSocketStreamConsumer " ))
181
221
}
182
- case ResponseAsWebSocketStream (_, _) => None
183
222
case MappedResponseAs (raw, g, _) =>
184
- tryAdjustResponseBody (raw, b, meta).map(_. flatMap(result => monad.eval(g(result, meta) )))
185
- case rfm : ResponseAsFromMetadata [_, _] => tryAdjustResponseBody (rfm(meta), b, meta)
223
+ adjustResponseBody (raw, b, meta).flatMap(result => monad.eval(g(result, meta)))
224
+ case rfm : ResponseAsFromMetadata [_, _] => adjustResponseBody (rfm(meta), b, meta)
186
225
case ResponseAsBoth (l, r) =>
187
- tryAdjustResponseBody(l, b, meta).map { lAdjusted =>
188
- tryAdjustResponseBody(r, b, meta) match {
189
- case None => lAdjusted.map((_, None ))
190
- case Some (rAdjusted) => lAdjusted.flatMap(lResult => rAdjusted.map(rResult => (lResult, Some (rResult))))
226
+ adjustResponseBody(l, b, meta)
227
+ .flatMap { lAdjusted =>
228
+ adjustResponseBody(r, b, meta)
229
+ .map(rAdjusted => (lAdjusted, Option (rAdjusted)))
230
+ .handleError { case _ : IllegalArgumentException =>
231
+ monad.unit((lAdjusted, Option .empty))
232
+ }
191
233
}
192
- }
234
+ . asInstanceOf [ F [ T ]] // needed by Scala2
193
235
}
194
236
}
195
237
}
0 commit comments