Skip to content

Commit

Permalink
Exclude null query vars by default and add new @method annotation inc…
Browse files Browse the repository at this point in the history
…ludeNullQueryVars (#372)
  • Loading branch information
techouse authored Oct 14, 2022
1 parent e3fd623 commit bd8d65f
Show file tree
Hide file tree
Showing 9 changed files with 548 additions and 17 deletions.
34 changes: 34 additions & 0 deletions chopper/lib/src/annotations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,39 @@ class Method {
/// hxxp://path/to/script?user[name]=john&user[surname]=doe&user[age]=21
final bool useBrackets;

/// Set to [true] to include query variables with null values. This includes nested maps.
/// The default is to exclude them.
///
/// NOTE: Empty strings are always included.
///
/// ```dart
/// @Get(
/// path: '/script',
/// includeNullQueryVars: true,
/// )
/// Future<Response<String>> getData({
/// @Query('foo') String? foo,
/// @Query('bar') String? bar,
/// @Query('baz') String? baz,
/// });
///
/// final response = await service.getData(
/// foo: 'foo_val',
/// bar: null, // omitting it would have the same effect
/// baz: 'baz_val',
/// );
/// ```
///
/// The above code produces hxxp://path/to/script&foo=foo_var&bar=&baz=baz_var
final bool includeNullQueryVars;

const Method(
this.method, {
this.optionalBody = false,
this.path = '',
this.headers = const {},
this.useBrackets = false,
this.includeNullQueryVars = false,
});
}

Expand All @@ -186,6 +213,7 @@ class Get extends Method {
super.path,
super.headers,
super.useBrackets,
super.includeNullQueryVars,
}) : super(HttpMethod.Get);
}

Expand All @@ -199,6 +227,7 @@ class Post extends Method {
super.path,
super.headers,
super.useBrackets,
super.includeNullQueryVars,
}) : super(HttpMethod.Post);
}

Expand All @@ -210,6 +239,7 @@ class Delete extends Method {
super.path,
super.headers,
super.useBrackets,
super.includeNullQueryVars,
}) : super(HttpMethod.Delete);
}

Expand All @@ -223,6 +253,7 @@ class Put extends Method {
super.path,
super.headers,
super.useBrackets,
super.includeNullQueryVars,
}) : super(HttpMethod.Put);
}

Expand All @@ -235,6 +266,7 @@ class Patch extends Method {
super.path,
super.headers,
super.useBrackets,
super.includeNullQueryVars,
}) : super(HttpMethod.Patch);
}

Expand All @@ -246,6 +278,7 @@ class Head extends Method {
super.path,
super.headers,
super.useBrackets,
super.includeNullQueryVars,
}) : super(HttpMethod.Head);
}

Expand All @@ -256,6 +289,7 @@ class Options extends Method {
super.path,
super.headers,
super.useBrackets,
super.includeNullQueryVars,
}) : super(HttpMethod.Options);
}

Expand Down
21 changes: 19 additions & 2 deletions chopper/lib/src/request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Request extends http.BaseRequest {
final bool multipart;
final List<PartValue> parts;
final bool useBrackets;
final bool includeNullQueryVars;

Request(
String method,
Expand All @@ -25,9 +26,16 @@ class Request extends http.BaseRequest {
this.multipart = false,
this.parts = const [],
this.useBrackets = false,
this.includeNullQueryVars = false,
}) : super(
method,
buildUri(origin, path, parameters, useBrackets: useBrackets),
buildUri(
origin,
path,
parameters,
useBrackets: useBrackets,
includeNullQueryVars: includeNullQueryVars,
),
) {
this.headers.addAll(headers);
}
Expand All @@ -44,6 +52,7 @@ class Request extends http.BaseRequest {
this.multipart = false,
this.parts = const [],
this.useBrackets = false,
this.includeNullQueryVars = false,
}) : origin = url.origin,
path = url.path,
parameters = {...url.queryParametersAll, ...?parameters},
Expand All @@ -54,6 +63,7 @@ class Request extends http.BaseRequest {
url.path,
{...url.queryParametersAll, ...?parameters},
useBrackets: useBrackets,
includeNullQueryVars: includeNullQueryVars,
),
) {
this.headers.addAll(headers);
Expand All @@ -70,6 +80,7 @@ class Request extends http.BaseRequest {
bool? multipart,
List<PartValue>? parts,
bool? useBrackets,
bool? includeNullQueryVars,
}) =>
Request(
method ?? this.method,
Expand All @@ -81,6 +92,7 @@ class Request extends http.BaseRequest {
multipart: multipart ?? this.multipart,
parts: parts ?? this.parts,
useBrackets: useBrackets ?? this.useBrackets,
includeNullQueryVars: includeNullQueryVars ?? this.includeNullQueryVars,
);

/// Builds a valid URI from [baseUrl], [url] and [parameters].
Expand All @@ -92,14 +104,19 @@ class Request extends http.BaseRequest {
String url,
Map<String, dynamic> parameters, {
bool useBrackets = false,
bool includeNullQueryVars = false,
}) {
// If the request's url is already a fully qualified URL, we can use it
// as-is and ignore the baseUrl.
final Uri uri = url.startsWith('http://') || url.startsWith('https://')
? Uri.parse(url)
: Uri.parse('${baseUrl.strip('/')}/${url.leftStrip('/')}');

final String query = mapToQuery(parameters, useBrackets: useBrackets);
final String query = mapToQuery(
parameters,
useBrackets: useBrackets,
includeNullQueryVars: includeNullQueryVars,
);

return query.isNotEmpty
? uri.replace(query: uri.hasQuery ? '${uri.query}&$query' : query)
Expand Down
24 changes: 20 additions & 4 deletions chopper/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,22 @@ final chopperLogger = Logger('Chopper');
/// Creates a valid URI query string from [map].
///
/// E.g., `{'foo': 'bar', 'ints': [ 1337, 42 ] }` will become 'foo=bar&ints=1337&ints=42'.
String mapToQuery(Map<String, dynamic> map, {bool useBrackets = false}) =>
_mapToQuery(map, useBrackets: useBrackets).join('&');
String mapToQuery(
Map<String, dynamic> map, {
bool useBrackets = false,
bool includeNullQueryVars = false,
}) =>
_mapToQuery(
map,
useBrackets: useBrackets,
includeNullQueryVars: includeNullQueryVars,
).join('&');

Iterable<_Pair<String, String>> _mapToQuery(
Map<String, dynamic> map, {
String? prefix,
bool useBrackets = false,
bool includeNullQueryVars = false,
}) {
final Set<_Pair<String, String>> pairs = {};

Expand All @@ -80,15 +89,22 @@ Iterable<_Pair<String, String>> _mapToQuery(
pairs.addAll(_iterableToQuery(name, value, useBrackets: useBrackets));
} else if (value is Map<String, dynamic>) {
pairs.addAll(
_mapToQuery(value, prefix: name, useBrackets: useBrackets),
_mapToQuery(
value,
prefix: name,
useBrackets: useBrackets,
includeNullQueryVars: includeNullQueryVars,
),
);
} else {
pairs.add(
_Pair<String, String>(name, _normalizeValue(value)),
);
}
} else {
pairs.add(_Pair<String, String>(name, ''));
if (includeNullQueryVars) {
pairs.add(_Pair<String, String>(name, ''));
}
}
});

