/
request.dart
314 lines (276 loc) · 9.65 KB
/
request.dart
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
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
import 'package:dio/dio.dart';
import 'package:http_mock_adapter/src/adapters/dio_adapter.dart';
import 'package:http_mock_adapter/src/exceptions.dart';
import 'package:http_mock_adapter/src/handlers/request_handler.dart';
import 'package:http_mock_adapter/src/interceptors/dio_interceptor.dart';
import 'package:http_mock_adapter/src/interfaces.dart';
import 'package:http_mock_adapter/src/matchers/matcher.dart';
import 'package:http_mock_adapter/src/matchers/matchers.dart';
import 'package:http_mock_adapter/src/types.dart';
import 'package:meta/meta.dart';
/// [Request] class contains members to hold network request information.
class Request {
/// This is the route specified by the client.
final String route;
/// An HTTP method such as [RequestMethods.GET] or [RequestMethods.POST].
final RequestMethods method;
/// The payload.
final dynamic data;
/// Query parameters to encompass additional parameters to the query.
final Map<String, dynamic> queryParameters;
/// Headers to encompass content-types.
final Map<String, dynamic> headers;
const Request({
this.route,
this.method = RequestMethods.GET,
this.data,
this.queryParameters = const {},
this.headers = const {
Headers.contentTypeHeader: Headers.jsonContentType,
Headers.contentLengthHeader: Matchers.integer,
},
});
/// [signature] is the [String] representation of the [Request]'s body.
String get signature =>
'$route/${method.value}/' +
sortedData(data) +
'/' +
sortedData(queryParameters) +
'/$headers';
}
/// [Signature] extension method adds [signature] getter to [RequestOptions]
/// in order to easily retrieve [Request]'s body representation as [String].
extension Signature on RequestOptions {
/// [signature] is the [String] representation of the [RequestOptions]'s body.
String get signature =>
'$path/$method/' +
sortedData(data) +
'/' +
sortedData(queryParameters) +
'/$headers';
}
/// [sortedData] sorts request [Signature]'s and [Request.signature]'s 'data'
/// and 'queryParameters' portion if it is a subtype of [Map].
/// This makes sure, that data passed during request and data saved inside
/// 'requestMap' while using [RequestRouted.onPost] or other [RequestRouted]
/// methods will be excatly same inside the [Signature] which is used to
/// compare executed request to the list of requests saved by [DioAdapter] or
/// by [DioInterceptor].
String sortedData(dynamic data) {
if (data is Map) {
final sortedKeys = data.keys.toList()..sort();
data = {for (final sortedKey in sortedKeys) sortedKey: data[sortedKey]};
}
return data.toString();
}
/// [MatchesRequest] enhances the [RequestOptions] by allowing different types
/// of matchers to validate the data and headers of the request.
extension MatchesRequest on RequestOptions {
/// Check values against matchers.
/// [request] is the configured [Request] which would contain the matchers if used.
bool matchesRequest(Request request) {
final matchesRequestBody =
data != null && request.data != null && !matches(data, request.data);
final matchesQueryParameters = queryParameters != null &&
request.queryParameters != null &&
!matches(queryParameters, request.queryParameters);
final matchesHeaders = headers != null &&
request.headers != null &&
!matches(headers, request.headers);
if (path != request.route ||
method != request.method.value ||
matchesRequestBody ||
matchesQueryParameters ||
matchesHeaders) {
return false;
}
return true;
}
/// Check the map keys and values determined by the definition.
bool matches(dynamic actual, dynamic expected) {
if (expected is Matcher) {
/// Check the match here to bypass the fallthrough strict equality check
/// at the end.
if (!expected.matches(actual)) {
return false;
}
} else if (actual is Map && expected is Map) {
for (final key in actual.keys.toList()) {
if (!expected.containsKey(key)) {
return false;
} else if (expected[key] is Matcher) {
// Check matcher for the configured request.
if (!expected[key].matches(actual[key])) {
return false;
}
} else if (expected[key] != actual[key]) {
// Exact match unless map.
if (expected[key] is Map && actual[key] is Map) {
if (!matches(actual[key], expected[key])) {
// Allow maps to use matchers.
return false;
}
} else if (expected[key].toString() != actual[key].toString()) {
// If some other kind of object like list then rely on `toString`
// to provide comparison value.
return false;
}
}
}
} else if (actual is List && expected is List) {
for (var index in Iterable.generate(actual.length)) {
if (!matches(actual[index], expected[index])) {
return false;
}
}
} else if (actual is Set && expected is Set) {
return !matches(actual.containsAll(expected), false);
} else if (actual != expected) {
// Fall back to original check.
return false;
}
return true;
}
}
/// Matcher of [Request] and [responseBody] based on route and [RequestHandler].
class RequestMatcher {
/// This is a request sent by the the client.
final Request request;
/// This is a request handler that processes requests.
final RequestHandler requestHandler;
/// This is an artificial response body to the request.
Responsable responseBody;
RequestMatcher(
this.request,
this.requestHandler, {
this.responseBody,
});
}
/// HTTP methods.
enum RequestMethods {
/// The [GET] method requests a representation of the specified resource.
/// Requests using [GET] should only retrieve data.
GET,
/// The [HEAD] method asks for a response identical to that of a [GET] request,
/// but without the response body.
HEAD,
/// The [POST] method is used to submit an entity to the specified resource,
/// often causing a change in state or side effects on the server.
POST,
/// The [PUT] method replaces all current representations of the
/// target resource with the request payload.
PUT,
/// The [DELETE] method deletes the specified resource.
DELETE,
/// The [PATCH] method is used to apply partial modifications to a resource.
PATCH,
}
/// [ValueToString] extension method grants [RequestMethods] enumeration
/// the ability to obtain [String] type depictions of enumeration's values.
extension ValueToString on RequestMethods {
/// Gets the [String] depiction of the current value.
String get value => toString().split('.').last;
}
/// [RequestRouted] exposes developer-friendly methods which take in route,
/// [Request], both of which ultimately get processed by [RequestHandler].
mixin RequestRouted {
/// Takes in route, request, and sets corresponding [RequestHandler].
@visibleForOverriding
RequestHandler onRoute(String route, {Request request = const Request()});
/// Takes in a route, requests with [RequestMethods.GET],
/// and sets corresponding [RequestHandler].
AdapterRequest get onGet => (
String route, {
dynamic data,
dynamic headers,
}) =>
onRoute(
route,
request: Request(
method: RequestMethods.GET,
data: data,
headers: headers,
),
);
/// Takes in a route, requests with [RequestMethods.HEAD],
/// and sets corresponding [RequestHandler].
AdapterRequest get onHead => (
String route, {
dynamic data,
dynamic headers,
}) =>
onRoute(
route,
request: Request(
method: RequestMethods.HEAD,
data: data,
headers: headers,
),
);
/// Takes in a route, requests with [RequestMethods.POST],
/// and sets corresponding [RequestHandler].
AdapterRequest get onPost => (
String route, {
dynamic data,
dynamic headers,
}) =>
onRoute(
route,
request: Request(
method: RequestMethods.POST,
data: data,
headers: headers,
),
);
/// Takes in a route, requests with [RequestMethods.PUT],
/// and sets corresponding [RequestHandler].
AdapterRequest get onPut => (
String route, {
dynamic data,
dynamic headers,
}) =>
onRoute(
route,
request: Request(
method: RequestMethods.PUT,
data: data,
headers: headers,
),
);
/// Takes in a route, requests with [RequestMethods.DELETE],
/// and sets corresponding [RequestHandler].
AdapterRequest get onDelete => (
String route, {
dynamic data,
dynamic headers,
}) =>
onRoute(
route,
request: Request(
method: RequestMethods.DELETE,
data: data,
headers: headers,
),
);
/// Takes in a route, requests with [RequestMethods.PATCH],
/// and sets corresponding [RequestHandler].
AdapterRequest get onPatch => (
String route, {
dynamic data,
dynamic headers,
}) =>
onRoute(
route,
request: Request(
method: RequestMethods.PATCH,
data: data,
headers: headers,
),
);
dynamic throwError(Responsable response) {
if (response.runtimeType == AdapterError) {
AdapterError error = response;
return throw error;
}
}
}