Skip to content

Commit d0827c3

Browse files
committed
refactor converters
1 parent ab9b5ea commit d0827c3

File tree

7 files changed

+230
-120
lines changed

7 files changed

+230
-120
lines changed

lib/src/api/aha_client.dart

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import 'package:aha_client/src/api/login/login_manager.dart';
21
import 'package:chopper/chopper.dart';
32
import 'package:http/http.dart';
43

54
import 'aha/aha_service.dart';
5+
import 'aha/models/switch_status.dart';
6+
import 'login/login_manager.dart';
67
import 'login/login_service.dart';
78
import 'login/models/session_info.dart';
8-
import 'util/xml_typed_converter.dart';
9+
import 'util/combined_converter.dart';
10+
import 'util/text_converter.dart';
11+
import 'util/xml_converter.dart';
912

1013
class AhaClient {
1114
static const defaultHostName = 'fritz.box';
@@ -26,8 +29,10 @@ class AhaClient {
2629
host: hostName,
2730
port: port,
2831
).toString(),
29-
converter: XmlTypedConverter()
30-
..registerConverter<SessionInfo>(SessionInfo.converter),
32+
converter: CombinedConverter([
33+
XmlConverter()..registerConverter(SessionInfo.converter),
34+
TextConverter()..registerConverter(SwitchStatus.converter),
35+
]),
3136
interceptors: [
3237
loginManager,
3338
],

lib/src/api/login/models/session_info.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'package:freezed_annotation/freezed_annotation.dart';
22
import 'package:xml/xml.dart';
33

4-
import '../../util/xml_typed_converter.dart';
4+
import '../../util/xml_converter.dart';
55
import 'right.dart';
66
import 'user.dart';
77

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import 'dart:async';
2+
3+
import 'package:chopper/chopper.dart';
4+
import 'package:meta/meta.dart';
5+
6+
class UnsupportedDataException implements Exception {
7+
final String _message;
8+
final Type _type;
9+
final Map<String, String> _reasons;
10+
11+
UnsupportedDataException._(this._message, this._type, this._reasons);
12+
13+
@override
14+
String toString() =>
15+
'$_message. No converter could convert $_type. Reasons:\n'
16+
'${_reasons.entries.map((e) => '${e.key}: ${e.value}').join('\n')}';
17+
}
18+
19+
@internal
20+
class CombinedConverter implements Converter {
21+
final List<Converter> _subConverters;
22+
23+
CombinedConverter([Iterable<Converter>? converters])
24+
: _subConverters = converters?.toList() ?? [];
25+
26+
void addConverter(Converter converter) {
27+
_subConverters.add(converter);
28+
}
29+
30+
@override
31+
FutureOr<Request> convertRequest(Request request) {
32+
final failures = <_ErrorInfo>[];
33+
for (final converter in _subConverters) {
34+
try {
35+
return converter.convertRequest(request);
36+
} on ConversionNotSupportedException catch (e) {
37+
failures.add(_ErrorInfo(converter.runtimeType, e));
38+
}
39+
}
40+
41+
throw failures.toRequestException(request);
42+
}
43+
44+
@override
45+
FutureOr<Response<BodyType>> convertResponse<BodyType, InnerType>(
46+
Response response,
47+
) {
48+
final failures = <_ErrorInfo>[];
49+
for (final converter in _subConverters) {
50+
try {
51+
return converter.convertResponse<BodyType, InnerType>(response);
52+
} on ConversionNotSupportedException catch (e) {
53+
failures.add(_ErrorInfo(converter.runtimeType, e));
54+
}
55+
}
56+
57+
throw failures.toResponseException<BodyType>(response);
58+
}
59+
}
60+
61+
@internal
62+
class ConversionNotSupportedException implements Exception {
63+
final String converterType;
64+
final String reason;
65+
66+
ConversionNotSupportedException(this.converterType, this.reason);
67+
}
68+
69+
class _ErrorInfo {
70+
final Type converter;
71+
final ConversionNotSupportedException exception;
72+
73+
const _ErrorInfo(this.converter, this.exception);
74+
}
75+
76+
extension _ErrorInfoListX on Iterable<_ErrorInfo> {
77+
UnsupportedDataException toRequestException(Request request) =>
78+
UnsupportedDataException._(
79+
'Failed to convert request to '
80+
'${buildUri(request.baseUrl, request.url, request.parameters)}',
81+
request.body.runtimeType,
82+
_asMap(),
83+
);
84+
85+
UnsupportedDataException toResponseException<TBody>(Response response) =>
86+
UnsupportedDataException._(
87+
'Failed to convert response from ${response.base.request?.url}',
88+
TBody,
89+
_asMap(),
90+
);
91+
92+
Map<String, String> _asMap() => Map.fromEntries(
93+
map((i) => MapEntry(i.converter.toString(), i.exception.toString())),
94+
);
95+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
mixin ContentTypeExtractor {
2+
static const contentTypeHeader = 'Content-Type';
3+
4+
String? getContentType(Map<String, String> headers) =>
5+
headers[contentTypeHeader.toLowerCase()]?.split(';').first;
6+
}

lib/src/api/util/text_converter.dart

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import 'dart:async';
22

3+
import 'package:aha_client/src/api/util/content_type_extractor.dart';
34
import 'package:chopper/chopper.dart';
45

56
import '../aha/models/optional.dart';
7+
import 'combined_converter.dart';
68

79
abstract class TextTypeConverter<T> {
810
String encode(T data);
911
T decode(String data);
1012
}
1113

12-
class TextConverter implements Converter {
14+
class TextConverter with ContentTypeExtractor implements Converter {
15+
static const _textContentType = 'text/plain';
16+
1317
final _typeConverters = <Type, TextTypeConverter>{};
1418

19+
void registerConverter<T>(TextTypeConverter<T> converter) =>
20+
_typeConverters[T] = converter;
21+
1522
@override
1623
FutureOr<Request> convertRequest(Request request) {
1724
return request.copyWith(
@@ -23,6 +30,7 @@ class TextConverter implements Converter {
2330
FutureOr<Response<BodyType>> convertResponse<BodyType, InnerType>(
2431
Response response,
2532
) {
33+
_checkContentType(response);
2634
return response.copyWith(
2735
body: _decode<BodyType, InnerType>(response.bodyString),
2836
);
@@ -37,8 +45,7 @@ class TextConverter implements Converter {
3745
}
3846
}
3947

40-
final converter = _typeConverters[data.runtimeType];
41-
return converter?.encode(data) ?? data.toString();
48+
return _findConverter(data.runtimeType).encode(data);
4249
}
4350

4451
TBody _decode<TBody, TInner>(String data) {
@@ -50,7 +57,28 @@ class TextConverter implements Converter {
5057
}
5158
}
5259

53-
final converter = _typeConverters[TBody] as TextTypeConverter<TBody>?;
54-
return converter?.decode(data) ?? data as TBody;
60+
return _findConverter<TBody>().decode(data);
61+
}
62+
63+
void _checkContentType(Response response) {
64+
final contentType = getContentType(response.headers);
65+
if (contentType != _textContentType) {
66+
throw ConversionNotSupportedException(
67+
runtimeType.toString(),
68+
'Content-Type "$contentType" is not supported',
69+
);
70+
}
71+
}
72+
73+
TextTypeConverter<T> _findConverter<T>([Type? t]) {
74+
final converter = _typeConverters[t ?? T] as TextTypeConverter<T>?;
75+
if (converter == null) {
76+
throw ConversionNotSupportedException(
77+
runtimeType.toString(),
78+
'Type has not been registered for text conversion',
79+
);
80+
}
81+
82+
return converter;
5583
}
5684
}
Lines changed: 86 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,103 @@
11
import 'dart:async';
2-
import 'dart:convert';
32

3+
import 'package:aha_client/src/api/util/content_type_extractor.dart';
44
import 'package:chopper/chopper.dart';
5+
import 'package:meta/meta.dart';
56
import 'package:xml/xml.dart';
67

7-
class XmlConverter implements Converter {
8-
static const contentTypeHeader = 'Content-Type';
9-
static const xmlContentType = 'text/xml';
10-
static const xmlAltContentType = 'application/xml';
8+
import 'combined_converter.dart';
9+
10+
abstract class XmlTypeConverter<T> {
11+
const XmlTypeConverter._();
12+
13+
XmlDocument toXml(T data);
14+
T fromXml(XmlDocument xml);
15+
}
16+
17+
abstract class SimpleXmlTypeConverter<T> implements XmlTypeConverter<T> {
18+
const SimpleXmlTypeConverter();
19+
20+
@override
21+
T fromXml(XmlDocument xml) => parseXml(xml.rootElement);
22+
23+
@override
24+
XmlDocument toXml(T data) {
25+
final builder = XmlBuilder()..processing('xml', 'version="1.0"');
26+
buildXml(data, builder);
27+
return builder.buildDocument();
28+
}
29+
30+
@protected
31+
void buildXml(T data, XmlBuilder builder);
32+
33+
@protected
34+
T parseXml(XmlElement element);
35+
}
36+
37+
class XmlConverter with ContentTypeExtractor implements Converter {
38+
static const _xmlContentType = 'text/xml';
39+
static const _allowedXmlContentTypes = [_xmlContentType, 'application/xml'];
40+
41+
final _typeConverters = <Type, XmlTypeConverter>{
42+
XmlDocument: const _XmlDocumentConverter(),
43+
};
44+
45+
void registerConverter<T>(XmlTypeConverter<T> converter) =>
46+
_typeConverters[T] = converter;
1147

1248
@override
1349
FutureOr<Request> convertRequest(Request request) {
14-
final rawBody = request.body;
15-
if (rawBody is XmlDocument) {
16-
return request.copyWith(
17-
body: rawBody.toXmlString(),
18-
headers: {
19-
...request.headers,
20-
contentTypeHeader: xmlContentType,
21-
},
22-
);
23-
} else {
24-
return request;
25-
}
50+
final converter = _findConverter(request.body.runtimeType);
51+
return request.copyWith(
52+
body: converter.toXml(request.body).toXmlString(),
53+
headers: {
54+
...request.headers,
55+
ContentTypeExtractor.contentTypeHeader: _xmlContentType,
56+
},
57+
);
2658
}
2759

2860
@override
2961
FutureOr<Response<BodyType>> convertResponse<BodyType, InnerType>(
3062
Response response,
3163
) {
32-
if (BodyType == XmlDocument) {
33-
final contentType =
34-
response.headers[contentTypeHeader.toLowerCase()]?.split(';').first;
35-
if (contentType != xmlContentType && contentType != xmlAltContentType) {
36-
throw Exception(
37-
'Cannot decode a response without the XML content type',
38-
);
39-
}
40-
41-
return response.copyWith(
42-
body: XmlDocument.parse(response.bodyString),
43-
) as Response<BodyType>;
44-
} else {
45-
return response.copyWith();
64+
_checkContentType(response);
65+
66+
final converter = _findConverter<BodyType>();
67+
return response.copyWith(
68+
body: converter.fromXml(XmlDocument.parse(response.bodyString)),
69+
);
70+
}
71+
72+
void _checkContentType(Response response) {
73+
final contentType = getContentType(response.headers);
74+
if (!_allowedXmlContentTypes.contains(contentType)) {
75+
throw ConversionNotSupportedException(
76+
runtimeType.toString(),
77+
'Content-Type "$contentType" is not supported',
78+
);
79+
}
80+
}
81+
82+
XmlTypeConverter<T> _findConverter<T>([Type? t]) {
83+
final converter = _typeConverters[t ?? T] as XmlTypeConverter<T>?;
84+
if (converter == null) {
85+
throw ConversionNotSupportedException(
86+
runtimeType.toString(),
87+
'Type has not been registered for XML conversion',
88+
);
4689
}
90+
91+
return converter;
4792
}
4893
}
94+
95+
class _XmlDocumentConverter implements XmlTypeConverter<XmlDocument> {
96+
const _XmlDocumentConverter();
97+
98+
@override
99+
XmlDocument fromXml(XmlDocument xml) => xml;
100+
101+
@override
102+
XmlDocument toXml(XmlDocument data) => data;
103+
}

0 commit comments

Comments
 (0)