Expand Down
116 changes: 114 additions & 2 deletions chopper/test/base_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ void main() {
final httpClient = MockClient((request) async {
expect(
request.url.toString(),
equals('$baseUrl/test/query?name=&int=&default_value='),
equals('$baseUrl/test/query?name='),
);
expect(request.method, equals('GET'));

Expand All @@ -132,7 +132,7 @@ void main() {
final httpClient = MockClient((request) async {
expect(
request.url.toString(),
equals('$baseUrl/test/query?name=&int=&default_value=42'),
equals('$baseUrl/test/query?name=&default_value=42'),
);
expect(request.method, equals('GET'));

Expand Down Expand Up @@ -923,6 +923,34 @@ void main() {
);
});

test('Include null query vars', () async {
final httpClient = MockClient((request) async {
expect(
request.url.toString(),
equals('$baseUrl/test/query_param_include_null_query_vars'
'?foo=foo_val'
'&bar='
'&baz=baz_val'),
);
expect(request.method, equals('GET'));

return http.Response('get response', 200);
});

final chopper = buildClient(httpClient);
final service = chopper.getService<HttpTestService>();

final response = await service.getUsingQueryParamIncludeNullQueryVars(
foo: 'foo_val',
baz: 'baz_val',
);

expect(response.body, equals('get response'));
expect(response.statusCode, equals(200));

httpClient.close();
});

test('List query param', () async {
final httpClient = MockClient((request) async {
expect(
Expand Down Expand Up @@ -1067,4 +1095,88 @@ void main() {

httpClient.close();
});

test('Map query param without including null query vars', () async {
final httpClient = MockClient((request) async {
expect(
request.url.toString(),
equals('$baseUrl/test/map_query_param'
'?value.bar=baz'
'&value.etc.abc=def'
'&value.etc.mno.opq=rst'
'&value.etc.mno.list=a'
'&value.etc.mno.list=123'
'&value.etc.mno.list=false'),
);
expect(request.method, equals('GET'));

return http.Response('get response', 200);
});

final chopper = buildClient(httpClient);
final service = chopper.getService<HttpTestService>();

final response = await service.getUsingMapQueryParam(<String, dynamic>{
'bar': 'baz',
'zap': null,
'etc': <String, dynamic>{
'abc': 'def',
'ghi': null,
'mno': <String, dynamic>{
'opq': 'rst',
'uvw': null,
'list': ['a', 123, false],
},
},
});

expect(response.body, equals('get response'));
expect(response.statusCode, equals(200));

httpClient.close();
});

test('Map query param including null query vars', () async {
final httpClient = MockClient((request) async {
expect(
request.url.toString(),
equals('$baseUrl/test/map_query_param_include_null_query_vars'
'?value.bar=baz'
'&value.zap='
'&value.etc.abc=def'
'&value.etc.ghi='
'&value.etc.mno.opq=rst'
'&value.etc.mno.uvw='
'&value.etc.mno.list=a'
'&value.etc.mno.list=123'
'&value.etc.mno.list=false'),
);
expect(request.method, equals('GET'));

return http.Response('get response', 200);
});

final chopper = buildClient(httpClient);
final service = chopper.getService<HttpTestService>();

final response = await service
.getUsingMapQueryParamIncludeNullQueryVars(<String, dynamic>{
'bar': 'baz',
'zap': null,
'etc': <String, dynamic>{
'abc': 'def',
'ghi': null,
'mno': <String, dynamic>{
'opq': 'rst',
'uvw': null,
'list': ['a', 123, false],
},
},
});

expect(response.body, equals('get response'));
expect(response.statusCode, equals(200));

httpClient.close();
});
}
Loading

0 comments on commit bd8d65f

Please sign in to comment.