-
Notifications
You must be signed in to change notification settings - Fork 5
/
feature_http_adapter.dart
167 lines (153 loc) · 5.43 KB
/
feature_http_adapter.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
// Copyright (c) 2020-2023 Navibyte (https://navibyte.com). All rights reserved.
// Use of this source code is governed by a “BSD-3-Clause”-style license that is
// specified in the LICENSE file.
//
// Docs: https://github.com/navibyte/geospatial
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import '/src/common/service/service_exception.dart';
import '/src/core/features/feature_failure.dart';
const _acceptJSON = {'accept': 'application/json'};
const _expectJSON = ['application/json'];
/// An adapter to fetch HTTP client, used by a feature service.
@internal
class FeatureHttpAdapter {
/// Create an adapter with an optional [client], [headers] and [extraParams].
const FeatureHttpAdapter({
http.Client? client,
Map<String, String>? headers,
Map<String, String>? extraParams,
}) : _client = client,
_baseHeaders = headers,
_extraParams = extraParams;
final http.Client? _client;
final Map<String, String>? _baseHeaders;
final Map<String, String>? _extraParams;
/// Makes `GET` request to [url] with optional [headers].
Future<http.Response> get(Uri url, {Map<String, String>? headers}) {
final httpUrl = _handleExtraParams(url);
final httpHeaders = _combineHeaders(headers);
//print('calling $httpUrl');
return _client != null
? _client!.get(httpUrl, headers: httpHeaders)
: http.get(httpUrl, headers: httpHeaders);
}
/// Makes `GET` request to [url] with optional [headers].
///
/// Returns an entity mapped from JSON Object using [toEntity].
Future<T> getEntityFromJsonObject<T>(
Uri url, {
required T Function(
Map<String, dynamic> data,
Map<String, String> responseHeaders,
) toEntity,
Map<String, String>? headers = _acceptJSON,
List<String>? expect = _expectJSON,
}) =>
getEntityFromJson(
url,
toEntity: (data, responseHeaders) =>
toEntity(data as Map<String, dynamic>, responseHeaders),
headers: headers,
expect: expect,
);
/// Makes `GET` request to [url] with optional [headers].
///
/// Returns an entity mapped from JSON element using [toEntity].
Future<T> getEntityFromJson<T>(
Uri url, {
required T Function(
dynamic data,
Map<String, String> responseHeaders,
) toEntity,
Map<String, String>? headers = _acceptJSON,
List<String>? expect = _expectJSON,
}) async {
try {
final response = await get(url, headers: headers);
switch (response.statusCode) {
case 200:
// optionally check that we got content type that was expected
// if expect list contains ..
// "application/json"
// .. then for example these types from header 'content-type' are ok
// "application/json"
// "application/json; charset=utf-8"
// not perfect checking anyways
if (expect != null) {
var ok = false;
final type = response.headers['content-type'];
if (type != null) {
for (final exp in expect) {
if (type.startsWith(exp)) {
ok = true;
break;
}
}
}
if (!ok) {
throw FormatException('Content type "$type" not expected.');
}
}
// decode JSON
final data = json.decode(response.body);
// map JSON data to an entity
return toEntity(data, response.headers);
case 302:
throw const ServiceException(FeatureFailure.found);
case 303:
throw const ServiceException(FeatureFailure.seeOther);
case 304:
throw const ServiceException(FeatureFailure.notModified);
case 307:
throw const ServiceException(FeatureFailure.temporaryRedirect);
case 308:
throw const ServiceException(FeatureFailure.permanentRedirect);
case 400:
throw const ServiceException(FeatureFailure.badRequest);
case 401:
throw const ServiceException(FeatureFailure.unauthorized);
case 403:
throw const ServiceException(FeatureFailure.forbidden);
case 404:
throw const ServiceException(FeatureFailure.notFound);
case 405:
throw const ServiceException(FeatureFailure.methodNotAllowed);
case 406:
throw const ServiceException(FeatureFailure.notAcceptable);
case 500:
throw const ServiceException(FeatureFailure.internalServerError);
default:
throw const ServiceException(FeatureFailure.queryFailed);
}
} on ServiceException<FeatureFailure> {
rethrow;
} catch (e, st) {
// other exceptions (including errors)
throw ServiceException(FeatureFailure.clientError, cause: e, trace: st);
}
}
Map<String, String>? _combineHeaders(Map<String, String>? headers) {
if (_baseHeaders != null) {
if (headers != null) {
return Map.from(_baseHeaders!)..addAll(headers);
} else {
return _baseHeaders;
}
} else {
return headers;
}
}
Uri _handleExtraParams(Uri url) {
if (_extraParams == null) {
return url;
} else {
final resultParams = Map.of(url.queryParameters);
for (final param in _extraParams!.entries) {
resultParams.putIfAbsent(param.key, () => param.value);
}
return url.replace(queryParameters: resultParams);
}
}
}